Merge pull request #3330 from hehuaiyu/fix/vscode-custom-provider-protocol-parity
fix(vscode): support custom provider protocols
This commit is contained in:
@@ -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, {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user