ghc-proxy 0.9.0 → 0.9.1
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 +1 -1
- package/dist/main.mjs +98 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,7 +119,7 @@ bunx ghc-proxy@latest selfcheck # Probe the packaged bundle (loads every to
|
|
|
119
119
|
| `--dump-failed-payloads` | `-D` | `false` | Dump failed `/responses` payloads on upstream 400 errors for debugging. Can also be enabled with `DUMP_FAILED_PAYLOADS=1`. |
|
|
120
120
|
| `--proxy-env` | -- | `false` | Use `HTTP_PROXY`/`HTTPS_PROXY` from env (Node.js only; Bun reads proxy env natively) |
|
|
121
121
|
| `--idle-timeout` | -- | `120` | Bun server idle timeout in seconds (`0` disables; Bun max is `255`; streaming routes disable idle timeout automatically) |
|
|
122
|
-
| `--upstream-timeout` | -- | `1800` | Upstream request timeout in seconds (0 to
|
|
122
|
+
| `--upstream-timeout` | -- | `1800` | Upstream request timeout in seconds (`0` disables). Enforced as a total-duration `AbortSignal`. Note both runtimes also apply their own ~300s **idle** timeout to `fetch` (Bun's built-in limit; Node's undici `headersTimeout`/`bodyTimeout`), which fires when no byte arrives for that long — a steadily streaming response is not capped by it, but a stalled one is rejected at ~300s and returned as a `504`. |
|
|
123
123
|
| `--upstream-queue-concurrency` | -- | `10` | Maximum concurrent Copilot upstream requests |
|
|
124
124
|
| `--upstream-queue-retries` | -- | `5` | Maximum retries for transient upstream responses. Completion requests (`/v1/messages`, `/chat/completions`, `/responses`) retry only `429`/`529`; effect-free requests also retry `408`, `500`, `502`, `503`, `504` |
|
|
125
125
|
| `--upstream-queue-base-delay` | -- | `2` | Base delay in seconds for upstream retry backoff when `Retry-After` is absent |
|
package/dist/main.mjs
CHANGED
|
@@ -7419,7 +7419,7 @@ const checkUsage = defineCommand({
|
|
|
7419
7419
|
});
|
|
7420
7420
|
//#endregion
|
|
7421
7421
|
//#region src/util/version.ts
|
|
7422
|
-
const VERSION = "0.9.
|
|
7422
|
+
const VERSION = "0.9.1";
|
|
7423
7423
|
//#endregion
|
|
7424
7424
|
//#region src/debug.ts
|
|
7425
7425
|
function getRuntimeInfo() {
|
|
@@ -47868,6 +47868,61 @@ function logRequest(method, url, status, elapsed, modelInfo, requestId) {
|
|
|
47868
47868
|
console.log(`${line}${formatModelMapping(modelInfo)}${rid}`);
|
|
47869
47869
|
}
|
|
47870
47870
|
//#endregion
|
|
47871
|
+
//#region src/lib/timeout-error.ts
|
|
47872
|
+
/**
|
|
47873
|
+
* Whether an error represents a request that timed out or was aborted.
|
|
47874
|
+
*
|
|
47875
|
+
* The shape differs by runtime, so the check is structural rather than a
|
|
47876
|
+
* single `name` comparison:
|
|
47877
|
+
* - Bun rejects with a flat `DOMException` named `TimeoutError` (its ~300s
|
|
47878
|
+
* `fetch` ceiling, `AbortSignal.timeout`) or `AbortError`.
|
|
47879
|
+
* - Node rejects with `TypeError('fetch failed' | 'terminated')` and puts the
|
|
47880
|
+
* real undici error on `.cause` (`HeadersTimeoutError`, `BodyTimeoutError`,
|
|
47881
|
+
* `ConnectTimeoutError`), so the top-level error carries no signal at all —
|
|
47882
|
+
* `TypeError('fetch failed')` is also what `ECONNREFUSED` and DNS failures
|
|
47883
|
+
* look like. The discriminator is the cause's `name`/`code`.
|
|
47884
|
+
*
|
|
47885
|
+
* Both runtimes enforce a ~300s upstream ceiling by default (Node's is
|
|
47886
|
+
* undici's `headersTimeout`/`bodyTimeout` default of `300e3`), which fires
|
|
47887
|
+
* long before the configured `--upstream-timeout` of 1800s.
|
|
47888
|
+
*
|
|
47889
|
+
* Kept in one place because the rule is checked on both sides of the stream
|
|
47890
|
+
* boundary: `src/server.ts` maps it to a 504 before the first byte, and the
|
|
47891
|
+
* Anthropic stream transducer maps it to an SSE error frame after. Two
|
|
47892
|
+
* implementations of "what counts as a timeout" is how one of them ends up
|
|
47893
|
+
* recognizing only half the errors.
|
|
47894
|
+
*/
|
|
47895
|
+
const TIMEOUT_ERROR_NAMES = new Set([
|
|
47896
|
+
"AbortError",
|
|
47897
|
+
"TimeoutError",
|
|
47898
|
+
"ConnectTimeoutError",
|
|
47899
|
+
"HeadersTimeoutError",
|
|
47900
|
+
"BodyTimeoutError"
|
|
47901
|
+
]);
|
|
47902
|
+
const TIMEOUT_ERROR_CODES = new Set([
|
|
47903
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
47904
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
47905
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
47906
|
+
"ETIMEDOUT"
|
|
47907
|
+
]);
|
|
47908
|
+
const MAX_CAUSE_DEPTH = 5;
|
|
47909
|
+
function matchesTimeoutShape(value, depth) {
|
|
47910
|
+
if (typeof value !== "object" || value === null) return false;
|
|
47911
|
+
const candidate = value;
|
|
47912
|
+
if (typeof candidate.name === "string" && TIMEOUT_ERROR_NAMES.has(candidate.name)) return true;
|
|
47913
|
+
if (typeof candidate.code === "string" && TIMEOUT_ERROR_CODES.has(candidate.code)) return true;
|
|
47914
|
+
if (depth >= MAX_CAUSE_DEPTH) return false;
|
|
47915
|
+
if (matchesTimeoutShape(candidate.cause, depth + 1)) return true;
|
|
47916
|
+
return Array.isArray(candidate.errors) && candidate.errors.some((inner) => matchesTimeoutShape(inner, depth + 1));
|
|
47917
|
+
}
|
|
47918
|
+
function isTimeoutLikeError(error) {
|
|
47919
|
+
try {
|
|
47920
|
+
return matchesTimeoutShape(error, 0);
|
|
47921
|
+
} catch {
|
|
47922
|
+
return false;
|
|
47923
|
+
}
|
|
47924
|
+
}
|
|
47925
|
+
//#endregion
|
|
47871
47926
|
//#region src/lib/sse-adapter.ts
|
|
47872
47927
|
/**
|
|
47873
47928
|
* Serializes Anthropic stream events into SSE output items
|
|
@@ -49418,6 +49473,14 @@ function createUpstreamSignal(clientSignal, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
49418
49473
|
}
|
|
49419
49474
|
/**
|
|
49420
49475
|
* Convenience wrapper that reads the upstream timeout from runtime config.
|
|
49476
|
+
*
|
|
49477
|
+
* This signal is a *total-duration* limit. Both runtimes separately apply an
|
|
49478
|
+
* ~300s **idle** timeout to `fetch` — Bun's is built in, Node's is undici's
|
|
49479
|
+
* `headersTimeout` / `bodyTimeout` default of `300e3` — which resets on every
|
|
49480
|
+
* byte received. A response that keeps streaming therefore runs past 300s and
|
|
49481
|
+
* is bounded only by this signal; a stalled one is rejected at ~300s by the
|
|
49482
|
+
* runtime instead. `isTimeoutLikeError` recognizes both runtimes' shapes so
|
|
49483
|
+
* every path maps to a 504.
|
|
49421
49484
|
*/
|
|
49422
49485
|
function createUpstreamSignalFromConfig(clientSignal) {
|
|
49423
49486
|
return createUpstreamSignal(clientSignal, authStore.upstreamTimeoutSeconds !== void 0 ? authStore.upstreamTimeoutSeconds * 1e3 : void 0);
|
|
@@ -50465,14 +50528,9 @@ var AnthropicStreamTranslator = class {
|
|
|
50465
50528
|
this.state.messageStartSent = true;
|
|
50466
50529
|
}
|
|
50467
50530
|
getErrorMessage(error) {
|
|
50468
|
-
if (
|
|
50531
|
+
if (isTimeoutLikeError(error)) return "Upstream streaming request timed out. Please retry.";
|
|
50469
50532
|
return "An unexpected error occurred during streaming.";
|
|
50470
50533
|
}
|
|
50471
|
-
isTimeoutError(error) {
|
|
50472
|
-
if (error instanceof DOMException) return error.name === "TimeoutError";
|
|
50473
|
-
if (error instanceof Error) return error.name === "TimeoutError";
|
|
50474
|
-
return false;
|
|
50475
|
-
}
|
|
50476
50534
|
toConversationDeltas(chunk) {
|
|
50477
50535
|
if (chunk.choices.length === 0) return [];
|
|
50478
50536
|
const choice = chunk.choices.toSorted((left, right) => left.index - right.index)[0];
|
|
@@ -53344,6 +53402,33 @@ function createUsageRoute() {
|
|
|
53344
53402
|
//#endregion
|
|
53345
53403
|
//#region src/server.ts
|
|
53346
53404
|
const isBun = typeof globalThis.Bun !== "undefined";
|
|
53405
|
+
/**
|
|
53406
|
+
* Maps a thrown error to a client response.
|
|
53407
|
+
*
|
|
53408
|
+
* `set.status` is written on every branch because `onError` returns a fresh
|
|
53409
|
+
* `Response` instead of falling through Elysia's normal path — `set.status`
|
|
53410
|
+
* would otherwise still hold whatever it was before the throw, and the access
|
|
53411
|
+
* log in `onAfterResponse` reads it. Without the write-back, a 504 is logged
|
|
53412
|
+
* as a 500.
|
|
53413
|
+
*
|
|
53414
|
+
* Exported so tests exercise this mapping rather than a copy of it.
|
|
53415
|
+
*/
|
|
53416
|
+
function handleRouteError({ code, error, set }) {
|
|
53417
|
+
if (code === "HTTP") return;
|
|
53418
|
+
if (isTimeoutLikeError(error)) {
|
|
53419
|
+
set.status = 504;
|
|
53420
|
+
return Response.json({ error: {
|
|
53421
|
+
message: "Upstream request timed out before a response was received.",
|
|
53422
|
+
type: "timeout_error"
|
|
53423
|
+
} }, { status: 504 });
|
|
53424
|
+
}
|
|
53425
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53426
|
+
set.status = 500;
|
|
53427
|
+
return Response.json({ error: {
|
|
53428
|
+
message,
|
|
53429
|
+
type: "error"
|
|
53430
|
+
} }, { status: 500 });
|
|
53431
|
+
}
|
|
53347
53432
|
function createServer(options) {
|
|
53348
53433
|
return new Elysia({
|
|
53349
53434
|
adapter: isBun ? void 0 : node(),
|
|
@@ -53363,18 +53448,11 @@ function createServer(options) {
|
|
|
53363
53448
|
const elapsed = formatElapsed(requestStart);
|
|
53364
53449
|
const status = typeof set.status === "number" ? set.status : 200;
|
|
53365
53450
|
logRequest(request.method, request.url, status, elapsed, getRequestModelMapping(request), requestId);
|
|
53366
|
-
}).onError(({ code, error }) => {
|
|
53367
|
-
|
|
53368
|
-
|
|
53369
|
-
|
|
53370
|
-
|
|
53371
|
-
} }, { status: 504 });
|
|
53372
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
53373
|
-
return Response.json({ error: {
|
|
53374
|
-
message,
|
|
53375
|
-
type: "error"
|
|
53376
|
-
} }, { status: 500 });
|
|
53377
|
-
}).get("/", () => "Server running").get("/health", () => ({
|
|
53451
|
+
}).onError(({ code, error, set }) => handleRouteError({
|
|
53452
|
+
code,
|
|
53453
|
+
error,
|
|
53454
|
+
set
|
|
53455
|
+
})).get("/", () => "Server running").get("/health", () => ({
|
|
53378
53456
|
status: "ok",
|
|
53379
53457
|
copilotToken: !!authStore.copilotToken,
|
|
53380
53458
|
modelsLoaded: !!modelCache.getModels(),
|
|
@@ -53547,7 +53625,7 @@ const start = defineCommand({
|
|
|
53547
53625
|
"upstream-timeout": {
|
|
53548
53626
|
type: "string",
|
|
53549
53627
|
default: "1800",
|
|
53550
|
-
description: "Upstream request timeout in seconds (0 to disable)"
|
|
53628
|
+
description: "Upstream request timeout in seconds (0 to disable). Enforced as a total-duration limit; both runtimes additionally apply their own ~300s idle timeout to fetch, which a steadily streaming response does not trip."
|
|
53551
53629
|
},
|
|
53552
53630
|
"upstream-queue-concurrency": {
|
|
53553
53631
|
type: "string",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ghc-proxy",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.1",
|
|
5
5
|
"description": "GitHub Copilot to OpenAI/Anthropic API proxy - Use Copilot with Claude Code, Cursor, and more",
|
|
6
6
|
"author": "wxxb789 <wxxb789@outlook.com>",
|
|
7
7
|
"homepage": "https://github.com/wxxb789/ghc-proxy",
|