wogiflow 1.0.31 → 1.0.32
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/.claude/commands/wogi-hybrid.md +28 -2
- package/.claude/commands/wogi-models-setup.md +254 -0
- package/.claude/commands/wogi-peer-review.md +123 -22
- package/package.json +1 -1
- package/scripts/flow-model-caller.js +46 -2
- package/scripts/flow-model-config.js +649 -0
- package/scripts/hooks/core/implementation-gate.js +25 -21
|
@@ -14,14 +14,40 @@ Let me check what's available on your system:
|
|
|
14
14
|
node scripts/flow-hybrid-detect.js providers
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
## Step 2:
|
|
17
|
+
## Step 2: Choose Setup Method
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
### Option A: Use Unified Model Setup (Recommended)
|
|
20
|
+
|
|
21
|
+
If you want to configure multiple providers at once (for both hybrid and peer review):
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
/wogi-models-setup
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This configures all your models in one place.
|
|
28
|
+
|
|
29
|
+
### Option B: Hybrid-Specific Setup
|
|
30
|
+
|
|
31
|
+
For hybrid-specific configuration only:
|
|
20
32
|
|
|
21
33
|
```bash
|
|
22
34
|
node scripts/flow-hybrid-interactive.js
|
|
23
35
|
```
|
|
24
36
|
|
|
37
|
+
### Model Selection
|
|
38
|
+
|
|
39
|
+
If models are already configured (via `/wogi-models-setup`), you can select which one to use:
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
const modelConfig = require('./scripts/flow-model-config');
|
|
43
|
+
const models = modelConfig.getEnabledModels();
|
|
44
|
+
|
|
45
|
+
// Show selection if multiple models available
|
|
46
|
+
if (models.length > 1) {
|
|
47
|
+
// Use AskUserQuestion to let user select
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
25
51
|
## How Hybrid Mode Works
|
|
26
52
|
|
|
27
53
|
1. **You give me a task** - "Add user authentication"
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
Configure external models for WogiFlow features (peer review, hybrid mode).
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This wizard helps you set up external LLM providers:
|
|
6
|
+
- **OpenAI** (GPT-4o, o1)
|
|
7
|
+
- **Google** (Gemini)
|
|
8
|
+
- **Anthropic** (Claude - for peer review)
|
|
9
|
+
- **Local LLM** (Ollama, LM Studio)
|
|
10
|
+
|
|
11
|
+
Configured models are shared between:
|
|
12
|
+
- `/wogi-peer-review` - Multi-model code review
|
|
13
|
+
- `/wogi-hybrid` - Local/cloud execution mode
|
|
14
|
+
|
|
15
|
+
## Setup Flow
|
|
16
|
+
|
|
17
|
+
### Step 1: Provider Selection
|
|
18
|
+
|
|
19
|
+
Use AskUserQuestion to let user select which providers to configure:
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
{
|
|
23
|
+
question: "Which AI providers do you want to configure?",
|
|
24
|
+
header: "Providers",
|
|
25
|
+
multiSelect: true,
|
|
26
|
+
options: [
|
|
27
|
+
{ label: "OpenAI", description: "GPT-4o, GPT-4o-mini, o1 models" },
|
|
28
|
+
{ label: "Google (Gemini)", description: "Gemini 2.0 Flash, Gemini Pro" },
|
|
29
|
+
{ label: "Anthropic", description: "Claude models (for peer review comparison)" },
|
|
30
|
+
{ label: "Local LLM", description: "Ollama or LM Studio (free, runs on your machine)" }
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Step 2: Configure Each Selected Provider
|
|
36
|
+
|
|
37
|
+
For each selected provider, run the appropriate setup:
|
|
38
|
+
|
|
39
|
+
#### OpenAI Setup
|
|
40
|
+
|
|
41
|
+
1. Ask for API key:
|
|
42
|
+
```
|
|
43
|
+
Please paste your OpenAI API key (starts with sk-):
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
2. Test connection:
|
|
47
|
+
```bash
|
|
48
|
+
node -e "require('./scripts/flow-model-config').testProviderConnection('openai').then(r => console.log(JSON.stringify(r)))"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
3. If successful, ask which models to enable:
|
|
52
|
+
```javascript
|
|
53
|
+
{
|
|
54
|
+
question: "Which OpenAI models do you want to enable?",
|
|
55
|
+
header: "OpenAI Models",
|
|
56
|
+
multiSelect: true,
|
|
57
|
+
options: [
|
|
58
|
+
{ label: "gpt-4o (Recommended)", description: "Best quality, good speed" },
|
|
59
|
+
{ label: "gpt-4o-mini", description: "Faster, cheaper, still capable" },
|
|
60
|
+
{ label: "o1-mini", description: "Advanced reasoning, slower" }
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
4. Save configuration:
|
|
66
|
+
```javascript
|
|
67
|
+
const modelConfig = require('./scripts/flow-model-config');
|
|
68
|
+
modelConfig.addProvider('openai', {
|
|
69
|
+
apiKey: userProvidedKey,
|
|
70
|
+
models: selectedModels
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Google (Gemini) Setup
|
|
75
|
+
|
|
76
|
+
1. Ask for API key:
|
|
77
|
+
```
|
|
78
|
+
Please paste your Google AI API key (get one at https://aistudio.google.com/apikey):
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
2. Test and select models (similar to OpenAI):
|
|
82
|
+
- gemini-2.0-flash-exp (Recommended)
|
|
83
|
+
- gemini-1.5-flash
|
|
84
|
+
- gemini-1.5-pro
|
|
85
|
+
|
|
86
|
+
#### Anthropic Setup
|
|
87
|
+
|
|
88
|
+
1. Ask for API key:
|
|
89
|
+
```
|
|
90
|
+
Please paste your Anthropic API key (starts with sk-ant-):
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
2. Select models:
|
|
94
|
+
- claude-sonnet-4 (Recommended)
|
|
95
|
+
- claude-3-5-haiku
|
|
96
|
+
- claude-opus-4
|
|
97
|
+
|
|
98
|
+
Note: Anthropic models are mainly useful for peer review to get different perspectives.
|
|
99
|
+
|
|
100
|
+
#### Local LLM Setup
|
|
101
|
+
|
|
102
|
+
1. Auto-detect local providers:
|
|
103
|
+
```bash
|
|
104
|
+
# Test Ollama
|
|
105
|
+
curl -s http://localhost:11434/api/tags 2>/dev/null | head -1
|
|
106
|
+
|
|
107
|
+
# Test LM Studio
|
|
108
|
+
curl -s http://localhost:1234/v1/models 2>/dev/null | head -1
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
2. If detected, list available models and let user select.
|
|
112
|
+
|
|
113
|
+
3. If not detected, show instructions:
|
|
114
|
+
```
|
|
115
|
+
No local LLM detected. To use local models:
|
|
116
|
+
|
|
117
|
+
Option 1: Install Ollama
|
|
118
|
+
1. Visit https://ollama.ai
|
|
119
|
+
2. Install and run: ollama pull qwen2.5-coder:7b
|
|
120
|
+
3. Re-run /wogi-models-setup
|
|
121
|
+
|
|
122
|
+
Option 2: Install LM Studio
|
|
123
|
+
1. Visit https://lmstudio.ai
|
|
124
|
+
2. Download and load a model
|
|
125
|
+
3. Start the server
|
|
126
|
+
4. Re-run /wogi-models-setup
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Step 3: Set Defaults (Optional)
|
|
130
|
+
|
|
131
|
+
Ask user about default preferences:
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
{
|
|
135
|
+
question: "Which models should be used by default for peer review?",
|
|
136
|
+
header: "Peer Review Default",
|
|
137
|
+
multiSelect: true,
|
|
138
|
+
options: [
|
|
139
|
+
// Show only configured models
|
|
140
|
+
{ label: "openai:gpt-4o", description: "..." },
|
|
141
|
+
{ label: "google:gemini-2.0-flash", description: "..." }
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Step 4: Summary
|
|
147
|
+
|
|
148
|
+
Display configuration summary:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
╔══════════════════════════════════════════════════════════╗
|
|
152
|
+
║ Model Configuration Complete ║
|
|
153
|
+
╚══════════════════════════════════════════════════════════╝
|
|
154
|
+
|
|
155
|
+
Configured Providers:
|
|
156
|
+
✓ OpenAI: gpt-4o, gpt-4o-mini
|
|
157
|
+
✓ Google: gemini-2.0-flash
|
|
158
|
+
✓ Local: qwen2.5-coder (via Ollama)
|
|
159
|
+
|
|
160
|
+
API Keys stored in: .env
|
|
161
|
+
Config saved to: .workflow/config.json
|
|
162
|
+
|
|
163
|
+
Default for peer review: gpt-4o, gemini-2.0-flash
|
|
164
|
+
Default for hybrid mode: local:qwen2.5-coder
|
|
165
|
+
|
|
166
|
+
You can now use:
|
|
167
|
+
/wogi-review - Multi-model code review
|
|
168
|
+
/wogi-hybrid - Hybrid execution mode
|
|
169
|
+
/wogi-peer-review - Same as /wogi-review
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Implementation Details
|
|
173
|
+
|
|
174
|
+
### Config Storage
|
|
175
|
+
|
|
176
|
+
API keys are stored as environment variable names (not values) in config:
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"models": {
|
|
180
|
+
"providers": {
|
|
181
|
+
"openai": {
|
|
182
|
+
"apiKeyEnv": "OPENAI_API_KEY",
|
|
183
|
+
"enabled": true,
|
|
184
|
+
"models": ["gpt-4o", "gpt-4o-mini"]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Actual keys go in `.env`:
|
|
192
|
+
```
|
|
193
|
+
OPENAI_API_KEY=sk-proj-...
|
|
194
|
+
GOOGLE_API_KEY=AIza...
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Migration
|
|
198
|
+
|
|
199
|
+
If old config exists (hybrid.executor or peerReview.apiKeys), migrate automatically:
|
|
200
|
+
```javascript
|
|
201
|
+
const modelConfig = require('./scripts/flow-model-config');
|
|
202
|
+
modelConfig.migrateOldConfig();
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Testing
|
|
206
|
+
|
|
207
|
+
Test any provider connection:
|
|
208
|
+
```bash
|
|
209
|
+
node scripts/flow-model-config.js test openai
|
|
210
|
+
node scripts/flow-model-config.js test local
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Error Handling
|
|
214
|
+
|
|
215
|
+
### Invalid API Key
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
✗ OpenAI connection failed: Invalid API key
|
|
219
|
+
|
|
220
|
+
Please check your API key and try again.
|
|
221
|
+
You can get a new key at: https://platform.openai.com/api-keys
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### No Local LLM
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
✗ No local LLM detected
|
|
228
|
+
|
|
229
|
+
Install Ollama (recommended): https://ollama.ai
|
|
230
|
+
Then run: ollama pull qwen2.5-coder:7b
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Network Error
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
✗ Connection failed: Network error
|
|
237
|
+
|
|
238
|
+
Please check your internet connection and try again.
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Quick Setup (Non-Interactive)
|
|
242
|
+
|
|
243
|
+
For users who already have API keys set in environment:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
# If OPENAI_API_KEY is already in environment
|
|
247
|
+
node -e "
|
|
248
|
+
const mc = require('./scripts/flow-model-config');
|
|
249
|
+
if (process.env.OPENAI_API_KEY) {
|
|
250
|
+
mc.addProvider('openai', { models: ['gpt-4o'] });
|
|
251
|
+
console.log('OpenAI configured');
|
|
252
|
+
}
|
|
253
|
+
"
|
|
254
|
+
```
|
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
Run a multi-model peer review where different AI models review the same code.
|
|
2
2
|
|
|
3
|
+
## Step 0: Model Selection (Every Run)
|
|
4
|
+
|
|
5
|
+
**Before starting the review, check for configured models and let user select:**
|
|
6
|
+
|
|
7
|
+
### Check Configuration
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const modelConfig = require('./scripts/flow-model-config');
|
|
11
|
+
|
|
12
|
+
// Run migration if needed (handles old config formats)
|
|
13
|
+
modelConfig.migrateOldConfig();
|
|
14
|
+
|
|
15
|
+
// Get enabled models
|
|
16
|
+
const models = modelConfig.getEnabledModels();
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### If No Models Configured
|
|
20
|
+
|
|
21
|
+
If `models.length === 0`:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
No external models configured for peer review.
|
|
25
|
+
|
|
26
|
+
Run /wogi-models-setup to configure:
|
|
27
|
+
- OpenAI (GPT-4o)
|
|
28
|
+
- Google (Gemini)
|
|
29
|
+
- Local LLM (Ollama)
|
|
30
|
+
|
|
31
|
+
Or use --manual flag for manual mode.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then either:
|
|
35
|
+
- Auto-launch `/wogi-models-setup` wizard
|
|
36
|
+
- Or use `--manual` mode if user prefers
|
|
37
|
+
|
|
38
|
+
### Model Selection Dialog
|
|
39
|
+
|
|
40
|
+
If models are configured, show selection dialog using AskUserQuestion:
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
{
|
|
44
|
+
question: "Select models for peer review (multiple allowed):",
|
|
45
|
+
header: "Models",
|
|
46
|
+
multiSelect: true,
|
|
47
|
+
options: [
|
|
48
|
+
// Dynamically populated from configured models
|
|
49
|
+
{ label: "openai:gpt-4o", description: "Best quality reasoning" },
|
|
50
|
+
{ label: "openai:gpt-4o-mini", description: "Faster, cheaper" },
|
|
51
|
+
{ label: "google:gemini-2.0-flash", description: "Fast, good at code" },
|
|
52
|
+
{ label: "local:qwen2.5-coder", description: "Free, runs locally" }
|
|
53
|
+
// ... other configured models
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Show only models that:**
|
|
59
|
+
1. Are configured in `models.providers`
|
|
60
|
+
2. Have `enabled: true`
|
|
61
|
+
3. Have API key set (check `process.env[apiKeyEnv]`) or are local
|
|
62
|
+
|
|
63
|
+
### After Selection
|
|
64
|
+
|
|
65
|
+
Save the selection for future runs (optional):
|
|
66
|
+
```javascript
|
|
67
|
+
modelConfig.setDefaultModels('peerReview', selectedModels);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Then proceed with the review using selected models.
|
|
71
|
+
|
|
3
72
|
## How It Works
|
|
4
73
|
|
|
5
74
|
1. **Primary model (Claude)** reviews the changes for improvements
|
|
@@ -37,41 +106,73 @@ Run a multi-model peer review where different AI models review the same code.
|
|
|
37
106
|
|
|
38
107
|
## Provider Configuration
|
|
39
108
|
|
|
40
|
-
|
|
109
|
+
### Recommended: Use `/wogi-models-setup`
|
|
41
110
|
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
"enabled": true,
|
|
46
|
-
"provider": "api",
|
|
47
|
-
"models": ["openai:gpt-4o", "google:gemini-pro"],
|
|
48
|
-
"apiKeys": {
|
|
49
|
-
"openai": "${OPENAI_API_KEY}",
|
|
50
|
-
"google": "${GOOGLE_API_KEY}"
|
|
51
|
-
}
|
|
52
|
-
}
|
|
111
|
+
The easiest way to configure models is the setup wizard:
|
|
112
|
+
```
|
|
113
|
+
/wogi-models-setup
|
|
53
114
|
```
|
|
54
115
|
|
|
55
|
-
|
|
116
|
+
This creates a unified configuration used by both peer review and hybrid mode.
|
|
117
|
+
|
|
118
|
+
### Config Location
|
|
119
|
+
|
|
120
|
+
Models are configured in `.workflow/config.json` under `models`:
|
|
121
|
+
|
|
56
122
|
```json
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
123
|
+
{
|
|
124
|
+
"models": {
|
|
125
|
+
"providers": {
|
|
126
|
+
"openai": {
|
|
127
|
+
"apiKeyEnv": "OPENAI_API_KEY",
|
|
128
|
+
"enabled": true,
|
|
129
|
+
"models": ["gpt-4o", "gpt-4o-mini"]
|
|
130
|
+
},
|
|
131
|
+
"google": {
|
|
132
|
+
"apiKeyEnv": "GOOGLE_API_KEY",
|
|
133
|
+
"enabled": true,
|
|
134
|
+
"models": ["gemini-2.0-flash"]
|
|
135
|
+
},
|
|
136
|
+
"local": {
|
|
137
|
+
"endpoint": "http://localhost:11434",
|
|
138
|
+
"provider": "ollama",
|
|
139
|
+
"enabled": true,
|
|
140
|
+
"models": ["qwen2.5-coder"]
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"defaults": {
|
|
144
|
+
"peerReview": ["openai:gpt-4o", "google:gemini-2.0-flash"]
|
|
145
|
+
}
|
|
63
146
|
}
|
|
64
147
|
}
|
|
65
148
|
```
|
|
66
149
|
|
|
67
|
-
|
|
150
|
+
API keys are stored in `.env` (not in config):
|
|
151
|
+
```
|
|
152
|
+
OPENAI_API_KEY=sk-proj-...
|
|
153
|
+
GOOGLE_API_KEY=AIza...
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Legacy Config (Auto-Migrated)
|
|
157
|
+
|
|
158
|
+
Old format configs are automatically migrated on first use:
|
|
68
159
|
```json
|
|
160
|
+
// Old format (still supported, auto-migrates)
|
|
69
161
|
"peerReview": {
|
|
70
|
-
"
|
|
71
|
-
|
|
162
|
+
"apiKeys": {
|
|
163
|
+
"openai": "${OPENAI_API_KEY}"
|
|
164
|
+
},
|
|
165
|
+
"models": ["openai:gpt-4o"]
|
|
72
166
|
}
|
|
73
167
|
```
|
|
74
168
|
|
|
169
|
+
### Manual Mode
|
|
170
|
+
|
|
171
|
+
For manual review (no API keys needed):
|
|
172
|
+
```
|
|
173
|
+
/wogi-peer-review --manual
|
|
174
|
+
```
|
|
175
|
+
|
|
75
176
|
When manual:
|
|
76
177
|
1. Outputs the review prompt
|
|
77
178
|
2. User runs in Cursor/other tool
|
package/package.json
CHANGED
|
@@ -260,9 +260,34 @@ async function callModel(modelString, prompt, options = {}) {
|
|
|
260
260
|
|
|
261
261
|
/**
|
|
262
262
|
* Get list of configured models for peer review
|
|
263
|
+
* Checks unified config first, falls back to legacy peerReview config
|
|
263
264
|
*/
|
|
264
265
|
function getConfiguredModels() {
|
|
265
266
|
const config = getConfig();
|
|
267
|
+
|
|
268
|
+
// Check unified models config first (new format)
|
|
269
|
+
if (config.models?.providers) {
|
|
270
|
+
const models = [];
|
|
271
|
+
for (const [provider, providerConfig] of Object.entries(config.models.providers)) {
|
|
272
|
+
if (providerConfig.enabled !== false && providerConfig.models?.length > 0) {
|
|
273
|
+
// Check if API key is available (or local provider)
|
|
274
|
+
const apiKeyEnv = providerConfig.apiKeyEnv;
|
|
275
|
+
const hasApiKey = !apiKeyEnv || process.env[apiKeyEnv];
|
|
276
|
+
|
|
277
|
+
if (hasApiKey || provider === 'local') {
|
|
278
|
+
for (const model of providerConfig.models) {
|
|
279
|
+
models.push(`${provider}:${model}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (models.length > 0) {
|
|
286
|
+
return models;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Fall back to legacy peerReview config
|
|
266
291
|
const peerReviewConfig = config.peerReview || {};
|
|
267
292
|
|
|
268
293
|
if (peerReviewConfig.models && Array.isArray(peerReviewConfig.models)) {
|
|
@@ -274,6 +299,7 @@ function getConfiguredModels() {
|
|
|
274
299
|
|
|
275
300
|
/**
|
|
276
301
|
* Check if model calling is available
|
|
302
|
+
* Checks unified config first, falls back to legacy peerReview config
|
|
277
303
|
*/
|
|
278
304
|
function isModelCallingAvailable() {
|
|
279
305
|
const config = getConfig();
|
|
@@ -284,7 +310,25 @@ function isModelCallingAvailable() {
|
|
|
284
310
|
return { available: false, reason: 'Manual mode configured' };
|
|
285
311
|
}
|
|
286
312
|
|
|
287
|
-
// Check
|
|
313
|
+
// Check unified config first
|
|
314
|
+
if (config.models?.providers) {
|
|
315
|
+
for (const [provider, providerConfig] of Object.entries(config.models.providers)) {
|
|
316
|
+
if (providerConfig.enabled !== false) {
|
|
317
|
+
// Local providers don't need API keys
|
|
318
|
+
if (provider === 'local') {
|
|
319
|
+
return { available: true };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Cloud providers need API keys
|
|
323
|
+
const apiKeyEnv = providerConfig.apiKeyEnv;
|
|
324
|
+
if (apiKeyEnv && process.env[apiKeyEnv]) {
|
|
325
|
+
return { available: true };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Fall back to legacy peerReview config
|
|
288
332
|
const models = getConfiguredModels();
|
|
289
333
|
|
|
290
334
|
for (const modelStr of models) {
|
|
@@ -301,7 +345,7 @@ function isModelCallingAvailable() {
|
|
|
301
345
|
|
|
302
346
|
return {
|
|
303
347
|
available: false,
|
|
304
|
-
reason: 'No
|
|
348
|
+
reason: 'No models configured. Run /wogi-models-setup to configure external models.'
|
|
305
349
|
};
|
|
306
350
|
}
|
|
307
351
|
|
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Unified Model Configuration
|
|
5
|
+
*
|
|
6
|
+
* Centralized model and API key management for all WogiFlow features:
|
|
7
|
+
* - Hybrid mode (local LLM execution)
|
|
8
|
+
* - Peer review (multi-model code review)
|
|
9
|
+
* - Model routing (task-based model selection)
|
|
10
|
+
*
|
|
11
|
+
* Config structure in .workflow/config.json:
|
|
12
|
+
* {
|
|
13
|
+
* "models": {
|
|
14
|
+
* "providers": {
|
|
15
|
+
* "openai": { "apiKeyEnv": "OPENAI_API_KEY", "enabled": true, "models": ["gpt-4o"] },
|
|
16
|
+
* "google": { "apiKeyEnv": "GOOGLE_API_KEY", "enabled": true, "models": ["gemini-2.0-flash"] },
|
|
17
|
+
* ...
|
|
18
|
+
* },
|
|
19
|
+
* "defaults": {
|
|
20
|
+
* "hybrid": "local:qwen2.5-coder",
|
|
21
|
+
* "peerReview": ["openai:gpt-4o", "google:gemini-2.0-flash"]
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* const modelConfig = require('./flow-model-config');
|
|
28
|
+
* const models = modelConfig.getEnabledModels();
|
|
29
|
+
* await modelConfig.addProvider('openai', { apiKey: 'sk-...' });
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
const { getProjectRoot, safeJsonParse, colors: c } = require('./flow-utils');
|
|
35
|
+
|
|
36
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
37
|
+
const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
|
|
38
|
+
const CONFIG_PATH = path.join(WORKFLOW_DIR, 'config.json');
|
|
39
|
+
const ENV_PATH = path.join(PROJECT_ROOT, '.env');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Known provider configurations
|
|
43
|
+
*/
|
|
44
|
+
const KNOWN_PROVIDERS = {
|
|
45
|
+
openai: {
|
|
46
|
+
displayName: 'OpenAI',
|
|
47
|
+
envKey: 'OPENAI_API_KEY',
|
|
48
|
+
endpoint: 'https://api.openai.com/v1',
|
|
49
|
+
testEndpoint: '/models',
|
|
50
|
+
models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-mini', 'o1-preview'],
|
|
51
|
+
defaultModel: 'gpt-4o'
|
|
52
|
+
},
|
|
53
|
+
google: {
|
|
54
|
+
displayName: 'Google (Gemini)',
|
|
55
|
+
envKey: 'GOOGLE_API_KEY',
|
|
56
|
+
endpoint: 'https://generativelanguage.googleapis.com/v1beta',
|
|
57
|
+
testEndpoint: '/models',
|
|
58
|
+
models: ['gemini-2.0-flash-exp', 'gemini-1.5-flash', 'gemini-1.5-pro', 'gemini-pro'],
|
|
59
|
+
defaultModel: 'gemini-2.0-flash-exp'
|
|
60
|
+
},
|
|
61
|
+
anthropic: {
|
|
62
|
+
displayName: 'Anthropic',
|
|
63
|
+
envKey: 'ANTHROPIC_API_KEY',
|
|
64
|
+
endpoint: 'https://api.anthropic.com/v1',
|
|
65
|
+
testEndpoint: '/messages',
|
|
66
|
+
models: ['claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022', 'claude-opus-4-20250514'],
|
|
67
|
+
defaultModel: 'claude-sonnet-4-20250514'
|
|
68
|
+
},
|
|
69
|
+
local: {
|
|
70
|
+
displayName: 'Local LLM',
|
|
71
|
+
envKey: null, // No API key needed
|
|
72
|
+
endpoint: 'http://localhost:11434',
|
|
73
|
+
testEndpoint: '/api/tags',
|
|
74
|
+
models: [], // Detected at runtime
|
|
75
|
+
defaultModel: null,
|
|
76
|
+
subProviders: ['ollama', 'lmstudio']
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read config file
|
|
82
|
+
* @returns {Object} Config object
|
|
83
|
+
*/
|
|
84
|
+
function readConfig() {
|
|
85
|
+
return safeJsonParse(CONFIG_PATH, {});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Write config file
|
|
90
|
+
* @param {Object} config - Config to write
|
|
91
|
+
*/
|
|
92
|
+
function writeConfig(config) {
|
|
93
|
+
fs.mkdirSync(WORKFLOW_DIR, { recursive: true });
|
|
94
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the models configuration section
|
|
99
|
+
* @returns {Object} Models config or empty structure
|
|
100
|
+
*/
|
|
101
|
+
function getModelsConfig() {
|
|
102
|
+
const config = readConfig();
|
|
103
|
+
return config.models || { providers: {}, defaults: {} };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Update models configuration
|
|
108
|
+
* @param {Object} modelsConfig - New models config
|
|
109
|
+
*/
|
|
110
|
+
function updateModelsConfig(modelsConfig) {
|
|
111
|
+
const config = readConfig();
|
|
112
|
+
config.models = modelsConfig;
|
|
113
|
+
writeConfig(config);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get all configured providers
|
|
118
|
+
* @returns {Array<{name: string, displayName: string, enabled: boolean, models: string[], apiKeySet: boolean}>}
|
|
119
|
+
*/
|
|
120
|
+
function getConfiguredProviders() {
|
|
121
|
+
const modelsConfig = getModelsConfig();
|
|
122
|
+
const providers = [];
|
|
123
|
+
|
|
124
|
+
for (const [name, providerConfig] of Object.entries(modelsConfig.providers || {})) {
|
|
125
|
+
const knownProvider = KNOWN_PROVIDERS[name] || {};
|
|
126
|
+
const apiKeyEnv = providerConfig.apiKeyEnv || knownProvider.envKey;
|
|
127
|
+
const apiKeySet = apiKeyEnv ? !!process.env[apiKeyEnv] : true;
|
|
128
|
+
|
|
129
|
+
providers.push({
|
|
130
|
+
name,
|
|
131
|
+
displayName: knownProvider.displayName || name,
|
|
132
|
+
enabled: providerConfig.enabled !== false,
|
|
133
|
+
models: providerConfig.models || [],
|
|
134
|
+
apiKeyEnv,
|
|
135
|
+
apiKeySet,
|
|
136
|
+
endpoint: providerConfig.endpoint || knownProvider.endpoint
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return providers;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get all enabled models in provider:model format
|
|
145
|
+
* @returns {string[]} Array of "provider:model" strings
|
|
146
|
+
*/
|
|
147
|
+
function getEnabledModels() {
|
|
148
|
+
const providers = getConfiguredProviders();
|
|
149
|
+
const models = [];
|
|
150
|
+
|
|
151
|
+
for (const provider of providers) {
|
|
152
|
+
if (provider.enabled && provider.apiKeySet) {
|
|
153
|
+
for (const model of provider.models) {
|
|
154
|
+
models.push(`${provider.name}:${model}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return models;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Add or update a provider configuration
|
|
164
|
+
* @param {string} providerName - Provider name (openai, google, anthropic, local)
|
|
165
|
+
* @param {Object} options - Provider options
|
|
166
|
+
* @param {string} [options.apiKey] - API key (will be stored in .env)
|
|
167
|
+
* @param {string[]} [options.models] - Models to enable
|
|
168
|
+
* @param {string} [options.endpoint] - Custom endpoint
|
|
169
|
+
* @param {boolean} [options.enabled] - Enable/disable provider
|
|
170
|
+
*/
|
|
171
|
+
function addProvider(providerName, options = {}) {
|
|
172
|
+
const modelsConfig = getModelsConfig();
|
|
173
|
+
const knownProvider = KNOWN_PROVIDERS[providerName];
|
|
174
|
+
|
|
175
|
+
if (!modelsConfig.providers) {
|
|
176
|
+
modelsConfig.providers = {};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Merge with existing config
|
|
180
|
+
const existingConfig = modelsConfig.providers[providerName] || {};
|
|
181
|
+
const newConfig = {
|
|
182
|
+
...existingConfig,
|
|
183
|
+
enabled: options.enabled !== undefined ? options.enabled : true,
|
|
184
|
+
models: options.models || existingConfig.models || (knownProvider?.models?.slice(0, 2) || []),
|
|
185
|
+
apiKeyEnv: knownProvider?.envKey || existingConfig.apiKeyEnv
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (options.endpoint) {
|
|
189
|
+
newConfig.endpoint = options.endpoint;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Store API key in .env if provided
|
|
193
|
+
if (options.apiKey && knownProvider?.envKey) {
|
|
194
|
+
updateEnvFile(knownProvider.envKey, options.apiKey);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
modelsConfig.providers[providerName] = newConfig;
|
|
198
|
+
updateModelsConfig(modelsConfig);
|
|
199
|
+
|
|
200
|
+
return newConfig;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Remove/disable a provider
|
|
205
|
+
* @param {string} providerName - Provider name
|
|
206
|
+
*/
|
|
207
|
+
function removeProvider(providerName) {
|
|
208
|
+
const modelsConfig = getModelsConfig();
|
|
209
|
+
|
|
210
|
+
if (modelsConfig.providers && modelsConfig.providers[providerName]) {
|
|
211
|
+
modelsConfig.providers[providerName].enabled = false;
|
|
212
|
+
updateModelsConfig(modelsConfig);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Update .env file with API key
|
|
218
|
+
* @param {string} keyName - Environment variable name
|
|
219
|
+
* @param {string} keyValue - API key value
|
|
220
|
+
*/
|
|
221
|
+
function updateEnvFile(keyName, keyValue) {
|
|
222
|
+
let envContent = '';
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
envContent = fs.readFileSync(ENV_PATH, 'utf-8');
|
|
226
|
+
} catch (err) {
|
|
227
|
+
// .env doesn't exist, will create
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Parse existing env vars
|
|
231
|
+
const lines = envContent.split('\n');
|
|
232
|
+
const envVars = {};
|
|
233
|
+
const comments = [];
|
|
234
|
+
|
|
235
|
+
for (const line of lines) {
|
|
236
|
+
if (line.startsWith('#') || line.trim() === '') {
|
|
237
|
+
comments.push(line);
|
|
238
|
+
} else {
|
|
239
|
+
const [key, ...valueParts] = line.split('=');
|
|
240
|
+
if (key) {
|
|
241
|
+
envVars[key.trim()] = valueParts.join('=').trim();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Update or add the key
|
|
247
|
+
envVars[keyName] = keyValue;
|
|
248
|
+
|
|
249
|
+
// Rebuild .env content
|
|
250
|
+
let newContent = '';
|
|
251
|
+
|
|
252
|
+
// Add header comment if new file
|
|
253
|
+
if (!envContent) {
|
|
254
|
+
newContent = '# WogiFlow API Keys\n# Generated by /wogi-models-setup\n\n';
|
|
255
|
+
} else if (comments.length > 0) {
|
|
256
|
+
newContent = comments.join('\n') + '\n';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Add env vars
|
|
260
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
261
|
+
newContent += `${key}=${value}\n`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fs.writeFileSync(ENV_PATH, newContent);
|
|
265
|
+
|
|
266
|
+
// Also set in current process
|
|
267
|
+
process.env[keyName] = keyValue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Test connection to a provider
|
|
272
|
+
* @param {string} providerName - Provider name
|
|
273
|
+
* @returns {Promise<{success: boolean, message: string, models?: string[]}>}
|
|
274
|
+
*/
|
|
275
|
+
async function testProviderConnection(providerName) {
|
|
276
|
+
const modelsConfig = getModelsConfig();
|
|
277
|
+
const providerConfig = modelsConfig.providers?.[providerName] || {};
|
|
278
|
+
const knownProvider = KNOWN_PROVIDERS[providerName];
|
|
279
|
+
|
|
280
|
+
if (!knownProvider) {
|
|
281
|
+
return { success: false, message: `Unknown provider: ${providerName}` };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const endpoint = providerConfig.endpoint || knownProvider.endpoint;
|
|
285
|
+
const apiKeyEnv = providerConfig.apiKeyEnv || knownProvider.envKey;
|
|
286
|
+
const apiKey = apiKeyEnv ? process.env[apiKeyEnv] : null;
|
|
287
|
+
|
|
288
|
+
// Local provider detection
|
|
289
|
+
if (providerName === 'local') {
|
|
290
|
+
return testLocalProvider(endpoint);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Cloud provider test
|
|
294
|
+
if (!apiKey) {
|
|
295
|
+
return { success: false, message: `API key not set (${apiKeyEnv})` };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const result = await testCloudProvider(providerName, endpoint, apiKey);
|
|
300
|
+
return result;
|
|
301
|
+
} catch (err) {
|
|
302
|
+
return { success: false, message: err.message };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Test local LLM provider (Ollama/LM Studio)
|
|
308
|
+
*/
|
|
309
|
+
async function testLocalProvider(endpoint) {
|
|
310
|
+
const https = require('https');
|
|
311
|
+
const http = require('http');
|
|
312
|
+
|
|
313
|
+
return new Promise((resolve) => {
|
|
314
|
+
const url = new URL('/api/tags', endpoint);
|
|
315
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
316
|
+
|
|
317
|
+
const req = client.request(url, { method: 'GET', timeout: 5000 }, (res) => {
|
|
318
|
+
let data = '';
|
|
319
|
+
res.on('data', chunk => data += chunk);
|
|
320
|
+
res.on('end', () => {
|
|
321
|
+
try {
|
|
322
|
+
const parsed = JSON.parse(data);
|
|
323
|
+
const models = parsed.models?.map(m => m.name) || [];
|
|
324
|
+
resolve({
|
|
325
|
+
success: true,
|
|
326
|
+
message: `Connected to Ollama. Found ${models.length} models.`,
|
|
327
|
+
models
|
|
328
|
+
});
|
|
329
|
+
} catch (err) {
|
|
330
|
+
resolve({ success: false, message: 'Invalid response from local LLM' });
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
req.on('error', () => {
|
|
336
|
+
// Try LM Studio endpoint
|
|
337
|
+
testLMStudio(endpoint).then(resolve);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
req.on('timeout', () => {
|
|
341
|
+
req.destroy();
|
|
342
|
+
resolve({ success: false, message: 'Connection timeout' });
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
req.end();
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Test LM Studio provider
|
|
351
|
+
*/
|
|
352
|
+
async function testLMStudio(baseEndpoint) {
|
|
353
|
+
const http = require('http');
|
|
354
|
+
|
|
355
|
+
return new Promise((resolve) => {
|
|
356
|
+
// LM Studio uses OpenAI-compatible endpoint
|
|
357
|
+
const endpoint = baseEndpoint.replace(':11434', ':1234');
|
|
358
|
+
const url = new URL('/v1/models', endpoint);
|
|
359
|
+
|
|
360
|
+
const req = http.request(url, { method: 'GET', timeout: 5000 }, (res) => {
|
|
361
|
+
let data = '';
|
|
362
|
+
res.on('data', chunk => data += chunk);
|
|
363
|
+
res.on('end', () => {
|
|
364
|
+
try {
|
|
365
|
+
const parsed = JSON.parse(data);
|
|
366
|
+
const models = parsed.data?.map(m => m.id) || [];
|
|
367
|
+
resolve({
|
|
368
|
+
success: true,
|
|
369
|
+
message: `Connected to LM Studio. Found ${models.length} models.`,
|
|
370
|
+
models,
|
|
371
|
+
provider: 'lmstudio'
|
|
372
|
+
});
|
|
373
|
+
} catch (err) {
|
|
374
|
+
resolve({ success: false, message: 'No local LLM detected' });
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
req.on('error', () => {
|
|
380
|
+
resolve({ success: false, message: 'No local LLM detected at localhost:11434 or :1234' });
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
req.end();
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Test cloud provider connection
|
|
389
|
+
*/
|
|
390
|
+
async function testCloudProvider(providerName, endpoint, apiKey) {
|
|
391
|
+
const https = require('https');
|
|
392
|
+
|
|
393
|
+
return new Promise((resolve, reject) => {
|
|
394
|
+
let url, headers;
|
|
395
|
+
|
|
396
|
+
switch (providerName) {
|
|
397
|
+
case 'openai':
|
|
398
|
+
url = new URL('/v1/models', endpoint);
|
|
399
|
+
headers = { 'Authorization': `Bearer ${apiKey}` };
|
|
400
|
+
break;
|
|
401
|
+
case 'google':
|
|
402
|
+
url = new URL(`/v1beta/models?key=${apiKey}`, endpoint);
|
|
403
|
+
headers = {};
|
|
404
|
+
break;
|
|
405
|
+
case 'anthropic':
|
|
406
|
+
// Anthropic doesn't have a models list endpoint, just test with a simple check
|
|
407
|
+
resolve({ success: true, message: 'API key format valid (Anthropic)' });
|
|
408
|
+
return;
|
|
409
|
+
default:
|
|
410
|
+
reject(new Error(`Unknown cloud provider: ${providerName}`));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const req = https.request(url, { method: 'GET', headers, timeout: 10000 }, (res) => {
|
|
415
|
+
let data = '';
|
|
416
|
+
res.on('data', chunk => data += chunk);
|
|
417
|
+
res.on('end', () => {
|
|
418
|
+
if (res.statusCode === 200) {
|
|
419
|
+
try {
|
|
420
|
+
const parsed = JSON.parse(data);
|
|
421
|
+
let models = [];
|
|
422
|
+
|
|
423
|
+
if (providerName === 'openai') {
|
|
424
|
+
models = parsed.data?.map(m => m.id).filter(id =>
|
|
425
|
+
id.startsWith('gpt-4') || id.startsWith('o1')
|
|
426
|
+
).slice(0, 10) || [];
|
|
427
|
+
} else if (providerName === 'google') {
|
|
428
|
+
models = parsed.models?.map(m => m.name.replace('models/', '')) || [];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
resolve({
|
|
432
|
+
success: true,
|
|
433
|
+
message: `Connected to ${providerName}. Found ${models.length} models.`,
|
|
434
|
+
models
|
|
435
|
+
});
|
|
436
|
+
} catch (err) {
|
|
437
|
+
resolve({ success: true, message: `Connected to ${providerName}` });
|
|
438
|
+
}
|
|
439
|
+
} else if (res.statusCode === 401) {
|
|
440
|
+
resolve({ success: false, message: 'Invalid API key' });
|
|
441
|
+
} else {
|
|
442
|
+
resolve({ success: false, message: `API error: ${res.statusCode}` });
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
req.on('error', (err) => reject(err));
|
|
448
|
+
req.on('timeout', () => {
|
|
449
|
+
req.destroy();
|
|
450
|
+
reject(new Error('Connection timeout'));
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
req.end();
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Migrate old config formats to new unified structure
|
|
459
|
+
* - hybrid.executor → models.providers
|
|
460
|
+
* - peerReview.apiKeys → models.providers
|
|
461
|
+
*/
|
|
462
|
+
function migrateOldConfig() {
|
|
463
|
+
const config = readConfig();
|
|
464
|
+
let migrated = false;
|
|
465
|
+
|
|
466
|
+
// Initialize models section if needed
|
|
467
|
+
if (!config.models) {
|
|
468
|
+
config.models = { providers: {}, defaults: {} };
|
|
469
|
+
}
|
|
470
|
+
if (!config.models.providers) {
|
|
471
|
+
config.models.providers = {};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Migrate hybrid.executor config
|
|
475
|
+
if (config.hybrid?.executor) {
|
|
476
|
+
const executor = config.hybrid.executor;
|
|
477
|
+
const provider = executor.provider;
|
|
478
|
+
|
|
479
|
+
if (provider && !config.models.providers[provider]) {
|
|
480
|
+
config.models.providers[provider] = {
|
|
481
|
+
enabled: true,
|
|
482
|
+
apiKeyEnv: executor.apiKeyEnv || KNOWN_PROVIDERS[provider]?.envKey,
|
|
483
|
+
models: executor.model ? [executor.model] : [],
|
|
484
|
+
endpoint: executor.providerEndpoint
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// Set as hybrid default
|
|
488
|
+
if (executor.model) {
|
|
489
|
+
config.models.defaults = config.models.defaults || {};
|
|
490
|
+
config.models.defaults.hybrid = `${provider}:${executor.model}`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
migrated = true;
|
|
494
|
+
console.log(`${c.cyan}[model-config]${c.reset} Migrated hybrid.executor to models.providers.${provider}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Migrate peerReview.apiKeys config
|
|
499
|
+
if (config.peerReview?.apiKeys) {
|
|
500
|
+
for (const [provider, keyRef] of Object.entries(config.peerReview.apiKeys)) {
|
|
501
|
+
if (!config.models.providers[provider]) {
|
|
502
|
+
// Extract env var name from ${VAR_NAME} format
|
|
503
|
+
const envKey = keyRef.replace(/^\$\{|\}$/g, '');
|
|
504
|
+
|
|
505
|
+
config.models.providers[provider] = {
|
|
506
|
+
enabled: true,
|
|
507
|
+
apiKeyEnv: envKey,
|
|
508
|
+
models: KNOWN_PROVIDERS[provider]?.models?.slice(0, 2) || []
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
migrated = true;
|
|
512
|
+
console.log(`${c.cyan}[model-config]${c.reset} Migrated peerReview.apiKeys.${provider} to models.providers`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Migrate peer review model defaults
|
|
517
|
+
if (config.peerReview.models && !config.models.defaults?.peerReview) {
|
|
518
|
+
config.models.defaults = config.models.defaults || {};
|
|
519
|
+
config.models.defaults.peerReview = config.peerReview.models;
|
|
520
|
+
migrated = true;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (migrated) {
|
|
525
|
+
writeConfig(config);
|
|
526
|
+
console.log(`${c.green}[model-config]${c.reset} Migration complete. Config saved.`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return migrated;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Get default models for a feature
|
|
534
|
+
* @param {string} feature - 'hybrid' or 'peerReview'
|
|
535
|
+
* @returns {string|string[]} Default model(s)
|
|
536
|
+
*/
|
|
537
|
+
function getDefaultModels(feature) {
|
|
538
|
+
const modelsConfig = getModelsConfig();
|
|
539
|
+
return modelsConfig.defaults?.[feature] || null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Set default models for a feature
|
|
544
|
+
* @param {string} feature - 'hybrid' or 'peerReview'
|
|
545
|
+
* @param {string|string[]} models - Model(s) to set as default
|
|
546
|
+
*/
|
|
547
|
+
function setDefaultModels(feature, models) {
|
|
548
|
+
const modelsConfig = getModelsConfig();
|
|
549
|
+
if (!modelsConfig.defaults) {
|
|
550
|
+
modelsConfig.defaults = {};
|
|
551
|
+
}
|
|
552
|
+
modelsConfig.defaults[feature] = models;
|
|
553
|
+
updateModelsConfig(modelsConfig);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Check if any models are configured
|
|
558
|
+
* @returns {boolean}
|
|
559
|
+
*/
|
|
560
|
+
function hasConfiguredModels() {
|
|
561
|
+
const providers = getConfiguredProviders();
|
|
562
|
+
return providers.some(p => p.enabled && p.apiKeySet && p.models.length > 0);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// CLI interface
|
|
566
|
+
if (require.main === module) {
|
|
567
|
+
const args = process.argv.slice(2);
|
|
568
|
+
const command = args[0];
|
|
569
|
+
|
|
570
|
+
switch (command) {
|
|
571
|
+
case 'list':
|
|
572
|
+
const providers = getConfiguredProviders();
|
|
573
|
+
console.log('\nConfigured Providers:');
|
|
574
|
+
for (const p of providers) {
|
|
575
|
+
const status = p.enabled && p.apiKeySet ? c.green + '✓' + c.reset : c.red + '✗' + c.reset;
|
|
576
|
+
console.log(` ${status} ${p.displayName}: ${p.models.join(', ') || '(no models)'}`);
|
|
577
|
+
}
|
|
578
|
+
console.log('\nEnabled Models:');
|
|
579
|
+
const models = getEnabledModels();
|
|
580
|
+
for (const m of models) {
|
|
581
|
+
console.log(` - ${m}`);
|
|
582
|
+
}
|
|
583
|
+
break;
|
|
584
|
+
|
|
585
|
+
case 'migrate':
|
|
586
|
+
migrateOldConfig();
|
|
587
|
+
break;
|
|
588
|
+
|
|
589
|
+
case 'test':
|
|
590
|
+
const providerName = args[1];
|
|
591
|
+
if (!providerName) {
|
|
592
|
+
console.error('Usage: flow-model-config test <provider>');
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
testProviderConnection(providerName).then(result => {
|
|
596
|
+
if (result.success) {
|
|
597
|
+
console.log(`${c.green}✓${c.reset} ${result.message}`);
|
|
598
|
+
if (result.models) {
|
|
599
|
+
console.log(' Models:', result.models.slice(0, 5).join(', '));
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
console.log(`${c.red}✗${c.reset} ${result.message}`);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
break;
|
|
606
|
+
|
|
607
|
+
default:
|
|
608
|
+
console.log(`
|
|
609
|
+
Wogi Flow - Model Configuration
|
|
610
|
+
|
|
611
|
+
Commands:
|
|
612
|
+
list List configured providers and models
|
|
613
|
+
migrate Migrate old config format to new unified format
|
|
614
|
+
test <provider> Test connection to a provider
|
|
615
|
+
|
|
616
|
+
Examples:
|
|
617
|
+
node flow-model-config.js list
|
|
618
|
+
node flow-model-config.js test openai
|
|
619
|
+
node flow-model-config.js migrate
|
|
620
|
+
`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
module.exports = {
|
|
625
|
+
// Core functions
|
|
626
|
+
getModelsConfig,
|
|
627
|
+
updateModelsConfig,
|
|
628
|
+
getConfiguredProviders,
|
|
629
|
+
getEnabledModels,
|
|
630
|
+
hasConfiguredModels,
|
|
631
|
+
|
|
632
|
+
// Provider management
|
|
633
|
+
addProvider,
|
|
634
|
+
removeProvider,
|
|
635
|
+
testProviderConnection,
|
|
636
|
+
|
|
637
|
+
// Defaults
|
|
638
|
+
getDefaultModels,
|
|
639
|
+
setDefaultModels,
|
|
640
|
+
|
|
641
|
+
// Migration
|
|
642
|
+
migrateOldConfig,
|
|
643
|
+
|
|
644
|
+
// Constants
|
|
645
|
+
KNOWN_PROVIDERS,
|
|
646
|
+
|
|
647
|
+
// Utility
|
|
648
|
+
updateEnvFile
|
|
649
|
+
};
|
|
@@ -79,6 +79,20 @@ const WOGI_COMMAND_PATTERNS = [
|
|
|
79
79
|
/\brun\s+(\/)?wogi-/i
|
|
80
80
|
];
|
|
81
81
|
|
|
82
|
+
// Maximum length for prompt display (DRY helper)
|
|
83
|
+
const MAX_DISPLAY_LENGTH = 80;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Truncate prompt for display in messages
|
|
87
|
+
* @param {string} prompt - The prompt to truncate
|
|
88
|
+
* @param {number} maxLength - Maximum length (default: 80)
|
|
89
|
+
* @returns {string} Truncated prompt with ellipsis if needed
|
|
90
|
+
*/
|
|
91
|
+
function truncatePrompt(prompt, maxLength = MAX_DISPLAY_LENGTH) {
|
|
92
|
+
if (!prompt || typeof prompt !== 'string') return '';
|
|
93
|
+
return prompt.length > maxLength ? prompt.slice(0, maxLength) + '...' : prompt;
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
/**
|
|
83
97
|
* Check if implementation gate should be enforced
|
|
84
98
|
* @returns {boolean}
|
|
@@ -159,17 +173,9 @@ function detectImplementationIntent(prompt) {
|
|
|
159
173
|
return { isImplementation: false, confidence: 'low', matches: [] };
|
|
160
174
|
}
|
|
161
175
|
|
|
162
|
-
// Determine confidence based on number
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
confidence = 'high';
|
|
166
|
-
} else if (matches.length >= 1) {
|
|
167
|
-
// Check for strong signals
|
|
168
|
-
const hasStrongSignal = matches.some(m =>
|
|
169
|
-
/\b(add|create|implement|fix|build)\b/i.test(m)
|
|
170
|
-
);
|
|
171
|
-
confidence = hasStrongSignal ? 'high' : 'medium';
|
|
172
|
-
}
|
|
176
|
+
// Determine confidence based on number of matches
|
|
177
|
+
// Simplified: any match from IMPLEMENTATION_PATTERNS is a strong signal
|
|
178
|
+
const confidence = matches.length >= 2 ? 'high' : 'medium';
|
|
173
179
|
|
|
174
180
|
return { isImplementation: true, confidence, matches };
|
|
175
181
|
}
|
|
@@ -183,7 +189,7 @@ function detectImplementationIntent(prompt) {
|
|
|
183
189
|
* @returns {Object} Result: { allowed, blocked, message, reason, confidence, suggestedAction }
|
|
184
190
|
*/
|
|
185
191
|
function checkImplementationGate(options = {}) {
|
|
186
|
-
const { prompt
|
|
192
|
+
const { prompt } = options;
|
|
187
193
|
|
|
188
194
|
// Empty or invalid prompt - allow
|
|
189
195
|
if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {
|
|
@@ -264,7 +270,7 @@ function checkImplementationGate(options = {}) {
|
|
|
264
270
|
return {
|
|
265
271
|
allowed: true,
|
|
266
272
|
blocked: false,
|
|
267
|
-
message: generateWarningMessage(prompt
|
|
273
|
+
message: generateWarningMessage(prompt),
|
|
268
274
|
reason: 'warn_only',
|
|
269
275
|
confidence,
|
|
270
276
|
suggestedAction: 'create-story',
|
|
@@ -276,7 +282,7 @@ function checkImplementationGate(options = {}) {
|
|
|
276
282
|
return {
|
|
277
283
|
allowed: false,
|
|
278
284
|
blocked: true,
|
|
279
|
-
message: generateBlockMessage(prompt
|
|
285
|
+
message: generateBlockMessage(prompt),
|
|
280
286
|
reason: 'no_active_task',
|
|
281
287
|
confidence,
|
|
282
288
|
suggestedAction: 'create-story',
|
|
@@ -287,11 +293,10 @@ function checkImplementationGate(options = {}) {
|
|
|
287
293
|
/**
|
|
288
294
|
* Generate warning message (soft mode)
|
|
289
295
|
*/
|
|
290
|
-
function generateWarningMessage(prompt
|
|
291
|
-
const truncatedPrompt = prompt.length > 80 ? prompt.slice(0, 80) + '...' : prompt;
|
|
296
|
+
function generateWarningMessage(prompt) {
|
|
292
297
|
return `Warning: No active WogiFlow task.
|
|
293
298
|
|
|
294
|
-
Consider: /wogi-start "${
|
|
299
|
+
Consider: /wogi-start "${truncatePrompt(prompt)}"
|
|
295
300
|
|
|
296
301
|
This will execute directly (git/npm/deploy) or create a story first (features/fixes).`;
|
|
297
302
|
}
|
|
@@ -299,13 +304,11 @@ This will execute directly (git/npm/deploy) or create a story first (features/fi
|
|
|
299
304
|
/**
|
|
300
305
|
* Generate block message (hard mode)
|
|
301
306
|
*/
|
|
302
|
-
function generateBlockMessage(prompt
|
|
303
|
-
const truncatedPrompt = prompt.length > 80 ? prompt.slice(0, 80) + '...' : prompt;
|
|
304
|
-
|
|
307
|
+
function generateBlockMessage(prompt) {
|
|
305
308
|
return `BLOCKED: No active WogiFlow task.
|
|
306
309
|
|
|
307
310
|
To proceed, run:
|
|
308
|
-
/wogi-start "${
|
|
311
|
+
/wogi-start "${truncatePrompt(prompt)}"
|
|
309
312
|
|
|
310
313
|
This will either:
|
|
311
314
|
- Execute directly (if operational: git, npm, deploy, build)
|
|
@@ -323,6 +326,7 @@ module.exports = {
|
|
|
323
326
|
checkImplementationGate,
|
|
324
327
|
generateWarningMessage,
|
|
325
328
|
generateBlockMessage,
|
|
329
|
+
truncatePrompt,
|
|
326
330
|
IMPLEMENTATION_PATTERNS,
|
|
327
331
|
EXPLORATION_PATTERNS
|
|
328
332
|
};
|