@velum-labs/routekit-tool-opencode 0.18.5 → 1.0.0
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/driver.d.ts +1 -1
- package/dist/driver.js +133 -103
- package/dist/launch.js +3 -2
- package/package.json +6 -6
package/dist/driver.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import type { DriverContext, HarnessDriver, StartSessionOptions } from "@velum-labs/routekit-harness-core";
|
|
2
|
+
import { z } from "zod";
|
|
3
3
|
export declare const opencodeDriverConfigSchema: z.ZodObject<{
|
|
4
4
|
command: z.ZodDefault<z.ZodString>;
|
|
5
5
|
serverUrl: z.ZodOptional<z.ZodString>;
|
package/dist/driver.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import { createOpencodeClient } from "@opencode-ai/sdk/client";
|
|
3
2
|
import { createOpencodeServer } from "@opencode-ai/sdk/server";
|
|
4
|
-
import {
|
|
3
|
+
import { asHarnessError, buildChildEnv, createCachedHarnessDriver, DEFAULT_AUTOMATION_APPROVAL_POLICY, HarnessError, nowIso, probeCliVersion, resolveDriverEnv, resumeStringField, SessionResourceRegistry, SingleFlightTurnController } from "@velum-labs/routekit-harness-core";
|
|
4
|
+
import { z } from "zod";
|
|
5
5
|
import { opencodeProviderConfig } from "./launch.js";
|
|
6
6
|
const RESUME_CURSOR_VERSION = 1;
|
|
7
7
|
const DEFAULT_COMMAND = "opencode";
|
|
@@ -17,9 +17,6 @@ export const opencodeDriverConfigSchema = z.object({
|
|
|
17
17
|
model: z.string().optional(),
|
|
18
18
|
providerId: z.string().optional()
|
|
19
19
|
});
|
|
20
|
-
function nowIso() {
|
|
21
|
-
return new Date().toISOString();
|
|
22
|
-
}
|
|
23
20
|
function itemTypeForTool(tool) {
|
|
24
21
|
const lower = tool.toLowerCase();
|
|
25
22
|
if (lower.includes("bash") || lower.includes("shell"))
|
|
@@ -40,6 +37,7 @@ class OpencodeSession {
|
|
|
40
37
|
#reasoning;
|
|
41
38
|
#sessionId;
|
|
42
39
|
#stopped = false;
|
|
40
|
+
#turns = new SingleFlightTurnController();
|
|
43
41
|
constructor(input) {
|
|
44
42
|
this.#backend = input.backend;
|
|
45
43
|
this.#sessionId = input.sessionId;
|
|
@@ -55,90 +53,119 @@ class OpencodeSession {
|
|
|
55
53
|
async *sendTurn(input) {
|
|
56
54
|
if (this.#stopped)
|
|
57
55
|
throw new HarnessError("session_closed", "opencode session is stopped");
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
// A function call so TS does not narrow `aborted` to a constant across the
|
|
61
|
-
// await below (the signal can fire mid-turn).
|
|
62
|
-
const isAborted = () => input.signal?.aborted === true;
|
|
63
|
-
yield { ...base, type: "turn.started", turnId };
|
|
64
|
-
if (isAborted()) {
|
|
65
|
-
yield { ...base, type: "turn.completed", turnId, endReason: "aborted" };
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
let result;
|
|
69
|
-
const reasoning = input.reasoning ?? this.#reasoning;
|
|
70
|
-
if (reasoning !== undefined &&
|
|
71
|
-
reasoning.mode !== "auto" &&
|
|
72
|
-
reasoning.mode !== "effort") {
|
|
73
|
-
throw new HarnessError("invalid_config", `OpenCode variants cannot represent reasoning mode "${reasoning.mode}"`);
|
|
74
|
-
}
|
|
56
|
+
const turn = this.#turns.start(input.signal);
|
|
57
|
+
let settled = false;
|
|
75
58
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
...(reasoning !== undefined ? { reasoning } : {}),
|
|
83
|
-
...(input.signal !== undefined ? { signal: input.signal } : {})
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
59
|
+
const base = { kind: this.#kind, sessionId: this.#sessionId, at: nowIso() };
|
|
60
|
+
const turnId = `${this.#sessionId}:turn:${Date.now()}`;
|
|
61
|
+
// A function call so TS does not narrow `aborted` to a constant across the
|
|
62
|
+
// await below (the signal can fire mid-turn).
|
|
63
|
+
const isAborted = () => turn.signal.aborted;
|
|
64
|
+
yield { ...base, type: "turn.started", turnId };
|
|
87
65
|
if (isAborted()) {
|
|
88
66
|
yield { ...base, type: "turn.completed", turnId, endReason: "aborted" };
|
|
89
67
|
return;
|
|
90
68
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
69
|
+
let result;
|
|
70
|
+
const reasoning = input.reasoning ?? this.#reasoning;
|
|
71
|
+
if (reasoning !== undefined && reasoning.mode !== "auto" && reasoning.mode !== "effort") {
|
|
72
|
+
throw new HarnessError("invalid_config", `OpenCode variants cannot represent reasoning mode "${reasoning.mode}"`);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
result = await this.#backend.prompt({
|
|
76
|
+
sessionId: this.#sessionId,
|
|
77
|
+
cwd: this.#cwd,
|
|
78
|
+
prompt: input.prompt,
|
|
79
|
+
...(this.#model !== undefined ? { model: this.#model } : {}),
|
|
80
|
+
...(this.#providerId !== undefined ? { providerId: this.#providerId } : {}),
|
|
81
|
+
...(reasoning !== undefined ? { reasoning } : {}),
|
|
82
|
+
signal: turn.signal
|
|
83
|
+
});
|
|
84
|
+
settled = true;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
settled = true;
|
|
88
|
+
if (isAborted()) {
|
|
89
|
+
yield { ...base, type: "turn.completed", turnId, endReason: "aborted" };
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const harnessError = asHarnessError(error);
|
|
93
|
+
yield {
|
|
94
|
+
...base,
|
|
95
|
+
type: "turn.failed",
|
|
96
|
+
turnId,
|
|
97
|
+
errorCode: harnessError.code,
|
|
98
|
+
message: harnessError.message
|
|
99
|
+
};
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let usage;
|
|
103
|
+
for (const part of result.parts) {
|
|
104
|
+
const raw = { source: "opencode.sdk.part", method: part.type };
|
|
105
|
+
switch (part.type) {
|
|
106
|
+
case "text":
|
|
107
|
+
if (part.text.length > 0) {
|
|
108
|
+
yield {
|
|
109
|
+
...base,
|
|
110
|
+
type: "content.delta",
|
|
111
|
+
turnId,
|
|
112
|
+
stream: "assistant_text",
|
|
113
|
+
text: part.text,
|
|
114
|
+
raw
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
case "reasoning":
|
|
119
|
+
if (part.text.length > 0) {
|
|
120
|
+
yield {
|
|
121
|
+
...base,
|
|
122
|
+
type: "content.delta",
|
|
123
|
+
turnId,
|
|
124
|
+
stream: "reasoning_text",
|
|
125
|
+
text: part.text,
|
|
126
|
+
raw
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
case "tool":
|
|
131
|
+
yield {
|
|
132
|
+
...base,
|
|
133
|
+
type: "item.completed",
|
|
134
|
+
turnId,
|
|
135
|
+
itemId: part.callId,
|
|
136
|
+
itemType: itemTypeForTool(part.tool),
|
|
137
|
+
status: part.status === "failed" ? "failed" : "completed",
|
|
138
|
+
raw
|
|
126
139
|
};
|
|
140
|
+
break;
|
|
141
|
+
case "step-finish":
|
|
142
|
+
if (part.tokens !== undefined) {
|
|
143
|
+
usage = {
|
|
144
|
+
inputTokens: part.tokens.input,
|
|
145
|
+
outputTokens: part.tokens.output,
|
|
146
|
+
reasoningOutputTokens: part.tokens.reasoning
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
default: {
|
|
151
|
+
const exhausted = part;
|
|
152
|
+
throw new Error(`unsupported opencode part: ${String(exhausted)}`);
|
|
127
153
|
}
|
|
128
|
-
break;
|
|
129
|
-
default: {
|
|
130
|
-
const exhausted = part;
|
|
131
|
-
throw new Error(`unsupported opencode part: ${String(exhausted)}`);
|
|
132
154
|
}
|
|
133
155
|
}
|
|
156
|
+
yield {
|
|
157
|
+
...base,
|
|
158
|
+
type: "turn.completed",
|
|
159
|
+
turnId,
|
|
160
|
+
endReason: "completed",
|
|
161
|
+
...(usage !== undefined ? { usage } : {})
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
if (settled)
|
|
166
|
+
turn.complete();
|
|
167
|
+
turn.dispose();
|
|
134
168
|
}
|
|
135
|
-
yield {
|
|
136
|
-
...base,
|
|
137
|
-
type: "turn.completed",
|
|
138
|
-
turnId,
|
|
139
|
-
endReason: "completed",
|
|
140
|
-
...(usage !== undefined ? { usage } : {})
|
|
141
|
-
};
|
|
142
169
|
}
|
|
143
170
|
async respondToRequest() {
|
|
144
171
|
// Unattended runs use an auto-approve policy applied at session
|
|
@@ -146,16 +173,26 @@ class OpencodeSession {
|
|
|
146
173
|
throw new HarnessError("protocol_parse", "opencode driver does not surface interactive approval requests in buffered mode");
|
|
147
174
|
}
|
|
148
175
|
async interrupt() {
|
|
149
|
-
|
|
176
|
+
this.#turns.interrupt();
|
|
177
|
+
await this.#backend
|
|
178
|
+
.abort({ sessionId: this.#sessionId, cwd: this.#cwd })
|
|
179
|
+
.catch(() => undefined);
|
|
150
180
|
}
|
|
151
181
|
resumeCursor() {
|
|
152
|
-
return {
|
|
182
|
+
return {
|
|
183
|
+
version: RESUME_CURSOR_VERSION,
|
|
184
|
+
kind: this.#kind,
|
|
185
|
+
data: { sessionId: this.#sessionId }
|
|
186
|
+
};
|
|
153
187
|
}
|
|
154
188
|
async stop() {
|
|
155
189
|
if (this.#stopped)
|
|
156
190
|
return;
|
|
157
191
|
this.#stopped = true;
|
|
158
|
-
|
|
192
|
+
this.#turns.interrupt(new Error("opencode session stopped"));
|
|
193
|
+
await this.#backend
|
|
194
|
+
.abort({ sessionId: this.#sessionId, cwd: this.#cwd })
|
|
195
|
+
.catch(() => undefined);
|
|
159
196
|
}
|
|
160
197
|
// Kept for symmetry with other drivers; opencode's policy is applied at
|
|
161
198
|
// session creation rather than per request.
|
|
@@ -164,10 +201,7 @@ class OpencodeSession {
|
|
|
164
201
|
}
|
|
165
202
|
}
|
|
166
203
|
function resumeSessionId(resume) {
|
|
167
|
-
|
|
168
|
-
return undefined;
|
|
169
|
-
const data = resume.data;
|
|
170
|
-
return typeof data.sessionId === "string" ? data.sessionId : undefined;
|
|
204
|
+
return resumeStringField(resume, "opencode", "sessionId");
|
|
171
205
|
}
|
|
172
206
|
class OpencodeInstance {
|
|
173
207
|
kind = "opencode";
|
|
@@ -176,7 +210,7 @@ class OpencodeInstance {
|
|
|
176
210
|
#status;
|
|
177
211
|
#backendFactory;
|
|
178
212
|
#backend;
|
|
179
|
-
#sessions = new
|
|
213
|
+
#sessions = new SessionResourceRegistry();
|
|
180
214
|
constructor(input) {
|
|
181
215
|
this.#config = input.config;
|
|
182
216
|
this.#context = input.context;
|
|
@@ -193,6 +227,7 @@ class OpencodeInstance {
|
|
|
193
227
|
return this.#backend;
|
|
194
228
|
}
|
|
195
229
|
async startSession(options) {
|
|
230
|
+
this.#sessions.assertOpen();
|
|
196
231
|
const backend = await this.#ensureBackend();
|
|
197
232
|
const resume = resumeSessionId(options.resume);
|
|
198
233
|
const created = await backend.createSession({
|
|
@@ -204,23 +239,16 @@ class OpencodeInstance {
|
|
|
204
239
|
sessionId: created.sessionId,
|
|
205
240
|
cwd: options.cwd,
|
|
206
241
|
approvalPolicy: options.approvalPolicy ?? DEFAULT_AUTOMATION_APPROVAL_POLICY,
|
|
207
|
-
...(options.model ?? this.#config.model !== undefined
|
|
242
|
+
...((options.model ?? this.#config.model !== undefined)
|
|
208
243
|
? { model: options.model ?? this.#config.model }
|
|
209
244
|
: {}),
|
|
210
|
-
...(this.#config.providerId !== undefined
|
|
211
|
-
|
|
212
|
-
: {}),
|
|
213
|
-
...(options.reasoning !== undefined
|
|
214
|
-
? { reasoning: options.reasoning }
|
|
215
|
-
: {})
|
|
245
|
+
...(this.#config.providerId !== undefined ? { providerId: this.#config.providerId } : {}),
|
|
246
|
+
...(options.reasoning !== undefined ? { reasoning: options.reasoning } : {})
|
|
216
247
|
});
|
|
217
|
-
this.#sessions.
|
|
218
|
-
return session;
|
|
248
|
+
return this.#sessions.manage(session);
|
|
219
249
|
}
|
|
220
250
|
async dispose() {
|
|
221
|
-
|
|
222
|
-
await session.stop();
|
|
223
|
-
this.#sessions.clear();
|
|
251
|
+
await this.#sessions.dispose();
|
|
224
252
|
await this.#backend?.dispose().catch(() => undefined);
|
|
225
253
|
this.#backend = undefined;
|
|
226
254
|
}
|
|
@@ -258,7 +286,11 @@ const defaultBackendFactory = async (config, context) => {
|
|
|
258
286
|
type: "tool",
|
|
259
287
|
tool: String(part.tool ?? "tool"),
|
|
260
288
|
callId: String(part.callID ?? part.id ?? "call"),
|
|
261
|
-
status: state?.status === "error"
|
|
289
|
+
status: state?.status === "error"
|
|
290
|
+
? "failed"
|
|
291
|
+
: state?.status === "completed"
|
|
292
|
+
? "completed"
|
|
293
|
+
: "running"
|
|
262
294
|
});
|
|
263
295
|
}
|
|
264
296
|
else if (part.type === "step-finish") {
|
|
@@ -288,9 +320,7 @@ const defaultBackendFactory = async (config, context) => {
|
|
|
288
320
|
body: {
|
|
289
321
|
parts: [{ type: "text", text: prompt }],
|
|
290
322
|
...modelBody,
|
|
291
|
-
...(reasoning?.mode === "effort"
|
|
292
|
-
? { variant: reasoning.effort }
|
|
293
|
-
: {})
|
|
323
|
+
...(reasoning?.mode === "effort" ? { variant: reasoning.effort } : {})
|
|
294
324
|
},
|
|
295
325
|
...(signal !== undefined ? { signal } : {})
|
|
296
326
|
});
|
package/dist/launch.js
CHANGED
|
@@ -2,7 +2,8 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { reasoningEffortDescriptors } from "@velum-labs/routekit-contracts";
|
|
5
|
-
import {
|
|
5
|
+
import { gatewayOpenAiBaseUrl } from "@velum-labs/routekit-runtime/network";
|
|
6
|
+
import { spawnTool } from "@velum-labs/routekit-runtime/process";
|
|
6
7
|
const PROVIDER_ID = "routekit";
|
|
7
8
|
export function opencodeModelArg(model) {
|
|
8
9
|
return `${PROVIDER_ID}/${model}`;
|
|
@@ -40,7 +41,7 @@ export function opencodeProviderConfig(spec) {
|
|
|
40
41
|
npm: "@ai-sdk/openai-compatible",
|
|
41
42
|
name: "RouteKit gateway",
|
|
42
43
|
options: {
|
|
43
|
-
baseURL:
|
|
44
|
+
baseURL: gatewayOpenAiBaseUrl(spec.gatewayUrl),
|
|
44
45
|
...(spec.auth?.token !== undefined ? { apiKey: spec.auth.token } : {})
|
|
45
46
|
},
|
|
46
47
|
models
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-tool-opencode",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/routekit.git",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@opencode-ai/sdk": "1.17.13",
|
|
30
30
|
"zod": "4.4.3",
|
|
31
|
-
"@velum-labs/routekit-contracts": "0.
|
|
32
|
-
"@velum-labs/routekit-harness-core": "0.
|
|
33
|
-
"@velum-labs/routekit-tools": "0.
|
|
34
|
-
"@velum-labs/routekit-runtime": "0.
|
|
31
|
+
"@velum-labs/routekit-contracts": "1.0.0",
|
|
32
|
+
"@velum-labs/routekit-harness-core": "1.0.0",
|
|
33
|
+
"@velum-labs/routekit-tools": "1.0.0",
|
|
34
|
+
"@velum-labs/routekit-runtime": "1.0.0"
|
|
35
35
|
},
|
|
36
36
|
"keywords": [
|
|
37
37
|
"llm",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"adapter"
|
|
42
42
|
],
|
|
43
43
|
"scripts": {
|
|
44
|
-
"build": "tsc -
|
|
44
|
+
"build": "tsc -p tsconfig.json",
|
|
45
45
|
"clean": "tsc -b --clean",
|
|
46
46
|
"test": "node --test \"dist/test/*.test.js\""
|
|
47
47
|
}
|