Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/guide/SETUP_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://z.ai/>.

> **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 <https://app.requesty.ai/api-keys>). For EU routing set the provider's `base_url` to `https://router.eu.requesty.ai/v1`. Docs: <https://docs.requesty.ai>.

## 4. Run

```bash
Expand Down
26 changes: 26 additions & 0 deletions src/providers/model_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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


Expand Down
36 changes: 36 additions & 0 deletions src/providers/openai_compatible_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",),
),
)


Expand Down
36 changes: 36 additions & 0 deletions tests/providers/test_model_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]


# ---------------------------------------------------------------------------
Expand Down
4 changes: 3 additions & 1 deletion tests/test_opencode_compat_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions tests/test_provider_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"cerebras",
"baseten",
"xai",
"requesty",
}

# A sample of (id -> (base_url, default_model)) — each vendor's published
Expand All @@ -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"),
}


Expand Down
7 changes: 7 additions & 0 deletions ui-desktop/src/app/settings/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]

Expand Down
2 changes: 2 additions & 0 deletions vscode-extension/clawcodex-vscode/src/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const PROVIDER_LABELS = {
zai: 'Z.ai',
minimax: 'MiniMax',
openrouter: 'OpenRouter',
requesty: 'Requesty',
moonshot: 'Moonshot',
ollama: 'Ollama',
together: 'Together AI',
Expand All @@ -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'],
Expand Down