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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,30 @@ Codex++ 是面向 OpenAI Codex / ChatGPT 桌面应用的外部启动器与管理
</td>
<td><a href="https://deepkey.top/register?aff=DNVc"><strong>deepkey|API KEY</strong></a><br>感谢 deepkey 赞助本项目!deepkey 起初只是连接顶级算力的上游供应商,凭借稳定低价的接口被学生群体发现并口口相传。随着开发者与学子的涌入,这里不再只是 API 的搬运工,而是共同探讨提示词工程、分享创新应用的温暖社区。从工具到伙伴,deepkey 见证了无数灵感的诞生与落地。通过<a href="https://deepkey.top/register?aff=DNVc">此链接注册</a>进入群聊,可享受福利并与小伙伴们一起探讨。</td>
</tr>
<tr>
<td align="center">
<a href="https://go.apimart.ai/gh-codexplusplus">
<img src="docs/images/sponsor-apimart.png" alt="API Mart" width="170">
</a>
</td>
<td><a href="https://go.apimart.ai/gh-codexplusplus"><strong>API Mart</strong></a><br>感谢 API Mart 赞助了本项目!API Mart 是专注 AI 图片和视频生成的低价 API 平台,GPT-Image-2 低至每张 0.006 美元,1 美元可生成 160 多张图片。图片、视频使用一套异步 API,提交任务获取 ID 后可通过轮询或回调取得结果;支持数万张批量任务,切换模型无需改代码。按量付费、无月费,通过<a href="https://go.apimart.ai/gh-codexplusplus">此链接注册</a>即可使用。</td>
</tr>
<tr>
<td align="center">
<a href="https://api.fenno.ai/s/ZZM7">
<img src="docs/images/sponsor-fenno-ai.png" alt="FennoAI" width="170">
</a>
</td>
<td><a href="https://api.fenno.ai/s/ZZM7"><strong>FennoAI</strong></a><br>FennoAI 是一家稳定、高效的 API 中转服务商,目前主要提供 Codex 中转服务,兼容 OpenAI 及 Anthropic 协议,可灵活接入 Codex、Claude Code、OpenCode 等主流编程工具,稳定支撑千亿 Token/日的企业级调用需求,支持国内及海外主体公对公结算、开票。通过<a href="https://api.fenno.ai/s/ZZM7">专属链接</a>购买订阅,仅需 1.99 美元即可获得价值 50 美元的 Coding Plan 额度;邀请好友购买最高可获得 20% 返佣。商务联系微信:whatflyto。</td>
</tr>
<tr>
<td align="center">
<a href="https://s.qiniu.com/7zUJri">
<img src="docs/images/sponsor-qiniu-ai.png" alt="七牛云" width="170">
</a>
</td>
<td><a href="https://s.qiniu.com/7zUJri"><strong>七牛云</strong></a><br>感谢七牛云 AI 赞助本项目!七牛云 AI 是七牛云(02567.HK)旗下企业级大模型 MaaS 平台,可一站式调用全球 150 多个主流模型,兼容全球主流模型厂商协议,覆盖文本、图像、音频、视频、文件处理等全模态能力,服务超过 169 万企业及开发者用户。企业用户可免费领取 1200 万 Token,邀请好友最高可获得百亿 Token。商务联系微信:whatflyto。</td>
</tr>
</table>

## 交流与支持
Expand Down
1 change: 1 addition & 0 deletions apps/codex-plus-manager/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6640,6 +6640,7 @@ mod tests {
session_provider: codex_plus_core::settings::RelaySessionProvider::Custom,
strategy: codex_plus_core::settings::AggregateRelayStrategy::Failover,
members: Vec::new(),
routes: Vec::new(),
}],
..BackendSettings::default()
};
Expand Down
123 changes: 120 additions & 3 deletions apps/codex-plus-manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import {
type ImageHandling,
type ModelWindowRow,
} from "./model-windows";
import { clampAggregateRoutePriority, normalizeAggregateRoutes, validateAggregateRoutes } from "./aggregate-routes";
import { relayAuthForLiveDraft, shouldBackfillRelayProfileBeforeSwitch } from "./relay-live-files";
import { resolveProviderName } from "./provider-name";
import { resolveProviderSyncCompletion } from "./provider-sync-flow";
Expand Down Expand Up @@ -314,9 +315,15 @@ type RelayAggregateMember = {
profileId: string;
weight: number;
};
type RelayAggregateRoute = {
pattern: string;
profileId: string;
priority: number;
};
type RelayAggregateConfig = {
strategy: RelayAggregateStrategy;
members: RelayAggregateMember[];
routes?: RelayAggregateRoute[];
};
type AggregateRelayMember = {
relayId: string;
Expand All @@ -328,6 +335,7 @@ type AggregateRelayProfile = {
sessionProvider?: RelaySessionProvider;
strategy: RelayAggregateStrategy;
members: AggregateRelayMember[];
routes?: { pattern: string; relayId: string; priority: number }[];
};

/// codex 的 config.toml 上下文表。skill 不在这里——它是 `$CODEX_HOME/skills/`
Expand Down Expand Up @@ -8743,6 +8751,28 @@ function AggregateRelayProfileEditor({
});
};
const totalWeight = aggregate.members.reduce((total, member) => total + clampAggregateWeight(member.weight), 0);
const routes = aggregate.routes ?? [];
const routeTargetOptions = aggregate.members
.map((member) => {
const candidate = candidates.find((item) => item.id === member.profileId);
return { value: member.profileId, label: candidate?.name || t("未命名供应商") };
})
.filter((option) => option.value.trim() !== "");
const updateRoute = (index: number, patch: Partial<RelayAggregateRoute>) => {
updateAggregate({
...aggregate,
routes: routes.map((route, routeIndex) => (routeIndex === index ? { ...route, ...patch } : route)),
});
};
const removeRoute = (index: number) => {
updateAggregate({ ...aggregate, routes: routes.filter((_, routeIndex) => routeIndex !== index) });
};
const addRoute = () => {
updateAggregate({
...aggregate,
routes: [...routes, { pattern: "", profileId: aggregate.members[0]?.profileId ?? "", priority: 0 }],
});
};

return (
<div className="relay-profile-editor aggregate-editor">
Expand Down Expand Up @@ -8837,11 +8867,68 @@ function AggregateRelayProfileEditor({
<div className="empty">{t("先添加至少 1 个已填写 Base URL / Key 的 API 供应商,再创建聚合供应商。")}</div>
)}
</div>
<div className="aggregate-routes">
<div className="aggregate-routes-head">
<div>
<strong>{t("路由规则")}</strong>
<span>{t("按模型名自动路由到指定成员;仅支持 * 通配符,chat/completions 协议不走路由。")}</span>
</div>
<UiBadge variant="outline">{routes.length}</UiBadge>
</div>
{routes.length ? (
<div className="aggregate-route-list">
{routes.map((route, index) => (
<div className="aggregate-route-row" key={index}>
<Input
onChange={(event) => updateRoute(index, { pattern: event.currentTarget.value })}
placeholder={t("例如 deepseek-*")}
value={route.pattern}
/>
<AppSelect
onChange={(value) => updateRoute(index, { profileId: value })}
options={routeTargetOptions}
value={route.profileId}
/>
{!routeTargetOptions.some((option) => option.value === route.profileId) ? (
<span className="aggregate-route-target-error">{t("路由目标必须是已勾选的聚合成员,请先在成员供应商中勾选。")}</span>
) : null}
<div className="aggregate-route-priority">
<span>{t("优先级")}</span>
<Input
min={0}
onChange={(event) =>
updateRoute(index, { priority: clampAggregateRoutePriority(Number.parseInt(event.currentTarget.value, 10)) })
}
type="number"
value={String(route.priority)}
/>
</div>
<button
className="aggregate-route-remove"
onClick={() => removeRoute(index)}
title={t("删除规则")}
type="button"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
))}
</div>
) : (
<div className="empty">{t("暂无路由规则,未匹配的模型会按聚合策略选择成员。")}</div>
)}
<div>
<Button disabled={!aggregate.members.length} onClick={addRoute} size="sm" variant="secondary">
<Plus className="h-4 w-4" />
{t("添加规则")}
</Button>
</div>
</div>
<div className="relay-grid compact aggregate-preview">
<Metric label={t("策略")} value={aggregateStrategyLabel(aggregate.strategy)} />
<Metric label={t("成员数量")} value={tf("{0} 个", [aggregate.members.length])} />
<Metric label={t("总权重")} value={`${totalWeight}`} />
<Metric label={t("序列化字段")} value="aggregate.strategy / aggregate.members" />
<Metric label={t("序列化字段")} value="aggregate.strategy / aggregate.members / aggregate.routes" />
</div>
<div className="hint-line relay-protocol-hint">
<ShieldCheck className="h-4 w-4" />
Expand Down Expand Up @@ -11166,6 +11253,7 @@ function hydrateAggregateRelayProfile(profile: RelayProfile, aggregate: Aggregat
profileId: member.relayId,
weight: clampAggregateWeight(member.weight),
})),
routes: normalizeAggregateRoutes(aggregate.routes ?? []),
},
};
}
Expand Down Expand Up @@ -11863,6 +11951,7 @@ function normalizeAggregateProfilesFromRelayProfiles(profiles: RelayProfile[]):
const candidates = profiles.filter((profile) => !isAggregateRelayProfile(profile));
return profiles.filter(isAggregateRelayProfile).map((profile) => {
const aggregate = normalizeAggregateConfig(profile.aggregate, candidates);
const memberIds = new Set(aggregate.members.map((member) => member.profileId));
return {
id: profile.id,
name: profile.name || t("聚合供应商"),
Expand All @@ -11872,6 +11961,11 @@ function normalizeAggregateProfilesFromRelayProfiles(profiles: RelayProfile[]):
relayId: member.profileId,
weight: clampAggregateWeight(member.weight),
})),
routes: normalizeAggregateRoutes(aggregate.routes ?? [], { dropEmptyPattern: true, memberIds }).map((route) => ({
pattern: route.pattern,
relayId: route.profileId,
priority: route.priority,
})),
};
});
}
Expand Down Expand Up @@ -12026,6 +12120,7 @@ function removeRelayProfile(settings: BackendSettings, id: string): BackendSetti
aggregate: {
...normalizeAggregateConfig(profile.aggregate, []),
members: normalizeAggregateConfig(profile.aggregate, []).members.filter((member) => member.profileId !== id),
routes: normalizeAggregateConfig(profile.aggregate, []).routes ?? [],
},
},
{ ...settings, relayProfiles: profiles },
Expand Down Expand Up @@ -12107,7 +12202,14 @@ function normalizeAggregateConfig(
seen.add(member.profileId);
return { profileId: member.profileId, weight: clampAggregateWeight(member.weight) };
});
return { strategy, members };
const routes = (aggregate?.routes ?? [])
.filter((route) => route.pattern.trim() !== "" || route.profileId.trim() !== "")
.map((route) => ({
pattern: route.pattern.trim(),
profileId: route.profileId,
priority: clampAggregateRoutePriority(route.priority),
}));
return { strategy, members, routes };
}

function aggregateMemberCandidates(settings: BackendSettings, aggregateId: string): RelayProfile[] {
Expand Down Expand Up @@ -12138,7 +12240,22 @@ function aggregateStrategyHelp(strategy: RelayAggregateStrategy): string {

function aggregateRelayProfileValidation(profile: RelayProfile): string | null {
const aggregate = normalizeAggregateConfig(profile.aggregate, []);
return aggregate.members.length >= 1 ? null : t("聚合供应商至少需要勾选 1 个已填写 Base URL / Key 的 API 供应商。");
if (aggregate.members.length < 1) {
return t("聚合供应商至少需要勾选 1 个已填写 Base URL / Key 的 API 供应商。");
}
const issues = validateAggregateRoutes(
aggregate.routes ?? [],
new Set(aggregate.members.map((member) => member.profileId)),
);
if (!issues) return null;
const first = issues[0];
if (first.code === "emptyPattern") {
return t("路由规则的模型匹配模式不能为空。");
}
if (first.code === "invalidPriority") {
return tf("路由规则「{0}」的优先级必须是大于等于 0 的整数。", [first.pattern]);
}
return tf("路由规则「{0}」的目标供应商必须是聚合成员,请先将其勾选为成员。", [first.pattern]);
}

function numberOrDefault(value: string, fallback: number) {
Expand Down
Loading
Loading