Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions src/server/claude-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ import {
import { clearableDeadline, idleDeadline } from "../lib/abort";
import { estimateTokens } from "../lib/token-estimate";
import { NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
import { registryEntryForProviderDestination } from "../providers/registry";
import { evidenceFromBody } from "../routing/request-evidence";
import { resolveWireProtocolOverride } from "./adapter-resolve";
import type { OcxConfig } from "../types";
import { readJsonRequestBody } from "./request-decompress";
import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext, type RequestLogEntry } from "./request-log";
import { conversationIdFromClaudeMetadata } from "./request-log-conversation";
import { conversationIdFromClaudeMetadata, normalizeLogConversationId, sessionLaneIdFromRequest } from "./request-log-conversation";
import { responseWithDeferredRequestLog } from "./relay";
import { handleResponses } from "./responses";
import {
Expand Down Expand Up @@ -786,8 +787,12 @@ async function handleClaudeMessagesWithBudget(
// bodies: it 400s on sampling params ("Unsupported parameter: max_output_tokens",
// verified live 2026-07-11). Strip them for that route; routed providers keep them.
let nativeRoute = false;
let opencodeGoRoute = false;
try {
const route = routeModel(config, internalBody.model as string, evidenceFromBody(internalBody));
// Match the fixed key-auth destination before per-model wire overrides, including
// renamed Go providers without treating custom or lookalike URLs as Go.
opencodeGoRoute = registryEntryForProviderDestination(route.provider)?.id === "opencode-go";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive Go affinity from the final combo target

When a Claude request names a combo using the random strategy, this preflight routeModel call and handleComboResponses' separate pick at src/server/responses/core.ts:2618 can select different targets. If preflight selects a non-Go key provider but dispatch selects OpenCode Go, opencodeGoRoute remains false and no metadata-derived session lane is added, so Go affinity is intermittently lost. Carry the validated metadata identity through the replay and apply it after the combo/fallback target is finalized, and add a combo regression case.

Useful? React with 👍 / 👎.

// Settle the wire once so the sampling decision below reads the effective
// adapter rather than the provider-wide default (#404).
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic");
Expand Down Expand Up @@ -851,15 +856,27 @@ async function handleClaudeMessagesWithBudget(
headers.set("chatgpt-account-id", token.chatgptAccountId);
}
}
if (nativeRoute) {
if (opencodeGoRoute) {
const session = req.headers.get("x-opencode-session");
if (session) headers.set("x-opencode-session", session);
}
const hasExplicitGoSession = opencodeGoRoute
&& (sessionLaneIdFromRequest(headers) !== undefined
|| normalizeLogConversationId(headers.get("x-opencode-session")) !== undefined);
const synthesizeGoSession = opencodeGoRoute && !hasExplicitGoSession
&& isRec(anthropicBody)
&& conversationIdFromClaudeMetadata(isRec(anthropicBody.metadata) ? anthropicBody.metadata : undefined) !== undefined;
// Go can also use the Responses adapter; its eligibility gate must win on both wires.
if (opencodeGoRoute ? synthesizeGoSession : nativeRoute) {
// ChatGPT-backend prompt-cache affinity rides the session_id HEADER (codex
// clients always send their session uuid; devlog 090 follow-up: body-level
// prompt_cache_key alone still yielded cached_tokens:0). Claude Code never sends
// the header, so synthesize a stable per-session uuid from the same cache key —
// the header, so synthesize a stable per-session uuid from the same cache key.
// Routed Go requests need this lane too for their x-opencode-session affinity —
// but ONLY for a real per-session key (metadata.user_id). The system-hash fallback
// key is shared across Desktop conversations, and a shared session_id's backend
// semantics are unproven (audit 133 R2#3): body prompt_cache_key only there.
if (cacheKeySource === "metadata" && !headers.has("session_id") && typeof internalBody.prompt_cache_key === "string") {
if (cacheKeySource === "metadata" && (synthesizeGoSession || !headers.has("session_id")) && typeof internalBody.prompt_cache_key === "string") {
headers.set("session_id", uuidFromHex(internalBody.prompt_cache_key));
}
}
Expand Down
141 changes: 138 additions & 3 deletions tests/providers/opencode-go-session-header.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { resolveOpenCodeGoTransport } from "../../src/providers/opencode-go-tran
import { getProviderRegistryEntry } from "../../src/providers/registry";
import { handleResponses } from "../../src/server/responses/core";
import { handleChatCompletions } from "../../src/server/chat-completions";
import { handleClaudeMessages } from "../../src/server/claude-messages";
import type { OcxConfig, OcxProviderConfig } from "../../src/types";

const MUSE_MODEL = "muse-spark-1.3-contributor";
Expand All @@ -25,7 +26,14 @@ function codexHeaders(child = "child-thread-a"): Record<string, string> {
};
}

function upstreamResponse(url: string): Response {
function upstreamResponse(url: string, stream = false): Response {
if (stream && url.endsWith("/chat/completions")) {
return new Response([
`data: ${JSON.stringify({ choices: [{ index: 0, delta: { role: "assistant", content: "ok" } }] })}\n\n`,
`data: ${JSON.stringify({ choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1 } })}\n\n`,
"data: [DONE]\n\n",
].join(""), { headers: { "content-type": "text/event-stream" } });
}
if (url.endsWith("/responses")) {
return Response.json({
id: "resp_opencode_go_session",
Expand Down Expand Up @@ -54,6 +62,8 @@ async function captureRequest(input: {
child?: string;
provider?: OcxProviderConfig;
nativeChat?: boolean;
claude?: boolean;
metadataUserId?: string;
headers?: Record<string, string>;
} = {}): Promise<{ url: string; headers: Headers }> {
const providerName = input.providerName ?? "opencode-go";
Expand All @@ -62,13 +72,26 @@ async function captureRequest(input: {
globalThis.fetch = (async (requestInput: RequestInfo | URL, init?: RequestInit) => {
const url = String(requestInput);
requests.push({ url, headers: new Headers(init?.headers) });
return upstreamResponse(url);
return upstreamResponse(url, input.claude);
}) as typeof fetch;

const config = {
providers: { [providerName]: input.provider ?? opencodeGo() },
} as unknown as OcxConfig;
const response = input.nativeChat ? await handleChatCompletions(
const response = input.claude ? await handleClaudeMessages(
new Request("http://localhost/v1/messages", {
method: "POST",
headers: input.headers ?? { "content-type": "application/json" },
body: JSON.stringify({
model: `${providerName}/${model}`, max_tokens: 64, stream: false,
system: "A shared system prompt is not a conversation identifier.",
messages: [{ role: "user", content: "ping" }],
...(input.metadataUserId !== undefined ? { metadata: { user_id: input.metadataUserId } } : {}),
}),
}),
config,
{ model: "", provider: "" },
) : input.nativeChat ? await handleChatCompletions(
new Request("http://localhost/v1/chat/completions", {
method: "POST",
headers: input.headers ?? codexHeaders(input.child),
Expand Down Expand Up @@ -97,6 +120,118 @@ describe("OpenCode Go session affinity (#3344)", () => {
const originalFetch = globalThis.fetch;
afterEach(() => { globalThis.fetch = originalFetch; });

test("Claude metadata gives stable Go affinity across turns and distinct conversations", async () => {
const input = { claude: true, model: CHAT_MODEL, metadataUserId: "user_test_account__session_conversation-a" };
const first = await captureRequest(input);
const continued = await captureRequest(input);
const next = await captureRequest({ ...input, metadataUserId: "user_test_account__session_conversation-b" });
expect(first.url).toBe("https://opencode.ai/zen/go/v1/chat/completions");
// Fixed SHA-256 vectors calculated independently of the production helpers.
expect(first.headers.get(SESSION_HEADER)).toBe("ocx_a89540229ef781fd5f7adf92a711b436");
expect(continued.headers.get(SESSION_HEADER)).toBe("ocx_a89540229ef781fd5f7adf92a711b436");
expect(next.headers.get(SESSION_HEADER)).toBe("ocx_55fec02e7f2c7f9358958ab6d1589530");
expect(first.headers.get(SESSION_HEADER)).not.toContain("conversation-a");
});

test("Claude recognizes renamed canonical Go destinations and omits shared system affinity", async () => {
const input = { claude: true, model: CHAT_MODEL, providerName: "renamed-go" };
const metadata = await captureRequest({ ...input, metadataUserId: "user_test_account__session_conversation-a" });
const desktop = await captureRequest(input);
expect(metadata.headers.get(SESSION_HEADER)).toBe("ocx_a89540229ef781fd5f7adf92a711b436");
expect(desktop.headers.has(SESSION_HEADER)).toBe(false);
});

test("Claude explicit Go header precedes metadata and matches native Chat affinity", async () => {
const headers = { "content-type": "application/json", [SESSION_HEADER]: "client-session-a" };
const claude = await captureRequest({ claude: true, model: CHAT_MODEL, headers, metadataUserId: "different-metadata-session" });
const chat = await captureRequest({ nativeChat: true, model: CHAT_MODEL, headers });
expect(claude.headers.get(SESSION_HEADER)).toBe("ocx_516d593899f34b7baca2db37c7b0c8c5");
expect(chat.headers.get(SESSION_HEADER)).toBe("ocx_516d593899f34b7baca2db37c7b0c8c5");
});

test("Claude affinity survives per-model Responses wire selection", async () => {
const input = { claude: true, metadataUserId: "user_test_account__session_conversation-a" };
const chat = await captureRequest({ ...input, model: CHAT_MODEL });
const responses = await captureRequest({ ...input, model: MUSE_MODEL });
expect(responses.url).toBe("https://opencode.ai/zen/go/v1/responses");
expect(chat.headers.get(SESSION_HEADER)).toBe("ocx_a89540229ef781fd5f7adf92a711b436");
expect(responses.headers.get(SESSION_HEADER)).toBe("ocx_a89540229ef781fd5f7adf92a711b436");
const explicit = await captureRequest({
...input, model: MUSE_MODEL,
headers: { "content-type": "application/json", [SESSION_HEADER]: "client-session-a" },
});
expect(explicit.headers.get(SESSION_HEADER)).toBe("ocx_516d593899f34b7baca2db37c7b0c8c5");
});

for (const [model, url] of [
[CHAT_MODEL, "https://opencode.ai/zen/go/v1/chat/completions"],
[MUSE_MODEL, "https://opencode.ai/zen/go/v1/responses"],
] as const) {
test(`Claude ${model} falls back to valid metadata after invalid explicit Go identity`, async () => {
// Interior tab is constructible in HTTP Headers but rejected by the identity owner.
for (const session of ["", " ", "invalid\tidentity", "x".repeat(4097)]) {
const captured = await captureRequest({
claude: true, model, metadataUserId: "user_test_account__session_conversation-a",
headers: { "content-type": "application/json", [SESSION_HEADER]: session },
});
expect(captured.url).toBe(url);
expect(captured.headers.get(SESSION_HEADER)).toBe("ocx_a89540229ef781fd5f7adf92a711b436");
const invalidLane = await captureRequest({
claude: true, model, metadataUserId: "user_test_account__session_conversation-a",
headers: { "content-type": "application/json", session_id: session },
});
expect(invalidLane.url).toBe(url);
expect(invalidLane.headers.get(SESSION_HEADER)).toBe("ocx_a89540229ef781fd5f7adf92a711b436");
}
});

test(`Claude ${model} omits Go affinity without usable metadata identity`, async () => {
for (const metadataUserId of [undefined, "", " \t\n ", "invalid\u0000identity", "x".repeat(4097)]) {
const captured = await captureRequest({ claude: true, model, metadataUserId });
expect(captured.url).toBe(url);
expect(captured.headers.has(SESSION_HEADER)).toBe(false);
expect(captured.headers.has("session_id")).toBe(false);
}
});

test(`Claude ${model} keeps explicit and operator identity with empty metadata`, async () => {
const input = {
claude: true, model, metadataUserId: "",
headers: { "content-type": "application/json", [SESSION_HEADER]: " client-session-a " },
};
const explicit = await captureRequest(input);
expect(explicit.url).toBe(url);
expect(explicit.headers.get(SESSION_HEADER)).toBe("ocx_516d593899f34b7baca2db37c7b0c8c5");
const operator = await captureRequest({ ...input, provider: opencodeGo({ headers: { "X-OpenCode-Session": "operator-session" } }) });
expect(operator.url).toBe(url);
expect(operator.headers.get(SESSION_HEADER)).toBe("operator-session");
});

test(`Claude ${model} preserves explicit session lanes and operator header precedence`, async () => {
for (const laneHeader of ["session_id", "session-id", "thread-id", "x-codex-parent-thread-id"]) {
const headers = { "content-type": "application/json", [laneHeader]: "native-client-session", [SESSION_HEADER]: "different-fallback" };
const input = { claude: true, model, headers, metadataUserId: "different-metadata-session" };
const claude = await captureRequest(input);
expect(claude.url).toBe(url);
expect(claude.headers.get(SESSION_HEADER)).toBe("ocx_a197dbb87311c29a5fbe51140e3845ce");
const operator = await captureRequest({ ...input, provider: opencodeGo({ headers: { "X-OpenCode-Session": "operator-session" } }) });
expect(operator.url).toBe(url);
expect(operator.headers.get(SESSION_HEADER)).toBe("operator-session");
}
});
}

test("Claude does not add Go affinity to custom or lookalike destinations", async () => {
for (const baseUrl of ["https://custom.example/v1", "https://opencode.ai.evil.test/zen/go/v1"]) {
const captured = await captureRequest({
claude: true, model: CHAT_MODEL, providerName: "custom-go",
provider: opencodeGo({ baseUrl }), metadataUserId: "user_test_account__session_conversation-a",
headers: { "content-type": "application/json", [SESSION_HEADER]: "client-session-a" },
});
expect(captured.headers.has(SESSION_HEADER)).toBe(false);
}
});

test("native Chat ingress preserves stable Go affinity and separates conversations", async () => {
const provider = opencodeGo();
const input = { nativeChat: true, model: "omen-alpha", provider };
Expand Down
Loading