mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47:54 +08:00
core/runtime: route clients by model-level preset
This commit is contained in:
@@ -268,7 +268,7 @@ async def _run_chat( # noqa: C901, PLR0912, PLR0915 — chat orchestration
|
|||||||
preferred_model=preferred_model,
|
preferred_model=preferred_model,
|
||||||
interactive=interactive,
|
interactive=interactive,
|
||||||
):
|
):
|
||||||
client = create_client(provider)
|
client = create_client(provider, model=preferred_model)
|
||||||
try:
|
try:
|
||||||
if client_supports_models(client):
|
if client_supports_models(client):
|
||||||
remote_ids = await fetch_remote_model_ids(client)
|
remote_ids = await fetch_remote_model_ids(client)
|
||||||
|
|||||||
@@ -532,8 +532,10 @@ def status_cmd(state: ReplState) -> None:
|
|||||||
last_rounds = state.agent.last_turn_rounds
|
last_rounds = state.agent.last_turn_rounds
|
||||||
ctx_tag = "api" if ctx_src == "api" else "est"
|
ctx_tag = "api" if ctx_src == "api" else "est"
|
||||||
ctx_tilde = "" if ctx_src == "api" else "~"
|
ctx_tilde = "" if ctx_src == "api" else "~"
|
||||||
|
effective = state.effective_provider
|
||||||
click.echo(
|
click.echo(
|
||||||
f"provider={state.provider_name} model={state.model}\n"
|
f"provider={state.provider_name} model={state.model} "
|
||||||
|
f"preset={effective.preset} url={effective.url}\n"
|
||||||
f"session={state.session_id} messages={len(state.agent.messages)} "
|
f"session={state.session_id} messages={len(state.agent.messages)} "
|
||||||
f"pending_retry={pending_disp}\n"
|
f"pending_retry={pending_disp}\n"
|
||||||
f"tools={'on' if state.tools_enabled else 'off'} "
|
f"tools={'on' if state.tools_enabled else 'off'} "
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from plyngent.cli.models_source import (
|
|||||||
fetch_remote_model_ids,
|
fetch_remote_model_ids,
|
||||||
model_choices_for_provider,
|
model_choices_for_provider,
|
||||||
)
|
)
|
||||||
|
from plyngent.config import EffectiveProvider, resolve_effective_provider
|
||||||
from plyngent.memory.database.store import normalize_workspace
|
from plyngent.memory.database.store import normalize_workspace
|
||||||
from plyngent.runtime import create_client
|
from plyngent.runtime import create_client
|
||||||
from plyngent.tools import InstanceState, SessionState
|
from plyngent.tools import InstanceState, SessionState
|
||||||
@@ -72,7 +73,7 @@ class ReplState:
|
|||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
# DeepSeek client uses a compatible but distinct param type; treat as ChatClient.
|
# DeepSeek client uses a compatible but distinct param type; treat as ChatClient.
|
||||||
self.client = cast("ChatClient", cast("object", create_client(self.provider)))
|
self.client = cast("ChatClient", cast("object", create_client(self.provider, model=self.model)))
|
||||||
self.workspace = Path(self.workspace).expanduser().resolve()
|
self.workspace = Path(self.workspace).expanduser().resolve()
|
||||||
self.instance_state.workspace_root = self.workspace
|
self.instance_state.workspace_root = self.workspace
|
||||||
self.instance_state.workspace.root = self.workspace
|
self.instance_state.workspace.root = self.workspace
|
||||||
@@ -274,7 +275,7 @@ class ReplState:
|
|||||||
# Preserve live stream toggle if agent already exists.
|
# Preserve live stream toggle if agent already exists.
|
||||||
if hasattr(self, "agent"):
|
if hasattr(self, "agent"):
|
||||||
self.stream_enabled = self.agent.stream
|
self.stream_enabled = self.agent.stream
|
||||||
self.client = cast("ChatClient", cast("object", create_client(self.provider)))
|
self.client = cast("ChatClient", cast("object", create_client(self.provider, model=self.model)))
|
||||||
self.agent = self._make_agent()
|
self.agent = self._make_agent()
|
||||||
# Restore history without re-marking already-stored messages as dirty.
|
# Restore history without re-marking already-stored messages as dirty.
|
||||||
self.agent.replace_messages(messages, persist_from=persist_from)
|
self.agent.replace_messages(messages, persist_from=persist_from)
|
||||||
@@ -285,9 +286,16 @@ class ReplState:
|
|||||||
self.invalidate_remote_models()
|
self.invalidate_remote_models()
|
||||||
|
|
||||||
def _models_cache_key(self) -> tuple[str, str]:
|
def _models_cache_key(self) -> tuple[str, str]:
|
||||||
|
# Remote model lists belong to the provider endpoint used for discovery,
|
||||||
|
# not model-level overrides (the model is not selected yet there).
|
||||||
url = getattr(self.provider, "url", "") or ""
|
url = getattr(self.provider, "url", "") or ""
|
||||||
return (self.provider_name, str(url))
|
return (self.provider_name, str(url))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effective_provider(self) -> EffectiveProvider:
|
||||||
|
"""Selected provider after model-level preset/url overrides."""
|
||||||
|
return resolve_effective_provider(self.provider, model=self.model)
|
||||||
|
|
||||||
def invalidate_remote_models(self) -> None:
|
def invalidate_remote_models(self) -> None:
|
||||||
"""Drop cached remote model catalog (provider/client change)."""
|
"""Drop cached remote model catalog (provider/client change)."""
|
||||||
self._remote_models = None
|
self._remote_models = None
|
||||||
|
|||||||
@@ -3,13 +3,7 @@ from __future__ import annotations
|
|||||||
import math
|
import math
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from plyngent.config.models import (
|
from plyngent.config.routing import EffectiveProvider, resolve_effective_provider
|
||||||
DeepseekProvider,
|
|
||||||
HttpTimeoutConfig,
|
|
||||||
OpenAICompatibleProvider,
|
|
||||||
OpenAIProvider,
|
|
||||||
Provider,
|
|
||||||
)
|
|
||||||
from plyngent.lmproto.deepseek import DeepseekOpenAIClient
|
from plyngent.lmproto.deepseek import DeepseekOpenAIClient
|
||||||
from plyngent.lmproto.openai import OpenAIClient
|
from plyngent.lmproto.openai import OpenAIClient
|
||||||
from plyngent.lmproto.openai_compatible import OpenAICompatibleClient, OpenAIConfig
|
from plyngent.lmproto.openai_compatible import OpenAICompatibleClient, OpenAIConfig
|
||||||
@@ -20,11 +14,11 @@ from plyngent.lmproto.openai_compatible.config import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Mapping
|
|
||||||
|
|
||||||
from plyngent.agent.responses_client import ResponsesChatClient
|
from plyngent.agent.responses_client import ResponsesChatClient
|
||||||
|
from plyngent.config.models import HttpTimeoutConfig, Provider
|
||||||
|
|
||||||
DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"
|
DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"
|
||||||
|
DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1"
|
||||||
DEFAULT_DEEPSEEK_BASE_URL = "https://api.deepseek.com"
|
DEFAULT_DEEPSEEK_BASE_URL = "https://api.deepseek.com"
|
||||||
|
|
||||||
type ProtocolClient = OpenAIClient | OpenAICompatibleClient | DeepseekOpenAIClient | ResponsesChatClient
|
type ProtocolClient = OpenAIClient | OpenAICompatibleClient | DeepseekOpenAIClient | ResponsesChatClient
|
||||||
@@ -62,7 +56,6 @@ def normalize_http_timeout(timeout: float | HttpTimeoutConfig | None) -> HttpTim
|
|||||||
raise InvalidHttpTimeoutError(msg)
|
raise InvalidHttpTimeoutError(msg)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
# Remaining union member: HttpTimeoutConfig
|
|
||||||
connect = DEFAULT_HTTP_CONNECT_TIMEOUT if timeout.connect is None else float(timeout.connect)
|
connect = DEFAULT_HTTP_CONNECT_TIMEOUT if timeout.connect is None else float(timeout.connect)
|
||||||
read = DEFAULT_HTTP_READ_TIMEOUT if timeout.read is None else float(timeout.read)
|
read = DEFAULT_HTTP_READ_TIMEOUT if timeout.read is None else float(timeout.read)
|
||||||
if not math.isfinite(connect) or connect <= 0:
|
if not math.isfinite(connect) or connect <= 0:
|
||||||
@@ -74,60 +67,50 @@ def normalize_http_timeout(timeout: float | HttpTimeoutConfig | None) -> HttpTim
|
|||||||
return (connect, read)
|
return (connect, read)
|
||||||
|
|
||||||
|
|
||||||
def provider_to_openai_config(provider: OpenAIProvider | OpenAICompatibleProvider | DeepseekProvider) -> OpenAIConfig:
|
def provider_to_openai_config(
|
||||||
"""Map a provider config entry to :class:`OpenAIConfig`."""
|
provider: Provider | EffectiveProvider,
|
||||||
if isinstance(provider, OpenAIProvider):
|
*,
|
||||||
base_url = provider.url or DEFAULT_OPENAI_BASE_URL
|
model: str | None = None,
|
||||||
elif isinstance(provider, DeepseekProvider):
|
) -> OpenAIConfig:
|
||||||
base_url = provider.url or DEFAULT_DEEPSEEK_BASE_URL
|
"""Map provider/effective config to the shared OpenAI-compatible HTTP config."""
|
||||||
|
if isinstance(provider, EffectiveProvider):
|
||||||
|
effective = provider
|
||||||
else:
|
else:
|
||||||
base_url = provider.url
|
effective = resolve_effective_provider(provider, model=model)
|
||||||
if not base_url:
|
if not effective.url and effective.preset in {"openai-compatible", "deepseek"}:
|
||||||
msg = "openai-compatible provider requires a non-empty url"
|
msg = f"{effective.preset} provider requires a non-empty url"
|
||||||
raise ProviderNotSupportedError(msg)
|
raise ProviderNotSupportedError(msg)
|
||||||
try:
|
try:
|
||||||
http_timeout = normalize_http_timeout(provider.timeout)
|
http_timeout = normalize_http_timeout(effective.timeout)
|
||||||
except InvalidHttpTimeoutError as exc:
|
except InvalidHttpTimeoutError as exc:
|
||||||
raise ProviderNotSupportedError(str(exc)) from exc
|
raise ProviderNotSupportedError(str(exc)) from exc
|
||||||
return OpenAIConfig(
|
return OpenAIConfig(
|
||||||
access_key_or_token=provider.access_key_or_token,
|
access_key_or_token=effective.access_key_or_token,
|
||||||
base_url=base_url,
|
base_url=effective.url,
|
||||||
timeout=http_timeout,
|
timeout=http_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _deepseek_convention(extras: Mapping[str, str]) -> str:
|
def create_client(provider: Provider, *, model: str | None = None) -> ProtocolClient:
|
||||||
return extras.get("convention", "openai").lower()
|
"""Build a protocol client for *provider*, applying model-level routing.
|
||||||
|
|
||||||
|
``preset`` always decides API conventions: ``openai`` → /responses,
|
||||||
def create_client(provider: Provider) -> ProtocolClient:
|
``openai-compatible`` → /chat/completions, ``anthropic`` → /messages.
|
||||||
"""Build a protocol client for the given provider config entry.
|
|
||||||
|
|
||||||
OpenAI platform providers are wrapped so the agent uses the Responses API
|
|
||||||
while still exposing a chat-completions-shaped interface.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ProviderNotSupportedError: When the provider preset (or DeepSeek convention)
|
|
||||||
has no implemented client yet.
|
|
||||||
"""
|
"""
|
||||||
if isinstance(provider, OpenAIProvider):
|
effective = resolve_effective_provider(provider, model=model)
|
||||||
|
if effective.preset == "openai":
|
||||||
from plyngent.agent.responses_client import wrap_openai_for_agent
|
from plyngent.agent.responses_client import wrap_openai_for_agent
|
||||||
|
|
||||||
return wrap_openai_for_agent(
|
return wrap_openai_for_agent(
|
||||||
OpenAIClient(provider_to_openai_config(provider)),
|
OpenAIClient(provider_to_openai_config(effective)),
|
||||||
provider_tools=provider.provider_tools or None,
|
provider_tools=effective.provider_tools or None,
|
||||||
)
|
)
|
||||||
if isinstance(provider, OpenAICompatibleProvider):
|
if effective.preset == "openai-compatible":
|
||||||
return OpenAICompatibleClient(provider_to_openai_config(provider))
|
return OpenAICompatibleClient(provider_to_openai_config(effective))
|
||||||
if isinstance(provider, DeepseekProvider):
|
if effective.preset == "deepseek":
|
||||||
convention = _deepseek_convention(provider.extras)
|
return DeepseekOpenAIClient(provider_to_openai_config(effective))
|
||||||
if convention in {"openai", "openai_compat", "openai-compatible"}:
|
if effective.preset == "anthropic":
|
||||||
return DeepseekOpenAIClient(provider_to_openai_config(provider))
|
msg = "anthropic preset is configured but native Anthropic client is not implemented yet"
|
||||||
if convention == "anthropic":
|
|
||||||
msg = "deepseek anthropic convention is not implemented"
|
|
||||||
raise ProviderNotSupportedError(msg)
|
|
||||||
msg = f"unknown deepseek convention {convention!r}"
|
|
||||||
raise ProviderNotSupportedError(msg)
|
raise ProviderNotSupportedError(msg)
|
||||||
# Remaining Provider variant: AnthropicProvider
|
msg = f"provider preset {effective.preset!r} is not supported"
|
||||||
msg = "anthropic provider client is not implemented"
|
|
||||||
raise ProviderNotSupportedError(msg)
|
raise ProviderNotSupportedError(msg)
|
||||||
|
|||||||
Reference in New Issue
Block a user