@lawaty/lacode 0.4.1 → 0.5.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/CHANGELOG.md +44 -1
- package/README.md +130 -551
- package/dist/agents.js +115 -3
- package/dist/commands.js +62 -15
- package/dist/index.js +326 -62
- package/dist/lacode.js +253 -11
- package/dist/pool.js +17 -6
- package/docs/CONFIGURATION.md +113 -0
- package/docs/INSTALL.md +209 -0
- package/docs/OMNIROUTE.md +232 -0
- package/package.json +1 -1
- package/presets/README.md +2 -2
- package/presets/free-tier.json +4 -4
package/docs/INSTALL.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Install and troubleshooting
|
|
2
|
+
|
|
3
|
+
Everything about getting LaCode loaded, and what to do when it silently is not. The one-line install is in the [README](../README.md#getting-started); this page is for the rest of it.
|
|
4
|
+
|
|
5
|
+
A plugin that fails to load is silent — opencode starts normally, prints nothing, logs nothing, and the agents are simply not there. Start at [If the agents are missing](#if-the-agents-are-missing).
|
|
6
|
+
|
|
7
|
+
## Where it is installed
|
|
8
|
+
|
|
9
|
+
opencode installs npm plugins itself, into one directory per specifier under its
|
|
10
|
+
cache — not into your project's `node_modules`, and not from anywhere a global
|
|
11
|
+
npm install put them. Ask opencode where that cache is instead of assuming,
|
|
12
|
+
because where it lives differs by platform:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
opencode debug paths
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
It prints the directories opencode is using on this machine — config, data, log
|
|
19
|
+
and the rest. Find the **cache** line and append
|
|
20
|
+
`/packages/<specifier>/node_modules/` to the path it reports. On Linux with no
|
|
21
|
+
`XDG_CACHE_HOME` set, that comes out as, for example:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
~/.cache/opencode/packages/@lawaty/lacode@latest/node_modules/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**That last line is an illustration of one platform's layout, not a path to
|
|
28
|
+
copy.** Where the same cache root sits on another operating system is not
|
|
29
|
+
something this document should guess at — read it from `opencode debug paths`
|
|
30
|
+
and use what it prints.
|
|
31
|
+
|
|
32
|
+
To inspect what a specifier actually resolved to, list its directory under that
|
|
33
|
+
cache root. An **empty** directory with no `node_modules` inside it means the
|
|
34
|
+
install failed — almost always an unpublished or misspelled package — and
|
|
35
|
+
opencode starts normally **without** the plugin. A **populated** directory there
|
|
36
|
+
means the install succeeded and tells you nothing at all about whether the
|
|
37
|
+
plugin was loaded; that is the next section, and telling those two apart is the
|
|
38
|
+
whole reason for reading it.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## If the agents are missing
|
|
42
|
+
|
|
43
|
+
A plugin that fails to load is silent: opencode starts normally, prints nothing,
|
|
44
|
+
logs nothing, and the agents are simply not there. Work down this. Every step
|
|
45
|
+
below is written to hold whichever opencode version you are running — none of
|
|
46
|
+
them depends on knowing the answer in advance.
|
|
47
|
+
|
|
48
|
+
**1 — Write the bare specifier and restart.**
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{ "plugin": ["@lawaty/lacode"] }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Nothing here hot-reloads, so the restart is part of the step, not a footnote.
|
|
55
|
+
|
|
56
|
+
**2 — Check whether the plugin loaded.**
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
opencode agent list | grep -E '^(explore|implement|context)'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A working install prints `explore`, `implement` and `context`, each with the
|
|
63
|
+
hidden `-1`..`-4` pool variants alongside. If the
|
|
64
|
+
grep returns nothing and the list holds only opencode's own built-ins, the plugin
|
|
65
|
+
did not load. Run it in the same shell environment opencode runs in — an
|
|
66
|
+
`agent list` under a different `HOME` or `XDG_CONFIG_HOME` tests a different
|
|
67
|
+
machine. **If the agents are listed, stop here: the one line above is the whole
|
|
68
|
+
install**, and nothing further in this section applies to you.
|
|
69
|
+
|
|
70
|
+
**3 — Decide which failure you are looking at.** Inspect the package directory
|
|
71
|
+
under the cache root from
|
|
72
|
+
[Where it is installed](#where-it-is-installed), and also check that `plugin`
|
|
73
|
+
really is in the config file opencode reads.
|
|
74
|
+
|
|
75
|
+
- **Empty** (no `node_modules` inside): the install failed — check the package
|
|
76
|
+
name, and that it is published. There is nothing to load yet.
|
|
77
|
+
- **Populated**: the install worked and the loader dropped the plugin. Go to
|
|
78
|
+
step 4. These two look identical from the outside, which is why this step
|
|
79
|
+
exists.
|
|
80
|
+
|
|
81
|
+
**4 — If the install worked and the agents are still missing, point the loader
|
|
82
|
+
at the built file.** Add a second entry; leave the bare name in place:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"plugin": [
|
|
87
|
+
"@lawaty/lacode",
|
|
88
|
+
"file:///ABSOLUTE-PATH-FROM-debug-paths/packages/@lawaty/lacode@latest/node_modules/@lawaty/lacode/dist/index.js"
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`ABSOLUTE-PATH-FROM-debug-paths` is a placeholder to be replaced, not a
|
|
94
|
+
directory: take the cache path `opencode debug paths` reports for **your**
|
|
95
|
+
machine and put it in front of `/packages/…`. It must be an absolute path with
|
|
96
|
+
`$HOME` already expanded and no `~` left in it. Re-derive it after a cache wipe
|
|
97
|
+
or a reinstall — the directory is keyed by specifier, so it moves when the
|
|
98
|
+
version does.
|
|
99
|
+
|
|
100
|
+
Why keep both entries instead of swapping the bare name out:
|
|
101
|
+
|
|
102
|
+
- The bare name is what makes opencode install the package into its cache, and
|
|
103
|
+
therefore what creates the path the `file://` entry points at.
|
|
104
|
+
- The `file://` entry is what actually loads the plugin.
|
|
105
|
+
- They **cannot double-inject** on opencode 1.18.35, where the bare name
|
|
106
|
+
contributes nothing at all: the pair loads the plugin exactly once. On any
|
|
107
|
+
other version this is untested, which is precisely why step 4 is something you
|
|
108
|
+
take *because* step 2 showed the agents missing, and never on the expectation
|
|
109
|
+
that it would help.
|
|
110
|
+
|
|
111
|
+
**5 — Restart, and expect two restarts after a cache wipe.** The first start
|
|
112
|
+
installs the package and creates the path the `file://` entry points at, but a
|
|
113
|
+
path that does not exist at startup is a path that cannot be loaded in that same
|
|
114
|
+
run. The second start finds it and loads the plugin. After that it is steady
|
|
115
|
+
state.
|
|
116
|
+
|
|
117
|
+
This package also publishes `main` and an `exports["./server"]` entry pointing at
|
|
118
|
+
the same built file, because those are two of the shapes a plugin loader
|
|
119
|
+
resolving an entry point consults. They are forward-compatibility metadata and
|
|
120
|
+
they cost nothing — but they do **not** fix this on 1.18.35: adding either one to
|
|
121
|
+
the *installed* cache copy was tested there and the bare specifier still loaded
|
|
122
|
+
nothing. No other version has been observed where they help either. Use the
|
|
123
|
+
`file://` entry.
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
## Which opencode versions this is known on
|
|
127
|
+
|
|
128
|
+
Exactly one version has been measured, and the procedure above is deliberately
|
|
129
|
+
not built around that fact. Treat this table as the evidence it is, which is one
|
|
130
|
+
row wide.
|
|
131
|
+
|
|
132
|
+
| opencode version | bare spec loads? | `file://` needed? | evidence |
|
|
133
|
+
| --- | --- | --- | --- |
|
|
134
|
+
| 1.18.35 | No — installs the package, then silently drops it | Yes | **Verified** on 1.18.35: a populated cache directory, zero agents injected, no error and no log line; the `file://` entry loads the full agent set. |
|
|
135
|
+
| Any other version | **Unknown** | **Unknown** | **Untested.** No other opencode version has been run against this package in either direction. |
|
|
136
|
+
|
|
137
|
+
Every command quoted on this page was run against 1.18.35. **If your version is
|
|
138
|
+
not the one measured here, follow the procedure above rather than reading this
|
|
139
|
+
table as coverage of your build**: each step stands on its own, and step 2 tells
|
|
140
|
+
you which situation you are in before any version-specific advice is relevant.
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
## macOS
|
|
144
|
+
|
|
145
|
+
The plugin runs on macOS. It is plain JavaScript with no native dependency, and
|
|
146
|
+
what the package publishes is the built JavaScript rather than TypeScript
|
|
147
|
+
sources, so nothing about loading it is Linux-only. Use the procedure above
|
|
148
|
+
unchanged.
|
|
149
|
+
|
|
150
|
+
Three things differ, and all three are worth reading rather than assuming:
|
|
151
|
+
|
|
152
|
+
- **Paths.** Do not copy a cache path out of this document — the example under
|
|
153
|
+
[Where it is installed](#where-it-is-installed) is one platform's layout. Run
|
|
154
|
+
`opencode debug paths` and build the `file://` entry from what it reports.
|
|
155
|
+
This page deliberately does not name a macOS cache root: where opencode keeps
|
|
156
|
+
it on a Mac has not been established here, and a plausible-looking guess is
|
|
157
|
+
worse than an instruction to look.
|
|
158
|
+
- **Notifications** are delivered through `osascript`, or through
|
|
159
|
+
`terminal-notifier` when `osascript` is not installed, rather than through the
|
|
160
|
+
Linux `notify-send` (US-26). `lacode doctor` reports which channel your machine
|
|
161
|
+
would actually use.
|
|
162
|
+
- **The repository's shell scripts are Linux-only by design**, and are not in the
|
|
163
|
+
published package. They drive one person's SSH topology, so each is on the side
|
|
164
|
+
it belongs to:
|
|
165
|
+
|
|
166
|
+
- **Remote hosts are plain npm clients.** A remote host installs the plugin the
|
|
167
|
+
same way (`opencode plugin @lawaty/lacode -g`); there is nothing to rsync. The
|
|
168
|
+
notification hop is a direct ssh (`ControlMaster` + `ControlPersist`) set up
|
|
169
|
+
once with `lacode tunnel setup` — no reverse forward, no listening port, no
|
|
170
|
+
systemd unit. See [Remote hosts](../README.md#remote-hosts) and docs/TUNNEL.md.
|
|
171
|
+
- The repo still carries the author's personal, Linux-only tooling from the older
|
|
172
|
+
reverse-forward setup (`bin/oc`, `bin/oc-notify`, `bin/oc-notify-receiver`,
|
|
173
|
+
`bin/oc-sync`). None of it is in the published package, and none of it is needed
|
|
174
|
+
for the npm-client path above.
|
|
175
|
+
|
|
176
|
+
**The test suite runs on macOS.** It is written to pass on a Mac as well as on
|
|
177
|
+
Linux, so "it works on a Mac" is a supported claim rather than an intention. The
|
|
178
|
+
few checks that exercise the Linux-only shell scripts above skip themselves on a
|
|
179
|
+
machine that cannot run them, naming the reason — a bash 3.2 with no `mapfile`,
|
|
180
|
+
or no GNU `timeout` — rather than passing silently or failing confusingly.
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
## The `lacode` CLI
|
|
184
|
+
|
|
185
|
+
The package also ships the `lacode` CLI (`notify`, `forward`, `doctor`, `receive`, `tunnel setup`) — see
|
|
186
|
+
[Remote hosts](../README.md#remote-hosts). The plugin install does **not** put it on your
|
|
187
|
+
`PATH`; run it from the package's `node_modules/.bin`, or install just the CLI
|
|
188
|
+
globally with `npm i -g @lawaty/lacode` if you want the bare command. (That
|
|
189
|
+
global install is for the command only — it is still never what makes the plugin
|
|
190
|
+
load.)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
## What the install gives you
|
|
194
|
+
|
|
195
|
+
Two things are worth knowing about what npm actually hands you:
|
|
196
|
+
|
|
197
|
+
- **`@opencode-ai/plugin` is a peer dependency.** It is not vendored into this
|
|
198
|
+
package and never will be: a second copy of the SDK inside the tarball would
|
|
199
|
+
mean opencode and the plugin holding two different module instances, and
|
|
200
|
+
instance identity is what plugin state hangs on. npm installs the peer for you;
|
|
201
|
+
if you install with a tool that does not, install `@opencode-ai/plugin` at the
|
|
202
|
+
same version yourself.
|
|
203
|
+
- **You get compiled JavaScript, not TypeScript sources.** The tarball ships
|
|
204
|
+
`dist/`, built from `src/` at pack time (`prepack` runs the build, so a stale
|
|
205
|
+
`dist/` cannot be published). This is not a preference: node strips types
|
|
206
|
+
natively from 22.18 but **refuses to for files under `node_modules`**, so a
|
|
207
|
+
package that shipped `.ts` worked fine in a checkout and failed on install —
|
|
208
|
+
`lacode --help` died with `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. A
|
|
209
|
+
published package cannot depend on its consumer's loader being lenient.
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# OmniRoute gateway integration
|
|
2
|
+
|
|
3
|
+
[OmniRoute](https://github.com/diegosouzapw/OmniRoute) is a local OpenAI-compatible gateway that routes a request across many providers with fallback and "combos". LaCode can register it as an opencode provider. It is **off by default**. The summary is in the [README](../README.md#configure); this page is the full reference, including a latency-measured combo that is worth applying on a new client.
|
|
4
|
+
|
|
5
|
+
## Configuring OmniRoute
|
|
6
|
+
|
|
7
|
+
[OmniRoute](https://github.com/diegosouzapw/OmniRoute) is a local
|
|
8
|
+
OpenAI-compatible AI gateway (default `http://localhost:20128/v1`) that routes a
|
|
9
|
+
request across many providers with fallback and "combos". LaCode can register it
|
|
10
|
+
as an opencode provider for you. It is **off by default**: if you do not set
|
|
11
|
+
`omniroute.enabled: true`, nothing below happens and LaCode makes no request.
|
|
12
|
+
|
|
13
|
+
**Setting it up on a client machine.** Four steps, in this order:
|
|
14
|
+
|
|
15
|
+
1. Install OmniRoute, then make it stay up: run `omniroute autostart enable`
|
|
16
|
+
once. That writes a systemd user unit in a headless session, or an XDG
|
|
17
|
+
autostart entry in a graphical one, and OmniRoute supervises itself from
|
|
18
|
+
there on. Its gateway listens on `http://localhost:20128/v1`. The gateway's
|
|
19
|
+
lifecycle is **the client's responsibility**: LaCode never starts, supervises
|
|
20
|
+
or stops it, it only probes it. The gateway must therefore already be
|
|
21
|
+
running **before** you start opencode — one that is down at startup is
|
|
22
|
+
disabled for that run, and the integration is per-run.
|
|
23
|
+
2. Add the plugin block to your opencode config:
|
|
24
|
+
`["@lawaty/lacode", { "omniroute": { "enabled": true, "apiKey": "<your omniroute key>", "combo": "auto", "models": [] } }]`
|
|
25
|
+
`auto` is the zero-config default, but on a free tier it often resolves to a
|
|
26
|
+
slow chain. [A tested fast combo](#a-tested-fast-combo-free-mix) below is
|
|
27
|
+
ordered for latency and is worth applying on a new client.
|
|
28
|
+
3. Optionally, to route pooled work through it, make `omniroute/auto 3` the
|
|
29
|
+
**first** line of `~/.config/lacode/pool.txt`. `pool.txt` is optional user
|
|
30
|
+
config; the bundled default preset does **not** include OmniRoute.
|
|
31
|
+
4. **Restart opencode.** Nothing here hot-reloads.
|
|
32
|
+
|
|
33
|
+
With `models: []` and `combo: "auto"`, the plugin registers exactly one model,
|
|
34
|
+
`omniroute/auto`, which then appears in the model picker for any agent — no
|
|
35
|
+
hand-maintained model list.
|
|
36
|
+
|
|
37
|
+
The full option set, for reference:
|
|
38
|
+
|
|
39
|
+
```jsonc
|
|
40
|
+
{
|
|
41
|
+
"plugin": [
|
|
42
|
+
["@lawaty/lacode", {
|
|
43
|
+
"omniroute": {
|
|
44
|
+
"enabled": true,
|
|
45
|
+
"baseURL": "http://localhost:20128/v1",
|
|
46
|
+
"apiKey": "sk_omniroute", // optional; OMNIROUTE_API_KEY overrides it
|
|
47
|
+
"combo": "static-best-free", // optional: let OmniRoute do the routing
|
|
48
|
+
"models": ["cc/claude-sonnet-4-6", "gemini-3-flash"], // hard allowlist
|
|
49
|
+
"tiers": {
|
|
50
|
+
"primary": ["cc/claude-sonnet-4-6"],
|
|
51
|
+
"backup": ["gemini-3-flash"]
|
|
52
|
+
},
|
|
53
|
+
"stickyPerSession": true,
|
|
54
|
+
"healthCheck": true,
|
|
55
|
+
"timeoutMs": 5000
|
|
56
|
+
}
|
|
57
|
+
}]
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Field | Default | Meaning |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `enabled` | `false` | Turns the integration on. |
|
|
65
|
+
| `baseURL` | `http://localhost:20128/v1` | Gateway base URL, normalised to exactly one `/v1`. |
|
|
66
|
+
| `apiKey` | — | Bearer key. **`OMNIROUTE_API_KEY` overrides this value.** The key is never logged. |
|
|
67
|
+
| `combo` | — | An OmniRoute combo id (or `auto`) to expose as a selectable model even though the gateway does not list it. Exposed first. |
|
|
68
|
+
| `models` | — | A **hard allowlist**: only these ids are ever exposed, even if the gateway lists more. |
|
|
69
|
+
| `tiers` | — | `{ "primary": [...], "backup": [...] }` preference order over models the allowlist already permits. |
|
|
70
|
+
| `stickyPerSession` | `true` | Keep a session on one model while that model is healthy. |
|
|
71
|
+
| `healthCheck` | `true` | Probe `GET /models` at startup; a failed check disables the integration for that run only. |
|
|
72
|
+
| `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. |
|
|
73
|
+
|
|
74
|
+
An enabled but invalid block (bad `baseURL`, a non-array `models`, …) is rejected
|
|
75
|
+
as a whole: LaCode logs one warning naming the problem and continues exactly as
|
|
76
|
+
if the integration were off. It never crashes opencode.
|
|
77
|
+
|
|
78
|
+
**What it registers.** On the first `config` pass LaCode health-checks the
|
|
79
|
+
gateway, reads `GET /models`, filters it through your `models` allowlist (or
|
|
80
|
+
exposes everything the gateway lists when no allowlist is set), and writes a
|
|
81
|
+
`provider.omniroute` entry — `npm: "@ai-sdk/openai-compatible"` with your
|
|
82
|
+
`baseURL` / `apiKey` — so `omniroute/<model-id>` becomes selectable. If you
|
|
83
|
+
already have a `provider.omniroute` block, LaCode leaves it completely untouched
|
|
84
|
+
and warns; it also never adds a `whitelist`. Because opencode loads
|
|
85
|
+
`@ai-sdk/openai-compatible` itself, no extra dependency is installed.
|
|
86
|
+
|
|
87
|
+
**Combo vs. allowlist.** Set `combo` and OmniRoute does the fallback for you
|
|
88
|
+
(let it route): the combo leads the exposed models, so opencode can hand a failed
|
|
89
|
+
request to the gateway's own fallback chain. Without a combo, LaCode exposes the
|
|
90
|
+
allowlist ordered by `tiers` (primary first). Candidate order is `filterModels`
|
|
91
|
+
(combo first, then the hard allowlist) followed by `orderByTier`, and that is
|
|
92
|
+
all — LaCode picks which model is exposed and in what order; it does not measure
|
|
93
|
+
latency or error rate at runtime, and it does not skip a model because it
|
|
94
|
+
failed.
|
|
95
|
+
|
|
96
|
+
**Using OmniRoute in the pool.** The integration only *registers* the provider —
|
|
97
|
+
it never touches your pool, and it does not place a slot for you. To route pooled
|
|
98
|
+
work (exploration, implementation, cartography) through the gateway, list it in
|
|
99
|
+
your pool like any other model:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
# ~/.config/lacode/pool.txt
|
|
103
|
+
omniroute/auto 3
|
|
104
|
+
opencode/space-bunny-free 2
|
|
105
|
+
opencode-go/longcat-2.5-preview-free 1
|
|
106
|
+
opencode/big-pickle 1
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`omniroute/auto` then takes slot 1 (weight 3) and drains pooled mechanical work
|
|
110
|
+
before it reaches the bundled opencode/opencode-go ("zen") models. Any
|
|
111
|
+
`omniroute/<id>` works, provided the id is exposed — by `combo`/`models`, or the
|
|
112
|
+
whole catalog when neither is set. Because the plugin writes the pool nothing,
|
|
113
|
+
the only thing that changes when the gateway is down is that `omniroute/<id>`
|
|
114
|
+
fails; every other slot keeps working. See
|
|
115
|
+
[docs/USER-STORIES.md](USER-STORIES.md) US-28.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
## A tested fast combo: `free-mix`
|
|
119
|
+
|
|
120
|
+
`combo: "auto"` is the zero-configuration default, but `auto` inherits whatever
|
|
121
|
+
the gateway chooses, and on a busy free tier that is frequently the slowest
|
|
122
|
+
chain available. The combo below was **measured** against a live free-tier
|
|
123
|
+
gateway and is ordered for latency rather than for breadth. Apply it during the
|
|
124
|
+
"Setting it up on a client machine" steps above and pooled work is usable
|
|
125
|
+
straight away, instead of starting from a gateway that queues for half a minute
|
|
126
|
+
per call.
|
|
127
|
+
|
|
128
|
+
```jsonc
|
|
129
|
+
{
|
|
130
|
+
"name": "free-mix",
|
|
131
|
+
// Order decides. Do NOT use "weighted" here — weights let the gateway
|
|
132
|
+
// re-shuffle a model that is returning 429 into first position again.
|
|
133
|
+
"strategy": "priority",
|
|
134
|
+
"models": [
|
|
135
|
+
{ "model": "gemini/gemini-3.1-flash-lite", "providerId": "gemini" },
|
|
136
|
+
{ "model": "gemini/gemini-3.8-flash", "providerId": "gemini" },
|
|
137
|
+
{ "model": "gemini/gemini-3.7-flash", "providerId": "gemini" },
|
|
138
|
+
{ "model": "gemini/gemini-3-flash-preview", "providerId": "gemini" },
|
|
139
|
+
{ "model": "openrouter/nvidia/nemotron-3-super-120b-a12b:free", "providerId": "openrouter" },
|
|
140
|
+
{ "model": "openrouter/nvidia/nemotron-3-ultra-550b-a55b:free", "providerId": "openrouter" },
|
|
141
|
+
{ "model": "openrouter/nvidia/nemotron-3.5-lightning:free", "providerId": "openrouter" },
|
|
142
|
+
{ "model": "openrouter/cohere/north-mini-code:free", "providerId": "openrouter" },
|
|
143
|
+
{ "model": "openrouter/poolside/laguna-s-2.1:free", "providerId": "openrouter" },
|
|
144
|
+
{ "model": "openrouter/dots-studio/dots-3-note-preview:free", "providerId": "openrouter" }
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Why this order, from 859 calls over a single day against that gateway:
|
|
150
|
+
|
|
151
|
+
| Model | Observed behaviour | Position |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| `gemini/gemini-3.1-flash-lite` | **165/172 succeeded (96%)**, ~4.5s median | **first** |
|
|
154
|
+
| `gemini-3.8-flash` / `3.7-flash` / `3-flash-preview` | succeed intermittently, but **burn exactly 30.0s** on a `503` queue-budget timeout when they do not | next |
|
|
155
|
+
| `openrouter/*:free` | **~90% `429`**, but fails *fast* (~1.8s) and occasionally succeeds | last, as a cheap final fallback |
|
|
156
|
+
| `gemini-2.5-flash`, `gemini-2.5-flash-lite` | **`404` on every attempt** — dead ids | **omitted entirely** |
|
|
157
|
+
|
|
158
|
+
The ordering matters because a combo pays for *every* step it walks past. Put the
|
|
159
|
+
96%-success model first and the common case is a single hop; the 30s models sit
|
|
160
|
+
behind it where they cost nothing, and the 429s sit last where they give up in
|
|
161
|
+
under two seconds.
|
|
162
|
+
|
|
163
|
+
Measured on the same gateway, same day, `max_tokens: 10`:
|
|
164
|
+
|
|
165
|
+
| | Before | After |
|
|
166
|
+
| --- | --- | --- |
|
|
167
|
+
| `free-mix` response | 42.7s / 41.8s / 34.7s | 4.1s / 3.1s / 3.2s / 4.0s / 3.2s |
|
|
168
|
+
| Median | **~41.8s** | **~3.2s** |
|
|
169
|
+
| Provider time burned on calls >25s | **56.9 of 102.4 min (56%)** | — |
|
|
170
|
+
|
|
171
|
+
Before the change, the real call sequence was a `429` after 2.7s, then a `503`
|
|
172
|
+
after 30.0s, then the actual work in 5.7s — roughly **33 seconds of dead time in
|
|
173
|
+
front of every successful request**, and a p90 of 100s when three 30s steps
|
|
174
|
+
chained.
|
|
175
|
+
|
|
176
|
+
These free-tier endpoints change availability constantly, so treat the ordering
|
|
177
|
+
as a **starting point to re-measure, not a permanent truth**. Re-check it with
|
|
178
|
+
OmniRoute's own call logs before assuming it still holds:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# one timed request through the gateway
|
|
182
|
+
curl -o /dev/null -w '%{http_code} %{time_starttransfer}s\n' \
|
|
183
|
+
-X POST http://localhost:20128/v1/chat/completions \
|
|
184
|
+
-H "Authorization: Bearer $OMNIROUTE_API_KEY" -H 'Content-Type: application/json' \
|
|
185
|
+
-d '{"model":"free-mix","messages":[{"role":"user","content":"Say OK"}],"max_tokens":10}'
|
|
186
|
+
|
|
187
|
+
# per-model status + duration, straight from OmniRoute's logs
|
|
188
|
+
python3 - <<'PY'
|
|
189
|
+
import json, glob, collections
|
|
190
|
+
d = "/home/<user>/.omniroute/call_logs/<YYYY-MM-DD>"
|
|
191
|
+
g = collections.defaultdict(collections.Counter)
|
|
192
|
+
for f in glob.glob(d + "/*.json"):
|
|
193
|
+
try: s = json.load(open(f))["summary"]
|
|
194
|
+
except Exception: continue
|
|
195
|
+
g[s.get("comboStepId")][s.get("status")] += 1
|
|
196
|
+
for k, v in sorted(g.items(), key=lambda x: str(x[0])):
|
|
197
|
+
print(f"{str(k)[:44]:44} {dict(v)}")
|
|
198
|
+
PY
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Apply it with the bundled CLI (`PUT /api/combos/<id>` updates in place, so the
|
|
202
|
+
combo is never briefly missing):
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
omniroute api combos get-api-combos-id- --id <combo-id> --json > free-mix.json
|
|
206
|
+
# edit name/strategy/models in free-mix.json, then:
|
|
207
|
+
omniroute api combos put-api-combos-id- --id <combo-id> --body @free-mix.json
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**A note on mid-session switching.** opencode 1.18.34 exposes no hook to change
|
|
211
|
+
an agent's main model mid-request, so LaCode cannot swap models inside a running
|
|
212
|
+
session; use an OmniRoute `combo` (or `auto`) if you want per-request fallback.
|
|
213
|
+
LaCode's selection engine decides which models are exposed and in what order, and
|
|
214
|
+
its cooldown/backoff rules are unit-tested.
|
|
215
|
+
|
|
216
|
+
**Getting a key.** Open the OmniRoute dashboard, go to API Manager / API Keys and
|
|
217
|
+
copy a key into `OMNIROUTE_API_KEY` (recommended) or `omniroute.apiKey`. On a
|
|
218
|
+
local instance with `REQUIRE_API_KEY=false`, the literal placeholder
|
|
219
|
+
`sk_omniroute` is accepted.
|
|
220
|
+
|
|
221
|
+
**Troubleshooting**
|
|
222
|
+
|
|
223
|
+
| Symptom | Cause | Fix |
|
|
224
|
+
| --- | --- | --- |
|
|
225
|
+
| `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. |
|
|
226
|
+
| `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. |
|
|
227
|
+
| `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 set it as `combo` — `combo` is registered whether or not the gateway advertises it, so a valid gateway route works there too. A valid gateway route is not automatically an exposed model. |
|
|
228
|
+
| `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. |
|
|
229
|
+
| `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`. |
|
|
230
|
+
| `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. |
|
|
231
|
+
| 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. |
|
|
232
|
+
| 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. |
|
package/package.json
CHANGED
package/presets/README.md
CHANGED
|
@@ -36,10 +36,10 @@ in place, no JSON and no objects:
|
|
|
36
36
|
|
|
37
37
|
```text
|
|
38
38
|
# provider/model-id weight
|
|
39
|
-
opencode-go/
|
|
39
|
+
opencode-go/longcat-2.5-preview-free 3
|
|
40
40
|
opencode/space-bunny-free 2
|
|
41
41
|
opencode/big-pickle 1
|
|
42
|
-
opencode/
|
|
42
|
+
opencode/longcat-2.5-preview-free 1
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
`#` starts a comment (whole-line or trailing), blank lines are ignored, and the
|
package/presets/free-tier.json
CHANGED
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
" 3. once EVERY slot is at its ceiling, the overflow goes to the least loaded slot,",
|
|
26
26
|
" so a burst past the summed ceilings degrades to even spreading and never blocks",
|
|
27
27
|
"",
|
|
28
|
-
"With this file: space-bunny takes 3 consecutive turns, then
|
|
29
|
-
"(opencode-go) takes 2, then big-pickle and
|
|
28
|
+
"With this file: space-bunny takes 3 consecutive turns, then longcat-2.5-preview",
|
|
29
|
+
"(opencode-go) takes 2, then big-pickle and longcat-2.5-preview (Zen) take 1 each -- sequence 1,1,1,2,2,3,4,...",
|
|
30
30
|
"Equal weights give a plain round robin in list order. 2,2,1,1 reproduces the old",
|
|
31
31
|
"primary/overflow cycle exactly, which is why the old tier values still work.",
|
|
32
32
|
"",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
],
|
|
68
68
|
"slots": [
|
|
69
69
|
{ "model": "opencode/space-bunny-free", "weight": 3 },
|
|
70
|
-
{ "model": "opencode-go/
|
|
70
|
+
{ "model": "opencode-go/longcat-2.5-preview-free", "weight": 2 },
|
|
71
71
|
{ "model": "opencode/big-pickle", "weight": 1 },
|
|
72
|
-
{ "model": "opencode/
|
|
72
|
+
{ "model": "opencode/longcat-2.5-preview-free", "weight": 1 }
|
|
73
73
|
]
|
|
74
74
|
}
|