Files
wehub-resource-sync fed8b2eed7
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

150 lines
5.5 KiB
Python

"""Generic provider for OpenAI-wire-compatible endpoints.
Each ``openai_compatible`` YAML file describes one logical endpoint
(Mistral, Together, Fireworks, Ollama, ...) with its own
``api_key_env`` and ``base_url``. Multiple files can coexist; the
plugin produces one set of models per file, each pre-configured with
the right credentials and URL.
The plugin also handles the **legacy** ``OPENAI_BASE_URL`` + ``LLM_NAME``
local-endpoint pattern that previously lived in ``OpenAIProvider``. That
path generates models dynamically from ``LLM_NAME``, using
``OPENAI_BASE_URL`` and ``OPENAI_API_KEY`` as the endpoint config.
"""
from __future__ import annotations
import logging
import os
from typing import List, Optional
from application.core.model_settings import (
AvailableModel,
ModelCapabilities,
ModelProvider,
)
from application.llm.openai import OpenAILLM
from application.llm.providers.base import Provider
logger = logging.getLogger(__name__)
def _parse_model_names(llm_name: Optional[str]) -> List[str]:
if not llm_name:
return []
return [name.strip() for name in llm_name.split(",") if name.strip()]
class OpenAICompatibleProvider(Provider):
name = "openai_compatible"
llm_class = OpenAILLM
def get_api_key(self, settings) -> Optional[str]:
# Per-model: each catalog supplies its own ``api_key_env``. There
# is no single plugin-wide key. LLMCreator reads the per-model
# ``api_key`` set during catalog materialization.
return None
def is_enabled(self, settings) -> bool:
# Concrete enablement happens per catalog (in ``get_models``).
# Returning True lets the registry call ``get_models`` so we can
# decide per-file whether to contribute models.
return True
def get_models(self, settings, catalogs) -> List[AvailableModel]:
out: List[AvailableModel] = []
for catalog in catalogs:
out.extend(self._materialize_yaml_catalog(catalog))
if settings.OPENAI_BASE_URL and settings.LLM_NAME:
out.extend(self._materialize_legacy_local_endpoint(settings))
return out
def _materialize_yaml_catalog(self, catalog) -> List[AvailableModel]:
"""Resolve one openai_compatible YAML into ready-to-dispatch models.
Skipped (with an INFO-level log) if ``api_key_env`` resolves to
nothing — no point publishing models the user can't actually
call. INFO rather than WARNING because operators may legitimately
drop multiple provider YAMLs as templates and only set the env
vars for the ones they actually use; a missing key is ambiguous,
not necessarily a misconfig.
"""
if not catalog.base_url:
raise ValueError(
f"{catalog.source_path}: openai_compatible YAML must set "
"'base_url'."
)
if not catalog.api_key_env:
raise ValueError(
f"{catalog.source_path}: openai_compatible YAML must set "
"'api_key_env'."
)
api_key = os.environ.get(catalog.api_key_env)
if not api_key:
logger.info(
"openai_compatible catalog %s skipped: env var %s is not set",
catalog.source_path,
catalog.api_key_env,
)
return []
out: List[AvailableModel] = []
for m in catalog.models:
out.append(self._with_endpoint(m, catalog.base_url, api_key))
return out
def _materialize_legacy_local_endpoint(self, settings) -> List[AvailableModel]:
"""Generate AvailableModels from ``LLM_NAME`` for the legacy
``OPENAI_BASE_URL`` deployment pattern (Ollama, LM Studio, ...).
Preserves the historical ``provider="openai"`` display behavior
by setting ``display_provider="openai"``.
"""
from application.core.model_yaml import resolve_attachment_alias
attachments = resolve_attachment_alias("image")
api_key = settings.OPENAI_API_KEY or settings.API_KEY
out: List[AvailableModel] = []
for model_name in _parse_model_names(settings.LLM_NAME):
out.append(
AvailableModel(
id=model_name,
provider=ModelProvider.OPENAI_COMPATIBLE,
display_name=model_name,
description=f"Custom OpenAI-compatible model at {settings.OPENAI_BASE_URL}",
base_url=settings.OPENAI_BASE_URL,
capabilities=ModelCapabilities(
supports_tools=True,
supported_attachment_types=attachments,
),
api_key=api_key,
display_provider="openai",
)
)
return out
@staticmethod
def _with_endpoint(
model: AvailableModel, base_url: str, api_key: str
) -> AvailableModel:
"""Return a copy of ``model`` carrying the catalog's endpoint config.
The catalog-level ``base_url`` is the default; an explicit
per-model ``base_url`` in the YAML wins.
"""
return AvailableModel(
id=model.id,
provider=model.provider,
display_name=model.display_name,
description=model.description,
capabilities=model.capabilities,
enabled=model.enabled,
base_url=model.base_url or base_url,
display_provider=model.display_provider,
api_key=api_key,
)