saros-proxy 0.11.0 → 1.1.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/README.md +149 -60
- package/assets/models.json +3349 -0
- package/dist/cli/autostart.d.ts +13 -6
- package/dist/cli/autostart.js +317 -60
- package/dist/cli/autostart.js.map +1 -1
- package/dist/cli/daemon.js +66 -8
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/harness-models.d.ts +5 -4
- package/dist/cli/harness-models.js +77 -30
- package/dist/cli/harness-models.js.map +1 -1
- package/dist/cli/help.js +5 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/opencode-config.d.ts +1 -0
- package/dist/cli/opencode-config.js +7 -6
- package/dist/cli/opencode-config.js.map +1 -1
- package/dist/cli/usage.js +13 -10
- package/dist/cli/usage.js.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -1
- package/dist/index.js +29 -36
- package/dist/index.js.map +1 -1
- package/dist/model-ladder.d.ts +30 -0
- package/dist/model-ladder.js +45 -0
- package/dist/model-ladder.js.map +1 -0
- package/dist/models-catalog.d.ts +25 -4
- package/dist/models-catalog.js +41 -12
- package/dist/models-catalog.js.map +1 -1
- package/dist/models-dev.d.ts +72 -0
- package/dist/models-dev.js +196 -0
- package/dist/models-dev.js.map +1 -0
- package/dist/models-fetcher.js +2 -0
- package/dist/models-fetcher.js.map +1 -1
- package/dist/models-refresh.d.ts +144 -0
- package/dist/models-refresh.js +522 -0
- package/dist/models-refresh.js.map +1 -0
- package/dist/models-sync.d.ts +6 -10
- package/dist/models-sync.js +26 -60
- package/dist/models-sync.js.map +1 -1
- package/dist/providers/commandcode.d.ts +20 -5
- package/dist/providers/commandcode.js +115 -7
- package/dist/providers/commandcode.js.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.js +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/opencode-go.d.ts +8 -0
- package/dist/providers/opencode-go.js +35 -7
- package/dist/providers/opencode-go.js.map +1 -1
- package/dist/providers/types.d.ts +10 -2
- package/dist/proxy-logic.d.ts +27 -4
- package/dist/proxy-logic.js +86 -32
- package/dist/proxy-logic.js.map +1 -1
- package/dist/proxy.js +41 -22
- package/dist/proxy.js.map +1 -1
- package/dist/token-usage.d.ts +9 -2
- package/dist/token-usage.js +20 -4
- package/dist/token-usage.js.map +1 -1
- package/dist/usage-client.d.ts +43 -13
- package/dist/usage-client.js +37 -54
- package/dist/usage-client.js.map +1 -1
- package/package.json +5 -4
- package/models.json +0 -1440
package/README.md
CHANGED
|
@@ -52,44 +52,47 @@ Manage multiple API keys behind a single endpoint — OpenCode-Go **and** Comman
|
|
|
52
52
|
|
|
53
53
|
## Architecture
|
|
54
54
|
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
55
|
+
```mermaid
|
|
56
|
+
flowchart TB
|
|
57
|
+
subgraph client["Your App / OpenCode"]
|
|
58
|
+
direction LR
|
|
59
|
+
r1["Request 1"]
|
|
60
|
+
r2["Request 2"]
|
|
61
|
+
r3["Request 3"]
|
|
62
|
+
rest["..."]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
subgraph saros["Saros :3000"]
|
|
66
|
+
direction LR
|
|
67
|
+
ks["Key Selector<br/>round-robin + failover"]
|
|
68
|
+
cb["Circuit Breaker<br/>threshold + cooldown"]
|
|
69
|
+
fw["Request Forwarder<br/>timeout + SSE"]
|
|
70
|
+
ks --> cb --> fw
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
subgraph keys["API keys"]
|
|
74
|
+
direction LR
|
|
75
|
+
ka["Key A<br/>sk-abc…"]
|
|
76
|
+
kb["Key B<br/>sk-def…"]
|
|
77
|
+
kc["Key C<br/>user_…"]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
oc["OpenCode-Go API<br/>opencode.ai"]
|
|
81
|
+
cc["CommandCode API<br/>api.commandcode.ai"]
|
|
82
|
+
|
|
83
|
+
r1 --> ks
|
|
84
|
+
r2 --> ks
|
|
85
|
+
r3 --> ks
|
|
86
|
+
rest --> ks
|
|
87
|
+
fw --> ka
|
|
88
|
+
fw --> kb
|
|
89
|
+
fw --> kc
|
|
90
|
+
ka --> oc
|
|
91
|
+
kb --> oc
|
|
92
|
+
kc --> oc
|
|
93
|
+
ka --> cc
|
|
94
|
+
kb --> cc
|
|
95
|
+
kc --> cc
|
|
93
96
|
```
|
|
94
97
|
|
|
95
98
|
### How Request Distribution Works
|
|
@@ -98,13 +101,46 @@ Here's how Saros picks which API key to use:
|
|
|
98
101
|
|
|
99
102
|
#### Usage-Weighted Rotation
|
|
100
103
|
|
|
101
|
-
Saros polls
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
Saros polls each key's quota endpoint and weights rotation by **remaining
|
|
105
|
+
monthly** quota. If key X is 30% into its monthly allowance and key Y is 70%,
|
|
106
|
+
10 requests are distributed 7 → X and 3 → Y — proportional balancing, not hard
|
|
107
|
+
cutoffs, so neither key absorbs the full load when the other runs low.
|
|
108
|
+
|
|
109
|
+
The number Saros compares is always a percentage of that key's **own provider
|
|
110
|
+
monthly budget**, never absolute capacity:
|
|
111
|
+
|
|
112
|
+
| provider | endpoint | what it reports |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| OpenCode Go | `GET /zen/go/v1/usage` | `rolling` (5h) / `weekly` / `monthly` as percents |
|
|
115
|
+
| CommandCode | `GET /alpha/billing/credits` | `fiveHour` and `weekly` as `used`/`cap` in credits, `credits.monthlyCredits` (remaining) and reset timestamps |
|
|
116
|
+
|
|
117
|
+
CommandCode's payload has no explicit monthly cap. The parser **infers** it
|
|
118
|
+
from the window caps (the 20% / 50% / 100% ladder is OpenCode Go's documented
|
|
119
|
+
plan shape; for CommandCode the 14/35 ratio was measured against its
|
|
120
|
+
dashboard): monthly cap = 2 × `weekly.cap`, falling back to 5 ×
|
|
121
|
+
`fiveHour.cap` when the weekly window is absent. The candidate cap is only
|
|
122
|
+
accepted when the credits agree with it — `credits.monthlyCredits <= cap` and
|
|
123
|
+
`cap − monthlyCredits >= max(weekly.used, fiveHour.used)`; otherwise the
|
|
124
|
+
fallback is tried and, if it also disagrees, the response is treated as
|
|
125
|
+
unusable (the previous usage state is kept rather than branding a broken
|
|
126
|
+
derivation as 0% usage). A payload without `monthlyCredits` reports the
|
|
127
|
+
monthly metric as unavailable (`n/a` in `saros-proxy usage`). Both parsers
|
|
128
|
+
emit the same 0-100 percent scale, so a mixed pool compares like with like.
|
|
129
|
+
Two consequences worth knowing:
|
|
130
|
+
|
|
131
|
+
- **Monthly is the weight; a spent window still excludes the key.** A key with
|
|
132
|
+
50% of the month left but a 5-hour or weekly window at 100% weighs 0 and is
|
|
133
|
+
not selected while another key has room — otherwise it would keep receiving
|
|
134
|
+
traffic and failing upstream. `/health`'s `usagePercent` is the monthly
|
|
135
|
+
number.
|
|
136
|
+
- **Percent is all there is.** OpenCode Go publishes percents only — no counts,
|
|
137
|
+
no caps — so "remaining capacity" in tokens or dollars cannot be derived.
|
|
105
138
|
|
|
106
139
|
- Refreshed in the background every 60s (stale data keeps working).
|
|
107
|
-
-
|
|
140
|
+
- Keys without usage data take the **median of the other keys' non-exhausted
|
|
141
|
+
weights**, not plain round-robin. Round-robin is only the fallback when no
|
|
142
|
+
key has usage data at all (or when every key is exhausted and a request must
|
|
143
|
+
share one).
|
|
108
144
|
- A key whose account has no plan for its provider (403 `EntitlementError` /
|
|
109
145
|
`upgrade_required` from the usage endpoint) is dropped from rotation without
|
|
110
146
|
tripping the circuit breaker, and returns as soon as a later refresh sees it
|
|
@@ -119,9 +155,9 @@ object for non-streaming bodies — is booked against the key that served the
|
|
|
119
155
|
request. Totals are persisted to `~/.config/saros/token-usage.json` (debounced
|
|
120
156
|
atomic writes) and shown by `saros-proxy usage`.
|
|
121
157
|
|
|
122
|
-
This is the
|
|
123
|
-
|
|
124
|
-
`saros-proxy usage`
|
|
158
|
+
This is the fallback signal: when a provider has no quota endpoint, or its usage
|
|
159
|
+
call fails, absolute token totals and request counts per key are what
|
|
160
|
+
`saros-proxy usage` can still show.
|
|
125
161
|
|
|
126
162
|
#### Circuit Breaker
|
|
127
163
|
|
|
@@ -140,11 +176,11 @@ Saros tracks which keys are currently in use. A key handling a streaming request
|
|
|
140
176
|
|
|
141
177
|
### Prompt Install(for the lazy ones)
|
|
142
178
|
|
|
143
|
-
Don't feel like reading through everything? Copy this prompt and paste it to your AI assistant. It'll read [`LLM_INSTRUCTIONS.md`](LLM_INSTRUCTIONS.md) and guide you through setup — no keys shared in chat.
|
|
179
|
+
Don't feel like reading through everything? Copy this prompt and paste it to your AI assistant. It'll read [`docs/LLM_INSTRUCTIONS.md`](docs/LLM_INSTRUCTIONS.md) and guide you through setup — no keys shared in chat.
|
|
144
180
|
|
|
145
181
|
```
|
|
146
182
|
Install Saros Proxy for my opencode. (https://github.com/vitorvilaca3011/saros-proxy)
|
|
147
|
-
Follow the instructions on LLM_INSTRUCTIONS.md.
|
|
183
|
+
Follow the instructions on docs/LLM_INSTRUCTIONS.md.
|
|
148
184
|
```
|
|
149
185
|
|
|
150
186
|
---
|
|
@@ -374,7 +410,7 @@ OPENCODE_GO_KEYS="account1:sk-xxx,cc:user_yyy"
|
|
|
374
410
|
- The `/health` endpoint reports per-provider key counts under `providers`, and a `keys` array with each key's state: `enabled`, `entitled`, `consecutiveFailures`, `disabledAt`, `retryAfterMs`, and `lastError` (`status` + `reason`: `auth`, `quota`, `rate_limit`, `server`). The credential is never part of the response.
|
|
375
411
|
- When no key can serve a request, the `503` body says which keys are out and why — `keys[]` with `reason`/`lastStatus`/`retryAfterMs`, plus a `causes[]` list (revoked key, exhausted quota, rate limit, missing plan, or a `@provider` pin with no usable key). The legacy `error` field (`All API keys are temporarily unavailable`) is still present.
|
|
376
412
|
- OpenCode-Go requires a stable per-conversation session id (`x-opencode-session`, used for routing and prompt caching). Saros forwards a client-supplied value untouched and derives one from the conversation (model + first user turn) when the client sends none — harnesses that only attach the header on some request paths would otherwise get `400 MissingSessionID`.
|
|
377
|
-
- Usage-based weighted rotation
|
|
413
|
+
- Usage-based weighted rotation uses each provider's queryable usage endpoint (OpenCode-Go `GET /zen/go/v1/usage`, CommandCode `GET /alpha/billing/credits`). Both contribute a monthly percentage of their own budget; token consumption stays tracked per key and shown by `saros-proxy usage` (see [Usage-Weighted Rotation](#usage-weighted-rotation) and [Token Accounting](#token-accounting)).
|
|
378
414
|
|
|
379
415
|
#### Smoke test (key identification)
|
|
380
416
|
|
|
@@ -445,14 +481,21 @@ into the "no passphrase source" boot error — a locked keyring is not reported
|
|
|
445
481
|
as an empty store.
|
|
446
482
|
|
|
447
483
|
**systemd-creds recipe** (keeps the passphrase out of the unit file and the
|
|
448
|
-
environment of the shell)
|
|
484
|
+
environment of the shell). This is a **system** unit recipe — run the encryption
|
|
485
|
+
as root, since the host key lives in `/var/lib/systemd/credential.secret`:
|
|
449
486
|
|
|
450
487
|
```bash
|
|
451
|
-
# One-time: encrypt stdin into a host
|
|
452
|
-
|
|
488
|
+
# One-time, as root: encrypt stdin into a host-bound credential.
|
|
489
|
+
# (--with-key=auto picks tpm2+host when the TPM is available, host otherwise.
|
|
490
|
+
# "host+user" is not a valid key type, and --user only exists from v256.)
|
|
491
|
+
systemd-creds encrypt --name=saros-passphrase --with-key=auto \
|
|
453
492
|
/dev/stdin /etc/saros/passphrase.cred
|
|
454
493
|
```
|
|
455
494
|
|
|
495
|
+
For the `systemd --user` unit created by `saros-proxy autostart install`, use
|
|
496
|
+
`Environment=`/`EnvironmentFile=` instead: user credentials need
|
|
497
|
+
`systemd-creds --user`, which only exists from systemd v256 (this host runs 255).
|
|
498
|
+
|
|
456
499
|
```ini
|
|
457
500
|
# /etc/systemd/system/saros-proxy.service
|
|
458
501
|
[Service]
|
|
@@ -555,16 +598,48 @@ saros-proxy probe
|
|
|
555
598
|
# Show per-key quota usage, token totals and most-used models
|
|
556
599
|
saros-proxy usage
|
|
557
600
|
|
|
558
|
-
# Install autostart (Windows: VBS script or Registry)
|
|
559
|
-
saros-proxy autostart install [--port <port>] [--method vbs|registry|auto]
|
|
601
|
+
# Install autostart (Linux: systemd --user; Windows: VBS script or Registry)
|
|
602
|
+
saros-proxy autostart install [--port <port>] [--method systemd|vbs|registry|auto]
|
|
560
603
|
|
|
561
604
|
# Uninstall autostart
|
|
562
|
-
saros-proxy autostart uninstall [--method vbs|registry|auto]
|
|
605
|
+
saros-proxy autostart uninstall [--method systemd|vbs|registry|auto]
|
|
563
606
|
|
|
564
607
|
# Check autostart status
|
|
565
|
-
saros-proxy autostart status [--method vbs|registry|auto]
|
|
608
|
+
saros-proxy autostart status [--method systemd|vbs|registry|auto]
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
On Linux the default is a `systemd --user` unit at
|
|
612
|
+
`~/.config/systemd/user/saros-proxy.service` (honouring `$XDG_CONFIG_HOME`), enabled
|
|
613
|
+
with `systemctl --user enable` and restarted with `systemctl --user restart`. On
|
|
614
|
+
Windows the default is AV-aware (`auto`): Registry when third-party antivirus is
|
|
615
|
+
detected, otherwise the Startup-folder VBS.
|
|
616
|
+
|
|
617
|
+
Manage the unit with systemd, not with the CLI: `saros-proxy stop` exits non-zero
|
|
618
|
+
while the service still answers `/health`, because the CLI does not own the unit's
|
|
619
|
+
process.
|
|
620
|
+
|
|
621
|
+
```bash
|
|
622
|
+
systemctl --user start|stop|restart|status saros-proxy.service
|
|
623
|
+
journalctl --user -u saros-proxy -f
|
|
566
624
|
```
|
|
567
625
|
|
|
626
|
+
The unit runs `serve` in the foreground: without a TTY it cannot prompt for a
|
|
627
|
+
passphrase, and the user manager does not inherit the environment of the shell
|
|
628
|
+
that ran `install`. If `config.yaml` holds `enc:v1:` envelopes, give the unit a
|
|
629
|
+
source through a drop-in (`systemctl --user edit saros-proxy.service`):
|
|
630
|
+
|
|
631
|
+
```ini
|
|
632
|
+
[Service]
|
|
633
|
+
Environment=SAROS_PASSPHRASE=your-passphrase
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
`EnvironmentFile=-%h/.config/saros/env` (mode 0600) keeps the value out of the
|
|
637
|
+
unit file. A credential is stronger, but `Environment=` can only carry the
|
|
638
|
+
credential *path* (`%d/<name>`), never its content — see the
|
|
639
|
+
[systemd-creds recipe](#api-key-encryption), which wraps `serve` in
|
|
640
|
+
`sh -c 'SAROS_PASSPHRASE="$(cat "$CREDENTIALS_DIRECTORY/<name>")" exec …'`.
|
|
641
|
+
`systemd-creds` only gained a `--user` mode in v256.
|
|
642
|
+
|
|
568
643
|
The `start` command automatically syncs model definitions to all enabled harnesses (see [Multi-Harness Integration](#multi-harness-integration-pi-oh-my-pi--dsh)).
|
|
569
644
|
|
|
570
645
|
### Model Sync Commands
|
|
@@ -716,7 +791,7 @@ Behavior:
|
|
|
716
791
|
- Sync never creates a harness config: harnesses whose config file is missing are skipped.
|
|
717
792
|
- Only `providers["saros-proxy"].models` is replaced; all other providers, fields, and settings are preserved, and a `<file>.backup` is created before the first write.
|
|
718
793
|
|
|
719
|
-
The model list written to each harness comes from the live provider catalogs (opencode-go) enriched with models.dev metadata, merged with any CommandCode-only models — falling back to bundled models when offline — so every harness stays current alongside OpenCode.
|
|
794
|
+
The model list written to each harness comes from the live provider catalogs (opencode-go) enriched with models.dev metadata, merged with any CommandCode-only models — falling back to the bundled `assets/models.json` when offline — so every harness stays current alongside OpenCode.
|
|
720
795
|
|
|
721
796
|
> **Why a static model list?** The harnesses use a static `models` list on purpose. `pi` has no `/v1/models` discovery for custom providers, and `oh-my-pi`'s discovery cannot recover reasoning/context/output metadata for provider model IDs. `dsh` replaces only `providers["saros-proxy"].models` in its patch layer (`cordis.patch.yml`), which dsh rewrites at every boot — a static, explicitly-managed list is the only reliable source. Dynamic discovery would silently break thinking-level (`:max`/`:high`) role configs.
|
|
722
797
|
|
|
@@ -727,9 +802,9 @@ The model list written to each harness comes from the live provider catalogs (op
|
|
|
727
802
|
Two kinds of upstream quota are visible in the provider documentation but are **not** modelled by Saros, on purpose — the proxy rotates keys and reasons about keys, not about what a single model or plan allows:
|
|
728
803
|
|
|
729
804
|
- **Model-level free tiers (CommandCode).** `glyph-cluster:free` is capped at 100 requests/day, `beam:free` has no daily cap, and the `laguna-s-2.1-free` tiers cost no credits. Saros counts tokens per key (`token-usage.json`) and per model, but it does not refuse a request because a *model* reached its daily free quota — the upstream returns the error and the key's circuit breaker decides what happens next.
|
|
730
|
-
- **Plan windows (OpenCode Go).** The Go plan meters each model against `5-hour — 20% of the monthly limit`, `weekly — 50%` and `monthly — 100%`.
|
|
805
|
+
- **Plan windows (OpenCode Go).** The Go plan meters each model against `5-hour — 20% of the monthly limit`, `weekly — 50%` and `monthly — 100%`. What was consumed is what feeds weighted rotation (see [Usage-Weighted Rotation](#usage-weighted-rotation)), but the proxy does not refuse a request because a model crossed a window: the upstream returns the error and that is what the client sees.
|
|
731
806
|
|
|
732
|
-
Both are documented here so the behaviour is not mistaken for a bug. Requests through the alternate OpenAI/Anthropic surfaces (`/v1/responses`, `/v1/messages`) and through the decision endpoint (`/v1/systemone`) are forwarded and counted: streamed usage nested under `response.usage` and the `input_tokens`/`output_tokens` shape are booked like any other call.
|
|
807
|
+
Both are documented here so the behaviour is not mistaken for a bug. Requests through the alternate OpenAI/Anthropic surfaces (`/v1/responses`, `/v1/messages`) and through the decision endpoint (`/v1/systemone`) are forwarded and counted: streamed usage nested under `response.usage` (Responses), the counts Anthropic splits between `message_start.message.usage` and `message_delta.usage` (Messages), and the `input_tokens`/`output_tokens` shape are booked like any other call — with one exception: Anthropic **cache** counters (`cache_read_input_tokens`, `cache_creation_input_tokens`) are not counted, so prompt-cached Messages traffic is under-booked on the prompt side.
|
|
733
808
|
|
|
734
809
|
---
|
|
735
810
|
|
|
@@ -796,6 +871,7 @@ Disable it with `SAROS_DEBUG_BUFFER=0`: the endpoint answers `404` and no entry
|
|
|
796
871
|
| `ECONNREFUSED` on startup | Port already in use | Change port or kill the existing process |
|
|
797
872
|
| `504 Gateway Timeout` | Upstream too slow | Increase `requestTimeoutMs` |
|
|
798
873
|
| `All API keys are temporarily unavailable` | All keys disabled by circuit breaker | Wait for cooldown or check upstream status |
|
|
874
|
+
| Upstream `403` with `error code: 1010` | Cloudflare in front of the provider rejected the `User-Agent` the client sent | Saros sets the provider's own identity (`cli`); if you see this, the request bypassed the proxy or a provider lost its `extraUpstreamHeaders` |
|
|
799
875
|
| CORS error | Client origin not allowed | Add it to `allowedOrigins` in config |
|
|
800
876
|
|
|
801
877
|
---
|
|
@@ -829,6 +905,18 @@ npm run build
|
|
|
829
905
|
|
|
830
906
|
Output goes to `dist/`.
|
|
831
907
|
|
|
908
|
+
### Refresh the bundled model catalog
|
|
909
|
+
|
|
910
|
+
`assets/models.json` is generated — do not edit it by hand. The generator merges models.dev metadata with the live opencode-go and CommandCode catalogs: a model that left upstream leaves the bundle, a model the upstream serves but models.dev does not list still enters with the live name and context window, and a field already in the file survives when models.dev omits it.
|
|
911
|
+
|
|
912
|
+
```bash
|
|
913
|
+
npm run models:refresh -- --dry-run # print the diff, write nothing
|
|
914
|
+
npm run models:refresh # rewrite assets/models.json
|
|
915
|
+
npm run models:refresh -- --allow-shrink # accept removing more than half of the models
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
A failed fetch — or a 2xx with an empty catalog — aborts before the file is read, and a merge that would remove more than half of the current models aborts unless `--allow-shrink` is given. The write goes through a temp file and an atomic rename, so a crash never leaves a truncated catalog.
|
|
919
|
+
|
|
832
920
|
### Project structure
|
|
833
921
|
|
|
834
922
|
```
|
|
@@ -842,7 +930,8 @@ src/
|
|
|
842
930
|
providers/ — Multi-provider key abstraction (KeyProvider, adapters, identifyKey)
|
|
843
931
|
models-fetcher.ts — Upstream model list fetching + caching
|
|
844
932
|
models-sync.ts — Auto-sync models from upstream to opencode config
|
|
845
|
-
models-catalog.ts — Single source of truth: loads
|
|
933
|
+
models-catalog.ts — Single source of truth: loads assets/models.json
|
|
934
|
+
models-refresh.ts — Generates that catalog (npm run models:refresh)
|
|
846
935
|
cli/
|
|
847
936
|
setup.ts — Interactive setup wizard
|
|
848
937
|
daemon.ts — Background process management
|