diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c95045586..b5bd6c6951 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - General settings: turn Low Power Mode into an Off/On/Automatic preference, with Automatic following the system Low Power Mode state (#2995). Thanks @elijahfriedman! ### Fixed +- Claude spend: price bare first-party model IDs from their models.dev vendor catalog, preserve explicit routes, and leave ambiguous cross-vendor matches unpriced (#3002). Thanks @Yuxin-Qiao! - Menu: prevent the system menu highlight from painting behind provider detail cards on macOS 27 beta (#2998). Thanks @Tan1103! - Cursor: keep an API-validated usage result when the Keychain cache is temporarily unavailable, instead of treating it as a missing or replaced session (#3000). Thanks @hxy91819! - Usage & Spend: sum priced subscriptions into a ~$ partial estimate with coverage shown when some subscriptions lack prices, instead of hiding the total (#3001). Thanks @Yuxin-Qiao! diff --git a/Sources/CodexBarCore/CodexLocalDataScope.swift b/Sources/CodexBarCore/CodexLocalDataScope.swift index b71c2b057f..32b0e4f365 100644 --- a/Sources/CodexBarCore/CodexLocalDataScope.swift +++ b/Sources/CodexBarCore/CodexLocalDataScope.swift @@ -32,6 +32,7 @@ struct CodexLocalDataScope: Sendable, Equatable { func applying(to options: CostUsageScanner.Options) -> CostUsageScanner.Options { var copy = options copy.codexSessionsRoot = self.sessionsRoot + copy.codexTraceDatabaseURL = copy.codexTraceDatabaseURL ?? self.stateDatabaseURL return copy } diff --git a/Sources/CodexBarCore/CostUsageFetcher.swift b/Sources/CodexBarCore/CostUsageFetcher.swift index b7fe5f3aef..35d50c751b 100644 --- a/Sources/CodexBarCore/CostUsageFetcher.swift +++ b/Sources/CodexBarCore/CostUsageFetcher.swift @@ -667,7 +667,11 @@ public struct CostUsageFetcher: Sendable { targets.insert(ModelsDevPricingTarget(providerID: target.providerID, modelID: target.modelID)) } } else { - targets.insert(ModelsDevPricingTarget(providerID: "anthropic", modelID: breakdown.modelName)) + for target in CostUsagePricing.claudeModelsDevPricingTargets(for: breakdown.modelName) { + targets.insert(ModelsDevPricingTarget( + providerID: target.providerID, + modelID: target.modelID)) + } } } } diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index 7d75ccf273..34028bb592 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "3c1ec2b780582978" + static let value = "50cb4b9e11791432" } diff --git a/Sources/CodexBarCore/PiSessionCostScanner.swift b/Sources/CodexBarCore/PiSessionCostScanner.swift index 72008ea6d5..eb9108531d 100644 --- a/Sources/CodexBarCore/PiSessionCostScanner.swift +++ b/Sources/CodexBarCore/PiSessionCostScanner.swift @@ -256,7 +256,8 @@ enum PiSessionCostScanner { modelsDevArtifact: modelsDevArtifact, formulaVersion: Self.costFormulaVersion, parserHash: CodexParserHash.value, - modelsDevProviderIDs: CostUsagePricing.codexModelsDevProviderIDs.union(["anthropic"]))) + modelsDevProviderIDs: CostUsagePricing.codexModelsDevProviderIDs.union( + Set(CostUsagePricing.claudeFirstPartyModelsDevProviderIDs)))) } private static func requestedWindowExpandsCache( diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift index 4ea8da01ee..4fce93e6ec 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift @@ -763,8 +763,7 @@ enum CostUsagePricing { : currentPricing, tokens: tokens) } - if let lookup = self.modelsDevLookup( - providerID: self.claudeModelsDevProviderID, + if let lookup = self.claudeModelsDevLookup( model: model, catalog: modelsDevCatalog, cacheRoot: modelsDevCacheRoot) @@ -850,3 +849,101 @@ enum CostUsagePricing { cacheRoot: cacheRoot) } } + +extension CostUsagePricing { + /// Bare Claude-routed IDs may match first-party models.dev vendors. Recognizable model families + /// stay with their vendor, while unknown bare IDs must have one unambiguous catalog match. + /// Provider-specific by design: first-party vendor routing for bare Claude model IDs. + static let claudeFirstPartyModelsDevProviderIDs: [String] = [ + Self.claudeModelsDevProviderID, + "openai", + "google", + "moonshot", + "kimi-for-coding", + "minimax", + "deepseek", + ] + + static func claudeModelsDevPricingTargets(for rawModel: String) -> [(providerID: String, modelID: String)] { + let trimmed = rawModel.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return [] } + if let slash = trimmed.firstIndex(of: "/") { + let codexTargets = self.codexModelsDevPricingTargets(for: trimmed) + if !codexTargets.isEmpty { + return codexTargets + } + + let routeID = String(trimmed[.. [String] { + let normalized = self.normalizeClaudeModel(rawModel) + return normalized == rawModel ? [rawModel] : [rawModel, normalized] + } + + private static func claudeFirstPartyModelsDevPreferredProviderIDs(for rawModel: String) -> [String]? { + let model = self.normalizeClaudeModel(rawModel).lowercased() + if model.hasPrefix("claude-") { return [self.claudeModelsDevProviderID] } + let openAIReasoningFamily = ["o1", "o3", "o4"].contains { + model == $0 || model.hasPrefix("\($0)-") + } + if openAIReasoningFamily + || ["gpt-", "chatgpt-", "text-embedding-"].contains(where: model.hasPrefix) + { + // Provider-specific by design: recognizable model families stay in their owning first-party catalog. + return ["openai"] + } + if ["gemini-", "gemma-", "deep-research-", "veo-", "lyria-"].contains(where: model.hasPrefix) { + return ["google"] + } + if model == "kimi-for-coding" || model == "k3" || model.hasPrefix("k3-") { + return ["kimi-for-coding"] + } + if model.hasPrefix("kimi-") || model.hasPrefix("moonshot-") { + return ["moonshot", "kimi-for-coding"] + } + if model.hasPrefix("minimax-") { return ["minimax"] } + if model.hasPrefix("deepseek-") { return ["deepseek"] } + return nil + } + + fileprivate static func claudeModelsDevLookup( + model rawModel: String, + catalog: ModelsDevCatalog?, + cacheRoot: URL?) -> ModelsDevPricingLookup? + { + let trimmed = rawModel.trimmingCharacters(in: .whitespacesAndNewlines) + let hasExplicitRoute = trimmed.contains("/") + let hasPreferredVendor = self.claudeFirstPartyModelsDevPreferredProviderIDs(for: trimmed) != nil + var matches: [ModelsDevPricingLookup] = [] + for target in self.claudeModelsDevPricingTargets(for: rawModel) { + if let lookup = self.modelsDevLookup( + providerID: target.providerID, + model: target.modelID, + catalog: catalog, + cacheRoot: cacheRoot) + { + if hasExplicitRoute || hasPreferredVendor { + return lookup + } + matches.append(lookup) + } + } + let matchedProviderIDs = Set(matches.map(\.pricing.providerID)) + guard matchedProviderIDs.count == 1 else { return nil } + return matches.first + } +} diff --git a/Tests/CodexBarTests/CodexLocalProjectUsageTests.swift b/Tests/CodexBarTests/CodexLocalProjectUsageTests.swift index fe6cce6a3d..dd2478a351 100644 --- a/Tests/CodexBarTests/CodexLocalProjectUsageTests.swift +++ b/Tests/CodexBarTests/CodexLocalProjectUsageTests.swift @@ -68,12 +68,15 @@ struct CodexLocalProjectUsageTests { func `local data scope avoids persisting raw Codex home paths`() { let home = FileManager.default.temporaryDirectory .appendingPathComponent("workspaces-private-home", isDirectory: true) - let scope = CodexLocalDataScope.resolve(options: CostUsageScanner.Options( - codexSessionsRoot: home.appendingPathComponent("sessions", isDirectory: true))) + let options = CostUsageScanner.Options( + codexSessionsRoot: home.appendingPathComponent("sessions", isDirectory: true)) + let scope = CodexLocalDataScope.resolve(options: options) + let scopedOptions = scope.applying(to: options) #expect(scope.codexHome == home.standardizedFileURL) #expect(scope.identifier.hasPrefix("codex-workspaces:")) #expect(!scope.identifier.contains(home.path)) + #expect(scopedOptions.codexTraceDatabaseURL == scope.stateDatabaseURL) } @Test diff --git a/Tests/CodexBarTests/CostUsageFetcherUnknownModelPricingTests.swift b/Tests/CodexBarTests/CostUsageFetcherUnknownModelPricingTests.swift index 0bf5b20d9f..4312b87959 100644 --- a/Tests/CodexBarTests/CostUsageFetcherUnknownModelPricingTests.swift +++ b/Tests/CodexBarTests/CostUsageFetcherUnknownModelPricingTests.swift @@ -66,6 +66,40 @@ struct CostUsageFetcherUnknownModelPricingTests { #expect(abs((breakdown.costUSD ?? 0) - 0.0000084) < 0.0000001) } + @Test + func `fetcher reprices a bare claude vendor model after an on demand catalog refresh`() async throws { + let fixture = try UnknownModelPricingFixture() + defer { fixture.environment.cleanup() } + let assistant: [String: Any] = [ + "type": "assistant", + "timestamp": fixture.environment.isoString(for: fixture.day), + "message": [ + "model": "deepseek-v4-flash", + "usage": [ + "input_tokens": 100, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 10, + ], + ], + ] + _ = try fixture.environment.writeClaudeProjectFile( + relativePath: "project-a/unknown-vendor-model.jsonl", + contents: fixture.environment.jsonl([assistant])) + + let snapshot = try await CostUsageFetcher.loadTokenSnapshot( + provider: .claude, + now: fixture.day, + refreshPricingInBackground: false, + scannerOptions: fixture.options, + modelsDevClient: ModelsDevClient(transport: CostUsageFetcherModelsDevTransport( + data: fixture.refreshedCatalog))) + + let breakdown = try #require(snapshot.daily.first?.modelBreakdowns?.first) + #expect(breakdown.modelName == "deepseek-v4-flash") + #expect(abs((breakdown.costUSD ?? 0) - 0.0000168) < 0.0000001) + } + @Test func `pricing retry preserves disabled pi session merging`() async throws { let fixture = try UnknownModelPricingFixture() @@ -273,6 +307,15 @@ private struct UnknownModelPricingFixture { } } }, + "deepseek": { + "id": "deepseek", + "models": { + "deepseek-v4-flash": { + "id": "deepseek-v4-flash", + "cost": { "input": 0.14, "output": 0.28 } + } + } + }, "anthropic": { "id": "anthropic", "models": { "claude-new": { "id": "claude-new", "cost": { "input": 3, "output": 15 } } } diff --git a/Tests/CodexBarTests/CostUsagePricingTests.swift b/Tests/CodexBarTests/CostUsagePricingTests.swift index fda9c2b3dc..ac55b8bdc9 100644 --- a/Tests/CodexBarTests/CostUsagePricingTests.swift +++ b/Tests/CodexBarTests/CostUsagePricingTests.swift @@ -1319,6 +1319,164 @@ extension CostUsagePricingTests { #expect(cost == expected) } + @Test + func `claude cost prices bare first-party model IDs from vendor catalogs`() throws { + let root = try Self.seedModelsDevCache(""" + { + "anthropic": { + "id": "anthropic", + "models": { + "claude-sonnet-4-6": { + "id": "claude-sonnet-4-6", + "cost": { "input": 3, "output": 15 } + }, + "deepseek-v4-flash": { + "id": "deepseek-v4-flash", + "cost": { "input": 99, "output": 199 } + } + } + }, + "openai": { + "id": "openai", + "models": { + "claude-sonnet-4-6": { + "id": "claude-sonnet-4-6", + "cost": { "input": 1, "output": 2 } + } + } + }, + "deepseek": { + "id": "deepseek", + "models": { + "deepseek-v4-flash": { + "id": "deepseek-v4-flash", + "cost": { "input": 0.14, "output": 0.28 } + } + } + }, + "opencode-go": { + "id": "opencode-go", + "models": { + "deepseek-v4-flash": { + "id": "deepseek-v4-flash", + "cost": { "input": 0.07, "output": 0.14 } + } + } + } + } + """) + + let bare = CostUsagePricing.claudeCostUSD( + model: "deepseek-v4-flash", + inputTokens: 100, + cacheReadInputTokens: 0, + cacheCreationInputTokens: 0, + outputTokens: 5, + modelsDevCacheRoot: root) + let prefixed = CostUsagePricing.claudeCostUSD( + model: "opencode-go/deepseek-v4-flash", + inputTokens: 100, + cacheReadInputTokens: 0, + cacheCreationInputTokens: 0, + outputTokens: 5, + modelsDevCacheRoot: root) + let anthropic = CostUsagePricing.claudeCostUSD( + model: "claude-sonnet-4-6", + inputTokens: 10, + cacheReadInputTokens: 0, + cacheCreationInputTokens: 0, + outputTokens: 5, + modelsDevCacheRoot: root) + + #expect(bare == (100.0 * 0.14e-6) + (5.0 * 0.28e-6)) + #expect(prefixed == (100.0 * 0.07e-6) + (5.0 * 0.14e-6)) + #expect(anthropic == (10.0 * 3e-6) + (5.0 * 15e-6)) + #expect(CostUsagePricing.claudeModelsDevPricingTargets(for: "deepseek-v4-flash").first?.providerID + == "deepseek") + #expect(CostUsagePricing.claudeModelsDevPricingTargets(for: "claude-sonnet-4-6").first?.providerID + == "anthropic") + #expect(Set(CostUsagePricing.claudeFirstPartyModelsDevProviderIDs).isSuperset(of: [ + "anthropic", + "openai", + "google", + "moonshot", + "kimi-for-coding", + "minimax", + "deepseek", + ])) + } + + @Test + func `claude cost rejects an ambiguous bare model catalog collision`() throws { + let root = try Self.seedModelsDevCache(""" + { + "openai": { + "id": "openai", + "models": { + "shared-model": { + "id": "shared-model", + "cost": { "input": 1, "output": 2 } + } + } + }, + "google": { + "id": "google", + "models": { + "shared-model": { + "id": "shared-model", + "cost": { "input": 3, "output": 4 } + } + } + } + } + """) + + let cost = CostUsagePricing.claudeCostUSD( + model: "shared-model", + inputTokens: 100, + cacheReadInputTokens: 0, + cacheCreationInputTokens: 0, + outputTokens: 5, + modelsDevCacheRoot: root) + + #expect(cost == nil) + } + + @Test + func `claude cost does not cross charge a prefixed route onto first-party rates`() throws { + let root = try Self.seedModelsDevCache(""" + { + "deepseek": { + "id": "deepseek", + "models": { + "deepseek-v4-flash": { + "id": "deepseek-v4-flash", + "cost": { "input": 0.14, "output": 0.28 } + } + } + } + } + """) + + let cost = CostUsagePricing.claudeCostUSD( + model: "opencode-go/deepseek-v4-flash", + inputTokens: 100, + cacheReadInputTokens: 0, + cacheCreationInputTokens: 0, + outputTokens: 5, + modelsDevCacheRoot: root) + let unknownRoute = CostUsagePricing.claudeCostUSD( + model: "unknown-route/deepseek-v4-flash", + inputTokens: 100, + cacheReadInputTokens: 0, + cacheCreationInputTokens: 0, + outputTokens: 5, + modelsDevCacheRoot: root) + + #expect(cost == nil) + #expect(unknownRoute == nil) + } + private static func seedModelsDevCache(_ json: String) throws -> URL { let root = try Self.cacheRoot() let catalog = try JSONDecoder().decode(ModelsDevCatalog.self, from: Data(json.utf8)) diff --git a/Tests/CodexBarTests/PiSessionCostScannerTests.swift b/Tests/CodexBarTests/PiSessionCostScannerTests.swift index 677b807fbd..7f5b1eb702 100644 --- a/Tests/CodexBarTests/PiSessionCostScannerTests.swift +++ b/Tests/CodexBarTests/PiSessionCostScannerTests.swift @@ -1091,6 +1091,78 @@ extension PiSessionCostScannerTests { #expect(abs((secondReport.data.first?.costUSD ?? 0) - 2.4) < 0.0000001) } + @Test + func `pi scanner reprices unchanged claude files when vendor rates change`() throws { + let env = try CostUsageTestEnvironment() + defer { env.cleanup() } + + let day = try env.makeLocalNoon(year: 2026, month: 7, day: 10) + let model = "deepseek-v4-flash" + func assistant(at timestamp: Date) -> [String: Any] { + [ + "type": "message", + "timestamp": env.isoString(for: timestamp), + "message": [ + "role": "assistant", + "provider": "anthropic", + "model": model, + "timestamp": Int(timestamp.timeIntervalSince1970 * 1000), + "usage": [ + "input": 150_000, + "output": 0, + "totalTokens": 150_000, + ], + ], + ] + } + _ = try env.writePiSessionFile( + relativePath: "2026-07-10T10-00-00-000Z_claude-vendor-catalog-change.jsonl", + contents: env.jsonl([ + assistant(at: day.addingTimeInterval(-1)), + assistant(at: day), + ])) + + let firstCatalog = try Self.deepSeekModelsDevCatalog(inputCostPerMillion: 4) + #expect(ModelsDevCache.save(catalog: firstCatalog, fetchedAt: day, cacheRoot: env.cacheRoot)) + let options = PiSessionCostScanner.Options( + piSessionsRoot: env.piSessionsRoot, + cacheRoot: env.cacheRoot, + refreshMinIntervalSeconds: 3600) + let firstReport = PiSessionCostScanner.loadDailyReport( + provider: .claude, + since: day, + until: day, + now: day, + options: options) + let firstCache = PiSessionCostCacheIO.load(cacheRoot: env.cacheRoot) + let firstPricingKey = try #require(firstCache.pricingKey) + #expect(firstReport.data.first?.totalTokens == 300_000) + #expect(abs((firstReport.data.first?.costUSD ?? 0) - 1.2) < 0.0000001) + + let secondCatalog = try Self.deepSeekModelsDevCatalog(inputCostPerMillion: 8) + #expect(ModelsDevCache.save( + catalog: secondCatalog, + fetchedAt: day.addingTimeInterval(1), + cacheRoot: env.cacheRoot)) + #expect(PiSessionCostScanner.loadCachedDailyReport( + provider: .claude, + since: day, + until: day, + now: day.addingTimeInterval(1), + cacheRoot: env.cacheRoot) == nil) + + let secondReport = PiSessionCostScanner.loadDailyReport( + provider: .claude, + since: day, + until: day, + now: day.addingTimeInterval(2), + options: options) + let secondCache = PiSessionCostCacheIO.load(cacheRoot: env.cacheRoot) + #expect(secondCache.pricingKey != firstPricingKey) + #expect(secondReport.data.first?.totalTokens == 300_000) + #expect(abs((secondReport.data.first?.costUSD ?? 0) - 2.4) < 0.0000001) + } + @Test func `pi pricing key ignores catalog fetch time when rates are unchanged`() throws { let env = try CostUsageTestEnvironment() @@ -1159,11 +1231,11 @@ extension PiSessionCostScannerTests { } } }, - "google": { - "id": "google", + "groq": { + "id": "groq", "models": { - "gemini-test": { - "id": "gemini-test", + "groq-test": { + "id": "groq-test", "cost": { "input": 1, "output": 2 } } } @@ -1209,11 +1281,11 @@ extension PiSessionCostScannerTests { } } }, - "google": { - "id": "google", + "groq": { + "id": "groq", "models": { - "gemini-test": { - "id": "gemini-test", + "groq-test": { + "id": "groq-test", "cost": { "input": 99, "output": 199 } } } @@ -1345,6 +1417,26 @@ extension PiSessionCostScannerTests { return try self.modelsDevCatalog(json) } + private static func deepSeekModelsDevCatalog(inputCostPerMillion: Double) throws -> ModelsDevCatalog { + let json = """ + { + "deepseek": { + "id": "deepseek", + "models": { + "deepseek-v4-flash": { + "id": "deepseek-v4-flash", + "cost": { + "input": \(inputCostPerMillion), + "output": 0.28 + } + } + } + } + } + """ + return try self.modelsDevCatalog(json) + } + private static func modelsDevCatalog(_ json: String) throws -> ModelsDevCatalog { try JSONDecoder().decode(ModelsDevCatalog.self, from: Data(json.utf8)) } diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index b41b4091e2..9d8c9d496d 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -1353,19 +1353,19 @@ struct ProviderArchitectureGatekeeperTests { reason: "This provider-specific core branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBarCore/CostUsageFetcher.swift", - line: 742, + line: 746, anchor: "provider: .codex,", expectedProviderIDs: ["codex"], reason: "This provider-specific core branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBarCore/CostUsageFetcher.swift", - line: 819, + line: 823, anchor: "provider: .codex,", expectedProviderIDs: ["codex"], reason: "This provider-specific core branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBarCore/CostUsageFetcher.swift", - line: 910, + line: 914, anchor: "provider: .codex,", expectedProviderIDs: ["codex"], reason: "This provider-specific core branch passes its already-selected identity to a shared helper."), @@ -3425,7 +3425,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact cost scanner dispatch selects a provider-owned transcript, cache, or pricing format."), AllowedProviderConstruct( path: "Sources/CodexBarCore/CostUsageFetcher.swift", - line: 1174, + line: 1178, anchor: "if provider == .vertexai {", expectedProviderIDs: ["claude", "vertexai"], expectedReferenceCount: 2, @@ -3433,7 +3433,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact cost scanner dispatch selects a provider-owned transcript, cache, or pricing format."), AllowedProviderConstruct( path: "Sources/CodexBarCore/CostUsageFetcher.swift", - line: 1529, + line: 1533, anchor: "if provider == .cursor {", expectedProviderIDs: ["cursor"], expectedReferenceCount: 1, @@ -3497,7 +3497,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact cost scanner dispatch selects a provider-owned transcript, cache, or pricing format."), AllowedProviderConstruct( path: "Sources/CodexBarCore/PiSessionCostScanner.swift", - line: 834, + line: 835, anchor: "case .codex:", expectedProviderIDs: ["claude", "codex"], expectedReferenceCount: 2, @@ -3505,7 +3505,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact cost scanner dispatch selects a provider-owned transcript, cache, or pricing format."), AllowedProviderConstruct( path: "Sources/CodexBarCore/PiSessionCostScanner.swift", - line: 883, + line: 884, anchor: ".codex", expectedProviderIDs: ["claude", "codex"], expectedReferenceCount: 2, @@ -3660,7 +3660,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact cost scanner dispatch selects a provider-owned transcript, cache, or pricing format."), AllowedProviderConstruct( path: "Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift", - line: 777, + line: 776, anchor: "guard let pricing = self.claude[key] else { return nil }", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, diff --git a/docs/model-pricing.md b/docs/model-pricing.md index 1b4b2f01a2..32a82055e0 100644 --- a/docs/model-pricing.md +++ b/docs/model-pricing.md @@ -8,7 +8,7 @@ read_when: # Model pricing metadata -CodexBar has an additive models.dev pricing pipeline for future cost lookup work. Existing hardcoded pricing remains unchanged for now. +CodexBar uses models.dev as an additive pricing source alongside bundled fallback rates. ## Source and cache @@ -23,14 +23,14 @@ The pipeline lets future scanner code read the last valid cache synchronously wi Pricing is scoped by provider id and model id. This prevents two providers with the same model id or display name from sharing pricing accidentally. -Planned local source mapping: +Local cost scanners preserve that scope when selecting a catalog: -- Codex/OpenAI logs: models.dev provider id `openai` -- Claude logs: models.dev provider id `anthropic` +- Bare Codex/OpenAI model IDs use provider id `openai`; approved provider-qualified routes stay on their route, and unknown prefixes remain unpriced. +- Recognizable bare Claude-session model families use their first-party vendor catalog, including Anthropic, OpenAI, Google, Moonshot/Kimi, MiniMax, and DeepSeek. +- Other bare Claude-session IDs are priced only when exactly one selected first-party catalog matches. Ambiguous cross-vendor matches remain unpriced. +- Provider-qualified Claude-session IDs stay on an approved explicit route and never fall through to another vendor. - Vertex AI Claude logs: models.dev provider id `google-vertex-anthropic` -The first integration PR only adds the parser, client, cache, provider-scoped lookup, and tests. It does not route live cost calculations through models.dev yet. - ## Units models.dev publishes costs as USD per 1M tokens. CodexBar converts those to USD per token in the metadata layer: