forgeoptimizer 0.1.0__py3-none-any.whl

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.
Files changed (159) hide show
  1. forgecli/__init__.py +4 -0
  2. forgecli/build/__init__.py +135 -0
  3. forgecli/build/apply.py +218 -0
  4. forgecli/build/caveman_optimize.py +37 -0
  5. forgecli/build/diff_extract.py +218 -0
  6. forgecli/build/llm.py +167 -0
  7. forgecli/build/optimize.py +37 -0
  8. forgecli/build/pipeline.py +76 -0
  9. forgecli/build/retrieval.py +157 -0
  10. forgecli/build/summarize.py +76 -0
  11. forgecli/build/test_run.py +75 -0
  12. forgecli/builder/__init__.py +11 -0
  13. forgecli/builder/builder.py +53 -0
  14. forgecli/builder/editor.py +36 -0
  15. forgecli/builder/formatter.py +31 -0
  16. forgecli/cli/__init__.py +5 -0
  17. forgecli/cli/bootstrap.py +281 -0
  18. forgecli/cli/commands_graph.py +149 -0
  19. forgecli/cli/commands_wrappers.py +80 -0
  20. forgecli/cli/daemon.py +744 -0
  21. forgecli/cli/main.py +234 -0
  22. forgecli/cli/ui.py +56 -0
  23. forgecli/config/__init__.py +28 -0
  24. forgecli/config/loader.py +85 -0
  25. forgecli/config/settings.py +206 -0
  26. forgecli/config/writer.py +90 -0
  27. forgecli/core/__init__.py +35 -0
  28. forgecli/core/container.py +68 -0
  29. forgecli/core/context.py +54 -0
  30. forgecli/core/credentials.py +114 -0
  31. forgecli/core/errors.py +27 -0
  32. forgecli/core/events.py +67 -0
  33. forgecli/core/logging.py +47 -0
  34. forgecli/core/models.py +214 -0
  35. forgecli/core/plugins.py +54 -0
  36. forgecli/core/service.py +22 -0
  37. forgecli/docs/__init__.py +5 -0
  38. forgecli/docs/generator.py +115 -0
  39. forgecli/engine/__init__.py +105 -0
  40. forgecli/engine/context.py +163 -0
  41. forgecli/engine/defaults.py +89 -0
  42. forgecli/engine/events.py +185 -0
  43. forgecli/engine/execution.py +519 -0
  44. forgecli/engine/plugins.py +145 -0
  45. forgecli/engine/runner.py +158 -0
  46. forgecli/engine/stages/__init__.py +33 -0
  47. forgecli/engine/stages/caveman_optimizer.py +47 -0
  48. forgecli/engine/stages/context_optimizer.py +44 -0
  49. forgecli/engine/stages/execution_engine_stage.py +102 -0
  50. forgecli/engine/stages/git_engine.py +30 -0
  51. forgecli/engine/stages/intent_analyzer.py +38 -0
  52. forgecli/engine/stages/model_router.py +65 -0
  53. forgecli/engine/stages/planning_engine.py +45 -0
  54. forgecli/engine/stages/repository_analyzer.py +54 -0
  55. forgecli/engine/stages/validation_engine.py +87 -0
  56. forgecli/git/__init__.py +9 -0
  57. forgecli/git/repo.py +55 -0
  58. forgecli/git/service.py +45 -0
  59. forgecli/graph/__init__.py +56 -0
  60. forgecli/graph/backend_graphify.py +448 -0
  61. forgecli/graph/edge.py +19 -0
  62. forgecli/graph/graph.py +85 -0
  63. forgecli/graph/graphify.py +405 -0
  64. forgecli/graph/indexer.py +82 -0
  65. forgecli/graph/node.py +47 -0
  66. forgecli/graph/repository.py +164 -0
  67. forgecli/memory/__init__.py +12 -0
  68. forgecli/memory/cache.py +61 -0
  69. forgecli/memory/history.py +136 -0
  70. forgecli/memory/store.py +85 -0
  71. forgecli/optimizer/__init__.py +13 -0
  72. forgecli/optimizer/caveman/__init__.py +169 -0
  73. forgecli/optimizer/caveman/cli.py +62 -0
  74. forgecli/optimizer/caveman/decorator.py +67 -0
  75. forgecli/optimizer/caveman/factory.py +51 -0
  76. forgecli/optimizer/caveman/ruleset.py +156 -0
  77. forgecli/optimizer/caveman/state.py +50 -0
  78. forgecli/optimizer/chunker.py +85 -0
  79. forgecli/optimizer/optimizer.py +81 -0
  80. forgecli/optimizer/ponytail/__init__.py +181 -0
  81. forgecli/optimizer/ponytail/cli.py +159 -0
  82. forgecli/optimizer/ponytail/decorator.py +70 -0
  83. forgecli/optimizer/ponytail/factory.py +51 -0
  84. forgecli/optimizer/ponytail/ruleset.py +168 -0
  85. forgecli/optimizer/ponytail/state.py +51 -0
  86. forgecli/optimizer/ranker.py +37 -0
  87. forgecli/optimizer/summarizer.py +44 -0
  88. forgecli/orchestrator/__init__.py +706 -0
  89. forgecli/planner/__init__.py +56 -0
  90. forgecli/planner/agent.py +61 -0
  91. forgecli/planner/plan.py +65 -0
  92. forgecli/planner/planner.py +17 -0
  93. forgecli/planner/render.py +267 -0
  94. forgecli/planner/serialize.py +104 -0
  95. forgecli/planner/software.py +832 -0
  96. forgecli/platform/__init__.py +91 -0
  97. forgecli/platform/core.py +201 -0
  98. forgecli/platform/deps.py +361 -0
  99. forgecli/platform/paths.py +234 -0
  100. forgecli/platform/shell.py +176 -0
  101. forgecli/platform/update.py +253 -0
  102. forgecli/plugins/__init__.py +249 -0
  103. forgecli/prompts/__init__.py +11 -0
  104. forgecli/prompts/loader.py +28 -0
  105. forgecli/prompts/registry.py +32 -0
  106. forgecli/prompts/renderer.py +23 -0
  107. forgecli/providers/__init__.py +39 -0
  108. forgecli/providers/anthropic.py +207 -0
  109. forgecli/providers/base.py +204 -0
  110. forgecli/providers/builtin.py +26 -0
  111. forgecli/providers/conversation.py +74 -0
  112. forgecli/providers/google.py +290 -0
  113. forgecli/providers/http_base.py +207 -0
  114. forgecli/providers/mock.py +111 -0
  115. forgecli/providers/openai.py +206 -0
  116. forgecli/providers/openai_compatible.py +787 -0
  117. forgecli/providers/router.py +340 -0
  118. forgecli/providers/router_state.py +89 -0
  119. forgecli/review/__init__.py +45 -0
  120. forgecli/review/analyzer.py +124 -0
  121. forgecli/review/analyzers/__init__.py +1 -0
  122. forgecli/review/analyzers/architecture.py +255 -0
  123. forgecli/review/analyzers/complexity.py +162 -0
  124. forgecli/review/analyzers/dead_code.py +255 -0
  125. forgecli/review/analyzers/duplicates.py +161 -0
  126. forgecli/review/analyzers/performance.py +161 -0
  127. forgecli/review/analyzers/security.py +244 -0
  128. forgecli/review/finding.py +68 -0
  129. forgecli/review/report.py +321 -0
  130. forgecli/review/repository.py +130 -0
  131. forgecli/review/suggestions.py +98 -0
  132. forgecli/runtime/__init__.py +6 -0
  133. forgecli/runtime/cache_store.py +75 -0
  134. forgecli/runtime/mcp_config.py +118 -0
  135. forgecli/runtime/prepare.py +203 -0
  136. forgecli/runtime/wrappers.py +150 -0
  137. forgecli/sdk/__init__.py +132 -0
  138. forgecli/sdk/events.py +206 -0
  139. forgecli/sdk/interfaces.py +282 -0
  140. forgecli/sdk/loader.py +239 -0
  141. forgecli/sdk/manager.py +678 -0
  142. forgecli/sdk/manifest.py +395 -0
  143. forgecli/sdk/sandbox.py +247 -0
  144. forgecli/sdk/version.py +313 -0
  145. forgecli/templates/__init__.py +9 -0
  146. forgecli/templates/engine.py +37 -0
  147. forgecli/templates/registry.py +32 -0
  148. forgecli/utils/__init__.py +19 -0
  149. forgecli/utils/fs.py +121 -0
  150. forgecli/utils/ids.py +11 -0
  151. forgecli/utils/io.py +27 -0
  152. forgecli/utils/paths.py +78 -0
  153. forgecli/utils/stats.py +179 -0
  154. forgecli/utils/timing.py +25 -0
  155. forgeoptimizer-0.1.0.dist-info/METADATA +177 -0
  156. forgeoptimizer-0.1.0.dist-info/RECORD +159 -0
  157. forgeoptimizer-0.1.0.dist-info/WHEEL +4 -0
  158. forgeoptimizer-0.1.0.dist-info/entry_points.txt +2 -0
  159. forgeoptimizer-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,787 @@
1
+ """OpenAI-compatible AI providers (OpenRouter, Groq, Mistral, Ollama, LM Studio, vLLM)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from forgecli.providers.base import ModelInfo
11
+ from forgecli.providers.http_base import HTTPChatProvider, messages_to_openai
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # OpenRouter
15
+ # ---------------------------------------------------------------------------
16
+
17
+
18
+ @dataclass
19
+ class OpenRouterConfig:
20
+ api_key_env: str = "OPENROUTER_API_KEY"
21
+ base_url: str = "https://openrouter.ai/api/v1"
22
+ default_model: str = "glm-5.2"
23
+ max_tokens: int = 4096
24
+ temperature: float = 0.2
25
+
26
+
27
+ class OpenRouterProvider(HTTPChatProvider):
28
+ name = "openrouter"
29
+
30
+ def __init__(
31
+ self,
32
+ config: OpenRouterConfig | None = None,
33
+ *,
34
+ api_key: str | None = None,
35
+ base_url: str | None = None,
36
+ client: httpx.AsyncClient | None = None,
37
+ ) -> None:
38
+ super().__init__(
39
+ config or OpenRouterConfig(),
40
+ api_key=api_key,
41
+ base_url=base_url,
42
+ client=client,
43
+ )
44
+
45
+ def _default_base_url(self) -> str:
46
+ return "https://openrouter.ai/api/v1"
47
+
48
+ def _chat_url(self) -> str:
49
+ return f"{self._base_url}/chat/completions"
50
+
51
+ def _format_request(self, request: Any) -> dict[str, Any]:
52
+ body = {
53
+ "model": request.model or self.config.default_model,
54
+ "messages": messages_to_openai(request.messages),
55
+ "temperature": request.temperature
56
+ if request.temperature is not None
57
+ else self.config.temperature,
58
+ "max_tokens": request.max_tokens or self.config.max_tokens,
59
+ }
60
+ return body
61
+
62
+ def _parse_response(self, payload: dict[str, Any]) -> Any:
63
+ # standard OpenAI response parsing
64
+ from forgecli.providers.openai import OpenAIProvider
65
+
66
+ # Re-use OpenAI's parser logic
67
+ temp_provider = OpenAIProvider()
68
+ return temp_provider._parse_response(payload)
69
+
70
+ def _known_models(self) -> list[ModelInfo]:
71
+ from forgecli.core.models import MODEL_CATALOG
72
+
73
+ return [
74
+ ModelInfo(id=m.id, name=m.display_name, context_window=128_000, supports_tools=True)
75
+ for m in MODEL_CATALOG
76
+ if m.provider == "openrouter"
77
+ ]
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # Groq
82
+ # ---------------------------------------------------------------------------
83
+
84
+
85
+ @dataclass
86
+ class GroqConfig:
87
+ api_key_env: str = "GROQ_API_KEY"
88
+ base_url: str = "https://api.groq.com/openai/v1"
89
+ default_model: str = "llama-4-scout"
90
+ max_tokens: int = 4096
91
+ temperature: float = 0.2
92
+
93
+
94
+ class GroqProvider(HTTPChatProvider):
95
+ name = "groq"
96
+
97
+ def __init__(
98
+ self,
99
+ config: GroqConfig | None = None,
100
+ *,
101
+ api_key: str | None = None,
102
+ base_url: str | None = None,
103
+ client: httpx.AsyncClient | None = None,
104
+ ) -> None:
105
+ super().__init__(
106
+ config or GroqConfig(),
107
+ api_key=api_key,
108
+ base_url=base_url,
109
+ client=client,
110
+ )
111
+
112
+ def _default_base_url(self) -> str:
113
+ return "https://api.groq.com/openai/v1"
114
+
115
+ def _chat_url(self) -> str:
116
+ return f"{self._base_url}/chat/completions"
117
+
118
+ def _format_request(self, request: Any) -> dict[str, Any]:
119
+ return {
120
+ "model": request.model or self.config.default_model,
121
+ "messages": messages_to_openai(request.messages),
122
+ "temperature": request.temperature
123
+ if request.temperature is not None
124
+ else self.config.temperature,
125
+ "max_tokens": request.max_tokens or self.config.max_tokens,
126
+ }
127
+
128
+ def _parse_response(self, payload: dict[str, Any]) -> Any:
129
+ from forgecli.providers.openai import OpenAIProvider
130
+
131
+ temp_provider = OpenAIProvider()
132
+ return temp_provider._parse_response(payload)
133
+
134
+ def _known_models(self) -> list[ModelInfo]:
135
+ from forgecli.core.models import MODEL_CATALOG
136
+
137
+ return [
138
+ ModelInfo(id=m.id, name=m.display_name, context_window=128_000, supports_tools=True)
139
+ for m in MODEL_CATALOG
140
+ if m.provider == "groq"
141
+ ]
142
+
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # Mistral
146
+ # ---------------------------------------------------------------------------
147
+
148
+
149
+ @dataclass
150
+ class MistralConfig:
151
+ api_key_env: str = "MISTRAL_API_KEY"
152
+ base_url: str = "https://api.mistral.ai/v1"
153
+ default_model: str = "mistral-large"
154
+ max_tokens: int = 4096
155
+ temperature: float = 0.2
156
+
157
+
158
+ class MistralProvider(HTTPChatProvider):
159
+ name = "mistral"
160
+
161
+ def __init__(
162
+ self,
163
+ config: MistralConfig | None = None,
164
+ *,
165
+ api_key: str | None = None,
166
+ base_url: str | None = None,
167
+ client: httpx.AsyncClient | None = None,
168
+ ) -> None:
169
+ super().__init__(
170
+ config or MistralConfig(),
171
+ api_key=api_key,
172
+ base_url=base_url,
173
+ client=client,
174
+ )
175
+
176
+ def _default_base_url(self) -> str:
177
+ return "https://api.mistral.ai/v1"
178
+
179
+ def _chat_url(self) -> str:
180
+ return f"{self._base_url}/chat/completions"
181
+
182
+ def _format_request(self, request: Any) -> dict[str, Any]:
183
+ return {
184
+ "model": request.model or self.config.default_model,
185
+ "messages": messages_to_openai(request.messages),
186
+ "temperature": request.temperature
187
+ if request.temperature is not None
188
+ else self.config.temperature,
189
+ "max_tokens": request.max_tokens or self.config.max_tokens,
190
+ }
191
+
192
+ def _parse_response(self, payload: dict[str, Any]) -> Any:
193
+ from forgecli.providers.openai import OpenAIProvider
194
+
195
+ temp_provider = OpenAIProvider()
196
+ return temp_provider._parse_response(payload)
197
+
198
+ def _known_models(self) -> list[ModelInfo]:
199
+ from forgecli.core.models import MODEL_CATALOG
200
+
201
+ return [
202
+ ModelInfo(id=m.id, name=m.display_name, context_window=128_000, supports_tools=True)
203
+ for m in MODEL_CATALOG
204
+ if m.provider == "mistral"
205
+ ]
206
+
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # Local Providers
210
+ # ---------------------------------------------------------------------------
211
+
212
+
213
+ class LocalProvider(HTTPChatProvider):
214
+ """Generic base class for local providers that support dynamic model list query."""
215
+
216
+ def _chat_url(self) -> str:
217
+ return f"{self._base_url}/chat/completions"
218
+
219
+ def _format_request(self, request: Any) -> dict[str, Any]:
220
+ return {
221
+ "model": request.model or self.config.default_model,
222
+ "messages": messages_to_openai(request.messages),
223
+ "temperature": request.temperature
224
+ if request.temperature is not None
225
+ else self.config.temperature,
226
+ "max_tokens": request.max_tokens or self.config.max_tokens,
227
+ }
228
+
229
+ def _parse_response(self, payload: dict[str, Any]) -> Any:
230
+ from forgecli.providers.openai import OpenAIProvider
231
+
232
+ temp_provider = OpenAIProvider()
233
+ return temp_provider._parse_response(payload)
234
+
235
+ async def list_models(self) -> list[ModelInfo]:
236
+ try:
237
+ response = await self._client.get(
238
+ f"{self._base_url}/models", headers=self._auth_headers(), timeout=5.0
239
+ )
240
+ if response.status_code == 200:
241
+ data = response.json()
242
+ models_list = data.get("data", [])
243
+ return [
244
+ ModelInfo(
245
+ id=str(m["id"]),
246
+ name=str(m.get("id")),
247
+ context_window=128_000,
248
+ supports_tools=True,
249
+ supports_vision=True,
250
+ )
251
+ for m in models_list
252
+ if isinstance(m, dict) and m.get("id") is not None
253
+ ]
254
+ except Exception:
255
+ pass
256
+ return self._known_models()
257
+
258
+
259
+ @dataclass
260
+ class OllamaConfig:
261
+ api_key_env: str = "OLLAMA_API_KEY"
262
+ base_url: str = "http://localhost:11434/v1"
263
+ default_model: str = "llama3"
264
+ max_tokens: int = 4096
265
+ temperature: float = 0.2
266
+
267
+
268
+ class OllamaProvider(LocalProvider):
269
+ name = "ollama"
270
+
271
+ def __init__(
272
+ self,
273
+ config: OllamaConfig | None = None,
274
+ *,
275
+ api_key: str | None = None,
276
+ base_url: str | None = None,
277
+ client: httpx.AsyncClient | None = None,
278
+ ) -> None:
279
+ super().__init__(
280
+ config or OllamaConfig(),
281
+ api_key=api_key,
282
+ base_url=base_url,
283
+ client=client,
284
+ )
285
+
286
+ def _default_base_url(self) -> str:
287
+ return "http://localhost:11434/v1"
288
+
289
+ async def list_models(self) -> list[ModelInfo]:
290
+ try:
291
+ import subprocess
292
+
293
+ result = subprocess.run(["ollama", "list"], capture_output=True, text=True, check=False)
294
+ if result.returncode == 0:
295
+ lines = result.stdout.strip().split("\n")
296
+ if len(lines) > 1:
297
+ models = []
298
+ for line in lines[1:]:
299
+ parts = line.split()
300
+ if parts:
301
+ model_id = parts[0]
302
+ models.append(
303
+ ModelInfo(
304
+ id=model_id,
305
+ name=model_id,
306
+ context_window=8192,
307
+ supports_tools=True,
308
+ )
309
+ )
310
+ if models:
311
+ return models
312
+ except Exception:
313
+ pass
314
+ return await super().list_models()
315
+
316
+ def _known_models(self) -> list[ModelInfo]:
317
+ from forgecli.core.models import MODEL_CATALOG
318
+
319
+ return [
320
+ ModelInfo(id=m.id, name=m.display_name, context_window=8192, supports_tools=True)
321
+ for m in MODEL_CATALOG
322
+ if m.provider == "ollama"
323
+ ]
324
+
325
+
326
+ @dataclass
327
+ class LMStudioConfig:
328
+ api_key_env: str = "LMSTUDIO_API_KEY"
329
+ base_url: str = "http://localhost:1234/v1"
330
+ default_model: str = "local-model"
331
+ max_tokens: int = 4096
332
+ temperature: float = 0.2
333
+
334
+
335
+ class LMStudioProvider(LocalProvider):
336
+ name = "lmstudio"
337
+
338
+ def __init__(
339
+ self,
340
+ config: LMStudioConfig | None = None,
341
+ *,
342
+ api_key: str | None = None,
343
+ base_url: str | None = None,
344
+ client: httpx.AsyncClient | None = None,
345
+ ) -> None:
346
+ super().__init__(
347
+ config or LMStudioConfig(),
348
+ api_key=api_key,
349
+ base_url=base_url,
350
+ client=client,
351
+ )
352
+
353
+ def _default_base_url(self) -> str:
354
+ return "http://localhost:1234/v1"
355
+
356
+ def _known_models(self) -> list[ModelInfo]:
357
+ from forgecli.core.models import MODEL_CATALOG
358
+
359
+ return [
360
+ ModelInfo(id=m.id, name=m.display_name, context_window=8192, supports_tools=True)
361
+ for m in MODEL_CATALOG
362
+ if m.provider == "lmstudio"
363
+ ]
364
+
365
+
366
+ @dataclass
367
+ class VllmConfig:
368
+ api_key_env: str = "VLLM_API_KEY"
369
+ base_url: str = "http://localhost:8000/v1"
370
+ default_model: str = "local-model"
371
+ max_tokens: int = 4096
372
+ temperature: float = 0.2
373
+
374
+
375
+ class VllmProvider(LocalProvider):
376
+ name = "vllm"
377
+
378
+ def __init__(
379
+ self,
380
+ config: VllmConfig | None = None,
381
+ *,
382
+ api_key: str | None = None,
383
+ base_url: str | None = None,
384
+ client: httpx.AsyncClient | None = None,
385
+ ) -> None:
386
+ super().__init__(
387
+ config or VllmConfig(),
388
+ api_key=api_key,
389
+ base_url=base_url,
390
+ client=client,
391
+ )
392
+
393
+ def _default_base_url(self) -> str:
394
+ return "http://localhost:8000/v1"
395
+
396
+ def _known_models(self) -> list[ModelInfo]:
397
+ from forgecli.core.models import MODEL_CATALOG
398
+
399
+ return [
400
+ ModelInfo(id=m.id, name=m.display_name, context_window=8192, supports_tools=True)
401
+ for m in MODEL_CATALOG
402
+ if m.provider == "vllm"
403
+ ]
404
+
405
+
406
+ # ---------------------------------------------------------------------------
407
+ # MiniMax
408
+ # ---------------------------------------------------------------------------
409
+
410
+
411
+ @dataclass
412
+ class MiniMaxConfig:
413
+ api_key_env: str = "MINIMAX_API_KEY"
414
+ base_url: str = "https://api.minimaxi.chat/v1"
415
+ default_model: str = "abab6.5g-chat"
416
+ max_tokens: int = 4096
417
+ temperature: float = 0.2
418
+
419
+
420
+ class MiniMaxProvider(HTTPChatProvider):
421
+ name = "minimax"
422
+
423
+ def __init__(
424
+ self,
425
+ config: MiniMaxConfig | None = None,
426
+ *,
427
+ api_key: str | None = None,
428
+ base_url: str | None = None,
429
+ client: httpx.AsyncClient | None = None,
430
+ ) -> None:
431
+ super().__init__(
432
+ config or MiniMaxConfig(),
433
+ api_key=api_key,
434
+ base_url=base_url,
435
+ client=client,
436
+ )
437
+
438
+ def _default_base_url(self) -> str:
439
+ return "https://api.minimaxi.chat/v1"
440
+
441
+ def _chat_url(self) -> str:
442
+ return f"{self._base_url}/chat/completions"
443
+
444
+ def _format_request(self, request: Any) -> dict[str, Any]:
445
+ return {
446
+ "model": request.model or self.config.default_model,
447
+ "messages": messages_to_openai(request.messages),
448
+ "temperature": request.temperature
449
+ if request.temperature is not None
450
+ else self.config.temperature,
451
+ "max_tokens": request.max_tokens or self.config.max_tokens,
452
+ }
453
+
454
+ def _parse_response(self, payload: dict[str, Any]) -> Any:
455
+ from forgecli.providers.openai import OpenAIProvider
456
+
457
+ temp_provider = OpenAIProvider()
458
+ return temp_provider._parse_response(payload)
459
+
460
+ def _known_models(self) -> list[ModelInfo]:
461
+ from forgecli.core.models import MODEL_CATALOG
462
+
463
+ return [
464
+ ModelInfo(id=m.id, name=m.display_name, context_window=128_000, supports_tools=True)
465
+ for m in MODEL_CATALOG
466
+ if m.provider == "minimax"
467
+ ]
468
+
469
+
470
+ # ---------------------------------------------------------------------------
471
+ # xAI (Grok)
472
+ # ---------------------------------------------------------------------------
473
+
474
+
475
+ @dataclass
476
+ class XaiConfig:
477
+ api_key_env: str = "XAI_API_KEY"
478
+ base_url: str = "https://api.x.ai/v1"
479
+ default_model: str = "grok-2"
480
+ max_tokens: int = 4096
481
+ temperature: float = 0.2
482
+
483
+
484
+ class XaiProvider(HTTPChatProvider):
485
+ name = "xai"
486
+
487
+ def __init__(
488
+ self,
489
+ config: XaiConfig | None = None,
490
+ *,
491
+ api_key: str | None = None,
492
+ base_url: str | None = None,
493
+ client: httpx.AsyncClient | None = None,
494
+ ) -> None:
495
+ super().__init__(
496
+ config or XaiConfig(),
497
+ api_key=api_key,
498
+ base_url=base_url,
499
+ client=client,
500
+ )
501
+
502
+ def _default_base_url(self) -> str:
503
+ return "https://api.x.ai/v1"
504
+
505
+ def _chat_url(self) -> str:
506
+ return f"{self._base_url}/chat/completions"
507
+
508
+ def _format_request(self, request: Any) -> dict[str, Any]:
509
+ return {
510
+ "model": request.model or self.config.default_model,
511
+ "messages": messages_to_openai(request.messages),
512
+ "temperature": request.temperature
513
+ if request.temperature is not None
514
+ else self.config.temperature,
515
+ "max_tokens": request.max_tokens or self.config.max_tokens,
516
+ }
517
+
518
+ def _parse_response(self, payload: dict[str, Any]) -> Any:
519
+ from forgecli.providers.openai import OpenAIProvider
520
+
521
+ temp_provider = OpenAIProvider()
522
+ return temp_provider._parse_response(payload)
523
+
524
+ def _known_models(self) -> list[ModelInfo]:
525
+ from forgecli.core.models import MODEL_CATALOG
526
+
527
+ return [
528
+ ModelInfo(id=m.id, name=m.display_name, context_window=128_000, supports_tools=True)
529
+ for m in MODEL_CATALOG
530
+ if m.provider == "xai"
531
+ ]
532
+
533
+
534
+ # ---------------------------------------------------------------------------
535
+ # Together AI
536
+ # ---------------------------------------------------------------------------
537
+
538
+
539
+ @dataclass
540
+ class TogetherConfig:
541
+ api_key_env: str = "TOGETHER_API_KEY"
542
+ base_url: str = "https://api.together.xyz/v1"
543
+ default_model: str = "llama-3.1-70b"
544
+ max_tokens: int = 4096
545
+ temperature: float = 0.2
546
+
547
+
548
+ class TogetherProvider(HTTPChatProvider):
549
+ name = "together"
550
+
551
+ def __init__(
552
+ self,
553
+ config: TogetherConfig | None = None,
554
+ *,
555
+ api_key: str | None = None,
556
+ base_url: str | None = None,
557
+ client: httpx.AsyncClient | None = None,
558
+ ) -> None:
559
+ super().__init__(
560
+ config or TogetherConfig(),
561
+ api_key=api_key,
562
+ base_url=base_url,
563
+ client=client,
564
+ )
565
+
566
+ def _default_base_url(self) -> str:
567
+ return "https://api.together.xyz/v1"
568
+
569
+ def _chat_url(self) -> str:
570
+ return f"{self._base_url}/chat/completions"
571
+
572
+ def _format_request(self, request: Any) -> dict[str, Any]:
573
+ return {
574
+ "model": request.model or self.config.default_model,
575
+ "messages": messages_to_openai(request.messages),
576
+ "temperature": request.temperature
577
+ if request.temperature is not None
578
+ else self.config.temperature,
579
+ "max_tokens": request.max_tokens or self.config.max_tokens,
580
+ }
581
+
582
+ def _parse_response(self, payload: dict[str, Any]) -> Any:
583
+ from forgecli.providers.openai import OpenAIProvider
584
+
585
+ temp_provider = OpenAIProvider()
586
+ return temp_provider._parse_response(payload)
587
+
588
+ def _known_models(self) -> list[ModelInfo]:
589
+ from forgecli.core.models import MODEL_CATALOG
590
+
591
+ return [
592
+ ModelInfo(id=m.id, name=m.display_name, context_window=128_000, supports_tools=True)
593
+ for m in MODEL_CATALOG
594
+ if m.provider == "together"
595
+ ]
596
+
597
+
598
+ # ---------------------------------------------------------------------------
599
+ # Fireworks AI
600
+ # ---------------------------------------------------------------------------
601
+
602
+
603
+ @dataclass
604
+ class FireworksConfig:
605
+ api_key_env: str = "FIREWORKS_API_KEY"
606
+ base_url: str = "https://api.fireworks.ai/inference/v1"
607
+ default_model: str = "llama-3.1-70b"
608
+ max_tokens: int = 4096
609
+ temperature: float = 0.2
610
+
611
+
612
+ class FireworksProvider(HTTPChatProvider):
613
+ name = "fireworks"
614
+
615
+ def __init__(
616
+ self,
617
+ config: FireworksConfig | None = None,
618
+ *,
619
+ api_key: str | None = None,
620
+ base_url: str | None = None,
621
+ client: httpx.AsyncClient | None = None,
622
+ ) -> None:
623
+ super().__init__(
624
+ config or FireworksConfig(),
625
+ api_key=api_key,
626
+ base_url=base_url,
627
+ client=client,
628
+ )
629
+
630
+ def _default_base_url(self) -> str:
631
+ return "https://api.fireworks.ai/inference/v1"
632
+
633
+ def _chat_url(self) -> str:
634
+ return f"{self._base_url}/chat/completions"
635
+
636
+ def _format_request(self, request: Any) -> dict[str, Any]:
637
+ return {
638
+ "model": request.model or self.config.default_model,
639
+ "messages": messages_to_openai(request.messages),
640
+ "temperature": request.temperature
641
+ if request.temperature is not None
642
+ else self.config.temperature,
643
+ "max_tokens": request.max_tokens or self.config.max_tokens,
644
+ }
645
+
646
+ def _parse_response(self, payload: dict[str, Any]) -> Any:
647
+ from forgecli.providers.openai import OpenAIProvider
648
+
649
+ temp_provider = OpenAIProvider()
650
+ return temp_provider._parse_response(payload)
651
+
652
+ def _known_models(self) -> list[ModelInfo]:
653
+ from forgecli.core.models import MODEL_CATALOG
654
+
655
+ return [
656
+ ModelInfo(id=m.id, name=m.display_name, context_window=128_000, supports_tools=True)
657
+ for m in MODEL_CATALOG
658
+ if m.provider == "fireworks"
659
+ ]
660
+
661
+
662
+ # ---------------------------------------------------------------------------
663
+ # Cohere
664
+ # ---------------------------------------------------------------------------
665
+
666
+
667
+ @dataclass
668
+ class CohereConfig:
669
+ api_key_env: str = "COHERE_API_KEY"
670
+ base_url: str = "https://api.cohere.com/v1"
671
+ default_model: str = "command-r-plus"
672
+ max_tokens: int = 4096
673
+ temperature: float = 0.2
674
+
675
+
676
+ class CohereProvider(HTTPChatProvider):
677
+ name = "cohere"
678
+
679
+ def __init__(
680
+ self,
681
+ config: CohereConfig | None = None,
682
+ *,
683
+ api_key: str | None = None,
684
+ base_url: str | None = None,
685
+ client: httpx.AsyncClient | None = None,
686
+ ) -> None:
687
+ super().__init__(
688
+ config or CohereConfig(),
689
+ api_key=api_key,
690
+ base_url=base_url,
691
+ client=client,
692
+ )
693
+
694
+ def _default_base_url(self) -> str:
695
+ return "https://api.cohere.com/v1"
696
+
697
+ def _chat_url(self) -> str:
698
+ return f"{self._base_url}/chat/completions"
699
+
700
+ def _format_request(self, request: Any) -> dict[str, Any]:
701
+ return {
702
+ "model": request.model or self.config.default_model,
703
+ "messages": messages_to_openai(request.messages),
704
+ "temperature": request.temperature
705
+ if request.temperature is not None
706
+ else self.config.temperature,
707
+ "max_tokens": request.max_tokens or self.config.max_tokens,
708
+ }
709
+
710
+ def _parse_response(self, payload: dict[str, Any]) -> Any:
711
+ from forgecli.providers.openai import OpenAIProvider
712
+
713
+ temp_provider = OpenAIProvider()
714
+ return temp_provider._parse_response(payload)
715
+
716
+ def _known_models(self) -> list[ModelInfo]:
717
+ from forgecli.core.models import MODEL_CATALOG
718
+
719
+ return [
720
+ ModelInfo(id=m.id, name=m.display_name, context_window=128_000, supports_tools=True)
721
+ for m in MODEL_CATALOG
722
+ if m.provider == "cohere"
723
+ ]
724
+
725
+
726
+ # ---------------------------------------------------------------------------
727
+ # NVIDIA NIM
728
+ # ---------------------------------------------------------------------------
729
+
730
+
731
+ @dataclass
732
+ class NvidiaConfig:
733
+ api_key_env: str = "NVIDIA_API_KEY"
734
+ base_url: str = "https://integrate.api.nvidia.com/v1"
735
+ default_model: str = "llama-3.1-70b"
736
+ max_tokens: int = 4096
737
+ temperature: float = 0.2
738
+
739
+
740
+ class NvidiaProvider(HTTPChatProvider):
741
+ name = "nvidia"
742
+
743
+ def __init__(
744
+ self,
745
+ config: NvidiaConfig | None = None,
746
+ *,
747
+ api_key: str | None = None,
748
+ base_url: str | None = None,
749
+ client: httpx.AsyncClient | None = None,
750
+ ) -> None:
751
+ super().__init__(
752
+ config or NvidiaConfig(),
753
+ api_key=api_key,
754
+ base_url=base_url,
755
+ client=client,
756
+ )
757
+
758
+ def _default_base_url(self) -> str:
759
+ return "https://integrate.api.nvidia.com/v1"
760
+
761
+ def _chat_url(self) -> str:
762
+ return f"{self._base_url}/chat/completions"
763
+
764
+ def _format_request(self, request: Any) -> dict[str, Any]:
765
+ return {
766
+ "model": request.model or self.config.default_model,
767
+ "messages": messages_to_openai(request.messages),
768
+ "temperature": request.temperature
769
+ if request.temperature is not None
770
+ else self.config.temperature,
771
+ "max_tokens": request.max_tokens or self.config.max_tokens,
772
+ }
773
+
774
+ def _parse_response(self, payload: dict[str, Any]) -> Any:
775
+ from forgecli.providers.openai import OpenAIProvider
776
+
777
+ temp_provider = OpenAIProvider()
778
+ return temp_provider._parse_response(payload)
779
+
780
+ def _known_models(self) -> list[ModelInfo]:
781
+ from forgecli.core.models import MODEL_CATALOG
782
+
783
+ return [
784
+ ModelInfo(id=m.id, name=m.display_name, context_window=128_000, supports_tools=True)
785
+ for m in MODEL_CATALOG
786
+ if m.provider == "nvidia"
787
+ ]