@smart-coders-hq/opencode-model-fallback 1.2.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 429 still occurs (first hit, or preemptive not available), listens for `session.status: retry` 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
70
+ Most important knobs:
99
71
 
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"
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
105
78
 
106
- ## Per-agent chains
107
-
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,112 +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:**
151
-
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.
104
+ If you still have `rate-limit-fallback.json`, it is discovered and auto-migrated on load.
162
105
 
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
- ## Health state machine
180
-
181
- ```
182
- healthy ──[rate limit detected]──→ rate_limited
183
- rate_limited ──[cooldownMs elapsed]──→ cooldown
184
- cooldown ──[retryOriginalAfterMs elapsed]──→ healthy
185
- ```
186
-
187
- - **healthy** — model is usable; preferred for fallback selection
188
- - **rate_limited** — recently hit a limit; skipped when walking fallback chain
189
- - **cooldown** — cooling off; used as last resort if no healthy model is available
190
- - State transitions are checked every 30 seconds via a background timer
191
- - 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.
192
121
 
193
122
  ## Troubleshooting
194
123
 
195
- **Toast doesn't appear**
196
- The TUI notification requires an active OpenCode TUI session. Headless/API usage won't show toasts but logs are always written.
197
-
198
- **"no fallback chain configured"**
199
- Your `model-fallback.json` has no `agents["*"].fallbackModels` (or no entry for the active agent). Add at least a wildcard entry with one model.
200
-
201
- **"all fallback models exhausted"**
202
- All configured fallback models are currently rate-limited. Wait for `cooldownMs` to elapse or add more models to the chain.
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`
203
129
 
204
- **"max fallback depth reached"**
205
- 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.
206
-
207
- **Check the logs:**
130
+ Check logs with:
208
131
 
209
132
  ```bash
210
133
  tail -f ~/.local/share/opencode/logs/model-fallback.log | jq .
211
134
  ```
212
135
 
213
- Key log events: `plugin.init`, `retry.detected`, `fallback.success`, `fallback.exhausted`, `health.transition`, `recovery.available`
214
-
215
- To see the full event stream (including `event.received` and `retry.nomatch`), set `"logLevel": "debug"` in your config and restart OpenCode.
216
-
217
- ## Release automation
218
-
219
- - Uses **Conventional Commits** + `semantic-release` for automated versioning/changelog/release notes
220
- - CI runs lint, tests, type check, and build on every push/PR via `.github/workflows/ci.yml`
221
- - Release workflow runs on `main` after successful CI via `.github/workflows/release.yml`
222
- - Published as `@smart-coders-hq/opencode-model-fallback`
223
- - To publish to npm, set repository secret `NPM_TOKEN`
136
+ For more event detail, set `"logLevel": "debug"` and restart OpenCode.
224
137
 
225
138
  ## Development
226
139
 
227
140
  ```bash
228
141
  bun install
229
- bun run lint # lint checks
230
- bun test # 145 tests across 11 files
231
- bunx tsc --noEmit # type check
232
- bun run build # build to dist/
142
+ bun run lint
143
+ bun test
144
+ bunx tsc --noEmit
145
+ bun run build
233
146
  ```
234
147
 
235
- Load locally in OpenCode:
148
+ Local plugin config:
236
149
 
237
150
  ```jsonc
238
151
  { "plugin": ["file:///absolute/path/to/dist/index.js"] }
239
152
  ```
240
153
 
241
- 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
@@ -1,5 +1,5 @@
1
1
  // src/plugin.ts
2
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
2
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
3
3
  import { homedir as homedir5 } from "os";
4
4
  import { dirname as dirname2, join as join4 } from "path";
5
5
 
@@ -3305,7 +3305,7 @@ function labelModel(key) {
3305
3305
  }
3306
3306
 
3307
3307
  // src/logging/logger.ts
3308
- import { appendFileSync, existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
3308
+ import { appendFileSync, mkdirSync, writeFileSync } from "fs";
3309
3309
  import { dirname } from "path";
3310
3310
 
3311
3311
  class Logger {
@@ -3314,6 +3314,7 @@ class Logger {
3314
3314
  enabled;
3315
3315
  minLevel;
3316
3316
  dirCreated = false;
3317
+ fileErrorNotified = false;
3317
3318
  constructor(client, logPath, enabled, minLevel = "info") {
3318
3319
  this.client = client;
3319
3320
  this.logPath = logPath;
@@ -3321,18 +3322,19 @@ class Logger {
3321
3322
  this.minLevel = minLevel;
3322
3323
  }
3323
3324
  log(level, event, fields = {}) {
3325
+ const sanitizedFields = sanitizeFields(fields);
3324
3326
  const entry = {
3325
3327
  ts: new Date().toISOString(),
3326
3328
  level,
3327
3329
  event,
3328
- ...fields
3330
+ ...sanitizedFields
3329
3331
  };
3330
3332
  const shouldWrite = this.enabled && (this.minLevel === "debug" || level !== "debug");
3331
3333
  if (shouldWrite) {
3332
3334
  this.writeToFile(entry);
3333
3335
  }
3334
3336
  if (level !== "debug") {
3335
- const message = `[model-fallback] ${event}${Object.keys(fields).length ? " " + JSON.stringify(fields) : ""}`;
3337
+ const message = `[model-fallback] ${event}${Object.keys(sanitizedFields).length ? " " + JSON.stringify(sanitizedFields) : ""}`;
3336
3338
  this.client.app.log({
3337
3339
  body: { service: "model-fallback", level, message }
3338
3340
  }).catch(() => {});
@@ -3356,13 +3358,65 @@ class Logger {
3356
3358
  mkdirSync(dirname(this.logPath), { recursive: true, mode: 448 });
3357
3359
  this.dirCreated = true;
3358
3360
  }
3359
- if (!existsSync3(this.logPath)) {
3360
- writeFileSync(this.logPath, "", { mode: 384 });
3361
- }
3361
+ try {
3362
+ writeFileSync(this.logPath, "", { mode: 384, flag: "ax" });
3363
+ } catch {}
3362
3364
  appendFileSync(this.logPath, JSON.stringify(entry) + `
3363
3365
  `, "utf-8");
3364
- } catch {}
3366
+ } catch (err) {
3367
+ if (!this.fileErrorNotified) {
3368
+ this.fileErrorNotified = true;
3369
+ const message = `[model-fallback] logging.file.write.failed ${JSON.stringify({
3370
+ logPath: this.logPath,
3371
+ error: summarizeError(err)
3372
+ })}`;
3373
+ this.client.app.log({
3374
+ body: { service: "model-fallback", level: "warn", message }
3375
+ }).catch(() => {});
3376
+ }
3377
+ }
3378
+ }
3379
+ }
3380
+ function sanitizeFields(fields) {
3381
+ const out = {};
3382
+ for (const [key, value] of Object.entries(fields)) {
3383
+ out[key] = sanitizeValue(key, value);
3384
+ }
3385
+ return out;
3386
+ }
3387
+ function sanitizeValue(key, value) {
3388
+ if (value === null || value === undefined)
3389
+ return value;
3390
+ if (isSensitiveKey(key)) {
3391
+ if (typeof value === "string") {
3392
+ return { redacted: true, length: value.length };
3393
+ }
3394
+ if (value instanceof Error) {
3395
+ return { redacted: true, type: value.name, code: getErrorCode(value) };
3396
+ }
3397
+ return { redacted: true, type: typeof value };
3398
+ }
3399
+ if (value instanceof Error) {
3400
+ return { name: value.name, message: value.message };
3401
+ }
3402
+ return value;
3403
+ }
3404
+ function isSensitiveKey(key) {
3405
+ return /(?:^|_)(message|prompt|content|parts|error|err|stack|body)(?:$|_)/i.test(key);
3406
+ }
3407
+ function getErrorCode(err) {
3408
+ const code = err.code;
3409
+ return typeof code === "string" ? code : undefined;
3410
+ }
3411
+ function summarizeError(err) {
3412
+ if (err && typeof err === "object") {
3413
+ const e = err;
3414
+ return {
3415
+ type: typeof e.name === "string" ? e.name : "Error",
3416
+ code: typeof e.code === "string" ? e.code : undefined
3417
+ };
3365
3418
  }
3419
+ return { type: typeof err };
3366
3420
  }
3367
3421
 
3368
3422
  // src/resolution/agent-resolver.ts
@@ -3505,7 +3559,7 @@ async function attemptFallback(sessionId, reason, client, store, config, logger,
3505
3559
  const result = await client.session.messages({ path: { id: sessionId } });
3506
3560
  messageEntries = Array.isArray(result.data) ? result.data : [];
3507
3561
  } catch (err) {
3508
- logger.error("replay.messages.failed", { sessionId, err: String(err) });
3562
+ logger.error("replay.messages.failed", { sessionId, err });
3509
3563
  return { success: false, error: "messages fetch failed" };
3510
3564
  }
3511
3565
  let lastUserEntry = null;
@@ -3575,7 +3629,7 @@ async function attemptFallback(sessionId, reason, client, store, config, logger,
3575
3629
  return { success: false, error: "all fallback models exhausted" };
3576
3630
  }
3577
3631
  const currentModel = sessionState.currentModel;
3578
- if (currentModel) {
3632
+ if (currentModel && shouldMarkRateLimited(reason)) {
3579
3633
  store.health.markRateLimited(currentModel, config.defaults.cooldownMs, config.defaults.retryOriginalAfterMs);
3580
3634
  }
3581
3635
  sessionState.lastFallbackAt = Date.now();
@@ -3583,7 +3637,7 @@ async function attemptFallback(sessionId, reason, client, store, config, logger,
3583
3637
  await client.session.abort({ path: { id: sessionId } });
3584
3638
  logger.debug("replay.abort.ok", { sessionId });
3585
3639
  } catch (err) {
3586
- logger.error("replay.abort.failed", { sessionId, err: String(err) });
3640
+ logger.error("replay.abort.failed", { sessionId, err });
3587
3641
  return { success: false, error: "abort failed" };
3588
3642
  }
3589
3643
  try {
@@ -3596,7 +3650,7 @@ async function attemptFallback(sessionId, reason, client, store, config, logger,
3596
3650
  messageID: lastUserEntry.id
3597
3651
  });
3598
3652
  } catch (err) {
3599
- logger.error("replay.revert.failed", { sessionId, err: String(err) });
3653
+ logger.error("replay.revert.failed", { sessionId, err });
3600
3654
  return { success: false, error: "revert failed" };
3601
3655
  }
3602
3656
  const promptParts = convertPartsForPrompt(lastUserEntry.parts);
@@ -3619,7 +3673,7 @@ async function attemptFallback(sessionId, reason, client, store, config, logger,
3619
3673
  logger.error("replay.prompt.failed", {
3620
3674
  sessionId,
3621
3675
  fallbackModel,
3622
- err: String(err)
3676
+ err
3623
3677
  });
3624
3678
  return { success: false, error: "prompt failed" };
3625
3679
  }
@@ -3634,11 +3688,14 @@ async function attemptFallback(sessionId, reason, client, store, config, logger,
3634
3688
  reason,
3635
3689
  depth: newDepth
3636
3690
  });
3637
- return { success: true, fallbackModel };
3691
+ return { success: true, fallbackModel, fromModel: currentModel };
3638
3692
  } finally {
3639
3693
  store.sessions.releaseLock(sessionId);
3640
3694
  }
3641
3695
  }
3696
+ function shouldMarkRateLimited(reason) {
3697
+ return reason === "rate_limit" || reason === "quota_exceeded";
3698
+ }
3642
3699
  function sanitizeParts(parts) {
3643
3700
  if (!Array.isArray(parts))
3644
3701
  return [];
@@ -3652,7 +3709,7 @@ class ModelHealthStore {
3652
3709
  onTransition;
3653
3710
  constructor(opts) {
3654
3711
  this.onTransition = opts?.onTransition;
3655
- this.timer = setInterval(() => this.tick(), 30000);
3712
+ this.timer = setInterval(() => this.tick(), 30000).unref();
3656
3713
  }
3657
3714
  get(modelKey2) {
3658
3715
  return this.store.get(modelKey2) ?? this.newHealth(modelKey2);
@@ -3782,6 +3839,7 @@ class SessionStateStore {
3782
3839
  agentName
3783
3840
  };
3784
3841
  state.currentModel = toModel;
3842
+ state.fallbackDepth++;
3785
3843
  state.recoveryNotifiedForModel = null;
3786
3844
  state.fallbackHistory.push(event);
3787
3845
  if (agentName)
@@ -4026,22 +4084,25 @@ function getLastUserModelAndAgent(data) {
4026
4084
  }
4027
4085
 
4028
4086
  // src/plugin.ts
4029
- var createPlugin = async ({ client, directory }) => {
4030
- const { config, path: configPath, warnings, migrated } = loadConfig(directory);
4031
- const logger = new Logger(client, config.logPath, config.logging, config.logLevel);
4032
- const cmdPath = join4(homedir5(), ".config/opencode/commands/fallback-status.md");
4087
+ function resolveFallbackStatusCommandPath() {
4088
+ return join4(homedir5(), ".config", "opencode", "commands", "fallback-status.md");
4089
+ }
4090
+ function ensureFallbackStatusCommand(logger, cmdPath) {
4033
4091
  try {
4034
- if (!existsSync4(cmdPath)) {
4035
- mkdirSync2(dirname2(cmdPath), { recursive: true });
4036
- writeFileSync2(cmdPath, `Call the fallback-status tool and display the full output.
4037
- `);
4038
- }
4039
- } catch (err) {
4040
- logger.warn("fallback-status.command.write.failed", {
4041
- cmdPath,
4042
- err: String(err)
4092
+ mkdirSync2(dirname2(cmdPath), { recursive: true, mode: 448 });
4093
+ writeFileSync2(cmdPath, `Call the fallback-status tool and display the full output.
4094
+ `, {
4095
+ flag: "wx"
4043
4096
  });
4097
+ } catch (err) {
4098
+ if (err.code !== "EEXIST") {
4099
+ logger.warn("fallback-status.command.write.failed", { cmdPath, err });
4100
+ }
4044
4101
  }
4102
+ }
4103
+ var createPlugin = async ({ client, directory }) => {
4104
+ const { config, path: configPath, warnings, migrated } = loadConfig(directory);
4105
+ const logger = new Logger(client, config.logPath, config.logging, config.logLevel);
4045
4106
  logger.info("plugin.init", {
4046
4107
  configPath,
4047
4108
  enabled: config.enabled,
@@ -4049,17 +4110,19 @@ var createPlugin = async ({ client, directory }) => {
4049
4110
  agentCount: Object.keys(config.agents).length
4050
4111
  });
4051
4112
  for (const w of warnings) {
4052
- logger.warn("config.warning", { message: w });
4113
+ logger.warn("config.warning", { warning: w });
4053
4114
  }
4054
4115
  if (migrated) {
4055
4116
  logger.info("config.migrated", {
4056
- message: "Auto-migrated from old rate-limit-fallback.json format"
4117
+ note: "Auto-migrated from old rate-limit-fallback.json format"
4057
4118
  });
4058
4119
  }
4059
4120
  if (!config.enabled) {
4060
4121
  logger.info("plugin.disabled");
4061
4122
  return {};
4062
4123
  }
4124
+ const cmdPath = resolveFallbackStatusCommandPath();
4125
+ ensureFallbackStatusCommand(logger, cmdPath);
4063
4126
  const store = new FallbackStore(config, logger);
4064
4127
  const hooks = {
4065
4128
  async event({ event }) {
@@ -4124,8 +4187,7 @@ async function handleEvent(event, client, store, config, logger, directory) {
4124
4187
  if (config.defaults.fallbackOn.includes(category)) {
4125
4188
  const result = await attemptFallback(sessionID, category, client, store, config, logger, directory);
4126
4189
  if (result.success && result.fallbackModel) {
4127
- const state = store.sessions.get(sessionID);
4128
- await notifyFallback(client, state.originalModel, result.fallbackModel, category);
4190
+ await notifyFallback(client, result.fromModel ?? null, result.fallbackModel, category);
4129
4191
  }
4130
4192
  }
4131
4193
  }
@@ -4145,12 +4207,16 @@ async function handleEvent(event, client, store, config, logger, directory) {
4145
4207
  }
4146
4208
  async function handleRetry(sessionId, message, client, store, config, logger, directory) {
4147
4209
  if (!matchesAnyPattern(message, config.patterns)) {
4148
- logger.debug("retry.nomatch", { sessionId, message });
4210
+ logger.debug("retry.nomatch", { sessionId, messageLength: message.length });
4149
4211
  return;
4150
4212
  }
4151
4213
  const category = classifyError(message);
4152
4214
  if (!config.defaults.fallbackOn.includes(category)) {
4153
- logger.debug("retry.ignored", { sessionId, message, category });
4215
+ logger.debug("retry.ignored", {
4216
+ sessionId,
4217
+ category,
4218
+ messageLength: message.length
4219
+ });
4154
4220
  return;
4155
4221
  }
4156
4222
  const sessionState = store.sessions.get(sessionId);
@@ -4178,15 +4244,14 @@ async function handleRetry(sessionId, message, client, store, config, logger, di
4178
4244
  }
4179
4245
  logger.info("retry.detected", {
4180
4246
  sessionId,
4181
- message,
4247
+ messageLength: message.length,
4182
4248
  category,
4183
4249
  agentName: sessionState.agentName,
4184
4250
  agentFile: sessionState.agentFile
4185
4251
  });
4186
4252
  const result = await attemptFallback(sessionId, category, client, store, config, logger, directory);
4187
4253
  if (result.success && result.fallbackModel) {
4188
- const state = store.sessions.get(sessionId);
4189
- await notifyFallback(client, state.originalModel, result.fallbackModel, category);
4254
+ await notifyFallback(client, result.fromModel ?? null, result.fallbackModel, category);
4190
4255
  }
4191
4256
  }
4192
4257
  async function handleIdle(sessionId, client, store, _config, logger) {
@@ -4206,7 +4271,8 @@ async function handleIdle(sessionId, client, store, _config, logger) {
4206
4271
  return;
4207
4272
  logger.info("recovery.available", {
4208
4273
  sessionId,
4209
- originalModel: state.originalModel
4274
+ originalModel: state.originalModel,
4275
+ currentModel: state.currentModel
4210
4276
  });
4211
4277
  await notifyRecovery(client, state.originalModel);
4212
4278
  state.recoveryNotifiedForModel = state.originalModel;
@@ -13,6 +13,7 @@ export declare class Logger {
13
13
  private enabled;
14
14
  private minLevel;
15
15
  private dirCreated;
16
+ private fileErrorNotified;
16
17
  constructor(client: Client, logPath: string, enabled: boolean, minLevel?: "debug" | "info");
17
18
  log(level: LogLevel, event: string, fields?: Record<string, unknown>): void;
18
19
  info(event: string, fields?: Record<string, unknown>): void;
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/logging/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAIvD,KAAK,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;AAEpC,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,QAAQ,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,UAAU,CAAS;gBAGzB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,OAAO,EAChB,QAAQ,GAAE,OAAO,GAAG,MAAe;IAQrC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,IAAI;IA0B/E,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI3D,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI3D,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI5D,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI5D,OAAO,CAAC,WAAW;CAepB"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/logging/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAIvD,KAAK,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;AAEpC,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,QAAQ,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,iBAAiB,CAAS;gBAGhC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,OAAO,EAChB,QAAQ,GAAE,OAAO,GAAG,MAAe;IAQrC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,IAAI;IA2B/E,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI3D,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI3D,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI5D,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI5D,OAAO,CAAC,WAAW;CA8BpB"}
@@ -3,6 +3,7 @@ import type { Event } from "@opencode-ai/sdk";
3
3
  import { loadConfig } from "./config/loader.js";
4
4
  import { Logger } from "./logging/logger.js";
5
5
  import { FallbackStore } from "./state/store.js";
6
+ export declare function ensureFallbackStatusCommand(logger: Logger, cmdPath: string): void;
6
7
  export declare const createPlugin: Plugin;
7
8
  export declare function handleEvent(event: Event, client: Parameters<Plugin>[0]["client"], store: FallbackStore, config: ReturnType<typeof loadConfig>["config"], logger: Logger, directory: string): Promise<void>;
8
9
  export declare function handleIdle(sessionId: string, client: Parameters<Plugin>[0]["client"], store: FallbackStore, _config: ReturnType<typeof loadConfig>["config"], logger: Logger): Promise<void>;
@@ -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;AAIjD,eAAO,MAAM,YAAY,EAAE,MAqG1B,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,CAyDf;AAqFD,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"}
@@ -6,6 +6,7 @@ type Client = PluginInput["client"];
6
6
  export interface ReplayResult {
7
7
  success: boolean;
8
8
  fallbackModel?: ModelKey;
9
+ fromModel?: ModelKey | null;
9
10
  error?: string;
10
11
  }
11
12
  export declare function attemptFallback(sessionId: string, reason: ErrorCategory, client: Client, store: FallbackStore, config: PluginConfig, logger: Logger, directory: string): Promise<ReplayResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../../src/replay/orchestrator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAGnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGzE,KAAK,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;AAEpC,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,QAAQ,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,YAAY,CAAC,CAwOvB"}
1
+ {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../../src/replay/orchestrator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAGnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGzE,KAAK,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;AAEpC,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,QAAQ,CAAC;IACzB,SAAS,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,YAAY,CAAC,CAwOvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"model-health.d.ts","sourceRoot":"","sources":["../../../src/state/model-health.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEtE,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,YAAY,CAAC,CAAmE;gBAE5E,IAAI,CAAC,EAAE;QACjB,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,WAAW,KAAK,IAAI,CAAC;KACjF;IAMD,GAAG,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW;IAIpC,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,GAAG,IAAI;IAc3F,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO;IAKrC,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM;IAQvC,MAAM,IAAI,WAAW,EAAE;IAIvB,OAAO,IAAI,IAAI;IAOf,OAAO,CAAC,IAAI;IA4BZ,OAAO,CAAC,SAAS;CAUlB"}
1
+ {"version":3,"file":"model-health.d.ts","sourceRoot":"","sources":["../../../src/state/model-health.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEtE,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,YAAY,CAAC,CAAmE;gBAE5E,IAAI,CAAC,EAAE;QACjB,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,WAAW,KAAK,IAAI,CAAC;KACjF;IAOD,GAAG,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW;IAIpC,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,GAAG,IAAI;IAc3F,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO;IAKrC,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM;IAQvC,MAAM,IAAI,WAAW,EAAE;IAIvB,OAAO,IAAI,IAAI;IAOf,OAAO,CAAC,IAAI;IA4BZ,OAAO,CAAC,SAAS;CAUlB"}
@@ -1 +1 @@
1
- {"version":3,"file":"session-state.d.ts","sourceRoot":"","sources":["../../../src/state/session-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiB,QAAQ,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEhG,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,KAAK,CAA2C;IAExD,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,oBAAoB;IAS5C,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IASvC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAKpC,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,SAAQ,GAAG,OAAO;IAM7D,cAAc,CACZ,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,QAAQ,EACnB,OAAO,EAAE,QAAQ,EACjB,MAAM,EAAE,aAAa,EACrB,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,IAAI;IAmBP,wBAAwB,CACtB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,QAAQ,EACnB,OAAO,EAAE,QAAQ,EACjB,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,IAAI;IAkBP,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI;IAQ1D,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAKxD,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAKxD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IASrC,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI/B,MAAM,IAAI,oBAAoB,EAAE;IAIhC,OAAO,CAAC,QAAQ;CAcjB"}
1
+ {"version":3,"file":"session-state.d.ts","sourceRoot":"","sources":["../../../src/state/session-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiB,QAAQ,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEhG,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,KAAK,CAA2C;IAExD,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,oBAAoB;IAS5C,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IASvC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAKpC,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,SAAQ,GAAG,OAAO;IAM7D,cAAc,CACZ,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,QAAQ,EACnB,OAAO,EAAE,QAAQ,EACjB,MAAM,EAAE,aAAa,EACrB,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,IAAI;IAmBP,wBAAwB,CACtB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,QAAQ,EACnB,OAAO,EAAE,QAAQ,EACjB,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,IAAI;IAmBP,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI;IAQ1D,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAKxD,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAKxD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IASrC,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI/B,MAAM,IAAI,oBAAoB,EAAE;IAIhC,OAAO,CAAC,QAAQ;CAcjB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smart-coders-hq/opencode-model-fallback",
3
- "version": "1.2.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.2.0",
6
+ "version": "1.3.1",
7
7
  "entry": "dist/index.js",
8
8
  "links": [
9
9
  {