Files
coder/aibridge/client.go
T
Atif Ali 7ff1278ab3 feat: track Junie as an AI Gateway client (#28266)
## Problem

Junie CLI works with AI Gateway, but every Junie session is recorded as
`Unknown`. Junie sends `User-Agent: Junie:SNAPSHOT` (observed directly
from the CLI), which `GuessClient` does not match, so usage, spend, and
audit views cannot attribute Junie traffic. There were also no docs for
pointing Junie at AI Gateway.

## Fix

- Detect the observed `junie:` user-agent prefix and add the `Junie`
client value.
- Add the Junie icon so sessions render with a logo instead of the
fallback question mark.
- Add a Junie client page and list it in the compatibility table.
- Add `Roo Code` and `Charm Crush` to the documented `client` filter
values. Both were already implemented but missing from the list.

## Supported configurations

| Provider  | API type          | Endpoint                 |
|-----------|-------------------|--------------------------|
| OpenAI    | `OpenAIResponses` | `/openai/v1/responses`   |
| Anthropic | `Anthropic`       | `/anthropic/v1/messages` |

BYOK works on both through `extraHeaders` with
`X-Coder-AI-Governance-Token`.

Preview:
https://coder.com/docs/@matifali-junie-ai-gateway-client/ai-coder/ai-gateway/clients/junie

> 🤖 This PR was created with the help of Coder Agents, and needs a human
review. 🧑‍💻
2026-08-19 21:13:49 +05:00

67 lines
2.5 KiB
Go

package aibridge
import (
"net/http"
"strings"
)
type Client string
const (
// Possible values for the "client" field in interception records.
// Must be kept in sync with documentation: https://github.com/coder/coder/blob/3cf867f84aa32d2febf7a26dc7e52be6beb8a2ac/docs/ai-coder/ai-gateway/monitoring.md?plain=1#L47-L57
ClientClaudeCode Client = "Claude Code"
ClientCodex Client = "Codex"
ClientZed Client = "Zed"
ClientCopilotVSC Client = "GitHub Copilot (VS Code)"
ClientCopilotCLI Client = "GitHub Copilot (CLI)"
ClientKilo Client = "Kilo Code"
ClientCoderAgents Client = "Coder Agents"
ClientCrush Client = "Charm Crush"
ClientMux Client = "Mux"
ClientRoo Client = "Roo Code"
ClientCursor Client = "Cursor"
ClientOpenCode Client = "OpenCode"
ClientJunie Client = "Junie"
ClientUnknown Client = "Unknown"
)
// GuessClient attempts to guess the client application from the request headers.
// Not all clients set proper user agent headers, so this is a best-effort approach.
// Based on https://github.com/coder/aibridge/issues/20#issuecomment-3769444101.
func GuessClient(r *http.Request) Client {
userAgent := strings.ToLower(r.UserAgent())
originator := r.Header.Get("originator")
// Must be kept in sync with documentation: https://github.com/coder/coder/blob/3cf867f84aa32d2febf7a26dc7e52be6beb8a2ac/docs/ai-coder/ai-gateway/monitoring.md?plain=1#L47-L57
switch {
case strings.HasPrefix(userAgent, "mux/"):
return ClientMux
case strings.HasPrefix(userAgent, "claude"):
return ClientClaudeCode
case strings.HasPrefix(userAgent, "codex"):
return ClientCodex
case strings.HasPrefix(userAgent, "zed/"):
return ClientZed
case strings.HasPrefix(userAgent, "githubcopilotchat/"):
return ClientCopilotVSC
case strings.HasPrefix(userAgent, "copilot/"):
return ClientCopilotCLI
case strings.HasPrefix(userAgent, "kilo-code/") || originator == "kilo-code":
return ClientKilo
case strings.HasPrefix(userAgent, "roo-code/") || originator == "roo-code":
return ClientRoo
case strings.HasPrefix(userAgent, "coder-agents/"):
return ClientCoderAgents
case strings.HasPrefix(userAgent, "charm crush/") || strings.HasPrefix(userAgent, "charm-crush/"):
return ClientCrush
case r.Header.Get("x-cursor-client-version") != "":
return ClientCursor
case strings.HasPrefix(userAgent, "opencode/"):
return ClientOpenCode
case strings.HasPrefix(userAgent, "junie:"):
return ClientJunie
}
return ClientUnknown
}