@alisio/plugin-opencode-go 0.1.0-alpha.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gustavo Gutiérrez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,6 @@
1
+ # @alisio/plugin-opencode-go
2
+
3
+ Built-in OpenCode Go provider for Alisio. It discovers the unauthenticated Go catalog and routes
4
+ only exact model IDs in the documented endpoint table: GPT, Grok and Muse through Responses; open
5
+ compatible models through Chat Completions; MiniMax and Qwen through Anthropic Messages. Unknown
6
+ catalog entries are hidden rather than guessed.
@@ -0,0 +1,5 @@
1
+ import { type Plugin } from "@alisio/sdk";
2
+ export declare function createOpenCodeGoPlugin(): Plugin;
3
+ export * from "./provider.ts";
4
+ declare const _default: Plugin;
5
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,37 @@
1
+ import { definePlugin } from "@alisio/sdk";
2
+ import { OpenCodeGoProvider } from "./provider.js";
3
+ const profileString = (request, key) => typeof request.profile[key] === "string" ? String(request.profile[key]) : "";
4
+ export function createOpenCodeGoPlugin() {
5
+ return definePlugin({
6
+ id: "opencode-go",
7
+ name: "OpenCode Go",
8
+ description: "OpenCode Go multi-protocol model gateway",
9
+ categories: ["model-provider"],
10
+ version: "0.1.0-alpha.1",
11
+ apiVersion: 1,
12
+ setup(api) {
13
+ api.providers.register({
14
+ id: "opencode-go",
15
+ name: "OpenCode Go",
16
+ description: "OpenCode Go multi-protocol model gateway",
17
+ fields: [
18
+ {
19
+ key: "apiKey",
20
+ label: "API key",
21
+ kind: "secret",
22
+ required: true,
23
+ description: "Stored only in the global credentials file",
24
+ },
25
+ ],
26
+ create(request) {
27
+ const apiKey = request.credentials.apiKey;
28
+ if (!apiKey)
29
+ throw new Error("OpenCode Go API key is required");
30
+ return new OpenCodeGoProvider({ apiKey, model: profileString(request, "model") });
31
+ },
32
+ });
33
+ },
34
+ });
35
+ }
36
+ export * from "./provider.js";
37
+ export default createOpenCodeGoPlugin();
@@ -0,0 +1,47 @@
1
+ import type { ModelInfo, ModelProvider, ProviderEvent } from "@alisio/sdk";
2
+ export type OpenCodeProtocol = "responses" | "chat" | "messages";
3
+ export type OpenCodeProduct = "opencode-go";
4
+ /** Explicit allow-list by documented family. Unknown future catalog entries fail closed. */
5
+ export declare function classifyOpenCodeGoModel(id: string): OpenCodeProtocol | undefined;
6
+ export interface OpenCodeGatewayConfig {
7
+ apiKey: string;
8
+ model: string;
9
+ product: OpenCodeProduct;
10
+ baseURL: string;
11
+ classify: (id: string) => OpenCodeProtocol | undefined;
12
+ userAgent?: string;
13
+ fetch?: typeof fetch;
14
+ }
15
+ export declare class OpenCodeGatewayProvider implements ModelProvider {
16
+ #private;
17
+ readonly model: string;
18
+ readonly id: string;
19
+ private readonly baseURL;
20
+ private readonly request;
21
+ private readonly fallbackSession;
22
+ private readonly product;
23
+ private readonly classify;
24
+ private readonly protocol?;
25
+ private readonly userAgent;
26
+ private readonly displayName;
27
+ constructor(config: OpenCodeGatewayConfig);
28
+ private headers;
29
+ listModels(signal: AbortSignal): Promise<ModelInfo[]>;
30
+ stream(request: Parameters<ModelProvider["stream"]>[0]): AsyncIterable<ProviderEvent>;
31
+ private post;
32
+ private chat;
33
+ private responsesInput;
34
+ private responses;
35
+ private anthropicMessages;
36
+ private messages;
37
+ }
38
+ export interface OpenCodeGoConfig {
39
+ apiKey: string;
40
+ model: string;
41
+ baseURL?: string;
42
+ userAgent?: string;
43
+ fetch?: typeof fetch;
44
+ }
45
+ export declare class OpenCodeGoProvider extends OpenCodeGatewayProvider {
46
+ constructor(config: OpenCodeGoConfig);
47
+ }
@@ -0,0 +1,486 @@
1
+ const protocolMap = (groups) => new Map(Object.entries(groups).flatMap(([protocol, models]) => (models ?? []).map((model) => [model, protocol])));
2
+ const GO_MODELS = protocolMap({
3
+ responses: [
4
+ "grok-4.7",
5
+ "grok-4.6",
6
+ "gpt-6-luna",
7
+ "gpt-5.6-luna",
8
+ "muse-spark-1.3-contributor",
9
+ "muse-spark-1.2-contributor",
10
+ ],
11
+ chat: [
12
+ "glm-5.3-flash",
13
+ "glm-5.3",
14
+ "glm-5.2",
15
+ "glm-5.1",
16
+ "kimi-k3",
17
+ "kimi-k2.7-code",
18
+ "kimi-k2.6",
19
+ "longcat-2.0",
20
+ "deepseek-v4.1-flash",
21
+ "deepseek-v4-pro",
22
+ "deepseek-v4-flash",
23
+ "deepseek-v4-flash-vision-exp",
24
+ "mimo-v2.6-flash",
25
+ "mimo-v2.6-pro",
26
+ "mimo-v2.5",
27
+ "mimo-v2.5-pro",
28
+ "hy4-preview",
29
+ "hy3",
30
+ "space-bunny-free",
31
+ "longcat-2.5-preview-free",
32
+ ],
33
+ messages: [
34
+ "minimax-m3",
35
+ "minimax-m2.7",
36
+ "minimax-m2.5",
37
+ "qwen3.8-max",
38
+ "qwen3.8-flash",
39
+ "qwen3.7-max",
40
+ "qwen3.7-plus",
41
+ "qwen3.6-plus",
42
+ ],
43
+ });
44
+ /** Explicit allow-list by documented family. Unknown future catalog entries fail closed. */
45
+ export function classifyOpenCodeGoModel(id) {
46
+ const model = id.replace(/^opencode-go\//, "").toLowerCase();
47
+ return GO_MODELS.get(model);
48
+ }
49
+ const dataUrl = (a) => `data:${a.mimeType};base64,${a.data}`;
50
+ const modelId = (id) => id.replace(/^opencode(?:-go)?\//, "");
51
+ async function* sse(response) {
52
+ if (!response.ok)
53
+ throw new Error(`OpenCode request failed: HTTP ${response.status}`);
54
+ if (!response.body)
55
+ throw new Error("OpenCode response has no body");
56
+ const decoder = new TextDecoder();
57
+ let buffer = "";
58
+ for await (const chunk of response.body) {
59
+ buffer += decoder.decode(chunk, { stream: true }).replace(/\r\n/g, "\n");
60
+ let boundary = buffer.indexOf("\n\n");
61
+ while (boundary >= 0) {
62
+ const frame = buffer.slice(0, boundary);
63
+ buffer = buffer.slice(boundary + 2);
64
+ const data = frame
65
+ .split("\n")
66
+ .filter((line) => line.startsWith("data:"))
67
+ .map((line) => line.slice(5).trimStart())
68
+ .join("\n");
69
+ if (data && data !== "[DONE]")
70
+ yield JSON.parse(data);
71
+ boundary = buffer.indexOf("\n\n");
72
+ }
73
+ }
74
+ }
75
+ function functionTools(tools) {
76
+ return tools.map((tool) => ({
77
+ type: "function",
78
+ function: { name: tool.name, description: tool.description, parameters: tool.inputSchema },
79
+ }));
80
+ }
81
+ function chatMessages(instructions, messages) {
82
+ const output = [{ role: "system", content: instructions }];
83
+ for (const message of messages) {
84
+ if (message.role === "user") {
85
+ const content = message.attachments?.length
86
+ ? [
87
+ ...(message.text.trim() ? [{ type: "text", text: message.text }] : []),
88
+ ...message.attachments.map((a) => ({
89
+ type: "image_url",
90
+ image_url: { url: dataUrl(a) },
91
+ })),
92
+ ]
93
+ : message.text;
94
+ output.push({ role: "user", content });
95
+ }
96
+ else if (message.role === "tool")
97
+ output.push({
98
+ role: "tool",
99
+ tool_call_id: message.callId,
100
+ content: JSON.stringify(message.result),
101
+ });
102
+ else
103
+ output.push({
104
+ role: "assistant",
105
+ content: message.text || null,
106
+ ...(message.calls.length
107
+ ? {
108
+ tool_calls: message.calls.map((call) => ({
109
+ id: call.id,
110
+ type: "function",
111
+ function: { name: call.name, arguments: call.arguments },
112
+ })),
113
+ }
114
+ : {}),
115
+ });
116
+ }
117
+ return output;
118
+ }
119
+ export class OpenCodeGatewayProvider {
120
+ model;
121
+ id;
122
+ baseURL;
123
+ request;
124
+ #apiKey;
125
+ fallbackSession = crypto.randomUUID();
126
+ product;
127
+ classify;
128
+ protocol;
129
+ userAgent;
130
+ displayName;
131
+ constructor(config) {
132
+ this.model = config.model;
133
+ this.product = config.product;
134
+ this.displayName = "OpenCode Go";
135
+ this.classify = config.classify;
136
+ this.baseURL = config.baseURL.replace(/\/$/, "");
137
+ this.userAgent = config.userAgent ?? "alisio/0.1.0-alpha.1";
138
+ const url = new URL(this.baseURL);
139
+ if (url.protocol !== "https:" && url.hostname !== "127.0.0.1" && url.hostname !== "localhost")
140
+ throw new Error("OpenCode base URL must use HTTPS");
141
+ if (url.username || url.password)
142
+ throw new Error("OpenCode base URL must not contain credentials");
143
+ this.request = config.fetch ?? fetch;
144
+ this.#apiKey = config.apiKey;
145
+ this.protocol = this.model ? this.classify(this.model) : undefined;
146
+ if (this.model && !this.protocol)
147
+ throw new Error(`Unsupported ${this.displayName} model family: ${modelId(this.model)}`);
148
+ this.id = `${this.product}:${this.protocol ?? "catalog"}:${this.baseURL}`;
149
+ }
150
+ headers(auth, sessionId, messages = false) {
151
+ return {
152
+ ...(auth ? { authorization: `Bearer ${this.#apiKey}` } : {}),
153
+ ...(auth ? { "content-type": "application/json" } : {}),
154
+ "user-agent": this.userAgent,
155
+ ...(messages ? { "anthropic-version": "2023-06-01" } : {}),
156
+ ...(sessionId ? { "x-opencode-session": sessionId } : {}),
157
+ };
158
+ }
159
+ async listModels(signal) {
160
+ const response = await this.request(`${this.baseURL}/models`, {
161
+ headers: this.headers(false),
162
+ signal,
163
+ });
164
+ if (!response.ok)
165
+ throw new Error(`${this.displayName} model catalog failed: HTTP ${response.status}`);
166
+ const body = (await response.json());
167
+ return (body.data ?? []).flatMap((entry) => {
168
+ if (typeof entry.id !== "string" || !this.classify(entry.id))
169
+ return [];
170
+ const context = [entry.context_window, entry.context_length].find((value) => typeof value === "number" && value > 0);
171
+ const maxOutput = entry.max_output_tokens;
172
+ const modalities = Array.isArray(entry.modalities)
173
+ ? entry.modalities.filter((value) => typeof value === "string")
174
+ : undefined;
175
+ const capabilitySource = [entry.api, entry.capabilities, entry.capability].find((value) => !!value && typeof value === "object");
176
+ const capabilities = capabilitySource
177
+ ? Object.fromEntries(Object.entries(capabilitySource).filter((value) => ["boolean", "string", "number"].includes(typeof value[1])))
178
+ : {};
179
+ return [
180
+ {
181
+ id: `${this.product}/${entry.id}`,
182
+ ...(typeof entry.name === "string" ? { name: entry.name } : {}),
183
+ ...(context ? { contextWindow: context } : {}),
184
+ ...(typeof maxOutput === "number" && maxOutput > 0 ? { maxOutputTokens: maxOutput } : {}),
185
+ ...(modalities?.length ? { modalities } : {}),
186
+ ...(Object.keys(capabilities).length ? { capabilities } : {}),
187
+ },
188
+ ];
189
+ });
190
+ }
191
+ async *stream(request) {
192
+ const model = modelId(request.model || this.model);
193
+ const protocol = this.classify(model);
194
+ if (!protocol)
195
+ throw new Error(`Unsupported ${this.displayName} model family: ${model}`);
196
+ if (this.protocol && protocol !== this.protocol)
197
+ throw new Error(`Model ${model} uses ${protocol}, but this session is bound to ${this.protocol}; start a fresh session`);
198
+ const sessionId = request.sessionId ?? this.fallbackSession;
199
+ if (protocol === "chat")
200
+ yield* this.chat(model, request, sessionId);
201
+ else if (protocol === "responses")
202
+ yield* this.responses(model, request, sessionId);
203
+ else
204
+ yield* this.messages(model, request, sessionId);
205
+ }
206
+ async post(path, body, signal, sessionId, messages = false) {
207
+ return this.request(`${this.baseURL}/${path}`, {
208
+ method: "POST",
209
+ headers: this.headers(true, sessionId, messages),
210
+ body: JSON.stringify(body),
211
+ signal,
212
+ });
213
+ }
214
+ async *chat(model, request, sessionId) {
215
+ const response = await this.post("chat/completions", {
216
+ model,
217
+ messages: chatMessages(request.instructions, request.messages),
218
+ stream: true,
219
+ stream_options: { include_usage: true },
220
+ max_tokens: request.maxOutputTokens,
221
+ ...(request.tools.length ? { tools: functionTools(request.tools) } : {}),
222
+ }, request.signal, sessionId);
223
+ let text = "";
224
+ let finish;
225
+ let usage;
226
+ const calls = new Map();
227
+ for await (const chunk of sse(response)) {
228
+ if (chunk.usage)
229
+ usage = {
230
+ input: chunk.usage.prompt_tokens ?? 0,
231
+ output: chunk.usage.completion_tokens ?? 0,
232
+ ...(typeof chunk.usage.prompt_cache_hit_tokens === "number"
233
+ ? { cachedInput: chunk.usage.prompt_cache_hit_tokens }
234
+ : {}),
235
+ };
236
+ const choice = chunk.choices?.[0];
237
+ if (!choice)
238
+ continue;
239
+ const reasoning = choice.delta?.reasoning_content;
240
+ if (typeof reasoning === "string" && reasoning)
241
+ yield { type: "reasoning_delta", delta: reasoning };
242
+ if (typeof choice.delta?.content === "string" && choice.delta.content) {
243
+ text += choice.delta.content;
244
+ yield { type: "text_delta", delta: choice.delta.content };
245
+ }
246
+ for (const part of choice.delta?.tool_calls ?? []) {
247
+ const call = calls.get(part.index) ?? { id: "", name: "", arguments: "" };
248
+ if (part.id)
249
+ call.id = part.id;
250
+ if (part.function?.name)
251
+ call.name += part.function.name;
252
+ if (part.function?.arguments)
253
+ call.arguments += part.function.arguments;
254
+ calls.set(part.index, call);
255
+ }
256
+ if (choice.finish_reason)
257
+ finish = choice.finish_reason;
258
+ }
259
+ if (finish !== "stop" && finish !== "tool_calls")
260
+ throw new Error(`OpenCode chat response incomplete: ${finish ?? "stream ended"}`);
261
+ const completed = [...calls.entries()].sort(([a], [b]) => a - b).map(([, call]) => call);
262
+ if (completed.some((call) => !call.id || !call.name))
263
+ throw new Error("Incomplete tool call");
264
+ yield { type: "completed", message: { role: "assistant", text, calls: completed }, usage };
265
+ }
266
+ responsesInput(messages) {
267
+ const input = [];
268
+ for (const message of messages) {
269
+ if (message.role === "user")
270
+ input.push({
271
+ role: "user",
272
+ content: message.attachments?.length
273
+ ? [
274
+ ...(message.text.trim() ? [{ type: "input_text", text: message.text }] : []),
275
+ ...message.attachments.map((a) => ({ type: "input_image", image_url: dataUrl(a) })),
276
+ ]
277
+ : message.text,
278
+ });
279
+ else if (message.role === "tool")
280
+ input.push({
281
+ type: "function_call_output",
282
+ call_id: message.callId,
283
+ output: JSON.stringify(message.result),
284
+ });
285
+ else if (message.providerData)
286
+ input.push(...message.providerData);
287
+ else
288
+ throw new Error("Missing provider continuation data for OpenCode Responses session");
289
+ }
290
+ return input;
291
+ }
292
+ async *responses(model, request, sessionId) {
293
+ const response = await this.post("responses", {
294
+ model,
295
+ instructions: request.instructions,
296
+ input: this.responsesInput(request.messages),
297
+ stream: true,
298
+ store: false,
299
+ include: ["reasoning.encrypted_content"],
300
+ max_output_tokens: request.maxOutputTokens,
301
+ tools: request.tools.map((tool) => ({
302
+ type: "function",
303
+ name: tool.name,
304
+ description: tool.description,
305
+ parameters: tool.inputSchema,
306
+ strict: false,
307
+ })),
308
+ }, request.signal, sessionId);
309
+ let complete = false;
310
+ for await (const event of sse(response)) {
311
+ if (event.type === "response.output_text.delta")
312
+ yield { type: "text_delta", delta: event.delta };
313
+ if (event.type === "response.reasoning_summary_text.delta")
314
+ yield { type: "reasoning_delta", delta: event.delta };
315
+ if (["response.failed", "response.incomplete", "error"].includes(event.type))
316
+ throw new Error(`OpenCode Responses request failed: ${event.type}`);
317
+ if (event.type === "response.completed") {
318
+ complete = true;
319
+ const output = event.response.output;
320
+ const calls = output
321
+ .filter((item) => item.type === "function_call")
322
+ .map((item) => ({ id: item.call_id, name: item.name, arguments: item.arguments }));
323
+ const text = output
324
+ .filter((item) => item.type === "message")
325
+ .flatMap((item) => item.content ?? [])
326
+ .filter((item) => item.type === "output_text")
327
+ .map((item) => item.text)
328
+ .join("");
329
+ const u = event.response.usage;
330
+ yield {
331
+ type: "completed",
332
+ message: { role: "assistant", text, calls, providerData: output },
333
+ usage: u
334
+ ? {
335
+ input: u.input_tokens ?? 0,
336
+ output: u.output_tokens ?? 0,
337
+ ...(typeof u.input_tokens_details?.cached_tokens === "number"
338
+ ? { cachedInput: u.input_tokens_details.cached_tokens }
339
+ : {}),
340
+ }
341
+ : undefined,
342
+ };
343
+ }
344
+ }
345
+ if (!complete)
346
+ throw new Error("OpenCode Responses stream ended before completion");
347
+ }
348
+ anthropicMessages(messages) {
349
+ const output = [];
350
+ for (const message of messages) {
351
+ if (message.role === "user") {
352
+ const content = [];
353
+ if (message.text)
354
+ content.push({ type: "text", text: message.text });
355
+ for (const attachment of message.attachments ?? [])
356
+ content.push({
357
+ type: "image",
358
+ source: { type: "base64", media_type: attachment.mimeType, data: attachment.data },
359
+ });
360
+ output.push({ role: "user", content });
361
+ }
362
+ else if (message.role === "tool")
363
+ if (output.at(-1)?.role === "user" &&
364
+ output.at(-1)?.content?.every((part) => part.type === "tool_result"))
365
+ output.at(-1)?.content.push({
366
+ type: "tool_result",
367
+ tool_use_id: message.callId,
368
+ content: JSON.stringify(message.result),
369
+ });
370
+ else
371
+ output.push({
372
+ role: "user",
373
+ content: [
374
+ {
375
+ type: "tool_result",
376
+ tool_use_id: message.callId,
377
+ content: JSON.stringify(message.result),
378
+ },
379
+ ],
380
+ });
381
+ else
382
+ output.push({
383
+ role: "assistant",
384
+ content: message.providerData ?? [
385
+ ...(message.text ? [{ type: "text", text: message.text }] : []),
386
+ ...message.calls.map((call) => ({
387
+ type: "tool_use",
388
+ id: call.id,
389
+ name: call.name,
390
+ input: JSON.parse(call.arguments || "{}"),
391
+ })),
392
+ ],
393
+ });
394
+ }
395
+ return output;
396
+ }
397
+ async *messages(model, request, sessionId) {
398
+ const response = await this.post("messages", {
399
+ model,
400
+ system: request.instructions,
401
+ messages: this.anthropicMessages(request.messages),
402
+ max_tokens: request.maxOutputTokens,
403
+ stream: true,
404
+ ...(request.tools.length
405
+ ? {
406
+ tools: request.tools.map((tool) => ({
407
+ name: tool.name,
408
+ description: tool.description,
409
+ input_schema: tool.inputSchema,
410
+ })),
411
+ }
412
+ : {}),
413
+ }, request.signal, sessionId, true);
414
+ let text = "";
415
+ let input = 0;
416
+ let output = 0;
417
+ let stopReason;
418
+ const blocks = [];
419
+ const argumentParts = new Map();
420
+ for await (const event of sse(response)) {
421
+ if (event.type === "message_start")
422
+ input = event.message?.usage?.input_tokens ?? 0;
423
+ if (event.type === "content_block_start") {
424
+ blocks[event.index] = event.content_block;
425
+ if (event.content_block?.type === "text" && event.content_block.text) {
426
+ text += event.content_block.text;
427
+ yield { type: "text_delta", delta: event.content_block.text };
428
+ }
429
+ if (event.content_block?.type === "tool_use")
430
+ argumentParts.set(event.index, "");
431
+ }
432
+ if (event.type === "content_block_delta") {
433
+ if (event.delta?.type === "text_delta") {
434
+ text += event.delta.text;
435
+ const block = blocks[event.index];
436
+ if (block?.type === "text")
437
+ block.text = (block.text ?? "") + event.delta.text;
438
+ yield { type: "text_delta", delta: event.delta.text };
439
+ }
440
+ if (event.delta?.type === "thinking_delta" && event.delta.thinking) {
441
+ const block = blocks[event.index];
442
+ if (block?.type === "thinking")
443
+ block.thinking = (block.thinking ?? "") + event.delta.thinking;
444
+ yield { type: "reasoning_delta", delta: event.delta.thinking };
445
+ }
446
+ if (event.delta?.type === "input_json_delta")
447
+ argumentParts.set(event.index, (argumentParts.get(event.index) ?? "") + event.delta.partial_json);
448
+ if (event.delta?.type === "signature_delta" && event.delta.signature) {
449
+ const block = blocks[event.index];
450
+ if (block?.type === "thinking")
451
+ block.signature = event.delta.signature;
452
+ }
453
+ }
454
+ if (event.type === "message_delta") {
455
+ output = event.usage?.output_tokens ?? output;
456
+ stopReason = event.delta?.stop_reason ?? stopReason;
457
+ }
458
+ }
459
+ if (stopReason !== "end_turn" && stopReason !== "tool_use")
460
+ throw new Error(`OpenCode Messages response incomplete: ${stopReason ?? "stream ended"}`);
461
+ const calls = [];
462
+ for (let index = 0; index < blocks.length; index++) {
463
+ const block = blocks[index];
464
+ if (!block || block.type !== "tool_use")
465
+ continue;
466
+ const args = argumentParts.get(index) || JSON.stringify(block.input ?? {});
467
+ calls.push({ id: block.id, name: block.name, arguments: args });
468
+ block.input = JSON.parse(args || "{}");
469
+ }
470
+ yield {
471
+ type: "completed",
472
+ message: { role: "assistant", text, calls, providerData: blocks },
473
+ usage: { input, output },
474
+ };
475
+ }
476
+ }
477
+ export class OpenCodeGoProvider extends OpenCodeGatewayProvider {
478
+ constructor(config) {
479
+ super({
480
+ ...config,
481
+ product: "opencode-go",
482
+ baseURL: config.baseURL ?? "https://opencode.ai/zen/go/v1",
483
+ classify: classifyOpenCodeGoModel,
484
+ });
485
+ }
486
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@alisio/plugin-opencode-go",
3
+ "version": "0.1.0-alpha.2",
4
+ "description": "Built-in OpenCode Go model provider for Alisio.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "types": "./dist/index.d.ts",
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "sideEffects": false,
20
+ "engines": {
21
+ "node": ">=22.16"
22
+ },
23
+ "peerDependencies": {
24
+ "@alisio/sdk": "^0.1.0-alpha.2"
25
+ },
26
+ "devDependencies": {
27
+ "@alisio/sdk": "0.1.0-alpha.2"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "registry": "https://registry.npmjs.com/"
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.build.json"
35
+ }
36
+ }