mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-07 00:05:49 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
776637fccb | ||
|
|
82acb08d26 | ||
|
|
7744e1b0b4 | ||
|
|
0b8a1add18 |
@@ -608,8 +608,9 @@ pub struct ProviderQuota {
|
||||
impl ProviderQuota {
|
||||
/// 返回账号展开面板使用的当前额度窗口用量。
|
||||
///
|
||||
/// 只选择具备完整时间边界且可按账号归属的窗口;优先级与 Dashboard
|
||||
/// 代表性额度窗口一致,同一优先级保持 Provider 投影顺序。
|
||||
/// Provider 已提供账号级本地用量时,固定窗口和无固定重置点的滚动窗口
|
||||
/// 都可作为代表用量;优先级与 Dashboard 代表性额度窗口一致,
|
||||
/// 同一优先级保持 Provider 投影顺序。
|
||||
#[must_use]
|
||||
pub fn representative_window_usage(&self) -> Option<&AccountUsage> {
|
||||
self.windows
|
||||
@@ -618,7 +619,6 @@ impl ProviderQuota {
|
||||
.filter(|(_, window)| {
|
||||
window.local_usage_attribution == QuotaLocalUsageAttribution::AccountWide
|
||||
&& window.window_seconds.is_some()
|
||||
&& window.reset_at.is_some()
|
||||
&& window.local_usage.is_some()
|
||||
})
|
||||
.min_by_key(|(index, window)| (quota_usage_priority(window), *index))
|
||||
|
||||
@@ -6,6 +6,7 @@ use gateway_core::engine::credential::OpaqueProviderData;
|
||||
use gateway_admin::{
|
||||
model::{
|
||||
PageSize, Revision,
|
||||
accounts::AccountUsage,
|
||||
auth::LoginCommand,
|
||||
client_keys::ClientKeyPageSize,
|
||||
observability::{DecimalAmount, RequestOutcome, TimeRange},
|
||||
@@ -135,6 +136,51 @@ fn representative_quota_should_prefer_short_window_and_highest_usage() {
|
||||
assert_eq!(quota.representative_used_percent(), Some(45.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn representative_window_usage_should_accept_account_wide_rolling_window_without_reset() {
|
||||
let usage = AccountUsage {
|
||||
account_id: "acct_xai".to_owned(),
|
||||
request_count: 5,
|
||||
success_count: 5,
|
||||
input_tokens: Some(100),
|
||||
output_tokens: Some(20),
|
||||
cached_tokens: Some(80),
|
||||
cache_write_tokens: Some(0),
|
||||
reasoning_tokens: Some(0),
|
||||
image_input_tokens: Some(0),
|
||||
image_output_tokens: Some(0),
|
||||
image_request_count: 0,
|
||||
image_request_failed_count: 0,
|
||||
total_tokens: Some(120),
|
||||
cost_coverage: Default::default(),
|
||||
costs: Vec::new(),
|
||||
last_used_at: Some(Utc::now()),
|
||||
request_buckets: Vec::new(),
|
||||
models: Vec::new(),
|
||||
};
|
||||
let quota = ProviderQuota {
|
||||
observed_at: None,
|
||||
refresh_token_expires_at: None,
|
||||
windows: vec![ProviderQuotaWindow {
|
||||
key: "free-rolling-24h".to_owned(),
|
||||
group: "shortTerm".to_owned(),
|
||||
label: "日限额".to_owned(),
|
||||
source: None,
|
||||
local_usage_attribution: QuotaLocalUsageAttribution::AccountWide,
|
||||
window_seconds: Some(86_400),
|
||||
used_percent: None,
|
||||
reset_at: None,
|
||||
limit_reached: false,
|
||||
local_usage: Some(usage.clone()),
|
||||
provider_data: None,
|
||||
}],
|
||||
limit_reached: false,
|
||||
provider_data: None,
|
||||
};
|
||||
|
||||
assert_eq!(quota.representative_window_usage(), Some(&usage));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exhausted_quota_should_project_full_usage_to_only_the_representative_window() {
|
||||
let mut quota = ProviderQuota {
|
||||
|
||||
@@ -116,18 +116,13 @@ const modalTitle = computed(() => {
|
||||
const oauthPanelTitle = computed(() => {
|
||||
if (props.reauthorizing)
|
||||
return accountName.value
|
||||
return isXai.value ? '使用 xAI OAuth 完成账号接入' : '使用 OpenAI OAuth 完成账号接入'
|
||||
return isXai.value ? 'xAI OAuth 授权' : 'OpenAI OAuth 授权'
|
||||
})
|
||||
|
||||
const oauthPanelDescription = computed(() => {
|
||||
if (props.reauthorizing) {
|
||||
return isXai.value
|
||||
? '生成新的授权链接,完成后粘贴回调地址、含 code 和 state 的查询字符串或授权码更新账号凭据'
|
||||
: '生成新的授权链接,完成后粘贴回调地址更新账号凭据'
|
||||
}
|
||||
return isXai.value
|
||||
? '复制授权链接到浏览器打开,完成后粘贴回调地址、含 code 和 state 的查询字符串或授权码即可导入'
|
||||
: '复制授权链接到浏览器打开,完成后把回调地址粘贴到下方即可导入'
|
||||
? '生成并打开授权链接 → 完成浏览器授权 → 粘贴回调地址、查询字符串或授权码'
|
||||
: '生成并打开授权链接 → 完成浏览器授权 → 粘贴回调地址'
|
||||
})
|
||||
|
||||
const canGenerateOauth = computed(() =>
|
||||
@@ -152,21 +147,21 @@ const description = computed<string | undefined>(() => {
|
||||
return undefined
|
||||
if (props.reauthorizing) {
|
||||
return isXai.value
|
||||
? '完成授权后粘贴回调地址、含 code 和 state 的查询字符串或授权码,系统会更新账号凭据'
|
||||
: '完成授权后粘贴回调地址,系统会更新账号凭据'
|
||||
? '完成新的 xAI 授权并替换此账号凭据'
|
||||
: '完成新的 OpenAI 授权并替换此账号凭据'
|
||||
}
|
||||
if (isBatch.value)
|
||||
return '导入 CPR 多平台账号包,系统会按文档中的平台自动分流'
|
||||
return '粘贴或上传 CPR 账号包,一次导入多个平台账号'
|
||||
if (isXai.value) {
|
||||
return mode.value === 'oauth'
|
||||
? '复制 xAI 授权链接,完成后粘贴回调地址、含 code 和 state 的查询字符串或授权码,不使用 xAI API Key'
|
||||
: '导入 xAI 账号文件,已存在账号会更新'
|
||||
? '通过浏览器授权导入 xAI 账号'
|
||||
: '粘贴或上传 xAI 账号文件,匹配已有账号时更新凭据'
|
||||
}
|
||||
if (mode.value === 'oauth')
|
||||
return '复制 OpenAI 授权链接,完成后粘贴回调地址,系统会自动写入或更新账号'
|
||||
return '通过浏览器授权导入 OpenAI 账号'
|
||||
if (mode.value === 'agent_identity')
|
||||
return '导入 Agent 身份文件,系统会按身份信息写入或更新账号'
|
||||
return '导入 CPR、Sub2API 或 CPA 账号文件,已存在账号会更新'
|
||||
return '粘贴或上传 Agent 身份文件,匹配已有账号时更新凭据'
|
||||
return '粘贴或上传 CPR、Sub2API 或 CPA 账号文件,匹配已有账号时更新凭据'
|
||||
})
|
||||
|
||||
function selectProvider(value: 'openai' | 'xai' | 'batch') {
|
||||
@@ -288,7 +283,7 @@ async function copyText(value: string, successText: string) {
|
||||
v-model="oauthCallback"
|
||||
:aria-label="isXai ? '回调地址或授权码' : '回调地址'"
|
||||
size="sm"
|
||||
:placeholder="isXai ? '粘贴完整回调地址、?code=...&state=... 或裸授权码' : 'http://localhost:1455/auth/callback?code=...&state=...'"
|
||||
:placeholder="isXai ? '回调地址、?code=...&state=... 或授权码' : 'http://localhost:1455/auth/callback?code=...&state=...'"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</BaseFormItem>
|
||||
|
||||
@@ -103,7 +103,7 @@ export function useAccountConnectionTest(options: { reload: () => Promise<unknow
|
||||
if (connectionTestStatus.value === 'running') {
|
||||
return {
|
||||
label: '正在测试',
|
||||
description: '正在发送一条真实 Responses 流式请求',
|
||||
description: '正在向所选模型发送请求并接收流式响应',
|
||||
icon: Clock3,
|
||||
badge: 'bg-(--cp-info-bg) text-(--cp-info-text)',
|
||||
iconClass: 'text-(--cp-info)',
|
||||
@@ -112,7 +112,7 @@ export function useAccountConnectionTest(options: { reload: () => Promise<unknow
|
||||
if (connectionTestStatus.value === 'success') {
|
||||
return {
|
||||
label: '连接正常',
|
||||
description: '账号凭据可用,已完成 Responses 流式验证',
|
||||
description: '请求已完成,可在下方查看模型、耗时和事件轨迹',
|
||||
icon: CheckCircle2,
|
||||
badge: 'bg-(--cp-success-bg) text-(--cp-success-text)',
|
||||
iconClass: 'text-(--cp-success)',
|
||||
@@ -121,7 +121,7 @@ export function useAccountConnectionTest(options: { reload: () => Promise<unknow
|
||||
if (connectionTestStatus.value === 'error') {
|
||||
return {
|
||||
label: '测试失败',
|
||||
description: '真实请求未完成,优先检查令牌状态、账号权限或上游网络',
|
||||
description: '请求未完成,请在下方查看上游错误与事件轨迹',
|
||||
icon: XCircle,
|
||||
badge: 'bg-(--cp-danger-bg) text-(--cp-danger-text)',
|
||||
iconClass: 'text-(--cp-danger)',
|
||||
@@ -129,7 +129,7 @@ export function useAccountConnectionTest(options: { reload: () => Promise<unknow
|
||||
}
|
||||
return {
|
||||
label: '准备测试',
|
||||
description: '点击开始后发送一条轻量 Responses 流式请求',
|
||||
description: '选择模型后,点击“开始测试”发送真实请求',
|
||||
icon: Wifi,
|
||||
badge: 'bg-(--cp-bg-subtle) text-(--cp-text-secondary)',
|
||||
iconClass: 'text-(--cp-text-muted)',
|
||||
|
||||
@@ -189,11 +189,11 @@ export function useAccountOnboarding(options: {
|
||||
}
|
||||
|
||||
if (importedCount === 0) {
|
||||
throw new Error(failures.length > 0 ? `批量导入失败:${failures.join(';')}` : '批量文件没有可导入的账号')
|
||||
throw new Error(failures.length > 0 ? `批量导入失败:${failures.join('、')}` : '批量文件没有可导入的账号')
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
return `已导入 ${importedCount} 个账号,${failures.length} 个文档失败:${failures.join(';')}`
|
||||
return `已导入 ${importedCount} 个账号,${failures.length} 个文档失败:${failures.join('、')}`
|
||||
}
|
||||
return `已导入 ${importedCount} 个账号`
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export function parseAccountSchedulingForm(
|
||||
|| parsedLimit < 1
|
||||
|| parsedLimit > MAX_ACCOUNT_CONCURRENCY)
|
||||
) {
|
||||
return { valid: false, message: `并发限制必须留空或填写 1 到 ${MAX_ACCOUNT_CONCURRENCY} 的整数` }
|
||||
return { valid: false, message: `并发限制必须留空,或为 1 到 ${MAX_ACCOUNT_CONCURRENCY} 的整数` }
|
||||
}
|
||||
|
||||
const parsedWeight = Number(weight.trim())
|
||||
|
||||
@@ -27,16 +27,13 @@ const open = defineModel<boolean>({ default: false })
|
||||
const createdOpen = defineModel<boolean>('createdOpen', { default: false })
|
||||
const form = defineModel<ApiKeyFormValue>('form', { required: true })
|
||||
const title = computed(() => props.editing ? '编辑 API Key' : '创建 API Key')
|
||||
const description = computed(() => props.editing
|
||||
? '调整访问范围与调度限制,分组替换原子生效。'
|
||||
: '为调用方选择一个或多个账号分组。')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseModal
|
||||
v-model="open"
|
||||
:title="title"
|
||||
:description="description"
|
||||
description="设置调用方可用的账号分组、并发和每分钟请求上限"
|
||||
variant="info"
|
||||
width="680px"
|
||||
body-max-height="min(680px, calc(100dvh - 190px))"
|
||||
@@ -47,7 +44,7 @@ const description = computed(() => props.editing
|
||||
<BaseInput
|
||||
v-model="form.name"
|
||||
aria-label="名称"
|
||||
placeholder="例如:生产环境、测试调用方..."
|
||||
placeholder="例如:生产环境"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</BaseFormItem>
|
||||
@@ -56,7 +53,7 @@ const description = computed(() => props.editing
|
||||
<BaseInput
|
||||
v-model="form.label"
|
||||
aria-label="标签(可选)"
|
||||
placeholder="备注信息..."
|
||||
placeholder="例如:后端服务"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</BaseFormItem>
|
||||
@@ -108,7 +105,7 @@ const description = computed(() => props.editing
|
||||
<BaseModal
|
||||
v-model="createdOpen"
|
||||
title="API Key 已创建"
|
||||
description="可以立即复制或导入 CCSwitch,之后也可从密钥列表再次操作"
|
||||
description="复制密钥,或直接导入 CCSwitch"
|
||||
variant="success"
|
||||
width="540px"
|
||||
>
|
||||
|
||||
@@ -38,13 +38,13 @@ const authPath = computed(() =>
|
||||
activePlatform.value === 'windows' ? '%userprofile%\\.codex\\auth.json' : '~/.codex/auth.json',
|
||||
)
|
||||
const codexAuthJson = computed(() => JSON.stringify({ OPENAI_API_KEY: keyValue.value }, null, 2))
|
||||
const defaultModel = 'gpt-5.5'
|
||||
const defaultModel = 'gpt-5.6-terra'
|
||||
|
||||
const codexConfigToml = computed(
|
||||
() => `model_provider = "OpenAI"
|
||||
model = "${defaultModel}"
|
||||
review_model = "${defaultModel}"
|
||||
model_reasoning_effort = "xhigh"
|
||||
model_reasoning_effort = "max"
|
||||
service_tier = "default"
|
||||
disable_response_storage = true
|
||||
network_access = "enabled"
|
||||
@@ -70,7 +70,7 @@ const visibleFiles = computed(() => [
|
||||
<BaseModal
|
||||
v-model="open"
|
||||
title="使用密钥"
|
||||
description="将以下配置文件添加到 Codex CLI 配置目录中"
|
||||
description="将下方内容分别保存到显示的 Codex CLI 配置文件"
|
||||
width="760px"
|
||||
>
|
||||
<div class="flex flex-col gap-5">
|
||||
|
||||
@@ -139,7 +139,6 @@ function diagnosticNameDisplay(name: string) {
|
||||
compact
|
||||
row-key="key"
|
||||
empty-text="暂无诊断数据"
|
||||
max-height="230px"
|
||||
min-width="640px"
|
||||
>
|
||||
<template #header-nameDisplay>
|
||||
|
||||
@@ -1 +1 @@
|
||||
version: "3.2.4"
|
||||
version: "3.2.5"
|
||||
|
||||
Reference in New Issue
Block a user