From 3164c3c89b6e7286f1f21108ee1e88eebb794ab0 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 04:59:35 +0500 Subject: [PATCH 1/3] feat(providers): add aimlapi.com as an OpenAI-compatible provider Vibe-Trading users who want to move between vendors mid-research currently need one account, one key and one .env block per vendor. AI/ML API is an OpenAI-compatible aggregator that fronts ~350 chat models behind a single key, so this adds it the way ModelScope (#1011) and Requesty (#474) were added: data rows in the five provider surfaces plus a capability record, no new plumbing. The capability flags are set from watched live requests on 2026-09-03, not from the vendor's docs, per the standard capabilities.py keeps for top_level_reasoning_effort. deepseek/deepseek-v4-pro, openai/gpt-5-5, anthropic/claude-sonnet-4.5, google/gemini-3.5-flash, alibaba/qwen3.5-plus and z-ai/glm-5.3-flash each returned 200 for a request carrying a top-level reasoning_effort, and the reasoning models among them returned reasoning_content. send_reasoning_content stays off: replaying reasoning on assistant turns is unverified there, as it is for OpenRouter. Attribution headers ride the existing default_headers hook, but the hook alone is not safe for an identifier. capabilities.default_headers is selected by provider *label*, and AIMLAPI_BASE_URL (or an ambient OPENAI_BASE_URL) can point that label at a proxy or a look-alike host, so the four headers are gated on the resolved base URL's hostname matching an exact allowlist. A substring or suffix test would have accepted api.aimlapi.com.evil.io; both that and notaimlapi.com are covered by tests. HTTP-Referer and X-Title name this project, not the gateway, following the convention OpenRouter established. Every model id shipped here was checked against the live catalog's ids and aliases, filtered to type == "openai/chat-completions", and each was also answered by a real request. Signed-off-by: Stan --- README.md | 2 +- README_ar.md | 2 +- README_es.md | 2 +- README_ja.md | 2 +- README_ko.md | 2 +- README_zh.md | 2 +- agent/.env.example | 9 + agent/cli/_legacy.py | 12 ++ agent/cli/onboard.py | 6 + agent/src/providers/capabilities.py | 46 ++++ agent/src/providers/llm.py | 46 +++- agent/src/providers/llm_providers.json | 9 + agent/src/swarm/models.py | 1 + agent/tests/test_aimlapi_provider.py | 236 +++++++++++++++++++++ agent/tests/test_llm_provider_defaults.py | 2 + agent/tests/test_llm_reasoning_effort.py | 7 +- desktop/electron/src/secure-credentials.ts | 1 + 17 files changed, 374 insertions(+), 13 deletions(-) create mode 100644 agent/tests/test_aimlapi_provider.py diff --git a/README.md b/README.md index de50895caf..df9d5f6c3d 100644 --- a/README.md +++ b/README.md @@ -811,7 +811,7 @@ vibe-trading-mcp # start MCP server (stdio) - OpenAI Codex can also be used with ChatGPT OAuth: set `LANGCHAIN_PROVIDER=openai-codex`, then run `vibe-trading provider login openai-codex`. This does not use `OPENAI_API_KEY`. - GitHub Copilot can be used with an active Copilot subscription instead of a separately billed LLM API key. See [GitHub Copilot SDK provider](#github-copilot-sdk-provider). -> **Supported LLM providers:** OpenRouter, Requesty, OpenAI, Anthropic (native Messages API), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, GitHub Copilot, Ollama (local). When no `*_BASE_URL` is set, each provider falls back to its canonical endpoint, so just a key is enough. See `.env.example` for config. +> **Supported LLM providers:** OpenRouter, Requesty, OpenAI, Anthropic (native Messages API), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, aimlapi.com, GitHub Copilot, Ollama (local). When no `*_BASE_URL` is set, each provider falls back to its canonical endpoint, so just a key is enough. See `.env.example` for config. > **Tip:** All markets work without any API keys thanks to automatic fallback. yfinance/Yahoo (HK/US/Canada/UK), OKX (crypto), mootdx (A-shares, TCP-direct, no IP throttle), and AKShare (A-shares, US, HK, futures, forex) are all free. LSE `.L` quotes must declare GBP or GBp so pence can be normalized before GBP accounting. Tushare token is optional — mootdx is the preferred no-token A-share fallback, with AKShare as a broader backup. diff --git a/README_ar.md b/README_ar.md index 98b62fa4cc..1849f7f838 100644 --- a/README_ar.md +++ b/README_ar.md @@ -769,7 +769,7 @@ vibe-trading-mcp # start MCP server (stdio) - **Docker** للمسار A - يمكن استخدام OpenAI Codex أيضاً عبر ChatGPT OAuth: اضبط `LANGCHAIN_PROVIDER=openai-codex`، ثم شغل `vibe-trading provider login openai-codex`. هذا لا يستخدم `OPENAI_API_KEY`. -> **مزودو LLM المدعومون:** OpenRouter, Requesty, OpenAI, Anthropic (Messages API الأصلي), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, GitHub Copilot, Ollama (local). عند عدم ضبط أي `*_BASE_URL`، يتراجع كل مزوّد إلى نقطة نهايته القانونية، فيكفي مفتاح واحد. راجع `.env.example` للإعداد. +> **مزودو LLM المدعومون:** OpenRouter, Requesty, OpenAI, Anthropic (Messages API الأصلي), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, aimlapi.com, GitHub Copilot, Ollama (local). عند عدم ضبط أي `*_BASE_URL`، يتراجع كل مزوّد إلى نقطة نهايته القانونية، فيكفي مفتاح واحد. راجع `.env.example` للإعداد. > **نصيحة:** تعمل كل الأسواق دون مفاتيح API بفضل fallback التلقائي. yfinance/Yahoo (HK/US/كندا/المملكة المتحدة)، وOKX (crypto)، وmootdx (أسهم A، اتصال TCP مباشر بدون قيود IP)، وAKShare (A-shares, US, HK, futures, forex) كلها مجانية. يجب أن تعلن أسعار LSE `.L` عملة GBP أو GBp لتطبيع البنس قبل المحاسبة بالجنيه. رمز Tushare اختياري — mootdx هو الـ fallback الموصى به لأسهم A بدون رمز، بينما يوفر AKShare احتياطياً أوسع تغطية. diff --git a/README_es.md b/README_es.md index 845a057f27..fb2b52d6b0 100644 --- a/README_es.md +++ b/README_es.md @@ -789,7 +789,7 @@ vibe-trading-mcp # inicia el servidor MCP (stdio) - **Docker** para el Path A - OpenAI Codex también se puede usar con ChatGPT OAuth: configura `LANGCHAIN_PROVIDER=openai-codex` y luego ejecuta `vibe-trading provider login openai-codex`. Esto no usa `OPENAI_API_KEY`. -> **Proveedores de LLM compatibles:** OpenRouter, Requesty, OpenAI, Anthropic (API de Messages nativa), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, GitHub Copilot, Ollama (local). Cuando no se configura ningún `*_BASE_URL`, cada proveedor recurre a su endpoint canónico, así que basta con una clave. Consulta `.env.example` para la configuración. +> **Proveedores de LLM compatibles:** OpenRouter, Requesty, OpenAI, Anthropic (API de Messages nativa), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, aimlapi.com, GitHub Copilot, Ollama (local). Cuando no se configura ningún `*_BASE_URL`, cada proveedor recurre a su endpoint canónico, así que basta con una clave. Consulta `.env.example` para la configuración. > **Consejo:** Todos los mercados funcionan sin ninguna clave de API gracias al fallback automático. yfinance/Yahoo (HK/US/Canadá/Reino Unido), OKX (cripto), mootdx (acciones A, conexión TCP directa, sin limitación de IP) y AKShare (acciones A, EE. UU., HK, futuros, forex) son gratuitos. Las cotizaciones LSE `.L` deben declarar GBP o GBp para normalizar los peniques antes de la contabilidad en GBP. El token de Tushare es opcional — mootdx es el fallback preferido sin token para acciones A, con AKShare como respaldo más amplio. diff --git a/README_ja.md b/README_ja.md index 4b5ae7bcb2..ca4d7fd610 100644 --- a/README_ja.md +++ b/README_ja.md @@ -774,7 +774,7 @@ vibe-trading-mcp # start MCP server (stdio) - Path A では **Docker** - OpenAI Codex は ChatGPT OAuth でも利用できます。`LANGCHAIN_PROVIDER=openai-codex` を設定し、`vibe-trading provider login openai-codex` を実行してください。`OPENAI_API_KEY` は使いません。 -> **Supported LLM providers:** OpenRouter、Requesty、OpenAI、Anthropic(ネイティブ Messages API)、DeepSeek、Gemini、Groq、DashScope/Qwen、Zhipu、Moonshot/Kimi、MiniMax、SiliconFlow(CN + Global)、Xiaomi MIMO、Novita AI、iFlytek Spark、Z.ai、NVIDIA NIM、ModelScope、GitHub Copilot、Ollama(local)。`*_BASE_URL` が未設定の場合、各プロバイダーは canonical なエンドポイントにフォールバックするため、key だけで十分です。設定は `.env.example` を参照してください。 +> **Supported LLM providers:** OpenRouter、Requesty、OpenAI、Anthropic(ネイティブ Messages API)、DeepSeek、Gemini、Groq、DashScope/Qwen、Zhipu、Moonshot/Kimi、MiniMax、SiliconFlow(CN + Global)、Xiaomi MIMO、Novita AI、iFlytek Spark、Z.ai、NVIDIA NIM、ModelScope、aimlapi.com、GitHub Copilot、Ollama(local)。`*_BASE_URL` が未設定の場合、各プロバイダーは canonical なエンドポイントにフォールバックするため、key だけで十分です。設定は `.env.example` を参照してください。 > **Tip:** 自動フォールバックにより、すべての市場は API key なしで利用できます。yfinance/Yahoo(HK/US/カナダ/英国)、OKX(crypto)、mootdx(A 株、TCP 直結で IP 制限なし)、AKShare(A-shares、US、HK、futures、forex)はすべて無料です。LSE `.L` クォートは GBP または GBp の明示が必要で、GBP 会計の前にペンスを正規化します。Tushare token は任意で、A 株は mootdx が推奨の no-token fallback、AKShare がより広いカバレッジのバックアップになります。 diff --git a/README_ko.md b/README_ko.md index f9d9422124..7e85baccdc 100644 --- a/README_ko.md +++ b/README_ko.md @@ -772,7 +772,7 @@ vibe-trading-mcp # start MCP server (stdio) - 경로 A용 **Docker** - OpenAI Codex도 ChatGPT OAuth로 사용할 수 있습니다. `LANGCHAIN_PROVIDER=openai-codex`를 설정한 뒤 `vibe-trading provider login openai-codex`를 실행하세요. 이 방식은 `OPENAI_API_KEY`를 사용하지 않습니다. -> **지원 LLM provider:** OpenRouter, Requesty, OpenAI, Anthropic (native Messages API), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, GitHub Copilot, Ollama(local). `*_BASE_URL`이 설정되지 않으면 각 provider는 canonical endpoint로 폴백하므로 key만 있으면 충분합니다. 설정은 `.env.example`을 참고하세요. +> **지원 LLM provider:** OpenRouter, Requesty, OpenAI, Anthropic (native Messages API), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, aimlapi.com, GitHub Copilot, Ollama(local). `*_BASE_URL`이 설정되지 않으면 각 provider는 canonical endpoint로 폴백하므로 key만 있으면 충분합니다. 설정은 `.env.example`을 참고하세요. > **팁:** 자동 fallback 덕분에 모든 시장은 API key 없이도 작동합니다. yfinance/Yahoo(HK/US/캐나다/영국), OKX(crypto), mootdx(A주, TCP 직결, IP 제한 없음), AKShare(A주, US, HK, futures, forex)는 모두 무료입니다. LSE `.L` 호가는 GBP 또는 GBp로 명시되어야 하며, GBP 회계 전에 펜스를 정규화합니다. Tushare token은 선택 사항이며, mootdx가 권장 no-token A주 fallback이고 AKShare는 더 넓은 커버리지의 백업입니다. diff --git a/README_zh.md b/README_zh.md index 911dbe3711..0116109b07 100644 --- a/README_zh.md +++ b/README_zh.md @@ -779,7 +779,7 @@ vibe-trading-mcp # start MCP server (stdio) - 路径 A 需要 **Docker** - OpenAI Codex 也可通过 ChatGPT OAuth 使用:设置 `LANGCHAIN_PROVIDER=openai-codex`,然后运行 `vibe-trading provider login openai-codex`。它不使用 `OPENAI_API_KEY`。 -> **支持的 LLM providers:** OpenRouter、Requesty、OpenAI、Anthropic(原生 Messages API)、DeepSeek、Gemini、Groq、DashScope/Qwen、Zhipu、Moonshot/Kimi、MiniMax、SiliconFlow(CN + Global)、Xiaomi MIMO、Novita AI、iFlytek 星火、Z.ai、NVIDIA NIM、ModelScope、GitHub Copilot、Ollama(本地)。未设置 `*_BASE_URL` 时,每个 provider 会回退到其规范端点,因此只需一个 key 即可。配置见 `.env.example`。 +> **支持的 LLM providers:** OpenRouter、Requesty、OpenAI、Anthropic(原生 Messages API)、DeepSeek、Gemini、Groq、DashScope/Qwen、Zhipu、Moonshot/Kimi、MiniMax、SiliconFlow(CN + Global)、Xiaomi MIMO、Novita AI、iFlytek 星火、Z.ai、NVIDIA NIM、ModelScope、aimlapi.com、GitHub Copilot、Ollama(本地)。未设置 `*_BASE_URL` 时,每个 provider 会回退到其规范端点,因此只需一个 key 即可。配置见 `.env.example`。 > **提示:** 由于自动 fallback,所有市场都可以在没有任何 API key 的情况下工作。yfinance/Yahoo(港股/美股/加拿大/英国)、OKX(加密)、mootdx(A 股,TCP 直连不封 IP)和 AKShare(A 股、美股、港股、期货、外汇)都是免费的。LSE `.L` 报价必须明示为 GBP 或 GBp,以便在英镑记账前将便士归一化。Tushare token 是可选项 —— mootdx 是首选的免 token A 股 fallback,AKShare 作为覆盖更广的兜底。 diff --git a/agent/.env.example b/agent/.env.example index c41cc3b75d..a6852a3f24 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -144,6 +144,15 @@ OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 # MODELSCOPE_API_KEY=xxx # MODELSCOPE_BASE_URL=https://api-inference.modelscope.cn/v1 +# --- aimlapi.com (AI/ML API) --- +# OpenAI-compatible gateway; one key for 350+ chat models under vendor/model ids. +# Key from https://aimlapi.com/app/keys. There is no /v1/completions endpoint — +# only /v1/chat/completions and /v1/responses. +# LANGCHAIN_PROVIDER=aimlapi +# LANGCHAIN_MODEL_NAME=deepseek/deepseek-v4-pro # or openai/gpt-5-5 / anthropic/claude-sonnet-4.5 +# AIMLAPI_API_KEY=xxx +# AIMLAPI_BASE_URL=https://api.aimlapi.com/v1 + # --- Ollama (local) --- # LANGCHAIN_PROVIDER=ollama # LANGCHAIN_MODEL_NAME=qwen2.5:32b diff --git a/agent/cli/_legacy.py b/agent/cli/_legacy.py index c0befd8b14..815bf6fd42 100644 --- a/agent/cli/_legacy.py +++ b/agent/cli/_legacy.py @@ -453,6 +453,7 @@ def _provider_key_env(provider: str | None) -> str | None: "iflytek": "SPARK_API_KEY", "zai": "ZAI_API_KEY", "modelscope": "MODELSCOPE_API_KEY", + "aimlapi": "AIMLAPI_API_KEY", }.get((provider or "").lower()) @@ -479,6 +480,7 @@ def _provider_base_env(provider: str | None) -> str | None: "iflytek": "SPARK_BASE_URL", "zai": "ZAI_BASE_URL", "modelscope": "MODELSCOPE_BASE_URL", + "aimlapi": "AIMLAPI_BASE_URL", "ollama": "OLLAMA_BASE_URL", }.get((provider or "").lower()) @@ -5599,6 +5601,16 @@ def _handle_prompt_command( "key_prefix": None, "key_placeholder": "api-key...", }, + { + "label": "aimlapi.com", + "provider": "aimlapi", + "key_env": "AIMLAPI_API_KEY", + "base_env": "AIMLAPI_BASE_URL", + "base_url": "https://api.aimlapi.com/v1", + "model": "deepseek/deepseek-v4-pro", + "key_prefix": None, + "key_placeholder": "api-key...", + }, { "label": "NVIDIA NIM", "provider": "nvidia", diff --git a/agent/cli/onboard.py b/agent/cli/onboard.py index 1b217c9144..824996832f 100644 --- a/agent/cli/onboard.py +++ b/agent/cli/onboard.py @@ -95,6 +95,12 @@ class Provider: "MODELSCOPE_API_KEY", "MODELSCOPE_BASE_URL", "https://api-inference.modelscope.cn/v1", None, ("Qwen/Qwen3.5-27B", "Qwen/Qwen3.5-397B-A17B", "Qwen/Qwen3-235B-A22B")), + Provider("aimlapi", "aimlapi.com", "OpenAI-compatible gateway — 350+ chat models, one key", + "deepseek/deepseek-v4-pro", + "AIMLAPI_API_KEY", "AIMLAPI_BASE_URL", + "https://api.aimlapi.com/v1", None, + ("deepseek/deepseek-v4-pro", "openai/gpt-5-5", + "anthropic/claude-sonnet-4.5", "google/gemini-3.5-flash")), Provider("nvidia", "NVIDIA NIM", "hosted NVIDIA API catalog", "nvidia/nemotron-3-ultra-550b-a55b", "NVIDIA_API_KEY", "NVIDIA_BASE_URL", diff --git a/agent/src/providers/capabilities.py b/agent/src/providers/capabilities.py index 46fc1b6caa..bfbad6b259 100644 --- a/agent/src/providers/capabilities.py +++ b/agent/src/providers/capabilities.py @@ -8,6 +8,7 @@ from functools import lru_cache from importlib.metadata import PackageNotFoundError, version from pathlib import Path +from types import MappingProxyType from typing import Mapping, Optional @@ -111,6 +112,50 @@ def _gh_cli_token() -> str: default_headers={"User-Agent": _VIBE_USER_AGENT}, ) +#: Attribution headers for AI/ML API requests. ``HTTP-Referer`` and ``X-Title`` +#: are the OpenRouter convention and name the *calling application* — this +#: project — not the gateway. The two ``X-AIMLAPI-*`` headers are the gateway's +#: own partner/channel attribution. Read-only so the constant cannot be mutated +#: by a caller that receives the merged header dict; ``build_llm`` copies it per +#: request. +#: +#: These are scoped by request host at the call site +#: (``_aimlapi_attribution_headers`` in ``llm.py``) rather than by provider +#: label, because ``AIMLAPI_BASE_URL`` can point the same label at a corporate +#: proxy or a look-alike domain, and an identifier must not follow it there. +AIMLAPI_ATTRIBUTION_HEADERS: Mapping[str, str] = MappingProxyType( + { + "HTTP-Referer": "https://github.com/HKUDS/Vibe-Trading", + "X-Title": "Vibe-Trading", + "X-AIMLAPI-Partner-ID": "part_vibetrading", + "X-AIMLAPI-Source": "agent/vibe-trading", + } +) + +#: Hosts that really are AI/ML API. Matched against the resolved base URL's +#: hostname *exactly*: a substring or suffix test would also accept +#: ``api.aimlapi.com.example.net``. +AIMLAPI_ATTRIBUTION_HOSTS: frozenset[str] = frozenset( + {"api.aimlapi.com", "aimlapi.com"} +) + +# AI/ML API is an OpenAI-compatible aggregator: one key, 350+ chat models under +# ``vendor/model`` ids. Verified live on 2026-09-03 against +# ``deepseek/deepseek-v4-pro``, ``openai/gpt-5-5``, +# ``anthropic/claude-sonnet-4.5``, ``google/gemini-3.5-flash``, +# ``alibaba/qwen3.5-plus`` and ``z-ai/glm-5.3-flash``: reasoning models return +# ``reasoning_content``, and a top-level ``reasoning_effort`` was accepted by +# every one of them, which is the bar ``top_level_reasoning_effort`` sets. +# ``send_reasoning_content`` stays off — replaying reasoning on assistant turns +# is unverified here, exactly as it is for OpenRouter. +_AIMLAPI_CAPABILITIES = ProviderCapabilities( + "aimlapi", + "AIMLAPI_API_KEY", + "AIMLAPI_BASE_URL", + capture_reasoning=True, + top_level_reasoning_effort=True, +) + # GLM thinking models (glm-4.5+/glm-5.x) stream the chain-of-thought as # ``reasoning_content`` with the final answer in ``content``. Capture the # reasoning like DeepSeek; do NOT replay it on assistant turns — @@ -231,6 +276,7 @@ def _gh_cli_token() -> str: "MODELSCOPE_API_KEY", "MODELSCOPE_BASE_URL", ), + "aimlapi": _AIMLAPI_CAPABILITIES, "ollama": ProviderCapabilities("ollama", None, "OLLAMA_BASE_URL"), "copilot": _COPILOT_CAPABILITIES, "github-copilot": _COPILOT_CAPABILITIES, diff --git a/agent/src/providers/llm.py b/agent/src/providers/llm.py index b2e9ddeb1e..b8e26da530 100644 --- a/agent/src/providers/llm.py +++ b/agent/src/providers/llm.py @@ -18,6 +18,8 @@ from src.config.accessor import get_env_config, reset_env_config from src.providers.capabilities import ( + AIMLAPI_ATTRIBUTION_HEADERS, + AIMLAPI_ATTRIBUTION_HOSTS, ProviderCapabilities, get_llm_credentials, get_provider_capabilities, @@ -1329,6 +1331,35 @@ def _sends_top_level_reasoning_effort(caps: ProviderCapabilities) -> bool: return _supports_top_level_reasoning_effort(caps) and _openai_label_points_at_openai(caps) +def _aimlapi_attribution_headers( + caps: ProviderCapabilities, base_url: str +) -> dict[str, str]: + """Return AI/ML API attribution headers when the request really goes there. + + Args: + caps: Resolved provider capabilities. + base_url: Base URL the request will actually be sent to. + + Returns: + A fresh header dict for AI/ML API's own hosts, empty otherwise. + + Notes: + The gate is the resolved *hostname*, not the provider label and not a + substring of the URL. ``AIMLAPI_BASE_URL`` (or an ambient + ``OPENAI_BASE_URL``) can point the label at a corporate proxy, and + ``api.aimlapi.com.example.net`` contains the real host as a prefix — + neither should receive an identifier meant for one vendor. + """ + if caps.name != "aimlapi": + return {} + host = ( + urlparse(base_url if "//" in base_url else f"https://{base_url}").hostname or "" + ) + if host.lower() not in AIMLAPI_ATTRIBUTION_HOSTS: + return {} + return dict(AIMLAPI_ATTRIBUTION_HEADERS) + + def uses_responses_api( provider: str, configured_responses_api: bool | None, @@ -1592,12 +1623,15 @@ def build_llm(*, model_name: Optional[str] = None, callbacks: Any = None) -> Any "vibe_provider": provider, "vibe_api_key": api_key, } - if caps.default_headers: - headers = dict(caps.default_headers) - if caps.name in {"moonshot", "kimi-coding"}: - custom_ua = get_env_config().llm.moonshot_user_agent.strip() - if custom_ua: - headers["User-Agent"] = custom_ua + headers = dict(caps.default_headers) + if caps.name in {"moonshot", "kimi-coding"}: + custom_ua = get_env_config().llm.moonshot_user_agent.strip() + if custom_ua: + headers["User-Agent"] = custom_ua + # Attribution goes underneath: anything the provider record already + # configures for this key wins, and the shared constant is never mutated. + headers = {**_aimlapi_attribution_headers(caps, creds["base_url"]), **headers} + if headers: _validate_explicit_headers(headers, source=f"{caps.name} provider configuration") kwargs["default_headers"] = headers if get_env_config().llm.vibe_trading_disable_http_proxy: diff --git a/agent/src/providers/llm_providers.json b/agent/src/providers/llm_providers.json index d13a869cea..78bb5f0077 100644 --- a/agent/src/providers/llm_providers.json +++ b/agent/src/providers/llm_providers.json @@ -212,6 +212,15 @@ "default_base_url": "https://api-inference.modelscope.cn/v1", "api_key_required": true }, + { + "name": "aimlapi", + "label": "aimlapi.com", + "api_key_env": "AIMLAPI_API_KEY", + "base_url_env": "AIMLAPI_BASE_URL", + "default_model": "deepseek/deepseek-v4-pro", + "default_base_url": "https://api.aimlapi.com/v1", + "api_key_required": true + }, { "name": "ollama", "label": "Ollama", diff --git a/agent/src/swarm/models.py b/agent/src/swarm/models.py index 4e1205e8db..7be51c6373 100644 --- a/agent/src/swarm/models.py +++ b/agent/src/swarm/models.py @@ -19,6 +19,7 @@ _REASONING_EFFORTS = frozenset({"none", "low", "medium", "high", "max"}) _PUBLIC_PROVIDERS = frozenset( { + "aimlapi", "anthropic", "dashscope", "deepseek", diff --git a/agent/tests/test_aimlapi_provider.py b/agent/tests/test_aimlapi_provider.py new file mode 100644 index 0000000000..ce683f87ee --- /dev/null +++ b/agent/tests/test_aimlapi_provider.py @@ -0,0 +1,236 @@ +"""AI/ML API provider wiring and attribution-header scoping. + +The attribution headers are worth their own file because every failure mode +here is silent: a malformed partner id, a header that never leaves, or a header +that leaves for the wrong host all return HTTP 200 and look exactly like a +working integration. +""" + +from __future__ import annotations + +import json +import os +import re +from pathlib import Path +from unittest.mock import patch + +import cli +from cli.onboard import PROVIDERS as ONBOARD_PROVIDERS +from src.config.accessor import reset_env_config +from src.providers.capabilities import ( + AIMLAPI_ATTRIBUTION_HEADERS, + AIMLAPI_ATTRIBUTION_HOSTS, + get_llm_credentials, + get_provider_capabilities, +) +from src.providers.llm import build_llm +from src.swarm.models import _PUBLIC_PROVIDERS + +# Backend contract for the partner id. A value that does not match is accepted +# by the gateway and then dropped, so a typo is invisible at runtime. +_PARTNER_ID_PATTERN = re.compile(r"^part_[A-Za-z0-9]{1,64}$") + +_BASE_ENV = { + "LANGCHAIN_PROVIDER": "aimlapi", + "AIMLAPI_API_KEY": "aimlapi-test-key", + "LANGCHAIN_MODEL_NAME": "deepseek/deepseek-v4-pro", +} + + +def _build_and_capture(env: dict[str, str]) -> dict: + """Build an LLM with ``env`` applied and return the ChatOpenAI kwargs.""" + import src.providers.llm as llm_mod + + captured: dict = {} + + class _FakeChatOpenAI: + def __init__(self, **kwargs: object) -> None: + captured.update(kwargs) + + try: + with patch.object(llm_mod, "_dotenv_loaded", True): + with patch.dict(os.environ, env, clear=True): + with patch.object(llm_mod, "ChatOpenAIWithReasoning", _FakeChatOpenAI): + build_llm() + finally: + reset_env_config() + return captured + + +# --------------------------------------------------------------------------- +# Registry wiring +# --------------------------------------------------------------------------- + + +def test_capabilities_match_the_live_wire_behaviour() -> None: + """AI/ML API is an OpenAI-compatible gateway with its own key namespace.""" + caps = get_provider_capabilities("aimlapi", "deepseek/deepseek-v4-pro") + + assert caps.name == "aimlapi" + assert caps.api_key_env == "AIMLAPI_API_KEY" + assert caps.base_url_env == "AIMLAPI_BASE_URL" + assert caps.capture_reasoning is True + assert caps.top_level_reasoning_effort is True + # ``extra_body.reasoning`` is an OpenRouter relay option, not a wire + # standard: this gateway takes the top-level field instead. + assert caps.openrouter_reasoning_body is False + assert caps.send_reasoning_content is False + + +def test_registered_in_every_provider_surface() -> None: + """A provider missing from one surface is unreachable from that surface.""" + providers_path = ( + Path(__file__).resolve().parents[1] / "src" / "providers" / "llm_providers.json" + ) + catalog = { + item["name"]: item + for item in json.loads(providers_path.read_text(encoding="utf-8")) + } + entry = catalog["aimlapi"] + onboard = next(item for item in ONBOARD_PROVIDERS if item.key == "aimlapi") + legacy = next( + item for item in cli._PROVIDER_CHOICES if item["provider"] == "aimlapi" + ) + + assert entry["default_base_url"] == onboard.base_url == legacy["base_url"] + assert entry["api_key_env"] == onboard.key_env == legacy["key_env"] + assert entry["base_url_env"] == onboard.base_env == legacy["base_env"] + assert entry["default_model"] == onboard.default_model == legacy["model"] + assert "aimlapi" in _PUBLIC_PROVIDERS + + +def test_display_name_is_the_vendor_domain() -> None: + """Every user-facing surface shows the vendor's own name.""" + providers_path = ( + Path(__file__).resolve().parents[1] / "src" / "providers" / "llm_providers.json" + ) + catalog = { + item["name"]: item + for item in json.loads(providers_path.read_text(encoding="utf-8")) + } + onboard = next(item for item in ONBOARD_PROVIDERS if item.key == "aimlapi") + legacy = next( + item for item in cli._PROVIDER_CHOICES if item["provider"] == "aimlapi" + ) + + assert catalog["aimlapi"]["label"] == "aimlapi.com" + assert onboard.label == "aimlapi.com" + assert str(legacy["label"]).startswith("aimlapi.com") + + +def test_base_url_falls_back_to_the_catalog_default() -> None: + """A key alone must be enough; no *_BASE_URL should mean api.aimlapi.com.""" + with patch.dict(os.environ, _BASE_ENV, clear=True): + creds = get_llm_credentials("aimlapi", "deepseek/deepseek-v4-pro") + + assert creds["base_url"] == "https://api.aimlapi.com/v1" + + +# --------------------------------------------------------------------------- +# Attribution headers +# --------------------------------------------------------------------------- + + +def test_partner_id_matches_the_gateway_pattern() -> None: + """A malformed partner id is dropped server-side without an error.""" + partner_id = AIMLAPI_ATTRIBUTION_HEADERS["X-AIMLAPI-Partner-ID"] + + assert _PARTNER_ID_PATTERN.match(partner_id) + + +def test_referer_and_title_identify_this_project() -> None: + """HTTP-Referer / X-Title name the calling app, not the gateway.""" + assert AIMLAPI_ATTRIBUTION_HEADERS["HTTP-Referer"] == ( + "https://github.com/HKUDS/Vibe-Trading" + ) + assert AIMLAPI_ATTRIBUTION_HEADERS["X-Title"] == "Vibe-Trading" + assert AIMLAPI_ATTRIBUTION_HEADERS["X-AIMLAPI-Source"] == "agent/vibe-trading" + + +def test_headers_reach_the_client_on_the_default_endpoint() -> None: + captured = _build_and_capture(dict(_BASE_ENV)) + headers = captured["default_headers"] + + assert headers["X-AIMLAPI-Partner-ID"] == ( + AIMLAPI_ATTRIBUTION_HEADERS["X-AIMLAPI-Partner-ID"] + ) + assert headers["X-AIMLAPI-Source"] == "agent/vibe-trading" + assert headers["HTTP-Referer"] == "https://github.com/HKUDS/Vibe-Trading" + assert headers["X-Title"] == "Vibe-Trading" + + +def test_shared_constant_is_never_mutated() -> None: + """Each build gets its own dict; the module constant stays pristine.""" + before = dict(AIMLAPI_ATTRIBUTION_HEADERS) + + captured = _build_and_capture(dict(_BASE_ENV)) + captured["default_headers"]["X-Title"] = "mutated" + captured["default_headers"]["X-Injected"] = "mutated" + + assert dict(AIMLAPI_ATTRIBUTION_HEADERS) == before + assert _build_and_capture(dict(_BASE_ENV))["default_headers"]["X-Title"] == ( + "Vibe-Trading" + ) + + +def test_headers_do_not_ride_to_another_provider() -> None: + """Attribution belongs to one gateway; OpenRouter must not carry it.""" + captured = _build_and_capture( + { + "LANGCHAIN_PROVIDER": "openrouter", + "OPENROUTER_API_KEY": "sk-or-test", + "OPENROUTER_BASE_URL": "https://openrouter.ai/api/v1", + "LANGCHAIN_MODEL_NAME": "deepseek/deepseek-v4-pro", + } + ) + + assert "default_headers" not in captured + + +def test_lookalike_hosts_get_no_attribution() -> None: + """The check is the resolved host, not a substring of the base URL. + + ``api.aimlapi.com.evil.io`` contains the real host as a prefix and + ``notaimlapi.com`` contains the vendor name; neither is the vendor, and a + partner header sent to either leaks an identifier to a third party. + """ + for base_url in ( + "https://api.aimlapi.com.evil.io/v1", + "https://notaimlapi.com/v1", + "https://aimlapi.com.evil.io/v1", + "https://proxy.internal/aimlapi/v1", + ): + env = dict(_BASE_ENV, AIMLAPI_BASE_URL=base_url) + captured = _build_and_capture(env) + + assert "default_headers" not in captured, base_url + + +def test_explicit_vendor_host_still_gets_attribution() -> None: + """A pinned but legitimate endpoint keeps its attribution.""" + for base_url in ( + "https://api.aimlapi.com/v1", + "https://API.AIMLAPI.COM/v1", + "https://aimlapi.com/v1", + ): + env = dict(_BASE_ENV, AIMLAPI_BASE_URL=base_url) + captured = _build_and_capture(env) + + assert captured["default_headers"]["X-AIMLAPI-Source"] == ( + "agent/vibe-trading" + ), base_url + + +def test_attribution_hosts_are_exact_names() -> None: + """Guard the host allowlist against a wildcard creeping in.""" + assert AIMLAPI_ATTRIBUTION_HOSTS == frozenset({"api.aimlapi.com", "aimlapi.com"}) + assert all("*" not in host for host in AIMLAPI_ATTRIBUTION_HOSTS) + + +def test_reasoning_effort_is_sent_top_level_not_in_extra_body() -> None: + """The gateway accepts the OpenAI field; the OpenRouter relay is not used.""" + env = dict(_BASE_ENV, LANGCHAIN_REASONING_EFFORT="low") + captured = _build_and_capture(env) + + assert captured["reasoning_effort"] == "low" + assert captured["extra_body"] is None diff --git a/agent/tests/test_llm_provider_defaults.py b/agent/tests/test_llm_provider_defaults.py index 2d8c941c45..5fe1e9901d 100644 --- a/agent/tests/test_llm_provider_defaults.py +++ b/agent/tests/test_llm_provider_defaults.py @@ -36,6 +36,7 @@ "spark": "4.0Ultra", "zai": "glm-5.1", "modelscope": "Qwen/Qwen3.5-27B", + "aimlapi": "deepseek/deepseek-v4-pro", } @@ -104,6 +105,7 @@ def test_interactive_onboard_suggests_current_primary_models() -> None: assert onboard_defaults["siliconflow-cn"] == "deepseek-ai/DeepSeek-V3.1-Terminus" assert onboard_defaults["siliconflow-global"] == "deepseek-ai/DeepSeek-V3.1-Terminus" assert onboard_defaults["modelscope"] == "Qwen/Qwen3.5-27B" + assert onboard_defaults["aimlapi"] == "deepseek/deepseek-v4-pro" assert onboard_defaults["nvidia"] == "nvidia/nemotron-3-ultra-550b-a55b" diff --git a/agent/tests/test_llm_reasoning_effort.py b/agent/tests/test_llm_reasoning_effort.py index a83c86afd5..b40754fab1 100644 --- a/agent/tests/test_llm_reasoning_effort.py +++ b/agent/tests/test_llm_reasoning_effort.py @@ -281,7 +281,12 @@ def test_every_allowlisted_provider_has_a_recorded_reason(self) -> None: allowlisted = { name for name, caps in _PROVIDERS.items() if caps.top_level_reasoning_effort } - assert allowlisted == {"openai", "deepseek"} + # aimlapi joined on 2026-09-03: a chat-completions request carrying a + # top-level reasoning_effort was answered 200 by deepseek/deepseek-v4-pro, + # openai/gpt-5-5, anthropic/claude-sonnet-4.5, google/gemini-3.5-flash, + # alibaba/qwen3.5-plus and z-ai/glm-5.3-flash through that gateway — + # five upstream vendors, none of which rejected the field. + assert allowlisted == {"openai", "deepseek", "aimlapi"} def test_explicit_openai_provider_keeps_effort_for_deepseek_model(self) -> None: kwargs = _capture_kwargs( diff --git a/desktop/electron/src/secure-credentials.ts b/desktop/electron/src/secure-credentials.ts index 283c900446..c64b152d87 100644 --- a/desktop/electron/src/secure-credentials.ts +++ b/desktop/electron/src/secure-credentials.ts @@ -27,6 +27,7 @@ const ENV_CREDENTIALS = new Set([ "MINIMAX_API_KEY", "MIMO_API_KEY", "MODELSCOPE_API_KEY", + "AIMLAPI_API_KEY", "SPARK_API_KEY", "ZAI_API_KEY", "TUSHARE_TOKEN", From 16f0bf5707e7c5d73bcad6d73227c5802a12aed5 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 05:05:07 +0500 Subject: [PATCH 2/3] =?UTF-8?q?chore(aimlapi):=20fork-only=20placement=20?= =?UTF-8?q?=E2=80=94=20do=20not=20send=20upstream?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the aimlapi.com entry to the head of the four hand-ordered provider surfaces (llm_providers.json, which drives the Web Settings dropdown; the onboarding wizard; the `vibe-trading init` menu; the README provider line in six languages) and gives it the same free-text "recommended" marker the repo already uses on OpenRouter — the only featured-provider concept this codebase has. No badge mechanism is invented. This is promotional placement, not a functional change, and it is isolated in its own commit so it can be dropped with a single `git revert` before any upstream pull request. Nothing outside the ordering changes: the machine provider id, credentials, endpoint and capability record are all untouched. Note the fork now shows two entries marked "recommended", ours and OpenRouter's. Signed-off-by: Stan The interactive `vibe-trading init` menu test pinned openrouter to literal choice 1; it now resolves the number through the `_provider_choice_number` helper the file already defines, so the assertion survives this reordering and reverts cleanly with it. Signed-off-by: Stan --- README.md | 2 +- README_ar.md | 2 +- README_es.md | 2 +- README_ja.md | 2 +- README_ko.md | 2 +- README_zh.md | 2 +- agent/cli/_legacy.py | 20 ++++++++++---------- agent/cli/onboard.py | 12 ++++++------ agent/src/providers/llm_providers.json | 18 +++++++++--------- agent/tests/test_cli_init.py | 6 +++++- 10 files changed, 36 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index df9d5f6c3d..4c9199fd01 100644 --- a/README.md +++ b/README.md @@ -811,7 +811,7 @@ vibe-trading-mcp # start MCP server (stdio) - OpenAI Codex can also be used with ChatGPT OAuth: set `LANGCHAIN_PROVIDER=openai-codex`, then run `vibe-trading provider login openai-codex`. This does not use `OPENAI_API_KEY`. - GitHub Copilot can be used with an active Copilot subscription instead of a separately billed LLM API key. See [GitHub Copilot SDK provider](#github-copilot-sdk-provider). -> **Supported LLM providers:** OpenRouter, Requesty, OpenAI, Anthropic (native Messages API), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, aimlapi.com, GitHub Copilot, Ollama (local). When no `*_BASE_URL` is set, each provider falls back to its canonical endpoint, so just a key is enough. See `.env.example` for config. +> **Supported LLM providers:** aimlapi.com, OpenRouter, Requesty, OpenAI, Anthropic (native Messages API), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, GitHub Copilot, Ollama (local). When no `*_BASE_URL` is set, each provider falls back to its canonical endpoint, so just a key is enough. See `.env.example` for config. > **Tip:** All markets work without any API keys thanks to automatic fallback. yfinance/Yahoo (HK/US/Canada/UK), OKX (crypto), mootdx (A-shares, TCP-direct, no IP throttle), and AKShare (A-shares, US, HK, futures, forex) are all free. LSE `.L` quotes must declare GBP or GBp so pence can be normalized before GBP accounting. Tushare token is optional — mootdx is the preferred no-token A-share fallback, with AKShare as a broader backup. diff --git a/README_ar.md b/README_ar.md index 1849f7f838..cca9d53908 100644 --- a/README_ar.md +++ b/README_ar.md @@ -769,7 +769,7 @@ vibe-trading-mcp # start MCP server (stdio) - **Docker** للمسار A - يمكن استخدام OpenAI Codex أيضاً عبر ChatGPT OAuth: اضبط `LANGCHAIN_PROVIDER=openai-codex`، ثم شغل `vibe-trading provider login openai-codex`. هذا لا يستخدم `OPENAI_API_KEY`. -> **مزودو LLM المدعومون:** OpenRouter, Requesty, OpenAI, Anthropic (Messages API الأصلي), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, aimlapi.com, GitHub Copilot, Ollama (local). عند عدم ضبط أي `*_BASE_URL`، يتراجع كل مزوّد إلى نقطة نهايته القانونية، فيكفي مفتاح واحد. راجع `.env.example` للإعداد. +> **مزودو LLM المدعومون:** aimlapi.com, OpenRouter, Requesty, OpenAI, Anthropic (Messages API الأصلي), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, GitHub Copilot, Ollama (local). عند عدم ضبط أي `*_BASE_URL`، يتراجع كل مزوّد إلى نقطة نهايته القانونية، فيكفي مفتاح واحد. راجع `.env.example` للإعداد. > **نصيحة:** تعمل كل الأسواق دون مفاتيح API بفضل fallback التلقائي. yfinance/Yahoo (HK/US/كندا/المملكة المتحدة)، وOKX (crypto)، وmootdx (أسهم A، اتصال TCP مباشر بدون قيود IP)، وAKShare (A-shares, US, HK, futures, forex) كلها مجانية. يجب أن تعلن أسعار LSE `.L` عملة GBP أو GBp لتطبيع البنس قبل المحاسبة بالجنيه. رمز Tushare اختياري — mootdx هو الـ fallback الموصى به لأسهم A بدون رمز، بينما يوفر AKShare احتياطياً أوسع تغطية. diff --git a/README_es.md b/README_es.md index fb2b52d6b0..f4b67e231f 100644 --- a/README_es.md +++ b/README_es.md @@ -789,7 +789,7 @@ vibe-trading-mcp # inicia el servidor MCP (stdio) - **Docker** para el Path A - OpenAI Codex también se puede usar con ChatGPT OAuth: configura `LANGCHAIN_PROVIDER=openai-codex` y luego ejecuta `vibe-trading provider login openai-codex`. Esto no usa `OPENAI_API_KEY`. -> **Proveedores de LLM compatibles:** OpenRouter, Requesty, OpenAI, Anthropic (API de Messages nativa), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, aimlapi.com, GitHub Copilot, Ollama (local). Cuando no se configura ningún `*_BASE_URL`, cada proveedor recurre a su endpoint canónico, así que basta con una clave. Consulta `.env.example` para la configuración. +> **Proveedores de LLM compatibles:** aimlapi.com, OpenRouter, Requesty, OpenAI, Anthropic (API de Messages nativa), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, GitHub Copilot, Ollama (local). Cuando no se configura ningún `*_BASE_URL`, cada proveedor recurre a su endpoint canónico, así que basta con una clave. Consulta `.env.example` para la configuración. > **Consejo:** Todos los mercados funcionan sin ninguna clave de API gracias al fallback automático. yfinance/Yahoo (HK/US/Canadá/Reino Unido), OKX (cripto), mootdx (acciones A, conexión TCP directa, sin limitación de IP) y AKShare (acciones A, EE. UU., HK, futuros, forex) son gratuitos. Las cotizaciones LSE `.L` deben declarar GBP o GBp para normalizar los peniques antes de la contabilidad en GBP. El token de Tushare es opcional — mootdx es el fallback preferido sin token para acciones A, con AKShare como respaldo más amplio. diff --git a/README_ja.md b/README_ja.md index ca4d7fd610..e84745a15f 100644 --- a/README_ja.md +++ b/README_ja.md @@ -774,7 +774,7 @@ vibe-trading-mcp # start MCP server (stdio) - Path A では **Docker** - OpenAI Codex は ChatGPT OAuth でも利用できます。`LANGCHAIN_PROVIDER=openai-codex` を設定し、`vibe-trading provider login openai-codex` を実行してください。`OPENAI_API_KEY` は使いません。 -> **Supported LLM providers:** OpenRouter、Requesty、OpenAI、Anthropic(ネイティブ Messages API)、DeepSeek、Gemini、Groq、DashScope/Qwen、Zhipu、Moonshot/Kimi、MiniMax、SiliconFlow(CN + Global)、Xiaomi MIMO、Novita AI、iFlytek Spark、Z.ai、NVIDIA NIM、ModelScope、aimlapi.com、GitHub Copilot、Ollama(local)。`*_BASE_URL` が未設定の場合、各プロバイダーは canonical なエンドポイントにフォールバックするため、key だけで十分です。設定は `.env.example` を参照してください。 +> **Supported LLM providers:** aimlapi.com、OpenRouter、Requesty、OpenAI、Anthropic(ネイティブ Messages API)、DeepSeek、Gemini、Groq、DashScope/Qwen、Zhipu、Moonshot/Kimi、MiniMax、SiliconFlow(CN + Global)、Xiaomi MIMO、Novita AI、iFlytek Spark、Z.ai、NVIDIA NIM、ModelScope、GitHub Copilot、Ollama(local)。`*_BASE_URL` が未設定の場合、各プロバイダーは canonical なエンドポイントにフォールバックするため、key だけで十分です。設定は `.env.example` を参照してください。 > **Tip:** 自動フォールバックにより、すべての市場は API key なしで利用できます。yfinance/Yahoo(HK/US/カナダ/英国)、OKX(crypto)、mootdx(A 株、TCP 直結で IP 制限なし)、AKShare(A-shares、US、HK、futures、forex)はすべて無料です。LSE `.L` クォートは GBP または GBp の明示が必要で、GBP 会計の前にペンスを正規化します。Tushare token は任意で、A 株は mootdx が推奨の no-token fallback、AKShare がより広いカバレッジのバックアップになります。 diff --git a/README_ko.md b/README_ko.md index 7e85baccdc..c3ddbe9455 100644 --- a/README_ko.md +++ b/README_ko.md @@ -772,7 +772,7 @@ vibe-trading-mcp # start MCP server (stdio) - 경로 A용 **Docker** - OpenAI Codex도 ChatGPT OAuth로 사용할 수 있습니다. `LANGCHAIN_PROVIDER=openai-codex`를 설정한 뒤 `vibe-trading provider login openai-codex`를 실행하세요. 이 방식은 `OPENAI_API_KEY`를 사용하지 않습니다. -> **지원 LLM provider:** OpenRouter, Requesty, OpenAI, Anthropic (native Messages API), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, aimlapi.com, GitHub Copilot, Ollama(local). `*_BASE_URL`이 설정되지 않으면 각 provider는 canonical endpoint로 폴백하므로 key만 있으면 충분합니다. 설정은 `.env.example`을 참고하세요. +> **지원 LLM provider:** aimlapi.com, OpenRouter, Requesty, OpenAI, Anthropic (native Messages API), DeepSeek, Gemini, Groq, DashScope/Qwen, Zhipu, Moonshot/Kimi, MiniMax, SiliconFlow (CN + Global), Xiaomi MIMO, Novita AI, iFlytek Spark, Z.ai, NVIDIA NIM, ModelScope, GitHub Copilot, Ollama(local). `*_BASE_URL`이 설정되지 않으면 각 provider는 canonical endpoint로 폴백하므로 key만 있으면 충분합니다. 설정은 `.env.example`을 참고하세요. > **팁:** 자동 fallback 덕분에 모든 시장은 API key 없이도 작동합니다. yfinance/Yahoo(HK/US/캐나다/영국), OKX(crypto), mootdx(A주, TCP 직결, IP 제한 없음), AKShare(A주, US, HK, futures, forex)는 모두 무료입니다. LSE `.L` 호가는 GBP 또는 GBp로 명시되어야 하며, GBP 회계 전에 펜스를 정규화합니다. Tushare token은 선택 사항이며, mootdx가 권장 no-token A주 fallback이고 AKShare는 더 넓은 커버리지의 백업입니다. diff --git a/README_zh.md b/README_zh.md index 0116109b07..afdc0e2f31 100644 --- a/README_zh.md +++ b/README_zh.md @@ -779,7 +779,7 @@ vibe-trading-mcp # start MCP server (stdio) - 路径 A 需要 **Docker** - OpenAI Codex 也可通过 ChatGPT OAuth 使用:设置 `LANGCHAIN_PROVIDER=openai-codex`,然后运行 `vibe-trading provider login openai-codex`。它不使用 `OPENAI_API_KEY`。 -> **支持的 LLM providers:** OpenRouter、Requesty、OpenAI、Anthropic(原生 Messages API)、DeepSeek、Gemini、Groq、DashScope/Qwen、Zhipu、Moonshot/Kimi、MiniMax、SiliconFlow(CN + Global)、Xiaomi MIMO、Novita AI、iFlytek 星火、Z.ai、NVIDIA NIM、ModelScope、aimlapi.com、GitHub Copilot、Ollama(本地)。未设置 `*_BASE_URL` 时,每个 provider 会回退到其规范端点,因此只需一个 key 即可。配置见 `.env.example`。 +> **支持的 LLM providers:** aimlapi.com、OpenRouter、Requesty、OpenAI、Anthropic(原生 Messages API)、DeepSeek、Gemini、Groq、DashScope/Qwen、Zhipu、Moonshot/Kimi、MiniMax、SiliconFlow(CN + Global)、Xiaomi MIMO、Novita AI、iFlytek 星火、Z.ai、NVIDIA NIM、ModelScope、GitHub Copilot、Ollama(本地)。未设置 `*_BASE_URL` 时,每个 provider 会回退到其规范端点,因此只需一个 key 即可。配置见 `.env.example`。 > **提示:** 由于自动 fallback,所有市场都可以在没有任何 API key 的情况下工作。yfinance/Yahoo(港股/美股/加拿大/英国)、OKX(加密)、mootdx(A 股,TCP 直连不封 IP)和 AKShare(A 股、美股、港股、期货、外汇)都是免费的。LSE `.L` 报价必须明示为 GBP 或 GBp,以便在英镑记账前将便士归一化。Tushare token 是可选项 —— mootdx 是首选的免 token A 股 fallback,AKShare 作为覆盖更广的兜底。 diff --git a/agent/cli/_legacy.py b/agent/cli/_legacy.py index 815bf6fd42..f31d4b1838 100644 --- a/agent/cli/_legacy.py +++ b/agent/cli/_legacy.py @@ -5541,6 +5541,16 @@ def _handle_prompt_command( _INIT_ENV_PATH = Path.home() / ".vibe-trading" / ".env" _PROVIDER_CHOICES: list[dict[str, str | None]] = [ + { + "label": "aimlapi.com (recommended - 350+ chat models)", + "provider": "aimlapi", + "key_env": "AIMLAPI_API_KEY", + "base_env": "AIMLAPI_BASE_URL", + "base_url": "https://api.aimlapi.com/v1", + "model": "deepseek/deepseek-v4-pro", + "key_prefix": None, + "key_placeholder": "api-key...", + }, { "label": "OpenRouter (recommended - multiple models)", "provider": "openrouter", @@ -5601,16 +5611,6 @@ def _handle_prompt_command( "key_prefix": None, "key_placeholder": "api-key...", }, - { - "label": "aimlapi.com", - "provider": "aimlapi", - "key_env": "AIMLAPI_API_KEY", - "base_env": "AIMLAPI_BASE_URL", - "base_url": "https://api.aimlapi.com/v1", - "model": "deepseek/deepseek-v4-pro", - "key_prefix": None, - "key_placeholder": "api-key...", - }, { "label": "NVIDIA NIM", "provider": "nvidia", diff --git a/agent/cli/onboard.py b/agent/cli/onboard.py index 824996832f..dc94698b64 100644 --- a/agent/cli/onboard.py +++ b/agent/cli/onboard.py @@ -51,6 +51,12 @@ class Provider: PROVIDERS: Final[tuple[Provider, ...]] = ( + Provider("aimlapi", "aimlapi.com", "recommended — 350+ chat models, one key", + "deepseek/deepseek-v4-pro", + "AIMLAPI_API_KEY", "AIMLAPI_BASE_URL", + "https://api.aimlapi.com/v1", None, + ("deepseek/deepseek-v4-pro", "openai/gpt-5-5", + "anthropic/claude-sonnet-4.5", "google/gemini-3.5-flash")), Provider("openrouter", "OpenRouter", "recommended — 200+ models, one key", "deepseek/deepseek-v4-pro", "OPENROUTER_API_KEY", "OPENROUTER_BASE_URL", @@ -95,12 +101,6 @@ class Provider: "MODELSCOPE_API_KEY", "MODELSCOPE_BASE_URL", "https://api-inference.modelscope.cn/v1", None, ("Qwen/Qwen3.5-27B", "Qwen/Qwen3.5-397B-A17B", "Qwen/Qwen3-235B-A22B")), - Provider("aimlapi", "aimlapi.com", "OpenAI-compatible gateway — 350+ chat models, one key", - "deepseek/deepseek-v4-pro", - "AIMLAPI_API_KEY", "AIMLAPI_BASE_URL", - "https://api.aimlapi.com/v1", None, - ("deepseek/deepseek-v4-pro", "openai/gpt-5-5", - "anthropic/claude-sonnet-4.5", "google/gemini-3.5-flash")), Provider("nvidia", "NVIDIA NIM", "hosted NVIDIA API catalog", "nvidia/nemotron-3-ultra-550b-a55b", "NVIDIA_API_KEY", "NVIDIA_BASE_URL", diff --git a/agent/src/providers/llm_providers.json b/agent/src/providers/llm_providers.json index 78bb5f0077..b7fd665194 100644 --- a/agent/src/providers/llm_providers.json +++ b/agent/src/providers/llm_providers.json @@ -1,4 +1,13 @@ [ + { + "name": "aimlapi", + "label": "aimlapi.com", + "api_key_env": "AIMLAPI_API_KEY", + "base_url_env": "AIMLAPI_BASE_URL", + "default_model": "deepseek/deepseek-v4-pro", + "default_base_url": "https://api.aimlapi.com/v1", + "api_key_required": true + }, { "name": "openrouter", "label": "OpenRouter", @@ -212,15 +221,6 @@ "default_base_url": "https://api-inference.modelscope.cn/v1", "api_key_required": true }, - { - "name": "aimlapi", - "label": "aimlapi.com", - "api_key_env": "AIMLAPI_API_KEY", - "base_url_env": "AIMLAPI_BASE_URL", - "default_model": "deepseek/deepseek-v4-pro", - "default_base_url": "https://api.aimlapi.com/v1", - "api_key_required": true - }, { "name": "ollama", "label": "Ollama", diff --git a/agent/tests/test_cli_init.py b/agent/tests/test_cli_init.py index 95de0ee4cb..9defecfc4f 100644 --- a/agent/tests/test_cli_init.py +++ b/agent/tests/test_cli_init.py @@ -65,7 +65,11 @@ def test_cmd_init_writes_agent_env_for_openrouter(self, tmp_path: Path) -> None: env_path = tmp_path / ".env" with patch.object(cli, "_INIT_ENV_PATH", env_path), \ - patch.object(cli.IntPrompt, "ask", return_value=1), \ + patch.object( + cli.IntPrompt, + "ask", + return_value=_provider_choice_number("openrouter"), + ), \ patch.object( cli.Prompt, "ask", From c0610a5a49227258f139d103b7d6627e5a3b7383 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 18:17:34 +0500 Subject: [PATCH 3/3] fix(aimlapi): use the registered partner id The placeholder part_vibetrading was a readable stand-in chosen before the partner was registered. Registration mints the id server-side, so the real value is part_m4LUWIsqHcT74jLfzge4zJAJ. A wrong or unknown partner id is accepted with a 200 and silently not attributed, so this would not have surfaced at runtime. --- agent/src/providers/capabilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/src/providers/capabilities.py b/agent/src/providers/capabilities.py index bfbad6b259..eee724d949 100644 --- a/agent/src/providers/capabilities.py +++ b/agent/src/providers/capabilities.py @@ -127,7 +127,7 @@ def _gh_cli_token() -> str: { "HTTP-Referer": "https://github.com/HKUDS/Vibe-Trading", "X-Title": "Vibe-Trading", - "X-AIMLAPI-Partner-ID": "part_vibetrading", + "X-AIMLAPI-Partner-ID": "part_m4LUWIsqHcT74jLfzge4zJAJ", "X-AIMLAPI-Source": "agent/vibe-trading", } )