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/dist/index.js ADDED
@@ -0,0 +1,332 @@
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.1
11
+ * @author Phorde
12
+ */
13
+ // Export version
14
+ export { VERSION, BUILD_DATE } from './version.js';
15
+ // Export types
16
+ export * from './types/index.js';
17
+ // Export core modules
18
+ export { Scout, createScout } from './core/scout.js';
19
+ export { FreeModelRacer, competeFreeModels, createRacer } from './core/racer.js';
20
+ // Import fs
21
+ import * as fs from 'fs/promises';
22
+ import * as path from 'path';
23
+ /**
24
+ * Main plugin function
25
+ *
26
+ * Initializes Free Fleet plugin and returns hooks for OpenCode integration.
27
+ *
28
+ * @param ctx - Plugin context provided by OpenCode
29
+ * @returns Plugin hooks
30
+ */
31
+ export const FreeFleetPlugin = async (ctx) => {
32
+ const { client, directory } = ctx;
33
+ console.log('\nšŸ¤– OpenCode Free Fleet - Initializing...');
34
+ console.log(` Version: 0.2.1`);
35
+ console.log(` Directory: ${directory}\n`);
36
+ // Get config directory
37
+ const configDir = process.env.HOME ? `${process.env.HOME}/.config/opencode` : directory;
38
+ // Free models cache path
39
+ const freeModelsCachePath = path.join(configDir, 'free-models.json');
40
+ // Initialize Scout instance
41
+ const { createScout } = await import('./core/scout.js');
42
+ const scout = createScout({
43
+ antigravityPath: path.join(configDir, 'antigravity-accounts.json'),
44
+ opencodeConfigPath: path.join(configDir, 'oh-my-opencode.json')
45
+ });
46
+ /**
47
+ * Run discovery and cache results
48
+ */
49
+ const runDiscovery = async () => {
50
+ try {
51
+ console.log('šŸ” Free Fleet: Starting model discovery...');
52
+ const results = await scout.discover();
53
+ scout.printSummary(results);
54
+ // Cache results to free-models.json
55
+ const cacheData = {
56
+ version: '0.2.1',
57
+ timestamp: new Date().toISOString(),
58
+ categories: Object.fromEntries(Object.entries(results).map(([cat, res]) => [
59
+ cat,
60
+ {
61
+ totalModels: res.models.length,
62
+ eliteModels: res.eliteModels.map(m => m.id),
63
+ topModels: res.rankedModels.slice(0, 5).map(m => ({
64
+ id: m.id,
65
+ isElite: res.eliteModels.includes(m)
66
+ }))
67
+ }
68
+ ]))
69
+ };
70
+ await fs.writeFile(freeModelsCachePath, JSON.stringify(cacheData, null, 2));
71
+ console.log(`āœ“ Free Fleet: Cached results to ${freeModelsCachePath}`);
72
+ // Log structured event
73
+ await client.app.log?.({
74
+ service: 'free-fleet',
75
+ level: 'info',
76
+ message: 'Model discovery completed',
77
+ extra: {
78
+ categories: Object.keys(results).length,
79
+ totalModels: Object.values(results).reduce((sum, r) => sum + r.models.length, 0),
80
+ cachedTo: freeModelsCachePath
81
+ }
82
+ });
83
+ }
84
+ catch (error) {
85
+ console.error('āŒ Free Fleet: Discovery failed', error);
86
+ await client.app.log?.({
87
+ service: 'free-fleet',
88
+ level: 'error',
89
+ message: 'Model discovery failed',
90
+ extra: { error: String(error) }
91
+ });
92
+ }
93
+ };
94
+ // Check if cache exists
95
+ let cacheExists = false;
96
+ try {
97
+ await fs.access(freeModelsCachePath);
98
+ cacheExists = true;
99
+ console.log(`āœ“ Free Fleet: Found cache at ${freeModelsCachePath}`);
100
+ }
101
+ catch {
102
+ console.log(`ā„¹ļø Free Fleet: No cache found, will run discovery on startup`);
103
+ }
104
+ return {
105
+ /**
106
+ * onStart Hook
107
+ * Runs when plugin is loaded
108
+ */
109
+ onStart: async () => {
110
+ console.log('āœ… Free Fleet: Plugin started\n');
111
+ await client.app.log?.({
112
+ service: 'free-fleet',
113
+ level: 'info',
114
+ message: 'Free Fleet plugin initialized',
115
+ extra: { version: '0.2.1', cacheExists }
116
+ });
117
+ // Run discovery if cache doesn't exist
118
+ if (!cacheExists) {
119
+ console.log('šŸ”„ Free Fleet: No cache found, running initial discovery...');
120
+ await runDiscovery();
121
+ }
122
+ },
123
+ /**
124
+ * Custom Tool: free_fleet_scout
125
+ * Triggers manual model discovery update
126
+ */
127
+ tool: {
128
+ 'free_fleet_scout': {
129
+ description: 'Discover and rank free LLM models from OpenRouter API',
130
+ args: {
131
+ category: {
132
+ type: 'string',
133
+ description: 'Optional category filter (coding, reasoning, speed, multimodal, writing)',
134
+ optional: true
135
+ },
136
+ top: {
137
+ type: 'number',
138
+ description: 'Number of top models to display (default: 5)',
139
+ optional: true
140
+ }
141
+ },
142
+ execute: async (args, context) => {
143
+ console.log('šŸ” free_fleet_scout: Starting manual discovery...');
144
+ const results = await scout.discover();
145
+ if (args.category) {
146
+ const category = args.category;
147
+ if (results[category]) {
148
+ const result = results[category];
149
+ const top = args.top || 5;
150
+ console.log(`\nšŸ“ˆ ${category.toUpperCase()} (top ${top}):`);
151
+ result.rankedModels.slice(0, top).forEach((model, i) => {
152
+ const isElite = result.eliteModels.includes(model);
153
+ console.log(` ${i + 1}. ${model.id}${isElite ? ' ⭐ ELITE' : ''}`);
154
+ });
155
+ await client.app.log?.({
156
+ service: 'free-fleet',
157
+ level: 'info',
158
+ message: `Discovery for category '${category}' completed`,
159
+ extra: {
160
+ category,
161
+ totalModels: result.models.length,
162
+ eliteModels: result.eliteModels.length,
163
+ topDisplayed: top
164
+ }
165
+ });
166
+ return {
167
+ success: true,
168
+ category,
169
+ totalModels: result.models.length,
170
+ eliteModels: result.eliteModels.length,
171
+ topModels: result.rankedModels.slice(0, top).map(m => m.id)
172
+ };
173
+ }
174
+ else {
175
+ await client.app.log?.({
176
+ service: 'free-fleet',
177
+ level: 'warn',
178
+ message: `Category '${args.category}' not found`,
179
+ extra: {
180
+ availableCategories: Object.keys(results),
181
+ requestedCategory: args.category
182
+ }
183
+ });
184
+ return {
185
+ success: false,
186
+ error: `Category '${args.category}' not found. Available: ${Object.keys(results).join(', ')}`
187
+ };
188
+ }
189
+ }
190
+ else {
191
+ scout.printSummary(results);
192
+ await client.app.log?.({
193
+ service: 'free-fleet',
194
+ level: 'info',
195
+ message: 'Full discovery completed',
196
+ extra: {
197
+ categories: Object.keys(results).length,
198
+ totalModels: Object.values(results).reduce((sum, r) => sum + r.models.length, 0)
199
+ }
200
+ });
201
+ return {
202
+ success: true,
203
+ categories: Object.keys(results).length,
204
+ totalModels: Object.values(results).reduce((sum, r) => sum + r.models.length, 0)
205
+ };
206
+ }
207
+ }
208
+ },
209
+ /**
210
+ * Custom Tool: free_fleet_router
211
+ * Accepts { category, prompt }, runs race, returns result
212
+ */
213
+ 'free_fleet_router': {
214
+ description: 'Race between free models and return fastest response',
215
+ args: {
216
+ category: {
217
+ type: 'string',
218
+ description: 'Category to use (coding, reasoning, speed, multimodal, writing)',
219
+ required: true
220
+ },
221
+ prompt: {
222
+ type: 'string',
223
+ description: 'Prompt to send to each model',
224
+ required: true
225
+ },
226
+ timeoutMs: {
227
+ type: 'number',
228
+ description: 'Timeout in milliseconds (default: 30000)',
229
+ optional: true
230
+ }
231
+ },
232
+ execute: async (args, context) => {
233
+ console.log(`šŸ free_fleet_router: Starting race for category '${args.category}'`);
234
+ try {
235
+ // Read cached models
236
+ const cacheContent = await fs.readFile(freeModelsCachePath, 'utf-8');
237
+ const cache = JSON.parse(cacheContent);
238
+ const categoryKey = args.category;
239
+ const categoryData = cache.categories[categoryKey];
240
+ if (!categoryData) {
241
+ await client.app.log?.({
242
+ service: 'free-fleet',
243
+ level: 'warn',
244
+ message: `Category '${args.category}' not found in cache`,
245
+ extra: { availableCategories: Object.keys(cache.categories) }
246
+ });
247
+ return {
248
+ success: false,
249
+ error: `Category '${args.category}' not found in cache. Run free_fleet_scout first.`
250
+ };
251
+ }
252
+ // Get models for this category
253
+ const models = categoryData.topModels.map((m) => `openrouter/${m.id}`);
254
+ console.log(`šŸ“‹ free_fleet_router: Competing with ${models.length} models:`);
255
+ models.forEach((m, i) => {
256
+ const modelData = categoryData.topModels.find((d) => d.id === m.replace('openrouter/', ''));
257
+ console.log(` ${i + 1}. ${m}${modelData?.isElite ? ' ⭐ ELITE' : ''}`);
258
+ });
259
+ // Import racer
260
+ const { FreeModelRacer } = await import('./core/racer.js');
261
+ const racer = new FreeModelRacer({
262
+ timeoutMs: args.timeoutMs,
263
+ onProgress: (model, status) => {
264
+ console.log(` ${model}: ${status}`);
265
+ }
266
+ });
267
+ // Execute race with simulated model execution
268
+ // In production, this would use the OpenCode client
269
+ const winner = await racer.race(models, async (model) => {
270
+ // This is where you'd execute with OpenCode client
271
+ // For now, simulate with a mock
272
+ const delay = Math.random() * 3000 + 500;
273
+ await new Promise(resolve => setTimeout(resolve, delay));
274
+ return {
275
+ model,
276
+ response: `Response from ${model} for: ${args.prompt.substring(0, 50)}...`,
277
+ delay
278
+ };
279
+ }, `router-${Date.now()}`);
280
+ console.log(`šŸ† free_fleet_router: Winner - ${winner.model} (${winner.duration.toFixed(0)}ms)`);
281
+ await client.app.log?.({
282
+ service: 'free-fleet',
283
+ level: 'info',
284
+ message: `Race completed for category '${args.category}'`,
285
+ extra: {
286
+ winner: winner.model,
287
+ duration: winner.duration,
288
+ modelsCompeted: models.length
289
+ }
290
+ });
291
+ return {
292
+ success: true,
293
+ winner: winner.model,
294
+ duration: winner.duration,
295
+ result: winner.result,
296
+ category: args.category
297
+ };
298
+ }
299
+ catch (error) {
300
+ const err = error;
301
+ // Check if cache doesn't exist
302
+ if (err.message.includes('ENOENT')) {
303
+ await client.app.log?.({
304
+ service: 'free-fleet',
305
+ level: 'warn',
306
+ message: 'No cache found',
307
+ extra: { error: err.message }
308
+ });
309
+ return {
310
+ success: false,
311
+ error: 'No cache found. Run free_fleet_scout first to build the model cache.'
312
+ };
313
+ }
314
+ console.error('āŒ free_fleet_router: Failed', error);
315
+ await client.app.log?.({
316
+ service: 'free-fleet',
317
+ level: 'error',
318
+ message: 'Race failed',
319
+ extra: { error: err.message, category: args.category }
320
+ });
321
+ return {
322
+ success: false,
323
+ error: err.message
324
+ };
325
+ }
326
+ }
327
+ }
328
+ }
329
+ };
330
+ };
331
+ // Export plugin as default
332
+ export default FreeFleetPlugin;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Type definitions for OpenCode Free Fleet v0.2.0
3
+ *
4
+ * Simplified types to fix compilation errors
5
+ */
6
+ /**
7
+ * Unified free model interface (provider-agnostic)
8
+ */
9
+ export interface FreeModel {
10
+ id: string;
11
+ provider: string;
12
+ name: string;
13
+ description?: string;
14
+ contextLength?: number;
15
+ maxOutputTokens?: number;
16
+ pricing: {
17
+ prompt: string;
18
+ completion: string;
19
+ request: string;
20
+ };
21
+ isFree: boolean;
22
+ isElite: boolean;
23
+ category: ModelCategory;
24
+ confidence?: number;
25
+ }
26
+ /**
27
+ * Model category identifiers
28
+ */
29
+ export type ModelCategory = 'coding' | 'reasoning' | 'speed' | 'multimodal' | 'writing';
30
+ /**
31
+ * Provider model interface
32
+ */
33
+ export interface ProviderModel {
34
+ id: string;
35
+ name: string;
36
+ description?: string;
37
+ context_length?: number;
38
+ max_output_tokens?: number;
39
+ pricing?: {
40
+ prompt: string;
41
+ completion: string;
42
+ request: string;
43
+ prompt_price?: string;
44
+ top_provider?: any;
45
+ };
46
+ architecture?: any;
47
+ top_provider?: any;
48
+ serverless_free?: boolean;
49
+ max_context_tokens?: number;
50
+ modality?: string;
51
+ tokenizer?: string;
52
+ context_window?: number;
53
+ }
54
+ /**
55
+ * Provider Adapter interface
56
+ */
57
+ export interface ProviderAdapter {
58
+ readonly providerId: string;
59
+ readonly providerName: string;
60
+ fetchModels(): Promise<ProviderModel[]>;
61
+ isFreeModel(model: ProviderModel): boolean;
62
+ normalizeModel(model: ProviderModel): FreeModel;
63
+ }
64
+ /**
65
+ * Scout configuration
66
+ */
67
+ export interface ScoutConfig {
68
+ antigravityPath?: string;
69
+ opencodeConfigPath?: string;
70
+ allowAntigravity?: boolean;
71
+ }
72
+ /**
73
+ * Scout result with ranked models
74
+ */
75
+ export interface ScoutResult {
76
+ category: ModelCategory;
77
+ models: FreeModel[];
78
+ rankedModels: FreeModel[];
79
+ eliteModels: FreeModel[];
80
+ }
81
+ /**
82
+ * Race execution result
83
+ */
84
+ export interface RaceResult<T = unknown> {
85
+ model: string;
86
+ result: T;
87
+ duration: number;
88
+ }
89
+ /**
90
+ * Race configuration
91
+ */
92
+ export interface RaceConfig {
93
+ timeoutMs?: number;
94
+ abortController?: AbortController;
95
+ onProgress?: (model: string, status: 'started' | 'completed' | 'failed', error?: Error) => void;
96
+ }
97
+ /**
98
+ * Provider discovery result
99
+ */
100
+ export interface ActiveProvidersResult {
101
+ providers: string[];
102
+ adapters: Map<string, ProviderAdapter>;
103
+ errors: string[];
104
+ }
105
+ /**
106
+ * Category configuration with model selection and fallback chain
107
+ */
108
+ export interface CategoryConfig {
109
+ model: string;
110
+ fallback: string[];
111
+ description: string;
112
+ }
113
+ /**
114
+ * Plugin context provided by OpenCode
115
+ */
116
+ export interface PluginContext {
117
+ project: any;
118
+ client: any;
119
+ $: any;
120
+ directory: string;
121
+ worktree: string;
122
+ }
123
+ /**
124
+ * Plugin hook definitions
125
+ */
126
+ export interface PluginHooks {
127
+ onStart?: () => Promise<void> | void;
128
+ tool?: Record<string, any>;
129
+ [key: string]: any;
130
+ }
131
+ /**
132
+ * Export type for Plugin function
133
+ */
134
+ export type PluginFunction = (ctx: PluginContext) => Promise<PluginHooks>;
135
+ /**
136
+ * SOTA benchmark elite families
137
+ */
138
+ export declare const ELITE_FAMILIES: {
139
+ readonly coding: readonly ["qwen-2.5-coder", "qwen3-coder", "deepseek-coder", "deepseek-v3", "llama-3.3-70b", "llama-3.3", "codestral", "starcoder"];
140
+ readonly reasoning: readonly ["deepseek-r1", "deepseek-reasoner", "qwq", "qwq-32b", "o1-open", "o3-mini", "reasoning", "r1"];
141
+ readonly speed: readonly ["mistral-small", "haiku", "flash", "gemma-2", "gemma-3", "distill", "nano", "lite"];
142
+ readonly multimodal: readonly ["vl", "vision", "molmo", "nemotron-vl", "pixtral", "qwen-vl"];
143
+ readonly writing: readonly ["trinity", "qwen-next", "chimera", "writer"];
144
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Type definitions for OpenCode Free Fleet v0.2.0
3
+ *
4
+ * Simplified types to fix compilation errors
5
+ */
6
+ /**
7
+ * SOTA benchmark elite families
8
+ */
9
+ export const ELITE_FAMILIES = {
10
+ coding: [
11
+ 'qwen-2.5-coder',
12
+ 'qwen3-coder',
13
+ 'deepseek-coder',
14
+ 'deepseek-v3',
15
+ 'llama-3.3-70b',
16
+ 'llama-3.3',
17
+ 'codestral',
18
+ 'starcoder'
19
+ ],
20
+ reasoning: [
21
+ 'deepseek-r1',
22
+ 'deepseek-reasoner',
23
+ 'qwq',
24
+ 'qwq-32b',
25
+ 'o1-open',
26
+ 'o3-mini',
27
+ 'reasoning',
28
+ 'r1'
29
+ ],
30
+ speed: [
31
+ 'mistral-small',
32
+ 'haiku',
33
+ 'flash',
34
+ 'gemma-2',
35
+ 'gemma-3',
36
+ 'distill',
37
+ 'nano',
38
+ 'lite'
39
+ ],
40
+ multimodal: [
41
+ 'vl',
42
+ 'vision',
43
+ 'molmo',
44
+ 'nemotron-vl',
45
+ 'pixtral',
46
+ 'qwen-vl'
47
+ ],
48
+ writing: [
49
+ 'trinity',
50
+ 'qwen-next',
51
+ 'chimera',
52
+ 'writer'
53
+ ]
54
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Version information
3
+ */
4
+ export declare const VERSION = "0.2.1";
5
+ export declare const BUILD_DATE = "2026-01-30";
6
+ export declare const RELEASE_NOTES = "Fix build issues and TypeScript errors";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Version information
3
+ */
4
+ export const VERSION = '0.2.1';
5
+ export const BUILD_DATE = '2026-01-30';
6
+ export const RELEASE_NOTES = 'Fix build issues and TypeScript errors';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-free-fleet",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Economic Load Balancing and Zero-Cost Model Discovery for OpenCode - Automatically ranks and competes free LLM models by benchmark performance",
5
5
  "author": {
6
6
  "name": "Phorde",
@@ -31,11 +31,12 @@
31
31
  "src/version.ts"
32
32
  ],
33
33
  "dependencies": {
34
- "@opencode-ai/plugin": "^1.1.0"
34
+ "@opencode-ai/plugin": "^1.1.0",
35
+ "typescript": "^5.9.3"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@eslint/js": "^9.39.1",
38
- "@types/node": "^20.11.5",
39
+ "@types/node": "^25.1.0",
39
40
  "@typescript-eslint/eslint-plugin": "8.47.0",
40
41
  "@typescript-eslint/parser": "8.47.0",
41
42
  "bun-types": "latest",
@@ -45,5 +46,12 @@
45
46
  "prettier": "^3.2.4",
46
47
  "typescript-eslint": "^8.47.0",
47
48
  "vitest": "^3.2.4"
49
+ },
50
+ "scripts": {
51
+ "build": "bun run build:tsc",
52
+ "build:tsc": "bun scripts/build.ts",
53
+ "test": "bun test",
54
+ "publish": "bun publish --access public",
55
+ "prepublishOnly": "bun run build"
48
56
  }
49
57
  }
package/src/version.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Version information
3
3
  */
4
- export const VERSION = '0.2.0';
4
+ export const VERSION = '0.2.1';
5
5
  export const BUILD_DATE = '2026-01-30';
6
- export const RELEASE_NOTES = 'Omni-Provider Support - Support for 75+ OpenCode providers';
6
+ export const RELEASE_NOTES = 'Fix build issues and TypeScript errors';