@bitkyc08/opencodex 2.83.0 → 2.84.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 (216) hide show
  1. package/gui/dist/assets/App-CZtqiGcw.js +56 -0
  2. package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
  3. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  4. package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
  5. package/gui/dist/assets/{quota-summary-DiJO6sYY.js → quota-summary-BbEeRfPY.js} +1 -1
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +2 -2
  8. package/src/adapters/coding-agent/protocol.ts +15 -8
  9. package/src/adapters/ollama-native.ts +29 -1
  10. package/src/adapters/openai-chat/messages.ts +8 -4
  11. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  12. package/src/adapters/openai-responses/passthrough.ts +3 -0
  13. package/src/adapters/openai-responses/request-strips.ts +10 -2
  14. package/src/adapters/qoder/adapter.ts +16 -14
  15. package/src/bridge/errors.ts +19 -0
  16. package/src/bridge.ts +1 -1
  17. package/src/claude/context-windows.ts +54 -1
  18. package/src/claude/intercept/connect-proxy.ts +10 -3
  19. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  20. package/src/claude/intercept/picker-listener.ts +49 -1
  21. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  22. package/src/claude/intercept/settings.ts +101 -9
  23. package/src/cli/account-api.ts +65 -0
  24. package/src/cli/account-auth.ts +26 -2
  25. package/src/cli/account.ts +41 -5
  26. package/src/cli/capabilities-access-remote.ts +8 -0
  27. package/src/cli/capabilities-base.ts +13 -1
  28. package/src/cli/catalog-command-result.ts +2 -1
  29. package/src/cli/claude.ts +15 -6
  30. package/src/cli/config-command.ts +8 -2
  31. package/src/cli/debug.ts +8 -0
  32. package/src/cli/dispatch.ts +16 -3
  33. package/src/cli/export-command.ts +14 -1
  34. package/src/cli/hub-disable.ts +99 -0
  35. package/src/cli/hub.ts +4 -2
  36. package/src/cli/index.ts +13 -4
  37. package/src/cli/link-child.ts +104 -0
  38. package/src/cli/link-enrollment.ts +96 -0
  39. package/src/cli/link.ts +39 -24
  40. package/src/cli/observe-stream.ts +6 -1
  41. package/src/cli/observe.ts +10 -2
  42. package/src/cli/provider.ts +12 -0
  43. package/src/cli/registry.ts +5 -4
  44. package/src/cli/runtime-api.ts +27 -2
  45. package/src/cli/status.ts +104 -20
  46. package/src/client/connect.ts +39 -4
  47. package/src/client/hub-client.ts +14 -7
  48. package/src/client/hub-state.ts +25 -14
  49. package/src/client/link-home.ts +248 -0
  50. package/src/client/link-management-relay.ts +61 -0
  51. package/src/client/link-relay.ts +6 -1
  52. package/src/client/link-status.ts +14 -0
  53. package/src/client/machine-api.ts +14 -7
  54. package/src/client/machine-listener.ts +29 -4
  55. package/src/clients/config-export.ts +46 -16
  56. package/src/clients/omo-role-models.ts +6 -3
  57. package/src/codex/agent-role-models.ts +26 -8
  58. package/src/codex/auth-api/account-list.ts +17 -2
  59. package/src/codex/auth-api/account-selection.ts +17 -7
  60. package/src/codex/auth-api/routes.ts +5 -1
  61. package/src/codex/autostart-health.ts +3 -1
  62. package/src/codex/catalog/bundled.ts +5 -1
  63. package/src/codex/catalog/effort.ts +2 -2
  64. package/src/codex/catalog/metadata.ts +4 -2
  65. package/src/codex/catalog/native-models.ts +16 -2
  66. package/src/codex/native-profile-startup.ts +4 -1
  67. package/src/codex/prompt-journal.ts +20 -2
  68. package/src/codex/prompt-layers/encoding.ts +49 -0
  69. package/src/codex/prompt-layers/import-source.ts +30 -0
  70. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  71. package/src/codex/prompt-layers/toml-read.ts +52 -4
  72. package/src/codex/prompt-layers.ts +383 -20
  73. package/src/codex/prompt-lock-claim.ts +13 -3
  74. package/src/codex/prompt-lock-evidence.ts +3 -3
  75. package/src/codex/prompt-lock-owner.ts +14 -6
  76. package/src/codex/prompt-text-probe.ts +34 -5
  77. package/src/codex/quota.ts +13 -6
  78. package/src/codex/role-auto-assign.ts +2 -5
  79. package/src/codex/routing/active-account.ts +21 -0
  80. package/src/codex/routing.ts +1 -1
  81. package/src/codex/runtime.ts +196 -8
  82. package/src/codex/shim-overlay.ts +8 -4
  83. package/src/codex/shim.ts +1 -1
  84. package/src/combos/failover.ts +3 -0
  85. package/src/config/api-key-reconcile.ts +34 -0
  86. package/src/config/diagnostics.ts +16 -0
  87. package/src/config/feature-flags.ts +5 -0
  88. package/src/config/live-reconcile.ts +43 -29
  89. package/src/config/live-topology.ts +29 -0
  90. package/src/config/load-degrade.ts +2 -1
  91. package/src/config/paths.ts +3 -0
  92. package/src/config/salvage.ts +3 -1
  93. package/src/config/schema/config-schema.ts +2 -0
  94. package/src/generated/compatibility-version.json +311 -171
  95. package/src/grok/grpc-web.ts +23 -1
  96. package/src/grok/reset-coupon-ledger.ts +75 -5
  97. package/src/grok/reset-coupons.ts +4 -2
  98. package/src/integrations/mutation-plan.ts +13 -7
  99. package/src/integrations/registry.ts +45 -11
  100. package/src/integrations/state.ts +19 -7
  101. package/src/integrations/target.ts +42 -5
  102. package/src/integrations/writer.ts +14 -6
  103. package/src/lib/app-owned-memory-stores.ts +4 -0
  104. package/src/lib/app-owned-memory.ts +11 -0
  105. package/src/lib/json-byte-size.ts +31 -0
  106. package/src/lib/optional-shutdown-hooks.ts +35 -0
  107. package/src/lib/outbound-credential-registry.ts +210 -0
  108. package/src/lib/passive-read.ts +35 -0
  109. package/src/lib/shadow-call.ts +44 -5
  110. package/src/link/codex-settings.ts +155 -0
  111. package/src/link/management-routes.ts +89 -0
  112. package/src/link/remote-probe.ts +38 -0
  113. package/src/link/ssh-argv.ts +26 -5
  114. package/src/link/ssh-runner.ts +58 -27
  115. package/src/link/status-projection.ts +22 -4
  116. package/src/link/supervisor.ts +144 -49
  117. package/src/link/tunnel-state.ts +11 -4
  118. package/src/oauth/anthropic-continuity.ts +4 -3
  119. package/src/oauth/anthropic-identity.ts +13 -2
  120. package/src/oauth/anthropic-model-quota.ts +13 -1
  121. package/src/oauth/anthropic-routing.ts +230 -23
  122. package/src/oauth/anthropic.ts +18 -4
  123. package/src/oauth/google-antigravity.ts +24 -7
  124. package/src/oauth/index.ts +24 -15
  125. package/src/oauth/kiro-account-identity.ts +106 -0
  126. package/src/oauth/kiro.ts +48 -5
  127. package/src/oauth/plan.ts +29 -0
  128. package/src/oauth/store.ts +40 -3
  129. package/src/oauth/types.ts +2 -0
  130. package/src/plugins/loader.ts +11 -0
  131. package/src/providers/anthropic-billing-ownership.ts +74 -0
  132. package/src/providers/anthropic-billing-recovery.ts +31 -0
  133. package/src/providers/anthropic-billing-refusal.ts +55 -0
  134. package/src/providers/anthropic-reset-admission.ts +62 -0
  135. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  136. package/src/providers/api-key-resolve.ts +17 -2
  137. package/src/providers/key-failover.ts +52 -5
  138. package/src/providers/model-rename-startup.ts +5 -3
  139. package/src/providers/quota/account-cache.ts +11 -0
  140. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  141. package/src/providers/quota/report-cache.ts +14 -0
  142. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  143. package/src/providers/quota.ts +4 -1
  144. package/src/providers/registry/entries-core.ts +14 -14
  145. package/src/providers/registry/entries-extended.ts +0 -1
  146. package/src/providers/registry/model-seeds.ts +17 -0
  147. package/src/providers/request-pacing.ts +9 -0
  148. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  149. package/src/responses/compaction.ts +14 -0
  150. package/src/responses/parser.ts +7 -1
  151. package/src/server/auth-cors.ts +42 -0
  152. package/src/server/configured-credentials.ts +194 -0
  153. package/src/server/index/link-management.ts +58 -0
  154. package/src/server/index/optional-listeners.ts +5 -1
  155. package/src/server/index/serve-options.ts +13 -0
  156. package/src/server/lifecycle.ts +2 -1
  157. package/src/server/management/agent-settings-routes.ts +28 -10
  158. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  159. package/src/server/management/codex-agent-role-routes.ts +19 -6
  160. package/src/server/management/codex-prompt-routes.ts +136 -5
  161. package/src/server/management/codex-role-auto-assign.ts +1 -1
  162. package/src/server/management/grok-coupon-routes.ts +78 -27
  163. package/src/server/management/link-enrollment.ts +192 -0
  164. package/src/server/management/link-route-state.ts +185 -0
  165. package/src/server/management/link-routes.ts +82 -402
  166. package/src/server/management/model-rows.ts +3 -1
  167. package/src/server/management/provider-overwrite-carry.ts +10 -0
  168. package/src/server/management/provider-routes.ts +1 -0
  169. package/src/server/management/route-registry.ts +5 -3
  170. package/src/server/management/shared.ts +3 -0
  171. package/src/server/management-api.ts +3 -1
  172. package/src/server/messages-native.ts +68 -42
  173. package/src/server/relay-eager.ts +2 -0
  174. package/src/server/request-log-filter.ts +6 -1
  175. package/src/server/request-log-terminal-status.ts +63 -6
  176. package/src/server/responses/adapter-dispatch.ts +232 -79
  177. package/src/server/responses/code-mode-collab-surface.ts +205 -0
  178. package/src/server/responses/codex-ws-exchange.ts +147 -6
  179. package/src/server/responses/codex-ws-wire.ts +203 -0
  180. package/src/server/responses/collaboration.ts +36 -7
  181. package/src/server/responses/compact.ts +126 -48
  182. package/src/server/responses/context-overflow.ts +13 -1
  183. package/src/server/responses/core-codex-account.ts +105 -1
  184. package/src/server/responses/core-combo-failure.ts +14 -0
  185. package/src/server/responses/core-combo.ts +7 -2
  186. package/src/server/responses/core-errors.ts +29 -1
  187. package/src/server/responses/core-options.ts +2 -0
  188. package/src/server/responses/deferred-collab-surface.ts +158 -0
  189. package/src/server/responses/deferred-send-reporter.ts +25 -0
  190. package/src/server/responses/fetch-helpers.ts +12 -1
  191. package/src/server/responses/passthrough-delivery.ts +4 -1
  192. package/src/server/responses/passthrough-dispatch.ts +15 -4
  193. package/src/server/responses/request-prepare.ts +6 -4
  194. package/src/server/responses/request-transport.ts +6 -8
  195. package/src/server/responses/sidecar-execution.ts +11 -2
  196. package/src/server/responses/ws-upstream.ts +9 -1
  197. package/src/server/startup-action-control.ts +22 -2
  198. package/src/server/startup-health-cache.ts +4 -0
  199. package/src/server/system-env-shell.ts +7 -2
  200. package/src/server/system-env.ts +11 -1
  201. package/src/service/cli.ts +5 -1
  202. package/src/service/desktop-startup.ts +147 -34
  203. package/src/service/desktop-supervision.d.mts +27 -1
  204. package/src/service/desktop-supervision.mjs +111 -5
  205. package/src/service/health-sentinel-launch.ts +176 -0
  206. package/src/service/health-sentinel-main.ts +447 -0
  207. package/src/service/health-sentinel-policy.ts +241 -0
  208. package/src/service/health-sentinel-probe.ts +54 -0
  209. package/src/service/health-sentinel-windows.ts +87 -0
  210. package/src/service/linux-linger.ts +20 -0
  211. package/src/types/config.ts +5 -0
  212. package/src/usage/expected-prices.ts +4 -0
  213. package/src/usage/log.ts +19 -1
  214. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  215. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  216. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
@@ -44,6 +44,8 @@ interface OperationRecord {
44
44
  status: "open" | "settled";
45
45
  code?: string;
46
46
  resetsLeft?: number | null;
47
+ /** Windows the settled claim reported cleared; absent on records written before this field. */
48
+ cleared?: string[];
47
49
  attempts: number;
48
50
  leaseUntil: number;
49
51
  createdAt: number;
@@ -64,7 +66,7 @@ export interface AnthropicResetOperationIdentity {
64
66
 
65
67
  export type AnthropicResetBegin =
66
68
  | { kind: "execute"; attempt: number }
67
- | { kind: "replay"; code: string; resetsLeft: number | null; settledAt: number }
69
+ | { kind: "replay"; code: string; resetsLeft: number | null; settledAt: number; claimedAt: number; cleared: string[] }
68
70
  | { kind: "identity-mismatch" }
69
71
  | { kind: "in-flight" }
70
72
  | { kind: "expired" }
@@ -102,7 +104,9 @@ function isRecordShape(value: unknown): value is OperationRecord {
102
104
  return typeof record.accountId === "string" && typeof record.grantId === "string"
103
105
  && typeof record.orgDigest === "string" && (record.status === "open" || record.status === "settled")
104
106
  && typeof record.attempts === "number" && typeof record.leaseUntil === "number"
105
- && typeof record.createdAt === "number" && typeof record.updatedAt === "number";
107
+ && typeof record.createdAt === "number" && typeof record.updatedAt === "number"
108
+ && (record.cleared === undefined
109
+ || Array.isArray(record.cleared) && record.cleared.every(window => typeof window === "string"));
106
110
  }
107
111
 
108
112
  /** Missing file → empty journal. Present but unreadable → throws; never an empty fallback. */
@@ -197,7 +201,10 @@ export function beginAnthropicResetOperation(
197
201
  return { kind: "identity-mismatch" };
198
202
  }
199
203
  if (existing.status === "settled") {
200
- return { kind: "replay", code: existing.code ?? "unavailable", resetsLeft: existing.resetsLeft ?? null, settledAt: existing.updatedAt };
204
+ return {
205
+ kind: "replay", code: existing.code ?? "unavailable", resetsLeft: existing.resetsLeft ?? null,
206
+ settledAt: existing.updatedAt, claimedAt: existing.createdAt, cleared: existing.cleared ?? [],
207
+ };
201
208
  }
202
209
  if (existing.leaseUntil > now) return { kind: "in-flight" };
203
210
  if (now - existing.createdAt >= ANTHROPIC_RESET_RETRY_WINDOW_MS) return { kind: "expired" };
@@ -232,6 +239,12 @@ export function beginAnthropicResetOperation(
232
239
  export interface AnthropicResetSettlement {
233
240
  code: string;
234
241
  resetsLeft: number | null;
242
+ /** When the canonical (first) settlement was journaled, not when this caller arrived. */
243
+ settledAt: number;
244
+ /** When the operation was first journaled, before its first claim was sent. */
245
+ claimedAt: number;
246
+ /** Windows the canonical settlement reported cleared. */
247
+ cleared: string[];
235
248
  }
236
249
 
237
250
  /**
@@ -241,7 +254,7 @@ export interface AnthropicResetSettlement {
241
254
  * throws, because an answer with no journal entry cannot be reported as durable.
242
255
  */
243
256
  export function settleAnthropicResetOperation(
244
- settlement: { operationId: string; code: string; resetsLeft: number | null },
257
+ settlement: { operationId: string; code: string; resetsLeft: number | null; cleared?: readonly string[] },
245
258
  options: AnthropicResetLedgerOptions = {},
246
259
  ): AnthropicResetSettlement {
247
260
  const now = options.now ?? Date.now();
@@ -250,14 +263,23 @@ export function settleAnthropicResetOperation(
250
263
  const journal = readJournal(filePath);
251
264
  const existing = journal.operations[settlement.operationId];
252
265
  if (!existing) throw new AnthropicResetLedgerError("unavailable");
253
- if (existing.status === "settled") return { code: existing.code ?? "unavailable", resetsLeft: existing.resetsLeft ?? null };
266
+ if (existing.status === "settled") {
267
+ return {
268
+ code: existing.code ?? "unavailable", resetsLeft: existing.resetsLeft ?? null,
269
+ settledAt: existing.updatedAt, claimedAt: existing.createdAt, cleared: existing.cleared ?? [],
270
+ };
271
+ }
254
272
  existing.status = "settled";
255
273
  existing.code = settlement.code;
256
274
  existing.resetsLeft = settlement.resetsLeft;
275
+ existing.cleared = [...(settlement.cleared ?? [])];
257
276
  existing.leaseUntil = 0;
258
277
  existing.updatedAt = now;
259
278
  writeJournal(filePath, journal, now);
260
- return { code: settlement.code, resetsLeft: settlement.resetsLeft };
279
+ return {
280
+ code: settlement.code, resetsLeft: settlement.resetsLeft, settledAt: now,
281
+ claimedAt: existing.createdAt, cleared: [...existing.cleared],
282
+ };
261
283
  });
262
284
  }
263
285
 
@@ -39,17 +39,25 @@ function defaultEntryFactory(service: string, account: string): ProviderKeychain
39
39
  let entryFactory: ProviderKeychainEntryFactory = defaultEntryFactory;
40
40
  const resolvedCache = new Map<string, string>();
41
41
  const warnedAccounts = new Set<string>();
42
+ let resolutionGeneration = 0;
43
+
44
+ /** Advances whenever cached secrets may have changed, so derived snapshots can rebuild. */
45
+ export function providerKeyResolutionGeneration(): number {
46
+ return resolutionGeneration;
47
+ }
42
48
 
43
49
  /** Test seam: swap the OS entry for an in-memory one and drop caches. */
44
50
  export function setProviderKeychainEntryFactoryForTests(factory: ProviderKeychainEntryFactory | null): void {
45
51
  entryFactory = factory ?? defaultEntryFactory;
46
52
  resolvedCache.clear();
53
+ resolutionGeneration++;
47
54
  warnedAccounts.clear();
48
55
  }
49
56
 
50
57
  /** Write-path seam: a store/restore mutated secrets, so cached reads and warnings are stale. */
51
58
  export function invalidateResolvedProviderKeyCache(): void {
52
59
  resolvedCache.clear();
60
+ resolutionGeneration++;
53
61
  warnedAccounts.clear();
54
62
  }
55
63
 
@@ -76,7 +84,7 @@ export function providerKeychainEntry(account: string): ProviderKeychainEntry {
76
84
  return entryFactory(PROVIDER_KEYCHAIN_SERVICE, account);
77
85
  }
78
86
 
79
- function readKeychain(account: string): string | undefined {
87
+ function readKeychain(account: string, quiet = false): string | undefined {
80
88
  const cached = resolvedCache.get(account);
81
89
  if (cached !== undefined) return cached;
82
90
  try {
@@ -88,7 +96,7 @@ function readKeychain(account: string): string | undefined {
88
96
  } catch {
89
97
  // fall through to the single warning below
90
98
  }
91
- if (!warnedAccounts.has(account)) {
99
+ if (!quiet && !warnedAccounts.has(account)) {
92
100
  warnedAccounts.add(account);
93
101
  console.warn(`[opencodex] provider key reference keychain:${account} could not be read from the OS keychain; requests for this provider have no credential until the keychain is available (no plaintext fallback)`);
94
102
  }
@@ -105,6 +113,13 @@ export function resolveProviderApiKey(value: string | undefined): string | undef
105
113
  return resolveEnvValue(value);
106
114
  }
107
115
 
116
+ /** The same resolution without the unreadable-reference warning, for observe-only callers. */
117
+ export function resolveProviderApiKeyQuietly(value: string | undefined): string | undefined {
118
+ if (!value) return undefined;
119
+ if (isKeychainReference(value)) return readKeychain(keychainAccount(value), true);
120
+ return resolveEnvValue(value);
121
+ }
122
+
108
123
  export type ProviderKeyStoreKind = "keychain" | "env" | "file" | "none";
109
124
 
110
125
  export function providerKeyStoreKind(provider: Pick<OcxProviderConfig, "apiKey"> | undefined): ProviderKeyStoreKind {
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Modelled after src/codex/routing.ts cooldown logic but scoped to plain API-key pools.
10
10
  */
11
- import { commitProviderApiKeySelection } from "./api-key-selection";
11
+ import { commitProviderApiKeySelection, providerApiKeySelectionIsCurrent } from "./api-key-selection";
12
12
  import type { ProviderApiKeySelection } from "../types/provider";
13
13
  import { routedProviderConfig } from "../router";
14
14
  import { getProviderRegistryEntry } from "./registry";
@@ -30,6 +30,16 @@ interface KeyCooldown {
30
30
 
31
31
  const DEFAULT_COOLDOWN_MS = 60_000;
32
32
  const MAX_COOLDOWN_MS = 10 * 60_000; // cap at 10 min for api-key rotation
33
+ /**
34
+ * Anthropic credit-balance refusal (#6958). An empty prepaid balance is account state, not a
35
+ * rate signal: it clears only when someone buys credits or auto-reload fires, so the
36
+ * ten-minute credential cap would hand a dead key back to rotation six times an hour. One hour
37
+ * keeps it out of replacement and proactive selection while a funded sibling exists, and still
38
+ * notices a top-up without a restart. When every key is held, requests keep using the active
39
+ * key and return Anthropic's original refusal. The hold is in memory; dashboard key management
40
+ * clears it.
41
+ */
42
+ export const ANTHROPIC_BILLING_COOLDOWN_MS = 60 * 60_000;
33
43
 
34
44
  /**
35
45
  * Cap for a cooldown the upstream itself dated, as opposed to one we inferred.
@@ -688,12 +698,13 @@ export function rateLimitRetryDelayMs(
688
698
  function rotateKeyAfterFailure(
689
699
  config: OcxConfig,
690
700
  providerName: string,
691
- failureStatus: 401 | 429,
701
+ failureStatus: 401 | 429 | "billing",
692
702
  retryAfterHeader: string | null | undefined,
693
703
  now = Date.now(),
694
704
  attemptedKey?: string,
695
705
  attemptedSelection?: ProviderApiKeySelection,
696
706
  quotaResetAt?: number,
707
+ acceptsFreshProvider?: (provider: OcxProviderConfig) => boolean,
697
708
  ): OcxProviderConfig | null {
698
709
  const provider = config.providers[providerName];
699
710
  if (!provider) return null;
@@ -704,6 +715,11 @@ function rotateKeyAfterFailure(
704
715
  | { failedId?: string; candidateId?: string }
705
716
  | { exhaustedCount: number; failedId?: string };
706
717
  const outcome = commitProviderApiKeySelection<Rotation | null>(config, providerName, freshProvider => {
718
+ // Run on every persisted transaction attempt, including a freshness rebase.
719
+ if (acceptsFreshProvider) {
720
+ try { if (!acceptsFreshProvider(freshProvider)) return { changed: false, value: null }; }
721
+ catch { return { changed: false, value: null }; }
722
+ }
707
723
  const pool = freshProvider.apiKeyPool;
708
724
  if (!pool || pool.length < 2) return { changed: false, value: null };
709
725
 
@@ -741,6 +757,8 @@ function rotateKeyAfterFailure(
741
757
  }, attemptedSelection);
742
758
  if (outcome.status === "unavailable") return null;
743
759
  if (outcome.status === "superseded") {
760
+ // Billing authority is bound to the sent selection; a newer manual choice grants no replay.
761
+ if (acceptsFreshProvider) return null;
744
762
  // A newer manual selection (including A→B→A) owns subsequent dispatch. Reusing the
745
763
  // same failed key here would loop forever; preserve its original failure instead.
746
764
  return outcome.provider.apiKey !== failedKey ? structuredClone(outcome.provider) : null;
@@ -750,7 +768,9 @@ function rotateKeyAfterFailure(
750
768
  // A 401 is a verdict about the credential itself, not a timing signal: the key is rejected
751
769
  // until an operator replaces it, and upstreams send no Retry-After for it. Hold it for the
752
770
  // full cap instead of the 429 default so a dead key is not re-tried once a minute.
753
- const cooldownMs = failureStatus === 401
771
+ const cooldownMs = failureStatus === "billing"
772
+ ? ANTHROPIC_BILLING_COOLDOWN_MS
773
+ : failureStatus === 401
754
774
  ? MAX_COOLDOWN_MS
755
775
  // A reset instant the upstream dated outranks both the header and the
756
776
  // default: it is the only one of the three that knows when the quota
@@ -762,7 +782,7 @@ function rotateKeyAfterFailure(
762
782
  sweepExpiredOnWrite(now);
763
783
  }
764
784
  if ("exhaustedCount" in outcome.value) {
765
- console.warn(`[key-failover] ${providerName}: all ${outcome.value.exhaustedCount} keys in cooldown after ${failureStatus}; returning the upstream status to the client`);
785
+ console.warn(`[key-failover] ${providerName}: all ${outcome.value.exhaustedCount} keys in cooldown after ${failureLabel(failureStatus)}; returning the upstream status to the client`);
766
786
  return null;
767
787
  }
768
788
 
@@ -771,12 +791,16 @@ function rotateKeyAfterFailure(
771
791
  if (outcome.value.candidateId) {
772
792
  console.warn(
773
793
  // Log ids only — labels are user-supplied free text and could carry secret material.
774
- `[key-failover] ${providerName}: ${failureStatus} on key ${outcome.value.failedId ?? "?"}; rotating to key ${outcome.value.candidateId}`,
794
+ `[key-failover] ${providerName}: ${failureLabel(failureStatus)} on key ${outcome.value.failedId ?? "?"}; rotating to key ${outcome.value.candidateId}`,
775
795
  );
776
796
  }
777
797
  return structuredClone(committed);
778
798
  }
779
799
 
800
+ function failureLabel(failureStatus: 401 | 429 | "billing"): string {
801
+ return failureStatus === "billing" ? "credit-balance refusal" : String(failureStatus);
802
+ }
803
+
780
804
  export function rotateKeyOn429(
781
805
  config: OcxConfig,
782
806
  providerName: string,
@@ -865,6 +889,29 @@ export function rotateProviderTransportOn401(
865
889
  return applyRotatedTransport(providerName, routedProvider, rotated, options.promptCacheKey);
866
890
  }
867
891
 
892
+ /** Exact Anthropic billing refusal: cool the key for ANTHROPIC_BILLING_COOLDOWN_MS. */
893
+ export function rotateProviderTransportOnAnthropicBilling(
894
+ config: OcxConfig,
895
+ providerName: string,
896
+ routedProvider: OcxProviderTransport,
897
+ options: Omit<RotateProviderTransportOptions, "retryAfter"> = {},
898
+ ): OcxProviderTransport | null {
899
+ if (routedProvider.adapter !== "anthropic"
900
+ || (routedProvider.authMode !== undefined && routedProvider.authMode !== "key")
901
+ || !hasKeyPoolFailover(routedProvider)) return null;
902
+ const headerShape = (provider: OcxProviderConfig) => JSON.stringify(Object.entries(provider.headers ?? {}).sort(([a], [b]) => a.localeCompare(b)));
903
+ const sentHeaders = headerShape(routedProvider);
904
+ const acceptsFreshProvider = (fresh: OcxProviderConfig): boolean => fresh.adapter === routedProvider.adapter
905
+ && (fresh.authMode === undefined || fresh.authMode === "key") && hasKeyPoolFailover(fresh)
906
+ && (fresh.apiKeyTransport ?? "x-api-key") === (routedProvider.apiKeyTransport ?? "x-api-key")
907
+ && headerShape(fresh) === sentHeaders
908
+ && providerApiKeySelectionIsCurrent({ ...config, providers: { ...config.providers, [providerName]: fresh } },
909
+ providerName, routedProvider);
910
+ const rotated = rotateKeyAfterFailure(config, providerName, "billing", null, options.now,
911
+ options.attemptedKey, options.attemptedSelection ?? routedProvider._apiKeyAttempt, undefined, acceptsFreshProvider);
912
+ return rotated ? applyRotatedTransport(providerName, routedProvider, rotated, options.promptCacheKey) : null;
913
+ }
914
+
868
915
  function applyRotatedTransport(
869
916
  providerName: string,
870
917
  routedProvider: OcxProviderTransport,
@@ -3,6 +3,7 @@ import { projectModelRenames } from "./model-rename-migration";
3
3
  import { projectStaleContextWindows } from "./stale-context-window-migration";
4
4
  import { projectStaleModelRosters } from "./stale-model-roster-migration";
5
5
  import { projectStaleVisionClassifications } from "./stale-vision-classification-migration";
6
+ import { projectStaleNoVisionSeeds } from "./stale-no-vision-seed-migration";
6
7
  import { projectDevinCliAuthMode } from "./devin-cli-authmode-migration";
7
8
  import type { OcxConfig } from "../types";
8
9
 
@@ -17,12 +18,13 @@ export function projectStartupConfigRepairs(config: OcxConfig): ReturnType<typeo
17
18
  const renames = projectModelRenames(config);
18
19
  const windows = projectStaleContextWindows(renames.config);
19
20
  const vision = projectStaleVisionClassifications(windows.config);
20
- const rosters = projectStaleModelRosters(vision.config);
21
+ const noVision = projectStaleNoVisionSeeds(vision.config);
22
+ const rosters = projectStaleModelRosters(noVision.config);
21
23
  const devinCli = projectDevinCliAuthMode(rosters.config);
22
24
  return {
23
25
  config: devinCli.config,
24
- changed: renames.changed || windows.changed || vision.changed || rosters.changed || devinCli.changed,
25
- warnings: [...renames.warnings, ...windows.warnings, ...vision.warnings, ...rosters.warnings, ...devinCli.warnings],
26
+ changed: renames.changed || windows.changed || vision.changed || noVision.changed || rosters.changed || devinCli.changed,
27
+ warnings: [...renames.warnings, ...windows.warnings, ...vision.warnings, ...noVision.warnings, ...rosters.warnings, ...devinCli.warnings],
26
28
  };
27
29
  }
28
30
 
@@ -473,6 +473,17 @@ export function clearAccountQuotaCache(provider?: string): void {
473
473
  persistAccountQuotaCache();
474
474
  }
475
475
 
476
+ /** Drop one account's cached bars and any joined probe so a reset is not ranked from the old window. */
477
+ export function forgetCachedProviderAccountQuota(provider: string, accountId: string): void {
478
+ hydrateAccountQuotaCache();
479
+ const key = accountCacheKey(provider, accountId);
480
+ accountQuotaCache.delete(key);
481
+ for (const flight of [...accountQuotaInflight.keys()]) {
482
+ if (flight === key || flight.startsWith(key + "\u0000")) accountQuotaInflight.delete(flight);
483
+ }
484
+ persistAccountQuotaCache();
485
+ }
486
+
476
487
  /**
477
488
  * Resolve a bearer for quota probing without silently adopting a newer global
478
489
  * Claude CLI credential into a background multiauth slot.
@@ -20,6 +20,8 @@ import {
20
20
  */
21
21
  export async function fetchAnthropicAccountQuota(
22
22
  instance: AnthropicInstanceId, accountId: string, force: boolean, config: OcxConfig = loadConfig(),
23
+ /** Never join an older account or vendor flight; a reset reread needs post-grant evidence. */
24
+ freshDispatch = false,
23
25
  ): Promise<AccountQuotaCacheEntry> {
24
26
  const unavailable = (): AccountQuotaCacheEntry => ({ ts: Date.now(), quota: null, unavailable: true });
25
27
  const target = config.providers[instance];
@@ -52,7 +54,7 @@ export async function fetchAnthropicAccountQuota(
52
54
  const flightKey = recovery.anthropicCredentialQuotaFlightKey(key, accountId);
53
55
  const flightCurrent = () => ownerCurrent()
54
56
  && recovery.anthropicCredentialQuotaFlightKey(key, accountId) === flightKey;
55
- const joinable = accountQuotaInflight.get(flightKey);
57
+ const joinable = freshDispatch ? undefined : accountQuotaInflight.get(flightKey);
56
58
  if (joinable) {
57
59
  const joined = await joinable;
58
60
  return ownerCurrent() && (joined.isCurrent?.() ?? flightCurrent()) ? joined : unavailable();
@@ -78,7 +80,7 @@ export async function fetchAnthropicAccountQuota(
78
80
  const result = await recovery.probeAnthropicQuotaWithRecovery(accountId, token, fresh => {
79
81
  if (!flightCurrent()) throw new AnthropicQuotaProbeOwnershipError("quota flight changed");
80
82
  recovery.assertAnthropicQuotaSendAllowed(accountId, token);
81
- return fetchAnthropicUsageQuotaForInstance(instance, token, fresh);
83
+ return fetchAnthropicUsageQuotaForInstance(instance, token, fresh || freshDispatch);
82
84
  }, () => ownerCurrent() && mayCommitAccountQuotaKey(key, writerGeneration));
83
85
  if (result && !result.isCurrent()) return unavailable();
84
86
  if (!result && !flightCurrent()) return unavailable();
@@ -344,3 +344,17 @@ export function isProviderQuotaReportCurrent(value: ProviderQuotaReport): boolea
344
344
  return (generation === undefined || isMainAccountIdentityGenerationLive(generation))
345
345
  && (accountReportCurrent.get(value)?.() ?? true);
346
346
  }
347
+
348
+ /**
349
+ * The reports this process already holds for the configured providers, for a passive read (#6959):
350
+ * no probe, token refresh, account snapshot or publication into routing evidence. The Home's own
351
+ * dashboard and traffic keep the cache current.
352
+ */
353
+ export function heldProviderQuotaReports(configuredProviders: readonly string[]): ProviderQuotaResponse {
354
+ const held = getProviderQuotaReportCache()?.response;
355
+ const configured = new Set(configuredProviders);
356
+ return {
357
+ generatedAt: held?.generatedAt ?? Date.now(),
358
+ reports: (held?.reports ?? []).filter(item => configured.has(item.provider) && isProviderQuotaReportCurrent(item)),
359
+ };
360
+ }
@@ -327,9 +327,9 @@ async function readAnthropicUsageQuota(accessToken: string): Promise<ProviderQuo
327
327
  * This endpoint reports quota only. Its body carries `five_hour`, `seven_day`, the
328
328
  * model-scoped weekly buckets (`seven_day_fable`/`_opus`/`_sonnet`) and a `limits` array,
329
329
  * and **no subscription or tier field** — nor does the OAuth token response, which yields only
330
- * `account.uuid` and `account.email_address` (`src/oauth/anthropic.ts`). That is why
331
- * `OAuthAccountSummary.plan` is `null` for Anthropic rather than populated here (#3777); it is
332
- * a missing upstream field, not an unfinished mapping.
330
+ * `account.uuid` and `account.email_address` (`src/oauth/anthropic.ts`). Subscription labels
331
+ * instead come from the authenticated profile in `src/oauth/anthropic-identity.ts`, never
332
+ * from this quota response.
333
333
  *
334
334
  * A tier must not be inferred from what is here. Percentages are normalized per account, so a
335
335
  * Max x5 seat at 50% is byte-identical to a Max x20 seat at 50%, and the presence of a
@@ -15,6 +15,7 @@ import {
15
15
  } from "./kiro-usage";
16
16
  import { mapQuotaRoster, readProviderApiKeyQuotas, type ProviderApiKeyQuota } from "./quota-key-accounts";
17
17
  import type { OcxConfig, OcxProviderConfig } from "../types";
18
+ import { isPassiveRead } from "../lib/passive-read";
18
19
  import type { ProviderQuota, QuotaFailureCode } from "./quota-types";
19
20
  import {
20
21
  accountReportCurrent,
@@ -23,7 +24,7 @@ import {
23
24
  captureProviderReportEpochs, providerReportEpochChanged,
24
25
  cacheKeyWithAggregationState,
25
26
  getProviderQuotaReportCache,
26
- hasCodexPoolProvider,
27
+ hasCodexPoolProvider, heldProviderQuotaReports,
27
28
  inflight,
28
29
  invalidationEpoch,
29
30
  isBuiltInChatGptForwardProvider,
@@ -229,6 +230,8 @@ export function flushProviderQuotaObservationsForTests(): Promise<void> {
229
230
  }
230
231
 
231
232
  export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh = false): Promise<ProviderQuotaResponse> {
233
+ // A passive read (a Remote Link Child's, #6959) shows the held reports and starts nothing.
234
+ if (isPassiveRead()) return heldProviderQuotaReports(Object.keys(config.providers));
232
235
  // A Pool report's cache signature and provider fetch must share one account snapshot.
233
236
  // Preserve force semantics when deciding whether that snapshot refreshes upstream data.
234
237
  const prefetchedCodexSnapshot = hasCodexPoolProvider(config)
@@ -32,12 +32,12 @@ import {
32
32
  OPENAI_API_GPT56_MAX_INPUT_TOKENS,
33
33
  OPENAI_API_GPT56_VIRTUAL_MODELS,
34
34
  OPENAI_API_GPT56_REASONING_EFFORTS,
35
- META_MUSE_REASONING_EFFORTS,
36
- META_MUSE_REASONING_EFFORT_MAP,
35
+ META_MODEL_API_REASONING_EFFORTS,
36
+ META_MODEL_API_REASONING_EFFORT_MAPS,
37
37
  META_MUSE_CODE_REASONING_EFFORTS,
38
38
  META_MUSE_CODE_REASONING_EFFORT_MAP,
39
39
  META_MUSE_CONTEXT_WINDOW,
40
- META_MUSE_MODELS,
40
+ META_MODEL_API_MODELS,
41
41
  OPENAI_DAYBREAK_MODELS,
42
42
  OPENAI_DAYBREAK_CONTEXT_WINDOWS,
43
43
  OPENAI_DAYBREAK_MAX_INPUT_TOKENS,
@@ -736,7 +736,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
736
736
  authKind: "key",
737
737
  dashboardUrl: "https://dev.meta.ai/docs/authentication",
738
738
  defaultModel: "muse-spark-1.3",
739
- models: META_MUSE_MODELS,
739
+ models: META_MODEL_API_MODELS,
740
740
  // Static roster: no authenticated /v1/models payload was ever observed (the only
741
741
  // contact was an unauthenticated GET returning 401 invalid_api_key), and Meta serves
742
742
  // non-agent families on this same base URL. Turning discovery on would publish an
@@ -746,20 +746,20 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
746
746
  // without this, registry transport canonicalization would retarget it and send their
747
747
  // saved key to Meta.
748
748
  preserveCustomDestination: true,
749
- modelContextWindows: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_CONTEXT_WINDOW])),
749
+ modelContextWindows: Object.fromEntries(META_MODEL_API_MODELS.map(id => [id, META_MUSE_CONTEXT_WINDOW])),
750
750
  // text+image only. Meta also documents video, audio (degraded on 1.3), and PDF, but
751
751
  // the catalog modality enum is text/image and over-advertising poisons the exported
752
752
  // client config (see tests/codex-integration/catalog-input-modality-enum.test.ts).
753
- modelInputModalities: Object.fromEntries(META_MUSE_MODELS.map(id => [id, ["text", "image"] as ["text", "image"]])),
754
- modelReasoningEfforts: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_REASONING_EFFORTS])),
755
- modelReasoningEffortMap: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_REASONING_EFFORT_MAP])),
753
+ modelInputModalities: Object.fromEntries(META_MODEL_API_MODELS.map(id => [id, ["text", "image"] as ["text", "image"]])),
754
+ modelReasoningEfforts: META_MODEL_API_REASONING_EFFORTS,
755
+ modelReasoningEffortMap: META_MODEL_API_REASONING_EFFORT_MAPS,
756
756
  // No defaultMaxOutputTokens: Meta publishes none. The only number in its docs
757
757
  // (131072) appears inside a third-party config sample, and the protocol pages call
758
758
  // the real limit "model-dependent".
759
759
  // Meta names its variable MODEL_API_KEY, but the env var opencodex reads is derived
760
760
  // from the provider id (META_MODEL_API_KEY). Saying only Meta's name would send a
761
761
  // user to export a variable this proxy never reads.
762
- note: "Pay-as-you-go Meta Model API. Get a key at https://dev.meta.ai (Meta calls it MODEL_API_KEY; export it here as META_MODEL_API_KEY) — a Meta developer account needs a payment method before it can serve requests, and every call is metered per token. A Muse Code subscription does NOT work here: Meta scopes that credential to the Muse Code CLI and bills any other key pay-as-you-go (dev.meta.ai/docs/muse-code/subscriptions). The Contributor tier (muse-spark-1.3-contributor) is cheap because Meta trains on your prompts — about 92% off input, 95% off output, 99% off cached input; do not send confidential material through it. Muse Spark is also reachable through resellers: command-code carries both tiers, opencode-go serves only muse-spark-1.3-contributor.",
762
+ note: "Pay-as-you-go Meta Model API. Get a key at https://dev.meta.ai (Meta calls it MODEL_API_KEY; export it here as META_MODEL_API_KEY) — a Meta developer account needs a payment method before it can serve requests, and every call is metered per token. A Muse Code subscription does NOT work here: Meta scopes that credential to the Muse Code CLI and bills any other key pay-as-you-go (dev.meta.ai/docs/muse-code/subscriptions). The Contributor tier (muse-spark-1.3-contributor) is cheap because Meta trains on your prompts — about 92% off input, 95% off output, 99% off cached input; do not send confidential material through it. Muse Wonder (muse-wonder-1.0, preview) is Standard mode only on this API ($4 input / $20 output per 1M tokens, no Contributor tier) and accepts reasoning effort up to max; the Spark rows stop at xhigh. Muse Spark is also reachable through resellers: command-code carries both tiers, opencode-go serves only muse-spark-1.3-contributor.",
763
763
  },
764
764
  /* [Decision Log]
765
765
  - 목적과 의도: Let an operator who already signed the Muse Code CLI in reach Muse Spark with that credential, instead of provisioning a second key.
@@ -787,14 +787,14 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
787
787
  oauthId: "meta-muse",
788
788
  dashboardUrl: "https://dev.meta.ai",
789
789
  defaultModel: "muse-spark-1.3",
790
- models: META_MUSE_MODELS,
790
+ models: META_MODEL_API_MODELS,
791
791
  // Same reason as meta-model: the authenticated roster carries muse-image-1.0 and
792
792
  // muse-voice-transcribe-1.0, which this Responses-agent provider cannot drive.
793
793
  liveModels: false,
794
- modelContextWindows: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_CONTEXT_WINDOW])),
795
- modelInputModalities: Object.fromEntries(META_MUSE_MODELS.map(id => [id, ["text", "image"] as ["text", "image"]])),
796
- modelReasoningEfforts: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_CODE_REASONING_EFFORTS])),
797
- modelReasoningEffortMap: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_CODE_REASONING_EFFORT_MAP])),
794
+ modelContextWindows: Object.fromEntries(META_MODEL_API_MODELS.map(id => [id, META_MUSE_CONTEXT_WINDOW])),
795
+ modelInputModalities: Object.fromEntries(META_MODEL_API_MODELS.map(id => [id, ["text", "image"] as ["text", "image"]])),
796
+ modelReasoningEfforts: Object.fromEntries(META_MODEL_API_MODELS.map(id => [id, META_MUSE_CODE_REASONING_EFFORTS])),
797
+ modelReasoningEffortMap: Object.fromEntries(META_MODEL_API_MODELS.map(id => [id, META_MUSE_CODE_REASONING_EFFORT_MAP])),
798
798
  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).",
799
799
  },
800
800
  {
@@ -1468,7 +1468,6 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1468
1468
  models: [...QODER_GLOBAL_MODELS],
1469
1469
  liveModels: true,
1470
1470
  reasoningEfforts: [...QODER_REASONING_EFFORTS],
1471
- noVisionModels: [...QODER_GLOBAL_MODELS],
1472
1471
  note: "Official Qoder Global CLI using QODER_PERSONAL_ACCESS_TOKEN. Models are discovered per account with `qoder --list-models`; the documented roster is a degraded fallback. The CLI runs single-turn with tools, MCP, settings hooks, and session persistence disabled. Requires `npm install -g @qoder-ai/qodercli`.",
1473
1472
  },
1474
1473
  {
@@ -255,6 +255,23 @@ export const META_MUSE_CODE_REASONING_EFFORT_MAP: Record<string, string> = Objec
255
255
  /** Both Muse Spark 1.3 tiers publish a 1,048,576-token window (dev.meta.ai/docs/models). */
256
256
  export const META_MUSE_CONTEXT_WINDOW = 1_048_576;
257
257
  export const META_MUSE_MODELS = ["muse-spark-1.3", "muse-spark-1.3-contributor"];
258
+ /*
259
+ * Muse Wonder (preview), announced 2026-10-12 (research.meta.ai/blog/muse-wonder). Per
260
+ * dev.meta.ai/docs/models and dev.meta.ai/docs/pricing-rate-limits, verified 2026-10-13:
261
+ * id muse-wonder-1.0, a 1,048,576-token window, reasoning effort "minimal" through "max",
262
+ * and Standard mode only on the Model API (no Contributor variant). Muse Wonder Full is
263
+ * reachable only inside Muse Code subscriptions and has no Model API id, so it is absent.
264
+ */
265
+ export const META_MUSE_WONDER_MODEL = "muse-wonder-1.0";
266
+ /** Roster both direct Meta providers serve: the Spark 1.3 tiers plus Muse Wonder. */
267
+ export const META_MODEL_API_MODELS = [...META_MUSE_MODELS, META_MUSE_WONDER_MODEL];
268
+ /** Model API ladders: Spark stops at xhigh; Wonder documents minimal..max. */
269
+ export const META_MODEL_API_REASONING_EFFORTS: Record<string, string[]> = Object.fromEntries(
270
+ META_MODEL_API_MODELS.map(id => [id, id === META_MUSE_WONDER_MODEL ? META_MUSE_CODE_REASONING_EFFORTS : META_MUSE_REASONING_EFFORTS]),
271
+ );
272
+ export const META_MODEL_API_REASONING_EFFORT_MAPS: Record<string, Record<string, string>> = Object.fromEntries(
273
+ META_MODEL_API_MODELS.map(id => [id, id === META_MUSE_WONDER_MODEL ? META_MUSE_CODE_REASONING_EFFORT_MAP : META_MUSE_REASONING_EFFORT_MAP]),
274
+ );
258
275
  /**
259
276
  * Daybreak program aliases. These `-latest` ids are the stable contract: OpenAI repoints
260
277
  * them at newer snapshots over time (red -> gpt-5.6-cyber, blue -> gpt-5.6-sol as of
@@ -33,6 +33,12 @@ export const REQUEST_PACING_CONCURRENCY_RETRY_AFTER_SECONDS = 5;
33
33
  let maxQueueDepth = REQUEST_PACING_MAX_QUEUE_DEPTH;
34
34
  let maxQueueAgeMs = REQUEST_PACING_MAX_QUEUE_AGE_MS;
35
35
 
36
+ // Optional owners register internal response markers; only this owned body rewrap carries them.
37
+ const responseProvenance = new Set<WeakMap<Response, unknown>>();
38
+ export function registerProviderResponseProvenance(markers: WeakMap<Response, unknown>): void {
39
+ responseProvenance.add(markers);
40
+ }
41
+
36
42
  export type RequestPacingQueueOverloadReason = "queue_full" | "queue_expired";
37
43
 
38
44
  export class RequestPacingQueueOverloadError extends Error {
@@ -588,6 +594,9 @@ export function trackProviderRequestSlotBody(
588
594
  // Retry helpers mark the response they return from, and recovery decisions key on these
589
595
  // identity markers; the rewrap must not make a non-replayable response look replayable.
590
596
  if (isNonReplayableResponse(response)) markResponseNonReplayable(wrapped);
597
+ for (const markers of responseProvenance) {
598
+ if (markers.has(response)) markers.set(wrapped, markers.get(response));
599
+ }
591
600
  return carryReplayRefusal(response, wrapped);
592
601
  }
593
602
 
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Drop a saved `noVisionModels` list that is still, byte for byte, a retired registry seed.
3
+ *
4
+ * `enrichProviderFromRegistry` fills `noVisionModels` all-or-nothing, so a provider row saved
5
+ * while the registry seeded a blanket list keeps that list after the registry stops seeding it.
6
+ * Qoder Global seeded its whole static discovery roster as text-only until the Global adapter
7
+ * began forwarding image input to the Qoder CLI (#6906). The saved copy would keep every image
8
+ * on the vision sidecar path, stripped or captioned before the adapter could forward it.
9
+ *
10
+ * This projection removes one thing: a saved list identical, in order, to a seed this file names,
11
+ * on a row the registry still owns (same adapter, and the registry transport rule enrichment uses),
12
+ * and only while the registry no longer seeds the list at all. Any edited list does not match and
13
+ * is left alone. `modelCapabilities` and `modelInputModalities` are untouched; they are where a
14
+ * deliberate per-model text-only declaration belongs.
15
+ */
16
+ import { PROVIDER_REGISTRY, providerMatchesRegistryTransport } from "./registry";
17
+ import type { OcxConfig } from "../types";
18
+
19
+ export interface StaleNoVisionSeed {
20
+ /** Registry provider id whose saved rows may carry the retired list. */
21
+ provider: string;
22
+ /** Every list the registry ever seeded, in seed order; only an exact match is removed. */
23
+ seeds: readonly (readonly string[])[];
24
+ }
25
+
26
+ export interface StaleNoVisionSeedProjection {
27
+ config: OcxConfig;
28
+ changed: boolean;
29
+ warnings: string[];
30
+ }
31
+
32
+ export const STALE_NO_VISION_SEEDS: readonly StaleNoVisionSeed[] = [
33
+ {
34
+ provider: "qoder",
35
+ seeds: [
36
+ // 2026-09-03 (Qoder Global PAT provider) until the DeepSeek V4.1 transition (#4282).
37
+ ["Qwen3.8-Max", "Qwen3.7-Max", "Qwen3.7-Plus", "Kimi-K3", "Kimi-K2.7-Code", "GLM-5.3", "GLM-5.2", "DeepSeek-V4-Pro"],
38
+ // #4282 until Global image input (#6906).
39
+ ["Qwen3.8-Max", "Qwen3.7-Max", "Qwen3.7-Plus", "Kimi-K3", "Kimi-K2.7-Code", "GLM-5.3", "GLM-5.2"],
40
+ ],
41
+ },
42
+ ];
43
+
44
+ function sameList(current: readonly unknown[], expected: readonly string[]): boolean {
45
+ return current.length === expected.length && current.every((value, index) => value === expected[index]);
46
+ }
47
+
48
+ /** Pure projection. The caller decides whether to persist. */
49
+ export function projectStaleNoVisionSeeds(
50
+ config: OcxConfig,
51
+ entries: readonly StaleNoVisionSeed[] = STALE_NO_VISION_SEEDS,
52
+ ): StaleNoVisionSeedProjection {
53
+ const warnings: string[] = [];
54
+ let changed = false;
55
+ for (const entry of entries) {
56
+ const prov = config.providers?.[entry.provider];
57
+ if (!prov || !Array.isArray(prov.noVisionModels)) continue;
58
+ const registry = PROVIDER_REGISTRY.find(row => row.id === entry.provider);
59
+ // While the registry still seeds a list, the saved one is not stale by this file's rule.
60
+ if (!registry || registry.noVisionModels !== undefined || registry.adapter !== prov.adapter) continue;
61
+ if (!providerMatchesRegistryTransport(entry.provider, prov)) continue;
62
+ const saved = prov.noVisionModels;
63
+ if (!entry.seeds.some(seed => sameList(saved, seed))) continue;
64
+ delete prov.noVisionModels;
65
+ changed = true;
66
+ warnings.push(
67
+ `removed the retired registry noVisionModels seed (${saved.length} model(s)) from "${entry.provider}"; `
68
+ + "image input now follows the provider's current classification.",
69
+ );
70
+ }
71
+ return { config, changed, warnings };
72
+ }
@@ -56,6 +56,20 @@ export const SUMMARY_PREFIX = "Another language model started to solve this prob
56
56
 
57
57
  export const OPAQUE_COMPACTION_NOTE = "[earlier conversation was compacted; the summary is stored in a format this model cannot read]";
58
58
 
59
+ /**
60
+ * Prompt contract for a routed model receiving a decoded `ocx1` checkpoint as user text (#6764).
61
+ * It preserves existing authorization scope and permission gates; it grants no authority.
62
+ * Kept apart from SUMMARY_PREFIX, which the v1 retention and recovery paths match exactly.
63
+ */
64
+ export const CHECKPOINT_CONTINUATION_NOTE = "This conversation was compacted automatically. A checkpoint summary from an earlier context window appears as a user message and records prior work. Treat earlier requests it reports as completed as history; do not repeat them unless a later user message asks you to. Continue an unfinished task only when it was already authorized and remains within the scope of that authorization. Do not ask for the same authorization again while it still applies. Preserve all existing permission and confirmation gates; if a decision or required permission is pending, wait for it before taking the dependent action. A checkpoint or delegation, including one from another thread, does not itself grant authority or expand the task's scope. Later user messages take priority; obey any pause, cancellation, replacement, or change of scope.";
65
+
66
+ /** Append the continuation note to existing instructions once. */
67
+ export function appendCheckpointContinuationNote(instructions: unknown): string {
68
+ const existing = typeof instructions === "string" ? instructions : "";
69
+ if (existing.includes(CHECKPOINT_CONTINUATION_NOTE)) return existing;
70
+ return existing.trim().length > 0 ? `${existing}\n\n${CHECKPOINT_CONTINUATION_NOTE}` : CHECKPOINT_CONTINUATION_NOTE;
71
+ }
72
+
59
73
  /**
60
74
  * Item types in the compact wire family. Each carries an `encrypted_content` blob the client
61
75
  * replays verbatim on every later turn, and the minting backend verifies it is unmodified.