@dianshuv/copilot-api 0.8.1 → 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.
Files changed (3) hide show
  1. package/README.md +32 -29
  2. package/dist/main.mjs +982 -1169
  3. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -10,14 +10,13 @@ import { createHash, randomUUID } from "node:crypto";
10
10
  import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
11
11
  import clipboard from "clipboardy";
12
12
  import { serve } from "srvx";
13
- import invariant from "tiny-invariant";
14
13
  import { PostHog } from "posthog-node";
15
14
  import { execSync } from "node:child_process";
16
15
  import process$1 from "node:process";
17
16
  import pc from "picocolors";
18
17
  import { Hono } from "hono";
19
18
  import { cors } from "hono/cors";
20
- import { stream, streamSSE } from "hono/streaming";
19
+ import { streamSSE } from "hono/streaming";
21
20
  import { events } from "fetch-event-stream";
22
21
 
23
22
  //#region src/lib/paths.ts
@@ -125,6 +124,7 @@ const state = {
125
124
  accountType: "individual",
126
125
  manualApprove: false,
127
126
  showToken: false,
127
+ showAllModels: false,
128
128
  verbose: false,
129
129
  autoTruncate: true,
130
130
  compressToolResults: false,
@@ -1348,7 +1348,7 @@ const patchClaude = defineCommand({
1348
1348
 
1349
1349
  //#endregion
1350
1350
  //#region package.json
1351
- var version = "0.8.1";
1351
+ var version = "0.9.1";
1352
1352
 
1353
1353
  //#endregion
1354
1354
  //#region src/lib/adaptive-rate-limiter.ts
@@ -1876,6 +1876,56 @@ function createRequestContextManager(staleMaxAgeSec) {
1876
1876
  };
1877
1877
  }
1878
1878
 
1879
+ //#endregion
1880
+ //#region src/lib/hidden-models.ts
1881
+ /**
1882
+ * Hardcoded list of GitHub Copilot model ids that are hidden from listing
1883
+ * endpoints (the /v1/models response, the startup ASCII banner, and the
1884
+ * --claude-code interactive prompts), unless `--show-all-models` is passed.
1885
+ *
1886
+ * Note: this is a DISPLAY filter only. Explicit POSTs to handler endpoints
1887
+ * with a hidden id are NOT rejected — they pass through to upstream verbatim.
1888
+ *
1889
+ * Bumping the list requires a code change + release. No env var, no config
1890
+ * file, no CLI append interface.
1891
+ */
1892
+ const HIDDEN_MODEL_IDS = new Set([
1893
+ "gpt-3.5-turbo",
1894
+ "gpt-3.5-turbo-0613",
1895
+ "gpt-4",
1896
+ "gpt-4-0613",
1897
+ "gpt-4-0125-preview",
1898
+ "gpt-4o",
1899
+ "gpt-4o-mini",
1900
+ "gpt-4-o-preview",
1901
+ "gpt-4o-2024-05-13",
1902
+ "gpt-4o-2024-08-06",
1903
+ "gpt-4o-2024-11-20",
1904
+ "gpt-4o-mini-2024-07-18",
1905
+ "gpt-4.1",
1906
+ "gpt-4.1-2025-04-14",
1907
+ "gpt-41-copilot",
1908
+ "gpt-5-mini",
1909
+ "gpt-5.3-codex",
1910
+ "gpt-5.4",
1911
+ "text-embedding-ada-002",
1912
+ "text-embedding-3-small",
1913
+ "text-embedding-3-small-inference",
1914
+ "gemini-2.5-pro",
1915
+ "gemini-3-flash-preview",
1916
+ "claude-opus-4.5",
1917
+ "claude-opus-4.6",
1918
+ "claude-opus-4.7-high",
1919
+ "claude-opus-4.7-xhigh",
1920
+ "claude-sonnet-4.5",
1921
+ "mai-code-1-flash-internal",
1922
+ "trajectory-compaction"
1923
+ ]);
1924
+ function isHiddenModel(id, showAll) {
1925
+ if (showAll) return false;
1926
+ return HIDDEN_MODEL_IDS.has(id);
1927
+ }
1928
+
1879
1929
  //#endregion
1880
1930
  //#region src/lib/history-ws.ts
1881
1931
  /**
@@ -3062,127 +3112,6 @@ function removeSystemReminderTags(text) {
3062
3112
  return result;
3063
3113
  }
3064
3114
 
3065
- //#endregion
3066
- //#region src/lib/repetition-detector.ts
3067
- /**
3068
- * Stream repetition detector.
3069
- *
3070
- * Uses the KMP failure function (prefix function) to detect repeated patterns
3071
- * in streaming text output. When a model gets stuck in a repetitive loop,
3072
- * it wastes tokens producing the same content over and over. This detector
3073
- * identifies such loops early so the caller can take action (log warning,
3074
- * abort stream, etc.).
3075
- *
3076
- * The algorithm works by maintaining a sliding buffer of recent text and
3077
- * computing the longest proper prefix that is also a suffix — if this
3078
- * length exceeds `(text.length - period) >= minRepetitions * period`,
3079
- * it means a pattern of length `period` has repeated enough times.
3080
- */
3081
- const DEFAULT_CONFIG = {
3082
- minPatternLength: 10,
3083
- minRepetitions: 3,
3084
- maxBufferSize: 5e3
3085
- };
3086
- var RepetitionDetector = class {
3087
- buffer = "";
3088
- config;
3089
- detected = false;
3090
- constructor(config) {
3091
- this.config = {
3092
- ...DEFAULT_CONFIG,
3093
- ...config
3094
- };
3095
- }
3096
- /**
3097
- * Feed a text chunk into the detector.
3098
- * Returns `true` if repetition has been detected (now or previously).
3099
- * Once detected, subsequent calls return `true` without further analysis.
3100
- */
3101
- feed(text) {
3102
- if (this.detected) return true;
3103
- if (!text) return false;
3104
- this.buffer += text;
3105
- if (this.buffer.length > this.config.maxBufferSize) this.buffer = this.buffer.slice(-this.config.maxBufferSize);
3106
- const minRequired = this.config.minPatternLength * this.config.minRepetitions;
3107
- if (this.buffer.length < minRequired) return false;
3108
- this.detected = detectRepetition(this.buffer, this.config.minPatternLength, this.config.minRepetitions);
3109
- return this.detected;
3110
- }
3111
- /** Reset detector state for a new stream */
3112
- reset() {
3113
- this.buffer = "";
3114
- this.detected = false;
3115
- }
3116
- /** Whether repetition has been detected */
3117
- get isDetected() {
3118
- return this.detected;
3119
- }
3120
- };
3121
- /**
3122
- * Detect if the tail of `text` contains a repeating pattern.
3123
- *
3124
- * Uses the KMP prefix function: for a string S, the prefix function π[i]
3125
- * gives the length of the longest proper prefix of S[0..i] that is also
3126
- * a suffix. If π[n-1] ≥ (n - period) where period = n - π[n-1], then
3127
- * the string is composed of a repeating unit of length `period`.
3128
- *
3129
- * We check the suffix of the buffer (last `checkLength` chars) to detect
3130
- * if a pattern of at least `minPatternLength` chars repeats at least
3131
- * `minRepetitions` times.
3132
- */
3133
- function detectRepetition(text, minPatternLength, minRepetitions) {
3134
- const minWindow = minPatternLength * minRepetitions;
3135
- const maxWindow = Math.min(text.length, 2e3);
3136
- const windowSizes = [
3137
- minWindow,
3138
- Math.floor(maxWindow * .5),
3139
- maxWindow
3140
- ].filter((w) => w >= minWindow && w <= text.length);
3141
- for (const windowSize of windowSizes) {
3142
- const window = text.slice(-windowSize);
3143
- const period = findRepeatingPeriod(window);
3144
- if (period >= minPatternLength) {
3145
- if (Math.floor(window.length / period) >= minRepetitions) return true;
3146
- }
3147
- }
3148
- return false;
3149
- }
3150
- /**
3151
- * Find the shortest repeating period in a string using KMP prefix function.
3152
- * Returns the period length, or the string length if no repetition found.
3153
- */
3154
- function findRepeatingPeriod(s) {
3155
- const n = s.length;
3156
- if (n === 0) return 0;
3157
- const pi = new Int32Array(n);
3158
- for (let i = 1; i < n; i++) {
3159
- let j = pi[i - 1] ?? 0;
3160
- while (j > 0 && s[i] !== s[j]) j = pi[j - 1] ?? 0;
3161
- if (s[i] === s[j]) j++;
3162
- pi[i] = j;
3163
- }
3164
- const period = n - pi[n - 1];
3165
- if (period < n && n % period === 0) return period;
3166
- if (period < n && pi[n - 1] >= period) return period;
3167
- return n;
3168
- }
3169
- /**
3170
- * Create a repetition detector callback for use in stream processing.
3171
- * Returns a function that accepts text deltas and logs a warning on first detection.
3172
- */
3173
- function createStreamRepetitionChecker(label, config) {
3174
- const detector = new RepetitionDetector(config);
3175
- let warned = false;
3176
- return (textDelta) => {
3177
- const isRepetitive = detector.feed(textDelta);
3178
- if (isRepetitive && !warned) {
3179
- warned = true;
3180
- consola.warn(`[RepetitionDetector] ${label}: Repetitive output detected in stream`);
3181
- }
3182
- return isRepetitive;
3183
- };
3184
- }
3185
-
3186
3115
  //#endregion
3187
3116
  //#region src/lib/tokenizer.ts
3188
3117
  const ENCODING_MAP = {
@@ -3396,197 +3325,68 @@ const getTokenCount = async (payload, model) => {
3396
3325
  };
3397
3326
 
3398
3327
  //#endregion
3399
- //#region src/lib/anthropic/beta.ts
3400
- /**
3401
- * Vendor-neutral utilities for manipulating the `anthropic-beta` request header.
3402
- *
3403
- * Lives in `lib/anthropic/` (not in either transport module) so both the
3404
- * Anthropic-native and OpenAI-translated transport layers can share these
3405
- * helpers without introducing cross-transport imports.
3406
- */
3407
- /** Anthropic beta feature that unlocks the 1M context window. */
3408
- const CONTEXT_1M_BETA_FEATURE = "context-1m-2025-08-07";
3328
+ //#region src/lib/auto-truncate-openai.ts
3409
3329
  /**
3410
- * Merge two comma-separated anthropic-beta header values. Trims whitespace,
3411
- * drops empty tokens, and dedupes by exact string match. Returns a canonical
3412
- * comma-joined string with no spaces.
3330
+ * Auto-truncate module: Automatically truncates conversation history
3331
+ * when it exceeds token or byte limits (OpenAI format).
3413
3332
  *
3414
- * Either input may be undefined / empty.
3333
+ * Key features:
3334
+ * - Binary search for optimal truncation point
3335
+ * - Considers both token and byte limits
3336
+ * - Preserves system messages
3337
+ * - Filters orphaned tool_result and tool_use messages
3338
+ * - Dynamic byte limit adjustment on 413 errors
3339
+ * - Optional smart compression of old tool_result content
3415
3340
  */
3416
- function mergeBetaFeatures(existing, incoming) {
3417
- const seen = /* @__PURE__ */ new Set();
3418
- const out = [];
3419
- for (const raw of [existing, incoming]) {
3420
- if (!raw) continue;
3421
- for (const part of raw.split(",")) {
3422
- const f = part.trim();
3423
- if (f.length === 0 || seen.has(f)) continue;
3424
- seen.add(f);
3425
- out.push(f);
3426
- }
3341
+ /** Estimate tokens for a single message (fast approximation) */
3342
+ function estimateMessageTokens$1(msg) {
3343
+ let charCount = 0;
3344
+ if (typeof msg.content === "string") charCount = msg.content.length;
3345
+ else if (Array.isArray(msg.content)) {
3346
+ for (const part of msg.content) if (part.type === "text") charCount += part.text.length;
3347
+ else if ("image_url" in part) charCount += Math.min(part.image_url.url.length, 1e4);
3427
3348
  }
3428
- return out.join(",");
3429
- }
3430
- /**
3431
- * Append the context-1m feature to an anthropic-beta header value, deduping
3432
- * any prior occurrence. Returns the merged comma-separated string.
3433
- */
3434
- function appendContext1mBeta(existing) {
3435
- return mergeBetaFeatures(existing, CONTEXT_1M_BETA_FEATURE);
3349
+ if (msg.tool_calls) charCount += JSON.stringify(msg.tool_calls).length;
3350
+ return Math.ceil(charCount / 4) + 10;
3436
3351
  }
3437
- /**
3438
- * True iff a model id appears to be the suffixed 1M-context variant of an
3439
- * Anthropic Claude model (e.g. claude-opus-4-8-1m, claude-opus-4.6-1m).
3440
- *
3441
- * Used as a state.models-independent signal for whether to inject the
3442
- * context-1m-2025-08-07 beta header, so the 1M intent survives a stale or
3443
- * empty model cache (where `resolveAnthropicModelForDirectPath` would return
3444
- * undefined). Forwarding the beta is harmless to upstreams that ignore it.
3445
- */
3446
- function isOneMillionSuffixedClaudeId(modelId) {
3447
- return modelId.startsWith("claude-") && modelId.endsWith("-1m");
3352
+ /** Extract system/developer messages from the beginning */
3353
+ function extractSystemMessages(messages) {
3354
+ let splitIndex = 0;
3355
+ while (splitIndex < messages.length) {
3356
+ const role = messages[splitIndex].role;
3357
+ if (role !== "system" && role !== "developer") break;
3358
+ splitIndex++;
3359
+ }
3360
+ return {
3361
+ systemMessages: messages.slice(0, splitIndex),
3362
+ conversationMessages: messages.slice(splitIndex)
3363
+ };
3448
3364
  }
3449
-
3450
- //#endregion
3451
- //#region src/lib/headers.ts
3452
- /**
3453
- * Vendor-neutral header-bag helpers.
3454
- *
3455
- * HTTP header names are case-insensitive, but a plain-object header bag is
3456
- * case-sensitive on its keys. Code that wants to look up "anthropic-beta"
3457
- * without knowing whether some other producer wrote "Anthropic-Beta" needs
3458
- * `findHeaderKey`. Code that wants to set a header without creating a
3459
- * second case variant of the same name needs `setHeader`.
3460
- */
3461
- /** Case-insensitive lookup of a header key in a plain-object header bag. */
3462
- function findHeaderKey(headers, name) {
3463
- const lower = name.toLowerCase();
3464
- return Object.keys(headers).find((k) => k.toLowerCase() === lower);
3365
+ /** Get tool_use IDs from an assistant message */
3366
+ function getToolCallIds(msg) {
3367
+ if (msg.role === "assistant" && msg.tool_calls) return msg.tool_calls.map((tc) => tc.id);
3368
+ return [];
3465
3369
  }
3466
- /** Case-insensitive read of a header value. */
3467
- function getHeader(headers, name) {
3468
- const key = findHeaderKey(headers, name);
3469
- return key === void 0 ? void 0 : headers[key];
3370
+ /** Filter orphaned tool_result messages */
3371
+ function filterOrphanedToolResults$1(messages) {
3372
+ const toolUseIds = /* @__PURE__ */ new Set();
3373
+ for (const msg of messages) for (const id of getToolCallIds(msg)) toolUseIds.add(id);
3374
+ let removedCount = 0;
3375
+ const filtered = messages.filter((msg) => {
3376
+ if (msg.role === "tool" && msg.tool_call_id && !toolUseIds.has(msg.tool_call_id)) {
3377
+ removedCount++;
3378
+ return false;
3379
+ }
3380
+ return true;
3381
+ });
3382
+ if (removedCount > 0) consola.debug(`[AutoTruncate:OpenAI] Filtered ${removedCount} orphaned tool_result`);
3383
+ return filtered;
3470
3384
  }
3471
- /**
3472
- * Set a header value at the existing case variant if one is present, else at
3473
- * the supplied canonical name. Prevents a second key (different case) from
3474
- * being added for the same logical header.
3475
- */
3476
- function setHeader(headers, name, value) {
3477
- const key = findHeaderKey(headers, name) ?? name;
3478
- headers[key] = value;
3479
- }
3480
-
3481
- //#endregion
3482
- //#region src/services/copilot/create-chat-completions.ts
3483
- const GPT_MODEL_PATTERN = /^gpt-/i;
3484
- const createChatCompletions = async (payload, options) => {
3485
- if (!state.copilotToken) throw new Error("Copilot token not found");
3486
- const vendor = options?.resolvedModel?.vendor;
3487
- const isOpenAIVendor = vendor === "OpenAI" || vendor === "Azure OpenAI";
3488
- const isLikelyGPT = !options?.resolvedModel && GPT_MODEL_PATTERN.test(payload.model);
3489
- let wire = payload;
3490
- if (isOpenAIVendor || isLikelyGPT) {
3491
- const { max_tokens, max_completion_tokens, ...rest } = payload;
3492
- const effective = max_completion_tokens ?? max_tokens;
3493
- wire = {
3494
- ...rest,
3495
- ...effective !== null && effective !== void 0 && { max_completion_tokens: effective }
3496
- };
3497
- }
3498
- const enableVision = wire.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
3499
- const isAgentCall = wire.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
3500
- const modelSupportsVision = options?.resolvedModel?.capabilities?.supports?.vision !== false;
3501
- const headers = {
3502
- ...copilotHeaders(state, {
3503
- vision: enableVision && modelSupportsVision,
3504
- modelRequestHeaders: options?.resolvedModel?.request_headers,
3505
- intent: isAgentCall ? "conversation-agent" : "conversation-panel"
3506
- }),
3507
- "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
3508
- };
3509
- if (options?.anthropicBeta) {
3510
- const existingKey = findHeaderKey(headers, "anthropic-beta") ?? "anthropic-beta";
3511
- headers[existingKey] = mergeBetaFeatures(headers[existingKey], options.anthropicBeta);
3512
- consola.debug(`[ChatCompletions] anthropic-beta after merge: ${headers[existingKey]}`);
3513
- }
3514
- const response = await copilotFetch("/chat/completions", {
3515
- method: "POST",
3516
- headers,
3517
- body: JSON.stringify(wire)
3518
- });
3519
- if (!response.ok) {
3520
- consola.error("Failed to create chat completions", response);
3521
- throw await HTTPError.fromResponse("Failed to create chat completions", response, options?.errorModelIdOverride ?? payload.model);
3522
- }
3523
- if (payload.stream) return events(response);
3524
- return await response.json();
3525
- };
3526
-
3527
- //#endregion
3528
- //#region src/lib/auto-truncate-openai.ts
3529
- /**
3530
- * Auto-truncate module: Automatically truncates conversation history
3531
- * when it exceeds token or byte limits (OpenAI format).
3532
- *
3533
- * Key features:
3534
- * - Binary search for optimal truncation point
3535
- * - Considers both token and byte limits
3536
- * - Preserves system messages
3537
- * - Filters orphaned tool_result and tool_use messages
3538
- * - Dynamic byte limit adjustment on 413 errors
3539
- * - Optional smart compression of old tool_result content
3540
- */
3541
- /** Estimate tokens for a single message (fast approximation) */
3542
- function estimateMessageTokens$1(msg) {
3543
- let charCount = 0;
3544
- if (typeof msg.content === "string") charCount = msg.content.length;
3545
- else if (Array.isArray(msg.content)) {
3546
- for (const part of msg.content) if (part.type === "text") charCount += part.text.length;
3547
- else if ("image_url" in part) charCount += Math.min(part.image_url.url.length, 1e4);
3548
- }
3549
- if (msg.tool_calls) charCount += JSON.stringify(msg.tool_calls).length;
3550
- return Math.ceil(charCount / 4) + 10;
3551
- }
3552
- /** Extract system/developer messages from the beginning */
3553
- function extractSystemMessages(messages) {
3554
- let splitIndex = 0;
3555
- while (splitIndex < messages.length) {
3556
- const role = messages[splitIndex].role;
3557
- if (role !== "system" && role !== "developer") break;
3558
- splitIndex++;
3559
- }
3560
- return {
3561
- systemMessages: messages.slice(0, splitIndex),
3562
- conversationMessages: messages.slice(splitIndex)
3563
- };
3564
- }
3565
- /** Get tool_use IDs from an assistant message */
3566
- function getToolCallIds(msg) {
3567
- if (msg.role === "assistant" && msg.tool_calls) return msg.tool_calls.map((tc) => tc.id);
3568
- return [];
3569
- }
3570
- /** Filter orphaned tool_result messages */
3571
- function filterOrphanedToolResults$1(messages) {
3572
- const toolUseIds = /* @__PURE__ */ new Set();
3573
- for (const msg of messages) for (const id of getToolCallIds(msg)) toolUseIds.add(id);
3574
- let removedCount = 0;
3575
- const filtered = messages.filter((msg) => {
3576
- if (msg.role === "tool" && msg.tool_call_id && !toolUseIds.has(msg.tool_call_id)) {
3577
- removedCount++;
3578
- return false;
3579
- }
3580
- return true;
3581
- });
3582
- if (removedCount > 0) consola.debug(`[AutoTruncate:OpenAI] Filtered ${removedCount} orphaned tool_result`);
3583
- return filtered;
3584
- }
3585
- /** Get tool_result IDs from all tool messages */
3586
- function getToolResultIds$1(messages) {
3587
- const ids = /* @__PURE__ */ new Set();
3588
- for (const msg of messages) if (msg.role === "tool" && msg.tool_call_id) ids.add(msg.tool_call_id);
3589
- return ids;
3385
+ /** Get tool_result IDs from all tool messages */
3386
+ function getToolResultIds$1(messages) {
3387
+ const ids = /* @__PURE__ */ new Set();
3388
+ for (const msg of messages) if (msg.role === "tool" && msg.tool_call_id) ids.add(msg.tool_call_id);
3389
+ return ids;
3590
3390
  }
3591
3391
  /** Filter orphaned tool_use messages (those without matching tool_result) */
3592
3392
  function filterOrphanedToolUse$1(messages) {
@@ -3713,7 +3513,7 @@ function createTruncationSystemContext$1(removedCount, compressedCount, summary)
3713
3513
  return context;
3714
3514
  }
3715
3515
  /** Create a truncation marker message (fallback when no system message) */
3716
- function createTruncationMarker$2(removedCount, compressedCount, summary) {
3516
+ function createTruncationMarker$1(removedCount, compressedCount, summary) {
3717
3517
  const parts = [];
3718
3518
  if (removedCount > 0) parts.push(`${removedCount} earlier messages removed`);
3719
3519
  if (compressedCount > 0) parts.push(`${compressedCount} tool results compressed`);
@@ -3840,7 +3640,7 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
3840
3640
  content: typeof lastSystem.content === "string" ? lastSystem.content + truncationContext : lastSystem.content
3841
3641
  };
3842
3642
  newSystemMessages = [...systemMessages.slice(0, lastSystemIdx), updatedSystem];
3843
- } else newMessages = [createTruncationMarker$2(removedCount, compressedCount, summary), ...preserved];
3643
+ } else newMessages = [createTruncationMarker$1(removedCount, compressedCount, summary), ...preserved];
3844
3644
  const newPayload = {
3845
3645
  ...payload,
3846
3646
  messages: [...newSystemMessages, ...newMessages]
@@ -3866,73 +3666,530 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
3866
3666
  }
3867
3667
 
3868
3668
  //#endregion
3869
- //#region src/lib/error-metrics.ts
3870
- function safeString(value, max = 200) {
3871
- try {
3872
- if (typeof value !== "string") return void 0;
3873
- return value.length > max ? value.slice(0, max) : value;
3874
- } catch {
3875
- return;
3876
- }
3669
+ //#region src/lib/openai-payload-prep.ts
3670
+ /**
3671
+ * OpenAI-shape payload preparation.
3672
+ *
3673
+ * Pre-flight steps for any request whose final payload is an OpenAI
3674
+ * ChatCompletionsPayload — auto-truncate decisions, 413 diagnostic logging,
3675
+ * and the non-streaming type guard. Used by both `routes/chat-completions`
3676
+ * (native OpenAI) and `routes/messages/translated-handler` (Anthropic that
3677
+ * gets translated into OpenAI shape before hitting upstream).
3678
+ */
3679
+ /** Type guard for non-streaming responses */
3680
+ function isNonStreaming(response) {
3681
+ return Object.hasOwn(response, "choices");
3877
3682
  }
3878
- function getStringProp(obj, key, max = 200) {
3683
+ /** Build final payload with auto-truncate if needed */
3684
+ async function buildFinalPayload(payload, model, autoTruncateConfig = {}) {
3685
+ if (!state.autoTruncate || !model) {
3686
+ if (state.autoTruncate && !model) consola.warn(`Auto-truncate: Model '${payload.model}' not found in cached models, skipping`);
3687
+ return {
3688
+ finalPayload: payload,
3689
+ truncateResult: null
3690
+ };
3691
+ }
3879
3692
  try {
3880
- if (typeof obj !== "object" || obj === null) return void 0;
3881
- return safeString(obj[key], max);
3882
- } catch {
3883
- return;
3693
+ const check = await checkNeedsCompactionOpenAI(payload, model, autoTruncateConfig);
3694
+ consola.debug(`Auto-truncate check: ${check.currentTokens} tokens (limit ${check.tokenLimit}), ${Math.round(check.currentBytes / 1024)}KB (limit ${check.byteLimit === Infinity ? "unlimited" : `${Math.round(check.byteLimit / 1024)}KB`}), needed: ${check.needed}${check.reason ? ` (${check.reason})` : ""}`);
3695
+ if (!check.needed) return {
3696
+ finalPayload: payload,
3697
+ truncateResult: null
3698
+ };
3699
+ let reasonText;
3700
+ if (check.reason === "both") reasonText = "tokens and size";
3701
+ else if (check.reason === "bytes") reasonText = "size";
3702
+ else reasonText = "tokens";
3703
+ consola.info(`Auto-truncate triggered: exceeds ${reasonText} limit`);
3704
+ const truncateResult = await autoTruncateOpenAI(payload, model, autoTruncateConfig);
3705
+ return {
3706
+ finalPayload: truncateResult.payload,
3707
+ truncateResult
3708
+ };
3709
+ } catch (error) {
3710
+ consola.warn("Auto-truncate failed, proceeding with original payload:", error instanceof Error ? error.message : error);
3711
+ return {
3712
+ finalPayload: payload,
3713
+ truncateResult: null
3714
+ };
3884
3715
  }
3885
3716
  }
3886
- function extractErrorMetrics(error) {
3887
- if (error instanceof HTTPError) {
3888
- const metrics = { status: error.status };
3889
- try {
3890
- const parsed = JSON.parse(error.responseText);
3891
- if (parsed.error?.code) metrics.copilotErrorCode = parsed.error.code;
3892
- } catch {}
3893
- return metrics;
3894
- }
3895
- if (!(error instanceof Error)) return {};
3896
- const metrics = {};
3897
- try {
3898
- metrics.errorName = getStringProp(error, "name");
3899
- metrics.errorMessage = getStringProp(error, "message");
3900
- metrics.errorCode = getStringProp(error, "code");
3901
- const cause = error.cause;
3902
- if (cause !== void 0 && cause !== null) {
3903
- metrics.causeName = getStringProp(cause, "name");
3904
- metrics.causeCode = getStringProp(cause, "code");
3717
+ /**
3718
+ * Log helpful debugging information when a 413 error occurs.
3719
+ * Also adjusts the dynamic byte limit for future requests.
3720
+ */
3721
+ async function logPayloadSizeInfo(payload, model) {
3722
+ const messageCount = payload.messages.length;
3723
+ const bodySize = JSON.stringify(payload).length;
3724
+ const bodySizeKB = Math.round(bodySize / 1024);
3725
+ onRequestTooLarge(bodySize);
3726
+ let imageCount = 0;
3727
+ let largeMessages = 0;
3728
+ let totalImageSize = 0;
3729
+ for (const msg of payload.messages) {
3730
+ if (Array.isArray(msg.content)) {
3731
+ for (const part of msg.content) if (part.type === "image_url") {
3732
+ imageCount++;
3733
+ if (part.image_url.url.startsWith("data:")) totalImageSize += part.image_url.url.length;
3734
+ }
3905
3735
  }
3736
+ if ((typeof msg.content === "string" ? msg.content.length : JSON.stringify(msg.content).length) > 5e4) largeMessages++;
3737
+ }
3738
+ consola.info("");
3739
+ consola.info("╭─────────────────────────────────────────────────────────╮");
3740
+ consola.info("│ 413 Request Entity Too Large │");
3741
+ consola.info("╰─────────────────────────────────────────────────────────╯");
3742
+ consola.info("");
3743
+ consola.info(` Request body size: ${bodySizeKB} KB (${bodySize.toLocaleString()} bytes)`);
3744
+ consola.info(` Message count: ${messageCount}`);
3745
+ if (model) try {
3746
+ const tokenCount = await getTokenCount(payload, model);
3747
+ const limit = model.capabilities?.limits?.max_prompt_tokens ?? 128e3;
3748
+ consola.info(` Estimated tokens: ${tokenCount.input.toLocaleString()} / ${limit.toLocaleString()}`);
3906
3749
  } catch {}
3907
- return metrics;
3750
+ if (imageCount > 0) {
3751
+ const imageSizeKB = Math.round(totalImageSize / 1024);
3752
+ consola.info(` Images: ${imageCount} (${imageSizeKB} KB base64 data)`);
3753
+ }
3754
+ if (largeMessages > 0) consola.info(` Large messages (>50KB): ${largeMessages}`);
3755
+ consola.info("");
3756
+ consola.info(" Suggestions:");
3757
+ if (!state.autoTruncate) consola.info(" • Enable --auto-truncate to automatically truncate history");
3758
+ if (imageCount > 0) consola.info(" • Remove or resize large images in the conversation");
3759
+ consola.info(" • Start a new conversation with /clear or /reset");
3760
+ consola.info(" • Reduce conversation history by deleting old messages");
3761
+ consola.info("");
3908
3762
  }
3909
3763
 
3910
3764
  //#endregion
3911
- //#region src/routes/shared.ts
3765
+ //#region src/lib/repetition-detector.ts
3912
3766
  /**
3913
- * Shared utilities for request handlers.
3914
- * Contains common functions used by both OpenAI and Anthropic message handlers.
3767
+ * Stream repetition detector.
3768
+ *
3769
+ * Uses the KMP failure function (prefix function) to detect repeated patterns
3770
+ * in streaming text output. When a model gets stuck in a repetitive loop,
3771
+ * it wastes tokens producing the same content over and over. This detector
3772
+ * identifies such loops early so the caller can take action (log warning,
3773
+ * abort stream, etc.).
3774
+ *
3775
+ * The algorithm works by maintaining a sliding buffer of recent text and
3776
+ * computing the longest proper prefix that is also a suffix — if this
3777
+ * length exceeds `(text.length - period) >= minRepetitions * period`,
3778
+ * it means a pattern of length `period` has repeated enough times.
3915
3779
  */
3916
- /** Helper to update tracker model */
3917
- function updateTrackerModel(trackingId, model, resolvedModel) {
3918
- if (!trackingId) return;
3919
- const request = requestTracker.getRequest(trackingId);
3920
- if (!request) return;
3921
- request.model = model;
3922
- request.resolvedModel = resolvedModel ?? model;
3923
- }
3924
- /** Helper to update only the resolved (post-translation) model */
3925
- function updateTrackerResolvedModel(trackingId, resolvedModel) {
3926
- if (!trackingId) return;
3927
- const request = requestTracker.getRequest(trackingId);
3928
- if (request) request.resolvedModel = resolvedModel;
3929
- }
3930
- /** Helper to update tracker status */
3931
- function updateTrackerStatus(trackingId, status) {
3932
- if (!trackingId) return;
3933
- requestTracker.updateRequest(trackingId, { status });
3934
- }
3935
- /** Record error response to history */
3780
+ const DEFAULT_CONFIG = {
3781
+ minPatternLength: 10,
3782
+ minRepetitions: 3,
3783
+ maxBufferSize: 5e3
3784
+ };
3785
+ var RepetitionDetector = class {
3786
+ buffer = "";
3787
+ config;
3788
+ detected = false;
3789
+ constructor(config) {
3790
+ this.config = {
3791
+ ...DEFAULT_CONFIG,
3792
+ ...config
3793
+ };
3794
+ }
3795
+ /**
3796
+ * Feed a text chunk into the detector.
3797
+ * Returns `true` if repetition has been detected (now or previously).
3798
+ * Once detected, subsequent calls return `true` without further analysis.
3799
+ */
3800
+ feed(text) {
3801
+ if (this.detected) return true;
3802
+ if (!text) return false;
3803
+ this.buffer += text;
3804
+ if (this.buffer.length > this.config.maxBufferSize) this.buffer = this.buffer.slice(-this.config.maxBufferSize);
3805
+ const minRequired = this.config.minPatternLength * this.config.minRepetitions;
3806
+ if (this.buffer.length < minRequired) return false;
3807
+ this.detected = detectRepetition(this.buffer, this.config.minPatternLength, this.config.minRepetitions);
3808
+ return this.detected;
3809
+ }
3810
+ /** Reset detector state for a new stream */
3811
+ reset() {
3812
+ this.buffer = "";
3813
+ this.detected = false;
3814
+ }
3815
+ /** Whether repetition has been detected */
3816
+ get isDetected() {
3817
+ return this.detected;
3818
+ }
3819
+ };
3820
+ /**
3821
+ * Detect if the tail of `text` contains a repeating pattern.
3822
+ *
3823
+ * Uses the KMP prefix function: for a string S, the prefix function π[i]
3824
+ * gives the length of the longest proper prefix of S[0..i] that is also
3825
+ * a suffix. If π[n-1] ≥ (n - period) where period = n - π[n-1], then
3826
+ * the string is composed of a repeating unit of length `period`.
3827
+ *
3828
+ * We check the suffix of the buffer (last `checkLength` chars) to detect
3829
+ * if a pattern of at least `minPatternLength` chars repeats at least
3830
+ * `minRepetitions` times.
3831
+ */
3832
+ function detectRepetition(text, minPatternLength, minRepetitions) {
3833
+ const minWindow = minPatternLength * minRepetitions;
3834
+ const maxWindow = Math.min(text.length, 2e3);
3835
+ const windowSizes = [
3836
+ minWindow,
3837
+ Math.floor(maxWindow * .5),
3838
+ maxWindow
3839
+ ].filter((w) => w >= minWindow && w <= text.length);
3840
+ for (const windowSize of windowSizes) {
3841
+ const window = text.slice(-windowSize);
3842
+ const period = findRepeatingPeriod(window);
3843
+ if (period >= minPatternLength) {
3844
+ if (Math.floor(window.length / period) >= minRepetitions) return true;
3845
+ }
3846
+ }
3847
+ return false;
3848
+ }
3849
+ /**
3850
+ * Find the shortest repeating period in a string using KMP prefix function.
3851
+ * Returns the period length, or the string length if no repetition found.
3852
+ */
3853
+ function findRepeatingPeriod(s) {
3854
+ const n = s.length;
3855
+ if (n === 0) return 0;
3856
+ const pi = new Int32Array(n);
3857
+ for (let i = 1; i < n; i++) {
3858
+ let j = pi[i - 1] ?? 0;
3859
+ while (j > 0 && s[i] !== s[j]) j = pi[j - 1] ?? 0;
3860
+ if (s[i] === s[j]) j++;
3861
+ pi[i] = j;
3862
+ }
3863
+ const period = n - pi[n - 1];
3864
+ if (period < n && n % period === 0) return period;
3865
+ if (period < n && pi[n - 1] >= period) return period;
3866
+ return n;
3867
+ }
3868
+ /**
3869
+ * Create a repetition detector callback for use in stream processing.
3870
+ * Returns a function that accepts text deltas and logs a warning on first detection.
3871
+ */
3872
+ function createStreamRepetitionChecker(label, config) {
3873
+ const detector = new RepetitionDetector(config);
3874
+ let warned = false;
3875
+ return (textDelta) => {
3876
+ const isRepetitive = detector.feed(textDelta);
3877
+ if (isRepetitive && !warned) {
3878
+ warned = true;
3879
+ consola.warn(`[RepetitionDetector] ${label}: Repetitive output detected in stream`);
3880
+ }
3881
+ return isRepetitive;
3882
+ };
3883
+ }
3884
+
3885
+ //#endregion
3886
+ //#region src/lib/anthropic/beta.ts
3887
+ /**
3888
+ * Vendor-neutral utilities for manipulating the `anthropic-beta` request header.
3889
+ *
3890
+ * Lives in `lib/anthropic/` (not in either transport module) so both the
3891
+ * Anthropic-native and OpenAI-translated transport layers can share these
3892
+ * helpers without introducing cross-transport imports.
3893
+ */
3894
+ /** Anthropic beta feature that unlocks the 1M context window. */
3895
+ const CONTEXT_1M_BETA_FEATURE = "context-1m-2025-08-07";
3896
+ /**
3897
+ * Merge two comma-separated anthropic-beta header values. Trims whitespace,
3898
+ * drops empty tokens, and dedupes by exact string match. Returns a canonical
3899
+ * comma-joined string with no spaces.
3900
+ *
3901
+ * Either input may be undefined / empty.
3902
+ */
3903
+ function mergeBetaFeatures(existing, incoming) {
3904
+ const seen = /* @__PURE__ */ new Set();
3905
+ const out = [];
3906
+ for (const raw of [existing, incoming]) {
3907
+ if (!raw) continue;
3908
+ for (const part of raw.split(",")) {
3909
+ const f = part.trim();
3910
+ if (f.length === 0 || seen.has(f)) continue;
3911
+ seen.add(f);
3912
+ out.push(f);
3913
+ }
3914
+ }
3915
+ return out.join(",");
3916
+ }
3917
+ /**
3918
+ * Append the context-1m feature to an anthropic-beta header value, deduping
3919
+ * any prior occurrence. Returns the merged comma-separated string.
3920
+ */
3921
+ function appendContext1mBeta(existing) {
3922
+ return mergeBetaFeatures(existing, CONTEXT_1M_BETA_FEATURE);
3923
+ }
3924
+ /**
3925
+ * True iff a model id appears to be the suffixed 1M-context variant of an
3926
+ * Anthropic Claude model (e.g. claude-opus-4-8-1m, claude-opus-4.6-1m).
3927
+ *
3928
+ * Used as a state.models-independent signal for whether to inject the
3929
+ * context-1m-2025-08-07 beta header, so the 1M intent survives a stale or
3930
+ * empty model cache (where `resolveAnthropicModelForDirectPath` would return
3931
+ * undefined). Forwarding the beta is harmless to upstreams that ignore it.
3932
+ */
3933
+ function isOneMillionSuffixedClaudeId(modelId) {
3934
+ return modelId.startsWith("claude-") && modelId.endsWith("-1m");
3935
+ }
3936
+
3937
+ //#endregion
3938
+ //#region src/lib/headers.ts
3939
+ /**
3940
+ * Vendor-neutral header-bag helpers.
3941
+ *
3942
+ * HTTP header names are case-insensitive, but a plain-object header bag is
3943
+ * case-sensitive on its keys. Code that wants to look up "anthropic-beta"
3944
+ * without knowing whether some other producer wrote "Anthropic-Beta" needs
3945
+ * `findHeaderKey`. Code that wants to set a header without creating a
3946
+ * second case variant of the same name needs `setHeader`.
3947
+ */
3948
+ /** Case-insensitive lookup of a header key in a plain-object header bag. */
3949
+ function findHeaderKey(headers, name) {
3950
+ const lower = name.toLowerCase();
3951
+ return Object.keys(headers).find((k) => k.toLowerCase() === lower);
3952
+ }
3953
+ /** Case-insensitive read of a header value. */
3954
+ function getHeader(headers, name) {
3955
+ const key = findHeaderKey(headers, name);
3956
+ return key === void 0 ? void 0 : headers[key];
3957
+ }
3958
+ /**
3959
+ * Set a header value at the existing case variant if one is present, else at
3960
+ * the supplied canonical name. Prevents a second key (different case) from
3961
+ * being added for the same logical header.
3962
+ */
3963
+ function setHeader(headers, name, value) {
3964
+ const key = findHeaderKey(headers, name) ?? name;
3965
+ headers[key] = value;
3966
+ }
3967
+
3968
+ //#endregion
3969
+ //#region src/services/copilot/create-chat-completions.ts
3970
+ const GPT_MODEL_PATTERN = /^gpt-/i;
3971
+ const createChatCompletions = async (payload, options) => {
3972
+ if (!state.copilotToken) throw new Error("Copilot token not found");
3973
+ const vendor = options?.resolvedModel?.vendor;
3974
+ const isOpenAIVendor = vendor === "OpenAI" || vendor === "Azure OpenAI";
3975
+ const isLikelyGPT = !options?.resolvedModel && GPT_MODEL_PATTERN.test(payload.model);
3976
+ let wire = payload;
3977
+ if (isOpenAIVendor || isLikelyGPT) {
3978
+ const { max_tokens, max_completion_tokens, ...rest } = payload;
3979
+ const effective = max_completion_tokens ?? max_tokens;
3980
+ wire = {
3981
+ ...rest,
3982
+ ...effective !== null && effective !== void 0 && { max_completion_tokens: effective }
3983
+ };
3984
+ }
3985
+ const enableVision = wire.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
3986
+ const isAgentCall = wire.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
3987
+ const modelSupportsVision = options?.resolvedModel?.capabilities?.supports?.vision !== false;
3988
+ const headers = {
3989
+ ...copilotHeaders(state, {
3990
+ vision: enableVision && modelSupportsVision,
3991
+ modelRequestHeaders: options?.resolvedModel?.request_headers,
3992
+ intent: isAgentCall ? "conversation-agent" : "conversation-panel"
3993
+ }),
3994
+ "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
3995
+ };
3996
+ if (options?.anthropicBeta) {
3997
+ const existingKey = findHeaderKey(headers, "anthropic-beta") ?? "anthropic-beta";
3998
+ headers[existingKey] = mergeBetaFeatures(headers[existingKey], options.anthropicBeta);
3999
+ consola.debug(`[ChatCompletions] anthropic-beta after merge: ${headers[existingKey]}`);
4000
+ }
4001
+ const response = await copilotFetch("/chat/completions", {
4002
+ method: "POST",
4003
+ headers,
4004
+ body: JSON.stringify(wire)
4005
+ });
4006
+ if (!response.ok) {
4007
+ consola.error("Failed to create chat completions", response);
4008
+ throw await HTTPError.fromResponse("Failed to create chat completions", response, options?.errorModelIdOverride ?? payload.model);
4009
+ }
4010
+ if (payload.stream) return events(response);
4011
+ return await response.json();
4012
+ };
4013
+
4014
+ //#endregion
4015
+ //#region src/lib/echo-model.ts
4016
+ /**
4017
+ * Capture the requested model id from a raw request `model` value, classifying
4018
+ * it into the three-state contract. `undefined`/missing → `absent`; `""` →
4019
+ * `empty`; any other string → `present`.
4020
+ */
4021
+ function captureRequestedModel(rawModel) {
4022
+ if (rawModel === void 0 || rawModel === null) return { kind: "absent" };
4023
+ if (typeof rawModel !== "string") return { kind: "absent" };
4024
+ if (rawModel === "") return { kind: "empty" };
4025
+ return {
4026
+ kind: "present",
4027
+ value: rawModel
4028
+ };
4029
+ }
4030
+ /**
4031
+ * Resolve a {@link RequestedModel} into the action to take on a model field:
4032
+ * - `{ write: true, value }` → set the field to `value`.
4033
+ * - `{ write: false, omit: true }` → remove the field (absent case).
4034
+ * - `null` → leave the field untouched (context-missing case).
4035
+ */
4036
+ function resolveFieldAction(requested) {
4037
+ if (requested.kind === "present") return { value: requested.value };
4038
+ if (requested.kind === "empty") return { value: "" };
4039
+ if (requested.kind === "absent") return { omit: true };
4040
+ return null;
4041
+ }
4042
+ /**
4043
+ * Apply the requested-model action to a single `model`-like key on a shallow
4044
+ * clone of `obj`. Returns a new object; never mutates `obj`. If `obj` does not
4045
+ * own `key`, it is returned (cloned) unchanged regardless of the action — we
4046
+ * only ever rewrite a field the upstream payload actually carries (supports
4047
+ * AC-MALFORMED-SSE: no model field → original passthrough).
4048
+ */
4049
+ function rewriteKey(obj, key, requested) {
4050
+ if (!Object.hasOwn(obj, key)) return obj;
4051
+ const action = resolveFieldAction(requested);
4052
+ if (action === null) return obj;
4053
+ if ("omit" in action) {
4054
+ const { [key]: _omitted, ...rest } = obj;
4055
+ return rest;
4056
+ }
4057
+ return {
4058
+ ...obj,
4059
+ [key]: action.value
4060
+ };
4061
+ }
4062
+ /**
4063
+ * Core rewrite over a plain record. Rewrites the documented model fields:
4064
+ * - top-level `model`,
4065
+ * - nested `message.model` (Anthropic message_start),
4066
+ * - nested `response.model` (OpenAI Responses event).
4067
+ * Returns a shallow clone; never mutates the input.
4068
+ */
4069
+ function echoRecord(body, requested) {
4070
+ let out = rewriteKey(body, "model", requested);
4071
+ if (isRecord(out.message) && Object.hasOwn(out.message, "model")) {
4072
+ const newMessage = rewriteKey(out.message, "model", requested);
4073
+ if (newMessage !== out.message) out = {
4074
+ ...out,
4075
+ message: newMessage
4076
+ };
4077
+ }
4078
+ if (isRecord(out.response) && Object.hasOwn(out.response, "model")) {
4079
+ const newResponse = rewriteKey(out.response, "model", requested);
4080
+ if (newResponse !== out.response) out = {
4081
+ ...out,
4082
+ response: newResponse
4083
+ };
4084
+ }
4085
+ return out;
4086
+ }
4087
+ /**
4088
+ * Rewrite the documented client-facing model field(s) of a JSON response body
4089
+ * to the requested model id. Handles every supported non-stream/body shape:
4090
+ * - top-level `model` (OpenAI chat/completions & Responses bodies, Anthropic
4091
+ * messages body),
4092
+ * - nested `message.model` (Anthropic `message_start` event object),
4093
+ * - nested `response.model` (OpenAI Responses streaming event object).
4094
+ *
4095
+ * Returns a shallow-cloned object; the input is never mutated. Fields that are
4096
+ * not present are left as-is (a payload with no model field round-trips
4097
+ * unchanged), supporting AC-MALFORMED-SSE.
4098
+ *
4099
+ * The generic is constrained to `object` (not an index-signature shape) so it
4100
+ * accepts the project's domain interfaces (`AnthropicResponse`,
4101
+ * `ChatCompletionChunk`, …) directly without forcing callers to widen them.
4102
+ */
4103
+ function echoModelInResponseBody(body, requested) {
4104
+ return echoRecord(body, requested);
4105
+ }
4106
+ function isRecord(value) {
4107
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4108
+ }
4109
+ /**
4110
+ * Rewrite the model field of an already-parsed SSE event payload to the
4111
+ * requested model id, dispatched by protocol shape:
4112
+ * - Anthropic `message_start` → `message.model`,
4113
+ * - OpenAI chunk → top-level `model`,
4114
+ * - OpenAI Responses event → nested `response.model`.
4115
+ *
4116
+ * The field dispatch is identical to {@link echoModelInResponseBody} (both
4117
+ * operate on the same documented model fields), so this delegates to the same
4118
+ * core rather than duplicating the shape logic — the two exports exist to name
4119
+ * the two responsibilities (body vs parsed-event) at call sites, per the PRD's
4120
+ * single-policy-point design. Events with no model field round-trip unchanged
4121
+ * (AC-MALFORMED-SSE); the input is never mutated.
4122
+ */
4123
+ function echoModelInParsedEvent(event, requested) {
4124
+ return echoRecord(event, requested);
4125
+ }
4126
+
4127
+ //#endregion
4128
+ //#region src/lib/error-metrics.ts
4129
+ function safeString(value, max = 200) {
4130
+ try {
4131
+ if (typeof value !== "string") return void 0;
4132
+ return value.length > max ? value.slice(0, max) : value;
4133
+ } catch {
4134
+ return;
4135
+ }
4136
+ }
4137
+ function getStringProp(obj, key, max = 200) {
4138
+ try {
4139
+ if (typeof obj !== "object" || obj === null) return void 0;
4140
+ return safeString(obj[key], max);
4141
+ } catch {
4142
+ return;
4143
+ }
4144
+ }
4145
+ function extractErrorMetrics(error) {
4146
+ if (error instanceof HTTPError) {
4147
+ const metrics = { status: error.status };
4148
+ try {
4149
+ const parsed = JSON.parse(error.responseText);
4150
+ if (parsed.error?.code) metrics.copilotErrorCode = parsed.error.code;
4151
+ } catch {}
4152
+ return metrics;
4153
+ }
4154
+ if (!(error instanceof Error)) return {};
4155
+ const metrics = {};
4156
+ try {
4157
+ metrics.errorName = getStringProp(error, "name");
4158
+ metrics.errorMessage = getStringProp(error, "message");
4159
+ metrics.errorCode = getStringProp(error, "code");
4160
+ const cause = error.cause;
4161
+ if (cause !== void 0 && cause !== null) {
4162
+ metrics.causeName = getStringProp(cause, "name");
4163
+ metrics.causeCode = getStringProp(cause, "code");
4164
+ }
4165
+ } catch {}
4166
+ return metrics;
4167
+ }
4168
+
4169
+ //#endregion
4170
+ //#region src/routes/observability-recording.ts
4171
+ /**
4172
+ * Observability recording for error paths.
4173
+ *
4174
+ * Writes failed requests to history and emits matching PostHog analytics
4175
+ * events for both non-streaming errors (before the stream starts) and
4176
+ * mid-stream errors (after the accumulator has partial data). Success-path
4177
+ * analytics live in `tracker-mutations.ts` alongside `completeTracking`.
4178
+ */
4179
+ function formatError(error) {
4180
+ if (error instanceof Error) return error.message || error.name;
4181
+ if (typeof error === "string") return error;
4182
+ try {
4183
+ const s = JSON.stringify(error);
4184
+ if (s && s !== "{}") return s;
4185
+ } catch {}
4186
+ try {
4187
+ return String(error);
4188
+ } catch {
4189
+ return "Unknown error";
4190
+ }
4191
+ }
4192
+ /** Record error response to history */
3936
4193
  function recordErrorResponse(ctx, model, error, endpoint, stream) {
3937
4194
  recordResponse(ctx.historyId, {
3938
4195
  success: false,
@@ -3941,26 +4198,83 @@ function recordErrorResponse(ctx, model, error, endpoint, stream) {
3941
4198
  input_tokens: 0,
3942
4199
  output_tokens: 0
3943
4200
  },
3944
- error: error instanceof Error ? error.message : "Unknown error",
4201
+ error: formatError(error),
3945
4202
  content: null
3946
4203
  }, Date.now() - ctx.startTime);
3947
- if (endpoint !== void 0) {
3948
- const metrics = extractErrorMetrics(error);
3949
- const { attempts } = getRetryAttempts(error);
3950
- captureRequest({
3951
- model,
3952
- inputTokens: 0,
3953
- outputTokens: 0,
3954
- durationMs: Date.now() - ctx.startTime,
3955
- success: false,
3956
- stream: stream ?? false,
3957
- toolCount: 0,
3958
- ...metrics,
3959
- errorPhase: stream ? "pre_stream" : "non_stream",
3960
- endpoint,
3961
- attempt: attempts
3962
- });
3963
- }
4204
+ const metrics = extractErrorMetrics(error);
4205
+ const { attempts } = getRetryAttempts(error);
4206
+ captureRequest({
4207
+ model,
4208
+ inputTokens: 0,
4209
+ outputTokens: 0,
4210
+ durationMs: Date.now() - ctx.startTime,
4211
+ success: false,
4212
+ stream: stream ?? false,
4213
+ toolCount: 0,
4214
+ ...metrics,
4215
+ errorPhase: stream ? "pre_stream" : "non_stream",
4216
+ endpoint,
4217
+ attempt: attempts
4218
+ });
4219
+ }
4220
+ /** Record streaming error to history (works with any accumulator type) */
4221
+ function recordStreamError(opts) {
4222
+ const { acc, fallbackModel, ctx, error, endpoint } = opts;
4223
+ const model = acc.model || fallbackModel;
4224
+ recordResponse(ctx.historyId, {
4225
+ success: false,
4226
+ model,
4227
+ usage: {
4228
+ input_tokens: 0,
4229
+ output_tokens: 0
4230
+ },
4231
+ error: formatError(error),
4232
+ content: null
4233
+ }, Date.now() - ctx.startTime);
4234
+ const metrics = extractErrorMetrics(error);
4235
+ captureRequest({
4236
+ model,
4237
+ inputTokens: acc.inputTokens ?? 0,
4238
+ outputTokens: acc.outputTokens ?? 0,
4239
+ durationMs: Date.now() - ctx.startTime,
4240
+ success: false,
4241
+ stream: true,
4242
+ toolCount: 0,
4243
+ ...metrics,
4244
+ errorPhase: "mid_stream",
4245
+ endpoint,
4246
+ attempt: 1
4247
+ });
4248
+ }
4249
+
4250
+ //#endregion
4251
+ //#region src/routes/tracker-mutations.ts
4252
+ /**
4253
+ * TUI tracker mutations and analytics completion.
4254
+ *
4255
+ * All in-place updates to the TUI request tracker (model, status, resolved
4256
+ * model) and the success/failure terminal transitions. `completeTracking`
4257
+ * additionally emits a PostHog analytics event for successful requests; error
4258
+ * paths emit their PostHog events through `observability-recording.ts`.
4259
+ */
4260
+ /** Helper to update tracker model */
4261
+ function updateTrackerModel(trackingId, model, resolvedModel) {
4262
+ if (!trackingId) return;
4263
+ const request = requestTracker.getRequest(trackingId);
4264
+ if (!request) return;
4265
+ request.model = model;
4266
+ request.resolvedModel = resolvedModel ?? model;
4267
+ }
4268
+ /** Helper to update only the resolved (post-translation) model */
4269
+ function updateTrackerResolvedModel(trackingId, resolvedModel) {
4270
+ if (!trackingId) return;
4271
+ const request = requestTracker.getRequest(trackingId);
4272
+ if (request) request.resolvedModel = resolvedModel;
4273
+ }
4274
+ /** Helper to update tracker status */
4275
+ function updateTrackerStatus(trackingId, status) {
4276
+ if (!trackingId) return;
4277
+ requestTracker.updateRequest(trackingId, { status });
3964
4278
  }
3965
4279
  /** Complete TUI tracking and send PostHog analytics */
3966
4280
  function completeTracking(trackingId, inputTokens, outputTokens, queueWaitMs, reasoningTokens, analytics) {
@@ -3988,149 +4302,90 @@ function completeTracking(trackingId, inputTokens, outputTokens, queueWaitMs, re
3988
4302
  stopReason: analytics.stopReason
3989
4303
  });
3990
4304
  }
3991
- function formatError(error) {
3992
- if (error instanceof Error) return error.message || error.name;
3993
- if (typeof error === "string") return error;
3994
- try {
3995
- const s = JSON.stringify(error);
3996
- if (s && s !== "{}") return s;
3997
- } catch {}
3998
- try {
3999
- return String(error);
4000
- } catch {
4001
- return "Unknown error";
4002
- }
4003
- }
4004
4305
  /** Fail TUI tracking */
4005
4306
  function failTracking(trackingId, error) {
4006
4307
  if (!trackingId) return;
4007
4308
  requestTracker.failRequest(trackingId, formatError(error));
4008
4309
  }
4310
+
4311
+ //#endregion
4312
+ //#region src/routes/entry-context.ts
4009
4313
  /**
4010
- * Create a marker to prepend to responses indicating auto-truncation occurred.
4011
- * Works with both OpenAI and Anthropic truncate results.
4314
+ * Construct the route-entry context for any handler.
4315
+ *
4316
+ * Returns `{ ctx, payload }` where `payload` is the normalized version of
4317
+ * `rawPayload` (possibly unchanged) and `ctx` is fully populated with the
4318
+ * captured R, history id, tracking id, and start time.
4012
4319
  */
4013
- function createTruncationMarker$1(result) {
4014
- if (!result.wasCompacted) return "";
4015
- const { originalTokens, compactedTokens, removedMessageCount } = result;
4016
- if (originalTokens === void 0 || compactedTokens === void 0 || removedMessageCount === void 0) return `\n\n---\n[Auto-truncated: conversation history was reduced to fit context limits]`;
4017
- const reduction = originalTokens - compactedTokens;
4018
- return `\n\n---\n[Auto-truncated: ${removedMessageCount} messages removed, ${originalTokens} → ${compactedTokens} tokens (${Math.round(reduction / originalTokens * 100)}% reduction)]`;
4019
- }
4020
- /** Record streaming error to history (works with any accumulator type) */
4021
- function recordStreamError(opts) {
4022
- const { acc, fallbackModel, ctx, error, endpoint } = opts;
4023
- const model = acc.model || fallbackModel;
4024
- recordResponse(ctx.historyId, {
4025
- success: false,
4026
- model,
4027
- usage: {
4028
- input_tokens: 0,
4029
- output_tokens: 0
4030
- },
4031
- error: formatError(error),
4032
- content: null
4033
- }, Date.now() - ctx.startTime);
4034
- if (endpoint !== void 0) {
4035
- const metrics = extractErrorMetrics(error);
4036
- captureRequest({
4037
- model,
4038
- inputTokens: acc.inputTokens ?? 0,
4039
- outputTokens: acc.outputTokens ?? 0,
4040
- durationMs: Date.now() - ctx.startTime,
4041
- success: false,
4042
- stream: true,
4043
- toolCount: 0,
4044
- ...metrics,
4045
- errorPhase: "mid_stream",
4046
- endpoint,
4047
- attempt: 1
4048
- });
4049
- }
4320
+ function createEntryContext(args) {
4321
+ const requestedModel = captureRequestedModel(args.rawPayload.model);
4322
+ const payload = args.normalizePayload ? args.normalizePayload(args.rawPayload) : args.rawPayload;
4323
+ const trackingId = args.c.get("trackingId");
4324
+ const startTime = (trackingId ? requestTracker.getRequest(trackingId) : void 0)?.startTime ?? Date.now();
4325
+ updateTrackerModel(trackingId, payload.model);
4326
+ return {
4327
+ payload,
4328
+ ctx: {
4329
+ historyId: recordRequest(args.endpoint, args.buildHistoryRequest(payload)),
4330
+ trackingId,
4331
+ startTime,
4332
+ requestedModel
4333
+ }
4334
+ };
4050
4335
  }
4051
- /** Type guard for non-streaming responses */
4052
- function isNonStreaming(response) {
4053
- return Object.hasOwn(response, "choices");
4336
+
4337
+ //#endregion
4338
+ //#region src/routes/response-context.ts
4339
+ /**
4340
+ * Resolve the requested model id (R) carried on the response context into a
4341
+ * {@link RequestedModel}. A context that never captured R (`undefined`) is
4342
+ * treated as `context-missing` so the echo passes the upstream value through
4343
+ * unchanged — it must never invent an id or throw.
4344
+ *
4345
+ * `createEntryContext` always populates `requestedModel`, so callers operating
4346
+ * on a ctx that came from the entry adapter never hit the `context-missing`
4347
+ * branch. The fallback exists for the (theoretically impossible) case where
4348
+ * a ctx was constructed by hand without `requestedModel` — e.g., a test
4349
+ * fixture or a future code path that bypasses the adapter.
4350
+ */
4351
+ function requestedModelOf(ctx) {
4352
+ return ctx.requestedModel ?? { kind: "context-missing" };
4054
4353
  }
4055
- /** Build final payload with auto-truncate if needed */
4056
- async function buildFinalPayload(payload, model, autoTruncateConfig = {}) {
4057
- if (!state.autoTruncate || !model) {
4058
- if (state.autoTruncate && !model) consola.warn(`Auto-truncate: Model '${payload.model}' not found in cached models, skipping`);
4059
- return {
4060
- finalPayload: payload,
4061
- truncateResult: null
4062
- };
4063
- }
4064
- try {
4065
- const check = await checkNeedsCompactionOpenAI(payload, model, autoTruncateConfig);
4066
- consola.debug(`Auto-truncate check: ${check.currentTokens} tokens (limit ${check.tokenLimit}), ${Math.round(check.currentBytes / 1024)}KB (limit ${check.byteLimit === Infinity ? "unlimited" : `${Math.round(check.byteLimit / 1024)}KB`}), needed: ${check.needed}${check.reason ? ` (${check.reason})` : ""}`);
4067
- if (!check.needed) return {
4068
- finalPayload: payload,
4069
- truncateResult: null
4070
- };
4071
- let reasonText;
4072
- if (check.reason === "both") reasonText = "tokens and size";
4073
- else if (check.reason === "bytes") reasonText = "size";
4074
- else reasonText = "tokens";
4075
- consola.info(`Auto-truncate triggered: exceeds ${reasonText} limit`);
4076
- const truncateResult = await autoTruncateOpenAI(payload, model, autoTruncateConfig);
4077
- return {
4078
- finalPayload: truncateResult.payload,
4079
- truncateResult
4080
- };
4081
- } catch (error) {
4082
- consola.warn("Auto-truncate failed, proceeding with original payload:", error instanceof Error ? error.message : error);
4083
- return {
4084
- finalPayload: payload,
4085
- truncateResult: null
4086
- };
4087
- }
4354
+ /**
4355
+ * Echo the requested model id into a JSON response body at the client write-out
4356
+ * boundary. Thin context-aware wrapper over {@link echoModelInResponseBody};
4357
+ * MUST be called AFTER history/posthog/TUI have read the upstream value.
4358
+ */
4359
+ function echoResponseBody(body, ctx) {
4360
+ return echoModelInResponseBody(body, requestedModelOf(ctx));
4088
4361
  }
4089
4362
  /**
4090
- * Log helpful debugging information when a 413 error occurs.
4091
- * Also adjusts the dynamic byte limit for future requests.
4363
+ * Echo the requested model id into an already-parsed SSE event payload at the
4364
+ * client write-out boundary. Thin context-aware wrapper over
4365
+ * {@link echoModelInParsedEvent}; MUST be called AFTER the stream accumulator
4366
+ * (the observability data source) has read the upstream value.
4092
4367
  */
4093
- async function logPayloadSizeInfo(payload, model) {
4094
- const messageCount = payload.messages.length;
4095
- const bodySize = JSON.stringify(payload).length;
4096
- const bodySizeKB = Math.round(bodySize / 1024);
4097
- onRequestTooLarge(bodySize);
4098
- let imageCount = 0;
4099
- let largeMessages = 0;
4100
- let totalImageSize = 0;
4101
- for (const msg of payload.messages) {
4102
- if (Array.isArray(msg.content)) {
4103
- for (const part of msg.content) if (part.type === "image_url") {
4104
- imageCount++;
4105
- if (part.image_url.url.startsWith("data:")) totalImageSize += part.image_url.url.length;
4106
- }
4107
- }
4108
- if ((typeof msg.content === "string" ? msg.content.length : JSON.stringify(msg.content).length) > 5e4) largeMessages++;
4109
- }
4110
- consola.info("");
4111
- consola.info("╭─────────────────────────────────────────────────────────╮");
4112
- consola.info("│ 413 Request Entity Too Large │");
4113
- consola.info("╰─────────────────────────────────────────────────────────╯");
4114
- consola.info("");
4115
- consola.info(` Request body size: ${bodySizeKB} KB (${bodySize.toLocaleString()} bytes)`);
4116
- consola.info(` Message count: ${messageCount}`);
4117
- if (model) try {
4118
- const tokenCount = await getTokenCount(payload, model);
4119
- const limit = model.capabilities?.limits?.max_prompt_tokens ?? 128e3;
4120
- consola.info(` Estimated tokens: ${tokenCount.input.toLocaleString()} / ${limit.toLocaleString()}`);
4121
- } catch {}
4122
- if (imageCount > 0) {
4123
- const imageSizeKB = Math.round(totalImageSize / 1024);
4124
- consola.info(` Images: ${imageCount} (${imageSizeKB} KB base64 data)`);
4125
- }
4126
- if (largeMessages > 0) consola.info(` Large messages (>50KB): ${largeMessages}`);
4127
- consola.info("");
4128
- consola.info(" Suggestions:");
4129
- if (!state.autoTruncate) consola.info(" • Enable --auto-truncate to automatically truncate history");
4130
- if (imageCount > 0) consola.info(" • Remove or resize large images in the conversation");
4131
- consola.info(" • Start a new conversation with /clear or /reset");
4132
- consola.info(" • Reduce conversation history by deleting old messages");
4133
- consola.info("");
4368
+ function echoParsedEvent(event, ctx) {
4369
+ return echoModelInParsedEvent(event, requestedModelOf(ctx));
4370
+ }
4371
+
4372
+ //#endregion
4373
+ //#region src/routes/truncation-marker.ts
4374
+ /**
4375
+ * Create a marker to prepend to responses indicating auto-truncation occurred.
4376
+ * Works with both OpenAI and Anthropic truncate results.
4377
+ *
4378
+ * Distinct from the file-private `createTruncationMarker` helpers in
4379
+ * `lib/auto-truncate-{openai,anthropic}.ts` — those build a synthetic upstream
4380
+ * `Message`/`AnthropicMessage` used as a no-system-message fallback inside the
4381
+ * truncate algorithm; this one returns the client-facing display suffix.
4382
+ */
4383
+ function formatClientTruncationMarker(result) {
4384
+ if (!result.wasCompacted) return "";
4385
+ const { originalTokens, compactedTokens, removedMessageCount } = result;
4386
+ if (originalTokens === void 0 || compactedTokens === void 0 || removedMessageCount === void 0) return `\n\n---\n[Auto-truncated: conversation history was reduced to fit context limits]`;
4387
+ const reduction = originalTokens - compactedTokens;
4388
+ return `\n\n---\n[Auto-truncated: ${removedMessageCount} messages removed, ${originalTokens} → ${compactedTokens} tokens (${Math.round(reduction / originalTokens * 100)}% reduction)]`;
4134
4389
  }
4135
4390
 
4136
4391
  //#endregion
@@ -4139,26 +4394,24 @@ function getReasoningTokensFromOpenAIUsage(usage) {
4139
4394
  return usage?.completion_tokens_details?.reasoning_tokens;
4140
4395
  }
4141
4396
  async function handleCompletion$1(c) {
4142
- const originalPayload = await c.req.json();
4143
- consola.debug("Request payload:", JSON.stringify(originalPayload).slice(-400));
4144
- const trackingId = c.get("trackingId");
4145
- const startTime = (trackingId ? requestTracker.getRequest(trackingId) : void 0)?.startTime ?? Date.now();
4146
- updateTrackerModel(trackingId, originalPayload.model);
4147
- const ctx = {
4148
- historyId: recordRequest("openai", {
4149
- model: originalPayload.model,
4150
- messages: convertOpenAIMessages(originalPayload.messages),
4151
- stream: originalPayload.stream ?? false,
4152
- tools: originalPayload.tools?.map((t) => ({
4397
+ const rawPayload = await c.req.json();
4398
+ consola.debug("Request payload:", JSON.stringify(rawPayload).slice(-400));
4399
+ const { ctx, payload: originalPayload } = createEntryContext({
4400
+ c,
4401
+ rawPayload,
4402
+ endpoint: "openai",
4403
+ buildHistoryRequest: (p) => ({
4404
+ model: p.model,
4405
+ messages: convertOpenAIMessages(p.messages),
4406
+ stream: p.stream ?? false,
4407
+ tools: p.tools?.map((t) => ({
4153
4408
  name: t.function.name,
4154
4409
  description: t.function.description
4155
4410
  })),
4156
- max_tokens: originalPayload.max_tokens ?? void 0,
4157
- temperature: originalPayload.temperature ?? void 0
4158
- }),
4159
- trackingId,
4160
- startTime
4161
- };
4411
+ max_tokens: p.max_tokens ?? void 0,
4412
+ temperature: p.temperature ?? void 0
4413
+ })
4414
+ });
4162
4415
  const selectedModel = findModelById(originalPayload.model);
4163
4416
  await logTokenCount(originalPayload, selectedModel);
4164
4417
  const { finalPayload, truncateResult } = await buildFinalPayload(originalPayload, selectedModel);
@@ -4173,21 +4426,20 @@ async function handleCompletion$1(c) {
4173
4426
  c,
4174
4427
  payload,
4175
4428
  selectedModel,
4176
- ctx,
4177
- trackingId
4429
+ ctx
4178
4430
  });
4179
4431
  }
4180
4432
  /**
4181
4433
  * Execute the API call with enhanced error handling for 413 errors.
4182
4434
  */
4183
4435
  async function executeRequest(opts) {
4184
- const { c, payload, selectedModel, ctx, trackingId } = opts;
4436
+ const { c, payload, selectedModel, ctx } = opts;
4185
4437
  try {
4186
4438
  const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(payload, { resolvedModel: selectedModel }));
4187
4439
  ctx.queueWaitMs = queueWaitMs;
4188
4440
  if (isNonStreaming(response)) return handleNonStreamingResponse$1(c, response, ctx, payload);
4189
4441
  consola.debug("Streaming response");
4190
- updateTrackerStatus(trackingId, "streaming");
4442
+ updateTrackerStatus(ctx.trackingId, "streaming");
4191
4443
  return streamSSE(c, async (stream) => {
4192
4444
  await handleStreamingResponse$1({
4193
4445
  stream,
@@ -4217,7 +4469,7 @@ function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
4217
4469
  consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
4218
4470
  let response = originalResponse;
4219
4471
  if (state.verbose && ctx.truncateResult?.wasCompacted && response.choices[0]?.message.content) {
4220
- const marker = createTruncationMarker$1(ctx.truncateResult);
4472
+ const marker = formatClientTruncationMarker(ctx.truncateResult);
4221
4473
  response = {
4222
4474
  ...response,
4223
4475
  choices: response.choices.map((choice, i) => i === 0 ? {
@@ -4262,7 +4514,7 @@ function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
4262
4514
  reasoningTokens,
4263
4515
  stopReason: choice.finish_reason
4264
4516
  });
4265
- return c.json(response);
4517
+ return c.json(echoResponseBody(response, ctx));
4266
4518
  }
4267
4519
  function buildResponseContent(choice) {
4268
4520
  return {
@@ -4303,7 +4555,7 @@ async function handleStreamingResponse$1(opts) {
4303
4555
  const checkRepetition = createStreamRepetitionChecker(`openai:${payload.model}`);
4304
4556
  try {
4305
4557
  if (state.verbose && ctx.truncateResult?.wasCompacted) {
4306
- const marker = createTruncationMarker$1(ctx.truncateResult);
4558
+ const marker = formatClientTruncationMarker(ctx.truncateResult);
4307
4559
  const markerChunk = {
4308
4560
  id: `compact-marker-${Date.now()}`,
4309
4561
  object: "chat.completion.chunk",
@@ -4317,15 +4569,14 @@ async function handleStreamingResponse$1(opts) {
4317
4569
  }]
4318
4570
  };
4319
4571
  await stream.writeSSE({
4320
- data: JSON.stringify(markerChunk),
4572
+ data: JSON.stringify(echoParsedEvent(markerChunk, ctx)),
4321
4573
  event: "message"
4322
4574
  });
4323
4575
  acc.content += marker;
4324
4576
  }
4325
4577
  for await (const chunk of response) {
4326
4578
  consola.debug("Streaming chunk:", JSON.stringify(chunk));
4327
- parseStreamChunk(chunk, acc, checkRepetition);
4328
- await stream.writeSSE(chunk);
4579
+ await accumulateAndEchoChunk(chunk, acc, checkRepetition, ctx, stream);
4329
4580
  }
4330
4581
  recordStreamSuccess(acc, payload.model, ctx);
4331
4582
  completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, acc.reasoningTokens, {
@@ -4358,45 +4609,70 @@ async function handleStreamingResponse$1(opts) {
4358
4609
  }]
4359
4610
  };
4360
4611
  await stream.writeSSE({
4361
- data: JSON.stringify(markerChunk),
4612
+ data: JSON.stringify(echoParsedEvent(markerChunk, ctx)),
4362
4613
  event: "message"
4363
4614
  });
4364
4615
  } catch {}
4365
4616
  }
4366
4617
  }
4367
- function parseStreamChunk(chunk, acc, checkRepetition) {
4368
- if (!chunk.data || chunk.data === "[DONE]") return;
4618
+ async function accumulateAndEchoChunk(chunk, acc, checkRepetition, ctx, stream) {
4619
+ if (!chunk.data || chunk.data === "[DONE]") {
4620
+ await stream.writeSSE(chunk);
4621
+ return;
4622
+ }
4623
+ let parsed;
4369
4624
  try {
4370
- const parsed = JSON.parse(chunk.data);
4371
- if (parsed.model && !acc.model) acc.model = parsed.model;
4372
- if (parsed.usage) {
4373
- acc.inputTokens = parsed.usage.prompt_tokens;
4374
- acc.outputTokens = parsed.usage.completion_tokens;
4375
- acc.reasoningTokens = getReasoningTokensFromOpenAIUsage(parsed.usage) ?? 0;
4376
- }
4377
- const choice = parsed.choices[0];
4378
- if (choice) {
4379
- if (choice.delta.content) {
4380
- acc.content += choice.delta.content;
4381
- checkRepetition(choice.delta.content);
4382
- }
4383
- if (choice.delta.tool_calls) for (const tc of choice.delta.tool_calls) {
4384
- const idx = tc.index;
4385
- if (!acc.toolCallMap.has(idx)) acc.toolCallMap.set(idx, {
4386
- id: tc.id ?? "",
4387
- name: tc.function?.name ?? "",
4388
- arguments: ""
4389
- });
4390
- const item = acc.toolCallMap.get(idx);
4391
- if (item) {
4392
- if (tc.id) item.id = tc.id;
4393
- if (tc.function?.name) item.name = tc.function.name;
4394
- if (tc.function?.arguments) item.arguments += tc.function.arguments;
4395
- }
4625
+ parsed = JSON.parse(chunk.data);
4626
+ } catch {
4627
+ await stream.writeSSE(chunk);
4628
+ return;
4629
+ }
4630
+ if (typeof parsed !== "object" || parsed === null) {
4631
+ await stream.writeSSE(chunk);
4632
+ return;
4633
+ }
4634
+ try {
4635
+ accumulateParsedChunk(parsed, acc, checkRepetition);
4636
+ } catch {}
4637
+ if (!Object.hasOwn(parsed, "model")) {
4638
+ await stream.writeSSE(chunk);
4639
+ return;
4640
+ }
4641
+ const echoed = echoParsedEvent(parsed, ctx);
4642
+ await stream.writeSSE({
4643
+ ...chunk,
4644
+ data: JSON.stringify(echoed)
4645
+ });
4646
+ }
4647
+ function accumulateParsedChunk(parsed, acc, checkRepetition) {
4648
+ if (parsed.model && !acc.model) acc.model = parsed.model;
4649
+ if (parsed.usage) {
4650
+ acc.inputTokens = parsed.usage.prompt_tokens;
4651
+ acc.outputTokens = parsed.usage.completion_tokens;
4652
+ acc.reasoningTokens = getReasoningTokensFromOpenAIUsage(parsed.usage) ?? 0;
4653
+ }
4654
+ const choice = parsed.choices?.[0];
4655
+ if (choice) {
4656
+ if (choice.delta?.content) {
4657
+ acc.content += choice.delta.content;
4658
+ checkRepetition(choice.delta.content);
4659
+ }
4660
+ if (choice.delta?.tool_calls) for (const tc of choice.delta.tool_calls) {
4661
+ const idx = tc.index;
4662
+ if (!acc.toolCallMap.has(idx)) acc.toolCallMap.set(idx, {
4663
+ id: tc.id ?? "",
4664
+ name: tc.function?.name ?? "",
4665
+ arguments: ""
4666
+ });
4667
+ const item = acc.toolCallMap.get(idx);
4668
+ if (item) {
4669
+ if (tc.id) item.id = tc.id;
4670
+ if (tc.function?.name) item.name = tc.function.name;
4671
+ if (tc.function?.arguments) item.arguments += tc.function.arguments;
4396
4672
  }
4397
- if (choice.finish_reason) acc.finishReason = choice.finish_reason;
4398
4673
  }
4399
- } catch {}
4674
+ if (choice.finish_reason) acc.finishReason = choice.finish_reason;
4675
+ }
4400
4676
  }
4401
4677
  function recordStreamSuccess(acc, fallbackModel, ctx) {
4402
4678
  for (const tc of acc.toolCallMap.values()) if (tc.id && tc.name) acc.toolCalls.push(tc);
@@ -4460,31 +4736,6 @@ completionRoutes.post("/", async (c) => {
4460
4736
  }
4461
4737
  });
4462
4738
 
4463
- //#endregion
4464
- //#region src/services/copilot/create-embeddings.ts
4465
- const createEmbeddings = async (payload) => {
4466
- if (!state.copilotToken) throw new Error("Copilot token not found");
4467
- const response = await copilotFetch("/embeddings", {
4468
- method: "POST",
4469
- headers: copilotHeaders(state),
4470
- body: JSON.stringify(payload)
4471
- });
4472
- if (!response.ok) throw await HTTPError.fromResponse("Failed to create embeddings", response);
4473
- return await response.json();
4474
- };
4475
-
4476
- //#endregion
4477
- //#region src/routes/embeddings/route.ts
4478
- const embeddingRoutes = new Hono();
4479
- embeddingRoutes.post("/", async (c) => {
4480
- try {
4481
- const response = await createEmbeddings(await c.req.json());
4482
- return c.json(response);
4483
- } catch (error) {
4484
- return forwardError(c, error);
4485
- }
4486
- });
4487
-
4488
4739
  //#endregion
4489
4740
  //#region src/routes/event-logging/route.ts
4490
4741
  const eventLoggingRoutes = new Hono();
@@ -4492,507 +4743,6 @@ eventLoggingRoutes.post("/batch", (c) => {
4492
4743
  return c.text("OK", 200);
4493
4744
  });
4494
4745
 
4495
- //#endregion
4496
- //#region src/routes/gemini/error.ts
4497
- const STATUS_MAP = {
4498
- 400: "INVALID_ARGUMENT",
4499
- 401: "PERMISSION_DENIED",
4500
- 403: "PERMISSION_DENIED",
4501
- 404: "NOT_FOUND",
4502
- 413: "INVALID_ARGUMENT",
4503
- 429: "RESOURCE_EXHAUSTED",
4504
- 500: "INTERNAL"
4505
- };
4506
- function geminiError(c, code, status, message) {
4507
- return c.json({ error: {
4508
- code,
4509
- message,
4510
- status
4511
- } }, code);
4512
- }
4513
- function forwardGeminiError(c, error) {
4514
- if (error instanceof HTTPError) {
4515
- const status = STATUS_MAP[error.status] ?? "INTERNAL";
4516
- const code = error.status;
4517
- let message = error.responseText;
4518
- try {
4519
- const parsed = JSON.parse(error.responseText);
4520
- if (parsed.error?.message) message = parsed.error.message;
4521
- } catch {}
4522
- consola.error(`HTTP ${code}:`, message.slice(0, 200));
4523
- return geminiError(c, code, status, message);
4524
- }
4525
- consola.error("Unexpected error:", error);
4526
- return geminiError(c, 500, "INTERNAL", error instanceof Error ? error.message : "Unknown error");
4527
- }
4528
-
4529
- //#endregion
4530
- //#region src/routes/gemini/gemini-to-openai.ts
4531
- function translateGeminiToOpenAI(request, model) {
4532
- const messages = [];
4533
- if (request.systemInstruction) {
4534
- const systemText = extractTextFromParts(request.systemInstruction.parts);
4535
- if (systemText) messages.push({
4536
- role: "system",
4537
- content: systemText
4538
- });
4539
- }
4540
- let globalCallIndex = 0;
4541
- const callIdQueue = /* @__PURE__ */ new Map();
4542
- if (!Array.isArray(request.contents)) return { payload: {
4543
- messages: [],
4544
- model
4545
- } };
4546
- for (const content of request.contents) {
4547
- const translated = translateContent(content, callIdQueue, () => `call_gemini_${globalCallIndex++}`);
4548
- messages.push(...translated);
4549
- }
4550
- const payload = {
4551
- messages,
4552
- model
4553
- };
4554
- const config = request.generationConfig;
4555
- if (config) {
4556
- if (config.temperature !== void 0) payload.temperature = config.temperature;
4557
- if (config.topP !== void 0) payload.top_p = config.topP;
4558
- if (config.maxOutputTokens !== void 0) payload.max_tokens = config.maxOutputTokens;
4559
- if (config.stopSequences !== void 0) payload.stop = config.stopSequences;
4560
- if (config.responseMimeType === "application/json") payload.response_format = { type: "json_object" };
4561
- }
4562
- if (request.tools) {
4563
- const tools = translateTools(request.tools);
4564
- if (tools.length > 0) payload.tools = tools;
4565
- }
4566
- if (request.toolConfig?.functionCallingConfig?.mode) payload.tool_choice = {
4567
- AUTO: "auto",
4568
- ANY: "required",
4569
- NONE: "none"
4570
- }[request.toolConfig.functionCallingConfig.mode];
4571
- return { payload };
4572
- }
4573
- function mapFunctionCallsToToolCalls(functionCalls, callIdQueue, generateId) {
4574
- return functionCalls.map((fc) => {
4575
- const id = generateId();
4576
- pushToQueue(callIdQueue, fc.functionCall.name, id);
4577
- return {
4578
- id,
4579
- type: "function",
4580
- function: {
4581
- name: fc.functionCall.name,
4582
- arguments: JSON.stringify(fc.functionCall.args)
4583
- }
4584
- };
4585
- });
4586
- }
4587
- function translateContent(content, callIdQueue, generateId) {
4588
- const role = content.role === "model" ? "assistant" : "user";
4589
- const messages = [];
4590
- const textParts = [];
4591
- const imageParts = [];
4592
- const functionCalls = [];
4593
- const functionResponses = [];
4594
- for (const part of content.parts) if (isTextPart(part)) {
4595
- if (!part.thought) textParts.push(part);
4596
- } else if (isInlineDataPart(part)) imageParts.push(part);
4597
- else if (isFunctionCallPart(part)) functionCalls.push(part);
4598
- else if (isFunctionResponsePart(part)) functionResponses.push(part);
4599
- else if (isFileDataPart(part)) throw new HTTPError("fileData parts are not supported", 400, "fileData parts are not supported");
4600
- if (imageParts.length > 0) {
4601
- const contentParts = [];
4602
- for (const part of content.parts) if (isTextPart(part) && !part.thought) contentParts.push({
4603
- type: "text",
4604
- text: part.text
4605
- });
4606
- else if (isInlineDataPart(part)) contentParts.push({
4607
- type: "image_url",
4608
- image_url: { url: `data:${part.inlineData.mimeType};base64,${part.inlineData.data}` }
4609
- });
4610
- const msg = {
4611
- role,
4612
- content: contentParts
4613
- };
4614
- if (functionCalls.length > 0 && role === "assistant") msg.tool_calls = mapFunctionCallsToToolCalls(functionCalls, callIdQueue, generateId);
4615
- messages.push(msg);
4616
- } else if (functionCalls.length > 0 && role === "assistant") {
4617
- const textContent = textParts.length > 0 ? textParts.map((p) => p.text).join("") : null;
4618
- messages.push({
4619
- role: "assistant",
4620
- content: textContent,
4621
- tool_calls: mapFunctionCallsToToolCalls(functionCalls, callIdQueue, generateId)
4622
- });
4623
- } else if (textParts.length > 0) messages.push({
4624
- role,
4625
- content: textParts.map((p) => p.text).join("")
4626
- });
4627
- let orphanIndex = 0;
4628
- for (const fr of functionResponses) {
4629
- const queue = callIdQueue.get(fr.functionResponse.name);
4630
- const id = queue && queue.length > 0 ? queue.shift() : `call_gemini_orphan_${orphanIndex++}`;
4631
- messages.push({
4632
- role: "tool",
4633
- content: JSON.stringify(fr.functionResponse.response),
4634
- tool_call_id: id
4635
- });
4636
- }
4637
- return messages;
4638
- }
4639
- function translateTools(geminiTools) {
4640
- const tools = [];
4641
- for (const tool of geminiTools) if (tool.functionDeclarations) for (const decl of tool.functionDeclarations) tools.push({
4642
- type: "function",
4643
- function: {
4644
- name: decl.name,
4645
- description: decl.description,
4646
- parameters: decl.parameters ?? {
4647
- type: "object",
4648
- properties: {}
4649
- }
4650
- }
4651
- });
4652
- return tools;
4653
- }
4654
- function pushToQueue(queue, name, id) {
4655
- const existing = queue.get(name);
4656
- if (existing) existing.push(id);
4657
- else queue.set(name, [id]);
4658
- }
4659
- function extractTextFromParts(parts) {
4660
- return parts.filter((p) => "text" in p && (!("thought" in p) || !p.thought)).map((p) => p.text).join("\n");
4661
- }
4662
- function isTextPart(part) {
4663
- return "text" in part;
4664
- }
4665
- function isInlineDataPart(part) {
4666
- return "inlineData" in part;
4667
- }
4668
- function isFunctionCallPart(part) {
4669
- return "functionCall" in part;
4670
- }
4671
- function isFunctionResponsePart(part) {
4672
- return "functionResponse" in part;
4673
- }
4674
- function isFileDataPart(part) {
4675
- return "fileData" in part;
4676
- }
4677
-
4678
- //#endregion
4679
- //#region src/routes/gemini/count-tokens-handler.ts
4680
- async function handleGeminiCountTokens(c, model) {
4681
- try {
4682
- const { payload } = translateGeminiToOpenAI(await c.req.json(), model);
4683
- const selectedModel = findModelById(model);
4684
- if (!selectedModel) {
4685
- consola.warn("Model not found for count_tokens, returning estimate");
4686
- return c.json({ totalTokens: 1 });
4687
- }
4688
- const tokenCount = await getTokenCount(payload, selectedModel);
4689
- const totalTokens = tokenCount.input + tokenCount.output;
4690
- consola.debug(`Gemini countTokens: ${totalTokens} tokens`);
4691
- return c.json({ totalTokens });
4692
- } catch (error) {
4693
- return forwardGeminiError(c, error);
4694
- }
4695
- }
4696
-
4697
- //#endregion
4698
- //#region src/routes/gemini/openai-to-gemini.ts
4699
- function translateOpenAIResponseToGemini(response, model) {
4700
- const choice = response.choices.at(0);
4701
- if (!choice) return {
4702
- candidates: [],
4703
- usageMetadata: buildUsageMetadata(response.usage),
4704
- modelVersion: model
4705
- };
4706
- const parts = [];
4707
- if (choice.message.content) parts.push({ text: choice.message.content });
4708
- if (choice.message.tool_calls) for (const tc of choice.message.tool_calls) {
4709
- const args = parseArgs(tc.function.arguments);
4710
- parts.push({ functionCall: {
4711
- name: tc.function.name,
4712
- args
4713
- } });
4714
- }
4715
- if (parts.length === 0) parts.push({ text: "" });
4716
- return {
4717
- candidates: [{
4718
- content: {
4719
- role: "model",
4720
- parts
4721
- },
4722
- finishReason: mapFinishReason(choice.finish_reason),
4723
- index: 0
4724
- }],
4725
- usageMetadata: buildUsageMetadata(response.usage),
4726
- modelVersion: model
4727
- };
4728
- }
4729
- function createGeminiStreamState() {
4730
- return {
4731
- toolCalls: /* @__PURE__ */ new Map(),
4732
- usage: {
4733
- promptTokens: 0,
4734
- completionTokens: 0,
4735
- totalTokens: 0
4736
- },
4737
- model: "",
4738
- finishReason: ""
4739
- };
4740
- }
4741
- function translateOpenAIChunkToGemini(chunk, state) {
4742
- const results = [];
4743
- if (!state.model && chunk.model) state.model = chunk.model;
4744
- if (chunk.usage) {
4745
- state.usage.promptTokens = chunk.usage.prompt_tokens;
4746
- state.usage.completionTokens = chunk.usage.completion_tokens;
4747
- state.usage.totalTokens = chunk.usage.total_tokens;
4748
- }
4749
- const choice = chunk.choices.at(0);
4750
- if (!choice) return results;
4751
- const delta = choice.delta;
4752
- if (delta.tool_calls) for (const tc of delta.tool_calls) {
4753
- const existing = state.toolCalls.get(tc.index);
4754
- if (existing) {
4755
- if (tc.function?.arguments) existing.args += tc.function.arguments;
4756
- } else {
4757
- const flushed = flushToolCalls(state, tc.index);
4758
- if (flushed) results.push(flushed);
4759
- state.toolCalls.set(tc.index, {
4760
- name: tc.function?.name ?? "",
4761
- args: tc.function?.arguments ?? ""
4762
- });
4763
- }
4764
- }
4765
- if (delta.content) results.push(buildGeminiChunk([{ text: delta.content }], choice.finish_reason, state));
4766
- if (choice.finish_reason) {
4767
- state.finishReason = choice.finish_reason;
4768
- const flushed = flushToolCalls(state);
4769
- if (flushed) results.push(flushed);
4770
- if (!delta.content) results.push(buildGeminiChunk([], choice.finish_reason, state));
4771
- }
4772
- return results;
4773
- }
4774
- function flushToolCalls(state, belowIndex) {
4775
- if (state.toolCalls.size === 0) return null;
4776
- const parts = [];
4777
- for (const [idx, tc] of state.toolCalls) {
4778
- if (belowIndex !== void 0 && idx >= belowIndex) continue;
4779
- const args = parseArgs(tc.args);
4780
- parts.push({ functionCall: {
4781
- name: tc.name,
4782
- args
4783
- } });
4784
- state.toolCalls.delete(idx);
4785
- }
4786
- if (parts.length === 0) return null;
4787
- return buildGeminiChunk(parts, null, state);
4788
- }
4789
- function buildGeminiChunk(parts, finishReason, state) {
4790
- const candidate = {
4791
- content: {
4792
- role: "model",
4793
- parts: parts.length > 0 ? parts : [{ text: "" }]
4794
- },
4795
- index: 0
4796
- };
4797
- if (finishReason) candidate.finishReason = mapFinishReason(finishReason);
4798
- return {
4799
- candidates: [candidate],
4800
- usageMetadata: {
4801
- promptTokenCount: state.usage.promptTokens,
4802
- candidatesTokenCount: state.usage.completionTokens,
4803
- totalTokenCount: state.usage.totalTokens
4804
- },
4805
- modelVersion: state.model
4806
- };
4807
- }
4808
- function parseArgs(raw) {
4809
- try {
4810
- return JSON.parse(raw);
4811
- } catch {
4812
- return { raw };
4813
- }
4814
- }
4815
- function mapFinishReason(reason) {
4816
- switch (reason) {
4817
- case "stop":
4818
- case "tool_calls": return "STOP";
4819
- case "length": return "MAX_TOKENS";
4820
- case "content_filter": return "SAFETY";
4821
- default: return "OTHER";
4822
- }
4823
- }
4824
- function buildUsageMetadata(usage) {
4825
- return {
4826
- promptTokenCount: usage?.prompt_tokens ?? 0,
4827
- candidatesTokenCount: usage?.completion_tokens ?? 0,
4828
- totalTokenCount: usage?.total_tokens ?? 0
4829
- };
4830
- }
4831
-
4832
- //#endregion
4833
- //#region src/routes/gemini/handler.ts
4834
- async function handleGeminiGenerate(c, model, isStream) {
4835
- try {
4836
- const geminiRequest = await c.req.json();
4837
- consola.debug("Gemini request for model:", model, "stream:", isStream);
4838
- const trackingId = c.get("trackingId");
4839
- const startTime = Date.now();
4840
- updateTrackerModel(trackingId, model);
4841
- const { payload } = translateGeminiToOpenAI(geminiRequest, model);
4842
- payload.stream = isStream;
4843
- const selectedModel = findModelById(model);
4844
- if (isNullish(payload.max_tokens) && selectedModel) payload.max_tokens = selectedModel.capabilities?.limits?.max_output_tokens;
4845
- const ctx = {
4846
- historyId: recordRequest("gemini", {
4847
- model,
4848
- messages: payload.messages.map((m) => ({
4849
- role: m.role,
4850
- content: typeof m.content === "string" ? m.content : JSON.stringify(m.content),
4851
- tool_calls: m.tool_calls,
4852
- tool_call_id: m.tool_call_id
4853
- })),
4854
- stream: isStream,
4855
- max_tokens: payload.max_tokens ?? void 0,
4856
- temperature: payload.temperature ?? void 0
4857
- }),
4858
- trackingId,
4859
- startTime
4860
- };
4861
- const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(payload, { resolvedModel: selectedModel }));
4862
- ctx.queueWaitMs = queueWaitMs;
4863
- if (isNonStreaming(response)) return handleNonStreamResponse(c, response, model, ctx, payload);
4864
- consola.debug("Streaming Gemini response");
4865
- updateTrackerStatus(trackingId, "streaming");
4866
- return stream(c, async (s) => {
4867
- c.header("Content-Type", "text/event-stream");
4868
- c.header("Cache-Control", "no-cache");
4869
- c.header("Connection", "keep-alive");
4870
- const streamState = createGeminiStreamState();
4871
- try {
4872
- for await (const rawEvent of response) {
4873
- if (rawEvent.data === "[DONE]") break;
4874
- let chunk;
4875
- try {
4876
- chunk = JSON.parse(rawEvent.data);
4877
- } catch (parseError) {
4878
- consola.debug("Failed to parse stream chunk:", parseError);
4879
- continue;
4880
- }
4881
- const geminiChunks = translateOpenAIChunkToGemini(chunk, streamState);
4882
- for (const gc of geminiChunks) await s.write(`data: ${JSON.stringify(gc)}\n\n`);
4883
- }
4884
- recordResponse(ctx.historyId, {
4885
- success: true,
4886
- model: streamState.model || model,
4887
- usage: {
4888
- input_tokens: streamState.usage.promptTokens,
4889
- output_tokens: streamState.usage.completionTokens
4890
- },
4891
- content: null
4892
- }, Date.now() - ctx.startTime);
4893
- completeTracking(ctx.trackingId, streamState.usage.promptTokens, streamState.usage.completionTokens, ctx.queueWaitMs, void 0, {
4894
- model: streamState.model || model,
4895
- stream: true,
4896
- durationMs: Date.now() - ctx.startTime,
4897
- stopReason: streamState.finishReason || void 0,
4898
- toolCount: payload.tools?.length ?? 0
4899
- });
4900
- } catch (error) {
4901
- recordStreamError({
4902
- acc: { model: streamState.model || model },
4903
- fallbackModel: model,
4904
- ctx,
4905
- error,
4906
- endpoint: "chat_completions"
4907
- });
4908
- failTracking(ctx.trackingId, error);
4909
- try {
4910
- await s.write(`data: ${JSON.stringify({ candidates: [{
4911
- content: {
4912
- role: "model",
4913
- parts: [{ text: `\n\n[copilot-api: upstream stream terminated. Please retry.]` }]
4914
- },
4915
- finishReason: "OTHER",
4916
- index: 0
4917
- }] })}\n\n`);
4918
- } catch {}
4919
- }
4920
- });
4921
- } catch (error) {
4922
- const trackingId = c.get("trackingId");
4923
- if (trackingId) failTracking(trackingId, error);
4924
- return forwardGeminiError(c, error);
4925
- }
4926
- }
4927
- function handleNonStreamResponse(c, response, model, ctx, payload) {
4928
- const geminiResponse = translateOpenAIResponseToGemini(response, model);
4929
- const usage = response.usage;
4930
- recordResponse(ctx.historyId, {
4931
- success: true,
4932
- model: response.model || model,
4933
- usage: {
4934
- input_tokens: usage?.prompt_tokens ?? 0,
4935
- output_tokens: usage?.completion_tokens ?? 0
4936
- },
4937
- stop_reason: response.choices[0]?.finish_reason,
4938
- content: response.choices[0] ? {
4939
- role: "assistant",
4940
- content: response.choices[0].message.content ?? ""
4941
- } : null
4942
- }, Date.now() - ctx.startTime);
4943
- completeTracking(ctx.trackingId, usage?.prompt_tokens ?? 0, usage?.completion_tokens ?? 0, ctx.queueWaitMs, void 0, {
4944
- model: response.model || model,
4945
- stream: false,
4946
- durationMs: Date.now() - ctx.startTime,
4947
- stopReason: response.choices[0]?.finish_reason,
4948
- toolCount: payload.tools?.length ?? 0
4949
- });
4950
- return c.json(geminiResponse);
4951
- }
4952
-
4953
- //#endregion
4954
- //#region src/routes/gemini/model-alias.ts
4955
- /**
4956
- * Maps Gemini model names to equivalent models available on GitHub Copilot.
4957
- *
4958
- * Two types of aliases:
4959
- *
4960
- * - **Forced**: Always applied regardless of Copilot model availability.
4961
- * Use when the old model name should never reach the backend.
4962
- *
4963
- * - **Conditional**: Only applied when the requested model is absent from
4964
- * the Copilot model list, so if Copilot adds native support the request
4965
- * goes through unchanged.
4966
- */
4967
- const GEMINI_FORCED_ALIASES = { "gemini-3.1-pro-preview-customtools": "gemini-3.1-pro-preview" };
4968
- const GEMINI_CONDITIONAL_ALIASES = {
4969
- "gemini-2.5-flash-lite": "gemini-3.5-flash",
4970
- "gemini-2.5-flash": "gemini-3.5-flash"
4971
- };
4972
- function resolveGeminiModelAlias(model) {
4973
- if (model in GEMINI_FORCED_ALIASES) return GEMINI_FORCED_ALIASES[model];
4974
- if (!(model in GEMINI_CONDITIONAL_ALIASES)) return model;
4975
- if (findModelById(model)) return model;
4976
- return GEMINI_CONDITIONAL_ALIASES[model];
4977
- }
4978
-
4979
- //#endregion
4980
- //#region src/routes/gemini/route.ts
4981
- const geminiRoutes = new Hono();
4982
- geminiRoutes.post("/:modelAction", async (c) => {
4983
- const modelAction = c.req.param("modelAction");
4984
- const colonIndex = modelAction.lastIndexOf(":");
4985
- if (colonIndex === -1) return geminiError(c, 400, "INVALID_ARGUMENT", "Missing action in URL");
4986
- const model = resolveGeminiModelAlias(modelAction.slice(0, Math.max(0, colonIndex)));
4987
- const action = modelAction.slice(Math.max(0, colonIndex + 1));
4988
- switch (action) {
4989
- case "generateContent": return handleGeminiGenerate(c, model, false);
4990
- case "streamGenerateContent": return handleGeminiGenerate(c, model, true);
4991
- case "countTokens": return handleGeminiCountTokens(c, model);
4992
- default: return geminiError(c, 400, "INVALID_ARGUMENT", `Unknown action: ${action}`);
4993
- }
4994
- });
4995
-
4996
4746
  //#endregion
4997
4747
  //#region src/routes/history/api.ts
4998
4748
  function handleGetEntries(c) {
@@ -8064,10 +7814,43 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
8064
7814
  stopReason: response.stop_reason ?? void 0
8065
7815
  });
8066
7816
  let finalResponse = response;
8067
- if (state.verbose && truncateResult?.wasCompacted) finalResponse = prependMarkerToResponse(response, createTruncationMarker$1(truncateResult));
7817
+ if (state.verbose && truncateResult?.wasCompacted) finalResponse = prependMarkerToResponse(response, formatClientTruncationMarker(truncateResult));
8068
7818
  logServerToolBlocks(finalResponse.content);
8069
7819
  finalResponse = filterServerToolBlocksFromResponse(finalResponse);
8070
- return c.json(finalResponse);
7820
+ return c.json(echoResponseBody(finalResponse, ctx));
7821
+ }
7822
+ /**
7823
+ * Echo the requester's original model id into a single already-serialized
7824
+ * native-Anthropic SSE `data:` payload at the write-out boundary.
7825
+ *
7826
+ * In the native Anthropic stream only `message_start` carries a model field
7827
+ * (`message.model`), so every other event type is forwarded byte-for-byte —
7828
+ * this both avoids a needless re-parse per text delta and guarantees events
7829
+ * with no model field round-trip unchanged. For `message_start`, the payload
7830
+ * is parsed and run through the shared echo policy point; the re-serialized
7831
+ * form is returned ONLY when the echo actually rewrote the object (identity
7832
+ * change). When nothing was rewritten — a model-less `message_start`, or
7833
+ * `context-missing` R — the original `forwardData` bytes are returned verbatim
7834
+ * so a passthrough never re-minifies / re-orders the upstream frame. If the
7835
+ * payload fails to parse (malformed upstream frame), the original string is
7836
+ * likewise returned untouched so the stream is never corrupted or interrupted
7837
+ * (AC-MALFORMED-SSE).
7838
+ *
7839
+ * Operates on the serialized output of the server-tool rewrite (not the raw
7840
+ * upstream frame), so the echo composes with any index remap that step made.
7841
+ */
7842
+ function echoForwardData(forwardData, eventType, ctx) {
7843
+ if (eventType !== "message_start") return forwardData;
7844
+ let parsed;
7845
+ try {
7846
+ parsed = JSON.parse(forwardData);
7847
+ } catch {
7848
+ return forwardData;
7849
+ }
7850
+ if (typeof parsed !== "object" || parsed === null) return forwardData;
7851
+ const echoed = echoParsedEvent(parsed, ctx);
7852
+ if (echoed === parsed) return forwardData;
7853
+ return JSON.stringify(echoed);
8071
7854
  }
8072
7855
  /**
8073
7856
  * Handle streaming direct Anthropic response (passthrough SSE events)
@@ -8094,9 +7877,10 @@ async function handleDirectAnthropicStreamingResponse(opts) {
8094
7877
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") checkRepetition(event.delta.text);
8095
7878
  const forwardData = serverToolFilter.rewriteEvent(event, rawEvent.data);
8096
7879
  if (forwardData === null) continue;
7880
+ const echoedData = echoForwardData(forwardData, event.type, ctx);
8097
7881
  await stream.writeSSE({
8098
7882
  event: rawEvent.event || event.type,
8099
- data: forwardData
7883
+ data: echoedData
8100
7884
  });
8101
7885
  }
8102
7886
  recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
@@ -8235,7 +8019,7 @@ function handleNonStreamingResponse(opts) {
8235
8019
  let anthropicResponse = translateToAnthropic(response, toolNameMapping);
8236
8020
  consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
8237
8021
  if (state.verbose && ctx.truncateResult?.wasCompacted) {
8238
- const marker = createTruncationMarker$1(ctx.truncateResult);
8022
+ const marker = formatClientTruncationMarker(ctx.truncateResult);
8239
8023
  anthropicResponse = prependMarkerToResponse(anthropicResponse, marker);
8240
8024
  }
8241
8025
  recordResponse(ctx.historyId, {
@@ -8276,7 +8060,7 @@ function handleNonStreamingResponse(opts) {
8276
8060
  toolCount: anthropicPayload.tools?.length ?? 0,
8277
8061
  stopReason: anthropicResponse.stop_reason ?? void 0
8278
8062
  });
8279
- return c.json(anthropicResponse);
8063
+ return c.json(echoResponseBody(anthropicResponse, ctx));
8280
8064
  }
8281
8065
  async function handleStreamingResponse(opts) {
8282
8066
  const { stream, response, toolNameMapping, anthropicPayload, ctx } = opts;
@@ -8290,7 +8074,7 @@ async function handleStreamingResponse(opts) {
8290
8074
  const checkRepetition = createStreamRepetitionChecker(`translated:${anthropicPayload.model}`);
8291
8075
  try {
8292
8076
  if (ctx.truncateResult?.wasCompacted) {
8293
- const marker = createTruncationMarker$1(ctx.truncateResult);
8077
+ const marker = formatClientTruncationMarker(ctx.truncateResult);
8294
8078
  await sendTruncationMarkerEvent(stream, streamState, marker);
8295
8079
  acc.content += marker;
8296
8080
  }
@@ -8300,7 +8084,8 @@ async function handleStreamingResponse(opts) {
8300
8084
  toolNameMapping,
8301
8085
  streamState,
8302
8086
  acc,
8303
- checkRepetition
8087
+ checkRepetition,
8088
+ ctx
8304
8089
  });
8305
8090
  recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
8306
8091
  completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, void 0, {
@@ -8363,7 +8148,7 @@ async function sendTruncationMarkerEvent(stream, streamState, marker) {
8363
8148
  streamState.contentBlockIndex++;
8364
8149
  }
8365
8150
  async function processStreamChunks(opts) {
8366
- const { stream, response, toolNameMapping, streamState, acc, checkRepetition } = opts;
8151
+ const { stream, response, toolNameMapping, streamState, acc, checkRepetition, ctx } = opts;
8367
8152
  for await (const rawEvent of response) {
8368
8153
  consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
8369
8154
  if (rawEvent.data === "[DONE]") break;
@@ -8381,9 +8166,10 @@ async function processStreamChunks(opts) {
8381
8166
  consola.debug("Translated Anthropic event:", JSON.stringify(event));
8382
8167
  processAnthropicEvent(event, acc);
8383
8168
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") checkRepetition(event.delta.text);
8169
+ const echoed = echoParsedEvent(event, ctx);
8384
8170
  await stream.writeSSE({
8385
- event: event.type,
8386
- data: JSON.stringify(event)
8171
+ event: echoed.type,
8172
+ data: JSON.stringify(echoed)
8387
8173
  });
8388
8174
  }
8389
8175
  }
@@ -8400,35 +8186,34 @@ function resolveModelFromBetaHeader(model, betaHeader) {
8400
8186
  return resolved;
8401
8187
  }
8402
8188
  async function handleCompletion(c) {
8403
- const anthropicPayload = await c.req.json();
8404
- consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
8405
- const betaHeader = c.req.header("anthropic-beta");
8406
- anthropicPayload.model = resolveModelFromBetaHeader(anthropicPayload.model, betaHeader);
8189
+ const rawPayload = await c.req.json();
8190
+ consola.debug("Anthropic request payload:", JSON.stringify(rawPayload));
8191
+ const { ctx, payload: anthropicPayload } = createEntryContext({
8192
+ c,
8193
+ rawPayload,
8194
+ endpoint: "anthropic",
8195
+ normalizePayload: (p) => ({
8196
+ ...p,
8197
+ model: resolveModelFromBetaHeader(p.model, c.req.header("anthropic-beta"))
8198
+ }),
8199
+ buildHistoryRequest: (p) => ({
8200
+ model: p.model,
8201
+ messages: convertAnthropicMessages(p.messages),
8202
+ stream: p.stream ?? false,
8203
+ tools: p.tools?.map((t) => ({
8204
+ name: t.name,
8205
+ description: t.description
8206
+ })),
8207
+ max_tokens: p.max_tokens,
8208
+ temperature: p.temperature,
8209
+ system: extractSystemPrompt(p.system)
8210
+ })
8211
+ });
8407
8212
  logToolInfo(anthropicPayload);
8408
8213
  const subagentMarker = parseSubagentMarkerFromFirstUser(anthropicPayload);
8409
8214
  const initiatorOverride = subagentMarker ? "agent" : void 0;
8410
8215
  if (subagentMarker) consola.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
8411
- const useDirectAnthropicApi = supportsDirectAnthropicApi(anthropicPayload.model);
8412
- const trackingId = c.get("trackingId");
8413
- const startTime = (trackingId ? requestTracker.getRequest(trackingId) : void 0)?.startTime ?? Date.now();
8414
- updateTrackerModel(trackingId, anthropicPayload.model);
8415
- const ctx = {
8416
- historyId: recordRequest("anthropic", {
8417
- model: anthropicPayload.model,
8418
- messages: convertAnthropicMessages(anthropicPayload.messages),
8419
- stream: anthropicPayload.stream ?? false,
8420
- tools: anthropicPayload.tools?.map((t) => ({
8421
- name: t.name,
8422
- description: t.description
8423
- })),
8424
- max_tokens: anthropicPayload.max_tokens,
8425
- temperature: anthropicPayload.temperature,
8426
- system: extractSystemPrompt(anthropicPayload.system)
8427
- }),
8428
- trackingId,
8429
- startTime
8430
- };
8431
- if (useDirectAnthropicApi) return handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiatorOverride);
8216
+ if (supportsDirectAnthropicApi(anthropicPayload.model)) return handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiatorOverride);
8432
8217
  return handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOverride);
8433
8218
  }
8434
8219
  /**
@@ -8528,7 +8313,7 @@ const modelRoutes = new Hono();
8528
8313
  modelRoutes.get("/", async (c) => {
8529
8314
  try {
8530
8315
  if (!state.models) await cacheModels();
8531
- const models = state.models?.data.map((model) => ({
8316
+ const models = state.models?.data.filter((model) => !isHiddenModel(model.id, state.showAllModels)).map((model) => ({
8532
8317
  id: model.id,
8533
8318
  object: "model",
8534
8319
  type: "model",
@@ -8590,13 +8375,23 @@ const createResponses = async (payload, { vision, initiator, resolvedModel }) =>
8590
8375
  //#endregion
8591
8376
  //#region src/routes/responses/stream-id-sync.ts
8592
8377
  const createStreamIdTracker = () => ({ outputItems: /* @__PURE__ */ new Map() });
8593
- const fixStreamIds = (data, event, tracker) => {
8378
+ /**
8379
+ * Rewrite a single Responses SSE event's `data`: synchronize item ids AND echo
8380
+ * the requester's model id into any nested `response.model` the event carries,
8381
+ * from one parse/serialize. Events whose payload has no `response.model` (text
8382
+ * deltas, the `error` event, …) round-trip with their model untouched
8383
+ * (AC-MALFORMED-SSE); unparseable/empty `data` is forwarded byte-for-byte.
8384
+ *
8385
+ * The echo is applied AFTER the handler's history/tracking has read the upstream
8386
+ * values from the same chunks, preserving the observability split (AC-OBS).
8387
+ */
8388
+ const fixStreamIds = (data, event, tracker, requestedModel) => {
8594
8389
  if (!data) return data;
8595
- const parsed = JSON.parse(data);
8390
+ const echoed = echoModelInParsedEvent(JSON.parse(data), requestedModel);
8596
8391
  switch (event) {
8597
- case "response.output_item.added": return handleOutputItemAdded(parsed, tracker);
8598
- case "response.output_item.done": return handleOutputItemDone(parsed, tracker);
8599
- default: return handleItemId(parsed, tracker);
8392
+ case "response.output_item.added": return handleOutputItemAdded(echoed, tracker);
8393
+ case "response.output_item.done": return handleOutputItemDone(echoed, tracker);
8394
+ default: return handleItemId(echoed, tracker);
8600
8395
  }
8601
8396
  };
8602
8397
  const handleOutputItemAdded = (parsed, tracker) => {
@@ -8808,31 +8603,34 @@ const TERMINAL_EVENTS = new Set([
8808
8603
  "error"
8809
8604
  ]);
8810
8605
  const handleResponses = async (c) => {
8811
- let payload = await c.req.json();
8812
- if (state.normalizeResponsesCallIds) payload = normalizeCallIds(payload);
8606
+ const { ctx, payload } = createEntryContext({
8607
+ c,
8608
+ rawPayload: await c.req.json(),
8609
+ endpoint: "openai",
8610
+ normalizePayload: (p) => {
8611
+ const np = state.normalizeResponsesCallIds ? normalizeCallIds(p) : p;
8612
+ useFunctionApplyPatch(np);
8613
+ removeWebSearchTool(np);
8614
+ return np;
8615
+ },
8616
+ buildHistoryRequest: (p) => {
8617
+ const historyTools = convertResponsesToolsToDefinitions(p.tools);
8618
+ return {
8619
+ model: p.model,
8620
+ messages: convertResponsesInputToMessages(p.input),
8621
+ stream: p.stream ?? false,
8622
+ tools: historyTools.length > 0 ? historyTools : void 0,
8623
+ max_tokens: p.max_output_tokens ?? void 0,
8624
+ temperature: p.temperature ?? void 0,
8625
+ system: p.instructions ?? void 0
8626
+ };
8627
+ }
8628
+ });
8813
8629
  consola.debug("Responses request payload:", JSON.stringify(payload));
8814
- const trackingId = c.get("trackingId");
8815
- const startTime = (trackingId ? requestTracker.getRequest(trackingId) : void 0)?.startTime ?? Date.now();
8816
- updateTrackerModel(trackingId, payload.model);
8817
- useFunctionApplyPatch(payload);
8818
- removeWebSearchTool(payload);
8819
8630
  const model = payload.model;
8820
8631
  const stream = payload.stream ?? false;
8821
8632
  const tools = convertResponsesToolsToDefinitions(payload.tools);
8822
- const historyId = recordRequest("openai", {
8823
- model,
8824
- messages: convertResponsesInputToMessages(payload.input),
8825
- stream,
8826
- tools: tools.length > 0 ? tools : void 0,
8827
- max_tokens: payload.max_output_tokens ?? void 0,
8828
- temperature: payload.temperature ?? void 0,
8829
- system: payload.instructions ?? void 0
8830
- });
8831
- const ctx = {
8832
- historyId,
8833
- trackingId,
8834
- startTime
8835
- };
8633
+ const { historyId, trackingId, startTime } = ctx;
8836
8634
  const selectedModel = findModelById(payload.model);
8837
8635
  if (!(selectedModel?.supported_endpoints?.includes(RESPONSES_ENDPOINT) ?? false)) {
8838
8636
  recordErrorResponse(ctx, model, /* @__PURE__ */ new Error("This model does not support the responses endpoint."), "responses", stream);
@@ -8872,7 +8670,7 @@ const handleResponses = async (c) => {
8872
8670
  const parsed = JSON.parse(rawData);
8873
8671
  if (typeof parsed.sequence_number === "number") lastSequenceNumber = parsed.sequence_number;
8874
8672
  } catch {}
8875
- const processedData = fixStreamIds(rawData, eventType, idTracker);
8673
+ const processedData = fixStreamIds(rawData, eventType, idTracker, requestedModelOf(ctx));
8876
8674
  await stream.writeSSE({
8877
8675
  id: chunk.id,
8878
8676
  event: eventType,
@@ -8935,7 +8733,7 @@ const handleResponses = async (c) => {
8935
8733
  toolCount: tools.length
8936
8734
  });
8937
8735
  consola.debug("Forwarding native Responses result:", JSON.stringify(result).slice(-400));
8938
- return c.json(result);
8736
+ return c.json(echoResponseBody(result, ctx));
8939
8737
  } catch (error) {
8940
8738
  recordErrorResponse(ctx, model, error, "responses", stream);
8941
8739
  failTracking(trackingId, error);
@@ -9046,17 +8844,14 @@ server.get("/health", (c) => {
9046
8844
  });
9047
8845
  server.route("/chat/completions", completionRoutes);
9048
8846
  server.route("/models", modelRoutes);
9049
- server.route("/embeddings", embeddingRoutes);
9050
8847
  server.route("/usage", usageRoute);
9051
8848
  server.route("/token", tokenRoute);
9052
8849
  server.route("/v1/chat/completions", completionRoutes);
9053
8850
  server.route("/v1/models", modelRoutes);
9054
- server.route("/v1/embeddings", embeddingRoutes);
9055
8851
  server.route("/v1/messages", messageRoutes);
9056
8852
  server.route("/api/event_logging", eventLoggingRoutes);
9057
8853
  server.route("/v1/responses", responsesRoutes);
9058
8854
  server.route("/responses", responsesRoutes);
9059
- server.route("/v1beta/models", geminiRoutes);
9060
8855
  server.route("/history", historyRoutes);
9061
8856
 
9062
8857
  //#endregion
@@ -9104,6 +8899,8 @@ async function runServer(options) {
9104
8899
  if (options.accountType !== "individual") consola.info(`Using ${options.accountType} plan GitHub account`);
9105
8900
  state.manualApprove = options.manual;
9106
8901
  state.showToken = options.showToken;
8902
+ state.showAllModels = options.showAllModels;
8903
+ if (options.showAllModels) consola.warn("--show-all-models: hidden model blacklist is BYPASSED for this run");
9107
8904
  state.autoTruncate = options.autoTruncate;
9108
8905
  state.compressToolResults = options.compressToolResults;
9109
8906
  state.redirectAnthropic = options.redirectAnthropic;
@@ -9148,17 +8945,27 @@ async function runServer(options) {
9148
8945
  consola.error(error instanceof Error ? error.message : String(error));
9149
8946
  process.exit(1);
9150
8947
  }
9151
- consola.info(`Available models:\n${state.models?.data.map((m) => formatModelInfo(m)).join("\n")}`);
8948
+ const allModels = state.models?.data ?? [];
8949
+ if (allModels.length === 0) {
8950
+ consola.error(`Upstream returned zero models for account type "${state.accountType}". Verify the account type matches your Copilot plan and that upstream is reachable.`);
8951
+ process.exit(1);
8952
+ }
8953
+ const visibleModels = allModels.filter((m) => !isHiddenModel(m.id, state.showAllModels));
8954
+ if (visibleModels.length === 0) consola.warn("All upstream models are filtered by the hardcoded blacklist. /v1/models will return an empty list, but explicit POSTs with a hidden id still pass through to upstream. Restart with --show-all-models to see the full catalogue.");
8955
+ else consola.info(`Available models:\n${visibleModels.map((m) => formatModelInfo(m)).join("\n")}`);
9152
8956
  const serverUrl = `http://${options.host ?? "localhost"}:${options.port}`;
9153
8957
  if (options.claudeCode) {
9154
- invariant(state.models, "Models should be loaded by now");
8958
+ if (visibleModels.length === 0) {
8959
+ consola.error("--claude-code interactive setup needs at least one visible model. Restart with --show-all-models or update src/lib/hidden-models.ts.");
8960
+ process.exit(1);
8961
+ }
9155
8962
  const selectedModel = await consola.prompt("Select a model to use with Claude Code", {
9156
8963
  type: "select",
9157
- options: state.models.data.map((model) => model.id)
8964
+ options: visibleModels.map((model) => model.id)
9158
8965
  });
9159
8966
  const selectedSmallModel = await consola.prompt("Select a small model to use with Claude Code", {
9160
8967
  type: "select",
9161
- options: state.models.data.map((model) => model.id)
8968
+ options: visibleModels.map((model) => model.id)
9162
8969
  });
9163
8970
  const command = generateEnvScript({
9164
8971
  ANTHROPIC_BASE_URL: serverUrl,
@@ -9278,6 +9085,11 @@ const start = defineCommand({
9278
9085
  default: false,
9279
9086
  description: "Show GitHub and Copilot tokens on fetch and refresh"
9280
9087
  },
9088
+ "show-all-models": {
9089
+ type: "boolean",
9090
+ default: false,
9091
+ description: "Show ALL upstream models, including the hardcoded blacklist (default: false, blacklist filtered from listings)"
9092
+ },
9281
9093
  "proxy-env": {
9282
9094
  type: "boolean",
9283
9095
  default: false,
@@ -9343,6 +9155,7 @@ const start = defineCommand({
9343
9155
  githubToken: args["github-token"],
9344
9156
  claudeCode: args["claude-code"],
9345
9157
  showToken: args["show-token"],
9158
+ showAllModels: args["show-all-models"],
9346
9159
  proxyEnv: args["proxy-env"],
9347
9160
  history: !args["no-history"],
9348
9161
  historyLimit: Number.parseInt(args["history-limit"], 10),