From afb8dac0202c52277f54f152661a80e9610b389f Mon Sep 17 00:00:00 2001 From: Felix <151852644+Felixkw12@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:57:15 +0200 Subject: [PATCH] feat: add Opper as an OpenAI-compatible provider alias Adds Opper (https://opper.ai), an EU-hosted AI gateway, as a built-in OpenAI-compatible alias in the same shape as the OpenRouter alias: alias entry with OPPER_API_KEY and base URL https://api.opper.ai/v3/compat, auto-config support with claude-sonnet-4-6 as the default model, the system-message merge shared with the other gateway aliases, a provider docs page, an example recipe, and test coverage across the touched packages. The models.dev catalog already carries the opper provider, so the default model and the example resolve through it and no catalog exemption is needed. Disclosure: Opper is my startup. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Felix Wunderlich --- agent-schema.json | 2 +- docs/configuration/models/index.md | 6 +- docs/data/nav.yml | 2 + docs/providers/opper/index.md | 69 +++++++++++++++++++ docs/providers/overview/index.md | 1 + examples/README.md | 1 + examples/opper.yaml | 17 +++++ pkg/config/auto.go | 2 + pkg/config/auto_test.go | 16 +++++ pkg/model/provider/aliases.go | 5 ++ pkg/model/provider/aliases_test.go | 1 + pkg/model/provider/openai/client.go | 1 + .../openai/system_message_merge_test.go | 11 +++ 13 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 docs/providers/opper/index.md create mode 100644 examples/opper.yaml diff --git a/agent-schema.json b/agent-schema.json index 39fc2d5866..f0ad1fb223 100644 --- a/agent-schema.json +++ b/agent-schema.json @@ -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, 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, 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.).", "examples": [ "openai", "anthropic", diff --git a/docs/configuration/models/index.md b/docs/configuration/models/index.md index 34b876ae26..6b30c46a5e 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, daoxe, ovhcloud, groq, fireworks-ai, deepseek, cerebras, togetherai, huggingface, moonshotai, vercel, cloudflare-workers-ai, cloudflare-ai-gateway, requesty, openrouter, 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, api-route, 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`, `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`, `api-route`, `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`, `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`, `atlascloud`, `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/data/nav.yml b/docs/data/nav.yml index ffaa521e6b..7a7f1ab145 100644 --- a/docs/data/nav.yml +++ b/docs/data/nav.yml @@ -227,6 +227,8 @@ url: /providers/opencode-go/ - title: OpenCode Zen url: /providers/opencode-zen/ + - title: Opper + url: /providers/opper/ - title: OpenRouter url: /providers/openrouter/ - title: OVHcloud diff --git a/docs/providers/opper/index.md b/docs/providers/opper/index.md new file mode 100644 index 0000000000..716d940b49 --- /dev/null +++ b/docs/providers/opper/index.md @@ -0,0 +1,69 @@ +--- +title: "Opper" +description: "Use Opper models with Docker Agent." +keywords: docker agent, ai agents, model providers, llm, opper +weight: 225 +canonical: https://docs.docker.com/ai/docker-agent/providers/opper/ +--- + +_Use Opper models with Docker Agent._ + +## Overview + +[Opper](https://opper.ai) is an EU-hosted AI gateway that serves models from +many providers behind one OpenAI-compatible endpoint, with one API key. +Docker Agent includes built-in support for Opper as an alias provider. + +## Setup + +1. Create an API key at [platform.opper.ai](https://platform.opper.ai). +2. Set the environment variable: + + ```bash + export OPPER_API_KEY=your-api-key + ``` + +## Usage + +### Inline Syntax + +```yaml +agents: + root: + model: opper/claude-sonnet-4-6 + description: Assistant using Opper + instruction: You are a helpful assistant. +``` + +### Named Model + +```yaml +models: + opper_model: + provider: opper + model: claude-sonnet-4-6 + temperature: 0.7 + max_tokens: 8192 + +agents: + root: + model: opper_model + description: Assistant using Opper + instruction: You are a helpful assistant. +``` + +## Available Models + +Opper model IDs come in two forms. A bare pool name such as +`claude-sonnet-4-6` or `gpt-5.4-mini` lets Opper pick a provider for each +request. A `provider/model` ID such as `anthropic/claude-sonnet-4-6` pins one +route. List the model IDs your key can call with: + +```bash +curl https://api.opper.ai/v3/compat/models \ + -H "Authorization: Bearer $OPPER_API_KEY" +``` + +The public catalogue is also available without a key at +`https://api.opper.ai/v3/models`. Docker Agent's default for Opper is +`claude-sonnet-4-6`. diff --git a/docs/providers/overview/index.md b/docs/providers/overview/index.md index 2f6e18ea85..b9347bf9b7 100644 --- a/docs/providers/overview/index.md +++ b/docs/providers/overview/index.md @@ -67,6 +67,7 @@ Use this table to find a built-in provider's config key and authentication metho | 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` | +| Opper | `opper` | `OPPER_API_KEY` | | API Route | `api-route` | `API_ROUTE_API_KEY` | | Atlas Cloud | [`atlascloud`](../atlascloud/index.md) | `ATLASCLOUD_API_KEY` | | Azure OpenAI | `azure` | `AZURE_API_KEY` + `base_url` (override the key with `token_key`) | diff --git a/examples/README.md b/examples/README.md index 5b77fca141..de091668ee 100644 --- a/examples/README.md +++ b/examples/README.md @@ -210,6 +210,7 @@ remote MCP endpoints. | [`atlascloud.yaml`](atlascloud.yaml) | Atlas Cloud open-model host (`atlascloud`). | | [`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. | | [`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 (`fireworks-ai`). | diff --git a/examples/opper.yaml b/examples/opper.yaml new file mode 100644 index 0000000000..3c2362d714 --- /dev/null +++ b/examples/opper.yaml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=../agent-schema.json + +models: + opper_model: + provider: opper + model: claude-sonnet-4-6 + +agents: + root: + model: opper_model + description: Assistant using Opper + 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 4bd613ca86..13670a6f2f 100644 --- a/pkg/config/auto.go +++ b/pkg/config/auto.go @@ -64,6 +64,7 @@ var cloudProviders = []providerConfig{ {"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"}, {"atlascloud", []string{"ATLASCLOUD_API_KEY"}, "ATLASCLOUD_API_KEY", "ATLASCLOUD_API_KEY"}, + {"opper", []string{"OPPER_API_KEY"}, "OPPER_API_KEY", "OPPER_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"}, {"daoxe", []string{"DAOXE_API_KEY"}, "DAOXE_API_KEY", "DAOXE_API_KEY"}, @@ -163,6 +164,7 @@ var DefaultModels = map[string]string{ "openrouter": "meta-llama/llama-4-maverick", "api-route": "gpt-6.1-sol", "atlascloud": "qwen/qwen3.8-max", + "opper": "claude-sonnet-4-6", "baseten": "deepseek-ai/DeepSeek-V4-Pro", "ovhcloud": "Qwen3.5-397B-A17B", "daoxe": "claude-sonnet-4-6", diff --git a/pkg/config/auto_test.go b/pkg/config/auto_test.go index 8566c6801a..ee08d0fbcd 100644 --- a/pkg/config/auto_test.go +++ b/pkg/config/auto_test.go @@ -88,6 +88,13 @@ func TestAvailableProviders_NoGateway(t *testing.T) { }, expectedProvider: "atlascloud", }, + { + name: "opper api key present", + envVars: map[string]string{ + "OPPER_API_KEY": "test-key", + }, + expectedProvider: "opper", + }, { name: "baseten api key present", envVars: map[string]string{ @@ -336,6 +343,15 @@ func TestAutoModelConfig(t *testing.T) { expectedModel: "gpt-6.1-sol", expectedMaxTokens: 32000, }, + { + name: "opper provider", + envVars: map[string]string{ + "OPPER_API_KEY": "test-key", + }, + expectedProvider: "opper", + expectedModel: "claude-sonnet-4-6", + expectedMaxTokens: 32000, + }, { name: "baseten provider", envVars: map[string]string{ diff --git a/pkg/model/provider/aliases.go b/pkg/model/provider/aliases.go index 53d1cca4e0..0c18b3fe63 100644 --- a/pkg/model/provider/aliases.go +++ b/pkg/model/provider/aliases.go @@ -75,6 +75,11 @@ var Aliases = map[string]Alias{ BaseURL: "https://api.atlascloud.ai/v1", TokenEnvVar: "ATLASCLOUD_API_KEY", }, + "opper": { + APIType: "openai", + BaseURL: "https://api.opper.ai/v3/compat", + TokenEnvVar: "OPPER_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 3284941807..e8e4489a1c 100644 --- a/pkg/model/provider/aliases_test.go +++ b/pkg/model/provider/aliases_test.go @@ -41,6 +41,7 @@ func TestCatalogAliases(t *testing.T) { "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"}, "atlascloud": {APIType: "openai", BaseURL: "https://api.atlascloud.ai/v1", TokenEnvVar: "ATLASCLOUD_API_KEY"}, + "opper": {APIType: "openai", BaseURL: "https://api.opper.ai/v3/compat", TokenEnvVar: "OPPER_API_KEY"}, "baseten": {APIType: "openai", BaseURL: "https://inference.baseten.co/v1", TokenEnvVar: "BASETEN_API_KEY"}, "daoxe": {APIType: "openai", BaseURL: "https://api.daoxe.com/v1", TokenEnvVar: "DAOXE_API_KEY"}, "ovhcloud": {APIType: "openai", BaseURL: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1", TokenEnvVar: "OVH_AI_ENDPOINTS_ACCESS_TOKEN"}, diff --git a/pkg/model/provider/openai/client.go b/pkg/model/provider/openai/client.go index 9a1f188e0c..4b5b5fa521 100644 --- a/pkg/model/provider/openai/client.go +++ b/pkg/model/provider/openai/client.go @@ -268,6 +268,7 @@ var openModelHostProviders = map[string]bool{ "daoxe": true, "ovhcloud": true, "openrouter": true, + "opper": true, "nebius": true, "nvidia": true, "cerebras": true, diff --git a/pkg/model/provider/openai/system_message_merge_test.go b/pkg/model/provider/openai/system_message_merge_test.go index 7a5f2c0cbf..c9e34da517 100644 --- a/pkg/model/provider/openai/system_message_merge_test.go +++ b/pkg/model/provider/openai/system_message_merge_test.go @@ -45,6 +45,16 @@ func TestDaoxe_MergesConsecutiveSystemMessages(t *testing.T) { }) } +func TestOpper_MergesConsecutiveSystemMessages(t *testing.T) { + t.Parallel() + + assertMergesConsecutiveMessages(t, &latest.ModelConfig{ + Provider: "opper", + Model: "claude-sonnet-4-6", + TokenKey: "MY_TOKEN", + }) +} + func TestBaseten_MergesConsecutiveSystemMessages(t *testing.T) { t.Parallel() @@ -172,6 +182,7 @@ func TestShouldMergeConsecutiveMessages_Gating(t *testing.T) { {"api-route, no base_url", &latest.ModelConfig{Provider: "api-route", Model: "gpt-6.1-sol"}, true}, {"baseten", &latest.ModelConfig{Provider: "baseten", Model: "zai-org/GLM-5.2"}, true}, {"daoxe, no base_url", &latest.ModelConfig{Provider: "daoxe", Model: "claude-sonnet-4-6"}, true}, + {"opper, no base_url", &latest.ModelConfig{Provider: "opper", Model: "claude-sonnet-4-6"}, 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-k3"}, true},