fix(agent): URLs OpenAI-compatibles validees pour les fournisseurs LLM
FlowDeck CI / lint (push) Successful in 57s
FlowDeck CI / test (push) Successful in 6m5s
FlowDeck CI / docker (push) Successful in 48s

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.
This commit is contained in:
2026-09-14 22:50:36 -04:00
parent fb14c7e709
commit a0db4d6e65
4 changed files with 42 additions and 28 deletions
+1 -2
View File
@@ -83,8 +83,7 @@ class Settings(BaseSettings):
# nvidia, together, perplexity, xai, # nvidia, together, perplexity, xai,
# qwencloud, minimax, morph, fireworks, # qwencloud, minimax, morph, fireworks,
# cerebras, sambanova, chutes, xiaomi, # cerebras, sambanova, chutes, xiaomi,
# ltx, sealion, sensenova, memtensor, # sealion, sensenova, ollama, offline)
# ollama, offline)
llm_model: str = "gpt-4o" llm_model: str = "gpt-4o"
llm_api_key: str = "" llm_api_key: str = ""
llm_api_base: str = "" # custom base URL (Ollama, OpenRouter, ...) llm_api_base: str = "" # custom base URL (Ollama, OpenRouter, ...)
+20 -22
View File
@@ -7,9 +7,9 @@ into guarded internal actions.
Supported providers (OpenAI-compatible chat-completions JSON response): Supported providers (OpenAI-compatible chat-completions JSON response):
openai, anthropic, mistral, cohere, google, groq, deepseek, openrouter, openai, anthropic, mistral, cohere, google, groq, deepseek, openrouter,
nvidia, together, perplexity, xai, qwencloud, minimax, morph, fireworks, nvidia, together, perplexity, xai, qwencloud, minimax, morph, fireworks,
cerebras, sambanova, chutes, xiaomi, ltx, sealion, sensenova, memtensor, cerebras, sambanova, chutes, xiaomi, sealion, sensenova,
ollama (local, no key). Anthropic/Google/Cohere expose an OpenAI-compatible ollama (local, no key). Anthropic, Google (`/v1beta/openai`) and Cohere
surface at their configured base URL. (`/compatibility/v1`) expose an OpenAI-compatible surface at their base URL.
When no API key is configured (or provider == "offline") the client falls back 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 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"), "openai": ("https://api.openai.com/v1", "gpt-4o"),
"anthropic": ("https://api.anthropic.com/v1", "claude-opus-4-8"), "anthropic": ("https://api.anthropic.com/v1", "claude-opus-4-8"),
"mistral": ("https://api.mistral.ai/v1", "mistral-large-latest"), "mistral": ("https://api.mistral.ai/v1", "mistral-large-latest"),
"cohere": ("https://api.cohere.com/v2", "command-r-plus"), "cohere": ("https://api.cohere.ai/compatibility/v1", "command-a-plus-05-2026"),
"google": ("https://generativelanguage.googleapis.com/v1beta", "gemini-2.0-pro"), "google": ("https://generativelanguage.googleapis.com/v1beta/openai", "gemini-2.0-flash"),
"groq": ("https://api.groq.com/openai/v1", "llama-3.3-70b-versatile"), "groq": ("https://api.groq.com/openai/v1", "llama-3.3-70b-versatile"),
"deepseek": ("https://api.deepseek.com/v1", "deepseek-chat"), "deepseek": ("https://api.deepseek.com/v1", "deepseek-chat"),
"openrouter": ("https://openrouter.ai/api/v1", "meta-llama/llama-3.3-70b-instruct"), "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"), "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"), "together": ("https://api.together.xyz/v1", "meta-llama/Llama-3.3-70B-Instruct-Turbo"),
"perplexity": ("https://api.perplexity.ai/v1", "sonar-pro"), "perplexity": ("https://api.perplexity.ai", "sonar-pro"),
"xai": ("https://api.x.ai/v1", "grok-2-latest"), "xai": ("https://api.x.ai/v1", "grok-4.6"),
"qwencloud": ("https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "qwen-max"), "qwencloud": ("https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "qwen-max"),
"minimax": ("https://api.minimax.chat/v1", "MiniMax-Text-01"), "minimax": ("https://api.minimax.chat/v1", "MiniMax-Text-01"),
"morph": ("https://api.morphllm.com/v1", "morph-v3-large"), "morph": ("https://api.morphllm.com/v1", "morph-v3-large"),
"fireworks": ("https://api.fireworks.ai/inference/v1", "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"), "cerebras": ("https://api.cerebras.ai/v1", "llama-3.3-70b"),
"sambanova": ("https://api.sambanova.ai/v1", "Meta-Llama-3.3-70B-Instruct"), "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"), "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"), "sealion": ("https://api.sea-lion.ai/v1", "aisingapore/Llama-SEA-LION-v3-70B-IT"),
"sensenova": ("https://token.sensenova.cn/v1", "SenseChat-5"), "sensenova": ("https://api.sensenova.cn/compatible-mode/v1", "SenseChat-5"),
"memtensor": ("https://memos.memtensor.cn/api/openmem/v1", "memos-chat"),
"ollama": ("http://localhost:11434/v1", "llama3.1"), "ollama": ("http://localhost:11434/v1", "llama3.1"),
"offline": (None, None), "offline": (None, None),
} }
@@ -85,10 +83,8 @@ PROVIDER_LABELS: dict[str, str] = {
"sambanova": "SambaNova", "sambanova": "SambaNova",
"chutes": "Chutes AI", "chutes": "Chutes AI",
"xiaomi": "Xiaomi (MiMo)", "xiaomi": "Xiaomi (MiMo)",
"ltx": "LTX Studio",
"sealion": "SEA-LION", "sealion": "SEA-LION",
"sensenova": "SenseNova", "sensenova": "SenseNova",
"memtensor": "MemTensor (MemOS)",
"ollama": "Ollama (local)", "ollama": "Ollama (local)",
"offline": "Hors-ligne (mock)", "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"], "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", "mistral": ["mistral-large-latest", "mistral-medium-latest", "mistral-small-latest",
"codestral-latest", "open-mistral-nemo", "pixtral-large-latest"], "codestral-latest", "open-mistral-nemo", "pixtral-large-latest"],
"cohere": ["command-r-plus", "command-r", "command-light", "command-a"], "cohere": ["command-a-plus-05-2026", "command-r-plus", "command-r", "command-a-03-2025"],
"google": ["gemini-2.0-pro", "gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash"], "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", "groq": ["llama-3.3-70b-versatile", "llama-3.1-8b-instant",
"mixtral-8x7b-32768", "gemma2-9b-it"], "mixtral-8x7b-32768", "gemma2-9b-it"],
"deepseek": ["deepseek-chat", "deepseek-reasoner"], "deepseek": ["deepseek-chat", "deepseek-reasoner"],
@@ -113,24 +109,26 @@ PROVIDER_MODELS: dict[str, list[str]] = {
"meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo", "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo",
"Qwen/Qwen2.5-72B-Instruct-Turbo", "mistralai/Mixtral-8x7B-Instruct-v0.1"], "Qwen/Qwen2.5-72B-Instruct-Turbo", "mistralai/Mixtral-8x7B-Instruct-v0.1"],
"perplexity": ["sonar-pro", "sonar", "sonar-reasoning", "sonar-deep-research"], "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"], "qwencloud": ["qwen-max", "qwen-plus", "qwen-turbo", "qwen-long"],
"minimax": ["MiniMax-Text-01", "abab6.5s-chat", "abab6.5-chat"], "minimax": ["MiniMax-Text-01", "abab6.5s-chat", "abab6.5-chat"],
"morph": ["morph-v3-large", "morph-v3-fast"], "morph": ["morph-v3-large", "morph-v3-fast"],
"fireworks": ["accounts/fireworks/models/llama-v3p3-70b-instruct", "fireworks": ["accounts/fireworks/models/deepseek-v4-pro-0813",
"accounts/fireworks/models/mixtral-8x22b-instruct", "accounts/fireworks/models/kimi-k2p6",
"accounts/fireworks/models/qwen2p5-72b-instruct"], "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"], "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", "sambanova": ["Meta-Llama-3.3-70B-Instruct", "Meta-Llama-3.1-405B-Instruct",
"Qwen2.5-72B-Instruct"], "Qwen2.5-72B-Instruct"],
"chutes": ["deepseek-ai/DeepSeek-V3", "deepseek-ai/DeepSeek-R1", "chutes": ["deepseek-ai/DeepSeek-V3", "deepseek-ai/DeepSeek-R1",
"Qwen/Qwen2.5-72B-Instruct"], "Qwen/Qwen2.5-72B-Instruct"],
"xiaomi": ["mimo-7b-rl", "mimo-7b"], "xiaomi": ["mimo-7b-rl", "mimo-7b"],
"ltx": ["ltx-video", "ltx-video-2"],
"sealion": ["aisingapore/Llama-SEA-LION-v3-70B-IT", "sealion": ["aisingapore/Llama-SEA-LION-v3-70B-IT",
"aisingapore/Gemma-SEA-LION-v3-9B-IT"], "aisingapore/Gemma-SEA-LION-v3-9B-IT"],
"sensenova": ["SenseChat-5", "SenseChat-5-Cantonese", "SenseChat-Turbo"], "sensenova": ["SenseChat-5", "SenseChat-5-Cantonese", "SenseChat-Turbo"],
"memtensor": ["memos-chat"],
"ollama": ["llama3.1", "llama3", "mistral", "qwen2.5", "gemma2", "mixtral"], "ollama": ["llama3.1", "llama3", "mistral", "qwen2.5", "gemma2", "mixtral"],
"offline": [], "offline": [],
} }
+3 -4
View File
@@ -296,8 +296,9 @@ async def fetch_provider_models(provider: str, *, api_key: str = "",
api_base: str = "", timeout: int = 20) -> list[str]: api_base: str = "", timeout: int = 20) -> list[str]:
"""Fetch the live model list from a provider (best-effort, no mock). """Fetch the live model list from a provider (best-effort, no mock).
OpenAI-compatible providers use `GET {base}/models` with a Bearer token; OpenAI-compatible providers (including Google's `/openai` surface and
Anthropic uses `x-api-key` + `anthropic-version`; Gemini an `x-goog-api-key`. 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. Returns a de-duplicated list capped at 300 models.
""" """
import httpx import httpx
@@ -311,8 +312,6 @@ async def fetch_provider_models(provider: str, *, api_key: str = "",
headers: dict = {} headers: dict = {}
if provider == "anthropic": if provider == "anthropic":
headers = {"x-api-key": api_key, "anthropic-version": "2023-06-01"} headers = {"x-api-key": api_key, "anthropic-version": "2023-06-01"}
elif provider == "google":
headers = {"x-goog-api-key": api_key}
elif api_key: elif api_key:
headers = {"Authorization": f"Bearer {api_key}"} headers = {"Authorization": f"Bearer {api_key}"}
+18
View File
@@ -88,6 +88,24 @@ def test_llm_client_providers_listed(client):
assert name in PROVIDERS 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): def test_llm_client_offline_no_credentials(client):
from app.services.llm_client import LLMClient from app.services.llm_client import LLMClient
llm = LLMClient(provider="openai", api_key="") llm = LLMClient(provider="openai", api_key="")