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 CHANGED
@@ -20,7 +20,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  //#endregion
22
22
  //#region package.json
23
- var version = "0.21.5";
23
+ var version = "0.21.7";
24
24
 
25
25
  //#endregion
26
26
  //#region src/utils/license.ts
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 [_modelId, model] of Object.entries(provider.models)) {
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
- // Route all Copilot models through the local file-based SDK.
579
- // OpenCode's built-in github-copilot model loader will automatically
580
- // choose sdk.responses(model) for GPT-5+ and sdk.chat(model) otherwise.
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: any) => {
877
- if (!tool || typeof tool !== "object") return tool;
878
-
879
- let result = tool;
880
-
881
- // Strip top-level non-standard fields from tool object
882
- if (
883
- "custom" in result ||
884
- "eager_input_streaming" in result
885
- ) {
886
- toolsChanged = true;
887
- const {
888
- custom: _custom,
889
- eager_input_streaming: _eis,
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
- // Also check nested function object (Chat Completions format)
896
- if (
897
- result.function &&
898
- typeof result.function === "object" &&
899
- ("custom" in result.function ||
900
- "eager_input_streaming" in result.function)
901
- ) {
902
- toolsChanged = true;
903
- const {
904
- custom: _c,
905
- eager_input_streaming: _e,
906
- ...cleanFn
907
- } = result.function;
908
- result = { ...result, function: cleanFn };
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
- return result;
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.slice(0, maxLen);
113
+ if (typeof value === "string") return truncate(value, maxLen);
48
114
  if (value instanceof Error)
49
- return (value.message || value.name || "Error").slice(0, maxLen);
50
- if (typeof value === "object" && value !== null) {
51
- const obj = value as Record<string, unknown>;
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 (typeof obj.data === "object" && obj.data !== null) {
55
- const data = obj.data as Record<string, unknown>;
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
- return `${prefix}${data.message}${status}`.slice(0, maxLen);
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.slice(0, maxLen);
66
- if (typeof obj.error === "string") return obj.error.slice(0, maxLen);
67
- if (typeof obj.error === "object" && obj.error !== null) {
68
- const inner = obj.error as Record<string, unknown>;
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.slice(0, maxLen);
147
+ return truncate(inner.message, maxLen);
71
148
  }
72
149
  // Last resort: JSON stringify with truncation
73
- try {
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).slice(0, maxLen);
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
- ? ((errorObj as Record<string, unknown>).data as Record<string, unknown>
157
- )?.statusCode ??
158
- (errorObj as Record<string, unknown>).statusCode
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 full error for debugging
168
- await log(`Session error: ${errorMsg}`, "warn");
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
- /thinking.?block|invalid.*signature|reasoning/i.test(errorMsg)
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(errorMsg)
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(errorMsg)
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 ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencodekit",
3
- "version": "0.21.5",
3
+ "version": "0.21.7",
4
4
  "description": "CLI tool for bootstrapping and managing OpenCodeKit projects",
5
5
  "keywords": [
6
6
  "agents",