@route_33/opencode 0.4.3 → 0.4.4
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 +4 -4
- package/dist/index.js +47 -119
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ The installer patches both configs: the server plugin into `opencode.json` and t
|
|
|
24
24
|
}
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
This plugin never writes your `opencode.json` or `opencode.jsonc`: opencode has no TUI-plugin auto-discovery, so without a `tui.json` entry the wizard simply never appears — the server plugin works regardless. The
|
|
27
|
+
This plugin never writes your `opencode.json` or `opencode.jsonc`: opencode has no TUI-plugin auto-discovery, so without a `tui.json` entry the wizard simply never appears — the server plugin works regardless. The only files the plugin ever writes inside your repo are an optional `.opencode/route33.key` (git-ignored automatically) and your own `.opencode/route33-overrides.json` (yours to commit or ignore).
|
|
28
28
|
|
|
29
29
|
## Auth
|
|
30
30
|
|
|
@@ -41,7 +41,7 @@ The catalog is discovered dynamically. The plugin calls `GET {baseURL}/models` w
|
|
|
41
41
|
3. If the live fetch fails, return the stale cache.
|
|
42
42
|
4. If there is no cache and no live response, preserve existing generated entries and inject no replacement models.
|
|
43
43
|
|
|
44
|
-
The plugin does not embed or serve a static catalog. Catalog cache envelopes are versioned; cache files from older plugin versions are ignored after an upgrade and repopulated from the gateway.
|
|
44
|
+
The plugin does not embed or serve a static catalog. The on-disk cache is keyed by gateway URL **and** API key, so the same key pointed at a different gateway (`ROUTE33_BASE_URL`, e.g. a local cluster) never shares a cache with production. Catalog cache envelopes are versioned; cache files from older plugin versions are ignored after an upgrade and repopulated from the gateway.
|
|
45
45
|
|
|
46
46
|
If discovery yields no catalog, the plugin logs exactly one warning:
|
|
47
47
|
|
|
@@ -66,7 +66,7 @@ The normative broadcast contract is [docs/gateway-limits-variants-broadcast.md](
|
|
|
66
66
|
|
|
67
67
|
## Managed model entries
|
|
68
68
|
|
|
69
|
-
Gateway-discovered `provider.route_33.models` entries are managed
|
|
69
|
+
Gateway-discovered `provider.route_33.models` entries are fully managed: every startup rebuilds the map from the current broadcast, then deep-merges your overrides on top. The ownership rule is the whole design — *in the broadcast (or in your overrides) ⇒ kept; anything else ⇒ dropped and logged* — so retired gateway models disappear automatically and no bookkeeping file is needed. (Versions ≤0.4.3 kept a `.opencode/route33-generated.json` ledger for this; the plugin deletes that obsolete file on first run.)
|
|
70
70
|
|
|
71
71
|
Keep customizations in `.opencode/route33-overrides.json`, because the installed OpenCode `ProviderConfig` does not allow unknown provider-level keys such as `modelOverrides`. Overrides deep-merge onto matching generated entries and can add custom models:
|
|
72
72
|
|
|
@@ -79,7 +79,7 @@ Keep customizations in `.opencode/route33-overrides.json`, because the installed
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
If the gateway fetch fails with no usable cache, regeneration is skipped: existing entries are never pruned
|
|
82
|
+
If the gateway fetch fails with no usable cache, regeneration is skipped entirely: existing entries are never pruned on zero information.
|
|
83
83
|
|
|
84
84
|
`ROUTE33_ENRICH_CAPS` has been removed; capability backfill from `models.dev` is now always on.
|
|
85
85
|
|
package/dist/index.js
CHANGED
|
@@ -376,8 +376,9 @@ var memory = /* @__PURE__ */ new Map();
|
|
|
376
376
|
|
|
377
377
|
// src/catalog/catalog-cache/hash-key.ts
|
|
378
378
|
import { createHash } from "crypto";
|
|
379
|
-
function hashKey(key) {
|
|
380
|
-
return createHash("sha256").update(
|
|
379
|
+
function hashKey(baseURL, key) {
|
|
380
|
+
return createHash("sha256").update(`${baseURL}
|
|
381
|
+
${key}`).digest("hex").slice(0, 12);
|
|
381
382
|
}
|
|
382
383
|
|
|
383
384
|
// src/catalog/catalog-cache/read-disk.ts
|
|
@@ -422,8 +423,8 @@ function isFresh(record, now, ttl) {
|
|
|
422
423
|
return now - record.fetchedAt <= ttl;
|
|
423
424
|
}
|
|
424
425
|
function readFreshCatalog(opts) {
|
|
425
|
-
const { key, env = process.env, now = Date.now() } = opts;
|
|
426
|
-
const keyHash = hashKey(key);
|
|
426
|
+
const { baseURL, key, env = process.env, now = Date.now() } = opts;
|
|
427
|
+
const keyHash = hashKey(baseURL, key);
|
|
427
428
|
const ttl = catalogTtlMs(env);
|
|
428
429
|
const inMemory = memory.get(keyHash);
|
|
429
430
|
if (inMemory && isFresh(inMemory, now, ttl)) return inMemory.models;
|
|
@@ -437,16 +438,16 @@ function readFreshCatalog(opts) {
|
|
|
437
438
|
|
|
438
439
|
// src/catalog/catalog-cache/read-stale-catalog.ts
|
|
439
440
|
function readStaleCatalog(opts) {
|
|
440
|
-
const { key, env = process.env } = opts;
|
|
441
|
-
const keyHash = hashKey(key);
|
|
441
|
+
const { baseURL, key, env = process.env } = opts;
|
|
442
|
+
const keyHash = hashKey(baseURL, key);
|
|
442
443
|
return (memory.get(keyHash) ?? readDisk(keyHash, env))?.models;
|
|
443
444
|
}
|
|
444
445
|
|
|
445
446
|
// src/catalog/catalog-cache/write-catalog.ts
|
|
446
447
|
import * as fs4 from "fs";
|
|
447
448
|
function writeCatalog(opts) {
|
|
448
|
-
const { key, models, env = process.env, now = Date.now() } = opts;
|
|
449
|
-
const keyHash = hashKey(key);
|
|
449
|
+
const { baseURL, key, models, env = process.env, now = Date.now() } = opts;
|
|
450
|
+
const keyHash = hashKey(baseURL, key);
|
|
450
451
|
const record = { fetchedAt: now, keyHash, models };
|
|
451
452
|
memory.set(keyHash, record);
|
|
452
453
|
try {
|
|
@@ -696,7 +697,7 @@ async function runLadder(opts) {
|
|
|
696
697
|
if (env[ENV_DISCOVERY] === "0")
|
|
697
698
|
return noneResult(`discovery disabled via ${ENV_DISCOVERY}=0`, true);
|
|
698
699
|
if (!key) return noneResult("no API key resolved", false);
|
|
699
|
-
const fresh = readFreshCatalog({ key, env, now });
|
|
700
|
+
const fresh = readFreshCatalog({ baseURL, key, env, now });
|
|
700
701
|
if (fresh) {
|
|
701
702
|
logger.info(
|
|
702
703
|
`catalog source: cache (${Object.keys(fresh).length} model(s))`
|
|
@@ -707,7 +708,7 @@ async function runLadder(opts) {
|
|
|
707
708
|
const live = await fetchCatalog({ baseURL, key, env, fetchImpl });
|
|
708
709
|
if (Object.keys(live).length === 0)
|
|
709
710
|
throw new Error("gateway returned an empty catalog");
|
|
710
|
-
writeCatalog({ key, models: live, env, now });
|
|
711
|
+
writeCatalog({ baseURL, key, models: live, env, now });
|
|
711
712
|
logger.info(`catalog source: live (${Object.keys(live).length} model(s))`);
|
|
712
713
|
return { models: live, source: "live" };
|
|
713
714
|
} catch (error) {
|
|
@@ -715,7 +716,7 @@ async function runLadder(opts) {
|
|
|
715
716
|
`live catalog fetch failed \u2014 trying stale cache: ${error instanceof Error ? error.message : String(error)}`
|
|
716
717
|
);
|
|
717
718
|
}
|
|
718
|
-
const stale = readStaleCatalog({ key, env });
|
|
719
|
+
const stale = readStaleCatalog({ baseURL, key, env });
|
|
719
720
|
if (stale) {
|
|
720
721
|
logger.info(
|
|
721
722
|
`catalog source: stale-cache (${Object.keys(stale).length} model(s))`
|
|
@@ -1061,102 +1062,36 @@ function mergeConfig(cfg, blocks) {
|
|
|
1061
1062
|
}
|
|
1062
1063
|
}
|
|
1063
1064
|
|
|
1064
|
-
// src/config/
|
|
1065
|
-
function cloneEntry(entry) {
|
|
1066
|
-
return { ...entry };
|
|
1067
|
-
}
|
|
1068
|
-
function cloneModels(patch) {
|
|
1069
|
-
const models = {};
|
|
1070
|
-
for (const [key, entry] of Object.entries(patch.models)) {
|
|
1071
|
-
models[key] = cloneEntry(entry);
|
|
1072
|
-
}
|
|
1073
|
-
return models;
|
|
1074
|
-
}
|
|
1075
|
-
function fillModelEntry(existing, patchEntry) {
|
|
1076
|
-
for (const [field, value] of Object.entries(patchEntry)) {
|
|
1077
|
-
if (value === void 0) continue;
|
|
1078
|
-
if (field === "limit" || existing[field] === void 0)
|
|
1079
|
-
existing[field] = value;
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
function mergeModels(target, patch) {
|
|
1083
|
-
if (target.models === void 0) {
|
|
1084
|
-
target.models = cloneModels(patch);
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
const models = target.models;
|
|
1088
|
-
for (const [key, patchEntry] of Object.entries(patch.models)) {
|
|
1089
|
-
const existing = models[key];
|
|
1090
|
-
if (existing === void 0) {
|
|
1091
|
-
models[key] = cloneEntry(patchEntry);
|
|
1092
|
-
} else {
|
|
1093
|
-
fillModelEntry(existing, cloneEntry(patchEntry));
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// src/config/generation-manifest.ts
|
|
1065
|
+
// src/config/read-model-overrides.ts
|
|
1099
1066
|
import * as fs6 from "fs";
|
|
1100
1067
|
import * as path7 from "path";
|
|
1101
1068
|
var SIDECAR_DIR = ".opencode";
|
|
1102
|
-
var MANIFEST_FILE = "route33-generated.json";
|
|
1103
1069
|
var OVERRIDES_FILE = "route33-overrides.json";
|
|
1104
1070
|
function isRecord7(value) {
|
|
1105
1071
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1106
1072
|
}
|
|
1107
|
-
function generatedPath(root) {
|
|
1108
|
-
return path7.join(root, SIDECAR_DIR, MANIFEST_FILE);
|
|
1109
|
-
}
|
|
1110
|
-
function overridesPath(root) {
|
|
1111
|
-
return path7.join(root, SIDECAR_DIR, OVERRIDES_FILE);
|
|
1112
|
-
}
|
|
1113
|
-
function parseManifest(value) {
|
|
1114
|
-
if (!isRecord7(value) || value.version !== 1 || !Array.isArray(value.generatedKeys) || typeof value.lastBroadcastHash !== "string") return void 0;
|
|
1115
|
-
if (!value.generatedKeys.every((key) => typeof key === "string")) return void 0;
|
|
1116
|
-
return { version: 1, generatedKeys: value.generatedKeys, lastBroadcastHash: value.lastBroadcastHash };
|
|
1117
|
-
}
|
|
1118
1073
|
function parseOverrides(value) {
|
|
1119
1074
|
if (!isRecord7(value) || !isRecord7(value.models) || !Object.values(value.models).every(isRecord7)) return void 0;
|
|
1120
1075
|
const models = {};
|
|
1121
1076
|
for (const [key, entry] of Object.entries(value.models)) if (isRecord7(entry)) models[key] = entry;
|
|
1122
1077
|
return models;
|
|
1123
1078
|
}
|
|
1124
|
-
function readJson(file) {
|
|
1125
|
-
try {
|
|
1126
|
-
if (!fs6.existsSync(file)) return void 0;
|
|
1127
|
-
const parsed = JSON.parse(fs6.readFileSync(file, "utf8"));
|
|
1128
|
-
return parsed;
|
|
1129
|
-
} catch (error) {
|
|
1130
|
-
logger.info(`route_33: unable to read generated-model sidecar: ${error instanceof Error ? error.message : String(error)}`);
|
|
1131
|
-
return void 0;
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
function readGenerationManifest(root) {
|
|
1135
|
-
const file = generatedPath(root);
|
|
1136
|
-
const manifest = parseManifest(readJson(file));
|
|
1137
|
-
if (manifest === void 0 && fs6.existsSync(file)) logger.info("route_33: generated-model manifest is malformed; treating this run as first-run regeneration");
|
|
1138
|
-
return manifest;
|
|
1139
|
-
}
|
|
1140
1079
|
function readModelOverrides(root) {
|
|
1141
|
-
const file =
|
|
1142
|
-
|
|
1143
|
-
if (overrides === void 0 && fs6.existsSync(file)) logger.info("route_33: model overrides sidecar is malformed; ignoring it for this run");
|
|
1144
|
-
return overrides ?? {};
|
|
1145
|
-
}
|
|
1146
|
-
function writeGenerationManifest(root, manifest) {
|
|
1080
|
+
const file = path7.join(root, SIDECAR_DIR, OVERRIDES_FILE);
|
|
1081
|
+
let parsed;
|
|
1147
1082
|
try {
|
|
1148
|
-
fs6.
|
|
1149
|
-
|
|
1150
|
-
`);
|
|
1151
|
-
return true;
|
|
1083
|
+
if (!fs6.existsSync(file)) return {};
|
|
1084
|
+
parsed = JSON.parse(fs6.readFileSync(file, "utf8"));
|
|
1152
1085
|
} catch (error) {
|
|
1153
|
-
logger.info(`route_33: unable to
|
|
1154
|
-
return
|
|
1086
|
+
logger.info(`route_33: unable to read model overrides sidecar: ${error instanceof Error ? error.message : String(error)}`);
|
|
1087
|
+
return {};
|
|
1155
1088
|
}
|
|
1089
|
+
const overrides = parseOverrides(parsed);
|
|
1090
|
+
if (overrides === void 0) logger.info("route_33: model overrides sidecar is malformed; ignoring it for this run");
|
|
1091
|
+
return overrides ?? {};
|
|
1156
1092
|
}
|
|
1157
1093
|
|
|
1158
1094
|
// src/config/regenerate-models.ts
|
|
1159
|
-
import { createHash as createHash2 } from "crypto";
|
|
1160
1095
|
function isRecord8(value) {
|
|
1161
1096
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1162
1097
|
}
|
|
@@ -1165,49 +1100,46 @@ function clone(value) {
|
|
|
1165
1100
|
if (!isRecord8(value)) return value;
|
|
1166
1101
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, clone(item)]));
|
|
1167
1102
|
}
|
|
1168
|
-
function
|
|
1103
|
+
function cloneEntry(entry) {
|
|
1169
1104
|
const copy = {};
|
|
1170
1105
|
for (const [key, value] of Object.entries(entry)) copy[key] = clone(value);
|
|
1171
1106
|
return copy;
|
|
1172
1107
|
}
|
|
1173
1108
|
function mergeEntry(base, override) {
|
|
1174
|
-
const result =
|
|
1109
|
+
const result = cloneEntry(base);
|
|
1175
1110
|
for (const [key, value] of Object.entries(override)) {
|
|
1176
1111
|
const existing = result[key];
|
|
1177
1112
|
result[key] = isRecord8(existing) && isRecord8(value) ? mergeEntry(existing, value) : clone(value);
|
|
1178
1113
|
}
|
|
1179
1114
|
return result;
|
|
1180
1115
|
}
|
|
1181
|
-
function stableJson(value) {
|
|
1182
|
-
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
1183
|
-
if (!isRecord8(value)) return JSON.stringify(value);
|
|
1184
|
-
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
1185
|
-
}
|
|
1186
|
-
function broadcastHash(models) {
|
|
1187
|
-
return createHash2("sha256").update(stableJson(models)).digest("hex");
|
|
1188
|
-
}
|
|
1189
1116
|
function regenerateModels(input) {
|
|
1190
1117
|
if (input.source === "none" && input.failedLiveFetch === true) {
|
|
1191
1118
|
const models2 = {};
|
|
1192
1119
|
for (const [key, entry] of Object.entries(input.currentModels ?? {}))
|
|
1193
|
-
if (entry !== void 0) models2[key] =
|
|
1120
|
+
if (entry !== void 0) models2[key] = cloneEntry(entry);
|
|
1194
1121
|
return { models: models2, prunedKeys: [], skipped: true };
|
|
1195
1122
|
}
|
|
1196
|
-
const current = input.currentModels ?? {};
|
|
1197
|
-
const generatedKeys = input.previousManifest?.generatedKeys ?? Object.keys(current);
|
|
1198
1123
|
const models = {};
|
|
1199
|
-
for (const [key, entry] of Object.entries(
|
|
1200
|
-
if (!generatedKeys.includes(key) && entry !== void 0) models[key] = cloneEntry2(entry);
|
|
1201
|
-
}
|
|
1202
|
-
for (const [key, entry] of Object.entries(input.freshModels)) models[key] = cloneEntry2(entry);
|
|
1124
|
+
for (const [key, entry] of Object.entries(input.freshModels)) models[key] = cloneEntry(entry);
|
|
1203
1125
|
for (const [key, override] of Object.entries(input.overrides ?? {}))
|
|
1204
1126
|
models[key] = mergeEntry(models[key] ?? {}, override);
|
|
1205
|
-
const
|
|
1206
|
-
return {
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1127
|
+
const prunedKeys = Object.keys(input.currentModels ?? {}).filter((key) => input.freshModels[key] === void 0 && input.overrides?.[key] === void 0).sort();
|
|
1128
|
+
return { models, prunedKeys };
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/config/remove-legacy-manifest.ts
|
|
1132
|
+
import * as fs7 from "fs";
|
|
1133
|
+
import * as path8 from "path";
|
|
1134
|
+
function removeLegacyManifest(root) {
|
|
1135
|
+
const file = path8.join(root, ".opencode", "route33-generated.json");
|
|
1136
|
+
try {
|
|
1137
|
+
if (!fs7.existsSync(file)) return;
|
|
1138
|
+
fs7.rmSync(file);
|
|
1139
|
+
logger.info("route_33: removed obsolete route33-generated.json (regeneration is stateless since 0.4.4)");
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
logger.info(`route_33: unable to remove legacy route33-generated.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
1142
|
+
}
|
|
1211
1143
|
}
|
|
1212
1144
|
|
|
1213
1145
|
// src/config/collected-ref.ts
|
|
@@ -1437,6 +1369,7 @@ function createConfigHook(options = {}) {
|
|
|
1437
1369
|
let key;
|
|
1438
1370
|
let catalogAvailable = false;
|
|
1439
1371
|
try {
|
|
1372
|
+
removeLegacyManifest(cwd);
|
|
1440
1373
|
key = resolveKey(cfg, env, cwd);
|
|
1441
1374
|
const baseURL = resolveBaseUrl(cfg, env, cwd);
|
|
1442
1375
|
const catalog = await getCatalog({
|
|
@@ -1466,21 +1399,16 @@ function createConfigHook(options = {}) {
|
|
|
1466
1399
|
const regeneration = regenerateModels({
|
|
1467
1400
|
currentModels: provider.models,
|
|
1468
1401
|
freshModels: blocks.route_33.models,
|
|
1469
|
-
previousManifest: readGenerationManifest(cwd),
|
|
1470
1402
|
overrides: readModelOverrides(cwd),
|
|
1471
1403
|
source: catalog.source,
|
|
1472
1404
|
failedLiveFetch: catalog.liveFetchFailed
|
|
1473
1405
|
});
|
|
1474
1406
|
if (regeneration.skipped) {
|
|
1475
|
-
logger.info("route_33: catalog fetch failed without a cache; preserving existing
|
|
1407
|
+
logger.info("route_33: catalog fetch failed without a cache; preserving existing model entries");
|
|
1476
1408
|
} else {
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
provider.models = regeneration.models;
|
|
1481
|
-
}
|
|
1482
|
-
for (const key2 of regeneration.prunedKeys)
|
|
1483
|
-
logger.info(`route_33: removed stale model entry '${key2}' (no longer in the gateway catalog); move custom entries to route_33 modelOverrides \u2014 see README`);
|
|
1409
|
+
provider.models = regeneration.models;
|
|
1410
|
+
for (const dropped of regeneration.prunedKeys)
|
|
1411
|
+
logger.info(`route_33: dropped model entry '${dropped}' (not in the gateway catalog); custom models belong in .opencode/route33-overrides.json \u2014 see README`);
|
|
1484
1412
|
}
|
|
1485
1413
|
}
|
|
1486
1414
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@route_33/opencode",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
-
"description": "opencode provider plugin for the route33.ai AI gateway
|
|
6
|
+
"description": "opencode provider plugin for the route33.ai AI gateway \u2014 key-scoped model discovery, cost/limit enrichment, auth, and the key-setup wizard.",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/Cronos-AI-Lab/harness-bench.git",
|