From a707347ce117f2428df3c017eb68f64a1047acf3 Mon Sep 17 00:00:00 2001 From: Thibault Jaigu Date: Thu, 24 Sep 2026 16:59:35 +0200 Subject: [PATCH] feat(providers): add Requesty gateway provider Add a requesty entry next to openrouter in PROVIDERS. It uses the OpenAI-compatible endpoint at https://router.requesty.ai/v1 with the key from REQUESTY_API_KEY or /config requesty_api_key. Everything after requesty/ is passed through as the model ID, so both catalog paths (requesty/openai/gpt-4o-mini) and managed policy IDs (requesty/claude-sonnet-4-6, requesty/gpt-5-mini@eu) work. The curated /model list is seeded from the managed policy catalog. Docs, i18n READMEs and a test file mirror the OpenRouter coverage. --- README.md | 18 ++- cheetahclaws/commands/config_cmd.py | 1 + cheetahclaws/providers.py | 33 +++++- docs/architecture.md | 3 +- docs/guides/recipes.md | 4 +- docs/guides/reference.md | 5 +- docs/guides/usage.md | 24 ++++ docs/i18n/README.CN.MD | 3 + docs/i18n/README.DE.MD | 3 + docs/i18n/README.ES.MD | 3 + docs/i18n/README.FR.MD | 3 + docs/i18n/README.JP.MD | 3 + docs/i18n/README.KO.MD | 3 + docs/i18n/README.PT.MD | 3 + tests/test_requesty_provider.py | 171 ++++++++++++++++++++++++++++ 15 files changed, 273 insertions(+), 7 deletions(-) create mode 100644 tests/test_requesty_provider.py diff --git a/README.md b/README.md index 62c36c1..e098939 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ Claude Code is a powerful, production-grade AI coding assistant — but its sour | Feature | Details | |---|---| -| Multi-provider | Anthropic · OpenAI · Gemini · Kimi · Qwen · Zhipu · DeepSeek · MiniMax · OpenRouter · Ollama · LM Studio · Custom endpoint | +| Multi-provider | Anthropic · OpenAI · Gemini · Kimi · Qwen · Zhipu · DeepSeek · MiniMax · OpenRouter · Requesty · Ollama · LM Studio · Custom endpoint | | Agent loop | Streaming API + automatic tool-use loop; the whole loop is in `agent.py` | | 28 built-in tools | Read · Write · Edit · Bash · Glob · Grep · WebFetch · WebSearch · NotebookEdit · GetDiagnostics · Memory* · Agent/SendMessage · Skill · AskUserQuestion · Task* · SleepTimer · EnterPlanMode/ExitPlanMode · *(MCP + plugin tools auto-added)* | | Tool profiles | `tool_profile` trims the tool surface sent each turn to save prompt tokens: `full` (default, everything) · `standard` (compact coding) · `research` (web + documents) · `orchestration` (agents + tasks). Set with `/config tool_profile=standard`. [Guide](docs/guides/usage.md#tool-profiles-tool_profile) | @@ -212,10 +212,13 @@ Claude Code is a powerful, production-grade AI coding assistant — but its sour | **DeepSeek** | `deepseek-chat` · `deepseek-reasoner` | 64k | `DEEPSEEK_API_KEY` | | **MiniMax** | `MiniMax-Text-01` · `MiniMax-VL-01` · `abab6.5s-chat` | 256k–1M | `MINIMAX_API_KEY` | | **OpenRouter** _(400+ models, one key)_ | `openrouter/deepseek/deepseek-v4-flash` · `openrouter/anthropic/claude-sonnet-4-6` · `openrouter/openai/gpt-5` | varies | `OPENROUTER_API_KEY` | +| **Requesty** _(700+ models, one key)_ | `requesty/openai/gpt-4o-mini` · `requesty/anthropic/claude-sonnet-4-6` · `requesty/claude-sonnet-4-6` | varies | `REQUESTY_API_KEY` | | **AWS Bedrock / Azure / Vertex** _(via litellm)_ | `litellm//` | varies | provider-specific | > **`openrouter/` gateway:** one key for 400+ models across vendors. The model ID keeps OpenRouter's upstream `/` path, so the call is double-prefixed: `openrouter/deepseek/deepseek-v4-flash`. To pin which upstream provider (and quantization) serves the request, append `@[/]` — `openrouter/deepseek/deepseek-v4-flash@gmicloud/fp8` — which is sent as OpenRouter's `provider` request-body object rather than glued into the model ID. See [usage.md](docs/guides/usage.md#openrouter-400-models-one-key). +> **`requesty/` gateway:** works the same way: everything after `requesty/` is sent as the model ID, either a catalog `/` path (`requesty/openai/gpt-4o-mini`) or a managed policy ID (`requesty/claude-sonnet-4-6`). Managed policy IDs ending in `@eu` (`requesty/gpt-5-mini@eu`) route through EU providers only. See [usage.md](docs/guides/usage.md#requesty-700-models-one-key). + > **`litellm/` adapter:** routes to 100+ providers behind one SDK — mainly for upstreams with awkward auth (Bedrock SigV4, Azure deployment routing, Vertex service-account JWTs). For plain OpenAI-shaped endpoints, prefer the zero-dependency `custom/` adapter. Install with `pip install ".[litellm]"`. See [recipes.md](docs/guides/recipes.md#alternative-cloud-providers-with-non-trivial-auth-via-the-litellm-provider). ### Open-Source (Local via Ollama) @@ -311,7 +314,7 @@ cheetahclaws --model gpt-4o # pick any model cheetahclaws --model deepseek-chat --thinking --verbose ``` -Provider get-key pages: [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com) · [Kimi](https://platform.moonshot.cn) · [Qwen](https://dashscope.aliyun.com) · [Zhipu](https://open.bigmodel.cn) · [DeepSeek](https://platform.deepseek.com) · [MiniMax](https://platform.minimaxi.chat) · [OpenRouter](https://openrouter.ai/keys). +Provider get-key pages: [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com) · [Kimi](https://platform.moonshot.cn) · [Qwen](https://dashscope.aliyun.com) · [Zhipu](https://open.bigmodel.cn) · [DeepSeek](https://platform.deepseek.com) · [MiniMax](https://platform.minimaxi.chat) · [OpenRouter](https://openrouter.ai/keys) · [Requesty](https://app.requesty.ai/api-keys). **One key for 400+ models** — [OpenRouter](https://openrouter.ai) fronts every major vendor behind one OpenAI-compatible endpoint, so a single key covers Claude, GPT, Gemini, DeepSeek, Llama, Qwen and the rest: @@ -322,6 +325,14 @@ cheetahclaws --model openrouter/anthropic/claude-sonnet-4-6 cheetahclaws --model openrouter/deepseek/deepseek-v4-flash@gmicloud/fp8 # pin the upstream provider ``` +[Requesty](https://www.requesty.ai) is another one-key gateway (700+ models, OpenAI-compatible): + +```bash +export REQUESTY_API_KEY=rqsty-... +cheetahclaws --model requesty/openai/gpt-4o-mini +cheetahclaws --model requesty/claude-sonnet-4-6 # managed policy ID +``` + **AWS Bedrock / Azure / Vertex** use the `litellm//` form (`pip install ".[litellm]"`) — full env-var recipes in [recipes.md](docs/guides/recipes.md#alternative-cloud-providers-with-non-trivial-auth-via-the-litellm-provider). > **Full per-provider guide** — every provider's get-key page + example model commands, plus Bedrock/Azure/Vertex env-var recipes: [docs/guides/usage.md](docs/guides/usage.md). @@ -387,11 +398,12 @@ cheetahclaws --model ollama/qwen2.5-coder # 2. provider/model cheetahclaws --model kimi:moonshot-v1-32k # 3. provider:model ``` -**Gateways keep the upstream path.** OpenRouter, NIM and LiteLLM address models by a `/` path of their own, so those calls are double-prefixed — only the **first** segment is the provider, everything after it is passed through verbatim: +**Gateways keep the upstream path.** OpenRouter, Requesty, NIM and LiteLLM address models by a `/` path of their own, so those calls are double-prefixed — only the **first** segment is the provider, everything after it is passed through verbatim: ```bash cheetahclaws --model openrouter/deepseek/deepseek-v4-flash # → OpenRouter, model "deepseek/deepseek-v4-flash" cheetahclaws --model nim/meta/llama-3.3-70b-instruct # → NVIDIA NIM +cheetahclaws --model requesty/openai/gpt-4o-mini # → Requesty, model "openai/gpt-4o-mini" cheetahclaws --model openrouter/deepseek/deepseek-v4-flash@gmicloud/fp8 # + pinned provider / quantization ``` diff --git a/cheetahclaws/commands/config_cmd.py b/cheetahclaws/commands/config_cmd.py index 59a907f..3437061 100644 --- a/cheetahclaws/commands/config_cmd.py +++ b/cheetahclaws/commands/config_cmd.py @@ -43,6 +43,7 @@ def cmd_model(args: str, _state, config) -> bool: info(" e.g. /model kimi:moonshot-v1-32k") info(" e.g. /model openrouter/deepseek/deepseek-v4-flash (multi-level: provider/upstream/model)") info(" e.g. /model openrouter/deepseek/deepseek-v4-flash@gmicloud/fp8 (pin provider/quantization)") + info(" e.g. /model requesty/openai/gpt-4o-mini (or a managed policy: requesty/claude-sonnet-4-6)") else: m = args.strip() # "/model ollama" with no model name → interactive picker diff --git a/cheetahclaws/providers.py b/cheetahclaws/providers.py index c14efae..81fc7b4 100644 --- a/cheetahclaws/providers.py +++ b/cheetahclaws/providers.py @@ -10,6 +10,7 @@ zhipu — Zhipu GLM (glm-4, glm-4-plus, ...) deepseek — DeepSeek (deepseek-v4-flash, deepseek-v4-pro, deepseek-chat, deepseek-reasoner) openrouter — OpenRouter (openrouter/deepseek/deepseek-v4-flash, ...) + requesty — Requesty (requesty/openai/gpt-4o-mini, requesty/claude-sonnet-4-6, ...) minimax — MiniMax (MiniMax-Text-01, abab6.5s-chat, ...) ollama — Local Ollama (llama3.3, qwen2.5-coder, ...) lmstudio — Local LM Studio (any loaded model) @@ -22,7 +23,8 @@ "custom/my-model" uses CUSTOM_BASE_URL from config "openrouter//" multi-level path: the first segment is the provider, everything after it is passed through as the upstream model ID - (e.g. "openrouter/deepseek/deepseek-v4-flash"). Also used by nim/ and litellm/. + (e.g. "openrouter/deepseek/deepseek-v4-flash"). Also used by nim/, requesty/ + and litellm/. "openrouter//@[/]" optional routing suffix: pins the secondary OpenRouter provider (and quantization level) via the `provider` request body, keeping the model field a real model ID. @@ -139,6 +141,35 @@ "mistralai/mistral-large-latest", ], }, + # Requesty: LLM gateway with 700+ models behind one OpenAI-compatible + # endpoint. Get a key at https://app.requesty.ai/api-keys. Like OpenRouter, + # the model ID after the first segment is passed through verbatim, either a + # catalog / path or a managed policy ID (no vendor prefix): + # /model requesty/openai/gpt-4o-mini + # /model requesty/claude-sonnet-4-6 + # Managed policy IDs ending in "@eu" route through EU providers only; the + # suffix is part of the model ID and is sent as is. + "requesty": { + "type": "openai", + "api_key_env": "REQUESTY_API_KEY", + "base_url": "https://router.requesty.ai/v1", + "context_limit": 128000, + "max_completion_tokens": 16384, + "models": [ + "claude-sonnet-4-6", + "claude-haiku-4-5", + "gpt-5.4", + "gpt-5.4-mini", + "deepseek-v4-flash", + "deepseek-v4-pro", + "gemini-3.5-flash", + "kimi-k3", + "glm-5.3", + "qwen3.8-max", + "openai/gpt-4o-mini", + "anthropic/claude-sonnet-4-6", + ], + }, "minimax": { "type": "openai", "api_key_env": "MINIMAX_API_KEY", diff --git a/docs/architecture.md b/docs/architecture.md index 5814fd9..6be6a27 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -308,6 +308,7 @@ based on the model string: "custom/my-endpoint" → custom "nim/meta/llama-3.3-70b-instruct" → nim (build.nvidia.com free tier) "openrouter/deepseek/deepseek-v4-flash" → openrouter (400+ models, one key) +"requesty/openai/gpt-4o-mini" → requesty (700+ models, one key) ``` `stream(model, system, messages, tool_schemas, config) -> Generator` @@ -315,7 +316,7 @@ is the one entry point agent.py uses. Internally it dispatches to `stream_anthropic()` (native SDK) or `stream_openai_compat()` (used by every OpenAI-compatible provider). -**Gateway model IDs are multi-level.** OpenRouter, NIM and LiteLLM +**Gateway model IDs are multi-level.** OpenRouter, Requesty, NIM and LiteLLM address models by an upstream `/` path, so only the first segment is the provider and `bare_model()` deliberately strips just that one. Two consequences the rest of the file relies on: diff --git a/docs/guides/recipes.md b/docs/guides/recipes.md index fe18f30..9a27d57 100644 --- a/docs/guides/recipes.md +++ b/docs/guides/recipes.md @@ -147,7 +147,9 @@ over both `custom/` and `litellm/openrouter/…`: the base URL and key env (`OPENROUTER_API_KEY`) are built in, the models show up in the `/model` Tab picker, and it is the only route that supports pinning the upstream provider with the `@[/]` suffix. See -[usage.md](usage.md#openrouter-400-models-one-key). +[usage.md](usage.md#openrouter-400-models-one-key). The same goes for +Requesty: use `requesty/` with `REQUESTY_API_KEY` rather than +`custom/` (see [usage.md](usage.md#requesty-700-models-one-key)). --- diff --git a/docs/guides/reference.md b/docs/guides/reference.md index 457bead..cb56ade 100644 --- a/docs/guides/reference.md +++ b/docs/guides/reference.md @@ -333,6 +333,7 @@ export ZHIPU_API_KEY=... # Zhipu GLM export DEEPSEEK_API_KEY=sk-... # DeepSeek export MINIMAX_API_KEY=... # MiniMax export OPENROUTER_API_KEY=sk-or-... # OpenRouter (400+ models, one key) +export REQUESTY_API_KEY=rqsty-... # Requesty (700+ models, one key) ``` #### `.env` file (loaded automatically) @@ -370,6 +371,7 @@ The env var always wins over any persisted value in `~/.cheetahclaws/config.json /config deepseek_api_key=sk-... /config minimax_api_key=... /config openrouter_api_key=sk-or-... +/config requesty_api_key=rqsty-... ``` Keys are saved to `~/.cheetahclaws/config.json` and loaded automatically on next launch. @@ -397,7 +399,8 @@ Keys are saved to `~/.cheetahclaws/config.json` and loaded automatically on next "kimi_api_key": "sk-...", "deepseek_api_key": "sk-...", "minimax_api_key": "...", - "openrouter_api_key": "sk-or-..." + "openrouter_api_key": "sk-or-...", + "requesty_api_key": "rqsty-..." } ``` diff --git a/docs/guides/usage.md b/docs/guides/usage.md index 8648da4..9d822a2 100644 --- a/docs/guides/usage.md +++ b/docs/guides/usage.md @@ -157,6 +157,30 @@ suffix if you would rather have OpenRouter reroute around an outage. > tokens but estimates $0 — set a **token** budget rather than a dollar one > if you rely on hard caps for exotic models. +### Requesty (700+ models, one key) + +[Requesty](https://www.requesty.ai) is an LLM gateway with 700+ models behind +one OpenAI-compatible endpoint (`https://router.requesty.ai/v1`). Get a key at +[app.requesty.ai/api-keys](https://app.requesty.ai/api-keys); docs are at +[docs.requesty.ai](https://docs.requesty.ai). + +```bash +export REQUESTY_API_KEY=rqsty-... # or: /config requesty_api_key=rqsty-... + +cheetahclaws --model requesty/openai/gpt-4o-mini +cheetahclaws --model requesty/anthropic/claude-sonnet-4-6 +cheetahclaws --model requesty/claude-sonnet-4-6 # managed policy ID +cheetahclaws --model requesty/gpt-5-mini@eu # EU providers only +``` + +As with OpenRouter, the first segment (`requesty`) is the provider and +everything after it is sent verbatim as the model ID. That can be a catalog +`/` path (the full list is at `GET /v1/models`) or a managed +policy ID with no vendor prefix (`GET /v1/models/managed`), which is a +Requesty maintained routing chain across several upstream providers for one +model. Managed policy IDs ending in `@eu` only route through EU providers. +Cost tracking uses the same per-model table as direct calls. + ### LiteLLM (AWS Bedrock / Azure / Vertex AI) Use the `litellm/` prefix when the upstream needs auth that's painful to diff --git a/docs/i18n/README.CN.MD b/docs/i18n/README.CN.MD index 0b6d148..6119838 100644 --- a/docs/i18n/README.CN.MD +++ b/docs/i18n/README.CN.MD @@ -212,10 +212,13 @@ Claude Code 是一款强大的、生产级的 AI 编码助手 —— 但它的 | **DeepSeek** | `deepseek-chat` · `deepseek-reasoner` | 64k | `DEEPSEEK_API_KEY` | | **MiniMax** | `MiniMax-Text-01` · `MiniMax-VL-01` · `abab6.5s-chat` | 256k–1M | `MINIMAX_API_KEY` | | **OpenRouter** _(400+ 模型,一把 key)_ | `openrouter/deepseek/deepseek-v4-flash` · `openrouter/anthropic/claude-sonnet-4-6` · `openrouter/openai/gpt-5` | 视情况而定 | `OPENROUTER_API_KEY` | +| **Requesty** _(700+ 模型,一把 key)_ | `requesty/openai/gpt-4o-mini` · `requesty/anthropic/claude-sonnet-4-6` · `requesty/claude-sonnet-4-6` | 视情况而定 | `REQUESTY_API_KEY` | | **AWS Bedrock / Azure / Vertex** _(通过 litellm)_ | `litellm//` | 视情况而定 | 特定于提供商 | > **`openrouter/` 网关:** 一把 key 覆盖跨厂商的 400+ 模型。模型 ID 保留 OpenRouter 上游的 `/` 路径,因此调用是双层前缀的:`openrouter/deepseek/deepseek-v4-flash`。若要指定由哪个上游提供商(及量化精度)来服务该请求,在模型后追加 `@[/]` —— `openrouter/deepseek/deepseek-v4-flash@gmicloud/fp8` —— 它会作为 OpenRouter 的 `provider` 请求体对象发送,而不是拼进模型 ID。参见 [usage.md](../guides/usage.md#openrouter-400-models-one-key)。 +> **`requesty/` 网关:** 用法相同:`requesty/` 之后的全部内容作为模型 ID 发送,可以是目录中的 `/` 路径(`requesty/openai/gpt-4o-mini`),也可以是托管策略 ID(`requesty/claude-sonnet-4-6`)。以 `@eu` 结尾的托管策略 ID(`requesty/gpt-5-mini@eu`)只经由欧盟提供商路由。参见 [usage.md](../guides/usage.md#requesty-700-models-one-key)。 + > **`litellm/` 适配器:** 在单一 SDK 后路由到 100+ 提供商 —— 主要用于认证方式较棘手的上游(Bedrock SigV4、Azure 部署路由、Vertex 服务账号 JWT)。对于普通的 OpenAI 形态端点,优先使用零依赖的 `custom/` 适配器。用 `pip install ".[litellm]"` 安装。参见 [recipes.md](../guides/recipes.md#alternative-cloud-providers-with-non-trivial-auth-via-the-litellm-provider)。 ### 开源(通过 Ollama 本地运行) diff --git a/docs/i18n/README.DE.MD b/docs/i18n/README.DE.MD index 496b9bc..6d38d46 100644 --- a/docs/i18n/README.DE.MD +++ b/docs/i18n/README.DE.MD @@ -209,10 +209,13 @@ Claude Code ist ein leistungsstarker, produktionsreifer KI-Coding-Assistent – | **DeepSeek** | `deepseek-chat` · `deepseek-reasoner` | 64k | `DEEPSEEK_API_KEY` | | **MiniMax** | `MiniMax-Text-01` · `MiniMax-VL-01` · `abab6.5s-chat` | 256k–1M | `MINIMAX_API_KEY` | | **OpenRouter** _(400+ Modelle, ein Schlüssel)_ | `openrouter/deepseek/deepseek-v4-flash` · `openrouter/anthropic/claude-sonnet-4-6` · `openrouter/openai/gpt-5` | variiert | `OPENROUTER_API_KEY` | +| **Requesty** _(700+ Modelle, ein Schlüssel)_ | `requesty/openai/gpt-4o-mini` · `requesty/anthropic/claude-sonnet-4-6` · `requesty/claude-sonnet-4-6` | variiert | `REQUESTY_API_KEY` | | **AWS Bedrock / Azure / Vertex** _(über litellm)_ | `litellm//` | variiert | anbieterspezifisch | > **`openrouter/`-Gateway:** ein Schlüssel für über 400 Modelle verschiedener Anbieter. Die Modell-ID behält den Upstream-Pfad `/` von OpenRouter bei, der Aufruf ist also doppelt präfixiert: `openrouter/deepseek/deepseek-v4-flash`. Um festzulegen, welcher Upstream-Anbieter (und welche Quantisierung) die Anfrage bedient, hänge `@[/]` an — `openrouter/deepseek/deepseek-v4-flash@gmicloud/fp8` — das als `provider`-Objekt im Request-Body gesendet wird, statt in die Modell-ID eingebaut zu werden. Siehe [usage.md](../guides/usage.md#openrouter-400-models-one-key). +> **`requesty/`-Gateway:** funktioniert genauso: alles nach `requesty/` wird als Modell-ID gesendet, entweder ein Katalogpfad `/` (`requesty/openai/gpt-4o-mini`) oder eine Managed-Policy-ID (`requesty/claude-sonnet-4-6`). Managed-Policy-IDs mit der Endung `@eu` (`requesty/gpt-5-mini@eu`) werden nur über EU-Anbieter geroutet. Siehe [usage.md](../guides/usage.md#requesty-700-models-one-key). + > **`litellm/`-Adapter:** leitet zu über 100 Anbietern hinter einem SDK weiter – hauptsächlich für Upstreams mit umständlicher Authentifizierung (Bedrock SigV4, Azure-Deployment-Routing, Vertex-Service-Account-JWTs). Für einfache OpenAI-förmige Endpunkte ist der abhängigkeitsfreie `custom/`-Adapter vorzuziehen. Installiere mit `pip install ".[litellm]"`. Siehe [recipes.md](../guides/recipes.md#alternative-cloud-providers-with-non-trivial-auth-via-the-litellm-provider). ### Open-Source (lokal über Ollama) diff --git a/docs/i18n/README.ES.MD b/docs/i18n/README.ES.MD index 8e5b4cd..a4e5cc5 100644 --- a/docs/i18n/README.ES.MD +++ b/docs/i18n/README.ES.MD @@ -209,10 +209,13 @@ Claude Code es un asistente de programación con IA potente y de nivel de produc | **DeepSeek** | `deepseek-chat` · `deepseek-reasoner` | 64k | `DEEPSEEK_API_KEY` | | **MiniMax** | `MiniMax-Text-01` · `MiniMax-VL-01` · `abab6.5s-chat` | 256k–1M | `MINIMAX_API_KEY` | | **OpenRouter** _(400+ modelos, una sola clave)_ | `openrouter/deepseek/deepseek-v4-flash` · `openrouter/anthropic/claude-sonnet-4-6` · `openrouter/openai/gpt-5` | varía | `OPENROUTER_API_KEY` | +| **Requesty** _(700+ modelos, una sola clave)_ | `requesty/openai/gpt-4o-mini` · `requesty/anthropic/claude-sonnet-4-6` · `requesty/claude-sonnet-4-6` | varía | `REQUESTY_API_KEY` | | **AWS Bedrock / Azure / Vertex** _(vía litellm)_ | `litellm//` | varía | específico del proveedor | > **Pasarela `openrouter/`:** una sola clave para más de 400 modelos de distintos proveedores. El ID del modelo conserva la ruta upstream `/` de OpenRouter, así que la llamada lleva doble prefijo: `openrouter/deepseek/deepseek-v4-flash`. Para fijar qué proveedor upstream (y qué cuantización) atiende la petición, añade `@[/]` — `openrouter/deepseek/deepseek-v4-flash@gmicloud/fp8` — que se envía como el objeto `provider` del cuerpo de la petición en lugar de pegarse al ID del modelo. Consulta [usage.md](../guides/usage.md#openrouter-400-models-one-key). +> **Pasarela `requesty/`:** funciona igual: todo lo que va después de `requesty/` se envía como ID del modelo, ya sea una ruta de catálogo `/` (`requesty/openai/gpt-4o-mini`) o un ID de política gestionada (`requesty/claude-sonnet-4-6`). Los IDs de política gestionada que terminan en `@eu` (`requesty/gpt-5-mini@eu`) solo se enrutan a través de proveedores de la UE. Consulta [usage.md](../guides/usage.md#requesty-700-models-one-key). + > **Adaptador `litellm/`:** enruta a más de 100 proveedores tras un único SDK — principalmente para upstreams con autenticación incómoda (Bedrock SigV4, enrutamiento de despliegues de Azure, JWT de cuentas de servicio de Vertex). Para endpoints con forma OpenAI simple, prefiere el adaptador `custom/` sin dependencias. Instala con `pip install ".[litellm]"`. Consulta [recipes.md](../guides/recipes.md#alternative-cloud-providers-with-non-trivial-auth-via-the-litellm-provider). ### Código abierto (local vía Ollama) diff --git a/docs/i18n/README.FR.MD b/docs/i18n/README.FR.MD index d1317fb..128ea25 100644 --- a/docs/i18n/README.FR.MD +++ b/docs/i18n/README.FR.MD @@ -209,10 +209,13 @@ Claude Code est un assistant de codage IA puissant et de qualité production — | **DeepSeek** | `deepseek-chat` · `deepseek-reasoner` | 64k | `DEEPSEEK_API_KEY` | | **MiniMax** | `MiniMax-Text-01` · `MiniMax-VL-01` · `abab6.5s-chat` | 256k–1M | `MINIMAX_API_KEY` | | **OpenRouter** _(400+ modèles, une seule clé)_ | `openrouter/deepseek/deepseek-v4-flash` · `openrouter/anthropic/claude-sonnet-4-6` · `openrouter/openai/gpt-5` | variable | `OPENROUTER_API_KEY` | +| **Requesty** _(700+ modèles, une seule clé)_ | `requesty/openai/gpt-4o-mini` · `requesty/anthropic/claude-sonnet-4-6` · `requesty/claude-sonnet-4-6` | variable | `REQUESTY_API_KEY` | | **AWS Bedrock / Azure / Vertex** _(via litellm)_ | `litellm//` | variable | spécifique au fournisseur | > **Passerelle `openrouter/` :** une seule clé pour plus de 400 modèles, tous fournisseurs confondus. L'ID du modèle conserve le chemin upstream `/` d'OpenRouter, l'appel porte donc un double préfixe : `openrouter/deepseek/deepseek-v4-flash`. Pour imposer le fournisseur upstream (et la quantification) qui traite la requête, ajoutez `@[/]` — `openrouter/deepseek/deepseek-v4-flash@gmicloud/fp8` — envoyé comme objet `provider` dans le corps de la requête plutôt que collé à l'ID du modèle. Voir [usage.md](../guides/usage.md#openrouter-400-models-one-key). +> **Passerelle `requesty/` :** même principe : tout ce qui suit `requesty/` est envoyé comme ID du modèle, soit un chemin de catalogue `/` (`requesty/openai/gpt-4o-mini`), soit un ID de politique gérée (`requesty/claude-sonnet-4-6`). Les ID de politique gérée terminés par `@eu` (`requesty/gpt-5-mini@eu`) ne passent que par des fournisseurs situés dans l'UE. Voir [usage.md](../guides/usage.md#requesty-700-models-one-key). + > **Adaptateur `litellm/` :** achemine vers 100+ fournisseurs derrière un seul SDK — principalement pour les upstreams à l'authentification délicate (SigV4 de Bedrock, routage de déploiement Azure, JWT de compte de service Vertex). Pour les points de terminaison standard au format OpenAI, préférez l'adaptateur `custom/` sans dépendance. Installez avec `pip install ".[litellm]"`. Voir [recipes.md](../guides/recipes.md#alternative-cloud-providers-with-non-trivial-auth-via-the-litellm-provider). ### Open-source (local via Ollama) diff --git a/docs/i18n/README.JP.MD b/docs/i18n/README.JP.MD index 1d6db8b..fdbe3f1 100644 --- a/docs/i18n/README.JP.MD +++ b/docs/i18n/README.JP.MD @@ -209,10 +209,13 @@ Claude Code は強力で本番グレードの AI コーディングアシスタ | **DeepSeek** | `deepseek-chat` · `deepseek-reasoner` | 64k | `DEEPSEEK_API_KEY` | | **MiniMax** | `MiniMax-Text-01` · `MiniMax-VL-01` · `abab6.5s-chat` | 256k–1M | `MINIMAX_API_KEY` | | **OpenRouter** _(400+ モデル、キー1つ)_ | `openrouter/deepseek/deepseek-v4-flash` · `openrouter/anthropic/claude-sonnet-4-6` · `openrouter/openai/gpt-5` | 様々 | `OPENROUTER_API_KEY` | +| **Requesty** _(700+ モデル、キー1つ)_ | `requesty/openai/gpt-4o-mini` · `requesty/anthropic/claude-sonnet-4-6` · `requesty/claude-sonnet-4-6` | 様々 | `REQUESTY_API_KEY` | | **AWS Bedrock / Azure / Vertex** _(litellm 経由)_ | `litellm//` | 様々 | プロバイダー固有 | > **`openrouter/` ゲートウェイ:** 1つのキーでベンダーをまたぐ400以上のモデルにアクセスできます。モデル ID は OpenRouter のアップストリーム `/` パスをそのまま保持するため、呼び出しは二重プレフィックスになります: `openrouter/deepseek/deepseek-v4-flash`。どのアップストリームプロバイダー(および量子化)がリクエストを処理するかを固定するには、`@[/]` を付加します — `openrouter/deepseek/deepseek-v4-flash@gmicloud/fp8` — これはモデル ID に連結されるのではなく、OpenRouter の `provider` リクエストボディオブジェクトとして送信されます。[usage.md](../guides/usage.md#openrouter-400-models-one-key) を参照。 +> **`requesty/` ゲートウェイ:** 使い方は同じです。`requesty/` 以降はすべてモデル ID として送信されます。カタログの `/` パス(`requesty/openai/gpt-4o-mini`)でも、マネージドポリシー ID(`requesty/claude-sonnet-4-6`)でも構いません。`@eu` で終わるマネージドポリシー ID(`requesty/gpt-5-mini@eu`)は EU のプロバイダーのみを経由します。[usage.md](../guides/usage.md#requesty-700-models-one-key) を参照してください。 + > **`litellm/` アダプター:** 1つの SDK の背後で100以上のプロバイダーにルーティングします — 主に扱いにくい認証を持つアップストリーム(Bedrock SigV4、Azure のデプロイメントルーティング、Vertex のサービスアカウント JWT)向けです。素の OpenAI 形式のエンドポイントには、依存関係ゼロの `custom/` アダプターを推奨します。`pip install ".[litellm]"` でインストールしてください。[recipes.md](../guides/recipes.md#alternative-cloud-providers-with-non-trivial-auth-via-the-litellm-provider) を参照。 ### オープンソース(Ollama 経由のローカル) diff --git a/docs/i18n/README.KO.MD b/docs/i18n/README.KO.MD index 7d045ae..de4b30b 100644 --- a/docs/i18n/README.KO.MD +++ b/docs/i18n/README.KO.MD @@ -209,10 +209,13 @@ Claude Code는 강력한 프로덕션 등급 AI 코딩 어시스턴트입니다 | **DeepSeek** | `deepseek-chat` · `deepseek-reasoner` | 64k | `DEEPSEEK_API_KEY` | | **MiniMax** | `MiniMax-Text-01` · `MiniMax-VL-01` · `abab6.5s-chat` | 256k–1M | `MINIMAX_API_KEY` | | **OpenRouter** _(400+ 모델, 키 하나)_ | `openrouter/deepseek/deepseek-v4-flash` · `openrouter/anthropic/claude-sonnet-4-6` · `openrouter/openai/gpt-5` | 다양 | `OPENROUTER_API_KEY` | +| **Requesty** _(700+ 모델, 키 하나)_ | `requesty/openai/gpt-4o-mini` · `requesty/anthropic/claude-sonnet-4-6` · `requesty/claude-sonnet-4-6` | 다양 | `REQUESTY_API_KEY` | | **AWS Bedrock / Azure / Vertex** _(litellm 경유)_ | `litellm//` | 다양 | 프로바이더별 상이 | > **`openrouter/` 게이트웨이:** 키 하나로 여러 벤더의 400개 이상 모델을 사용합니다. 모델 ID는 OpenRouter의 업스트림 `/` 경로를 그대로 유지하므로 호출은 접두사가 두 겹입니다: `openrouter/deepseek/deepseek-v4-flash`. 어떤 업스트림 프로바이더(및 양자화)가 요청을 처리할지 고정하려면 `@[/]`를 덧붙이세요 — `openrouter/deepseek/deepseek-v4-flash@gmicloud/fp8` — 이는 모델 ID에 붙는 대신 OpenRouter의 `provider` 요청 본문 객체로 전송됩니다. [usage.md](../guides/usage.md#openrouter-400-models-one-key) 참고. +> **`requesty/` 게이트웨이:** 사용 방식은 같습니다. `requesty/` 뒤의 모든 내용이 모델 ID로 전송되며, 카탈로그의 `/` 경로(`requesty/openai/gpt-4o-mini`)나 관리형 정책 ID(`requesty/claude-sonnet-4-6`)를 쓸 수 있습니다. `@eu`로 끝나는 관리형 정책 ID(`requesty/gpt-5-mini@eu`)는 EU 프로바이더만 거칩니다. [usage.md](../guides/usage.md#requesty-700-models-one-key)를 참고하세요. + > **`litellm/` 어댑터:** 하나의 SDK 뒤에서 100개 이상의 프로바이더로 라우팅합니다 — 주로 까다로운 인증이 필요한 업스트림(Bedrock SigV4, Azure 배포 라우팅, Vertex 서비스 계정 JWT)을 위한 것입니다. 일반적인 OpenAI 형태의 엔드포인트에는 의존성이 없는 `custom/` 어댑터를 선호하세요. `pip install ".[litellm]"`로 설치합니다. [recipes.md](../guides/recipes.md#alternative-cloud-providers-with-non-trivial-auth-via-the-litellm-provider) 참고. ### 오픈소스 (Ollama 경유 로컬) diff --git a/docs/i18n/README.PT.MD b/docs/i18n/README.PT.MD index 13a63bf..a6778da 100644 --- a/docs/i18n/README.PT.MD +++ b/docs/i18n/README.PT.MD @@ -209,10 +209,13 @@ O Claude Code é um assistente de IA para codificação poderoso e de nível de | **DeepSeek** | `deepseek-chat` · `deepseek-reasoner` | 64k | `DEEPSEEK_API_KEY` | | **MiniMax** | `MiniMax-Text-01` · `MiniMax-VL-01` · `abab6.5s-chat` | 256k–1M | `MINIMAX_API_KEY` | | **OpenRouter** _(400+ modelos, uma única chave)_ | `openrouter/deepseek/deepseek-v4-flash` · `openrouter/anthropic/claude-sonnet-4-6` · `openrouter/openai/gpt-5` | varia | `OPENROUTER_API_KEY` | +| **Requesty** _(700+ modelos, uma única chave)_ | `requesty/openai/gpt-4o-mini` · `requesty/anthropic/claude-sonnet-4-6` · `requesty/claude-sonnet-4-6` | varia | `REQUESTY_API_KEY` | | **AWS Bedrock / Azure / Vertex** _(via litellm)_ | `litellm//` | varia | específico do provedor | > **Gateway `openrouter/`:** uma única chave para mais de 400 modelos de vários fornecedores. O ID do modelo mantém o caminho upstream `/` do OpenRouter, então a chamada tem prefixo duplo: `openrouter/deepseek/deepseek-v4-flash`. Para fixar qual fornecedor upstream (e qual quantização) atende a requisição, acrescente `@[/]` — `openrouter/deepseek/deepseek-v4-flash@gmicloud/fp8` — que é enviado como o objeto `provider` no corpo da requisição, em vez de ser colado ao ID do modelo. Veja [usage.md](../guides/usage.md#openrouter-400-models-one-key). +> **Gateway `requesty/`:** funciona da mesma forma: tudo o que vem depois de `requesty/` é enviado como ID do modelo, seja um caminho de catálogo `/` (`requesty/openai/gpt-4o-mini`) ou um ID de política gerenciada (`requesty/claude-sonnet-4-6`). IDs de política gerenciada terminados em `@eu` (`requesty/gpt-5-mini@eu`) passam apenas por provedores da UE. Veja [usage.md](../guides/usage.md#requesty-700-models-one-key). + > **Adaptador `litellm/`:** roteia para mais de 100 provedores por trás de um único SDK — principalmente para upstreams com autenticação complicada (SigV4 do Bedrock, roteamento de deployment do Azure, JWTs de service-account do Vertex). Para endpoints simples no formato OpenAI, prefira o adaptador `custom/` sem dependências. Instale com `pip install ".[litellm]"`. Veja [recipes.md](../guides/recipes.md#alternative-cloud-providers-with-non-trivial-auth-via-the-litellm-provider). ### Open-Source (local via Ollama) diff --git a/tests/test_requesty_provider.py b/tests/test_requesty_provider.py new file mode 100644 index 0000000..9f974d9 --- /dev/null +++ b/tests/test_requesty_provider.py @@ -0,0 +1,171 @@ +"""Tests for the Requesty provider entry + multi-level model routing. + +Requesty serves 700+ models behind one OpenAI-compatible endpoint. Like +OpenRouter, the first segment of `requesty/` is the provider and the +rest is passed through verbatim: either a catalog `/` path +(`requesty/openai/gpt-4o-mini`) or a managed policy ID with no vendor prefix +(`requesty/claude-sonnet-4-6`, `requesty/gpt-5-mini@eu`). +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import pytest + +from cheetahclaws.providers import ( + PROVIDERS, AssistantTurn, TextChunk, + bare_model, calc_cost, detect_provider, stream, +) + + +# ── Provider registration ──────────────────────────────────────────────── + + +def test_requesty_provider_entry_present(): + assert "requesty" in PROVIDERS + e = PROVIDERS["requesty"] + assert e["type"] == "openai" + assert e["base_url"] == "https://router.requesty.ai/v1" + assert e["api_key_env"] == "REQUESTY_API_KEY" + assert len(e["models"]) >= 5, "expect a curated model list for the /model picker" + + +@pytest.mark.parametrize("model_id,expected_bare", [ + ("requesty/openai/gpt-4o-mini", "openai/gpt-4o-mini"), + ("requesty/anthropic/claude-sonnet-4-6", "anthropic/claude-sonnet-4-6"), + ("requesty/claude-sonnet-4-6", "claude-sonnet-4-6"), + ("requesty/gpt-5-mini@eu", "gpt-5-mini@eu"), +]) +def test_requesty_routing_strips_only_first_segment(model_id, expected_bare): + """`requesty/` must route to requesty and keep the rest verbatim, + including managed policy IDs and their `@eu` suffix.""" + assert detect_provider(model_id) == "requesty" + assert bare_model(model_id) == expected_bare + + +def _fake_stream(captured): + def fake_stream(api_key, base_url, model, system, messages, tool_schemas, config): + captured["api_key"] = api_key + captured["base_url"] = base_url + captured["model"] = model + captured["config"] = config + yield TextChunk("hi") + yield AssistantTurn("hi", [], in_tokens=1, out_tokens=1) + return fake_stream + + +@pytest.mark.parametrize("model_id,expected_model", [ + ("requesty/openai/gpt-4o-mini", "openai/gpt-4o-mini"), + ("requesty/gpt-5-mini@eu", "gpt-5-mini@eu"), +]) +def test_stream_dispatches_to_requesty_endpoint(monkeypatch, model_id, expected_model): + """`stream()` must resolve requesty/... to the Requesty base_url and pass + the model ID through unchanged, using the requesty_api_key.""" + captured = {} + monkeypatch.setattr("cheetahclaws.providers.stream_openai_compat", + _fake_stream(captured)) + + cfg = {"requesty_api_key": "rqsty-test-123"} + events = list(stream(model_id, "sys", [], [], cfg)) + + assert captured["api_key"] == "rqsty-test-123" + assert captured["base_url"] == "https://router.requesty.ai/v1" + assert captured["model"] == expected_model + assert "_openrouter_provider" not in captured["config"] + assert any(isinstance(ev, AssistantTurn) for ev in events) + + +def test_requesty_api_key_from_env(monkeypatch): + captured = {} + monkeypatch.setattr("cheetahclaws.providers.stream_openai_compat", + _fake_stream(captured)) + monkeypatch.setenv("REQUESTY_API_KEY", "rqsty-env-456") + + list(stream("requesty/openai/gpt-4o-mini", "sys", [], [], {})) + + assert captured["api_key"] == "rqsty-env-456" + + +# ── Provider identity must survive the prefix strip ────────────────────── + + +def _capture_request(monkeypatch): + """Patch openai.OpenAI and return the dict that receives create()'s kwargs.""" + captured: dict = {} + + class FakeCompletions: + def create(self, **kwargs): + captured["kwargs"] = kwargs + return [] + + class FakeChat: + completions = FakeCompletions() + + class FakeOpenAI: + def __init__(self, *args, **kwargs): + captured["client"] = kwargs + self.chat = FakeChat + + monkeypatch.setattr("openai.OpenAI", FakeOpenAI) + return captured + + +def test_requesty_deepseek_route_omits_deepseek_only_fields(monkeypatch): + """A requesty/deepseek-v4-flash route must not pick up the DeepSeek API + fields (extra_body.thinking, reasoning_effort).""" + captured = _capture_request(monkeypatch) + + list(stream( + "requesty/deepseek-v4-flash", "sys", [], [], + {"requesty_api_key": "rqsty-x", "thinking": False, + "reasoning_effort": "high"}, + )) + + kwargs = captured["kwargs"] + assert "thinking" not in (kwargs.get("extra_body") or {}) + assert "reasoning_effort" not in kwargs + assert captured["client"]["base_url"] == "https://router.requesty.ai/v1" + + +def test_requesty_uses_max_tokens_and_its_own_cap(monkeypatch): + """A route whose vendor segment is "openai" must send `max_tokens`, not + the OpenAI-only `max_completion_tokens`, capped by the requesty entry.""" + captured = _capture_request(monkeypatch) + + list(stream("requesty/openai/gpt-5", "sys", [], [], + {"requesty_api_key": "rqsty-x", "max_tokens": 64000})) + + kwargs = captured["kwargs"] + assert "max_completion_tokens" not in kwargs + assert kwargs["max_tokens"] <= PROVIDERS["requesty"]["max_completion_tokens"] + + +# ── Per-model registry lookups (cost, context window) ──────────────────── + + +@pytest.mark.parametrize("model_id", [ + "requesty/anthropic/claude-sonnet-4-6", + "requesty/claude-sonnet-4-6", + "requesty/claude-sonnet-4-6@eu", +]) +def test_requesty_usage_is_priced(model_id): + direct = calc_cost("claude-sonnet-4-6", 1_000_000, 1_000_000) + assert direct > 0 + assert calc_cost(model_id, 1_000_000, 1_000_000) == direct + + +def test_requesty_context_window_falls_back_to_vendor_provider(): + """requesty/anthropic/... with no per-model entry reads Anthropic's own + window instead of the gateway's generic default.""" + from cheetahclaws.compaction import get_context_limit + model = "requesty/anthropic/claude-sonnet-4-6" + assert (get_context_limit(model, {"model": model}) + == PROVIDERS["anthropic"]["context_limit"]) + + +def test_requesty_eu_suffix_keeps_model_family_overlay(): + from cheetahclaws.prompts.select import _family_overlay_for_model + assert _family_overlay_for_model("requesty/claude-sonnet-4-6@eu") == "claude.md"