@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.
- package/README.md +32 -29
- package/dist/main.mjs +801 -1265
- 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 {
|
|
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.
|
|
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/
|
|
3328
|
+
//#region src/lib/auto-truncate-openai.ts
|
|
3552
3329
|
/**
|
|
3553
|
-
*
|
|
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$
|
|
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$
|
|
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/
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
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
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
*
|
|
4070
|
-
*
|
|
4071
|
-
*
|
|
4072
|
-
*
|
|
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
|
-
|
|
4075
|
-
|
|
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
|
-
*
|
|
4079
|
-
*
|
|
4080
|
-
*
|
|
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
|
|
4083
|
-
|
|
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
|
-
*
|
|
4087
|
-
*
|
|
4088
|
-
*
|
|
4089
|
-
*
|
|
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
|
-
*
|
|
4189
|
-
*
|
|
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
|
|
4192
|
-
|
|
4193
|
-
const
|
|
4194
|
-
|
|
4195
|
-
const
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
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
|
-
*
|
|
4269
|
-
*
|
|
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
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
if (
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
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
|
|
4321
|
-
consola.debug("Request payload:", JSON.stringify(
|
|
4322
|
-
const
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
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:
|
|
4336
|
-
temperature:
|
|
4337
|
-
})
|
|
4338
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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
|
|
8667
|
-
consola.debug("Anthropic request payload:", JSON.stringify(
|
|
8668
|
-
const
|
|
8669
|
-
|
|
8670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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),
|