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,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Adapters for OpenCode Free Fleet v0.2.0
|
|
3
|
+
*
|
|
4
|
+
* Each adapter knows how to fetch models and identify free tier models
|
|
5
|
+
* for a specific provider.
|
|
6
|
+
*
|
|
7
|
+
* v0.2.1 - Build Repair: Removed dynamic imports
|
|
8
|
+
*/
|
|
9
|
+
import { ELITE_FAMILIES } from '../../types/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* OpenRouter Adapter
|
|
12
|
+
* All models with pricing.prompt === "0" AND pricing.completion === "0" are free
|
|
13
|
+
*/
|
|
14
|
+
class OpenRouterAdapter {
|
|
15
|
+
providerId = 'openrouter';
|
|
16
|
+
providerName = 'OpenRouter';
|
|
17
|
+
async fetchModels() {
|
|
18
|
+
console.log('🔗 OpenRouter: Fetching models...');
|
|
19
|
+
const response = await fetch('https://openrouter.ai/api/v1/models', {
|
|
20
|
+
headers: { 'Accept': 'application/json' }
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`OpenRouter API error: ${response.status} ${response.statusText}`);
|
|
24
|
+
}
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
const models = data.data || [];
|
|
27
|
+
console.log(`✓ OpenRouter: Found ${models.length} models`);
|
|
28
|
+
return models.map((model) => ({
|
|
29
|
+
id: model.id,
|
|
30
|
+
name: model.name,
|
|
31
|
+
description: model.description,
|
|
32
|
+
context_length: model.context_length,
|
|
33
|
+
architecture: model.architecture,
|
|
34
|
+
pricing: model.pricing,
|
|
35
|
+
top_provider: model.top_provider
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
isFreeModel(model) {
|
|
39
|
+
// OpenRouter: Free if both prompt and completion are "0"
|
|
40
|
+
const isFreePrompt = model.pricing?.prompt === '0' || model.pricing?.prompt === '0.0';
|
|
41
|
+
const isFreeCompletion = model.pricing?.completion === '0' || model.pricing?.completion === '0.0';
|
|
42
|
+
return isFreePrompt && isFreeCompletion;
|
|
43
|
+
}
|
|
44
|
+
normalizeModel(model) {
|
|
45
|
+
const isFree = this.isFreeModel(model);
|
|
46
|
+
// Determine category
|
|
47
|
+
const id = model.id.toLowerCase();
|
|
48
|
+
let category = 'writing';
|
|
49
|
+
if (id.includes('coder') || id.includes('code') || id.includes('function')) {
|
|
50
|
+
category = 'coding';
|
|
51
|
+
}
|
|
52
|
+
else if (id.includes('r1') || id.includes('reasoning') || id.includes('cot') || id.includes('qwq')) {
|
|
53
|
+
category = 'reasoning';
|
|
54
|
+
}
|
|
55
|
+
else if (id.includes('flash') || id.includes('distill') || id.includes('nano') || id.includes('lite')) {
|
|
56
|
+
category = 'speed';
|
|
57
|
+
}
|
|
58
|
+
else if (id.includes('vl') || id.includes('vision') || id.includes('molmo')) {
|
|
59
|
+
category = 'multimodal';
|
|
60
|
+
}
|
|
61
|
+
// Determine if elite
|
|
62
|
+
const elitePatterns = ELITE_FAMILIES[category] || [];
|
|
63
|
+
const isElite = elitePatterns.some((pattern) => id.includes(pattern.toLowerCase()));
|
|
64
|
+
return {
|
|
65
|
+
id: model.id,
|
|
66
|
+
provider: this.providerId,
|
|
67
|
+
name: model.name || model.id.split('/')[1],
|
|
68
|
+
description: model.description,
|
|
69
|
+
contextLength: model.context_length,
|
|
70
|
+
maxOutputTokens: model.max_output_tokens || model.architecture?.['context_length'],
|
|
71
|
+
pricing: {
|
|
72
|
+
prompt: model.pricing?.prompt || '0',
|
|
73
|
+
completion: model.pricing?.completion || '0',
|
|
74
|
+
request: model.pricing?.request || '0'
|
|
75
|
+
},
|
|
76
|
+
isFree,
|
|
77
|
+
isElite,
|
|
78
|
+
category
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Groq Adapter
|
|
84
|
+
* Currently (as of 2026), most Groq models are free
|
|
85
|
+
*/
|
|
86
|
+
class GroqAdapter {
|
|
87
|
+
providerId = 'groq';
|
|
88
|
+
providerName = 'Groq';
|
|
89
|
+
async fetchModels() {
|
|
90
|
+
console.log('🚀 Groq: Fetching models...');
|
|
91
|
+
const response = await fetch('https://api.groq.com/openai/v1/models', {
|
|
92
|
+
headers: { 'Accept': 'application/json' }
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new Error(`Groq API error: ${response.status} ${response.statusText}`);
|
|
96
|
+
}
|
|
97
|
+
const data = await response.json();
|
|
98
|
+
const models = data.data || [];
|
|
99
|
+
console.log(`✓ Groq: Found ${models.length} models`);
|
|
100
|
+
return models.map((model) => ({
|
|
101
|
+
id: model.id,
|
|
102
|
+
name: model.name,
|
|
103
|
+
description: model.description,
|
|
104
|
+
context_length: model.context_length,
|
|
105
|
+
pricing: model.pricing || {},
|
|
106
|
+
top_provider: null
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
isFreeModel(model) {
|
|
110
|
+
// Groq: Assume all models are free (current policy)
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
normalizeModel(model) {
|
|
114
|
+
const isFree = this.isFreeModel(model);
|
|
115
|
+
// Determine category
|
|
116
|
+
const id = model.id.toLowerCase();
|
|
117
|
+
let category = 'writing';
|
|
118
|
+
if (id.includes('coder') || id.includes('code') || id.includes('function')) {
|
|
119
|
+
category = 'coding';
|
|
120
|
+
}
|
|
121
|
+
else if (id.includes('r1') || id.includes('reasoning') || id.includes('cot') || id.includes('qwq')) {
|
|
122
|
+
category = 'reasoning';
|
|
123
|
+
}
|
|
124
|
+
else if (id.includes('flash') || id.includes('distill') || id.includes('nano') || id.includes('lite')) {
|
|
125
|
+
category = 'speed';
|
|
126
|
+
}
|
|
127
|
+
else if (id.includes('vl') || id.includes('vision') || id.includes('molmo')) {
|
|
128
|
+
category = 'multimodal';
|
|
129
|
+
}
|
|
130
|
+
// Determine if elite
|
|
131
|
+
const elitePatterns = ELITE_FAMILIES[category] || [];
|
|
132
|
+
const isElite = elitePatterns.some((pattern) => id.includes(pattern.toLowerCase()));
|
|
133
|
+
return {
|
|
134
|
+
id: model.id,
|
|
135
|
+
provider: this.providerId,
|
|
136
|
+
name: model.name || model.id.split('/')[1],
|
|
137
|
+
description: model.description,
|
|
138
|
+
contextLength: model.context_length,
|
|
139
|
+
maxOutputTokens: model.max_output_tokens || model.architecture?.['context_length'],
|
|
140
|
+
pricing: {
|
|
141
|
+
prompt: model.pricing?.prompt || '0',
|
|
142
|
+
completion: model.pricing?.completion || '0',
|
|
143
|
+
request: model.pricing?.request || '0'
|
|
144
|
+
},
|
|
145
|
+
isFree,
|
|
146
|
+
isElite,
|
|
147
|
+
category
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Cerebras Adapter
|
|
153
|
+
* All models are currently free
|
|
154
|
+
*/
|
|
155
|
+
class CerebrasAdapter {
|
|
156
|
+
providerId = 'cerebras';
|
|
157
|
+
providerName = 'Cerebras';
|
|
158
|
+
async fetchModels() {
|
|
159
|
+
console.log('⚡ Cerebras: Fetching models...');
|
|
160
|
+
const response = await fetch('https://api.cerebras.ai/v1/models', {
|
|
161
|
+
headers: { 'Accept': 'application/json' }
|
|
162
|
+
});
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
throw new Error(`Cerebras API error: ${response.status} ${response.statusText}`);
|
|
165
|
+
}
|
|
166
|
+
const data = await response.json();
|
|
167
|
+
const models = data.models || [];
|
|
168
|
+
console.log(`✓ Cerebras: Found ${models.length} models`);
|
|
169
|
+
return models.map((model) => ({
|
|
170
|
+
id: model.id,
|
|
171
|
+
name: model.name,
|
|
172
|
+
description: model.description,
|
|
173
|
+
context_length: model.context_length || model.context_window,
|
|
174
|
+
pricing: model.pricing || {},
|
|
175
|
+
top_provider: null
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
isFreeModel(model) {
|
|
179
|
+
// Cerebras: Assume all models are free (current policy)
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
normalizeModel(model) {
|
|
183
|
+
const isFree = this.isFreeModel(model);
|
|
184
|
+
// Determine category
|
|
185
|
+
const id = model.id.toLowerCase();
|
|
186
|
+
let category = 'writing';
|
|
187
|
+
if (id.includes('coder') || id.includes('code') || id.includes('function')) {
|
|
188
|
+
category = 'coding';
|
|
189
|
+
}
|
|
190
|
+
else if (id.includes('r1') || id.includes('reasoning') || id.includes('cot') || id.includes('qwq')) {
|
|
191
|
+
category = 'reasoning';
|
|
192
|
+
}
|
|
193
|
+
else if (id.includes('flash') || id.includes('distill') || id.includes('nano') || id.includes('lite')) {
|
|
194
|
+
category = 'speed';
|
|
195
|
+
}
|
|
196
|
+
else if (id.includes('vl') || id.includes('vision') || id.includes('molmo')) {
|
|
197
|
+
category = 'multimodal';
|
|
198
|
+
}
|
|
199
|
+
// Determine if elite
|
|
200
|
+
const elitePatterns = ELITE_FAMILIES[category] || [];
|
|
201
|
+
const isElite = elitePatterns.some((pattern) => id.includes(pattern.toLowerCase()));
|
|
202
|
+
return {
|
|
203
|
+
id: model.id,
|
|
204
|
+
provider: this.providerId,
|
|
205
|
+
name: model.name || model.id.split('/')[1],
|
|
206
|
+
description: model.description,
|
|
207
|
+
contextLength: model.context_length || model.context_window,
|
|
208
|
+
maxOutputTokens: model.max_output_tokens || model.architecture?.['context_length'],
|
|
209
|
+
pricing: {
|
|
210
|
+
prompt: model.pricing?.prompt || '0',
|
|
211
|
+
completion: model.pricing?.completion || '0',
|
|
212
|
+
request: model.pricing?.request || '0'
|
|
213
|
+
},
|
|
214
|
+
isFree,
|
|
215
|
+
isElite,
|
|
216
|
+
category
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Google Adapter
|
|
222
|
+
* Free models are limited (Gemini Flash, Nano)
|
|
223
|
+
*/
|
|
224
|
+
class GoogleAdapter {
|
|
225
|
+
providerId = 'google';
|
|
226
|
+
providerName = 'Google';
|
|
227
|
+
async fetchModels() {
|
|
228
|
+
console.log('🔵 Google: Fetching models...');
|
|
229
|
+
// Note: This requires OAuth flow
|
|
230
|
+
// For now, return a placeholder list
|
|
231
|
+
const freeModels = [
|
|
232
|
+
{
|
|
233
|
+
id: 'gemini-1.5-flash',
|
|
234
|
+
name: 'Gemini 1.5 Flash',
|
|
235
|
+
description: 'Fast, lightweight multimodal model (Free Tier)',
|
|
236
|
+
context_length: 28000,
|
|
237
|
+
pricing: { prompt: '0', completion: '0', request: '0' }
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'gemini-1.5-flash-8b',
|
|
241
|
+
name: 'Gemini 1.5 Flash-8B',
|
|
242
|
+
description: 'Even smaller and faster (Free Tier)',
|
|
243
|
+
context_length: 1000000,
|
|
244
|
+
pricing: { prompt: '0', completion: '0', request: '0' }
|
|
245
|
+
}
|
|
246
|
+
];
|
|
247
|
+
console.log(`✓ Google: Found ${freeModels.length} free models (cached)`);
|
|
248
|
+
return freeModels;
|
|
249
|
+
}
|
|
250
|
+
isFreeModel(model) {
|
|
251
|
+
// Google: Check if explicitly marked as free (pricing === "0")
|
|
252
|
+
const isFreePrompt = model.pricing?.prompt === '0' || model.pricing?.prompt === '0.0';
|
|
253
|
+
const isFreeCompletion = model.pricing?.completion === '0' || model.pricing?.completion === '0.0';
|
|
254
|
+
return isFreePrompt && isFreeCompletion;
|
|
255
|
+
}
|
|
256
|
+
normalizeModel(model) {
|
|
257
|
+
const isFree = this.isFreeModel(model);
|
|
258
|
+
// Determine category
|
|
259
|
+
const id = model.id.toLowerCase();
|
|
260
|
+
let category = 'writing';
|
|
261
|
+
if (id.includes('coder') || id.includes('code') || id.includes('function')) {
|
|
262
|
+
category = 'coding';
|
|
263
|
+
}
|
|
264
|
+
else if (id.includes('r1') || id.includes('reasoning') || id.includes('cot') || id.includes('qwq')) {
|
|
265
|
+
category = 'reasoning';
|
|
266
|
+
}
|
|
267
|
+
else if (id.includes('flash') || id.includes('distill') || id.includes('nano') || id.includes('lite')) {
|
|
268
|
+
category = 'speed';
|
|
269
|
+
}
|
|
270
|
+
else if (id.includes('vl') || id.includes('vision') || id.includes('molmo')) {
|
|
271
|
+
category = 'multimodal';
|
|
272
|
+
}
|
|
273
|
+
// Determine if elite
|
|
274
|
+
const elitePatterns = ELITE_FAMILIES[category] || [];
|
|
275
|
+
const isElite = elitePatterns.some((pattern) => id.includes(pattern.toLowerCase()));
|
|
276
|
+
return {
|
|
277
|
+
id: model.id,
|
|
278
|
+
provider: this.providerId,
|
|
279
|
+
name: model.name || model.id.split('/')[1],
|
|
280
|
+
description: model.description,
|
|
281
|
+
contextLength: model.context_length,
|
|
282
|
+
maxOutputTokens: model.max_output_tokens || model.architecture?.['context_length'],
|
|
283
|
+
pricing: {
|
|
284
|
+
prompt: model.pricing?.prompt || '0',
|
|
285
|
+
completion: model.pricing?.completion || '0',
|
|
286
|
+
request: model.pricing?.request || '0'
|
|
287
|
+
},
|
|
288
|
+
isFree,
|
|
289
|
+
isElite,
|
|
290
|
+
category
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* DeepSeek Adapter
|
|
296
|
+
* DeepSeek-V3.2 has 5M free tokens
|
|
297
|
+
*/
|
|
298
|
+
class DeepSeekAdapter {
|
|
299
|
+
providerId = 'deepseek';
|
|
300
|
+
providerName = 'DeepSeek';
|
|
301
|
+
async fetchModels() {
|
|
302
|
+
console.log('🟣 DeepSeek: Fetching models...');
|
|
303
|
+
const response = await fetch('https://api.deepseek.com/v1/models', {
|
|
304
|
+
headers: { 'Accept': 'application/json' }
|
|
305
|
+
});
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
throw new Error(`DeepSeek API error: ${response.status} ${response.statusText}`);
|
|
308
|
+
}
|
|
309
|
+
const data = await response.json();
|
|
310
|
+
const models = data.data || [];
|
|
311
|
+
console.log(`✓ DeepSeek: Found ${models.length} models`);
|
|
312
|
+
return models.map((model) => ({
|
|
313
|
+
id: model.id,
|
|
314
|
+
name: model.name,
|
|
315
|
+
description: model.description,
|
|
316
|
+
context_length: model.context_length || model.max_context_tokens,
|
|
317
|
+
architecture: {
|
|
318
|
+
modality: model.modality,
|
|
319
|
+
tokenizer: model.tokenizer || 'unknown'
|
|
320
|
+
},
|
|
321
|
+
pricing: model.pricing || {},
|
|
322
|
+
top_provider: null
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
isFreeModel(model) {
|
|
326
|
+
// DeepSeek: Check if marked as free or known free model
|
|
327
|
+
const knownFreeModels = ['deepseek-chat', 'deepseek-coder', 'deepseek-v3', 'deepseek-r1'];
|
|
328
|
+
const modelId = model.id.toLowerCase();
|
|
329
|
+
return knownFreeModels.some(freeModel => modelId.includes(freeModel));
|
|
330
|
+
}
|
|
331
|
+
normalizeModel(model) {
|
|
332
|
+
const isFree = this.isFreeModel(model);
|
|
333
|
+
// Determine category
|
|
334
|
+
const id = model.id.toLowerCase();
|
|
335
|
+
let category = 'writing';
|
|
336
|
+
if (id.includes('coder') || id.includes('code') || id.includes('function')) {
|
|
337
|
+
category = 'coding';
|
|
338
|
+
}
|
|
339
|
+
else if (id.includes('r1') || id.includes('reasoning') || id.includes('cot') || id.includes('qwq')) {
|
|
340
|
+
category = 'reasoning';
|
|
341
|
+
}
|
|
342
|
+
else if (id.includes('flash') || id.includes('distill') || id.includes('nano') || id.includes('lite')) {
|
|
343
|
+
category = 'speed';
|
|
344
|
+
}
|
|
345
|
+
else if (id.includes('vl') || id.includes('vision') || id.includes('molmo')) {
|
|
346
|
+
category = 'multimodal';
|
|
347
|
+
}
|
|
348
|
+
// Determine if elite
|
|
349
|
+
const elitePatterns = ELITE_FAMILIES[category] || [];
|
|
350
|
+
const isElite = elitePatterns.some((pattern) => id.includes(pattern.toLowerCase()));
|
|
351
|
+
return {
|
|
352
|
+
id: model.id,
|
|
353
|
+
provider: this.providerId,
|
|
354
|
+
name: model.name || model.id.split('/')[1],
|
|
355
|
+
description: model.description,
|
|
356
|
+
contextLength: model.context_length || model.max_context_tokens,
|
|
357
|
+
maxOutputTokens: model.max_output_tokens || model.architecture?.['context_length'],
|
|
358
|
+
pricing: {
|
|
359
|
+
prompt: model.pricing?.prompt || '0',
|
|
360
|
+
completion: model.pricing?.completion || '0',
|
|
361
|
+
request: model.pricing?.request || '0'
|
|
362
|
+
},
|
|
363
|
+
isFree,
|
|
364
|
+
isElite,
|
|
365
|
+
category
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* ModelScope Adapter
|
|
371
|
+
* Some free serverless inference models
|
|
372
|
+
*/
|
|
373
|
+
class ModelScopeAdapter {
|
|
374
|
+
providerId = 'modelscope';
|
|
375
|
+
providerName = 'ModelScope';
|
|
376
|
+
async fetchModels() {
|
|
377
|
+
console.log('🔬 ModelScope: Fetching free serverless models...');
|
|
378
|
+
// Note: This requires authentication
|
|
379
|
+
// For now, return a placeholder list
|
|
380
|
+
const freeModels = [
|
|
381
|
+
{
|
|
382
|
+
id: 'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo',
|
|
383
|
+
name: 'Meta Llama 3.1 70B',
|
|
384
|
+
description: 'Llama 3.1 with 128K context (Serverless Free)',
|
|
385
|
+
context_length: 128000,
|
|
386
|
+
pricing: { prompt: '0', completion: '0', request: '0' }
|
|
387
|
+
}
|
|
388
|
+
];
|
|
389
|
+
console.log(`✓ ModelScope: Found ${freeModels.length} free models (cached)`);
|
|
390
|
+
return freeModels;
|
|
391
|
+
}
|
|
392
|
+
isFreeModel(model) {
|
|
393
|
+
// ModelScope: Check if marked as serverless free
|
|
394
|
+
return model.serverless_free === true || model.pricing?.prompt === '0';
|
|
395
|
+
}
|
|
396
|
+
normalizeModel(model) {
|
|
397
|
+
const isFree = this.isFreeModel(model);
|
|
398
|
+
// Determine category
|
|
399
|
+
const id = model.id.toLowerCase();
|
|
400
|
+
let category = 'writing';
|
|
401
|
+
if (id.includes('coder') || id.includes('code') || id.includes('function')) {
|
|
402
|
+
category = 'coding';
|
|
403
|
+
}
|
|
404
|
+
else if (id.includes('r1') || id.includes('reasoning') || id.includes('cot') || id.includes('qwq')) {
|
|
405
|
+
category = 'reasoning';
|
|
406
|
+
}
|
|
407
|
+
else if (id.includes('flash') || id.includes('distill') || id.includes('nano') || id.includes('lite')) {
|
|
408
|
+
category = 'speed';
|
|
409
|
+
}
|
|
410
|
+
else if (id.includes('vl') || id.includes('vision') || id.includes('molmo')) {
|
|
411
|
+
category = 'multimodal';
|
|
412
|
+
}
|
|
413
|
+
// Determine if elite
|
|
414
|
+
const elitePatterns = ELITE_FAMILIES[category] || [];
|
|
415
|
+
const isElite = elitePatterns.some((pattern) => id.includes(pattern.toLowerCase()));
|
|
416
|
+
return {
|
|
417
|
+
id: model.id,
|
|
418
|
+
provider: this.providerId,
|
|
419
|
+
name: model.name || model.id.split('/')[1],
|
|
420
|
+
description: model.description,
|
|
421
|
+
contextLength: model.context_length,
|
|
422
|
+
maxOutputTokens: model.max_output_tokens || model.architecture?.['context_length'],
|
|
423
|
+
pricing: {
|
|
424
|
+
prompt: model.pricing?.prompt || '0',
|
|
425
|
+
completion: model.pricing?.completion || '0',
|
|
426
|
+
request: model.pricing?.request || '0'
|
|
427
|
+
},
|
|
428
|
+
isFree,
|
|
429
|
+
isElite,
|
|
430
|
+
category
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Hugging Face Adapter
|
|
436
|
+
* Some free serverless inference models
|
|
437
|
+
*/
|
|
438
|
+
class HuggingFaceAdapter {
|
|
439
|
+
providerId = 'huggingface';
|
|
440
|
+
providerName = 'Hugging Face';
|
|
441
|
+
async fetchModels() {
|
|
442
|
+
console.log('🤗 Hugging Face: Fetching free serverless models...');
|
|
443
|
+
// Note: This requires complex filtering
|
|
444
|
+
// For now, return a placeholder list
|
|
445
|
+
const freeModels = [
|
|
446
|
+
{
|
|
447
|
+
id: 'Qwen/Qwen2.5-72B-Instruct-Turbo',
|
|
448
|
+
name: 'Qwen 2.5 72B',
|
|
449
|
+
description: 'Qwen 2.5 with 128K context (Serverless Free)',
|
|
450
|
+
context_length: 128000,
|
|
451
|
+
pricing: { prompt: '0', completion: '0', request: '0' }
|
|
452
|
+
}
|
|
453
|
+
];
|
|
454
|
+
console.log(`✓ Hugging Face: Found ${freeModels.length} free models (cached)`);
|
|
455
|
+
return freeModels;
|
|
456
|
+
}
|
|
457
|
+
isFreeModel(model) {
|
|
458
|
+
// Hugging Face: Check if marked as serverless free
|
|
459
|
+
return model.serverless_free === true || model.pricing?.prompt === '0';
|
|
460
|
+
}
|
|
461
|
+
normalizeModel(model) {
|
|
462
|
+
const isFree = this.isFreeModel(model);
|
|
463
|
+
// Determine category
|
|
464
|
+
const id = model.id.toLowerCase();
|
|
465
|
+
let category = 'writing';
|
|
466
|
+
if (id.includes('coder') || id.includes('code') || id.includes('function')) {
|
|
467
|
+
category = 'coding';
|
|
468
|
+
}
|
|
469
|
+
else if (id.includes('r1') || id.includes('reasoning') || id.includes('cot') || id.includes('qwq')) {
|
|
470
|
+
category = 'reasoning';
|
|
471
|
+
}
|
|
472
|
+
else if (id.includes('flash') || id.includes('distill') || id.includes('nano') || id.includes('lite')) {
|
|
473
|
+
category = 'speed';
|
|
474
|
+
}
|
|
475
|
+
else if (id.includes('vl') || id.includes('vision') || id.includes('molmo')) {
|
|
476
|
+
category = 'multimodal';
|
|
477
|
+
}
|
|
478
|
+
// Determine if elite
|
|
479
|
+
const elitePatterns = ELITE_FAMILIES[category] || [];
|
|
480
|
+
const isElite = elitePatterns.some((pattern) => id.includes(pattern.toLowerCase()));
|
|
481
|
+
return {
|
|
482
|
+
id: model.id,
|
|
483
|
+
provider: this.providerId,
|
|
484
|
+
name: model.name || model.id.split('/')[1],
|
|
485
|
+
description: model.description,
|
|
486
|
+
contextLength: model.context_length,
|
|
487
|
+
maxOutputTokens: model.max_output_tokens || model.architecture?.['context_length'],
|
|
488
|
+
pricing: {
|
|
489
|
+
prompt: model.pricing?.prompt || '0',
|
|
490
|
+
completion: model.pricing?.completion || '0',
|
|
491
|
+
request: model.pricing?.request || '0'
|
|
492
|
+
},
|
|
493
|
+
isFree,
|
|
494
|
+
isElite,
|
|
495
|
+
category
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Base Adapter implementation
|
|
501
|
+
*/
|
|
502
|
+
class BaseAdapter {
|
|
503
|
+
providerId;
|
|
504
|
+
providerName;
|
|
505
|
+
constructor(providerId, providerName) {
|
|
506
|
+
this.providerId = providerId;
|
|
507
|
+
this.providerName = providerName;
|
|
508
|
+
}
|
|
509
|
+
async fetchModels() {
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
isFreeModel(model) {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
normalizeModel(model) {
|
|
516
|
+
return {
|
|
517
|
+
id: model.id,
|
|
518
|
+
provider: this.providerId,
|
|
519
|
+
name: model.name,
|
|
520
|
+
pricing: { prompt: '0', completion: '0', request: '0' },
|
|
521
|
+
isFree: false,
|
|
522
|
+
isElite: false,
|
|
523
|
+
category: 'writing'
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Create adapter instance by provider ID
|
|
529
|
+
*/
|
|
530
|
+
export function createAdapter(providerId) {
|
|
531
|
+
const adapters = {
|
|
532
|
+
'openrouter': () => new OpenRouterAdapter(),
|
|
533
|
+
'groq': () => new GroqAdapter(),
|
|
534
|
+
'cerebras': () => new CerebrasAdapter(),
|
|
535
|
+
'google': () => new GoogleAdapter(),
|
|
536
|
+
'deepseek': () => new DeepSeekAdapter(),
|
|
537
|
+
'modelscope': () => new ModelScopeAdapter(),
|
|
538
|
+
'huggingface': () => new HuggingFaceAdapter()
|
|
539
|
+
};
|
|
540
|
+
const adapterFactory = adapters[providerId];
|
|
541
|
+
if (!adapterFactory) {
|
|
542
|
+
console.warn(`⚠️ No adapter found for provider: ${providerId}, using base adapter`);
|
|
543
|
+
return new BaseAdapter(providerId, providerId);
|
|
544
|
+
}
|
|
545
|
+
return adapterFactory();
|
|
546
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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 declare const CONFIRMED_FREE_MODELS: Set<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Oracle result with confidence score
|
|
13
|
+
*/
|
|
14
|
+
export interface ModelMetadata {
|
|
15
|
+
id: string;
|
|
16
|
+
provider: string;
|
|
17
|
+
name: string;
|
|
18
|
+
isFree: boolean;
|
|
19
|
+
confidence: number;
|
|
20
|
+
reason: string;
|
|
21
|
+
lastVerified?: string;
|
|
22
|
+
pricing?: {
|
|
23
|
+
prompt: string;
|
|
24
|
+
completion: string;
|
|
25
|
+
request: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Provider adapter for metadata lookup
|
|
30
|
+
*/
|
|
31
|
+
export interface MetadataAdapter {
|
|
32
|
+
providerId: string;
|
|
33
|
+
providerName: string;
|
|
34
|
+
/**
|
|
35
|
+
* Fetch metadata for a specific model
|
|
36
|
+
*/
|
|
37
|
+
fetchModelMetadata(modelId: string): Promise<ModelMetadata>;
|
|
38
|
+
/**
|
|
39
|
+
* Batch fetch multiple models
|
|
40
|
+
*/
|
|
41
|
+
fetchModelsMetadata(modelIds?: string[]): Promise<ModelMetadata[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Check if this adapter is available
|
|
44
|
+
*/
|
|
45
|
+
isAvailable(): boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Unified metadata oracle
|
|
49
|
+
* Aggregates data from multiple metadata sources
|
|
50
|
+
*/
|
|
51
|
+
export declare class MetadataOracle {
|
|
52
|
+
private adapters;
|
|
53
|
+
constructor();
|
|
54
|
+
/**
|
|
55
|
+
* Initialize all metadata adapters
|
|
56
|
+
*/
|
|
57
|
+
private _initializeAdapters;
|
|
58
|
+
/**
|
|
59
|
+
* Check which adapters are available
|
|
60
|
+
*/
|
|
61
|
+
getAvailableAdapters(): string[];
|
|
62
|
+
/**
|
|
63
|
+
* Fetch metadata for a specific model from all available sources
|
|
64
|
+
* This is the main method Scout should call for free tier detection
|
|
65
|
+
*/
|
|
66
|
+
fetchModelMetadata(modelId: string): Promise<ModelMetadata>;
|
|
67
|
+
/**
|
|
68
|
+
* Batch fetch metadata for multiple models
|
|
69
|
+
*/
|
|
70
|
+
fetchModelsMetadata(modelIds: string[]): Promise<ModelMetadata[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Get list of all confirmed free models
|
|
73
|
+
*/
|
|
74
|
+
getConfirmedFreeModels(): Set<string>;
|
|
75
|
+
/**
|
|
76
|
+
* Add a manually confirmed free model
|
|
77
|
+
* This allows updating of static knowledge base
|
|
78
|
+
*/
|
|
79
|
+
addConfirmedFreeModel(modelId: string): void;
|
|
80
|
+
/**
|
|
81
|
+
* Remove a model from confirmed free list
|
|
82
|
+
*/
|
|
83
|
+
removeConfirmedFreeModel(modelId: string): void;
|
|
84
|
+
}
|