Spaces:
Sleeping
Sleeping
Graham Paasch commited on
Commit ·
6f9e2de
1
Parent(s): 703134e
Add Blaxel, SambaNova, Nebius, and Hugging Face LLM providers
Browse files- agent/llm_client.py +191 -1
agent/llm_client.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
LLM Client for Overgrowth Pipeline
|
| 3 |
-
Supports multiple providers: OpenAI, Anthropic,
|
| 4 |
"""
|
| 5 |
|
| 6 |
import os
|
|
@@ -42,6 +42,10 @@ class LLMClient:
|
|
| 42 |
# Support both standard and MCP hackathon naming conventions
|
| 43 |
self.openai_key = _get_env(["OPENAI_API_KEY", "OPENAI_MCP_1ST_BDAY"])
|
| 44 |
self.anthropic_key = _get_env(["ANTHROPIC_API_KEY", "ANTHROPIC_MCP_1ST_BDAY"])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
# Determine which provider to use
|
| 47 |
self.provider = self._detect_provider()
|
|
@@ -67,12 +71,24 @@ class LLMClient:
|
|
| 67 |
status = {
|
| 68 |
"openai_key": "present" if _present(["OPENAI_API_KEY", "OPENAI_MCP_1ST_BDAY"]) else "missing",
|
| 69 |
"anthropic_key": "present" if _present(["ANTHROPIC_API_KEY", "ANTHROPIC_MCP_1ST_BDAY"]) else "missing",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
"provider": "unknown",
|
| 71 |
}
|
| 72 |
if status["anthropic_key"] == "present":
|
| 73 |
status["provider"] = "anthropic"
|
| 74 |
elif status["openai_key"] == "present":
|
| 75 |
status["provider"] = "openai"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
return status
|
| 77 |
|
| 78 |
def _detect_provider(self) -> Optional[str]:
|
|
@@ -81,6 +97,14 @@ class LLMClient:
|
|
| 81 |
return "anthropic"
|
| 82 |
elif self.openai_key:
|
| 83 |
return "openai"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
return None
|
| 85 |
|
| 86 |
def chat(
|
|
@@ -101,6 +125,14 @@ class LLMClient:
|
|
| 101 |
return self._call_openai(messages, temperature, max_tokens, stream)
|
| 102 |
elif self.provider == "anthropic":
|
| 103 |
return self._call_anthropic(messages, temperature, max_tokens, stream)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
def chat_stream(
|
| 106 |
self,
|
|
@@ -116,6 +148,9 @@ class LLMClient:
|
|
| 116 |
yield from self._stream_openai(messages, temperature, max_tokens)
|
| 117 |
elif self.provider == "anthropic":
|
| 118 |
yield from self._stream_anthropic(messages, temperature, max_tokens)
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
def _call_openrouter(self, messages, temperature, max_tokens, stream):
|
| 121 |
"""Call OpenRouter API"""
|
|
@@ -308,6 +343,161 @@ class LLMClient:
|
|
| 308 |
yield from self._stream_openai(messages, temperature, max_tokens)
|
| 309 |
else:
|
| 310 |
raise RuntimeError(f"Anthropic streaming failed: {err_msg}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
|
| 312 |
def _mock_response(self, messages: List[LLMMessage]) -> str:
|
| 313 |
"""Deprecated: mocks disabled to avoid hiding real failures."""
|
|
|
|
| 1 |
"""
|
| 2 |
LLM Client for Overgrowth Pipeline
|
| 3 |
+
Supports multiple providers: OpenAI, Anthropic, Blaxel, SambaNova, Nebius, Hugging Face
|
| 4 |
"""
|
| 5 |
|
| 6 |
import os
|
|
|
|
| 42 |
# Support both standard and MCP hackathon naming conventions
|
| 43 |
self.openai_key = _get_env(["OPENAI_API_KEY", "OPENAI_MCP_1ST_BDAY"])
|
| 44 |
self.anthropic_key = _get_env(["ANTHROPIC_API_KEY", "ANTHROPIC_MCP_1ST_BDAY"])
|
| 45 |
+
self.blaxel_key = _get_env(["BLAXEL_MCP_1ST_BDAY"])
|
| 46 |
+
self.sambanova_key = _get_env(["SAMBA_NOVA_MCP_1ST_BDAY"])
|
| 47 |
+
self.nebius_key = _get_env(["NEBIUS_MCP_1ST_BDAY"])
|
| 48 |
+
self.huggingface_key = _get_env(["HUGGING_FACE_MCP_1ST_BDAY"])
|
| 49 |
|
| 50 |
# Determine which provider to use
|
| 51 |
self.provider = self._detect_provider()
|
|
|
|
| 71 |
status = {
|
| 72 |
"openai_key": "present" if _present(["OPENAI_API_KEY", "OPENAI_MCP_1ST_BDAY"]) else "missing",
|
| 73 |
"anthropic_key": "present" if _present(["ANTHROPIC_API_KEY", "ANTHROPIC_MCP_1ST_BDAY"]) else "missing",
|
| 74 |
+
"blaxel_key": "present" if _present(["BLAXEL_MCP_1ST_BDAY"]) else "missing",
|
| 75 |
+
"sambanova_key": "present" if _present(["SAMBA_NOVA_MCP_1ST_BDAY"]) else "missing",
|
| 76 |
+
"nebius_key": "present" if _present(["NEBIUS_MCP_1ST_BDAY"]) else "missing",
|
| 77 |
+
"huggingface_key": "present" if _present(["HUGGING_FACE_MCP_1ST_BDAY"]) else "missing",
|
| 78 |
"provider": "unknown",
|
| 79 |
}
|
| 80 |
if status["anthropic_key"] == "present":
|
| 81 |
status["provider"] = "anthropic"
|
| 82 |
elif status["openai_key"] == "present":
|
| 83 |
status["provider"] = "openai"
|
| 84 |
+
elif status["blaxel_key"] == "present":
|
| 85 |
+
status["provider"] = "blaxel"
|
| 86 |
+
elif status["sambanova_key"] == "present":
|
| 87 |
+
status["provider"] = "sambanova"
|
| 88 |
+
elif status["nebius_key"] == "present":
|
| 89 |
+
status["provider"] = "nebius"
|
| 90 |
+
elif status["huggingface_key"] == "present":
|
| 91 |
+
status["provider"] = "huggingface"
|
| 92 |
return status
|
| 93 |
|
| 94 |
def _detect_provider(self) -> Optional[str]:
|
|
|
|
| 97 |
return "anthropic"
|
| 98 |
elif self.openai_key:
|
| 99 |
return "openai"
|
| 100 |
+
elif self.blaxel_key:
|
| 101 |
+
return "blaxel"
|
| 102 |
+
elif self.sambanova_key:
|
| 103 |
+
return "sambanova"
|
| 104 |
+
elif self.nebius_key:
|
| 105 |
+
return "nebius"
|
| 106 |
+
elif self.huggingface_key:
|
| 107 |
+
return "huggingface"
|
| 108 |
return None
|
| 109 |
|
| 110 |
def chat(
|
|
|
|
| 125 |
return self._call_openai(messages, temperature, max_tokens, stream)
|
| 126 |
elif self.provider == "anthropic":
|
| 127 |
return self._call_anthropic(messages, temperature, max_tokens, stream)
|
| 128 |
+
elif self.provider == "blaxel":
|
| 129 |
+
return self._call_blaxel(messages, temperature, max_tokens)
|
| 130 |
+
elif self.provider == "sambanova":
|
| 131 |
+
return self._call_sambanova(messages, temperature, max_tokens)
|
| 132 |
+
elif self.provider == "nebius":
|
| 133 |
+
return self._call_nebius(messages, temperature, max_tokens)
|
| 134 |
+
elif self.provider == "huggingface":
|
| 135 |
+
return self._call_huggingface(messages, temperature, max_tokens)
|
| 136 |
|
| 137 |
def chat_stream(
|
| 138 |
self,
|
|
|
|
| 148 |
yield from self._stream_openai(messages, temperature, max_tokens)
|
| 149 |
elif self.provider == "anthropic":
|
| 150 |
yield from self._stream_anthropic(messages, temperature, max_tokens)
|
| 151 |
+
else:
|
| 152 |
+
# Other providers: no streaming support; fall back to single response
|
| 153 |
+
yield self.chat(messages, temperature=temperature, max_tokens=max_tokens)
|
| 154 |
|
| 155 |
def _call_openrouter(self, messages, temperature, max_tokens, stream):
|
| 156 |
"""Call OpenRouter API"""
|
|
|
|
| 343 |
yield from self._stream_openai(messages, temperature, max_tokens)
|
| 344 |
else:
|
| 345 |
raise RuntimeError(f"Anthropic streaming failed: {err_msg}")
|
| 346 |
+
|
| 347 |
+
# ----- Additional Providers (non-streaming) -----
|
| 348 |
+
def _call_blaxel(self, messages, temperature, max_tokens):
|
| 349 |
+
"""Call Blaxel sandbox API (OpenAI-style)."""
|
| 350 |
+
import requests
|
| 351 |
+
call_id = str(uuid.uuid4())
|
| 352 |
+
model = os.getenv("BLAXEL_MODEL", "blaxel/claude-3-haiku")
|
| 353 |
+
base_url = os.getenv("BLAXEL_BASE_URL", "https://api.blaxel.ai/v0")
|
| 354 |
+
endpoint = f\"{base_url.rstrip('/')}/chat/completions\"
|
| 355 |
+
if monitor:
|
| 356 |
+
monitor.start_call(call_id, \"llm\", \"blaxel\", model, temperature=temperature)
|
| 357 |
+
payload = {
|
| 358 |
+
\"model\": model,
|
| 359 |
+
\"messages\": [{\"role\": m.role, \"content\": m.content} for m in messages],
|
| 360 |
+
\"temperature\": temperature,
|
| 361 |
+
\"max_tokens\": max_tokens,
|
| 362 |
+
}
|
| 363 |
+
headers = {\"Authorization\": f\"Bearer {self.blaxel_key}\"}
|
| 364 |
+
try:
|
| 365 |
+
resp = requests.post(endpoint, json=payload, headers=headers, timeout=30)
|
| 366 |
+
resp.raise_for_status()
|
| 367 |
+
data = resp.json()
|
| 368 |
+
content = data.get(\"choices\", [{}])[0].get(\"message\", {}).get(\"content\", \"\")
|
| 369 |
+
usage = data.get(\"usage\", {})
|
| 370 |
+
if monitor:
|
| 371 |
+
monitor.complete_call(
|
| 372 |
+
call_id,
|
| 373 |
+
success=True,
|
| 374 |
+
input_tokens=usage.get(\"prompt_tokens\"),
|
| 375 |
+
output_tokens=usage.get(\"completion_tokens\")
|
| 376 |
+
)
|
| 377 |
+
return content
|
| 378 |
+
except Exception as e:
|
| 379 |
+
if monitor:
|
| 380 |
+
monitor.complete_call(call_id, success=False, error_message=str(e))
|
| 381 |
+
raise
|
| 382 |
+
|
| 383 |
+
def _call_sambanova(self, messages, temperature, max_tokens):
|
| 384 |
+
"""Call SambaNova API (OpenAI-compatible)."""
|
| 385 |
+
import requests
|
| 386 |
+
call_id = str(uuid.uuid4())
|
| 387 |
+
model = os.getenv("SAMBA_NOVA_MODEL", "Meta-Llama-3-8B-Instruct")
|
| 388 |
+
base_url = os.getenv("SAMBA_NOVA_BASE_URL", "https://api.sambanova.ai/v1")
|
| 389 |
+
endpoint = f\"{base_url.rstrip('/')}/chat/completions\"
|
| 390 |
+
if monitor:
|
| 391 |
+
monitor.start_call(call_id, \"llm\", \"sambanova\", model, temperature=temperature)
|
| 392 |
+
payload = {
|
| 393 |
+
\"model\": model,
|
| 394 |
+
\"messages\": [{\"role\": m.role, \"content\": m.content} for m in messages],
|
| 395 |
+
\"temperature\": temperature,
|
| 396 |
+
\"max_tokens\": max_tokens,
|
| 397 |
+
}
|
| 398 |
+
headers = {
|
| 399 |
+
\"Authorization\": f\"Bearer {self.sambanova_key}\",
|
| 400 |
+
\"Content-Type\": \"application/json\"
|
| 401 |
+
}
|
| 402 |
+
try:
|
| 403 |
+
resp = requests.post(endpoint, json=payload, headers=headers, timeout=30)
|
| 404 |
+
resp.raise_for_status()
|
| 405 |
+
data = resp.json()
|
| 406 |
+
content = data.get(\"choices\", [{}])[0].get(\"message\", {}).get(\"content\", \"\")
|
| 407 |
+
usage = data.get(\"usage\", {})
|
| 408 |
+
if monitor:
|
| 409 |
+
monitor.complete_call(
|
| 410 |
+
call_id,
|
| 411 |
+
success=True,
|
| 412 |
+
input_tokens=usage.get(\"prompt_tokens\"),
|
| 413 |
+
output_tokens=usage.get(\"completion_tokens\")
|
| 414 |
+
)
|
| 415 |
+
return content
|
| 416 |
+
except Exception as e:
|
| 417 |
+
if monitor:
|
| 418 |
+
monitor.complete_call(call_id, success=False, error_message=str(e))
|
| 419 |
+
raise
|
| 420 |
+
|
| 421 |
+
def _call_nebius(self, messages, temperature, max_tokens):
|
| 422 |
+
"""Call Nebius Token Factory API (OpenAI-compatible)."""
|
| 423 |
+
import requests
|
| 424 |
+
call_id = str(uuid.uuid4())
|
| 425 |
+
model = os.getenv("NEBIUS_MODEL", "gpt-3.5-turbo")
|
| 426 |
+
base_url = os.getenv("NEBIUS_BASE_URL", "https://api.studio.nebius.ai/v1")
|
| 427 |
+
endpoint = f\"{base_url.rstrip('/')}/chat/completions\"
|
| 428 |
+
if monitor:
|
| 429 |
+
monitor.start_call(call_id, \"llm\", \"nebius\", model, temperature=temperature)
|
| 430 |
+
payload = {
|
| 431 |
+
\"model\": model,
|
| 432 |
+
\"messages\": [{\"role\": m.role, \"content\": m.content} for m in messages],
|
| 433 |
+
\"temperature\": temperature,
|
| 434 |
+
\"max_tokens\": max_tokens,
|
| 435 |
+
}
|
| 436 |
+
headers = {
|
| 437 |
+
\"Authorization\": f\"Bearer {self.nebius_key}\",
|
| 438 |
+
\"Content-Type\": \"application/json\"
|
| 439 |
+
}
|
| 440 |
+
try:
|
| 441 |
+
resp = requests.post(endpoint, json=payload, headers=headers, timeout=30)
|
| 442 |
+
resp.raise_for_status()
|
| 443 |
+
data = resp.json()
|
| 444 |
+
content = data.get(\"choices\", [{}])[0].get(\"message\", {}).get(\"content\", \"\")
|
| 445 |
+
usage = data.get(\"usage\", {})
|
| 446 |
+
if monitor:
|
| 447 |
+
monitor.complete_call(
|
| 448 |
+
call_id,
|
| 449 |
+
success=True,
|
| 450 |
+
input_tokens=usage.get(\"prompt_tokens\"),
|
| 451 |
+
output_tokens=usage.get(\"completion_tokens\")
|
| 452 |
+
)
|
| 453 |
+
return content
|
| 454 |
+
except Exception as e:
|
| 455 |
+
if monitor:
|
| 456 |
+
monitor.complete_call(call_id, success=False, error_message=str(e))
|
| 457 |
+
raise
|
| 458 |
+
|
| 459 |
+
def _call_huggingface(self, messages, temperature, max_tokens):
|
| 460 |
+
"""Call Hugging Face Inference API (text generation)."""
|
| 461 |
+
import requests
|
| 462 |
+
call_id = str(uuid.uuid4())
|
| 463 |
+
model = os.getenv("HUGGINGFACE_MODEL", "tiiuae/falcon-7b-instruct")
|
| 464 |
+
endpoint = f\"https://api-inference.huggingface.co/models/{model}\"
|
| 465 |
+
if monitor:
|
| 466 |
+
monitor.start_call(call_id, \"llm\", \"huggingface\", model, temperature=temperature)
|
| 467 |
+
# Simple prompt concatenation
|
| 468 |
+
prompt = \"\\n\".join(f\"{m.role.upper()}: {m.content}\" for m in messages)
|
| 469 |
+
payload = {
|
| 470 |
+
\"inputs\": prompt,
|
| 471 |
+
\"parameters\": {
|
| 472 |
+
\"max_new_tokens\": max_tokens,
|
| 473 |
+
\"temperature\": temperature,
|
| 474 |
+
\"return_full_text\": False
|
| 475 |
+
}
|
| 476 |
+
}
|
| 477 |
+
headers = {
|
| 478 |
+
\"Authorization\": f\"Bearer {self.huggingface_key}\",
|
| 479 |
+
\"Content-Type\": \"application/json\"
|
| 480 |
+
}
|
| 481 |
+
try:
|
| 482 |
+
resp = requests.post(endpoint, json=payload, headers=headers, timeout=60)
|
| 483 |
+
resp.raise_for_status()
|
| 484 |
+
data = resp.json()
|
| 485 |
+
# Response can be list or dict
|
| 486 |
+
text = \"\"
|
| 487 |
+
if isinstance(data, list) and data:
|
| 488 |
+
if isinstance(data[0], dict):
|
| 489 |
+
text = data[0].get(\"generated_text\", \"\") or data[0].get(\"generated_texts\", \"\")
|
| 490 |
+
else:
|
| 491 |
+
text = str(data[0])
|
| 492 |
+
elif isinstance(data, dict):
|
| 493 |
+
text = data.get(\"generated_text\", \"\") or data.get(\"generated_texts\", \"\") or \"\"
|
| 494 |
+
if monitor:
|
| 495 |
+
monitor.complete_call(call_id, success=True)
|
| 496 |
+
return text
|
| 497 |
+
except Exception as e:
|
| 498 |
+
if monitor:
|
| 499 |
+
monitor.complete_call(call_id, success=False, error_message=str(e))
|
| 500 |
+
raise
|
| 501 |
|
| 502 |
def _mock_response(self, messages: List[LLMMessage]) -> str:
|
| 503 |
"""Deprecated: mocks disabled to avoid hiding real failures."""
|