Merge afdd7abaff into 4641c03340
This commit is contained in:
commit
f289a156e6
|
|
@ -3,4 +3,4 @@ OPENAI_API_KEY=
|
|||
GOOGLE_API_KEY=
|
||||
ANTHROPIC_API_KEY=
|
||||
XAI_API_KEY=
|
||||
OPENROUTER_API_KEY=
|
||||
OPENROUTER_API_KEY=
|
||||
|
|
@ -217,3 +217,6 @@ __marimo__/
|
|||
|
||||
# Cache
|
||||
**/data_cache/
|
||||
|
||||
# Research Results
|
||||
results/*
|
||||
17
cli/main.py
17
cli/main.py
|
|
@ -547,15 +547,26 @@ def get_user_selections():
|
|||
)
|
||||
selected_research_depth = select_research_depth()
|
||||
|
||||
# Step 6: LLM Provider
|
||||
# Step 5: LLM Provider
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 6: LLM Provider", "Select your LLM provider"
|
||||
"Step 5: LLM Provider", "Select which service to talk to"
|
||||
)
|
||||
)
|
||||
selected_llm_provider, backend_url = select_llm_provider()
|
||||
|
||||
# Step 7: Thinking agents
|
||||
# GitHub Copilot: run OAuth before proceeding
|
||||
if selected_llm_provider.lower() == "copilot":
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 5b: Copilot Auth",
|
||||
"Authenticate with the GitHub CLI to use GitHub Copilot",
|
||||
)
|
||||
)
|
||||
if not perform_copilot_oauth():
|
||||
exit(1)
|
||||
|
||||
# Step 6: Thinking agents
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 7: Thinking Agents", "Select your thinking agents for analysis"
|
||||
|
|
|
|||
182
cli/utils.py
182
cli/utils.py
|
|
@ -1,3 +1,4 @@
|
|||
import subprocess
|
||||
import questionary
|
||||
from typing import List, Optional, Tuple, Dict
|
||||
|
||||
|
|
@ -134,13 +135,76 @@ def select_research_depth() -> int:
|
|||
return choice
|
||||
|
||||
|
||||
def _resolve_model_options(
|
||||
provider: str, static_options: dict
|
||||
) -> list[tuple[str, str]]:
|
||||
"""Return model options for the given provider.
|
||||
|
||||
For Copilot, fetches the live model list from the API.
|
||||
For all other providers, looks up the static options dict.
|
||||
Exits with an error message if no models are available.
|
||||
"""
|
||||
if provider.lower() == "copilot":
|
||||
options = fetch_copilot_models()
|
||||
if not options:
|
||||
console.print("[red]No Copilot models available. Exiting...[/red]")
|
||||
exit(1)
|
||||
else:
|
||||
options = static_options.get(provider.lower())
|
||||
if not options:
|
||||
console.print(f"[red]No models available for provider '{provider}'. Exiting...[/red]")
|
||||
exit(1)
|
||||
return options
|
||||
|
||||
|
||||
def select_shallow_thinking_agent(provider) -> str:
|
||||
"""Select shallow thinking llm engine using an interactive selection."""
|
||||
|
||||
# Define shallow thinking llm engine options with their corresponding model names
|
||||
# Ordering: medium → light → heavy (balanced first for quick tasks)
|
||||
# Within same tier, newer models first
|
||||
SHALLOW_AGENT_OPTIONS = {
|
||||
"openai": [
|
||||
("GPT-5 Mini - Balanced speed, cost, and capability", "gpt-5-mini"),
|
||||
("GPT-5 Nano - High-throughput, simple tasks", "gpt-5-nano"),
|
||||
("GPT-5.4 - Latest frontier, 1M context", "gpt-5.4"),
|
||||
("GPT-4.1 - Smartest non-reasoning model", "gpt-4.1"),
|
||||
],
|
||||
"anthropic": [
|
||||
("Claude Sonnet 4.6 - Best speed and intelligence balance", "claude-sonnet-4-6"),
|
||||
("Claude Haiku 4.5 - Fast, near-instant responses", "claude-haiku-4-5"),
|
||||
("Claude Sonnet 4.5 - Agents and coding", "claude-sonnet-4-5"),
|
||||
],
|
||||
"google": [
|
||||
("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"),
|
||||
("Gemini 2.5 Flash - Balanced, stable", "gemini-2.5-flash"),
|
||||
("Gemini 3.1 Flash Lite - Most cost-efficient", "gemini-3.1-flash-lite-preview"),
|
||||
("Gemini 2.5 Flash Lite - Fast, low-cost", "gemini-2.5-flash-lite"),
|
||||
],
|
||||
"xai": [
|
||||
("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"),
|
||||
("Grok 4 Fast (Non-Reasoning) - Speed optimized", "grok-4-fast-non-reasoning"),
|
||||
("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"),
|
||||
],
|
||||
"openrouter": [
|
||||
("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"),
|
||||
("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"),
|
||||
],
|
||||
"ollama": [
|
||||
("Qwen3:latest (8B, local)", "qwen3:latest"),
|
||||
("GPT-OSS:latest (20B, local)", "gpt-oss:latest"),
|
||||
("GLM-4.7-Flash:latest (30B, local)", "glm-4.7-flash:latest"),
|
||||
],
|
||||
"copilot": [],
|
||||
}
|
||||
|
||||
options = _resolve_model_options(provider, SHALLOW_AGENT_OPTIONS)
|
||||
|
||||
choice = questionary.select(
|
||||
"Select Your [Quick-Thinking LLM Engine]:",
|
||||
choices=[
|
||||
questionary.Choice(display, value=value)
|
||||
for display, value in options
|
||||
for display, value in get_model_options(provider, "quick")
|
||||
],
|
||||
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
||||
|
|
@ -165,11 +229,53 @@ def select_shallow_thinking_agent(provider) -> str:
|
|||
def select_deep_thinking_agent(provider) -> str:
|
||||
"""Select deep thinking llm engine using an interactive selection."""
|
||||
|
||||
# Define deep thinking llm engine options with their corresponding model names
|
||||
# Ordering: heavy → medium → light (most capable first for deep tasks)
|
||||
# Within same tier, newer models first
|
||||
DEEP_AGENT_OPTIONS = {
|
||||
"openai": [
|
||||
("GPT-5.4 - Latest frontier, 1M context", "gpt-5.4"),
|
||||
("GPT-5.2 - Strong reasoning, cost-effective", "gpt-5.2"),
|
||||
("GPT-5 Mini - Balanced speed, cost, and capability", "gpt-5-mini"),
|
||||
("GPT-5.4 Pro - Most capable, expensive ($30/$180 per 1M tokens)", "gpt-5.4-pro"),
|
||||
],
|
||||
"anthropic": [
|
||||
("Claude Opus 4.6 - Most intelligent, agents and coding", "claude-opus-4-6"),
|
||||
("Claude Opus 4.5 - Premium, max intelligence", "claude-opus-4-5"),
|
||||
("Claude Sonnet 4.6 - Best speed and intelligence balance", "claude-sonnet-4-6"),
|
||||
("Claude Sonnet 4.5 - Agents and coding", "claude-sonnet-4-5"),
|
||||
],
|
||||
"google": [
|
||||
("Gemini 3.1 Pro - Reasoning-first, complex workflows", "gemini-3.1-pro-preview"),
|
||||
("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"),
|
||||
("Gemini 2.5 Pro - Stable pro model", "gemini-2.5-pro"),
|
||||
("Gemini 2.5 Flash - Balanced, stable", "gemini-2.5-flash"),
|
||||
],
|
||||
"xai": [
|
||||
("Grok 4 - Flagship model", "grok-4-0709"),
|
||||
("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"),
|
||||
("Grok 4 Fast (Reasoning) - High-performance", "grok-4-fast-reasoning"),
|
||||
("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"),
|
||||
],
|
||||
"openrouter": [
|
||||
("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"),
|
||||
("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"),
|
||||
],
|
||||
"ollama": [
|
||||
("GLM-4.7-Flash:latest (30B, local)", "glm-4.7-flash:latest"),
|
||||
("GPT-OSS:latest (20B, local)", "gpt-oss:latest"),
|
||||
("Qwen3:latest (8B, local)", "qwen3:latest"),
|
||||
],
|
||||
"copilot": [], # populated dynamically by fetch_copilot_models()
|
||||
}
|
||||
|
||||
options = _resolve_model_options(provider, DEEP_AGENT_OPTIONS)
|
||||
|
||||
choice = questionary.select(
|
||||
"Select Your [Deep-Thinking LLM Engine]:",
|
||||
choices=[
|
||||
questionary.Choice(display, value=value)
|
||||
for display, value in get_model_options(provider, "deep")
|
||||
for display, value in options
|
||||
],
|
||||
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
||||
style=questionary.Style(
|
||||
|
|
@ -187,9 +293,23 @@ def select_deep_thinking_agent(provider) -> str:
|
|||
|
||||
return choice
|
||||
|
||||
def fetch_copilot_models() -> list[tuple[str, str]]:
|
||||
"""Fetch models from the GitHub Copilot inference API.
|
||||
|
||||
Returns a list of (display_label, model_id) tuples sorted by model ID.
|
||||
Requires authentication via ``gh auth login`` with a Copilot subscription.
|
||||
"""
|
||||
from tradingagents.llm_clients.copilot_client import list_copilot_models
|
||||
|
||||
console.print("[dim]Fetching available Copilot models...[/dim]")
|
||||
models = list_copilot_models()
|
||||
if not models:
|
||||
console.print("[yellow]Warning: Could not fetch Copilot models.[/yellow]")
|
||||
return models
|
||||
|
||||
|
||||
def select_llm_provider() -> tuple[str, str]:
|
||||
"""Select the OpenAI api url using interactive selection."""
|
||||
# Define OpenAI api options with their corresponding endpoints
|
||||
"""Select the LLM provider using interactive selection."""
|
||||
BASE_URLS = [
|
||||
("OpenAI", "https://api.openai.com/v1"),
|
||||
("Google", "https://generativelanguage.googleapis.com/v1"),
|
||||
|
|
@ -197,8 +317,9 @@ def select_llm_provider() -> tuple[str, str]:
|
|||
("xAI", "https://api.x.ai/v1"),
|
||||
("Openrouter", "https://openrouter.ai/api/v1"),
|
||||
("Ollama", "http://localhost:11434/v1"),
|
||||
("Copilot", ""), # resolved at runtime via GraphQL
|
||||
]
|
||||
|
||||
|
||||
choice = questionary.select(
|
||||
"Select your LLM Provider:",
|
||||
choices=[
|
||||
|
|
@ -214,17 +335,64 @@ def select_llm_provider() -> tuple[str, str]:
|
|||
]
|
||||
),
|
||||
).ask()
|
||||
|
||||
|
||||
if choice is None:
|
||||
console.print("\n[red]no OpenAI backend selected. Exiting...[/red]")
|
||||
console.print("\n[red]No LLM provider selected. Exiting...[/red]")
|
||||
exit(1)
|
||||
|
||||
|
||||
display_name, url = choice
|
||||
print(f"You selected: {display_name}\tURL: {url}")
|
||||
|
||||
return display_name, url
|
||||
|
||||
|
||||
def perform_copilot_oauth() -> bool:
|
||||
"""Ensure the user is authenticated with the GitHub CLI for Copilot.
|
||||
|
||||
Checks for an existing token and verifies Copilot access. If the token
|
||||
is missing, offers to run ``gh auth login`` interactively.
|
||||
|
||||
Returns True if a valid token with Copilot access is available, False otherwise.
|
||||
"""
|
||||
from tradingagents.llm_clients.copilot_client import check_copilot_auth, get_github_token
|
||||
|
||||
token = get_github_token()
|
||||
if token:
|
||||
if check_copilot_auth():
|
||||
console.print("[green]✓ Authenticated with GitHub Copilot[/green]")
|
||||
return True
|
||||
console.print(
|
||||
"[yellow]⚠ GitHub token found but Copilot access failed. "
|
||||
"Check your Copilot subscription.[/yellow]"
|
||||
)
|
||||
return False
|
||||
|
||||
console.print(
|
||||
"[yellow]⚠ No GitHub token found.[/yellow] "
|
||||
"You need to authenticate to use GitHub Copilot."
|
||||
)
|
||||
should_login = questionary.confirm("Run `gh auth login` now?", default=True).ask()
|
||||
if not should_login:
|
||||
console.print("[red]GitHub authentication skipped. Exiting...[/red]")
|
||||
return False
|
||||
|
||||
try:
|
||||
result = subprocess.run(["gh", "auth", "login"])
|
||||
if result.returncode != 0:
|
||||
console.print("[red]`gh auth login` failed.[/red]")
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
console.print("[red]Error: `gh` command not found. Please install the GitHub CLI.[/red]")
|
||||
return False
|
||||
|
||||
if check_copilot_auth():
|
||||
console.print("[green]✓ Authenticated with GitHub Copilot[/green]")
|
||||
return True
|
||||
|
||||
console.print("[red]Could not retrieve token or verify Copilot access after login.[/red]")
|
||||
return False
|
||||
|
||||
|
||||
def ask_openai_reasoning_effort() -> str:
|
||||
"""Ask for OpenAI reasoning effort level."""
|
||||
choices = [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
"""GitHub Copilot LLM client.
|
||||
|
||||
Authenticates via the ``gh`` CLI (``gh auth token``) and calls the Copilot
|
||||
inference API (api.individual.githubcopilot.com) using headers reverse-
|
||||
engineered from the Copilot CLI (copilot-developer-cli integration ID).
|
||||
|
||||
No env var or separate auth module needed — run ``gh auth login`` once.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from copy import deepcopy
|
||||
from typing import Any, Optional
|
||||
|
||||
import requests
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
from .base_client import BaseLLMClient, normalize_content
|
||||
from .validators import validate_model
|
||||
|
||||
# Required headers for the Copilot inference API (reverse-engineered from
|
||||
# /usr/local/lib/node_modules/@github/copilot).
|
||||
_COPILOT_HEADERS = {
|
||||
"Copilot-Integration-Id": "copilot-developer-cli",
|
||||
"X-GitHub-Api-Version": "2025-05-01",
|
||||
"Openai-Intent": "conversation-agent",
|
||||
}
|
||||
|
||||
# Models that only support /responses, not /chat/completions on the Copilot endpoint.
|
||||
_RESPONSES_ONLY_MODELS = frozenset((
|
||||
"gpt-5.4", "gpt-5.4-mini",
|
||||
"gpt-5.3-codex", "gpt-5.2-codex",
|
||||
"gpt-5.1-codex", "gpt-5.1-codex-mini", "gpt-5.1-codex-max",
|
||||
))
|
||||
|
||||
_PASSTHROUGH_KWARGS = (
|
||||
"timeout", "max_retries", "reasoning_effort",
|
||||
"callbacks", "http_client", "http_async_client",
|
||||
)
|
||||
|
||||
|
||||
def get_github_token() -> Optional[str]:
|
||||
"""Return a GitHub token via the ``gh`` CLI."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gh", "auth", "token"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return result.stdout.strip()
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _get_copilot_api_url() -> str:
|
||||
"""Resolve the Copilot inference base URL via GraphQL, falling back to the
|
||||
standard individual endpoint."""
|
||||
token = get_github_token()
|
||||
if token:
|
||||
try:
|
||||
resp = requests.post(
|
||||
"https://api.github.com/graphql",
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
json={"query": "{ viewer { copilotEndpoints { api } } }"},
|
||||
timeout=5,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
api = resp.json()["data"]["viewer"]["copilotEndpoints"]["api"]
|
||||
if api:
|
||||
return api.rstrip("/")
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
return "https://api.individual.githubcopilot.com"
|
||||
|
||||
|
||||
def list_copilot_models() -> list[tuple[str, str]]:
|
||||
"""Fetch available Copilot models from the inference API.
|
||||
|
||||
Returns a list of ``(display_label, model_id)`` tuples sorted by model ID.
|
||||
Requires ``gh auth login`` with an active Copilot subscription.
|
||||
"""
|
||||
token = get_github_token()
|
||||
if not token:
|
||||
return []
|
||||
try:
|
||||
url = _get_copilot_api_url()
|
||||
resp = requests.get(
|
||||
f"{url}/models",
|
||||
headers={"Authorization": f"Bearer {token}", **_COPILOT_HEADERS},
|
||||
timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
models = data.get("data", data) if isinstance(data, dict) else data
|
||||
chat_models = [m for m in models if not m.get("id", "").startswith("text-embedding")]
|
||||
return [(m["id"], m["id"]) for m in sorted(chat_models, key=lambda x: x.get("id", ""))]
|
||||
except requests.exceptions.RequestException:
|
||||
return []
|
||||
|
||||
|
||||
def check_copilot_auth() -> bool:
|
||||
"""Return True if a GitHub token with Copilot access is available."""
|
||||
token = get_github_token()
|
||||
if not token:
|
||||
return False
|
||||
try:
|
||||
url = _get_copilot_api_url()
|
||||
resp = requests.get(
|
||||
f"{url}/models",
|
||||
headers={"Authorization": f"Bearer {token}", **_COPILOT_HEADERS},
|
||||
timeout=5,
|
||||
)
|
||||
return resp.status_code == 200
|
||||
except requests.exceptions.RequestException:
|
||||
return False # Network error should be treated as an auth failure
|
||||
|
||||
|
||||
class NormalizedChatOpenAI(ChatOpenAI):
|
||||
"""ChatOpenAI with normalized content output."""
|
||||
|
||||
def _create_chat_result(self, response, generation_info=None):
|
||||
return super()._create_chat_result(
|
||||
_sanitize_copilot_response(response), generation_info
|
||||
)
|
||||
|
||||
def invoke(self, input, config=None, **kwargs):
|
||||
return normalize_content(super().invoke(input, config, **kwargs))
|
||||
|
||||
|
||||
def _sanitize_copilot_response(response: Any) -> Any:
|
||||
"""Normalize Copilot token usage fields for langchain_openai.
|
||||
|
||||
Copilot can return ``service_tier`` along with ``None`` values in
|
||||
``cached_tokens`` or ``reasoning_tokens``. ``langchain_openai`` subtracts
|
||||
those fields from the prompt/completion totals, which raises ``TypeError``
|
||||
when the detail value is ``None``.
|
||||
"""
|
||||
if isinstance(response, dict):
|
||||
response_dict = deepcopy(response)
|
||||
elif hasattr(response, "model_dump"):
|
||||
response_dict = response.model_dump()
|
||||
else:
|
||||
return response
|
||||
|
||||
usage = response_dict.get("usage")
|
||||
if not isinstance(usage, dict):
|
||||
return response_dict
|
||||
|
||||
if response_dict.get("service_tier") not in {"priority", "flex"}:
|
||||
return response_dict
|
||||
|
||||
prompt_details = usage.get("prompt_tokens_details")
|
||||
if isinstance(prompt_details, dict) and prompt_details.get("cached_tokens") is None:
|
||||
prompt_details["cached_tokens"] = 0
|
||||
|
||||
completion_details = usage.get("completion_tokens_details")
|
||||
if (
|
||||
isinstance(completion_details, dict)
|
||||
and completion_details.get("reasoning_tokens") is None
|
||||
):
|
||||
completion_details["reasoning_tokens"] = 0
|
||||
|
||||
return response_dict
|
||||
|
||||
|
||||
class CopilotClient(BaseLLMClient):
|
||||
"""Client for GitHub Copilot inference API.
|
||||
|
||||
Uses the gh CLI for authentication. Automatically routes models that only
|
||||
support the Responses API (gpt-5.4, codex variants) to ``/responses``
|
||||
instead of ``/chat/completions``.
|
||||
"""
|
||||
|
||||
def get_llm(self) -> Any:
|
||||
"""Return configured ChatOpenAI instance pointed at the Copilot API."""
|
||||
token = get_github_token()
|
||||
if not token:
|
||||
raise RuntimeError(
|
||||
"No GitHub token found. Run `gh auth login` to authenticate."
|
||||
)
|
||||
copilot_url = _get_copilot_api_url()
|
||||
|
||||
llm_kwargs = {
|
||||
"model": self.model,
|
||||
"base_url": copilot_url,
|
||||
"api_key": token,
|
||||
"default_headers": dict(_COPILOT_HEADERS),
|
||||
}
|
||||
|
||||
for key in _PASSTHROUGH_KWARGS:
|
||||
if key in self.kwargs:
|
||||
llm_kwargs[key] = self.kwargs[key]
|
||||
|
||||
if self.model in _RESPONSES_ONLY_MODELS:
|
||||
llm_kwargs["use_responses_api"] = True
|
||||
|
||||
return NormalizedChatOpenAI(**llm_kwargs)
|
||||
|
||||
def validate_model(self) -> bool:
|
||||
return validate_model("copilot", self.model)
|
||||
|
|
@ -2,6 +2,7 @@ from typing import Optional
|
|||
|
||||
from .base_client import BaseLLMClient
|
||||
from .openai_client import OpenAIClient
|
||||
from .copilot_client import CopilotClient
|
||||
from .anthropic_client import AnthropicClient
|
||||
from .google_client import GoogleClient
|
||||
|
||||
|
|
@ -15,7 +16,7 @@ def create_llm_client(
|
|||
"""Create an LLM client for the specified provider.
|
||||
|
||||
Args:
|
||||
provider: LLM provider (openai, anthropic, google, xai, ollama, openrouter)
|
||||
provider: LLM provider (openai, anthropic, google, xai, ollama, openrouter, copilot)
|
||||
model: Model name/identifier
|
||||
base_url: Optional base URL for API endpoint
|
||||
**kwargs: Additional provider-specific arguments
|
||||
|
|
@ -34,6 +35,9 @@ def create_llm_client(
|
|||
"""
|
||||
provider_lower = provider.lower()
|
||||
|
||||
if provider_lower == "copilot":
|
||||
return CopilotClient(model, base_url, **kwargs)
|
||||
|
||||
if provider_lower in ("openai", "ollama", "openrouter"):
|
||||
return OpenAIClient(model, base_url, provider=provider_lower, **kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -83,4 +83,4 @@ class OpenAIClient(BaseLLMClient):
|
|||
|
||||
def validate_model(self) -> bool:
|
||||
"""Validate model for the provider."""
|
||||
return validate_model(self.provider, self.model)
|
||||
return validate_model(self.provider, self.model)
|
||||
|
|
@ -4,20 +4,63 @@ from .model_catalog import get_known_models
|
|||
|
||||
|
||||
VALID_MODELS = {
|
||||
provider: models
|
||||
for provider, models in get_known_models().items()
|
||||
if provider not in ("ollama", "openrouter")
|
||||
"openai": [
|
||||
# GPT-5 series
|
||||
"gpt-5.4-pro",
|
||||
"gpt-5.4",
|
||||
"gpt-5.2",
|
||||
"gpt-5.1",
|
||||
"gpt-5",
|
||||
"gpt-5-mini",
|
||||
"gpt-5-nano",
|
||||
# GPT-4.1 series
|
||||
"gpt-4.1",
|
||||
"gpt-4.1-mini",
|
||||
"gpt-4.1-nano",
|
||||
],
|
||||
"anthropic": [
|
||||
# Claude 4.6 series (latest)
|
||||
"claude-opus-4-6",
|
||||
"claude-sonnet-4-6",
|
||||
# Claude 4.5 series
|
||||
"claude-opus-4-5",
|
||||
"claude-sonnet-4-5",
|
||||
"claude-haiku-4-5",
|
||||
],
|
||||
"google": [
|
||||
# Gemini 3.1 series (preview)
|
||||
"gemini-3.1-pro-preview",
|
||||
"gemini-3.1-flash-lite-preview",
|
||||
# Gemini 3 series (preview)
|
||||
"gemini-3-flash-preview",
|
||||
# Gemini 2.5 series
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-flash-lite",
|
||||
],
|
||||
"xai": [
|
||||
# Grok 4.1 series
|
||||
"grok-4-1-fast-reasoning",
|
||||
"grok-4-1-fast-non-reasoning",
|
||||
# Grok 4 series
|
||||
"grok-4-0709",
|
||||
"grok-4-fast-reasoning",
|
||||
"grok-4-fast-non-reasoning",
|
||||
],
|
||||
# GitHub Copilot: model availability depends on plan and may change.
|
||||
# Accept any model ID and let the API validate it.
|
||||
"copilot": [],
|
||||
}
|
||||
|
||||
|
||||
def validate_model(provider: str, model: str) -> bool:
|
||||
"""Check if model name is valid for the given provider.
|
||||
|
||||
For ollama, openrouter - any model is accepted.
|
||||
For ollama, openrouter, copilot - any model is accepted.
|
||||
"""
|
||||
provider_lower = provider.lower()
|
||||
|
||||
if provider_lower in ("ollama", "openrouter"):
|
||||
if provider_lower in ("ollama", "openrouter", "copilot"):
|
||||
return True
|
||||
|
||||
if provider_lower not in VALID_MODELS:
|
||||
|
|
|
|||
Loading…
Reference in New Issue