mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
* fix(opencode): session stability, free-tier routing, and CLI defaults - Wire generateSessionId() into opencodeHeaders so x-opencode-session is a deterministic fingerprint instead of randomUUID() per request, enabling upstream prompt caching across a conversation - Thread request body through buildHeaders() so session fingerprint has access to model, system, messages, and tools - Default CLI header synthesis to ON (opt-out via false), align values with 9router proven defaults (opencode/desktop/global) - Auto-echo listing-valid model names for noAuth providers so response.model matches /v1/models listing - Short-circuit free-tier model resolution to opencode provider first to prevent prefix inference misrouting when catalog is unreachable * fix(opencode): make free-tier default flip self-consistent + add coverage PR #10571 flipped OPENCODE_SYNTHESIZE_CLI_HEADERS to on-by-default and changed the synthesized UA/client/project default values, but shipped with 2 broken assertions in the existing #5997 regression test and no coverage for the new session-fingerprinting, free-tier routing, or noAuth echoModel logic (Hard Rule #18). - Update tests/unit/opencode-cli-headers-synthesis-5997.test.ts to match the new on-by-default behavior and new default values; add an explicit opt-out coverage test so the forward-only path is still guarded. - Fix 20 further test failures in tests/unit/opencode-executor.test.ts and tests/unit/refactor-buildHeaders-opencode.test.ts caused by the same default flip (pin OPENCODE_SYNTHESIZE_CLI_HEADERS=false for the characterization suites that predate #10571; use a genuinely CLI-looking UA where the preserved-UA test requires one). - Fix a real bug found via TDD while adding the mandated free-tier routing regression test: the big-pickle/*-free short-circuit in open-sse/services/model.ts checked activeProviders?.has("opencode") literally, but getActiveProviderSet() canonicalizes every connection's provider id through resolveProviderAlias(), which rewrites "opencode" to "opencode-zen" via a manual override — so an active no-auth opencode connection could never satisfy the check. Now checks both opencode-family candidate ids. Proven with a test that fails on the original code and passes with the fix (both connections active with a stale synced catalog omitting big-pickle). - Extract the noAuth-provider echoModel aliasing in chatCore.ts into a pure, directly-testable helper (open-sse/handlers/chatCore/noAuthEchoModel.ts), matching the existing chatCore god-file decomposition pattern. - Add regression tests for generateSessionId()-based x-opencode-session fingerprinting (stable within a conversation, changes on model/message changes), the free-tier routing short-circuit, and the noAuth echoModel aliasing. - Add the changelog.d/ fragment and sync docs/reference/ENVIRONMENT.md's OPENCODE_SYNTHESIZE_CLI_HEADERS/OPENCODE_USER_AGENT/OPENCODE_CLIENT/ OPENCODE_PROJECT rows to the new defaults. Does NOT resolve whether flipping OPENCODE_SYNTHESIZE_CLI_HEADERS's default was the right call, and does NOT touch the separate open PR #10357 which flips the same flag with a different literal default value - that decision is left to the maintainer at merge time. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
176 lines
6.2 KiB
TypeScript
176 lines
6.2 KiB
TypeScript
/**
|
|
* Regression test for PR #10571 — `x-opencode-session` must be a STABLE,
|
|
* conversation-scoped fingerprint (via `generateSessionId()`) instead of a
|
|
* fresh random UUID on every request, so upstream prompt caching can hit
|
|
* across requests belonging to the same conversation.
|
|
*
|
|
* `open-sse/utils/opencodeHeaders.ts::applyCliDefaults` now derives
|
|
* `x-opencode-session` from `generateSessionId(sessionBody)`
|
|
* (`open-sse/services/sessionManager.ts`) when a `sessionBody` is supplied,
|
|
* falling back to `randomUUID()` only when no fingerprint can be derived
|
|
* (e.g. an empty/missing body).
|
|
*/
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { forwardOpencodeClientHeaders } from "../../open-sse/utils/opencodeHeaders.ts";
|
|
import { OpencodeExecutor } from "../../open-sse/executors/opencode.ts";
|
|
|
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
const SESSION_HASH_RE = /^[0-9a-f]{16}$/i;
|
|
|
|
const CLI_DEFAULTS = { userAgent: "opencode", client: "desktop", project: "global" };
|
|
|
|
const CONVERSATION_A = {
|
|
model: "big-pickle",
|
|
messages: [{ role: "user", content: "Hello there" }],
|
|
};
|
|
|
|
const CONVERSATION_A_SECOND_TURN = {
|
|
model: "big-pickle",
|
|
messages: [
|
|
{ role: "user", content: "Hello there" },
|
|
{ role: "assistant", content: "Hi! How can I help?" },
|
|
{ role: "user", content: "What's the weather?" },
|
|
],
|
|
};
|
|
|
|
const CONVERSATION_B_DIFFERENT_MODEL = {
|
|
model: "deepseek-v4-flash-free",
|
|
messages: [{ role: "user", content: "Hello there" }],
|
|
};
|
|
|
|
const CONVERSATION_C_DIFFERENT_FIRST_MESSAGE = {
|
|
model: "big-pickle",
|
|
messages: [{ role: "user", content: "A completely different opening message" }],
|
|
};
|
|
|
|
test("x-opencode-session is a stable fingerprint hash (not a random UUID) when sessionBody is provided", () => {
|
|
const headers: Record<string, string> = {};
|
|
forwardOpencodeClientHeaders(
|
|
headers,
|
|
{},
|
|
{ cliDefaults: CLI_DEFAULTS, sessionBody: CONVERSATION_A }
|
|
);
|
|
assert.match(headers["x-opencode-session"] ?? "", SESSION_HASH_RE);
|
|
assert.doesNotMatch(
|
|
headers["x-opencode-session"] ?? "",
|
|
UUID_RE,
|
|
"must not be a random UUID when a fingerprint can be derived"
|
|
);
|
|
});
|
|
|
|
test("x-opencode-session stays STABLE across requests in the same conversation (same model + growing message history keeps the first-user-message fingerprint)", () => {
|
|
const headersFirstTurn: Record<string, string> = {};
|
|
forwardOpencodeClientHeaders(
|
|
headersFirstTurn,
|
|
{},
|
|
{ cliDefaults: CLI_DEFAULTS, sessionBody: CONVERSATION_A }
|
|
);
|
|
|
|
const headersSecondTurn: Record<string, string> = {};
|
|
forwardOpencodeClientHeaders(
|
|
headersSecondTurn,
|
|
{},
|
|
{ cliDefaults: CLI_DEFAULTS, sessionBody: CONVERSATION_A_SECOND_TURN }
|
|
);
|
|
|
|
assert.equal(
|
|
headersFirstTurn["x-opencode-session"],
|
|
headersSecondTurn["x-opencode-session"],
|
|
"same conversation (same model + same first user message) must yield the same session id across turns"
|
|
);
|
|
});
|
|
|
|
test("x-opencode-session CHANGES when the model differs", () => {
|
|
const headersA: Record<string, string> = {};
|
|
forwardOpencodeClientHeaders(
|
|
headersA,
|
|
{},
|
|
{ cliDefaults: CLI_DEFAULTS, sessionBody: CONVERSATION_A }
|
|
);
|
|
|
|
const headersB: Record<string, string> = {};
|
|
forwardOpencodeClientHeaders(
|
|
headersB,
|
|
{},
|
|
{ cliDefaults: CLI_DEFAULTS, sessionBody: CONVERSATION_B_DIFFERENT_MODEL }
|
|
);
|
|
|
|
assert.notEqual(
|
|
headersA["x-opencode-session"],
|
|
headersB["x-opencode-session"],
|
|
"a different model must produce a different session id"
|
|
);
|
|
});
|
|
|
|
test("x-opencode-session CHANGES when the first user message (conversation identity) differs", () => {
|
|
const headersA: Record<string, string> = {};
|
|
forwardOpencodeClientHeaders(
|
|
headersA,
|
|
{},
|
|
{ cliDefaults: CLI_DEFAULTS, sessionBody: CONVERSATION_A }
|
|
);
|
|
|
|
const headersC: Record<string, string> = {};
|
|
forwardOpencodeClientHeaders(
|
|
headersC,
|
|
{},
|
|
{ cliDefaults: CLI_DEFAULTS, sessionBody: CONVERSATION_C_DIFFERENT_FIRST_MESSAGE }
|
|
);
|
|
|
|
assert.notEqual(
|
|
headersA["x-opencode-session"],
|
|
headersC["x-opencode-session"],
|
|
"a different conversation (different first user message) must produce a different session id"
|
|
);
|
|
});
|
|
|
|
test("x-opencode-session falls back to a random UUID when no sessionBody is provided", () => {
|
|
const headers: Record<string, string> = {};
|
|
forwardOpencodeClientHeaders(headers, {}, { cliDefaults: CLI_DEFAULTS });
|
|
assert.match(headers["x-opencode-session"] ?? "", UUID_RE);
|
|
});
|
|
|
|
test("client-supplied x-opencode-session always wins over the derived fingerprint", () => {
|
|
const headers: Record<string, string> = {};
|
|
forwardOpencodeClientHeaders(
|
|
headers,
|
|
{ "x-opencode-session": "client-supplied-session-id" },
|
|
{ cliDefaults: CLI_DEFAULTS, sessionBody: CONVERSATION_A }
|
|
);
|
|
assert.equal(headers["x-opencode-session"], "client-supplied-session-id");
|
|
});
|
|
|
|
test("OpencodeExecutor.buildHeaders derives a stable x-opencode-session from the request body across calls with the same conversation", () => {
|
|
const executor = new OpencodeExecutor("opencode-go");
|
|
const headersFirst = executor.buildHeaders(null, true, null, "big-pickle", undefined, {
|
|
model: "big-pickle",
|
|
messages: [{ role: "user", content: "Same conversation" }],
|
|
});
|
|
const headersSecond = executor.buildHeaders(null, true, null, "big-pickle", undefined, {
|
|
model: "big-pickle",
|
|
messages: [
|
|
{ role: "user", content: "Same conversation" },
|
|
{ role: "assistant", content: "..." },
|
|
{ role: "user", content: "follow-up" },
|
|
],
|
|
});
|
|
|
|
assert.match(headersFirst["x-opencode-session"] ?? "", SESSION_HASH_RE);
|
|
assert.equal(headersFirst["x-opencode-session"], headersSecond["x-opencode-session"]);
|
|
});
|
|
|
|
test("OpencodeExecutor.buildHeaders derives a DIFFERENT x-opencode-session for a different conversation body", () => {
|
|
const executor = new OpencodeExecutor("opencode-go");
|
|
const headersA = executor.buildHeaders(null, true, null, "big-pickle", undefined, {
|
|
model: "big-pickle",
|
|
messages: [{ role: "user", content: "Conversation one" }],
|
|
});
|
|
const headersB = executor.buildHeaders(null, true, null, "big-pickle", undefined, {
|
|
model: "big-pickle",
|
|
messages: [{ role: "user", content: "Conversation two, totally different" }],
|
|
});
|
|
|
|
assert.notEqual(headersA["x-opencode-session"], headersB["x-opencode-session"]);
|
|
});
|