Skip to content
Closed
26 changes: 19 additions & 7 deletions open-sse/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,13 +362,25 @@ export function unavailableResponse(
) {
const retryAfterSec = normalizeRetryAfterSeconds(retryAfter);
const msg = retryAfterHuman ? `${message} (${retryAfterHuman})` : message;
return new Response(JSON.stringify({ error: { message: msg } }), {
status: statusCode,
headers: {
"Content-Type": "application/json",
"Retry-After": String(retryAfterSec),
},
});
const errorType = statusCode === 429 ? "rate_limit_error" : "server_error";
const errorCode = statusCode === 429 ? "rate_limited" : "unavailable";
return new Response(
JSON.stringify({
type: "error",
error: {
message: msg,
type: errorType,
code: errorCode,
},
}),
{
status: statusCode,
headers: {
"Content-Type": "application/json",
"Retry-After": String(retryAfterSec),
},
}
);
}

export function providerCircuitOpenResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ interface ApiKey {
usageLimitEnabled?: boolean;
dailyUsageLimitUsd?: number | null;
weeklyUsageLimitUsd?: number | null;
usageCommandShowUsd?: boolean;
allowedQuotas?: string[] | null;
createdAt: string;
}
Expand Down Expand Up @@ -706,6 +707,7 @@ export default function ApiManagerPageClient() {
usageLimitEnabled: boolean,
dailyUsageLimitUsd: number | null,
weeklyUsageLimitUsd: number | null,
usageCommandShowUsd: boolean,
blockedModels: string[]
) => {
if (!editingKey || !editingKey.id) return;
Expand Down Expand Up @@ -777,6 +779,7 @@ export default function ApiManagerPageClient() {
usageLimitEnabled,
dailyUsageLimitUsd,
weeklyUsageLimitUsd,
usageCommandShowUsd,
}),
});

Expand Down Expand Up @@ -1544,6 +1547,7 @@ const PermissionsModal = memo(function PermissionsModal({
usageLimitEnabled: boolean,
dailyUsageLimitUsd: number | null,
weeklyUsageLimitUsd: number | null,
usageCommandShowUsd: boolean,
blockedModels: string[]
) => void;
}) {
Expand Down Expand Up @@ -1628,6 +1632,9 @@ const PermissionsModal = memo(function PermissionsModal({
apiKey?.allowUsageCommand === true
);
const [usageLimitEnabled, setUsageLimitEnabled] = useState(apiKey?.usageLimitEnabled === true);
const [usageCommandShowUsd, setUsageCommandShowUsd] = useState(
apiKey?.usageCommandShowUsd === true
);
const [dailyUsageLimitUsd, setDailyUsageLimitUsd] = useState(
typeof apiKey?.dailyUsageLimitUsd === "number" && apiKey.dailyUsageLimitUsd > 0
? String(apiKey.dailyUsageLimitUsd)
Expand Down Expand Up @@ -1830,6 +1837,7 @@ const PermissionsModal = memo(function PermissionsModal({
usageLimitEnabled,
parseUsdLimitInput(dailyUsageLimitUsd),
parseUsdLimitInput(weeklyUsageLimitUsd),
usageCommandShowUsd,
blockedModels
);
}, [
Expand Down Expand Up @@ -1863,6 +1871,7 @@ const PermissionsModal = memo(function PermissionsModal({
disableNonPublicModels,
usageCommandEnabled,
usageLimitEnabled,
usageCommandShowUsd,
dailyUsageLimitUsd,
weeklyUsageLimitUsd,
parseUsdLimitInput,
Expand Down Expand Up @@ -2441,11 +2450,13 @@ const PermissionsModal = memo(function PermissionsModal({
<p className="text-xs text-text-muted">{t("localUsageCommandDesc")}</p>
<UsageLimitSettings
enabled={usageLimitEnabled}
showUsdInUsageCommand={usageCommandShowUsd}
dailyLimitUsd={dailyUsageLimitUsd}
weeklyLimitUsd={weeklyUsageLimitUsd}
enabledLabel={tc("enabled")}
disabledLabel={tc("disabled")}
onEnabledChange={setUsageLimitEnabled}
onShowUsdInUsageCommandChange={setUsageCommandShowUsd}
onDailyLimitUsdChange={setDailyUsageLimitUsd}
onWeeklyLimitUsdChange={setWeeklyUsageLimitUsd}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@ import { Input } from "@/shared/components";

export function UsageLimitSettings({
enabled,
showUsdInUsageCommand,
dailyLimitUsd,
weeklyLimitUsd,
enabledLabel,
disabledLabel,
onEnabledChange,
onShowUsdInUsageCommandChange,
onDailyLimitUsdChange,
onWeeklyLimitUsdChange,
}: {
enabled: boolean;
showUsdInUsageCommand: boolean;
dailyLimitUsd: string;
weeklyLimitUsd: string;
enabledLabel: string;
disabledLabel: string;
onEnabledChange: (enabled: boolean) => void;
onShowUsdInUsageCommandChange: (enabled: boolean) => void;
onDailyLimitUsdChange: (value: string) => void;
onWeeklyLimitUsdChange: (value: string) => void;
}) {
Expand Down Expand Up @@ -70,6 +74,29 @@ export function UsageLimitSettings({
/>
</div>
</div>
<div className="mt-3 flex items-start justify-between gap-3 rounded-md border border-border/40 bg-background/40 p-2.5">
<div className="min-w-0">
<p className="text-xs font-medium text-text-main">@@om-usage USD display</p>
<p className="mt-0.5 text-[11px] text-text-muted">
When disabled, @@om-usage reports this key quota as percentages.
</p>
</div>
<button
type="button"
role="switch"
aria-checked={showUsdInUsageCommand}
disabled={!enabled}
onClick={() => onShowUsdInUsageCommandChange(!showUsdInUsageCommand)}
className={`inline-flex shrink-0 items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-semibold transition-colors ${
showUsdInUsageCommand
? "border border-emerald-500/30 bg-emerald-500/15 text-emerald-700 dark:text-emerald-300"
: "border border-border bg-black/5 text-text-muted dark:bg-white/5"
} ${!enabled ? "cursor-not-allowed opacity-50" : ""}`}
>
<span className="material-symbols-outlined text-[14px]">attach_money</span>
{showUsdInUsageCommand ? enabledLabel : disabledLabel}
</button>
</div>
<p className="mt-2 text-[11px] text-text-muted">
Weekly quota follows the cached Claude weekly reset when available; otherwise it falls back
to a rolling 7 day window. Daily quota uses the Fortaleza calendar day.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,19 @@ import { useTranslations } from "next-intl";
import { cn } from "@/shared/utils/cn";
import { useApiKey } from "../../providers/hooks/useApiKey";
import { useProviderModels } from "../../providers/hooks/useProviderModels";
import {
CODEX_PLAYGROUND_REASONING_EFFORT,
type CodexResponseMetadata,
buildCodexResponseMetadata,
buildResponsesInputMessages,
extractResponsesOutputText,
formatCodexUsageValue,
isCodexPlaygroundProvider,
normalizeCodexResponsesModel,
} from "./llmChatResponsesMetadata";

const ENDPOINT = "/api/v1/chat/completions";
const RESPONSES_ENDPOINT = "/api/v1/responses";

/** Header used to test a specific API key's policy from the dashboard playground
* without exposing the key secret to the browser — the gateway resolves the key
Expand Down Expand Up @@ -57,6 +68,7 @@ interface Message {
role: "user" | "assistant";
content: string;
model?: string;
metadata?: CodexResponseMetadata;
}

interface Stats {
Expand Down Expand Up @@ -118,6 +130,59 @@ function extractUsage(line: string): { prompt_tokens?: number; completion_tokens
}
}

function CodexResponseMetadataPanel({
metadata,
rawOpen,
onToggleRaw,
}: {
metadata: CodexResponseMetadata;
rawOpen: boolean;
onToggleRaw: () => void;
}) {
const usageEntries = Object.entries(metadata.usage);
const fields = [
["requested", metadata.requestedModel],
["resolved", metadata.resolvedModel],
["reasoning", metadata.reasoningEffort],
["response", metadata.responseId],
].filter(([, value]) => typeof value === "string" && value.trim().length > 0);

return (
<div className="mt-2 rounded-md border border-border bg-bg-subtle/70 px-3 py-2 text-[11px] text-text-muted">
{usageEntries.length > 0 && (
<div className="font-mono leading-relaxed break-words text-text-muted">
{usageEntries.map(([key, value]) => `${key}=${formatCodexUsageValue(value)}`).join(" · ")}
</div>
)}
{fields.length > 0 && (
<div className="mt-2 grid gap-x-4 gap-y-1 sm:grid-cols-2">
{fields.map(([label, value]) => (
<div key={label} className="min-w-0 font-mono">
<span className="text-text-muted/70">{label}=</span>
<span className="break-all text-text-main/80">{value}</span>
</div>
))}
</div>
)}
<button
type="button"
onClick={onToggleRaw}
className="mt-2 inline-flex items-center gap-1 text-[11px] font-medium text-accent hover:text-accent/80"
>
<span className="material-symbols-outlined text-[13px]">
{rawOpen ? "expand_less" : "data_object"}
</span>
{rawOpen ? "Hide raw Responses JSON" : "View raw Responses JSON"}
</button>
{rawOpen && (
<pre className="mt-2 max-h-56 overflow-auto rounded border border-border bg-bg-card p-2 text-[11px] text-text-main">
{JSON.stringify(metadata.raw, null, 2)}
</pre>
)}
</div>
);
}

export function LlmChatCard({
providerId,
initialModel,
Expand Down Expand Up @@ -157,6 +222,7 @@ export function LlmChatCard({
const [input, setInput] = useState<string>("");
const [streaming, setStreaming] = useState<boolean>(false);
const [stats, setStats] = useState<Stats | null>(null);
const [expandedRawMessages, setExpandedRawMessages] = useState<Set<number>>(() => new Set());
const abortRef = useRef<AbortController | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
Expand All @@ -168,6 +234,8 @@ export function LlmChatCard({
// also covers vendor-namespaced ids (e.g. `moonshotai/kimi-k2.6`) that already
// contain a slash but still need the provider prefix (#3050).
const qualifiedModel = qualifyPlaygroundModel(effectiveModel, providerId);
const isCodexPlayground = isCodexPlaygroundProvider(providerId);
const codexResponsesModel = normalizeCodexResponsesModel(effectiveModel || qualifiedModel);

// Autofocus textarea in embedded mode
useEffect(() => {
Expand Down Expand Up @@ -216,6 +284,70 @@ export function LlmChatCard({
};
const playgroundKeyId = resolvePlaygroundKeyId(selectedKey, keys);
if (playgroundKeyId) headers[PLAYGROUND_KEY_ID_HEADER] = playgroundKeyId;
if (isCodexPlayground) {
const res = await fetch(RESPONSES_ENDPOINT, {
method: "POST",
signal: controller.signal,
credentials: "same-origin",
headers,
body: JSON.stringify({
model: codexResponsesModel,
input: buildResponsesInputMessages([...messages, userMsg]),
reasoning: { effort: CODEX_PLAYGROUND_REASONING_EFFORT },
stream: false,
store: false,
}),
});

const payload: unknown = await res.json().catch(() => null);

if (!res.ok) {
const errData = payload && typeof payload === "object" ? payload : null;
const errMsg =
errData && (errData as Record<string, unknown>).error
? String(
(((errData as Record<string, unknown>).error as Record<string, unknown>)
?.message as string | undefined) ?? `HTTP ${res.status}`
)
: `HTTP ${res.status}`;
setMessages((prev) => {
const next = [...prev];
const last = next[next.length - 1];
next[next.length - 1] = { ...last, role: "assistant", content: `[Error: ${errMsg}]` };
return next;
});
return;
}

const metadata = buildCodexResponseMetadata({
payload,
headers: res.headers,
requestedModel: codexResponsesModel,
reasoningEffort: CODEX_PLAYGROUND_REASONING_EFFORT,
fallbackLatencyMs: performance.now() - t0,
});
const responseText = extractResponsesOutputText(payload) || "[No text output]";

setMessages((prev) => {
const next = [...prev];
const last = next[next.length - 1];
next[next.length - 1] = {
...last,
role: "assistant",
content: responseText,
model: metadata.resolvedModel || codexResponsesModel,
metadata,
};
return next;
});
setStats({
latencyMs: metadata.latencyMs,
tokensIn: metadata.tokensIn,
tokensOut: metadata.tokensOut,
});
return;
}

const res = await fetch(ENDPOINT, {
method: "POST",
signal: controller.signal,
Expand Down Expand Up @@ -325,7 +457,17 @@ export function LlmChatCard({
// Refocus textarea so user can keep typing
requestAnimationFrame(() => textareaRef.current?.focus());
}
}, [input, streaming, selectedKey, keys, providerId, qualifiedModel, messages]);
}, [
input,
streaming,
selectedKey,
keys,
providerId,
qualifiedModel,
isCodexPlayground,
codexResponsesModel,
messages,
]);

const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
Expand All @@ -342,8 +484,18 @@ export function LlmChatCard({
if (streaming) abortRef.current?.abort();
setMessages([]);
setStats(null);
setExpandedRawMessages(new Set());
}, [streaming]);

const toggleRawMessage = useCallback((index: number) => {
setExpandedRawMessages((prev) => {
const next = new Set(prev);
if (next.has(index)) next.delete(index);
else next.add(index);
return next;
});
}, []);

useImperativeHandle(
controlsRef,
() => ({
Expand Down Expand Up @@ -502,6 +654,13 @@ export function LlmChatCard({
<span className="inline-block w-1.5 h-3.5 bg-text-main ml-0.5 align-text-bottom animate-pulse" />
)}
</div>
{!isUser && !isError && msg.metadata && (
<CodexResponseMetadataPanel
metadata={msg.metadata}
rawOpen={expandedRawMessages.has(i)}
onToggleRaw={() => toggleRawMessage(i)}
/>
)}
</div>
</div>
);
Expand Down
Loading
Loading