opencode-free-fleet 0.1.0 ā 0.2.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 +321 -215
- 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 +19 -2
- package/src/version.ts +2 -2
|
@@ -0,0 +1,503 @@
|
|
|
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 * as fs from 'fs/promises';
|
|
10
|
+
/**
|
|
11
|
+
* Metadata Oracle
|
|
12
|
+
* Aggregates data from multiple metadata sources
|
|
13
|
+
*/
|
|
14
|
+
import { MetadataOracle } from './oracle.js';
|
|
15
|
+
/**
|
|
16
|
+
* Default scout configuration
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_CONFIG = {
|
|
19
|
+
antigravityPath: `${process.env.HOME || ''}/.config/opencode/antigravity-accounts.json`,
|
|
20
|
+
opencodeConfigPath: `${process.env.HOME || ''}/.config/opencode/oh-my-opencode.json`,
|
|
21
|
+
allowAntigravity: false // Default to BLOCK Google/Gemini
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Scout class for model discovery and ranking
|
|
25
|
+
*/
|
|
26
|
+
export class Scout {
|
|
27
|
+
config;
|
|
28
|
+
blocklist = new Set();
|
|
29
|
+
antigravityActive = false;
|
|
30
|
+
metadataOracle;
|
|
31
|
+
constructor(config = {}) {
|
|
32
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
33
|
+
this.metadataOracle = new MetadataOracle();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Initialize metadata oracle and adapters
|
|
37
|
+
*/
|
|
38
|
+
async initialize() {
|
|
39
|
+
// Import Metadata Oracle
|
|
40
|
+
console.log('š® Metadata Oracle: Initializing adapters...');
|
|
41
|
+
// Note: Adapters are now implemented directly in src/core/adapters/index.ts
|
|
42
|
+
// The Oracle uses them as needed
|
|
43
|
+
// Check antigravity-auth plugin presence
|
|
44
|
+
try {
|
|
45
|
+
const antigravityPath = `${process.env.HOME || ''}/.config/opencode/plugins/opencode-antigravity-auth`;
|
|
46
|
+
await fs.access(antigravityPath);
|
|
47
|
+
this.antigravityActive = true;
|
|
48
|
+
console.log('ā Scout: Antigravity auth plugin detected');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
this.antigravityActive = false;
|
|
52
|
+
console.log('ā¹ļø Scout: Could not read antigravity-auth config (may not be configured)');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* PHASE A: Safety Check - Build blocklist of paid/authenticated models
|
|
57
|
+
*
|
|
58
|
+
* Enhanced in v0.2.0:
|
|
59
|
+
* - Check for Antigravity auth presence/configuration
|
|
60
|
+
* - Respect allowAntigravity flag to optionally include Google/Gemini
|
|
61
|
+
* - NOTE: Blocklist is now used for metadata filtering, not model exclusion
|
|
62
|
+
*/
|
|
63
|
+
async buildBlocklist() {
|
|
64
|
+
const blocklist = new Set();
|
|
65
|
+
// Antigravity auth is already checked in initialize()
|
|
66
|
+
if (this.antigravityActive && !this.config.allowAntigravity) {
|
|
67
|
+
console.log('š« Scout: Blocking Google/Gemini from Free Fleet (allowAntigravity=false)');
|
|
68
|
+
blocklist.add('google');
|
|
69
|
+
blocklist.add('gemini');
|
|
70
|
+
blocklist.add('opencode');
|
|
71
|
+
}
|
|
72
|
+
this.blocklist = blocklist;
|
|
73
|
+
if (blocklist.size > 0) {
|
|
74
|
+
console.log(`š« Scout: Blocklist - ${Array.from(blocklist).join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log('ā Scout: No active blocklist (allowAntigravity=true or no Antigravity)');
|
|
78
|
+
}
|
|
79
|
+
return blocklist;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Detect active providers from OpenCode configuration
|
|
83
|
+
*/
|
|
84
|
+
async detectActiveProviders() {
|
|
85
|
+
console.log('\nš Scout: Detecting active OpenCode providers...');
|
|
86
|
+
const providers = [];
|
|
87
|
+
const adapters = new Map();
|
|
88
|
+
const errors = [];
|
|
89
|
+
let uniqueProviders = [];
|
|
90
|
+
try {
|
|
91
|
+
if (!this.config.opencodeConfigPath) {
|
|
92
|
+
throw new Error('OpenCode config path not set');
|
|
93
|
+
}
|
|
94
|
+
const configContent = await fs.readFile(this.config.opencodeConfigPath, 'utf-8');
|
|
95
|
+
const config = JSON.parse(configContent);
|
|
96
|
+
// Check for configured providers
|
|
97
|
+
if (config.providers) {
|
|
98
|
+
providers.push(...Object.keys(config.providers));
|
|
99
|
+
}
|
|
100
|
+
// Also check categories for provider patterns
|
|
101
|
+
if (config.categories) {
|
|
102
|
+
const categoryModels = Object.values(config.categories)
|
|
103
|
+
.map((cat) => cat.model)
|
|
104
|
+
.concat(...Object.values(config.categories).map((cat) => cat.fallback || []))
|
|
105
|
+
.map((model) => model.split('/')[0]);
|
|
106
|
+
for (const provider of categoryModels) {
|
|
107
|
+
if (!providers.includes(provider)) {
|
|
108
|
+
providers.push(provider);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Remove duplicates
|
|
113
|
+
uniqueProviders = [...new Set(providers)];
|
|
114
|
+
console.log(`š Scout: Detected providers: ${uniqueProviders.join(', ')}`);
|
|
115
|
+
// Create adapters for each provider
|
|
116
|
+
const { createAdapter } = await import('./adapters/index.js');
|
|
117
|
+
for (const providerId of uniqueProviders) {
|
|
118
|
+
try {
|
|
119
|
+
const adapter = createAdapter(providerId);
|
|
120
|
+
adapters.set(providerId, adapter);
|
|
121
|
+
console.log(`ā Scout: Created adapter for ${providerId}`);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const err = error;
|
|
125
|
+
errors.push(`Adapter for ${providerId} failed: ${err.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const err = error;
|
|
131
|
+
errors.push(`Failed to read OpenCode config: ${err.message}`);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
providers: uniqueProviders,
|
|
135
|
+
adapters,
|
|
136
|
+
errors
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* PHASE B: Fetch and Normalize Models with Metadata Oracle
|
|
141
|
+
*
|
|
142
|
+
* Enhanced in v0.2.0:
|
|
143
|
+
* - Fetch models from provider adapters
|
|
144
|
+
* - Enrich with metadata from MetadataOracle (Models.dev)
|
|
145
|
+
* - Determine free tier based on aggregated metadata + confidence scoring
|
|
146
|
+
* - Use metadata.isFree field instead of hardcoded free tier detection
|
|
147
|
+
*/
|
|
148
|
+
async fetchAllModels() {
|
|
149
|
+
console.log('\nš” Scout: Fetching models with metadata enrichment...\n');
|
|
150
|
+
const { providers, adapters, errors } = await this.detectActiveProviders();
|
|
151
|
+
if (errors.length > 0) {
|
|
152
|
+
console.error('\nā ļø Scout: Provider detection had errors:');
|
|
153
|
+
errors.forEach(err => console.error(` - ${err}`));
|
|
154
|
+
}
|
|
155
|
+
if (adapters.size === 0) {
|
|
156
|
+
console.warn('\nā ļø Scout: No active providers found');
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
// Fetch from all active providers
|
|
160
|
+
const providerModels = new Map();
|
|
161
|
+
for (const [providerId, adapter] of adapters.entries()) {
|
|
162
|
+
try {
|
|
163
|
+
console.log(`\nš” Scout: Fetching from ${providerId}...`);
|
|
164
|
+
const models = await adapter.fetchModels();
|
|
165
|
+
// Enrich with metadata from MetadataOracle
|
|
166
|
+
const enrichedModels = [];
|
|
167
|
+
for (const providerModel of models) {
|
|
168
|
+
const modelMetadata = await this.metadataOracle.fetchModelMetadata(providerModel.id);
|
|
169
|
+
const isFree = modelMetadata.isFree;
|
|
170
|
+
const isElite = await this.isEliteModel(providerModel.id);
|
|
171
|
+
enrichedModels.push({
|
|
172
|
+
id: providerModel.id,
|
|
173
|
+
provider: providerId,
|
|
174
|
+
name: providerModel.name || providerModel.id.split('/')[1],
|
|
175
|
+
description: providerModel.description,
|
|
176
|
+
contextLength: providerModel.context_length,
|
|
177
|
+
maxOutputTokens: providerModel.max_output_tokens,
|
|
178
|
+
pricing: {
|
|
179
|
+
prompt: modelMetadata.pricing?.prompt || '0',
|
|
180
|
+
completion: modelMetadata.pricing?.completion || '0',
|
|
181
|
+
request: modelMetadata.pricing?.request || '0'
|
|
182
|
+
},
|
|
183
|
+
isFree,
|
|
184
|
+
isElite,
|
|
185
|
+
category: this.categorizeModel(providerModel.id, providerModel),
|
|
186
|
+
confidence: modelMetadata.confidence || 0.5
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// Apply blocklist filter (for metadata filtering)
|
|
190
|
+
const blocklist = await this.buildBlocklist();
|
|
191
|
+
const filteredModels = enrichedModels.filter(model => {
|
|
192
|
+
const isBlocked = blocklist.has(model.provider);
|
|
193
|
+
return !isBlocked;
|
|
194
|
+
});
|
|
195
|
+
providerModels.set(providerId, filteredModels);
|
|
196
|
+
console.log(`\nā Scout: ${providerId} - ${filteredModels.length} models, ${filteredModels.filter(m => m.isFree).length} free\n`);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
console.error(`\nā Scout: Failed to fetch from ${providerId}: ${error}`);
|
|
200
|
+
// Add error models as empty array to continue
|
|
201
|
+
providerModels.set(providerId, []);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Flatten all enriched models into single array
|
|
205
|
+
const allModels = [];
|
|
206
|
+
for (const models of providerModels.values()) {
|
|
207
|
+
allModels.push(...models);
|
|
208
|
+
}
|
|
209
|
+
console.log(`\nā Scout: Total models discovered: ${allModels.length}`);
|
|
210
|
+
return allModels;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Categorize a model based on its ID patterns
|
|
214
|
+
*/
|
|
215
|
+
categorizeModel(modelId, providerModel) {
|
|
216
|
+
const id = modelId.toLowerCase();
|
|
217
|
+
let category = 'writing';
|
|
218
|
+
if (id.includes('coder') || id.includes('code') || id.includes('function')) {
|
|
219
|
+
category = 'coding';
|
|
220
|
+
}
|
|
221
|
+
else if (id.includes('r1') || id.includes('reasoning') || id.includes('cot') || id.includes('qwq')) {
|
|
222
|
+
category = 'reasoning';
|
|
223
|
+
}
|
|
224
|
+
else if (id.includes('flash') || id.includes('distill') || id.includes('nano') || id.includes('lite')) {
|
|
225
|
+
category = 'speed';
|
|
226
|
+
}
|
|
227
|
+
else if (id.includes('vl') || id.includes('vision') || id.includes('molmo')) {
|
|
228
|
+
category = 'multimodal';
|
|
229
|
+
}
|
|
230
|
+
return category;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Check if a model is in Elite families
|
|
234
|
+
*/
|
|
235
|
+
async isEliteModel(modelId) {
|
|
236
|
+
const { ELITE_FAMILIES } = await import('../types/index.js');
|
|
237
|
+
const elitePatterns = ELITE_FAMILIES.reasoning || []; // Default to reasoning for async
|
|
238
|
+
const id = modelId.toLowerCase();
|
|
239
|
+
return elitePatterns.some((pattern) => id.includes(pattern.toLowerCase()));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Extract parameter count from model ID
|
|
243
|
+
* Looks for patterns like "70b", "8b", "32b" in the ID
|
|
244
|
+
*/
|
|
245
|
+
extractParams(id) {
|
|
246
|
+
const match = id.match(/(\d+)b/i);
|
|
247
|
+
return match ? parseInt(match[1]) : 0;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Extract date from model ID (if available)
|
|
251
|
+
* Looks for patterns like "2025-01", "v0.1" in the ID
|
|
252
|
+
*/
|
|
253
|
+
extractDate(id) {
|
|
254
|
+
// Simplified version - in production, we'd use actual metadata
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Get provider priority from metadata
|
|
259
|
+
* Higher priority providers are listed first in OpenCode settings
|
|
260
|
+
*/
|
|
261
|
+
getProviderPriority(providerId) {
|
|
262
|
+
const priorityMap = {
|
|
263
|
+
'models.dev': 1,
|
|
264
|
+
'openrouter': 2,
|
|
265
|
+
'groq': 4,
|
|
266
|
+
'cerebras': 5,
|
|
267
|
+
'google': 6,
|
|
268
|
+
'deepseek': 7,
|
|
269
|
+
'modelscope': 8,
|
|
270
|
+
'huggingface': 9
|
|
271
|
+
};
|
|
272
|
+
return priorityMap[providerId] || 99;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get elite patterns for a specific category
|
|
276
|
+
*/
|
|
277
|
+
async _getElitePatterns(category) {
|
|
278
|
+
const { ELITE_FAMILIES } = await import('../types/index.js');
|
|
279
|
+
return ELITE_FAMILIES[category] || [];
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* PHASE C: Ranking Algorithm - Multi-Provider SOTA Benchmarking
|
|
283
|
+
*
|
|
284
|
+
* Enhanced in v0.2.0:
|
|
285
|
+
* - Priority 1: Metadata confidence score (from Models.dev, etc.)
|
|
286
|
+
* - Priority 2: Elite family membership (SOTA benchmarks)
|
|
287
|
+
* - Priority 3: Provider priority (from metadata provider ranking)
|
|
288
|
+
* - Priority 4: Parameter count (larger > smaller, except for speed)
|
|
289
|
+
* - Priority 5: Release date (newer > older)
|
|
290
|
+
* - Priority 6: Alphabetical order (tiebreaker)
|
|
291
|
+
*/
|
|
292
|
+
async rankModelsByBenchmark(models, category) {
|
|
293
|
+
// Need to do async sort carefully since Array.sort doesn't support async
|
|
294
|
+
// So we pre-calculate values that need async
|
|
295
|
+
const enrichedForSort = await Promise.all(models.map(async (m) => {
|
|
296
|
+
return {
|
|
297
|
+
...m,
|
|
298
|
+
isElite: await this.isEliteModel(m.id)
|
|
299
|
+
};
|
|
300
|
+
}));
|
|
301
|
+
return enrichedForSort.sort((a, b) => {
|
|
302
|
+
// Priority 1: Metadata confidence score (higher is better)
|
|
303
|
+
const aConfidence = a.confidence || 0;
|
|
304
|
+
const bConfidence = b.confidence || 0;
|
|
305
|
+
if (aConfidence !== bConfidence) {
|
|
306
|
+
return bConfidence - aConfidence; // Higher confidence first
|
|
307
|
+
}
|
|
308
|
+
// Priority 2: Elite family membership (SOTA benchmarks)
|
|
309
|
+
// Already resolved in isElite
|
|
310
|
+
if (a.isElite && !b.isElite)
|
|
311
|
+
return -1;
|
|
312
|
+
if (!a.isElite && b.isElite)
|
|
313
|
+
return 1;
|
|
314
|
+
// Priority 3: Provider priority (from metadata)
|
|
315
|
+
const aPriority = this.getProviderPriority(a.provider);
|
|
316
|
+
const bPriority = this.getProviderPriority(b.provider);
|
|
317
|
+
if (aPriority !== bPriority) {
|
|
318
|
+
return aPriority - bPriority; // Lower number = higher priority
|
|
319
|
+
}
|
|
320
|
+
// Priority 4: Parameter count (larger > smaller, except for speed)
|
|
321
|
+
const aParams = this.extractParams(a.id);
|
|
322
|
+
const bParams = this.extractParams(b.id);
|
|
323
|
+
if (category === 'speed') {
|
|
324
|
+
if (aParams > 0 && bParams > 0 && aParams !== bParams) {
|
|
325
|
+
return aParams - bParams; // Smaller first
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
if (aParams > 0 && bParams > 0 && aParams !== bParams) {
|
|
330
|
+
return bParams - aParams; // Larger first
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Priority 5: Release date (newer > older)
|
|
334
|
+
const aDate = this.extractDate(a.id);
|
|
335
|
+
const bDate = this.extractDate(b.id);
|
|
336
|
+
if (aDate && bDate && aDate !== bDate) {
|
|
337
|
+
return aDate > bDate ? -1 : 1;
|
|
338
|
+
}
|
|
339
|
+
// Priority 6: Alphabetical order (tiebreaker)
|
|
340
|
+
return a.id.localeCompare(b.id);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* PHASE D: Functional Categorization
|
|
345
|
+
*
|
|
346
|
+
* Sorts models into functional categories based on ID patterns:
|
|
347
|
+
* - coding: IDs with "coder", "code", "function"
|
|
348
|
+
* - reasoning: IDs with "r1", "reasoning", "cot", "qwq"
|
|
349
|
+
* - speed: IDs with "flash", "distill", "nano", "lite"
|
|
350
|
+
* - multimodal: IDs with "vl", "vision"
|
|
351
|
+
* - writing: General purpose models not in other categories
|
|
352
|
+
*/
|
|
353
|
+
categorizeModels(models) {
|
|
354
|
+
const categories = {
|
|
355
|
+
coding: [],
|
|
356
|
+
reasoning: [],
|
|
357
|
+
speed: [],
|
|
358
|
+
multimodal: [],
|
|
359
|
+
writing: []
|
|
360
|
+
};
|
|
361
|
+
for (const model of models) {
|
|
362
|
+
const id = model.id.toLowerCase();
|
|
363
|
+
let categorized = false;
|
|
364
|
+
// Check each category
|
|
365
|
+
if (id.includes('coder') || id.includes('code') || id.includes('function')) {
|
|
366
|
+
categories.coding.push(model);
|
|
367
|
+
categorized = true;
|
|
368
|
+
}
|
|
369
|
+
if (id.includes('r1') || id.includes('reasoning') || id.includes('cot') || id.includes('qwq')) {
|
|
370
|
+
categories.reasoning.push(model);
|
|
371
|
+
categorized = true;
|
|
372
|
+
}
|
|
373
|
+
if (id.includes('flash') || id.includes('distill') || id.includes('nano') || id.includes('lite')) {
|
|
374
|
+
categories.speed.push(model);
|
|
375
|
+
categorized = true;
|
|
376
|
+
}
|
|
377
|
+
if (id.includes('vl') || id.includes('vision') || id.includes('molmo')) {
|
|
378
|
+
categories.multimodal.push(model);
|
|
379
|
+
categorized = true;
|
|
380
|
+
}
|
|
381
|
+
// Add to writing if not categorized elsewhere (general purpose)
|
|
382
|
+
if (!categorized) {
|
|
383
|
+
categories.writing.push(model);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return categories;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Generate category configuration from ranked models
|
|
390
|
+
*/
|
|
391
|
+
generateCategoryConfig(category, rankedModels) {
|
|
392
|
+
const topModels = rankedModels.slice(0, Math.min(5, rankedModels.length));
|
|
393
|
+
const modelIds = topModels.map((m) => `${m.provider}/${m.id}`);
|
|
394
|
+
return {
|
|
395
|
+
model: modelIds[0],
|
|
396
|
+
fallback: modelIds.slice(1),
|
|
397
|
+
description: `Auto-ranked by Free Fleet v0.2.0 (Metadata Oracle) - ${category.toUpperCase()} category`
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Main discovery and ranking method
|
|
402
|
+
* Returns categorized and ranked free models from ALL active providers
|
|
403
|
+
* with metadata enrichment from external APIs
|
|
404
|
+
*/
|
|
405
|
+
async discover() {
|
|
406
|
+
console.log('\nš¤ Free Fleet v0.2.0 (Metadata Oracle) - Starting omni-provider discovery...\n');
|
|
407
|
+
// Initialize metadata oracle and antigravity check
|
|
408
|
+
await this.initialize();
|
|
409
|
+
// Detect active providers
|
|
410
|
+
const detectionResult = await this.detectActiveProviders();
|
|
411
|
+
if (detectionResult.errors.length > 0) {
|
|
412
|
+
console.error('\nā ļø Scout: Provider detection had errors:');
|
|
413
|
+
detectionResult.errors.forEach(err => console.error(` - ${err}`));
|
|
414
|
+
}
|
|
415
|
+
if (detectionResult.providers.length === 0) {
|
|
416
|
+
console.warn('\nā ļø Scout: No active providers found');
|
|
417
|
+
// Don't throw, return empty result to allow graceful failure
|
|
418
|
+
// throw new Error('No active providers detected. Please configure at least one provider in OpenCode.');
|
|
419
|
+
}
|
|
420
|
+
// PHASE B: Fetch and Normalize with Metadata Oracle
|
|
421
|
+
const allModels = await this.fetchAllModels();
|
|
422
|
+
console.log(`\nā Scout: Total models discovered: ${allModels.length}`);
|
|
423
|
+
console.log(`\nā Scout: Free models: ${allModels.filter(m => m.isFree).length}\n`);
|
|
424
|
+
// PHASE C + D: Categorize and Rank
|
|
425
|
+
console.log('\nš Scout: Categorizing and ranking models with metadata...\n');
|
|
426
|
+
const categorizedModels = this.categorizeModels(allModels);
|
|
427
|
+
for (const [category, models] of Object.entries(categorizedModels)) {
|
|
428
|
+
console.log(` ${category}: ${models.length} models (${models.filter(m => m.isFree).length} free)`);
|
|
429
|
+
}
|
|
430
|
+
const results = {};
|
|
431
|
+
for (const [category, models] of Object.entries(categorizedModels)) {
|
|
432
|
+
const cat = category;
|
|
433
|
+
// Apply multi-provider benchmark ranking
|
|
434
|
+
const rankedModels = await this.rankModelsByBenchmark(models, cat);
|
|
435
|
+
// Identify elite models
|
|
436
|
+
const eliteModels = rankedModels.filter((model) => model.isElite);
|
|
437
|
+
results[cat] = {
|
|
438
|
+
category: cat,
|
|
439
|
+
models,
|
|
440
|
+
rankedModels,
|
|
441
|
+
eliteModels
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return results;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Print summary of results
|
|
448
|
+
*/
|
|
449
|
+
printSummary(results) {
|
|
450
|
+
console.log('\n' + '='.repeat(60));
|
|
451
|
+
console.log('š Free Fleet v0.2.0 (Metadata Oracle) Discovery Results\n');
|
|
452
|
+
for (const [category, result] of Object.entries(results)) {
|
|
453
|
+
console.log(`\nš ${category.toUpperCase()} (top ${Math.min(5, result.rankedModels.length)}):`);
|
|
454
|
+
result.rankedModels.slice(0, 5).forEach((model, i) => {
|
|
455
|
+
const isElite = result.eliteModels.includes(model);
|
|
456
|
+
const providerTag = model.provider ? `[${model.provider.toUpperCase()}]` : '';
|
|
457
|
+
const confidence = model.confidence || 0;
|
|
458
|
+
const confidenceBadge = confidence >= 1.0 ? 'ā
' : (confidence >= 0.7 ? 'ā ļø' : 'ā');
|
|
459
|
+
console.log(` ${i + 1}. ${providerTag}${model.id}${isElite ? ' ā ELITE' : ''} [${confidence.toFixed(2)}] ${model.category?.toUpperCase()}`);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
console.log('\n' + '='.repeat(60) + '\n');
|
|
463
|
+
// Print free tier breakdown by provider
|
|
464
|
+
const providerStats = new Map();
|
|
465
|
+
for (const [category, result] of Object.entries(results)) {
|
|
466
|
+
for (const model of result.models) {
|
|
467
|
+
const provider = model.provider || 'unknown';
|
|
468
|
+
if (!providerStats.has(provider)) {
|
|
469
|
+
providerStats.set(provider, { total: 0, free: 0 });
|
|
470
|
+
}
|
|
471
|
+
const stats = providerStats.get(provider);
|
|
472
|
+
// Add check for undefined
|
|
473
|
+
if (stats) {
|
|
474
|
+
stats.total++;
|
|
475
|
+
if (model.isFree)
|
|
476
|
+
stats.free++;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
console.log('\nš Free Models by Provider (Metadata Oracle):');
|
|
481
|
+
for (const [provider, stats] of providerStats.entries()) {
|
|
482
|
+
console.log(` ${provider}: ${stats.free}/${stats.total} (${((stats.free / stats.total) * 100).toFixed(1)}%) free`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Get configuration info
|
|
487
|
+
*/
|
|
488
|
+
async getConfiguration() {
|
|
489
|
+
return {
|
|
490
|
+
antigravityActive: this.antigravityActive,
|
|
491
|
+
allowAntigravity: this.config.allowAntigravity || false,
|
|
492
|
+
blocklist: Array.from(this.blocklist),
|
|
493
|
+
hasMetadataOracle: true,
|
|
494
|
+
providersAvailable: Object.keys((await this.detectActiveProviders()).adapters)
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Create a new Scout instance with optional config
|
|
500
|
+
*/
|
|
501
|
+
export function createScout(config) {
|
|
502
|
+
return new Scout(config);
|
|
503
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Free Fleet
|
|
3
|
+
*
|
|
4
|
+
* Economic Load Balancing and Zero-Cost Model Discovery for OpenCode
|
|
5
|
+
*
|
|
6
|
+
* This plugin automatically discovers, ranks, and competes free LLM models
|
|
7
|
+
* based on SOTA benchmark performance, enabling zero-cost, zero-latency
|
|
8
|
+
* execution for OpenCode agents.
|
|
9
|
+
*
|
|
10
|
+
* @version 0.2.2
|
|
11
|
+
* @author Phorde
|
|
12
|
+
*/
|
|
13
|
+
export { VERSION, BUILD_DATE } from './version.js';
|
|
14
|
+
export * from './types/index.js';
|
|
15
|
+
export { Scout, createScout } from './core/scout.js';
|
|
16
|
+
export { FreeModelRacer, competeFreeModels, createRacer } from './core/racer.js';
|
|
17
|
+
import type { Plugin } from '@opencode-ai/plugin';
|
|
18
|
+
/**
|
|
19
|
+
* Main plugin function
|
|
20
|
+
*
|
|
21
|
+
* Initializes Free Fleet plugin and returns hooks for OpenCode integration.
|
|
22
|
+
*
|
|
23
|
+
* @param ctx - Plugin context provided by OpenCode
|
|
24
|
+
* @returns Plugin hooks
|
|
25
|
+
*/
|
|
26
|
+
export declare const FreeFleetPlugin: Plugin;
|
|
27
|
+
export default FreeFleetPlugin;
|