@ashsec/copilot-api 0.7.2 → 0.7.5
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/main.js +417 -93
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -15,7 +15,6 @@ import { execSync } from "node:child_process";
|
|
|
15
15
|
import process$1 from "node:process";
|
|
16
16
|
import { Hono } from "hono";
|
|
17
17
|
import { cors } from "hono/cors";
|
|
18
|
-
import { logger } from "hono/logger";
|
|
19
18
|
import { streamSSE } from "hono/streaming";
|
|
20
19
|
|
|
21
20
|
//#region src/lib/paths.ts
|
|
@@ -97,11 +96,16 @@ const GITHUB_APP_SCOPES = ["read:user"].join(" ");
|
|
|
97
96
|
//#region src/lib/error.ts
|
|
98
97
|
var HTTPError = class extends Error {
|
|
99
98
|
response;
|
|
100
|
-
|
|
99
|
+
requestPayload;
|
|
100
|
+
constructor(message, response, requestPayload) {
|
|
101
101
|
super(message);
|
|
102
102
|
this.response = response;
|
|
103
|
+
this.requestPayload = requestPayload;
|
|
103
104
|
}
|
|
104
105
|
};
|
|
106
|
+
function isContentFilterError(obj) {
|
|
107
|
+
return typeof obj === "object" && obj !== null && "error" in obj && typeof obj.error === "object" && obj.error?.code === "content_filter";
|
|
108
|
+
}
|
|
105
109
|
async function forwardError(c, error) {
|
|
106
110
|
consola.error("Error occurred:", error);
|
|
107
111
|
if (error instanceof HTTPError) {
|
|
@@ -113,6 +117,15 @@ async function forwardError(c, error) {
|
|
|
113
117
|
errorJson = errorText;
|
|
114
118
|
}
|
|
115
119
|
consola.error("HTTP error:", errorJson);
|
|
120
|
+
if (isContentFilterError(errorJson)) {
|
|
121
|
+
consola.box("CONTENT FILTER TRIGGERED");
|
|
122
|
+
consola.error("Full error response:");
|
|
123
|
+
console.log(JSON.stringify(errorJson, null, 2));
|
|
124
|
+
if (error.requestPayload) {
|
|
125
|
+
consola.error("Request payload that triggered the filter:");
|
|
126
|
+
console.log(JSON.stringify(error.requestPayload, null, 2));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
116
129
|
return c.json({ error: {
|
|
117
130
|
message: errorText,
|
|
118
131
|
type: "error"
|
|
@@ -208,6 +221,75 @@ function getAzureDeploymentName(modelId) {
|
|
|
208
221
|
return modelId.slice(13);
|
|
209
222
|
}
|
|
210
223
|
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/lib/retry-fetch.ts
|
|
226
|
+
const RETRY_DELAYS_MS = [
|
|
227
|
+
100,
|
|
228
|
+
200,
|
|
229
|
+
300
|
|
230
|
+
];
|
|
231
|
+
/**
|
|
232
|
+
* Check if an error is retryable (transient network error)
|
|
233
|
+
*/
|
|
234
|
+
function isRetryableError(error) {
|
|
235
|
+
if (!(error instanceof Error)) return false;
|
|
236
|
+
const message = error.message.toLowerCase();
|
|
237
|
+
const causeMessage = error.cause instanceof Error ? error.cause.message.toLowerCase() : "";
|
|
238
|
+
return [
|
|
239
|
+
"fetch failed",
|
|
240
|
+
"other side closed",
|
|
241
|
+
"connection reset",
|
|
242
|
+
"econnreset",
|
|
243
|
+
"socket hang up",
|
|
244
|
+
"socket connection was closed unexpectedly",
|
|
245
|
+
"etimedout",
|
|
246
|
+
"econnrefused",
|
|
247
|
+
"network error",
|
|
248
|
+
"aborted",
|
|
249
|
+
"timeout"
|
|
250
|
+
].some((pattern) => message.includes(pattern) || causeMessage.includes(pattern));
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Check if an HTTP response status is retryable
|
|
254
|
+
*/
|
|
255
|
+
function isRetryableStatus(status) {
|
|
256
|
+
return status === 408 || status === 429 || status >= 500 && status <= 599;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Fetch with automatic fast retry on transient failures
|
|
260
|
+
* Retries with delays: 100ms, 200ms, 300ms (max ~600ms total wait)
|
|
261
|
+
*/
|
|
262
|
+
async function fetchWithRetry(input, init) {
|
|
263
|
+
const maxAttempts = RETRY_DELAYS_MS.length + 1;
|
|
264
|
+
let lastError;
|
|
265
|
+
let lastResponse;
|
|
266
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) try {
|
|
267
|
+
const headers = new Headers(init?.headers);
|
|
268
|
+
headers.set("Connection", "close");
|
|
269
|
+
const response = await fetch(input, {
|
|
270
|
+
...init,
|
|
271
|
+
headers,
|
|
272
|
+
keepalive: false
|
|
273
|
+
});
|
|
274
|
+
if (isRetryableStatus(response.status) && attempt < maxAttempts - 1) {
|
|
275
|
+
lastResponse = response;
|
|
276
|
+
const delayMs = RETRY_DELAYS_MS[attempt];
|
|
277
|
+
consola.warn(`HTTP ${response.status} (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delayMs}ms`);
|
|
278
|
+
await sleep(delayMs);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
return response;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
lastError = error;
|
|
284
|
+
if (!isRetryableError(error) || attempt === maxAttempts - 1) throw error;
|
|
285
|
+
const delayMs = RETRY_DELAYS_MS[attempt];
|
|
286
|
+
consola.warn(`Fetch failed (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delayMs}ms:`, lastError.message);
|
|
287
|
+
await sleep(delayMs);
|
|
288
|
+
}
|
|
289
|
+
if (lastResponse) return lastResponse;
|
|
290
|
+
throw lastError;
|
|
291
|
+
}
|
|
292
|
+
|
|
211
293
|
//#endregion
|
|
212
294
|
//#region src/services/azure-openai/create-chat-completions.ts
|
|
213
295
|
const AZURE_API_VERSION = "2024-10-21";
|
|
@@ -219,7 +301,7 @@ async function createAzureOpenAIChatCompletions(config$1, payload) {
|
|
|
219
301
|
model: deploymentName,
|
|
220
302
|
...max_tokens != null && { max_completion_tokens: max_tokens }
|
|
221
303
|
};
|
|
222
|
-
const response = await
|
|
304
|
+
const response = await fetchWithRetry(`${config$1.endpoint}/openai/deployments/${deploymentName}/chat/completions?api-version=${AZURE_API_VERSION}`, {
|
|
223
305
|
method: "POST",
|
|
224
306
|
headers: {
|
|
225
307
|
"api-key": config$1.apiKey,
|
|
@@ -229,7 +311,7 @@ async function createAzureOpenAIChatCompletions(config$1, payload) {
|
|
|
229
311
|
});
|
|
230
312
|
if (!response.ok) {
|
|
231
313
|
consola.error("Failed to create Azure OpenAI chat completions:", response);
|
|
232
|
-
throw new HTTPError("Failed to create Azure OpenAI chat completions", response);
|
|
314
|
+
throw new HTTPError("Failed to create Azure OpenAI chat completions", response, payload);
|
|
233
315
|
}
|
|
234
316
|
if (payload.stream) return events(response);
|
|
235
317
|
return await response.json();
|
|
@@ -240,7 +322,7 @@ async function createAzureOpenAIChatCompletions(config$1, payload) {
|
|
|
240
322
|
const AZURE_DEPLOYMENTS_API_VERSION = "2022-12-01";
|
|
241
323
|
async function getAzureOpenAIDeployments(config$1) {
|
|
242
324
|
try {
|
|
243
|
-
const response = await
|
|
325
|
+
const response = await fetchWithRetry(`${config$1.endpoint}/openai/deployments?api-version=${AZURE_DEPLOYMENTS_API_VERSION}`, { headers: {
|
|
244
326
|
"api-key": config$1.apiKey,
|
|
245
327
|
"Content-Type": "application/json"
|
|
246
328
|
} });
|
|
@@ -268,7 +350,7 @@ async function getAzureOpenAIDeployments(config$1) {
|
|
|
268
350
|
//#region src/services/copilot/get-models.ts
|
|
269
351
|
const getModels = async () => {
|
|
270
352
|
const url = `${copilotBaseUrl(state)}/models`;
|
|
271
|
-
const response = await
|
|
353
|
+
const response = await fetchWithRetry(url, { headers: copilotHeaders(state) });
|
|
272
354
|
if (!response.ok) {
|
|
273
355
|
const errorBody = await response.text();
|
|
274
356
|
let errorDetails;
|
|
@@ -515,7 +597,8 @@ const checkUsage = defineCommand({
|
|
|
515
597
|
//#region src/lib/auto-replace.ts
|
|
516
598
|
const SYSTEM_REPLACEMENTS = [{
|
|
517
599
|
id: "system-anthropic-billing",
|
|
518
|
-
|
|
600
|
+
name: "Remove Anthropic billing header",
|
|
601
|
+
pattern: "x-anthropic-billing-header:[^\\n]*\\n?",
|
|
519
602
|
replacement: "",
|
|
520
603
|
isRegex: true,
|
|
521
604
|
enabled: true,
|
|
@@ -529,7 +612,7 @@ let isLoaded = false;
|
|
|
529
612
|
async function loadReplacements() {
|
|
530
613
|
try {
|
|
531
614
|
const data = await fs.readFile(PATHS.REPLACEMENTS_CONFIG_PATH);
|
|
532
|
-
userReplacements = JSON.parse(data).filter((r) => !r.isSystem);
|
|
615
|
+
userReplacements = JSON.parse(data.toString()).filter((r) => !r.isSystem);
|
|
533
616
|
isLoaded = true;
|
|
534
617
|
consola.debug(`Loaded ${userReplacements.length} user replacement rules`);
|
|
535
618
|
} catch {
|
|
@@ -572,10 +655,11 @@ async function getUserReplacements() {
|
|
|
572
655
|
/**
|
|
573
656
|
* Add a new user replacement rule
|
|
574
657
|
*/
|
|
575
|
-
async function addReplacement(pattern, replacement, isRegex = false) {
|
|
658
|
+
async function addReplacement(pattern, replacement, isRegex = false, name) {
|
|
576
659
|
await ensureLoaded();
|
|
577
660
|
const rule = {
|
|
578
661
|
id: `user-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
662
|
+
name,
|
|
579
663
|
pattern,
|
|
580
664
|
replacement,
|
|
581
665
|
isRegex,
|
|
@@ -604,6 +688,26 @@ async function removeReplacement(id) {
|
|
|
604
688
|
return true;
|
|
605
689
|
}
|
|
606
690
|
/**
|
|
691
|
+
* Update an existing user replacement rule
|
|
692
|
+
*/
|
|
693
|
+
async function updateReplacement(id, updates) {
|
|
694
|
+
await ensureLoaded();
|
|
695
|
+
const rule = userReplacements.find((r) => r.id === id);
|
|
696
|
+
if (!rule) return null;
|
|
697
|
+
if (rule.isSystem) {
|
|
698
|
+
consola.warn("Cannot update system replacement rule");
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
if (updates.name !== void 0) rule.name = updates.name;
|
|
702
|
+
if (updates.pattern !== void 0) rule.pattern = updates.pattern;
|
|
703
|
+
if (updates.replacement !== void 0) rule.replacement = updates.replacement;
|
|
704
|
+
if (updates.isRegex !== void 0) rule.isRegex = updates.isRegex;
|
|
705
|
+
if (updates.enabled !== void 0) rule.enabled = updates.enabled;
|
|
706
|
+
await saveReplacements();
|
|
707
|
+
consola.info(`Updated replacement rule: ${rule.name || rule.id}`);
|
|
708
|
+
return rule;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
607
711
|
* Toggle a replacement rule on/off
|
|
608
712
|
*/
|
|
609
713
|
async function toggleReplacement(id) {
|
|
@@ -630,18 +734,32 @@ async function clearUserReplacements() {
|
|
|
630
734
|
consola.info("Cleared all user replacement rules");
|
|
631
735
|
}
|
|
632
736
|
/**
|
|
633
|
-
* Apply a single replacement rule to text
|
|
737
|
+
* Apply a single replacement rule to text and return info about whether it matched
|
|
634
738
|
*/
|
|
635
739
|
function applyRule(text, rule) {
|
|
636
|
-
if (!rule.enabled) return
|
|
740
|
+
if (!rule.enabled) return {
|
|
741
|
+
result: text,
|
|
742
|
+
matched: false
|
|
743
|
+
};
|
|
637
744
|
if (rule.isRegex) try {
|
|
638
745
|
const regex = new RegExp(rule.pattern, "g");
|
|
639
|
-
|
|
746
|
+
const result$1 = text.replace(regex, rule.replacement);
|
|
747
|
+
return {
|
|
748
|
+
result: result$1,
|
|
749
|
+
matched: result$1 !== text
|
|
750
|
+
};
|
|
640
751
|
} catch {
|
|
641
752
|
consola.warn(`Invalid regex pattern in rule ${rule.id}: ${rule.pattern}`);
|
|
642
|
-
return
|
|
753
|
+
return {
|
|
754
|
+
result: text,
|
|
755
|
+
matched: false
|
|
756
|
+
};
|
|
643
757
|
}
|
|
644
|
-
|
|
758
|
+
const result = text.split(rule.pattern).join(rule.replacement);
|
|
759
|
+
return {
|
|
760
|
+
result,
|
|
761
|
+
matched: result !== text
|
|
762
|
+
};
|
|
645
763
|
}
|
|
646
764
|
/**
|
|
647
765
|
* Apply all replacement rules to text
|
|
@@ -649,11 +767,15 @@ function applyRule(text, rule) {
|
|
|
649
767
|
async function applyReplacements(text) {
|
|
650
768
|
let result = text;
|
|
651
769
|
const allRules = await getAllReplacements();
|
|
770
|
+
const appliedRules = [];
|
|
652
771
|
for (const rule of allRules) {
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
772
|
+
const { result: newResult, matched } = applyRule(result, rule);
|
|
773
|
+
if (matched) {
|
|
774
|
+
result = newResult;
|
|
775
|
+
appliedRules.push(rule.name || rule.id);
|
|
776
|
+
}
|
|
656
777
|
}
|
|
778
|
+
if (appliedRules.length > 0) consola.info(`Replacements applied: ${appliedRules.join(", ")}`);
|
|
657
779
|
return result;
|
|
658
780
|
}
|
|
659
781
|
/**
|
|
@@ -690,8 +812,9 @@ function formatRule(rule, index) {
|
|
|
690
812
|
const status = rule.enabled ? "✓" : "✗";
|
|
691
813
|
const type = rule.isRegex ? "regex" : "string";
|
|
692
814
|
const system = rule.isSystem ? " [system]" : "";
|
|
815
|
+
const name = rule.name ? ` "${rule.name}"` : "";
|
|
693
816
|
const replacement = rule.replacement || "(empty)";
|
|
694
|
-
return `${index + 1}. [${status}] (${type})${system} "${rule.pattern}" → "${replacement}"`;
|
|
817
|
+
return `${index + 1}. [${status}] (${type})${system}${name} "${rule.pattern}" → "${replacement}"`;
|
|
695
818
|
}
|
|
696
819
|
async function listReplacements() {
|
|
697
820
|
const all = await getAllReplacements();
|
|
@@ -704,6 +827,14 @@ async function listReplacements() {
|
|
|
704
827
|
console.log();
|
|
705
828
|
}
|
|
706
829
|
async function addNewReplacement() {
|
|
830
|
+
const name = await consola.prompt("Name (optional, short description):", {
|
|
831
|
+
type: "text",
|
|
832
|
+
default: ""
|
|
833
|
+
});
|
|
834
|
+
if (typeof name === "symbol") {
|
|
835
|
+
consola.info("Cancelled.");
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
707
838
|
const matchType = await consola.prompt("Match type:", {
|
|
708
839
|
type: "select",
|
|
709
840
|
options: [{
|
|
@@ -737,8 +868,87 @@ async function addNewReplacement() {
|
|
|
737
868
|
consola.info("Cancelled.");
|
|
738
869
|
return;
|
|
739
870
|
}
|
|
740
|
-
const rule = await addReplacement(pattern, replacement, matchType === "regex");
|
|
741
|
-
consola.success(`Added rule: ${rule.id}`);
|
|
871
|
+
const rule = await addReplacement(pattern, replacement, matchType === "regex", name || void 0);
|
|
872
|
+
consola.success(`Added rule: ${rule.name || rule.id}`);
|
|
873
|
+
}
|
|
874
|
+
async function editExistingReplacement() {
|
|
875
|
+
const userRules = await getUserReplacements();
|
|
876
|
+
if (userRules.length === 0) {
|
|
877
|
+
consola.info("No user rules to edit.");
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const options = userRules.map((rule$1, i) => ({
|
|
881
|
+
label: formatRule(rule$1, i),
|
|
882
|
+
value: rule$1.id
|
|
883
|
+
}));
|
|
884
|
+
const selected = await consola.prompt("Select rule to edit:", {
|
|
885
|
+
type: "select",
|
|
886
|
+
options
|
|
887
|
+
});
|
|
888
|
+
if (typeof selected === "symbol") {
|
|
889
|
+
consola.info("Cancelled.");
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const rule = userRules.find((r) => r.id === selected);
|
|
893
|
+
if (!rule) {
|
|
894
|
+
consola.error("Rule not found.");
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
consola.info(`\nEditing rule: ${rule.name || rule.id}`);
|
|
898
|
+
consola.info("Press Enter to keep current value.\n");
|
|
899
|
+
const name = await consola.prompt("Name:", {
|
|
900
|
+
type: "text",
|
|
901
|
+
default: rule.name || ""
|
|
902
|
+
});
|
|
903
|
+
if (typeof name === "symbol") {
|
|
904
|
+
consola.info("Cancelled.");
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const matchType = await consola.prompt("Match type:", {
|
|
908
|
+
type: "select",
|
|
909
|
+
options: [{
|
|
910
|
+
label: "String (exact match)",
|
|
911
|
+
value: "string"
|
|
912
|
+
}, {
|
|
913
|
+
label: "Regex (regular expression)",
|
|
914
|
+
value: "regex"
|
|
915
|
+
}],
|
|
916
|
+
initial: rule.isRegex ? "regex" : "string"
|
|
917
|
+
});
|
|
918
|
+
if (typeof matchType === "symbol") {
|
|
919
|
+
consola.info("Cancelled.");
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const pattern = await consola.prompt("Pattern to match:", {
|
|
923
|
+
type: "text",
|
|
924
|
+
default: rule.pattern
|
|
925
|
+
});
|
|
926
|
+
if (typeof pattern === "symbol" || !pattern) {
|
|
927
|
+
consola.info("Cancelled.");
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (matchType === "regex") try {
|
|
931
|
+
new RegExp(pattern);
|
|
932
|
+
} catch {
|
|
933
|
+
consola.error(`Invalid regex pattern: ${pattern}`);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
const replacement = await consola.prompt("Replacement text:", {
|
|
937
|
+
type: "text",
|
|
938
|
+
default: rule.replacement
|
|
939
|
+
});
|
|
940
|
+
if (typeof replacement === "symbol") {
|
|
941
|
+
consola.info("Cancelled.");
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const updated = await updateReplacement(selected, {
|
|
945
|
+
name: name || void 0,
|
|
946
|
+
pattern,
|
|
947
|
+
replacement,
|
|
948
|
+
isRegex: matchType === "regex"
|
|
949
|
+
});
|
|
950
|
+
if (updated) consola.success(`Updated rule: ${updated.name || updated.id}`);
|
|
951
|
+
else consola.error("Failed to update rule.");
|
|
742
952
|
}
|
|
743
953
|
async function removeExistingReplacement() {
|
|
744
954
|
const userRules = await getUserReplacements();
|
|
@@ -821,6 +1031,10 @@ async function mainMenu() {
|
|
|
821
1031
|
label: "➕ Add new rule",
|
|
822
1032
|
value: "add"
|
|
823
1033
|
},
|
|
1034
|
+
{
|
|
1035
|
+
label: "✏️ Edit rule",
|
|
1036
|
+
value: "edit"
|
|
1037
|
+
},
|
|
824
1038
|
{
|
|
825
1039
|
label: "➖ Remove rule",
|
|
826
1040
|
value: "remove"
|
|
@@ -851,6 +1065,9 @@ async function mainMenu() {
|
|
|
851
1065
|
case "add":
|
|
852
1066
|
await addNewReplacement();
|
|
853
1067
|
break;
|
|
1068
|
+
case "edit":
|
|
1069
|
+
await editExistingReplacement();
|
|
1070
|
+
break;
|
|
854
1071
|
case "remove":
|
|
855
1072
|
await removeExistingReplacement();
|
|
856
1073
|
break;
|
|
@@ -1055,6 +1272,84 @@ function generateEnvScript(envVars, commandToRun = "") {
|
|
|
1055
1272
|
return commandBlock || commandToRun;
|
|
1056
1273
|
}
|
|
1057
1274
|
|
|
1275
|
+
//#endregion
|
|
1276
|
+
//#region src/lib/request-logger.ts
|
|
1277
|
+
const REQUEST_CONTEXT_KEY = "requestContext";
|
|
1278
|
+
const colors = {
|
|
1279
|
+
reset: "\x1B[0m",
|
|
1280
|
+
dim: "\x1B[2m",
|
|
1281
|
+
bold: "\x1B[1m",
|
|
1282
|
+
cyan: "\x1B[36m",
|
|
1283
|
+
green: "\x1B[32m",
|
|
1284
|
+
yellow: "\x1B[33m",
|
|
1285
|
+
red: "\x1B[31m",
|
|
1286
|
+
magenta: "\x1B[35m",
|
|
1287
|
+
blue: "\x1B[34m",
|
|
1288
|
+
white: "\x1B[37m",
|
|
1289
|
+
gray: "\x1B[90m"
|
|
1290
|
+
};
|
|
1291
|
+
/**
|
|
1292
|
+
* Get the current time formatted as HH:MM:SS
|
|
1293
|
+
*/
|
|
1294
|
+
function getTimeString() {
|
|
1295
|
+
return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", {
|
|
1296
|
+
hour12: false,
|
|
1297
|
+
hour: "2-digit",
|
|
1298
|
+
minute: "2-digit",
|
|
1299
|
+
second: "2-digit"
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Get status color based on HTTP status code
|
|
1304
|
+
*/
|
|
1305
|
+
function getStatusColor(status) {
|
|
1306
|
+
if (status >= 500) return colors.red;
|
|
1307
|
+
if (status >= 400) return colors.yellow;
|
|
1308
|
+
if (status >= 300) return colors.cyan;
|
|
1309
|
+
return colors.green;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Set request context for logging
|
|
1313
|
+
*/
|
|
1314
|
+
function setRequestContext(c, ctx) {
|
|
1315
|
+
const existing = c.get(REQUEST_CONTEXT_KEY);
|
|
1316
|
+
if (existing) c.set(REQUEST_CONTEXT_KEY, {
|
|
1317
|
+
...existing,
|
|
1318
|
+
...ctx
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Custom request logger middleware
|
|
1323
|
+
*/
|
|
1324
|
+
async function requestLogger(c, next) {
|
|
1325
|
+
const startTime = Date.now();
|
|
1326
|
+
const method = c.req.method;
|
|
1327
|
+
const path$1 = c.req.path + (c.req.raw.url.includes("?") ? "?" + c.req.raw.url.split("?")[1] : "");
|
|
1328
|
+
c.set(REQUEST_CONTEXT_KEY, { startTime });
|
|
1329
|
+
await next();
|
|
1330
|
+
const ctx = c.get(REQUEST_CONTEXT_KEY);
|
|
1331
|
+
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
1332
|
+
const status = c.res.status;
|
|
1333
|
+
const statusColor = getStatusColor(status);
|
|
1334
|
+
const lines = [];
|
|
1335
|
+
lines.push(`${colors.dim}${"─".repeat(60)}${colors.reset}`);
|
|
1336
|
+
const statusBadge = `${statusColor}${status}${colors.reset}`;
|
|
1337
|
+
const durationStr = `${colors.cyan}${duration}s${colors.reset}`;
|
|
1338
|
+
lines.push(`${colors.bold}${method}${colors.reset} ${path$1} ${statusBadge} ${durationStr}`);
|
|
1339
|
+
if (ctx?.provider && ctx?.model) {
|
|
1340
|
+
const providerColor = ctx.provider === "Azure OpenAI" ? colors.blue : colors.magenta;
|
|
1341
|
+
lines.push(` ${colors.gray}Provider:${colors.reset} ${providerColor}${ctx.provider}${colors.reset} ${colors.gray}->${colors.reset} ${colors.white}${ctx.model}${colors.reset}`);
|
|
1342
|
+
}
|
|
1343
|
+
if (ctx?.inputTokens !== void 0 || ctx?.outputTokens !== void 0) {
|
|
1344
|
+
const tokenParts = [];
|
|
1345
|
+
if (ctx.inputTokens !== void 0) tokenParts.push(`${colors.gray}Input:${colors.reset} ${colors.yellow}${ctx.inputTokens.toLocaleString()}${colors.reset}`);
|
|
1346
|
+
if (ctx.outputTokens !== void 0) tokenParts.push(`${colors.gray}Output:${colors.reset} ${colors.green}${ctx.outputTokens.toLocaleString()}${colors.reset}`);
|
|
1347
|
+
lines.push(` ${tokenParts.join(" ")}`);
|
|
1348
|
+
}
|
|
1349
|
+
lines.push(` ${colors.dim}${getTimeString()}${colors.reset}`);
|
|
1350
|
+
console.log(lines.join("\n"));
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1058
1353
|
//#endregion
|
|
1059
1354
|
//#region src/lib/approval.ts
|
|
1060
1355
|
const awaitApproval = async () => {
|
|
@@ -1294,7 +1589,7 @@ const createChatCompletions = async (payload) => {
|
|
|
1294
1589
|
...copilotHeaders(state, enableVision),
|
|
1295
1590
|
"X-Initiator": isAgentCall ? "agent" : "user"
|
|
1296
1591
|
};
|
|
1297
|
-
const response = await
|
|
1592
|
+
const response = await fetchWithRetry(`${copilotBaseUrl(state)}/chat/completions`, {
|
|
1298
1593
|
method: "POST",
|
|
1299
1594
|
headers,
|
|
1300
1595
|
body: JSON.stringify(payload)
|
|
@@ -1316,28 +1611,45 @@ async function handleCompletion$1(c) {
|
|
|
1316
1611
|
consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
|
|
1317
1612
|
if (isAzureOpenAIModel(payload.model)) {
|
|
1318
1613
|
if (!state.azureOpenAIConfig) return c.json({ error: "Azure OpenAI not configured" }, 500);
|
|
1319
|
-
|
|
1614
|
+
setRequestContext(c, {
|
|
1615
|
+
provider: "Azure OpenAI",
|
|
1616
|
+
model: payload.model
|
|
1617
|
+
});
|
|
1320
1618
|
if (state.manualApprove) await awaitApproval();
|
|
1321
1619
|
const response$1 = await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, payload);
|
|
1322
|
-
if (isNonStreaming
|
|
1620
|
+
if (isNonStreaming(response$1)) {
|
|
1323
1621
|
consola.debug("Non-streaming response:", JSON.stringify(response$1));
|
|
1622
|
+
if (response$1.usage) setRequestContext(c, {
|
|
1623
|
+
inputTokens: response$1.usage.prompt_tokens,
|
|
1624
|
+
outputTokens: response$1.usage.completion_tokens
|
|
1625
|
+
});
|
|
1324
1626
|
return c.json(response$1);
|
|
1325
1627
|
}
|
|
1326
1628
|
consola.debug("Streaming response");
|
|
1327
1629
|
return streamSSE(c, async (stream) => {
|
|
1328
1630
|
for await (const chunk of response$1) {
|
|
1329
1631
|
consola.debug("Streaming chunk:", JSON.stringify(chunk));
|
|
1632
|
+
if (chunk.data && chunk.data !== "[DONE]") {
|
|
1633
|
+
const parsed = JSON.parse(chunk.data);
|
|
1634
|
+
if (parsed.usage) setRequestContext(c, {
|
|
1635
|
+
inputTokens: parsed.usage.prompt_tokens,
|
|
1636
|
+
outputTokens: parsed.usage.completion_tokens
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1330
1639
|
await stream.writeSSE(chunk);
|
|
1331
1640
|
}
|
|
1332
1641
|
});
|
|
1333
1642
|
}
|
|
1334
|
-
|
|
1643
|
+
setRequestContext(c, {
|
|
1644
|
+
provider: "Copilot",
|
|
1645
|
+
model: payload.model
|
|
1646
|
+
});
|
|
1335
1647
|
const selectedModel = state.models?.data.find((model) => model.id === payload.model);
|
|
1336
1648
|
try {
|
|
1337
1649
|
if (selectedModel) {
|
|
1338
1650
|
const tokenCount = await getTokenCount(payload, selectedModel);
|
|
1339
|
-
|
|
1340
|
-
}
|
|
1651
|
+
setRequestContext(c, { inputTokens: tokenCount.input });
|
|
1652
|
+
}
|
|
1341
1653
|
} catch (error) {
|
|
1342
1654
|
consola.warn("Failed to calculate token count:", error);
|
|
1343
1655
|
}
|
|
@@ -1350,19 +1662,30 @@ async function handleCompletion$1(c) {
|
|
|
1350
1662
|
consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
|
|
1351
1663
|
}
|
|
1352
1664
|
const response = await createChatCompletions(payload);
|
|
1353
|
-
if (isNonStreaming
|
|
1665
|
+
if (isNonStreaming(response)) {
|
|
1354
1666
|
consola.debug("Non-streaming response:", JSON.stringify(response));
|
|
1667
|
+
if (response.usage) setRequestContext(c, {
|
|
1668
|
+
inputTokens: response.usage.prompt_tokens,
|
|
1669
|
+
outputTokens: response.usage.completion_tokens
|
|
1670
|
+
});
|
|
1355
1671
|
return c.json(response);
|
|
1356
1672
|
}
|
|
1357
1673
|
consola.debug("Streaming response");
|
|
1358
1674
|
return streamSSE(c, async (stream) => {
|
|
1359
1675
|
for await (const chunk of response) {
|
|
1360
1676
|
consola.debug("Streaming chunk:", JSON.stringify(chunk));
|
|
1677
|
+
if (chunk.data && chunk.data !== "[DONE]") {
|
|
1678
|
+
const parsed = JSON.parse(chunk.data);
|
|
1679
|
+
if (parsed.usage) setRequestContext(c, {
|
|
1680
|
+
inputTokens: parsed.usage.prompt_tokens,
|
|
1681
|
+
outputTokens: parsed.usage.completion_tokens
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1361
1684
|
await stream.writeSSE(chunk);
|
|
1362
1685
|
}
|
|
1363
1686
|
});
|
|
1364
1687
|
}
|
|
1365
|
-
const isNonStreaming
|
|
1688
|
+
const isNonStreaming = (response) => Object.hasOwn(response, "choices");
|
|
1366
1689
|
|
|
1367
1690
|
//#endregion
|
|
1368
1691
|
//#region src/routes/chat-completions/route.ts
|
|
@@ -1379,7 +1702,7 @@ completionRoutes.post("/", async (c) => {
|
|
|
1379
1702
|
//#region src/services/copilot/create-embeddings.ts
|
|
1380
1703
|
const createEmbeddings = async (payload) => {
|
|
1381
1704
|
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
1382
|
-
const response = await
|
|
1705
|
+
const response = await fetchWithRetry(`${copilotBaseUrl(state)}/embeddings`, {
|
|
1383
1706
|
method: "POST",
|
|
1384
1707
|
headers: copilotHeaders(state),
|
|
1385
1708
|
body: JSON.stringify(payload)
|
|
@@ -1652,12 +1975,7 @@ function translateChunkToAnthropicEvents(chunk, state$1) {
|
|
|
1652
1975
|
content: [],
|
|
1653
1976
|
model: chunk.model,
|
|
1654
1977
|
stop_reason: null,
|
|
1655
|
-
stop_sequence: null
|
|
1656
|
-
usage: {
|
|
1657
|
-
input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
1658
|
-
output_tokens: 0,
|
|
1659
|
-
...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
|
|
1660
|
-
}
|
|
1978
|
+
stop_sequence: null
|
|
1661
1979
|
}
|
|
1662
1980
|
});
|
|
1663
1981
|
state$1.messageStartSent = true;
|
|
@@ -1739,6 +2057,9 @@ function translateChunkToAnthropicEvents(chunk, state$1) {
|
|
|
1739
2057
|
});
|
|
1740
2058
|
state$1.contentBlockOpen = false;
|
|
1741
2059
|
}
|
|
2060
|
+
const inputTokens = chunk.usage?.prompt_tokens ?? 0;
|
|
2061
|
+
const outputTokens = chunk.usage?.completion_tokens ?? 0;
|
|
2062
|
+
const cachedTokens = chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0;
|
|
1742
2063
|
events$1.push({
|
|
1743
2064
|
type: "message_delta",
|
|
1744
2065
|
delta: {
|
|
@@ -1746,9 +2067,10 @@ function translateChunkToAnthropicEvents(chunk, state$1) {
|
|
|
1746
2067
|
stop_sequence: null
|
|
1747
2068
|
},
|
|
1748
2069
|
usage: {
|
|
1749
|
-
input_tokens:
|
|
1750
|
-
output_tokens:
|
|
1751
|
-
|
|
2070
|
+
input_tokens: inputTokens,
|
|
2071
|
+
output_tokens: outputTokens,
|
|
2072
|
+
cache_creation_input_tokens: 0,
|
|
2073
|
+
cache_read_input_tokens: cachedTokens
|
|
1752
2074
|
}
|
|
1753
2075
|
}, { type: "message_stop" });
|
|
1754
2076
|
}
|
|
@@ -1765,73 +2087,68 @@ async function handleCompletion(c) {
|
|
|
1765
2087
|
const openAIPayload = await applyReplacementsToPayload(translatedPayload);
|
|
1766
2088
|
consola.debug("Translated OpenAI request payload:", JSON.stringify(openAIPayload));
|
|
1767
2089
|
if (state.manualApprove) await awaitApproval();
|
|
1768
|
-
|
|
2090
|
+
const isAzureModel = isAzureOpenAIModel(openAIPayload.model);
|
|
2091
|
+
if (isAzureModel) {
|
|
1769
2092
|
if (!state.azureOpenAIConfig) return c.json({ error: "Azure OpenAI not configured" }, 500);
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2093
|
+
setRequestContext(c, {
|
|
2094
|
+
provider: "Azure OpenAI",
|
|
2095
|
+
model: openAIPayload.model
|
|
2096
|
+
});
|
|
2097
|
+
} else setRequestContext(c, {
|
|
2098
|
+
provider: "Copilot",
|
|
2099
|
+
model: openAIPayload.model
|
|
2100
|
+
});
|
|
2101
|
+
if (anthropicPayload.stream) {
|
|
2102
|
+
const streamPayload = {
|
|
2103
|
+
...openAIPayload,
|
|
2104
|
+
stream: true,
|
|
2105
|
+
stream_options: { include_usage: true }
|
|
2106
|
+
};
|
|
2107
|
+
const eventStream = isAzureModel ? await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, streamPayload) : await createChatCompletions(streamPayload);
|
|
1779
2108
|
return streamSSE(c, async (stream) => {
|
|
1780
2109
|
const streamState = {
|
|
1781
2110
|
messageStartSent: false,
|
|
1782
|
-
contentBlockIndex: 0,
|
|
1783
2111
|
contentBlockOpen: false,
|
|
2112
|
+
contentBlockIndex: 0,
|
|
1784
2113
|
toolCalls: {}
|
|
1785
2114
|
};
|
|
1786
|
-
for await (const
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2115
|
+
for await (const event of eventStream) {
|
|
2116
|
+
if (!event.data || event.data === "[DONE]") continue;
|
|
2117
|
+
try {
|
|
2118
|
+
const chunk = JSON.parse(event.data);
|
|
2119
|
+
consola.debug("OpenAI chunk:", JSON.stringify(chunk));
|
|
2120
|
+
const anthropicEvents = translateChunkToAnthropicEvents(chunk, streamState);
|
|
2121
|
+
for (const anthropicEvent of anthropicEvents) {
|
|
2122
|
+
consola.debug("Anthropic event:", JSON.stringify(anthropicEvent));
|
|
2123
|
+
await stream.writeSSE({
|
|
2124
|
+
event: anthropicEvent.type,
|
|
2125
|
+
data: JSON.stringify(anthropicEvent)
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
if (chunk.usage) setRequestContext(c, {
|
|
2129
|
+
inputTokens: chunk.usage.prompt_tokens,
|
|
2130
|
+
outputTokens: chunk.usage.completion_tokens
|
|
1797
2131
|
});
|
|
2132
|
+
} catch (error) {
|
|
2133
|
+
consola.error("Failed to parse chunk:", error, event.data);
|
|
1798
2134
|
}
|
|
1799
2135
|
}
|
|
1800
2136
|
});
|
|
1801
2137
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
return streamSSE(c, async (stream) => {
|
|
1812
|
-
const streamState = {
|
|
1813
|
-
messageStartSent: false,
|
|
1814
|
-
contentBlockIndex: 0,
|
|
1815
|
-
contentBlockOpen: false,
|
|
1816
|
-
toolCalls: {}
|
|
1817
|
-
};
|
|
1818
|
-
for await (const rawEvent of response) {
|
|
1819
|
-
consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
|
|
1820
|
-
if (rawEvent.data === "[DONE]") break;
|
|
1821
|
-
if (!rawEvent.data) continue;
|
|
1822
|
-
const chunk = JSON.parse(rawEvent.data);
|
|
1823
|
-
const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
|
|
1824
|
-
for (const event of events$1) {
|
|
1825
|
-
consola.debug("Translated Anthropic event:", JSON.stringify(event));
|
|
1826
|
-
await stream.writeSSE({
|
|
1827
|
-
event: event.type,
|
|
1828
|
-
data: JSON.stringify(event)
|
|
1829
|
-
});
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
2138
|
+
const nonStreamPayload = {
|
|
2139
|
+
...openAIPayload,
|
|
2140
|
+
stream: false
|
|
2141
|
+
};
|
|
2142
|
+
const response = isAzureModel ? await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, nonStreamPayload) : await createChatCompletions(nonStreamPayload);
|
|
2143
|
+
consola.debug("Response from upstream:", JSON.stringify(response).slice(-400));
|
|
2144
|
+
if (response.usage) setRequestContext(c, {
|
|
2145
|
+
inputTokens: response.usage.prompt_tokens,
|
|
2146
|
+
outputTokens: response.usage.completion_tokens
|
|
1832
2147
|
});
|
|
2148
|
+
const anthropicResponse = translateToAnthropic(response);
|
|
2149
|
+
consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
|
|
2150
|
+
return c.json(anthropicResponse);
|
|
1833
2151
|
}
|
|
1834
|
-
const isNonStreaming = (response) => Object.hasOwn(response, "choices");
|
|
1835
2152
|
|
|
1836
2153
|
//#endregion
|
|
1837
2154
|
//#region src/routes/messages/route.ts
|
|
@@ -1898,7 +2215,7 @@ replacementsRoute.get("/", async (c) => {
|
|
|
1898
2215
|
replacementsRoute.post("/", async (c) => {
|
|
1899
2216
|
const body = await c.req.json();
|
|
1900
2217
|
if (!body.pattern) return c.json({ error: "Pattern is required" }, 400);
|
|
1901
|
-
const rule = await addReplacement(body.pattern, body.replacement ?? "", body.isRegex ?? false);
|
|
2218
|
+
const rule = await addReplacement(body.pattern, body.replacement ?? "", body.isRegex ?? false, body.name);
|
|
1902
2219
|
return c.json(rule, 201);
|
|
1903
2220
|
});
|
|
1904
2221
|
replacementsRoute.delete("/:id", async (c) => {
|
|
@@ -1906,6 +2223,13 @@ replacementsRoute.delete("/:id", async (c) => {
|
|
|
1906
2223
|
if (!await removeReplacement(id)) return c.json({ error: "Replacement not found or is a system rule" }, 404);
|
|
1907
2224
|
return c.json({ success: true });
|
|
1908
2225
|
});
|
|
2226
|
+
replacementsRoute.patch("/:id", async (c) => {
|
|
2227
|
+
const id = c.req.param("id");
|
|
2228
|
+
const body = await c.req.json();
|
|
2229
|
+
const rule = await updateReplacement(id, body);
|
|
2230
|
+
if (!rule) return c.json({ error: "Replacement not found or is a system rule" }, 404);
|
|
2231
|
+
return c.json(rule);
|
|
2232
|
+
});
|
|
1909
2233
|
replacementsRoute.patch("/:id/toggle", async (c) => {
|
|
1910
2234
|
const id = c.req.param("id");
|
|
1911
2235
|
const rule = await toggleReplacement(id);
|
|
@@ -1948,7 +2272,7 @@ usageRoute.get("/", async (c) => {
|
|
|
1948
2272
|
//#endregion
|
|
1949
2273
|
//#region src/server.ts
|
|
1950
2274
|
const server = new Hono();
|
|
1951
|
-
server.use(
|
|
2275
|
+
server.use(requestLogger);
|
|
1952
2276
|
server.use(cors());
|
|
1953
2277
|
server.get("/", (c) => c.text("Server running"));
|
|
1954
2278
|
server.route("/chat/completions", completionRoutes);
|