@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/README.md +17 -3
- package/dist/cli.js +805 -34
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2017 -1240
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +2012 -1240
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1523
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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];
|