@route_33/opencode 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +1786 -0
- package/dist/tui/index.d.ts +5 -0
- package/dist/tui/index.js +476 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1786 @@
|
|
|
1
|
+
// src/auth/auth-json.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
var PROVIDER_ID = "route_33";
|
|
6
|
+
function usable(value) {
|
|
7
|
+
if (typeof value !== "string") return void 0;
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
return trimmed ? trimmed : void 0;
|
|
10
|
+
}
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function readAuthEntry(env = process.env) {
|
|
15
|
+
try {
|
|
16
|
+
const dataHome = usable(env.XDG_DATA_HOME) ?? path.join(os.homedir(), ".local", "share");
|
|
17
|
+
const authPath = path.join(dataHome, "opencode", "auth.json");
|
|
18
|
+
const parsed = JSON.parse(fs.readFileSync(authPath, "utf8"));
|
|
19
|
+
if (!isRecord(parsed)) return {};
|
|
20
|
+
const entry = parsed[PROVIDER_ID];
|
|
21
|
+
if (!isRecord(entry) || entry.type !== "api") return {};
|
|
22
|
+
const result = {};
|
|
23
|
+
const key = usable(entry.key);
|
|
24
|
+
if (key !== void 0) result.key = key;
|
|
25
|
+
const baseURL = isRecord(entry.metadata) ? usable(entry.metadata.baseURL) : void 0;
|
|
26
|
+
if (baseURL !== void 0) result.baseURL = baseURL;
|
|
27
|
+
return result;
|
|
28
|
+
} catch {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/auth/workspace-key-file.ts
|
|
34
|
+
import * as fs2 from "fs";
|
|
35
|
+
import * as path3 from "path";
|
|
36
|
+
|
|
37
|
+
// src/shared/default-base-url.ts
|
|
38
|
+
var DEFAULT_BASE_URL = "https://api.route33.ai/v1";
|
|
39
|
+
|
|
40
|
+
// src/shared/default-catalog-ttl-ms.ts
|
|
41
|
+
var DEFAULT_CATALOG_TTL_MS = 36e5;
|
|
42
|
+
|
|
43
|
+
// src/shared/env-api-key.ts
|
|
44
|
+
var ENV_API_KEY = "ROUTE33_API_KEY";
|
|
45
|
+
|
|
46
|
+
// src/shared/env-base-url.ts
|
|
47
|
+
var ENV_BASE_URL = "ROUTE33_BASE_URL";
|
|
48
|
+
|
|
49
|
+
// src/shared/env-cache-dir.ts
|
|
50
|
+
var ENV_CACHE_DIR = "ROUTE33_CACHE_DIR";
|
|
51
|
+
|
|
52
|
+
// src/shared/env-catalog-ttl-ms.ts
|
|
53
|
+
var ENV_CATALOG_TTL_MS = "ROUTE33_CATALOG_TTL_MS";
|
|
54
|
+
|
|
55
|
+
// src/shared/env-disable-id-shim.ts
|
|
56
|
+
var ENV_DISABLE_ID_SHIM = "ROUTE33_DISABLE_ID_SHIM";
|
|
57
|
+
|
|
58
|
+
// src/shared/env-discovery.ts
|
|
59
|
+
var ENV_DISCOVERY = "ROUTE33_DISCOVERY";
|
|
60
|
+
|
|
61
|
+
// src/shared/env-enrich-caps.ts
|
|
62
|
+
var ENV_ENRICH_CAPS = "ROUTE33_ENRICH_CAPS";
|
|
63
|
+
|
|
64
|
+
// src/shared/env-strict.ts
|
|
65
|
+
var ENV_STRICT = "ROUTE33_STRICT";
|
|
66
|
+
|
|
67
|
+
// src/shared/logger.ts
|
|
68
|
+
var logger = {
|
|
69
|
+
info: (...args) => {
|
|
70
|
+
console.log("[route_33]", ...args);
|
|
71
|
+
},
|
|
72
|
+
warn: (...args) => {
|
|
73
|
+
console.warn("[route_33]", ...args);
|
|
74
|
+
},
|
|
75
|
+
error: (...args) => {
|
|
76
|
+
console.error("[route_33]", ...args);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/shared/models-dev-ttl-ms.ts
|
|
81
|
+
var MODELS_DEV_TTL_MS = 864e5;
|
|
82
|
+
|
|
83
|
+
// src/shared/resolve-cache-dir.ts
|
|
84
|
+
import * as os2 from "os";
|
|
85
|
+
import * as path2 from "path";
|
|
86
|
+
function resolveCacheDir(env = process.env) {
|
|
87
|
+
const override = env[ENV_CACHE_DIR];
|
|
88
|
+
if (override) return override;
|
|
89
|
+
const xdgCacheHome = env.XDG_CACHE_HOME;
|
|
90
|
+
const base = xdgCacheHome ?? path2.join(os2.homedir(), ".cache");
|
|
91
|
+
return path2.join(base, "opencode-route33");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/auth/workspace-key-file.ts
|
|
95
|
+
var WORKSPACE_KEY_RELATIVE_PATH = path3.join(".opencode", "route33.key");
|
|
96
|
+
var WORKSPACE_GITIGNORE_RELATIVE_PATH = path3.join(".opencode", ".gitignore");
|
|
97
|
+
var GITIGNORE_ENTRIES = ["route33.key", "tui.json"];
|
|
98
|
+
function usable2(value) {
|
|
99
|
+
if (typeof value !== "string") return void 0;
|
|
100
|
+
const trimmed = value.trim();
|
|
101
|
+
return trimmed ? trimmed : void 0;
|
|
102
|
+
}
|
|
103
|
+
function isRecord2(value) {
|
|
104
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
105
|
+
}
|
|
106
|
+
function readWorkspaceKey(worktree) {
|
|
107
|
+
if (worktree === void 0) return void 0;
|
|
108
|
+
try {
|
|
109
|
+
const raw = fs2.readFileSync(
|
|
110
|
+
path3.join(worktree, WORKSPACE_KEY_RELATIVE_PATH),
|
|
111
|
+
"utf8"
|
|
112
|
+
);
|
|
113
|
+
const parsed = JSON.parse(raw);
|
|
114
|
+
if (!isRecord2(parsed) || parsed.type !== "api") return void 0;
|
|
115
|
+
return usable2(parsed.key);
|
|
116
|
+
} catch {
|
|
117
|
+
return void 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function ensureWorkspaceKeyGitignore(worktree) {
|
|
121
|
+
if (worktree === void 0 || worktree.trim() === "") return;
|
|
122
|
+
if (path3.resolve(worktree) === "/") return;
|
|
123
|
+
const gitignorePath = path3.join(worktree, WORKSPACE_GITIGNORE_RELATIVE_PATH);
|
|
124
|
+
try {
|
|
125
|
+
fs2.mkdirSync(path3.dirname(gitignorePath), { recursive: true });
|
|
126
|
+
let content = "";
|
|
127
|
+
try {
|
|
128
|
+
content = fs2.readFileSync(gitignorePath, "utf8");
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
const lines = content.split("\n");
|
|
132
|
+
const missing = GITIGNORE_ENTRIES.filter(
|
|
133
|
+
(entry) => !lines.some(
|
|
134
|
+
(line) => line.trim() === entry || line.trim() === `/${entry}`
|
|
135
|
+
)
|
|
136
|
+
);
|
|
137
|
+
if (missing.length === 0) return;
|
|
138
|
+
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
139
|
+
fs2.writeFileSync(gitignorePath, `${content}${prefix}${missing.join("\n")}
|
|
140
|
+
`);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
logger.warn(
|
|
143
|
+
`opencode-route33: could not update ${gitignorePath} (${String(error)}) \u2014 verify route33.key and tui.json are git-ignored before committing`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/auth/resolve-base-url.ts
|
|
149
|
+
var UNRESOLVED_TEMPLATE_MARKER = "{env:";
|
|
150
|
+
function usable3(value) {
|
|
151
|
+
const trimmed = value?.trim();
|
|
152
|
+
return trimmed ? trimmed : void 0;
|
|
153
|
+
}
|
|
154
|
+
function urlFromConfig(cfg) {
|
|
155
|
+
const literal = usable3(cfg?.provider?.route_33?.options?.baseURL);
|
|
156
|
+
if (literal !== void 0 && !literal.includes(UNRESOLVED_TEMPLATE_MARKER))
|
|
157
|
+
return literal;
|
|
158
|
+
return void 0;
|
|
159
|
+
}
|
|
160
|
+
function resolveBaseUrl(cfg, env = process.env) {
|
|
161
|
+
return usable3(env[ENV_BASE_URL]) ?? urlFromConfig(cfg) ?? readAuthEntry(env).baseURL ?? DEFAULT_BASE_URL;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/auth/api-key-method.ts
|
|
165
|
+
function buildApiKeyMethod() {
|
|
166
|
+
return {
|
|
167
|
+
type: "api",
|
|
168
|
+
label: "route33.ai API key (sk-r33-\u2026)",
|
|
169
|
+
prompts: [
|
|
170
|
+
{
|
|
171
|
+
type: "text",
|
|
172
|
+
key: "key",
|
|
173
|
+
message: "route33.ai API key",
|
|
174
|
+
placeholder: "sk-r33-\u2026",
|
|
175
|
+
validate: (value) => value.trim() === "" ? "API key is required" : void 0
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
type: "text",
|
|
179
|
+
key: "baseURL",
|
|
180
|
+
message: "Gateway URL (Enter for default)",
|
|
181
|
+
placeholder: DEFAULT_BASE_URL
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
authorize: async (inputs) => {
|
|
185
|
+
const key = inputs?.key?.trim();
|
|
186
|
+
if (!key) return { type: "failed" };
|
|
187
|
+
const url = inputs?.baseURL?.trim();
|
|
188
|
+
const result = {
|
|
189
|
+
type: "success",
|
|
190
|
+
key
|
|
191
|
+
};
|
|
192
|
+
if (url && url !== DEFAULT_BASE_URL) result.metadata = { baseURL: url };
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/auth/resolve-key.ts
|
|
199
|
+
var UNRESOLVED_TEMPLATE_MARKER2 = "{env:";
|
|
200
|
+
function usable4(value) {
|
|
201
|
+
const trimmed = value?.trim();
|
|
202
|
+
return trimmed ? trimmed : void 0;
|
|
203
|
+
}
|
|
204
|
+
function keyFromConfig(cfg) {
|
|
205
|
+
const literal = usable4(cfg?.provider?.route_33?.options?.apiKey);
|
|
206
|
+
if (literal !== void 0 && !literal.includes(UNRESOLVED_TEMPLATE_MARKER2))
|
|
207
|
+
return literal;
|
|
208
|
+
return void 0;
|
|
209
|
+
}
|
|
210
|
+
function resolveKey(cfg, env = process.env, worktree) {
|
|
211
|
+
return usable4(env[ENV_API_KEY]) ?? keyFromConfig(cfg) ?? readWorkspaceKey(worktree) ?? readAuthEntry(env).key;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/auth/route33-auth.ts
|
|
215
|
+
var ROUTE33_PROVIDER_ID = "route_33";
|
|
216
|
+
function buildRoute33Auth(env = process.env, worktree) {
|
|
217
|
+
return {
|
|
218
|
+
provider: ROUTE33_PROVIDER_ID,
|
|
219
|
+
methods: [buildApiKeyMethod()],
|
|
220
|
+
loader: async (_auth, _provider) => {
|
|
221
|
+
try {
|
|
222
|
+
const key = resolveKey(void 0, env, worktree);
|
|
223
|
+
return key ? { apiKey: key, headers: { Authorization: `Bearer ${key}` } } : {};
|
|
224
|
+
} catch {
|
|
225
|
+
return {};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/bootstrap/ensure-tui-registration.ts
|
|
232
|
+
import * as path7 from "path";
|
|
233
|
+
|
|
234
|
+
// src/bootstrap/global-context.ts
|
|
235
|
+
import * as os3 from "os";
|
|
236
|
+
import * as path4 from "path";
|
|
237
|
+
import { fileURLToPath } from "url";
|
|
238
|
+
function isGlobalContext(worktree, moduleUrl) {
|
|
239
|
+
try {
|
|
240
|
+
if (moduleUrl === void 0) return false;
|
|
241
|
+
if (worktree === void 0) return true;
|
|
242
|
+
const root = path4.resolve(worktree);
|
|
243
|
+
if (root === "/") return true;
|
|
244
|
+
const modulePath = fileURLToPath(moduleUrl);
|
|
245
|
+
return !modulePath.startsWith(root + path4.sep);
|
|
246
|
+
} catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function usable5(value) {
|
|
251
|
+
const trimmed = value?.trim();
|
|
252
|
+
return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
|
|
253
|
+
}
|
|
254
|
+
function globalTuiJsonPath(env) {
|
|
255
|
+
const configHome = usable5(env.XDG_CONFIG_HOME) ?? path4.join(os3.homedir(), ".config");
|
|
256
|
+
return path4.join(configHome, "opencode", "tui.json");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/bootstrap/tui-json-upsert.ts
|
|
260
|
+
import * as fs4 from "fs";
|
|
261
|
+
import * as path6 from "path";
|
|
262
|
+
import {
|
|
263
|
+
applyEdits,
|
|
264
|
+
modify,
|
|
265
|
+
parse as parseJsonc
|
|
266
|
+
} from "jsonc-parser";
|
|
267
|
+
|
|
268
|
+
// src/bootstrap/tui-spec-resolution.ts
|
|
269
|
+
import * as fs3 from "fs";
|
|
270
|
+
import * as os4 from "os";
|
|
271
|
+
import * as path5 from "path";
|
|
272
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
273
|
+
var NPM_TUI_SPEC = "@route_33/opencode/tui";
|
|
274
|
+
var OLD_NPM_TUI_SPEC = "opencode-route33/tui";
|
|
275
|
+
var TUI_BUNDLE_NAME = "route33-tui.js";
|
|
276
|
+
var TUI_SCHEMA = "https://opencode.ai/tui.json";
|
|
277
|
+
var SERVER_BUNDLE_NAME = "route33.js";
|
|
278
|
+
var DIST_TUI_ENTRY_SUFFIX = "/opencode-route33/dist/tui/index.js";
|
|
279
|
+
var SCOPED_DIST_TUI_ENTRY_SUFFIX = "/@route_33/opencode/dist/tui/index.js";
|
|
280
|
+
function fileExists(candidate) {
|
|
281
|
+
try {
|
|
282
|
+
return fs3.statSync(candidate).isFile();
|
|
283
|
+
} catch {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function specFromModuleUrl(moduleUrl) {
|
|
288
|
+
try {
|
|
289
|
+
if (moduleUrl === void 0) return void 0;
|
|
290
|
+
const selfPath = fileURLToPath2(moduleUrl);
|
|
291
|
+
const dir = path5.dirname(selfPath);
|
|
292
|
+
if (path5.basename(selfPath) === SERVER_BUNDLE_NAME) {
|
|
293
|
+
const sibling = path5.join(dir, TUI_BUNDLE_NAME);
|
|
294
|
+
return fileExists(sibling) ? sibling : void 0;
|
|
295
|
+
}
|
|
296
|
+
if (path5.basename(selfPath) === "index.js" && path5.basename(dir) === "dist") {
|
|
297
|
+
const tuiEntry = path5.join(dir, "tui", "index.js");
|
|
298
|
+
return fileExists(tuiEntry) ? tuiEntry : void 0;
|
|
299
|
+
}
|
|
300
|
+
return void 0;
|
|
301
|
+
} catch {
|
|
302
|
+
return void 0;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function resolveTuiSpec(worktree, env, moduleUrl) {
|
|
306
|
+
const fromModule = specFromModuleUrl(moduleUrl);
|
|
307
|
+
if (fromModule !== void 0) return fromModule;
|
|
308
|
+
if (worktree !== void 0 && worktree.trim() !== "" && path5.resolve(worktree) !== "/") {
|
|
309
|
+
const local = path5.join(worktree, ".opencode", "plugins", TUI_BUNDLE_NAME);
|
|
310
|
+
if (fileExists(local)) return local;
|
|
311
|
+
}
|
|
312
|
+
const configHome = env.XDG_CONFIG_HOME?.trim() || path5.join(os4.homedir(), ".config");
|
|
313
|
+
const global = path5.join(configHome, "opencode", "plugins", TUI_BUNDLE_NAME);
|
|
314
|
+
if (fileExists(global)) return global;
|
|
315
|
+
return NPM_TUI_SPEC;
|
|
316
|
+
}
|
|
317
|
+
function isOurEntry(entry) {
|
|
318
|
+
return typeof entry === "string" && (entry === NPM_TUI_SPEC || entry === OLD_NPM_TUI_SPEC || entry.endsWith(`/${TUI_BUNDLE_NAME}`) || entry.endsWith(DIST_TUI_ENTRY_SUFFIX) || entry.endsWith(SCOPED_DIST_TUI_ENTRY_SUFFIX));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/bootstrap/tui-json-upsert.ts
|
|
322
|
+
function isRecord3(value) {
|
|
323
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
324
|
+
}
|
|
325
|
+
function fileExists2(candidate) {
|
|
326
|
+
try {
|
|
327
|
+
return fs4.statSync(candidate).isFile();
|
|
328
|
+
} catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function upsertTuiSpec(file, spec) {
|
|
333
|
+
try {
|
|
334
|
+
if (!fileExists2(file)) {
|
|
335
|
+
fs4.mkdirSync(path6.dirname(file), { recursive: true });
|
|
336
|
+
fs4.writeFileSync(
|
|
337
|
+
file,
|
|
338
|
+
JSON.stringify({ $schema: TUI_SCHEMA, plugin: [spec] }, null, 2) + "\n",
|
|
339
|
+
"utf8"
|
|
340
|
+
);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const text = fs4.readFileSync(file, "utf8");
|
|
344
|
+
const errors = [];
|
|
345
|
+
const parsed = parseJsonc(text, errors, {
|
|
346
|
+
allowTrailingComma: true,
|
|
347
|
+
allowEmptyContent: true
|
|
348
|
+
});
|
|
349
|
+
if (!isRecord3(parsed)) {
|
|
350
|
+
logger.warn(
|
|
351
|
+
`tui.json at ${file} did not parse to an object \u2014 skipping TUI registration`
|
|
352
|
+
);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (parsed.plugin !== void 0 && !Array.isArray(parsed.plugin)) {
|
|
356
|
+
logger.warn(
|
|
357
|
+
`tui.json at ${file} has a non-array "plugin" key \u2014 skipping TUI registration`
|
|
358
|
+
);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const entries = Array.isArray(parsed.plugin) ? parsed.plugin : [];
|
|
362
|
+
const ourIndices = entries.flatMap(
|
|
363
|
+
(entry, index) => isOurEntry(entry) ? [index] : []
|
|
364
|
+
);
|
|
365
|
+
const firstOurs = ourIndices[0];
|
|
366
|
+
if (ourIndices.length === 1 && firstOurs !== void 0 && entries[firstOurs] === spec)
|
|
367
|
+
return;
|
|
368
|
+
const formattingOptions = { insertSpaces: true, tabSize: 2 };
|
|
369
|
+
let next = text;
|
|
370
|
+
if (!Array.isArray(parsed.plugin)) {
|
|
371
|
+
next = applyEdits(
|
|
372
|
+
next,
|
|
373
|
+
modify(next, ["plugin"], [spec], { formattingOptions })
|
|
374
|
+
);
|
|
375
|
+
} else if (firstOurs === void 0) {
|
|
376
|
+
next = applyEdits(
|
|
377
|
+
next,
|
|
378
|
+
modify(next, ["plugin", -1], spec, {
|
|
379
|
+
formattingOptions,
|
|
380
|
+
isArrayInsertion: true
|
|
381
|
+
})
|
|
382
|
+
);
|
|
383
|
+
} else {
|
|
384
|
+
if (entries[firstOurs] !== spec) {
|
|
385
|
+
next = applyEdits(
|
|
386
|
+
next,
|
|
387
|
+
modify(next, ["plugin", firstOurs], spec, { formattingOptions })
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
for (const index of ourIndices.slice(1).sort((a, b) => b - a)) {
|
|
391
|
+
next = applyEdits(
|
|
392
|
+
next,
|
|
393
|
+
modify(next, ["plugin", index], void 0, { formattingOptions })
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (next !== text) fs4.writeFileSync(file, next, "utf8");
|
|
398
|
+
} catch (error) {
|
|
399
|
+
logger.warn(
|
|
400
|
+
`could not register the route33 TUI plugin in tui.json: ${String(error)}`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/bootstrap/ensure-tui-registration.ts
|
|
406
|
+
function ensureTuiRegistration(deps) {
|
|
407
|
+
try {
|
|
408
|
+
const { worktree, env } = deps;
|
|
409
|
+
if (env.ROUTE33_TUI_PROMPT === "0") return;
|
|
410
|
+
const usableWorktree = worktree !== void 0 && worktree.trim() !== "" && path7.resolve(worktree) !== "/" ? worktree : void 0;
|
|
411
|
+
const spec = resolveTuiSpec(usableWorktree, env, deps.moduleUrl);
|
|
412
|
+
if (usableWorktree !== void 0) {
|
|
413
|
+
upsertTuiSpec(path7.join(usableWorktree, ".opencode", "tui.json"), spec);
|
|
414
|
+
}
|
|
415
|
+
if (isGlobalContext(worktree, deps.moduleUrl)) {
|
|
416
|
+
upsertTuiSpec(globalTuiJsonPath(env), spec);
|
|
417
|
+
}
|
|
418
|
+
} catch (error) {
|
|
419
|
+
logger.warn(
|
|
420
|
+
`could not register the route33 TUI plugin in tui.json: ${String(error)}`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/config/live-auth-fetch.ts
|
|
426
|
+
function buildLiveAuthFetch(resolveKeyNow, innerFetch) {
|
|
427
|
+
return (input, init) => {
|
|
428
|
+
const key = resolveKeyNow();
|
|
429
|
+
if (key === void 0) return innerFetch(input, init);
|
|
430
|
+
const headers = new Headers(init?.headers);
|
|
431
|
+
headers.set("Authorization", `Bearer ${key}`);
|
|
432
|
+
return innerFetch(input, { ...init, headers });
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/config/openai-chat-compat-fetch.ts
|
|
437
|
+
function rewriteOpenAiBody(body) {
|
|
438
|
+
try {
|
|
439
|
+
const parsed = JSON.parse(body);
|
|
440
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
441
|
+
return void 0;
|
|
442
|
+
const request = parsed;
|
|
443
|
+
const model = request["model"];
|
|
444
|
+
if (typeof model !== "string" || !model.startsWith("openai/"))
|
|
445
|
+
return void 0;
|
|
446
|
+
const tools = request["tools"];
|
|
447
|
+
const dropEffort = "reasoning_effort" in request && Array.isArray(tools) && tools.length > 0;
|
|
448
|
+
const renameMaxTokens = "max_tokens" in request;
|
|
449
|
+
if (!renameMaxTokens && !dropEffort) return void 0;
|
|
450
|
+
const rewritten = { ...request };
|
|
451
|
+
if (dropEffort) delete rewritten["reasoning_effort"];
|
|
452
|
+
if (renameMaxTokens) {
|
|
453
|
+
if (!("max_completion_tokens" in rewritten))
|
|
454
|
+
rewritten["max_completion_tokens"] = rewritten["max_tokens"];
|
|
455
|
+
delete rewritten["max_tokens"];
|
|
456
|
+
}
|
|
457
|
+
return JSON.stringify(rewritten);
|
|
458
|
+
} catch {
|
|
459
|
+
return void 0;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function buildOpenAiChatCompatFetch(fetchImpl) {
|
|
463
|
+
return (input, init) => {
|
|
464
|
+
const body = init?.body;
|
|
465
|
+
const rewritten = typeof body === "string" ? rewriteOpenAiBody(body) : void 0;
|
|
466
|
+
const finalInit = rewritten === void 0 ? init : { ...init, body: rewritten };
|
|
467
|
+
return (fetchImpl ?? globalThis.fetch)(input, finalInit);
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/config/build-options.ts
|
|
472
|
+
function buildOptions(baseURL, apiKey, resolveKeyNow = () => void 0) {
|
|
473
|
+
const options = { baseURL };
|
|
474
|
+
options.fetch = buildLiveAuthFetch(
|
|
475
|
+
resolveKeyNow,
|
|
476
|
+
buildOpenAiChatCompatFetch()
|
|
477
|
+
);
|
|
478
|
+
if (apiKey !== void 0) {
|
|
479
|
+
options.apiKey = apiKey;
|
|
480
|
+
options.headers = { Authorization: `Bearer ${apiKey}` };
|
|
481
|
+
}
|
|
482
|
+
return options;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/config/native-sdk-by-vendor.ts
|
|
486
|
+
var NATIVE_SDK_BY_VENDOR = {
|
|
487
|
+
anthropic: "@ai-sdk/anthropic",
|
|
488
|
+
mistralai: "@ai-sdk/mistral",
|
|
489
|
+
openai: "@ai-sdk/openai"
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/config/to-model-patch.ts
|
|
493
|
+
function cloneCost(cost) {
|
|
494
|
+
const copy = { input: cost.input, output: cost.output };
|
|
495
|
+
if (cost.cache_read !== void 0) copy.cache_read = cost.cache_read;
|
|
496
|
+
if (cost.cache_write !== void 0) copy.cache_write = cost.cache_write;
|
|
497
|
+
if (cost.context_over_200k !== void 0)
|
|
498
|
+
copy.context_over_200k = { ...cost.context_over_200k };
|
|
499
|
+
return copy;
|
|
500
|
+
}
|
|
501
|
+
function cloneVariants(variants) {
|
|
502
|
+
return Object.fromEntries(
|
|
503
|
+
Object.entries(variants).map(([name, options]) => [name, { ...options }])
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
function toModelPatch(meta, routingSlug, forceReasoning, nativeNpm) {
|
|
507
|
+
const entry = { name: meta.name };
|
|
508
|
+
if (routingSlug !== void 0) entry.id = routingSlug;
|
|
509
|
+
if (nativeNpm !== void 0) entry.provider = { npm: nativeNpm };
|
|
510
|
+
const caps = meta.capabilities;
|
|
511
|
+
if (caps.attachment !== void 0) entry.attachment = caps.attachment;
|
|
512
|
+
if (caps.reasoning !== void 0) entry.reasoning = caps.reasoning;
|
|
513
|
+
if (forceReasoning) entry.reasoning = true;
|
|
514
|
+
if (caps.temperature !== void 0) entry.temperature = caps.temperature;
|
|
515
|
+
if (caps.tool_call !== void 0) entry.tool_call = caps.tool_call;
|
|
516
|
+
if (meta.cost !== void 0) entry.cost = cloneCost(meta.cost);
|
|
517
|
+
if (meta.limit !== void 0)
|
|
518
|
+
entry.limit = { context: meta.limit.context, output: meta.limit.output };
|
|
519
|
+
if (meta.variants !== void 0)
|
|
520
|
+
entry.variants = cloneVariants(meta.variants);
|
|
521
|
+
return entry;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/config/build-provider-blocks.ts
|
|
525
|
+
var PROVIDER_DISPLAY_NAME = "route_33";
|
|
526
|
+
function vendorOf(slug2) {
|
|
527
|
+
return slug2.slice(0, slug2.indexOf("/"));
|
|
528
|
+
}
|
|
529
|
+
function buildProviderBlocks(enriched, options) {
|
|
530
|
+
const { apiKey, baseURL, aliasTargets } = options;
|
|
531
|
+
const resolveKeyNow = options.resolveKeyNow ?? (() => void 0);
|
|
532
|
+
const route33Models = {};
|
|
533
|
+
for (const slug2 of Object.keys(enriched).sort()) {
|
|
534
|
+
const meta = enriched[slug2];
|
|
535
|
+
if (meta === void 0) continue;
|
|
536
|
+
const aliasTarget = aliasTargets?.[slug2];
|
|
537
|
+
const vendor = vendorOf(slug2);
|
|
538
|
+
const nativeNpm = NATIVE_SDK_BY_VENDOR[vendor];
|
|
539
|
+
route33Models[slug2] = toModelPatch(
|
|
540
|
+
meta,
|
|
541
|
+
aliasTarget,
|
|
542
|
+
vendor === "anthropic",
|
|
543
|
+
nativeNpm
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
route_33: {
|
|
548
|
+
npm: "@ai-sdk/openai-compatible",
|
|
549
|
+
name: PROVIDER_DISPLAY_NAME,
|
|
550
|
+
options: buildOptions(baseURL, apiKey, resolveKeyNow),
|
|
551
|
+
models: route33Models
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/catalog/catalog-cache/cache-state.ts
|
|
557
|
+
var memory = /* @__PURE__ */ new Map();
|
|
558
|
+
|
|
559
|
+
// src/catalog/catalog-cache/hash-key.ts
|
|
560
|
+
import { createHash } from "crypto";
|
|
561
|
+
function hashKey(key) {
|
|
562
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 12);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/catalog/catalog-cache/read-disk.ts
|
|
566
|
+
import * as fs5 from "fs";
|
|
567
|
+
|
|
568
|
+
// src/catalog/catalog-cache/cache-file-path.ts
|
|
569
|
+
import * as path8 from "path";
|
|
570
|
+
function cacheFilePath(keyHash, env) {
|
|
571
|
+
return path8.join(resolveCacheDir(env), `catalog-${keyHash}.json`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/catalog/catalog-cache/read-disk.ts
|
|
575
|
+
function isRecord4(value) {
|
|
576
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
577
|
+
}
|
|
578
|
+
function readDisk(keyHash, env) {
|
|
579
|
+
try {
|
|
580
|
+
const parsed = JSON.parse(
|
|
581
|
+
fs5.readFileSync(cacheFilePath(keyHash, env), "utf8")
|
|
582
|
+
);
|
|
583
|
+
if (!isRecord4(parsed) || typeof parsed.fetchedAt !== "number" || !isRecord4(parsed.models))
|
|
584
|
+
return void 0;
|
|
585
|
+
return {
|
|
586
|
+
fetchedAt: parsed.fetchedAt,
|
|
587
|
+
keyHash,
|
|
588
|
+
models: parsed.models
|
|
589
|
+
};
|
|
590
|
+
} catch {
|
|
591
|
+
return void 0;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/catalog/catalog-cache/read-fresh-catalog.ts
|
|
596
|
+
function catalogTtlMs(env) {
|
|
597
|
+
const raw = Number(env[ENV_CATALOG_TTL_MS]);
|
|
598
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_CATALOG_TTL_MS;
|
|
599
|
+
}
|
|
600
|
+
function isFresh(record, now, ttl) {
|
|
601
|
+
return now - record.fetchedAt <= ttl;
|
|
602
|
+
}
|
|
603
|
+
function readFreshCatalog(opts) {
|
|
604
|
+
const { key, env = process.env, now = Date.now() } = opts;
|
|
605
|
+
const keyHash = hashKey(key);
|
|
606
|
+
const ttl = catalogTtlMs(env);
|
|
607
|
+
const inMemory = memory.get(keyHash);
|
|
608
|
+
if (inMemory && isFresh(inMemory, now, ttl)) return inMemory.models;
|
|
609
|
+
const onDisk = readDisk(keyHash, env);
|
|
610
|
+
if (onDisk && isFresh(onDisk, now, ttl)) {
|
|
611
|
+
memory.set(keyHash, onDisk);
|
|
612
|
+
return onDisk.models;
|
|
613
|
+
}
|
|
614
|
+
return void 0;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/catalog/catalog-cache/read-stale-catalog.ts
|
|
618
|
+
function readStaleCatalog(opts) {
|
|
619
|
+
const { key, env = process.env } = opts;
|
|
620
|
+
const keyHash = hashKey(key);
|
|
621
|
+
return (memory.get(keyHash) ?? readDisk(keyHash, env))?.models;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/catalog/catalog-cache/write-catalog.ts
|
|
625
|
+
import * as fs6 from "fs";
|
|
626
|
+
function writeCatalog(opts) {
|
|
627
|
+
const { key, models, env = process.env, now = Date.now() } = opts;
|
|
628
|
+
const keyHash = hashKey(key);
|
|
629
|
+
const record = { fetchedAt: now, keyHash, models };
|
|
630
|
+
memory.set(keyHash, record);
|
|
631
|
+
try {
|
|
632
|
+
fs6.mkdirSync(resolveCacheDir(env), { recursive: true });
|
|
633
|
+
fs6.writeFileSync(cacheFilePath(keyHash, env), JSON.stringify(record));
|
|
634
|
+
} catch (error) {
|
|
635
|
+
logger.warn(
|
|
636
|
+
`could not persist catalog cache: ${error instanceof Error ? error.message : String(error)}`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/catalog/catalog-fetch-error.ts
|
|
642
|
+
var CatalogFetchError = class extends Error {
|
|
643
|
+
reason;
|
|
644
|
+
statusCode;
|
|
645
|
+
constructor(reason, message, statusCode) {
|
|
646
|
+
super(`catalog fetch failed (${reason}): ${message}`);
|
|
647
|
+
this.name = "CatalogFetchError";
|
|
648
|
+
this.reason = reason;
|
|
649
|
+
this.statusCode = statusCode;
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// src/catalog/default-fetch-timeout-ms.ts
|
|
654
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 5e3;
|
|
655
|
+
|
|
656
|
+
// src/catalog/extract-pricing.ts
|
|
657
|
+
function isRecord5(value) {
|
|
658
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
659
|
+
}
|
|
660
|
+
var OPENROUTER_KEYS = [
|
|
661
|
+
"prompt",
|
|
662
|
+
"completion",
|
|
663
|
+
"request",
|
|
664
|
+
"input_cache_read",
|
|
665
|
+
"input_cache_write"
|
|
666
|
+
];
|
|
667
|
+
var MODELS_DEV_KEYS = [
|
|
668
|
+
"input",
|
|
669
|
+
"output",
|
|
670
|
+
"cache_read",
|
|
671
|
+
"cache_write"
|
|
672
|
+
];
|
|
673
|
+
function perTokenRate(value) {
|
|
674
|
+
if (typeof value !== "string" || value.trim() === "") return void 0;
|
|
675
|
+
const parsed = Number(value);
|
|
676
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
|
|
677
|
+
}
|
|
678
|
+
function toPer1M(perToken) {
|
|
679
|
+
return Number((perToken * 1e6).toPrecision(12));
|
|
680
|
+
}
|
|
681
|
+
function per1MRate(value) {
|
|
682
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
683
|
+
}
|
|
684
|
+
function extractOpenRouterPricing(raw) {
|
|
685
|
+
const input = perTokenRate(raw.prompt);
|
|
686
|
+
const output = perTokenRate(raw.completion);
|
|
687
|
+
if (input === void 0 || output === void 0) return void 0;
|
|
688
|
+
const cost = { input: toPer1M(input), output: toPer1M(output) };
|
|
689
|
+
for (const [field, target] of [
|
|
690
|
+
["input_cache_read", "cache_read"],
|
|
691
|
+
["input_cache_write", "cache_write"]
|
|
692
|
+
]) {
|
|
693
|
+
if (raw[field] === void 0) continue;
|
|
694
|
+
const rate = perTokenRate(raw[field]);
|
|
695
|
+
if (rate === void 0) return void 0;
|
|
696
|
+
cost[target] = toPer1M(rate);
|
|
697
|
+
}
|
|
698
|
+
return cost;
|
|
699
|
+
}
|
|
700
|
+
function extractModelsDevPricing(raw) {
|
|
701
|
+
const input = per1MRate(raw.input);
|
|
702
|
+
const output = per1MRate(raw.output);
|
|
703
|
+
if (input === void 0 || output === void 0) return void 0;
|
|
704
|
+
const cost = { input, output };
|
|
705
|
+
for (const field of ["cache_read", "cache_write"]) {
|
|
706
|
+
if (raw[field] === void 0) continue;
|
|
707
|
+
const rate = per1MRate(raw[field]);
|
|
708
|
+
if (rate === void 0) return void 0;
|
|
709
|
+
cost[field] = rate;
|
|
710
|
+
}
|
|
711
|
+
return cost;
|
|
712
|
+
}
|
|
713
|
+
function extractPricing(raw) {
|
|
714
|
+
if (!isRecord5(raw)) return void 0;
|
|
715
|
+
const hasOpenRouterKeys = OPENROUTER_KEYS.some((key) => key in raw);
|
|
716
|
+
const hasModelsDevKeys = MODELS_DEV_KEYS.some((key) => key in raw);
|
|
717
|
+
if (hasOpenRouterKeys && hasModelsDevKeys) return void 0;
|
|
718
|
+
if (hasOpenRouterKeys) return extractOpenRouterPricing(raw);
|
|
719
|
+
if (hasModelsDevKeys) return extractModelsDevPricing(raw);
|
|
720
|
+
return void 0;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// src/catalog/normalize-model-id.ts
|
|
724
|
+
var FACADE_ID_PATTERN = /^([^:/]+):global:global:([^:]+):(.+)$/;
|
|
725
|
+
function normalizeId(raw, env = process.env) {
|
|
726
|
+
const id = raw.trim();
|
|
727
|
+
if (!id) {
|
|
728
|
+
logger.warn(
|
|
729
|
+
`dropping empty model id from catalog (raw: ${JSON.stringify(raw)})`
|
|
730
|
+
);
|
|
731
|
+
return void 0;
|
|
732
|
+
}
|
|
733
|
+
if (!id.includes(":")) return id;
|
|
734
|
+
const shimEnabled = env[ENV_DISABLE_ID_SHIM] !== "1";
|
|
735
|
+
const facade = shimEnabled ? FACADE_ID_PATTERN.exec(id) : null;
|
|
736
|
+
if (facade?.[2] && facade[3])
|
|
737
|
+
return `${facade[2]}/${facade[3]}`;
|
|
738
|
+
logger.warn(
|
|
739
|
+
shimEnabled ? `dropping unrecognized colon-form model id "${id}" (not facade form, not a canonical slug)` : `dropping colon-form model id "${id}" (facade-id shim disabled via ${ENV_DISABLE_ID_SHIM}=1)`
|
|
740
|
+
);
|
|
741
|
+
return void 0;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/catalog/fetch-catalog.ts
|
|
745
|
+
function isRecord6(value) {
|
|
746
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
747
|
+
}
|
|
748
|
+
function parseEntries(payload) {
|
|
749
|
+
if (Array.isArray(payload)) return payload;
|
|
750
|
+
if (isRecord6(payload) && Array.isArray(payload.data)) return payload.data;
|
|
751
|
+
throw new CatalogFetchError(
|
|
752
|
+
"parse",
|
|
753
|
+
"response is neither {data:[\u2026]} nor a bare array"
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
function toCatalogModels(entries, env) {
|
|
757
|
+
const models = {};
|
|
758
|
+
for (const entry of entries) {
|
|
759
|
+
if (!isRecord6(entry) || typeof entry.id !== "string") continue;
|
|
760
|
+
const slug2 = normalizeId(entry.id, env);
|
|
761
|
+
if (!slug2 || models[slug2]) continue;
|
|
762
|
+
const model = { id: slug2 };
|
|
763
|
+
const displayName = typeof entry.display_name === "string" ? entry.display_name.trim() : "";
|
|
764
|
+
if (displayName !== "") model.displayName = displayName;
|
|
765
|
+
if (isRecord6(entry.pricing) && extractPricing(entry.pricing) !== void 0)
|
|
766
|
+
model.pricing = entry.pricing;
|
|
767
|
+
models[slug2] = model;
|
|
768
|
+
}
|
|
769
|
+
return models;
|
|
770
|
+
}
|
|
771
|
+
async function fetchCatalog(opts) {
|
|
772
|
+
const {
|
|
773
|
+
baseURL,
|
|
774
|
+
key,
|
|
775
|
+
env = process.env,
|
|
776
|
+
timeoutMs = DEFAULT_FETCH_TIMEOUT_MS
|
|
777
|
+
} = opts;
|
|
778
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
779
|
+
const url = `${baseURL.replace(/\/+$/, "")}/models`;
|
|
780
|
+
const controller = new AbortController();
|
|
781
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
782
|
+
try {
|
|
783
|
+
let response;
|
|
784
|
+
try {
|
|
785
|
+
response = await fetchImpl(url, {
|
|
786
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
787
|
+
signal: controller.signal
|
|
788
|
+
});
|
|
789
|
+
} catch (error) {
|
|
790
|
+
if (controller.signal.aborted)
|
|
791
|
+
throw new CatalogFetchError(
|
|
792
|
+
"timeout",
|
|
793
|
+
`no response within ${timeoutMs}ms`
|
|
794
|
+
);
|
|
795
|
+
throw new CatalogFetchError(
|
|
796
|
+
"network",
|
|
797
|
+
error instanceof Error ? error.message : String(error)
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
if (!response.ok) {
|
|
801
|
+
throw new CatalogFetchError(
|
|
802
|
+
"http",
|
|
803
|
+
`${url} responded ${response.status}`,
|
|
804
|
+
response.status
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
let payload;
|
|
808
|
+
try {
|
|
809
|
+
payload = await response.json();
|
|
810
|
+
} catch {
|
|
811
|
+
if (controller.signal.aborted)
|
|
812
|
+
throw new CatalogFetchError(
|
|
813
|
+
"timeout",
|
|
814
|
+
`body not read within ${timeoutMs}ms`
|
|
815
|
+
);
|
|
816
|
+
throw new CatalogFetchError("parse", "response body is not valid JSON");
|
|
817
|
+
}
|
|
818
|
+
const models = toCatalogModels(parseEntries(payload), env);
|
|
819
|
+
logger.info(`fetched ${Object.keys(models).length} model(s) from ${url}`);
|
|
820
|
+
return models;
|
|
821
|
+
} finally {
|
|
822
|
+
clearTimeout(timer);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/catalog/catalog-service.ts
|
|
827
|
+
function seedResult(seed, why) {
|
|
828
|
+
const models = {};
|
|
829
|
+
for (const entry of seed) {
|
|
830
|
+
models[entry.slug] = { id: entry.slug };
|
|
831
|
+
}
|
|
832
|
+
logger.info(`catalog source: seed (${why}; ${seed.length} model(s))`);
|
|
833
|
+
return { models, source: "seed" };
|
|
834
|
+
}
|
|
835
|
+
async function runLadder(opts) {
|
|
836
|
+
const { key, baseURL, seed, env = process.env, fetchImpl, now } = opts;
|
|
837
|
+
if (env[ENV_DISCOVERY] === "0")
|
|
838
|
+
return seedResult(seed, `discovery disabled via ${ENV_DISCOVERY}=0`);
|
|
839
|
+
if (!key) return seedResult(seed, "no API key resolved");
|
|
840
|
+
const fresh = readFreshCatalog({ key, env, now });
|
|
841
|
+
if (fresh) {
|
|
842
|
+
logger.info(
|
|
843
|
+
`catalog source: cache (${Object.keys(fresh).length} model(s))`
|
|
844
|
+
);
|
|
845
|
+
return { models: fresh, source: "cache" };
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
const live = await fetchCatalog({ baseURL, key, env, fetchImpl });
|
|
849
|
+
if (Object.keys(live).length === 0)
|
|
850
|
+
throw new Error("gateway returned an empty catalog");
|
|
851
|
+
writeCatalog({ key, models: live, env, now });
|
|
852
|
+
logger.info(`catalog source: live (${Object.keys(live).length} model(s))`);
|
|
853
|
+
return { models: live, source: "live" };
|
|
854
|
+
} catch (error) {
|
|
855
|
+
logger.warn(
|
|
856
|
+
`live catalog fetch failed \u2014 trying stale cache: ${error instanceof Error ? error.message : String(error)}`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
const stale = readStaleCatalog({ key, env });
|
|
860
|
+
if (stale) {
|
|
861
|
+
logger.info(
|
|
862
|
+
`catalog source: stale-cache (${Object.keys(stale).length} model(s))`
|
|
863
|
+
);
|
|
864
|
+
return { models: stale, source: "stale-cache" };
|
|
865
|
+
}
|
|
866
|
+
return seedResult(seed, "live fetch failed and no cache available");
|
|
867
|
+
}
|
|
868
|
+
async function getCatalog(opts) {
|
|
869
|
+
try {
|
|
870
|
+
return await runLadder(opts);
|
|
871
|
+
} catch (error) {
|
|
872
|
+
logger.error(
|
|
873
|
+
`unexpected catalog-service failure \u2014 degrading to seed: ${error instanceof Error ? error.message : String(error)}`
|
|
874
|
+
);
|
|
875
|
+
try {
|
|
876
|
+
return seedResult(opts.seed, "unexpected internal failure");
|
|
877
|
+
} catch {
|
|
878
|
+
return { models: {}, source: "seed" };
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/catalog/seed/anthropic-gateway-limit.ts
|
|
884
|
+
var ANTHROPIC_GATEWAY_LIMIT = {
|
|
885
|
+
context: 1e6,
|
|
886
|
+
output: 32e3
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
// src/catalog/seed/slug.ts
|
|
890
|
+
var slug = (s) => s;
|
|
891
|
+
|
|
892
|
+
// src/catalog/seed/claude-opus.ts
|
|
893
|
+
var CLAUDE_OPUS = {
|
|
894
|
+
slug: slug("anthropic/claude-opus-4.8"),
|
|
895
|
+
name: "Claude Opus 4.8",
|
|
896
|
+
cost: {
|
|
897
|
+
input: 5,
|
|
898
|
+
// source: models.dev 2026-07-04 (key: anthropic/claude-opus-4-8, dash form)
|
|
899
|
+
output: 25,
|
|
900
|
+
// source: models.dev 2026-07-04
|
|
901
|
+
cache_read: 0.5,
|
|
902
|
+
// source: models.dev 2026-07-04
|
|
903
|
+
cache_write: 6.25
|
|
904
|
+
// source: models.dev 2026-07-04
|
|
905
|
+
},
|
|
906
|
+
limit: ANTHROPIC_GATEWAY_LIMIT,
|
|
907
|
+
capabilities: { reasoning: true }
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
// src/catalog/seed/claude-sonnet.ts
|
|
911
|
+
var CLAUDE_SONNET = {
|
|
912
|
+
slug: slug("anthropic/claude-sonnet-4.6"),
|
|
913
|
+
name: "Claude Sonnet 4.6",
|
|
914
|
+
cost: {
|
|
915
|
+
input: 3,
|
|
916
|
+
// source: models.dev 2026-07-04 (key: anthropic/claude-sonnet-4-6, dash form)
|
|
917
|
+
output: 15,
|
|
918
|
+
// source: models.dev 2026-07-04
|
|
919
|
+
cache_read: 0.3,
|
|
920
|
+
// source: models.dev 2026-07-04
|
|
921
|
+
cache_write: 3.75
|
|
922
|
+
// source: models.dev 2026-07-04
|
|
923
|
+
},
|
|
924
|
+
limit: ANTHROPIC_GATEWAY_LIMIT,
|
|
925
|
+
capabilities: { reasoning: true }
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
// src/catalog/seed/gemini-3-1-pro-preview.ts
|
|
929
|
+
var GEMINI_3_1_PRO_PREVIEW = {
|
|
930
|
+
slug: slug("google/gemini-3.1-pro-preview"),
|
|
931
|
+
name: "Gemini 3.1 Pro Preview",
|
|
932
|
+
cost: {
|
|
933
|
+
input: 2,
|
|
934
|
+
// source: models.dev 2026-07-04
|
|
935
|
+
output: 12,
|
|
936
|
+
// source: models.dev 2026-07-04
|
|
937
|
+
cache_read: 0.2,
|
|
938
|
+
// source: models.dev 2026-07-04
|
|
939
|
+
// models.dev's context tier boundary is exactly 200,000 tokens here —
|
|
940
|
+
// context_over_200k is the lossless encoding (D20 "certainty achieved").
|
|
941
|
+
context_over_200k: {
|
|
942
|
+
input: 4,
|
|
943
|
+
// source: models.dev 2026-07-04
|
|
944
|
+
output: 18,
|
|
945
|
+
// source: models.dev 2026-07-04
|
|
946
|
+
cache_read: 0.4
|
|
947
|
+
// source: models.dev 2026-07-04
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
limit: { context: 1048576, output: 65536 },
|
|
951
|
+
// source: models.dev 2026-07-04
|
|
952
|
+
capabilities: {}
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
// src/catalog/seed/glm-5-2.ts
|
|
956
|
+
var GLM_5_2 = {
|
|
957
|
+
slug: slug("z-ai/glm-5.2"),
|
|
958
|
+
name: "GLM-5.2",
|
|
959
|
+
cost: {
|
|
960
|
+
input: 1.4,
|
|
961
|
+
// source: models.dev 2026-07-04 (provider key: zai)
|
|
962
|
+
output: 4.4,
|
|
963
|
+
// source: models.dev 2026-07-04
|
|
964
|
+
cache_read: 0.26
|
|
965
|
+
// source: models.dev 2026-07-04
|
|
966
|
+
// cache_write deliberately OMITTED: models.dev lists a literal 0, which
|
|
967
|
+
// the seed contract forbids as a recorded price. Omission makes opencode
|
|
968
|
+
// bill cache writes at the input rate — the conservative D20 fallback
|
|
969
|
+
// (overestimate over a possibly-wrong free write).
|
|
970
|
+
},
|
|
971
|
+
limit: { context: 1e6, output: 131072 },
|
|
972
|
+
// source: models.dev 2026-07-04
|
|
973
|
+
capabilities: {}
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
// src/catalog/seed/gpt-4o-mini.ts
|
|
977
|
+
var GPT_4O_MINI = {
|
|
978
|
+
slug: slug("openai/gpt-4o-mini"),
|
|
979
|
+
name: "GPT-4o mini",
|
|
980
|
+
cost: {
|
|
981
|
+
input: 0.15,
|
|
982
|
+
// source: models.dev 2026-07-04
|
|
983
|
+
output: 0.6,
|
|
984
|
+
// source: models.dev 2026-07-04
|
|
985
|
+
cache_read: 0.075
|
|
986
|
+
// source: models.dev 2026-07-04
|
|
987
|
+
},
|
|
988
|
+
limit: { context: 128e3, output: 16384 },
|
|
989
|
+
// source: models.dev 2026-07-04
|
|
990
|
+
capabilities: {}
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
// src/catalog/seed/gpt-5-4-mini.ts
|
|
994
|
+
var GPT_5_4_MINI = {
|
|
995
|
+
slug: slug("openai/gpt-5.4-mini"),
|
|
996
|
+
name: "GPT-5.4 mini",
|
|
997
|
+
cost: {
|
|
998
|
+
input: 0.75,
|
|
999
|
+
// source: models.dev 2026-07-04
|
|
1000
|
+
output: 4.5,
|
|
1001
|
+
// source: models.dev 2026-07-04
|
|
1002
|
+
cache_read: 0.075
|
|
1003
|
+
// source: models.dev 2026-07-04
|
|
1004
|
+
},
|
|
1005
|
+
limit: { context: 4e5, output: 128e3 },
|
|
1006
|
+
// source: models.dev 2026-07-04
|
|
1007
|
+
// Reasoning model; opencode's variants gate (transform.ts) requires the flag.
|
|
1008
|
+
capabilities: { reasoning: true }
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
// src/catalog/seed/gpt-5-5.ts
|
|
1012
|
+
var GPT_5_5 = {
|
|
1013
|
+
slug: slug("openai/gpt-5.5"),
|
|
1014
|
+
name: "GPT-5.5",
|
|
1015
|
+
cost: {
|
|
1016
|
+
input: 5,
|
|
1017
|
+
// source: models.dev 2026-07-04
|
|
1018
|
+
output: 30,
|
|
1019
|
+
// source: models.dev 2026-07-04
|
|
1020
|
+
cache_read: 0.5,
|
|
1021
|
+
// source: models.dev 2026-07-04
|
|
1022
|
+
// models.dev lists a context tier at 272k tokens (input 10 / output 45 /
|
|
1023
|
+
// cache_read 1) AND ships this pre-computed context_over_200k with the
|
|
1024
|
+
// same rates. The 272k boundary itself is inexpressible in the config
|
|
1025
|
+
// schema; billing the higher tier from 200k instead only ever
|
|
1026
|
+
// OVER-estimates the 200k–272k window — D20's conservative branch,
|
|
1027
|
+
// encoded precisely rather than flattened to a max rate.
|
|
1028
|
+
context_over_200k: {
|
|
1029
|
+
input: 10,
|
|
1030
|
+
// source: models.dev 2026-07-04
|
|
1031
|
+
output: 45,
|
|
1032
|
+
// source: models.dev 2026-07-04
|
|
1033
|
+
cache_read: 1
|
|
1034
|
+
// source: models.dev 2026-07-04
|
|
1035
|
+
}
|
|
1036
|
+
},
|
|
1037
|
+
limit: { context: 105e4, output: 128e3 },
|
|
1038
|
+
// source: models.dev 2026-07-04
|
|
1039
|
+
// Reasoning model; opencode's variants gate (transform.ts) requires the flag.
|
|
1040
|
+
capabilities: { reasoning: true }
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
// src/catalog/seed/kimi-k2-6.ts
|
|
1044
|
+
var KIMI_K2_6 = {
|
|
1045
|
+
slug: slug("moonshotai/kimi-k2.6"),
|
|
1046
|
+
name: "Kimi K2.6",
|
|
1047
|
+
cost: {
|
|
1048
|
+
input: 0.95,
|
|
1049
|
+
// source: models.dev 2026-07-04
|
|
1050
|
+
output: 4,
|
|
1051
|
+
// source: models.dev 2026-07-04
|
|
1052
|
+
cache_read: 0.16
|
|
1053
|
+
// source: models.dev 2026-07-04
|
|
1054
|
+
},
|
|
1055
|
+
limit: { context: 262144, output: 262144 },
|
|
1056
|
+
// source: models.dev 2026-07-04
|
|
1057
|
+
capabilities: {}
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
// src/catalog/seed/kimi-k2-7-code.ts
|
|
1061
|
+
var KIMI_K2_7_CODE = {
|
|
1062
|
+
slug: slug("moonshotai/kimi-k2.7-code"),
|
|
1063
|
+
name: "Kimi K2.7 Code",
|
|
1064
|
+
cost: {
|
|
1065
|
+
input: 0.95,
|
|
1066
|
+
// source: models.dev 2026-07-04
|
|
1067
|
+
output: 4,
|
|
1068
|
+
// source: models.dev 2026-07-04
|
|
1069
|
+
cache_read: 0.19
|
|
1070
|
+
// source: models.dev 2026-07-04
|
|
1071
|
+
},
|
|
1072
|
+
limit: { context: 262144, output: 262144 },
|
|
1073
|
+
// source: models.dev 2026-07-04
|
|
1074
|
+
capabilities: {}
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
// src/catalog/seed/kimi-k3.ts
|
|
1078
|
+
var KIMI_K3 = {
|
|
1079
|
+
slug: slug("moonshotai/kimi-k3"),
|
|
1080
|
+
name: "Kimi K3",
|
|
1081
|
+
cost: {
|
|
1082
|
+
input: 3,
|
|
1083
|
+
// source: models.dev 2026-07-18
|
|
1084
|
+
output: 15,
|
|
1085
|
+
// source: models.dev 2026-07-18
|
|
1086
|
+
cache_read: 0.3
|
|
1087
|
+
// source: models.dev 2026-07-18
|
|
1088
|
+
},
|
|
1089
|
+
limit: { context: 1048576, output: 131072 },
|
|
1090
|
+
// source: models.dev 2026-07-18
|
|
1091
|
+
capabilities: { reasoning: true },
|
|
1092
|
+
// opencode-go parity: the openai-compatible lowerer wires reasoning_effort: "max".
|
|
1093
|
+
variants: { max: { reasoningEffort: "max" } }
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
// src/catalog/seed/seed.ts
|
|
1097
|
+
var SEED = [
|
|
1098
|
+
GPT_5_5,
|
|
1099
|
+
GPT_5_4_MINI,
|
|
1100
|
+
GEMINI_3_1_PRO_PREVIEW,
|
|
1101
|
+
KIMI_K2_6,
|
|
1102
|
+
KIMI_K2_7_CODE,
|
|
1103
|
+
KIMI_K3,
|
|
1104
|
+
GPT_4O_MINI,
|
|
1105
|
+
GLM_5_2,
|
|
1106
|
+
CLAUDE_OPUS,
|
|
1107
|
+
CLAUDE_SONNET,
|
|
1108
|
+
// D10 dash-form aliases — spread from their canonical entries so
|
|
1109
|
+
// cost/limit/capabilities stay identical by construction.
|
|
1110
|
+
{
|
|
1111
|
+
...CLAUDE_OPUS,
|
|
1112
|
+
slug: slug("anthropic/claude-opus-4-8"),
|
|
1113
|
+
name: `${CLAUDE_OPUS.name} (alias)`,
|
|
1114
|
+
aliasOf: CLAUDE_OPUS.slug
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
...CLAUDE_SONNET,
|
|
1118
|
+
slug: slug("anthropic/claude-sonnet-4-6"),
|
|
1119
|
+
name: `${CLAUDE_SONNET.name} (alias)`,
|
|
1120
|
+
aliasOf: CLAUDE_SONNET.slug
|
|
1121
|
+
}
|
|
1122
|
+
];
|
|
1123
|
+
|
|
1124
|
+
// src/enrich/static-meta.ts
|
|
1125
|
+
var lookupSeed = (slug2) => SEED.find((entry) => entry.slug === slug2) ?? SEED.find((entry) => entry.aliasOf === slug2);
|
|
1126
|
+
|
|
1127
|
+
// src/enrich/enrich-model.ts
|
|
1128
|
+
function resolveCost(catalogModel, modelsDevLookup, seed) {
|
|
1129
|
+
const r33Cost = catalogModel?.pricing !== void 0 ? extractPricing(catalogModel.pricing) : void 0;
|
|
1130
|
+
return r33Cost ?? modelsDevLookup?.cost ?? seed?.cost;
|
|
1131
|
+
}
|
|
1132
|
+
function resolveLimit(seed, modelsDevLookup) {
|
|
1133
|
+
return seed?.limit ?? modelsDevLookup?.limit;
|
|
1134
|
+
}
|
|
1135
|
+
function resolveCapabilities(seed, modelsDevLookup, env) {
|
|
1136
|
+
const base = seed?.capabilities ?? {};
|
|
1137
|
+
if (env[ENV_ENRICH_CAPS] !== "1" || modelsDevLookup === void 0)
|
|
1138
|
+
return base;
|
|
1139
|
+
const result = { ...base };
|
|
1140
|
+
if (result.reasoning === void 0 && modelsDevLookup.reasoning !== void 0)
|
|
1141
|
+
result.reasoning = modelsDevLookup.reasoning;
|
|
1142
|
+
if (result.tool_call === void 0 && modelsDevLookup.tool_call !== void 0)
|
|
1143
|
+
result.tool_call = modelsDevLookup.tool_call;
|
|
1144
|
+
if (result.attachment === void 0 && modelsDevLookup.attachment !== void 0)
|
|
1145
|
+
result.attachment = modelsDevLookup.attachment;
|
|
1146
|
+
if (result.temperature === void 0 && modelsDevLookup.temperature !== void 0) {
|
|
1147
|
+
result.temperature = modelsDevLookup.temperature;
|
|
1148
|
+
}
|
|
1149
|
+
return result;
|
|
1150
|
+
}
|
|
1151
|
+
function enrichModel(slug2, sources) {
|
|
1152
|
+
const env = sources.env ?? process.env;
|
|
1153
|
+
const seed = lookupSeed(slug2);
|
|
1154
|
+
const modelsDevLookup = sources.modelsDev.lookup(slug2);
|
|
1155
|
+
const capabilities = resolveCapabilities(seed, modelsDevLookup, env);
|
|
1156
|
+
const catalogModel = sources.catalogModel;
|
|
1157
|
+
const name = catalogModel !== void 0 && catalogModel.id === slug2 && catalogModel.displayName !== void 0 ? catalogModel.displayName : slug2;
|
|
1158
|
+
const meta = { name, capabilities };
|
|
1159
|
+
const limit = resolveLimit(seed, modelsDevLookup);
|
|
1160
|
+
if (limit !== void 0) meta.limit = limit;
|
|
1161
|
+
if (seed?.variants !== void 0) meta.variants = seed.variants;
|
|
1162
|
+
const cost = resolveCost(sources.catalogModel, modelsDevLookup, seed);
|
|
1163
|
+
if (cost === void 0) {
|
|
1164
|
+
logger.warn(
|
|
1165
|
+
`cost metadata unavailable for ${slug2} \u2014 reported cost will be $0 until added`
|
|
1166
|
+
);
|
|
1167
|
+
return meta;
|
|
1168
|
+
}
|
|
1169
|
+
meta.cost = cost;
|
|
1170
|
+
return meta;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// src/enrich/models-dev/load-models-dev.ts
|
|
1174
|
+
import * as fs7 from "fs";
|
|
1175
|
+
import * as path9 from "path";
|
|
1176
|
+
|
|
1177
|
+
// src/enrich/models-dev/is-record.ts
|
|
1178
|
+
function isRecord7(value) {
|
|
1179
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/enrich/models-dev/fetch-live.ts
|
|
1183
|
+
var MODELS_DEV_API_URL = "https://models.dev/api.json";
|
|
1184
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
1185
|
+
async function fetchLive(fetchImpl) {
|
|
1186
|
+
const controller = new AbortController();
|
|
1187
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
1188
|
+
try {
|
|
1189
|
+
const response = await fetchImpl(MODELS_DEV_API_URL, {
|
|
1190
|
+
signal: controller.signal
|
|
1191
|
+
});
|
|
1192
|
+
if (!response.ok) {
|
|
1193
|
+
logger.warn(`models.dev responded ${response.status}`);
|
|
1194
|
+
return void 0;
|
|
1195
|
+
}
|
|
1196
|
+
const body = await response.json();
|
|
1197
|
+
return isRecord7(body) ? body : void 0;
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
logger.warn("models.dev fetch failed:", error);
|
|
1200
|
+
return void 0;
|
|
1201
|
+
} finally {
|
|
1202
|
+
clearTimeout(timer);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// src/enrich/models-dev/num.ts
|
|
1207
|
+
function num(value) {
|
|
1208
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// src/enrich/models-dev/normalize-cost.ts
|
|
1212
|
+
var EXPRESSIBLE_TIER_SIZE = 2e5;
|
|
1213
|
+
function parseRateBlock(value) {
|
|
1214
|
+
if (!isRecord7(value)) return void 0;
|
|
1215
|
+
const input = num(value.input);
|
|
1216
|
+
const output = num(value.output);
|
|
1217
|
+
if (input === void 0 || output === void 0) return void 0;
|
|
1218
|
+
const block = { input, output };
|
|
1219
|
+
const cacheRead = num(value.cache_read);
|
|
1220
|
+
if (cacheRead !== void 0) block.cache_read = cacheRead;
|
|
1221
|
+
const cacheWrite = num(value.cache_write);
|
|
1222
|
+
if (cacheWrite !== void 0) block.cache_write = cacheWrite;
|
|
1223
|
+
return block;
|
|
1224
|
+
}
|
|
1225
|
+
function isExpressibleContextTier(tier) {
|
|
1226
|
+
const marker = tier.tier;
|
|
1227
|
+
return isRecord7(marker) && marker.type === "context" && marker.size === EXPRESSIBLE_TIER_SIZE;
|
|
1228
|
+
}
|
|
1229
|
+
function maxRate(current, candidate) {
|
|
1230
|
+
const rate = num(candidate);
|
|
1231
|
+
if (rate === void 0) return current;
|
|
1232
|
+
return current === void 0 || rate > current ? rate : current;
|
|
1233
|
+
}
|
|
1234
|
+
function normalizeCost(raw) {
|
|
1235
|
+
if (!isRecord7(raw)) return void 0;
|
|
1236
|
+
const input = num(raw.input);
|
|
1237
|
+
const output = num(raw.output);
|
|
1238
|
+
if (input === void 0 || output === void 0) return void 0;
|
|
1239
|
+
const cost = { input, output };
|
|
1240
|
+
const cacheRead = num(raw.cache_read);
|
|
1241
|
+
if (cacheRead !== void 0) cost.cache_read = cacheRead;
|
|
1242
|
+
const cacheWrite = num(raw.cache_write);
|
|
1243
|
+
if (cacheWrite !== void 0) cost.cache_write = cacheWrite;
|
|
1244
|
+
const over200k = parseRateBlock(raw.context_over_200k);
|
|
1245
|
+
if (over200k) cost.context_over_200k = over200k;
|
|
1246
|
+
const tiers = Array.isArray(raw.tiers) ? raw.tiers.filter(isRecord7) : [];
|
|
1247
|
+
for (const tier of tiers) {
|
|
1248
|
+
if (isExpressibleContextTier(tier)) {
|
|
1249
|
+
if (cost.context_over_200k === void 0) {
|
|
1250
|
+
const converted = parseRateBlock(tier);
|
|
1251
|
+
if (converted) cost.context_over_200k = converted;
|
|
1252
|
+
}
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
cost.input = maxRate(cost.input, tier.input) ?? cost.input;
|
|
1256
|
+
cost.output = maxRate(cost.output, tier.output) ?? cost.output;
|
|
1257
|
+
const collapsedRead = maxRate(cost.cache_read, tier.cache_read);
|
|
1258
|
+
if (collapsedRead !== void 0) cost.cache_read = collapsedRead;
|
|
1259
|
+
const collapsedWrite = maxRate(cost.cache_write, tier.cache_write);
|
|
1260
|
+
if (collapsedWrite !== void 0) cost.cache_write = collapsedWrite;
|
|
1261
|
+
}
|
|
1262
|
+
return cost;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// src/enrich/models-dev/vendor-aliases.ts
|
|
1266
|
+
var VENDOR_ALIASES = {
|
|
1267
|
+
"z-ai": "zai"
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
// src/enrich/models-dev/probe-candidates.ts
|
|
1271
|
+
function swapLast(value, from, to) {
|
|
1272
|
+
const index = value.lastIndexOf(from);
|
|
1273
|
+
return index === -1 ? value : `${value.slice(0, index)}${to}${value.slice(index + 1)}`;
|
|
1274
|
+
}
|
|
1275
|
+
function probeCandidates(slug2) {
|
|
1276
|
+
const sep2 = slug2.indexOf("/");
|
|
1277
|
+
if (sep2 <= 0 || sep2 >= slug2.length - 1) return [];
|
|
1278
|
+
const vendor = slug2.slice(0, sep2);
|
|
1279
|
+
const model = slug2.slice(sep2 + 1);
|
|
1280
|
+
const vendors = [vendor];
|
|
1281
|
+
const alias = VENDOR_ALIASES[vendor];
|
|
1282
|
+
if (alias !== void 0 && alias !== vendor) vendors.push(alias);
|
|
1283
|
+
const swaps = [];
|
|
1284
|
+
for (const candidate of [
|
|
1285
|
+
model.replaceAll(".", "-"),
|
|
1286
|
+
model.replaceAll("-", "."),
|
|
1287
|
+
swapLast(model, ".", "-"),
|
|
1288
|
+
swapLast(model, "-", ".")
|
|
1289
|
+
]) {
|
|
1290
|
+
if (candidate !== model && !swaps.includes(candidate))
|
|
1291
|
+
swaps.push(candidate);
|
|
1292
|
+
}
|
|
1293
|
+
const candidates = vendors.map(
|
|
1294
|
+
(v) => [v, model]
|
|
1295
|
+
);
|
|
1296
|
+
for (const v of vendors) {
|
|
1297
|
+
for (const swapped of swaps) candidates.push([v, swapped]);
|
|
1298
|
+
}
|
|
1299
|
+
return candidates;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/enrich/models-dev/lookup.ts
|
|
1303
|
+
function normalizeLimit(raw) {
|
|
1304
|
+
if (!isRecord7(raw)) return void 0;
|
|
1305
|
+
const context = num(raw.context);
|
|
1306
|
+
const output = num(raw.output);
|
|
1307
|
+
if (context === void 0 || output === void 0) return void 0;
|
|
1308
|
+
const limit = { context, output };
|
|
1309
|
+
const input = num(raw.input);
|
|
1310
|
+
if (input !== void 0) limit.input = input;
|
|
1311
|
+
return limit;
|
|
1312
|
+
}
|
|
1313
|
+
function buildLookup(model) {
|
|
1314
|
+
const result = {};
|
|
1315
|
+
const cost = normalizeCost(model.cost);
|
|
1316
|
+
if (cost) result.cost = cost;
|
|
1317
|
+
const limit = normalizeLimit(model.limit);
|
|
1318
|
+
if (limit) result.limit = limit;
|
|
1319
|
+
for (const field of [
|
|
1320
|
+
"reasoning",
|
|
1321
|
+
"tool_call",
|
|
1322
|
+
"attachment",
|
|
1323
|
+
"temperature"
|
|
1324
|
+
]) {
|
|
1325
|
+
const value = model[field];
|
|
1326
|
+
if (typeof value === "boolean") result[field] = value;
|
|
1327
|
+
}
|
|
1328
|
+
return result;
|
|
1329
|
+
}
|
|
1330
|
+
function lookupSlug(data, slug2) {
|
|
1331
|
+
for (const [providerKey, modelKey] of probeCandidates(slug2)) {
|
|
1332
|
+
const provider = data[providerKey];
|
|
1333
|
+
if (!isRecord7(provider) || !isRecord7(provider.models)) continue;
|
|
1334
|
+
const model = provider.models[modelKey];
|
|
1335
|
+
if (isRecord7(model)) return buildLookup(model);
|
|
1336
|
+
}
|
|
1337
|
+
return void 0;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/enrich/models-dev/load-models-dev.ts
|
|
1341
|
+
var CACHE_FILE = "models-dev.json";
|
|
1342
|
+
function readCache(cachePath) {
|
|
1343
|
+
try {
|
|
1344
|
+
const parsed = JSON.parse(fs7.readFileSync(cachePath, "utf8"));
|
|
1345
|
+
if (!isRecord7(parsed)) return void 0;
|
|
1346
|
+
const fetchedAt = num(parsed.fetchedAt);
|
|
1347
|
+
if (fetchedAt === void 0 || !isRecord7(parsed.data)) return void 0;
|
|
1348
|
+
return { fetchedAt, data: parsed.data };
|
|
1349
|
+
} catch {
|
|
1350
|
+
return void 0;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
function writeCache(cachePath, envelope) {
|
|
1354
|
+
try {
|
|
1355
|
+
fs7.mkdirSync(path9.dirname(cachePath), { recursive: true });
|
|
1356
|
+
fs7.writeFileSync(cachePath, JSON.stringify(envelope), "utf8");
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
logger.warn("failed to write models.dev cache:", error);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function makeSource(source, data) {
|
|
1362
|
+
return { source, lookup: (slug2) => lookupSlug(data, slug2) };
|
|
1363
|
+
}
|
|
1364
|
+
async function loadModelsDev(options = {}) {
|
|
1365
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1366
|
+
const cacheDir = options.cacheDir ?? resolveCacheDir();
|
|
1367
|
+
const ttlMs = options.ttlMs ?? MODELS_DEV_TTL_MS;
|
|
1368
|
+
const now = options.now ?? Date.now;
|
|
1369
|
+
const cachePath = path9.join(cacheDir, CACHE_FILE);
|
|
1370
|
+
const cached = readCache(cachePath);
|
|
1371
|
+
if (cached && now() - cached.fetchedAt < ttlMs)
|
|
1372
|
+
return makeSource("cache", cached.data);
|
|
1373
|
+
const live = await fetchLive(fetchImpl);
|
|
1374
|
+
if (live) {
|
|
1375
|
+
writeCache(cachePath, { fetchedAt: now(), data: live });
|
|
1376
|
+
return makeSource("live", live);
|
|
1377
|
+
}
|
|
1378
|
+
if (cached) {
|
|
1379
|
+
logger.warn("models.dev unreachable \u2014 serving stale cache");
|
|
1380
|
+
return makeSource("stale-cache", cached.data);
|
|
1381
|
+
}
|
|
1382
|
+
logger.warn(
|
|
1383
|
+
"models.dev unavailable (no network, no cache) \u2014 enrichment source disabled"
|
|
1384
|
+
);
|
|
1385
|
+
return { source: null, lookup: () => void 0 };
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// src/config/build-alias-targets.ts
|
|
1389
|
+
var SEED_ALIAS_TARGETS = Object.fromEntries(
|
|
1390
|
+
SEED.flatMap(
|
|
1391
|
+
(entry) => entry.aliasOf === void 0 ? [] : [[entry.slug, entry.aliasOf]]
|
|
1392
|
+
)
|
|
1393
|
+
);
|
|
1394
|
+
function buildAliasTargets(catalog) {
|
|
1395
|
+
const targets = { ...SEED_ALIAS_TARGETS };
|
|
1396
|
+
if (catalog.source === "seed") return targets;
|
|
1397
|
+
for (const entry of SEED) {
|
|
1398
|
+
if (entry.aliasOf === void 0) continue;
|
|
1399
|
+
const aliasSlug = entry.slug;
|
|
1400
|
+
const canonicalSlug = entry.aliasOf;
|
|
1401
|
+
const aliasPresent = catalog.models[aliasSlug] !== void 0;
|
|
1402
|
+
const canonicalPresent = catalog.models[canonicalSlug] !== void 0;
|
|
1403
|
+
if (aliasPresent && !canonicalPresent) {
|
|
1404
|
+
targets[canonicalSlug] = aliasSlug;
|
|
1405
|
+
delete targets[aliasSlug];
|
|
1406
|
+
} else if (aliasPresent && canonicalPresent) {
|
|
1407
|
+
delete targets[aliasSlug];
|
|
1408
|
+
delete targets[canonicalSlug];
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return targets;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// src/config/enrich-catalog.ts
|
|
1415
|
+
function enrichOne(slug2, catalogModel, modelsDev, env) {
|
|
1416
|
+
const sources = { modelsDev, env };
|
|
1417
|
+
if (catalogModel !== void 0) sources.catalogModel = catalogModel;
|
|
1418
|
+
return enrichModel(slug2, sources);
|
|
1419
|
+
}
|
|
1420
|
+
function enrichCatalog(catalog, modelsDev, env) {
|
|
1421
|
+
const enriched = {};
|
|
1422
|
+
for (const slug2 of Object.keys(catalog.models)) {
|
|
1423
|
+
enriched[slug2] = enrichOne(slug2, catalog.models[slug2], modelsDev, env);
|
|
1424
|
+
}
|
|
1425
|
+
for (const entry of SEED) {
|
|
1426
|
+
if (entry.aliasOf === void 0) continue;
|
|
1427
|
+
const aliasSlug = entry.slug;
|
|
1428
|
+
const canonicalSlug = entry.aliasOf;
|
|
1429
|
+
const aliasPresent = enriched[aliasSlug] !== void 0;
|
|
1430
|
+
const canonicalPresent = enriched[canonicalSlug] !== void 0;
|
|
1431
|
+
if (aliasPresent === canonicalPresent) continue;
|
|
1432
|
+
if (canonicalPresent) {
|
|
1433
|
+
enriched[aliasSlug] = enrichOne(
|
|
1434
|
+
aliasSlug,
|
|
1435
|
+
catalog.models[canonicalSlug],
|
|
1436
|
+
modelsDev,
|
|
1437
|
+
env
|
|
1438
|
+
);
|
|
1439
|
+
} else {
|
|
1440
|
+
enriched[canonicalSlug] = enrichOne(
|
|
1441
|
+
canonicalSlug,
|
|
1442
|
+
catalog.models[aliasSlug],
|
|
1443
|
+
modelsDev,
|
|
1444
|
+
env
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return enriched;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// src/config/merge-models.ts
|
|
1452
|
+
function cloneEntry(entry) {
|
|
1453
|
+
return { ...entry };
|
|
1454
|
+
}
|
|
1455
|
+
function cloneModels(patch) {
|
|
1456
|
+
const models = {};
|
|
1457
|
+
for (const [key, entry] of Object.entries(patch.models)) {
|
|
1458
|
+
models[key] = cloneEntry(entry);
|
|
1459
|
+
}
|
|
1460
|
+
return models;
|
|
1461
|
+
}
|
|
1462
|
+
function fillModelEntry(existing, patchEntry) {
|
|
1463
|
+
for (const [field, value] of Object.entries(patchEntry)) {
|
|
1464
|
+
if (value === void 0) continue;
|
|
1465
|
+
if (existing[field] === void 0) existing[field] = value;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
function mergeModels(target, patch) {
|
|
1469
|
+
if (target.models === void 0) {
|
|
1470
|
+
target.models = cloneModels(patch);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
const models = target.models;
|
|
1474
|
+
for (const [key, patchEntry] of Object.entries(patch.models)) {
|
|
1475
|
+
const existing = models[key];
|
|
1476
|
+
if (existing === void 0) {
|
|
1477
|
+
models[key] = cloneEntry(patchEntry);
|
|
1478
|
+
} else {
|
|
1479
|
+
fillModelEntry(existing, cloneEntry(patchEntry));
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// src/config/merge-provider.ts
|
|
1485
|
+
var FORCED_OPTION_FIELDS = /* @__PURE__ */ new Set([
|
|
1486
|
+
"apiKey",
|
|
1487
|
+
"baseURL",
|
|
1488
|
+
"headers"
|
|
1489
|
+
]);
|
|
1490
|
+
function mergeProvider(target, patch) {
|
|
1491
|
+
if (target.npm === void 0) target.npm = patch.npm;
|
|
1492
|
+
if (target.name === void 0) target.name = patch.name;
|
|
1493
|
+
if (target.options === void 0) {
|
|
1494
|
+
target.options = { ...patch.options };
|
|
1495
|
+
} else {
|
|
1496
|
+
const options = target.options;
|
|
1497
|
+
for (const [field, value] of Object.entries(patch.options)) {
|
|
1498
|
+
if (value === void 0) continue;
|
|
1499
|
+
if (FORCED_OPTION_FIELDS.has(field) || options[field] === void 0)
|
|
1500
|
+
options[field] = value;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
mergeModels(target, patch);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// src/config/merge-config.ts
|
|
1507
|
+
function mergeConfig(cfg, blocks) {
|
|
1508
|
+
if (cfg.provider === void 0) cfg.provider = {};
|
|
1509
|
+
const provider = cfg.provider;
|
|
1510
|
+
for (const [providerID, patch] of Object.entries(blocks)) {
|
|
1511
|
+
const target = provider[providerID] ?? {};
|
|
1512
|
+
provider[providerID] = target;
|
|
1513
|
+
mergeProvider(target, patch);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// src/config/collected-ref.ts
|
|
1518
|
+
function parseRef(ref, source) {
|
|
1519
|
+
const sep2 = ref.indexOf("/");
|
|
1520
|
+
if (sep2 <= 0 || sep2 >= ref.length - 1) return void 0;
|
|
1521
|
+
const providerId = ref.slice(0, sep2);
|
|
1522
|
+
const suffix = ref.slice(sep2 + 1);
|
|
1523
|
+
if (providerId === "route_33")
|
|
1524
|
+
return { providerId, modelKey: suffix, ref, source };
|
|
1525
|
+
if (providerId !== "route_33_anthropic") return void 0;
|
|
1526
|
+
return {
|
|
1527
|
+
providerId: "route_33",
|
|
1528
|
+
modelKey: `anthropic/${suffix}`,
|
|
1529
|
+
ref,
|
|
1530
|
+
source,
|
|
1531
|
+
legacyReplacement: `route_33/anthropic/${suffix}`
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
function pushRef(refs, value, source) {
|
|
1535
|
+
if (typeof value !== "string") return;
|
|
1536
|
+
const parsed = parseRef(value, source);
|
|
1537
|
+
if (parsed) refs.push(parsed);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// src/config/collect-cfg-refs.ts
|
|
1541
|
+
function collectCfgRefs(cfg) {
|
|
1542
|
+
const refs = [];
|
|
1543
|
+
pushRef(refs, cfg.model, 'opencode config "model"');
|
|
1544
|
+
pushRef(refs, cfg.small_model, 'opencode config "small_model"');
|
|
1545
|
+
for (const [name, agent] of Object.entries(cfg.agent ?? {})) {
|
|
1546
|
+
pushRef(refs, agent?.model, `opencode agent "${name}"`);
|
|
1547
|
+
}
|
|
1548
|
+
return refs;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// src/config/collect-flavour-refs.ts
|
|
1552
|
+
import * as fs8 from "fs";
|
|
1553
|
+
import * as os5 from "os";
|
|
1554
|
+
import * as path10 from "path";
|
|
1555
|
+
import { parse as parseJsonc2 } from "jsonc-parser";
|
|
1556
|
+
function isRecord8(value) {
|
|
1557
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1558
|
+
}
|
|
1559
|
+
function findFlavourPath(env, cwd) {
|
|
1560
|
+
const candidates = [];
|
|
1561
|
+
const configDir = env.OPENCODE_CONFIG_DIR?.trim();
|
|
1562
|
+
if (configDir) candidates.push(path10.join(configDir, "oh-my-openagent.jsonc"));
|
|
1563
|
+
candidates.push(path10.join(cwd, ".opencode", "oh-my-openagent.jsonc"));
|
|
1564
|
+
const configHome = env.XDG_CONFIG_HOME?.trim() || path10.join(os5.homedir(), ".config");
|
|
1565
|
+
candidates.push(path10.join(configHome, "opencode", "oh-my-openagent.jsonc"));
|
|
1566
|
+
for (const candidate of candidates) {
|
|
1567
|
+
try {
|
|
1568
|
+
if (fs8.existsSync(candidate)) return candidate;
|
|
1569
|
+
} catch {
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return void 0;
|
|
1573
|
+
}
|
|
1574
|
+
function collectSectionRefs(refs, section, label) {
|
|
1575
|
+
if (!isRecord8(section)) return;
|
|
1576
|
+
for (const [name, entry] of Object.entries(section)) {
|
|
1577
|
+
if (!isRecord8(entry)) continue;
|
|
1578
|
+
pushRef(refs, entry.model, `flavour ${label} "${name}"`);
|
|
1579
|
+
if (label === "agent" && isRecord8(entry.ultrawork)) {
|
|
1580
|
+
pushRef(
|
|
1581
|
+
refs,
|
|
1582
|
+
entry.ultrawork.model,
|
|
1583
|
+
`flavour ${label} "${name}" (ultrawork)`
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function collectFlavourRefs(env, cwd) {
|
|
1589
|
+
const flavourPath = findFlavourPath(env, cwd);
|
|
1590
|
+
if (flavourPath === void 0) return [];
|
|
1591
|
+
const refs = [];
|
|
1592
|
+
try {
|
|
1593
|
+
const parsed = parseJsonc2(fs8.readFileSync(flavourPath, "utf8"));
|
|
1594
|
+
if (!isRecord8(parsed)) {
|
|
1595
|
+
logger.warn(
|
|
1596
|
+
`flavour file ${flavourPath} did not parse to an object \u2014 skipping flavour ref validation`
|
|
1597
|
+
);
|
|
1598
|
+
return [];
|
|
1599
|
+
}
|
|
1600
|
+
collectSectionRefs(refs, parsed.agents, "agent");
|
|
1601
|
+
collectSectionRefs(refs, parsed.categories, "category");
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
logger.warn(
|
|
1604
|
+
`flavour file ${flavourPath} could not be read \u2014 skipping flavour ref validation: ${String(error)}`
|
|
1605
|
+
);
|
|
1606
|
+
return [];
|
|
1607
|
+
}
|
|
1608
|
+
return refs;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/config/describe-missing.ts
|
|
1612
|
+
function availableKeys(cfg, providerId) {
|
|
1613
|
+
return Object.keys(cfg.provider?.[providerId]?.models ?? {});
|
|
1614
|
+
}
|
|
1615
|
+
function describeMissing(cfg, missing) {
|
|
1616
|
+
const sections = missing.map((entry) => {
|
|
1617
|
+
const available = availableKeys(cfg, entry.providerId).sort().map((key) => `${entry.providerId}/${key}`).join(", ");
|
|
1618
|
+
return [
|
|
1619
|
+
` ${entry.ref}`,
|
|
1620
|
+
` referenced by: ${entry.sources.join(", ")}`,
|
|
1621
|
+
...entry.legacyReplacement === void 0 ? [] : [` rename to "${entry.legacyReplacement}"`],
|
|
1622
|
+
` available on ${entry.providerId}: ${available || "(none)"}`
|
|
1623
|
+
].join("\n");
|
|
1624
|
+
});
|
|
1625
|
+
return `[route_33] ${missing.length} unresolvable model reference(s):
|
|
1626
|
+
|
|
1627
|
+
${sections.join("\n\n")}`;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/config/find-missing-refs.ts
|
|
1631
|
+
function findMissingRefs(cfg, refs) {
|
|
1632
|
+
const byRef = /* @__PURE__ */ new Map();
|
|
1633
|
+
for (const ref of refs) {
|
|
1634
|
+
const models = cfg.provider?.[ref.providerId]?.models;
|
|
1635
|
+
if (ref.legacyReplacement === void 0 && models !== void 0 && models[ref.modelKey] !== void 0)
|
|
1636
|
+
continue;
|
|
1637
|
+
const existing = byRef.get(ref.ref);
|
|
1638
|
+
if (existing === void 0) {
|
|
1639
|
+
const missing = {
|
|
1640
|
+
providerId: ref.providerId,
|
|
1641
|
+
modelKey: ref.modelKey,
|
|
1642
|
+
ref: ref.ref,
|
|
1643
|
+
sources: [ref.source]
|
|
1644
|
+
};
|
|
1645
|
+
if (ref.legacyReplacement !== void 0)
|
|
1646
|
+
missing.legacyReplacement = ref.legacyReplacement;
|
|
1647
|
+
byRef.set(ref.ref, missing);
|
|
1648
|
+
} else if (!existing.sources.includes(ref.source)) {
|
|
1649
|
+
existing.sources.push(ref.source);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
return [...byRef.values()].sort((a, b) => a.ref.localeCompare(b.ref));
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// src/config/inject-stubs.ts
|
|
1656
|
+
function injectStubs(cfg, missing) {
|
|
1657
|
+
if (cfg.provider === void 0) cfg.provider = {};
|
|
1658
|
+
for (const entry of missing) {
|
|
1659
|
+
const provider = cfg.provider[entry.providerId] ?? {};
|
|
1660
|
+
cfg.provider[entry.providerId] = provider;
|
|
1661
|
+
const models = provider.models ?? {};
|
|
1662
|
+
provider.models = models;
|
|
1663
|
+
models[entry.modelKey] = { name: entry.modelKey };
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// src/config/validation-error.ts
|
|
1668
|
+
var Route33ValidationError = class extends Error {
|
|
1669
|
+
missingRefs;
|
|
1670
|
+
constructor(message, missingRefs) {
|
|
1671
|
+
super(message);
|
|
1672
|
+
this.name = "Route33ValidationError";
|
|
1673
|
+
this.missingRefs = missingRefs;
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
// src/config/validate-refs.ts
|
|
1678
|
+
var REMEDIATION = [
|
|
1679
|
+
"Remediation:",
|
|
1680
|
+
` 1. Set ${ENV_STRICT}=0 to downgrade this error to a warning (missing models get a name-only stub entry).`,
|
|
1681
|
+
' 2. Declare the model explicitly under "provider" in opencode.jsonc.',
|
|
1682
|
+
" 3. Verify the resolved API key has access to this model \u2014 the R33 catalog is key-scoped."
|
|
1683
|
+
].join("\n");
|
|
1684
|
+
function validateRefs(cfg, options = {}) {
|
|
1685
|
+
const env = options.env ?? process.env;
|
|
1686
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1687
|
+
const refs = [...collectCfgRefs(cfg), ...collectFlavourRefs(env, cwd)];
|
|
1688
|
+
const missing = findMissingRefs(cfg, refs);
|
|
1689
|
+
if (missing.length === 0) return;
|
|
1690
|
+
const details = describeMissing(cfg, missing);
|
|
1691
|
+
const missingIds = missing.map((entry) => entry.ref);
|
|
1692
|
+
if (missing.some((entry) => entry.legacyReplacement !== void 0)) {
|
|
1693
|
+
throw new Route33ValidationError(details, missingIds);
|
|
1694
|
+
}
|
|
1695
|
+
if (env[ENV_STRICT] === "0") {
|
|
1696
|
+
logger.warn(
|
|
1697
|
+
`${details}
|
|
1698
|
+
|
|
1699
|
+
(${ENV_STRICT}=0 \u2014 continuing with name-only stub entries)`
|
|
1700
|
+
);
|
|
1701
|
+
injectStubs(cfg, missing);
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (options.keyResolved === false) {
|
|
1705
|
+
logger.warn(
|
|
1706
|
+
`no route_33 API key resolved \u2014 catalog is seed-only; ${missing.length} referenced model(s) are unavailable until a key is configured: ${missingIds.join(", ")}. Enter a key via the project-key prompt, \`opencode auth login\`, ${ENV_API_KEY}, or a provider.route_33.options.apiKey literal, then restart. (Continuing with name-only stub entries.)`
|
|
1707
|
+
);
|
|
1708
|
+
injectStubs(cfg, missing);
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
throw new Route33ValidationError(`${details}
|
|
1712
|
+
|
|
1713
|
+
${REMEDIATION}`, missingIds);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/config/config-hook.ts
|
|
1717
|
+
function describeError(error) {
|
|
1718
|
+
return error instanceof Error ? error.message : String(error);
|
|
1719
|
+
}
|
|
1720
|
+
function createConfigHook(options = {}) {
|
|
1721
|
+
const env = options.env ?? process.env;
|
|
1722
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1723
|
+
const fetchImpl = options.fetchImpl;
|
|
1724
|
+
return async (cfg) => {
|
|
1725
|
+
let key;
|
|
1726
|
+
try {
|
|
1727
|
+
key = resolveKey(cfg, env, cwd);
|
|
1728
|
+
const baseURL = resolveBaseUrl(cfg, env);
|
|
1729
|
+
const catalog = await getCatalog({
|
|
1730
|
+
key,
|
|
1731
|
+
baseURL,
|
|
1732
|
+
seed: SEED,
|
|
1733
|
+
env,
|
|
1734
|
+
fetchImpl
|
|
1735
|
+
});
|
|
1736
|
+
const modelsDevOptions = {
|
|
1737
|
+
cacheDir: resolveCacheDir(env)
|
|
1738
|
+
};
|
|
1739
|
+
if (fetchImpl !== void 0) modelsDevOptions.fetchImpl = fetchImpl;
|
|
1740
|
+
const modelsDev = await loadModelsDev(modelsDevOptions);
|
|
1741
|
+
const enriched = enrichCatalog(catalog, modelsDev, env);
|
|
1742
|
+
const blocks = buildProviderBlocks(enriched, {
|
|
1743
|
+
apiKey: key,
|
|
1744
|
+
baseURL,
|
|
1745
|
+
aliasTargets: buildAliasTargets(catalog),
|
|
1746
|
+
// Per-request resolution for the live-auth fetch: a prompt-entered
|
|
1747
|
+
// key applies to the very next request, no restart. The static
|
|
1748
|
+
// apiKey above stays the boot-time/catalog path.
|
|
1749
|
+
resolveKeyNow: () => resolveKey(void 0, env, cwd)
|
|
1750
|
+
});
|
|
1751
|
+
mergeConfig(cfg, blocks);
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
logger.error(
|
|
1754
|
+
`model discovery/injection failed unexpectedly \u2014 opencode startup continues without injected models: ${describeError(error)}`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
try {
|
|
1758
|
+
validateRefs(cfg, { env, cwd, keyResolved: key !== void 0 });
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
if (error instanceof Route33ValidationError) {
|
|
1761
|
+
logger.error(error.message);
|
|
1762
|
+
throw error;
|
|
1763
|
+
}
|
|
1764
|
+
logger.error(
|
|
1765
|
+
`ref validation failed unexpectedly \u2014 skipping: ${describeError(error)}`
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// src/index.ts
|
|
1772
|
+
var Route33Plugin = async (input, _options) => {
|
|
1773
|
+
ensureTuiRegistration({
|
|
1774
|
+
worktree: input?.worktree ?? input?.directory,
|
|
1775
|
+
env: process.env,
|
|
1776
|
+
moduleUrl: import.meta.url
|
|
1777
|
+
});
|
|
1778
|
+
ensureWorkspaceKeyGitignore(input?.worktree ?? input?.directory);
|
|
1779
|
+
return {
|
|
1780
|
+
config: createConfigHook(),
|
|
1781
|
+
auth: buildRoute33Auth(process.env, input?.worktree ?? input?.directory)
|
|
1782
|
+
};
|
|
1783
|
+
};
|
|
1784
|
+
export {
|
|
1785
|
+
Route33Plugin
|
|
1786
|
+
};
|