@bitkyc08/opencodex 2.58.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 (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. package/gui/dist/assets/index-C5-RdDmD.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
+ }
@@ -2,9 +2,9 @@
2
2
  //
3
3
  // Some routed models decorate the first and last lines as
4
4
  // `*** Begin Patch ***` / `*** End Patch ***`. Codex rejects those otherwise
5
- // valid custom-tool payloads. Repair is deliberately limited to a complete,
6
- // structurally recognizable top-level patch: arbitrary `exec` JavaScript is
7
- // caller-authored executable input and must remain byte-identical.
5
+ // valid custom-tool payloads. Repair of executable bodies is deliberately limited to
6
+ // unambiguous wrapper mistakes: one recognized alternate field or one complete outer
7
+ // Markdown fence. Ordinary `exec` JavaScript remains byte-identical.
8
8
  //
9
9
  // This is the same intent boundary as `src/lib/tool-argument-integers.ts`:
10
10
  // repair the one faithful reading, leave genuine patch content alone.
@@ -22,20 +22,40 @@ const PATCH_BEGIN = "*** Begin Patch";
22
22
  const PATCH_END = "*** End Patch";
23
23
  const TOP_LEVEL_PATCH_ENVELOPE = /^(\*\*\* Begin Patch(?: \*\*\*)?)(\r?\n)([\s\S]*)(\r?\n)(\*\*\* End Patch(?: \*\*\*)?)(\r?\n)?$/;
24
24
  const PATCH_OPERATION_LINE = /^\*\*\* (?:Add|Update|Delete) File: .+$/m;
25
+ const OUTER_MARKDOWN_CODE_FENCE = /^```[^\r\n]*\r?\n([\s\S]*?)\r?\n```$/;
26
+ const FREEFORM_FALLBACK_KEYS: Readonly<Record<string, readonly string[]>> = {
27
+ exec: ["code", "script", "js", "javascript", "command", "cmd", "content"],
28
+ apply_patch: ["patch", "content"],
29
+ };
30
+
31
+ function stripMarkdownCodeFence(text: string, toolName: string): string {
32
+ if (toolName !== "exec" && toolName !== "apply_patch") return text;
33
+ const match = OUTER_MARKDOWN_CODE_FENCE.exec(text.trim());
34
+ return match ? match[1] : text;
35
+ }
25
36
 
26
37
  /** Unwrap the `{input:string}` function-call wrapper used for freeform tools. */
27
- export function unwrapFreeformToolInput(argumentsText: unknown): string {
38
+ export function unwrapFreeformToolInput(argumentsText: unknown, toolName = ""): string {
28
39
  if (typeof argumentsText !== "string") return "";
29
40
  try {
30
41
  const parsed: unknown = JSON.parse(argumentsText);
31
42
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
32
- const input = (parsed as { input?: unknown }).input;
33
- if (typeof input === "string") return input;
43
+ const record = parsed as Record<string, unknown>;
44
+ if (Object.prototype.hasOwnProperty.call(record, "input")) {
45
+ return typeof record.input === "string"
46
+ ? stripMarkdownCodeFence(record.input, toolName)
47
+ : argumentsText;
48
+ }
49
+ const fallbackKeys = FREEFORM_FALLBACK_KEYS[toolName] ?? [];
50
+ const candidates = fallbackKeys.filter(key => typeof record[key] === "string");
51
+ if (candidates.length === 1) {
52
+ return stripMarkdownCodeFence(record[candidates[0]] as string, toolName);
53
+ }
34
54
  }
35
55
  } catch {
36
56
  // The string is the freeform body, not nested JSON.
37
57
  }
38
- return argumentsText;
58
+ return stripMarkdownCodeFence(argumentsText, toolName);
39
59
  }
40
60
 
41
61
  /**
@@ -92,17 +112,18 @@ export function mayBecomePatchEnvelope(text: string): boolean {
92
112
  /**
93
113
  * Repair freeform input before Codex sees it.
94
114
  *
95
- * Only a bare or reserved-`functions` `apply_patch` payload may receive delimiter
96
- * repair. Remote namespaces own their grammar; those bodies and every other
97
- * freeform input are unwrapped and left byte-exact.
115
+ * Only a bare or reserved-`functions` tool may receive fallback-field or outer-fence
116
+ * repair, and only `apply_patch` may receive delimiter repair. Remote namespaces own
117
+ * their grammar; those bodies and every other freeform input are unwrapped and left
118
+ * byte-exact.
98
119
  */
99
120
  export function repairFreeformToolInput(
100
121
  argumentsText: unknown,
101
122
  toolName = "",
102
123
  namespace?: string,
103
124
  ): string {
104
- const unwrapped = unwrapFreeformToolInput(argumentsText);
105
125
  const ownsApplyPatchGrammar = namespace === undefined || namespace === "functions";
126
+ const unwrapped = unwrapFreeformToolInput(argumentsText, ownsApplyPatchGrammar ? toolName : "");
106
127
  return ownsApplyPatchGrammar && toolName === "apply_patch"
107
128
  ? normalizeApplyPatchDelimiters(unwrapped)
108
129
  : unwrapped;