Skip to content

Commit d27fef9

Browse files
lee101claude
andcommitted
Add real Cerebras provider with OpenPaths fallback
Add a direct Cerebras provider (CEREBRAS_API_KEY, https://api.cerebras.ai) instead of routing Cerebras models through OpenRouter. The cerebras/* models (gpt-oss-120b, zai-glm-4.7) auto-detect to a direct Cerebras key when present and otherwise fall back to OpenPaths (OPENPATHS_API_KEY), which also serves the Cerebras-hosted open-weight models. - model-provider-info: add Cerebras provider, base-url override (CEREBRAS_BASE_URL), prefix normalization, and env-backed auto-detection with OpenPaths fallback - models.json: rename slugs openpaths/* -> cerebras/* - README: document provider env-var auto-detection - scripts/e2e_cerebras_openpaths.sh + .env.example: e2e smoke test via OpenPaths Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 31be4c3 commit d27fef9

7 files changed

Lines changed: 288 additions & 4 deletions

File tree

.env.example

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copy to .env (gitignored) and fill in real keys for local / e2e testing.
2+
3+
# OpenAI
4+
OPENAI_API_KEY=sk-...
5+
6+
# Cerebras — fast open-weight coding models (gpt-oss-120b, zai-glm-4.7).
7+
# Direct key (https://api.cerebras.ai). When set, cerebras/* models use it directly.
8+
CEREBRAS_API_KEY=csk-...
9+
10+
# OpenPaths (https://openpaths.io) — a router that also serves the Cerebras-hosted
11+
# models, so this key alone is enough to reach cerebras/* models.
12+
OPENPATHS_API_KEY=op-...
13+
14+
# Optional endpoint overrides
15+
# CEREBRAS_BASE_URL=https://api.cerebras.ai
16+
# OPENPATHS_BASE_URL=https://openpaths.io

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,34 @@ export OPENAI_API_KEY=sk-...
5454
codex-infinity "your prompt"
5555
```
5656

57+
### Model providers (auto-detected)
58+
59+
`codex-infinity` auto-detects which provider to use from the model slug and the API keys present in your environment — no `config.toml` edits required. Export a key and select a matching model with `-m`:
60+
61+
| Provider | Env var | Example models |
62+
|----------|---------|----------------|
63+
| OpenAI | `OPENAI_API_KEY` | `gpt-5.4`, `o3` |
64+
| Cerebras | `CEREBRAS_API_KEY` | `cerebras/gpt-oss-120b`, `cerebras/zai-glm-4.7` |
65+
| OpenPaths | `OPENPATHS_API_KEY` | `openpaths/auto`, `cerebras/gpt-oss-120b`, `composer-2.5` |
66+
| OpenRouter | `OPENROUTER_API_KEY` | `anthropic/claude-opus-4.6`, `google/gemini-3.5-flash` |
67+
| Google Gemini | `GEMINI_API_KEY` | `google/gemini-3.5-flash` |
68+
| Z.AI (Zhipu) | `ZAI_API_KEY` | `glm-4.7`, `z-ai/glm-5` |
69+
| DeepSeek | `DEEPSEEK_API_KEY` | `deepseek/deepseek-v4-flash` |
70+
| Cursor | `CURSOR_API_KEY` | `cursor/composer-2.5` |
71+
| Local (OSS) | — (`--oss`) | LM Studio / Ollama models |
72+
73+
**Cerebras** runs the fast open-weight coding models (`gpt-oss-120b`, `zai-glm-4.7`). A `cerebras/*` model prefers a direct Cerebras key (`CEREBRAS_API_KEY`, `https://api.cerebras.ai`) and otherwise falls back to **OpenPaths** ([openpaths.io](https://openpaths.io)), a router that also serves the Cerebras-hosted models — so a single `OPENPATHS_API_KEY` is enough to reach them. Override the endpoints with `CEREBRAS_BASE_URL` / `OPENPATHS_BASE_URL` if needed.
74+
75+
```shell
76+
# Direct Cerebras
77+
export CEREBRAS_API_KEY=csk-...
78+
codex-infinity -m cerebras/gpt-oss-120b "refactor this module"
79+
80+
# Or via OpenPaths (also serves Cerebras models)
81+
export OPENPATHS_API_KEY=op-...
82+
codex-infinity -m cerebras/zai-glm-4.7 "explain this bug"
83+
```
84+
5785
## CLI flags
5886

5987
| Flag | Description |

codex-rs/model-provider-info/src/lib.rs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ const ZHIPU_PROVIDER_NAME: &str = "Z.AI (Zhipu)";
4646
pub const ZHIPU_PROVIDER_ID: &str = "zhipu";
4747
const DEEPSEEK_PROVIDER_NAME: &str = "DeepSeek";
4848
pub const DEEPSEEK_PROVIDER_ID: &str = "deepseek";
49+
const CEREBRAS_PROVIDER_NAME: &str = "Cerebras";
50+
pub const CEREBRAS_PROVIDER_ID: &str = "cerebras";
4951
const AMAZON_BEDROCK_PROVIDER_NAME: &str = "Amazon Bedrock";
5052
pub const AMAZON_BEDROCK_PROVIDER_ID: &str = "amazon-bedrock";
5153
pub const AMAZON_BEDROCK_DEFAULT_BASE_URL: &str =
@@ -477,6 +479,30 @@ impl ModelProviderInfo {
477479
}
478480
}
479481

482+
pub fn create_cerebras_provider() -> ModelProviderInfo {
483+
ModelProviderInfo {
484+
name: CEREBRAS_PROVIDER_NAME.into(),
485+
base_url: Some(cerebras_base_url()),
486+
env_key: Some("CEREBRAS_API_KEY".into()),
487+
env_key_instructions: Some(
488+
"Get your API key from https://cloud.cerebras.ai and set CEREBRAS_API_KEY".into(),
489+
),
490+
experimental_bearer_token: None,
491+
auth: None,
492+
wire_api: WireApi::Responses,
493+
query_params: None,
494+
http_headers: None,
495+
env_http_headers: None,
496+
request_max_retries: None,
497+
stream_max_retries: None,
498+
stream_idle_timeout_ms: None,
499+
websocket_connect_timeout_ms: None,
500+
requires_openai_auth: false,
501+
supports_websockets: false,
502+
..Default::default()
503+
}
504+
}
505+
480506
pub fn create_deepseek_provider() -> ModelProviderInfo {
481507
ModelProviderInfo {
482508
name: DEEPSEEK_PROVIDER_NAME.into(),
@@ -537,7 +563,13 @@ impl ModelProviderInfo {
537563
if self.name == GEMINI_PROVIDER_NAME {
538564
slug.strip_prefix("google/").unwrap_or(slug)
539565
} else if self.name == OPENPATHS_PROVIDER_NAME {
540-
slug.strip_prefix("openpaths/").unwrap_or(slug)
566+
// OpenPaths is a router that can also serve Cerebras-hosted models,
567+
// so accept either the `openpaths/` or `cerebras/` prefix.
568+
slug.strip_prefix("openpaths/")
569+
.or_else(|| slug.strip_prefix("cerebras/"))
570+
.unwrap_or(slug)
571+
} else if self.name == CEREBRAS_PROVIDER_NAME {
572+
slug.strip_prefix("cerebras/").unwrap_or(slug)
541573
} else if self.name == CURSOR_PROVIDER_NAME {
542574
slug.strip_prefix("cursor/").unwrap_or(slug)
543575
} else if self.name == ZHIPU_PROVIDER_NAME {
@@ -589,6 +621,7 @@ pub fn built_in_model_providers(
589621
(GEMINI_PROVIDER_ID, P::create_gemini_provider()),
590622
(ZHIPU_PROVIDER_ID, P::create_zhipu_provider()),
591623
(DEEPSEEK_PROVIDER_ID, P::create_deepseek_provider()),
624+
(CEREBRAS_PROVIDER_ID, P::create_cerebras_provider()),
592625
(AMAZON_BEDROCK_PROVIDER_ID, amazon_bedrock_provider),
593626
(
594627
OLLAMA_OSS_PROVIDER_ID,
@@ -625,6 +658,21 @@ fn openpaths_base_url() -> String {
625658
.unwrap_or_else(|| "https://openpaths.io/v1".to_string())
626659
}
627660

661+
fn cerebras_base_url() -> String {
662+
std::env::var("CEREBRAS_BASE_URL")
663+
.ok()
664+
.filter(|value| !value.trim().is_empty())
665+
.map(|value| {
666+
let trimmed = value.trim().trim_end_matches('/');
667+
if trimmed.ends_with("/v1") {
668+
trimmed.to_string()
669+
} else {
670+
format!("{trimmed}/v1")
671+
}
672+
})
673+
.unwrap_or_else(|| "https://api.cerebras.ai/v1".to_string())
674+
}
675+
628676
fn cursor_base_url() -> String {
629677
std::env::var("CURSOR_BASE_URL")
630678
.ok()
@@ -638,9 +686,7 @@ fn is_composer_model_slug(lower: &str) -> bool {
638686
.strip_prefix("openpaths/")
639687
.or_else(|| lower.strip_prefix("cursor/"))
640688
.unwrap_or(lower);
641-
slug == "composer-2.5"
642-
|| slug == "composer-2.5-fast"
643-
|| slug.starts_with("composer-2.5-")
689+
slug == "composer-2.5" || slug == "composer-2.5-fast" || slug.starts_with("composer-2.5-")
644690
}
645691

646692
pub fn infer_builtin_provider_id_for_model(model: &str) -> Option<&'static str> {
@@ -694,6 +740,14 @@ pub fn infer_builtin_provider_id_for_model(model: &str) -> Option<&'static str>
694740
Some(("openpaths", _)) if non_empty_env_var("OPENPATHS_API_KEY") => {
695741
Some(OPENPATHS_PROVIDER_ID)
696742
}
743+
// Prefer a direct Cerebras key, but fall back to OpenPaths, which also
744+
// serves the Cerebras-hosted open-weight models.
745+
Some(("cerebras", _)) if non_empty_env_var("CEREBRAS_API_KEY") => {
746+
Some(CEREBRAS_PROVIDER_ID)
747+
}
748+
Some(("cerebras", _)) if non_empty_env_var("OPENPATHS_API_KEY") => {
749+
Some(OPENPATHS_PROVIDER_ID)
750+
}
697751
Some(("cursor", _)) if non_empty_env_var("CURSOR_API_KEY") => Some(CURSOR_PROVIDER_ID),
698752
Some(("deepseek", _)) if non_empty_env_var("DEEPSEEK_API_KEY") => {
699753
Some(DEEPSEEK_PROVIDER_ID)

codex-rs/model-provider-info/src/model_provider_info_tests.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,32 @@ fn openpaths_provider_normalizes_openpaths_prefix() {
297297
);
298298
}
299299

300+
#[test]
301+
fn cerebras_provider_normalizes_cerebras_prefix() {
302+
let provider = ModelProviderInfo::create_cerebras_provider();
303+
assert_eq!(provider.env_key.as_deref(), Some("CEREBRAS_API_KEY"));
304+
assert_eq!(
305+
provider.base_url.as_deref(),
306+
Some("https://api.cerebras.ai/v1")
307+
);
308+
assert_eq!(
309+
provider.effective_model_name("cerebras/gpt-oss-120b"),
310+
"gpt-oss-120b"
311+
);
312+
assert_eq!(provider.effective_model_name("zai-glm-4.7"), "zai-glm-4.7");
313+
}
314+
315+
#[test]
316+
fn openpaths_provider_normalizes_cerebras_prefix() {
317+
// OpenPaths is a router that also serves the Cerebras-hosted models, so a
318+
// `cerebras/` slug routed to OpenPaths must drop the prefix too.
319+
let provider = ModelProviderInfo::create_openpaths_provider();
320+
assert_eq!(
321+
provider.effective_model_name("cerebras/gpt-oss-120b"),
322+
"gpt-oss-120b"
323+
);
324+
}
325+
300326
#[test]
301327
fn cursor_provider_normalizes_cursor_prefix() {
302328
let provider = ModelProviderInfo::create_cursor_provider();
@@ -315,6 +341,7 @@ fn infer_builtin_provider_prefers_env_backed_routes() {
315341
let _gemini_remove_guard = EnvVarGuard::remove("GEMINI_API_KEY");
316342
let _openrouter_remove_guard = EnvVarGuard::remove("OPENROUTER_API_KEY");
317343
let _cursor_remove_guard = EnvVarGuard::remove("CURSOR_API_KEY");
344+
let _cerebras_remove_guard = EnvVarGuard::remove("CEREBRAS_API_KEY");
318345
let openpaths_remove_guard = EnvVarGuard::remove("OPENPATHS_API_KEY");
319346
assert_eq!(
320347
infer_builtin_provider_id_for_model("google/gemini-3.5-flash"),
@@ -370,6 +397,19 @@ fn infer_builtin_provider_prefers_env_backed_routes() {
370397
infer_builtin_provider_id_for_model("composer-2.5-fast"),
371398
Some(OPENPATHS_PROVIDER_ID)
372399
);
400+
// With only an OpenPaths key, Cerebras-hosted models route through OpenPaths.
401+
assert_eq!(
402+
infer_builtin_provider_id_for_model("cerebras/gpt-oss-120b"),
403+
Some(OPENPATHS_PROVIDER_ID)
404+
);
405+
406+
// A direct Cerebras key takes precedence over the OpenPaths fallback.
407+
let cerebras_set_guard = EnvVarGuard::set("CEREBRAS_API_KEY", "csk-key");
408+
assert_eq!(
409+
infer_builtin_provider_id_for_model("cerebras/zai-glm-4.7"),
410+
Some(CEREBRAS_PROVIDER_ID)
411+
);
412+
drop(cerebras_set_guard);
373413

374414
drop(_openpaths_set_guard);
375415
let _cursor_set_guard = EnvVarGuard::set("CURSOR_API_KEY", "cursor-key");

codex-rs/models-manager/models.json

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,100 @@
588588
"supports_search_tool": true,
589589
"additional_speed_tiers": [],
590590
"supports_reasoning_summaries": true
591+
},
592+
{
593+
"slug": "cerebras/gpt-oss-120b",
594+
"display_name": "GPT-OSS 120B (Cerebras)",
595+
"description": "OpenAI gpt-oss-120b on Cerebras (~3000 tok/s open-weight coding model). Auto-routes to a direct Cerebras key (CEREBRAS_API_KEY) or falls back to OpenPaths (OPENPATHS_API_KEY).",
596+
"default_reasoning_level": "medium",
597+
"supported_reasoning_levels": [
598+
{
599+
"effort": "low",
600+
"description": "Fast responses with lighter reasoning"
601+
},
602+
{
603+
"effort": "medium",
604+
"description": "Balances speed and reasoning depth"
605+
},
606+
{
607+
"effort": "high",
608+
"description": "Greater reasoning depth for complex problems"
609+
}
610+
],
611+
"shell_type": "shell_command",
612+
"visibility": "list",
613+
"supported_in_api": true,
614+
"priority": 42,
615+
"upgrade": null,
616+
"base_instructions": "You are Codex, a coding agent. Follow the user's instructions and use available tools to complete software engineering tasks.",
617+
"supports_reasoning_summaries": false,
618+
"support_verbosity": false,
619+
"default_verbosity": null,
620+
"apply_patch_tool_type": "freeform",
621+
"web_search_tool_type": "text",
622+
"truncation_policy": {
623+
"mode": "tokens",
624+
"limit": 10000
625+
},
626+
"supports_parallel_tool_calls": true,
627+
"supports_image_detail_original": false,
628+
"context_window": 131072,
629+
"max_context_window": 131072,
630+
"auto_compact_token_limit": null,
631+
"experimental_supported_tools": [],
632+
"input_modalities": [
633+
"text"
634+
],
635+
"availability_nux": null,
636+
"additional_speed_tiers": [],
637+
"supports_search_tool": false
638+
},
639+
{
640+
"slug": "cerebras/zai-glm-4.7",
641+
"display_name": "GLM-4.7 (Cerebras)",
642+
"description": "Zhipu GLM-4.7 on Cerebras — strong reasoning and coding. Auto-routes to a direct Cerebras key (CEREBRAS_API_KEY) or falls back to OpenPaths (OPENPATHS_API_KEY).",
643+
"default_reasoning_level": "medium",
644+
"supported_reasoning_levels": [
645+
{
646+
"effort": "low",
647+
"description": "Fast responses with lighter reasoning"
648+
},
649+
{
650+
"effort": "medium",
651+
"description": "Balances speed and reasoning depth"
652+
},
653+
{
654+
"effort": "high",
655+
"description": "Greater reasoning depth for complex problems"
656+
}
657+
],
658+
"shell_type": "shell_command",
659+
"visibility": "list",
660+
"supported_in_api": true,
661+
"priority": 43,
662+
"upgrade": null,
663+
"base_instructions": "You are Codex, a coding agent. Follow the user's instructions and use available tools to complete software engineering tasks.",
664+
"supports_reasoning_summaries": false,
665+
"support_verbosity": false,
666+
"default_verbosity": null,
667+
"apply_patch_tool_type": "freeform",
668+
"web_search_tool_type": "text",
669+
"truncation_policy": {
670+
"mode": "tokens",
671+
"limit": 10000
672+
},
673+
"supports_parallel_tool_calls": true,
674+
"supports_image_detail_original": false,
675+
"context_window": 131072,
676+
"max_context_window": 131072,
677+
"auto_compact_token_limit": null,
678+
"experimental_supported_tools": [],
679+
"input_modalities": [
680+
"text"
681+
],
682+
"availability_nux": null,
683+
"additional_speed_tiers": [],
684+
"supports_search_tool": false
591685
}
592686
]
593687
}

codex-rs/models-manager/src/manager_tests.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,4 +842,10 @@ fn bundled_models_json_roundtrips() {
842842
.any(|model| model.slug == "deepseek/deepseek-v4-flash"),
843843
"bundled models.json should include a DeepSeek model"
844844
);
845+
for slug in ["cerebras/gpt-oss-120b", "cerebras/zai-glm-4.7"] {
846+
assert!(
847+
response.models.iter().any(|model| model.slug == slug),
848+
"bundled models.json should include the Cerebras model {slug}"
849+
);
850+
}
845851
}

scripts/e2e_cerebras_openpaths.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env bash
2+
# E2E smoke test: verify the Cerebras-hosted models are reachable through OpenPaths.
3+
#
4+
# Codex's `cerebras/*` models prefer a direct CEREBRAS_API_KEY and otherwise fall
5+
# back to OpenPaths (openpaths.io), which also serves the Cerebras-hosted
6+
# open-weight models. This script confirms the key works and the models respond.
7+
#
8+
# Usage:
9+
# ./scripts/e2e_cerebras_openpaths.sh
10+
# Reads OPENPATHS_API_KEY (and optional OPENPATHS_BASE_URL) from the environment
11+
# or from a gitignored .env at the repo root.
12+
set -euo pipefail
13+
14+
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
15+
if [[ -f "${repo_root}/.env" ]]; then
16+
# shellcheck disable=SC1091
17+
set -a; source "${repo_root}/.env"; set +a
18+
fi
19+
20+
: "${OPENPATHS_API_KEY:?Set OPENPATHS_API_KEY (e.g. in ${repo_root}/.env)}"
21+
base_url="${OPENPATHS_BASE_URL:-https://openpaths.io}"
22+
base_url="${base_url%/}"
23+
24+
models=("gpt-oss-120b" "zai-glm-4.7")
25+
failed=0
26+
27+
for model in "${models[@]}"; do
28+
echo "== ${model} via ${base_url} =="
29+
resp="$(curl -sS -m 60 "${base_url}/v1/chat/completions" \
30+
-H "Authorization: Bearer ${OPENPATHS_API_KEY}" \
31+
-H "Content-Type: application/json" \
32+
-d "{\"model\":\"${model}\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with the single word: pong\"}],\"stream\":false}")"
33+
content="$(printf '%s' "${resp}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["choices"][0]["message"]["content"])' 2>/dev/null || true)"
34+
if [[ -n "${content}" ]]; then
35+
echo " OK -> ${content}"
36+
else
37+
echo " FAIL -> ${resp}"
38+
failed=1
39+
fi
40+
done
41+
42+
if [[ "${failed}" -ne 0 ]]; then
43+
echo "e2e: at least one Cerebras model failed via OpenPaths" >&2
44+
exit 1
45+
fi
46+
echo "e2e: all Cerebras models reachable via OpenPaths"

0 commit comments

Comments
 (0)