diff --git a/packages/vscode/src/DOCUMENTATION.md b/packages/vscode/src/DOCUMENTATION.md index 5b758856..7aefa3ab 100644 --- a/packages/vscode/src/DOCUMENTATION.md +++ b/packages/vscode/src/DOCUMENTATION.md @@ -72,7 +72,7 @@ The webview build emits each worker as one self-contained file. VS Code webviews - Includes session activity snapshot bridge handler used by webview parity routes (`/api/session-activity`). - Includes Zen utility model parity handler used by shared notification settings (`/api/zen/models`). - Owns managed OpenCode upgrade status and mutation handlers, including capability reporting, upgrade serialization, and process restart after a successful upgrade. - - Provider handlers cover source lookup, disconnect (`DELETE /api/provider/:id/auth`), and custom provider upsert (`PUT /api/provider`; create/update OpenAI-compatible config with explicit `scope` for user/project/custom layers; requires `env` or stored auth; secrets via OpenCode auth API). Updates preserve existing provider, option, and retained-model fields that the form does not manage while honoring explicit model, header, and env removal. Legacy `providers` entries migrate to the canonical `provider` key when edited. + - Provider handlers cover source lookup, disconnect (`DELETE /api/provider/:id/auth`), and custom provider upsert (`PUT /api/provider`; create/update OpenAI Chat Completions, OpenAI Responses, or Anthropic Messages config with explicit `scope` for user/project/custom layers; requires `env` or stored auth; secrets via OpenCode auth API). Updates preserve existing provider, option, and retained-model fields that the form does not manage while honoring explicit model, header, and env removal. Legacy `providers` entries migrate to the canonical `provider` key when edited. - Quota handlers keep managed exe.dev, Ollama Cloud, and Cursor credentials in the extension data directory with the same private-file contract as the web runtime. exe.dev uses one command-scoped usage token for the aggregate billing shared by every `exe-*` model provider. - `opencode-upgrade-runtime.ts` diff --git a/packages/vscode/src/opencodeConfig.providers.test.ts b/packages/vscode/src/opencodeConfig.providers.test.ts index 1da73ede..8a4ac246 100644 --- a/packages/vscode/src/opencodeConfig.providers.test.ts +++ b/packages/vscode/src/opencodeConfig.providers.test.ts @@ -72,6 +72,33 @@ describe('custom provider config persistence (VS Code parity)', () => { }).ok, true); }); + test('accepts supported protocol adapters and rejects arbitrary npm packages', () => { + for (const npm of ['@ai-sdk/openai-compatible', '@ai-sdk/openai', '@ai-sdk/anthropic']) { + const result = validateCustomProviderConfig('ok', { + name: 'X', + npm, + env: ['MY_KEY'], + options: { baseURL: 'https://api.example.com/v1' }, + models: { m: { name: 'M' } }, + }); + assert.equal(result.ok, true); + if (result.ok) assert.equal(result.value.config.npm, npm); + } + + const unsupported = validateCustomProviderConfig('ok', { + name: 'X', + npm: 'untrusted-provider-package', + env: ['MY_KEY'], + options: { baseURL: 'https://api.example.com/v1' }, + models: { m: { name: 'M' } }, + }); + assert.equal(unsupported.ok, false); + assert.equal( + unsupported.error, + 'Custom providers must use @ai-sdk/openai-compatible, @ai-sdk/openai, or @ai-sdk/anthropic', + ); + }); + test('upsertProviderConfig writes and round-trips project config', () => { const result = upsertProviderConfig('campus-llm', { name: 'Campus LLM', @@ -109,6 +136,23 @@ describe('custom provider config persistence (VS Code parity)', () => { assert.equal(sources.project.path, result.path); }); + test('round-trips non-default protocol adapters through project config', () => { + for (const [providerId, npm] of [ + ['responses-provider', '@ai-sdk/openai'], + ['anthropic-provider', '@ai-sdk/anthropic'], + ]) { + const result = upsertProviderConfig(providerId, { + name: providerId, + npm, + env: ['PROVIDER_KEY'], + options: { baseURL: 'https://api.example.com/v1' }, + models: { model: { name: 'Model' } }, + }, projectDir, 'project'); + assert.equal(result.config.npm, npm); + assert.equal(readJson(result.path).provider[providerId].npm, npm); + } + }); + test('upsertProviderConfig updates existing entry and clears disabled_providers', () => { const configPath = path.join(projectDir, 'opencode.json'); writeJson(configPath, { diff --git a/packages/vscode/src/opencodeConfig.ts b/packages/vscode/src/opencodeConfig.ts index 341dc87f..629aecaa 100644 --- a/packages/vscode/src/opencodeConfig.ts +++ b/packages/vscode/src/opencodeConfig.ts @@ -2266,6 +2266,11 @@ export const removeProviderConfig = (providerId: string, workingDirectory?: stri const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-_]*$/; const BASE_URL_PATTERN = /^https?:\/\//; const OPENAI_COMPATIBLE_NPM = '@ai-sdk/openai-compatible'; +const CUSTOM_PROVIDER_NPM_PACKAGES = new Set([ + OPENAI_COMPATIBLE_NPM, + '@ai-sdk/openai', + '@ai-sdk/anthropic', +]); type JsonValue = string | number | boolean | null | JsonObject | JsonValue[]; type JsonObject = { [key: string]: JsonValue }; @@ -2301,8 +2306,11 @@ export const validateCustomProviderConfig = ( } const npm = typeof config.npm === 'string' ? config.npm.trim() : OPENAI_COMPATIBLE_NPM; - if (npm !== OPENAI_COMPATIBLE_NPM) { - return { ok: false as const, error: `Custom providers must use npm package ${OPENAI_COMPATIBLE_NPM}` }; + if (!CUSTOM_PROVIDER_NPM_PACKAGES.has(npm)) { + return { + ok: false as const, + error: 'Custom providers must use @ai-sdk/openai-compatible, @ai-sdk/openai, or @ai-sdk/anthropic', + }; } const optionsBlock = isPlainObject(config.options) ? config.options : null; @@ -2340,7 +2348,7 @@ export const validateCustomProviderConfig = ( } const normalized: NormalizedCustomProviderConfig = { - npm: OPENAI_COMPATIBLE_NPM, + npm, name, options: { baseURL,