@yansigit/opencodex 2.33.0 → 2.35.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 (196) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +141 -23
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +162 -0
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +17 -1
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  17. package/src/adapters/cursor/native-exec-network.ts +3 -5
  18. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  19. package/src/adapters/cursor/native-exec-shell.ts +116 -31
  20. package/src/adapters/cursor/native-exec.ts +38 -10
  21. package/src/adapters/cursor/protobuf-events.ts +28 -2
  22. package/src/adapters/cursor/protobuf-request.ts +93 -41
  23. package/src/adapters/cursor/request-builder.ts +39 -10
  24. package/src/adapters/cursor/tool-definitions.ts +27 -3
  25. package/src/adapters/cursor/tool-result-normalize.ts +51 -6
  26. package/src/adapters/cursor/types.ts +23 -4
  27. package/src/adapters/cursor.ts +170 -29
  28. package/src/adapters/google-aistudio-parser.ts +49 -0
  29. package/src/adapters/google-antigravity-replay.ts +105 -25
  30. package/src/adapters/google-antigravity-wire.ts +5 -0
  31. package/src/adapters/google-errors.ts +41 -12
  32. package/src/adapters/google-http.ts +12 -11
  33. package/src/adapters/google.ts +219 -36
  34. package/src/adapters/image.ts +1 -1
  35. package/src/adapters/kiro-constants.ts +15 -0
  36. package/src/adapters/kiro-tools.ts +43 -15
  37. package/src/adapters/kiro.ts +54 -9
  38. package/src/adapters/openai-chat.ts +286 -242
  39. package/src/adapters/openai-responses.ts +335 -24
  40. package/src/adapters/run-turn-queue.ts +36 -1
  41. package/src/adapters/tool-catalog-nudge.ts +2 -2
  42. package/src/adapters/xai-tool-schema.ts +436 -0
  43. package/src/bridge.ts +67 -26
  44. package/src/chat/inbound.ts +29 -1
  45. package/src/chat/outbound.ts +15 -7
  46. package/src/claude/agents-inject.ts +8 -1
  47. package/src/claude/outbound.ts +10 -8
  48. package/src/cli/account-api.ts +27 -7
  49. package/src/cli/account-extended.ts +10 -3
  50. package/src/cli/account.ts +29 -5
  51. package/src/cli/alias.ts +66 -0
  52. package/src/cli/claude.ts +26 -1
  53. package/src/cli/dispatch.ts +13 -1
  54. package/src/cli/help.ts +1 -0
  55. package/src/cli/index.ts +6 -1
  56. package/src/cli/init.ts +1 -0
  57. package/src/cli/models-runtime.ts +95 -0
  58. package/src/cli/models.ts +13 -7
  59. package/src/cli/provider-runtime.ts +16 -2
  60. package/src/cli/registry.ts +6 -1
  61. package/src/cli/telemetry-commands.ts +25 -0
  62. package/src/cli/v2.ts +34 -10
  63. package/src/codex/account-pause.ts +2 -1
  64. package/src/codex/account-priority.ts +3 -2
  65. package/src/codex/app-server-processes.ts +80 -6
  66. package/src/codex/auth-api.ts +48 -8
  67. package/src/codex/auth-context.ts +21 -18
  68. package/src/codex/catalog/aggregation.ts +6 -0
  69. package/src/codex/catalog/model-metadata.ts +13 -1
  70. package/src/codex/catalog/native-models.ts +5 -2
  71. package/src/codex/catalog/parsing.ts +16 -0
  72. package/src/codex/catalog/provider-fetch.ts +20 -3
  73. package/src/codex/catalog/sync.ts +127 -2
  74. package/src/codex/catalog.ts +1 -1
  75. package/src/codex/codex-write-lock.ts +3 -1
  76. package/src/codex/convergence-types.ts +1 -1
  77. package/src/codex/convergence.ts +22 -2
  78. package/src/codex/desired-state.ts +2 -2
  79. package/src/codex/desktop-app-restart.ts +18 -5
  80. package/src/codex/inject-coordination.ts +83 -0
  81. package/src/codex/inject.ts +14 -1
  82. package/src/codex/log-guard/inspect.ts +22 -4
  83. package/src/codex/model-entitlements.ts +9 -2
  84. package/src/codex/prompt-layers.ts +371 -25
  85. package/src/codex/prompt-text-probe.ts +238 -0
  86. package/src/codex/quota.ts +123 -18
  87. package/src/codex/routing.ts +9 -0
  88. package/src/codex/subagent-model-fallback.ts +198 -27
  89. package/src/codex/transition-state.ts +107 -8
  90. package/src/combos/types.ts +10 -0
  91. package/src/compatibility/openai-responses.ts +33 -1
  92. package/src/config/autonomous-remediation.ts +21 -0
  93. package/src/config/provider-validation.ts +14 -0
  94. package/src/config/rebase-provenance.ts +68 -0
  95. package/src/config.ts +191 -17
  96. package/src/generated/compatibility-version.json +279 -159
  97. package/src/generated/model-metadata.ts +3 -0
  98. package/src/images/loop.ts +5 -4
  99. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  100. package/src/lab/fabric/producer-child.ts +1 -1
  101. package/src/lib/config-ownership.ts +20 -0
  102. package/src/lib/errors.ts +11 -2
  103. package/src/lib/package-tree-integrity.ts +101 -0
  104. package/src/oauth/aistudio-credentials.ts +65 -0
  105. package/src/oauth/aistudio-native-daemon.ts +116 -0
  106. package/src/oauth/aistudio-session-sync.ts +95 -0
  107. package/src/oauth/generic-account-failover.ts +231 -0
  108. package/src/oauth/google-aistudio-auth.ts +98 -0
  109. package/src/oauth/index.ts +57 -5
  110. package/src/oauth/key-providers.ts +18 -1
  111. package/src/oauth/kiro.ts +45 -0
  112. package/src/oauth/login-cli.ts +65 -1
  113. package/src/oauth/types.ts +15 -0
  114. package/src/providers/codex-capacity.ts +5 -2
  115. package/src/providers/command-code-efforts.ts +38 -6
  116. package/src/providers/context-cap.ts +4 -3
  117. package/src/providers/default-aliases.ts +65 -0
  118. package/src/providers/derive.ts +29 -1
  119. package/src/providers/fastwire.ts +7 -1
  120. package/src/providers/model-presets.ts +119 -0
  121. package/src/providers/new-model-policy.ts +146 -0
  122. package/src/providers/provider-id-rewrite.ts +2 -1
  123. package/src/providers/quota.ts +157 -46
  124. package/src/providers/registry.ts +184 -71
  125. package/src/providers/slug-codec.ts +52 -0
  126. package/src/responses/code-mode-helper-compat.ts +50 -0
  127. package/src/responses/custom-tool-compat.ts +34 -10
  128. package/src/responses/parser.ts +4 -0
  129. package/src/responses/schema.ts +5 -1
  130. package/src/responses/thought-signature-replay.ts +17 -0
  131. package/src/router.ts +43 -2
  132. package/src/routing/account-pool/cooldown.ts +8 -0
  133. package/src/routing/account-pool/index.ts +1 -0
  134. package/src/routing/analytics.ts +1 -0
  135. package/src/routing/quota.ts +10 -0
  136. package/src/server/auth-cors.ts +24 -0
  137. package/src/server/chat-completions.ts +26 -16
  138. package/src/server/chat-native-sse.ts +3 -3
  139. package/src/server/chat-native.ts +30 -11
  140. package/src/server/claude-messages.ts +1 -1
  141. package/src/server/effort-policy.ts +16 -0
  142. package/src/server/index.ts +180 -14
  143. package/src/server/lifecycle.ts +52 -1
  144. package/src/server/management/agent-settings-routes.ts +31 -15
  145. package/src/server/management/codex-prompt-routes.ts +570 -0
  146. package/src/server/management/combo-routes.ts +2 -1
  147. package/src/server/management/config-routes.ts +27 -9
  148. package/src/server/management/context.ts +9 -0
  149. package/src/server/management/logs-usage-routes.ts +11 -5
  150. package/src/server/management/model-routes.ts +266 -0
  151. package/src/server/management/oauth-account-routes.ts +13 -3
  152. package/src/server/management/provider-routes.ts +137 -3
  153. package/src/server/management/routing-profile-routes.ts +2 -2
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/port-reclaim.ts +19 -1
  156. package/src/server/relay-eager.ts +147 -20
  157. package/src/server/relay.ts +251 -19
  158. package/src/server/request-log-conversation.ts +33 -0
  159. package/src/server/request-log.ts +48 -21
  160. package/src/server/responses/collaboration.ts +42 -5
  161. package/src/server/responses/combo-stream-preflight.ts +10 -3
  162. package/src/server/responses/core.ts +575 -140
  163. package/src/server/responses/empty-completion-guard.ts +35 -0
  164. package/src/server/responses/fetch-helpers.ts +14 -6
  165. package/src/server/responses/input-admission.ts +3 -1
  166. package/src/server/responses/passthrough-error.ts +33 -9
  167. package/src/server/responses/policy-fallback.ts +1 -1
  168. package/src/server/responses/responses-field-backfill.ts +105 -13
  169. package/src/server/responses/ws-upstream.ts +35 -5
  170. package/src/server/responses-custom-tool-repair.ts +52 -7
  171. package/src/server/responses-terminal-repair.ts +25 -4
  172. package/src/server/sse-frame-buffer.ts +31 -4
  173. package/src/server/ws-bridge.ts +14 -2
  174. package/src/smoke/fingerprint-cache.ts +133 -0
  175. package/src/smoke/live-scenarios.ts +33 -0
  176. package/src/smoke/runner.ts +119 -0
  177. package/src/telemetry/dispatcher.ts +44 -0
  178. package/src/telemetry/fingerprint.ts +24 -0
  179. package/src/telemetry/hook.ts +43 -0
  180. package/src/telemetry/ledger.ts +54 -0
  181. package/src/telemetry/types.ts +23 -0
  182. package/src/types/config.ts +66 -14
  183. package/src/types/provider.ts +79 -1
  184. package/src/types/request.ts +18 -10
  185. package/src/types/tools.ts +30 -11
  186. package/src/types.ts +1 -0
  187. package/src/usage/command-code-manifest.ts +116 -0
  188. package/src/usage/cost.ts +2 -2
  189. package/src/usage/expected-prices.ts +126 -24
  190. package/src/usage/log.ts +18 -8
  191. package/src/usage/summary.ts +34 -12
  192. package/src/web-search/exa-executor.ts +40 -9
  193. package/src/web-search/index.ts +16 -8
  194. package/src/web-search/loop.ts +5 -4
  195. package/gui/dist/assets/index-DKLr4LTE.js +0 -102
  196. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
@@ -14,6 +14,9 @@ export const CODEX_CAPACITY_MAX_QUOTA_AGE_MS = 30 * 60_000;
14
14
  export type CodexCapacityQuota = {
15
15
  fiveHourPercent?: number;
16
16
  fiveHourResetAt?: number;
17
+ shortPercent?: number;
18
+ shortResetAt?: number;
19
+ shortWindowSeconds?: number;
17
20
  weeklyPercent?: number;
18
21
  weeklyResetAt?: number;
19
22
  monthlyPercent?: number;
@@ -106,7 +109,7 @@ function futureResetMs(value: unknown, now: number): number | undefined {
106
109
 
107
110
  function hasKnownQuotaWindow(quota: CodexCapacityQuota | null): quota is CodexCapacityQuota {
108
111
  if (!quota) return false;
109
- return normalizedPercent(quota.fiveHourPercent) !== undefined
112
+ return normalizedPercent(quota.fiveHourPercent ?? quota.shortPercent) !== undefined
110
113
  || normalizedPercent(quota.weeklyPercent) !== undefined
111
114
  || normalizedPercent(quota.monthlyPercent) !== undefined
112
115
  || !!quota.customWindows?.some(window => normalizedPercent(window.percent) !== undefined);
@@ -197,7 +200,7 @@ export function aggregateCodexPoolCapacity(
197
200
  && now - quota.updatedAt <= CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
198
201
  if (quota && !quotaFresh) staleQuotaAccounts += 1;
199
202
  const standard = quota ? [
200
- ["fiveHour", quota.fiveHourPercent, quota.fiveHourResetAt],
203
+ ["fiveHour", quota.fiveHourPercent ?? quota.shortPercent, quota.fiveHourResetAt ?? quota.shortResetAt],
201
204
  ["weekly", quota.weeklyPercent, quota.weeklyResetAt],
202
205
  ["monthly", quota.monthlyPercent, quota.monthlyResetAt],
203
206
  ] as const : [];
@@ -9,12 +9,44 @@ const COMMAND_CODE_MODEL_EFFORTS = {
9
9
  efforts: ["high", "max"],
10
10
  profileUrl: "https://commandcode.ai/models/deepseek-v4-flash",
11
11
  },
12
- // Ox Alpha (stealth preview, added in Command Code v1.31.0): free 1M-context
13
- // reasoning model on every plan. The profile does not publish an effort ladder,
14
- // so mirror the OpenRouter contract (reasoning mandatory; max/high/low).
15
- "stealth/ox-alpha": {
16
- efforts: ["low", "high", "max"],
17
- profileUrl: "https://commandcode.ai/models/ox-alpha",
12
+ /*
13
+ * Three live routes that reached the catalog without an effort ladder (#2647).
14
+ * Without a row here the model advertises no efforts at all, so a client that
15
+ * sends one gets it stripped or rejected rather than honored.
16
+ *
17
+ * PROVENANCE, stated plainly: these three ladders are the reporter's
18
+ * (darwintree, #2647), recorded as reported and NOT independently verified.
19
+ * All three profileUrls return HTTP 200, but commandcode.ai renders these
20
+ * pages client-side and ships the ladder inside a serialized React payload
21
+ * whose `reasoningEfforts` array is EMPTY in the delivered HTML. There is no
22
+ * fetchable statement of these ladders to check them against.
23
+ *
24
+ * Do not assume the refresh path launders this. It does not:
25
+ * `parsedProfileEfforts` below matches prose of the form
26
+ * "Reasoning efforts ... are supported;", and `grep -c -i 'reasoning efforts'`
27
+ * against the live pages returns 0 — for these three AND for the older rows
28
+ * (deepseek-v4-pro, GLM-5.3, muse-spark-1.2 all measured 0 on 2026-08-27).
29
+ * So `refreshCommandCodeReasoningEfforts` returns undefined and the caller
30
+ * keeps whatever is written here, indefinitely. The self-correction mechanism
31
+ * is currently dead for EVERY row in this table, which is a pre-existing
32
+ * defect worth its own fix (teach the parser to read the embedded payload),
33
+ * not something these three rows introduced.
34
+ *
35
+ * The practical consequence: a wrong ladder here stays wrong until a human
36
+ * changes it. It degrades safely — an effort the upstream rejects surfaces as
37
+ * an error rather than silent corruption — but it does not self-heal.
38
+ */
39
+ "deepseek/deepseek-v4-flash-vision-exp": {
40
+ efforts: ["high", "max"],
41
+ profileUrl: "https://commandcode.ai/models/deepseek-v4-flash-vision-exp",
42
+ },
43
+ "gpt-5.6-luna": {
44
+ efforts: ["low", "medium", "high", "xhigh", "max"],
45
+ profileUrl: "https://commandcode.ai/models/gpt-5-6-luna",
46
+ },
47
+ "google/gemini-3.7-flash": {
48
+ efforts: ["low", "medium", "high"],
49
+ profileUrl: "https://commandcode.ai/models/gemini-3-7-flash",
18
50
  },
19
51
  // Keys must match the EXACT upstream /provider/v1/models ids (GLM ships as
20
52
  // `zai-org/GLM-5.3`, not `zai-org/glm-5.3`). The table doubles as the router's
@@ -1,4 +1,5 @@
1
1
  import type { OcxConfig } from "../types";
2
+ import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
2
3
 
3
4
  export const DEFAULT_PROVIDER_CONTEXT_CAP = 350_000;
4
5
 
@@ -50,7 +51,7 @@ export function setProviderContextCap(config: OcxConfig, provider: string, enabl
50
51
  delete next[provider];
51
52
  }
52
53
  if (Object.keys(next).length > 0) config.providerContextCaps = next;
53
- else delete config.providerContextCaps;
54
+ else deleteConfigTopLevelKey(config, "providerContextCaps");
54
55
  }
55
56
 
56
57
  /**
@@ -71,12 +72,12 @@ export function setGlobalContextCapValue(config: OcxConfig, value: number, apply
71
72
  /** Enable the cap for every named provider at the current value, or clear all caps. */
72
73
  export function setAllProviderContextCaps(config: OcxConfig, providerNames: string[], enabled: boolean): void {
73
74
  if (!enabled) {
74
- delete config.providerContextCaps;
75
+ deleteConfigTopLevelKey(config, "providerContextCaps");
75
76
  return;
76
77
  }
77
78
  const value = globalContextCapValue(config);
78
79
  const next: Record<string, number> = {};
79
80
  for (const name of providerNames) next[name] = value;
80
81
  if (Object.keys(next).length > 0) config.providerContextCaps = next;
81
- else delete config.providerContextCaps;
82
+ else deleteConfigTopLevelKey(config, "providerContextCaps");
82
83
  }
@@ -0,0 +1,65 @@
1
+ import type { OcxConfig, OcxProviderConfig } from "../types";
2
+
3
+ export const MODEL_ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
4
+
5
+ /** Ordered, most-specific-first built-in aliases. Updated with the provider registry. */
6
+ export const DEFAULT_MODEL_ALIASES: ReadonlyArray<{ match: RegExp; alias: string }> = [
7
+ { match: /^claude-opus-5/, alias: "opus" },
8
+ { match: /^claude-sonnet-5/, alias: "sonnet" },
9
+ { match: /^claude-haiku/, alias: "haiku" },
10
+ { match: /^gemini-3(?:\.\d+)?-pro/, alias: "g3p" },
11
+ { match: /^gemini-3(?:\.\d+)?-flash/, alias: "g3f" },
12
+ { match: /^deepseek-v4/, alias: "ds4" },
13
+ { match: /^grok-4/, alias: "grok" },
14
+ ];
15
+
16
+ export interface EffectiveModelAlias { alias: string; source: "user" | "builtin" }
17
+
18
+ function builtinRule(id: string): { match: RegExp; alias: string } | undefined {
19
+ const tail = id.slice(id.lastIndexOf("/") + 1);
20
+ return DEFAULT_MODEL_ALIASES.find(rule => rule.match.test(id) || rule.match.test(tail));
21
+ }
22
+
23
+ export function defaultAliasesEnabled(config: Pick<OcxConfig, "defaultModelAliases">, provider: OcxProviderConfig): boolean {
24
+ return provider.defaultAliases ?? config.defaultModelAliases ?? false;
25
+ }
26
+
27
+ export function effectiveModelAliases(
28
+ config: Pick<OcxConfig, "defaultModelAliases">,
29
+ provider: OcxProviderConfig,
30
+ knownIds: Iterable<string>,
31
+ ): Map<string, EffectiveModelAlias> {
32
+ const result = new Map<string, EffectiveModelAlias>();
33
+ for (const [id, alias] of Object.entries(provider.modelAliases ?? {})) {
34
+ if (typeof alias === "string" && MODEL_ALIAS_PATTERN.test(alias)) result.set(id, { alias, source: "user" });
35
+ }
36
+ if (!defaultAliasesEnabled(config, provider)) return result;
37
+ const claims = new Map<string, string[]>();
38
+ for (const id of knownIds) {
39
+ if (result.has(id)) continue;
40
+ const rule = builtinRule(id);
41
+ if (!rule) continue;
42
+ const key = rule.alias.toLowerCase();
43
+ claims.set(key, [...(claims.get(key) ?? []), id]);
44
+ }
45
+ for (const [alias, ids] of claims) {
46
+ if (ids.length !== 1) continue;
47
+ const id = ids[0]!;
48
+ // Catalog drift must skip, never shadow, a native id or explicit user alias.
49
+ if ([...knownIds].some(candidate => candidate.toLowerCase() === alias)) continue;
50
+ if ([...result.values()].some(value => value.alias.toLowerCase() === alias)) continue;
51
+ result.set(id, { alias: builtinRule(id)!.alias, source: "builtin" });
52
+ }
53
+ return result;
54
+ }
55
+
56
+ export function resolveModelAlias(
57
+ config: Pick<OcxConfig, "defaultModelAliases">,
58
+ provider: OcxProviderConfig,
59
+ knownIds: Iterable<string>,
60
+ requested: string,
61
+ ): string | undefined {
62
+ const needle = requested.toLowerCase();
63
+ return [...effectiveModelAliases(config, provider, knownIds)]
64
+ .find(([, value]) => value.alias.toLowerCase() === needle)?.[0];
65
+ }
@@ -49,7 +49,7 @@ export interface DerivedKeyLoginProvider {
49
49
  thinkingBudgetModels?: string[];
50
50
  escapeBuiltinToolNames?: boolean;
51
51
  openaiChatEofTolerance?: boolean;
52
- googleMode?: "ai-studio" | "vertex" | "cloud-code-assist";
52
+ googleMode?: "ai-studio" | "vertex" | "cloud-code-assist" | "ai-studio-web";
53
53
  project?: string;
54
54
  location?: string;
55
55
  }
@@ -392,6 +392,32 @@ function serviceTierModelDefaultsFor(
392
392
  : undefined;
393
393
  }
394
394
 
395
+ /**
396
+ * Materialize the registry's verbosity opt-out into the provider config at seed/enrich time.
397
+ *
398
+ * The catalog hint pass must not read PROVIDER_REGISTRY: a gather flight captures its registry
399
+ * authority up front and forbids any later read, so consulting the registry per model turned
400
+ * every hint pass into a post-lookup read and dropped a custom-destination flight's own
401
+ * discovery result (tests/codex-gather-authority.test.ts).
402
+ *
403
+ * Registry values go in first so an explicit user entry still wins, matching
404
+ * `applyReasoningSummaryDefaults`. `supportsVerbosity` is the provider-wide default, expanded
405
+ * across the seeded model list so an id discovered later still inherits it through the same
406
+ * Record the hint pass already reads.
407
+ */
408
+ function applyVerbosityDefaults(prov: OcxProviderConfig, entry: ProviderRegistryEntry | undefined): void {
409
+ if (!entry) return;
410
+ const perModel = entry.modelSupportsVerbosity;
411
+ if (!perModel && entry.supportsVerbosity === undefined) return;
412
+ prov.modelSupportsVerbosity = {
413
+ ...(perModel ?? {}),
414
+ ...(prov.modelSupportsVerbosity ?? {}),
415
+ };
416
+ if (entry.supportsVerbosity !== undefined) {
417
+ prov.supportsVerbosity ??= entry.supportsVerbosity;
418
+ }
419
+ }
420
+
395
421
  /**
396
422
  * Last-resort enrichment for a provider whose NAME matches no registry id.
397
423
  *
@@ -429,6 +455,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
429
455
  // destinations, so a templated or overridable base URL cannot be claimed by it.
430
456
  enrichReasoningSummariesByDestination(prov);
431
457
  applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(registryEntryForProviderDestination(prov), prov));
458
+ applyVerbosityDefaults(prov, registryEntryForProviderDestination(prov));
432
459
  return;
433
460
  }
434
461
  const explicitDirectReasoning: DirectReasoningEffortOverrides = {
@@ -493,6 +520,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
493
520
  if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
494
521
  applyReasoningSummaryDefaults(prov, entry.modelSupportsReasoningSummaries);
495
522
  applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(entry, prov));
523
+ applyVerbosityDefaults(prov, entry);
496
524
  // Registry-only repair policy (#938): fill only when the runtime provider has
497
525
  // no explicit policy, and deep-clone so saved/user values never alias the
498
526
  // registry constant.
@@ -246,6 +246,7 @@ export function tierObservationContext(
246
246
  policy: ResolvedFastPolicy,
247
247
  fastMode: boolean | undefined,
248
248
  callerTier: string | undefined,
249
+ responseTierAuthoritative?: boolean,
249
250
  ): TierObservationContext {
250
251
  return {
251
252
  capability: policy.capability,
@@ -253,6 +254,7 @@ export function tierObservationContext(
253
254
  fastWire: policy.fastWire,
254
255
  demandDecision: fastMode === true ? "force-fast" : fastMode === false ? "force-default" : "inherit",
255
256
  ...(callerTier !== undefined ? { callerTier } : {}),
257
+ ...(responseTierAuthoritative !== undefined ? { responseTierAuthoritative } : {}),
256
258
  };
257
259
  }
258
260
 
@@ -344,7 +346,11 @@ export function createAdapterTierMetadata(
344
346
 
345
347
  const responseCanConfirmFast = effectiveFastRequested
346
348
  && context.eligibility === "eligible"
347
- && wireValue !== null;
349
+ && wireValue !== null
350
+ // A destination whose echo is not authoritative can neither confirm nor deny Fast. The
351
+ // ChatGPT-internal Codex backend echoes "default" on priority-scheduled turns, so believing
352
+ // it reported every Fast request as `response-declined` (#2558).
353
+ && context.responseTierAuthoritative !== false;
348
354
  return {
349
355
  outcome,
350
356
  observeResponseServiceTier(value: unknown) {
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Shipped per-provider "latest/core" model presets (#2465).
3
+ *
4
+ * Adding a high-volume provider exposes its entire catalog to the Codex picker on day one:
5
+ * openrouter ships 400+ rows, an Anthropic provider ships every historical snapshot. The useful
6
+ * set is a handful of current flagships, so a newly added provider seeds from a curated preset
7
+ * and "everything" stays one click away.
8
+ *
9
+ * Rules are ID PATTERNS rather than literal id lists so a vendor snapshot suffix does not stale
10
+ * the preset between releases: `claude-opus-5-20260814` and `claude-opus-5` both match one rule.
11
+ *
12
+ * The preset is a SEED, not a lock. Materialization evaluates these rules against the provider's
13
+ * current catalog and stores CONCRETE ids in `selectedModels`, so the visibility hot path
14
+ * (`filterCatalogVisibleModels`) never learns about patterns and an older binary still sees a
15
+ * plain allowlist. Design: devlog/_plan/260824_model_ux_aliases_and_defaults/030_default_preset.md
16
+ */
17
+
18
+ import type { OcxProviderConfig } from "../types";
19
+
20
+ export interface ModelPresetRule {
21
+ readonly pattern: RegExp;
22
+ }
23
+
24
+ export interface ModelPreset {
25
+ /** Bumped whenever this provider's rules change; drives upgrade reconciliation. */
26
+ readonly version: number;
27
+ readonly rules: readonly ModelPresetRule[];
28
+ }
29
+
30
+ /**
31
+ * Curated on the same release train as the provider registry. A provider WITHOUT an entry has no
32
+ * preset and behaves exactly as today (mode "all").
33
+ *
34
+ * Deliberately limited to high-volume catalogs. A provider that already ships a short curated
35
+ * list gains nothing from a preset and would only add a marker to reconcile.
36
+ */
37
+ export const MODEL_PRESETS: Readonly<Record<string, ModelPreset>> = Object.freeze({
38
+ openrouter: {
39
+ version: 1,
40
+ rules: [
41
+ { pattern: /^anthropic\/claude-(opus|sonnet|haiku)-[45]/ },
42
+ { pattern: /^google\/gemini-3(\.\d+)?-(pro|flash)/ },
43
+ { pattern: /^openai\/gpt-5(\.\d+)?/ },
44
+ { pattern: /^deepseek\/deepseek-(v4|r2)/ },
45
+ { pattern: /^x-ai\/grok-4(\.\d+)?/ },
46
+ { pattern: /^moonshotai\/kimi-k[23]/ },
47
+ { pattern: /^z-ai\/glm-[45]/ },
48
+ ],
49
+ },
50
+ anthropic: {
51
+ version: 1,
52
+ rules: [
53
+ { pattern: /^claude-opus-[45]/ },
54
+ { pattern: /^claude-sonnet-[45]/ },
55
+ { pattern: /^claude-haiku-[45]/ },
56
+ { pattern: /^claude-fable-5/ },
57
+ ],
58
+ },
59
+ "anthropic-apikey": {
60
+ version: 1,
61
+ rules: [
62
+ { pattern: /^claude-opus-[45]/ },
63
+ { pattern: /^claude-sonnet-[45]/ },
64
+ { pattern: /^claude-haiku-[45]/ },
65
+ { pattern: /^claude-fable-5/ },
66
+ ],
67
+ },
68
+ });
69
+
70
+ /** The preset for a provider, or undefined when none is shipped. */
71
+ export function modelPresetFor(providerName: string): ModelPreset | undefined {
72
+ return MODEL_PRESETS[providerName];
73
+ }
74
+
75
+ /** True when a provider has a shipped preset at all. */
76
+ export function hasModelPreset(providerName: string): boolean {
77
+ return modelPresetFor(providerName) !== undefined;
78
+ }
79
+
80
+ /**
81
+ * Evaluate a provider's rules against concrete catalog ids.
82
+ *
83
+ * Input order is preserved so a caller can show the preset in the same order the picker does.
84
+ * Duplicates are collapsed; a model matching several rules is selected once.
85
+ */
86
+ export function materializeModelPreset(
87
+ providerName: string,
88
+ catalogModelIds: Iterable<string>,
89
+ ): string[] {
90
+ const preset = modelPresetFor(providerName);
91
+ if (!preset) return [];
92
+ const seen = new Set<string>();
93
+ const out: string[] = [];
94
+ for (const id of catalogModelIds) {
95
+ if (seen.has(id)) continue;
96
+ // `RegExp.test` on a shared literal is safe here because no rule uses the `g` flag; a
97
+ // sticky/global pattern would carry lastIndex between calls and skip rows.
98
+ if (preset.rules.some(rule => rule.pattern.test(id))) {
99
+ seen.add(id);
100
+ out.push(id);
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+
106
+
107
+ /**
108
+ * Flip a provider out of preset mode when the user edits its selection (#2465).
109
+ *
110
+ * No-op unless the provider is currently in preset mode: "all" has no marker to keep, and
111
+ * "custom" is already terminal. The applied version is retained so the GUI can still offer
112
+ * "a newer preset is available" without losing what the user chose.
113
+ */
114
+ export function markModelPresetDiverged(provider: OcxProviderConfig): void {
115
+ const marker = provider.modelPreset;
116
+ if (marker?.mode !== "preset") return;
117
+ provider.modelPreset = { ...marker, mode: "custom" };
118
+ }
119
+
@@ -0,0 +1,146 @@
1
+ import type { OcxConfig } from "../types";
2
+ import { routedSlug } from "./slug-codec";
3
+
4
+ export const MODEL_REMOVAL_GRACE_FETCHES = 3;
5
+ export const MAX_KNOWN_MODELS_PER_PROVIDER = 2_000;
6
+ export const MAX_RECENT_ARRIVALS_PER_PROVIDER = 50;
7
+
8
+ export type KnownModelBaseline = NonNullable<NonNullable<OcxConfig["modelDiscovery"]>["knownModels"]>[string];
9
+ export type NewModelPolicy = "on" | "off";
10
+
11
+ export interface NewModelPolicyResult {
12
+ newIds: string[];
13
+ nextBaseline: KnownModelBaseline;
14
+ slugsToDisable: string[];
15
+ arrivals: Array<{ id: string; at: string }>;
16
+ overflow: boolean;
17
+ }
18
+
19
+ /**
20
+ * Whether a transition actually changes persisted state.
21
+ *
22
+ * `updatedAt` moves on every successful fetch, so comparing whole baselines would report a
23
+ * change every time and rewrite config.json on every catalog convergence — a write amplification
24
+ * that also churns the config generation other writers revalidate against. Only the fields that
25
+ * carry meaning are compared.
26
+ */
27
+ function baselineDiffers(prior: KnownModelBaseline | undefined, next: KnownModelBaseline): boolean {
28
+ if (!prior) return true;
29
+ const sameList = (a: readonly string[], b: readonly string[]) =>
30
+ a.length === b.length && a.every((value, index) => value === b[index]);
31
+ if (!sameList(prior.ids, next.ids) || !sameList(prior.removed, next.removed)) return true;
32
+ const priorMissing = prior.missing ?? {};
33
+ const nextMissing = next.missing ?? {};
34
+ const keys = new Set([...Object.keys(priorMissing), ...Object.keys(nextMissing)]);
35
+ for (const key of keys) if (priorMissing[key] !== nextMissing[key]) return true;
36
+ return false;
37
+ }
38
+
39
+ /** Pure successful-discovery transition. An absent baseline bootstraps without hiding anything. */
40
+ export function applyNewModelPolicy(options: {
41
+ provider: string;
42
+ discoveredIds: Iterable<string>;
43
+ baseline?: KnownModelBaseline;
44
+ policy: NewModelPolicy;
45
+ hasSelectedModels?: boolean;
46
+ now: string;
47
+ }): NewModelPolicyResult {
48
+ const discovered = [...new Set(options.discoveredIds)].sort();
49
+ const prior = options.baseline;
50
+ if (!prior) {
51
+ return {
52
+ newIds: [],
53
+ nextBaseline: { ids: discovered, removed: [], updatedAt: options.now },
54
+ slugsToDisable: [], arrivals: [],
55
+ overflow: discovered.length > MAX_KNOWN_MODELS_PER_PROVIDER,
56
+ };
57
+ }
58
+ const active = new Set(prior.ids);
59
+ const removed = new Set(prior.removed);
60
+ const seen = new Set(discovered);
61
+ const newIds = discovered.filter(id => !active.has(id) && !removed.has(id));
62
+ const missing: Record<string, number> = {};
63
+ for (const id of active) {
64
+ if (seen.has(id)) continue;
65
+ const count = (prior.missing?.[id] ?? 0) + 1;
66
+ if (count >= MODEL_REMOVAL_GRACE_FETCHES) {
67
+ active.delete(id);
68
+ removed.add(id);
69
+ } else missing[id] = count;
70
+ }
71
+ for (const id of discovered) active.add(id);
72
+ const unionSize = active.size + removed.size;
73
+ const overflow = unionSize > MAX_KNOWN_MODELS_PER_PROVIDER;
74
+ const arrivals = newIds.map(id => ({ id, at: options.now }));
75
+ return {
76
+ newIds,
77
+ nextBaseline: {
78
+ ids: [...active].sort(), removed: [...removed].sort(), updatedAt: options.now,
79
+ ...(Object.keys(missing).length ? { missing } : {}),
80
+ },
81
+ // A non-empty preset/custom allowlist already excludes arrivals. This explicit no-op is
82
+ // deliberate: preset mode owns which matching flagships arrive on.
83
+ slugsToDisable: !overflow && options.policy === "off" && !options.hasSelectedModels
84
+ ? newIds.map(id => routedSlug(options.provider, id)) : [],
85
+ arrivals,
86
+ overflow,
87
+ };
88
+ }
89
+
90
+ export function effectiveNewModelPolicy(config: OcxConfig, provider: string): NewModelPolicy {
91
+ const local = config.providers[provider]?.newModelPolicy;
92
+ if (local === "on" || local === "off") return local;
93
+ return config.modelDiscovery?.newModelPolicy ?? "on";
94
+ }
95
+
96
+ /** Apply authoritative provider rows to a mutable convergence copy; degraded providers are omitted. */
97
+ export function reconcileSuccessfulModelDiscoveries(options: {
98
+ config: OcxConfig;
99
+ models: Iterable<{ provider: string; id: string; custom?: boolean }>;
100
+ authoritativeProviders: Iterable<string>;
101
+ now: string;
102
+ }): boolean {
103
+ const byProvider = new Map<string, string[]>();
104
+ for (const model of options.models) {
105
+ if (model.custom) continue;
106
+ const ids = byProvider.get(model.provider) ?? [];
107
+ ids.push(model.id); byProvider.set(model.provider, ids);
108
+ }
109
+ let changed = false;
110
+ for (const provider of options.authoritativeProviders) {
111
+ const configured = options.config.providers[provider];
112
+ if (!configured || configured.liveModels === false) continue;
113
+ const discoveredIds = byProvider.get(provider) ?? [];
114
+ const discovery = options.config.modelDiscovery ??= {};
115
+ const known = discovery.knownModels ??= {};
116
+ const result = applyNewModelPolicy({
117
+ provider, discoveredIds, baseline: known[provider],
118
+ policy: effectiveNewModelPolicy(options.config, provider),
119
+ hasSelectedModels: (configured.selectedModels?.length ?? 0) > 0,
120
+ now: options.now,
121
+ });
122
+ if (result.overflow) continue;
123
+ const priorBaseline = known[provider];
124
+ const baselineChanged = baselineDiffers(priorBaseline, result.nextBaseline);
125
+ known[provider] = result.nextBaseline;
126
+ // Keep the previous timestamp when nothing else moved, so a steady-state roster does not
127
+ // make the baseline look dirty on the next comparison either.
128
+ if (!baselineChanged && priorBaseline) known[provider] = priorBaseline;
129
+ if (result.slugsToDisable.length) {
130
+ const disabled = options.config.disabledModels ??= [];
131
+ for (const slug of result.slugsToDisable) {
132
+ if (disabled.includes(slug)) continue;
133
+ disabled.push(slug);
134
+ changed = true;
135
+ }
136
+ }
137
+ if (result.arrivals.length) {
138
+ const recent = discovery.recentArrivals ??= {};
139
+ recent[provider] = [...(recent[provider] ?? []), ...result.arrivals]
140
+ .slice(-MAX_RECENT_ARRIVALS_PER_PROVIDER);
141
+ changed = true;
142
+ }
143
+ if (baselineChanged) changed = true;
144
+ }
145
+ return changed;
146
+ }
@@ -1,4 +1,5 @@
1
1
  import type { OcxConfig } from "../types";
2
+ import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
2
3
 
3
4
  export interface ProviderRewriteResult {
4
5
  /** Number of references re-pointed. */
@@ -181,6 +182,6 @@ export function dropProviderCustomModels(config: OcxConfig, provider: string): n
181
182
  // `[]`, so the `customModels` field is absent either way. Only that field —
182
183
  // the `customModelCatalogMigration` marker is deliberately left in place.
183
184
  if (kept.length > 0) config.customModels = kept;
184
- else delete config.customModels;
185
+ else deleteConfigTopLevelKey(config, "customModels");
185
186
  return existing.length - kept.length;
186
187
  }