ghc-proxy 0.10.0 → 0.10.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.
Files changed (3) hide show
  1. package/README.md +59 -37
  2. package/dist/main.mjs +344 -145
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/ghc-proxy)](https://www.npmjs.com/package/ghc-proxy)
4
4
  [![CI](https://github.com/wxxb789/ghc-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/wxxb789/ghc-proxy/actions/workflows/ci.yml)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/wxxb789/ghc-proxy/blob/master/LICENSE)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/wxxb789/ghc-proxy/blob/main/LICENSE)
6
6
 
7
7
  A proxy that turns your GitHub Copilot subscription into an OpenAI and Anthropic compatible API. Use it to power [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview), [Cursor](https://www.cursor.com/), or any tool that speaks the OpenAI Chat Completions, OpenAI Responses, or Anthropic Messages protocol.
8
8
 
@@ -107,13 +107,13 @@ bunx ghc-proxy@latest selfcheck # Probe tokenizer chunks and Bun/Node runti
107
107
 
108
108
  | Option | Alias | Default | Description |
109
109
  |--------|-------|---------|-------------|
110
- | `--port` | `-p` | `4141` | Port to listen on |
110
+ | `--port` | `-p` | `4141` | Port to listen on (`1..65535`; malformed or zero values fail before startup) |
111
111
  | `--verbose` | `-v` | `false` | Enable verbose logging |
112
112
  | `--account-type` | `-a` | `individual` | `individual`, `business`, or `enterprise` |
113
113
  | `--rate-limit` | `-r` | -- | Minimum seconds between requests |
114
114
  | `--wait` | `-w` | `false` | Queue requests instead of rejecting with 429 when `--rate-limit` cooldown has not elapsed (requires `--rate-limit`) |
115
115
  | `--manual` | -- | `false` | Manually approve each request |
116
- | `--github-token` | `-g` | -- | Pass a GitHub token directly (from `auth`) |
116
+ | `--github-token` | `-g` | -- | Use a GitHub token for this process only (normally obtained with `auth`); this flag does not persist it to `config.json` |
117
117
  | `--claude-code` | `-c` | `false` | Generate a Claude Code launch command |
118
118
  | `--show-token` | -- | `false` | Display tokens on auth and refresh |
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`. |
@@ -189,30 +189,31 @@ All fields are optional. The full schema:
189
189
 
190
190
  | Field | Type | Default | Description |
191
191
  |-------|------|---------|-------------|
192
- | `githubToken` | `string` | -- | Persisted GitHub token. Normally written automatically by `auth` / `--github-token`; you rarely set this by hand |
193
- | `modelRewrites` | `{ from, to }[]` | -- | Glob-pattern model substitution rules (see [Model Rewrites](#model-rewrites)) |
194
- | `modelFallback` | `object` | -- | Override default model fallbacks (see [Customizing Fallbacks](#customizing-fallbacks)) |
192
+ | `githubToken` | `string` | unset | Persisted GitHub token. The device-code flow (`auth` or first startup) writes it automatically; `start --github-token` is runtime-only and does not write this field |
193
+ | `modelRewrites` | `{ from, to }[]` | `[]` | Glob-pattern model substitution rules (see [Model Rewrites](#model-rewrites)) |
194
+ | `modelFallback` | `object` | built-in family defaults | Override default model fallbacks (see [Customizing Fallbacks](#customizing-fallbacks)) |
195
195
  | `modelFallback.claudeOpus` | `string` | `claude-opus-5` | Fallback for `claude-opus-*` models |
196
196
  | `modelFallback.claudeSonnet` | `string` | `claude-sonnet-5` | Fallback for `claude-sonnet-*` models |
197
197
  | `modelFallback.claudeHaiku` | `string` | `claude-haiku-4.5` | Fallback for `claude-haiku-*` models |
198
- | `smallModel` | `string` | -- | Target model for compact request routing (see [Small-Model Routing](#small-model-routing)) |
198
+ | `smallModel` | `string` | unset | Target model for compact request routing (see [Small-Model Routing](#small-model-routing)) |
199
199
  | `compactUseSmallModel` | `boolean` | `false` | Route compact/summarization requests to `smallModel` |
200
200
  | `useFunctionApplyPatch` | `boolean` | `true` | Rewrite `apply_patch` custom tool as function tool on Responses path |
201
201
  | `responsesApiAutoCompactInput` | `boolean` | `false` | Automatically trim Responses `input` to the latest `compaction` item |
202
202
  | `responsesApiAutoContextManagement` | `boolean` | `false` | Automatically inject Responses `context_management` for selected models |
203
- | `responsesApiContextManagementModels` | `string[]` | -- | Models eligible for auto-injected Responses `context_management` |
204
- | `responsesApiParameterFilters` | `{ models, params }[]` | -- | Extra rules to strip request parameters on the Responses boundary (see [Responses Parameter Filters](#responses-parameter-filters)) |
205
- | `responsesApiParameterFiltersReplaceDefault` | `boolean` | `false` | Disable the built-in reasoning-model default rule so only your `responsesApiParameterFilters` apply |
206
- | `responsesOfficialEmulator` | `boolean` | `false` | Enable local OpenAI-style Responses state emulation for `previous_response_id`, `conversation`, retrieve, input_items, delete, and input_tokens |
207
- | `responsesOfficialEmulatorTtlSeconds` | `number` | `14400` | In-memory TTL for locally emulated Responses state |
208
- | `modelReasoningEfforts` | `Record<string, string>` | -- | Per-model reasoning effort defaults for Anthropic-to-Responses translation. Each value must be one of `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` (ascending) |
203
+ | `responsesApiContextManagementModels` | `string[]` | `[]` | Models eligible for auto-injected Responses `context_management` |
204
+ | `responsesApiParameterFilters` | `{ models, params }[]` | `[]` | Extra rules to strip request parameters on the Responses boundary; the built-in reasoning-model rule remains active unless replaced (see [Responses Parameter Filters](#responses-parameter-filters)) |
205
+ | `responsesApiParameterFiltersReplaceDefault` | `boolean` | `false` | Disable the built-in reasoning-model default rule so only your `responsesApiParameterFilters` apply |
206
+ | `chatCompletionsUseMaxCompletionTokens` | `string[]` | `[]` | Extra model globs that rename Chat Completions `max_tokens` to `max_completion_tokens`; adds to the built-in `gpt-5.4` / `gpt-5.4-*` rules |
207
+ | `responsesOfficialEmulator` | `boolean` | `false` | Enable local OpenAI-style Responses state emulation for `previous_response_id`, `conversation`, retrieve, input_items, delete, and input_tokens |
208
+ | `responsesOfficialEmulatorTtlSeconds` | `number` | `14400` | In-memory TTL for locally emulated Responses state |
209
+ | `modelReasoningEfforts` | `Record<string, string>` | `{}`; unlisted models use `high` | Per-model reasoning effort defaults for Anthropic-to-Responses translation. Each value must be one of `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` (ascending) |
209
210
  | `upstreamQueueConcurrency` | `number` | `10` | Maximum concurrent Copilot upstream requests |
210
211
  | `upstreamQueueMaxRetries` | `number` | `1` | Maximum retries across capacity and approved pre-connection failures (`0..2`) |
211
212
  | `upstreamRecoveryBudgetSeconds` | `number` | `60` | Shared recovery deadline after the first retryable outcome or active-cooldown encounter (`1..120` seconds) |
212
- | `overloadFallbacks` | `Record<string, string>` | -- | Exact effective-model mappings for one opt-in fallback dispatch after terminal model `529`; absent means disabled |
213
+ | `overloadFallbacks` | `Record<string, string>` | `{}` (disabled) | Exact effective-model mappings for one opt-in fallback dispatch after terminal model `529` |
213
214
  | `upstreamQueueBaseDelaySeconds` | `number` | `2` | Base delay (seconds) for upstream retry backoff when `Retry-After` is absent |
214
215
  | `upstreamQueueMaxDelaySeconds` | `number` | `60` | Maximum computed backoff (seconds); does not clamp `Retry-After` |
215
- | `gheDomain` | `string` | -- | GitHub Enterprise Cloud company domain (persisted automatically after GHE.com auth) |
216
+ | `gheDomain` | `string` | unset | GitHub Enterprise Cloud company domain (persisted automatically after GHE.com auth) |
216
217
 
217
218
  Example:
218
219
 
@@ -229,9 +230,10 @@ Example:
229
230
  "compactUseSmallModel": true,
230
231
  "useFunctionApplyPatch": true,
231
232
  "responsesApiAutoCompactInput": false,
232
- "responsesApiAutoContextManagement": false,
233
- "responsesApiContextManagementModels": ["gpt-5", "gpt-5-mini"],
234
- "responsesOfficialEmulator": false,
233
+ "responsesApiAutoContextManagement": false,
234
+ "responsesApiContextManagementModels": ["gpt-5", "gpt-5-mini"],
235
+ "chatCompletionsUseMaxCompletionTokens": [],
236
+ "responsesOfficialEmulator": false,
235
237
  "responsesOfficialEmulatorTtlSeconds": 14400,
236
238
  "modelReasoningEfforts": {
237
239
  "gpt-5": "high",
@@ -359,9 +361,11 @@ The proxy authenticates with GitHub using the [device code OAuth flow](https://d
359
361
 
360
362
  When the Copilot token response includes `endpoints.api`, `ghc-proxy` now prefers that runtime API base automatically instead of relying only on the configured account type. This keeps enterprise/business routing aligned with the endpoint GitHub actually returned for the current token.
361
363
 
362
- Incoming requests hit an [Elysia](https://elysiajs.com/) server. `chat/completions` requests are validated, normalized into the shared planning pipeline, and then forwarded to Copilot. `responses` requests use a native Responses path with explicit compatibility policies. `messages` requests are routed per-model and can use native Anthropic passthrough, the Responses translation path, or the existing chat-completions fallback. The translator tracks exact vs lossy vs unsupported behavior explicitly; see the [Messages Routing and Translation Guide](./docs/messages-routing-and-translation.md) and the [Anthropic Translation Matrix](./docs/anthropic-translation-matrix.md) for the current support surface.
363
-
364
- For Anthropic `search_result` blocks, current live probes show Copilot native `/v1/messages` accepts top-level search results and pure search-result tool outputs, but rejects top-level `citations` and mixed text/search-result tool output arrays. The native path sanitizes those known rejection cases, while translated paths flatten search results to text.
364
+ Incoming requests hit an [Elysia](https://elysiajs.com/) server. `chat/completions` requests are validated, normalized into the shared planning pipeline, and then forwarded to Copilot. `responses` requests use a native Responses path with explicit compatibility policies. `messages` requests are routed per-model and can use native Anthropic passthrough, the Responses translation path, or the existing chat-completions fallback. The translator tracks exact vs lossy vs unsupported behavior explicitly; see the [Messages Routing and Translation Guide](./docs/messages-routing-and-translation.md) and the [Anthropic Translation Matrix](./docs/anthropic-translation-matrix.md) for the current support surface.
365
+
366
+ The built-in, read-only Dashboard projects process health, model routing, behavior, and recent request lifecycle metadata without storing request or response content. See [Dashboard Observability](./docs/design/dashboard-observability.md).
367
+
368
+ For Anthropic `search_result` blocks, an April 17, 2026 probe against `claude-opus-4.6` on Copilot native `/v1/messages` accepted top-level search results and pure search-result tool outputs, but rejected top-level `citations` and mixed text/search-result tool output arrays. The native path sanitizes those observed rejection cases, while translated paths flatten search results to text; re-run the probe before treating that dated upstream result as universal.
365
369
 
366
370
  ### Request Routing
367
371
 
@@ -402,22 +406,37 @@ This keeps the existing chat pipeline stable while allowing newer Copilot models
402
406
  | `POST` | `/v1/messages` | Messages API with per-model routing across native Messages, Responses translation, or chat-completions fallback |
403
407
  | `POST` | `/v1/messages/count_tokens` | Token counting |
404
408
 
405
- **Utility:**
409
+ **Utility:**
406
410
 
407
411
  | Method | Path | Description |
408
412
  |--------|------|-------------|
409
413
  | `GET` | `/health` | Liveness/readiness probe — returns `{ status, copilotToken, modelsLoaded, version }` |
410
414
  | `GET` | `/usage` | Copilot quota / usage monitoring |
411
- | `GET` | `/token` | Inspect the current Copilot token |
412
-
413
- > **Note:** The `/v1/` prefix is optional for OpenAI-compatible endpoints (`/chat/completions`, `/responses`, `/models`, `/embeddings`). Anthropic endpoints (`/v1/messages`, `/v1/messages/count_tokens`) require the `/v1` prefix. The utility endpoints (`/health`, `/usage`, `/token`) are root-only and not exposed under `/v1`.
415
+ | `GET` | `/token` | Inspect the current Copilot token |
416
+
417
+ **Local Dashboard (read-only):**
418
+
419
+ | Method | Path | Description |
420
+ |--------|------|-------------|
421
+ | `GET` | `/dashboard` | Dashboard application |
422
+ | `GET` | `/dashboard/styles.css` | Dashboard stylesheet |
423
+ | `GET` | `/dashboard/app.js` | Dashboard client script |
424
+ | `GET` | `/dashboard/api/overview` | Process, authentication, quota, request, and queue summary |
425
+ | `GET` | `/dashboard/api/models` | Upstream model metadata and effective proxy capabilities |
426
+ | `GET` | `/dashboard/api/behavior` | Active routing, compatibility policies, strategies, and effect counters |
427
+ | `GET` | `/dashboard/api/requests` | Active requests and the most recent 256 completed request summaries |
428
+
429
+ Dashboard routes are restricted to local access and return `403` when the peer, request host, or supplied `Origin` fails the loopback/same-origin checks. They are excluded from request history and access logging. See [Dashboard Observability](./docs/design/dashboard-observability.md) for the projection and security contract.
430
+
431
+ > **Note:** The `/v1/` prefix is optional for OpenAI-compatible endpoints (`/chat/completions`, `/responses`, `/models`, `/embeddings`). Anthropic endpoints (`/v1/messages`, `/v1/messages/count_tokens`) require the `/v1` prefix. The utility and Dashboard endpoints are root-only and not exposed under `/v1`.
414
432
 
415
433
  ## Responses Compatibility
416
434
 
417
435
  `/v1/responses` is designed to stay close to the OpenAI wire format while making Copilot limitations explicit:
418
436
 
419
- - requests are validated before any mutation
420
- - common official request fields such as `conversation`, `previous_response_id`, `max_tool_calls`, `truncation`, `user`, `prompt`, and `text` are now modeled explicitly instead of relying on loose passthrough alone
437
+ - requests are validated before any mutation
438
+ - client-supplied `top_k` is rejected with `400` on the OpenAI Chat Completions and Responses boundaries because neither official OpenAI schema defines it; clients that send it by mistake receive an explicit error instead of a silent drop. Anthropic Messages `top_k` remains supported and is preserved when the proxy translates that request internally for Copilot
439
+ - common official request fields such as `conversation`, `previous_response_id`, `max_tool_calls`, `truncation`, `user`, `prompt`, and `text` are now modeled explicitly instead of relying on loose passthrough alone
421
440
  - official `text.format` options are modeled explicitly, including `text`, `json_object`, and `json_schema`
422
441
  - an opt-in `responsesOfficialEmulator` mode adds in-memory OpenAI-style state for `previous_response_id`, `conversation`, `GET /responses/{id}`, `GET /responses/{id}/input_items`, `DELETE /responses/{id}`, and `POST /responses/input_tokens`
423
442
  - emulator state is memory-only and expires after `responsesOfficialEmulatorTtlSeconds` (default `14400`, or 4 hours)
@@ -427,9 +446,9 @@ This keeps the existing chat pipeline stable while allowing newer Copilot models
427
446
  - automatic trimming of Responses `input` to the latest `compaction` item is disabled by default and only applies when `responsesApiAutoCompactInput` is `true`
428
447
  - reasoning defaults for Anthropic -> Responses translation can be tuned with `modelReasoningEfforts`
429
448
  - request parameters that a model rejects (e.g. `temperature`/`top_p` on reasoning models) are stripped on the Responses boundary rather than leaked upstream as a `400`; see [Responses Parameter Filters](#responses-parameter-filters)
430
- - built-in web search (`web_search`, `web_search_preview`, and their dated variants) is forwarded to Copilot rather than blocked; every `/responses` model probed accepts it and runs a real search, see [docs/research/responses-web-search.md](docs/research/responses-web-search.md)
431
- - external image URLs on the Responses path fail explicitly with `400`; use `file_id` or data URL image input instead
432
- - official `input_file` and `item_reference` input items are modeled explicitly and validated before forwarding
449
+ - built-in web search (`web_search`, `web_search_preview`, and their dated variants) is forwarded to Copilot rather than blocked; every `/responses` model reached by the August 4, 2026 acceptance sweep accepted the tool, while functional search execution was verified on `gpt-5.6-sol` and `gpt-5.6-terra`, see [docs/research/responses-web-search.md](docs/research/responses-web-search.md)
450
+ - external image URLs on the Responses path fail explicitly with `400`; use `file_id` or data URL image input instead
451
+ - official `input_file` and `item_reference` input items are modeled explicitly and validated, but the verified Copilot GPT Responses boundary is stateless: it rejects `store: true` and cannot resolve returned item IDs on later requests. The proxy deliberately applies a proxy-wide `store: false` policy, removes all `item_reference` items before dispatch, and removes `function_call_output` items whose `call_id` has no matching `function_call` in the same input array. Without the optional emulator, a caller that requested storage still receives a successful stateless response; retrieve/delete/continuation semantics are available only from the local emulator
433
452
 
434
453
  Example opt-in configuration for these two Responses-specific policies:
435
454
 
@@ -451,7 +470,7 @@ Some Copilot models reject request parameters that the OpenAI wire format allows
451
470
 
452
471
  This is expressed as a small rule engine that runs on both the native `/v1/responses` path and the `/v1/messages` → Responses translation path:
453
472
 
454
- - **Built-in default rule:** any model that advertises `reasoning_effort` has `temperature` and `top_p` stripped. This covers the whole reasoning family (including future point releases like `gpt-5.4-mini`) with no configuration.
473
+ - **Built-in default rule:** any model that advertises `reasoning_effort` has `temperature` stripped. It also has `top_p` stripped except for `*-codex` / `*-codex-*` models, which are exempt because the July 26, 2026 probe found the tested Codex model accepted `top_p` while its reasoning-model siblings rejected it. This exemption narrows only the built-in rule; an operator rule can still strip `top_p`.
455
474
  - **`responsesApiParameterFilters`:** add your own rules. Each rule is `{ "models": [glob, ...], "params": [name, ...] }`; every rule whose `models` glob matches the resolved model contributes its `params`. Rules are **added** to the default (the union of parameters is stripped). Model globs use the same `*` wildcard as `modelRewrites`.
456
475
  - **`responsesApiParameterFiltersReplaceDefault`:** set to `true` to disable the built-in reasoning-model rule, so only your `responsesApiParameterFilters` apply — use this to fully **overwrite** the default behavior.
457
476
 
@@ -461,7 +480,7 @@ Stripped parameters are removed entirely (never sent as `null`), because upstrea
461
480
  {
462
481
  "responsesApiParameterFilters": [
463
482
  { "models": ["gpt-5*", "o1*"], "params": ["temperature", "top_p"] },
464
- { "models": ["some-model"], "params": ["top_k"] }
483
+ { "models": ["some-model"], "params": ["service_tier"] }
465
484
  ],
466
485
  "responsesApiParameterFiltersReplaceDefault": false
467
486
  }
@@ -516,16 +535,19 @@ services:
516
535
  ```bash
517
536
  git clone https://github.com/wxxb789/ghc-proxy.git
518
537
  cd ghc-proxy
519
- bun install
520
- bun run dev
538
+ bun install
539
+ bun run dev # Start with --watch
540
+ # Or use the production-style source command:
541
+ bun run start
521
542
  ```
522
543
 
523
544
  ## Development
524
545
 
525
546
  ```bash
526
- bun install # Install dependencies
527
- bun run dev # Start with --watch
528
- bun run build # Build with tsdown
547
+ bun install # Install dependencies
548
+ bun run dev # Start with --watch
549
+ bun run start # Start without --watch
550
+ bun run build # Build with tsdown
529
551
  bun run lint # ESLint
530
552
  bun run typecheck # tsc --noEmit
531
553
  bun test # Run tests
package/dist/main.mjs CHANGED
@@ -6735,14 +6735,27 @@ var FixedRing = class {
6735
6735
  }
6736
6736
  return values;
6737
6737
  }
6738
+ updateFirst(predicate, update) {
6739
+ for (let offset = 0; offset < this.size; offset++) {
6740
+ const index = (this.next - 1 - offset + this.entries.length) % this.entries.length;
6741
+ const value = this.entries[index];
6742
+ if (value !== void 0 && predicate(value)) {
6743
+ this.entries[index] = update(value);
6744
+ return true;
6745
+ }
6746
+ }
6747
+ return false;
6748
+ }
6738
6749
  };
6739
6750
  var RequestActivityStore = class {
6740
6751
  active = /* @__PURE__ */ new Map();
6741
6752
  recent = new FixedRing(256);
6742
6753
  effectCounts = {};
6754
+ effectBuffers = /* @__PURE__ */ new Map();
6743
6755
  started = 0;
6744
6756
  completed = 0;
6745
6757
  failed = 0;
6758
+ aborted = 0;
6746
6759
  now;
6747
6760
  constructor(now = Date.now) {
6748
6761
  this.now = now;
@@ -6787,11 +6800,28 @@ var RequestActivityStore = class {
6787
6800
  const request = this.active.get(requestId);
6788
6801
  if (request) request.selectedStrategy = sanitizeTag(strategy);
6789
6802
  }
6803
+ beginEffectBuffer(requestId) {
6804
+ if (this.active.has(requestId) && !this.effectBuffers.has(requestId)) this.effectBuffers.set(requestId, {});
6805
+ }
6806
+ commitEffectBuffer(requestId) {
6807
+ const buffer = this.effectBuffers.get(requestId);
6808
+ if (!buffer) return;
6809
+ this.effectBuffers.delete(requestId);
6810
+ for (const [effect, count] of Object.entries(buffer)) if (count) this.recordEffect(requestId, effect, count);
6811
+ }
6812
+ discardEffectBuffer(requestId) {
6813
+ this.effectBuffers.delete(requestId);
6814
+ }
6790
6815
  recordEffect(requestId, effect, count = 1) {
6791
6816
  if (!Number.isFinite(count) || count <= 0) return;
6792
6817
  const request = this.active.get(requestId);
6793
6818
  if (!request) return;
6794
6819
  const normalizedCount = Math.floor(count);
6820
+ const buffer = this.effectBuffers.get(requestId);
6821
+ if (buffer) {
6822
+ buffer[effect] = (buffer[effect] ?? 0) + normalizedCount;
6823
+ return;
6824
+ }
6795
6825
  this.effectCounts[effect] = (this.effectCounts[effect] ?? 0) + normalizedCount;
6796
6826
  request.effectCounts[effect] = (request.effectCounts[effect] ?? 0) + normalizedCount;
6797
6827
  }
@@ -6801,16 +6831,33 @@ var RequestActivityStore = class {
6801
6831
  }
6802
6832
  markStreaming(requestId) {
6803
6833
  const request = this.active.get(requestId);
6804
- if (request) request.state = "streaming";
6834
+ if (request && request.state !== "aborted") request.state = "streaming";
6835
+ }
6836
+ markAborted(requestId) {
6837
+ const request = this.active.get(requestId);
6838
+ if (request) {
6839
+ request.state = "aborted";
6840
+ return;
6841
+ }
6842
+ if (this.recent.updateFirst((request) => request.requestId === requestId && request.state === "completed", (request) => ({
6843
+ ...request,
6844
+ state: "aborted"
6845
+ }))) {
6846
+ this.completed--;
6847
+ this.aborted++;
6848
+ }
6805
6849
  }
6806
6850
  complete(requestId, status) {
6807
6851
  const request = this.active.get(requestId);
6808
6852
  if (!request) return false;
6809
6853
  this.active.delete(requestId);
6810
- const failed = status >= 400 || request.errorSummary !== void 0;
6811
- const completedRequest = projectRequest(request, Math.max(0, this.now() - request.startedAtMs), failed ? "failed" : "completed", status);
6854
+ this.effectBuffers.delete(requestId);
6855
+ const aborted = request.state === "aborted" && request.errorSummary === void 0;
6856
+ const failed = !aborted && (status >= 400 || request.errorSummary !== void 0);
6857
+ const completedRequest = projectRequest(request, Math.max(0, this.now() - request.startedAtMs), aborted ? "aborted" : failed ? "failed" : "completed", status);
6812
6858
  this.recent.push(completedRequest);
6813
- this.completed++;
6859
+ if (aborted) this.aborted++;
6860
+ else this.completed++;
6814
6861
  if (failed) this.failed++;
6815
6862
  return true;
6816
6863
  }
@@ -6822,7 +6869,8 @@ var RequestActivityStore = class {
6822
6869
  totals: {
6823
6870
  started: this.started,
6824
6871
  completed: this.completed,
6825
- failed: this.failed
6872
+ failed: this.failed,
6873
+ aborted: this.aborted
6826
6874
  },
6827
6875
  effectCounts: { ...this.effectCounts }
6828
6876
  };
@@ -6834,18 +6882,21 @@ var RequestActivityStore = class {
6834
6882
  totals: {
6835
6883
  started: this.started,
6836
6884
  completed: this.completed,
6837
- failed: this.failed
6885
+ failed: this.failed,
6886
+ aborted: this.aborted
6838
6887
  },
6839
6888
  effectCounts: { ...this.effectCounts }
6840
6889
  };
6841
6890
  }
6842
6891
  reset() {
6843
6892
  this.active.clear();
6893
+ this.effectBuffers.clear();
6844
6894
  this.recent = new FixedRing(256);
6845
6895
  for (const key of Object.keys(this.effectCounts)) delete this.effectCounts[key];
6846
6896
  this.started = 0;
6847
6897
  this.completed = 0;
6848
6898
  this.failed = 0;
6899
+ this.aborted = 0;
6849
6900
  }
6850
6901
  };
6851
6902
  function classifyObservedEndpoint(rawUrl) {
@@ -6920,14 +6971,21 @@ function readSafeErrorCategory(error) {
6920
6971
  const body = "body" in error ? error.body : void 0;
6921
6972
  if (typeof body !== "object" || body === null) return void 0;
6922
6973
  const nested = "error" in body ? body.error : void 0;
6923
- let candidate;
6924
- if (typeof nested === "object" && nested !== null) candidate = "code" in nested ? nested.code : "type" in nested ? nested.type : void 0;
6925
- else if ("type" in body) candidate = body.type;
6926
- return typeof candidate === "string" && OBSERVED_ERROR_CATEGORIES.has(candidate) ? candidate : void 0;
6974
+ if (typeof nested === "object" && nested !== null) {
6975
+ const code = "code" in nested ? nested.code : void 0;
6976
+ if (isObservedErrorCategory(code)) return code;
6977
+ const type = "type" in nested ? nested.type : void 0;
6978
+ return isObservedErrorCategory(type) ? type : void 0;
6979
+ }
6980
+ const type = "type" in body ? body.type : void 0;
6981
+ return isObservedErrorCategory(type) ? type : void 0;
6927
6982
  } catch {
6928
6983
  return;
6929
6984
  }
6930
6985
  }
6986
+ function isObservedErrorCategory(value) {
6987
+ return typeof value === "string" && OBSERVED_ERROR_CATEGORIES.has(value);
6988
+ }
6931
6989
  //#endregion
6932
6990
  //#region src/state/runtime.ts
6933
6991
  var RuntimeStore = class {
@@ -7496,7 +7554,10 @@ var UpstreamRequestQueue = class {
7496
7554
  let response;
7497
7555
  try {
7498
7556
  this.throwIfRecoveryExpired(recovery, lastConnectionError);
7499
- if (context.fallbackAttempt) recovery.fallbackFetchStarted = true;
7557
+ if (context.fallbackAttempt) {
7558
+ recovery.fallbackFetchStarted = true;
7559
+ recovery.onFallbackFetchStart?.();
7560
+ }
7500
7561
  response = await this.fetchBeforeDeadline(fetcher, signal, recovery, lastConnectionError);
7501
7562
  lastConnectionError = void 0;
7502
7563
  } catch (error) {
@@ -8745,7 +8806,7 @@ const checkUsage = defineCommand({
8745
8806
  });
8746
8807
  //#endregion
8747
8808
  //#region src/util/version.ts
8748
- const VERSION = "0.10.0";
8809
+ const VERSION = "0.10.1";
8749
8810
  //#endregion
8750
8811
  //#region src/debug.ts
8751
8812
  function getRuntimeInfo() {
@@ -48660,8 +48721,8 @@ async function getTokenCount(payload, model) {
48660
48721
  output: outputTokens
48661
48722
  };
48662
48723
  }
48663
- async function estimateResponsesInputTokens(inputItems, model) {
48664
- return (await getEncoder(getTokenizerFromModel(model))).encode(JSON.stringify(inputItems)).length;
48724
+ async function estimateSerializedTokens(value, model) {
48725
+ return (await getEncoder(getTokenizerFromModel(model))).encode(JSON.stringify(value)).length;
48665
48726
  }
48666
48727
  //#endregion
48667
48728
  //#region \0@oxc-project+runtime@0.134.0/helpers/esm/taggedTemplateLiteral.js
@@ -48671,7 +48732,7 @@ function _taggedTemplateLiteral(e, t) {
48671
48732
  //#endregion
48672
48733
  //#region src/routes/dashboard/assets.ts
48673
48734
  var _templateObject;
48674
- const DASHBOARD_HTML = String.raw(_templateObject || (_templateObject = _taggedTemplateLiteral(["<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>ghc-proxy Dashboard</title>\n <link rel=\"icon\" href=\"data:,\">\n <link rel=\"stylesheet\" href=\"/dashboard/styles.css\">\n</head>\n<body>\n <header class=\"app-header\">\n <div class=\"brand-block\">\n <div class=\"brand\">ghc-proxy</div>\n <div class=\"status-line\">\n <span id=\"health-dot\" class=\"status-dot unknown\"></span>\n <span id=\"health-label\">Connecting</span>\n <span id=\"version-label\"></span>\n </div>\n </div>\n <nav class=\"tabs\" aria-label=\"Dashboard views\">\n <button type=\"button\" class=\"tab active\" data-tab=\"overview\">Overview</button>\n <button type=\"button\" class=\"tab\" data-tab=\"models\">Models</button>\n <button type=\"button\" class=\"tab\" data-tab=\"behavior\">Behavior</button>\n <button type=\"button\" class=\"tab\" data-tab=\"requests\">Requests</button>\n </nav>\n <div class=\"header-actions\">\n <label class=\"toggle\"><input id=\"live-refresh\" type=\"checkbox\" checked> Live</label>\n <label class=\"theme-toggle\">\n <input id=\"theme-toggle\" type=\"checkbox\" role=\"switch\" aria-label=\"Use dark theme\">\n <span id=\"theme-label\">Light</span>\n </label>\n <button id=\"refresh-button\" type=\"button\" class=\"command\">Refresh</button>\n </div>\n </header>\n\n <main>\n <section id=\"view-overview\" class=\"view active\" data-view=\"overview\">\n <div class=\"metric-strip\" aria-label=\"Runtime summary\">\n <div><span class=\"metric-label\">Uptime</span><strong id=\"metric-uptime\">-</strong></div>\n <div><span class=\"metric-label\">Active</span><strong id=\"metric-active\">0</strong></div>\n <div><span class=\"metric-label\">Completed</span><strong id=\"metric-completed\">0</strong></div>\n <div><span class=\"metric-label\">Failed</span><strong id=\"metric-failed\">0</strong></div>\n <div><span class=\"metric-label\">Queue</span><strong id=\"metric-queue\">0 / 0</strong></div>\n </div>\n\n <div class=\"overview-grid\">\n <section class=\"panel\">\n <div class=\"section-heading\"><h1>Runtime</h1><span id=\"overview-updated\" class=\"muted\"></span></div>\n <dl id=\"runtime-details\" class=\"kv-list\"></dl>\n </section>\n <section class=\"panel\">\n <div class=\"section-heading\"><h1>Authentication</h1></div>\n <div class=\"table-scroll\"><table><thead><tr><th>Service</th><th>Status</th><th>Identity</th><th>Last check</th></tr></thead><tbody id=\"auth-body\"></tbody></table></div>\n </section>\n </div>\n\n <section class=\"panel full-width\">\n <div class=\"section-heading\"><h1>Quota</h1><span id=\"quota-status\" class=\"muted\"></span></div>\n <div class=\"table-scroll\"><table><thead><tr><th>Pool</th><th>Remaining</th><th>Entitlement</th><th>Percent</th><th>Overage</th></tr></thead><tbody id=\"quota-body\"></tbody></table></div>\n </section>\n </section>\n\n <section id=\"view-models\" class=\"view\" data-view=\"models\" hidden>\n <div class=\"toolbar model-toolbar\">\n <div><h1>Models</h1><span id=\"model-count\" class=\"muted\"></span></div>\n <div class=\"model-controls\">\n <label class=\"toggle\"><input id=\"model-group-vendor\" type=\"checkbox\" checked> Group vendor</label>\n <select id=\"model-sort\" aria-label=\"Order models by name\">\n <option value=\"asc\">Name A-Z</option>\n <option value=\"desc\">Name Z-A</option>\n </select>\n <input id=\"model-filter\" type=\"search\" placeholder=\"Filter models\" autocomplete=\"off\">\n <button id=\"copy-models\" type=\"button\" class=\"command\" disabled>Copy selected (0)</button>\n </div>\n </div>\n <div id=\"model-copy-status\" class=\"muted model-copy-status\" role=\"status\" aria-live=\"polite\"></div>\n <div class=\"table-scroll model-table\"><table><thead><tr><th class=\"model-select-header\" aria-label=\"Select model\"></th><th>Model</th><th>Vendor</th><th>Messages route</th><th>Upstream endpoints</th><th>Limits</th><th>Capabilities</th><th>Proxy compatibility</th></tr></thead><tbody id=\"models-body\"></tbody></table></div>\n </section>\n\n <section id=\"view-behavior\" class=\"view\" data-view=\"behavior\" hidden>\n <div class=\"toolbar\"><div><h1>Behavior</h1><span class=\"muted\">Current process configuration</span></div></div>\n <div class=\"behavior-grid\">\n <section class=\"panel\"><div class=\"section-heading\"><h2>Model routing</h2></div><dl id=\"behavior-routing\" class=\"kv-list\"></dl></section>\n <section class=\"panel\"><div class=\"section-heading\"><h2>Parameters and context</h2></div><dl id=\"behavior-parameters\" class=\"kv-list\"></dl></section>\n </div>\n <section class=\"panel full-width\">\n <div class=\"section-heading\"><h2>Effects since startup</h2></div>\n <div class=\"table-scroll\"><table><thead><tr><th>Category</th><th>Effect</th><th>Count</th></tr></thead><tbody id=\"effects-body\"></tbody></table></div>\n </section>\n </section>\n\n <section id=\"view-requests\" class=\"view requests-view\" data-view=\"requests\" hidden>\n <div class=\"toolbar\"><div><h1>Requests</h1><span id=\"request-count\" class=\"muted\"></span></div><span class=\"muted\">256 completed max</span></div>\n <div class=\"request-layout\">\n <div class=\"request-list table-scroll\"><table><thead><tr><th>State</th><th>Endpoint</th><th>Model</th><th>Strategy</th><th>Status</th><th>Duration</th><th>Started</th></tr></thead><tbody id=\"requests-body\"></tbody></table></div>\n <aside class=\"request-detail\" aria-label=\"Selected request details\">\n <div class=\"section-heading\"><h2>Request detail</h2></div>\n <pre id=\"request-detail\">No request selected</pre>\n </aside>\n </div>\n </section>\n </main>\n\n <div id=\"error-banner\" class=\"error-banner\" role=\"status\" hidden></div>\n <script src=\"/dashboard/app.js\" defer><\/script>\n</body>\n</html>"])));
48735
+ const DASHBOARD_HTML = String.raw(_templateObject || (_templateObject = _taggedTemplateLiteral(["<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>ghc-proxy Dashboard</title>\n <link rel=\"icon\" href=\"data:,\">\n <link rel=\"stylesheet\" href=\"/dashboard/styles.css\">\n</head>\n<body>\n <header class=\"app-header\">\n <div class=\"brand-block\">\n <div class=\"brand\">ghc-proxy</div>\n <div class=\"status-line\">\n <span id=\"health-dot\" class=\"status-dot unknown\"></span>\n <span id=\"health-label\">Connecting</span>\n <span id=\"version-label\"></span>\n </div>\n </div>\n <nav class=\"tabs\" aria-label=\"Dashboard views\">\n <button type=\"button\" class=\"tab active\" data-tab=\"overview\">Overview</button>\n <button type=\"button\" class=\"tab\" data-tab=\"models\">Models</button>\n <button type=\"button\" class=\"tab\" data-tab=\"behavior\">Behavior</button>\n <button type=\"button\" class=\"tab\" data-tab=\"requests\">Requests</button>\n </nav>\n <div class=\"header-actions\">\n <label class=\"toggle\"><input id=\"live-refresh\" type=\"checkbox\" checked> Live</label>\n <label class=\"theme-toggle\">\n <input id=\"theme-toggle\" type=\"checkbox\" role=\"switch\" aria-label=\"Use dark theme\">\n <span id=\"theme-label\">Light</span>\n </label>\n <button id=\"refresh-button\" type=\"button\" class=\"command\">Refresh</button>\n </div>\n </header>\n\n <main>\n <section id=\"view-overview\" class=\"view active\" data-view=\"overview\">\n <div class=\"metric-strip\" aria-label=\"Runtime summary\">\n <div><span class=\"metric-label\">Uptime</span><strong id=\"metric-uptime\">-</strong></div>\n <div><span class=\"metric-label\">Active</span><strong id=\"metric-active\">0</strong></div>\n <div><span class=\"metric-label\">Completed</span><strong id=\"metric-completed\">0</strong></div>\n <div><span class=\"metric-label\">Failed</span><strong id=\"metric-failed\">0</strong></div>\n <div><span class=\"metric-label\">Aborted</span><strong id=\"metric-aborted\">0</strong></div>\n <div><span class=\"metric-label\">Queue</span><strong id=\"metric-queue\">0 / 0</strong></div>\n </div>\n\n <div class=\"overview-grid\">\n <section class=\"panel\">\n <div class=\"section-heading\"><h1>Runtime</h1><span id=\"overview-updated\" class=\"muted\"></span></div>\n <dl id=\"runtime-details\" class=\"kv-list\"></dl>\n </section>\n <section class=\"panel\">\n <div class=\"section-heading\"><h1>Authentication</h1></div>\n <div class=\"table-scroll\"><table><thead><tr><th>Service</th><th>Status</th><th>Identity</th><th>Last check</th></tr></thead><tbody id=\"auth-body\"></tbody></table></div>\n </section>\n </div>\n\n <section class=\"panel full-width\">\n <div class=\"section-heading\"><h1>Quota</h1><span id=\"quota-status\" class=\"muted\"></span></div>\n <div class=\"table-scroll\"><table><thead><tr><th>Pool</th><th>Remaining</th><th>Entitlement</th><th>Percent</th><th>Overage</th></tr></thead><tbody id=\"quota-body\"></tbody></table></div>\n </section>\n </section>\n\n <section id=\"view-models\" class=\"view\" data-view=\"models\" hidden>\n <div class=\"toolbar model-toolbar\">\n <div><h1>Models</h1><span id=\"model-count\" class=\"muted\"></span></div>\n <div class=\"model-controls\">\n <label class=\"toggle\"><input id=\"model-group-vendor\" type=\"checkbox\" checked> Group vendor</label>\n <select id=\"model-sort\" aria-label=\"Order models by name\">\n <option value=\"asc\">Name A-Z</option>\n <option value=\"desc\">Name Z-A</option>\n </select>\n <input id=\"model-filter\" type=\"search\" placeholder=\"Filter models\" autocomplete=\"off\">\n <button id=\"copy-models\" type=\"button\" class=\"command\" disabled>Copy selected (0)</button>\n </div>\n </div>\n <div id=\"model-copy-status\" class=\"muted model-copy-status\" role=\"status\" aria-live=\"polite\"></div>\n <div class=\"table-scroll model-table\"><table><thead><tr><th class=\"model-select-header\" aria-label=\"Select model\"></th><th>Model</th><th>Vendor</th><th>Messages route</th><th>Upstream endpoints</th><th>Limits</th><th>Capabilities</th><th>Proxy compatibility</th></tr></thead><tbody id=\"models-body\"></tbody></table></div>\n </section>\n\n <section id=\"view-behavior\" class=\"view\" data-view=\"behavior\" hidden>\n <div class=\"toolbar\"><div><h1>Behavior</h1><span class=\"muted\">Current process configuration</span></div></div>\n <div class=\"behavior-grid\">\n <section class=\"panel\"><div class=\"section-heading\"><h2>Model routing</h2></div><dl id=\"behavior-routing\" class=\"kv-list\"></dl></section>\n <section class=\"panel\"><div class=\"section-heading\"><h2>Parameters and context</h2></div><dl id=\"behavior-parameters\" class=\"kv-list\"></dl></section>\n </div>\n <section class=\"panel full-width\">\n <div class=\"section-heading\"><h2>Effects since startup</h2></div>\n <div class=\"table-scroll\"><table><thead><tr><th>Category</th><th>Effect</th><th>Count</th></tr></thead><tbody id=\"effects-body\"></tbody></table></div>\n </section>\n </section>\n\n <section id=\"view-requests\" class=\"view requests-view\" data-view=\"requests\" hidden>\n <div class=\"toolbar\"><div><h1>Requests</h1><span id=\"request-count\" class=\"muted\"></span></div><span class=\"muted\">256 finished max</span></div>\n <div class=\"request-layout\">\n <div class=\"request-list table-scroll\"><table><thead><tr><th>State</th><th>Endpoint</th><th>Model</th><th>Strategy</th><th>Status</th><th>Duration</th><th>Started</th></tr></thead><tbody id=\"requests-body\"></tbody></table></div>\n <aside class=\"request-detail\" aria-label=\"Selected request details\">\n <div class=\"section-heading\"><h2>Request detail</h2></div>\n <pre id=\"request-detail\">No request selected</pre>\n </aside>\n </div>\n </section>\n </main>\n\n <div id=\"error-banner\" class=\"error-banner\" role=\"status\" hidden></div>\n <script src=\"/dashboard/app.js\" defer><\/script>\n</body>\n</html>"])));
48675
48736
  const DASHBOARD_CSS = String.raw`:root {
48676
48737
  color-scheme: light;
48677
48738
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
@@ -48804,7 +48865,7 @@ h2 { font-size: 15px; }
48804
48865
 
48805
48866
  .metric-strip {
48806
48867
  display: grid;
48807
- grid-template-columns: repeat(5, minmax(110px, 1fr));
48868
+ grid-template-columns: repeat(6, minmax(100px, 1fr));
48808
48869
  margin-bottom: 18px;
48809
48870
  border: 1px solid var(--border);
48810
48871
  background: var(--surface);
@@ -48848,7 +48909,7 @@ tbody tr:hover td { background: var(--surface-hover); }
48848
48909
  .mono { font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; font-size: 12px; overflow-wrap: anywhere; }
48849
48910
  .badge { display: inline-flex; align-items: center; min-height: 22px; padding: 2px 7px; border: 1px solid var(--border); border-radius: 4px; background: var(--surface-subtle); color: var(--text-secondary); font-size: 11px; white-space: nowrap; }
48850
48911
  .badge.ok, .badge.completed { color: var(--ok); border-color: var(--ok-border); background: var(--ok-bg); }
48851
- .badge.degraded, .badge.streaming, .badge.in_flight { color: var(--warn); border-color: var(--warn-border); background: var(--warn-bg); }
48912
+ .badge.degraded, .badge.streaming, .badge.in_flight, .badge.aborted { color: var(--warn); border-color: var(--warn-border); background: var(--warn-bg); }
48852
48913
  .badge.failed, .badge.missing { color: var(--bad); border-color: var(--bad-border); background: var(--bad-bg); }
48853
48914
 
48854
48915
  .behavior-grid { margin-bottom: 18px; }
@@ -48870,7 +48931,7 @@ tbody tr:hover td { background: var(--surface-hover); }
48870
48931
  .metric-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
48871
48932
  .metric-strip > div { border-bottom: 1px solid var(--border); }
48872
48933
  .metric-strip > div:nth-child(2n) { border-right: 0; }
48873
- .metric-strip > div:last-child { grid-column: 1 / -1; border-bottom: 0; }
48934
+ .metric-strip > div:nth-last-child(-n + 2) { border-bottom: 0; }
48874
48935
  .overview-grid, .behavior-grid, .request-layout { grid-template-columns: 1fr; }
48875
48936
  .request-list, .request-detail pre { max-height: none; }
48876
48937
  }
@@ -49017,6 +49078,7 @@ function renderOverview(data) {
49017
49078
  byId('metric-active').textContent = formatNumber(data.activity.activeRequests);
49018
49079
  byId('metric-completed').textContent = formatNumber(data.activity.completed);
49019
49080
  byId('metric-failed').textContent = formatNumber(data.activity.failed);
49081
+ byId('metric-aborted').textContent = formatNumber(data.activity.aborted);
49020
49082
  const queue = data.activity.upstreamQueue || {};
49021
49083
  byId('metric-queue').textContent = formatNumber(queue.active) + ' / ' + formatNumber(queue.pending);
49022
49084
  byId('overview-updated').textContent = 'Updated ' + new Date().toLocaleTimeString();
@@ -49024,7 +49086,7 @@ function renderOverview(data) {
49024
49086
  const runtime = byId('runtime-details');
49025
49087
  clearNode(runtime);
49026
49088
  appendKv(runtime, 'Started', formatDate(data.startedAt));
49027
- appendKv(runtime, 'Recent completed', data.activity.recentRequests);
49089
+ appendKv(runtime, 'Recent finished', data.activity.recentRequests);
49028
49090
  appendKv(runtime, 'Upstream slots', formatNumber(queue.active) + ' / ' + formatNumber(queue.concurrency));
49029
49091
  appendKv(runtime, 'Pending queue', formatNumber(queue.pending) + ' / ' + formatNumber(queue.maxPending));
49030
49092
  appendKv(runtime, 'Cooldowns', (queue.accountCooldown ? 'account ' : '') + formatNumber(queue.modelCooldowns) + ' model');
@@ -49298,7 +49360,7 @@ function renderRequests(data) {
49298
49360
  ? dashboardState.requests[0].requestId
49299
49361
  : null;
49300
49362
  }
49301
- byId('request-count').textContent = active.length + ' active / ' + recent.length + ' completed';
49363
+ byId('request-count').textContent = active.length + ' active / ' + recent.length + ' finished';
49302
49364
  const body = byId('requests-body');
49303
49365
  clearNode(body);
49304
49366
  dashboardState.requests.forEach(function (request) {
@@ -50140,6 +50202,11 @@ function formatSearchResultBlock(block) {
50140
50202
  ].filter((part) => Boolean(part)).join("\n");
50141
50203
  }
50142
50204
  //#endregion
50205
+ //#region src/translator/anthropic/types.ts
50206
+ function isAnthropicBuiltinTool(tool) {
50207
+ return typeof tool.type === "string" && tool.type !== "custom";
50208
+ }
50209
+ //#endregion
50143
50210
  //#region src/translator/anthropic/anthropic-normalizer.ts
50144
50211
  function textBlock(text) {
50145
50212
  return {
@@ -50280,11 +50347,11 @@ function normalizeAnthropicRequest(payload) {
50280
50347
  topP: payload.top_p,
50281
50348
  topK: payload.top_k,
50282
50349
  userId: payload.metadata?.user_id,
50283
- tools: payload.tools?.map((tool) => ({
50350
+ tools: payload.tools?.flatMap((tool) => !isAnthropicBuiltinTool(tool) && tool.name !== void 0 && tool.input_schema !== void 0 ? [{
50284
50351
  name: tool.name,
50285
50352
  description: tool.description,
50286
50353
  inputSchema: tool.input_schema
50287
- })),
50354
+ }] : []),
50288
50355
  toolChoice: normalizeToolChoice(payload.tool_choice),
50289
50356
  thinking: normalizeThinking(payload.thinking),
50290
50357
  outputEffort: payload.output_config?.effort ?? void 0,
@@ -50757,11 +50824,6 @@ function toConversationTurn(turn) {
50757
50824
  };
50758
50825
  }
50759
50826
  function recordAnthropicRequestIssues(request, context) {
50760
- if (request.serviceTier !== void 0) context.record({
50761
- kind: "unsupported_service_tier",
50762
- severity: "warning",
50763
- message: "Anthropic service_tier is not supported by the upstream Copilot CAPI payload and was dropped."
50764
- }, { fatalInStrict: true });
50765
50827
  for (const turn of request.turns) {
50766
50828
  if (turn.role === "user") {
50767
50829
  for (const block of turn.blocks) if (block.kind === "tool_result" && block.isError) context.record({
@@ -50811,6 +50873,15 @@ function applyThinkingBudgetOverride(plan, request, options) {
50811
50873
  }
50812
50874
  function normalizeAnthropicConversation(payload, policy) {
50813
50875
  const context = new TranslationContext(policy);
50876
+ if (payload.service_tier !== void 0) throw new TranslationFailure("Anthropic service_tier cannot be translated to Chat Completions.", {
50877
+ status: 400,
50878
+ kind: "unsupported_service_tier"
50879
+ });
50880
+ const builtinTool = payload.tools?.find(isAnthropicBuiltinTool);
50881
+ if (builtinTool) throw new TranslationFailure(`Anthropic built-in/toolset "${builtinTool.type ?? builtinTool.name ?? "unknown"}" cannot be translated to Chat Completions.`, {
50882
+ status: 400,
50883
+ kind: "unsupported_server_tool"
50884
+ });
50814
50885
  const normalized = normalizeAnthropicRequest(payload);
50815
50886
  recordAnthropicRequestIssues(normalized, context);
50816
50887
  return {
@@ -52249,12 +52320,22 @@ function translateSystemPrompt(system) {
52249
52320
  }
52250
52321
  function convertAnthropicTools(tools) {
52251
52322
  if (!tools || tools.length === 0) return null;
52252
- return tools.map((tool) => ({
52253
- type: "function",
52254
- name: tool.name,
52255
- parameters: normalizeFunctionParametersSchemaForCopilot(tool.input_schema),
52256
- ...tool.description ? { description: tool.description } : {}
52257
- }));
52323
+ return tools.map((tool) => {
52324
+ if (isAnthropicBuiltinTool(tool)) throw new TranslationFailure(`Anthropic built-in/toolset "${tool.type ?? tool.name ?? "unknown"}" cannot be translated to the Responses API.`, {
52325
+ status: 400,
52326
+ kind: "unsupported_server_tool"
52327
+ });
52328
+ if (tool.name === void 0 || tool.input_schema === void 0) throw new TranslationFailure("Anthropic function tools require both name and input_schema.", {
52329
+ status: 400,
52330
+ kind: "invalid_tool_schema"
52331
+ });
52332
+ return {
52333
+ type: "function",
52334
+ name: tool.name,
52335
+ parameters: normalizeFunctionParametersSchemaForCopilot(tool.input_schema),
52336
+ ...tool.description ? { description: tool.description } : {}
52337
+ };
52338
+ });
52258
52339
  }
52259
52340
  function convertAnthropicToolChoice(choice) {
52260
52341
  if (!choice) return "auto";
@@ -54356,6 +54437,69 @@ var cors = (config) => {
54356
54437
  });
54357
54438
  };
54358
54439
  //#endregion
54440
+ //#region src/lib/upstream-signal.ts
54441
+ const DEFAULT_TIMEOUT_MS = 18e5;
54442
+ const CLIENT_ABORT_MARKER = "ghcProxyClientAbort";
54443
+ var ClientAbortError = class extends DOMException {
54444
+ [CLIENT_ABORT_MARKER] = true;
54445
+ constructor() {
54446
+ super("The client aborted the request.", "AbortError");
54447
+ }
54448
+ };
54449
+ function isClientAbortError(error) {
54450
+ return errorCauseChainSome(error, (candidate) => candidate[CLIENT_ABORT_MARKER] === true);
54451
+ }
54452
+ function createUpstreamDeadlineFromConfig(now = performance.now()) {
54453
+ const timeoutMs = authStore.upstreamTimeoutSeconds !== void 0 ? authStore.upstreamTimeoutSeconds * 1e3 : DEFAULT_TIMEOUT_MS;
54454
+ return timeoutMs > 0 ? now + timeoutMs : null;
54455
+ }
54456
+ function createUpstreamSignal(clientSignal, timeoutMs = DEFAULT_TIMEOUT_MS, onClientAbort) {
54457
+ const controller = new AbortController();
54458
+ const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
54459
+ const onAbort = () => {
54460
+ onClientAbort?.();
54461
+ controller.abort(new ClientAbortError());
54462
+ };
54463
+ if (clientSignal && !clientSignal.aborted) clientSignal.addEventListener("abort", onAbort);
54464
+ return {
54465
+ signal: controller.signal,
54466
+ clientSignal,
54467
+ cleanup: () => {
54468
+ if (timeout) clearTimeout(timeout);
54469
+ clientSignal?.removeEventListener("abort", onAbort);
54470
+ }
54471
+ };
54472
+ }
54473
+ /**
54474
+ * Convenience wrapper that reads the upstream timeout from runtime config.
54475
+ *
54476
+ * This signal is a *total-duration* limit. Both runtimes separately apply an
54477
+ * ~300s **idle** timeout to `fetch` — Bun's is built in, Node's is undici's
54478
+ * `headersTimeout` / `bodyTimeout` default of `300e3` — which resets on every
54479
+ * byte received. A response that keeps streaming therefore runs past 300s and
54480
+ * is bounded only by this signal; a stalled one is rejected at ~300s by the
54481
+ * runtime instead. `isTimeoutLikeError` recognizes both runtimes' shapes so
54482
+ * every path maps to a 504.
54483
+ */
54484
+ function createUpstreamSignalFromConfig(clientSignal, deadlineMonotonicMs = createUpstreamDeadlineFromConfig(), onClientAbort) {
54485
+ const remainingMs = deadlineMonotonicMs === null ? void 0 : deadlineMonotonicMs - performance.now();
54486
+ return {
54487
+ ...remainingMs !== void 0 && remainingMs <= 0 ? createExpiredUpstreamSignal(clientSignal, onClientAbort) : createUpstreamSignal(clientSignal, remainingMs ?? 0, onClientAbort),
54488
+ deadlineMonotonicMs
54489
+ };
54490
+ }
54491
+ function createExpiredUpstreamSignal(clientSignal, onClientAbort) {
54492
+ const controller = new AbortController();
54493
+ controller.abort();
54494
+ const onAbort = () => onClientAbort?.();
54495
+ if (!clientSignal.aborted) clientSignal.addEventListener("abort", onAbort);
54496
+ return {
54497
+ signal: controller.signal,
54498
+ clientSignal,
54499
+ cleanup: () => clientSignal.removeEventListener("abort", onAbort)
54500
+ };
54501
+ }
54502
+ //#endregion
54359
54503
  //#region src/deliver/index.ts
54360
54504
  function deliverResult(request, result, modelMapping) {
54361
54505
  setRequestModelMapping(request, modelMapping);
@@ -54621,10 +54765,23 @@ const anthropicMessageSchema = union([
54621
54765
  }).loose()
54622
54766
  ]);
54623
54767
  const anthropicToolSchema = object({
54624
- name: string().min(1),
54768
+ type: string().min(1).nullable().optional(),
54769
+ name: string().min(1).optional(),
54625
54770
  description: string().optional(),
54626
- input_schema: createObjectSchemaDefinitionSchema("tool input_schema must describe an object")
54627
- }).loose();
54771
+ input_schema: createObjectSchemaDefinitionSchema("tool input_schema must describe an object").optional()
54772
+ }).loose().superRefine((tool, ctx) => {
54773
+ const isBuiltinTool = isAnthropicBuiltinTool(tool);
54774
+ if (!isBuiltinTool && tool.input_schema === void 0) ctx.addIssue({
54775
+ code: "custom",
54776
+ message: "tool input_schema must describe an object",
54777
+ path: ["input_schema"]
54778
+ });
54779
+ if (!isBuiltinTool && tool.name === void 0) ctx.addIssue({
54780
+ code: "custom",
54781
+ message: "function tools require a name",
54782
+ path: ["name"]
54783
+ });
54784
+ });
54628
54785
  const anthropicToolChoiceSchema = union([
54629
54786
  object({ type: literal("auto") }).loose(),
54630
54787
  object({ type: literal("any") }).loose(),
@@ -54831,6 +54988,11 @@ const openAIChatPayloadSchema = object({
54831
54988
  reasoning_effort: _enum(REASONING_EFFORT_VALUES).nullable().optional(),
54832
54989
  thinking_budget: number().int().positive().nullable().optional()
54833
54990
  }).loose().superRefine((payload, ctx) => {
54991
+ if (Object.hasOwn(payload, "top_k")) ctx.addIssue({
54992
+ code: "custom",
54993
+ message: "top_k is not supported by the OpenAI Chat Completions API",
54994
+ path: ["top_k"]
54995
+ });
54834
54996
  const toolChoice = payload.tool_choice;
54835
54997
  if (toolChoice && typeof toolChoice === "object" && "function" in toolChoice && !payload.tools?.some((tool) => tool.function.name === toolChoice.function.name)) ctx.addIssue({
54836
54998
  code: "custom",
@@ -55135,6 +55297,11 @@ function createResponsesPayloadSchema(options) {
55135
55297
  "priority"
55136
55298
  ]).nullable().optional()
55137
55299
  }).loose().superRefine((payload, ctx) => {
55300
+ if (Object.hasOwn(payload, "top_k")) ctx.addIssue({
55301
+ code: "custom",
55302
+ message: "top_k is not supported by the OpenAI Responses API",
55303
+ path: ["top_k"]
55304
+ });
55138
55305
  if (payload.previous_response_id && payload.conversation) ctx.addIssue({
55139
55306
  code: "custom",
55140
55307
  message: "previous_response_id cannot be used together with conversation",
@@ -55266,54 +55433,6 @@ protocolRegistry.register("responses", responsesProtocol);
55266
55433
  protocolRegistry.register("responses-input-tokens", responsesInputTokensProtocol);
55267
55434
  protocolRegistry.register("embeddings", embeddingsProtocol);
55268
55435
  //#endregion
55269
- //#region src/lib/upstream-signal.ts
55270
- const DEFAULT_TIMEOUT_MS = 18e5;
55271
- function createUpstreamDeadlineFromConfig(now = performance.now()) {
55272
- const timeoutMs = authStore.upstreamTimeoutSeconds !== void 0 ? authStore.upstreamTimeoutSeconds * 1e3 : DEFAULT_TIMEOUT_MS;
55273
- return timeoutMs > 0 ? now + timeoutMs : null;
55274
- }
55275
- function createUpstreamSignal(clientSignal, timeoutMs = DEFAULT_TIMEOUT_MS) {
55276
- const controller = new AbortController();
55277
- const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
55278
- const onAbort = () => controller.abort();
55279
- if (clientSignal && !clientSignal.aborted) clientSignal.addEventListener("abort", onAbort);
55280
- return {
55281
- signal: controller.signal,
55282
- clientSignal,
55283
- cleanup: () => {
55284
- if (timeout) clearTimeout(timeout);
55285
- clientSignal?.removeEventListener("abort", onAbort);
55286
- }
55287
- };
55288
- }
55289
- /**
55290
- * Convenience wrapper that reads the upstream timeout from runtime config.
55291
- *
55292
- * This signal is a *total-duration* limit. Both runtimes separately apply an
55293
- * ~300s **idle** timeout to `fetch` — Bun's is built in, Node's is undici's
55294
- * `headersTimeout` / `bodyTimeout` default of `300e3` — which resets on every
55295
- * byte received. A response that keeps streaming therefore runs past 300s and
55296
- * is bounded only by this signal; a stalled one is rejected at ~300s by the
55297
- * runtime instead. `isTimeoutLikeError` recognizes both runtimes' shapes so
55298
- * every path maps to a 504.
55299
- */
55300
- function createUpstreamSignalFromConfig(clientSignal, deadlineMonotonicMs = createUpstreamDeadlineFromConfig()) {
55301
- const remainingMs = deadlineMonotonicMs === null ? void 0 : deadlineMonotonicMs - performance.now();
55302
- return {
55303
- ...remainingMs !== void 0 && remainingMs <= 0 ? createExpiredUpstreamSignal(clientSignal) : createUpstreamSignal(clientSignal, remainingMs ?? 0),
55304
- deadlineMonotonicMs
55305
- };
55306
- }
55307
- function createExpiredUpstreamSignal(clientSignal) {
55308
- const controller = new AbortController();
55309
- controller.abort();
55310
- return {
55311
- signal: controller.signal,
55312
- clientSignal,
55313
- cleanup: () => {}
55314
- };
55315
- }
55316
- //#endregion
55317
55436
  //#region src/pipeline/runner.ts
55318
55437
  async function runPipeline(params, config) {
55319
55438
  const upstreamDeadlineMonotonicMs = createUpstreamDeadlineFromConfig();
@@ -55329,6 +55448,7 @@ async function runPipeline(params, config) {
55329
55448
  const fallbackPossible = shouldPreservePristinePayload(config.protocol, baseSourceModel);
55330
55449
  const pristinePayload = fallbackPossible ? structuredClone(payload) : payload;
55331
55450
  const sourceAttempt = await prepareAttempt(payload, meta, params, config, recovery, upstreamDeadlineMonotonicMs, { offerLocalModelCooldown: (sourceModel) => fallbackPossible && validateFallback(config.protocol, pristinePayload, sourceModel).ok });
55451
+ sourceAttempt.commitObservability();
55332
55452
  try {
55333
55453
  return {
55334
55454
  result: await sourceAttempt.execute(),
@@ -55375,14 +55495,18 @@ async function runPipeline(params, config) {
55375
55495
  }
55376
55496
  error.recovery.retryLimit = error.recovery.retryCount;
55377
55497
  emitFallbackEvent(error.recovery, candidate.target.id, "selected", 529);
55498
+ error.recovery.onFallbackFetchStart = fallbackAttempt.commitObservability;
55378
55499
  try {
55379
55500
  const result = await fallbackAttempt.execute();
55501
+ fallbackAttempt.commitObservability();
55380
55502
  emitFallbackEvent(error.recovery, candidate.target.id, "succeeded");
55381
55503
  return {
55382
55504
  result: discloseActualModel(result, candidate.target.id),
55383
55505
  modelMapping: fallbackMapping
55384
55506
  };
55385
55507
  } catch (fallbackError) {
55508
+ if (error.recovery.fallbackFetchStarted) fallbackAttempt.commitObservability();
55509
+ else fallbackAttempt.discardObservability();
55386
55510
  if (params.signal.aborted) throw params.signal.reason;
55387
55511
  if (fallbackError instanceof FallbackCooldownError) {
55388
55512
  emitFallbackEvent(error.recovery, candidate.target.id, "target-cooldown", 529);
@@ -55394,6 +55518,8 @@ async function runPipeline(params, config) {
55394
55518
  }
55395
55519
  emitFallbackEvent(error.recovery, candidate.target.id, "target-failed", fallbackError instanceof HTTPError ? fallbackError.status : void 0, isRetryableConnectionEstablishmentError(fallbackError));
55396
55520
  throw fallbackError;
55521
+ } finally {
55522
+ delete error.recovery.onFallbackFetchStart;
55397
55523
  }
55398
55524
  }
55399
55525
  }
@@ -55406,17 +55532,24 @@ async function prepareAttempt(payload, meta, params, config, recovery, upstreamD
55406
55532
  const baseModel = resolved.model;
55407
55533
  const selectedModel = options.target ?? resolved.resolvedModel;
55408
55534
  const modelMapping = options.modelMapping ?? resolved.modelMapping;
55409
- runtimeStore.requests.recordModelMapping(params.requestId, modelMapping);
55535
+ const deferObservability = options.fallbackAttempt === true;
55536
+ if (deferObservability) runtimeStore.requests.beginEffectBuffer(params.requestId);
55537
+ else runtimeStore.requests.recordModelMapping(params.requestId, modelMapping);
55410
55538
  const recordedModelStepCount = modelMapping.steps.length;
55411
- if (options.target) payload.model = options.target.id;
55412
- if (config.afterTransform) await config.afterTransform({
55413
- payload,
55414
- meta,
55415
- headers: params.headers,
55416
- selectedModel
55417
- });
55539
+ try {
55540
+ if (options.target) payload.model = options.target.id;
55541
+ if (config.afterTransform) await config.afterTransform({
55542
+ payload,
55543
+ meta,
55544
+ headers: params.headers,
55545
+ selectedModel
55546
+ });
55547
+ } catch (error) {
55548
+ if (deferObservability) runtimeStore.requests.discardEffectBuffer(params.requestId);
55549
+ throw error;
55550
+ }
55418
55551
  params.signal.throwIfAborted();
55419
- const upstreamSignal = createUpstreamSignalFromConfig(params.signal, upstreamDeadlineMonotonicMs);
55552
+ const upstreamSignal = createUpstreamSignalFromConfig(params.signal, upstreamDeadlineMonotonicMs, () => runtimeStore.requests.markAborted(params.requestId));
55420
55553
  const copilotClient = createCopilotClient(recovery, {
55421
55554
  offerLocalModelCooldown: options.offerLocalModelCooldown,
55422
55555
  fallbackAttempt: options.fallbackAttempt
@@ -55433,12 +55566,31 @@ async function prepareAttempt(payload, meta, params, config, recovery, upstreamD
55433
55566
  recovery
55434
55567
  });
55435
55568
  const entry = config.strategyRegistry.select(selectedModel, ctx);
55436
- runtimeStore.requests.recordStrategy(params.requestId, entry.name);
55437
55569
  const strategyEffect = effectForStrategy(config.protocol, entry.name);
55438
- if (strategyEffect) runtimeStore.requests.recordEffect(params.requestId, strategyEffect);
55570
+ let observabilityCommitted = !deferObservability;
55571
+ let observabilityDiscarded = false;
55572
+ if (!deferObservability) {
55573
+ runtimeStore.requests.recordStrategy(params.requestId, entry.name);
55574
+ if (strategyEffect) runtimeStore.requests.recordEffect(params.requestId, strategyEffect);
55575
+ }
55576
+ const commitObservability = () => {
55577
+ if (observabilityCommitted || observabilityDiscarded) return;
55578
+ observabilityCommitted = true;
55579
+ runtimeStore.requests.recordModelMapping(params.requestId, modelMapping);
55580
+ runtimeStore.requests.recordStrategy(params.requestId, entry.name);
55581
+ if (strategyEffect) runtimeStore.requests.recordEffect(params.requestId, strategyEffect);
55582
+ runtimeStore.requests.commitEffectBuffer(params.requestId);
55583
+ };
55584
+ const discardObservability = () => {
55585
+ if (observabilityCommitted || observabilityDiscarded) return;
55586
+ observabilityDiscarded = true;
55587
+ runtimeStore.requests.discardEffectBuffer(params.requestId);
55588
+ };
55439
55589
  return {
55440
55590
  baseModel,
55441
55591
  modelMapping,
55592
+ commitObservability,
55593
+ discardObservability,
55442
55594
  execute: async () => {
55443
55595
  try {
55444
55596
  params.signal.throwIfAborted();
@@ -55447,12 +55599,13 @@ async function prepareAttempt(payload, meta, params, config, recovery, upstreamD
55447
55599
  upstreamSignal.cleanup();
55448
55600
  throw error;
55449
55601
  } finally {
55450
- if (modelMapping.steps.length !== recordedModelStepCount) runtimeStore.requests.recordModelMapping(params.requestId, modelMapping);
55602
+ if (observabilityCommitted && modelMapping.steps.length !== recordedModelStepCount) runtimeStore.requests.recordModelMapping(params.requestId, modelMapping);
55451
55603
  }
55452
55604
  }
55453
55605
  };
55454
55606
  } catch (error) {
55455
55607
  upstreamSignal.cleanup();
55608
+ if (deferObservability) runtimeStore.requests.discardEffectBuffer(params.requestId);
55456
55609
  throw error;
55457
55610
  }
55458
55611
  }
@@ -55725,10 +55878,10 @@ function normalizeEmbeddingRequest(payload) {
55725
55878
  * derives its own upstream signal — otherwise a client disconnect would leave
55726
55879
  * the upstream request running and the configured timeout unenforced.
55727
55880
  */
55728
- async function handleEmbeddingsCore(body, headers, client, signal, recovery) {
55881
+ async function handleEmbeddingsCore(body, headers, client, signal, recovery, onClientAbort) {
55729
55882
  const { payload } = protocolRegistry.ingest("embeddings", body, headers);
55730
55883
  const copilotClient = client ?? createCopilotClient(recovery);
55731
- const upstreamSignal = signal ? createUpstreamSignalFromConfig(signal) : void 0;
55884
+ const upstreamSignal = signal ? createUpstreamSignalFromConfig(signal, void 0, onClientAbort) : void 0;
55732
55885
  try {
55733
55886
  return await copilotClient.createEmbeddings(normalizeEmbeddingRequest(payload), { signal: upstreamSignal?.signal });
55734
55887
  } finally {
@@ -55739,7 +55892,8 @@ async function handleEmbeddingsCore(body, headers, client, signal, recovery) {
55739
55892
  //#region src/routes/embeddings/route.ts
55740
55893
  function createEmbeddingRoutes() {
55741
55894
  return new Elysia().use(requestGuardPlugin).post("/embeddings", async ({ body, request }) => {
55742
- return handleEmbeddingsCore(body, request.headers, void 0, request.signal, createRequestRecoveryRecord(request));
55895
+ const { requestId } = getOrCreateRequestCorrelation(request);
55896
+ return handleEmbeddingsCore(body, request.headers, void 0, request.signal, createRequestRecoveryRecord(request), () => runtimeStore.requests.markAborted(requestId));
55743
55897
  }, { guarded: true });
55744
55898
  }
55745
55899
  //#endregion
@@ -55754,18 +55908,42 @@ const ESTIMATION_FACTOR = {
55754
55908
  grok: 1.03,
55755
55909
  gpt: 1.1
55756
55910
  };
55911
+ const BUILTIN_TOOLSET_TOKENS = {
55912
+ browser_toolset_20260801: 7550,
55913
+ computer_toolset_20260801: 4590
55914
+ };
55757
55915
  /**
55758
55916
  * Core handler for counting tokens.
55759
55917
  */
55760
55918
  async function handleCountTokensCore({ body, headers }) {
55761
55919
  const anthropicBeta = headers.get("anthropic-beta") ?? void 0;
55762
55920
  const { payload: anthropicPayload } = protocolRegistry.ingest("anthropic-count-tokens", body, headers);
55921
+ const functionTools = [];
55922
+ const uncalibratedBuiltinTools = [];
55923
+ let calibratedBuiltinTokens = 0;
55924
+ let hasBuiltinTools = false;
55925
+ for (const tool of anthropicPayload.tools ?? []) {
55926
+ if (!isAnthropicBuiltinTool(tool)) {
55927
+ functionTools.push(tool);
55928
+ continue;
55929
+ }
55930
+ hasBuiltinTools = true;
55931
+ const builtinTokens = tool.type ? BUILTIN_TOOLSET_TOKENS[tool.type] : void 0;
55932
+ if (builtinTokens === void 0) uncalibratedBuiltinTools.push(tool);
55933
+ else calibratedBuiltinTokens += builtinTokens;
55934
+ }
55935
+ const countPayload = hasBuiltinTools ? {
55936
+ ...anthropicPayload,
55937
+ tools: functionTools
55938
+ } : anthropicPayload;
55763
55939
  const adapter = createAnthropicAdapter();
55764
- const openAIPayload = withTranslationErrors(() => adapter.toTokenCountPayload(anthropicPayload));
55765
- const tokenCount = await getTokenCount(openAIPayload, resolveModelOrThrow(openAIPayload.model));
55766
- if (anthropicPayload.tools && anthropicPayload.tools.length > 0) {
55940
+ const openAIPayload = withTranslationErrors(() => adapter.toTokenCountPayload(countPayload));
55941
+ const selectedModel = resolveModelOrThrow(openAIPayload.model);
55942
+ const tokenCount = await getTokenCount(openAIPayload, selectedModel);
55943
+ if (uncalibratedBuiltinTools.length > 0) tokenCount.input += await estimateSerializedTokens(uncalibratedBuiltinTools, selectedModel);
55944
+ if (functionTools.length > 0) {
55767
55945
  let mcpToolExist = false;
55768
- if (anthropicBeta?.startsWith("claude-code")) mcpToolExist = anthropicPayload.tools.some((tool) => tool.name.startsWith("mcp__"));
55946
+ if (anthropicBeta?.startsWith("claude-code")) mcpToolExist = functionTools.some((tool) => tool.name?.startsWith("mcp__") ?? false);
55769
55947
  if (!mcpToolExist) {
55770
55948
  const overhead = TOOL_OVERHEAD_TOKENS[inferModelFamily(anthropicPayload.model)];
55771
55949
  if (overhead) tokenCount.input = tokenCount.input + overhead;
@@ -55774,6 +55952,7 @@ async function handleCountTokensCore({ body, headers }) {
55774
55952
  let finalTokenCount = tokenCount.input + tokenCount.output;
55775
55953
  const factor = ESTIMATION_FACTOR[inferModelFamily(anthropicPayload.model)];
55776
55954
  if (factor) finalTokenCount = Math.round(finalTokenCount * factor);
55955
+ finalTokenCount += calibratedBuiltinTokens;
55777
55956
  consola.info("Token count:", finalTokenCount);
55778
55957
  return { input_tokens: finalTokenCount };
55779
55958
  }
@@ -55958,7 +56137,7 @@ function deleteStoredResponseOrThrow(responseId) {
55958
56137
  async function estimateEmulatorInputTokens(payload, selectedModel) {
55959
56138
  return {
55960
56139
  object: "response.input_tokens",
55961
- input_tokens: await estimateResponsesInputTokens(resolveEffectiveInputForInputTokens(payload), selectedModel)
56140
+ input_tokens: await estimateSerializedTokens(resolveEffectiveInputForInputTokens(payload), selectedModel)
55962
56141
  };
55963
56142
  }
55964
56143
  function resolveEffectiveInputForInputTokens(payload) {
@@ -56155,6 +56334,8 @@ async function handleResponsesCore({ body, signal, headers, requestId, callerReq
56155
56334
  const { vision, initiator } = getResponsesRequestOptions(payload);
56156
56335
  const prepared = emulatorPrepared;
56157
56336
  const requestPayload = originalPayload ?? payload;
56337
+ let effectiveInputItems;
56338
+ if (prepared?.shouldStore) effectiveInputItems = Array.isArray(payload.input) ? structuredClone(payload.input) : [];
56158
56339
  const strategyRequestId = recovery.requestId;
56159
56340
  return {
56160
56341
  requestId: strategyRequestId,
@@ -56170,7 +56351,7 @@ async function handleResponsesCore({ body, signal, headers, requestId, callerReq
56170
56351
  }, requestPayload, prepared) : void 0,
56171
56352
  onTerminalResponse(terminalResponse) {
56172
56353
  if (terminalResponse.status === "failed" || terminalResponse.error) runtimeStore.requests.recordError(strategyRequestId, "Upstream HTTP 200 (response_failed)");
56173
- if (prepared?.shouldStore) persistEmulatorResponse(terminalResponse, prepared.effectiveInputItems);
56354
+ if (effectiveInputItems) persistEmulatorResponse(terminalResponse, effectiveInputItems);
56174
56355
  },
56175
56356
  onStreamEndWithoutTerminal() {
56176
56357
  if (upstreamSignal.clientSignal?.aborted) return;
@@ -56250,7 +56431,7 @@ function applyResponsesInputPolicies(payload) {
56250
56431
  }
56251
56432
  /**
56252
56433
  * Remove input items that Copilot cannot resolve and would trigger 404:
56253
- * - `item_reference` items (opaque IDs from store=true sessions)
56434
+ * - `item_reference` items (Copilot does not resolve returned item IDs later)
56254
56435
  * - `function_call_output` items whose `call_id` has no matching prior
56255
56436
  * `function_call` in the same input array (orphaned outputs)
56256
56437
  */
@@ -56316,7 +56497,10 @@ var UpstreamResourceDispatcher = class {
56316
56497
  return this.client.getResponseInputItems(responseId, params, options);
56317
56498
  }
56318
56499
  createInputTokens(payload, options) {
56319
- return this.client.createResponseInputTokens(payload, options);
56500
+ return this.client.createResponseInputTokens({
56501
+ ...payload,
56502
+ store: false
56503
+ }, options);
56320
56504
  }
56321
56505
  delete(responseId, options) {
56322
56506
  return this.client.deleteResponse(responseId, options);
@@ -56334,33 +56518,45 @@ function createResourceDispatcher(client, recovery) {
56334
56518
  }
56335
56519
  //#endregion
56336
56520
  //#region src/routes/responses/resource-handler.ts
56337
- async function handleRetrieveResponseCore({ params, url, headers, signal, client, recovery }) {
56521
+ async function handleRetrieveResponseCore({ params, url, headers, signal, client, recovery, onClientAbort }) {
56338
56522
  const responseId = requireResponseId(params.responseId);
56339
- return await createResourceDispatcher(client, recovery).retrieve(responseId, getRetrieveParamsFromUrl(url), {
56523
+ const dispatcher = createResourceDispatcher(client, recovery);
56524
+ return withUpstreamSignal(signal, onClientAbort, (upstreamSignal) => dispatcher.retrieve(responseId, getRetrieveParamsFromUrl(url), {
56340
56525
  requestContext: readCapiRequestContext(headers),
56341
- signal
56342
- });
56526
+ signal: upstreamSignal
56527
+ }));
56343
56528
  }
56344
- async function handleListResponseInputItemsCore({ params, url, headers, signal, client, recovery }) {
56529
+ async function handleListResponseInputItemsCore({ params, url, headers, signal, client, recovery, onClientAbort }) {
56345
56530
  const responseId = requireResponseId(params.responseId);
56346
- return await createResourceDispatcher(client, recovery).listInputItems(responseId, getInputItemsParamsFromUrl(url), {
56531
+ const dispatcher = createResourceDispatcher(client, recovery);
56532
+ return withUpstreamSignal(signal, onClientAbort, (upstreamSignal) => dispatcher.listInputItems(responseId, getInputItemsParamsFromUrl(url), {
56347
56533
  requestContext: readCapiRequestContext(headers),
56348
- signal
56349
- });
56534
+ signal: upstreamSignal
56535
+ }));
56350
56536
  }
56351
- async function handleCreateResponseInputTokensCore({ body, headers, signal, client, recovery }) {
56537
+ async function handleCreateResponseInputTokensCore({ body, headers, signal, client, recovery, onClientAbort }) {
56352
56538
  const { payload, meta } = protocolRegistry.ingest("responses-input-tokens", body, headers);
56353
- return await createResourceDispatcher(client, recovery).createInputTokens(payload, {
56539
+ const dispatcher = createResourceDispatcher(client, recovery);
56540
+ return withUpstreamSignal(signal, onClientAbort, (upstreamSignal) => dispatcher.createInputTokens(payload, {
56354
56541
  requestContext: meta.requestContext,
56355
- signal
56356
- });
56542
+ signal: upstreamSignal
56543
+ }));
56357
56544
  }
56358
- async function handleDeleteResponseCore({ params, headers, signal, client, recovery }) {
56545
+ async function handleDeleteResponseCore({ params, headers, signal, client, recovery, onClientAbort }) {
56359
56546
  const responseId = requireResponseId(params.responseId);
56360
- return await createResourceDispatcher(client, recovery).delete(responseId, {
56547
+ const dispatcher = createResourceDispatcher(client, recovery);
56548
+ return withUpstreamSignal(signal, onClientAbort, (upstreamSignal) => dispatcher.delete(responseId, {
56361
56549
  requestContext: readCapiRequestContext(headers),
56362
- signal
56363
- });
56550
+ signal: upstreamSignal
56551
+ }));
56552
+ }
56553
+ async function withUpstreamSignal(clientSignal, onClientAbort, dispatch) {
56554
+ const upstreamSignal = createUpstreamSignalFromConfig(clientSignal, void 0, onClientAbort);
56555
+ try {
56556
+ return await dispatch(upstreamSignal.signal);
56557
+ } finally {
56558
+ upstreamSignal.cleanup();
56559
+ }
56364
56560
  }
56365
56561
  function requireResponseId(responseId) {
56366
56562
  if (!responseId) throwInvalidRequestError("Response id is required.", "response_id");
@@ -56432,35 +56628,43 @@ function createResponsesRoutes() {
56432
56628
  yield* delivery.stream;
56433
56629
  }, { guarded: true }).post("/responses/input_tokens", async ({ body, request, server }) => {
56434
56630
  disableIdleTimeout(server, request);
56631
+ const { requestId } = getOrCreateRequestCorrelation(request);
56435
56632
  return handleCreateResponseInputTokensCore({
56436
56633
  body,
56437
56634
  headers: request.headers,
56438
56635
  signal: request.signal,
56439
- recovery: createRequestRecoveryRecord(request)
56636
+ recovery: createRequestRecoveryRecord(request),
56637
+ onClientAbort: () => runtimeStore.requests.markAborted(requestId)
56440
56638
  });
56441
56639
  }, { guarded: true }).get("/responses/:responseId/input_items", async ({ params, request }) => {
56640
+ const { requestId } = getOrCreateRequestCorrelation(request);
56442
56641
  return handleListResponseInputItemsCore({
56443
56642
  params,
56444
56643
  url: request.url,
56445
56644
  headers: request.headers,
56446
56645
  signal: request.signal,
56447
- recovery: createRequestRecoveryRecord(request)
56646
+ recovery: createRequestRecoveryRecord(request),
56647
+ onClientAbort: () => runtimeStore.requests.markAborted(requestId)
56448
56648
  });
56449
56649
  }, { guarded: true }).get("/responses/:responseId", async ({ params, request, server }) => {
56450
56650
  if (hasStreamingResponsesQuery(request)) disableIdleTimeout(server, request);
56651
+ const { requestId } = getOrCreateRequestCorrelation(request);
56451
56652
  return handleRetrieveResponseCore({
56452
56653
  params,
56453
56654
  url: request.url,
56454
56655
  headers: request.headers,
56455
56656
  signal: request.signal,
56456
- recovery: createRequestRecoveryRecord(request)
56657
+ recovery: createRequestRecoveryRecord(request),
56658
+ onClientAbort: () => runtimeStore.requests.markAborted(requestId)
56457
56659
  });
56458
56660
  }, { guarded: true }).delete("/responses/:responseId", async ({ params, request }) => {
56661
+ const { requestId } = getOrCreateRequestCorrelation(request);
56459
56662
  return handleDeleteResponseCore({
56460
56663
  params,
56461
56664
  headers: request.headers,
56462
56665
  signal: request.signal,
56463
- recovery: createRequestRecoveryRecord(request)
56666
+ recovery: createRequestRecoveryRecord(request),
56667
+ onClientAbort: () => runtimeStore.requests.markAborted(requestId)
56464
56668
  });
56465
56669
  }, { guarded: true });
56466
56670
  }
@@ -56619,7 +56823,7 @@ function createServer$1(options) {
56619
56823
  set
56620
56824
  });
56621
56825
  const status = response?.status ?? (error instanceof HTTPError ? error.status : 500);
56622
- runtimeStore.requests.recordError(getOrCreateRequestCorrelation(request).requestId, sanitizeObservedError(error, code, status));
56826
+ if (!isClientAbortError(error)) runtimeStore.requests.recordError(getOrCreateRequestCorrelation(request).requestId, sanitizeObservedError(error, code, status));
56623
56827
  return response;
56624
56828
  }).get("/", () => "Server running").get("/health", () => ({
56625
56829
  status: "ok",
@@ -56632,6 +56836,7 @@ function createServer$1(options) {
56632
56836
  }
56633
56837
  //#endregion
56634
56838
  //#region src/start.ts
56839
+ const UNSIGNED_INTEGER_RE = /^\d+$/;
56635
56840
  async function maybeCopyClaudeCodeCommand(serverUrl) {
56636
56841
  const models = modelCache.getModels();
56637
56842
  if (!models) return;
@@ -56711,24 +56916,16 @@ async function runServer(options) {
56711
56916
  process$1.on("SIGTERM", shutdown);
56712
56917
  process$1.on("SIGINT", shutdown);
56713
56918
  }
56714
- function parseIntArg(raw, name, fallbackMsg) {
56919
+ function parseIntArg(raw, name, fallbackMsg, min = 0, max = Number.MAX_SAFE_INTEGER) {
56715
56920
  if (raw === void 0) return void 0;
56716
- const n = Number.parseInt(raw, 10);
56717
- if (Number.isNaN(n) || n < 0) {
56921
+ const normalized = raw.trim();
56922
+ const n = Number(normalized);
56923
+ if (!UNSIGNED_INTEGER_RE.test(normalized) || !Number.isSafeInteger(n) || n < min || n > max) {
56718
56924
  consola.warn(`Invalid --${name} value "${raw}". ${fallbackMsg}`);
56719
56925
  return;
56720
56926
  }
56721
56927
  return n;
56722
56928
  }
56723
- function parseBoundedIntArg(raw, name, fallbackMsg, min, max) {
56724
- if (raw === void 0) return void 0;
56725
- const value = Number(raw);
56726
- if (!raw.trim() || !Number.isInteger(value) || value < min || value > max) {
56727
- consola.warn(`Invalid --${name} value "${raw}". ${fallbackMsg}`);
56728
- return;
56729
- }
56730
- return value;
56731
- }
56732
56929
  function secondsToMs(seconds) {
56733
56930
  return seconds === void 0 ? void 0 : seconds * 1e3;
56734
56931
  }
@@ -56840,16 +57037,18 @@ const start = defineCommand({
56840
57037
  }
56841
57038
  },
56842
57039
  run({ args }) {
57040
+ const port = parseIntArg(args.port, "port", "Server not started.", 1, 65535);
57041
+ if (port === void 0) throw new Error(`Invalid --port value "${args.port}".`);
56843
57042
  const rateLimit = parseIntArg(args["rate-limit"], "rate-limit", "Rate limiting disabled.");
56844
57043
  const idleTimeoutSeconds = parseIntArg(args["idle-timeout"], "idle-timeout", "Falling back to Bun default.");
56845
- const upstreamTimeoutSeconds = parseIntArg(args["upstream-timeout"], "upstream-timeout", "Falling back to default (300s).");
56846
- const upstreamQueueConcurrency = parseIntArg(args["upstream-queue-concurrency"], "upstream-queue-concurrency", "Using default upstream queue concurrency.");
56847
- const upstreamQueueMaxRetries = parseBoundedIntArg(args["upstream-queue-retries"], "upstream-queue-retries", "Using default upstream queue retry count.", 0, 2);
56848
- const upstreamRecoveryBudgetSeconds = parseBoundedIntArg(args["upstream-recovery-budget"], "upstream-recovery-budget", "Using default upstream recovery budget.", 1, 120);
57044
+ const upstreamTimeoutSeconds = parseIntArg(args["upstream-timeout"], "upstream-timeout", "Falling back to default (1800s).");
57045
+ const upstreamQueueConcurrency = parseIntArg(args["upstream-queue-concurrency"], "upstream-queue-concurrency", "Using default upstream queue concurrency.", 1);
57046
+ const upstreamQueueMaxRetries = parseIntArg(args["upstream-queue-retries"], "upstream-queue-retries", "Using default upstream queue retry count.", 0, 2);
57047
+ const upstreamRecoveryBudgetSeconds = parseIntArg(args["upstream-recovery-budget"], "upstream-recovery-budget", "Using default upstream recovery budget.", 1, 120);
56849
57048
  const upstreamQueueBaseDelaySeconds = parseIntArg(args["upstream-queue-base-delay"], "upstream-queue-base-delay", "Using default upstream queue base delay.");
56850
57049
  const upstreamQueueMaxDelaySeconds = parseIntArg(args["upstream-queue-max-delay"], "upstream-queue-max-delay", "Using default upstream queue max delay.");
56851
57050
  return runServer({
56852
- port: Number.parseInt(args.port, 10),
57051
+ port,
56853
57052
  verbose: args.verbose,
56854
57053
  accountType: args["account-type"],
56855
57054
  manual: args.manual,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ghc-proxy",
3
3
  "type": "module",
4
- "version": "0.10.0",
4
+ "version": "0.10.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",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "scripts": {
29
29
  "build": "tsdown",
30
- "dev": "bun run --watch ./src/main.ts",
30
+ "dev": "bun run --watch ./src/main.ts start",
31
31
  "knip": "knip-bun",
32
32
  "lint": "eslint --cache",
33
33
  "lint:all": "eslint --cache .",
@@ -42,7 +42,7 @@
42
42
  "smoke:packaged": "bun run scripts/smoke/packaged-cli.ts",
43
43
  "smoke:translation": "bun run scripts/smoke/translation.ts",
44
44
  "serve": "NODE_ENV=production bun run ./src/main.ts start --wait --idle-timeout=255 --upstream-timeout=1800",
45
- "start": "NODE_ENV=production bun run ./src/main.ts",
45
+ "start": "NODE_ENV=production bun run ./src/main.ts start",
46
46
  "typecheck": "npx tsc --project tsconfig.json"
47
47
  },
48
48
  "dependencies": {