opencodekit 0.21.5 → 0.21.7
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 +66 -41
- package/dist/template/.opencode/plugin/lib/memory-hooks.ts +117 -37
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -84,6 +84,21 @@ function normalizeDomain(url: string): string {
|
|
|
84
84
|
return url.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
const DEFAULT_COPILOT_API_BASE = "https://api.githubcopilot.com";
|
|
88
|
+
|
|
89
|
+
function toCopilotMessagesBase(url: string | undefined): string {
|
|
90
|
+
const base = (url?.trim() || DEFAULT_COPILOT_API_BASE)
|
|
91
|
+
.replace(/\/+$/, "")
|
|
92
|
+
.replace(/\/v1$/, "");
|
|
93
|
+
return `${base}/v1`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isClaudeCopilotModel(modelId: string, model: any): boolean {
|
|
97
|
+
return [modelId, model?.id, model?.api?.id]
|
|
98
|
+
.filter((value): value is string => typeof value === "string")
|
|
99
|
+
.some((value) => value.toLowerCase().includes("claude"));
|
|
100
|
+
}
|
|
101
|
+
|
|
87
102
|
function getUrls(domain: string) {
|
|
88
103
|
return {
|
|
89
104
|
DEVICE_CODE_URL: `https://${domain}/login/device/code`,
|
|
@@ -565,7 +580,7 @@ export const CopilotAuthPlugin: Plugin = async ({ client: sdk, directory }) => {
|
|
|
565
580
|
: undefined;
|
|
566
581
|
|
|
567
582
|
if (provider && provider.models) {
|
|
568
|
-
for (const [
|
|
583
|
+
for (const [modelId, model] of Object.entries(provider.models)) {
|
|
569
584
|
model.cost = {
|
|
570
585
|
input: 0,
|
|
571
586
|
output: 0,
|
|
@@ -575,9 +590,18 @@ export const CopilotAuthPlugin: Plugin = async ({ client: sdk, directory }) => {
|
|
|
575
590
|
},
|
|
576
591
|
};
|
|
577
592
|
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
//
|
|
593
|
+
// OpenCode 1.14.33 routes Copilot Claude models through the
|
|
594
|
+
// Anthropic Messages API when Copilot /models reports /v1/messages.
|
|
595
|
+
// Keep that route: the local OpenAI-compatible SDK calls
|
|
596
|
+
// /chat/completions, which returns 404 for current Claude models.
|
|
597
|
+
if (isClaudeCopilotModel(modelId, model)) {
|
|
598
|
+
model.api.npm = "@ai-sdk/anthropic";
|
|
599
|
+
model.api.url = toCopilotMessagesBase(baseURL ?? model.api.url);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Route OpenAI-compatible Copilot models through the local file-based
|
|
604
|
+
// SDK so GPT/Gemini keep the reasoning_opaque and Responses fixes.
|
|
581
605
|
model.api.npm = localCopilotSdk;
|
|
582
606
|
}
|
|
583
607
|
}
|
|
@@ -872,45 +896,40 @@ export const CopilotAuthPlugin: Plugin = async ({ client: sdk, directory }) => {
|
|
|
872
896
|
const toolSource = modifiedBody || body;
|
|
873
897
|
if (Array.isArray(toolSource?.tools)) {
|
|
874
898
|
let toolsChanged = false;
|
|
875
|
-
const sanitizedTools = toolSource.tools.map(
|
|
876
|
-
(tool
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
...clean
|
|
891
|
-
} = result;
|
|
892
|
-
result = clean;
|
|
893
|
-
}
|
|
899
|
+
const sanitizedTools = toolSource.tools.map((tool: any) => {
|
|
900
|
+
if (!tool || typeof tool !== "object") return tool;
|
|
901
|
+
|
|
902
|
+
let result = tool;
|
|
903
|
+
|
|
904
|
+
// Strip top-level non-standard fields from tool object
|
|
905
|
+
if ("custom" in result || "eager_input_streaming" in result) {
|
|
906
|
+
toolsChanged = true;
|
|
907
|
+
const {
|
|
908
|
+
custom: _custom,
|
|
909
|
+
eager_input_streaming: _eis,
|
|
910
|
+
...clean
|
|
911
|
+
} = result;
|
|
912
|
+
result = clean;
|
|
913
|
+
}
|
|
894
914
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
915
|
+
// Also check nested function object (Chat Completions format)
|
|
916
|
+
if (
|
|
917
|
+
result.function &&
|
|
918
|
+
typeof result.function === "object" &&
|
|
919
|
+
("custom" in result.function ||
|
|
920
|
+
"eager_input_streaming" in result.function)
|
|
921
|
+
) {
|
|
922
|
+
toolsChanged = true;
|
|
923
|
+
const {
|
|
924
|
+
custom: _c,
|
|
925
|
+
eager_input_streaming: _e,
|
|
926
|
+
...cleanFn
|
|
927
|
+
} = result.function;
|
|
928
|
+
result = { ...result, function: cleanFn };
|
|
929
|
+
}
|
|
910
930
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
);
|
|
931
|
+
return result;
|
|
932
|
+
});
|
|
914
933
|
|
|
915
934
|
if (toolsChanged) {
|
|
916
935
|
modifiedBody = {
|
|
@@ -1248,6 +1267,12 @@ export const CopilotAuthPlugin: Plugin = async ({ client: sdk, directory }) => {
|
|
|
1248
1267
|
"chat.params": async (input: any, output: any) => {
|
|
1249
1268
|
if (!input.model?.providerID?.includes("github-copilot")) return;
|
|
1250
1269
|
|
|
1270
|
+
// Copilot rejects eager tool streaming on Anthropic Messages routing.
|
|
1271
|
+
if (input.model?.api?.npm === "@ai-sdk/anthropic") {
|
|
1272
|
+
output.options ??= {};
|
|
1273
|
+
output.options.toolStreaming = false;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1251
1276
|
// GPT models don't support maxOutputTokens through the Copilot proxy
|
|
1252
1277
|
if (input.model?.api?.id?.includes("gpt")) {
|
|
1253
1278
|
output.maxOutputTokens = undefined;
|
|
@@ -42,43 +42,115 @@ interface HookDeps {
|
|
|
42
42
|
* Handles: Error instances, objects with .message, strings, and arbitrary objects.
|
|
43
43
|
* Never returns "[object Object]".
|
|
44
44
|
*/
|
|
45
|
+
function truncate(value: string, maxLen: number): string {
|
|
46
|
+
return value.length > maxLen ? `${value.slice(0, maxLen - 1)}…` : value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
50
|
+
return typeof value === "object" && value !== null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function addDetail(parts: string[], seen: Set<string>, value: unknown) {
|
|
54
|
+
if (typeof value !== "string") return;
|
|
55
|
+
const detail = value.trim();
|
|
56
|
+
if (!detail || seen.has(detail)) return;
|
|
57
|
+
seen.add(detail);
|
|
58
|
+
parts.push(detail);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collectErrorDetails(
|
|
62
|
+
value: unknown,
|
|
63
|
+
parts: string[],
|
|
64
|
+
seen: Set<string>,
|
|
65
|
+
depth = 0,
|
|
66
|
+
) {
|
|
67
|
+
if (!value || depth > 2) return;
|
|
68
|
+
if (typeof value === "string") {
|
|
69
|
+
addDetail(parts, seen, value);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (value instanceof Error) {
|
|
73
|
+
addDetail(parts, seen, value.message || value.name);
|
|
74
|
+
collectErrorDetails(value.cause, parts, seen, depth + 1);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!isRecord(value)) return;
|
|
78
|
+
|
|
79
|
+
addDetail(parts, seen, value.message);
|
|
80
|
+
addDetail(parts, seen, value.name);
|
|
81
|
+
addDetail(parts, seen, value.code);
|
|
82
|
+
addDetail(parts, seen, value.providerID);
|
|
83
|
+
addDetail(parts, seen, value.modelID);
|
|
84
|
+
|
|
85
|
+
collectErrorDetails(value.data, parts, seen, depth + 1);
|
|
86
|
+
collectErrorDetails(value.error, parts, seen, depth + 1);
|
|
87
|
+
collectErrorDetails(value.cause, parts, seen, depth + 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function safeStringifyError(value: unknown, maxLen = 1000): string {
|
|
91
|
+
try {
|
|
92
|
+
const json = JSON.stringify(value, (key, nestedValue) => {
|
|
93
|
+
if (/authorization|api.?key|token|secret|password/i.test(key)) {
|
|
94
|
+
return "[redacted]";
|
|
95
|
+
}
|
|
96
|
+
if (nestedValue instanceof Error) {
|
|
97
|
+
return {
|
|
98
|
+
name: nestedValue.name,
|
|
99
|
+
message: nestedValue.message,
|
|
100
|
+
cause: nestedValue.cause,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return nestedValue;
|
|
104
|
+
});
|
|
105
|
+
return truncate(json, maxLen);
|
|
106
|
+
} catch {
|
|
107
|
+
return "Non-serializable error object";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
45
111
|
function extractErrorMessage(value: unknown, maxLen = 200): string {
|
|
46
112
|
if (!value) return "Unknown error";
|
|
47
|
-
if (typeof value === "string") return value
|
|
113
|
+
if (typeof value === "string") return truncate(value, maxLen);
|
|
48
114
|
if (value instanceof Error)
|
|
49
|
-
return (value.message || value.name || "Error"
|
|
50
|
-
if (
|
|
51
|
-
const obj = value
|
|
115
|
+
return truncate(value.message || value.name || "Error", maxLen);
|
|
116
|
+
if (isRecord(value)) {
|
|
117
|
+
const obj = value;
|
|
118
|
+
const parts: string[] = [];
|
|
119
|
+
const seen = new Set<string>();
|
|
120
|
+
collectErrorDetails(value, parts, seen);
|
|
52
121
|
|
|
53
122
|
// Handle OpenCode error structure: { name, data: { message, statusCode } }
|
|
54
|
-
if (
|
|
55
|
-
const data = obj.data
|
|
123
|
+
if (isRecord(obj.data)) {
|
|
124
|
+
const data = obj.data;
|
|
56
125
|
if (typeof data.message === "string") {
|
|
57
126
|
const prefix = typeof obj.name === "string" ? `${obj.name}: ` : "";
|
|
58
127
|
const status =
|
|
59
128
|
typeof data.statusCode === "number" ? ` (${data.statusCode})` : "";
|
|
60
|
-
|
|
129
|
+
const cause = parts
|
|
130
|
+
.filter((part) => part !== obj.name && part !== data.message)
|
|
131
|
+
.join(" — ");
|
|
132
|
+
return truncate(
|
|
133
|
+
`${prefix}${data.message}${status}${cause ? ` — ${cause}` : ""}`,
|
|
134
|
+
maxLen,
|
|
135
|
+
);
|
|
61
136
|
}
|
|
62
137
|
}
|
|
63
138
|
|
|
139
|
+
if (parts.length > 0) return truncate(parts.join(" — "), maxLen);
|
|
140
|
+
|
|
64
141
|
// Common error shapes: { message }, { error }, { error: { message } }
|
|
65
|
-
if (typeof obj.message === "string") return obj.message
|
|
66
|
-
if (typeof obj.error === "string") return obj.error
|
|
67
|
-
if (
|
|
68
|
-
const inner = obj.error
|
|
142
|
+
if (typeof obj.message === "string") return truncate(obj.message, maxLen);
|
|
143
|
+
if (typeof obj.error === "string") return truncate(obj.error, maxLen);
|
|
144
|
+
if (isRecord(obj.error)) {
|
|
145
|
+
const inner = obj.error;
|
|
69
146
|
if (typeof inner.message === "string")
|
|
70
|
-
return inner.message
|
|
147
|
+
return truncate(inner.message, maxLen);
|
|
71
148
|
}
|
|
72
149
|
// Last resort: JSON stringify with truncation
|
|
73
|
-
|
|
74
|
-
const json = JSON.stringify(value);
|
|
75
|
-
return json.slice(0, maxLen);
|
|
76
|
-
} catch {
|
|
77
|
-
return "Non-serializable error object";
|
|
78
|
-
}
|
|
150
|
+
return safeStringifyError(value, maxLen);
|
|
79
151
|
}
|
|
80
152
|
// number, boolean, symbol, etc.
|
|
81
|
-
return String(value)
|
|
153
|
+
return truncate(String(value), maxLen);
|
|
82
154
|
}
|
|
83
155
|
|
|
84
156
|
export function createHooks(deps: HookDeps) {
|
|
@@ -153,9 +225,12 @@ export function createHooks(deps: HookDeps) {
|
|
|
153
225
|
// Extract status code from error object for classification
|
|
154
226
|
const rawCode =
|
|
155
227
|
typeof errorObj === "object" && errorObj !== null
|
|
156
|
-
? ((
|
|
157
|
-
|
|
158
|
-
|
|
228
|
+
? ((
|
|
229
|
+
(errorObj as Record<string, unknown>).data as Record<
|
|
230
|
+
string,
|
|
231
|
+
unknown
|
|
232
|
+
>
|
|
233
|
+
)?.statusCode ?? (errorObj as Record<string, unknown>).statusCode)
|
|
159
234
|
: undefined;
|
|
160
235
|
const statusCode =
|
|
161
236
|
typeof rawCode === "number"
|
|
@@ -164,12 +239,19 @@ export function createHooks(deps: HookDeps) {
|
|
|
164
239
|
? Number(rawCode) || undefined
|
|
165
240
|
: undefined;
|
|
166
241
|
|
|
167
|
-
// Log
|
|
168
|
-
|
|
242
|
+
// Log structured error details for debugging. Toasts stay short; logs carry
|
|
243
|
+
// the payload needed to trace provider/config failures.
|
|
244
|
+
await log(
|
|
245
|
+
`Session error: ${errorMsg}; payload=${safeStringifyError(errorObj)}`,
|
|
246
|
+
"warn",
|
|
247
|
+
);
|
|
169
248
|
|
|
170
249
|
// Classify error and provide specific guidance
|
|
171
250
|
let guidance: string;
|
|
172
|
-
if (
|
|
251
|
+
if (/ProviderInitError/i.test(errorMsg)) {
|
|
252
|
+
guidance =
|
|
253
|
+
"Provider initialization failed — check .opencode/plugin provider config and run with --print-logs --log-level DEBUG";
|
|
254
|
+
} else if (
|
|
173
255
|
/token.{0,20}(exceed|limit)/i.test(errorMsg) ||
|
|
174
256
|
errorMsg.includes("context_length_exceeded")
|
|
175
257
|
) {
|
|
@@ -190,18 +272,18 @@ export function createHooks(deps: HookDeps) {
|
|
|
190
272
|
/bad request|invalid.*request/i.test(errorMsg)
|
|
191
273
|
) {
|
|
192
274
|
// Sub-classify 400 errors for more specific guidance
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
) {
|
|
196
|
-
guidance =
|
|
197
|
-
"Thinking block error — start a new session to reset";
|
|
275
|
+
if (/thinking.?block|invalid.*signature|reasoning/i.test(errorMsg)) {
|
|
276
|
+
guidance = "Thinking block error — start a new session to reset";
|
|
198
277
|
} else if (
|
|
199
|
-
/context.*(too|exceed|length|large|limit)|too.?long|max.?length|content.?length/i.test(
|
|
278
|
+
/context.*(too|exceed|length|large|limit)|too.?long|max.?length|content.?length/i.test(
|
|
279
|
+
errorMsg,
|
|
280
|
+
)
|
|
200
281
|
) {
|
|
201
|
-
guidance =
|
|
202
|
-
"Request too large — use /compact to reduce context";
|
|
282
|
+
guidance = "Request too large — use /compact to reduce context";
|
|
203
283
|
} else if (
|
|
204
|
-
/invalid.*\bid\b|item.*\bid\b|unknown.*\bid\b|malformed.*\bid\b/i.test(
|
|
284
|
+
/invalid.*\bid\b|item.*\bid\b|unknown.*\bid\b|malformed.*\bid\b/i.test(
|
|
285
|
+
errorMsg,
|
|
286
|
+
)
|
|
205
287
|
) {
|
|
206
288
|
guidance = "API format error — start a new session";
|
|
207
289
|
} else {
|
|
@@ -212,9 +294,7 @@ export function createHooks(deps: HookDeps) {
|
|
|
212
294
|
/timeout|ETIMEDOUT|ECONNRESET|network|fetch failed/i.test(errorMsg)
|
|
213
295
|
) {
|
|
214
296
|
guidance = "Network error — check connection and retry";
|
|
215
|
-
} else if (
|
|
216
|
-
/invalid.*signature|thinking block/i.test(errorMsg)
|
|
217
|
-
) {
|
|
297
|
+
} else if (/invalid.*signature|thinking block/i.test(errorMsg)) {
|
|
218
298
|
guidance = "API format error — try starting a new session";
|
|
219
299
|
} else if (
|
|
220
300
|
statusCode === 500 ||
|