Skip to content

Unlock and correcly mapp all OpenCode Zen models - #4468

Open
neta79 wants to merge 6 commits into
maximhq:devfrom
immemora:provider/opencode
Open

Unlock and correcly mapp all OpenCode Zen models#4468
neta79 wants to merge 6 commits into
maximhq:devfrom
immemora:provider/opencode

Conversation

@neta79

@neta79 neta79 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

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:

  • Gemini
  • Qwen
  • Claude

Changes

  • Adds model mapping logic
  • Borrows logic from Anthropic connector by boxying it without any code changes

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

Build

# Go workspace
go work init ./core ./framework ./transports

# Provider package
cd core && go build ./providers/opencode/... && go vet ./providers/opencode/...

# Unit tests (100 tests — constructors + 49 unsupported ops × 2 providers)
go test ./providers/opencode/... -v

# Transport binary
go build -o /tmp/bifrost ./transports/bifrost-http/

# UI
cd ui && npm run build

Integration smoke test (requires OpenCode API key)

// config.json
{
  "general": { "port": 8080 },
  "providers": {
    "opencode-go": {
      "keys": [{"value": "sk-...", "models": ["*"]}]
    }
  }
}
# Start
/tmp/bifrost -app-dir /path/to/config

# Models list
curl -s http://localhost:8080/v1/models -H "Authorization: Bearer sk-..."

# Chat completion
curl -s http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Say exactly: hello world"}],"max_tokens":20}'

# Streaming
curl -s http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Say hi"}],"max_tokens":20,"stream":true}'

Verified with live Go API key:

  • Models list returns Go-eligible models prefixed opencode-go/
  • Chat returns content + reasoning + usage with prompt_tokens, completion_tokens, cached_tokens, reasoning_tokens
  • SSE streaming works: reasoning deltas → content deltas → final usage chunk → [DONE]
  • Error format ({"type":"error","error":{...}}) confirmed against Go API directly

Screenshots/Recordings

N/A — no visual UI changes beyond the provider icon (outline box shape, consistent with existing icon style).

Breaking changes

  • Yes
  • No

Related issues

None.

Security considerations

None that I'm aware of.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate (100 tests, interface check)
  • I updated documentation where needed (provider doc + docs.json nav)
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable (N/A — requires upstream CI secrets)

G-XD and others added 6 commits June 14, 2026 09:57
* 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.
@neta79
neta79 requested a review from a team as a code owner June 16, 2026 16:03
@CLAassistant

CLAassistant commented Jun 16, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
3 out of 4 committers have signed the CLA.

✅ neta79
✅ webagil-kevin
✅ G-XD
❌ Pratham-Mishra04
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds two new OpenCode providers (opencode-go, opencode-zen) to Bifrost with per-model adapter routing (OpenAI-compatible, Anthropic, Gemini). Fixes governance routing-pin propagation via a new BifrostContextKeyRoutingPinnedAPIKeyID context key committed in RunPreRequestHooks. Extends compat dropparams to preserve MaxOutputTokens when equivalent token-cap spellings are supported. Updates load-test scripts.

Changes

OpenCode Provider and Routing-Pin Infrastructure

Layer / File(s) Summary
Schema constants, provider identifiers, and routing-pin context key
core/schemas/bifrost.go
Adds OpencodeGo/OpencodeZen ModelProvider constants to StandardProviders, introduces BifrostContextKeyRoutingPinnedAPIKeyID context key, and applies whitespace alignment to BifrostResponseExtraFields fields.
Governance routing-pin write and bifrost core commit logic
plugins/governance/main.go, plugins/governance/routing_test.go, core/bifrost.go, core/bifrost_test.go
applyRoutingRules writes KeyID to the new non-reserved BifrostContextKeyRoutingPinnedAPIKeyID. PluginPipeline.RunPreRequestHooks unblocks restricted writes then reads and commits the pin into BifrostContextKeyAPIKeyID. Tests cover pin commit, override of caller-supplied key, and no-pin preservation.
OpenCode routing tables, types, and validation
core/providers/opencode/routing.go, core/providers/opencode/types.go, core/providers/opencode/validation.go
Defines adapter/auth/match-kind constants, exact-match maps for Zen/Go model IDs, prefix-based class routing, resolveRoute/buildResolvedRoute resolver, routeExecutionMetadata struct, and validateChatRoute/validateResponsesRoute helpers.
OpenCode error envelope parsing
core/providers/opencode/errors.go
Defines opencodeErrorBody/opencodeErrorInner structs and parseOpencodeError, which overlays Opencode's nested message/type into BifrostError with a non-blank fallback.
opencodeProvider struct, constructors, and main dispatch
core/providers/opencode/opencode.go
Introduces opencodeProvider with network config, streaming client, and raw-passthrough flags. NewOpencodeZenProvider/NewOpencodeGoProvider initialize clients. ChatCompletion/Responses variants resolve adapter, validate route, and dispatch to adapter-specific execute functions.
OpenAI, Anthropic, and Gemini adapter implementations
core/providers/opencode/openaiadapter.go, core/providers/opencode/anthropicadapter.go, core/providers/opencode/geminiadapter.go
Implements non-streaming and streaming execute methods for all three adapters, constructing provider instances, setting URL path context values, forwarding raw-passthrough flags, and wiring post-hook callbacks.
Unsupported operation stubs
core/providers/opencode/cachedcontents.go, core/providers/opencode/opencode.go
All cached-content CRUD/list methods and bulk operations (embeddings, rerank, OCR, speech, transcription, batch, files, containers, passthrough) return standardized unsupported-operation errors.
Provider wiring, config schema, UI registration, and docs
core/bifrost.go, transports/config.schema.json, ui/lib/constants/config.ts, ui/lib/constants/icons.tsx, ui/lib/constants/logs.ts, docs/docs.json, docs/providers/supported-providers/opencode.mdx
Wires opencode constructors into createBaseProvider. Config schema and UI constants register the two new provider keys. Docs page covers setup, chat completions, responses API, model listing, pricing, and caveats.
OpenCode provider and routing tests
core/providers/opencode/opencode_test.go, core/providers/opencode/routing_test.go
Compile-time Provider interface check, constructor URL/key tests, table-driven unsupported-operation assertions, and TestResolveRoute covering exact, prefix, and fallback routing cases.

Compat Plugin max_output_tokens Fix

Layer / File(s) Summary
MaxOutputTokens preservation logic and tests
plugins/compat/dropparams.go, plugins/compat/dropparams_test.go
dropUnsupportedParams preserves MaxOutputTokens when any of max_output_tokens, max_tokens, or max_completion_tokens is supported. Three tests cover table-driven preserve/drop cases, a mixed same-request case, and a chat regression guard.

Load-Test Script Fixes

Layer / File(s) Summary
Load-test script build, clone, and config fixes
.github/workflows/scripts/load-test.sh
build_bifrost_http adds ui/.gitkeep placeholder; setup_mocker fixes clone working directory; create_config adds models:["*"] and removes custom_provider_config.allowed_requests from the mocker config.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • maximhq/bifrost#4358: Contains the same load-test script adjustments (ui .gitkeep, mocker clone working-directory fix, models: ["*"], removal of allowed_requests).
  • maximhq/bifrost#4359: Directly overlaps with the routing-pin commit fix in RunPreRequestHooks and the new BifrostContextKeyRoutingPinnedAPIKeyID context key.
  • maximhq/bifrost#4362: Modifies the same plugins/compat/dropparams.go Responses-API MaxOutputTokens preservation logic and tests.

Suggested reviewers

  • akshaydeo
  • danpiths

Poem

🐇 A rabbit hops through provider land,
Where OpenCode Zen and Go now stand.
Routing pins no longer slip away,
Max output tokens get to stay.
With adapters three and errors neat,
This gateway build is now complete! 🎉

🚥 Pre-merge checks | ❌ 5

❌ Failed checks (2 warnings, 3 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The linked issue #123 concerns Files API support for OpenAI/Anthropic, but the PR implements per-family upstream routing for OpenCode providers to unlock Gemini, Qwen, and Claude models. The PR description references issue #4380 (not linked) as the actual predecessor, suggesting the linked issue is tangential to the PR objectives. Verify and link the correct issue (#4380 or equivalent) that this PR builds upon, or clarify how Files API support (issue #123) relates to the routing and adapter implementation in this changeset.
Docstring Coverage ⚠️ Warning Docstring coverage is 55.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Unlock and correcly mapp all OpenCode Zen models' contains a typo ('correcly' instead of 'correctly') and is unclear about the actual scope. The PR adds both Zen and Go provider support with per-family routing, not just 'Zen models'. The title is only partially related to the main objective. Revise the title to accurately reflect the full scope, such as 'Add OpenCode Zen and Go providers with per-family upstream routing' and fix the typo in 'correctly'.
Out of Scope Changes check ❓ Inconclusive The changeset consistently implements per-family upstream routing, adapter selection, and model management for OpenCode providers with supporting tests. However, load-test script changes (.github/workflows/scripts/load-test.sh) introducing UI placeholders and config modifications appear tangential to the core routing feature and lack clear justification in the PR description. Clarify the purpose of load-test script modifications and their relationship to the OpenCode provider feature, or consider moving them to a separate chore/infrastructure PR.
Description check ❓ Inconclusive PR description provides summary, changes overview, and test instructions but lacks several required template sections. Complete the PR description by filling out: specific design decisions/trade-offs under Changes, all affected area checkboxes, detailed test validation steps, and confirm breaking changes status. Consider linking issue #4380 mentioned in summary.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
coderabbitai Bot requested review from akshaydeo and danpiths June 16, 2026 16:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Keep runtime base-provider validation in sync with the schema.

transports/config.schema.json now allows opencode-go and opencode-zen as custom_provider_config.base_provider_type, but SupportedBaseProviders still 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.json is 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

setOpencodeAnthropicBaseURL is 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

📥 Commits

Reviewing files that changed from the base of the PR and between fa9d8f0 and 034d3af.

📒 Files selected for processing (25)
  • .github/workflows/scripts/load-test.sh
  • core/bifrost.go
  • core/bifrost_test.go
  • core/providers/opencode/anthropicadapter.go
  • core/providers/opencode/cachedcontents.go
  • core/providers/opencode/errors.go
  • core/providers/opencode/geminiadapter.go
  • core/providers/opencode/openaiadapter.go
  • core/providers/opencode/opencode.go
  • core/providers/opencode/opencode_test.go
  • core/providers/opencode/routing.go
  • core/providers/opencode/routing_test.go
  • core/providers/opencode/types.go
  • core/providers/opencode/validation.go
  • core/schemas/bifrost.go
  • docs/docs.json
  • docs/providers/supported-providers/opencode.mdx
  • plugins/compat/dropparams.go
  • plugins/compat/dropparams_test.go
  • plugins/governance/main.go
  • plugins/governance/routing_test.go
  • transports/config.schema.json
  • ui/lib/constants/config.ts
  • ui/lib/constants/icons.tsx
  • ui/lib/constants/logs.ts

Comment on lines +124 to +126
default:
return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRequestMarshal, context.DeadlineExceeded)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +195 to +199
func buildResolvedRoute(spec routeSpec, matchedBy routeMatchKind, classPrefix, modelID string) resolvedRoute {
path := spec.path
if strings.Contains(path, "{model}") {
path = strings.ReplaceAll(path, "{model}", modelID)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +53 to +134
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
<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

@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 2/5

I don't think this is safe to merge yet.

  • Nil Opencode requests can panic before validation returns a Bifrost error.
  • Claude, Qwen, and Gemini Opencode routes can send provider-specific auth headers instead of gateway auth.
  • Pre-request hook panics can leave restricted context writes blocked after recovery.
  • The Anthropic adapter contains a no-op helper that looks like it changes base URL behavior but does not.

Focus on core/providers/opencode/opencode.go, the Opencode adapter files, and core/bifrost.go.

T-Rex T-Rex Logs

What T-Rex did

  • Checked the changed Opencode provider files for route selection, adapter delegation, validation, error parsing, and URL construction.
  • Performed focused Go validation, but the local Go toolchain was older than the version required by the module.
  • Read all changed files in core/providers/opencode: routing.go, opencode.go, anthropicadapter.go, geminiadapter.go, openaiadapter.go, errors.go, validation.go, and types.go.
  • Identified setOpencodeAnthropicBaseURL as dead code, which creates local copies, suppresses unused warning with _, and returns an unmodified provider.
  • Verified this is not a runtime-breaking issue because the BaseURL flows through the p.networkConfig copy passed to NewAnthropicProvider.
  • Attempted Go compilation and test execution but go.mod requires Go 1.26.4, which is unavailable in the sandbox (Go 1.24.4).
  • Analyzed routing logic, URL construction, and auth style propagation for all four adapter kinds by code inspection.
  • Verified Gemini path template substitution produces correct URLs, such as /v1/models/gemini-3-flash:generateContent.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
core/providers/opencode/opencode.go Adds the shared Opencode provider entry points and operation support matrix.
core/providers/opencode/routing.go Adds per-model and per-family route metadata for OpenCode Zen and Go.
core/providers/opencode/anthropicadapter.go Delegates Claude/Qwen-style Opencode routes to the Anthropic provider implementation.
core/providers/opencode/geminiadapter.go Delegates Gemini-style Opencode routes to the Gemini provider implementation.
core/bifrost.go Registers the new providers and changes pre-request hook context write handling.

Reviews (1): Last reviewed commit: "feat(opencode): route models by adapter ..." | Re-trigger Greptile

Comment on lines +112 to +116
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Guard nil requests

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.

Suggested change
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
}

Comment on lines +16 to +22
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Apply gateway auth

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.

Comment on lines +16 to +23
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Apply gateway auth

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.

Comment thread core/bifrost.go
Comment on lines 6859 to +6883
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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Restore deferred unblock

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Remove dead base URL override

  • setOpencodeAnthropicBaseURL copies 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).

View artifacts

T-Rex Ran code and verified through T-Rex

return provider.ResponsesStream(ctx, postHookRunner, postHookSpanFinalizer, key, request)
}

func setOpencodeAnthropicBaseURL(provider *anthropic.AnthropicProvider, baseURL string) *anthropic.AnthropicProvider {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 setOpencodeAnthropicBaseURL is dead code — never modifies the Anthropic provider

  • Bug
    • The function setOpencodeAnthropicBaseURL in 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.
  • Cause
    • The function body creates local struct copies (providerConfig := *provider) and a pointer to the copy, but never assigns to any field. The _ = providerConfigNetworkPtr line suppresses the unused variable warning, making the no-op less obvious.
  • Fix
    • Either remove the function entirely (BaseURL already flows correctly through p.networkConfig passed to NewAnthropicProvider), or implement the intended logic to override the provider's BaseURL if that was the goal.
Artifacts

Supporting artifact from the T-Rex run

  • Contains supporting evidence from the run (text/markdown; charset=utf-8).

View artifacts

T-Rex Ran code and verified through T-Rex

@neta79

neta79 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

All feedbacks gathered. Update will follow.

@akshaydeo
akshaydeo force-pushed the dev branch 3 times, most recently from fa15f50 to ca190fc Compare June 21, 2026 11:44
@akshaydeo
akshaydeo force-pushed the dev branch 2 times, most recently from ac30a53 to 7c66b20 Compare July 1, 2026 12:24
@akshaydeo
akshaydeo force-pushed the dev branch 2 times, most recently from 44564de to 493bff0 Compare July 18, 2026 01:10
@akshaydeo
akshaydeo force-pushed the dev branch 3 times, most recently from 244a01d to ce1b2a6 Compare August 13, 2026 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants