@azumag/opencode-rate-limit-fallback 1.49.0 → 1.57.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.
@@ -113,6 +113,26 @@ export interface HealthTrackerConfig {
113
113
  * Use this for backward compatibility.
114
114
  */
115
115
  export type HealthPersistenceConfig = HealthTrackerConfig;
116
+ /**
117
+ * Dynamic prioritization configuration
118
+ */
119
+ export interface DynamicPrioritizationConfig {
120
+ enabled: boolean;
121
+ updateInterval: number;
122
+ successRateWeight: number;
123
+ responseTimeWeight: number;
124
+ recentUsageWeight: number;
125
+ minSamples: number;
126
+ maxHistorySize: number;
127
+ }
128
+ /**
129
+ * Dynamic prioritization metrics
130
+ */
131
+ export interface DynamicPrioritizationMetrics {
132
+ enabled: boolean;
133
+ reorders: number;
134
+ modelsWithDynamicScores: number;
135
+ }
116
136
  /**
117
137
  * Health metrics for a model
118
138
  */
@@ -139,12 +159,45 @@ export interface ErrorPattern {
139
159
  patterns: (string | RegExp)[];
140
160
  priority: number;
141
161
  }
162
+ /**
163
+ * Pattern candidate extracted from an error
164
+ */
165
+ export interface PatternCandidate {
166
+ provider?: string;
167
+ patterns: (string | RegExp)[];
168
+ sourceError: string;
169
+ extractedAt: number;
170
+ }
171
+ /**
172
+ * Learned pattern with metadata
173
+ */
174
+ export interface LearnedPattern extends ErrorPattern {
175
+ confidence: number;
176
+ learnedAt: string;
177
+ sampleCount: number;
178
+ lastUsed?: number;
179
+ }
180
+ /**
181
+ * Learning configuration
182
+ */
183
+ export interface LearningConfig {
184
+ enabled: boolean;
185
+ autoApproveThreshold: number;
186
+ maxLearnedPatterns: number;
187
+ minErrorFrequency: number;
188
+ learningWindowMs: number;
189
+ }
142
190
  /**
143
191
  * Error pattern configuration
144
192
  */
145
193
  export interface ErrorPatternsConfig {
146
194
  custom?: ErrorPattern[];
147
195
  enableLearning?: boolean;
196
+ learnedPatterns?: LearnedPattern[];
197
+ autoApproveThreshold?: number;
198
+ maxLearnedPatterns?: number;
199
+ minErrorFrequency?: number;
200
+ learningWindowMs?: number;
148
201
  }
149
202
  /**
150
203
  * Configuration hot reload settings
@@ -193,6 +246,7 @@ export interface PluginConfig {
193
246
  verbose?: boolean;
194
247
  errorPatterns?: ErrorPatternsConfig;
195
248
  configReload?: ConfigReloadConfig;
249
+ dynamicPrioritization?: DynamicPrioritizationConfig;
196
250
  }
197
251
  /**
198
252
  * Fallback state for tracking progress
@@ -357,6 +411,7 @@ export interface MetricsData {
357
411
  total: CircuitBreakerMetrics;
358
412
  byModel: Map<string, CircuitBreakerMetrics>;
359
413
  };
414
+ dynamicPrioritization: DynamicPrioritizationMetrics;
360
415
  startedAt: number;
361
416
  generatedAt: number;
362
417
  }
@@ -2,9 +2,9 @@
2
2
  * Configuration loading and validation
3
3
  */
4
4
  import { existsSync, readFileSync } from "fs";
5
- import { join } from "path";
5
+ import { join, resolve, normalize, relative } 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, DEFAULT_CONFIG_RELOAD_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, DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG, DEFAULT_ERROR_PATTERN_LEARNING_CONFIG, } from '../config/defaults.js';
8
8
  /**
9
9
  * Default plugin configuration
10
10
  */
@@ -19,7 +19,35 @@ export const DEFAULT_CONFIG = {
19
19
  log: DEFAULT_LOG_CONFIG,
20
20
  metrics: DEFAULT_METRICS_CONFIG,
21
21
  configReload: DEFAULT_CONFIG_RELOAD_CONFIG,
22
+ dynamicPrioritization: DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG,
23
+ errorPatterns: DEFAULT_ERROR_PATTERN_LEARNING_CONFIG,
22
24
  };
25
+ /**
26
+ * Validate that a path does not contain directory traversal attempts
27
+ */
28
+ function validatePathSafety(path, allowedDirs) {
29
+ try {
30
+ const resolvedPath = resolve(path);
31
+ const normalizedPath = normalize(path);
32
+ // Check for obvious path traversal patterns
33
+ if (normalizedPath.includes('..')) {
34
+ return false;
35
+ }
36
+ // Check that resolved path is within allowed directories
37
+ for (const allowedDir of allowedDirs) {
38
+ const resolvedAllowedDir = resolve(allowedDir);
39
+ const relativePath = relative(resolvedAllowedDir, resolvedPath);
40
+ // If relative path does not start with '..', the path is within the allowed directory
41
+ if (!relativePath.startsWith('..')) {
42
+ return true;
43
+ }
44
+ }
45
+ return false;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
23
51
  /**
24
52
  * Validate configuration values
25
53
  */
@@ -59,6 +87,14 @@ export function validateConfig(config) {
59
87
  ...DEFAULT_CONFIG.configReload,
60
88
  ...config.configReload,
61
89
  } : DEFAULT_CONFIG.configReload,
90
+ dynamicPrioritization: config.dynamicPrioritization ? {
91
+ ...DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG,
92
+ ...config.dynamicPrioritization,
93
+ } : DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG,
94
+ errorPatterns: config.errorPatterns ? {
95
+ ...DEFAULT_ERROR_PATTERN_LEARNING_CONFIG,
96
+ ...config.errorPatterns,
97
+ } : DEFAULT_ERROR_PATTERN_LEARNING_CONFIG,
62
98
  };
63
99
  }
64
100
  /**
@@ -70,18 +106,18 @@ export function loadConfig(directory, worktree, logger) {
70
106
  // Build search paths: worktree first, then directory, then home locations
71
107
  const searchDirs = [];
72
108
  if (worktree) {
73
- searchDirs.push(worktree);
109
+ searchDirs.push(resolve(worktree));
74
110
  }
75
111
  if (!worktree || worktree !== directory) {
76
- searchDirs.push(directory);
112
+ searchDirs.push(resolve(directory));
77
113
  }
114
+ searchDirs.push(resolve(homedir));
115
+ searchDirs.push(resolve(xdgConfigHome));
78
116
  const configPaths = [];
79
117
  for (const dir of searchDirs) {
80
118
  configPaths.push(join(dir, ".opencode", "rate-limit-fallback.json"));
81
119
  configPaths.push(join(dir, "rate-limit-fallback.json"));
82
120
  }
83
- configPaths.push(join(homedir, ".opencode", "rate-limit-fallback.json"));
84
- configPaths.push(join(xdgConfigHome, "opencode", "rate-limit-fallback.json"));
85
121
  // Log search paths for debugging
86
122
  if (logger) {
87
123
  logger.debug(`Searching for config file in ${configPaths.length} locations`);
@@ -92,6 +128,13 @@ export function loadConfig(directory, worktree, logger) {
92
128
  }
93
129
  for (const configPath of configPaths) {
94
130
  if (existsSync(configPath)) {
131
+ // Validate path safety before reading
132
+ if (!validatePathSafety(configPath, searchDirs)) {
133
+ if (logger) {
134
+ logger.warn(`Config file rejected due to path validation: ${configPath}`);
135
+ }
136
+ continue;
137
+ }
95
138
  try {
96
139
  const content = readFileSync(configPath, "utf-8");
97
140
  const userConfig = JSON.parse(content);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Similarity utility functions
3
+ */
4
+ /**
5
+ * Calculate Jaccard similarity between two strings
6
+ * @param str1 - First string
7
+ * @param str2 - Second string
8
+ * @returns Similarity score between 0 and 1
9
+ */
10
+ export declare function calculateJaccardSimilarity(str1: string, str2: string): number;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Similarity utility functions
3
+ */
4
+ /**
5
+ * Calculate Jaccard similarity between two strings
6
+ * @param str1 - First string
7
+ * @param str2 - Second string
8
+ * @returns Similarity score between 0 and 1
9
+ */
10
+ export function calculateJaccardSimilarity(str1, str2) {
11
+ // Tokenize strings
12
+ const tokens1 = new Set(str1.split(/\s+/).filter(t => t.length > 0));
13
+ const tokens2 = new Set(str2.split(/\s+/).filter(t => t.length > 0));
14
+ if (tokens1.size === 0 && tokens2.size === 0) {
15
+ return 1;
16
+ }
17
+ if (tokens1.size === 0 || tokens2.size === 0) {
18
+ return 0;
19
+ }
20
+ // Calculate intersection and union
21
+ const intersection = new Set([...tokens1].filter(x => tokens2.has(x)));
22
+ const union = new Set([...tokens1, ...tokens2]);
23
+ return intersection.size / union.size;
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azumag/opencode-rate-limit-fallback",
3
- "version": "1.49.0",
3
+ "version": "1.57.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",