@openhoo/hoopilot 0.7.4 → 0.8.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/cli.js CHANGED
@@ -293,22 +293,31 @@ function normalizeCopilotUsage(body) {
293
293
  }
294
294
  function normalizeQuotaDetail(detail) {
295
295
  const entitlement = numberOrUndefined(detail.entitlement);
296
+ const overageCount = numberOrUndefined(detail.overage_count);
296
297
  const remaining = numberOrUndefined(detail.remaining) ?? numberOrUndefined(detail.quota_remaining);
297
298
  return removeUndefinedQuota({
298
299
  entitlement,
299
- overageCount: numberOrUndefined(detail.overage_count),
300
+ hasQuota: typeof detail.has_quota === "boolean" ? detail.has_quota : void 0,
301
+ overageCount,
302
+ overageEntitlement: numberOrUndefined(detail.overage_entitlement),
300
303
  overagePermitted: typeof detail.overage_permitted === "boolean" ? detail.overage_permitted : void 0,
301
304
  percentRemaining: numberOrUndefined(detail.percent_remaining),
305
+ quotaId: stringOrUndefined(detail.quota_id),
306
+ quotaResetAt: stringOrUndefined(detail.quota_reset_at),
302
307
  remaining,
308
+ timestampUtc: stringOrUndefined(detail.timestamp_utc),
309
+ tokenBasedBilling: typeof detail.token_based_billing === "boolean" ? detail.token_based_billing : void 0,
303
310
  unlimited: typeof detail.unlimited === "boolean" ? detail.unlimited : void 0,
304
- used: usedFrom(entitlement, remaining)
311
+ used: usedFrom(entitlement, remaining, overageCount)
305
312
  });
306
313
  }
307
- function usedFrom(entitlement, remaining) {
314
+ function usedFrom(entitlement, remaining, overageCount) {
308
315
  if (entitlement === void 0 || remaining === void 0) {
309
316
  return void 0;
310
317
  }
311
- return Math.max(0, entitlement - remaining);
318
+ const base = entitlement - remaining;
319
+ const overage = remaining === 0 ? overageCount ?? 0 : 0;
320
+ return Math.max(0, base + overage);
312
321
  }
313
322
  function numberOrUndefined(value) {
314
323
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
@@ -438,7 +447,18 @@ function oauthHeaders() {
438
447
  return headers;
439
448
  }
440
449
  function normalizeDomain(value) {
441
- return value.replace(/^https?:\/\//, "").replace(/\/+$/, "");
450
+ const raw = value.trim();
451
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`;
452
+ let url;
453
+ try {
454
+ url = new URL(withScheme);
455
+ } catch {
456
+ throw new Error(`Invalid GitHub domain: ${value}.`);
457
+ }
458
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !url.hostname || url.pathname !== "" && url.pathname !== "/" || url.search || url.hash) {
459
+ throw new Error(`Invalid GitHub domain: ${value}. Provide only a hostname.`);
460
+ }
461
+ return url.host;
442
462
  }
443
463
  function positiveSeconds(value, fallback) {
444
464
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
@@ -888,6 +908,641 @@ function epochSeconds() {
888
908
  return Math.floor(Date.now() / 1e3);
889
909
  }
890
910
 
911
+ // src/anthropic.ts
912
+ var AnthropicCompatibilityError = class extends Error {
913
+ constructor(message) {
914
+ super(message);
915
+ this.name = "AnthropicCompatibilityError";
916
+ }
917
+ };
918
+ function anthropicMessagesToResponsesRequest(request) {
919
+ return removeUndefined2({
920
+ input: anthropicMessagesToResponsesInput(request.messages),
921
+ instructions: anthropicSystemToInstructions(request.system),
922
+ max_output_tokens: typeof request.max_tokens === "number" && Number.isFinite(request.max_tokens) ? request.max_tokens : void 0,
923
+ metadata: request.metadata,
924
+ model: normalizeRequestedModel(request.model),
925
+ parallel_tool_calls: true,
926
+ reasoning: anthropicThinkingToReasoning(request.thinking),
927
+ stop: anthropicStopSequences(request.stop_sequences),
928
+ stream: request.stream === true,
929
+ temperature: request.temperature,
930
+ tool_choice: anthropicToolChoice(request.tool_choice),
931
+ tools: anthropicTools(request.tools),
932
+ top_p: request.top_p
933
+ });
934
+ }
935
+ function responsesResponseToAnthropicMessage(response, fallbackModel) {
936
+ const content = anthropicContentFromResponsesOutput(response);
937
+ const usage = anthropicUsage(response.usage);
938
+ return {
939
+ content,
940
+ id: textValue(response.id) || `msg_${randomId2()}`,
941
+ model: textValue(response.model) || fallbackModel,
942
+ role: "assistant",
943
+ stop_reason: anthropicStopReason(response, content),
944
+ stop_sequence: null,
945
+ type: "message",
946
+ usage
947
+ };
948
+ }
949
+ function responsesStreamToAnthropicStream(stream, options) {
950
+ const decoder = new TextDecoder();
951
+ const encoder = new TextEncoder();
952
+ let buffer = "";
953
+ const state = {
954
+ blocks: /* @__PURE__ */ new Map(),
955
+ completed: false,
956
+ messageId: options.messageId ?? `msg_${randomId2()}`,
957
+ model: options.model,
958
+ nextBlockIndex: 0,
959
+ sawToolUse: false,
960
+ started: false,
961
+ usage: anthropicUsage(void 0)
962
+ };
963
+ return new ReadableStream({
964
+ async start(controller) {
965
+ const enqueue = (event, data) => {
966
+ controller.enqueue(encoder.encode(encodeSse(event, data)));
967
+ };
968
+ const reader = stream.getReader();
969
+ try {
970
+ while (true) {
971
+ const result = await reader.read();
972
+ if (result.done) {
973
+ break;
974
+ }
975
+ buffer += decoder.decode(result.value, { stream: true });
976
+ const blocks = buffer.split(/\r?\n\r?\n/);
977
+ buffer = blocks.pop() ?? "";
978
+ for (const block of blocks) {
979
+ processResponsesSseBlock(block, state, enqueue);
980
+ }
981
+ }
982
+ const tail = `${buffer}${decoder.decode()}`;
983
+ if (tail.trim()) {
984
+ processResponsesSseBlock(tail, state, enqueue);
985
+ }
986
+ finishAnthropicStream(state, enqueue);
987
+ controller.close();
988
+ } catch (error) {
989
+ await reader.cancel(error).catch(() => {
990
+ });
991
+ controller.error(error);
992
+ } finally {
993
+ reader.releaseLock();
994
+ }
995
+ }
996
+ });
997
+ }
998
+ function estimateAnthropicMessageTokens(request) {
999
+ const chars = estimatedTextSize(request.system) + estimatedTextSize(request.messages) + estimatedTextSize(request.tools) + estimatedTextSize(request.tool_choice) + estimatedTextSize(request.thinking);
1000
+ const messageCount = Array.isArray(request.messages) ? request.messages.length : 1;
1001
+ const toolCount = Array.isArray(request.tools) ? request.tools.length : 0;
1002
+ const inputTokens = Math.max(1, Math.ceil(chars / 4) + messageCount * 4 + toolCount * 16);
1003
+ return {
1004
+ input_tokens: inputTokens,
1005
+ total_tokens: inputTokens
1006
+ };
1007
+ }
1008
+ function anthropicMessagesToResponsesInput(messages) {
1009
+ if (!Array.isArray(messages)) {
1010
+ throw new AnthropicCompatibilityError("Anthropic Messages requests require messages[].");
1011
+ }
1012
+ const input = [];
1013
+ for (const message of messages) {
1014
+ const record = asRecord(message);
1015
+ const role = anthropicRole(record.role);
1016
+ const parts = anthropicContentParts(record.content);
1017
+ const messageParts = [];
1018
+ const flushMessage = () => {
1019
+ if (messageParts.length === 0) {
1020
+ return;
1021
+ }
1022
+ input.push({
1023
+ content: [...messageParts],
1024
+ role,
1025
+ type: "message"
1026
+ });
1027
+ messageParts.length = 0;
1028
+ };
1029
+ for (const part of parts) {
1030
+ const type = textValue(part.type) || "text";
1031
+ if (type === "text") {
1032
+ const text = textValue(part.text);
1033
+ if (text) {
1034
+ messageParts.push({
1035
+ text,
1036
+ type: role === "assistant" ? "output_text" : "input_text"
1037
+ });
1038
+ }
1039
+ continue;
1040
+ }
1041
+ if (type === "image") {
1042
+ if (role !== "user") {
1043
+ throw new AnthropicCompatibilityError(
1044
+ "Anthropic image content is only supported for user messages."
1045
+ );
1046
+ }
1047
+ messageParts.push(anthropicImageToResponsesPart(part));
1048
+ continue;
1049
+ }
1050
+ if (type === "tool_use") {
1051
+ flushMessage();
1052
+ input.push({
1053
+ arguments: JSON.stringify(asRecord(part.input)),
1054
+ call_id: textValue(part.id) || `call_${randomId2()}`,
1055
+ name: textValue(part.name),
1056
+ type: "function_call"
1057
+ });
1058
+ continue;
1059
+ }
1060
+ if (type === "tool_result") {
1061
+ flushMessage();
1062
+ input.push({
1063
+ call_id: textValue(part.tool_use_id),
1064
+ output: anthropicToolResultOutput(part.content),
1065
+ type: "function_call_output"
1066
+ });
1067
+ continue;
1068
+ }
1069
+ if (type === "thinking" || type === "redacted_thinking") {
1070
+ continue;
1071
+ }
1072
+ throw new AnthropicCompatibilityError(
1073
+ `Anthropic content block type "${type}" is not supported.`
1074
+ );
1075
+ }
1076
+ flushMessage();
1077
+ }
1078
+ return input;
1079
+ }
1080
+ function anthropicRole(value) {
1081
+ const role = textValue(value);
1082
+ if (role === "assistant" || role === "user") {
1083
+ return role;
1084
+ }
1085
+ if (!role) {
1086
+ return "user";
1087
+ }
1088
+ throw new AnthropicCompatibilityError(`Anthropic message role "${role}" is not supported.`);
1089
+ }
1090
+ function anthropicContentParts(content) {
1091
+ if (typeof content === "string") {
1092
+ return [{ text: content, type: "text" }];
1093
+ }
1094
+ if (Array.isArray(content)) {
1095
+ return content.map(
1096
+ (part) => typeof part === "string" ? { text: part, type: "text" } : asRecord(part)
1097
+ );
1098
+ }
1099
+ if (content === void 0 || content === null) {
1100
+ return [];
1101
+ }
1102
+ return [asRecord(content)];
1103
+ }
1104
+ function anthropicImageToResponsesPart(part) {
1105
+ const source = asRecord(part.source);
1106
+ const sourceType = textValue(source.type);
1107
+ if (sourceType === "base64") {
1108
+ const mediaType = textValue(source.media_type) || "image/png";
1109
+ const data = textValue(source.data);
1110
+ if (!data) {
1111
+ throw new AnthropicCompatibilityError("Anthropic base64 image content requires source.data.");
1112
+ }
1113
+ return {
1114
+ detail: "auto",
1115
+ image_url: `data:${mediaType};base64,${data}`,
1116
+ type: "input_image"
1117
+ };
1118
+ }
1119
+ if (sourceType === "url") {
1120
+ const url = textValue(source.url);
1121
+ if (!url) {
1122
+ throw new AnthropicCompatibilityError("Anthropic URL image content requires source.url.");
1123
+ }
1124
+ return {
1125
+ detail: "auto",
1126
+ image_url: url,
1127
+ type: "input_image"
1128
+ };
1129
+ }
1130
+ throw new AnthropicCompatibilityError(
1131
+ `Anthropic image source type "${sourceType || "unknown"}" is not supported.`
1132
+ );
1133
+ }
1134
+ function anthropicToolResultOutput(content) {
1135
+ if (typeof content === "string") {
1136
+ return content;
1137
+ }
1138
+ if (Array.isArray(content)) {
1139
+ return content.map((part) => {
1140
+ const record = asRecord(part);
1141
+ return textValue(record.text) || textValue(record.content) || JSON.stringify(part);
1142
+ }).filter(Boolean).join("\n");
1143
+ }
1144
+ if (content === void 0 || content === null) {
1145
+ return "";
1146
+ }
1147
+ return typeof content === "object" ? JSON.stringify(content) : String(content);
1148
+ }
1149
+ function anthropicSystemToInstructions(system) {
1150
+ if (typeof system === "string") {
1151
+ return system || void 0;
1152
+ }
1153
+ if (!Array.isArray(system)) {
1154
+ return void 0;
1155
+ }
1156
+ const text = system.map((part) => textValue(asRecord(part).text) || textValue(part)).filter(Boolean).join("\n");
1157
+ return text || void 0;
1158
+ }
1159
+ function anthropicTools(tools) {
1160
+ if (!Array.isArray(tools)) {
1161
+ return void 0;
1162
+ }
1163
+ const converted = tools.map((tool) => {
1164
+ const record = asRecord(tool);
1165
+ return removeUndefined2({
1166
+ description: record.description,
1167
+ name: record.name,
1168
+ parameters: record.input_schema,
1169
+ strict: record.strict,
1170
+ type: "function"
1171
+ });
1172
+ });
1173
+ return converted.length > 0 ? converted : void 0;
1174
+ }
1175
+ function anthropicToolChoice(toolChoice) {
1176
+ if (toolChoice === void 0 || toolChoice === null) {
1177
+ return void 0;
1178
+ }
1179
+ const record = asRecord(toolChoice);
1180
+ const type = textValue(record.type);
1181
+ if (type === "auto") {
1182
+ return "auto";
1183
+ }
1184
+ if (type === "any") {
1185
+ return "required";
1186
+ }
1187
+ if (type === "none") {
1188
+ return "none";
1189
+ }
1190
+ if (type === "tool") {
1191
+ return { name: textValue(record.name), type: "function" };
1192
+ }
1193
+ throw new AnthropicCompatibilityError(
1194
+ `Anthropic tool_choice type "${type || "unknown"}" is not supported.`
1195
+ );
1196
+ }
1197
+ function anthropicThinkingToReasoning(thinking) {
1198
+ const record = asRecord(thinking);
1199
+ if (Object.keys(record).length === 0) {
1200
+ return void 0;
1201
+ }
1202
+ const type = textValue(record.type);
1203
+ if (type && type !== "enabled") {
1204
+ return void 0;
1205
+ }
1206
+ const budget = typeof record.budget_tokens === "number" ? record.budget_tokens : 0;
1207
+ return {
1208
+ effort: budget >= 16e3 ? "high" : budget >= 4e3 ? "medium" : "low"
1209
+ };
1210
+ }
1211
+ function anthropicStopSequences(stopSequences) {
1212
+ if (!Array.isArray(stopSequences) || stopSequences.length === 0) {
1213
+ return void 0;
1214
+ }
1215
+ return stopSequences.map((sequence) => textValue(sequence)).filter(Boolean);
1216
+ }
1217
+ function anthropicContentFromResponsesOutput(response) {
1218
+ const content = [];
1219
+ const output = Array.isArray(response.output) ? response.output : [];
1220
+ for (const item of output) {
1221
+ const record = asRecord(item);
1222
+ const type = textValue(record.type);
1223
+ if (type === "message") {
1224
+ const parts = Array.isArray(record.content) ? record.content : [];
1225
+ for (const part of parts) {
1226
+ const partRecord = asRecord(part);
1227
+ const text = textValue(partRecord.text) || textValue(partRecord.output_text);
1228
+ if (text) {
1229
+ content.push({ text, type: "text" });
1230
+ }
1231
+ }
1232
+ continue;
1233
+ }
1234
+ if (type === "function_call") {
1235
+ content.push({
1236
+ id: textValue(record.call_id) || textValue(record.id) || `call_${randomId2()}`,
1237
+ input: parseToolInput(textValue(record.arguments)),
1238
+ name: textValue(record.name),
1239
+ type: "tool_use"
1240
+ });
1241
+ }
1242
+ }
1243
+ if (content.length === 0) {
1244
+ const outputText = textValue(response.output_text);
1245
+ if (outputText) {
1246
+ content.push({ text: outputText, type: "text" });
1247
+ }
1248
+ }
1249
+ return content;
1250
+ }
1251
+ function anthropicStopReason(response, content) {
1252
+ if (content.some((part) => part.type === "tool_use")) {
1253
+ return "tool_use";
1254
+ }
1255
+ const incompleteReason = textValue(asRecord(response.incomplete_details).reason);
1256
+ if (textValue(response.status) === "incomplete" || incompleteReason === "max_output_tokens") {
1257
+ return "max_tokens";
1258
+ }
1259
+ return "end_turn";
1260
+ }
1261
+ function anthropicUsage(usage) {
1262
+ const record = asRecord(usage);
1263
+ const inputTokens = firstNumber2(record.input_tokens, record.prompt_tokens) ?? 0;
1264
+ const outputTokens = firstNumber2(record.output_tokens, record.completion_tokens) ?? 0;
1265
+ const details = asRecord(record.input_tokens_details);
1266
+ return removeUndefined2({
1267
+ cache_creation_input_tokens: firstNumber2(record.cache_creation_input_tokens),
1268
+ cache_read_input_tokens: firstNumber2(record.cache_read_input_tokens, details.cached_tokens) ?? void 0,
1269
+ input_tokens: inputTokens,
1270
+ output_tokens: outputTokens
1271
+ });
1272
+ }
1273
+ function processResponsesSseBlock(block, state, enqueue) {
1274
+ const { data, event } = parseSseBlock(block);
1275
+ if (!data || data === "[DONE]") {
1276
+ return;
1277
+ }
1278
+ const parsed = parseJsonObject(data);
1279
+ if (!parsed) {
1280
+ return;
1281
+ }
1282
+ const type = textValue(parsed.type) || event;
1283
+ if (type === "response.created") {
1284
+ const response = asRecord(parsed.response);
1285
+ state.messageId = textValue(response.id) || state.messageId;
1286
+ state.model = textValue(response.model) || state.model;
1287
+ startAnthropicMessage(state, enqueue);
1288
+ return;
1289
+ }
1290
+ if (type === "response.output_item.added") {
1291
+ const item = asRecord(parsed.item);
1292
+ if (textValue(item.type) === "function_call") {
1293
+ ensureToolBlock(state, parsed, item, enqueue);
1294
+ }
1295
+ return;
1296
+ }
1297
+ if (type === "response.output_text.delta") {
1298
+ const blockState = ensureTextBlock(state, parsed, enqueue);
1299
+ const delta = textValue(parsed.delta);
1300
+ if (delta) {
1301
+ blockState.sentText += delta;
1302
+ enqueue("content_block_delta", {
1303
+ delta: { text: delta, type: "text_delta" },
1304
+ index: blockState.index,
1305
+ type: "content_block_delta"
1306
+ });
1307
+ }
1308
+ return;
1309
+ }
1310
+ if (type === "response.output_text.done" || type === "response.content_part.done") {
1311
+ const blockState = ensureTextBlock(state, parsed, enqueue);
1312
+ const text = textValue(parsed.text) || textValue(asRecord(parsed.part).text);
1313
+ if (text && !blockState.sentText) {
1314
+ blockState.sentText = text;
1315
+ enqueue("content_block_delta", {
1316
+ delta: { text, type: "text_delta" },
1317
+ index: blockState.index,
1318
+ type: "content_block_delta"
1319
+ });
1320
+ }
1321
+ stopBlock(blockState, enqueue);
1322
+ return;
1323
+ }
1324
+ if (type === "response.function_call_arguments.delta") {
1325
+ const blockState = ensureToolBlock(state, parsed, {}, enqueue);
1326
+ const delta = textValue(parsed.delta);
1327
+ if (delta) {
1328
+ blockState.sentText += delta;
1329
+ enqueue("content_block_delta", {
1330
+ delta: { partial_json: delta, type: "input_json_delta" },
1331
+ index: blockState.index,
1332
+ type: "content_block_delta"
1333
+ });
1334
+ }
1335
+ return;
1336
+ }
1337
+ if (type === "response.function_call_arguments.done") {
1338
+ const blockState = ensureToolBlock(state, parsed, {}, enqueue);
1339
+ const args = textValue(parsed.arguments);
1340
+ if (args && !blockState.sentText) {
1341
+ blockState.sentText = args;
1342
+ enqueue("content_block_delta", {
1343
+ delta: { partial_json: args, type: "input_json_delta" },
1344
+ index: blockState.index,
1345
+ type: "content_block_delta"
1346
+ });
1347
+ }
1348
+ stopBlock(blockState, enqueue);
1349
+ return;
1350
+ }
1351
+ if (type === "response.output_item.done") {
1352
+ const item = asRecord(parsed.item);
1353
+ if (textValue(item.type) === "function_call") {
1354
+ const blockState = ensureToolBlock(state, parsed, item, enqueue);
1355
+ const args = textValue(item.arguments);
1356
+ if (args && !blockState.sentText) {
1357
+ blockState.sentText = args;
1358
+ enqueue("content_block_delta", {
1359
+ delta: { partial_json: args, type: "input_json_delta" },
1360
+ index: blockState.index,
1361
+ type: "content_block_delta"
1362
+ });
1363
+ }
1364
+ stopBlock(blockState, enqueue);
1365
+ }
1366
+ return;
1367
+ }
1368
+ if (type === "response.completed") {
1369
+ const response = asRecord(parsed.response);
1370
+ state.model = textValue(response.model) || state.model;
1371
+ state.usage = anthropicUsage(response.usage);
1372
+ finishAnthropicStream(state, enqueue);
1373
+ return;
1374
+ }
1375
+ if (type === "response.failed" || event === "error") {
1376
+ const error = asRecord(asRecord(parsed.response).error);
1377
+ enqueue("error", {
1378
+ error: {
1379
+ message: textValue(error.message) || textValue(parsed.message) || "Upstream stream failed.",
1380
+ type: textValue(error.type) || "api_error"
1381
+ },
1382
+ type: "error"
1383
+ });
1384
+ state.completed = true;
1385
+ }
1386
+ }
1387
+ function startAnthropicMessage(state, enqueue) {
1388
+ if (state.started) {
1389
+ return;
1390
+ }
1391
+ state.started = true;
1392
+ enqueue("message_start", {
1393
+ message: {
1394
+ content: [],
1395
+ id: state.messageId,
1396
+ model: state.model,
1397
+ role: "assistant",
1398
+ stop_reason: null,
1399
+ stop_sequence: null,
1400
+ type: "message",
1401
+ usage: anthropicUsage(void 0)
1402
+ },
1403
+ type: "message_start"
1404
+ });
1405
+ }
1406
+ function finishAnthropicStream(state, enqueue) {
1407
+ if (state.completed) {
1408
+ return;
1409
+ }
1410
+ startAnthropicMessage(state, enqueue);
1411
+ for (const block of [...state.blocks.values()].sort((left, right) => left.index - right.index)) {
1412
+ stopBlock(block, enqueue);
1413
+ }
1414
+ enqueue("message_delta", {
1415
+ delta: {
1416
+ stop_reason: state.sawToolUse ? "tool_use" : "end_turn",
1417
+ stop_sequence: null
1418
+ },
1419
+ type: "message_delta",
1420
+ usage: state.usage
1421
+ });
1422
+ enqueue("message_stop", { type: "message_stop" });
1423
+ state.completed = true;
1424
+ }
1425
+ function ensureTextBlock(state, payload, enqueue) {
1426
+ startAnthropicMessage(state, enqueue);
1427
+ const key = `text:${indexValue(payload.output_index)}:${indexValue(payload.content_index)}`;
1428
+ let block = state.blocks.get(key);
1429
+ if (!block) {
1430
+ block = { index: state.nextBlockIndex++, sentText: "", stopped: false, type: "text" };
1431
+ state.blocks.set(key, block);
1432
+ enqueue("content_block_start", {
1433
+ content_block: { text: "", type: "text" },
1434
+ index: block.index,
1435
+ type: "content_block_start"
1436
+ });
1437
+ }
1438
+ return block;
1439
+ }
1440
+ function ensureToolBlock(state, payload, item, enqueue) {
1441
+ startAnthropicMessage(state, enqueue);
1442
+ state.sawToolUse = true;
1443
+ const key = `tool:${indexValue(payload.output_index)}`;
1444
+ let block = state.blocks.get(key);
1445
+ if (!block) {
1446
+ block = { index: state.nextBlockIndex++, sentText: "", stopped: false, type: "tool_use" };
1447
+ state.blocks.set(key, block);
1448
+ enqueue("content_block_start", {
1449
+ content_block: {
1450
+ id: textValue(item.call_id) || textValue(item.id) || `call_${randomId2()}`,
1451
+ input: {},
1452
+ name: textValue(item.name),
1453
+ type: "tool_use"
1454
+ },
1455
+ index: block.index,
1456
+ type: "content_block_start"
1457
+ });
1458
+ }
1459
+ return block;
1460
+ }
1461
+ function stopBlock(block, enqueue) {
1462
+ if (block.stopped) {
1463
+ return;
1464
+ }
1465
+ block.stopped = true;
1466
+ enqueue("content_block_stop", {
1467
+ index: block.index,
1468
+ type: "content_block_stop"
1469
+ });
1470
+ }
1471
+ function parseSseBlock(block) {
1472
+ let event = "message";
1473
+ const data = [];
1474
+ for (const line of block.split(/\r?\n/)) {
1475
+ const trimmed = line.trim();
1476
+ if (trimmed.startsWith("event:")) {
1477
+ event = trimmed.slice("event:".length).trim() || event;
1478
+ } else if (trimmed.startsWith("data:")) {
1479
+ data.push(trimmed.slice("data:".length).trim());
1480
+ }
1481
+ }
1482
+ return { data: data.join("\n"), event };
1483
+ }
1484
+ function parseJsonObject(text) {
1485
+ try {
1486
+ return asRecord(JSON.parse(text));
1487
+ } catch {
1488
+ return void 0;
1489
+ }
1490
+ }
1491
+ function parseToolInput(argumentsText) {
1492
+ const parsed = parseJsonObject(argumentsText);
1493
+ return parsed ?? {};
1494
+ }
1495
+ function estimatedTextSize(value) {
1496
+ if (value === void 0 || value === null) {
1497
+ return 0;
1498
+ }
1499
+ if (typeof value === "string") {
1500
+ return value.length;
1501
+ }
1502
+ if (typeof value === "number" || typeof value === "boolean") {
1503
+ return String(value).length;
1504
+ }
1505
+ if (Array.isArray(value)) {
1506
+ return value.reduce((sum, item) => sum + estimatedTextSize(item), 0);
1507
+ }
1508
+ if (typeof value === "object") {
1509
+ return Object.values(value).reduce((sum, item) => sum + estimatedTextSize(item), 0);
1510
+ }
1511
+ return 0;
1512
+ }
1513
+ function textValue(value) {
1514
+ if (typeof value === "string") {
1515
+ return value;
1516
+ }
1517
+ if (typeof value === "number" || typeof value === "boolean") {
1518
+ return String(value);
1519
+ }
1520
+ return "";
1521
+ }
1522
+ function firstNumber2(...values) {
1523
+ for (const value of values) {
1524
+ if (typeof value === "number" && Number.isFinite(value)) {
1525
+ return value;
1526
+ }
1527
+ }
1528
+ return void 0;
1529
+ }
1530
+ function indexValue(value) {
1531
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1532
+ }
1533
+ function removeUndefined2(record) {
1534
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
1535
+ }
1536
+ function encodeSse(event, data) {
1537
+ return `event: ${event}
1538
+ data: ${JSON.stringify(data)}
1539
+
1540
+ `;
1541
+ }
1542
+ function randomId2() {
1543
+ return crypto.randomUUID().replaceAll("-", "");
1544
+ }
1545
+
891
1546
  // src/metrics.ts
892
1547
  var PROMETHEUS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
893
1548
  var DURATION_BUCKETS_SECONDS = [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60];
@@ -1094,11 +1749,43 @@ var MetricsRegistry = class {
1094
1749
  gauge("remaining", "Remaining quota for the Copilot category.", (q) => q.remaining);
1095
1750
  gauge("entitlement", "Quota entitlement for the Copilot category.", (q) => q.entitlement);
1096
1751
  gauge("used", "Used quota (entitlement minus remaining) for the category.", (q) => q.used);
1752
+ gauge("overage_count", "Overage count for the Copilot category.", (q) => q.overageCount);
1753
+ gauge(
1754
+ "overage_entitlement",
1755
+ "Overage entitlement for the Copilot category.",
1756
+ (q) => q.overageEntitlement
1757
+ );
1097
1758
  gauge(
1098
1759
  "percent_remaining",
1099
1760
  "Percent of quota remaining for the Copilot category.",
1100
1761
  (q) => q.percentRemaining
1101
1762
  );
1763
+ booleanGauge(
1764
+ "unlimited",
1765
+ "Whether the Copilot quota category is unlimited.",
1766
+ (q) => q.unlimited
1767
+ );
1768
+ booleanGauge(
1769
+ "overage_permitted",
1770
+ "Whether overage is permitted for the Copilot category.",
1771
+ (q) => q.overagePermitted
1772
+ );
1773
+ booleanGauge("has_quota", "Whether the Copilot quota category has a quota.", (q) => q.hasQuota);
1774
+ booleanGauge(
1775
+ "token_based_billing",
1776
+ "Whether the Copilot quota category uses token-based billing.",
1777
+ (q) => q.tokenBasedBilling
1778
+ );
1779
+ dateGauge(
1780
+ "category_reset_timestamp_seconds",
1781
+ "Unix epoch of the Copilot category-specific quota reset.",
1782
+ (q) => q.quotaResetAt
1783
+ );
1784
+ dateGauge(
1785
+ "category_snapshot_timestamp_seconds",
1786
+ "Unix epoch of the Copilot category quota snapshot.",
1787
+ (q) => q.timestampUtc
1788
+ );
1102
1789
  const resetMs = usage.quotaResetDate ? Date.parse(usage.quotaResetDate) : Number.NaN;
1103
1790
  if (Number.isFinite(resetMs)) {
1104
1791
  lines.push(
@@ -1117,6 +1804,30 @@ var MetricsRegistry = class {
1117
1804
  })} 1`
1118
1805
  );
1119
1806
  }
1807
+ function booleanGauge(suffix, help, pick) {
1808
+ const present = categories.filter(([, quota]) => pick(quota) !== void 0);
1809
+ if (present.length === 0) {
1810
+ return;
1811
+ }
1812
+ lines.push(`# HELP hoopilot_copilot_quota_${suffix} ${help}`);
1813
+ lines.push(`# TYPE hoopilot_copilot_quota_${suffix} gauge`);
1814
+ for (const [category, quota] of present) {
1815
+ lines.push(
1816
+ `hoopilot_copilot_quota_${suffix}${labels({ category })} ${pick(quota) ? 1 : 0}`
1817
+ );
1818
+ }
1819
+ }
1820
+ function dateGauge(suffix, help, pick) {
1821
+ const present = categories.map(([category, quota]) => [category, Date.parse(pick(quota) ?? "")]).filter(([, timestamp]) => Number.isFinite(timestamp));
1822
+ if (present.length === 0) {
1823
+ return;
1824
+ }
1825
+ lines.push(`# HELP hoopilot_copilot_quota_${suffix} ${help}`);
1826
+ lines.push(`# TYPE hoopilot_copilot_quota_${suffix} gauge`);
1827
+ for (const [category, timestamp] of present) {
1828
+ lines.push(`hoopilot_copilot_quota_${suffix}${labels({ category })} ${timestamp / 1e3}`);
1829
+ }
1830
+ }
1120
1831
  }
1121
1832
  };
1122
1833
  function observeResponseUsage(response, fallbackModel, onUsage, signal) {
@@ -1259,6 +1970,7 @@ var DEFAULT_HOST = "127.0.0.1";
1259
1970
  var DEFAULT_PORT = 4141;
1260
1971
  var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
1261
1972
  var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
1973
+ var JSON_OBJECT_MESSAGE = "Request body must be a JSON object.";
1262
1974
  var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
1263
1975
  var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
1264
1976
  var REQUEST_TOO_LARGE_MESSAGE = `Request body must be ${MAX_REQUEST_BODY_BYTES} bytes or smaller.`;
@@ -1328,6 +2040,14 @@ function createHoopilotHandler(options = {}) {
1328
2040
  if (request.method === "GET" && apiPath === "/v1/models") {
1329
2041
  return finish(await handleModels(client, metrics, request.signal, requestLogger));
1330
2042
  }
2043
+ if (request.method === "POST" && apiPath === "/v1/messages") {
2044
+ return finish(
2045
+ await handleAnthropicMessages(client, metrics, recordTokens, request, requestLogger)
2046
+ );
2047
+ }
2048
+ if (request.method === "POST" && apiPath === "/v1/messages/count_tokens") {
2049
+ return finish(handleAnthropicCountTokens(await readJson(request)));
2050
+ }
1331
2051
  if (request.method === "POST" && apiPath === "/v1/chat/completions") {
1332
2052
  return finish(
1333
2053
  await handleChatCompletions(client, metrics, recordTokens, request, requestLogger)
@@ -1351,16 +2071,16 @@ function createHoopilotHandler(options = {}) {
1351
2071
  return finish(jsonError(401, "copilot_auth_error", error.message));
1352
2072
  }
1353
2073
  const message = errorMessage(error);
1354
- if (message === INVALID_JSON_MESSAGE) {
2074
+ if (message === INVALID_JSON_MESSAGE || message === JSON_OBJECT_MESSAGE) {
1355
2075
  requestLogger.warn(
1356
2076
  { err: errorDetails(error), event: "http.request.failed" },
1357
- "request body was invalid json"
2077
+ "request body was not usable json"
1358
2078
  );
1359
2079
  return finish(jsonError(400, "invalid_request_error", message));
1360
- } else if (error instanceof OpenAICompatibilityError) {
2080
+ } else if (error instanceof OpenAICompatibilityError || error instanceof AnthropicCompatibilityError) {
1361
2081
  requestLogger.warn(
1362
2082
  { err: errorDetails(error), event: "http.request.failed" },
1363
- "request body used unsupported OpenAI compatibility fields"
2083
+ "request body used unsupported compatibility fields"
1364
2084
  );
1365
2085
  return finish(jsonError(400, "invalid_request_error", message));
1366
2086
  } else if (error instanceof RequestBodyTooLargeError) {
@@ -1404,6 +2124,40 @@ function startHoopilotServer(options = {}) {
1404
2124
  url: `http://${urlHost(host)}:${server.port}`
1405
2125
  };
1406
2126
  }
2127
+ async function handleAnthropicMessages(client, metrics, recordTokens, request, logger) {
2128
+ const anthropicRequest = await readJson(request);
2129
+ const responsesRequest = anthropicMessagesToResponsesRequest(anthropicRequest);
2130
+ const upstream = await client.responses(JSON.stringify(responsesRequest), request.signal);
2131
+ metrics.recordUpstream("/responses", upstream.ok);
2132
+ if (!upstream.ok) {
2133
+ return proxyError(upstream, logger);
2134
+ }
2135
+ logUpstreamSuccess(logger, "/responses", upstream.status);
2136
+ const model = normalizeRequestedModel(responsesRequest.model);
2137
+ if (isStreamingResponse(upstream) && upstream.body) {
2138
+ const observed = observeResponseUsage(upstream, model, recordTokens, request.signal);
2139
+ if (!observed.body) {
2140
+ return proxyResponse(observed);
2141
+ }
2142
+ return proxyResponse(
2143
+ new Response(responsesStreamToAnthropicStream(observed.body, { model }), {
2144
+ headers: observed.headers,
2145
+ status: observed.status,
2146
+ statusText: observed.statusText
2147
+ })
2148
+ );
2149
+ }
2150
+ const body = asRecord(await upstream.json());
2151
+ const usage = extractTokenUsage(body.usage);
2152
+ if (usage) {
2153
+ const responseModel = typeof body.model === "string" ? body.model.trim() : "";
2154
+ recordTokens(responseModel || model, usage);
2155
+ }
2156
+ return jsonResponse(responsesResponseToAnthropicMessage(body, model));
2157
+ }
2158
+ function handleAnthropicCountTokens(body) {
2159
+ return jsonResponse(estimateAnthropicMessageTokens(body));
2160
+ }
1407
2161
  async function handleModels(client, metrics, signal, logger) {
1408
2162
  const upstream = await client.models(signal);
1409
2163
  metrics.recordUpstream("/models", upstream.ok);
@@ -1511,20 +2265,24 @@ function proxyResponse(upstream) {
1511
2265
  }
1512
2266
  async function readJson(request) {
1513
2267
  const text = await readRequestText(request);
2268
+ return parseJsonObject2(text);
2269
+ }
2270
+ function parseJsonObject2(text) {
2271
+ let parsed;
1514
2272
  try {
1515
- return asRecord(JSON.parse(text));
2273
+ parsed = JSON.parse(text);
1516
2274
  } catch {
1517
2275
  throw new Error(INVALID_JSON_MESSAGE);
1518
2276
  }
2277
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2278
+ throw new Error(JSON_OBJECT_MESSAGE);
2279
+ }
2280
+ return parsed;
1519
2281
  }
1520
2282
  async function readJsonText(request) {
1521
2283
  const text = await readRequestText(request);
1522
- try {
1523
- JSON.parse(text);
1524
- return text;
1525
- } catch {
1526
- throw new Error(INVALID_JSON_MESSAGE);
1527
- }
2284
+ parseJsonObject2(text);
2285
+ return text;
1528
2286
  }
1529
2287
  async function readRequestText(request) {
1530
2288
  const contentLength = request.headers.get("content-length");
@@ -1599,9 +2357,10 @@ function websocketUnsupportedResponse() {
1599
2357
  }
1600
2358
  function corsHeaders() {
1601
2359
  return {
1602
- "access-control-allow-headers": "authorization, content-type, x-api-key",
2360
+ "access-control-allow-headers": "anthropic-beta, anthropic-dangerous-direct-browser-access, anthropic-version, authorization, content-type, x-api-key, x-request-id",
1603
2361
  "access-control-allow-methods": "GET, POST, OPTIONS",
1604
- "access-control-allow-origin": "*"
2362
+ "access-control-allow-origin": "*",
2363
+ "access-control-expose-headers": "x-request-id"
1605
2364
  };
1606
2365
  }
1607
2366
  function isAuthorized(request, apiKey) {
@@ -1753,6 +2512,10 @@ function canonicalApiPath(path) {
1753
2512
  return "/v1/chat/completions";
1754
2513
  case "/completions":
1755
2514
  return "/v1/completions";
2515
+ case "/messages":
2516
+ return "/v1/messages";
2517
+ case "/messages/count_tokens":
2518
+ return "/v1/messages/count_tokens";
1756
2519
  case "/responses":
1757
2520
  return "/v1/responses";
1758
2521
  case "/usage":
@@ -1777,6 +2540,12 @@ function routeFor(method, path) {
1777
2540
  if (method === "GET" && path === "/v1/models") {
1778
2541
  return "models";
1779
2542
  }
2543
+ if (method === "POST" && path === "/v1/messages") {
2544
+ return "anthropic_messages";
2545
+ }
2546
+ if (method === "POST" && path === "/v1/messages/count_tokens") {
2547
+ return "anthropic_count_tokens";
2548
+ }
1780
2549
  if (method === "POST" && path === "/v1/chat/completions") {
1781
2550
  return "chat_completions";
1782
2551
  }
@@ -1814,8 +2583,8 @@ function metricsResponse(metrics) {
1814
2583
  });
1815
2584
  }
1816
2585
  async function handleUsage(metrics, readUsage, signal) {
1817
- const proxy = metrics.snapshot();
1818
2586
  const { copilot, error } = await readUsage(signal);
2587
+ const proxy = metrics.snapshot();
1819
2588
  const body = { copilot: copilot ?? null, object: "usage", proxy };
1820
2589
  if (error) {
1821
2590
  body.copilot_error = error;
@@ -1840,10 +2609,10 @@ function createUsageReader(client, metrics, now = Date.now, ttlMs = USAGE_CACHE_
1840
2609
  metrics.recordCopilotQuota(value);
1841
2610
  return { copilot: value };
1842
2611
  } catch (error) {
1843
- metrics.recordUpstream(usagePath, false);
1844
2612
  if (error instanceof CopilotAuthError) {
1845
2613
  return { error: error.message };
1846
2614
  }
2615
+ metrics.recordUpstream(usagePath, false);
1847
2616
  return { error: errorMessage(error) };
1848
2617
  }
1849
2618
  };
@@ -2449,8 +3218,7 @@ async function main2(argv = Bun.argv.slice(2)) {
2449
3218
  const command = argv[0];
2450
3219
  if (command === "update" || command === "upgrade") {
2451
3220
  const args2 = withRuntimeEnv(parseArgs(argv.slice(1)));
2452
- if (args2.help) {
2453
- console.log(helpText(await getVersion()));
3221
+ if (await printMetaOption(args2)) {
2454
3222
  return;
2455
3223
  }
2456
3224
  const logger2 = commandLogger(args2, command);
@@ -2463,8 +3231,7 @@ async function main2(argv = Bun.argv.slice(2)) {
2463
3231
  }
2464
3232
  if (command === "login") {
2465
3233
  const args2 = withRuntimeEnv(parseArgs(argv.slice(1)));
2466
- if (args2.help) {
2467
- console.log(helpText(await getVersion()));
3234
+ if (await printMetaOption(args2)) {
2468
3235
  return;
2469
3236
  }
2470
3237
  args2.logger = commandLogger(args2, "login");
@@ -2473,8 +3240,7 @@ async function main2(argv = Bun.argv.slice(2)) {
2473
3240
  }
2474
3241
  if (command === "models") {
2475
3242
  const args2 = withRuntimeEnv(parseArgs(argv.slice(1)));
2476
- if (args2.help) {
2477
- console.log(helpText(await getVersion()));
3243
+ if (await printMetaOption(args2)) {
2478
3244
  return;
2479
3245
  }
2480
3246
  args2.logger = commandLogger(args2, "models");
@@ -2483,8 +3249,7 @@ async function main2(argv = Bun.argv.slice(2)) {
2483
3249
  }
2484
3250
  if (command === "usage") {
2485
3251
  const args2 = withRuntimeEnv(parseArgs(argv.slice(1)));
2486
- if (args2.help) {
2487
- console.log(helpText(await getVersion()));
3252
+ if (await printMetaOption(args2)) {
2488
3253
  return;
2489
3254
  }
2490
3255
  args2.logger = commandLogger(args2, "usage");
@@ -2492,12 +3257,7 @@ async function main2(argv = Bun.argv.slice(2)) {
2492
3257
  return;
2493
3258
  }
2494
3259
  const args = withRuntimeEnv(parseArgs(argv));
2495
- if (args.help) {
2496
- console.log(helpText(await getVersion()));
2497
- return;
2498
- }
2499
- if (args.version) {
2500
- console.log(await getVersion());
3260
+ if (await printMetaOption(args)) {
2501
3261
  return;
2502
3262
  }
2503
3263
  const logger = commandLogger(args, "serve");
@@ -2519,6 +3279,17 @@ async function main2(argv = Bun.argv.slice(2)) {
2519
3279
  );
2520
3280
  }
2521
3281
  }
3282
+ async function printMetaOption(args) {
3283
+ if (args.help) {
3284
+ console.log(helpText(await getVersion()));
3285
+ return true;
3286
+ }
3287
+ if (args.version) {
3288
+ console.log(await getVersion());
3289
+ return true;
3290
+ }
3291
+ return false;
3292
+ }
2522
3293
  function parseArgs(argv) {
2523
3294
  const args = {};
2524
3295
  const rest = [...argv];