From b2c2a782499bee81abb4c05f29c546207f5f79a1 Mon Sep 17 00:00:00 2001 From: Lukin Date: Sun, 5 Jul 2026 00:12:53 +0800 Subject: [PATCH] Record serving subscription provider accounts Co-Authored-By: Codex --- apps/admin/src/lib/api/requests.test.ts | 21 ++++ apps/admin/src/lib/api/requests.ts | 73 +++++++++++++- .../src/lib/components/DecisionChain.svelte | 16 ++- .../src/lib/components/DecisionChain.test.ts | 9 ++ .../src/lib/components/RequestsTable.svelte | 98 +++++++++++-------- .../src/lib/components/RequestsTable.test.ts | 2 + apps/admin/src/locales/en.json | 5 + apps/admin/src/locales/ja.json | 5 + apps/admin/src/locales/ko.json | 5 + apps/admin/src/locales/zh-hans.json | 5 + apps/admin/src/locales/zh-hant.json | 5 + .../routes/keys/[keyId]/key-detail.test.ts | 2 + .../routes/requests/[traceId]/+page.svelte | 41 +++++--- .../src/routes/requests/requests.test.ts | 23 +++-- apps/gateway/src/routes/admin/admin.test.ts | 1 + apps/gateway/src/routes/chat.ts | 4 +- apps/gateway/src/routes/gemini.ts | 4 + apps/gateway/src/routes/image-telemetry.ts | 1 + apps/gateway/src/routes/messages-pipeline.ts | 3 + apps/gateway/src/routes/messages.ts | 5 + .../src/routes/payload-capture.test.ts | 1 + apps/gateway/src/routes/responses.ts | 4 + .../src/runtime/serving-account.test.ts | 23 +++++ apps/gateway/src/runtime/serving-account.ts | 15 +++ implementation-notes.md | 33 +++---- packages/core/src/executor/attempt-record.ts | 17 +++- packages/core/src/routing/route-request.ts | 4 + packages/core/src/signals/aggregate.test.ts | 1 + packages/core/src/signals/collector.test.ts | 1 + packages/core/src/store/ports.test.ts | 1 + .../core/src/store/sqlite/telemetry.test.ts | 1 + .../core/src/store/store-contract.test.ts | 1 + packages/core/src/telemetry/decision.test.ts | 23 +++++ packages/core/src/telemetry/decision.ts | 49 +++++++--- packages/shared/src/decision/schema.test.ts | 13 +++ packages/shared/src/decision/schema.ts | 10 ++ packages/shared/src/index.ts | 2 + 37 files changed, 432 insertions(+), 95 deletions(-) diff --git a/apps/admin/src/lib/api/requests.test.ts b/apps/admin/src/lib/api/requests.test.ts index ed351d4d..89ebda5b 100644 --- a/apps/admin/src/lib/api/requests.test.ts +++ b/apps/admin/src/lib/api/requests.test.ts @@ -39,6 +39,8 @@ function rawRecord(overrides: Record = {}): Record = {}): Record { expect(row.decided_by).toBe('eval'); }); + it('surfaces the served provider and final subscription account on list rows', () => { + const row = toListItem(rawRecord()); + expect(row.served_provider).toBe('anthropic'); + expect(row.serving_account).toEqual({ provider_id: 'anthropic', account: 'claude-team-a' }); + + const nonSubscription = toListItem(rawRecord({ serving_account: null })); + expect(nonSubscription.served_provider).toBe('anthropic'); + expect(nonSubscription.serving_account).toBeNull(); + }); + it('falls back to "—" for key_prefix when the record carries none (never plaintext)', () => { const row = toListItem(rawRecord({ key_prefix: null })); expect(row.key_prefix).toBe('—'); @@ -136,6 +149,8 @@ describe('toDetail', () => { expect(d.key_name).toBe('Production backend'); expect(d.requested_model).toBe('gpt-4o'); expect(d.final_model).toBe('premium'); // served model alias + expect(d.served_provider).toBe('anthropic'); + expect(d.serving_account).toEqual({ provider_id: 'anthropic', account: 'claude-team-a' }); expect(d.lane).toBe('coding'); expect(d.status).toBe('ok'); expect(d.latency_ms).toBe(460); @@ -254,6 +269,12 @@ describe('toDetail', () => { }); // The ok attempt carries no detail. expect(d.provider_attempts[1]?.error_detail ?? null).toBeNull(); + expect(d.provider_attempts[1]?.provider).toBe('anthropic'); + expect(d.provider_attempts[1]?.provider_model).toBe('claude-x'); + expect(d.provider_attempts[1]?.serving_account).toEqual({ + provider_id: 'anthropic', + account: 'claude-team-a', + }); }); it('maps a missing error_detail to null (legacy records)', () => { diff --git a/apps/admin/src/lib/api/requests.ts b/apps/admin/src/lib/api/requests.ts index 30112897..1d9299d0 100644 --- a/apps/admin/src/lib/api/requests.ts +++ b/apps/admin/src/lib/api/requests.ts @@ -46,6 +46,8 @@ export interface RequestListItem { complexity: string; decided_by: 'rules' | 'eval' | 'default' | 'fallback'; // decision layer (classification stage) lane: string; + served_provider: string | null; + serving_account: ServingAccountView | null; final_model: string | null; fallback_count: number; // execution fallback count (provider attempts - 1) status: 'ok' | 'error'; @@ -80,6 +82,11 @@ export interface TokenUsageView { total: number | null; // input + output when present; null when neither is measured } +export interface ServingAccountView { + provider_id: string; + account: string; +} + // Redacted per-attempt upstream failure detail (admin-debug-error-detail). // Present only for an attempt that failed at the upstream; the backend has // already key-scrubbed `provider_raw` (Principle 7), so this is safe to display. @@ -91,7 +98,9 @@ export interface AttemptErrorDetail { export interface ProviderAttempt { model: string; - provider: string; + provider: string | null; + provider_model: string | null; + serving_account: ServingAccountView | null; outcome: 'success' | 'error' | 'timeout' | 'rate_limited' | 'circuit_open' | 'skipped'; skip_reason?: string; latency_ms: number; @@ -111,6 +120,8 @@ export interface RequestDetail { key_prefix: string | null; key_name: string | null; requested_model: string | null; // what the client asked for + served_provider: string | null; // concrete provider that served the request + serving_account: ServingAccountView | null; // final subscription account, if any final_model: string | null; // the served model alias (null = no provider served) lane: string; // selected lane ('' on a legacy record) status: 'ok' | 'error'; @@ -183,6 +194,7 @@ interface RawAttempt { error_class?: string | null; latency_ms?: number; cost_usd?: number | null; + provider_name?: string | null; provider_model?: string | null; // Redacted upstream failure detail (admin-debug-error-detail). Null/absent on // ok/skipped rows and on legacy records. @@ -254,6 +266,10 @@ interface RawDecisionRecord { status?: string; error_reason?: string | null; }; + serving_account?: { + provider_id?: string | null; + account?: string | null; + } | null; } const BASE = '/admin/api/requests'; @@ -329,6 +345,42 @@ function attemptOutcome(a: RawAttempt): ProviderAttempt['outcome'] { return 'error'; } +function nonEmptyString(v: unknown): string | null { + return typeof v === 'string' && v.length > 0 ? v : null; +} + +function aliasPrefix(alias: string | null): string | null { + if (!alias) return null; + const slash = alias.indexOf('/'); + return slash > 0 ? alias.slice(0, slash) : null; +} + +function normalizeServingAccount(raw: RawDecisionRecord): ServingAccountView | null { + const providerId = nonEmptyString(raw.serving_account?.provider_id); + const account = nonEmptyString(raw.serving_account?.account); + return providerId && account ? { provider_id: providerId, account } : null; +} + +function successfulAttempt(raw: RawDecisionRecord, attempts: RawAttempt[]): RawAttempt | null { + const finalAlias = nonEmptyString(raw.final?.model_alias); + if (finalAlias) { + const exact = attempts.find( + (a) => a.status === 'ok' && a.skipped !== true && a.alias === finalAlias, + ); + if (exact) return exact; + } + return attempts.find((a) => a.status === 'ok' && a.skipped !== true) ?? null; +} + +function servedProvider(raw: RawDecisionRecord, attempts: RawAttempt[]): string | null { + const attempt = successfulAttempt(raw, attempts); + return ( + nonEmptyString(attempt?.provider_name) ?? + aliasPrefix(nonEmptyString(raw.final?.model_alias)) ?? + aliasPrefix(nonEmptyString(attempt?.alias)) + ); +} + // Project the recorded `usage` block -> the UI token view. Reads only the four // recorded counts (Principle 1 — never recomputes upstream figures); DERIVES // `nonCached` (input − cached, clamped ≥0) and `total` (input + output) for the @@ -376,6 +428,7 @@ export function computeTtfbMs(raw: RawDecisionRecord): number | null { // record does not carry are derived or safely defaulted; NEVER fabricated. export function toListItem(raw: RawDecisionRecord): RequestListItem { const attempts = Array.isArray(raw.provider_attempts) ? raw.provider_attempts : []; + const account = normalizeServingAccount(raw); const status: RequestListItem['status'] = raw.final?.status === 'error' ? 'error' : 'ok'; const errorClass = status === 'error' @@ -404,6 +457,8 @@ export function toListItem(raw: RawDecisionRecord): RequestListItem { complexity: raw.classifier?.complexity ?? '', decided_by: normalizeDecidedBy(raw.classifier?.decided_by), lane: raw.lane?.selected_lane ?? '', + served_provider: servedProvider(raw, attempts), + serving_account: account, final_model: raw.final?.model_alias ?? null, // Prefer the recorded value; fall back to deriving from attempts for legacy // records (Principle 5: execution-stage count, distinct from decided_by). @@ -473,6 +528,7 @@ export function toDetail(raw: RawDecisionRecord): RequestDetail { const completion = sumCost(attempts); const status = raw.final?.status === 'error' ? 'error' : 'ok'; const evalCacheHit = raw.classifier?.eval_cache_hit ?? null; + const account = normalizeServingAccount(raw); return { trace_id: String(raw.trace_id ?? raw.request_id ?? ''), // Same source as the list "Time" column (created_at, flattened by the detail @@ -486,6 +542,8 @@ export function toDetail(raw: RawDecisionRecord): RequestDetail { typeof raw.key_prefix === 'string' && raw.key_prefix.length > 0 ? raw.key_prefix : null, key_name: typeof raw.key_name === 'string' && raw.key_name.length > 0 ? raw.key_name : null, requested_model: raw.requested_model ?? null, + served_provider: servedProvider(raw, attempts), + serving_account: account, final_model: raw.final?.model_alias ?? null, lane: raw.lane?.selected_lane ?? '', status, @@ -518,7 +576,18 @@ export function toDetail(raw: RawDecisionRecord): RequestDetail { lane_candidates: Array.isArray(raw.lane?.candidate_chain) ? raw.lane.candidate_chain : [], provider_attempts: attempts.map((a) => ({ model: String(a.alias ?? ''), - provider: String(a.provider_model ?? a.alias ?? ''), + provider: + nonEmptyString(a.provider_name) ?? + aliasPrefix(nonEmptyString(a.alias)) ?? + nonEmptyString(a.provider_model), + provider_model: nonEmptyString(a.provider_model), + serving_account: + account && + a.status === 'ok' && + a.skipped !== true && + nonEmptyString(a.alias) === nonEmptyString(raw.final?.model_alias) + ? account + : null, outcome: attemptOutcome(a), skip_reason: a.skip_reason ?? undefined, latency_ms: typeof a.latency_ms === 'number' ? a.latency_ms : 0, diff --git a/apps/admin/src/lib/components/DecisionChain.svelte b/apps/admin/src/lib/components/DecisionChain.svelte index 1718b624..16b20a16 100644 --- a/apps/admin/src/lib/components/DecisionChain.svelte +++ b/apps/admin/src/lib/components/DecisionChain.svelte @@ -74,6 +74,10 @@ return String(value); } } + + function accountTitle(account: RequestDetail['provider_attempts'][number]['serving_account']): string { + return account ? `${account.provider_id}/${account.account}` : ''; + }
@@ -236,8 +240,18 @@ {#each detail.provider_attempts as a, i (i)}
  • - {a.provider} + {a.provider ?? '—'} + {#if a.serving_account} + {a.serving_account.account} + {/if} {a.model} + {#if a.provider_model && a.provider_model !== a.model} + {$t('wire:')} {a.provider_model} + {/if} {$t(attemptCodeLabel(a.outcome))} diff --git a/apps/admin/src/lib/components/DecisionChain.test.ts b/apps/admin/src/lib/components/DecisionChain.test.ts index d982c5f7..3c7e3086 100644 --- a/apps/admin/src/lib/components/DecisionChain.test.ts +++ b/apps/admin/src/lib/components/DecisionChain.test.ts @@ -19,6 +19,8 @@ function detail(overrides: Partial = {}): RequestDetail { key_prefix: 'helm_live_ab12', key_name: null, requested_model: 'gpt-4o', + served_provider: 'anthropic', + serving_account: { provider_id: 'anthropic', account: 'claude-team-a' }, final_model: 'claude-x', lane: 'premium', status: 'ok', @@ -45,6 +47,8 @@ function detail(overrides: Partial = {}): RequestDetail { { model: 'gpt-x', provider: 'openai', + provider_model: 'gpt-x', + serving_account: null, outcome: 'error', latency_ms: 120, error_class: 'upstream_error', @@ -57,6 +61,8 @@ function detail(overrides: Partial = {}): RequestDetail { { model: 'claude-x', provider: 'anthropic', + provider_model: 'claude-x', + serving_account: { provider_id: 'anthropic', account: 'claude-team-a' }, outcome: 'success', latency_ms: 340, error_detail: null, @@ -64,6 +70,8 @@ function detail(overrides: Partial = {}): RequestDetail { { model: 'small-x', provider: 'local', + provider_model: 'small-x', + serving_account: null, outcome: 'skipped', skip_reason: 'capability_unsatisfiable', latency_ms: 0, @@ -237,6 +245,7 @@ describe('DecisionChain', () => { expect(rows[0]).toHaveTextContent(/error/i); expect(rows[0]).toHaveTextContent('120'); expect(rows[1]).toHaveTextContent(/success/i); + expect(rows[1]).toHaveTextContent('claude-team-a'); expect(rows[2]).toHaveTextContent(/skipped/i); // Codes render as human labels; the raw code is preserved in the title tooltip. expect(rows[2]).toHaveTextContent('No compatible model'); diff --git a/apps/admin/src/lib/components/RequestsTable.svelte b/apps/admin/src/lib/components/RequestsTable.svelte index 40b5645d..f410fdee 100644 --- a/apps/admin/src/lib/components/RequestsTable.svelte +++ b/apps/admin/src/lib/components/RequestsTable.svelte @@ -58,6 +58,10 @@ return 'badge-neutral'; } } + + function accountTitle(account: RequestListItem['serving_account']): string | undefined { + return account ? `${account.provider_id}/${account.account}` : undefined; + }