@bitkyc08/opencodex 2.59.0 → 2.60.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 (194) hide show
  1. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  2. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +4 -1
  5. package/src/adapters/anthropic-image-codec.ts +16 -2
  6. package/src/adapters/anthropic-image-normalize.ts +49 -2
  7. package/src/adapters/anthropic.ts +4 -1
  8. package/src/adapters/base.ts +23 -0
  9. package/src/adapters/coding-agent/turn.ts +22 -2
  10. package/src/adapters/command-code.ts +50 -3
  11. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  12. package/src/adapters/cursor/discovery.ts +11 -8
  13. package/src/adapters/cursor/live-transport.ts +26 -9
  14. package/src/adapters/cursor/request-builder.ts +7 -1
  15. package/src/adapters/cursor/transport.ts +19 -0
  16. package/src/adapters/cursor.ts +3 -1
  17. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  18. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  19. package/src/adapters/devin.ts +50 -13
  20. package/src/adapters/google-antigravity-replay.ts +1 -1
  21. package/src/adapters/google-antigravity-wire.ts +29 -5
  22. package/src/adapters/google-http.ts +49 -10
  23. package/src/adapters/google-tool-schema.ts +595 -31
  24. package/src/adapters/google-wire-compiler.ts +93 -10
  25. package/src/adapters/google-wire-shape.ts +461 -0
  26. package/src/adapters/google.ts +37 -6
  27. package/src/adapters/openai-chat-images.ts +3 -1
  28. package/src/adapters/openai-chat.ts +5 -1
  29. package/src/adapters/openai-responses/image-gen.ts +8 -6
  30. package/src/adapters/openai-responses/passthrough.ts +15 -3
  31. package/src/adapters/openai-responses/reasoning.ts +7 -0
  32. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  33. package/src/bridge/sse.ts +3 -151
  34. package/src/cli/account-extended.ts +4 -4
  35. package/src/cli/dispatch.ts +3 -3
  36. package/src/cli/doctor.ts +28 -9
  37. package/src/cli/hub.ts +3 -2
  38. package/src/cli/index.ts +7 -2
  39. package/src/cli/opencode.ts +2 -2
  40. package/src/cli/provider.ts +13 -1
  41. package/src/client/machine-api.ts +2 -2
  42. package/src/client/machine-listener.ts +2 -2
  43. package/src/client/runtime.ts +26 -2
  44. package/src/codex/account-store.ts +65 -0
  45. package/src/codex/auth-api/account-list.ts +19 -11
  46. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  47. package/src/codex/catalog/gather-capture.ts +21 -2
  48. package/src/codex/catalog/model-hints.ts +29 -28
  49. package/src/codex/catalog/parsing.ts +7 -0
  50. package/src/codex/catalog/provider-models.ts +19 -2
  51. package/src/codex/catalog/retained-sync.ts +22 -26
  52. package/src/codex/catalog/routed-gather.ts +19 -0
  53. package/src/codex/context-compat.ts +5 -2
  54. package/src/codex/desired-state.ts +4 -1
  55. package/src/codex/history-job.ts +6 -6
  56. package/src/codex/history-provider.ts +20 -166
  57. package/src/codex/history-rollout-read.ts +174 -0
  58. package/src/codex/internal/catalog-writer.ts +33 -1
  59. package/src/codex/model-cache.ts +47 -0
  60. package/src/codex/model-entitlements.ts +29 -10
  61. package/src/codex/observed-model-denials.ts +101 -8
  62. package/src/codex/prompt-text-probe.ts +9 -6
  63. package/src/codex/routing.ts +7 -1
  64. package/src/codex/shim.ts +1 -1
  65. package/src/codex/subagent-model-fallback.ts +22 -4
  66. package/src/combos/failover.ts +3 -0
  67. package/src/config/admitted-identity.ts +222 -0
  68. package/src/config/diagnostics.ts +22 -1
  69. package/src/config/feature-flags.ts +5 -0
  70. package/src/config/load-degrade.ts +18 -0
  71. package/src/config/proxy-env.ts +8 -2
  72. package/src/config/schema/compaction-triggers.ts +11 -0
  73. package/src/config/schema/config-schema.ts +4 -0
  74. package/src/config/schema/leaf-validators.ts +12 -0
  75. package/src/config.ts +2 -2
  76. package/src/generated/compatibility-version.json +249 -169
  77. package/src/grok/reset-coupons.ts +38 -19
  78. package/src/images/loop.ts +6 -1
  79. package/src/integrations/aside-profile-context.ts +37 -3
  80. package/src/integrations/aside-profile-journal.ts +68 -3
  81. package/src/integrations/aside-profiles.ts +128 -3
  82. package/src/integrations/mutation-plan.ts +815 -0
  83. package/src/integrations/writer.ts +85 -99
  84. package/src/lab/live/transport.ts +4 -0
  85. package/src/lab/live/types.ts +5 -0
  86. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  87. package/src/lib/admin-secrets.ts +9 -1
  88. package/src/lib/debug-log-buffer.ts +6 -1
  89. package/src/lib/debug.ts +23 -0
  90. package/src/lib/errors.ts +79 -0
  91. package/src/lib/http-response-semantics.ts +57 -0
  92. package/src/lib/lab-live-pinned-sender.ts +26 -12
  93. package/src/lib/pinned-http.ts +142 -2
  94. package/src/lib/plain-data.ts +103 -0
  95. package/src/lib/process-control.ts +13 -5
  96. package/src/lib/provider-outbound.ts +50 -2
  97. package/src/lib/socks5-fetch.ts +136 -26
  98. package/src/lib/spend-ledger-owner.ts +364 -0
  99. package/src/lib/spend-reservation-ledger.ts +218 -27
  100. package/src/lib/windows-system-proxy.ts +16 -11
  101. package/src/oauth/callback-server.ts +4 -3
  102. package/src/oauth/generic-account-failover.ts +1 -0
  103. package/src/oauth/health.ts +12 -1
  104. package/src/oauth/index.ts +3 -107
  105. package/src/oauth/login-flow-state.ts +127 -0
  106. package/src/providers/derive.ts +34 -17
  107. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  108. package/src/providers/key-failover.ts +66 -18
  109. package/src/providers/model-rename-migration.ts +55 -1
  110. package/src/providers/model-rename-startup.ts +7 -5
  111. package/src/providers/openai-virtual-models.ts +42 -2
  112. package/src/providers/quota/antigravity.ts +22 -2
  113. package/src/providers/quota/vendor-probes-key.ts +1 -1
  114. package/src/providers/registry/entries-core.ts +39 -17
  115. package/src/providers/registry/entries-extended.ts +35 -1
  116. package/src/providers/registry/model-ids.ts +168 -0
  117. package/src/providers/registry/model-seeds.ts +9 -0
  118. package/src/providers/registry/types.ts +2 -0
  119. package/src/providers/resolved-model-policy-merge.ts +167 -0
  120. package/src/providers/resolved-model-policy.ts +406 -0
  121. package/src/providers/stale-vision-classification-migration.ts +137 -0
  122. package/src/responses/apply-patch-envelope.ts +0 -12
  123. package/src/responses/freeform-wrapper-scan.ts +279 -0
  124. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  125. package/src/responses/progressive-freeform-input.ts +130 -0
  126. package/src/responses/reasoning-envelope.ts +30 -0
  127. package/src/responses/state.ts +5 -12
  128. package/src/responses/tool-name-aliases.ts +15 -1
  129. package/src/router.ts +91 -115
  130. package/src/routing/compatibility/behavior.ts +9 -0
  131. package/src/routing/compatibility/subject.ts +16 -1
  132. package/src/server/adapter-resolve.ts +9 -0
  133. package/src/server/auth-cors.ts +3 -0
  134. package/src/server/chat-completions.ts +5 -2
  135. package/src/server/claude-messages.ts +6 -3
  136. package/src/server/effort-row.ts +11 -3
  137. package/src/server/grok-responses-control-frame.ts +160 -1
  138. package/src/server/index/serve-options.ts +86 -29
  139. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  140. package/src/server/index/websocket-handler.ts +6 -1
  141. package/src/server/index.ts +16 -13
  142. package/src/server/management/aside-profile-routes.ts +266 -7
  143. package/src/server/management/config-routes.ts +18 -2
  144. package/src/server/management/context.ts +3 -0
  145. package/src/server/management/integration-routes.ts +287 -5
  146. package/src/server/management/metrics-routes.ts +20 -0
  147. package/src/server/management/model-rows.ts +224 -12
  148. package/src/server/management/route-registry.ts +14 -0
  149. package/src/server/management/shared.ts +10 -3
  150. package/src/server/management/system-restart.ts +7 -2
  151. package/src/server/management/system-routes.ts +2 -0
  152. package/src/server/management/usage-aggregate-cache.ts +4 -0
  153. package/src/server/management-api.ts +2 -0
  154. package/src/server/management-auth.ts +15 -1
  155. package/src/server/readiness.ts +29 -10
  156. package/src/server/relay-eager.ts +24 -2
  157. package/src/server/relay.ts +119 -10
  158. package/src/server/request-log.ts +55 -2
  159. package/src/server/request-metrics.ts +236 -0
  160. package/src/server/responses/adapter-continuation.ts +3 -3
  161. package/src/server/responses/adapter-dispatch.ts +11 -6
  162. package/src/server/responses/compact.ts +32 -10
  163. package/src/server/responses/compaction-routing.ts +111 -0
  164. package/src/server/responses/core-codex-account.ts +8 -3
  165. package/src/server/responses/core-combo.ts +7 -7
  166. package/src/server/responses/core-normalize.ts +6 -12
  167. package/src/server/responses/core-opaque-recovery.ts +1 -0
  168. package/src/server/responses/core-options.ts +4 -0
  169. package/src/server/responses/encrypted-payload.ts +20 -2
  170. package/src/server/responses/passthrough-delivery.ts +31 -14
  171. package/src/server/responses/passthrough-dispatch.ts +36 -9
  172. package/src/server/responses/policy-fallback.ts +5 -13
  173. package/src/server/responses/request-prepare.ts +67 -17
  174. package/src/server/responses/request-send-budget.ts +5 -1
  175. package/src/server/responses/request-sidecar-auth.ts +1 -1
  176. package/src/server/responses/request-transport.ts +2 -2
  177. package/src/server/responses/run-turn-execution.ts +25 -3
  178. package/src/server/responses/sidecar-execution.ts +17 -2
  179. package/src/server/responses/ws-upstream.ts +14 -27
  180. package/src/server/responses-custom-tool-repair.ts +27 -54
  181. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  182. package/src/server/sse-payload-rewrite.ts +1 -1
  183. package/src/tray/windows-tray.ps1 +155 -3
  184. package/src/types/config.ts +11 -3
  185. package/src/types/provider.ts +18 -0
  186. package/src/types/request.ts +2 -0
  187. package/src/types/tools.ts +14 -0
  188. package/src/types.ts +1 -0
  189. package/src/vision/eligibility.ts +88 -9
  190. package/src/vision/plan.ts +34 -10
  191. package/src/web-search/executor.ts +41 -2
  192. package/src/web-search/loop.ts +6 -1
  193. package/gui/dist/assets/index-C5IebErG.js +0 -136
  194. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -0,0 +1,406 @@
1
+ import type { ModelCapabilities, OcxProviderConfig } from "../types";
2
+ import { MODEL_ADAPTER_OVERRIDE_ALLOWED, pinnedWireAdapter } from "../types";
3
+ import { isCanonicalOpenAiForwardProvider } from "./openai-tiers";
4
+ import { resolveProviderAuthTransport } from "./fastwire";
5
+ import { registryEntrySupportsLiveModelDiscovery } from "./static-model-discovery";
6
+ import type { InboundWire, ProviderRegistryEntry, ResponsesTerminalRepairPolicy } from "./registry/types";
7
+ import {
8
+ anthropicFamilyContextWindow,
9
+ detachedClone,
10
+ legacyModelValue,
11
+ legacyModelSource,
12
+ mapFill,
13
+ nestedMapFill,
14
+ positiveCapMap,
15
+ recursivelyFreeze,
16
+ resolvedBaseUrl,
17
+ sameStringArray,
18
+ scalar,
19
+ stableUnion,
20
+ staticHeaders,
21
+ wireDefault,
22
+ type StaticPolicySource,
23
+ } from "./resolved-model-policy-merge";
24
+ export type { StaticPolicySource } from "./resolved-model-policy-merge";
25
+ export type StaticProviderPolicyField =
26
+ | "adapter" | "baseUrl" | "apiKeyTransport" | "headers" | "authMode" | "codexAccountMode"
27
+ | "responsesPath" | "chatCompletionsPath" | "keyOptional" | "freeTier" | "modelSuffixBracketStrip"
28
+ | "defaultModel" | "models" | "liveModels" | "contextWindow" | "modelContextWindows"
29
+ | "modelDisplayNames" | "modelInputModalities" | "modelMaxInputTokens" | "defaultMaxOutputTokens"
30
+ | "modelMaxOutputTokens" | "reasoningEfforts" | "modelReasoningEfforts" | "modelReasoningEffortsAuthoritative"
31
+ | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
32
+ | "reasoningWireFormat" | "noVisionModels" | "noReasoningModels" | "noTemperatureModels"
33
+ | "noTopPModels" | "noPenaltyModels" | "noJsonSchemaModels" | "parallelToolCalls"
34
+ | "promptCacheKey" | "chatServiceTier" | "openaiChatEofTolerance" | "statelessResponses"
35
+ | "requiresAdjacentResponsesToolResults" | "requiresPairedResponsesToolResults" | "annotateEmptyToolOutputs"
36
+ | "fastWire" | "supportsServiceTier" | "modelSupportsServiceTier" | "supportsOpenAiWebSearchToolFields"
37
+ | "supportsResponsesCustomTools" | "preserveResponsesReasoningContent" | "dropResponsesReasoningItems"
38
+ | "modelSupportsReasoningSummaries"
39
+ | "supportsVerbosity" | "modelSupportsVerbosity" | "responsesItemIdRepair" | "autoToolChoiceOnlyModels"
40
+ | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels"
41
+ | "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "showThinkingSummary"
42
+ | "escapeBuiltinToolNames" | "googleMode" | "project" | "location" | "modelCapabilities"
43
+ | "modelAutoCompactTokenLimits" | "modelSuppressSyntheticMax" | "modelReasoningSummaryDelivery"
44
+ | "codexToolMode" | "modelAdapters";
45
+ type StaticProviderPolicyShape = Pick<OcxProviderConfig, StaticProviderPolicyField>;
46
+ export type ResolvedProviderStaticPolicy = Readonly<Partial<StaticProviderPolicyShape>> & Readonly<{
47
+ adapter: string;
48
+ baseUrl: string;
49
+ }>;
50
+ export interface ResolvedPerModelStaticPolicy {
51
+ readonly adapter: string;
52
+ readonly contextWindow?: number;
53
+ readonly inputModalities?: readonly string[];
54
+ readonly maxInputTokens?: number;
55
+ readonly maxOutputTokens?: number;
56
+ readonly reasoningEfforts?: readonly string[];
57
+ readonly defaultReasoningEffort?: string;
58
+ readonly supportsReasoningSummaries?: boolean;
59
+ readonly supportsVerbosity?: boolean;
60
+ readonly supportsServiceTier?: boolean;
61
+ readonly fastTierDescription?: string;
62
+ readonly directReasoningEffort?: boolean;
63
+ readonly responsesUpstreamStreaming?: boolean;
64
+ readonly responsesTerminalRepair?: Readonly<ResponsesTerminalRepairPolicy>;
65
+ }
66
+ export interface ResolvedModelPolicy {
67
+ readonly version: 1;
68
+ readonly providerName: string;
69
+ readonly modelId: string;
70
+ readonly transportMatchedRegistry: boolean;
71
+ readonly effectiveAlias?: string | null;
72
+ readonly provider: ResolvedProviderStaticPolicy;
73
+ readonly model: Readonly<ResolvedPerModelStaticPolicy>;
74
+ readonly provenance: Readonly<{
75
+ alias: StaticPolicySource;
76
+ provider: Readonly<Partial<Record<StaticProviderPolicyField, StaticPolicySource>>>;
77
+ model: Readonly<Partial<Record<keyof ResolvedPerModelStaticPolicy, StaticPolicySource>>>;
78
+ }>;
79
+ }
80
+ export interface ResolveModelPolicyInput {
81
+ readonly providerName: string;
82
+ /** Final wire identity after resolveModelAlias and resolveOpenAiVirtualModel; public selection stays diagnostic-only. */
83
+ readonly modelId: string;
84
+ readonly provider: Readonly<OcxProviderConfig>;
85
+ readonly registryEntry?: Readonly<ProviderRegistryEntry>;
86
+ readonly transportMatchedRegistry: boolean;
87
+ readonly inboundWire?: InboundWire;
88
+ /** Exact post-rewrite model capability declaration captured by the caller. */
89
+ readonly modelCapabilities?: Readonly<ModelCapabilities>;
90
+ /** Credential-free live admission result: key only after a usable override, otherwise the registry mode. */
91
+ readonly effectiveAuth?: Readonly<{ authMode: NonNullable<OcxProviderConfig["authMode"]> }>;
92
+ /** Capture-time collision-aware alias decision. Omit to use provider then registry fallback. */
93
+ readonly effectiveAlias?: string | null;
94
+ readonly effectiveAliasSource?: Exclude<StaticPolicySource, "hard-pin" | "provider-default">;
95
+ }
96
+ export interface ObservedModelLimits {
97
+ readonly contextWindow?: number;
98
+ readonly maxInputTokens?: number;
99
+ readonly maxOutputTokens?: number;
100
+ }
101
+ /** Call-local projection: observed values stay outside ResolvedModelPolicy and may only narrow caps. */
102
+ export function clampObservedModelLimits(
103
+ policy: Readonly<Pick<ResolvedPerModelStaticPolicy, "contextWindow" | "maxInputTokens" | "maxOutputTokens">>,
104
+ observed: Readonly<ObservedModelLimits>,
105
+ ): Readonly<ObservedModelLimits> {
106
+ const clamp = (staticCap: number | undefined, observedValue: number | undefined): number | undefined => {
107
+ if (observedValue === undefined) return staticCap;
108
+ return staticCap === undefined ? observedValue : Math.min(staticCap, observedValue);
109
+ };
110
+ const contextWindow = clamp(policy.contextWindow, observed.contextWindow);
111
+ const clampedMaxInputTokens = clamp(policy.maxInputTokens, observed.maxInputTokens);
112
+ const maxInputTokens = clampedMaxInputTokens !== undefined && contextWindow !== undefined
113
+ ? Math.min(clampedMaxInputTokens, contextWindow)
114
+ : clampedMaxInputTokens;
115
+ const maxOutputTokens = clamp(policy.maxOutputTokens, observed.maxOutputTokens);
116
+ return Object.freeze({
117
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
118
+ ...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
119
+ ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
120
+ });
121
+ }
122
+ export function resolveModelPolicy(input: ResolveModelPolicyInput): ResolvedModelPolicy {
123
+ const entry = input.transportMatchedRegistry ? input.registryEntry : undefined;
124
+ const provider = input.provider;
125
+ const providerPolicy: Partial<StaticProviderPolicyShape> = {};
126
+ const providerProvenance: Partial<Record<StaticProviderPolicyField, StaticPolicySource>> = {};
127
+ const [baseUrl, baseUrlSource] = resolvedBaseUrl(entry, provider);
128
+ const put = <K extends StaticProviderPolicyField>(key: K, value: StaticProviderPolicyShape[K] | undefined, source: StaticPolicySource): void => {
129
+ if (value !== undefined) providerPolicy[key] = detachedClone(value) as never;
130
+ providerProvenance[key] = source;
131
+ };
132
+ const putScalar = <K extends StaticProviderPolicyField>(key: K, registryValue: StaticProviderPolicyShape[K] | undefined): void => {
133
+ const [value, source] = scalar(provider[key], registryValue);
134
+ put(key, value, source);
135
+ };
136
+ const putUnion = (key: StaticProviderPolicyField, registryValue: readonly string[] | undefined): void => {
137
+ const [value, source] = stableUnion(registryValue, provider[key] as string[] | undefined);
138
+ put(key, value as never, source);
139
+ };
140
+ const putMergedMap = <K extends StaticProviderPolicyField, T>(
141
+ key: K,
142
+ registryValue: Readonly<Record<string, T>> | undefined,
143
+ operatorValue: Readonly<Record<string, T>> | undefined,
144
+ ): void => {
145
+ const [value, source] = mapFill(registryValue, operatorValue);
146
+ put(key, value as StaticProviderPolicyShape[K], source);
147
+ };
148
+ put("adapter", entry?.adapter ?? provider.adapter, entry ? "registry" : "operator");
149
+ put("baseUrl", baseUrl, baseUrlSource);
150
+ putScalar("apiKeyTransport", entry?.apiKeyTransport);
151
+ const [headers, headersSource] = staticHeaders(entry?.staticHeaders, provider.headers);
152
+ put("headers", headers, headersSource);
153
+ const resolvedAuthMode = input.effectiveAuth?.authMode
154
+ ?? (entry?.authKind === "forward" || entry?.authKind === "oauth"
155
+ ? entry.authKind
156
+ : provider.authMode === "forward" ? undefined : provider.authMode ?? entry?.authKind);
157
+ put("authMode", resolvedAuthMode, input.effectiveAuth
158
+ ? "captured-auth"
159
+ : entry?.authKind === "forward" || entry?.authKind === "oauth" ? "registry"
160
+ : provider.authMode !== undefined ? "operator" : entry ? "registry" : "unknown");
161
+ putScalar("codexAccountMode", entry?.codexAccountMode);
162
+ const keyAuthDefaults = entry?.allowKeyAuthOverride === true
163
+ && resolvedAuthMode === "key"
164
+ && ["authorization_bearer", "x_api_key"].includes(resolveProviderAuthTransport(
165
+ provider.adapter,
166
+ resolvedAuthMode,
167
+ provider.apiKeyTransport,
168
+ ))
169
+ ? entry.keyAuthServiceTier
170
+ : undefined;
171
+ for (const key of [
172
+ "responsesPath", "chatCompletionsPath", "keyOptional", "freeTier", "modelSuffixBracketStrip",
173
+ "defaultModel", "models", "liveModels", "contextWindow", "defaultMaxOutputTokens",
174
+ "reasoningWireFormat", "parallelToolCalls", "promptCacheKey",
175
+ "openaiChatEofTolerance", "statelessResponses",
176
+ "requiresAdjacentResponsesToolResults", "requiresPairedResponsesToolResults",
177
+ "annotateEmptyToolOutputs", "fastWire", "supportsServiceTier",
178
+ "supportsOpenAiWebSearchToolFields", "supportsResponsesCustomTools",
179
+ "preserveResponsesReasoningContent", "dropResponsesReasoningItems",
180
+ "supportsVerbosity", "responsesItemIdRepair",
181
+ "showThinkingSummary", "escapeBuiltinToolNames", "googleMode", "project", "location",
182
+ ] as const) putScalar(key, entry?.[key] as StaticProviderPolicyShape[typeof key] | undefined);
183
+ const legacyClinePassLadder = entry !== undefined && input.providerName === "cline-pass"
184
+ && provider.reasoningWireFormat === "gateway-object"
185
+ && sameStringArray(provider.reasoningEfforts, ["low"]);
186
+ put("reasoningEfforts", legacyClinePassLadder ? entry?.reasoningEfforts : provider.reasoningEfforts ?? entry?.reasoningEfforts,
187
+ legacyClinePassLadder || provider.reasoningEfforts === undefined ? entry?.reasoningEfforts ? "registry" : "unknown" : "operator");
188
+ put("chatServiceTier", provider.chatServiceTier ?? keyAuthDefaults?.chatServiceTier ?? entry?.chatServiceTier,
189
+ provider.chatServiceTier !== undefined ? "operator" : keyAuthDefaults?.chatServiceTier !== undefined ? "registry" : entry?.chatServiceTier !== undefined ? "registry" : "unknown");
190
+ put("supportsServiceTier", provider.supportsServiceTier ?? keyAuthDefaults?.supportsServiceTier ?? entry?.supportsServiceTier,
191
+ provider.supportsServiceTier !== undefined ? "operator" : keyAuthDefaults?.supportsServiceTier !== undefined ? "registry" : entry?.supportsServiceTier !== undefined ? "registry" : "unknown");
192
+ if (entry && !registryEntrySupportsLiveModelDiscovery(entry)) put("liveModels", false, "registry");
193
+ putMergedMap("modelDisplayNames", entry?.modelDisplayNames, provider.modelDisplayNames);
194
+ putMergedMap("modelInputModalities", entry?.modelInputModalities, provider.modelInputModalities);
195
+ putMergedMap("modelMaxOutputTokens", entry?.modelMaxOutputTokens, provider.modelMaxOutputTokens);
196
+ putMergedMap("modelReasoningEfforts", entry?.modelReasoningEfforts, provider.modelReasoningEfforts);
197
+ // Operator-only: no registry entry declares it, and it decides whether the map above is the
198
+ // wire contract or only the catalog's. A policy reader that omitted it would report the same
199
+ // effective ladder for two providers that send different efforts.
200
+ put("modelReasoningEffortsAuthoritative", provider.modelReasoningEffortsAuthoritative,
201
+ provider.modelReasoningEffortsAuthoritative !== undefined ? "operator" : "unknown");
202
+ putMergedMap("modelDefaultReasoningEfforts", entry?.modelDefaultReasoningEfforts, provider.modelDefaultReasoningEfforts);
203
+ const registryServiceTier = !entry?.modelServiceTierCapabilityBaseUrlGuard
204
+ || entry.modelServiceTierCapabilityBaseUrlGuard(provider.baseUrl)
205
+ ? entry?.modelSupportsServiceTier
206
+ : undefined;
207
+ const registryServiceTierDefaults = registryServiceTier || keyAuthDefaults?.modelSupportsServiceTier
208
+ ? { ...(registryServiceTier ?? {}), ...(keyAuthDefaults?.modelSupportsServiceTier ?? {}) }
209
+ : undefined;
210
+ putMergedMap("modelSupportsServiceTier", registryServiceTierDefaults, provider.modelSupportsServiceTier);
211
+ putMergedMap("modelSupportsReasoningSummaries", entry?.modelSupportsReasoningSummaries, provider.modelSupportsReasoningSummaries);
212
+ putMergedMap("modelSupportsVerbosity", entry?.modelSupportsVerbosity, provider.modelSupportsVerbosity);
213
+ for (const key of [
214
+ "modelCapabilities", "modelAutoCompactTokenLimits", "modelSuppressSyntheticMax",
215
+ "modelReasoningSummaryDelivery", "codexToolMode", "modelAdapters",
216
+ ] as const) putScalar(key, undefined);
217
+ for (const key of ["modelContextWindows", "modelMaxInputTokens"] as const) {
218
+ const [value, source] = input.providerName === "openai-apikey"
219
+ ? positiveCapMap(entry?.[key], provider[key])
220
+ : mapFill(entry?.[key], provider[key]);
221
+ put(key, value, source);
222
+ }
223
+ const [effortMap, effortMapSource] = mapFill(entry?.reasoningEffortMap, provider.reasoningEffortMap);
224
+ put("reasoningEffortMap", effortMap, effortMapSource);
225
+ const [modelEffortMap, modelEffortMapSource] = nestedMapFill(entry?.modelReasoningEffortMap, provider.modelReasoningEffortMap);
226
+ put("modelReasoningEffortMap", modelEffortMap, modelEffortMapSource);
227
+ for (const key of [
228
+ "noVisionModels", "noReasoningModels", "noTemperatureModels", "noTopPModels",
229
+ "noPenaltyModels", "noJsonSchemaModels", "autoToolChoiceOnlyModels",
230
+ "preserveReasoningContentModels", "requiresReasoningPlaceholderModels",
231
+ "reasoningSplitModels", "reasoningDetailsModels", "thinkingToggleModels", "thinkingBudgetModels",
232
+ ] as const) putUnion(key, entry?.[key]);
233
+ for (const directModel of entry?.directReasoningEffortModels ?? []) {
234
+ const staleBudget = [directModel, ...(entry?.thinkingBudgetModels ?? [])];
235
+ const routedStaleBudget = [...(entry?.thinkingBudgetModels ?? []), directModel];
236
+ if (sameStringArray(provider.thinkingBudgetModels, staleBudget)
237
+ || sameStringArray(provider.thinkingBudgetModels, routedStaleBudget)) {
238
+ providerPolicy.thinkingBudgetModels = [...(entry?.thinkingBudgetModels ?? [])];
239
+ providerProvenance.thinkingBudgetModels = "registry";
240
+ }
241
+ const explicitEfforts = Object.entries(provider.modelReasoningEfforts ?? {})
242
+ .filter(([key]) => key.toLowerCase() === directModel.toLowerCase());
243
+ if (explicitEfforts.length > 0) {
244
+ for (const key of Object.keys(providerPolicy.modelReasoningEfforts ?? {})) {
245
+ if (key.toLowerCase() === directModel.toLowerCase()) delete providerPolicy.modelReasoningEfforts![key];
246
+ }
247
+ for (const [key, value] of explicitEfforts) (providerPolicy.modelReasoningEfforts ??= {})[key] = [...value];
248
+ } else if (entry?.modelReasoningEfforts?.[directModel] !== undefined) {
249
+ (providerPolicy.modelReasoningEfforts ??= {})[directModel] = [...entry.modelReasoningEfforts[directModel]!];
250
+ }
251
+ const explicitDefault = Object.entries(provider.modelDefaultReasoningEfforts ?? {})
252
+ .filter(([key]) => key.toLowerCase() === directModel.toLowerCase());
253
+ if (explicitDefault.length > 0) {
254
+ for (const key of Object.keys(providerPolicy.modelDefaultReasoningEfforts ?? {})) {
255
+ if (key.toLowerCase() === directModel.toLowerCase()) delete providerPolicy.modelDefaultReasoningEfforts![key];
256
+ }
257
+ for (const [key, value] of explicitDefault) (providerPolicy.modelDefaultReasoningEfforts ??= {})[key] = value;
258
+ } else if (entry?.modelDefaultReasoningEfforts?.[directModel] !== undefined) {
259
+ (providerPolicy.modelDefaultReasoningEfforts ??= {})[directModel] = entry.modelDefaultReasoningEfforts[directModel]!;
260
+ }
261
+ const explicitMap = Object.entries(provider.modelReasoningEffortMap ?? {})
262
+ .filter(([key]) => key.toLowerCase() === directModel.toLowerCase());
263
+ for (const key of Object.keys(providerPolicy.modelReasoningEffortMap ?? {})) {
264
+ if (key.toLowerCase() === directModel.toLowerCase()) delete providerPolicy.modelReasoningEffortMap![key];
265
+ }
266
+ if (explicitMap.length > 0) {
267
+ for (const [key, value] of explicitMap) (providerPolicy.modelReasoningEffortMap ??= {})[key] = { ...value };
268
+ } else {
269
+ (providerPolicy.modelReasoningEffortMap ??= {})[directModel] = {};
270
+ }
271
+ }
272
+ const modelValue = <T>(record: Readonly<Record<string, T>> | undefined): T | undefined =>
273
+ legacyModelValue(record, input.modelId);
274
+ const modelSource = <T>(operator: Readonly<Record<string, T>> | undefined, registry: Readonly<Record<string, T>> | undefined): StaticPolicySource =>
275
+ legacyModelSource(operator, registry, input.modelId);
276
+ const modelOrProviderSource = <T>(
277
+ operatorMap: Readonly<Record<string, T>> | undefined, registryMap: Readonly<Record<string, T>> | undefined,
278
+ operatorDefault: T | undefined, registryDefault: T | undefined,
279
+ ): StaticPolicySource => {
280
+ const exact = modelSource(operatorMap, registryMap);
281
+ return exact !== "unknown" ? exact : operatorDefault !== undefined ? "operator" : registryDefault !== undefined ? "registry" : "unknown";
282
+ };
283
+ const configuredAdapter = provider.modelAdapters?.[input.modelId];
284
+ const pin = pinnedWireAdapter(input.providerName, input.modelId);
285
+ const normalizedModelId = input.modelId.trim().toLowerCase();
286
+ const registryWire = wireDefault(entry?.modelWireDefaults?.[normalizedModelId], provider, entry,
287
+ input.inboundWire ?? "responses", resolvedAuthMode);
288
+ const explicitWire = configuredAdapter && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(configuredAdapter)
289
+ ? configuredAdapter
290
+ : undefined;
291
+ const providerAdapter = providerPolicy.adapter ?? provider.adapter;
292
+ const effectiveProviderForWire = resolvedAuthMode === provider.authMode
293
+ ? provider
294
+ : { ...provider, authMode: resolvedAuthMode };
295
+ const canonicalForward = isCanonicalOpenAiForwardProvider(effectiveProviderForWire);
296
+ const adapter = pin
297
+ ?? (!canonicalForward ? explicitWire ?? registryWire : undefined)
298
+ ?? providerAdapter;
299
+ const adapterSource: StaticPolicySource = pin
300
+ ? "hard-pin"
301
+ : explicitWire && !canonicalForward
302
+ ? "operator"
303
+ : registryWire && !canonicalForward
304
+ ? "registry"
305
+ : "provider-default";
306
+ const declaredModalities = input.modelCapabilities?.inputModalities;
307
+ const capabilityModalities = Array.isArray(declaredModalities) && declaredModalities.length > 0
308
+ ? [...declaredModalities]
309
+ : undefined;
310
+ const exactContextWindow = modelValue(providerPolicy.modelContextWindows);
311
+ const familyContextWindow = providerPolicy.adapter === "anthropic"
312
+ ? anthropicFamilyContextWindow(providerPolicy.modelContextWindows, input.modelId)
313
+ : undefined;
314
+ const modelContextWindow = exactContextWindow ?? familyContextWindow ?? providerPolicy.contextWindow;
315
+ const exactMaxInputTokens = modelValue(providerPolicy.modelMaxInputTokens);
316
+ const modelMaxInputTokens = exactMaxInputTokens !== undefined && modelContextWindow !== undefined
317
+ ? Math.min(exactMaxInputTokens, modelContextWindow)
318
+ : exactMaxInputTokens;
319
+ const exactMaxOutputTokens = modelValue(providerPolicy.modelMaxOutputTokens);
320
+ const modelMaxOutputTokens = exactMaxOutputTokens ?? providerPolicy.defaultMaxOutputTokens;
321
+ const modelReasoningEfforts = modelValue(providerPolicy.modelReasoningEfforts) ?? providerPolicy.reasoningEfforts;
322
+ const modelSupportsReasoningSummaries = modelValue(providerPolicy.modelSupportsReasoningSummaries)
323
+ ?? (modelValue(providerPolicy.modelReasoningSummaryDelivery) !== undefined ? true : undefined);
324
+ const modelSupportsVerbosity = modelValue(providerPolicy.modelSupportsVerbosity) ?? providerPolicy.supportsVerbosity;
325
+ const modelSupportsServiceTier = modelValue(providerPolicy.modelSupportsServiceTier) ?? providerPolicy.supportsServiceTier;
326
+ const model: ResolvedPerModelStaticPolicy = {
327
+ adapter,
328
+ ...(modelContextWindow !== undefined ? { contextWindow: modelContextWindow } : {}),
329
+ ...(capabilityModalities ?? modelValue(providerPolicy.modelInputModalities) !== undefined
330
+ ? { inputModalities: capabilityModalities ?? modelValue(providerPolicy.modelInputModalities) }
331
+ : {}),
332
+ ...(modelMaxInputTokens !== undefined ? { maxInputTokens: modelMaxInputTokens } : {}),
333
+ ...(modelMaxOutputTokens !== undefined ? { maxOutputTokens: modelMaxOutputTokens } : {}),
334
+ ...(modelReasoningEfforts !== undefined ? { reasoningEfforts: modelReasoningEfforts } : {}),
335
+ ...(modelValue(providerPolicy.modelDefaultReasoningEfforts) !== undefined ? { defaultReasoningEffort: modelValue(providerPolicy.modelDefaultReasoningEfforts) } : {}),
336
+ ...(modelSupportsReasoningSummaries !== undefined ? { supportsReasoningSummaries: modelSupportsReasoningSummaries } : {}),
337
+ ...(modelSupportsVerbosity !== undefined ? { supportsVerbosity: modelSupportsVerbosity } : {}),
338
+ ...(modelSupportsServiceTier !== undefined ? { supportsServiceTier: modelSupportsServiceTier } : {}),
339
+ ...(modelSupportsServiceTier === true && entry?.fastTierDescription !== undefined
340
+ ? { fastTierDescription: entry.fastTierDescription }
341
+ : {}),
342
+ ...(entry?.directReasoningEffortModels?.includes(input.modelId) ? { directReasoningEffort: true } : {}),
343
+ ...(entry?.modelResponsesUpstreamStreaming?.[normalizedModelId] !== undefined
344
+ ? { responsesUpstreamStreaming: entry.modelResponsesUpstreamStreaming[normalizedModelId] }
345
+ : {}),
346
+ ...(entry?.modelResponsesTerminalRepair?.[normalizedModelId] !== undefined
347
+ ? { responsesTerminalRepair: entry.modelResponsesTerminalRepair[normalizedModelId] }
348
+ : {}),
349
+ };
350
+ const modelProvenance: Partial<Record<keyof ResolvedPerModelStaticPolicy, StaticPolicySource>> = {
351
+ adapter: adapterSource,
352
+ };
353
+ modelProvenance.contextWindow = modelOrProviderSource(provider.modelContextWindows, entry?.modelContextWindows, provider.contextWindow, entry?.contextWindow);
354
+ if (modelProvenance.contextWindow === "unknown" && familyContextWindow !== undefined) {
355
+ modelProvenance.contextWindow = anthropicFamilyContextWindow(provider.modelContextWindows, input.modelId) !== undefined
356
+ ? "operator"
357
+ : "registry";
358
+ }
359
+ modelProvenance.inputModalities = capabilityModalities
360
+ ? "operator-capability"
361
+ : modelSource(provider.modelInputModalities, entry?.modelInputModalities);
362
+ modelProvenance.maxInputTokens = modelSource(provider.modelMaxInputTokens, entry?.modelMaxInputTokens);
363
+ modelProvenance.maxOutputTokens = modelOrProviderSource(
364
+ provider.modelMaxOutputTokens,
365
+ entry?.modelMaxOutputTokens,
366
+ provider.defaultMaxOutputTokens,
367
+ entry?.defaultMaxOutputTokens,
368
+ );
369
+ modelProvenance.reasoningEfforts = modelOrProviderSource(provider.modelReasoningEfforts, entry?.modelReasoningEfforts, provider.reasoningEfforts, entry?.reasoningEfforts);
370
+ modelProvenance.defaultReasoningEffort = modelSource(
371
+ provider.modelDefaultReasoningEfforts,
372
+ entry?.modelDefaultReasoningEfforts,
373
+ );
374
+ modelProvenance.supportsReasoningSummaries = modelSource(provider.modelSupportsReasoningSummaries, entry?.modelSupportsReasoningSummaries);
375
+ if (modelProvenance.supportsReasoningSummaries === "unknown" && modelValue(provider.modelReasoningSummaryDelivery) !== undefined) {
376
+ modelProvenance.supportsReasoningSummaries = "operator";
377
+ }
378
+ modelProvenance.supportsVerbosity = modelOrProviderSource(provider.modelSupportsVerbosity, entry?.modelSupportsVerbosity, provider.supportsVerbosity, entry?.supportsVerbosity);
379
+ const exactServiceTierSource = modelSource(provider.modelSupportsServiceTier, registryServiceTierDefaults);
380
+ modelProvenance.supportsServiceTier = exactServiceTierSource !== "unknown" ? exactServiceTierSource
381
+ : providerProvenance.supportsServiceTier ?? "unknown";
382
+ modelProvenance.responsesUpstreamStreaming = model.responsesUpstreamStreaming === undefined ? "unknown" : "registry";
383
+ modelProvenance.responsesTerminalRepair = model.responsesTerminalRepair === undefined ? "unknown" : "registry";
384
+ modelProvenance.fastTierDescription = model.fastTierDescription === undefined ? "unknown" : "registry";
385
+ modelProvenance.directReasoningEffort = model.directReasoningEffort === undefined ? "unknown" : "registry";
386
+ const alias = input.effectiveAlias !== undefined
387
+ ? input.effectiveAlias
388
+ : provider.alias !== undefined
389
+ ? provider.alias
390
+ : entry?.alias;
391
+ const aliasSource: StaticPolicySource = input.effectiveAlias !== undefined
392
+ ? input.effectiveAliasSource ?? "operator"
393
+ : provider.alias !== undefined
394
+ ? "operator"
395
+ : entry?.alias !== undefined ? "registry" : "unknown";
396
+ return recursivelyFreeze(detachedClone({
397
+ version: 1 as const,
398
+ providerName: input.providerName,
399
+ modelId: input.modelId,
400
+ transportMatchedRegistry: entry !== undefined,
401
+ ...(alias !== undefined ? { effectiveAlias: alias } : {}),
402
+ provider: providerPolicy as ResolvedProviderStaticPolicy,
403
+ model,
404
+ provenance: { alias: aliasSource, provider: providerProvenance, model: modelProvenance },
405
+ }));
406
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Repair vision classifications a saved config inherited from a stale registry seed.
3
+ *
4
+ * `enrichProviderFromRegistry` is fill-only and asymmetric, which is right for a hand-tuned
5
+ * value but freezes a wrong one:
6
+ *
7
+ * - `noVisionModels` is filled ALL-OR-NOTHING (`if (!prov.noVisionModels && seed.noVisionModels)`),
8
+ * so a config saved while the list was current keeps that list forever.
9
+ * - `modelInputModalities` is filled per-key BENEATH the saved value, so a saved `["text"]` for
10
+ * one id survives every later registry correction.
11
+ *
12
+ * Correcting the registry therefore fixes new installs only. This projection is the other half.
13
+ * It repairs the two saved values a stale seed left behind, in both states that reach a running
14
+ * process:
15
+ *
16
+ * - the full pair: modalities still exactly the stale declaration, and the id in the list. Both
17
+ * are rewritten.
18
+ * - the half-repaired row: modalities already corrected but the id still in the list. The
19
+ * sidecar predicate reads `noVisionModels` BEFORE the modality list, so that row keeps
20
+ * stripping images until the name goes as well; this projection removes it and leaves the
21
+ * modalities untouched.
22
+ *
23
+ * The paired modalities value is the guard in both states, which is also why it has to be
24
+ * readable: a name in the list with no modality declaration beside it is ambiguous — either a
25
+ * half-finished repair or an entry the operator added on purpose — and this projection does not
26
+ * guess which. It leaves that row alone.
27
+ *
28
+ * Identity is the same rule enrichment uses, read from the same helper: the registry must still
29
+ * own the row (`providerMatchesRegistryTransport`, the check `enrichProviderFromRegistry` makes
30
+ * before it writes registry metadata) and the row must still be on the entry's adapter.
31
+ * `opencode-go` is an existing key preset without `preserveCustomDestination`, so the id alone
32
+ * claims a row — exactly as it does for enrichment — while an entry that opts into destination
33
+ * preservation narrows this projection for free.
34
+ *
35
+ * Nothing here writes `modelCapabilities`. That is the dedicated per-model axis, it outranks
36
+ * every source this file touches, and it is where a deliberate text-only override belongs
37
+ * (`ocx provider edit <provider> --model <id> --text-only` writes it).
38
+ *
39
+ * Scope is deliberately one entry. The Go gateway's `deepseek-v4.1-flash` was declared text-only
40
+ * from jawcode metadata and was measured natively multimodal on 2026-09-19 (see the note at that
41
+ * entry in `registry/entries-core.ts`). Its sibling `deepseek-v4-flash` still rejects images
42
+ * upstream, and the sibling Zen tiers (`opencode-zen`, `opencode-free`) could not be probed at
43
+ * all — an unverified tier is not evidence, so neither is touched here.
44
+ */
45
+ import { PROVIDER_REGISTRY, providerMatchesRegistryTransport } from "./registry";
46
+ import type { OcxConfig } from "../types";
47
+
48
+ export interface StaleVisionClassification {
49
+ /** Registry provider id whose saved rows may carry the wrong classification. */
50
+ provider: string;
51
+ model: string;
52
+ /** The stale saved modalities this migration is allowed to replace, and nothing else. */
53
+ fromModalities: string[];
54
+ toModalities: string[];
55
+ }
56
+
57
+ export interface StaleVisionClassificationProjection {
58
+ config: OcxConfig;
59
+ changed: boolean;
60
+ warnings: string[];
61
+ }
62
+
63
+ export const STALE_VISION_CLASSIFICATIONS: readonly StaleVisionClassification[] = [
64
+ {
65
+ provider: "opencode-go",
66
+ model: "deepseek-v4.1-flash",
67
+ fromModalities: ["text"],
68
+ toModalities: ["text", "image"],
69
+ },
70
+ ];
71
+
72
+ /**
73
+ * Whether the registry still owns the row whose saved values would be rewritten.
74
+ *
75
+ * `providerMatchesRegistryTransport` is the rule enrichment applies, so this projection and the
76
+ * fill that put the stale values there answer identity the same way. Adapter equality stays as
77
+ * an additional tightening: a row moved onto another wire is not the row the seed described,
78
+ * however the registry entry is pinned.
79
+ */
80
+ function providerStillMatchesRegistry(id: string, prov: OcxConfig["providers"][string]): boolean {
81
+ const entry = PROVIDER_REGISTRY.find(row => row.id === id);
82
+ if (entry === undefined || entry.adapter !== prov.adapter) return false;
83
+ return providerMatchesRegistryTransport(id, prov);
84
+ }
85
+
86
+ function sameModalities(current: unknown, expected: readonly string[]): boolean {
87
+ return Array.isArray(current)
88
+ && current.length === expected.length
89
+ && current.every((value, index) => value === expected[index]);
90
+ }
91
+
92
+ /** Pure projection. The caller decides whether to persist. */
93
+ export function projectStaleVisionClassifications(
94
+ config: OcxConfig,
95
+ entries: readonly StaleVisionClassification[] = STALE_VISION_CLASSIFICATIONS,
96
+ ): StaleVisionClassificationProjection {
97
+ const warnings: string[] = [];
98
+ const repaired = new Map<string, string[]>();
99
+
100
+ for (const entry of entries) {
101
+ const prov = config.providers?.[entry.provider];
102
+ if (!prov) continue;
103
+ if (!providerStillMatchesRegistry(entry.provider, prov)) continue;
104
+ const modalities = prov.modelInputModalities;
105
+ if (!modalities) continue;
106
+ // The paired modalities value is the guard, and it decides which of the two states above this
107
+ // row is in. Anything else is a declaration this file does not own.
108
+ const saved = modalities[entry.model];
109
+ const stale = sameModalities(saved, entry.fromModalities);
110
+ const alreadyMigrated = sameModalities(saved, entry.toModalities);
111
+ if (!stale && !alreadyMigrated) continue;
112
+ const visionList = Array.isArray(prov.noVisionModels) ? prov.noVisionModels : undefined;
113
+ const listed = visionList !== undefined && visionList.includes(entry.model);
114
+ // Half-repaired rows have nothing left to do once the name is gone; the full pair is rewritten
115
+ // whether or not the name was ever listed, because `derive.ts` fills the two fields
116
+ // independently and a per-key modality fill can land without the all-or-nothing list.
117
+ if (!stale && !listed) continue;
118
+ if (stale) modalities[entry.model] = [...entry.toModalities];
119
+ if (visionList !== undefined && listed) {
120
+ prov.noVisionModels = visionList.filter(id => id !== entry.model);
121
+ }
122
+ const repairedList = repaired.get(entry.provider) ?? [];
123
+ repairedList.push(stale
124
+ ? `${entry.model} ${entry.fromModalities.join("+")} -> ${entry.toModalities.join("+")}`
125
+ : `${entry.model} dropped from noVisionModels (modalities already ${entry.toModalities.join("+")})`);
126
+ repaired.set(entry.provider, repairedList);
127
+ }
128
+
129
+ for (const [provider, list] of repaired) {
130
+ warnings.push(
131
+ `repaired the stale registry vision seed for ${list.length} model(s) on "${provider}": `
132
+ + `${list.join(", ")}.`,
133
+ );
134
+ }
135
+
136
+ return { config, changed: repaired.size > 0, warnings };
137
+ }
@@ -34,18 +34,6 @@ function stripMarkdownCodeFence(text: string, toolName: string): string {
34
34
  return match ? match[1] : text;
35
35
  }
36
36
 
37
- /**
38
- * The single-field wrappers `unwrapFreeformToolInput` accepts for one tool name, besides the
39
- * canonical `input`.
40
- *
41
- * Exported so the streaming side can hold a buffer that is still turning into one of these.
42
- * A second list of key names beside this one is how the streamed bytes and the completed item
43
- * come to disagree, which is the defect it exists to prevent (#5047).
44
- */
45
- export function freeformFallbackKeys(toolName: string): readonly string[] {
46
- return FREEFORM_FALLBACK_KEYS[toolName] ?? [];
47
- }
48
-
49
37
  /** Unwrap the `{input:string}` function-call wrapper used for freeform tools. */
50
38
  export function unwrapFreeformToolInput(argumentsText: unknown, toolName = ""): string {
51
39
  if (typeof argumentsText !== "string") return "";