@openhoo/hoopilot 2.1.2 → 2.1.4

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/codexx.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  buildCodexxInvocation,
4
4
  main,
5
5
  verifyCodexxModel
6
- } from "./chunk-CYR6I4C3.js";
6
+ } from "./chunk-4ZG5QEYJ.js";
7
7
  export {
8
8
  buildCodexxInvocation,
9
9
  main,
package/dist/index.d.ts CHANGED
@@ -93,6 +93,8 @@ interface CopilotAuthOptions {
93
93
  env?: NodeJS.ProcessEnv;
94
94
  fetch?: FetchLike;
95
95
  githubApiBaseUrl?: string;
96
+ upstreamStreamIdleTimeoutMs?: number;
97
+ upstreamTimeoutMs?: number;
96
98
  }
97
99
  interface CopilotAccess {
98
100
  apiBaseUrl: string;
package/dist/index.js CHANGED
@@ -195,7 +195,7 @@ function chatCompletionToResponse(completion, responseId) {
195
195
  }
196
196
  function responsesCompactionResult(upstreamText, isSse) {
197
197
  const summary = compactionSummaryText(upstreamText, isSse);
198
- return { output: [compactionSummaryMessageItem(summary)] };
198
+ return { output: [compactionSummaryOutputMessageItem(summary)] };
199
199
  }
200
200
  function isResponsesCompactionRequest(request) {
201
201
  return responseInputItems(request.input).some(
@@ -839,8 +839,14 @@ function messageOutputItem(text, id = `msg_${randomId()}`) {
839
839
  type: "message"
840
840
  };
841
841
  }
842
- function compactionSummaryMessageItem(text, id = `msg_${randomId()}`) {
843
- return {
842
+ function compactionSummaryOutputMessageItem(text) {
843
+ return compactionSummaryMessageItem(text, `msg_${randomId()}`);
844
+ }
845
+ function compactionSummaryInputMessageItem(text) {
846
+ return compactionSummaryMessageItem(text);
847
+ }
848
+ function compactionSummaryMessageItem(text, id) {
849
+ return removeUndefined({
844
850
  content: [
845
851
  {
846
852
  text: `${COMPACTION_SUMMARY_PREFIX}
@@ -851,7 +857,7 @@ ${text}`,
851
857
  id,
852
858
  role: "user",
853
859
  type: "message"
854
- };
860
+ });
855
861
  }
856
862
  function compactionOutputItem(text, id = `cmpct_${randomId()}`) {
857
863
  return {
@@ -875,7 +881,7 @@ function normalizeCompactionInputForCopilot(input, options) {
875
881
  if (type === "compaction" || type === "compaction_summary" || type === "context_compaction") {
876
882
  const text = contentToText(record.encrypted_content);
877
883
  if (text) {
878
- normalized.push(compactionSummaryMessageItem(text));
884
+ normalized.push(compactionSummaryInputMessageItem(text));
879
885
  }
880
886
  continue;
881
887
  }
@@ -956,6 +962,7 @@ function extractTokenUsage(usage) {
956
962
  asRecord(record.output_tokens_details).reasoning_tokens
957
963
  );
958
964
  const cached = firstNumber(
965
+ record.cache_read_input_tokens,
959
966
  asRecord(record.prompt_tokens_details).cached_tokens,
960
967
  asRecord(record.input_tokens_details).cached_tokens
961
968
  );
@@ -1128,9 +1135,10 @@ var AnthropicCompatibilityError = class extends Error {
1128
1135
  }
1129
1136
  };
1130
1137
  function anthropicMessagesToResponsesRequest(request) {
1131
- return removeUndefined({
1132
- input: anthropicMessagesToResponsesInput(request.messages),
1133
- instructions: anthropicSystemToInstructions(request.system),
1138
+ const system = anthropicSystemToResponses(request.system);
1139
+ const response = removeUndefined({
1140
+ input: [...system.input, ...anthropicMessagesToResponsesInput(request.messages)],
1141
+ instructions: system.instructions,
1134
1142
  max_output_tokens: typeof request.max_tokens === "number" && Number.isFinite(request.max_tokens) ? request.max_tokens : void 0,
1135
1143
  metadata: request.metadata,
1136
1144
  model: normalizeRequestedModel(request.model),
@@ -1143,6 +1151,8 @@ function anthropicMessagesToResponsesRequest(request) {
1143
1151
  tools: anthropicTools(request.tools),
1144
1152
  top_p: request.top_p
1145
1153
  });
1154
+ applyCacheControlToLastBlock(response, anthropicCacheControl(request.cache_control));
1155
+ return response;
1146
1156
  }
1147
1157
  function responsesResponseToAnthropicMessage(response, fallbackModel) {
1148
1158
  const content = anthropicContentFromResponsesOutput(response);
@@ -1239,6 +1249,7 @@ function anthropicMessagesToResponsesInput(messages) {
1239
1249
  throw new AnthropicCompatibilityError("Anthropic Messages requests require messages[].");
1240
1250
  }
1241
1251
  const input = [];
1252
+ let fallbackToolCallIndex = 0;
1242
1253
  for (const message of messages) {
1243
1254
  const record = asRecord(message);
1244
1255
  const role = anthropicRole(record.role);
@@ -1260,10 +1271,13 @@ function anthropicMessagesToResponsesInput(messages) {
1260
1271
  if (type === "text") {
1261
1272
  const text = textValue(part.text);
1262
1273
  if (text) {
1263
- messageParts.push({
1264
- text,
1265
- type: role === "assistant" ? "output_text" : "input_text"
1266
- });
1274
+ messageParts.push(
1275
+ removeUndefined({
1276
+ cache_control: anthropicCacheControl(part.cache_control),
1277
+ text,
1278
+ type: role === "assistant" ? "output_text" : "input_text"
1279
+ })
1280
+ );
1267
1281
  }
1268
1282
  continue;
1269
1283
  }
@@ -1278,21 +1292,27 @@ function anthropicMessagesToResponsesInput(messages) {
1278
1292
  }
1279
1293
  if (type === "tool_use") {
1280
1294
  flushMessage();
1281
- input.push({
1282
- arguments: JSON.stringify(asRecord(part.input)),
1283
- call_id: textValue(part.id) || `call_${randomId()}`,
1284
- name: textValue(part.name),
1285
- type: "function_call"
1286
- });
1295
+ input.push(
1296
+ removeUndefined({
1297
+ arguments: JSON.stringify(asRecord(part.input)),
1298
+ cache_control: anthropicCacheControl(part.cache_control),
1299
+ call_id: textValue(part.id) || `call_hoopilot_${fallbackToolCallIndex++}`,
1300
+ name: textValue(part.name),
1301
+ type: "function_call"
1302
+ })
1303
+ );
1287
1304
  continue;
1288
1305
  }
1289
1306
  if (type === "tool_result") {
1290
1307
  flushMessage();
1291
- input.push({
1292
- call_id: textValue(part.tool_use_id),
1293
- output: anthropicToolResultOutput(part.content),
1294
- type: "function_call_output"
1295
- });
1308
+ input.push(
1309
+ removeUndefined({
1310
+ cache_control: anthropicCacheControl(part.cache_control),
1311
+ call_id: textValue(part.tool_use_id),
1312
+ output: anthropicToolResultOutput(part.content),
1313
+ type: "function_call_output"
1314
+ })
1315
+ );
1296
1316
  continue;
1297
1317
  }
1298
1318
  if (type === "thinking" || type === "redacted_thinking") {
@@ -1339,22 +1359,24 @@ function anthropicImageToResponsesPart(part) {
1339
1359
  if (!data) {
1340
1360
  throw new AnthropicCompatibilityError("Anthropic base64 image content requires source.data.");
1341
1361
  }
1342
- return {
1362
+ return removeUndefined({
1363
+ cache_control: anthropicCacheControl(part.cache_control),
1343
1364
  detail: "auto",
1344
1365
  image_url: `data:${mediaType};base64,${data}`,
1345
1366
  type: "input_image"
1346
- };
1367
+ });
1347
1368
  }
1348
1369
  if (sourceType === "url") {
1349
1370
  const url = textValue(source.url);
1350
1371
  if (!url) {
1351
1372
  throw new AnthropicCompatibilityError("Anthropic URL image content requires source.url.");
1352
1373
  }
1353
- return {
1374
+ return removeUndefined({
1375
+ cache_control: anthropicCacheControl(part.cache_control),
1354
1376
  detail: "auto",
1355
1377
  image_url: url,
1356
1378
  type: "input_image"
1357
- };
1379
+ });
1358
1380
  }
1359
1381
  throw new AnthropicCompatibilityError(
1360
1382
  `Anthropic image source type "${sourceType || "unknown"}" is not supported.`
@@ -1375,15 +1397,42 @@ function anthropicToolResultOutput(content) {
1375
1397
  }
1376
1398
  return typeof content === "object" ? JSON.stringify(content) : String(content);
1377
1399
  }
1378
- function anthropicSystemToInstructions(system) {
1400
+ function anthropicSystemToResponses(system) {
1379
1401
  if (typeof system === "string") {
1380
- return system || void 0;
1402
+ return { input: [], instructions: system || void 0 };
1381
1403
  }
1382
1404
  if (!Array.isArray(system)) {
1405
+ return { input: [] };
1406
+ }
1407
+ const parts = system.map((part) => anthropicSystemPartToResponsesPart(part)).filter((part) => part !== void 0);
1408
+ if (parts.length === 0) {
1409
+ return { input: [] };
1410
+ }
1411
+ if (parts.some((part) => part.cache_control !== void 0)) {
1412
+ return {
1413
+ input: [
1414
+ {
1415
+ content: parts,
1416
+ role: "system",
1417
+ type: "message"
1418
+ }
1419
+ ]
1420
+ };
1421
+ }
1422
+ const text = parts.map((part) => textValue(part.text)).filter(Boolean).join("\n");
1423
+ return { input: [], instructions: text || void 0 };
1424
+ }
1425
+ function anthropicSystemPartToResponsesPart(part) {
1426
+ const record = asRecord(part);
1427
+ const text = textValue(record.text) || textValue(part);
1428
+ if (!text) {
1383
1429
  return void 0;
1384
1430
  }
1385
- const text = system.map((part) => textValue(asRecord(part).text) || textValue(part)).filter(Boolean).join("\n");
1386
- return text || void 0;
1431
+ return removeUndefined({
1432
+ cache_control: anthropicCacheControl(record.cache_control),
1433
+ text,
1434
+ type: "input_text"
1435
+ });
1387
1436
  }
1388
1437
  function anthropicTools(tools) {
1389
1438
  if (!Array.isArray(tools)) {
@@ -1392,6 +1441,7 @@ function anthropicTools(tools) {
1392
1441
  const converted = tools.map((tool) => {
1393
1442
  const record = asRecord(tool);
1394
1443
  return removeUndefined({
1444
+ cache_control: anthropicCacheControl(record.cache_control),
1395
1445
  description: record.description,
1396
1446
  name: record.name,
1397
1447
  parameters: record.input_schema,
@@ -1401,6 +1451,55 @@ function anthropicTools(tools) {
1401
1451
  });
1402
1452
  return converted.length > 0 ? converted : void 0;
1403
1453
  }
1454
+ function anthropicCacheControl(value) {
1455
+ if (value === void 0 || value === null) {
1456
+ return void 0;
1457
+ }
1458
+ const record = asRecord(value);
1459
+ const type = textValue(record.type);
1460
+ if (type !== "ephemeral") {
1461
+ throw new AnthropicCompatibilityError(
1462
+ `Anthropic cache_control type "${type || "unknown"}" is not supported.`
1463
+ );
1464
+ }
1465
+ const ttl = textValue(record.ttl);
1466
+ if (ttl && ttl !== "5m" && ttl !== "1h") {
1467
+ throw new AnthropicCompatibilityError(`Anthropic cache_control ttl "${ttl}" is not supported.`);
1468
+ }
1469
+ return removeUndefined({
1470
+ ttl: ttl || void 0,
1471
+ type
1472
+ });
1473
+ }
1474
+ function applyCacheControlToLastBlock(request, cacheControl) {
1475
+ if (!cacheControl) {
1476
+ return;
1477
+ }
1478
+ const input = Array.isArray(request.input) ? request.input : [];
1479
+ for (let itemIndex = input.length - 1; itemIndex >= 0; itemIndex -= 1) {
1480
+ const item = asRecord(input[itemIndex]);
1481
+ const content = Array.isArray(item.content) ? item.content : [];
1482
+ for (let partIndex = content.length - 1; partIndex >= 0; partIndex -= 1) {
1483
+ const part = asRecord(content[partIndex]);
1484
+ if (part.cache_control === void 0 && isCacheableResponsesPart(part)) {
1485
+ part.cache_control = cacheControl;
1486
+ return;
1487
+ }
1488
+ }
1489
+ }
1490
+ const tools = Array.isArray(request.tools) ? request.tools : [];
1491
+ for (let index = tools.length - 1; index >= 0; index -= 1) {
1492
+ const tool = asRecord(tools[index]);
1493
+ if (tool.cache_control === void 0) {
1494
+ tool.cache_control = cacheControl;
1495
+ return;
1496
+ }
1497
+ }
1498
+ }
1499
+ function isCacheableResponsesPart(part) {
1500
+ const type = textValue(part.type);
1501
+ return type === "input_text" || type === "output_text" || type === "text" || type === "input_image";
1502
+ }
1404
1503
  function anthropicToolChoice(toolChoice) {
1405
1504
  if (toolChoice === void 0 || toolChoice === null) {
1406
1505
  return void 0;
@@ -1906,6 +2005,14 @@ var COPILOT_USAGE_API_VERSION = "2025-04-01";
1906
2005
  var EDITOR_PLUGIN_VERSION = "hoopilot/0.1.0";
1907
2006
  var EDITOR_VERSION = "Hoopilot/0.1.0";
1908
2007
  var HOOPILOT_USER_AGENT = "hoopilot/0.1.0";
2008
+ var DEFAULT_UPSTREAM_TIMEOUT_MS = 12e4;
2009
+ var DEFAULT_UPSTREAM_STREAM_IDLE_TIMEOUT_MS = 12e4;
2010
+ var CopilotUpstreamTimeoutError = class extends Error {
2011
+ constructor(message) {
2012
+ super(message);
2013
+ this.name = "CopilotUpstreamTimeoutError";
2014
+ }
2015
+ };
1909
2016
  function applyCopilotHeaders(headers, token) {
1910
2017
  headers.set("accept", headers.get("accept") ?? "application/json");
1911
2018
  headers.set("authorization", `Bearer ${token}`);
@@ -1958,6 +2065,8 @@ var CopilotClient = class {
1958
2065
  #allowUnsafeUpstream;
1959
2066
  #fetch;
1960
2067
  #githubApiBaseUrl;
2068
+ #upstreamStreamIdleTimeoutMs;
2069
+ #upstreamTimeoutMs;
1961
2070
  constructor(options = {}) {
1962
2071
  this.#auth = new CopilotAuth(options);
1963
2072
  this.#allowUnsafeUpstream = envValue(options.env?.HOOPILOT_ALLOW_UNSAFE_UPSTREAM) === "1";
@@ -1965,6 +2074,18 @@ var CopilotClient = class {
1965
2074
  this.#githubApiBaseUrl = trimTrailingSlash(
1966
2075
  options.githubApiBaseUrl ?? envValue(options.env?.HOOPILOT_GITHUB_API_BASE_URL) ?? DEFAULT_GITHUB_API_BASE_URL
1967
2076
  );
2077
+ this.#upstreamTimeoutMs = parseTimeoutMs(
2078
+ options.upstreamTimeoutMs,
2079
+ options.env?.HOOPILOT_UPSTREAM_TIMEOUT_MS,
2080
+ DEFAULT_UPSTREAM_TIMEOUT_MS,
2081
+ "HOOPILOT_UPSTREAM_TIMEOUT_MS"
2082
+ );
2083
+ this.#upstreamStreamIdleTimeoutMs = parseTimeoutMs(
2084
+ options.upstreamStreamIdleTimeoutMs,
2085
+ options.env?.HOOPILOT_UPSTREAM_STREAM_IDLE_TIMEOUT_MS,
2086
+ DEFAULT_UPSTREAM_STREAM_IDLE_TIMEOUT_MS,
2087
+ "HOOPILOT_UPSTREAM_STREAM_IDLE_TIMEOUT_MS"
2088
+ );
1968
2089
  }
1969
2090
  /**
1970
2091
  * Fetch the Copilot account's quota / premium-request usage from the GitHub
@@ -1983,7 +2104,7 @@ var CopilotClient = class {
1983
2104
  }
1984
2105
  const access = await this.#auth.getAccess();
1985
2106
  const headers = applyGithubApiHeaders(new Headers(), access.token);
1986
- return this.#fetch(`${this.#githubApiBaseUrl}/copilot_internal/user`, {
2107
+ return this.#fetchWithTimeout(`${this.#githubApiBaseUrl}/copilot_internal/user`, {
1987
2108
  headers,
1988
2109
  method: "GET",
1989
2110
  signal
@@ -2030,12 +2151,139 @@ var CopilotClient = class {
2030
2151
  );
2031
2152
  }
2032
2153
  const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
2033
- return this.#fetch(`${access.apiBaseUrl}${path}`, {
2154
+ return this.#fetchWithTimeout(`${access.apiBaseUrl}${path}`, {
2034
2155
  ...init,
2035
2156
  headers
2036
2157
  });
2037
2158
  }
2159
+ async #fetchWithTimeout(input, init) {
2160
+ const timeout = abortSignalWithTimeout(init.signal ?? void 0, this.#upstreamTimeoutMs);
2161
+ try {
2162
+ const response = await this.#fetch(input, {
2163
+ ...init,
2164
+ signal: timeout.signal
2165
+ });
2166
+ return responseWithStreamIdleTimeout(response, this.#upstreamStreamIdleTimeoutMs, input);
2167
+ } catch (error) {
2168
+ if (timeout.timedOut()) {
2169
+ throw new CopilotUpstreamTimeoutError(
2170
+ `Copilot upstream request timed out after ${this.#upstreamTimeoutMs} ms before response headers arrived.`
2171
+ );
2172
+ }
2173
+ throw error;
2174
+ } finally {
2175
+ timeout.cleanup();
2176
+ }
2177
+ }
2038
2178
  };
2179
+ function parseTimeoutMs(optionValue, envRaw, fallback, name) {
2180
+ const raw = optionValue ?? envValue(envRaw);
2181
+ if (raw === void 0) {
2182
+ return fallback;
2183
+ }
2184
+ const value = typeof raw === "number" ? raw : Number(raw);
2185
+ if (!Number.isInteger(value) || value < 0) {
2186
+ throw new Error(`${name} must be a non-negative integer number of milliseconds.`);
2187
+ }
2188
+ return value;
2189
+ }
2190
+ function abortSignalWithTimeout(parent, timeoutMs) {
2191
+ if (timeoutMs === 0) {
2192
+ return { cleanup: () => {
2193
+ }, signal: parent, timedOut: () => false };
2194
+ }
2195
+ const controller = new AbortController();
2196
+ let timedOut = false;
2197
+ const timer = setTimeout(() => {
2198
+ if (controller.signal.aborted) {
2199
+ return;
2200
+ }
2201
+ timedOut = true;
2202
+ controller.abort(
2203
+ new CopilotUpstreamTimeoutError(`Copilot upstream request timed out after ${timeoutMs} ms.`)
2204
+ );
2205
+ }, timeoutMs);
2206
+ const onAbort = () => controller.abort(parent?.reason);
2207
+ if (parent?.aborted) {
2208
+ controller.abort(parent.reason);
2209
+ } else {
2210
+ parent?.addEventListener("abort", onAbort, { once: true });
2211
+ }
2212
+ return {
2213
+ cleanup: () => {
2214
+ clearTimeout(timer);
2215
+ parent?.removeEventListener("abort", onAbort);
2216
+ },
2217
+ signal: controller.signal,
2218
+ timedOut: () => timedOut
2219
+ };
2220
+ }
2221
+ function responseWithStreamIdleTimeout(response, idleTimeoutMs, input) {
2222
+ if (!response.body || idleTimeoutMs === 0) {
2223
+ return response;
2224
+ }
2225
+ return new Response(streamWithIdleTimeout(response.body, idleTimeoutMs, input), {
2226
+ headers: response.headers,
2227
+ status: response.status,
2228
+ statusText: response.statusText
2229
+ });
2230
+ }
2231
+ function streamWithIdleTimeout(body, idleTimeoutMs, input) {
2232
+ const reader = body.getReader();
2233
+ let released = false;
2234
+ const release = () => {
2235
+ if (!released) {
2236
+ released = true;
2237
+ reader.releaseLock();
2238
+ }
2239
+ };
2240
+ return new ReadableStream({
2241
+ async pull(controller) {
2242
+ let timer;
2243
+ const read = reader.read();
2244
+ read.catch(() => {
2245
+ });
2246
+ try {
2247
+ const result = await Promise.race([
2248
+ read,
2249
+ new Promise((_, reject) => {
2250
+ timer = setTimeout(() => {
2251
+ reject(
2252
+ new CopilotUpstreamTimeoutError(
2253
+ `Copilot upstream stream was idle for ${idleTimeoutMs} ms while reading ${input}.`
2254
+ )
2255
+ );
2256
+ }, idleTimeoutMs);
2257
+ })
2258
+ ]);
2259
+ if (timer) {
2260
+ clearTimeout(timer);
2261
+ }
2262
+ if (result.done) {
2263
+ controller.close();
2264
+ release();
2265
+ return;
2266
+ }
2267
+ controller.enqueue(result.value);
2268
+ } catch (error) {
2269
+ if (timer) {
2270
+ clearTimeout(timer);
2271
+ }
2272
+ await reader.cancel(error).catch(() => {
2273
+ });
2274
+ controller.error(error);
2275
+ release();
2276
+ }
2277
+ },
2278
+ async cancel(reason) {
2279
+ try {
2280
+ await reader.cancel(reason);
2281
+ } finally {
2282
+ release();
2283
+ }
2284
+ }
2285
+ });
2286
+ }
2039
2287
  function normalizeCopilotUsage(body) {
2040
2288
  const record = asRecord(body);
2041
2289
  const quotas = {};
@@ -3990,6 +4238,13 @@ function buildApp(deps) {
3990
4238
  );
3991
4239
  return jsonError(413, "request_too_large", message);
3992
4240
  }
4241
+ if (error instanceof CopilotUpstreamTimeoutError) {
4242
+ logger.warn(
4243
+ { err: errorDetails(error), event: "copilot.request.timeout" },
4244
+ "copilot upstream request timed out"
4245
+ );
4246
+ return jsonError(504, "copilot_timeout", message);
4247
+ }
3993
4248
  logger.error({ err: errorDetails(error), event: "http.request.failed" }, "request failed");
3994
4249
  return jsonError(500, "internal_error", message);
3995
4250
  }).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(