@ashsec/copilot-api 0.8.0 → 0.11.2

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/dist/main.js CHANGED
@@ -5,7 +5,6 @@ import fs from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { randomUUID } from "node:crypto";
8
- import { events } from "fetch-event-stream";
9
8
  import clipboard from "clipboardy";
10
9
  import { serve } from "srvx";
11
10
  import invariant from "tiny-invariant";
@@ -17,28 +16,102 @@ import process$1 from "node:process";
17
16
  import { Hono } from "hono";
18
17
  import { cors } from "hono/cors";
19
18
  import { streamSSE } from "hono/streaming";
19
+ import { events } from "fetch-event-stream";
20
20
  import util from "node:util";
21
21
 
22
22
  //#region package.json
23
- var version = "0.8.0";
23
+ var name = "@ashsec/copilot-api";
24
+ var version = "0.11.2";
25
+ var description = "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!";
26
+ var keywords = [
27
+ "proxy",
28
+ "github-copilot",
29
+ "openai-compatible"
30
+ ];
31
+ var homepage = "https://github.com/ericc-ch/copilot-api";
32
+ var bugs = "https://github.com/ericc-ch/copilot-api/issues";
33
+ var repository = {
34
+ "type": "git",
35
+ "url": "git+https://github.com/ericc-ch/copilot-api.git"
36
+ };
37
+ var author = "Erick Christian <erickchristian48@gmail.com>";
38
+ var type = "module";
39
+ var bin = { "copilot-api": "./dist/main.js" };
40
+ var files = ["dist"];
41
+ var scripts = {
42
+ "build": "tsdown",
43
+ "dev": "bun run --watch ./src/main.ts",
44
+ "knip": "knip-bun",
45
+ "lint": "eslint --cache",
46
+ "lint:all": "eslint --cache .",
47
+ "prepack": "bun run build",
48
+ "prepare": "simple-git-hooks",
49
+ "release": "bumpp && bun publish --access public",
50
+ "start": "NODE_ENV=production bun run ./src/main.ts",
51
+ "typecheck": "tsc"
52
+ };
53
+ var simple_git_hooks = { "pre-commit": "bunx lint-staged" };
54
+ var lint_staged = { "*": "bun run lint --fix" };
55
+ var dependencies = {
56
+ "citty": "^0.1.6",
57
+ "clipboardy": "^5.0.0",
58
+ "consola": "^3.4.2",
59
+ "fetch-event-stream": "^0.1.5",
60
+ "gpt-tokenizer": "^3.0.1",
61
+ "hono": "^4.9.9",
62
+ "ms": "^2.1.3",
63
+ "proxy-from-env": "^1.1.0",
64
+ "srvx": "^0.8.9",
65
+ "tiny-invariant": "^1.3.3",
66
+ "undici": "^7.16.0",
67
+ "zod": "^4.1.11"
68
+ };
69
+ var devDependencies = {
70
+ "@echristian/eslint-config": "^0.0.54",
71
+ "@types/bun": "^1.2.23",
72
+ "@types/proxy-from-env": "^1.0.4",
73
+ "bumpp": "^10.2.3",
74
+ "eslint": "^9.37.0",
75
+ "knip": "^5.64.1",
76
+ "lint-staged": "^16.2.3",
77
+ "prettier-plugin-packagejson": "^2.5.19",
78
+ "simple-git-hooks": "^2.13.1",
79
+ "tsdown": "^0.15.6",
80
+ "typescript": "^5.9.3"
81
+ };
82
+ var package_default = {
83
+ name,
84
+ version,
85
+ description,
86
+ keywords,
87
+ homepage,
88
+ bugs,
89
+ repository,
90
+ author,
91
+ type,
92
+ bin,
93
+ files,
94
+ scripts,
95
+ "simple-git-hooks": simple_git_hooks,
96
+ "lint-staged": lint_staged,
97
+ dependencies,
98
+ devDependencies
99
+ };
24
100
 
25
101
  //#endregion
26
102
  //#region src/lib/paths.ts
27
103
  const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api");
28
104
  const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
29
- const AZURE_OPENAI_CONFIG_PATH = path.join(APP_DIR, "azure_openai_config");
30
105
  const REPLACEMENTS_CONFIG_PATH = path.join(APP_DIR, "replacements.json");
31
106
  const PATHS = {
32
107
  APP_DIR,
33
108
  CONFIG_PATH: path.join(APP_DIR, "config.json"),
34
109
  GITHUB_TOKEN_PATH,
35
- AZURE_OPENAI_CONFIG_PATH,
36
110
  REPLACEMENTS_CONFIG_PATH
37
111
  };
38
112
  async function ensurePaths() {
39
113
  await fs.mkdir(PATHS.APP_DIR, { recursive: true });
40
114
  await ensureFile(PATHS.GITHUB_TOKEN_PATH);
41
- await ensureFile(PATHS.AZURE_OPENAI_CONFIG_PATH);
42
115
  }
43
116
  async function ensureFile(filePath) {
44
117
  try {
@@ -180,56 +253,6 @@ async function getGitHubUser() {
180
253
  return await response.json();
181
254
  }
182
255
 
183
- //#endregion
184
- //#region src/services/azure-openai/config.ts
185
- const AZURE_OPENAI_MODEL_PREFIX = "azure_openai_";
186
- async function loadAzureOpenAIConfig() {
187
- try {
188
- const content = await fs.readFile(PATHS.AZURE_OPENAI_CONFIG_PATH, "utf8");
189
- if (!content.trim()) return null;
190
- const decoded = Buffer.from(content.trim(), "base64").toString("utf8");
191
- const config$1 = JSON.parse(decoded);
192
- if (!config$1.endpoint || !config$1.apiKey) return null;
193
- return config$1;
194
- } catch {
195
- return null;
196
- }
197
- }
198
- async function saveAzureOpenAIConfig(config$1) {
199
- const encoded = Buffer.from(JSON.stringify(config$1)).toString("base64");
200
- await fs.writeFile(PATHS.AZURE_OPENAI_CONFIG_PATH, encoded, "utf8");
201
- await fs.chmod(PATHS.AZURE_OPENAI_CONFIG_PATH, 384);
202
- consola.success("Azure OpenAI configuration saved");
203
- }
204
- async function promptAzureOpenAISetup() {
205
- if (!await consola.prompt("Would you like to add a custom Azure OpenAI endpoint?", {
206
- type: "confirm",
207
- initial: false
208
- })) return null;
209
- const endpoint = await consola.prompt("Enter your Azure OpenAI endpoint URL (e.g., https://your-resource.openai.azure.com):", { type: "text" });
210
- if (!endpoint || typeof endpoint !== "string" || !endpoint.trim()) {
211
- consola.warn("No endpoint provided, skipping Azure OpenAI setup");
212
- return null;
213
- }
214
- const apiKey = await consola.prompt("Enter your Azure OpenAI API key:", { type: "text" });
215
- if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
216
- consola.warn("No API key provided, skipping Azure OpenAI setup");
217
- return null;
218
- }
219
- const config$1 = {
220
- endpoint: endpoint.trim().replace(/\/$/, ""),
221
- apiKey: apiKey.trim()
222
- };
223
- await saveAzureOpenAIConfig(config$1);
224
- return config$1;
225
- }
226
- function isAzureOpenAIModel(modelId) {
227
- return modelId.startsWith(AZURE_OPENAI_MODEL_PREFIX);
228
- }
229
- function getAzureDeploymentName(modelId) {
230
- return modelId.slice(13);
231
- }
232
-
233
256
  //#endregion
234
257
  //#region src/lib/retry-fetch.ts
235
258
  const RETRY_DELAYS_MS = [
@@ -273,8 +296,8 @@ async function fetchWithRetry(input, init) {
273
296
  let lastError;
274
297
  let lastResponse;
275
298
  for (let attempt = 0; attempt < maxAttempts; attempt++) try {
276
- const headers = new Headers(init?.headers);
277
- headers.set("Connection", "close");
299
+ const headers = toHeaderRecord(init?.headers);
300
+ headers.Connection = "close";
278
301
  const response = await fetch(input, {
279
302
  ...init,
280
303
  headers,
@@ -296,63 +319,24 @@ async function fetchWithRetry(input, init) {
296
319
  await sleep(delayMs);
297
320
  }
298
321
  if (lastResponse) return lastResponse;
299
- throw lastError;
300
- }
301
-
302
- //#endregion
303
- //#region src/services/azure-openai/create-chat-completions.ts
304
- const AZURE_API_VERSION = "2024-10-21";
305
- async function createAzureOpenAIChatCompletions(config$1, payload) {
306
- const deploymentName = getAzureDeploymentName(payload.model);
307
- const { max_tokens,...restPayload } = payload;
308
- const azurePayload = {
309
- ...restPayload,
310
- model: deploymentName,
311
- ...max_tokens != null && { max_completion_tokens: max_tokens }
312
- };
313
- const response = await fetchWithRetry(`${config$1.endpoint}/openai/deployments/${deploymentName}/chat/completions?api-version=${AZURE_API_VERSION}`, {
314
- method: "POST",
315
- headers: {
316
- "api-key": config$1.apiKey,
317
- "Content-Type": "application/json"
318
- },
319
- body: JSON.stringify(azurePayload)
320
- });
321
- if (!response.ok) {
322
- consola.error("Failed to create Azure OpenAI chat completions:", response);
323
- throw new HTTPError("Failed to create Azure OpenAI chat completions", response, payload);
324
- }
325
- if (payload.stream) return events(response);
326
- return await response.json();
322
+ throw lastError ?? /* @__PURE__ */ new Error("Request failed without a captured error");
327
323
  }
328
-
329
- //#endregion
330
- //#region src/services/azure-openai/get-models.ts
331
- const AZURE_DEPLOYMENTS_API_VERSION = "2022-12-01";
332
- async function getAzureOpenAIDeployments(config$1) {
333
- try {
334
- const response = await fetchWithRetry(`${config$1.endpoint}/openai/deployments?api-version=${AZURE_DEPLOYMENTS_API_VERSION}`, { headers: {
335
- "api-key": config$1.apiKey,
336
- "Content-Type": "application/json"
337
- } });
338
- if (!response.ok) {
339
- const errorText = await response.text().catch(() => "");
340
- consola.error(`Failed to fetch Azure OpenAI deployments: ${response.status}`, errorText);
341
- throw new HTTPError("Failed to fetch Azure OpenAI deployments", response);
342
- }
343
- return (await response.json()).data.filter((deployment) => deployment.status === "succeeded").map((deployment) => ({
344
- id: `${AZURE_OPENAI_MODEL_PREFIX}${deployment.id}`,
345
- deploymentName: deployment.id,
346
- model: deployment.model,
347
- created: deployment.created_at,
348
- object: "deployment",
349
- owned_by: deployment.owner || "azure-openai"
350
- }));
351
- } catch (error) {
352
- if (error instanceof HTTPError) throw error;
353
- consola.error("Failed to fetch Azure OpenAI deployments:", error);
354
- return [];
324
+ function toHeaderRecord(headersInit) {
325
+ const headers = {};
326
+ if (!headersInit) return headers;
327
+ if (headersInit instanceof Headers) {
328
+ for (const [key, value] of headersInit.entries()) headers[key] = value;
329
+ return headers;
330
+ }
331
+ if (Array.isArray(headersInit)) {
332
+ for (const entry of headersInit) if (Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && typeof entry[1] === "string") {
333
+ const [key, value] = entry;
334
+ headers[key] = value;
335
+ }
336
+ return headers;
355
337
  }
338
+ for (const [key, value] of Object.entries(headersInit)) if (typeof value === "string") headers[key] = value;
339
+ return headers;
356
340
  }
357
341
 
358
342
  //#endregion
@@ -418,24 +402,6 @@ const cacheVSCodeVersion = async () => {
418
402
  state.vsCodeVersion = response;
419
403
  consola.info(`Using VSCode version: ${response}`);
420
404
  };
421
- async function setupAzureOpenAI() {
422
- let config$1 = await loadAzureOpenAIConfig();
423
- if (!config$1) config$1 = await promptAzureOpenAISetup();
424
- if (!config$1) {
425
- consola.info("Azure OpenAI not configured");
426
- return;
427
- }
428
- state.azureOpenAIConfig = config$1;
429
- consola.info("Azure OpenAI configuration loaded");
430
- try {
431
- const deployments = await getAzureOpenAIDeployments(config$1);
432
- state.azureOpenAIDeployments = deployments;
433
- if (deployments.length > 0) consola.info(`Loaded ${deployments.length} Azure OpenAI deployment(s):\n${deployments.map((d) => `- ${d.id} (${d.model})`).join("\n")}`);
434
- else consola.warn("No Azure OpenAI deployments found");
435
- } catch (error) {
436
- consola.warn("Failed to fetch Azure OpenAI deployments:", error);
437
- }
438
- }
439
405
 
440
406
  //#endregion
441
407
  //#region src/services/github/poll-access-token.ts
@@ -583,13 +549,13 @@ const checkUsage = defineCommand({
583
549
  const premiumUsed = premiumTotal - premium.remaining;
584
550
  const premiumPercentUsed = premiumTotal > 0 ? premiumUsed / premiumTotal * 100 : 0;
585
551
  const premiumPercentRemaining = premium.percent_remaining;
586
- function summarizeQuota(name, snap) {
587
- if (!snap) return `${name}: N/A`;
552
+ function summarizeQuota(name$1, snap) {
553
+ if (!snap) return `${name$1}: N/A`;
588
554
  const total = snap.entitlement;
589
555
  const used = total - snap.remaining;
590
556
  const percentUsed = total > 0 ? used / total * 100 : 0;
591
557
  const percentRemaining = snap.percent_remaining;
592
- return `${name}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
558
+ return `${name$1}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
593
559
  }
594
560
  const premiumLine = `Premium: ${premiumUsed}/${premiumTotal} used (${premiumPercentUsed.toFixed(1)}% used, ${premiumPercentRemaining.toFixed(1)}% remaining)`;
595
561
  const chatLine = summarizeQuota("Chat", usage.quota_snapshots.chat);
@@ -665,11 +631,11 @@ async function getUserReplacements() {
665
631
  * Add a new user replacement rule
666
632
  */
667
633
  async function addReplacement(pattern, replacement, options) {
668
- const { isRegex = false, name } = options ?? {};
634
+ const { isRegex = false, name: name$1 } = options ?? {};
669
635
  await ensureLoaded();
670
636
  const rule = {
671
637
  id: `user-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
672
- name,
638
+ name: name$1,
673
639
  pattern,
674
640
  replacement,
675
641
  isRegex,
@@ -785,34 +751,49 @@ async function applyReplacements(text) {
785
751
  appliedRules.push(rule.name || rule.id);
786
752
  }
787
753
  }
788
- if (appliedRules.length > 0) consola.info(`Replacements applied: ${appliedRules.join(", ")}`);
789
- return result;
754
+ return {
755
+ text: result,
756
+ appliedRules
757
+ };
790
758
  }
791
759
  /**
792
760
  * Apply replacements to a chat completions payload
793
761
  * This modifies message content in place
794
762
  */
795
763
  async function applyReplacementsToPayload(payload) {
764
+ const allAppliedRules = [];
796
765
  const processedMessages = await Promise.all(payload.messages.map(async (message) => {
797
- if (typeof message.content === "string") return {
798
- ...message,
799
- content: await applyReplacements(message.content)
800
- };
766
+ if (typeof message.content === "string") {
767
+ const { text, appliedRules } = await applyReplacements(message.content);
768
+ allAppliedRules.push(...appliedRules);
769
+ return {
770
+ ...message,
771
+ content: text
772
+ };
773
+ }
801
774
  if (Array.isArray(message.content)) return {
802
775
  ...message,
803
776
  content: await Promise.all(message.content.map(async (part) => {
804
- if (typeof part === "object" && part.type === "text" && part.text) return {
805
- ...part,
806
- text: await applyReplacements(part.text)
807
- };
777
+ if (typeof part === "object" && part.type === "text" && part.text) {
778
+ const { text, appliedRules } = await applyReplacements(part.text);
779
+ allAppliedRules.push(...appliedRules);
780
+ return {
781
+ ...part,
782
+ text
783
+ };
784
+ }
808
785
  return part;
809
786
  }))
810
787
  };
811
788
  return message;
812
789
  }));
790
+ const uniqueRules = [...new Set(allAppliedRules)];
813
791
  return {
814
- ...payload,
815
- messages: processedMessages
792
+ payload: {
793
+ ...payload,
794
+ messages: processedMessages
795
+ },
796
+ appliedRules: uniqueRules
816
797
  };
817
798
  }
818
799
 
@@ -820,11 +801,20 @@ async function applyReplacementsToPayload(payload) {
820
801
  //#region src/config.ts
821
802
  function formatRule(rule, index) {
822
803
  const status = rule.enabled ? "✓" : "✗";
823
- const type = rule.isRegex ? "regex" : "string";
804
+ const type$1 = rule.isRegex ? "regex" : "string";
824
805
  const system = rule.isSystem ? " [system]" : "";
825
- const name = rule.name ? ` "${rule.name}"` : "";
806
+ const name$1 = rule.name ? ` "${rule.name}"` : "";
826
807
  const replacement = rule.replacement || "(empty)";
827
- return `${index + 1}. [${status}] (${type})${system}${name} "${rule.pattern}" → "${replacement}"`;
808
+ return `${index + 1}. [${status}] (${type$1})${system}${name$1} "${rule.pattern}" → "${replacement}"`;
809
+ }
810
+ function isValidPatternForMatchType(pattern, matchType) {
811
+ if (matchType !== "regex") return true;
812
+ try {
813
+ new RegExp(pattern);
814
+ return true;
815
+ } catch {
816
+ return false;
817
+ }
828
818
  }
829
819
  async function listReplacements() {
830
820
  const all = await getAllReplacements();
@@ -837,11 +827,11 @@ async function listReplacements() {
837
827
  console.log();
838
828
  }
839
829
  async function addNewReplacement() {
840
- const name = await consola.prompt("Name (optional, short description):", {
830
+ const name$1 = await consola.prompt("Name (optional, short description):", {
841
831
  type: "text",
842
832
  default: ""
843
833
  });
844
- if (typeof name === "symbol") {
834
+ if (typeof name$1 === "symbol") {
845
835
  consola.info("Cancelled.");
846
836
  return;
847
837
  }
@@ -864,9 +854,7 @@ async function addNewReplacement() {
864
854
  consola.info("Cancelled.");
865
855
  return;
866
856
  }
867
- if (matchType === "regex") try {
868
- new RegExp(pattern);
869
- } catch {
857
+ if (!isValidPatternForMatchType(pattern, matchType)) {
870
858
  consola.error(`Invalid regex pattern: ${pattern}`);
871
859
  return;
872
860
  }
@@ -878,7 +866,10 @@ async function addNewReplacement() {
878
866
  consola.info("Cancelled.");
879
867
  return;
880
868
  }
881
- const rule = await addReplacement(pattern, replacement, matchType === "regex", name || void 0);
869
+ const rule = await addReplacement(pattern, replacement, {
870
+ isRegex: matchType === "regex",
871
+ name: name$1 || void 0
872
+ });
882
873
  consola.success(`Added rule: ${rule.name || rule.id}`);
883
874
  }
884
875
  async function editExistingReplacement() {
@@ -906,11 +897,11 @@ async function editExistingReplacement() {
906
897
  }
907
898
  consola.info(`\nEditing rule: ${rule.name || rule.id}`);
908
899
  consola.info("Press Enter to keep current value.\n");
909
- const name = await consola.prompt("Name:", {
900
+ const name$1 = await consola.prompt("Name:", {
910
901
  type: "text",
911
902
  default: rule.name || ""
912
903
  });
913
- if (typeof name === "symbol") {
904
+ if (typeof name$1 === "symbol") {
914
905
  consola.info("Cancelled.");
915
906
  return;
916
907
  }
@@ -937,9 +928,7 @@ async function editExistingReplacement() {
937
928
  consola.info("Cancelled.");
938
929
  return;
939
930
  }
940
- if (matchType === "regex") try {
941
- new RegExp(pattern);
942
- } catch {
931
+ if (!isValidPatternForMatchType(pattern, matchType)) {
943
932
  consola.error(`Invalid regex pattern: ${pattern}`);
944
933
  return;
945
934
  }
@@ -952,7 +941,7 @@ async function editExistingReplacement() {
952
941
  return;
953
942
  }
954
943
  const updated = await updateReplacement(selected, {
955
- name: name || void 0,
944
+ name: name$1 || void 0,
956
945
  pattern,
957
946
  replacement,
958
947
  isRegex: matchType === "regex"
@@ -1009,7 +998,7 @@ async function testReplacements() {
1009
998
  consola.info("Cancelled.");
1010
999
  return;
1011
1000
  }
1012
- const result = await applyReplacements(testText);
1001
+ const { text: result } = await applyReplacements(testText);
1013
1002
  consola.info("\n📝 Original:");
1014
1003
  console.log(testText);
1015
1004
  consola.info("\n✨ After replacements:");
@@ -1269,7 +1258,8 @@ function mergeDefaultExtraPrompts(config$1) {
1269
1258
  };
1270
1259
  }
1271
1260
  function mergeConfigWithDefaults() {
1272
- const { mergedConfig, changed } = mergeDefaultExtraPrompts(readConfigFromDisk());
1261
+ const config$1 = readConfigFromDisk();
1262
+ const { mergedConfig, changed } = mergeDefaultExtraPrompts(config$1);
1273
1263
  if (changed) try {
1274
1264
  fs$1.writeFileSync(PATHS.CONFIG_PATH, `${JSON.stringify(mergedConfig, null, 2)}\n`, "utf8");
1275
1265
  } catch (writeError) {
@@ -1285,14 +1275,168 @@ function getConfig() {
1285
1275
  function getExtraPromptForModel(model) {
1286
1276
  return getConfig().extraPrompts?.[model] ?? "";
1287
1277
  }
1288
- function getSmallModel() {
1289
- return getConfig().smallModel ?? "gpt-5-mini";
1290
- }
1291
- function getReasoningEffortForModel(model) {
1278
+ function getReasoningEffortForModel(model, override) {
1279
+ if (override) return override;
1292
1280
  return getConfig().modelReasoningEfforts?.[model] ?? "high";
1293
1281
  }
1294
- function shouldCompactUseSmallModel() {
1295
- return getConfig().compactUseSmallModel ?? true;
1282
+
1283
+ //#endregion
1284
+ //#region src/lib/model-suffix.ts
1285
+ /**
1286
+ * Hardcoded reasoning config per model, derived from Copilot CLI v0.0.414.
1287
+ * Models not in this map do not support per-request reasoning effort control.
1288
+ */
1289
+ const MODEL_REASONING_CONFIG = {
1290
+ "claude-sonnet-4.6": {
1291
+ supportedEfforts: [
1292
+ "low",
1293
+ "medium",
1294
+ "high"
1295
+ ],
1296
+ defaultEffort: "medium"
1297
+ },
1298
+ "claude-opus-4.6": {
1299
+ supportedEfforts: [
1300
+ "low",
1301
+ "medium",
1302
+ "high"
1303
+ ],
1304
+ defaultEffort: "high"
1305
+ },
1306
+ "claude-opus-4.6-fast": {
1307
+ supportedEfforts: [
1308
+ "low",
1309
+ "medium",
1310
+ "high"
1311
+ ],
1312
+ defaultEffort: "high"
1313
+ },
1314
+ "claude-opus-4.6-1m": {
1315
+ supportedEfforts: [
1316
+ "low",
1317
+ "medium",
1318
+ "high"
1319
+ ],
1320
+ defaultEffort: "high"
1321
+ },
1322
+ "gpt-5.3-codex": {
1323
+ supportedEfforts: [
1324
+ "low",
1325
+ "medium",
1326
+ "high",
1327
+ "xhigh"
1328
+ ],
1329
+ defaultEffort: "medium"
1330
+ },
1331
+ "gpt-5.2-codex": {
1332
+ supportedEfforts: [
1333
+ "low",
1334
+ "medium",
1335
+ "high",
1336
+ "xhigh"
1337
+ ],
1338
+ defaultEffort: "high"
1339
+ },
1340
+ "gpt-5.2": {
1341
+ supportedEfforts: [
1342
+ "low",
1343
+ "medium",
1344
+ "high"
1345
+ ],
1346
+ defaultEffort: "medium"
1347
+ },
1348
+ "gpt-5.1-codex": {
1349
+ supportedEfforts: [
1350
+ "low",
1351
+ "medium",
1352
+ "high"
1353
+ ],
1354
+ defaultEffort: "medium"
1355
+ },
1356
+ "gpt-5.1-codex-max": {
1357
+ supportedEfforts: [
1358
+ "low",
1359
+ "medium",
1360
+ "high"
1361
+ ],
1362
+ defaultEffort: "medium"
1363
+ },
1364
+ "gpt-5.1": {
1365
+ supportedEfforts: [
1366
+ "low",
1367
+ "medium",
1368
+ "high"
1369
+ ],
1370
+ defaultEffort: "medium"
1371
+ },
1372
+ "gpt-5.1-codex-mini": {
1373
+ supportedEfforts: [
1374
+ "low",
1375
+ "medium",
1376
+ "high"
1377
+ ],
1378
+ defaultEffort: "medium"
1379
+ },
1380
+ "gpt-5-mini": {
1381
+ supportedEfforts: [
1382
+ "low",
1383
+ "medium",
1384
+ "high"
1385
+ ],
1386
+ defaultEffort: "medium"
1387
+ }
1388
+ };
1389
+ const VALID_EFFORTS = new Set([
1390
+ "low",
1391
+ "medium",
1392
+ "high",
1393
+ "xhigh"
1394
+ ]);
1395
+ /**
1396
+ * Parse a model string that may contain a reasoning effort suffix.
1397
+ * Format: "model-name:effort" (e.g. "claude-sonnet-4.6:high")
1398
+ *
1399
+ * If the suffix is not a valid effort level or the model doesn't support it,
1400
+ * the suffix is ignored and the full string is treated as the model name.
1401
+ */
1402
+ function parseModelSuffix(model) {
1403
+ const colonIndex = model.lastIndexOf(":");
1404
+ if (colonIndex === -1) return { baseModel: model };
1405
+ const potentialBase = model.slice(0, colonIndex);
1406
+ const potentialEffort = model.slice(colonIndex + 1);
1407
+ if (!VALID_EFFORTS.has(potentialEffort)) return { baseModel: model };
1408
+ const effort = potentialEffort;
1409
+ const config$1 = MODEL_REASONING_CONFIG[potentialBase];
1410
+ if (!config$1) return { baseModel: model };
1411
+ if (!config$1.supportedEfforts.includes(effort)) return {
1412
+ baseModel: potentialBase,
1413
+ reasoningEffort: config$1.defaultEffort
1414
+ };
1415
+ return {
1416
+ baseModel: potentialBase,
1417
+ reasoningEffort: effort
1418
+ };
1419
+ }
1420
+ /**
1421
+ * Generate virtual model entries for models that support reasoning effort.
1422
+ * Each supported effort level gets its own virtual model entry.
1423
+ */
1424
+ function generateVirtualModels(models) {
1425
+ const virtualModels = [];
1426
+ for (const model of models) {
1427
+ const config$1 = MODEL_REASONING_CONFIG[model.id];
1428
+ if (!config$1) continue;
1429
+ for (const effort of config$1.supportedEfforts) virtualModels.push({
1430
+ id: `${model.id}:${effort}`,
1431
+ object: "model",
1432
+ type: "model",
1433
+ created: 0,
1434
+ created_at: (/* @__PURE__ */ new Date(0)).toISOString(),
1435
+ owned_by: model.vendor,
1436
+ display_name: `${model.name} (${effort} thinking)`
1437
+ });
1438
+ }
1439
+ return virtualModels;
1296
1440
  }
1297
1441
 
1298
1442
  //#endregion
@@ -1347,7 +1491,8 @@ function getShell() {
1347
1491
  const { platform, ppid, env } = process$1;
1348
1492
  if (platform === "win32") {
1349
1493
  try {
1350
- if (execSync(`wmic process get ParentProcessId,Name | findstr "${ppid}"`, { stdio: "pipe" }).toString().toLowerCase().includes("powershell.exe")) return "powershell";
1494
+ const command = `wmic process get ParentProcessId,Name | findstr "${ppid}"`;
1495
+ if (execSync(command, { stdio: "pipe" }).toString().toLowerCase().includes("powershell.exe")) return "powershell";
1351
1496
  } catch {
1352
1497
  return "cmd";
1353
1498
  }
@@ -1405,7 +1550,8 @@ function normalizeApiKeys(apiKeys) {
1405
1550
  return [...new Set(normalizedKeys)];
1406
1551
  }
1407
1552
  function getConfiguredApiKeys() {
1408
- return normalizeApiKeys(getConfig().auth?.apiKeys);
1553
+ const config$1 = getConfig();
1554
+ return normalizeApiKeys(config$1.auth?.apiKeys);
1409
1555
  }
1410
1556
  function extractRequestApiKey(c) {
1411
1557
  const xApiKey = c.req.header("x-api-key")?.trim();
@@ -1438,6 +1584,26 @@ function createAuthMiddleware(options = {}) {
1438
1584
  };
1439
1585
  }
1440
1586
 
1587
+ //#endregion
1588
+ //#region src/lib/api-key-guard.ts
1589
+ /**
1590
+ * API key guard middleware that silently drops connections when the API key
1591
+ * doesn't match the expected value. Unauthorized requests get NO response.
1592
+ *
1593
+ * Only active when state.apiKeyAuth is set (via --api-key-auth CLI flag).
1594
+ */
1595
+ async function apiKeyGuard(c, next) {
1596
+ if (!state.apiKeyAuth) {
1597
+ await next();
1598
+ return;
1599
+ }
1600
+ if (extractRequestApiKey(c) === state.apiKeyAuth) {
1601
+ await next();
1602
+ return;
1603
+ }
1604
+ await new Promise(() => {});
1605
+ }
1606
+
1441
1607
  //#endregion
1442
1608
  //#region src/lib/request-logger.ts
1443
1609
  const REQUEST_CONTEXT_KEY = "requestContext";
@@ -1498,7 +1664,8 @@ async function logRawRequest(c) {
1498
1664
  if (method !== "GET" && method !== "HEAD") try {
1499
1665
  const body = await c.req.raw.clone().text();
1500
1666
  if (body) try {
1501
- const sanitized = sanitizeRequestBody(JSON.parse(body));
1667
+ const parsed = JSON.parse(body);
1668
+ const sanitized = sanitizeRequestBody(parsed);
1502
1669
  lines.push(`${colors.dim}Body (sanitized):${colors.reset}`, ` ${JSON.stringify(sanitized, null, 2).split("\n").join("\n ")}`);
1503
1670
  } catch {
1504
1671
  lines.push(`${colors.dim}Body:${colors.reset} [${body.length} bytes]`);
@@ -1520,6 +1687,34 @@ function setRequestContext(c, ctx) {
1520
1687
  });
1521
1688
  }
1522
1689
  /**
1690
+ * Format the input size for display
1691
+ */
1692
+ function formatInputSize(bytes) {
1693
+ return bytes >= 1024 ? `${(bytes / 1024).toFixed(1)}KB` : `${bytes}B`;
1694
+ }
1695
+ /**
1696
+ * Build the model routing log line
1697
+ */
1698
+ function buildModelLine(ctx) {
1699
+ const parts = [];
1700
+ if (ctx.requestedModel && ctx.requestedModel !== ctx.model) parts.push(`${colors.gray}${ctx.requestedModel}${colors.reset} ${colors.dim}→${colors.reset} ${colors.white}${ctx.model}${colors.reset}`);
1701
+ else parts.push(`${colors.white}${ctx.model}${colors.reset}`);
1702
+ if (ctx.provider) parts.push(`${colors.dim}via${colors.reset} ${colors.magenta}${ctx.provider}${colors.reset}`);
1703
+ if (ctx.inputLength !== void 0) parts.push(`${colors.dim}·${colors.reset} ${colors.yellow}${formatInputSize(ctx.inputLength)}${colors.reset}`);
1704
+ return ` ${parts.join(" ")}`;
1705
+ }
1706
+ /**
1707
+ * Build the modifications log line (effort, replacements, tokens)
1708
+ */
1709
+ function buildModificationsLine(ctx) {
1710
+ const modParts = [];
1711
+ if (ctx.reasoningEffort) modParts.push(`${colors.blue}effort=${ctx.reasoningEffort}${colors.reset}`);
1712
+ if (ctx.replacements && ctx.replacements.length > 0) modParts.push(`${colors.green}replace: ${ctx.replacements.join(", ")}${colors.reset}`);
1713
+ if (ctx.inputTokens !== void 0) modParts.push(`${colors.yellow}${ctx.inputTokens.toLocaleString()} tokens${colors.reset}`);
1714
+ if (modParts.length === 0) return void 0;
1715
+ return ` ${modParts.join(` ${colors.dim}·${colors.reset} `)}`;
1716
+ }
1717
+ /**
1523
1718
  * Custom request logger middleware
1524
1719
  */
1525
1720
  async function requestLogger(c, next) {
@@ -1527,7 +1722,11 @@ async function requestLogger(c, next) {
1527
1722
  const startTime = Date.now();
1528
1723
  const method = c.req.method;
1529
1724
  const path$1 = c.req.path + (c.req.raw.url.includes("?") ? "?" + c.req.raw.url.split("?")[1] : "");
1530
- c.set(REQUEST_CONTEXT_KEY, { startTime });
1725
+ const contentLength = c.req.header("content-length");
1726
+ c.set(REQUEST_CONTEXT_KEY, {
1727
+ startTime,
1728
+ inputLength: contentLength ? Number(contentLength) : void 0
1729
+ });
1531
1730
  await next();
1532
1731
  const ctx = c.get(REQUEST_CONTEXT_KEY);
1533
1732
  const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
@@ -1538,15 +1737,10 @@ async function requestLogger(c, next) {
1538
1737
  const statusBadge = `${statusColor}${status}${colors.reset}`;
1539
1738
  const durationStr = `${colors.cyan}${duration}s${colors.reset}`;
1540
1739
  lines.push(`${colors.bold}${method}${colors.reset} ${path$1} ${statusBadge} ${durationStr}`);
1541
- if (ctx?.provider && ctx.model) {
1542
- const providerColor = ctx.provider === "Azure OpenAI" ? colors.blue : colors.magenta;
1543
- lines.push(` ${colors.gray}Provider:${colors.reset} ${providerColor}${ctx.provider}${colors.reset} ${colors.gray}->${colors.reset} ${colors.white}${ctx.model}${colors.reset}`);
1544
- }
1545
- if (ctx?.inputTokens !== void 0 || ctx?.outputTokens !== void 0) {
1546
- const tokenParts = [];
1547
- if (ctx.inputTokens !== void 0) tokenParts.push(`${colors.gray}Input:${colors.reset} ${colors.yellow}${ctx.inputTokens.toLocaleString()}${colors.reset}`);
1548
- if (ctx.outputTokens !== void 0) tokenParts.push(`${colors.gray}Output:${colors.reset} ${colors.green}${ctx.outputTokens.toLocaleString()}${colors.reset}`);
1549
- lines.push(` ${tokenParts.join(" ")}`);
1740
+ if (ctx?.model) lines.push(buildModelLine(ctx));
1741
+ if (ctx) {
1742
+ const modsLine = buildModificationsLine(ctx);
1743
+ if (modsLine) lines.push(modsLine);
1550
1744
  }
1551
1745
  lines.push(` ${colors.dim}${getTimeString()}${colors.reset}`);
1552
1746
  console.log(lines.join("\n"));
@@ -1561,13 +1755,17 @@ const awaitApproval = async () => {
1561
1755
  //#endregion
1562
1756
  //#region src/lib/model-resolver.ts
1563
1757
  /**
1564
- * Normalize a model name by converting dashes to dots between numbers.
1758
+ * Normalize a model name by converting dashes to dots between numbers
1759
+ * and converting Anthropic's [1m] suffix to Copilot's -1m suffix.
1565
1760
  * e.g., "claude-opus-4-5" -> "claude-opus-4.5"
1761
+ * "claude-opus-4-6[1m]" -> "claude-opus-4.6-1m"
1566
1762
  * "gpt-4-1" -> "gpt-4.1"
1567
1763
  * "gpt-5-1-codex" -> "gpt-5.1-codex"
1568
1764
  */
1569
1765
  function normalizeModelName(model) {
1570
- return model.replaceAll(/(\d)-(\d)/g, (_, p1, p2) => `${p1}.${p2}`);
1766
+ let normalized = model.replace("[1m]", "-1m");
1767
+ normalized = normalized.replaceAll(/(\d)-(\d)/g, (_, p1, p2) => `${p1}.${p2}`);
1768
+ return normalized;
1571
1769
  }
1572
1770
 
1573
1771
  //#endregion
@@ -1778,7 +1976,8 @@ const numTokensForTools = (tools, encoder, constants) => {
1778
1976
  * Calculate the token count of messages, supporting multiple GPT encoders
1779
1977
  */
1780
1978
  const getTokenCount = async (payload, model) => {
1781
- const encoder = await getEncodeChatFunction(getTokenizerFromModel(model));
1979
+ const tokenizer = getTokenizerFromModel(model);
1980
+ const encoder = await getEncodeChatFunction(tokenizer);
1782
1981
  const simplifiedMessages = payload.messages;
1783
1982
  const inputMessages = simplifiedMessages.filter((msg) => msg.role !== "assistant");
1784
1983
  const outputMessages = simplifiedMessages.filter((msg) => msg.role === "assistant");
@@ -1823,50 +2022,29 @@ const createChatCompletions = async (payload, options) => {
1823
2022
  //#region src/routes/chat-completions/handler.ts
1824
2023
  async function handleCompletion$1(c) {
1825
2024
  await checkRateLimit(state);
1826
- let payload = await applyReplacementsToPayload(await c.req.json());
1827
- payload = {
1828
- ...payload,
1829
- model: normalizeModelName(payload.model)
2025
+ const rawPayload = await c.req.json();
2026
+ const requestedModel = rawPayload.model;
2027
+ const { baseModel, reasoningEffort } = parseModelSuffix(rawPayload.model);
2028
+ rawPayload.model = baseModel;
2029
+ const { payload: replacedPayload, appliedRules } = await applyReplacementsToPayload(rawPayload);
2030
+ let payload = {
2031
+ ...replacedPayload,
2032
+ model: normalizeModelName(replacedPayload.model)
1830
2033
  };
1831
2034
  consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
1832
- if (isAzureOpenAIModel(payload.model)) {
1833
- if (!state.azureOpenAIConfig) return c.json({ error: "Azure OpenAI not configured" }, 500);
1834
- setRequestContext(c, {
1835
- provider: "Azure OpenAI",
1836
- model: payload.model
1837
- });
1838
- if (state.manualApprove) await awaitApproval();
1839
- const response$1 = await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, payload);
1840
- if (isNonStreaming$1(response$1)) {
1841
- consola.debug("Non-streaming response:", JSON.stringify(response$1));
1842
- if (response$1.usage) setRequestContext(c, {
1843
- inputTokens: response$1.usage.prompt_tokens,
1844
- outputTokens: response$1.usage.completion_tokens
1845
- });
1846
- return c.json(response$1);
1847
- }
1848
- consola.debug("Streaming response");
1849
- return streamSSE(c, async (stream) => {
1850
- for await (const chunk of response$1) {
1851
- consola.debug("Streaming chunk:", JSON.stringify(chunk));
1852
- if (chunk.data && chunk.data !== "[DONE]") {
1853
- const parsed = JSON.parse(chunk.data);
1854
- if (parsed.usage) setRequestContext(c, {
1855
- inputTokens: parsed.usage.prompt_tokens,
1856
- outputTokens: parsed.usage.completion_tokens
1857
- });
1858
- }
1859
- await stream.writeSSE(chunk);
1860
- }
1861
- });
1862
- }
1863
2035
  setRequestContext(c, {
1864
- provider: "Copilot",
1865
- model: payload.model
2036
+ requestedModel,
2037
+ provider: "ChatCompletions",
2038
+ model: payload.model,
2039
+ replacements: appliedRules,
2040
+ reasoningEffort
1866
2041
  });
1867
2042
  const selectedModel = state.models?.data.find((model) => model.id === payload.model);
1868
2043
  try {
1869
- if (selectedModel) setRequestContext(c, { inputTokens: (await getTokenCount(payload, selectedModel)).input });
2044
+ if (selectedModel) {
2045
+ const tokenCount = await getTokenCount(payload, selectedModel);
2046
+ setRequestContext(c, { inputTokens: tokenCount.input });
2047
+ }
1870
2048
  } catch (error) {
1871
2049
  consola.warn("Failed to calculate token count:", error);
1872
2050
  }
@@ -1933,7 +2111,8 @@ const createEmbeddings = async (payload) => {
1933
2111
  const embeddingRoutes = new Hono();
1934
2112
  embeddingRoutes.post("/", async (c) => {
1935
2113
  try {
1936
- const response = await createEmbeddings(await c.req.json());
2114
+ const paylod = await c.req.json();
2115
+ const response = await createEmbeddings(paylod);
1937
2116
  return c.json(response);
1938
2117
  } catch (error) {
1939
2118
  return await forwardError(c, error);
@@ -2219,8 +2398,8 @@ const formatArgs = (args) => args.map((arg) => typeof arg === "string" ? arg : u
2219
2398
  depth: null,
2220
2399
  colors: false
2221
2400
  })).join(" ");
2222
- const sanitizeName = (name) => {
2223
- const normalized = name.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
2401
+ const sanitizeName = (name$1) => {
2402
+ const normalized = name$1.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
2224
2403
  return normalized === "" ? "handler" : normalized;
2225
2404
  };
2226
2405
  const getLogStream = (filePath) => {
@@ -2274,10 +2453,10 @@ process.on("SIGTERM", () => {
2274
2453
  process.exit(0);
2275
2454
  });
2276
2455
  let lastCleanup = 0;
2277
- const createHandlerLogger = (name) => {
2456
+ const createHandlerLogger = (name$1) => {
2278
2457
  ensureLogDirectory();
2279
- const sanitizedName = sanitizeName(name);
2280
- const instance = consola.withTag(name);
2458
+ const sanitizedName = sanitizeName(name$1);
2459
+ const instance = consola.withTag(name$1);
2281
2460
  if (state.verbose) instance.level = 5;
2282
2461
  instance.setReporters([]);
2283
2462
  instance.addReporter({ log(logObj) {
@@ -2291,7 +2470,8 @@ const createHandlerLogger = (name) => {
2291
2470
  const timestamp = date.toLocaleString("sv-SE", { hour12: false });
2292
2471
  const filePath = path.join(LOG_DIR, `${sanitizedName}-${dateKey}.log`);
2293
2472
  const message = formatArgs(logObj.args);
2294
- appendLine(filePath, `[${timestamp}] [${logObj.type}] [${logObj.tag || name}]${message ? ` ${message}` : ""}`);
2473
+ const line = `[${timestamp}] [${logObj.type}] [${logObj.tag || name$1}]${message ? ` ${message}` : ""}`;
2474
+ appendLine(filePath, line);
2295
2475
  } });
2296
2476
  return instance;
2297
2477
  };
@@ -2323,7 +2503,7 @@ const createResponses = async (payload, { vision, initiator }) => {
2323
2503
  const MESSAGE_TYPE = "message";
2324
2504
  const CODEX_PHASE_MODEL = "gpt-5.3-codex";
2325
2505
  const THINKING_TEXT = "Thinking...";
2326
- const translateAnthropicMessagesToResponsesPayload = (payload) => {
2506
+ const translateAnthropicMessagesToResponsesPayload = (payload, effortOverride) => {
2327
2507
  const input = [];
2328
2508
  for (const message of payload.messages) input.push(...translateMessage(message, payload.model));
2329
2509
  const translatedTools = convertAnthropicTools(payload.tools);
@@ -2345,7 +2525,7 @@ const translateAnthropicMessagesToResponsesPayload = (payload) => {
2345
2525
  store: false,
2346
2526
  parallel_tool_calls: true,
2347
2527
  reasoning: {
2348
- effort: getReasoningEffortForModel(payload.model),
2528
+ effort: getReasoningEffortForModel(payload.model, effortOverride),
2349
2529
  summary: "detailed"
2350
2530
  },
2351
2531
  include: ["reasoning.encrypted_content"]
@@ -2646,8 +2826,9 @@ const mapResponsesStopReason = (response) => {
2646
2826
  const mapResponsesUsage = (response) => {
2647
2827
  const inputTokens = response.usage?.input_tokens ?? 0;
2648
2828
  const outputTokens = response.usage?.output_tokens ?? 0;
2829
+ const inputCachedTokens = response.usage?.input_tokens_details?.cached_tokens;
2649
2830
  return {
2650
- input_tokens: inputTokens - (response.usage?.input_tokens_details?.cached_tokens ?? 0),
2831
+ input_tokens: inputTokens - (inputCachedTokens ?? 0),
2651
2832
  output_tokens: outputTokens,
2652
2833
  ...response.usage?.input_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: response.usage.input_tokens_details.cached_tokens }
2653
2834
  };
@@ -2663,9 +2844,10 @@ const parseUserId = (userId) => {
2663
2844
  const userMatch = userId.match(/user_([^_]+)_account/);
2664
2845
  const safetyIdentifier = userMatch ? userMatch[1] : null;
2665
2846
  const sessionMatch = userId.match(/_session_(.+)$/);
2847
+ const promptCacheKey = sessionMatch ? sessionMatch[1] : null;
2666
2848
  return {
2667
2849
  safetyIdentifier,
2668
- promptCacheKey: sessionMatch ? sessionMatch[1] : null
2850
+ promptCacheKey
2669
2851
  };
2670
2852
  };
2671
2853
  const convertToolResultContent = (content) => {
@@ -2747,11 +2929,11 @@ const handleOutputItemAdded$1 = (rawEvent, state$1) => {
2747
2929
  const events$1 = new Array();
2748
2930
  const functionCallDetails = extractFunctionCallDetails(rawEvent);
2749
2931
  if (!functionCallDetails) return events$1;
2750
- const { outputIndex, toolCallId, name, initialArguments } = functionCallDetails;
2932
+ const { outputIndex, toolCallId, name: name$1, initialArguments } = functionCallDetails;
2751
2933
  const blockIndex = openFunctionCallBlock(state$1, {
2752
2934
  outputIndex,
2753
2935
  toolCallId,
2754
- name,
2936
+ name: name$1,
2755
2937
  events: events$1
2756
2938
  });
2757
2939
  if (initialArguments !== void 0 && initialArguments.length > 0) {
@@ -3044,15 +3226,16 @@ const buildErrorEvent = (message) => ({
3044
3226
  });
3045
3227
  const getBlockKey = (outputIndex, contentIndex) => `${outputIndex}:${contentIndex}`;
3046
3228
  const openFunctionCallBlock = (state$1, params) => {
3047
- const { outputIndex, toolCallId, name, events: events$1 } = params;
3229
+ const { outputIndex, toolCallId, name: name$1, events: events$1 } = params;
3048
3230
  let functionCallState = state$1.functionCallStateByOutputIndex.get(outputIndex);
3049
3231
  if (!functionCallState) {
3050
3232
  const blockIndex$1 = state$1.nextContentBlockIndex;
3051
3233
  state$1.nextContentBlockIndex += 1;
3234
+ const resolvedToolCallId = toolCallId ?? `tool_call_${blockIndex$1}`;
3052
3235
  functionCallState = {
3053
3236
  blockIndex: blockIndex$1,
3054
- toolCallId: toolCallId ?? `tool_call_${blockIndex$1}`,
3055
- name: name ?? "function",
3237
+ toolCallId: resolvedToolCallId,
3238
+ name: name$1 ?? "function",
3056
3239
  consecutiveWhitespaceCount: 0
3057
3240
  };
3058
3241
  state$1.functionCallStateByOutputIndex.set(outputIndex, functionCallState);
@@ -3077,20 +3260,26 @@ const openFunctionCallBlock = (state$1, params) => {
3077
3260
  const extractFunctionCallDetails = (rawEvent) => {
3078
3261
  const item = rawEvent.item;
3079
3262
  if (item.type !== "function_call") return;
3263
+ const outputIndex = rawEvent.output_index;
3264
+ const toolCallId = item.call_id;
3265
+ const name$1 = item.name;
3266
+ const initialArguments = item.arguments;
3080
3267
  return {
3081
- outputIndex: rawEvent.output_index,
3082
- toolCallId: item.call_id,
3083
- name: item.name,
3084
- initialArguments: item.arguments
3268
+ outputIndex,
3269
+ toolCallId,
3270
+ name: name$1,
3271
+ initialArguments
3085
3272
  };
3086
3273
  };
3087
3274
 
3088
3275
  //#endregion
3089
3276
  //#region src/routes/responses/utils.ts
3090
3277
  const getResponsesRequestOptions = (payload) => {
3278
+ const vision = hasVisionInput(payload);
3279
+ const initiator = hasAgentInitiator(payload) ? "agent" : "user";
3091
3280
  return {
3092
- vision: hasVisionInput(payload),
3093
- initiator: hasAgentInitiator(payload) ? "agent" : "user"
3281
+ vision,
3282
+ initiator
3094
3283
  };
3095
3284
  };
3096
3285
  const hasAgentInitiator = (payload) => {
@@ -3132,7 +3321,8 @@ const createMessages = async (payload, anthropicBetaHeader, options) => {
3132
3321
  "X-Initiator": initiator
3133
3322
  };
3134
3323
  if (anthropicBetaHeader) {
3135
- const filteredBeta = anthropicBetaHeader.split(",").map((item) => item.trim()).filter((item) => item !== "claude-code-20250219").join(",");
3324
+ const unsupportedBetas = new Set(["claude-code-20250219", "context-1m-2025-08-07"]);
3325
+ const filteredBeta = anthropicBetaHeader.split(",").map((item) => item.trim()).filter((item) => !unsupportedBetas.has(item)).join(",");
3136
3326
  if (filteredBeta) headers["anthropic-beta"] = filteredBeta;
3137
3327
  } else if (payload.thinking?.budget_tokens) headers["anthropic-beta"] = "interleaved-thinking-2025-05-14";
3138
3328
  const response = await fetch(`${copilotBaseUrl(state)}/v1/messages`, {
@@ -3357,38 +3547,62 @@ async function handleCompletion(c) {
3357
3547
  await checkRateLimit(state);
3358
3548
  const anthropicPayload = await c.req.json();
3359
3549
  logger$1.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
3550
+ const requestedModel = anthropicPayload.model;
3551
+ const { baseModel, reasoningEffort: suffixEffort } = parseModelSuffix(anthropicPayload.model);
3552
+ anthropicPayload.model = normalizeModelName(baseModel);
3360
3553
  const subagentMarker = parseSubagentMarkerFromFirstUser(anthropicPayload);
3361
3554
  const initiatorOverride = subagentMarker ? "agent" : void 0;
3362
3555
  if (subagentMarker) logger$1.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
3363
3556
  const isCompact = isCompactRequest(anthropicPayload);
3364
3557
  const anthropicBeta = c.req.header("anthropic-beta");
3365
3558
  logger$1.debug("Anthropic Beta header:", anthropicBeta);
3366
- const noTools = !anthropicPayload.tools || anthropicPayload.tools.length === 0;
3367
- if (anthropicBeta && noTools && !isCompact) anthropicPayload.model = getSmallModel();
3368
- if (isCompact) {
3369
- logger$1.debug("Is compact request:", isCompact);
3370
- if (shouldCompactUseSmallModel()) anthropicPayload.model = getSmallModel();
3371
- } else mergeToolResultForClaude(anthropicPayload);
3559
+ applyModelVariantRouting(anthropicPayload, anthropicBeta);
3560
+ if (isCompact) logger$1.debug("Is compact request:", isCompact);
3561
+ else mergeToolResultForClaude(anthropicPayload);
3372
3562
  if (state.manualApprove) await awaitApproval();
3373
3563
  const selectedModel = state.models?.data.find((m) => m.id === anthropicPayload.model);
3564
+ let apiType = "ChatCompletions";
3565
+ if (shouldUseMessagesApi(selectedModel)) apiType = "Messages";
3566
+ else if (shouldUseResponsesApi(selectedModel)) apiType = "Responses";
3567
+ const bodyEffort = getBodyReasoningEffort(anthropicPayload);
3568
+ const effectiveEffort = suffixEffort ?? bodyEffort;
3569
+ setRequestContext(c, {
3570
+ requestedModel,
3571
+ model: anthropicPayload.model,
3572
+ provider: apiType,
3573
+ reasoningEffort: effectiveEffort
3574
+ });
3374
3575
  if (shouldUseMessagesApi(selectedModel)) return await handleWithMessagesApi(c, anthropicPayload, {
3375
3576
  anthropicBetaHeader: anthropicBeta,
3376
3577
  initiatorOverride,
3377
- selectedModel
3578
+ selectedModel,
3579
+ effortOverride: suffixEffort
3580
+ });
3581
+ if (shouldUseResponsesApi(selectedModel)) return await handleWithResponsesApi(c, anthropicPayload, {
3582
+ initiatorOverride,
3583
+ effortOverride: suffixEffort
3378
3584
  });
3379
- if (shouldUseResponsesApi(selectedModel)) return await handleWithResponsesApi(c, anthropicPayload, initiatorOverride);
3380
3585
  return await handleWithChatCompletions(c, anthropicPayload, initiatorOverride);
3381
3586
  }
3382
3587
  const RESPONSES_ENDPOINT$1 = "/responses";
3383
3588
  const MESSAGES_ENDPOINT = "/v1/messages";
3384
3589
  const handleWithChatCompletions = async (c, anthropicPayload, initiatorOverride) => {
3385
- let finalPayload = await applyReplacementsToPayload(translateToOpenAI(anthropicPayload));
3386
- finalPayload = {
3387
- ...finalPayload,
3388
- model: normalizeModelName(finalPayload.model)
3590
+ const openAIPayload = translateToOpenAI(anthropicPayload);
3591
+ const { payload: replacedPayload, appliedRules } = await applyReplacementsToPayload(openAIPayload);
3592
+ const finalPayload = {
3593
+ ...replacedPayload,
3594
+ model: normalizeModelName(replacedPayload.model)
3389
3595
  };
3596
+ if (appliedRules.length > 0) setRequestContext(c, { replacements: appliedRules });
3597
+ try {
3598
+ const selectedModel = state.models?.data.find((m) => m.id === finalPayload.model);
3599
+ if (selectedModel) {
3600
+ const tokenCount = await getTokenCount(finalPayload, selectedModel);
3601
+ setRequestContext(c, { inputTokens: tokenCount.input });
3602
+ }
3603
+ } catch {}
3390
3604
  logger$1.debug("Translated OpenAI request payload:", JSON.stringify(finalPayload));
3391
- const response = isAzureOpenAIModel(finalPayload.model) && state.azureOpenAIConfig ? await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, finalPayload) : await createChatCompletions(finalPayload, { initiator: initiatorOverride });
3605
+ const response = await createChatCompletions(finalPayload, { initiator: initiatorOverride });
3392
3606
  if (isNonStreaming(response)) {
3393
3607
  logger$1.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
3394
3608
  const anthropicResponse = translateToAnthropic(response);
@@ -3407,7 +3621,8 @@ const handleWithChatCompletions = async (c, anthropicPayload, initiatorOverride)
3407
3621
  logger$1.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
3408
3622
  if (rawEvent.data === "[DONE]") break;
3409
3623
  if (!rawEvent.data) continue;
3410
- const events$1 = translateChunkToAnthropicEvents(JSON.parse(rawEvent.data), streamState);
3624
+ const chunk = JSON.parse(rawEvent.data);
3625
+ const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
3411
3626
  for (const event of events$1) {
3412
3627
  logger$1.debug("Translated Anthropic event:", JSON.stringify(event));
3413
3628
  await stream.writeSSE({
@@ -3418,8 +3633,9 @@ const handleWithChatCompletions = async (c, anthropicPayload, initiatorOverride)
3418
3633
  }
3419
3634
  });
3420
3635
  };
3421
- const handleWithResponsesApi = async (c, anthropicPayload, initiatorOverride) => {
3422
- const responsesPayload = translateAnthropicMessagesToResponsesPayload(anthropicPayload);
3636
+ const handleWithResponsesApi = async (c, anthropicPayload, options) => {
3637
+ const { initiatorOverride, effortOverride } = options ?? {};
3638
+ const responsesPayload = translateAnthropicMessagesToResponsesPayload(anthropicPayload, effortOverride);
3423
3639
  logger$1.debug("Translated Responses payload:", JSON.stringify(responsesPayload));
3424
3640
  const { vision, initiator } = getResponsesRequestOptions(responsesPayload);
3425
3641
  const response = await createResponses(responsesPayload, {
@@ -3471,14 +3687,15 @@ const handleWithResponsesApi = async (c, anthropicPayload, initiatorOverride) =>
3471
3687
  return c.json(anthropicResponse);
3472
3688
  };
3473
3689
  const handleWithMessagesApi = async (c, anthropicPayload, options) => {
3474
- const { anthropicBetaHeader, initiatorOverride, selectedModel } = options ?? {};
3690
+ const { anthropicBetaHeader, initiatorOverride, selectedModel, effortOverride } = options ?? {};
3475
3691
  for (const msg of anthropicPayload.messages) if (msg.role === "assistant" && Array.isArray(msg.content)) msg.content = msg.content.filter((block) => {
3476
3692
  if (block.type !== "thinking") return true;
3477
3693
  return block.thinking && block.thinking !== "Thinking..." && block.signature && !block.signature.includes("@");
3478
3694
  });
3479
3695
  if (selectedModel?.capabilities.supports.adaptive_thinking) {
3480
- anthropicPayload.thinking = { type: "adaptive" };
3481
- anthropicPayload.output_config = { effort: getAnthropicEffortForModel(anthropicPayload.model) };
3696
+ if (!anthropicPayload.thinking) anthropicPayload.thinking = { type: "adaptive" };
3697
+ const clientEffort = anthropicPayload.output_config?.effort;
3698
+ anthropicPayload.output_config = { effort: effortOverride ? getAnthropicEffortForModel(anthropicPayload.model, effortOverride) : clientEffort ?? getAnthropicEffortForModel(anthropicPayload.model) };
3482
3699
  }
3483
3700
  logger$1.debug("Translated Messages payload:", JSON.stringify(anthropicPayload));
3484
3701
  const response = await createMessages(anthropicPayload, anthropicBetaHeader, { initiator: initiatorOverride });
@@ -3499,6 +3716,21 @@ const handleWithMessagesApi = async (c, anthropicPayload, options) => {
3499
3716
  logger$1.debug("Non-streaming Messages result:", JSON.stringify(response).slice(-400));
3500
3717
  return c.json(response);
3501
3718
  };
3719
+ /**
3720
+ * Route to model variants based on client signals (1m context, fast mode).
3721
+ * Mutates the payload in place.
3722
+ */
3723
+ function applyModelVariantRouting(payload, anthropicBeta) {
3724
+ if (anthropicBeta?.includes("context-1m")) {
3725
+ const candidate = `${payload.model}-1m`;
3726
+ if (state.models?.data.some((m) => m.id === candidate)) payload.model = candidate;
3727
+ }
3728
+ if (payload.speed === "fast") {
3729
+ const candidate = `${payload.model}-fast`;
3730
+ if (state.models?.data.some((m) => m.id === candidate)) payload.model = candidate;
3731
+ delete payload.speed;
3732
+ }
3733
+ }
3502
3734
  const shouldUseResponsesApi = (selectedModel) => {
3503
3735
  return selectedModel?.supported_endpoints?.includes(RESPONSES_ENDPOINT$1) ?? false;
3504
3736
  };
@@ -3507,8 +3739,25 @@ const shouldUseMessagesApi = (selectedModel) => {
3507
3739
  };
3508
3740
  const isNonStreaming = (response) => Object.hasOwn(response, "choices");
3509
3741
  const isAsyncIterable$1 = (value) => Boolean(value) && typeof value[Symbol.asyncIterator] === "function";
3510
- const getAnthropicEffortForModel = (model) => {
3511
- const reasoningEffort = getReasoningEffortForModel(model);
3742
+ /**
3743
+ * Extract reasoning effort info from the Anthropic request body for logging.
3744
+ * Claude Code sends effort as `output_config.effort` (low/medium/high/max)
3745
+ * and thinking mode as `thinking.type` (enabled/adaptive).
3746
+ * When effort is "high" (the default), Claude Code omits output_config.effort entirely.
3747
+ */
3748
+ function getBodyReasoningEffort(payload) {
3749
+ if (!payload.thinking && !payload.output_config?.effort) return void 0;
3750
+ const parts = [];
3751
+ const effort = payload.output_config?.effort ?? (payload.thinking ? "high" : void 0);
3752
+ if (effort) parts.push(effort);
3753
+ if (payload.thinking) {
3754
+ parts.push(payload.thinking.type);
3755
+ if (payload.thinking.budget_tokens) parts.push(`${payload.thinking.budget_tokens.toLocaleString()} budget`);
3756
+ }
3757
+ return parts.length > 0 ? parts.join(", ") : void 0;
3758
+ }
3759
+ const getAnthropicEffortForModel = (model, override) => {
3760
+ const reasoningEffort = getReasoningEffortForModel(model, override);
3512
3761
  if (reasoningEffort === "xhigh") return "max";
3513
3762
  if (reasoningEffort === "none" || reasoningEffort === "minimal") return "low";
3514
3763
  return reasoningEffort;
@@ -3597,19 +3846,10 @@ modelRoutes.get("/", async (c) => {
3597
3846
  owned_by: model.vendor,
3598
3847
  display_name: model.name
3599
3848
  })) ?? [];
3600
- const azureModels = state.azureOpenAIDeployments?.map((deployment) => ({
3601
- id: deployment.id,
3602
- object: "model",
3603
- type: "model",
3604
- created: deployment.created,
3605
- created_at: (/* @__PURE__ */ new Date(deployment.created * 1e3)).toISOString(),
3606
- owned_by: deployment.owned_by,
3607
- display_name: `${deployment.deploymentName} (${deployment.model})`
3608
- })) ?? [];
3609
- const allModels = [...copilotModels, ...azureModels];
3849
+ const virtualModels = state.models ? generateVirtualModels(state.models.data) : [];
3610
3850
  return c.json({
3611
3851
  object: "list",
3612
- data: allModels,
3852
+ data: [...copilotModels, ...virtualModels],
3613
3853
  has_more: false
3614
3854
  });
3615
3855
  } catch (error) {
@@ -3629,20 +3869,27 @@ replacementsRoute.get("/", async (c) => {
3629
3869
  replacementsRoute.post("/", async (c) => {
3630
3870
  const body = await c.req.json();
3631
3871
  if (!body.pattern) return c.json({ error: "Pattern is required" }, 400);
3632
- const rule = await addReplacement(body.pattern, body.replacement ?? "", body.isRegex ?? false, body.name);
3872
+ const rule = await addReplacement(body.pattern, body.replacement ?? "", {
3873
+ isRegex: body.isRegex ?? false,
3874
+ name: body.name
3875
+ });
3633
3876
  return c.json(rule, 201);
3634
3877
  });
3635
3878
  replacementsRoute.delete("/:id", async (c) => {
3636
- if (!await removeReplacement(c.req.param("id"))) return c.json({ error: "Replacement not found or is a system rule" }, 404);
3879
+ const id = c.req.param("id");
3880
+ if (!await removeReplacement(id)) return c.json({ error: "Replacement not found or is a system rule" }, 404);
3637
3881
  return c.json({ success: true });
3638
3882
  });
3639
3883
  replacementsRoute.patch("/:id", async (c) => {
3640
- const rule = await updateReplacement(c.req.param("id"), await c.req.json());
3884
+ const id = c.req.param("id");
3885
+ const body = await c.req.json();
3886
+ const rule = await updateReplacement(id, body);
3641
3887
  if (!rule) return c.json({ error: "Replacement not found or is a system rule" }, 404);
3642
3888
  return c.json(rule);
3643
3889
  });
3644
3890
  replacementsRoute.patch("/:id/toggle", async (c) => {
3645
- const rule = await toggleReplacement(c.req.param("id"));
3891
+ const id = c.req.param("id");
3892
+ const rule = await toggleReplacement(id);
3646
3893
  if (!rule) return c.json({ error: "Replacement not found or is a system rule" }, 404);
3647
3894
  return c.json(rule);
3648
3895
  });
@@ -3692,12 +3939,36 @@ const handleItemId = (parsed, tracker) => {
3692
3939
  //#region src/routes/responses/handler.ts
3693
3940
  const logger = createHandlerLogger("responses-handler");
3694
3941
  const RESPONSES_ENDPOINT = "/responses";
3942
+ function isResponsesReasoningEffort(value) {
3943
+ return value === "none" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh";
3944
+ }
3945
+ function normalizeResponsesReasoning(payload, suffixEffort) {
3946
+ const topLevelEffortRaw = payload.reasoningEffort ?? payload.reasoning_effort;
3947
+ const topLevelEffort = isResponsesReasoningEffort(topLevelEffortRaw) ? topLevelEffortRaw : void 0;
3948
+ if (topLevelEffort) payload.reasoning = payload.reasoning ? {
3949
+ ...payload.reasoning,
3950
+ effort: payload.reasoning.effort ?? topLevelEffort
3951
+ } : { effort: topLevelEffort };
3952
+ delete payload.reasoningEffort;
3953
+ delete payload.reasoning_effort;
3954
+ if (suffixEffort) payload.reasoning = payload.reasoning ? {
3955
+ ...payload.reasoning,
3956
+ effort: suffixEffort
3957
+ } : { effort: suffixEffort };
3958
+ return payload.reasoning?.effort ?? void 0;
3959
+ }
3695
3960
  const handleResponses = async (c) => {
3696
3961
  await checkRateLimit(state);
3697
3962
  const payload = await c.req.json();
3963
+ const requestedModel = payload.model;
3964
+ const { baseModel, reasoningEffort: suffixEffort } = parseModelSuffix(payload.model);
3965
+ payload.model = baseModel;
3966
+ const effectiveEffort = normalizeResponsesReasoning(payload, suffixEffort);
3698
3967
  setRequestContext(c, {
3699
- provider: "Copilot (Responses)",
3700
- model: payload.model
3968
+ requestedModel,
3969
+ provider: "Responses",
3970
+ model: payload.model,
3971
+ reasoningEffort: effectiveEffort
3701
3972
  });
3702
3973
  logger.debug("Responses request payload:", JSON.stringify(payload));
3703
3974
  useFunctionApplyPatch(payload);
@@ -3806,6 +4077,7 @@ usageRoute.get("/", async (c) => {
3806
4077
  //#endregion
3807
4078
  //#region src/server.ts
3808
4079
  const server = new Hono();
4080
+ server.use(apiKeyGuard);
3809
4081
  server.use(requestLogger);
3810
4082
  server.use(cors());
3811
4083
  server.use("*", createAuthMiddleware());
@@ -3825,8 +4097,13 @@ server.route("/v1/messages", messageRoutes);
3825
4097
 
3826
4098
  //#endregion
3827
4099
  //#region src/start.ts
4100
+ function getAllModelIds() {
4101
+ const baseModelIds = state.models?.data.map((model) => model.id) ?? [];
4102
+ const virtualModelIds = state.models ? generateVirtualModels(state.models.data).map((model) => model.id) : [];
4103
+ return [...baseModelIds, ...virtualModelIds];
4104
+ }
3828
4105
  async function runServer(options) {
3829
- consola.info(`copilot-api v${version}`);
4106
+ consola.info(`copilot-api v${package_default.version}`);
3830
4107
  if (options.insecure) {
3831
4108
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
3832
4109
  consola.warn("SSL certificate verification disabled (insecure mode)");
@@ -3844,6 +4121,9 @@ async function runServer(options) {
3844
4121
  state.showToken = options.showToken;
3845
4122
  state.debug = options.debug;
3846
4123
  state.verbose = options.verbose;
4124
+ state.apiKeyAuth = options.apiKeyAuth;
4125
+ if (options.apiKeyAuth) consola.info("API key authentication enabled - unauthorized requests will be silently dropped");
4126
+ if (options.host) consola.info(`Binding to host: ${options.host}`);
3847
4127
  if (options.debug) consola.info("Debug mode enabled - raw HTTP requests will be logged");
3848
4128
  await ensurePaths();
3849
4129
  mergeConfigWithDefaults();
@@ -3854,12 +4134,9 @@ async function runServer(options) {
3854
4134
  } else await setupGitHubToken();
3855
4135
  await setupCopilotToken();
3856
4136
  await cacheModels();
3857
- await setupAzureOpenAI();
3858
- const copilotModelIds = state.models?.data.map((model) => model.id) ?? [];
3859
- const azureModelIds = state.azureOpenAIDeployments?.map((deployment) => deployment.id) ?? [];
3860
- const allModelIds = [...copilotModelIds, ...azureModelIds];
4137
+ const allModelIds = getAllModelIds();
3861
4138
  consola.info(`Available models: \n${allModelIds.map((id) => `- ${id}`).join("\n")}`);
3862
- const serverUrl = `http://localhost:${options.port}`;
4139
+ const serverUrl = `http://${options.host ?? "localhost"}:${options.port}`;
3863
4140
  if (options.claudeCode) {
3864
4141
  invariant(state.models, "Models should be loaded by now");
3865
4142
  const selectedModel = await consola.prompt("Select a model to use with Claude Code", {
@@ -3892,9 +4169,21 @@ async function runServer(options) {
3892
4169
  serve({
3893
4170
  fetch: server.fetch,
3894
4171
  port: options.port,
4172
+ hostname: options.host,
3895
4173
  bun: { idleTimeout: 255 }
3896
4174
  });
3897
4175
  }
4176
+ /**
4177
+ * Resolve --api-key-auth value: use provided value, fall back to env, or error if flag used without value.
4178
+ */
4179
+ function resolveApiKeyAuth(cliValue) {
4180
+ if (cliValue === void 0) return void 0;
4181
+ if (cliValue !== "" && cliValue !== "true") return cliValue;
4182
+ const envValue = process.env.COPILOT_API_KEY_AUTH;
4183
+ if (envValue) return envValue;
4184
+ consola.error("--api-key-auth requires a value or COPILOT_API_KEY_AUTH environment variable");
4185
+ process.exit(1);
4186
+ }
3898
4187
  const start = defineCommand({
3899
4188
  meta: {
3900
4189
  name: "start",
@@ -3966,6 +4255,14 @@ const start = defineCommand({
3966
4255
  type: "boolean",
3967
4256
  default: false,
3968
4257
  description: "Log raw HTTP requests received by the server (headers, method, path)"
4258
+ },
4259
+ "api-key-auth": {
4260
+ type: "string",
4261
+ description: "API key for incoming request authentication. Requests with mismatched keys are silently dropped."
4262
+ },
4263
+ host: {
4264
+ type: "string",
4265
+ description: "Hostname/IP to bind the server to (e.g., 0.0.0.0 for all interfaces)"
3969
4266
  }
3970
4267
  },
3971
4268
  run({ args }) {
@@ -3983,17 +4280,19 @@ const start = defineCommand({
3983
4280
  showToken: args["show-token"],
3984
4281
  proxyEnv: args["proxy-env"],
3985
4282
  insecure: args.insecure,
3986
- debug: args.debug
4283
+ debug: args.debug,
4284
+ apiKeyAuth: resolveApiKeyAuth(args["api-key-auth"]),
4285
+ host: args.host
3987
4286
  });
3988
4287
  }
3989
4288
  });
3990
4289
 
3991
4290
  //#endregion
3992
4291
  //#region src/main.ts
3993
- await runMain(defineCommand({
4292
+ const main = defineCommand({
3994
4293
  meta: {
3995
4294
  name: "copilot-api",
3996
- version,
4295
+ version: package_default.version,
3997
4296
  description: "A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools."
3998
4297
  },
3999
4298
  subCommands: {
@@ -4003,7 +4302,8 @@ await runMain(defineCommand({
4003
4302
  debug,
4004
4303
  config
4005
4304
  }
4006
- }));
4305
+ });
4306
+ await runMain(main);
4007
4307
 
4008
4308
  //#endregion
4009
4309
  export { };