@smart-coders-hq/opencode-model-fallback 1.3.0 → 1.3.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 CHANGED
@@ -1,29 +1,27 @@
1
1
  # opencode-model-fallback
2
2
 
3
- OpenCode plugin that adds automatic model fallback when your primary model hits a rate limit or quota. Instead of waiting in a retry loop, it immediately switches to the next healthy model in a configured chain per-agent, with a health state machine that tracks recovery.
3
+ OpenCode plugin that automatically switches to the next model in a configured fallback chain when the current one hits a rate limit, quota error, timeout, overload, or configured 5xx path.
4
4
 
5
- ## How it works
5
+ ## Features
6
6
 
7
- 1. **Preemptive redirect** — intercepts outgoing messages via `chat.message` hook; if the target model is known to be rate-limited, redirects the message to a healthy fallback _before_ it hits the provider (no 429 round-trip)
8
- 2. **Reactive fallback** if a fallback-triggering error still occurs (first hit, or preemptive not available), listens for both `session.status: retry` and `session.error` (`APIError`) events, aborts the retry loop, reverts the failed message, and replays it with the next healthy fallback model
9
- 3. Shows an inline toast notification and logs the event
10
- 4. Tracks model health globally (rate limits are account-wide) — automatically recovers after configurable cooldown periods
11
- 5. **Depth reset** when the TUI reverts to the original model between messages, `fallbackDepth` resets so `maxFallbackDepth` only guards true cascading failures within a single message
7
+ - Preemptive redirect via `chat.message` when a model is already known to be rate-limited
8
+ - Reactive fallback from both `session.status` retry events and `session.error` API errors
9
+ - Per-agent ordered fallback chains with `"*"` wildcard support
10
+ - Global model health tracking with automatic recovery windows
11
+ - `/fallback-status` slash command for session depth, history, and model health
12
+ - Structured logs with provider free-form error text redacted
12
13
 
13
14
  ## Installation
14
15
 
15
- Add to the `plugin` array in your `~/.config/opencode/opencode.jsonc`:
16
+ Add the plugin to `~/.config/opencode/opencode.jsonc`:
16
17
 
17
18
  ```jsonc
18
19
  {
19
- "plugin": [
20
- // ... existing plugins
21
- "@smart-coders-hq/opencode-model-fallback",
22
- ],
20
+ "plugin": ["@smart-coders-hq/opencode-model-fallback"],
23
21
  }
24
22
  ```
25
23
 
26
- Or load locally during development:
24
+ For local development:
27
25
 
28
26
  ```jsonc
29
27
  {
@@ -31,14 +29,14 @@ Or load locally during development:
31
29
  }
32
30
  ```
33
31
 
34
- Then create a config file (see [Configuration](#configuration)).
35
-
36
32
  ## Configuration
37
33
 
38
- Place `model-fallback.json` at either:
34
+ Create `model-fallback.json` in either:
35
+
36
+ - `.opencode/model-fallback.json`
37
+ - `~/.config/opencode/model-fallback.json`
39
38
 
40
- - `.opencode/model-fallback.json` — project-local
41
- - `~/.config/opencode/model-fallback.json` — global
39
+ Minimal example:
42
40
 
43
41
  ```json
44
42
  {
@@ -63,49 +61,22 @@ Place `model-fallback.json` at either:
63
61
  ]
64
62
  }
65
63
  },
66
- "patterns": [
67
- "rate limit",
68
- "usage limit",
69
- "too many requests",
70
- "quota exceeded",
71
- "overloaded",
72
- "capacity exceeded",
73
- "credits exhausted",
74
- "billing limit",
75
- "429"
76
- ],
77
64
  "logging": true,
78
65
  "logLevel": "info",
79
66
  "logPath": "~/.local/share/opencode/logs/model-fallback.log"
80
67
  }
81
68
  ```
82
69
 
83
- ### All config fields
84
-
85
- | Field | Type | Default | Description |
86
- | ------------------------------- | -------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
87
- | `enabled` | boolean | `true` | Enable/disable the plugin |
88
- | `defaults.fallbackOn` | string[] | all categories | Error categories that trigger fallback |
89
- | `defaults.cooldownMs` | number | `300000` (5 min) | How long before a rate-limited model enters cooldown. Min: 10000 |
90
- | `defaults.retryOriginalAfterMs` | number | `900000` (15 min) | How long before a cooldown model is considered healthy again. Min: 10000 |
91
- | `defaults.maxFallbackDepth` | number | `3` | Maximum number of fallbacks per session. Max: 10 |
92
- | `agents` | object | `{"*": {}}` | Per-agent fallback chains (see below) |
93
- | `patterns` | string[] | see defaults | Case-insensitive substrings to match in retry messages |
94
- | `logging` | boolean | `true` | Write structured logs to `logPath` |
95
- | `logLevel` | string | `"info"` | Minimum log level written to file: `"info"` suppresses debug noise, `"debug"` logs every event (useful for incident investigation) |
96
- | `logPath` | string | `~/.local/share/opencode/logs/model-fallback.log` | Log file path (must be within `$HOME`) |
97
-
98
- ### Error categories
99
-
100
- - `rate_limit` — 429, "rate limit", "too many requests", "usage limit"
101
- - `quota_exceeded` — "quota exceeded", "credits exhausted", "billing limit"
102
- - `overloaded` — "overloaded", "capacity exceeded"
103
- - `timeout` — "timeout", "timed out"
104
- - `5xx` — 500/502/503/504, "internal server error", "bad gateway"
70
+ Most important knobs:
105
71
 
106
- ## Per-agent chains
72
+ - `defaults.fallbackOn` - which error categories trigger fallback
73
+ - `defaults.cooldownMs` - how long a rate-limited model stays unavailable
74
+ - `defaults.retryOriginalAfterMs` - when a cooled-down model becomes healthy again
75
+ - `defaults.maxFallbackDepth` - max cascading fallbacks within one message
76
+ - `agents` - ordered fallback chains per agent
77
+ - `logging`, `logLevel`, `logPath` - structured file logging controls
107
78
 
108
- Configure different fallback chains for different agents using the agent name as the key. The `"*"` wildcard is used for any agent without a specific entry.
79
+ Per-agent example:
109
80
 
110
81
  ```json
111
82
  {
@@ -130,120 +101,54 @@ Configure different fallback chains for different agents using the agent name as
130
101
  }
131
102
  ```
132
103
 
133
- Models are tried in order. Rate-limited models are skipped; cooldown models are used as a last resort.
134
-
135
- ## Migrating from opencode-rate-limit-fallback
136
-
137
- If you have an existing `rate-limit-fallback.json` config, the plugin auto-migrates it on load — no manual steps needed.
138
-
139
- **Old format:**
140
-
141
- ```json
142
- {
143
- "fallbackModel": "anthropic/claude-opus-4-5",
144
- "cooldownMs": 300000,
145
- "patterns": ["rate limit"],
146
- "logging": true
147
- }
148
- ```
149
-
150
- **Automatically converted to:**
104
+ If you still have `rate-limit-fallback.json`, it is discovered and auto-migrated on load.
151
105
 
152
- ```json
153
- {
154
- "agents": { "*": { "fallbackModels": ["anthropic/claude-opus-4-5"] } },
155
- "defaults": { "cooldownMs": 300000 },
156
- "patterns": ["rate limit"],
157
- "logging": true
158
- }
159
- ```
160
-
161
- The plugin checks both `rate-limit-fallback.json` and `model-fallback.json` — old configs are found and migrated automatically.
162
-
163
- ## `/fallback-status` command
106
+ ## `/fallback-status`
164
107
 
165
108
  Run `/fallback-status` in any OpenCode session to see:
166
109
 
167
- - Current session's fallback depth and history
168
- - Health state of all tracked models (healthy / cooldown / rate_limited) with time remaining
169
- - Which agent is active
110
+ - current session fallback depth and history
111
+ - tracked model health and time remaining
112
+ - active agent name
170
113
 
171
- With the `verbose` flag:
114
+ Verbose mode adds token and cost breakdown by fallback period:
172
115
 
173
- ```
116
+ ```text
174
117
  /fallback-status verbose:true
175
118
  ```
176
119
 
177
- Includes token/cost breakdown per model period.
178
-
179
- When enabled, the plugin auto-creates `~/.config/opencode/commands/fallback-status.md` at startup so the slash command is available without manual setup.
180
-
181
- ## Health state machine
182
-
183
- ```
184
- healthy ──[rate limit detected]──→ rate_limited
185
- rate_limited ──[cooldownMs elapsed]──→ cooldown
186
- cooldown ──[retryOriginalAfterMs elapsed]──→ healthy
187
- ```
188
-
189
- - **healthy** — model is usable; preferred for fallback selection
190
- - **rate_limited** — recently hit a limit; skipped when walking fallback chain
191
- - **cooldown** — cooling off; used as last resort if no healthy model is available
192
- - State transitions are checked every 30 seconds via a background timer (the timer is unref'ed so it does not keep the process alive)
193
- - When the original model recovers to healthy, a toast appears on the next `session.idle`
120
+ When enabled, the plugin auto-creates `~/.config/opencode/commands/fallback-status.md` at startup.
194
121
 
195
122
  ## Troubleshooting
196
123
 
197
- **Toast doesn't appear**
198
- The TUI notification requires an active OpenCode TUI session. Headless/API usage won't show toasts but logs are always written.
124
+ - **No toast appears** - toasts require an active OpenCode TUI session; headless/API runs still log events
125
+ - **`/fallback-status` is missing** - verify `~/.config/opencode/commands/` is writable and check logs for `fallback-status.command.write.failed`
126
+ - **"no fallback chain configured"** - add `agents["*"]` or an entry for the active agent with at least one `fallbackModels` value
127
+ - **"all fallback models exhausted"** - every configured fallback is currently rate-limited; wait for recovery or add more models
128
+ - **"max fallback depth reached"** - all models in the chain failed within one message; start a new session or raise `maxFallbackDepth`
199
129
 
200
- **`/fallback-status` command is missing**
201
- The plugin writes `~/.config/opencode/commands/fallback-status.md` on startup. If the command does not appear, verify the directory is writable and check for `fallback-status.command.write.failed` in OpenCode logs.
202
-
203
- **"no fallback chain configured"**
204
- Your `model-fallback.json` has no `agents["*"].fallbackModels` (or no entry for the active agent). Add at least a wildcard entry with one model.
205
-
206
- **"all fallback models exhausted"**
207
- All configured fallback models are currently rate-limited. Wait for `cooldownMs` to elapse or add more models to the chain.
208
-
209
- **"max fallback depth reached"**
210
- The session has hit `maxFallbackDepth` cascading fallbacks within a single message (all models failing in sequence). Depth resets automatically when the TUI reverts to the original model between messages, so this typically indicates all configured models are rate-limited simultaneously. Start a new session or increase `maxFallbackDepth` in config.
211
-
212
- **Check the logs:**
130
+ Check logs with:
213
131
 
214
132
  ```bash
215
133
  tail -f ~/.local/share/opencode/logs/model-fallback.log | jq .
216
134
  ```
217
135
 
218
- Key log events: `plugin.init`, `retry.detected`, `fallback.success`, `fallback.exhausted`, `health.transition`, `recovery.available`
219
-
220
- To see the full event stream (including `event.received` and `retry.nomatch`), set `"logLevel": "debug"` in your config and restart OpenCode.
221
-
222
- For safety, free-form provider error text is redacted in plugin logs; use category/model/session fields for diagnosis.
223
-
224
- ## Release automation
225
-
226
- - Uses **Conventional Commits** + `semantic-release` for automated versioning/changelog/release notes
227
- - CI runs lint, tests, type check, and build on every push/PR via `.github/workflows/ci.yml`
228
- - Trusted release gate runs on pushes to `main` via `.github/workflows/release-gate.yml`
229
- - Release workflow (`.github/workflows/release.yml`) runs on successful `Release Gate` completion (`workflow_run`), only for `push` events on `main`, and only when `head_sha` matches `github.sha`
230
- - Published as `@smart-coders-hq/opencode-model-fallback`
231
- - To publish to npm, set repository secret `NPM_TOKEN`
136
+ For more event detail, set `"logLevel": "debug"` and restart OpenCode.
232
137
 
233
138
  ## Development
234
139
 
235
140
  ```bash
236
141
  bun install
237
- bun run lint # lint checks
238
- bun test # 163 tests across 16 files
239
- bunx tsc --noEmit # type check
240
- bun run build # build to dist/
142
+ bun run lint
143
+ bun test
144
+ bunx tsc --noEmit
145
+ bun run build
241
146
  ```
242
147
 
243
- Load locally in OpenCode:
148
+ Local plugin config:
244
149
 
245
150
  ```jsonc
246
151
  { "plugin": ["file:///absolute/path/to/dist/index.js"] }
247
152
  ```
248
153
 
249
- Config for testing: place `model-fallback.json` in `.opencode/` in your project directory.
154
+ For local testing, place `model-fallback.json` in `.opencode/` in your project directory.
package/dist/index.js CHANGED
@@ -4271,7 +4271,8 @@ async function handleIdle(sessionId, client, store, _config, logger) {
4271
4271
  return;
4272
4272
  logger.info("recovery.available", {
4273
4273
  sessionId,
4274
- originalModel: state.originalModel
4274
+ originalModel: state.originalModel,
4275
+ currentModel: state.currentModel
4275
4276
  });
4276
4277
  await notifyRecovery(client, state.originalModel);
4277
4278
  state.recoveryNotifiedForModel = state.originalModel;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAK9C,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAIhD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAG7C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAQjD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAWjF;AAED,eAAO,MAAM,YAAY,EAAE,MA2F1B,CAAC;AAEF,wBAAsB,WAAW,CAC/B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EACvC,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,QAAQ,CAAC,EAC/C,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAwDf;AAwFD,wBAAsB,UAAU,CAC9B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EACvC,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,QAAQ,CAAC,EAChD,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAuBf"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAK9C,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAIhD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAG7C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAQjD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAWjF;AAED,eAAO,MAAM,YAAY,EAAE,MA2F1B,CAAC;AAEF,wBAAsB,WAAW,CAC/B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EACvC,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,QAAQ,CAAC,EAC/C,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAwDf;AAwFD,wBAAsB,UAAU,CAC9B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EACvC,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,QAAQ,CAAC,EAChD,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAwBf"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smart-coders-hq/opencode-model-fallback",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Ordered model fallback chains with health tracking for OpenCode",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/plugin.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "id": "opencode-model-fallback",
4
4
  "name": "Model Fallback",
5
5
  "description": "Ordered model fallback chains with health tracking and preemptive redirects for OpenCode",
6
- "version": "1.3.0",
6
+ "version": "1.3.1",
7
7
  "entry": "dist/index.js",
8
8
  "links": [
9
9
  {