chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import CodexBarCore
|
||||
|
||||
struct ClaudeAdminAPIUsageTests {
|
||||
private func makeContext(
|
||||
apiKey: String = "sk-ant-admin-test",
|
||||
sourceMode: ProviderSourceMode = .api) -> ProviderFetchContext
|
||||
{
|
||||
let browserDetection = BrowserDetection(cacheTTL: 0)
|
||||
let env = [ClaudeAdminAPISettingsReader.adminAPIKeyEnvironmentKey: apiKey]
|
||||
return ProviderFetchContext(
|
||||
runtime: .app,
|
||||
sourceMode: sourceMode,
|
||||
includeCredits: false,
|
||||
webTimeout: 1,
|
||||
webDebugDumpHTML: false,
|
||||
verbose: false,
|
||||
env: env,
|
||||
settings: nil,
|
||||
fetcher: UsageFetcher(environment: env),
|
||||
claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
|
||||
browserDetection: browserDetection)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `prefers primary Anthropic admin key environment variable`() {
|
||||
let token = ClaudeAdminAPISettingsReader.apiKey(environment: [
|
||||
ClaudeAdminAPISettingsReader.alternateAdminAPIKeyEnvironmentKey: "sk-ant-admin-alt",
|
||||
ClaudeAdminAPISettingsReader.adminAPIKeyEnvironmentKey: "sk-ant-admin-primary",
|
||||
])
|
||||
|
||||
#expect(token == "sk-ant-admin-primary")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `routes Claude token account admin keys into admin api environment`() {
|
||||
let env = TokenAccountSupportCatalog.envOverride(for: .claude, token: "Bearer sk-ant-admin-token")
|
||||
|
||||
#expect(env?[ClaudeAdminAPISettingsReader.adminAPIKeyEnvironmentKey] == "sk-ant-admin-token")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `auto source uses configured admin api key`() async {
|
||||
let descriptor = ProviderDescriptorRegistry.descriptor(for: .claude)
|
||||
let context = self.makeContext(sourceMode: .auto)
|
||||
let strategies = await descriptor.fetchPlan.pipeline.resolveStrategies(context)
|
||||
|
||||
#expect(strategies.map(\.id) == ["claude.admin-api"])
|
||||
}
|
||||
|
||||
@Test
|
||||
func `parses Anthropic admin cost and messages usage into daily summaries`() throws {
|
||||
let now = Date(timeIntervalSince1970: 1_700_179_200)
|
||||
let costs = """
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"starting_at": "2023-11-14T00:00:00Z",
|
||||
"ending_at": "2023-11-15T00:00:00Z",
|
||||
"results": [
|
||||
{
|
||||
"currency": "USD",
|
||||
"amount": "12345.00",
|
||||
"description": "Claude Sonnet 4 Usage - Input Tokens",
|
||||
"cost_type": "tokens"
|
||||
},
|
||||
{
|
||||
"currency": "USD",
|
||||
"amount": "2500.00",
|
||||
"description": "Web Search Usage",
|
||||
"cost_type": "web_search"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"starting_at": "2023-11-15T00:00:00Z",
|
||||
"ending_at": "2023-11-16T00:00:00Z",
|
||||
"results": [
|
||||
{
|
||||
"currency": "USD",
|
||||
"amount": "5000",
|
||||
"description": "Claude Haiku Usage - Output Tokens",
|
||||
"cost_type": "tokens"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"next_page": null
|
||||
}
|
||||
"""
|
||||
let messages = """
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"starting_at": "2023-11-14T00:00:00Z",
|
||||
"ending_at": "2023-11-15T00:00:00Z",
|
||||
"results": [
|
||||
{
|
||||
"uncached_input_tokens": 1500,
|
||||
"cache_creation": {
|
||||
"ephemeral_1h_input_tokens": 1000,
|
||||
"ephemeral_5m_input_tokens": 500
|
||||
},
|
||||
"cache_read_input_tokens": 200,
|
||||
"output_tokens": 500,
|
||||
"model": "claude-sonnet-4-20250514"
|
||||
},
|
||||
{
|
||||
"uncached_input_tokens": 100,
|
||||
"output_tokens": 50,
|
||||
"model": "claude-opus-4-20250514"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"starting_at": "2023-11-15T00:00:00Z",
|
||||
"ending_at": "2023-11-16T00:00:00Z",
|
||||
"results": [
|
||||
{
|
||||
"uncached_input_tokens": 200,
|
||||
"cache_read_input_tokens": 300,
|
||||
"output_tokens": 100,
|
||||
"model": "claude-sonnet-4-20250514"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"next_page": null
|
||||
}
|
||||
"""
|
||||
|
||||
let snapshot = try ClaudeAdminAPIUsageFetcher._parseSnapshotForTesting(
|
||||
costs: Data(costs.utf8),
|
||||
messages: Data(messages.utf8),
|
||||
now: now)
|
||||
|
||||
#expect(snapshot.daily.count == 2)
|
||||
#expect(snapshot.daily[0].costUSD == 148.45)
|
||||
#expect(snapshot.daily[0].inputTokens == 1600)
|
||||
#expect(snapshot.daily[0].cacheCreationInputTokens == 1500)
|
||||
#expect(snapshot.daily[0].cacheReadInputTokens == 200)
|
||||
#expect(snapshot.daily[0].outputTokens == 550)
|
||||
#expect(snapshot.daily[0].totalTokens == 3850)
|
||||
#expect(snapshot.last30Days.costUSD == 198.45)
|
||||
#expect(snapshot.last30Days.totalTokens == 4450)
|
||||
#expect(snapshot.topModels.first?.name == "claude-sonnet-4-20250514")
|
||||
#expect(snapshot.topModels.first?.totalTokens == 4300)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `maps Anthropic admin usage to Claude usage snapshot`() {
|
||||
let now = Date(timeIntervalSince1970: 1_700_179_200)
|
||||
let apiUsage = ClaudeAdminAPIUsageSnapshot(
|
||||
daily: [
|
||||
ClaudeAdminAPIUsageSnapshot.DailyBucket(
|
||||
day: "2023-11-14",
|
||||
startTime: now,
|
||||
endTime: now.addingTimeInterval(86400),
|
||||
costUSD: 8.5,
|
||||
inputTokens: 1000,
|
||||
cacheCreationInputTokens: 400,
|
||||
cacheReadInputTokens: 300,
|
||||
outputTokens: 250,
|
||||
totalTokens: 1950,
|
||||
costItems: [],
|
||||
models: []),
|
||||
],
|
||||
updatedAt: now)
|
||||
|
||||
let usage = apiUsage.toUsageSnapshot()
|
||||
|
||||
#expect(usage.primary == nil)
|
||||
#expect(usage.providerCost?.used == 8.5)
|
||||
#expect(usage.providerCost?.limit == 0)
|
||||
#expect(usage.providerCost?.period == "Last 30 days")
|
||||
#expect(usage.claudeAdminAPIUsage?.last30Days.totalTokens == 1950)
|
||||
#expect(usage.identity?.providerID == .claude)
|
||||
#expect(usage.identity?.loginMethod == "Admin API")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `current day summary is zero when Claude admin history is stale`() throws {
|
||||
let now = try Self.localNoon(year: 2023, month: 11, day: 17)
|
||||
let bucketDay = try Self.localNoon(year: 2023, month: 11, day: 14)
|
||||
let apiUsage = ClaudeAdminAPIUsageSnapshot(
|
||||
daily: [
|
||||
ClaudeAdminAPIUsageSnapshot.DailyBucket(
|
||||
day: "2023-11-14",
|
||||
startTime: bucketDay,
|
||||
endTime: bucketDay.addingTimeInterval(86400),
|
||||
costUSD: 8.5,
|
||||
inputTokens: 1000,
|
||||
cacheCreationInputTokens: 400,
|
||||
cacheReadInputTokens: 300,
|
||||
outputTokens: 250,
|
||||
totalTokens: 1950,
|
||||
costItems: [],
|
||||
models: []),
|
||||
],
|
||||
updatedAt: now)
|
||||
|
||||
#expect(apiUsage.currentDay.costUSD == 0)
|
||||
#expect(apiUsage.currentDay.totalTokens == 0)
|
||||
#expect(apiUsage.latestDay.costUSD == 8.5)
|
||||
#expect(apiUsage.latestDay.totalTokens == 1950)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `fetch strategy reports admin api source label`() async throws {
|
||||
let strategy = ClaudeAdminAPIFetchStrategy(usageFetcher: { apiKey in
|
||||
#expect(apiKey == "sk-ant-admin-test")
|
||||
return ClaudeAdminAPIUsageSnapshot(daily: [], updatedAt: Date(timeIntervalSince1970: 1_700_000_000))
|
||||
})
|
||||
|
||||
let result = try await strategy.fetch(self.makeContext())
|
||||
|
||||
#expect(result.sourceLabel == "admin-api")
|
||||
#expect(result.usage.identity?.loginMethod == "Admin API")
|
||||
}
|
||||
|
||||
private static func localNoon(year: Int, month: Int, day: Int) throws -> Date {
|
||||
try #require(Calendar.current.date(from: DateComponents(year: year, month: month, day: day, hour: 12)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user