@dan-ai-studio/dshopencodego 0.1.5
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.en.md +156 -0
- package/README.md +156 -0
- package/cordis.patch.yml +3 -0
- package/lib/build-info.json +11 -0
- package/lib/client.js +1215 -0
- package/lib/index.js +2036 -0
- package/lib/types/adapter.d.ts +84 -0
- package/lib/types/adapter.js +311 -0
- package/lib/types/catalog/constants.d.ts +16 -0
- package/lib/types/catalog/constants.js +16 -0
- package/lib/types/catalog/contract.d.ts +26 -0
- package/lib/types/catalog/contract.js +131 -0
- package/lib/types/catalog/gateway.d.ts +20 -0
- package/lib/types/catalog/gateway.js +59 -0
- package/lib/types/catalog/index.d.ts +108 -0
- package/lib/types/catalog/index.js +288 -0
- package/lib/types/catalog/json-response.d.ts +19 -0
- package/lib/types/catalog/json-response.js +72 -0
- package/lib/types/catalog/metadata.d.ts +73 -0
- package/lib/types/catalog/metadata.js +259 -0
- package/lib/types/catalog/protocol.d.ts +65 -0
- package/lib/types/catalog/protocol.js +87 -0
- package/lib/types/catalog/reading.d.ts +41 -0
- package/lib/types/catalog/reading.js +68 -0
- package/lib/types/catalog/service.d.ts +32 -0
- package/lib/types/catalog/service.js +45 -0
- package/lib/types/config.d.ts +93 -0
- package/lib/types/config.js +76 -0
- package/lib/types/conversion/context.d.ts +55 -0
- package/lib/types/conversion/context.js +202 -0
- package/lib/types/conversion/index.d.ts +9 -0
- package/lib/types/conversion/index.js +7 -0
- package/lib/types/conversion/replay.d.ts +56 -0
- package/lib/types/conversion/replay.js +242 -0
- package/lib/types/conversion/stream.d.ts +46 -0
- package/lib/types/conversion/stream.js +203 -0
- package/lib/types/go-limits.d.ts +41 -0
- package/lib/types/go-limits.js +79 -0
- package/lib/types/index.d.ts +54 -0
- package/lib/types/index.js +195 -0
- package/lib/types/models.d.ts +90 -0
- package/lib/types/models.js +86 -0
- package/lib/types/remotes.d.ts +12 -0
- package/lib/types/remotes.js +28 -0
- package/lib/types/session-header.d.ts +36 -0
- package/lib/types/session-header.js +45 -0
- package/lib/types/usage/contract.d.ts +39 -0
- package/lib/types/usage/contract.js +106 -0
- package/lib/types/usage/index.d.ts +11 -0
- package/lib/types/usage/index.js +8 -0
- package/lib/types/usage/meter.d.ts +53 -0
- package/lib/types/usage/meter.js +65 -0
- package/lib/types/usage/service.d.ts +48 -0
- package/lib/types/usage/service.js +74 -0
- package/lib/types/usage/windows.d.ts +51 -0
- package/lib/types/usage/windows.js +84 -0
- package/package.json +147 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,2036 @@
|
|
|
1
|
+
var __knownSymbol = (name2, symbol) => (symbol = Symbol[name2]) ? symbol : /* @__PURE__ */ 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 { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
49
|
+
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
50
|
+
import * as llm2 from "@deepseek-ai/dsh-llm";
|
|
51
|
+
import { LlmError as LlmError11 } from "@deepseek-ai/dsh-llm";
|
|
52
|
+
|
|
53
|
+
// src/adapter.ts
|
|
54
|
+
import { getSupportedThinkingLevels, normalizeContext } from "@earendil-works/pi-ai";
|
|
55
|
+
import {
|
|
56
|
+
attributionHeaders as attributionHeaders4,
|
|
57
|
+
contentHasImage as contentHasImage2,
|
|
58
|
+
LlmAdapter,
|
|
59
|
+
LlmError as LlmError7,
|
|
60
|
+
ReasoningEffortId
|
|
61
|
+
} from "@deepseek-ai/dsh-llm";
|
|
62
|
+
import { idleWatchdog, timeoutOf } from "@deepseek-ai/dsh-timeout";
|
|
63
|
+
|
|
64
|
+
// src/catalog/index.ts
|
|
65
|
+
import { createProvider } from "@earendil-works/pi-ai";
|
|
66
|
+
import { getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
|
|
67
|
+
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
68
|
+
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
69
|
+
import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
|
|
70
|
+
import { attributionHeaders as attributionHeaders2, LlmError as LlmError3 } from "@deepseek-ai/dsh-llm";
|
|
71
|
+
|
|
72
|
+
// src/catalog/constants.ts
|
|
73
|
+
var PROVIDER_ID = "opencode-go";
|
|
74
|
+
var DISPLAY_NAME = "OpenCode Go";
|
|
75
|
+
var DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
|
|
76
|
+
var MODEL_METADATA_URL = "https://models.dev/api.json";
|
|
77
|
+
var MODEL_METADATA_PROVIDER = "opencode-go";
|
|
78
|
+
var METADATA_FETCH_TIMEOUT_MS = 1e4;
|
|
79
|
+
var MODEL_LISTING_MAX_BYTES = 1024 * 1024;
|
|
80
|
+
var MODEL_METADATA_MAX_BYTES = 16 * 1024 * 1024;
|
|
81
|
+
|
|
82
|
+
// src/catalog/gateway.ts
|
|
83
|
+
import { attributionHeaders, LlmError as LlmError2 } from "@deepseek-ai/dsh-llm";
|
|
84
|
+
|
|
85
|
+
// src/catalog/json-response.ts
|
|
86
|
+
import { LlmError } from "@deepseek-ai/dsh-llm";
|
|
87
|
+
async function readBoundedJson(response, url, maxBytes) {
|
|
88
|
+
const declared = Number(response.headers.get("content-length") ?? Number.NaN);
|
|
89
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
90
|
+
await response.body?.cancel().catch(() => {
|
|
91
|
+
});
|
|
92
|
+
throw new LlmError(`${url} declares ${declared} bytes, over the ${maxBytes}-byte cap`, "DISCOVERY_FAILED");
|
|
93
|
+
}
|
|
94
|
+
let text;
|
|
95
|
+
try {
|
|
96
|
+
text = await readBoundedText(response, maxBytes);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error instanceof LlmError) throw error;
|
|
99
|
+
throw new LlmError(`could not read ${url}`, "DISCOVERY_FAILED", { cause: error });
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(text);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new LlmError(`${url} did not answer with JSON`, "DISCOVERY_FAILED", { cause: error });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function readBoundedText(response, maxBytes) {
|
|
108
|
+
if (response.body === null) return "";
|
|
109
|
+
const reader = response.body.getReader();
|
|
110
|
+
const chunks = [];
|
|
111
|
+
let total = 0;
|
|
112
|
+
try {
|
|
113
|
+
while (true) {
|
|
114
|
+
const { done, value } = await reader.read();
|
|
115
|
+
if (done) break;
|
|
116
|
+
total += value.byteLength;
|
|
117
|
+
if (total > maxBytes) {
|
|
118
|
+
await reader.cancel().catch(() => {
|
|
119
|
+
});
|
|
120
|
+
throw new LlmError(`response exceeds the ${maxBytes}-byte cap`, "DISCOVERY_FAILED");
|
|
121
|
+
}
|
|
122
|
+
chunks.push(value);
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
reader.releaseLock();
|
|
126
|
+
}
|
|
127
|
+
const merged = new Uint8Array(total);
|
|
128
|
+
let offset = 0;
|
|
129
|
+
for (const chunk of chunks) {
|
|
130
|
+
merged.set(chunk, offset);
|
|
131
|
+
offset += chunk.byteLength;
|
|
132
|
+
}
|
|
133
|
+
return new TextDecoder().decode(merged);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/catalog/gateway.ts
|
|
137
|
+
function readModelIds(body) {
|
|
138
|
+
const data = body?.data;
|
|
139
|
+
if (!Array.isArray(data)) throw new Error('the model listing has no "data" array');
|
|
140
|
+
const ids = [];
|
|
141
|
+
for (const entry of data) {
|
|
142
|
+
const id = entry?.id;
|
|
143
|
+
if (typeof id === "string" && id.length > 0) ids.push(id);
|
|
144
|
+
}
|
|
145
|
+
return [...new Set(ids)];
|
|
146
|
+
}
|
|
147
|
+
async function fetchModelIds(baseURL, signal) {
|
|
148
|
+
const url = `${baseURL.replace(/\/+$/, "")}/models`;
|
|
149
|
+
const timeout = AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS);
|
|
150
|
+
const response = await fetch(url, {
|
|
151
|
+
method: "GET",
|
|
152
|
+
redirect: "error",
|
|
153
|
+
// An explicit request header rather than `RequestInit.cache`: Node's fetch
|
|
154
|
+
// does not implement HTTP caching, and the header is what any intermediary
|
|
155
|
+
// in front of the gateway reads.
|
|
156
|
+
headers: { ...attributionHeaders(), accept: "application/json", "cache-control": "no-cache" },
|
|
157
|
+
signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout])
|
|
158
|
+
}).catch((error) => {
|
|
159
|
+
throw new LlmError2(`could not reach ${url}`, "DISCOVERY_FAILED", { cause: error });
|
|
160
|
+
});
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
await response.body?.cancel().catch(() => {
|
|
163
|
+
});
|
|
164
|
+
throw new LlmError2(`${url} answered HTTP ${response.status}`, "DISCOVERY_FAILED");
|
|
165
|
+
}
|
|
166
|
+
const body = await readBoundedJson(response, url, MODEL_LISTING_MAX_BYTES);
|
|
167
|
+
try {
|
|
168
|
+
return readModelIds(body);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw new LlmError2(`${url} returned an invalid model listing`, "DISCOVERY_FAILED", { cause: error });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/catalog/protocol.ts
|
|
175
|
+
var WIRE_PROTOCOLS = [
|
|
176
|
+
"anthropic-messages",
|
|
177
|
+
"openai-completions",
|
|
178
|
+
"openai-responses"
|
|
179
|
+
];
|
|
180
|
+
var NPM_PROTOCOLS = {
|
|
181
|
+
"@ai-sdk/anthropic": "anthropic-messages",
|
|
182
|
+
"@ai-sdk/openai": "openai-responses",
|
|
183
|
+
"@ai-sdk/openai-compatible": "openai-completions"
|
|
184
|
+
};
|
|
185
|
+
var RESPONSES_FAMILIES = ["grok", "gpt", "muse"];
|
|
186
|
+
function protocolOfNpm(npm) {
|
|
187
|
+
return typeof npm === "string" ? NPM_PROTOCOLS[npm] : void 0;
|
|
188
|
+
}
|
|
189
|
+
function inferProtocol(id) {
|
|
190
|
+
const lower = id.toLowerCase();
|
|
191
|
+
return RESPONSES_FAMILIES.some((family) => lower.startsWith(family)) ? "openai-responses" : "openai-completions";
|
|
192
|
+
}
|
|
193
|
+
function asWireProtocol(value) {
|
|
194
|
+
return value !== void 0 && WIRE_PROTOCOLS.includes(value) ? value : void 0;
|
|
195
|
+
}
|
|
196
|
+
function decideProtocol(id, evidence) {
|
|
197
|
+
if (evidence.override !== void 0) return { api: evidence.override, source: "override" };
|
|
198
|
+
if (evidence.builtin !== void 0) return { api: evidence.builtin, source: "builtin" };
|
|
199
|
+
if (evidence.online !== void 0) return { api: evidence.online, source: "online" };
|
|
200
|
+
return { api: inferProtocol(id), source: "inferred" };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/catalog/metadata.ts
|
|
204
|
+
var LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
205
|
+
function record(value) {
|
|
206
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
207
|
+
}
|
|
208
|
+
function positiveInteger(value) {
|
|
209
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
210
|
+
}
|
|
211
|
+
function nonEmptyString(value) {
|
|
212
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
213
|
+
}
|
|
214
|
+
function validReleaseDate(value) {
|
|
215
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return void 0;
|
|
216
|
+
return Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value ? value : void 0;
|
|
217
|
+
}
|
|
218
|
+
function rates(value) {
|
|
219
|
+
const cost = record(value);
|
|
220
|
+
const rate = (key) => {
|
|
221
|
+
const entry = cost[key];
|
|
222
|
+
return typeof entry === "number" && Number.isFinite(entry) && entry >= 0 ? entry : 0;
|
|
223
|
+
};
|
|
224
|
+
return { input: rate("input"), output: rate("output"), cacheRead: rate("cache_read"), cacheWrite: rate("cache_write") };
|
|
225
|
+
}
|
|
226
|
+
function thinkingLevels(metadata, known) {
|
|
227
|
+
const options = metadata["reasoning_options"];
|
|
228
|
+
if (!Array.isArray(options)) return known?.thinkingLevelMap;
|
|
229
|
+
const map = Object.fromEntries(LEVELS.map((level) => [level, null]));
|
|
230
|
+
for (const item of options) {
|
|
231
|
+
const option = record(item);
|
|
232
|
+
if (option["type"] === "toggle" || option["type"] === "budget_tokens") {
|
|
233
|
+
map.off = "off";
|
|
234
|
+
map.high = "high";
|
|
235
|
+
}
|
|
236
|
+
if (option["type"] !== "effort" || !Array.isArray(option["values"])) continue;
|
|
237
|
+
for (const value of option["values"]) {
|
|
238
|
+
const level = value === "none" ? "off" : value;
|
|
239
|
+
if (typeof level === "string" && LEVELS.includes(level)) {
|
|
240
|
+
map[level] = String(value);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (known?.reasoning === true && known.thinkingLevelMap?.off !== null) {
|
|
245
|
+
map.off ??= known.thinkingLevelMap?.off ?? "off";
|
|
246
|
+
}
|
|
247
|
+
return map;
|
|
248
|
+
}
|
|
249
|
+
function familySibling(id, api, documentFamily, entries, builtin) {
|
|
250
|
+
if (documentFamily !== void 0) {
|
|
251
|
+
for (const [other, value] of Object.entries(entries)) {
|
|
252
|
+
if (other === id || record(value)["family"] !== documentFamily) continue;
|
|
253
|
+
const candidate = builtin.get(other);
|
|
254
|
+
if (candidate !== void 0 && candidate.api === api) return candidate;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
let best;
|
|
258
|
+
const lower = id.toLowerCase();
|
|
259
|
+
for (const [other, candidate] of builtin) {
|
|
260
|
+
if (other === id || candidate.api !== api) continue;
|
|
261
|
+
const score = commonPrefixLength(lower, other.toLowerCase());
|
|
262
|
+
if (score < 3) continue;
|
|
263
|
+
if (best === void 0 || score > best.score) best = { model: candidate, score };
|
|
264
|
+
}
|
|
265
|
+
return best?.model;
|
|
266
|
+
}
|
|
267
|
+
function commonPrefixLength(left, right) {
|
|
268
|
+
let index = 0;
|
|
269
|
+
while (index < left.length && index < right.length && left[index] === right[index]) index += 1;
|
|
270
|
+
return index;
|
|
271
|
+
}
|
|
272
|
+
function compatFor(api, metadata, exact, sibling) {
|
|
273
|
+
const inherited = exact !== void 0 && exact.api === api ? exact.compat : sibling !== void 0 && sibling.api === api ? sibling.compat : void 0;
|
|
274
|
+
if (api === "openai-completions") {
|
|
275
|
+
return {
|
|
276
|
+
supportsStore: false,
|
|
277
|
+
supportsDeveloperRole: false,
|
|
278
|
+
maxTokensField: "max_tokens",
|
|
279
|
+
...record(metadata["interleaved"])["field"] === "reasoning_content" ? { requiresReasoningContentOnAssistantMessages: true } : {},
|
|
280
|
+
...inherited
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (api === "openai-responses") {
|
|
284
|
+
return { sessionAffinityFormat: "openai-nosession", ...inherited };
|
|
285
|
+
}
|
|
286
|
+
return inherited ?? {};
|
|
287
|
+
}
|
|
288
|
+
function modelBaseURL(api, baseURL) {
|
|
289
|
+
const base = baseURL.replace(/\/+$/, "");
|
|
290
|
+
return api === "anthropic-messages" ? base.replace(/\/v1$/, "") : base;
|
|
291
|
+
}
|
|
292
|
+
function readOnlineMetadata(body, sources) {
|
|
293
|
+
const provider = record(record(body)[MODEL_METADATA_PROVIDER]);
|
|
294
|
+
const rawModels = provider["models"];
|
|
295
|
+
if (rawModels === null || typeof rawModels !== "object" || Array.isArray(rawModels)) {
|
|
296
|
+
throw new Error(`models.dev has no ${MODEL_METADATA_PROVIDER} models object`);
|
|
297
|
+
}
|
|
298
|
+
const providerNpm = provider["npm"];
|
|
299
|
+
const entries = record(rawModels);
|
|
300
|
+
const models = /* @__PURE__ */ new Map();
|
|
301
|
+
const errors = /* @__PURE__ */ new Map();
|
|
302
|
+
for (const [id, value] of Object.entries(entries)) {
|
|
303
|
+
const metadata = record(value);
|
|
304
|
+
try {
|
|
305
|
+
const exact = sources.builtin.get(id);
|
|
306
|
+
const override = asWireProtocol(sources.overrides[id]);
|
|
307
|
+
if (sources.overrides[id] !== void 0 && override === void 0) {
|
|
308
|
+
throw new Error(`configured protocol "${sources.overrides[id]}" is not one of anthropic-messages, openai-completions, openai-responses`);
|
|
309
|
+
}
|
|
310
|
+
const decision = decideProtocol(id, {
|
|
311
|
+
...exact?.api === void 0 ? {} : { builtin: exact.api },
|
|
312
|
+
...protocolOfNpm(metadata["provider"] === void 0 ? providerNpm : record(metadata["provider"])["npm"] ?? providerNpm) === void 0 ? {} : { online: protocolOfNpm(record(metadata["provider"])["npm"] ?? providerNpm) },
|
|
313
|
+
...override === void 0 ? {} : { override }
|
|
314
|
+
});
|
|
315
|
+
if (decision === void 0) throw new Error("no protocol could be decided");
|
|
316
|
+
const api = decision.api;
|
|
317
|
+
const limit = record(metadata["limit"]);
|
|
318
|
+
const onlineContext = positiveInteger(limit["context"]);
|
|
319
|
+
const onlineInputLimit = positiveInteger(limit["input"]);
|
|
320
|
+
const onlineOutput = positiveInteger(limit["output"]);
|
|
321
|
+
const builtinContext = exact?.api === api ? positiveInteger(exact.contextWindow) : void 0;
|
|
322
|
+
const builtinOutput = exact?.api === api ? positiveInteger(exact.maxTokens) : void 0;
|
|
323
|
+
const contextWindow = onlineContext ?? builtinContext ?? sources.defaults.contextWindow;
|
|
324
|
+
const maxTokens = onlineOutput ?? builtinOutput ?? sources.defaults.maxTokens;
|
|
325
|
+
const onlineInput = record(metadata["modalities"])["input"];
|
|
326
|
+
const input = Array.isArray(onlineInput) && onlineInput.includes("text") ? onlineInput.includes("image") ? ["text", "image"] : ["text"] : exact?.api === api ? exact.input : sources.defaults.input;
|
|
327
|
+
const reasoning = typeof metadata["reasoning"] === "boolean" ? metadata["reasoning"] : exact?.api === api ? exact.reasoning : false;
|
|
328
|
+
const family = nonEmptyString(metadata["family"]);
|
|
329
|
+
const sibling = familySibling(id, api, family, entries, sources.builtin);
|
|
330
|
+
const cost = rates(metadata["cost"]);
|
|
331
|
+
const tiers = record(metadata["cost"])["tiers"];
|
|
332
|
+
if (Array.isArray(tiers)) {
|
|
333
|
+
const parsed = tiers.flatMap((item) => {
|
|
334
|
+
const tier = record(record(item)["tier"]);
|
|
335
|
+
const size = positiveInteger(tier["size"]);
|
|
336
|
+
return tier["type"] === "context" && size !== void 0 ? [{ ...rates(item), inputTokensAbove: size }] : [];
|
|
337
|
+
}).sort((a, b) => a.inputTokensAbove - b.inputTokensAbove);
|
|
338
|
+
if (parsed.length > 0) cost.tiers = parsed;
|
|
339
|
+
}
|
|
340
|
+
models.set(id, {
|
|
341
|
+
id,
|
|
342
|
+
name: nonEmptyString(metadata["name"]) ?? id,
|
|
343
|
+
api,
|
|
344
|
+
protocolSource: decision.source,
|
|
345
|
+
contextWindow,
|
|
346
|
+
maxInputTokens: onlineInputLimit,
|
|
347
|
+
maxTokens,
|
|
348
|
+
assumedLimits: onlineContext === void 0 && builtinContext === void 0 || onlineOutput === void 0 && builtinOutput === void 0,
|
|
349
|
+
input,
|
|
350
|
+
reasoning,
|
|
351
|
+
thinkingLevelMap: reasoning ? thinkingLevels(metadata, exact?.api === api ? exact : sibling) : void 0,
|
|
352
|
+
compat: compatFor(api, metadata, exact, sibling),
|
|
353
|
+
cost,
|
|
354
|
+
deprecated: metadata["status"] === "deprecated",
|
|
355
|
+
releaseDate: validReleaseDate(metadata["release_date"])
|
|
356
|
+
});
|
|
357
|
+
} catch (error) {
|
|
358
|
+
errors.set(id, error instanceof Error ? error.message : String(error));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return { models, errors };
|
|
362
|
+
}
|
|
363
|
+
function toPiModel(facts, baseURL) {
|
|
364
|
+
return {
|
|
365
|
+
id: facts.id,
|
|
366
|
+
name: facts.name,
|
|
367
|
+
provider: "opencode-go",
|
|
368
|
+
api: facts.api,
|
|
369
|
+
baseUrl: modelBaseURL(facts.api, baseURL),
|
|
370
|
+
reasoning: facts.reasoning,
|
|
371
|
+
...facts.thinkingLevelMap === void 0 ? {} : { thinkingLevelMap: facts.thinkingLevelMap },
|
|
372
|
+
input: [...facts.input],
|
|
373
|
+
contextWindow: facts.contextWindow,
|
|
374
|
+
maxTokens: facts.maxTokens,
|
|
375
|
+
cost: facts.cost,
|
|
376
|
+
compat: facts.compat
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/catalog/index.ts
|
|
381
|
+
function builtinModels(baseURL) {
|
|
382
|
+
return new Map(getBuiltinModels(PROVIDER_ID).map((model) => [model.id, {
|
|
383
|
+
...model,
|
|
384
|
+
provider: PROVIDER_ID,
|
|
385
|
+
baseUrl: modelBaseURLFor(model.api, baseURL)
|
|
386
|
+
}]));
|
|
387
|
+
}
|
|
388
|
+
function modelBaseURLFor(api, baseURL) {
|
|
389
|
+
const base = baseURL.replace(/\/+$/, "");
|
|
390
|
+
return api === "anthropic-messages" ? base.replace(/\/v1$/, "") : base;
|
|
391
|
+
}
|
|
392
|
+
function harnessApiKeyAuth() {
|
|
393
|
+
return {
|
|
394
|
+
apiKey: {
|
|
395
|
+
name: "OpenCode Go API key",
|
|
396
|
+
resolve: () => Promise.resolve({ auth: {}, source: "OpenCode Go API key" })
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function buildProvider(baseURL, models) {
|
|
401
|
+
return createProvider({
|
|
402
|
+
id: PROVIDER_ID,
|
|
403
|
+
name: DISPLAY_NAME,
|
|
404
|
+
baseUrl: baseURL,
|
|
405
|
+
auth: harnessApiKeyAuth(),
|
|
406
|
+
models: [...models],
|
|
407
|
+
api: {
|
|
408
|
+
"anthropic-messages": anthropicMessagesApi(),
|
|
409
|
+
"openai-completions": openAICompletionsApi(),
|
|
410
|
+
"openai-responses": openAIResponsesApi()
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
var OpencodeGoCatalog = class {
|
|
415
|
+
options;
|
|
416
|
+
served;
|
|
417
|
+
pending;
|
|
418
|
+
metadataDocument;
|
|
419
|
+
metadataETag;
|
|
420
|
+
lastMetadata;
|
|
421
|
+
constructor(options) {
|
|
422
|
+
this.options = options;
|
|
423
|
+
}
|
|
424
|
+
/** The cached snapshot, refreshed when it is older than the configured TTL. */
|
|
425
|
+
snapshot(force = false) {
|
|
426
|
+
if (!force && this.served !== void 0 && Date.now() - this.served.fetchedAtMs < this.options.refreshMs) {
|
|
427
|
+
return Promise.resolve(this.served);
|
|
428
|
+
}
|
|
429
|
+
this.pending ??= this.build().then((snapshot) => {
|
|
430
|
+
this.served = snapshot;
|
|
431
|
+
return snapshot;
|
|
432
|
+
}).finally(() => {
|
|
433
|
+
this.pending = void 0;
|
|
434
|
+
});
|
|
435
|
+
return this.pending;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* The snapshot that can serve one exact model.
|
|
439
|
+
*
|
|
440
|
+
* A model this build has never seen forces one revalidation even inside the
|
|
441
|
+
* TTL, because "unknown" and "newly added" look identical from here.
|
|
442
|
+
* @param id - the requested model id.
|
|
443
|
+
* @returns the snapshot holding the model.
|
|
444
|
+
* @throws {LlmError} `MODEL_METADATA_UNAVAILABLE` when the gateway advertises
|
|
445
|
+
* the id but this build cannot configure it, naming the reason.
|
|
446
|
+
*/
|
|
447
|
+
async forModel(id) {
|
|
448
|
+
const cached = this.served;
|
|
449
|
+
let snapshot = await this.snapshot();
|
|
450
|
+
if (!snapshot.facts.has(id) && snapshot === cached) snapshot = await this.snapshot(true);
|
|
451
|
+
const reason = snapshot.unavailable.get(id);
|
|
452
|
+
if (reason !== void 0) {
|
|
453
|
+
throw new LlmError3(
|
|
454
|
+
`opencode-go model "${id}" is advertised but cannot be configured: ${reason}; refresh the model list to retry`,
|
|
455
|
+
"MODEL_METADATA_UNAVAILABLE"
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return snapshot;
|
|
459
|
+
}
|
|
460
|
+
/** Revalidate both sources, reusing the metadata document when it is unchanged. */
|
|
461
|
+
async build() {
|
|
462
|
+
const builtin = builtinModels(this.options.baseURL);
|
|
463
|
+
const [listing, metadata] = await Promise.allSettled([
|
|
464
|
+
fetchModelIds(this.options.baseURL),
|
|
465
|
+
this.refreshMetadata(builtin)
|
|
466
|
+
]);
|
|
467
|
+
if (metadata.status === "rejected") {
|
|
468
|
+
this.options.observers?.onFallback?.({
|
|
469
|
+
url: MODEL_METADATA_URL,
|
|
470
|
+
error: metadata.reason,
|
|
471
|
+
kept: this.lastMetadata?.models.size ?? 0
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
if (listing.status === "rejected") {
|
|
475
|
+
const previous = this.served;
|
|
476
|
+
this.options.observers?.onFallback?.({
|
|
477
|
+
url: `${this.options.baseURL.replace(/\/+$/, "")}/models`,
|
|
478
|
+
error: listing.reason,
|
|
479
|
+
kept: previous?.facts.size ?? 0
|
|
480
|
+
});
|
|
481
|
+
const facts = previous?.facts ?? /* @__PURE__ */ new Map();
|
|
482
|
+
return {
|
|
483
|
+
facts,
|
|
484
|
+
unavailable: previous?.unavailable ?? /* @__PURE__ */ new Map(),
|
|
485
|
+
provider: buildProvider(this.options.baseURL, [...facts.values()].map((fact) => toPiModel(fact, this.options.baseURL))),
|
|
486
|
+
live: false,
|
|
487
|
+
listingFailure: listing.reason,
|
|
488
|
+
fetchedAtMs: Date.now()
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return this.assemble(
|
|
492
|
+
listing.value,
|
|
493
|
+
metadata.status === "fulfilled" ? metadata.value : this.lastMetadata,
|
|
494
|
+
builtin
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
/** Merge the listing with the metadata document and the installed entries. */
|
|
498
|
+
assemble(ids, online, builtin) {
|
|
499
|
+
const facts = /* @__PURE__ */ new Map();
|
|
500
|
+
const unavailable = /* @__PURE__ */ new Map();
|
|
501
|
+
for (const id of ids) {
|
|
502
|
+
const parsed = online?.models.get(id);
|
|
503
|
+
if (parsed !== void 0) {
|
|
504
|
+
facts.set(id, parsed);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const reason = online?.errors.get(id);
|
|
508
|
+
if (reason !== void 0) {
|
|
509
|
+
unavailable.set(id, reason);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
facts.set(id, this.inferredFacts(id, builtin));
|
|
513
|
+
}
|
|
514
|
+
if (unavailable.size > 0) {
|
|
515
|
+
this.options.observers?.onUnconfigured?.(
|
|
516
|
+
[...unavailable].map(([id, reason]) => ({ id, reason }))
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
facts,
|
|
521
|
+
unavailable,
|
|
522
|
+
provider: buildProvider(this.options.baseURL, [...facts.values()].map((fact) => toPiModel(fact, this.options.baseURL))),
|
|
523
|
+
live: true,
|
|
524
|
+
fetchedAtMs: Date.now()
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/** Facts for an id no source describes, from the ladder and the route defaults. */
|
|
528
|
+
inferredFacts(id, builtin) {
|
|
529
|
+
const exact = builtin.get(id);
|
|
530
|
+
const api = exact?.api ?? inferFamily(id);
|
|
531
|
+
const contextWindow = exact !== void 0 ? exact.contextWindow : this.options.defaults.contextWindow;
|
|
532
|
+
const maxTokens = exact !== void 0 ? exact.maxTokens : this.options.defaults.maxTokens;
|
|
533
|
+
return {
|
|
534
|
+
id,
|
|
535
|
+
name: exact?.name ?? id,
|
|
536
|
+
api,
|
|
537
|
+
protocolSource: exact === void 0 ? "inferred" : "builtin",
|
|
538
|
+
contextWindow,
|
|
539
|
+
maxInputTokens: void 0,
|
|
540
|
+
maxTokens,
|
|
541
|
+
assumedLimits: exact === void 0,
|
|
542
|
+
input: exact?.input ?? this.options.defaults.input,
|
|
543
|
+
reasoning: exact?.reasoning ?? false,
|
|
544
|
+
thinkingLevelMap: exact?.thinkingLevelMap,
|
|
545
|
+
compat: exact?.compat ?? {},
|
|
546
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
547
|
+
deprecated: false,
|
|
548
|
+
releaseDate: void 0
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
/** Conditional metadata GET: an unchanged document keeps its parsed form. */
|
|
552
|
+
async refreshMetadata(builtin) {
|
|
553
|
+
const response = await fetch(MODEL_METADATA_URL, {
|
|
554
|
+
redirect: "error",
|
|
555
|
+
headers: {
|
|
556
|
+
...attributionHeaders2(),
|
|
557
|
+
accept: "application/json",
|
|
558
|
+
"cache-control": "no-cache",
|
|
559
|
+
...this.metadataETag === void 0 ? {} : { "if-none-match": this.metadataETag }
|
|
560
|
+
},
|
|
561
|
+
signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS)
|
|
562
|
+
});
|
|
563
|
+
if (response.status === 304 && this.metadataDocument !== void 0) {
|
|
564
|
+
await response.body?.cancel().catch(() => {
|
|
565
|
+
});
|
|
566
|
+
const parsed2 = readOnlineMetadata(this.metadataDocument, {
|
|
567
|
+
builtin,
|
|
568
|
+
defaults: this.options.defaults,
|
|
569
|
+
overrides: this.options.overrides
|
|
570
|
+
});
|
|
571
|
+
this.lastMetadata = parsed2;
|
|
572
|
+
return parsed2;
|
|
573
|
+
}
|
|
574
|
+
if (!response.ok) {
|
|
575
|
+
await response.body?.cancel().catch(() => {
|
|
576
|
+
});
|
|
577
|
+
throw new Error(`models.dev answered ${response.status}`);
|
|
578
|
+
}
|
|
579
|
+
const document = await readBoundedJson(response, MODEL_METADATA_URL, MODEL_METADATA_MAX_BYTES);
|
|
580
|
+
this.metadataDocument = document;
|
|
581
|
+
this.metadataETag = response.headers.get("etag") ?? void 0;
|
|
582
|
+
const parsed = readOnlineMetadata(document, {
|
|
583
|
+
builtin,
|
|
584
|
+
defaults: this.options.defaults,
|
|
585
|
+
overrides: this.options.overrides
|
|
586
|
+
});
|
|
587
|
+
this.lastMetadata = parsed;
|
|
588
|
+
return parsed;
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
function inferFamily(id) {
|
|
592
|
+
const lower = id.toLowerCase();
|
|
593
|
+
return ["grok", "gpt", "muse"].some((family) => lower.startsWith(family)) ? "openai-responses" : "openai-completions";
|
|
594
|
+
}
|
|
595
|
+
async function discoverCatalogModels(catalog) {
|
|
596
|
+
const snapshot = await catalog.snapshot(true);
|
|
597
|
+
if (!snapshot.live) {
|
|
598
|
+
const detail = snapshot.listingFailure instanceof LlmError3 ? snapshot.listingFailure.message : "the live model listing is unreachable";
|
|
599
|
+
throw new LlmError3(`dshopencodego: ${detail}; refresh the model list to retry`, "DISCOVERY_FAILED", {
|
|
600
|
+
cause: snapshot.listingFailure
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return [
|
|
604
|
+
...[...snapshot.facts.values()].map((fact) => ({
|
|
605
|
+
id: fact.id,
|
|
606
|
+
name: fact.name,
|
|
607
|
+
contextWindow: fact.contextWindow,
|
|
608
|
+
maxTokens: fact.maxTokens
|
|
609
|
+
})),
|
|
610
|
+
...[...snapshot.unavailable].map(([id, reason]) => ({ id, name: `${id} (unconfigured: ${reason})` }))
|
|
611
|
+
];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/config.ts
|
|
615
|
+
import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
616
|
+
import z from "@deepseek-ai/schemastery";
|
|
617
|
+
var DEFAULT_API_KEY_ENV = "OPENCODE_GO_API_KEY";
|
|
618
|
+
var DEFAULT_REFRESH_MINUTES = 60;
|
|
619
|
+
var DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
620
|
+
var DEFAULT_MAX_REQUEST_IMAGE_BYTES = 6 * 1024 * 1024;
|
|
621
|
+
var DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 144e4;
|
|
622
|
+
var DEFAULT_REQUEST_IMAGE_MAX_BYTES = 4e5;
|
|
623
|
+
var fields = {
|
|
624
|
+
enabled: z.boolean().default(true),
|
|
625
|
+
modelVisibility: z.dict(z.boolean().required()).default({}),
|
|
626
|
+
apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
|
|
627
|
+
baseURL: z.string().default(DEFAULT_BASE_URL),
|
|
628
|
+
refreshMinutes: z.number().step(1).min(1).max(7 * 24 * 60).default(DEFAULT_REFRESH_MINUTES),
|
|
629
|
+
streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
630
|
+
maxRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_IMAGE_BYTES),
|
|
631
|
+
requestImagePixelBudget: z.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET),
|
|
632
|
+
requestImageMaxBytes: z.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_MAX_BYTES),
|
|
633
|
+
modelLimits: z.dict(z.union([z.const(null), z.object({
|
|
634
|
+
contextWindow: z.union([z.const(null), z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER)]),
|
|
635
|
+
maxTokens: z.union([z.const(null), z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER)])
|
|
636
|
+
})])).default({}),
|
|
637
|
+
modelProtocols: z.dict(z.string()).default({})
|
|
638
|
+
};
|
|
639
|
+
var PlainConfig = z.object(fields);
|
|
640
|
+
var Config = z.object(Object.fromEntries(
|
|
641
|
+
Object.entries(fields).map(([key, schema]) => [key, schema.volatile()])
|
|
642
|
+
));
|
|
643
|
+
function readConfig(config) {
|
|
644
|
+
return Object.fromEntries(Object.keys(fields).map((key) => [key, config[key].get()]));
|
|
645
|
+
}
|
|
646
|
+
function assertBaseURL(raw) {
|
|
647
|
+
let url;
|
|
648
|
+
try {
|
|
649
|
+
url = new URL(raw);
|
|
650
|
+
} catch {
|
|
651
|
+
throw new Error(`dshopencodego: baseURL "${raw}" is not a valid URL`);
|
|
652
|
+
}
|
|
653
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
654
|
+
throw new Error(`dshopencodego: baseURL "${raw}" must be http or https`);
|
|
655
|
+
}
|
|
656
|
+
if (url.search.length > 0 || url.hash.length > 0) {
|
|
657
|
+
throw new Error(`dshopencodego: baseURL "${raw}" must not carry a query or fragment`);
|
|
658
|
+
}
|
|
659
|
+
return url.toString().replace(/\/+$/, "");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/conversion/context.ts
|
|
663
|
+
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
664
|
+
import * as llm from "@deepseek-ai/dsh-llm";
|
|
665
|
+
import { contentHasImage, LlmError as LlmError5, requestImageHandleText } from "@deepseek-ai/dsh-llm";
|
|
666
|
+
import { requestImageDimensions } from "@deepseek-ai/dsh-attachment";
|
|
667
|
+
|
|
668
|
+
// src/conversion/replay.ts
|
|
669
|
+
import { LlmError as LlmError4 } from "@deepseek-ai/dsh-llm";
|
|
670
|
+
function parseArguments(raw) {
|
|
671
|
+
try {
|
|
672
|
+
const parsed = JSON.parse(raw);
|
|
673
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
674
|
+
return parsed;
|
|
675
|
+
}
|
|
676
|
+
} catch {
|
|
677
|
+
}
|
|
678
|
+
return {};
|
|
679
|
+
}
|
|
680
|
+
function emptyPiUsage() {
|
|
681
|
+
return {
|
|
682
|
+
input: 0,
|
|
683
|
+
output: 0,
|
|
684
|
+
cacheRead: 0,
|
|
685
|
+
cacheWrite: 0,
|
|
686
|
+
totalTokens: 0,
|
|
687
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function toPiReplayState(message, requestedModel = message.model) {
|
|
691
|
+
const responseModel = message.api === "anthropic-messages" && message.model !== requestedModel ? message.model : message.responseModel;
|
|
692
|
+
const response = {
|
|
693
|
+
kind: "pi-ai",
|
|
694
|
+
version: 2,
|
|
695
|
+
api: message.api,
|
|
696
|
+
provider: message.provider,
|
|
697
|
+
model: requestedModel,
|
|
698
|
+
...responseModel === void 0 ? {} : { responseModel },
|
|
699
|
+
...message.responseId === void 0 ? {} : { responseId: message.responseId },
|
|
700
|
+
...message.providerThinkingLevel === void 0 ? {} : { providerThinkingLevel: message.providerThinkingLevel },
|
|
701
|
+
stopReason: message.stopReason
|
|
702
|
+
};
|
|
703
|
+
return {
|
|
704
|
+
response,
|
|
705
|
+
blocks: message.content.map((block) => {
|
|
706
|
+
switch (block.type) {
|
|
707
|
+
case "text":
|
|
708
|
+
return {
|
|
709
|
+
type: "text",
|
|
710
|
+
...block.textSignature === void 0 ? {} : { textSignature: block.textSignature }
|
|
711
|
+
};
|
|
712
|
+
case "thinking":
|
|
713
|
+
return {
|
|
714
|
+
type: "reasoning",
|
|
715
|
+
...block.thinkingSignature === void 0 ? {} : { thinkingSignature: block.thinkingSignature },
|
|
716
|
+
...block.redacted === void 0 ? {} : { redacted: block.redacted }
|
|
717
|
+
};
|
|
718
|
+
case "toolCall":
|
|
719
|
+
return {
|
|
720
|
+
type: "tool-call",
|
|
721
|
+
...block.thoughtSignature === void 0 ? {} : { thoughtSignature: block.thoughtSignature }
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
})
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function invalidReplay(message) {
|
|
728
|
+
throw new LlmError4(`invalid pi-ai replay state: ${message}`, "INVALID_REPLAY_STATE");
|
|
729
|
+
}
|
|
730
|
+
function readReplayState(value) {
|
|
731
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return invalidReplay("expected a replay envelope");
|
|
732
|
+
const envelope = value;
|
|
733
|
+
const rawResponse = envelope["response"];
|
|
734
|
+
if (typeof rawResponse !== "object" || rawResponse === null || Array.isArray(rawResponse)) {
|
|
735
|
+
return invalidReplay("expected a response object");
|
|
736
|
+
}
|
|
737
|
+
const response = rawResponse;
|
|
738
|
+
if (response["kind"] !== "pi-ai") return invalidReplay("unknown state kind");
|
|
739
|
+
if (response["version"] !== 2) return invalidReplay(`unsupported version ${String(response["version"])}`);
|
|
740
|
+
for (const key of ["api", "provider", "model"]) {
|
|
741
|
+
if (typeof response[key] !== "string" || response[key].length === 0) return invalidReplay(`${key} must be a non-empty string`);
|
|
742
|
+
}
|
|
743
|
+
if (!["stop", "length", "toolUse", "error", "aborted"].includes(String(response["stopReason"]))) {
|
|
744
|
+
return invalidReplay("unknown stopReason");
|
|
745
|
+
}
|
|
746
|
+
for (const key of ["responseModel", "responseId", "providerThinkingLevel"]) {
|
|
747
|
+
if (response[key] !== void 0 && typeof response[key] !== "string") return invalidReplay(`${key} must be a string`);
|
|
748
|
+
}
|
|
749
|
+
const blocks = envelope["blocks"];
|
|
750
|
+
if (!Array.isArray(blocks)) return invalidReplay("blocks must be an array");
|
|
751
|
+
for (const [index, value2] of blocks.entries()) {
|
|
752
|
+
if (typeof value2 !== "object" || value2 === null || Array.isArray(value2)) return invalidReplay(`block ${index} must be an object`);
|
|
753
|
+
const block = value2;
|
|
754
|
+
if (!["text", "reasoning", "tool-call"].includes(String(block["type"]))) {
|
|
755
|
+
return invalidReplay(`block ${index} has an unknown type`);
|
|
756
|
+
}
|
|
757
|
+
for (const signature of ["textSignature", "thinkingSignature", "thoughtSignature"]) {
|
|
758
|
+
if (block[signature] !== void 0 && typeof block[signature] !== "string") {
|
|
759
|
+
return invalidReplay(`block ${index} ${signature} must be a string`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (block["redacted"] !== void 0 && typeof block["redacted"] !== "boolean") {
|
|
763
|
+
return invalidReplay(`block ${index} redacted must be boolean`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return { response, blocks };
|
|
767
|
+
}
|
|
768
|
+
function foreignAssistant(message) {
|
|
769
|
+
const source = message.source.kind === "model" ? message.source : void 0;
|
|
770
|
+
const content = [];
|
|
771
|
+
for (const block of message.content) {
|
|
772
|
+
switch (block.type) {
|
|
773
|
+
case "text":
|
|
774
|
+
content.push({ type: "text", text: block.text });
|
|
775
|
+
break;
|
|
776
|
+
case "reasoning":
|
|
777
|
+
content.push({ type: "thinking", thinking: block.text });
|
|
778
|
+
break;
|
|
779
|
+
case "tool-call":
|
|
780
|
+
content.push({
|
|
781
|
+
type: "toolCall",
|
|
782
|
+
id: block.id,
|
|
783
|
+
name: block.name,
|
|
784
|
+
arguments: parseArguments(block.arguments)
|
|
785
|
+
});
|
|
786
|
+
break;
|
|
787
|
+
case "image":
|
|
788
|
+
throw new LlmError4("pi-ai chat history cannot represent structured assistant image output", "UNSUPPORTED_CONTENT");
|
|
789
|
+
default:
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
role: "assistant",
|
|
795
|
+
content,
|
|
796
|
+
// Deliberately never equals a catalog API: absent replay state is foreign
|
|
797
|
+
// even when the source names this same provider and model.
|
|
798
|
+
api: "dsh-foreign",
|
|
799
|
+
provider: source?.provider ?? "dsh-foreign",
|
|
800
|
+
model: source?.model ?? "dsh-foreign",
|
|
801
|
+
usage: emptyPiUsage(),
|
|
802
|
+
stopReason: content.some((piece) => piece.type === "toolCall") ? "toolUse" : "stop",
|
|
803
|
+
timestamp: 0
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function replayedAssistant(message, source, rawState) {
|
|
807
|
+
const state = readReplayState(rawState);
|
|
808
|
+
if (state.response.provider !== source.provider) return invalidReplay("provider does not match assistant source");
|
|
809
|
+
if (state.response.model !== source.model) return invalidReplay("model does not match assistant source");
|
|
810
|
+
if (state.blocks.length !== message.content.length) return invalidReplay("block count does not match assistant content");
|
|
811
|
+
const content = message.content.map((block, index) => {
|
|
812
|
+
const replay = state.blocks[index];
|
|
813
|
+
if (replay === void 0 || replay.type !== block.type) return invalidReplay(`block ${index} does not match assistant content`);
|
|
814
|
+
switch (block.type) {
|
|
815
|
+
case "text":
|
|
816
|
+
return {
|
|
817
|
+
type: "text",
|
|
818
|
+
text: block.text,
|
|
819
|
+
...replay.type === "text" && replay.textSignature !== void 0 ? { textSignature: replay.textSignature } : {}
|
|
820
|
+
};
|
|
821
|
+
case "reasoning":
|
|
822
|
+
return {
|
|
823
|
+
type: "thinking",
|
|
824
|
+
thinking: block.text,
|
|
825
|
+
...replay.type === "reasoning" && replay.thinkingSignature !== void 0 ? { thinkingSignature: replay.thinkingSignature } : {},
|
|
826
|
+
...replay.type === "reasoning" && replay.redacted !== void 0 ? { redacted: replay.redacted } : {}
|
|
827
|
+
};
|
|
828
|
+
case "tool-call":
|
|
829
|
+
return {
|
|
830
|
+
type: "toolCall",
|
|
831
|
+
id: block.id,
|
|
832
|
+
name: block.name,
|
|
833
|
+
arguments: parseArguments(block.arguments),
|
|
834
|
+
...replay.type === "tool-call" && replay.thoughtSignature !== void 0 ? { thoughtSignature: replay.thoughtSignature } : {}
|
|
835
|
+
};
|
|
836
|
+
default:
|
|
837
|
+
return invalidReplay(`block ${index} has an unsupported Harness type`);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
return {
|
|
841
|
+
role: "assistant",
|
|
842
|
+
content,
|
|
843
|
+
api: state.response.api,
|
|
844
|
+
provider: state.response.provider,
|
|
845
|
+
// Anthropic reports aliases and fallbacks as `model`, unlike Completions'
|
|
846
|
+
// informational `responseModel`.
|
|
847
|
+
model: state.response.api === "anthropic-messages" ? state.response.responseModel ?? state.response.model : state.response.model,
|
|
848
|
+
...state.response.responseModel === void 0 ? {} : { responseModel: state.response.responseModel },
|
|
849
|
+
...state.response.responseId === void 0 ? {} : { responseId: state.response.responseId },
|
|
850
|
+
...state.response.providerThinkingLevel === void 0 ? {} : { providerThinkingLevel: state.response.providerThinkingLevel },
|
|
851
|
+
usage: emptyPiUsage(),
|
|
852
|
+
stopReason: state.response.stopReason,
|
|
853
|
+
timestamp: 0
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function toPiAssistant(message, onDegrade) {
|
|
857
|
+
const source = message.source;
|
|
858
|
+
if (source.kind !== "model" || source.replayState === void 0) return foreignAssistant(message);
|
|
859
|
+
try {
|
|
860
|
+
return replayedAssistant(message, source, source.replayState);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
if (!(error instanceof LlmError4) || error.code !== "INVALID_REPLAY_STATE") throw error;
|
|
863
|
+
onDegrade?.(error.message);
|
|
864
|
+
return foreignAssistant(message);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/conversion/context.ts
|
|
869
|
+
function flattenText(message) {
|
|
870
|
+
return message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
871
|
+
}
|
|
872
|
+
function toolMessage(message) {
|
|
873
|
+
return message.role === "tool" ? message : void 0;
|
|
874
|
+
}
|
|
875
|
+
function assertSupportedImageRoles(messages) {
|
|
876
|
+
for (const message of messages) {
|
|
877
|
+
if (message.role !== "user" && toolMessage(message) === void 0 && contentHasImage(message.content)) {
|
|
878
|
+
throw new LlmError5(`pi-ai cannot represent an image in an in-history ${message.role} message`, "UNSUPPORTED_CONTENT");
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function toolsOf(options) {
|
|
883
|
+
return options.tools?.map((tool) => ({
|
|
884
|
+
name: tool.name,
|
|
885
|
+
description: tool.description,
|
|
886
|
+
// ToolSchema.parameters is a JSON Schema object; pi-ai's TSchema is
|
|
887
|
+
// structurally JSON Schema, so it assigns directly.
|
|
888
|
+
parameters: tool.parameters
|
|
889
|
+
}));
|
|
890
|
+
}
|
|
891
|
+
function splitSystemPrompt(options) {
|
|
892
|
+
if (options.system !== void 0) return { systemPrompt: options.system, messages: options.messages };
|
|
893
|
+
const [first, ...rest] = options.messages;
|
|
894
|
+
if (first?.role !== "system") return { systemPrompt: void 0, messages: options.messages };
|
|
895
|
+
const text = flattenText(first);
|
|
896
|
+
return { systemPrompt: text.length > 0 ? text : void 0, messages: rest };
|
|
897
|
+
}
|
|
898
|
+
function collectImageRefs(blocks, refs) {
|
|
899
|
+
for (const block of blocks) {
|
|
900
|
+
if (block.type === "image" && block.offloaded !== true) refs.set(block.attachment.attachmentId, block.attachment);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
async function prepareRequestImages(messages, attachments, policy, signal) {
|
|
904
|
+
const refs = /* @__PURE__ */ new Map();
|
|
905
|
+
for (const message of messages) collectImageRefs(message.content, refs);
|
|
906
|
+
const ordered = [...refs.values()];
|
|
907
|
+
const prepared = await Promise.all(ordered.map((ref) => attachments.readImageRequest(
|
|
908
|
+
ref,
|
|
909
|
+
{ ...requestImageDimensions(ref.width, ref.height, policy.maxPixels), maxBytes: policy.maxBytes },
|
|
910
|
+
signal
|
|
911
|
+
)));
|
|
912
|
+
const versions = /* @__PURE__ */ new Map();
|
|
913
|
+
for (const [index, ref] of ordered.entries()) versions.set(ref.attachmentId, prepared[index]);
|
|
914
|
+
return versions;
|
|
915
|
+
}
|
|
916
|
+
async function userContent(blocks, requestImages, resolveImageAccess) {
|
|
917
|
+
const content = [];
|
|
918
|
+
for (const block of blocks) {
|
|
919
|
+
if (block.type === "text") {
|
|
920
|
+
if (block.text.length > 0) content.push({ type: "text", text: block.text });
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
if (block.type === "image") {
|
|
924
|
+
const version = requestImages.get(block.attachment.attachmentId);
|
|
925
|
+
if (version === void 0) {
|
|
926
|
+
throw new LlmError5(
|
|
927
|
+
"dshopencodego: an image in this request has no prepared bytes; image input requires the attachment service",
|
|
928
|
+
"UNSUPPORTED_CONTENT"
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
content.push({
|
|
932
|
+
type: "text",
|
|
933
|
+
text: requestImageHandleText(block.attachment, version, resolveImageAccess(block.attachment))
|
|
934
|
+
});
|
|
935
|
+
content.push({ type: "image", data: Buffer.from(version.data).toString("base64"), mimeType: version.mediaType });
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return content.every((block) => block.type === "text") ? content.map((block) => block.text).join("") : content;
|
|
939
|
+
}
|
|
940
|
+
function projectImagesForRequest(messages, images, requestImages) {
|
|
941
|
+
const offloadedImageText2 = llm.offloadedImageText;
|
|
942
|
+
const placeholder = (ref) => offloadedImageText2(ref, images.resolveImageAccess(ref));
|
|
943
|
+
if (images.maxRequestImageBytes !== void 0) {
|
|
944
|
+
const over = llm.requiredImageOffload(
|
|
945
|
+
messages,
|
|
946
|
+
{ representation: "base64", maxBytes: images.maxRequestImageBytes, byteQuantum: 1 },
|
|
947
|
+
(block) => requestImages.get(block.attachment.attachmentId).bytes
|
|
948
|
+
);
|
|
949
|
+
if (over > 0) {
|
|
950
|
+
throw new LlmError5(
|
|
951
|
+
`request images exceed the ${images.maxRequestImageBytes}-byte base64 bound; ${over} more oldest occurrence(s) must be offloaded`,
|
|
952
|
+
llm.IMAGE_OFFLOAD_REQUIRED_CODE,
|
|
953
|
+
{ offloadImages: over }
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return llm.projectOffloadedImages(messages, placeholder);
|
|
958
|
+
}
|
|
959
|
+
async function toPiContext(options, images, onReplayDegrade) {
|
|
960
|
+
assertSupportedImageRoles(options.messages);
|
|
961
|
+
const split = splitSystemPrompt(options);
|
|
962
|
+
const resolveImageAccess = images?.resolveImageAccess ?? (() => void 0);
|
|
963
|
+
const policy = images?.requestImagePolicy ?? { maxPixels: 144e4, maxBytes: 4e5 };
|
|
964
|
+
const requestImages = images === void 0 ? /* @__PURE__ */ new Map() : await prepareRequestImages(split.messages, images.attachments, policy, options.signal);
|
|
965
|
+
const history = images === void 0 ? split.messages : projectImagesForRequest(split.messages, images, requestImages);
|
|
966
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
967
|
+
const messages = [];
|
|
968
|
+
for (const message of history) {
|
|
969
|
+
const tool = toolMessage(message);
|
|
970
|
+
if (tool !== void 0) {
|
|
971
|
+
const content2 = await userContent(tool.content, requestImages, resolveImageAccess);
|
|
972
|
+
messages.push({
|
|
973
|
+
role: "toolResult",
|
|
974
|
+
toolCallId: tool.toolCallId,
|
|
975
|
+
toolName: toolNames.get(tool.toolCallId) ?? "unknown",
|
|
976
|
+
content: typeof content2 === "string" ? [{ type: "text", text: content2.length > 0 ? content2 : "(no output)" }] : content2,
|
|
977
|
+
isError: tool.isError ?? false,
|
|
978
|
+
timestamp: 0
|
|
979
|
+
});
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
if (message.role === "system") {
|
|
983
|
+
messages.push({ role: "user", content: flattenText(message), timestamp: 0 });
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (message.role === "assistant") {
|
|
987
|
+
const assistant = toPiAssistant(message, onReplayDegrade);
|
|
988
|
+
for (const block of assistant.content) {
|
|
989
|
+
if (block.type === "toolCall") toolNames.set(brandString(block.id), block.name);
|
|
990
|
+
}
|
|
991
|
+
messages.push(assistant);
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
const content = await userContent(message.content, requestImages, resolveImageAccess);
|
|
995
|
+
messages.push({ role: "user", content, timestamp: 0 });
|
|
996
|
+
}
|
|
997
|
+
const tools = toolsOf(options);
|
|
998
|
+
return {
|
|
999
|
+
...split.systemPrompt === void 0 ? {} : { systemPrompt: split.systemPrompt },
|
|
1000
|
+
messages,
|
|
1001
|
+
...tools === void 0 || tools.length === 0 ? {} : { tools }
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// src/conversion/stream.ts
|
|
1006
|
+
import { brandString as brandString2 } from "@deepseek-ai/dsh-brand";
|
|
1007
|
+
import {
|
|
1008
|
+
CONTEXT_WINDOW_EXCEEDED_CODE,
|
|
1009
|
+
EMPTY_RESPONSE_CODE,
|
|
1010
|
+
isContextWindowExceededError,
|
|
1011
|
+
isQuotaExceededError,
|
|
1012
|
+
LlmError as LlmError6,
|
|
1013
|
+
QUOTA_EXCEEDED_CODE
|
|
1014
|
+
} from "@deepseek-ai/dsh-llm";
|
|
1015
|
+
import { isContextOverflow } from "@earendil-works/pi-ai";
|
|
1016
|
+
function mapUsage(usage) {
|
|
1017
|
+
return {
|
|
1018
|
+
inputTokens: usage.input,
|
|
1019
|
+
outputTokens: usage.output,
|
|
1020
|
+
totalTokens: usage.totalTokens,
|
|
1021
|
+
...usage.cacheRead > 0 ? { cacheReadTokens: usage.cacheRead } : {},
|
|
1022
|
+
...usage.cacheWrite > 0 ? { cacheWriteTokens: usage.cacheWrite } : {}
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function classifyPiAiError(message) {
|
|
1026
|
+
if (/\b(?:401|403)\b/.test(message)) return "AUTH";
|
|
1027
|
+
if (isQuotaExceededError(message)) return QUOTA_EXCEEDED_CODE;
|
|
1028
|
+
if (/\b429\b|rate.?limit/i.test(message)) return "RATE_LIMIT";
|
|
1029
|
+
if (/\b413\b|payload too large|request body too large|length limit exceeded/i.test(message)) return "INVALID_REQUEST";
|
|
1030
|
+
if (/\b400\b|invalid.?request/i.test(message)) return "INVALID_REQUEST";
|
|
1031
|
+
if (/\b5\d\d\b/.test(message)) return "SERVER";
|
|
1032
|
+
if (/\btime(?:d)?\s*out\b|timeout/i.test(message)) return "TIMEOUT";
|
|
1033
|
+
if (/stream ended (?:before|without)\b/i.test(message)) return "TRANSPORT";
|
|
1034
|
+
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)) {
|
|
1035
|
+
return "TRANSPORT";
|
|
1036
|
+
}
|
|
1037
|
+
return "PI_AI_ERROR";
|
|
1038
|
+
}
|
|
1039
|
+
function mapStopReason(message, contextWindow) {
|
|
1040
|
+
const overflow = isContextOverflow(message, contextWindow) || message.stopReason === "error" && message.errorMessage !== void 0 && isContextWindowExceededError(message.errorMessage);
|
|
1041
|
+
if (overflow) {
|
|
1042
|
+
return {
|
|
1043
|
+
kind: "error",
|
|
1044
|
+
failure: {
|
|
1045
|
+
message: message.errorMessage ?? `pi-ai detected context overflow for model "${message.model}"`,
|
|
1046
|
+
code: CONTEXT_WINDOW_EXCEEDED_CODE
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
switch (message.stopReason) {
|
|
1051
|
+
case "stop":
|
|
1052
|
+
if (message.content.length === 0) {
|
|
1053
|
+
return {
|
|
1054
|
+
kind: "error",
|
|
1055
|
+
failure: {
|
|
1056
|
+
message: `model "${message.model}" returned a completed response with no content`,
|
|
1057
|
+
code: EMPTY_RESPONSE_CODE
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
return { kind: "stop" };
|
|
1062
|
+
case "length":
|
|
1063
|
+
return { kind: "max-tokens" };
|
|
1064
|
+
case "toolUse":
|
|
1065
|
+
return { kind: "tool-calls" };
|
|
1066
|
+
case "pending":
|
|
1067
|
+
return {
|
|
1068
|
+
kind: "error",
|
|
1069
|
+
failure: { message: `pi-ai stream for model "${message.model}" ended pending`, code: "PI_AI_ERROR" }
|
|
1070
|
+
};
|
|
1071
|
+
case "deferred":
|
|
1072
|
+
return {
|
|
1073
|
+
kind: "error",
|
|
1074
|
+
failure: { message: `pi-ai deferred response for model "${message.model}" is not supported`, code: "PI_AI_ERROR" }
|
|
1075
|
+
};
|
|
1076
|
+
case "aborted":
|
|
1077
|
+
return {
|
|
1078
|
+
kind: "aborted",
|
|
1079
|
+
failure: { message: message.errorMessage ?? "pi-ai stream aborted", code: "ABORTED" }
|
|
1080
|
+
};
|
|
1081
|
+
case "error": {
|
|
1082
|
+
const text = message.errorMessage ?? "pi-ai stream error";
|
|
1083
|
+
return { kind: "error", failure: { message: text, code: classifyPiAiError(text) } };
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
async function* toStreamChunks(events, contextWindow, callerSignal, requestedModel) {
|
|
1088
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
1089
|
+
for await (const event of events) {
|
|
1090
|
+
switch (event.type) {
|
|
1091
|
+
case "start":
|
|
1092
|
+
break;
|
|
1093
|
+
case "text_start":
|
|
1094
|
+
yield { type: "block-start", index: event.contentIndex, blockType: "text" };
|
|
1095
|
+
break;
|
|
1096
|
+
case "text_delta":
|
|
1097
|
+
yield { type: "text-delta", index: event.contentIndex, text: event.delta };
|
|
1098
|
+
break;
|
|
1099
|
+
case "text_end":
|
|
1100
|
+
yield { type: "block-end", index: event.contentIndex, block: { type: "text", text: event.content } };
|
|
1101
|
+
break;
|
|
1102
|
+
case "thinking_start":
|
|
1103
|
+
yield { type: "block-start", index: event.contentIndex, blockType: "reasoning" };
|
|
1104
|
+
break;
|
|
1105
|
+
case "thinking_delta":
|
|
1106
|
+
yield { type: "reasoning-delta", index: event.contentIndex, text: event.delta };
|
|
1107
|
+
break;
|
|
1108
|
+
case "thinking_end":
|
|
1109
|
+
yield { type: "block-end", index: event.contentIndex, block: { type: "reasoning", text: event.content } };
|
|
1110
|
+
break;
|
|
1111
|
+
case "toolcall_start": {
|
|
1112
|
+
const partial = event.partial.content[event.contentIndex];
|
|
1113
|
+
toolCalls.set(event.contentIndex, {
|
|
1114
|
+
id: partial?.type === "toolCall" ? partial.id : "",
|
|
1115
|
+
name: partial?.type === "toolCall" ? partial.name : ""
|
|
1116
|
+
});
|
|
1117
|
+
yield { type: "block-start", index: event.contentIndex, blockType: "tool-call" };
|
|
1118
|
+
break;
|
|
1119
|
+
}
|
|
1120
|
+
case "toolcall_delta": {
|
|
1121
|
+
const known = toolCalls.get(event.contentIndex);
|
|
1122
|
+
yield {
|
|
1123
|
+
type: "tool-call-delta",
|
|
1124
|
+
index: event.contentIndex,
|
|
1125
|
+
id: brandString2(known?.id ?? ""),
|
|
1126
|
+
...known !== void 0 && known.name.length > 0 ? { name: known.name } : {},
|
|
1127
|
+
argumentsDelta: event.delta
|
|
1128
|
+
};
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
case "toolcall_end":
|
|
1132
|
+
yield {
|
|
1133
|
+
type: "block-end",
|
|
1134
|
+
index: event.contentIndex,
|
|
1135
|
+
block: {
|
|
1136
|
+
type: "tool-call",
|
|
1137
|
+
id: brandString2(event.toolCall.id),
|
|
1138
|
+
name: event.toolCall.name,
|
|
1139
|
+
// pi-ai parses the arguments; the Harness vocabulary keeps the raw
|
|
1140
|
+
// JSON string it stores and replays.
|
|
1141
|
+
arguments: JSON.stringify(event.toolCall.arguments)
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
break;
|
|
1145
|
+
case "done":
|
|
1146
|
+
yield { type: "usage", usage: mapUsage(event.message.usage) };
|
|
1147
|
+
yield {
|
|
1148
|
+
type: "finish",
|
|
1149
|
+
reason: mapStopReason(event.message, contextWindow),
|
|
1150
|
+
replayState: toPiReplayState(event.message, requestedModel)
|
|
1151
|
+
};
|
|
1152
|
+
return;
|
|
1153
|
+
case "error":
|
|
1154
|
+
yield { type: "usage", usage: mapUsage(event.error.usage) };
|
|
1155
|
+
yield {
|
|
1156
|
+
type: "finish",
|
|
1157
|
+
reason: mapStopReason(
|
|
1158
|
+
callerSignal?.aborted ? { ...event.error, stopReason: "aborted" } : event.error,
|
|
1159
|
+
contextWindow
|
|
1160
|
+
)
|
|
1161
|
+
};
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
throw new LlmError6("pi-ai event stream ended without a terminal event", "STREAM_CLOSED");
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/go-limits.ts
|
|
1169
|
+
var GO_QUOTAS = {
|
|
1170
|
+
"glm-5.3-flash": { monthlyUsd: 60, monthlyRequests: 31580 },
|
|
1171
|
+
"glm-5.3": { monthlyUsd: 15, monthlyRequests: 1080 },
|
|
1172
|
+
"glm-5.2": { monthlyUsd: 60, monthlyRequests: 4300 },
|
|
1173
|
+
"glm-5.1": { monthlyUsd: 60, monthlyRequests: 4300 },
|
|
1174
|
+
"kimi-k3": { monthlyUsd: 15, monthlyRequests: 490 },
|
|
1175
|
+
"kimi-k2.7-code": { monthlyUsd: 60, monthlyRequests: 6750 },
|
|
1176
|
+
"kimi-k2.6": { monthlyUsd: 60, monthlyRequests: 5750 },
|
|
1177
|
+
"longcat-2.0": { monthlyUsd: 60, monthlyRequests: 57200 },
|
|
1178
|
+
"mimo-v2.6-flash": { monthlyUsd: 60, monthlyRequests: 150400 },
|
|
1179
|
+
"mimo-v2.6-pro": { monthlyUsd: 15, monthlyRequests: 16300 },
|
|
1180
|
+
"mimo-v2.5": { monthlyUsd: 60, monthlyRequests: 150400 },
|
|
1181
|
+
"mimo-v2.5-pro": { monthlyUsd: 15, monthlyRequests: 16300 },
|
|
1182
|
+
"minimax-m3": { monthlyUsd: 60, monthlyRequests: 16e3 },
|
|
1183
|
+
"minimax-m2.7": { monthlyUsd: 60, monthlyRequests: 17e3 },
|
|
1184
|
+
"minimax-m2.5": { monthlyUsd: 60 },
|
|
1185
|
+
"muse-spark-1.3-contributor": { monthlyUsd: 60, monthlyRequests: 226600 },
|
|
1186
|
+
"muse-spark-1.2-contributor": { monthlyUsd: 60, monthlyRequests: 226600 },
|
|
1187
|
+
"qwen3.8-max": { monthlyUsd: 15, monthlyRequests: 810 },
|
|
1188
|
+
"qwen3.8-flash": { monthlyUsd: 30, monthlyRequests: 27e3 },
|
|
1189
|
+
"qwen3.7-max": { monthlyUsd: 30, monthlyRequests: 840 },
|
|
1190
|
+
"qwen3.7-plus": { monthlyUsd: 60, monthlyRequests: 21600 },
|
|
1191
|
+
"qwen3.6-plus": { monthlyUsd: 60, monthlyRequests: 16300 },
|
|
1192
|
+
"deepseek-v4.1-flash": { monthlyUsd: 60, monthlyRequests: 13e4 },
|
|
1193
|
+
"deepseek-v4-pro": { monthlyUsd: 15, monthlyRequests: 5200 },
|
|
1194
|
+
"deepseek-v4-flash": { monthlyUsd: 30, monthlyRequests: 65e3 },
|
|
1195
|
+
"deepseek-v4-flash-vision-exp": { monthlyUsd: 15, monthlyRequests: 32500 },
|
|
1196
|
+
"hy4-preview": { monthlyUsd: 30, monthlyRequests: 6770 },
|
|
1197
|
+
"hy3": { monthlyUsd: 60, monthlyRequests: 21500 },
|
|
1198
|
+
"space-bunny-free": { monthlyUsd: "unlimited", monthlyRequests: "unlimited" },
|
|
1199
|
+
"grok-4.7": { monthlyUsd: 15, monthlyRequests: 845 },
|
|
1200
|
+
"grok-4.6": { monthlyUsd: 15, monthlyRequests: 845 },
|
|
1201
|
+
"gpt-6-luna": { monthlyUsd: 15, monthlyRequests: 21130 },
|
|
1202
|
+
"gpt-5.6-luna": { monthlyUsd: 15, monthlyRequests: 10250 }
|
|
1203
|
+
};
|
|
1204
|
+
function goQuotaFor(id) {
|
|
1205
|
+
return Object.hasOwn(GO_QUOTAS, id) ? GO_QUOTAS[id] : void 0;
|
|
1206
|
+
}
|
|
1207
|
+
function monthlyRequestsRank(quota) {
|
|
1208
|
+
if (quota === void 0) return -1;
|
|
1209
|
+
const requests = quota.monthlyRequests;
|
|
1210
|
+
if (requests === "unlimited") return Number.MAX_SAFE_INTEGER;
|
|
1211
|
+
if (requests === void 0) return quota.monthlyUsd === "unlimited" ? Number.MAX_SAFE_INTEGER : -1;
|
|
1212
|
+
return requests;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// src/models.ts
|
|
1216
|
+
var DEFAULT_ENABLED_COUNT = 5;
|
|
1217
|
+
function isTrainingTier(model) {
|
|
1218
|
+
return /contributor/i.test(model.id) || /contributor/i.test(model.name);
|
|
1219
|
+
}
|
|
1220
|
+
function recommendedIds(models) {
|
|
1221
|
+
const ranked = models.filter((model) => model.configurationMissing === void 0 && model.deprecated !== true && !isTrainingTier(model)).toSorted((left, right) => monthlyRequestsRank(goQuotaFor(right.id)) - monthlyRequestsRank(goQuotaFor(left.id)) || left.id.localeCompare(right.id));
|
|
1222
|
+
return new Set(ranked.slice(0, DEFAULT_ENABLED_COUNT).map((model) => model.id));
|
|
1223
|
+
}
|
|
1224
|
+
function isModelEnabled(model, visibility) {
|
|
1225
|
+
if (model.configurationMissing !== void 0) return false;
|
|
1226
|
+
const explicit = visibility !== void 0 && Object.hasOwn(visibility, model.id) ? visibility[model.id] : void 0;
|
|
1227
|
+
if (typeof explicit === "boolean") return explicit;
|
|
1228
|
+
return typeof model.recommended === "boolean" ? model.recommended : model.deprecated !== true;
|
|
1229
|
+
}
|
|
1230
|
+
function validReleaseDate2(value) {
|
|
1231
|
+
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
|
|
1232
|
+
}
|
|
1233
|
+
function isNewModel(model, now = Date.now()) {
|
|
1234
|
+
if (model.deprecated === true || !validReleaseDate2(model.releaseDate)) return false;
|
|
1235
|
+
const days = Math.floor(now / 864e5) - Date.parse(model.releaseDate) / 864e5;
|
|
1236
|
+
return days >= 0 && days < 7;
|
|
1237
|
+
}
|
|
1238
|
+
function sortModels(models, now = Date.now()) {
|
|
1239
|
+
const rank = (model) => model.deprecated === true ? 2 : isNewModel(model, now) ? 0 : 1;
|
|
1240
|
+
return [...models].sort((left, right) => rank(left) - rank(right) || (isNewModel(left, now) && isNewModel(right, now) ? right.releaseDate.localeCompare(left.releaseDate) : 0));
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/session-header.ts
|
|
1244
|
+
import { randomUUID } from "node:crypto";
|
|
1245
|
+
import { attributionHeaders as attributionHeaders3 } from "@deepseek-ai/dsh-llm";
|
|
1246
|
+
var SESSION_HEADER = "x-opencode-session";
|
|
1247
|
+
function opencodeSessionValue(sessionId) {
|
|
1248
|
+
return sessionId !== void 0 && sessionId.length > 0 ? sessionId : randomUUID();
|
|
1249
|
+
}
|
|
1250
|
+
function providerHeaders(sessionId) {
|
|
1251
|
+
return {
|
|
1252
|
+
[SESSION_HEADER]: opencodeSessionValue(sessionId),
|
|
1253
|
+
...attributionHeaders3()
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// src/adapter.ts
|
|
1258
|
+
var DISABLES_THINKING_WHEN_UNSET = /* @__PURE__ */ new Set(["deepseek", "zai", "qwen", "qwen-chat-template"]);
|
|
1259
|
+
function withModelLimit(model, limits) {
|
|
1260
|
+
const limit = limits[model.id];
|
|
1261
|
+
if (limit === null || limit === void 0) return model;
|
|
1262
|
+
return {
|
|
1263
|
+
...model,
|
|
1264
|
+
contextWindow: limit.contextWindow ?? model.contextWindow,
|
|
1265
|
+
maxTokens: limit.maxTokens ?? model.maxTokens
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
var OpencodeGoAdapter = class extends LlmAdapter {
|
|
1269
|
+
cache;
|
|
1270
|
+
options;
|
|
1271
|
+
constructor(options) {
|
|
1272
|
+
super();
|
|
1273
|
+
this.options = options;
|
|
1274
|
+
}
|
|
1275
|
+
/** The catalog for one configuration, rebuilt when its owned facts change. */
|
|
1276
|
+
catalogOf(config) {
|
|
1277
|
+
const key = `${config.baseURL}|${String(config.refreshMinutes)}|${JSON.stringify(config.modelProtocols)}`;
|
|
1278
|
+
if (this.cache?.key !== key) {
|
|
1279
|
+
this.cache = {
|
|
1280
|
+
key,
|
|
1281
|
+
catalog: new OpencodeGoCatalog({
|
|
1282
|
+
baseURL: assertBaseURL(config.baseURL),
|
|
1283
|
+
refreshMs: config.refreshMinutes * 6e4,
|
|
1284
|
+
defaults: {
|
|
1285
|
+
contextWindow: 262144,
|
|
1286
|
+
maxTokens: 32768,
|
|
1287
|
+
input: ["text"]
|
|
1288
|
+
},
|
|
1289
|
+
overrides: config.modelProtocols,
|
|
1290
|
+
observers: {
|
|
1291
|
+
...this.options.onFallback === void 0 ? {} : { onFallback: this.options.onFallback },
|
|
1292
|
+
...this.options.onUnconfigured === void 0 ? {} : { onUnconfigured: this.options.onUnconfigured }
|
|
1293
|
+
}
|
|
1294
|
+
})
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
return this.cache.catalog;
|
|
1298
|
+
}
|
|
1299
|
+
providerInfo(provider) {
|
|
1300
|
+
return { id: provider, name: DISPLAY_NAME };
|
|
1301
|
+
}
|
|
1302
|
+
async listModels(_provider) {
|
|
1303
|
+
const config = this.options.config();
|
|
1304
|
+
const snapshot = await this.catalogOf(config).snapshot();
|
|
1305
|
+
const facts = [...snapshot.facts.values()];
|
|
1306
|
+
const recommended = recommendedIds(facts);
|
|
1307
|
+
return facts.map((fact) => ({ ...fact, recommended: recommended.has(fact.id) })).filter((fact) => isModelEnabled(fact, config.modelVisibility)).map((fact) => ({
|
|
1308
|
+
provider: PROVIDER_ID,
|
|
1309
|
+
id: fact.id,
|
|
1310
|
+
name: fact.name,
|
|
1311
|
+
inputModalities: [...fact.input]
|
|
1312
|
+
}));
|
|
1313
|
+
}
|
|
1314
|
+
async resolveModel(_provider, model, _signal) {
|
|
1315
|
+
const config = this.options.config();
|
|
1316
|
+
const snapshot = await this.catalogOf(config).forModel(model);
|
|
1317
|
+
const facts = snapshot.facts.get(model);
|
|
1318
|
+
if (facts === void 0) throw new LlmError7(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
|
|
1319
|
+
return this.modelInfo(withModelLimit(toPiModel(facts, config.baseURL), config.modelLimits));
|
|
1320
|
+
}
|
|
1321
|
+
/** Describe one model: capacities plus the reasoning levels it really offers. */
|
|
1322
|
+
modelInfo(model) {
|
|
1323
|
+
const reasoning = {};
|
|
1324
|
+
const levels = model.reasoning ? getSupportedThinkingLevels(model) : [];
|
|
1325
|
+
if (levels.length > 0) {
|
|
1326
|
+
const format = model.compat?.thinkingFormat;
|
|
1327
|
+
const fallback = format !== void 0 && DISABLES_THINKING_WHEN_UNSET.has(format) ? levels.includes("high") ? "high" : levels.findLast((level) => level !== "off") : void 0;
|
|
1328
|
+
reasoning.reasoning = {
|
|
1329
|
+
efforts: levels.map((level) => ({
|
|
1330
|
+
id: ReasoningEffortId(level),
|
|
1331
|
+
name: `${level.charAt(0).toUpperCase()}${level.slice(1)}`
|
|
1332
|
+
})),
|
|
1333
|
+
...fallback === void 0 ? {} : { defaultEffort: ReasoningEffortId(fallback) }
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
return {
|
|
1337
|
+
provider: PROVIDER_ID,
|
|
1338
|
+
id: model.id,
|
|
1339
|
+
name: model.name,
|
|
1340
|
+
inputModalities: [...model.input],
|
|
1341
|
+
context: { contextWindow: model.contextWindow },
|
|
1342
|
+
...reasoning
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
/** Validate an explicit effort against the model's own levels, without clamping. */
|
|
1346
|
+
resolveReasoningLevel(model, effort) {
|
|
1347
|
+
if (effort === void 0) return void 0;
|
|
1348
|
+
const supported = getSupportedThinkingLevels(model);
|
|
1349
|
+
if (supported.some((level) => level === effort)) return effort;
|
|
1350
|
+
throw new LlmError7(
|
|
1351
|
+
`opencode-go model "${model.id}" does not support reasoning effort "${effort}"`,
|
|
1352
|
+
"UNSUPPORTED_REASONING_EFFORT"
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
async *stream(options) {
|
|
1356
|
+
var _stack = [];
|
|
1357
|
+
try {
|
|
1358
|
+
if (options.stop !== void 0) {
|
|
1359
|
+
throw new LlmError7("dshopencodego does not support GenerateOptions.stop", "UNSUPPORTED_OPTION");
|
|
1360
|
+
}
|
|
1361
|
+
const config = this.options.config();
|
|
1362
|
+
const snapshot = await this.catalogOf(config).forModel(options.model);
|
|
1363
|
+
const facts = snapshot.facts.get(options.model);
|
|
1364
|
+
if (facts === void 0) throw new LlmError7(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
|
|
1365
|
+
const model = withModelLimit(toPiModel(facts, config.baseURL), config.modelLimits);
|
|
1366
|
+
const outputLimit = config.modelLimits[model.id]?.maxTokens;
|
|
1367
|
+
const maxTokens = outputLimit === null || outputLimit === void 0 ? options.maxTokens : Math.min(options.maxTokens ?? outputLimit, outputLimit);
|
|
1368
|
+
const apiKey = await this.options.resolveApiKey();
|
|
1369
|
+
if (apiKey === void 0 || apiKey.length === 0) {
|
|
1370
|
+
throw new LlmError7("dshopencodego: no credential resolved for the opencode-go route", "MISSING_CREDENTIAL");
|
|
1371
|
+
}
|
|
1372
|
+
const reasoning = this.resolveReasoningLevel(model, options.reasoningEffort);
|
|
1373
|
+
const consumer = new AbortController();
|
|
1374
|
+
const upstream = options.signal === void 0 ? consumer.signal : AbortSignal.any([options.signal, consumer.signal]);
|
|
1375
|
+
const watchdog = __using(_stack, idleWatchdog(upstream, config.streamIdleTimeoutMs, "LLM_STREAM_IDLE_TIMEOUT"));
|
|
1376
|
+
try {
|
|
1377
|
+
const containsImage = options.messages.some((message) => contentHasImage2(message.content));
|
|
1378
|
+
if (containsImage && !model.input.includes("image")) {
|
|
1379
|
+
throw new LlmError7(`opencode-go model "${model.id}" does not support image input`, "UNSUPPORTED_CONTENT");
|
|
1380
|
+
}
|
|
1381
|
+
let imageRequest;
|
|
1382
|
+
if (containsImage) {
|
|
1383
|
+
const access = this.options.imageAccess;
|
|
1384
|
+
const store = access?.resolveAttachments();
|
|
1385
|
+
if (access === void 0 || store === void 0) {
|
|
1386
|
+
throw new LlmError7("dshopencodego image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1387
|
+
}
|
|
1388
|
+
imageRequest = {
|
|
1389
|
+
attachments: store,
|
|
1390
|
+
resolveImageAccess: (ref) => access.resolveImageAccess(store, ref),
|
|
1391
|
+
maxRequestImageBytes: config.maxRequestImageBytes,
|
|
1392
|
+
requestImagePolicy: { maxPixels: config.requestImagePixelBudget, maxBytes: config.requestImageMaxBytes }
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
const context = imageRequest === void 0 ? await toPiContext(options, void 0, this.options.onReplayDegrade) : await toPiContext({ ...options, signal: watchdog.signal }, imageRequest, this.options.onReplayDegrade);
|
|
1396
|
+
const events = snapshot.provider.streamSimple(model, normalizeContext(context), {
|
|
1397
|
+
apiKey,
|
|
1398
|
+
...reasoning === void 0 || reasoning === "off" ? {} : { reasoning },
|
|
1399
|
+
...options.temperature === void 0 ? {} : { temperature: options.temperature },
|
|
1400
|
+
...maxTokens === void 0 ? {} : { maxTokens },
|
|
1401
|
+
...options.sessionId === void 0 ? {} : { sessionId: String(options.sessionId) },
|
|
1402
|
+
signal: watchdog.signal,
|
|
1403
|
+
// The gateway refuses a request without `x-opencode-session`; the
|
|
1404
|
+
// attribution user-agent rides along on the same object.
|
|
1405
|
+
headers: providerHeaders(options.sessionId === void 0 ? void 0 : String(options.sessionId)),
|
|
1406
|
+
// The agent recovery layer owns visible attempts; one adapter call is
|
|
1407
|
+
// one SDK attempt.
|
|
1408
|
+
maxRetries: 0
|
|
1409
|
+
});
|
|
1410
|
+
const iterator = toStreamChunks(events, model.contextWindow, options.signal, model.id)[Symbol.asyncIterator]();
|
|
1411
|
+
let exhausted = false;
|
|
1412
|
+
try {
|
|
1413
|
+
while (true) {
|
|
1414
|
+
const result = await watchdog.next(iterator);
|
|
1415
|
+
if (timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) {
|
|
1416
|
+
throw new LlmError7("opencode-go stream idle timeout", "TIMEOUT");
|
|
1417
|
+
}
|
|
1418
|
+
if (result.done) {
|
|
1419
|
+
exhausted = true;
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
if (result.value.type === "usage") {
|
|
1423
|
+
this.options.onUsage?.({ model: options.model, usage: result.value.usage });
|
|
1424
|
+
}
|
|
1425
|
+
yield result.value;
|
|
1426
|
+
}
|
|
1427
|
+
} finally {
|
|
1428
|
+
if (!exhausted) {
|
|
1429
|
+
consumer.abort("opencode-go stream consumer stopped");
|
|
1430
|
+
try {
|
|
1431
|
+
await iterator.return(void 0);
|
|
1432
|
+
} catch (_abortedSdkTeardown) {
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
if (timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) {
|
|
1438
|
+
throw new LlmError7("opencode-go stream idle timeout", "TIMEOUT", { cause: error });
|
|
1439
|
+
}
|
|
1440
|
+
if (options.signal?.aborted) {
|
|
1441
|
+
throw new LlmError7("opencode-go request aborted by caller", "ABORTED", { cause: error });
|
|
1442
|
+
}
|
|
1443
|
+
throw error;
|
|
1444
|
+
} finally {
|
|
1445
|
+
consumer.abort("opencode-go stream consumer stopped");
|
|
1446
|
+
}
|
|
1447
|
+
} catch (_) {
|
|
1448
|
+
var _error = _, _hasError = true;
|
|
1449
|
+
} finally {
|
|
1450
|
+
__callDispose(_stack, _error, _hasError);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
// src/catalog/contract.ts
|
|
1456
|
+
function parseGoQuota(value) {
|
|
1457
|
+
if (value === null || typeof value !== "object") throw new Error("invalid go quota");
|
|
1458
|
+
const row = value;
|
|
1459
|
+
const usd = row["monthlyUsd"];
|
|
1460
|
+
if (usd !== "unlimited" && (typeof usd !== "number" || !Number.isFinite(usd))) {
|
|
1461
|
+
throw new Error("invalid go quota monthlyUsd");
|
|
1462
|
+
}
|
|
1463
|
+
const requests = row["monthlyRequests"];
|
|
1464
|
+
if (requests !== void 0 && requests !== "unlimited" && (typeof requests !== "number" || !Number.isFinite(requests))) {
|
|
1465
|
+
throw new Error("invalid go quota monthlyRequests");
|
|
1466
|
+
}
|
|
1467
|
+
return {
|
|
1468
|
+
monthlyUsd: usd,
|
|
1469
|
+
...requests === void 0 ? {} : { monthlyRequests: requests }
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
function parseCost(value) {
|
|
1473
|
+
if (value === null || typeof value !== "object") throw new Error("invalid model cost");
|
|
1474
|
+
const row = value;
|
|
1475
|
+
const rate = (key) => {
|
|
1476
|
+
const entry = row[key];
|
|
1477
|
+
if (typeof entry !== "number" || !Number.isFinite(entry) || entry < 0) throw new Error(`invalid model cost "${key}"`);
|
|
1478
|
+
return entry;
|
|
1479
|
+
};
|
|
1480
|
+
return {
|
|
1481
|
+
input: rate("input"),
|
|
1482
|
+
output: rate("output"),
|
|
1483
|
+
...typeof row["cacheRead"] === "number" ? { cacheRead: rate("cacheRead") } : {},
|
|
1484
|
+
...typeof row["cacheWrite"] === "number" ? { cacheWrite: rate("cacheWrite") } : {}
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
function parseModel(value) {
|
|
1488
|
+
if (value === null || typeof value !== "object") throw new Error("invalid catalog model");
|
|
1489
|
+
const row = value;
|
|
1490
|
+
if (typeof row["id"] !== "string" || row["id"].length === 0) throw new Error("invalid catalog model id");
|
|
1491
|
+
return {
|
|
1492
|
+
id: row["id"],
|
|
1493
|
+
name: typeof row["name"] === "string" ? row["name"] : row["id"],
|
|
1494
|
+
...typeof row["contextWindow"] === "number" ? { contextWindow: row["contextWindow"] } : {},
|
|
1495
|
+
...typeof row["maxInputTokens"] === "number" ? { maxInputTokens: row["maxInputTokens"] } : {},
|
|
1496
|
+
...typeof row["maxTokens"] === "number" ? { maxTokens: row["maxTokens"] } : {},
|
|
1497
|
+
...typeof row["deprecated"] === "boolean" ? { deprecated: row["deprecated"] } : {},
|
|
1498
|
+
...typeof row["releaseDate"] === "string" ? { releaseDate: row["releaseDate"] } : {},
|
|
1499
|
+
...row["goQuota"] === void 0 ? {} : { goQuota: parseGoQuota(row["goQuota"]) },
|
|
1500
|
+
...row["cost"] === void 0 ? {} : { cost: parseCost(row["cost"]) },
|
|
1501
|
+
...row["protocolSource"] === "builtin" || row["protocolSource"] === "online" || row["protocolSource"] === "inferred" || row["protocolSource"] === "override" ? { protocolSource: row["protocolSource"] } : {},
|
|
1502
|
+
...typeof row["assumedLimits"] === "boolean" ? { assumedLimits: row["assumedLimits"] } : {},
|
|
1503
|
+
...typeof row["configurationMissing"] === "string" ? { configurationMissing: row["configurationMissing"] } : {}
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function parseCatalogReading(value) {
|
|
1507
|
+
if (value === null || typeof value !== "object") throw new Error("invalid catalog reading");
|
|
1508
|
+
const row = value;
|
|
1509
|
+
const counts = row["counts"];
|
|
1510
|
+
if (!Array.isArray(row["models"]) || typeof row["stale"] !== "boolean" || typeof row["fetchedAtMs"] !== "number" || counts === null || typeof counts !== "object") {
|
|
1511
|
+
throw new Error("invalid catalog reading");
|
|
1512
|
+
}
|
|
1513
|
+
const numbers = counts;
|
|
1514
|
+
const count = (key) => {
|
|
1515
|
+
const entry = numbers[key];
|
|
1516
|
+
if (typeof entry !== "number" || !Number.isFinite(entry) || entry < 0) throw new Error(`invalid catalog count "${key}"`);
|
|
1517
|
+
return entry;
|
|
1518
|
+
};
|
|
1519
|
+
return {
|
|
1520
|
+
models: row["models"].map(parseModel),
|
|
1521
|
+
stale: row["stale"],
|
|
1522
|
+
...typeof row["error"] === "string" ? { error: row["error"] } : {},
|
|
1523
|
+
fetchedAtMs: row["fetchedAtMs"],
|
|
1524
|
+
counts: {
|
|
1525
|
+
total: count("total"),
|
|
1526
|
+
enabled: count("enabled"),
|
|
1527
|
+
deprecated: count("deprecated"),
|
|
1528
|
+
unconfigured: count("unconfigured"),
|
|
1529
|
+
inferred: count("inferred")
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
var codec = {
|
|
1534
|
+
mode: "strict",
|
|
1535
|
+
typeSymbol: "@dan-ai-studio/dshopencodego#CatalogReading",
|
|
1536
|
+
schema: { parse: parseCatalogReading },
|
|
1537
|
+
create: () => ({ parse: parseCatalogReading })
|
|
1538
|
+
};
|
|
1539
|
+
var catalogRemote = {
|
|
1540
|
+
package: "@dan-ai-studio/dshopencodego",
|
|
1541
|
+
descriptors: [
|
|
1542
|
+
{
|
|
1543
|
+
id: "@dan-ai-studio/dshopencodego#opencodeGoCatalog/read",
|
|
1544
|
+
service: "opencodeGoCatalog",
|
|
1545
|
+
namespace: "opencodeGoCatalog",
|
|
1546
|
+
method: "read",
|
|
1547
|
+
invocation: { kind: "direct" },
|
|
1548
|
+
parameters: [],
|
|
1549
|
+
result: codec
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
id: "@dan-ai-studio/dshopencodego#opencodeGoCatalog/refresh",
|
|
1553
|
+
service: "opencodeGoCatalog",
|
|
1554
|
+
namespace: "opencodeGoCatalog",
|
|
1555
|
+
method: "refresh",
|
|
1556
|
+
invocation: { kind: "direct" },
|
|
1557
|
+
parameters: [],
|
|
1558
|
+
result: codec
|
|
1559
|
+
}
|
|
1560
|
+
]
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
// src/usage/contract.ts
|
|
1564
|
+
function parseWindow(value, key) {
|
|
1565
|
+
if (value === null || typeof value !== "object") throw new Error(`invalid usage window "${key}"`);
|
|
1566
|
+
const row = value;
|
|
1567
|
+
if (row["status"] !== "ok" && row["status"] !== "rate-limited" || typeof row["percent"] !== "number" || !Number.isFinite(row["percent"]) || typeof row["resetsAt"] !== "string") {
|
|
1568
|
+
throw new Error(`invalid usage window "${key}"`);
|
|
1569
|
+
}
|
|
1570
|
+
return { status: row["status"], percent: row["percent"], resetsAt: row["resetsAt"] };
|
|
1571
|
+
}
|
|
1572
|
+
function parseUsageWindows(value) {
|
|
1573
|
+
if (value === null || typeof value !== "object") throw new Error("invalid usage reading");
|
|
1574
|
+
const row = value;
|
|
1575
|
+
return {
|
|
1576
|
+
...typeof row["source"] === "string" ? { source: row["source"] } : {},
|
|
1577
|
+
rolling: parseWindow(row["rolling"], "rolling"),
|
|
1578
|
+
weekly: parseWindow(row["weekly"], "weekly"),
|
|
1579
|
+
monthly: parseWindow(row["monthly"], "monthly")
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
function parseTotals(value) {
|
|
1583
|
+
if (value === null || typeof value !== "object") throw new Error("invalid meter totals");
|
|
1584
|
+
const row = value;
|
|
1585
|
+
const count = (key) => {
|
|
1586
|
+
const entry = row[key];
|
|
1587
|
+
if (typeof entry !== "number" || !Number.isFinite(entry) || entry < 0) throw new Error(`invalid meter field "${key}"`);
|
|
1588
|
+
return entry;
|
|
1589
|
+
};
|
|
1590
|
+
return {
|
|
1591
|
+
calls: count("calls"),
|
|
1592
|
+
inputTokens: count("inputTokens"),
|
|
1593
|
+
outputTokens: count("outputTokens"),
|
|
1594
|
+
cacheReadTokens: count("cacheReadTokens"),
|
|
1595
|
+
cacheWriteTokens: count("cacheWriteTokens"),
|
|
1596
|
+
totalTokens: count("totalTokens")
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
function parseGoMeter(value) {
|
|
1600
|
+
if (value === null || typeof value !== "object") throw new Error("invalid meter reading");
|
|
1601
|
+
const row = value;
|
|
1602
|
+
if (typeof row["sinceMs"] !== "number" || typeof row["atMs"] !== "number" || !Array.isArray(row["models"])) {
|
|
1603
|
+
throw new Error("invalid meter reading");
|
|
1604
|
+
}
|
|
1605
|
+
return {
|
|
1606
|
+
sinceMs: row["sinceMs"],
|
|
1607
|
+
atMs: row["atMs"],
|
|
1608
|
+
totals: parseTotals(row["totals"]),
|
|
1609
|
+
models: row["models"].map((entry) => {
|
|
1610
|
+
if (entry === null || typeof entry !== "object") throw new Error("invalid meter model entry");
|
|
1611
|
+
const model = entry["model"];
|
|
1612
|
+
if (typeof model !== "string") throw new Error("invalid meter model entry");
|
|
1613
|
+
return { model, ...parseTotals(entry) };
|
|
1614
|
+
})
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
var codec2 = (typeSymbol, parse) => ({
|
|
1618
|
+
mode: "strict",
|
|
1619
|
+
typeSymbol,
|
|
1620
|
+
schema: { parse },
|
|
1621
|
+
create: () => ({ parse })
|
|
1622
|
+
});
|
|
1623
|
+
var usageRemote = {
|
|
1624
|
+
package: "@dan-ai-studio/dshopencodego",
|
|
1625
|
+
descriptors: [
|
|
1626
|
+
{
|
|
1627
|
+
id: "@dan-ai-studio/dshopencodego#opencodeGoUsage/readWindows",
|
|
1628
|
+
service: "opencodeGoUsage",
|
|
1629
|
+
namespace: "opencodeGoUsage",
|
|
1630
|
+
method: "readWindows",
|
|
1631
|
+
invocation: { kind: "direct" },
|
|
1632
|
+
parameters: [],
|
|
1633
|
+
result: codec2("@dan-ai-studio/dshopencodego#GoUsageWindows", parseUsageWindows)
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
id: "@dan-ai-studio/dshopencodego#opencodeGoUsage/readMeter",
|
|
1637
|
+
service: "opencodeGoUsage",
|
|
1638
|
+
namespace: "opencodeGoUsage",
|
|
1639
|
+
method: "readMeter",
|
|
1640
|
+
invocation: { kind: "direct" },
|
|
1641
|
+
parameters: [],
|
|
1642
|
+
result: codec2("@dan-ai-studio/dshopencodego#GoMeter", parseGoMeter)
|
|
1643
|
+
}
|
|
1644
|
+
]
|
|
1645
|
+
};
|
|
1646
|
+
|
|
1647
|
+
// src/remotes.ts
|
|
1648
|
+
var contribution = {
|
|
1649
|
+
package: usageRemote.package,
|
|
1650
|
+
descriptors: [...usageRemote.descriptors, ...catalogRemote.descriptors]
|
|
1651
|
+
};
|
|
1652
|
+
function registerRemotes(ctx) {
|
|
1653
|
+
ctx.inject(["typert"], (scope) => {
|
|
1654
|
+
scope.effect(() => scope.typert.register({
|
|
1655
|
+
package: contribution.package,
|
|
1656
|
+
face: "host",
|
|
1657
|
+
schemas: [],
|
|
1658
|
+
model: { services: [], events: [], objects: [] },
|
|
1659
|
+
invocations: contribution.descriptors
|
|
1660
|
+
}));
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// src/catalog/service.ts
|
|
1665
|
+
import { RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
1666
|
+
import { LlmError as LlmError8 } from "@deepseek-ai/dsh-llm";
|
|
1667
|
+
|
|
1668
|
+
// src/catalog/reading.ts
|
|
1669
|
+
function catalogReading(snapshot, visibility, listingFailure) {
|
|
1670
|
+
const rows = [
|
|
1671
|
+
...[...snapshot.facts.values()].map((fact) => {
|
|
1672
|
+
const quota = goQuotaFor(fact.id);
|
|
1673
|
+
const priced = fact.cost.input > 0 || fact.cost.output > 0;
|
|
1674
|
+
return {
|
|
1675
|
+
id: fact.id,
|
|
1676
|
+
name: fact.name,
|
|
1677
|
+
contextWindow: fact.contextWindow,
|
|
1678
|
+
maxTokens: fact.maxTokens,
|
|
1679
|
+
...fact.maxInputTokens === void 0 ? {} : { maxInputTokens: fact.maxInputTokens },
|
|
1680
|
+
deprecated: fact.deprecated,
|
|
1681
|
+
...fact.releaseDate === void 0 ? {} : { releaseDate: fact.releaseDate },
|
|
1682
|
+
...quota === void 0 ? {} : { goQuota: quota },
|
|
1683
|
+
...!priced ? {} : {
|
|
1684
|
+
cost: {
|
|
1685
|
+
input: fact.cost.input,
|
|
1686
|
+
output: fact.cost.output,
|
|
1687
|
+
cacheRead: fact.cost.cacheRead,
|
|
1688
|
+
cacheWrite: fact.cost.cacheWrite
|
|
1689
|
+
}
|
|
1690
|
+
},
|
|
1691
|
+
protocolSource: fact.protocolSource,
|
|
1692
|
+
assumedLimits: fact.assumedLimits
|
|
1693
|
+
};
|
|
1694
|
+
}),
|
|
1695
|
+
...[...snapshot.unavailable].map(([id, reason]) => ({ id, name: id, configurationMissing: reason }))
|
|
1696
|
+
];
|
|
1697
|
+
const recommended = recommendedIds(rows);
|
|
1698
|
+
const models = rows.map((row) => ({ ...row, recommended: recommended.has(row.id) }));
|
|
1699
|
+
const enabled = models.filter((model) => isModelEnabled(model, visibility)).length;
|
|
1700
|
+
return {
|
|
1701
|
+
models: sortModels(models),
|
|
1702
|
+
stale: !snapshot.live,
|
|
1703
|
+
...!snapshot.live && listingFailure !== void 0 ? { error: listingFailure } : {},
|
|
1704
|
+
fetchedAtMs: snapshot.fetchedAtMs,
|
|
1705
|
+
counts: {
|
|
1706
|
+
total: models.length,
|
|
1707
|
+
enabled,
|
|
1708
|
+
deprecated: models.filter((model) => model.deprecated === true).length,
|
|
1709
|
+
unconfigured: models.filter((model) => model.configurationMissing !== void 0).length,
|
|
1710
|
+
inferred: models.filter((model) => model.protocolSource === "inferred").length
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// src/catalog/service.ts
|
|
1716
|
+
var OpencodeGoCatalogService = class extends TypertRemoteService {
|
|
1717
|
+
options;
|
|
1718
|
+
constructor(ctx, options) {
|
|
1719
|
+
super(ctx, "opencodeGoCatalog");
|
|
1720
|
+
this.options = options;
|
|
1721
|
+
}
|
|
1722
|
+
/** The cached reading. */
|
|
1723
|
+
read() {
|
|
1724
|
+
return this.project(false);
|
|
1725
|
+
}
|
|
1726
|
+
/** Revalidate both sources, then read. */
|
|
1727
|
+
refresh() {
|
|
1728
|
+
return this.project(true);
|
|
1729
|
+
}
|
|
1730
|
+
async project(force) {
|
|
1731
|
+
const catalog = this.options.catalog();
|
|
1732
|
+
const snapshot = await catalog.snapshot(force);
|
|
1733
|
+
const failure = snapshot.listingFailure;
|
|
1734
|
+
return catalogReading(
|
|
1735
|
+
snapshot,
|
|
1736
|
+
this.options.visibility(),
|
|
1737
|
+
failure === void 0 ? void 0 : failure instanceof LlmError8 || failure instanceof Error ? failure.message : String(failure)
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
// src/usage/meter.ts
|
|
1743
|
+
var EMPTY = {
|
|
1744
|
+
calls: 0,
|
|
1745
|
+
inputTokens: 0,
|
|
1746
|
+
outputTokens: 0,
|
|
1747
|
+
cacheReadTokens: 0,
|
|
1748
|
+
cacheWriteTokens: 0,
|
|
1749
|
+
totalTokens: 0
|
|
1750
|
+
};
|
|
1751
|
+
function add(totals, usage) {
|
|
1752
|
+
const inputTokens = totals.inputTokens + usage.inputTokens;
|
|
1753
|
+
const outputTokens = totals.outputTokens + usage.outputTokens;
|
|
1754
|
+
const cacheReadTokens = totals.cacheReadTokens + (usage.cacheReadTokens ?? 0);
|
|
1755
|
+
const cacheWriteTokens = totals.cacheWriteTokens + (usage.cacheWriteTokens ?? 0);
|
|
1756
|
+
return {
|
|
1757
|
+
calls: totals.calls + 1,
|
|
1758
|
+
inputTokens,
|
|
1759
|
+
outputTokens,
|
|
1760
|
+
cacheReadTokens,
|
|
1761
|
+
cacheWriteTokens,
|
|
1762
|
+
// The four counts are disjoint (uncached input, cached input, output), so
|
|
1763
|
+
// their sum is the billed total. The provider's own `totalTokens` is not
|
|
1764
|
+
// used: it is optional, and mixing the two would double-count a cache read.
|
|
1765
|
+
totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
var UsageMeter = class {
|
|
1769
|
+
sinceMs = Date.now();
|
|
1770
|
+
totals = EMPTY;
|
|
1771
|
+
byModel = /* @__PURE__ */ new Map();
|
|
1772
|
+
/**
|
|
1773
|
+
* Record one completed call.
|
|
1774
|
+
* @param model - the model id the call was made with.
|
|
1775
|
+
* @param usage - the provider's own usage for that call.
|
|
1776
|
+
*/
|
|
1777
|
+
record(model, usage) {
|
|
1778
|
+
this.totals = add(this.totals, usage);
|
|
1779
|
+
this.byModel.set(model, add(this.byModel.get(model) ?? EMPTY, usage));
|
|
1780
|
+
}
|
|
1781
|
+
/** The current reading, busiest model first. */
|
|
1782
|
+
snapshot(now = Date.now()) {
|
|
1783
|
+
return {
|
|
1784
|
+
sinceMs: this.sinceMs,
|
|
1785
|
+
atMs: now,
|
|
1786
|
+
totals: this.totals,
|
|
1787
|
+
models: [...this.byModel.entries()].map(([model, totals]) => ({ model, ...totals })).sort((left, right) => right.totalTokens - left.totalTokens || left.model.localeCompare(right.model))
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1792
|
+
// src/usage/windows.ts
|
|
1793
|
+
import { attributionHeaders as attributionHeaders5, LlmError as LlmError9 } from "@deepseek-ai/dsh-llm";
|
|
1794
|
+
var USAGE_MAX_BYTES = 1024 * 1024;
|
|
1795
|
+
var USAGE_TIMEOUT_MS = 1e4;
|
|
1796
|
+
function parseGoUsage(value) {
|
|
1797
|
+
if (value === null || typeof value !== "object") throw new Error("invalid usage response");
|
|
1798
|
+
const source = value;
|
|
1799
|
+
const result = {};
|
|
1800
|
+
for (const key of ["rolling", "weekly", "monthly"]) {
|
|
1801
|
+
const row = source[key];
|
|
1802
|
+
if (row === null || row === void 0 || row.status !== "ok" && row.status !== "rate-limited" || typeof row.percent !== "number" || !Number.isFinite(row.percent) || row.percent < 0 || typeof row.resetsAt !== "string" || !Number.isFinite(Date.parse(row.resetsAt))) {
|
|
1803
|
+
throw new Error(`invalid usage response: window "${key}" is missing or malformed`);
|
|
1804
|
+
}
|
|
1805
|
+
result[key] = { status: row.status, percent: row.percent, resetsAt: row.resetsAt };
|
|
1806
|
+
}
|
|
1807
|
+
return result;
|
|
1808
|
+
}
|
|
1809
|
+
async function readUsageWindows(options) {
|
|
1810
|
+
if (options.apiKey === void 0 || options.apiKey.length === 0) {
|
|
1811
|
+
throw new LlmError9("dshopencodego: no credential is configured for the opencode-go route", "USAGE_UNAVAILABLE");
|
|
1812
|
+
}
|
|
1813
|
+
const url = `${options.baseURL.replace(/\/+$/, "")}/usage`;
|
|
1814
|
+
const timeout = AbortSignal.timeout(USAGE_TIMEOUT_MS);
|
|
1815
|
+
let response;
|
|
1816
|
+
try {
|
|
1817
|
+
response = await fetch(url, {
|
|
1818
|
+
redirect: "error",
|
|
1819
|
+
headers: {
|
|
1820
|
+
...attributionHeaders5(),
|
|
1821
|
+
accept: "application/json",
|
|
1822
|
+
authorization: `Bearer ${options.apiKey}`
|
|
1823
|
+
},
|
|
1824
|
+
signal: options.signal === void 0 ? timeout : AbortSignal.any([options.signal, timeout])
|
|
1825
|
+
});
|
|
1826
|
+
} catch (error) {
|
|
1827
|
+
throw new LlmError9(`could not reach ${url}`, "USAGE_UNAVAILABLE", { cause: error });
|
|
1828
|
+
}
|
|
1829
|
+
if (!response.ok) {
|
|
1830
|
+
await response.body?.cancel().catch(() => {
|
|
1831
|
+
});
|
|
1832
|
+
throw new LlmError9(`${url} answered HTTP ${response.status}`, "USAGE_UNAVAILABLE");
|
|
1833
|
+
}
|
|
1834
|
+
const body = await readBoundedJson(response, url, USAGE_MAX_BYTES);
|
|
1835
|
+
try {
|
|
1836
|
+
return {
|
|
1837
|
+
...parseGoUsage(body?.usage),
|
|
1838
|
+
...options.source === void 0 ? {} : { source: options.source }
|
|
1839
|
+
};
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
throw new LlmError9(`${url} returned an invalid usage document`, "USAGE_UNAVAILABLE", { cause: error });
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// src/usage/service.ts
|
|
1846
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1847
|
+
import { RemoteError as RemoteError2, TypertRemoteService as TypertRemoteService2 } from "@deepseek-ai/dsh-typert-protocol";
|
|
1848
|
+
import { LlmError as LlmError10 } from "@deepseek-ai/dsh-llm";
|
|
1849
|
+
function usageFailure(error, source) {
|
|
1850
|
+
const missing = error instanceof LlmError10 && error.code === "MISSING_CREDENTIAL";
|
|
1851
|
+
return new RemoteError2(
|
|
1852
|
+
"dshopencodego/usage-unavailable",
|
|
1853
|
+
error instanceof Error ? error.message : "OpenCode Go usage is unavailable",
|
|
1854
|
+
missing ? { retryable: false, retainPrevious: false } : { retryable: true, retainPrevious: true, ...source === void 0 ? {} : { source } },
|
|
1855
|
+
{ cause: error }
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
var OpencodeGoUsageService = class extends TypertRemoteService2 {
|
|
1859
|
+
identity;
|
|
1860
|
+
options;
|
|
1861
|
+
constructor(ctx, options) {
|
|
1862
|
+
super(ctx, "opencodeGoUsage");
|
|
1863
|
+
this.options = options;
|
|
1864
|
+
}
|
|
1865
|
+
/** The account's three windows, or a domain failure the client can render. */
|
|
1866
|
+
async readWindows() {
|
|
1867
|
+
const baseURL = assertBaseURL(this.options.baseURL());
|
|
1868
|
+
let key;
|
|
1869
|
+
try {
|
|
1870
|
+
key = await this.options.resolveApiKey();
|
|
1871
|
+
} catch (error) {
|
|
1872
|
+
this.identity = void 0;
|
|
1873
|
+
throw usageFailure(error, void 0);
|
|
1874
|
+
}
|
|
1875
|
+
if (key === void 0 || key.length === 0) {
|
|
1876
|
+
this.identity = void 0;
|
|
1877
|
+
throw usageFailure(new LlmError10("No OpenCode Go API key is configured", "MISSING_CREDENTIAL"), void 0);
|
|
1878
|
+
}
|
|
1879
|
+
if (this.identity?.baseURL !== baseURL || this.identity.key !== key) {
|
|
1880
|
+
this.identity = { baseURL, key, source: randomUUID2() };
|
|
1881
|
+
}
|
|
1882
|
+
const { source } = this.identity;
|
|
1883
|
+
try {
|
|
1884
|
+
return await readUsageWindows({ baseURL, apiKey: key, source });
|
|
1885
|
+
} catch (error) {
|
|
1886
|
+
throw usageFailure(error, source);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
/** What this process has spent through the route since it started. */
|
|
1890
|
+
readMeter() {
|
|
1891
|
+
return this.options.meter.snapshot();
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
|
|
1895
|
+
// src/index.ts
|
|
1896
|
+
var name = "dshopencodego";
|
|
1897
|
+
var inject = ["llm"];
|
|
1898
|
+
function isLiveConfig(raw) {
|
|
1899
|
+
return typeof raw === "object" && raw !== null && typeof raw.enabled === "object" && typeof raw.enabled?.get === "function";
|
|
1900
|
+
}
|
|
1901
|
+
function apply(ctx, raw) {
|
|
1902
|
+
const live = isLiveConfig(raw) ? raw : void 0;
|
|
1903
|
+
const constant = live === void 0 ? PlainConfig(raw ?? {}) : void 0;
|
|
1904
|
+
const current = () => live === void 0 ? constant : readConfig(live);
|
|
1905
|
+
assertBaseURL(current().baseURL);
|
|
1906
|
+
const resolveApiKey = async () => {
|
|
1907
|
+
const ref = current().apiKeyEnv;
|
|
1908
|
+
const credentials = ctx.get("credentials");
|
|
1909
|
+
const hit = credentials !== void 0 ? (await credentials.resolve(credentialRef(ref)))?.value : launchEnvironmentOf(ctx).get(ref)?.value;
|
|
1910
|
+
if (hit !== void 0 && hit.length > 0) return llm2.assertUsableApiKey(hit, name, ref);
|
|
1911
|
+
throw new LlmError11(
|
|
1912
|
+
`dshopencodego: no credential; the profile resolves ${ref}, which is not set \u2014 store ${ref} through the credentials service (the Web Models page writes it) or export it`,
|
|
1913
|
+
"MISSING_CREDENTIAL"
|
|
1914
|
+
);
|
|
1915
|
+
};
|
|
1916
|
+
const meter = new UsageMeter();
|
|
1917
|
+
const adapter = new OpencodeGoAdapter({
|
|
1918
|
+
config: current,
|
|
1919
|
+
resolveApiKey,
|
|
1920
|
+
imageAccess: {
|
|
1921
|
+
resolveAttachments: () => ctx.get("attachments"),
|
|
1922
|
+
resolveImageAccess: (attachments, ref) => llm2.resolveImageAttachmentAccess(
|
|
1923
|
+
attachments,
|
|
1924
|
+
(hostPath) => ctx.get("fs")?.processPathFromHostPath(hostPath),
|
|
1925
|
+
ref
|
|
1926
|
+
)
|
|
1927
|
+
},
|
|
1928
|
+
onFallback: ({ url, error }) => {
|
|
1929
|
+
ctx.logger.warn(`dshopencodego: could not refresh ${url}; using the last known model data (${String(error)})`);
|
|
1930
|
+
},
|
|
1931
|
+
onUnconfigured: (entries) => {
|
|
1932
|
+
ctx.logger.warn(`dshopencodego: gateway models this build cannot configure: ${entries.map((entry) => `${entry.id} (${entry.reason})`).join(", ")}`);
|
|
1933
|
+
},
|
|
1934
|
+
onReplayDegrade: (reason) => {
|
|
1935
|
+
ctx.logger.warn(`dshopencodego: unusable replay state on assistant history; sending provider-neutral content (${reason})`);
|
|
1936
|
+
},
|
|
1937
|
+
onUsage: ({ model, usage }) => {
|
|
1938
|
+
meter.record(model, usage);
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
registerRemotes(ctx);
|
|
1942
|
+
ctx.plugin(OpencodeGoUsageService, {
|
|
1943
|
+
baseURL: () => current().baseURL,
|
|
1944
|
+
resolveApiKey,
|
|
1945
|
+
meter
|
|
1946
|
+
});
|
|
1947
|
+
ctx.plugin(OpencodeGoCatalogService, {
|
|
1948
|
+
catalog: () => adapter.catalogOf(current()),
|
|
1949
|
+
visibility: () => current().modelVisibility
|
|
1950
|
+
});
|
|
1951
|
+
let registration;
|
|
1952
|
+
let visibilityFacts = JSON.stringify(current().modelVisibility);
|
|
1953
|
+
const applyRoute = (credentialConfigured) => {
|
|
1954
|
+
const enabled = current().enabled;
|
|
1955
|
+
if (enabled && credentialConfigured && registration === void 0) {
|
|
1956
|
+
try {
|
|
1957
|
+
registration = ctx.llm.registerAdapter([PROVIDER_ID], adapter);
|
|
1958
|
+
} catch (error) {
|
|
1959
|
+
ctx.logger.error(
|
|
1960
|
+
`dshopencodego: not registering the "${PROVIDER_ID}" route \u2014 another adapter already owns it; uninstall or disable the other provider first (${String(error)})`
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
} else if ((!enabled || !credentialConfigured) && registration !== void 0) {
|
|
1964
|
+
registration();
|
|
1965
|
+
registration = void 0;
|
|
1966
|
+
if (!enabled) {
|
|
1967
|
+
ctx.logger.info("dshopencodego: disabled by configuration; the route and its models are withdrawn");
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
const syncRoute = () => {
|
|
1972
|
+
const visibility = JSON.stringify(current().modelVisibility);
|
|
1973
|
+
if (visibility !== visibilityFacts) {
|
|
1974
|
+
visibilityFacts = visibility;
|
|
1975
|
+
registration?.replace([PROVIDER_ID]);
|
|
1976
|
+
}
|
|
1977
|
+
const credentials = ctx.get("credentials");
|
|
1978
|
+
const ref = current().apiKeyEnv;
|
|
1979
|
+
if (credentials === void 0) {
|
|
1980
|
+
applyRoute(launchEnvironmentOf(ctx).get(ref)?.value !== void 0);
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
void credentials.describe(credentialRef(ref)).then((info) => {
|
|
1984
|
+
if (ref === current().apiKeyEnv) applyRoute(info.configured);
|
|
1985
|
+
}).catch((error) => {
|
|
1986
|
+
ctx.logger.error(`dshopencodego: credential describe failed; keeping the previous route state (${String(error)})`);
|
|
1987
|
+
});
|
|
1988
|
+
};
|
|
1989
|
+
syncRoute();
|
|
1990
|
+
const undiscover = ctx.llm.registerModelDiscovery(name, async (request) => {
|
|
1991
|
+
if (request.provider !== PROVIDER_ID && !(request.baseURL ?? "").includes("opencode.ai")) {
|
|
1992
|
+
throw new LlmError11(
|
|
1993
|
+
"dshopencodego discovers only OpenCode zen/go endpoints; enter this provider's models by hand",
|
|
1994
|
+
"DISCOVERY_UNSUPPORTED"
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
return discoverCatalogModels(adapter.catalogOf(current()));
|
|
1998
|
+
});
|
|
1999
|
+
ctx.effect(() => () => {
|
|
2000
|
+
registration?.();
|
|
2001
|
+
undiscover();
|
|
2002
|
+
});
|
|
2003
|
+
ctx.on("internal/config", function(_raw, next) {
|
|
2004
|
+
const value = next();
|
|
2005
|
+
if (this === ctx.fiber) assertBaseURL(PlainConfig(value).baseURL);
|
|
2006
|
+
return value;
|
|
2007
|
+
});
|
|
2008
|
+
ctx.on("loader/volatile-update", syncRoute);
|
|
2009
|
+
ctx.inject(["credentials"], (credentialsCtx) => {
|
|
2010
|
+
credentialsCtx.on("credentials/reference-updated", (ref) => {
|
|
2011
|
+
if (ref === current().apiKeyEnv) syncRoute();
|
|
2012
|
+
});
|
|
2013
|
+
syncRoute();
|
|
2014
|
+
});
|
|
2015
|
+
ctx.logger.info(`dshopencodego: route "${PROVIDER_ID}" registered as ${DISPLAY_NAME}`);
|
|
2016
|
+
}
|
|
2017
|
+
export {
|
|
2018
|
+
Config,
|
|
2019
|
+
DEFAULT_API_KEY_ENV,
|
|
2020
|
+
DEFAULT_BASE_URL,
|
|
2021
|
+
DISPLAY_NAME,
|
|
2022
|
+
OpencodeGoAdapter,
|
|
2023
|
+
OpencodeGoCatalog,
|
|
2024
|
+
PROVIDER_ID,
|
|
2025
|
+
PlainConfig,
|
|
2026
|
+
SESSION_HEADER,
|
|
2027
|
+
apply,
|
|
2028
|
+
assertBaseURL,
|
|
2029
|
+
discoverCatalogModels,
|
|
2030
|
+
inject,
|
|
2031
|
+
isModelEnabled,
|
|
2032
|
+
name,
|
|
2033
|
+
opencodeSessionValue,
|
|
2034
|
+
providerHeaders,
|
|
2035
|
+
sortModels
|
|
2036
|
+
};
|