@lawaty/lacode 0.2.0 → 0.2.1
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 +21 -14
- package/dist/index.js +3 -44
- package/dist/pool.js +2 -36
- package/docs/USER-STORIES.md +7 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -352,7 +352,7 @@ as an opencode provider for you. It is **off by default**: if you do not set
|
|
|
352
352
|
| `enabled` | `false` | Turns the integration on. |
|
|
353
353
|
| `baseURL` | `http://localhost:20128/v1` | Gateway base URL, normalised to exactly one `/v1`. |
|
|
354
354
|
| `apiKey` | — | Bearer key. **`OMNIROUTE_API_KEY` overrides this value.** The key is never logged. |
|
|
355
|
-
| `combo` | — | An OmniRoute combo (or `auto`)
|
|
355
|
+
| `combo` | — | An OmniRoute combo id (or `auto`) to expose as a selectable model even though the gateway does not list it. Exposed first. |
|
|
356
356
|
| `models` | — | A **hard allowlist**: only these ids are ever exposed, even if the gateway lists more. |
|
|
357
357
|
| `tiers` | — | `{ "primary": [...], "backup": [...] }` preference order over models the allowlist already permits. |
|
|
358
358
|
| `stickyPerSession` | `true` | Keep a session on one model while that model is healthy. |
|
|
@@ -379,18 +379,25 @@ model while it is cooling after a transient failure — 429, 408 and 5xx, honour
|
|
|
379
379
|
`Retry-After` and otherwise using capped exponential backoff. Non-transient
|
|
380
380
|
errors (400, 401, 403, 404) are surfaced to you, never failed over.
|
|
381
381
|
|
|
382
|
-
**OmniRoute
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
382
|
+
**Using OmniRoute in the pool.** The integration only *registers* the provider —
|
|
383
|
+
it never touches your pool, and it does not place a slot for you. To route pooled
|
|
384
|
+
work (exploration, implementation, cartography) through the gateway, list it in
|
|
385
|
+
your pool like any other model:
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
# ~/.config/lacode/pool.txt
|
|
389
|
+
omniroute/auto 3
|
|
390
|
+
opencode/space-bunny-free 2
|
|
391
|
+
opencode-go/step-5-preview-free 1
|
|
392
|
+
opencode/big-pickle 1
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
`omniroute/auto` then takes slot 1 (weight 3) and drains pooled mechanical work
|
|
396
|
+
before it reaches the bundled opencode/opencode-go ("zen") models. Any
|
|
397
|
+
`omniroute/<id>` works, provided the id is exposed — by `combo`/`models`, or the
|
|
398
|
+
whole catalog when neither is set. Because the plugin writes the pool nothing,
|
|
399
|
+
the only thing that changes when the gateway is down is that `omniroute/<id>`
|
|
400
|
+
fails; every other slot keeps working. See
|
|
394
401
|
[docs/USER-STORIES.md](docs/USER-STORIES.md) US-28.
|
|
395
402
|
|
|
396
403
|
**A note on mid-session switching.** opencode 1.18.34 exposes no hook to change
|
|
@@ -413,7 +420,7 @@ local instance with `REQUIRE_API_KEY=false`, the literal placeholder
|
|
|
413
420
|
| `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
421
|
| `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
422
|
| 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
|
|
423
|
+
| 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
424
|
|
|
418
425
|
### Which models run which agent
|
|
419
426
|
|
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`;
|
|
@@ -2639,7 +2607,6 @@ var omniRouteHooks = async (input, options, deps = {}) => {
|
|
|
2639
2607
|
});
|
|
2640
2608
|
const selector = createSelector(config, { now: deps.now });
|
|
2641
2609
|
let disabled = false;
|
|
2642
|
-
let poolModel = null;
|
|
2643
2610
|
const probe = async () => {
|
|
2644
2611
|
if (config.healthCheck) {
|
|
2645
2612
|
const healthy = await checkHealth({
|
|
@@ -2674,8 +2641,6 @@ var omniRouteHooks = async (input, options, deps = {}) => {
|
|
|
2674
2641
|
return;
|
|
2675
2642
|
}
|
|
2676
2643
|
selector.setCandidates(candidates);
|
|
2677
|
-
const chosen = config.combo ?? (config.models !== void 0 && config.models.length > 0 ? candidates[0] : "auto");
|
|
2678
|
-
poolModel = `omniroute/${chosen}`;
|
|
2679
2644
|
await log("info", `omniroute: ${candidates.length} model(s) from ${config.baseURL}: ${candidates.join(", ")}`);
|
|
2680
2645
|
};
|
|
2681
2646
|
let started = null;
|
|
@@ -2687,10 +2652,8 @@ var omniRouteHooks = async (input, options, deps = {}) => {
|
|
|
2687
2652
|
return;
|
|
2688
2653
|
}
|
|
2689
2654
|
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
2655
|
const models = {};
|
|
2693
|
-
for (const id of
|
|
2656
|
+
for (const id of exposed) models[id] = { name: id };
|
|
2694
2657
|
providers.omniroute = {
|
|
2695
2658
|
name: "OmniRoute",
|
|
2696
2659
|
// The documented opencode registration mechanism for a gateway speaking
|
|
@@ -2709,10 +2672,6 @@ var omniRouteHooks = async (input, options, deps = {}) => {
|
|
|
2709
2672
|
},
|
|
2710
2673
|
models
|
|
2711
2674
|
};
|
|
2712
|
-
if (poolModel !== null) {
|
|
2713
|
-
const ps = state(options ?? {});
|
|
2714
|
-
setPrimarySlot(ps, { model: poolModel });
|
|
2715
|
-
}
|
|
2716
2675
|
};
|
|
2717
2676
|
return {
|
|
2718
2677
|
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