mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
@@ -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"];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
55
src/app/api/settings/codex-service-tier/route.ts
Normal file
55
src/app/api/settings/codex-service-tier/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user