fix(cli): support OpenCode V2 config format in setup-opencode (#11070) (#11079)

Cherry-picked the value commit (55da60f5) onto the current tip, dropping the stale base-red sync commits. Focused tests: opencode-v2-config-11070 + merge-provider-guard + config-dir-single-source 12/12; mutation coverage gate no-drift (new test registered in tap.testFiles by the PR itself). Fixes #11070 — setup-opencode now emits both V1 and V2 blocks. Thank you @rqzbeh!
This commit is contained in:
Rouzbeh†
2026-08-22 22:31:25 +03:30
committed by GitHub
parent 3157e8a7ad
commit 9a67185297
3 changed files with 76 additions and 7 deletions

View File

@@ -84,11 +84,29 @@ export const buildOpenCodeProviderConfig = ({
};
};
export const buildOpenCodeV2ProviderConfig = (
input: OpenCodeConfigInput
): Record<string, any> => {
const v1Config = buildOpenCodeProviderConfig(input);
return {
name: v1Config.name,
package: "@opencode-ai/ai/providers/openai-compatible",
settings: {
baseURL: v1Config.options.baseURL,
apiKey: v1Config.options.apiKey,
},
models: v1Config.models,
};
};
export const buildOpenCodeConfigDocument = (input: OpenCodeConfigInput) => ({
$schema: "https://opencode.ai/config.json",
provider: {
omniroute: buildOpenCodeProviderConfig(input),
},
providers: {
omniroute: buildOpenCodeV2ProviderConfig(input),
},
});
export const mergeOpenCodeConfig = (
@@ -100,18 +118,18 @@ export const mergeOpenCodeConfig = (
? existingConfig
: {};
// Same guard as the root above, one level down. Spreading a non-object here
// does not throw, it splays the value into index keys: an existing
// `"provider": ["a", "b"]` merged to `{"0": "a", "1": "b", omniroute: ... }`
// and a string was exploded one character per key. mergeOpenCodeConfigText
// refuses the same input outright, so the two disagreed on what to do with a
// malformed config.
const existingProvider = (safeConfig as Record<string, unknown>).provider;
const safeProvider =
existingProvider && typeof existingProvider === "object" && !Array.isArray(existingProvider)
? (existingProvider as Record<string, unknown>)
: {};
const existingProviders = (safeConfig as Record<string, unknown>).providers;
const safeProviders =
existingProviders && typeof existingProviders === "object" && !Array.isArray(existingProviders)
? (existingProviders as Record<string, unknown>)
: {};
return {
...safeConfig,
$schema: safeConfig.$schema || "https://opencode.ai/config.json",
@@ -119,6 +137,10 @@ export const mergeOpenCodeConfig = (
...safeProvider,
omniroute: buildOpenCodeProviderConfig(input),
},
providers: {
...safeProviders,
omniroute: buildOpenCodeV2ProviderConfig(input),
},
};
};
@@ -127,6 +149,7 @@ export const mergeOpenCodeConfigText = (
input: OpenCodeConfigInput
) => {
const providerConfig = buildOpenCodeProviderConfig(input);
const v2ProviderConfig = buildOpenCodeV2ProviderConfig(input);
const content = typeof existingText === "string" ? existingText : "";
const trimmedContent = content.trim();
@@ -161,6 +184,11 @@ export const mergeOpenCodeConfigText = (
const providerEdits = modify(nextText, ["provider", "omniroute"], providerConfig, {
formattingOptions: { insertSpaces: true, tabSize: 2 },
});
nextText = applyEdits(nextText, providerEdits);
return applyEdits(nextText, providerEdits);
const v2ProviderEdits = modify(nextText, ["providers", "omniroute"], v2ProviderConfig, {
formattingOptions: { insertSpaces: true, tabSize: 2 },
});
return applyEdits(nextText, v2ProviderEdits);
};

View File

@@ -283,6 +283,7 @@
"tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts",
"tests/unit/openapi-security-tiers.test.ts",
"tests/unit/opencode-autocombo-search-pair.test.ts",
"tests/unit/opencode-v2-config-11070.test.ts",
"tests/unit/openrouter-free-model-credits-exhausted.test.ts",
"tests/unit/openrouter-passthrough-models.test.ts",
"tests/unit/openrouter-quota-6842.test.ts",

View File

@@ -0,0 +1,40 @@
import test from "node:test";
import assert from "node:assert/strict";
const opencodeConfig = await import("../../src/shared/services/opencodeConfig.ts");
test("buildOpenCodeConfigDocument includes both V1 (provider) and V2 (providers) definitions", () => {
const doc = opencodeConfig.buildOpenCodeConfigDocument({
baseUrl: "http://localhost:20128/v1",
apiKey: "{env:OMNIROUTE_API_KEY}",
models: ["auto/best-coding"],
});
assert.ok(doc.provider?.omniroute, "V1 provider.omniroute must be present");
assert.equal(doc.provider.omniroute.npm, "@ai-sdk/openai-compatible");
assert.equal(doc.provider.omniroute.options.baseURL, "http://localhost:20128/v1");
assert.ok(doc.providers?.omniroute, "V2 providers.omniroute must be present");
assert.equal(doc.providers.omniroute.package, "@opencode-ai/ai/providers/openai-compatible");
assert.equal(doc.providers.omniroute.settings.baseURL, "http://localhost:20128/v1");
assert.equal(doc.providers.omniroute.settings.apiKey, "{env:OMNIROUTE_API_KEY}");
assert.ok(doc.providers.omniroute.models["auto/best-coding"].limit, "V2 model limit must be present");
});
test("mergeOpenCodeConfig preserves existing properties and updates both provider and providers", () => {
const existing = {
$schema: "https://opencode.ai/config.json",
customField: "keep-me",
};
const merged = opencodeConfig.mergeOpenCodeConfig(existing, {
baseUrl: "http://localhost:20128/v1",
apiKey: "sk_test_key",
models: ["auto/best-coding"],
});
assert.equal(merged.customField, "keep-me");
assert.ok(merged.provider?.omniroute);
assert.ok(merged.providers?.omniroute);
assert.equal(merged.providers.omniroute.settings.apiKey, "sk_test_key");
});