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