@bitkyc08/opencodex 2.6.31-preview.20260707 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.ko.md +19 -3
  2. package/README.md +17 -2
  3. package/README.zh-CN.md +16 -3
  4. package/gui/dist/assets/index-BGdxwydf.js +34 -0
  5. package/gui/dist/assets/index-DANCQ2Jt.css +1 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic.ts +62 -1
  9. package/src/adapters/cursor/cursor-errors.ts +28 -1
  10. package/src/adapters/cursor/discovery.ts +56 -10
  11. package/src/adapters/cursor/effort-map.ts +35 -7
  12. package/src/adapters/cursor/live-models.ts +3 -0
  13. package/src/adapters/cursor/live-transport.ts +136 -7
  14. package/src/adapters/cursor/protobuf-request.ts +24 -1
  15. package/src/adapters/cursor/request-builder.ts +6 -5
  16. package/src/adapters/cursor/transport-retry.ts +22 -3
  17. package/src/adapters/cursor.ts +2 -1
  18. package/src/adapters/kiro.ts +1 -1
  19. package/src/adapters/openai-chat.ts +75 -26
  20. package/src/bridge.ts +50 -4
  21. package/src/cli/debug.ts +203 -0
  22. package/src/cli/doctor.ts +11 -0
  23. package/src/cli/help.ts +11 -0
  24. package/src/cli/index.ts +10 -0
  25. package/src/cli/v2.ts +131 -0
  26. package/src/codex/account-store.ts +42 -1
  27. package/src/codex/auth-api.ts +43 -0
  28. package/src/codex/catalog.ts +356 -29
  29. package/src/codex/data/upstream-models.json +830 -0
  30. package/src/codex/features.ts +178 -0
  31. package/src/codex/project-config-warnings.ts +388 -0
  32. package/src/codex/sync.ts +8 -0
  33. package/src/codex/warmup.ts +193 -0
  34. package/src/config.ts +7 -5
  35. package/src/lib/debug-log-buffer.ts +42 -0
  36. package/src/lib/debug-settings.ts +84 -0
  37. package/src/lib/debug.ts +18 -9
  38. package/src/lib/errors.ts +104 -1
  39. package/src/oauth/cursor.ts +35 -12
  40. package/src/oauth/store.ts +4 -3
  41. package/src/oauth/token-guardian.ts +32 -7
  42. package/src/providers/derive.ts +8 -0
  43. package/src/providers/kiro-models.ts +3 -3
  44. package/src/providers/registry.ts +77 -56
  45. package/src/reasoning-effort.ts +34 -12
  46. package/src/responses/parser.ts +7 -2
  47. package/src/router.ts +7 -3
  48. package/src/server/adapter-resolve.ts +1 -1
  49. package/src/server/index.ts +27 -3
  50. package/src/server/management-api.ts +168 -7
  51. package/src/server/relay.ts +2 -2
  52. package/src/server/request-log.ts +86 -2
  53. package/src/server/responses.ts +209 -0
  54. package/src/types.ts +38 -2
  55. package/src/usage/debug.ts +32 -5
  56. package/src/usage/summary.ts +6 -6
  57. package/src/web-search/index.ts +1 -1
  58. package/gui/dist/assets/index-ByGC8-Bm.css +0 -1
  59. package/gui/dist/assets/index-CWujz83O.js +0 -15
@@ -18,9 +18,12 @@ import { getValidAccessTokenForAccount, listOAuthProviders, OAuthLoginRequiredEr
18
18
  import {
19
19
  getValidCodexToken,
20
20
  listCodexAccountIds,
21
+ markCodexAccountValidated,
22
+ markCodexAccountValidationFailed,
21
23
  readCodexAccountRecord,
22
24
  TokenRefreshError,
23
25
  } from "../codex/account-store";
26
+ import { codexWarmupFailureReason, warmCodexAccount } from "../codex/warmup";
24
27
 
25
28
  export interface TokenGuardianHandle {
26
29
  stop(): void;
@@ -29,6 +32,7 @@ export interface TokenGuardianHandle {
29
32
  export interface GuardianSweepResult {
30
33
  enabled: boolean;
31
34
  refreshed: string[];
35
+ warmed: string[];
32
36
  failed: string[];
33
37
  skippedBackoff: string[];
34
38
  }
@@ -40,6 +44,8 @@ const DEFAULTS = {
40
44
  leadSeconds: 900,
41
45
  failureBackoffBaseSeconds: 300,
42
46
  failureBackoffMaxSeconds: 3600,
47
+ codexWarmupMaxAgeSeconds: 691_200, // 8d — matches Codex managed-auth last_refresh cadence.
48
+ codexWarmupModel: "gpt-5.4-mini",
43
49
  };
44
50
 
45
51
  interface BackoffEntry {
@@ -67,6 +73,9 @@ function resolved(g: OcxTokenGuardianConfig | undefined) {
67
73
  leadSeconds: num(g?.leadSeconds, DEFAULTS.leadSeconds, 0),
68
74
  backoffBaseSeconds: num(g?.failureBackoffBaseSeconds, DEFAULTS.failureBackoffBaseSeconds, 0),
69
75
  backoffMaxSeconds: num(g?.failureBackoffMaxSeconds, DEFAULTS.failureBackoffMaxSeconds, 0),
76
+ codexWarmupEnabled: g?.codexWarmupEnabled === true,
77
+ codexWarmupMaxAgeSeconds: num(g?.codexWarmupMaxAgeSeconds, DEFAULTS.codexWarmupMaxAgeSeconds, 60),
78
+ codexWarmupModel: g?.codexWarmupModel?.trim() || DEFAULTS.codexWarmupModel,
70
79
  };
71
80
  }
72
81
 
@@ -106,7 +115,7 @@ async function runWithConcurrency(tasks: Array<() => Promise<void>>, limit: numb
106
115
  export async function guardianSweep(nowMs: number = Date.now()): Promise<GuardianSweepResult> {
107
116
  const config: OcxConfig = loadConfig();
108
117
  const g = config.tokenGuardian;
109
- const result: GuardianSweepResult = { enabled: !!g?.enabled, refreshed: [], failed: [], skippedBackoff: [] };
118
+ const result: GuardianSweepResult = { enabled: !!g?.enabled, refreshed: [], warmed: [], failed: [], skippedBackoff: [] };
110
119
  if (!g?.enabled) return result;
111
120
 
112
121
  const opts = resolved(g);
@@ -142,18 +151,34 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
142
151
  if (resolveRefreshPolicy("chatgpt", config) === "proactive") {
143
152
  for (const id of listCodexAccountIds()) {
144
153
  const record = readCodexAccountRecord(id);
145
- const cred = record?.deletedAt == null ? record?.credential : undefined;
154
+ if (!record || record.deletedAt != null) continue;
155
+ const cred = record.credential;
146
156
  if (!cred) continue;
147
- if (cred.expiresAt > nowMs + horizonMs) continue;
157
+ const needsRefresh = cred.expiresAt <= nowMs + horizonMs;
158
+ const needsWarmup = opts.codexWarmupEnabled
159
+ && (record.lastCodexValidatedAt === undefined || nowMs - record.lastCodexValidatedAt > opts.codexWarmupMaxAgeSeconds * 1000);
160
+ if (!needsRefresh && !needsWarmup) continue;
148
161
  const key = `codex:${id}`;
149
162
  if (inBackoff(key, nowMs)) { result.skippedBackoff.push(key); continue; }
150
163
  tasks.push(async () => {
151
164
  try {
152
- await getValidCodexToken(id);
165
+ const token = await getValidCodexToken(id);
166
+ if (needsRefresh) result.refreshed.push(key);
167
+ if (needsWarmup) {
168
+ await warmCodexAccount({
169
+ accessToken: token.accessToken,
170
+ chatgptAccountId: token.chatgptAccountId,
171
+ model: opts.codexWarmupModel,
172
+ });
173
+ markCodexAccountValidated(id, Date.now());
174
+ result.warmed.push(key);
175
+ }
153
176
  backoff.delete(key);
154
- result.refreshed.push(key);
155
177
  } catch (err) {
156
178
  const permanent = err instanceof TokenRefreshError && (err.reason === "revoked" || err.reason === "expired");
179
+ if (needsWarmup && !(err instanceof TokenRefreshError)) {
180
+ markCodexAccountValidationFailed(id, codexWarmupFailureReason(err));
181
+ }
157
182
  recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, permanent);
158
183
  result.failed.push(key);
159
184
  }
@@ -186,8 +211,8 @@ export function startTokenGuardian(): TokenGuardianHandle {
186
211
  const runSweep = () => {
187
212
  void guardianSweep()
188
213
  .then(r => {
189
- if (r.enabled && (r.refreshed.length || r.failed.length)) {
190
- console.log(`🛡️ token-guardian: refreshed ${r.refreshed.length}, failed ${r.failed.length}`);
214
+ if (r.enabled && (r.refreshed.length || r.warmed.length || r.failed.length)) {
215
+ console.log(`🛡️ token-guardian: refreshed ${r.refreshed.length}, warmed ${r.warmed.length}, failed ${r.failed.length}`);
191
216
  }
192
217
  })
193
218
  .catch(err => console.log(`token-guardian sweep error: ${err instanceof Error ? err.message : String(err)}`))
@@ -23,6 +23,8 @@ export interface DerivedKeyLoginProvider {
23
23
  noPenaltyModels?: string[];
24
24
  autoToolChoiceOnlyModels?: string[];
25
25
  preserveReasoningContentModels?: string[];
26
+ thinkingToggleModels?: string[];
27
+ thinkingBudgetModels?: string[];
26
28
  escapeBuiltinToolNames?: boolean;
27
29
  }
28
30
 
@@ -80,9 +82,11 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
80
82
  ...(entry.noTemperatureModels ? { noTemperatureModels: [...entry.noTemperatureModels] } : {}),
81
83
  ...(entry.noTopPModels ? { noTopPModels: [...entry.noTopPModels] } : {}),
82
84
  ...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
85
+ ...(entry.parallelToolCalls !== undefined ? { parallelToolCalls: entry.parallelToolCalls } : {}),
83
86
  ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
84
87
  ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
85
88
  ...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}),
89
+ ...(entry.thinkingBudgetModels ? { thinkingBudgetModels: [...entry.thinkingBudgetModels] } : {}),
86
90
  ...(entry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: entry.escapeBuiltinToolNames } : {}),
87
91
  ...(entry.googleMode ? { googleMode: entry.googleMode } : {}),
88
92
  ...(entry.project ? { project: entry.project } : {}),
@@ -117,6 +121,8 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
117
121
  ...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
118
122
  ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
119
123
  ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
124
+ ...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}),
125
+ ...(entry.thinkingBudgetModels ? { thinkingBudgetModels: [...entry.thinkingBudgetModels] } : {}),
120
126
  ...(entry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: entry.escapeBuiltinToolNames } : {}),
121
127
  ...(entry.googleMode ? { googleMode: entry.googleMode } : {}),
122
128
  ...(entry.project ? { project: entry.project } : {}),
@@ -177,9 +183,11 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
177
183
  if (!prov.noTemperatureModels && seed.noTemperatureModels) prov.noTemperatureModels = [...seed.noTemperatureModels];
178
184
  if (!prov.noTopPModels && seed.noTopPModels) prov.noTopPModels = [...seed.noTopPModels];
179
185
  if (!prov.noPenaltyModels && seed.noPenaltyModels) prov.noPenaltyModels = [...seed.noPenaltyModels];
186
+ if (prov.parallelToolCalls === undefined && seed.parallelToolCalls !== undefined) prov.parallelToolCalls = seed.parallelToolCalls;
180
187
  if (!prov.autoToolChoiceOnlyModels && seed.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...seed.autoToolChoiceOnlyModels];
181
188
  if (!prov.preserveReasoningContentModels && seed.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...seed.preserveReasoningContentModels];
182
189
  if (!prov.thinkingToggleModels && seed.thinkingToggleModels) prov.thinkingToggleModels = [...seed.thinkingToggleModels];
190
+ if (!prov.thinkingBudgetModels && seed.thinkingBudgetModels) prov.thinkingBudgetModels = [...seed.thinkingBudgetModels];
183
191
  if (prov.escapeBuiltinToolNames === undefined && seed.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = seed.escapeBuiltinToolNames;
184
192
  }
185
193
 
@@ -38,10 +38,10 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
38
38
  "qwen3-coder-next": 256_000,
39
39
  };
40
40
 
41
- const KIRO_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
41
+ const KIRO_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
42
42
 
43
- // Codex does not accept raw "max" in catalog metadata; Kiro xhigh already maps to the maximum
44
- // fake-thinking budget in src/adapters/kiro.ts.
43
+ // Kiro has no upstream reasoning_effort enum; these labels map to fake-thinking budgets in
44
+ // src/adapters/kiro.ts.
45
45
  export const KIRO_MODEL_REASONING_EFFORTS: Record<string, string[]> = Object.fromEntries(
46
46
  KIRO_MODELS.map(id => [id, KIRO_REASONING_EFFORTS]),
47
47
  );
@@ -37,9 +37,12 @@ export interface ProviderRegistryEntry {
37
37
  noTemperatureModels?: string[];
38
38
  noTopPModels?: string[];
39
39
  noPenaltyModels?: string[];
40
+ /** Opt this provider into parallel tool calls (see OcxProviderConfig.parallelToolCalls). */
41
+ parallelToolCalls?: boolean;
40
42
  autoToolChoiceOnlyModels?: string[];
41
43
  preserveReasoningContentModels?: string[];
42
44
  thinkingToggleModels?: string[];
45
+ thinkingBudgetModels?: string[];
43
46
  escapeBuiltinToolNames?: boolean;
44
47
  oauthId?: string;
45
48
  jawcodeBundle?: string;
@@ -56,37 +59,40 @@ export type ProviderConfigSeed = Pick<
56
59
  | "liveModels" | "contextWindow" | "modelContextWindows" | "modelInputModalities"
57
60
  | "reasoningEfforts" | "modelReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
58
61
  | "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
59
- | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "thinkingToggleModels" | "escapeBuiltinToolNames"
62
+ | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames"
60
63
  | "googleMode" | "project" | "location"
61
64
  >;
62
65
 
63
-
64
- const OLLAMA_REASONING_MAP: Record<string, string> = { xhigh: "max" };
65
-
66
66
  // Shared between the OAuth (Claude account) and API-key Anthropic entries so both expose the
67
67
  // same static model seed.
68
- const ANTHROPIC_MODELS = ["claude-sonnet-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"];
69
- const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-sonnet-5": 1_000_000 };
68
+ // 260709 refresh: claude-fable-5 added (official models overview); evidence in
69
+ // devlog/model_update/260709_model_refresh/002_cursor_registry_drift.md.
70
+ const ANTHROPIC_MODELS = ["claude-fable-5", "claude-sonnet-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"];
71
+ const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-sonnet-5": 1_000_000, "claude-fable-5": 1_000_000 };
70
72
 
71
73
  const ZAI_GLM_52_MODELS = ["glm-5.2", "glm-5.2[1m]"];
72
- const ZAI_GLM_52_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
73
- const ZAI_GLM_52_REASONING_MAP: Record<string, string> = {
74
- none: "none",
75
- minimal: "none",
76
- low: "high",
77
- medium: "high",
78
- high: "high",
79
- xhigh: "max",
80
- max: "max",
74
+ const ZAI_GLM_52_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
75
+ const OPENAI_GPT56_MODELS = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"];
76
+ const OPENAI_GPT56_CONTEXT_WINDOW = 372_000;
77
+ const OPENAI_GPT56_CONTEXT_WINDOWS = {
78
+ "gpt-5.6-sol": OPENAI_GPT56_CONTEXT_WINDOW,
79
+ "gpt-5.6-terra": OPENAI_GPT56_CONTEXT_WINDOW,
80
+ "gpt-5.6-luna": OPENAI_GPT56_CONTEXT_WINDOW,
81
+ };
82
+ const OPENROUTER_GPT56_MODELS = OPENAI_GPT56_MODELS.map(id => `openai/${id}`);
83
+ const OPENROUTER_GPT56_CONTEXT_WINDOWS = {
84
+ "openai/gpt-5.6-sol": OPENAI_GPT56_CONTEXT_WINDOW,
85
+ "openai/gpt-5.6-terra": OPENAI_GPT56_CONTEXT_WINDOW,
86
+ "openai/gpt-5.6-luna": OPENAI_GPT56_CONTEXT_WINDOW,
81
87
  };
82
88
 
83
89
  /**
84
90
  * Vendor thinking-toggle models (MiMo v2.x, GLM 5/5.1 on Zen Go): the wire knob is
85
- * `thinking: {type: enabled|disabled}` — a binary. Advertise a two-step Codex ladder
86
- * (low = thinking off, high = thinking on) and map efforts onto the toggle. Zen Go
91
+ * `thinking: {type: enabled|disabled}` — a binary. Advertise the full Codex picker ladder
92
+ * and map efforts onto the toggle. Zen Go
87
93
  * pass-through probed live 2026-07-07 (glm-5.2 toggle verified; mimo/minimax accept shape).
88
94
  */
89
- const THINKING_TOGGLE_EFFORTS = ["low", "high"];
95
+ const THINKING_TOGGLE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
90
96
  const THINKING_TOGGLE_MAP: Record<string, string> = {
91
97
  none: "disabled",
92
98
  minimal: "disabled",
@@ -99,8 +105,16 @@ const THINKING_TOGGLE_MAP: Record<string, string> = {
99
105
  const OPENCODE_GO_THINKING_TOGGLE_MODELS = [
100
106
  "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2-omni", "mimo-v2-pro", "glm-5", "glm-5.1",
101
107
  ];
108
+ const THINKING_BUDGET_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
109
+ const THINKING_BUDGET_MODELS = [
110
+ "qwen3.5-397b", "qwen3.6-35b",
111
+ "qwen3.5-plus", "qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus",
112
+ ];
113
+ const OPENCODE_GO_THINKING_BUDGET_MODELS = ["qwen3.5-plus", "qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus"];
102
114
  const DEEPSEEK_THINKING_MODELS = ["deepseek-v4-pro", "deepseek-v4-flash"];
103
- const DEEPSEEK_THINKING_EFFORTS = ["high", "xhigh"];
115
+ // "max" is advertised too: the wire map routes xhigh->max and max->max, so the picker
116
+ // should surface the max tier instead of hiding it behind xhigh.
117
+ const DEEPSEEK_THINKING_EFFORTS = ["high", "xhigh", "max"];
104
118
  const DEEPSEEK_THINKING_REASONING_MAP: Record<string, string> = {
105
119
  low: "high",
106
120
  medium: "high",
@@ -108,7 +122,7 @@ const DEEPSEEK_THINKING_REASONING_MAP: Record<string, string> = {
108
122
  xhigh: "max",
109
123
  max: "max",
110
124
  };
111
- const KIMI_THINKING_MODELS = ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5", "kimi-k2-0905-preview"];
125
+ const KIMI_THINKING_MODELS = ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5"];
112
126
  const KIMI_LOCKED_PARAMETER_MODELS = ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5"];
113
127
  const NEURALWATT_REASONING_HISTORY_MODELS = [
114
128
  "glm-5.2",
@@ -118,28 +132,17 @@ const NEURALWATT_REASONING_HISTORY_MODELS = [
118
132
  const UMANS_MODELS = [
119
133
  "umans-coder",
120
134
  "umans-kimi-k2.7",
121
- "umans-kimi-k2.6",
122
135
  "umans-flash",
123
136
  "umans-glm-5.2",
124
137
  "umans-glm-5.1",
125
138
  "umans-qwen3.6-35b-a3b",
126
139
  ];
127
- const UMANS_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
128
- const UMANS_GLM_REASONING_EFFORTS = ["high", "xhigh"];
129
- const UMANS_GLM_REASONING_MAP: Record<string, string> = {
130
- none: "high",
131
- minimal: "high",
132
- low: "high",
133
- medium: "high",
134
- high: "high",
135
- xhigh: "max",
136
- max: "max",
137
- };
140
+ const UMANS_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
141
+ const UMANS_GLM_REASONING_EFFORTS = ["high", "xhigh", "max"];
138
142
  const UMANS_TEXT_ONLY_MODELS = ["umans-glm-5.2", "umans-glm-5.1"];
139
143
  const UMANS_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
140
144
  "umans-coder": 262_144,
141
145
  "umans-kimi-k2.7": 262_144,
142
- "umans-kimi-k2.6": 262_144,
143
146
  "umans-flash": 262_144,
144
147
  "umans-glm-5.2": 405_504,
145
148
  "umans-glm-5.1": 202_752,
@@ -167,7 +170,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
167
170
  authKind: "oauth",
168
171
  featured: false,
169
172
  dashboardPreset: true,
170
- note: "Experimental Cursor bridge. Live transport and live model discovery are enabled after a standalone PKCE browser login via 'ocx login cursor'; native read/write/delete/shell/fetch execution stays disabled unless provider.unsafeAllowNativeLocalExec is explicitly set for a trusted local experiment.",
173
+ note: "Experimental Cursor bridge. Live transport and live model discovery are enabled after a standalone PKCE browser login via 'ocx login cursor'; native read/write/delete/shell/fetch execution stays disabled unless you set \"unsafeAllowNativeLocalExec\": true on providers.cursor in ~/.opencodex/config.json (dashboard: Providers → Cursor → Edit JSON) for a trusted local experiment.",
171
174
  models: cursorModelIds(CURSOR_STATIC_MODELS),
172
175
  liveModels: true,
173
176
  defaultModel: "auto",
@@ -190,9 +193,29 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
190
193
  oauthId: "xai",
191
194
  jawcodeBundle: "xai",
192
195
  note: "Log in with your Grok account",
193
- models: ["grok-4.3", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
194
- defaultModel: "grok-4.3",
195
- noReasoningModels: ["grok-build-0.1", "grok-composer-2.5-fast"],
196
+ // Parallel tool calls: officially supported and default-on per docs.x.ai function-calling
197
+ // (verified 260709, devlog/_plan/260709_parallel_tool_calls). Streamed calls arrive whole
198
+ // per chunk, so the buffered parser assembles them losslessly.
199
+ parallelToolCalls: true,
200
+ // Live /v1/models discovery is the authoritative lineup (verified 260709: returns grok-4.5);
201
+ // the static list below is the logged-out fallback seed.
202
+ liveModels: true,
203
+ // 260709 refresh: lineup + metadata from official docs.x.ai (grok-4.5 announced 07-08);
204
+ // grok-composer-2.5-fast kept as account-verified (absent from public docs). Evidence:
205
+ // devlog/model_update/260709_model_refresh/001_xai_lineup.md.
206
+ models: ["grok-4.5", "grok-4.3", "grok-4.20-multi-agent-0309", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
207
+ defaultModel: "grok-4.5",
208
+ noReasoningModels: ["grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
209
+ // grok-4.5 reasoning is always-on with low/medium/high control (no off tier upstream).
210
+ modelReasoningEfforts: { "grok-4.5": ["low", "medium", "high"] },
211
+ modelContextWindows: {
212
+ "grok-4.5": 500_000,
213
+ "grok-4.3": 1_000_000,
214
+ "grok-4.20-multi-agent-0309": 1_000_000,
215
+ "grok-4.20-0309-reasoning": 1_000_000,
216
+ "grok-4.20-0309-non-reasoning": 1_000_000,
217
+ "grok-build-0.1": 256_000,
218
+ },
196
219
  noVisionModels: ["grok-build-0.1", "grok-composer-2.5-fast"],
197
220
  },
198
221
  {
@@ -207,7 +230,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
207
230
  note: "Log in with your Claude account",
208
231
  models: [...ANTHROPIC_MODELS],
209
232
  modelContextWindows: { ...ANTHROPIC_MODEL_CONTEXT_WINDOWS },
210
- defaultModel: "claude-sonnet-4-6",
233
+ defaultModel: "claude-sonnet-5",
211
234
  },
212
235
  {
213
236
  id: "anthropic-apikey",
@@ -223,7 +246,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
223
246
  models: [...ANTHROPIC_MODELS],
224
247
  liveModels: true,
225
248
  modelContextWindows: { ...ANTHROPIC_MODEL_CONTEXT_WINDOWS },
226
- defaultModel: "claude-sonnet-4-6",
249
+ defaultModel: "claude-sonnet-5",
227
250
  },
228
251
  {
229
252
  id: "kimi",
@@ -260,7 +283,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
260
283
  modelContextWindows: KIRO_MODEL_CONTEXT_WINDOWS,
261
284
  modelReasoningEfforts: KIRO_MODEL_REASONING_EFFORTS,
262
285
  },
263
- { id: "openai-apikey", label: "OpenAI (API key)", adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", authKind: "key", featured: true, dashboardUrl: "https://platform.openai.com/api-keys", defaultModel: "gpt-5.5" },
286
+ { id: "openai-apikey", label: "OpenAI (API key)", adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", authKind: "key", featured: true, dashboardUrl: "https://platform.openai.com/api-keys", defaultModel: "gpt-5.5", models: ["gpt-5.5", ...OPENAI_GPT56_MODELS], liveModels: true, modelContextWindows: OPENAI_GPT56_CONTEXT_WINDOWS },
264
287
  {
265
288
  id: "umans",
266
289
  label: "Umans AI Coding Plan",
@@ -277,15 +300,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
277
300
  modelReasoningEfforts: {
278
301
  "umans-coder": UMANS_REASONING_EFFORTS,
279
302
  "umans-kimi-k2.7": UMANS_REASONING_EFFORTS,
280
- "umans-kimi-k2.6": UMANS_REASONING_EFFORTS,
281
- "umans-flash": ["low", "medium", "high"],
303
+ "umans-flash": UMANS_REASONING_EFFORTS,
282
304
  "umans-glm-5.2": UMANS_GLM_REASONING_EFFORTS,
283
305
  "umans-glm-5.1": UMANS_GLM_REASONING_EFFORTS,
284
- "umans-qwen3.6-35b-a3b": ["low", "medium", "high"],
285
- },
286
- modelReasoningEffortMap: {
287
- "umans-glm-5.2": UMANS_GLM_REASONING_MAP,
288
- "umans-glm-5.1": UMANS_GLM_REASONING_MAP,
306
+ "umans-qwen3.6-35b-a3b": UMANS_REASONING_EFFORTS,
289
307
  },
290
308
  noVisionModels: UMANS_TEXT_ONLY_MODELS,
291
309
  escapeBuiltinToolNames: true,
@@ -299,12 +317,15 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
299
317
  "kimi-k2.7-code": [],
300
318
  "kimi-k2.7-code-highspeed": [],
301
319
  ...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_EFFORTS])),
320
+ ...Object.fromEntries(OPENCODE_GO_THINKING_BUDGET_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
302
321
  },
322
+ // glm-5.2 uses identity labels now that `max` is a native Codex level (no alias map);
323
+ // the thinking-toggle map is a REAL wire alias (effort -> enabled/disabled) and stays.
303
324
  modelReasoningEffortMap: {
304
- "glm-5.2": ZAI_GLM_52_REASONING_MAP,
305
325
  ...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_MAP])),
306
326
  },
307
327
  thinkingToggleModels: OPENCODE_GO_THINKING_TOGGLE_MODELS,
328
+ thinkingBudgetModels: THINKING_BUDGET_MODELS,
308
329
  noReasoningModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
309
330
  // Text-only Zen Go models (jawcode metadata) — the vision sidecar describes images for
310
331
  // every model listed here (and the catalog advertises image input on their behalf).
@@ -345,12 +366,14 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
345
366
  "kimi-k2.6": [],
346
367
  "kimi-k2.6-fast": [],
347
368
  "kimi-k2.7-code": [],
348
- "qwen3.5-397b": ["low", "medium", "high"],
369
+ // Qwen3.x uses thinking_budget, NOT graded reasoning_effort; the adapter maps the five
370
+ // Codex picker levels onto budget fractions.
371
+ "qwen3.5-397b": THINKING_BUDGET_EFFORTS,
349
372
  "qwen3.5-397b-fast": [],
350
- "qwen3.6-35b": ["low", "medium", "high"],
373
+ "qwen3.6-35b": THINKING_BUDGET_EFFORTS,
351
374
  "qwen3.6-35b-fast": [],
352
375
  },
353
- modelReasoningEffortMap: { "glm-5.2": ZAI_GLM_52_REASONING_MAP },
376
+ thinkingBudgetModels: THINKING_BUDGET_MODELS,
354
377
  noReasoningModels: ["glm-5.2-fast", "kimi-k2.5-fast", "kimi-k2.6-fast", "qwen3.5-397b-fast", "qwen3.6-35b-fast"],
355
378
  noVisionModels: ["glm-5.2", "glm-5.2-fast", "qwen3.5-397b", "qwen3.5-397b-fast"],
356
379
  noTemperatureModels: ["kimi-k2.7-code"],
@@ -359,13 +382,13 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
359
382
  autoToolChoiceOnlyModels: ["kimi-k2.7-code"],
360
383
  preserveReasoningContentModels: NEURALWATT_REASONING_HISTORY_MODELS,
361
384
  },
362
- { id: "openrouter", label: "OpenRouter", adapter: "openai-chat", baseUrl: "https://openrouter.ai/api/v1", authKind: "key", featured: true, dashboardUrl: "https://openrouter.ai/keys", jawcodeBundle: "openrouter", models: ["anthropic/claude-sonnet-5"], modelContextWindows: { "anthropic/claude-sonnet-5": 1_000_000 } },
385
+ { id: "openrouter", label: "OpenRouter", adapter: "openai-chat", baseUrl: "https://openrouter.ai/api/v1", authKind: "key", featured: true, dashboardUrl: "https://openrouter.ai/keys", jawcodeBundle: "openrouter", models: ["anthropic/claude-sonnet-5", ...OPENROUTER_GPT56_MODELS], modelContextWindows: { "anthropic/claude-sonnet-5": 1_000_000, ...OPENROUTER_GPT56_CONTEXT_WINDOWS } },
363
386
  { id: "groq", label: "Groq", adapter: "openai-chat", baseUrl: "https://api.groq.com/openai/v1", authKind: "key", featured: true, dashboardUrl: "https://console.groq.com/keys" },
364
387
  { id: "google", label: "Google Gemini", adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", authKind: "key", featured: true, dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3-pro", jawcodeBundle: "google", extraMetadataAliases: ["gemini"] },
365
388
  { id: "google-vertex", label: "Google Vertex AI", adapter: "google", baseUrl: "https://aiplatform.googleapis.com", authKind: "key", dashboardUrl: "https://console.cloud.google.com/vertex-ai", defaultModel: "gemini-3-pro", googleMode: "vertex", jawcodeBundle: "google", extraMetadataAliases: ["gemini-vertex"] },
366
389
  { id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, defaultModel: "gemini-3.5-flash-low", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
367
390
  { id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
368
- { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", featured: true, note: "Local — key usually blank", reasoningEffortMap: OLLAMA_REASONING_MAP },
391
+ { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", featured: true, note: "Local — key usually blank" },
369
392
  { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", featured: true, note: "Local — key usually blank" },
370
393
  { id: "lm-studio", label: "LM Studio (local)", adapter: "openai-chat", baseUrl: "http://localhost:1234/v1", authKind: "local", featured: true, note: "Local — no key needed" },
371
394
  {
@@ -393,7 +416,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
393
416
  {
394
417
  id: "moonshot", label: "Moonshot (Kimi API)", baseUrl: "https://api.moonshot.ai/v1", adapter: "openai-chat", authKind: "key",
395
418
  dashboardUrl: "https://platform.moonshot.ai/console/api-keys", defaultModel: "kimi-k2.7-code", jawcodeBundle: "moonshot",
396
- models: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5", "kimi-k2-0905-preview"],
419
+ models: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5"],
397
420
  noReasoningModels: KIMI_THINKING_MODELS,
398
421
  modelReasoningEfforts: Object.fromEntries(KIMI_THINKING_MODELS.map(id => [id, []])),
399
422
  noTemperatureModels: KIMI_LOCKED_PARAMETER_MODELS,
@@ -412,7 +435,6 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
412
435
  models: ["glm-5.2", "glm-5.2[1m]", "glm-5.1", "glm-5", "glm-4.6"],
413
436
  noVisionModels: ZAI_GLM_52_MODELS,
414
437
  modelReasoningEfforts: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, ZAI_GLM_52_REASONING_EFFORTS])),
415
- modelReasoningEffortMap: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, ZAI_GLM_52_REASONING_MAP])),
416
438
  preserveReasoningContentModels: ZAI_GLM_52_MODELS,
417
439
  },
418
440
  { id: "nanogpt", label: "NanoGPT", baseUrl: "https://nano-gpt.com/api/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://nano-gpt.com/api" },
@@ -430,7 +452,6 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
430
452
  adapter: "openai-chat",
431
453
  authKind: "key",
432
454
  dashboardUrl: "https://ollama.com/settings/keys",
433
- reasoningEffortMap: OLLAMA_REASONING_MAP,
434
455
  models: ["glm-5.2", "deepseek-v4-pro", "qwen3-coder", "gpt-oss:120b", "kimi-k2.6", "minimax-m3", "qwen3.5", "gemma4"],
435
456
  defaultModel: "glm-5.2",
436
457
  noVisionModels: [
@@ -1,11 +1,14 @@
1
1
  import type { OcxProviderConfig } from "./types";
2
2
  import { modelInList } from "./types";
3
3
 
4
+ // Descriptions mirror the upstream bundled models.json canonical wording (openai/codex PR #31684).
4
5
  export const CODEX_REASONING_LEVELS: { effort: string; description: string }[] = [
5
6
  { effort: "low", description: "Fast responses with lighter reasoning" },
6
- { effort: "medium", description: "Balances speed and reasoning depth" },
7
+ { effort: "medium", description: "Balances speed and reasoning depth for everyday tasks" },
7
8
  { effort: "high", description: "Greater reasoning depth for complex problems" },
8
- { effort: "xhigh", description: "Extended reasoning for the hardest problems" },
9
+ { effort: "xhigh", description: "Extra high reasoning depth for complex problems" },
10
+ { effort: "max", description: "Maximum reasoning depth for the hardest problems" },
11
+ { effort: "ultra", description: "Maximum reasoning with automatic task delegation" },
9
12
  ];
10
13
 
11
14
  const CODEX_REASONING_ORDER = CODEX_REASONING_LEVELS.map(l => l.effort);
@@ -45,15 +48,28 @@ export function sanitizeCodexReasoningEfforts(efforts: readonly string[] | undef
45
48
  export function configuredReasoningEfforts(provider: OcxProviderConfig, modelId: string): string[] | undefined {
46
49
  if (modelInList(provider.noReasoningModels, modelId)) return [];
47
50
  const modelEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId);
48
- if (modelEfforts !== undefined) return sanitizeCodexReasoningEfforts(modelEfforts) ?? [];
49
- if (provider.reasoningEfforts !== undefined) return sanitizeCodexReasoningEfforts(provider.reasoningEfforts) ?? [];
51
+ if (modelEfforts !== undefined) return healMaxTier(provider, modelId, sanitizeCodexReasoningEfforts(modelEfforts) ?? []);
52
+ if (provider.reasoningEfforts !== undefined) return healMaxTier(provider, modelId, sanitizeCodexReasoningEfforts(provider.reasoningEfforts) ?? []);
50
53
  return undefined;
51
54
  }
52
55
 
56
+ /**
57
+ * Stale-ladder self-heal: saved configs seeded before `max` became a native Codex level can
58
+ * advertise a ladder that stops at `xhigh` while the wire map already routes xhigh -> max
59
+ * (e.g. opencode-go glm-5.2, deepseek thinking models). When the map proves the provider
60
+ * accepts wire `max`, append `max` so the picker actually shows the top tier. Thinking-toggle
61
+ * maps (xhigh -> "enabled") never match, so binary-toggle models stay two-step.
62
+ */
63
+ function healMaxTier(provider: OcxProviderConfig, modelId: string, efforts: string[]): string[] {
64
+ if (efforts.includes("max") || !efforts.includes("xhigh")) return efforts;
65
+ const wireMap = reasoningEffortMapFor(provider, modelId);
66
+ if (wireMap?.xhigh !== "max" && wireMap?.max !== "max") return efforts;
67
+ return sanitizeCodexReasoningEfforts([...efforts, "max"]) ?? efforts;
68
+ }
69
+
53
70
  function requestToCodexEffort(requested: string): string | undefined {
54
71
  if (requested === "none") return undefined;
55
72
  if (requested === "minimal") return "low";
56
- if (requested === "max") return "xhigh";
57
73
  return CODEX_REASONING_SET.has(requested) ? requested : undefined;
58
74
  }
59
75
 
@@ -82,21 +98,27 @@ export function reasoningEffortMapFor(provider: OcxProviderConfig, modelId: stri
82
98
  }
83
99
 
84
100
  /**
85
- * Translate Codex's reasoning label into the provider's real wire value. The Codex catalog must only
86
- * advertise labels Codex itself accepts (`low`/`medium`/`high`/`xhigh`), but some upstreams use
87
- * different values (`max`) or a smaller subset (`low`/`medium`/`high`).
101
+ * Translate Codex's reasoning label into the provider's real wire value. Prefer identity labels
102
+ * (`xhigh` stays `xhigh`, `max` stays `max`); provider maps are only for real upstream aliases.
88
103
  */
89
104
  export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string, requested: string | undefined): string | undefined {
90
105
  if (!requested) return undefined;
91
106
  if (modelInList(provider.noReasoningModels, modelId)) return undefined;
92
107
 
108
+ // Upstream codex-rs converts ultra -> max before ANY provider request (core/src/client.rs
109
+ // `reasoning_effort_for_request`), so "ultra" must never influence the provider wire — not even
110
+ // through a raw alias. Apply the boundary before alias/clamp resolution.
111
+ const boundary = requested === "ultra" ? "max" : requested;
112
+
93
113
  const wireMap = reasoningEffortMapFor(provider, modelId);
94
- if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, requested)) return wireMap[requested];
114
+ if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, boundary)) return wireMap[boundary];
95
115
 
96
116
  const supported = configuredReasoningEfforts(provider, modelId);
97
- const codexEffort = supported !== undefined ? clampToSupportedCodexEffort(requested, supported) : requestToCodexEffort(requested);
117
+ const codexEffort = supported !== undefined ? clampToSupportedCodexEffort(boundary, supported) : requestToCodexEffort(boundary);
98
118
  if (!codexEffort) return undefined;
99
119
 
100
- if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, codexEffort)) return wireMap[codexEffort];
101
- return codexEffort;
120
+ // Belt for the odd config where the supported ladder is ultra-only and the clamp lands on it.
121
+ const wire = codexEffort === "ultra" ? "max" : codexEffort;
122
+ if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, wire)) return wireMap[wire];
123
+ return wire;
102
124
  }
@@ -464,8 +464,13 @@ export function parseRequest(body: unknown): OcxParsedRequest {
464
464
  const tc = mapToolChoice(data.tool_choice);
465
465
  if (tc !== undefined) options.toolChoice = tc;
466
466
  if (data.parallel_tool_calls !== undefined) options.parallelToolCalls = data.parallel_tool_calls;
467
- if (data.reasoning?.effort && REASONING_EFFORTS.has(data.reasoning.effort)) {
468
- options.reasoning = data.reasoning.effort;
467
+ // Upstream codex-rs converts "ultra" to "max" at the inference boundary (core/src/client.rs
468
+ // `reasoning_effort_for_request`), so current clients never send it — but a catalog that
469
+ // advertises ultra plus an older/direct caller can. Degrade it to max like upstream instead of
470
+ // silently dropping reasoning altogether.
471
+ const requestedEffort = data.reasoning?.effort === "ultra" ? "max" : data.reasoning?.effort;
472
+ if (requestedEffort && REASONING_EFFORTS.has(requestedEffort)) {
473
+ options.reasoning = requestedEffort;
469
474
  }
470
475
  const summaryMode = data.reasoning?.summary;
471
476
  if (!summaryMode || summaryMode === "none") options.hideThinkingSummary = true;
package/src/router.ts CHANGED
@@ -29,9 +29,8 @@ const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string
29
29
  },
30
30
  ];
31
31
 
32
- // Merge registry-default effort maps under user values so persisted built-in provider configs
33
- // that predate reasoningEffortMap/modelReasoningEffortMap still get correct wire translations
34
- // (e.g. ollama-cloud xhigh -> max) without a disk migration. User overrides win per-key.
32
+ // Merge registry-default effort maps under user values so built-in provider configs can
33
+ // carry real upstream aliases without a disk migration. User overrides win per-key.
35
34
  function mergeRecord(
36
35
  seed: Record<string, string> | undefined,
37
36
  user: Record<string, string> | undefined,
@@ -97,6 +96,7 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
97
96
  const autoToolChoiceOnlyModels = mergeStringArray(registryEntry.autoToolChoiceOnlyModels, provider.autoToolChoiceOnlyModels);
98
97
  const preserveReasoningContentModels = mergeStringArray(registryEntry.preserveReasoningContentModels, provider.preserveReasoningContentModels);
99
98
  const thinkingToggleModels = mergeStringArray(registryEntry.thinkingToggleModels, provider.thinkingToggleModels);
99
+ const thinkingBudgetModels = mergeStringArray(registryEntry.thinkingBudgetModels, provider.thinkingBudgetModels);
100
100
 
101
101
  return {
102
102
  ...provider,
@@ -113,6 +113,9 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
113
113
  ...(provider.contextWindow === undefined && registryEntry.contextWindow !== undefined ? { contextWindow: registryEntry.contextWindow } : {}),
114
114
  ...(provider.reasoningEfforts === undefined && registryEntry.reasoningEfforts !== undefined ? { reasoningEfforts: registryEntry.reasoningEfforts } : {}),
115
115
  ...(provider.escapeBuiltinToolNames === undefined && registryEntry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: registryEntry.escapeBuiltinToolNames } : {}),
116
+ // Scalar backfill: a persisted config created before the flag shipped inherits the registry
117
+ // opt-in, while an explicit user `false` keeps overriding registry `true`.
118
+ ...(provider.parallelToolCalls === undefined && registryEntry.parallelToolCalls !== undefined ? { parallelToolCalls: registryEntry.parallelToolCalls } : {}),
116
119
  ...(modelContextWindows ? { modelContextWindows } : {}),
117
120
  ...(modelInputModalities ? { modelInputModalities } : {}),
118
121
  ...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
@@ -126,6 +129,7 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
126
129
  ...(autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels } : {}),
127
130
  ...(preserveReasoningContentModels ? { preserveReasoningContentModels } : {}),
128
131
  ...(thinkingToggleModels ? { thinkingToggleModels } : {}),
132
+ ...(thinkingBudgetModels ? { thinkingBudgetModels } : {}),
129
133
  };
130
134
  }
131
135
 
@@ -10,7 +10,7 @@ import type { OcxProviderConfig } from "../types";
10
10
  /** Providers whose listed model ids must be driven over the Anthropic wire even if the provider's
11
11
  * configured adapter is something else (the upstream only speaks Anthropic for these models). */
12
12
  const ANTHROPIC_WIRE_MODELS: Record<string, Set<string>> = {
13
- "opencode-go": new Set(["minimax-m2.5", "minimax-m2.7", "minimax-m3", "qwen3.5-plus", "qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus"]),
13
+ "opencode-go": new Set(["minimax-m2.5", "minimax-m2.7", "minimax-m3"]),
14
14
  };
15
15
 
16
16
  /** Return a provider config whose adapter is forced to "anthropic" when the model id is wire-pinned. */