@route_33/opencode 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/index.js +65 -124
- package/dist/tui/index.js +7 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -24,13 +24,13 @@ 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
|
|
|
31
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.
|
|
32
32
|
|
|
33
|
-
Your key is never written into the resolved provider configuration: authentication is attached per request at fetch time, so `opencode debug config` output contains no credential material.
|
|
33
|
+
Your key is never written into the resolved provider configuration: authentication is attached per request at fetch time, so `opencode debug config` output contains no credential material. One inert exception: the options carry the non-secret marker `__route33_fetch_shim_auth__` as `apiKey`, because the native per-model SDKs used for `anthropic/`, `openai/`, and `mistralai/` models (`@ai-sdk/anthropic`, `@ai-sdk/openai`, `@ai-sdk/mistral`) refuse to construct a request without one — the marker satisfies that null check, never authenticates anything (the gateway reads only the request-time `Authorization` header, which the fetch shim injects), and no vendor env var (`ANTHROPIC_API_KEY` etc.) is needed or consulted. The marker is skipped by key resolution and never overrides an `apiKey` you authored yourself; it is visible in config dumps precisely because it is not a secret.
|
|
34
34
|
|
|
35
35
|
## Model catalog
|
|
36
36
|
|
|
@@ -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
|
@@ -94,6 +94,9 @@ var logger = {
|
|
|
94
94
|
// src/shared/models-dev-ttl-ms.ts
|
|
95
95
|
var MODELS_DEV_TTL_MS = 864e5;
|
|
96
96
|
|
|
97
|
+
// src/shared/native-sdk-api-key-placeholder.ts
|
|
98
|
+
var NATIVE_SDK_API_KEY_PLACEHOLDER = "__route33_fetch_shim_auth__";
|
|
99
|
+
|
|
97
100
|
// src/shared/resolve-cache-dir.ts
|
|
98
101
|
import * as os2 from "os";
|
|
99
102
|
import * as path3 from "path";
|
|
@@ -217,9 +220,10 @@ function usable4(value) {
|
|
|
217
220
|
}
|
|
218
221
|
function keyFromConfig(cfg) {
|
|
219
222
|
const literal = usable4(cfg?.provider?.route_33?.options?.apiKey);
|
|
220
|
-
if (literal
|
|
221
|
-
|
|
222
|
-
return void 0;
|
|
223
|
+
if (literal === void 0) return void 0;
|
|
224
|
+
if (literal.includes(UNRESOLVED_TEMPLATE_MARKER2)) return void 0;
|
|
225
|
+
if (literal === NATIVE_SDK_API_KEY_PLACEHOLDER) return void 0;
|
|
226
|
+
return literal;
|
|
223
227
|
}
|
|
224
228
|
function resolveKey(cfg, env = process.env, worktree) {
|
|
225
229
|
return usable4(env[ENV_API_KEY]) ?? keyFromConfig(cfg) ?? readWorkspaceKey(worktree) ?? (worktree !== void 0 && isInsideGitWorktree(worktree) ? void 0 : readAuthEntry(env).key);
|
|
@@ -290,7 +294,10 @@ function buildOpenAiChatCompatFetch(fetchImpl) {
|
|
|
290
294
|
|
|
291
295
|
// src/config/build-options.ts
|
|
292
296
|
function buildOptions(baseURL, resolveKeyNow = () => void 0) {
|
|
293
|
-
const options = {
|
|
297
|
+
const options = {
|
|
298
|
+
baseURL,
|
|
299
|
+
apiKey: NATIVE_SDK_API_KEY_PLACEHOLDER
|
|
300
|
+
};
|
|
294
301
|
options.fetch = buildLiveAuthFetch(
|
|
295
302
|
resolveKeyNow,
|
|
296
303
|
buildOpenAiChatCompatFetch()
|
|
@@ -376,8 +383,9 @@ var memory = /* @__PURE__ */ new Map();
|
|
|
376
383
|
|
|
377
384
|
// src/catalog/catalog-cache/hash-key.ts
|
|
378
385
|
import { createHash } from "crypto";
|
|
379
|
-
function hashKey(key) {
|
|
380
|
-
return createHash("sha256").update(
|
|
386
|
+
function hashKey(baseURL, key) {
|
|
387
|
+
return createHash("sha256").update(`${baseURL}
|
|
388
|
+
${key}`).digest("hex").slice(0, 12);
|
|
381
389
|
}
|
|
382
390
|
|
|
383
391
|
// src/catalog/catalog-cache/read-disk.ts
|
|
@@ -422,8 +430,8 @@ function isFresh(record, now, ttl) {
|
|
|
422
430
|
return now - record.fetchedAt <= ttl;
|
|
423
431
|
}
|
|
424
432
|
function readFreshCatalog(opts) {
|
|
425
|
-
const { key, env = process.env, now = Date.now() } = opts;
|
|
426
|
-
const keyHash = hashKey(key);
|
|
433
|
+
const { baseURL, key, env = process.env, now = Date.now() } = opts;
|
|
434
|
+
const keyHash = hashKey(baseURL, key);
|
|
427
435
|
const ttl = catalogTtlMs(env);
|
|
428
436
|
const inMemory = memory.get(keyHash);
|
|
429
437
|
if (inMemory && isFresh(inMemory, now, ttl)) return inMemory.models;
|
|
@@ -437,16 +445,16 @@ function readFreshCatalog(opts) {
|
|
|
437
445
|
|
|
438
446
|
// src/catalog/catalog-cache/read-stale-catalog.ts
|
|
439
447
|
function readStaleCatalog(opts) {
|
|
440
|
-
const { key, env = process.env } = opts;
|
|
441
|
-
const keyHash = hashKey(key);
|
|
448
|
+
const { baseURL, key, env = process.env } = opts;
|
|
449
|
+
const keyHash = hashKey(baseURL, key);
|
|
442
450
|
return (memory.get(keyHash) ?? readDisk(keyHash, env))?.models;
|
|
443
451
|
}
|
|
444
452
|
|
|
445
453
|
// src/catalog/catalog-cache/write-catalog.ts
|
|
446
454
|
import * as fs4 from "fs";
|
|
447
455
|
function writeCatalog(opts) {
|
|
448
|
-
const { key, models, env = process.env, now = Date.now() } = opts;
|
|
449
|
-
const keyHash = hashKey(key);
|
|
456
|
+
const { baseURL, key, models, env = process.env, now = Date.now() } = opts;
|
|
457
|
+
const keyHash = hashKey(baseURL, key);
|
|
450
458
|
const record = { fetchedAt: now, keyHash, models };
|
|
451
459
|
memory.set(keyHash, record);
|
|
452
460
|
try {
|
|
@@ -622,6 +630,7 @@ function toCatalogModels(entries, env) {
|
|
|
622
630
|
if (capabilities !== void 0) model.capabilities = capabilities;
|
|
623
631
|
const variants = parseVariants(entry.variants);
|
|
624
632
|
if (variants !== void 0) model.variants = variants;
|
|
633
|
+
if (typeof entry.hasZdrLane === "boolean") model.hasZdrLane = entry.hasZdrLane;
|
|
625
634
|
const modalities = parseModalities(entry.modalities);
|
|
626
635
|
if (modalities !== void 0) model.modalities = modalities;
|
|
627
636
|
if (typeof entry.attachment === "boolean") model.attachment = entry.attachment;
|
|
@@ -696,7 +705,7 @@ async function runLadder(opts) {
|
|
|
696
705
|
if (env[ENV_DISCOVERY] === "0")
|
|
697
706
|
return noneResult(`discovery disabled via ${ENV_DISCOVERY}=0`, true);
|
|
698
707
|
if (!key) return noneResult("no API key resolved", false);
|
|
699
|
-
const fresh = readFreshCatalog({ key, env, now });
|
|
708
|
+
const fresh = readFreshCatalog({ baseURL, key, env, now });
|
|
700
709
|
if (fresh) {
|
|
701
710
|
logger.info(
|
|
702
711
|
`catalog source: cache (${Object.keys(fresh).length} model(s))`
|
|
@@ -707,7 +716,7 @@ async function runLadder(opts) {
|
|
|
707
716
|
const live = await fetchCatalog({ baseURL, key, env, fetchImpl });
|
|
708
717
|
if (Object.keys(live).length === 0)
|
|
709
718
|
throw new Error("gateway returned an empty catalog");
|
|
710
|
-
writeCatalog({ key, models: live, env, now });
|
|
719
|
+
writeCatalog({ baseURL, key, models: live, env, now });
|
|
711
720
|
logger.info(`catalog source: live (${Object.keys(live).length} model(s))`);
|
|
712
721
|
return { models: live, source: "live" };
|
|
713
722
|
} catch (error) {
|
|
@@ -715,7 +724,7 @@ async function runLadder(opts) {
|
|
|
715
724
|
`live catalog fetch failed \u2014 trying stale cache: ${error instanceof Error ? error.message : String(error)}`
|
|
716
725
|
);
|
|
717
726
|
}
|
|
718
|
-
const stale = readStaleCatalog({ key, env });
|
|
727
|
+
const stale = readStaleCatalog({ baseURL, key, env });
|
|
719
728
|
if (stale) {
|
|
720
729
|
logger.info(
|
|
721
730
|
`catalog source: stale-cache (${Object.keys(stale).length} model(s))`
|
|
@@ -1035,6 +1044,11 @@ var FORCED_OPTION_FIELDS = /* @__PURE__ */ new Set([
|
|
|
1035
1044
|
"baseURL",
|
|
1036
1045
|
"headers"
|
|
1037
1046
|
]);
|
|
1047
|
+
function isForcedValue(field, value) {
|
|
1048
|
+
if (field === "apiKey" && value === NATIVE_SDK_API_KEY_PLACEHOLDER)
|
|
1049
|
+
return false;
|
|
1050
|
+
return FORCED_OPTION_FIELDS.has(field);
|
|
1051
|
+
}
|
|
1038
1052
|
function mergeProvider(target, patch) {
|
|
1039
1053
|
if (target.npm === void 0) target.npm = patch.npm;
|
|
1040
1054
|
if (target.name === void 0) target.name = patch.name;
|
|
@@ -1044,7 +1058,7 @@ function mergeProvider(target, patch) {
|
|
|
1044
1058
|
const options = target.options;
|
|
1045
1059
|
for (const [field, value] of Object.entries(patch.options)) {
|
|
1046
1060
|
if (value === void 0) continue;
|
|
1047
|
-
if (
|
|
1061
|
+
if (isForcedValue(field, value) || options[field] === void 0)
|
|
1048
1062
|
options[field] = value;
|
|
1049
1063
|
}
|
|
1050
1064
|
}
|
|
@@ -1061,102 +1075,36 @@ function mergeConfig(cfg, blocks) {
|
|
|
1061
1075
|
}
|
|
1062
1076
|
}
|
|
1063
1077
|
|
|
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
|
|
1078
|
+
// src/config/read-model-overrides.ts
|
|
1099
1079
|
import * as fs6 from "fs";
|
|
1100
1080
|
import * as path7 from "path";
|
|
1101
1081
|
var SIDECAR_DIR = ".opencode";
|
|
1102
|
-
var MANIFEST_FILE = "route33-generated.json";
|
|
1103
1082
|
var OVERRIDES_FILE = "route33-overrides.json";
|
|
1104
1083
|
function isRecord7(value) {
|
|
1105
1084
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1106
1085
|
}
|
|
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
1086
|
function parseOverrides(value) {
|
|
1119
1087
|
if (!isRecord7(value) || !isRecord7(value.models) || !Object.values(value.models).every(isRecord7)) return void 0;
|
|
1120
1088
|
const models = {};
|
|
1121
1089
|
for (const [key, entry] of Object.entries(value.models)) if (isRecord7(entry)) models[key] = entry;
|
|
1122
1090
|
return models;
|
|
1123
1091
|
}
|
|
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
1092
|
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) {
|
|
1093
|
+
const file = path7.join(root, SIDECAR_DIR, OVERRIDES_FILE);
|
|
1094
|
+
let parsed;
|
|
1147
1095
|
try {
|
|
1148
|
-
fs6.
|
|
1149
|
-
|
|
1150
|
-
`);
|
|
1151
|
-
return true;
|
|
1096
|
+
if (!fs6.existsSync(file)) return {};
|
|
1097
|
+
parsed = JSON.parse(fs6.readFileSync(file, "utf8"));
|
|
1152
1098
|
} catch (error) {
|
|
1153
|
-
logger.info(`route_33: unable to
|
|
1154
|
-
return
|
|
1099
|
+
logger.info(`route_33: unable to read model overrides sidecar: ${error instanceof Error ? error.message : String(error)}`);
|
|
1100
|
+
return {};
|
|
1155
1101
|
}
|
|
1102
|
+
const overrides = parseOverrides(parsed);
|
|
1103
|
+
if (overrides === void 0) logger.info("route_33: model overrides sidecar is malformed; ignoring it for this run");
|
|
1104
|
+
return overrides ?? {};
|
|
1156
1105
|
}
|
|
1157
1106
|
|
|
1158
1107
|
// src/config/regenerate-models.ts
|
|
1159
|
-
import { createHash as createHash2 } from "crypto";
|
|
1160
1108
|
function isRecord8(value) {
|
|
1161
1109
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1162
1110
|
}
|
|
@@ -1165,49 +1113,46 @@ function clone(value) {
|
|
|
1165
1113
|
if (!isRecord8(value)) return value;
|
|
1166
1114
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, clone(item)]));
|
|
1167
1115
|
}
|
|
1168
|
-
function
|
|
1116
|
+
function cloneEntry(entry) {
|
|
1169
1117
|
const copy = {};
|
|
1170
1118
|
for (const [key, value] of Object.entries(entry)) copy[key] = clone(value);
|
|
1171
1119
|
return copy;
|
|
1172
1120
|
}
|
|
1173
1121
|
function mergeEntry(base, override) {
|
|
1174
|
-
const result =
|
|
1122
|
+
const result = cloneEntry(base);
|
|
1175
1123
|
for (const [key, value] of Object.entries(override)) {
|
|
1176
1124
|
const existing = result[key];
|
|
1177
1125
|
result[key] = isRecord8(existing) && isRecord8(value) ? mergeEntry(existing, value) : clone(value);
|
|
1178
1126
|
}
|
|
1179
1127
|
return result;
|
|
1180
1128
|
}
|
|
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
1129
|
function regenerateModels(input) {
|
|
1190
1130
|
if (input.source === "none" && input.failedLiveFetch === true) {
|
|
1191
1131
|
const models2 = {};
|
|
1192
1132
|
for (const [key, entry] of Object.entries(input.currentModels ?? {}))
|
|
1193
|
-
if (entry !== void 0) models2[key] =
|
|
1133
|
+
if (entry !== void 0) models2[key] = cloneEntry(entry);
|
|
1194
1134
|
return { models: models2, prunedKeys: [], skipped: true };
|
|
1195
1135
|
}
|
|
1196
|
-
const current = input.currentModels ?? {};
|
|
1197
|
-
const generatedKeys = input.previousManifest?.generatedKeys ?? Object.keys(current);
|
|
1198
1136
|
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);
|
|
1137
|
+
for (const [key, entry] of Object.entries(input.freshModels)) models[key] = cloneEntry(entry);
|
|
1203
1138
|
for (const [key, override] of Object.entries(input.overrides ?? {}))
|
|
1204
1139
|
models[key] = mergeEntry(models[key] ?? {}, override);
|
|
1205
|
-
const
|
|
1206
|
-
return {
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1140
|
+
const prunedKeys = Object.keys(input.currentModels ?? {}).filter((key) => input.freshModels[key] === void 0 && input.overrides?.[key] === void 0).sort();
|
|
1141
|
+
return { models, prunedKeys };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/config/remove-legacy-manifest.ts
|
|
1145
|
+
import * as fs7 from "fs";
|
|
1146
|
+
import * as path8 from "path";
|
|
1147
|
+
function removeLegacyManifest(root) {
|
|
1148
|
+
const file = path8.join(root, ".opencode", "route33-generated.json");
|
|
1149
|
+
try {
|
|
1150
|
+
if (!fs7.existsSync(file)) return;
|
|
1151
|
+
fs7.rmSync(file);
|
|
1152
|
+
logger.info("route_33: removed obsolete route33-generated.json (regeneration is stateless since 0.4.4)");
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
logger.info(`route_33: unable to remove legacy route33-generated.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
1155
|
+
}
|
|
1211
1156
|
}
|
|
1212
1157
|
|
|
1213
1158
|
// src/config/collected-ref.ts
|
|
@@ -1437,6 +1382,7 @@ function createConfigHook(options = {}) {
|
|
|
1437
1382
|
let key;
|
|
1438
1383
|
let catalogAvailable = false;
|
|
1439
1384
|
try {
|
|
1385
|
+
removeLegacyManifest(cwd);
|
|
1440
1386
|
key = resolveKey(cfg, env, cwd);
|
|
1441
1387
|
const baseURL = resolveBaseUrl(cfg, env, cwd);
|
|
1442
1388
|
const catalog = await getCatalog({
|
|
@@ -1466,21 +1412,16 @@ function createConfigHook(options = {}) {
|
|
|
1466
1412
|
const regeneration = regenerateModels({
|
|
1467
1413
|
currentModels: provider.models,
|
|
1468
1414
|
freshModels: blocks.route_33.models,
|
|
1469
|
-
previousManifest: readGenerationManifest(cwd),
|
|
1470
1415
|
overrides: readModelOverrides(cwd),
|
|
1471
1416
|
source: catalog.source,
|
|
1472
1417
|
failedLiveFetch: catalog.liveFetchFailed
|
|
1473
1418
|
});
|
|
1474
1419
|
if (regeneration.skipped) {
|
|
1475
|
-
logger.info("route_33: catalog fetch failed without a cache; preserving existing
|
|
1420
|
+
logger.info("route_33: catalog fetch failed without a cache; preserving existing model entries");
|
|
1476
1421
|
} 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`);
|
|
1422
|
+
provider.models = regeneration.models;
|
|
1423
|
+
for (const dropped of regeneration.prunedKeys)
|
|
1424
|
+
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
1425
|
}
|
|
1485
1426
|
}
|
|
1486
1427
|
} catch (error) {
|
package/dist/tui/index.js
CHANGED
|
@@ -31,6 +31,9 @@ var logger = {
|
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
// src/shared/native-sdk-api-key-placeholder.ts
|
|
35
|
+
var NATIVE_SDK_API_KEY_PLACEHOLDER = "__route33_fetch_shim_auth__";
|
|
36
|
+
|
|
34
37
|
// src/shared/resolve-cache-dir.ts
|
|
35
38
|
import * as os from "os";
|
|
36
39
|
import * as path2 from "path";
|
|
@@ -142,9 +145,10 @@ function usable3(value) {
|
|
|
142
145
|
}
|
|
143
146
|
function keyFromConfig(cfg) {
|
|
144
147
|
const literal = usable3(cfg?.provider?.route_33?.options?.apiKey);
|
|
145
|
-
if (literal
|
|
146
|
-
|
|
147
|
-
return void 0;
|
|
148
|
+
if (literal === void 0) return void 0;
|
|
149
|
+
if (literal.includes(UNRESOLVED_TEMPLATE_MARKER)) return void 0;
|
|
150
|
+
if (literal === NATIVE_SDK_API_KEY_PLACEHOLDER) return void 0;
|
|
151
|
+
return literal;
|
|
148
152
|
}
|
|
149
153
|
function resolveKey(cfg, env = process.env, worktree) {
|
|
150
154
|
return usable3(env[ENV_API_KEY]) ?? keyFromConfig(cfg) ?? readWorkspaceKey(worktree) ?? (worktree !== void 0 && isInsideGitWorktree(worktree) ? void 0 : readAuthEntry(env).key);
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@route_33/opencode",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
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",
|