Skip to content
Merged
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
1 change: 1 addition & 0 deletions apps/src/app/apikeys/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const ROTATION_STRATEGY_LABELS: Record<string, string> = {
account_rotation: "账号轮转",
aggregate_api_rotation: "聚合API轮转",
hybrid_rotation: "混合轮转(账号优先)",
hybrid_aggregate_first_rotation: "混合轮转(聚合优先)",
};

function userCanOwnApiKey(user: AppUser): boolean {
Expand Down
1 change: 1 addition & 0 deletions apps/src/app/models/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const ROTATION_STRATEGY_LABELS: Record<string, string> = {
account_rotation: "OpenAI 账号池",
aggregate_api_rotation: "聚合 API",
hybrid_rotation: "混合路由",
hybrid_aggregate_first_rotation: "混合路由",
};

function routeStrategyLabel(
Expand Down
1 change: 1 addition & 0 deletions apps/src/app/platform-mode/page-sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const ROTATION_STRATEGY_LABELS: Record<string, string> = {
account_rotation: "OpenAI 账号池",
aggregate_api_rotation: "聚合 API",
hybrid_rotation: "混合路由",
hybrid_aggregate_first_rotation: "混合路由",
};

function rotationStrategyLabel(strategy: string, t: Translate): string {
Expand Down
15 changes: 10 additions & 5 deletions apps/src/components/modals/api-key-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const ROTATION_STRATEGY_LABELS: Record<string, string> = {
account_rotation: "账号轮转",
aggregate_api_rotation: "聚合API轮转",
hybrid_rotation: "混合轮转(账号优先)",
hybrid_aggregate_first_rotation: "混合轮转(聚合优先)",
};

const ACCOUNT_PLAN_FILTER_LABELS: Record<string, string> = {
Expand Down Expand Up @@ -172,7 +173,8 @@ export function ApiKeyModal({
const memberOwnershipEnabled = isAdminMode && showMemberOwnership;
const usesAccountPlanFilter =
rotationStrategy === "account_rotation" ||
rotationStrategy === "hybrid_rotation";
rotationStrategy === "hybrid_rotation" ||
rotationStrategy === "hybrid_aggregate_first_rotation";
const billableUsers = useMemo(
() => appUsers.filter((user) => userCanOwnApiKey(user)),
[appUsers],
Expand Down Expand Up @@ -494,13 +496,16 @@ export function ApiKeyModal({
<SelectItem value="hybrid_rotation">
{t("混合轮转(账号优先)")}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<SelectItem value="hybrid_aggregate_first_rotation">
{t("混合轮转(聚合优先)")}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<p className="col-span-2 -mt-1 text-[11px] text-muted-foreground">
{t(
"账号轮转只走账号池;聚合API轮转只走聚合API;混合轮转先走账号池,账号耗尽后使用聚合API兜底。",
"账号轮转只走账号池;聚合API轮转只走聚合API;混合轮转(账号优先)先走账号池,账号耗尽后使用聚合API兜底;混合轮转(聚合优先)先走聚合API,聚合不可用时回落账号池。",
)}
</p>
</>
Expand Down
5 changes: 3 additions & 2 deletions apps/src/lib/i18n/messages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export const EN_MESSAGES: MessageCatalog = {
账号轮转: "Account rotation",
聚合API轮转: "Aggregate API rotation",
"混合轮转(账号优先)": "Hybrid rotation (accounts first)",
"混合轮转(聚合优先)": "Hybrid rotation (aggregate APIs first)",
"总使用 Token": "Total token usage",
最近调用: "Last called",
从未调用: "Never called",
Expand Down Expand Up @@ -762,8 +763,8 @@ export const EN_MESSAGES: MessageCatalog = {
"Configure gateway credentials. You can bind a specific model, reasoning tier, or custom upstream.",
"密钥名称 (可选)": "Key name (optional)",
"例如:主机房 / 测试": "e.g. Main room / Test",
"账号轮转只走账号池;聚合API轮转只走聚合API;混合轮转先走账号池,账号耗尽后使用聚合API兜底。":
"Account rotation uses only the account pool; aggregate API rotation uses only aggregate APIs; hybrid rotation tries accounts first and falls back to aggregate APIs when accounts are exhausted.",
"账号轮转只走账号池;聚合API轮转只走聚合API;混合轮转(账号优先)先走账号池,账号耗尽后使用聚合API兜底;混合轮转(聚合优先)先走聚合API,聚合不可用时回落账号池。":
"Account rotation uses only the account pool; aggregate API rotation uses only aggregate APIs; hybrid rotation (accounts first) tries accounts first and falls back to aggregate APIs when accounts are exhausted; hybrid rotation (aggregate APIs first) tries aggregate APIs first and falls back to the account pool when they are unavailable.",
"仅对账号轮转和混合轮转生效,可限制这把平台密钥只从指定账号计划类型中选路由账号。":
"Applies only to account rotation and hybrid rotation. Limits this API key to routing through accounts with the selected plan type.",
"默认按路径通配:": "Path matching by default:",
Expand Down
5 changes: 3 additions & 2 deletions apps/src/lib/i18n/messages/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ export const KO_MESSAGES: MessageCatalog = {
账号轮转: "계정 로테이션",
聚合API轮转: "집계 API 로테이션",
"混合轮转(账号优先)": "하이브리드 로테이션(계정 우선)",
"混合轮转(聚合优先)": "하이브리드 로테이션(집계 API 우선)",
协议: "프로토콜",
轮转策略: "로테이션 전략",
绑定模型: "바인딩 모델",
Expand Down Expand Up @@ -1190,8 +1191,8 @@ export const KO_MESSAGES: MessageCatalog = {
"원하는 색상 테마를 선택해 작업 분위기에 맞추세요",
在渐变版本和默认版本之间切换: "그라데이션 버전과 기본 버전 사이 전환",
暂无请求日志: "요청 로그가 없습니다",
"账号轮转只走账号池;聚合API轮转只走聚合API;混合轮转先走账号池,账号耗尽后使用聚合API兜底。":
"계정 로테이션은 계정 풀만 사용하고, 집계 API 로테이션은 집계 API만 사용합니다. 하이브리드는 계정을 먼저 사용한 뒤 계정이 소진되면 집계 API로 폴백합니다.",
"账号轮转只走账号池;聚合API轮转只走聚合API;混合轮转(账号优先)先走账号池,账号耗尽后使用聚合API兜底;混合轮转(聚合优先)先走聚合API,聚合不可用时回落账号池。":
"계정 로테이션은 계정 풀만 사용하고, 집계 API 로테이션은 집계 API만 사용합니다. 하이브리드(계정 우선)는 계정을 먼저 사용한 뒤 계정이 소진되면 집계 API로 폴백하고, 하이브리드(집계 API 우선)는 집계 API를 먼저 사용한 뒤 사용할 수 없을 때 계정 풀로 폴백합니다.",
"仅对账号轮转和混合轮转生效,可限制这把平台密钥只从指定账号计划类型中选路由账号。":
"계정 로테이션과 하이브리드 로테이션에만 적용되며, 이 API 키가 지정한 계정 플랜 유형에서만 라우팅 계정을 선택하도록 제한합니다.",
账号选路策略: "계정 라우팅 전략",
Expand Down
5 changes: 3 additions & 2 deletions apps/src/lib/i18n/messages/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ export const RU_MESSAGES: MessageCatalog = {
账号轮转: "Ротация аккаунтов",
聚合API轮转: "Ротация агрегированных API",
"混合轮转(账号优先)": "Гибридная ротация (сначала аккаунты)",
"混合轮转(聚合优先)": "Гибридная ротация (сначала агрегированные API)",
协议: "Протокол",
轮转策略: "Стратегия ротации",
绑定模型: "Связанная модель",
Expand Down Expand Up @@ -1146,8 +1147,8 @@ export const RU_MESSAGES: MessageCatalog = {
在渐变版本和默认版本之间切换:
"Переключение между градиентной и стандартной версией",
暂无请求日志: "Нет логов запросов",
"账号轮转只走账号池;聚合API轮转只走聚合API;混合轮转先走账号池,账号耗尽后使用聚合API兜底。":
"Ротация аккаунтов использует только пул аккаунтов; ротация API использует только API; гибрид сначала пробует аккаунты и переключается на API при исчерпании аккаунтов.",
"账号轮转只走账号池;聚合API轮转只走聚合API;混合轮转(账号优先)先走账号池,账号耗尽后使用聚合API兜底;混合轮转(聚合优先)先走聚合API,聚合不可用时回落账号池。":
"Ротация аккаунтов использует только пул аккаунтов; ротация агрегированных API использует только API; гибридная ротация (сначала аккаунты) сначала пробует аккаунты и переключается на API при исчерпании аккаунтов; гибридная ротация (сначала API) сначала пробует API и переключается на пул аккаунтов при их недоступности.",
"仅对账号轮转和混合轮转生效,可限制这把平台密钥只从指定账号计划类型中选路由账号。":
"Применяется только к ротации аккаунтов и гибридной ротации и ограничивает маршрутизацию этим API-ключом аккаунтами выбранного типа плана.",
账号选路策略: "Стратегия маршрутизации аккаунтов",
Expand Down
2 changes: 2 additions & 0 deletions crates/service/src/apikey/apikey_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,15 @@ pub(crate) fn create_api_key(
};
let account_plan_filter = if rotation_strategy == crate::apikey_profile::ROTATION_ACCOUNT
|| rotation_strategy == crate::apikey_profile::ROTATION_HYBRID
|| rotation_strategy == crate::apikey_profile::ROTATION_HYBRID_AGGREGATE_FIRST
{
crate::account_plan::normalize_account_plan_filter(account_plan_filter)?
} else {
None
};
let account_group_filter = if rotation_strategy == crate::apikey_profile::ROTATION_ACCOUNT
|| rotation_strategy == crate::apikey_profile::ROTATION_HYBRID
|| rotation_strategy == crate::apikey_profile::ROTATION_HYBRID_AGGREGATE_FIRST
{
crate::account_group::normalize_account_group_filter(account_group_filter)
} else {
Expand Down
7 changes: 7 additions & 0 deletions crates/service/src/apikey/apikey_profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub(crate) const AUTH_X_API_KEY: &str = "x_api_key";
pub(crate) const ROTATION_ACCOUNT: &str = "account_rotation";
pub(crate) const ROTATION_AGGREGATE_API: &str = "aggregate_api_rotation";
pub(crate) const ROTATION_HYBRID: &str = "hybrid_rotation";
pub(crate) const ROTATION_HYBRID_AGGREGATE_FIRST: &str = "hybrid_aggregate_first_rotation";

/// 函数 `normalize_key`
///
Expand Down Expand Up @@ -174,6 +175,12 @@ pub(crate) fn normalize_rotation_strategy(value: Option<String>) -> Result<Strin
| "mixed_rotation"
| "混合轮转"
| "账号优先聚合兜底" => Ok(ROTATION_HYBRID.to_string()),
"hybrid_aggregate_first"
| "hybrid_aggregate_first_rotation"
| "mixed_aggregate_first"
| "aggregate_first"
| "聚合优先"
| "聚合优先账号兜底" => Ok(ROTATION_HYBRID_AGGREGATE_FIRST.to_string()),
other => Err(format!("unsupported rotation strategy: {other}")),
},
None => Ok(ROTATION_ACCOUNT.to_string()),
Expand Down
18 changes: 18 additions & 0 deletions crates/service/src/apikey/apikey_profile_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use super::{
is_gemini_generate_content_request_path, normalize_protocol_type, normalize_rotation_strategy,
resolve_gateway_protocol_type, PROTOCOL_ANTHROPIC_NATIVE, PROTOCOL_GEMINI_NATIVE,
PROTOCOL_OPENAI_COMPAT, ROTATION_ACCOUNT, ROTATION_AGGREGATE_API, ROTATION_HYBRID,
ROTATION_HYBRID_AGGREGATE_FIRST,
};

#[test]
Expand All @@ -22,6 +23,23 @@ fn normalize_rotation_strategy_accepts_hybrid_aliases() {
}
}

#[test]
fn normalize_rotation_strategy_accepts_hybrid_aggregate_first_aliases() {
for value in [
"hybrid_aggregate_first_rotation",
"hybrid_aggregate_first",
"mixed_aggregate_first",
"aggregate_first",
"聚合优先",
"聚合优先账号兜底",
] {
assert_eq!(
normalize_rotation_strategy(Some(value.to_string())).as_deref(),
Ok(ROTATION_HYBRID_AGGREGATE_FIRST)
);
}
}

#[test]
fn normalize_rotation_strategy_keeps_existing_values() {
assert_eq!(
Expand Down
3 changes: 3 additions & 0 deletions crates/service/src/apikey/apikey_update_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ pub(crate) fn update_api_key_model(
let normalized_account_plan_filter = if normalized_rotation_strategy
== crate::apikey_profile::ROTATION_ACCOUNT
|| normalized_rotation_strategy == crate::apikey_profile::ROTATION_HYBRID
|| normalized_rotation_strategy
== crate::apikey_profile::ROTATION_HYBRID_AGGREGATE_FIRST
{
crate::account_plan::normalize_account_plan_filter(account_plan_filter)?
} else {
Expand Down Expand Up @@ -105,6 +107,7 @@ pub(crate) fn update_api_key_model(
Some(ROTATION_AGGREGATE_API) => Some(None),
Some(crate::apikey_profile::ROTATION_ACCOUNT)
| Some(crate::apikey_profile::ROTATION_HYBRID)
| Some(crate::apikey_profile::ROTATION_HYBRID_AGGREGATE_FIRST)
if update_account_group_filter =>
{
Some(crate::account_group::normalize_account_group_filter(
Expand Down
10 changes: 10 additions & 0 deletions crates/service/src/codex_profile_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,12 @@ fn rotation_strategy_selects_catalog_ownership() {
),
crate::codex_model_catalog::GatewayCatalogPolicy::Managed
);
assert_eq!(
crate::codex_model_catalog::gateway_catalog_policy_for_rotation_strategy(
crate::apikey_profile::ROTATION_HYBRID_AGGREGATE_FIRST
),
crate::codex_model_catalog::GatewayCatalogPolicy::Managed
);
}

#[test]
Expand All @@ -320,6 +326,10 @@ fn api_key_candidates_expose_route_and_catalog_ownership() {
(crate::apikey_profile::ROTATION_ACCOUNT, "official"),
(crate::apikey_profile::ROTATION_AGGREGATE_API, "managed"),
(crate::apikey_profile::ROTATION_HYBRID, "managed"),
(
crate::apikey_profile::ROTATION_HYBRID_AGGREGATE_FIRST,
"managed",
),
] {
let candidate = api_key_candidate(ApiKeyCodexProfileCandidate {
id: format!("key-{rotation_strategy}"),
Expand Down
1 change: 1 addition & 0 deletions crates/service/src/gateway/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,7 @@ pub(crate) fn gateway_supports_official_responses_websocket(
}
if api_key.rotation_strategy == crate::apikey_profile::ROTATION_AGGREGATE_API
|| api_key.rotation_strategy == crate::apikey_profile::ROTATION_HYBRID
|| api_key.rotation_strategy == crate::apikey_profile::ROTATION_HYBRID_AGGREGATE_FIRST
{
return false;
}
Expand Down
4 changes: 4 additions & 0 deletions crates/service/src/gateway/upstream/executor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub(super) enum GatewayUpstreamRouteKind {
AccountRotation,
AggregateApi,
HybridAccountFirst,
HybridAggregateFirst,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -47,6 +48,9 @@ pub(super) fn resolve_gateway_upstream_execution_plan(
let route_kind = match rotation_strategy {
crate::apikey_profile::ROTATION_AGGREGATE_API => GatewayUpstreamRouteKind::AggregateApi,
crate::apikey_profile::ROTATION_HYBRID => GatewayUpstreamRouteKind::HybridAccountFirst,
crate::apikey_profile::ROTATION_HYBRID_AGGREGATE_FIRST => {
GatewayUpstreamRouteKind::HybridAggregateFirst
}
_ => GatewayUpstreamRouteKind::AccountRotation,
};
GatewayUpstreamExecutionPlan {
Expand Down
7 changes: 7 additions & 0 deletions crates/service/src/gateway/upstream/executor/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ fn protocol_and_rotation_map_to_execution_plan() {
route_kind: GatewayUpstreamRouteKind::HybridAccountFirst,
}
);
assert_eq!(
resolve_gateway_upstream_execution_plan("openai_compat", "hybrid_aggregate_first_rotation"),
GatewayUpstreamExecutionPlan {
executor_kind: GatewayUpstreamExecutorKind::CodexResponses,
route_kind: GatewayUpstreamRouteKind::HybridAggregateFirst,
}
);
assert_eq!(
resolve_gateway_upstream_execution_plan("gemini_native", "aggregate_api_rotation"),
GatewayUpstreamExecutionPlan {
Expand Down
Loading