@bitkyc08/opencodex 2.7.9-preview.20260712.1 → 2.7.9

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 (39) hide show
  1. package/README.md +3 -1
  2. package/gui/dist/assets/index-BAAFKwsh.js +40 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +2 -2
  5. package/src/adapters/cursor/transport-retry.ts +5 -3
  6. package/src/adapters/google-errors.ts +9 -19
  7. package/src/adapters/google-http.ts +29 -66
  8. package/src/adapters/kiro-errors.ts +10 -23
  9. package/src/adapters/kiro-retry.ts +26 -58
  10. package/src/adapters/upstream-http-error.ts +48 -0
  11. package/src/bridge.ts +6 -2
  12. package/src/claude/gateway-cache.ts +3 -3
  13. package/src/claude/outbound.ts +117 -40
  14. package/src/cli/claude.ts +36 -4
  15. package/src/config.ts +54 -3
  16. package/src/lib/destination-policy.ts +167 -0
  17. package/src/lib/injection-debug-log.ts +34 -0
  18. package/src/lib/upstream-retry.ts +53 -3
  19. package/src/lib/windows-secret-acl.ts +173 -0
  20. package/src/oauth/index.ts +9 -7
  21. package/src/oauth/store.ts +1 -0
  22. package/src/providers/registry.ts +10 -3
  23. package/src/providers/xai-transport.ts +89 -0
  24. package/src/router.ts +6 -1
  25. package/src/server/auth-cors.ts +4 -0
  26. package/src/server/claude-messages.ts +32 -2
  27. package/src/server/management-api.ts +159 -33
  28. package/src/server/request-decompress.ts +45 -12
  29. package/src/server/responses.ts +21 -12
  30. package/src/server/system-env.ts +110 -68
  31. package/src/service.ts +4 -0
  32. package/src/types.ts +25 -5
  33. package/src/vision/anthropic-describe.ts +185 -0
  34. package/src/vision/index.ts +219 -10
  35. package/src/web-search/anthropic-executor.ts +187 -0
  36. package/src/web-search/executor.ts +4 -2
  37. package/src/web-search/index.ts +80 -18
  38. package/src/web-search/loop.ts +14 -2
  39. package/gui/dist/assets/index-Csp2AZYr.js +0 -40
@@ -16,7 +16,7 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-Csp2AZYr.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-BAAFKwsh.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-Cq8maiJf.css">
21
21
  </head>
22
22
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.7.9-preview.20260712.1",
3
+ "version": "2.7.9",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -36,7 +36,7 @@
36
36
  "dev:proxy": "bun run src/cli/index.ts start",
37
37
  "dev:gui": "cd gui && bun run dev",
38
38
  "start": "bun run src/cli/index.ts start",
39
- "test": "bun test ./tests/",
39
+ "test": "bun test --isolate ./tests/",
40
40
  "typecheck": "bun x tsc --noEmit",
41
41
  "privacy:scan": "bun scripts/privacy-scan.ts",
42
42
  "generate:jawcode-metadata": "bun scripts/generate-jawcode-metadata.ts",
@@ -1,6 +1,6 @@
1
1
  import type { CursorRunRequest, CursorServerMessage } from "./types";
2
2
  import type { CursorTransport, CursorTransportFactory, CursorTransportFactoryInput } from "./transport";
3
- import { abortError, sleepWithAbort } from "../../lib/upstream-retry";
3
+ import { abortError, retryBackoffDelayMs, sleepWithAbort } from "../../lib/upstream-retry";
4
4
  import { debugProviderDiagnostic } from "../../lib/debug";
5
5
  import { safeCursorErrorMessage } from "./cursor-errors";
6
6
 
@@ -40,8 +40,10 @@ export function isRetryableCursorError(err: unknown): boolean {
40
40
  }
41
41
 
42
42
  export function cursorRetryDelayMs(attempt: number): number {
43
- const exp = Math.min(CURSOR_RETRY_BASE_MS * 2 ** attempt, CURSOR_RETRY_MAX_MS);
44
- return Math.floor(exp * (0.8 + Math.random() * 0.4));
43
+ return retryBackoffDelayMs(attempt, {
44
+ baseDelayMs: CURSOR_RETRY_BASE_MS,
45
+ maxDelayMs: CURSOR_RETRY_MAX_MS,
46
+ });
45
47
  }
46
48
 
47
49
  /**
@@ -1,14 +1,4 @@
1
- import { redactSecretString } from "../lib/redact";
2
-
3
- const ABSOLUTE_PATH_PATTERN = /(?:\/Users\/[^ "';,]+|\/home\/[^ "';,]+|\/root\/[^ "';,]*|[A-Za-z]:\\Users\\[^ "';,]+)/g;
4
-
5
- function sanitizeGoogleErrorText(value: string): string {
6
- return redactSecretString(value).replace(ABSOLUTE_PATH_PATTERN, "[REDACTED_PATH]");
7
- }
8
-
9
- function safeString(value: unknown): string | undefined {
10
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
11
- }
1
+ import { parseUpstreamJsonPayload, safeUpstreamErrorString, sanitizeUpstreamErrorText } from "./upstream-http-error";
12
2
 
13
3
  /** Pull the human detail out of the Google API error envelope `{error:{message,status,code}}`. */
14
4
  function googleErrorDetail(payloadText: string): { message?: string; status?: string } {
@@ -16,13 +6,13 @@ function googleErrorDetail(payloadText: string): { message?: string; status?: st
16
6
  if (!trimmed || (!trimmed.startsWith("{") && !trimmed.startsWith("["))) {
17
7
  return { message: trimmed || undefined };
18
8
  }
19
- try {
20
- const parsed = JSON.parse(trimmed) as { error?: { message?: unknown; status?: unknown } };
21
- const err = parsed.error;
22
- return { message: safeString(err?.message), status: safeString(err?.status) };
23
- } catch {
24
- return {};
25
- }
9
+ const parsed = parseUpstreamJsonPayload(trimmed);
10
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
11
+ const err = (parsed as { error?: { message?: unknown; status?: unknown } }).error;
12
+ return {
13
+ message: safeUpstreamErrorString(err?.message),
14
+ status: safeUpstreamErrorString(err?.status),
15
+ };
26
16
  }
27
17
 
28
18
  function classifyGoogle(label: string, status: number | undefined, enumStatus: string | undefined, text: string): string {
@@ -59,7 +49,7 @@ function classifyGoogle(label: string, status: number | undefined, enumStatus: s
59
49
  export function safeGoogleHttpErrorMessage(label: string, status: number, payloadText: string): string {
60
50
  const { message, status: enumStatus } = googleErrorDetail(payloadText);
61
51
  const prefix = classifyGoogle(label, status, enumStatus, [message, enumStatus].filter(Boolean).join(" "));
62
- const detail = message ? sanitizeGoogleErrorText(message).slice(0, 500) : `HTTP ${status}`;
52
+ const detail = message ? sanitizeUpstreamErrorText(message).slice(0, 500) : `HTTP ${status}`;
63
53
  return `${prefix}: ${detail}`;
64
54
  }
65
55
 
@@ -1,57 +1,22 @@
1
1
  import type { AdapterFetchContext, AdapterRequest } from "./base";
2
2
  import { isQuotaExhaustedBody, retryableGoogleStatus, safeGoogleHttpErrorMessage } from "./google-errors";
3
- import { clearableDeadline } from "../lib/abort";
4
- import { readBoundedResponseBody } from "../lib/bounded-body";
5
- import { abortError, sleepWithAbort } from "../lib/upstream-retry";
3
+ import { normalizeUpstreamHttpErrorResponse, readDisplaySafeErrorPayloadText } from "./upstream-http-error";
4
+ import {
5
+ abortError,
6
+ cancelResponseBodyBestEffort,
7
+ fetchWithAttemptDeadline,
8
+ retryBackoffDelayMs,
9
+ sleepWithAbort,
10
+ } from "../lib/upstream-retry";
6
11
 
7
12
  const GOOGLE_RETRY_ATTEMPTS = 3;
8
13
  const GOOGLE_RETRY_BASE_MS = 250;
9
14
  const GOOGLE_RETRY_MAX_MS = 2_000;
10
15
 
11
- function retryAfterMs(headers: Headers): number | undefined {
12
- const raw = headers.get("retry-after")?.trim();
13
- if (!raw) return undefined;
14
- const seconds = Number(raw);
15
- if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
16
- const dateMs = Date.parse(raw);
17
- if (!Number.isFinite(dateMs)) return undefined;
18
- return Math.max(0, dateMs - Date.now());
19
- }
20
-
21
- function retryDelayMs(attempt: number, headers?: Headers): number {
22
- const retryAfter = headers ? retryAfterMs(headers) : undefined;
23
- if (retryAfter !== undefined) return Math.min(retryAfter, GOOGLE_RETRY_MAX_MS);
24
- const exp = Math.min(GOOGLE_RETRY_BASE_MS * (2 ** attempt), GOOGLE_RETRY_MAX_MS);
25
- return Math.floor(exp * (0.8 + Math.random() * 0.4));
26
- }
27
-
28
- function cancelResponseBodyBestEffort(res: Response): void {
29
- try {
30
- const cancellation = res.body?.cancel();
31
- if (cancellation) void cancellation.catch(() => {});
32
- } catch {
33
- // Cancellation is cleanup only; retries must not wait for or fail because of it.
34
- }
35
- }
36
-
37
- async function boundedBodyText(res: Response, signal?: AbortSignal): Promise<string> {
38
- try {
39
- const body = await readBoundedResponseBody(res, { signal });
40
- return body.displaySafe ? body.text : "";
41
- } catch (error) {
42
- if (signal?.aborted) throw error;
43
- return "";
44
- }
45
- }
46
-
47
16
  async function normalizeFinalGoogleError(label: string, res: Response, signal?: AbortSignal): Promise<Response> {
48
- if (res.ok) return res;
49
- const payloadText = await boundedBodyText(res, signal);
50
- const headers = new Headers(res.headers);
51
- headers.delete("content-encoding");
52
- headers.delete("content-length");
53
- return new Response(safeGoogleHttpErrorMessage(label, res.status, payloadText), {
54
- status: res.status, statusText: res.statusText, headers,
17
+ return normalizeUpstreamHttpErrorResponse(res, {
18
+ signal,
19
+ formatMessage: payloadText => safeGoogleHttpErrorMessage(label, res.status, payloadText),
55
20
  });
56
21
  }
57
22
 
@@ -67,41 +32,39 @@ export async function fetchGoogleWithRetry(label: string, request: AdapterReques
67
32
  for (let attempt = 0; attempt < GOOGLE_RETRY_ATTEMPTS; attempt++) {
68
33
  if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
69
34
  try {
70
- const attemptTimeout = clearableDeadline(timeoutMs, ctx.abortSignal);
71
- let res: Response;
72
- try {
73
- res = await fetch(request.url, {
74
- method: request.method, headers: request.headers, body: request.body,
75
- signal: attemptTimeout.signal,
76
- });
77
- } finally {
78
- // Only the header timer is cleared. The composed signal still contains the parent, so a
79
- // caller abort after headers continue to cancel consumption of the returned response body.
80
- attemptTimeout.clear();
81
- }
35
+ const res = await fetchWithAttemptDeadline(request.url, {
36
+ method: request.method,
37
+ headers: request.headers,
38
+ body: request.body,
39
+ }, timeoutMs, ctx.abortSignal);
82
40
  if (!retryableGoogleStatus(res.status) || attempt === GOOGLE_RETRY_ATTEMPTS - 1) {
83
41
  return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
84
42
  }
85
43
  // A 429 may be a transient rate limit (retry) or hard quota exhaustion (do NOT retry —
86
44
  // it won't recover for hours and burns retries). Peek the body to tell them apart.
87
45
  if (res.status === 429 && !ctx.returnRawErrors) {
88
- const peek = await boundedBodyText(res, ctx.abortSignal);
46
+ const peek = await readDisplaySafeErrorPayloadText(res, ctx.abortSignal);
89
47
  if (isQuotaExhaustedBody(peek)) {
90
- const headers = new Headers(res.headers);
91
- headers.delete("content-encoding");
92
- headers.delete("content-length");
93
- return new Response(safeGoogleHttpErrorMessage(label, res.status, peek), {
94
- status: res.status, statusText: res.statusText, headers,
48
+ return normalizeUpstreamHttpErrorResponse(res, {
49
+ signal: ctx.abortSignal,
50
+ formatMessage: payloadText => safeGoogleHttpErrorMessage(label, res.status, payloadText || peek),
95
51
  });
96
52
  }
97
53
  }
98
54
  cancelResponseBodyBestEffort(res);
99
- await sleepWithAbort(retryDelayMs(attempt, res.headers), ctx.abortSignal);
55
+ await sleepWithAbort(retryBackoffDelayMs(attempt, {
56
+ baseDelayMs: GOOGLE_RETRY_BASE_MS,
57
+ maxDelayMs: GOOGLE_RETRY_MAX_MS,
58
+ headers: res.headers,
59
+ }), ctx.abortSignal);
100
60
  } catch (err) {
101
61
  if (ctx.abortSignal?.aborted) throw err;
102
62
  lastError = err;
103
63
  if (attempt === GOOGLE_RETRY_ATTEMPTS - 1) throw err;
104
- await sleepWithAbort(retryDelayMs(attempt), ctx.abortSignal);
64
+ await sleepWithAbort(retryBackoffDelayMs(attempt, {
65
+ baseDelayMs: GOOGLE_RETRY_BASE_MS,
66
+ maxDelayMs: GOOGLE_RETRY_MAX_MS,
67
+ }), ctx.abortSignal);
105
68
  }
106
69
  }
107
70
  throw lastError ?? new Error(`${label} fetch failed`);
@@ -1,35 +1,22 @@
1
- import { redactSecretString } from "../lib/redact";
2
-
3
- const ABSOLUTE_PATH_PATTERN = /(?:\/Users\/[^ "';,]+|\/home\/[^ "';,]+|[A-Za-z]:\\Users\\[^ "';,]+)/g;
1
+ import { parseUpstreamJsonPayload, safeUpstreamErrorString, sanitizeUpstreamErrorText } from "./upstream-http-error";
4
2
  const DETAIL_KEYS = ["__type", "code", "error", "name", "message", "Message", "errorMessage"];
5
3
 
6
- function sanitizeKiroErrorText(value: string): string {
7
- return redactSecretString(value).replace(ABSOLUTE_PATH_PATTERN, "[REDACTED_PATH]");
8
- }
9
-
10
- function safeString(value: unknown): string | undefined {
11
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
12
- }
13
-
14
4
  function headerValue(headers: Headers | Record<string, unknown>, name: string): string | undefined {
15
- if (headers instanceof Headers) return name.startsWith(":") ? undefined : safeString(headers.get(name));
16
- return safeString(headers[name]) || safeString(headers[name.toLowerCase()]);
5
+ if (headers instanceof Headers) return name.startsWith(":") ? undefined : safeUpstreamErrorString(headers.get(name));
6
+ return safeUpstreamErrorString(headers[name]) || safeUpstreamErrorString(headers[name.toLowerCase()]);
17
7
  }
18
8
 
19
9
  function payloadDetails(payloadText: string): string[] {
20
10
  const trimmed = payloadText.trim();
21
11
  if (!trimmed) return [];
22
12
  if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return [trimmed];
23
- try {
24
- const parsed = JSON.parse(trimmed) as unknown;
25
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
26
- const obj = parsed as Record<string, unknown>;
27
- return DETAIL_KEYS.map(key => safeString(obj[key])).filter((v): v is string => !!v);
28
- }
29
- if (typeof parsed === "string" && parsed.trim()) return [parsed.trim()];
30
- } catch {
31
- return [];
13
+ const parsed = parseUpstreamJsonPayload(trimmed);
14
+ if (parsed === undefined) return [];
15
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
16
+ const obj = parsed as Record<string, unknown>;
17
+ return DETAIL_KEYS.map(key => safeUpstreamErrorString(obj[key])).filter((v): v is string => !!v);
32
18
  }
19
+ if (typeof parsed === "string" && parsed.trim()) return [parsed.trim()];
33
20
  return [];
34
21
  }
35
22
 
@@ -87,7 +74,7 @@ function classifyKiroText(status: number | undefined, text: string): string {
87
74
  function normalizedKiroErrorMessage(headers: Headers | Record<string, unknown>, payloadText: string, status?: number): string {
88
75
  const headerType = headerValue(headers, ":exception-type") || headerValue(headers, ":error-type");
89
76
  const parts = [headerType, ...payloadDetails(payloadText)].filter((part): part is string => !!part);
90
- const detail = parts.length > 0 ? sanitizeKiroErrorText(parts.join(": ")).slice(0, 500) : status ? `HTTP ${status}` : "";
77
+ const detail = parts.length > 0 ? sanitizeUpstreamErrorText(parts.join(": ")).slice(0, 500) : status ? `HTTP ${status}` : "";
91
78
  const prefix = classifyKiroText(status, [detail, headerType].filter(Boolean).join(" "));
92
79
  return detail ? `${prefix}: ${detail}` : prefix;
93
80
  }
@@ -1,8 +1,14 @@
1
1
  import type { AdapterFetchContext, AdapterRequest } from "./base";
2
2
  import { safeKiroHttpErrorMessage } from "./kiro-errors";
3
- import { clearableDeadline } from "../lib/abort";
4
- import { readBoundedResponseBody } from "../lib/bounded-body";
5
- import { abortError, isConnectionResetError, sleepWithAbort } from "../lib/upstream-retry";
3
+ import { normalizeUpstreamHttpErrorResponse } from "./upstream-http-error";
4
+ import {
5
+ abortError,
6
+ cancelResponseBodyBestEffort,
7
+ fetchWithAttemptDeadline,
8
+ isConnectionResetError,
9
+ retryBackoffDelayMs,
10
+ sleepWithAbort,
11
+ } from "../lib/upstream-retry";
6
12
 
7
13
  const KIRO_RETRY_ATTEMPTS = 3;
8
14
  const KIRO_RETRY_BASE_MS = 250;
@@ -12,52 +18,14 @@ function retryableKiroStatus(status: number): boolean {
12
18
  return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
13
19
  }
14
20
 
15
- function retryAfterMs(headers: Headers): number | undefined {
16
- const raw = headers.get("retry-after")?.trim();
17
- if (!raw) return undefined;
18
- const seconds = Number(raw);
19
- if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
20
- const dateMs = Date.parse(raw);
21
- if (!Number.isFinite(dateMs)) return undefined;
22
- return Math.max(0, dateMs - Date.now());
23
- }
24
-
25
- function retryDelayMs(attempt: number, headers?: Headers): number {
26
- const retryAfter = headers ? retryAfterMs(headers) : undefined;
27
- if (retryAfter !== undefined) return Math.min(retryAfter, KIRO_RETRY_MAX_MS);
28
- const exp = Math.min(KIRO_RETRY_BASE_MS * (2 ** attempt), KIRO_RETRY_MAX_MS);
29
- return Math.floor(exp * (0.8 + Math.random() * 0.4));
30
- }
31
-
32
- function cancelResponseBodyBestEffort(res: Response): void {
33
- try {
34
- const cancellation = res.body?.cancel();
35
- if (cancellation) void cancellation.catch(() => {});
36
- } catch {
37
- // Cancellation is cleanup only; retries must not wait for or fail because of it.
38
- }
39
- }
40
-
41
21
  function retryableKiroFetchError(err: unknown): boolean {
42
22
  return isConnectionResetError(err) || (err instanceof Error && err.name === "TimeoutError");
43
23
  }
44
24
 
45
25
  async function normalizeFinalKiroHttpError(res: Response, signal?: AbortSignal): Promise<Response> {
46
- if (res.ok) return res;
47
- let payloadText = "";
48
- try {
49
- const body = await readBoundedResponseBody(res, { signal });
50
- if (body.displaySafe) payloadText = body.text;
51
- } catch (error) {
52
- if (signal?.aborted) throw error;
53
- }
54
- const headers = new Headers(res.headers);
55
- headers.delete("content-encoding");
56
- headers.delete("content-length");
57
- return new Response(safeKiroHttpErrorMessage(res.status, res.headers, payloadText), {
58
- status: res.status,
59
- statusText: res.statusText,
60
- headers,
26
+ return normalizeUpstreamHttpErrorResponse(res, {
27
+ signal,
28
+ formatMessage: payloadText => safeKiroHttpErrorMessage(res.status, res.headers, payloadText),
61
29
  });
62
30
  }
63
31
 
@@ -67,28 +35,28 @@ export async function fetchKiroWithRetry(request: AdapterRequest, ctx: AdapterFe
67
35
  for (let attempt = 0; attempt < KIRO_RETRY_ATTEMPTS; attempt++) {
68
36
  if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
69
37
  try {
70
- const attemptTimeout = clearableDeadline(timeoutMs, ctx.abortSignal);
71
- let res: Response;
72
- try {
73
- res = await fetch(request.url, {
74
- method: request.method,
75
- headers: request.headers,
76
- body: request.body,
77
- signal: attemptTimeout.signal,
78
- });
79
- } finally {
80
- attemptTimeout.clear();
81
- }
38
+ const res = await fetchWithAttemptDeadline(request.url, {
39
+ method: request.method,
40
+ headers: request.headers,
41
+ body: request.body,
42
+ }, timeoutMs, ctx.abortSignal);
82
43
  if (!retryableKiroStatus(res.status) || attempt === KIRO_RETRY_ATTEMPTS - 1) {
83
44
  return ctx.returnRawErrors ? res : normalizeFinalKiroHttpError(res, ctx.abortSignal);
84
45
  }
85
46
  cancelResponseBodyBestEffort(res);
86
- await sleepWithAbort(retryDelayMs(attempt, res.headers), ctx.abortSignal);
47
+ await sleepWithAbort(retryBackoffDelayMs(attempt, {
48
+ baseDelayMs: KIRO_RETRY_BASE_MS,
49
+ maxDelayMs: KIRO_RETRY_MAX_MS,
50
+ headers: res.headers,
51
+ }), ctx.abortSignal);
87
52
  } catch (err) {
88
53
  if (ctx.abortSignal?.aborted) throw err;
89
54
  if (!retryableKiroFetchError(err) || attempt === KIRO_RETRY_ATTEMPTS - 1) throw err;
90
55
  lastError = err;
91
- await sleepWithAbort(retryDelayMs(attempt), ctx.abortSignal);
56
+ await sleepWithAbort(retryBackoffDelayMs(attempt, {
57
+ baseDelayMs: KIRO_RETRY_BASE_MS,
58
+ maxDelayMs: KIRO_RETRY_MAX_MS,
59
+ }), ctx.abortSignal);
92
60
  }
93
61
  }
94
62
  throw lastError ?? new Error("Kiro fetch failed");
@@ -0,0 +1,48 @@
1
+ import { readBoundedResponseBody } from "../lib/bounded-body";
2
+ import { redactSecretString } from "../lib/redact";
3
+
4
+ const ABSOLUTE_PATH_PATTERN = /(?:\/Users\/[^ "';,]+|\/home\/[^ "';,]+|\/root\/[^ "';,]*|[A-Za-z]:\\Users\\[^ "';,]+)/g;
5
+
6
+ export function sanitizeUpstreamErrorText(value: string): string {
7
+ return redactSecretString(value).replace(ABSOLUTE_PATH_PATTERN, "[REDACTED_PATH]");
8
+ }
9
+
10
+ export function safeUpstreamErrorString(value: unknown): string | undefined {
11
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
12
+ }
13
+
14
+ export function parseUpstreamJsonPayload(payloadText: string): unknown | undefined {
15
+ const trimmed = payloadText.trim();
16
+ if (!trimmed || (!trimmed.startsWith("{") && !trimmed.startsWith("["))) return undefined;
17
+ try {
18
+ return JSON.parse(trimmed) as unknown;
19
+ } catch {
20
+ return undefined;
21
+ }
22
+ }
23
+
24
+ export async function readDisplaySafeErrorPayloadText(res: Response, signal?: AbortSignal): Promise<string> {
25
+ try {
26
+ const body = await readBoundedResponseBody(res, { signal });
27
+ return body.displaySafe ? body.text : "";
28
+ } catch (error) {
29
+ if (signal?.aborted) throw error;
30
+ return "";
31
+ }
32
+ }
33
+
34
+ export async function normalizeUpstreamHttpErrorResponse(
35
+ res: Response,
36
+ opts: { signal?: AbortSignal; formatMessage: (payloadText: string) => string | Promise<string> },
37
+ ): Promise<Response> {
38
+ if (res.ok) return res;
39
+ const payloadText = await readDisplaySafeErrorPayloadText(res, opts.signal);
40
+ const headers = new Headers(res.headers);
41
+ headers.delete("content-encoding");
42
+ headers.delete("content-length");
43
+ return new Response(await opts.formatMessage(payloadText), {
44
+ status: res.status,
45
+ statusText: res.statusText,
46
+ headers,
47
+ });
48
+ }
package/src/bridge.ts CHANGED
@@ -379,11 +379,14 @@ export function bridgeToResponsesSSE(
379
379
  // Finalize an open web-search cell. `status` is "completed" on a normal end, or "failed" when
380
380
  // the stream terminates (error/incomplete) while a search was still in flight, so Codex never
381
381
  // leaves a "Searching the web" spinner spinning forever.
382
- const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[]) => {
382
+ // `sources` rides on the done item (additive field; codex-rs serde ignores unknown fields) so
383
+ // downstream translators (claude outbound) can fill web_search_tool_result content.
384
+ const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[]) => {
383
385
  if (!currentWebSearch) return;
384
386
  const item = {
385
387
  type: "web_search_call", id: currentWebSearch.itemId, status,
386
388
  action: webSearchAction(queries),
389
+ ...(sources && sources.length > 0 ? { sources } : {}),
387
390
  };
388
391
  emit("response.output_item.done", { output_index: currentWebSearch.outputIndex, item });
389
392
  finishedItems.push(item as OutputItem);
@@ -569,7 +572,7 @@ export function bridgeToResponsesSSE(
569
572
  });
570
573
  currentWebSearch = { itemId: event.id, outputIndex };
571
574
  }
572
- closeCurrentWebSearch(event.status ?? "completed", event.queries);
575
+ closeCurrentWebSearch(event.status ?? "completed", event.queries, event.sources);
573
576
  // Queue this search's sources for the next assistant message (dedup by URL).
574
577
  if (event.sources) {
575
578
  const seen = new Set(pendingWebSources.map(s => s.url));
@@ -861,6 +864,7 @@ export function buildResponseJSON(
861
864
  output.push({
862
865
  type: "web_search_call", id: e.id, status: e.status ?? "completed",
863
866
  action: webSearchAction(e.queries),
867
+ ...(e.sources && e.sources.length > 0 ? { sources: e.sources } : {}),
864
868
  });
865
869
  if (e.sources) {
866
870
  const seen = new Set(pendingWebSources.map(s => s.url));
@@ -30,7 +30,6 @@ export function writeGatewayModelCache(baseUrl: string, models: readonly Gateway
30
30
  try {
31
31
  // Mirror the CLI's usable-id filter so our file matches what it would cache.
32
32
  const usable = models.filter(m => /^(claude|anthropic)/i.test(m.id));
33
- if (usable.length === 0) return null;
34
33
  const cacheDir = join(configDir, "cache");
35
34
  mkdirSync(cacheDir, { recursive: true });
36
35
  const path = join(cacheDir, "gateway-models.json");
@@ -56,8 +55,9 @@ export async function refreshGatewayModelCacheFromProxy(port: number, timeoutMs
56
55
  signal: AbortSignal.timeout(timeoutMs),
57
56
  });
58
57
  if (!res.ok) return null;
59
- const body = await res.json() as { data?: Array<Record<string, unknown>> };
60
- const models: GatewayModelRow[] = (Array.isArray(body.data) ? body.data : [])
58
+ const body = await res.json() as { data?: unknown };
59
+ if (!Array.isArray(body.data)) return null;
60
+ const models: GatewayModelRow[] = body.data
61
61
  .filter(m => typeof m.id === "string" && (m.id as string).length > 0)
62
62
  .map(m => ({
63
63
  id: m.id as string,