Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,7 @@ Qwen3.7 Plus/3.8 Max/3.8 Flash use Anthropic Messages. Legacy MiniMax M2.5 and
Union Alpha also retain Messages. Models without a listed wire retain Chat.
New native pins apply to the pinned Go destination with key authentication.
Responses models still accept an explicit `modelAdapters` override.
When reading an older saved configuration, OpenCodex ignores obsolete Chat/Responses overrides for the newly pinned Gemini and Qwen models, along with their formerly valid Responses-only hosted-tool preferences. Other providers, accounts and routing remain available. Reads leave the file unchanged; a later explicit configuration edit saves the upgraded values. New overrides for these native models are still rejected.
Gemini uses `low`, `medium` and `high` thinking levels; higher requested efforts clamp to `high`.

OpenCode Go requires a stable session identifier for routing. OpenCodex sends
Expand Down
2 changes: 1 addition & 1 deletion scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@
"config-account-thresholds.test.ts": "config",
"config-blocked-model-redirects.test.ts": "config",
"config-catalog-auto-refresh.test.ts": "config",
"config-commandcode-claude-pin.test.ts": "config", "config-opencode-zen-upgrade.test.ts": "config",
"config-commandcode-claude-pin.test.ts": "config", "config-opencode-zen-upgrade.test.ts": "config", "config-opencode-go-upgrade.test.ts": "config",
"config-load-degrade.test.ts": "config",
"protocol-settings.test.ts": "config",
"config-mutation-lock.test.ts": "config",
Expand Down
4 changes: 2 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ import { persistConfigUnlocked, readRawConfigJson } from "./config/persist-unloc
import { withConfigMutationLockSync, bumpGenerationForCooperatingConfigWrite } from "./config/mutation-lock";
import { getDefaultConfig } from "./config/proxy-env";
import { configSchema } from "./config/schema/config-schema";
import { sanitizeOpenCodeZenOverridesForLoad } from "./config/opencode-zen-upgrade";
import { sanitizeOpenCodeOverridesForLoad } from "./config/opencode-wire-upgrade";
import {
hardenExistingSecret,
normalizeApiKeyIds,
Expand Down Expand Up @@ -223,7 +223,7 @@ export function loadConfig(): OcxConfig {
try {
const raw = readFileSync(configPath, "utf-8").replace(/^\uFEFF/, "");
const parsed = JSON.parse(raw);
sanitizeOpenCodeZenOverridesForLoad(parsed);
sanitizeOpenCodeOverridesForLoad(parsed);
sanitizeAliasesForLoad(parsed);
sanitizeReasoningPinsForLoad(parsed);
sanitizeModelDisplayNamesForLoad(parsed);
Expand Down
4 changes: 2 additions & 2 deletions src/config/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { isMissingPathError } from "./atomic-write";
import { getConfigDir, getConfigPath } from "./paths";
import { getDefaultConfig } from "./proxy-env";
import { salvageConfigCandidate } from "./salvage";
import { sanitizeOpenCodeZenOverridesForLoad } from "./opencode-zen-upgrade";
import { sanitizeOpenCodeOverridesForLoad } from "./opencode-wire-upgrade";
import {
sanitizeReasoningPinsForLoad,
sanitizeRetryOn429ForLoad,
Expand Down Expand Up @@ -766,7 +766,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
export function configDiagnosticsFromRaw(raw: string): ConfigDiagnostics {
try {
const parsed = JSON.parse(raw.replace(/^\uFEFF/, ""));
sanitizeOpenCodeZenOverridesForLoad(parsed);
sanitizeOpenCodeOverridesForLoad(parsed);
sanitizeReasoningPinsForLoad(parsed);
// Same degradation as loadConfig: a hand-edited invalid retryOn429 must not trip the
// schema and send the caller a default-config fallback (the config command could then
Expand Down
54 changes: 54 additions & 0 deletions src/config/opencode-wire-upgrade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { OPENCODE_GO_LEGACY_NAME_PINS } from "../types/opencode-go";
import { captureWireAdapterHardPins } from "../types/wire";
import { declaredUnsupportedHostedTools, isHostedToolUnsupportedForModel } from "../responses/hosted-tool-policy";
import { sanitizeOpenCodeZenOverridesForLoad } from "./opencode-zen-upgrade";

function record(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown> : undefined;
}

/** File reads alone repair overrides admitted before OpenCode's native-wire upgrade. */
export function sanitizeOpenCodeOverridesForLoad(parsed: unknown): void {
sanitizeOpenCodeZenOverridesForLoad(parsed);
const providers = record(record(parsed)?.providers);
const provider = providers && Object.hasOwn(providers, "opencode-go")
? record(providers["opencode-go"]) : undefined;
if (!provider) return;
// Use validation's destination/auth rules, including the Go preset's fixed URL.
const pins = captureWireAdapterHardPins("opencode-go", provider);
const newlyPinned = (model: string): boolean => Object.hasOwn(pins, model)
&& !OPENCODE_GO_LEGACY_NAME_PINS.has(model);
const overrides = record(provider.modelAdapters) ?? {};
const preferences = record(provider.modelPreferHostedTools);
let preferencesChanged = false;
if (preferences) {
const unsupported = declaredUnsupportedHostedTools(provider as { unsupportedHostedTools?: readonly string[] });
for (const [model, tools] of Object.entries(preferences)) {
// Go's registry owns the old provider-wide Chat wire. Only an explicit
// Responses override could previously make these native models eligible.
if (!newlyPinned(model.trim()) || overrides[model] !== "openai-responses"
|| !Array.isArray(tools) || tools.length === 0 || tools.some(tool => tool !== "image_generation")
|| isHostedToolUnsupportedForModel(model, "image_generation", undefined, unsupported)) continue;
delete preferences[model];
preferencesChanged = true;
}
if (preferencesChanged && Object.keys(preferences).length === 0) delete provider.modelPreferHostedTools;
}
let overridesChanged = false;
for (const [model, adapter] of Object.entries(overrides)) {
// Leave malformed or previously incompatible preferences visible on every pass.
if (preferences && Object.hasOwn(preferences, model)) continue;
if (newlyPinned(model.trim()) && (adapter === "openai-chat" || adapter === "openai-responses")) {
delete overrides[model];
overridesChanged = true;
}
}
if (overridesChanged) {
if (Object.keys(overrides).length === 0) delete provider.modelAdapters;
console.warn("OpenCode Go native models now use their required APIs; obsolete Chat/Responses overrides were ignored while loading config.json.");
}
if (preferencesChanged) {
console.warn("OpenCode Go native models now use their required APIs; legacy Responses-only hosted-tool preferences were ignored while loading config.json.");
}
}
2 changes: 1 addition & 1 deletion structure/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ capture/route time and explicit false or empty declarations retain their field-s
`src/config/provider-validation.ts` owns the pure provider payload checks shared by persisted config,
CLI writes, and management DTO validation. `src/config.ts` imports those checks for Zod refinement
and re-exports them as a compatibility facade; it must not grow a second copy. Validation error text,
ordering, and cross-field rules are part of the write/load contract: management requests and hand-edited `config.json` accept and reject the same provider shapes. Before validation, `src/config/opencode-zen-upgrade.ts` normalizes saved Gemini overrides and their Responses-only hosted-tool preferences on canonical keyed OpenCode Zen routes. Runtime and diagnostics-backed reads share this upgrade, preserving unrelated providers and accounts. Reads warn without rewriting the file; explicit edits persist the normalized configuration, and new writes still reject incompatible overrides and preferences. Other destinations, authentication modes and invalid override values retain normal validation.
ordering, and cross-field rules are part of the write/load contract: management requests and hand-edited `config.json` accept and reject the same provider shapes. Before validation, `src/config/opencode-wire-upgrade.ts` shares the Zen upgrade in `src/config/opencode-zen-upgrade.ts` and normalizes saved Chat/Responses overrides for newly pinned Go Gemini and Qwen models, including previously valid Responses-only hosted-tool preferences. Go uses validation's historical fixed-destination and key-auth rules; its older Anthropic pins and overridable Responses defaults retain normal validation. Runtime and diagnostics-backed reads share this upgrade, preserving unrelated providers, accounts and routing. Reads warn without rewriting the file; explicit edits persist the normalized configuration, and new writes still reject incompatible overrides and preferences. Unsupported authentication modes and invalid override values retain normal validation.

Provider `projectContext` accepts `"off"` or `"on"` only for native `command-code`; `src/config/schema/leaf-validators.ts` rejects other adapters on load, while `src/config/provider-validation.ts` and `src/server/auth-cors.ts` reject them on management writes. This editor-owned outbound-file field follows [provider and adapter selection](providers-and-adapters.md).
The Google tool-schema policy uses a closed enum at this boundary. Unknown values fail config load,
Expand Down
Loading
Loading