Skip to content
Draft
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
35 changes: 32 additions & 3 deletions src/providers/opencode-zen-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,24 +65,53 @@ function userAgentAcceptsFreeTier(value: string | undefined): boolean {
return true;
}

/** OpenCode `Identifier.create`: `prefix_` + 6-byte timestamp hex + 14 base62. */
/**
* OpenCode `Identifier.create`: `prefix_` + 6-byte timestamp hex + 14 base62.
* Sessions are descending ids (timestamp bits inverted); Console's free-tier
* gate rejects a `ses_` id minted ascending (2026-10-13).
*/
function createOpenCodeId(prefix: "ses" | "msg"): string {
const now = Date.now();
if (now !== idLastMs) {
idLastMs = now;
idCounter = 0;
}
idCounter += 1;
const packed = (BigInt(now) * 0x1000n + BigInt(idCounter)) & ((1n << 48n) - 1n);
const ascending = BigInt(now) * 0x1000n + BigInt(idCounter);
const packed = (prefix === "ses" ? ~ascending : ascending) & ((1n << 48n) - 1n);
const hex = packed.toString(16).padStart(12, "0");
const bytes = randomBytes(14);
let tail = "";
for (let i = 0; i < 14; i++) tail += B62[bytes[i]! % 62]!;
return `${prefix}_${hex}${tail}`;
}

/** Decoded id timestamp must sit within this window of now (inverted for `ses_`). */
const ID_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
const ID_MAX_SKEW_MS = 24 * 60 * 60 * 1000;

function openCodeIdTimestampMs(value: string, descending: boolean): number {
const raw = BigInt(`0x${value.slice(4, 16)}`);
const mask = (1n << 48n) - 1n;
const packed = descending ? ~raw & mask : raw;
// Only the low 48 bits of ms * 0x1000 survive; restore the high bits from now.
const span = 1n << 48n;
const nowPacked = BigInt(Date.now()) * 0x1000n;
let full = (nowPacked & ~mask) | packed;
if (full > nowPacked + span / 2n) full -= span;
else if (full < nowPacked - span / 2n) full += span;
return Number(full / 0x1000n);
}

export function isOpenCodeZenIdentifier(value: string, prefix: "ses" | "msg"): boolean {
return OC_ID.test(value) && value.startsWith(`${prefix}_`);
if (!OC_ID.test(value) || !value.startsWith(`${prefix}_`)) return false;
const recent = (descending: boolean) => {
const age = Date.now() - openCodeIdTimestampMs(value, descending);
return age <= ID_MAX_AGE_MS && age >= -ID_MAX_SKEW_MS;
};
// A session minted in the wrong direction is what Console rejects, so it must not pass.
const descending = prefix === "ses";
return recent(descending) && !recent(!descending);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/server/responses/adapter-continuation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ export function createAdapterContinuations(
// listener that can cancel its reader even while the parser holds the body lock.
// The shared signal is not aborted on timeout: this generator must still emit
// the error that the already-live Responses bridge turns into a terminal.
if (nextParsed.stream) {
if (nextParsed.stream || (response.headers.get("content-type") ?? "").includes("text/event-stream")) {
yield* readResponseStreamWithInactivity(
response,
upstream.signal,
Expand Down
7 changes: 5 additions & 2 deletions src/server/responses/adapter-delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,12 +203,15 @@ export async function deliverAdapterResponse(
});
}

// Zen's free tier also forces SSE upstream for a JSON client, so the wire wins over the request.
const upstreamAnswersStream = upstreamRequestsStream
|| (upstreamResponse.headers.get("content-type") ?? "").includes("text/event-stream");
if (transportState.activeAdapter.parseResponse
|| (upstreamRequestsStream && transportState.activeAdapter.parseStream)) {
|| (upstreamAnswersStream && transportState.activeAdapter.parseStream)) {
let events: AdapterEvent[];
try {
const initialEvents: AdapterEvent[] = [];
if (upstreamRequestsStream && transportState.activeAdapter.parseStream) {
if (upstreamAnswersStream && transportState.activeAdapter.parseStream) {
// The canonical ChatGPT adapter coerces its actual upstream request to SSE even for a
// JSON client. Routed compaction still owns the non-streaming client response, so fold
// that SSE through the adapter instead of handing it to parseResponse as JSON.
Expand Down
71 changes: 71 additions & 0 deletions tests/providers/opencode-zen-free-tier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,20 @@ import { PROVIDER_REGISTRY, getProviderRegistryEntry } from "../../src/providers
import { parseRequest } from "../../src/responses/parser";
import { resolveWireProtocolOverride } from "../../src/server/adapter-resolve";
import { handleChatCompletions } from "../../src/server/chat-completions";
import { handleResponses } from "../../src/server/responses/core";
import type { OcxConfig, OcxProviderConfig } from "../../src/types";
import { acquireOwnedSpendHome } from "../helpers/owned-spend-home";
import { withTestTranslatorBudget } from "../helpers/translator-budget";

describe("opencode zen free-tier path", () => {
const originalFetch = globalThis.fetch;
let releaseSpendHome: (() => void) | undefined;
const savedSocks = { ALL_PROXY: process.env.ALL_PROXY, all_proxy: process.env.all_proxy };
afterEach(() => {
for (const [key, value] of Object.entries(savedSocks)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
// Released first so a failed assertion cannot leave the spend-ledger lease live.
releaseSpendHome?.();
releaseSpendHome = undefined;
Expand Down Expand Up @@ -95,6 +101,33 @@ describe("opencode zen free-tier path", () => {
expect(isOpenCodeZenIdentifier(session, "ses")).toBe(true);
});

test("session ids are descending like OpenCode's and ascending ones are replaced", () => {
// Console's free-tier gate rejects an ascending ses_ id (2026-10-13). Pin to the clock
// that minted these ids, captured from OpenCode 1.18.29.
const realNow = Date.now;
Date.now = () => 1_791_902_765_000;
try {
expect(isOpenCodeZenIdentifier("ses_eca460d85ffeIj7pkg4yxND6rK", "ses")).toBe(true);
expect(isOpenCodeZenIdentifier("msg_135b9f297001Gp7pfvGFtRTQPX", "msg")).toBe(true);
expect(isOpenCodeZenIdentifier("ses_135b9f297001Gp7pfvGFtRTQPX", "ses")).toBe(false);
} finally {
Date.now = realNow;
}

const entry = getProviderRegistryEntry("opencode-free")!;
const mask = (1n << 48n) - 1n;
const ascending = BigInt(Date.now()) * 0x1000n & mask;
const minted = deriveOpenCodeZenSessionId("descending-check");
const decoded = ~BigInt(`0x${minted.slice(4, 16)}`) & mask;
expect(Math.abs(Number((decoded - ascending) >> 12n))).toBeLessThan(60_000);
const replaced = resolveOpenCodeZenTransport({
...providerConfigSeed(entry),
headers: { "x-opencode-session": `ses_${ascending.toString(16).padStart(12, "0")}Ij7pkg4yxND6rK` },
});
expect(isOpenCodeZenIdentifier(replaced.headers?.["x-opencode-session"] ?? "", "ses")).toBe(true);
expect(replaced.headers?.["x-opencode-session"]?.startsWith(`ses_${ascending.toString(16)}`)).toBe(false);
});

test("explicit non-desktop client overrides survive", () => {
const entry = getProviderRegistryEntry("opencode-free")!;
const provider = resolveOpenCodeZenTransport({
Expand Down Expand Up @@ -145,6 +178,9 @@ describe("opencode zen free-tier path", () => {
providers: { "opencode-zen": provider },
} as unknown as OcxConfig;

// An ambient SOCKS5 ALL_PROXY routes dispatch around the mocked fetch.
delete process.env.ALL_PROXY;
delete process.env.all_proxy;
// A direct handler dispatch skips startServer, so it must hold the spend-ledger lease itself.
releaseSpendHome = acquireOwnedSpendHome();
let seen: Headers | undefined;
Expand Down Expand Up @@ -189,6 +225,41 @@ describe("opencode zen free-tier path", () => {
expect(upstream.tools?.some(tool => tool.function?.name === "grep")).toBe(true);
});

test("a non-streaming Responses client folds the SSE Zen free tier is forced onto", async () => {
const entry = getProviderRegistryEntry("opencode-free")!;
const config = {
defaultProvider: "opencode-free",
providers: { "opencode-free": providerConfigSeed(entry) },
} as unknown as OcxConfig;
delete process.env.ALL_PROXY;
delete process.env.all_proxy;
releaseSpendHome = acquireOwnedSpendHome();
let upstreamStream: unknown;
globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
upstreamStream = JSON.parse(typeof init?.body === "string" ? init.body : "{}").stream;
const chunk = (delta: Record<string, unknown>, finish: string | null) =>
`data: ${JSON.stringify({ id: "c1", object: "chat.completion.chunk", model: "big-pickle", choices: [{ index: 0, delta, finish_reason: finish }] })}\n\n`;
return new Response(`${chunk({ role: "assistant", content: "pong" }, null)}${chunk({}, "stop")}data: [DONE]\n\n`, {
headers: { "content-type": "text/event-stream" },
});
}) as typeof fetch;

const res = await handleResponses(
new Request("http://localhost/v1/responses", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ model: "opencode-free/big-pickle", input: "ping", stream: false }),
}),
config,
{ model: "", provider: "" },
);
expect(upstreamStream).toBe(true);
expect(res.status).toBe(200);
const body = await res.json() as { status?: string; output?: Array<{ content?: Array<{ text?: string }> }> };
expect(body.status).toBe("completed");
expect(body.output?.flatMap(item => item.content ?? []).map(part => part.text).join("")).toBe("pong");
});

test("paid Zen models do not inherit the free-tier tool catalog", () => {
const shaped = applyOpenCodeZenFreeTierBody(
{ model: "gpt-5.4", messages: [], stream: false },
Expand Down
Loading