feat(llm): add OpenCode Go as a managed provider (chat-only, no Responses/CountTokens) - #933
feat(llm): add OpenCode Go as a managed provider (chat-only, no Responses/CountTokens)#933mkurkar wants to merge 2 commits into
Conversation
Adds support for OpenCode Go (https://opencode.ai/zen/go/v1), an OpenAI-compatible inference gateway that only serves /v1/chat/completions and rejects the auxiliary routes Bifrost normally pings. Default model is kimi-k3 (one of the 16 IDs the gateway actually serves — Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, Hy3). Backend (llm, bifrost): * llm/service_types.go: register ServiceTypeOpenCodeGo = "opencodego". * llm/providers.go: add opencodego to the OpenAI-compatible registry with DefaultModel = "kimi-k3". * llm/configuration.go: validate via APIKey only (URL auto-defaults). * bifrost/config.go: map opencodego to schemas.OpenAI, register it with IsSupported, extract applyProviderURLDefaults as a single source of truth shared by primary and fallback paths, and exclude opencodego from SupportsNativeTools (the gateway doesn't speak web_search). * bifrost/bifrost.go: wrap an OpenAI-base primary that does not speak the Responses API (opencodego, openaicompatible-with-Responses-off) under its own custom-provider slot 'openai::primary' with chatOnly set. GetConfigForProvider attaches AllowedRequests with only ChatCompletion / ChatCompletionStream = true, which implicitly refuses POSTs to /v1/responses and /v1/responses/input_tokens. Bifrost returns 'unsupported_operation' for CountTokens, which the plugin maps to llm.ErrUnsupportedTokenCount → llm.EstimateTokens fallback. b.provider now carries the registered name so req.Provider = b.provider targets the wrapped slot; this also lands on Bifrost's default chat arm (skipping normalizeReasoningEffort). Tests: * bifrost/bifrost_test.go: new TestNew_OpenCodeGoPrimaryIsWrapped AsChatOnlyCustom, TestNew_DirectOpenAIAndAzurePrimariesRemainStandard; extend TestProviderAccount_ChatOnlyCustomConfig with CountTokens. * bifrost/config_test.go: opencodego entries for SupportsNativeTools and FilterNativeTools. * llm/providers_test.go, llm/configuration_test.go: opencodego registry and IsValidService cases. * bifrost/bifrost_test.go: TestCountTokensOmitsMaxOutputTokens now sets UseResponsesAPI:true to reflect the production config for direct OpenAI (where the chat-only wrap does not fire). Webapp (System Console): * webapp/src/components/system_console/service.tsx: add 'opencodego' to the service-type dropdown, the display-name map ('OpenCode Go'), and isOpenAIType. API URL and Organization ID fields stay hidden (server-side defaults applied to URL; the gateway has no org concept). Docs: docs/providers.md would benefit from a follow-up entry for OpenCode Go but is not part of this commit.
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds OpenCode Go as a chat-only OpenAI-compatible service, including validation, provider registration, URL defaults, native-tool filtering, Bifrost wrapping, tests, and system console support. ChangesOpenCode Go integration
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Admin
participant ServiceConsole
participant ServiceConfig
participant Bifrost
participant OpenCodeGo
Admin->>ServiceConsole: select OpenCode Go and enter API key
ServiceConsole->>ServiceConfig: submit service configuration
ServiceConfig->>Bifrost: map provider and apply default URL
Bifrost->>Bifrost: register chat-only custom provider
Bifrost->>OpenCodeGo: send chat completion request
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ 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: 1
🤖 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 `@webapp/src/components/system_console/service.tsx`:
- Line 52: Update the service display-map entry and dropdown fallback for the
“opencodego” service to use intl.formatMessage(...) with an extracted message
instead of the hardcoded “OpenCode Go” string. Ensure both user-facing paths
reuse the i18n-derived label and remove the hardcoded values.
🪄 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: Repository UI (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 05d3ae02-8ec2-4213-a886-12e70e203d13
📒 Files selected for processing (10)
bifrost/bifrost.gobifrost/bifrost_test.gobifrost/config.gobifrost/config_test.gollm/configuration.gollm/configuration_test.gollm/providers.gollm/providers_test.gollm/service_types.gowebapp/src/components/system_console/service.tsx
The 'OpenCode Go' display label was hardcoded in two places — the mapServiceTypeToDisplayName entry and the dropdown fallback — so formatjs extract would miss it on regen and the label would never be localizable. Follow the existing scaleAIToDisplayName pattern: extract a dedicated helper that calls intl.formatMessage, and drop the hardcoded map entry + dropdown fallback in favor of the helper. webapp/src/i18n/en.json is regenerated by 'make i18n-extract' to include the new '4WZ6dc/F' message key.
Summary
Adds OpenCode Go as a first-class managed provider behind the existing OpenAI-compatible plumbing, registering it with a custom-provider slot whose
AllowedRequestsgate refuses every endpoint the gateway does not implement.Default model is
kimi-k3.Problem
OpenCode Go is an OpenAI-compatible inference gateway that exposes only
/v1/chat/completionsunderhttps://opencode.ai/zen/go/v1(Bearer auth).Routing the current
openaicompatibleconfig through Bifrost produced twohard failures, both of which crashed the parser or the gateway:
CountTokensalways posts a Responses-API request body to/v1/responses/input_tokens, which the gateway does not serve. The 404 HTML response page is not JSON; Bifrost's parser crashes.CountTokenssucceeded (it did not, see (1)), Bifrost would injectreasoning.effortinto the body for reasoning-model names, which the gateway rejects asinvalid_request_errorper its standard chat-completions contract.The chat stream itself works once both issues are bypassed. The gateway's 16 model IDs are lowercase-hyphenated (
kimi-k3,grok-4.5,glm-5.2,qwen3.7-max,deepseek-v4-pro, etc.); not the upstream-native names.Approach
Mirror the existing chat-only fallback wrap, but apply it on the primary path so
/v1/responses*is blocked there too:opencodegoas a newServiceTypein theopenAICompatibleProvidersregistry. Default model:kimi-k3.bifrost/bifrost.go::New): whencfg.Provider == schemas.OpenAI && !cfg.UseResponsesAPI, the primary is registered under a custom name (openai::primary) withchatOnly = true.GetConfigForProviderthen attachesAllowedRequests{ChatCompletion: true, ChatCompletionStream: true}—which (per
schemas.AllowedRequestssemantics) implicitly refuses every other operation includingCountTokens. Bifrost returnsunsupported_operationsynchronously; the plugin maps that tollm.ErrUnsupportedTokenCount, which falls through tollm.EstimateTokens.b.providernow stores the registered name (primaryEntry.registeredName()) instead of the bare base type. As a bonus this lands on Bifrost'sdefault:arm invendored.../providers/openai/chat.go(which skipsnormalizeReasoningEffort), doubling the chat-path protection.applyProviderURLDefaultsso the primary path (new) and theserviceConfigToFallbackEntrypath both injecthttps://opencode.ai/zen/go/v1foropencodego,https://api.cohere.ai/compatibility/v1forcohere, andhttps://api.mistral.ai/v1formistral.normalizeOpenAIBaseURLthen strips the trailing/v1so Bifrost can prepend/v1/chat/completions.SupportsNativeTools(ServiceTypeOpenCodeGo)returns
falseexplicitly even though the base type isschemas.OpenAI, the gateway does not implement native tools. Request-time filteringalready short-circuits, but the public predicate stays accurate for built-in-tool fallback gating.
bots/bots.goOpenCode Gocannot serve
/v1/audio/transcriptions, so it cannot be the transcript generator. Existing allowlist (OpenAI / OpenAI-Compatible / Azure) is intentionally narrower than the chat provider list.Files changed
llm/service_types.goServiceTypeOpenCodeGo = "opencodego".llm/providers.goDefaultModel: "kimi-k3".llm/configuration.goIsValidServicerequires APIKey only; URL defaults.bifrost/config.goMapServiceTypeToProvider→schemas.OpenAI,IsSupportedadds opencodego,applyProviderURLDefaultshelper,SupportsNativeToolsexcludes opencodego.bifrost/bifrost.goLLM.accountfield added for tests;providercarriesregisteredName().webapp/src/components/system_console/service.tsxisOpenAIType; API URL and Org ID fields stay hidden (matchesmistralUX).Tests
bifrost/bifrost_test.goTestNew_OpenCodeGoPrimaryIsWrappedAsChatOnlyCustom— assertsb.provider == "openai::primary", the bareschemas.OpenAIslot isnot registered, and
AllowedRequestsrejectsResponses/ResponsesStream/CountTokenswhile allowingChatCompletion/ChatCompletionStream.TestNew_DirectOpenAIAndAzurePrimariesRemainStandard— guardsthe inverse: real OpenAI/Azure primaries keep the bare base-type slot,
not a custom slot.
TestProviderAccount_ChatOnlyCustomConfigwithAllowedRequests.CountTokensassertion.bifrost/config_test.go— opencodego entries forSupportsNativeToolsand
FilterNativeToolsForServiceType.llm/providers_test.go,llm/configuration_test.go— registry andIsValidServicecases.bifrost/bifrost_test.go::TestCountTokensOmitsMaxOutputTokensupdated:synthetic
Provider: schemas.OpenAI, UseResponsesAPI: falseconfigs areno longer reachable from
NewFromServiceConfig(which forcestrue),so the test now uses the production value to keep exercising the
body-shape assertion instead of the chat-only gate.
All
./llm/... ./bifrost/... ./bots/... ./config/... ./api/...tests pass.Risk & follow-ups
cfg.Provider == schemas.OpenAI && !cfg.UseResponsesAPI, which also coversopenaicompatiblewith the operator toggle off. That is arguably the right behavior (same gateway pitfall) and is consistent with how the fallback path already worked, but it is a behavior change for any openaicompatible bot created before this PR. Worth a CHANGELOG entry.supportsModelFetchinginwebapp/src/components/system_console/service.tsxdoes not includeopencodego, so admins must type the model name. OpenCode Go does serve a/v1/modelslisting — adding it would be a one-liner behind a feature flag if desired.docs/providers.mdshould gain a short OpenCode Go section. Out of scope here; happy to follow up./v1/audio/transcriptions, the switch inbots/bots.go::GetTranscribecan grow a case.Verification
go test ./llm/... ./bifrost/... ./bots/... ./config/... ./api/...—green on Go 1.26.5.
make distproduces a 113 MB tar.gz with fivecross-compiled server binaries plus the webapp bundle that contains the
new "OpenCode Go" dropdown entry.
Screenshot
Summary by CodeRabbit
New Features
Bug Fixes