opencodekit 0.18.18 → 0.18.19
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/index.js +1 -1
- package/dist/template/.opencode/memory.db +0 -0
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/plugin/copilot-auth.ts +123 -28
- package/dist/template/.opencode/plugin/lib/notify.ts +10 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -67,6 +67,15 @@ const RESPONSES_API_ALTERNATE_INPUT_TYPES = [
|
|
|
67
67
|
"reasoning",
|
|
68
68
|
];
|
|
69
69
|
|
|
70
|
+
// Expected ID prefixes per Responses API item type.
|
|
71
|
+
// The OpenAI Responses API validates that item IDs start with specific prefixes.
|
|
72
|
+
// GitHub Copilot's backend (especially GPT models) returns non-standard prefixes
|
|
73
|
+
// (e.g., "h_" instead of "fc_") that the API then rejects on replay.
|
|
74
|
+
const RESPONSES_API_EXPECTED_PREFIXES: Record<string, string> = {
|
|
75
|
+
function_call: "fc_",
|
|
76
|
+
// function_call_output.call_id prefix is validated directly in sanitizeResponseInputIds
|
|
77
|
+
};
|
|
78
|
+
|
|
70
79
|
function normalizeDomain(url: string): string {
|
|
71
80
|
return url.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
72
81
|
}
|
|
@@ -357,48 +366,133 @@ const MAX_RESPONSE_API_ID_LENGTH = 64;
|
|
|
357
366
|
* the OpenAI spec. We hash them to a deterministic 64-char string.
|
|
358
367
|
* Preserves the original prefix (e.g., "fc_", "msg_", "call_") so that
|
|
359
368
|
* OpenAI's prefix validation passes.
|
|
369
|
+
*
|
|
370
|
+
* @param id - The original ID to sanitize
|
|
371
|
+
* @param forcedPrefix - If provided, use this prefix instead of the detected one.
|
|
372
|
+
* Used when the original prefix is wrong (e.g., Copilot returns "h_" instead of "fc_").
|
|
360
373
|
* See: https://github.com/vercel/ai/issues/5171
|
|
361
374
|
*/
|
|
362
|
-
function sanitizeResponseId(id: string): string {
|
|
363
|
-
|
|
364
|
-
// Detect and preserve the original prefix (e.g., "fc_", "msg_", "call_", "resp_")
|
|
365
|
-
// The OpenAI Responses API validates that IDs start with specific prefixes
|
|
375
|
+
function sanitizeResponseId(id: string, forcedPrefix?: string): string {
|
|
376
|
+
// Detect the original prefix (e.g., "fc_", "msg_", "call_", "resp_", "h_")
|
|
366
377
|
const prefixMatch = id.match(/^([a-z]+_)/);
|
|
367
|
-
const
|
|
368
|
-
|
|
378
|
+
const detectedPrefix = prefixMatch ? prefixMatch[1] : "";
|
|
379
|
+
const prefix = forcedPrefix ?? detectedPrefix;
|
|
380
|
+
|
|
381
|
+
// If no forced prefix and within length, return as-is
|
|
382
|
+
if (!forcedPrefix && (!id || id.length <= MAX_RESPONSE_API_ID_LENGTH)) {
|
|
383
|
+
return id;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Strip the original prefix to get the core ID
|
|
387
|
+
const coreId = id.slice(detectedPrefix.length);
|
|
388
|
+
|
|
389
|
+
// If just a prefix swap and within length, do a simple replacement
|
|
390
|
+
if (forcedPrefix && (prefix.length + coreId.length) <= MAX_RESPONSE_API_ID_LENGTH) {
|
|
391
|
+
return `${prefix}${coreId}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Hash the full original ID for deterministic uniqueness
|
|
369
395
|
let hash = 0;
|
|
370
396
|
for (let i = 0; i < id.length; i++) {
|
|
371
397
|
hash = ((hash << 5) - hash + id.charCodeAt(i)) | 0;
|
|
372
398
|
}
|
|
373
399
|
const hashStr = Math.abs(hash).toString(36);
|
|
374
|
-
// Take some chars from
|
|
375
|
-
const afterPrefix = id.slice(prefix.length);
|
|
400
|
+
// Take some chars from the core for additional uniqueness
|
|
376
401
|
const maxMiddleLen =
|
|
377
402
|
MAX_RESPONSE_API_ID_LENGTH - prefix.length - hashStr.length - 1;
|
|
378
|
-
const middle =
|
|
403
|
+
const middle = coreId.slice(0, Math.max(0, maxMiddleLen));
|
|
379
404
|
// Format: prefix + middle + "_" + hash (ensure total <= 64)
|
|
380
405
|
return `${prefix}${middle}_${hashStr}`.slice(0, MAX_RESPONSE_API_ID_LENGTH);
|
|
381
406
|
}
|
|
382
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Check if an ID has the expected prefix for its item type.
|
|
410
|
+
* Returns the expected prefix if the ID is wrong, or null if it's fine.
|
|
411
|
+
*/
|
|
412
|
+
function getExpectedPrefix(item: any): string | null {
|
|
413
|
+
if (!item || typeof item !== "object" || !item.type) return null;
|
|
414
|
+
const expected = RESPONSES_API_EXPECTED_PREFIXES[item.type];
|
|
415
|
+
if (!expected) return null;
|
|
416
|
+
if (typeof item.id === "string" && !item.id.startsWith(expected)) {
|
|
417
|
+
return expected;
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
383
422
|
/**
|
|
384
423
|
* Sanitize all IDs in a Responses API input array.
|
|
385
|
-
*
|
|
424
|
+
*
|
|
425
|
+
* Handles TWO classes of invalid IDs:
|
|
426
|
+
* 1. Wrong prefix — Copilot GPT models return IDs like "h_xxx" instead of "fc_xxx"
|
|
427
|
+
* for function_call items. These are short but have the wrong prefix.
|
|
428
|
+
* 2. Excessive length — Copilot returns 400+ char IDs that exceed the 64-char limit.
|
|
429
|
+
*
|
|
430
|
+
* Uses a two-pass approach:
|
|
431
|
+
* - Pass 1: Build an ID remap for all invalid IDs (both prefix and length issues)
|
|
432
|
+
* - Pass 2: Apply the remap to both `id` and `call_id` fields consistently,
|
|
433
|
+
* so function_call_output.call_id stays in sync with function_call.id
|
|
386
434
|
*/
|
|
387
435
|
function sanitizeResponseInputIds(input: any[]): any[] {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
436
|
+
// Pass 1: Build ID remapping
|
|
437
|
+
const idRemap = new Map<string, string>();
|
|
438
|
+
|
|
439
|
+
for (const item of input) {
|
|
440
|
+
if (!item || typeof item !== "object") continue;
|
|
441
|
+
|
|
442
|
+
// Check for wrong prefix (e.g., function_call with "h_" instead of "fc_")
|
|
443
|
+
const expectedPrefix = getExpectedPrefix(item);
|
|
444
|
+
if (expectedPrefix && typeof item.id === "string" && !idRemap.has(item.id)) {
|
|
445
|
+
const newId = sanitizeResponseId(item.id, expectedPrefix);
|
|
446
|
+
if (newId !== item.id) {
|
|
447
|
+
idRemap.set(item.id, newId);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Check for excessive length on id
|
|
391
452
|
if (
|
|
392
|
-
typeof
|
|
393
|
-
|
|
453
|
+
typeof item.id === "string" &&
|
|
454
|
+
item.id.length > MAX_RESPONSE_API_ID_LENGTH &&
|
|
455
|
+
!idRemap.has(item.id)
|
|
394
456
|
) {
|
|
395
|
-
|
|
457
|
+
idRemap.set(item.id, sanitizeResponseId(item.id));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Check for wrong prefix on call_id (defensive: handles truncated conversations
|
|
461
|
+
// where function_call_output appears without its corresponding function_call)
|
|
462
|
+
if (
|
|
463
|
+
item.type === "function_call_output" &&
|
|
464
|
+
typeof item.call_id === "string" &&
|
|
465
|
+
!item.call_id.startsWith("fc_") &&
|
|
466
|
+
!idRemap.has(item.call_id)
|
|
467
|
+
) {
|
|
468
|
+
const newCallId = sanitizeResponseId(item.call_id, "fc_");
|
|
469
|
+
if (newCallId !== item.call_id) {
|
|
470
|
+
idRemap.set(item.call_id, newCallId);
|
|
471
|
+
}
|
|
396
472
|
}
|
|
473
|
+
|
|
474
|
+
// Check for excessive length on call_id
|
|
397
475
|
if (
|
|
398
|
-
typeof
|
|
399
|
-
|
|
476
|
+
typeof item.call_id === "string" &&
|
|
477
|
+
item.call_id.length > MAX_RESPONSE_API_ID_LENGTH &&
|
|
478
|
+
!idRemap.has(item.call_id)
|
|
400
479
|
) {
|
|
401
|
-
|
|
480
|
+
idRemap.set(item.call_id, sanitizeResponseId(item.call_id));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// No changes needed
|
|
485
|
+
if (idRemap.size === 0) return input;
|
|
486
|
+
|
|
487
|
+
// Pass 2: Apply remapping to both id and call_id fields
|
|
488
|
+
return input.map((item: any) => {
|
|
489
|
+
if (!item || typeof item !== "object") return item;
|
|
490
|
+
const sanitized = { ...item };
|
|
491
|
+
if (typeof sanitized.id === "string" && idRemap.has(sanitized.id)) {
|
|
492
|
+
sanitized.id = idRemap.get(sanitized.id);
|
|
493
|
+
}
|
|
494
|
+
if (typeof sanitized.call_id === "string" && idRemap.has(sanitized.call_id)) {
|
|
495
|
+
sanitized.call_id = idRemap.get(sanitized.call_id);
|
|
402
496
|
}
|
|
403
497
|
return sanitized;
|
|
404
498
|
});
|
|
@@ -600,21 +694,22 @@ export const CopilotAuthPlugin: Plugin = async ({ client: sdk }) => {
|
|
|
600
694
|
|
|
601
695
|
// Responses API
|
|
602
696
|
if (body?.input) {
|
|
603
|
-
// Sanitize
|
|
604
|
-
//
|
|
697
|
+
// Sanitize IDs from Copilot backend:
|
|
698
|
+
// 1. Wrong prefix — GPT models return "h_xxx" instead of "fc_xxx"
|
|
699
|
+
// 2. Excessive length — Copilot returns 400+ char IDs (max is 64)
|
|
605
700
|
const sanitizedInput = sanitizeResponseInputIds(body.input);
|
|
606
701
|
const inputWasSanitized =
|
|
607
702
|
sanitizedInput !== body.input &&
|
|
608
703
|
JSON.stringify(sanitizedInput) !== JSON.stringify(body.input);
|
|
609
704
|
|
|
610
705
|
if (inputWasSanitized) {
|
|
611
|
-
log("info", "Sanitized
|
|
612
|
-
|
|
613
|
-
(item: any) =>
|
|
614
|
-
|
|
615
|
-
item.id
|
|
616
|
-
|
|
617
|
-
|
|
706
|
+
log("info", "Sanitized IDs in Responses API input (prefix or length)", {
|
|
707
|
+
items_fixed: body.input.filter(
|
|
708
|
+
(item: any, i: number) =>
|
|
709
|
+
item && sanitizedInput[i] && (
|
|
710
|
+
item.id !== sanitizedInput[i].id ||
|
|
711
|
+
item.call_id !== sanitizedInput[i].call_id
|
|
712
|
+
),
|
|
618
713
|
).length,
|
|
619
714
|
});
|
|
620
715
|
modifiedBody = {
|
|
@@ -29,13 +29,17 @@ export function isWSL(): boolean {
|
|
|
29
29
|
*/
|
|
30
30
|
export async function notify($: any, title: string, message: string): Promise<void> {
|
|
31
31
|
const platform = process.platform;
|
|
32
|
-
const safeTitle = title || "OpenCode";
|
|
33
|
-
const safeMessage = message || "";
|
|
34
32
|
|
|
35
33
|
try {
|
|
36
34
|
if (platform === "darwin") {
|
|
35
|
+
// Escape backslashes and double quotes for AppleScript string literals
|
|
36
|
+
const escapeAS = (s: string) => (s || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
37
|
+
const safeTitle = escapeAS(title || "OpenCode");
|
|
38
|
+
const safeMessage = escapeAS(message || "");
|
|
37
39
|
await $`osascript -e ${`display notification "${safeMessage}" with title "${safeTitle}"`}`;
|
|
38
40
|
} else if (platform === "linux") {
|
|
41
|
+
const safeTitle = title || "OpenCode";
|
|
42
|
+
const safeMessage = message || "";
|
|
39
43
|
if (isWSL()) {
|
|
40
44
|
// WSL: try notify-send, fail silently
|
|
41
45
|
await $`notify-send ${safeTitle} ${safeMessage}`.catch(() => {});
|
|
@@ -43,6 +47,10 @@ export async function notify($: any, title: string, message: string): Promise<vo
|
|
|
43
47
|
await $`notify-send ${safeTitle} ${safeMessage}`;
|
|
44
48
|
}
|
|
45
49
|
} else if (platform === "win32") {
|
|
50
|
+
// Escape single quotes for PowerShell string literals
|
|
51
|
+
const escapePS = (s: string) => (s || "").replace(/'/g, "''");
|
|
52
|
+
const safeTitle = escapePS(title || "OpenCode");
|
|
53
|
+
const safeMessage = escapePS(message || "");
|
|
46
54
|
// Windows: PowerShell toast (fire and forget)
|
|
47
55
|
await $`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${safeMessage}', '${safeTitle}')"`.catch(
|
|
48
56
|
() => {},
|