wogiflow 1.0.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/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,1381 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Model Provider Abstraction
|
|
5
|
+
*
|
|
6
|
+
* Unified interface for local and cloud LLM providers:
|
|
7
|
+
* - Ollama (local)
|
|
8
|
+
* - LM Studio (local)
|
|
9
|
+
* - Anthropic (cloud)
|
|
10
|
+
* - OpenAI (cloud)
|
|
11
|
+
*
|
|
12
|
+
* Usage as module:
|
|
13
|
+
* const { createProvider, listProviders } = require('./flow-providers');
|
|
14
|
+
* const provider = createProvider({ type: 'anthropic', apiKey: '...' });
|
|
15
|
+
* const response = await provider.complete(prompt, options);
|
|
16
|
+
*
|
|
17
|
+
* Usage as CLI:
|
|
18
|
+
* flow providers list # List available providers
|
|
19
|
+
* flow providers test <type> # Test a provider
|
|
20
|
+
* flow providers configure <type> # Configure a provider
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const https = require('https');
|
|
26
|
+
const http = require('http');
|
|
27
|
+
const { getProjectRoot, colors: c } = require('./flow-utils');
|
|
28
|
+
|
|
29
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
30
|
+
const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
|
|
31
|
+
const CONFIG_PATH = path.join(WORKFLOW_DIR, 'config.json');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Provider types
|
|
35
|
+
*/
|
|
36
|
+
const PROVIDER_TYPES = {
|
|
37
|
+
OLLAMA: 'ollama',
|
|
38
|
+
LM_STUDIO: 'lm-studio',
|
|
39
|
+
ANTHROPIC: 'anthropic',
|
|
40
|
+
OPENAI: 'openai',
|
|
41
|
+
GOOGLE: 'google'
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Model capability heuristics
|
|
46
|
+
* Used as fallback when API doesn't provide capability info
|
|
47
|
+
*/
|
|
48
|
+
const MODEL_CAPABILITIES = {
|
|
49
|
+
// High-quality code models
|
|
50
|
+
'qwen': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 32768 },
|
|
51
|
+
'qwen3': { codeQuality: 'excellent', instructionFollowing: 'excellent', contextWindow: 131072 },
|
|
52
|
+
'codellama': { codeQuality: 'high', instructionFollowing: 'medium', contextWindow: 16384 },
|
|
53
|
+
'deepseek': { codeQuality: 'excellent', instructionFollowing: 'high', contextWindow: 32768 },
|
|
54
|
+
'deepseek-coder': { codeQuality: 'excellent', instructionFollowing: 'high', contextWindow: 16384 },
|
|
55
|
+
'nemotron': { codeQuality: 'high', instructionFollowing: 'excellent', contextWindow: 32768 },
|
|
56
|
+
'starcoder': { codeQuality: 'high', instructionFollowing: 'medium', contextWindow: 8192 },
|
|
57
|
+
|
|
58
|
+
// General purpose models
|
|
59
|
+
'llama3': { codeQuality: 'medium', instructionFollowing: 'high', contextWindow: 8192 },
|
|
60
|
+
'llama3.1': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 131072 },
|
|
61
|
+
'llama3.2': { codeQuality: 'medium', instructionFollowing: 'high', contextWindow: 131072 },
|
|
62
|
+
'mistral': { codeQuality: 'medium', instructionFollowing: 'high', contextWindow: 32768 },
|
|
63
|
+
'mixtral': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 32768 },
|
|
64
|
+
'phi': { codeQuality: 'medium', instructionFollowing: 'medium', contextWindow: 4096 },
|
|
65
|
+
'phi3': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 128000 },
|
|
66
|
+
'gemma': { codeQuality: 'medium', instructionFollowing: 'high', contextWindow: 8192 },
|
|
67
|
+
'gemma2': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 8192 },
|
|
68
|
+
|
|
69
|
+
// Cloud models - Full capability
|
|
70
|
+
'claude': { codeQuality: 'excellent', instructionFollowing: 'excellent', contextWindow: 200000 },
|
|
71
|
+
'gpt-4': { codeQuality: 'excellent', instructionFollowing: 'excellent', contextWindow: 128000 },
|
|
72
|
+
'gpt-3.5': { codeQuality: 'medium', instructionFollowing: 'high', contextWindow: 16385 },
|
|
73
|
+
|
|
74
|
+
// Cloud models - Executor tier (cheaper/faster)
|
|
75
|
+
'gpt-4o-mini': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 128000, costTier: 'cheap' },
|
|
76
|
+
'claude-3-haiku': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 200000, costTier: 'cheap' },
|
|
77
|
+
'claude-3-5-haiku': { codeQuality: 'high', instructionFollowing: 'excellent', contextWindow: 200000, costTier: 'cheap' },
|
|
78
|
+
'gemini-flash': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 1000000, costTier: 'cheap' },
|
|
79
|
+
'gemini-2.0-flash': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 1000000, costTier: 'cheap' },
|
|
80
|
+
'gemini-1.5-flash': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 1000000, costTier: 'cheap' },
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Detect model capabilities from model name
|
|
85
|
+
*/
|
|
86
|
+
function detectModelCapabilities(modelName) {
|
|
87
|
+
if (!modelName) return null;
|
|
88
|
+
|
|
89
|
+
const nameLower = modelName.toLowerCase();
|
|
90
|
+
|
|
91
|
+
// Try exact matches first
|
|
92
|
+
for (const [pattern, caps] of Object.entries(MODEL_CAPABILITIES)) {
|
|
93
|
+
if (nameLower.includes(pattern)) {
|
|
94
|
+
return {
|
|
95
|
+
...caps,
|
|
96
|
+
source: 'heuristic',
|
|
97
|
+
matchedPattern: pattern
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Default fallback for unknown models
|
|
103
|
+
return {
|
|
104
|
+
codeQuality: 'unknown',
|
|
105
|
+
instructionFollowing: 'unknown',
|
|
106
|
+
contextWindow: 4096,
|
|
107
|
+
source: 'default'
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get recommended model for a task type
|
|
113
|
+
*/
|
|
114
|
+
function getRecommendedModel(availableModels, taskType = 'code') {
|
|
115
|
+
if (!availableModels || availableModels.length === 0) return null;
|
|
116
|
+
|
|
117
|
+
const withCaps = availableModels.map(m => ({
|
|
118
|
+
...m,
|
|
119
|
+
capabilities: detectModelCapabilities(m.id || m.name)
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
// Score models based on task
|
|
123
|
+
const scored = withCaps.map(m => {
|
|
124
|
+
let score = 0;
|
|
125
|
+
const caps = m.capabilities;
|
|
126
|
+
|
|
127
|
+
if (taskType === 'code') {
|
|
128
|
+
if (caps.codeQuality === 'excellent') score += 10;
|
|
129
|
+
else if (caps.codeQuality === 'high') score += 7;
|
|
130
|
+
else if (caps.codeQuality === 'medium') score += 4;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (caps.instructionFollowing === 'excellent') score += 5;
|
|
134
|
+
else if (caps.instructionFollowing === 'high') score += 3;
|
|
135
|
+
|
|
136
|
+
// Prefer larger context windows for complex tasks
|
|
137
|
+
if (caps.contextWindow >= 32768) score += 3;
|
|
138
|
+
else if (caps.contextWindow >= 16384) score += 2;
|
|
139
|
+
|
|
140
|
+
return { ...m, score };
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Sort by score descending
|
|
144
|
+
scored.sort((a, b) => b.score - a.score);
|
|
145
|
+
|
|
146
|
+
return scored[0] || null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Default provider configurations
|
|
151
|
+
*/
|
|
152
|
+
const DEFAULT_CONFIGS = {
|
|
153
|
+
ollama: {
|
|
154
|
+
endpoint: 'http://localhost:11434',
|
|
155
|
+
model: 'llama3.2',
|
|
156
|
+
temperature: 0.7,
|
|
157
|
+
maxTokens: 4096,
|
|
158
|
+
timeout: 120000
|
|
159
|
+
},
|
|
160
|
+
'lm-studio': {
|
|
161
|
+
endpoint: 'http://localhost:1234/v1',
|
|
162
|
+
model: 'local-model',
|
|
163
|
+
temperature: 0.7,
|
|
164
|
+
maxTokens: 4096,
|
|
165
|
+
timeout: 120000
|
|
166
|
+
},
|
|
167
|
+
anthropic: {
|
|
168
|
+
endpoint: 'https://api.anthropic.com/v1',
|
|
169
|
+
model: 'claude-sonnet-4-20250514',
|
|
170
|
+
temperature: 0.7,
|
|
171
|
+
maxTokens: 4096,
|
|
172
|
+
timeout: 60000
|
|
173
|
+
},
|
|
174
|
+
openai: {
|
|
175
|
+
endpoint: 'https://api.openai.com/v1',
|
|
176
|
+
model: 'gpt-4o',
|
|
177
|
+
temperature: 0.7,
|
|
178
|
+
maxTokens: 4096,
|
|
179
|
+
timeout: 60000
|
|
180
|
+
},
|
|
181
|
+
google: {
|
|
182
|
+
endpoint: 'https://generativelanguage.googleapis.com/v1beta',
|
|
183
|
+
model: 'gemini-2.0-flash-exp',
|
|
184
|
+
temperature: 0.7,
|
|
185
|
+
maxTokens: 4096,
|
|
186
|
+
timeout: 60000
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Base provider class
|
|
192
|
+
*/
|
|
193
|
+
class BaseProvider {
|
|
194
|
+
constructor(config) {
|
|
195
|
+
this.config = config;
|
|
196
|
+
this.name = 'base';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async complete(prompt, options = {}) {
|
|
200
|
+
throw new Error('Not implemented');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async test() {
|
|
204
|
+
try {
|
|
205
|
+
const response = await this.complete('Say "OK" if you can hear me.', {
|
|
206
|
+
maxTokens: 10
|
|
207
|
+
});
|
|
208
|
+
return { success: true, response };
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return { success: false, error: error.message };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async listModels() {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Ollama provider
|
|
221
|
+
*/
|
|
222
|
+
class OllamaProvider extends BaseProvider {
|
|
223
|
+
constructor(config) {
|
|
224
|
+
super(config);
|
|
225
|
+
this.name = 'ollama';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async complete(prompt, options = {}) {
|
|
229
|
+
const endpoint = this.config.endpoint || DEFAULT_CONFIGS.ollama.endpoint;
|
|
230
|
+
const url = new URL('/api/generate', endpoint);
|
|
231
|
+
|
|
232
|
+
const body = {
|
|
233
|
+
model: options.model || this.config.model || DEFAULT_CONFIGS.ollama.model,
|
|
234
|
+
prompt,
|
|
235
|
+
stream: false,
|
|
236
|
+
options: {
|
|
237
|
+
temperature: options.temperature || this.config.temperature || 0.7,
|
|
238
|
+
num_predict: options.maxTokens || this.config.maxTokens || 4096
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const response = await this._request(url, body);
|
|
243
|
+
return {
|
|
244
|
+
content: response.response,
|
|
245
|
+
model: response.model,
|
|
246
|
+
usage: {
|
|
247
|
+
promptTokens: response.prompt_eval_count,
|
|
248
|
+
completionTokens: response.eval_count,
|
|
249
|
+
totalTokens: (response.prompt_eval_count || 0) + (response.eval_count || 0)
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async listModels() {
|
|
255
|
+
const endpoint = this.config.endpoint || DEFAULT_CONFIGS.ollama.endpoint;
|
|
256
|
+
const url = new URL('/api/tags', endpoint);
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const response = await this._request(url, null, 'GET');
|
|
260
|
+
return (response.models || []).map(m => ({
|
|
261
|
+
id: m.name,
|
|
262
|
+
name: m.name,
|
|
263
|
+
size: m.size,
|
|
264
|
+
capabilities: detectModelCapabilities(m.name)
|
|
265
|
+
}));
|
|
266
|
+
} catch {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get detailed model info from Ollama API
|
|
273
|
+
*/
|
|
274
|
+
async getModelInfo(modelName) {
|
|
275
|
+
const endpoint = this.config.endpoint || DEFAULT_CONFIGS.ollama.endpoint;
|
|
276
|
+
const url = new URL('/api/show', endpoint);
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const response = await this._request(url, { name: modelName });
|
|
280
|
+
|
|
281
|
+
// Extract capabilities from model metadata if available
|
|
282
|
+
const modelfile = response.modelfile || '';
|
|
283
|
+
const parameters = response.parameters || '';
|
|
284
|
+
|
|
285
|
+
// Parse context length from modelfile or parameters
|
|
286
|
+
let contextWindow = 4096;
|
|
287
|
+
const ctxMatch = modelfile.match(/num_ctx\s+(\d+)/i) ||
|
|
288
|
+
parameters.match(/num_ctx\s+(\d+)/i);
|
|
289
|
+
if (ctxMatch) {
|
|
290
|
+
contextWindow = parseInt(ctxMatch[1], 10);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Get heuristic capabilities and override with API data
|
|
294
|
+
const heuristicCaps = detectModelCapabilities(modelName);
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
name: modelName,
|
|
298
|
+
modelfile: modelfile.slice(0, 500), // Truncate for display
|
|
299
|
+
parameters,
|
|
300
|
+
capabilities: {
|
|
301
|
+
...heuristicCaps,
|
|
302
|
+
contextWindow: Math.max(contextWindow, heuristicCaps.contextWindow),
|
|
303
|
+
source: 'api+heuristic'
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
} catch {
|
|
307
|
+
// Fall back to heuristic only
|
|
308
|
+
return {
|
|
309
|
+
name: modelName,
|
|
310
|
+
capabilities: detectModelCapabilities(modelName)
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
_request(url, body, method = 'POST') {
|
|
316
|
+
return new Promise((resolve, reject) => {
|
|
317
|
+
const options = {
|
|
318
|
+
method,
|
|
319
|
+
hostname: url.hostname,
|
|
320
|
+
port: url.port || 11434,
|
|
321
|
+
path: url.pathname,
|
|
322
|
+
headers: {
|
|
323
|
+
'Content-Type': 'application/json'
|
|
324
|
+
},
|
|
325
|
+
timeout: this.config.timeout || 120000
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const req = http.request(options, (res) => {
|
|
329
|
+
let data = '';
|
|
330
|
+
res.on('data', chunk => data += chunk);
|
|
331
|
+
res.on('end', () => {
|
|
332
|
+
try {
|
|
333
|
+
resolve(JSON.parse(data));
|
|
334
|
+
} catch {
|
|
335
|
+
reject(new Error(`Invalid response: ${data.slice(0, 100)}`));
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
req.on('error', reject);
|
|
341
|
+
req.on('timeout', () => reject(new Error('Request timeout')));
|
|
342
|
+
|
|
343
|
+
if (body) {
|
|
344
|
+
req.write(JSON.stringify(body));
|
|
345
|
+
}
|
|
346
|
+
req.end();
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* LM Studio provider (OpenAI-compatible)
|
|
353
|
+
*/
|
|
354
|
+
class LMStudioProvider extends BaseProvider {
|
|
355
|
+
constructor(config) {
|
|
356
|
+
super(config);
|
|
357
|
+
this.name = 'lm-studio';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async complete(prompt, options = {}) {
|
|
361
|
+
const endpoint = this.config.endpoint || DEFAULT_CONFIGS['lm-studio'].endpoint;
|
|
362
|
+
const url = new URL('/chat/completions', endpoint);
|
|
363
|
+
|
|
364
|
+
const body = {
|
|
365
|
+
model: options.model || this.config.model || 'local-model',
|
|
366
|
+
messages: [{ role: 'user', content: prompt }],
|
|
367
|
+
temperature: options.temperature || this.config.temperature || 0.7,
|
|
368
|
+
max_tokens: options.maxTokens || this.config.maxTokens || 4096
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const response = await this._request(url, body);
|
|
372
|
+
return {
|
|
373
|
+
content: response.choices?.[0]?.message?.content || '',
|
|
374
|
+
model: response.model,
|
|
375
|
+
usage: {
|
|
376
|
+
promptTokens: response.usage?.prompt_tokens,
|
|
377
|
+
completionTokens: response.usage?.completion_tokens,
|
|
378
|
+
totalTokens: response.usage?.total_tokens
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
_request(url, body) {
|
|
384
|
+
return new Promise((resolve, reject) => {
|
|
385
|
+
const options = {
|
|
386
|
+
method: 'POST',
|
|
387
|
+
hostname: url.hostname,
|
|
388
|
+
port: url.port || 1234,
|
|
389
|
+
path: url.pathname,
|
|
390
|
+
headers: {
|
|
391
|
+
'Content-Type': 'application/json'
|
|
392
|
+
},
|
|
393
|
+
timeout: this.config.timeout || 120000
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const req = http.request(options, (res) => {
|
|
397
|
+
let data = '';
|
|
398
|
+
res.on('data', chunk => data += chunk);
|
|
399
|
+
res.on('end', () => {
|
|
400
|
+
try {
|
|
401
|
+
resolve(JSON.parse(data));
|
|
402
|
+
} catch {
|
|
403
|
+
reject(new Error(`Invalid response: ${data.slice(0, 100)}`));
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
req.on('error', reject);
|
|
409
|
+
req.on('timeout', () => reject(new Error('Request timeout')));
|
|
410
|
+
req.write(JSON.stringify(body));
|
|
411
|
+
req.end();
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async listModels() {
|
|
416
|
+
const endpoint = this.config.endpoint || DEFAULT_CONFIGS['lm-studio'].endpoint;
|
|
417
|
+
return new Promise((resolve) => {
|
|
418
|
+
const url = new URL('/v1/models', endpoint);
|
|
419
|
+
const req = http.request({
|
|
420
|
+
method: 'GET',
|
|
421
|
+
hostname: url.hostname,
|
|
422
|
+
port: url.port || 1234,
|
|
423
|
+
path: url.pathname,
|
|
424
|
+
timeout: 5000
|
|
425
|
+
}, (res) => {
|
|
426
|
+
let data = '';
|
|
427
|
+
res.on('data', chunk => data += chunk);
|
|
428
|
+
res.on('end', () => {
|
|
429
|
+
try {
|
|
430
|
+
const parsed = JSON.parse(data);
|
|
431
|
+
resolve((parsed.data || []).map(m => ({ id: m.id, name: m.id })));
|
|
432
|
+
} catch {
|
|
433
|
+
resolve([]);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
req.on('error', () => resolve([]));
|
|
438
|
+
req.on('timeout', () => resolve([]));
|
|
439
|
+
req.end();
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Anthropic provider
|
|
446
|
+
*/
|
|
447
|
+
class AnthropicProvider extends BaseProvider {
|
|
448
|
+
constructor(config) {
|
|
449
|
+
super(config);
|
|
450
|
+
this.name = 'anthropic';
|
|
451
|
+
this.apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async complete(prompt, options = {}) {
|
|
455
|
+
if (!this.apiKey) {
|
|
456
|
+
throw new Error('Anthropic API key not configured. Set ANTHROPIC_API_KEY or provide apiKey in config.');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const endpoint = this.config.endpoint || DEFAULT_CONFIGS.anthropic.endpoint;
|
|
460
|
+
const url = new URL('/messages', endpoint);
|
|
461
|
+
|
|
462
|
+
const body = {
|
|
463
|
+
model: options.model || this.config.model || DEFAULT_CONFIGS.anthropic.model,
|
|
464
|
+
max_tokens: options.maxTokens || this.config.maxTokens || 4096,
|
|
465
|
+
messages: [{ role: 'user', content: prompt }]
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
if (options.system || this.config.system) {
|
|
469
|
+
body.system = options.system || this.config.system;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const response = await this._request(url, body);
|
|
473
|
+
|
|
474
|
+
if (response.error) {
|
|
475
|
+
throw new Error(response.error.message);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
content: response.content?.[0]?.text || '',
|
|
480
|
+
model: response.model,
|
|
481
|
+
stopReason: response.stop_reason,
|
|
482
|
+
usage: {
|
|
483
|
+
promptTokens: response.usage?.input_tokens,
|
|
484
|
+
completionTokens: response.usage?.output_tokens,
|
|
485
|
+
totalTokens: (response.usage?.input_tokens || 0) + (response.usage?.output_tokens || 0)
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
_request(url, body) {
|
|
491
|
+
return new Promise((resolve, reject) => {
|
|
492
|
+
const options = {
|
|
493
|
+
method: 'POST',
|
|
494
|
+
hostname: url.hostname,
|
|
495
|
+
path: url.pathname,
|
|
496
|
+
headers: {
|
|
497
|
+
'Content-Type': 'application/json',
|
|
498
|
+
'x-api-key': this.apiKey,
|
|
499
|
+
'anthropic-version': '2023-06-01'
|
|
500
|
+
},
|
|
501
|
+
timeout: this.config.timeout || 60000
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const req = https.request(options, (res) => {
|
|
505
|
+
let data = '';
|
|
506
|
+
res.on('data', chunk => data += chunk);
|
|
507
|
+
res.on('end', () => {
|
|
508
|
+
try {
|
|
509
|
+
resolve(JSON.parse(data));
|
|
510
|
+
} catch {
|
|
511
|
+
reject(new Error(`Invalid response: ${data.slice(0, 100)}`));
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
req.on('error', reject);
|
|
517
|
+
req.on('timeout', () => reject(new Error('Request timeout')));
|
|
518
|
+
req.write(JSON.stringify(body));
|
|
519
|
+
req.end();
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* OpenAI provider
|
|
526
|
+
*/
|
|
527
|
+
class OpenAIProvider extends BaseProvider {
|
|
528
|
+
constructor(config) {
|
|
529
|
+
super(config);
|
|
530
|
+
this.name = 'openai';
|
|
531
|
+
this.apiKey = config.apiKey || process.env.OPENAI_API_KEY;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async complete(prompt, options = {}) {
|
|
535
|
+
if (!this.apiKey) {
|
|
536
|
+
throw new Error('OpenAI API key not configured. Set OPENAI_API_KEY or provide apiKey in config.');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const endpoint = this.config.endpoint || DEFAULT_CONFIGS.openai.endpoint;
|
|
540
|
+
const url = new URL('/chat/completions', endpoint);
|
|
541
|
+
|
|
542
|
+
const messages = [{ role: 'user', content: prompt }];
|
|
543
|
+
|
|
544
|
+
if (options.system || this.config.system) {
|
|
545
|
+
messages.unshift({ role: 'system', content: options.system || this.config.system });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const body = {
|
|
549
|
+
model: options.model || this.config.model || DEFAULT_CONFIGS.openai.model,
|
|
550
|
+
messages,
|
|
551
|
+
temperature: options.temperature || this.config.temperature || 0.7,
|
|
552
|
+
max_tokens: options.maxTokens || this.config.maxTokens || 4096
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const response = await this._request(url, body);
|
|
556
|
+
|
|
557
|
+
if (response.error) {
|
|
558
|
+
throw new Error(response.error.message);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
content: response.choices?.[0]?.message?.content || '',
|
|
563
|
+
model: response.model,
|
|
564
|
+
stopReason: response.choices?.[0]?.finish_reason,
|
|
565
|
+
usage: {
|
|
566
|
+
promptTokens: response.usage?.prompt_tokens,
|
|
567
|
+
completionTokens: response.usage?.completion_tokens,
|
|
568
|
+
totalTokens: response.usage?.total_tokens
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async listModels() {
|
|
574
|
+
if (!this.apiKey) return [];
|
|
575
|
+
|
|
576
|
+
const endpoint = this.config.endpoint || DEFAULT_CONFIGS.openai.endpoint;
|
|
577
|
+
const url = new URL('/models', endpoint);
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
const response = await this._request(url, null, 'GET');
|
|
581
|
+
return (response.data || [])
|
|
582
|
+
.filter(m => m.id.includes('gpt'))
|
|
583
|
+
.map(m => ({
|
|
584
|
+
id: m.id,
|
|
585
|
+
name: m.id,
|
|
586
|
+
owned_by: m.owned_by
|
|
587
|
+
}));
|
|
588
|
+
} catch {
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
_request(url, body, method = 'POST') {
|
|
594
|
+
return new Promise((resolve, reject) => {
|
|
595
|
+
const options = {
|
|
596
|
+
method,
|
|
597
|
+
hostname: url.hostname,
|
|
598
|
+
path: url.pathname,
|
|
599
|
+
headers: {
|
|
600
|
+
'Content-Type': 'application/json',
|
|
601
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
602
|
+
},
|
|
603
|
+
timeout: this.config.timeout || 60000
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const req = https.request(options, (res) => {
|
|
607
|
+
let data = '';
|
|
608
|
+
res.on('data', chunk => data += chunk);
|
|
609
|
+
res.on('end', () => {
|
|
610
|
+
try {
|
|
611
|
+
resolve(JSON.parse(data));
|
|
612
|
+
} catch {
|
|
613
|
+
reject(new Error(`Invalid response: ${data.slice(0, 100)}`));
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
req.on('error', reject);
|
|
619
|
+
req.on('timeout', () => reject(new Error('Request timeout')));
|
|
620
|
+
|
|
621
|
+
if (body) {
|
|
622
|
+
req.write(JSON.stringify(body));
|
|
623
|
+
}
|
|
624
|
+
req.end();
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Google (Gemini) provider
|
|
631
|
+
*/
|
|
632
|
+
class GoogleProvider extends BaseProvider {
|
|
633
|
+
constructor(config) {
|
|
634
|
+
super(config);
|
|
635
|
+
this.name = 'google';
|
|
636
|
+
this.apiKey = config.apiKey || process.env.GOOGLE_API_KEY;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async complete(prompt, options = {}) {
|
|
640
|
+
if (!this.apiKey) {
|
|
641
|
+
throw new Error('Google API key not configured. Set GOOGLE_API_KEY or provide apiKey in config.');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const model = options.model || this.config.model || DEFAULT_CONFIGS.google.model;
|
|
645
|
+
const endpoint = this.config.endpoint || DEFAULT_CONFIGS.google.endpoint;
|
|
646
|
+
const url = new URL(`/models/${model}:generateContent`, endpoint);
|
|
647
|
+
// API key passed via header for security (not URL to avoid logging exposure)
|
|
648
|
+
|
|
649
|
+
const body = {
|
|
650
|
+
contents: [{
|
|
651
|
+
parts: [{ text: prompt }]
|
|
652
|
+
}],
|
|
653
|
+
generationConfig: {
|
|
654
|
+
temperature: options.temperature || this.config.temperature || 0.7,
|
|
655
|
+
maxOutputTokens: options.maxTokens || this.config.maxTokens || 4096
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
// Add system instruction if provided
|
|
660
|
+
if (options.system || this.config.system) {
|
|
661
|
+
body.systemInstruction = {
|
|
662
|
+
parts: [{ text: options.system || this.config.system }]
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const response = await this._request(url, body, 'POST', { 'x-goog-api-key': this.apiKey });
|
|
667
|
+
|
|
668
|
+
if (response.error) {
|
|
669
|
+
throw new Error(response.error.message);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const content = response.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
content,
|
|
676
|
+
model,
|
|
677
|
+
stopReason: response.candidates?.[0]?.finishReason,
|
|
678
|
+
usage: {
|
|
679
|
+
promptTokens: response.usageMetadata?.promptTokenCount,
|
|
680
|
+
completionTokens: response.usageMetadata?.candidatesTokenCount,
|
|
681
|
+
totalTokens: response.usageMetadata?.totalTokenCount
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async listModels() {
|
|
687
|
+
if (!this.apiKey) return [];
|
|
688
|
+
|
|
689
|
+
const endpoint = this.config.endpoint || DEFAULT_CONFIGS.google.endpoint;
|
|
690
|
+
const url = new URL('/models', endpoint);
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
const response = await this._request(url, null, 'GET', { 'x-goog-api-key': this.apiKey });
|
|
694
|
+
return (response.models || [])
|
|
695
|
+
.filter(m => m.name.includes('gemini'))
|
|
696
|
+
.map(m => ({
|
|
697
|
+
id: m.name.replace('models/', ''),
|
|
698
|
+
name: m.displayName || m.name,
|
|
699
|
+
description: m.description
|
|
700
|
+
}));
|
|
701
|
+
} catch {
|
|
702
|
+
return [];
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
_request(url, body, method = 'POST', additionalHeaders = {}) {
|
|
707
|
+
return new Promise((resolve, reject) => {
|
|
708
|
+
const options = {
|
|
709
|
+
method,
|
|
710
|
+
hostname: url.hostname,
|
|
711
|
+
path: url.pathname + url.search,
|
|
712
|
+
headers: {
|
|
713
|
+
'Content-Type': 'application/json',
|
|
714
|
+
...additionalHeaders
|
|
715
|
+
},
|
|
716
|
+
timeout: this.config.timeout || 60000
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const req = https.request(options, (res) => {
|
|
720
|
+
let data = '';
|
|
721
|
+
res.on('data', chunk => data += chunk);
|
|
722
|
+
res.on('end', () => {
|
|
723
|
+
try {
|
|
724
|
+
resolve(JSON.parse(data));
|
|
725
|
+
} catch {
|
|
726
|
+
reject(new Error(`Invalid response: ${data.slice(0, 100)}`));
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
req.on('error', reject);
|
|
732
|
+
req.on('timeout', () => reject(new Error('Request timeout')));
|
|
733
|
+
|
|
734
|
+
if (body) {
|
|
735
|
+
req.write(JSON.stringify(body));
|
|
736
|
+
}
|
|
737
|
+
req.end();
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Create a provider instance
|
|
744
|
+
*/
|
|
745
|
+
function createProvider(config) {
|
|
746
|
+
const type = config.type || config.provider;
|
|
747
|
+
|
|
748
|
+
switch (type) {
|
|
749
|
+
case PROVIDER_TYPES.OLLAMA:
|
|
750
|
+
return new OllamaProvider(config);
|
|
751
|
+
case PROVIDER_TYPES.LM_STUDIO:
|
|
752
|
+
return new LMStudioProvider(config);
|
|
753
|
+
case PROVIDER_TYPES.ANTHROPIC:
|
|
754
|
+
return new AnthropicProvider(config);
|
|
755
|
+
case PROVIDER_TYPES.OPENAI:
|
|
756
|
+
return new OpenAIProvider(config);
|
|
757
|
+
case PROVIDER_TYPES.GOOGLE:
|
|
758
|
+
return new GoogleProvider(config);
|
|
759
|
+
default:
|
|
760
|
+
throw new Error(`Unknown provider type: ${type}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Create executor provider from hybrid config
|
|
766
|
+
* Supports both legacy config (provider/model) and new executor config
|
|
767
|
+
*/
|
|
768
|
+
function createExecutorFromConfig(hybridConfig) {
|
|
769
|
+
// New executor config structure
|
|
770
|
+
if (hybridConfig.executor && hybridConfig.executor.provider) {
|
|
771
|
+
const executor = hybridConfig.executor;
|
|
772
|
+
return createProvider({
|
|
773
|
+
type: executor.provider,
|
|
774
|
+
endpoint: executor.providerEndpoint,
|
|
775
|
+
model: executor.model,
|
|
776
|
+
apiKey: executor.apiKey,
|
|
777
|
+
...hybridConfig.settings
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Legacy config structure (backward compatible)
|
|
782
|
+
if (hybridConfig.provider) {
|
|
783
|
+
return createProvider({
|
|
784
|
+
type: hybridConfig.provider,
|
|
785
|
+
endpoint: hybridConfig.providerEndpoint,
|
|
786
|
+
model: hybridConfig.model,
|
|
787
|
+
apiKey: hybridConfig.apiKey,
|
|
788
|
+
...hybridConfig.settings
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Get executor configuration from hybrid config
|
|
797
|
+
* Normalizes legacy and new config formats
|
|
798
|
+
*/
|
|
799
|
+
function getExecutorConfig(hybridConfig) {
|
|
800
|
+
// New executor config
|
|
801
|
+
if (hybridConfig.executor && hybridConfig.executor.provider) {
|
|
802
|
+
return {
|
|
803
|
+
type: hybridConfig.executor.type || 'local',
|
|
804
|
+
provider: hybridConfig.executor.provider,
|
|
805
|
+
providerEndpoint: hybridConfig.executor.providerEndpoint,
|
|
806
|
+
model: hybridConfig.executor.model,
|
|
807
|
+
apiKey: hybridConfig.executor.apiKey
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Legacy config
|
|
812
|
+
if (hybridConfig.provider) {
|
|
813
|
+
const isLocal = ['ollama', 'lm-studio'].includes(hybridConfig.provider);
|
|
814
|
+
return {
|
|
815
|
+
type: isLocal ? 'local' : 'cloud',
|
|
816
|
+
provider: hybridConfig.provider,
|
|
817
|
+
providerEndpoint: hybridConfig.providerEndpoint,
|
|
818
|
+
model: hybridConfig.model,
|
|
819
|
+
apiKey: hybridConfig.apiKey
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* List all available providers
|
|
828
|
+
*/
|
|
829
|
+
function listProviders() {
|
|
830
|
+
return [
|
|
831
|
+
{
|
|
832
|
+
type: PROVIDER_TYPES.OLLAMA,
|
|
833
|
+
name: 'Ollama',
|
|
834
|
+
local: true,
|
|
835
|
+
requiresKey: false,
|
|
836
|
+
defaultEndpoint: DEFAULT_CONFIGS.ollama.endpoint,
|
|
837
|
+
executorModels: ['qwen3-coder', 'deepseek-coder', 'codellama', 'nemotron']
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
type: PROVIDER_TYPES.LM_STUDIO,
|
|
841
|
+
name: 'LM Studio',
|
|
842
|
+
local: true,
|
|
843
|
+
requiresKey: false,
|
|
844
|
+
defaultEndpoint: DEFAULT_CONFIGS['lm-studio'].endpoint,
|
|
845
|
+
executorModels: ['loaded model']
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
type: PROVIDER_TYPES.ANTHROPIC,
|
|
849
|
+
name: 'Anthropic',
|
|
850
|
+
local: false,
|
|
851
|
+
requiresKey: true,
|
|
852
|
+
envVar: 'ANTHROPIC_API_KEY',
|
|
853
|
+
defaultEndpoint: DEFAULT_CONFIGS.anthropic.endpoint,
|
|
854
|
+
executorModels: ['claude-3-5-haiku-latest', 'claude-3-haiku-20240307']
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
type: PROVIDER_TYPES.OPENAI,
|
|
858
|
+
name: 'OpenAI',
|
|
859
|
+
local: false,
|
|
860
|
+
requiresKey: true,
|
|
861
|
+
envVar: 'OPENAI_API_KEY',
|
|
862
|
+
defaultEndpoint: DEFAULT_CONFIGS.openai.endpoint,
|
|
863
|
+
executorModels: ['gpt-4o-mini', 'gpt-4o']
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
type: PROVIDER_TYPES.GOOGLE,
|
|
867
|
+
name: 'Google (Gemini)',
|
|
868
|
+
local: false,
|
|
869
|
+
requiresKey: true,
|
|
870
|
+
envVar: 'GOOGLE_API_KEY',
|
|
871
|
+
defaultEndpoint: DEFAULT_CONFIGS.google.endpoint,
|
|
872
|
+
executorModels: ['gemini-2.0-flash-exp', 'gemini-1.5-flash']
|
|
873
|
+
}
|
|
874
|
+
];
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Detect available providers
|
|
879
|
+
*/
|
|
880
|
+
async function detectProviders() {
|
|
881
|
+
const available = [];
|
|
882
|
+
|
|
883
|
+
// Check Ollama (local)
|
|
884
|
+
try {
|
|
885
|
+
const ollama = new OllamaProvider({});
|
|
886
|
+
const models = await ollama.listModels();
|
|
887
|
+
if (models.length > 0) {
|
|
888
|
+
available.push({
|
|
889
|
+
type: PROVIDER_TYPES.OLLAMA,
|
|
890
|
+
name: 'Ollama',
|
|
891
|
+
local: true,
|
|
892
|
+
cost: 'free',
|
|
893
|
+
models: models.slice(0, 5)
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
} catch {
|
|
897
|
+
// Not available
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Check LM Studio (local)
|
|
901
|
+
try {
|
|
902
|
+
const lmStudio = new LMStudioProvider({});
|
|
903
|
+
const models = await lmStudio.listModels();
|
|
904
|
+
if (models.length > 0) {
|
|
905
|
+
available.push({
|
|
906
|
+
type: PROVIDER_TYPES.LM_STUDIO,
|
|
907
|
+
name: 'LM Studio',
|
|
908
|
+
local: true,
|
|
909
|
+
cost: 'free',
|
|
910
|
+
models: models
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
} catch {
|
|
914
|
+
// Not available
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Check Anthropic (cloud - if key present)
|
|
918
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
919
|
+
available.push({
|
|
920
|
+
type: PROVIDER_TYPES.ANTHROPIC,
|
|
921
|
+
name: 'Anthropic',
|
|
922
|
+
local: false,
|
|
923
|
+
cost: 'paid',
|
|
924
|
+
models: [
|
|
925
|
+
{ id: 'claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku (Best for executor)', recommended: true },
|
|
926
|
+
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4' },
|
|
927
|
+
{ id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet' }
|
|
928
|
+
]
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Check OpenAI (cloud - if key present)
|
|
933
|
+
if (process.env.OPENAI_API_KEY) {
|
|
934
|
+
available.push({
|
|
935
|
+
type: PROVIDER_TYPES.OPENAI,
|
|
936
|
+
name: 'OpenAI',
|
|
937
|
+
local: false,
|
|
938
|
+
cost: 'paid',
|
|
939
|
+
models: [
|
|
940
|
+
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini (Best for executor)', recommended: true },
|
|
941
|
+
{ id: 'gpt-4o', name: 'GPT-4o' }
|
|
942
|
+
]
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Check Google (cloud - if key present)
|
|
947
|
+
if (process.env.GOOGLE_API_KEY) {
|
|
948
|
+
available.push({
|
|
949
|
+
type: PROVIDER_TYPES.GOOGLE,
|
|
950
|
+
name: 'Google (Gemini)',
|
|
951
|
+
local: false,
|
|
952
|
+
cost: 'paid',
|
|
953
|
+
models: [
|
|
954
|
+
{ id: 'gemini-2.0-flash-exp', name: 'Gemini 2.0 Flash (Best for executor)', recommended: true },
|
|
955
|
+
{ id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash' }
|
|
956
|
+
]
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return available;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Load provider from config
|
|
965
|
+
*/
|
|
966
|
+
function loadProviderFromConfig() {
|
|
967
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
try {
|
|
972
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
973
|
+
const hybridConfig = config.hybrid || {};
|
|
974
|
+
|
|
975
|
+
if (!hybridConfig.enabled || !hybridConfig.provider) {
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return createProvider({
|
|
980
|
+
type: hybridConfig.provider,
|
|
981
|
+
endpoint: hybridConfig.providerEndpoint,
|
|
982
|
+
model: hybridConfig.model,
|
|
983
|
+
apiKey: hybridConfig.apiKey,
|
|
984
|
+
...hybridConfig.settings
|
|
985
|
+
});
|
|
986
|
+
} catch {
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// ============================================================
|
|
992
|
+
// Token Budgeting with Auto-Detection
|
|
993
|
+
// ============================================================
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Auto-detect model context limit from provider API
|
|
997
|
+
* No manual config needed - queries Ollama/LM Studio directly
|
|
998
|
+
*
|
|
999
|
+
* @param {string} providerType - 'ollama' or 'lm-studio'
|
|
1000
|
+
* @param {string} endpoint - Provider endpoint URL
|
|
1001
|
+
* @param {string} modelName - Model name to check
|
|
1002
|
+
* @returns {Promise<number>} Context window size in tokens
|
|
1003
|
+
*/
|
|
1004
|
+
async function getModelContextLimit(providerType, endpoint, modelName) {
|
|
1005
|
+
try {
|
|
1006
|
+
if (providerType === PROVIDER_TYPES.OLLAMA || providerType === 'ollama') {
|
|
1007
|
+
// Ollama /api/show returns model metadata including num_ctx
|
|
1008
|
+
const provider = new OllamaProvider({ endpoint });
|
|
1009
|
+
const info = await provider.getModelInfo(modelName);
|
|
1010
|
+
|
|
1011
|
+
if (info?.capabilities?.contextWindow) {
|
|
1012
|
+
return info.capabilities.contextWindow;
|
|
1013
|
+
}
|
|
1014
|
+
return 4096; // Ollama default
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (providerType === PROVIDER_TYPES.LM_STUDIO || providerType === 'lm-studio' || providerType === 'lmstudio') {
|
|
1018
|
+
// LM Studio /v1/models returns context_length in model metadata
|
|
1019
|
+
const url = new URL('/v1/models', endpoint);
|
|
1020
|
+
|
|
1021
|
+
return new Promise((resolve) => {
|
|
1022
|
+
const req = http.get(url.toString(), { timeout: 5000 }, (res) => {
|
|
1023
|
+
let data = '';
|
|
1024
|
+
res.on('data', chunk => data += chunk);
|
|
1025
|
+
res.on('end', () => {
|
|
1026
|
+
try {
|
|
1027
|
+
const parsed = JSON.parse(data);
|
|
1028
|
+
// LM Studio returns context_length in model data
|
|
1029
|
+
const model = parsed.data?.find(m => m.id === modelName) || parsed.data?.[0];
|
|
1030
|
+
if (model?.context_length) {
|
|
1031
|
+
resolve(model.context_length);
|
|
1032
|
+
} else {
|
|
1033
|
+
// Default for LM Studio
|
|
1034
|
+
resolve(8192);
|
|
1035
|
+
}
|
|
1036
|
+
} catch {
|
|
1037
|
+
resolve(8192);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
req.on('error', () => resolve(8192));
|
|
1043
|
+
req.on('timeout', () => {
|
|
1044
|
+
req.destroy();
|
|
1045
|
+
resolve(8192);
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Cloud providers - use known limits from capabilities
|
|
1051
|
+
const caps = detectModelCapabilities(modelName);
|
|
1052
|
+
return caps?.contextWindow || 8192;
|
|
1053
|
+
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
if (process.env.DEBUG) {
|
|
1056
|
+
console.warn(`Could not detect context limit: ${err.message}`);
|
|
1057
|
+
}
|
|
1058
|
+
return 8192; // Safe fallback
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Estimate token count from text
|
|
1064
|
+
* Uses conservative 4 chars per token estimate for English/code
|
|
1065
|
+
*
|
|
1066
|
+
* @param {string} text - Text to estimate
|
|
1067
|
+
* @returns {number} Estimated token count
|
|
1068
|
+
*/
|
|
1069
|
+
function estimateTokens(text) {
|
|
1070
|
+
if (!text) return 0;
|
|
1071
|
+
// 4 chars per token is conservative estimate
|
|
1072
|
+
// Actual tokenizers vary but this is a reasonable approximation
|
|
1073
|
+
return Math.ceil(text.length / 4);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Create a token budgeting helper for a model
|
|
1078
|
+
*
|
|
1079
|
+
* @param {number} contextLimit - Total context window
|
|
1080
|
+
* @param {number} reserveForResponse - Tokens to reserve for response (default: 20%)
|
|
1081
|
+
* @returns {Object} Budget helper functions
|
|
1082
|
+
*/
|
|
1083
|
+
function createTokenBudget(contextLimit, reserveForResponse = null) {
|
|
1084
|
+
// Reserve 20% for response by default, minimum 1000 tokens
|
|
1085
|
+
const responseReserve = reserveForResponse || Math.max(1000, Math.floor(contextLimit * 0.2));
|
|
1086
|
+
const promptBudget = contextLimit - responseReserve;
|
|
1087
|
+
|
|
1088
|
+
return {
|
|
1089
|
+
contextLimit,
|
|
1090
|
+
responseReserve,
|
|
1091
|
+
promptBudget,
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Check if prompt fits within budget
|
|
1095
|
+
*/
|
|
1096
|
+
fitsWithin(text) {
|
|
1097
|
+
return estimateTokens(text) <= promptBudget;
|
|
1098
|
+
},
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Get remaining budget after some text
|
|
1102
|
+
*/
|
|
1103
|
+
remaining(text) {
|
|
1104
|
+
return promptBudget - estimateTokens(text);
|
|
1105
|
+
},
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Truncate text to fit budget with optional ellipsis
|
|
1109
|
+
*/
|
|
1110
|
+
truncateToFit(text, targetTokens = promptBudget) {
|
|
1111
|
+
const currentTokens = estimateTokens(text);
|
|
1112
|
+
if (currentTokens <= targetTokens) return text;
|
|
1113
|
+
|
|
1114
|
+
// Truncate to approximate target (4 chars per token)
|
|
1115
|
+
const targetChars = targetTokens * 4;
|
|
1116
|
+
return text.substring(0, targetChars - 50) + '\n\n... (truncated to fit context window)';
|
|
1117
|
+
},
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Get usage summary
|
|
1121
|
+
*/
|
|
1122
|
+
summarize(text) {
|
|
1123
|
+
const used = estimateTokens(text);
|
|
1124
|
+
const percent = Math.round((used / promptBudget) * 100);
|
|
1125
|
+
return {
|
|
1126
|
+
used,
|
|
1127
|
+
budget: promptBudget,
|
|
1128
|
+
remaining: promptBudget - used,
|
|
1129
|
+
percent,
|
|
1130
|
+
overBudget: used > promptBudget,
|
|
1131
|
+
contextLimit
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Initialize token budgeting for a hybrid session
|
|
1139
|
+
* Auto-detects context limit from provider
|
|
1140
|
+
*
|
|
1141
|
+
* @param {Object} config - Hybrid config from config.json
|
|
1142
|
+
* @returns {Promise<Object>} Token budget helper
|
|
1143
|
+
*/
|
|
1144
|
+
async function initializeTokenBudget(config) {
|
|
1145
|
+
const {
|
|
1146
|
+
provider,
|
|
1147
|
+
providerEndpoint,
|
|
1148
|
+
model,
|
|
1149
|
+
maxContextTokens // Optional manual override
|
|
1150
|
+
} = config;
|
|
1151
|
+
|
|
1152
|
+
// Use manual override if provided
|
|
1153
|
+
if (maxContextTokens && maxContextTokens > 0) {
|
|
1154
|
+
console.log(`📊 Using configured context window: ${maxContextTokens.toLocaleString()} tokens`);
|
|
1155
|
+
return createTokenBudget(maxContextTokens);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// Auto-detect from provider
|
|
1159
|
+
const contextLimit = await getModelContextLimit(provider, providerEndpoint, model);
|
|
1160
|
+
console.log(`📊 Detected context window: ${contextLimit.toLocaleString()} tokens`);
|
|
1161
|
+
|
|
1162
|
+
return createTokenBudget(contextLimit);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Module exports
|
|
1166
|
+
module.exports = {
|
|
1167
|
+
PROVIDER_TYPES,
|
|
1168
|
+
DEFAULT_CONFIGS,
|
|
1169
|
+
MODEL_CAPABILITIES,
|
|
1170
|
+
BaseProvider,
|
|
1171
|
+
OllamaProvider,
|
|
1172
|
+
LMStudioProvider,
|
|
1173
|
+
AnthropicProvider,
|
|
1174
|
+
OpenAIProvider,
|
|
1175
|
+
GoogleProvider,
|
|
1176
|
+
createProvider,
|
|
1177
|
+
createExecutorFromConfig,
|
|
1178
|
+
getExecutorConfig,
|
|
1179
|
+
listProviders,
|
|
1180
|
+
detectProviders,
|
|
1181
|
+
loadProviderFromConfig,
|
|
1182
|
+
detectModelCapabilities,
|
|
1183
|
+
getRecommendedModel,
|
|
1184
|
+
|
|
1185
|
+
// Token budgeting
|
|
1186
|
+
getModelContextLimit,
|
|
1187
|
+
estimateTokens,
|
|
1188
|
+
createTokenBudget,
|
|
1189
|
+
initializeTokenBudget
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Format capability level with color
|
|
1194
|
+
*/
|
|
1195
|
+
function formatCapability(level) {
|
|
1196
|
+
switch (level) {
|
|
1197
|
+
case 'excellent': return `${c.green}excellent${c.reset}`;
|
|
1198
|
+
case 'high': return `${c.green}high${c.reset}`;
|
|
1199
|
+
case 'medium': return `${c.yellow}medium${c.reset}`;
|
|
1200
|
+
case 'unknown': return `${c.dim}unknown${c.reset}`;
|
|
1201
|
+
default: return level;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// CLI Handler
|
|
1206
|
+
if (require.main === module) {
|
|
1207
|
+
const args = process.argv.slice(2);
|
|
1208
|
+
const command = args[0];
|
|
1209
|
+
|
|
1210
|
+
async function main() {
|
|
1211
|
+
switch (command) {
|
|
1212
|
+
case 'list': {
|
|
1213
|
+
const providers = listProviders();
|
|
1214
|
+
console.log(`\n${c.cyan}${c.bold}Available Providers${c.reset}\n`);
|
|
1215
|
+
|
|
1216
|
+
for (const p of providers) {
|
|
1217
|
+
const icon = p.local ? '🏠' : '☁️';
|
|
1218
|
+
const keyStatus = p.requiresKey
|
|
1219
|
+
? (process.env[p.envVar] ? `${c.green}✓ API key set${c.reset}` : `${c.yellow}⚠ Requires ${p.envVar}${c.reset}`)
|
|
1220
|
+
: `${c.green}✓ No key required${c.reset}`;
|
|
1221
|
+
|
|
1222
|
+
console.log(`${icon} ${c.bold}${p.name}${c.reset} (${p.type})`);
|
|
1223
|
+
console.log(` ${keyStatus}`);
|
|
1224
|
+
console.log(` Endpoint: ${p.defaultEndpoint}`);
|
|
1225
|
+
console.log('');
|
|
1226
|
+
}
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
case 'detect': {
|
|
1231
|
+
console.log(`\n${c.cyan}Detecting available providers...${c.reset}\n`);
|
|
1232
|
+
const available = await detectProviders();
|
|
1233
|
+
|
|
1234
|
+
if (available.length === 0) {
|
|
1235
|
+
console.log(`${c.yellow}No providers detected.${c.reset}`);
|
|
1236
|
+
console.log(`${c.dim}Make sure Ollama/LM Studio is running, or set API keys.${c.reset}`);
|
|
1237
|
+
} else {
|
|
1238
|
+
for (const p of available) {
|
|
1239
|
+
console.log(`${c.green}✅ ${p.name}${c.reset}`);
|
|
1240
|
+
if (p.models && p.models.length > 0) {
|
|
1241
|
+
console.log(` Models: ${p.models.map(m => m.id).join(', ')}`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
break;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
case 'test': {
|
|
1249
|
+
const providerType = args[1];
|
|
1250
|
+
if (!providerType) {
|
|
1251
|
+
console.error(`${c.red}Error: Provider type required${c.reset}`);
|
|
1252
|
+
console.log(`${c.dim}Usage: flow providers test <ollama|lm-studio|anthropic|openai>${c.reset}`);
|
|
1253
|
+
process.exit(1);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
console.log(`${c.cyan}Testing ${providerType}...${c.reset}`);
|
|
1257
|
+
|
|
1258
|
+
try {
|
|
1259
|
+
const provider = createProvider({ type: providerType });
|
|
1260
|
+
const result = await provider.test();
|
|
1261
|
+
|
|
1262
|
+
if (result.success) {
|
|
1263
|
+
console.log(`${c.green}✅ ${providerType} is working${c.reset}`);
|
|
1264
|
+
console.log(` Response: ${result.response.content.slice(0, 50)}...`);
|
|
1265
|
+
} else {
|
|
1266
|
+
console.log(`${c.red}❌ ${providerType} test failed${c.reset}`);
|
|
1267
|
+
console.log(` Error: ${result.error}`);
|
|
1268
|
+
}
|
|
1269
|
+
} catch (err) {
|
|
1270
|
+
console.log(`${c.red}❌ ${providerType} test failed${c.reset}`);
|
|
1271
|
+
console.log(` Error: ${err.message}`);
|
|
1272
|
+
}
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
case 'capabilities': {
|
|
1277
|
+
const modelName = args[1];
|
|
1278
|
+
|
|
1279
|
+
if (modelName) {
|
|
1280
|
+
// Show capabilities for specific model
|
|
1281
|
+
const caps = detectModelCapabilities(modelName);
|
|
1282
|
+
console.log(`\n${c.cyan}${c.bold}Model Capabilities: ${modelName}${c.reset}\n`);
|
|
1283
|
+
console.log(` Code Quality: ${formatCapability(caps.codeQuality)}`);
|
|
1284
|
+
console.log(` Instruction Following: ${formatCapability(caps.instructionFollowing)}`);
|
|
1285
|
+
console.log(` Context Window: ${caps.contextWindow.toLocaleString()} tokens`);
|
|
1286
|
+
console.log(` Detection Source: ${caps.source}`);
|
|
1287
|
+
if (caps.matchedPattern) {
|
|
1288
|
+
console.log(` Matched Pattern: ${caps.matchedPattern}`);
|
|
1289
|
+
}
|
|
1290
|
+
} else {
|
|
1291
|
+
// List all known capabilities
|
|
1292
|
+
console.log(`\n${c.cyan}${c.bold}Known Model Capabilities${c.reset}\n`);
|
|
1293
|
+
console.log(`${c.dim}Pattern Code Quality Instructions Context${c.reset}`);
|
|
1294
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1295
|
+
|
|
1296
|
+
for (const [pattern, caps] of Object.entries(MODEL_CAPABILITIES)) {
|
|
1297
|
+
const cq = caps.codeQuality.padEnd(12);
|
|
1298
|
+
const inf = caps.instructionFollowing.padEnd(12);
|
|
1299
|
+
const ctx = caps.contextWindow.toLocaleString().padStart(10);
|
|
1300
|
+
console.log(` ${pattern.padEnd(16)} ${cq} ${inf} ${ctx}`);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
console.log('');
|
|
1304
|
+
console.log(`${c.dim}Use "flow providers capabilities <model-name>" to check a specific model${c.reset}`);
|
|
1305
|
+
}
|
|
1306
|
+
break;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
case 'recommend': {
|
|
1310
|
+
console.log(`\n${c.cyan}Finding best model for code tasks...${c.reset}\n`);
|
|
1311
|
+
|
|
1312
|
+
const available = await detectProviders();
|
|
1313
|
+
|
|
1314
|
+
if (available.length === 0) {
|
|
1315
|
+
console.log(`${c.yellow}No providers detected.${c.reset}`);
|
|
1316
|
+
process.exit(1);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
for (const provider of available) {
|
|
1320
|
+
if (provider.models && provider.models.length > 0) {
|
|
1321
|
+
const recommended = getRecommendedModel(provider.models, 'code');
|
|
1322
|
+
if (recommended) {
|
|
1323
|
+
console.log(`${c.green}${provider.name}:${c.reset} ${recommended.id || recommended.name}`);
|
|
1324
|
+
const caps = recommended.capabilities;
|
|
1325
|
+
console.log(` Code: ${caps.codeQuality} | Instructions: ${caps.instructionFollowing}`);
|
|
1326
|
+
console.log(` Context: ${caps.contextWindow.toLocaleString()} tokens | Score: ${recommended.score}`);
|
|
1327
|
+
console.log('');
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
break;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
default: {
|
|
1335
|
+
console.log(`
|
|
1336
|
+
${c.cyan}Wogi Flow - Model Providers${c.reset}
|
|
1337
|
+
|
|
1338
|
+
${c.bold}Usage:${c.reset}
|
|
1339
|
+
flow providers list List all available providers
|
|
1340
|
+
flow providers detect Detect running local providers
|
|
1341
|
+
flow providers test <type> Test a provider connection
|
|
1342
|
+
flow providers capabilities List known model capabilities
|
|
1343
|
+
flow providers capabilities <model> Show capabilities for a model
|
|
1344
|
+
flow providers recommend Find best model for code tasks
|
|
1345
|
+
|
|
1346
|
+
${c.bold}Supported Providers:${c.reset}
|
|
1347
|
+
ollama Local Ollama instance
|
|
1348
|
+
lm-studio Local LM Studio instance
|
|
1349
|
+
anthropic Anthropic API (requires ANTHROPIC_API_KEY)
|
|
1350
|
+
openai OpenAI API (requires OPENAI_API_KEY)
|
|
1351
|
+
|
|
1352
|
+
${c.bold}Model Capabilities:${c.reset}
|
|
1353
|
+
The system detects model capabilities using:
|
|
1354
|
+
1. API queries (when available, e.g., Ollama /api/show)
|
|
1355
|
+
2. Heuristics based on model name patterns
|
|
1356
|
+
|
|
1357
|
+
Capabilities tracked:
|
|
1358
|
+
- Code Quality: How well the model generates code
|
|
1359
|
+
- Instruction Following: How well it follows prompts
|
|
1360
|
+
- Context Window: Maximum tokens supported
|
|
1361
|
+
|
|
1362
|
+
${c.bold}Configuration:${c.reset}
|
|
1363
|
+
Set in .workflow/config.json under "hybrid":
|
|
1364
|
+
{
|
|
1365
|
+
"hybrid": {
|
|
1366
|
+
"enabled": true,
|
|
1367
|
+
"provider": "anthropic",
|
|
1368
|
+
"model": "claude-sonnet-4-20250514",
|
|
1369
|
+
"apiKey": "sk-..." // or use environment variable
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
main().catch(err => {
|
|
1378
|
+
console.error(`${c.red}Error: ${err.message}${c.reset}`);
|
|
1379
|
+
process.exit(1);
|
|
1380
|
+
});
|
|
1381
|
+
}
|