diff --git a/agent-schema.json b/agent-schema.json index b992f44d0..e38dec3d4 100644 --- a/agent-schema.json +++ b/agent-schema.json @@ -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", diff --git a/docs/_data/nav.yml b/docs/_data/nav.yml index fe2f143f7..dd5e2cbb4 100644 --- a/docs/_data/nav.yml +++ b/docs/_data/nav.yml @@ -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 diff --git a/docs/concepts/models/index.md b/docs/concepts/models/index.md index cf6a73eda..c580dd229 100644 --- a/docs/concepts/models/index.md +++ b/docs/concepts/models/index.md @@ -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` | diff --git a/docs/configuration/models/index.md b/docs/configuration/models/index.md index 5cff81760..567a889a5 100644 --- a/docs/configuration/models/index.md +++ b/docs/configuration/models/index.md @@ -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: diff --git a/docs/providers/cerebras/index.md b/docs/providers/cerebras/index.md new file mode 100644 index 000000000..27e941dff --- /dev/null +++ b/docs/providers/cerebras/index.md @@ -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 +``` diff --git a/docs/providers/overview/index.md b/docs/providers/overview/index.md index eb8e13084..ccd4fce39 100644 --- a/docs/providers/overview/index.md +++ b/docs/providers/overview/index.md @@ -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` | diff --git a/examples/README.md b/examples/README.md index 1298d3b3f..4b7d9fe28 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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. | diff --git a/examples/cerebras.yaml b/examples/cerebras.yaml new file mode 100644 index 000000000..a9e575695 --- /dev/null +++ b/examples/cerebras.yaml @@ -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 diff --git a/pkg/config/auto.go b/pkg/config/auto.go index a6179e695..e91fcfbc0 100644 --- a/pkg/config/auto.go +++ b/pkg/config/auto.go @@ -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", diff --git a/pkg/config/auto_test.go b/pkg/config/auto_test.go index 83ad2dfe2..d6be8a071 100644 --- a/pkg/config/auto_test.go +++ b/pkg/config/auto_test.go @@ -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", diff --git a/pkg/model/provider/aliases.go b/pkg/model/provider/aliases.go index ae3eb6642..f9a977cce 100644 --- a/pkg/model/provider/aliases.go +++ b/pkg/model/provider/aliases.go @@ -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", diff --git a/pkg/model/provider/aliases_test.go b/pkg/model/provider/aliases_test.go index fd8755280..abb08ab90 100644 --- a/pkg/model/provider/aliases_test.go +++ b/pkg/model/provider/aliases_test.go @@ -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() diff --git a/pkg/model/provider/cerebras_test.go b/pkg/model/provider/cerebras_test.go new file mode 100644 index 000000000..6e87ffe09 --- /dev/null +++ b/pkg/model/provider/cerebras_test.go @@ -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) +} diff --git a/pkg/model/provider/openai/client.go b/pkg/model/provider/openai/client.go index 319bce256..4bbd4990a 100644 --- a/pkg/model/provider/openai/client.go +++ b/pkg/model/provider/openai/client.go @@ -241,6 +241,7 @@ var openModelHostProviders = map[string]bool{ "ovhcloud": true, "openrouter": true, "nebius": true, + "cerebras": true, } // shouldMergeConsecutiveMessages reports whether the chat-completions request diff --git a/pkg/model/provider/openai/repro_issue3344_test.go b/pkg/model/provider/openai/repro_issue3344_test.go index 63db783d5..bda25aa77 100644 --- a/pkg/model/provider/openai/repro_issue3344_test.go +++ b/pkg/model/provider/openai/repro_issue3344_test.go @@ -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},