From a0db4d6e65cbb1485488c202535fd45a2e89bf3f Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Mon, 14 Sep 2026 22:50:36 -0400 Subject: [PATCH] fix(agent): URLs OpenAI-compatibles validees pour les fournisseurs LLM Cohere -> /compatibility/v1, Google Gemini -> /v1beta/openai, Perplexity -> host racine, Chutes -> llm.chutes.ai/v1, SenseNova -> compatible-mode/v1. Mise a jour des modeles par defaut (xAI grok-4.6, Fireworks deepseek-v4-pro-0813, Cohere command-a-plus). Retrait de LTX Studio (aucune API chat-completions) et MemTensor/MemOS (API memoire non OpenAI-compatible, auth Token + /chat). Google utilise desormais l'auth Bearer pour lister les modeles. Test de non-regression ajoute. --- app/config.py | 3 +-- app/services/llm_client.py | 42 ++++++++++++++++++-------------------- app/services/llm_config.py | 7 +++---- tests/test_agent.py | 18 ++++++++++++++++ 4 files changed, 42 insertions(+), 28 deletions(-) diff --git a/app/config.py b/app/config.py index 11160db..57a7e98 100644 --- a/app/config.py +++ b/app/config.py @@ -83,8 +83,7 @@ class Settings(BaseSettings): # nvidia, together, perplexity, xai, # qwencloud, minimax, morph, fireworks, # cerebras, sambanova, chutes, xiaomi, - # ltx, sealion, sensenova, memtensor, - # ollama, offline) + # sealion, sensenova, ollama, offline) llm_model: str = "gpt-4o" llm_api_key: str = "" llm_api_base: str = "" # custom base URL (Ollama, OpenRouter, ...) diff --git a/app/services/llm_client.py b/app/services/llm_client.py index 609c2f2..e6f2a79 100644 --- a/app/services/llm_client.py +++ b/app/services/llm_client.py @@ -7,9 +7,9 @@ into guarded internal actions. Supported providers (OpenAI-compatible chat-completions JSON response): openai, anthropic, mistral, cohere, google, groq, deepseek, openrouter, nvidia, together, perplexity, xai, qwencloud, minimax, morph, fireworks, - cerebras, sambanova, chutes, xiaomi, ltx, sealion, sensenova, memtensor, - ollama (local, no key). Anthropic/Google/Cohere expose an OpenAI-compatible - surface at their configured base URL. + cerebras, sambanova, chutes, xiaomi, sealion, sensenova, + ollama (local, no key). Anthropic, Google (`/v1beta/openai`) and Cohere + (`/compatibility/v1`) expose an OpenAI-compatible surface at their base URL. When no API key is configured (or provider == "offline") the client falls back to a deterministic, dependency-free *mock planner*. This keeps the whole agent @@ -37,28 +37,26 @@ PROVIDERS = { "openai": ("https://api.openai.com/v1", "gpt-4o"), "anthropic": ("https://api.anthropic.com/v1", "claude-opus-4-8"), "mistral": ("https://api.mistral.ai/v1", "mistral-large-latest"), - "cohere": ("https://api.cohere.com/v2", "command-r-plus"), - "google": ("https://generativelanguage.googleapis.com/v1beta", "gemini-2.0-pro"), + "cohere": ("https://api.cohere.ai/compatibility/v1", "command-a-plus-05-2026"), + "google": ("https://generativelanguage.googleapis.com/v1beta/openai", "gemini-2.0-flash"), "groq": ("https://api.groq.com/openai/v1", "llama-3.3-70b-versatile"), "deepseek": ("https://api.deepseek.com/v1", "deepseek-chat"), "openrouter": ("https://openrouter.ai/api/v1", "meta-llama/llama-3.3-70b-instruct"), "nvidia": ("https://integrate.api.nvidia.com/v1", "nvidia/nemotron-3-super-120b-a12b"), "together": ("https://api.together.xyz/v1", "meta-llama/Llama-3.3-70B-Instruct-Turbo"), - "perplexity": ("https://api.perplexity.ai/v1", "sonar-pro"), - "xai": ("https://api.x.ai/v1", "grok-2-latest"), + "perplexity": ("https://api.perplexity.ai", "sonar-pro"), + "xai": ("https://api.x.ai/v1", "grok-4.6"), "qwencloud": ("https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "qwen-max"), "minimax": ("https://api.minimax.chat/v1", "MiniMax-Text-01"), "morph": ("https://api.morphllm.com/v1", "morph-v3-large"), "fireworks": ("https://api.fireworks.ai/inference/v1", - "accounts/fireworks/models/llama-v3p3-70b-instruct"), + "accounts/fireworks/models/deepseek-v4-pro-0813"), "cerebras": ("https://api.cerebras.ai/v1", "llama-3.3-70b"), "sambanova": ("https://api.sambanova.ai/v1", "Meta-Llama-3.3-70B-Instruct"), - "chutes": ("https://api.chutes.ai/v1", "deepseek-ai/DeepSeek-V3"), + "chutes": ("https://llm.chutes.ai/v1", "deepseek-ai/DeepSeek-V3"), "xiaomi": ("https://api.xiaomimimo.com/v1", "mimo-7b-rl"), - "ltx": ("https://api.ltx.io/v1", "ltx-video"), "sealion": ("https://api.sea-lion.ai/v1", "aisingapore/Llama-SEA-LION-v3-70B-IT"), - "sensenova": ("https://token.sensenova.cn/v1", "SenseChat-5"), - "memtensor": ("https://memos.memtensor.cn/api/openmem/v1", "memos-chat"), + "sensenova": ("https://api.sensenova.cn/compatible-mode/v1", "SenseChat-5"), "ollama": ("http://localhost:11434/v1", "llama3.1"), "offline": (None, None), } @@ -85,10 +83,8 @@ PROVIDER_LABELS: dict[str, str] = { "sambanova": "SambaNova", "chutes": "Chutes AI", "xiaomi": "Xiaomi (MiMo)", - "ltx": "LTX Studio", "sealion": "SEA-LION", "sensenova": "SenseNova", - "memtensor": "MemTensor (MemOS)", "ollama": "Ollama (local)", "offline": "Hors-ligne (mock)", } @@ -99,8 +95,8 @@ PROVIDER_MODELS: dict[str, list[str]] = { "anthropic": ["claude-opus-4-8", "claude-sonnet-4-5", "claude-3-5-sonnet", "claude-haiku-4-5"], "mistral": ["mistral-large-latest", "mistral-medium-latest", "mistral-small-latest", "codestral-latest", "open-mistral-nemo", "pixtral-large-latest"], - "cohere": ["command-r-plus", "command-r", "command-light", "command-a"], - "google": ["gemini-2.0-pro", "gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash"], + "cohere": ["command-a-plus-05-2026", "command-r-plus", "command-r", "command-a-03-2025"], + "google": ["gemini-2.0-flash", "gemini-2.0-flash-lite", "gemini-1.5-pro", "gemini-1.5-flash"], "groq": ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768", "gemma2-9b-it"], "deepseek": ["deepseek-chat", "deepseek-reasoner"], @@ -113,24 +109,26 @@ PROVIDER_MODELS: dict[str, list[str]] = { "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo", "Qwen/Qwen2.5-72B-Instruct-Turbo", "mistralai/Mixtral-8x7B-Instruct-v0.1"], "perplexity": ["sonar-pro", "sonar", "sonar-reasoning", "sonar-deep-research"], - "xai": ["grok-2-latest", "grok-2", "grok-beta", "grok-3"], + "xai": ["grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning", + "grok-build-0.1"], "qwencloud": ["qwen-max", "qwen-plus", "qwen-turbo", "qwen-long"], "minimax": ["MiniMax-Text-01", "abab6.5s-chat", "abab6.5-chat"], "morph": ["morph-v3-large", "morph-v3-fast"], - "fireworks": ["accounts/fireworks/models/llama-v3p3-70b-instruct", - "accounts/fireworks/models/mixtral-8x22b-instruct", - "accounts/fireworks/models/qwen2p5-72b-instruct"], + "fireworks": ["accounts/fireworks/models/deepseek-v4-pro-0813", + "accounts/fireworks/models/kimi-k2p6", + "accounts/fireworks/models/glm-5p2", + "accounts/fireworks/models/minimax-m3", + "accounts/fireworks/models/gpt-oss-120b", + "accounts/fireworks/models/qwen3-8b"], "cerebras": ["llama-3.3-70b", "llama3.1-8b", "llama-3.1-70b"], "sambanova": ["Meta-Llama-3.3-70B-Instruct", "Meta-Llama-3.1-405B-Instruct", "Qwen2.5-72B-Instruct"], "chutes": ["deepseek-ai/DeepSeek-V3", "deepseek-ai/DeepSeek-R1", "Qwen/Qwen2.5-72B-Instruct"], "xiaomi": ["mimo-7b-rl", "mimo-7b"], - "ltx": ["ltx-video", "ltx-video-2"], "sealion": ["aisingapore/Llama-SEA-LION-v3-70B-IT", "aisingapore/Gemma-SEA-LION-v3-9B-IT"], "sensenova": ["SenseChat-5", "SenseChat-5-Cantonese", "SenseChat-Turbo"], - "memtensor": ["memos-chat"], "ollama": ["llama3.1", "llama3", "mistral", "qwen2.5", "gemma2", "mixtral"], "offline": [], } diff --git a/app/services/llm_config.py b/app/services/llm_config.py index 7a54568..7ef8ed4 100644 --- a/app/services/llm_config.py +++ b/app/services/llm_config.py @@ -296,8 +296,9 @@ async def fetch_provider_models(provider: str, *, api_key: str = "", api_base: str = "", timeout: int = 20) -> list[str]: """Fetch the live model list from a provider (best-effort, no mock). - OpenAI-compatible providers use `GET {base}/models` with a Bearer token; - Anthropic uses `x-api-key` + `anthropic-version`; Gemini an `x-goog-api-key`. + OpenAI-compatible providers (including Google's `/openai` surface and + Cohere's compatibility API) use `GET {base}/models` with a Bearer token; + Anthropic's native model listing uses `x-api-key` + `anthropic-version`. Returns a de-duplicated list capped at 300 models. """ import httpx @@ -311,8 +312,6 @@ async def fetch_provider_models(provider: str, *, api_key: str = "", headers: dict = {} if provider == "anthropic": headers = {"x-api-key": api_key, "anthropic-version": "2023-06-01"} - elif provider == "google": - headers = {"x-goog-api-key": api_key} elif api_key: headers = {"Authorization": f"Bearer {api_key}"} diff --git a/tests/test_agent.py b/tests/test_agent.py index 53e8baf..bd8c5f6 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -88,6 +88,24 @@ def test_llm_client_providers_listed(client): assert name in PROVIDERS +def test_llm_client_openai_compatible_bases(client): + """Providers that need an OpenAI-compatible surface must point at it. + + Anthropic/Google/Cohere native APIs are not chat-completions endpoints: + the client always speaks `/chat/completions` with a Bearer token, so the + base URLs must target the compatibility surfaces (validated live). + """ + from app.services.llm_client import PROVIDERS + assert PROVIDERS["cohere"][0] == "https://api.cohere.ai/compatibility/v1" + assert PROVIDERS["google"][0] == ( + "https://generativelanguage.googleapis.com/v1beta/openai") + # Perplexity serves /chat/completions at the host root (no /v1 prefix). + assert PROVIDERS["perplexity"][0] == "https://api.perplexity.ai" + # Chutes' OpenAI-compatible inference host. + assert PROVIDERS["chutes"][0] == "https://llm.chutes.ai/v1" + assert PROVIDERS["sensenova"][0].endswith("/compatible-mode/v1") + + def test_llm_client_offline_no_credentials(client): from app.services.llm_client import LLMClient llm = LLMClient(provider="openai", api_key="")