Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 152 additions & 12 deletions apps/src/app/aggregate-api/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
PencilLine,
Plus,
RefreshCw,
Settings2,
ShieldCheck,
Trash2,
Unplug,
Expand All @@ -25,6 +26,16 @@ import { ConfirmDialog } from "@/components/modals/confirm-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
Expand Down Expand Up @@ -53,6 +64,8 @@ import { useDesktopPageActive } from "@/hooks/useDesktopPageActive";
import { usePageTransitionReady } from "@/hooks/usePageTransitionReady";
import { useRuntimeCapabilities } from "@/hooks/useRuntimeCapabilities";
import { accountClient } from "@/lib/api/account-client";
import { appClient } from "@/lib/api/app-client";
import { getAppErrorMessage } from "@/lib/api/transport";
import { aggregateApiProviderMatchesFilter } from "@/lib/aggregate-api-provider";
import { useI18n } from "@/lib/i18n/provider";
import { useAppStore } from "@/lib/store/useAppStore";
Expand Down Expand Up @@ -115,6 +128,8 @@ export default function AggregateApiPage() {
const { t } = useI18n();
const queryClient = useQueryClient();
const serviceStatus = useAppStore((state) => state.serviceStatus);
const appSettings = useAppStore((state) => state.appSettings);
const setAppSettings = useAppStore((state) => state.setAppSettings);
const { canAccessManagementRpc } = useRuntimeCapabilities();
const isServiceReady = canAccessManagementRpc && serviceStatus.connected;
const isPageActive = useDesktopPageActive("/aggregate-api/");
Expand All @@ -139,6 +154,9 @@ export default function AggregateApiPage() {
const [associationItems, setAssociationItems] = useState<AggregateApiFetchedModel[]>([]);
const [fetchingModelsApiId, setFetchingModelsApiId] = useState<string | null>(null);
const [associatingModels, setAssociatingModels] = useState(false);
const [probeSettingsOpen, setProbeSettingsOpen] = useState(false);
const [probeUserAgentMode, setProbeUserAgentMode] = useState("codex");
const [probeUserAgent, setProbeUserAgent] = useState("");

const { data: aggregateApis = [], isLoading } = useQuery({
queryKey: ["aggregate-apis"],
Expand All @@ -158,6 +176,7 @@ export default function AggregateApiPage() {
setEditingId(null);
setDeleteId(null);
setRevealedSecrets({});
setProbeSettingsOpen(false);
});
return () => window.cancelAnimationFrame(frameId);
}, [isPageActive]);
Expand Down Expand Up @@ -219,6 +238,29 @@ export default function AggregateApiPage() {
},
});

const probeSettingsMutation = useMutation({
mutationFn: () =>
appClient.setSettings({
aggregateApiProbeUserAgentMode: probeUserAgentMode,
aggregateApiProbeUserAgent: probeUserAgent.trim(),
}),
onSuccess: (settings) => {
queryClient.setQueryData(["app-settings-snapshot"], settings);
setAppSettings(settings);
setProbeSettingsOpen(false);
toast.success(t("连通性测试设置已更新"));
},
onError: (error: unknown) => {
toast.error(`${t("更新连通性测试设置失败")}: ${getAppErrorMessage(error)}`);
},
});

const openProbeSettings = () => {
setProbeUserAgentMode(appSettings.aggregateApiProbeUserAgentMode || "codex");
setProbeUserAgent(appSettings.aggregateApiProbeUserAgent || "");
setProbeSettingsOpen(true);
};

const balanceMutation = useMutation({
mutationFn: (apiId: string) => accountClient.refreshAggregateApiBalance(apiId),
onMutate: (apiId) => setRefreshingBalanceId(apiId),
Expand Down Expand Up @@ -363,18 +405,36 @@ export default function AggregateApiPage() {
{t("连通性测试只使用已配置路由对应的模型。")}
</p>
</div>
<Select value={providerFilter} onValueChange={(value) => setProviderFilter(value || "all")}>
<SelectTrigger className="h-9 w-[150px]"><SelectValue /></SelectTrigger>
<SelectContent><SelectGroup>
<SelectItem value="all">{t("全部类型")}</SelectItem>
<SelectItem value="codex">Codex</SelectItem>
<SelectItem value="claude">Claude</SelectItem>
<SelectItem value="gemini">Gemini</SelectItem>
<SelectItem value="compatible">
{t("通用兼容(Codex + Claude)")}
</SelectItem>
</SelectGroup></SelectContent>
</Select>
<div className="flex items-center gap-2">
<Select value={providerFilter} onValueChange={(value) => setProviderFilter(value || "all")}>
<SelectTrigger className="h-9 w-[150px]"><SelectValue /></SelectTrigger>
<SelectContent><SelectGroup>
<SelectItem value="all">{t("全部类型")}</SelectItem>
<SelectItem value="codex">Codex</SelectItem>
<SelectItem value="claude">Claude</SelectItem>
<SelectItem value="gemini">Gemini</SelectItem>
<SelectItem value="compatible">
{t("通用兼容(Codex + Claude)")}
</SelectItem>
</SelectGroup></SelectContent>
</Select>
<Tooltip>
<TooltipTrigger
render={
<Button
type="button"
variant="outline"
size="icon"
aria-label={t("连通性测试设置")}
onClick={openProbeSettings}
/>
}
>
<Settings2 className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent>{t("连通性测试设置")}</TooltipContent>
</Tooltip>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
Expand Down Expand Up @@ -530,6 +590,86 @@ export default function AggregateApiPage() {
defaultSort={defaultCreateSort}
/>

<Dialog open={probeSettingsOpen} onOpenChange={setProbeSettingsOpen}>
<DialogContent className="glass-card sm:max-w-[520px]">
<DialogHeader>
<DialogTitle>{t("连通性测试设置")}</DialogTitle>
<DialogDescription>
{t("设置 Codex 类型 route 执行连通性测试时使用的客户端标识。")}
</DialogDescription>
</DialogHeader>

<div className="grid gap-5 py-2">
<div className="grid gap-2">
<Label>{t("测试客户端")}</Label>
<Select
value={probeUserAgentMode}
onValueChange={(value) => setProbeUserAgentMode(value || "codex")}
>
<SelectTrigger className="w-full">
<SelectValue>
{(value) =>
String(value || "") === "custom"
? t("自定义 User-Agent")
: t("Codex 官方客户端(默认)")
}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="codex">{t("Codex 官方客户端(默认)")}</SelectItem>
<SelectItem value="custom">{t("自定义 User-Agent")}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{probeUserAgentMode === "codex"
? t("发送官方格式的 User-Agent、originator 和 Codex 客户端指纹请求头。")
: t("仅使用指定的 User-Agent,不附加 Codex 客户端指纹。")}
</p>
</div>

{probeUserAgentMode === "custom" ? (
<div className="grid gap-2">
<Label htmlFor="aggregate-api-probe-user-agent">User-Agent</Label>
<Input
id="aggregate-api-probe-user-agent"
className="font-mono"
value={probeUserAgent}
maxLength={512}
placeholder="Custom-Client/1.0"
onChange={(event) => setProbeUserAgent(event.target.value)}
/>
{!probeUserAgent.trim() ? (
<p className="text-xs text-destructive">{t("请输入自定义 User-Agent")}</p>
) : null}
</div>
) : null}
</div>

<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setProbeSettingsOpen(false)}
disabled={probeSettingsMutation.isPending}
>
{t("取消")}
</Button>
<Button
type="button"
onClick={() => probeSettingsMutation.mutate()}
disabled={
probeSettingsMutation.isPending
|| (probeUserAgentMode === "custom" && !probeUserAgent.trim())
}
>
{probeSettingsMutation.isPending ? t("保存中...") : t("保存")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

<AggregateApiModelAssociationModal
open={Boolean(associationApiId)}
onOpenChange={(open) => {
Expand Down
3 changes: 3 additions & 0 deletions apps/src/lib/api/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1811,6 +1811,9 @@ export function normalizeAppSettings(payload: unknown): AppSettings {
source.threadAwareAccountDistributionEnabled,
true
),
aggregateApiProbeUserAgentMode:
asString(source.aggregateApiProbeUserAgentMode) || "codex",
aggregateApiProbeUserAgent: asString(source.aggregateApiProbeUserAgent),
quotaGuard: normalizeQuotaGuard(source.quotaGuard ?? source.quota_guard),
gatewayOriginator:
asString(source.gatewayOriginator) || DEFAULT_CODEX_ORIGINATOR,
Expand Down
13 changes: 13 additions & 0 deletions apps/src/lib/i18n/messages/sections/en-aggregate-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ export const EN_AGGREGATE_API_MESSAGES: MessageCatalog = {
上游连接: "Upstream connections",
"连通性测试只使用已配置路由对应的模型。":
"Connection tests only use models referenced by configured routes.",
连通性测试设置: "Connection test settings",
"设置 Codex 类型 route 执行连通性测试时使用的客户端标识。":
"Configure the client identity used when testing Codex-type routes.",
测试客户端: "Test client",
"Codex 官方客户端(默认)": "Official Codex client (default)",
"自定义 User-Agent": "Custom User-Agent",
"发送官方格式的 User-Agent、originator 和 Codex 客户端指纹请求头。":
"Send the official User-Agent, originator, and Codex client fingerprint headers.",
"仅使用指定的 User-Agent,不附加 Codex 客户端指纹。":
"Use only the specified User-Agent without Codex client fingerprint headers.",
"请输入自定义 User-Agent": "Enter a custom User-Agent",
连通性测试设置已更新: "Connection test settings updated",
更新连通性测试设置失败: "Failed to update connection test settings",
"通用兼容(Codex + Claude)": "Compatible (Codex + Claude)",
"按请求路径原样转发 Codex 与 Claude 协议;自定义 action 会自动关闭。":
"Forwards Codex and Claude protocols using the incoming path; custom action is disabled automatically.",
Expand Down
13 changes: 13 additions & 0 deletions apps/src/lib/i18n/messages/sections/ko-aggregate-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ export const KO_AGGREGATE_API_MESSAGES: MessageCatalog = {
上游连接: "업스트림 연결",
"连通性测试只使用已配置路由对应的模型。":
"연결 테스트는 설정된 라우트에 연결된 모델만 사용합니다.",
连通性测试设置: "연결 테스트 설정",
"设置 Codex 类型 route 执行连通性测试时使用的客户端标识。":
"Codex 유형 route 연결 테스트에 사용할 클라이언트 식별자를 설정합니다.",
测试客户端: "테스트 클라이언트",
"Codex 官方客户端(默认)": "Codex 공식 클라이언트 (기본값)",
"自定义 User-Agent": "사용자 지정 User-Agent",
"发送官方格式的 User-Agent、originator 和 Codex 客户端指纹请求头。":
"공식 형식의 User-Agent, originator 및 Codex 클라이언트 지문 헤더를 전송합니다.",
"仅使用指定的 User-Agent,不附加 Codex 客户端指纹。":
"지정한 User-Agent만 사용하고 Codex 클라이언트 지문은 추가하지 않습니다.",
"请输入自定义 User-Agent": "사용자 지정 User-Agent를 입력하세요",
连通性测试设置已更新: "연결 테스트 설정이 업데이트되었습니다",
更新连通性测试设置失败: "연결 테스트 설정 업데이트 실패",
"通用兼容(Codex + Claude)": "호환 모드 (Codex + Claude)",
"按请求路径原样转发 Codex 与 Claude 协议;自定义 action 会自动关闭。":
"요청 경로를 그대로 사용해 Codex 및 Claude 프로토콜을 전달합니다. 사용자 지정 action은 자동으로 꺼집니다.",
Expand Down
13 changes: 13 additions & 0 deletions apps/src/lib/i18n/messages/sections/ru-aggregate-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ export const RU_AGGREGATE_API_MESSAGES: MessageCatalog = {
上游连接: "Upstream-подключения",
"连通性测试只使用已配置路由对应的模型。":
"Проверка подключения использует только модели из настроенных маршрутов.",
连通性测试设置: "Настройки проверки подключения",
"设置 Codex 类型 route 执行连通性测试时使用的客户端标识。":
"Настройте идентификатор клиента для проверки маршрутов типа Codex.",
测试客户端: "Клиент для проверки",
"Codex 官方客户端(默认)": "Официальный клиент Codex (по умолчанию)",
"自定义 User-Agent": "Пользовательский User-Agent",
"发送官方格式的 User-Agent、originator 和 Codex 客户端指纹请求头。":
"Отправлять официальный User-Agent, originator и заголовки отпечатка клиента Codex.",
"仅使用指定的 User-Agent,不附加 Codex 客户端指纹。":
"Использовать только указанный User-Agent без отпечатка клиента Codex.",
"请输入自定义 User-Agent": "Введите пользовательский User-Agent",
连通性测试设置已更新: "Настройки проверки подключения обновлены",
更新连通性测试设置失败: "Не удалось обновить настройки проверки подключения",
"通用兼容(Codex + Claude)": "Совместимый режим (Codex + Claude)",
"按请求路径原样转发 Codex 与 Claude 协议;自定义 action 会自动关闭。":
"Передаёт протоколы Codex и Claude по исходному пути запроса; пользовательский action отключается автоматически.",
Expand Down
2 changes: 2 additions & 0 deletions apps/src/lib/store/useAppStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export const useAppStore = create<AppState>((set) => ({
compactModelForwardRules: "",
accountMaxInflight: 1,
threadAwareAccountDistributionEnabled: true,
aggregateApiProbeUserAgentMode: "codex",
aggregateApiProbeUserAgent: "",
quotaGuard: {
enabled: true,
primaryMinRemainingPercent: 5,
Expand Down
2 changes: 2 additions & 0 deletions apps/src/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export interface AppSettings extends GatewayTransportValues {
compactModelForwardRules: string;
accountMaxInflight: number;
threadAwareAccountDistributionEnabled: boolean;
aggregateApiProbeUserAgentMode: string;
aggregateApiProbeUserAgent: string;
quotaGuard: QuotaGuardSettings;
gatewayOriginator: string;
gatewayOriginatorDefault: string;
Expand Down
21 changes: 18 additions & 3 deletions crates/service/src/aggregate_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1348,12 +1348,27 @@ fn build_gemini_probe_body() -> serde_json::Value {
/// # 返回
/// 返回函数执行结果
fn add_codex_probe_headers(
builder: reqwest::blocking::RequestBuilder,
mut builder: reqwest::blocking::RequestBuilder,
) -> Result<reqwest::blocking::RequestBuilder, String> {
let mode = crate::app_settings::current_aggregate_api_probe_user_agent_mode();
if mode == crate::app_settings::AGGREGATE_API_PROBE_USER_AGENT_MODE_CODEX {
let request_id = gateway::next_trace_id();
builder = builder
.header("user-agent", gateway::current_codex_user_agent())
.header("originator", gateway::current_wire_originator())
.header("session-id", request_id.as_str())
.header("thread-id", request_id.as_str())
.header("x-client-request-id", request_id.as_str())
.header("x-codex-window-id", format!("{request_id}:0"));
} else {
let user_agent = crate::app_settings::current_aggregate_api_probe_user_agent();
if user_agent.is_empty() {
return Err("aggregate api probe custom user agent is required".to_string());
}
builder = builder.header("user-agent", user_agent);
}
Ok(builder
.header("accept", "application/json")
.header("user-agent", gateway::current_codex_user_agent())
.header("originator", gateway::current_wire_originator())
.header("accept-encoding", "identity"))
}

Expand Down
Loading