feat: add OrcaRouter as a built-in OpenAI-compatible provider

Adds orcarouter as a first-class built-in alias provider, mirroring the
existing openrouter entry:

- aliases.go: register orcarouter with base_url https://api.orcarouter.ai/v1
  and ORCAROUTER_API_KEY
- config/auto.go: register orcarouter in cloudProviders (auto-selection) and
  DefaultModels (orcarouter/auto adaptive router)
- openai client: treat orcarouter as an open-model host so consecutive
  system messages are merged before sending
- docs: new providers/orcarouter page, plus rows in the provider overview,
  and configuration/models provider list
- agent-schema.json: document the new alias in the provider description
- tests: cover the alias config, auto-selection, default model, and max
  tokens for orcarouter

Rebased onto current main, retaining canonical provider IDs and adding
navigation, a runnable example, and hermetic provider wire regressions.

Gate the OrcaRouter live-API test behind explicit opt-in so configured
credentials alone never trigger paid inference during normal tests.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: JinhaoSong322 <jinhao.song@myflashcloud.com>
Signed-off-by: Arnaud Héritier <aheritier@users.noreply.github.com>
This commit is contained in:
JinhaoSong322andClaude authored and Arnaud Héritier committed 2026-10-27 14:07:49 +00:00
1 parent e0c3a3c3f9
commit bb8b58db16
14 files changed
+185 -9

No files matched your search

+1 -1
View File
@@ -224,7 +224,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, opper, runcrate, api-route, atlascloud, azure, xai, ollama, mistral, baseten, daoxe, ovhcloud, groq, fireworks-ai, deepseek, cerebras, togetherai, huggingface, moonshotai, vercel, cloudflare-workers-ai, cloudflare-ai-gateway, nvidia, github-copilot, chatgpt, opencode, opencode-go, 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, opper, runcrate, api-route, orcarouter, atlascloud, azure, xai, ollama, mistral, baseten, daoxe, ovhcloud, groq, fireworks-ai, deepseek, cerebras, togetherai, huggingface, moonshotai, vercel, cloudflare-workers-ai, cloudflare-ai-gateway, nvidia, github-copilot, chatgpt, opencode, opencode-go, etc.).",
"examples": [
"openai",
"anthropic",
+3 -3
View File
@@ -18,7 +18,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, nvidia, minimax, baseten, daoxe, ovhcloud, groq, fireworks-ai, deepseek, cerebras, togetherai, huggingface, moonshotai, vercel, cloudflare-workers-ai, cloudflare-ai-gateway, requesty, openrouter, opper, runcrate, api-route, atlascloud,
# dmr, mistral, xai, nebius, nvidia, minimax, baseten, daoxe, ovhcloud, groq, fireworks-ai, deepseek, cerebras, togetherai, huggingface, moonshotai, vercel, cloudflare-workers-ai, cloudflare-ai-gateway, requesty, openrouter, opper, runcrate, api-route, orcarouter, atlascloud,
# azure, ollama, github-copilot, or a named provider defined
# under the top-level `providers:` section.
model: string # Required: model identifier
@@ -60,7 +60,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`, `nvidia`, `minimax`, `baseten`, `daoxe`, `ovhcloud`, `groq`, `fireworks-ai`, `deepseek`, `cerebras`, `togetherai`, `huggingface`, `moonshotai`, `vercel`, `cloudflare-workers-ai`, `cloudflare-ai-gateway`, `requesty`, `openrouter`, `opper`, `runcrate`, `api-route`, `atlascloud`, `azure`, `ollama`, `github-copilot`, `chatgpt`, `opencode`, `opencode-go`, or any [named provider](../../providers/custom/index.md). |
| `provider` | string | ✓/✗ | Required for regular model definitions; omitted for `first_available` selectors. Provider: `openai`, `anthropic`, `google`, `amazon-bedrock`, `dmr`, `mistral`, `xai`, `nebius`, `nvidia`, `minimax`, `baseten`, `daoxe`, `ovhcloud`, `groq`, `fireworks-ai`, `deepseek`, `cerebras`, `togetherai`, `huggingface`, `moonshotai`, `vercel`, `cloudflare-workers-ai`, `cloudflare-ai-gateway`, `requesty`, `openrouter`, `opper`, `runcrate`, `api-route`, `orcarouter`, `atlascloud`, `azure`, `ollama`, `github-copilot`, `chatgpt`, `opencode`, `opencode-go`, or any [named provider](../../providers/custom/index.md). |
| `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`) |
| `description` | string | ✗ | Informational, human-readable summary of the model's purpose or strengths (e.g., "fast and cheap, good for summaries"). Not sent to the model. Can be combined with `first_available` (a selector's description is kept when it resolves). |
| `temperature` | float | ✗ | Sampling randomness. Range is provider-dependent — typically `0.0–2.0` (Anthropic caps at `1.0`). `0.0` is deterministic. |
@@ -558,7 +558,7 @@ for a complete local-server configuration.
## Custom HTTP Headers
For OpenAI-compatible providers (`openai`, `github-copilot`, `mistral`, `xai`,
`nebius`, `nvidia`, `minimax`, `baseten`, `daoxe`, `ovhcloud`, `groq`, `fireworks-ai`, `deepseek`, `cerebras`, `togetherai`, `huggingface`, `moonshotai`, `vercel`, `cloudflare-workers-ai`, `cloudflare-ai-gateway`, `requesty`, `openrouter`, `opper`, `runcrate`, `atlascloud`, `ollama`, and any custom provider using the OpenAI API),
`nebius`, `nvidia`, `minimax`, `baseten`, `daoxe`, `ovhcloud`, `groq`, `fireworks-ai`, `deepseek`, `cerebras`, `togetherai`, `huggingface`, `moonshotai`, `vercel`, `cloudflare-workers-ai`, `cloudflare-ai-gateway`, `requesty`, `openrouter`, `opper`, `runcrate`, `orcarouter`, `atlascloud`, `ollama`, and any custom provider using the OpenAI API),
`provider_opts.http_headers` adds arbitrary HTTP headers to every outgoing
request:
+2
View File
@@ -231,6 +231,8 @@
url: /providers/opper/
- title: OpenRouter
url: /providers/openrouter/
- title: OrcaRouter
url: /providers/orcarouter/
- title: OVHcloud
url: /providers/ovhcloud/
- title: Runcrate
+88
View File
@@ -0,0 +1,88 @@
---
title: "OrcaRouter"
description: "Use OrcaRouter models with Docker Agent."
keywords: docker agent, ai agents, model providers, llm, orcarouter
weight: 231
canonical: https://docs.docker.com/ai/docker-agent/providers/orcarouter/
---
_Use OrcaRouter models with Docker Agent._
## Overview
[OrcaRouter](https://www.orcarouter.ai) provides access to models from many providers through an OpenAI-compatible API. Docker Agent includes built-in support for OrcaRouter as an alias provider.
## Setup
1. Get an API key from [OrcaRouter](https://www.orcarouter.ai)
2. Set the environment variable:
```bash
$ export ORCAROUTER_API_KEY=your-api-key
```
## Usage
### Inline Syntax
Set the provider and model in the agent definition:
```yaml
agents:
root:
model: orcarouter/orcarouter/auto
description: Assistant using OrcaRouter
instruction: You are a helpful assistant.
```
`orcarouter/auto` is OrcaRouter's adaptive router. You can also reference a model served by the gateway with its upstream provider prefix, such as `orcarouter/anthropic/claude-sonnet-4.5` or `orcarouter/deepseek/deepseek-v4-pro`. Docker Agent splits only the first slash, so the full upstream model ID is preserved.
### Named Model
For more control over parameters:
```yaml
models:
orca_router:
provider: orcarouter
model: orcarouter/auto
temperature: 0.7
max_tokens: 8192
agents:
root:
model: orca_router
description: Assistant using OrcaRouter
instruction: You are a helpful assistant.
```
## Pricing and Model Metadata
Docker Agent fetches OrcaRouter model metadata from [models.dev](https://models.dev/), including pricing per 1M input/output tokens, cache pricing when available, context limits, output limits, and modalities. This powers cost tracking and the model picker in the same way as other first-class providers.
If models.dev is unavailable, Docker Agent falls back to its embedded catalog snapshot.
## How It Works
OrcaRouter is implemented as a built-in alias in Docker Agent:
- **API Type:** OpenAI-compatible (`openai`)
- **Base URL:** `https://api.orcarouter.ai/v1`
- **Token Variable:** `ORCAROUTER_API_KEY`
## Example: Code Assistant
```yaml
agents:
coder:
model: orcarouter/orcarouter/auto
description: Code assistant using OrcaRouter
instruction: |
You are an expert programmer.
Write clean, maintainable code.
Explain trade-offs when helpful.
toolsets:
- type: filesystem
- type: shell
- type: think
```
+1
View File
@@ -69,6 +69,7 @@ Use this table to find a built-in provider's config key and authentication metho
| OpenRouter | `openrouter` | `OPENROUTER_API_KEY` |
| Opper | `opper` | `OPPER_API_KEY` |
| API Route | `api-route` | `API_ROUTE_API_KEY` |
| OrcaRouter | `orcarouter` | `ORCAROUTER_API_KEY` |
| Atlas Cloud | [`atlascloud`](../atlascloud/index.md) | `ATLASCLOUD_API_KEY` |
| Runcrate | [`runcrate`](../runcrate/index.md) | `RUNCRATE_API_KEY` |
| Azure OpenAI | `azure` | `AZURE_API_KEY` + `base_url` (override the key with `token_key`) |
+1
View File
@@ -211,6 +211,7 @@ remote MCP endpoints.
| [`baseten.yaml`](baseten.yaml) | Baseten cloud provider. |
| [`daoxe.yaml`](daoxe.yaml) | DaoXE multi-model gateway provider. |
| [`opper.yaml`](opper.yaml) | Opper multi-model gateway provider. |
| [`orcarouter.yaml`](orcarouter.yaml) | OrcaRouter multi-model gateway provider. |
| [`ovhcloud.yaml`](ovhcloud.yaml) | OVHcloud AI Endpoints provider. |
| [`runcrate.yaml`](runcrate.yaml) | Runcrate open-model host (`runcrate`). |
| [`groq.yaml`](groq.yaml) | Groq fast-inference provider. |
+17
View File
@@ -0,0 +1,17 @@
# yaml-language-server: $schema=../agent-schema.json
models:
orcarouter_model:
provider: orcarouter
model: orcarouter/auto
agents:
root:
model: orcarouter_model
description: Assistant using OrcaRouter
instruction: |
You are a helpful assistant.
toolsets:
- type: filesystem
- type: shell
- type: think
+2
View File
@@ -63,6 +63,7 @@ var cloudProviders = []providerConfig{
{"mistral", []string{"MISTRAL_API_KEY"}, "MISTRAL_API_KEY", "MISTRAL_API_KEY"},
{"openrouter", []string{"OPENROUTER_API_KEY"}, "OPENROUTER_API_KEY", "OPENROUTER_API_KEY"},
{"api-route", []string{"API_ROUTE_API_KEY"}, "API_ROUTE_API_KEY", "API_ROUTE_API_KEY"},
{"orcarouter", []string{"ORCAROUTER_API_KEY"}, "ORCAROUTER_API_KEY", "ORCAROUTER_API_KEY"},
{"atlascloud", []string{"ATLASCLOUD_API_KEY"}, "ATLASCLOUD_API_KEY", "ATLASCLOUD_API_KEY"},
{"opper", []string{"OPPER_API_KEY"}, "OPPER_API_KEY", "OPPER_API_KEY"},
{"runcrate", []string{"RUNCRATE_API_KEY"}, "RUNCRATE_API_KEY", "RUNCRATE_API_KEY"},
@@ -164,6 +165,7 @@ var DefaultModels = map[string]string{
"mistral": "mistral-small-latest",
"openrouter": "meta-llama/llama-4-maverick",
"api-route": "gpt-6.1-sol",
"orcarouter": "orcarouter/auto",
"atlascloud": "qwen/qwen3.8-max",
"opper": "claude-sonnet-4-6",
"runcrate": "deepseek-ai/DeepSeek-V4.1-Flash",
+23 -2
View File
@@ -81,6 +81,13 @@ func TestAvailableProviders_NoGateway(t *testing.T) {
},
expectedProvider: "api-route",
},
{
name: "orcarouter api key present",
envVars: map[string]string{
"ORCAROUTER_API_KEY": "test-key",
},
expectedProvider: "orcarouter",
},
{
name: "atlascloud api key present",
envVars: map[string]string{
@@ -368,6 +375,15 @@ func TestAutoModelConfig(t *testing.T) {
expectedModel: "deepseek-ai/DeepSeek-V4.1-Flash",
expectedMaxTokens: 32000,
},
{
name: "orcarouter provider",
envVars: map[string]string{
"ORCAROUTER_API_KEY": "test-key",
},
expectedProvider: "orcarouter",
expectedModel: "orcarouter/auto",
expectedMaxTokens: 32000,
},
{
name: "baseten provider",
envVars: map[string]string{
@@ -549,7 +565,7 @@ func TestDefaultModels(t *testing.T) {
t.Parallel()
// Test that DefaultModels map has all expected providers
expectedProviders := []string{"openai", "anthropic", "google", "dmr", "mistral", "openrouter", "atlascloud", "baseten", "ovhcloud", "groq", "fireworks-ai", "deepseek", "cerebras", "togetherai", "huggingface", "moonshotai", "vercel", "amazon-bedrock", "opencode", "opencode-go", "github-copilot"}
expectedProviders := []string{"openai", "anthropic", "google", "dmr", "mistral", "openrouter", "api-route", "orcarouter", "atlascloud", "baseten", "ovhcloud", "groq", "fireworks-ai", "deepseek", "cerebras", "togetherai", "huggingface", "moonshotai", "vercel", "amazon-bedrock", "opencode", "opencode-go", "github-copilot"}
for _, provider := range expectedProviders {
t.Run(provider, func(t *testing.T) {
@@ -567,6 +583,7 @@ func TestDefaultModels(t *testing.T) {
assert.Equal(t, "ai/qwen3:latest", DefaultModels["dmr"])
assert.Equal(t, "mistral-small-latest", DefaultModels["mistral"])
assert.Equal(t, "meta-llama/llama-4-maverick", DefaultModels["openrouter"])
assert.Equal(t, "orcarouter/auto", DefaultModels["orcarouter"])
assert.Equal(t, "qwen/qwen3.8-max", DefaultModels["atlascloud"])
assert.Equal(t, "deepseek-ai/DeepSeek-V4-Pro-0813", DefaultModels["baseten"])
assert.Equal(t, "Qwen3.5-397B-A17B", DefaultModels["ovhcloud"])
@@ -587,7 +604,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", "atlascloud", "baseten", "ovhcloud", "groq", "fireworks-ai", "deepseek", "cerebras", "togetherai", "huggingface", "moonshotai", "vercel", "opencode", "github-copilot"}
providers := []string{"openai", "anthropic", "google", "mistral", "openrouter", "api-route", "orcarouter", "atlascloud", "baseten", "ovhcloud", "groq", "fireworks-ai", "deepseek", "cerebras", "togetherai", "huggingface", "moonshotai", "vercel", "opencode", "github-copilot"}
for _, provider := range providers {
t.Run(provider, func(t *testing.T) {
@@ -609,6 +626,10 @@ func TestAutoModelConfig_IntegrationWithDefaultModels(t *testing.T) {
envVars["MISTRAL_API_KEY"] = "test-key"
case "openrouter":
envVars["OPENROUTER_API_KEY"] = "test-key"
case "api-route":
envVars["API_ROUTE_API_KEY"] = "test-key"
case "orcarouter":
envVars["ORCAROUTER_API_KEY"] = "test-key"
case "atlascloud":
envVars["ATLASCLOUD_API_KEY"] = "test-key"
case "baseten":
+5
View File
@@ -75,6 +75,11 @@ var Aliases = map[string]Alias{
BaseURL: "https://api.runcrate.ai/v1",
TokenEnvVar: "RUNCRATE_API_KEY",
},
"orcarouter": {
APIType: "openai",
BaseURL: "https://api.orcarouter.ai/v1",
TokenEnvVar: "ORCAROUTER_API_KEY",
},
"atlascloud": {
APIType: "openai",
BaseURL: "https://api.atlascloud.ai/v1",
+1
View File
@@ -40,6 +40,7 @@ func TestCatalogAliases(t *testing.T) {
expected := map[string]Alias{
"openrouter": {APIType: "openai", BaseURL: "https://openrouter.ai/api/v1", TokenEnvVar: "OPENROUTER_API_KEY"},
"api-route": {APIType: "openai", BaseURL: "https://global.api-route.com/v1", TokenEnvVar: "API_ROUTE_API_KEY"},
"orcarouter": {APIType: "openai", BaseURL: "https://api.orcarouter.ai/v1", TokenEnvVar: "ORCAROUTER_API_KEY"},
"atlascloud": {APIType: "openai", BaseURL: "https://api.atlascloud.ai/v1", TokenEnvVar: "ATLASCLOUD_API_KEY"},
"runcrate": {APIType: "openai", BaseURL: "https://api.runcrate.ai/v1", TokenEnvVar: "RUNCRATE_API_KEY"},
"opper": {APIType: "openai", BaseURL: "https://api.opper.ai/v3/compat", TokenEnvVar: "OPPER_API_KEY"},
+1
View File
@@ -283,6 +283,7 @@ var openModelHostProviders = map[string]bool{
"openrouter": true,
"opper": true,
"runcrate": true,
"orcarouter": true,
"nebius": true,
"nvidia": true,
"cerebras": true,
@@ -35,6 +35,16 @@ func TestChatCompletions_MergesConsecutiveSystemMessages(t *testing.T) {
})
}
func TestOrcaRouter_MergesConsecutiveSystemMessages(t *testing.T) {
t.Parallel()
assertMergesConsecutiveMessages(t, &latest.ModelConfig{
Provider: "orcarouter",
Model: "orcarouter/auto",
TokenKey: "MY_TOKEN",
})
}
func TestDaoxe_MergesConsecutiveSystemMessages(t *testing.T) {
t.Parallel()
@@ -64,6 +64,15 @@ var openAIAliasProviders = []openAIAliasProvider{
mergesSystemMessages: true,
liveOptInEnvVar: "DOCKER_AGENT_RUN_API_ROUTE_LIVE_TEST",
},
{
provider: "orcarouter",
envVar: "ORCAROUTER_API_KEY",
testKey: "test-orcarouter-key",
model: "orcarouter/auto",
greeting: "Hello from OrcaRouter",
mergesSystemMessages: true,
liveOptInEnvVar: "DOCKER_AGENT_RUN_ORCAROUTER_LIVE_TEST",
},
{
provider: "atlascloud",
envVar: "ATLASCLOUD_API_KEY",
@@ -319,7 +328,8 @@ func TestOpenAIAliasProvider_EndToEndRequest(t *testing.T) {
//
// DEEPSEEK_API_KEY=sk-... go test -run TestOpenAIAliasProvider_LiveAPI ./pkg/model/provider/
//
// API Route additionally requires DOCKER_AGENT_RUN_API_ROUTE_LIVE_TEST=1.
// API Route additionally requires DOCKER_AGENT_RUN_API_ROUTE_LIVE_TEST=1;
// OrcaRouter requires DOCKER_AGENT_RUN_ORCAROUTER_LIVE_TEST=1.
func TestOpenAIAliasProvider_LiveAPI(t *testing.T) {
for _, p := range openAIAliasProviders {
t.Run(p.provider, func(t *testing.T) {
@@ -329,17 +339,31 @@ func TestOpenAIAliasProvider_LiveAPI(t *testing.T) {
}
func TestAPIRouteLiveAPIRequiresExplicitOptIn(t *testing.T) {
assertLiveAPIRequiresExplicitOptIn(t, "api-route")
}
func TestOrcaRouterLiveAPIRequiresExplicitOptIn(t *testing.T) {
assertLiveAPIRequiresExplicitOptIn(t, "orcarouter")
}
func assertLiveAPIRequiresExplicitOptIn(t *testing.T, providerName string) {
t.Helper()
var p openAIAliasProvider
for _, candidate := range openAIAliasProviders {
if candidate.provider == "api-route" {
if candidate.provider == providerName {
p = candidate
break
}
}
require.NotEmpty(t, p.liveOptInEnvVar)
var requests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
var mu sync.Mutex
var hosts []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests.Add(1)
mu.Lock()
hosts = append(hosts, r.Host)
mu.Unlock()
w.Header().Set("Content-Type", "text/event-stream")
writeSSEChunk(w, map[string]any{
"id": "chatcmpl-test", "object": "chat.completion.chunk", "model": p.model,
@@ -364,6 +388,9 @@ func TestAPIRouteLiveAPIRequiresExplicitOptIn(t *testing.T) {
runOpenAIAliasProviderLiveAPI(t, p, server.URL)
})
require.Equal(t, int32(1), requests.Load())
mu.Lock()
defer mu.Unlock()
require.Equal(t, []string{strings.TrimPrefix(server.URL, "http://")}, hosts)
}
func runOpenAIAliasProviderLiveAPI(t *testing.T, p openAIAliasProvider, baseURL string) {