Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions apps/manager-server/internal/repository/usageevent/stream_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
43 changes: 36 additions & 7 deletions apps/manager-server/internal/usage/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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),
Expand Down
12 changes: 12 additions & 0 deletions apps/manager-server/internal/usage/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "-"
}
Expand Down Expand Up @@ -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 = "-"
}
Expand Down
24 changes: 22 additions & 2 deletions apps/manager-server/internal/usage/import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,21 +518,41 @@ 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",
"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)
}
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const t = ((key: string, options?: Record<string, unknown>) => {
'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',
Expand Down Expand Up @@ -185,6 +187,8 @@ describe('RealtimeEventsPanel', () => {
executorType: 'codex',
reasoningEffort: 'medium',
serviceTier: 'priority',
requestServiceTier: 'priority',
responseServiceTier: 'default',
cacheReadTokens: 4,
cacheCreationTokens: 1,
failStatusCode: 429,
Expand All @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
Expand Down Expand Up @@ -861,9 +863,14 @@ export function RealtimeEventsPanel({
) : (
<span className={styles.mutedCell}>-</span>
)}
{serviceTier !== '-' ? (
{requestServiceTier !== '-' ? (
<small>{`${t('monitoring.request_service_tier_short')}: ${requestServiceTier}`}</small>
) : serviceTier !== '-' ? (
<small>{`${shortLabel(t, 'monitoring.service_tier_short', 'monitoring.service_tier')}: ${serviceTier}`}</small>
) : null}
{responseServiceTier !== '-' ? (
<small>{`${t('monitoring.response_service_tier_short')}: ${responseServiceTier}`}</small>
) : null}
</div>
</td>
<td>
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/features/monitoring/model/eventRows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand Down
15 changes: 14 additions & 1 deletion apps/web/src/features/monitoring/model/eventRows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -217,6 +226,8 @@ export const buildEventRows = (
totalCost,
reasoningEffort,
serviceTier,
requestServiceTier,
responseServiceTier,
executorType,
failStatusCode: normalizedFailStatusCode,
failSummary,
Expand Down Expand Up @@ -247,6 +258,8 @@ export const buildEventRows = (
projectId,
reasoningEffort,
serviceTier,
requestServiceTier,
responseServiceTier,
executorType,
normalizedFailStatusCode,
failSummary,
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/features/monitoring/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ export type MonitoringEventRow = {
totalCost: number;
reasoningEffort?: string;
serviceTier?: string;
requestServiceTier?: string;
responseServiceTier?: string;
executorType?: string;
failStatusCode?: number | null;
failSummary?: string;
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "Код ошибки",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "失败状态码",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/i18n/locales/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "失敗狀態碼",
Expand Down
Loading
Loading