refactor(config): retire RuntimeSettingsCompat host usage

This commit is contained in:
jxxghp
2026-08-26 15:55:21 +08:00
parent cdab54254d
commit 9dbe424c3d
162 changed files with 1966 additions and 1745 deletions
+27 -28
View File
@@ -13,9 +13,8 @@ from typing import Any, Dict, Optional
from uuid import uuid4
from app.application.notification import get_notification_configs
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.adapters.network.http import RequestUtils
@@ -61,11 +60,11 @@ class OpenAIAudioProvider(AudioCapabilityProvider):
@staticmethod
def _input_credentials() -> tuple[Optional[str], Optional[str]]:
return settings.AUDIO_INPUT_API_KEY, settings.AUDIO_INPUT_BASE_URL
return get_runtime_setting('AUDIO_INPUT_API_KEY'), get_runtime_setting('AUDIO_INPUT_BASE_URL')
@staticmethod
def _output_credentials() -> tuple[Optional[str], Optional[str]]:
return settings.AUDIO_OUTPUT_API_KEY, settings.AUDIO_OUTPUT_BASE_URL
return get_runtime_setting('AUDIO_OUTPUT_API_KEY'), get_runtime_setting('AUDIO_OUTPUT_BASE_URL')
def is_available_for_audio_input(self) -> bool:
api_key, _ = self._input_credentials()
@@ -89,9 +88,9 @@ class OpenAIAudioProvider(AudioCapabilityProvider):
audio_file = BytesIO(content)
audio_file.name = filename
response = client.audio.transcriptions.create(
model=settings.AUDIO_INPUT_MODEL,
model=get_runtime_setting('AUDIO_INPUT_MODEL'),
file=audio_file,
language=settings.AUDIO_INPUT_LANGUAGE or "zh",
language=get_runtime_setting('AUDIO_INPUT_LANGUAGE') or "zh",
response_format="verbose_json",
)
text = getattr(response, "text", None)
@@ -109,12 +108,12 @@ class OpenAIAudioProvider(AudioCapabilityProvider):
if not api_key:
raise ValueError("音频输出 provider 未配置 API Key")
client = self._build_client(api_key=api_key, base_url=base_url)
voice_dir = settings.TEMP_PATH / "voice"
voice_dir = get_runtime_setting('TEMP_PATH') / "voice"
voice_dir.mkdir(parents=True, exist_ok=True)
output_path = voice_dir / f"{uuid4().hex}.opus"
response = client.audio.speech.create(
model=settings.AUDIO_OUTPUT_MODEL,
voice=settings.AUDIO_OUTPUT_VOICE,
model=get_runtime_setting('AUDIO_OUTPUT_MODEL'),
voice=get_runtime_setting('AUDIO_OUTPUT_VOICE'),
input=text,
response_format="opus",
)
@@ -163,22 +162,22 @@ class OpenAIChatAudioProvider(AudioCapabilityProvider):
@staticmethod
def _input_credentials() -> tuple[Optional[str], Optional[str]]:
return settings.AUDIO_INPUT_API_KEY, settings.AUDIO_INPUT_BASE_URL
return get_runtime_setting('AUDIO_INPUT_API_KEY'), get_runtime_setting('AUDIO_INPUT_BASE_URL')
@staticmethod
def _output_credentials() -> tuple[Optional[str], Optional[str]]:
return settings.AUDIO_OUTPUT_API_KEY, settings.AUDIO_OUTPUT_BASE_URL
return get_runtime_setting('AUDIO_OUTPUT_API_KEY'), get_runtime_setting('AUDIO_OUTPUT_BASE_URL')
def _normalize_stt_model(self) -> str:
return self._normalize_model(
model=settings.AUDIO_INPUT_MODEL,
model=get_runtime_setting('AUDIO_INPUT_MODEL'),
supported_models=self.SUPPORTED_STT_MODELS,
default_model=self.DEFAULT_STT_MODEL,
)
def _normalize_tts_model(self) -> str:
return self._normalize_model(
model=settings.AUDIO_OUTPUT_MODEL,
model=get_runtime_setting('AUDIO_OUTPUT_MODEL'),
supported_models=self.SUPPORTED_TTS_MODELS,
default_model=self.DEFAULT_TTS_MODEL,
)
@@ -268,7 +267,7 @@ class OpenAIChatAudioProvider(AudioCapabilityProvider):
return None
suffix = Path(filename or "").suffix.lower() or ".audio"
voice_dir = settings.TEMP_PATH / "voice"
voice_dir = get_runtime_setting('TEMP_PATH') / "voice"
voice_dir.mkdir(parents=True, exist_ok=True)
input_path = voice_dir / f"{uuid4().hex}{suffix}"
output_path = input_path.with_suffix(self.TRANSCODED_STT_SUFFIX)
@@ -391,7 +390,7 @@ class OpenAIChatAudioProvider(AudioCapabilityProvider):
if not normalized_audio:
return None
content, filename = normalized_audio
language = (settings.AUDIO_INPUT_LANGUAGE or "").strip()
language = (get_runtime_setting('AUDIO_INPUT_LANGUAGE') or "").strip()
prompt = "请将这段音频完整转写为文字,只输出转写结果,不要添加解释。"
if language:
prompt += f"音频主要语言是 {language}"
@@ -426,7 +425,7 @@ class OpenAIChatAudioProvider(AudioCapabilityProvider):
logger.error(
"%s TTS 当前不支持该模型或模型未配置: %s",
self.DISPLAY_NAME,
settings.AUDIO_OUTPUT_MODEL,
get_runtime_setting('AUDIO_OUTPUT_MODEL'),
)
return None
@@ -435,7 +434,7 @@ class OpenAIChatAudioProvider(AudioCapabilityProvider):
if not api_key:
raise ValueError("音频输出 provider 未配置 API Key")
client = self._build_client(api_key=api_key, base_url=base_url)
voice_dir = settings.TEMP_PATH / "voice"
voice_dir = get_runtime_setting('TEMP_PATH') / "voice"
voice_dir.mkdir(parents=True, exist_ok=True)
wav_path = voice_dir / f"{uuid4().hex}.wav"
request = {
@@ -448,7 +447,7 @@ class OpenAIChatAudioProvider(AudioCapabilityProvider):
],
"audio": {
"format": self.AUDIO_RESPONSE_FORMAT,
"voice": settings.AUDIO_OUTPUT_VOICE or self.DEFAULT_VOICE,
"voice": get_runtime_setting('AUDIO_OUTPUT_VOICE') or self.DEFAULT_VOICE,
},
}
if self.INCLUDE_AUDIO_MODALITIES:
@@ -487,7 +486,7 @@ class MiMoAudioProvider(OpenAIChatAudioProvider):
)
def _normalize_tts_model(self) -> str:
model = (settings.AUDIO_OUTPUT_MODEL or "").strip().lower()
model = (get_runtime_setting('AUDIO_OUTPUT_MODEL') or "").strip().lower()
if not model or not model.startswith("mimo-"):
return self.DEFAULT_TTS_MODEL
return model
@@ -546,21 +545,21 @@ class MiniMaxAudioProvider(OpenAIChatAudioProvider):
def _normalize_stt_model(self) -> str:
"""将非 MiniMax 的默认转写模型名兜底为 MiniMax 对话模型。"""
model = (settings.AUDIO_INPUT_MODEL or "").strip()
model = (get_runtime_setting('AUDIO_INPUT_MODEL') or "").strip()
if not model or model.lower().startswith(("gpt-", "mimo-")):
return self.DEFAULT_STT_MODEL
return model
def _normalize_tts_model(self) -> str:
"""将非 MiniMax 语音模型兜底为官方 T2A 模型。"""
model = (settings.AUDIO_OUTPUT_MODEL or "").strip().lower()
model = (get_runtime_setting('AUDIO_OUTPUT_MODEL') or "").strip().lower()
if model in self.SUPPORTED_TTS_MODELS:
return model
return self.DEFAULT_TTS_MODEL
def _normalize_voice_id(self) -> str:
"""将其他 provider 的默认音色兜底为 MiniMax 中文系统音色。"""
voice_id = (settings.AUDIO_OUTPUT_VOICE or "").strip()
voice_id = (get_runtime_setting('AUDIO_OUTPUT_VOICE') or "").strip()
if not voice_id or voice_id in {"alloy", "mimo_default"}:
return self.DEFAULT_VOICE
return voice_id
@@ -599,7 +598,7 @@ class MiniMaxAudioProvider(OpenAIChatAudioProvider):
"Content-Type": "application/json",
"Accept": "application/json",
},
proxies=settings.PROXY or {},
proxies=get_runtime_setting('PROXY') or {},
timeout=60,
).post_res(
url=self._build_t2a_url(base_url),
@@ -637,7 +636,7 @@ class MiniMaxAudioProvider(OpenAIChatAudioProvider):
if not audio_data:
raise ValueError("MiniMax T2A 响应中没有音频数据")
voice_dir = settings.TEMP_PATH / "voice"
voice_dir = get_runtime_setting('TEMP_PATH') / "voice"
voice_dir.mkdir(parents=True, exist_ok=True)
output_path = voice_dir / f"{uuid4().hex}.opus"
output_path.write_bytes(self._decode_audio_payload(audio_data))
@@ -681,9 +680,9 @@ class AgentCapabilityManager:
@classmethod
def get_audio_provider(cls, mode: str) -> Optional[AudioCapabilityProvider]:
provider_name = cls._normalize_provider_name(
settings.AUDIO_INPUT_PROVIDER
get_runtime_setting('AUDIO_INPUT_PROVIDER')
if (mode or "").lower() == "input"
else settings.AUDIO_OUTPUT_PROVIDER
else get_runtime_setting('AUDIO_OUTPUT_PROVIDER')
)
provider = cls._audio_providers.get(provider_name)
if provider:
@@ -701,12 +700,12 @@ class AgentCapabilityManager:
@staticmethod
def supports_audio_input() -> bool:
"""当前 Agent 是否启用音频输入能力。"""
return bool(settings.LLM_SUPPORT_AUDIO_INPUT)
return bool(get_runtime_setting('LLM_SUPPORT_AUDIO_INPUT'))
@staticmethod
def supports_audio_output() -> bool:
"""当前 Agent 是否启用音频输出能力。"""
return bool(settings.LLM_SUPPORT_AUDIO_OUTPUT)
return bool(get_runtime_setting('LLM_SUPPORT_AUDIO_OUTPUT'))
@classmethod
def is_audio_input_available(cls) -> bool:
+24 -26
View File
@@ -11,10 +11,8 @@ from urllib.parse import urlsplit
from langchain_core.messages import AIMessage, AIMessageChunk
from app.agent.llm.gateway import resolve_llm_provider_runtime
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
if TYPE_CHECKING:
from app.agent.llm.server_tools import ServerToolResolution
@@ -148,8 +146,8 @@ def _resolve_llm_proxy(use_proxy: bool | None = None) -> str | None:
"""
解析本次 LLM 调用应使用的系统代理地址。
"""
should_use_proxy = settings.LLM_USE_PROXY if use_proxy is None else use_proxy
return settings.PROXY_HOST if should_use_proxy and settings.PROXY_HOST else None
should_use_proxy = get_runtime_setting('LLM_USE_PROXY') if use_proxy is None else use_proxy
return get_runtime_setting('PROXY_HOST') if should_use_proxy and get_runtime_setting('PROXY_HOST') else None
def _build_httpx_proxy_kwargs(proxy_url: str | None) -> dict[str, str]:
@@ -539,7 +537,7 @@ class LLMHelper:
record_input = cls._source_input_limit(model_record)
metadata_input = cls._source_input_limit(metadata_source)
profile_input = cls._positive_token_limit(profile.get("max_input_tokens"))
configured_k = cls._positive_token_limit(settings.LLM_MAX_CONTEXT_TOKENS)
configured_k = cls._positive_token_limit(get_runtime_setting('LLM_MAX_CONTEXT_TOKENS'))
configured_input = configured_k * 1000 if configured_k else None
endpoint_matched = runtime.get("model_profile_endpoint_matched") is True
@@ -790,8 +788,8 @@ class LLMHelper:
base_url_preset: Optional[str] = None,
) -> Optional[bool]:
"""复用 provider 目录缓存解析当前模型是否支持图片输入。"""
provider_name = str(provider if provider is not None else settings.LLM_PROVIDER).strip()
model_name = str(model if model is not None else settings.LLM_MODEL).strip()
provider_name = str(provider if provider is not None else get_runtime_setting('LLM_PROVIDER')).strip()
model_name = str(model if model is not None else get_runtime_setting('LLM_MODEL')).strip()
if not provider_name or not model_name:
return None
@@ -799,11 +797,11 @@ class LLMHelper:
metadata = resolve_llm_provider_runtime().resolve_cached_model_metadata(
provider_id=provider_name,
model_id=model_name,
base_url=base_url if base_url is not None else settings.LLM_BASE_URL,
base_url=base_url if base_url is not None else get_runtime_setting('LLM_BASE_URL'),
base_url_preset_id=(
base_url_preset
if base_url_preset is not None
else settings.LLM_BASE_URL_PRESET
else get_runtime_setting('LLM_BASE_URL_PRESET')
),
)
except Exception as err:
@@ -828,7 +826,7 @@ class LLMHelper:
被兼容端点以 400 拒绝。无参调用保持旧版“只读总开关”语义,
未知自定义模型也保持原有开关语义。
"""
if not settings.LLM_SUPPORT_IMAGE_INPUT:
if not get_runtime_setting('LLM_SUPPORT_IMAGE_INPUT'):
return False
if provider is None and model is None:
return True
@@ -857,8 +855,8 @@ class LLMHelper:
这主要用于单测 stub 环境以及极端的最小运行环境,正常生产路径仍优先
走 `LLMProviderManager.resolve_runtime()`。
"""
api_key_value = api_key if api_key is not None else settings.LLM_API_KEY
base_url_value = base_url if base_url is not None else settings.LLM_BASE_URL
api_key_value = api_key if api_key is not None else get_runtime_setting('LLM_API_KEY')
base_url_value = base_url if base_url is not None else get_runtime_setting('LLM_BASE_URL')
if not api_key_value:
raise ValueError("未配置LLM API Key")
@@ -1037,7 +1035,7 @@ class LLMHelper:
"""
规范化 API 协议配置,未知值统一回退为 ``auto`` 以保持兼容。
"""
normalized = str(api_protocol or settings.LLM_API_PROTOCOL or "").strip().lower()
normalized = str(api_protocol or get_runtime_setting('LLM_API_PROTOCOL') or "").strip().lower()
if normalized in {"auto", "chat_completions", "responses"}:
return normalized
if normalized:
@@ -1185,15 +1183,15 @@ class LLMHelper:
:param prompt_cache_key: 同一 Agent 会话内稳定且脱敏的提示词缓存路由键。
:return: LLM实例
"""
provider_name = str(provider if provider is not None else settings.LLM_PROVIDER).lower()
model_name = model if model is not None else settings.LLM_MODEL
api_key_value = api_key if api_key is not None else settings.LLM_API_KEY
base_url_value = base_url if base_url is not None else settings.LLM_BASE_URL
provider_name = str(provider if provider is not None else get_runtime_setting('LLM_PROVIDER')).lower()
model_name = model if model is not None else get_runtime_setting('LLM_MODEL')
api_key_value = api_key if api_key is not None else get_runtime_setting('LLM_API_KEY')
base_url_value = base_url if base_url is not None else get_runtime_setting('LLM_BASE_URL')
base_url_preset_value = (
base_url_preset if base_url_preset is not None else settings.LLM_BASE_URL_PRESET
base_url_preset if base_url_preset is not None else get_runtime_setting('LLM_BASE_URL_PRESET')
)
user_agent_value = user_agent if user_agent is not None else settings.LLM_USER_AGENT
temperature_value = temperature if temperature is not None else settings.LLM_TEMPERATURE
user_agent_value = user_agent if user_agent is not None else get_runtime_setting('LLM_USER_AGENT')
temperature_value = temperature if temperature is not None else get_runtime_setting('LLM_TEMPERATURE')
normalized_thinking_level = cls._resolve_thinking_level(
thinking_level=thinking_level,
)
@@ -1228,12 +1226,12 @@ class LLMHelper:
mode=(
web_search_mode
if web_search_mode is not None
else getattr(settings, "LLM_WEB_SEARCH_MODE", "local")
else get_runtime_setting("LLM_WEB_SEARCH_MODE", "local")
),
api_protocol=(
api_protocol
if api_protocol is not None
else settings.LLM_API_PROTOCOL
else get_runtime_setting('LLM_API_PROTOCOL')
),
base_url=runtime.get("base_url"),
)
@@ -1345,7 +1343,7 @@ class LLMHelper:
credentials=aws_auth,
base_url=runtime.get("base_url"),
use_proxy=use_proxy,
read_timeout=settings.LLM_TOOL_TIMEOUT,
read_timeout=get_runtime_setting('LLM_TOOL_TIMEOUT'),
)
model = bedrock_model_cls(
model_id=model_name,
@@ -1492,8 +1490,8 @@ class LLMHelper:
:param api_protocol: OpenAI 兼容接口 API 协议,未显式传入时沿用已保存配置。
:param web_search_mode: 联网搜索模式,未显式传入时沿用已保存配置。
"""
provider_name = provider if provider is not None else settings.LLM_PROVIDER
model_name = model if model is not None else settings.LLM_MODEL
provider_name = provider if provider is not None else get_runtime_setting('LLM_PROVIDER')
model_name = model if model is not None else get_runtime_setting('LLM_MODEL')
start = time.perf_counter()
llm_kwargs = {
"streaming": False,
+21 -22
View File
@@ -20,9 +20,8 @@ import aiofiles
import httpx
import jwt
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.application.configuration import get_configured_system_config
from app.runtime.log import logger
from app.schemas.types import LlmProviderAction, SystemConfigKey
@@ -268,7 +267,7 @@ class LLMProviderManager(metaclass=Singleton):
self._models_dev_data: dict[str, Any] | None = None
self._models_dev_loaded_at: float = 0
self._models_dev_cache_path = (
Path(settings.TEMP_PATH) / "llm_provider_models_dev_cache.json"
Path(get_runtime_setting('TEMP_PATH')) / "llm_provider_models_dev_cache.json"
)
def _cleanup_auth_sessions_locked(self, now: Optional[float] = None) -> None:
@@ -1498,13 +1497,13 @@ class LLMProviderManager(metaclass=Singleton):
def _build_httpx_kwargs(self, use_proxy: Optional[bool] = None) -> dict[str, Any]:
"""构造用于 httpx 客户端的参数,如代理等。"""
should_use_proxy = settings.LLM_USE_PROXY if use_proxy is None else use_proxy
should_use_proxy = get_runtime_setting('LLM_USE_PROXY') if use_proxy is None else use_proxy
kwargs: dict[str, Any] = {
"timeout": self._DEFAULT_TIMEOUT,
"trust_env": False,
}
if should_use_proxy and settings.PROXY_HOST:
kwargs[self._httpx_proxy_key()] = settings.PROXY_HOST
if should_use_proxy and get_runtime_setting('PROXY_HOST'):
kwargs[self._httpx_proxy_key()] = get_runtime_setting('PROXY_HOST')
return kwargs
@staticmethod
@@ -1616,7 +1615,7 @@ class LLMProviderManager(metaclass=Singleton):
async def _fetch_models_dev(self, use_proxy: Optional[bool] = None) -> dict[str, Any]:
"""通过网络请求获取最新 models.dev 数据。"""
headers = {"User-Agent": settings.USER_AGENT}
headers = {"User-Agent": get_runtime_setting('USER_AGENT')}
async with httpx.AsyncClient(**self._build_httpx_kwargs(use_proxy)) as client:
response = await client.get(self._MODELS_DEV_URL, headers=headers)
response.raise_for_status()
@@ -2043,10 +2042,10 @@ class LLMProviderManager(metaclass=Singleton):
from google import genai
from google.genai.types import HttpOptions
should_use_proxy = settings.LLM_USE_PROXY if use_proxy is None else use_proxy
should_use_proxy = get_runtime_setting('LLM_USE_PROXY') if use_proxy is None else use_proxy
client_args: dict[str, Any] = {"trust_env": False}
if should_use_proxy and settings.PROXY_HOST:
client_args[self._httpx_proxy_key()] = settings.PROXY_HOST
if should_use_proxy and get_runtime_setting('PROXY_HOST'):
client_args[self._httpx_proxy_key()] = get_runtime_setting('PROXY_HOST')
http_options = HttpOptions(
client_args=client_args,
async_client_args=client_args,
@@ -2160,10 +2159,10 @@ class LLMProviderManager(metaclass=Singleton):
"""
from botocore.config import Config
should_use_proxy = settings.LLM_USE_PROXY if use_proxy is None else use_proxy
should_use_proxy = get_runtime_setting('LLM_USE_PROXY') if use_proxy is None else use_proxy
proxies = None
if should_use_proxy and settings.PROXY_HOST:
proxies = {"http": settings.PROXY_HOST, "https": settings.PROXY_HOST}
if should_use_proxy and get_runtime_setting('PROXY_HOST'):
proxies = {"http": get_runtime_setting('PROXY_HOST'), "https": get_runtime_setting('PROXY_HOST')}
return Config(
connect_timeout=10,
read_timeout=60,
@@ -2388,7 +2387,7 @@ class LLMProviderManager(metaclass=Singleton):
仅补充 Copilot 必需的意图头,避免重复覆盖。
"""
headers = {
"User-Agent": settings.USER_AGENT,
"User-Agent": get_runtime_setting('USER_AGENT'),
"Openai-Intent": "conversation-edits",
"x-initiator": "user",
}
@@ -2769,7 +2768,7 @@ class LLMProviderManager(metaclass=Singleton):
f"{self._CHATGPT_ISSUER}/api/accounts/deviceauth/usercode",
headers={
"Content-Type": "application/json",
"User-Agent": settings.USER_AGENT,
"User-Agent": get_runtime_setting('USER_AGENT'),
},
json={"client_id": self._CHATGPT_CLIENT_ID},
)
@@ -2806,7 +2805,7 @@ class LLMProviderManager(metaclass=Singleton):
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": settings.USER_AGENT,
"User-Agent": get_runtime_setting('USER_AGENT'),
},
json={
"client_id": self._COPILOT_CLIENT_ID,
@@ -3036,11 +3035,11 @@ class LLMProviderManager(metaclass=Singleton):
"""管理动作:使用传入配置或当前已保存配置执行一次最小 LLM 调用。"""
from app.agent.llm.helper import LLMHelper, LLMTestTimeout
provider_name = provider or settings.LLM_PROVIDER
model = params.get("model") if params.get("model") is not None else settings.LLM_MODEL
provider_name = provider or get_runtime_setting('LLM_PROVIDER')
model = params.get("model") if params.get("model") is not None else get_runtime_setting('LLM_MODEL')
enabled = params.get("enabled")
enabled = bool(enabled) if enabled is not None else bool(settings.AI_AGENT_ENABLE)
api_key = params.get("api_key") if params.get("api_key") is not None else settings.LLM_API_KEY
enabled = bool(enabled) if enabled is not None else bool(get_runtime_setting('AI_AGENT_ENABLE'))
api_key = params.get("api_key") if params.get("api_key") is not None else get_runtime_setting('LLM_API_KEY')
data = {"provider": provider_name, "model": model}
if not provider_name:
@@ -3151,7 +3150,7 @@ class LLMProviderManager(metaclass=Singleton):
f"{self._CHATGPT_ISSUER}/api/accounts/deviceauth/token",
headers={
"Content-Type": "application/json",
"User-Agent": settings.USER_AGENT,
"User-Agent": get_runtime_setting('USER_AGENT'),
},
json={
"device_auth_id": session.context["device_auth_id"],
@@ -3196,7 +3195,7 @@ class LLMProviderManager(metaclass=Singleton):
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": settings.USER_AGENT,
"User-Agent": get_runtime_setting('USER_AGENT'),
},
json={
"client_id": self._COPILOT_CLIENT_ID,