@openhoo/hoopilot 1.3.0 → 2.0.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.
package/dist/index.js CHANGED
@@ -35,8 +35,12 @@ function parseUrl(rawUrl) {
35
35
  }
36
36
  return url;
37
37
  }
38
+ var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
39
+ function isLoopbackHostname(host) {
40
+ return LOOPBACK_HOSTNAMES.has(host);
41
+ }
38
42
  function isLoopbackHttpUrl(url) {
39
- return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1" || url.hostname === "[::1]");
43
+ return url.protocol === "http:" && isLoopbackHostname(url.hostname);
40
44
  }
41
45
  async function truncatedResponseText(response, max = 500) {
42
46
  const text = await response.text();
@@ -45,6 +49,48 @@ async function truncatedResponseText(response, max = 500) {
45
49
  function asRecord(value) {
46
50
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
47
51
  }
52
+ function errorMessage(error) {
53
+ return error instanceof Error ? error.message : String(error);
54
+ }
55
+ function firstNumber(...values) {
56
+ for (const value of values) {
57
+ if (typeof value === "number" && Number.isFinite(value)) {
58
+ return value;
59
+ }
60
+ }
61
+ return void 0;
62
+ }
63
+ function randomId() {
64
+ return crypto.randomUUID().replaceAll("-", "");
65
+ }
66
+ function removeUndefined(value) {
67
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
68
+ }
69
+ function safeJsonParse(text) {
70
+ try {
71
+ return JSON.parse(text);
72
+ } catch {
73
+ return void 0;
74
+ }
75
+ }
76
+ function parseJsonObject(text) {
77
+ try {
78
+ return asRecord(JSON.parse(text));
79
+ } catch {
80
+ return void 0;
81
+ }
82
+ }
83
+ var STREAMING_PROXY_MODES = [
84
+ "auto",
85
+ "buffer",
86
+ "live"
87
+ ];
88
+ function parseStreamingProxyMode(value) {
89
+ if (STREAMING_PROXY_MODES.includes(value)) {
90
+ return value;
91
+ }
92
+ throw new Error(`Invalid stream mode: ${value}. Expected ${STREAMING_PROXY_MODES.join(", ")}.`);
93
+ }
48
94
 
49
95
  // src/openai.ts
50
96
  var DEFAULT_MODEL = "gpt-4.1";
@@ -172,13 +218,6 @@ function compactionOutputFromResponsesSse(text) {
172
218
  }
173
219
  return deltas ? [messageOutputItem(deltas)] : [];
174
220
  }
175
- function safeJsonParse(text) {
176
- try {
177
- return JSON.parse(text);
178
- } catch {
179
- return void 0;
180
- }
181
- }
182
221
  function chatCompletionToCompletion(completion) {
183
222
  return removeUndefined({
184
223
  choices: completionChoices(completion).map((choice, index) => {
@@ -372,7 +411,7 @@ function responsesStreamFromChatStream(chatStream, options) {
372
411
  if (isNew) {
373
412
  enqueue("response.output_item.added", {
374
413
  item: functionCallItem(existing, "in_progress"),
375
- output_index: existing.outputIndex ?? 0,
414
+ output_index: existing.outputIndex,
376
415
  type: "response.output_item.added"
377
416
  });
378
417
  }
@@ -382,7 +421,7 @@ function responsesStreamFromChatStream(chatStream, options) {
382
421
  enqueue("response.function_call_arguments.delta", {
383
422
  delta: argumentDelta,
384
423
  item_id: existing.itemId,
385
- output_index: existing.outputIndex ?? 0,
424
+ output_index: existing.outputIndex,
386
425
  type: "response.function_call_arguments.delta"
387
426
  });
388
427
  }
@@ -432,11 +471,9 @@ function responsesStreamFromChatStream(chatStream, options) {
432
471
  type: "response.output_item.done"
433
472
  });
434
473
  }
435
- for (const tool of [...tools.values()].sort(
436
- (a, b) => (a.outputIndex ?? 0) - (b.outputIndex ?? 0)
437
- )) {
474
+ for (const tool of [...tools.values()].sort((a, b) => a.outputIndex - b.outputIndex)) {
438
475
  const item = functionCallItem(tool);
439
- const outputIndex = tool.outputIndex ?? 0;
476
+ const outputIndex = tool.outputIndex;
440
477
  outputEntries.push([outputIndex, item]);
441
478
  enqueue("response.function_call_arguments.done", {
442
479
  arguments: tool.arguments,
@@ -679,7 +716,6 @@ function outputItemsFromMessage(message) {
679
716
  functionCallItem({
680
717
  arguments: contentToText(fn.arguments),
681
718
  id: contentToText(record.id) || `call_${randomId()}`,
682
- index: output.length,
683
719
  name: contentToText(fn.name)
684
720
  })
685
721
  );
@@ -761,21 +797,18 @@ function extractTokenUsage(usage) {
761
797
  asRecord(record.prompt_tokens_details).cached_tokens,
762
798
  asRecord(record.input_tokens_details).cached_tokens
763
799
  );
764
- return removeUndefined({
765
- cachedTokens: cached,
800
+ const result = {
766
801
  completionTokens,
767
802
  promptTokens,
768
- reasoningTokens: reasoning,
769
803
  totalTokens: total ?? promptTokens + completionTokens
770
- });
771
- }
772
- function firstNumber(...values) {
773
- for (const value of values) {
774
- if (typeof value === "number" && Number.isFinite(value)) {
775
- return value;
776
- }
804
+ };
805
+ if (cached !== void 0) {
806
+ result.cachedTokens = cached;
777
807
  }
778
- return void 0;
808
+ if (reasoning !== void 0) {
809
+ result.reasoningTokens = reasoning;
810
+ }
811
+ return result;
779
812
  }
780
813
  function firstChoice(completion) {
781
814
  return completionChoices(completion)[0] ?? {};
@@ -804,7 +837,7 @@ function processCompletionSseBlock(block, enqueue, markTerminal) {
804
837
  enqueue("[DONE]");
805
838
  return;
806
839
  }
807
- const parsed = parseJson(data);
840
+ const parsed = parseJsonObject(data);
808
841
  if (!parsed) {
809
842
  return;
810
843
  }
@@ -869,7 +902,7 @@ function processChatSseLine(line, handlers) {
869
902
  if (!data || data === "[DONE]") {
870
903
  return;
871
904
  }
872
- const parsed = parseJson(data);
905
+ const parsed = parseJsonObject(data);
873
906
  if (!parsed) {
874
907
  return;
875
908
  }
@@ -921,19 +954,6 @@ function encodeDataSse(data) {
921
954
 
922
955
  `;
923
956
  }
924
- function parseJson(data) {
925
- try {
926
- return asRecord(JSON.parse(data));
927
- } catch {
928
- return void 0;
929
- }
930
- }
931
- function removeUndefined(record) {
932
- return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
933
- }
934
- function randomId() {
935
- return crypto.randomUUID().replaceAll("-", "");
936
- }
937
957
  function epochSeconds() {
938
958
  return Math.floor(Date.now() / 1e3);
939
959
  }
@@ -946,13 +966,13 @@ var AnthropicCompatibilityError = class extends Error {
946
966
  }
947
967
  };
948
968
  function anthropicMessagesToResponsesRequest(request) {
949
- return removeUndefined2({
969
+ return removeUndefined({
950
970
  input: anthropicMessagesToResponsesInput(request.messages),
951
971
  instructions: anthropicSystemToInstructions(request.system),
952
972
  max_output_tokens: typeof request.max_tokens === "number" && Number.isFinite(request.max_tokens) ? request.max_tokens : void 0,
953
973
  metadata: request.metadata,
954
974
  model: normalizeRequestedModel(request.model),
955
- parallel_tool_calls: true,
975
+ parallel_tool_calls: asRecord(request.tool_choice).disable_parallel_tool_use !== true,
956
976
  reasoning: anthropicThinkingToReasoning(request.thinking),
957
977
  stop: anthropicStopSequences(request.stop_sequences),
958
978
  stream: request.stream === true,
@@ -967,7 +987,7 @@ function responsesResponseToAnthropicMessage(response, fallbackModel) {
967
987
  const usage = anthropicUsage(response.usage);
968
988
  return {
969
989
  content,
970
- id: textValue(response.id) || `msg_${randomId2()}`,
990
+ id: textValue(response.id) || `msg_${randomId()}`,
971
991
  model: textValue(response.model) || fallbackModel,
972
992
  role: "assistant",
973
993
  stop_reason: anthropicStopReason(response, content),
@@ -1044,7 +1064,7 @@ function createAnthropicStreamState(options) {
1044
1064
  return {
1045
1065
  blocks: /* @__PURE__ */ new Map(),
1046
1066
  completed: false,
1047
- messageId: options.messageId ?? `msg_${randomId2()}`,
1067
+ messageId: options.messageId ?? `msg_${randomId()}`,
1048
1068
  model: options.model,
1049
1069
  nextBlockIndex: 0,
1050
1070
  sawToolUse: false,
@@ -1098,7 +1118,7 @@ function anthropicMessagesToResponsesInput(messages) {
1098
1118
  flushMessage();
1099
1119
  input.push({
1100
1120
  arguments: JSON.stringify(asRecord(part.input)),
1101
- call_id: textValue(part.id) || `call_${randomId2()}`,
1121
+ call_id: textValue(part.id) || `call_${randomId()}`,
1102
1122
  name: textValue(part.name),
1103
1123
  type: "function_call"
1104
1124
  });
@@ -1209,7 +1229,7 @@ function anthropicTools(tools) {
1209
1229
  }
1210
1230
  const converted = tools.map((tool) => {
1211
1231
  const record = asRecord(tool);
1212
- return removeUndefined2({
1232
+ return removeUndefined({
1213
1233
  description: record.description,
1214
1234
  name: record.name,
1215
1235
  parameters: record.input_schema,
@@ -1280,7 +1300,7 @@ function anthropicContentFromResponsesOutput(response) {
1280
1300
  }
1281
1301
  if (type === "function_call") {
1282
1302
  content.push({
1283
- id: textValue(record.call_id) || textValue(record.id) || `call_${randomId2()}`,
1303
+ id: textValue(record.call_id) || textValue(record.id) || `call_${randomId()}`,
1284
1304
  input: parseToolInput(textValue(record.arguments)),
1285
1305
  name: textValue(record.name),
1286
1306
  type: "tool_use"
@@ -1307,12 +1327,12 @@ function anthropicStopReason(response, content) {
1307
1327
  }
1308
1328
  function anthropicUsage(usage) {
1309
1329
  const record = asRecord(usage);
1310
- const inputTokens = firstNumber2(record.input_tokens, record.prompt_tokens) ?? 0;
1311
- const outputTokens = firstNumber2(record.output_tokens, record.completion_tokens) ?? 0;
1330
+ const inputTokens = firstNumber(record.input_tokens, record.prompt_tokens) ?? 0;
1331
+ const outputTokens = firstNumber(record.output_tokens, record.completion_tokens) ?? 0;
1312
1332
  const details = asRecord(record.input_tokens_details);
1313
- return removeUndefined2({
1314
- cache_creation_input_tokens: firstNumber2(record.cache_creation_input_tokens),
1315
- cache_read_input_tokens: firstNumber2(record.cache_read_input_tokens, details.cached_tokens) ?? void 0,
1333
+ return removeUndefined({
1334
+ cache_creation_input_tokens: firstNumber(record.cache_creation_input_tokens),
1335
+ cache_read_input_tokens: firstNumber(record.cache_read_input_tokens, details.cached_tokens) ?? void 0,
1316
1336
  input_tokens: inputTokens,
1317
1337
  output_tokens: outputTokens
1318
1338
  });
@@ -1494,7 +1514,7 @@ function ensureToolBlock(state, payload, item, enqueue) {
1494
1514
  state.blocks.set(key, block);
1495
1515
  enqueue("content_block_start", {
1496
1516
  content_block: {
1497
- id: textValue(item.call_id) || textValue(item.id) || `call_${randomId2()}`,
1517
+ id: textValue(item.call_id) || textValue(item.id) || `call_${randomId()}`,
1498
1518
  input: {},
1499
1519
  name: textValue(item.name),
1500
1520
  type: "tool_use"
@@ -1528,13 +1548,6 @@ function parseSseBlock(block) {
1528
1548
  }
1529
1549
  return { data: data.join("\n"), event };
1530
1550
  }
1531
- function parseJsonObject(text) {
1532
- try {
1533
- return asRecord(JSON.parse(text));
1534
- } catch {
1535
- return void 0;
1536
- }
1537
- }
1538
1551
  function parseToolInput(argumentsText) {
1539
1552
  const parsed = parseJsonObject(argumentsText);
1540
1553
  return parsed ?? {};
@@ -1566,32 +1579,18 @@ function textValue(value) {
1566
1579
  }
1567
1580
  return "";
1568
1581
  }
1569
- function firstNumber2(...values) {
1570
- for (const value of values) {
1571
- if (typeof value === "number" && Number.isFinite(value)) {
1572
- return value;
1573
- }
1574
- }
1575
- return void 0;
1576
- }
1577
1582
  function indexValue(value) {
1578
1583
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
1579
1584
  }
1580
- function removeUndefined2(record) {
1581
- return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
1582
- }
1583
1585
  function encodeSse2(event, data) {
1584
1586
  return `event: ${event}
1585
1587
  data: ${JSON.stringify(data)}
1586
1588
 
1587
1589
  `;
1588
1590
  }
1589
- function randomId2() {
1590
- return crypto.randomUUID().replaceAll("-", "");
1591
- }
1592
1591
 
1593
1592
  // src/auth-store.ts
1594
- import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
1593
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
1595
1594
  import { dirname, join } from "path";
1596
1595
  var StoredCopilotAuthError = class extends Error {
1597
1596
  constructor(message) {
@@ -1642,7 +1641,7 @@ function readStoredCopilotAuth(path = authStorePath()) {
1642
1641
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1643
1642
  throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} must contain a JSON object.`);
1644
1643
  }
1645
- const record = parsed;
1644
+ const record = asRecord(parsed);
1646
1645
  const token = typeof record.token === "string" ? record.token.trim() : "";
1647
1646
  if (!token) {
1648
1647
  throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} does not contain a token.`);
@@ -1668,7 +1667,15 @@ function writeStoredCopilotAuth(auth, path = authStorePath()) {
1668
1667
  `;
1669
1668
  const tmpPath = `${path}.${process.pid}.tmp`;
1670
1669
  writeFileSync(tmpPath, data, { mode: 384 });
1671
- renameSync(tmpPath, path);
1670
+ try {
1671
+ renameSync(tmpPath, path);
1672
+ } catch (error) {
1673
+ try {
1674
+ rmSync(tmpPath, { force: true });
1675
+ } catch {
1676
+ }
1677
+ throw error;
1678
+ }
1672
1679
  try {
1673
1680
  chmodSync(path, 384);
1674
1681
  } catch {
@@ -1713,23 +1720,20 @@ var CopilotAuth = class {
1713
1720
  throw error;
1714
1721
  }
1715
1722
  if (stored) {
1716
- return this.#cacheAccess({
1723
+ this.#cachedAccess = {
1717
1724
  apiBaseUrl: trimTrailingSlash(
1718
1725
  this.#hasCopilotApiBaseUrlOverride ? this.#copilotApiBaseUrl : stored.apiBaseUrl ?? this.#copilotApiBaseUrl
1719
1726
  ),
1720
1727
  expiresAtMs: Date.now() + STORED_TOKEN_TTL_MS,
1721
1728
  source: "github-copilot-oauth",
1722
1729
  token: stored.token
1723
- });
1730
+ };
1731
+ return this.#cachedAccess;
1724
1732
  }
1725
1733
  throw new CopilotAuthError(
1726
1734
  "No GitHub Copilot OAuth credential found. Run `hoopilot login` to sign in through your browser."
1727
1735
  );
1728
1736
  }
1729
- #cacheAccess(access) {
1730
- this.#cachedAccess = access;
1731
- return access;
1732
- }
1733
1737
  };
1734
1738
 
1735
1739
  // src/copilot.ts
@@ -1737,23 +1741,26 @@ var DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
1737
1741
  var ALLOWED_COPILOT_API_HOSTS = ["api.githubcopilot.com"];
1738
1742
  var ALLOWED_GITHUB_API_HOSTS = ["api.github.com"];
1739
1743
  var COPILOT_USAGE_API_VERSION = "2025-04-01";
1744
+ var EDITOR_PLUGIN_VERSION = "hoopilot/0.1.0";
1745
+ var EDITOR_VERSION = "Hoopilot/0.1.0";
1746
+ var HOOPILOT_USER_AGENT = "hoopilot/0.1.0";
1740
1747
  function applyCopilotHeaders(headers, token) {
1741
1748
  headers.set("accept", headers.get("accept") ?? "application/json");
1742
1749
  headers.set("authorization", `Bearer ${token}`);
1743
1750
  headers.set("copilot-integration-id", "vscode-chat");
1744
- headers.set("editor-plugin-version", "hoopilot/0.1.0");
1745
- headers.set("editor-version", "Hoopilot/0.1.0");
1751
+ headers.set("editor-plugin-version", EDITOR_PLUGIN_VERSION);
1752
+ headers.set("editor-version", EDITOR_VERSION);
1746
1753
  headers.set("openai-intent", "conversation-panel");
1747
- headers.set("user-agent", "hoopilot/0.1.0");
1754
+ headers.set("user-agent", HOOPILOT_USER_AGENT);
1748
1755
  headers.set("x-github-api-version", "2026-06-01");
1749
1756
  return headers;
1750
1757
  }
1751
1758
  function applyGithubApiHeaders(headers, token) {
1752
1759
  headers.set("accept", headers.get("accept") ?? "application/json");
1753
1760
  headers.set("authorization", `token ${token}`);
1754
- headers.set("editor-plugin-version", "hoopilot/0.1.0");
1755
- headers.set("editor-version", "Hoopilot/0.1.0");
1756
- headers.set("user-agent", "hoopilot/0.1.0");
1761
+ headers.set("editor-plugin-version", EDITOR_PLUGIN_VERSION);
1762
+ headers.set("editor-version", EDITOR_VERSION);
1763
+ headers.set("user-agent", HOOPILOT_USER_AGENT);
1757
1764
  headers.set("x-github-api-version", COPILOT_USAGE_API_VERSION);
1758
1765
  return headers;
1759
1766
  }
@@ -1766,7 +1773,7 @@ function parseRateLimitHeaders(headers, nowMs = Date.now()) {
1766
1773
  if (limit === void 0 && remaining === void 0 && used === void 0 && resetEpochSeconds === void 0 && retryAfterSeconds === void 0) {
1767
1774
  return void 0;
1768
1775
  }
1769
- return removeUndefinedRateLimit({
1776
+ return removeUndefined({
1770
1777
  limit,
1771
1778
  observedAtMs: nowMs,
1772
1779
  remaining,
@@ -1784,11 +1791,6 @@ function headerInt(headers, name) {
1784
1791
  const value = Number.parseInt(raw.trim(), 10);
1785
1792
  return Number.isFinite(value) && value >= 0 ? value : void 0;
1786
1793
  }
1787
- function removeUndefinedRateLimit(rateLimit) {
1788
- return Object.fromEntries(
1789
- Object.entries(rateLimit).filter(([, value]) => value !== void 0)
1790
- );
1791
- }
1792
1794
  var CopilotClient = class {
1793
1795
  #auth;
1794
1796
  #allowUnsafeUpstream;
@@ -1885,7 +1887,7 @@ function normalizeCopilotUsage(body) {
1885
1887
  for (const category of /* @__PURE__ */ new Set([...Object.keys(remaining), ...Object.keys(monthly)])) {
1886
1888
  const entitlement = numberOrUndefined(monthly[category]);
1887
1889
  const left = numberOrUndefined(remaining[category]);
1888
- quotas[category] = removeUndefinedQuota({
1890
+ quotas[category] = removeUndefined({
1889
1891
  entitlement,
1890
1892
  percentRemaining: entitlement !== void 0 && entitlement > 0 && left !== void 0 ? left / entitlement * 100 : void 0,
1891
1893
  remaining: left,
@@ -1893,7 +1895,7 @@ function normalizeCopilotUsage(body) {
1893
1895
  });
1894
1896
  }
1895
1897
  }
1896
- return removeUndefinedUsage({
1898
+ return removeUndefined({
1897
1899
  accessTypeSku: stringOrUndefined(record.access_type_sku),
1898
1900
  chatEnabled: typeof record.chat_enabled === "boolean" ? record.chat_enabled : void 0,
1899
1901
  plan: stringOrUndefined(record.copilot_plan),
@@ -1905,7 +1907,7 @@ function normalizeQuotaDetail(detail) {
1905
1907
  const entitlement = numberOrUndefined(detail.entitlement);
1906
1908
  const overageCount = numberOrUndefined(detail.overage_count);
1907
1909
  const remaining = numberOrUndefined(detail.remaining) ?? numberOrUndefined(detail.quota_remaining);
1908
- return removeUndefinedQuota({
1910
+ return removeUndefined({
1909
1911
  entitlement,
1910
1912
  hasQuota: typeof detail.has_quota === "boolean" ? detail.has_quota : void 0,
1911
1913
  overageCount,
@@ -1929,21 +1931,10 @@ function usedFrom(entitlement, remaining, overageCount) {
1929
1931
  const overage = remaining === 0 ? overageCount ?? 0 : 0;
1930
1932
  return Math.max(0, base + overage);
1931
1933
  }
1932
- function numberOrUndefined(value) {
1933
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1934
- }
1934
+ var numberOrUndefined = firstNumber;
1935
1935
  function stringOrUndefined(value) {
1936
1936
  return typeof value === "string" && value.length > 0 ? value : void 0;
1937
1937
  }
1938
- function removeUndefinedQuota(quota) {
1939
- return Object.fromEntries(
1940
- Object.entries(quota).filter(([, value]) => value !== void 0)
1941
- );
1942
- }
1943
- function removeUndefinedUsage(usage) {
1944
- const entries = Object.entries(usage).filter(([, value]) => value !== void 0);
1945
- return Object.fromEntries(entries);
1946
- }
1947
1938
 
1948
1939
  // src/github-device.ts
1949
1940
  import { setTimeout as sleep } from "timers/promises";
@@ -2075,11 +2066,16 @@ function positiveSeconds(value, fallback) {
2075
2066
  }
2076
2067
  async function parseJsonResponse(response, context) {
2077
2068
  const text = await response.text();
2069
+ let value;
2078
2070
  try {
2079
- return JSON.parse(text);
2071
+ value = JSON.parse(text);
2080
2072
  } catch {
2081
2073
  throw new Error(`${context}: ${text.slice(0, 500)}`);
2082
2074
  }
2075
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2076
+ throw new Error(`${context}: ${text.slice(0, 500)}`);
2077
+ }
2078
+ return value;
2083
2079
  }
2084
2080
 
2085
2081
  // src/logger.ts
@@ -2143,21 +2139,29 @@ function createHoopilotLogger(options = {}) {
2143
2139
  timestamp: pino.stdTimeFunctions.isoTime
2144
2140
  };
2145
2141
  if (format === "pretty") {
2146
- return pino(
2147
- pinoOptions,
2148
- pretty({
2149
- colorize: options.colorize ?? process.stderr.isTTY,
2150
- destination: options.stream ?? 1,
2151
- ignore: "pid,hostname",
2152
- singleLine: true,
2153
- translateTime: "SYS:standard"
2154
- })
2142
+ return asHoopilotLogger(
2143
+ pino(
2144
+ pinoOptions,
2145
+ pretty({
2146
+ // Probe the same sink we write to (stdout / fd 1), so colors are not
2147
+ // emitted into a redirected file when only stderr is a TTY. A custom
2148
+ // stream's TTY-ness is unknown, so default to no color there.
2149
+ colorize: options.colorize ?? (options.stream ? false : process.stdout.isTTY),
2150
+ destination: options.stream ?? 1,
2151
+ ignore: "pid,hostname",
2152
+ singleLine: true,
2153
+ translateTime: "SYS:standard"
2154
+ })
2155
+ )
2155
2156
  );
2156
2157
  }
2157
2158
  if (options.stream) {
2158
- return pino(pinoOptions, options.stream);
2159
+ return asHoopilotLogger(pino(pinoOptions, options.stream));
2159
2160
  }
2160
- return pino(pinoOptions);
2161
+ return asHoopilotLogger(pino(pinoOptions));
2162
+ }
2163
+ function asHoopilotLogger(logger) {
2164
+ return logger;
2161
2165
  }
2162
2166
  function parseLogFormat(value) {
2163
2167
  if (!value) {
@@ -2283,26 +2287,23 @@ var MetricsRegistry = class {
2283
2287
  const resource = this.#rateLimitResource(rateLimit.resource);
2284
2288
  this.#githubRateLimit.set(resource, { ...rateLimit, resource });
2285
2289
  }
2286
- // Sanitize the model into a bounded label. The model can originate from a
2287
- // client request, so cap its length, strip characters that would corrupt the
2288
- // exposition format, and fold overflow past the cardinality limit into
2289
- // UNKNOWN_MODEL to keep the series count bounded.
2290
- #modelLabel(model) {
2291
- const cleaned = cleanLabel(model).slice(0, MAX_MODEL_LABEL_LENGTH) || UNKNOWN_MODEL;
2292
- if (!this.#tokens.has(cleaned) && this.#tokens.size >= MAX_TRACKED_MODELS) {
2290
+ // Clean a raw value into a bounded exposition-format label: cap its length,
2291
+ // strip characters that would corrupt the format, and fold overflow past the
2292
+ // cardinality limit into UNKNOWN_MODEL so the series count stays bounded.
2293
+ #boundedLabel(value, tracked, maxEntries) {
2294
+ const cleaned = cleanLabel(value).slice(0, MAX_MODEL_LABEL_LENGTH) || UNKNOWN_MODEL;
2295
+ if (!tracked.has(cleaned) && tracked.size >= maxEntries) {
2293
2296
  return UNKNOWN_MODEL;
2294
2297
  }
2295
2298
  return cleaned;
2296
2299
  }
2297
- // The resource comes from a trusted upstream header, but clean and bound it
2298
- // with the same discipline as model labels: strip control characters that
2299
- // would corrupt the exposition format and fold overflow into "unknown".
2300
+ // The model can originate from a (possibly hostile) client request.
2301
+ #modelLabel(model) {
2302
+ return this.#boundedLabel(model, this.#tokens, MAX_TRACKED_MODELS);
2303
+ }
2304
+ // The resource comes from a trusted upstream header, but is bounded the same way.
2300
2305
  #rateLimitResource(resource) {
2301
- const cleaned = cleanLabel(resource).slice(0, MAX_MODEL_LABEL_LENGTH) || UNKNOWN_MODEL;
2302
- if (!this.#githubRateLimit.has(cleaned) && this.#githubRateLimit.size >= MAX_TRACKED_RATELIMIT_RESOURCES) {
2303
- return UNKNOWN_MODEL;
2304
- }
2305
- return cleaned;
2306
+ return this.#boundedLabel(resource, this.#githubRateLimit, MAX_TRACKED_RATELIMIT_RESOURCES);
2306
2307
  }
2307
2308
  #observeDuration(route, seconds) {
2308
2309
  const value = Number.isFinite(seconds) && seconds >= 0 ? seconds : 0;
@@ -2628,7 +2629,7 @@ function recordResponseTextUsage(text, isSse, fallbackModel, onUsage, onOutcome)
2628
2629
  considerSseLine(line, accumulator.consider);
2629
2630
  }
2630
2631
  } else {
2631
- const parsed = safeParse(text);
2632
+ const parsed = safeJsonParse(text);
2632
2633
  if (parsed !== void 0) {
2633
2634
  accumulator.consider(parsed);
2634
2635
  }
@@ -2690,7 +2691,7 @@ async function consumeUsage(stream, isSse, fallbackModel, onUsage, signal, onOut
2690
2691
  considerSseLine(finalBuffer, accumulator.consider);
2691
2692
  }
2692
2693
  } else if (!overflowed && finalBuffer) {
2693
- const parsed = safeParse(finalBuffer);
2694
+ const parsed = safeJsonParse(finalBuffer);
2694
2695
  if (parsed !== void 0) {
2695
2696
  accumulator.consider(parsed);
2696
2697
  }
@@ -2733,18 +2734,11 @@ function considerSseLine(line, consider) {
2733
2734
  if (!data || data === "[DONE]") {
2734
2735
  return;
2735
2736
  }
2736
- const parsed = safeParse(data);
2737
+ const parsed = safeJsonParse(data);
2737
2738
  if (parsed !== void 0) {
2738
2739
  consider(parsed);
2739
2740
  }
2740
2741
  }
2741
- function safeParse(text) {
2742
- try {
2743
- return JSON.parse(text);
2744
- } catch {
2745
- return void 0;
2746
- }
2747
- }
2748
2742
  function modelText(value) {
2749
2743
  return typeof value === "string" ? value.trim() : "";
2750
2744
  }
@@ -2820,6 +2814,10 @@ function formatNumber(value) {
2820
2814
  return Number.isInteger(value) ? value.toString() : String(value);
2821
2815
  }
2822
2816
 
2817
+ // src/server.ts
2818
+ import { createHash, timingSafeEqual } from "crypto";
2819
+ import { Elysia } from "elysia";
2820
+
2823
2821
  // src/dashboard.ts
2824
2822
  var DASHBOARD_HTML = `<!doctype html>
2825
2823
  <html lang="en">
@@ -3220,21 +3218,24 @@ footer.foot .end { margin-left:auto; }
3220
3218
  function mk(tag, cls, txt){ var e = document.createElement(tag); if (cls) e.className = cls; if (txt !== undefined && txt !== null) e.textContent = txt; return e; }
3221
3219
 
3222
3220
  // Set numeric text and flash on discrete change.
3223
- function setNum(id, value, kind){
3221
+ function setNum(id, value, kind, num){
3224
3222
  var el = byId(id); if (!el) return;
3225
3223
  el.classList.remove("skel");
3226
3224
  var s = String(value);
3227
3225
  if (el.textContent !== s){
3228
3226
  el.textContent = s;
3227
+ // Compare on the raw number (num) when provided, so directional flash works
3228
+ // even when value is a pre-formatted display string.
3229
+ var n = (num !== undefined) ? num : value;
3229
3230
  var prev = lastRender[id];
3230
3231
  if (prev !== undefined){
3231
3232
  var cls = "flash";
3232
- if (kind === "delta" && typeof value === "number" && typeof prev === "number"){
3233
- cls = value > prev ? "flash-up" : (value < prev ? "flash-down" : null);
3233
+ if (kind === "delta" && typeof n === "number" && typeof prev === "number"){
3234
+ cls = n > prev ? "flash-up" : (n < prev ? "flash-down" : null);
3234
3235
  }
3235
3236
  if (cls){ el.classList.remove("flash","flash-up","flash-down"); void el.offsetWidth; el.classList.add(cls); }
3236
3237
  }
3237
- lastRender[id] = value;
3238
+ lastRender[id] = n;
3238
3239
  }
3239
3240
  }
3240
3241
  function setText(id, s){ var el = byId(id); if (el){ el.classList.remove("skel"); el.textContent = s; } }
@@ -3315,7 +3316,7 @@ footer.foot .end { margin-left:auto; }
3315
3316
  if (beat && dot){ dot.classList.remove("heartbeat"); void dot.offsetWidth; dot.classList.add("heartbeat"); }
3316
3317
  }
3317
3318
  function showBanner(text, ok){
3318
- var b = byId("banner"); b.textContent = text; b.className = "banner show" + (ok ? " ok" : ""); b.classList.add("show");
3319
+ var b = byId("banner"); b.textContent = text; b.className = "show" + (ok ? " ok" : "");
3319
3320
  if (ok){ setTimeout(function(){ b.classList.remove("show"); }, 2000); }
3320
3321
  }
3321
3322
  function hideBanner(){ byId("banner").classList.remove("show"); }
@@ -3422,7 +3423,7 @@ footer.foot .end { margin-left:auto; }
3422
3423
  if (isFinite(reqPerSec)){ pushHist(hist.req, reqPerSec); setNum("req-num", rate(reqPerSec)); } else setText("req-num","\\u2014");
3423
3424
  if (isFinite(tokPerSec)){ pushHist(hist.tok, tokPerSec); setNum("tok-num", humanInt(tokPerSec)); } else setText("tok-num","\\u2014");
3424
3425
  var inflight = proxy.inFlight || 0;
3425
- pushHist(hist.inflight, inflight); setNum("inflight-num", String(inflight), "delta");
3426
+ pushHist(hist.inflight, inflight); setNum("inflight-num", String(inflight), "delta", inflight);
3426
3427
  byId("v-inflight").classList.toggle("active", inflight > 0);
3427
3428
  setText("uptime-num", fmtUptime(proxy.uptimeSeconds || 0));
3428
3429
 
@@ -3565,7 +3566,7 @@ footer.foot .end { margin-left:auto; }
3565
3566
 
3566
3567
  function renderUpstream(up, delta, restarted){
3567
3568
  setNum("up-total", humanInt(up.total||0));
3568
- setNum("up-errors", humanInt(up.errors||0), "delta");
3569
+ setNum("up-errors", humanInt(up.errors||0), "delta", up.errors||0);
3569
3570
  var er = up.total ? (up.errors/up.total*100) : 0;
3570
3571
  var rt = byId("up-rate"); rt.textContent = pct(er); rt.className = "v rate " + (er > 5 ? "danger" : er > 1 ? "warn" : "ok");
3571
3572
  byId("up-errblk").classList.toggle("hot", (up.errors||0) > 0);
@@ -3639,8 +3640,9 @@ async function getVersion() {
3639
3640
  resolved = BAKED_VERSION;
3640
3641
  } else {
3641
3642
  try {
3642
- const manifest = await Bun.file(new URL("../package.json", import.meta.url)).json();
3643
- resolved = typeof manifest.version === "string" ? manifest.version : "0.0.0";
3643
+ const manifest = asRecord(await Bun.file(new URL("../package.json", import.meta.url)).json());
3644
+ const version = manifest.version;
3645
+ resolved = typeof version === "string" ? version : "0.0.0";
3644
3646
  } catch {
3645
3647
  resolved = "0.0.0";
3646
3648
  }
@@ -3666,6 +3668,18 @@ var RequestBodyTooLargeError = class extends Error {
3666
3668
  this.name = "RequestBodyTooLargeError";
3667
3669
  }
3668
3670
  };
3671
+ var InvalidJsonError = class extends Error {
3672
+ constructor() {
3673
+ super(INVALID_JSON_MESSAGE);
3674
+ this.name = "InvalidJsonError";
3675
+ }
3676
+ };
3677
+ var JsonNotObjectError = class extends Error {
3678
+ constructor() {
3679
+ super(JSON_OBJECT_MESSAGE);
3680
+ this.name = "JsonNotObjectError";
3681
+ }
3682
+ };
3669
3683
  function createHoopilotHandler(options = {}) {
3670
3684
  const client = new CopilotClient(options);
3671
3685
  const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
@@ -3675,8 +3689,19 @@ function createHoopilotHandler(options = {}) {
3675
3689
  const readUsage = createUsageReader(client, metrics);
3676
3690
  const recordTokens = (model, usage) => metrics.recordTokens(model, usage);
3677
3691
  const recordExtraction = (extracted) => metrics.recordTokenExtraction(extracted);
3678
- const streamingProxyMode = resolveStreamingProxyMode(options);
3679
- const bufferProxyBodies = shouldBufferProxyBodies(streamingProxyMode);
3692
+ const bufferProxyBodies = shouldBufferProxyBodies(resolveStreamingProxyMode(options));
3693
+ const requestContext = /* @__PURE__ */ new WeakMap();
3694
+ const app = buildApp({
3695
+ apiKey,
3696
+ allowedOrigins,
3697
+ bufferProxyBodies,
3698
+ client,
3699
+ metrics,
3700
+ readUsage,
3701
+ recordExtraction,
3702
+ recordTokens,
3703
+ requestContext
3704
+ });
3680
3705
  return async (request) => {
3681
3706
  const startedAt = performance.now();
3682
3707
  const url = new URL(request.url);
@@ -3692,7 +3717,24 @@ function createHoopilotHandler(options = {}) {
3692
3717
  metrics.startRequest();
3693
3718
  const origin = request.headers.get("origin")?.trim() || void 0;
3694
3719
  const corsOrigin = resolveCorsAllowOrigin(origin, allowedOrigins);
3695
- const finish = (response) => finishResponse(response, {
3720
+ const inner = normalizeInnerRequest(request, apiPath, url);
3721
+ requestContext.set(inner, {
3722
+ apiPath,
3723
+ logger: requestLogger,
3724
+ origin,
3725
+ originalPath: url.pathname
3726
+ });
3727
+ let response;
3728
+ try {
3729
+ response = await app.handle(inner);
3730
+ } catch (error) {
3731
+ requestLogger.error(
3732
+ { err: errorDetails(error), event: "http.request.failed" },
3733
+ "request failed"
3734
+ );
3735
+ response = jsonError(500, "internal_error", errorMessage(error));
3736
+ }
3737
+ return finishResponse(response, {
3696
3738
  corsOrigin,
3697
3739
  logger: requestLogger,
3698
3740
  method: request.method,
@@ -3703,144 +3745,175 @@ function createHoopilotHandler(options = {}) {
3703
3745
  closeConnection: bufferProxyBodies,
3704
3746
  trackStreamingBody: !bufferProxyBodies
3705
3747
  });
3748
+ };
3749
+ }
3750
+ function buildApp(deps) {
3751
+ const {
3752
+ apiKey,
3753
+ allowedOrigins,
3754
+ bufferProxyBodies,
3755
+ client,
3756
+ metrics,
3757
+ readUsage,
3758
+ recordExtraction,
3759
+ recordTokens,
3760
+ requestContext
3761
+ } = deps;
3762
+ const contextFor = (request) => {
3763
+ const stored = requestContext.get(request);
3764
+ if (stored) {
3765
+ return stored;
3766
+ }
3767
+ const originalPath = new URL(request.url).pathname;
3768
+ return {
3769
+ apiPath: canonicalApiPath(originalPath),
3770
+ logger: noopLogger,
3771
+ origin: request.headers.get("origin")?.trim() || void 0,
3772
+ originalPath
3773
+ };
3774
+ };
3775
+ const loggerFor = (request) => contextFor(request).logger;
3776
+ const noBody = { parse: "none" };
3777
+ return new Elysia().onRequest(({ request }) => {
3778
+ const { apiPath, logger, origin } = contextFor(request);
3706
3779
  const browserOrigin = forbiddenBrowserOrigin(origin, request, allowedOrigins);
3707
3780
  if (browserOrigin) {
3708
- requestLogger.warn(
3781
+ logger.warn(
3709
3782
  { event: "http.request.forbidden_origin", origin: browserOrigin },
3710
3783
  "blocked cross-origin browser request"
3711
3784
  );
3712
- return finish(jsonError(403, "forbidden_origin", FORBIDDEN_BROWSER_ORIGIN_MESSAGE));
3785
+ return jsonError(403, "forbidden_origin", FORBIDDEN_BROWSER_ORIGIN_MESSAGE);
3713
3786
  }
3714
3787
  if (request.method === "OPTIONS") {
3715
- return finish(new Response(null, { headers: corsHeaders() }));
3788
+ return new Response(null, { headers: corsHeaders() });
3716
3789
  }
3717
3790
  if (request.method === "GET" && apiPath === "/dashboard") {
3718
- return finish(dashboardResponse());
3791
+ return dashboardResponse();
3719
3792
  }
3720
3793
  if (!isAuthorized(request, apiKey)) {
3721
- requestLogger.warn({ event: "http.request.unauthorized" }, "invalid hoopilot api key");
3722
- return finish(jsonError(401, "invalid_api_key", "Invalid or missing Hoopilot API key."));
3794
+ logger.warn({ event: "http.request.unauthorized" }, "invalid hoopilot api key");
3795
+ return jsonError(401, "invalid_api_key", "Invalid or missing Hoopilot API key.");
3723
3796
  }
3724
- try {
3725
- if (request.method === "GET" && (apiPath === "/" || apiPath === "/healthz")) {
3726
- return finish(jsonResponse({ name: "hoopilot", object: "health", status: "ok" }));
3727
- }
3728
- if (request.method === "GET" && apiPath === "/metrics") {
3729
- return finish(metricsResponse(metrics));
3730
- }
3731
- if (request.method === "GET" && apiPath === "/v1/usage") {
3732
- return finish(await handleUsage(metrics, readUsage, request.signal));
3733
- }
3734
- if (request.method === "GET" && apiPath === "/v1/responses") {
3735
- return finish(websocketUnsupportedResponse());
3736
- }
3737
- if (request.method === "GET" && apiPath === "/v1/models") {
3738
- return finish(await handleModels(client, metrics, request.signal, requestLogger));
3739
- }
3740
- if (request.method === "POST" && apiPath === "/v1/messages") {
3741
- return finish(
3742
- await handleAnthropicMessages(
3743
- client,
3744
- metrics,
3745
- recordTokens,
3746
- recordExtraction,
3747
- request,
3748
- requestLogger,
3749
- bufferProxyBodies
3750
- )
3751
- );
3752
- }
3753
- if (request.method === "POST" && apiPath === "/v1/messages/count_tokens") {
3754
- return finish(handleAnthropicCountTokens(await readJson(request)));
3755
- }
3756
- if (request.method === "POST" && apiPath === "/v1/chat/completions") {
3757
- return finish(
3758
- await handleChatCompletions(
3759
- client,
3760
- metrics,
3761
- recordTokens,
3762
- recordExtraction,
3763
- request,
3764
- requestLogger,
3765
- bufferProxyBodies
3766
- )
3767
- );
3768
- }
3769
- if (request.method === "POST" && apiPath === "/v1/completions") {
3770
- return finish(
3771
- await handleCompletions(
3772
- client,
3773
- metrics,
3774
- recordTokens,
3775
- recordExtraction,
3776
- request,
3777
- requestLogger,
3778
- bufferProxyBodies
3779
- )
3780
- );
3781
- }
3782
- if (request.method === "POST" && apiPath === "/v1/responses/compact") {
3783
- return finish(
3784
- await handleResponsesCompact(
3785
- client,
3786
- metrics,
3787
- recordTokens,
3788
- recordExtraction,
3789
- request,
3790
- requestLogger
3791
- )
3792
- );
3793
- }
3794
- if (request.method === "POST" && apiPath === "/v1/responses") {
3795
- return finish(
3796
- await handleResponses(
3797
- client,
3798
- metrics,
3799
- recordTokens,
3800
- recordExtraction,
3801
- request,
3802
- requestLogger,
3803
- bufferProxyBodies
3804
- )
3805
- );
3806
- }
3807
- return finish(jsonError(404, "not_found", `No route for ${request.method} ${url.pathname}.`));
3808
- } catch (error) {
3809
- if (error instanceof CopilotAuthError) {
3810
- requestLogger.warn(
3811
- { err: errorDetails(error), event: "copilot.auth.missing" },
3812
- "copilot auth failed"
3813
- );
3814
- return finish(jsonError(401, "copilot_auth_error", error.message));
3815
- }
3816
- const message = errorMessage(error);
3817
- if (message === INVALID_JSON_MESSAGE || message === JSON_OBJECT_MESSAGE) {
3818
- requestLogger.warn(
3819
- { err: errorDetails(error), event: "http.request.failed" },
3820
- "request body was not usable json"
3821
- );
3822
- return finish(jsonError(400, "invalid_request_error", message));
3823
- } else if (error instanceof OpenAICompatibilityError || error instanceof AnthropicCompatibilityError) {
3824
- requestLogger.warn(
3825
- { err: errorDetails(error), event: "http.request.failed" },
3826
- "request body used unsupported compatibility fields"
3827
- );
3828
- return finish(jsonError(400, "invalid_request_error", message));
3829
- } else if (error instanceof RequestBodyTooLargeError) {
3830
- requestLogger.warn(
3831
- { err: errorDetails(error), event: "http.request.failed" },
3832
- "request body exceeded size limit"
3833
- );
3834
- return finish(jsonError(413, "request_too_large", message));
3835
- } else {
3836
- requestLogger.error(
3837
- { err: errorDetails(error), event: "http.request.failed" },
3838
- "request failed"
3839
- );
3840
- }
3841
- return finish(jsonError(500, "internal_error", message));
3797
+ }).onError(({ code, error, request }) => {
3798
+ const { logger, originalPath } = contextFor(request);
3799
+ if (code === "NOT_FOUND") {
3800
+ return jsonError(404, "not_found", `No route for ${request.method} ${originalPath}.`);
3801
+ }
3802
+ if (error instanceof CopilotAuthError) {
3803
+ logger.warn(
3804
+ { err: errorDetails(error), event: "copilot.auth.missing" },
3805
+ "copilot auth failed"
3806
+ );
3807
+ return jsonError(401, "copilot_auth_error", error.message);
3808
+ }
3809
+ const message = errorMessage(error);
3810
+ if (error instanceof InvalidJsonError || error instanceof JsonNotObjectError) {
3811
+ logger.warn(
3812
+ { err: errorDetails(error), event: "http.request.failed" },
3813
+ "request body was not usable json"
3814
+ );
3815
+ return jsonError(400, "invalid_request_error", message);
3816
+ }
3817
+ if (error instanceof OpenAICompatibilityError || error instanceof AnthropicCompatibilityError) {
3818
+ logger.warn(
3819
+ { err: errorDetails(error), event: "http.request.failed" },
3820
+ "request body used unsupported compatibility fields"
3821
+ );
3822
+ return jsonError(400, "invalid_request_error", message);
3842
3823
  }
3824
+ if (error instanceof RequestBodyTooLargeError) {
3825
+ logger.warn(
3826
+ { err: errorDetails(error), event: "http.request.failed" },
3827
+ "request body exceeded size limit"
3828
+ );
3829
+ return jsonError(413, "request_too_large", message);
3830
+ }
3831
+ logger.error({ err: errorDetails(error), event: "http.request.failed" }, "request failed");
3832
+ return jsonError(500, "internal_error", message);
3833
+ }).get("/", () => jsonResponse({ name: "hoopilot", object: "health", status: "ok" })).get("/healthz", () => jsonResponse({ name: "hoopilot", object: "health", status: "ok" })).get("/metrics", () => metricsResponse(metrics)).get("/v1/usage", ({ request }) => handleUsage(metrics, readUsage, request.signal)).get(
3834
+ "/v1/models",
3835
+ ({ request }) => handleModels(client, metrics, request.signal, loggerFor(request))
3836
+ ).get("/v1/responses", () => websocketUnsupportedResponse()).post(
3837
+ "/v1/messages",
3838
+ ({ request }) => handleAnthropicMessages(
3839
+ client,
3840
+ metrics,
3841
+ recordTokens,
3842
+ recordExtraction,
3843
+ request,
3844
+ loggerFor(request),
3845
+ bufferProxyBodies
3846
+ ),
3847
+ noBody
3848
+ ).post(
3849
+ "/v1/messages/count_tokens",
3850
+ ({ request }) => handleAnthropicCountTokens(request),
3851
+ noBody
3852
+ ).post(
3853
+ "/v1/chat/completions",
3854
+ ({ request }) => handleChatCompletions(
3855
+ client,
3856
+ metrics,
3857
+ recordTokens,
3858
+ recordExtraction,
3859
+ request,
3860
+ loggerFor(request),
3861
+ bufferProxyBodies
3862
+ ),
3863
+ noBody
3864
+ ).post(
3865
+ "/v1/completions",
3866
+ ({ request }) => handleCompletions(
3867
+ client,
3868
+ metrics,
3869
+ recordTokens,
3870
+ recordExtraction,
3871
+ request,
3872
+ loggerFor(request),
3873
+ bufferProxyBodies
3874
+ ),
3875
+ noBody
3876
+ ).post(
3877
+ "/v1/responses/compact",
3878
+ ({ request }) => handleResponsesCompact(
3879
+ client,
3880
+ metrics,
3881
+ recordTokens,
3882
+ recordExtraction,
3883
+ request,
3884
+ loggerFor(request)
3885
+ ),
3886
+ noBody
3887
+ ).post(
3888
+ "/v1/responses",
3889
+ ({ request }) => handleResponses(
3890
+ client,
3891
+ metrics,
3892
+ recordTokens,
3893
+ recordExtraction,
3894
+ request,
3895
+ loggerFor(request),
3896
+ bufferProxyBodies
3897
+ ),
3898
+ noBody
3899
+ );
3900
+ }
3901
+ function normalizeInnerRequest(request, canonicalPath, url) {
3902
+ if (canonicalPath === url.pathname) {
3903
+ return request;
3904
+ }
3905
+ const target = new URL(url);
3906
+ target.pathname = canonicalPath;
3907
+ const init = {
3908
+ headers: request.headers,
3909
+ method: request.method,
3910
+ signal: request.signal
3843
3911
  };
3912
+ if (request.body) {
3913
+ init.body = request.body;
3914
+ init.duplex = "half";
3915
+ }
3916
+ return new Request(target, init);
3844
3917
  }
3845
3918
  function startHoopilotServer(options = {}) {
3846
3919
  const host = options.host ?? envValue(options.env?.HOST) ?? DEFAULT_HOST;
@@ -3919,7 +3992,8 @@ async function handleAnthropicMessages(client, metrics, recordTokens, recordExtr
3919
3992
  recordExtraction(usage !== void 0);
3920
3993
  return jsonResponse(responsesResponseToAnthropicMessage(body, model));
3921
3994
  }
3922
- function handleAnthropicCountTokens(body) {
3995
+ async function handleAnthropicCountTokens(request) {
3996
+ const body = await readJson(request);
3923
3997
  return jsonResponse(estimateAnthropicMessageTokens(body));
3924
3998
  }
3925
3999
  async function handleModels(client, metrics, signal, logger) {
@@ -4005,14 +4079,14 @@ async function handleCompletions(client, metrics, recordTokens, recordExtraction
4005
4079
  return jsonResponse(chatCompletionToCompletion(completion));
4006
4080
  }
4007
4081
  async function handleResponses(client, metrics, recordTokens, recordExtraction, request, logger, bufferProxyBodies) {
4008
- const body = await readJsonText(request);
4082
+ const { json, text: body } = await readJsonText(request);
4009
4083
  const upstream = await client.responses(body, request.signal);
4010
4084
  metrics.recordUpstream("/responses", upstream.ok);
4011
4085
  if (!upstream.ok) {
4012
4086
  return proxyError(upstream, logger);
4013
4087
  }
4014
4088
  logUpstreamSuccess(logger, "/responses", upstream.status);
4015
- const model = normalizeRequestedModel(asRecord(safeParseJson(body)).model);
4089
+ const model = normalizeRequestedModel(json.model);
4016
4090
  return proxyResponse(
4017
4091
  await responseWithObservedUsage(
4018
4092
  upstream,
@@ -4100,17 +4174,16 @@ function parseJsonObject2(text) {
4100
4174
  try {
4101
4175
  parsed = JSON.parse(text);
4102
4176
  } catch {
4103
- throw new Error(INVALID_JSON_MESSAGE);
4177
+ throw new InvalidJsonError();
4104
4178
  }
4105
4179
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4106
- throw new Error(JSON_OBJECT_MESSAGE);
4180
+ throw new JsonNotObjectError();
4107
4181
  }
4108
4182
  return parsed;
4109
4183
  }
4110
4184
  async function readJsonText(request) {
4111
4185
  const text = await readRequestText(request);
4112
- parseJsonObject2(text);
4113
- return text;
4186
+ return { json: parseJsonObject2(text), text };
4114
4187
  }
4115
4188
  async function readRequestText(request) {
4116
4189
  const contentLength = request.headers.get("content-length");
@@ -4168,7 +4241,7 @@ function jsonError(status, code, message) {
4168
4241
  );
4169
4242
  }
4170
4243
  function upstreamErrorResponse(status, text) {
4171
- const parsedError = asRecord(asRecord(safeParseJson(text)).error);
4244
+ const parsedError = asRecord(asRecord(safeJsonParse(text)).error);
4172
4245
  if (Object.keys(parsedError).length > 0) {
4173
4246
  return jsonResponse({ error: parsedError }, status);
4174
4247
  }
@@ -4190,13 +4263,18 @@ function corsHeaders() {
4190
4263
  "access-control-expose-headers": "x-request-id"
4191
4264
  };
4192
4265
  }
4266
+ function secretEquals(candidate, secret) {
4267
+ const a = createHash("sha256").update(candidate).digest();
4268
+ const b = createHash("sha256").update(secret).digest();
4269
+ return timingSafeEqual(a, b);
4270
+ }
4193
4271
  function isAuthorized(request, apiKey) {
4194
4272
  if (!apiKey) {
4195
4273
  return true;
4196
4274
  }
4197
4275
  const authorization = request.headers.get("authorization") ?? "";
4198
4276
  const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
4199
- return bearer === apiKey || request.headers.get("x-api-key") === apiKey;
4277
+ return bearer !== void 0 && secretEquals(bearer, apiKey) || secretEquals(request.headers.get("x-api-key") ?? "", apiKey);
4200
4278
  }
4201
4279
  function forbiddenBrowserOrigin(origin, request, allowedOrigins) {
4202
4280
  if (origin) {
@@ -4233,7 +4311,7 @@ function upstreamAuthMessage(message) {
4233
4311
  return `GitHub Copilot rejected the credential or account access: ${message}`;
4234
4312
  }
4235
4313
  function isLoopbackHost(host) {
4236
- return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
4314
+ return isLoopbackHostname(host);
4237
4315
  }
4238
4316
  function urlHost(host) {
4239
4317
  return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
@@ -4252,9 +4330,6 @@ function normalizeServerPort(value) {
4252
4330
  }
4253
4331
  return port;
4254
4332
  }
4255
- function errorMessage(error) {
4256
- return error instanceof Error ? error.message : String(error);
4257
- }
4258
4333
  function serverLogger(options) {
4259
4334
  if (options.logger) {
4260
4335
  return options.logger.child({ component: "server" });
@@ -4270,10 +4345,7 @@ function serverLogger(options) {
4270
4345
  }
4271
4346
  function resolveStreamingProxyMode(options) {
4272
4347
  const value = options.streamingProxyMode ?? envValue(options.env?.HOOPILOT_STREAM_MODE) ?? envValue(options.env?.HOOPILOT_STREAMING_PROXY_MODE) ?? "auto";
4273
- if (value === "auto" || value === "buffer" || value === "live") {
4274
- return value;
4275
- }
4276
- throw new Error(`Invalid stream mode: ${value}. Expected auto, live, or buffer.`);
4348
+ return parseStreamingProxyMode(value);
4277
4349
  }
4278
4350
  function shouldBufferProxyBodies(mode) {
4279
4351
  if (mode === "buffer") {
@@ -4331,11 +4403,13 @@ function responseWithRequestId(response, requestId, closeConnection, corsOrigin)
4331
4403
  function trackStreamCompletion(body, onComplete) {
4332
4404
  const reader = body.getReader();
4333
4405
  let fired = false;
4334
- const fire = () => {
4335
- if (!fired) {
4336
- fired = true;
4337
- onComplete();
4406
+ const release = () => {
4407
+ if (fired) {
4408
+ return;
4338
4409
  }
4410
+ fired = true;
4411
+ onComplete();
4412
+ reader.releaseLock();
4339
4413
  };
4340
4414
  return new ReadableStream({
4341
4415
  async pull(controller) {
@@ -4343,18 +4417,25 @@ function trackStreamCompletion(body, onComplete) {
4343
4417
  const { done, value } = await reader.read();
4344
4418
  if (done) {
4345
4419
  controller.close();
4346
- fire();
4420
+ release();
4347
4421
  return;
4348
4422
  }
4349
4423
  controller.enqueue(value);
4350
4424
  } catch (error) {
4351
- fire();
4425
+ release();
4352
4426
  controller.error(error);
4353
4427
  }
4354
4428
  },
4355
- cancel(reason) {
4356
- fire();
4357
- return reader.cancel(reason);
4429
+ async cancel(reason) {
4430
+ if (!fired) {
4431
+ fired = true;
4432
+ onComplete();
4433
+ }
4434
+ try {
4435
+ await reader.cancel(reason);
4436
+ } finally {
4437
+ reader.releaseLock();
4438
+ }
4358
4439
  }
4359
4440
  });
4360
4441
  }
@@ -4402,47 +4483,26 @@ function canonicalApiPath(path) {
4402
4483
  return withoutTrailingSlash;
4403
4484
  }
4404
4485
  }
4486
+ var API_ROUTES = [
4487
+ { method: "GET", path: "/", name: "health" },
4488
+ { method: "GET", path: "/healthz", name: "health" },
4489
+ { method: "GET", path: "/dashboard", name: "dashboard" },
4490
+ { method: "GET", path: "/metrics", name: "metrics" },
4491
+ { method: "GET", path: "/v1/usage", name: "usage" },
4492
+ { method: "GET", path: "/v1/models", name: "models" },
4493
+ { method: "GET", path: "/v1/responses", name: "responses_websocket" },
4494
+ { method: "POST", path: "/v1/messages", name: "anthropic_messages" },
4495
+ { method: "POST", path: "/v1/messages/count_tokens", name: "anthropic_count_tokens" },
4496
+ { method: "POST", path: "/v1/chat/completions", name: "chat_completions" },
4497
+ { method: "POST", path: "/v1/completions", name: "completions" },
4498
+ { method: "POST", path: "/v1/responses/compact", name: "responses_compact" },
4499
+ { method: "POST", path: "/v1/responses", name: "responses" }
4500
+ ];
4405
4501
  function routeFor(method, path) {
4406
4502
  if (method === "OPTIONS") {
4407
4503
  return "cors.preflight";
4408
4504
  }
4409
- if (method === "GET" && (path === "/" || path === "/healthz")) {
4410
- return "health";
4411
- }
4412
- if (method === "GET" && path === "/dashboard") {
4413
- return "dashboard";
4414
- }
4415
- if (method === "GET" && path === "/metrics") {
4416
- return "metrics";
4417
- }
4418
- if (method === "GET" && path === "/v1/usage") {
4419
- return "usage";
4420
- }
4421
- if (method === "GET" && path === "/v1/models") {
4422
- return "models";
4423
- }
4424
- if (method === "POST" && path === "/v1/messages") {
4425
- return "anthropic_messages";
4426
- }
4427
- if (method === "POST" && path === "/v1/messages/count_tokens") {
4428
- return "anthropic_count_tokens";
4429
- }
4430
- if (method === "POST" && path === "/v1/chat/completions") {
4431
- return "chat_completions";
4432
- }
4433
- if (method === "POST" && path === "/v1/completions") {
4434
- return "completions";
4435
- }
4436
- if (method === "POST" && path === "/v1/responses/compact") {
4437
- return "responses_compact";
4438
- }
4439
- if (method === "POST" && path === "/v1/responses") {
4440
- return "responses";
4441
- }
4442
- if (method === "GET" && path === "/v1/responses") {
4443
- return "responses_websocket";
4444
- }
4445
- return "not_found";
4505
+ return API_ROUTES.find((entry) => entry.method === method && entry.path === path)?.name ?? "not_found";
4446
4506
  }
4447
4507
  function isStreamingResponse(response) {
4448
4508
  return response.headers.get("content-type")?.includes("text/event-stream") ?? false;
@@ -4520,24 +4580,19 @@ function createUsageReader(client, metrics, now = Date.now, ttlMs = USAGE_CACHE_
4520
4580
  }
4521
4581
  };
4522
4582
  }
4523
- function safeParseJson(text) {
4524
- try {
4525
- return JSON.parse(text);
4526
- } catch {
4527
- return void 0;
4528
- }
4529
- }
4530
4583
  export {
4531
4584
  AnthropicCompatibilityError,
4532
4585
  COPILOT_USAGE_API_VERSION,
4533
4586
  CopilotAuth,
4534
4587
  CopilotAuthError,
4535
4588
  CopilotClient,
4589
+ DEFAULT_COPILOT_API_BASE_URL,
4536
4590
  DEFAULT_GITHUB_API_BASE_URL,
4537
4591
  DEFAULT_LOG_FORMAT,
4538
4592
  DEFAULT_LOG_LEVEL,
4539
4593
  DEFAULT_MODEL,
4540
4594
  MetricsRegistry,
4595
+ OpenAICompatibilityError,
4541
4596
  PROMETHEUS_CONTENT_TYPE,
4542
4597
  anthropicMessagesToResponsesRequest,
4543
4598
  applyCopilotHeaders,
@@ -4563,6 +4618,7 @@ export {
4563
4618
  parseLogLevel,
4564
4619
  parseRateLimitHeaders,
4565
4620
  readStoredCopilotAuth,
4621
+ recordResponseTextUsage,
4566
4622
  responsesCompactionResult,
4567
4623
  responsesRequestToChatCompletion,
4568
4624
  responsesResponseToAnthropicMessage,