@lawaty/lacode 0.2.0 → 0.2.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/README.md +58 -23
- package/dist/commands.js +30 -0
- package/dist/index.js +35 -47
- package/dist/pool.js +2 -36
- package/docs/USER-STORIES.md +7 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -324,6 +324,29 @@ request across many providers with fallback and "combos". LaCode can register it
|
|
|
324
324
|
as an opencode provider for you. It is **off by default**: if you do not set
|
|
325
325
|
`omniroute.enabled: true`, nothing below happens and LaCode makes no request.
|
|
326
326
|
|
|
327
|
+
**Setting it up on a client machine.** Four steps, in this order:
|
|
328
|
+
|
|
329
|
+
1. Install OmniRoute, then make it stay up: run `omniroute autostart enable`
|
|
330
|
+
once. That writes a systemd user unit in a headless session, or an XDG
|
|
331
|
+
autostart entry in a graphical one, and OmniRoute supervises itself from
|
|
332
|
+
there on. Its gateway listens on `http://localhost:20128/v1`. The gateway's
|
|
333
|
+
lifecycle is **the client's responsibility**: LaCode never starts, supervises
|
|
334
|
+
or stops it, it only probes it. The gateway must therefore already be
|
|
335
|
+
running **before** you start opencode — one that is down at startup is
|
|
336
|
+
disabled for that run, and the integration is per-run.
|
|
337
|
+
2. Add the plugin block to your opencode config:
|
|
338
|
+
`["@lawaty/lacode", { "omniroute": { "enabled": true, "apiKey": "<your omniroute key>", "combo": "auto", "models": [] } }]`
|
|
339
|
+
3. Optionally, to route pooled work through it, make `omniroute/auto 3` the
|
|
340
|
+
**first** line of `~/.config/lacode/pool.txt`. `pool.txt` is optional user
|
|
341
|
+
config; the bundled default preset does **not** include OmniRoute.
|
|
342
|
+
4. **Restart opencode.** Nothing here hot-reloads.
|
|
343
|
+
|
|
344
|
+
With `models: []` and `combo: "auto"`, the plugin registers exactly one model,
|
|
345
|
+
`omniroute/auto`, which then appears in the model picker for any agent — no
|
|
346
|
+
hand-maintained model list.
|
|
347
|
+
|
|
348
|
+
The full option set, for reference:
|
|
349
|
+
|
|
327
350
|
```jsonc
|
|
328
351
|
{
|
|
329
352
|
"plugin": [
|
|
@@ -352,12 +375,12 @@ as an opencode provider for you. It is **off by default**: if you do not set
|
|
|
352
375
|
| `enabled` | `false` | Turns the integration on. |
|
|
353
376
|
| `baseURL` | `http://localhost:20128/v1` | Gateway base URL, normalised to exactly one `/v1`. |
|
|
354
377
|
| `apiKey` | — | Bearer key. **`OMNIROUTE_API_KEY` overrides this value.** The key is never logged. |
|
|
355
|
-
| `combo` | — | An OmniRoute combo (or `auto`)
|
|
378
|
+
| `combo` | — | An OmniRoute combo id (or `auto`) to expose as a selectable model even though the gateway does not list it. Exposed first. |
|
|
356
379
|
| `models` | — | A **hard allowlist**: only these ids are ever exposed, even if the gateway lists more. |
|
|
357
380
|
| `tiers` | — | `{ "primary": [...], "backup": [...] }` preference order over models the allowlist already permits. |
|
|
358
381
|
| `stickyPerSession` | `true` | Keep a session on one model while that model is healthy. |
|
|
359
382
|
| `healthCheck` | `true` | Probe `GET /models` at startup; a failed check disables the integration for that run only. |
|
|
360
|
-
| `timeoutMs` | `5000` | Budget for LaCode's own calls to the gateway. |
|
|
383
|
+
| `timeoutMs` | `5000` | Budget for LaCode's own probe and model-cache calls to the gateway. It is not written into the provider options, so it does not bound real traffic — opencode's own default applies there. |
|
|
361
384
|
|
|
362
385
|
An enabled but invalid block (bad `baseURL`, a non-array `models`, …) is rejected
|
|
363
386
|
as a whole: LaCode logs one warning naming the problem and continues exactly as
|
|
@@ -372,25 +395,34 @@ already have a `provider.omniroute` block, LaCode leaves it completely untouched
|
|
|
372
395
|
and warns; it also never adds a `whitelist`. Because opencode loads
|
|
373
396
|
`@ai-sdk/openai-compatible` itself, no extra dependency is installed.
|
|
374
397
|
|
|
375
|
-
**Combo vs. allowlist.** Set `combo` and OmniRoute does the fallback for you
|
|
376
|
-
it route)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
398
|
+
**Combo vs. allowlist.** Set `combo` and OmniRoute does the fallback for you
|
|
399
|
+
(let it route): the combo leads the exposed models, so opencode can hand a failed
|
|
400
|
+
request to the gateway's own fallback chain. Without a combo, LaCode exposes the
|
|
401
|
+
allowlist ordered by `tiers` (primary first). Candidate order is `filterModels`
|
|
402
|
+
(combo first, then the hard allowlist) followed by `orderByTier`, and that is
|
|
403
|
+
all — LaCode picks which model is exposed and in what order; it does not measure
|
|
404
|
+
latency or error rate at runtime, and it does not skip a model because it
|
|
405
|
+
failed.
|
|
406
|
+
|
|
407
|
+
**Using OmniRoute in the pool.** The integration only *registers* the provider —
|
|
408
|
+
it never touches your pool, and it does not place a slot for you. To route pooled
|
|
409
|
+
work (exploration, implementation, cartography) through the gateway, list it in
|
|
410
|
+
your pool like any other model:
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
# ~/.config/lacode/pool.txt
|
|
414
|
+
omniroute/auto 3
|
|
415
|
+
opencode/space-bunny-free 2
|
|
416
|
+
opencode-go/step-5-preview-free 1
|
|
417
|
+
opencode/big-pickle 1
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
`omniroute/auto` then takes slot 1 (weight 3) and drains pooled mechanical work
|
|
421
|
+
before it reaches the bundled opencode/opencode-go ("zen") models. Any
|
|
422
|
+
`omniroute/<id>` works, provided the id is exposed — by `combo`/`models`, or the
|
|
423
|
+
whole catalog when neither is set. Because the plugin writes the pool nothing,
|
|
424
|
+
the only thing that changes when the gateway is down is that `omniroute/<id>`
|
|
425
|
+
fails; every other slot keeps working. See
|
|
394
426
|
[docs/USER-STORIES.md](docs/USER-STORIES.md) US-28.
|
|
395
427
|
|
|
396
428
|
**A note on mid-session switching.** opencode 1.18.34 exposes no hook to change
|
|
@@ -408,12 +440,14 @@ local instance with `REQUIRE_API_KEY=false`, the literal placeholder
|
|
|
408
440
|
|
|
409
441
|
| Symptom | Cause | Fix |
|
|
410
442
|
| --- | --- | --- |
|
|
411
|
-
| `omniroute health check failed` at startup | Gateway not running, wrong `baseURL`, or a startup race | Start OmniRoute on `http://localhost:20128` and restart opencode. The integration is disabled for that run only, not permanently. |
|
|
443
|
+
| `omniroute health check failed` at startup | Gateway not running, wrong `baseURL`, or a startup race | Start OmniRoute on `http://localhost:20128` and restart opencode; `omniroute autostart enable` makes that durable across restarts. The integration is disabled for that run only, not permanently. |
|
|
444
|
+
| `omniroute health check failed` at startup, then a repeating `cooling omniroute/<id> (hang)` and `Model not found` churn on one slot | OmniRoute was started *after* opencode, and `pool.txt` names an `omniroute/<id>` slot | The integration is off for that run, but the pool still routes to the slot: the request hangs, the reaper cools it for 600s, and the cycle repeats. Start OmniRoute, then restart opencode. |
|
|
445
|
+
| `ProviderModelNotFoundError: Model not found` on an id the gateway serves | The id is a valid OmniRoute route but is not in the plugin's `models` allowlist, so the plugin never exposed it | Add the id to `models`, or use `combo: "auto"` with `omniroute/auto`. A valid gateway route is not automatically an exposed model. |
|
|
412
446
|
| `no usable models` | The gateway returned an empty list, or every id was filtered out by `models` | `curl -H "Authorization: Bearer $OMNIROUTE_API_KEY" http://localhost:20128/v1/models` and check `models`/`combo` against the returned ids. |
|
|
413
447
|
| `401` / `Invalid API key` | `REQUIRE_API_KEY=true` and the key is missing or wrong | Create a key in the dashboard, export `OMNIROUTE_API_KEY`, or set `REQUIRE_API_KEY=false` locally and use `sk_omniroute`. |
|
|
414
448
|
| `404` on a URL containing `/v1/v1/` | A `baseURL` that already carried `/v1` was given twice | LaCode normalises this, so it should not come from LaCode; check a hand-written `provider.omniroute` block, which LaCode leaves untouched. |
|
|
415
449
|
| Provider registered but models are missing | You already had `provider.omniroute` | Remove it and let LaCode write it, or add the models yourself — LaCode never modifies an existing provider. |
|
|
416
|
-
| Pooled work still runs on the bundled models | The gateway was unhealthy/disabled at startup, or
|
|
450
|
+
| Pooled work still runs on the bundled models | The gateway was unhealthy/disabled at startup, or you have no `omniroute/…` slot in your pool | Check the startup log for `omniroute health check failed` / `no usable models`, then add `omniroute/<model>` to `pool.txt` (or `pool.json`) to route pooled work through the gateway. |
|
|
417
451
|
|
|
418
452
|
### Which models run which agent
|
|
419
453
|
|
|
@@ -524,6 +558,7 @@ are reading:
|
|
|
524
558
|
/context-init # build the map, or verify and refresh an existing one
|
|
525
559
|
/context-update # incremental refresh; accepts a focus, e.g. /context-update src/payments
|
|
526
560
|
/context-review # audit it for staleness, contradictions, duplication and bloat
|
|
561
|
+
/lacode-doctor # check the pool, model resolution and the OmniRoute gateway. Report only
|
|
527
562
|
```
|
|
528
563
|
|
|
529
564
|
The map is local and untracked — it describes your checkout on your machine, so
|
package/dist/commands.js
CHANGED
|
@@ -67,6 +67,36 @@ Audit the five context files for:
|
|
|
67
67
|
|
|
68
68
|
Fix what you find in \`.opencode/context/\` only, then report: findings, fixes,
|
|
69
69
|
and anything you could not verify.`
|
|
70
|
+
},
|
|
71
|
+
"lacode-doctor": {
|
|
72
|
+
description: "Check the LaCode setup \u2014 pool slots, model resolution, and the OmniRoute gateway. Report only; changes nothing.",
|
|
73
|
+
template: `Diagnose this machine's LaCode setup. Report only; change nothing.
|
|
74
|
+
|
|
75
|
+
Print one PASS/FAIL line per check below, with the exact fix for any FAIL.
|
|
76
|
+
|
|
77
|
+
1. Pool status \u2014 call the \`pool_status\` tool. Report the slot list, each
|
|
78
|
+
slot's load against its weight ceiling, and which models are cooling.
|
|
79
|
+
|
|
80
|
+
2. OmniRoute configured \u2014 inspect the \`@lawaty/lacode\` plugin options in the
|
|
81
|
+
user's opencode config for an \`omniroute\` block with \`enabled: true\`. If it
|
|
82
|
+
is absent or disabled, print the two OmniRoute checks below as "not
|
|
83
|
+
configured" and stop after the pool section.
|
|
84
|
+
|
|
85
|
+
3. Gateway reachable \u2014 when omniroute is configured, request its model list
|
|
86
|
+
(\`GET <baseURL>/v1/models\`, default baseURL \`http://localhost:20128/v1\`)
|
|
87
|
+
and report reachable or not. On failure, say plainly: start OmniRoute BEFORE
|
|
88
|
+
opencode, then restart opencode \u2014 provider registration is per-run. To make
|
|
89
|
+
that durable, run \`omniroute autostart enable\` (OmniRoute's own autostart;
|
|
90
|
+
LaCode never starts the gateway itself).
|
|
91
|
+
|
|
92
|
+
4. Slots resolve \u2014 for every pool slot whose provider is \`omniroute\`, confirm
|
|
93
|
+
the model id is one the gateway advertises AND is registered. The known
|
|
94
|
+
trap: an id that is a valid gateway route but is NOT in the plugin's
|
|
95
|
+
\`omniroute.models\` allowlist fails with
|
|
96
|
+
\`ProviderModelNotFoundError: Model not found\`; the fix is to add that id to
|
|
97
|
+
the \`models\` array, or use the always-registered \`auto\`.
|
|
98
|
+
|
|
99
|
+
Change nothing. Print a final one-line summary of failures.`
|
|
70
100
|
}
|
|
71
101
|
};
|
|
72
102
|
function applyCommands(cfg, commands = COMMANDS) {
|
package/dist/index.js
CHANGED
|
@@ -250,51 +250,19 @@ function state(opts = {}) {
|
|
|
250
250
|
dir,
|
|
251
251
|
modelsFile,
|
|
252
252
|
inlineModels: models,
|
|
253
|
-
primarySlot: null,
|
|
254
253
|
origin: "unresolved",
|
|
255
254
|
slots: []
|
|
256
255
|
};
|
|
257
256
|
}
|
|
258
257
|
return g[key];
|
|
259
258
|
}
|
|
260
|
-
function replacePrimarySlot(base, primary) {
|
|
261
|
-
const problems = [];
|
|
262
|
-
const model = typeof primary?.model === "string" ? primary.model.trim() : "";
|
|
263
|
-
if (!model.includes("/")) {
|
|
264
|
-
problems.push(`primary slot: model must be written provider/model-id, got "${model}"`);
|
|
265
|
-
return { slots: base, problems };
|
|
266
|
-
}
|
|
267
|
-
const provider = providerOf(model);
|
|
268
|
-
if (base.some((slot) => providerOf(slot.model) === provider)) {
|
|
269
|
-
problems.push(`primary slot ${model}: provider ${provider} is already in the pool; the primary slot is skipped`);
|
|
270
|
-
return { slots: base, problems };
|
|
271
|
-
}
|
|
272
|
-
if (base.length === 0) return { slots: [{ index: 1, model, weight: Math.max(1, primary.weight ?? 1) }], problems };
|
|
273
|
-
let weight = primary.weight;
|
|
274
|
-
if (weight !== void 0 && (typeof weight !== "number" || !Number.isFinite(weight) || weight <= 0)) {
|
|
275
|
-
problems.push(`primary slot ${model}: weight must be a positive number; inheriting the replaced slot's weight`);
|
|
276
|
-
weight = void 0;
|
|
277
|
-
}
|
|
278
|
-
const slots = base.map((slot, i) => i === 0 ? { ...slot, model, weight: weight ?? slot.weight } : slot);
|
|
279
|
-
return { slots, problems };
|
|
280
|
-
}
|
|
281
|
-
function setPrimarySlot(s, primary) {
|
|
282
|
-
s.primarySlot = primary;
|
|
283
|
-
if (s.slots.length > 0) s.slots = [];
|
|
284
|
-
}
|
|
285
259
|
function ensureSlots(s, onWarn) {
|
|
286
260
|
if (s.slots.length > 0) return s.slots;
|
|
287
261
|
const { slots, problems, origin } = resolveModels({ models: s.inlineModels, modelsFile: s.modelsFile });
|
|
288
|
-
|
|
289
|
-
if (s.primarySlot !== null) {
|
|
290
|
-
const replaced = replacePrimarySlot(slots, s.primarySlot);
|
|
291
|
-
resolved = replaced.slots;
|
|
292
|
-
problems.push(...replaced.problems);
|
|
293
|
-
}
|
|
294
|
-
s.slots = resolved;
|
|
262
|
+
s.slots = slots;
|
|
295
263
|
s.origin = origin;
|
|
296
264
|
for (const problem of problems) onWarn(problem);
|
|
297
|
-
return
|
|
265
|
+
return slots;
|
|
298
266
|
}
|
|
299
267
|
var claimKey = (sessionID, callID) => `${sessionID}:${callID}`;
|
|
300
268
|
var selfFile = (id) => `claims.${id}.json`;
|
|
@@ -1304,6 +1272,36 @@ Audit the five context files for:
|
|
|
1304
1272
|
|
|
1305
1273
|
Fix what you find in \`.opencode/context/\` only, then report: findings, fixes,
|
|
1306
1274
|
and anything you could not verify.`
|
|
1275
|
+
},
|
|
1276
|
+
"lacode-doctor": {
|
|
1277
|
+
description: "Check the LaCode setup \u2014 pool slots, model resolution, and the OmniRoute gateway. Report only; changes nothing.",
|
|
1278
|
+
template: `Diagnose this machine's LaCode setup. Report only; change nothing.
|
|
1279
|
+
|
|
1280
|
+
Print one PASS/FAIL line per check below, with the exact fix for any FAIL.
|
|
1281
|
+
|
|
1282
|
+
1. Pool status \u2014 call the \`pool_status\` tool. Report the slot list, each
|
|
1283
|
+
slot's load against its weight ceiling, and which models are cooling.
|
|
1284
|
+
|
|
1285
|
+
2. OmniRoute configured \u2014 inspect the \`@lawaty/lacode\` plugin options in the
|
|
1286
|
+
user's opencode config for an \`omniroute\` block with \`enabled: true\`. If it
|
|
1287
|
+
is absent or disabled, print the two OmniRoute checks below as "not
|
|
1288
|
+
configured" and stop after the pool section.
|
|
1289
|
+
|
|
1290
|
+
3. Gateway reachable \u2014 when omniroute is configured, request its model list
|
|
1291
|
+
(\`GET <baseURL>/v1/models\`, default baseURL \`http://localhost:20128/v1\`)
|
|
1292
|
+
and report reachable or not. On failure, say plainly: start OmniRoute BEFORE
|
|
1293
|
+
opencode, then restart opencode \u2014 provider registration is per-run. To make
|
|
1294
|
+
that durable, run \`omniroute autostart enable\` (OmniRoute's own autostart;
|
|
1295
|
+
LaCode never starts the gateway itself).
|
|
1296
|
+
|
|
1297
|
+
4. Slots resolve \u2014 for every pool slot whose provider is \`omniroute\`, confirm
|
|
1298
|
+
the model id is one the gateway advertises AND is registered. The known
|
|
1299
|
+
trap: an id that is a valid gateway route but is NOT in the plugin's
|
|
1300
|
+
\`omniroute.models\` allowlist fails with
|
|
1301
|
+
\`ProviderModelNotFoundError: Model not found\`; the fix is to add that id to
|
|
1302
|
+
the \`models\` array, or use the always-registered \`auto\`.
|
|
1303
|
+
|
|
1304
|
+
Change nothing. Print a final one-line summary of failures.`
|
|
1307
1305
|
}
|
|
1308
1306
|
};
|
|
1309
1307
|
function applyCommands(cfg, commands = COMMANDS) {
|
|
@@ -2639,7 +2637,6 @@ var omniRouteHooks = async (input, options, deps = {}) => {
|
|
|
2639
2637
|
});
|
|
2640
2638
|
const selector = createSelector(config, { now: deps.now });
|
|
2641
2639
|
let disabled = false;
|
|
2642
|
-
let poolModel = null;
|
|
2643
2640
|
const probe = async () => {
|
|
2644
2641
|
if (config.healthCheck) {
|
|
2645
2642
|
const healthy = await checkHealth({
|
|
@@ -2650,7 +2647,7 @@ var omniRouteHooks = async (input, options, deps = {}) => {
|
|
|
2650
2647
|
});
|
|
2651
2648
|
if (!healthy) {
|
|
2652
2649
|
disabled = true;
|
|
2653
|
-
await warnOnce(`omniroute health check failed at ${config.baseURL}; falling back to lacode's normal behaviour for this run`);
|
|
2650
|
+
await warnOnce(`omniroute health check failed at ${config.baseURL}; falling back to lacode's normal behaviour for this run; pool slots naming omniroute/<id> will fail until the gateway is up`);
|
|
2654
2651
|
return;
|
|
2655
2652
|
}
|
|
2656
2653
|
}
|
|
@@ -2674,8 +2671,6 @@ var omniRouteHooks = async (input, options, deps = {}) => {
|
|
|
2674
2671
|
return;
|
|
2675
2672
|
}
|
|
2676
2673
|
selector.setCandidates(candidates);
|
|
2677
|
-
const chosen = config.combo ?? (config.models !== void 0 && config.models.length > 0 ? candidates[0] : "auto");
|
|
2678
|
-
poolModel = `omniroute/${chosen}`;
|
|
2679
2674
|
await log("info", `omniroute: ${candidates.length} model(s) from ${config.baseURL}: ${candidates.join(", ")}`);
|
|
2680
2675
|
};
|
|
2681
2676
|
let started = null;
|
|
@@ -2687,10 +2682,8 @@ var omniRouteHooks = async (input, options, deps = {}) => {
|
|
|
2687
2682
|
return;
|
|
2688
2683
|
}
|
|
2689
2684
|
const exposed = selector.candidates();
|
|
2690
|
-
const target = poolModel !== null ? poolModel.slice("omniroute/".length) : null;
|
|
2691
|
-
const ids = target !== null && !exposed.includes(target) ? [target, ...exposed] : exposed;
|
|
2692
2685
|
const models = {};
|
|
2693
|
-
for (const id of
|
|
2686
|
+
for (const id of exposed) models[id] = { name: id };
|
|
2694
2687
|
providers.omniroute = {
|
|
2695
2688
|
name: "OmniRoute",
|
|
2696
2689
|
// The documented opencode registration mechanism for a gateway speaking
|
|
@@ -2704,15 +2697,10 @@ var omniRouteHooks = async (input, options, deps = {}) => {
|
|
|
2704
2697
|
baseURL: config.baseURL,
|
|
2705
2698
|
// Absent rather than blank: an empty bearer on a keyless gateway is a
|
|
2706
2699
|
// 401 that looks like a configuration error.
|
|
2707
|
-
...config.apiKey ? { apiKey: config.apiKey } : {}
|
|
2708
|
-
timeout: config.timeoutMs
|
|
2700
|
+
...config.apiKey ? { apiKey: config.apiKey } : {}
|
|
2709
2701
|
},
|
|
2710
2702
|
models
|
|
2711
2703
|
};
|
|
2712
|
-
if (poolModel !== null) {
|
|
2713
|
-
const ps = state(options ?? {});
|
|
2714
|
-
setPrimarySlot(ps, { model: poolModel });
|
|
2715
|
-
}
|
|
2716
2704
|
};
|
|
2717
2705
|
return {
|
|
2718
2706
|
config: async (cfg) => {
|
package/dist/pool.js
CHANGED
|
@@ -250,51 +250,19 @@ function state(opts = {}) {
|
|
|
250
250
|
dir,
|
|
251
251
|
modelsFile,
|
|
252
252
|
inlineModels: models,
|
|
253
|
-
primarySlot: null,
|
|
254
253
|
origin: "unresolved",
|
|
255
254
|
slots: []
|
|
256
255
|
};
|
|
257
256
|
}
|
|
258
257
|
return g[key];
|
|
259
258
|
}
|
|
260
|
-
function replacePrimarySlot(base, primary) {
|
|
261
|
-
const problems = [];
|
|
262
|
-
const model = typeof primary?.model === "string" ? primary.model.trim() : "";
|
|
263
|
-
if (!model.includes("/")) {
|
|
264
|
-
problems.push(`primary slot: model must be written provider/model-id, got "${model}"`);
|
|
265
|
-
return { slots: base, problems };
|
|
266
|
-
}
|
|
267
|
-
const provider = providerOf(model);
|
|
268
|
-
if (base.some((slot) => providerOf(slot.model) === provider)) {
|
|
269
|
-
problems.push(`primary slot ${model}: provider ${provider} is already in the pool; the primary slot is skipped`);
|
|
270
|
-
return { slots: base, problems };
|
|
271
|
-
}
|
|
272
|
-
if (base.length === 0) return { slots: [{ index: 1, model, weight: Math.max(1, primary.weight ?? 1) }], problems };
|
|
273
|
-
let weight = primary.weight;
|
|
274
|
-
if (weight !== void 0 && (typeof weight !== "number" || !Number.isFinite(weight) || weight <= 0)) {
|
|
275
|
-
problems.push(`primary slot ${model}: weight must be a positive number; inheriting the replaced slot's weight`);
|
|
276
|
-
weight = void 0;
|
|
277
|
-
}
|
|
278
|
-
const slots = base.map((slot, i) => i === 0 ? { ...slot, model, weight: weight ?? slot.weight } : slot);
|
|
279
|
-
return { slots, problems };
|
|
280
|
-
}
|
|
281
|
-
function setPrimarySlot(s, primary) {
|
|
282
|
-
s.primarySlot = primary;
|
|
283
|
-
if (s.slots.length > 0) s.slots = [];
|
|
284
|
-
}
|
|
285
259
|
function ensureSlots(s, onWarn) {
|
|
286
260
|
if (s.slots.length > 0) return s.slots;
|
|
287
261
|
const { slots, problems, origin } = resolveModels({ models: s.inlineModels, modelsFile: s.modelsFile });
|
|
288
|
-
|
|
289
|
-
if (s.primarySlot !== null) {
|
|
290
|
-
const replaced = replacePrimarySlot(slots, s.primarySlot);
|
|
291
|
-
resolved = replaced.slots;
|
|
292
|
-
problems.push(...replaced.problems);
|
|
293
|
-
}
|
|
294
|
-
s.slots = resolved;
|
|
262
|
+
s.slots = slots;
|
|
295
263
|
s.origin = origin;
|
|
296
264
|
for (const problem of problems) onWarn(problem);
|
|
297
|
-
return
|
|
265
|
+
return slots;
|
|
298
266
|
}
|
|
299
267
|
var claimKey = (sessionID, callID) => `${sessionID}:${callID}`;
|
|
300
268
|
var selfFile = (id) => `claims.${id}.json`;
|
|
@@ -615,11 +583,9 @@ export {
|
|
|
615
583
|
refreshCatalog,
|
|
616
584
|
release,
|
|
617
585
|
releaseStuck,
|
|
618
|
-
replacePrimarySlot,
|
|
619
586
|
resolveModels,
|
|
620
587
|
retryAfterMs,
|
|
621
588
|
selfFile,
|
|
622
|
-
setPrimarySlot,
|
|
623
589
|
slotFor,
|
|
624
590
|
snapshot,
|
|
625
591
|
splitModel,
|
package/docs/USER-STORIES.md
CHANGED
|
@@ -140,16 +140,16 @@ correct exactly when a whitelist exists (it is a real restriction, and the pool
|
|
|
140
140
|
models would otherwise resolve to nothing) and is not merely unnecessary when one
|
|
141
141
|
does not.
|
|
142
142
|
|
|
143
|
-
### US-28: A local gateway (OmniRoute)
|
|
144
|
-
**As a** user running a local OpenAI-compatible gateway, **I want** LaCode to
|
|
143
|
+
### US-28: A local gateway (OmniRoute) is registered as an ordinary provider
|
|
144
|
+
**As a** user running a local OpenAI-compatible gateway, **I want** LaCode to register it so its models are selectable and I can name one in my pool, **so that** *I* decide whether pooled work drains through it instead of the bundled models.
|
|
145
145
|
|
|
146
146
|
**Story ID(s) for tests:** US-28
|
|
147
147
|
**Acceptance criteria:**
|
|
148
|
-
- [ ] Given `omniroute.enabled` with a reachable gateway
|
|
149
|
-
- [ ] Given
|
|
150
|
-
- [ ] Given
|
|
151
|
-
- [ ] Given
|
|
152
|
-
- [ ] Given
|
|
148
|
+
- [ ] Given `omniroute.enabled` with a reachable gateway, When the config hook runs, Then LaCode registers a `provider.omniroute` entry exposing the gateway's models (everything it lists, or filtered by `models`/`combo`), and writes NOTHING into the pool.
|
|
149
|
+
- [ ] Given any pool source (bundled preset, `pool.json`, `pool.txt`, or `options.models`), When OmniRoute is enabled and healthy, Then every slot resolves exactly as it would without the integration — the slot list is unchanged, so opting in means the user lists `omniroute/<model>` themselves.
|
|
150
|
+
- [ ] Given a pool slot naming an `omniroute/<model>`, When the pool resolves and binds, Then that slot routes as an ordinary slot, and the pool does not warn the provider is missing, because OmniRoute registers the provider before the pool's config hook runs.
|
|
151
|
+
- [ ] Given OmniRoute is disabled, enabled with an invalid block, unhealthy, or returns no usable models, When the config hook runs, Then no provider is registered, no pool slot changes, and a failure is warned about once.
|
|
152
|
+
- [ ] Given the gateway is unreachable, When pooled work runs, Then the only thing affected is a slot that names `omniroute/<model>`; every other slot keeps working, because the plugin never put an `omniroute` slot in the pool.
|
|
153
153
|
|
|
154
154
|
---
|
|
155
155
|
|
package/package.json
CHANGED