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
8 changes: 6 additions & 2 deletions open-sse/config/codexIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ function uuidFromStableValue(value: string): string {
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-a${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
}

export function getCodexInstallationId(providerSpecificData?: Record<string, unknown> | null): string {
export function getCodexInstallationId(
providerSpecificData?: Record<string, unknown> | null
): string {
const explicit = normalizeUuid(providerSpecificData?.codexInstallationId);
if (explicit) return explicit;

Expand Down Expand Up @@ -73,7 +75,9 @@ export function applyCodexClientMetadata(
): void {
if (!identity) return;
const existing =
body.client_metadata && typeof body.client_metadata === "object" && !Array.isArray(body.client_metadata)
body.client_metadata &&
typeof body.client_metadata === "object" &&
!Array.isArray(body.client_metadata)
? (body.client_metadata as Record<string, unknown>)
: {};
body.client_metadata = {
Expand Down
18 changes: 12 additions & 6 deletions open-sse/executors/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ function getResponsesSubpath(endpointPath: unknown): string | null {
return match[1] || "";
}

function isCompactResponsesEndpoint(endpointPath: unknown): boolean {
export function isCompactResponsesEndpoint(endpointPath: unknown): boolean {
return getResponsesSubpath(endpointPath)?.toLowerCase() === "/compact";
}

Expand Down Expand Up @@ -714,7 +714,10 @@ export class CodexExecutor extends BaseExecutor {
input.credentials,
input.body as Record<string, unknown> | null
);
const identity = createCodexClientIdentity(sessionId, input.credentials?.providerSpecificData ?? null);
const identity = createCodexClientIdentity(
sessionId,
input.credentials?.providerSpecificData ?? null
);
const credentials = identity
? {
...input.credentials,
Expand Down Expand Up @@ -1082,17 +1085,20 @@ export class CodexExecutor extends BaseExecutor {
}
}

// Store: The Codex OAuth backend rejects store=true with
// "Store must be set to false". Default to false unless the provider
// explicitly opts in (e.g. API-key accounts that support persistence).
// Store: regular Codex Responses rejects store=true with
// "Store must be set to false", while /responses/compact rejects the
// store field entirely. Default regular requests to false unless the
// provider explicitly opts in (e.g. API-key accounts that support persistence).
// Ref: sub2api openai_codex_transform.go line 75-80
const explicitStoreSetting =
credentials?.providerSpecificData &&
typeof credentials.providerSpecificData === "object" &&
!Array.isArray(credentials.providerSpecificData)
? credentials.providerSpecificData.openaiStoreEnabled
: undefined;
if (explicitStoreSetting === true) {
if (isCompactRequest) {
delete body.store;
} else if (explicitStoreSetting === true) {
body.store = true;
} else {
// backend rejects store=true ("Store must be set to false"), so default to false.
Expand Down
8 changes: 7 additions & 1 deletion open-sse/handlers/chatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import {
parseCodexQuotaHeaders,
getCodexModelScope,
getCodexDualWindowCooldownMs,
isCompactResponsesEndpoint,
} from "../executors/codex.ts";
import { invalidateCodexQuotaCache } from "../services/codexQuotaFetcher.ts";
import { translateNonStreamingResponse } from "./responseTranslator.ts";
Expand Down Expand Up @@ -1339,7 +1340,12 @@ export async function handleChatCore({
delete b.streaming;
}

const stream = resolveStreamFlag(body?.stream, acceptHeader);
// Codex /responses/compact is JSON-only: Codex CLI does not send stream=false,
// so route shape must override the usual Accept/header fallback.
const stream =
nativeCodexPassthrough && isCompactResponsesEndpoint(endpointPath)
? false
: resolveStreamFlag(body?.stream, acceptHeader);
const settings = await getCachedSettings();
setGeminiThoughtSignatureMode(settings.antigravitySignatureCacheMode);
const semanticCacheEnabled = settings.semanticCacheEnabled !== false;
Expand Down
75 changes: 75 additions & 0 deletions tests/integration/chat-pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,39 @@ function buildOpenAIResponsesSSE({
);
}

function buildOpenAIResponsesJson({
text = "responses compacted from codex",
model = "gpt-5.5",
usage = null,
} = {}) {
return new Response(
JSON.stringify({
id: "resp_compact",
object: "response",
status: "completed",
model,
output: [
{
id: "msg_compact",
type: "message",
role: "assistant",
content: [{ type: "output_text", text, annotations: [] }],
},
],
output_text: text,
usage: usage || {
input_tokens: 90,
output_tokens: 15,
total_tokens: 105,
},
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
}

async function resetStorage() {
globalThis.fetch = originalFetch;
process.env.REQUIRE_API_KEY = "false";
Expand Down Expand Up @@ -531,6 +564,48 @@ test("chat pipeline persists Codex responses cache and reasoning tokens to call
assert.equal(callLog.tokens.reasoning, 13);
});

test("chat pipeline treats Codex /responses/compact as non-streaming JSON", async () => {
await seedConnection("codex", { apiKey: "sk-codex-compact" });
const fetchCalls = [];

globalThis.fetch = async (url, init = {}) => {
fetchCalls.push({
url: String(url),
headers: toPlainHeaders(init.headers),
body: init.body ? JSON.parse(String(init.body)) : null,
});
return buildOpenAIResponsesJson();
};

const response = await handleChat(
buildRequest({
url: "http://localhost/v1/responses/compact",
headers: { Accept: "text/event-stream" },
body: {
model: "codex/gpt-5.5",
input: "Compact this session",
},
})
);

const json = (await response.json()) as { object?: string; output_text?: string };
const callLog = await waitFor(() => getLatestCallLog());

assert.equal(response.status, 200);
assert.equal(fetchCalls.length, 1);
assert.match(fetchCalls[0].url, /\/responses\/compact$/);
assert.equal(fetchCalls[0].headers.Accept, "application/json");
assert.equal(fetchCalls[0].body.stream, undefined);
assert.equal(fetchCalls[0].body.store, undefined);
assert.equal(json.object, "response");
assert.equal(json.output_text, "responses compacted from codex");

assert.ok(callLog, "expected a compact call log row to be created");
assert.equal(callLog.provider, "codex");
assert.equal(callLog.path, "/v1/responses/compact");
assert.equal(callLog.status, 200);
});

test("chat pipeline serves repeated /v1/responses requests as MISS then HIT and logs cache hits separately", async () => {
await seedConnection("codex", { apiKey: "sk-codex-cache-seq" });
const fetchCalls = [];
Expand Down
39 changes: 28 additions & 11 deletions tests/unit/executor-codex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,7 @@ test("CodexExecutor.buildHeaders binds workspace ids and disables SSE accept for
assert.equal(standardHeaders.Version, "0.125.0");
assert.equal(standardHeaders["Openai-Beta"], "responses=experimental");
assert.equal(standardHeaders["X-Codex-Beta-Features"], "responses_websockets");
assert.equal(
standardHeaders["User-Agent"],
"codex-cli/0.125.0 (Windows 10.0.26200; x64)"
);
assert.equal(standardHeaders["User-Agent"], "codex-cli/0.125.0 (Windows 10.0.26200; x64)");
assert.equal(compactHeaders.Accept, "application/json");
});

Expand All @@ -161,10 +158,7 @@ test("CodexExecutor.buildHeaders honors safe env overrides for Version and User-
() => {
const headers = executor.buildHeaders({ accessToken: "codex-token" }, true);
assert.equal(headers.Version, "0.125.0");
assert.equal(
headers["User-Agent"],
"codex-cli/0.125.0 (Windows 10.0.26200; x64)"
);
assert.equal(headers["User-Agent"], "codex-cli/0.125.0 (Windows 10.0.26200; x64)");
}
);

Expand Down Expand Up @@ -280,7 +274,7 @@ test("CodexExecutor.transformRequest preserves compact requests and native passt
assert.equal(result.stream, undefined);
assert.equal(result.service_tier, "priority");
assert.equal(result.reasoning.effort, "medium");
assert.equal(result.store, false);
assert.equal(result.store, undefined);
assert.equal(result.instructions, "keep this");
});

Expand All @@ -295,7 +289,7 @@ test("CodexExecutor.transformRequest preserves store-enabled responses state whe
};

const result = executor.transformRequest("gpt-5.3-codex", body, false, {
requestEndpointPath: "/responses/compact",
requestEndpointPath: "/responses",
providerSpecificData: {
openaiStoreEnabled: true,
requestDefaults: { serviceTier: "priority" },
Expand All @@ -307,6 +301,30 @@ test("CodexExecutor.transformRequest preserves store-enabled responses state whe
assert.equal(result.previous_response_id, "resp_prev_123");
});

test("CodexExecutor.transformRequest strips store from compact requests even when store is enabled", () => {
const executor = new CodexExecutor();
const body = {
_nativeCodexPassthrough: true,
_omnirouteResponsesStore: true,
instructions: "keep this",
store: true,
stream: false,
};

const result = executor.transformRequest("gpt-5.3-codex", body, false, {
requestEndpointPath: "/responses/compact",
providerSpecificData: {
openaiStoreEnabled: true,
requestDefaults: { serviceTier: "priority" },
},
});

assert.equal(result._omnirouteResponsesStore, undefined);
assert.equal(result.store, undefined);
assert.equal(result.stream, undefined);
assert.equal(result.instructions, "keep this");
});

test("CodexExecutor.transformRequest rehydrates missing function_call items for stateful tool outputs", () => {
const executor = new CodexExecutor();
rememberResponseFunctionCalls("resp_prev_tool_123", [
Expand Down Expand Up @@ -348,7 +366,6 @@ test("CodexExecutor.transformRequest rehydrates missing function_call items for
output: '{"ok":true}',
});
});

test("CodexExecutor.transformRequest applies per-connection reasoning and service tier defaults", () => {
const executor = new CodexExecutor();
const result = executor.transformRequest(
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/t20-t22-provider-headers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,6 @@ test("T20: codex config advertises current client headers and auto-review model"
assert.equal(codex.headers.Version, "0.125.0");
assert.equal(codex.headers["Openai-Beta"], "responses=experimental");
assert.equal(codex.headers["X-Codex-Beta-Features"], "responses_websockets");
assert.equal(codex.headers["User-Agent"], "codex-cli/0.125.0 (Windows 10.0.26100; x64)");
assert.equal(codex.headers["User-Agent"], "codex-cli/0.125.0 (Windows 10.0.26200; x64)");
assert.ok(codex.models.some((model) => model.id === "codex-auto-review"));
});