@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.
- package/README.md +3 -1
- package/gui/dist/assets/index-BAAFKwsh.js +40 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/cursor/transport-retry.ts +5 -3
- package/src/adapters/google-errors.ts +9 -19
- package/src/adapters/google-http.ts +29 -66
- package/src/adapters/kiro-errors.ts +10 -23
- package/src/adapters/kiro-retry.ts +26 -58
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/bridge.ts +6 -2
- package/src/claude/gateway-cache.ts +3 -3
- package/src/claude/outbound.ts +117 -40
- package/src/cli/claude.ts +36 -4
- package/src/config.ts +54 -3
- package/src/lib/destination-policy.ts +167 -0
- package/src/lib/injection-debug-log.ts +34 -0
- package/src/lib/upstream-retry.ts +53 -3
- package/src/lib/windows-secret-acl.ts +173 -0
- package/src/oauth/index.ts +9 -7
- package/src/oauth/store.ts +1 -0
- package/src/providers/registry.ts +10 -3
- package/src/providers/xai-transport.ts +89 -0
- package/src/router.ts +6 -1
- package/src/server/auth-cors.ts +4 -0
- package/src/server/claude-messages.ts +32 -2
- package/src/server/management-api.ts +159 -33
- package/src/server/request-decompress.ts +45 -12
- package/src/server/responses.ts +21 -12
- package/src/server/system-env.ts +110 -68
- package/src/service.ts +4 -0
- package/src/types.ts +25 -5
- package/src/vision/anthropic-describe.ts +185 -0
- package/src/vision/index.ts +219 -10
- package/src/web-search/anthropic-executor.ts +187 -0
- package/src/web-search/executor.ts +4 -2
- package/src/web-search/index.ts +80 -18
- package/src/web-search/loop.ts +14 -2
- package/gui/dist/assets/index-Csp2AZYr.js +0 -40
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
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 {
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 ?
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
46
|
+
const peek = await readDisplaySafeErrorPayloadText(res, ctx.abortSignal);
|
|
89
47
|
if (isQuotaExhaustedBody(peek)) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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(
|
|
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(
|
|
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 {
|
|
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 :
|
|
16
|
-
return
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 ?
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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?:
|
|
60
|
-
|
|
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,
|