2026-05-13 16:43:53 +08:00
|
|
|
import os
|
2026-03-28 01:01:13 +08:00
|
|
|
from typing import Optional, Dict
|
|
|
|
|
|
|
|
|
|
from nanobot.providers.azure_openai_provider import AzureOpenAIProvider
|
|
|
|
|
from nanobot.providers.openai_codex_provider import OpenAICodexProvider
|
|
|
|
|
from nanobot.providers.registry import find_by_name
|
|
|
|
|
|
2026-03-28 14:46:50 +08:00
|
|
|
from app.core.patched_openai_compat_provider import PatchedOpenAICompatProvider
|
|
|
|
|
|
2026-03-28 01:01:13 +08:00
|
|
|
|
|
|
|
|
def normalize_provider_name(provider: Optional[str]) -> Optional[str]:
|
|
|
|
|
if not provider:
|
|
|
|
|
return None
|
|
|
|
|
normalized = provider.strip().lower()
|
|
|
|
|
alias_map = {
|
|
|
|
|
"azure": "azure_openai",
|
|
|
|
|
"local": "vllm",
|
|
|
|
|
}
|
|
|
|
|
return alias_map.get(normalized, normalized)
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 16:43:53 +08:00
|
|
|
def _running_in_docker() -> bool:
|
|
|
|
|
# Best-effort, cross-platform detection.
|
|
|
|
|
if os.environ.get("DATACLAW_RUNNING_IN_DOCKER", "").strip().lower() in ("1", "true", "yes", "y"):
|
|
|
|
|
return True
|
|
|
|
|
return os.path.exists("/.dockerenv")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _rewrite_localhost_api_base(api_base: Optional[str]) -> Optional[str]:
|
|
|
|
|
"""
|
|
|
|
|
When running inside Docker, `localhost` points to the container itself.
|
|
|
|
|
For host-local LLMs (Ollama/vLLM), users often configure `http://localhost:...`,
|
|
|
|
|
which breaks in containers. We rewrite it to `host.docker.internal`.
|
|
|
|
|
"""
|
|
|
|
|
if not api_base:
|
|
|
|
|
return api_base
|
|
|
|
|
base = api_base.strip()
|
|
|
|
|
if base.startswith("http://localhost") or base.startswith("https://localhost"):
|
|
|
|
|
return base.replace("://localhost", "://host.docker.internal", 1)
|
|
|
|
|
if base.startswith("http://127.0.0.1") or base.startswith("https://127.0.0.1"):
|
|
|
|
|
return base.replace("://127.0.0.1", "://host.docker.internal", 1)
|
|
|
|
|
return api_base
|
|
|
|
|
|
|
|
|
|
|
2026-03-28 01:01:13 +08:00
|
|
|
def build_llm_provider(
|
|
|
|
|
*,
|
|
|
|
|
model: str,
|
|
|
|
|
provider: Optional[str] = None,
|
|
|
|
|
api_key: Optional[str] = None,
|
|
|
|
|
api_base: Optional[str] = None,
|
|
|
|
|
extra_headers: Optional[Dict[str, str]] = None,
|
|
|
|
|
):
|
|
|
|
|
provider_name = normalize_provider_name(provider)
|
|
|
|
|
spec = find_by_name(provider_name) if provider_name else None
|
|
|
|
|
backend = spec.backend if spec else "openai_compat"
|
2026-05-13 16:43:53 +08:00
|
|
|
if _running_in_docker():
|
|
|
|
|
api_base = _rewrite_localhost_api_base(api_base)
|
2026-03-28 01:01:13 +08:00
|
|
|
|
|
|
|
|
if backend == "openai_codex" or model.startswith("openai-codex/"):
|
|
|
|
|
return OpenAICodexProvider(default_model=model)
|
|
|
|
|
|
|
|
|
|
if backend == "azure_openai":
|
|
|
|
|
if not api_key or not api_base:
|
|
|
|
|
raise ValueError("Azure OpenAI requires api_key and api_base.")
|
|
|
|
|
return AzureOpenAIProvider(
|
|
|
|
|
api_key=api_key,
|
|
|
|
|
api_base=api_base,
|
|
|
|
|
default_model=model,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if backend == "anthropic":
|
|
|
|
|
from nanobot.providers.anthropic_provider import AnthropicProvider
|
|
|
|
|
|
|
|
|
|
return AnthropicProvider(
|
|
|
|
|
api_key=api_key,
|
|
|
|
|
api_base=api_base,
|
|
|
|
|
default_model=model,
|
|
|
|
|
extra_headers=extra_headers,
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-28 14:46:50 +08:00
|
|
|
return PatchedOpenAICompatProvider(
|
2026-03-28 01:01:13 +08:00
|
|
|
api_key=api_key,
|
|
|
|
|
api_base=api_base,
|
|
|
|
|
default_model=model,
|
|
|
|
|
extra_headers=extra_headers,
|
|
|
|
|
spec=spec,
|
|
|
|
|
)
|