@hsupu/copilot-api 0.7.4 → 0.7.5
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 +10 -0
- package/dist/main.js +597 -288
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -3,8 +3,9 @@ import { defineCommand, runMain } from "citty";
|
|
|
3
3
|
import consola from "consola";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
|
-
import path from "node:path";
|
|
6
|
+
import path, { join } from "node:path";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
8
9
|
import clipboard from "clipboardy";
|
|
9
10
|
import { serve } from "srvx";
|
|
10
11
|
import invariant from "tiny-invariant";
|
|
@@ -136,7 +137,7 @@ function formatRequestTooLargeError() {
|
|
|
136
137
|
}
|
|
137
138
|
};
|
|
138
139
|
}
|
|
139
|
-
|
|
140
|
+
function forwardError(c, error) {
|
|
140
141
|
consola.error("Error occurred:", error);
|
|
141
142
|
if (error instanceof HTTPError) {
|
|
142
143
|
if (error.status === 413) {
|
|
@@ -290,6 +291,24 @@ async function pollAccessToken(deviceCode) {
|
|
|
290
291
|
//#region src/lib/token.ts
|
|
291
292
|
const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8");
|
|
292
293
|
const writeGithubToken = (token) => fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token);
|
|
294
|
+
/**
|
|
295
|
+
* Refresh the Copilot token with exponential backoff retry.
|
|
296
|
+
* Returns the new token on success, or null if all retries fail.
|
|
297
|
+
*/
|
|
298
|
+
async function refreshCopilotTokenWithRetry(maxRetries = 3) {
|
|
299
|
+
let lastError = null;
|
|
300
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) try {
|
|
301
|
+
const { token } = await getCopilotToken();
|
|
302
|
+
return token;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
lastError = error;
|
|
305
|
+
const delay = Math.min(1e3 * 2 ** attempt, 3e4);
|
|
306
|
+
consola.warn(`Token refresh attempt ${attempt + 1}/${maxRetries} failed, retrying in ${delay}ms`);
|
|
307
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
308
|
+
}
|
|
309
|
+
consola.error("All token refresh attempts failed:", lastError);
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
293
312
|
const setupCopilotToken = async () => {
|
|
294
313
|
const { token, refresh_in } = await getCopilotToken();
|
|
295
314
|
state.copilotToken = token;
|
|
@@ -298,14 +317,12 @@ const setupCopilotToken = async () => {
|
|
|
298
317
|
const refreshInterval = (refresh_in - 60) * 1e3;
|
|
299
318
|
setInterval(async () => {
|
|
300
319
|
consola.debug("Refreshing Copilot token");
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
state.copilotToken =
|
|
320
|
+
const newToken = await refreshCopilotTokenWithRetry();
|
|
321
|
+
if (newToken) {
|
|
322
|
+
state.copilotToken = newToken;
|
|
304
323
|
consola.debug("Copilot token refreshed");
|
|
305
|
-
if (state.showToken) consola.info("Refreshed Copilot token:",
|
|
306
|
-
}
|
|
307
|
-
consola.error("Failed to refresh Copilot token (will retry on next interval):", error);
|
|
308
|
-
}
|
|
324
|
+
if (state.showToken) consola.info("Refreshed Copilot token:", newToken);
|
|
325
|
+
} else consola.error("Failed to refresh Copilot token after retries, using existing token");
|
|
309
326
|
}, refreshInterval);
|
|
310
327
|
};
|
|
311
328
|
async function setupGitHubToken(options) {
|
|
@@ -520,6 +537,167 @@ const logout = defineCommand({
|
|
|
520
537
|
}
|
|
521
538
|
});
|
|
522
539
|
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/patch-claude.ts
|
|
542
|
+
const ORIGINAL_PATTERN = /function HR\(A\)\{if\(A\.includes\("\[1m\]"\)\)return 1e6;return 200000\}/;
|
|
543
|
+
const PATCHED_PATTERN = /function HR\(A\)\{if\(A\.includes\("\[1m\]"\)\)return 1e6;return \d+\}/;
|
|
544
|
+
/**
|
|
545
|
+
* Search volta tools directory for Claude Code
|
|
546
|
+
*/
|
|
547
|
+
function findInVoltaTools(voltaHome) {
|
|
548
|
+
const paths = [];
|
|
549
|
+
const toolsDir = join(voltaHome, "tools", "image", "node");
|
|
550
|
+
if (!existsSync(toolsDir)) return paths;
|
|
551
|
+
try {
|
|
552
|
+
for (const version of readdirSync(toolsDir)) {
|
|
553
|
+
const claudePath = join(toolsDir, version, "lib", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
|
|
554
|
+
if (existsSync(claudePath)) paths.push(claudePath);
|
|
555
|
+
}
|
|
556
|
+
} catch {}
|
|
557
|
+
return paths;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Find Claude Code CLI path by checking common locations
|
|
561
|
+
*/
|
|
562
|
+
function findClaudeCodePath() {
|
|
563
|
+
const possiblePaths = [];
|
|
564
|
+
const home = process.env.HOME || "";
|
|
565
|
+
const voltaHome = process.env.VOLTA_HOME || join(home, ".volta");
|
|
566
|
+
if (existsSync(voltaHome)) possiblePaths.push(...findInVoltaTools(voltaHome));
|
|
567
|
+
const npmPrefix = process.env.npm_config_prefix;
|
|
568
|
+
if (npmPrefix) possiblePaths.push(join(npmPrefix, "lib", "node_modules", "@anthropic-ai", "claude-code", "cli.js"));
|
|
569
|
+
const globalPaths = [
|
|
570
|
+
join(home, ".npm-global", "lib", "node_modules"),
|
|
571
|
+
"/usr/local/lib/node_modules",
|
|
572
|
+
"/usr/lib/node_modules"
|
|
573
|
+
];
|
|
574
|
+
for (const base of globalPaths) possiblePaths.push(join(base, "@anthropic-ai", "claude-code", "cli.js"));
|
|
575
|
+
const bunGlobal = join(home, ".bun", "install", "global");
|
|
576
|
+
if (existsSync(bunGlobal)) possiblePaths.push(join(bunGlobal, "node_modules", "@anthropic-ai", "claude-code", "cli.js"));
|
|
577
|
+
return possiblePaths.find((p) => existsSync(p)) ?? null;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Get current context limit from Claude Code
|
|
581
|
+
*/
|
|
582
|
+
function getCurrentLimit(content) {
|
|
583
|
+
const match = content.match(PATCHED_PATTERN);
|
|
584
|
+
if (!match) return null;
|
|
585
|
+
const limitMatch = match[0].match(/return (\d+)\}$/);
|
|
586
|
+
return limitMatch ? Number.parseInt(limitMatch[1], 10) : null;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Patch Claude Code to use a different context limit
|
|
590
|
+
*/
|
|
591
|
+
function patchClaudeCode(cliPath, newLimit) {
|
|
592
|
+
const content = readFileSync(cliPath, "utf8");
|
|
593
|
+
if (getCurrentLimit(content) === newLimit) {
|
|
594
|
+
consola.info(`Already patched with limit ${newLimit}`);
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
const replacement = `function HR(A){if(A.includes("[1m]"))return 1e6;return ${newLimit}}`;
|
|
598
|
+
let newContent;
|
|
599
|
+
if (ORIGINAL_PATTERN.test(content)) newContent = content.replace(ORIGINAL_PATTERN, replacement);
|
|
600
|
+
else if (PATCHED_PATTERN.test(content)) newContent = content.replace(PATCHED_PATTERN, replacement);
|
|
601
|
+
else return false;
|
|
602
|
+
writeFileSync(cliPath, newContent);
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Restore Claude Code to original 200k limit
|
|
607
|
+
*/
|
|
608
|
+
function restoreClaudeCode(cliPath) {
|
|
609
|
+
const content = readFileSync(cliPath, "utf8");
|
|
610
|
+
if (getCurrentLimit(content) === 2e5) {
|
|
611
|
+
consola.info("Already at original 200000 limit");
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
if (!PATCHED_PATTERN.test(content)) return false;
|
|
615
|
+
const newContent = content.replace(PATCHED_PATTERN, "function HR(A){if(A.includes(\"[1m]\"))return 1e6;return 200000}");
|
|
616
|
+
writeFileSync(cliPath, newContent);
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
function showStatus(currentLimit) {
|
|
620
|
+
if (currentLimit === null) {
|
|
621
|
+
consola.warn("Could not detect current limit - CLI may have been updated");
|
|
622
|
+
consola.info("Look for the HR function pattern in cli.js");
|
|
623
|
+
} else if (currentLimit === 2e5) consola.info("Status: Original (200k context window)");
|
|
624
|
+
else consola.info(`Status: Patched (${currentLimit} context window)`);
|
|
625
|
+
}
|
|
626
|
+
const patchClaude = defineCommand({
|
|
627
|
+
meta: {
|
|
628
|
+
name: "patch-claude",
|
|
629
|
+
description: "Patch Claude Code's context window limit to match Copilot's limits"
|
|
630
|
+
},
|
|
631
|
+
args: {
|
|
632
|
+
limit: {
|
|
633
|
+
alias: "l",
|
|
634
|
+
type: "string",
|
|
635
|
+
default: "128000",
|
|
636
|
+
description: "Context window limit in tokens (default: 128000 for Copilot)"
|
|
637
|
+
},
|
|
638
|
+
restore: {
|
|
639
|
+
alias: "r",
|
|
640
|
+
type: "boolean",
|
|
641
|
+
default: false,
|
|
642
|
+
description: "Restore original 200k limit"
|
|
643
|
+
},
|
|
644
|
+
path: {
|
|
645
|
+
alias: "p",
|
|
646
|
+
type: "string",
|
|
647
|
+
description: "Path to Claude Code cli.js (auto-detected if not specified)"
|
|
648
|
+
},
|
|
649
|
+
status: {
|
|
650
|
+
alias: "s",
|
|
651
|
+
type: "boolean",
|
|
652
|
+
default: false,
|
|
653
|
+
description: "Show current patch status without modifying"
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
run({ args }) {
|
|
657
|
+
const cliPath = args.path || findClaudeCodePath();
|
|
658
|
+
if (!cliPath) {
|
|
659
|
+
consola.error("Could not find Claude Code installation");
|
|
660
|
+
consola.info("Searched in: volta, npm global, bun global");
|
|
661
|
+
consola.info("Use --path to specify the path to cli.js manually");
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
if (!existsSync(cliPath)) {
|
|
665
|
+
consola.error(`File not found: ${cliPath}`);
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
consola.info(`Claude Code path: ${cliPath}`);
|
|
669
|
+
const content = readFileSync(cliPath, "utf8");
|
|
670
|
+
const currentLimit = getCurrentLimit(content);
|
|
671
|
+
if (args.status) {
|
|
672
|
+
showStatus(currentLimit);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (args.restore) {
|
|
676
|
+
if (restoreClaudeCode(cliPath)) consola.success("Restored to original 200k limit");
|
|
677
|
+
else {
|
|
678
|
+
consola.error("Failed to restore - pattern not found");
|
|
679
|
+
consola.info("Claude Code may have been updated to a new version");
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const limit = Number.parseInt(args.limit, 10);
|
|
685
|
+
if (Number.isNaN(limit) || limit < 1e3) {
|
|
686
|
+
consola.error("Invalid limit value. Must be a number >= 1000");
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
if (patchClaudeCode(cliPath, limit)) {
|
|
690
|
+
consola.success(`Patched context window: 200000 → ${limit}`);
|
|
691
|
+
consola.info("Note: You may need to re-run this after Claude Code updates");
|
|
692
|
+
} else {
|
|
693
|
+
consola.error("Failed to patch - pattern not found");
|
|
694
|
+
consola.info("Claude Code may have been updated to a new version");
|
|
695
|
+
consola.info("Check the cli.js for the HR function pattern");
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
523
701
|
//#endregion
|
|
524
702
|
//#region src/lib/history.ts
|
|
525
703
|
function generateId$1() {
|
|
@@ -771,44 +949,74 @@ function exportHistory(format = "json") {
|
|
|
771
949
|
|
|
772
950
|
//#endregion
|
|
773
951
|
//#region src/lib/proxy.ts
|
|
952
|
+
/**
|
|
953
|
+
* Custom dispatcher that routes requests through proxies based on environment variables.
|
|
954
|
+
* Extends Agent to properly inherit the Dispatcher interface.
|
|
955
|
+
*/
|
|
956
|
+
var ProxyDispatcher = class extends Agent {
|
|
957
|
+
proxies = /* @__PURE__ */ new Map();
|
|
958
|
+
dispatch(options, handler) {
|
|
959
|
+
try {
|
|
960
|
+
const origin = this.getOriginUrl(options.origin);
|
|
961
|
+
const proxyUrl = this.getProxyUrl(origin);
|
|
962
|
+
if (!proxyUrl) {
|
|
963
|
+
consola.debug(`HTTP proxy bypass: ${origin.hostname}`);
|
|
964
|
+
return super.dispatch(options, handler);
|
|
965
|
+
}
|
|
966
|
+
const agent = this.getOrCreateProxyAgent(proxyUrl);
|
|
967
|
+
consola.debug(`HTTP proxy route: ${origin.hostname} via ${this.formatProxyLabel(proxyUrl)}`);
|
|
968
|
+
return agent.dispatch(options, handler);
|
|
969
|
+
} catch {
|
|
970
|
+
return super.dispatch(options, handler);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
getOriginUrl(origin) {
|
|
974
|
+
return typeof origin === "string" ? new URL(origin) : origin;
|
|
975
|
+
}
|
|
976
|
+
getProxyUrl(origin) {
|
|
977
|
+
const raw = getProxyForUrl(origin.toString());
|
|
978
|
+
return raw && raw.length > 0 ? raw : void 0;
|
|
979
|
+
}
|
|
980
|
+
getOrCreateProxyAgent(proxyUrl) {
|
|
981
|
+
let agent = this.proxies.get(proxyUrl);
|
|
982
|
+
if (!agent) {
|
|
983
|
+
agent = new ProxyAgent(proxyUrl);
|
|
984
|
+
this.proxies.set(proxyUrl, agent);
|
|
985
|
+
}
|
|
986
|
+
return agent;
|
|
987
|
+
}
|
|
988
|
+
formatProxyLabel(proxyUrl) {
|
|
989
|
+
try {
|
|
990
|
+
const u = new URL(proxyUrl);
|
|
991
|
+
return `${u.protocol}//${u.host}`;
|
|
992
|
+
} catch {
|
|
993
|
+
return proxyUrl;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
async close() {
|
|
997
|
+
await super.close();
|
|
998
|
+
await Promise.all([...this.proxies.values()].map((p) => p.close()));
|
|
999
|
+
this.proxies.clear();
|
|
1000
|
+
}
|
|
1001
|
+
destroy(errOrCallback, callback) {
|
|
1002
|
+
for (const agent of this.proxies.values()) if (typeof errOrCallback === "function") agent.destroy(errOrCallback);
|
|
1003
|
+
else if (callback) agent.destroy(errOrCallback ?? null, callback);
|
|
1004
|
+
else agent.destroy(errOrCallback ?? null).catch(() => {});
|
|
1005
|
+
this.proxies.clear();
|
|
1006
|
+
if (typeof errOrCallback === "function") {
|
|
1007
|
+
super.destroy(errOrCallback);
|
|
1008
|
+
return;
|
|
1009
|
+
} else if (callback) {
|
|
1010
|
+
super.destroy(errOrCallback ?? null, callback);
|
|
1011
|
+
return;
|
|
1012
|
+
} else return super.destroy(errOrCallback ?? null);
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
774
1015
|
function initProxyFromEnv() {
|
|
775
1016
|
if (typeof Bun !== "undefined") return;
|
|
776
1017
|
try {
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
setGlobalDispatcher({
|
|
780
|
-
dispatch(options, handler) {
|
|
781
|
-
try {
|
|
782
|
-
const origin = typeof options.origin === "string" ? new URL(options.origin) : options.origin;
|
|
783
|
-
const raw = getProxyForUrl(origin.toString());
|
|
784
|
-
const proxyUrl = raw && raw.length > 0 ? raw : void 0;
|
|
785
|
-
if (!proxyUrl) {
|
|
786
|
-
consola.debug(`HTTP proxy bypass: ${origin.hostname}`);
|
|
787
|
-
return direct.dispatch(options, handler);
|
|
788
|
-
}
|
|
789
|
-
let agent = proxies.get(proxyUrl);
|
|
790
|
-
if (!agent) {
|
|
791
|
-
agent = new ProxyAgent(proxyUrl);
|
|
792
|
-
proxies.set(proxyUrl, agent);
|
|
793
|
-
}
|
|
794
|
-
let label = proxyUrl;
|
|
795
|
-
try {
|
|
796
|
-
const u = new URL(proxyUrl);
|
|
797
|
-
label = `${u.protocol}//${u.host}`;
|
|
798
|
-
} catch {}
|
|
799
|
-
consola.debug(`HTTP proxy route: ${origin.hostname} via ${label}`);
|
|
800
|
-
return agent.dispatch(options, handler);
|
|
801
|
-
} catch {
|
|
802
|
-
return direct.dispatch(options, handler);
|
|
803
|
-
}
|
|
804
|
-
},
|
|
805
|
-
close() {
|
|
806
|
-
return direct.close();
|
|
807
|
-
},
|
|
808
|
-
destroy() {
|
|
809
|
-
return direct.destroy();
|
|
810
|
-
}
|
|
811
|
-
});
|
|
1018
|
+
const dispatcher = new ProxyDispatcher();
|
|
1019
|
+
setGlobalDispatcher(dispatcher);
|
|
812
1020
|
consola.debug("HTTP proxy configured from environment (per-URL)");
|
|
813
1021
|
} catch (err) {
|
|
814
1022
|
consola.debug("Proxy setup skipped:", err);
|
|
@@ -893,24 +1101,56 @@ function formatTokens(input, output) {
|
|
|
893
1101
|
/**
|
|
894
1102
|
* Console renderer that shows request lifecycle with apt-get style footer
|
|
895
1103
|
*
|
|
896
|
-
* Log format:
|
|
897
|
-
* - Start: [....] METHOD /path model-name
|
|
898
|
-
* - Streaming: [<-->] METHOD /path model-name streaming...
|
|
899
|
-
* - Complete: [ OK ] METHOD /path 200 1.2s 1.5K/500 model-name
|
|
1104
|
+
* Log format (status prefix first, then timestamp):
|
|
1105
|
+
* - Start: [....] HH:MM:SS METHOD /path model-name
|
|
1106
|
+
* - Streaming: [<-->] HH:MM:SS METHOD /path model-name streaming...
|
|
1107
|
+
* - Complete: [ OK ] HH:MM:SS METHOD /path 200 1.2s 1.5K/500 model-name
|
|
1108
|
+
* - Error: [FAIL] HH:MM:SS METHOD /path 500 1.2s model-name: error message
|
|
900
1109
|
*
|
|
901
1110
|
* Features:
|
|
902
1111
|
* - /history API requests are displayed in gray (dim)
|
|
903
1112
|
* - Sticky footer shows active request count, updated in-place on the last line
|
|
904
1113
|
* - Footer disappears when all requests complete
|
|
1114
|
+
* - Intercepts consola output to properly handle footer
|
|
905
1115
|
*/
|
|
906
1116
|
var ConsoleRenderer = class {
|
|
907
1117
|
activeRequests = /* @__PURE__ */ new Map();
|
|
908
1118
|
showActive;
|
|
909
1119
|
footerVisible = false;
|
|
910
1120
|
isTTY;
|
|
1121
|
+
originalReporters = [];
|
|
911
1122
|
constructor(options) {
|
|
912
1123
|
this.showActive = options?.showActive ?? true;
|
|
913
1124
|
this.isTTY = process.stdout.isTTY;
|
|
1125
|
+
this.installConsolaReporter();
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Install a custom consola reporter that coordinates with footer
|
|
1129
|
+
*/
|
|
1130
|
+
installConsolaReporter() {
|
|
1131
|
+
this.originalReporters = [...consola.options.reporters];
|
|
1132
|
+
consola.setReporters([{ log: (logObj) => {
|
|
1133
|
+
this.clearFooterForLog();
|
|
1134
|
+
const message = logObj.args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg)).join(" ");
|
|
1135
|
+
const prefix = this.getLogPrefix(logObj.type);
|
|
1136
|
+
if (prefix) process.stdout.write(`${prefix} ${message}\n`);
|
|
1137
|
+
else process.stdout.write(`${message}\n`);
|
|
1138
|
+
this.renderFooter();
|
|
1139
|
+
} }]);
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Get log prefix based on log type
|
|
1143
|
+
*/
|
|
1144
|
+
getLogPrefix(type) {
|
|
1145
|
+
switch (type) {
|
|
1146
|
+
case "error":
|
|
1147
|
+
case "fatal": return pc.red("✖");
|
|
1148
|
+
case "warn": return pc.yellow("⚠");
|
|
1149
|
+
case "info": return pc.cyan("ℹ");
|
|
1150
|
+
case "success": return pc.green("✔");
|
|
1151
|
+
case "debug": return pc.gray("●");
|
|
1152
|
+
default: return "";
|
|
1153
|
+
}
|
|
914
1154
|
}
|
|
915
1155
|
/**
|
|
916
1156
|
* Get footer text based on active request count
|
|
@@ -953,8 +1193,8 @@ var ConsoleRenderer = class {
|
|
|
953
1193
|
*/
|
|
954
1194
|
printLog(message, isGray = false) {
|
|
955
1195
|
this.clearFooterForLog();
|
|
956
|
-
if (isGray)
|
|
957
|
-
else
|
|
1196
|
+
if (isGray) process.stdout.write(pc.dim(message) + "\n");
|
|
1197
|
+
else process.stdout.write(message + "\n");
|
|
958
1198
|
this.renderFooter();
|
|
959
1199
|
}
|
|
960
1200
|
onRequestStart(request) {
|
|
@@ -963,7 +1203,7 @@ var ConsoleRenderer = class {
|
|
|
963
1203
|
const time = formatTime();
|
|
964
1204
|
const modelInfo = request.model ? ` ${request.model}` : "";
|
|
965
1205
|
const queueInfo = request.queuePosition !== void 0 && request.queuePosition > 0 ? ` [q#${request.queuePosition}]` : "";
|
|
966
|
-
const message =
|
|
1206
|
+
const message = `[....] ${time} ${request.method} ${request.path}${modelInfo}${queueInfo}`;
|
|
967
1207
|
this.printLog(message, request.isHistoryAccess);
|
|
968
1208
|
}
|
|
969
1209
|
}
|
|
@@ -974,7 +1214,7 @@ var ConsoleRenderer = class {
|
|
|
974
1214
|
if (this.showActive && update.status === "streaming") {
|
|
975
1215
|
const time = formatTime();
|
|
976
1216
|
const modelInfo = request.model ? ` ${request.model}` : "";
|
|
977
|
-
const message =
|
|
1217
|
+
const message = `[<-->] ${time} ${request.method} ${request.path}${modelInfo} streaming...`;
|
|
978
1218
|
this.printLog(message, request.isHistoryAccess);
|
|
979
1219
|
}
|
|
980
1220
|
}
|
|
@@ -988,7 +1228,7 @@ var ConsoleRenderer = class {
|
|
|
988
1228
|
const isError = request.status === "error" || status >= 400;
|
|
989
1229
|
const prefix = isError ? "[FAIL]" : "[ OK ]";
|
|
990
1230
|
const tokensPart = tokens ? ` ${tokens}` : "";
|
|
991
|
-
let content = `${
|
|
1231
|
+
let content = `${prefix} ${time} ${request.method} ${request.path} ${status} ${duration}${tokensPart}${modelInfo}`;
|
|
992
1232
|
if (isError) {
|
|
993
1233
|
const errorInfo = request.error ? `: ${request.error}` : "";
|
|
994
1234
|
content += errorInfo;
|
|
@@ -1001,6 +1241,7 @@ var ConsoleRenderer = class {
|
|
|
1001
1241
|
this.footerVisible = false;
|
|
1002
1242
|
}
|
|
1003
1243
|
this.activeRequests.clear();
|
|
1244
|
+
if (this.originalReporters.length > 0) consola.setReporters(this.originalReporters);
|
|
1004
1245
|
}
|
|
1005
1246
|
};
|
|
1006
1247
|
|
|
@@ -1392,14 +1633,14 @@ const getTokenCount = async (payload, model) => {
|
|
|
1392
1633
|
//#endregion
|
|
1393
1634
|
//#region src/lib/auto-compact.ts
|
|
1394
1635
|
const DEFAULT_CONFIG = {
|
|
1395
|
-
targetTokens:
|
|
1396
|
-
safetyMarginPercent:
|
|
1636
|
+
targetTokens: 12e4,
|
|
1637
|
+
safetyMarginPercent: 2
|
|
1397
1638
|
};
|
|
1398
1639
|
/**
|
|
1399
1640
|
* Check if payload needs compaction based on model limits.
|
|
1400
1641
|
* Uses a safety margin to account for token counting differences.
|
|
1401
1642
|
*/
|
|
1402
|
-
async function checkNeedsCompaction(payload, model, safetyMarginPercent =
|
|
1643
|
+
async function checkNeedsCompaction(payload, model, safetyMarginPercent = 2) {
|
|
1403
1644
|
const currentTokens = (await getTokenCount(payload, model)).input;
|
|
1404
1645
|
const rawLimit = model.capabilities?.limits?.max_prompt_tokens ?? 128e3;
|
|
1405
1646
|
const limit = Math.floor(rawLimit * (1 - safetyMarginPercent / 100));
|
|
@@ -1442,6 +1683,13 @@ function extractSystemMessages(messages) {
|
|
|
1442
1683
|
};
|
|
1443
1684
|
}
|
|
1444
1685
|
/**
|
|
1686
|
+
* Extract tool_use ids from assistant messages with tool_calls.
|
|
1687
|
+
*/
|
|
1688
|
+
function getToolUseIds(message) {
|
|
1689
|
+
if (message.role === "assistant" && message.tool_calls) return message.tool_calls.map((tc) => tc.id);
|
|
1690
|
+
return [];
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1445
1693
|
* Find messages to keep from the end to stay under target tokens.
|
|
1446
1694
|
* Returns the starting index of messages to preserve.
|
|
1447
1695
|
*/
|
|
@@ -1456,6 +1704,41 @@ function findPreserveIndex(messages, targetTokens, systemTokens) {
|
|
|
1456
1704
|
return 0;
|
|
1457
1705
|
}
|
|
1458
1706
|
/**
|
|
1707
|
+
* Filter out orphaned tool_result messages that don't have a matching tool_use
|
|
1708
|
+
* in the preserved message list. This prevents API errors when truncation
|
|
1709
|
+
* separates tool_use/tool_result pairs.
|
|
1710
|
+
*/
|
|
1711
|
+
function filterOrphanedToolResults(messages) {
|
|
1712
|
+
const availableToolUseIds = /* @__PURE__ */ new Set();
|
|
1713
|
+
for (const msg of messages) for (const id of getToolUseIds(msg)) availableToolUseIds.add(id);
|
|
1714
|
+
const filteredMessages = [];
|
|
1715
|
+
let removedCount = 0;
|
|
1716
|
+
for (const msg of messages) {
|
|
1717
|
+
if (msg.role === "tool" && msg.tool_call_id && !availableToolUseIds.has(msg.tool_call_id)) {
|
|
1718
|
+
removedCount++;
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
filteredMessages.push(msg);
|
|
1722
|
+
}
|
|
1723
|
+
if (removedCount > 0) consola.info(`Auto-compact: Removed ${removedCount} orphaned tool_result message(s) without matching tool_use`);
|
|
1724
|
+
return filteredMessages;
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Ensure the message list starts with a user message.
|
|
1728
|
+
* If it starts with assistant or tool messages, skip them until we find a user message.
|
|
1729
|
+
* This is required because OpenAI API expects conversations to start with user messages
|
|
1730
|
+
* (after system messages).
|
|
1731
|
+
*/
|
|
1732
|
+
function ensureStartsWithUser(messages) {
|
|
1733
|
+
let startIndex = 0;
|
|
1734
|
+
while (startIndex < messages.length) {
|
|
1735
|
+
if (messages[startIndex].role === "user") break;
|
|
1736
|
+
startIndex++;
|
|
1737
|
+
}
|
|
1738
|
+
if (startIndex > 0) consola.info(`Auto-compact: Skipped ${startIndex} leading non-user message(s) to ensure valid sequence`);
|
|
1739
|
+
return messages.slice(startIndex);
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1459
1742
|
* Calculate estimated tokens for system messages.
|
|
1460
1743
|
*/
|
|
1461
1744
|
function estimateSystemTokens(systemMessages) {
|
|
@@ -1473,6 +1756,7 @@ function createTruncationMarker(removedCount) {
|
|
|
1473
1756
|
/**
|
|
1474
1757
|
* Perform auto-compaction on a payload that exceeds token limits.
|
|
1475
1758
|
* This uses simple truncation - no LLM calls required.
|
|
1759
|
+
* Uses iterative approach with decreasing target tokens until under limit.
|
|
1476
1760
|
*/
|
|
1477
1761
|
async function autoCompact(payload, model, config = {}) {
|
|
1478
1762
|
const cfg = {
|
|
@@ -1493,8 +1777,49 @@ async function autoCompact(payload, model, config = {}) {
|
|
|
1493
1777
|
const { systemMessages, remainingMessages } = extractSystemMessages(payload.messages);
|
|
1494
1778
|
const systemTokens = estimateSystemTokens(systemMessages);
|
|
1495
1779
|
consola.debug(`Auto-compact: ${systemMessages.length} system messages (~${systemTokens} tokens)`);
|
|
1496
|
-
const
|
|
1497
|
-
const
|
|
1780
|
+
const MAX_ITERATIONS = 5;
|
|
1781
|
+
const MIN_TARGET = 2e4;
|
|
1782
|
+
let currentTarget = Math.min(cfg.targetTokens, limit);
|
|
1783
|
+
let lastResult = null;
|
|
1784
|
+
for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
|
|
1785
|
+
const result = await tryCompactWithTarget({
|
|
1786
|
+
payload,
|
|
1787
|
+
model,
|
|
1788
|
+
systemMessages,
|
|
1789
|
+
remainingMessages,
|
|
1790
|
+
systemTokens,
|
|
1791
|
+
targetTokens: currentTarget,
|
|
1792
|
+
limit,
|
|
1793
|
+
originalTokens
|
|
1794
|
+
});
|
|
1795
|
+
if (!result.wasCompacted) return result;
|
|
1796
|
+
lastResult = result;
|
|
1797
|
+
if (result.compactedTokens <= limit) {
|
|
1798
|
+
consola.info(`Auto-compact: ${originalTokens} → ${result.compactedTokens} tokens (removed ${result.removedMessageCount} messages)`);
|
|
1799
|
+
return result;
|
|
1800
|
+
}
|
|
1801
|
+
consola.warn(`Auto-compact: Still over limit (${result.compactedTokens} > ${limit}), trying more aggressive truncation`);
|
|
1802
|
+
currentTarget = Math.floor(currentTarget * .7);
|
|
1803
|
+
if (currentTarget < MIN_TARGET) {
|
|
1804
|
+
consola.error("Auto-compact: Cannot reduce further, target too low");
|
|
1805
|
+
return result;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
consola.error(`Auto-compact: Exhausted ${MAX_ITERATIONS} iterations, returning best effort`);
|
|
1809
|
+
return lastResult ?? {
|
|
1810
|
+
payload,
|
|
1811
|
+
wasCompacted: false,
|
|
1812
|
+
originalTokens,
|
|
1813
|
+
compactedTokens: originalTokens,
|
|
1814
|
+
removedMessageCount: 0
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Helper to attempt compaction with a specific target token count.
|
|
1819
|
+
*/
|
|
1820
|
+
async function tryCompactWithTarget(opts) {
|
|
1821
|
+
const { payload, model, systemMessages, remainingMessages, systemTokens, targetTokens, originalTokens } = opts;
|
|
1822
|
+
const preserveIndex = findPreserveIndex(remainingMessages, targetTokens, systemTokens);
|
|
1498
1823
|
if (preserveIndex === 0) {
|
|
1499
1824
|
consola.warn("Auto-compact: Cannot truncate further without losing all conversation history");
|
|
1500
1825
|
return {
|
|
@@ -1506,8 +1831,21 @@ async function autoCompact(payload, model, config = {}) {
|
|
|
1506
1831
|
};
|
|
1507
1832
|
}
|
|
1508
1833
|
const removedMessages = remainingMessages.slice(0, preserveIndex);
|
|
1509
|
-
|
|
1510
|
-
|
|
1834
|
+
let preservedMessages = remainingMessages.slice(preserveIndex);
|
|
1835
|
+
preservedMessages = filterOrphanedToolResults(preservedMessages);
|
|
1836
|
+
preservedMessages = ensureStartsWithUser(preservedMessages);
|
|
1837
|
+
preservedMessages = filterOrphanedToolResults(preservedMessages);
|
|
1838
|
+
if (preservedMessages.length === 0) {
|
|
1839
|
+
consola.warn("Auto-compact: All messages were filtered out after cleanup, cannot compact");
|
|
1840
|
+
return {
|
|
1841
|
+
payload,
|
|
1842
|
+
wasCompacted: false,
|
|
1843
|
+
originalTokens,
|
|
1844
|
+
compactedTokens: originalTokens,
|
|
1845
|
+
removedMessageCount: 0
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
consola.debug(`Auto-compact: Removing ${removedMessages.length} messages, keeping ${preservedMessages.length}`);
|
|
1511
1849
|
const truncationMarker = createTruncationMarker(removedMessages.length);
|
|
1512
1850
|
const newPayload = {
|
|
1513
1851
|
...payload,
|
|
@@ -1518,25 +1856,6 @@ async function autoCompact(payload, model, config = {}) {
|
|
|
1518
1856
|
]
|
|
1519
1857
|
};
|
|
1520
1858
|
const newTokenCount = await getTokenCount(newPayload, model);
|
|
1521
|
-
consola.info(`Auto-compact: Reduced from ${originalTokens} to ${newTokenCount.input} tokens`);
|
|
1522
|
-
if (newTokenCount.input > limit) {
|
|
1523
|
-
consola.warn(`Auto-compact: Still over limit (${newTokenCount.input} > ${limit}), trying more aggressive truncation`);
|
|
1524
|
-
const aggressiveTarget = Math.floor(effectiveTarget * .7);
|
|
1525
|
-
if (aggressiveTarget < 2e4) {
|
|
1526
|
-
consola.error("Auto-compact: Cannot reduce further, target too low");
|
|
1527
|
-
return {
|
|
1528
|
-
payload: newPayload,
|
|
1529
|
-
wasCompacted: true,
|
|
1530
|
-
originalTokens,
|
|
1531
|
-
compactedTokens: newTokenCount.input,
|
|
1532
|
-
removedMessageCount: removedMessages.length
|
|
1533
|
-
};
|
|
1534
|
-
}
|
|
1535
|
-
return autoCompact(payload, model, {
|
|
1536
|
-
...cfg,
|
|
1537
|
-
targetTokens: aggressiveTarget
|
|
1538
|
-
});
|
|
1539
|
-
}
|
|
1540
1859
|
return {
|
|
1541
1860
|
payload: newPayload,
|
|
1542
1861
|
wasCompacted: true,
|
|
@@ -1563,11 +1882,12 @@ var RequestQueue = class {
|
|
|
1563
1882
|
lastRequestTime = 0;
|
|
1564
1883
|
async enqueue(execute, rateLimitSeconds) {
|
|
1565
1884
|
return new Promise((resolve, reject) => {
|
|
1566
|
-
|
|
1885
|
+
const request = {
|
|
1567
1886
|
execute,
|
|
1568
1887
|
resolve,
|
|
1569
1888
|
reject
|
|
1570
|
-
}
|
|
1889
|
+
};
|
|
1890
|
+
this.queue.push(request);
|
|
1571
1891
|
if (this.queue.length > 1) {
|
|
1572
1892
|
const position = this.queue.length;
|
|
1573
1893
|
const waitTime = Math.ceil((position - 1) * rateLimitSeconds);
|
|
@@ -1638,16 +1958,149 @@ const createChatCompletions = async (payload) => {
|
|
|
1638
1958
|
};
|
|
1639
1959
|
|
|
1640
1960
|
//#endregion
|
|
1641
|
-
//#region src/routes/
|
|
1642
|
-
|
|
1643
|
-
|
|
1961
|
+
//#region src/routes/shared.ts
|
|
1962
|
+
/** Helper to update tracker model */
|
|
1963
|
+
function updateTrackerModel(trackingId, model) {
|
|
1964
|
+
if (!trackingId) return;
|
|
1965
|
+
const request = requestTracker.getRequest(trackingId);
|
|
1966
|
+
if (request) request.model = model;
|
|
1967
|
+
}
|
|
1968
|
+
/** Helper to update tracker status */
|
|
1969
|
+
function updateTrackerStatus(trackingId, status) {
|
|
1970
|
+
if (!trackingId) return;
|
|
1971
|
+
requestTracker.updateRequest(trackingId, { status });
|
|
1644
1972
|
}
|
|
1973
|
+
/** Record error response to history */
|
|
1974
|
+
function recordErrorResponse(ctx, model, error) {
|
|
1975
|
+
recordResponse(ctx.historyId, {
|
|
1976
|
+
success: false,
|
|
1977
|
+
model,
|
|
1978
|
+
usage: {
|
|
1979
|
+
input_tokens: 0,
|
|
1980
|
+
output_tokens: 0
|
|
1981
|
+
},
|
|
1982
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1983
|
+
content: null
|
|
1984
|
+
}, Date.now() - ctx.startTime);
|
|
1985
|
+
}
|
|
1986
|
+
/** Complete TUI tracking */
|
|
1987
|
+
function completeTracking(trackingId, inputTokens, outputTokens) {
|
|
1988
|
+
if (!trackingId) return;
|
|
1989
|
+
requestTracker.updateRequest(trackingId, {
|
|
1990
|
+
inputTokens,
|
|
1991
|
+
outputTokens
|
|
1992
|
+
});
|
|
1993
|
+
requestTracker.completeRequest(trackingId, 200, {
|
|
1994
|
+
inputTokens,
|
|
1995
|
+
outputTokens
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
/** Fail TUI tracking */
|
|
1999
|
+
function failTracking(trackingId, error) {
|
|
2000
|
+
if (!trackingId) return;
|
|
2001
|
+
requestTracker.failRequest(trackingId, error instanceof Error ? error.message : "Stream error");
|
|
2002
|
+
}
|
|
2003
|
+
/** Record streaming error to history (works with any accumulator type) */
|
|
2004
|
+
function recordStreamError(opts) {
|
|
2005
|
+
const { acc, fallbackModel, ctx, error } = opts;
|
|
2006
|
+
recordResponse(ctx.historyId, {
|
|
2007
|
+
success: false,
|
|
2008
|
+
model: acc.model || fallbackModel,
|
|
2009
|
+
usage: {
|
|
2010
|
+
input_tokens: 0,
|
|
2011
|
+
output_tokens: 0
|
|
2012
|
+
},
|
|
2013
|
+
error: error instanceof Error ? error.message : "Stream error",
|
|
2014
|
+
content: null
|
|
2015
|
+
}, Date.now() - ctx.startTime);
|
|
2016
|
+
}
|
|
2017
|
+
/** Type guard for non-streaming responses */
|
|
2018
|
+
function isNonStreaming(response) {
|
|
2019
|
+
return Object.hasOwn(response, "choices");
|
|
2020
|
+
}
|
|
2021
|
+
/** Build final payload with auto-compact if needed */
|
|
2022
|
+
async function buildFinalPayload(payload, model) {
|
|
2023
|
+
if (!state.autoCompact || !model) {
|
|
2024
|
+
if (state.autoCompact && !model) consola.warn(`Auto-compact: Model '${payload.model}' not found in cached models, skipping`);
|
|
2025
|
+
return {
|
|
2026
|
+
finalPayload: payload,
|
|
2027
|
+
compactResult: null
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
try {
|
|
2031
|
+
const check = await checkNeedsCompaction(payload, model);
|
|
2032
|
+
consola.debug(`Auto-compact check: ${check.currentTokens} tokens, limit ${check.limit}, needed: ${check.needed}`);
|
|
2033
|
+
if (!check.needed) return {
|
|
2034
|
+
finalPayload: payload,
|
|
2035
|
+
compactResult: null
|
|
2036
|
+
};
|
|
2037
|
+
consola.info(`Auto-compact triggered: ${check.currentTokens} tokens > ${check.limit} limit`);
|
|
2038
|
+
const compactResult = await autoCompact(payload, model);
|
|
2039
|
+
return {
|
|
2040
|
+
finalPayload: compactResult.payload,
|
|
2041
|
+
compactResult
|
|
2042
|
+
};
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
consola.warn("Auto-compact failed, proceeding with original payload:", error instanceof Error ? error.message : error);
|
|
2045
|
+
return {
|
|
2046
|
+
finalPayload: payload,
|
|
2047
|
+
compactResult: null
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Log helpful debugging information when a 413 error occurs.
|
|
2053
|
+
*/
|
|
2054
|
+
async function logPayloadSizeInfo(payload, model) {
|
|
2055
|
+
const messageCount = payload.messages.length;
|
|
2056
|
+
const bodySize = JSON.stringify(payload).length;
|
|
2057
|
+
const bodySizeKB = Math.round(bodySize / 1024);
|
|
2058
|
+
let imageCount = 0;
|
|
2059
|
+
let largeMessages = 0;
|
|
2060
|
+
let totalImageSize = 0;
|
|
2061
|
+
for (const msg of payload.messages) {
|
|
2062
|
+
if (Array.isArray(msg.content)) {
|
|
2063
|
+
for (const part of msg.content) if (part.type === "image_url") {
|
|
2064
|
+
imageCount++;
|
|
2065
|
+
if (part.image_url.url.startsWith("data:")) totalImageSize += part.image_url.url.length;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
if ((typeof msg.content === "string" ? msg.content.length : JSON.stringify(msg.content).length) > 5e4) largeMessages++;
|
|
2069
|
+
}
|
|
2070
|
+
consola.info("");
|
|
2071
|
+
consola.info("╭─────────────────────────────────────────────────────────╮");
|
|
2072
|
+
consola.info("│ 413 Request Entity Too Large │");
|
|
2073
|
+
consola.info("╰─────────────────────────────────────────────────────────╯");
|
|
2074
|
+
consola.info("");
|
|
2075
|
+
consola.info(` Request body size: ${bodySizeKB} KB (${bodySize.toLocaleString()} bytes)`);
|
|
2076
|
+
consola.info(` Message count: ${messageCount}`);
|
|
2077
|
+
if (model) try {
|
|
2078
|
+
const tokenCount = await getTokenCount(payload, model);
|
|
2079
|
+
const limit = model.capabilities?.limits?.max_prompt_tokens ?? 128e3;
|
|
2080
|
+
consola.info(` Estimated tokens: ${tokenCount.input.toLocaleString()} / ${limit.toLocaleString()}`);
|
|
2081
|
+
} catch {}
|
|
2082
|
+
if (imageCount > 0) {
|
|
2083
|
+
const imageSizeKB = Math.round(totalImageSize / 1024);
|
|
2084
|
+
consola.info(` Images: ${imageCount} (${imageSizeKB} KB base64 data)`);
|
|
2085
|
+
}
|
|
2086
|
+
if (largeMessages > 0) consola.info(` Large messages (>50KB): ${largeMessages}`);
|
|
2087
|
+
consola.info("");
|
|
2088
|
+
consola.info(" Suggestions:");
|
|
2089
|
+
if (!state.autoCompact) consola.info(" • Enable --auto-compact to automatically truncate history");
|
|
2090
|
+
if (imageCount > 0) consola.info(" • Remove or resize large images in the conversation");
|
|
2091
|
+
consola.info(" • Start a new conversation with /clear or /reset");
|
|
2092
|
+
consola.info(" • Reduce conversation history by deleting old messages");
|
|
2093
|
+
consola.info("");
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
//#endregion
|
|
2097
|
+
//#region src/routes/chat-completions/handler.ts
|
|
1645
2098
|
async function handleCompletion$1(c) {
|
|
1646
2099
|
const originalPayload = await c.req.json();
|
|
1647
2100
|
consola.debug("Request payload:", JSON.stringify(originalPayload).slice(-400));
|
|
1648
2101
|
const trackingId = c.get("trackingId");
|
|
1649
2102
|
const startTime = (trackingId ? requestTracker.getRequest(trackingId) : void 0)?.startTime ?? Date.now();
|
|
1650
|
-
updateTrackerModel
|
|
2103
|
+
updateTrackerModel(trackingId, originalPayload.model);
|
|
1651
2104
|
const ctx = {
|
|
1652
2105
|
historyId: recordRequest("openai", {
|
|
1653
2106
|
model: originalPayload.model,
|
|
@@ -1665,19 +2118,32 @@ async function handleCompletion$1(c) {
|
|
|
1665
2118
|
};
|
|
1666
2119
|
const selectedModel = state.models?.data.find((model) => model.id === originalPayload.model);
|
|
1667
2120
|
await logTokenCount(originalPayload, selectedModel);
|
|
1668
|
-
const { finalPayload, compactResult } = await buildFinalPayload
|
|
2121
|
+
const { finalPayload, compactResult } = await buildFinalPayload(originalPayload, selectedModel);
|
|
1669
2122
|
if (compactResult) ctx.compactResult = compactResult;
|
|
1670
2123
|
const payload = isNullish(finalPayload.max_tokens) ? {
|
|
1671
2124
|
...finalPayload,
|
|
1672
|
-
max_tokens:
|
|
2125
|
+
max_tokens: selectedModel?.capabilities?.limits?.max_output_tokens
|
|
1673
2126
|
} : finalPayload;
|
|
1674
2127
|
if (isNullish(originalPayload.max_tokens)) consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
|
|
1675
2128
|
if (state.manualApprove) await awaitApproval();
|
|
2129
|
+
return executeRequest({
|
|
2130
|
+
c,
|
|
2131
|
+
payload,
|
|
2132
|
+
selectedModel,
|
|
2133
|
+
ctx,
|
|
2134
|
+
trackingId
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* Execute the API call with enhanced error handling for 413 errors.
|
|
2139
|
+
*/
|
|
2140
|
+
async function executeRequest(opts) {
|
|
2141
|
+
const { c, payload, selectedModel, ctx, trackingId } = opts;
|
|
1676
2142
|
try {
|
|
1677
2143
|
const response = await executeWithRateLimit(state, () => createChatCompletions(payload));
|
|
1678
|
-
if (isNonStreaming
|
|
2144
|
+
if (isNonStreaming(response)) return handleNonStreamingResponse$1(c, response, ctx);
|
|
1679
2145
|
consola.debug("Streaming response");
|
|
1680
|
-
updateTrackerStatus
|
|
2146
|
+
updateTrackerStatus(trackingId, "streaming");
|
|
1681
2147
|
return streamSSE(c, async (stream) => {
|
|
1682
2148
|
await handleStreamingResponse$1({
|
|
1683
2149
|
stream,
|
|
@@ -1687,39 +2153,11 @@ async function handleCompletion$1(c) {
|
|
|
1687
2153
|
});
|
|
1688
2154
|
});
|
|
1689
2155
|
} catch (error) {
|
|
1690
|
-
|
|
2156
|
+
if (error instanceof HTTPError && error.status === 413) await logPayloadSizeInfo(payload, selectedModel);
|
|
2157
|
+
recordErrorResponse(ctx, payload.model, error);
|
|
1691
2158
|
throw error;
|
|
1692
2159
|
}
|
|
1693
2160
|
}
|
|
1694
|
-
async function buildFinalPayload$1(payload, model) {
|
|
1695
|
-
if (!state.autoCompact || !model) {
|
|
1696
|
-
if (state.autoCompact && !model) consola.warn(`Auto-compact: Model '${payload.model}' not found in cached models, skipping`);
|
|
1697
|
-
return {
|
|
1698
|
-
finalPayload: payload,
|
|
1699
|
-
compactResult: null
|
|
1700
|
-
};
|
|
1701
|
-
}
|
|
1702
|
-
try {
|
|
1703
|
-
const check = await checkNeedsCompaction(payload, model);
|
|
1704
|
-
consola.debug(`Auto-compact check: ${check.currentTokens} tokens, limit ${check.limit}, needed: ${check.needed}`);
|
|
1705
|
-
if (!check.needed) return {
|
|
1706
|
-
finalPayload: payload,
|
|
1707
|
-
compactResult: null
|
|
1708
|
-
};
|
|
1709
|
-
consola.info(`Auto-compact triggered: ${check.currentTokens} tokens > ${check.limit} limit`);
|
|
1710
|
-
const compactResult = await autoCompact(payload, model);
|
|
1711
|
-
return {
|
|
1712
|
-
finalPayload: compactResult.payload,
|
|
1713
|
-
compactResult
|
|
1714
|
-
};
|
|
1715
|
-
} catch (error) {
|
|
1716
|
-
consola.warn("Auto-compact failed, proceeding with original payload:", error);
|
|
1717
|
-
return {
|
|
1718
|
-
finalPayload: payload,
|
|
1719
|
-
compactResult: null
|
|
1720
|
-
};
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
2161
|
async function logTokenCount(payload, selectedModel) {
|
|
1724
2162
|
try {
|
|
1725
2163
|
if (selectedModel) {
|
|
@@ -1730,27 +2168,6 @@ async function logTokenCount(payload, selectedModel) {
|
|
|
1730
2168
|
consola.debug("Failed to calculate token count:", error);
|
|
1731
2169
|
}
|
|
1732
2170
|
}
|
|
1733
|
-
function updateTrackerModel$1(trackingId, model) {
|
|
1734
|
-
if (!trackingId) return;
|
|
1735
|
-
const request = requestTracker.getRequest(trackingId);
|
|
1736
|
-
if (request) request.model = model;
|
|
1737
|
-
}
|
|
1738
|
-
function updateTrackerStatus$1(trackingId, status) {
|
|
1739
|
-
if (!trackingId) return;
|
|
1740
|
-
requestTracker.updateRequest(trackingId, { status });
|
|
1741
|
-
}
|
|
1742
|
-
function recordErrorResponse$1(ctx, model, error) {
|
|
1743
|
-
recordResponse(ctx.historyId, {
|
|
1744
|
-
success: false,
|
|
1745
|
-
model,
|
|
1746
|
-
usage: {
|
|
1747
|
-
input_tokens: 0,
|
|
1748
|
-
output_tokens: 0
|
|
1749
|
-
},
|
|
1750
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
1751
|
-
content: null
|
|
1752
|
-
}, Date.now() - ctx.startTime);
|
|
1753
|
-
}
|
|
1754
2171
|
function handleNonStreamingResponse$1(c, originalResponse, ctx) {
|
|
1755
2172
|
consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
|
|
1756
2173
|
let response = originalResponse;
|
|
@@ -1848,7 +2265,7 @@ async function handleStreamingResponse$1(opts) {
|
|
|
1848
2265
|
acc.content += marker;
|
|
1849
2266
|
}
|
|
1850
2267
|
recordStreamSuccess(acc, payload.model, ctx);
|
|
1851
|
-
completeTracking
|
|
2268
|
+
completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens);
|
|
1852
2269
|
} catch (error) {
|
|
1853
2270
|
recordStreamError({
|
|
1854
2271
|
acc,
|
|
@@ -1856,7 +2273,7 @@ async function handleStreamingResponse$1(opts) {
|
|
|
1856
2273
|
ctx,
|
|
1857
2274
|
error
|
|
1858
2275
|
});
|
|
1859
|
-
failTracking
|
|
2276
|
+
failTracking(ctx.trackingId, error);
|
|
1860
2277
|
throw error;
|
|
1861
2278
|
}
|
|
1862
2279
|
}
|
|
@@ -1864,42 +2281,31 @@ function parseStreamChunk(chunk, acc) {
|
|
|
1864
2281
|
if (!chunk.data || chunk.data === "[DONE]") return;
|
|
1865
2282
|
try {
|
|
1866
2283
|
const parsed = JSON.parse(chunk.data);
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
}
|
|
1872
|
-
function accumulateModel(parsed, acc) {
|
|
1873
|
-
if (parsed.model && !acc.model) acc.model = parsed.model;
|
|
1874
|
-
}
|
|
1875
|
-
function accumulateUsage(parsed, acc) {
|
|
1876
|
-
if (parsed.usage) {
|
|
1877
|
-
acc.inputTokens = parsed.usage.prompt_tokens;
|
|
1878
|
-
acc.outputTokens = parsed.usage.completion_tokens;
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
function accumulateChoice(choice, acc) {
|
|
1882
|
-
if (!choice) return;
|
|
1883
|
-
if (choice.delta.content) acc.content += choice.delta.content;
|
|
1884
|
-
if (choice.delta.tool_calls) accumulateToolCalls(choice.delta.tool_calls, acc);
|
|
1885
|
-
if (choice.finish_reason) acc.finishReason = choice.finish_reason;
|
|
1886
|
-
}
|
|
1887
|
-
function accumulateToolCalls(toolCalls, acc) {
|
|
1888
|
-
if (!toolCalls) return;
|
|
1889
|
-
for (const tc of toolCalls) {
|
|
1890
|
-
const idx = tc.index;
|
|
1891
|
-
if (!acc.toolCallMap.has(idx)) acc.toolCallMap.set(idx, {
|
|
1892
|
-
id: tc.id ?? "",
|
|
1893
|
-
name: tc.function?.name ?? "",
|
|
1894
|
-
arguments: ""
|
|
1895
|
-
});
|
|
1896
|
-
const item = acc.toolCallMap.get(idx);
|
|
1897
|
-
if (item) {
|
|
1898
|
-
if (tc.id) item.id = tc.id;
|
|
1899
|
-
if (tc.function?.name) item.name = tc.function.name;
|
|
1900
|
-
if (tc.function?.arguments) item.arguments += tc.function.arguments;
|
|
2284
|
+
if (parsed.model && !acc.model) acc.model = parsed.model;
|
|
2285
|
+
if (parsed.usage) {
|
|
2286
|
+
acc.inputTokens = parsed.usage.prompt_tokens;
|
|
2287
|
+
acc.outputTokens = parsed.usage.completion_tokens;
|
|
1901
2288
|
}
|
|
1902
|
-
|
|
2289
|
+
const choice = parsed.choices[0];
|
|
2290
|
+
if (choice) {
|
|
2291
|
+
if (choice.delta.content) acc.content += choice.delta.content;
|
|
2292
|
+
if (choice.delta.tool_calls) for (const tc of choice.delta.tool_calls) {
|
|
2293
|
+
const idx = tc.index;
|
|
2294
|
+
if (!acc.toolCallMap.has(idx)) acc.toolCallMap.set(idx, {
|
|
2295
|
+
id: tc.id ?? "",
|
|
2296
|
+
name: tc.function?.name ?? "",
|
|
2297
|
+
arguments: ""
|
|
2298
|
+
});
|
|
2299
|
+
const item = acc.toolCallMap.get(idx);
|
|
2300
|
+
if (item) {
|
|
2301
|
+
if (tc.id) item.id = tc.id;
|
|
2302
|
+
if (tc.function?.name) item.name = tc.function.name;
|
|
2303
|
+
if (tc.function?.arguments) item.arguments += tc.function.arguments;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
if (choice.finish_reason) acc.finishReason = choice.finish_reason;
|
|
2307
|
+
}
|
|
2308
|
+
} catch {}
|
|
1903
2309
|
}
|
|
1904
2310
|
function recordStreamSuccess(acc, fallbackModel, ctx) {
|
|
1905
2311
|
for (const tc of acc.toolCallMap.values()) if (tc.id && tc.name) acc.toolCalls.push(tc);
|
|
@@ -1931,35 +2337,6 @@ function recordStreamSuccess(acc, fallbackModel, ctx) {
|
|
|
1931
2337
|
})) : void 0
|
|
1932
2338
|
}, Date.now() - ctx.startTime);
|
|
1933
2339
|
}
|
|
1934
|
-
function recordStreamError(opts) {
|
|
1935
|
-
const { acc, fallbackModel, ctx, error } = opts;
|
|
1936
|
-
recordResponse(ctx.historyId, {
|
|
1937
|
-
success: false,
|
|
1938
|
-
model: acc.model || fallbackModel,
|
|
1939
|
-
usage: {
|
|
1940
|
-
input_tokens: 0,
|
|
1941
|
-
output_tokens: 0
|
|
1942
|
-
},
|
|
1943
|
-
error: error instanceof Error ? error.message : "Stream error",
|
|
1944
|
-
content: null
|
|
1945
|
-
}, Date.now() - ctx.startTime);
|
|
1946
|
-
}
|
|
1947
|
-
function completeTracking$1(trackingId, inputTokens, outputTokens) {
|
|
1948
|
-
if (!trackingId) return;
|
|
1949
|
-
requestTracker.updateRequest(trackingId, {
|
|
1950
|
-
inputTokens,
|
|
1951
|
-
outputTokens
|
|
1952
|
-
});
|
|
1953
|
-
requestTracker.completeRequest(trackingId, 200, {
|
|
1954
|
-
inputTokens,
|
|
1955
|
-
outputTokens
|
|
1956
|
-
});
|
|
1957
|
-
}
|
|
1958
|
-
function failTracking$1(trackingId, error) {
|
|
1959
|
-
if (!trackingId) return;
|
|
1960
|
-
requestTracker.failRequest(trackingId, error instanceof Error ? error.message : "Stream error");
|
|
1961
|
-
}
|
|
1962
|
-
const isNonStreaming$1 = (response) => Object.hasOwn(response, "choices");
|
|
1963
2340
|
function convertOpenAIMessages(messages) {
|
|
1964
2341
|
return messages.map((msg) => {
|
|
1965
2342
|
const result = {
|
|
@@ -3160,6 +3537,15 @@ function mapOpenAIStopReasonToAnthropic(finishReason) {
|
|
|
3160
3537
|
//#endregion
|
|
3161
3538
|
//#region src/routes/messages/non-stream-translation.ts
|
|
3162
3539
|
const OPENAI_TOOL_NAME_LIMIT = 64;
|
|
3540
|
+
/**
|
|
3541
|
+
* Ensure all tool_use blocks have corresponding tool_result responses.
|
|
3542
|
+
* This handles edge cases where conversation history may be incomplete:
|
|
3543
|
+
* - Session interruptions where tool execution was cut off
|
|
3544
|
+
* - Previous request failures
|
|
3545
|
+
* - Client sending truncated history
|
|
3546
|
+
*
|
|
3547
|
+
* Adding placeholder responses prevents API errors and maintains protocol compliance.
|
|
3548
|
+
*/
|
|
3163
3549
|
function fixMessageSequence(messages) {
|
|
3164
3550
|
const fixedMessages = [];
|
|
3165
3551
|
for (let i = 0; i < messages.length; i++) {
|
|
@@ -3318,7 +3704,7 @@ function getTruncatedToolName(originalName, toolNameMapping) {
|
|
|
3318
3704
|
for (let i = 0; i < originalName.length; i++) {
|
|
3319
3705
|
const char = originalName.codePointAt(i) ?? 0;
|
|
3320
3706
|
hash = (hash << 5) - hash + char;
|
|
3321
|
-
hash = hash
|
|
3707
|
+
hash = Math.trunc(hash);
|
|
3322
3708
|
}
|
|
3323
3709
|
const hashSuffix = Math.abs(hash).toString(36).slice(0, 8);
|
|
3324
3710
|
const truncatedName = originalName.slice(0, OPENAI_TOOL_NAME_LIMIT - 9) + "_" + hashSuffix;
|
|
@@ -3655,60 +4041,11 @@ async function handleCompletion(c) {
|
|
|
3655
4041
|
});
|
|
3656
4042
|
});
|
|
3657
4043
|
} catch (error) {
|
|
4044
|
+
if (error instanceof HTTPError && error.status === 413) await logPayloadSizeInfo(openAIPayload, selectedModel);
|
|
3658
4045
|
recordErrorResponse(ctx, anthropicPayload.model, error);
|
|
3659
4046
|
throw error;
|
|
3660
4047
|
}
|
|
3661
4048
|
}
|
|
3662
|
-
function updateTrackerModel(trackingId, model) {
|
|
3663
|
-
if (!trackingId) return;
|
|
3664
|
-
const request = requestTracker.getRequest(trackingId);
|
|
3665
|
-
if (request) request.model = model;
|
|
3666
|
-
}
|
|
3667
|
-
async function buildFinalPayload(payload, model) {
|
|
3668
|
-
if (!state.autoCompact || !model) {
|
|
3669
|
-
if (state.autoCompact && !model) consola.warn(`Auto-compact: Model '${payload.model}' not found in cached models, skipping`);
|
|
3670
|
-
return {
|
|
3671
|
-
finalPayload: payload,
|
|
3672
|
-
compactResult: null
|
|
3673
|
-
};
|
|
3674
|
-
}
|
|
3675
|
-
try {
|
|
3676
|
-
const check = await checkNeedsCompaction(payload, model);
|
|
3677
|
-
consola.debug(`Auto-compact check: ${check.currentTokens} tokens, limit ${check.limit}, needed: ${check.needed}`);
|
|
3678
|
-
if (!check.needed) return {
|
|
3679
|
-
finalPayload: payload,
|
|
3680
|
-
compactResult: null
|
|
3681
|
-
};
|
|
3682
|
-
consola.info(`Auto-compact triggered: ${check.currentTokens} tokens > ${check.limit} limit`);
|
|
3683
|
-
const compactResult = await autoCompact(payload, model);
|
|
3684
|
-
return {
|
|
3685
|
-
finalPayload: compactResult.payload,
|
|
3686
|
-
compactResult
|
|
3687
|
-
};
|
|
3688
|
-
} catch (error) {
|
|
3689
|
-
consola.warn("Auto-compact failed, proceeding with original payload:", error);
|
|
3690
|
-
return {
|
|
3691
|
-
finalPayload: payload,
|
|
3692
|
-
compactResult: null
|
|
3693
|
-
};
|
|
3694
|
-
}
|
|
3695
|
-
}
|
|
3696
|
-
function updateTrackerStatus(trackingId, status) {
|
|
3697
|
-
if (!trackingId) return;
|
|
3698
|
-
requestTracker.updateRequest(trackingId, { status });
|
|
3699
|
-
}
|
|
3700
|
-
function recordErrorResponse(ctx, model, error) {
|
|
3701
|
-
recordResponse(ctx.historyId, {
|
|
3702
|
-
success: false,
|
|
3703
|
-
model,
|
|
3704
|
-
usage: {
|
|
3705
|
-
input_tokens: 0,
|
|
3706
|
-
output_tokens: 0
|
|
3707
|
-
},
|
|
3708
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
3709
|
-
content: null
|
|
3710
|
-
}, Date.now() - ctx.startTime);
|
|
3711
|
-
}
|
|
3712
4049
|
function handleNonStreamingResponse(opts) {
|
|
3713
4050
|
const { c, response, toolNameMapping, ctx } = opts;
|
|
3714
4051
|
consola.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
|
|
@@ -3802,7 +4139,7 @@ async function handleStreamingResponse(opts) {
|
|
|
3802
4139
|
completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens);
|
|
3803
4140
|
} catch (error) {
|
|
3804
4141
|
consola.error("Stream error:", error);
|
|
3805
|
-
|
|
4142
|
+
recordStreamError({
|
|
3806
4143
|
acc,
|
|
3807
4144
|
fallbackModel: anthropicPayload.model,
|
|
3808
4145
|
ctx,
|
|
@@ -3942,34 +4279,6 @@ function recordStreamingResponse(acc, fallbackModel, ctx) {
|
|
|
3942
4279
|
toolCalls: acc.toolCalls.length > 0 ? acc.toolCalls : void 0
|
|
3943
4280
|
}, Date.now() - ctx.startTime);
|
|
3944
4281
|
}
|
|
3945
|
-
function recordStreamingError(opts) {
|
|
3946
|
-
const { acc, fallbackModel, ctx, error } = opts;
|
|
3947
|
-
recordResponse(ctx.historyId, {
|
|
3948
|
-
success: false,
|
|
3949
|
-
model: acc.model || fallbackModel,
|
|
3950
|
-
usage: {
|
|
3951
|
-
input_tokens: 0,
|
|
3952
|
-
output_tokens: 0
|
|
3953
|
-
},
|
|
3954
|
-
error: error instanceof Error ? error.message : "Stream error",
|
|
3955
|
-
content: null
|
|
3956
|
-
}, Date.now() - ctx.startTime);
|
|
3957
|
-
}
|
|
3958
|
-
function completeTracking(trackingId, inputTokens, outputTokens) {
|
|
3959
|
-
if (!trackingId) return;
|
|
3960
|
-
requestTracker.updateRequest(trackingId, {
|
|
3961
|
-
inputTokens,
|
|
3962
|
-
outputTokens
|
|
3963
|
-
});
|
|
3964
|
-
requestTracker.completeRequest(trackingId, 200, {
|
|
3965
|
-
inputTokens,
|
|
3966
|
-
outputTokens
|
|
3967
|
-
});
|
|
3968
|
-
}
|
|
3969
|
-
function failTracking(trackingId, error) {
|
|
3970
|
-
if (!trackingId) return;
|
|
3971
|
-
requestTracker.failRequest(trackingId, error instanceof Error ? error.message : "Stream error");
|
|
3972
|
-
}
|
|
3973
4282
|
function convertAnthropicMessages(messages) {
|
|
3974
4283
|
return messages.map((msg) => {
|
|
3975
4284
|
if (typeof msg.content === "string") return {
|
|
@@ -4017,7 +4326,6 @@ function extractToolCallsFromContent(content) {
|
|
|
4017
4326
|
});
|
|
4018
4327
|
return tools.length > 0 ? tools : void 0;
|
|
4019
4328
|
}
|
|
4020
|
-
const isNonStreaming = (response) => Object.hasOwn(response, "choices");
|
|
4021
4329
|
|
|
4022
4330
|
//#endregion
|
|
4023
4331
|
//#region src/routes/messages/route.ts
|
|
@@ -4320,7 +4628,8 @@ const main = defineCommand({
|
|
|
4320
4628
|
logout,
|
|
4321
4629
|
start,
|
|
4322
4630
|
"check-usage": checkUsage,
|
|
4323
|
-
debug
|
|
4631
|
+
debug,
|
|
4632
|
+
"patch-claude": patchClaude
|
|
4324
4633
|
}
|
|
4325
4634
|
});
|
|
4326
4635
|
await runMain(main);
|