@voria/cli 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/voria +17 -2
- package/package.json +1 -1
- package/python/voria/core/llm/model_discovery.py +82 -175
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# voria
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**AI-Powered Bug Fixing Tool**
|
|
4
4
|
|
|
5
5
|
voria is a CLI tool that automatically fixes bugs and implements features in your codebase. Describe an issue or provide a GitHub issue number, and voria will generate a fix, test it, iterate on failures, and create a pull request - all automatically.
|
|
6
6
|
|
package/bin/voria
CHANGED
|
@@ -131,7 +131,19 @@ function saveConfig(config, isGlobal = true) {
|
|
|
131
131
|
fs.writeFileSync(file, JSON.stringify(config, null, 2));
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Dynamically fetch models for a provider using Python backend
|
|
136
|
+
*/
|
|
137
|
+
function fetchModels(provider, apiKey) {
|
|
138
|
+
try {
|
|
139
|
+
const scriptPath = path.join(__dirname, '..', 'python', 'voria', 'core', 'llm', 'model_discovery.py');
|
|
140
|
+
const output = execSync(`python3 "${scriptPath}" "${provider}" "${apiKey}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
141
|
+
return JSON.parse(output);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
135
147
|
// Interactive setup
|
|
136
148
|
async function runSetup() {
|
|
137
149
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -170,8 +182,11 @@ async function runSetup() {
|
|
|
170
182
|
return;
|
|
171
183
|
}
|
|
172
184
|
|
|
185
|
+
console.log(`\n${colors.dim(" Fetching latest models...")}`);
|
|
186
|
+
const fetchedModels = fetchModels(provider, apiKey);
|
|
187
|
+
const models = fetchedModels.length > 0 ? fetchedModels.map(m => m.name) : (PROVIDER_MODELS[provider] || ['gpt-4o']);
|
|
188
|
+
|
|
173
189
|
console.log(`\n${colors.bold("Select Model:")}`);
|
|
174
|
-
const models = PROVIDER_MODELS[provider] || ['default'];
|
|
175
190
|
models.forEach((model, index) => {
|
|
176
191
|
console.log(` ${colors.blue((index + 1) + ")")} ${model}`);
|
|
177
192
|
});
|
package/package.json
CHANGED
|
@@ -5,9 +5,10 @@ Fetches available models at runtime based on API keys.
|
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import httpx
|
|
8
|
-
from dataclasses import dataclass
|
|
8
|
+
from dataclasses import dataclass, asdict
|
|
9
9
|
from typing import List, Optional
|
|
10
10
|
import logging
|
|
11
|
+
import json
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
|
|
@@ -26,6 +27,36 @@ class ModelInfo:
|
|
|
26
27
|
class ModelDiscovery:
|
|
27
28
|
"""Fetch available models from LLM providers."""
|
|
28
29
|
|
|
30
|
+
@staticmethod
|
|
31
|
+
async def fetch_generic_openai_compatible(api_key: str, base_url: str, provider_name: str) -> List[ModelInfo]:
|
|
32
|
+
"""Fetch models from an OpenAI-compatible API."""
|
|
33
|
+
try:
|
|
34
|
+
async with httpx.AsyncClient() as client:
|
|
35
|
+
response = await client.get(
|
|
36
|
+
f"{base_url.rstrip('/')}/models",
|
|
37
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
38
|
+
timeout=10.0,
|
|
39
|
+
)
|
|
40
|
+
if response.status_code == 200:
|
|
41
|
+
data = response.json()
|
|
42
|
+
models = []
|
|
43
|
+
for model in data.get("data", []):
|
|
44
|
+
model_id = model.get("id", "")
|
|
45
|
+
models.append(
|
|
46
|
+
ModelInfo(
|
|
47
|
+
name=model_id,
|
|
48
|
+
display_name=model_id,
|
|
49
|
+
description=f"{provider_name} Model",
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
return models
|
|
53
|
+
else:
|
|
54
|
+
logger.warning(f"{provider_name} API returned {response.status_code}")
|
|
55
|
+
return []
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.warning(f"Failed to fetch {provider_name} models: {e}")
|
|
58
|
+
return []
|
|
59
|
+
|
|
29
60
|
@staticmethod
|
|
30
61
|
async def fetch_modal_models(api_key: str) -> List[ModelInfo]:
|
|
31
62
|
"""Fetch available models from Modal Z.ai API."""
|
|
@@ -39,7 +70,6 @@ class ModelDiscovery:
|
|
|
39
70
|
if response.status_code == 200:
|
|
40
71
|
data = response.json()
|
|
41
72
|
models = []
|
|
42
|
-
# Modal returns model data in "data" key
|
|
43
73
|
for model in data.get("data", []):
|
|
44
74
|
models.append(
|
|
45
75
|
ModelInfo(
|
|
@@ -49,34 +79,16 @@ class ModelDiscovery:
|
|
|
49
79
|
description=f"Modal Z.ai - {model.get('created', 'N/A')}",
|
|
50
80
|
)
|
|
51
81
|
)
|
|
52
|
-
return (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
else:
|
|
56
|
-
logger.warning(
|
|
57
|
-
f"Modal API returned {response.status_code}, using fallback models"
|
|
58
|
-
)
|
|
59
|
-
return await ModelDiscovery._get_modal_fallback()
|
|
60
|
-
except Exception as e:
|
|
61
|
-
logger.warning(f"Failed to fetch Modal models: {e}, using fallback")
|
|
82
|
+
return models if models else await ModelDiscovery._get_modal_fallback()
|
|
83
|
+
return await ModelDiscovery._get_modal_fallback()
|
|
84
|
+
except Exception:
|
|
62
85
|
return await ModelDiscovery._get_modal_fallback()
|
|
63
86
|
|
|
64
87
|
@staticmethod
|
|
65
88
|
async def _get_modal_fallback() -> List[ModelInfo]:
|
|
66
|
-
"""Fallback models for Modal when API unavailable."""
|
|
67
89
|
return [
|
|
68
|
-
ModelInfo(
|
|
69
|
-
|
|
70
|
-
display_name="GLM-5.1-FP8 (745B, Latest)",
|
|
71
|
-
max_tokens=4096,
|
|
72
|
-
description="Latest Modal Z.ai model - 745B parameters",
|
|
73
|
-
),
|
|
74
|
-
ModelInfo(
|
|
75
|
-
name="zai-org/GLM-4",
|
|
76
|
-
display_name="GLM-4 (370B, Legacy)",
|
|
77
|
-
max_tokens=2048,
|
|
78
|
-
description="Previous generation Modal model",
|
|
79
|
-
),
|
|
90
|
+
ModelInfo(name="zai-org/GLM-5.1-FP8", display_name="GLM-5.1-FP8 (Latest)"),
|
|
91
|
+
ModelInfo(name="zai-org/GLM-4", display_name="GLM-4 (Legacy)"),
|
|
80
92
|
]
|
|
81
93
|
|
|
82
94
|
@staticmethod
|
|
@@ -92,81 +104,26 @@ class ModelDiscovery:
|
|
|
92
104
|
if response.status_code == 200:
|
|
93
105
|
data = response.json()
|
|
94
106
|
models = []
|
|
95
|
-
|
|
96
|
-
suitable_models = {
|
|
97
|
-
"gpt-4o",
|
|
98
|
-
"gpt-4-turbo",
|
|
99
|
-
"gpt-4",
|
|
100
|
-
"gpt-3.5-turbo",
|
|
101
|
-
}
|
|
107
|
+
suitable_prefixes = {"gpt-4", "gpt-3.5", "o1-"}
|
|
102
108
|
for model in data.get("data", []):
|
|
103
109
|
model_id = model.get("id", "")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
ModelInfo(
|
|
110
|
-
name=model_id,
|
|
111
|
-
display_name=model_id,
|
|
112
|
-
description=f"OpenAI - {model.get('owned_by', 'N/A')}",
|
|
113
|
-
)
|
|
114
|
-
)
|
|
115
|
-
# Sort by recency (gpt-4o > gpt-4-turbo > gpt-4 > gpt-3.5-turbo)
|
|
116
|
-
return (
|
|
117
|
-
sorted(
|
|
118
|
-
models,
|
|
119
|
-
key=lambda x: (
|
|
120
|
-
not x.name.startswith("gpt-4o"),
|
|
121
|
-
not x.name.startswith("gpt-4-turbo"),
|
|
122
|
-
not x.name.startswith("gpt-4"),
|
|
123
|
-
),
|
|
124
|
-
)
|
|
125
|
-
if models
|
|
126
|
-
else await ModelDiscovery._get_openai_fallback()
|
|
127
|
-
)
|
|
128
|
-
else:
|
|
129
|
-
logger.warning(
|
|
130
|
-
f"OpenAI API returned {response.status_code}, using fallback models"
|
|
131
|
-
)
|
|
132
|
-
return await ModelDiscovery._get_openai_fallback()
|
|
133
|
-
except Exception as e:
|
|
134
|
-
logger.warning(f"Failed to fetch OpenAI models: {e}, using fallback")
|
|
110
|
+
if any(model_id.startswith(p) for p in suitable_prefixes):
|
|
111
|
+
models.append(ModelInfo(name=model_id, display_name=model_id))
|
|
112
|
+
return models if models else await ModelDiscovery._get_openai_fallback()
|
|
113
|
+
return await ModelDiscovery._get_openai_fallback()
|
|
114
|
+
except Exception:
|
|
135
115
|
return await ModelDiscovery._get_openai_fallback()
|
|
136
116
|
|
|
137
117
|
@staticmethod
|
|
138
118
|
async def _get_openai_fallback() -> List[ModelInfo]:
|
|
139
|
-
"""Fallback models for OpenAI when API unavailable."""
|
|
140
119
|
return [
|
|
141
|
-
ModelInfo(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
max_tokens=128000,
|
|
145
|
-
description="Best intelligence at scale for agentic, coding, and professional workflows. $2.50 input, $15 output per 1M tokens",
|
|
146
|
-
),
|
|
147
|
-
ModelInfo(
|
|
148
|
-
name="gpt-5.4-mini",
|
|
149
|
-
display_name="GPT-5.4-mini (Mini Model)",
|
|
150
|
-
max_tokens=128000,
|
|
151
|
-
description="Strongest mini model yet for coding, computer use, and agentic tasks. $0.75 input, $4.50 output per 1M tokens",
|
|
152
|
-
),
|
|
153
|
-
ModelInfo(
|
|
154
|
-
name="gpt-5.4-nano",
|
|
155
|
-
display_name="GPT-5.4-nano (Cheapest)",
|
|
156
|
-
max_tokens=128000,
|
|
157
|
-
description="Cheapest GPT-5.4-class model for simple high-volume tasks. $0.20 input, $1.25 output per 1M tokens",
|
|
158
|
-
),
|
|
159
|
-
ModelInfo(
|
|
160
|
-
name="gpt-4o",
|
|
161
|
-
display_name="GPT-4o (Previous High Quality)",
|
|
162
|
-
max_tokens=128000,
|
|
163
|
-
description="Previous latest model - optimized for speed and cost",
|
|
164
|
-
),
|
|
120
|
+
ModelInfo(name="gpt-4o", display_name="GPT-4o"),
|
|
121
|
+
ModelInfo(name="gpt-4o-mini", display_name="GPT-4o-mini"),
|
|
122
|
+
ModelInfo(name="o1-preview", display_name="o1-preview"),
|
|
165
123
|
]
|
|
166
124
|
|
|
167
125
|
@staticmethod
|
|
168
126
|
async def fetch_gemini_models(api_key: str) -> List[ModelInfo]:
|
|
169
|
-
"""Fetch available models from Google Gemini API."""
|
|
170
127
|
try:
|
|
171
128
|
async with httpx.AsyncClient() as client:
|
|
172
129
|
response = await client.get(
|
|
@@ -176,106 +133,33 @@ class ModelDiscovery:
|
|
|
176
133
|
if response.status_code == 200:
|
|
177
134
|
data = response.json()
|
|
178
135
|
models = []
|
|
179
|
-
# Filter to generative models
|
|
180
136
|
for model in data.get("models", []):
|
|
181
|
-
|
|
182
|
-
if "gemini" in
|
|
183
|
-
models.append(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
description=f"Google Gemini - {model.get('displayName', 'N/A')}",
|
|
188
|
-
)
|
|
189
|
-
)
|
|
190
|
-
return (
|
|
191
|
-
models
|
|
192
|
-
if models
|
|
193
|
-
else await ModelDiscovery._get_gemini_fallback()
|
|
194
|
-
)
|
|
195
|
-
else:
|
|
196
|
-
logger.warning(
|
|
197
|
-
f"Gemini API returned {response.status_code}, using fallback models"
|
|
198
|
-
)
|
|
199
|
-
return await ModelDiscovery._get_gemini_fallback()
|
|
200
|
-
except Exception as e:
|
|
201
|
-
logger.warning(f"Failed to fetch Gemini models: {e}, using fallback")
|
|
137
|
+
name = model.get("name", "").replace("models/", "")
|
|
138
|
+
if "gemini" in name.lower():
|
|
139
|
+
models.append(ModelInfo(name=name, display_name=name))
|
|
140
|
+
return models if models else await ModelDiscovery._get_gemini_fallback()
|
|
141
|
+
return await ModelDiscovery._get_gemini_fallback()
|
|
142
|
+
except Exception:
|
|
202
143
|
return await ModelDiscovery._get_gemini_fallback()
|
|
203
144
|
|
|
204
145
|
@staticmethod
|
|
205
146
|
async def _get_gemini_fallback() -> List[ModelInfo]:
|
|
206
|
-
"""Fallback models for Gemini when API unavailable."""
|
|
207
147
|
return [
|
|
208
|
-
ModelInfo(
|
|
209
|
-
|
|
210
|
-
display_name="Gemini 3.1 Pro (Latest SOTA Reasoning)",
|
|
211
|
-
max_tokens=200000,
|
|
212
|
-
description="Latest SOTA reasoning model with unprecedented depth and nuance. $2 input, $12 output per context window",
|
|
213
|
-
),
|
|
214
|
-
ModelInfo(
|
|
215
|
-
name="gemini-3-flash",
|
|
216
|
-
display_name="Gemini 3 Flash (Latest, Fastest)",
|
|
217
|
-
max_tokens=200000,
|
|
218
|
-
description="Most intelligent model built for speed, combining frontier intelligence with superior search and grounding",
|
|
219
|
-
),
|
|
220
|
-
ModelInfo(
|
|
221
|
-
name="gemini-3.1-flash-lite",
|
|
222
|
-
display_name="Gemini 3.1 Flash Lite (Cheapest)",
|
|
223
|
-
max_tokens=200000,
|
|
224
|
-
description="Most cost-efficient model, optimized for high-volume agentic tasks. $0.25 input, $1.50 output",
|
|
225
|
-
),
|
|
226
|
-
ModelInfo(
|
|
227
|
-
name="gemini-2.0-flash",
|
|
228
|
-
display_name="Gemini 2.0 Flash (Previous)",
|
|
229
|
-
max_tokens=2000,
|
|
230
|
-
description="Previous generation Gemini model",
|
|
231
|
-
),
|
|
148
|
+
ModelInfo(name="gemini-1.5-pro", display_name="Gemini 1.5 Pro"),
|
|
149
|
+
ModelInfo(name="gemini-1.5-flash", display_name="Gemini 1.5 Flash"),
|
|
232
150
|
]
|
|
233
151
|
|
|
234
152
|
@staticmethod
|
|
235
153
|
async def fetch_claude_models(api_key: str) -> List[ModelInfo]:
|
|
236
|
-
|
|
237
|
-
try:
|
|
238
|
-
async with httpx.AsyncClient() as client:
|
|
239
|
-
# Claude doesn't have a public models endpoint, use documented models
|
|
240
|
-
# Make a test call to verify API key works
|
|
241
|
-
response = await client.get(
|
|
242
|
-
"https://api.anthropic.com/v1/models",
|
|
243
|
-
headers={"x-api-key": api_key},
|
|
244
|
-
timeout=10.0,
|
|
245
|
-
)
|
|
246
|
-
# If we get here, API key works - return known models
|
|
247
|
-
return await ModelDiscovery._get_claude_fallback()
|
|
248
|
-
except Exception as e:
|
|
249
|
-
logger.warning(f"Failed to verify Claude API: {e}, returning known models")
|
|
250
|
-
return await ModelDiscovery._get_claude_fallback()
|
|
251
|
-
|
|
252
|
-
@staticmethod
|
|
253
|
-
async def _get_claude_fallback() -> List[ModelInfo]:
|
|
254
|
-
"""Known Claude models (Anthropic doesn't provide list endpoint)."""
|
|
154
|
+
# Anthropic doesn't have a models endpoint, just return hardcoded
|
|
255
155
|
return [
|
|
256
|
-
ModelInfo(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
max_tokens=200000,
|
|
260
|
-
description="Most intelligent broadly available model for complex reasoning. $5 input, $25 output per 1M tokens",
|
|
261
|
-
),
|
|
262
|
-
ModelInfo(
|
|
263
|
-
name="claude-sonnet-4.6",
|
|
264
|
-
display_name="Claude Sonnet 4.6 (Best Value)",
|
|
265
|
-
max_tokens=200000,
|
|
266
|
-
description="Best balance of speed and intelligence. $3 input, $15 output per 1M tokens",
|
|
267
|
-
),
|
|
268
|
-
ModelInfo(
|
|
269
|
-
name="claude-haiku-4.5",
|
|
270
|
-
display_name="Claude Haiku 4.5 (Fastest, Cheapest)",
|
|
271
|
-
max_tokens=200000,
|
|
272
|
-
description="Fast and cost-efficient for simpler tasks. $0.80 input, $4 output per 1M tokens",
|
|
273
|
-
),
|
|
156
|
+
ModelInfo(name="claude-3-5-sonnet-20240620", display_name="Claude 3.5 Sonnet"),
|
|
157
|
+
ModelInfo(name="claude-3-opus-20240229", display_name="Claude 3 Opus"),
|
|
158
|
+
ModelInfo(name="claude-3-haiku-20240307", display_name="Claude 3 Haiku"),
|
|
274
159
|
]
|
|
275
160
|
|
|
276
161
|
@staticmethod
|
|
277
162
|
async def discover_all(provider: str, api_key: str) -> List[ModelInfo]:
|
|
278
|
-
"""Discover all models for a given provider."""
|
|
279
163
|
provider = provider.lower().strip()
|
|
280
164
|
if provider == "modal":
|
|
281
165
|
return await ModelDiscovery.fetch_modal_models(api_key)
|
|
@@ -285,5 +169,28 @@ class ModelDiscovery:
|
|
|
285
169
|
return await ModelDiscovery.fetch_gemini_models(api_key)
|
|
286
170
|
elif provider == "claude":
|
|
287
171
|
return await ModelDiscovery.fetch_claude_models(api_key)
|
|
172
|
+
elif provider == "deepseek":
|
|
173
|
+
return await ModelDiscovery.fetch_generic_openai_compatible(api_key, "https://api.deepseek.com/v1", "DeepSeek")
|
|
174
|
+
elif provider == "kimi":
|
|
175
|
+
return await ModelDiscovery.fetch_generic_openai_compatible(api_key, "https://api.moonshot.cn/v1", "Kimi")
|
|
176
|
+
elif provider == "minimax":
|
|
177
|
+
return await ModelDiscovery.fetch_generic_openai_compatible(api_key, "https://api.minimax.chat/v1", "Minimax")
|
|
178
|
+
elif provider == "siliconflow":
|
|
179
|
+
return await ModelDiscovery.fetch_generic_openai_compatible(api_key, "https://api.siliconflow.cn/v1", "SiliconFlow")
|
|
288
180
|
else:
|
|
289
|
-
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
import sys
|
|
185
|
+
if len(sys.argv) < 3:
|
|
186
|
+
print(json.dumps([]))
|
|
187
|
+
sys.exit(0)
|
|
188
|
+
|
|
189
|
+
provider = sys.argv[1]
|
|
190
|
+
api_key = sys.argv[2]
|
|
191
|
+
|
|
192
|
+
async def main():
|
|
193
|
+
models = await ModelDiscovery.discover_all(provider, api_key)
|
|
194
|
+
print(json.dumps([asdict(m) for m in models]))
|
|
195
|
+
|
|
196
|
+
asyncio.run(main())
|