@bitkyc08/opencodex 2.88.0 → 2.89.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 (205) hide show
  1. package/README.md +11 -1
  2. package/bin/ocx.mjs +15 -4
  3. package/gui/dist/assets/App-BnRSujOq.js +58 -0
  4. package/gui/dist/assets/App-dnkJ85te.css +1 -0
  5. package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-7z1COnmz.js} +1 -1
  6. package/gui/dist/assets/{index-B5bsvxor.js → index-uchD-ngf.js} +13 -13
  7. package/gui/dist/assets/{quota-summary-D8y2wTsB.js → quota-summary-BY_10IEz.js} +1 -1
  8. package/gui/dist/index.html +1 -1
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/cache-retention.ts +117 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -4
  12. package/src/adapters/anthropic.ts +10 -3
  13. package/src/adapters/coding-agent/protocol.ts +6 -0
  14. package/src/adapters/google-http.ts +3 -2
  15. package/src/adapters/google-vertex-endpoint.ts +67 -0
  16. package/src/adapters/google.ts +35 -29
  17. package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
  18. package/src/adapters/openai-responses/passthrough.ts +17 -12
  19. package/src/adapters/openai-responses/reasoning.ts +68 -1
  20. package/src/adapters/registry.ts +1 -1
  21. package/src/adapters/upstream-http-error.ts +3 -2
  22. package/src/adapters/zed.ts +7 -4
  23. package/src/bridge/internal.ts +11 -1
  24. package/src/bridge/response-json.ts +2 -2
  25. package/src/bridge/sse.ts +10 -4
  26. package/src/claude/inbound.ts +12 -26
  27. package/src/claude/outbound.ts +5 -0
  28. package/src/cli/account-api.ts +13 -1
  29. package/src/cli/account-extended.ts +17 -5
  30. package/src/cli/account-key-quota.ts +4 -1
  31. package/src/cli/account.ts +1 -1
  32. package/src/cli/capabilities-agents-routing.ts +45 -8
  33. package/src/cli/capabilities-base.ts +5 -5
  34. package/src/cli/capabilities-provider-models.ts +5 -4
  35. package/src/cli/combo-result.ts +8 -2
  36. package/src/cli/combo.ts +17 -0
  37. package/src/cli/help.ts +1 -1
  38. package/src/cli/message-command.ts +4 -1
  39. package/src/cli/message-remote-args.ts +69 -0
  40. package/src/cli/message-remote-command.ts +77 -0
  41. package/src/cli/provider-runtime.ts +69 -1
  42. package/src/cli/registry.ts +3 -3
  43. package/src/codex/account-pause.ts +6 -1
  44. package/src/codex/account-runtime-state.ts +5 -0
  45. package/src/codex/account-store.ts +30 -3
  46. package/src/codex/auth-api/account-list.ts +3 -0
  47. package/src/codex/auth-api/main-account-probe.ts +3 -2
  48. package/src/codex/auth-context.ts +9 -4
  49. package/src/codex/catalog/aggregation.ts +15 -0
  50. package/src/codex/catalog/auto-review.ts +2 -1
  51. package/src/codex/catalog/build-entries.ts +17 -7
  52. package/src/codex/catalog/derive-entry.ts +3 -1
  53. package/src/codex/catalog/metadata.ts +2 -0
  54. package/src/codex/catalog/model-visibility.ts +0 -2
  55. package/src/codex/catalog/ownership.ts +71 -0
  56. package/src/codex/catalog/parsing.ts +12 -3
  57. package/src/codex/catalog/retained-sync.ts +2 -1
  58. package/src/codex/catalog/routed-removal.ts +4 -4
  59. package/src/codex/convergence.ts +2 -1
  60. package/src/codex/low-quota-desktop-notifier.ts +125 -0
  61. package/src/codex/low-quota-protection.ts +57 -31
  62. package/src/codex/main-account-cache.ts +77 -3
  63. package/src/codex/main-account.ts +6 -2
  64. package/src/codex/native-residue.ts +2 -3
  65. package/src/codex/quota.ts +26 -2
  66. package/src/combos/failover.ts +2 -1
  67. package/src/combos/jev-model-backend.ts +30 -7
  68. package/src/combos/jev-model-request.ts +20 -0
  69. package/src/combos/jev-quota-activation.ts +153 -0
  70. package/src/combos/jev-quota-config.ts +62 -0
  71. package/src/combos/jev-quota-route.ts +26 -0
  72. package/src/combos/jev-quota.ts +88 -0
  73. package/src/combos/jev.ts +25 -12
  74. package/src/combos/types.ts +27 -0
  75. package/src/compatibility/index.ts +3 -0
  76. package/src/compatibility/openai-responses.ts +8 -1
  77. package/src/compatibility/opengateway-responses.ts +61 -0
  78. package/src/config/account-auto-pause-revisions.ts +136 -0
  79. package/src/config/diagnostics.ts +13 -0
  80. package/src/config/live-reconcile.ts +9 -0
  81. package/src/config/persist-unlocked.ts +10 -1
  82. package/src/config/removed-opencode-free.ts +135 -0
  83. package/src/config/schema/account-auto-pause.ts +14 -0
  84. package/src/config/schema/config-schema.ts +15 -0
  85. package/src/config/schema/leaf-validators.ts +7 -0
  86. package/src/config.ts +10 -3
  87. package/src/generated/compatibility-version.json +344 -144
  88. package/src/images/loop.ts +32 -9
  89. package/src/lib/account-auto-pause-response.ts +16 -0
  90. package/src/lib/bun-runtime.ts +18 -1
  91. package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
  92. package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
  93. package/src/lib/decision-quota-sink.ts +76 -0
  94. package/src/lib/state-store-registrations.ts +25 -0
  95. package/src/link/ssh-argv.ts +3 -0
  96. package/src/messaging/envelope.ts +6 -3
  97. package/src/messaging/remote-auth.ts +83 -0
  98. package/src/messaging/remote-bridge.ts +270 -0
  99. package/src/messaging/remote-contract.ts +108 -0
  100. package/src/messaging/remote-enrollment-recovery.ts +59 -0
  101. package/src/messaging/remote-enrollment.ts +244 -0
  102. package/src/messaging/remote-envelope.ts +40 -0
  103. package/src/messaging/remote-files.ts +117 -0
  104. package/src/messaging/remote-owner.ts +169 -0
  105. package/src/messaging/remote-ports.ts +56 -0
  106. package/src/messaging/remote-process.ts +131 -0
  107. package/src/messaging/remote-replies.ts +98 -0
  108. package/src/messaging/remote-rpc-admission.ts +32 -0
  109. package/src/messaging/remote-send.ts +72 -0
  110. package/src/messaging/remote-store.ts +111 -0
  111. package/src/messaging/remote-tunnels.ts +65 -0
  112. package/src/messaging/rpc.ts +13 -1
  113. package/src/oauth/account-auto-pause-policy.ts +66 -0
  114. package/src/oauth/account-auto-pause.ts +171 -0
  115. package/src/oauth/account-quota-rank.ts +2 -1
  116. package/src/oauth/anthropic-account-refusal.ts +6 -0
  117. package/src/oauth/anthropic-model-family.ts +8 -0
  118. package/src/oauth/anthropic-model-quota.ts +2 -6
  119. package/src/oauth/api-key-pool-preserve.ts +37 -0
  120. package/src/oauth/index.ts +5 -35
  121. package/src/oauth/store.ts +35 -8
  122. package/src/oauth/types.ts +2 -0
  123. package/src/plugins/upstream-hooks.ts +14 -1
  124. package/src/providers/api-key-pause.ts +32 -0
  125. package/src/providers/api-key-resolve.ts +10 -4
  126. package/src/providers/api-key-selection.ts +6 -1
  127. package/src/providers/api-keys.ts +8 -4
  128. package/src/providers/claude-cli-usage.ts +5 -0
  129. package/src/providers/key-failover.ts +11 -11
  130. package/src/providers/model-rename-fields.ts +1 -0
  131. package/src/providers/opencode-zen-rate-limit.ts +6 -72
  132. package/src/providers/quota/account-cache.ts +15 -1
  133. package/src/providers/quota/anthropic-account-quota.ts +2 -1
  134. package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
  135. package/src/providers/quota/anthropic-decision-windows.ts +47 -0
  136. package/src/providers/quota/antigravity.ts +39 -6
  137. package/src/providers/quota/report-cache.ts +14 -0
  138. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  139. package/src/providers/quota-decision-publication.ts +77 -0
  140. package/src/providers/quota-decision-snapshot.ts +145 -0
  141. package/src/providers/quota-routing-cache.ts +3 -0
  142. package/src/providers/quota-types.ts +6 -1
  143. package/src/providers/quota.ts +3 -2
  144. package/src/providers/registry/entries-extended.ts +8 -52
  145. package/src/providers/registry/model-seeds.ts +1 -1
  146. package/src/providers/registry.ts +1 -2
  147. package/src/providers/stale-vision-classification-migration.ts +1 -1
  148. package/src/responses/parser.ts +18 -10
  149. package/src/responses/web-search-replay.ts +138 -0
  150. package/src/routing/compatibility/behavior.ts +4 -1
  151. package/src/server/auth-cors.ts +7 -0
  152. package/src/server/background-lifecycle.ts +2 -1
  153. package/src/server/chat-native.ts +28 -13
  154. package/src/server/claude-messages.ts +2 -1
  155. package/src/server/index/optional-listeners.ts +6 -0
  156. package/src/server/management/combo-routes.ts +17 -2
  157. package/src/server/management/oauth-account-routes.ts +10 -1
  158. package/src/server/management/provider-completion-test.ts +289 -0
  159. package/src/server/management/provider-routes.ts +9 -1
  160. package/src/server/management/route-registry.ts +2 -0
  161. package/src/server/messages-native.ts +18 -4
  162. package/src/server/messages-shared.ts +4 -1
  163. package/src/server/request-log.ts +7 -2
  164. package/src/server/responses/account-auto-pause-recovery.ts +26 -0
  165. package/src/server/responses/adapter-dispatch.ts +77 -28
  166. package/src/server/responses/combo-session-recall.ts +25 -0
  167. package/src/server/responses/combo-stream-preflight.ts +3 -1
  168. package/src/server/responses/combo-tool-loop-pin.ts +70 -0
  169. package/src/server/responses/compact.ts +8 -5
  170. package/src/server/responses/context-overflow.ts +141 -2
  171. package/src/server/responses/core-codex-account.ts +7 -1
  172. package/src/server/responses/core-combo.ts +31 -5
  173. package/src/server/responses/core.ts +12 -9
  174. package/src/server/responses/empty-completion-guard.ts +2 -0
  175. package/src/server/responses/fetch-helpers.ts +35 -4
  176. package/src/server/responses/jev-model-invoke.ts +13 -14
  177. package/src/server/responses/native-steering-log.ts +2 -2
  178. package/src/server/responses/passthrough-delivery.ts +3 -2
  179. package/src/server/responses/passthrough-dispatch.ts +30 -33
  180. package/src/server/responses/passthrough-observations.ts +38 -0
  181. package/src/server/responses/request-compression.ts +98 -0
  182. package/src/server/responses/request-transport.ts +32 -3
  183. package/src/server/responses/run-turn-execution.ts +2 -0
  184. package/src/server/responses/sidecar-execution.ts +16 -3
  185. package/src/server/responses/terminal-guard.ts +2 -0
  186. package/src/server/startup-action-control.ts +2 -2
  187. package/src/server/startup-health-cache.ts +2 -2
  188. package/src/server/windows-tray-control.ts +2 -2
  189. package/src/service/definition.ts +9 -1
  190. package/src/types/account-auto-pause.ts +15 -0
  191. package/src/types/config.ts +23 -1
  192. package/src/types/provider.ts +9 -5
  193. package/src/types/request.ts +20 -2
  194. package/src/types.ts +1 -0
  195. package/src/usage/cost.ts +17 -1
  196. package/src/usage/expected-prices.ts +3 -1
  197. package/src/usage/jev-stats.ts +4 -0
  198. package/src/usage/log.ts +2 -0
  199. package/src/usage/summary.ts +7 -0
  200. package/src/web-search/loop.ts +37 -11
  201. package/src/web-search/progress-stream.ts +4 -1
  202. package/src/web-search/run-turn-loop.ts +9 -3
  203. package/src/web-search/sidecar-search-replay.ts +96 -0
  204. package/gui/dist/assets/App-CHC89EXQ.js +0 -58
  205. package/gui/dist/assets/App-wT3mXqFz.css +0 -1
@@ -4,6 +4,7 @@ import type { OcxConfig, OcxProviderConfig } from "../../types";
4
4
  import { PROVIDER_REGISTRY } from "../../providers/registry";
5
5
  import { fastPolicyForModel, serviceTierSupportFromPolicy } from "../../providers/service-tier";
6
6
  import { resolveProviderAuthTransport } from "../../providers/fastwire";
7
+ import { defaultAnthropicCacheRetention } from "../../adapters/anthropic/cache-retention";
7
8
  import { localFingerprint } from "../../lab/digest";
8
9
  import type { LabBehaviorSource, LabBehaviorValues } from "../../lab/live/types";
9
10
 
@@ -237,7 +238,9 @@ export function resolveProductionBehaviorValues(
237
238
  }),
238
239
  "tools.builtinNameEscaping": behaviorRow("provider_config", effective.escapeBuiltinToolNames === true),
239
240
  "cache.forwarding": behaviorRow("provider_config", effective.promptCacheKey === true),
240
- "cache.retention": behaviorRow("global_config", config.cacheRetention ?? "short"),
241
+ // The wire TTL, not just the setting: an omitted value means 1h on first-party Anthropic.
242
+ "cache.retention": behaviorRow("global_config", config.cacheRetention
243
+ ?? (adapter === "anthropic" ? defaultAnthropicCacheRetention(effective) : "short")),
241
244
  "anthropic.eofPolicy": behaviorRow("provider_config", effective.anthropicEofTolerance === true ? "tolerant" : "strict"),
242
245
  "openai-chat.eofPolicy": behaviorRow("provider_config", effective.openaiChatEofTolerance === true ? "tolerant" : "strict"),
243
246
  "google.mode": behaviorRow("provider_config", effective.googleMode ?? null),
@@ -40,6 +40,7 @@ import { DECLARABLE_HOSTED_TOOL_TYPES } from "../responses/hosted-tool-policy";
40
40
  import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
41
41
  import { providerConfigSeed } from "../providers/derive";
42
42
  import { anthropicOAuthInstanceConfigError } from "../config/schema/anthropic-account-pool";
43
+ import { accountAutoPausePolicySchema } from "../config/schema/account-auto-pause";
43
44
  import type { OcxConfig, OcxProviderConfig } from "../types";
44
45
  import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
45
46
  import { modelAutoCompactTokenLimitsConfigError } from "../providers/auto-compact-budget";
@@ -740,6 +741,9 @@ export function providerManagementConfigError(
740
741
  return "provider must be a plain object";
741
742
  }
742
743
  const raw = provider as Record<string, unknown>;
744
+ if (raw.accountAutoPause !== undefined && !accountAutoPausePolicySchema.safeParse(raw.accountAutoPause).success) {
745
+ return "accountAutoPause must be an object with optional boolean enabled and integer consecutiveFailures (1-100)";
746
+ }
743
747
  const contextTiersError = contextTierRecordConfigError(raw.modelContextTiers);
744
748
  if (contextTiersError) return contextTiersError;
745
749
  const capabilitiesError = modelCapabilitiesConfigError(raw.modelCapabilities);
@@ -796,6 +800,8 @@ export function providerManagementConfigError(
796
800
  // sends recover, not what the canonical forward seed is. Validated below
797
801
  // (retryOnResetPolicyConfigError).
798
802
  delete canonicalCandidate.retryOnReset;
803
+ // Validated account eligibility policy is independent of the canonical transport.
804
+ delete canonicalCandidate.accountAutoPause;
799
805
  // Context windows are the same kind of user-owned overlay as requestPacing: the operator
800
806
  // narrowing what their own native rows advertise. They can only ever LOWER the measured
801
807
  // window (see nativeOpenAiContextWindow), so admitting them cannot widen what the proxy
@@ -1087,6 +1093,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
1087
1093
  apiKeyPool: "redacted",
1088
1094
  // Ordering preference only; it names no key material, so an editor may read and set it.
1089
1095
  apiKeyPoolStrategy: "editor",
1096
+ accountAutoPause: "editor",
1090
1097
  apiKeySelectionRevision: "runtime",
1091
1098
  _apiKeyAttempt: "runtime",
1092
1099
  defaultModel: "editor",
@@ -1,5 +1,6 @@
1
1
  import type { OcxConfig, StorageCleanupPolicy } from "../types";
2
2
  import { registerCodexLowQuotaProtection, type LowQuotaRegistration } from "../codex/low-quota-protection";
3
+ import { resolveLowQuotaDesktopSink } from "../codex/low-quota-desktop-notifier";
3
4
  import type { LowQuotaEvent } from "../codex/low-quota-events";
4
5
  import { startStateStoreSweeper } from "../lib/state-store-sweeper";
5
6
  import {
@@ -209,7 +210,7 @@ export function acquireServerBackgroundLifecycle(
209
210
  fanoutRevoked: false,
210
211
  applyPolicy,
211
212
  resources: acquireServerResourceOwner(),
212
- lowQuota: registerCodexLowQuotaProtection(config),
213
+ lowQuota: registerCodexLowQuotaProtection(config, { desktopSink: () => resolveLowQuotaDesktopSink() }),
213
214
  };
214
215
  try {
215
216
  if (!processLoops) {
@@ -1,3 +1,4 @@
1
+ import { captureAccountAutoPauseAttempt, wasAccountAutoPaused } from "../oauth/account-auto-pause";
1
2
  import { createPhysicalSendReporter } from "../lib/request-execution-budget";
2
3
  import { createInferenceSendBudget } from "./inference/context";
3
4
  import { observeAdvisorNativeReasoning } from "./inference/advisor-diagnostics";
@@ -64,11 +65,10 @@ import { RequestPacingQueueOverloadError } from "../providers/request-pacing";
64
65
  import { stampApiKeyAccountLabel } from "../providers/label";
65
66
  import { distributedKeyPhysicalFetch, distributedKeyResponseOwner } from "../providers/api-key-response-ownership";
66
67
  import { providerApiKeySelectionIsCurrent, resolveCurrentProviderApiKeyTransport } from "../providers/api-key-selection";
67
- import { enrichOpenCodeZenFreeTierMessage } from "../providers/opencode-zen-rate-limit";
68
68
  import type { OcxProviderTransport } from "../providers/xai-transport";
69
69
  import type { RouteResult } from "../router";
70
70
  import type { OcxConfig, OcxProviderConfig } from "../types";
71
- import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, sendWithConnectionPolicy } from "./responses/fetch-helpers";
71
+ import { fetchWithHeaderTimeout, physicalSendObserverInit, providerFetch, safeHostLabel, sendWithConnectionPolicy } from "./responses/fetch-helpers";
72
72
  import { isLocalUpstream } from "../lib/local-upstream";
73
73
  import { linkAbortSignal } from "./responses";
74
74
  import {
@@ -449,7 +449,7 @@ export async function runNativeChatAttempt(
449
449
  let responseOwnsDistributedKey = true;
450
450
  const send = async (
451
451
  request: AdapterRequest,
452
- recovery?: "rate-limit-429" | "key-429" | "key-401" | UpstreamSendRecovery,
452
+ recovery?: "rate-limit-429" | "key-401" | "key-429" | UpstreamSendRecovery,
453
453
  singleShot = false,
454
454
  ): Promise<Response> => {
455
455
  try {
@@ -516,6 +516,7 @@ export async function runNativeChatAttempt(
516
516
  const spendReport = createPhysicalSendReporter(physicalBudget, () => ({
517
517
  poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
518
518
  }), execution.comboDispatchPermit);
519
+ const observeAutoPause = captureAccountAutoPauseAttempt(route.providerName, activeProvider, { config, oauth: undefined }, headers);
519
520
  let dispatched: Response;
520
521
  try {
521
522
  // Capture this request's spend policy at its first physical boundary, including
@@ -538,6 +539,7 @@ export async function runNativeChatAttempt(
538
539
  request.url,
539
540
  applyUpstreamRecoveryInit({
540
541
  ...init, method: request.method, headers, body: request.body,
542
+ ...physicalSendObserverInit(observeAutoPause, request.url),
541
543
  }, transportRecovery),
542
544
  // Reselection can replace the provider transport and the wire shape, so the
543
545
  // egress route is bound to the provider this send actually uses. Omitting it
@@ -552,7 +554,7 @@ export async function runNativeChatAttempt(
552
554
  responseOwnsDistributedKey = !activeProvider._apiKeyAttempt?.distributed
553
555
  || distributedKeyResponseOwner(dispatched, config, route.providerName) !== null;
554
556
  if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
555
- return dispatched;
557
+ return observeAutoPause ? observeAutoPause(dispatched) : dispatched;
556
558
  },
557
559
  }),
558
560
  );
@@ -585,6 +587,23 @@ export async function runNativeChatAttempt(
585
587
  response = await send(activeRequest);
586
588
  const retryPolicy = rateLimitRetryPolicyFor(activeProvider);
587
589
  let retries = 0;
590
+ let autoPauseMoves = 0;
591
+ for (;;) {
592
+ const priorResponse = response;
593
+ for (; wasAccountAutoPaused(response) && config.providers[route.providerName]?.apiKey && autoPauseMoves < 3 && transientSendAvailable(); autoPauseMoves++) {
594
+ const fresh = resolveCurrentProviderApiKeyTransport(config, route.providerName, { ...activeProvider, _apiKeyAttempt: undefined });
595
+ const next = fresh && (selectProactiveApiKeyTransport(config, route.providerName, fresh) ?? fresh);
596
+ if (!next) break;
597
+ try { void response.body?.cancel().catch(() => {}); } catch { /* closed */ }
598
+ activeProvider = next;
599
+ stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
600
+ activeAdapter = createOpenAIChatAdapter(activeProvider);
601
+ releaseRetainedRequest();
602
+ activeRequest = buildActiveRequest();
603
+ retainRequest(activeRequest);
604
+ response = await send(activeRequest, "key-401");
605
+ }
606
+
588
607
  while (
589
608
  response.status === 429
590
609
  // A 429 this proxy synthesized for a refused reset replay is not a provider rate
@@ -632,6 +651,10 @@ export async function runNativeChatAttempt(
632
651
  retainRequest(activeRequest);
633
652
  response = await send(activeRequest, failureStatus === 401 ? "key-401" : "key-429");
634
653
  }
654
+ if (response !== priorResponse && wasAccountAutoPaused(response)) continue;
655
+ break;
656
+ }
657
+
635
658
  } catch (error) {
636
659
  releaseRetainedRequest();
637
660
  cleanupAbort();
@@ -694,20 +717,12 @@ export async function runNativeChatAttempt(
694
717
  && (isCyberPolicyCode(upstreamCode) || isCyberPolicyMessage(upstreamMessage))
695
718
  ? upstreamMessage
696
719
  : detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`;
697
- // Zen's keyless free tier refuses the request outright rather than rate-limiting it, and
698
- // the raw `MissingSessionID` tells a user nothing about why or what to do (#4121).
699
- const clientMessage = enrichOpenCodeZenFreeTierMessage(message, {
700
- providerName: route.providerName,
701
- baseUrl: route.provider.baseUrl,
702
- adapter: route.provider.adapter,
703
- upstreamErrorType: upstreamType,
704
- });
705
720
  const classified = classifyError(
706
721
  response.status,
707
722
  upstreamType ?? (response.status === 401 ? "authentication_error"
708
723
  : response.status === 429 ? "rate_limit_error"
709
724
  : response.status >= 500 ? "server_error" : "invalid_request_error"),
710
- clientMessage,
725
+ message,
711
726
  );
712
727
  // The verdict is read once, from the response that carries it and from the code a
713
728
  // re-wrapped body kept -- never from the status, which a real rate limit shares.
@@ -1110,7 +1110,8 @@ async function handleClaudeMessagesWithBudget(
1110
1110
  const finalizeNativeLog = createFinalRequestLog(logIds, logCtx).finish;
1111
1111
  const upstream = await handleResponses(internalReq, {
1112
1112
  ...buildClaudeReplayConfig(config),
1113
- // Operator config wins; otherwise the caller's own 1h breakpoints keep 1h here.
1113
+ // Operator config wins; otherwise the caller's explicit TTLs (all 1h, or a pinned 5m) carry
1114
+ // over, and TTL-less breakpoints take the destination default.
1114
1115
  cacheRetention: config.cacheRetention ?? callerCacheRetention(anthropicBody),
1115
1116
  }, logCtx, {
1116
1117
  // Routing keeps Claude-only sidecar overrides; admission policy must follow the live owner.
@@ -17,6 +17,7 @@ import { createLinkSupervisor, type LinkSupervisor } from "../../link/supervisor
17
17
  import { linkManagementRouteAllowed } from "../../link/management-routes";
18
18
  import { saveConfigPreservingClaudeCode } from "../../config/live-reconcile";
19
19
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
20
+ import { startJevQuotaActivation } from "../../combos/jev-quota-activation";
20
21
  export { LINK_INGRESS_HOSTNAME } from "./link-listener";
21
22
  import type { ServerIngress } from "./serve-options";
22
23
 
@@ -51,6 +52,7 @@ export function createOptionalListenerSet<T>(linkDeps: LinkListenerDeps = {}): O
51
52
  const claudeIntercept: ClaudeInterceptLifecycle<T> = createClaudeInterceptLifecycle<T>();
52
53
  const linkListener: LinkListenerLifecycle<T> = createLinkListenerLifecycle<T>(linkDeps);
53
54
  let activeConfig: OcxConfig | undefined;
55
+ let stopJevQuotaActivation: (() => void) | undefined;
54
56
  const supervisor = createLinkSupervisor({
55
57
  apiKeys: () => activeConfig?.apiKeys ?? [],
56
58
  revokeApiKey: id => {
@@ -114,6 +116,8 @@ export function createOptionalListenerSet<T>(linkDeps: LinkListenerDeps = {}): O
114
116
  maxRequestBodySize: ctx.maxRequestBodySize,
115
117
  dispatch: ctx.dispatch,
116
118
  });
119
+ // Last, after every listener started: JEV advisory quota follows the live config while serving.
120
+ stopJevQuotaActivation = startJevQuotaActivation(ctx.config);
117
121
  },
118
122
  ensureStarted: () => linkListener.ensureStarted(),
119
123
  close: () => linkListener.close(),
@@ -132,6 +136,8 @@ export function createOptionalListenerSet<T>(linkDeps: LinkListenerDeps = {}): O
132
136
  try { await claudeIntercept.stop(); } catch (error) { failure ??= error; }
133
137
  unregisterSupervisorAdmission?.();
134
138
  unregisterSupervisorAdmission = undefined;
139
+ stopJevQuotaActivation?.();
140
+ stopJevQuotaActivation = undefined;
135
141
  if (failure) throw failure;
136
142
  },
137
143
  };
@@ -63,7 +63,7 @@ import {
63
63
  } from "../../vision/eligibility";
64
64
  import type { OcxClaudeCodeConfig, OcxComboConfig, OcxConfig, OcxCustomModel, OcxProviderConfig, OcxComboCooldownWaitPolicy } from "../../types";
65
65
  import { drainAndShutdown } from "../lifecycle";
66
- import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
66
+ import { notifyLiveConfigObservers, reconcileLiveStateStores } from "../../lib/state-store-registrations";
67
67
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
68
68
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
69
69
  import type { PersistedUsageAttempt } from "../../usage/log";
@@ -162,6 +162,7 @@ function parseVisionSidecarTargets(
162
162
  return { targets };
163
163
  }
164
164
 
165
+ /** Handle the `/api/combos` management routes (list, save, delete and related combo endpoints), validating and preserving the opt-in quota signal fields; null when no route matches. */
165
166
  export async function handleComboRoutes(ctx: ManagementContext): Promise<Response | null> {
166
167
  const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
167
168
 
@@ -222,6 +223,8 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
222
223
  const requestedCombo: Record<string, unknown> = body.combo;
223
224
  const effectiveCombo = {
224
225
  ...requestedCombo,
226
+ ...(!Object.hasOwn(requestedCombo, "decisionQuotaSignals") && requestedCombo.strategy === "jev" && previous?.decisionQuotaSignals !== undefined ? { decisionQuotaSignals: previous.decisionQuotaSignals } : {}),
227
+ ...(!Object.hasOwn(requestedCombo, "decisionQuotaTiers") && requestedCombo.strategy === "jev" && previous?.decisionQuotaTiers !== undefined ? { decisionQuotaTiers: previous.decisionQuotaTiers } : {}),
225
228
  ...(!Object.hasOwn(requestedCombo, "allowMeteredTargets") && previous?.allowMeteredTargets === true
226
229
  ? { allowMeteredTargets: true }
227
230
  : {}),
@@ -244,6 +247,11 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
244
247
  ...(!Object.hasOwn(requestedCombo, "imageInput") && previous?.imageInput !== undefined
245
248
  ? { imageInput: previous.imageInput }
246
249
  : {}),
250
+ // #7229: the dashboard does not edit the row description, so a round trip that omits it
251
+ // (including a rename) keeps it. An explicit empty string still clears it.
252
+ ...(!Object.hasOwn(requestedCombo, "description") && previous?.description !== undefined
253
+ ? { description: previous.description }
254
+ : {}),
247
255
  // `lastResort` rides on each target, so a GUI that re-sends the target list without the
248
256
  // flag would strip it. Carry it over per target, matched on provider+model.
249
257
  ...(Array.isArray(requestedCombo.targets) && Array.isArray(previous?.targets)
@@ -516,6 +524,8 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
516
524
  if (!(error instanceof ConfigWritePublishedError)) {
517
525
  return jsonResponse({ error: "combo could not be saved" }, 500);
518
526
  }
527
+ // Published but bookkeeping failed: live observers (JEV quota activation) still follow the saved combo.
528
+ notifyLiveConfigObservers(config);
519
529
  throw error;
520
530
  }
521
531
  reconcileLiveStateStores();
@@ -548,7 +558,12 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
548
558
  const { clearComboSelectionState, clearComboTargetCooldowns } = await import("../../combos");
549
559
  delete config.combos![id];
550
560
  if (Object.keys(config.combos!).length === 0) deleteConfigTopLevelKey(config, "combos");
551
- saveConfigPreservingClaudeCode(config);
561
+ try {
562
+ saveConfigPreservingClaudeCode(config);
563
+ } catch (error) {
564
+ if (error instanceof ConfigWritePublishedError) notifyLiveConfigObservers(config);
565
+ throw error;
566
+ }
552
567
  reconcileLiveStateStores();
553
568
  clearComboSelectionState(id);
554
569
  clearComboTargetCooldowns(id);
@@ -510,7 +510,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
510
510
  reauthReason: summary.needsReauth === true ? "refresh_failed" : undefined,
511
511
  });
512
512
  return { ...summary, ...oauthAccountHealthFields(provider, summary.id, health), quotaMode,
513
- ...(supportsPause ? { paused: full?.paused === true } : {}),
513
+ ...(supportsPause ? { paused: full?.paused === true, ...(full?.pauseDetails ? { pauseDetails: full.pauseDetails } : {}) } : {}),
514
514
  ...(isAnthropicOAuthInstance(provider) && supportsPause ? { autoSwitchThresholdOverride: full?.autoSwitchThresholdOverride ?? null,
515
515
  effectiveAutoSwitchThreshold: effectiveAnthropicAccountThresholdForInstance(provider as AnthropicInstanceId, config, full),
516
516
  autoSwitchThreshold: effectiveAnthropicAccountThresholdForInstance(provider as AnthropicInstanceId, config) } : {}),
@@ -1116,6 +1116,15 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
1116
1116
  clearProviderQuotaCache();
1117
1117
  return jsonResponse({ ...result, name, store: providerKeyStoreKind(config.providers[name]) });
1118
1118
  }
1119
+ if (url.pathname === "/api/providers/keys/pause" && req.method === "PUT") {
1120
+ const body = await readManagementJsonBodyOr(req, {});
1121
+ if (!isPlainRecord(body)) return jsonResponse({ error: "body must be an object" }, 400);
1122
+ if (typeof body.name !== "string" || typeof body.id !== "string" || typeof body.paused !== "boolean") return jsonResponse({ error: "name, id and paused required" }, 400);
1123
+ if (!isValidProviderName(body.name) || !hasOwnProvider(config.providers, body.name)) return jsonResponse({ error: "unknown provider" }, 404);
1124
+ const { setProviderApiKeyPaused } = await import("../../providers/api-key-pause");
1125
+ return setProviderApiKeyPaused(config, body.name, body.id, body.paused)
1126
+ ? jsonResponse({ ok: true }) : jsonResponse({ error: "key not found" }, 404);
1127
+ }
1119
1128
  if (url.pathname === "/api/providers/keys/active" && req.method === "PUT") {
1120
1129
  const body = await readManagementJsonBodyOr(req, {}) as { name?: string; id?: string };
1121
1130
  const name = (body.name ?? "").trim();
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Explicit single-model diagnostic, isolated from discovery, routing health and request history.
3
+ * Unsupported transports: TLS profiles, HTTP version pins, enabled pacing, custom fetch + proxy.
4
+ * Adding an exclusion requires a regression test. No retries, failover, or output preview.
5
+ */
6
+ import type { OcxConfig, OcxProviderConfig } from "../../types";
7
+ import { routedProviderConfig } from "../../providers/routed-config";
8
+ import { PROVIDER_REGISTRY } from "../../providers/registry";
9
+ import { resolveModelAlias } from "../../providers/default-aliases";
10
+ import { providerApiKeySelectionIsCurrent } from "../../providers/api-key-selection";
11
+ import { resolveWireProtocolOverride } from "../adapter-resolve";
12
+ import { openAIChatTransport, stripBracketedModelSuffix } from "../../adapters/openai-chat/wire";
13
+ import { buildResponsesTransport } from "../../adapters/openai-responses/transport";
14
+ import { openRouterProviderPayload, resolveOpenRouterRouting } from "../../providers/openrouter-routing";
15
+ import { resolveVercelGatewayRouting, vercelGatewayProviderPayload } from "../../providers/vercel-gateway-routing";
16
+ import { assertProviderDestinationAllowed, providerAllowsPrivateNetwork } from "../../lib/destination-policy";
17
+ import { providerOutboundPost, ProviderOutboundPolicyError, ProviderOutboundSendCancelledError,
18
+ type ProviderOutboundDependencies } from "../../lib/provider-outbound";
19
+ import { redactSecretString } from "../../lib/redact";
20
+ import { decodeServerSentEvents } from "../../lib/sse-decoder";
21
+ import { createTranslatorBudget } from "../../lib/translator-budget";
22
+ import { jsonResponse } from "../auth-cors";
23
+ import type { ManagementContext } from "./context";
24
+ import { managementBodyTooLargeResponse, readManagementJsonBody } from "./body";
25
+
26
+ export const COMPLETION_TEST_PROMPT = "Reply exactly OK.";
27
+ export const COMPLETION_TEST_OUTPUT_TOKEN_CAP = 32;
28
+ export const COMPLETION_TEST_TIMEOUT_MS = 20_000;
29
+ export const COMPLETION_TEST_MAX_BYTES = 64 * 1024;
30
+
31
+ type Wire = "openai-chat" | "openai-responses";
32
+ type Code = "destination_refused" | "credential_missing" | "credential_changed" | "http_status" | "redirect"
33
+ | "network" | "stream_error" | "premature_eof" | "no_content" | "timeout" | "aborted" | "too_large";
34
+ const MESSAGES: Record<Code, string> = {
35
+ destination_refused: "Provider destination refused by policy.",
36
+ credential_missing: "Provider credential is missing.",
37
+ credential_changed: "Provider request authority changed before send.",
38
+ http_status: "Upstream returned an unsuccessful HTTP status.",
39
+ redirect: "Upstream redirect refused.",
40
+ network: "Upstream network request failed.",
41
+ stream_error: "Upstream stream reported an error.",
42
+ premature_eof: "Upstream stream ended without a terminal event.",
43
+ no_content: "Upstream completed without a text delta.",
44
+ timeout: "Completion test deadline exceeded.",
45
+ aborted: "Completion test was cancelled.",
46
+ too_large: "Upstream stream exceeded the byte limit.",
47
+ };
48
+ export type ProviderCompletionTestResult = {
49
+ applicable: false;
50
+ reason: "disabled" | "forward_auth" | "oauth_auth" | "unsupported_adapter" | "unsupported_transport";
51
+ adapter?: string;
52
+ detail?: "tls_profile" | "upstream_http_version" | "request_pacing" | "custom_executor_egress";
53
+ } | {
54
+ applicable: true; ok: boolean; provider: string; requestedModel: string; wireModel: string; wire: Wire;
55
+ upstreamModel: string | null; modelComparison: "match" | "mismatch" | "unknown";
56
+ comparisonNote?: "missing_metadata" | "conflicting_metadata";
57
+ identityVerified: false; ttftMs: number | null; totalLatencyMs: number; outputTokenCap: 32;
58
+ termination: "completed" | "failed" | "timeout" | "aborted" | "too_large";
59
+ error: null | { code: Code; status?: number; message: string };
60
+ };
61
+ export interface ProviderCompletionTestInput { config: OcxConfig; name: string; model: string; signal?: AbortSignal }
62
+ export interface ProviderCompletionTestDependencies {
63
+ now?: () => number;
64
+ timeoutMs?: number;
65
+ outboundDependencies?: ProviderOutboundDependencies;
66
+ }
67
+
68
+ function transportExclusion(row: OcxProviderConfig): Extract<ProviderCompletionTestResult, { applicable: false }>["detail"] {
69
+ if (row.tlsProfile !== undefined) return "tls_profile";
70
+ if (row.upstreamHttpVersion !== undefined) return "upstream_http_version";
71
+ if (row.requestPacing?.enabled) return "request_pacing";
72
+ if (typeof (row as { fetch?: unknown }).fetch === "function" && "proxy" in row) return "custom_executor_egress";
73
+ return undefined;
74
+ }
75
+ function nativeModel(config: OcxConfig, row: OcxProviderConfig, model: string): string {
76
+ const known = new Set([...(row.models ?? []), ...Object.keys(row.modelAliases ?? {})]);
77
+ return known.has(model) ? model : resolveModelAlias(config, row, known, model) ?? model;
78
+ }
79
+ function derive(config: OcxConfig, name: string, row: OcxProviderConfig, model: string) {
80
+ const resolved = routedProviderConfig(name, row);
81
+ const native = nativeModel(config, resolved, model);
82
+ const routed = resolveWireProtocolOverride(name, native, resolved);
83
+ const wire = routed.adapter;
84
+ if (wire !== "openai-chat" && wire !== "openai-responses") throw new Error("unsupported adapter");
85
+ const transport = wire === "openai-chat" ? openAIChatTransport(routed)
86
+ : buildResponsesTransport(routed, { headers: new Headers() });
87
+ const headers = new Headers(transport.headers);
88
+ headers.set("Accept", "text/event-stream");
89
+ const wireModel = routed.modelSuffixBracketStrip ? stripBracketedModelSuffix(native) : native;
90
+ const openRouterRouting = wire === "openai-chat" ? resolveOpenRouterRouting(routed, native) : undefined;
91
+ const vercelRouting = wire === "openai-chat" ? resolveVercelGatewayRouting(routed, native) : undefined;
92
+ const providerRouting = vercelRouting ? vercelGatewayProviderPayload(vercelRouting)
93
+ : openRouterRouting ? openRouterProviderPayload(openRouterRouting) : undefined;
94
+ // Normal inference admits registry-local defaults as well as the operator's explicit opt-in.
95
+ // Materialize that effective policy for the outbound wrapper's narrower local HTTP safeguard.
96
+ const allowLocalCleartextPost = providerAllowsPrivateNetwork(name, routed);
97
+ // Capture primitive values now: rows and their nested header objects may mutate in place.
98
+ const selection = routed._apiKeyAttempt;
99
+ const fingerprint = JSON.stringify({ wire, providerRouting, allowLocalCleartextPost, url: transport.url, wireModel, headers: [...headers].sort(([a], [b]) => a.localeCompare(b)),
100
+ authMode: routed.authMode, apiKey: routed.apiKey,
101
+ selection: selection && { entryId: selection.entryId, reference: selection.reference, revision: selection.revision },
102
+ allowPrivateNetwork: routed.allowPrivateNetwork, proxy: routed.proxy, noProxy: routed.noProxy, disabled: row.disabled });
103
+ return { routed, wire: wire as Wire, wireModel, providerRouting, allowLocalCleartextPost, headers, url: transport.url, fingerprint };
104
+ }
105
+ function maskedModel(raw: string, secrets: Array<string | undefined>): string {
106
+ if (secrets.some(secret => typeof secret === "string" && secret.length > 0 && raw.includes(secret))) return "[redacted]";
107
+ return redactSecretString(raw.replace(/[\x00-\x1f\x7f-\x9f]/g, "").slice(0, 256));
108
+ }
109
+ function abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
110
+ return new Promise((resolve, reject) => {
111
+ const abort = () => reject(signal.reason);
112
+ signal.addEventListener("abort", abort, { once: true });
113
+ if (signal.aborted) abort();
114
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
115
+ });
116
+ }
117
+ class DiagnosticFailure extends Error { constructor(readonly code: Code) { super(MESSAGES[code]); } }
118
+
119
+ /** Direct diagnostic; callers supply an existing provider name and an already validated model. */
120
+ export async function runProviderCompletionTest(
121
+ { config, name, model, signal: requestSignal }: ProviderCompletionTestInput,
122
+ deps: ProviderCompletionTestDependencies = {},
123
+ ): Promise<ProviderCompletionTestResult> {
124
+ const row = config.providers[name];
125
+ if (!Object.hasOwn(config.providers, name) || !row) throw new Error("unknown provider");
126
+ if (row.disabled) return { applicable: false, reason: "disabled" };
127
+ const authKind = PROVIDER_REGISTRY.find(entry => entry.id === name)?.authKind;
128
+ if (row.authMode === "forward" || authKind === "forward") return { applicable: false, reason: "forward_auth" };
129
+ if (row.authMode === "oauth" || authKind === "oauth") return { applicable: false, reason: "oauth_auth" };
130
+ const detail = transportExclusion(row);
131
+ if (detail) return { applicable: false, reason: "unsupported_transport", detail };
132
+ // Metadata resolution must never dereference keychain/env/pool credentials.
133
+ const { apiKey: _key, apiKeyPool: _pool, _apiKeyAttempt: _attempt, ...metadata } = row;
134
+ let snapshot: OcxProviderConfig = metadata;
135
+ let destinationRefused = false;
136
+ try { snapshot = routedProviderConfig(name, metadata); } catch { destinationRefused = true; }
137
+ const native = nativeModel(config, snapshot, model);
138
+ const wire = resolveWireProtocolOverride(name, native, snapshot).adapter;
139
+ if (wire !== "openai-chat" && wire !== "openai-responses") return { applicable: false, reason: "unsupported_adapter", adapter: wire };
140
+ const now = deps.now ?? (() => performance.now());
141
+ const result: Extract<ProviderCompletionTestResult, { applicable: true }> = {
142
+ applicable: true, ok: false, provider: name, requestedModel: model,
143
+ wireModel: snapshot.modelSuffixBracketStrip ? stripBracketedModelSuffix(native) : native, wire,
144
+ upstreamModel: null, modelComparison: "unknown", comparisonNote: "missing_metadata", identityVerified: false,
145
+ ttftMs: null, totalLatencyMs: 0, outputTokenCap: COMPLETION_TEST_OUTPUT_TOKEN_CAP, termination: "failed", error: null,
146
+ };
147
+ const fail = (code: Code, status?: number) => {
148
+ result.error = { code, message: MESSAGES[code], ...(status !== undefined ? { status } : {}) };
149
+ result.termination = code === "timeout" || code === "aborted" || code === "too_large" ? code : "failed";
150
+ return result;
151
+ };
152
+ if (destinationRefused) return fail("destination_refused");
153
+ try { assertProviderDestinationAllowed(name, snapshot); } catch { return fail("destination_refused"); }
154
+ let resolved: OcxProviderConfig;
155
+ try { resolved = routedProviderConfig(name, row); } catch { return fail("credential_missing"); }
156
+ if (resolved.authMode === "key" && !resolved.keyOptional && !resolved.apiKey?.trim()) return fail("credential_missing");
157
+ let authority: ReturnType<typeof derive>;
158
+ try { authority = derive(config, name, row, model); } catch { return fail("network"); }
159
+ const secrets = [authority.routed.apiKey, ...Object.values(authority.routed.headers ?? {}),
160
+ ...[...authority.headers].filter(([name]) => name === "authorization" || name === "x-api-key").map(([, value]) => value)];
161
+ result.wire = authority.wire;
162
+ result.wireModel = authority.wireModel;
163
+ const controller = new AbortController();
164
+ const deadline = AbortSignal.timeout(deps.timeoutMs ?? COMPLETION_TEST_TIMEOUT_MS);
165
+ const signal = AbortSignal.any([...(requestSignal ? [requestSignal] : []), deadline, controller.signal]);
166
+ const started = now();
167
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
168
+ let response: Response | undefined;
169
+ const translatorBudget = createTranslatorBudget();
170
+ let observed: string | undefined;
171
+ let conflicting = false, terminal = false, content = false, finishReason = false;
172
+ const observe = (value: unknown) => {
173
+ if (typeof value !== "string") return;
174
+ if (observed === undefined) observed = value;
175
+ else if (value !== observed) conflicting = true;
176
+ };
177
+ try {
178
+ signal.throwIfAborted();
179
+ const body = JSON.stringify(authority.wire === "openai-chat"
180
+ ? { model: authority.wireModel, messages: [{ role: "user", content: COMPLETION_TEST_PROMPT }], stream: true, max_tokens: 32,
181
+ ...(authority.providerRouting ? { provider: authority.providerRouting } : {}) }
182
+ : { model: authority.wireModel, input: COMPLETION_TEST_PROMPT, stream: true, max_output_tokens: 32, store: false });
183
+ const pending = providerOutboundPost(name, { ...authority.routed, allowPrivateNetwork: authority.allowLocalCleartextPost }, authority.url, { headers: authority.headers, body, signal }, {
184
+ ...deps.outboundDependencies,
185
+ allowLocalCleartextPost: authority.allowLocalCleartextPost,
186
+ beforeSend: () => {
187
+ try {
188
+ const current = config.providers[name];
189
+ return current === row && !current.disabled && !transportExclusion(current)
190
+ && derive(config, name, current, model).fingerprint === authority.fingerprint
191
+ && (!authority.routed.apiKey || providerApiKeySelectionIsCurrent(config, name, authority.routed));
192
+ } catch { return false; }
193
+ },
194
+ });
195
+ // Even an executor ignoring AbortSignal cannot retain a late response body.
196
+ void pending.then(late => { if (signal.aborted) void late.body?.cancel().catch(() => {}); }, () => {});
197
+ response = await abortable(pending, signal);
198
+ if (response.status >= 300 && response.status < 400) return fail("redirect");
199
+ if (!response.ok) return fail("http_status", response.status);
200
+ if (!response.body) return fail("premature_eof");
201
+ reader = response.body.getReader();
202
+ let bytes = 0;
203
+ const counted = new ReadableStream<Uint8Array>({
204
+ async pull(target) {
205
+ try {
206
+ const next = await abortable(reader!.read(), signal);
207
+ if (next.done) { target.close(); return; }
208
+ bytes += next.value.byteLength;
209
+ if (bytes > COMPLETION_TEST_MAX_BYTES) throw new DiagnosticFailure("too_large");
210
+ target.enqueue(next.value);
211
+ } catch (error) { target.error(error); }
212
+ },
213
+ cancel() { void reader!.cancel().catch(() => {}); },
214
+ }, { highWaterMark: 0 });
215
+ for await (const event of decodeServerSentEvents(counted, { signal, translatorBudget })) {
216
+ if (authority.wire === "openai-chat" && event.data.trim() === "[DONE]") { terminal = true; break; }
217
+ let chunk: Record<string, any>;
218
+ try { chunk = JSON.parse(event.data); } catch { throw new DiagnosticFailure("stream_error"); }
219
+ if (!chunk || typeof chunk !== "object" || chunk.error || event.event === "error" || chunk.type === "error" || chunk.type === "response.failed") {
220
+ throw new DiagnosticFailure("stream_error");
221
+ }
222
+ let text = false;
223
+ if (authority.wire === "openai-chat") {
224
+ observe(chunk.model);
225
+ if (Array.isArray(chunk.choices)) for (const choice of chunk.choices) {
226
+ if (typeof choice?.delta?.content === "string" && choice.delta.content.length > 0) text = true;
227
+ if (choice?.finish_reason != null) finishReason = true;
228
+ }
229
+ } else {
230
+ const type = chunk.type ?? event.event;
231
+ if (["response.created", "response.completed", "response.incomplete"].includes(type)) observe(chunk.response?.model);
232
+ text = type === "response.output_text.delta" && typeof chunk.delta === "string" && chunk.delta.length > 0;
233
+ terminal = type === "response.completed" || type === "response.incomplete";
234
+ if (type === "response.failed") throw new DiagnosticFailure("stream_error");
235
+ }
236
+ if (text) { content = true; result.ttftMs ??= Math.max(0, Math.round(now() - started)); }
237
+ if (terminal) break;
238
+ }
239
+ signal.throwIfAborted();
240
+ if (!terminal && !(authority.wire === "openai-chat" && finishReason)) return fail("premature_eof");
241
+ if (!content) return fail("no_content");
242
+ result.ok = true;
243
+ result.termination = "completed";
244
+ return result;
245
+ } catch (error) {
246
+ return fail(requestSignal?.aborted ? "aborted" : deadline.aborted ? "timeout"
247
+ : error instanceof DiagnosticFailure ? error.code
248
+ : error instanceof ProviderOutboundSendCancelledError ? "credential_changed"
249
+ : error instanceof ProviderOutboundPolicyError ? "destination_refused" : "network");
250
+ } finally {
251
+ translatorBudget.dispose();
252
+ result.totalLatencyMs = Math.max(0, Math.round(now() - started));
253
+ if (observed !== undefined && !conflicting) {
254
+ result.modelComparison = observed === authority.wireModel ? "match" : "mismatch";
255
+ delete result.comparisonNote;
256
+ result.upstreamModel = maskedModel(observed, secrets);
257
+ } else if (conflicting) result.comparisonNote = "conflicting_metadata";
258
+ controller.abort();
259
+ // Cancellation is initiated on every exit; do not wait on a misbehaving executor.
260
+ if (reader) { void reader.cancel().catch(() => {}); try { reader.releaseLock(); } catch { /* read pending */ } }
261
+ else void response?.body?.cancel().catch(() => {});
262
+ }
263
+ }
264
+
265
+ const inFlight = new Set<string>();
266
+ export async function handleProviderCompletionTestRoute(
267
+ { req, url, config }: Pick<ManagementContext, "req" | "url" | "config">,
268
+ deps: ProviderCompletionTestDependencies = {},
269
+ ): Promise<Response> {
270
+ let body: unknown;
271
+ try { body = await readManagementJsonBody(req); } catch (error) {
272
+ return managementBodyTooLargeResponse(error, req, config) ?? jsonResponse({ error: "invalid completion test request" }, 400);
273
+ }
274
+ if (!body || typeof body !== "object" || Array.isArray(body)
275
+ || Object.keys(body).some(key => key !== "model" && key !== "confirm")
276
+ || (body as { confirm?: unknown }).confirm !== true || typeof (body as { model?: unknown }).model !== "string") {
277
+ return jsonResponse({ error: "invalid completion test request" }, 400);
278
+ }
279
+ const model = (body as { model: string }).model.trim();
280
+ if (!model || model.length > 256 || /[\x00-\x1f\x7f-\x9f]/.test((body as { model: string }).model)) {
281
+ return jsonResponse({ error: "invalid completion test request" }, 400);
282
+ }
283
+ const name = url.searchParams.get("name") ?? "";
284
+ if (!Object.hasOwn(config.providers, name) || !config.providers[name]) return jsonResponse({ error: "unknown provider" }, 404);
285
+ if (inFlight.has(name)) return jsonResponse({ error: "completion test already running for this provider" }, 409);
286
+ inFlight.add(name);
287
+ try { return jsonResponse(await runProviderCompletionTest({ config, name, model, signal: req.signal }, deps)); }
288
+ finally { inFlight.delete(name); }
289
+ }
@@ -764,7 +764,7 @@ function applyProviderPatchFields(
764
764
  if (Object.hasOwn(rawBody, "headers")) {
765
765
  const headersValue = rawBody.headers;
766
766
  if (headersValue === null || (isPlainRecord(headersValue) && Object.keys(headersValue).length === 0)) {
767
- // Registry-owned static metadata (e.g. opencode-free's x-opencode-client marker)
767
+ // Registry-owned static metadata (e.g. meta-muse's x-api-version marker)
768
768
  // is not user-managed: restoring it keeps the upstream transport intact after a
769
769
  // clear instead of deleting the whole block.
770
770
  const entry = getProviderRegistryEntry(name);
@@ -1357,6 +1357,12 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1357
1357
  if (submittedModelContextTiers && liveTiers) candidate.modelContextTiers = Object.assign(Object.create(null), liveTiers, candidate.modelContextTiers ?? {});
1358
1358
  else if (!submittedModelContextTiers && liveTiers) candidate.modelContextTiers = { ...liveTiers };
1359
1359
  else if (!submittedModelContextTiers) delete candidate.modelContextTiers;
1360
+ // Read omitted policy at publication time, after any concurrent operator edit.
1361
+ if (!Object.hasOwn(rawProvider, "accountAutoPause")) {
1362
+ const livePolicy = config.providers[name]?.accountAutoPause;
1363
+ if (livePolicy === undefined) delete candidate.accountAutoPause;
1364
+ else candidate.accountAutoPause = structuredClone(livePolicy);
1365
+ }
1360
1366
  // Full forms omit the opt-in; preserve the latest value, including a concurrent clear.
1361
1367
  if (!Object.hasOwn(rawProvider, "inferContextWindow")) {
1362
1368
  const liveInference = config.providers[name]?.inferContextWindow;
@@ -1602,6 +1608,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1602
1608
  });
1603
1609
  }
1604
1610
 
1611
+ if (url.pathname === "/api/providers/test-completion" && req.method === "POST") return (await import("./provider-completion-test")).handleProviderCompletionTestRoute(ctx);
1612
+
1605
1613
  // Lightweight connectivity probe: perform the provider's live /models fetch DIRECTLY and
1606
1614
  // report only real upstream evidence. The catalog aggregate (fetchAllModels) deliberately
1607
1615
  // hides fetch failures behind stale/static fallbacks, so a catalog-presence check would