diff --git a/agent-schema.json b/agent-schema.json index e38dec3d4..9c95afbbf 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, cerebras, 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, fireworks, deepseek, cerebras, etc.).", "examples": [ "openai", "anthropic", diff --git a/docs/_data/nav.yml b/docs/_data/nav.yml index dd5e2cbb4..d679302ad 100644 --- a/docs/_data/nav.yml +++ b/docs/_data/nav.yml @@ -147,6 +147,8 @@ url: /providers/deepseek/ - title: Docker Model Runner url: /providers/dmr/ + - title: Fireworks AI + url: /providers/fireworks/ - title: GitHub Copilot url: /providers/github-copilot/ - title: Google Gemini diff --git a/docs/concepts/models/index.md b/docs/concepts/models/index.md index c580dd229..092c7a358 100644 --- a/docs/concepts/models/index.md +++ b/docs/concepts/models/index.md @@ -84,6 +84,7 @@ for details. | Baseten | `baseten` | DeepSeek, Kimi, GLM, Llama models | `BASETEN_API_KEY` | | OVHcloud | `ovhcloud` | Qwen, Llama, Mistral, DeepSeek (EU-hosted) | `OVH_AI_ENDPOINTS_ACCESS_TOKEN` | | Groq | `groq` | Llama, Qwen, GPT-OSS (fast inference) | `GROQ_API_KEY` | +| Fireworks AI | `fireworks` | Kimi, Llama, Qwen, DeepSeek, GLM (open models) | `FIREWORKS_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` | diff --git a/docs/configuration/models/index.md b/docs/configuration/models/index.md index 567a889a5..1f727e11c 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, cerebras, requesty, openrouter, + # dmr, mistral, xai, nebius, minimax, baseten, ovhcloud, groq, fireworks, 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`, `cerebras`, `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`, `fireworks`, `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`, `cerebras`, `requesty`, `openrouter`, `ollama`, and any custom provider using the OpenAI API), +`nebius`, `minimax`, `baseten`, `ovhcloud`, `groq`, `fireworks`, `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/fireworks/index.md b/docs/providers/fireworks/index.md new file mode 100644 index 000000000..111a12628 --- /dev/null +++ b/docs/providers/fireworks/index.md @@ -0,0 +1,102 @@ +--- +title: "Fireworks AI" +description: "Use Fireworks AI models with docker-agent." +permalink: /providers/fireworks/ +--- + +# Fireworks AI + +_Use Fireworks AI models with docker-agent._ + +## Overview + +[Fireworks AI](https://fireworks.ai/) is a fast inference host for open-weight +models, serving Kimi K2, Llama, Qwen, DeepSeek, GLM and others through an +OpenAI-compatible API. docker-agent includes built-in support for Fireworks AI +as an alias provider. + +## Setup + +1. Create an API key from the [Fireworks dashboard](https://fireworks.ai/account/api-keys). +2. Set the environment variable: + + ```bash + export FIREWORKS_API_KEY=your-api-key + ``` + +## Usage + +### Inline Syntax + +The simplest way to use Fireworks AI: + +```yaml +agents: + root: + model: fireworks/accounts/fireworks/models/kimi-k2-instruct + description: Assistant using Fireworks AI + instruction: You are a helpful assistant. +``` + +### Named Model + +For more control over parameters: + +```yaml +models: + fireworks_model: + provider: fireworks + model: accounts/fireworks/models/kimi-k2-instruct + temperature: 0.7 + max_tokens: 8192 + +agents: + root: + model: fireworks_model + description: Assistant using Fireworks AI + instruction: You are a helpful assistant. +``` + +## Available Models + +Fireworks serves a broad, changing catalog of open-weight models. Model IDs use +the `accounts/fireworks/models/` form. Check the +[Fireworks model library](https://fireworks.ai/models) for current IDs, context +limits, and pricing. + +| Model | Description | +| --- | --- | +| `accounts/fireworks/models/kimi-k2-instruct` | Kimi K2, large open MoE chat and tool-calling model | +| `accounts/fireworks/models/llama-v3p3-70b-instruct` | Llama 3.3 70B instruct | +| `accounts/fireworks/models/qwen3-235b-a22b` | Qwen 3 235B MoE | + +> Model IDs are case-sensitive and must be passed exactly as the catalogue lists +> them. + +## How It Works + +Fireworks AI is implemented as a built-in alias in docker-agent: + +- **API Type:** OpenAI-compatible (`openai_chatcompletions`) +- **Base URL:** `https://api.fireworks.ai/inference/v1` +- **Token Variable:** `FIREWORKS_API_KEY` + +Because Fireworks fronts open-weight models whose chat templates may reject more +than one leading system message, docker-agent coalesces its per-source system +messages into a single one for this provider. + +## Example: Code Assistant + +```yaml +agents: + coder: + model: fireworks/accounts/fireworks/models/kimi-k2-instruct + description: Code assistant using Kimi K2 on Fireworks AI + 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 ccd4fce39..727f8b75e 100644 --- a/docs/providers/overview/index.md +++ b/docs/providers/overview/index.md @@ -68,6 +68,7 @@ docker-agent also includes built-in aliases for these providers: | Baseten | `baseten` | `BASETEN_API_KEY` | | OVHcloud | `ovhcloud` | `OVH_AI_ENDPOINTS_ACCESS_TOKEN` | | Groq | `groq` | `GROQ_API_KEY` | +| Fireworks AI | `fireworks` | `FIREWORKS_API_KEY` | | DeepSeek | `deepseek` | `DEEPSEEK_API_KEY` | | Cerebras | `cerebras` | `CEREBRAS_API_KEY` | | Requesty | `requesty` | `REQUESTY_API_KEY` | diff --git a/examples/README.md b/examples/README.md index 4b7d9fe28..8639da790 100644 --- a/examples/README.md +++ b/examples/README.md @@ -196,6 +196,7 @@ remote MCP endpoints. | [`baseten.yaml`](baseten.yaml) | Baseten cloud provider. | | [`ovhcloud.yaml`](ovhcloud.yaml) | OVHcloud AI Endpoints provider. | | [`groq.yaml`](groq.yaml) | Groq fast-inference provider. | +| [`fireworks.yaml`](fireworks.yaml) | Fireworks AI open-model 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. | diff --git a/examples/fireworks.yaml b/examples/fireworks.yaml new file mode 100644 index 000000000..e39868c88 --- /dev/null +++ b/examples/fireworks.yaml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=../agent-schema.json + +models: + fireworks_model: + provider: fireworks + model: accounts/fireworks/models/kimi-k2-instruct + +agents: + root: + model: fireworks_model + description: Assistant using Fireworks AI + 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 e91fcfbc0..d7eaa755a 100644 --- a/pkg/config/auto.go +++ b/pkg/config/auto.go @@ -47,6 +47,7 @@ var cloudProviders = []providerConfig{ {"baseten", []string{"BASETEN_API_KEY"}, "BASETEN_API_KEY"}, {"ovhcloud", []string{"OVH_AI_ENDPOINTS_ACCESS_TOKEN"}, "OVH_AI_ENDPOINTS_ACCESS_TOKEN"}, {"groq", []string{"GROQ_API_KEY"}, "GROQ_API_KEY"}, + {"fireworks", []string{"FIREWORKS_API_KEY"}, "FIREWORKS_API_KEY"}, {"deepseek", []string{"DEEPSEEK_API_KEY"}, "DEEPSEEK_API_KEY"}, {"cerebras", []string{"CEREBRAS_API_KEY"}, "CEREBRAS_API_KEY"}, {"amazon-bedrock", []string{ @@ -115,6 +116,7 @@ var DefaultModels = map[string]string{ "baseten": "deepseek-ai/DeepSeek-V3.1", "ovhcloud": "Qwen3.5-397B-A17B", "groq": "llama-3.3-70b-versatile", + "fireworks": "accounts/fireworks/models/kimi-k2-instruct", "deepseek": "deepseek-chat", "cerebras": "gpt-oss-120b", "amazon-bedrock": "global.anthropic.claude-sonnet-4-5-20250929-v1:0", diff --git a/pkg/config/auto_test.go b/pkg/config/auto_test.go index d6be8a071..4b7b98982 100644 --- a/pkg/config/auto_test.go +++ b/pkg/config/auto_test.go @@ -76,6 +76,13 @@ func TestAvailableProviders_NoGateway(t *testing.T) { }, expectedProvider: "groq", }, + { + name: "fireworks api key present", + envVars: map[string]string{ + "FIREWORKS_API_KEY": "test-key", + }, + expectedProvider: "fireworks", + }, { name: "deepseek api key present", envVars: map[string]string{ @@ -279,6 +286,15 @@ func TestAutoModelConfig(t *testing.T) { expectedModel: "llama-3.3-70b-versatile", expectedMaxTokens: 32000, }, + { + name: "fireworks provider", + envVars: map[string]string{ + "FIREWORKS_API_KEY": "test-key", + }, + expectedProvider: "fireworks", + expectedModel: "accounts/fireworks/models/kimi-k2-instruct", + expectedMaxTokens: 32000, + }, { name: "deepseek provider", envVars: map[string]string{ @@ -379,7 +395,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", "cerebras", "amazon-bedrock", "opencode-zen", "opencode-go"} + expectedProviders := []string{"openai", "anthropic", "google", "dmr", "mistral", "openrouter", "baseten", "ovhcloud", "groq", "fireworks", "deepseek", "cerebras", "amazon-bedrock", "opencode-zen", "opencode-go"} for _, provider := range expectedProviders { t.Run(provider, func(t *testing.T) { @@ -399,6 +415,7 @@ func TestDefaultModels(t *testing.T) { assert.Equal(t, "deepseek-ai/DeepSeek-V3.1", DefaultModels["baseten"]) assert.Equal(t, "Qwen3.5-397B-A17B", DefaultModels["ovhcloud"]) assert.Equal(t, "llama-3.3-70b-versatile", DefaultModels["groq"]) + assert.Equal(t, "accounts/fireworks/models/kimi-k2-instruct", DefaultModels["fireworks"]) 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"]) @@ -410,7 +427,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", "cerebras", "opencode-zen"} + providers := []string{"openai", "anthropic", "google", "mistral", "openrouter", "baseten", "ovhcloud", "groq", "fireworks", "deepseek", "cerebras", "opencode-zen"} for _, provider := range providers { t.Run(provider, func(t *testing.T) { @@ -436,6 +453,8 @@ func TestAutoModelConfig_IntegrationWithDefaultModels(t *testing.T) { envVars["OVH_AI_ENDPOINTS_ACCESS_TOKEN"] = "test-token" case "groq": envVars["GROQ_API_KEY"] = "test-key" + case "fireworks": + envVars["FIREWORKS_API_KEY"] = "test-key" case "deepseek": envVars["DEEPSEEK_API_KEY"] = "test-key" case "cerebras": @@ -561,14 +580,22 @@ func TestAvailableProviders_PrecedenceOrder(t *testing.T) { providers = AvailableProviders(t.Context(), "", env) assert.Equal(t, "ovhcloud", providers[0]) - // groq wins over deepseek + // groq wins over fireworks env = environment.NewMapEnvProvider(map[string]string{ - "GROQ_API_KEY": "test-key", - "DEEPSEEK_API_KEY": "test-key", + "GROQ_API_KEY": "test-key", + "FIREWORKS_API_KEY": "test-key", }) providers = AvailableProviders(t.Context(), "", env) assert.Equal(t, "groq", providers[0]) + // fireworks wins over deepseek + env = environment.NewMapEnvProvider(map[string]string{ + "FIREWORKS_API_KEY": "test-key", + "DEEPSEEK_API_KEY": "test-key", + }) + providers = AvailableProviders(t.Context(), "", env) + assert.Equal(t, "fireworks", providers[0]) + // deepseek wins over cerebras env = environment.NewMapEnvProvider(map[string]string{ "DEEPSEEK_API_KEY": "test-key", diff --git a/pkg/config/examples_test.go b/pkg/config/examples_test.go index 223f80d87..6fd38a647 100644 --- a/pkg/config/examples_test.go +++ b/pkg/config/examples_test.go @@ -22,6 +22,7 @@ var modelsDevAbsentProviders = map[string]bool{ "dmr": true, // Docker Model Runner (local, not in catalog) "opencode-zen": true, // not yet registered in models.dev "ovhcloud": true, // OVHcloud AI Endpoints (not yet in models.dev) + "fireworks": true, // models.dev catalogs Fireworks under the "fireworks-ai" id, not "fireworks" } func collectExamples(t *testing.T) []string { diff --git a/pkg/model/provider/aliases.go b/pkg/model/provider/aliases.go index f9a977cce..009319a96 100644 --- a/pkg/model/provider/aliases.go +++ b/pkg/model/provider/aliases.go @@ -83,6 +83,11 @@ var Aliases = map[string]Alias{ BaseURL: "https://api.groq.com/openai/v1", TokenEnvVar: "GROQ_API_KEY", }, + "fireworks": { + APIType: "openai", + BaseURL: "https://api.fireworks.ai/inference/v1", + TokenEnvVar: "FIREWORKS_API_KEY", + }, "deepseek": { APIType: "openai", BaseURL: "https://api.deepseek.com/v1", diff --git a/pkg/model/provider/aliases_test.go b/pkg/model/provider/aliases_test.go index abb08ab90..7c23c1328 100644 --- a/pkg/model/provider/aliases_test.go +++ b/pkg/model/provider/aliases_test.go @@ -85,6 +85,20 @@ func TestGroqAlias(t *testing.T) { assert.True(t, IsCatalogProvider("groq")) } +func TestFireworksAlias(t *testing.T) { + t.Parallel() + + alias, ok := LookupAlias("fireworks") + require.True(t, ok) + assert.Equal(t, Alias{ + APIType: "openai", + BaseURL: "https://api.fireworks.ai/inference/v1", + TokenEnvVar: "FIREWORKS_API_KEY", + }, alias) + assert.True(t, IsKnownProvider("fireworks")) + assert.True(t, IsCatalogProvider("fireworks")) +} + func TestDeepSeekAlias(t *testing.T) { t.Parallel() diff --git a/pkg/model/provider/fireworks_test.go b/pkg/model/provider/fireworks_test.go new file mode 100644 index 000000000..00f4b8a3c --- /dev/null +++ b/pkg/model/provider/fireworks_test.go @@ -0,0 +1,177 @@ +//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" +) + +// TestFireworksProvider_EndToEndRequest drives a real request through the full +// stack (alias resolution -> OpenAI chat-completions client -> HTTP -> SSE +// parsing) against a local server emulating Fireworks AI's OpenAI-compatible +// API. +// +// It proves the fireworks alias is wired correctly without a live key: +// - the request is authenticated with FIREWORKS_API_KEY (alias TokenEnvVar), +// - it is routed to the chat-completions endpoint (alias APIType "openai"), +// - the configured model is sent verbatim, +// - the streamed content is reassembled correctly, and +// - because Fireworks fronts open-weight models with strict chat templates, +// the per-source system messages are coalesced into a single leading one +// (open-model-host merge, issue #3344). +func TestFireworksProvider_EndToEndRequest(t *testing.T) { + t.Parallel() + + const apiKey = "fw-test-fireworks-key" + + var ( + mu sync.Mutex + receivedMethod string + receivedAuth string + receivedPath string + receivedModel string + receivedMessages string + systemCount int + ) + + 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 struct { + Model string `json:"model"` + Messages []struct { + Role string `json:"role"` + Content json.RawMessage `json:"content"` + } `json:"messages"` + } + if err := json.NewDecoder(r.Body).Decode(&payload); err == nil { + count := 0 + for _, m := range payload.Messages { + if m.Role == "system" { + count++ + } + } + msgs, _ := json.Marshal(payload.Messages) + mu.Lock() + receivedModel = payload.Model + receivedMessages = string(msgs) + systemCount = count + mu.Unlock() + } + + w.Header().Set("Content-Type", "text/event-stream") + flusher, _ := w.(http.Flusher) + + for _, delta := range []string{"Hello", " from", " Fireworks"} { + writeSSEChunk(w, map[string]any{ + "id": "chatcmpl-test", "object": "chat.completion.chunk", "model": "accounts/fireworks/models/kimi-k2-instruct", + "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": "accounts/fireworks/models/kimi-k2-instruct", + "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 fireworks alias, exercising the real + // resolution path. + modelCfg := &latest.ModelConfig{ + Provider: "fireworks", + Model: "accounts/fireworks/models/kimi-k2-instruct", + BaseURL: server.URL, + } + env := environment.NewMapEnvProvider(map[string]string{"FIREWORKS_API_KEY": apiKey}) + + provider, err := fullTestRegistry().New(t.Context(), modelCfg, env) + require.NoError(t, err) + + // Two system messages (agent instruction + toolset instruction) plus a user + // turn: exactly the shape docker-agent builds for an agent with a toolset. + stream, err := provider.CreateChatCompletionStream( + t.Context(), + []chat.Message{ + {Role: chat.MessageRoleSystem, Content: "AGENT-INSTRUCTION: you are helpful."}, + {Role: chat.MessageRoleSystem, Content: "TOOLSET-INSTRUCTION: use tools wisely."}, + {Role: chat.MessageRoleUser, Content: "PING-MARKER"}, + }, + []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 FIREWORKS_API_KEY from the alias TokenEnvVar") + assert.Equal(t, "/chat/completions", receivedPath, "fireworks alias must route to the chat-completions endpoint") + assert.Equal(t, "accounts/fireworks/models/kimi-k2-instruct", receivedModel, "the configured model must be sent verbatim") + assert.Equal(t, 1, systemCount, "fireworks is an open-model host: consecutive system messages must be coalesced into one (issue #3344)") + assert.Contains(t, receivedMessages, "AGENT-INSTRUCTION", "the coalesced system message must retain the agent instruction") + assert.Contains(t, receivedMessages, "TOOLSET-INSTRUCTION", "the coalesced system message must retain the toolset instruction") + assert.Contains(t, receivedMessages, "PING-MARKER", "the outgoing request must carry the user message content") + assert.Equal(t, "Hello from Fireworks", content, "streamed deltas must be reassembled in order") +} + +// TestFireworksLiveAPI performs a real request against the Fireworks AI API. It +// is skipped unless FIREWORKS_API_KEY is set in the environment, so the default +// test run stays hermetic while allowing an on-demand real check via: +// +// FIREWORKS_API_KEY=fw-... go test -run TestFireworksLiveAPI ./pkg/model/provider/ +func TestFireworksLiveAPI(t *testing.T) { + apiKey := os.Getenv("FIREWORKS_API_KEY") + if apiKey == "" { + t.Skip("FIREWORKS_API_KEY not set; skipping live Fireworks AI API test") + } + + // No BaseURL/TokenKey: both come from the built-in fireworks alias, so this + // hits https://api.fireworks.ai/inference/v1 for real. + modelCfg := &latest.ModelConfig{ + Provider: "fireworks", + Model: "accounts/fireworks/models/kimi-k2-instruct", + } + + 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 Fireworks AI API must return a non-empty completion") + t.Logf("Fireworks live response: %q", content) +} diff --git a/pkg/model/provider/openai/client.go b/pkg/model/provider/openai/client.go index 4bbd4990a..b3d2de263 100644 --- a/pkg/model/provider/openai/client.go +++ b/pkg/model/provider/openai/client.go @@ -242,6 +242,7 @@ var openModelHostProviders = map[string]bool{ "openrouter": true, "nebius": true, "cerebras": true, + "fireworks": 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 bda25aa77..6aee1c199 100644 --- a/pkg/model/provider/openai/repro_issue3344_test.go +++ b/pkg/model/provider/openai/repro_issue3344_test.go @@ -194,6 +194,7 @@ func TestShouldMergeConsecutiveMessages_Gating(t *testing.T) { {"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}, + {"open-model host fireworks", &latest.ModelConfig{Provider: "fireworks", Model: "accounts/fireworks/models/kimi-k2-instruct"}, 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},