Files
tracer-cloud--opensre/core/llm/factory.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

148 lines
5.0 KiB
Python

"""Single LLM factory: one provider-routing decision for every model role.
The provider/transport decision (CLI-backed vs LiteLLM vs native SDK, and which
vendor) is resolved once in :func:`resolve_llm_route` and reused by every role, so
an Azure/LiteLLM routing fix cannot drift between the investigation agent and the
reasoning/classification/toolcall clients.
Roles differ only in the *client family* they build: :data:`LLMRole.AGENT` builds
a tool-calling client (``tool_schemas`` / ``invoke``); the other roles build the
streaming reasoning client (``invoke`` / ``invoke_stream`` / ``with_structured_output``)
for a given model tier. ``get_llm(role)`` is the single entrypoint — callers pass an ``LLMRole``.
Public interface:
- ``get_llm(role)`` — the cached client for a role; the entrypoint every surface calls.
- ``reset_llm_clients()`` — clear the cache after a ``/model`` switch or env change.
``resolve_llm_route()`` (the routing decision) and ``build_llm_client(model_type)``
(an uncached build) are exposed for tests and benchmarks, not day-to-day callers.
This module owns routing and the public entrypoint. Constructing the concrete
client for a route lives in :mod:`core.llm.client_builders`; the client cache lives
in :mod:`core.llm.internal.client_cache`.
"""
from __future__ import annotations
import re
from enum import Enum
from typing import Any, Literal, overload
from core.llm import client_builders
from core.llm.internal.client_cache import LLMClientCache
from core.llm.internal.client_cache_key import current_llm_client_cache_key
from core.llm.transport_mode import use_litellm_for_provider
from core.llm.types import AgentLLMClient, LLMRoute, ModelType
class LLMRole(Enum):
"""The model tier a caller needs, independent of provider/transport."""
AGENT = "agent" # tool-calling ReAct (action, gather, investigation)
REASONING = "reasoning" # streamed assistant answer / complex reasoning
CLASSIFICATION = "classification" # mid-tier classifier
TOOLCALL = "toolcall" # lightweight tool selection / action planning
# The non-agent roles map onto the model-tier attribute suffix used by settings.
_MODEL_TYPE_BY_ROLE: dict[LLMRole, ModelType] = {
LLMRole.REASONING: "reasoning",
LLMRole.CLASSIFICATION: "classification",
LLMRole.TOOLCALL: "toolcall",
}
def resolve_llm_route() -> LLMRoute:
"""Resolve settings + runtime provider + transport once (the single routing decision)."""
settings = _resolve_settings_or_raise()
from config.llm_auth.auth_method import (
effective_llm_provider,
get_configured_llm_auth_method,
)
provider = settings.provider
runtime_provider = effective_llm_provider(provider, get_configured_llm_auth_method(provider))
return LLMRoute(
settings=settings,
provider=runtime_provider,
cli_provider_registration=_cli_provider_registration(runtime_provider),
use_litellm=use_litellm_for_provider(runtime_provider),
)
def _resolve_settings_or_raise() -> Any:
from pydantic import ValidationError
from config.config import resolve_llm_settings
try:
return resolve_llm_settings()
except ValidationError as exc:
errors = exc.errors()
if len(errors) == 1:
msg = re.sub(r"^[Vv]alue error,\s*", "", errors[0].get("msg", "")).strip()
raise RuntimeError(msg or str(exc)) from exc
raise RuntimeError(str(exc)) from exc
def _cli_provider_registration(provider: str) -> Any:
"""CLI registry entry for *provider*, or None."""
from platform.harness_ports import cli_provider_registration
return cli_provider_registration(provider)
# ---------------------------------------------------------------------------
# Public entrypoint (cache lives in ``core.llm.internal.client_cache``)
# ---------------------------------------------------------------------------
_cache = LLMClientCache()
@overload
def get_llm(role: Literal[LLMRole.AGENT]) -> AgentLLMClient:
pass
@overload
def get_llm(role: LLMRole) -> Any:
pass
def get_llm(role: LLMRole) -> Any:
"""Return the cached LLM client for *role*, building it once per config."""
cached = _cache.get(role, current_llm_client_cache_key())
if cached is not None:
return cached
route = resolve_llm_route()
if role is LLMRole.AGENT:
client = client_builders.build_agent_client(route)
else:
client = client_builders.build_reasoning_client(route, _MODEL_TYPE_BY_ROLE[role])
_cache.store(role, client)
return client
def reset_llm_clients() -> None:
"""Clear all cached role clients (tests, benchmarks, ``/model`` switch, env sync)."""
_cache.clear()
def build_llm_client(model_type: ModelType) -> Any:
"""Build a fresh (uncached) reasoning-family client for the current config."""
return client_builders.build_reasoning_client(resolve_llm_route(), model_type)
__all__ = [
"LLMRole",
"LLMRoute",
"build_llm_client",
"get_llm",
"reset_llm_clients",
"resolve_llm_route",
]