feat(providers): support provider-scoped proxy config
This commit is contained in:
+10
-1
@@ -1812,14 +1812,23 @@ def _login_openai_codex() -> None:
|
||||
try:
|
||||
from oauth_cli_kit import get_token, login_oauth_interactive
|
||||
|
||||
from nanobot.config.loader import load_config, resolve_config_env_vars
|
||||
|
||||
proxy = None
|
||||
try:
|
||||
proxy = resolve_config_env_vars(load_config()).providers.openai_codex.proxy or None
|
||||
except ValueError as e:
|
||||
console.print(f"[red]{e}[/red]")
|
||||
raise typer.Exit(1) from e
|
||||
token = None
|
||||
with suppress(Exception):
|
||||
token = get_token()
|
||||
token = get_token(proxy=proxy)
|
||||
if not (token and token.access):
|
||||
console.print("[cyan]Starting interactive OAuth login...[/cyan]\n")
|
||||
token = login_oauth_interactive(
|
||||
print_fn=lambda s: console.print(s),
|
||||
prompt_fn=lambda s: typer.prompt(s),
|
||||
proxy=proxy,
|
||||
)
|
||||
if not (token and token.access):
|
||||
console.print("[red]✗ Authentication failed[/red]")
|
||||
|
||||
@@ -80,6 +80,10 @@ def save_config(config: Config, config_path: Path | None = None) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
data = config.model_dump(mode="json", by_alias=True)
|
||||
if config.providers.openai_codex.proxy is not None:
|
||||
data.setdefault("providers", {})["openaiCodex"] = {
|
||||
"proxy": config.providers.openai_codex.proxy,
|
||||
}
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
@@ -179,6 +179,7 @@ class ProviderConfig(Base):
|
||||
extra_headers: dict[str, str] | None = None # Custom headers (e.g. APP-Code for AiHubMix)
|
||||
extra_body: dict[str, Any] | None = None # Extra provider request fields; shape depends on provider/API surface
|
||||
extra_query: dict[str, str] | None = None # Extra query params (e.g. api-version for Azure-style gateways)
|
||||
proxy: str | None = None # OpenAI-compatible/Codex HTTP proxy URL
|
||||
thinking_style: str | None = None # Thinking/reasoning style for custom providers
|
||||
|
||||
# Valid values mirror the keys of _THINKING_STYLE_MAP in
|
||||
|
||||
@@ -58,6 +58,11 @@ def _make_provider_core(
|
||||
if spec and spec.is_transcription_only:
|
||||
raise ValueError(f"Provider '{provider_name}' only supports transcription.")
|
||||
backend = spec.backend if spec else "openai_compat"
|
||||
if p and p.proxy and backend not in {"openai_compat", "openai_codex"}:
|
||||
raise ValueError(
|
||||
f"providers.{provider_name}.proxy is only supported for "
|
||||
"OpenAI-compatible providers and OpenAI Codex."
|
||||
)
|
||||
|
||||
if backend == "azure_openai":
|
||||
if not p or not p.api_base:
|
||||
@@ -79,7 +84,10 @@ def _make_provider_core(
|
||||
if backend == "openai_codex":
|
||||
from nanobot.providers.openai_codex_provider import OpenAICodexProvider
|
||||
|
||||
provider = OpenAICodexProvider(default_model=model)
|
||||
provider = OpenAICodexProvider(
|
||||
default_model=model,
|
||||
proxy=getattr(p, "proxy", None) if p else None,
|
||||
)
|
||||
elif backend == "azure_openai":
|
||||
from nanobot.providers.azure_openai_provider import AzureOpenAIProvider
|
||||
|
||||
@@ -124,6 +132,7 @@ def _make_provider_core(
|
||||
extra_body=p.extra_body if p else None,
|
||||
api_type=p.api_type if p and provider_name == "openai" else "auto",
|
||||
extra_query=p.extra_query if p else None,
|
||||
proxy=p.proxy if p else None,
|
||||
)
|
||||
|
||||
provider.generation = resolved.to_generation_settings()
|
||||
@@ -218,6 +227,7 @@ def provider_signature(
|
||||
fallback.temperature,
|
||||
fallback.reasoning_effort,
|
||||
fallback.context_window_tokens,
|
||||
getattr(fp, "proxy", None) if fp else None,
|
||||
)
|
||||
|
||||
provider_name = config.get_provider_name(resolved.model, preset=resolved)
|
||||
@@ -237,6 +247,7 @@ def provider_signature(
|
||||
resolved.temperature,
|
||||
resolved.reasoning_effort,
|
||||
resolved.context_window_tokens,
|
||||
getattr(p, "proxy", None) if p else None,
|
||||
tuple(_fallback_signature(fallback) for fallback in fallback_presets),
|
||||
)
|
||||
|
||||
|
||||
@@ -33,9 +33,14 @@ class OpenAICodexProvider(LLMProvider):
|
||||
|
||||
supports_progress_deltas = True
|
||||
|
||||
def __init__(self, default_model: str = "openai-codex/gpt-5.1-codex"):
|
||||
def __init__(
|
||||
self,
|
||||
default_model: str = "openai-codex/gpt-5.1-codex",
|
||||
proxy: str | None = None,
|
||||
):
|
||||
super().__init__(api_key=None, api_base=None)
|
||||
self.default_model = default_model
|
||||
self.proxy = proxy or None
|
||||
|
||||
async def _call_codex(
|
||||
self,
|
||||
@@ -52,9 +57,6 @@ class OpenAICodexProvider(LLMProvider):
|
||||
model = model or self.default_model
|
||||
system_prompt, input_items = convert_messages(messages)
|
||||
|
||||
token = await asyncio.to_thread(get_codex_token)
|
||||
headers = _build_headers(token.account_id, token.access)
|
||||
|
||||
body: dict[str, Any] = {
|
||||
"model": _strip_model_prefix(model),
|
||||
"store": False,
|
||||
@@ -74,9 +76,13 @@ class OpenAICodexProvider(LLMProvider):
|
||||
body["tools"] = convert_tools(tools)
|
||||
|
||||
try:
|
||||
token = await asyncio.to_thread(get_codex_token, proxy=self.proxy)
|
||||
headers = _build_headers(token.account_id, token.access)
|
||||
|
||||
try:
|
||||
content, tool_calls, finish_reason, usage, reasoning_content = await _request_codex(
|
||||
DEFAULT_CODEX_URL, headers, body, verify=True,
|
||||
proxy=self.proxy,
|
||||
on_content_delta=on_content_delta,
|
||||
on_thinking_delta=on_thinking_delta,
|
||||
on_tool_call_delta=on_tool_call_delta,
|
||||
@@ -87,6 +93,7 @@ class OpenAICodexProvider(LLMProvider):
|
||||
logger.warning("SSL verification failed for Codex API; retrying with verify=False")
|
||||
content, tool_calls, finish_reason, usage, reasoning_content = await _request_codex(
|
||||
DEFAULT_CODEX_URL, headers, body, verify=False,
|
||||
proxy=self.proxy,
|
||||
on_content_delta=on_content_delta,
|
||||
on_thinking_delta=on_thinking_delta,
|
||||
on_tool_call_delta=on_tool_call_delta,
|
||||
@@ -199,12 +206,17 @@ async def _request_codex(
|
||||
headers: dict[str, str],
|
||||
body: dict[str, Any],
|
||||
verify: bool,
|
||||
proxy: str | None = None,
|
||||
on_content_delta: Callable[[str], Awaitable[None]] | None = None,
|
||||
on_thinking_delta: Callable[[str], Awaitable[None]] | None = None,
|
||||
on_tool_call_delta: Callable[[dict[str, Any]], Awaitable[None]] | None = None,
|
||||
) -> tuple[str, list[ToolCallRequest], str, dict[str, int], str | None]:
|
||||
idle_timeout_s = resolve_stream_idle_timeout_s()
|
||||
async with httpx.AsyncClient(timeout=idle_timeout_s, verify=verify) as client:
|
||||
client_kwargs: dict[str, Any] = {"timeout": idle_timeout_s, "verify": verify}
|
||||
if proxy:
|
||||
client_kwargs["proxy"] = proxy
|
||||
client_kwargs["trust_env"] = False
|
||||
async with httpx.AsyncClient(**client_kwargs) as client:
|
||||
async with client.stream("POST", url, headers=headers, json=body) as response:
|
||||
if response.status_code != 200:
|
||||
text = await response.aread()
|
||||
|
||||
@@ -358,6 +358,7 @@ class OpenAICompatProvider(LLMProvider):
|
||||
extra_body: dict[str, Any] | None = None,
|
||||
api_type: str = "auto",
|
||||
extra_query: dict[str, str] | None = None,
|
||||
proxy: str | None = None,
|
||||
):
|
||||
super().__init__(api_key, api_base)
|
||||
self.default_model = default_model
|
||||
@@ -366,6 +367,7 @@ class OpenAICompatProvider(LLMProvider):
|
||||
self._extra_body = extra_body or {}
|
||||
self._api_type = api_type if spec and spec.name == "openai" else "auto"
|
||||
self._extra_query = extra_query or {}
|
||||
self._proxy = proxy or None
|
||||
|
||||
if api_key and spec and spec.env_key:
|
||||
self._setup_env(api_key, api_base)
|
||||
@@ -396,7 +398,14 @@ class OpenAICompatProvider(LLMProvider):
|
||||
|
||||
timeout_s = _openai_compat_timeout_s()
|
||||
http_client: httpx.AsyncClient | None = None
|
||||
if self._is_local:
|
||||
if self._proxy:
|
||||
http_client = httpx.AsyncClient(
|
||||
timeout=timeout_s,
|
||||
proxy=self._proxy,
|
||||
trust_env=False,
|
||||
follow_redirects=True,
|
||||
)
|
||||
elif self._is_local:
|
||||
# Local model servers (Ollama, llama.cpp, vLLM) often close idle
|
||||
# HTTP connections before the client-side keepalive expires. When
|
||||
# two LLM calls happen seconds apart (e.g. heartbeat _decide then
|
||||
|
||||
@@ -22,7 +22,7 @@ from nanobot.audio.transcription_registry import (
|
||||
resolve_transcription_provider,
|
||||
transcription_provider_names,
|
||||
)
|
||||
from nanobot.config.loader import get_config_path, load_config, save_config
|
||||
from nanobot.config.loader import get_config_path, load_config, resolve_config_env_vars, save_config
|
||||
from nanobot.config.schema import ModelPresetConfig, ProviderConfig
|
||||
from nanobot.providers.image_generation import (
|
||||
get_image_gen_provider,
|
||||
@@ -1166,14 +1166,19 @@ def login_oauth_provider(query: QueryParams) -> dict[str, Any]:
|
||||
except ImportError:
|
||||
raise WebUISettingsError("oauth_cli_kit is not installed", status=500) from None
|
||||
|
||||
try:
|
||||
proxy = resolve_config_env_vars(load_config()).providers.openai_codex.proxy or None
|
||||
except ValueError as e:
|
||||
raise WebUISettingsError(str(e), status=400) from e
|
||||
token = None
|
||||
with suppress(Exception):
|
||||
token = get_token()
|
||||
token = get_token(proxy=proxy)
|
||||
if not (token and token.access):
|
||||
messages: list[str] = []
|
||||
token = login_oauth_interactive(
|
||||
print_fn=lambda message: messages.append(str(message)),
|
||||
prompt_fn=lambda _prompt: "",
|
||||
proxy=proxy,
|
||||
)
|
||||
if not (token and token.access):
|
||||
raise WebUISettingsError("OAuth login failed", status=401)
|
||||
|
||||
Reference in New Issue
Block a user