@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
@@ -3,6 +3,9 @@ import { OPENAI_API_PROVIDER_ID } from "./openai-tiers";
3
3
  import type { OcxParsedRequest } from "../types";
4
4
  import type { RouteResult } from "../router";
5
5
  import type { RequestLogContext } from "../server/request-log";
6
+ import type { InboundWire } from "./registry/types";
7
+ import { providerMatchesRegistryTransportWithStaticGuards } from "./static-model-discovery";
8
+ import { resolveModelPolicy } from "./resolved-model-policy";
6
9
 
7
10
  export interface OpenAiVirtualModelResolution {
8
11
  selectedModelId: string;
@@ -49,18 +52,55 @@ export function resolveOpenAiVirtualModel(
49
52
  return validateOpenAiVirtualModelDefinition(selectedModelId, definition);
50
53
  }
51
54
 
55
+ export function captureOpenAiVirtualWirePolicy(
56
+ route: RouteResult,
57
+ resolution: OpenAiVirtualModelResolution,
58
+ inboundWire: InboundWire = "responses",
59
+ ): void {
60
+ // applyOpenAiVirtualModel is also a public mutation helper with older focused callers that
61
+ // construct only providerName/modelId/provider. Production RouteResult builders always capture
62
+ // staticPolicy; a legacy partial shape has no authority to recapture and keeps the old rewrite.
63
+ if (!route.staticPolicy) return;
64
+ const entry = PROVIDER_REGISTRY.find(candidate => candidate.id === route.providerName);
65
+ route.staticPolicy = resolveModelPolicy({
66
+ providerName: route.providerName,
67
+ modelId: resolution.wireModelId,
68
+ provider: route.provider,
69
+ registryEntry: entry,
70
+ transportMatchedRegistry: !!entry
71
+ && providerMatchesRegistryTransportWithStaticGuards(route.providerName, route.provider),
72
+ inboundWire,
73
+ modelCapabilities: route.provider.modelCapabilities?.[resolution.wireModelId],
74
+ ...(route.provider.authMode ? { effectiveAuth: { authMode: route.provider.authMode } } : {}),
75
+ ...(route.staticPolicy?.effectiveAlias !== undefined
76
+ ? { effectiveAlias: route.staticPolicy.effectiveAlias }
77
+ : {}),
78
+ });
79
+ }
80
+
52
81
  export function applyOpenAiVirtualModel(
53
82
  parsed: OcxParsedRequest,
54
83
  route: RouteResult,
55
84
  logCtx: RequestLogContext,
85
+ inboundWire: InboundWire = "responses",
56
86
  ): OpenAiVirtualModelResolution | undefined {
57
- const selectedModelId = logCtx.model && logCtx.model !== route.modelId ? logCtx.model : route.modelId;
58
- const resolution = resolveOpenAiVirtualModel(route.providerName, selectedModelId);
87
+ // Routing has already removed the provider namespace and resolved model aliases. Never use
88
+ // logCtx.model for identity: it is reporting state and can still contain a namespaced selector.
89
+ const resolution = resolveOpenAiVirtualModel(route.providerName, route.modelId)
90
+ ?? (() => {
91
+ // Preserve the public helper's idempotent second-call contract from its own mutation
92
+ // provenance, without promoting reporting state back into routing authority.
93
+ const selected = parsed._openAiVirtualSelectedModelId;
94
+ if (typeof selected !== "string") return undefined;
95
+ const remembered = resolveOpenAiVirtualModel(route.providerName, selected);
96
+ return remembered?.wireModelId === route.modelId ? remembered : undefined;
97
+ })();
59
98
  if (!resolution) return undefined;
60
99
 
61
100
  logCtx.model = resolution.selectedModelId;
62
101
  logCtx.resolvedModel = resolution.wireModelId;
63
102
  route.modelId = resolution.wireModelId;
103
+ captureOpenAiVirtualWirePolicy(route, resolution, inboundWire);
64
104
  parsed.modelId = resolution.wireModelId;
65
105
  parsed._openAiVirtualSelectedModelId = resolution.selectedModelId;
66
106
 
@@ -1,6 +1,6 @@
1
1
  import { antigravityUserAgent } from "../../adapters/client-fingerprint";
2
2
  import { DestinationDnsResolutionError } from "../../lib/destination-policy";
3
- import { PinnedHttpError } from "../../lib/pinned-http";
3
+ import { PinnedHttpError, type PinnedHttpErrorCode } from "../../lib/pinned-http";
4
4
  import { ProviderOutboundPolicyError, providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../../lib/provider-outbound";
5
5
  import { getValidAccessToken } from "../../oauth";
6
6
  import { getAccountCredential, getCredential } from "../../oauth/store";
@@ -193,10 +193,30 @@ type AntigravityQuotaProbeResult =
193
193
  | { kind: "available"; quota: ProviderQuota; source: "google-antigravity:retrieveUserQuotaSummary" | "google-antigravity:fetchAvailableModels" }
194
194
  | { kind: "unavailable"; failure: QuotaFailureCode; legacy: { kind: "null" } | { kind: "throw"; error: unknown } };
195
195
 
196
+ /**
197
+ * Every pinned-transport failure code, classified once.
198
+ *
199
+ * A conditional that named one code and sent the rest to `timeout` was correct only for as long
200
+ * as the union held exactly the codes it was written against. When the transport learned to
201
+ * report a coding it cannot undo, that answer was reported as a timeout, which is a different
202
+ * operational story entirely. A total map makes a new code a compile error here rather than a
203
+ * quiet misdiagnosis.
204
+ */
205
+ const PINNED_QUOTA_FAILURES = {
206
+ connect_timeout: "timeout",
207
+ first_byte_timeout: "timeout",
208
+ inactivity_timeout: "timeout",
209
+ // The response arrived and cannot be used: too large, coded in a format this transport cannot
210
+ // undo, or coded bytes that did not decode. None of these is a timing failure.
211
+ output_byte_limit: "response_unusable",
212
+ unsupported_content_encoding: "response_unusable",
213
+ content_decode_failed: "response_unusable",
214
+ } satisfies Record<PinnedHttpErrorCode, QuotaFailureCode>;
215
+
196
216
  function quotaTransportFailure(error: unknown): QuotaFailureCode {
197
217
  if (error instanceof ProviderOutboundPolicyError) return "destination_blocked";
198
218
  if (error instanceof DestinationDnsResolutionError) return "dns_failed";
199
- if (error instanceof PinnedHttpError) return error.code === "output_byte_limit" ? "response_unusable" : "timeout";
219
+ if (error instanceof PinnedHttpError) return PINNED_QUOTA_FAILURES[error.code];
200
220
  if (error instanceof DOMException && error.name === "TimeoutError") return "timeout";
201
221
  return "transport_error";
202
222
  }
@@ -915,7 +915,7 @@ async function fetchNeuralwattQuota(provider: string, config: OcxProviderConfig)
915
915
  }
916
916
 
917
917
 
918
- function normalizedBaseUrl(value: string): string | null {
918
+ export function normalizedBaseUrl(value: string): string | null {
919
919
  try {
920
920
  const url = new URL(value);
921
921
  if (url.username || url.password || url.search || url.hash) return null;
@@ -30,6 +30,8 @@ import {
30
30
  OPENAI_API_GPT56_REASONING_EFFORTS,
31
31
  META_MUSE_REASONING_EFFORTS,
32
32
  META_MUSE_REASONING_EFFORT_MAP,
33
+ META_MUSE_CODE_REASONING_EFFORTS,
34
+ META_MUSE_CODE_REASONING_EFFORT_MAP,
33
35
  META_MUSE_CONTEXT_WINDOW,
34
36
  META_MUSE_MODELS,
35
37
  OPENAI_DAYBREAK_MODELS,
@@ -599,12 +601,15 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
599
601
  label: "Meta Muse Code (CLI credential)",
600
602
  adapter: "openai-responses",
601
603
  baseUrl: "https://api.meta.ai/v1",
602
- // Meta own client sends this on every Muse Code call. We never have, so a future
603
- // server-side requirement would break every Muse request with no local signal.
604
+ // Meta's own client sends these on every Muse Code call. The compatibility marker
605
+ // is transparent while selecting the credential surface that accepts `max`.
604
606
  // Declared here rather than in a transport hook so it also covers model discovery
605
607
  // (src/oauth/index.ts:1176) and still yields to a user-set header
606
608
  // (mergeRegistryStaticHeaders, src/providers/registry.ts:3494).
607
- staticHeaders: { "x-api-version": "1.0.0" },
609
+ staticHeaders: {
610
+ "User-Agent": "muse-build/1.3.0 (opencodex compatibility)",
611
+ "x-api-version": "1.0.0",
612
+ },
608
613
  authKind: "oauth",
609
614
  oauthId: "meta-muse",
610
615
  dashboardUrl: "https://dev.meta.ai",
@@ -615,8 +620,8 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
615
620
  liveModels: false,
616
621
  modelContextWindows: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_CONTEXT_WINDOW])),
617
622
  modelInputModalities: Object.fromEntries(META_MUSE_MODELS.map(id => [id, ["text", "image"] as ["text", "image"]])),
618
- modelReasoningEfforts: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_REASONING_EFFORTS])),
619
- modelReasoningEffortMap: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_REASONING_EFFORT_MAP])),
623
+ modelReasoningEfforts: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_CODE_REASONING_EFFORTS])),
624
+ modelReasoningEffortMap: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_CODE_REASONING_EFFORT_MAP])),
620
625
  note: "Signs in to Meta with a browser device code on any platform, then mints the Muse Code subscription key. That grant is reimplemented from the one the Muse Code CLI performs and has NOT been exercised against Meta from OpenCodex, so treat the first login as unverified. If the Muse Code CLI is already signed in on macOS, the existing key is imported instead of starting a new grant. A pasted key from https://dev.meta.ai still works as a fallback when a device login cannot complete, and faces the same format check and live validation. A device login authenticates as Meta own Muse Code client, which is a stronger claim than reusing a key the CLI already minted. Meta scopes that credential to the Muse Code CLI, so this is an UNSUPPORTED use: Meta does not authorize subscription coverage outside its own CLI, how these calls settle is not observable from the API, and you should treat every call as billable against your account. The key, imported or pasted, is copied into OpenCodex's auth store. For an account signed in with the device login, OpenCodex refreshes Meta's subscription windows on demand from the same key endpoint the login uses, at most once every five minutes. For an imported or pasted key there is no endpoint to query them on demand, so OpenCodex reads them from streaming responses and shows the last observed value with its age; refreshing one then requires another streaming turn, and translated (non-passthrough) turns report none. Rate limits apply per team, not per key. For a supported path use the meta-model provider with your own key (export it as META_MODEL_API_KEY).",
621
626
  },
622
627
  {
@@ -652,6 +657,11 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
652
657
  // Zen Go can close a Chat stream after a fully assembled function call without sending
653
658
  // finish_reason or [DONE] (#2260). The adapter still rejects incomplete argument JSON.
654
659
  openaiChatEofTolerance: true,
660
+ // Muse Spark on OpenCode Go can sit silent during prolonged reasoning and close without a protocol terminal.
661
+ modelResponsesTerminalRepair: {
662
+ "muse-spark-1.2-contributor": { graceMs: 5_000 },
663
+ "muse-spark-1.3-contributor": { graceMs: 5_000 },
664
+ },
655
665
  // Go rejects reasoning.encrypted_content with previous_response_id (#3838).
656
666
  // Use explicit replay history and the existing stateless Responses policy.
657
667
  statelessResponses: true,
@@ -696,17 +706,25 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
696
706
  "glm-5.3-flash": ["text", "image"],
697
707
  // Experimental DeepSeek vision preview — expected to merge into deepseek-v4-flash later.
698
708
  [DEEPSEEK_VISION_PREVIEW_MODEL]: ["text", "image"],
699
- // This route is text-only upstream — it is already listed in this preset's
700
- // noVisionModels, which routes images through the proxy's vision sidecar and
701
- // makes the catalog advertise image input on its behalf. The positive
702
- // text-only declaration is what reaches an EXISTING install: derive.ts fills
703
- // noVisionModels all-or-nothing, so a config persisted before this id joined
704
- // the list keeps a stale list, the sidecar predicate never matches, the row
705
- // carries no modality at all, and any combo containing it collapses to
706
- // ["text"] (#4505). modelInputModalities IS per-key filled, so this
707
- // declaration lands on old configs. It states the route's real upstream
708
- // capability and keeps the sidecar explicitly distinct from native vision.
709
- "deepseek-v4.1-flash": ["text"],
709
+ // This route became natively multimodal; it is NOT a sidecar consumer.
710
+ //
711
+ // History: the id was declared text-only here and listed in this preset's
712
+ // noVisionModels, which routed its images through the vision sidecar (#4505).
713
+ // That classification came from jawcode metadata and went stale. Probed
714
+ // 2026-09-19 against https://opencode.ai/zen/go/v1/chat/completions with the
715
+ // headers this proxy sends: the route accepts an image_url part and the model
716
+ // reads it correctly (a four-band colour chart was described in the right
717
+ // order). Its sibling deepseek-v4-flash on the same gateway still answers
718
+ // HTTP 400 "Model only supports text input", which is what keeps the two
719
+ // distinct here rather than collapsing them.
720
+ //
721
+ // The declaration is what reaches an EXISTING install: derive.ts fills
722
+ // noVisionModels all-or-nothing, so a config persisted while the stale list
723
+ // was current keeps it forever, and modelInputModalities is filled per-key
724
+ // BENEATH the saved value. Both halves are repaired by
725
+ // stale-vision-classification-migration.ts; correcting the registry alone
726
+ // would fix new installs and leave existing ones stripping images.
727
+ "deepseek-v4.1-flash": ["text", "image"],
710
728
  // Muse Spark Contributor is natively multimodal on Zen Go: it accepts input_image
711
729
  // parts over /responses (probed 2026-08-26). Without this declaration the catalog
712
730
  // advertises it text-only and the Codex app blocks image attachments client-side with
@@ -760,7 +778,10 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
760
778
  // Kimi K2.7 Code accepts text+image+video: do NOT list it here.
761
779
  noVisionModels: [
762
780
  "glm-5.3", "glm-5.2", "glm-5", "glm-5.1",
763
- "deepseek-v4.1-flash", "deepseek-v4-flash",
781
+ // deepseek-v4.1-flash is deliberately absent: probed natively multimodal on this
782
+ // gateway 2026-09-19 (see the modelInputModalities note above). Its sibling
783
+ // deepseek-v4-flash stays listed — that route rejects image_url upstream.
784
+ "deepseek-v4-flash",
764
785
  "mimo-v2-pro", "mimo-v2.5-pro",
765
786
  "minimax-m2.5", "minimax-m2.7",
766
787
  "qwen3.7-max",
@@ -1237,3 +1258,4 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
1237
1258
  note: "Serverless Inference subscription API. Live discovery exposes only kimi-k2-instruct because Vultr documents it as the sole tool-calling model.",
1238
1259
  },
1239
1260
  ];
1261
+
@@ -746,9 +746,22 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
746
746
  id: "volcengine-coding-plan",
747
747
  label: "Volcengine Ark Coding Plan",
748
748
  baseUrl: "https://ark.cn-beijing.volces.com/api/coding/v3",
749
- adapter: "openai-chat",
749
+ responsesPath: "/responses",
750
+ adapter: "openai-responses",
750
751
  authKind: "key",
752
+ supportsServiceTier: false,
751
753
  preserveCustomDestination: true,
754
+ // A row already saved on Chat keeps Chat. This is a `preserveCustomDestination` key entry,
755
+ // so `providerMatchesRegistryTransport` refuses the adapter mismatch and the request path
756
+ // returns the stored row untouched; the alias below still hands it this entry's metadata.
757
+ // Deliberately no startup config migration: the Z.AI one (`zai-responses-migration.ts`) is
758
+ // safe only because it rewrites rows the router already canonicalizes, and it gates on
759
+ // `providerMatchesRegistryTransport` to guarantee that. A Chat row here is NOT canonicalized,
760
+ // so migrating it would change a wire the operator is actually using, and a marker added
761
+ // now cannot tell the old default apart from a deliberate pre-upgrade Chat choice.
762
+ destinationAliases: [{ baseUrl: "https://ark.cn-beijing.volces.com/api/coding/v3", adapter: "openai-chat" }],
763
+ // Validated Ark Coding Plan continuations reject replayed reasoning items.
764
+ dropResponsesReasoningItems: true,
752
765
  dashboardUrl: "https://console.volcengine.com/ark/region:ark+cn-beijing/overview",
753
766
  defaultModel: "ark-code-latest",
754
767
  models: VOLCENGINE_CODING_PLAN_MODELS,
@@ -798,6 +811,21 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
798
811
  defaultModel: "qwen3.8-max",
799
812
  models: ALIBABA_TOKEN_PLAN_MODELS,
800
813
  liveModels: false,
814
+ // Alibaba documents an OpenAI-compatible Responses API on this same /compatible-mode/v1 base
815
+ // and ships an official Codex integration guide on wire_api = "responses" (#5097). The
816
+ // gateway serves the same models over both wires, and qwen3.8-flash, qwen3.7-plus and
817
+ // glm-5.3 carry live end-to-end evidence there (custom tools, reasoning replay, streaming,
818
+ // multi-turn continuation).
819
+ //
820
+ // That is deliberately NOT expressed as a modelWireDefaults pin. Pinning would move every
821
+ // existing Codex user of those models onto a different upstream with no config change, and
822
+ // one delta is unresolved: preserveReasoningContentModels below is read by the CHAT adapter,
823
+ // while the Responses serializer reads preserveResponsesReasoningContent, which this entry
824
+ // does not set. On the Responses wire those models would replay with blanked reasoning
825
+ // content -- less state than they carry today. Z.AI and DeepSeek set both flags together for
826
+ // exactly this reason. Until that flag is justified against this gateway, Responses stays a
827
+ // documented per-model modelAdapters opt-in;
828
+ // tests/providers/alibaba-token-plan-responses-optin.test.ts holds both halves.
801
829
  note: "Token Plan Personal Edition · China (Beijing)",
802
830
  modelInputModalities: ALIBABA_TOKEN_PLAN_INPUT_MODALITIES,
803
831
  modelContextWindows: ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS,
@@ -1027,6 +1055,11 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1027
1055
  // Same DeepSeek routes as the Go preset above, behind the same vendor, so they carry
1028
1056
  // the same json_schema rejection (#1338 / #1415).
1029
1057
  noJsonSchemaModels: [...DEEPSEEK_GATEWAY_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS],
1058
+ // Muse Spark on Zen can sit silent during prolonged reasoning and close without a protocol terminal.
1059
+ modelResponsesTerminalRepair: {
1060
+ "muse-spark-1.2-contributor-free": { graceMs: 5_000 },
1061
+ "muse-spark-1.3-contributor-free": { graceMs: 5_000 },
1062
+ },
1030
1063
  },
1031
1064
  { id: "vercel-ai-gateway", label: "Vercel AI Gateway", baseUrl: "https://ai-gateway.vercel.sh/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://vercel.com/dashboard" },
1032
1065
  {
@@ -1329,3 +1362,4 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1329
1362
  note: "Official CodeBuddy Code CLI (Tencent Cloud), China/internal environment. Uses the documented CODEBUDDY_API_KEY + headless CLI surface; never reads desktop sessions or private console endpoints. Region-isolated from codebuddy (Global); credentials are never exchanged across regions. v1 disables CLI tools (--tools \"\"): text/reasoning only for now. Requires `npm i -g @tencent-ai/codebuddy-code`. AUP/routing authorization flagged for maintainer security review.",
1330
1363
  },
1331
1364
  ];
1365
+
@@ -0,0 +1,168 @@
1
+ import type { ProviderRegistryEntry } from "./types";
2
+
3
+ /**
4
+ * What a registry field's KEYS mean for selector decoding.
5
+ *
6
+ * An encoded selector is decoded against the native model ids a provider is known to accept.
7
+ * That set used to be a hand-written list of eight registry maps, so an id declared only in an
8
+ * unlisted map could not be decoded, and every new model-keyed field had to be remembered.
9
+ *
10
+ * Every field of ProviderRegistryEntry is classified below, including the ones that carry no
11
+ * model identity at all. That is deliberate: the "satisfies" check makes a new registry field a
12
+ * TYPE error until somebody decides what its keys mean, rather than letting it default to
13
+ * invisible. The two checks are different and both matter - the satisfies clause proves the
14
+ * classification is exhaustive when this compiles, and the parity test proves the fields the
15
+ * shipped registry actually carries are the ones classified here.
16
+ *
17
+ * These are decode hints and nothing more. A hint does not publish a catalog row, grant
18
+ * availability or entitlement, and it does not confer transport authority: the caller still
19
+ * checks registry transport identity before asking for them, and an ambiguous selector is still
20
+ * rejected rather than guessed.
21
+ */
22
+ type RegistryFieldModelIdRole =
23
+ | { readonly kind: "none" }
24
+ | { readonly kind: "record-keys" }
25
+ | { readonly kind: "nested-record-keys"; readonly key: "modelSupportsServiceTier" };
26
+
27
+ const NONE = { kind: "none" } as const;
28
+ const RECORD_KEYS = { kind: "record-keys" } as const;
29
+ /**
30
+ * Key-auth declares its own service-tier support under a nested map.
31
+ *
32
+ * Reading its keys is identity evidence only. It does not activate the key-auth service tier,
33
+ * which stays the business of the service-tier resolver.
34
+ */
35
+ const KEY_AUTH_SERVICE_TIER = { kind: "nested-record-keys", key: "modelSupportsServiceTier" } as const;
36
+
37
+ /**
38
+ * Not a configuration surface. Exported so the parity suite can compare this classification
39
+ * against the fields the shipped registry entries actually carry at runtime.
40
+ */
41
+ export const REGISTRY_FIELD_MODEL_ID_ROLES = {
42
+ id: NONE,
43
+ label: NONE,
44
+ adapter: NONE,
45
+ baseUrl: NONE,
46
+ apiKeyTransport: NONE,
47
+ alias: NONE,
48
+ authKind: NONE,
49
+ codexAccountMode: NONE,
50
+ allowKeyAuthOverride: NONE,
51
+ allowPrivateNetworkByDefault: NONE,
52
+ keyOptional: NONE,
53
+ apiKeyValidation: NONE,
54
+ freeTier: NONE,
55
+ allowBaseUrlOverride: NONE,
56
+ preserveCustomDestination: NONE,
57
+ baseUrlChoices: NONE,
58
+ staticHeaders: NONE,
59
+ modelSuffixBracketStrip: NONE,
60
+ featured: NONE,
61
+ sponsor: NONE,
62
+ dashboardPreset: NONE,
63
+ note: NONE,
64
+ dashboardUrl: NONE,
65
+ defaultModel: NONE,
66
+ models: NONE,
67
+ liveModels: NONE,
68
+ modelWireDefaults: RECORD_KEYS,
69
+ fastWire: NONE,
70
+ modelResponsesUpstreamStreaming: RECORD_KEYS,
71
+ modelResponsesTerminalRepair: RECORD_KEYS,
72
+ responsesItemIdRepair: NONE,
73
+ responsesPath: NONE,
74
+ chatCompletionsPath: NONE,
75
+ destinationAliases: NONE,
76
+ statelessResponses: NONE,
77
+ requiresAdjacentResponsesToolResults: NONE,
78
+ requiresPairedResponsesToolResults: NONE,
79
+ annotateEmptyToolOutputs: NONE,
80
+ supportsServiceTier: NONE,
81
+ supportsOpenAiWebSearchToolFields: NONE,
82
+ supportsResponsesCustomTools: NONE,
83
+ modelSupportsServiceTier: RECORD_KEYS,
84
+ keyAuthServiceTier: KEY_AUTH_SERVICE_TIER,
85
+ fastTierDescription: NONE,
86
+ modelServiceTierCapabilityBaseUrlGuard: NONE,
87
+ preserveResponsesReasoningContent: NONE,
88
+ dropResponsesReasoningItems: NONE,
89
+ modelSupportsReasoningSummaries: RECORD_KEYS,
90
+ modelSupportsVerbosity: RECORD_KEYS,
91
+ supportsVerbosity: NONE,
92
+ modelDiscovery: NONE,
93
+ contextWindow: NONE,
94
+ modelContextWindows: RECORD_KEYS,
95
+ modelDisplayNames: RECORD_KEYS,
96
+ modelInputModalities: RECORD_KEYS,
97
+ defaultMaxOutputTokens: NONE,
98
+ modelMaxOutputTokens: RECORD_KEYS,
99
+ reasoningEfforts: NONE,
100
+ modelReasoningEfforts: RECORD_KEYS,
101
+ modelDefaultReasoningEfforts: RECORD_KEYS,
102
+ reasoningEffortMap: NONE,
103
+ modelReasoningEffortMap: RECORD_KEYS,
104
+ directReasoningEffortModels: NONE,
105
+ reasoningWireFormat: NONE,
106
+ noVisionModels: NONE,
107
+ noReasoningModels: NONE,
108
+ noTemperatureModels: NONE,
109
+ noTopPModels: NONE,
110
+ noPenaltyModels: NONE,
111
+ noJsonSchemaModels: NONE,
112
+ parallelToolCalls: NONE,
113
+ promptCacheKey: NONE,
114
+ chatServiceTier: NONE,
115
+ openaiChatEofTolerance: NONE,
116
+ autoToolChoiceOnlyModels: NONE,
117
+ preserveReasoningContentModels: NONE,
118
+ requiresReasoningPlaceholderModels: NONE,
119
+ showThinkingSummary: NONE,
120
+ reasoningSplitModels: NONE,
121
+ reasoningDetailsModels: NONE,
122
+ thinkingToggleModels: NONE,
123
+ thinkingBudgetModels: NONE,
124
+ escapeBuiltinToolNames: NONE,
125
+ oauthId: NONE,
126
+ virtualModels: RECORD_KEYS,
127
+ modelMaxInputTokens: RECORD_KEYS,
128
+ jawcodeBundle: NONE,
129
+ extraMetadataAliases: NONE,
130
+ metadataModelIdNormalize: NONE,
131
+ googleMode: NONE,
132
+ project: NONE,
133
+ location: NONE,
134
+ } satisfies Record<keyof ProviderRegistryEntry, RegistryFieldModelIdRole>;
135
+
136
+ /**
137
+ * The native model ids this registry entry names, in registry-field order, first seen wins.
138
+ *
139
+ * Only KEYS are collected. virtualModels is a map whose keys are the selected identities and
140
+ * whose values name a transport target, so its wireModelId values are deliberately not read:
141
+ * a wire target is not something a client may select by name.
142
+ */
143
+ export function registryModelIdKeys(entry: ProviderRegistryEntry): readonly string[] {
144
+ const ids: string[] = [];
145
+ const seen = new Set<string>();
146
+ const add = (key: string): void => {
147
+ if (key.length === 0 || seen.has(key)) return;
148
+ seen.add(key);
149
+ ids.push(key);
150
+ };
151
+ for (const field of Object.keys(REGISTRY_FIELD_MODEL_ID_ROLES) as (keyof ProviderRegistryEntry)[]) {
152
+ const role = REGISTRY_FIELD_MODEL_ID_ROLES[field];
153
+ if (role.kind === "none") continue;
154
+ if (role.kind === "record-keys") {
155
+ const map = entry[field];
156
+ if (typeof map !== "object" || map === null) continue;
157
+ for (const key of Object.keys(map)) add(key);
158
+ continue;
159
+ }
160
+ // The nested role belongs to keyAuthServiceTier alone, so its owner is read by name.
161
+ // ProviderRegistryEntry is an interface and carries no implicit index signature, so a
162
+ // by-name read is what keeps this loop typed instead of asserted through unknown.
163
+ const nested = entry.keyAuthServiceTier?.[role.key];
164
+ if (typeof nested !== "object" || nested === null) continue;
165
+ for (const key of Object.keys(nested)) add(key);
166
+ }
167
+ return Object.freeze(ids);
168
+ }
@@ -177,6 +177,15 @@ export const META_MUSE_REASONING_EFFORTS = ["minimal", "low", "medium", "high",
177
177
  export const META_MUSE_REASONING_EFFORT_MAP: Record<string, string> = Object.fromEntries(
178
178
  META_MUSE_REASONING_EFFORTS.map(effort => [effort, effort]),
179
179
  );
180
+ /*
181
+ * Muse Code credentials have a separate capability contract. Meta's authenticated
182
+ * /muse-code/models roster advertises max for both 1.3 models, and the Responses API
183
+ * accepts it when the request identifies the Muse client surface.
184
+ */
185
+ export const META_MUSE_CODE_REASONING_EFFORTS = [...META_MUSE_REASONING_EFFORTS, "max"];
186
+ export const META_MUSE_CODE_REASONING_EFFORT_MAP: Record<string, string> = Object.fromEntries(
187
+ META_MUSE_CODE_REASONING_EFFORTS.map(effort => [effort, effort]),
188
+ );
180
189
  /** Both Muse Spark 1.3 tiers publish a 1,048,576-token window (dev.meta.ai/docs/models). */
181
190
  export const META_MUSE_CONTEXT_WINDOW = 1_048_576;
182
191
  export const META_MUSE_MODELS = ["muse-spark-1.3", "muse-spark-1.3-contributor"];
@@ -264,6 +264,8 @@ export interface ProviderRegistryEntry {
264
264
  modelServiceTierCapabilityBaseUrlGuard?: (baseUrl: string) => boolean;
265
265
  /** Registry default for plaintext reasoning replay; see `OcxProviderConfig.preserveResponsesReasoningContent`. Registry-only like `supportsServiceTier`. */
266
266
  preserveResponsesReasoningContent?: boolean;
267
+ /** Registry default for dropping replayed reasoning items for Responses upstreams that reject them. */
268
+ dropResponsesReasoningItems?: boolean;
267
269
  /** Registry defaults for per-model Codex reasoning propagation; explicit user keys win during enrichment. */
268
270
  modelSupportsReasoningSummaries?: Record<string, boolean>;
269
271
  /** Registry defaults for per-model Codex Responses verbosity support. */
@@ -0,0 +1,167 @@
1
+ import type { OcxProviderConfig } from "../types";
2
+ import { MODEL_ADAPTER_OVERRIDE_ALLOWED } from "../types";
3
+ import type { InboundWire, ModelWireDefault, ProviderRegistryEntry } from "./registry/types";
4
+
5
+ export type StaticPolicySource =
6
+ | "operator" | "operator-capability" | "captured-auth"
7
+ | "registry" | "hard-pin" | "provider-default" | "unknown";
8
+
9
+ export function detachedClone<T>(value: T): T {
10
+ if (Array.isArray(value)) return value.map(item => detachedClone(item)) as T;
11
+ if (value && typeof value === "object") {
12
+ return Object.fromEntries(Object.entries(value as Record<string, unknown>)
13
+ .map(([key, nested]) => [key, detachedClone(nested)])) as T;
14
+ }
15
+ return value;
16
+ }
17
+
18
+ export function recursivelyFreeze<T>(value: T): T {
19
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
20
+ for (const nested of Object.values(value as Record<string, unknown>)) recursivelyFreeze(nested);
21
+ return Object.freeze(value);
22
+ }
23
+
24
+ export function scalar<T>(operator: T | undefined, registry: T | undefined): [T | undefined, StaticPolicySource] {
25
+ if (operator !== undefined) return [detachedClone(operator), "operator"];
26
+ if (registry !== undefined) return [detachedClone(registry), "registry"];
27
+ return [undefined, "unknown"];
28
+ }
29
+
30
+ export function mapFill<T>(
31
+ registry: Readonly<Record<string, T>> | undefined,
32
+ operator: Readonly<Record<string, T>> | undefined,
33
+ ): [Record<string, T> | undefined, StaticPolicySource] {
34
+ if (!registry && !operator) return [undefined, "unknown"];
35
+ return [detachedClone({ ...(registry ?? {}), ...(operator ?? {}) }), operator ? "operator" : "registry"];
36
+ }
37
+
38
+ export function nestedMapFill(
39
+ registry: Readonly<Record<string, Record<string, string>>> | undefined,
40
+ operator: Readonly<Record<string, Record<string, string>>> | undefined,
41
+ ): [Record<string, Record<string, string>> | undefined, StaticPolicySource] {
42
+ if (!registry && !operator) return [undefined, "unknown"];
43
+ const merged: Record<string, Record<string, string>> = {};
44
+ for (const [key, value] of Object.entries(registry ?? {})) merged[key] = { ...value };
45
+ for (const [key, value] of Object.entries(operator ?? {})) {
46
+ merged[key] = { ...(merged[key] ?? {}), ...value };
47
+ }
48
+ return [merged, operator ? "operator" : "registry"];
49
+ }
50
+
51
+ export function positiveCapMap(
52
+ registry: Readonly<Record<string, number>> | undefined,
53
+ operator: Readonly<Record<string, number>> | undefined,
54
+ ): [Record<string, number> | undefined, StaticPolicySource] {
55
+ if (!registry && !operator) return [undefined, "unknown"];
56
+ const merged = { ...(registry ?? {}) };
57
+ for (const [key, value] of Object.entries(operator ?? {})) {
58
+ merged[key] = typeof merged[key] === "number" ? Math.min(merged[key]!, value) : value;
59
+ }
60
+ return [merged, operator ? "operator" : "registry"];
61
+ }
62
+
63
+ export function stableUnion(
64
+ registry: readonly string[] | undefined,
65
+ operator: readonly string[] | undefined,
66
+ ): [string[] | undefined, StaticPolicySource] {
67
+ if (!registry && !operator) return [undefined, "unknown"];
68
+ return [[...new Set([...(registry ?? []), ...(operator ?? [])])], operator ? "operator" : "registry"];
69
+ }
70
+
71
+ export function staticHeaders(
72
+ registry: Readonly<Record<string, string>> | undefined,
73
+ operator: Readonly<Record<string, string>> | undefined,
74
+ ): [Record<string, string> | undefined, StaticPolicySource] {
75
+ if (!registry && !operator) return [undefined, "unknown"];
76
+ if (!registry) return [{ ...(operator ?? {}) }, "operator"];
77
+ if (!operator) return [{ ...registry }, "registry"];
78
+ const claimed = new Set(Object.keys(operator).map(name => name.toLowerCase()));
79
+ const merged: Record<string, string> = { ...operator };
80
+ for (const [name, value] of Object.entries(registry)) {
81
+ if (!claimed.has(name.toLowerCase())) merged[name] = value;
82
+ }
83
+ return [merged, "operator"];
84
+ }
85
+
86
+ export function resolvedBaseUrl(
87
+ entry: Readonly<ProviderRegistryEntry> | undefined,
88
+ provider: Readonly<OcxProviderConfig>,
89
+ ): [string, StaticPolicySource] {
90
+ if (!entry) return [provider.baseUrl, "operator"];
91
+ const configured = typeof provider.baseUrl === "string" ? provider.baseUrl.trim() : "";
92
+ const configuredIsResolved = configured.length > 0 && !/\{[^}]*\}/.test(configured);
93
+ if (entry.allowBaseUrlOverride && !configuredIsResolved) {
94
+ throw new Error(`Invalid baseUrl for provider "${entry.id}": expected a nonblank URL without unresolved placeholders`);
95
+ }
96
+ return (/\{[^}]*\}/.test(entry.baseUrl) || entry.allowBaseUrlOverride) && configuredIsResolved
97
+ ? [configured, "operator"]
98
+ : [entry.baseUrl, "registry"];
99
+ }
100
+
101
+ export function sameStringArray(left: readonly string[] | undefined, right: readonly string[]): boolean {
102
+ return left?.length === right.length && left.every((value, index) => value === right[index]);
103
+ }
104
+
105
+ /** Legacy config-map lookup: exact id, colon family, then case-folded exact id. */
106
+ export function legacyModelValue<T>(record: Readonly<Record<string, T>> | undefined, modelId: string): T | undefined {
107
+ if (!record) return undefined;
108
+ if (Object.hasOwn(record, modelId)) return record[modelId];
109
+ const colon = modelId.indexOf(":");
110
+ if (colon > 0 && Object.hasOwn(record, modelId.slice(0, colon))) return record[modelId.slice(0, colon)];
111
+ const folded = modelId.toLowerCase();
112
+ return Object.entries(record).find(([key]) => key.toLowerCase() === folded)?.[1];
113
+ }
114
+
115
+ /** Provenance from the same merged-key walk as legacyModelValue. */
116
+ export function legacyModelSource<T>(
117
+ operator: Readonly<Record<string, T>> | undefined,
118
+ registry: Readonly<Record<string, T>> | undefined,
119
+ modelId: string,
120
+ ): StaticPolicySource {
121
+ const merged = { ...(registry ?? {}), ...(operator ?? {}) };
122
+ let winningKey: string | undefined;
123
+ if (Object.hasOwn(merged, modelId)) winningKey = modelId;
124
+ const colon = modelId.indexOf(":");
125
+ if (winningKey === undefined && colon > 0 && Object.hasOwn(merged, modelId.slice(0, colon))) {
126
+ winningKey = modelId.slice(0, colon);
127
+ }
128
+ if (winningKey === undefined) {
129
+ const folded = modelId.toLowerCase();
130
+ winningKey = Object.keys(merged).find(key => key.toLowerCase() === folded);
131
+ }
132
+ if (winningKey === undefined) return "unknown";
133
+ return Object.hasOwn(operator ?? {}, winningKey) ? "operator" : "registry";
134
+ }
135
+
136
+ export function anthropicFamilyContextWindow(
137
+ record: Readonly<Record<string, number>> | undefined,
138
+ id: string,
139
+ ): number | undefined {
140
+ if (!record || !id.toLowerCase().startsWith("claude-")) return undefined;
141
+ let candidate = id;
142
+ while (true) {
143
+ const cut = candidate.lastIndexOf("-");
144
+ if (cut <= 0 || !/^\d+$/.test(candidate.slice(cut + 1))) return undefined;
145
+ candidate = candidate.slice(0, cut);
146
+ const value = record[candidate]
147
+ ?? Object.entries(record).find(([key]) => key.toLowerCase() === candidate.toLowerCase())?.[1];
148
+ if (typeof value === "number" && value > 0) return value;
149
+ }
150
+ }
151
+
152
+ export function wireDefault(
153
+ declared: ModelWireDefault | undefined,
154
+ provider: Readonly<OcxProviderConfig>,
155
+ entry: Readonly<ProviderRegistryEntry> | undefined,
156
+ inbound: InboundWire,
157
+ effectiveAuthMode: OcxProviderConfig["authMode"] | undefined,
158
+ ): string | undefined {
159
+ if (declared === undefined || !MODEL_ADAPTER_OVERRIDE_ALLOWED.has(provider.adapter)) return undefined;
160
+ if (typeof declared !== "string") {
161
+ if (!declared.inbound.includes(inbound)) return undefined;
162
+ const authMode = effectiveAuthMode ?? provider.authMode ?? entry?.authKind;
163
+ if (declared.authModes && authMode && !declared.authModes.includes(authMode)) return undefined;
164
+ }
165
+ const wire = typeof declared === "string" ? declared : declared.wire;
166
+ return MODEL_ADAPTER_OVERRIDE_ALLOWED.has(wire) ? wire : undefined;
167
+ }