feat: add Cerebras as a supported model provider

Add cerebras as a built-in OpenAI-compatible alias provider, following the
groq/deepseek pattern. Registers the alias (base URL https://api.cerebras.ai/v1,
token CEREBRAS_API_KEY), auto-detection in cloudProviders, default model
gpt-oss-120b, schema and docs updates, an example config, and unit tests.

Unlike DeepSeek (a first-party API with a fixed model lineup), Cerebras fronts
open-weight models such as GPT-OSS and GLM whose chat templates may only accept
a single leading system message. It is therefore added to openModelHostProviders
so docker-agent coalesces its per-source system messages (agent instruction plus
each toolset's instruction) into one, as already done for baseten, ovhcloud,
openrouter, and nebius (issues #3344, #2327).

The default gpt-oss-120b is present in the embedded models.dev snapshot, so
capability detection resolves out of the box.

Also adds an end-to-end test that drives a real HTTP request through the full
stack against a local OpenAI-compatible server, plus a CEREBRAS_API_KEY-gated
live API test.

Closes #3350
This commit is contained in:
Sayt-0 committed 2026-07-01 14:01:42 +02:00
1 parent e3a240acbf
commit 5d400aad56
15 files changed
+338 -9

No files matched your search

+1 -1
View File
@@ -182,7 +182,7 @@
"properties": {
"provider": {
"type": "string",
"description": "The underlying provider type. Defaults to \"openai\" when not set. Supported values: openai, anthropic, google, amazon-bedrock, dmr, and any built-in alias (requesty, openrouter, azure, xai, ollama, mistral, baseten, ovhcloud, groq, deepseek, etc.).",
"description": "The underlying provider type. Defaults to \"openai\" when not set. Supported values: openai, anthropic, google, amazon-bedrock, dmr, and any built-in alias (requesty, openrouter, azure, xai, ollama, mistral, baseten, ovhcloud, groq, deepseek, cerebras, etc.).",
"examples": [
"openai",
"anthropic",
+2
View File
@@ -141,6 +141,8 @@
url: /providers/bedrock/
- title: Baseten
url: /providers/baseten/
- title: Cerebras
url: /providers/cerebras/
- title: DeepSeek
url: /providers/deepseek/
- title: Docker Model Runner
+1
View File
@@ -85,6 +85,7 @@ for details.
| OVHcloud | `ovhcloud` | Qwen, Llama, Mistral, DeepSeek (EU-hosted) | `OVH_AI_ENDPOINTS_ACCESS_TOKEN` |
| Groq | `groq` | Llama, Qwen, GPT-OSS (fast inference) | `GROQ_API_KEY` |
| DeepSeek | `deepseek` | DeepSeek-V3 chat and R1 reasoner | `DEEPSEEK_API_KEY` |
| Cerebras | `cerebras` | GPT-OSS, GLM (fast inference) | `CEREBRAS_API_KEY` |
| Requesty | `requesty` | Multi-provider gateway | `REQUESTY_API_KEY` |
| OpenRouter | `openrouter` | Multi-provider gateway | `OPENROUTER_API_KEY` |
| Azure OpenAI | `azure` | gpt-4o, gpt-5 on Azure | `AZURE_API_KEY` + `base_url` |
+3 -3
View File
@@ -17,7 +17,7 @@ models:
first_available: [list] # Optional: candidate model refs, tried in order by available credentials.
# Mutually exclusive with other model settings.
provider: string # Required unless using first_available. One of: openai, anthropic, google, amazon-bedrock,
# dmr, mistral, xai, nebius, minimax, baseten, ovhcloud, groq, deepseek, requesty, openrouter,
# dmr, mistral, xai, nebius, minimax, baseten, ovhcloud, groq, deepseek, cerebras, requesty, openrouter,
# azure, ollama, github-copilot, or a named provider defined
# under the top-level `providers:` section.
model: string # Required: model identifier
@@ -48,7 +48,7 @@ models:
| Property | Type | Required | Description |
| --------------------- | ---------- | -------- | ------------------------------------------------------------------------------------- |
| `first_available` | array | ✗ | Candidate model references tried in order; selects the first whose credentials are configured. Mutually exclusive with other model settings. |
| `provider` | string | ✓/✗ | Required for regular model definitions; omitted for `first_available` selectors. Provider: `openai`, `anthropic`, `google`, `amazon-bedrock`, `dmr`, `mistral`, `xai`, `nebius`, `minimax`, `baseten`, `ovhcloud`, `groq`, `deepseek`, `requesty`, `openrouter`, `azure`, `ollama`, `github-copilot`, or any [named provider]({{ '/providers/custom/' | relative_url }}). |
| `provider` | string | ✓/✗ | Required for regular model definitions; omitted for `first_available` selectors. Provider: `openai`, `anthropic`, `google`, `amazon-bedrock`, `dmr`, `mistral`, `xai`, `nebius`, `minimax`, `baseten`, `ovhcloud`, `groq`, `deepseek`, `cerebras`, `requesty`, `openrouter`, `azure`, `ollama`, `github-copilot`, or any [named provider]({{ '/providers/custom/' | relative_url }}). |
| `model` | string | ✓/✗ | Required for regular model definitions; omitted for `first_available` selectors. Model name (e.g., `gpt-4o`, `claude-sonnet-4-5`, `gemini-3.5-flash`) |
| `temperature` | float | ✗ | Sampling randomness. Range is provider-dependent — typically `0.0–2.0` (Anthropic caps at `1.0`). `0.0` is deterministic. |
| `max_tokens` | int | ✗ | Maximum response length in tokens |
@@ -404,7 +404,7 @@ See the [Anthropic provider page]({{ '/providers/anthropic/#thinking-display' |
## Custom HTTP Headers
For OpenAI-compatible providers (`openai`, `github-copilot`, `mistral`, `xai`,
`nebius`, `minimax`, `baseten`, `ovhcloud`, `groq`, `deepseek`, `requesty`, `openrouter`, `ollama`, and any custom provider using the OpenAI API),
`nebius`, `minimax`, `baseten`, `ovhcloud`, `groq`, `deepseek`, `cerebras`, `requesty`, `openrouter`, `ollama`, and any custom provider using the OpenAI API),
`provider_opts.http_headers` adds arbitrary HTTP headers to every outgoing
request:
+103
View File
@@ -0,0 +1,103 @@
---
title: "Cerebras"
description: "Use Cerebras models with docker-agent."
permalink: /providers/cerebras/
---
# Cerebras
_Use Cerebras models with docker-agent._
## Overview
[Cerebras](https://www.cerebras.ai/) serves open-weight models such as GPT-OSS
and GLM through an OpenAI-compatible API on its wafer-scale hardware, delivering
some of the highest tokens/sec available. That speed makes it a strong fit for
latency-sensitive coding workflows. docker-agent includes built-in support for
Cerebras as an alias provider.
## Setup
1. Create an API key from the [Cerebras Cloud console](https://cloud.cerebras.ai/).
2. Set the environment variable:
```bash
export CEREBRAS_API_KEY=your-api-key
```
## Usage
### Inline Syntax
The simplest way to use Cerebras:
```yaml
agents:
root:
model: cerebras/gpt-oss-120b
description: Assistant using Cerebras
instruction: You are a helpful assistant.
```
### Named Model
For more control over parameters:
```yaml
models:
cerebras_model:
provider: cerebras
model: gpt-oss-120b
temperature: 0.7
max_tokens: 8192
agents:
root:
model: cerebras_model
description: Assistant using Cerebras
instruction: You are a helpful assistant.
```
## Available Models
Cerebras hosts a curated set of open-weight models. Check the
[Cerebras models documentation](https://inference-docs.cerebras.ai/models/overview)
for current model IDs, context limits, and pricing.
| Model | Description |
| --- | --- |
| `gpt-oss-120b` | Open-weight GPT-OSS reasoning model with tool calling |
| `zai-glm-4.7` | Z.AI GLM-4.7 reasoning model with tool calling |
> Model IDs are case-sensitive and must be passed exactly as the catalogue lists
> them. Cerebras may serve additional models not in the built-in catalog; those
> still work but resolve to default capability metadata locally.
## How It Works
Cerebras is implemented as a built-in alias in docker-agent:
- **API Type:** OpenAI-compatible (`openai_chatcompletions`)
- **Base URL:** `https://api.cerebras.ai/v1`
- **Token Variable:** `CEREBRAS_API_KEY`
Because Cerebras fronts open-weight models whose chat templates may only accept
a single leading system message, docker-agent coalesces its per-source system
messages (agent instruction plus each toolset's instructions) into one before
sending the request.
## Example: Code Assistant
```yaml
agents:
coder:
model: cerebras/gpt-oss-120b
description: Fast code assistant using Cerebras
instruction: |
You are an expert programmer.
Write clean, well-documented code and follow language best practices.
toolsets:
- type: filesystem
- type: shell
- type: think
```
+1
View File
@@ -69,6 +69,7 @@ docker-agent also includes built-in aliases for these providers:
| OVHcloud | `ovhcloud` | `OVH_AI_ENDPOINTS_ACCESS_TOKEN` |
| Groq | `groq` | `GROQ_API_KEY` |
| DeepSeek | `deepseek` | `DEEPSEEK_API_KEY` |
| Cerebras | `cerebras` | `CEREBRAS_API_KEY` |
| Requesty | `requesty` | `REQUESTY_API_KEY` |
| OpenRouter | `openrouter` | `OPENROUTER_API_KEY` |
| Azure OpenAI | `azure` | `AZURE_API_KEY` + `base_url` |
+1
View File
@@ -197,6 +197,7 @@ remote MCP endpoints.
| [`ovhcloud.yaml`](ovhcloud.yaml) | OVHcloud AI Endpoints provider. |
| [`groq.yaml`](groq.yaml) | Groq fast-inference provider. |
| [`deepseek.yaml`](deepseek.yaml) | DeepSeek chat and reasoning provider. |
| [`cerebras.yaml`](cerebras.yaml) | Cerebras fast-inference provider. |
| [`grok.yaml`](grok.yaml) | xAI Grok model. |
| [`github-copilot.yaml`](github-copilot.yaml) | GitHub Copilot models via OAuth device-flow. |
| [`fallback_models.yaml`](fallback_models.yaml) | Automatic fallback to a secondary model when the primary fails. |
+17
View File
@@ -0,0 +1,17 @@
# yaml-language-server: $schema=../agent-schema.json
models:
cerebras_model:
provider: cerebras
model: gpt-oss-120b
agents:
root:
model: cerebras_model
description: Assistant using Cerebras
instruction: |
You are a helpful assistant.
toolsets:
- type: filesystem
- type: shell
- type: think
+2
View File
@@ -48,6 +48,7 @@ var cloudProviders = []providerConfig{
{"ovhcloud", []string{"OVH_AI_ENDPOINTS_ACCESS_TOKEN"}, "OVH_AI_ENDPOINTS_ACCESS_TOKEN"},
{"groq", []string{"GROQ_API_KEY"}, "GROQ_API_KEY"},
{"deepseek", []string{"DEEPSEEK_API_KEY"}, "DEEPSEEK_API_KEY"},
{"cerebras", []string{"CEREBRAS_API_KEY"}, "CEREBRAS_API_KEY"},
{"amazon-bedrock", []string{
"AWS_BEARER_TOKEN_BEDROCK",
"AWS_ACCESS_KEY_ID",
@@ -115,6 +116,7 @@ var DefaultModels = map[string]string{
"ovhcloud": "Qwen3.5-397B-A17B",
"groq": "llama-3.3-70b-versatile",
"deepseek": "deepseek-chat",
"cerebras": "gpt-oss-120b",
"amazon-bedrock": "global.anthropic.claude-sonnet-4-5-20250929-v1:0",
"opencode-go": "deepseek-v4-flash",
"opencode-zen": "deepseek-v4-flash-free",
+32 -5
View File
@@ -83,6 +83,13 @@ func TestAvailableProviders_NoGateway(t *testing.T) {
},
expectedProvider: "deepseek",
},
{
name: "cerebras api key present",
envVars: map[string]string{
"CEREBRAS_API_KEY": "test-key",
},
expectedProvider: "cerebras",
},
{
name: "no api keys - defaults to dmr",
envVars: map[string]string{},
@@ -281,6 +288,15 @@ func TestAutoModelConfig(t *testing.T) {
expectedModel: "deepseek-chat",
expectedMaxTokens: 32000,
},
{
name: "cerebras provider",
envVars: map[string]string{
"CEREBRAS_API_KEY": "test-key",
},
expectedProvider: "cerebras",
expectedModel: "gpt-oss-120b",
expectedMaxTokens: 32000,
},
{
name: "dmr provider (no api keys)",
envVars: map[string]string{},
@@ -363,7 +379,7 @@ func TestDefaultModels(t *testing.T) {
t.Parallel()
// Test that DefaultModels map has all expected providers
expectedProviders := []string{"openai", "anthropic", "google", "dmr", "mistral", "openrouter", "baseten", "ovhcloud", "groq", "deepseek", "amazon-bedrock", "opencode-zen", "opencode-go"}
expectedProviders := []string{"openai", "anthropic", "google", "dmr", "mistral", "openrouter", "baseten", "ovhcloud", "groq", "deepseek", "cerebras", "amazon-bedrock", "opencode-zen", "opencode-go"}
for _, provider := range expectedProviders {
t.Run(provider, func(t *testing.T) {
@@ -384,6 +400,7 @@ func TestDefaultModels(t *testing.T) {
assert.Equal(t, "Qwen3.5-397B-A17B", DefaultModels["ovhcloud"])
assert.Equal(t, "llama-3.3-70b-versatile", DefaultModels["groq"])
assert.Equal(t, "deepseek-chat", DefaultModels["deepseek"])
assert.Equal(t, "gpt-oss-120b", DefaultModels["cerebras"])
assert.Equal(t, "global.anthropic.claude-sonnet-4-5-20250929-v1:0", DefaultModels["amazon-bedrock"])
assert.Equal(t, "deepseek-v4-flash", DefaultModels["opencode-go"])
assert.Equal(t, "deepseek-v4-flash-free", DefaultModels["opencode-zen"])
@@ -393,7 +410,7 @@ func TestAutoModelConfig_IntegrationWithDefaultModels(t *testing.T) {
t.Parallel()
// Verify that AutoModelConfig always returns a model from DefaultModels
providers := []string{"openai", "anthropic", "google", "mistral", "openrouter", "baseten", "ovhcloud", "groq", "deepseek", "opencode-zen"}
providers := []string{"openai", "anthropic", "google", "mistral", "openrouter", "baseten", "ovhcloud", "groq", "deepseek", "cerebras", "opencode-zen"}
for _, provider := range providers {
t.Run(provider, func(t *testing.T) {
@@ -421,6 +438,8 @@ func TestAutoModelConfig_IntegrationWithDefaultModels(t *testing.T) {
envVars["GROQ_API_KEY"] = "test-key"
case "deepseek":
envVars["DEEPSEEK_API_KEY"] = "test-key"
case "cerebras":
envVars["CEREBRAS_API_KEY"] = "test-key"
case "opencode-zen":
envVars["OPENCODE_API_KEY"] = "test-key"
}
@@ -550,14 +569,22 @@ func TestAvailableProviders_PrecedenceOrder(t *testing.T) {
providers = AvailableProviders(t.Context(), "", env)
assert.Equal(t, "groq", providers[0])
// deepseek wins over amazon-bedrock
// deepseek wins over cerebras
env = environment.NewMapEnvProvider(map[string]string{
"DEEPSEEK_API_KEY": "test-key",
"AWS_ACCESS_KEY_ID": "test-key",
"DEEPSEEK_API_KEY": "test-key",
"CEREBRAS_API_KEY": "test-key",
})
providers = AvailableProviders(t.Context(), "", env)
assert.Equal(t, "deepseek", providers[0])
// cerebras wins over amazon-bedrock
env = environment.NewMapEnvProvider(map[string]string{
"CEREBRAS_API_KEY": "test-key",
"AWS_ACCESS_KEY_ID": "test-key",
})
providers = AvailableProviders(t.Context(), "", env)
assert.Equal(t, "cerebras", providers[0])
// Only OPENCODE_API_KEY set - opencode-zen should win (higher priority than opencode-go)
env = environment.NewMapEnvProvider(map[string]string{
"OPENCODE_API_KEY": "test-key",
+5
View File
@@ -88,6 +88,11 @@ var Aliases = map[string]Alias{
BaseURL: "https://api.deepseek.com/v1",
TokenEnvVar: "DEEPSEEK_API_KEY",
},
"cerebras": {
APIType: "openai",
BaseURL: "https://api.cerebras.ai/v1",
TokenEnvVar: "CEREBRAS_API_KEY",
},
"github-copilot": {
APIType: "openai",
BaseURL: "https://api.githubcopilot.com",
+14
View File
@@ -99,6 +99,20 @@ func TestDeepSeekAlias(t *testing.T) {
assert.True(t, IsCatalogProvider("deepseek"))
}
func TestCerebrasAlias(t *testing.T) {
t.Parallel()
alias, ok := LookupAlias("cerebras")
require.True(t, ok)
assert.Equal(t, Alias{
APIType: "openai",
BaseURL: "https://api.cerebras.ai/v1",
TokenEnvVar: "CEREBRAS_API_KEY",
}, alias)
assert.True(t, IsKnownProvider("cerebras"))
assert.True(t, IsCatalogProvider("cerebras"))
}
func TestEachAlias(t *testing.T) {
t.Parallel()
+154
View File
@@ -0,0 +1,154 @@
//go:build !js && !docker_agent_no_openai
package provider
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/docker/docker-agent/pkg/chat"
"github.com/docker/docker-agent/pkg/config/latest"
"github.com/docker/docker-agent/pkg/environment"
"github.com/docker/docker-agent/pkg/tools"
)
// TestCerebrasProvider_EndToEndRequest drives a real request through the full
// stack (alias resolution -> OpenAI chat-completions client -> HTTP -> SSE
// parsing) against a local server emulating Cerebras's OpenAI-compatible API.
//
// It proves the cerebras alias is wired correctly without a live key:
// - the request is authenticated with CEREBRAS_API_KEY (alias TokenEnvVar),
// - it is routed to the chat-completions endpoint (alias APIType "openai"),
// - the configured model is sent verbatim, and
// - the streamed content is reassembled correctly.
func TestCerebrasProvider_EndToEndRequest(t *testing.T) {
t.Parallel()
const apiKey = "csk-test-cerebras-key"
var (
mu sync.Mutex
receivedMethod string
receivedAuth string
receivedPath string
receivedModel string
receivedMessages string
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
receivedMethod = r.Method
receivedAuth = r.Header.Get("Authorization")
receivedPath = r.URL.Path
mu.Unlock()
var payload map[string]any
if err := json.NewDecoder(r.Body).Decode(&payload); err == nil {
mu.Lock()
if m, ok := payload["model"].(string); ok {
receivedModel = m
}
if msgs, err := json.Marshal(payload["messages"]); err == nil {
receivedMessages = string(msgs)
}
mu.Unlock()
}
w.Header().Set("Content-Type", "text/event-stream")
flusher, _ := w.(http.Flusher)
for _, delta := range []string{"Hello", " from", " Cerebras"} {
writeSSEChunk(w, map[string]any{
"id": "chatcmpl-test", "object": "chat.completion.chunk", "model": "gpt-oss-120b",
"choices": []map[string]any{{"index": 0, "delta": map[string]any{"content": delta}, "finish_reason": nil}},
})
flusher.Flush()
}
writeSSEChunk(w, map[string]any{
"id": "chatcmpl-test", "object": "chat.completion.chunk", "model": "gpt-oss-120b",
"choices": []map[string]any{{"index": 0, "delta": map[string]any{}, "finish_reason": "stop"}},
})
_, _ = w.Write([]byte("data: [DONE]\n\n"))
flusher.Flush()
}))
defer server.Close()
// BaseURL points at the mock server; TokenKey and api_type are left unset so
// they are filled in from the built-in cerebras alias, exercising the real
// resolution path.
modelCfg := &latest.ModelConfig{
Provider: "cerebras",
Model: "gpt-oss-120b",
BaseURL: server.URL,
}
env := environment.NewMapEnvProvider(map[string]string{"CEREBRAS_API_KEY": apiKey})
provider, err := fullTestRegistry().New(t.Context(), modelCfg, env)
require.NoError(t, err)
stream, err := provider.CreateChatCompletionStream(
t.Context(),
[]chat.Message{{Role: chat.MessageRoleUser, Content: "Hi"}},
[]tools.Tool{},
)
require.NoError(t, err)
defer stream.Close()
content := collectStreamContent(t, stream)
mu.Lock()
defer mu.Unlock()
assert.Equal(t, http.MethodPost, receivedMethod, "chat completions must be sent as a POST")
assert.Equal(t, "Bearer "+apiKey, receivedAuth, "auth must use the CEREBRAS_API_KEY from the alias TokenEnvVar")
assert.Equal(t, "/chat/completions", receivedPath, "cerebras alias must route to the chat-completions endpoint")
assert.Equal(t, "gpt-oss-120b", receivedModel, "the configured model must be sent verbatim")
assert.Contains(t, receivedMessages, `"role":"user"`, "the outgoing request must carry the user message role")
assert.Contains(t, receivedMessages, "Hi", "the outgoing request must carry the user message content")
assert.Equal(t, "Hello from Cerebras", content, "streamed deltas must be reassembled in order")
}
// TestCerebrasLiveAPI performs a real request against the Cerebras API. It is
// skipped unless CEREBRAS_API_KEY is set in the environment, so the default
// test run stays hermetic while allowing an on-demand real check via:
//
// CEREBRAS_API_KEY=csk-... go test -run TestCerebrasLiveAPI ./pkg/model/provider/
func TestCerebrasLiveAPI(t *testing.T) {
apiKey := os.Getenv("CEREBRAS_API_KEY")
if apiKey == "" {
t.Skip("CEREBRAS_API_KEY not set; skipping live Cerebras API test")
}
// No BaseURL/TokenKey: both come from the built-in cerebras alias, so this
// hits https://api.cerebras.ai/v1 for real.
modelCfg := &latest.ModelConfig{
Provider: "cerebras",
Model: "gpt-oss-120b",
}
provider, err := fullTestRegistry().New(t.Context(), modelCfg, environment.NewOsEnvProvider())
require.NoError(t, err)
ctx, cancel := context.WithTimeout(t.Context(), 60*time.Second)
defer cancel()
stream, err := provider.CreateChatCompletionStream(
ctx,
[]chat.Message{{Role: chat.MessageRoleUser, Content: "Reply with the single word: pong"}},
[]tools.Tool{},
)
require.NoError(t, err)
defer stream.Close()
content := collectStreamContent(t, stream)
require.NotEmpty(t, content, "live Cerebras API must return a non-empty completion")
t.Logf("Cerebras live response: %q", content)
}
+1
View File
@@ -241,6 +241,7 @@ var openModelHostProviders = map[string]bool{
"ovhcloud": true,
"openrouter": true,
"nebius": true,
"cerebras": true,
}
// shouldMergeConsecutiveMessages reports whether the chat-completions request
@@ -193,6 +193,7 @@ func TestShouldMergeConsecutiveMessages_Gating(t *testing.T) {
{"open-model host alias nebius", &latest.ModelConfig{Provider: "nebius", Model: "Qwen/Qwen3"}, true},
{"baseten", &latest.ModelConfig{Provider: "baseten", Model: "zai-org/GLM-5.2"}, true},
{"ovhcloud", &latest.ModelConfig{Provider: "ovhcloud", Model: "Qwen3.5-397B-A17B"}, true},
{"open-model host alias cerebras", &latest.ModelConfig{Provider: "cerebras", Model: "qwen-3-coder-480b"}, true},
{"explicit api_type openai_chatcompletions", &latest.ModelConfig{Provider: "custom", Model: "qwen3", ProviderOpts: map[string]any{"api_type": "openai_chatcompletions"}}, true},
// First-party APIs with fixed model lineups: unchanged (no merge).
{"first-party mistral", &latest.ModelConfig{Provider: "mistral", Model: "mistral-small"}, false},