chopratejas--headroom
0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
527 行
18 KiB
Python
527 行
18 KiB
Python
"""any-llm backend for Headroom.
|
|
|
|
Talk to 38+ LLM providers (OpenAI, Mistral, Groq, Ollama, Bedrock, etc.)
|
|
through a single interface. Auth and format translation handled automatically.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import uuid
|
|
from collections.abc import AsyncIterator
|
|
from typing import Any, cast
|
|
|
|
from .base import Backend, BackendResponse, StreamEvent
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
from any_llm import AnyLLM
|
|
|
|
ANYLLM_AVAILABLE = True
|
|
except ImportError:
|
|
ANYLLM_AVAILABLE = False
|
|
AnyLLM = None # type: ignore
|
|
|
|
|
|
class AnyLLMBackend(Backend):
|
|
"""Backend using any-llm for multi-provider support."""
|
|
|
|
def __init__(
|
|
self,
|
|
provider: str = "openai",
|
|
api_key: str | None = None,
|
|
api_base: str | None = None,
|
|
):
|
|
if not ANYLLM_AVAILABLE:
|
|
raise ImportError(
|
|
"any-llm-sdk is required for AnyLLMBackend. "
|
|
"Install with: pip install 'any-llm-sdk[all]'"
|
|
)
|
|
|
|
self.provider = provider.lower()
|
|
# Normalize empty-string overrides (e.g. an env var set to "") to None
|
|
# so provider defaults stay active instead of forwarding a blank value.
|
|
self.api_key = api_key or None
|
|
self.api_base = api_base or None
|
|
|
|
# Create the AnyLLM instance once and reuse. api_key/api_base are only
|
|
# forwarded when set so providers keep their own env-var defaults
|
|
# (e.g. OPENAI_API_KEY / OPENAI_BASE_URL) otherwise.
|
|
create_kwargs: dict[str, Any] = {}
|
|
if self.api_key is not None:
|
|
create_kwargs["api_key"] = self.api_key
|
|
if self.api_base is not None:
|
|
create_kwargs["api_base"] = self.api_base
|
|
self.llm = AnyLLM.create(self.provider, **create_kwargs)
|
|
|
|
logger.info(
|
|
f"any-llm backend initialized (provider={provider}, "
|
|
f"api_base={self.api_base or 'default'})"
|
|
)
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return f"anyllm-{self.provider}"
|
|
|
|
def map_model_id(self, model: str) -> str:
|
|
"""Pass through model name - any-llm handles provider-specific naming."""
|
|
return model
|
|
|
|
def supports_model(self, model: str) -> bool:
|
|
"""any-llm supports any model the provider supports."""
|
|
return True
|
|
|
|
def _convert_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
"""Convert Anthropic message format to OpenAI/any-llm format."""
|
|
converted = []
|
|
for msg in messages:
|
|
role = msg.get("role", "user")
|
|
content = msg.get("content", "")
|
|
|
|
if isinstance(content, str):
|
|
converted.append({"role": role, "content": content})
|
|
continue
|
|
|
|
if isinstance(content, list):
|
|
text_parts = []
|
|
has_complex_content = False
|
|
|
|
for block in content:
|
|
if isinstance(block, dict):
|
|
if block.get("type") == "text":
|
|
text_parts.append(block.get("text", ""))
|
|
elif block.get("type") in ("tool_use", "tool_result", "image"):
|
|
has_complex_content = True
|
|
break
|
|
|
|
if not has_complex_content and text_parts:
|
|
converted.append({"role": role, "content": "\n".join(text_parts)})
|
|
else:
|
|
openai_content = self._convert_content_blocks(content)
|
|
converted.append({"role": role, "content": openai_content})
|
|
|
|
return converted
|
|
|
|
def _convert_content_blocks(self, blocks: list[dict[str, Any]]) -> list[dict[str, Any]] | str:
|
|
"""Convert Anthropic content blocks to OpenAI format."""
|
|
openai_blocks = []
|
|
|
|
for block in blocks:
|
|
block_type = block.get("type")
|
|
|
|
if block_type == "text":
|
|
openai_blocks.append({"type": "text", "text": block.get("text", "")})
|
|
|
|
elif block_type == "image":
|
|
source = block.get("source", {})
|
|
if source.get("type") == "base64":
|
|
media_type = source.get("media_type", "image/png")
|
|
data = source.get("data", "")
|
|
openai_blocks.append(
|
|
{
|
|
"type": "image_url",
|
|
"image_url": {"url": f"data:{media_type};base64,{data}"},
|
|
}
|
|
)
|
|
elif source.get("type") == "url":
|
|
openai_blocks.append(
|
|
{"type": "image_url", "image_url": {"url": source.get("url", "")}}
|
|
)
|
|
|
|
if len(openai_blocks) == 1 and openai_blocks[0].get("type") == "text":
|
|
return str(openai_blocks[0]["text"])
|
|
|
|
return openai_blocks if openai_blocks else ""
|
|
|
|
def _to_anthropic_response(
|
|
self,
|
|
response: Any,
|
|
original_model: str,
|
|
) -> dict[str, Any]:
|
|
"""Convert any-llm/OpenAI response to Anthropic format."""
|
|
msg_id = f"msg_{uuid.uuid4().hex[:24]}"
|
|
|
|
choice = response.choices[0]
|
|
message = choice.message
|
|
|
|
content = []
|
|
if message.content:
|
|
content.append({"type": "text", "text": message.content})
|
|
|
|
if hasattr(message, "tool_calls") and message.tool_calls:
|
|
for tc in message.tool_calls:
|
|
args = tc.function.arguments
|
|
if isinstance(args, str):
|
|
try:
|
|
args = json.loads(args)
|
|
except (json.JSONDecodeError, TypeError):
|
|
pass
|
|
content.append(
|
|
{
|
|
"type": "tool_use",
|
|
"id": tc.id,
|
|
"name": tc.function.name,
|
|
"input": args,
|
|
}
|
|
)
|
|
|
|
stop_reason_map = {
|
|
"stop": "end_turn",
|
|
"length": "max_tokens",
|
|
"tool_calls": "tool_use",
|
|
"content_filter": "end_turn",
|
|
}
|
|
finish_reason = getattr(choice, "finish_reason", "stop") or "stop"
|
|
stop_reason = stop_reason_map.get(finish_reason, "end_turn")
|
|
|
|
usage = {"input_tokens": 0, "output_tokens": 0}
|
|
if hasattr(response, "usage") and response.usage:
|
|
usage = {
|
|
"input_tokens": getattr(response.usage, "prompt_tokens", 0) or 0,
|
|
"output_tokens": getattr(response.usage, "completion_tokens", 0) or 0,
|
|
}
|
|
|
|
return {
|
|
"id": msg_id,
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": content,
|
|
"model": original_model,
|
|
"stop_reason": stop_reason,
|
|
"stop_sequence": None,
|
|
"usage": usage,
|
|
}
|
|
|
|
async def send_message(
|
|
self,
|
|
body: dict[str, Any],
|
|
headers: dict[str, str],
|
|
) -> BackendResponse:
|
|
"""Send message via any-llm."""
|
|
original_model = body.get("model", "gpt-4o")
|
|
|
|
try:
|
|
messages = self._convert_messages(body.get("messages", []))
|
|
|
|
# Add system message if present
|
|
if "system" in body:
|
|
system = body["system"]
|
|
if isinstance(system, str):
|
|
messages.insert(0, {"role": "system", "content": system})
|
|
elif isinstance(system, list):
|
|
system_text = " ".join(
|
|
s.get("text", "") if isinstance(s, dict) else str(s) for s in system
|
|
)
|
|
messages.insert(0, {"role": "system", "content": system_text})
|
|
|
|
kwargs: dict[str, Any] = {"model": original_model, "messages": messages}
|
|
|
|
if "max_tokens" in body:
|
|
kwargs["max_tokens"] = body["max_tokens"]
|
|
if "temperature" in body:
|
|
kwargs["temperature"] = body["temperature"]
|
|
if "top_p" in body:
|
|
kwargs["top_p"] = body["top_p"]
|
|
if "stop_sequences" in body:
|
|
kwargs["stop"] = body["stop_sequences"]
|
|
if "tools" in body:
|
|
kwargs["tools"] = body["tools"]
|
|
if "tool_choice" in body:
|
|
kwargs["tool_choice"] = body["tool_choice"]
|
|
|
|
logger.debug(f"any-llm request: provider={self.provider}, model={original_model}")
|
|
|
|
response = await self.llm.acompletion(**kwargs)
|
|
anthropic_response = self._to_anthropic_response(response, original_model)
|
|
|
|
return BackendResponse(
|
|
body=anthropic_response,
|
|
status_code=200,
|
|
headers={"content-type": "application/json"},
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"any-llm error: {e}")
|
|
return self._error_response(e)
|
|
|
|
async def stream_message(
|
|
self,
|
|
body: dict[str, Any],
|
|
headers: dict[str, str],
|
|
) -> AsyncIterator[StreamEvent]:
|
|
"""Stream message via any-llm."""
|
|
original_model = body.get("model", "gpt-4o")
|
|
|
|
try:
|
|
messages = self._convert_messages(body.get("messages", []))
|
|
|
|
if "system" in body:
|
|
system = body["system"]
|
|
if isinstance(system, str):
|
|
messages.insert(0, {"role": "system", "content": system})
|
|
|
|
kwargs: dict[str, Any] = {
|
|
"model": original_model,
|
|
"messages": messages,
|
|
"stream": True,
|
|
}
|
|
|
|
if "max_tokens" in body:
|
|
kwargs["max_tokens"] = body["max_tokens"]
|
|
if "temperature" in body:
|
|
kwargs["temperature"] = body["temperature"]
|
|
if "top_p" in body:
|
|
kwargs["top_p"] = body["top_p"]
|
|
if "stop_sequences" in body:
|
|
kwargs["stop"] = body["stop_sequences"]
|
|
if "tools" in body:
|
|
kwargs["tools"] = body["tools"]
|
|
if "tool_choice" in body:
|
|
kwargs["tool_choice"] = body["tool_choice"]
|
|
|
|
msg_id = f"msg_{uuid.uuid4().hex[:24]}"
|
|
|
|
yield StreamEvent(
|
|
event_type="message_start",
|
|
data={
|
|
"type": "message_start",
|
|
"message": {
|
|
"id": msg_id,
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [],
|
|
"model": original_model,
|
|
"stop_reason": None,
|
|
"stop_sequence": None,
|
|
"usage": {"input_tokens": 0, "output_tokens": 0},
|
|
},
|
|
},
|
|
)
|
|
|
|
yield StreamEvent(
|
|
event_type="content_block_start",
|
|
data={
|
|
"type": "content_block_start",
|
|
"index": 0,
|
|
"content_block": {"type": "text", "text": ""},
|
|
},
|
|
)
|
|
|
|
stream_response = await self.llm.acompletion(**kwargs)
|
|
output_tokens = 0
|
|
|
|
async for chunk in cast(AsyncIterator[Any], stream_response):
|
|
if hasattr(chunk, "choices") and chunk.choices:
|
|
delta = chunk.choices[0].delta
|
|
if hasattr(delta, "content") and delta.content:
|
|
yield StreamEvent(
|
|
event_type="content_block_delta",
|
|
data={
|
|
"type": "content_block_delta",
|
|
"index": 0,
|
|
"delta": {"type": "text_delta", "text": delta.content},
|
|
},
|
|
)
|
|
output_tokens += 1
|
|
|
|
yield StreamEvent(
|
|
event_type="content_block_stop",
|
|
data={"type": "content_block_stop", "index": 0},
|
|
)
|
|
|
|
yield StreamEvent(
|
|
event_type="message_delta",
|
|
data={
|
|
"type": "message_delta",
|
|
"delta": {"stop_reason": "end_turn", "stop_sequence": None},
|
|
"usage": {"output_tokens": output_tokens},
|
|
},
|
|
)
|
|
|
|
yield StreamEvent(
|
|
event_type="message_stop",
|
|
data={"type": "message_stop"},
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"any-llm streaming error: {e}")
|
|
yield StreamEvent(
|
|
event_type="error",
|
|
data={
|
|
"type": "error",
|
|
"error": {"type": "api_error", "message": str(e)},
|
|
},
|
|
)
|
|
|
|
async def send_openai_message(
|
|
self,
|
|
body: dict[str, Any],
|
|
headers: dict[str, str],
|
|
) -> BackendResponse:
|
|
"""Send OpenAI-format message via any-llm (no conversion)."""
|
|
original_model = body.get("model", "gpt-4o")
|
|
|
|
try:
|
|
kwargs: dict[str, Any] = {"model": original_model, "messages": body.get("messages", [])}
|
|
|
|
for param in [
|
|
"max_tokens",
|
|
"temperature",
|
|
"top_p",
|
|
"stop",
|
|
"tools",
|
|
"tool_choice",
|
|
"response_format",
|
|
"seed",
|
|
"n",
|
|
]:
|
|
if param in body:
|
|
kwargs[param] = body[param]
|
|
|
|
logger.debug(
|
|
f"any-llm OpenAI request: provider={self.provider}, model={original_model}"
|
|
)
|
|
|
|
response: Any = await self.llm.acompletion(**kwargs)
|
|
|
|
choices = []
|
|
for c in response.choices:
|
|
msg: dict[str, Any] = {
|
|
"role": c.message.role,
|
|
"content": c.message.content,
|
|
}
|
|
if c.message.tool_calls:
|
|
msg["tool_calls"] = [
|
|
{
|
|
"id": tc.id,
|
|
"type": "function",
|
|
**(
|
|
{
|
|
"function": {
|
|
"name": tc.function.name,
|
|
"arguments": tc.function.arguments,
|
|
}
|
|
}
|
|
if hasattr(tc, "function") and tc.function
|
|
else {}
|
|
),
|
|
}
|
|
for tc in c.message.tool_calls
|
|
]
|
|
choices.append(
|
|
{
|
|
"index": c.index,
|
|
"message": msg,
|
|
"finish_reason": c.finish_reason,
|
|
}
|
|
)
|
|
|
|
usage = response.usage
|
|
response_dict: dict[str, Any] = {
|
|
"id": response.id,
|
|
"object": "chat.completion",
|
|
"created": response.created,
|
|
"model": original_model,
|
|
"choices": choices,
|
|
"usage": {
|
|
"prompt_tokens": usage.prompt_tokens if usage else 0,
|
|
"completion_tokens": usage.completion_tokens if usage else 0,
|
|
"total_tokens": usage.total_tokens if usage else 0,
|
|
},
|
|
}
|
|
|
|
return BackendResponse(
|
|
body=response_dict,
|
|
status_code=200,
|
|
headers={"content-type": "application/json"},
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"any-llm OpenAI error: {e}")
|
|
return self._error_response(e, openai_format=True)
|
|
|
|
def _error_response(self, e: Exception, openai_format: bool = False) -> BackendResponse:
|
|
"""Build error response from exception."""
|
|
error_type = "api_error"
|
|
status_code = 500
|
|
|
|
error_str = str(e).lower()
|
|
if "authentication" in error_str or "api_key" in error_str or "api key" in error_str:
|
|
error_type = "invalid_api_key" if openai_format else "authentication_error"
|
|
status_code = 401
|
|
elif "rate" in error_str or "limit" in error_str:
|
|
error_type = "rate_limit_exceeded" if openai_format else "rate_limit_error"
|
|
status_code = 429
|
|
elif "not found" in error_str or "model" in error_str:
|
|
error_type = "model_not_found" if openai_format else "not_found_error"
|
|
status_code = 404
|
|
|
|
body: dict[str, Any]
|
|
if openai_format:
|
|
body = {"error": {"message": str(e), "type": error_type, "code": error_type}}
|
|
else:
|
|
body = {"type": "error", "error": {"type": error_type, "message": str(e)}}
|
|
|
|
return BackendResponse(body=body, status_code=status_code, error=str(e))
|
|
|
|
async def stream_openai_message(
|
|
self,
|
|
body: dict[str, Any],
|
|
headers: dict[str, str],
|
|
) -> AsyncIterator[str]:
|
|
"""Stream OpenAI-format chat completion via any-llm.
|
|
|
|
Yields SSE-formatted strings ready to send to the client.
|
|
"""
|
|
original_model = body.get("model", "gpt-4o")
|
|
|
|
try:
|
|
kwargs: dict[str, Any] = {
|
|
"model": original_model,
|
|
"messages": body.get("messages", []),
|
|
"stream": True,
|
|
}
|
|
|
|
for param in [
|
|
"max_tokens",
|
|
"temperature",
|
|
"top_p",
|
|
"stop",
|
|
"tools",
|
|
"tool_choice",
|
|
"response_format",
|
|
"seed",
|
|
"n",
|
|
]:
|
|
if param in body:
|
|
kwargs[param] = body[param]
|
|
|
|
if "stream_options" in body:
|
|
kwargs["stream_options"] = body["stream_options"]
|
|
|
|
stream_response = await self.llm.acompletion(**kwargs)
|
|
|
|
async for chunk in cast(AsyncIterator[Any], stream_response):
|
|
chunk_dict = chunk.model_dump(exclude_none=True, exclude_unset=True)
|
|
yield f"data: {json.dumps(chunk_dict)}\n\n"
|
|
|
|
yield "data: [DONE]\n\n"
|
|
|
|
except Exception as e:
|
|
logger.error(f"any-llm OpenAI streaming error: {e}")
|
|
error_data = {
|
|
"error": {
|
|
"message": str(e),
|
|
"type": "api_error",
|
|
"code": "backend_error",
|
|
}
|
|
}
|
|
yield f"data: {json.dumps(error_data)}\n\n"
|
|
yield "data: [DONE]\n\n"
|
|
|
|
async def close(self) -> None:
|
|
"""Clean up (no-op for any-llm)."""
|
|
pass
|