@openhoo/hoopilot 0.7.5 → 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
@@ -447,7 +447,18 @@ function oauthHeaders() {
447
447
  return headers;
448
448
  }
449
449
  function normalizeDomain(value) {
450
- 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;
451
462
  }
452
463
  function positiveSeconds(value, fallback) {
453
464
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
@@ -897,6 +908,641 @@ function epochSeconds() {
897
908
  return Math.floor(Date.now() / 1e3);
898
909
  }
899
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
+
900
1546
  // src/metrics.ts
901
1547
  var PROMETHEUS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
902
1548
  var DURATION_BUCKETS_SECONDS = [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60];
@@ -1324,6 +1970,7 @@ var DEFAULT_HOST = "127.0.0.1";
1324
1970
  var DEFAULT_PORT = 4141;
1325
1971
  var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
1326
1972
  var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
1973
+ var JSON_OBJECT_MESSAGE = "Request body must be a JSON object.";
1327
1974
  var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
1328
1975
  var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
1329
1976
  var REQUEST_TOO_LARGE_MESSAGE = `Request body must be ${MAX_REQUEST_BODY_BYTES} bytes or smaller.`;
@@ -1393,6 +2040,14 @@ function createHoopilotHandler(options = {}) {
1393
2040
  if (request.method === "GET" && apiPath === "/v1/models") {
1394
2041
  return finish(await handleModels(client, metrics, request.signal, requestLogger));
1395
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
+ }
1396
2051
  if (request.method === "POST" && apiPath === "/v1/chat/completions") {
1397
2052
  return finish(
1398
2053
  await handleChatCompletions(client, metrics, recordTokens, request, requestLogger)
@@ -1416,16 +2071,16 @@ function createHoopilotHandler(options = {}) {
1416
2071
  return finish(jsonError(401, "copilot_auth_error", error.message));
1417
2072
  }
1418
2073
  const message = errorMessage(error);
1419
- if (message === INVALID_JSON_MESSAGE) {
2074
+ if (message === INVALID_JSON_MESSAGE || message === JSON_OBJECT_MESSAGE) {
1420
2075
  requestLogger.warn(
1421
2076
  { err: errorDetails(error), event: "http.request.failed" },
1422
- "request body was invalid json"
2077
+ "request body was not usable json"
1423
2078
  );
1424
2079
  return finish(jsonError(400, "invalid_request_error", message));
1425
- } else if (error instanceof OpenAICompatibilityError) {
2080
+ } else if (error instanceof OpenAICompatibilityError || error instanceof AnthropicCompatibilityError) {
1426
2081
  requestLogger.warn(
1427
2082
  { err: errorDetails(error), event: "http.request.failed" },
1428
- "request body used unsupported OpenAI compatibility fields"
2083
+ "request body used unsupported compatibility fields"
1429
2084
  );
1430
2085
  return finish(jsonError(400, "invalid_request_error", message));
1431
2086
  } else if (error instanceof RequestBodyTooLargeError) {
@@ -1469,6 +2124,40 @@ function startHoopilotServer(options = {}) {
1469
2124
  url: `http://${urlHost(host)}:${server.port}`
1470
2125
  };
1471
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
+ }
1472
2161
  async function handleModels(client, metrics, signal, logger) {
1473
2162
  const upstream = await client.models(signal);
1474
2163
  metrics.recordUpstream("/models", upstream.ok);
@@ -1576,20 +2265,24 @@ function proxyResponse(upstream) {
1576
2265
  }
1577
2266
  async function readJson(request) {
1578
2267
  const text = await readRequestText(request);
2268
+ return parseJsonObject2(text);
2269
+ }
2270
+ function parseJsonObject2(text) {
2271
+ let parsed;
1579
2272
  try {
1580
- return asRecord(JSON.parse(text));
2273
+ parsed = JSON.parse(text);
1581
2274
  } catch {
1582
2275
  throw new Error(INVALID_JSON_MESSAGE);
1583
2276
  }
2277
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2278
+ throw new Error(JSON_OBJECT_MESSAGE);
2279
+ }
2280
+ return parsed;
1584
2281
  }
1585
2282
  async function readJsonText(request) {
1586
2283
  const text = await readRequestText(request);
1587
- try {
1588
- JSON.parse(text);
1589
- return text;
1590
- } catch {
1591
- throw new Error(INVALID_JSON_MESSAGE);
1592
- }
2284
+ parseJsonObject2(text);
2285
+ return text;
1593
2286
  }
1594
2287
  async function readRequestText(request) {
1595
2288
  const contentLength = request.headers.get("content-length");
@@ -1664,9 +2357,10 @@ function websocketUnsupportedResponse() {
1664
2357
  }
1665
2358
  function corsHeaders() {
1666
2359
  return {
1667
- "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",
1668
2361
  "access-control-allow-methods": "GET, POST, OPTIONS",
1669
- "access-control-allow-origin": "*"
2362
+ "access-control-allow-origin": "*",
2363
+ "access-control-expose-headers": "x-request-id"
1670
2364
  };
1671
2365
  }
1672
2366
  function isAuthorized(request, apiKey) {
@@ -1818,6 +2512,10 @@ function canonicalApiPath(path) {
1818
2512
  return "/v1/chat/completions";
1819
2513
  case "/completions":
1820
2514
  return "/v1/completions";
2515
+ case "/messages":
2516
+ return "/v1/messages";
2517
+ case "/messages/count_tokens":
2518
+ return "/v1/messages/count_tokens";
1821
2519
  case "/responses":
1822
2520
  return "/v1/responses";
1823
2521
  case "/usage":
@@ -1842,6 +2540,12 @@ function routeFor(method, path) {
1842
2540
  if (method === "GET" && path === "/v1/models") {
1843
2541
  return "models";
1844
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
+ }
1845
2549
  if (method === "POST" && path === "/v1/chat/completions") {
1846
2550
  return "chat_completions";
1847
2551
  }
@@ -2514,8 +3218,7 @@ async function main2(argv = Bun.argv.slice(2)) {
2514
3218
  const command = argv[0];
2515
3219
  if (command === "update" || command === "upgrade") {
2516
3220
  const args2 = withRuntimeEnv(parseArgs(argv.slice(1)));
2517
- if (args2.help) {
2518
- console.log(helpText(await getVersion()));
3221
+ if (await printMetaOption(args2)) {
2519
3222
  return;
2520
3223
  }
2521
3224
  const logger2 = commandLogger(args2, command);
@@ -2528,8 +3231,7 @@ async function main2(argv = Bun.argv.slice(2)) {
2528
3231
  }
2529
3232
  if (command === "login") {
2530
3233
  const args2 = withRuntimeEnv(parseArgs(argv.slice(1)));
2531
- if (args2.help) {
2532
- console.log(helpText(await getVersion()));
3234
+ if (await printMetaOption(args2)) {
2533
3235
  return;
2534
3236
  }
2535
3237
  args2.logger = commandLogger(args2, "login");
@@ -2538,8 +3240,7 @@ async function main2(argv = Bun.argv.slice(2)) {
2538
3240
  }
2539
3241
  if (command === "models") {
2540
3242
  const args2 = withRuntimeEnv(parseArgs(argv.slice(1)));
2541
- if (args2.help) {
2542
- console.log(helpText(await getVersion()));
3243
+ if (await printMetaOption(args2)) {
2543
3244
  return;
2544
3245
  }
2545
3246
  args2.logger = commandLogger(args2, "models");
@@ -2548,8 +3249,7 @@ async function main2(argv = Bun.argv.slice(2)) {
2548
3249
  }
2549
3250
  if (command === "usage") {
2550
3251
  const args2 = withRuntimeEnv(parseArgs(argv.slice(1)));
2551
- if (args2.help) {
2552
- console.log(helpText(await getVersion()));
3252
+ if (await printMetaOption(args2)) {
2553
3253
  return;
2554
3254
  }
2555
3255
  args2.logger = commandLogger(args2, "usage");
@@ -2557,12 +3257,7 @@ async function main2(argv = Bun.argv.slice(2)) {
2557
3257
  return;
2558
3258
  }
2559
3259
  const args = withRuntimeEnv(parseArgs(argv));
2560
- if (args.help) {
2561
- console.log(helpText(await getVersion()));
2562
- return;
2563
- }
2564
- if (args.version) {
2565
- console.log(await getVersion());
3260
+ if (await printMetaOption(args)) {
2566
3261
  return;
2567
3262
  }
2568
3263
  const logger = commandLogger(args, "serve");
@@ -2584,6 +3279,17 @@ async function main2(argv = Bun.argv.slice(2)) {
2584
3279
  );
2585
3280
  }
2586
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
+ }
2587
3293
  function parseArgs(argv) {
2588
3294
  const args = {};
2589
3295
  const rest = [...argv];