@dianshuv/copilot-api 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +9 -49
  2. package/dist/main.mjs +235 -696
  3. package/package.json +1 -2
package/README.md CHANGED
@@ -46,11 +46,9 @@ make down
46
46
  | Command | Description |
47
47
  |---------|-------------|
48
48
  | `start` | Start the API server (handles auth if needed) |
49
- | `auth` | Run GitHub authentication flow only |
49
+ | `login` | Run GitHub authentication flow only |
50
50
  | `logout` | Remove stored GitHub token |
51
- | `check-usage` | Show Copilot usage and quota |
52
51
  | `debug` | Display diagnostic information |
53
- | `patch-claude` | Patch Claude Code's context window limit |
54
52
 
55
53
  ### Start Command Options
56
54
 
@@ -60,45 +58,25 @@ make down
60
58
  | `--host`, `-H` | Host/interface to bind to (`0.0.0.0` = all interfaces) | `127.0.0.1` |
61
59
  | `--verbose`, `-v` | Enable verbose logging | false |
62
60
  | `--account-type`, `-a` | Account type (individual, business, enterprise) | individual |
63
- | `--manual` | Manual request approval mode | false |
64
61
  | `--no-rate-limit` | Disable adaptive rate limiting | false |
65
- | `--retry-interval` | Seconds to wait before retrying after rate limit | 10 |
66
- | `--request-interval` | Seconds between requests in rate-limited mode | 10 |
67
- | `--recovery-timeout` | Minutes before attempting recovery | 10 |
68
- | `--consecutive-successes` | Successes needed to exit rate-limited mode | 5 |
69
- | `--github-token`, `-g` | Provide GitHub token directly | none |
70
- | `--claude-code`, `-c` | Generate Claude Code launch command | false |
71
- | `--show-token` | Show tokens on fetch/refresh | false |
72
- | `--show-all-models` | Show all upstream models including the hardcoded blacklist (see "Hidden Models" below) | false |
73
- | `--proxy-env` | Use proxy from environment | false |
74
- | `--no-history` | Disable request history UI at `/history` | false |
75
- | `--history-limit` | Max history entries in memory | 1000 |
76
- | `--no-auto-truncate` | Disable auto-truncate when exceeding token limits | false |
77
- | `--compress-tool-results` | Compress old tool results before truncating | false |
78
- | `--redirect-anthropic` | Force Anthropic through OpenAI translation | false |
79
- | `--strip-server-tools` | Strip server-side tools from Anthropic requests | false |
80
- | `--context-editing` | Context editing mode: off, clear-thinking, clear-tooluse, clear-both | off |
81
- | `--timezone-offset` | Timezone offset in hours from UTC for log timestamps (e.g., +8, -5, 0) | +8 |
62
+ | `--github-token`, `-g` | Provide GitHub token directly (or `GH_TOKEN` env) | none |
82
63
  | `--posthog-key` | PostHog API key for token usage analytics (opt-in) | none |
83
64
  | `--api-key` | Proxy API key for inbound authentication (see [Authentication](#authentication)). Empty = disabled | none |
84
65
 
85
66
  ### Hidden Models
86
67
 
87
68
  By default the proxy hides 30 stale / duplicate / unused upstream model ids from
88
- its **listing surfaces** (`/v1/models`, the startup banner, and the
89
- `--claude-code` interactive prompts). Pass `--show-all-models` to see the full
90
- upstream catalogue.
69
+ its **listing surfaces** (`/v1/models` and the startup banner).
91
70
 
92
71
  **Important**: this is a *display* filter only. The remaining POST endpoints
93
72
  (`/v1/chat/completions`, `/v1/messages`, `/v1/responses`) do **not** reject
94
73
  hidden ids — an explicit `POST` with a hidden id is still forwarded to
95
- upstream verbatim, even without `--show-all-models`. (The embedding and
96
- Gemini ids in the list have no callable endpoint in this proxy at all;
97
- they're hidden purely to denoise the `/v1/models` listing — the upstream
98
- `/models` catalogue still returns them.)
74
+ upstream verbatim. (The embedding and Gemini ids in the list have no callable
75
+ endpoint in this proxy at all; they're hidden purely to denoise the
76
+ `/v1/models` listing — the upstream `/models` catalogue still returns them.)
99
77
 
100
- To change the blacklist, edit `src/lib/hidden-models.ts` and re-publish (this
101
- fork uses `/publish`).
78
+ To see the full catalogue or change the blacklist, edit
79
+ `src/lib/hidden-models.ts` and re-publish (this fork uses `/publish`).
102
80
 
103
81
  Currently hidden (grouped):
104
82
 
@@ -111,15 +89,6 @@ Currently hidden (grouped):
111
89
  - **Older / variant Claude** — `claude-opus-4.5`, `claude-opus-4.6`, `claude-opus-4.7-high`, `claude-opus-4.7-xhigh`, `claude-sonnet-4.5`
112
90
  - **Special-purpose** — `mai-code-1-flash-internal`, `trajectory-compaction`
113
91
 
114
- ### Patch-Claude Command Options
115
-
116
- | Option | Description | Default |
117
- |--------|-------------|---------|
118
- | `--limit`, `-l` | Context window limit in tokens | 128000 |
119
- | `--restore`, `-r` | Restore original 200k limit | false |
120
- | `--path`, `-p` | Path to Claude Code cli.js | auto-detect |
121
- | `--status`, `-s` | Show current patch status | false |
122
-
123
92
  ## API Endpoints
124
93
 
125
94
  ### OpenAI Compatible
@@ -155,7 +124,6 @@ When enabled (default), auto-truncate automatically compacts conversation histor
155
124
 
156
125
  - **Token-based truncation**: Uses the model's `max_context_window_tokens` from the Copilot API to determine when truncation is needed. A 2% safety margin is applied.
157
126
  - **No preset byte limit**: There is no hardcoded request body size limit. If the Copilot API returns a 413 (Request Entity Too Large), the proxy dynamically learns the byte limit and applies it to subsequent requests.
158
- - **Smart compression**: With `--compress-tool-results`, old tool results are compressed before removing messages, preserving more conversation context.
159
127
  - **Orphan filtering**: After truncation, orphaned tool results (without matching tool calls) are automatically removed.
160
128
 
161
129
  ## Authentication
@@ -244,9 +212,7 @@ Per client:
244
212
  - **Claude Code** (`.claude/settings.json`, see [below](#using-with-claude-code)):
245
213
  change `ANTHROPIC_AUTH_TOKEN` from `"dummy"` to your real key. Claude Code
246
214
  talks to the proxy's `/v1/messages` (Anthropic) surface; the auth gate accepts
247
- the token from either header. `--claude-code` keeps emitting the `"dummy"`
248
- placeholder on purpose (so the secret never lands in your clipboard / shell
249
- history) and prints a reminder to replace it when auth is on.
215
+ the token from either header.
250
216
  - **Raw OpenAI SDK** → `Authorization: Bearer <key>`; **raw Anthropic SDK** →
251
217
  `x-api-key: <key>`.
252
218
 
@@ -308,12 +274,6 @@ Create `.claude/settings.json` in your project:
308
274
  }
309
275
  ```
310
276
 
311
- Or use the interactive setup:
312
-
313
- ```sh
314
- bun run start --claude-code
315
- ```
316
-
317
277
  ## Upstream Project
318
278
 
319
279
  For the original project documentation, features, and updates, see: [ericc-ch/copilot-api](https://github.com/ericc-ch/copilot-api)
package/dist/main.mjs CHANGED
@@ -3,16 +3,12 @@ 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, { dirname, join } from "node:path";
6
+ import path from "node:path";
7
7
  import { getProxyForUrl } from "proxy-from-env";
8
8
  import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
9
9
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
10
- import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
11
- import clipboard from "clipboardy";
12
10
  import { serve } from "srvx";
13
11
  import { PostHog } from "posthog-node";
14
- import { execSync } from "node:child_process";
15
- import process$1 from "node:process";
16
12
  import pc from "picocolors";
17
13
  import { Hono } from "hono";
18
14
  import { cors } from "hono/cors";
@@ -782,45 +778,6 @@ async function logUser() {
782
778
  consola.info(`Logged in as ${user.login}`);
783
779
  }
784
780
 
785
- //#endregion
786
- //#region src/auth.ts
787
- async function runAuth(options) {
788
- if (options.verbose) {
789
- consola.level = 5;
790
- consola.info("Verbose logging enabled");
791
- }
792
- state.showToken = options.showToken;
793
- initProxyFromEnv();
794
- await ensurePaths();
795
- await setupGitHubToken({ force: true });
796
- consola.success("GitHub token written to", PATHS.GITHUB_TOKEN_PATH);
797
- }
798
- const auth = defineCommand({
799
- meta: {
800
- name: "auth",
801
- description: "Run GitHub auth flow without running the server"
802
- },
803
- args: {
804
- verbose: {
805
- alias: "v",
806
- type: "boolean",
807
- default: false,
808
- description: "Enable verbose logging"
809
- },
810
- "show-token": {
811
- type: "boolean",
812
- default: false,
813
- description: "Show GitHub token on auth"
814
- }
815
- },
816
- run({ args }) {
817
- return runAuth({
818
- verbose: args.verbose,
819
- showToken: args["show-token"]
820
- });
821
- }
822
- });
823
-
824
781
  //#endregion
825
782
  //#region src/services/github/get-copilot-usage.ts
826
783
  const getCopilotUsage = async () => {
@@ -829,43 +786,6 @@ const getCopilotUsage = async () => {
829
786
  return await response.json();
830
787
  };
831
788
 
832
- //#endregion
833
- //#region src/check-usage.ts
834
- const checkUsage = defineCommand({
835
- meta: {
836
- name: "check-usage",
837
- description: "Show current GitHub Copilot usage/quota information"
838
- },
839
- async run() {
840
- initProxyFromEnv();
841
- await ensurePaths();
842
- await setupGitHubToken();
843
- try {
844
- const usage = await getCopilotUsage();
845
- const premium = usage.quota_snapshots.premium_interactions;
846
- const premiumTotal = premium.entitlement;
847
- const premiumUsed = premiumTotal - premium.remaining;
848
- const premiumPercentUsed = premiumTotal > 0 ? premiumUsed / premiumTotal * 100 : 0;
849
- const premiumPercentRemaining = premium.percent_remaining;
850
- function summarizeQuota(name, snap) {
851
- if (!snap) return `${name}: N/A`;
852
- const total = snap.entitlement;
853
- const used = total - snap.remaining;
854
- const percentUsed = total > 0 ? used / total * 100 : 0;
855
- const percentRemaining = snap.percent_remaining;
856
- return `${name}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
857
- }
858
- const premiumLine = `Premium: ${premiumUsed}/${premiumTotal} used (${premiumPercentUsed.toFixed(1)}% used, ${premiumPercentRemaining.toFixed(1)}% remaining)`;
859
- const chatLine = summarizeQuota("Chat", usage.quota_snapshots.chat);
860
- const completionsLine = summarizeQuota("Completions", usage.quota_snapshots.completions);
861
- consola.box(`Copilot Usage (plan: ${usage.copilot_plan})\nQuota resets: ${usage.quota_reset_date}\n\nQuotas:\n ${premiumLine}\n ${chatLine}\n ${completionsLine}`);
862
- } catch (err) {
863
- consola.error("Failed to fetch Copilot usage:", err);
864
- process.exit(1);
865
- }
866
- }
867
- });
868
-
869
789
  //#endregion
870
790
  //#region src/debug.ts
871
791
  async function getPackageVersion() {
@@ -1008,6 +928,45 @@ const debug = defineCommand({
1008
928
  }
1009
929
  });
1010
930
 
931
+ //#endregion
932
+ //#region src/login.ts
933
+ async function runLogin(options) {
934
+ if (options.verbose) {
935
+ consola.level = 5;
936
+ consola.info("Verbose logging enabled");
937
+ }
938
+ state.showToken = options.showToken;
939
+ initProxyFromEnv();
940
+ await ensurePaths();
941
+ await setupGitHubToken({ force: true });
942
+ consola.success("GitHub token written to", PATHS.GITHUB_TOKEN_PATH);
943
+ }
944
+ const login = defineCommand({
945
+ meta: {
946
+ name: "login",
947
+ description: "Run GitHub auth flow without running the server"
948
+ },
949
+ args: {
950
+ verbose: {
951
+ alias: "v",
952
+ type: "boolean",
953
+ default: false,
954
+ description: "Enable verbose logging"
955
+ },
956
+ "show-token": {
957
+ type: "boolean",
958
+ default: false,
959
+ description: "Show GitHub token on auth"
960
+ }
961
+ },
962
+ run({ args }) {
963
+ return runLogin({
964
+ verbose: args.verbose,
965
+ showToken: args["show-token"]
966
+ });
967
+ }
968
+ });
969
+
1011
970
  //#endregion
1012
971
  //#region src/logout.ts
1013
972
  async function runLogout() {
@@ -1032,323 +991,9 @@ const logout = defineCommand({
1032
991
  }
1033
992
  });
1034
993
 
1035
- //#endregion
1036
- //#region src/patch-claude-code.ts
1037
- const SUPPORTED_VERSIONS = {
1038
- v2a: {
1039
- min: "2.0.0",
1040
- max: "2.1.10"
1041
- },
1042
- v2b: { min: "2.1.11" }
1043
- };
1044
- const PATTERNS = {
1045
- funcOriginal: /function HR\(A\)\{if\(A\.includes\("\[1m\]"\)\)return 1e6;return 200000\}/,
1046
- funcPatched: /function HR\(A\)\{if\(A\.includes\("\[1m\]"\)\)return 1e6;return \d+\}/,
1047
- variable: /var ([A-Za-z_$]\w*)=(\d+)(?=,\w+=20000,)/
1048
- };
1049
- /**
1050
- * Parse semver version string to comparable parts
1051
- */
1052
- function parseVersion(version) {
1053
- return version.split(".").map((n) => Number.parseInt(n, 10) || 0);
1054
- }
1055
- /**
1056
- * Compare two semver versions
1057
- * Returns: -1 if a < b, 0 if a == b, 1 if a > b
1058
- */
1059
- function compareVersions(a, b) {
1060
- const partsA = parseVersion(a);
1061
- const partsB = parseVersion(b);
1062
- const len = Math.max(partsA.length, partsB.length);
1063
- for (let i = 0; i < len; i++) {
1064
- const numA = partsA[i] || 0;
1065
- const numB = partsB[i] || 0;
1066
- if (numA < numB) return -1;
1067
- if (numA > numB) return 1;
1068
- }
1069
- return 0;
1070
- }
1071
- function getPatternTypeForVersion(version) {
1072
- if (compareVersions(version, SUPPORTED_VERSIONS.v2a.min) >= 0 && compareVersions(version, SUPPORTED_VERSIONS.v2a.max) <= 0) return "func";
1073
- if (compareVersions(version, SUPPORTED_VERSIONS.v2b.min) >= 0) return "variable";
1074
- return null;
1075
- }
1076
- /**
1077
- * Get supported version range string for error messages
1078
- */
1079
- function getSupportedRangeString() {
1080
- return `${SUPPORTED_VERSIONS.v2a.min}-${SUPPORTED_VERSIONS.v2a.max}, ${SUPPORTED_VERSIONS.v2b.min}+`;
1081
- }
1082
- /**
1083
- * Get Claude Code version from package.json
1084
- */
1085
- function getClaudeCodeVersion(cliPath) {
1086
- try {
1087
- const packageJsonPath = join(dirname(cliPath), "package.json");
1088
- if (!existsSync(packageJsonPath)) return null;
1089
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1090
- if (typeof packageJson === "object" && packageJson !== null && "version" in packageJson && typeof packageJson.version === "string") return packageJson.version;
1091
- return null;
1092
- } catch {
1093
- return null;
1094
- }
1095
- }
1096
- /**
1097
- * Search volta tools directory for Claude Code
1098
- */
1099
- function findInVoltaTools(voltaHome) {
1100
- const paths = [];
1101
- const packagesPath = join(voltaHome, "tools", "image", "packages", "@anthropic-ai", "claude-code", "lib", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
1102
- if (existsSync(packagesPath)) paths.push(packagesPath);
1103
- const toolsDir = join(voltaHome, "tools", "image", "node");
1104
- if (existsSync(toolsDir)) try {
1105
- for (const version of readdirSync(toolsDir)) {
1106
- const claudePath = join(toolsDir, version, "lib", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
1107
- if (existsSync(claudePath)) paths.push(claudePath);
1108
- }
1109
- } catch {}
1110
- return paths;
1111
- }
1112
- /**
1113
- * Find all Claude Code CLI paths by checking common locations
1114
- */
1115
- function findAllClaudeCodePaths() {
1116
- const possiblePaths = [];
1117
- const home = process.env.HOME || "";
1118
- const voltaHome = process.env.VOLTA_HOME || join(home, ".volta");
1119
- if (existsSync(voltaHome)) possiblePaths.push(...findInVoltaTools(voltaHome));
1120
- const npmPrefix = process.env.npm_config_prefix;
1121
- if (npmPrefix) possiblePaths.push(join(npmPrefix, "lib", "node_modules", "@anthropic-ai", "claude-code", "cli.js"));
1122
- const globalPaths = [
1123
- join(home, ".npm-global", "lib", "node_modules"),
1124
- "/usr/local/lib/node_modules",
1125
- "/usr/lib/node_modules"
1126
- ];
1127
- for (const base of globalPaths) possiblePaths.push(join(base, "@anthropic-ai", "claude-code", "cli.js"));
1128
- const bunGlobal = join(home, ".bun", "install", "global");
1129
- if (existsSync(bunGlobal)) possiblePaths.push(join(bunGlobal, "node_modules", "@anthropic-ai", "claude-code", "cli.js"));
1130
- return [...new Set(possiblePaths.filter((p) => existsSync(p)))];
1131
- }
1132
- /**
1133
- * Get installation info for a CLI path
1134
- */
1135
- function getInstallationInfo(cliPath) {
1136
- return {
1137
- path: cliPath,
1138
- version: getClaudeCodeVersion(cliPath),
1139
- limit: getCurrentLimit(readFileSync(cliPath, "utf8"))
1140
- };
1141
- }
1142
- function getCurrentLimitInfo(content) {
1143
- const varMatch = content.match(PATTERNS.variable);
1144
- if (varMatch) return {
1145
- limit: Number.parseInt(varMatch[2], 10),
1146
- varName: varMatch[1]
1147
- };
1148
- const funcMatch = content.match(PATTERNS.funcPatched);
1149
- if (funcMatch) {
1150
- const limitMatch = funcMatch[0].match(/return (\d+)\}$/);
1151
- return limitMatch ? { limit: Number.parseInt(limitMatch[1], 10) } : null;
1152
- }
1153
- return null;
1154
- }
1155
- /**
1156
- * Get current context limit from Claude Code (legacy wrapper)
1157
- */
1158
- function getCurrentLimit(content) {
1159
- return getCurrentLimitInfo(content)?.limit ?? null;
1160
- }
1161
- /**
1162
- * Check if Claude Code version is supported for patching
1163
- */
1164
- function checkVersionSupport(cliPath) {
1165
- const version = getClaudeCodeVersion(cliPath);
1166
- if (!version) return {
1167
- supported: false,
1168
- version: null,
1169
- patternType: null,
1170
- error: "Could not detect Claude Code version"
1171
- };
1172
- const patternType = getPatternTypeForVersion(version);
1173
- if (!patternType) return {
1174
- supported: false,
1175
- version,
1176
- patternType: null,
1177
- error: `Version ${version} is not supported. Supported: ${getSupportedRangeString()}`
1178
- };
1179
- return {
1180
- supported: true,
1181
- version,
1182
- patternType
1183
- };
1184
- }
1185
- /**
1186
- * Patch Claude Code to use a different context limit
1187
- */
1188
- function patchClaudeCode(cliPath, newLimit) {
1189
- const content = readFileSync(cliPath, "utf8");
1190
- const versionCheck = checkVersionSupport(cliPath);
1191
- if (!versionCheck.supported) {
1192
- consola.error(versionCheck.error);
1193
- return "failed";
1194
- }
1195
- consola.info(`Claude Code version: ${versionCheck.version}`);
1196
- const limitInfo = getCurrentLimitInfo(content);
1197
- if (limitInfo?.limit === newLimit) return "already_patched";
1198
- let newContent;
1199
- if (versionCheck.patternType === "variable") {
1200
- if (!limitInfo?.varName) {
1201
- consola.error("Could not detect variable name for patching");
1202
- return "failed";
1203
- }
1204
- newContent = content.replace(PATTERNS.variable, `var ${limitInfo.varName}=${newLimit}`);
1205
- } else {
1206
- const replacement = `function HR(A){if(A.includes("[1m]"))return 1e6;return ${newLimit}}`;
1207
- const pattern = PATTERNS.funcOriginal.test(content) ? PATTERNS.funcOriginal : PATTERNS.funcPatched;
1208
- newContent = content.replace(pattern, replacement);
1209
- }
1210
- writeFileSync(cliPath, newContent);
1211
- return "success";
1212
- }
1213
- /**
1214
- * Restore Claude Code to original 200k limit
1215
- */
1216
- function restoreClaudeCode(cliPath) {
1217
- const content = readFileSync(cliPath, "utf8");
1218
- const versionCheck = checkVersionSupport(cliPath);
1219
- if (!versionCheck.supported) {
1220
- consola.error(versionCheck.error);
1221
- return false;
1222
- }
1223
- consola.info(`Claude Code version: ${versionCheck.version}`);
1224
- const limitInfo = getCurrentLimitInfo(content);
1225
- if (limitInfo?.limit === 2e5) {
1226
- consola.info("Already at original 200000 limit");
1227
- return true;
1228
- }
1229
- let newContent;
1230
- if (versionCheck.patternType === "variable") {
1231
- if (!limitInfo?.varName) {
1232
- consola.error("Could not detect variable name for restoring");
1233
- return false;
1234
- }
1235
- newContent = content.replace(PATTERNS.variable, `var ${limitInfo.varName}=200000`);
1236
- } else newContent = content.replace(PATTERNS.funcPatched, "function HR(A){if(A.includes(\"[1m]\"))return 1e6;return 200000}");
1237
- writeFileSync(cliPath, newContent);
1238
- return true;
1239
- }
1240
- function showStatus(cliPath, currentLimit) {
1241
- const version = getClaudeCodeVersion(cliPath);
1242
- if (version) consola.info(`Claude Code version: ${version}`);
1243
- if (currentLimit === null) {
1244
- consola.warn("Could not detect current limit - CLI may have been updated");
1245
- consola.info("Look for a variable like 'var XXX=200000' followed by ',YYY=20000,' in cli.js");
1246
- } else if (currentLimit === 2e5) consola.info("Status: Original (200k context window)");
1247
- else consola.info(`Status: Patched (${currentLimit} context window)`);
1248
- }
1249
- const patchClaude = defineCommand({
1250
- meta: {
1251
- name: "patch-claude",
1252
- description: "Patch Claude Code's context window limit to match Copilot's limits"
1253
- },
1254
- args: {
1255
- limit: {
1256
- alias: "l",
1257
- type: "string",
1258
- default: "128000",
1259
- description: "Context window limit in tokens (default: 128000 for Copilot)"
1260
- },
1261
- restore: {
1262
- alias: "r",
1263
- type: "boolean",
1264
- default: false,
1265
- description: "Restore original 200k limit"
1266
- },
1267
- path: {
1268
- alias: "p",
1269
- type: "string",
1270
- description: "Path to Claude Code cli.js (auto-detected if not specified)"
1271
- },
1272
- status: {
1273
- alias: "s",
1274
- type: "boolean",
1275
- default: false,
1276
- description: "Show current patch status without modifying"
1277
- }
1278
- },
1279
- async run({ args }) {
1280
- let cliPath;
1281
- if (args.path) {
1282
- cliPath = args.path;
1283
- if (!existsSync(cliPath)) {
1284
- consola.error(`File not found: ${cliPath}`);
1285
- process.exit(1);
1286
- }
1287
- } else {
1288
- const installations = findAllClaudeCodePaths();
1289
- if (installations.length === 0) {
1290
- consola.error("Could not find Claude Code installation");
1291
- consola.info("Searched in: volta, npm global, bun global");
1292
- consola.info("Use --path to specify the path to cli.js manually");
1293
- process.exit(1);
1294
- }
1295
- if (installations.length === 1) cliPath = installations[0];
1296
- else {
1297
- consola.info(`Found ${installations.length} Claude Code installations:`);
1298
- const options = installations.map((path) => {
1299
- const info = getInstallationInfo(path);
1300
- let status = "unknown";
1301
- if (info.limit === 2e5) status = "original";
1302
- else if (info.limit) status = `patched: ${info.limit}`;
1303
- return {
1304
- label: `v${info.version ?? "?"} (${status}) - ${path}`,
1305
- value: path
1306
- };
1307
- });
1308
- const selected = await consola.prompt("Select installation to patch:", {
1309
- type: "select",
1310
- options
1311
- });
1312
- if (typeof selected === "symbol") process.exit(0);
1313
- cliPath = selected;
1314
- }
1315
- }
1316
- consola.info(`Claude Code path: ${cliPath}`);
1317
- const currentLimit = getCurrentLimit(readFileSync(cliPath, "utf8"));
1318
- if (args.status) {
1319
- showStatus(cliPath, currentLimit);
1320
- return;
1321
- }
1322
- if (args.restore) {
1323
- if (restoreClaudeCode(cliPath)) consola.success("Restored to original 200k limit");
1324
- else {
1325
- consola.error("Failed to restore - pattern not found");
1326
- consola.info("Claude Code may have been updated to a new version");
1327
- process.exit(1);
1328
- }
1329
- return;
1330
- }
1331
- const limit = Number.parseInt(args.limit, 10);
1332
- if (Number.isNaN(limit) || limit < 1e3) {
1333
- consola.error("Invalid limit value. Must be a number >= 1000");
1334
- process.exit(1);
1335
- }
1336
- const result = patchClaudeCode(cliPath, limit);
1337
- if (result === "success") {
1338
- consola.success(`Patched context window: ${currentLimit ?? 2e5} → ${limit}`);
1339
- consola.info("Note: You may need to re-run this after Claude Code updates");
1340
- } else if (result === "already_patched") consola.success(`Already patched with limit ${limit}`);
1341
- else {
1342
- consola.error("Failed to patch - pattern not found");
1343
- consola.info("Claude Code may have been updated to a new version");
1344
- process.exit(1);
1345
- }
1346
- }
1347
- });
1348
-
1349
994
  //#endregion
1350
995
  //#region package.json
1351
- var version = "0.11.3";
996
+ var version = "0.12.0";
1352
997
 
1353
998
  //#endregion
1354
999
  //#region src/lib/adaptive-rate-limiter.ts
@@ -1872,37 +1517,6 @@ function buildStartupAuthLines(params) {
1872
1517
  return [source === "none" ? `Inbound auth: 认证关闭 (no proxy API key configured)` : `Inbound auth: 认证开启 (source: ${source})`, `Binding to: ${bindAddress}`];
1873
1518
  }
1874
1519
  /**
1875
- * The env var Claude Code reads for its inbound credential, and the placeholder
1876
- * value the `--claude-code` setup always embeds for it. Exported so the
1877
- * generated env script (src/start.ts) and the auth hint below reference the SAME
1878
- * literals — changing the placeholder or the var name in one place can't silently
1879
- * desync the other (the hint would otherwise keep naming a string the generated
1880
- * command no longer contains).
1881
- */
1882
- const CLAUDE_CODE_AUTH_TOKEN_ENV = "ANTHROPIC_AUTH_TOKEN";
1883
- const CLAUDE_CODE_AUTH_TOKEN_PLACEHOLDER = "dummy";
1884
- /**
1885
- * Build the auth-aware hint lines for the `--claude-code` setup (Issue 05).
1886
- *
1887
- * The generated env script ALWAYS sets `ANTHROPIC_AUTH_TOKEN="dummy"` — a real
1888
- * key is deliberately never embedded, so the secret can't land in the clipboard
1889
- * or shell history. When inbound auth is ON, that placeholder won't authenticate
1890
- * against this proxy, so the operator must replace it. This builder returns the
1891
- * visible hint that tells them which variable to change:
1892
- * - auth OFF (`source === "none"`) → no hint (today's behavior, unchanged).
1893
- * - auth ON (`flag` / `env`) → a one-line hint naming
1894
- * `ANTHROPIC_AUTH_TOKEN` as the field to set to the proxy API key value.
1895
- *
1896
- * Like {@link buildStartupAuthLines}, the key value is **never** an input here —
1897
- * the builder only knows the `source` tag — so it is structurally impossible for
1898
- * the secret to leak into the hint. Pure (tag in → strings out) so the copy is
1899
- * pinned by unit tests.
1900
- */
1901
- function buildClaudeCodeAuthHint(source) {
1902
- if (source === "none") return [];
1903
- return [`Inbound auth is ON: replace ${CLAUDE_CODE_AUTH_TOKEN_ENV}="${CLAUDE_CODE_AUTH_TOKEN_PLACEHOLDER}" with your proxy API key value before using Claude Code.`];
1904
- }
1905
- /**
1906
1520
  * Configure the proxy API key on global state from a raw configured value.
1907
1521
  *
1908
1522
  * The value is trimmed; a trimmed-empty value (or `undefined`) is treated as
@@ -2841,58 +2455,6 @@ async function shutdownPostHog() {
2841
2455
  }
2842
2456
  }
2843
2457
 
2844
- //#endregion
2845
- //#region src/lib/shell.ts
2846
- function getShell() {
2847
- const { platform, ppid, env } = process$1;
2848
- if (platform === "win32") {
2849
- try {
2850
- if (execSync(`wmic process get ParentProcessId,Name | findstr "${ppid}"`, { stdio: "pipe" }).toString().toLowerCase().includes("powershell.exe")) return "powershell";
2851
- } catch {
2852
- return "cmd";
2853
- }
2854
- return "cmd";
2855
- } else {
2856
- const shellPath = env.SHELL;
2857
- if (shellPath) {
2858
- if (shellPath.endsWith("zsh")) return "zsh";
2859
- if (shellPath.endsWith("fish")) return "fish";
2860
- if (shellPath.endsWith("bash")) return "bash";
2861
- }
2862
- return "sh";
2863
- }
2864
- }
2865
- /**
2866
- * Generates a copy-pasteable script to set multiple environment variables
2867
- * and run a subsequent command.
2868
- * @param {EnvVars} envVars - An object of environment variables to set.
2869
- * @param {string} commandToRun - The command to run after setting the variables.
2870
- * @returns {string} The formatted script string.
2871
- */
2872
- function generateEnvScript(envVars, commandToRun = "") {
2873
- const shell = getShell();
2874
- const filteredEnvVars = Object.entries(envVars).filter(([, value]) => value !== void 0);
2875
- let commandBlock;
2876
- switch (shell) {
2877
- case "powershell":
2878
- commandBlock = filteredEnvVars.map(([key, value]) => `$env:${key} = "${value.replaceAll("\"", "`\"")}"`).join("; ");
2879
- break;
2880
- case "cmd":
2881
- commandBlock = filteredEnvVars.map(([key, value]) => `set ${key}=${value}`).join(" & ");
2882
- break;
2883
- case "fish":
2884
- commandBlock = filteredEnvVars.map(([key, value]) => `set -gx ${key} "${value.replaceAll("\"", String.raw`\"`)}"`).join("; ");
2885
- break;
2886
- default: {
2887
- const assignments = filteredEnvVars.map(([key, value]) => `${key}="${value.replaceAll("\"", String.raw`\"`)}"`).join(" ");
2888
- commandBlock = filteredEnvVars.length > 0 ? `export ${assignments}` : "";
2889
- break;
2890
- }
2891
- }
2892
- if (commandBlock && commandToRun) return `${commandBlock}${shell === "cmd" ? " & " : " && "}${commandToRun}`;
2893
- return commandBlock || commandToRun;
2894
- }
2895
-
2896
2458
  //#endregion
2897
2459
  //#region src/lib/shutdown.ts
2898
2460
  const DRAIN_POLL_INTERVAL_MS = 500;
@@ -7392,6 +6954,81 @@ function supportsDirectAnthropicApi(modelId) {
7392
6954
  return resolveAnthropicModelForDirectPath(modelId) !== void 0;
7393
6955
  }
7394
6956
 
6957
+ //#endregion
6958
+ //#region src/lib/stream-keepalive.ts
6959
+ /** SSE comment line used as a keepalive heartbeat. */
6960
+ const SSE_PING = ": ping\n\n";
6961
+ /**
6962
+ * Grace period before opening a keepalive stream. Normal upstream responses
6963
+ * resolve sub-second (response headers arrive immediately, the body is not
6964
+ * buffered); only a request queued behind the rate limiter takes >=10s. 3s
6965
+ * cleanly separates the two — a request still pending after 3s is queued.
6966
+ */
6967
+ const RATE_LIMIT_GRACE_MS = 3e3;
6968
+ /**
6969
+ * Interval between keepalive pings once the stream is open. Frequent enough to
6970
+ * prove liveness and defeat idle-connection drops, far below Claude Code's
6971
+ * default 10-minute request timeout.
6972
+ */
6973
+ const KEEPALIVE_PING_INTERVAL_MS = 5e3;
6974
+ /**
6975
+ * Capture a promise's outcome as a value. The returned promise never rejects,
6976
+ * so it can be raced against a timer and awaited again later without producing
6977
+ * an unhandled rejection or a second pending chain on the original promise.
6978
+ */
6979
+ function settle(promise) {
6980
+ return promise.then((value) => ({
6981
+ kind: "done",
6982
+ value
6983
+ }), (error) => ({
6984
+ kind: "error",
6985
+ error
6986
+ }));
6987
+ }
6988
+ /**
6989
+ * Race an already-settled promise against a grace timer. Resolves with the
6990
+ * settled outcome if it arrives within `graceMs`, otherwise `{ kind: "timeout" }`.
6991
+ * The underlying work keeps running — await the SAME `settled` promise afterward
6992
+ * to obtain its eventual result.
6993
+ */
6994
+ async function raceWithGrace(settled, graceMs) {
6995
+ let timer;
6996
+ const timeout = new Promise((resolve) => {
6997
+ timer = setTimeout(() => resolve({ kind: "timeout" }), graceMs);
6998
+ });
6999
+ const result = await Promise.race([settled, timeout]);
7000
+ if (timer !== void 0) clearTimeout(timer);
7001
+ return result;
7002
+ }
7003
+ /**
7004
+ * Drive an open SSE stream while the upstream request is still queued: emit a
7005
+ * ping immediately, then every `pingIntervalMs`, until `settled` resolves —
7006
+ * then hand off to `onResponse` (success) or `onError` (failure). Pings stop on
7007
+ * client abort, on resolution, and never write to an aborted stream.
7008
+ */
7009
+ async function runStreamWithKeepalive(opts) {
7010
+ const { stream, settled, pingIntervalMs, onResponse, onError } = opts;
7011
+ let pinging = true;
7012
+ const sendPing = () => {
7013
+ if (pinging && !stream.aborted) stream.write(SSE_PING);
7014
+ };
7015
+ sendPing();
7016
+ const interval = setInterval(sendPing, pingIntervalMs);
7017
+ stream.onAbort(() => {
7018
+ pinging = false;
7019
+ clearInterval(interval);
7020
+ });
7021
+ let result;
7022
+ try {
7023
+ result = await settled;
7024
+ } finally {
7025
+ pinging = false;
7026
+ clearInterval(interval);
7027
+ }
7028
+ if (result.kind === "done") await onResponse(result.value);
7029
+ else if (result.kind === "error") await onError(result.error);
7030
+ }
7031
+
7395
7032
  //#endregion
7396
7033
  //#region src/routes/messages/message-utils.ts
7397
7034
  function convertAnthropicMessages(messages) {
@@ -8599,27 +8236,66 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
8599
8236
  } else if (state.autoTruncate && !selectedModel) consola.debug(`[Anthropic] Model '${anthropicPayload.model}' not found, skipping auto-truncate`);
8600
8237
  if (state.manualApprove) await awaitApproval();
8601
8238
  const clientAnthropicBetaHeader = c.req.header("anthropic-beta");
8239
+ const isStreaming = anthropicPayload.stream === true;
8602
8240
  try {
8603
- const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createAnthropicMessages(effectivePayload, {
8241
+ const settled = settle(executeWithAdaptiveRateLimit(() => createAnthropicMessages(effectivePayload, {
8604
8242
  initiator: initiatorOverride,
8605
8243
  injectContext1mBeta: needsContext1mBeta,
8606
8244
  errorModelIdOverride: needsContext1mBeta ? originalModelId : void 0,
8607
8245
  clientAnthropicBetaHeader
8608
- }));
8609
- ctx.queueWaitMs = queueWaitMs;
8610
- if (Symbol.asyncIterator in response) {
8611
- consola.debug("Streaming response from Copilot (direct Anthropic)");
8612
- updateTrackerStatus(ctx.trackingId, "streaming");
8613
- return streamSSE(c, async (stream) => {
8614
- await handleDirectAnthropicStreamingResponse({
8615
- stream,
8616
- response,
8617
- anthropicPayload: effectivePayload,
8618
- ctx
8246
+ })));
8247
+ const raced = isStreaming ? await raceWithGrace(settled, RATE_LIMIT_GRACE_MS) : await settled;
8248
+ if (raced.kind === "error") throw raced.error;
8249
+ if (raced.kind === "done") {
8250
+ const { result: response, queueWaitMs } = raced.value;
8251
+ ctx.queueWaitMs = queueWaitMs;
8252
+ if (Symbol.asyncIterator in response) {
8253
+ consola.debug("Streaming response from Copilot (direct Anthropic)");
8254
+ updateTrackerStatus(ctx.trackingId, "streaming");
8255
+ return streamSSE(c, async (stream) => {
8256
+ await handleDirectAnthropicStreamingResponse({
8257
+ stream,
8258
+ response,
8259
+ anthropicPayload: effectivePayload,
8260
+ ctx
8261
+ });
8619
8262
  });
8620
- });
8263
+ }
8264
+ return handleDirectAnthropicNonStreamingResponse(c, recoverLeakedToolCallsInResponse(response, toolNameSet(effectivePayload.tools)), ctx, truncateResult, effectivePayload);
8621
8265
  }
8622
- return handleDirectAnthropicNonStreamingResponse(c, recoverLeakedToolCallsInResponse(response, toolNameSet(effectivePayload.tools)), ctx, truncateResult, effectivePayload);
8266
+ consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (direct Anthropic)");
8267
+ updateTrackerStatus(ctx.trackingId, "streaming");
8268
+ return streamSSE(c, async (stream) => {
8269
+ await runStreamWithKeepalive({
8270
+ stream,
8271
+ settled,
8272
+ pingIntervalMs: KEEPALIVE_PING_INTERVAL_MS,
8273
+ onResponse: async ({ result: response, queueWaitMs }) => {
8274
+ ctx.queueWaitMs = queueWaitMs;
8275
+ await handleDirectAnthropicStreamingResponse({
8276
+ stream,
8277
+ response,
8278
+ anthropicPayload: effectivePayload,
8279
+ ctx
8280
+ });
8281
+ },
8282
+ onError: async (error) => {
8283
+ recordStreamError({
8284
+ acc: createAnthropicStreamAccumulator(),
8285
+ fallbackModel: anthropicPayload.model,
8286
+ ctx,
8287
+ error,
8288
+ endpoint: "messages"
8289
+ });
8290
+ failTracking(ctx.trackingId, error);
8291
+ const errorEvent = translateErrorToAnthropicErrorEvent(error);
8292
+ await stream.writeSSE({
8293
+ event: errorEvent.type,
8294
+ data: JSON.stringify(errorEvent)
8295
+ });
8296
+ }
8297
+ });
8298
+ });
8623
8299
  } catch (error) {
8624
8300
  if (error instanceof HTTPError && error.status === 413) logPayloadSizeInfoAnthropic(effectivePayload, selectedModel);
8625
8301
  recordErrorResponse(ctx, anthropicPayload.model, error, "messages", anthropicPayload.stream ?? false);
@@ -8864,30 +8540,71 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
8864
8540
  if (autoTruncateConfig.tokenLimitCacheKeyOverride !== void 0) errorModelIdOverride = autoTruncateConfig.tokenLimitCacheKeyOverride;
8865
8541
  else if (needsContext1mBeta && selectedModel) errorModelIdOverride = selectedModel.id;
8866
8542
  else if (hasOneMillionSuffix) errorModelIdOverride = originalModelId;
8543
+ const isStreaming = anthropicPayload.stream === true;
8867
8544
  try {
8868
- const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(openAIPayload, {
8545
+ const settled = settle(executeWithAdaptiveRateLimit(() => createChatCompletions(openAIPayload, {
8869
8546
  initiator: initiatorOverride,
8870
8547
  resolvedModel: selectedModel,
8871
8548
  anthropicBeta,
8872
8549
  errorModelIdOverride
8873
- }));
8874
- ctx.queueWaitMs = queueWaitMs;
8875
- if (isNonStreaming(response)) return handleNonStreamingResponse({
8876
- c,
8877
- response,
8878
- toolNameMapping,
8879
- ctx,
8880
- anthropicPayload
8881
- });
8882
- consola.debug("Streaming response from Copilot");
8550
+ })));
8551
+ const raced = isStreaming ? await raceWithGrace(settled, RATE_LIMIT_GRACE_MS) : await settled;
8552
+ if (raced.kind === "error") throw raced.error;
8553
+ if (raced.kind === "done") {
8554
+ const { result: response, queueWaitMs } = raced.value;
8555
+ ctx.queueWaitMs = queueWaitMs;
8556
+ if (isNonStreaming(response)) return handleNonStreamingResponse({
8557
+ c,
8558
+ response,
8559
+ toolNameMapping,
8560
+ ctx,
8561
+ anthropicPayload
8562
+ });
8563
+ consola.debug("Streaming response from Copilot");
8564
+ updateTrackerStatus(ctx.trackingId, "streaming");
8565
+ return streamSSE(c, async (stream) => {
8566
+ await handleStreamingResponse({
8567
+ stream,
8568
+ response,
8569
+ toolNameMapping,
8570
+ anthropicPayload,
8571
+ ctx
8572
+ });
8573
+ });
8574
+ }
8575
+ consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (translated)");
8883
8576
  updateTrackerStatus(ctx.trackingId, "streaming");
8884
8577
  return streamSSE(c, async (stream) => {
8885
- await handleStreamingResponse({
8578
+ await runStreamWithKeepalive({
8886
8579
  stream,
8887
- response,
8888
- toolNameMapping,
8889
- anthropicPayload,
8890
- ctx
8580
+ settled,
8581
+ pingIntervalMs: KEEPALIVE_PING_INTERVAL_MS,
8582
+ onResponse: async ({ result: response, queueWaitMs }) => {
8583
+ ctx.queueWaitMs = queueWaitMs;
8584
+ if (isNonStreaming(response)) return;
8585
+ await handleStreamingResponse({
8586
+ stream,
8587
+ response,
8588
+ toolNameMapping,
8589
+ anthropicPayload,
8590
+ ctx
8591
+ });
8592
+ },
8593
+ onError: async (error) => {
8594
+ recordStreamError({
8595
+ acc: createAnthropicStreamAccumulator(),
8596
+ fallbackModel: anthropicPayload.model,
8597
+ ctx,
8598
+ error,
8599
+ endpoint: "messages"
8600
+ });
8601
+ failTracking(ctx.trackingId, error);
8602
+ const errorEvent = translateErrorToAnthropicErrorEvent(error);
8603
+ await stream.writeSSE({
8604
+ event: errorEvent.type,
8605
+ data: JSON.stringify(errorEvent)
8606
+ });
8607
+ }
8891
8608
  });
8892
8609
  });
8893
8610
  } catch (error) {
@@ -9770,7 +9487,6 @@ function formatModelInfo(model) {
9770
9487
  async function runServer(options) {
9771
9488
  consola.info(`copilot-api v${version}`);
9772
9489
  configureProxyApiKey(options.apiKey);
9773
- if (options.proxyEnv) initProxyFromEnv();
9774
9490
  if (options.verbose) {
9775
9491
  consola.level = 5;
9776
9492
  consola.info("Verbose logging enabled");
@@ -9784,34 +9500,11 @@ async function runServer(options) {
9784
9500
  process.exit(1);
9785
9501
  }
9786
9502
  if (options.accountType !== "individual") consola.info(`Using ${options.accountType} plan GitHub account`);
9787
- state.manualApprove = options.manual;
9788
- state.showToken = options.showToken;
9789
- state.showAllModels = options.showAllModels;
9790
- if (options.showAllModels) consola.warn("--show-all-models: hidden model blacklist is BYPASSED for this run");
9791
- state.autoTruncate = options.autoTruncate;
9792
- state.compressToolResults = options.compressToolResults;
9793
- state.redirectAnthropic = options.redirectAnthropic;
9794
- state.stripServerTools = options.stripServerTools;
9795
- state.contextEditingMode = options.contextEditing;
9796
- state.timezoneOffset = options.timezoneOffset;
9797
- if (options.rateLimit) initAdaptiveRateLimiter({
9798
- baseRetryIntervalSeconds: options.retryInterval,
9799
- requestIntervalSeconds: options.requestInterval,
9800
- recoveryTimeoutMinutes: options.recoveryTimeout,
9801
- consecutiveSuccessesForRecovery: options.consecutiveSuccesses
9802
- });
9503
+ if (options.rateLimit) initAdaptiveRateLimiter();
9803
9504
  else consola.info("Rate limiting disabled");
9804
- if (!options.autoTruncate) consola.info("Auto-truncate disabled");
9805
- if (options.compressToolResults) consola.info("Tool result compression enabled");
9806
- if (options.redirectAnthropic) consola.info("Anthropic API redirect enabled (using OpenAI translation)");
9807
- if (options.stripServerTools) consola.info("Server-side tools will be stripped from requests");
9808
- if (options.contextEditing !== "off") consola.info(`Context editing mode: ${options.contextEditing}`);
9809
- initHistory(options.history, options.historyLimit);
9810
- if (options.history) {
9811
- const limitText = options.historyLimit === 0 ? "unlimited" : `max ${options.historyLimit}`;
9812
- consola.info(`History recording enabled (${limitText} entries)`);
9813
- startMemoryPressureMonitor();
9814
- }
9505
+ initHistory(true, 1e3);
9506
+ consola.info("History recording enabled (max 1000 entries)");
9507
+ startMemoryPressureMonitor();
9815
9508
  if (options.posthogKey) {
9816
9509
  initPostHog(options.posthogKey);
9817
9510
  if (isPostHogEnabled()) consola.info("PostHog analytics enabled");
@@ -9838,42 +9531,10 @@ async function runServer(options) {
9838
9531
  process.exit(1);
9839
9532
  }
9840
9533
  const visibleModels = allModels.filter((m) => !isHiddenModel(m.id, state.showAllModels));
9841
- 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.");
9534
+ 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. Edit src/lib/hidden-models.ts to change the blacklist.");
9842
9535
  else consola.info(`Available models:\n${visibleModels.map((m) => formatModelInfo(m)).join("\n")}`);
9843
9536
  const serverUrl = `http://${resolveClientHost(options.host, void 0)}:${options.port}`;
9844
- if (options.claudeCode) {
9845
- if (visibleModels.length === 0) {
9846
- consola.error("--claude-code interactive setup needs at least one visible model. Restart with --show-all-models or update src/lib/hidden-models.ts.");
9847
- process.exit(1);
9848
- }
9849
- const selectedModel = await consola.prompt("Select a model to use with Claude Code", {
9850
- type: "select",
9851
- options: visibleModels.map((model) => model.id)
9852
- });
9853
- const selectedSmallModel = await consola.prompt("Select a small model to use with Claude Code", {
9854
- type: "select",
9855
- options: visibleModels.map((model) => model.id)
9856
- });
9857
- const command = generateEnvScript({
9858
- ANTHROPIC_BASE_URL: serverUrl,
9859
- [CLAUDE_CODE_AUTH_TOKEN_ENV]: CLAUDE_CODE_AUTH_TOKEN_PLACEHOLDER,
9860
- ANTHROPIC_MODEL: selectedModel,
9861
- ANTHROPIC_DEFAULT_SONNET_MODEL: selectedModel,
9862
- ANTHROPIC_SMALL_FAST_MODEL: selectedSmallModel,
9863
- ANTHROPIC_DEFAULT_HAIKU_MODEL: selectedSmallModel,
9864
- DISABLE_NON_ESSENTIAL_MODEL_CALLS: "1",
9865
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
9866
- }, "claude");
9867
- try {
9868
- clipboard.writeSync(command);
9869
- consola.success("Copied Claude Code command to clipboard!");
9870
- } catch {
9871
- consola.warn("Failed to copy to clipboard. Here is the Claude Code command:");
9872
- consola.log(command);
9873
- }
9874
- for (const line of buildClaudeCodeAuthHint(options.apiKeySource)) consola.warn(line);
9875
- }
9876
- consola.box(`🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage${options.history ? `\n📜 History UI: ${serverUrl}/history` : ""}`);
9537
+ consola.box(`🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage\n📜 History UI: ${serverUrl}/history`);
9877
9538
  for (const line of buildStartupAuthLines({
9878
9539
  source: options.apiKeySource,
9879
9540
  bindAddress: resolveBindAddress(options.host, void 0)
@@ -9894,23 +9555,6 @@ async function runServer(options) {
9894
9555
  } }
9895
9556
  }));
9896
9557
  }
9897
- function parseTimezoneOffset(value) {
9898
- if (typeof value !== "string") return 8;
9899
- const n = Number(value);
9900
- if (!Number.isFinite(n)) return 8;
9901
- return n;
9902
- }
9903
- const validContextEditingModes = [
9904
- "off",
9905
- "clear-thinking",
9906
- "clear-tooluse",
9907
- "clear-both"
9908
- ];
9909
- function parseContextEditing(value) {
9910
- if (validContextEditingModes.includes(value)) return value;
9911
- consola.warn(`Invalid context editing mode: "${value}", using "off". Valid: ${validContextEditingModes.join(", ")}`);
9912
- return "off";
9913
- }
9914
9558
  const start = defineCommand({
9915
9559
  meta: {
9916
9560
  name: "start",
@@ -9940,101 +9584,15 @@ const start = defineCommand({
9940
9584
  default: "individual",
9941
9585
  description: "Account type to use (individual, business, enterprise)"
9942
9586
  },
9943
- manual: {
9944
- type: "boolean",
9945
- default: false,
9946
- description: "Enable manual request approval"
9947
- },
9948
9587
  "no-rate-limit": {
9949
9588
  type: "boolean",
9950
9589
  default: false,
9951
9590
  description: "Disable adaptive rate limiting"
9952
9591
  },
9953
- "retry-interval": {
9954
- type: "string",
9955
- default: "10",
9956
- description: "Seconds to wait before retrying after rate limit error (default: 10)"
9957
- },
9958
- "request-interval": {
9959
- type: "string",
9960
- default: "10",
9961
- description: "Seconds between requests in rate-limited mode (default: 10)"
9962
- },
9963
- "recovery-timeout": {
9964
- type: "string",
9965
- default: "10",
9966
- description: "Minutes before attempting to recover from rate-limited mode (default: 10)"
9967
- },
9968
- "consecutive-successes": {
9969
- type: "string",
9970
- default: "5",
9971
- description: "Number of consecutive successes needed to recover from rate-limited mode (default: 5)"
9972
- },
9973
9592
  "github-token": {
9974
9593
  alias: "g",
9975
9594
  type: "string",
9976
- description: "Provide GitHub token directly (must be generated using the `auth` subcommand). Falls back to the GH_TOKEN env var if the flag is omitted — prefer the env for automation since argv is visible via /proc/<pid>/cmdline."
9977
- },
9978
- "claude-code": {
9979
- alias: "c",
9980
- type: "boolean",
9981
- default: false,
9982
- description: "Generate a command to launch Claude Code with Copilot API config"
9983
- },
9984
- "show-token": {
9985
- type: "boolean",
9986
- default: false,
9987
- description: "Show GitHub and Copilot tokens on fetch and refresh"
9988
- },
9989
- "show-all-models": {
9990
- type: "boolean",
9991
- default: false,
9992
- description: "Show ALL upstream models, including the hardcoded blacklist (default: false, blacklist filtered from listings)"
9993
- },
9994
- "proxy-env": {
9995
- type: "boolean",
9996
- default: false,
9997
- description: "Initialize proxy from environment variables"
9998
- },
9999
- "no-history": {
10000
- type: "boolean",
10001
- default: false,
10002
- description: "Disable request history recording and Web UI"
10003
- },
10004
- "history-limit": {
10005
- type: "string",
10006
- default: "1000",
10007
- description: "Maximum number of history entries to keep in memory (0 = unlimited)"
10008
- },
10009
- "no-auto-truncate": {
10010
- type: "boolean",
10011
- default: false,
10012
- description: "Disable automatic conversation history truncation when exceeding limits"
10013
- },
10014
- "compress-tool-results": {
10015
- type: "boolean",
10016
- default: false,
10017
- description: "Compress old tool_result content before truncating messages (may lose context details)"
10018
- },
10019
- "redirect-anthropic": {
10020
- type: "boolean",
10021
- default: false,
10022
- description: "Redirect Anthropic models through OpenAI translation (instead of direct API)"
10023
- },
10024
- "strip-server-tools": {
10025
- type: "boolean",
10026
- default: false,
10027
- description: "Strip Anthropic server-side tools (web_search, etc.) from requests"
10028
- },
10029
- "context-editing": {
10030
- type: "string",
10031
- default: "off",
10032
- description: "Context editing mode: off, clear-thinking, clear-tooluse, clear-both"
10033
- },
10034
- "timezone-offset": {
10035
- type: "string",
10036
- default: "+8",
10037
- description: "Timezone offset in hours from UTC for log timestamps (e.g., +8, -5, 0)"
9595
+ description: "Provide GitHub token directly (must be generated using the `login` subcommand). Falls back to the GH_TOKEN env var if the flag is omitted — prefer the env for automation since argv is visible via /proc/<pid>/cmdline."
10038
9596
  },
10039
9597
  "posthog-key": {
10040
9598
  type: "string",
@@ -10055,25 +9613,8 @@ const start = defineCommand({
10055
9613
  host: resolveBindHost(args.host, process.env.HOST),
10056
9614
  verbose: args.verbose,
10057
9615
  accountType: args["account-type"],
10058
- manual: args.manual,
10059
9616
  rateLimit: !args["no-rate-limit"],
10060
- retryInterval: Number.parseInt(args["retry-interval"], 10),
10061
- requestInterval: Number.parseInt(args["request-interval"], 10),
10062
- recoveryTimeout: Number.parseInt(args["recovery-timeout"], 10),
10063
- consecutiveSuccesses: Number.parseInt(args["consecutive-successes"], 10),
10064
9617
  githubToken: args["github-token"] || process.env.GH_TOKEN,
10065
- claudeCode: args["claude-code"],
10066
- showToken: args["show-token"],
10067
- showAllModels: args["show-all-models"],
10068
- proxyEnv: args["proxy-env"],
10069
- history: !args["no-history"],
10070
- historyLimit: Number.parseInt(args["history-limit"], 10),
10071
- autoTruncate: !args["no-auto-truncate"],
10072
- compressToolResults: args["compress-tool-results"],
10073
- redirectAnthropic: args["redirect-anthropic"],
10074
- stripServerTools: args["strip-server-tools"],
10075
- contextEditing: parseContextEditing(args["context-editing"]),
10076
- timezoneOffset: parseTimezoneOffset(args["timezone-offset"]),
10077
9618
  posthogKey: args["posthog-key"],
10078
9619
  apiKey: resolvedApiKey.key,
10079
9620
  apiKeySource: resolvedApiKey.source
@@ -10090,12 +9631,10 @@ await runMain(defineCommand({
10090
9631
  description: "A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools."
10091
9632
  },
10092
9633
  subCommands: {
10093
- auth,
9634
+ login,
10094
9635
  logout,
10095
9636
  start,
10096
- "check-usage": checkUsage,
10097
- debug,
10098
- "patch-claude": patchClaude
9637
+ debug
10099
9638
  }
10100
9639
  }));
10101
9640
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.11.3",
3
+ "version": "0.12.0",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",
@@ -36,7 +36,6 @@
36
36
  "dependencies": {
37
37
  "@anthropic-ai/tokenizer": "^0.0.4",
38
38
  "citty": "^0.2.0",
39
- "clipboardy": "^5.1.0",
40
39
  "consola": "^3.4.2",
41
40
  "fetch-event-stream": "^0.1.6",
42
41
  "gpt-tokenizer": "^3.4.0",