tamfis-code 0.2.3__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.
@@ -0,0 +1,423 @@
1
+ """Provider integrations for TAMFIS-CODE - HF, NVIDIA NIM, OpenRouter, and Ollama"""
2
+
3
+ import os
4
+ import json
5
+ import subprocess
6
+ from typing import Optional, Dict, Any, AsyncIterator, List
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum
9
+ import httpx
10
+ from openai import AsyncOpenAI
11
+
12
+
13
+ class ProviderType(Enum):
14
+ HF = "hf"
15
+ NVIDIA = "nvidia"
16
+ OPENROUTER = "openrouter"
17
+ OLLAMA = "ollama"
18
+ LOCAL = "local"
19
+ AUTO = "auto"
20
+
21
+
22
+ @dataclass
23
+ class ProviderConfig:
24
+ """Configuration for a provider"""
25
+ name: str
26
+ base_url: str
27
+ api_key_env: str
28
+ default_model: str
29
+ models: List[str] = field(default_factory=list)
30
+ weight: int = 1
31
+ reasoning_supported: bool = False
32
+ vision_supported: bool = False
33
+
34
+
35
+ class ProviderManager:
36
+ """Manages multiple AI providers with vision support"""
37
+
38
+ PROVIDERS = {
39
+ # NOTE: provider catalogs drift as vendors ship/retire models --
40
+ # these lists are a reasonable, current-as-of-writing starting point
41
+ # for local/offline mode, not a live catalog feed. Review periodically.
42
+ ProviderType.HF: ProviderConfig(
43
+ name="Hugging Face",
44
+ base_url="https://router.huggingface.co/v1",
45
+ api_key_env="HF_TOKEN",
46
+ # meta-llama/Llama-3.2-3B-Instruct is no longer served by any
47
+ # provider HF's router enables for a standard token (confirmed
48
+ # live: 400 model_not_supported); Qwen2.5-7B-Instruct responds
49
+ # normally, so it's the default instead.
50
+ default_model="Qwen/Qwen2.5-7B-Instruct",
51
+ models=[
52
+ # Text models
53
+ "Qwen/Qwen2.5-7B-Instruct",
54
+ "meta-llama/Llama-3.2-3B-Instruct",
55
+ "mistralai/Mistral-7B-Instruct-v0.3",
56
+ # Vision models
57
+ "microsoft/Phi-3.5-vision-instruct",
58
+ "meta-llama/Llama-3.2-11B-Vision-Instruct",
59
+ "Qwen/Qwen2-VL-7B-Instruct",
60
+ ],
61
+ weight=2,
62
+ vision_supported=True
63
+ ),
64
+ ProviderType.NVIDIA: ProviderConfig(
65
+ name="NVIDIA NIM (API Catalog)",
66
+ base_url="https://integrate.api.nvidia.com/v1",
67
+ api_key_env="NVIDIA_API_KEY",
68
+ default_model="nvidia/nemotron-3-super-120b-a12b",
69
+ models=[
70
+ # Text models
71
+ "nvidia/nemotron-3-super-120b-a12b",
72
+ "nvidia/nemotron-3-ultra-550b-a55b",
73
+ "moonshotai/kimi-k2.6",
74
+ "meta/llama-3.1-405b-instruct",
75
+ "meta/llama-3.1-70b-instruct",
76
+ "mistralai/mistral-large-2-123b",
77
+ "google/gemma-2-27b-it",
78
+ "microsoft/phi-3-medium-128k-instruct",
79
+ ],
80
+ weight=3,
81
+ reasoning_supported=True,
82
+ vision_supported=False
83
+ ),
84
+ ProviderType.OPENROUTER: ProviderConfig(
85
+ name="OpenRouter",
86
+ base_url="https://openrouter.ai/api/v1",
87
+ api_key_env="OPENROUTER_API_KEY",
88
+ default_model="openai/gpt-4o-mini",
89
+ models=[
90
+ # Text models
91
+ "openai/gpt-4o-mini",
92
+ "google/gemini-2.5-flash",
93
+ "meta-llama/llama-3.3-70b-instruct:free",
94
+ "mistralai/mistral-7b-instruct:free",
95
+ # Vision models
96
+ "google/gemini-2.5-flash",
97
+ "openai/gpt-4o-mini",
98
+ ],
99
+ weight=1,
100
+ vision_supported=True
101
+ ),
102
+ ProviderType.OLLAMA: ProviderConfig(
103
+ name="Ollama (Local)",
104
+ base_url=os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1"),
105
+ api_key_env="OLLAMA_API_KEY",
106
+ default_model="llama3.2:3b",
107
+ # Deliberately the lowest weight of any provider (below OpenRouter's 1):
108
+ # Ollama needs no API key, so it's "available" merely by being installed
109
+ # and running. If it outranked keyed cloud providers, `auto` would pick
110
+ # a 3B local model over a properly configured HF/NVIDIA/OpenRouter
111
+ # account whenever Ollama happened to be running -- it should only be
112
+ # chosen when nothing else is configured, per the fallback in
113
+ # _select_best_provider below.
114
+ models=[
115
+ "llama3.2:3b",
116
+ "llama3.2:1b",
117
+ "phi3:mini",
118
+ "qwen2.5:3b",
119
+ "qwen2.5:7b",
120
+ "codellama:7b",
121
+ "codellama:13b",
122
+ "mistral:7b",
123
+ "mixtral:8x7b",
124
+ "tamgpt-fast-1:latest",
125
+ "tamgpt-r1-elite:latest",
126
+ "tamgpt-vision-pro:latest",
127
+ "deepseek-coder:6.7b",
128
+ "gemma2:9b",
129
+ "nomic-embed-text:latest",
130
+ # Vision-capable (if available)
131
+ "llava:7b",
132
+ "llava:13b",
133
+ "bakllava:7b",
134
+ ],
135
+ weight=0,
136
+ reasoning_supported=True,
137
+ vision_supported=True
138
+ ),
139
+ }
140
+
141
+ def __init__(self):
142
+ self.clients: Dict[ProviderType, AsyncOpenAI] = {}
143
+ self.config = self._load_config()
144
+ self._init_clients()
145
+
146
+ def _load_config(self) -> Dict[str, Any]:
147
+ """Load provider configuration from environment"""
148
+ config = {}
149
+ for provider in ProviderType:
150
+ if provider == ProviderType.AUTO:
151
+ continue
152
+ env_var = f"TAMFIS_PROVIDER_{provider.value.upper()}_ENABLED"
153
+ config[provider.value] = os.environ.get(env_var, "true").lower() == "true"
154
+ return config
155
+
156
+ def _get_api_key(self, provider_type: ProviderType) -> Optional[str]:
157
+ """Get API key for a provider from environment"""
158
+ config = self.PROVIDERS.get(provider_type)
159
+ if not config:
160
+ return None
161
+
162
+ # Ollama doesn't need a key
163
+ if provider_type == ProviderType.OLLAMA:
164
+ return "ollama"
165
+
166
+ key = os.environ.get(config.api_key_env, "")
167
+ return key if key else None
168
+
169
+ def _has_valid_api_key(self, provider_type: ProviderType) -> bool:
170
+ """Check if a provider has a valid API key set"""
171
+ if provider_type == ProviderType.AUTO:
172
+ return False
173
+
174
+ # Ollama always available if the service is running
175
+ if provider_type == ProviderType.OLLAMA:
176
+ return self._check_ollama_available()
177
+
178
+ config = self.PROVIDERS.get(provider_type)
179
+ if not config:
180
+ return False
181
+
182
+ key = os.environ.get(config.api_key_env, "")
183
+ if not key:
184
+ return False
185
+
186
+ # Check if it's a placeholder
187
+ placeholder_patterns = ["your_", "_key", "_token", "YOUR_", "_API_KEY"]
188
+ if any(p in key for p in placeholder_patterns):
189
+ return False
190
+
191
+ # Valid key must be at least 8 characters
192
+ if len(key) < 8:
193
+ return False
194
+
195
+ return True
196
+
197
+ def _check_ollama_available(self) -> bool:
198
+ """Check if Ollama service is running and accessible"""
199
+ try:
200
+ result = subprocess.run(
201
+ ["ollama", "list"],
202
+ capture_output=True,
203
+ text=True,
204
+ timeout=5
205
+ )
206
+ return result.returncode == 0 and "NAME" in result.stdout
207
+ except:
208
+ return False
209
+
210
+ def _get_ollama_models(self) -> List[str]:
211
+ """Get list of available Ollama models"""
212
+ try:
213
+ result = subprocess.run(
214
+ ["ollama", "list"],
215
+ capture_output=True,
216
+ text=True,
217
+ timeout=5
218
+ )
219
+ if result.returncode == 0:
220
+ models = []
221
+ for line in result.stdout.split('\n')[1:]: # Skip header
222
+ if line.strip():
223
+ parts = line.split()
224
+ if parts:
225
+ models.append(parts[0])
226
+ return models
227
+ except:
228
+ pass
229
+ return []
230
+
231
+ def _init_clients(self):
232
+ """Initialize OpenAI-compatible clients for each provider"""
233
+ for provider_type, config in self.PROVIDERS.items():
234
+ if provider_type == ProviderType.AUTO:
235
+ continue
236
+ if not self.config.get(provider_type.value, True):
237
+ continue
238
+
239
+ # Skip if no valid API key (except Ollama)
240
+ if not self._has_valid_api_key(provider_type) and provider_type != ProviderType.OLLAMA:
241
+ continue
242
+
243
+ api_key = self._get_api_key(provider_type)
244
+
245
+ try:
246
+ self.clients[provider_type] = AsyncOpenAI(
247
+ base_url=config.base_url,
248
+ api_key=api_key or "dummy",
249
+ timeout=120.0,
250
+ max_retries=2,
251
+ )
252
+ except Exception as e:
253
+ print(f"Failed to initialize {config.name}: {e}")
254
+
255
+ def get_client(self, provider: ProviderType) -> Optional[AsyncOpenAI]:
256
+ """Get a client for a specific provider"""
257
+ if provider == ProviderType.AUTO:
258
+ provider = self._select_best_provider()
259
+ return self.clients.get(provider)
260
+
261
+ def _select_best_provider(self) -> ProviderType:
262
+ """Select the best available provider based on weight and availability"""
263
+ available = []
264
+ for p, client in self.clients.items():
265
+ if client and self._has_valid_api_key(p):
266
+ available.append((p, self.PROVIDERS[p].weight))
267
+
268
+ if not available:
269
+ if self._check_ollama_available():
270
+ return ProviderType.OLLAMA
271
+ return ProviderType.NVIDIA
272
+
273
+ available.sort(key=lambda x: x[1], reverse=True)
274
+ return available[0][0]
275
+
276
+ def list_available_providers(self) -> List[Dict[str, Any]]:
277
+ """List all available providers with their status"""
278
+ result = []
279
+ ollama_models = self._get_ollama_models() if self._check_ollama_available() else []
280
+
281
+ for provider_type, client in self.clients.items():
282
+ config = self.PROVIDERS.get(provider_type)
283
+ if config:
284
+ valid_key = self._has_valid_api_key(provider_type)
285
+ available = client is not None and valid_key
286
+
287
+ if provider_type == ProviderType.OLLAMA:
288
+ available = self._check_ollama_available()
289
+ valid_key = available
290
+
291
+ result.append({
292
+ "name": config.name,
293
+ "type": provider_type.value,
294
+ "available": available,
295
+ "default_model": config.default_model,
296
+ "models": ollama_models if provider_type == ProviderType.OLLAMA else config.models,
297
+ "weight": config.weight,
298
+ "api_key_set": valid_key,
299
+ "reasoning_supported": config.reasoning_supported,
300
+ "vision_supported": config.vision_supported,
301
+ "key_preview": self._get_api_key(provider_type)[:8] + "..." if self._get_api_key(provider_type) and provider_type != ProviderType.OLLAMA else "local"
302
+ })
303
+ return result
304
+
305
+ async def chat_completion(
306
+ self,
307
+ provider: ProviderType,
308
+ messages: List[Dict[str, str]],
309
+ model: Optional[str] = None,
310
+ stream: bool = True,
311
+ temperature: float = 0.10,
312
+ max_tokens: int = 16384,
313
+ reasoning_effort: Optional[str] = "high",
314
+ **kwargs
315
+ ) -> AsyncIterator[str]:
316
+ """Stream a chat completion from a provider"""
317
+ client = self.get_client(provider)
318
+ if not client:
319
+ raise ValueError(f"Provider {provider} not available")
320
+
321
+ config = self.PROVIDERS.get(provider)
322
+ if not config:
323
+ raise ValueError(f"Unknown provider: {provider}")
324
+
325
+ model = model or config.default_model
326
+
327
+ # For Ollama, use a valid model name
328
+ if provider == ProviderType.OLLAMA:
329
+ ollama_models = self._get_ollama_models()
330
+ if model not in ollama_models and ollama_models:
331
+ model = ollama_models[0]
332
+
333
+ try:
334
+ response = await client.chat.completions.create(
335
+ model=model,
336
+ messages=messages,
337
+ stream=stream,
338
+ temperature=temperature,
339
+ max_tokens=max_tokens,
340
+ **kwargs
341
+ )
342
+
343
+ if stream:
344
+ async for chunk in response:
345
+ if chunk.choices and chunk.choices[0].delta.content:
346
+ yield chunk.choices[0].delta.content
347
+ else:
348
+ if response.choices and response.choices[0].message.content:
349
+ yield response.choices[0].message.content
350
+
351
+ except Exception as e:
352
+ print(f"[{config.name} Error] {e}")
353
+ fallback_chain = [ProviderType.NVIDIA, ProviderType.HF, ProviderType.OPENROUTER, ProviderType.OLLAMA]
354
+ for fallback in fallback_chain:
355
+ if fallback != provider and self.get_client(fallback):
356
+ try:
357
+ async for chunk in self.chat_completion(
358
+ fallback, messages, model, stream, temperature, max_tokens, **kwargs
359
+ ):
360
+ yield chunk
361
+ return
362
+ except:
363
+ continue
364
+ raise
365
+
366
+ async def chat_completion_sync(
367
+ self,
368
+ provider: ProviderType,
369
+ messages: List[Dict[str, str]],
370
+ model: Optional[str] = None,
371
+ temperature: float = 0.10,
372
+ max_tokens: int = 16384,
373
+ **kwargs
374
+ ) -> str:
375
+ """Get a single chat completion (non-streaming)"""
376
+ result = []
377
+ async for chunk in self.chat_completion(
378
+ provider, messages, model, False, temperature, max_tokens, **kwargs
379
+ ):
380
+ result.append(chunk)
381
+ return ''.join(result)
382
+
383
+
384
+ # Convenience functions
385
+ async def chat_with_hf(messages: List[Dict[str, str]], **kwargs) -> AsyncIterator[str]:
386
+ manager = ProviderManager()
387
+ async for chunk in manager.chat_completion(ProviderType.HF, messages, **kwargs):
388
+ yield chunk
389
+
390
+
391
+ async def chat_with_nvidia(messages: List[Dict[str, str]], **kwargs) -> AsyncIterator[str]:
392
+ manager = ProviderManager()
393
+ async for chunk in manager.chat_completion(ProviderType.NVIDIA, messages, **kwargs):
394
+ yield chunk
395
+
396
+
397
+ async def chat_with_openrouter(messages: List[Dict[str, str]], **kwargs) -> AsyncIterator[str]:
398
+ manager = ProviderManager()
399
+ async for chunk in manager.chat_completion(ProviderType.OPENROUTER, messages, **kwargs):
400
+ yield chunk
401
+
402
+
403
+ async def chat_with_ollama(messages: List[Dict[str, str]], **kwargs) -> AsyncIterator[str]:
404
+ manager = ProviderManager()
405
+ async for chunk in manager.chat_completion(ProviderType.OLLAMA, messages, **kwargs):
406
+ yield chunk
407
+
408
+
409
+ def get_provider_status() -> Dict[str, Any]:
410
+ manager = ProviderManager()
411
+ return {
412
+ "available": manager.list_available_providers(),
413
+ "default": manager._select_best_provider().value if manager.clients else "none",
414
+ "config": {
415
+ p.value: {
416
+ "enabled": manager.config.get(p.value, True),
417
+ "api_key_set": manager._has_valid_api_key(p),
418
+ "key_preview": manager._get_api_key(p)[:8] + "..." if manager._get_api_key(p) else "Not set"
419
+ }
420
+ for p in ProviderType
421
+ if p != ProviderType.AUTO and p in manager.PROVIDERS
422
+ }
423
+ }