@bitkyc08/opencodex 2.82.0 → 2.83.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 (85) hide show
  1. package/gui/dist/assets/{App-Ds_qL7o8.js → App-Ba8uG696.js} +4 -4
  2. package/gui/dist/assets/{Tray-BqHF5Trd.js → Tray-IImbNPRy.js} +1 -1
  3. package/gui/dist/assets/{index-D3FBCNiP.js → index-BHRZdJuv.js} +12 -12
  4. package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-DiJO6sYY.js} +1 -1
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/cursor/catalog.ts +2 -2
  8. package/src/adapters/cursor/effort-map.ts +1 -1
  9. package/src/adapters/devin/live-models.ts +2 -2
  10. package/src/adapters/google-truncation.ts +22 -0
  11. package/src/adapters/google.ts +7 -3
  12. package/src/adapters/ollama-native.ts +18 -14
  13. package/src/adapters/openai-responses/passthrough.ts +2 -0
  14. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  15. package/src/adapters/openai-responses/reasoning.ts +11 -1
  16. package/src/bridge/errors.ts +7 -1
  17. package/src/claude/context-windows.ts +3 -3
  18. package/src/claude/desktop-picker.ts +1 -1
  19. package/src/cli/account-auth.ts +20 -12
  20. package/src/cli/resolve.ts +1 -1
  21. package/src/clients/config-export/model-metadata.ts +16 -0
  22. package/src/clients/config-export/zcode-store.ts +46 -11
  23. package/src/clients/config-export/zcode.ts +3 -5
  24. package/src/clients/config-export.ts +1 -1
  25. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  26. package/src/codex/auth-context.ts +8 -3
  27. package/src/codex/catalog/native-models.ts +1 -1
  28. package/src/codex/catalog/provider-models.ts +25 -0
  29. package/src/codex/desktop-app/darwin.ts +1 -1
  30. package/src/codex/desktop-app/handoff.ts +1 -1
  31. package/src/codex/desktop-app/linux.ts +1 -1
  32. package/src/codex/desktop-app/lock.ts +1 -1
  33. package/src/codex/desktop-app/types.ts +1 -1
  34. package/src/codex/desktop-app/windows.ts +1 -1
  35. package/src/codex/desktop-app-restart.ts +1 -1
  36. package/src/codex/main-account.ts +6 -7
  37. package/src/generated/compatibility-version.json +91 -75
  38. package/src/grok/inject.ts +245 -221
  39. package/src/grok/reset-coupon-ledger.ts +223 -57
  40. package/src/grok/reset-coupons.ts +13 -6
  41. package/src/integrations/cursor-detect.ts +1 -1
  42. package/src/lib/errors.ts +25 -0
  43. package/src/lib/redact.ts +2 -2
  44. package/src/oauth/index.ts +1 -1
  45. package/src/oauth/meta-muse-device.ts +1 -1
  46. package/src/oauth/types.ts +1 -1
  47. package/src/providers/anthropic-fast.ts +1 -1
  48. package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
  49. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  50. package/src/providers/devin-provider-merge-migration.ts +1 -1
  51. package/src/providers/github-copilot-auto.ts +169 -0
  52. package/src/providers/model-presets.ts +1 -1
  53. package/src/providers/muse-subscription-usage.ts +1 -1
  54. package/src/providers/registry/entries-core.ts +6 -6
  55. package/src/providers/registry/entries-extended.ts +4 -0
  56. package/src/providers/registry/model-seeds.ts +3 -3
  57. package/src/providers/xai-fast-model.ts +1 -1
  58. package/src/server/inbound-body-admission.ts +24 -7
  59. package/src/server/index/link-listener.ts +30 -17
  60. package/src/server/index/serve-options.ts +8 -2
  61. package/src/server/index.ts +1 -1
  62. package/src/server/lifecycle.ts +31 -4
  63. package/src/server/management/grok-coupon-routes.ts +277 -52
  64. package/src/server/management/link-routes.ts +42 -20
  65. package/src/server/management/route-registry.ts +2 -2
  66. package/src/server/relay-eager.ts +2 -0
  67. package/src/server/relay.ts +18 -7
  68. package/src/server/request-log-failure-attribution.ts +3 -0
  69. package/src/server/request-log-terminal-status.ts +186 -0
  70. package/src/server/request-log.ts +15 -118
  71. package/src/server/responses/adapter-continuation.ts +9 -1
  72. package/src/server/responses/adapter-dispatch.ts +65 -6
  73. package/src/server/responses/compact.ts +3 -2
  74. package/src/server/responses/core-opaque-recovery.ts +5 -5
  75. package/src/server/responses/core.ts +6 -6
  76. package/src/server/responses/empty-completion-guard.ts +18 -2
  77. package/src/server/responses/jev-model-invoke.ts +1 -1
  78. package/src/server/responses/passthrough-dispatch.ts +115 -60
  79. package/src/server/responses/passthrough-execution.ts +3 -3
  80. package/src/server/responses/request-transport.ts +125 -5
  81. package/src/server/responses/run-turn-execution.ts +13 -5
  82. package/src/server/responses/sidecar-execution.ts +5 -0
  83. package/src/server/responses/spend-storage-error.ts +33 -0
  84. package/src/types/request.ts +5 -0
  85. package/src/usage/expected-prices.ts +1 -1
@@ -0,0 +1,186 @@
1
+ import {
2
+ CYBER_POLICY_ERROR_CODE,
3
+ httpStatusFromTerminalError,
4
+ isCyberPolicyCode,
5
+ isCyberPolicyMessage,
6
+ isRateLimitOrQuotaFailureMessage,
7
+ recognizedUpstreamError,
8
+ } from "../lib/errors";
9
+ import { redactSecretString } from "../lib/redact";
10
+
11
+ /** First bounded upstream diagnostics shared by request contexts and final rows. */
12
+ export interface UpstreamErrorDiagnostics {
13
+ upstreamErrorCode?: string;
14
+ upstreamErrorType?: string;
15
+ upstreamRequestId?: string;
16
+ }
17
+
18
+ interface TerminalStatusContext extends UpstreamErrorDiagnostics {
19
+ terminalHttpStatus?: number;
20
+ terminalErrorCode?: typeof CYBER_POLICY_ERROR_CODE;
21
+ }
22
+
23
+ export interface UpstreamErrorPayload {
24
+ type?: unknown;
25
+ code?: unknown;
26
+ message?: unknown;
27
+ error?: { type?: unknown; code?: unknown; message?: unknown };
28
+ last_error?: { type?: unknown; code?: unknown; message?: unknown };
29
+ response?: {
30
+ error?: { type?: unknown; code?: unknown; message?: unknown };
31
+ incomplete_details?: { code?: unknown; message?: unknown; reason?: unknown };
32
+ };
33
+ }
34
+
35
+ // Bare error evidence is provisional: only a genuine terminal owns account-health status.
36
+ const provisionalStatuses = new WeakMap<TerminalStatusContext, number>();
37
+ const genuineTerminals = new WeakSet<TerminalStatusContext>();
38
+
39
+ const UPSTREAM_DIAGNOSTIC_TOKEN = /^[A-Za-z0-9_.:-]{1,128}$/;
40
+
41
+ /**
42
+ * Upstream diagnostics are provider-controlled. The token alphabet alone also admits
43
+ * credential formats (an echoed `sk-...` key fits it), so a value the shared redactor
44
+ * would change is dropped rather than recorded, warned, or projected into log rows.
45
+ */
46
+ function isSafeDiagnosticToken(value: unknown): value is string {
47
+ return typeof value === "string" && UPSTREAM_DIAGNOSTIC_TOKEN.test(value) && redactSecretString(value) === value;
48
+ }
49
+
50
+ /**
51
+ * Error classes published by the supported provider APIs (OpenAI/Responses, Anthropic, Gemini).
52
+ * The type is diagnostic only, so the vocabulary is closed: a gateway can echo anything in
53
+ * `error.type`, including a configured credential whose format no redactor recognizes, and a
54
+ * value outside this set is simply not recorded, which is what 2.82.0 did for every type.
55
+ */
56
+ const KNOWN_UPSTREAM_ERROR_TYPES: ReadonlySet<string> = new Set([
57
+ "api_error", "authentication_error", "billing_error", "insufficient_quota", "invalid_request_error",
58
+ "not_found_error", "overloaded_error", "permission_error", "rate_limit_error", "request_too_large",
59
+ "requests", "server_error", "timeout_error", "tokens",
60
+ "INVALID_ARGUMENT", "FAILED_PRECONDITION", "PERMISSION_DENIED", "UNAUTHENTICATED", "NOT_FOUND",
61
+ "RESOURCE_EXHAUSTED", "INTERNAL", "UNAVAILABLE", "DEADLINE_EXCEEDED",
62
+ ]);
63
+
64
+ /** Bare top-level error codes worth keeping: the classes recognizedUpstreamError maps, plus server_error. */
65
+ const KNOWN_BARE_ERROR_CODES: ReadonlySet<string> = new Set([
66
+ "rate_limit_exceeded", "rate_limit_error", "server_is_overloaded", "overloaded_error", "server_error",
67
+ ]);
68
+
69
+ function noteBoundedDiagnostic(
70
+ logCtx: TerminalStatusContext,
71
+ field: "upstreamErrorCode" | "upstreamErrorType" | "upstreamRequestId",
72
+ value: unknown,
73
+ ): void {
74
+ if (logCtx[field] !== undefined) return;
75
+ if (!isSafeDiagnosticToken(value)) return;
76
+ if (field === "upstreamErrorType" && !KNOWN_UPSTREAM_ERROR_TYPES.has(value)) return;
77
+ logCtx[field] = value;
78
+ const status = logCtx.terminalHttpStatus;
79
+ if (status === undefined || status >= 500) console.warn(`[opencodex] upstream failure${status ? ` status=${status}` : ""}${logCtx.upstreamErrorCode ? ` code=${logCtx.upstreamErrorCode}` : ""}${logCtx.upstreamRequestId ? ` request_id=${logCtx.upstreamRequestId}` : ""}`);
80
+ }
81
+
82
+ export function noteUpstreamRequestId(logCtx: TerminalStatusContext, headers: Headers): void {
83
+ noteBoundedDiagnostic(logCtx, "upstreamRequestId", headers.get("openai-request-id") ?? headers.get("x-request-id"));
84
+ }
85
+
86
+ export function captureUpstreamTerminalDiagnostics(logCtx: TerminalStatusContext, json: UpstreamErrorPayload): void {
87
+ captureTerminalHttpStatus(logCtx, json);
88
+ noteBoundedDiagnostic(logCtx, "upstreamErrorCode",
89
+ json.error?.code ?? json.last_error?.code ?? json.response?.error?.code
90
+ // A bare top-level code (new in 2.83.0) has no envelope contract, so only a known class is kept.
91
+ ?? (typeof json.code === "string" && KNOWN_BARE_ERROR_CODES.has(json.code) ? json.code : undefined));
92
+ noteBoundedDiagnostic(logCtx, "upstreamErrorType",
93
+ json.error?.type ?? json.last_error?.type ?? json.response?.error?.type);
94
+ }
95
+
96
+ export function upstreamDiagnosticLogFields(logCtx: UpstreamErrorDiagnostics): UpstreamErrorDiagnostics {
97
+ return {
98
+ ...(logCtx.upstreamErrorCode ? { upstreamErrorCode: logCtx.upstreamErrorCode } : {}),
99
+ ...(logCtx.upstreamErrorType ? { upstreamErrorType: logCtx.upstreamErrorType } : {}),
100
+ ...(logCtx.upstreamRequestId ? { upstreamRequestId: logCtx.upstreamRequestId } : {}),
101
+ };
102
+ }
103
+
104
+ function captureTerminalHttpStatus(
105
+ logCtx: TerminalStatusContext,
106
+ json: UpstreamErrorPayload,
107
+ ): void {
108
+ const type = json.type;
109
+ const terminal = type === "response.failed" || type === "response.completed" || type === "response.incomplete";
110
+ if (!terminal && type !== "error") return;
111
+ if (terminal && provisionalStatuses.has(logCtx)) {
112
+ const provisional = provisionalStatuses.get(logCtx);
113
+ provisionalStatuses.delete(logCtx);
114
+ if (logCtx.terminalHttpStatus === provisional) {
115
+ delete logCtx.terminalHttpStatus;
116
+ delete logCtx.terminalErrorCode;
117
+ }
118
+ }
119
+ if (logCtx.terminalHttpStatus !== undefined || (type === "error" && genuineTerminals.has(logCtx))) return;
120
+ if (terminal) genuineTerminals.add(logCtx);
121
+ const noteStatus = (status: number): void => {
122
+ logCtx.terminalHttpStatus = status;
123
+ if (type === "error") provisionalStatuses.set(logCtx, status);
124
+ };
125
+ // A completed terminal only retires provisional bare-error evidence; it records no status.
126
+ if (type === "response.completed") return;
127
+ const responseError = json.response?.error;
128
+ const responseDetails = json.response?.incomplete_details;
129
+ const candidates: Array<{ type?: unknown; code?: unknown; message?: unknown } | undefined> = [
130
+ json.error, json.last_error, responseError, responseDetails, json,
131
+ ];
132
+ const policy = candidates.some(candidate => (
133
+ candidate?.code === null || typeof candidate?.code === "string"
134
+ ) && isCyberPolicyCode(candidate.code as string | null | undefined))
135
+ || candidates.some(candidate => (
136
+ typeof candidate?.message === "string"
137
+ && candidate.message.trim().length > 0
138
+ && isCyberPolicyMessage(candidate.message)
139
+ ));
140
+ if (policy) {
141
+ logCtx.terminalErrorCode = CYBER_POLICY_ERROR_CODE;
142
+ noteStatus(400);
143
+ return;
144
+ }
145
+ // A quota terminal can carry only a structured reason, without an error message.
146
+ // Keep this separate from normal output limits and from the policy precedence above.
147
+ const quotaTag = (value: unknown): boolean => value === "usage_limit_reached"
148
+ || value === "rate_limit_exceeded" || value === "insufficient_quota";
149
+ const structuredRefusal = candidates.some(candidate => [400, 401, 403, 499].includes(
150
+ httpStatusFromTerminalError({
151
+ type: typeof candidate?.type === "string" ? candidate.type : undefined,
152
+ code: typeof candidate?.code === "string" ? candidate.code : undefined,
153
+ }),
154
+ ));
155
+ const ordinaryIncompleteReason = typeof responseDetails?.reason === "string"
156
+ && ["max_output_tokens", "content_filter", "steered", "upstream_stall_timeout", "adapter_eof"].includes(responseDetails.reason);
157
+ if (type === "response.incomplete" && !structuredRefusal && (quotaTag(responseDetails?.reason) || candidates.some(candidate =>
158
+ quotaTag(candidate?.code)
159
+ || quotaTag(candidate?.type) || candidate?.type === "rate_limit_error"
160
+ || (!ordinaryIncompleteReason && typeof candidate?.message === "string" && isRateLimitOrQuotaFailureMessage(candidate.message))
161
+ ))) {
162
+ // The shared quota classifier also accepts a numeric HTTP status as its message.
163
+ // Preserve explicit payment-required evidence rather than relabeling it as 429.
164
+ noteStatus(candidates.some(candidate => typeof candidate?.message === "string"
165
+ && Number(candidate.message.trim()) === 402) ? 402 : 429);
166
+ return;
167
+ }
168
+ if (type === "error") {
169
+ const structured = recognizedUpstreamError({
170
+ code: candidates.find(candidate => typeof candidate?.code === "string")?.code,
171
+ type: candidates.slice(0, -1).find(candidate => typeof candidate?.type === "string")?.type,
172
+ });
173
+ // Unrecognized bare errors leave the status to explicit event fields (combo preflight).
174
+ if (structured) noteStatus(structured.httpStatus);
175
+ return;
176
+ }
177
+ if (type !== "response.failed" || !responseError || typeof responseError !== "object") return;
178
+ const responseCode = responseError.code === null || typeof responseError.code === "string"
179
+ ? responseError.code
180
+ : undefined;
181
+ noteStatus(httpStatusFromTerminalError({
182
+ type: typeof responseError.type === "string" ? responseError.type : undefined,
183
+ code: responseCode,
184
+ message: typeof responseError.message === "string" ? responseError.message : undefined,
185
+ }));
186
+ }
@@ -4,6 +4,8 @@ import { stampApiKeyAccountLabel, usesApiKeyAccount } from "../providers/label";
4
4
  import { KEY_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
5
5
  import { attemptAccountChanged, sealRequestAttemptIdentity } from "./request-log-account-rotation";
6
6
  export { sealRequestAttemptIdentity };
7
+ import { captureUpstreamTerminalDiagnostics, upstreamDiagnosticLogFields, type UpstreamErrorDiagnostics, type UpstreamErrorPayload } from "./request-log-terminal-status";
8
+ export { noteUpstreamRequestId } from "./request-log-terminal-status";
7
9
  import { readBoundedResponseBody } from "../lib/bounded-body";
8
10
  import type { ResponsesTerminalStatus } from "../bridge";
9
11
  import {
@@ -12,8 +14,6 @@ import {
12
14
  httpStatusFromTerminalError as httpStatusFromClassifiedTerminalError,
13
15
  isClientClosedMessage,
14
16
  isCyberPolicyCode,
15
- isCyberPolicyMessage,
16
- isRateLimitOrQuotaFailureMessage,
17
17
  isUpstreamResetReplayRefusedMessage,
18
18
  upstreamErrorMessageFromPayload,
19
19
  } from "../lib/errors";
@@ -104,7 +104,7 @@ function cacheDiagnosticHooks(): CacheDiagnosticHooks | undefined {
104
104
  return (globalThis as Record<symbol, CacheDiagnosticHooks | undefined>)[CACHE_DIAGNOSTIC_HOOK];
105
105
  }
106
106
 
107
- export interface RequestLogContext {
107
+ export interface RequestLogContext extends UpstreamErrorDiagnostics {
108
108
  model: string;
109
109
  provider: string;
110
110
  /** Optional process-lifetime aggregate sink, injected by the server composition owner. */
@@ -232,8 +232,6 @@ export interface RequestLogContext {
232
232
  * message) extracted from a `response.failed` SSE payload or non-streaming error body, so the
233
233
  * request log / GUI shows the actual upstream failure rather than only the HTTP-mapped code. */
234
234
  upstreamError?: string;
235
- upstreamErrorCode?: string;
236
- upstreamRequestId?: string;
237
235
  /** HTTP status derived from a terminal `response.failed` SSE payload (429/401/503/etc.). */
238
236
  terminalHttpStatus?: number;
239
237
  /** Recognized structured terminal code whose exact identity must survive status mapping. */
@@ -256,6 +254,8 @@ export interface RequestLogContext {
256
254
  conversationStateScrub?: "account-change";
257
255
  transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
258
256
  terminalSource?: "upstream" | "synthetic";
257
+ /** Closed failure evidence supplied by the transport, never inferred from provider text. */
258
+ causeHint?: RequestFailureCause;
259
259
  /** Bounded route-decision trace (RI-01); never contains secrets. */
260
260
  routeDecision?: RouteDecisionTraceV1;
261
261
  /** Full eligible provider/model membership from a policy route; never logged. */
@@ -284,7 +284,7 @@ export function rebindCacheDiagnosticBody(
284
284
  cacheDiagnosticHooks()?.rebind(body, draft);
285
285
  }
286
286
 
287
- export interface RequestLogEntry {
287
+ export interface RequestLogEntry extends UpstreamErrorDiagnostics {
288
288
  requestId: string;
289
289
  /** The logical request this row belongs to (#4546); absent on rows written without a budget. */
290
290
  logicalRequestId?: string;
@@ -344,8 +344,6 @@ export interface RequestLogEntry {
344
344
  closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
345
345
  /** Secret-redacted upstream error reason, surfaced in /api/logs and the GUI detail modal. */
346
346
  upstreamError?: string;
347
- upstreamErrorCode?: string;
348
- upstreamRequestId?: string;
349
347
  usageStatus: UsageStatus;
350
348
  usage?: OcxUsage;
351
349
  totalTokens?: number;
@@ -844,6 +842,7 @@ export function requestLogErrorCode(
844
842
  status: number,
845
843
  upstreamError?: string,
846
844
  terminalErrorCode?: string,
845
+ replayRefusal = false,
847
846
  ): string | undefined {
848
847
  if (status >= 200 && status < 400) return undefined;
849
848
  // A structured terminal code is authoritative even when the provider message is localized,
@@ -872,10 +871,8 @@ export function requestLogErrorCode(
872
871
  return "permission_denied";
873
872
  }
874
873
  if (status === 429) {
875
- // A refused ambiguous reset answers 429 by design (it must not invite a client
876
- // retry that could duplicate inference); classify it by its message so the log
877
- // distinguishes a proxy refusal from provider throttling.
878
- if (upstreamError?.trim() && isUpstreamResetReplayRefusedMessage(upstreamError)) {
874
+ // Prefer transport provenance; retain the legacy message fallback for older callers.
875
+ if (replayRefusal || (upstreamError?.trim() && isUpstreamResetReplayRefusedMessage(upstreamError))) {
879
876
  return UPSTREAM_RESET_REPLAY_REFUSED_CODE;
880
877
  }
881
878
  return "rate_limit_exceeded";
@@ -1099,24 +1096,6 @@ export function inspectResponseLogSsePayloadParsed(
1099
1096
  * a non-streaming JSON error body. We keep the FIRST non-empty reason (the original failure) and
1100
1097
  * run it through redactSecretString so secrets never reach /api/logs. Pure; safe on any text.
1101
1098
  */
1102
- const UPSTREAM_DIAGNOSTIC_TOKEN = /^[A-Za-z0-9_.:-]{1,128}$/;
1103
-
1104
- function noteBoundedDiagnostic(
1105
- logCtx: RequestLogContext,
1106
- field: "upstreamErrorCode" | "upstreamRequestId",
1107
- value: unknown,
1108
- ): void {
1109
- if (logCtx[field] !== undefined) return;
1110
- if (typeof value !== "string" || !UPSTREAM_DIAGNOSTIC_TOKEN.test(value)) return;
1111
- logCtx[field] = value;
1112
- const status = logCtx.terminalHttpStatus;
1113
- if (status === undefined || status >= 500) console.warn(`[opencodex] upstream failure${status ? ` status=${status}` : ""}${logCtx.upstreamErrorCode ? ` code=${logCtx.upstreamErrorCode}` : ""}${logCtx.upstreamRequestId ? ` request_id=${logCtx.upstreamRequestId}` : ""}`);
1114
- }
1115
-
1116
- export function noteUpstreamRequestId(logCtx: RequestLogContext, headers: Headers): void {
1117
- noteBoundedDiagnostic(logCtx, "upstreamRequestId", headers.get("openai-request-id") ?? headers.get("x-request-id"));
1118
- }
1119
-
1120
1099
  function captureUpstreamError(logCtx: RequestLogContext, text: string | null): void {
1121
1100
  if (!text) return;
1122
1101
  let parsed: unknown | undefined;
@@ -1134,21 +1113,8 @@ function captureUpstreamErrorParsed(
1134
1113
  parsed: unknown | undefined,
1135
1114
  ): void {
1136
1115
  if (parsed !== undefined && parsed !== null) {
1137
- const json = parsed as {
1138
- type?: unknown;
1139
- error?: { message?: unknown; code?: unknown };
1140
- last_error?: { message?: unknown; code?: unknown };
1141
- response?: {
1142
- error?: { type?: unknown; code?: unknown; message?: unknown };
1143
- incomplete_details?: { reason?: unknown; message?: unknown };
1144
- };
1145
- };
1146
- captureTerminalHttpStatus(logCtx, json);
1147
- noteBoundedDiagnostic(
1148
- logCtx,
1149
- "upstreamErrorCode",
1150
- json.error?.code ?? json.response?.error?.code ?? json.last_error?.code,
1151
- );
1116
+ const json = parsed as UpstreamErrorPayload;
1117
+ captureUpstreamTerminalDiagnostics(logCtx, json);
1152
1118
  const reason = json?.response?.incomplete_details?.reason;
1153
1119
  if (json.type === "response.incomplete"
1154
1120
  && logCtx.terminalIncompleteReason === undefined
@@ -1192,75 +1158,6 @@ function incompleteReasonLabel(reason: string): string {
1192
1158
  }
1193
1159
  }
1194
1160
 
1195
- function captureTerminalHttpStatus(
1196
- logCtx: RequestLogContext,
1197
- json: {
1198
- type?: unknown;
1199
- code?: unknown;
1200
- message?: unknown;
1201
- error?: { type?: unknown; code?: unknown; message?: unknown };
1202
- last_error?: { type?: unknown; code?: unknown; message?: unknown };
1203
- response?: {
1204
- error?: { type?: unknown; code?: unknown; message?: unknown };
1205
- incomplete_details?: { code?: unknown; message?: unknown; reason?: unknown };
1206
- };
1207
- },
1208
- ): void {
1209
- if (logCtx.terminalHttpStatus !== undefined) return;
1210
- const type = json.type;
1211
- if (type !== "response.failed" && type !== "response.incomplete" && type !== "error") return;
1212
- const responseError = json.response?.error;
1213
- const responseDetails = json.response?.incomplete_details;
1214
- const candidates: Array<{ type?: unknown; code?: unknown; message?: unknown } | undefined> = [
1215
- json.error, json.last_error, responseError, responseDetails, json,
1216
- ];
1217
- const policy = candidates.some(candidate => (
1218
- candidate?.code === null || typeof candidate?.code === "string"
1219
- ) && isCyberPolicyCode(candidate.code as string | null | undefined))
1220
- || candidates.some(candidate => (
1221
- typeof candidate?.message === "string"
1222
- && candidate.message.trim().length > 0
1223
- && isCyberPolicyMessage(candidate.message)
1224
- ));
1225
- if (policy) {
1226
- logCtx.terminalErrorCode = CYBER_POLICY_ERROR_CODE;
1227
- logCtx.terminalHttpStatus = 400;
1228
- return;
1229
- }
1230
- // A quota terminal can carry only a structured reason, without an error message.
1231
- // Keep this separate from normal output limits and from the policy precedence above.
1232
- const quotaTag = (value: unknown): boolean => value === "usage_limit_reached"
1233
- || value === "rate_limit_exceeded" || value === "insufficient_quota";
1234
- const structuredRefusal = candidates.some(candidate => [400, 401, 403, 499].includes(
1235
- httpStatusFromTerminalError({
1236
- type: typeof candidate?.type === "string" ? candidate.type : undefined,
1237
- code: typeof candidate?.code === "string" ? candidate.code : undefined,
1238
- }),
1239
- ));
1240
- const ordinaryIncompleteReason = typeof responseDetails?.reason === "string"
1241
- && ["max_output_tokens", "content_filter", "steered", "upstream_stall_timeout", "adapter_eof"].includes(responseDetails.reason);
1242
- if (type === "response.incomplete" && !structuredRefusal && (quotaTag(responseDetails?.reason) || candidates.some(candidate =>
1243
- quotaTag(candidate?.code)
1244
- || quotaTag(candidate?.type) || candidate?.type === "rate_limit_error"
1245
- || (!ordinaryIncompleteReason && typeof candidate?.message === "string" && isRateLimitOrQuotaFailureMessage(candidate.message))
1246
- ))) {
1247
- // The shared quota classifier also accepts a numeric HTTP status as its message.
1248
- // Preserve explicit payment-required evidence rather than relabeling it as 429.
1249
- logCtx.terminalHttpStatus = candidates.some(candidate => typeof candidate?.message === "string"
1250
- && Number(candidate.message.trim()) === 402) ? 402 : 429;
1251
- return;
1252
- }
1253
- if (type !== "response.failed" || !responseError || typeof responseError !== "object") return;
1254
- const responseCode = responseError.code === null || typeof responseError.code === "string"
1255
- ? responseError.code
1256
- : undefined;
1257
- logCtx.terminalHttpStatus = httpStatusFromTerminalError({
1258
- type: typeof responseError.type === "string" ? responseError.type : undefined,
1259
- code: responseCode,
1260
- message: typeof responseError.message === "string" ? responseError.message : undefined,
1261
- });
1262
- }
1263
-
1264
1161
  /** Map a terminal Responses error object to the HTTP status we record in /api/logs. */
1265
1162
  export function httpStatusFromTerminalError(error: {
1266
1163
  type?: string;
@@ -1467,12 +1364,12 @@ export function addFinalRequestLog(
1467
1364
  const effectiveStatus = status >= 500 && logCtx.upstreamError && isClientClosedMessage(logCtx.upstreamError)
1468
1365
  ? 499
1469
1366
  : status;
1470
- // A locally assigned code wins: it names a refusal this proxy made itself, which no
1471
- // status-plus-upstream-message classification can reconstruct.
1367
+ // Locally assigned codes name proxy refusals that status and upstream text cannot reconstruct.
1472
1368
  const errorCode = logCtx.errorCode ?? requestLogErrorCode(
1473
1369
  effectiveStatus,
1474
1370
  logCtx.upstreamError,
1475
1371
  logCtx.terminalErrorCode,
1372
+ logCtx.causeHint === "transport-ambiguous" && logCtx.terminalSource === "synthetic",
1476
1373
  );
1477
1374
  // A response.failed whose classified status is 499 is still a client cancel, not an upstream
1478
1375
  // terminal failure — keep /api/logs closeReason aligned with that.
@@ -1502,6 +1399,7 @@ export function addFinalRequestLog(
1502
1399
  ...(logCtx.transportPhase ? { transportPhase: logCtx.transportPhase } : {}),
1503
1400
  ...(logCtx.terminalSource ? { terminalSource: logCtx.terminalSource } : {}),
1504
1401
  outputObserved: logCtx.firstOutputMs !== undefined,
1402
+ ...(logCtx.causeHint ? { causeHint: logCtx.causeHint } : {}),
1505
1403
  locallyAnswered: logCtx.localTerminalReason !== undefined,
1506
1404
  ...(logCtx.activeAttempt ? { attempt: logCtx.activeAttempt } : {}),
1507
1405
  });
@@ -1620,8 +1518,7 @@ export function addFinalRequestLog(
1620
1518
  ...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
1621
1519
  ...(closeReason ? { closeReason } : {}),
1622
1520
  ...(logCtx.upstreamError ? { upstreamError: logCtx.upstreamError } : {}),
1623
- ...(logCtx.upstreamErrorCode ? { upstreamErrorCode: logCtx.upstreamErrorCode } : {}),
1624
- ...(logCtx.upstreamRequestId ? { upstreamRequestId: logCtx.upstreamRequestId } : {}),
1521
+ ...upstreamDiagnosticLogFields(logCtx),
1625
1522
  usageStatus,
1626
1523
  ...(loggedUsage ? { usage: loggedUsage } : {}),
1627
1524
  ...(totalTokens !== undefined ? { totalTokens } : {}),
@@ -83,6 +83,7 @@ export function createAdapterContinuations(
83
83
  | "oauthDispatch"
84
84
  | "invalidateSameTargetRequest"
85
85
  | "resolveSelectionAdapter"
86
+ | "resolveCopilotSelection"
86
87
  | "anthropicInstance"
87
88
  | "currentAnthropicRouteDecision"
88
89
  | "anthropicRouteDecision"
@@ -156,7 +157,8 @@ export function createAdapterContinuations(
156
157
  /**
157
158
  * One bounded internal re-ask for Anthropic end_turn-without-tool-call turns. Replays the
158
159
  * continuation on a 429 with the same-key retry budget (hoisted per request), then falls
159
- * back to key/account failover; a failure becomes an in-stream adapter error so the client
160
+ * back to key/account failover, rebuilding Copilot model, wire, and credential negotiation.
161
+ * A failure becomes an in-stream adapter error so the client
160
162
  * never sees a second hidden HTTP response or an unbounded retry loop.
161
163
  */
162
164
  const fetchTerminalGuardContinuation = async function* (
@@ -377,6 +379,12 @@ export function createAdapterContinuations(
377
379
  if (rotated) {
378
380
  try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
379
381
  route.provider = rotated;
382
+ try { await transportState.resolveCopilotSelection(nextParsed); }
383
+ catch (error) {
384
+ yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}`,
385
+ ...(options.abortSignal?.aborted || upstream.signal.aborted ? { status: 499 } : {}) };
386
+ return;
387
+ }
380
388
  invalidateSameTargetRequest();
381
389
  transportState.activeAdapter = resolveSelectionAdapter(
382
390
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
@@ -1,3 +1,4 @@
1
+ import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
1
2
  import { classifyAnthropic429 } from "../../oauth/anthropic-rate-limit-policy";
2
3
  import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
3
4
  import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
@@ -134,12 +135,15 @@ export async function prepareAdapterExchange(
134
135
  | "transportToken"
135
136
  | "oauthDispatch"
136
137
  | "imageTierBias"
138
+ | "oauth401ReplayAttempted"
137
139
  | "isOAuth401ReplayProvider"
138
140
  | "sentOAuthSnapshot"
139
141
  | "refreshResolvedOAuthSelection"
140
142
  | "replayOAuthCredentialSnapshot"
141
143
  | "invalidateSameTargetRequest"
142
144
  | "resolveSelectionAdapter"
145
+ | "resolveCopilotSelection"
146
+ | "copilotRefusalResponse"
143
147
  | "anthropicInstance"
144
148
  | "currentAnthropicRouteDecision"
145
149
  | "anthropicRouteDecision"
@@ -156,6 +160,7 @@ export async function prepareAdapterExchange(
156
160
  responseEffects: Pick<ResponsesEffects, "cancelResponseCompletion" | "notifyResponseComplete" | "refreshRequestToolAliases">,
157
161
  sendBudgetState: Pick<
158
162
  ResponsesSendBudget,
163
+ | "adapterSendBudget"
159
164
  | "adapterDispatchBudget"
160
165
  | "noteAdapterPhysicalSend"
161
166
  | "noteAdapterRecoveryWithheld"
@@ -182,6 +187,8 @@ export async function prepareAdapterExchange(
182
187
  refreshResolvedOAuthSelection,
183
188
  invalidateSameTargetRequest,
184
189
  resolveSelectionAdapter,
190
+ resolveCopilotSelection,
191
+ copilotRefusalResponse,
185
192
  anthropicInstance,
186
193
  currentAnthropicRouteDecision,
187
194
  anthropicSessionKey,
@@ -199,6 +206,7 @@ export async function prepareAdapterExchange(
199
206
  } = requestState;
200
207
  const { cancelResponseCompletion, notifyResponseComplete, refreshRequestToolAliases } = responseEffects;
201
208
  const {
209
+ adapterSendBudget,
202
210
  adapterDispatchBudget,
203
211
  noteAdapterPhysicalSend,
204
212
  noteAdapterRecoveryWithheld,
@@ -222,6 +230,17 @@ export async function prepareAdapterExchange(
222
230
 
223
231
  const upstream = new AbortController();
224
232
  const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
233
+ /** Contain replacement negotiation before adapter admission and release its upstream abort link. */
234
+ const resolveRotatedCopilotSelection = async (): Promise<Response | undefined> => {
235
+ try { await resolveCopilotSelection(parsed); }
236
+ catch (error) {
237
+ cleanupUpstreamAbort();
238
+ upstream.abort();
239
+ if ((options.abortSignal ?? req.signal).aborted) return clientCancelledResponse();
240
+ return copilotRefusalResponse(error)
241
+ ?? formatErrorResponse(502, "upstream_error", "GitHub Copilot Auto negotiation failed.");
242
+ }
243
+ };
225
244
  const connectMs = config.connectTimeoutMs ?? 200_000;
226
245
  // Bridge stall budget (seconds of silence before upstream_stall_timeout); the retry backoff
227
246
  // heartbeat interval is derived from it so the watchdog is always fed during deliberate waits.
@@ -338,10 +357,15 @@ export async function prepareAdapterExchange(
338
357
  * is invisible to it and a missed bump would replay a request built with a stale key.
339
358
  */
340
359
 
360
+ const initialRecovery = route.providerName === "github-copilot"
361
+ ? sendBudgetState.pendingHopPermit?.sendClass === "auth-recovery" ? "oauth-account-429"
362
+ : transportState.oauth401ReplayAttempted ? "oauth-401" : undefined
363
+ : undefined;
341
364
  let upstreamResponse: Response;
365
+ let handoffPermit: ResponsesSendBudget["pendingHopPermit"];
342
366
  try {
343
367
  if (transportState.activeAdapter.fetchResponse) {
344
- transportState.noteRoutedAttemptSend(inputTokenEstimate);
368
+ transportState.noteRoutedAttemptSend(inputTokenEstimate, initialRecovery);
345
369
  const producer = adapterDispatchBudget?.beginSpendProducer?.();
346
370
  try {
347
371
  upstreamResponse = await withProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal, pacingSlot =>
@@ -374,7 +398,22 @@ export async function prepareAdapterExchange(
374
398
  // reset-only semantics so combo failover hops on the first 5xx.
375
399
  const transientPolicy = transientRetryPolicyFor(route.provider);
376
400
  const resetPolicy = resetReplayPolicyFor(route.provider);
377
- const compactPrepaid = options.compactionRecoveryAttempted ? sendBudgetState.pendingHopPermit : undefined;
401
+ // Rebook a native Copilot hop against the negotiated destination before dispatch.
402
+ // Raw externally counted permits settle through the retry helper's send reporter.
403
+ if (route.providerName === "github-copilot" && adapterSendBudget && sendBudgetState.pendingHopPermit
404
+ && sendBudgetState.pendingHopPermit.sendClass === "auth-recovery") {
405
+ const original = sendBudgetState.pendingHopPermit;
406
+ sendBudgetState.pendingHopPermit = undefined;
407
+ original.release();
408
+ const decision = adapterSendBudget.reserveDispatch({
409
+ sendClass: original.sendClass,
410
+ targetKey: `${route.providerName}|${builtInitialRequest.url}|${route.modelId}`,
411
+ countedExternally: true,
412
+ });
413
+ if (!decision.allowed) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
414
+ handoffPermit = decision.permit;
415
+ }
416
+ const compactPrepaid = handoffPermit ?? (options.compactionRecoveryAttempted ? sendBudgetState.pendingHopPermit : undefined);
378
417
  if (compactPrepaid) sendBudgetState.pendingHopPermit = undefined;
379
418
  let compactPrepaidUsed = false;
380
419
  // A combo-owned or compaction-prepaid send is settled by the physical-dispatch receipt
@@ -407,7 +446,7 @@ export async function prepareAdapterExchange(
407
446
  if (!compactPrepaid.use()) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
408
447
  compactPrepaidUsed = true;
409
448
  }
410
- transportState.noteRoutedAttemptSend(inputTokenEstimate, recovery);
449
+ transportState.noteRoutedAttemptSend(inputTokenEstimate, recovery ?? initialRecovery);
411
450
  return fetchWithHeaderTimeout(builtInitialRequest.url, applyUpstreamRecoveryInit({
412
451
  method: builtInitialRequest.method,
413
452
  headers: builtInitialRequest.headers,
@@ -456,6 +495,11 @@ export async function prepareAdapterExchange(
456
495
  cleanupUpstreamAbort();
457
496
  upstream.abort();
458
497
  if (options.abortSignal?.aborted) return clientCancelledResponse();
498
+ const storageRefusal = spendLedgerStorageErrorResponse(err, logCtx);
499
+ if (storageRefusal) {
500
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
501
+ return storageRefusal;
502
+ }
459
503
  const refusal = err instanceof UpstreamRetryEvidenceError ? err.cause : err;
460
504
  const codexRefusal = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
461
505
  now: Date.now(), accountSelector: route.codexAccountNamespace,
@@ -486,6 +530,7 @@ export async function prepareAdapterExchange(
486
530
  const msg = describeUpstreamConnectFailure(err, connectMs);
487
531
  return formatErrorResponse(502, "upstream_error", msg);
488
532
  } finally {
533
+ handoffPermit?.release();
489
534
  builtInitialRequest.releaseBodyObservation?.();
490
535
  }
491
536
 
@@ -509,7 +554,7 @@ export async function prepareAdapterExchange(
509
554
  // Console Go answers a transient 400 "Invalid upload request." for bodies it accepts
510
555
  // moments later; at most one byte-identical replay is allowed per request.
511
556
  const consoleGoUploadRetryGuard: { attempted: boolean } = { attempted: false };
512
- let oauth401ReplayAttempted = false;
557
+ let oauth401ReplayAttempted = transportState.oauth401ReplayAttempted;
513
558
  let antigravityAuthRotationAttempted = false;
514
559
  // At most one reasoning-effort downgrade per request. This sits outside the recovery loop
515
560
  // below for the same reason the two guards above do: a guard declared inside it is reset by
@@ -692,6 +737,13 @@ export async function prepareAdapterExchange(
692
737
  retryRequest.releaseBodyObservation?.();
693
738
  }
694
739
  } catch (err) {
740
+ const storageRefusal = !options.abortSignal?.aborted && spendLedgerStorageErrorResponse(err, logCtx);
741
+ if (storageRefusal) {
742
+ cleanupUpstreamAbort();
743
+ upstream.abort();
744
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
745
+ return { failed: storageRefusal };
746
+ }
695
747
  const codexRefusal = !options.abortSignal?.aborted && mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
696
748
  now: Date.now(), accountSelector: route.codexAccountNamespace,
697
749
  });
@@ -793,7 +845,7 @@ export async function prepareAdapterExchange(
793
845
  && !oauth401ReplayAttempted
794
846
  && !sendBudgetExhausted()
795
847
  ) {
796
- oauth401ReplayAttempted = true;
848
+ oauth401ReplayAttempted = transportState.oauth401ReplayAttempted = true;
797
849
  let refreshed: OAuthAccessSnapshot;
798
850
  try {
799
851
  refreshed = await refreshResolvedOAuthSelection(transportState.sentOAuthSnapshot);
@@ -864,7 +916,7 @@ export async function prepareAdapterExchange(
864
916
  if (route.providerName === "kiro") {
865
917
  parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
866
918
  }
867
- const refreshedProvider = resolveProviderTransport(
919
+ let refreshedProvider = resolveProviderTransport(
868
920
  route.providerName,
869
921
  {
870
922
  ...route.provider,
@@ -877,6 +929,9 @@ export async function prepareAdapterExchange(
877
929
  : undefined,
878
930
  );
879
931
  route.provider = refreshedProvider;
932
+ const copilotRefusal = await resolveRotatedCopilotSelection();
933
+ if (copilotRefusal) return copilotRefusal;
934
+ refreshedProvider = route.provider;
880
935
  invalidateSameTargetRequest();
881
936
  transportState.activeAdapter = resolveSelectionAdapter(
882
937
  resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy),
@@ -929,6 +984,8 @@ export async function prepareAdapterExchange(
929
984
  // until runtime cleanup (one per rotated key).
930
985
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
931
986
  route.provider = rotated;
987
+ const selectionFailure = await resolveRotatedCopilotSelection();
988
+ if (selectionFailure) return selectionFailure;
932
989
  invalidateSameTargetRequest();
933
990
  transportState.activeAdapter = resolveSelectionAdapter(
934
991
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
@@ -1075,6 +1132,8 @@ export async function prepareAdapterExchange(
1075
1132
  // until runtime cleanup (one per rotated key under a rate-limit storm).
1076
1133
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1077
1134
  route.provider = rotated;
1135
+ const selectionFailure = await resolveRotatedCopilotSelection();
1136
+ if (selectionFailure) return selectionFailure;
1078
1137
  invalidateSameTargetRequest();
1079
1138
  transportState.activeAdapter = resolveSelectionAdapter(
1080
1139
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),