mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 02:47:45 +08:00
Merge remote-tracking branch 'origin/main' into codex/usage-pending-reasoning-reset-expiry-20260712
# Conflicts: # crates/aether-ai-formats/src/formats/openai/responses/mod.rs # crates/aether-usage/runtime/src/runtime.rs # frontend/src/features/usage/components/UsageRecordsTable.vue # frontend/src/features/usage/components/__tests__/UsageRecordsTable.spec.ts
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { parseProviderKeyBatchImport } from '../providerKeyBatchImport'
|
||||
|
||||
describe('provider key batch import parser', () => {
|
||||
it('parses required names and keys separated by four hyphens', () => {
|
||||
const result = parseProviderKeyBatchImport([
|
||||
'primary----sk-primary',
|
||||
'backup----sk-backup',
|
||||
'night----sk-night----suffix',
|
||||
'# ignored comment',
|
||||
].join('\n'))
|
||||
|
||||
expect(result.errors).toEqual([])
|
||||
expect(result.items).toEqual([
|
||||
{ lineNumber: 1, name: 'primary', apiKey: 'sk-primary' },
|
||||
{ lineNumber: 2, name: 'backup', apiKey: 'sk-backup' },
|
||||
{ lineNumber: 3, name: 'night', apiKey: 'sk-night----suffix' },
|
||||
])
|
||||
})
|
||||
|
||||
it('reports invalid format, missing fields and duplicates', () => {
|
||||
const result = parseProviderKeyBatchImport([
|
||||
'one----sk-1',
|
||||
'two----sk-1',
|
||||
'one----sk-2',
|
||||
'----sk-3',
|
||||
'three----',
|
||||
'sk-without-name',
|
||||
].join('\n'))
|
||||
|
||||
expect(result.items).toHaveLength(1)
|
||||
expect(result.errors.map(error => error.message)).toEqual([
|
||||
'Key 与前面行重复',
|
||||
'名称与前面行重复',
|
||||
'名称不能为空',
|
||||
'Key 不能为空',
|
||||
'格式应为 名称----Key',
|
||||
])
|
||||
})
|
||||
|
||||
it('does not impose a client-side item limit', () => {
|
||||
const input = Array.from({ length: 750 }, (_, index) => `key-${index}----sk-${index}`).join('\n')
|
||||
const result = parseProviderKeyBatchImport(input)
|
||||
|
||||
expect(result.items).toHaveLength(750)
|
||||
expect(result.errors).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,52 +0,0 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
AUTO_QUOTA_REFRESH_COOLDOWN_SECONDS,
|
||||
isProviderQuotaAutoRefreshCoolingDown,
|
||||
markProviderQuotaAutoRefreshAttempt,
|
||||
resetProviderQuotaAutoRefreshCooldownForTests,
|
||||
} from '../quotaAutoRefreshCooldown'
|
||||
|
||||
describe('quota auto refresh cooldown', () => {
|
||||
afterEach(() => {
|
||||
resetProviderQuotaAutoRefreshCooldownForTests()
|
||||
})
|
||||
|
||||
it('starts cooldown after recording an auto refresh attempt', () => {
|
||||
markProviderQuotaAutoRefreshAttempt('provider-1', 1_000)
|
||||
|
||||
expect(isProviderQuotaAutoRefreshCoolingDown('provider-1', 1_000)).toBe(true)
|
||||
expect(
|
||||
isProviderQuotaAutoRefreshCoolingDown(
|
||||
'provider-1',
|
||||
1_000 + AUTO_QUOTA_REFRESH_COOLDOWN_SECONDS - 1,
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('expires cooldown after the configured window', () => {
|
||||
markProviderQuotaAutoRefreshAttempt('provider-1', 1_000)
|
||||
|
||||
expect(
|
||||
isProviderQuotaAutoRefreshCoolingDown(
|
||||
'provider-1',
|
||||
1_000 + AUTO_QUOTA_REFRESH_COOLDOWN_SECONDS,
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('tracks cooldown independently per provider', () => {
|
||||
markProviderQuotaAutoRefreshAttempt('provider-1', 1_000)
|
||||
markProviderQuotaAutoRefreshAttempt('provider-2', 1_200)
|
||||
|
||||
expect(isProviderQuotaAutoRefreshCoolingDown('provider-1', 1_301)).toBe(false)
|
||||
expect(isProviderQuotaAutoRefreshCoolingDown('provider-2', 1_301)).toBe(true)
|
||||
})
|
||||
|
||||
it('ignores empty provider ids', () => {
|
||||
markProviderQuotaAutoRefreshAttempt('', 1_000)
|
||||
|
||||
expect(isProviderQuotaAutoRefreshCoolingDown('', 1_001)).toBe(false)
|
||||
expect(isProviderQuotaAutoRefreshCoolingDown(null, 1_001)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
export interface ProviderKeyBatchImportItem {
|
||||
lineNumber: number
|
||||
name: string
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
export interface ProviderKeyBatchImportError {
|
||||
lineNumber: number | null
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ProviderKeyBatchImportParseResult {
|
||||
items: ProviderKeyBatchImportItem[]
|
||||
errors: ProviderKeyBatchImportError[]
|
||||
}
|
||||
|
||||
export const PROVIDER_KEY_BATCH_SEPARATOR = '----'
|
||||
|
||||
function splitNamedKey(line: string): { name: string; apiKey: string } | null {
|
||||
const separatorIndex = line.indexOf(PROVIDER_KEY_BATCH_SEPARATOR)
|
||||
if (separatorIndex < 0) return null
|
||||
return {
|
||||
name: line.slice(0, separatorIndex),
|
||||
apiKey: line.slice(separatorIndex + PROVIDER_KEY_BATCH_SEPARATOR.length),
|
||||
}
|
||||
}
|
||||
|
||||
export function parseProviderKeyBatchImport(input: string): ProviderKeyBatchImportParseResult {
|
||||
const items: ProviderKeyBatchImportItem[] = []
|
||||
const errors: ProviderKeyBatchImportError[] = []
|
||||
const seenKeys = new Set<string>()
|
||||
const seenNames = new Set<string>()
|
||||
|
||||
for (const [index, rawLine] of input.split(/\r?\n/).entries()) {
|
||||
const lineNumber = index + 1
|
||||
const line = rawLine.trim()
|
||||
if (!line || line.startsWith('#')) continue
|
||||
|
||||
const named = splitNamedKey(line)
|
||||
if (!named) {
|
||||
errors.push({ lineNumber, message: `格式应为 名称${PROVIDER_KEY_BATCH_SEPARATOR}Key` })
|
||||
continue
|
||||
}
|
||||
const name = named.name.trim()
|
||||
const apiKey = named.apiKey.trim()
|
||||
if (!name) {
|
||||
errors.push({ lineNumber, message: '名称不能为空' })
|
||||
continue
|
||||
}
|
||||
if (!apiKey) {
|
||||
errors.push({ lineNumber, message: 'Key 不能为空' })
|
||||
continue
|
||||
}
|
||||
if (name.length > 100) {
|
||||
errors.push({ lineNumber, message: '名称不能超过 100 个字符' })
|
||||
continue
|
||||
}
|
||||
if (seenKeys.has(apiKey)) {
|
||||
errors.push({ lineNumber, message: 'Key 与前面行重复' })
|
||||
continue
|
||||
}
|
||||
if (seenNames.has(name)) {
|
||||
errors.push({ lineNumber, message: '名称与前面行重复' })
|
||||
continue
|
||||
}
|
||||
|
||||
seenKeys.add(apiKey)
|
||||
seenNames.add(name)
|
||||
items.push({ lineNumber, name, apiKey })
|
||||
}
|
||||
|
||||
return { items, errors }
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
const AUTO_QUOTA_REFRESH_COOLDOWN_SECONDS = 5 * 60
|
||||
|
||||
const lastAutoQuotaRefreshAttemptAtByProvider = new Map<string, number>()
|
||||
|
||||
function normalizeUnixSeconds(value: number): number {
|
||||
return Math.max(Math.floor(value), 0)
|
||||
}
|
||||
|
||||
export function isProviderQuotaAutoRefreshCoolingDown(
|
||||
providerId: string | null | undefined,
|
||||
nowSeconds = Math.floor(Date.now() / 1000),
|
||||
): boolean {
|
||||
const id = String(providerId || '').trim()
|
||||
if (!id) return false
|
||||
|
||||
const lastAttemptAt = lastAutoQuotaRefreshAttemptAtByProvider.get(id)
|
||||
if (lastAttemptAt == null) return false
|
||||
|
||||
return normalizeUnixSeconds(nowSeconds) - lastAttemptAt < AUTO_QUOTA_REFRESH_COOLDOWN_SECONDS
|
||||
}
|
||||
|
||||
export function markProviderQuotaAutoRefreshAttempt(
|
||||
providerId: string | null | undefined,
|
||||
nowSeconds = Math.floor(Date.now() / 1000),
|
||||
): void {
|
||||
const id = String(providerId || '').trim()
|
||||
if (!id) return
|
||||
|
||||
lastAutoQuotaRefreshAttemptAtByProvider.set(id, normalizeUnixSeconds(nowSeconds))
|
||||
}
|
||||
|
||||
export function resetProviderQuotaAutoRefreshCooldownForTests(): void {
|
||||
lastAutoQuotaRefreshAttemptAtByProvider.clear()
|
||||
}
|
||||
|
||||
export { AUTO_QUOTA_REFRESH_COOLDOWN_SECONDS }
|
||||
Reference in New Issue
Block a user