dsh-opencode-go 0.1.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/LICENSE +21 -0
- package/README.md +121 -0
- package/THIRD_PARTY_NOTICES.md +3 -0
- package/cordis.patch.yml +3 -0
- package/docs/independent-package.md +13 -0
- package/docs/verification.md +50 -0
- package/examples/headless.patch.yml +4 -0
- package/examples/migrate-from-fork.patch.yml +3 -0
- package/lib/build-info.json +10 -0
- package/lib/client.js +853 -0
- package/lib/index.js +1158 -0
- package/lib/types/adapter.d.ts +101 -0
- package/lib/types/catalog.d.ts +95 -0
- package/lib/types/client/Section.d.ts +26 -0
- package/lib/types/client/index.d.ts +34 -0
- package/lib/types/client/locales.d.ts +50 -0
- package/lib/types/client/section-controller.d.ts +175 -0
- package/lib/types/client/staged-form.d.ts +184 -0
- package/lib/types/config.d.ts +52 -0
- package/lib/types/conversion/config.d.ts +4 -0
- package/lib/types/conversion/context.d.ts +49 -0
- package/lib/types/conversion/index.d.ts +5 -0
- package/lib/types/conversion/replay.d.ts +62 -0
- package/lib/types/conversion/stream.d.ts +42 -0
- package/lib/types/index.d.ts +51 -0
- package/package.json +174 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1158 @@
|
|
|
1
|
+
var __knownSymbol = (name2, symbol) => (symbol = Symbol[name2]) ? symbol : Symbol.for("Symbol." + name2);
|
|
2
|
+
var __typeError = (msg) => {
|
|
3
|
+
throw TypeError(msg);
|
|
4
|
+
};
|
|
5
|
+
var __using = (stack, value, async) => {
|
|
6
|
+
if (value != null) {
|
|
7
|
+
if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
|
|
8
|
+
var dispose, inner;
|
|
9
|
+
if (async) dispose = value[__knownSymbol("asyncDispose")];
|
|
10
|
+
if (dispose === void 0) {
|
|
11
|
+
dispose = value[__knownSymbol("dispose")];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") __typeError("Object not disposable");
|
|
15
|
+
if (inner) dispose = function() {
|
|
16
|
+
try {
|
|
17
|
+
inner.call(this);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
return Promise.reject(e);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
stack.push([async, dispose, value]);
|
|
23
|
+
} else if (async) {
|
|
24
|
+
stack.push([async]);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
var __callDispose = (stack, error, hasError) => {
|
|
29
|
+
var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
|
|
30
|
+
return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
|
|
31
|
+
};
|
|
32
|
+
var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
|
|
33
|
+
var next = (it) => {
|
|
34
|
+
while (it = stack.pop()) {
|
|
35
|
+
try {
|
|
36
|
+
var result = it[1] && it[1].call(it[2]);
|
|
37
|
+
if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
fail(e);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (hasError) throw error;
|
|
43
|
+
};
|
|
44
|
+
return next();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/index.ts
|
|
48
|
+
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
49
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
50
|
+
import { LlmError as LlmError6, assertUsableApiKey, resolveImageAttachmentAccess } from "@deepseek-ai/dsh-llm";
|
|
51
|
+
|
|
52
|
+
// src/adapter.ts
|
|
53
|
+
import { randomUUID } from "node:crypto";
|
|
54
|
+
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
55
|
+
import {
|
|
56
|
+
LlmAdapter,
|
|
57
|
+
LlmError as LlmError5,
|
|
58
|
+
ReasoningEffortId,
|
|
59
|
+
attributionHeaders as attributionHeaders2,
|
|
60
|
+
contentHasImage as contentHasImage2
|
|
61
|
+
} from "@deepseek-ai/dsh-llm";
|
|
62
|
+
|
|
63
|
+
// src/conversion/context.ts
|
|
64
|
+
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
65
|
+
import { contentHasImage, IMAGE_OFFLOAD_REQUIRED_CODE, LlmError as LlmError2, offloadedImageText, projectOffloadedImages, requestImageHandleText, requiredImageOffload } from "@deepseek-ai/dsh-llm";
|
|
66
|
+
|
|
67
|
+
// src/conversion/replay.ts
|
|
68
|
+
import { LlmError } from "@deepseek-ai/dsh-llm";
|
|
69
|
+
function parseArguments(raw) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(raw);
|
|
72
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
function emptyPiUsage() {
|
|
80
|
+
return {
|
|
81
|
+
input: 0,
|
|
82
|
+
output: 0,
|
|
83
|
+
cacheRead: 0,
|
|
84
|
+
cacheWrite: 0,
|
|
85
|
+
totalTokens: 0,
|
|
86
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function toPiReplayState(message, requestedModel = message.model) {
|
|
90
|
+
const responseModel = message.api === "anthropic-messages" && message.model !== requestedModel ? message.model : message.responseModel;
|
|
91
|
+
const response = {
|
|
92
|
+
kind: "pi-ai",
|
|
93
|
+
version: 2,
|
|
94
|
+
api: message.api,
|
|
95
|
+
provider: message.provider,
|
|
96
|
+
model: requestedModel,
|
|
97
|
+
...responseModel === void 0 ? {} : { responseModel },
|
|
98
|
+
...message.responseId === void 0 ? {} : { responseId: message.responseId },
|
|
99
|
+
...message.providerThinkingLevel === void 0 ? {} : { providerThinkingLevel: message.providerThinkingLevel },
|
|
100
|
+
stopReason: message.stopReason
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
response,
|
|
104
|
+
blocks: message.content.map((block) => {
|
|
105
|
+
switch (block.type) {
|
|
106
|
+
case "text":
|
|
107
|
+
return {
|
|
108
|
+
type: "text",
|
|
109
|
+
...block.textSignature === void 0 ? {} : { textSignature: block.textSignature }
|
|
110
|
+
};
|
|
111
|
+
case "thinking":
|
|
112
|
+
return {
|
|
113
|
+
type: "reasoning",
|
|
114
|
+
...block.thinkingSignature === void 0 ? {} : { thinkingSignature: block.thinkingSignature },
|
|
115
|
+
...block.redacted === void 0 ? {} : { redacted: block.redacted }
|
|
116
|
+
};
|
|
117
|
+
case "toolCall":
|
|
118
|
+
return {
|
|
119
|
+
type: "tool-call",
|
|
120
|
+
...block.thoughtSignature === void 0 ? {} : { thoughtSignature: block.thoughtSignature }
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function invalidReplay(message) {
|
|
127
|
+
throw new LlmError(`invalid pi-ai replay state: ${message}`, "INVALID_REPLAY_STATE");
|
|
128
|
+
}
|
|
129
|
+
function readReplayState(value) {
|
|
130
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return invalidReplay("expected a replay envelope");
|
|
131
|
+
const envelope = value;
|
|
132
|
+
const rawResponse = envelope["response"];
|
|
133
|
+
if (typeof rawResponse !== "object" || rawResponse === null || Array.isArray(rawResponse)) return invalidReplay("expected a response object");
|
|
134
|
+
const response = rawResponse;
|
|
135
|
+
if (response["kind"] !== "pi-ai") return invalidReplay("unknown state kind");
|
|
136
|
+
if (response["version"] !== 2) return invalidReplay(`unsupported version ${String(response["version"])}`);
|
|
137
|
+
for (const key of ["api", "provider", "model"]) {
|
|
138
|
+
if (typeof response[key] !== "string" || response[key].length === 0) return invalidReplay(`${key} must be a non-empty string`);
|
|
139
|
+
}
|
|
140
|
+
if (!["stop", "length", "toolUse", "error", "aborted"].includes(String(response["stopReason"]))) {
|
|
141
|
+
return invalidReplay("unknown stopReason");
|
|
142
|
+
}
|
|
143
|
+
if (response["responseModel"] !== void 0 && typeof response["responseModel"] !== "string") return invalidReplay("responseModel must be a string");
|
|
144
|
+
if (response["responseId"] !== void 0 && typeof response["responseId"] !== "string") return invalidReplay("responseId must be a string");
|
|
145
|
+
if (response["providerThinkingLevel"] !== void 0 && typeof response["providerThinkingLevel"] !== "string") return invalidReplay("providerThinkingLevel must be a string");
|
|
146
|
+
const blocks = envelope["blocks"];
|
|
147
|
+
if (!Array.isArray(blocks)) return invalidReplay("blocks must be an array");
|
|
148
|
+
for (const [index, value2] of blocks.entries()) {
|
|
149
|
+
if (typeof value2 !== "object" || value2 === null || Array.isArray(value2)) return invalidReplay(`block ${index} must be an object`);
|
|
150
|
+
const block = value2;
|
|
151
|
+
if (!["text", "reasoning", "tool-call"].includes(String(block["type"]))) return invalidReplay(`block ${index} has an unknown type`);
|
|
152
|
+
for (const signature of ["textSignature", "thinkingSignature", "thoughtSignature"]) {
|
|
153
|
+
if (block[signature] !== void 0 && typeof block[signature] !== "string") return invalidReplay(`block ${index} ${signature} must be a string`);
|
|
154
|
+
}
|
|
155
|
+
if (block["redacted"] !== void 0 && typeof block["redacted"] !== "boolean") return invalidReplay(`block ${index} redacted must be boolean`);
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
response,
|
|
159
|
+
blocks
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function foreignAssistant(message) {
|
|
163
|
+
const source = message.source.kind === "model" ? message.source : void 0;
|
|
164
|
+
const content = [];
|
|
165
|
+
for (const block of message.content) {
|
|
166
|
+
switch (block.type) {
|
|
167
|
+
case "text":
|
|
168
|
+
content.push({ type: "text", text: block.text });
|
|
169
|
+
break;
|
|
170
|
+
case "reasoning":
|
|
171
|
+
content.push({ type: "thinking", thinking: block.text });
|
|
172
|
+
break;
|
|
173
|
+
case "tool-call":
|
|
174
|
+
content.push({
|
|
175
|
+
type: "toolCall",
|
|
176
|
+
id: block.id,
|
|
177
|
+
name: block.name,
|
|
178
|
+
arguments: parseArguments(block.arguments)
|
|
179
|
+
});
|
|
180
|
+
break;
|
|
181
|
+
case "image":
|
|
182
|
+
throw new LlmError("pi-ai chat history cannot represent structured assistant image output", "UNSUPPORTED_CONTENT");
|
|
183
|
+
default:
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
role: "assistant",
|
|
189
|
+
content,
|
|
190
|
+
// Deliberately never equals a catalog API: absent replay state is foreign
|
|
191
|
+
// even if source names the same provider/model as this request.
|
|
192
|
+
api: "dsh-foreign",
|
|
193
|
+
provider: source?.provider ?? "dsh-foreign",
|
|
194
|
+
model: source?.model ?? "dsh-foreign",
|
|
195
|
+
usage: emptyPiUsage(),
|
|
196
|
+
stopReason: content.some((piece) => piece.type === "toolCall") ? "toolUse" : "stop",
|
|
197
|
+
timestamp: 0
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function replayedAssistant(message, source, rawState) {
|
|
201
|
+
const state = readReplayState(rawState);
|
|
202
|
+
if (state.response.provider !== source.provider) return invalidReplay("provider does not match assistant source");
|
|
203
|
+
if (state.response.model !== source.model) return invalidReplay("model does not match assistant source");
|
|
204
|
+
if (state.blocks.length !== message.content.length) return invalidReplay("block count does not match assistant content");
|
|
205
|
+
const content = message.content.map((block, index) => {
|
|
206
|
+
const replay = state.blocks[index];
|
|
207
|
+
if (replay === void 0 || replay.type !== block.type) return invalidReplay(`block ${index} does not match assistant content`);
|
|
208
|
+
switch (block.type) {
|
|
209
|
+
case "text":
|
|
210
|
+
return {
|
|
211
|
+
type: "text",
|
|
212
|
+
text: block.text,
|
|
213
|
+
...replay.type === "text" && replay.textSignature !== void 0 ? { textSignature: replay.textSignature } : {}
|
|
214
|
+
};
|
|
215
|
+
case "reasoning":
|
|
216
|
+
return {
|
|
217
|
+
type: "thinking",
|
|
218
|
+
thinking: block.text,
|
|
219
|
+
...replay.type === "reasoning" && replay.thinkingSignature !== void 0 ? { thinkingSignature: replay.thinkingSignature } : {},
|
|
220
|
+
...replay.type === "reasoning" && replay.redacted !== void 0 ? { redacted: replay.redacted } : {}
|
|
221
|
+
};
|
|
222
|
+
case "tool-call":
|
|
223
|
+
return {
|
|
224
|
+
type: "toolCall",
|
|
225
|
+
id: block.id,
|
|
226
|
+
name: block.name,
|
|
227
|
+
arguments: parseArguments(block.arguments),
|
|
228
|
+
...replay.type === "tool-call" && replay.thoughtSignature !== void 0 ? { thoughtSignature: replay.thoughtSignature } : {}
|
|
229
|
+
};
|
|
230
|
+
/* v8 ignore next -- readReplayState rejects unknown replay tags, so an equal plugin-added Harness tag cannot reach this switch */
|
|
231
|
+
default:
|
|
232
|
+
return invalidReplay(`block ${index} has an unsupported Harness type`);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
role: "assistant",
|
|
237
|
+
content,
|
|
238
|
+
api: state.response.api,
|
|
239
|
+
provider: state.response.provider,
|
|
240
|
+
// Anthropic reports aliases and fallbacks as model, unlike Completions' informational responseModel.
|
|
241
|
+
model: state.response.api === "anthropic-messages" ? state.response.responseModel ?? state.response.model : state.response.model,
|
|
242
|
+
...state.response.responseModel === void 0 ? {} : { responseModel: state.response.responseModel },
|
|
243
|
+
...state.response.responseId === void 0 ? {} : { responseId: state.response.responseId },
|
|
244
|
+
...state.response.providerThinkingLevel === void 0 ? {} : { providerThinkingLevel: state.response.providerThinkingLevel },
|
|
245
|
+
usage: emptyPiUsage(),
|
|
246
|
+
stopReason: state.response.stopReason,
|
|
247
|
+
timestamp: 0
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function toPiAssistant(message, onDegrade) {
|
|
251
|
+
const source = message.source;
|
|
252
|
+
if (source.kind !== "model" || source.replayState === void 0) return foreignAssistant(message);
|
|
253
|
+
try {
|
|
254
|
+
return replayedAssistant(message, source, source.replayState);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (!(error instanceof LlmError) || error.code !== "INVALID_REPLAY_STATE") throw error;
|
|
257
|
+
onDegrade?.(error.message);
|
|
258
|
+
return foreignAssistant(message);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/conversion/context.ts
|
|
263
|
+
import { requestImageDimensions } from "@deepseek-ai/dsh-attachment";
|
|
264
|
+
|
|
265
|
+
// src/conversion/config.ts
|
|
266
|
+
var DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
267
|
+
var DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
|
|
268
|
+
var DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
|
|
269
|
+
|
|
270
|
+
// src/conversion/context.ts
|
|
271
|
+
function flattenText(message) {
|
|
272
|
+
return message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
273
|
+
}
|
|
274
|
+
function toolResultText(blocks) {
|
|
275
|
+
return blocks.map((block) => block.type === "text" ? block.text : block.type === "tool-result" ? toolResultText(block.content) : "").join("");
|
|
276
|
+
}
|
|
277
|
+
function assertSupportedImageRoles(messages) {
|
|
278
|
+
for (const message of messages) {
|
|
279
|
+
if (message.role !== "user" && contentHasImage(message.content)) {
|
|
280
|
+
throw new LlmError2(
|
|
281
|
+
`pi-ai cannot represent an image in an in-history ${message.role} message`,
|
|
282
|
+
"UNSUPPORTED_CONTENT"
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function userContent(blocks, requestImages, resolveImageAccess) {
|
|
288
|
+
const content = [];
|
|
289
|
+
for (const block of blocks) {
|
|
290
|
+
switch (block.type) {
|
|
291
|
+
case "text":
|
|
292
|
+
if (block.text.length > 0) content.push({ type: "text", text: block.text });
|
|
293
|
+
break;
|
|
294
|
+
case "image": {
|
|
295
|
+
const version = requestImages.get(block.attachment.attachmentId);
|
|
296
|
+
content.push({
|
|
297
|
+
type: "text",
|
|
298
|
+
text: requestImageHandleText(block.attachment, version, resolveImageAccess(block.attachment))
|
|
299
|
+
});
|
|
300
|
+
content.push({
|
|
301
|
+
type: "image",
|
|
302
|
+
data: Buffer.from(version.data).toString("base64"),
|
|
303
|
+
mimeType: version.mediaType
|
|
304
|
+
});
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case "tool-result":
|
|
308
|
+
{
|
|
309
|
+
const nested = await userContent(block.content, requestImages, resolveImageAccess);
|
|
310
|
+
if (typeof nested === "string") {
|
|
311
|
+
if (nested.length > 0) content.push({ type: "text", text: nested });
|
|
312
|
+
} else {
|
|
313
|
+
content.push(...nested);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
default:
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (content.every((block) => block.type === "text")) return content.map((block) => block.text).join("");
|
|
322
|
+
return content;
|
|
323
|
+
}
|
|
324
|
+
function collectImageRefs(blocks, refs) {
|
|
325
|
+
for (const block of blocks) {
|
|
326
|
+
if (block.type === "image") {
|
|
327
|
+
if (block.offloaded !== true) refs.set(block.attachment.attachmentId, block.attachment);
|
|
328
|
+
} else if (block.type === "tool-result") {
|
|
329
|
+
collectImageRefs(block.content, refs);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async function prepareRequestImages(messages, attachments, budget, signal) {
|
|
334
|
+
const refs = /* @__PURE__ */ new Map();
|
|
335
|
+
for (const message of messages) collectImageRefs(message.content, refs);
|
|
336
|
+
const orderedRefs = [...refs.values()];
|
|
337
|
+
const prepared = await Promise.all(orderedRefs.map(
|
|
338
|
+
(ref) => attachments.readImageRequest(ref, requestImageTarget(ref, budget), signal)
|
|
339
|
+
));
|
|
340
|
+
const versions = /* @__PURE__ */ new Map();
|
|
341
|
+
for (const [index, ref] of orderedRefs.entries()) {
|
|
342
|
+
versions.set(ref.attachmentId, prepared[index]);
|
|
343
|
+
}
|
|
344
|
+
return versions;
|
|
345
|
+
}
|
|
346
|
+
function toolsOf(options) {
|
|
347
|
+
return options.tools?.map((tool) => ({
|
|
348
|
+
name: tool.name,
|
|
349
|
+
description: tool.description,
|
|
350
|
+
// ToolSchema.parameters is a JSON Schema object; pi-ai's TSchema
|
|
351
|
+
// (TypeBox) is structurally JSON Schema, so it assigns directly.
|
|
352
|
+
parameters: tool.parameters
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
function splitSystemPrompt(options) {
|
|
356
|
+
if (options.system !== void 0) return { systemPrompt: options.system, messages: options.messages };
|
|
357
|
+
const [first, ...rest] = options.messages;
|
|
358
|
+
if (first?.role !== "system") return { systemPrompt: void 0, messages: options.messages };
|
|
359
|
+
const text = flattenText(first);
|
|
360
|
+
return { systemPrompt: text.length > 0 ? text : void 0, messages: rest };
|
|
361
|
+
}
|
|
362
|
+
function piContext(systemPrompt, options, messages) {
|
|
363
|
+
const tools = toolsOf(options);
|
|
364
|
+
return {
|
|
365
|
+
...systemPrompt !== void 0 ? { systemPrompt } : {},
|
|
366
|
+
messages,
|
|
367
|
+
...tools !== void 0 && tools.length > 0 ? { tools } : {}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function appendAssistant(message, messages, toolNames, onReplayDegrade) {
|
|
371
|
+
const assistant = toPiAssistant(message, onReplayDegrade);
|
|
372
|
+
for (const block of assistant.content) {
|
|
373
|
+
if (block.type === "toolCall") toolNames.set(brandString(block.id), block.name);
|
|
374
|
+
}
|
|
375
|
+
messages.push(assistant);
|
|
376
|
+
}
|
|
377
|
+
function textOnlyContext(options, onReplayDegrade) {
|
|
378
|
+
assertSupportedImageRoles(options.messages);
|
|
379
|
+
const split = splitSystemPrompt(options);
|
|
380
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
381
|
+
const messages = [];
|
|
382
|
+
for (const message of split.messages) {
|
|
383
|
+
if (contentHasImage(message.content)) {
|
|
384
|
+
throw new LlmError2("pi-ai image conversion requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
385
|
+
}
|
|
386
|
+
if (message.role === "system") {
|
|
387
|
+
messages.push({ role: "user", content: flattenText(message), timestamp: 0 });
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (message.role === "assistant") {
|
|
391
|
+
appendAssistant(message, messages, toolNames, onReplayDegrade);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const text = flattenText(message);
|
|
395
|
+
const results = message.content.filter((block) => block.type === "tool-result");
|
|
396
|
+
if (text.length > 0 || results.length === 0) messages.push({ role: "user", content: text, timestamp: 0 });
|
|
397
|
+
for (const result of results) {
|
|
398
|
+
messages.push({
|
|
399
|
+
role: "toolResult",
|
|
400
|
+
toolCallId: result.toolCallId,
|
|
401
|
+
toolName: toolNames.get(result.toolCallId) ?? "unknown",
|
|
402
|
+
content: [{
|
|
403
|
+
type: "text",
|
|
404
|
+
text: toolResultText(result.content) || "(no output)"
|
|
405
|
+
}],
|
|
406
|
+
isError: result.isError ?? false,
|
|
407
|
+
timestamp: 0
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return piContext(split.systemPrompt, options, messages);
|
|
412
|
+
}
|
|
413
|
+
function requestImageTarget(ref, budget) {
|
|
414
|
+
return { ...requestImageDimensions(ref.width, ref.height, budget.maxPixels), maxBytes: budget.maxBytes };
|
|
415
|
+
}
|
|
416
|
+
function toPiContext(options, images, onReplayDegrade) {
|
|
417
|
+
return images === void 0 ? textOnlyContext(options, onReplayDegrade) : toPiContextWithImages(options, images, onReplayDegrade);
|
|
418
|
+
}
|
|
419
|
+
async function toPiContextWithImages(options, images, onReplayDegrade) {
|
|
420
|
+
const { attachments, resolveImageAccess, maxRequestImageBytes } = images;
|
|
421
|
+
const requestImagePolicy = images.requestImagePolicy ?? {
|
|
422
|
+
maxPixels: DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
|
|
423
|
+
maxBytes: DEFAULT_REQUEST_IMAGE_MAX_BYTES
|
|
424
|
+
};
|
|
425
|
+
assertSupportedImageRoles(options.messages);
|
|
426
|
+
const split = splitSystemPrompt(options);
|
|
427
|
+
const requestImages = await prepareRequestImages(split.messages, attachments, requestImagePolicy, options.signal);
|
|
428
|
+
if (maxRequestImageBytes !== void 0) {
|
|
429
|
+
const offloadImages = requiredImageOffload(
|
|
430
|
+
split.messages,
|
|
431
|
+
{ representation: "base64", maxBytes: maxRequestImageBytes },
|
|
432
|
+
(block) => requestImages.get(block.attachment.attachmentId).bytes
|
|
433
|
+
);
|
|
434
|
+
if (offloadImages > 0) {
|
|
435
|
+
throw new LlmError2(
|
|
436
|
+
`pi-ai request images exceed the ${maxRequestImageBytes}-byte base64 bound; ${offloadImages} more oldest occurrence(s) must be offloaded.`,
|
|
437
|
+
IMAGE_OFFLOAD_REQUIRED_CODE,
|
|
438
|
+
{ offloadImages }
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const exactMessages = projectOffloadedImages(
|
|
443
|
+
split.messages,
|
|
444
|
+
(ref) => offloadedImageText(ref, resolveImageAccess(ref))
|
|
445
|
+
);
|
|
446
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
447
|
+
const messages = [];
|
|
448
|
+
for (const message of exactMessages) {
|
|
449
|
+
if (message.role === "system") {
|
|
450
|
+
messages.push({ role: "user", content: flattenText(message), timestamp: 0 });
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (message.role === "assistant") {
|
|
454
|
+
appendAssistant(message, messages, toolNames, onReplayDegrade);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const regular = message.content.filter((block) => block.type !== "tool-result");
|
|
458
|
+
const content = await userContent(regular, requestImages, resolveImageAccess);
|
|
459
|
+
const results = message.content.filter((block) => block.type === "tool-result");
|
|
460
|
+
if (content.length > 0 || results.length === 0) {
|
|
461
|
+
messages.push({ role: "user", content, timestamp: 0 });
|
|
462
|
+
}
|
|
463
|
+
for (const result of results) {
|
|
464
|
+
const resultContent = await userContent(result.content, requestImages, resolveImageAccess);
|
|
465
|
+
messages.push({
|
|
466
|
+
role: "toolResult",
|
|
467
|
+
toolCallId: result.toolCallId,
|
|
468
|
+
toolName: toolNames.get(result.toolCallId) ?? "unknown",
|
|
469
|
+
content: typeof resultContent === "string" ? [{ type: "text", text: resultContent || "(no output)" }] : resultContent,
|
|
470
|
+
isError: result.isError ?? false,
|
|
471
|
+
timestamp: 0
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return piContext(split.systemPrompt, options, messages);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/conversion/stream.ts
|
|
479
|
+
import { brandString as brandString2 } from "@deepseek-ai/dsh-brand";
|
|
480
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError as LlmError3, QUOTA_EXCEEDED_CODE } from "@deepseek-ai/dsh-llm";
|
|
481
|
+
import { isContextOverflow } from "@earendil-works/pi-ai";
|
|
482
|
+
function mapUsage(usage) {
|
|
483
|
+
return {
|
|
484
|
+
inputTokens: usage.input,
|
|
485
|
+
outputTokens: usage.output,
|
|
486
|
+
totalTokens: usage.totalTokens,
|
|
487
|
+
...usage.cacheRead > 0 ? { cacheReadTokens: usage.cacheRead } : {},
|
|
488
|
+
...usage.cacheWrite > 0 ? { cacheWriteTokens: usage.cacheWrite } : {}
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function classifyPiAiError(message) {
|
|
492
|
+
if (/\b(?:401|403)\b/.test(message)) return "AUTH";
|
|
493
|
+
if (isQuotaExceededError(message)) return QUOTA_EXCEEDED_CODE;
|
|
494
|
+
if (/\b429\b|rate.?limit/i.test(message)) return "RATE_LIMIT";
|
|
495
|
+
if (/\b413\b|failed to buffer the request body:\s*length limit exceeded|payload too large|request body too large/i.test(message)) return "INVALID_REQUEST";
|
|
496
|
+
if (/\b400\b|invalid.?request/i.test(message)) return "INVALID_REQUEST";
|
|
497
|
+
if (/\b5\d\d\b/.test(message)) return "SERVER";
|
|
498
|
+
if (/\btime(?:d)?\s*out\b|timeout/i.test(message)) return "TIMEOUT";
|
|
499
|
+
if (/stream ended (?:before|without)\b/i.test(message)) return "TRANSPORT";
|
|
500
|
+
if (/\b(?:network|connection|socket|fetch)\b|\bECONN[A-Z]+\b/i.test(message) || /\b(?:other side closed|HTTP2 request did not get a response|WebSocket closed unexpectedly)\b/i.test(message) || /\bterminated\b|premature close/i.test(message)) {
|
|
501
|
+
return "TRANSPORT";
|
|
502
|
+
}
|
|
503
|
+
return "PI_AI_ERROR";
|
|
504
|
+
}
|
|
505
|
+
function mapStopReason(message, contextWindow) {
|
|
506
|
+
const piAiOverflow = isContextOverflow(message, contextWindow);
|
|
507
|
+
const harnessOverflow = message.stopReason === "error" && message.errorMessage !== void 0 && isContextWindowExceededError(message.errorMessage);
|
|
508
|
+
if (piAiOverflow || harnessOverflow) {
|
|
509
|
+
return {
|
|
510
|
+
kind: "error",
|
|
511
|
+
failure: {
|
|
512
|
+
message: message.errorMessage ?? `pi-ai detected context overflow for model "${message.model}"`,
|
|
513
|
+
code: CONTEXT_WINDOW_EXCEEDED_CODE
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
switch (message.stopReason) {
|
|
518
|
+
case "stop":
|
|
519
|
+
if (message.content.length === 0) {
|
|
520
|
+
return {
|
|
521
|
+
kind: "error",
|
|
522
|
+
failure: {
|
|
523
|
+
message: `model "${message.model}" returned a completed response with no content`,
|
|
524
|
+
code: EMPTY_RESPONSE_CODE
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return { kind: "stop" };
|
|
529
|
+
case "length":
|
|
530
|
+
return { kind: "max-tokens" };
|
|
531
|
+
case "toolUse":
|
|
532
|
+
return { kind: "tool-calls" };
|
|
533
|
+
case "pending":
|
|
534
|
+
return {
|
|
535
|
+
kind: "error",
|
|
536
|
+
failure: { message: `pi-ai stream for model "${message.model}" ended pending`, code: "PI_AI_ERROR" }
|
|
537
|
+
};
|
|
538
|
+
case "deferred":
|
|
539
|
+
return {
|
|
540
|
+
kind: "error",
|
|
541
|
+
failure: { message: `pi-ai deferred response for model "${message.model}" is not supported`, code: "PI_AI_ERROR" }
|
|
542
|
+
};
|
|
543
|
+
case "aborted":
|
|
544
|
+
return {
|
|
545
|
+
kind: "aborted",
|
|
546
|
+
failure: { message: message.errorMessage ?? "pi-ai stream aborted", code: "ABORTED" }
|
|
547
|
+
};
|
|
548
|
+
case "error": {
|
|
549
|
+
const text = message.errorMessage ?? "pi-ai stream error";
|
|
550
|
+
return { kind: "error", failure: { message: text, code: classifyPiAiError(text) } };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async function* toStreamChunks(events, contextWindow, callerSignal, requestedModel) {
|
|
555
|
+
const toolIds = /* @__PURE__ */ new Map();
|
|
556
|
+
for await (const event of events) {
|
|
557
|
+
switch (event.type) {
|
|
558
|
+
case "start":
|
|
559
|
+
break;
|
|
560
|
+
case "text_start":
|
|
561
|
+
yield { type: "block-start", index: event.contentIndex, blockType: "text" };
|
|
562
|
+
break;
|
|
563
|
+
case "text_delta":
|
|
564
|
+
yield { type: "text-delta", index: event.contentIndex, text: event.delta };
|
|
565
|
+
break;
|
|
566
|
+
case "text_end":
|
|
567
|
+
yield { type: "block-end", index: event.contentIndex, block: { type: "text", text: event.content } };
|
|
568
|
+
break;
|
|
569
|
+
case "thinking_start":
|
|
570
|
+
yield { type: "block-start", index: event.contentIndex, blockType: "reasoning" };
|
|
571
|
+
break;
|
|
572
|
+
case "thinking_delta":
|
|
573
|
+
yield { type: "reasoning-delta", index: event.contentIndex, text: event.delta };
|
|
574
|
+
break;
|
|
575
|
+
case "thinking_end":
|
|
576
|
+
yield { type: "block-end", index: event.contentIndex, block: { type: "reasoning", text: event.content } };
|
|
577
|
+
break;
|
|
578
|
+
case "toolcall_start": {
|
|
579
|
+
const partial = event.partial.content[event.contentIndex];
|
|
580
|
+
const id = partial?.type === "toolCall" ? partial.id : "";
|
|
581
|
+
const name2 = partial?.type === "toolCall" ? partial.name : "";
|
|
582
|
+
toolIds.set(event.contentIndex, { id, name: name2 });
|
|
583
|
+
yield { type: "block-start", index: event.contentIndex, blockType: "tool-call" };
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
case "toolcall_delta": {
|
|
587
|
+
const known = toolIds.get(event.contentIndex);
|
|
588
|
+
yield {
|
|
589
|
+
type: "tool-call-delta",
|
|
590
|
+
index: event.contentIndex,
|
|
591
|
+
id: brandString2(known?.id ?? ""),
|
|
592
|
+
...known?.name !== void 0 && known.name.length > 0 ? { name: known.name } : {},
|
|
593
|
+
argumentsDelta: event.delta
|
|
594
|
+
};
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
case "toolcall_end":
|
|
598
|
+
yield {
|
|
599
|
+
type: "block-end",
|
|
600
|
+
index: event.contentIndex,
|
|
601
|
+
block: {
|
|
602
|
+
type: "tool-call",
|
|
603
|
+
id: brandString2(event.toolCall.id),
|
|
604
|
+
name: event.toolCall.name,
|
|
605
|
+
// pi-ai hands back the PARSED arguments; the harness vocabulary
|
|
606
|
+
// keeps the raw string.
|
|
607
|
+
arguments: JSON.stringify(event.toolCall.arguments)
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
break;
|
|
611
|
+
case "done":
|
|
612
|
+
yield { type: "usage", usage: mapUsage(event.message.usage) };
|
|
613
|
+
yield {
|
|
614
|
+
type: "finish",
|
|
615
|
+
reason: mapStopReason(event.message, contextWindow),
|
|
616
|
+
replayState: toPiReplayState(event.message, requestedModel)
|
|
617
|
+
};
|
|
618
|
+
return;
|
|
619
|
+
case "error":
|
|
620
|
+
yield { type: "usage", usage: mapUsage(event.error.usage) };
|
|
621
|
+
yield {
|
|
622
|
+
type: "finish",
|
|
623
|
+
reason: mapStopReason(
|
|
624
|
+
callerSignal?.aborted ? { ...event.error, stopReason: "aborted" } : event.error,
|
|
625
|
+
contextWindow
|
|
626
|
+
)
|
|
627
|
+
};
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
throw new LlmError3("pi-ai event stream ended without done/error", "STREAM_CLOSED");
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/adapter.ts
|
|
635
|
+
import { idleWatchdog, timeoutOf } from "@deepseek-ai/dsh-timeout";
|
|
636
|
+
|
|
637
|
+
// src/catalog.ts
|
|
638
|
+
import { createProvider } from "@earendil-works/pi-ai";
|
|
639
|
+
import { getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
|
|
640
|
+
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
641
|
+
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
642
|
+
import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
|
|
643
|
+
import { attributionHeaders, LlmError as LlmError4 } from "@deepseek-ai/dsh-llm";
|
|
644
|
+
var PROVIDER_ID = "opencode-go";
|
|
645
|
+
var DISPLAY_NAME = "OpenCode Go";
|
|
646
|
+
var DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
|
|
647
|
+
var CATALOG_ADDITIONS = [
|
|
648
|
+
{
|
|
649
|
+
id: "deepseek-v4.1-flash",
|
|
650
|
+
siblingId: "deepseek-v4-flash",
|
|
651
|
+
inputSiblingId: "deepseek-v4-flash-vision-exp",
|
|
652
|
+
name: "DeepSeek V4.1 Flash"
|
|
653
|
+
}
|
|
654
|
+
];
|
|
655
|
+
var MODELS_FETCH_TIMEOUT_MS = 1e4;
|
|
656
|
+
function curatedModels(baseURL) {
|
|
657
|
+
const models = /* @__PURE__ */ new Map();
|
|
658
|
+
for (const model of getBuiltinModels("opencode-go")) {
|
|
659
|
+
models.set(model.id, { ...model, provider: PROVIDER_ID, baseUrl: baseURL });
|
|
660
|
+
}
|
|
661
|
+
for (const addition of CATALOG_ADDITIONS) {
|
|
662
|
+
if (models.has(addition.id)) continue;
|
|
663
|
+
const sibling = models.get(addition.siblingId);
|
|
664
|
+
if (sibling === void 0) continue;
|
|
665
|
+
const inputSibling = models.get(addition.inputSiblingId);
|
|
666
|
+
if (inputSibling === void 0) continue;
|
|
667
|
+
models.set(addition.id, {
|
|
668
|
+
...sibling,
|
|
669
|
+
id: addition.id,
|
|
670
|
+
name: addition.name,
|
|
671
|
+
input: [...inputSibling.input],
|
|
672
|
+
provider: PROVIDER_ID,
|
|
673
|
+
baseUrl: baseURL
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
if (models.size === 0) {
|
|
677
|
+
throw new LlmError4(
|
|
678
|
+
"llm-opencode-go: pi-ai's installed catalog describes no opencode-go models; cannot seed the curated table",
|
|
679
|
+
"INVALID_CONFIG"
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
return models;
|
|
683
|
+
}
|
|
684
|
+
function readLiveModelIds(body) {
|
|
685
|
+
const data = body?.data;
|
|
686
|
+
if (!Array.isArray(data)) throw new Error('the model listing has no "data" array');
|
|
687
|
+
const ids = [];
|
|
688
|
+
for (const entry of data) {
|
|
689
|
+
const id = entry?.id;
|
|
690
|
+
if (typeof id === "string" && id.length > 0) ids.push(id);
|
|
691
|
+
}
|
|
692
|
+
return ids;
|
|
693
|
+
}
|
|
694
|
+
async function fetchLiveModelIds(baseURL) {
|
|
695
|
+
const url = `${baseURL.replace(/\/+$/, "")}/models`;
|
|
696
|
+
let response;
|
|
697
|
+
try {
|
|
698
|
+
response = await fetch(url, {
|
|
699
|
+
method: "GET",
|
|
700
|
+
headers: { accept: "application/json", ...attributionHeaders() },
|
|
701
|
+
signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS)
|
|
702
|
+
});
|
|
703
|
+
} catch (error) {
|
|
704
|
+
throw new LlmError4(`could not reach ${url}`, "DISCOVERY_FAILED", { cause: error });
|
|
705
|
+
}
|
|
706
|
+
if (!response.ok) {
|
|
707
|
+
throw new LlmError4(`${url} answered ${response.status}`, "DISCOVERY_FAILED");
|
|
708
|
+
}
|
|
709
|
+
return readLiveModelIds(await response.json());
|
|
710
|
+
}
|
|
711
|
+
function harnessApiKeyAuth() {
|
|
712
|
+
return {
|
|
713
|
+
apiKey: {
|
|
714
|
+
name: "OpenCode API key",
|
|
715
|
+
/* v8 ignore next -- pi-ai calls resolve only for a request without an apiKey override, and this adapter always passes one */
|
|
716
|
+
resolve: () => Promise.resolve({ auth: {}, source: "OpenCode API key" })
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
function buildProvider(baseURL, models) {
|
|
721
|
+
return createProvider({
|
|
722
|
+
id: PROVIDER_ID,
|
|
723
|
+
name: DISPLAY_NAME,
|
|
724
|
+
baseUrl: baseURL,
|
|
725
|
+
auth: harnessApiKeyAuth(),
|
|
726
|
+
models: [...models],
|
|
727
|
+
api: {
|
|
728
|
+
"anthropic-messages": anthropicMessagesApi(),
|
|
729
|
+
"openai-completions": openAICompletionsApi(),
|
|
730
|
+
"openai-responses": openAIResponsesApi()
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
var OpencodeGoCatalog = class {
|
|
735
|
+
/**
|
|
736
|
+
* @param baseURL - The endpoint the gateway serves; also the listing base.
|
|
737
|
+
* @param refreshMs - How long one live resolution stays authoritative.
|
|
738
|
+
* @param onFallback - Observes a failed live listing and how many curated
|
|
739
|
+
* models kept serving because of it.
|
|
740
|
+
* @param onOmitted - Observes live ids the curated table cannot route.
|
|
741
|
+
*/
|
|
742
|
+
constructor(baseURL, refreshMs, onFallback, onOmitted) {
|
|
743
|
+
this.baseURL = baseURL;
|
|
744
|
+
this.refreshMs = refreshMs;
|
|
745
|
+
this.onFallback = onFallback;
|
|
746
|
+
this.onOmitted = onOmitted;
|
|
747
|
+
}
|
|
748
|
+
served;
|
|
749
|
+
pending;
|
|
750
|
+
/**
|
|
751
|
+
* The current catalog, fetching when expired or never fetched.
|
|
752
|
+
* @returns the snapshot now serving, shared by concurrent callers.
|
|
753
|
+
*/
|
|
754
|
+
snapshot() {
|
|
755
|
+
if (this.served !== void 0 && Date.now() - this.served.fetchedAtMs < this.refreshMs) {
|
|
756
|
+
return Promise.resolve(this.served);
|
|
757
|
+
}
|
|
758
|
+
this.pending ??= this.build().then((snapshot) => {
|
|
759
|
+
this.served = snapshot;
|
|
760
|
+
return snapshot;
|
|
761
|
+
}).finally(() => {
|
|
762
|
+
this.pending = void 0;
|
|
763
|
+
});
|
|
764
|
+
return this.pending;
|
|
765
|
+
}
|
|
766
|
+
/** One resolution: intersect the curated table with the live listing. */
|
|
767
|
+
async build() {
|
|
768
|
+
const curated = curatedModels(this.baseURL);
|
|
769
|
+
let liveIds;
|
|
770
|
+
let failure;
|
|
771
|
+
try {
|
|
772
|
+
liveIds = await fetchLiveModelIds(this.baseURL);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
failure = error;
|
|
775
|
+
}
|
|
776
|
+
if (liveIds === void 0) {
|
|
777
|
+
this.onFallback({ url: `${this.baseURL.replace(/\/+$/, "")}/models`, error: failure, kept: curated.size });
|
|
778
|
+
return {
|
|
779
|
+
models: curated,
|
|
780
|
+
provider: buildProvider(this.baseURL, [...curated.values()]),
|
|
781
|
+
live: false,
|
|
782
|
+
fetchedAtMs: Date.now()
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
const live = new Set(liveIds);
|
|
786
|
+
const omitted = liveIds.filter((id) => !curated.has(id));
|
|
787
|
+
if (omitted.length > 0) this.onOmitted(omitted);
|
|
788
|
+
const served = [...curated.values()].filter((model) => live.has(model.id));
|
|
789
|
+
return {
|
|
790
|
+
models: new Map(served.map((model) => [model.id, model])),
|
|
791
|
+
provider: buildProvider(this.baseURL, served),
|
|
792
|
+
live: true,
|
|
793
|
+
fetchedAtMs: Date.now()
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
async function discoverCatalogModels(catalog) {
|
|
798
|
+
const snapshot = await catalog.snapshot();
|
|
799
|
+
if (!snapshot.live) {
|
|
800
|
+
throw new LlmError4("llm-opencode-go: the live model listing is unreachable; try again later", "DISCOVERY_FAILED");
|
|
801
|
+
}
|
|
802
|
+
return [...snapshot.models.values()].map((model) => ({
|
|
803
|
+
id: model.id,
|
|
804
|
+
name: model.name,
|
|
805
|
+
contextWindow: model.contextWindow,
|
|
806
|
+
maxTokens: model.maxTokens
|
|
807
|
+
}));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/config.ts
|
|
811
|
+
import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
812
|
+
import z from "@deepseek-ai/schemastery";
|
|
813
|
+
var DEFAULT_API_KEY_ENV = "OPENCODE_API_KEY";
|
|
814
|
+
var DEFAULT_REFRESH_MINUTES = 60;
|
|
815
|
+
var DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
816
|
+
var Config = z.object({
|
|
817
|
+
enabled: z.boolean().default(true),
|
|
818
|
+
apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
|
|
819
|
+
baseURL: z.string().default(DEFAULT_BASE_URL),
|
|
820
|
+
refreshMinutes: z.number().step(1).min(1).max(7 * 24 * 60).default(DEFAULT_REFRESH_MINUTES),
|
|
821
|
+
streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
822
|
+
// The image defaults are the generic pi-ai adapter's: one normalized
|
|
823
|
+
// request image fits the budget, and fifteen of them fit the payload cap.
|
|
824
|
+
maxRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_IMAGE_BYTES),
|
|
825
|
+
requestImagePixelBudget: z.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET),
|
|
826
|
+
requestImageMaxBytes: z.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_MAX_BYTES)
|
|
827
|
+
});
|
|
828
|
+
function assertBaseURL(raw) {
|
|
829
|
+
let url;
|
|
830
|
+
try {
|
|
831
|
+
url = new URL(raw);
|
|
832
|
+
} catch {
|
|
833
|
+
throw new Error(`llm-opencode-go: baseURL "${raw}" is not a valid URL`);
|
|
834
|
+
}
|
|
835
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
836
|
+
throw new Error(`llm-opencode-go: baseURL "${raw}" must be http or https`);
|
|
837
|
+
}
|
|
838
|
+
if (url.search.length > 0 || url.hash.length > 0) {
|
|
839
|
+
throw new Error(`llm-opencode-go: baseURL "${raw}" must not carry a query or fragment`);
|
|
840
|
+
}
|
|
841
|
+
return url.toString().replace(/\/+$/, "");
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// src/adapter.ts
|
|
845
|
+
function opencodeSessionValue(sessionId) {
|
|
846
|
+
return sessionId !== void 0 && sessionId.length > 0 ? sessionId : randomUUID();
|
|
847
|
+
}
|
|
848
|
+
var OpencodeGoAdapter = class extends LlmAdapter {
|
|
849
|
+
constructor(options) {
|
|
850
|
+
super();
|
|
851
|
+
this.options = options;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* One catalog instance per endpoint/refresh pair. A settings write that
|
|
855
|
+
* changes either gets a fresh resolver (and a fresh live-listing fetch) on
|
|
856
|
+
* the next operation; an unchanged configuration keeps its cached snapshot
|
|
857
|
+
* for the whole refresh interval.
|
|
858
|
+
*/
|
|
859
|
+
catalogCache;
|
|
860
|
+
/**
|
|
861
|
+
* The catalog resolver for one configuration, rebuilding on the facts it
|
|
862
|
+
* owns. Public for the plugin's discovery registration, which resolves the
|
|
863
|
+
* current configuration the same way the adapter does.
|
|
864
|
+
* @param config - the configuration whose endpoint and refresh interval the
|
|
865
|
+
* resolver serves; a change to either yields a fresh resolver.
|
|
866
|
+
* @returns the resolver caching one snapshot per endpoint/refresh pair.
|
|
867
|
+
*/
|
|
868
|
+
catalogOf(config) {
|
|
869
|
+
const key = `${config.baseURL}|${String(config.refreshMinutes)}`;
|
|
870
|
+
if (this.catalogCache?.key !== key) {
|
|
871
|
+
this.catalogCache = {
|
|
872
|
+
key,
|
|
873
|
+
catalog: new OpencodeGoCatalog(
|
|
874
|
+
assertBaseURL(config.baseURL),
|
|
875
|
+
config.refreshMinutes * 6e4,
|
|
876
|
+
/* v8 ignore next -- the plugin always passes both observers; the defaults exist for direct construction */
|
|
877
|
+
this.options.onFallback ?? (() => {
|
|
878
|
+
}),
|
|
879
|
+
/* v8 ignore next -- the plugin always passes both observers; the defaults exist for direct construction */
|
|
880
|
+
this.options.onOmitted ?? (() => {
|
|
881
|
+
})
|
|
882
|
+
)
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
return this.catalogCache.catalog;
|
|
886
|
+
}
|
|
887
|
+
providerInfo(provider) {
|
|
888
|
+
return { id: provider, name: DISPLAY_NAME };
|
|
889
|
+
}
|
|
890
|
+
async listModels(_provider) {
|
|
891
|
+
const snapshot = await this.catalogOf(this.options.config()).snapshot();
|
|
892
|
+
return [...snapshot.models.values()].map((model) => ({
|
|
893
|
+
provider: PROVIDER_ID,
|
|
894
|
+
id: model.id,
|
|
895
|
+
name: model.name,
|
|
896
|
+
inputModalities: [...model.input]
|
|
897
|
+
}));
|
|
898
|
+
}
|
|
899
|
+
async resolveModel(_provider, model, _signal) {
|
|
900
|
+
const snapshot = await this.catalogOf(this.options.config()).snapshot();
|
|
901
|
+
const resolved = snapshot.models.get(model);
|
|
902
|
+
if (resolved === void 0) {
|
|
903
|
+
throw new LlmError5(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
|
|
904
|
+
}
|
|
905
|
+
return this.modelInfo(resolved);
|
|
906
|
+
}
|
|
907
|
+
/** Describe one model: capacities plus the reasoning levels it actually offers. */
|
|
908
|
+
modelInfo(model) {
|
|
909
|
+
const reasoning = {};
|
|
910
|
+
if (model.reasoning) {
|
|
911
|
+
reasoning.reasoning = {
|
|
912
|
+
efforts: getSupportedThinkingLevels(model).map((level) => ({
|
|
913
|
+
id: ReasoningEffortId(level),
|
|
914
|
+
name: `${level.charAt(0).toUpperCase()}${level.slice(1)}`
|
|
915
|
+
}))
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
provider: PROVIDER_ID,
|
|
920
|
+
id: model.id,
|
|
921
|
+
name: model.name,
|
|
922
|
+
inputModalities: [...model.input],
|
|
923
|
+
context: { contextWindow: model.contextWindow },
|
|
924
|
+
...reasoning
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
/** Validate an explicit effort against the model's own levels, without clamping. */
|
|
928
|
+
resolveReasoningLevel(model, effort) {
|
|
929
|
+
if (effort === void 0) return void 0;
|
|
930
|
+
const supported = getSupportedThinkingLevels(model);
|
|
931
|
+
if (supported.some((level) => level === effort)) return effort;
|
|
932
|
+
throw new LlmError5(
|
|
933
|
+
`opencode-go model "${model.id}" does not support reasoning effort "${effort}"`,
|
|
934
|
+
"UNSUPPORTED_REASONING_EFFORT"
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
async *stream(options) {
|
|
938
|
+
var _stack = [];
|
|
939
|
+
try {
|
|
940
|
+
if (options.stop !== void 0) {
|
|
941
|
+
throw new LlmError5("llm-opencode-go does not support GenerateOptions.stop", "UNSUPPORTED_OPTION");
|
|
942
|
+
}
|
|
943
|
+
const config = this.options.config();
|
|
944
|
+
const snapshot = await this.catalogOf(config).snapshot();
|
|
945
|
+
const model = snapshot.models.get(options.model);
|
|
946
|
+
if (model === void 0) {
|
|
947
|
+
throw new LlmError5(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
|
|
948
|
+
}
|
|
949
|
+
const apiKey = await this.options.resolveApiKey();
|
|
950
|
+
if (apiKey === void 0 || apiKey.length === 0) {
|
|
951
|
+
throw new LlmError5("llm-opencode-go: no credential resolved for the route", "MISSING_CREDENTIAL");
|
|
952
|
+
}
|
|
953
|
+
const reasoning = this.resolveReasoningLevel(model, options.reasoningEffort);
|
|
954
|
+
const consumer = new AbortController();
|
|
955
|
+
const upstream = options.signal === void 0 ? consumer.signal : AbortSignal.any([options.signal, consumer.signal]);
|
|
956
|
+
const watchdog = __using(_stack, idleWatchdog(upstream, config.streamIdleTimeoutMs, "LLM_STREAM_IDLE_TIMEOUT"));
|
|
957
|
+
try {
|
|
958
|
+
const containsImage = options.messages.some((message) => contentHasImage2(message.content));
|
|
959
|
+
if (containsImage && !model.input.includes("image")) {
|
|
960
|
+
throw new LlmError5(`opencode-go model "${model.id}" does not support image input`, "UNSUPPORTED_CONTENT");
|
|
961
|
+
}
|
|
962
|
+
let imageRequest;
|
|
963
|
+
if (containsImage) {
|
|
964
|
+
const access = this.options.imageAccess;
|
|
965
|
+
const store = access?.resolveAttachments();
|
|
966
|
+
if (access === void 0 || store === void 0) {
|
|
967
|
+
throw new LlmError5("llm-opencode-go image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
968
|
+
}
|
|
969
|
+
imageRequest = {
|
|
970
|
+
attachments: store,
|
|
971
|
+
resolveImageAccess: (ref) => access.resolveImageAccess(store, ref),
|
|
972
|
+
maxRequestImageBytes: config.maxRequestImageBytes,
|
|
973
|
+
requestImagePolicy: {
|
|
974
|
+
maxPixels: config.requestImagePixelBudget,
|
|
975
|
+
maxBytes: config.requestImageMaxBytes
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
const context = imageRequest === void 0 ? toPiContext(options, void 0, this.options.onReplayDegrade) : await toPiContext({ ...options, signal: watchdog.signal }, imageRequest, this.options.onReplayDegrade);
|
|
980
|
+
const events = snapshot.provider.streamSimple(model, context, {
|
|
981
|
+
apiKey,
|
|
982
|
+
...reasoning === void 0 || reasoning === "off" ? {} : { reasoning },
|
|
983
|
+
...options.temperature === void 0 ? {} : { temperature: options.temperature },
|
|
984
|
+
...options.maxTokens === void 0 ? {} : { maxTokens: options.maxTokens },
|
|
985
|
+
...options.sessionId === void 0 ? {} : { sessionId: String(options.sessionId) },
|
|
986
|
+
signal: watchdog.signal,
|
|
987
|
+
// Harness-owned request identity: the gateway refuses requests without
|
|
988
|
+
// `x-opencode-session` and profiles clients by User-Agent, and
|
|
989
|
+
// attribution merges last in pi-ai's client.
|
|
990
|
+
headers: {
|
|
991
|
+
"x-opencode-session": opencodeSessionValue(options.sessionId === void 0 ? void 0 : String(options.sessionId)),
|
|
992
|
+
...attributionHeaders2()
|
|
993
|
+
},
|
|
994
|
+
// The agent recovery layer owns visible attempts; one adapter call is
|
|
995
|
+
// one SDK attempt.
|
|
996
|
+
maxRetries: 0
|
|
997
|
+
});
|
|
998
|
+
const iterator = toStreamChunks(events, model.contextWindow, options.signal, model.id)[Symbol.asyncIterator]();
|
|
999
|
+
let exhausted = false;
|
|
1000
|
+
try {
|
|
1001
|
+
while (true) {
|
|
1002
|
+
const result = await watchdog.next(iterator);
|
|
1003
|
+
if (timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) {
|
|
1004
|
+
throw new LlmError5("opencode-go stream idle timeout", "TIMEOUT");
|
|
1005
|
+
}
|
|
1006
|
+
if (result.done) {
|
|
1007
|
+
exhausted = true;
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
yield result.value;
|
|
1011
|
+
}
|
|
1012
|
+
} finally {
|
|
1013
|
+
if (!exhausted) {
|
|
1014
|
+
consumer.abort("opencode-go stream consumer stopped");
|
|
1015
|
+
try {
|
|
1016
|
+
await iterator.return(void 0);
|
|
1017
|
+
} catch (_abortedSdkTeardown) {
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
if (timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) {
|
|
1023
|
+
throw new LlmError5("opencode-go stream idle timeout", "TIMEOUT", { cause: error });
|
|
1024
|
+
}
|
|
1025
|
+
if (options.signal?.aborted) {
|
|
1026
|
+
throw new LlmError5("opencode-go request aborted by caller", "ABORTED", { cause: error });
|
|
1027
|
+
}
|
|
1028
|
+
throw error;
|
|
1029
|
+
}
|
|
1030
|
+
} catch (_) {
|
|
1031
|
+
var _error = _, _hasError = true;
|
|
1032
|
+
} finally {
|
|
1033
|
+
__callDispose(_stack, _error, _hasError);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
// src/index.ts
|
|
1039
|
+
var name = "llm-opencode-go";
|
|
1040
|
+
var inject = ["llm"];
|
|
1041
|
+
var NS = "llm-opencode-go";
|
|
1042
|
+
function apply(ctx, raw) {
|
|
1043
|
+
const entry = Config(raw);
|
|
1044
|
+
assertBaseURL(entry.baseURL);
|
|
1045
|
+
let current = () => entry;
|
|
1046
|
+
const resolveApiKey = async () => {
|
|
1047
|
+
const ref = current().apiKeyEnv;
|
|
1048
|
+
const credentials = ctx.get("credentials");
|
|
1049
|
+
const hit = credentials !== void 0 ? (await credentials.resolve(credentialRef(ref)))?.value : launchEnvironmentOf(ctx).get(ref)?.value;
|
|
1050
|
+
if (hit !== void 0 && hit.length > 0) return assertUsableApiKey(hit, name, ref);
|
|
1051
|
+
throw new LlmError6(
|
|
1052
|
+
`llm-opencode-go: no credential; the profile resolves ${ref}, which is not set \u2014 store ${ref} through the credentials service or export it`,
|
|
1053
|
+
"MISSING_CREDENTIAL"
|
|
1054
|
+
);
|
|
1055
|
+
};
|
|
1056
|
+
const logger = {
|
|
1057
|
+
fallback: ({ error }) => {
|
|
1058
|
+
ctx.logger.warn(`llm-opencode-go: live model listing unreachable; serving the curated table until the next refresh (${String(error)})`);
|
|
1059
|
+
},
|
|
1060
|
+
omitted: (ids) => {
|
|
1061
|
+
ctx.logger.info(`llm-opencode-go: live listing carries ids the curated table cannot route; omitted: ${ids.join(", ")}`);
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
const adapter = new OpencodeGoAdapter({
|
|
1065
|
+
config: () => current(),
|
|
1066
|
+
resolveApiKey,
|
|
1067
|
+
imageAccess: {
|
|
1068
|
+
resolveAttachments: () => ctx.get("attachments"),
|
|
1069
|
+
resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(
|
|
1070
|
+
attachments,
|
|
1071
|
+
(hostPath) => ctx.get("fs")?.processPathFromHostPath(hostPath),
|
|
1072
|
+
ref
|
|
1073
|
+
)
|
|
1074
|
+
},
|
|
1075
|
+
onFallback: logger.fallback,
|
|
1076
|
+
onOmitted: logger.omitted,
|
|
1077
|
+
onReplayDegrade: (reason) => {
|
|
1078
|
+
ctx.logger.warn(`llm-opencode-go: unusable replay state on assistant history; sending provider-neutral content (${reason})`);
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
let registration;
|
|
1082
|
+
const applyRoute = (configured) => {
|
|
1083
|
+
if (configured && current().enabled && registration === void 0) {
|
|
1084
|
+
try {
|
|
1085
|
+
registration = ctx.llm.registerAdapter([PROVIDER_ID], adapter);
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
ctx.logger.error(`llm-opencode-go: not registering the "${PROVIDER_ID}" route (${String(error)})`);
|
|
1088
|
+
}
|
|
1089
|
+
} else if ((!configured || !current().enabled) && registration !== void 0) {
|
|
1090
|
+
registration();
|
|
1091
|
+
registration = void 0;
|
|
1092
|
+
if (!current().enabled) {
|
|
1093
|
+
ctx.logger.info("llm-opencode-go: disabled by configuration; the route and its models are withdrawn");
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
const syncRoute = () => {
|
|
1098
|
+
const credentials = ctx.get("credentials");
|
|
1099
|
+
if (credentials === void 0) {
|
|
1100
|
+
applyRoute(launchEnvironmentOf(ctx).get(current().apiKeyEnv)?.value !== void 0);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
void credentials.describe(credentialRef(current().apiKeyEnv)).then((info) => {
|
|
1104
|
+
applyRoute(info.configured);
|
|
1105
|
+
}).catch((error) => {
|
|
1106
|
+
ctx.logger.error(`llm-opencode-go: credential describe failed; keeping the previous route state (${String(error)})`);
|
|
1107
|
+
});
|
|
1108
|
+
};
|
|
1109
|
+
syncRoute();
|
|
1110
|
+
const undiscover = ctx.llm.registerModelDiscovery(name, async (request) => {
|
|
1111
|
+
if (request.provider !== PROVIDER_ID && !(request.baseURL ?? "").includes("opencode.ai")) {
|
|
1112
|
+
throw new LlmError6(
|
|
1113
|
+
"llm-opencode-go discovers only OpenCode zen/go endpoints; enter this provider's models by hand",
|
|
1114
|
+
"DISCOVERY_UNSUPPORTED"
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
return discoverCatalogModels(adapter.catalogOf(current()));
|
|
1118
|
+
});
|
|
1119
|
+
ctx.effect(() => () => {
|
|
1120
|
+
registration?.();
|
|
1121
|
+
undiscover();
|
|
1122
|
+
});
|
|
1123
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
1124
|
+
settingsCtx.settings.installSection(ctx, NS, Config, entry, {
|
|
1125
|
+
validate: (value) => {
|
|
1126
|
+
assertBaseURL(value.baseURL);
|
|
1127
|
+
},
|
|
1128
|
+
setSource: (source) => {
|
|
1129
|
+
current = source;
|
|
1130
|
+
},
|
|
1131
|
+
onChange: () => {
|
|
1132
|
+
syncRoute();
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
1136
|
+
ctx.inject(["credentials"], (credentialsCtx) => {
|
|
1137
|
+
credentialsCtx.on("credentials/reference-updated", (ref) => {
|
|
1138
|
+
if (ref === current().apiKeyEnv) syncRoute();
|
|
1139
|
+
});
|
|
1140
|
+
syncRoute();
|
|
1141
|
+
});
|
|
1142
|
+
ctx.logger.info(`llm-opencode-go: route "${PROVIDER_ID}" registered as ${DISPLAY_NAME}`);
|
|
1143
|
+
}
|
|
1144
|
+
export {
|
|
1145
|
+
Config,
|
|
1146
|
+
DEFAULT_BASE_URL,
|
|
1147
|
+
DISPLAY_NAME,
|
|
1148
|
+
NS,
|
|
1149
|
+
OpencodeGoAdapter,
|
|
1150
|
+
OpencodeGoCatalog,
|
|
1151
|
+
PROVIDER_ID,
|
|
1152
|
+
apply,
|
|
1153
|
+
assertBaseURL,
|
|
1154
|
+
discoverCatalogModels,
|
|
1155
|
+
inject,
|
|
1156
|
+
name,
|
|
1157
|
+
readLiveModelIds
|
|
1158
|
+
};
|