diff --git a/aibridge/client.go b/aibridge/client.go
index 68caffdd30..0d0a492338 100644
--- a/aibridge/client.go
+++ b/aibridge/client.go
@@ -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
}
diff --git a/aibridge/client_test.go b/aibridge/client_test.go
index 985c254a26..253a374a69 100644
--- a/aibridge/client_test.go
+++ b/aibridge/client_test.go
@@ -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",
diff --git a/aibridge/internal/integrationtest/bridge_internal_test.go b/aibridge/internal/integrationtest/bridge_internal_test.go
index 9c75108685..7a8ae1116f 100644
--- a/aibridge/internal/integrationtest/bridge_internal_test.go
+++ b/aibridge/internal/integrationtest/bridge_internal_test.go
@@ -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,
diff --git a/docs/ai-coder/ai-gateway/monitoring.md b/docs/ai-coder/ai-gateway/monitoring.md
index 7b9e680905..9b982fb0af 100644
--- a/docs/ai-coder/ai-gateway/monitoring.md
+++ b/docs/ai-coder/ai-gateway/monitoring.md
@@ -94,6 +94,7 @@ Available query filters:
- `Coder Agents`
- `Mux`
- `Cursor`
+ - `OpenCode`
- `Unknown`
diff --git a/site/src/pages/AIBridgePage/RequestLogsPage/icons/AIBridgeClientIcon.stories.tsx b/site/src/pages/AIBridgePage/RequestLogsPage/icons/AIBridgeClientIcon.stories.tsx
new file mode 100644
index 0000000000..f94c75c205
--- /dev/null
+++ b/site/src/pages/AIBridgePage/RequestLogsPage/icons/AIBridgeClientIcon.stories.tsx
@@ -0,0 +1,31 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { AIBridgeClientIcon } from "./AIBridgeClientIcon";
+
+const meta: Meta = {
+ title: "pages/AIBridgePage/AIBridgeClientIcon",
+ component: AIBridgeClientIcon,
+ args: {
+ className: "size-8",
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const OpenCode: Story = {
+ args: {
+ client: "OpenCode",
+ },
+};
+
+export const ClaudeCode: Story = {
+ args: {
+ client: "Claude Code",
+ },
+};
+
+export const Unknown: Story = {
+ args: {
+ client: "Unknown",
+ },
+};
diff --git a/site/src/pages/AIBridgePage/RequestLogsPage/icons/AIBridgeClientIcon.tsx b/site/src/pages/AIBridgePage/RequestLogsPage/icons/AIBridgeClientIcon.tsx
index 120a494979..780a2fa8b0 100644
--- a/site/src/pages/AIBridgePage/RequestLogsPage/icons/AIBridgeClientIcon.tsx
+++ b/site/src/pages/AIBridgePage/RequestLogsPage/icons/AIBridgeClientIcon.tsx
@@ -71,6 +71,13 @@ export const AIBridgeClientIcon = ({
className={cn(iconClassName, className)}
/>
);
+ case "OpenCode":
+ return (
+
+ );
case "Charm Crush":
return (