From 1e2b7e728de7b1c1db13017f185af5adcd5fc9fc Mon Sep 17 00:00:00 2001
From: Randi <55005611+rdself@users.noreply.github.com>
Date: Sun, 12 Apr 2026 20:40:32 -0400
Subject: [PATCH] Fix Codex combo fallback and move Codex defaults to
connections (#1176)
Integrated into release/v3.6.5. Added CHANGELOG breaking change entry for the removed /api/settings/codex-service-tier endpoint and deduplicated getCodexRequestDefaults in page.tsx (now imports from requestDefaults.ts).
---
CHANGELOG.md | 16 +-
open-sse/executors/codex.ts | 65 ++++----
open-sse/utils/streamPayloadCollector.ts | 24 +--
.../dashboard/providers/[id]/page.tsx | 52 +++++++
.../components/CodexServiceTierTab.tsx | 103 -------------
.../(dashboard)/dashboard/settings/page.tsx | 2 -
src/app/api/providers/[id]/route.ts | 4 +-
src/app/api/providers/route.ts | 3 +
.../api/settings/codex-service-tier/route.ts | 55 -------
src/instrumentation-node.ts | 29 ++--
src/lib/db/providers.ts | 25 +++-
src/lib/providers/codexConnectionDefaults.ts | 84 +++++++++++
src/lib/providers/requestDefaults.ts | 101 +++++++++++++
src/shared/validation/schemas.ts | 123 +++++++++------
tests/unit/codex-connection-defaults.test.mjs | 132 ++++++++++++++++
tests/unit/executor-codex.test.mjs | 141 +++++++++++++++---
tests/unit/plan3-p0.test.mjs | 32 ++--
tests/unit/stream-utils.test.mjs | 43 ++++++
18 files changed, 735 insertions(+), 299 deletions(-)
delete mode 100644 src/app/(dashboard)/dashboard/settings/components/CodexServiceTierTab.tsx
delete mode 100644 src/app/api/settings/codex-service-tier/route.ts
create mode 100644 src/lib/providers/codexConnectionDefaults.ts
create mode 100644 src/lib/providers/requestDefaults.ts
create mode 100644 tests/unit/codex-connection-defaults.test.mjs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3afd902107..b50e508e0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,21 @@
## [Unreleased]
----
+### ✨ New Features
+
+- **Antigravity AI Credits Fallback:** Automatically retries with `GOOGLE_ONE_AI` credit injection when free-tier quota is exhausted. Per-account credit balance (5-hour TTL) is cached from SSE `remainingCredits` and exposed as a numeric badge in the Provider Usage dashboard (#1190 — thanks @sFaxsy)
+- **Claude Code Native Parity:** Full header/body signing parity with the Claude Code 2.1.87 OAuth client — CCH xxHash64 body signing, dynamic per-request fingerprint, bidirectional TitleCase ↔ lowercase tool name remapping (14 tools), API constraint enforcement (`temperature=1` for thinking, max 4 `cache_control` blocks, auto-inject ephemeral on last user message), and optional ZWJ obfuscation (#1188 — thanks @RaviTharuma)
+- **Per-Connection Codex Defaults:** Codex Fast Service Tier and Reasoning Effort settings are now per-connection instead of a single global toggle. Existing connections are migrated automatically on startup via an idempotent backfill migration (#1176 — thanks @rdself)
+
+### 🐛 Bug Fixes
+
+- **Search Cache Coalescing with TTL=0:** Fixed a bug where providers configured with `cacheTTLMs: 0` (caching explicitly disabled) still had concurrent requests coalesced and returned `{ cached: true }`. Now each call gets its own independent upstream fetch (#1178 — thanks @sjhddh)
+- **Codex Combo Smoke Test False Positives:** Fixed combo tests incorrectly reporting `ERROR` for valid Codex streaming responses when `response.output` is empty but text deltas were emitted. The summary now falls back to accumulated delta text (#1176 — thanks @rdself)
+- **Electron NODE_PATH Resolution (Windows):** Fixed Electron desktop startup failures on Windows packaged builds caused by native modules (`better-sqlite3`) being under `app.asar.unpacked` while helpers were in `app/node_modules`. `resolveServerNodePath()` now merges both locations with deduplication and existence checks (#1172 — thanks @backryun)
+
+### ⚠️ Breaking Changes
+
+- **`DELETE /api/settings/codex-service-tier` removed:** This endpoint no longer exists. Codex Service Tier configuration has moved to per-connection `providerSpecificData.requestDefaults`. Existing connections are migrated automatically on first startup after upgrade. Any external scripts or integrations that call this endpoint should be updated — use `PUT /api/providers/:id` with `providerSpecificData.requestDefaults.serviceTier` instead (#1176).
## [3.6.4] — 2026-04-12
diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts
index 5e08855074..db67c0932a 100644
--- a/open-sse/executors/codex.ts
+++ b/open-sse/executors/codex.ts
@@ -2,6 +2,8 @@ import { BaseExecutor } from "./base.ts";
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.ts";
import { PROVIDERS } from "../config/constants.ts";
import { refreshCodexToken } from "../services/tokenRefresh.ts";
+import { getThinkingBudgetConfig, ThinkingMode } from "../services/thinkingBudget.ts";
+import { getCodexRequestDefaults } from "@/lib/providers/requestDefaults";
// ─── T09: Codex vs Spark Scope-Aware Rate Limiting ────────────────────────
// Codex has two independent quota pools: "codex" (standard) and "spark" (premium).
@@ -160,7 +162,6 @@ export function getCodexDualWindowCooldownMs(
const EFFORT_ORDER = ["none", "low", "medium", "high", "xhigh"] as const;
type EffortLevel = (typeof EFFORT_ORDER)[number];
const CODEX_FAST_WIRE_VALUE = "priority";
-let defaultFastServiceTierEnabled = false;
function stringifyCodexInstructionContent(content: unknown): string {
if (typeof content === "string") {
@@ -285,10 +286,6 @@ function normalizeServiceTierValue(value: unknown): string | undefined {
return normalized;
}
-export function setDefaultFastServiceTierEnabled(enabled: boolean): void {
- defaultFastServiceTierEnabled = enabled;
-}
-
/**
* Maximum reasoning effort allowed per Codex model.
* Models not listed here default to "xhigh" (unrestricted).
@@ -318,6 +315,12 @@ function clampEffort(model: string, requested: string): string {
return requested;
}
+function normalizeEffortValue(value: unknown): string | undefined {
+ if (typeof value !== "string") return undefined;
+ const normalized = value.trim().toLowerCase();
+ return normalized || undefined;
+}
+
/**
* Codex Executor - handles OpenAI Codex API (Responses API format)
* Automatically injects default instructions if missing.
@@ -394,6 +397,9 @@ export class CodexExecutor extends BaseExecutor {
transformRequest(model, body, stream, credentials) {
const nativeCodexPassthrough = body?._nativeCodexPassthrough === true;
const isCompactRequest = isCompactResponsesEndpoint(credentials?.requestEndpointPath);
+ const requestDefaults = getCodexRequestDefaults(credentials?.providerSpecificData);
+ const thinkingBudgetConfig = getThinkingBudgetConfig();
+ const allowConnectionReasoningDefaults = thinkingBudgetConfig.mode === ThinkingMode.PASSTHROUGH;
// Codex /responses rejects stream=false, but /responses/compact rejects the stream field entirely.
if (isCompactRequest) {
@@ -407,8 +413,8 @@ export class CodexExecutor extends BaseExecutor {
const requestServiceTier = normalizeServiceTierValue(body.service_tier);
if (requestServiceTier) {
body.service_tier = requestServiceTier;
- } else if (defaultFastServiceTierEnabled) {
- body.service_tier = CODEX_FAST_WIRE_VALUE;
+ } else if (requestDefaults.serviceTier) {
+ body.service_tier = requestDefaults.serviceTier;
}
// If no instructions provided, inject default Codex instructions
@@ -435,38 +441,43 @@ export class CodexExecutor extends BaseExecutor {
delete body.messages;
delete body.prompt;
- if (nativeCodexPassthrough) {
- return body;
- }
-
- // Extract thinking level from model name suffix
- // e.g., gpt-5.3-codex-high → high, gpt-5.3-codex → medium (default)
const effortLevels = ["none", "low", "medium", "high", "xhigh"];
let modelEffort: string | null = null;
- // Track the clean model name (suffix stripped) for clamp lookup
- let cleanModel = model;
+ let cleanModel = typeof body.model === "string" ? body.model : model;
for (const level of effortLevels) {
- if (model.endsWith(`-${level}`)) {
+ if (typeof cleanModel === "string" && cleanModel.endsWith(`-${level}`)) {
modelEffort = level;
- // Strip suffix from model name for actual API call
- body.model = body.model.replace(`-${level}`, "");
+ body.model = cleanModel.slice(0, -`-${level}`.length);
cleanModel = body.model;
break;
}
}
- // Priority: explicit reasoning.effort > reasoning_effort param > model suffix > default (medium)
- if (!body.reasoning) {
- const rawEffort = body.reasoning_effort || modelEffort || "medium";
- // Clamp effort to the model's maximum allowed level (feature-07)
- const effort = clampEffort(cleanModel, rawEffort);
- body.reasoning = { effort };
- } else if (body.reasoning.effort) {
- // Also clamp if reasoning object was provided directly
- body.reasoning.effort = clampEffort(cleanModel, body.reasoning.effort);
+ const explicitReasoning = normalizeEffortValue(body?.reasoning?.effort);
+ const requestReasoningEffort = normalizeEffortValue(body.reasoning_effort);
+ const fallbackReasoningEffort = allowConnectionReasoningDefaults
+ ? requestDefaults.reasoningEffort || "medium"
+ : undefined;
+ const rawEffort =
+ explicitReasoning || requestReasoningEffort || modelEffort || fallbackReasoningEffort;
+
+ if (explicitReasoning) {
+ body.reasoning = {
+ ...(body.reasoning && typeof body.reasoning === "object" ? body.reasoning : {}),
+ effort: clampEffort(cleanModel, explicitReasoning),
+ };
+ } else if (rawEffort) {
+ body.reasoning = {
+ ...(body.reasoning && typeof body.reasoning === "object" ? body.reasoning : {}),
+ effort: clampEffort(cleanModel, rawEffort),
+ };
}
delete body.reasoning_effort;
+ if (nativeCodexPassthrough) {
+ return body;
+ }
+
// Remove unsupported parameters for Codex API
delete body.temperature;
delete body.top_p;
diff --git a/open-sse/utils/streamPayloadCollector.ts b/open-sse/utils/streamPayloadCollector.ts
index a9c9e8c9c5..96395bee99 100644
--- a/open-sse/utils/streamPayloadCollector.ts
+++ b/open-sse/utils/streamPayloadCollector.ts
@@ -261,6 +261,16 @@ function buildResponsesSummary(
let latestResponse: JsonRecord | null = null;
let usage: JsonRecord | null = null;
const textParts: string[] = [];
+ const buildOutputFromText = () =>
+ textParts.length > 0
+ ? [
+ {
+ type: "message",
+ role: "assistant",
+ content: [{ type: "output_text", text: textParts.join("") }],
+ },
+ ]
+ : [];
for (const payload of payloads) {
const eventType = toString(payload.type);
@@ -292,11 +302,12 @@ function buildResponsesSummary(
const picked = completed || latestResponse;
if (picked && Object.keys(picked).length > 0) {
+ const pickedOutput = Array.isArray(picked.output) ? picked.output : [];
return {
id: toString(picked.id, `resp_${Date.now()}`),
object: "response",
model: toString(picked.model, fallbackModel || "unknown"),
- output: Array.isArray(picked.output) ? picked.output : [],
+ output: pickedOutput.length > 0 ? pickedOutput : buildOutputFromText(),
usage: picked.usage ?? usage ?? null,
status: toString(picked.status, completed ? "completed" : "in_progress"),
created_at: toNumber(picked.created_at, Math.floor(Date.now() / 1000)),
@@ -308,16 +319,7 @@ function buildResponsesSummary(
id: `resp_${Date.now()}`,
object: "response",
model: fallbackModel || "unknown",
- output:
- textParts.length > 0
- ? [
- {
- type: "message",
- role: "assistant",
- content: [{ type: "output_text", text: textParts.join("") }],
- },
- ]
- : [],
+ output: buildOutputFromText(),
usage: usage ?? null,
status: "completed",
created_at: Math.floor(Date.now() / 1000),
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx
index d1f600a35d..383fd89f7a 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx
+++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx
@@ -46,6 +46,7 @@ import { resolveManagedModelAlias } from "@/shared/utils/providerModelAliases";
import { maskEmail, pickMaskedDisplayValue, pickDisplayValue } from "@/shared/utils/maskEmail";
import useEmailPrivacyStore from "@/store/emailPrivacyStore";
import EmailPrivacyToggle from "@/shared/components/EmailPrivacyToggle";
+import { getCodexRequestDefaults as _getCodexRequestDefaults } from "@/lib/providers/requestDefaults";
type CompatByProtocolMap = Partial<
Record<
@@ -535,6 +536,13 @@ interface EditCompatibleNodeModalProps {
const CC_COMPATIBLE_LABEL = "CC Compatible";
const CC_COMPATIBLE_DETAILS_TITLE = "CC Compatible Details";
const CC_COMPATIBLE_DEFAULT_CHAT_PATH = "/v1/messages?beta=true";
+const CODEX_REASONING_STRENGTH_OPTIONS = [
+ { value: "none", label: "None" },
+ { value: "low", label: "Low" },
+ { value: "medium", label: "Medium" },
+ { value: "high", label: "High" },
+ { value: "xhigh", label: "XHigh" },
+];
function normalizeCodexLimitPolicy(policy: unknown): { use5h: boolean; useWeekly: boolean } {
const record =
@@ -547,6 +555,21 @@ function normalizeCodexLimitPolicy(policy: unknown): { use5h: boolean; useWeekly
};
}
+/**
+ * UI adapter around the canonical getCodexRequestDefaults from requestDefaults.ts.
+ * Adds the "medium" fallback for reasoningEffort required by the connection form.
+ */
+function getCodexRequestDefaults(providerSpecificData: unknown): {
+ reasoningEffort: string;
+ serviceTier?: "priority";
+} {
+ const defaults = _getCodexRequestDefaults(providerSpecificData);
+ return {
+ reasoningEffort: defaults.reasoningEffort ?? "medium",
+ ...(defaults.serviceTier ? { serviceTier: defaults.serviceTier } : {}),
+ };
+}
+
function compatProtocolLabelKey(protocol: string): string {
if (protocol === "openai") return "compatProtocolOpenAI";
if (protocol === "openai-responses") return "compatProtocolOpenAIResponses";
@@ -5340,6 +5363,8 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
tag: "",
customUserAgent: "",
accountId: "",
+ codexReasoningEffort: "medium",
+ codexFastServiceTier: false,
});
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState(null);
@@ -5358,6 +5383,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
const isVertex = connection?.provider === "vertex";
const isGlm = connection?.provider === "glm";
const isCloudflare = connection?.provider === "cloudflare-ai";
+ const isCodex = connection?.provider === "codex";
const defaultRegion = "us-central1";
useEffect(() => {
@@ -5371,6 +5397,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
typeof rawCustomUserAgent === "string" ? rawCustomUserAgent : "";
const rawAccountId = connection.providerSpecificData?.accountId;
const existingAccountId = typeof rawAccountId === "string" ? rawAccountId : "";
+ const codexRequestDefaults = getCodexRequestDefaults(connection.providerSpecificData);
setFormData({
name: connection.name || "",
priority: connection.priority || 1,
@@ -5383,6 +5410,8 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
tag: (connection.providerSpecificData?.tag as string) || "",
customUserAgent: existingCustomUserAgent,
accountId: existingAccountId,
+ codexReasoningEffort: codexRequestDefaults.reasoningEffort,
+ codexFastServiceTier: codexRequestDefaults.serviceTier === "priority",
});
// Load existing extra keys from providerSpecificData
const existing = connection.providerSpecificData?.extraApiKeys;
@@ -5533,6 +5562,12 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
...(connection.providerSpecificData || {}),
tag: formData.tag.trim() || undefined,
};
+ if (isCodex) {
+ updates.providerSpecificData.requestDefaults = {
+ reasoningEffort: formData.codexReasoningEffort,
+ ...(formData.codexFastServiceTier ? { serviceTier: "priority" } : {}),
+ };
+ }
}
const error = (await onSave(updates)) as void | unknown;
if (error) {
@@ -5570,6 +5605,23 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
placeholder="e.g. personal, work, team-a"
hint="Used to group accounts in the provider view"
/>
+ {isCodex && (
+
{t("email")}
diff --git a/src/app/(dashboard)/dashboard/settings/components/CodexServiceTierTab.tsx b/src/app/(dashboard)/dashboard/settings/components/CodexServiceTierTab.tsx
deleted file mode 100644
index da90a481c4..0000000000
--- a/src/app/(dashboard)/dashboard/settings/components/CodexServiceTierTab.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import { Card } from "@/shared/components";
-
-export default function CodexServiceTierTab() {
- const [enabled, setEnabled] = useState(false);
- const [loading, setLoading] = useState(true);
- const [saving, setSaving] = useState(false);
- const [status, setStatus] = useState<"" | "saved" | "error">("");
-
- useEffect(() => {
- fetch("/api/settings/codex-service-tier")
- .then((res) => res.json())
- .then((data) => {
- setEnabled(Boolean(data.enabled));
- setLoading(false);
- })
- .catch(() => setLoading(false));
- }, []);
-
- const save = async (nextEnabled: boolean) => {
- setEnabled(nextEnabled);
- setSaving(true);
- setStatus("");
-
- try {
- const res = await fetch("/api/settings/codex-service-tier", {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ enabled: nextEnabled }),
- });
-
- if (res.ok) {
- setStatus("saved");
- setTimeout(() => setStatus(""), 2000);
- } else {
- setStatus("error");
- setEnabled(!nextEnabled);
- }
- } catch {
- setStatus("error");
- setEnabled(!nextEnabled);
- } finally {
- setSaving(false);
- }
- };
-
- return (
-
-
-
-
- bolt
-
-
-
-
Codex Fast Service Tier
-
- Inject `service_tier=priority` into Codex requests when the client leaves it unset.
-
-
- {status === "saved" && (
-
- check_circle
- Saved
-
- )}
- {status === "error" && (
-
- error
- Failed to save
-
- )}
-
-
-
-
-
Force fast tier for Codex
-
- Off by default. Applies only to Codex requests and does not override an explicit tier.
- Codex fast mode is sent upstream as `service_tier=priority`.
-
-
-
save(!enabled)}
- disabled={loading || saving}
- className={`relative inline-flex h-6 w-11 items-center rounded-full border transition-colors ${
- enabled
- ? "bg-sky-500 border-sky-500"
- : "bg-black/10 border-black/10 dark:bg-white/10 dark:border-white/10"
- }`}
- >
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/dashboard/settings/page.tsx b/src/app/(dashboard)/dashboard/settings/page.tsx
index e7a11c27a6..2d62d6a3ae 100644
--- a/src/app/(dashboard)/dashboard/settings/page.tsx
+++ b/src/app/(dashboard)/dashboard/settings/page.tsx
@@ -12,7 +12,6 @@ import ComboDefaultsTab from "./components/ComboDefaultsTab";
import ProxyTab from "./components/ProxyTab";
import AppearanceTab from "./components/AppearanceTab";
import ThinkingBudgetTab from "./components/ThinkingBudgetTab";
-import CodexServiceTierTab from "./components/CodexServiceTierTab";
import SystemPromptTab from "./components/SystemPromptTab";
import ModelAliasesUnified from "./components/ModelAliasesUnified";
import BackgroundDegradationTab from "./components/BackgroundDegradationTab";
@@ -93,7 +92,6 @@ export default function SettingsPage() {
{activeTab === "ai" && (
-
diff --git a/src/app/api/providers/[id]/route.ts b/src/app/api/providers/[id]/route.ts
index 43c6624736..5ade90e58b 100644
--- a/src/app/api/providers/[id]/route.ts
+++ b/src/app/api/providers/[id]/route.ts
@@ -9,6 +9,7 @@ import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/lib/cloudSync";
import { updateProviderConnectionSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
+import { normalizeProviderSpecificData } from "@/lib/providers/requestDefaults";
function normalizeCodexLimitPolicy(
incoming: unknown,
@@ -142,7 +143,8 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
}
}
- updateData.providerSpecificData = mergedPsd;
+ updateData.providerSpecificData =
+ normalizeProviderSpecificData(existing.provider, mergedPsd) || {};
}
const updated = await updateProviderConnection(id, updateData);
diff --git a/src/app/api/providers/route.ts b/src/app/api/providers/route.ts
index bfdb267c0f..e09af29cb1 100644
--- a/src/app/api/providers/route.ts
+++ b/src/app/api/providers/route.ts
@@ -16,6 +16,7 @@ import { syncToCloud } from "@/lib/cloudSync";
import { createProviderSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { normalizeQoderPatProviderData } from "@omniroute/open-sse/services/qoderCli";
+import { normalizeProviderSpecificData } from "@/lib/providers/requestDefaults";
// GET /api/providers - List all connections
export async function GET() {
@@ -132,6 +133,8 @@ export async function POST(request: Request) {
};
}
+ providerSpecificData = normalizeProviderSpecificData(provider, providerSpecificData) || null;
+
const newConnection = await createProviderConnection({
provider,
authType: "apikey",
diff --git a/src/app/api/settings/codex-service-tier/route.ts b/src/app/api/settings/codex-service-tier/route.ts
deleted file mode 100644
index 8ec575ed06..0000000000
--- a/src/app/api/settings/codex-service-tier/route.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { NextResponse } from "next/server";
-import { getSettings, updateSettings } from "@/lib/localDb";
-import { setDefaultFastServiceTierEnabled } from "@omniroute/open-sse/executors/codex.ts";
-import { updateCodexServiceTierSchema } from "@/shared/validation/schemas";
-import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
-
-export async function GET() {
- try {
- const settings = await getSettings();
- const persisted =
- typeof settings.codexServiceTier === "string"
- ? JSON.parse(settings.codexServiceTier)
- : settings.codexServiceTier;
-
- return NextResponse.json({
- enabled: typeof persisted?.enabled === "boolean" ? persisted.enabled : false,
- });
- } catch (error) {
- console.error("[API ERROR] /api/settings/codex-service-tier GET:", error);
- return NextResponse.json({ error: "Failed to get config" }, { status: 500 });
- }
-}
-
-export async function PUT(request: Request) {
- let rawBody;
- try {
- rawBody = await request.json();
- } catch {
- return NextResponse.json(
- {
- error: {
- message: "Invalid request",
- details: [{ field: "body", message: "Invalid JSON body" }],
- },
- },
- { status: 400 }
- );
- }
-
- try {
- const validation = validateBody(updateCodexServiceTierSchema, rawBody);
- if (isValidationFailure(validation)) {
- return NextResponse.json({ error: validation.error }, { status: 400 });
- }
-
- const config = validation.data;
- await updateSettings({ codexServiceTier: config });
- setDefaultFastServiceTierEnabled(config.enabled);
-
- return NextResponse.json(config);
- } catch (error) {
- console.error("[API ERROR] /api/settings/codex-service-tier PUT:", error);
- return NextResponse.json({ error: "Failed to update config" }, { status: 500 });
- }
-}
diff --git a/src/instrumentation-node.ts b/src/instrumentation-node.ts
index 2e4d113e3b..becbc020fe 100755
--- a/src/instrumentation-node.ts
+++ b/src/instrumentation-node.ts
@@ -105,10 +105,11 @@ export async function registerNodejs(): Promise {
}
try {
- const [{ setCustomAliases }, { setDefaultFastServiceTierEnabled }] = await Promise.all([
- import("@omniroute/open-sse/services/modelDeprecation.ts"),
- import("@omniroute/open-sse/executors/codex.ts"),
- ]);
+ const [{ setCustomAliases }, { migrateCodexConnectionDefaultsFromLegacySettings }] =
+ await Promise.all([
+ import("@omniroute/open-sse/services/modelDeprecation.ts"),
+ import("@/lib/providers/codexConnectionDefaults"),
+ ]);
const settings = await getSettings();
if (settings.modelAliases) {
@@ -124,16 +125,20 @@ export async function registerNodejs(): Promise {
}
}
- const persisted =
- typeof settings.codexServiceTier === "string"
- ? JSON.parse(settings.codexServiceTier)
- : settings.codexServiceTier;
-
- if (typeof persisted?.enabled === "boolean") {
- setDefaultFastServiceTierEnabled(persisted.enabled);
+ const migration = await migrateCodexConnectionDefaultsFromLegacySettings();
+ if (migration.migrated) {
console.log(
- `[STARTUP] Restored Codex fast service tier: ${persisted.enabled ? "on" : "off"}`
+ `[STARTUP] Migrated Codex connection defaults for ${migration.updatedConnectionIds.length} connection(s)`
);
+ if (settings.cloudEnabled === true) {
+ const [{ syncToCloud }, { getConsistentMachineId }] = await Promise.all([
+ import("@/lib/cloudSync"),
+ import("@/shared/utils/machineId"),
+ ]);
+ const machineId = await getConsistentMachineId();
+ await syncToCloud(machineId);
+ console.log("[STARTUP] Synced migrated Codex connection defaults to cloud");
+ }
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
diff --git a/src/lib/db/providers.ts b/src/lib/db/providers.ts
index 756634d870..75a9a09709 100644
--- a/src/lib/db/providers.ts
+++ b/src/lib/db/providers.ts
@@ -7,6 +7,7 @@ import { getDbInstance, rowToCamel, cleanNulls } from "./core";
import { backupDbFile } from "./backup";
import { encryptConnectionFields, decryptConnectionFields } from "./encryption";
import { invalidateDbCache } from "./readCache";
+import { normalizeProviderSpecificData } from "@/lib/providers/requestDefaults";
type JsonRecord = Record;
@@ -67,6 +68,10 @@ export async function getProviderConnectionById(id: string) {
export async function createProviderConnection(data: JsonRecord) {
const db = getDbInstance() as unknown as DbLike;
const now = new Date().toISOString();
+ const normalizedProviderSpecificData = normalizeProviderSpecificData(
+ toStringOrNull(data.provider),
+ data.providerSpecificData
+ );
// Upsert check
// For Codex/OpenAI, a single email can have multiple workspaces (Team + Personal)
@@ -121,7 +126,11 @@ export async function createProviderConnection(data: JsonRecord) {
if (existing) {
const existingId = toStringOrNull(existing.id);
if (!existingId) return null;
- const merged = { ...toRecord(rowToCamel(existing)), ...data, updatedAt: now };
+ const merged: JsonRecord = { ...toRecord(rowToCamel(existing)), ...data, updatedAt: now };
+ merged.providerSpecificData = normalizeProviderSpecificData(
+ toStringOrNull(merged.provider),
+ merged.providerSpecificData
+ );
_updateConnectionRow(db, existingId, merged);
backupDbFile("pre-write");
return cleanNulls(merged);
@@ -192,8 +201,8 @@ export async function createProviderConnection(data: JsonRecord) {
connection[field] = data[field];
}
}
- if (data.providerSpecificData && Object.keys(data.providerSpecificData).length > 0) {
- connection.providerSpecificData = data.providerSpecificData;
+ if (normalizedProviderSpecificData && Object.keys(normalizedProviderSpecificData).length > 0) {
+ connection.providerSpecificData = normalizedProviderSpecificData;
}
_insertConnectionRow(db, encryptConnectionFields({ ...connection }));
@@ -347,7 +356,15 @@ export async function updateProviderConnection(id: string, data: JsonRecord) {
const existing = db.prepare("SELECT * FROM provider_connections WHERE id = ?").get(id);
if (!existing) return null;
- const merged = { ...rowToCamel(existing), ...data, updatedAt: new Date().toISOString() };
+ const merged: JsonRecord = {
+ ...toRecord(rowToCamel(existing)),
+ ...data,
+ updatedAt: new Date().toISOString(),
+ };
+ merged.providerSpecificData = normalizeProviderSpecificData(
+ toStringOrNull(merged.provider),
+ merged.providerSpecificData
+ );
_updateConnectionRow(db, id, encryptConnectionFields({ ...merged }));
backupDbFile("pre-write");
invalidateDbCache("connections"); // Bust connections read cache
diff --git a/src/lib/providers/codexConnectionDefaults.ts b/src/lib/providers/codexConnectionDefaults.ts
new file mode 100644
index 0000000000..ba124f4d59
--- /dev/null
+++ b/src/lib/providers/codexConnectionDefaults.ts
@@ -0,0 +1,84 @@
+import { getProviderConnections, updateProviderConnection } from "@/lib/db/providers";
+import { getSettings, updateSettings } from "@/lib/db/settings";
+import { getCodexRequestDefaults } from "./requestDefaults";
+
+type JsonRecord = Record;
+
+const MIGRATION_SETTING_KEY = "codexConnectionDefaultsMigrationV1";
+
+function asRecord(value: unknown): JsonRecord {
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
+}
+
+function parseLegacyCodexServiceTier(value: unknown): { enabled: boolean } {
+ if (typeof value === "string") {
+ try {
+ return parseLegacyCodexServiceTier(JSON.parse(value));
+ } catch {
+ return { enabled: false };
+ }
+ }
+
+ const record = asRecord(value);
+ return { enabled: record.enabled === true };
+}
+
+export async function migrateCodexConnectionDefaultsFromLegacySettings(): Promise<{
+ migrated: boolean;
+ updatedConnectionIds: string[];
+ legacyFastEnabled: boolean;
+}> {
+ const settings = await getSettings();
+ if (settings[MIGRATION_SETTING_KEY]) {
+ return {
+ migrated: false,
+ updatedConnectionIds: [],
+ legacyFastEnabled: parseLegacyCodexServiceTier(settings.codexServiceTier).enabled,
+ };
+ }
+
+ const legacyFastEnabled = parseLegacyCodexServiceTier(settings.codexServiceTier).enabled;
+ const codexConnections = await getProviderConnections({ provider: "codex" });
+ const updatedConnectionIds: string[] = [];
+
+ for (const connection of codexConnections) {
+ const providerSpecificData = asRecord(connection.providerSpecificData);
+ const existingDefaults = getCodexRequestDefaults(providerSpecificData);
+ const nextDefaults: JsonRecord = { ...existingDefaults };
+
+ if (!existingDefaults.reasoningEffort) {
+ nextDefaults.reasoningEffort = "medium";
+ }
+ if (legacyFastEnabled && !existingDefaults.serviceTier) {
+ nextDefaults.serviceTier = "priority";
+ }
+
+ const defaultsChanged =
+ nextDefaults.reasoningEffort !== existingDefaults.reasoningEffort ||
+ nextDefaults.serviceTier !== existingDefaults.serviceTier;
+
+ if (!defaultsChanged) continue;
+
+ await updateProviderConnection(connection.id, {
+ providerSpecificData: {
+ ...providerSpecificData,
+ requestDefaults: nextDefaults,
+ },
+ });
+ updatedConnectionIds.push(connection.id);
+ }
+
+ await updateSettings({
+ [MIGRATION_SETTING_KEY]: {
+ completedAt: new Date().toISOString(),
+ updatedConnectionIds,
+ legacyFastEnabled,
+ },
+ });
+
+ return {
+ migrated: true,
+ updatedConnectionIds,
+ legacyFastEnabled,
+ };
+}
diff --git a/src/lib/providers/requestDefaults.ts b/src/lib/providers/requestDefaults.ts
new file mode 100644
index 0000000000..bfa6af7540
--- /dev/null
+++ b/src/lib/providers/requestDefaults.ts
@@ -0,0 +1,101 @@
+type JsonRecord = Record;
+
+export const CODEX_REASONING_EFFORT_VALUES = ["none", "low", "medium", "high", "xhigh"] as const;
+
+export type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORT_VALUES)[number];
+
+const CODEX_REASONING_EFFORT_SET = new Set(CODEX_REASONING_EFFORT_VALUES);
+
+function asRecord(value: unknown): JsonRecord {
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
+}
+
+function normalizeString(value: unknown): string | undefined {
+ if (typeof value !== "string") return undefined;
+ const normalized = value.trim().toLowerCase();
+ return normalized || undefined;
+}
+
+export function normalizeCodexReasoningEffort(value: unknown): CodexReasoningEffort | undefined {
+ const normalized = normalizeString(value);
+ if (!normalized || !CODEX_REASONING_EFFORT_SET.has(normalized)) {
+ return undefined;
+ }
+ return normalized as CodexReasoningEffort;
+}
+
+export function normalizeCodexServiceTier(value: unknown): "priority" | undefined {
+ const normalized = normalizeString(value);
+ if (!normalized) return undefined;
+ if (normalized === "fast" || normalized === "priority") return "priority";
+ return undefined;
+}
+
+export function normalizeRequestDefaults(
+ provider: string | null | undefined,
+ value: unknown
+): JsonRecord | undefined {
+ const record = asRecord(value);
+ if (Object.keys(record).length === 0) return undefined;
+
+ const normalized: JsonRecord = { ...record };
+
+ if (provider === "codex") {
+ const reasoningEffort = normalizeCodexReasoningEffort(record.reasoningEffort);
+ if (reasoningEffort) {
+ normalized.reasoningEffort = reasoningEffort;
+ } else {
+ delete normalized.reasoningEffort;
+ }
+
+ const serviceTier = normalizeCodexServiceTier(record.serviceTier);
+ if (serviceTier) {
+ normalized.serviceTier = serviceTier;
+ } else {
+ delete normalized.serviceTier;
+ }
+ }
+
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
+}
+
+export function normalizeProviderSpecificData(
+ provider: string | null | undefined,
+ value: unknown
+): JsonRecord | undefined {
+ const record = asRecord(value);
+ if (Object.keys(record).length === 0) return undefined;
+
+ const normalized: JsonRecord = { ...record };
+
+ if ("requestDefaults" in normalized) {
+ const requestDefaults = normalizeRequestDefaults(provider, normalized.requestDefaults);
+ if (requestDefaults) {
+ normalized.requestDefaults = requestDefaults;
+ } else {
+ delete normalized.requestDefaults;
+ }
+ }
+
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
+}
+
+export function getProviderRequestDefaults(
+ provider: string | null | undefined,
+ providerSpecificData: unknown
+): JsonRecord {
+ return normalizeRequestDefaults(provider, asRecord(providerSpecificData).requestDefaults) || {};
+}
+
+export function getCodexRequestDefaults(providerSpecificData: unknown): {
+ reasoningEffort?: CodexReasoningEffort;
+ serviceTier?: "priority";
+} {
+ const defaults = getProviderRequestDefaults("codex", providerSpecificData);
+ const reasoningEffort = normalizeCodexReasoningEffort(defaults.reasoningEffort);
+ const serviceTier = normalizeCodexServiceTier(defaults.serviceTier);
+ return {
+ ...(reasoningEffort ? { reasoningEffort } : {}),
+ ...(serviceTier ? { serviceTier } : {}),
+ };
+}
diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts
index 9065c1878c..691e021908 100644
--- a/src/shared/validation/schemas.ts
+++ b/src/shared/validation/schemas.ts
@@ -11,6 +11,79 @@ function isHttpUrl(value: string): boolean {
}
}
+const CODEX_REASONING_EFFORT_VALUES = new Set(["none", "low", "medium", "high", "xhigh"]);
+const REQUEST_DEFAULT_SERVICE_TIER_VALUES = new Set(["priority", "fast"]);
+
+function validateProviderSpecificData(
+ data: Record | undefined,
+ ctx: z.RefinementCtx
+): void {
+ if (!data) return;
+
+ const baseUrl = data.baseUrl;
+ if (baseUrl !== undefined && (typeof baseUrl !== "string" || !isHttpUrl(baseUrl))) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "providerSpecificData.baseUrl must be a valid http(s) URL",
+ path: ["baseUrl"],
+ });
+ }
+
+ const customUserAgent = data.customUserAgent;
+ if (
+ customUserAgent !== undefined &&
+ customUserAgent !== null &&
+ (typeof customUserAgent !== "string" || customUserAgent.length > 500)
+ ) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "providerSpecificData.customUserAgent must be a string up to 500 chars",
+ path: ["customUserAgent"],
+ });
+ }
+
+ const requestDefaults = data.requestDefaults;
+ if (requestDefaults === undefined) return;
+ if (!requestDefaults || typeof requestDefaults !== "object" || Array.isArray(requestDefaults)) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "providerSpecificData.requestDefaults must be an object",
+ path: ["requestDefaults"],
+ });
+ return;
+ }
+
+ const requestDefaultsRecord = requestDefaults as Record;
+ const reasoningEffort = requestDefaultsRecord.reasoningEffort;
+ if (
+ reasoningEffort !== undefined &&
+ reasoningEffort !== null &&
+ (typeof reasoningEffort !== "string" ||
+ !CODEX_REASONING_EFFORT_VALUES.has(reasoningEffort.trim().toLowerCase()))
+ ) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message:
+ "providerSpecificData.requestDefaults.reasoningEffort must be one of none, low, medium, high, xhigh",
+ path: ["requestDefaults", "reasoningEffort"],
+ });
+ }
+
+ const serviceTier = requestDefaultsRecord.serviceTier;
+ if (
+ serviceTier !== undefined &&
+ serviceTier !== null &&
+ (typeof serviceTier !== "string" ||
+ !REQUEST_DEFAULT_SERVICE_TIER_VALUES.has(serviceTier.trim().toLowerCase()))
+ ) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "providerSpecificData.requestDefaults.serviceTier must be priority when provided",
+ path: ["requestDefaults", "serviceTier"],
+ });
+ }
+}
+
// Re-export validation helpers from dedicated module to avoid webpack barrel-file
// optimization bug that truncates exports from large files.
export { validateBody, isValidationFailure } from "./helpers";
@@ -30,27 +103,7 @@ export const createProviderSchema = z.object({
.record(z.string(), z.unknown())
.optional()
.superRefine((data, ctx) => {
- if (!data) return;
- const baseUrl = data.baseUrl;
- if (baseUrl !== undefined && (typeof baseUrl !== "string" || !isHttpUrl(baseUrl))) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: "providerSpecificData.baseUrl must be a valid http(s) URL",
- path: ["baseUrl"],
- });
- }
- const customUserAgent = data.customUserAgent;
- if (
- customUserAgent !== undefined &&
- customUserAgent !== null &&
- (typeof customUserAgent !== "string" || customUserAgent.length > 500)
- ) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: "providerSpecificData.customUserAgent must be a string up to 500 chars",
- path: ["customUserAgent"],
- });
- }
+ validateProviderSpecificData(data, ctx);
}),
});
@@ -643,12 +696,6 @@ export const updateThinkingBudgetSchema = z
}
});
-export const updateCodexServiceTierSchema = z
- .object({
- enabled: z.boolean(),
- })
- .strict();
-
const ipFilterModeSchema = z.enum(["blacklist", "whitelist"]);
const tempBanSchema = z.object({
ip: z.string().trim().min(1),
@@ -1101,27 +1148,7 @@ export const updateProviderConnectionSchema = z
.record(z.string(), z.unknown())
.optional()
.superRefine((data, ctx) => {
- if (!data) return;
- const baseUrl = data.baseUrl;
- if (baseUrl !== undefined && (typeof baseUrl !== "string" || !isHttpUrl(baseUrl))) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: "providerSpecificData.baseUrl must be a valid http(s) URL",
- path: ["baseUrl"],
- });
- }
- const customUserAgent = data.customUserAgent;
- if (
- customUserAgent !== undefined &&
- customUserAgent !== null &&
- (typeof customUserAgent !== "string" || customUserAgent.length > 500)
- ) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: "providerSpecificData.customUserAgent must be a string up to 500 chars",
- path: ["customUserAgent"],
- });
- }
+ validateProviderSpecificData(data, ctx);
}),
})
.superRefine((value, ctx) => {
diff --git a/tests/unit/codex-connection-defaults.test.mjs b/tests/unit/codex-connection-defaults.test.mjs
new file mode 100644
index 0000000000..76cfaf218f
--- /dev/null
+++ b/tests/unit/codex-connection-defaults.test.mjs
@@ -0,0 +1,132 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+
+const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-codex-defaults-"));
+process.env.DATA_DIR = TEST_DATA_DIR;
+
+const core = await import("../../src/lib/db/core.ts");
+const providersDb = await import("../../src/lib/db/providers.ts");
+const settingsDb = await import("../../src/lib/db/settings.ts");
+const { migrateCodexConnectionDefaultsFromLegacySettings } =
+ await import("../../src/lib/providers/codexConnectionDefaults.ts");
+
+async function resetStorage() {
+ core.resetDbInstance();
+ fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
+ fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
+}
+
+test.beforeEach(async () => {
+ await resetStorage();
+});
+
+test.after(async () => {
+ core.resetDbInstance();
+ fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
+});
+
+test("migration backfills Codex request defaults, preserves existing providerSpecificData, and is idempotent", async () => {
+ const first = await providersDb.createProviderConnection({
+ provider: "codex",
+ authType: "oauth",
+ email: "first@example.com",
+ providerSpecificData: {
+ workspaceId: "ws-first",
+ tag: "team-a",
+ codexLimitPolicy: { use5h: false, useWeekly: true },
+ },
+ });
+ const second = await providersDb.createProviderConnection({
+ provider: "codex",
+ authType: "oauth",
+ email: "second@example.com",
+ providerSpecificData: {
+ workspaceId: "ws-second",
+ tag: "team-b",
+ requestDefaults: { reasoningEffort: "high" },
+ },
+ });
+ const untouched = await providersDb.createProviderConnection({
+ provider: "codex",
+ authType: "oauth",
+ email: "third@example.com",
+ providerSpecificData: {
+ workspaceId: "ws-third",
+ tag: "team-c",
+ requestDefaults: { reasoningEffort: "low", serviceTier: "priority" },
+ },
+ });
+
+ await settingsDb.updateSettings({ codexServiceTier: { enabled: true } });
+
+ const firstRun = await migrateCodexConnectionDefaultsFromLegacySettings();
+ const rows = await providersDb.getProviderConnections({ provider: "codex" });
+ const byId = new Map(rows.map((row) => [row.id, row]));
+ const settings = await settingsDb.getSettings();
+
+ assert.equal(firstRun.migrated, true);
+ assert.deepEqual(firstRun.updatedConnectionIds.sort(), [first.id, second.id].sort());
+ assert.deepEqual(byId.get(first.id).providerSpecificData.requestDefaults, {
+ reasoningEffort: "medium",
+ serviceTier: "priority",
+ });
+ assert.deepEqual(byId.get(second.id).providerSpecificData.requestDefaults, {
+ reasoningEffort: "high",
+ serviceTier: "priority",
+ });
+ assert.deepEqual(byId.get(untouched.id).providerSpecificData.requestDefaults, {
+ reasoningEffort: "low",
+ serviceTier: "priority",
+ });
+ assert.equal(byId.get(first.id).providerSpecificData.tag, "team-a");
+ assert.deepEqual(byId.get(first.id).providerSpecificData.codexLimitPolicy, {
+ use5h: false,
+ useWeekly: true,
+ });
+ assert.ok(settings.codexConnectionDefaultsMigrationV1);
+
+ const secondRun = await migrateCodexConnectionDefaultsFromLegacySettings();
+ assert.equal(secondRun.migrated, false);
+ assert.deepEqual(secondRun.updatedConnectionIds, []);
+});
+
+test("provider connection persistence normalizes request defaults without dropping unrelated keys", async () => {
+ const created = await providersDb.createProviderConnection({
+ provider: "codex",
+ authType: "oauth",
+ email: "normalize@example.com",
+ providerSpecificData: {
+ workspaceId: "ws-normalize",
+ tag: "team-z",
+ requestDefaults: {
+ reasoningEffort: "HIGH",
+ serviceTier: "fast",
+ customFlag: "keep-me",
+ },
+ },
+ });
+
+ assert.deepEqual(created.providerSpecificData.requestDefaults, {
+ reasoningEffort: "high",
+ serviceTier: "priority",
+ customFlag: "keep-me",
+ });
+ assert.equal(created.providerSpecificData.workspaceId, "ws-normalize");
+ assert.equal(created.providerSpecificData.tag, "team-z");
+
+ const updated = await providersDb.updateProviderConnection(created.id, {
+ providerSpecificData: {
+ ...created.providerSpecificData,
+ requestDefaults: { reasoningEffort: "medium" },
+ },
+ });
+
+ assert.deepEqual(updated.providerSpecificData.requestDefaults, {
+ reasoningEffort: "medium",
+ });
+ assert.equal(updated.providerSpecificData.workspaceId, "ws-normalize");
+ assert.equal(updated.providerSpecificData.tag, "team-z");
+});
diff --git a/tests/unit/executor-codex.test.mjs b/tests/unit/executor-codex.test.mjs
index a4002382f2..96865046d2 100644
--- a/tests/unit/executor-codex.test.mjs
+++ b/tests/unit/executor-codex.test.mjs
@@ -7,8 +7,16 @@ import {
getCodexRateLimitKey,
getCodexResetTime,
parseCodexQuotaHeaders,
- setDefaultFastServiceTierEnabled,
} from "../../open-sse/executors/codex.ts";
+import {
+ DEFAULT_THINKING_CONFIG,
+ setThinkingBudgetConfig,
+ ThinkingMode,
+} from "../../open-sse/services/thinkingBudget.ts";
+
+test.afterEach(() => {
+ setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG);
+});
test("Codex helper functions isolate rate-limit scopes and parse quota headers", () => {
const quota = parseCodexQuotaHeaders(
@@ -102,26 +110,121 @@ test("CodexExecutor.transformRequest injects default instructions, clamps reason
test("CodexExecutor.transformRequest preserves compact requests and native passthrough semantics", () => {
const executor = new CodexExecutor();
- setDefaultFastServiceTierEnabled(true);
+ const body = {
+ _nativeCodexPassthrough: true,
+ instructions: "keep this",
+ stream: false,
+ };
+ const result = executor.transformRequest("gpt-5.3-codex", body, false, {
+ requestEndpointPath: "/responses/compact",
+ providerSpecificData: {
+ requestDefaults: { serviceTier: "priority" },
+ },
+ });
- try {
- const body = {
- _nativeCodexPassthrough: true,
- instructions: "keep this",
- stream: false,
- };
- const result = executor.transformRequest("gpt-5.3-codex", body, false, {
- requestEndpointPath: "/responses/compact",
- });
+ assert.equal(result._nativeCodexPassthrough, undefined);
+ 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.instructions, "keep this");
+});
- assert.equal(result._nativeCodexPassthrough, undefined);
- assert.equal(result.stream, undefined);
- assert.equal(result.service_tier, "priority");
- assert.equal(result.store, false);
- assert.equal(result.instructions, "keep this");
- } finally {
- setDefaultFastServiceTierEnabled(false);
- }
+test("CodexExecutor.transformRequest applies per-connection reasoning and service tier defaults", () => {
+ const executor = new CodexExecutor();
+ const result = executor.transformRequest(
+ "gpt-5.3-codex",
+ { model: "gpt-5.3-codex", input: [] },
+ false,
+ {
+ providerSpecificData: {
+ requestDefaults: {
+ reasoningEffort: "high",
+ serviceTier: "priority",
+ },
+ },
+ }
+ );
+
+ assert.equal(result.reasoning.effort, "high");
+ assert.equal(result.service_tier, "priority");
+});
+
+test("CodexExecutor.transformRequest keeps explicit request values ahead of connection defaults", () => {
+ const executor = new CodexExecutor();
+ const result = executor.transformRequest(
+ "gpt-5.3-codex",
+ {
+ model: "gpt-5.3-codex",
+ input: [],
+ reasoning_effort: "none",
+ service_tier: "standard",
+ },
+ false,
+ {
+ providerSpecificData: {
+ requestDefaults: {
+ reasoningEffort: "high",
+ serviceTier: "priority",
+ },
+ },
+ }
+ );
+
+ assert.equal(result.reasoning.effort, "none");
+ assert.equal(result.service_tier, "standard");
+});
+
+test("CodexExecutor.transformRequest lets model suffix beat connection reasoning defaults", () => {
+ const executor = new CodexExecutor();
+ const result = executor.transformRequest(
+ "gpt-5.3-codex-high",
+ { model: "gpt-5.3-codex-high", input: [] },
+ false,
+ {
+ providerSpecificData: {
+ requestDefaults: {
+ reasoningEffort: "low",
+ },
+ },
+ }
+ );
+
+ assert.equal(result.model, "gpt-5.3-codex");
+ assert.equal(result.reasoning.effort, "high");
+});
+
+test("CodexExecutor.transformRequest does not apply connection reasoning defaults when Thinking Budget is not passthrough", () => {
+ const executor = new CodexExecutor();
+ setThinkingBudgetConfig({ mode: ThinkingMode.AUTO });
+
+ const noDefaults = executor.transformRequest(
+ "gpt-5.3-codex",
+ { model: "gpt-5.3-codex", input: [] },
+ false,
+ {
+ providerSpecificData: {
+ requestDefaults: {
+ reasoningEffort: "high",
+ },
+ },
+ }
+ );
+ const explicit = executor.transformRequest(
+ "gpt-5.3-codex",
+ { model: "gpt-5.3-codex", input: [], reasoning_effort: "high" },
+ false,
+ {
+ providerSpecificData: {
+ requestDefaults: {
+ reasoningEffort: "low",
+ },
+ },
+ }
+ );
+
+ assert.equal(noDefaults.reasoning, undefined);
+ assert.equal(explicit.reasoning.effort, "high");
});
test("CodexExecutor.refreshCredentials refreshes OAuth tokens and returns null without a refresh token", async () => {
diff --git a/tests/unit/plan3-p0.test.mjs b/tests/unit/plan3-p0.test.mjs
index 8abba76620..428511cf9b 100644
--- a/tests/unit/plan3-p0.test.mjs
+++ b/tests/unit/plan3-p0.test.mjs
@@ -8,7 +8,7 @@ import { shouldUseNativeCodexPassthrough } from "../../open-sse/handlers/chatCor
import { translateRequest } from "../../open-sse/translator/index.ts";
import { GithubExecutor } from "../../open-sse/executors/github.ts";
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
-import { CodexExecutor, setDefaultFastServiceTierEnabled } from "../../open-sse/executors/codex.ts";
+import { CodexExecutor } from "../../open-sse/executors/codex.ts";
import { translateNonStreamingResponse } from "../../open-sse/handlers/responseTranslator.ts";
import { extractUsageFromResponse } from "../../open-sse/handlers/usageExtractor.ts";
import {
@@ -218,20 +218,19 @@ test("shouldUseNativeCodexPassthrough only enables responses-native Codex reques
);
});
-test("CodexExecutor can force fast service tier from settings", () => {
- setDefaultFastServiceTierEnabled(true);
-
- try {
- const executor = new CodexExecutor();
- const transformed = executor.transformRequest(
- "gpt-5.1-codex",
- { model: "gpt-5.1-codex", input: [] },
- true
- );
- assert.equal(transformed.service_tier, "priority");
- } finally {
- setDefaultFastServiceTierEnabled(false);
- }
+test("CodexExecutor can apply per-connection fast service tier defaults", () => {
+ const executor = new CodexExecutor();
+ const transformed = executor.transformRequest(
+ "gpt-5.1-codex",
+ { model: "gpt-5.1-codex", input: [] },
+ true,
+ {
+ providerSpecificData: {
+ requestDefaults: { serviceTier: "priority" },
+ },
+ }
+ );
+ assert.equal(transformed.service_tier, "priority");
});
test("CodexExecutor always requests SSE accept header", () => {
@@ -275,7 +274,8 @@ test("CodexExecutor preserves native responses payloads for Codex passthrough",
assert.equal(transformed.instructions, "custom system prompt");
assert.equal(transformed.store, false);
assert.deepEqual(transformed.metadata, { source: "codex-client" });
- assert.equal(transformed.reasoning_effort, "high");
+ assert.equal(transformed.reasoning.effort, "high");
+ assert.equal(transformed.reasoning_effort, undefined);
assert.ok(!("_nativeCodexPassthrough" in transformed));
});
diff --git a/tests/unit/stream-utils.test.mjs b/tests/unit/stream-utils.test.mjs
index 97cd1791f4..26dbcdf6e0 100644
--- a/tests/unit/stream-utils.test.mjs
+++ b/tests/unit/stream-utils.test.mjs
@@ -237,6 +237,49 @@ test("createSSEStream passthrough preserves Responses API events and completion
assert.equal(onCompletePayload.providerPayload.summary.object, "response");
});
+test("buildStreamSummaryFromEvents falls back to response.output_text.delta when completed output is empty", () => {
+ const summary = buildStreamSummaryFromEvents(
+ [
+ {
+ index: 0,
+ data: {
+ type: "response.output_text.delta",
+ delta: "Hello ",
+ },
+ },
+ {
+ index: 1,
+ data: {
+ type: "response.output_text.delta",
+ delta: "world",
+ },
+ },
+ {
+ index: 2,
+ data: {
+ type: "response.completed",
+ response: {
+ id: "resp_fallback",
+ object: "response",
+ model: "gpt-5.4",
+ status: "completed",
+ output: [],
+ usage: { output_tokens: 2 },
+ },
+ },
+ },
+ ],
+ FORMATS.OPENAI_RESPONSES,
+ "gpt-5.4"
+ );
+
+ assert.equal(summary.object, "response");
+ assert.equal(summary.output[0].type, "message");
+ assert.equal(summary.output[0].content[0].type, "output_text");
+ assert.equal(summary.output[0].content[0].text, "Hello world");
+ assert.equal(summary.usage.output_tokens, 2);
+});
+
test("createSSEStream translate mode aborts on Responses failure with rate limit error", async () => {
let onCompletePayload = null;