feat(codex): add fast tier settings toggle

Add a default-off dashboard setting that injects Codex fast service tier only when the request did not already specify one.

Also preserve service_tier through OpenAI-to-Responses translation and restore the setting at startup.
This commit is contained in:
Kfir Amar
2026-03-14 20:33:46 +02:00
parent 75a6d850fc
commit 00188f75ae
8 changed files with 246 additions and 2 deletions

View File

@@ -6,6 +6,20 @@ import { refreshCodexToken } from "../services/tokenRefresh.ts";
// Ordered list of effort levels from lowest to highest
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 normalizeServiceTierValue(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const normalized = value.trim().toLowerCase();
if (!normalized) return undefined;
if (normalized === "fast") return CODEX_FAST_WIRE_VALUE;
return normalized;
}
export function setDefaultFastServiceTierEnabled(enabled: boolean): void {
defaultFastServiceTierEnabled = enabled;
}
/**
* Maximum reasoning effort allowed per Codex model.
@@ -103,6 +117,13 @@ export class CodexExecutor extends BaseExecutor {
// Ensure store is false (Codex requirement)
body.store = false;
const requestServiceTier = normalizeServiceTierValue(body.service_tier);
if (requestServiceTier) {
body.service_tier = requestServiceTier;
} else if (defaultFastServiceTierEnabled) {
body.service_tier = CODEX_FAST_WIRE_VALUE;
}
// 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"];

View File

@@ -363,6 +363,7 @@ export function openaiToOpenAIResponsesRequest(
}
// Pass through relevant fields
if (root.service_tier !== undefined) result.service_tier = root.service_tier;
if (root.temperature !== undefined) result.temperature = root.temperature;
if (root.max_tokens !== undefined) result.max_tokens = root.max_tokens;
if (root.top_p !== undefined) result.top_p = root.top_p;

View File

@@ -0,0 +1,100 @@
"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 (
<Card>
<div className="flex items-center gap-3 mb-5">
<div className="p-2 rounded-lg bg-sky-500/10 text-sky-500">
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
bolt
</span>
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold">Codex Fast Service Tier</h3>
<p className="text-sm text-text-muted">
Inject `service_tier=fast` into Codex requests when the client leaves it unset.
</p>
</div>
{status === "saved" && (
<span className="text-xs font-medium text-emerald-500 flex items-center gap-1">
<span className="material-symbols-outlined text-[14px]">check_circle</span>
Saved
</span>
)}
{status === "error" && (
<span className="text-xs font-medium text-rose-500 flex items-center gap-1">
<span className="material-symbols-outlined text-[14px]">error</span>
Failed to save
</span>
)}
</div>
<div className="flex items-center justify-between p-4 rounded-lg bg-surface/30 border border-border/30">
<div>
<p className="text-sm font-medium">Force fast tier for Codex</p>
<p className="text-xs text-text-muted mt-0.5">
Off by default. Applies only to Codex requests and does not override an explicit tier.
</p>
</div>
<button
onClick={() => save(!enabled)}
disabled={loading || saving}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
enabled ? "bg-sky-500" : "bg-white/10"
}`}
>
<span
className={`inline-block h-4 w-4 rounded-full bg-white transition-transform ${
enabled ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
</div>
</Card>
);
}

View File

@@ -12,6 +12,7 @@ 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 ModelAliasesTab from "./components/ModelAliasesTab";
import BackgroundDegradationTab from "./components/BackgroundDegradationTab";
@@ -85,6 +86,7 @@ export default function SettingsPage() {
{activeTab === "ai" && (
<div className="flex flex-col gap-6">
<ThinkingBudgetTab />
<CodexServiceTierTab />
<SystemPromptTab />
<CacheStatsCard />
</div>

View File

@@ -0,0 +1,55 @@
import { NextResponse, type Request } 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;
setDefaultFastServiceTierEnabled(config.enabled);
await updateSettings({ codexServiceTier: config });
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 });
}
}

View File

@@ -52,7 +52,9 @@ export async function register() {
try {
const { getSettings } = await import("@/lib/db/settings");
const { setCustomAliases } = await import("@omniroute/open-sse/services/modelDeprecation.ts");
const { setDefaultFastServiceTierEnabled } = await import("@omniroute/open-sse/executors/codex.ts");
const settings = await getSettings();
if (settings.modelAliases) {
const aliases =
typeof settings.modelAliases === "string"
@@ -65,9 +67,19 @@ export async function register() {
);
}
}
const persisted =
typeof settings.codexServiceTier === "string"
? JSON.parse(settings.codexServiceTier)
: settings.codexServiceTier;
if (typeof persisted?.enabled === "boolean") {
setDefaultFastServiceTierEnabled(persisted.enabled);
console.log(`[STARTUP] Restored Codex fast service tier: ${persisted.enabled ? "on" : "off"}`);
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
console.warn("[STARTUP] Could not restore model aliases:", msg);
console.warn("[STARTUP] Could not restore runtime settings:", msg);
}
// Compliance: Initialize audit_log table + cleanup expired logs

View File

@@ -439,6 +439,12 @@ 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),

View File

@@ -6,7 +6,10 @@ import { getModelInfoCore } from "../../open-sse/services/model.ts";
import { detectFormat } from "../../open-sse/services/provider.ts";
import { translateRequest } from "../../open-sse/translator/index.ts";
import { GithubExecutor } from "../../open-sse/executors/github.ts";
import { CodexExecutor } from "../../open-sse/executors/codex.ts";
import {
CodexExecutor,
setDefaultFastServiceTierEnabled,
} from "../../open-sse/executors/codex.ts";
import { translateNonStreamingResponse } from "../../open-sse/handlers/responseTranslator.ts";
import { extractUsageFromResponse } from "../../open-sse/handlers/usageExtractor.ts";
import { parseSSEToResponsesOutput } from "../../open-sse/handlers/sseParser.ts";
@@ -60,6 +63,32 @@ test("CodexExecutor forces stream=true for upstream compatibility", () => {
assert.equal(transformed.stream, true);
});
test("CodexExecutor maps fast service tier to priority", () => {
const executor = new CodexExecutor();
const transformed = executor.transformRequest(
"gpt-5.1-codex",
{ model: "gpt-5.1-codex", input: [], service_tier: "fast" },
true
);
assert.equal(transformed.service_tier, "priority");
});
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 always requests SSE accept header", () => {
const executor = new CodexExecutor();
const headers = executor.buildHeaders({ accessToken: "test-token" }, false);
@@ -166,6 +195,24 @@ test("translateRequest normalizes openai-responses input string into list payloa
assert.equal(translated.input[0].content[0].text, "hello from responses");
});
test("translateRequest preserves service_tier when converting openai to openai-responses", () => {
const translated = translateRequest(
FORMATS.OPENAI,
FORMATS.OPENAI_RESPONSES,
"gpt-5.1-codex",
{
model: "gpt-5.1-codex",
messages: [{ role: "user", content: "hello from chat completions" }],
service_tier: "fast",
stream: false,
},
false
);
assert.equal(translated.service_tier, "fast");
assert.ok(Array.isArray(translated.input));
});
test("parseSSEToResponsesOutput parses completed response from SSE payload", () => {
const rawSSE = [
"event: response.created",