mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-15 15:24:06 +00:00
feat: support multiple custom OpenAI-compatible providers
This change allows users to define arbitrary custom providers in config:
providers:
my_provider:
api_base: ...
api_key: sk-xxx
Usage:
nanobot /my_provider/gpt-4 hello
nanobot --provider my_provider hello
Changes:
- ProvidersConfig: add extra=allow to accept arbitrary fields
- _match_provider: check for custom provider by prefix and by fallback
- registry: add create_dynamic_spec() for dynamic provider specs
This commit is contained in:
parent
2d9260cb9f
commit
e9e1489cee
@ -194,7 +194,13 @@ class BedrockProviderConfig(ProviderConfig):
|
|||||||
|
|
||||||
|
|
||||||
class ProvidersConfig(Base):
|
class ProvidersConfig(Base):
|
||||||
"""Configuration for LLM providers."""
|
"""Configuration for LLM providers.
|
||||||
|
|
||||||
|
Supports custom providers via extra fields — any additional field
|
||||||
|
becomes an OpenAI-compatible custom provider.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
custom: ProviderConfig = Field(default_factory=ProviderConfig) # Any OpenAI-compatible endpoint
|
custom: ProviderConfig = Field(default_factory=ProviderConfig) # Any OpenAI-compatible endpoint
|
||||||
azure_openai: ProviderConfig = Field(default_factory=ProviderConfig) # Azure OpenAI (model = deployment name)
|
azure_openai: ProviderConfig = Field(default_factory=ProviderConfig) # Azure OpenAI (model = deployment name)
|
||||||
@ -245,6 +251,15 @@ class ProvidersConfig(Base):
|
|||||||
raise ValueError("providers.<name>.api_type is only supported for providers.openai")
|
raise ValueError("providers.<name>.api_type is only supported for providers.openai")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def convert_extra_providers(self):
|
||||||
|
"""Convert extra fields (custom providers) to ProviderConfig objects."""
|
||||||
|
if self.model_extra:
|
||||||
|
for key, value in self.model_extra.items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
self.model_extra[key] = ProviderConfig.model_validate(value)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
class HeartbeatConfig(Base):
|
class HeartbeatConfig(Base):
|
||||||
"""Heartbeat service configuration (now backed by cron)."""
|
"""Heartbeat service configuration (now backed by cron)."""
|
||||||
@ -381,7 +396,10 @@ class Config(BaseSettings):
|
|||||||
preset: ModelPresetConfig | None = None,
|
preset: ModelPresetConfig | None = None,
|
||||||
) -> tuple["ProviderConfig | None", str | None]:
|
) -> tuple["ProviderConfig | None", str | None]:
|
||||||
"""Match provider config and its registry name. Returns (config, spec_name)."""
|
"""Match provider config and its registry name. Returns (config, spec_name)."""
|
||||||
from nanobot.providers.registry import PROVIDERS, find_by_name
|
from nanobot.providers.registry import (
|
||||||
|
PROVIDERS,
|
||||||
|
find_by_name,
|
||||||
|
)
|
||||||
|
|
||||||
resolved = preset or self.resolve_preset()
|
resolved = preset or self.resolve_preset()
|
||||||
forced = resolved.provider
|
forced = resolved.provider
|
||||||
@ -390,6 +408,11 @@ class Config(BaseSettings):
|
|||||||
if spec:
|
if spec:
|
||||||
p = getattr(self.providers, spec.name, None)
|
p = getattr(self.providers, spec.name, None)
|
||||||
return (p, spec.name) if p else (None, None)
|
return (p, spec.name) if p else (None, None)
|
||||||
|
# Check for custom provider by name (try both original and normalized)
|
||||||
|
for name_to_try in (forced, forced.replace("-", "_")):
|
||||||
|
p = getattr(self.providers, name_to_try, None)
|
||||||
|
if p and isinstance(p, ProviderConfig):
|
||||||
|
return p, name_to_try
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
model_lower = (model or resolved.model).lower()
|
model_lower = (model or resolved.model).lower()
|
||||||
@ -410,6 +433,14 @@ class Config(BaseSettings):
|
|||||||
if spec.is_oauth or spec.is_local or spec.is_direct or p.api_key:
|
if spec.is_oauth or spec.is_local or spec.is_direct or p.api_key:
|
||||||
return p, spec.name
|
return p, spec.name
|
||||||
|
|
||||||
|
# Check for custom provider by prefix (e.g., "myprovider/gpt-4")
|
||||||
|
# Try both original prefix and normalized (snake_case) prefix
|
||||||
|
if model_prefix:
|
||||||
|
for prefix_to_try in (model_prefix, normalized_prefix):
|
||||||
|
p = getattr(self.providers, prefix_to_try, None)
|
||||||
|
if p and isinstance(p, ProviderConfig) and p.api_base:
|
||||||
|
return p, prefix_to_try
|
||||||
|
|
||||||
# Match by keyword (order follows PROVIDERS registry)
|
# Match by keyword (order follows PROVIDERS registry)
|
||||||
for spec in PROVIDERS:
|
for spec in PROVIDERS:
|
||||||
if spec.is_transcription_only:
|
if spec.is_transcription_only:
|
||||||
@ -445,6 +476,15 @@ class Config(BaseSettings):
|
|||||||
p = getattr(self.providers, spec.name, None)
|
p = getattr(self.providers, spec.name, None)
|
||||||
if p and p.api_key:
|
if p and p.api_key:
|
||||||
return p, spec.name
|
return p, spec.name
|
||||||
|
|
||||||
|
# Final fallback: check for any configured custom provider
|
||||||
|
for attr_name in dir(self.providers):
|
||||||
|
if attr_name.startswith("_"):
|
||||||
|
continue
|
||||||
|
p = getattr(self.providers, attr_name, None)
|
||||||
|
if isinstance(p, ProviderConfig) and p.api_base:
|
||||||
|
return p, attr_name
|
||||||
|
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
def get_provider(
|
def get_provider(
|
||||||
|
|||||||
@ -545,3 +545,16 @@ def find_by_name(name: str) -> ProviderSpec | None:
|
|||||||
if spec.name == normalized:
|
if spec.name == normalized:
|
||||||
return spec
|
return spec
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def create_dynamic_spec(name: str) -> ProviderSpec:
|
||||||
|
"""Create a dynamic ProviderSpec for custom user-defined providers."""
|
||||||
|
normalized = to_snake(name.replace("-", "_"))
|
||||||
|
return ProviderSpec(
|
||||||
|
name=normalized,
|
||||||
|
keywords=(),
|
||||||
|
env_key="",
|
||||||
|
display_name=name.title(),
|
||||||
|
backend="openai_compat",
|
||||||
|
is_direct=True,
|
||||||
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user