Merge pull request #3330 from hehuaiyu/fix/vscode-custom-provider-protocol-parity

fix(vscode): support custom provider protocols
This commit is contained in:
Bohdan Triapitsyn
2026-09-05 01:36:12 +03:00
committed by GitHub
3 changed files with 56 additions and 4 deletions
+1 -1
View File
@@ -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`
@@ -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, {
+11 -3
View File
@@ -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,