diff --git a/apps/manager-server/internal/repository/usageevent/repository.go b/apps/manager-server/internal/repository/usageevent/repository.go index 703c2b6de..e05730743 100644 --- a/apps/manager-server/internal/repository/usageevent/repository.go +++ b/apps/manager-server/internal/repository/usageevent/repository.go @@ -107,9 +107,12 @@ func (r *repository) InsertBatch(ctx context.Context, events []model.UsageEvent) if event.RequestServiceTier == "" { event.RequestServiceTier = event.ServiceTier } - if event.ResponseServiceTier != "" { - event.ServiceTier = event.ResponseServiceTier - } + event.ServiceTier = usage.EffectiveServiceTier(usage.CacheInputContext{ + ExecutorType: event.ExecutorType, + Provider: event.Provider, + ProviderSnapshot: event.AuthProviderSnapshot, + AuthType: event.AuthType, + }, event.RequestServiceTier, event.ServiceTier, event.ResponseServiceTier) failed := 0 if event.Failed { failed = 1 diff --git a/apps/manager-server/internal/repository/usageevent/stream_test.go b/apps/manager-server/internal/repository/usageevent/stream_test.go index f767b3732..2d28c6ed1 100644 --- a/apps/manager-server/internal/repository/usageevent/stream_test.go +++ b/apps/manager-server/internal/repository/usageevent/stream_test.go @@ -66,6 +66,44 @@ func TestWriteCompatibleUsageMatchesBuildPayload(t *testing.T) { } } +func TestInsertBatchSelectsServiceTierByProviderSemantics(t *testing.T) { + db, err := sqliterepo.Open(filepath.Join(t.TempDir(), "usage.sqlite")) + if err != nil { + t.Fatalf("open database: %v", err) + } + t.Cleanup(func() { _ = db.Close() }) + repo := New(db) + + codex := streamTestEvent("codex-tier", 100, "POST /v1/responses", "gpt-5.4") + codex.ExecutorType = "codex" + codex.RequestServiceTier = "priority" + codex.ResponseServiceTier = "default" + codex.ServiceTier = "priority" + nonCodex := streamTestEvent("openai-tier", 200, "POST /v1/responses", "gpt-5.4") + nonCodex.Provider = "openai-compatible" + nonCodex.RequestServiceTier = "priority" + nonCodex.ResponseServiceTier = "default" + nonCodex.ServiceTier = "priority" + + if _, err := repo.InsertBatch(context.Background(), []usage.Event{codex, nonCodex}); err != nil { + t.Fatalf("insert events: %v", err) + } + recent, err := repo.ListRecent(context.Background(), 2) + if err != nil { + t.Fatalf("list recent: %v", err) + } + byHash := make(map[string]usage.Event, len(recent)) + for _, event := range recent { + byHash[event.EventHash] = event + } + if event := byHash["codex-tier"]; event.ServiceTier != "priority" || event.RequestServiceTier != "priority" || event.ResponseServiceTier != "default" { + t.Fatalf("codex tiers = %q/%q/%q", event.ServiceTier, event.RequestServiceTier, event.ResponseServiceTier) + } + if event := byHash["openai-tier"]; event.ServiceTier != "default" || event.RequestServiceTier != "priority" || event.ResponseServiceTier != "default" { + t.Fatalf("non-Codex tiers = %q/%q/%q", event.ServiceTier, event.RequestServiceTier, event.ResponseServiceTier) + } +} + func TestWriteExportJSONLUsesRecentLimitAndAscendingKeysetOrder(t *testing.T) { db, err := sqliterepo.Open(filepath.Join(t.TempDir(), "usage.sqlite")) if err != nil { diff --git a/apps/manager-server/internal/usage/event.go b/apps/manager-server/internal/usage/event.go index da407c8cf..d8ba68a7c 100644 --- a/apps/manager-server/internal/usage/event.go +++ b/apps/manager-server/internal/usage/event.go @@ -173,11 +173,40 @@ type CacheInputContext struct { ExecutorType string Provider string ProviderSnapshot string + AuthType string ResolvedModel string RequestedModel string DisplayModel string } +// EffectiveServiceTier returns the tier used for billing and aggregation. +// Codex reports default/auto even when Fast Mode was requested, so Codex uses +// the request tier. Other providers retain response-tier precedence. +func EffectiveServiceTier(context CacheInputContext, requestTier, legacyTier, responseTier string) string { + identity := strings.ToLower(strings.Join([]string{ + context.ExecutorType, + context.Provider, + context.ProviderSnapshot, + context.AuthType, + }, " ")) + if strings.Contains(identity, "codex") { + if requestTier != "" { + return requestTier + } + if legacyTier != "" { + return legacyTier + } + return responseTier + } + if responseTier != "" { + return responseTier + } + if legacyTier != "" { + return legacyTier + } + return requestTier +} + type RawCacheAccountingHints struct { ExplicitMode string ExplicitTotal int64 @@ -501,22 +530,22 @@ func NormalizeRaw(raw []byte) (Event, error) { } provider := readString(record, "provider", "type", "auth_type", "authType") executorType := readString(record, "executor_type", "executorType") + authType := readString(record, "auth_type", "authType") requestServiceTier := readString(record, "request_service_tier", "requestServiceTier", "service_tier", "serviceTier") responseServiceTier := readString(record, "response_service_tier", "responseServiceTier") - serviceTier := responseServiceTier - if serviceTier == "" { - serviceTier = requestServiceTier - } authProviderSnapshot := readString(record, "auth_provider_snapshot", "authProviderSnapshot") - cacheAccounting := NormalizeCacheAccounting(CacheInputContext{ + usageContext := CacheInputContext{ ExplicitMode: cacheInputModeFromRecord(record), ExecutorType: executorType, Provider: provider, ProviderSnapshot: authProviderSnapshot, + AuthType: authType, ResolvedModel: resolvedModel, RequestedModel: requestedModel, DisplayModel: model, - }, inputTokens, cachedTokens, cacheTokens, cacheReadTokens, cacheCreationTokens) + } + serviceTier := EffectiveServiceTier(usageContext, requestServiceTier, "", responseServiceTier) + cacheAccounting := NormalizeCacheAccounting(usageContext, inputTokens, cachedTokens, cacheTokens, cacheReadTokens, cacheCreationTokens) if totalTokens <= 0 { totalTokens = cacheAccounting.TotalInputTokens + maxInt64(outputTokens, 0) + maxInt64(reasoningTokens, 0) } @@ -533,7 +562,7 @@ func NormalizeRaw(raw []byte) (Event, error) { Endpoint: endpoint, Method: method, Path: path, - AuthType: readString(record, "auth_type", "authType"), + AuthType: authType, AuthIndex: authIndex, Source: source, SourceHash: hashString(sourceRaw), diff --git a/apps/manager-server/internal/usage/import.go b/apps/manager-server/internal/usage/import.go index 40ec7ec0d..804c0c318 100644 --- a/apps/manager-server/internal/usage/import.go +++ b/apps/manager-server/internal/usage/import.go @@ -484,6 +484,12 @@ func eventFromExportedRecord(record map[string]any) (Event, bool, error) { RawJSON: rawJSON, CreatedAtMS: readInt(record, "created_at_ms", "createdAtMs"), } + event.ServiceTier = EffectiveServiceTier(CacheInputContext{ + ExecutorType: event.ExecutorType, + Provider: event.Provider, + ProviderSnapshot: event.AuthProviderSnapshot, + AuthType: event.AuthType, + }, event.RequestServiceTier, event.ServiceTier, event.ResponseServiceTier) if event.Endpoint == "" { event.Endpoint = "-" } @@ -669,6 +675,12 @@ func eventFromLegacyDetail( RawJSON: rawJSON, CreatedAtMS: now, } + event.ServiceTier = EffectiveServiceTier(CacheInputContext{ + ExecutorType: event.ExecutorType, + Provider: event.Provider, + ProviderSnapshot: event.AuthProviderSnapshot, + AuthType: event.AuthType, + }, event.RequestServiceTier, event.ServiceTier, event.ResponseServiceTier) if event.Model == "" { event.Model = "-" } diff --git a/apps/manager-server/internal/usage/import_test.go b/apps/manager-server/internal/usage/import_test.go index 7145ca007..9df5b763d 100644 --- a/apps/manager-server/internal/usage/import_test.go +++ b/apps/manager-server/internal/usage/import_test.go @@ -518,14 +518,15 @@ func TestNormalizeRawHandlesCurrentCPAGPT56QueuePayloadWithoutCacheMode(t *testi event.NormalizedCacheReadTokens != 30 || event.NormalizedCacheCreationTokens != 17 { t.Fatalf("normalized cache accounting = %#v", event) } - if event.RequestServiceTier != "priority" || event.ResponseServiceTier != "default" || event.ServiceTier != "default" { + if event.RequestServiceTier != "priority" || event.ResponseServiceTier != "default" || event.ServiceTier != "priority" { t.Fatalf("service tiers = %q/%q/%q", event.RequestServiceTier, event.ResponseServiceTier, event.ServiceTier) } } -func TestNormalizeRawPrefersResponseServiceTier(t *testing.T) { +func TestNormalizeRawPrefersRequestServiceTierForCodex(t *testing.T) { payload := `{ "timestamp": "2026-07-10T00:00:00Z", + "executor_type": "codex", "model": "gpt-5.6-sol", "service_tier": "priority", "request_service_tier": "priority", @@ -533,6 +534,25 @@ func TestNormalizeRawPrefersResponseServiceTier(t *testing.T) { "tokens": {"input_tokens": 1, "total_tokens": 1} }` + event, err := NormalizeRaw([]byte(payload)) + if err != nil { + t.Fatalf("normalize raw: %v", err) + } + if event.RequestServiceTier != "priority" || event.ResponseServiceTier != "default" || event.ServiceTier != "priority" { + t.Fatalf("service tiers = %q/%q/%q", event.RequestServiceTier, event.ResponseServiceTier, event.ServiceTier) + } +} + +func TestNormalizeRawPrefersResponseServiceTierForNonCodex(t *testing.T) { + payload := `{ + "timestamp": "2026-07-10T00:00:00Z", + "provider": "openai-compatible", + "model": "gpt-5.4", + "request_service_tier": "priority", + "response_service_tier": "default", + "tokens": {"input_tokens": 1, "total_tokens": 1} + }` + event, err := NormalizeRaw([]byte(payload)) if err != nil { t.Fatalf("normalize raw: %v", err) diff --git a/apps/web/src/features/monitoring/components/RealtimeEventsPanel.test.tsx b/apps/web/src/features/monitoring/components/RealtimeEventsPanel.test.tsx index d2a89fede..62f962489 100644 --- a/apps/web/src/features/monitoring/components/RealtimeEventsPanel.test.tsx +++ b/apps/web/src/features/monitoring/components/RealtimeEventsPanel.test.tsx @@ -49,6 +49,8 @@ const t = ((key: string, options?: Record) => { 'monitoring.result_failed': 'Failed', 'monitoring.result_success': 'Success', 'monitoring.service_tier_short': 'Tier', + 'monitoring.request_service_tier_short': 'Requested tier', + 'monitoring.response_service_tier_short': 'Reported tier', 'monitoring.this_call_cost': 'Cost', 'monitoring.this_call_usage': 'Usage', 'monitoring.ttft_short': 'TTFT', @@ -185,6 +187,8 @@ describe('RealtimeEventsPanel', () => { executorType: 'codex', reasoningEffort: 'medium', serviceTier: 'priority', + requestServiceTier: 'priority', + responseServiceTier: 'default', cacheReadTokens: 4, cacheCreationTokens: 1, failStatusCode: 429, @@ -198,7 +202,8 @@ describe('RealtimeEventsPanel', () => { expect(markup).not.toContain('>Executor: codex<'); expect(markup).not.toContain('Executor: codex'); expect(markup).toContain('medium'); - expect(markup).toContain('Tier: priority'); + expect(markup).toContain('Requested tier: priority'); + expect(markup).toContain('Reported tier: default'); expect(markup).toContain('client-gpt'); expect(markup).toContain('gpt-5.4'); expect(markup).not.toContain('Resolved'); diff --git a/apps/web/src/features/monitoring/components/RealtimeEventsPanel.tsx b/apps/web/src/features/monitoring/components/RealtimeEventsPanel.tsx index 6d19fd843..6aa163e88 100644 --- a/apps/web/src/features/monitoring/components/RealtimeEventsPanel.tsx +++ b/apps/web/src/features/monitoring/components/RealtimeEventsPanel.tsx @@ -813,6 +813,8 @@ export function RealtimeEventsPanel({ row.resolvedModel.trim() !== row.model; const reasoningEffort = formatOptionalText(row.reasoningEffort); const serviceTier = formatOptionalText(row.serviceTier); + const requestServiceTier = formatOptionalText(row.requestServiceTier); + const responseServiceTier = formatOptionalText(row.responseServiceTier); const failureDetails = buildFailureDetails(row, t, locale); const failureTooltipId = failureDetails ? `${tooltipIdPrefix}-failure-tooltip-${row.id}` @@ -861,9 +863,14 @@ export function RealtimeEventsPanel({ ) : ( - )} - {serviceTier !== '-' ? ( + {requestServiceTier !== '-' ? ( + {`${t('monitoring.request_service_tier_short')}: ${requestServiceTier}`} + ) : serviceTier !== '-' ? ( {`${shortLabel(t, 'monitoring.service_tier_short', 'monitoring.service_tier')}: ${serviceTier}`} ) : null} + {responseServiceTier !== '-' ? ( + {`${t('monitoring.response_service_tier_short')}: ${responseServiceTier}`} + ) : null} diff --git a/apps/web/src/features/monitoring/model/eventRows.test.ts b/apps/web/src/features/monitoring/model/eventRows.test.ts index 25fdc5208..cc391aab8 100644 --- a/apps/web/src/features/monitoring/model/eventRows.test.ts +++ b/apps/web/src/features/monitoring/model/eventRows.test.ts @@ -89,13 +89,18 @@ describe('buildEventRows', () => { const [row] = buildRows({ executor_type: 'codex', service_tier: 'priority', + request_service_tier: 'priority', + response_service_tier: 'default', reasoning_effort: 'medium', }); expect(row.executorType).toBe('codex'); expect(row.serviceTier).toBe('priority'); + expect(row.requestServiceTier).toBe('priority'); + expect(row.responseServiceTier).toBe('default'); expect(row.searchText).toContain('codex'); expect(row.searchText).toContain('priority'); + expect(row.searchText).toContain('default'); expect(row.searchText).toContain('medium'); }); diff --git a/apps/web/src/features/monitoring/model/eventRows.ts b/apps/web/src/features/monitoring/model/eventRows.ts index d0e1df582..7870165f4 100644 --- a/apps/web/src/features/monitoring/model/eventRows.ts +++ b/apps/web/src/features/monitoring/model/eventRows.ts @@ -129,7 +129,16 @@ export const buildEventRows = ( const sourceKey = sourceMeta.identityKey || `source:${sourceLabel}`; const taskKey = `${detail.timestamp}|${sourceKey}|${authIndex}`; const reasoningEffort = readString(detail.reasoning_effort ?? detail.reasoningEffort); - const serviceTier = readString(detail.service_tier ?? detail.serviceTier); + const requestServiceTier = readString( + detail.request_service_tier ?? detail.requestServiceTier + ); + const responseServiceTier = readString( + detail.response_service_tier ?? detail.responseServiceTier + ); + const serviceTier = + requestServiceTier || + readString(detail.service_tier ?? detail.serviceTier) || + responseServiceTier; const executorType = readString(detail.executor_type ?? detail.executorType); const failStatusCodeRaw = detail.fail_status_code ?? detail.failStatusCode; const failStatusCode = @@ -217,6 +226,8 @@ export const buildEventRows = ( totalCost, reasoningEffort, serviceTier, + requestServiceTier, + responseServiceTier, executorType, failStatusCode: normalizedFailStatusCode, failSummary, @@ -247,6 +258,8 @@ export const buildEventRows = ( projectId, reasoningEffort, serviceTier, + requestServiceTier, + responseServiceTier, executorType, normalizedFailStatusCode, failSummary, diff --git a/apps/web/src/features/monitoring/model/types.ts b/apps/web/src/features/monitoring/model/types.ts index 3005768ea..2469f4c2a 100644 --- a/apps/web/src/features/monitoring/model/types.ts +++ b/apps/web/src/features/monitoring/model/types.ts @@ -183,6 +183,8 @@ export type MonitoringEventRow = { totalCost: number; reasoningEffort?: string; serviceTier?: string; + requestServiceTier?: string; + responseServiceTier?: string; executorType?: string; failStatusCode?: number | null; failSummary?: string; diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index 0548b717f..e7cdb80b5 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -1773,6 +1773,8 @@ "reasoning_effort_short": "Effort", "service_tier": "Service Tier", "service_tier_short": "Tier", + "request_service_tier_short": "Requested tier", + "response_service_tier_short": "Reported tier", "executor_type": "Executor Type", "executor_type_short": "Executor", "fail_status_code": "Failure Status Code", diff --git a/apps/web/src/i18n/locales/ru.json b/apps/web/src/i18n/locales/ru.json index 37a096944..cec5f53a2 100644 --- a/apps/web/src/i18n/locales/ru.json +++ b/apps/web/src/i18n/locales/ru.json @@ -1773,6 +1773,8 @@ "reasoning_effort_short": "Уровень", "service_tier": "Уровень сервиса", "service_tier_short": "Сервис", + "request_service_tier_short": "Запрошенный уровень", + "response_service_tier_short": "Уровень ответа", "executor_type": "Тип исполнителя", "executor_type_short": "Исполнитель", "fail_status_code": "Код ошибки", diff --git a/apps/web/src/i18n/locales/zh-CN.json b/apps/web/src/i18n/locales/zh-CN.json index 5a412e964..fa37357f8 100644 --- a/apps/web/src/i18n/locales/zh-CN.json +++ b/apps/web/src/i18n/locales/zh-CN.json @@ -1773,6 +1773,8 @@ "reasoning_effort_short": "强度", "service_tier": "服务等级", "service_tier_short": "等级", + "request_service_tier_short": "请求等级", + "response_service_tier_short": "响应等级", "executor_type": "执行器类型", "executor_type_short": "执行器", "fail_status_code": "失败状态码", diff --git a/apps/web/src/i18n/locales/zh-TW.json b/apps/web/src/i18n/locales/zh-TW.json index 0013f0a04..d5f2c8958 100644 --- a/apps/web/src/i18n/locales/zh-TW.json +++ b/apps/web/src/i18n/locales/zh-TW.json @@ -1773,6 +1773,8 @@ "reasoning_effort_short": "強度", "service_tier": "服務等級", "service_tier_short": "等級", + "request_service_tier_short": "請求等級", + "response_service_tier_short": "回應等級", "executor_type": "執行器類型", "executor_type_short": "執行器", "fail_status_code": "失敗狀態碼", diff --git a/apps/web/src/utils/usage.test.ts b/apps/web/src/utils/usage.test.ts index 6097437b7..786f9f358 100644 --- a/apps/web/src/utils/usage.test.ts +++ b/apps/web/src/utils/usage.test.ts @@ -107,6 +107,44 @@ describe('usage source candidates', () => { }); describe('usage detail collection', () => { + it('preserves Codex identity from legacy auth type metadata', () => { + const usageData = { + apis: { + 'POST /v1/responses': { + models: { + 'gpt-5.4': { + details: [ + { + timestamp: '2026-07-20T00:00:00Z', + source: 'codex-account', + auth_index: 'auth-1', + auth_type: 'codex', + request_service_tier: 'priority', + response_service_tier: 'default', + tokens: { input_tokens: 100_000 }, + failed: false, + }, + ], + }, + }, + }, + }, + }; + + for (const detail of [ + collectUsageDetails(usageData)[0], + collectUsageDetailsWithEndpoint(usageData)[0], + ]) { + expect(detail.auth_type).toBe('codex'); + expect(detail.provider).toBe('codex'); + expect( + calculateCost(detail, { + 'gpt-5.4': { prompt: 2.5, completion: 5, cache: 1 }, + }) + ).toBeCloseTo(0.5); + } + }); + it('copies project id snapshots into normalized usage details', () => { const usageData = { apis: { @@ -689,11 +727,40 @@ describe('calculateCost model price preference', () => { expect(cost).toBeCloseTo(0.5); }); - it('uses the response service tier for billing', () => { + it('uses the requested service tier for Codex billing', () => { + const cost = calculateCost( + { + tokens: { input_tokens: 100_000 }, + __modelName: 'gpt-5.4', + executor_type: 'codex', + request_service_tier: 'priority', + response_service_tier: 'default', + }, + { 'gpt-5.4': { prompt: 2.5, completion: 5, cache: 1 } } + ); + expect(cost).toBeCloseTo(0.5); + }); + + it('recognizes Codex OAuth from auth type metadata', () => { + const cost = calculateCost( + { + tokens: { input_tokens: 100_000 }, + __modelName: 'gpt-5.4', + auth_type: 'codex', + request_service_tier: 'priority', + response_service_tier: 'default', + }, + { 'gpt-5.4': { prompt: 2.5, completion: 5, cache: 1 } } + ); + expect(cost).toBeCloseTo(0.5); + }); + + it('keeps the response service tier for non-Codex billing', () => { const cost = calculateCost( { tokens: { input_tokens: 100_000 }, __modelName: 'gpt-5.4', + provider: 'openai-compatible', request_service_tier: 'priority', response_service_tier: 'default', }, diff --git a/apps/web/src/utils/usage.ts b/apps/web/src/utils/usage.ts index 45065218d..85f2be17d 100644 --- a/apps/web/src/utils/usage.ts +++ b/apps/web/src/utils/usage.ts @@ -132,6 +132,8 @@ export interface UsageDetail { authProjectIdSnapshot?: string; auth_snapshot_at_ms?: number; authSnapshotAtMs?: number; + auth_type?: string; + authType?: string; reasoning_effort?: string; reasoningEffort?: string; service_tier?: string; @@ -820,12 +822,15 @@ export function collectUsageDetails(usageData: unknown): UsageDetail[] { auth_snapshot_at_ms: toPositiveNumber( detailRaw.auth_snapshot_at_ms ?? detailRaw.authSnapshotAtMs ), + auth_type: readDetailString(detailRaw.auth_type ?? detailRaw.authType), reasoning_effort: readDetailString( detailRaw.reasoning_effort ?? detailRaw.reasoningEffort ), service_tier: readDetailString(detailRaw.service_tier ?? detailRaw.serviceTier), executor_type: readDetailString(detailRaw.executor_type ?? detailRaw.executorType), - provider: readDetailString(detailRaw.provider), + provider: readDetailString( + detailRaw.provider ?? detailRaw.type ?? detailRaw.auth_type ?? detailRaw.authType + ), requested_model: readDetailString( detailRaw.requested_model ?? detailRaw.requestedModel ?? detailRaw.alias ), @@ -946,12 +951,15 @@ export function collectUsageDetailsWithEndpoint(usageData: unknown): UsageDetail auth_snapshot_at_ms: toPositiveNumber( detailRaw.auth_snapshot_at_ms ?? detailRaw.authSnapshotAtMs ), + auth_type: readDetailString(detailRaw.auth_type ?? detailRaw.authType), reasoning_effort: readDetailString( detailRaw.reasoning_effort ?? detailRaw.reasoningEffort ), service_tier: readDetailString(detailRaw.service_tier ?? detailRaw.serviceTier), executor_type: readDetailString(detailRaw.executor_type ?? detailRaw.executorType), - provider: readDetailString(detailRaw.provider), + provider: readDetailString( + detailRaw.provider ?? detailRaw.type ?? detailRaw.auth_type ?? detailRaw.authType + ), requested_model: readDetailString( detailRaw.requested_model ?? detailRaw.requestedModel ?? detailRaw.alias ), @@ -1056,6 +1064,13 @@ export function calculateCost( | 'requestServiceTier' | 'response_service_tier' | 'responseServiceTier' + | 'executor_type' + | 'executorType' + | 'provider' + | 'auth_provider_snapshot' + | 'authProviderSnapshot' + | 'auth_type' + | 'authType' >, modelPrices: Record ): number { @@ -1120,13 +1135,31 @@ export function calculateCost( inputMultiplier + (completionTokens / TOKENS_PER_PRICE_UNIT) * completionPrice * outputMultiplier; - const serviceTier = - detail.response_service_tier ?? - detail.responseServiceTier ?? - detail.service_tier ?? - detail.serviceTier ?? - detail.request_service_tier ?? - detail.requestServiceTier; + const identity = [ + detail.executor_type, + detail.executorType, + detail.provider, + detail.auth_provider_snapshot, + detail.authProviderSnapshot, + detail.auth_type, + detail.authType, + ] + .filter(Boolean) + .join(' ') + .toLowerCase(); + const serviceTier = identity.includes('codex') + ? detail.request_service_tier || + detail.requestServiceTier || + detail.service_tier || + detail.serviceTier || + detail.response_service_tier || + detail.responseServiceTier + : detail.response_service_tier || + detail.responseServiceTier || + detail.service_tier || + detail.serviceTier || + detail.request_service_tier || + detail.requestServiceTier; let multiplier = getServiceTierMultiplier(behaviorModel, serviceTier); if (longContext && ['priority', 'fast'].includes(String(serviceTier ?? '').toLowerCase())) { multiplier = 1;