From d30e380f6836b1bdb72f9da5b400e1d497864935 Mon Sep 17 00:00:00 2001 From: JinhaoSong322 Date: Thu, 20 Aug 2026 09:10:10 +0000 Subject: [PATCH] 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, concepts/models table, 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 Co-Authored-By: Claude Signed-off-by: JinhaoSong322 --- agent-schema.json | 2 +- docs/concepts/models/index.md | 1 + docs/configuration/models/index.md | 6 +- docs/providers/orcarouter/index.md | 88 +++++++++++++++++++++++++++++ docs/providers/overview/index.md | 1 + pkg/config/auto.go | 2 + pkg/config/auto_test.go | 27 ++++++++- pkg/model/provider/aliases.go | 5 ++ pkg/model/provider/aliases_test.go | 1 + pkg/model/provider/openai/client.go | 1 + 10 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 docs/providers/orcarouter/index.md diff --git a/agent-schema.json b/agent-schema.json index 274e2d27e5..8d8e145f68 100644 --- a/agent-schema.json +++ b/agent-schema.json @@ -212,7 +212,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, fireworks, deepseek, cerebras, together, huggingface, moonshot, vercel, cloudflare-workers-ai, cloudflare-ai-gateway, nvidia, github-copilot, chatgpt, 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, orcarouter, azure, xai, ollama, mistral, baseten, ovhcloud, groq, fireworks, deepseek, cerebras, together, huggingface, moonshot, vercel, cloudflare-workers-ai, cloudflare-ai-gateway, nvidia, github-copilot, chatgpt, etc.).", "examples": [ "openai", "anthropic", diff --git a/docs/concepts/models/index.md b/docs/concepts/models/index.md index d966769d45..6e227ab99f 100644 --- a/docs/concepts/models/index.md +++ b/docs/concepts/models/index.md @@ -96,6 +96,7 @@ for details. | Cloudflare AI Gateway | `cloudflare-ai-gateway` | Multi-provider gateway | `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` + `CLOUDFLARE_GATEWAY_ID` | | Requesty | `requesty` | Multi-provider gateway | `REQUESTY_API_KEY` | | OpenRouter | `openrouter` | Multi-provider gateway | `OPENROUTER_API_KEY` | +| OrcaRouter | `orcarouter` | Multi-provider gateway | `ORCAROUTER_API_KEY` | | Azure OpenAI | `azure` | gpt-4o, gpt-5 on Azure | `AZURE_API_KEY` + `base_url` | | [Ollama](../../providers/local/index.md) | `ollama` | Any local Ollama model | None (local; optional `base_url`) | | GitHub Copilot | `github-copilot` | Copilot-hosted OpenAI/Anthropic | `GITHUB_TOKEN` (PAT with `copilot`) | diff --git a/docs/configuration/models/index.md b/docs/configuration/models/index.md index 0626bd48ef..3ad2ed5f5c 100644 --- a/docs/configuration/models/index.md +++ b/docs/configuration/models/index.md @@ -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, ovhcloud, groq, fireworks, deepseek, cerebras, together, huggingface, moonshot, vercel, cloudflare-workers-ai, cloudflare-ai-gateway, requesty, openrouter, + # dmr, mistral, xai, nebius, nvidia, minimax, baseten, ovhcloud, groq, fireworks, deepseek, cerebras, together, huggingface, moonshot, vercel, cloudflare-workers-ai, cloudflare-ai-gateway, requesty, openrouter, orcarouter, # azure, ollama, github-copilot, or a named provider defined # under the top-level `providers:` section. model: string # Required: model identifier @@ -56,7 +56,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`, `ovhcloud`, `groq`, `fireworks`, `deepseek`, `cerebras`, `together`, `huggingface`, `moonshot`, `vercel`, `cloudflare-workers-ai`, `cloudflare-ai-gateway`, `requesty`, `openrouter`, `azure`, `ollama`, `github-copilot`, `chatgpt`, 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`, `ovhcloud`, `groq`, `fireworks`, `deepseek`, `cerebras`, `together`, `huggingface`, `moonshot`, `vercel`, `cloudflare-workers-ai`, `cloudflare-ai-gateway`, `requesty`, `openrouter`, `orcarouter`, `azure`, `ollama`, `github-copilot`, `chatgpt`, 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. | @@ -508,7 +508,7 @@ See the [Anthropic provider page](../../providers/anthropic/index.md#thinking-di ## Custom HTTP Headers For OpenAI-compatible providers (`openai`, `github-copilot`, `mistral`, `xai`, -`nebius`, `nvidia`, `minimax`, `baseten`, `ovhcloud`, `groq`, `fireworks`, `deepseek`, `cerebras`, `together`, `huggingface`, `moonshot`, `vercel`, `cloudflare-workers-ai`, `cloudflare-ai-gateway`, `requesty`, `openrouter`, `ollama`, and any custom provider using the OpenAI API), +`nebius`, `nvidia`, `minimax`, `baseten`, `ovhcloud`, `groq`, `fireworks`, `deepseek`, `cerebras`, `together`, `huggingface`, `moonshot`, `vercel`, `cloudflare-workers-ai`, `cloudflare-ai-gateway`, `requesty`, `openrouter`, `orcarouter`, `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/orcarouter/index.md b/docs/providers/orcarouter/index.md new file mode 100644 index 0000000000..ce60f810c3 --- /dev/null +++ b/docs/providers/orcarouter/index.md @@ -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. It also runs gateway-level, zero-trust security for AI agents on the same endpoint — screening every prompt/response and governing every tool call on a default-deny basis, with no application code changes. + +## 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 + +The simplest way to use OrcaRouter: + +```yaml +agents: + root: + model: orcarouter/orcarouter/auto + description: Assistant using OrcaRouter + instruction: You are a helpful assistant. +``` + +`orcarouter/auto` is OrcaRouter's adaptive router, which routes each request to the best available model for the task. You can also reference any 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 +``` diff --git a/docs/providers/overview/index.md b/docs/providers/overview/index.md index 662ca2a430..783e6b0172 100644 --- a/docs/providers/overview/index.md +++ b/docs/providers/overview/index.md @@ -60,6 +60,7 @@ Docker Agent also includes built-in aliases for these providers: | Cloudflare AI Gateway | `cloudflare-ai-gateway` | `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` + `CLOUDFLARE_GATEWAY_ID` | | Requesty | `requesty` | `REQUESTY_API_KEY` | | OpenRouter | `openrouter` | `OPENROUTER_API_KEY` | +| OrcaRouter | `orcarouter` | `ORCAROUTER_API_KEY` | | Azure OpenAI | `azure` | `AZURE_API_KEY` + `base_url` | | [Ollama](../local/index.md) | `ollama` | None (local; optional `base_url`) | | GitHub Copilot | `github-copilot` | `GITHUB_TOKEN` (PAT with `copilot` scope) | diff --git a/pkg/config/auto.go b/pkg/config/auto.go index f66b22f042..55472b13d8 100644 --- a/pkg/config/auto.go +++ b/pkg/config/auto.go @@ -61,6 +61,7 @@ var cloudProviders = []providerConfig{ }, "GOOGLE_API_KEY (or GEMINI_API_KEY, GOOGLE_GENAI_USE_VERTEXAI)", "GOOGLE_API_KEY"}, {"mistral", []string{"MISTRAL_API_KEY"}, "MISTRAL_API_KEY", "MISTRAL_API_KEY"}, {"openrouter", []string{"OPENROUTER_API_KEY"}, "OPENROUTER_API_KEY", "OPENROUTER_API_KEY"}, + {"orcarouter", []string{"ORCAROUTER_API_KEY"}, "ORCAROUTER_API_KEY", "ORCAROUTER_API_KEY"}, {"baseten", []string{"BASETEN_API_KEY"}, "BASETEN_API_KEY", "BASETEN_API_KEY"}, {"ovhcloud", []string{"OVH_AI_ENDPOINTS_ACCESS_TOKEN"}, "OVH_AI_ENDPOINTS_ACCESS_TOKEN", "OVH_AI_ENDPOINTS_ACCESS_TOKEN"}, {"groq", []string{"GROQ_API_KEY"}, "GROQ_API_KEY", "GROQ_API_KEY"}, @@ -157,6 +158,7 @@ var DefaultModels = map[string]string{ "dmr": "ai/qwen3:latest", "mistral": "mistral-small-latest", "openrouter": "meta-llama/llama-3.3-70b-instruct", + "orcarouter": "orcarouter/auto", "baseten": "deepseek-ai/DeepSeek-V3.1", "ovhcloud": "Qwen3.5-397B-A17B", "groq": "llama-3.3-70b-versatile", diff --git a/pkg/config/auto_test.go b/pkg/config/auto_test.go index 8e6b4dfe2b..ffad94c958 100644 --- a/pkg/config/auto_test.go +++ b/pkg/config/auto_test.go @@ -72,6 +72,13 @@ func TestAvailableProviders_NoGateway(t *testing.T) { }, expectedProvider: "openrouter", }, + { + name: "orcarouter api key present", + envVars: map[string]string{ + "ORCAROUTER_API_KEY": "test-key", + }, + expectedProvider: "orcarouter", + }, { name: "baseten api key present", envVars: map[string]string{ @@ -304,6 +311,15 @@ func TestAutoModelConfig(t *testing.T) { expectedModel: "meta-llama/llama-3.3-70b-instruct", 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{ @@ -455,6 +471,10 @@ func TestPreferredMaxTokens(t *testing.T) { provider: "openrouter", expectedTokens: 32000, }, + { + provider: "orcarouter", + expectedTokens: 32000, + }, { provider: "unknown-provider", expectedTokens: 32000, @@ -476,7 +496,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", "fireworks", "deepseek", "cerebras", "together", "huggingface", "moonshot", "vercel", "amazon-bedrock", "opencode-zen", "opencode-go", "github-copilot"} + expectedProviders := []string{"openai", "anthropic", "google", "dmr", "mistral", "openrouter", "orcarouter", "baseten", "ovhcloud", "groq", "fireworks", "deepseek", "cerebras", "together", "huggingface", "moonshot", "vercel", "amazon-bedrock", "opencode-zen", "opencode-go", "github-copilot"} for _, provider := range expectedProviders { t.Run(provider, func(t *testing.T) { @@ -494,6 +514,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-3.3-70b-instruct", DefaultModels["openrouter"]) + assert.Equal(t, "orcarouter/auto", DefaultModels["orcarouter"]) 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"]) @@ -513,7 +534,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", "fireworks", "deepseek", "cerebras", "together", "huggingface", "moonshot", "vercel", "opencode-zen", "github-copilot"} + providers := []string{"openai", "anthropic", "google", "mistral", "openrouter", "orcarouter", "baseten", "ovhcloud", "groq", "fireworks", "deepseek", "cerebras", "together", "huggingface", "moonshot", "vercel", "opencode-zen", "github-copilot"} for _, provider := range providers { t.Run(provider, func(t *testing.T) { @@ -535,6 +556,8 @@ func TestAutoModelConfig_IntegrationWithDefaultModels(t *testing.T) { envVars["MISTRAL_API_KEY"] = "test-key" case "openrouter": envVars["OPENROUTER_API_KEY"] = "test-key" + case "orcarouter": + envVars["ORCAROUTER_API_KEY"] = "test-key" case "baseten": envVars["BASETEN_API_KEY"] = "test-key" case "ovhcloud": diff --git a/pkg/model/provider/aliases.go b/pkg/model/provider/aliases.go index 49e6123643..209dfd6e40 100644 --- a/pkg/model/provider/aliases.go +++ b/pkg/model/provider/aliases.go @@ -64,6 +64,11 @@ var Aliases = map[string]Alias{ BaseURL: "https://openrouter.ai/api/v1", TokenEnvVar: "OPENROUTER_API_KEY", }, + "orcarouter": { + APIType: "openai", + BaseURL: "https://api.orcarouter.ai/v1", + TokenEnvVar: "ORCAROUTER_API_KEY", + }, "mistral": { APIType: "openai", BaseURL: "https://api.mistral.ai/v1", diff --git a/pkg/model/provider/aliases_test.go b/pkg/model/provider/aliases_test.go index e0c48a1b56..2e5de2fb51 100644 --- a/pkg/model/provider/aliases_test.go +++ b/pkg/model/provider/aliases_test.go @@ -37,6 +37,7 @@ func TestCatalogAliases(t *testing.T) { expected := map[string]Alias{ "openrouter": {APIType: "openai", BaseURL: "https://openrouter.ai/api/v1", TokenEnvVar: "OPENROUTER_API_KEY"}, + "orcarouter": {APIType: "openai", BaseURL: "https://api.orcarouter.ai/v1", TokenEnvVar: "ORCAROUTER_API_KEY"}, "baseten": {APIType: "openai", BaseURL: "https://inference.baseten.co/v1", TokenEnvVar: "BASETEN_API_KEY"}, "ovhcloud": {APIType: "openai", BaseURL: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1", TokenEnvVar: "OVH_AI_ENDPOINTS_ACCESS_TOKEN"}, "groq": {APIType: "openai", BaseURL: "https://api.groq.com/openai/v1", TokenEnvVar: "GROQ_API_KEY"}, diff --git a/pkg/model/provider/openai/client.go b/pkg/model/provider/openai/client.go index 5b37c58cd7..9c259ada39 100644 --- a/pkg/model/provider/openai/client.go +++ b/pkg/model/provider/openai/client.go @@ -261,6 +261,7 @@ var openModelHostProviders = map[string]bool{ "baseten": true, "ovhcloud": true, "openrouter": true, + "orcarouter": true, "nebius": true, "nvidia": true, "cerebras": true,