@route_33/opencode 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -4
- package/dist/index.d.ts +7 -0
- package/dist/index.js +55 -269
- package/dist/tui/index.js +80 -18
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -4,17 +4,31 @@ An opencode provider plugin for the [route33.ai](https://route33.ai) AI gateway:
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
opencode plugin @route_33/opencode
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The installer patches both configs: the server plugin into `opencode.json` and the key-setup wizard (TUI plugin) into `tui.json`. To install manually instead, declare both entries yourself:
|
|
8
12
|
|
|
9
|
-
```
|
|
13
|
+
```jsonc
|
|
14
|
+
// opencode.json — server plugin (model discovery, cost/limit enrichment, auth)
|
|
10
15
|
{
|
|
11
|
-
"plugin": ["@route_33/opencode@^0.
|
|
16
|
+
"plugin": ["@route_33/opencode@^0.2"]
|
|
12
17
|
}
|
|
13
18
|
```
|
|
14
19
|
|
|
20
|
+
```jsonc
|
|
21
|
+
// tui.json — TUI plugin (the key-setup wizard)
|
|
22
|
+
{
|
|
23
|
+
"plugin": ["@route_33/opencode@^0.2"]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This plugin never writes your config: opencode has no TUI-plugin auto-discovery, so without a `tui.json` entry the wizard simply never appears — the server plugin works regardless.
|
|
28
|
+
|
|
15
29
|
## Auth
|
|
16
30
|
|
|
17
|
-
Run `opencode auth login` and choose `route_33`, or set the `ROUTE33_API_KEY` environment variable. Without a key, the TUI wizard offers to save one: per-project inside a git workspace, global otherwise. A globally stored key applies only outside git workspaces — inside a workspace, keys resolve from the environment, the workspace config, or `.opencode/route33.key` (per-project keys are the metering boundary). `ROUTE33_BASE_URL` overrides the default gateway URL; `ROUTE33_TUI_PROMPT=0` disables the wizard.
|
|
31
|
+
Run `opencode auth login` and choose `route_33`, or set the `ROUTE33_API_KEY` environment variable. Without a key, the TUI wizard offers to save one: per-project inside a git workspace, global otherwise. A globally stored key applies only outside git workspaces — inside a workspace, keys resolve from the environment, the workspace config, or `.opencode/route33.key` (per-project keys are the metering boundary). Skipping the project-key wizard snoozes it for 7 days — "Skip" for that workspace only, "Skip for all workspaces" everywhere (both live in opencode's TUI kv store; remove `project-key-prompt.dismissed-at*` from `~/.local/state/opencode/kv.json` to re-arm sooner). `ROUTE33_BASE_URL` overrides the default gateway URL; `ROUTE33_TUI_PROMPT=0` disables the wizard.
|
|
18
32
|
|
|
19
33
|
## License
|
|
20
34
|
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ import { Plugin } from '@opencode-ai/plugin';
|
|
|
10
10
|
* validation — plus the `route_33` auth method and options loader.
|
|
11
11
|
* Discovery runs once at startup; restart opencode after a key or config
|
|
12
12
|
* change.
|
|
13
|
+
*
|
|
14
|
+
* The companion TUI plugin (`exports["./tui"]`, the key-setup wizard) is
|
|
15
|
+
* installed by the USER declaring it in `tui.json` — opencode has no TUI
|
|
16
|
+
* plugin auto-discovery, and this plugin never writes user config
|
|
17
|
+
* (upstream calls third-party tui.json mutation undesirable). The
|
|
18
|
+
* sanctioned install is `opencode plugin @route_33/opencode`, which
|
|
19
|
+
* patches both configs.
|
|
13
20
|
*/
|
|
14
21
|
declare const Route33Plugin: Plugin;
|
|
15
22
|
|
package/dist/index.js
CHANGED
|
@@ -245,215 +245,6 @@ function buildRoute33Auth(env = process.env, worktree) {
|
|
|
245
245
|
};
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
// src/bootstrap/ensure-tui-registration.ts
|
|
249
|
-
import * as path8 from "path";
|
|
250
|
-
|
|
251
|
-
// src/bootstrap/global-context.ts
|
|
252
|
-
import * as os3 from "os";
|
|
253
|
-
import * as path5 from "path";
|
|
254
|
-
import { fileURLToPath } from "url";
|
|
255
|
-
function isGlobalContext(worktree, moduleUrl) {
|
|
256
|
-
try {
|
|
257
|
-
if (moduleUrl === void 0) return false;
|
|
258
|
-
if (worktree === void 0) return true;
|
|
259
|
-
const root = path5.resolve(worktree);
|
|
260
|
-
if (root === "/") return true;
|
|
261
|
-
const modulePath = fileURLToPath(moduleUrl);
|
|
262
|
-
return !modulePath.startsWith(root + path5.sep);
|
|
263
|
-
} catch {
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
function usable5(value) {
|
|
268
|
-
const trimmed = value?.trim();
|
|
269
|
-
return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
|
|
270
|
-
}
|
|
271
|
-
function globalTuiJsonPath(env) {
|
|
272
|
-
const configHome = usable5(env.XDG_CONFIG_HOME) ?? path5.join(os3.homedir(), ".config");
|
|
273
|
-
return path5.join(configHome, "opencode", "tui.json");
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// src/bootstrap/tui-json-upsert.ts
|
|
277
|
-
import * as fs4 from "fs";
|
|
278
|
-
import * as path7 from "path";
|
|
279
|
-
import {
|
|
280
|
-
applyEdits,
|
|
281
|
-
modify,
|
|
282
|
-
parse as parseJsonc
|
|
283
|
-
} from "jsonc-parser";
|
|
284
|
-
|
|
285
|
-
// src/bootstrap/tui-spec-resolution.ts
|
|
286
|
-
import * as fs3 from "fs";
|
|
287
|
-
import * as os4 from "os";
|
|
288
|
-
import * as path6 from "path";
|
|
289
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
290
|
-
var NPM_TUI_SPEC = "@route_33/opencode";
|
|
291
|
-
var DEAD_SUBPATH_TUI_SPEC = "@route_33/opencode/tui";
|
|
292
|
-
var OLD_NPM_TUI_SPEC = "opencode-route33/tui";
|
|
293
|
-
var TUI_BUNDLE_NAME = "route33-tui.js";
|
|
294
|
-
var TUI_SCHEMA = "https://opencode.ai/tui.json";
|
|
295
|
-
var SERVER_BUNDLE_NAME = "route33.js";
|
|
296
|
-
var DIST_TUI_ENTRY_SUFFIX = "/opencode-route33/dist/tui/index.js";
|
|
297
|
-
var SCOPED_DIST_TUI_ENTRY_SUFFIX = "/@route_33/opencode/dist/tui/index.js";
|
|
298
|
-
var NPM_PACKAGE_DIR = path6.join("node_modules", "@route_33", "opencode");
|
|
299
|
-
function fileExists(candidate) {
|
|
300
|
-
try {
|
|
301
|
-
return fs3.statSync(candidate).isFile();
|
|
302
|
-
} catch {
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
function npmSpecFromModulePath(selfPath) {
|
|
307
|
-
const segments = selfPath.split(path6.sep);
|
|
308
|
-
for (let i = 0; i < segments.length - 1; i++) {
|
|
309
|
-
const pkgDir = segments[i + 1];
|
|
310
|
-
if (segments[i] === "@route_33" && pkgDir !== void 0 && pkgDir.startsWith("opencode@")) {
|
|
311
|
-
const pin = pkgDir.slice("opencode@".length);
|
|
312
|
-
if (pin !== "") return `${NPM_TUI_SPEC}@${pin}`;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return selfPath.includes(`${NPM_PACKAGE_DIR}${path6.sep}`) ? NPM_TUI_SPEC : void 0;
|
|
316
|
-
}
|
|
317
|
-
function specFromModuleUrl(moduleUrl) {
|
|
318
|
-
try {
|
|
319
|
-
if (moduleUrl === void 0) return void 0;
|
|
320
|
-
const selfPath = fileURLToPath2(moduleUrl);
|
|
321
|
-
const dir = path6.dirname(selfPath);
|
|
322
|
-
if (path6.basename(selfPath) === SERVER_BUNDLE_NAME) {
|
|
323
|
-
const sibling = path6.join(dir, TUI_BUNDLE_NAME);
|
|
324
|
-
return fileExists(sibling) ? sibling : void 0;
|
|
325
|
-
}
|
|
326
|
-
if (path6.basename(selfPath) === "index.js" && path6.basename(dir) === "dist") {
|
|
327
|
-
const npmSpec = npmSpecFromModulePath(selfPath);
|
|
328
|
-
if (npmSpec !== void 0) return npmSpec;
|
|
329
|
-
const tuiEntry = path6.join(dir, "tui", "index.js");
|
|
330
|
-
return fileExists(tuiEntry) ? tuiEntry : void 0;
|
|
331
|
-
}
|
|
332
|
-
return void 0;
|
|
333
|
-
} catch {
|
|
334
|
-
return void 0;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
function resolveTuiSpec(worktree, env, moduleUrl) {
|
|
338
|
-
const fromModule = specFromModuleUrl(moduleUrl);
|
|
339
|
-
if (fromModule !== void 0) return fromModule;
|
|
340
|
-
if (worktree !== void 0 && worktree.trim() !== "" && path6.resolve(worktree) !== "/") {
|
|
341
|
-
const local = path6.join(worktree, ".opencode", "plugins", TUI_BUNDLE_NAME);
|
|
342
|
-
if (fileExists(local)) return local;
|
|
343
|
-
}
|
|
344
|
-
const configHome = env.XDG_CONFIG_HOME?.trim() || path6.join(os4.homedir(), ".config");
|
|
345
|
-
const global = path6.join(configHome, "opencode", "plugins", TUI_BUNDLE_NAME);
|
|
346
|
-
if (fileExists(global)) return global;
|
|
347
|
-
return NPM_TUI_SPEC;
|
|
348
|
-
}
|
|
349
|
-
function isOurEntry(entry) {
|
|
350
|
-
return typeof entry === "string" && (entry === NPM_TUI_SPEC || entry.startsWith(`${NPM_TUI_SPEC}@`) || entry === DEAD_SUBPATH_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));
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// src/bootstrap/tui-json-upsert.ts
|
|
354
|
-
function isRecord3(value) {
|
|
355
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
356
|
-
}
|
|
357
|
-
function fileExists2(candidate) {
|
|
358
|
-
try {
|
|
359
|
-
return fs4.statSync(candidate).isFile();
|
|
360
|
-
} catch {
|
|
361
|
-
return false;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
function upsertTuiSpec(file, spec) {
|
|
365
|
-
try {
|
|
366
|
-
if (!fileExists2(file)) {
|
|
367
|
-
fs4.mkdirSync(path7.dirname(file), { recursive: true });
|
|
368
|
-
fs4.writeFileSync(
|
|
369
|
-
file,
|
|
370
|
-
JSON.stringify({ $schema: TUI_SCHEMA, plugin: [spec] }, null, 2) + "\n",
|
|
371
|
-
"utf8"
|
|
372
|
-
);
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
const text = fs4.readFileSync(file, "utf8");
|
|
376
|
-
const errors = [];
|
|
377
|
-
const parsed = parseJsonc(text, errors, {
|
|
378
|
-
allowTrailingComma: true,
|
|
379
|
-
allowEmptyContent: true
|
|
380
|
-
});
|
|
381
|
-
if (!isRecord3(parsed)) {
|
|
382
|
-
logger.warn(
|
|
383
|
-
`tui.json at ${file} did not parse to an object \u2014 skipping TUI registration`
|
|
384
|
-
);
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
if (parsed.plugin !== void 0 && !Array.isArray(parsed.plugin)) {
|
|
388
|
-
logger.warn(
|
|
389
|
-
`tui.json at ${file} has a non-array "plugin" key \u2014 skipping TUI registration`
|
|
390
|
-
);
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
const entries = Array.isArray(parsed.plugin) ? parsed.plugin : [];
|
|
394
|
-
const ourIndices = entries.flatMap(
|
|
395
|
-
(entry, index) => isOurEntry(entry) ? [index] : []
|
|
396
|
-
);
|
|
397
|
-
const firstOurs = ourIndices[0];
|
|
398
|
-
if (ourIndices.length === 1 && firstOurs !== void 0 && entries[firstOurs] === spec)
|
|
399
|
-
return;
|
|
400
|
-
const formattingOptions = { insertSpaces: true, tabSize: 2 };
|
|
401
|
-
let next = text;
|
|
402
|
-
if (!Array.isArray(parsed.plugin)) {
|
|
403
|
-
next = applyEdits(
|
|
404
|
-
next,
|
|
405
|
-
modify(next, ["plugin"], [spec], { formattingOptions })
|
|
406
|
-
);
|
|
407
|
-
} else if (firstOurs === void 0) {
|
|
408
|
-
next = applyEdits(
|
|
409
|
-
next,
|
|
410
|
-
modify(next, ["plugin", -1], spec, {
|
|
411
|
-
formattingOptions,
|
|
412
|
-
isArrayInsertion: true
|
|
413
|
-
})
|
|
414
|
-
);
|
|
415
|
-
} else {
|
|
416
|
-
if (entries[firstOurs] !== spec) {
|
|
417
|
-
next = applyEdits(
|
|
418
|
-
next,
|
|
419
|
-
modify(next, ["plugin", firstOurs], spec, { formattingOptions })
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
for (const index of ourIndices.slice(1).sort((a, b) => b - a)) {
|
|
423
|
-
next = applyEdits(
|
|
424
|
-
next,
|
|
425
|
-
modify(next, ["plugin", index], void 0, { formattingOptions })
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
if (next !== text) fs4.writeFileSync(file, next, "utf8");
|
|
430
|
-
} catch (error) {
|
|
431
|
-
logger.warn(
|
|
432
|
-
`could not register the route33 TUI plugin in tui.json: ${String(error)}`
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// src/bootstrap/ensure-tui-registration.ts
|
|
438
|
-
function ensureTuiRegistration(deps) {
|
|
439
|
-
try {
|
|
440
|
-
const { worktree, env } = deps;
|
|
441
|
-
if (env.ROUTE33_TUI_PROMPT === "0") return;
|
|
442
|
-
const usableWorktree = worktree !== void 0 && worktree.trim() !== "" && path8.resolve(worktree) !== "/" ? worktree : void 0;
|
|
443
|
-
const spec = resolveTuiSpec(usableWorktree, env, deps.moduleUrl);
|
|
444
|
-
if (usableWorktree !== void 0) {
|
|
445
|
-
upsertTuiSpec(path8.join(usableWorktree, ".opencode", "tui.json"), spec);
|
|
446
|
-
}
|
|
447
|
-
if (isGlobalContext(worktree, deps.moduleUrl)) {
|
|
448
|
-
upsertTuiSpec(globalTuiJsonPath(env), spec);
|
|
449
|
-
}
|
|
450
|
-
} catch (error) {
|
|
451
|
-
logger.warn(
|
|
452
|
-
`could not register the route33 TUI plugin in tui.json: ${String(error)}`
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
248
|
// src/config/live-auth-fetch.ts
|
|
458
249
|
function buildLiveAuthFetch(resolveKeyNow, innerFetch) {
|
|
459
250
|
return (input, init) => {
|
|
@@ -595,24 +386,24 @@ function hashKey(key) {
|
|
|
595
386
|
}
|
|
596
387
|
|
|
597
388
|
// src/catalog/catalog-cache/read-disk.ts
|
|
598
|
-
import * as
|
|
389
|
+
import * as fs3 from "fs";
|
|
599
390
|
|
|
600
391
|
// src/catalog/catalog-cache/cache-file-path.ts
|
|
601
|
-
import * as
|
|
392
|
+
import * as path5 from "path";
|
|
602
393
|
function cacheFilePath(keyHash, env) {
|
|
603
|
-
return
|
|
394
|
+
return path5.join(resolveCacheDir(env), `catalog-${keyHash}.json`);
|
|
604
395
|
}
|
|
605
396
|
|
|
606
397
|
// src/catalog/catalog-cache/read-disk.ts
|
|
607
|
-
function
|
|
398
|
+
function isRecord3(value) {
|
|
608
399
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
609
400
|
}
|
|
610
401
|
function readDisk(keyHash, env) {
|
|
611
402
|
try {
|
|
612
403
|
const parsed = JSON.parse(
|
|
613
|
-
|
|
404
|
+
fs3.readFileSync(cacheFilePath(keyHash, env), "utf8")
|
|
614
405
|
);
|
|
615
|
-
if (!
|
|
406
|
+
if (!isRecord3(parsed) || typeof parsed.fetchedAt !== "number" || !isRecord3(parsed.models))
|
|
616
407
|
return void 0;
|
|
617
408
|
return {
|
|
618
409
|
fetchedAt: parsed.fetchedAt,
|
|
@@ -654,15 +445,15 @@ function readStaleCatalog(opts) {
|
|
|
654
445
|
}
|
|
655
446
|
|
|
656
447
|
// src/catalog/catalog-cache/write-catalog.ts
|
|
657
|
-
import * as
|
|
448
|
+
import * as fs4 from "fs";
|
|
658
449
|
function writeCatalog(opts) {
|
|
659
450
|
const { key, models, env = process.env, now = Date.now() } = opts;
|
|
660
451
|
const keyHash = hashKey(key);
|
|
661
452
|
const record = { fetchedAt: now, keyHash, models };
|
|
662
453
|
memory.set(keyHash, record);
|
|
663
454
|
try {
|
|
664
|
-
|
|
665
|
-
|
|
455
|
+
fs4.mkdirSync(resolveCacheDir(env), { recursive: true });
|
|
456
|
+
fs4.writeFileSync(cacheFilePath(keyHash, env), JSON.stringify(record));
|
|
666
457
|
} catch (error) {
|
|
667
458
|
logger.warn(
|
|
668
459
|
`could not persist catalog cache: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -686,7 +477,7 @@ var CatalogFetchError = class extends Error {
|
|
|
686
477
|
var DEFAULT_FETCH_TIMEOUT_MS = 5e3;
|
|
687
478
|
|
|
688
479
|
// src/catalog/extract-pricing.ts
|
|
689
|
-
function
|
|
480
|
+
function isRecord4(value) {
|
|
690
481
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
691
482
|
}
|
|
692
483
|
var OPENROUTER_KEYS = [
|
|
@@ -743,7 +534,7 @@ function extractModelsDevPricing(raw) {
|
|
|
743
534
|
return cost;
|
|
744
535
|
}
|
|
745
536
|
function extractPricing(raw) {
|
|
746
|
-
if (!
|
|
537
|
+
if (!isRecord4(raw)) return void 0;
|
|
747
538
|
const hasOpenRouterKeys = OPENROUTER_KEYS.some((key) => key in raw);
|
|
748
539
|
const hasModelsDevKeys = MODELS_DEV_KEYS.some((key) => key in raw);
|
|
749
540
|
if (hasOpenRouterKeys && hasModelsDevKeys) return void 0;
|
|
@@ -774,12 +565,12 @@ function normalizeId(raw, env = process.env) {
|
|
|
774
565
|
}
|
|
775
566
|
|
|
776
567
|
// src/catalog/fetch-catalog.ts
|
|
777
|
-
function
|
|
568
|
+
function isRecord5(value) {
|
|
778
569
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
779
570
|
}
|
|
780
571
|
function parseEntries(payload) {
|
|
781
572
|
if (Array.isArray(payload)) return payload;
|
|
782
|
-
if (
|
|
573
|
+
if (isRecord5(payload) && Array.isArray(payload.data)) return payload.data;
|
|
783
574
|
throw new CatalogFetchError(
|
|
784
575
|
"parse",
|
|
785
576
|
"response is neither {data:[\u2026]} nor a bare array"
|
|
@@ -788,13 +579,13 @@ function parseEntries(payload) {
|
|
|
788
579
|
function toCatalogModels(entries, env) {
|
|
789
580
|
const models = {};
|
|
790
581
|
for (const entry of entries) {
|
|
791
|
-
if (!
|
|
582
|
+
if (!isRecord5(entry) || typeof entry.id !== "string") continue;
|
|
792
583
|
const slug2 = normalizeId(entry.id, env);
|
|
793
584
|
if (!slug2 || models[slug2]) continue;
|
|
794
585
|
const model = { id: slug2 };
|
|
795
586
|
const displayName = typeof entry.display_name === "string" ? entry.display_name.trim() : "";
|
|
796
587
|
if (displayName !== "") model.displayName = displayName;
|
|
797
|
-
if (
|
|
588
|
+
if (isRecord5(entry.pricing) && extractPricing(entry.pricing) !== void 0)
|
|
798
589
|
model.pricing = entry.pricing;
|
|
799
590
|
models[slug2] = model;
|
|
800
591
|
}
|
|
@@ -1203,11 +994,11 @@ function enrichModel(slug2, sources) {
|
|
|
1203
994
|
}
|
|
1204
995
|
|
|
1205
996
|
// src/enrich/models-dev/load-models-dev.ts
|
|
1206
|
-
import * as
|
|
1207
|
-
import * as
|
|
997
|
+
import * as fs5 from "fs";
|
|
998
|
+
import * as path6 from "path";
|
|
1208
999
|
|
|
1209
1000
|
// src/enrich/models-dev/is-record.ts
|
|
1210
|
-
function
|
|
1001
|
+
function isRecord6(value) {
|
|
1211
1002
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1212
1003
|
}
|
|
1213
1004
|
|
|
@@ -1226,7 +1017,7 @@ async function fetchLive(fetchImpl) {
|
|
|
1226
1017
|
return void 0;
|
|
1227
1018
|
}
|
|
1228
1019
|
const body = await response.json();
|
|
1229
|
-
return
|
|
1020
|
+
return isRecord6(body) ? body : void 0;
|
|
1230
1021
|
} catch (error) {
|
|
1231
1022
|
logger.warn("models.dev fetch failed:", error);
|
|
1232
1023
|
return void 0;
|
|
@@ -1243,7 +1034,7 @@ function num(value) {
|
|
|
1243
1034
|
// src/enrich/models-dev/normalize-cost.ts
|
|
1244
1035
|
var EXPRESSIBLE_TIER_SIZE = 2e5;
|
|
1245
1036
|
function parseRateBlock(value) {
|
|
1246
|
-
if (!
|
|
1037
|
+
if (!isRecord6(value)) return void 0;
|
|
1247
1038
|
const input = num(value.input);
|
|
1248
1039
|
const output = num(value.output);
|
|
1249
1040
|
if (input === void 0 || output === void 0) return void 0;
|
|
@@ -1256,7 +1047,7 @@ function parseRateBlock(value) {
|
|
|
1256
1047
|
}
|
|
1257
1048
|
function isExpressibleContextTier(tier) {
|
|
1258
1049
|
const marker = tier.tier;
|
|
1259
|
-
return
|
|
1050
|
+
return isRecord6(marker) && marker.type === "context" && marker.size === EXPRESSIBLE_TIER_SIZE;
|
|
1260
1051
|
}
|
|
1261
1052
|
function maxRate(current, candidate) {
|
|
1262
1053
|
const rate = num(candidate);
|
|
@@ -1264,7 +1055,7 @@ function maxRate(current, candidate) {
|
|
|
1264
1055
|
return current === void 0 || rate > current ? rate : current;
|
|
1265
1056
|
}
|
|
1266
1057
|
function normalizeCost(raw) {
|
|
1267
|
-
if (!
|
|
1058
|
+
if (!isRecord6(raw)) return void 0;
|
|
1268
1059
|
const input = num(raw.input);
|
|
1269
1060
|
const output = num(raw.output);
|
|
1270
1061
|
if (input === void 0 || output === void 0) return void 0;
|
|
@@ -1275,7 +1066,7 @@ function normalizeCost(raw) {
|
|
|
1275
1066
|
if (cacheWrite !== void 0) cost.cache_write = cacheWrite;
|
|
1276
1067
|
const over200k = parseRateBlock(raw.context_over_200k);
|
|
1277
1068
|
if (over200k) cost.context_over_200k = over200k;
|
|
1278
|
-
const tiers = Array.isArray(raw.tiers) ? raw.tiers.filter(
|
|
1069
|
+
const tiers = Array.isArray(raw.tiers) ? raw.tiers.filter(isRecord6) : [];
|
|
1279
1070
|
for (const tier of tiers) {
|
|
1280
1071
|
if (isExpressibleContextTier(tier)) {
|
|
1281
1072
|
if (cost.context_over_200k === void 0) {
|
|
@@ -1305,10 +1096,10 @@ function swapLast(value, from, to) {
|
|
|
1305
1096
|
return index === -1 ? value : `${value.slice(0, index)}${to}${value.slice(index + 1)}`;
|
|
1306
1097
|
}
|
|
1307
1098
|
function probeCandidates(slug2) {
|
|
1308
|
-
const
|
|
1309
|
-
if (
|
|
1310
|
-
const vendor = slug2.slice(0,
|
|
1311
|
-
const model = slug2.slice(
|
|
1099
|
+
const sep = slug2.indexOf("/");
|
|
1100
|
+
if (sep <= 0 || sep >= slug2.length - 1) return [];
|
|
1101
|
+
const vendor = slug2.slice(0, sep);
|
|
1102
|
+
const model = slug2.slice(sep + 1);
|
|
1312
1103
|
const vendors = [vendor];
|
|
1313
1104
|
const alias = VENDOR_ALIASES[vendor];
|
|
1314
1105
|
if (alias !== void 0 && alias !== vendor) vendors.push(alias);
|
|
@@ -1333,7 +1124,7 @@ function probeCandidates(slug2) {
|
|
|
1333
1124
|
|
|
1334
1125
|
// src/enrich/models-dev/lookup.ts
|
|
1335
1126
|
function normalizeLimit(raw) {
|
|
1336
|
-
if (!
|
|
1127
|
+
if (!isRecord6(raw)) return void 0;
|
|
1337
1128
|
const context = num(raw.context);
|
|
1338
1129
|
const output = num(raw.output);
|
|
1339
1130
|
if (context === void 0 || output === void 0) return void 0;
|
|
@@ -1362,9 +1153,9 @@ function buildLookup(model) {
|
|
|
1362
1153
|
function lookupSlug(data, slug2) {
|
|
1363
1154
|
for (const [providerKey, modelKey] of probeCandidates(slug2)) {
|
|
1364
1155
|
const provider = data[providerKey];
|
|
1365
|
-
if (!
|
|
1156
|
+
if (!isRecord6(provider) || !isRecord6(provider.models)) continue;
|
|
1366
1157
|
const model = provider.models[modelKey];
|
|
1367
|
-
if (
|
|
1158
|
+
if (isRecord6(model)) return buildLookup(model);
|
|
1368
1159
|
}
|
|
1369
1160
|
return void 0;
|
|
1370
1161
|
}
|
|
@@ -1373,10 +1164,10 @@ function lookupSlug(data, slug2) {
|
|
|
1373
1164
|
var CACHE_FILE = "models-dev.json";
|
|
1374
1165
|
function readCache(cachePath) {
|
|
1375
1166
|
try {
|
|
1376
|
-
const parsed = JSON.parse(
|
|
1377
|
-
if (!
|
|
1167
|
+
const parsed = JSON.parse(fs5.readFileSync(cachePath, "utf8"));
|
|
1168
|
+
if (!isRecord6(parsed)) return void 0;
|
|
1378
1169
|
const fetchedAt = num(parsed.fetchedAt);
|
|
1379
|
-
if (fetchedAt === void 0 || !
|
|
1170
|
+
if (fetchedAt === void 0 || !isRecord6(parsed.data)) return void 0;
|
|
1380
1171
|
return { fetchedAt, data: parsed.data };
|
|
1381
1172
|
} catch {
|
|
1382
1173
|
return void 0;
|
|
@@ -1384,8 +1175,8 @@ function readCache(cachePath) {
|
|
|
1384
1175
|
}
|
|
1385
1176
|
function writeCache(cachePath, envelope) {
|
|
1386
1177
|
try {
|
|
1387
|
-
|
|
1388
|
-
|
|
1178
|
+
fs5.mkdirSync(path6.dirname(cachePath), { recursive: true });
|
|
1179
|
+
fs5.writeFileSync(cachePath, JSON.stringify(envelope), "utf8");
|
|
1389
1180
|
} catch (error) {
|
|
1390
1181
|
logger.warn("failed to write models.dev cache:", error);
|
|
1391
1182
|
}
|
|
@@ -1398,7 +1189,7 @@ async function loadModelsDev(options = {}) {
|
|
|
1398
1189
|
const cacheDir = options.cacheDir ?? resolveCacheDir();
|
|
1399
1190
|
const ttlMs = options.ttlMs ?? MODELS_DEV_TTL_MS;
|
|
1400
1191
|
const now = options.now ?? Date.now;
|
|
1401
|
-
const cachePath =
|
|
1192
|
+
const cachePath = path6.join(cacheDir, CACHE_FILE);
|
|
1402
1193
|
const cached = readCache(cachePath);
|
|
1403
1194
|
if (cached && now() - cached.fetchedAt < ttlMs)
|
|
1404
1195
|
return makeSource("cache", cached.data);
|
|
@@ -1548,10 +1339,10 @@ function mergeConfig(cfg, blocks) {
|
|
|
1548
1339
|
|
|
1549
1340
|
// src/config/collected-ref.ts
|
|
1550
1341
|
function parseRef(ref, source) {
|
|
1551
|
-
const
|
|
1552
|
-
if (
|
|
1553
|
-
const providerId = ref.slice(0,
|
|
1554
|
-
const suffix = ref.slice(
|
|
1342
|
+
const sep = ref.indexOf("/");
|
|
1343
|
+
if (sep <= 0 || sep >= ref.length - 1) return void 0;
|
|
1344
|
+
const providerId = ref.slice(0, sep);
|
|
1345
|
+
const suffix = ref.slice(sep + 1);
|
|
1555
1346
|
if (providerId === "route_33")
|
|
1556
1347
|
return { providerId, modelKey: suffix, ref, source };
|
|
1557
1348
|
if (providerId !== "route_33_anthropic") return void 0;
|
|
@@ -1581,34 +1372,34 @@ function collectCfgRefs(cfg) {
|
|
|
1581
1372
|
}
|
|
1582
1373
|
|
|
1583
1374
|
// src/config/collect-flavour-refs.ts
|
|
1584
|
-
import * as
|
|
1585
|
-
import * as
|
|
1586
|
-
import * as
|
|
1587
|
-
import { parse as
|
|
1588
|
-
function
|
|
1375
|
+
import * as fs6 from "fs";
|
|
1376
|
+
import * as os3 from "os";
|
|
1377
|
+
import * as path7 from "path";
|
|
1378
|
+
import { parse as parseJsonc } from "jsonc-parser";
|
|
1379
|
+
function isRecord7(value) {
|
|
1589
1380
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1590
1381
|
}
|
|
1591
1382
|
function findFlavourPath(env, cwd) {
|
|
1592
1383
|
const candidates = [];
|
|
1593
1384
|
const configDir = env.OPENCODE_CONFIG_DIR?.trim();
|
|
1594
|
-
if (configDir) candidates.push(
|
|
1595
|
-
candidates.push(
|
|
1596
|
-
const configHome = env.XDG_CONFIG_HOME?.trim() ||
|
|
1597
|
-
candidates.push(
|
|
1385
|
+
if (configDir) candidates.push(path7.join(configDir, "oh-my-openagent.jsonc"));
|
|
1386
|
+
candidates.push(path7.join(cwd, ".opencode", "oh-my-openagent.jsonc"));
|
|
1387
|
+
const configHome = env.XDG_CONFIG_HOME?.trim() || path7.join(os3.homedir(), ".config");
|
|
1388
|
+
candidates.push(path7.join(configHome, "opencode", "oh-my-openagent.jsonc"));
|
|
1598
1389
|
for (const candidate of candidates) {
|
|
1599
1390
|
try {
|
|
1600
|
-
if (
|
|
1391
|
+
if (fs6.existsSync(candidate)) return candidate;
|
|
1601
1392
|
} catch {
|
|
1602
1393
|
}
|
|
1603
1394
|
}
|
|
1604
1395
|
return void 0;
|
|
1605
1396
|
}
|
|
1606
1397
|
function collectSectionRefs(refs, section, label) {
|
|
1607
|
-
if (!
|
|
1398
|
+
if (!isRecord7(section)) return;
|
|
1608
1399
|
for (const [name, entry] of Object.entries(section)) {
|
|
1609
|
-
if (!
|
|
1400
|
+
if (!isRecord7(entry)) continue;
|
|
1610
1401
|
pushRef(refs, entry.model, `flavour ${label} "${name}"`);
|
|
1611
|
-
if (label === "agent" &&
|
|
1402
|
+
if (label === "agent" && isRecord7(entry.ultrawork)) {
|
|
1612
1403
|
pushRef(
|
|
1613
1404
|
refs,
|
|
1614
1405
|
entry.ultrawork.model,
|
|
@@ -1622,8 +1413,8 @@ function collectFlavourRefs(env, cwd) {
|
|
|
1622
1413
|
if (flavourPath === void 0) return [];
|
|
1623
1414
|
const refs = [];
|
|
1624
1415
|
try {
|
|
1625
|
-
const parsed =
|
|
1626
|
-
if (!
|
|
1416
|
+
const parsed = parseJsonc(fs6.readFileSync(flavourPath, "utf8"));
|
|
1417
|
+
if (!isRecord7(parsed)) {
|
|
1627
1418
|
logger.warn(
|
|
1628
1419
|
`flavour file ${flavourPath} did not parse to an object \u2014 skipping flavour ref validation`
|
|
1629
1420
|
);
|
|
@@ -1802,11 +1593,6 @@ function createConfigHook(options = {}) {
|
|
|
1802
1593
|
|
|
1803
1594
|
// src/index.ts
|
|
1804
1595
|
var Route33Plugin = async (input, _options) => {
|
|
1805
|
-
ensureTuiRegistration({
|
|
1806
|
-
worktree: input?.worktree ?? input?.directory,
|
|
1807
|
-
env: process.env,
|
|
1808
|
-
moduleUrl: import.meta.url
|
|
1809
|
-
});
|
|
1810
1596
|
ensureWorkspaceKeyGitignore(input?.worktree ?? input?.directory);
|
|
1811
1597
|
return {
|
|
1812
1598
|
config: createConfigHook(),
|
package/dist/tui/index.js
CHANGED
|
@@ -155,7 +155,9 @@ var KEY_SELECT_TITLE = "route33.ai project key";
|
|
|
155
155
|
var KEY_SELECT_ENTER_LABEL = "Enter a project key";
|
|
156
156
|
var KEY_SELECT_ENTER_DESCRIPTION = "Per workspace on purpose \u2014 one shared key would lose the per-project metrics";
|
|
157
157
|
var KEY_SELECT_SKIP_LABEL = "Skip";
|
|
158
|
-
var KEY_SELECT_SKIP_DESCRIPTION = "Ask again in 7 days";
|
|
158
|
+
var KEY_SELECT_SKIP_DESCRIPTION = "Ask again in 7 days \u2014 this workspace only";
|
|
159
|
+
var KEY_SELECT_SKIP_ALL_LABEL = "Skip for all workspaces";
|
|
160
|
+
var KEY_SELECT_SKIP_ALL_DESCRIPTION = "Ask again in 7 days \u2014 everywhere";
|
|
159
161
|
var KEY_SELECT_PLACEHOLDER = "No route33.ai key found for this workspace yet.";
|
|
160
162
|
var KEY_PROMPT_TITLE = "Paste your route33.ai project key";
|
|
161
163
|
var KEY_SAVED_MESSAGE = "route33.ai project key saved to .opencode/route33.key \u2014 live immediately, no restart needed.";
|
|
@@ -211,8 +213,10 @@ async function runProjectKeyPromptFlow({
|
|
|
211
213
|
dismissal,
|
|
212
214
|
now = Date.now
|
|
213
215
|
}) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
const choice = await ui.askChoice();
|
|
217
|
+
if (choice === "skip" || choice === "skip-all") {
|
|
218
|
+
if (choice === "skip-all") dismissal.dismissGlobally(now());
|
|
219
|
+
else dismissal.dismiss(now());
|
|
216
220
|
ui.close();
|
|
217
221
|
return "skipped";
|
|
218
222
|
}
|
|
@@ -262,6 +266,9 @@ async function runActivation(deps) {
|
|
|
262
266
|
}
|
|
263
267
|
}
|
|
264
268
|
|
|
269
|
+
// src/tui/dismissal-store.ts
|
|
270
|
+
import { resolve as resolve3 } from "path";
|
|
271
|
+
|
|
265
272
|
// src/tui/dismissal-cooldown.ts
|
|
266
273
|
var DISMISSAL_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
267
274
|
function isWithinCooldown(dismissedAt, now) {
|
|
@@ -279,15 +286,53 @@ function readDismissedAt(kv, key) {
|
|
|
279
286
|
return void 0;
|
|
280
287
|
}
|
|
281
288
|
}
|
|
282
|
-
function
|
|
289
|
+
function readByWorkspace(kv, key) {
|
|
290
|
+
try {
|
|
291
|
+
const value = kv.get(key);
|
|
292
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
293
|
+
return {};
|
|
294
|
+
}
|
|
295
|
+
const entries = {};
|
|
296
|
+
for (const [path5, at] of Object.entries(value)) {
|
|
297
|
+
if (typeof at === "number" && Number.isFinite(at)) entries[path5] = at;
|
|
298
|
+
}
|
|
299
|
+
return entries;
|
|
300
|
+
} catch {
|
|
301
|
+
return {};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function createDismissalStore(kv, key = PROJECT_KEY_PROMPT_DISMISSED_AT_KEY, worktree) {
|
|
305
|
+
const scoped = worktree !== void 0 ? resolve3(worktree) : void 0;
|
|
306
|
+
const byWorkspaceKey = `${key}.by-workspace`;
|
|
307
|
+
const dismissGlobally = (now) => {
|
|
308
|
+
try {
|
|
309
|
+
kv.set(key, now);
|
|
310
|
+
} catch {
|
|
311
|
+
}
|
|
312
|
+
};
|
|
283
313
|
return {
|
|
284
|
-
isDismissed: (now) =>
|
|
314
|
+
isDismissed: (now) => {
|
|
315
|
+
if (isWithinCooldown(readDismissedAt(kv, key), now)) return true;
|
|
316
|
+
if (scoped === void 0) return false;
|
|
317
|
+
return isWithinCooldown(readByWorkspace(kv, byWorkspaceKey)[scoped], now);
|
|
318
|
+
},
|
|
285
319
|
dismiss: (now) => {
|
|
320
|
+
if (scoped === void 0) {
|
|
321
|
+
dismissGlobally(now);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
286
324
|
try {
|
|
287
|
-
kv
|
|
325
|
+
const entries = readByWorkspace(kv, byWorkspaceKey);
|
|
326
|
+
const pruned = {};
|
|
327
|
+
for (const [path5, at] of Object.entries(entries)) {
|
|
328
|
+
if (isWithinCooldown(at, now)) pruned[path5] = at;
|
|
329
|
+
}
|
|
330
|
+
pruned[scoped] = now;
|
|
331
|
+
kv.set(byWorkspaceKey, pruned);
|
|
288
332
|
} catch {
|
|
289
333
|
}
|
|
290
|
-
}
|
|
334
|
+
},
|
|
335
|
+
dismissGlobally
|
|
291
336
|
};
|
|
292
337
|
}
|
|
293
338
|
|
|
@@ -298,6 +343,8 @@ var PROJECT_COPY = {
|
|
|
298
343
|
enterDescription: KEY_SELECT_ENTER_DESCRIPTION,
|
|
299
344
|
skipLabel: KEY_SELECT_SKIP_LABEL,
|
|
300
345
|
skipDescription: KEY_SELECT_SKIP_DESCRIPTION,
|
|
346
|
+
skipAllLabel: KEY_SELECT_SKIP_ALL_LABEL,
|
|
347
|
+
skipAllDescription: KEY_SELECT_SKIP_ALL_DESCRIPTION,
|
|
301
348
|
selectPlaceholder: KEY_SELECT_PLACEHOLDER,
|
|
302
349
|
promptTitle: KEY_PROMPT_TITLE
|
|
303
350
|
};
|
|
@@ -314,12 +361,12 @@ var GLOBAL_COPY = {
|
|
|
314
361
|
// src/tui/tui-api-adapter.ts
|
|
315
362
|
var READY_POLL_INTERVAL_MS = 100;
|
|
316
363
|
function askChoice(api, copy) {
|
|
317
|
-
return new Promise((
|
|
364
|
+
return new Promise((resolve4) => {
|
|
318
365
|
let settled = false;
|
|
319
366
|
const settle = (choice) => {
|
|
320
367
|
if (settled) return;
|
|
321
368
|
settled = true;
|
|
322
|
-
|
|
369
|
+
resolve4(choice);
|
|
323
370
|
};
|
|
324
371
|
api.ui.dialog.replace(
|
|
325
372
|
() => api.ui.DialogSelect({
|
|
@@ -335,10 +382,21 @@ function askChoice(api, copy) {
|
|
|
335
382
|
title: copy.skipLabel,
|
|
336
383
|
value: "skip",
|
|
337
384
|
description: copy.skipDescription
|
|
338
|
-
}
|
|
385
|
+
},
|
|
386
|
+
// Only the project copy defines a skip-all scope; the global
|
|
387
|
+
// prompt's plain skip is global already.
|
|
388
|
+
...copy.skipAllLabel !== void 0 ? [
|
|
389
|
+
{
|
|
390
|
+
title: copy.skipAllLabel,
|
|
391
|
+
value: "skip-all",
|
|
392
|
+
description: copy.skipAllDescription ?? ""
|
|
393
|
+
}
|
|
394
|
+
] : []
|
|
339
395
|
],
|
|
340
396
|
onSelect: (option) => {
|
|
341
|
-
settle(
|
|
397
|
+
settle(
|
|
398
|
+
option.value === "enter" ? "enter" : option.value === "skip-all" ? "skip-all" : "skip"
|
|
399
|
+
);
|
|
342
400
|
}
|
|
343
401
|
}),
|
|
344
402
|
() => {
|
|
@@ -348,12 +406,12 @@ function askChoice(api, copy) {
|
|
|
348
406
|
});
|
|
349
407
|
}
|
|
350
408
|
function askKey(api, copy) {
|
|
351
|
-
return new Promise((
|
|
409
|
+
return new Promise((resolve4) => {
|
|
352
410
|
let settled = false;
|
|
353
411
|
const settle = (value) => {
|
|
354
412
|
if (settled) return;
|
|
355
413
|
settled = true;
|
|
356
|
-
|
|
414
|
+
resolve4(value);
|
|
357
415
|
};
|
|
358
416
|
api.ui.dialog.replace(
|
|
359
417
|
() => api.ui.DialogPrompt({
|
|
@@ -375,20 +433,20 @@ function askKey(api, copy) {
|
|
|
375
433
|
});
|
|
376
434
|
}
|
|
377
435
|
function waitUntilReady(api) {
|
|
378
|
-
return new Promise((
|
|
436
|
+
return new Promise((resolve4) => {
|
|
379
437
|
if (api.state.ready) {
|
|
380
|
-
|
|
438
|
+
resolve4(true);
|
|
381
439
|
return;
|
|
382
440
|
}
|
|
383
441
|
const timer = setInterval(() => {
|
|
384
442
|
if (api.lifecycle.signal.aborted) {
|
|
385
443
|
clearInterval(timer);
|
|
386
|
-
|
|
444
|
+
resolve4(false);
|
|
387
445
|
return;
|
|
388
446
|
}
|
|
389
447
|
if (api.state.ready) {
|
|
390
448
|
clearInterval(timer);
|
|
391
|
-
|
|
449
|
+
resolve4(true);
|
|
392
450
|
}
|
|
393
451
|
}, READY_POLL_INTERVAL_MS);
|
|
394
452
|
});
|
|
@@ -448,7 +506,11 @@ async function activate(api) {
|
|
|
448
506
|
await runActivation({
|
|
449
507
|
ui,
|
|
450
508
|
keyStore,
|
|
451
|
-
dismissal: createDismissalStore(
|
|
509
|
+
dismissal: createDismissalStore(
|
|
510
|
+
kv,
|
|
511
|
+
PROJECT_KEY_PROMPT_DISMISSED_AT_KEY,
|
|
512
|
+
process.cwd()
|
|
513
|
+
),
|
|
452
514
|
globalUi,
|
|
453
515
|
globalStore,
|
|
454
516
|
globalDismissal: createDismissalStore(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@route_33/opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"description": "opencode provider plugin for the route33.ai AI gateway — key-scoped model discovery, cost/limit enrichment, auth, and the key-setup wizard.",
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"types": "./dist/index.d.ts",
|
|
20
20
|
"import": "./dist/index.js"
|
|
21
21
|
},
|
|
22
|
+
"./server": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js"
|
|
25
|
+
},
|
|
22
26
|
"./tui": {
|
|
23
27
|
"types": "./dist/tui/index.d.ts",
|
|
24
28
|
"import": "./dist/tui/index.js"
|