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