Unlock and correcly mapp all OpenCode Zen models - #4468
Conversation
* fix: load-test script build paths and provider config * fix: ensure ui placeholder exists even when ui dir is empty Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: G-XD <gxd0606@gmail.com> --------- Signed-off-by: G-XD <gxd0606@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…q#4362) compat.should_drop_params dropped max_output_tokens from /v1/responses because the chat-named model-parameter catalog lists max_tokens, not max_output_tokens. Keep it when the chat token cap is supported, mirroring the existing max_completion_tokens rule. Adds the first unit tests for the compat plugin. Fixes maximhq#4354 Co-authored-by: Akshay Deo <akshay@akshaydeo.com>
…yID` after `PreRequestHook` unblock (maximhq#4359) ## Summary Routing rules that pin a specific API key by ID were silently broken. During `PreRequestHook` execution, core blocks writes to reserved context keys (including `BifrostContextKeyAPIKeyID`) to prevent plugins from overriding caller-supplied values. The governance plugin was writing the routing-rule key pin directly to that reserved key, so the write was dropped and the pin never took effect. ## Changes - Introduced a new non-reserved context key `BifrostContextKeyRoutingPinnedAPIKeyID` that the governance plugin writes its routing-rule key pin to during the blocked `PreRequestHook` phase. - After all `PreRequestHook`s complete and the restricted-write block is lifted, core's `RunPreRequestHooks` commits the routing pin from `BifrostContextKeyRoutingPinnedAPIKeyID` into the reserved `BifrostContextKeyAPIKeyID`. A non-empty routing pin overrides any caller-supplied pin, since the routing rule represents authoritative server-side policy. - The `defer ctx.UnblockRestrictedWrites()` was replaced with an explicit call after the plugin loop, so the commit step runs after the block is lifted rather than after the function returns. - Updated the governance plugin's `applyRoutingRules` to write to the new non-reserved key instead of the reserved one. - Updated tests in both `core/bifrost_test.go` and `plugins/governance/routing_test.go` to exercise the real propagation path, including the restricted-write block and plugin scope, and to assert the pin lands on the correct context key. ## Type of change - [x] Bug fix ## Affected areas - [x] Core (Go) - [x] Plugins ## How to test ```sh go test ./core/... ./plugins/governance/... ``` The new `TestRunPreRequestHooks_CommitsRoutingPinnedKey` test covers three cases: 1. A routing pin is committed to the reserved `BifrostContextKeyAPIKeyID`. 2. A routing pin overrides a caller-supplied `BifrostContextKeyAPIKeyID`. 3. A caller-supplied `BifrostContextKeyAPIKeyID` is preserved when no routing pin is set. The updated `TestEvaluateRoutingRules_MultiTargetDeterministicWithPinnedKey` test exercises the full propagation path through `applyRoutingRules` under the same restricted-write block that production uses. ## Breaking changes - [ ] Yes - [x] No ## Security considerations The routing pin is written to a dedicated non-reserved key and committed to the reserved key exclusively by core after the plugin phase completes. This preserves the invariant that only core is the authoritative writer of `BifrostContextKeyAPIKeyID`, preventing plugins from directly overriding key selection outside of the sanctioned routing-rule mechanism. ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Routing plugins can now pin specific API keys that take precedence over caller-supplied selections through dedicated routing-pinned key handling in the pre-request hook phase. * **Tests** * Added tests to verify routing-pinned API key commitment, precedence handling, and proper context propagation through key selection logic. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Implement the OpenCode Zen (pay-as-you-go) and OpenCode Go (subscription-based) AI gateway providers. Both expose an OpenAI-compatible API and share a common implementation via openai.HandleOpenAI* delegation, differing only in default base URL and provider key. Supports: - Chat completions (non-streaming and streaming via SSE) - Responses API (converted internally to chat completions) - List models - Custom error format parsing (Opencode specific JSON envelope) Includes: - Core provider files: opencode.go, errors.go, cachedcontents.go - Provider registration in bifrost.go and schemas/bifrost.go - Config schema entries in transports/config.schema.json - UI integration: icons, model placeholders, key requirements, labels - Full provider documentation with setup guides and caveats - Compile-time interface conformance check and constructor unit tests
…ontext for streaming - Replace hardcoded "/v1/chat/completions" path in ChatCompletionStream with providerUtils.GetPathFromContext() to support context-based URL path overrides (consistent with other providers). - Add TestOpencodeUnsupportedOperations covering all unsupported operations (text, embedding, image, video, speech, batch, file, container, passthrough, cached content) for both OpencodeZen and OpencodeGo provider keys, verifying error message format, provider key, and request type in error details.
Add route resolution for Opencode providers so chat and responses requests dispatch to the correct OpenAI, Anthropic, or Gemini adapter based on model ID.\n\nSplit provider-specific execution into dedicated adapter helpers, add route validation, and cover exact, class-based, and default routing behavior with tests.
|
|
📝 WalkthroughWalkthroughAdds two new OpenCode providers ( ChangesOpenCode Provider and Routing-Pin Infrastructure
Compat Plugin max_output_tokens Fix
Load-Test Script Fixes
Sequence Diagram(s)sequenceDiagram
participant Client
participant PluginPipeline
participant GovernancePlugin
participant opencodeProvider
participant BackendAdapter
Client->>PluginPipeline: RunPreRequestHooks(ctx, request)
PluginPipeline->>GovernancePlugin: PreRequestHook(pluginCtx, request)
GovernancePlugin->>pluginCtx: set BifrostContextKeyRoutingPinnedAPIKeyID = KeyID
PluginPipeline->>ctx: UnblockRestrictedWrites()
PluginPipeline->>ctx: commit BifrostContextKeyRoutingPinnedAPIKeyID → BifrostContextKeyAPIKeyID
PluginPipeline-->>Client: hooks done
Client->>opencodeProvider: ChatCompletion(ctx, key, request)
opencodeProvider->>opencodeProvider: resolveRoute(providerKey, modelID)
opencodeProvider->>opencodeProvider: validateChatRoute(route, request)
alt OpenAI adapter
opencodeProvider->>BackendAdapter: executeOpenAIChat(...)
else Anthropic adapter
opencodeProvider->>BackendAdapter: executeAnthropicChat(...)
else Gemini adapter
opencodeProvider->>BackendAdapter: executeGeminiChat(...)
end
BackendAdapter-->>opencodeProvider: BifrostChatResponse / BifrostError
opencodeProvider-->>Client: response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ❌ 5❌ Failed checks (2 warnings, 3 inconclusive)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
core/schemas/bifrost.go (1)
67-75:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep runtime base-provider validation in sync with the schema.
transports/config.schema.jsonnow allowsopencode-goandopencode-zenascustom_provider_config.base_provider_type, butSupportedBaseProvidersstill omits both. Add them here so schema-valid OpenCode custom-provider configs are not rejected by Go-side validation. As per coding guidelines,transports/config.schema.jsonis the source of truth for config fields.Proposed fix
var SupportedBaseProviders = []ModelProvider{ Anthropic, Bedrock, Cohere, Gemini, + OpencodeGo, + OpencodeZen, OpenAI, HuggingFace, Replicate, }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/schemas/bifrost.go` around lines 67 - 75, The SupportedBaseProviders slice in core/schemas/bifrost.go is missing the opencode-go and opencode-zen provider types that are now allowed by transports/config.schema.json. Add these two providers to the SupportedBaseProviders array to keep the Go-side runtime validation in sync with the schema, which is the source of truth for config fields. This ensures that schema-valid OpenCode custom-provider configurations are not rejected by Go validation.Source: Coding guidelines
🧹 Nitpick comments (1)
core/providers/opencode/anthropicadapter.go (1)
21-21: ⚡ Quick win
setOpencodeAnthropicBaseURLis currently a no-op and should be removed or implemented.The helper never applies
baseURL(Lines 78-84), so all four call sites are behaviorally inert. Please either implement the intended mutation or delete the helper/calls to avoid false confidence about URL wiring.Also applies to: 39-39, 55-55, 73-73, 78-84
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/providers/opencode/anthropicadapter.go` at line 21, The `setOpencodeAnthropicBaseURL` function defined at lines 78-84 in core/providers/opencode/anthropicadapter.go does not actually apply the baseURL parameter to the provider, making it a no-op. Either implement the function to properly mutate and return the provider with the baseURL applied, or remove the function definition entirely. If removing the function, also delete all four call sites to `setOpencodeAnthropicBaseURL` at lines 21, 39, 55, and 73 in the same file. Choose implementation only if URL base configuration is actually needed; otherwise deletion is preferred to avoid creating false confidence about URL wiring.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@core/providers/opencode/opencode.go`:
- Around line 124-126: The default branches in the opencode.go file are
incorrectly returning context.DeadlineExceeded, which misclassifies unexpected
routing or adapter state failures as timeout errors. This causes incorrect retry
and observability behavior. Replace context.DeadlineExceeded with an appropriate
adapter-resolution error (such as an error created with providerUtils that
reflects the actual failure mode of unexpected/invalid routing state) at all
affected locations: the default case at lines 124-126 (anchor), and the matching
default branches at lines 142-144, 166-168, and 187-189 (siblings). Use a
consistent error type across all four locations that properly indicates an
adapter-resolution failure rather than a timeout.
In `@core/providers/opencode/routing.go`:
- Around line 195-199: The modelID parameter is being directly substituted into
the route path in the buildResolvedRoute function without escaping, creating a
security vulnerability where special path and query characters in the modelID
could be used to alter the upstream endpoint. Before performing the
strings.ReplaceAll operation on the path, apply proper URL path escaping to the
modelID variable using a function like url.PathEscape from the net/url package
to ensure special characters are properly encoded and cannot be used for
injection attacks.
In `@docs/providers/supported-providers/opencode.mdx`:
- Around line 53-134: The Tabs block starting at line 53 in opencode.mdx is
missing the required API tab per the documentation guidelines. After the Web UI
Tab closing tag, add a new Tab with title "API" that includes a curl command
example demonstrating how to call the OpenCode Zen endpoint using the Bifrost
API with proper headers (Authorization and Content-Type) and a JSON payload
containing the provider name (opencode-zen), model name, and messages. This API
tab should be positioned before the config.json tabs to follow the standard
documentation structure of Web UI / API / config.json.
---
Outside diff comments:
In `@core/schemas/bifrost.go`:
- Around line 67-75: The SupportedBaseProviders slice in core/schemas/bifrost.go
is missing the opencode-go and opencode-zen provider types that are now allowed
by transports/config.schema.json. Add these two providers to the
SupportedBaseProviders array to keep the Go-side runtime validation in sync with
the schema, which is the source of truth for config fields. This ensures that
schema-valid OpenCode custom-provider configurations are not rejected by Go
validation.
---
Nitpick comments:
In `@core/providers/opencode/anthropicadapter.go`:
- Line 21: The `setOpencodeAnthropicBaseURL` function defined at lines 78-84 in
core/providers/opencode/anthropicadapter.go does not actually apply the baseURL
parameter to the provider, making it a no-op. Either implement the function to
properly mutate and return the provider with the baseURL applied, or remove the
function definition entirely. If removing the function, also delete all four
call sites to `setOpencodeAnthropicBaseURL` at lines 21, 39, 55, and 73 in the
same file. Choose implementation only if URL base configuration is actually
needed; otherwise deletion is preferred to avoid creating false confidence about
URL wiring.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 016489cd-390b-4378-87f1-68e5a9dab002
📒 Files selected for processing (25)
.github/workflows/scripts/load-test.shcore/bifrost.gocore/bifrost_test.gocore/providers/opencode/anthropicadapter.gocore/providers/opencode/cachedcontents.gocore/providers/opencode/errors.gocore/providers/opencode/geminiadapter.gocore/providers/opencode/openaiadapter.gocore/providers/opencode/opencode.gocore/providers/opencode/opencode_test.gocore/providers/opencode/routing.gocore/providers/opencode/routing_test.gocore/providers/opencode/types.gocore/providers/opencode/validation.gocore/schemas/bifrost.godocs/docs.jsondocs/providers/supported-providers/opencode.mdxplugins/compat/dropparams.goplugins/compat/dropparams_test.goplugins/governance/main.goplugins/governance/routing_test.gotransports/config.schema.jsonui/lib/constants/config.tsui/lib/constants/icons.tsxui/lib/constants/logs.ts
| default: | ||
| return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRequestMarshal, context.DeadlineExceeded) | ||
| } |
There was a problem hiding this comment.
Return an adapter-resolution error here, not context.DeadlineExceeded.
These default branches represent an unexpected routing/adapter state. Returning a timeout error misclassifies the failure mode and can trigger incorrect retry/observability behavior.
Also applies to: 142-144, 166-168, 187-189
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@core/providers/opencode/opencode.go` around lines 124 - 126, The default
branches in the opencode.go file are incorrectly returning
context.DeadlineExceeded, which misclassifies unexpected routing or adapter
state failures as timeout errors. This causes incorrect retry and observability
behavior. Replace context.DeadlineExceeded with an appropriate
adapter-resolution error (such as an error created with providerUtils that
reflects the actual failure mode of unexpected/invalid routing state) at all
affected locations: the default case at lines 124-126 (anchor), and the matching
default branches at lines 142-144, 166-168, and 187-189 (siblings). Use a
consistent error type across all four locations that properly indicates an
adapter-resolution failure rather than a timeout.
| func buildResolvedRoute(spec routeSpec, matchedBy routeMatchKind, classPrefix, modelID string) resolvedRoute { | ||
| path := spec.path | ||
| if strings.Contains(path, "{model}") { | ||
| path = strings.ReplaceAll(path, "{model}", modelID) | ||
| } |
There was a problem hiding this comment.
Escape model IDs before substituting into route paths.
modelID is interpolated directly into "/v1/models/{model}". A crafted model value containing path/query metacharacters can alter the upstream endpoint path.
Suggested fix
import (
+ "net/url"
"strings"
schemas "github.com/maximhq/bifrost/core/schemas"
)
@@
func buildResolvedRoute(spec routeSpec, matchedBy routeMatchKind, classPrefix, modelID string) resolvedRoute {
path := spec.path
if strings.Contains(path, "{model}") {
- path = strings.ReplaceAll(path, "{model}", modelID)
+ path = strings.ReplaceAll(path, "{model}", url.PathEscape(modelID))
}
return resolvedRoute{📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func buildResolvedRoute(spec routeSpec, matchedBy routeMatchKind, classPrefix, modelID string) resolvedRoute { | |
| path := spec.path | |
| if strings.Contains(path, "{model}") { | |
| path = strings.ReplaceAll(path, "{model}", modelID) | |
| } | |
| import ( | |
| "net/url" | |
| "strings" | |
| schemas "github.com/maximhq/bifrost/core/schemas" | |
| ) | |
| func buildResolvedRoute(spec routeSpec, matchedBy routeMatchKind, classPrefix, modelID string) resolvedRoute { | |
| path := spec.path | |
| if strings.Contains(path, "{model}") { | |
| path = strings.ReplaceAll(path, "{model}", url.PathEscape(modelID)) | |
| } | |
| return resolvedRoute{ |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@core/providers/opencode/routing.go` around lines 195 - 199, The modelID
parameter is being directly substituted into the route path in the
buildResolvedRoute function without escaping, creating a security vulnerability
where special path and query characters in the modelID could be used to alter
the upstream endpoint. Before performing the strings.ReplaceAll operation on the
path, apply proper URL path escaping to the modelID variable using a function
like url.PathEscape from the net/url package to ensure special characters are
properly encoded and cannot be used for injection attacks.
| <Tabs> | ||
| <Tab title="Web UI"> | ||
|
|
||
| 1. Navigate to **Models** > **Model Providers**. Look for **OpenCode Zen** or **OpenCode Go** under **Configured Providers**. If missing, click **Add New Provider** and select the desired provider. | ||
| 2. Click **Add Key** or edit an existing key. | ||
| 3. Set a name for your key. | ||
| 4. Paste your OpenCode API key directly or use an environment variable (for example, `env.OPENCODE_API_KEY`). | ||
| 5. Set **Allowed Models** to **All Models** (default) or the specific model allowlist you want this key to serve. | ||
| 6. Save the provider configuration. | ||
|
|
||
| </Tab> | ||
| <Tab title="config.json (Zen)"> | ||
|
|
||
| ```json | ||
| { | ||
| "providers": { | ||
| "opencode-zen": { | ||
| "keys": [ | ||
| { | ||
| "name": "zen-key-1", | ||
| "value": "env.OPENCODE_API_KEY", | ||
| "models": ["*"], | ||
| "weight": 1.0 | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| The default Base URL is `https://opencode.ai/zen`. Override via `network_config.base_url` if needed. | ||
|
|
||
| </Tab> | ||
| <Tab title="config.json (Go)"> | ||
|
|
||
| ```json | ||
| { | ||
| "providers": { | ||
| "opencode-go": { | ||
| "keys": [ | ||
| { | ||
| "name": "go-key-1", | ||
| "value": "env.OPENCODE_API_KEY", | ||
| "models": ["*"], | ||
| "weight": 1.0 | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| The default Base URL is `https://opencode.ai/zen/go`. Override via `network_config.base_url` if needed. | ||
|
|
||
| </Tab> | ||
| <Tab title="Go SDK"> | ||
| For OpenCode Zen: | ||
|
|
||
| ```go | ||
| case schemas.OpencodeZen: | ||
| return []schemas.Key{{ | ||
| Name: "zen-key-1", | ||
| Value: *schemas.NewEnvVar("env.OPENCODE_API_KEY"), | ||
| Models: []string{"*"}, | ||
| Weight: 1.0, | ||
| }}, nil | ||
| ``` | ||
|
|
||
| For OpenCode Go: | ||
|
|
||
| ```go | ||
| case schemas.OpencodeGo: | ||
| return []schemas.Key{{ | ||
| Name: "go-key-1", | ||
| Value: *schemas.NewEnvVar("env.OPENCODE_API_KEY"), | ||
| Models: []string{"*"}, | ||
| Weight: 1.0, | ||
| }}, nil | ||
| ``` | ||
|
|
||
| </Tab> | ||
| </Tabs> |
There was a problem hiding this comment.
Add the required API tab in the setup tabs block.
Line 53 starts a Mintlify tabs section with Web UI/config.json/Go SDK, but it omits the required API tab for docs pages in this repo.
🛠 Suggested patch
<Tabs>
<Tab title="Web UI">
...
</Tab>
+<Tab title="API">
+
+```bash
+curl "$BIFROST_URL/v1/chat/completions" \
+ -H "Authorization: Bearer $BIFROST_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "provider": "opencode-zen",
+ "model": "deepseek-v4-flash",
+ "messages": [{"role":"user","content":"Hello"}]
+ }'
+```
+
+</Tab>
<Tab title="config.json (Zen)">
...As per coding guidelines: docs/**/*.mdx: “Mintlify MDX documentation must have Web UI / API / config.json tabs.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Tabs> | |
| <Tab title="Web UI"> | |
| 1. Navigate to **Models** > **Model Providers**. Look for **OpenCode Zen** or **OpenCode Go** under **Configured Providers**. If missing, click **Add New Provider** and select the desired provider. | |
| 2. Click **Add Key** or edit an existing key. | |
| 3. Set a name for your key. | |
| 4. Paste your OpenCode API key directly or use an environment variable (for example, `env.OPENCODE_API_KEY`). | |
| 5. Set **Allowed Models** to **All Models** (default) or the specific model allowlist you want this key to serve. | |
| 6. Save the provider configuration. | |
| </Tab> | |
| <Tab title="config.json (Zen)"> | |
| ```json | |
| { | |
| "providers": { | |
| "opencode-zen": { | |
| "keys": [ | |
| { | |
| "name": "zen-key-1", | |
| "value": "env.OPENCODE_API_KEY", | |
| "models": ["*"], | |
| "weight": 1.0 | |
| } | |
| ] | |
| } | |
| } | |
| } | |
| ``` | |
| The default Base URL is `https://opencode.ai/zen`. Override via `network_config.base_url` if needed. | |
| </Tab> | |
| <Tab title="config.json (Go)"> | |
| ```json | |
| { | |
| "providers": { | |
| "opencode-go": { | |
| "keys": [ | |
| { | |
| "name": "go-key-1", | |
| "value": "env.OPENCODE_API_KEY", | |
| "models": ["*"], | |
| "weight": 1.0 | |
| } | |
| ] | |
| } | |
| } | |
| } | |
| ``` | |
| The default Base URL is `https://opencode.ai/zen/go`. Override via `network_config.base_url` if needed. | |
| </Tab> | |
| <Tab title="Go SDK"> | |
| For OpenCode Zen: | |
| ```go | |
| case schemas.OpencodeZen: | |
| return []schemas.Key{{ | |
| Name: "zen-key-1", | |
| Value: *schemas.NewEnvVar("env.OPENCODE_API_KEY"), | |
| Models: []string{"*"}, | |
| Weight: 1.0, | |
| }}, nil | |
| ``` | |
| For OpenCode Go: | |
| ```go | |
| case schemas.OpencodeGo: | |
| return []schemas.Key{{ | |
| Name: "go-key-1", | |
| Value: *schemas.NewEnvVar("env.OPENCODE_API_KEY"), | |
| Models: []string{"*"}, | |
| Weight: 1.0, | |
| }}, nil | |
| ``` | |
| </Tab> | |
| </Tabs> | |
| <Tabs> | |
| <Tab title="Web UI"> | |
| 1. Navigate to **Models** > **Model Providers**. Look for **OpenCode Zen** or **OpenCode Go** under **Configured Providers**. If missing, click **Add New Provider** and select the desired provider. | |
| 2. Click **Add Key** or edit an existing key. | |
| 3. Set a name for your key. | |
| 4. Paste your OpenCode API key directly or use an environment variable (for example, `env.OPENCODE_API_KEY`). | |
| 5. Set **Allowed Models** to **All Models** (default) or the specific model allowlist you want this key to serve. | |
| 6. Save the provider configuration. | |
| </Tab> | |
| <Tab title="API"> | |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/providers/supported-providers/opencode.mdx` around lines 53 - 134, The
Tabs block starting at line 53 in opencode.mdx is missing the required API tab
per the documentation guidelines. After the Web UI Tab closing tag, add a new
Tab with title "API" that includes a curl command example demonstrating how to
call the OpenCode Zen endpoint using the Bifrost API with proper headers
(Authorization and Content-Type) and a JSON payload containing the provider name
(opencode-zen), model name, and messages. This API tab should be positioned
before the config.json tabs to follow the standard documentation structure of
Web UI / API / config.json.
Source: Coding guidelines
Confidence Score: 2/5I don't think this is safe to merge yet.
Focus on
What T-Rex did
Important Files Changed
Reviews (1): Last reviewed commit: "feat(opencode): route models by adapter ..." | Re-trigger Greptile |
| func (p *opencodeProvider) ChatCompletion(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostChatRequest) (*schemas.BifrostChatResponse, *schemas.BifrostError) { | ||
| route := resolveRoute(p.GetProviderKey(), request.Model) | ||
| if err := validateChatRoute(route, request); err != nil { | ||
| return nil, err | ||
| } |
There was a problem hiding this comment.
validateChatRoute has a nil-request check, but this method dereferences request.Model before that check runs. A caller that passes a nil chat request through the provider interface gets a panic instead of the intended Bifrost error. The same ordering appears in the stream and responses entry points, so those should be guarded before route resolution as well.
| func (p *opencodeProvider) ChatCompletion(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostChatRequest) (*schemas.BifrostChatResponse, *schemas.BifrostError) { | |
| route := resolveRoute(p.GetProviderKey(), request.Model) | |
| if err := validateChatRoute(route, request); err != nil { | |
| return nil, err | |
| } | |
| func (p *opencodeProvider) ChatCompletion(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostChatRequest) (*schemas.BifrostChatResponse, *schemas.BifrostError) { | |
| if request == nil { | |
| return nil, validateChatRoute(resolvedRoute{}, request) | |
| } | |
| route := resolveRoute(p.GetProviderKey(), request.Model) | |
| if err := validateChatRoute(route, request); err != nil { | |
| return nil, err | |
| } |
| provider := gemini.NewGeminiProvider(&schemas.ProviderConfig{ | ||
| NetworkConfig: p.networkConfig, | ||
| SendBackRawRequest: p.sendBackRawRequest, | ||
| SendBackRawResponse: p.sendBackRawResponse, | ||
| }, p.logger) | ||
| ctx.SetValue(schemas.BifrostContextKeyURLPath, route.Path+":generateContent") | ||
| return provider.ChatCompletion(ctx, key, request) |
There was a problem hiding this comment.
The routing table records an auth style for Gemini gateway routes, but the adapter never uses it. Delegating directly to GeminiProvider makes these OpenCode gateway calls send x-goog-api-key instead of the gateway auth used by the OpenAI-compatible paths. A configured OpenCode API key can therefore work for deepseek-* routes while every gemini-* route fails authentication against the same gateway.
| provider := anthropic.NewAnthropicProvider(&schemas.ProviderConfig{ | ||
| NetworkConfig: p.networkConfig, | ||
| SendBackRawRequest: p.sendBackRawRequest, | ||
| SendBackRawResponse: p.sendBackRawResponse, | ||
| }, p.logger) | ||
| provider = setOpencodeAnthropicBaseURL(provider, p.networkConfig.BaseURL) | ||
| ctx.SetValue(schemas.BifrostContextKeyURLPath, route.Path) | ||
| return provider.ChatCompletion(ctx, key, request) |
There was a problem hiding this comment.
The route resolver distinguishes Anthropic-style routes with authStyleAnthropicKey, but the adapter never consumes that value. It delegates to the Anthropic provider, which sends x-api-key and Anthropic headers to the OpenCode gateway. That means Claude/Qwen models can be routed to the right path but still fail with gateway authentication errors when using a normal OpenCode key.
| ctx.BlockRestrictedWrites() | ||
| defer ctx.UnblockRestrictedWrites() | ||
| for _, plugin := range p.llmPlugins { | ||
| pluginName := plugin.GetName() | ||
| p.logger.Debug("running pre-request hook for plugin %s", pluginName) | ||
| spanCtx, handle := p.tracer.StartSpan(ctx, fmt.Sprintf("plugin.%s.prerequesthook", sanitizeSpanName(pluginName)), schemas.SpanKindPlugin) | ||
| if spanCtx != nil { | ||
| if spanID, ok := spanCtx.Value(schemas.BifrostContextKeySpanID).(string); ok { | ||
| ctx.SetValue(schemas.BifrostContextKeySpanID, spanID) | ||
| } | ||
| } | ||
|
|
||
| pluginCtx := ctx.WithPluginScope(&pluginName) | ||
| err := plugin.PreRequestHook(pluginCtx, req) | ||
| pluginCtx.ReleasePluginScope() | ||
|
|
||
| if err != nil { | ||
| p.tracer.SetAttribute(handle, "error", err.Error()) | ||
| p.tracer.EndSpan(handle, schemas.SpanStatusError, err.Error()) | ||
| p.preHookErrors = append(p.preHookErrors, err) | ||
| p.logger.Warn("error in PreRequestHook for plugin %s: %s", pluginName, err.Error()) | ||
| continue | ||
| } | ||
| p.tracer.EndSpan(handle, schemas.SpanStatusOk, "") | ||
| } | ||
| ctx.UnblockRestrictedWrites() |
There was a problem hiding this comment.
RunPreRequestHooks used to unblock restricted context writes with a defer, but now it only calls UnblockRestrictedWrites after every plugin returns normally. If a pre-request plugin panics and the request path recovers, the context stays blocked and later core writes to reserved keys are silently dropped. ReleasePluginScope has the same problem on the panic path, so the hook scope should also be released defensively.
| return provider.ResponsesStream(ctx, postHookRunner, postHookSpanFinalizer, key, request) | ||
| } | ||
|
|
||
| func setOpencodeAnthropicBaseURL(provider *anthropic.AnthropicProvider, baseURL string) *anthropic.AnthropicProvider { |
There was a problem hiding this comment.
setOpencodeAnthropicBaseURLcopies the Anthropic provider into local variables and returns the original pointer without changing any field.- The helper is called by every Anthropic adapter path, so future readers can assume it changes routing while it currently does nothing.
- The base URL already flows through the provider config passed into
NewAnthropicProvider, so this should either be removed or replaced with the intended override.
Artifacts
Analysis of no-op Anthropic base URL helper
- Contains supporting evidence from the run (text/markdown; charset=utf-8).
| return provider.ResponsesStream(ctx, postHookRunner, postHookSpanFinalizer, key, request) | ||
| } | ||
|
|
||
| func setOpencodeAnthropicBaseURL(provider *anthropic.AnthropicProvider, baseURL string) *anthropic.AnthropicProvider { |
There was a problem hiding this comment.
setOpencodeAnthropicBaseURL is dead code — never modifies the Anthropic provider
- Bug
- The function
setOpencodeAnthropicBaseURLin anthropicadapter.go copies the provider struct locally but never writes any changes back, then returns the original unmodified pointer. It is called on every Anthropic adapter path but has no effect.
- The function
- Cause
- The function body creates local struct copies (
providerConfig := *provider) and a pointer to the copy, but never assigns to any field. The_ = providerConfigNetworkPtrline suppresses the unused variable warning, making the no-op less obvious.
- The function body creates local struct copies (
- Fix
- Either remove the function entirely (BaseURL already flows correctly through
p.networkConfigpassed toNewAnthropicProvider), or implement the intended logic to override the provider's BaseURL if that was the goal.
- Either remove the function entirely (BaseURL already flows correctly through
Artifacts
Supporting artifact from the T-Rex run
- Contains supporting evidence from the run (text/markdown; charset=utf-8).
|
All feedbacks gathered. Update will follow. |
fa15f50 to
ca190fc
Compare
ac30a53 to
7c66b20
Compare
44564de to
493bff0
Compare
244a01d to
ce1b2a6
Compare
Summary
This PR improves upon #4380 by introducing per-family upstream model management.
This effectively unlocks all models which aren't supplied with chat-completion endpoint.
It ties together and maps Anthropic/Gemini/Responses/OpenAI upstream endpoints and api styles correctly, so that bifrost can effectively serve them.
This unlocks these families:
Changes
Type of change
Affected areas
Build
Integration smoke test (requires OpenCode API key)
Verified with live Go API key:
opencode-go/usagewithprompt_tokens,completion_tokens,cached_tokens,reasoning_tokens[DONE]{"type":"error","error":{...}}) confirmed against Go API directlyScreenshots/Recordings
N/A — no visual UI changes beyond the provider icon (outline box shape, consistent with existing icon style).
Breaking changes
Related issues
None.
Security considerations
None that I'm aware of.
Checklist
docs/contributing/README.mdand followed the guidelines