@azumag/opencode-rate-limit-fallback 1.40.0 → 1.49.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 CHANGED
@@ -51,10 +51,15 @@ Restart OpenCode to load the plugin.
51
51
 
52
52
  Create a configuration file at one of these locations:
53
53
 
54
- - `~/.opencode/rate-limit-fallback.json` (recommended)
55
- - `~/.config/opencode/rate-limit-fallback.json`
56
- - `<project>/.opencode/rate-limit-fallback.json`
57
- - `<project>/rate-limit-fallback.json`
54
+ **Config file search order (highest to lowest priority):**
55
+ 1. `<worktree>/.opencode/rate-limit-fallback.json`
56
+ 2. `<worktree>/rate-limit-fallback.json`
57
+ 3. `<project>/.opencode/rate-limit-fallback.json`
58
+ 4. `<project>/rate-limit-fallback.json`
59
+ 5. `~/.opencode/rate-limit-fallback.json` (recommended for most users)
60
+ 6. `~/.config/opencode/rate-limit-fallback.json`
61
+
62
+ > **Note**: Project-local and worktree configs (1-4) take precedence over global configs (5-6).
58
63
 
59
64
  ### Example Configuration
60
65
 
@@ -99,16 +104,39 @@ Create a configuration file at one of these locations:
99
104
 
100
105
  ### Configuration Options
101
106
 
102
- | Option | Type | Default | Description |
103
- |--------|------|---------|-------------|
104
- | `enabled` | boolean | `true` | Enable/disable the plugin |
105
- | `cooldownMs` | number | `60000` | Cooldown period (ms) before retrying a rate-limited model |
106
- | `fallbackMode` | string | `"cycle"` | Behavior when all models are exhausted (see below) |
107
- | `fallbackModels` | array | See below | List of fallback models in priority order |
108
- | `maxSubagentDepth` | number | `10` | Maximum nesting depth for subagent hierarchies |
109
- | `enableSubagentFallback` | boolean | `true` | Enable/disable fallback for subagent sessions |
110
- | `retryPolicy` | object | See below | Retry policy configuration (see below) |
111
- | `circuitBreaker` | object | See below | Circuit breaker configuration (see below) |
107
+ | Option | Type | Default | Description |
108
+ |--------|------|---------|-------------|
109
+ | `enabled` | boolean | `true` | Enable/disable the plugin |
110
+ | `cooldownMs` | number | `60000` | Cooldown period (ms) before retrying a rate-limited model |
111
+ | `fallbackMode` | string | `"cycle"` | Behavior when all models are exhausted (see below) |
112
+ | `fallbackModels` | array | See below | List of fallback models in priority order |
113
+ | `maxSubagentDepth` | number | `10` | Maximum nesting depth for subagent hierarchies |
114
+ | `enableSubagentFallback` | boolean | `true` | Enable/disable fallback for subagent sessions |
115
+ | `retryPolicy` | object | See below | Retry policy configuration (see below) |
116
+ | `circuitBreaker` | object | See below | Circuit breaker configuration (see below) |
117
+
118
+ ### Git Worktree Support
119
+
120
+ When using git worktrees, the plugin searches for config files in the worktree directory first, before the project directory. This allows you to have different fallback configurations for different worktrees.
121
+
122
+ **Example structure:**
123
+ ```
124
+ my-repo/
125
+ .git/
126
+ .opencode/rate-limit-fallback.json (project-level config)
127
+ my-worktree/ (worktree)
128
+ .opencode/rate-limit-fallback.json (worktree-specific, higher priority)
129
+ ```
130
+
131
+ **Config file search order with worktrees (highest to lowest priority):**
132
+ 1. `<worktree>/.opencode/rate-limit-fallback.json`
133
+ 2. `<worktree>/rate-limit-fallback.json`
134
+ 3. `<project>/.opencode/rate-limit-fallback.json`
135
+ 4. `<project>/rate-limit-fallback.json`
136
+ 5. `~/.opencode/rate-limit-fallback.json`
137
+ 6. `~/.config/opencode/rate-limit-fallback.json`
138
+
139
+ > **Note**: If you're using git worktrees and want different configurations per worktree, create config files in the worktree directories (locations 1-2). Otherwise, a single project-level or global config is sufficient.
112
140
 
113
141
  ### Fallback Modes
114
142
 
@@ -221,13 +249,119 @@ The circuit breaker maintains three states for each model:
221
249
  | Production | 5 | 60000 | 1 |
222
250
  | High Availability | 10 | 30000 | 2 |
223
251
 
224
- ### Default Fallback Models
252
+ ### ⚠️ Important: Configuration Required
253
+
254
+ **As of v1.43.0, this plugin requires explicit configuration.**
255
+
256
+ The default fallback models array is empty, meaning no fallback behavior will occur until you create a configuration file.
257
+
258
+ **You must create a config file at one of these locations:**
259
+
260
+ **Config file search order (highest to lowest priority):**
261
+ 1. `<worktree>/.opencode/rate-limit-fallback.json`
262
+ 2. `<worktree>/rate-limit-fallback.json`
263
+ 3. `<project>/.opencode/rate-limit-fallback.json`
264
+ 4. `<project>/rate-limit-fallback.json`
265
+ 5. `~/.opencode/rate-limit-fallback.json` (recommended for most users)
266
+ 6. `~/.config/opencode/rate-limit-fallback.json`
267
+
268
+ > **Note**: Project-local and worktree configs (1-4) take precedence over global configs (5-6).
269
+
270
+ **If no config file is found, the plugin will:**
271
+ - Log a warning message
272
+ - Not perform any fallback operations
273
+ - Continue functioning normally with rate-limited models
274
+
275
+ **Minimum working configuration:**
276
+ ```json
277
+ {
278
+ "fallbackModels": [
279
+ { "providerID": "anthropic", "modelID": "claude-3-5-sonnet-20250514" }
280
+ ]
281
+ }
282
+ ```
283
+
284
+ ## Migrating from v1.42.x or earlier
285
+
286
+ ### Breaking Change: Empty Default Models
287
+
288
+ **What changed?**
289
+ - v1.43.0 removed the default fallback models
290
+ - You must now explicitly configure your fallback models
291
+ - The plugin will not work without a configuration file
292
+
293
+ **Why was this changed?**
294
+ - To prevent unintended model usage (e.g., Gemini when not wanted)
295
+ - To make configuration errors obvious immediately
296
+ - To give users explicit control over which models to use
297
+
298
+ ### How to Migrate
299
+
300
+ 1. **Create a config file** at one of the locations listed above
301
+ 2. **Add your desired fallback models** to the `fallbackModels` array
302
+ 3. **Restart OpenCode** to load the new configuration
303
+
304
+ ### Example Migration
305
+
306
+ **Before v1.43.0** (no config needed, used defaults):
307
+ ```
308
+ Plugin automatically used Claude and Gemini models as fallbacks
309
+ ```
310
+
311
+ **After v1.43.0** (must create config):
312
+ ```json
313
+ {
314
+ "fallbackModels": [
315
+ { "providerID": "anthropic", "modelID": "claude-3-5-sonnet-20250514" },
316
+ { "providerID": "google", "modelID": "gemini-2.5-pro" }
317
+ ],
318
+ "enabled": true
319
+ }
320
+ ```
321
+
322
+ ## Troubleshooting
323
+
324
+ ### "No fallback models configured" warning
325
+
326
+ **Problem**: You see a warning about no fallback models configured.
327
+
328
+ **Solution**: Create a config file with your desired fallback models. See the Configuration section above for details.
329
+
330
+ ### Plugin isn't falling back when rate limited
331
+
332
+ **Problem**: Rate limits occur but no fallback happens.
333
+
334
+ **Solutions**:
335
+ 1. Check that a config file exists and is valid
336
+ 2. Verify that `fallbackModels` is not empty in your config
337
+ 3. Check that `enabled: true` is set in your config
338
+ 4. Review logs for error messages
339
+
340
+ ### "Config file not found" warning
341
+
342
+ **Problem**: You see warnings about config file not being found.
343
+
344
+ **Solution**: Create a config file at one of the recommended locations:
345
+
346
+ **Config file search order (highest to lowest priority):**
347
+ 1. `<worktree>/.opencode/rate-limit-fallback.json`
348
+ 2. `<worktree>/rate-limit-fallback.json`
349
+ 3. `<project>/.opencode/rate-limit-fallback.json`
350
+ 4. `<project>/rate-limit-fallback.json`
351
+ 5. `~/.opencode/rate-limit-fallback.json` (recommended for most users)
352
+ 6. `~/.config/opencode/rate-limit-fallback.json`
353
+
354
+ > **Note**: Project-local and worktree configs (1-4) take precedence over global configs (5-6).
355
+
356
+ ### All models exhausted quickly
225
357
 
226
- If no configuration is provided, the following models are used:
358
+ **Problem**: Fallback models are exhausted in a short time.
227
359
 
228
- 1. `anthropic/claude-3-5-sonnet-20250514`
229
- 2. `google/gemini-2.5-pro`
230
- 3. `google/gemini-2.5-flash`
360
+ **Solutions**:
361
+ 1. Add more fallback models to your config
362
+ 2. Increase `cooldownMs` to allow models to recover
363
+ 3. Consider using `fallbackMode: "cycle"` to reset automatically
364
+ 4. Check your API rate limits
231
365
 
232
366
  ## How It Works
233
367
 
package/dist/index.js CHANGED
@@ -13,6 +13,36 @@ import { ConfigValidator } from "./src/config/Validator.js";
13
13
  import { ErrorPatternRegistry } from "./src/errors/PatternRegistry.js";
14
14
  import { HealthTracker } from "./src/health/HealthTracker.js";
15
15
  import { DiagnosticReporter } from "./src/diagnostics/Reporter.js";
16
+ import { ConfigWatcher } from "./src/config/Watcher.js";
17
+ import { ConfigReloader } from "./src/main/ConfigReloader.js";
18
+ // ============================================================================
19
+ // Helper Functions
20
+ // ============================================================================
21
+ /**
22
+ * Get the difference between two objects (returns keys with different values)
23
+ */
24
+ function getObjectDiff(obj1, obj2) {
25
+ const diffs = [];
26
+ const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
27
+ for (const key of allKeys) {
28
+ const val1 = obj1[key];
29
+ const val2 = obj2[key];
30
+ if (typeof val1 !== typeof val2) {
31
+ diffs.push(`${key}: ${val1} → ${val2}`);
32
+ continue;
33
+ }
34
+ if (val1 === undefined && val2 !== undefined) {
35
+ diffs.push(`${key}: undefined → ${JSON.stringify(val2)}`);
36
+ }
37
+ else if (val1 !== undefined && val2 === undefined) {
38
+ diffs.push(`${key}: ${JSON.stringify(val1)} → undefined`);
39
+ }
40
+ else if (val1 !== val2) {
41
+ diffs.push(`${key}: ${JSON.stringify(val1)} → ${JSON.stringify(val2)}`);
42
+ }
43
+ }
44
+ return diffs;
45
+ }
16
46
  // ============================================================================
17
47
  // Event Type Guards
18
48
  // ============================================================================
@@ -57,22 +87,58 @@ function isSubagentSessionCreatedEvent(event) {
57
87
  // Main Plugin Export
58
88
  // ============================================================================
59
89
  export const RateLimitFallback = async ({ client, directory, worktree }) => {
60
- const { config, source: configSource } = loadConfig(directory, worktree);
61
- // Detect headless mode (no TUI)
90
+ // Detect headless mode (no TUI) before loading config for logging
62
91
  const isHeadless = !client.tui;
92
+ // We need a temporary logger to log config loading process
93
+ // Use a minimal config initially
94
+ const tempLogConfig = {
95
+ level: isHeadless ? 'info' : 'warn',
96
+ format: 'simple',
97
+ enableTimestamp: true,
98
+ };
99
+ const tempLogger = createLogger(tempLogConfig, "RateLimitFallback");
100
+ // Log headless mode detection
101
+ if (isHeadless) {
102
+ tempLogger.info("Running in headless mode (no TUI detected)");
103
+ }
104
+ const configLoadResult = loadConfig(directory, worktree, tempLogger);
105
+ const { config, source: configSource } = configLoadResult;
63
106
  // Auto-adjust log level for headless mode to ensure visibility
64
107
  const logConfig = {
65
108
  ...config.log,
66
109
  level: isHeadless ? 'info' : (config.log?.level ?? 'warn'),
67
110
  };
68
- // Create logger instance
111
+ // Create final logger instance with loaded config
69
112
  const logger = createLogger(logConfig, "RateLimitFallback");
70
113
  if (configSource) {
71
- logger.info(`Config loaded from ${configSource}`);
114
+ logger.info(`Config loaded from: ${configSource}`);
72
115
  }
73
116
  else {
74
117
  logger.info("No config file found, using defaults");
75
118
  }
119
+ // Log verbose mode status
120
+ if (config.verbose) {
121
+ logger.info("Verbose mode enabled - showing diagnostic information");
122
+ }
123
+ // Log config merge diff in verbose mode
124
+ if (config.verbose && configSource) {
125
+ if (configLoadResult.rawUserConfig &&
126
+ typeof configLoadResult.rawUserConfig === 'object' &&
127
+ configLoadResult.rawUserConfig !== null &&
128
+ !Array.isArray(configLoadResult.rawUserConfig) &&
129
+ Object.keys(configLoadResult.rawUserConfig).length > 0) {
130
+ logger.info("Configuration merge details:");
131
+ const diffs = getObjectDiff(configLoadResult.rawUserConfig, config);
132
+ if (diffs.length > 0) {
133
+ for (const diff of diffs) {
134
+ logger.info(` ${diff}`);
135
+ }
136
+ }
137
+ else {
138
+ logger.info(" No changes from defaults");
139
+ }
140
+ }
141
+ }
76
142
  // Initialize configuration validator
77
143
  const validator = new ConfigValidator(logger);
78
144
  const validation = configSource
@@ -108,13 +174,32 @@ export const RateLimitFallback = async ({ client, directory, worktree }) => {
108
174
  errorPatternRegistry, logger);
109
175
  // Log startup diagnostics if verbose mode
110
176
  if (config.verbose) {
111
- logger.debug("Verbose mode enabled - showing diagnostic information");
112
177
  diagnostics.logCurrentConfig();
113
178
  }
114
179
  // Initialize components
115
180
  const subagentTracker = new SubagentTracker(config);
116
181
  const metricsManager = new MetricsManager(config.metrics ?? { enabled: false, output: { console: true, format: "pretty" }, resetInterval: "daily" }, logger);
117
182
  const fallbackHandler = new FallbackHandler(config, client, logger, metricsManager, subagentTracker, healthTracker);
183
+ // Initialize config reloader if hot reload is enabled
184
+ let configWatcher;
185
+ if (config.configReload?.enabled) {
186
+ const componentRefs = {
187
+ fallbackHandler,
188
+ metricsManager,
189
+ };
190
+ const configReloader = new ConfigReloader(config, configSource, logger, validator, client, componentRefs, directory, worktree, config.configReload?.notifyOnReload ?? true);
191
+ configWatcher = new ConfigWatcher(configSource || '', logger, async () => { await configReloader.reloadConfig(); }, {
192
+ enabled: config.configReload.enabled,
193
+ watchFile: config.configReload.watchFile,
194
+ debounceMs: config.configReload.debounceMs,
195
+ });
196
+ configWatcher.start();
197
+ logger.info('Config hot reload enabled', {
198
+ configPath: configSource || 'none',
199
+ debounceMs: config.configReload.debounceMs,
200
+ notifyOnReload: config.configReload.notifyOnReload,
201
+ });
202
+ }
118
203
  // Cleanup stale entries periodically
119
204
  const cleanupInterval = setInterval(() => {
120
205
  subagentTracker.cleanupStaleEntries();
@@ -181,6 +266,9 @@ export const RateLimitFallback = async ({ client, directory, worktree }) => {
181
266
  if (healthTracker) {
182
267
  healthTracker.destroy();
183
268
  }
269
+ if (configWatcher) {
270
+ configWatcher.stop();
271
+ }
184
272
  },
185
273
  };
186
274
  };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Configuration file watcher for hot reload functionality
3
+ */
4
+ import type { Logger } from '../../logger.js';
5
+ /**
6
+ * Options for config watching
7
+ */
8
+ export interface ConfigWatchOptions {
9
+ enabled: boolean;
10
+ watchFile: boolean;
11
+ debounceMs: number;
12
+ }
13
+ /**
14
+ * ConfigWatcher class - watches config file for changes
15
+ */
16
+ export declare class ConfigWatcher {
17
+ private watcher?;
18
+ private debounceTimer?;
19
+ private configPath;
20
+ private logger;
21
+ private onReload;
22
+ private options;
23
+ private isReloading;
24
+ constructor(configPath: string, logger: Logger, onReload: () => Promise<void>, options: ConfigWatchOptions);
25
+ /**
26
+ * Start watching the config file
27
+ */
28
+ start(): void;
29
+ /**
30
+ * Stop watching the config file
31
+ */
32
+ stop(): void;
33
+ /**
34
+ * Handle config file change event
35
+ */
36
+ private handleConfigChange;
37
+ /**
38
+ * Check if watcher is currently active
39
+ */
40
+ isActive(): boolean;
41
+ /**
42
+ * Check if a reload is currently in progress
43
+ */
44
+ isReloadingInProgress(): boolean;
45
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Configuration file watcher for hot reload functionality
3
+ */
4
+ import { watch } from 'fs';
5
+ /**
6
+ * ConfigWatcher class - watches config file for changes
7
+ */
8
+ export class ConfigWatcher {
9
+ watcher;
10
+ debounceTimer;
11
+ configPath;
12
+ logger;
13
+ onReload;
14
+ options;
15
+ isReloading;
16
+ constructor(configPath, logger, onReload, options) {
17
+ this.configPath = configPath;
18
+ this.logger = logger;
19
+ this.onReload = onReload;
20
+ this.options = options;
21
+ this.isReloading = false;
22
+ }
23
+ /**
24
+ * Start watching the config file
25
+ */
26
+ start() {
27
+ if (!this.options.enabled || !this.options.watchFile) {
28
+ this.logger.info('Config hot reload is disabled');
29
+ return;
30
+ }
31
+ if (!this.configPath) {
32
+ this.logger.warn('No config file path provided, cannot watch for changes');
33
+ return;
34
+ }
35
+ try {
36
+ this.watcher = watch(this.configPath, (eventType, filename) => {
37
+ this.logger.debug(`Config file event: ${eventType} ${filename || ''}`);
38
+ // Debounce the reload
39
+ if (this.debounceTimer) {
40
+ clearTimeout(this.debounceTimer);
41
+ }
42
+ this.debounceTimer = setTimeout(() => {
43
+ this.handleConfigChange();
44
+ }, this.options.debounceMs);
45
+ });
46
+ this.logger.info(`Watching config file for changes: ${this.configPath}`);
47
+ }
48
+ catch (error) {
49
+ const errorMessage = error instanceof Error ? error.message : String(error);
50
+ this.logger.warn(`Failed to watch config file: ${errorMessage}`);
51
+ this.logger.warn('Hot reload will not be available');
52
+ }
53
+ }
54
+ /**
55
+ * Stop watching the config file
56
+ */
57
+ stop() {
58
+ if (this.debounceTimer) {
59
+ clearTimeout(this.debounceTimer);
60
+ this.debounceTimer = undefined;
61
+ }
62
+ if (this.watcher) {
63
+ try {
64
+ this.watcher.close();
65
+ this.watcher = undefined;
66
+ this.logger.info('Stopped watching config file');
67
+ }
68
+ catch (error) {
69
+ const errorMessage = error instanceof Error ? error.message : String(error);
70
+ this.logger.warn(`Failed to stop watching config file: ${errorMessage}`);
71
+ this.watcher = undefined;
72
+ }
73
+ }
74
+ }
75
+ /**
76
+ * Handle config file change event
77
+ */
78
+ async handleConfigChange() {
79
+ if (this.isReloading) {
80
+ this.logger.warn('Config changed while reload in progress, changes will be picked up after current reload completes');
81
+ return;
82
+ }
83
+ this.isReloading = true;
84
+ try {
85
+ this.logger.info('Config file changed, reloading...');
86
+ await this.onReload();
87
+ this.logger.info('Config reload completed successfully');
88
+ }
89
+ catch (error) {
90
+ const errorMessage = error instanceof Error ? error.message : String(error);
91
+ this.logger.error('Config reload failed', { error: errorMessage });
92
+ }
93
+ finally {
94
+ this.isReloading = false;
95
+ }
96
+ }
97
+ /**
98
+ * Check if watcher is currently active
99
+ */
100
+ isActive() {
101
+ return this.watcher !== undefined;
102
+ }
103
+ /**
104
+ * Check if a reload is currently in progress
105
+ */
106
+ isReloadingInProgress() {
107
+ return this.isReloading;
108
+ }
109
+ }
@@ -69,3 +69,12 @@ export declare const DEFAULT_METRICS_CONFIG: {
69
69
  };
70
70
  readonly resetInterval: "daily";
71
71
  };
72
+ /**
73
+ * Default config reload configuration
74
+ */
75
+ export declare const DEFAULT_CONFIG_RELOAD_CONFIG: {
76
+ readonly enabled: false;
77
+ readonly watchFile: true;
78
+ readonly debounceMs: 1000;
79
+ readonly notifyOnReload: true;
80
+ };
@@ -89,3 +89,15 @@ export const DEFAULT_METRICS_CONFIG = {
89
89
  },
90
90
  resetInterval: "daily",
91
91
  };
92
+ // ============================================================================
93
+ // Config Reload Defaults
94
+ // ============================================================================
95
+ /**
96
+ * Default config reload configuration
97
+ */
98
+ export const DEFAULT_CONFIG_RELOAD_CONFIG = {
99
+ enabled: false,
100
+ watchFile: true,
101
+ debounceMs: 1000,
102
+ notifyOnReload: true,
103
+ };
@@ -46,7 +46,9 @@ export declare class FallbackHandler {
46
46
  */
47
47
  private abortSession;
48
48
  /**
49
- * Retry the prompt with a different model
49
+ * Queue the prompt asynchronously (non-blocking), then abort the retry loop.
50
+ * promptAsync FIRST queues pending work so the server doesn't dispose on idle.
51
+ * abort SECOND cancels the retry loop; the server sees the queued prompt and processes it.
50
52
  */
51
53
  retryWithModel(targetSessionID: string, model: FallbackModel, parts: MessagePart[], hierarchy: SessionHierarchy | null): Promise<void>;
52
54
  /**
@@ -69,4 +71,8 @@ export declare class FallbackHandler {
69
71
  * Clean up all resources
70
72
  */
71
73
  destroy(): void;
74
+ /**
75
+ * Update configuration (for hot reload)
76
+ */
77
+ updateConfig(newConfig: PluginConfig): void;
72
78
  }
@@ -95,7 +95,9 @@ export class FallbackHandler {
95
95
  }
96
96
  }
97
97
  /**
98
- * Retry the prompt with a different model
98
+ * Queue the prompt asynchronously (non-blocking), then abort the retry loop.
99
+ * promptAsync FIRST queues pending work so the server doesn't dispose on idle.
100
+ * abort SECOND cancels the retry loop; the server sees the queued prompt and processes it.
99
101
  */
100
102
  async retryWithModel(targetSessionID, model, parts, hierarchy) {
101
103
  // Track the new model for this session
@@ -129,13 +131,16 @@ export class FallbackHandler {
129
131
  }
130
132
  // Convert internal MessagePart to SDK-compatible format
131
133
  const sdkParts = convertPartsToSDKFormat(parts);
132
- await this.client.session.prompt({
134
+ // 1. promptAsync: queue the new prompt (returns immediately, non-blocking)
135
+ await this.client.session.promptAsync({
133
136
  path: { id: targetSessionID },
134
137
  body: {
135
138
  parts: sdkParts,
136
139
  model: { providerID: model.providerID, modelID: model.modelID },
137
140
  },
138
141
  });
142
+ // 2. abort: cancel the retry loop; server sees queued prompt and processes it
143
+ await this.abortSession(targetSessionID);
139
144
  await safeShowToast(this.client, {
140
145
  body: {
141
146
  title: "Fallback Successful",
@@ -176,8 +181,6 @@ export class FallbackHandler {
176
181
  if (this.healthTracker && currentProviderID && currentModelID) {
177
182
  this.healthTracker.recordFailure(currentProviderID, currentModelID);
178
183
  }
179
- // Abort current session with error handling
180
- await this.abortSession(targetSessionID);
181
184
  await safeShowToast(this.client, {
182
185
  body: {
183
186
  title: "Rate Limit Detected",
@@ -435,4 +438,32 @@ export class FallbackHandler {
435
438
  this.circuitBreaker.destroy();
436
439
  }
437
440
  }
441
+ /**
442
+ * Update configuration (for hot reload)
443
+ */
444
+ updateConfig(newConfig) {
445
+ this.config = newConfig;
446
+ // Update model selector
447
+ this.modelSelector.updateConfig(newConfig);
448
+ // Update retry manager
449
+ this.retryManager.updateConfig(newConfig.retryPolicy || {});
450
+ // Recreate circuit breaker if configuration changed significantly
451
+ const oldCircuitBreakerEnabled = this.circuitBreaker !== undefined;
452
+ if (newConfig.circuitBreaker?.enabled !== oldCircuitBreakerEnabled) {
453
+ // Destroy existing circuit breaker
454
+ if (this.circuitBreaker) {
455
+ this.circuitBreaker.destroy();
456
+ }
457
+ // Create new circuit breaker if enabled
458
+ if (newConfig.circuitBreaker?.enabled) {
459
+ this.circuitBreaker = new CircuitBreaker(newConfig.circuitBreaker, this.logger, this.metricsManager, this.client);
460
+ this.modelSelector.setCircuitBreaker(this.circuitBreaker);
461
+ }
462
+ else {
463
+ this.circuitBreaker = undefined;
464
+ this.modelSelector.setCircuitBreaker(undefined);
465
+ }
466
+ }
467
+ this.logger.debug('FallbackHandler configuration updated');
468
+ }
438
469
  }
@@ -42,4 +42,12 @@ export declare class ModelSelector {
42
42
  * Clean up stale rate-limited models
43
43
  */
44
44
  cleanupStaleEntries(): void;
45
+ /**
46
+ * Update configuration (for hot reload)
47
+ */
48
+ updateConfig(newConfig: PluginConfig): void;
49
+ /**
50
+ * Set circuit breaker (for hot reload)
51
+ */
52
+ setCircuitBreaker(circuitBreaker: CircuitBreaker | undefined): void;
45
53
  }
@@ -160,4 +160,16 @@ export class ModelSelector {
160
160
  }
161
161
  }
162
162
  }
163
+ /**
164
+ * Update configuration (for hot reload)
165
+ */
166
+ updateConfig(newConfig) {
167
+ this.config = newConfig;
168
+ }
169
+ /**
170
+ * Set circuit breaker (for hot reload)
171
+ */
172
+ setCircuitBreaker(circuitBreaker) {
173
+ this.circuitBreaker = circuitBreaker;
174
+ }
163
175
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Configuration reloader for hot reload functionality
3
+ */
4
+ import type { Logger } from '../../logger.js';
5
+ import type { PluginConfig, OpenCodeClient, ReloadResult, ReloadMetrics } from '../types/index.js';
6
+ import { ConfigValidator } from '../config/Validator.js';
7
+ /**
8
+ * Component references for updating on reload
9
+ */
10
+ export interface ComponentRefs {
11
+ fallbackHandler: {
12
+ updateConfig: (newConfig: PluginConfig) => void;
13
+ };
14
+ metricsManager?: {
15
+ updateConfig: (newConfig: PluginConfig) => void;
16
+ };
17
+ }
18
+ /**
19
+ * ConfigReloader class - handles configuration reload logic
20
+ */
21
+ export declare class ConfigReloader {
22
+ private config;
23
+ private configPath;
24
+ private logger;
25
+ private validator;
26
+ private client;
27
+ private components;
28
+ private directory;
29
+ private worktree?;
30
+ private notifyOnReload;
31
+ private reloadMetrics;
32
+ constructor(config: PluginConfig, configPath: string | null, logger: Logger, validator: ConfigValidator, client: OpenCodeClient, components: ComponentRefs, directory: string, worktree?: string, notifyOnReload?: boolean);
33
+ /**
34
+ * Reload configuration from file
35
+ */
36
+ reloadConfig(): Promise<ReloadResult>;
37
+ /**
38
+ * Apply configuration changes to components
39
+ */
40
+ private applyConfigChanges;
41
+ /**
42
+ * Get list of changed configuration settings
43
+ */
44
+ private getChangedSettings;
45
+ /**
46
+ * Get current configuration
47
+ */
48
+ getCurrentConfig(): PluginConfig;
49
+ /**
50
+ * Get reload metrics
51
+ */
52
+ getReloadMetrics(): ReloadMetrics;
53
+ /**
54
+ * Show success toast notification
55
+ */
56
+ private showSuccessToast;
57
+ /**
58
+ * Show error toast notification
59
+ */
60
+ private showErrorToast;
61
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Configuration reloader for hot reload functionality
3
+ */
4
+ import { loadConfig } from '../utils/config.js';
5
+ import { safeShowToast } from '../utils/helpers.js';
6
+ /**
7
+ * ConfigReloader class - handles configuration reload logic
8
+ */
9
+ export class ConfigReloader {
10
+ config;
11
+ configPath;
12
+ logger;
13
+ validator;
14
+ client;
15
+ components;
16
+ directory;
17
+ worktree;
18
+ notifyOnReload;
19
+ reloadMetrics;
20
+ constructor(config, configPath, logger, validator, client, components, directory, worktree, notifyOnReload = true) {
21
+ this.config = config;
22
+ this.configPath = configPath;
23
+ this.logger = logger;
24
+ this.validator = validator;
25
+ this.client = client;
26
+ this.components = components;
27
+ this.directory = directory;
28
+ this.worktree = worktree;
29
+ this.notifyOnReload = notifyOnReload;
30
+ this.reloadMetrics = {
31
+ totalReloads: 0,
32
+ successfulReloads: 0,
33
+ failedReloads: 0,
34
+ };
35
+ }
36
+ /**
37
+ * Reload configuration from file
38
+ */
39
+ async reloadConfig() {
40
+ const result = {
41
+ success: false,
42
+ timestamp: Date.now(),
43
+ };
44
+ // Track reload metrics
45
+ this.reloadMetrics.totalReloads++;
46
+ this.reloadMetrics.lastReloadTime = result.timestamp;
47
+ if (!this.configPath) {
48
+ result.error = 'No config file path available';
49
+ this.reloadMetrics.failedReloads++;
50
+ this.reloadMetrics.lastReloadSuccess = false;
51
+ return result;
52
+ }
53
+ try {
54
+ // Load new config
55
+ this.logger.debug(`Loading config from: ${this.configPath}`);
56
+ const loadResult = loadConfig(this.directory, this.worktree, this.logger);
57
+ const newConfig = loadResult.config;
58
+ const source = loadResult.source;
59
+ // Validate new config
60
+ const validation = source
61
+ ? this.validator.validateFile(source, newConfig.configValidation)
62
+ : this.validator.validate(newConfig, newConfig.configValidation);
63
+ if (!validation.isValid && newConfig.configValidation?.strict) {
64
+ result.error = `Validation failed: ${validation.errors.map(e => `${e.path}: ${e.message}`).join(', ')}`;
65
+ this.logger.error('Config validation failed in strict mode');
66
+ this.logger.error(`Errors: ${result.error}`);
67
+ this.showErrorToast('Config Reload Failed', result.error);
68
+ this.reloadMetrics.failedReloads++;
69
+ this.reloadMetrics.lastReloadSuccess = false;
70
+ return result;
71
+ }
72
+ if (validation.errors.length > 0) {
73
+ this.logger.warn(`Config validation found ${validation.errors.length} error(s)`);
74
+ for (const error of validation.errors) {
75
+ this.logger.warn(` ${error.path}: ${error.message}`);
76
+ }
77
+ }
78
+ // Apply the new configuration
79
+ this.applyConfigChanges(newConfig);
80
+ result.success = true;
81
+ this.reloadMetrics.successfulReloads++;
82
+ this.reloadMetrics.lastReloadSuccess = true;
83
+ this.logger.info('Configuration reloaded successfully');
84
+ this.logger.debug(`Reload metrics: ${this.reloadMetrics.successfulReloads}/${this.reloadMetrics.totalReloads} successful`);
85
+ if (this.notifyOnReload) {
86
+ this.showSuccessToast('Configuration Reloaded', 'Settings have been applied');
87
+ }
88
+ return result;
89
+ }
90
+ catch (error) {
91
+ const errorMessage = error instanceof Error ? error.message : String(error);
92
+ result.error = `Failed to reload config: ${errorMessage}`;
93
+ this.logger.error(result.error);
94
+ this.showErrorToast('Config Reload Failed', errorMessage);
95
+ this.reloadMetrics.failedReloads++;
96
+ this.reloadMetrics.lastReloadSuccess = false;
97
+ return result;
98
+ }
99
+ }
100
+ /**
101
+ * Apply configuration changes to components
102
+ */
103
+ applyConfigChanges(newConfig) {
104
+ const oldConfig = this.config;
105
+ // Update internal config reference
106
+ this.config = newConfig;
107
+ // Update components with new config
108
+ this.components.fallbackHandler.updateConfig(newConfig);
109
+ if (this.components.metricsManager) {
110
+ this.components.metricsManager.updateConfig(newConfig);
111
+ }
112
+ // Log configuration changes
113
+ const changedSettings = this.getChangedSettings(oldConfig, newConfig);
114
+ if (changedSettings.length > 0) {
115
+ this.logger.info('Configuration changes applied:');
116
+ for (const change of changedSettings) {
117
+ this.logger.info(` ${change}`);
118
+ }
119
+ }
120
+ }
121
+ /**
122
+ * Get list of changed configuration settings
123
+ */
124
+ getChangedSettings(oldConfig, newConfig) {
125
+ const changes = [];
126
+ // Check fallbackModels
127
+ if (JSON.stringify(oldConfig.fallbackModels) !== JSON.stringify(newConfig.fallbackModels)) {
128
+ changes.push(`fallbackModels: ${oldConfig.fallbackModels.length} → ${newConfig.fallbackModels.length} models`);
129
+ }
130
+ // Check cooldownMs
131
+ if (oldConfig.cooldownMs !== newConfig.cooldownMs) {
132
+ changes.push(`cooldownMs: ${oldConfig.cooldownMs}ms → ${newConfig.cooldownMs}ms`);
133
+ }
134
+ // Check fallbackMode
135
+ if (oldConfig.fallbackMode !== newConfig.fallbackMode) {
136
+ changes.push(`fallbackMode: ${oldConfig.fallbackMode} → ${newConfig.fallbackMode}`);
137
+ }
138
+ // Check retryPolicy
139
+ if (JSON.stringify(oldConfig.retryPolicy) !== JSON.stringify(newConfig.retryPolicy)) {
140
+ changes.push('retryPolicy: updated');
141
+ }
142
+ // Check circuitBreaker
143
+ if (JSON.stringify(oldConfig.circuitBreaker) !== JSON.stringify(newConfig.circuitBreaker)) {
144
+ changes.push('circuitBreaker: updated');
145
+ }
146
+ // Check metrics
147
+ if (JSON.stringify(oldConfig.metrics) !== JSON.stringify(newConfig.metrics)) {
148
+ changes.push('metrics: updated');
149
+ }
150
+ // Check log
151
+ if (JSON.stringify(oldConfig.log) !== JSON.stringify(newConfig.log)) {
152
+ changes.push('log: updated');
153
+ }
154
+ // Check enableHealthBasedSelection
155
+ if (oldConfig.enableHealthBasedSelection !== newConfig.enableHealthBasedSelection) {
156
+ changes.push(`enableHealthBasedSelection: ${oldConfig.enableHealthBasedSelection} → ${newConfig.enableHealthBasedSelection}`);
157
+ }
158
+ // Check verbose
159
+ if (oldConfig.verbose !== newConfig.verbose) {
160
+ changes.push(`verbose: ${oldConfig.verbose} → ${newConfig.verbose}`);
161
+ }
162
+ return changes;
163
+ }
164
+ /**
165
+ * Get current configuration
166
+ */
167
+ getCurrentConfig() {
168
+ return this.config;
169
+ }
170
+ /**
171
+ * Get reload metrics
172
+ */
173
+ getReloadMetrics() {
174
+ return { ...this.reloadMetrics };
175
+ }
176
+ /**
177
+ * Show success toast notification
178
+ */
179
+ showSuccessToast(title, message) {
180
+ safeShowToast(this.client, {
181
+ body: {
182
+ title,
183
+ message,
184
+ variant: 'success',
185
+ duration: 3000,
186
+ },
187
+ });
188
+ }
189
+ /**
190
+ * Show error toast notification
191
+ */
192
+ showErrorToast(title, message) {
193
+ safeShowToast(this.client, {
194
+ body: {
195
+ title,
196
+ message,
197
+ variant: 'error',
198
+ duration: 5000,
199
+ },
200
+ });
201
+ }
202
+ }
@@ -2,7 +2,7 @@
2
2
  * Metrics Manager - Handles metrics collection, aggregation, and reporting
3
3
  */
4
4
  import type { Logger } from '../../logger.js';
5
- import type { MetricsConfig, MetricsData, RateLimitMetrics, FallbackTargetMetrics, ModelPerformanceMetrics, CircuitBreakerStateType } from '../types/index.js';
5
+ import type { MetricsConfig, MetricsData, RateLimitMetrics, FallbackTargetMetrics, ModelPerformanceMetrics, CircuitBreakerStateType, PluginConfig } from '../types/index.js';
6
6
  /**
7
7
  * Metrics Manager class for collecting and reporting metrics
8
8
  */
@@ -98,5 +98,9 @@ export declare class MetricsManager {
98
98
  * Clean up resources
99
99
  */
100
100
  destroy(): void;
101
+ /**
102
+ * Update configuration (for hot reload)
103
+ */
104
+ updateConfig(newConfig: PluginConfig): void;
101
105
  }
102
106
  export type { MetricsConfig, MetricsData, RateLimitMetrics, FallbackTargetMetrics, ModelPerformanceMetrics };
@@ -613,4 +613,31 @@ export class MetricsManager {
613
613
  this.resetTimer = null;
614
614
  }
615
615
  }
616
+ /**
617
+ * Update configuration (for hot reload)
618
+ */
619
+ updateConfig(newConfig) {
620
+ const oldEnabled = this.config.enabled;
621
+ const oldResetInterval = this.config.resetInterval;
622
+ this.config = newConfig.metrics || { enabled: false, output: { console: true, format: "pretty" }, resetInterval: "daily" };
623
+ // Restart reset timer if reset interval changed
624
+ if (oldResetInterval !== this.config.resetInterval && this.config.enabled) {
625
+ this.startResetTimer();
626
+ }
627
+ // Enable/disable reset timer based on config.enabled change
628
+ if (oldEnabled !== this.config.enabled) {
629
+ if (this.config.enabled) {
630
+ this.startResetTimer();
631
+ }
632
+ else if (this.resetTimer) {
633
+ clearInterval(this.resetTimer);
634
+ this.resetTimer = null;
635
+ }
636
+ }
637
+ this.logger.debug('MetricsManager configuration updated', {
638
+ enabled: this.config.enabled,
639
+ resetInterval: this.config.resetInterval,
640
+ output: this.config.output,
641
+ });
642
+ }
616
643
  }
@@ -146,6 +146,33 @@ export interface ErrorPatternsConfig {
146
146
  custom?: ErrorPattern[];
147
147
  enableLearning?: boolean;
148
148
  }
149
+ /**
150
+ * Configuration hot reload settings
151
+ */
152
+ export interface ConfigReloadConfig {
153
+ enabled: boolean;
154
+ watchFile: boolean;
155
+ debounceMs: number;
156
+ notifyOnReload: boolean;
157
+ }
158
+ /**
159
+ * Result of a configuration reload operation
160
+ */
161
+ export interface ReloadResult {
162
+ success: boolean;
163
+ error?: string;
164
+ timestamp: number;
165
+ }
166
+ /**
167
+ * Metrics for configuration reload operations
168
+ */
169
+ export interface ReloadMetrics {
170
+ totalReloads: number;
171
+ successfulReloads: number;
172
+ failedReloads: number;
173
+ lastReloadTime?: number;
174
+ lastReloadSuccess?: boolean;
175
+ }
149
176
  /**
150
177
  * Plugin configuration
151
178
  */
@@ -165,6 +192,7 @@ export interface PluginConfig {
165
192
  healthPersistence?: HealthPersistenceConfig;
166
193
  verbose?: boolean;
167
194
  errorPatterns?: ErrorPatternsConfig;
195
+ configReload?: ConfigReloadConfig;
168
196
  }
169
197
  /**
170
198
  * Fallback state for tracking progress
@@ -413,6 +441,18 @@ export type OpenCodeClient = {
413
441
  };
414
442
  };
415
443
  }) => Promise<unknown>;
444
+ promptAsync: (args: {
445
+ path: {
446
+ id: string;
447
+ };
448
+ body: {
449
+ parts: SDKMessagePartInput[];
450
+ model: {
451
+ providerID: string;
452
+ modelID: string;
453
+ };
454
+ };
455
+ }) => Promise<unknown>;
416
456
  };
417
457
  tui?: {
418
458
  showToast: (toast: ToastMessage) => Promise<unknown>;
@@ -427,6 +467,17 @@ export type PluginContext = {
427
467
  };
428
468
  /**
429
469
  * Default fallback models
470
+ *
471
+ * NOTE: This is intentionally empty to force users to explicitly configure
472
+ * their fallback models. This prevents unintended model usage (e.g., gemini
473
+ * when not wanted) and makes configuration errors obvious immediately.
474
+ *
475
+ * Users must create a config file in one of these locations:
476
+ * - <worktree>/.opencode/rate-limit-fallback.json
477
+ * - <directory>/.opencode/rate-limit-fallback.json
478
+ * - <directory>/rate-limit-fallback.json
479
+ * - ~/.opencode/rate-limit-fallback.json
480
+ * - $XDG_CONFIG_HOME/opencode/rate-limit-fallback.json
430
481
  */
431
482
  export declare const DEFAULT_FALLBACK_MODELS: FallbackModel[];
432
483
  /**
@@ -6,12 +6,19 @@
6
6
  // ============================================================================
7
7
  /**
8
8
  * Default fallback models
9
+ *
10
+ * NOTE: This is intentionally empty to force users to explicitly configure
11
+ * their fallback models. This prevents unintended model usage (e.g., gemini
12
+ * when not wanted) and makes configuration errors obvious immediately.
13
+ *
14
+ * Users must create a config file in one of these locations:
15
+ * - <worktree>/.opencode/rate-limit-fallback.json
16
+ * - <directory>/.opencode/rate-limit-fallback.json
17
+ * - <directory>/rate-limit-fallback.json
18
+ * - ~/.opencode/rate-limit-fallback.json
19
+ * - $XDG_CONFIG_HOME/opencode/rate-limit-fallback.json
9
20
  */
10
- export const DEFAULT_FALLBACK_MODELS = [
11
- { providerID: "anthropic", modelID: "claude-3-5-sonnet-20250514" },
12
- { providerID: "google", modelID: "gemini-2.5-pro" },
13
- { providerID: "google", modelID: "gemini-2.5-flash" },
14
- ];
21
+ export const DEFAULT_FALLBACK_MODELS = [];
15
22
  /**
16
23
  * Default retry policy
17
24
  */
@@ -2,6 +2,7 @@
2
2
  * Configuration loading and validation
3
3
  */
4
4
  import type { PluginConfig } from '../types/index.js';
5
+ import type { Logger } from '../../logger.js';
5
6
  /**
6
7
  * Default plugin configuration
7
8
  */
@@ -12,6 +13,7 @@ export declare const DEFAULT_CONFIG: PluginConfig;
12
13
  export interface ConfigLoadResult {
13
14
  config: PluginConfig;
14
15
  source: string | null;
16
+ rawUserConfig?: Partial<PluginConfig>;
15
17
  }
16
18
  /**
17
19
  * Validate configuration values
@@ -20,4 +22,4 @@ export declare function validateConfig(config: Partial<PluginConfig>): PluginCon
20
22
  /**
21
23
  * Load and validate config from file paths
22
24
  */
23
- export declare function loadConfig(directory: string, worktree?: string): ConfigLoadResult;
25
+ export declare function loadConfig(directory: string, worktree?: string, logger?: Logger): ConfigLoadResult;
@@ -4,7 +4,7 @@
4
4
  import { existsSync, readFileSync } from "fs";
5
5
  import { join } from "path";
6
6
  import { DEFAULT_FALLBACK_MODELS, VALID_FALLBACK_MODES, VALID_RESET_INTERVALS, DEFAULT_RETRY_POLICY, VALID_RETRY_STRATEGIES, DEFAULT_CIRCUIT_BREAKER_CONFIG, } from '../types/index.js';
7
- import { DEFAULT_HEALTH_TRACKER_CONFIG, DEFAULT_COOLDOWN_MS, DEFAULT_FALLBACK_MODE, DEFAULT_LOG_CONFIG, DEFAULT_METRICS_CONFIG, } from '../config/defaults.js';
7
+ import { DEFAULT_HEALTH_TRACKER_CONFIG, DEFAULT_COOLDOWN_MS, DEFAULT_FALLBACK_MODE, DEFAULT_LOG_CONFIG, DEFAULT_METRICS_CONFIG, DEFAULT_CONFIG_RELOAD_CONFIG, } from '../config/defaults.js';
8
8
  /**
9
9
  * Default plugin configuration
10
10
  */
@@ -18,6 +18,7 @@ export const DEFAULT_CONFIG = {
18
18
  healthPersistence: DEFAULT_HEALTH_TRACKER_CONFIG,
19
19
  log: DEFAULT_LOG_CONFIG,
20
20
  metrics: DEFAULT_METRICS_CONFIG,
21
+ configReload: DEFAULT_CONFIG_RELOAD_CONFIG,
21
22
  };
22
23
  /**
23
24
  * Validate configuration values
@@ -54,12 +55,16 @@ export function validateConfig(config) {
54
55
  } : DEFAULT_CONFIG.metrics.output,
55
56
  resetInterval: resetInterval && VALID_RESET_INTERVALS.includes(resetInterval) ? resetInterval : DEFAULT_CONFIG.metrics.resetInterval,
56
57
  } : DEFAULT_CONFIG.metrics,
58
+ configReload: config.configReload ? {
59
+ ...DEFAULT_CONFIG.configReload,
60
+ ...config.configReload,
61
+ } : DEFAULT_CONFIG.configReload,
57
62
  };
58
63
  }
59
64
  /**
60
65
  * Load and validate config from file paths
61
66
  */
62
- export function loadConfig(directory, worktree) {
67
+ export function loadConfig(directory, worktree, logger) {
63
68
  const homedir = process.env.HOME || "";
64
69
  const xdgConfigHome = process.env.XDG_CONFIG_HOME || join(homedir, ".config");
65
70
  // Build search paths: worktree first, then directory, then home locations
@@ -77,17 +82,57 @@ export function loadConfig(directory, worktree) {
77
82
  }
78
83
  configPaths.push(join(homedir, ".opencode", "rate-limit-fallback.json"));
79
84
  configPaths.push(join(xdgConfigHome, "opencode", "rate-limit-fallback.json"));
85
+ // Log search paths for debugging
86
+ if (logger) {
87
+ logger.debug(`Searching for config file in ${configPaths.length} locations`);
88
+ for (const configPath of configPaths) {
89
+ const exists = existsSync(configPath);
90
+ logger.debug(` ${exists ? "✓" : "✗"} ${configPath}`);
91
+ }
92
+ }
80
93
  for (const configPath of configPaths) {
81
94
  if (existsSync(configPath)) {
82
95
  try {
83
96
  const content = readFileSync(configPath, "utf-8");
84
97
  const userConfig = JSON.parse(content);
85
- return { config: validateConfig(userConfig), source: configPath };
98
+ if (logger) {
99
+ logger.info(`Config loaded from: ${configPath}`);
100
+ }
101
+ return {
102
+ config: validateConfig(userConfig),
103
+ source: configPath,
104
+ rawUserConfig: userConfig,
105
+ };
86
106
  }
87
- catch {
107
+ catch (error) {
108
+ if (logger) {
109
+ logger.warn(`Failed to parse config file: ${configPath}`, { error: error instanceof Error ? error.message : String(error) });
110
+ }
88
111
  // Skip invalid config files silently - caller will log via structured logger
89
112
  }
90
113
  }
91
114
  }
115
+ if (logger) {
116
+ // Log that no config file was found
117
+ logger.info(`No config file found in any of the ${configPaths.length} search paths. Using default configuration.`);
118
+ // Show a warning if default fallback models is empty (which is now the case)
119
+ if (DEFAULT_CONFIG.fallbackModels.length === 0) {
120
+ logger.warn('No fallback models configured. The plugin will not be able to fallback when rate limited.');
121
+ logger.warn('Please create a config file with your fallback models.');
122
+ logger.warn('Config file locations (in order of priority):');
123
+ for (const configPath of configPaths) {
124
+ logger.warn(` - ${configPath}`);
125
+ }
126
+ logger.warn('Example config:');
127
+ logger.warn(JSON.stringify({
128
+ fallbackModels: [
129
+ { providerID: "anthropic", modelID: "claude-3-5-sonnet-20250514" },
130
+ ],
131
+ cooldownMs: 60000,
132
+ enabled: true,
133
+ fallbackMode: "cycle",
134
+ }, null, 2));
135
+ }
136
+ }
92
137
  return { config: DEFAULT_CONFIG, source: null };
93
138
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azumag/opencode-rate-limit-fallback",
3
- "version": "1.40.0",
3
+ "version": "1.49.0",
4
4
  "description": "OpenCode plugin that automatically switches to fallback models when rate limited",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",