feat: add OpenCode AI Bridge client support (#26098)

Adds OpenCode to AI Bridge client detection so requests with user agents
like `opencode/1.16.0 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14`
show up as a first-class client instead of `Unknown`.

This also wires the existing OpenCode frontend asset into the AIBridge
UI, adds a Storybook story for the client icon, and updates the
monitoring docs list of supported client values.

<details>
<summary>Coder Agents generated</summary>

This pull request was generated by Coder Agents.

</details>
This commit is contained in:
Danny Kopping
2026-06-08 06:26:30 +02:00
committed by GitHub
parent 69f9b0e535
commit 47a8c9572f
6 changed files with 58 additions and 0 deletions
+3
View File
@@ -21,6 +21,7 @@ const (
ClientMux Client = "Mux"
ClientRoo Client = "Roo Code"
ClientCursor Client = "Cursor"
ClientOpenCode Client = "OpenCode"
ClientUnknown Client = "Unknown"
)
@@ -55,6 +56,8 @@ func GuessClient(r *http.Request) Client {
return ClientCrush
case r.Header.Get("x-cursor-client-version") != "":
return ClientCursor
case strings.HasPrefix(userAgent, "opencode/"):
return ClientOpenCode
}
return ClientUnknown
}
+5
View File
@@ -99,6 +99,11 @@ func TestGuessClient(t *testing.T) {
headers: map[string]string{"x-cursor-client-version": "abc123"},
wantClient: aibridge.ClientCursor,
},
{
name: "opencode",
userAgent: "opencode/1.16.0 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14",
wantClient: aibridge.ClientOpenCode,
},
{
name: "unknown_client",
userAgent: "ccclaude-cli/calude-with-wrong-prefix",
@@ -905,6 +905,17 @@ func TestSimple(t *testing.T) {
userAgent: "codex_cli_rs/0.87.0 (Mac OS 26.2.0; arm64)",
expectedClient: aibridge.ClientCodex,
},
{
name: config.ProviderOpenAI + "_opencode",
fixture: fixtures.OaiChatSimple,
basePath: "",
expectedPath: "/chat/completions",
getResponseIDFunc: getOpenAIResponseID,
path: pathOpenAIChatCompletions,
expectedMsgID: "chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N",
userAgent: "opencode/1.16.0 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14",
expectedClient: aibridge.ClientOpenCode,
},
{
name: config.ProviderAnthropic + "_baseURL_path",
fixture: fixtures.AntSimple,
+1
View File
@@ -94,6 +94,7 @@ Available query filters:
- `Coder Agents`
- `Mux`
- `Cursor`
- `OpenCode`
- `Unknown`
</details><br>
@@ -0,0 +1,31 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { AIBridgeClientIcon } from "./AIBridgeClientIcon";
const meta: Meta<typeof AIBridgeClientIcon> = {
title: "pages/AIBridgePage/AIBridgeClientIcon",
component: AIBridgeClientIcon,
args: {
className: "size-8",
},
};
export default meta;
type Story = StoryObj<typeof AIBridgeClientIcon>;
export const OpenCode: Story = {
args: {
client: "OpenCode",
},
};
export const ClaudeCode: Story = {
args: {
client: "Claude Code",
},
};
export const Unknown: Story = {
args: {
client: "Unknown",
},
};
@@ -71,6 +71,13 @@ export const AIBridgeClientIcon = ({
className={cn(iconClassName, className)}
/>
);
case "OpenCode":
return (
<ExternalImage
src="/icon/opencode.svg"
className={cn(iconClassName, className)}
/>
);
case "Charm Crush":
return (
<ExternalImage