Files
codex-retry-gateway/ui-src/src/App.tsx
T

2459 lines
109 KiB
TypeScript

import { FormEvent, startTransition, useEffect, useState } from "react";
type PageKey = "overview" | "requests" | "profiles" | "rules" | "logs";
type Tone = "" | "success" | "error";
type ReasoningMatchMode = "formula_518n_minus_2" | "manual";
type AuthMode = "passthrough" | "fixed_bearer" | "manual_bearer" | "auth_json";
type GatewayConfig = {
profile_name?: string;
image_profile_name?: string;
listen_host?: string;
listen_port?: number;
management_access_key?: string;
management_access_key_configured?: boolean;
upstream_base_url?: string;
upstream_auth_mode?: string;
upstream_auth_env?: string | null;
upstream_auth_json_key?: string | null;
image_base_url?: string;
image_auth_mode?: string;
image_auth_env?: string | null;
image_auth_json_key?: string | null;
request_history_limit?: number;
model_remap?: string;
endpoints?: string[];
reasoning_match_mode?: ReasoningMatchMode;
reasoning_equals?: number[];
retryable_status_codes?: number[];
retryable_error_messages?: string[];
upstream_fetch_retry_attempts?: number;
upstream_fetch_retry_backoff_ms?: number;
non_stream_status_code?: number;
log_match?: boolean;
};
type Metrics = {
started_at?: string;
persistent_since?: string | null;
total_proxy_request_count?: number;
inspected_response_count?: number;
matched_response_count?: number;
reasoning_516_count?: number;
reasoning_516_ratio?: number;
token_totals?: {
input_tokens?: number;
output_tokens?: number;
total_tokens?: number;
reasoning_tokens?: number;
cached_tokens?: number;
};
observed_reasoning_counts?: Record<string, number>;
observed_reasoning_counts_total_keys?: number;
observed_reasoning_counts_omitted?: number;
};
type StatusPayload = {
ok: boolean;
listen?: string;
config?: GatewayConfig;
state?: {
provider_name?: string;
codex_current_base_url?: string;
latest_backup_path?: string;
};
paths?: {
config_path?: string;
profiles_dir?: string;
image_profiles_dir?: string;
log_path?: string;
thread_rules_path?: string;
};
metrics?: Metrics;
};
type Usage = {
input_tokens?: number | null;
output_tokens?: number | null;
total_tokens?: number | null;
reasoning_tokens?: number | null;
cached_tokens?: number | null;
};
type RequestEntry = {
seq: number;
request_id?: string | null;
response_id?: string | null;
thread_id?: string | null;
lifecycle_state?: string | null;
started_at?: string;
first_response_at?: string | null;
first_response_delay_ms?: number | null;
finished_at?: string | null;
last_activity_at?: string | null;
duration_ms?: number | null;
profile_name?: string;
method?: string;
path?: string;
request_body_bytes?: number | null;
response_bytes_received?: number | null;
model?: string | null;
requested_model?: string | null;
forwarded_model?: string | null;
reasoning_effort?: string | null;
reasoning_summary?: string | null;
response_stream?: boolean;
stream_chunk_count?: number | null;
usage_last_updated_at?: string | null;
upstream_attempt_count?: number | null;
reasoning_guard_enabled?: boolean;
reasoning_guard_thread_override?: string | null;
reasoning_retry_enabled?: boolean;
reasoning_retry_query_count?: number | null;
reasoning_retry_round_count?: number | null;
reasoning_retry_current_round?: number | null;
reasoning_retry_current_width?: number | null;
reasoning_retry_current_firsts?: Array<{
round?: number | null;
slot?: number | null;
first_response_at?: string | null;
first_response_delay_ms?: number | null;
outcome?: string | null;
status_code?: number | null;
upstream_status_code?: number | null;
reasoning_tokens?: number | null;
matched?: boolean | null;
}> | null;
reasoning_retry_winner_round?: number | null;
reasoning_retry_winner_slot?: number | null;
reasoning_retry_stop_reason?: string | null;
reasoning_retry_thread_mode?: string | null;
reasoning_retry_extra_inspected_count?: number | null;
reasoning_retry_extra_matched_count?: number | null;
reasoning_retry_extra_usage?: Usage | null;
reasoning_retry_extra_reasoning_counts?: Record<string, number> | null;
matched?: boolean;
status_code?: number | null;
upstream_status_code?: number | null;
upstream?: {
origin?: string;
path?: string;
route?: "default" | "images" | string;
auth_mode?: string;
auth_source?: string;
authorization_configured?: boolean;
status?: number;
content_type?: string;
} | null;
reasoning_tokens?: number | null;
usage?: Usage | null;
error?: string | null;
};
type RequestsPayload = {
total_entries?: number;
latest_seq?: number;
entries?: RequestEntry[];
};
type ThreadRuleEntry = {
thread_id: string;
reasoning_intercept_enabled: boolean;
updated_at?: string | null;
};
type ThreadRulesPayload = {
ok: boolean;
thread_rules_path?: string;
rules?: ThreadRuleEntry[];
saved_rule?: ThreadRuleEntry | null;
removed_rule?: ThreadRuleEntry | null;
message?: string;
};
type ProfileFormModel = {
listen_host?: string;
listen_port?: string;
upstream_base_url?: string;
auth_mode?: string;
auth_env?: string;
auth_file?: string;
manual_secret_file?: string;
manual_secret_configured?: boolean;
auth_json_path?: string;
auth_json_key?: string;
request_history_limit?: string;
model_remap?: string;
reasoning_match_mode?: ReasoningMatchMode;
reasoning_equals?: string;
retryable_status_codes?: string;
retryable_error_messages?: string[];
upstream_fetch_retry_attempts?: string;
upstream_fetch_retry_backoff_ms?: string;
endpoints?: string[];
};
type Profile = {
name: string;
active: boolean;
file_path?: string;
summary?: {
listen_host?: string;
listen_port?: string;
upstream_base_url?: string;
auth_mode?: string;
auth_source?: string;
request_history_limit?: string;
model_remap?: string;
reasoning_match_mode?: ReasoningMatchMode;
reasoning_equals?: string;
};
form?: ProfileFormModel;
};
type ImageProfileFormModel = {
base_url?: string;
auth_mode?: string;
auth_env?: string;
auth_file?: string;
manual_secret_file?: string;
manual_secret_configured?: boolean;
auth_json_path?: string;
auth_json_key?: string;
};
type ImageProfile = {
name: string;
active: boolean;
file_path?: string;
summary?: {
base_url?: string;
auth_mode?: string;
auth_source?: string;
};
form?: ImageProfileFormModel;
};
type ProfilesPayload = {
profiles_dir?: string;
active_profile?: string;
applied_profile?: {
profile?: string;
hot_swapped?: boolean;
listen?: string;
upstream_base_url?: string;
} | null;
profiles?: Profile[];
};
type ImageProfilesPayload = {
image_profiles_dir?: string;
active_image_profile?: string;
applied_image_profile?: {
image_profile?: string;
hot_swapped?: boolean;
image_base_url?: string;
} | null;
image_profiles?: ImageProfile[];
};
type LogEntry = {
seq: number;
at?: string;
message?: string;
};
type LogsPayload = {
total_entries?: number;
latest_seq?: number;
entries?: LogEntry[];
};
type MessageState = {
text: string;
tone: Tone;
};
type ProfileFormState = {
name: string;
listen_host: string;
listen_port: string;
upstream_base_url: string;
auth_mode: AuthMode;
auth_env: string;
auth_file: string;
manual_secret: string;
manual_secret_file: string;
manual_secret_configured: boolean;
auth_json_path: string;
auth_json_key: string;
request_history_limit: string;
model_remap: string;
reasoning_match_mode: ReasoningMatchMode;
reasoning_equals: string;
retryable_status_codes: string;
retryable_error_messages: string;
upstream_fetch_retry_attempts: string;
upstream_fetch_retry_backoff_ms: string;
endpoints: string;
};
type ImageProfileFormState = {
name: string;
base_url: string;
auth_mode: AuthMode;
auth_env: string;
auth_file: string;
manual_secret: string;
manual_secret_file: string;
manual_secret_configured: boolean;
auth_json_path: string;
auth_json_key: string;
};
type RuleFormState = {
reasoning_match_mode: ReasoningMatchMode;
reasoning_equals: string;
retryable_status_codes: string;
retryable_error_messages: string;
upstream_fetch_retry_attempts: string;
upstream_fetch_retry_backoff_ms: string;
endpoints: string;
non_stream_status_code: string;
log_match: boolean;
};
const api = {
status: "/__codex_retry_gateway/api/status",
config: "/__codex_retry_gateway/api/config",
logs: "/__codex_retry_gateway/api/logs",
requests: "/__codex_retry_gateway/api/requests",
threadRules: "/__codex_retry_gateway/api/thread-rules",
profiles: "/__codex_retry_gateway/api/profiles",
profileProbe: "/__codex_retry_gateway/api/profiles/probe",
profileSwitch: "/__codex_retry_gateway/api/profiles/switch",
imageProfiles: "/__codex_retry_gateway/api/image-profiles",
imageProfileProbe: "/__codex_retry_gateway/api/image-profiles/probe",
imageProfileSwitch: "/__codex_retry_gateway/api/image-profiles/switch",
restore: "/__codex_retry_gateway/api/restore",
};
const ACCESS_KEY_STORAGE_KEY = "codex_retry_gateway_access_key";
const REQUEST_PAGE_SIZE = 20;
const LOG_PAGE_SIZE = 200;
const zhNumberFormatter = new Intl.NumberFormat("zh-CN");
const zhTimestampFormatter = new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h23",
});
type ProfileProbePayload = {
ok: boolean;
profile?: string;
image_profile?: string;
upstream_base_url?: string;
image_base_url?: string;
auth_mode?: string;
auth_source?: string;
authorization_configured?: boolean;
model_remap?: string;
probes?: Array<{
kind?: string;
target?: string;
status?: number;
requested_model?: string;
forwarded_model?: string;
content_type?: string;
body_preview?: string;
}>;
};
const pageCopy: Record<PageKey, { title: string; subtitle: string; index: string }> = {
overview: {
title: "概览",
subtitle: "当前 gateway 运行态,以及基于持久请求记录回灌的累计统计。",
index: "01",
},
requests: {
title: "请求",
subtitle: "每次请求的时间戳、状态、token usage 和命中情况。",
index: "02",
},
profiles: {
title: "Profiles",
subtitle: "文本和图片上游分别保存、探测与热切换,互不改写对方配置。",
index: "03",
},
rules: {
title: "规则",
subtitle: "热更新当前拦截规则;profile 默认规则请在 Profiles 页保存。",
index: "04",
},
logs: {
title: "日志",
subtitle: "当前进程的实时日志,不含请求正文或响应正文。",
index: "05",
},
};
const defaultProfileForm: ProfileFormState = {
name: "",
listen_host: "100.115.235.115",
listen_port: "4610",
upstream_base_url: "",
auth_mode: "passthrough",
auth_env: "CODEX_RETRY_GATEWAY_UPSTREAM_API_KEY",
auth_file: "",
manual_secret: "",
manual_secret_file: "",
manual_secret_configured: false,
auth_json_path: "",
auth_json_key: "OPENAI_API_KEY",
request_history_limit: "0",
model_remap: "",
reasoning_match_mode: "formula_518n_minus_2",
reasoning_equals: "516,1034,1552",
retryable_status_codes: "429,503",
retryable_error_messages: "Selected model is at capacity. Please try a different model.\nstream disconnected before completion: Concurrency limit exceeded for account, please retry later",
upstream_fetch_retry_attempts: "5",
upstream_fetch_retry_backoff_ms: "350",
endpoints: "/responses\n/chat/completions\n/v1/responses\n/v1/chat/completions",
};
const defaultImageProfileForm: ImageProfileFormState = {
name: "",
base_url: "",
auth_mode: "fixed_bearer",
auth_env: "CODEX_RETRY_GATEWAY_IMAGE_API_KEY",
auth_file: "",
manual_secret: "",
manual_secret_file: "",
manual_secret_configured: false,
auth_json_path: "",
auth_json_key: "OPENAI_API_KEY",
};
function numberFormat(value: unknown) {
return typeof value === "number" && Number.isFinite(value)
? zhNumberFormatter.format(value)
: "-";
}
function timestamp(value?: string | null) {
if (!value) {
return "-";
}
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : zhTimestampFormatter.format(date);
}
function durationSeconds(value?: number | null) {
return typeof value === "number" && Number.isFinite(value) ? `${(value / 1000).toFixed(2)} s` : "-";
}
function compactDurationSeconds(value?: number | null) {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
return "?";
}
const seconds = value / 1000;
if (seconds >= 100) {
return `${seconds.toFixed(0)}s`;
}
return `${seconds.toFixed(1).replace(/\\.0$/, "")}s`;
}
function secondsSince(startedAt: string | null | undefined, updatedAt: string | null | undefined) {
if (!startedAt || !updatedAt) {
return "-";
}
const startedMs = new Date(startedAt).getTime();
const updatedMs = new Date(updatedAt).getTime();
if (Number.isNaN(startedMs) || Number.isNaN(updatedMs)) {
return "-";
}
return `${Math.max(0, (updatedMs - startedMs) / 1000).toFixed(1)} s`;
}
function bytesFormat(value?: number | null) {
if (typeof value !== "number" || !Number.isFinite(value)) {
return "-";
}
if (value < 1024) {
return `${value} B`;
}
if (value < 1024 * 1024) {
return `${(value / 1024).toFixed(1)} KB`;
}
return `${(value / 1024 / 1024).toFixed(2)} MB`;
}
function lifecycleTone(value?: string | null) {
if (value === "finish") {
return "";
}
if (value === "receive_first") {
return "warn";
}
return "pending";
}
function percent(value?: number) {
return typeof value === "number" && Number.isFinite(value) ? `${(value * 100).toFixed(2)}%` : "0.00%";
}
function effectiveInputTokens(inputTokens?: number | null, cachedTokens?: number | null) {
if (typeof inputTokens !== "number" || !Number.isFinite(inputTokens)) {
return null;
}
const cached = typeof cachedTokens === "number" && Number.isFinite(cachedTokens) ? cachedTokens : 0;
return Math.max(0, inputTokens - cached);
}
function cachedRatio(inputTokens?: number | null, cachedTokens?: number | null) {
if (typeof inputTokens !== "number" || !Number.isFinite(inputTokens) || inputTokens <= 0) {
return null;
}
const cached = typeof cachedTokens === "number" && Number.isFinite(cachedTokens) ? cachedTokens : 0;
return Math.max(0, Math.min(1, cached / inputTokens));
}
function requestPrimaryId(entry: RequestEntry) {
return entry.response_id || entry.request_id || "-";
}
function hasReasoningRetryInfo(entry: RequestEntry) {
return Boolean(
entry.reasoning_retry_enabled ||
entry.reasoning_retry_thread_mode ||
entry.reasoning_retry_query_count ||
entry.reasoning_retry_round_count ||
entry.reasoning_retry_stop_reason,
);
}
function retryRoundWidthText(entry: RequestEntry) {
const round = entry.reasoning_retry_current_round || entry.reasoning_retry_round_count || 0;
const width = entry.reasoning_retry_current_width || 0;
if (!round) {
return "-";
}
return width ? `${numberFormat(round)}(${numberFormat(width)})` : numberFormat(round);
}
function retryFirstLabel(first: NonNullable<RequestEntry["reasoning_retry_current_firsts"]>[number], fallbackRound?: number | null) {
const round = first.round ?? fallbackRound ?? null;
const slot = first.slot ?? null;
if (typeof round === "number" && Number.isInteger(round)) {
if (typeof slot === "number" && Number.isInteger(slot) && slot > 1) {
return `${numberFormat(round)}-${numberFormat(slot)}`;
}
return numberFormat(round);
}
if (typeof slot === "number" && Number.isInteger(slot)) {
return `#${numberFormat(slot)}`;
}
return "?";
}
function retryFirstText(
first: NonNullable<RequestEntry["reasoning_retry_current_firsts"]>[number],
fallbackRound?: number | null,
) {
const delay = durationSeconds(first.first_response_delay_ms);
const outcome = first.outcome || "pending";
const reasoning = typeof first.reasoning_tokens === "number" ? ` r${numberFormat(first.reasoning_tokens)}` : "";
const status = typeof first.status_code === "number" ? ` ${first.status_code}` : "";
return `${retryFirstLabel(first, fallbackRound)} ${delay} ${outcome}${status}${reasoning}`;
}
function retryFirstCompactText(
first: NonNullable<RequestEntry["reasoning_retry_current_firsts"]>[number],
) {
return compactDurationSeconds(first.first_response_delay_ms);
}
function formatRetryStopReason(value?: string | null) {
const reason = `${value || ""}`.trim();
if (!reason) {
return "-";
}
const labels: Record<string, string> = {
success: "成功",
missing_thread_id: "缺少 thread_id",
thread_guard_disabled: "thread 已关闭拦截",
completed_without_retry: "未触发调度",
reasoning_guard: "reasoning 命中",
retryable_upstream_error: "上游可重试错误",
fatal: "致命错误",
exhausted_without_winner: "无赢家",
};
return labels[reason] || reason;
}
function formatRetryThreadMode(value?: string | null) {
const mode = `${value || ""}`.trim();
if (!mode || mode === "disabled") {
return "未启用";
}
if (mode === "thread_guard_disabled") {
return "thread 已关闭拦截";
}
if (mode === "thread_id") {
return "按 thread_id";
}
if (mode === "missing_thread_id") {
return "缺少 thread_id";
}
return mode;
}
function sortedReasoningCounts(value?: Record<string, number> | null) {
return Object.entries(value || {}).sort((left, right) => {
const countDelta = Number(right[1] || 0) - Number(left[1] || 0);
if (countDelta !== 0) {
return countDelta;
}
return Number(left[0] || 0) - Number(right[0] || 0);
});
}
function formatThreadGuardOverride(value?: string | null) {
const override = `${value || ""}`.trim();
if (override === "disabled") {
return "已关闭";
}
if (override === "enabled") {
return "强制开启";
}
return "默认开启";
}
function threadGuardBadgeTone(value?: string | null) {
const override = `${value || ""}`.trim();
if (override === "disabled") {
return "warn";
}
if (override === "enabled") {
return "success";
}
return "";
}
function splitList(value: string) {
return value
.split(/[\s,]+/)
.map((item) => item.trim())
.filter(Boolean);
}
function splitLines(value: string) {
return value
.split(/\r?\n/)
.map((item) => item.trim())
.filter(Boolean);
}
function normalizeReasoningMode(value: unknown): ReasoningMatchMode {
return value === "manual" ? "manual" : "formula_518n_minus_2";
}
function formatReasoningMode(mode: ReasoningMatchMode) {
return mode === "manual" ? "manual" : "518n-2";
}
function formatReasoningRule(mode: ReasoningMatchMode, reasoningEquals?: string) {
if (mode === "manual") {
return reasoningEquals || "-";
}
return "516, 1034, 1552, ...";
}
async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
const headers = new Headers(options?.headers || {});
const accessKey = window.localStorage.getItem(ACCESS_KEY_STORAGE_KEY)?.trim();
if (accessKey && !headers.has("x-codex-retry-gateway-key")) {
headers.set("x-codex-retry-gateway-key", accessKey);
}
const response = await fetch(url, { cache: "no-store", ...options, headers });
const payload = await response.json();
if (!response.ok) {
throw new Error(payload?.error?.message || "请求失败");
}
return payload as T;
}
function profileFormFromStatus(status: StatusPayload | null): ProfileFormState {
const config = status?.config || {};
return {
...defaultProfileForm,
listen_host: config.listen_host || defaultProfileForm.listen_host,
listen_port: String(config.listen_port || defaultProfileForm.listen_port),
upstream_base_url: config.upstream_base_url || "",
auth_mode: (config.upstream_auth_mode as ProfileFormState["auth_mode"]) || "passthrough",
auth_env: config.upstream_auth_env || defaultProfileForm.auth_env,
manual_secret: "",
manual_secret_file: "",
manual_secret_configured: false,
auth_json_key: config.upstream_auth_json_key || defaultProfileForm.auth_json_key,
request_history_limit: String(config.request_history_limit ?? defaultProfileForm.request_history_limit),
model_remap: config.model_remap || "",
reasoning_match_mode: normalizeReasoningMode(config.reasoning_match_mode),
reasoning_equals: Array.isArray(config.reasoning_equals)
? config.reasoning_equals.join(",")
: defaultProfileForm.reasoning_equals,
retryable_status_codes: Array.isArray(config.retryable_status_codes)
? config.retryable_status_codes.join(",")
: defaultProfileForm.retryable_status_codes,
retryable_error_messages: Array.isArray(config.retryable_error_messages)
? config.retryable_error_messages.join("\n")
: defaultProfileForm.retryable_error_messages,
upstream_fetch_retry_attempts: String(
config.upstream_fetch_retry_attempts ?? defaultProfileForm.upstream_fetch_retry_attempts,
),
upstream_fetch_retry_backoff_ms: String(
config.upstream_fetch_retry_backoff_ms ?? defaultProfileForm.upstream_fetch_retry_backoff_ms,
),
endpoints: Array.isArray(config.endpoints) ? config.endpoints.join("\n") : defaultProfileForm.endpoints,
};
}
function profileFormFromProfile(profile: Profile): ProfileFormState {
const form = profile.form || {};
return {
...defaultProfileForm,
name: profile.name,
listen_host: form.listen_host || "",
listen_port: form.listen_port || "",
upstream_base_url: form.upstream_base_url || "",
auth_mode: (form.auth_mode as ProfileFormState["auth_mode"]) || "passthrough",
auth_env: form.auth_env || "",
auth_file: form.auth_file || "",
manual_secret: "",
manual_secret_file: form.manual_secret_file || "",
manual_secret_configured: Boolean(form.manual_secret_configured),
auth_json_path: form.auth_json_path || "",
auth_json_key: form.auth_json_key || "OPENAI_API_KEY",
request_history_limit: form.request_history_limit || defaultProfileForm.request_history_limit,
model_remap: form.model_remap || "",
reasoning_match_mode: normalizeReasoningMode(form.reasoning_match_mode),
reasoning_equals: form.reasoning_equals || defaultProfileForm.reasoning_equals,
retryable_status_codes: form.retryable_status_codes || defaultProfileForm.retryable_status_codes,
retryable_error_messages: Array.isArray(form.retryable_error_messages)
? form.retryable_error_messages.join("\n")
: defaultProfileForm.retryable_error_messages,
upstream_fetch_retry_attempts:
form.upstream_fetch_retry_attempts || defaultProfileForm.upstream_fetch_retry_attempts,
upstream_fetch_retry_backoff_ms:
form.upstream_fetch_retry_backoff_ms || defaultProfileForm.upstream_fetch_retry_backoff_ms,
endpoints: Array.isArray(form.endpoints) ? form.endpoints.join("\n") : "",
};
}
function imageProfileFormFromStatus(status: StatusPayload | null): ImageProfileFormState {
const config = status?.config || {};
return {
...defaultImageProfileForm,
name: config.image_profile_name || "",
base_url: config.image_base_url || "",
auth_mode: (config.image_auth_mode as AuthMode) || defaultImageProfileForm.auth_mode,
auth_env: config.image_auth_env || defaultImageProfileForm.auth_env,
auth_json_key: config.image_auth_json_key || defaultImageProfileForm.auth_json_key,
};
}
function imageProfileFormFromProfile(profile: ImageProfile): ImageProfileFormState {
const form = profile.form || {};
return {
...defaultImageProfileForm,
name: profile.name,
base_url: form.base_url || "",
auth_mode: (form.auth_mode as AuthMode) || defaultImageProfileForm.auth_mode,
auth_env: form.auth_env || defaultImageProfileForm.auth_env,
auth_file: form.auth_file || "",
manual_secret: "",
manual_secret_file: form.manual_secret_file || "",
manual_secret_configured: Boolean(form.manual_secret_configured),
auth_json_path: form.auth_json_path || "",
auth_json_key: form.auth_json_key || defaultImageProfileForm.auth_json_key,
};
}
function ruleFormFromStatus(status: StatusPayload | null): RuleFormState {
const config = status?.config || {};
return {
reasoning_match_mode: normalizeReasoningMode(config.reasoning_match_mode),
reasoning_equals: Array.isArray(config.reasoning_equals) ? config.reasoning_equals.join(", ") : "",
retryable_status_codes: Array.isArray(config.retryable_status_codes)
? config.retryable_status_codes.join(", ")
: defaultProfileForm.retryable_status_codes,
retryable_error_messages: Array.isArray(config.retryable_error_messages)
? config.retryable_error_messages.join("\n")
: defaultProfileForm.retryable_error_messages,
upstream_fetch_retry_attempts: String(
config.upstream_fetch_retry_attempts ?? defaultProfileForm.upstream_fetch_retry_attempts,
),
upstream_fetch_retry_backoff_ms: String(
config.upstream_fetch_retry_backoff_ms ?? defaultProfileForm.upstream_fetch_retry_backoff_ms,
),
endpoints: Array.isArray(config.endpoints) ? config.endpoints.join("\n") : "",
non_stream_status_code: String(config.non_stream_status_code || 502),
log_match: Boolean(config.log_match),
};
}
export default function App() {
useEffect(() => {
const url = new URL(window.location.href);
const accessKey = url.searchParams.get("key")?.trim();
if (!accessKey) {
return;
}
window.localStorage.setItem(ACCESS_KEY_STORAGE_KEY, accessKey);
url.searchParams.delete("key");
window.history.replaceState({}, "", url.toString());
}, []);
const [page, setPage] = useState<PageKey>(() => {
const hash = window.location.hash.replace("#", "") as PageKey;
return pageCopy[hash] ? hash : "overview";
});
const [status, setStatus] = useState<StatusPayload | null>(null);
const [requests, setRequests] = useState<RequestEntry[]>([]);
const [requestsTotal, setRequestsTotal] = useState(0);
const [requestsMeta, setRequestsMeta] = useState("正在读取请求记录...");
const [threadRules, setThreadRules] = useState<ThreadRuleEntry[]>([]);
const [threadRulesMeta, setThreadRulesMeta] = useState("正在读取 thread 规则...");
const [profiles, setProfiles] = useState<Profile[]>([]);
const [profilesMeta, setProfilesMeta] = useState("正在读取 profiles...");
const [imageProfiles, setImageProfiles] = useState<ImageProfile[]>([]);
const [imageProfilesMeta, setImageProfilesMeta] = useState("正在读取图片 profiles...");
const [logs, setLogs] = useState<string>("正在读取日志...");
const [logsMeta, setLogsMeta] = useState("正在读取日志...");
const [latestLogSeq, setLatestLogSeq] = useState(0);
const [requestQuery, setRequestQuery] = useState("");
const [requestFilter, setRequestFilter] = useState("all");
const [requestLimit, setRequestLimit] = useState(REQUEST_PAGE_SIZE);
const [ruleForm, setRuleForm] = useState<RuleFormState>(ruleFormFromStatus(null));
const [profileForm, setProfileForm] = useState<ProfileFormState>(defaultProfileForm);
const [imageProfileForm, setImageProfileForm] = useState<ImageProfileFormState>(defaultImageProfileForm);
const [ruleMessage, setRuleMessage] = useState<MessageState>({ text: "", tone: "" });
const [threadRuleMessage, setThreadRuleMessage] = useState<MessageState>({ text: "", tone: "" });
const [profileMessage, setProfileMessage] = useState<MessageState>({ text: "", tone: "" });
const [profileProbeResult, setProfileProbeResult] = useState<ProfileProbePayload | null>(null);
const [imageProfileMessage, setImageProfileMessage] = useState<MessageState>({ text: "", tone: "" });
const [imageProfileProbeResult, setImageProfileProbeResult] = useState<ProfileProbePayload | null>(null);
const [probingProfile, setProbingProfile] = useState("");
const [deletingProfile, setDeletingProfile] = useState("");
const [switchingTo, setSwitchingTo] = useState("");
const [probingImageProfile, setProbingImageProfile] = useState("");
const [deletingImageProfile, setDeletingImageProfile] = useState("");
const [switchingImageTo, setSwitchingImageTo] = useState("");
const [restoreRequested, setRestoreRequested] = useState(false);
const [shouldSyncProfileFormToActive, setShouldSyncProfileFormToActive] = useState(true);
const [shouldSyncImageProfileFormToActive, setShouldSyncImageProfileFormToActive] = useState(true);
const [threadRuleBusyThreadId, setThreadRuleBusyThreadId] = useState("");
const metrics = status?.metrics || {};
const tokens = metrics.token_totals || {};
const currentPage = pageCopy[page];
const effectiveInputTotal = effectiveInputTokens(tokens.input_tokens, tokens.cached_tokens);
const cachedTotalRatio = cachedRatio(tokens.input_tokens, tokens.cached_tokens);
const reasoningChips = Object.entries(metrics.observed_reasoning_counts || {}).sort(
(left, right) => Number(right[1]) - Number(left[1]),
);
async function loadStatus(refreshRuleForm = false) {
const payload = await fetchJson<StatusPayload>(api.status);
startTransition(() => {
setStatus(payload);
if (refreshRuleForm) {
setRuleForm(ruleFormFromStatus(payload));
setProfileForm(profileFormFromStatus(payload));
setImageProfileForm(imageProfileFormFromStatus(payload));
}
});
}
async function loadRequests(limitOverride = requestLimit) {
const url = new URL(api.requests, window.location.origin);
url.searchParams.set("limit", String(limitOverride));
if (requestQuery.trim()) {
url.searchParams.set("query", requestQuery.trim());
}
if (requestFilter !== "all") {
url.searchParams.set("filter", requestFilter);
}
const payload = await fetchJson<RequestsPayload>(url.toString());
const entries = payload.entries || [];
startTransition(() => {
setRequests(entries);
setRequestsTotal(payload.total_entries ?? entries.length);
setRequestsMeta(
`查询命中 ${payload.total_entries ?? entries.length} 条,当前展示最近 ${entries.length} 条,最新序号 ${payload.latest_seq ?? 0}。`,
);
});
}
async function loadThreadRules() {
const payload = await fetchJson<ThreadRulesPayload>(api.threadRules);
const rules = payload.rules || [];
startTransition(() => {
setThreadRules(rules);
setThreadRulesMeta(`当前覆盖 ${rules.length} 个 thread;文件:${payload.thread_rules_path || "-"}`);
});
}
async function loadProfiles() {
const [payload, imagePayload] = await Promise.all([
fetchJson<ProfilesPayload>(api.profiles),
fetchJson<ImageProfilesPayload>(api.imageProfiles),
]);
const items = payload.profiles || [];
const imageItems = imagePayload.image_profiles || [];
startTransition(() => {
setProfiles(items);
setProfilesMeta(`目录:${payload.profiles_dir || "-"};当前运行:${payload.active_profile || "-"}。`);
setImageProfiles(imageItems);
setImageProfilesMeta(`目录:${imagePayload.image_profiles_dir || "-"};当前运行:${imagePayload.active_image_profile || "未配置"}。`);
});
}
async function loadLogs(incremental = false) {
const url = new URL(api.logs, window.location.origin);
if (incremental && latestLogSeq > 0) {
url.searchParams.set("since_seq", String(latestLogSeq));
} else {
url.searchParams.set("limit", String(LOG_PAGE_SIZE));
}
const payload = await fetchJson<LogsPayload>(url.toString());
const rendered = (payload.entries || [])
.map((entry) => `${entry.at || "-"} ${entry.message || ""}`)
.join("\n");
startTransition(() => {
setLogs((current) => {
if (!incremental || latestLogSeq === 0) {
return rendered || "当前还没有日志。";
}
return rendered ? `${current.trim()}\n${rendered}` : current;
});
setLogsMeta(`已载入 ${payload.total_entries ?? payload.entries?.length ?? 0} 条日志,最新序号 ${payload.latest_seq ?? latestLogSeq}。`);
if (typeof payload.latest_seq === "number") {
setLatestLogSeq(payload.latest_seq);
}
});
}
async function loadPageData(targetPage: PageKey, { incrementalLogs = true } = {}) {
if (targetPage === "requests") {
await loadRequests();
return;
}
if (targetPage === "rules") {
await loadThreadRules();
return;
}
if (targetPage === "profiles") {
await loadProfiles();
return;
}
if (targetPage === "logs") {
await loadLogs(incrementalLogs && latestLogSeq > 0);
}
}
async function refreshLiveData(targetPage: PageKey = page) {
if (restoreRequested || switchingTo || switchingImageTo) {
return;
}
const tasks: Array<Promise<unknown>> = [loadStatus(false)];
if (targetPage !== "overview") {
tasks.push(loadPageData(targetPage, { incrementalLogs: true }));
}
await Promise.all(tasks);
}
useEffect(() => {
window.location.hash = page;
}, [page]);
useEffect(() => {
if (page === "profiles") {
setShouldSyncProfileFormToActive(true);
setShouldSyncImageProfileFormToActive(true);
}
}, [page]);
useEffect(() => {
const onHashChange = () => {
const next = window.location.hash.replace("#", "") as PageKey;
if (pageCopy[next]) {
setPage(next);
}
};
window.addEventListener("hashchange", onHashChange);
return () => window.removeEventListener("hashchange", onHashChange);
}, []);
useEffect(() => {
loadStatus(true).catch((error) => {
setRuleMessage({ text: error?.message || String(error), tone: "error" });
});
}, []);
useEffect(() => {
const timer = window.setInterval(() => {
loadStatus(false).catch((error) => {
setRuleMessage({ text: error?.message || String(error), tone: "error" });
});
}, 10000);
return () => window.clearInterval(timer);
}, [restoreRequested, switchingTo, switchingImageTo]);
useEffect(() => {
if (page !== "requests" && page !== "logs" && page !== "rules") {
return;
}
const timer = window.setInterval(() => {
loadPageData(page, { incrementalLogs: true }).catch((error) => {
if (page === "rules") {
setThreadRuleMessage({ text: error?.message || String(error), tone: "error" });
return;
}
setRuleMessage({ text: error?.message || String(error), tone: "error" });
});
}, 2500);
return () => window.clearInterval(timer);
}, [page, requestQuery, requestFilter, latestLogSeq, requestLimit]);
useEffect(() => {
if (page === "overview" || page === "requests") {
return;
}
loadPageData(page, { incrementalLogs: false }).catch((error) => {
if (page === "rules") {
setThreadRuleMessage({ text: error?.message || String(error), tone: "error" });
return;
}
setRuleMessage({ text: error?.message || String(error), tone: "error" });
});
}, [page]);
useEffect(() => {
if (page !== "profiles" || !shouldSyncProfileFormToActive) {
return;
}
const activeProfile = profiles.find((profile) => profile.active);
if (!activeProfile) {
return;
}
setProfileForm(profileFormFromProfile(activeProfile));
setProfileMessage({ text: "", tone: "" });
setShouldSyncProfileFormToActive(false);
}, [page, profiles, shouldSyncProfileFormToActive]);
useEffect(() => {
if (page !== "profiles" || !shouldSyncImageProfileFormToActive) {
return;
}
const activeProfile = imageProfiles.find((profile) => profile.active);
if (!activeProfile) {
return;
}
setImageProfileForm(imageProfileFormFromProfile(activeProfile));
setImageProfileMessage({ text: "", tone: "" });
setShouldSyncImageProfileFormToActive(false);
}, [page, imageProfiles, shouldSyncImageProfileFormToActive]);
async function saveRules(event: FormEvent) {
event.preventDefault();
setRuleMessage({ text: "正在保存配置...", tone: "" });
try {
const payload = await fetchJson<StatusPayload>(api.config, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
reasoning_match_mode: ruleForm.reasoning_match_mode,
reasoning_equals: splitList(ruleForm.reasoning_equals)
.map((value) => Number.parseInt(value, 10))
.filter((value) => Number.isInteger(value)),
retryable_status_codes: splitList(ruleForm.retryable_status_codes)
.map((value) => Number.parseInt(value, 10))
.filter((value) => Number.isInteger(value)),
retryable_error_messages: splitLines(ruleForm.retryable_error_messages),
upstream_fetch_retry_attempts: Number.parseInt(ruleForm.upstream_fetch_retry_attempts, 10),
upstream_fetch_retry_backoff_ms: Number.parseInt(ruleForm.upstream_fetch_retry_backoff_ms, 10),
endpoints: splitLines(ruleForm.endpoints),
non_stream_status_code: Number.parseInt(ruleForm.non_stream_status_code, 10),
log_match: ruleForm.log_match,
}),
});
setStatus(payload);
setRuleForm(ruleFormFromStatus(payload));
setRuleMessage({ text: "配置已保存,并已对当前 gateway 立即生效。", tone: "success" });
} catch (error) {
setRuleMessage({ text: error instanceof Error ? error.message : String(error), tone: "error" });
}
}
async function updateThreadRule(threadId: string, reasoningInterceptEnabled: boolean) {
if (!threadId.trim()) {
return;
}
setThreadRuleBusyThreadId(threadId);
setThreadRuleMessage({
text: reasoningInterceptEnabled ? `正在为 ${threadId} 开启拦截...` : `正在为 ${threadId} 关闭拦截...`,
tone: "",
});
try {
const payload = await fetchJson<ThreadRulesPayload>(api.threadRules, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
thread_id: threadId,
reasoning_intercept_enabled: reasoningInterceptEnabled,
}),
});
setThreadRules(payload.rules || []);
setThreadRulesMeta(`当前覆盖 ${(payload.rules || []).length} 个 thread;文件:${payload.thread_rules_path || "-"}`);
setThreadRuleMessage({
text: payload.message || (reasoningInterceptEnabled ? "thread 已开启拦截" : "thread 已关闭拦截"),
tone: "success",
});
await Promise.all([loadStatus(false), loadRequests(requestLimit), loadThreadRules()]);
} catch (error) {
setThreadRuleMessage({ text: error instanceof Error ? error.message : String(error), tone: "error" });
} finally {
setThreadRuleBusyThreadId("");
}
}
async function restoreThreadRule(threadId: string) {
if (!threadId.trim()) {
return;
}
setThreadRuleBusyThreadId(threadId);
setThreadRuleMessage({ text: `正在恢复 ${threadId} 的默认策略...`, tone: "" });
try {
const payload = await fetchJson<ThreadRulesPayload>(`${api.threadRules}/${encodeURIComponent(threadId)}`, {
method: "DELETE",
});
setThreadRules(payload.rules || []);
setThreadRulesMeta(`当前覆盖 ${(payload.rules || []).length} 个 thread;文件:${payload.thread_rules_path || "-"}`);
setThreadRuleMessage({ text: payload.message || "thread 已恢复默认拦截策略", tone: "success" });
await Promise.all([loadStatus(false), loadRequests(requestLimit), loadThreadRules()]);
} catch (error) {
setThreadRuleMessage({ text: error instanceof Error ? error.message : String(error), tone: "error" });
} finally {
setThreadRuleBusyThreadId("");
}
}
useEffect(() => {
setRequestLimit(REQUEST_PAGE_SIZE);
}, [requestQuery, requestFilter]);
useEffect(() => {
if (page !== "requests") {
return;
}
const timer = window.setTimeout(() => {
loadRequests(requestLimit).catch((error) => {
setRuleMessage({ text: error?.message || String(error), tone: "error" });
});
}, 180);
return () => window.clearTimeout(timer);
}, [page, requestQuery, requestFilter, requestLimit]);
async function saveProfile(event: FormEvent) {
event.preventDefault();
setProfileMessage({ text: "正在保存 profile...", tone: "" });
try {
const payload = await fetchJson<ProfilesPayload>(api.profiles, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
name: profileForm.name,
listen_host: profileForm.listen_host,
listen_port: Number.parseInt(profileForm.listen_port, 10),
upstream_base_url: profileForm.upstream_base_url,
auth_mode: profileForm.auth_mode,
auth_env: profileForm.auth_env,
auth_file: profileForm.auth_file,
manual_secret: profileForm.manual_secret,
manual_secret_file: profileForm.manual_secret_file,
manual_secret_configured: profileForm.manual_secret_configured,
auth_json_path: profileForm.auth_json_path,
auth_json_key: profileForm.auth_json_key,
request_history_limit: Number.parseInt(profileForm.request_history_limit, 10),
model_remap: profileForm.model_remap,
reasoning_match_mode: profileForm.reasoning_match_mode,
reasoning_equals: splitList(profileForm.reasoning_equals),
retryable_status_codes: splitList(profileForm.retryable_status_codes),
retryable_error_messages: splitLines(profileForm.retryable_error_messages),
upstream_fetch_retry_attempts: Number.parseInt(profileForm.upstream_fetch_retry_attempts, 10),
upstream_fetch_retry_backoff_ms: Number.parseInt(profileForm.upstream_fetch_retry_backoff_ms, 10),
endpoints: splitLines(profileForm.endpoints),
}),
});
setProfiles(payload.profiles || []);
setProfilesMeta(`目录:${payload.profiles_dir || "-"};当前运行:${payload.active_profile || "-"}。`);
setProfileForm((current) => ({
...current,
manual_secret: "",
manual_secret_configured: current.auth_mode === "manual_bearer",
}));
setProfileMessage({
text: payload.applied_profile ? "profile 已保存,并已对当前运行实例后台热应用。" : "profile 已保存。需要运行它时,点击左侧卡片里的“切换”。",
tone: "success",
});
} catch (error) {
setProfileMessage({ text: error instanceof Error ? error.message : String(error), tone: "error" });
}
}
function editProfile(profile: Profile) {
setShouldSyncProfileFormToActive(false);
setProfileForm(profileFormFromProfile(profile));
setProfileMessage({ text: "编辑后保存 profile env;如果保存的是当前运行 profile,后端会直接后台热应用。", tone: "" });
}
async function switchProfile(profile: Profile) {
if (profile.active) {
return;
}
if (!window.confirm(`切换到 profile "${profile.name}" 会尝试后台热切换,不重启当前 gateway。确定继续吗?`)) {
return;
}
setSwitchingTo(profile.name);
try {
await fetchJson(api.profileSwitch, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ profile: profile.name }),
});
waitForProfile(profile.name);
} catch (error) {
setSwitchingTo("");
setProfileMessage({ text: error instanceof Error ? error.message : String(error), tone: "error" });
}
}
async function probeProfile(profile: Profile) {
setProbingProfile(profile.name);
setProfileMessage({ text: `正在探测 profile ${profile.name}...`, tone: "" });
try {
const payload = await fetchJson<ProfileProbePayload>(api.profileProbe, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
profile: profile.name,
model: "gpt-5.5-fast",
input: "ping",
}),
});
setProfileProbeResult(payload);
setProfileMessage({ text: `profile ${profile.name} 探针已完成,不影响当前运行实例。`, tone: "success" });
} catch (error) {
setProfileMessage({ text: error instanceof Error ? error.message : String(error), tone: "error" });
} finally {
setProbingProfile("");
}
}
async function removeProfile(profile: Profile) {
if (profile.active) {
setProfileMessage({ text: "当前运行的 profile 不能直接删除;请先切换到其他 profile。", tone: "error" });
return;
}
if (!window.confirm(`删除 profile "${profile.name}" 会移除对应 env 文件。确定继续吗?`)) {
return;
}
setDeletingProfile(profile.name);
try {
const payload = await fetchJson<ProfilesPayload>(`${api.profiles}/${encodeURIComponent(profile.name)}`, {
method: "DELETE",
});
setProfiles(payload.profiles || []);
setProfilesMeta(`目录:${payload.profiles_dir || "-"};当前运行:${payload.active_profile || "-"}。`);
if (profileForm.name === profile.name) {
setProfileForm(defaultProfileForm);
}
if (profileProbeResult?.profile === profile.name) {
setProfileProbeResult(null);
}
setProfileMessage({ text: `profile ${profile.name} 已删除。`, tone: "success" });
} catch (error) {
setProfileMessage({ text: error instanceof Error ? error.message : String(error), tone: "error" });
} finally {
setDeletingProfile("");
}
}
async function saveImageProfile(event: FormEvent) {
event.preventDefault();
setImageProfileMessage({ text: "正在保存图片 profile...", tone: "" });
try {
const payload = await fetchJson<ImageProfilesPayload>(api.imageProfiles, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
name: imageProfileForm.name,
base_url: imageProfileForm.base_url,
auth_mode: imageProfileForm.auth_mode,
auth_env: imageProfileForm.auth_env,
auth_file: imageProfileForm.auth_file,
manual_secret: imageProfileForm.manual_secret,
manual_secret_file: imageProfileForm.manual_secret_file,
manual_secret_configured: imageProfileForm.manual_secret_configured,
auth_json_path: imageProfileForm.auth_json_path,
auth_json_key: imageProfileForm.auth_json_key,
}),
});
setImageProfiles(payload.image_profiles || []);
setImageProfilesMeta(`目录:${payload.image_profiles_dir || "-"};当前运行:${payload.active_image_profile || "未配置"}。`);
setImageProfileForm((current) => ({
...current,
manual_secret: "",
manual_secret_configured: current.auth_mode === "manual_bearer",
}));
setImageProfileMessage({
text: payload.applied_image_profile
? "图片 profile 已保存,并已对当前运行实例后台热应用。"
: "图片 profile 已保存。需要运行它时,点击左侧卡片里的“切换”。",
tone: "success",
});
} catch (error) {
setImageProfileMessage({ text: error instanceof Error ? error.message : String(error), tone: "error" });
}
}
function editImageProfile(profile: ImageProfile) {
setShouldSyncImageProfileFormToActive(false);
setImageProfileForm(imageProfileFormFromProfile(profile));
setImageProfileMessage({ text: "编辑后保存图片 profile env;如果保存的是当前运行 profile,后端会直接后台热应用。", tone: "" });
}
async function switchImageProfile(profile: ImageProfile) {
if (profile.active) {
return;
}
if (!window.confirm(`切换到图片 profile "${profile.name}" 会立即改写 /images/* 的上游,不重启 gateway。确定继续吗?`)) {
return;
}
setSwitchingImageTo(profile.name);
try {
await fetchJson(api.imageProfileSwitch, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ profile: profile.name }),
});
waitForImageProfile(profile.name);
} catch (error) {
setSwitchingImageTo("");
setImageProfileMessage({ text: error instanceof Error ? error.message : String(error), tone: "error" });
}
}
async function probeImageProfile(profile: ImageProfile) {
setProbingImageProfile(profile.name);
setImageProfileMessage({ text: `正在探测图片 profile ${profile.name}...`, tone: "" });
try {
const payload = await fetchJson<ProfileProbePayload>(api.imageProfileProbe, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ profile: profile.name }),
});
setImageProfileProbeResult(payload);
setImageProfileMessage({ text: `图片 profile ${profile.name} 探针已完成,不影响当前运行实例。`, tone: "success" });
} catch (error) {
setImageProfileMessage({ text: error instanceof Error ? error.message : String(error), tone: "error" });
} finally {
setProbingImageProfile("");
}
}
async function removeImageProfile(profile: ImageProfile) {
if (profile.active) {
setImageProfileMessage({ text: "当前运行的图片 profile 不能直接删除;请先切换到其他图片 profile。", tone: "error" });
return;
}
if (!window.confirm(`删除图片 profile "${profile.name}" 会移除对应 env 文件。确定继续吗?`)) {
return;
}
setDeletingImageProfile(profile.name);
try {
const payload = await fetchJson<ImageProfilesPayload>(`${api.imageProfiles}/${encodeURIComponent(profile.name)}`, {
method: "DELETE",
});
setImageProfiles(payload.image_profiles || []);
setImageProfilesMeta(`目录:${payload.image_profiles_dir || "-"};当前运行:${payload.active_image_profile || "未配置"}。`);
if (imageProfileForm.name === profile.name) {
setImageProfileForm(defaultImageProfileForm);
}
if (imageProfileProbeResult?.image_profile === profile.name) {
setImageProfileProbeResult(null);
}
setImageProfileMessage({ text: `图片 profile ${profile.name} 已删除。`, tone: "success" });
} catch (error) {
setImageProfileMessage({ text: error instanceof Error ? error.message : String(error), tone: "error" });
} finally {
setDeletingImageProfile("");
}
}
function waitForProfile(profileName: string) {
const deadline = Date.now() + 12000;
const tick = async () => {
if (Date.now() > deadline) {
setSwitchingTo("");
setProfileMessage({ text: "热切换已提交,但暂时没有看到当前实例切到目标 profile。", tone: "error" });
return;
}
try {
const payload = await fetchJson<StatusPayload>(api.status);
if (payload.config?.profile_name === profileName) {
setSwitchingTo("");
setShouldSyncProfileFormToActive(true);
Promise.all([loadStatus(true), loadProfiles()]).catch(() => {});
return;
}
} catch {
// keep polling
}
window.setTimeout(tick, 500);
};
window.setTimeout(tick, 300);
}
function waitForImageProfile(profileName: string) {
const deadline = Date.now() + 12000;
const tick = async () => {
if (Date.now() > deadline) {
setSwitchingImageTo("");
setImageProfileMessage({ text: "图片热切换已提交,但暂时没有看到当前实例切到目标 profile。", tone: "error" });
return;
}
try {
const payload = await fetchJson<StatusPayload>(api.status);
if (payload.config?.image_profile_name === profileName) {
setSwitchingImageTo("");
setShouldSyncImageProfileFormToActive(true);
Promise.all([loadStatus(true), loadProfiles()]).catch(() => {});
return;
}
} catch {
// keep polling
}
window.setTimeout(tick, 500);
};
window.setTimeout(tick, 300);
}
async function restoreConfig() {
if (!window.confirm("恢复后会关闭当前 gateway,并把 Codex 配置切回原上游。确定继续吗?")) {
return;
}
setRestoreRequested(true);
setRuleMessage({ text: "正在触发恢复,页面很快会失联...", tone: "" });
try {
await fetchJson(api.restore, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
});
setRuleMessage({ text: "恢复脚本已启动,等待 gateway 关闭。", tone: "success" });
} catch (error) {
setRestoreRequested(false);
setRuleMessage({ text: error instanceof Error ? error.message : String(error), tone: "error" });
}
}
return (
<>
<div className="app-shell">
<aside className="sidebar">
<div className="brand">
<span className="eyebrow">Gateway Console</span>
<h1>Codex Retry Gateway</h1>
<p>分页面管理 profile、请求 token、拦截规则和运行日志。页面只记录元数据,不保存请求正文或响应正文。</p>
</div>
<nav className="nav" aria-label="Gateway pages">
{(Object.keys(pageCopy) as PageKey[]).map((key) => (
<button
key={key}
type="button"
data-active={page === key}
onClick={() => setPage(key)}
>
{pageCopy[key].title} <span>{pageCopy[key].index}</span>
</button>
))}
</nav>
<SideCard label="当前 Profile" value={status?.config?.profile_name || "-"} strong />
<SideCard label="当前图片 Profile" value={status?.config?.image_profile_name || "未配置"} />
<SideCard label="监听地址" value={status?.listen || "-"} />
<SideCard label="Auth 模式" value={status?.config?.upstream_auth_mode || "-"} />
</aside>
<main className="content">
<header className="topbar">
<div>
<h2>{currentPage.title}</h2>
<p>{currentPage.subtitle}</p>
</div>
<div className="toolbar">
<button className="ghost" type="button" onClick={() => refreshLiveData()}>
刷新
</button>
<a className="badge" href={api.status}>
status api
</a>
</div>
</header>
{page === "overview" && (
<section className="page" data-active="true">
<div className="grid">
<Card
title="运行态"
hint="配置、provider、Codex 当前 base_url 和 profile 状态。"
badge={status?.ok ? "online" : "读取中"}
>
<div className="stat-grid">
<Stat label="Profile" value={status?.config?.profile_name || "-"} />
<Stat label="代理请求" value={numberFormat(metrics.total_proxy_request_count || 0)} />
<Stat label="检查响应" value={numberFormat(metrics.inspected_response_count || 0)} />
<Stat label="规则命中" value={numberFormat(metrics.matched_response_count || 0)} />
</div>
</Card>
<Card title="Token Totals" hint="按持久请求记录累计;重启后会从 requests history 回灌。">
<div className="token-strip">
<TokenCard label="Input" value={numberFormat(effectiveInputTotal ?? 0)} />
<TokenCard label="Output" value={numberFormat(tokens.output_tokens || 0)} />
<TokenCard label="Total" value={numberFormat(tokens.total_tokens || 0)} />
<TokenCard label="Reasoning" value={numberFormat(tokens.reasoning_tokens || 0)} />
<TokenCard
label="Cached"
value={`${numberFormat(tokens.cached_tokens || 0)}${cachedTotalRatio !== null ? ` (${percent(cachedTotalRatio)})` : ""}`}
/>
</div>
</Card>
<div className="grid two">
<Card title="连接信息">
<InfoList
rows={[
["监听地址", status?.listen || "-"],
["文本上游", status?.config?.upstream_base_url || "-"],
["文本认证", status?.config?.upstream_auth_mode || "passthrough"],
["图片 Profile", status?.config?.image_profile_name || "未配置"],
["图片上游", status?.config?.image_base_url || "未配置"],
["图片认证", status?.config?.image_base_url ? status?.config?.image_auth_mode || "fixed_bearer" : "未启用"],
["Provider", status?.state?.provider_name || "未检测到安装状态"],
["Codex Base URL", status?.state?.codex_current_base_url || "-"],
]}
/>
</Card>
<Card title="规则观察">
<InfoList
rows={[
["516 命中", numberFormat(metrics.reasoning_516_count || 0)],
["516 占比", percent(metrics.reasoning_516_ratio || 0)],
["累计起点", timestamp(metrics.persistent_since || metrics.started_at)],
["本次启动", timestamp(metrics.started_at)],
["Config", status?.paths?.config_path || "-"],
["备份", status?.state?.latest_backup_path || "-"],
]}
/>
<div className="chips">
{reasoningChips.length === 0 ? (
<span className="chip">还没有 reasoning 观测</span>
) : (
<>
{reasoningChips.map(([reasoning, count]) => (
<span className="chip" key={reasoning}>
reasoning {reasoning}: {count}
</span>
))}
{(metrics.observed_reasoning_counts_omitted || 0) > 0 ? (
<span className="chip">
其余 {metrics.observed_reasoning_counts_omitted} 项未展开
</span>
) : null}
</>
)}
</div>
</Card>
</div>
</div>
</section>
)}
{page === "requests" && (
<section className="page" data-active="true">
<Card
title="请求记录"
hint={requestsMeta}
actions={
<div className="toolbar compact-toolbar">
<input
type="search"
value={requestQuery}
placeholder="搜索模型 / 路径 / 状态"
onChange={(event) => setRequestQuery(event.target.value)}
/>
<select value={requestFilter} onChange={(event) => setRequestFilter(event.target.value)}>
<option value="all">全部请求</option>
<option value="matched">规则命中</option>
<option value="stream">流式响应</option>
<option value="error">有错误</option>
</select>
<button className="secondary" type="button" onClick={() => loadRequests()}>
刷新请求
</button>
<button
className="ghost"
type="button"
disabled={requests.length >= requestsTotal && requestsTotal > 0}
onClick={() => setRequestLimit((current) => current + REQUEST_PAGE_SIZE)}
>
更多
</button>
</div>
}
>
<div className="request-list">
{requests.length === 0 ? (
<div className="empty-state">没有匹配的请求记录。</div>
) : (
requests.map((entry) => {
const usage = entry.usage || {};
const upstream = entry.upstream || {};
const statusTone = entry.error ? "error" : entry.matched ? "warn" : "";
const effectiveInput = effectiveInputTokens(usage.input_tokens, usage.cached_tokens);
const cachedHitRatio = cachedRatio(usage.input_tokens, usage.cached_tokens);
const showReasoningRetry = hasReasoningRetryInfo(entry);
const retryReasoningCounts = sortedReasoningCounts(entry.reasoning_retry_extra_reasoning_counts);
const retryCurrentFirsts = Array.isArray(entry.reasoning_retry_current_firsts)
? entry.reasoning_retry_current_firsts
: [];
const threadRuleBusy = Boolean(entry.thread_id) && threadRuleBusyThreadId === entry.thread_id;
return (
<article className="request-card" key={entry.seq}>
<div className="request-head">
<div>
<div className="request-title">
<span className={`badge ${statusTone}`}>{entry.status_code ?? "-"}</span>
<strong>{entry.profile_name || status?.config?.profile_name || "-"}</strong>
<span className={`badge ${lifecycleTone(entry.lifecycle_state)}`}>{entry.lifecycle_state || "sent"}</span>
<span className="meta-pill meta-sent">
<span className="meta-key"></span>
{timestamp(entry.started_at)}
</span>
{retryCurrentFirsts.length > 0 ? (
<span className="meta-pill meta-first meta-first-list-pill">
<span className="meta-key"></span>
{retryCurrentFirsts.map((first, index) => (
<span className="meta-first-mini" key={`${first.round || entry.reasoning_retry_current_round || "r"}-${first.slot || index}`}>
{retryFirstCompactText(first)}
</span>
))}
</span>
) : (
<span className="meta-pill meta-first">
<span className="meta-key"></span>
{durationSeconds(entry.first_response_delay_ms)}
</span>
)}
<span className="meta-pill meta-total">
<span className="meta-key"></span>
{durationSeconds(entry.duration_ms)}
</span>
<span className="meta-pill meta-progress">
<span className="meta-key"></span>
{timestamp(entry.last_activity_at || entry.usage_last_updated_at)}
</span>
<span className="meta-pill meta-body">
<span className="meta-key"></span>
{bytesFormat(entry.request_body_bytes)}
</span>
<span className="meta-pill meta-stream">
<span className="meta-key"></span>
{entry.response_stream
? `${numberFormat(entry.stream_chunk_count)} chunk / ${bytesFormat(entry.response_bytes_received)} / ${secondsSince(entry.started_at, entry.last_activity_at || entry.usage_last_updated_at || entry.finished_at)}`
: "-"}
</span>
{showReasoningRetry ? (
<span className="meta-pill meta-retry">
<span className="meta-key">重打</span>
{`${retryRoundWidthText(entry)} / ${numberFormat(entry.reasoning_retry_query_count || 0)}q`}
</span>
) : null}
<span className="meta-pill meta-received">
<span className="meta-key"></span>
{timestamp(entry.finished_at)}
</span>
</div>
<div className="request-subtitle">
<code>{`${entry.method || "-"} ${entry.path || "-"}`}</code>
<span className="hint">{entry.response_stream ? "stream" : "non-stream"}</span>
<span className="hint">id {requestPrimaryId(entry)}</span>
<span className="hint">thread {entry.thread_id || "-"}</span>
</div>
</div>
<div className="request-badges">
{entry.matched ? <span className="badge warn">matched</span> : <span className="badge">pass</span>}
{entry.error ? <span className="badge error">error</span> : null}
<span className={`badge ${threadGuardBadgeTone(entry.reasoning_guard_thread_override)}`}>
guard {formatThreadGuardOverride(entry.reasoning_guard_thread_override)}
</span>
{showReasoningRetry ? (
<span className={`badge ${entry.reasoning_retry_stop_reason === "success" ? "success" : "warn"}`}>
retry {formatRetryStopReason(entry.reasoning_retry_stop_reason)}
</span>
) : null}
</div>
</div>
<div className="request-grid">
<div className="request-block">
<label>模型</label>
<code>{entry.requested_model || entry.model || "-"}</code>
<span className="hint">
{entry.forwarded_model && entry.forwarded_model !== entry.requested_model
? `转发为 ${entry.forwarded_model}`
: entry.forwarded_model || "-"}
</span>
<span className="hint">
{entry.reasoning_effort
? `强度 ${entry.reasoning_effort}`
: "强度 -"}
{entry.reasoning_summary ? ` / summary ${entry.reasoning_summary}` : ""}
</span>
</div>
<div className="request-block">
<label>{upstream.route === "images" ? "图片上游" : "文本上游"}</label>
<code>{`${upstream.origin || "-"}${upstream.path || ""}`}</code>
<span className="hint">
{upstream.auth_mode || "-"} / {upstream.auth_source || "-"}
{upstream.authorization_configured === false ? " / no auth" : ""}
</span>
<span className="hint">
重试 {numberFormat(entry.upstream_attempt_count)}
{entry.upstream_status_code ? ` / upstream ${entry.upstream_status_code}` : ""}
</span>
</div>
<div className="request-block">
<label>Token</label>
<div className="request-token-row">
<span className="token-pill token-in">in {numberFormat(effectiveInput)}</span>
<span className="token-pill token-out">out {numberFormat(usage.output_tokens)}</span>
<span className="token-pill token-cached">cached {numberFormat(usage.cached_tokens)}{cachedHitRatio !== null ? ` (${percent(cachedHitRatio)})` : ""}</span>
<span className="token-pill token-reasoning">reasoning {numberFormat(entry.reasoning_tokens ?? usage.reasoning_tokens)}</span>
<span className="token-pill token-total">total {numberFormat(usage.total_tokens)}</span>
</div>
</div>
<div className="request-block">
<label>IDs</label>
<code>{`response ${entry.response_id || "-"}`}</code>
<span className="hint">{`request ${entry.request_id || "-"}`}</span>
<span className="hint">{`thread ${entry.thread_id || "-"}`}</span>
{entry.thread_id ? (
<div className="thread-rule-actions">
<button
className="ghost"
type="button"
disabled={threadRuleBusy || entry.reasoning_guard_thread_override === "enabled"}
onClick={() => updateThreadRule(entry.thread_id || "", true)}
>
{threadRuleBusy && entry.reasoning_guard_thread_override !== "enabled" ? "处理中" : "开启拦截"}
</button>
<button
className="ghost"
type="button"
disabled={threadRuleBusy || entry.reasoning_guard_thread_override === "disabled"}
onClick={() => updateThreadRule(entry.thread_id || "", false)}
>
{threadRuleBusy && entry.reasoning_guard_thread_override !== "disabled" ? "处理中" : "关闭拦截"}
</button>
{entry.reasoning_guard_thread_override && entry.reasoning_guard_thread_override !== "default" ? (
<button
className="secondary"
type="button"
disabled={threadRuleBusy}
onClick={() => restoreThreadRule(entry.thread_id || "")}
>
{threadRuleBusy ? "处理中" : "恢复默认"}
</button>
) : null}
</div>
) : null}
</div>
{showReasoningRetry ? (
<div className="request-block reasoning-retry-block">
<label>Reasoning Retry</label>
<code>{formatRetryThreadMode(entry.reasoning_retry_thread_mode)}</code>
<span className="hint">
guard {formatThreadGuardOverride(entry.reasoning_guard_thread_override)}
</span>
<span className="hint">
schedule 1,1,2,2,4,4... / query {numberFormat(entry.reasoning_retry_query_count || 0)}
{" / "}
round {retryRoundWidthText(entry)}
</span>
<span className="hint">
winner {entry.reasoning_retry_winner_round && entry.reasoning_retry_winner_slot
? `round ${entry.reasoning_retry_winner_round} slot ${entry.reasoning_retry_winner_slot}`
: "-"}
{" / "}
stop {formatRetryStopReason(entry.reasoning_retry_stop_reason)}
</span>
<span className="hint">
extra matched {numberFormat(entry.reasoning_retry_extra_matched_count || 0)}
{" / "}
inspected {numberFormat(entry.reasoning_retry_extra_inspected_count || 0)}
{" / "}
extra reasoning {numberFormat(entry.reasoning_retry_extra_usage?.reasoning_tokens)}
</span>
{retryReasoningCounts.length > 0 ? (
<div className="retry-chip-row">
{retryReasoningCounts.slice(0, 4).map(([reasoning, count]) => (
<span className="chip compact-chip" key={reasoning}>
{reasoning}: {numberFormat(count)}
</span>
))}
</div>
) : null}
{retryCurrentFirsts.length > 0 ? (
<div className="retry-first-list" aria-label="current retry wave first responses">
{retryCurrentFirsts.map((first, index) => (
<span
className={`retry-first-chip ${first.first_response_delay_ms == null ? "pending" : ""}`}
key={`${first.round || entry.reasoning_retry_current_round || "r"}-${first.slot || index}`}
title={`first ${timestamp(first.first_response_at)} / upstream ${first.upstream_status_code ?? "-"}`}
>
{retryFirstText(first, entry.reasoning_retry_current_round)}
</span>
))}
</div>
) : null}
</div>
) : null}
</div>
</article>
);
})
)}
</div>
</Card>
</section>
)}
{page === "profiles" && (
<section className="page" data-active="true">
<div className="grid profile-layout">
<Card
title="文本 Profile 列表"
hint={profilesMeta}
actions={
<button
className="secondary"
type="button"
onClick={() => {
setShouldSyncProfileFormToActive(false);
setProfileForm(profileFormFromStatus(status));
setProfileMessage({ text: "", tone: "" });
}}
>
新建文本 Profile
</button>
}
>
<div className="grid profile-list-grid">
{profiles.length === 0 ? (
<div className="profile-card">当前还没有 profile env 文件。</div>
) : (
profiles.map((profile) => (
<article className="profile-card compact-profile-card" data-active-profile={profile.active} key={profile.name}>
<div className="profile-head">
<div className="profile-meta">
<h3>{profile.name}</h3>
<div className="profile-path hint">{profile.file_path || ""}</div>
</div>
<div className="profile-actions">
{profile.active ? <span className="badge">当前运行</span> : <span className="badge warn">可切换</span>}
<button className="ghost" type="button" onClick={() => editProfile(profile)}>
编辑
</button>
<button className="ghost" type="button" disabled={probingProfile === profile.name} onClick={() => probeProfile(profile)}>
{probingProfile === profile.name ? "探测中" : "探针"}
</button>
<button className="danger" type="button" disabled={deletingProfile === profile.name} onClick={() => removeProfile(profile)}>
{deletingProfile === profile.name ? "删除中" : "删除"}
</button>
<button className="primary" type="button" disabled={profile.active} onClick={() => switchProfile(profile)}>
切换
</button>
</div>
</div>
<div className="mini-stats">
<MiniStat label="监听" value={`${profile.summary?.listen_host || "-"}:${profile.summary?.listen_port || "-"}`} />
<MiniStat label="文本上游" value={profile.summary?.upstream_base_url || "-"} />
<MiniStat label="文本认证" value={`${profile.summary?.auth_mode || "passthrough"} / ${profile.summary?.auth_source || "-"}`} />
<MiniStat label="History" value={profile.summary?.request_history_limit || "0"} />
<MiniStat label="Model Remap" value={profile.summary?.model_remap || "-"} />
<MiniStat
label="Reasoning"
value={formatReasoningRule(
normalizeReasoningMode(profile.summary?.reasoning_match_mode),
profile.summary?.reasoning_equals,
)}
/>
<MiniStat
label="Rule Mode"
value={formatReasoningMode(normalizeReasoningMode(profile.summary?.reasoning_match_mode))}
/>
</div>
</article>
))
)}
</div>
</Card>
<Card title={profileForm.name ? `编辑文本 ${profileForm.name}` : "编辑文本 Profile"} hint="手动 token/password 只能写入系统 secret 文件,保存后不会从后端读回或展示。">
<form onSubmit={saveProfile}>
<Field label="Profile 名称">
<input value={profileForm.name} placeholder="default 或 sub2api-auth-json" onChange={(event) => setProfileForm({ ...profileForm, name: event.target.value })} />
</Field>
<div className="field-row">
<Field label="监听 Host">
<input value={profileForm.listen_host} placeholder="100.115.235.115" onChange={(event) => setProfileForm({ ...profileForm, listen_host: event.target.value })} />
</Field>
<Field label="监听 Port">
<input type="number" min={1} max={65535} value={profileForm.listen_port} onChange={(event) => setProfileForm({ ...profileForm, listen_port: event.target.value })} />
</Field>
</div>
<Field label="文本上游 Base URL">
<input value={profileForm.upstream_base_url} placeholder="https://example.com/v1" onChange={(event) => setProfileForm({ ...profileForm, upstream_base_url: event.target.value })} />
</Field>
<Field label="Model Remap" hint="每行一个 from=to,也支持用分号分隔;例如 gpt-5.5-fast=gpt-5.5;gpt-5.4-fast=gpt-5.4。切换 profile 后会自动把请求模型改写为目标 provider 可用模型。">
<textarea value={profileForm.model_remap} onChange={(event) => setProfileForm({ ...profileForm, model_remap: event.target.value })} />
</Field>
<Field label="文本 Auth 模式">
<select value={profileForm.auth_mode} onChange={(event) => setProfileForm({ ...profileForm, auth_mode: event.target.value as ProfileFormState["auth_mode"] })}>
<option value="passthrough">passthrough:透传 Codex Authorization</option>
<option value="manual_bearer">manual_bearer:手动写入系统 secret 文件</option>
<option value="fixed_bearer">fixed_bearer:从 env/file 读取 Bearer</option>
<option value="auth_json">auth_json:从 ~/.codex/auth.json 读取字段</option>
</select>
</Field>
{profileForm.auth_mode === "manual_bearer" && (
<Field
label="文本 Token / Password"
hint={profileForm.manual_secret_configured ? "系统中已有 secret;留空表示保留现有值,填入新值会覆盖写入。" : "只写入系统 secret 文件,保存后不会显示或返回。"}
>
<input
type="password"
value={profileForm.manual_secret}
autoComplete="new-password"
placeholder={profileForm.manual_secret_configured ? "已配置;留空保留" : "输入一次 token/password"}
onChange={(event) => setProfileForm({ ...profileForm, manual_secret: event.target.value })}
/>
</Field>
)}
{profileForm.auth_mode === "fixed_bearer" && (
<>
<Field label="文本 Token 环境变量名" hint="只填变量名,不填 token 值。">
<input value={profileForm.auth_env} placeholder="CODEX_RETRY_GATEWAY_UPSTREAM_API_KEY" onChange={(event) => setProfileForm({ ...profileForm, auth_env: event.target.value })} />
</Field>
<Field label="文本 Token 文件路径">
<input value={profileForm.auth_file} placeholder="/run/secrets/provider-token" onChange={(event) => setProfileForm({ ...profileForm, auth_file: event.target.value })} />
</Field>
</>
)}
{profileForm.auth_mode === "auth_json" && (
<>
<Field label="文本 auth.json 路径">
<input value={profileForm.auth_json_path} placeholder="留空表示 ~/.codex/auth.json" onChange={(event) => setProfileForm({ ...profileForm, auth_json_path: event.target.value })} />
</Field>
<Field label="文本 auth.json 字段名" hint="这里填 JSON key,例如 OPENAI_API_KEY,不粘贴 sk- 开头的密钥值。">
<input value={profileForm.auth_json_key} placeholder="OPENAI_API_KEY" onChange={(event) => setProfileForm({ ...profileForm, auth_json_key: event.target.value })} />
</Field>
</>
)}
<Field label="History Limit" hint="0 表示不裁剪;正整数表示后台持久化记录的保留上限。">
<input
type="number"
min={0}
value={profileForm.request_history_limit}
onChange={(event) => setProfileForm({ ...profileForm, request_history_limit: event.target.value })}
/>
</Field>
<Field label="reasoning_match_mode" hint="`518n-2` 会命中 516、1034、1552 等;`manual` 则只按下面的列表拦截。">
<select
value={profileForm.reasoning_match_mode}
onChange={(event) =>
setProfileForm({ ...profileForm, reasoning_match_mode: normalizeReasoningMode(event.target.value) })
}
>
<option value="formula_518n_minus_2">formula_518n_minus_2</option>
<option value="manual">manual</option>
</select>
</Field>
<Field label="reasoning_equals">
<input value={profileForm.reasoning_equals} placeholder="516,1034,1552" onChange={(event) => setProfileForm({ ...profileForm, reasoning_equals: event.target.value })} />
</Field>
<Field label="retryable_status_codes">
<input value={profileForm.retryable_status_codes} placeholder="429,503" onChange={(event) => setProfileForm({ ...profileForm, retryable_status_codes: event.target.value })} />
</Field>
<Field label="retryable_error_messages" hint="每行一条错误文案;上游 JSON 错误包含任一行时会转成本地重试状态码。">
<textarea value={profileForm.retryable_error_messages} onChange={(event) => setProfileForm({ ...profileForm, retryable_error_messages: event.target.value })} />
</Field>
<div className="field-row">
<Field label="upstream_fetch_retry_attempts">
<input type="number" min={1} value={profileForm.upstream_fetch_retry_attempts} onChange={(event) => setProfileForm({ ...profileForm, upstream_fetch_retry_attempts: event.target.value })} />
</Field>
<Field label="upstream_fetch_retry_backoff_ms">
<input type="number" min={0} step={50} value={profileForm.upstream_fetch_retry_backoff_ms} onChange={(event) => setProfileForm({ ...profileForm, upstream_fetch_retry_backoff_ms: event.target.value })} />
</Field>
</div>
<Field label="endpoints">
<textarea value={profileForm.endpoints} onChange={(event) => setProfileForm({ ...profileForm, endpoints: event.target.value })} />
</Field>
<div className="toolbar">
<button className="primary" type="submit">
保存 Profile
</button>
<button className="ghost" type="button" onClick={() => setProfileForm(profileFormFromStatus(status))}>
清空表单
</button>
</div>
<Message message={profileMessage} />
</form>
</Card>
<Card title="文本 Profile 探针" hint="不切换当前 gateway,只对选定文本 profile 做上游连通性与最小请求探测。默认测试模型是 gpt-5.5-fast,若配置了 remap,会显示实际转发模型。">
{profileProbeResult ? (
<div className="grid">
<InfoList
rows={[
["Profile", profileProbeResult.profile || "-"],
["上游", profileProbeResult.upstream_base_url || "-"],
["Auth", `${profileProbeResult.auth_mode || "-"} / ${profileProbeResult.auth_source || "-"}`],
["Model Remap", profileProbeResult.model_remap || "-"],
]}
/>
{(profileProbeResult.probes || []).map((probe) => (
<div className="profile-card" key={`${probe.kind}-${probe.target}`}>
<div className="profile-head">
<div>
<h3>{probe.kind || "probe"}</h3>
<div className="hint">{probe.target || "-"}</div>
</div>
<span className="badge">{probe.status ?? "-"}</span>
</div>
<div className="mini-stats">
<MiniStat label="请求模型" value={probe.requested_model || "-"} />
<MiniStat label="转发模型" value={probe.forwarded_model || "-"} />
<MiniStat label="Content-Type" value={probe.content_type || "-"} />
</div>
<pre className="log-output">{probe.body_preview || "无返回摘要"}</pre>
</div>
))}
</div>
) : (
<div className="hint">先在左侧 profile 卡片点击“探针”,这里会显示不切换情况下的探测结果。</div>
)}
</Card>
<Card
title="图片 Profile 列表"
hint={imageProfilesMeta}
actions={
<button
className="secondary"
type="button"
onClick={() => {
setShouldSyncImageProfileFormToActive(false);
setImageProfileForm(defaultImageProfileForm);
setImageProfileMessage({ text: "", tone: "" });
}}
>
新建图片 Profile
</button>
}
>
<div className="grid profile-list-grid">
{imageProfiles.length === 0 ? (
<div className="profile-card">当前还没有图片 profile env 文件。</div>
) : (
imageProfiles.map((profile) => (
<article className="profile-card compact-profile-card" data-active-profile={profile.active} key={profile.name}>
<div className="profile-head">
<div className="profile-meta">
<h3>{profile.name}</h3>
<div className="profile-path hint">{profile.file_path || ""}</div>
</div>
<div className="profile-actions">
{profile.active ? <span className="badge">当前运行</span> : <span className="badge warn">可切换</span>}
<button className="ghost" type="button" onClick={() => editImageProfile(profile)}>
编辑
</button>
<button className="ghost" type="button" disabled={probingImageProfile === profile.name} onClick={() => probeImageProfile(profile)}>
{probingImageProfile === profile.name ? "探测中" : "探针"}
</button>
<button className="danger" type="button" disabled={deletingImageProfile === profile.name} onClick={() => removeImageProfile(profile)}>
{deletingImageProfile === profile.name ? "删除中" : "删除"}
</button>
<button className="primary" type="button" disabled={profile.active} onClick={() => switchImageProfile(profile)}>
切换
</button>
</div>
</div>
<div className="mini-stats">
<MiniStat label="图片上游" value={profile.summary?.base_url || "回退文本上游"} />
<MiniStat label="图片认证" value={`${profile.summary?.auth_mode || "fixed_bearer"} / ${profile.summary?.auth_source || "-"}`} />
</div>
</article>
))
)}
</div>
</Card>
<Card title={imageProfileForm.name ? `编辑图片 ${imageProfileForm.name}` : "编辑图片 Profile"} hint="图片 API key 只写入系统 secret 文件,保存后不会从后端读回或展示。">
<form onSubmit={saveImageProfile}>
<Field label="图片 Profile 名称">
<input value={imageProfileForm.name} placeholder="image-primary" onChange={(event) => setImageProfileForm({ ...imageProfileForm, name: event.target.value })} />
</Field>
<Field label="图片上游 Base URL" hint="留空时 /images/* 和 /v1/images/* 回退到当前文本上游。">
<input value={imageProfileForm.base_url} placeholder="https://image-provider.example/v1" onChange={(event) => setImageProfileForm({ ...imageProfileForm, base_url: event.target.value })} />
</Field>
<Field label="图片 Auth 模式">
<select value={imageProfileForm.auth_mode} onChange={(event) => setImageProfileForm({ ...imageProfileForm, auth_mode: event.target.value as AuthMode })}>
<option value="passthrough">passthrough:透传 Codex Authorization</option>
<option value="manual_bearer">manual_bearer:手动写入系统 secret 文件</option>
<option value="fixed_bearer">fixed_bearer:从 env/file 读取 Bearer</option>
<option value="auth_json">auth_json:从 ~/.codex/auth.json 读取字段</option>
</select>
</Field>
{imageProfileForm.auth_mode === "manual_bearer" && (
<Field
label="图片 API Key"
hint={imageProfileForm.manual_secret_configured ? "系统中已有图片 key;留空表示保留,填入新值会覆盖写入。" : "只写入系统 secret 文件,保存后不会显示或返回。"}
>
<input
type="password"
value={imageProfileForm.manual_secret}
autoComplete="new-password"
placeholder={imageProfileForm.manual_secret_configured ? "已配置;留空保留" : "输入一次图片 API key"}
onChange={(event) => setImageProfileForm({ ...imageProfileForm, manual_secret: event.target.value })}
/>
</Field>
)}
{imageProfileForm.auth_mode === "fixed_bearer" && (
<>
<Field label="图片 Key 环境变量名" hint="只填变量名,不填 key 值。">
<input value={imageProfileForm.auth_env} placeholder="CODEX_RETRY_GATEWAY_IMAGE_API_KEY" onChange={(event) => setImageProfileForm({ ...imageProfileForm, auth_env: event.target.value })} />
</Field>
<Field label="图片 Key 文件路径">
<input value={imageProfileForm.auth_file} placeholder="/run/secrets/image-provider-token" onChange={(event) => setImageProfileForm({ ...imageProfileForm, auth_file: event.target.value })} />
</Field>
</>
)}
{imageProfileForm.auth_mode === "auth_json" && (
<>
<Field label="图片 auth.json 路径">
<input value={imageProfileForm.auth_json_path} placeholder="留空表示 ~/.codex/auth.json" onChange={(event) => setImageProfileForm({ ...imageProfileForm, auth_json_path: event.target.value })} />
</Field>
<Field label="图片 auth.json 字段名">
<input value={imageProfileForm.auth_json_key} placeholder="OPENAI_API_KEY" onChange={(event) => setImageProfileForm({ ...imageProfileForm, auth_json_key: event.target.value })} />
</Field>
</>
)}
<div className="toolbar">
<button className="primary" type="submit">
保存图片 Profile
</button>
<button className="ghost" type="button" onClick={() => setImageProfileForm(defaultImageProfileForm)}>
清空表单
</button>
</div>
<Message message={imageProfileMessage} />
</form>
</Card>
<Card title="图片 Profile 探针" hint="不切换当前 gateway,只对选定图片 profile 请求 /v1/models 检查连通性和认证。">
{imageProfileProbeResult ? (
<div className="grid">
<InfoList
rows={[
["图片 Profile", imageProfileProbeResult.image_profile || "-"],
["上游", imageProfileProbeResult.image_base_url || "-"],
["Auth", `${imageProfileProbeResult.auth_mode || "-"} / ${imageProfileProbeResult.auth_source || "-"}`],
]}
/>
{(imageProfileProbeResult.probes || []).map((probe) => (
<div className="profile-card" key={`${probe.kind}-${probe.target}`}>
<div className="profile-head">
<div>
<h3>{probe.kind || "probe"}</h3>
<div className="hint">{probe.target || "-"}</div>
</div>
<span className="badge">{probe.status ?? "-"}</span>
</div>
<div className="mini-stats">
<MiniStat label="Content-Type" value={probe.content_type || "-"} />
</div>
<pre className="log-output">{probe.body_preview || "无返回摘要"}</pre>
</div>
))}
</div>
) : (
<div className="hint">先在图片 profile 卡片点击“探针”,这里会显示不切换情况下的探测结果。</div>
)}
</Card>
</div>
</section>
)}
{page === "rules" && (
<section className="page" data-active="true">
<div className="grid rules-layout">
<Card title="当前运行规则" hint="保存后会热生效,只影响当前正在运行的 gateway config;长期 profile 默认值请去 Profiles 页保存。">
<form onSubmit={saveRules}>
<Field label="reasoning_match_mode" hint="`518n-2` 会命中 516、1034、1552 等;切到 `manual` 时才只按手写列表判断。">
<select
value={ruleForm.reasoning_match_mode}
onChange={(event) =>
setRuleForm({ ...ruleForm, reasoning_match_mode: normalizeReasoningMode(event.target.value) })
}
>
<option value="formula_518n_minus_2">formula_518n_minus_2</option>
<option value="manual">manual</option>
</select>
</Field>
<Field label="reasoning_equals">
<input value={ruleForm.reasoning_equals} placeholder="例如:516, 1034, 1552" onChange={(event) => setRuleForm({ ...ruleForm, reasoning_equals: event.target.value })} />
</Field>
<Field label="retryable_status_codes">
<input value={ruleForm.retryable_status_codes} placeholder="例如:429, 503" onChange={(event) => setRuleForm({ ...ruleForm, retryable_status_codes: event.target.value })} />
</Field>
<Field label="retryable_error_messages" hint="每行一条错误文案;命中后会转成本地 non_stream_status_code。">
<textarea value={ruleForm.retryable_error_messages} onChange={(event) => setRuleForm({ ...ruleForm, retryable_error_messages: event.target.value })} />
</Field>
<div className="field-row">
<Field label="upstream_fetch_retry_attempts">
<input type="number" min={1} value={ruleForm.upstream_fetch_retry_attempts} onChange={(event) => setRuleForm({ ...ruleForm, upstream_fetch_retry_attempts: event.target.value })} />
</Field>
<Field label="upstream_fetch_retry_backoff_ms">
<input type="number" min={0} step={50} value={ruleForm.upstream_fetch_retry_backoff_ms} onChange={(event) => setRuleForm({ ...ruleForm, upstream_fetch_retry_backoff_ms: event.target.value })} />
</Field>
</div>
<Field label="endpoints">
<textarea value={ruleForm.endpoints} onChange={(event) => setRuleForm({ ...ruleForm, endpoints: event.target.value })} />
</Field>
<div className="field-row">
<Field label="non_stream_status_code">
<input type="number" min={100} max={599} value={ruleForm.non_stream_status_code} onChange={(event) => setRuleForm({ ...ruleForm, non_stream_status_code: event.target.value })} />
</Field>
<label className="inline-toggle">
<input type="checkbox" checked={ruleForm.log_match} onChange={(event) => setRuleForm({ ...ruleForm, log_match: event.target.checked })} />
命中时写日志
</label>
</div>
<div className="toolbar">
<button className="primary" type="submit">
保存并立即生效
</button>
<button className="danger" type="button" onClick={restoreConfig}>
恢复 Codex 原设置并关闭网关
</button>
</div>
<Message message={ruleMessage} />
</form>
</Card>
<Card title="Thread 覆盖" hint={threadRulesMeta}>
<div className="thread-rule-list">
{threadRules.length === 0 ? (
<div className="empty-state">当前没有按 thread_id 的拦截覆盖,全部走默认规则。</div>
) : (
threadRules.map((rule) => {
const busy = threadRuleBusyThreadId === rule.thread_id;
return (
<article className="thread-rule-card" key={rule.thread_id}>
<div className="thread-rule-head">
<div>
<strong>{rule.thread_id}</strong>
<div className="hint">更新于 {timestamp(rule.updated_at)}</div>
</div>
<span className={`badge ${threadGuardBadgeTone(rule.reasoning_intercept_enabled ? "enabled" : "disabled")}`}>
{rule.reasoning_intercept_enabled ? "强制开启" : "已关闭"}
</span>
</div>
<div className="thread-rule-actions">
<button className="ghost" type="button" disabled={busy || rule.reasoning_intercept_enabled} onClick={() => updateThreadRule(rule.thread_id, true)}>
{busy && !rule.reasoning_intercept_enabled ? "处理中" : "开启拦截"}
</button>
<button className="ghost" type="button" disabled={busy || !rule.reasoning_intercept_enabled} onClick={() => updateThreadRule(rule.thread_id, false)}>
{busy && rule.reasoning_intercept_enabled ? "处理中" : "关闭拦截"}
</button>
<button className="secondary" type="button" disabled={busy} onClick={() => restoreThreadRule(rule.thread_id)}>
{busy ? "处理中" : "恢复默认"}
</button>
</div>
</article>
);
})
)}
</div>
<Message message={threadRuleMessage} />
</Card>
</div>
</section>
)}
{page === "logs" && (
<section className="page" data-active="true">
<Card
title="实时日志"
hint={logsMeta}
actions={
<button className="secondary" type="button" onClick={() => loadLogs(false)}>
刷新日志
</button>
}
>
<pre className="log-output">{logs}</pre>
</Card>
</section>
)}
</main>
</div>
<div className="switch-overlay" data-active={Boolean(switchingTo)}>
<div className="switch-dialog">
<span className="eyebrow">Profile Switch</span>
<h2>正在切换到 {switchingTo}</h2>
<p className="muted">gateway 正在后台热切换当前运行配置,页面会自动等待目标 profile 生效。</p>
<div className="progress-line">
<span />
</div>
</div>
</div>
</>
);
}
function Card({
title,
hint,
badge,
actions,
children,
}: {
title: string;
hint?: string;
badge?: string;
actions?: React.ReactNode;
children: React.ReactNode;
}) {
return (
<div className="card">
<div className="card-inner">
<div className="card-head">
<div>
<h3>{title}</h3>
{hint ? <div className="hint">{hint}</div> : null}
</div>
{badge ? <span className="badge">{badge}</span> : actions}
</div>
{children}
</div>
</div>
);
}
function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) {
return (
<label className="field">
<span>{label}</span>
{children}
{hint ? <span className="hint">{hint}</span> : null}
</label>
);
}
function Stat({ label, value }: { label: string; value: string }) {
return (
<div className="stat">
<label>{label}</label>
<strong>{value}</strong>
</div>
);
}
function TokenCard({ label, value }: { label: string; value: string }) {
return (
<div className="token-card">
<label>{label}</label>
<strong>{value}</strong>
</div>
);
}
function InfoList({ rows }: { rows: Array<[string, string]> }) {
return (
<div className="info-list">
{rows.map(([label, value]) => (
<div className="info-row" key={label}>
<label>{label}</label>
<span>{value}</span>
</div>
))}
</div>
);
}
function MiniStat({ label, value }: { label: string; value: string }) {
return (
<div className="mini-stat">
<label>{label}</label>
<span>{value}</span>
</div>
);
}
function SideCard({ label, value, strong = false }: { label: string; value: string; strong?: boolean }) {
return (
<div className="side-card">
<label>{label}</label>
{strong ? <strong>{value}</strong> : <span>{value}</span>}
</div>
);
}
function Message({ message }: { message: MessageState }) {
return (
<div className="message" data-tone={message.tone}>
{message.text}
</div>
);
}