mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
core/runtime: configurable provider HTTP request timeouts
This commit is contained in:
@@ -9,6 +9,7 @@ from .models import AgentConfig as AgentConfig
|
||||
from .models import AnthropicProvider as AnthropicProvider
|
||||
from .models import DatabaseConfig as DatabaseConfig
|
||||
from .models import DeepseekProvider as DeepseekProvider
|
||||
from .models import HttpTimeoutConfig as HttpTimeoutConfig
|
||||
from .models import ModelConfig as ModelConfig
|
||||
from .models import OpenAICompatibleProvider as OpenAICompatibleProvider
|
||||
from .models import OpenAIProvider as OpenAIProvider
|
||||
|
||||
@@ -50,6 +50,23 @@ class ModelConfig(Struct, omit_defaults=True):
|
||||
cost_factor: float = 1.0
|
||||
|
||||
|
||||
class HttpTimeoutConfig(Struct, omit_defaults=True):
|
||||
"""Per-provider HTTP timeouts (seconds) for the LLM API client.
|
||||
|
||||
TOML examples::
|
||||
|
||||
timeout = 120
|
||||
timeout = { connect = 10, read = 600 }
|
||||
|
||||
``connect`` bounds TCP/TLS setup; ``read`` bounds idle wait between response
|
||||
bytes (SSE streams can run longer than *read* as long as chunks keep arriving).
|
||||
Omitted fields use product defaults (10s connect / 600s read).
|
||||
"""
|
||||
|
||||
connect: float | None = None
|
||||
read: float | None = None
|
||||
|
||||
|
||||
class ProviderConfig(Struct, tag_field="preset", omit_defaults=True):
|
||||
"""Base config for all LLM providers.
|
||||
|
||||
@@ -60,6 +77,8 @@ class ProviderConfig(Struct, tag_field="preset", omit_defaults=True):
|
||||
access_key_or_token: str
|
||||
url: str = ""
|
||||
models: dict[str, ModelConfig] = field(default_factory=dict)
|
||||
# float = single timeout for the session; table = connect/read split; omit = defaults.
|
||||
timeout: float | HttpTimeoutConfig | None = None
|
||||
|
||||
|
||||
def _default_openai_models() -> dict[str, ModelConfig]:
|
||||
|
||||
@@ -113,6 +113,7 @@ class BaseOpenAIClient:
|
||||
self.session = niquests.AsyncSession(
|
||||
base_url=config.base_url,
|
||||
auth=BearerTokenAuth(config.access_key_or_token),
|
||||
timeout=config.timeout,
|
||||
)
|
||||
self.encoder = msgspec.json.Encoder()
|
||||
self.decoder = msgspec.json.Decoder(ChatCompletionResponse)
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Product defaults when provider omits ``timeout`` (connect vs read idle).
|
||||
DEFAULT_HTTP_CONNECT_TIMEOUT = 10.0
|
||||
DEFAULT_HTTP_READ_TIMEOUT = 600.0
|
||||
|
||||
type HttpTimeout = float | tuple[float, float]
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpenAIConfig:
|
||||
access_key_or_token: str
|
||||
base_url: str = "https://api.openai.com/v1"
|
||||
# Passed to ``niquests.AsyncSession(timeout=...)``.
|
||||
# ``float`` = single timeout; ``(connect, read)`` = split; factory always sets a concrete value.
|
||||
timeout: HttpTimeout = (DEFAULT_HTTP_CONNECT_TIMEOUT, DEFAULT_HTTP_READ_TIMEOUT)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from .client_factory import InvalidHttpTimeoutError as InvalidHttpTimeoutError
|
||||
from .client_factory import ProviderNotSupportedError as ProviderNotSupportedError
|
||||
from .client_factory import create_client as create_client
|
||||
from .client_factory import normalize_http_timeout as normalize_http_timeout
|
||||
from .client_factory import provider_to_openai_config as provider_to_openai_config
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from plyngent.config.models import (
|
||||
DeepseekProvider,
|
||||
HttpTimeoutConfig,
|
||||
OpenAICompatibleProvider,
|
||||
OpenAIProvider,
|
||||
Provider,
|
||||
@@ -11,6 +13,11 @@ from plyngent.config.models import (
|
||||
from plyngent.lmproto.deepseek import DeepseekOpenAIClient
|
||||
from plyngent.lmproto.openai import OpenAIClient
|
||||
from plyngent.lmproto.openai_compatible import OpenAICompatibleClient, OpenAIConfig
|
||||
from plyngent.lmproto.openai_compatible.config import (
|
||||
DEFAULT_HTTP_CONNECT_TIMEOUT,
|
||||
DEFAULT_HTTP_READ_TIMEOUT,
|
||||
HttpTimeout,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
@@ -29,6 +36,44 @@ class ProviderNotSupportedError(NotImplementedError):
|
||||
"""Raised when a provider preset cannot be turned into a runtime client."""
|
||||
|
||||
|
||||
class InvalidHttpTimeoutError(ValueError):
|
||||
"""Raised when a provider ``timeout`` value is not usable for HTTP clients."""
|
||||
|
||||
|
||||
def normalize_http_timeout(timeout: float | HttpTimeoutConfig | None) -> HttpTimeout:
|
||||
"""Normalize TOML/provider timeout into a niquests session timeout.
|
||||
|
||||
* ``None`` → product defaults ``(connect=10, read=600)``
|
||||
* ``float`` / ``int`` → single timeout (niquests applies to the request)
|
||||
* :class:`HttpTimeoutConfig` → ``(connect, read)`` with defaults for omitted fields
|
||||
|
||||
All finite values must be ``> 0``.
|
||||
"""
|
||||
if timeout is None:
|
||||
return (DEFAULT_HTTP_CONNECT_TIMEOUT, DEFAULT_HTTP_READ_TIMEOUT)
|
||||
if isinstance(timeout, bool):
|
||||
# ``bool`` is an ``int`` subclass; reject before the numeric branch.
|
||||
msg = "timeout must be a positive number or { connect, read }, not a boolean"
|
||||
raise InvalidHttpTimeoutError(msg)
|
||||
if isinstance(timeout, int | float):
|
||||
value = float(timeout)
|
||||
if not math.isfinite(value) or value <= 0:
|
||||
msg = f"timeout must be a finite number > 0, got {timeout!r}"
|
||||
raise InvalidHttpTimeoutError(msg)
|
||||
return value
|
||||
|
||||
# Remaining union member: HttpTimeoutConfig
|
||||
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)
|
||||
if not math.isfinite(connect) or connect <= 0:
|
||||
msg = f"timeout.connect must be a finite number > 0, got {timeout.connect!r}"
|
||||
raise InvalidHttpTimeoutError(msg)
|
||||
if not math.isfinite(read) or read <= 0:
|
||||
msg = f"timeout.read must be a finite number > 0, got {timeout.read!r}"
|
||||
raise InvalidHttpTimeoutError(msg)
|
||||
return (connect, read)
|
||||
|
||||
|
||||
def provider_to_openai_config(provider: OpenAIProvider | OpenAICompatibleProvider | DeepseekProvider) -> OpenAIConfig:
|
||||
"""Map a provider config entry to :class:`OpenAIConfig`."""
|
||||
if isinstance(provider, OpenAIProvider):
|
||||
@@ -40,9 +85,14 @@ def provider_to_openai_config(provider: OpenAIProvider | OpenAICompatibleProvide
|
||||
if not base_url:
|
||||
msg = "openai-compatible provider requires a non-empty url"
|
||||
raise ProviderNotSupportedError(msg)
|
||||
try:
|
||||
http_timeout = normalize_http_timeout(provider.timeout)
|
||||
except InvalidHttpTimeoutError as exc:
|
||||
raise ProviderNotSupportedError(str(exc)) from exc
|
||||
return OpenAIConfig(
|
||||
access_key_or_token=provider.access_key_or_token,
|
||||
base_url=base_url,
|
||||
timeout=http_timeout,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user