From 25a43f87f8e7a835aef65ad4a9dec1c20f7b5ffb Mon Sep 17 00:00:00 2001 From: Thibault Jaigu Date: Thu, 24 Sep 2026 21:45:12 +0100 Subject: [PATCH] feat(providers): add Requesty as an OpenAI-compatible provider Adds a requesty registry row (https://router.requesty.ai/v1, REQUESTY_API_KEY) in hybrid catalog mode like OpenRouter. Model discovery reads Requesty's managed models endpoint and falls back to the full /models catalog. Also adds the key group in desktop settings, the VS Code extension labels, docs and tests. --- CHANGELOG.md | 7 ++++ README.md | 4 +-- docs/guide/SETUP_GUIDE.md | 3 ++ src/providers/model_discovery.py | 26 ++++++++++++++ src/providers/openai_compatible_specs.py | 36 +++++++++++++++++++ tests/providers/test_model_discovery.py | 36 +++++++++++++++++++ tests/test_opencode_compat_providers.py | 4 ++- tests/test_provider_registry.py | 2 ++ ui-desktop/src/app/settings/constants.ts | 7 ++++ .../clawcodex-vscode/src/state.js | 2 ++ 10 files changed, 124 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a3962a26..b46b726f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Requesty provider.** `requesty` is a new OpenAI-compatible registry row + (`https://router.requesty.ai/v1`, key from `REQUESTY_API_KEY`). Its model + list is discovered from Requesty's managed models endpoint, falling back to + the full `/models` catalog. + ## [1.7.0] - 2026-09-23 ### Added diff --git a/README.md b/README.md index ec850f094..0d4d3b8e7 100644 --- a/README.md +++ b/README.md @@ -406,10 +406,10 @@ providers = [ "nvidia-nim", "atlascloud", "wanjie-ark", "volcengine", "xiaomi-mimo", "novita", "fireworks", "siliconflow", "siliconflow-cn", "arcee", "moonshot", "huggingface", "together", "stepfun", "deepinfra", "meta", - "groq", "cerebras", "baseten", "xai", + "groq", "cerebras", "baseten", "xai", "requesty", # Local servers (no API key required) "ollama", "vllm", "sglang", -] # 30 providers; aliases like `nim`, `kimi`, `hf`, `grok` resolve automatically +] # 31 providers; aliases like `nim`, `kimi`, `hf`, `grok` resolve automatically ``` Any new OpenAI-compatible vendor is a one-row addition to diff --git a/docs/guide/SETUP_GUIDE.md b/docs/guide/SETUP_GUIDE.md index b0ce2e05f..08c871169 100644 --- a/docs/guide/SETUP_GUIDE.md +++ b/docs/guide/SETUP_GUIDE.md @@ -98,11 +98,14 @@ Create `~/.clawcodex/config.json` (only the providers you actually use are requi | `minimax` | `https://api.minimaxi.com/anthropic` | `MiniMax-M2.7` | | `openrouter` | `https://openrouter.ai/api/v1` | `deepseek/deepseek-v4-pro` | | `deepseek` | `https://api.deepseek.com` | `deepseek-flash` | +| `requesty` | `https://router.requesty.ai/v1` | `anthropic/claude-sonnet-4-5` (also `openai/gpt-4o-mini`) | > **DeepSeek:** `deepseek-flash` is DeepSeek-V4.1-Flash — 1M context, 384K max output, thinking on by default, and the first DeepSeek model that accepts images. `deepseek-v4-pro` is being retired: the id still works, but from 2026-09-14 its requests run V4.1 Flash and bill at the Flash price. The older spellings (`deepseek-v4-flash`, `deepseek-chat`, `deepseek-reasoner`) still resolve. > **Z.ai (GLM):** clawcodex uses Z.ai's OpenAI-compatible GLM Coding Plan at `https://api.z.ai/api/coding/paas/v4`, serving `GLM-5.1` (stable) and `GLM-5.2` (preview). The legacy provider name `glm` is still accepted as an alias for `zai`. Get a key at . +> **Requesty:** an OpenAI-compatible gateway to many vendors. Use a catalog id such as `openai/gpt-4o-mini` or a managed model id such as `claude-sonnet-5`. Put the key in the provider's `api_key`, or export `REQUESTY_API_KEY` (get one at ). For EU routing set the provider's `base_url` to `https://router.eu.requesty.ai/v1`. Docs: . + ## 4. Run ```bash diff --git a/src/providers/model_discovery.py b/src/providers/model_discovery.py index f81783a7c..8f3faf43f 100644 --- a/src/providers/model_discovery.py +++ b/src/providers/model_discovery.py @@ -129,6 +129,30 @@ def fetch_openai_compatible_models( return None +def fetch_requesty_models( + base_url: str, api_key: str | None = None, *, timeout: float = FETCH_TIMEOUT_S, +) -> list[str] | None: + """GET {base}/models/managed → chat data[].id, else the {base}/models + catalog. Requesty's managed list is its curated set of routing policies, + so it is preferred; the full catalog is the fallback. None on failure.""" + try: + payload = _http_get_json( + base_url.rstrip("/") + "/models/managed", api_key=api_key, timeout=timeout, + ) + data = payload.get("data") if isinstance(payload, dict) else None + if isinstance(data, list): + models = [ + str(item["id"]) for item in data + if isinstance(item, dict) and item.get("id") + and item.get("api", "chat") == "chat" + ] + if models: + return models + except Exception: # noqa: BLE001 + logger.debug("model discovery: requesty managed fetch failed for %s", base_url, exc_info=True) + return fetch_openai_compatible_models(base_url, api_key, timeout=timeout) + + def fetch_ollama_models( base_url: str, *, timeout: float = FETCH_TIMEOUT_S, ) -> list[str] | None: @@ -160,6 +184,8 @@ def _fetch_for_kind( return fetch_ollama_models(base_url, timeout=timeout) if kind == "openai-compatible": return fetch_openai_compatible_models(base_url, api_key, timeout=timeout) + if kind == "requesty": + return fetch_requesty_models(base_url, api_key, timeout=timeout) return None diff --git a/src/providers/openai_compatible_specs.py b/src/providers/openai_compatible_specs.py index 174ebf7be..df74e45bd 100644 --- a/src/providers/openai_compatible_specs.py +++ b/src/providers/openai_compatible_specs.py @@ -476,6 +476,42 @@ def resolved_class_name(self) -> str: env_vars=("XAI_API_KEY", "GROK_API_KEY"), aliases=("x-ai", "x_ai", "grok"), ), + # Requesty, a multi-vendor gateway like OpenRouter. Model ids are either + # catalog ids in ``vendor/model`` form (``anthropic/claude-sonnet-4-5``, + # the same default OpenRouter uses) or Requesty's managed routing + # policies (short ids such as ``claude-sonnet-5``, listed at + # ``GET /models/managed``). The rest of the curated list is seeded from + # the managed ids; discovery (the ``requesty`` catalog kind) appends the + # live managed list and falls back to the full ``/models`` catalog. EU + # routing is available at https://router.eu.requesty.ai/v1 via the + # provider's ``base_url``. + ProviderSpec( + id="requesty", + dynamic_catalog="requesty", + catalog_mode="hybrid", + label="Requesty (multi-vendor gateway)", + default_base_url="https://router.requesty.ai/v1", + default_model="anthropic/claude-sonnet-4-5", + available_models=( + "anthropic/claude-sonnet-4-5", + "claude-sonnet-5", + "claude-opus-5", + "claude-haiku-4-5", + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + "gpt-5.5", + "gpt-5.4", + "gpt-5.4-mini", + "gpt-5.3-codex", + "deepseek-v4.1-flash", + "gemini-3.5-flash", + "kimi-k3", + "glm-5.2", + "grok-4.5", + ), + env_vars=("REQUESTY_API_KEY",), + ), ) diff --git a/tests/providers/test_model_discovery.py b/tests/providers/test_model_discovery.py index 843598a46..e60979cda 100644 --- a/tests/providers/test_model_discovery.py +++ b/tests/providers/test_model_discovery.py @@ -65,6 +65,42 @@ def _boom(url, *, api_key, timeout): monkeypatch.setattr(md, "_http_get_json", _boom) assert md.fetch_openai_compatible_models("http://x/v1") is None assert md.fetch_ollama_models("http://x/v1") is None + assert md.fetch_requesty_models("http://x/v1") is None + + +def test_requesty_parser_prefers_managed_chat_models(monkeypatch: pytest.MonkeyPatch) -> None: + seen: list[str] = [] + + def _fake(url, *, api_key, timeout): + seen.append(url) + return {"data": [ + {"id": "claude-sonnet-4-5", "api": "chat"}, + {"id": "gpt-5.4-mini@eu", "api": "chat"}, + {"id": "text-embedding-3-small", "api": "embedding"}, + ]} + + monkeypatch.setattr(md, "_http_get_json", _fake) + models = md.fetch_requesty_models("https://router.requesty.ai/v1/") + assert models == ["claude-sonnet-4-5", "gpt-5.4-mini@eu"] + assert seen == ["https://router.requesty.ai/v1/models/managed"] + + +def test_requesty_parser_falls_back_to_full_catalog(monkeypatch: pytest.MonkeyPatch) -> None: + seen: list[str] = [] + + def _fake(url, *, api_key, timeout): + seen.append(url) + if url.endswith("/models/managed"): + raise OSError("unavailable") + return {"data": [{"id": "openai/gpt-4o-mini"}]} + + monkeypatch.setattr(md, "_http_get_json", _fake) + models = md.fetch_requesty_models("https://router.requesty.ai/v1") + assert models == ["openai/gpt-4o-mini"] + assert seen == [ + "https://router.requesty.ai/v1/models/managed", + "https://router.requesty.ai/v1/models", + ] # --------------------------------------------------------------------------- diff --git a/tests/test_opencode_compat_providers.py b/tests/test_opencode_compat_providers.py index 5ec53a69d..f3d92804f 100644 --- a/tests/test_opencode_compat_providers.py +++ b/tests/test_opencode_compat_providers.py @@ -28,7 +28,8 @@ # kimi-k3 was registered, so it belongs to exactly the population these # parameterized guards describe — curated list leads, discovery appends, # vendor env var sources the key. -PORTED = ("groq", "cerebras", "baseten", "xai", "moonshot") +# ``requesty`` is a hosted hybrid gateway row for the same reason. +PORTED = ("groq", "cerebras", "baseten", "xai", "moonshot", "requesty") @pytest.mark.parametrize("provider_id", PORTED) @@ -102,6 +103,7 @@ def test_the_key_is_sourced_from_the_vendor_env_var(provider_id: str) -> None: "baseten": "BASETEN_API_KEY", "xai": "XAI_API_KEY", "moonshot": "MOONSHOT_API_KEY", + "requesty": "REQUESTY_API_KEY", }[provider_id] assert expected in SPECS_BY_ID[provider_id].env_vars diff --git a/tests/test_provider_registry.py b/tests/test_provider_registry.py index a820f73e0..9e9cb5c37 100644 --- a/tests/test_provider_registry.py +++ b/tests/test_provider_registry.py @@ -53,6 +53,7 @@ "cerebras", "baseten", "xai", + "requesty", } # A sample of (id -> (base_url, default_model)) — each vendor's published @@ -73,6 +74,7 @@ "cerebras": ("https://api.cerebras.ai/v1", "gpt-oss-120b"), "baseten": ("https://inference.baseten.co/v1", "deepseek-ai/DeepSeek-V4-Pro"), "xai": ("https://api.x.ai/v1", "grok-4.5"), + "requesty": ("https://router.requesty.ai/v1", "anthropic/claude-sonnet-4-5"), } diff --git a/ui-desktop/src/app/settings/constants.ts b/ui-desktop/src/app/settings/constants.ts index c95c48e76..d4a59523c 100644 --- a/ui-desktop/src/app/settings/constants.ts +++ b/ui-desktop/src/app/settings/constants.ts @@ -220,6 +220,13 @@ export const PROVIDER_GROUPS: ProviderPrefix[] = [ description: 'Authenticate via AWS profile + region', docsUrl: 'https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-regions.html', priority: 23 + }, + { + prefix: 'REQUESTY_', + name: 'Requesty', + description: 'OpenAI-compatible gateway to many vendors', + docsUrl: 'https://app.requesty.ai/api-keys', + priority: 24 } ] diff --git a/vscode-extension/clawcodex-vscode/src/state.js b/vscode-extension/clawcodex-vscode/src/state.js index 8a16bcc67..01422f006 100644 --- a/vscode-extension/clawcodex-vscode/src/state.js +++ b/vscode-extension/clawcodex-vscode/src/state.js @@ -12,6 +12,7 @@ const PROVIDER_LABELS = { zai: 'Z.ai', minimax: 'MiniMax', openrouter: 'OpenRouter', + requesty: 'Requesty', moonshot: 'Moonshot', ollama: 'Ollama', together: 'Together AI', @@ -36,6 +37,7 @@ const PROVIDER_ENV_VARS = { zai: ['ZAI_API_KEY', 'Z_AI_API_KEY'], minimax: ['MINIMAX_API_KEY'], openrouter: ['OPENROUTER_API_KEY'], + requesty: ['REQUESTY_API_KEY'], moonshot: ['MOONSHOT_API_KEY'], together: ['TOGETHER_API_KEY'], fireworks: ['FIREWORKS_API_KEY'],