@azumag/opencode-rate-limit-fallback 1.31.0 → 1.36.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.
@@ -23,6 +23,16 @@ export const DEFAULT_RETRY_POLICY = {
23
23
  jitterEnabled: false,
24
24
  jitterFactor: 0.1,
25
25
  };
26
+ /**
27
+ * Default circuit breaker configuration
28
+ */
29
+ export const DEFAULT_CIRCUIT_BREAKER_CONFIG = {
30
+ enabled: false,
31
+ failureThreshold: 5,
32
+ recoveryTimeoutMs: 60000,
33
+ halfOpenMaxCalls: 1,
34
+ successThreshold: 2,
35
+ };
26
36
  /**
27
37
  * Valid fallback modes
28
38
  */
@@ -6,6 +6,13 @@ import type { PluginConfig } from '../types/index.js';
6
6
  * Default plugin configuration
7
7
  */
8
8
  export declare const DEFAULT_CONFIG: PluginConfig;
9
+ /**
10
+ * Result of config loading, includes which file was loaded
11
+ */
12
+ export interface ConfigLoadResult {
13
+ config: PluginConfig;
14
+ source: string | null;
15
+ }
9
16
  /**
10
17
  * Validate configuration values
11
18
  */
@@ -13,4 +20,4 @@ export declare function validateConfig(config: Partial<PluginConfig>): PluginCon
13
20
  /**
14
21
  * Load and validate config from file paths
15
22
  */
16
- export declare function loadConfig(directory: string): PluginConfig;
23
+ export declare function loadConfig(directory: string, worktree?: string): ConfigLoadResult;
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { existsSync, readFileSync } from "fs";
5
5
  import { join } from "path";
6
- import { DEFAULT_FALLBACK_MODELS, VALID_FALLBACK_MODES, VALID_RESET_INTERVALS, DEFAULT_RETRY_POLICY, VALID_RETRY_STRATEGIES, } from '../types/index.js';
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
7
  /**
8
8
  * Default plugin configuration
9
9
  */
@@ -13,6 +13,7 @@ export const DEFAULT_CONFIG = {
13
13
  enabled: true,
14
14
  fallbackMode: "cycle",
15
15
  retryPolicy: DEFAULT_RETRY_POLICY,
16
+ circuitBreaker: DEFAULT_CIRCUIT_BREAKER_CONFIG,
16
17
  log: {
17
18
  level: "warn",
18
19
  format: "simple",
@@ -37,13 +38,17 @@ export function validateConfig(config) {
37
38
  return {
38
39
  ...DEFAULT_CONFIG,
39
40
  ...config,
40
- fallbackModels: config.fallbackModels || DEFAULT_CONFIG.fallbackModels,
41
+ fallbackModels: Array.isArray(config.fallbackModels) ? config.fallbackModels : DEFAULT_CONFIG.fallbackModels,
41
42
  fallbackMode: mode && VALID_FALLBACK_MODES.includes(mode) ? mode : DEFAULT_CONFIG.fallbackMode,
42
43
  retryPolicy: config.retryPolicy ? {
43
44
  ...DEFAULT_CONFIG.retryPolicy,
44
45
  ...config.retryPolicy,
45
46
  strategy: strategy && VALID_RETRY_STRATEGIES.includes(strategy) ? strategy : DEFAULT_CONFIG.retryPolicy.strategy,
46
47
  } : DEFAULT_CONFIG.retryPolicy,
48
+ circuitBreaker: config.circuitBreaker ? {
49
+ ...DEFAULT_CONFIG.circuitBreaker,
50
+ ...config.circuitBreaker,
51
+ } : DEFAULT_CONFIG.circuitBreaker,
47
52
  log: config.log ? { ...DEFAULT_CONFIG.log, ...config.log } : DEFAULT_CONFIG.log,
48
53
  metrics: config.metrics ? {
49
54
  ...DEFAULT_CONFIG.metrics,
@@ -59,20 +64,30 @@ export function validateConfig(config) {
59
64
  /**
60
65
  * Load and validate config from file paths
61
66
  */
62
- export function loadConfig(directory) {
67
+ export function loadConfig(directory, worktree) {
63
68
  const homedir = process.env.HOME || "";
64
- const configPaths = [
65
- join(directory, ".opencode", "rate-limit-fallback.json"),
66
- join(directory, "rate-limit-fallback.json"),
67
- join(homedir, ".opencode", "rate-limit-fallback.json"),
68
- join(homedir, ".config", "opencode", "rate-limit-fallback.json"),
69
- ];
69
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME || join(homedir, ".config");
70
+ // Build search paths: worktree first, then directory, then home locations
71
+ const searchDirs = [];
72
+ if (worktree) {
73
+ searchDirs.push(worktree);
74
+ }
75
+ if (!worktree || worktree !== directory) {
76
+ searchDirs.push(directory);
77
+ }
78
+ const configPaths = [];
79
+ for (const dir of searchDirs) {
80
+ configPaths.push(join(dir, ".opencode", "rate-limit-fallback.json"));
81
+ configPaths.push(join(dir, "rate-limit-fallback.json"));
82
+ }
83
+ configPaths.push(join(homedir, ".opencode", "rate-limit-fallback.json"));
84
+ configPaths.push(join(xdgConfigHome, "opencode", "rate-limit-fallback.json"));
70
85
  for (const configPath of configPaths) {
71
86
  if (existsSync(configPath)) {
72
87
  try {
73
88
  const content = readFileSync(configPath, "utf-8");
74
89
  const userConfig = JSON.parse(content);
75
- return validateConfig(userConfig);
90
+ return { config: validateConfig(userConfig), source: configPath };
76
91
  }
77
92
  catch (error) {
78
93
  // Log config errors to console immediately before logger is initialized
@@ -81,5 +96,5 @@ export function loadConfig(directory) {
81
96
  }
82
97
  }
83
98
  }
84
- return DEFAULT_CONFIG;
99
+ return { config: DEFAULT_CONFIG, source: null };
85
100
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azumag/opencode-rate-limit-fallback",
3
- "version": "1.31.0",
3
+ "version": "1.36.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",
@@ -1,7 +0,0 @@
1
- /**
2
- * Rate limit error detection
3
- */
4
- /**
5
- * Check if error is rate limit related
6
- */
7
- export declare function isRateLimitError(error: unknown): boolean;
@@ -1,34 +0,0 @@
1
- /**
2
- * Rate limit error detection
3
- */
4
- /**
5
- * Check if error is rate limit related
6
- */
7
- export function isRateLimitError(error) {
8
- if (!error || typeof error !== "object")
9
- return false;
10
- // More type-safe error object structure
11
- const err = error;
12
- // Check for 429 status code in APIError (strict check)
13
- if (err.name === "APIError" && err.data?.statusCode === 429) {
14
- return true;
15
- }
16
- // Type-safe access to error fields
17
- const responseBody = String(err.data?.responseBody || "").toLowerCase();
18
- const message = String(err.data?.message || err.message || "").toLowerCase();
19
- // Strict rate limit indicators only - avoid false positives
20
- const strictRateLimitIndicators = [
21
- "rate limit",
22
- "rate_limit",
23
- "ratelimit",
24
- "too many requests",
25
- "quota exceeded",
26
- ];
27
- // Check for 429 in text (explicit HTTP status code)
28
- if (responseBody.includes("429") || message.includes("429")) {
29
- return true;
30
- }
31
- // Check for strict rate limit keywords
32
- return strictRateLimitIndicators.some((indicator) => responseBody.includes(indicator) ||
33
- message.includes(indicator));
34
- }