@dianshuv/copilot-api 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +32 -29
  2. package/dist/main.mjs +801 -1265
  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.9.0";
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
  /**
@@ -3042,158 +3092,6 @@ const awaitApproval = async () => {
3042
3092
  if (!await consola.prompt(`Accept incoming request?`, { type: "confirm" })) throw new HTTPError("Request rejected", 403, JSON.stringify({ message: "Request rejected" }));
3043
3093
  };
3044
3094
 
3045
- //#endregion
3046
- //#region src/lib/echo-model.ts
3047
- /**
3048
- * Capture the requested model id from a raw request `model` value, classifying
3049
- * it into the three-state contract. `undefined`/missing → `absent`; `""` →
3050
- * `empty`; any other string → `present`.
3051
- */
3052
- function captureRequestedModel(rawModel) {
3053
- if (rawModel === void 0 || rawModel === null) return { kind: "absent" };
3054
- if (typeof rawModel !== "string") return { kind: "absent" };
3055
- if (rawModel === "") return { kind: "empty" };
3056
- return {
3057
- kind: "present",
3058
- value: rawModel
3059
- };
3060
- }
3061
- /**
3062
- * Resolve a {@link RequestedModel} into the action to take on a model field:
3063
- * - `{ write: true, value }` → set the field to `value`.
3064
- * - `{ write: false, omit: true }` → remove the field (absent case).
3065
- * - `null` → leave the field untouched (context-missing case).
3066
- */
3067
- function resolveFieldAction(requested) {
3068
- if (requested.kind === "present") return { value: requested.value };
3069
- if (requested.kind === "empty") return { value: "" };
3070
- if (requested.kind === "absent") return { omit: true };
3071
- return null;
3072
- }
3073
- /**
3074
- * Apply the requested-model action to a single `model`-like key on a shallow
3075
- * clone of `obj`. Returns a new object; never mutates `obj`. If `obj` does not
3076
- * own `key`, it is returned (cloned) unchanged regardless of the action — we
3077
- * only ever rewrite a field the upstream payload actually carries (supports
3078
- * AC-MALFORMED-SSE: no model field → original passthrough).
3079
- */
3080
- function rewriteKey(obj, key, requested) {
3081
- if (!Object.hasOwn(obj, key)) return obj;
3082
- const action = resolveFieldAction(requested);
3083
- if (action === null) return obj;
3084
- if ("omit" in action) {
3085
- const { [key]: _omitted, ...rest } = obj;
3086
- return rest;
3087
- }
3088
- return {
3089
- ...obj,
3090
- [key]: action.value
3091
- };
3092
- }
3093
- /**
3094
- * Core rewrite over a plain record. Rewrites the documented model fields:
3095
- * - top-level `model`, top-level `modelVersion` (Gemini),
3096
- * - nested `message.model` (Anthropic message_start),
3097
- * - nested `response.model` (OpenAI Responses event).
3098
- * Returns a shallow clone; never mutates the input.
3099
- */
3100
- function echoRecord(body, requested) {
3101
- let out = rewriteKey(body, "model", requested);
3102
- out = rewriteKey(out, "modelVersion", requested);
3103
- if (isRecord$1(out.message) && Object.hasOwn(out.message, "model")) {
3104
- const newMessage = rewriteKey(out.message, "model", requested);
3105
- if (newMessage !== out.message) out = {
3106
- ...out,
3107
- message: newMessage
3108
- };
3109
- }
3110
- if (isRecord$1(out.response) && Object.hasOwn(out.response, "model")) {
3111
- const newResponse = rewriteKey(out.response, "model", requested);
3112
- if (newResponse !== out.response) out = {
3113
- ...out,
3114
- response: newResponse
3115
- };
3116
- }
3117
- return out;
3118
- }
3119
- /**
3120
- * Rewrite the documented client-facing model field(s) of a JSON response body
3121
- * to the requested model id. Handles every supported non-stream/body shape:
3122
- * - top-level `model` (OpenAI chat/completions & Responses bodies, Anthropic
3123
- * messages body, embeddings),
3124
- * - nested `message.model` (Anthropic `message_start` event object),
3125
- * - nested `response.model` (OpenAI Responses streaming event object),
3126
- * - top-level `modelVersion` (Gemini body & chunk).
3127
- *
3128
- * Returns a shallow-cloned object; the input is never mutated. Fields that are
3129
- * not present are left as-is (a payload with no model field round-trips
3130
- * unchanged), supporting AC-MALFORMED-SSE.
3131
- *
3132
- * The generic is constrained to `object` (not an index-signature shape) so it
3133
- * accepts the project's domain interfaces (`AnthropicResponse`,
3134
- * `ChatCompletionChunk`, …) directly without forcing callers to widen them.
3135
- */
3136
- function echoModelInResponseBody(body, requested) {
3137
- return echoRecord(body, requested);
3138
- }
3139
- /**
3140
- * Ensure a response body's top-level `model` field equals the requested model
3141
- * id — **setting it even when the body omits it**. This differs from
3142
- * {@link echoModelInResponseBody}, which only rewrites a `model` field the
3143
- * payload already carries (the passthrough rule that protects malformed-SSE /
3144
- * model-less events). Some upstreams omit `model` from an otherwise-valid
3145
- * response body (notably the Copilot embeddings endpoint, whose 200 body carries
3146
- * only `data` + `usage`); for those, the documented client-facing contract is
3147
- * still "`response.model` == R", so the field must be added, not skipped.
3148
- *
3149
- * Three-state per AC-MISSING-MODEL, keyed on the REQUESTER's model (not the
3150
- * upstream's):
3151
- * - `present` → set `model` to R (added if absent, overwritten if present).
3152
- * - `empty` → set `model` to "" (the client sent an empty string).
3153
- * - `absent` → omit `model` (client sent no model → never invent one); if the
3154
- * body happened to carry an upstream `model`, drop it.
3155
- * - `context-missing` → leave the body untouched (defensive passthrough).
3156
- *
3157
- * Only the documented client's own string R is ever written — never the upstream
3158
- * id — so this introduces no side channel (AC-NO-SIDECHANNEL). Returns a shallow
3159
- * clone; the input is never mutated (protects the AC-OBS data source).
3160
- */
3161
- function echoTopLevelModel(body, requested) {
3162
- if (requested.kind === "context-missing") return body;
3163
- const rec = body;
3164
- if (requested.kind === "absent") {
3165
- if (!Object.hasOwn(rec, "model")) return body;
3166
- const { model: _dropped, ...rest } = rec;
3167
- return rest;
3168
- }
3169
- const value = requested.kind === "present" ? requested.value : "";
3170
- return {
3171
- ...rec,
3172
- model: value
3173
- };
3174
- }
3175
- function isRecord$1(value) {
3176
- return typeof value === "object" && value !== null && !Array.isArray(value);
3177
- }
3178
- /**
3179
- * Rewrite the model field of an already-parsed SSE event payload to the
3180
- * requested model id, dispatched by protocol shape:
3181
- * - Anthropic `message_start` → `message.model`,
3182
- * - OpenAI chunk → top-level `model`,
3183
- * - OpenAI Responses event → nested `response.model`,
3184
- * - Gemini chunk → `modelVersion`.
3185
- *
3186
- * The field dispatch is identical to {@link echoModelInResponseBody} (both
3187
- * operate on the same documented model fields), so this delegates to the same
3188
- * core rather than duplicating the shape logic — the two exports exist to name
3189
- * the two responsibilities (body vs parsed-event) at call sites, per the PRD's
3190
- * single-policy-point design. Events with no model field round-trip unchanged
3191
- * (AC-MALFORMED-SSE); the input is never mutated.
3192
- */
3193
- function echoModelInParsedEvent(event, requested) {
3194
- return echoRecord(event, requested);
3195
- }
3196
-
3197
3095
  //#endregion
3198
3096
  //#region src/lib/message-sanitizer.ts
3199
3097
  const startPattern = /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\n*/;
@@ -3214,127 +3112,6 @@ function removeSystemReminderTags(text) {
3214
3112
  return result;
3215
3113
  }
3216
3114
 
3217
- //#endregion
3218
- //#region src/lib/repetition-detector.ts
3219
- /**
3220
- * Stream repetition detector.
3221
- *
3222
- * Uses the KMP failure function (prefix function) to detect repeated patterns
3223
- * in streaming text output. When a model gets stuck in a repetitive loop,
3224
- * it wastes tokens producing the same content over and over. This detector
3225
- * identifies such loops early so the caller can take action (log warning,
3226
- * abort stream, etc.).
3227
- *
3228
- * The algorithm works by maintaining a sliding buffer of recent text and
3229
- * computing the longest proper prefix that is also a suffix — if this
3230
- * length exceeds `(text.length - period) >= minRepetitions * period`,
3231
- * it means a pattern of length `period` has repeated enough times.
3232
- */
3233
- const DEFAULT_CONFIG = {
3234
- minPatternLength: 10,
3235
- minRepetitions: 3,
3236
- maxBufferSize: 5e3
3237
- };
3238
- var RepetitionDetector = class {
3239
- buffer = "";
3240
- config;
3241
- detected = false;
3242
- constructor(config) {
3243
- this.config = {
3244
- ...DEFAULT_CONFIG,
3245
- ...config
3246
- };
3247
- }
3248
- /**
3249
- * Feed a text chunk into the detector.
3250
- * Returns `true` if repetition has been detected (now or previously).
3251
- * Once detected, subsequent calls return `true` without further analysis.
3252
- */
3253
- feed(text) {
3254
- if (this.detected) return true;
3255
- if (!text) return false;
3256
- this.buffer += text;
3257
- if (this.buffer.length > this.config.maxBufferSize) this.buffer = this.buffer.slice(-this.config.maxBufferSize);
3258
- const minRequired = this.config.minPatternLength * this.config.minRepetitions;
3259
- if (this.buffer.length < minRequired) return false;
3260
- this.detected = detectRepetition(this.buffer, this.config.minPatternLength, this.config.minRepetitions);
3261
- return this.detected;
3262
- }
3263
- /** Reset detector state for a new stream */
3264
- reset() {
3265
- this.buffer = "";
3266
- this.detected = false;
3267
- }
3268
- /** Whether repetition has been detected */
3269
- get isDetected() {
3270
- return this.detected;
3271
- }
3272
- };
3273
- /**
3274
- * Detect if the tail of `text` contains a repeating pattern.
3275
- *
3276
- * Uses the KMP prefix function: for a string S, the prefix function π[i]
3277
- * gives the length of the longest proper prefix of S[0..i] that is also
3278
- * a suffix. If π[n-1] ≥ (n - period) where period = n - π[n-1], then
3279
- * the string is composed of a repeating unit of length `period`.
3280
- *
3281
- * We check the suffix of the buffer (last `checkLength` chars) to detect
3282
- * if a pattern of at least `minPatternLength` chars repeats at least
3283
- * `minRepetitions` times.
3284
- */
3285
- function detectRepetition(text, minPatternLength, minRepetitions) {
3286
- const minWindow = minPatternLength * minRepetitions;
3287
- const maxWindow = Math.min(text.length, 2e3);
3288
- const windowSizes = [
3289
- minWindow,
3290
- Math.floor(maxWindow * .5),
3291
- maxWindow
3292
- ].filter((w) => w >= minWindow && w <= text.length);
3293
- for (const windowSize of windowSizes) {
3294
- const window = text.slice(-windowSize);
3295
- const period = findRepeatingPeriod(window);
3296
- if (period >= minPatternLength) {
3297
- if (Math.floor(window.length / period) >= minRepetitions) return true;
3298
- }
3299
- }
3300
- return false;
3301
- }
3302
- /**
3303
- * Find the shortest repeating period in a string using KMP prefix function.
3304
- * Returns the period length, or the string length if no repetition found.
3305
- */
3306
- function findRepeatingPeriod(s) {
3307
- const n = s.length;
3308
- if (n === 0) return 0;
3309
- const pi = new Int32Array(n);
3310
- for (let i = 1; i < n; i++) {
3311
- let j = pi[i - 1] ?? 0;
3312
- while (j > 0 && s[i] !== s[j]) j = pi[j - 1] ?? 0;
3313
- if (s[i] === s[j]) j++;
3314
- pi[i] = j;
3315
- }
3316
- const period = n - pi[n - 1];
3317
- if (period < n && n % period === 0) return period;
3318
- if (period < n && pi[n - 1] >= period) return period;
3319
- return n;
3320
- }
3321
- /**
3322
- * Create a repetition detector callback for use in stream processing.
3323
- * Returns a function that accepts text deltas and logs a warning on first detection.
3324
- */
3325
- function createStreamRepetitionChecker(label, config) {
3326
- const detector = new RepetitionDetector(config);
3327
- let warned = false;
3328
- return (textDelta) => {
3329
- const isRepetitive = detector.feed(textDelta);
3330
- if (isRepetitive && !warned) {
3331
- warned = true;
3332
- consola.warn(`[RepetitionDetector] ${label}: Repetitive output detected in stream`);
3333
- }
3334
- return isRepetitive;
3335
- };
3336
- }
3337
-
3338
3115
  //#endregion
3339
3116
  //#region src/lib/tokenizer.ts
3340
3117
  const ENCODING_MAP = {
@@ -3548,139 +3325,10 @@ const getTokenCount = async (payload, model) => {
3548
3325
  };
3549
3326
 
3550
3327
  //#endregion
3551
- //#region src/lib/anthropic/beta.ts
3328
+ //#region src/lib/auto-truncate-openai.ts
3552
3329
  /**
3553
- * Vendor-neutral utilities for manipulating the `anthropic-beta` request header.
3554
- *
3555
- * Lives in `lib/anthropic/` (not in either transport module) so both the
3556
- * Anthropic-native and OpenAI-translated transport layers can share these
3557
- * helpers without introducing cross-transport imports.
3558
- */
3559
- /** Anthropic beta feature that unlocks the 1M context window. */
3560
- const CONTEXT_1M_BETA_FEATURE = "context-1m-2025-08-07";
3561
- /**
3562
- * Merge two comma-separated anthropic-beta header values. Trims whitespace,
3563
- * drops empty tokens, and dedupes by exact string match. Returns a canonical
3564
- * comma-joined string with no spaces.
3565
- *
3566
- * Either input may be undefined / empty.
3567
- */
3568
- function mergeBetaFeatures(existing, incoming) {
3569
- const seen = /* @__PURE__ */ new Set();
3570
- const out = [];
3571
- for (const raw of [existing, incoming]) {
3572
- if (!raw) continue;
3573
- for (const part of raw.split(",")) {
3574
- const f = part.trim();
3575
- if (f.length === 0 || seen.has(f)) continue;
3576
- seen.add(f);
3577
- out.push(f);
3578
- }
3579
- }
3580
- return out.join(",");
3581
- }
3582
- /**
3583
- * Append the context-1m feature to an anthropic-beta header value, deduping
3584
- * any prior occurrence. Returns the merged comma-separated string.
3585
- */
3586
- function appendContext1mBeta(existing) {
3587
- return mergeBetaFeatures(existing, CONTEXT_1M_BETA_FEATURE);
3588
- }
3589
- /**
3590
- * True iff a model id appears to be the suffixed 1M-context variant of an
3591
- * Anthropic Claude model (e.g. claude-opus-4-8-1m, claude-opus-4.6-1m).
3592
- *
3593
- * Used as a state.models-independent signal for whether to inject the
3594
- * context-1m-2025-08-07 beta header, so the 1M intent survives a stale or
3595
- * empty model cache (where `resolveAnthropicModelForDirectPath` would return
3596
- * undefined). Forwarding the beta is harmless to upstreams that ignore it.
3597
- */
3598
- function isOneMillionSuffixedClaudeId(modelId) {
3599
- return modelId.startsWith("claude-") && modelId.endsWith("-1m");
3600
- }
3601
-
3602
- //#endregion
3603
- //#region src/lib/headers.ts
3604
- /**
3605
- * Vendor-neutral header-bag helpers.
3606
- *
3607
- * HTTP header names are case-insensitive, but a plain-object header bag is
3608
- * case-sensitive on its keys. Code that wants to look up "anthropic-beta"
3609
- * without knowing whether some other producer wrote "Anthropic-Beta" needs
3610
- * `findHeaderKey`. Code that wants to set a header without creating a
3611
- * second case variant of the same name needs `setHeader`.
3612
- */
3613
- /** Case-insensitive lookup of a header key in a plain-object header bag. */
3614
- function findHeaderKey(headers, name) {
3615
- const lower = name.toLowerCase();
3616
- return Object.keys(headers).find((k) => k.toLowerCase() === lower);
3617
- }
3618
- /** Case-insensitive read of a header value. */
3619
- function getHeader(headers, name) {
3620
- const key = findHeaderKey(headers, name);
3621
- return key === void 0 ? void 0 : headers[key];
3622
- }
3623
- /**
3624
- * Set a header value at the existing case variant if one is present, else at
3625
- * the supplied canonical name. Prevents a second key (different case) from
3626
- * being added for the same logical header.
3627
- */
3628
- function setHeader(headers, name, value) {
3629
- const key = findHeaderKey(headers, name) ?? name;
3630
- headers[key] = value;
3631
- }
3632
-
3633
- //#endregion
3634
- //#region src/services/copilot/create-chat-completions.ts
3635
- const GPT_MODEL_PATTERN = /^gpt-/i;
3636
- const createChatCompletions = async (payload, options) => {
3637
- if (!state.copilotToken) throw new Error("Copilot token not found");
3638
- const vendor = options?.resolvedModel?.vendor;
3639
- const isOpenAIVendor = vendor === "OpenAI" || vendor === "Azure OpenAI";
3640
- const isLikelyGPT = !options?.resolvedModel && GPT_MODEL_PATTERN.test(payload.model);
3641
- let wire = payload;
3642
- if (isOpenAIVendor || isLikelyGPT) {
3643
- const { max_tokens, max_completion_tokens, ...rest } = payload;
3644
- const effective = max_completion_tokens ?? max_tokens;
3645
- wire = {
3646
- ...rest,
3647
- ...effective !== null && effective !== void 0 && { max_completion_tokens: effective }
3648
- };
3649
- }
3650
- const enableVision = wire.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
3651
- const isAgentCall = wire.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
3652
- const modelSupportsVision = options?.resolvedModel?.capabilities?.supports?.vision !== false;
3653
- const headers = {
3654
- ...copilotHeaders(state, {
3655
- vision: enableVision && modelSupportsVision,
3656
- modelRequestHeaders: options?.resolvedModel?.request_headers,
3657
- intent: isAgentCall ? "conversation-agent" : "conversation-panel"
3658
- }),
3659
- "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
3660
- };
3661
- if (options?.anthropicBeta) {
3662
- const existingKey = findHeaderKey(headers, "anthropic-beta") ?? "anthropic-beta";
3663
- headers[existingKey] = mergeBetaFeatures(headers[existingKey], options.anthropicBeta);
3664
- consola.debug(`[ChatCompletions] anthropic-beta after merge: ${headers[existingKey]}`);
3665
- }
3666
- const response = await copilotFetch("/chat/completions", {
3667
- method: "POST",
3668
- headers,
3669
- body: JSON.stringify(wire)
3670
- });
3671
- if (!response.ok) {
3672
- consola.error("Failed to create chat completions", response);
3673
- throw await HTTPError.fromResponse("Failed to create chat completions", response, options?.errorModelIdOverride ?? payload.model);
3674
- }
3675
- if (payload.stream) return events(response);
3676
- return await response.json();
3677
- };
3678
-
3679
- //#endregion
3680
- //#region src/lib/auto-truncate-openai.ts
3681
- /**
3682
- * Auto-truncate module: Automatically truncates conversation history
3683
- * when it exceeds token or byte limits (OpenAI format).
3330
+ * Auto-truncate module: Automatically truncates conversation history
3331
+ * when it exceeds token or byte limits (OpenAI format).
3684
3332
  *
3685
3333
  * Key features:
3686
3334
  * - Binary search for optimal truncation point
@@ -3865,7 +3513,7 @@ function createTruncationSystemContext$1(removedCount, compressedCount, summary)
3865
3513
  return context;
3866
3514
  }
3867
3515
  /** Create a truncation marker message (fallback when no system message) */
3868
- function createTruncationMarker$2(removedCount, compressedCount, summary) {
3516
+ function createTruncationMarker$1(removedCount, compressedCount, summary) {
3869
3517
  const parts = [];
3870
3518
  if (removedCount > 0) parts.push(`${removedCount} earlier messages removed`);
3871
3519
  if (compressedCount > 0) parts.push(`${compressedCount} tool results compressed`);
@@ -3992,7 +3640,7 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
3992
3640
  content: typeof lastSystem.content === "string" ? lastSystem.content + truncationContext : lastSystem.content
3993
3641
  };
3994
3642
  newSystemMessages = [...systemMessages.slice(0, lastSystemIdx), updatedSystem];
3995
- } else newMessages = [createTruncationMarker$2(removedCount, compressedCount, summary), ...preserved];
3643
+ } else newMessages = [createTruncationMarker$1(removedCount, compressedCount, summary), ...preserved];
3996
3644
  const newPayload = {
3997
3645
  ...payload,
3998
3646
  messages: [...newSystemMessages, ...newMessages]
@@ -4018,79 +3666,597 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
4018
3666
  }
4019
3667
 
4020
3668
  //#endregion
4021
- //#region src/lib/error-metrics.ts
4022
- function safeString(value, max = 200) {
4023
- try {
4024
- if (typeof value !== "string") return void 0;
4025
- return value.length > max ? value.slice(0, max) : value;
4026
- } catch {
4027
- return;
4028
- }
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");
4029
3682
  }
4030
- 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
+ }
4031
3692
  try {
4032
- if (typeof obj !== "object" || obj === null) return void 0;
4033
- return safeString(obj[key], max);
4034
- } catch {
4035
- 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
+ };
4036
3715
  }
4037
3716
  }
4038
- function extractErrorMetrics(error) {
4039
- if (error instanceof HTTPError) {
4040
- const metrics = { status: error.status };
4041
- try {
4042
- const parsed = JSON.parse(error.responseText);
4043
- if (parsed.error?.code) metrics.copilotErrorCode = parsed.error.code;
4044
- } catch {}
4045
- return metrics;
4046
- }
4047
- if (!(error instanceof Error)) return {};
4048
- const metrics = {};
4049
- try {
4050
- metrics.errorName = getStringProp(error, "name");
4051
- metrics.errorMessage = getStringProp(error, "message");
4052
- metrics.errorCode = getStringProp(error, "code");
4053
- const cause = error.cause;
4054
- if (cause !== void 0 && cause !== null) {
4055
- metrics.causeName = getStringProp(cause, "name");
4056
- 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
+ }
4057
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()}`);
4058
3749
  } catch {}
4059
- 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("");
4060
3762
  }
4061
3763
 
4062
3764
  //#endregion
4063
- //#region src/routes/shared.ts
4064
- /**
4065
- * Shared utilities for request handlers.
4066
- * Contains common functions used by both OpenAI and Anthropic message handlers.
4067
- */
3765
+ //#region src/lib/repetition-detector.ts
4068
3766
  /**
4069
- * Resolve the requested model id (R) carried on the response context into a
4070
- * {@link RequestedModel}. A context that never captured R (`undefined`) is
4071
- * treated as `context-missing` so the echo passes the upstream value through
4072
- * unchanged it must never invent an id or throw.
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.
4073
3779
  */
4074
- function requestedModelOf(ctx) {
4075
- return ctx.requestedModel ?? { kind: "context-missing" };
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;
4076
4167
  }
4168
+
4169
+ //#endregion
4170
+ //#region src/routes/observability-recording.ts
4077
4171
  /**
4078
- * Echo the requested model id into a JSON response body at the client write-out
4079
- * boundary. Thin context-aware wrapper over {@link echoModelInResponseBody};
4080
- * MUST be called AFTER history/posthog/TUI have read the upstream value.
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`.
4081
4178
  */
4082
- function echoResponseBody(body, ctx) {
4083
- return echoModelInResponseBody(body, requestedModelOf(ctx));
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 */
4193
+ function recordErrorResponse(ctx, model, error, endpoint, stream) {
4194
+ recordResponse(ctx.historyId, {
4195
+ success: false,
4196
+ model,
4197
+ usage: {
4198
+ input_tokens: 0,
4199
+ output_tokens: 0
4200
+ },
4201
+ error: formatError(error),
4202
+ content: null
4203
+ }, Date.now() - ctx.startTime);
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
+ });
4084
4248
  }
4249
+
4250
+ //#endregion
4251
+ //#region src/routes/tracker-mutations.ts
4085
4252
  /**
4086
- * Echo the requested model id into an already-parsed SSE event payload at the
4087
- * client write-out boundary. Thin context-aware wrapper over
4088
- * {@link echoModelInParsedEvent}; MUST be called AFTER the stream accumulator
4089
- * (the observability data source) has read the upstream value.
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`.
4090
4259
  */
4091
- function echoParsedEvent(event, ctx) {
4092
- return echoModelInParsedEvent(event, requestedModelOf(ctx));
4093
- }
4094
4260
  /** Helper to update tracker model */
4095
4261
  function updateTrackerModel(trackingId, model, resolvedModel) {
4096
4262
  if (!trackingId) return;
@@ -4110,36 +4276,6 @@ function updateTrackerStatus(trackingId, status) {
4110
4276
  if (!trackingId) return;
4111
4277
  requestTracker.updateRequest(trackingId, { status });
4112
4278
  }
4113
- /** Record error response to history */
4114
- function recordErrorResponse(ctx, model, error, endpoint, stream) {
4115
- recordResponse(ctx.historyId, {
4116
- success: false,
4117
- model,
4118
- usage: {
4119
- input_tokens: 0,
4120
- output_tokens: 0
4121
- },
4122
- error: error instanceof Error ? error.message : "Unknown error",
4123
- content: null
4124
- }, Date.now() - ctx.startTime);
4125
- if (endpoint !== void 0) {
4126
- const metrics = extractErrorMetrics(error);
4127
- const { attempts } = getRetryAttempts(error);
4128
- captureRequest({
4129
- model,
4130
- inputTokens: 0,
4131
- outputTokens: 0,
4132
- durationMs: Date.now() - ctx.startTime,
4133
- success: false,
4134
- stream: stream ?? false,
4135
- toolCount: 0,
4136
- ...metrics,
4137
- errorPhase: stream ? "pre_stream" : "non_stream",
4138
- endpoint,
4139
- attempt: attempts
4140
- });
4141
- }
4142
- }
4143
4279
  /** Complete TUI tracking and send PostHog analytics */
4144
4280
  function completeTracking(trackingId, inputTokens, outputTokens, queueWaitMs, reasoningTokens, analytics) {
4145
4281
  if (!trackingId) return;
@@ -4166,149 +4302,90 @@ function completeTracking(trackingId, inputTokens, outputTokens, queueWaitMs, re
4166
4302
  stopReason: analytics.stopReason
4167
4303
  });
4168
4304
  }
4169
- function formatError(error) {
4170
- if (error instanceof Error) return error.message || error.name;
4171
- if (typeof error === "string") return error;
4172
- try {
4173
- const s = JSON.stringify(error);
4174
- if (s && s !== "{}") return s;
4175
- } catch {}
4176
- try {
4177
- return String(error);
4178
- } catch {
4179
- return "Unknown error";
4180
- }
4181
- }
4182
4305
  /** Fail TUI tracking */
4183
4306
  function failTracking(trackingId, error) {
4184
4307
  if (!trackingId) return;
4185
4308
  requestTracker.failRequest(trackingId, formatError(error));
4186
4309
  }
4310
+
4311
+ //#endregion
4312
+ //#region src/routes/entry-context.ts
4187
4313
  /**
4188
- * Create a marker to prepend to responses indicating auto-truncation occurred.
4189
- * 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.
4190
4319
  */
4191
- function createTruncationMarker$1(result) {
4192
- if (!result.wasCompacted) return "";
4193
- const { originalTokens, compactedTokens, removedMessageCount } = result;
4194
- if (originalTokens === void 0 || compactedTokens === void 0 || removedMessageCount === void 0) return `\n\n---\n[Auto-truncated: conversation history was reduced to fit context limits]`;
4195
- const reduction = originalTokens - compactedTokens;
4196
- return `\n\n---\n[Auto-truncated: ${removedMessageCount} messages removed, ${originalTokens} → ${compactedTokens} tokens (${Math.round(reduction / originalTokens * 100)}% reduction)]`;
4197
- }
4198
- /** Record streaming error to history (works with any accumulator type) */
4199
- function recordStreamError(opts) {
4200
- const { acc, fallbackModel, ctx, error, endpoint } = opts;
4201
- const model = acc.model || fallbackModel;
4202
- recordResponse(ctx.historyId, {
4203
- success: false,
4204
- model,
4205
- usage: {
4206
- input_tokens: 0,
4207
- output_tokens: 0
4208
- },
4209
- error: formatError(error),
4210
- content: null
4211
- }, Date.now() - ctx.startTime);
4212
- if (endpoint !== void 0) {
4213
- const metrics = extractErrorMetrics(error);
4214
- captureRequest({
4215
- model,
4216
- inputTokens: acc.inputTokens ?? 0,
4217
- outputTokens: acc.outputTokens ?? 0,
4218
- durationMs: Date.now() - ctx.startTime,
4219
- success: false,
4220
- stream: true,
4221
- toolCount: 0,
4222
- ...metrics,
4223
- errorPhase: "mid_stream",
4224
- endpoint,
4225
- attempt: 1
4226
- });
4227
- }
4228
- }
4229
- /** Type guard for non-streaming responses */
4230
- function isNonStreaming(response) {
4231
- return Object.hasOwn(response, "choices");
4232
- }
4233
- /** Build final payload with auto-truncate if needed */
4234
- async function buildFinalPayload(payload, model, autoTruncateConfig = {}) {
4235
- if (!state.autoTruncate || !model) {
4236
- if (state.autoTruncate && !model) consola.warn(`Auto-truncate: Model '${payload.model}' not found in cached models, skipping`);
4237
- return {
4238
- finalPayload: payload,
4239
- truncateResult: null
4240
- };
4241
- }
4242
- try {
4243
- const check = await checkNeedsCompactionOpenAI(payload, model, autoTruncateConfig);
4244
- 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})` : ""}`);
4245
- if (!check.needed) return {
4246
- finalPayload: payload,
4247
- truncateResult: null
4248
- };
4249
- let reasonText;
4250
- if (check.reason === "both") reasonText = "tokens and size";
4251
- else if (check.reason === "bytes") reasonText = "size";
4252
- else reasonText = "tokens";
4253
- consola.info(`Auto-truncate triggered: exceeds ${reasonText} limit`);
4254
- const truncateResult = await autoTruncateOpenAI(payload, model, autoTruncateConfig);
4255
- return {
4256
- finalPayload: truncateResult.payload,
4257
- truncateResult
4258
- };
4259
- } catch (error) {
4260
- consola.warn("Auto-truncate failed, proceeding with original payload:", error instanceof Error ? error.message : error);
4261
- return {
4262
- finalPayload: payload,
4263
- truncateResult: null
4264
- };
4265
- }
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
+ };
4266
4335
  }
4336
+
4337
+ //#endregion
4338
+ //#region src/routes/response-context.ts
4267
4339
  /**
4268
- * Log helpful debugging information when a 413 error occurs.
4269
- * Also adjusts the dynamic byte limit for future requests.
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.
4270
4350
  */
4271
- async function logPayloadSizeInfo(payload, model) {
4272
- const messageCount = payload.messages.length;
4273
- const bodySize = JSON.stringify(payload).length;
4274
- const bodySizeKB = Math.round(bodySize / 1024);
4275
- onRequestTooLarge(bodySize);
4276
- let imageCount = 0;
4277
- let largeMessages = 0;
4278
- let totalImageSize = 0;
4279
- for (const msg of payload.messages) {
4280
- if (Array.isArray(msg.content)) {
4281
- for (const part of msg.content) if (part.type === "image_url") {
4282
- imageCount++;
4283
- if (part.image_url.url.startsWith("data:")) totalImageSize += part.image_url.url.length;
4284
- }
4285
- }
4286
- if ((typeof msg.content === "string" ? msg.content.length : JSON.stringify(msg.content).length) > 5e4) largeMessages++;
4287
- }
4288
- consola.info("");
4289
- consola.info("╭─────────────────────────────────────────────────────────╮");
4290
- consola.info("│ 413 Request Entity Too Large │");
4291
- consola.info("╰─────────────────────────────────────────────────────────╯");
4292
- consola.info("");
4293
- consola.info(` Request body size: ${bodySizeKB} KB (${bodySize.toLocaleString()} bytes)`);
4294
- consola.info(` Message count: ${messageCount}`);
4295
- if (model) try {
4296
- const tokenCount = await getTokenCount(payload, model);
4297
- const limit = model.capabilities?.limits?.max_prompt_tokens ?? 128e3;
4298
- consola.info(` Estimated tokens: ${tokenCount.input.toLocaleString()} / ${limit.toLocaleString()}`);
4299
- } catch {}
4300
- if (imageCount > 0) {
4301
- const imageSizeKB = Math.round(totalImageSize / 1024);
4302
- consola.info(` Images: ${imageCount} (${imageSizeKB} KB base64 data)`);
4303
- }
4304
- if (largeMessages > 0) consola.info(` Large messages (>50KB): ${largeMessages}`);
4305
- consola.info("");
4306
- consola.info(" Suggestions:");
4307
- if (!state.autoTruncate) consola.info(" • Enable --auto-truncate to automatically truncate history");
4308
- if (imageCount > 0) consola.info(" • Remove or resize large images in the conversation");
4309
- consola.info(" • Start a new conversation with /clear or /reset");
4310
- consola.info(" • Reduce conversation history by deleting old messages");
4311
- consola.info("");
4351
+ function requestedModelOf(ctx) {
4352
+ return ctx.requestedModel ?? { kind: "context-missing" };
4353
+ }
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));
4361
+ }
4362
+ /**
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.
4367
+ */
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)]`;
4312
4389
  }
4313
4390
 
4314
4391
  //#endregion
@@ -4317,28 +4394,24 @@ function getReasoningTokensFromOpenAIUsage(usage) {
4317
4394
  return usage?.completion_tokens_details?.reasoning_tokens;
4318
4395
  }
4319
4396
  async function handleCompletion$1(c) {
4320
- const originalPayload = await c.req.json();
4321
- consola.debug("Request payload:", JSON.stringify(originalPayload).slice(-400));
4322
- const requestedModel = captureRequestedModel(originalPayload.model);
4323
- const trackingId = c.get("trackingId");
4324
- const startTime = (trackingId ? requestTracker.getRequest(trackingId) : void 0)?.startTime ?? Date.now();
4325
- updateTrackerModel(trackingId, originalPayload.model);
4326
- const ctx = {
4327
- historyId: recordRequest("openai", {
4328
- model: originalPayload.model,
4329
- messages: convertOpenAIMessages(originalPayload.messages),
4330
- stream: originalPayload.stream ?? false,
4331
- 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) => ({
4332
4408
  name: t.function.name,
4333
4409
  description: t.function.description
4334
4410
  })),
4335
- max_tokens: originalPayload.max_tokens ?? void 0,
4336
- temperature: originalPayload.temperature ?? void 0
4337
- }),
4338
- trackingId,
4339
- startTime,
4340
- requestedModel
4341
- };
4411
+ max_tokens: p.max_tokens ?? void 0,
4412
+ temperature: p.temperature ?? void 0
4413
+ })
4414
+ });
4342
4415
  const selectedModel = findModelById(originalPayload.model);
4343
4416
  await logTokenCount(originalPayload, selectedModel);
4344
4417
  const { finalPayload, truncateResult } = await buildFinalPayload(originalPayload, selectedModel);
@@ -4353,21 +4426,20 @@ async function handleCompletion$1(c) {
4353
4426
  c,
4354
4427
  payload,
4355
4428
  selectedModel,
4356
- ctx,
4357
- trackingId
4429
+ ctx
4358
4430
  });
4359
4431
  }
4360
4432
  /**
4361
4433
  * Execute the API call with enhanced error handling for 413 errors.
4362
4434
  */
4363
4435
  async function executeRequest(opts) {
4364
- const { c, payload, selectedModel, ctx, trackingId } = opts;
4436
+ const { c, payload, selectedModel, ctx } = opts;
4365
4437
  try {
4366
4438
  const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(payload, { resolvedModel: selectedModel }));
4367
4439
  ctx.queueWaitMs = queueWaitMs;
4368
4440
  if (isNonStreaming(response)) return handleNonStreamingResponse$1(c, response, ctx, payload);
4369
4441
  consola.debug("Streaming response");
4370
- updateTrackerStatus(trackingId, "streaming");
4442
+ updateTrackerStatus(ctx.trackingId, "streaming");
4371
4443
  return streamSSE(c, async (stream) => {
4372
4444
  await handleStreamingResponse$1({
4373
4445
  stream,
@@ -4397,7 +4469,7 @@ function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
4397
4469
  consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
4398
4470
  let response = originalResponse;
4399
4471
  if (state.verbose && ctx.truncateResult?.wasCompacted && response.choices[0]?.message.content) {
4400
- const marker = createTruncationMarker$1(ctx.truncateResult);
4472
+ const marker = formatClientTruncationMarker(ctx.truncateResult);
4401
4473
  response = {
4402
4474
  ...response,
4403
4475
  choices: response.choices.map((choice, i) => i === 0 ? {
@@ -4483,7 +4555,7 @@ async function handleStreamingResponse$1(opts) {
4483
4555
  const checkRepetition = createStreamRepetitionChecker(`openai:${payload.model}`);
4484
4556
  try {
4485
4557
  if (state.verbose && ctx.truncateResult?.wasCompacted) {
4486
- const marker = createTruncationMarker$1(ctx.truncateResult);
4558
+ const marker = formatClientTruncationMarker(ctx.truncateResult);
4487
4559
  const markerChunk = {
4488
4560
  id: `compact-marker-${Date.now()}`,
4489
4561
  object: "chat.completion.chunk",
@@ -4664,51 +4736,6 @@ completionRoutes.post("/", async (c) => {
4664
4736
  }
4665
4737
  });
4666
4738
 
4667
- //#endregion
4668
- //#region src/services/copilot/create-embeddings.ts
4669
- const createEmbeddings = async (payload) => {
4670
- if (!state.copilotToken) throw new Error("Copilot token not found");
4671
- const response = await copilotFetch("/embeddings", {
4672
- method: "POST",
4673
- headers: copilotHeaders(state),
4674
- body: JSON.stringify(payload)
4675
- });
4676
- if (!response.ok) throw await HTTPError.fromResponse("Failed to create embeddings", response);
4677
- return await response.json();
4678
- };
4679
-
4680
- //#endregion
4681
- //#region src/routes/embeddings/route.ts
4682
- const embeddingRoutes = new Hono();
4683
- function isRecord(value) {
4684
- return typeof value === "object" && value !== null && !Array.isArray(value);
4685
- }
4686
- embeddingRoutes.post("/", async (c) => {
4687
- const startTime = Date.now();
4688
- try {
4689
- const rawBody = await c.req.json();
4690
- const payload = rawBody;
4691
- const requestedModel = captureRequestedModel(isRecord(rawBody) ? rawBody.model : void 0);
4692
- const response = await createEmbeddings(payload);
4693
- if (!isRecord(response)) return c.json(response);
4694
- const upstreamModel = typeof response.model === "string" ? response.model : "";
4695
- const usage = response.usage;
4696
- captureRequest({
4697
- model: upstreamModel,
4698
- inputTokens: isRecord(usage) && typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0,
4699
- outputTokens: 0,
4700
- durationMs: Date.now() - startTime,
4701
- success: true,
4702
- stream: false,
4703
- toolCount: 0,
4704
- endpoint: "embeddings"
4705
- });
4706
- return c.json(echoTopLevelModel(response, requestedModel));
4707
- } catch (error) {
4708
- return forwardError(c, error);
4709
- }
4710
- });
4711
-
4712
4739
  //#endregion
4713
4740
  //#region src/routes/event-logging/route.ts
4714
4741
  const eventLoggingRoutes = new Hono();
@@ -4716,510 +4743,6 @@ eventLoggingRoutes.post("/batch", (c) => {
4716
4743
  return c.text("OK", 200);
4717
4744
  });
4718
4745
 
4719
- //#endregion
4720
- //#region src/routes/gemini/error.ts
4721
- const STATUS_MAP = {
4722
- 400: "INVALID_ARGUMENT",
4723
- 401: "PERMISSION_DENIED",
4724
- 403: "PERMISSION_DENIED",
4725
- 404: "NOT_FOUND",
4726
- 413: "INVALID_ARGUMENT",
4727
- 429: "RESOURCE_EXHAUSTED",
4728
- 500: "INTERNAL"
4729
- };
4730
- function geminiError(c, code, status, message) {
4731
- return c.json({ error: {
4732
- code,
4733
- message,
4734
- status
4735
- } }, code);
4736
- }
4737
- function forwardGeminiError(c, error) {
4738
- if (error instanceof HTTPError) {
4739
- const status = STATUS_MAP[error.status] ?? "INTERNAL";
4740
- const code = error.status;
4741
- let message = error.responseText;
4742
- try {
4743
- const parsed = JSON.parse(error.responseText);
4744
- if (parsed.error?.message) message = parsed.error.message;
4745
- } catch {}
4746
- consola.error(`HTTP ${code}:`, message.slice(0, 200));
4747
- return geminiError(c, code, status, message);
4748
- }
4749
- consola.error("Unexpected error:", error);
4750
- return geminiError(c, 500, "INTERNAL", error instanceof Error ? error.message : "Unknown error");
4751
- }
4752
-
4753
- //#endregion
4754
- //#region src/routes/gemini/gemini-to-openai.ts
4755
- function translateGeminiToOpenAI(request, model) {
4756
- const messages = [];
4757
- if (request.systemInstruction) {
4758
- const systemText = extractTextFromParts(request.systemInstruction.parts);
4759
- if (systemText) messages.push({
4760
- role: "system",
4761
- content: systemText
4762
- });
4763
- }
4764
- let globalCallIndex = 0;
4765
- const callIdQueue = /* @__PURE__ */ new Map();
4766
- if (!Array.isArray(request.contents)) return { payload: {
4767
- messages: [],
4768
- model
4769
- } };
4770
- for (const content of request.contents) {
4771
- const translated = translateContent(content, callIdQueue, () => `call_gemini_${globalCallIndex++}`);
4772
- messages.push(...translated);
4773
- }
4774
- const payload = {
4775
- messages,
4776
- model
4777
- };
4778
- const config = request.generationConfig;
4779
- if (config) {
4780
- if (config.temperature !== void 0) payload.temperature = config.temperature;
4781
- if (config.topP !== void 0) payload.top_p = config.topP;
4782
- if (config.maxOutputTokens !== void 0) payload.max_tokens = config.maxOutputTokens;
4783
- if (config.stopSequences !== void 0) payload.stop = config.stopSequences;
4784
- if (config.responseMimeType === "application/json") payload.response_format = { type: "json_object" };
4785
- }
4786
- if (request.tools) {
4787
- const tools = translateTools(request.tools);
4788
- if (tools.length > 0) payload.tools = tools;
4789
- }
4790
- if (request.toolConfig?.functionCallingConfig?.mode) payload.tool_choice = {
4791
- AUTO: "auto",
4792
- ANY: "required",
4793
- NONE: "none"
4794
- }[request.toolConfig.functionCallingConfig.mode];
4795
- return { payload };
4796
- }
4797
- function mapFunctionCallsToToolCalls(functionCalls, callIdQueue, generateId) {
4798
- return functionCalls.map((fc) => {
4799
- const id = generateId();
4800
- pushToQueue(callIdQueue, fc.functionCall.name, id);
4801
- return {
4802
- id,
4803
- type: "function",
4804
- function: {
4805
- name: fc.functionCall.name,
4806
- arguments: JSON.stringify(fc.functionCall.args)
4807
- }
4808
- };
4809
- });
4810
- }
4811
- function translateContent(content, callIdQueue, generateId) {
4812
- const role = content.role === "model" ? "assistant" : "user";
4813
- const messages = [];
4814
- const textParts = [];
4815
- const imageParts = [];
4816
- const functionCalls = [];
4817
- const functionResponses = [];
4818
- for (const part of content.parts) if (isTextPart(part)) {
4819
- if (!part.thought) textParts.push(part);
4820
- } else if (isInlineDataPart(part)) imageParts.push(part);
4821
- else if (isFunctionCallPart(part)) functionCalls.push(part);
4822
- else if (isFunctionResponsePart(part)) functionResponses.push(part);
4823
- else if (isFileDataPart(part)) throw new HTTPError("fileData parts are not supported", 400, "fileData parts are not supported");
4824
- if (imageParts.length > 0) {
4825
- const contentParts = [];
4826
- for (const part of content.parts) if (isTextPart(part) && !part.thought) contentParts.push({
4827
- type: "text",
4828
- text: part.text
4829
- });
4830
- else if (isInlineDataPart(part)) contentParts.push({
4831
- type: "image_url",
4832
- image_url: { url: `data:${part.inlineData.mimeType};base64,${part.inlineData.data}` }
4833
- });
4834
- const msg = {
4835
- role,
4836
- content: contentParts
4837
- };
4838
- if (functionCalls.length > 0 && role === "assistant") msg.tool_calls = mapFunctionCallsToToolCalls(functionCalls, callIdQueue, generateId);
4839
- messages.push(msg);
4840
- } else if (functionCalls.length > 0 && role === "assistant") {
4841
- const textContent = textParts.length > 0 ? textParts.map((p) => p.text).join("") : null;
4842
- messages.push({
4843
- role: "assistant",
4844
- content: textContent,
4845
- tool_calls: mapFunctionCallsToToolCalls(functionCalls, callIdQueue, generateId)
4846
- });
4847
- } else if (textParts.length > 0) messages.push({
4848
- role,
4849
- content: textParts.map((p) => p.text).join("")
4850
- });
4851
- let orphanIndex = 0;
4852
- for (const fr of functionResponses) {
4853
- const queue = callIdQueue.get(fr.functionResponse.name);
4854
- const id = queue && queue.length > 0 ? queue.shift() : `call_gemini_orphan_${orphanIndex++}`;
4855
- messages.push({
4856
- role: "tool",
4857
- content: JSON.stringify(fr.functionResponse.response),
4858
- tool_call_id: id
4859
- });
4860
- }
4861
- return messages;
4862
- }
4863
- function translateTools(geminiTools) {
4864
- const tools = [];
4865
- for (const tool of geminiTools) if (tool.functionDeclarations) for (const decl of tool.functionDeclarations) tools.push({
4866
- type: "function",
4867
- function: {
4868
- name: decl.name,
4869
- description: decl.description,
4870
- parameters: decl.parameters ?? {
4871
- type: "object",
4872
- properties: {}
4873
- }
4874
- }
4875
- });
4876
- return tools;
4877
- }
4878
- function pushToQueue(queue, name, id) {
4879
- const existing = queue.get(name);
4880
- if (existing) existing.push(id);
4881
- else queue.set(name, [id]);
4882
- }
4883
- function extractTextFromParts(parts) {
4884
- return parts.filter((p) => "text" in p && (!("thought" in p) || !p.thought)).map((p) => p.text).join("\n");
4885
- }
4886
- function isTextPart(part) {
4887
- return "text" in part;
4888
- }
4889
- function isInlineDataPart(part) {
4890
- return "inlineData" in part;
4891
- }
4892
- function isFunctionCallPart(part) {
4893
- return "functionCall" in part;
4894
- }
4895
- function isFunctionResponsePart(part) {
4896
- return "functionResponse" in part;
4897
- }
4898
- function isFileDataPart(part) {
4899
- return "fileData" in part;
4900
- }
4901
-
4902
- //#endregion
4903
- //#region src/routes/gemini/count-tokens-handler.ts
4904
- async function handleGeminiCountTokens(c, model) {
4905
- try {
4906
- const { payload } = translateGeminiToOpenAI(await c.req.json(), model);
4907
- const selectedModel = findModelById(model);
4908
- if (!selectedModel) {
4909
- consola.warn("Model not found for count_tokens, returning estimate");
4910
- return c.json({ totalTokens: 1 });
4911
- }
4912
- const tokenCount = await getTokenCount(payload, selectedModel);
4913
- const totalTokens = tokenCount.input + tokenCount.output;
4914
- consola.debug(`Gemini countTokens: ${totalTokens} tokens`);
4915
- return c.json({ totalTokens });
4916
- } catch (error) {
4917
- return forwardGeminiError(c, error);
4918
- }
4919
- }
4920
-
4921
- //#endregion
4922
- //#region src/routes/gemini/openai-to-gemini.ts
4923
- function translateOpenAIResponseToGemini(response, model) {
4924
- const choice = response.choices.at(0);
4925
- if (!choice) return {
4926
- candidates: [],
4927
- usageMetadata: buildUsageMetadata(response.usage),
4928
- modelVersion: model
4929
- };
4930
- const parts = [];
4931
- if (choice.message.content) parts.push({ text: choice.message.content });
4932
- if (choice.message.tool_calls) for (const tc of choice.message.tool_calls) {
4933
- const args = parseArgs(tc.function.arguments);
4934
- parts.push({ functionCall: {
4935
- name: tc.function.name,
4936
- args
4937
- } });
4938
- }
4939
- if (parts.length === 0) parts.push({ text: "" });
4940
- return {
4941
- candidates: [{
4942
- content: {
4943
- role: "model",
4944
- parts
4945
- },
4946
- finishReason: mapFinishReason(choice.finish_reason),
4947
- index: 0
4948
- }],
4949
- usageMetadata: buildUsageMetadata(response.usage),
4950
- modelVersion: model
4951
- };
4952
- }
4953
- function createGeminiStreamState() {
4954
- return {
4955
- toolCalls: /* @__PURE__ */ new Map(),
4956
- usage: {
4957
- promptTokens: 0,
4958
- completionTokens: 0,
4959
- totalTokens: 0
4960
- },
4961
- model: "",
4962
- finishReason: ""
4963
- };
4964
- }
4965
- function translateOpenAIChunkToGemini(chunk, state) {
4966
- const results = [];
4967
- if (!state.model && chunk.model) state.model = chunk.model;
4968
- if (chunk.usage) {
4969
- state.usage.promptTokens = chunk.usage.prompt_tokens;
4970
- state.usage.completionTokens = chunk.usage.completion_tokens;
4971
- state.usage.totalTokens = chunk.usage.total_tokens;
4972
- }
4973
- const choice = chunk.choices.at(0);
4974
- if (!choice) return results;
4975
- const delta = choice.delta;
4976
- if (delta.tool_calls) for (const tc of delta.tool_calls) {
4977
- const existing = state.toolCalls.get(tc.index);
4978
- if (existing) {
4979
- if (tc.function?.arguments) existing.args += tc.function.arguments;
4980
- } else {
4981
- const flushed = flushToolCalls(state, tc.index);
4982
- if (flushed) results.push(flushed);
4983
- state.toolCalls.set(tc.index, {
4984
- name: tc.function?.name ?? "",
4985
- args: tc.function?.arguments ?? ""
4986
- });
4987
- }
4988
- }
4989
- if (delta.content) results.push(buildGeminiChunk([{ text: delta.content }], choice.finish_reason, state));
4990
- if (choice.finish_reason) {
4991
- state.finishReason = choice.finish_reason;
4992
- const flushed = flushToolCalls(state);
4993
- if (flushed) results.push(flushed);
4994
- if (!delta.content) results.push(buildGeminiChunk([], choice.finish_reason, state));
4995
- }
4996
- return results;
4997
- }
4998
- function flushToolCalls(state, belowIndex) {
4999
- if (state.toolCalls.size === 0) return null;
5000
- const parts = [];
5001
- for (const [idx, tc] of state.toolCalls) {
5002
- if (belowIndex !== void 0 && idx >= belowIndex) continue;
5003
- const args = parseArgs(tc.args);
5004
- parts.push({ functionCall: {
5005
- name: tc.name,
5006
- args
5007
- } });
5008
- state.toolCalls.delete(idx);
5009
- }
5010
- if (parts.length === 0) return null;
5011
- return buildGeminiChunk(parts, null, state);
5012
- }
5013
- function buildGeminiChunk(parts, finishReason, state) {
5014
- const candidate = {
5015
- content: {
5016
- role: "model",
5017
- parts: parts.length > 0 ? parts : [{ text: "" }]
5018
- },
5019
- index: 0
5020
- };
5021
- if (finishReason) candidate.finishReason = mapFinishReason(finishReason);
5022
- return {
5023
- candidates: [candidate],
5024
- usageMetadata: {
5025
- promptTokenCount: state.usage.promptTokens,
5026
- candidatesTokenCount: state.usage.completionTokens,
5027
- totalTokenCount: state.usage.totalTokens
5028
- },
5029
- modelVersion: state.model
5030
- };
5031
- }
5032
- function parseArgs(raw) {
5033
- try {
5034
- return JSON.parse(raw);
5035
- } catch {
5036
- return { raw };
5037
- }
5038
- }
5039
- function mapFinishReason(reason) {
5040
- switch (reason) {
5041
- case "stop":
5042
- case "tool_calls": return "STOP";
5043
- case "length": return "MAX_TOKENS";
5044
- case "content_filter": return "SAFETY";
5045
- default: return "OTHER";
5046
- }
5047
- }
5048
- function buildUsageMetadata(usage) {
5049
- return {
5050
- promptTokenCount: usage?.prompt_tokens ?? 0,
5051
- candidatesTokenCount: usage?.completion_tokens ?? 0,
5052
- totalTokenCount: usage?.total_tokens ?? 0
5053
- };
5054
- }
5055
-
5056
- //#endregion
5057
- //#region src/routes/gemini/handler.ts
5058
- async function handleGeminiGenerate(c, model, isStream, requestedModel) {
5059
- try {
5060
- const geminiRequest = await c.req.json();
5061
- consola.debug("Gemini request for model:", model, "stream:", isStream);
5062
- const trackingId = c.get("trackingId");
5063
- const startTime = Date.now();
5064
- updateTrackerModel(trackingId, model);
5065
- const { payload } = translateGeminiToOpenAI(geminiRequest, model);
5066
- payload.stream = isStream;
5067
- const selectedModel = findModelById(model);
5068
- if (isNullish(payload.max_tokens) && selectedModel) payload.max_tokens = selectedModel.capabilities?.limits?.max_output_tokens;
5069
- const ctx = {
5070
- historyId: recordRequest("gemini", {
5071
- model,
5072
- messages: payload.messages.map((m) => ({
5073
- role: m.role,
5074
- content: typeof m.content === "string" ? m.content : JSON.stringify(m.content),
5075
- tool_calls: m.tool_calls,
5076
- tool_call_id: m.tool_call_id
5077
- })),
5078
- stream: isStream,
5079
- max_tokens: payload.max_tokens ?? void 0,
5080
- temperature: payload.temperature ?? void 0
5081
- }),
5082
- trackingId,
5083
- startTime,
5084
- requestedModel
5085
- };
5086
- const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(payload, { resolvedModel: selectedModel }));
5087
- ctx.queueWaitMs = queueWaitMs;
5088
- if (isNonStreaming(response)) return handleNonStreamResponse(c, response, model, ctx, payload);
5089
- consola.debug("Streaming Gemini response");
5090
- updateTrackerStatus(trackingId, "streaming");
5091
- return stream(c, async (s) => {
5092
- c.header("Content-Type", "text/event-stream");
5093
- c.header("Cache-Control", "no-cache");
5094
- c.header("Connection", "keep-alive");
5095
- const streamState = createGeminiStreamState();
5096
- try {
5097
- for await (const rawEvent of response) {
5098
- if (rawEvent.data === "[DONE]") break;
5099
- let chunk;
5100
- try {
5101
- chunk = JSON.parse(rawEvent.data);
5102
- } catch (parseError) {
5103
- consola.debug("Failed to parse stream chunk:", parseError);
5104
- continue;
5105
- }
5106
- const geminiChunks = translateOpenAIChunkToGemini(chunk, streamState);
5107
- for (const gc of geminiChunks) await s.write(`data: ${JSON.stringify(echoParsedEvent(gc, ctx))}\n\n`);
5108
- }
5109
- recordResponse(ctx.historyId, {
5110
- success: true,
5111
- model: streamState.model || model,
5112
- usage: {
5113
- input_tokens: streamState.usage.promptTokens,
5114
- output_tokens: streamState.usage.completionTokens
5115
- },
5116
- content: null
5117
- }, Date.now() - ctx.startTime);
5118
- completeTracking(ctx.trackingId, streamState.usage.promptTokens, streamState.usage.completionTokens, ctx.queueWaitMs, void 0, {
5119
- model: streamState.model || model,
5120
- stream: true,
5121
- durationMs: Date.now() - ctx.startTime,
5122
- stopReason: streamState.finishReason || void 0,
5123
- toolCount: payload.tools?.length ?? 0
5124
- });
5125
- } catch (error) {
5126
- recordStreamError({
5127
- acc: { model: streamState.model || model },
5128
- fallbackModel: model,
5129
- ctx,
5130
- error,
5131
- endpoint: "chat_completions"
5132
- });
5133
- failTracking(ctx.trackingId, error);
5134
- try {
5135
- await s.write(`data: ${JSON.stringify({ candidates: [{
5136
- content: {
5137
- role: "model",
5138
- parts: [{ text: `\n\n[copilot-api: upstream stream terminated. Please retry.]` }]
5139
- },
5140
- finishReason: "OTHER",
5141
- index: 0
5142
- }] })}\n\n`);
5143
- } catch {}
5144
- }
5145
- });
5146
- } catch (error) {
5147
- const trackingId = c.get("trackingId");
5148
- if (trackingId) failTracking(trackingId, error);
5149
- return forwardGeminiError(c, error);
5150
- }
5151
- }
5152
- function handleNonStreamResponse(c, response, model, ctx, payload) {
5153
- const geminiResponse = translateOpenAIResponseToGemini(response, model);
5154
- const usage = response.usage;
5155
- recordResponse(ctx.historyId, {
5156
- success: true,
5157
- model: response.model || model,
5158
- usage: {
5159
- input_tokens: usage?.prompt_tokens ?? 0,
5160
- output_tokens: usage?.completion_tokens ?? 0
5161
- },
5162
- stop_reason: response.choices[0]?.finish_reason,
5163
- content: response.choices[0] ? {
5164
- role: "assistant",
5165
- content: response.choices[0].message.content ?? ""
5166
- } : null
5167
- }, Date.now() - ctx.startTime);
5168
- completeTracking(ctx.trackingId, usage?.prompt_tokens ?? 0, usage?.completion_tokens ?? 0, ctx.queueWaitMs, void 0, {
5169
- model: response.model || model,
5170
- stream: false,
5171
- durationMs: Date.now() - ctx.startTime,
5172
- stopReason: response.choices[0]?.finish_reason,
5173
- toolCount: payload.tools?.length ?? 0
5174
- });
5175
- return c.json(echoResponseBody(geminiResponse, ctx));
5176
- }
5177
-
5178
- //#endregion
5179
- //#region src/routes/gemini/model-alias.ts
5180
- /**
5181
- * Maps Gemini model names to equivalent models available on GitHub Copilot.
5182
- *
5183
- * Two types of aliases:
5184
- *
5185
- * - **Forced**: Always applied regardless of Copilot model availability.
5186
- * Use when the old model name should never reach the backend.
5187
- *
5188
- * - **Conditional**: Only applied when the requested model is absent from
5189
- * the Copilot model list, so if Copilot adds native support the request
5190
- * goes through unchanged.
5191
- */
5192
- const GEMINI_FORCED_ALIASES = { "gemini-3.1-pro-preview-customtools": "gemini-3.1-pro-preview" };
5193
- const GEMINI_CONDITIONAL_ALIASES = {
5194
- "gemini-2.5-flash-lite": "gemini-3.5-flash",
5195
- "gemini-2.5-flash": "gemini-3.5-flash"
5196
- };
5197
- function resolveGeminiModelAlias(model) {
5198
- if (model in GEMINI_FORCED_ALIASES) return GEMINI_FORCED_ALIASES[model];
5199
- if (!(model in GEMINI_CONDITIONAL_ALIASES)) return model;
5200
- if (findModelById(model)) return model;
5201
- return GEMINI_CONDITIONAL_ALIASES[model];
5202
- }
5203
-
5204
- //#endregion
5205
- //#region src/routes/gemini/route.ts
5206
- const geminiRoutes = new Hono();
5207
- geminiRoutes.post("/:modelAction", async (c) => {
5208
- const modelAction = c.req.param("modelAction");
5209
- const colonIndex = modelAction.lastIndexOf(":");
5210
- if (colonIndex === -1) return geminiError(c, 400, "INVALID_ARGUMENT", "Missing action in URL");
5211
- const rawModel = modelAction.slice(0, Math.max(0, colonIndex));
5212
- const requestedModel = captureRequestedModel(rawModel);
5213
- const model = resolveGeminiModelAlias(rawModel);
5214
- const action = modelAction.slice(Math.max(0, colonIndex + 1));
5215
- switch (action) {
5216
- case "generateContent": return handleGeminiGenerate(c, model, false, requestedModel);
5217
- case "streamGenerateContent": return handleGeminiGenerate(c, model, true, requestedModel);
5218
- case "countTokens": return handleGeminiCountTokens(c, model);
5219
- default: return geminiError(c, 400, "INVALID_ARGUMENT", `Unknown action: ${action}`);
5220
- }
5221
- });
5222
-
5223
4746
  //#endregion
5224
4747
  //#region src/routes/history/api.ts
5225
4748
  function handleGetEntries(c) {
@@ -8291,7 +7814,7 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
8291
7814
  stopReason: response.stop_reason ?? void 0
8292
7815
  });
8293
7816
  let finalResponse = response;
8294
- if (state.verbose && truncateResult?.wasCompacted) finalResponse = prependMarkerToResponse(response, createTruncationMarker$1(truncateResult));
7817
+ if (state.verbose && truncateResult?.wasCompacted) finalResponse = prependMarkerToResponse(response, formatClientTruncationMarker(truncateResult));
8295
7818
  logServerToolBlocks(finalResponse.content);
8296
7819
  finalResponse = filterServerToolBlocksFromResponse(finalResponse);
8297
7820
  return c.json(echoResponseBody(finalResponse, ctx));
@@ -8496,7 +8019,7 @@ function handleNonStreamingResponse(opts) {
8496
8019
  let anthropicResponse = translateToAnthropic(response, toolNameMapping);
8497
8020
  consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
8498
8021
  if (state.verbose && ctx.truncateResult?.wasCompacted) {
8499
- const marker = createTruncationMarker$1(ctx.truncateResult);
8022
+ const marker = formatClientTruncationMarker(ctx.truncateResult);
8500
8023
  anthropicResponse = prependMarkerToResponse(anthropicResponse, marker);
8501
8024
  }
8502
8025
  recordResponse(ctx.historyId, {
@@ -8551,7 +8074,7 @@ async function handleStreamingResponse(opts) {
8551
8074
  const checkRepetition = createStreamRepetitionChecker(`translated:${anthropicPayload.model}`);
8552
8075
  try {
8553
8076
  if (ctx.truncateResult?.wasCompacted) {
8554
- const marker = createTruncationMarker$1(ctx.truncateResult);
8077
+ const marker = formatClientTruncationMarker(ctx.truncateResult);
8555
8078
  await sendTruncationMarkerEvent(stream, streamState, marker);
8556
8079
  acc.content += marker;
8557
8080
  }
@@ -8663,37 +8186,34 @@ function resolveModelFromBetaHeader(model, betaHeader) {
8663
8186
  return resolved;
8664
8187
  }
8665
8188
  async function handleCompletion(c) {
8666
- const anthropicPayload = await c.req.json();
8667
- consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
8668
- const requestedModel = captureRequestedModel(anthropicPayload.model);
8669
- const betaHeader = c.req.header("anthropic-beta");
8670
- 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
+ });
8671
8212
  logToolInfo(anthropicPayload);
8672
8213
  const subagentMarker = parseSubagentMarkerFromFirstUser(anthropicPayload);
8673
8214
  const initiatorOverride = subagentMarker ? "agent" : void 0;
8674
8215
  if (subagentMarker) consola.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
8675
- const useDirectAnthropicApi = supportsDirectAnthropicApi(anthropicPayload.model);
8676
- const trackingId = c.get("trackingId");
8677
- const startTime = (trackingId ? requestTracker.getRequest(trackingId) : void 0)?.startTime ?? Date.now();
8678
- updateTrackerModel(trackingId, anthropicPayload.model);
8679
- const ctx = {
8680
- historyId: recordRequest("anthropic", {
8681
- model: anthropicPayload.model,
8682
- messages: convertAnthropicMessages(anthropicPayload.messages),
8683
- stream: anthropicPayload.stream ?? false,
8684
- tools: anthropicPayload.tools?.map((t) => ({
8685
- name: t.name,
8686
- description: t.description
8687
- })),
8688
- max_tokens: anthropicPayload.max_tokens,
8689
- temperature: anthropicPayload.temperature,
8690
- system: extractSystemPrompt(anthropicPayload.system)
8691
- }),
8692
- trackingId,
8693
- startTime,
8694
- requestedModel
8695
- };
8696
- if (useDirectAnthropicApi) return handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiatorOverride);
8216
+ if (supportsDirectAnthropicApi(anthropicPayload.model)) return handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiatorOverride);
8697
8217
  return handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOverride);
8698
8218
  }
8699
8219
  /**
@@ -8793,7 +8313,7 @@ const modelRoutes = new Hono();
8793
8313
  modelRoutes.get("/", async (c) => {
8794
8314
  try {
8795
8315
  if (!state.models) await cacheModels();
8796
- const models = state.models?.data.map((model) => ({
8316
+ const models = state.models?.data.filter((model) => !isHiddenModel(model.id, state.showAllModels)).map((model) => ({
8797
8317
  id: model.id,
8798
8318
  object: "model",
8799
8319
  type: "model",
@@ -9083,33 +8603,34 @@ const TERMINAL_EVENTS = new Set([
9083
8603
  "error"
9084
8604
  ]);
9085
8605
  const handleResponses = async (c) => {
9086
- let payload = await c.req.json();
9087
- const requestedModel = captureRequestedModel(payload.model);
9088
- 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
+ });
9089
8629
  consola.debug("Responses request payload:", JSON.stringify(payload));
9090
- const trackingId = c.get("trackingId");
9091
- const startTime = (trackingId ? requestTracker.getRequest(trackingId) : void 0)?.startTime ?? Date.now();
9092
- updateTrackerModel(trackingId, payload.model);
9093
- useFunctionApplyPatch(payload);
9094
- removeWebSearchTool(payload);
9095
8630
  const model = payload.model;
9096
8631
  const stream = payload.stream ?? false;
9097
8632
  const tools = convertResponsesToolsToDefinitions(payload.tools);
9098
- const historyId = recordRequest("openai", {
9099
- model,
9100
- messages: convertResponsesInputToMessages(payload.input),
9101
- stream,
9102
- tools: tools.length > 0 ? tools : void 0,
9103
- max_tokens: payload.max_output_tokens ?? void 0,
9104
- temperature: payload.temperature ?? void 0,
9105
- system: payload.instructions ?? void 0
9106
- });
9107
- const ctx = {
9108
- historyId,
9109
- trackingId,
9110
- startTime,
9111
- requestedModel
9112
- };
8633
+ const { historyId, trackingId, startTime } = ctx;
9113
8634
  const selectedModel = findModelById(payload.model);
9114
8635
  if (!(selectedModel?.supported_endpoints?.includes(RESPONSES_ENDPOINT) ?? false)) {
9115
8636
  recordErrorResponse(ctx, model, /* @__PURE__ */ new Error("This model does not support the responses endpoint."), "responses", stream);
@@ -9149,7 +8670,7 @@ const handleResponses = async (c) => {
9149
8670
  const parsed = JSON.parse(rawData);
9150
8671
  if (typeof parsed.sequence_number === "number") lastSequenceNumber = parsed.sequence_number;
9151
8672
  } catch {}
9152
- const processedData = fixStreamIds(rawData, eventType, idTracker, requestedModel);
8673
+ const processedData = fixStreamIds(rawData, eventType, idTracker, requestedModelOf(ctx));
9153
8674
  await stream.writeSSE({
9154
8675
  id: chunk.id,
9155
8676
  event: eventType,
@@ -9323,17 +8844,14 @@ server.get("/health", (c) => {
9323
8844
  });
9324
8845
  server.route("/chat/completions", completionRoutes);
9325
8846
  server.route("/models", modelRoutes);
9326
- server.route("/embeddings", embeddingRoutes);
9327
8847
  server.route("/usage", usageRoute);
9328
8848
  server.route("/token", tokenRoute);
9329
8849
  server.route("/v1/chat/completions", completionRoutes);
9330
8850
  server.route("/v1/models", modelRoutes);
9331
- server.route("/v1/embeddings", embeddingRoutes);
9332
8851
  server.route("/v1/messages", messageRoutes);
9333
8852
  server.route("/api/event_logging", eventLoggingRoutes);
9334
8853
  server.route("/v1/responses", responsesRoutes);
9335
8854
  server.route("/responses", responsesRoutes);
9336
- server.route("/v1beta/models", geminiRoutes);
9337
8855
  server.route("/history", historyRoutes);
9338
8856
 
9339
8857
  //#endregion
@@ -9381,6 +8899,8 @@ async function runServer(options) {
9381
8899
  if (options.accountType !== "individual") consola.info(`Using ${options.accountType} plan GitHub account`);
9382
8900
  state.manualApprove = options.manual;
9383
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");
9384
8904
  state.autoTruncate = options.autoTruncate;
9385
8905
  state.compressToolResults = options.compressToolResults;
9386
8906
  state.redirectAnthropic = options.redirectAnthropic;
@@ -9425,17 +8945,27 @@ async function runServer(options) {
9425
8945
  consola.error(error instanceof Error ? error.message : String(error));
9426
8946
  process.exit(1);
9427
8947
  }
9428
- 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")}`);
9429
8956
  const serverUrl = `http://${options.host ?? "localhost"}:${options.port}`;
9430
8957
  if (options.claudeCode) {
9431
- 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
+ }
9432
8962
  const selectedModel = await consola.prompt("Select a model to use with Claude Code", {
9433
8963
  type: "select",
9434
- options: state.models.data.map((model) => model.id)
8964
+ options: visibleModels.map((model) => model.id)
9435
8965
  });
9436
8966
  const selectedSmallModel = await consola.prompt("Select a small model to use with Claude Code", {
9437
8967
  type: "select",
9438
- options: state.models.data.map((model) => model.id)
8968
+ options: visibleModels.map((model) => model.id)
9439
8969
  });
9440
8970
  const command = generateEnvScript({
9441
8971
  ANTHROPIC_BASE_URL: serverUrl,
@@ -9555,6 +9085,11 @@ const start = defineCommand({
9555
9085
  default: false,
9556
9086
  description: "Show GitHub and Copilot tokens on fetch and refresh"
9557
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
+ },
9558
9093
  "proxy-env": {
9559
9094
  type: "boolean",
9560
9095
  default: false,
@@ -9620,6 +9155,7 @@ const start = defineCommand({
9620
9155
  githubToken: args["github-token"],
9621
9156
  claudeCode: args["claude-code"],
9622
9157
  showToken: args["show-token"],
9158
+ showAllModels: args["show-all-models"],
9623
9159
  proxyEnv: args["proxy-env"],
9624
9160
  history: !args["no-history"],
9625
9161
  historyLimit: Number.parseInt(args["history-limit"], 10),