feat: track OpenCode sessions (#26128)

This follows #26098 by teaching AI Bridge to read OpenCode session IDs
from the X-OpenCode-Session header, so OpenCode requests are grouped
consistently in interception logs.

Adds unit and integration test coverage for the new header.

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

This pull request was generated with Coder Agents assistance.
</details>
This commit is contained in:
Danny Kopping
2026-06-08 10:12:03 +02:00
committed by GitHub
parent 2211f9ce4b
commit 9afd3d0bea
3 changed files with 29 additions and 0 deletions
@@ -1049,6 +1049,16 @@ func TestSessionIDTracking(t *testing.T) {
"User-Agent": []string{"Zed/0.219.4+stable.119.abc123 (macos; aarch64)"},
},
},
{
name: "opencode",
fixture: fixtures.AntSimple,
expectedClient: aibridge.ClientOpenCode,
expectSessionID: "ses_15a48edefffe7oY0YcIHRv29dD",
header: http.Header{
"User-Agent": []string{"opencode/1.16.0 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14"},
"X-OpenCode-Session": []string{"ses_15a48edefffe7oY0YcIHRv29dD"},
},
},
}
for _, tc := range testCases {
+2
View File
@@ -75,6 +75,8 @@ func GuessSessionID(client Client, r *http.Request) *string {
return cleanRef(r.Header.Get("X-KILOCODE-TASKID"))
case ClientCoderAgents:
return cleanRef(r.Header.Get("X-Coder-Chat-Id"))
case ClientOpenCode:
return cleanRef(r.Header.Get("X-OpenCode-Session"))
case ClientCrush:
return nil // Crush does not send a session ID header.
case ClientRoo:
+17
View File
@@ -182,6 +182,23 @@ func TestGuessSessionID(t *testing.T) {
name: "coder_agents_without_chat_id",
client: aibridge.ClientCoderAgents,
},
// OpenCode.
{
name: "opencode_with_session_header",
client: aibridge.ClientOpenCode,
headers: map[string]string{"X-OpenCode-Session": "ses_15a48edefffe7oY0YcIHRv29dD"},
sessionID: utils.PtrTo("ses_15a48edefffe7oY0YcIHRv29dD"),
},
{
name: "opencode_with_whitespace_in_header",
client: aibridge.ClientOpenCode,
headers: map[string]string{"X-OpenCode-Session": " ses_15a48edefffe7oY0YcIHRv29dD "},
sessionID: utils.PtrTo("ses_15a48edefffe7oY0YcIHRv29dD"),
},
{
name: "opencode_without_session_header",
client: aibridge.ClientOpenCode,
},
// Crush.
{
name: "crush_returns_empty",