mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 10:02:15 +00:00
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:
15 files changed
+338
-9
No files matched your search
+1
-1
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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` |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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},
|
||||
|
||||
Reference in new issue
Block a user