mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user