diff --git a/Sources/CodexBar/CostHistoryChartMenuView.swift b/Sources/CodexBar/CostHistoryChartMenuView.swift index 581f684166..f45b88e316 100644 --- a/Sources/CodexBar/CostHistoryChartMenuView.swift +++ b/Sources/CodexBar/CostHistoryChartMenuView.swift @@ -232,6 +232,13 @@ struct CostHistoryChartMenuView: View { } if let total = self.totalCostUSD { + if let disclaimer = Self.estimateDisclaimer(provider: self.provider) { + Text(disclaimer) + .font(.caption2) + .foregroundStyle(Color(nsColor: .tertiaryLabelColor)) + .lineLimit(1) + .truncationMode(.tail) + } Text(String( format: L("Est. total (%@): %@"), self.windowLabel ?? Self.windowLabel(days: self.historyDays), @@ -283,6 +290,10 @@ struct CostHistoryChartMenuView: View { .frame(minWidth: self.width, maxWidth: .infinity, alignment: .top) } + static func estimateDisclaimer(provider: UsageProvider) -> String? { + provider == .codex ? L("codex_api_estimate_not_billed") : nil + } + private struct Model { let points: [Point] let pointsByDateKey: [String: Point] diff --git a/Sources/CodexBar/InlineUsageDashboardContent.swift b/Sources/CodexBar/InlineUsageDashboardContent.swift index dde22b0ce6..9d5a258f63 100644 --- a/Sources/CodexBar/InlineUsageDashboardContent.swift +++ b/Sources/CodexBar/InlineUsageDashboardContent.swift @@ -343,12 +343,21 @@ extension UsageMenuCardView.Model { comparisonPeriodsEnabled: Bool) -> InlineUsageDashboardModel { let historyDays = max(1, min(365, snapshot.historyDays)) - let historyTitle = snapshot.historyLabel + let defaultHistoryTitle = snapshot.historyLabel ?? (historyDays == 1 ? L("Today") : historyDays == 30 ? L("30d cost") : "\(String(format: L("Last %d days"), historyDays)) \(L("Cost"))") + let codexHistoryPeriod = snapshot.historyLabel + ?? (historyDays == 1 + ? L("Today") + : historyDays == 30 + ? "30d" + : String(format: L("Last %d days"), historyDays)) + let historyTitle = provider == .codex + ? "\(codexHistoryPeriod) · \(L("codex_api_estimate_header"))" + : defaultHistoryTitle let tokenHistoryTitle = snapshot.historyLabel.map { "\($0) \(L("tokens"))" } ?? (historyDays == 1 ? L("Today tokens") @@ -388,19 +397,28 @@ extension UsageMenuCardView.Model { details .append("\(requestHistoryTitle): \(UsageFormatter.tokenCountString(requestCount)) \(L("requests"))") } - if let hint = Self.tokenUsageHint(provider: provider) { - details.append(hint) + let hintLines = Self.tokenUsageHintLines(provider: provider) + if hintLines.isEmpty == false { + details.append(contentsOf: hintLines) } else { details.append(L("cost_estimate_hint")) } } let providerName = ProviderDefaults.metadata[provider]?.displayName ?? provider.rawValue + let codexEstimateHeader = L("codex_api_estimate_header") + let accessibilityLabel = if provider == .codex { + "\(providerName) \(periodLabel) \(codexEstimateHeader) trend" + } else { + "\(providerName) \(periodLabel) cost trend" + } var model = InlineUsageDashboardModel( - accessibilityLabel: "\(providerName) \(periodLabel) cost trend", + accessibilityLabel: accessibilityLabel, valueStyle: Self.costValueStyle(currencyCode: snapshot.currencyCode), kpis: [ .init( - title: usesLatestPrimary ? L("Latest") : L("Today"), + title: provider == .codex + ? "\(L("Today")) · \(L("codex_api_estimate_header"))" + : usesLatestPrimary ? L("Latest") : L("Today"), value: primaryCostUSD.map { Self.costString($0, currencyCode: snapshot.currencyCode) } ?? "—", emphasis: true), .init( diff --git a/Sources/CodexBar/MenuCardView+Costs.swift b/Sources/CodexBar/MenuCardView+Costs.swift index d9c4d43321..adb1e6bbee 100644 --- a/Sources/CodexBar/MenuCardView+Costs.swift +++ b/Sources/CodexBar/MenuCardView+Costs.swift @@ -169,21 +169,34 @@ extension UsageMenuCardView.Model { } static func tokenUsageHint(provider: UsageProvider) -> String? { + let lines = Self.tokenUsageHintLines(provider: provider) + return lines.isEmpty ? nil : lines.joined(separator: "\n") + } + + static func tokenUsageHeader(provider: UsageProvider) -> String { + provider == .codex ? L("codex_api_estimate_header") : L("cost_header_estimated") + } + + static func tokenUsageHintLines(provider: UsageProvider) -> [String] { switch provider { case .codex: - L("Estimated from local Codex logs for the selected account.") + [ + L("Estimated from local Codex logs for the selected account."), + L("codex_api_estimate_not_billed"), + L("codex_api_estimate_hint"), + ] case .claude: - UsageFormatter.costEstimateHint(provider: provider) + [UsageFormatter.costEstimateHint(provider: provider)] case .vertexai: - L("cost_estimate_hint") + [L("cost_estimate_hint")] case .bedrock: - L("AWS Cost Explorer billing can lag.") + [L("AWS Cost Explorer billing can lag.")] case .openai: - L("Reported by OpenAI Admin API organization usage.") + [L("Reported by OpenAI Admin API organization usage.")] case .mistral: - L("Reported by Mistral billing usage.") + [L("Reported by Mistral billing usage.")] default: - nil + [] } } diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift index c564fd34df..cd54ddb0a2 100644 --- a/Sources/CodexBar/MenuCardView.swift +++ b/Sources/CodexBar/MenuCardView.swift @@ -213,7 +213,7 @@ struct UsageMenuCardView: View { } if let tokenUsage = liveModel.tokenUsage { VStack(alignment: .leading, spacing: 6) { - Text(L("cost_header_estimated")) + Text(UsageMenuCardView.Model.tokenUsageHeader(provider: liveModel.provider)) .font(.body) .fontWeight(.medium) Text(tokenUsage.sessionLine) @@ -758,7 +758,7 @@ struct UsageMenuCardCostSectionView: View { VStack(alignment: .leading, spacing: 10) { if let tokenUsage = liveModel.tokenUsage { VStack(alignment: .leading, spacing: 6) { - Text(L("cost_header_estimated")) + Text(UsageMenuCardView.Model.tokenUsageHeader(provider: liveModel.provider)) .font(.body) .fontWeight(.medium) Text(tokenUsage.sessionLine) diff --git a/Sources/CodexBar/PreferencesProviderDetailView.swift b/Sources/CodexBar/PreferencesProviderDetailView.swift index f2cf4da0c3..1fac04a315 100644 --- a/Sources/CodexBar/PreferencesProviderDetailView.swift +++ b/Sources/CodexBar/PreferencesProviderDetailView.swift @@ -423,8 +423,14 @@ struct ProviderMetricsInlineView: View { } if let tokenUsage = self.model.tokenUsage { - ProviderMetricInlineTextRow(title: L("Cost"), value: tokenUsage.sessionLine) + let isCodexEstimate = self.model.provider == .codex + ProviderMetricInlineTextRow( + title: isCodexEstimate ? L("codex_api_estimate_header") : L("Cost"), + value: tokenUsage.sessionLine) ProviderMetricInlineTextRow(title: "", value: tokenUsage.monthLine) + if isCodexEstimate, let hint = tokenUsage.hintLine, !hint.isEmpty { + ProviderMetricInlineTextRow(title: "", value: hint) + } } } } diff --git a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings index 0d1bfacd7d..9fdbf60f0f 100644 --- a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings @@ -719,6 +719,9 @@ /* Cost estimation */ "cost_header_estimated" = "التكلفة (تقديرية)"; "cost_estimate_hint" = "تقديرات من سجلات محلية · قد تختلف عن فاتورتك"; +"codex_api_estimate_header" = "تقدير مكافئ لواجهة API"; +"codex_api_estimate_hint" = "الاستخدام المحلي × أسعار API العامة · ليس فاتورة اشتراك أو قيمة خطة"; +"codex_api_estimate_not_billed" = "ليس فاتورة اشتراك أو قيمة خطة"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "لم يتم اكتشاف أي JetBrains IDE مع مساعدة الذكاء الاصطناعي. قم بتثبيت JetBrains IDE وفعل AI Assistant."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "OpenRouter API الرمز غير مكون. حدد OPENROUTER_API_KEY متغير البيئة أو قم بالتكوين في الإعدادات."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "z.ai API الرمز لم يعثر عليه. اضبط apiKey في ~/.codexbar/config.json أو Z_AI_API_KEY."; diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index f9f988ad46..015ec26bd3 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -697,6 +697,9 @@ /* Cost estimation */ "cost_header_estimated" = "Cost (estimat)"; "cost_estimate_hint" = "Estimat a partir de registres locals · pot diferir de la vostra factura"; +"codex_api_estimate_header" = "Estimació equivalent a l'API"; +"codex_api_estimate_hint" = "Ús local × preus públics de l'API · no és una factura de subscripció ni el valor d'un pla"; +"codex_api_estimate_not_billed" = "no és una factura de subscripció ni el valor d'un pla"; /* Popup panels */ "No usage configured." = "No hi ha cap ús configurat."; diff --git a/Sources/CodexBar/Resources/de.lproj/Localizable.strings b/Sources/CodexBar/Resources/de.lproj/Localizable.strings index eefef346ad..da34973999 100644 --- a/Sources/CodexBar/Resources/de.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/de.lproj/Localizable.strings @@ -712,6 +712,9 @@ /* Cost estimation */ "cost_header_estimated" = "Kosten (geschätzt)"; "cost_estimate_hint" = "Schätzung aus lokalen Protokollen · kann von Ihrer Rechnung abweichen"; +"codex_api_estimate_header" = "API-äquivalente Schätzung"; +"codex_api_estimate_hint" = "Lokale Nutzung × öffentliche API-Preise · keine Abonnementrechnung oder Planwert"; +"codex_api_estimate_not_billed" = "keine Abonnementrechnung oder Planwert"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "Keine JetBrains-IDE mit AI Assistant erkannt. Installieren Sie eine JetBrains-IDE und aktivieren Sie AI Assistant."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "OpenRouter-API-Token nicht konfiguriert. Legen Sie die Umgebungsvariable OPENROUTER_API_KEY fest oder konfigurieren Sie sie in den Einstellungen."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "z.ai-API-Token nicht gefunden. Legen Sie apiKey in ~/.codexbar/config.json oder Z_AI_API_KEY fest."; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index 5cce96aecb..27437fd608 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -719,6 +719,9 @@ /* Cost estimation */ "cost_header_estimated" = "Cost (estimated)"; "cost_estimate_hint" = "Estimated from local logs · may differ from your bill"; +"codex_api_estimate_header" = "API-equivalent estimate"; +"codex_api_estimate_hint" = "Local usage × public API prices · not a subscription bill or plan value"; +"codex_api_estimate_not_billed" = "not a subscription bill or plan value"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY."; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index 1a4184fa7e..1cc5823804 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -707,6 +707,9 @@ /* Cost estimation */ "cost_header_estimated" = "Coste (estimado)"; "cost_estimate_hint" = "Estimado a partir de registros locales · puede diferir de tu factura"; +"codex_api_estimate_header" = "Estimación equivalente a la API"; +"codex_api_estimate_hint" = "Uso local × precios públicos de la API · no es una factura de suscripción ni el valor de un plan"; +"codex_api_estimate_not_billed" = "no es una factura de suscripción ni el valor de un plan"; /* Popup panels */ "No usage configured." = "No hay uso configurado."; diff --git a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings index 7a8650b62f..b524d9bb27 100644 --- a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings @@ -719,6 +719,9 @@ /* Cost estimation */ "cost_header_estimated" = "هزینه (تخمینی)"; "cost_estimate_hint" = "برآورد شده از چوب های محلی · ممکن است با صورتحساب شما متفاوت باشد"; +"codex_api_estimate_header" = "برآورد معادل API"; +"codex_api_estimate_hint" = "استفاده محلی × قیمت‌های عمومی API · نه صورتحساب اشتراک است و نه ارزش طرح"; +"codex_api_estimate_not_billed" = "نه صورتحساب اشتراک است و نه ارزش طرح"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "هیچ JetBrains IDE ای با AI Assistant شناسایی نشد. یک JetBrains IDE نصب کنید و AI Assistant را فعال کنید."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "OpenRouter API توکن پیکربندی نشده است. متغیر محیطی OPENROUTER_API_KEY تنظیم کنید یا در تنظیمات پیکربندی کنید."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "z.ai API توکن پیدا نشد. apiKey را در ~/.codexbar/config.json یا Z_AI_API_KEY تنظیم کنید."; diff --git a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings index badad1af1a..72b0aaef22 100644 --- a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings @@ -714,6 +714,9 @@ /* Cost estimation */ "cost_header_estimated" = "Coût (estimé)"; "cost_estimate_hint" = "Estimé à partir des journaux locaux · peut différer de votre facture"; +"codex_api_estimate_header" = "Estimation équivalente à l’API"; +"codex_api_estimate_hint" = "Utilisation locale × tarifs API publics · ce n’est ni une facture d’abonnement ni la valeur d’un forfait"; +"codex_api_estimate_not_billed" = "ce n’est ni une facture d’abonnement ni la valeur d’un forfait"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "Aucun IDE JetBrains avec AI Assistant détecté. Installez un IDE JetBrains et activez AI Assistant."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "Jeton API OpenRouter non configuré. Définissez la variable d'environnement OPENROUTER_API_KEY ou configurez-la dans Paramètres."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "Jeton API z.ai introuvable. Définissez apiKey dans ~/.codexbar/config.json ou Z_AI_API_KEY."; diff --git a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings index 39d1b906c4..761318953f 100644 --- a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings @@ -691,6 +691,9 @@ /* Cost estimation */ "cost_header_estimated" = "Custo (estimado)"; "cost_estimate_hint" = "Estimado a partir de rexistros locais · pode diferir da túa factura"; +"codex_api_estimate_header" = "Estimación equivalente á API"; +"codex_api_estimate_hint" = "Uso local × prezos públicos da API · non é unha factura de subscrición nin o valor dun plan"; +"codex_api_estimate_not_billed" = "non é unha factura de subscrición nin o valor dun plan"; /* Popup panels */ "No usage configured." = "Non hai ningún uso configurado."; diff --git a/Sources/CodexBar/Resources/id.lproj/Localizable.strings b/Sources/CodexBar/Resources/id.lproj/Localizable.strings index 3e415a9faf..7dd2d7d286 100644 --- a/Sources/CodexBar/Resources/id.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/id.lproj/Localizable.strings @@ -721,6 +721,9 @@ /* Cost estimation */ "cost_header_estimated" = "Biaya (perkiraan)"; "cost_estimate_hint" = "Diperkirakan dari log lokal · mungkin berbeda dari tagihan Anda"; +"codex_api_estimate_header" = "Estimasi setara API"; +"codex_api_estimate_hint" = "Penggunaan lokal × harga API publik · bukan tagihan langganan atau nilai paket"; +"codex_api_estimate_not_billed" = "bukan tagihan langganan atau nilai paket"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "Tidak ada JetBrains IDE dengan AI Assistant terdeteksi. Pasang JetBrains IDE dan aktifkan AI Assistant."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "Token API OpenRouter belum dikonfigurasi. Atur variabel lingkungan OPENROUTER_API_KEY atau konfigurasi di Pengaturan."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "Token API z.ai tidak ditemukan. Atur apiKey di ~/.codexbar/config.json atau Z_AI_API_KEY."; diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index 3820c71f39..34519457f7 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -721,6 +721,9 @@ /* Cost estimation */ "cost_header_estimated" = "Costo (stimato)"; "cost_estimate_hint" = "Stimato dai log locali · può differire dalla fattura"; +"codex_api_estimate_header" = "Stima equivalente API"; +"codex_api_estimate_hint" = "Utilizzo locale × prezzi API pubblici · non è una fattura di abbonamento né il valore di un piano"; +"codex_api_estimate_not_billed" = "non è una fattura di abbonamento né il valore di un piano"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "Nessun IDE JetBrains con AI Assistant rilevato. Installa un IDE JetBrains e abilita AI Assistant."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "Token API OpenRouter non configurato. Imposta la variabile d'ambiente OPENROUTER_API_KEY oppure configuralo nelle Impostazioni."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "Token API z.ai non trovato. Imposta apiKey in ~/.codexbar/config.json o Z_AI_API_KEY."; diff --git a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings index 5589f64b64..490b2b4c73 100644 --- a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings @@ -711,6 +711,9 @@ /* Cost estimation */ "cost_header_estimated" = "コスト(推定)"; "cost_estimate_hint" = "ローカルログからの推定値 · 請求額と異なる場合があります"; +"codex_api_estimate_header" = "API相当の見積もり"; +"codex_api_estimate_hint" = "ローカル使用量 × 公開API価格 · サブスクリプションの請求額やプラン価値ではありません"; +"codex_api_estimate_not_billed" = "サブスクリプションの請求額やプラン価値ではありません"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "AI Assistant 対応の JetBrains IDE が検出されませんでした。JetBrains IDE をインストールし、AI Assistant を有効にしてください。"; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "OpenRouter API トークンが設定されていません。環境変数 OPENROUTER_API_KEY を設定するか、設定で構成してください。"; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "z.ai API トークンが見つかりません。~/.codexbar/config.json の apiKey または Z_AI_API_KEY を設定してください。"; diff --git a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings index 67b0dc7439..c12a84440a 100644 --- a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings @@ -685,6 +685,9 @@ "not_found" = "찾을 수 없음"; "cost_header_estimated" = "비용(추정)"; "cost_estimate_hint" = "로컬 로그에서 추정 · 실제 청구액과 다를 수 있음"; +"codex_api_estimate_header" = "API 등가 추정치"; +"codex_api_estimate_hint" = "로컬 사용량 × 공개 API 가격 · 구독 청구서나 플랜 가치가 아닙니다"; +"codex_api_estimate_not_billed" = "구독 청구서나 플랜 가치가 아닙니다"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "AI Assistant가 있는 JetBrains IDE를 찾지 못했습니다. JetBrains IDE를 설치하고 AI Assistant를 사용하세요."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "OpenRouter API 토큰이 구성되지 않았습니다. OPENROUTER_API_KEY 환경 변수를 설정하거나 설정에서 구성하세요."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "z.ai API 토큰을 찾을 수 없습니다. ~/.codexbar/config.json에 apiKey를 설정하거나 Z_AI_API_KEY를 설정하세요."; diff --git a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings index c456a2cf1b..e4d70185f9 100644 --- a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings @@ -714,6 +714,9 @@ /* Cost estimation */ "cost_header_estimated" = "Kosten (geschat)"; "cost_estimate_hint" = "Geschat op basis van lokale logboeken · kan afwijken van uw factuur"; +"codex_api_estimate_header" = "API-equivalente schatting"; +"codex_api_estimate_hint" = "Lokaal gebruik × openbare API-prijzen · geen abonnementsfactuur of planwaarde"; +"codex_api_estimate_not_billed" = "geen abonnementsfactuur of planwaarde"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "Geen JetBrains IDE met AI Assistant gedetecteerd. Installeer een JetBrains IDE en schakel AI Assistant in."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "OpenRouter API-token niet geconfigureerd. Stel de omgevingsvariabele OPENROUTER_API_KEY in of configureer deze in Instellingen."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "z.ai API-token niet gevonden. Stel apiKey in ~/.codexbar/config.json of Z_AI_API_KEY."; diff --git a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings index b6ed2914ee..279e8a5023 100644 --- a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings @@ -721,6 +721,9 @@ /* Cost estimation */ "cost_header_estimated" = "Koszt (szacowany)"; "cost_estimate_hint" = "Oszacowano na podstawie lokalnych logów · może różnić się od rachunku"; +"codex_api_estimate_header" = "Szacunek równoważny API"; +"codex_api_estimate_hint" = "Lokalne użycie × publiczne ceny API · to nie jest rachunek za subskrypcję ani wartość planu"; +"codex_api_estimate_not_billed" = "to nie jest rachunek za subskrypcję ani wartość planu"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "Nie wykryto IDE JetBrains z AI Assistant. Zainstaluj IDE JetBrains i włącz AI Assistant."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "Token API OpenRouter nie jest skonfigurowany. Ustaw zmienną środowiskową OPENROUTER_API_KEY albo skonfiguruj go w Ustawieniach."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "Nie znaleziono tokenu API z.ai. Ustaw `apiKey` w ~/.codexbar/config.json albo Z_AI_API_KEY."; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index 169614263e..2295844532 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -711,6 +711,9 @@ /* Cost estimation */ "cost_header_estimated" = "Custo (estimado)"; "cost_estimate_hint" = "Estimado a partir de logs locais · pode diferir da sua fatura"; +"codex_api_estimate_header" = "Estimativa equivalente à API"; +"codex_api_estimate_hint" = "Uso local × preços públicos de API · não é uma fatura de assinatura nem o valor de um plano"; +"codex_api_estimate_not_billed" = "não é uma fatura de assinatura nem o valor de um plano"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "Nenhuma IDE JetBrains com AI Assistant detectada. Instale uma IDE JetBrains e ative o AI Assistant."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "Token de API do OpenRouter não configurado. Defina a variável de ambiente OPENROUTER_API_KEY ou configure em Ajustes."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "Token de API do z.ai não encontrado. Defina apiKey em ~/.codexbar/config.json ou Z_AI_API_KEY."; diff --git a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings index b2048369dc..745e217c7e 100644 --- a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings @@ -715,6 +715,9 @@ /* Cost estimation */ "cost_header_estimated" = "Стоимость (оценочная)"; "cost_estimate_hint" = "Оценка на основе локальных журналов · может отличаться от суммы в счёте."; +"codex_api_estimate_header" = "Оценка, эквивалентная API"; +"codex_api_estimate_hint" = "Локальное использование × публичные цены API · это не счёт за подписку и не ценность плана"; +"codex_api_estimate_not_billed" = "это не счёт за подписку и не ценность плана"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "JetBrains IDE с включённым AI Assistant не обнаружена. Установите JetBrains IDE и включите AI Assistant."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "API-токен OpenRouter не настроен. Задайте переменную окружения OPENROUTER_API_KEY или настройте его в настройках."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "API-токен z.ai не найден. Задайте apiKey в ~/.codexbar/config.json или Z_AI_API_KEY."; diff --git a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings index 3150d5082a..7241af848d 100644 --- a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings @@ -713,6 +713,9 @@ /* Cost estimation */ "cost_header_estimated" = "Kostnad (uppskattad)"; "cost_estimate_hint" = "Uppskattat från lokala loggar · kan skilja sig från din faktura"; +"codex_api_estimate_header" = "API-ekvivalent uppskattning"; +"codex_api_estimate_hint" = "Lokal användning × offentliga API-priser · inte en prenumerationsfaktura eller ett planvärde"; +"codex_api_estimate_not_billed" = "inte en prenumerationsfaktura eller ett planvärde"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "Ingen JetBrains IDE med AI Assistant hittades. Installera en JetBrains IDE och aktivera AI Assistant."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "OpenRouter-API-token är inte konfigurerad. Ange miljövariabeln OPENROUTER_API_KEY eller konfigurera i Inställningar."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "z.ai-API-token hittades inte. Ange apiKey i ~/.codexbar/config.json eller Z_AI_API_KEY."; diff --git a/Sources/CodexBar/Resources/th.lproj/Localizable.strings b/Sources/CodexBar/Resources/th.lproj/Localizable.strings index cdd97835b3..8fe4f65d57 100644 --- a/Sources/CodexBar/Resources/th.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/th.lproj/Localizable.strings @@ -719,6 +719,9 @@ /* Cost estimation */ "cost_header_estimated" = "ต้นทุน (โดยประมาณ)"; "cost_estimate_hint" = "ประมาณการจากบันทึกท้องถิ่น · อาจแตกต่างจากใบเรียกเก็บเงินของคุณ"; +"codex_api_estimate_header" = "ค่าประมาณเทียบเท่า API"; +"codex_api_estimate_hint" = "การใช้งานในเครื่อง × ราคา API สาธารณะ · ไม่ใช่ใบเรียกเก็บเงินค่าสมาชิกหรือมูลค่าแพ็กเกจ"; +"codex_api_estimate_not_billed" = "ไม่ใช่ใบเรียกเก็บเงินค่าสมาชิกหรือมูลค่าแพ็กเกจ"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "ตรวจไม่พบ JetBrains IDE ที่มี AI Assistant ติดตั้ง JetBrains IDE และเปิดใช้งาน AI Assistant"; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "โทเค็น OpenRouter API ไม่ได้กําหนดค่า ตั้งค่าตัวแปรสภาพแวดล้อม OPENROUTER_API_KEY หรือกําหนดค่าในการตั้งค่า"; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "ไม่พบโทเค็น z.ai API ตั้งค่า apiKey เป็น ~/.codexbar/config.json หรือ Z_AI_API_KEY"; diff --git a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings index 977d72c3b9..ce86e7092f 100644 --- a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings @@ -715,6 +715,9 @@ /* Cost estimation */ "cost_header_estimated" = "Maliyet (tahmini)"; "cost_estimate_hint" = "Yerel günlüklerden tahmini · faturanızdan farklı olabilir"; +"codex_api_estimate_header" = "API eşdeğeri tahmin"; +"codex_api_estimate_hint" = "Yerel kullanım × herkese açık API fiyatları · abonelik faturası veya plan değeri değildir"; +"codex_api_estimate_not_billed" = "abonelik faturası veya plan değeri değildir"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "AI Asistan içeren JetBrains IDE algılanmadı. Bir JetBrains IDE kurun ve AI Asistan'ı etkinleştirin."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "OpenRouter API jetonu yapılandırılmamış. OPENROUTER_API_KEY ortam değişkenini ayarlayın veya Ayarlar'dan yapılandırın."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "z.ai API jetonu bulunamadı. ~/.codexbar/config.json dosyasında apiKey ayarlayın veya Z_AI_API_KEY kullanın."; diff --git a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings index 7412ff0148..77770d861f 100644 --- a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings @@ -714,6 +714,9 @@ /* Cost estimation */ "cost_header_estimated" = "Вартість (орієнтовна)"; "cost_estimate_hint" = "Оцінка з місцевих журналів · може відрізнятися від вашого рахунку"; +"codex_api_estimate_header" = "Оцінка, еквівалентна API"; +"codex_api_estimate_hint" = "Локальне використання × публічні ціни API · це не рахунок за підписку і не цінність плану"; +"codex_api_estimate_not_billed" = "це не рахунок за підписку і не цінність плану"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "Не виявлено JetBrains IDE з AI Assistant. Встановіть JetBrains IDE і ввімкніть AI Assistant."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "Маркер OpenRouter API не налаштовано. Установіть змінну середовища OPENROUTER_API_KEY або налаштуйте її в налаштуваннях."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "Маркер API z.ai не знайдено. Установіть apiKey у ~/.codexbar/config.json або Z_AI_API_KEY."; diff --git a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings index e528d101b9..765c9d31cb 100644 --- a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings @@ -710,6 +710,9 @@ /* Cost estimation */ "cost_header_estimated" = "Chi phí (ước tính)"; "cost_estimate_hint" = "Ước tính từ nhật ký cục bộ · có thể khác với hóa đơn của bạn"; +"codex_api_estimate_header" = "Ước tính tương đương API"; +"codex_api_estimate_hint" = "Mức dùng cục bộ × giá API công khai · không phải hóa đơn đăng ký hoặc giá trị gói"; +"codex_api_estimate_not_billed" = "không phải hóa đơn đăng ký hoặc giá trị gói"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "Không phát hiện thấy IDE JetBrains nào có Trợ lý AI. Cài đặt JetBrains IDE và bật Trợ lý AI."; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "OpenRouter API token chưa được định cấu hình. Đặt biến môi trường OPENROUTER_API_KEY hoặc định cấu hình trong Cài đặt ."; "z.ai API token not found. Set apiKey in ~/.codexbar/config.json or Z_AI_API_KEY." = "không tìm thấy z.ai API token. Đặt apiKey trong ~/.codexbar/config.json hoặc Z_AI_API_KEY."; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index 8eaebc33b0..73f1b63937 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -675,6 +675,9 @@ "macOS Tahoe can block menu bar apps in System Settings → Menu Bar → Allow in the Menu Bar. CodexBar is running, but macOS may be hiding its icon. Open Menu Bar settings and turn CodexBar on." = "macOS Tahoe 可能会在“系统设置”→“菜单栏”→“允许显示在菜单栏”中阻止菜单栏应用。CodexBar 正在运行,但 macOS 可能隐藏了它的图标。请打开菜单栏设置并启用 CodexBar。"; "cost_header_estimated" = "费用(估算)"; "cost_estimate_hint" = "根据本地日志估算 · 可能与账单不同"; +"codex_api_estimate_header" = "API 等价估算"; +"codex_api_estimate_hint" = "本地用量 × 公开 API 价格 · 不是订阅账单或套餐价值"; +"codex_api_estimate_not_billed" = "不是订阅账单或套餐价值"; "Estimated from local Codex logs for the selected account." = "根据所选账户的本地 Codex 日志估算。"; "No JetBrains IDE with AI Assistant detected. Install a JetBrains IDE and enable AI Assistant." = "未检测到启用 AI Assistant 的 JetBrains IDE。请安装 JetBrains IDE 并启用 AI Assistant。"; "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." = "未配置 OpenRouter API 令牌。请设置 OPENROUTER_API_KEY 环境变量,或在“设置”中配置。"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index 15a0a51e1a..2fca662b20 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -699,6 +699,9 @@ "macOS Tahoe can block menu bar apps in System Settings → Menu Bar → Allow in the Menu Bar. CodexBar is running, but macOS may be hiding its icon. Open Menu Bar settings and turn CodexBar on." = "macOS Tahoe 可能會在「系統設定」→「選單列」→「允許顯示在選單列」中封鎖選單列 App。CodexBar 正在執行,但 macOS 可能隱藏了它的圖示。請開啟選單列設定並啟用 CodexBar。"; "cost_header_estimated" = "費用(估算)"; "cost_estimate_hint" = "根據本機記錄估算 · 可能與帳單不同"; +"codex_api_estimate_header" = "API 等值估算"; +"codex_api_estimate_hint" = "本機用量 × 公開 API 價格 · 不是訂閱帳單或方案價值"; +"codex_api_estimate_not_billed" = "不是訂閱帳單或方案價值"; "copilot_device_code" = "裝置碼已複製到剪貼簿:%1$@\n\n請到以下網址驗證:%2$@"; "copilot_waiting_text" = "請在瀏覽器中完成登入。\n登入完成後,此視窗會自動關閉。"; "vertex_ai_login_instructions" = "要追蹤 Vertex AI 使用量,請透過 Google Cloud 進行認證。\n\n1. 開啟終端\n2. 執行:gcloud auth application-default login\n3. 依照瀏覽器提示登入\n4. 設定你的專案:gcloud config set project PROJECT_ID\n\n要現在開啟終端嗎?"; diff --git a/Sources/CodexBar/StatusItemController+CostMenuCard.swift b/Sources/CodexBar/StatusItemController+CostMenuCard.swift index 88cdb1fd23..0f5039f002 100644 --- a/Sources/CodexBar/StatusItemController+CostMenuCard.swift +++ b/Sources/CodexBar/StatusItemController+CostMenuCard.swift @@ -1,4 +1,5 @@ import AppKit +import CodexBarCore import SwiftUI private struct CostMenuCardRowView: View { @@ -32,17 +33,24 @@ extension StatusItemController { L("Cost") } + static func costMenuTitleForProvider(_ provider: UsageProvider) -> String { + provider == .codex ? L("codex_api_estimate_header") : self.costMenuTitle + } + func makeCostMenuCardItem( model: UsageMenuCardView.Model, submenu: NSMenu?, width: CGFloat) -> NSMenuItem { + let title = Self.costMenuTitleForProvider(model.provider) let tooltipLines = Self.costMenuTooltipLines(tokenUsage: model.tokenUsage) let visibleDetailLines = Self.costMenuVisibleDetailLines( + provider: model.provider, tokenUsage: model.tokenUsage, hasSubmenu: submenu != nil) guard visibleDetailLines.isEmpty == false, self.menuCardRenderingEnabledForController else { return Self.makeNativeCostMenuCardItem( + title: title, visibleDetailLines: visibleDetailLines, tooltipLines: tooltipLines, submenu: submenu) @@ -50,7 +58,7 @@ extension StatusItemController { let item = self.makeMenuCardItem( CostMenuCardRowView( - title: Self.costMenuTitle, + title: title, detailLines: visibleDetailLines, width: width), id: "menuCardCost", @@ -60,17 +68,18 @@ extension StatusItemController { submenu: submenu, submenuIndicatorAlignment: .trailing, submenuIndicatorTopPadding: 0) - item.title = Self.costMenuTitle + item.title = title item.toolTip = tooltipLines.joined(separator: "\n") return item } private static func makeNativeCostMenuCardItem( + title: String, visibleDetailLines: [String], tooltipLines: [String], submenu: NSMenu?) -> NSMenuItem { - let item = NSMenuItem(title: Self.costMenuTitle, action: nil, keyEquivalent: "") + let item = NSMenuItem(title: title, action: nil, keyEquivalent: "") item.isEnabled = true item.representedObject = "menuCardCost" item.submenu = submenu @@ -82,7 +91,9 @@ extension StatusItemController { if #available(macOS 14.4, *) { item.subtitle = visibleDetailLines.joined(separator: "\n") } else if !visibleDetailLines.isEmpty { - item.attributedTitle = Self.costMenuFallbackAttributedTitle(visibleDetailLines: visibleDetailLines) + item.attributedTitle = Self.costMenuFallbackAttributedTitle( + title: title, + visibleDetailLines: visibleDetailLines) } return item } @@ -99,10 +110,20 @@ extension StatusItemController { } static func costMenuVisibleDetailLines( + provider: UsageProvider, tokenUsage: UsageMenuCardView.Model.TokenUsageSection?, hasSubmenu: Bool) -> [String] { - guard !hasSubmenu else { return [] } + // A submenu hides the regular detail rows, so retain the provenance hint on the parent + // item. Otherwise Codex's API-equivalent estimate can be opened as a chart labelled as + // cost with no visible non-billing disclaimer. + guard !hasSubmenu else { + guard provider == .codex else { return [] } + return tokenUsage?.hintLine? + .split(separator: "\n") + .map(String.init) + .filter { !$0.isEmpty } ?? [] + } let primaryLines = ([ tokenUsage?.sessionLine, tokenUsage?.monthLine, @@ -117,9 +138,12 @@ extension StatusItemController { .filter { !$0.isEmpty } } - static func costMenuFallbackAttributedTitle(visibleDetailLines: [String]) -> NSAttributedString { + static func costMenuFallbackAttributedTitle( + title: String, + visibleDetailLines: [String]) -> NSAttributedString + { let detailText = visibleDetailLines.joined(separator: " | ") - let title = detailText.isEmpty ? self.costMenuTitle : "\(self.costMenuTitle) \(detailText)" + let title = detailText.isEmpty ? title : "\(title) \(detailText)" let attributedTitle = NSMutableAttributedString( string: title, attributes: [.font: NSFont.menuFont(ofSize: NSFont.systemFontSize)]) diff --git a/Sources/CodexBar/UsageStore+WidgetSnapshot.swift b/Sources/CodexBar/UsageStore+WidgetSnapshot.swift index 061aaa2ff4..f2a2ec2a31 100644 --- a/Sources/CodexBar/UsageStore+WidgetSnapshot.swift +++ b/Sources/CodexBar/UsageStore+WidgetSnapshot.swift @@ -91,15 +91,26 @@ extension UsageStore { providerCost: providerCost) } - private nonisolated static func widgetTokenUsageSummary( + nonisolated static func widgetTokenUsageSummary( from snapshot: CostUsageTokenSnapshot?, provider: UsageProvider) -> WidgetSnapshot.TokenUsageSummary? { guard let snapshot else { return nil } let fallbackTokens = snapshot.daily.compactMap(\.totalTokens).reduce(0, +) let monthTokensValue = snapshot.last30DaysTokens ?? (fallbackTokens > 0 ? fallbackTokens : nil) - let sessionLabel = provider == .bedrock || provider == .mistral ? "Latest billing day" : "Today" - let monthLabel = snapshot.historyLabel ?? (snapshot.historyDays == 1 ? "Today" : "\(snapshot.historyDays)d") + let sessionLabel = if provider == .bedrock || provider == .mistral { + "Latest billing day" + } else if provider == .codex { + "Today API est. · not billed" + } else { + "Today" + } + let defaultMonthLabel = snapshot.historyDays == 1 ? "Today" : "\(snapshot.historyDays)d" + let monthLabel = if provider == .codex { + "\(snapshot.historyLabel ?? defaultMonthLabel) API est. · not billed" + } else { + snapshot.historyLabel ?? defaultMonthLabel + } return WidgetSnapshot.TokenUsageSummary( sessionCostUSD: snapshot.sessionCostUSD, sessionTokens: snapshot.sessionTokens, diff --git a/Sources/CodexBarCLI/CLICostCommand.swift b/Sources/CodexBarCLI/CLICostCommand.swift index 59bd25ab1d..cc6dcea545 100644 --- a/Sources/CodexBarCLI/CLICostCommand.swift +++ b/Sources/CodexBarCLI/CLICostCommand.swift @@ -103,7 +103,10 @@ extension CodexBarCLI { useColor: Bool) -> String { let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName - let header = Self.costHeaderLine("\(name) Cost (API-rate estimate)", useColor: useColor) + let title = provider == .codex + ? "\(name) API-equivalent estimate (not billed)" + : "\(name) Cost (API-rate estimate)" + let header = Self.costHeaderLine(title, useColor: useColor) if groupBy == .project, provider == .codex { return Self.renderProjectCostText(header: header, snapshot: snapshot) } @@ -122,7 +125,7 @@ extension CodexBarCLI { "\(historyLabel): \(monthCost) · \($0) tokens" } ?? "\(historyLabel): \(monthCost)" - let hintLine = UsageFormatter.costEstimateHint(provider: provider) + let hintLine = Self.costEstimateHint(provider: provider) return [header, todayLine, monthLine, hintLine].joined(separator: "\n") } @@ -132,7 +135,7 @@ extension CodexBarCLI { var lines = [header, "Projects (\(historyLabel)):"] guard !snapshot.projects.isEmpty else { lines.append("—") - lines.append(UsageFormatter.costEstimateHint(provider: .codex)) + lines.append(Self.costEstimateHint(provider: .codex)) return lines.joined(separator: "\n") } for project in snapshot.projects { @@ -155,10 +158,16 @@ extension CodexBarCLI { } } } - lines.append(UsageFormatter.costEstimateHint(provider: .codex)) + lines.append(Self.costEstimateHint(provider: .codex)) return lines.joined(separator: "\n") } + private static func costEstimateHint(provider: UsageProvider) -> String { + provider == .codex + ? "Not a subscription bill or plan value · local usage × public API prices" + : UsageFormatter.costEstimateHint(provider: provider) + } + private static func costHeaderLine(_ header: String, useColor: Bool) -> String { guard useColor else { return header } return "\u{001B}[1;36m\(header)\u{001B}[0m" diff --git a/Sources/CodexBarWidget/CodexBarWidgetViews.swift b/Sources/CodexBarWidget/CodexBarWidgetViews.swift index 4457269111..705b3f3d17 100644 --- a/Sources/CodexBarWidget/CodexBarWidgetViews.swift +++ b/Sources/CodexBarWidget/CodexBarWidgetViews.swift @@ -209,7 +209,10 @@ enum CompactMetricFormatter { } ?? "—" let detail = entry.tokenUsage?.sessionTokens.map(WidgetFormat.tokenCount) let label = entry.tokenUsage.map { - WidgetFormat.tokenRowTitle("\($0.sessionLabel) cost", summary: $0, entryUpdatedAt: entry.updatedAt) + WidgetFormat.tokenRowTitle( + Self.costMetricLabel($0.sessionLabel, provider: entry.provider), + summary: $0, + entryUpdatedAt: entry.updatedAt) } ?? "Today cost" return CompactMetricDisplay(value: value, label: label, detail: detail) case .last30DaysCost: @@ -218,11 +221,22 @@ enum CompactMetricFormatter { } ?? "—" let detail = entry.tokenUsage?.last30DaysTokens.map(WidgetFormat.tokenCount) let label = entry.tokenUsage.map { - WidgetFormat.tokenRowTitle("\($0.last30DaysLabel) cost", summary: $0, entryUpdatedAt: entry.updatedAt) + WidgetFormat.tokenRowTitle( + Self.costMetricLabel($0.last30DaysLabel, provider: entry.provider), + summary: $0, + entryUpdatedAt: entry.updatedAt) } ?? "30d cost" return CompactMetricDisplay(value: value, label: label, detail: detail) } } + + static func costMetricLabel(_ label: String, provider: UsageProvider) -> String { + guard provider == .codex else { return "\(label) cost" } + // Existing widget timelines may predate the estimate labels. Do not leave a bare + // dollar value until the app next republishes it. + guard !label.contains("API est.") else { return label } + return "\(label) API est. · not billed" + } } private struct ProviderSwitcherRow: View { diff --git a/Tests/CodexBarTests/CLICostTests.swift b/Tests/CodexBarTests/CLICostTests.swift index 008bf2f6e5..86dd682932 100644 --- a/Tests/CodexBarTests/CLICostTests.swift +++ b/Tests/CodexBarTests/CLICostTests.swift @@ -89,7 +89,7 @@ struct CLICostTests { .replacingOccurrences(of: "\u{00A0}", with: " ") .replacingOccurrences(of: "$ ", with: "$") - #expect(output.contains("Codex Cost (API-rate estimate)")) + #expect(output.contains("Codex API-equivalent estimate (not billed)")) #expect(output.contains("Projects (Last 30 days):")) #expect(output.contains("client-a: $7.50 · 7K tokens")) #expect(output.contains("/work/client-a")) @@ -97,6 +97,7 @@ struct CLICostTests { #expect(output.contains(" - client-a: $2.25 · 2K tokens")) #expect(output.contains("/Users/test/.codex/worktrees/abcd/client-a")) #expect(output.contains("Unknown project: $2.49 · 2K tokens")) + #expect(output.contains("Not a subscription bill or plan value · local usage × public API prices")) } @Test diff --git a/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift b/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift index 66c216ea0e..f3bb1a811a 100644 --- a/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift +++ b/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift @@ -945,8 +945,15 @@ extension CodexBarWidgetProviderTests { let todayMetric = CompactMetricFormatter.display(for: entry, metric: .todayCost) let historyMetric = CompactMetricFormatter.display(for: entry, metric: .last30DaysCost) - #expect(todayMetric.label.hasPrefix("Today cost · ")) - #expect(historyMetric.label.hasPrefix("30d cost · ")) + #expect(todayMetric.label.hasPrefix("Today API est. · not billed · ")) + #expect(historyMetric.label.hasPrefix("30d API est. · not billed · ")) + #expect(CompactMetricFormatter.costMetricLabel("7d", provider: .codex) == "7d API est. · not billed") + #expect(CompactMetricFormatter.costMetricLabel("90d", provider: .codex) == "90d API est. · not billed") + #expect(CompactMetricFormatter.costMetricLabel("This month", provider: .codex) == + "This month API est. · not billed") + #expect(CompactMetricFormatter.costMetricLabel( + "This month API est. · not billed", + provider: .codex) == "This month API est. · not billed") } @Test diff --git a/Tests/CodexBarTests/CostHistoryChartMenuViewTests.swift b/Tests/CodexBarTests/CostHistoryChartMenuViewTests.swift index 13060cbe06..281bb25b54 100644 --- a/Tests/CodexBarTests/CostHistoryChartMenuViewTests.swift +++ b/Tests/CodexBarTests/CostHistoryChartMenuViewTests.swift @@ -5,6 +5,14 @@ import Testing @MainActor struct CostHistoryChartMenuViewTests { + @Test + func `Codex chart exposes the estimate disclaimer`() { + #expect( + CostHistoryChartMenuView.estimateDisclaimer(provider: .codex) + == "not a subscription bill or plan value") + #expect(CostHistoryChartMenuView.estimateDisclaimer(provider: .claude) == nil) + } + @Test @MainActor func `model breakdown keeps every item behind a bounded scrolling viewport`() { diff --git a/Tests/CodexBarTests/InlineCostHistoryDashboardLabelTests.swift b/Tests/CodexBarTests/InlineCostHistoryDashboardLabelTests.swift index 08869b079d..e594a78a1c 100644 --- a/Tests/CodexBarTests/InlineCostHistoryDashboardLabelTests.swift +++ b/Tests/CodexBarTests/InlineCostHistoryDashboardLabelTests.swift @@ -174,7 +174,7 @@ struct InlineCostHistoryDashboardLabelTests { @Test func `costHistoryInlineDashboard sets currencyCode from snapshot`() throws { let now = Date(timeIntervalSince1970: 1_700_179_200) - let metadata = try #require(ProviderDefaults.metadata[.claude]) + let metadata = try #require(ProviderDefaults.metadata[.codex]) let tokenSnapshot = CostUsageTokenSnapshot( sessionTokens: 275, sessionCostUSD: 0.25, @@ -194,7 +194,7 @@ struct InlineCostHistoryDashboardLabelTests { updatedAt: now) let model = UsageMenuCardView.Model.make(.init( - provider: .claude, + provider: .codex, metadata: metadata, snapshot: UsageSnapshot( primary: nil, @@ -218,6 +218,11 @@ struct InlineCostHistoryDashboardLabelTests { let dashboard = try #require(model.inlineUsageDashboard) #expect(dashboard.currencyCode == "USD") + #expect(dashboard.kpis[0].title == "Today · API-equivalent estimate") + #expect(dashboard.kpis[1].title == "30d · API-equivalent estimate") + #expect(dashboard.detailLines.contains("not a subscription bill or plan value")) + #expect(dashboard.detailLines.contains( + "Local usage × public API prices · not a subscription bill or plan value")) } @Test diff --git a/Tests/CodexBarTests/MenuCardModelTests.swift b/Tests/CodexBarTests/MenuCardModelTests.swift index ce5f01de36..ee844f0275 100644 --- a/Tests/CodexBarTests/MenuCardModelTests.swift +++ b/Tests/CodexBarTests/MenuCardModelTests.swift @@ -1050,7 +1050,10 @@ struct MenuCardModelTests { #expect(model.tokenUsage?.monthLine.contains("456") == true) #expect(model.tokenUsage?.monthLine.contains("tokens") == true) - #expect(model.tokenUsage?.hintLine == "Estimated from local Codex logs for the selected account.") + #expect(model.tokenUsage?.hintLine == + "Estimated from local Codex logs for the selected account.\n" + + "not a subscription bill or plan value\n" + + "Local usage × public API prices · not a subscription bill or plan value") } @Test diff --git a/Tests/CodexBarTests/StatusMenuCostMenuCardTests.swift b/Tests/CodexBarTests/StatusMenuCostMenuCardTests.swift index 5edc2432fb..122f90e86a 100644 --- a/Tests/CodexBarTests/StatusMenuCostMenuCardTests.swift +++ b/Tests/CodexBarTests/StatusMenuCostMenuCardTests.swift @@ -8,7 +8,7 @@ import Testing @Suite(.serialized) struct StatusMenuCostMenuCardTests { @Test - func `cost menu shows no detail lines`() { + func `cost menu keeps the estimate hint beside a history submenu`() { let tokenUsage = UsageMenuCardView.Model.TokenUsageSection( sessionLine: "Today: $74.83 - 87M tokens", monthLine: "Last 30 days: $4,279.64 - 5.7B tokens", @@ -17,12 +17,19 @@ struct StatusMenuCostMenuCardTests { errorCopyText: nil) let visibleLines = StatusItemController.costMenuVisibleDetailLines( + provider: .codex, tokenUsage: tokenUsage, hasSubmenu: true) - #expect(visibleLines == []) + #expect(visibleLines == ["Costs are estimated from local usage."]) + #expect(StatusItemController.costMenuVisibleDetailLines( + provider: .claude, + tokenUsage: tokenUsage, + hasSubmenu: true) == []) - let fallbackTitle = StatusItemController.costMenuFallbackAttributedTitle(visibleDetailLines: visibleLines) - #expect(fallbackTitle.string == "Cost") + let fallbackTitle = StatusItemController.costMenuFallbackAttributedTitle( + title: "API-equivalent estimate", + visibleDetailLines: visibleLines) + #expect(fallbackTitle.string == "API-equivalent estimate Costs are estimated from local usage.") } @Test @@ -35,6 +42,7 @@ struct StatusMenuCostMenuCardTests { errorCopyText: nil) let visibleLines = StatusItemController.costMenuVisibleDetailLines( + provider: .codex, tokenUsage: tokenUsage, hasSubmenu: false) #expect(visibleLines == [ @@ -43,7 +51,9 @@ struct StatusMenuCostMenuCardTests { "Cost refresh failed.", ]) - let fallbackTitle = StatusItemController.costMenuFallbackAttributedTitle(visibleDetailLines: visibleLines) + let fallbackTitle = StatusItemController.costMenuFallbackAttributedTitle( + title: "API-equivalent estimate", + visibleDetailLines: visibleLines) #expect(fallbackTitle.string.contains("Today: $74.83 - 87M tokens")) #expect(fallbackTitle.string.contains("Last 30 days: $4,279.64 - 5.7B tokens")) #expect(fallbackTitle.string.contains("Cost refresh failed.")) @@ -139,11 +149,17 @@ struct StatusMenuCostMenuCardTests { #expect(view is any MenuCardMeasuring) #expect(abs(view.frame.width - width) <= 0.5) - #expect(item.title == "Cost") + #expect(item.title == "API-equivalent estimate") #expect(item.toolTip?.contains("$52,431.09") == true) #expect(item.submenu == nil) } + @Test + func `cost menu title distinguishes Codex estimates from billing-backed cost`() { + #expect(StatusItemController.costMenuTitleForProvider(.codex) == "API-equivalent estimate") + #expect(StatusItemController.costMenuTitleForProvider(.mistral) == "Cost") + } + private func makeSettings() -> SettingsStore { let suite = "StatusMenuCostMenuCardTests-\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! diff --git a/Tests/CodexBarTests/StatusMenuHostedSubmenuRefreshTests.swift b/Tests/CodexBarTests/StatusMenuHostedSubmenuRefreshTests.swift index cab2d718ba..b20fa9e22f 100644 --- a/Tests/CodexBarTests/StatusMenuHostedSubmenuRefreshTests.swift +++ b/Tests/CodexBarTests/StatusMenuHostedSubmenuRefreshTests.swift @@ -181,7 +181,7 @@ struct StatusMenuHostedSubmenuRefreshTests { let costItem = try #require(menu.items.first { ($0.representedObject as? String) == "menuCardCost" }) #expect(costItem.view == nil) - #expect(costItem.title == StatusItemController.costMenuTitle) + #expect(costItem.title == StatusItemController.costMenuTitleForProvider(.claude)) #expect(costItem.isEnabled) let submenu = try #require(costItem.submenu) #expect(submenu.items.first?.representedObject as? String == StatusItemController.costHistoryChartID) diff --git a/Tests/CodexBarTests/StatusMenuLocalizationRefreshTests.swift b/Tests/CodexBarTests/StatusMenuLocalizationRefreshTests.swift index 3516a51fcc..d6a190ea99 100644 --- a/Tests/CodexBarTests/StatusMenuLocalizationRefreshTests.swift +++ b/Tests/CodexBarTests/StatusMenuLocalizationRefreshTests.swift @@ -75,7 +75,8 @@ struct StatusMenuLocalizationRefreshTests { controller.menuRefreshEnabledOverrideForTesting = true #expect(Self.switcherButtons(in: menu).first?.title == "Resumen") - #expect(menu.items.first(where: { $0.representedObject as? String == "menuCardCost" })?.title == "Coste") + let initialCostTitle = menu.items.first(where: { $0.representedObject as? String == "menuCardCost" })?.title + #expect(initialCostTitle == "Estimación equivalente a la API") let initialSwitcher = menu.items.first?.view as? ProviderSwitcherView let initialSwitcherID = initialSwitcher.map(ObjectIdentifier.init) @@ -98,7 +99,8 @@ struct StatusMenuLocalizationRefreshTests { #expect(rebuildCount == 1) let updatedSwitcher = menu.items.first?.view as? ProviderSwitcherView #expect(Self.switcherButtons(in: menu).first?.title == "Overview") - #expect(menu.items.first(where: { $0.representedObject as? String == "menuCardCost" })?.title == "Cost") + let updatedCostTitle = menu.items.first(where: { $0.representedObject as? String == "menuCardCost" })?.title + #expect(updatedCostTitle == "API-equivalent estimate") if let initialSwitcherID, let updatedSwitcher { #expect(initialSwitcherID != ObjectIdentifier(updatedSwitcher)) } diff --git a/Tests/CodexBarTests/WidgetSnapshotTests.swift b/Tests/CodexBarTests/WidgetSnapshotTests.swift index dcd79751f7..9264145c19 100644 --- a/Tests/CodexBarTests/WidgetSnapshotTests.swift +++ b/Tests/CodexBarTests/WidgetSnapshotTests.swift @@ -1,8 +1,29 @@ import Foundation import Testing +@testable import CodexBar @testable import CodexBarCore struct WidgetSnapshotTests { + @Test + func `Codex widget labels disclose API estimates`() { + let snapshot = CostUsageTokenSnapshot( + sessionTokens: 1200, + sessionCostUSD: 1.25, + last30DaysTokens: 9000, + last30DaysCostUSD: 9.99, + historyDays: 30, + daily: [], + updatedAt: Date(timeIntervalSince1970: 0)) + + let codex = UsageStore.widgetTokenUsageSummary(from: snapshot, provider: .codex) + let claude = UsageStore.widgetTokenUsageSummary(from: snapshot, provider: .claude) + + #expect(codex?.sessionLabel == "Today API est. · not billed") + #expect(codex?.last30DaysLabel == "30d API est. · not billed") + #expect(claude?.sessionLabel == "Today") + #expect(claude?.last30DaysLabel == "30d") + } + @Test func `widget snapshot round trip`() throws { let entry = WidgetSnapshot.ProviderEntry(