opencode-free-fleet 0.1.0 → 0.2.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 +85 -284
- package/dist/core/adapters/index.d.ts +13 -0
- package/dist/core/adapters/index.js +546 -0
- package/dist/core/oracle.d.ts +84 -0
- package/dist/core/oracle.js +234 -0
- package/dist/core/racer.d.ts +105 -0
- package/dist/core/racer.js +209 -0
- package/dist/core/scout.d.ts +124 -0
- package/dist/core/scout.js +503 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +332 -0
- package/dist/types/index.d.ts +144 -0
- package/dist/types/index.js +54 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +6 -0
- package/package.json +11 -3
- package/src/version.ts +2 -2
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata Oracle - Cross-Provider Model Metadata Lookup
|
|
3
|
+
*
|
|
4
|
+
* v0.2.1 - Build Repair: Removed problematic Z.Ai SDK import
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Static knowledge base of confirmed free models
|
|
8
|
+
* This can be updated without code changes
|
|
9
|
+
*/
|
|
10
|
+
export const CONFIRMED_FREE_MODELS = new Set([
|
|
11
|
+
// OpenRouter (verified free via pricing)
|
|
12
|
+
'openrouter/qwen/qwen3-coder:free',
|
|
13
|
+
'openrouter/deepseek/deepseek-v3.2',
|
|
14
|
+
'openrouter/deepseek/deepseek-r1-0528:free',
|
|
15
|
+
'openrouter/z-ai/glm-4.5-air:free',
|
|
16
|
+
'openrouter/arcee-ai/trinity-large-preview:free',
|
|
17
|
+
'openrouter/mistralai/mistral-small-3.1-24b-instruct:free',
|
|
18
|
+
'openrouter/mistralai/mistral-tiny:free',
|
|
19
|
+
'openrouter/nvidia/nemotron-3-nano-30b-a3b:free',
|
|
20
|
+
'openrouter/nvidia/nemotron-3-nano-12b-v2-vl:free',
|
|
21
|
+
'openrouter/nvidia/nemotron-3-nano-9b-v2:free',
|
|
22
|
+
'openrouter/google/gemma-3n-e2b-it:free',
|
|
23
|
+
'openrouter/google/gemma-3n-e4b-it:free',
|
|
24
|
+
// DeepSeek (official documentation)
|
|
25
|
+
'deepseek/deepseek-chat',
|
|
26
|
+
'deepseek/deepseek-v3',
|
|
27
|
+
'deepseek/deepseek-r1',
|
|
28
|
+
// Groq (current policy)
|
|
29
|
+
'groq/llama-3.1-8b-instruct',
|
|
30
|
+
'groq/llama-3.1-70b-versatile-instruct',
|
|
31
|
+
'groq/mixtral-8x7b-instruct',
|
|
32
|
+
// Hugging Face (serverless free tier)
|
|
33
|
+
'huggingface/Qwen/Qwen2.5-72B-Instruct-Turbo',
|
|
34
|
+
// Google (limited free tier)
|
|
35
|
+
'google/gemini-1.5-flash',
|
|
36
|
+
'google/gemini-1.5-flash-8b'
|
|
37
|
+
]);
|
|
38
|
+
/**
|
|
39
|
+
* External metadata APIs
|
|
40
|
+
* Source for free tier verification
|
|
41
|
+
*/
|
|
42
|
+
const METADATA_SOURCES = [
|
|
43
|
+
'models.dev' // Open source model metadata database
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Models.dev metadata API client
|
|
47
|
+
*/
|
|
48
|
+
class ModelsDevAdapter {
|
|
49
|
+
providerId = 'models.dev';
|
|
50
|
+
providerName = 'Models.dev';
|
|
51
|
+
async fetchModelsMetadata(modelIds) {
|
|
52
|
+
if (modelIds && modelIds.length === 0) {
|
|
53
|
+
console.log('📊 Models.dev: No model IDs provided, fetching all known free models');
|
|
54
|
+
}
|
|
55
|
+
const all = await this._fetchFromModelsDev();
|
|
56
|
+
return modelIds ? all.filter(m => modelIds.includes(m.id)) : all;
|
|
57
|
+
}
|
|
58
|
+
async fetchModelMetadata(modelId) {
|
|
59
|
+
const all = await this._fetchFromModelsDev();
|
|
60
|
+
const model = all.find(m => m.id === modelId);
|
|
61
|
+
if (!model) {
|
|
62
|
+
return {
|
|
63
|
+
id: modelId,
|
|
64
|
+
provider: this.providerId,
|
|
65
|
+
name: modelId,
|
|
66
|
+
isFree: false,
|
|
67
|
+
confidence: 0,
|
|
68
|
+
reason: 'Model not found in Models.dev',
|
|
69
|
+
pricing: { prompt: '0', completion: '0', request: '0' }
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Determine if free based on Models.dev data
|
|
73
|
+
const isFree = model.pricing?.prompt === '0' || model.pricing?.prompt === '0.0' ||
|
|
74
|
+
model.pricing?.completion === '0' || model.pricing?.completion === '0.0';
|
|
75
|
+
return {
|
|
76
|
+
id: model.id,
|
|
77
|
+
provider: this.providerId,
|
|
78
|
+
name: model.name || model.id,
|
|
79
|
+
isFree,
|
|
80
|
+
confidence: isFree ? 1.0 : 0.7,
|
|
81
|
+
reason: isFree ? `Confirmed free via Models.dev (prompt=${model.pricing?.prompt}, completion=${model.pricing?.completion})` : 'Uncertain pricing - SDK may differ',
|
|
82
|
+
lastVerified: new Date().toISOString(),
|
|
83
|
+
pricing: {
|
|
84
|
+
prompt: model.pricing?.prompt || '0',
|
|
85
|
+
completion: model.pricing?.completion || '0',
|
|
86
|
+
request: model.pricing?.request || '0'
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async _fetchFromModelsDev() {
|
|
91
|
+
console.log('📊 Models.dev: Fetching model metadata...');
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch('https://models.dev/api/v1/models', {
|
|
94
|
+
headers: {
|
|
95
|
+
'Accept': 'application/json'
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
throw new Error(`Models.dev API error: ${response.status} ${response.statusText}`);
|
|
100
|
+
}
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
const models = data.data || [];
|
|
103
|
+
console.log(`✓ Models.dev: Found ${models.length} models`);
|
|
104
|
+
return models;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error('❌ Models.dev API error:', error);
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
isAvailable() {
|
|
112
|
+
// Always available (public API)
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Unified metadata oracle
|
|
118
|
+
* Aggregates data from multiple metadata sources
|
|
119
|
+
*/
|
|
120
|
+
export class MetadataOracle {
|
|
121
|
+
adapters = new Map();
|
|
122
|
+
constructor() {
|
|
123
|
+
this._initializeAdapters();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Initialize all metadata adapters
|
|
127
|
+
*/
|
|
128
|
+
_initializeAdapters() {
|
|
129
|
+
console.log('🔮 Metadata Oracle: Initializing adapters...');
|
|
130
|
+
// Models.dev - Always available
|
|
131
|
+
this.adapters.set('models.dev', new ModelsDevAdapter());
|
|
132
|
+
// Note: Removed Z.Ai SDK, Google Cloud AI SDK, etc.
|
|
133
|
+
// These adapters are now implemented directly in src/core/adapters/index.ts
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check which adapters are available
|
|
137
|
+
*/
|
|
138
|
+
getAvailableAdapters() {
|
|
139
|
+
const available = [];
|
|
140
|
+
for (const [providerId, adapter] of this.adapters.entries()) {
|
|
141
|
+
if (adapter.isAvailable()) {
|
|
142
|
+
available.push(providerId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return available;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Fetch metadata for a specific model from all available sources
|
|
149
|
+
* This is the main method Scout should call for free tier detection
|
|
150
|
+
*/
|
|
151
|
+
async fetchModelMetadata(modelId) {
|
|
152
|
+
console.log(`\n🔮 Metadata Oracle: Fetching metadata for ${modelId}...\n`);
|
|
153
|
+
const availableAdapters = this.getAvailableAdapters();
|
|
154
|
+
const allMetadata = [];
|
|
155
|
+
// Try each available adapter
|
|
156
|
+
for (const providerId of availableAdapters) {
|
|
157
|
+
const adapter = this.adapters.get(providerId);
|
|
158
|
+
if (!adapter)
|
|
159
|
+
continue;
|
|
160
|
+
try {
|
|
161
|
+
const metadata = await adapter.fetchModelMetadata(modelId);
|
|
162
|
+
allMetadata.push(metadata);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.warn(`⚠️ Metadata Oracle: Adapter ${providerId} failed: ${error}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (allMetadata.length === 0) {
|
|
169
|
+
return {
|
|
170
|
+
id: modelId,
|
|
171
|
+
provider: 'unknown',
|
|
172
|
+
name: modelId,
|
|
173
|
+
isFree: false,
|
|
174
|
+
confidence: 0,
|
|
175
|
+
reason: 'Model not found in any metadata source',
|
|
176
|
+
pricing: { prompt: '0', completion: '0', request: '0' }
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// Merge results with confidence scoring
|
|
180
|
+
const freeResults = allMetadata.filter(m => m.isFree);
|
|
181
|
+
const hasFreeResult = freeResults.length > 0;
|
|
182
|
+
// Determine overall confidence
|
|
183
|
+
let confidence = 0.3; // Low confidence if no metadata
|
|
184
|
+
let reason = 'No metadata found';
|
|
185
|
+
if (hasFreeResult) {
|
|
186
|
+
confidence = 1.0; // High confidence if at least one source says free
|
|
187
|
+
reason = `Confirmed free by ${freeResults.map(m => m.provider).join(', ')}`;
|
|
188
|
+
}
|
|
189
|
+
else if (allMetadata.length > 0) {
|
|
190
|
+
confidence = 0.7; // Medium confidence if metadata exists but no free result
|
|
191
|
+
reason = `Metadata found but not confirmed free (providers: ${allMetadata.map(m => m.provider).join(', ')})`;
|
|
192
|
+
}
|
|
193
|
+
// Return first free result (highest confidence)
|
|
194
|
+
const finalMetadata = hasFreeResult ? freeResults[0] : allMetadata[0];
|
|
195
|
+
return {
|
|
196
|
+
...finalMetadata,
|
|
197
|
+
confidence,
|
|
198
|
+
reason,
|
|
199
|
+
lastVerified: new Date().toISOString()
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Batch fetch metadata for multiple models
|
|
204
|
+
*/
|
|
205
|
+
async fetchModelsMetadata(modelIds) {
|
|
206
|
+
console.log(`\n🔮 Metadata Oracle: Fetching metadata for ${modelIds.length} models...\n`);
|
|
207
|
+
const availableAdapters = this.getAvailableAdapters();
|
|
208
|
+
const allMetadata = [];
|
|
209
|
+
for (const modelId of modelIds) {
|
|
210
|
+
const metadata = await this.fetchModelMetadata(modelId);
|
|
211
|
+
allMetadata.push(metadata);
|
|
212
|
+
}
|
|
213
|
+
return allMetadata;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Get list of all confirmed free models
|
|
217
|
+
*/
|
|
218
|
+
getConfirmedFreeModels() {
|
|
219
|
+
return CONFIRMED_FREE_MODELS;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Add a manually confirmed free model
|
|
223
|
+
* This allows updating of static knowledge base
|
|
224
|
+
*/
|
|
225
|
+
addConfirmedFreeModel(modelId) {
|
|
226
|
+
CONFIRMED_FREE_MODELS.add(modelId);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Remove a model from confirmed free list
|
|
230
|
+
*/
|
|
231
|
+
removeConfirmedFreeModel(modelId) {
|
|
232
|
+
CONFIRMED_FREE_MODELS.delete(modelId);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Racer - Zero-Latency Model Competition
|
|
3
|
+
*
|
|
4
|
+
* This module implements Promise.any-based racing between multiple free models,
|
|
5
|
+
* accepting the first valid response. This eliminates waterfall latency
|
|
6
|
+
* and optimizes for zero-cost execution.
|
|
7
|
+
*
|
|
8
|
+
* Key Features:
|
|
9
|
+
* - Promise.any for race condition
|
|
10
|
+
* - AbortController for timeout handling
|
|
11
|
+
* - Progress callbacks for monitoring
|
|
12
|
+
* - Error aggregation for all failures
|
|
13
|
+
*/
|
|
14
|
+
import type { RaceResult, RaceConfig } from '../types/index.js';
|
|
15
|
+
/**
|
|
16
|
+
* Racer class for competing free models
|
|
17
|
+
*/
|
|
18
|
+
export declare class FreeModelRacer {
|
|
19
|
+
private config;
|
|
20
|
+
private activeRaces;
|
|
21
|
+
constructor(config?: RaceConfig);
|
|
22
|
+
/**
|
|
23
|
+
* Compete between multiple free models and return the fastest valid response
|
|
24
|
+
* Uses Promise.any for race condition - fires all requests simultaneously
|
|
25
|
+
* and accepts the first valid response.
|
|
26
|
+
*
|
|
27
|
+
* @param models - Array of model identifiers to compete
|
|
28
|
+
* @param executeWithModel - Function to execute task with a specific model
|
|
29
|
+
* @param raceId - Optional ID for tracking and cancellation
|
|
30
|
+
* @returns Object containing the winning model and its result
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const racer = new FreeModelRacer({ timeoutMs: 15000 });
|
|
35
|
+
*
|
|
36
|
+
* const models = [
|
|
37
|
+
* 'deepseek/deepseek-v3.2',
|
|
38
|
+
* 'zai-coding-plan/glm-4.7-flash',
|
|
39
|
+
* 'openrouter/mistralai/mistral-small-3.1-24b-instruct:free'
|
|
40
|
+
* ];
|
|
41
|
+
*
|
|
42
|
+
* const winner = await racer.race(
|
|
43
|
+
* models,
|
|
44
|
+
* async (model) => {
|
|
45
|
+
* return await client.chat.completions.create({ model, messages });
|
|
46
|
+
* },
|
|
47
|
+
* 'session-123'
|
|
48
|
+
* );
|
|
49
|
+
*
|
|
50
|
+
* console.log(`Fastest: ${winner.model} (${winner.duration}ms)`);
|
|
51
|
+
* return winner.result;
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
race<T>(models: string[], executeWithModel: (model: string) => Promise<T>, raceId?: string): Promise<RaceResult<T>>;
|
|
55
|
+
/**
|
|
56
|
+
* Race models from a category config
|
|
57
|
+
*/
|
|
58
|
+
raceFromCategory<T>(categoryConfig: {
|
|
59
|
+
model: string;
|
|
60
|
+
fallback: string[];
|
|
61
|
+
}, executeWithModel: (model: string) => Promise<T>, raceId?: string): Promise<RaceResult<T>>;
|
|
62
|
+
/**
|
|
63
|
+
* Cancel an active race by ID
|
|
64
|
+
*/
|
|
65
|
+
cancelRace(raceId: string): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Cancel all active races
|
|
68
|
+
*/
|
|
69
|
+
cancelAllRaces(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Get count of active races
|
|
72
|
+
*/
|
|
73
|
+
getActiveRaceCount(): number;
|
|
74
|
+
/**
|
|
75
|
+
* Check if a race is currently active
|
|
76
|
+
*/
|
|
77
|
+
isRaceActive(raceId: string): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Create a timeout promise that rejects when abort signal is received
|
|
80
|
+
*/
|
|
81
|
+
private createTimeoutPromise;
|
|
82
|
+
/**
|
|
83
|
+
* Update race configuration
|
|
84
|
+
*/
|
|
85
|
+
updateConfig(config: Partial<RaceConfig>): void;
|
|
86
|
+
/**
|
|
87
|
+
* Get current race configuration
|
|
88
|
+
*/
|
|
89
|
+
getConfig(): Readonly<RaceConfig>;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Compete between multiple free models (convenience function)
|
|
93
|
+
*
|
|
94
|
+
* This is a stateless version of FreeModelRacer for simple one-off races.
|
|
95
|
+
*
|
|
96
|
+
* @param models - Array of model identifiers to compete
|
|
97
|
+
* @param executeWithModel - Function to execute task with a specific model
|
|
98
|
+
* @param config - Optional race configuration
|
|
99
|
+
* @returns Object containing the winning model and its result
|
|
100
|
+
*/
|
|
101
|
+
export declare function competeFreeModels<T>(models: string[], executeWithModel: (model: string) => Promise<T>, config?: RaceConfig): Promise<RaceResult<T>>;
|
|
102
|
+
/**
|
|
103
|
+
* Create a new Racer instance with optional config
|
|
104
|
+
*/
|
|
105
|
+
export declare function createRacer(config?: RaceConfig): FreeModelRacer;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Racer - Zero-Latency Model Competition
|
|
3
|
+
*
|
|
4
|
+
* This module implements Promise.any-based racing between multiple free models,
|
|
5
|
+
* accepting the first valid response. This eliminates waterfall latency
|
|
6
|
+
* and optimizes for zero-cost execution.
|
|
7
|
+
*
|
|
8
|
+
* Key Features:
|
|
9
|
+
* - Promise.any for race condition
|
|
10
|
+
* - AbortController for timeout handling
|
|
11
|
+
* - Progress callbacks for monitoring
|
|
12
|
+
* - Error aggregation for all failures
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Racer class for competing free models
|
|
16
|
+
*/
|
|
17
|
+
export class FreeModelRacer {
|
|
18
|
+
config;
|
|
19
|
+
activeRaces = new Map();
|
|
20
|
+
constructor(config = {}) {
|
|
21
|
+
this.config = {
|
|
22
|
+
timeoutMs: 30000,
|
|
23
|
+
...config
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Compete between multiple free models and return the fastest valid response
|
|
28
|
+
* Uses Promise.any for race condition - fires all requests simultaneously
|
|
29
|
+
* and accepts the first valid response.
|
|
30
|
+
*
|
|
31
|
+
* @param models - Array of model identifiers to compete
|
|
32
|
+
* @param executeWithModel - Function to execute task with a specific model
|
|
33
|
+
* @param raceId - Optional ID for tracking and cancellation
|
|
34
|
+
* @returns Object containing the winning model and its result
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const racer = new FreeModelRacer({ timeoutMs: 15000 });
|
|
39
|
+
*
|
|
40
|
+
* const models = [
|
|
41
|
+
* 'deepseek/deepseek-v3.2',
|
|
42
|
+
* 'zai-coding-plan/glm-4.7-flash',
|
|
43
|
+
* 'openrouter/mistralai/mistral-small-3.1-24b-instruct:free'
|
|
44
|
+
* ];
|
|
45
|
+
*
|
|
46
|
+
* const winner = await racer.race(
|
|
47
|
+
* models,
|
|
48
|
+
* async (model) => {
|
|
49
|
+
* return await client.chat.completions.create({ model, messages });
|
|
50
|
+
* },
|
|
51
|
+
* 'session-123'
|
|
52
|
+
* );
|
|
53
|
+
*
|
|
54
|
+
* console.log(`Fastest: ${winner.model} (${winner.duration}ms)`);
|
|
55
|
+
* return winner.result;
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
async race(models, executeWithModel, raceId = `race-${Date.now()}`) {
|
|
59
|
+
if (models.length === 0) {
|
|
60
|
+
throw new Error('Racer: No models provided for competition');
|
|
61
|
+
}
|
|
62
|
+
console.log(`🏁 Racer: Starting race '${raceId}' with ${models.length} models`);
|
|
63
|
+
// Create abort controller for this race
|
|
64
|
+
const abortController = new AbortController();
|
|
65
|
+
this.activeRaces.set(raceId, abortController);
|
|
66
|
+
const startTime = performance.now();
|
|
67
|
+
try {
|
|
68
|
+
const racePromises = models.map(async (model) => {
|
|
69
|
+
try {
|
|
70
|
+
// Notify started
|
|
71
|
+
this.config.onProgress?.(model, 'started');
|
|
72
|
+
const result = await Promise.race([
|
|
73
|
+
executeWithModel(model),
|
|
74
|
+
this.createTimeoutPromise(this.config.timeoutMs, abortController.signal)
|
|
75
|
+
]);
|
|
76
|
+
const duration = performance.now() - startTime;
|
|
77
|
+
// Notify completed
|
|
78
|
+
this.config.onProgress?.(model, 'completed');
|
|
79
|
+
console.log(`✅ Racer: ${model} completed in ${duration.toFixed(0)}ms`);
|
|
80
|
+
return { model, result, duration };
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const err = error;
|
|
84
|
+
// Notify failed
|
|
85
|
+
this.config.onProgress?.(model, 'failed', err);
|
|
86
|
+
// Check if aborted
|
|
87
|
+
if (err.name === 'AbortError') {
|
|
88
|
+
console.log(`⏹️ Racer: ${model} aborted`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.log(`❌ Racer: ${model} failed - ${err.message}`);
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Model ${model} failed: ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const winner = await Promise.any(racePromises);
|
|
97
|
+
// Abort all other pending requests
|
|
98
|
+
abortController.abort();
|
|
99
|
+
this.activeRaces.delete(raceId);
|
|
100
|
+
console.log(`🏆 Racer: Winner is ${winner.model} (${winner.duration.toFixed(0)}ms)`);
|
|
101
|
+
console.log(` Competed against: ${models.join(', ')}`);
|
|
102
|
+
return winner;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
// Clean up
|
|
106
|
+
this.activeRaces.delete(raceId);
|
|
107
|
+
// AggregateError: all models failed
|
|
108
|
+
const err = error;
|
|
109
|
+
if (err.name === 'AggregateError' && err.errors) {
|
|
110
|
+
const errorDetails = err.errors.map((e) => e.message).join('\n');
|
|
111
|
+
throw new Error(`Racer: All ${models.length} models failed:\n${errorDetails}`);
|
|
112
|
+
}
|
|
113
|
+
// Check if race was externally aborted
|
|
114
|
+
if (err.name === 'AbortError') {
|
|
115
|
+
console.log(`🛑 Racer: Race '${raceId}' externally aborted`);
|
|
116
|
+
throw new Error(`was aborted`);
|
|
117
|
+
}
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Race models from a category config
|
|
123
|
+
*/
|
|
124
|
+
async raceFromCategory(categoryConfig, executeWithModel, raceId) {
|
|
125
|
+
const allModels = [categoryConfig.model, ...categoryConfig.fallback];
|
|
126
|
+
return this.race(allModels, executeWithModel, raceId);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Cancel an active race by ID
|
|
130
|
+
*/
|
|
131
|
+
cancelRace(raceId) {
|
|
132
|
+
const controller = this.activeRaces.get(raceId);
|
|
133
|
+
if (controller) {
|
|
134
|
+
controller.abort();
|
|
135
|
+
this.activeRaces.delete(raceId);
|
|
136
|
+
console.log(`🛑 Racer: Cancelled race '${raceId}'`);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Cancel all active races
|
|
143
|
+
*/
|
|
144
|
+
cancelAllRaces() {
|
|
145
|
+
for (const [raceId, controller] of this.activeRaces.entries()) {
|
|
146
|
+
controller.abort();
|
|
147
|
+
console.log(`🛑 Racer: Cancelled race '${raceId}'`);
|
|
148
|
+
}
|
|
149
|
+
this.activeRaces.clear();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get count of active races
|
|
153
|
+
*/
|
|
154
|
+
getActiveRaceCount() {
|
|
155
|
+
return this.activeRaces.size;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Check if a race is currently active
|
|
159
|
+
*/
|
|
160
|
+
isRaceActive(raceId) {
|
|
161
|
+
return this.activeRaces.has(raceId);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Create a timeout promise that rejects when abort signal is received
|
|
165
|
+
*/
|
|
166
|
+
createTimeoutPromise(timeoutMs, signal) {
|
|
167
|
+
return new Promise((_, reject) => {
|
|
168
|
+
const timeout = setTimeout(() => {
|
|
169
|
+
reject(new Error('Timeout'));
|
|
170
|
+
}, timeoutMs);
|
|
171
|
+
signal.addEventListener('abort', () => {
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
reject(new Error('AbortError'));
|
|
174
|
+
}, { once: true });
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Update race configuration
|
|
179
|
+
*/
|
|
180
|
+
updateConfig(config) {
|
|
181
|
+
this.config = { ...this.config, ...config };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get current race configuration
|
|
185
|
+
*/
|
|
186
|
+
getConfig() {
|
|
187
|
+
return { ...this.config };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Compete between multiple free models (convenience function)
|
|
192
|
+
*
|
|
193
|
+
* This is a stateless version of FreeModelRacer for simple one-off races.
|
|
194
|
+
*
|
|
195
|
+
* @param models - Array of model identifiers to compete
|
|
196
|
+
* @param executeWithModel - Function to execute task with a specific model
|
|
197
|
+
* @param config - Optional race configuration
|
|
198
|
+
* @returns Object containing the winning model and its result
|
|
199
|
+
*/
|
|
200
|
+
export async function competeFreeModels(models, executeWithModel, config) {
|
|
201
|
+
const racer = new FreeModelRacer(config);
|
|
202
|
+
return racer.race(models, executeWithModel);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Create a new Racer instance with optional config
|
|
206
|
+
*/
|
|
207
|
+
export function createRacer(config) {
|
|
208
|
+
return new FreeModelRacer(config);
|
|
209
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Scout - Multi-Provider Free Model Discovery & Benchmark-Based Ranking
|
|
3
|
+
*
|
|
4
|
+
* v0.2.0 Upgrade: Metadata Oracle + Smart Free Tier Detection
|
|
5
|
+
*
|
|
6
|
+
* This module discovers free LLM models from ALL connected providers,
|
|
7
|
+
* aggregates metadata from external APIs, and ranks them by SOTA benchmark performance.
|
|
8
|
+
*/
|
|
9
|
+
import type { ScoutConfig, ScoutResult, ModelCategory } from '../types/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* Scout class for model discovery and ranking
|
|
12
|
+
*/
|
|
13
|
+
export declare class Scout {
|
|
14
|
+
private config;
|
|
15
|
+
private blocklist;
|
|
16
|
+
private antigravityActive;
|
|
17
|
+
private metadataOracle;
|
|
18
|
+
constructor(config?: Partial<ScoutConfig>);
|
|
19
|
+
/**
|
|
20
|
+
* Initialize metadata oracle and adapters
|
|
21
|
+
*/
|
|
22
|
+
private initialize;
|
|
23
|
+
/**
|
|
24
|
+
* PHASE A: Safety Check - Build blocklist of paid/authenticated models
|
|
25
|
+
*
|
|
26
|
+
* Enhanced in v0.2.0:
|
|
27
|
+
* - Check for Antigravity auth presence/configuration
|
|
28
|
+
* - Respect allowAntigravity flag to optionally include Google/Gemini
|
|
29
|
+
* - NOTE: Blocklist is now used for metadata filtering, not model exclusion
|
|
30
|
+
*/
|
|
31
|
+
private buildBlocklist;
|
|
32
|
+
/**
|
|
33
|
+
* Detect active providers from OpenCode configuration
|
|
34
|
+
*/
|
|
35
|
+
private detectActiveProviders;
|
|
36
|
+
/**
|
|
37
|
+
* PHASE B: Fetch and Normalize Models with Metadata Oracle
|
|
38
|
+
*
|
|
39
|
+
* Enhanced in v0.2.0:
|
|
40
|
+
* - Fetch models from provider adapters
|
|
41
|
+
* - Enrich with metadata from MetadataOracle (Models.dev)
|
|
42
|
+
* - Determine free tier based on aggregated metadata + confidence scoring
|
|
43
|
+
* - Use metadata.isFree field instead of hardcoded free tier detection
|
|
44
|
+
*/
|
|
45
|
+
private fetchAllModels;
|
|
46
|
+
/**
|
|
47
|
+
* Categorize a model based on its ID patterns
|
|
48
|
+
*/
|
|
49
|
+
private categorizeModel;
|
|
50
|
+
/**
|
|
51
|
+
* Check if a model is in Elite families
|
|
52
|
+
*/
|
|
53
|
+
private isEliteModel;
|
|
54
|
+
/**
|
|
55
|
+
* Extract parameter count from model ID
|
|
56
|
+
* Looks for patterns like "70b", "8b", "32b" in the ID
|
|
57
|
+
*/
|
|
58
|
+
private extractParams;
|
|
59
|
+
/**
|
|
60
|
+
* Extract date from model ID (if available)
|
|
61
|
+
* Looks for patterns like "2025-01", "v0.1" in the ID
|
|
62
|
+
*/
|
|
63
|
+
private extractDate;
|
|
64
|
+
/**
|
|
65
|
+
* Get provider priority from metadata
|
|
66
|
+
* Higher priority providers are listed first in OpenCode settings
|
|
67
|
+
*/
|
|
68
|
+
private getProviderPriority;
|
|
69
|
+
/**
|
|
70
|
+
* Get elite patterns for a specific category
|
|
71
|
+
*/
|
|
72
|
+
private _getElitePatterns;
|
|
73
|
+
/**
|
|
74
|
+
* PHASE C: Ranking Algorithm - Multi-Provider SOTA Benchmarking
|
|
75
|
+
*
|
|
76
|
+
* Enhanced in v0.2.0:
|
|
77
|
+
* - Priority 1: Metadata confidence score (from Models.dev, etc.)
|
|
78
|
+
* - Priority 2: Elite family membership (SOTA benchmarks)
|
|
79
|
+
* - Priority 3: Provider priority (from metadata provider ranking)
|
|
80
|
+
* - Priority 4: Parameter count (larger > smaller, except for speed)
|
|
81
|
+
* - Priority 5: Release date (newer > older)
|
|
82
|
+
* - Priority 6: Alphabetical order (tiebreaker)
|
|
83
|
+
*/
|
|
84
|
+
private rankModelsByBenchmark;
|
|
85
|
+
/**
|
|
86
|
+
* PHASE D: Functional Categorization
|
|
87
|
+
*
|
|
88
|
+
* Sorts models into functional categories based on ID patterns:
|
|
89
|
+
* - coding: IDs with "coder", "code", "function"
|
|
90
|
+
* - reasoning: IDs with "r1", "reasoning", "cot", "qwq"
|
|
91
|
+
* - speed: IDs with "flash", "distill", "nano", "lite"
|
|
92
|
+
* - multimodal: IDs with "vl", "vision"
|
|
93
|
+
* - writing: General purpose models not in other categories
|
|
94
|
+
*/
|
|
95
|
+
private categorizeModels;
|
|
96
|
+
/**
|
|
97
|
+
* Generate category configuration from ranked models
|
|
98
|
+
*/
|
|
99
|
+
private generateCategoryConfig;
|
|
100
|
+
/**
|
|
101
|
+
* Main discovery and ranking method
|
|
102
|
+
* Returns categorized and ranked free models from ALL active providers
|
|
103
|
+
* with metadata enrichment from external APIs
|
|
104
|
+
*/
|
|
105
|
+
discover(): Promise<Record<ModelCategory, ScoutResult>>;
|
|
106
|
+
/**
|
|
107
|
+
* Print summary of results
|
|
108
|
+
*/
|
|
109
|
+
printSummary(results: Record<ModelCategory, ScoutResult>): void;
|
|
110
|
+
/**
|
|
111
|
+
* Get configuration info
|
|
112
|
+
*/
|
|
113
|
+
getConfiguration(): Promise<{
|
|
114
|
+
antigravityActive: boolean;
|
|
115
|
+
allowAntigravity: boolean;
|
|
116
|
+
blocklist: string[];
|
|
117
|
+
hasMetadataOracle: boolean;
|
|
118
|
+
providersAvailable?: string[];
|
|
119
|
+
}>;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Create a new Scout instance with optional config
|
|
123
|
+
*/
|
|
124
|
+
export declare function createScout(config?: Partial<ScoutConfig>): Scout;
|