Skip to content

Commit 543bf2c

Browse files
committed
refactor: extract shared AI span helpers, SpanMetricRow, parsePeriodToMs; remove duplicate GenerationRow type
1 parent dc7f1ad commit 543bf2c

File tree

11 files changed

+142
-273
lines changed

11 files changed

+142
-273
lines changed

apps/webapp/app/components/runs/v3/PromptSpanDetails.tsx

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { lazy, Suspense, useState } from "react";
22
import { CodeBlock } from "~/components/code/CodeBlock";
33
import { Header3 } from "~/components/primitives/Headers";
4-
54
import { TextLink } from "~/components/primitives/TextLink";
5+
import { tryPrettyJson } from "./ai/aiHelpers";
6+
import { SpanMetricRow as MetricRow } from "./ai/SpanMetricRow";
67
import { useEnvironment } from "~/hooks/useEnvironment";
78
import { useOrganization } from "~/hooks/useOrganizations";
89
import { useProject } from "~/hooks/useProject";
@@ -145,19 +146,3 @@ export function PromptSpanDetails({ promptData }: { promptData: PromptSpanData }
145146
);
146147
}
147148

148-
function MetricRow({ label, value }: { label: string; value: React.ReactNode }) {
149-
return (
150-
<div className="grid h-7 grid-cols-[1fr_auto] items-center gap-4 rounded-sm px-1.5 transition odd:bg-charcoal-750/40 @[28rem]:grid-cols-[8rem_1fr] hover:bg-white/[0.04]">
151-
<span className="text-text-dimmed">{label}</span>
152-
<span className="text-right text-text-bright @[28rem]:text-left">{value}</span>
153-
</div>
154-
);
155-
}
156-
157-
function tryPrettyJson(raw: string): string {
158-
try {
159-
return JSON.stringify(JSON.parse(raw), null, 2);
160-
} catch {
161-
return raw;
162-
}
163-
}

apps/webapp/app/components/runs/v3/ai/AIEmbedSpanDetails.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Header3 } from "~/components/primitives/Headers";
22
import { Paragraph } from "~/components/primitives/Paragraph";
3+
import { formatDuration } from "./aiHelpers";
4+
import { SpanMetricRow as MetricRow } from "./SpanMetricRow";
35

46
export type AIEmbedData = {
57
model: string;
@@ -62,19 +64,3 @@ export function AIEmbedSpanDetails({ data }: { data: AIEmbedData }) {
6264
);
6365
}
6466

65-
function MetricRow({ label, value }: { label: string; value: React.ReactNode }) {
66-
return (
67-
<div className="grid h-7 grid-cols-[1fr_auto] items-center gap-4 rounded-sm px-1.5 transition odd:bg-charcoal-750/40 @[28rem]:grid-cols-[8rem_1fr] hover:bg-white/[0.04]">
68-
<span className="text-text-dimmed">{label}</span>
69-
<span className="text-right text-text-bright @[28rem]:text-left">{value}</span>
70-
</div>
71-
);
72-
}
73-
74-
function formatDuration(ms: number): string {
75-
if (ms < 1000) return `${Math.round(ms)}ms`;
76-
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
77-
const mins = Math.floor(ms / 60_000);
78-
const secs = ((ms % 60_000) / 1000).toFixed(0);
79-
return `${mins}m ${secs}s`;
80-
}

apps/webapp/app/components/runs/v3/ai/AISpanDetails.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { Header3 } from "~/components/primitives/Headers";
55
import { Paragraph } from "~/components/primitives/Paragraph";
66
import { TabButton, TabContainer } from "~/components/primitives/Tabs";
77
import { TextLink } from "~/components/primitives/TextLink";
8+
import { tryPrettyJson } from "./aiHelpers";
9+
import { SpanMetricRow as PromptMetricRow } from "./SpanMetricRow";
810
import { useEnvironment } from "~/hooks/useEnvironment";
911
import { useOrganization } from "~/hooks/useOrganizations";
1012
import { useProject } from "~/hooks/useProject";
@@ -293,22 +295,6 @@ function PromptTab({
293295
);
294296
}
295297

296-
function PromptMetricRow({ label, value }: { label: string; value: React.ReactNode }) {
297-
return (
298-
<div className="grid h-7 grid-cols-[1fr_auto] items-center gap-4 rounded-sm px-1.5 transition odd:bg-charcoal-750/40 @[28rem]:grid-cols-[8rem_1fr] hover:bg-white/[0.04]">
299-
<span className="text-text-dimmed">{label}</span>
300-
<span className="text-right text-text-bright @[28rem]:text-left">{value}</span>
301-
</div>
302-
);
303-
}
304-
305-
function tryPrettyJson(value: string): string {
306-
try {
307-
return JSON.stringify(JSON.parse(value), null, 2);
308-
} catch {
309-
return value;
310-
}
311-
}
312298

313299
// ---------------------------------------------------------------------------
314300
// Helpers

apps/webapp/app/components/runs/v3/ai/AIToolCallSpanDetails.tsx

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Header3 } from "~/components/primitives/Headers";
22
import { CodeBlock } from "~/components/code/CodeBlock";
33
import { TruncatedCopyableValue } from "~/components/primitives/TruncatedCopyableValue";
4+
import { formatDuration, tryPrettyJson } from "./aiHelpers";
5+
import { SpanMetricRow as MetricRow } from "./SpanMetricRow";
46

57
export type AIToolCallData = {
68
toolName: string;
@@ -72,27 +74,3 @@ export function AIToolCallSpanDetails({ data }: { data: AIToolCallData }) {
7274
);
7375
}
7476

75-
function MetricRow({ label, value }: { label: string; value: React.ReactNode }) {
76-
return (
77-
<div className="grid h-7 grid-cols-[1fr_auto] items-center gap-4 rounded-sm px-1.5 transition odd:bg-charcoal-750/40 @[28rem]:grid-cols-[8rem_1fr] hover:bg-white/[0.04]">
78-
<span className="text-text-dimmed">{label}</span>
79-
<span className="text-right text-text-bright @[28rem]:text-left">{value}</span>
80-
</div>
81-
);
82-
}
83-
84-
function formatDuration(ms: number): string {
85-
if (ms < 1000) return `${Math.round(ms)}ms`;
86-
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
87-
const mins = Math.floor(ms / 60_000);
88-
const secs = ((ms % 60_000) / 1000).toFixed(0);
89-
return `${mins}m ${secs}s`;
90-
}
91-
92-
function tryPrettyJson(value: string): string {
93-
try {
94-
return JSON.stringify(JSON.parse(value), null, 2);
95-
} catch {
96-
return value;
97-
}
98-
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { ReactNode } from "react";
2+
3+
export function SpanMetricRow({ label, value }: { label: string; value: ReactNode }) {
4+
return (
5+
<div className="grid h-7 grid-cols-[1fr_auto] items-center gap-4 rounded-sm px-1.5 transition odd:bg-charcoal-750/40 @[28rem]:grid-cols-[8rem_1fr] hover:bg-white/[0.04]">
6+
<span className="text-text-dimmed">{label}</span>
7+
<span className="text-right text-text-bright @[28rem]:text-left">{value}</span>
8+
</div>
9+
);
10+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Shared primitive helpers for AI span data extraction
2+
3+
export function rec(v: unknown): Record<string, unknown> {
4+
return v && typeof v === "object" ? (v as Record<string, unknown>) : {};
5+
}
6+
7+
export function str(v: unknown): string | undefined {
8+
return typeof v === "string" ? v : undefined;
9+
}
10+
11+
export function num(v: unknown): number | undefined {
12+
return typeof v === "number" ? v : undefined;
13+
}
14+
15+
export function tryPrettyJson(value: string): string {
16+
try {
17+
return JSON.stringify(JSON.parse(value), null, 2);
18+
} catch {
19+
return value;
20+
}
21+
}
22+
23+
export function formatDuration(ms: number): string {
24+
if (ms < 1000) return `${Math.round(ms)}ms`;
25+
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
26+
const mins = Math.floor(ms / 60_000);
27+
const secs = ((ms % 60_000) / 1000).toFixed(0);
28+
return `${mins}m ${secs}s`;
29+
}
30+
31+
/**
32+
* Parse provider metadata from a JSON string.
33+
* Handles Anthropic, Azure, OpenAI, Gateway, and OpenRouter formats.
34+
*/
35+
export function parseProviderMetadata(
36+
raw: unknown
37+
): {
38+
serviceTier?: string;
39+
resolvedProvider?: string;
40+
gatewayCost?: string;
41+
responseId?: string;
42+
} | undefined {
43+
if (typeof raw !== "string") return undefined;
44+
try {
45+
const parsed = JSON.parse(raw) as Record<string, unknown>;
46+
if (!parsed || typeof parsed !== "object") return undefined;
47+
48+
let serviceTier: string | undefined;
49+
let resolvedProvider: string | undefined;
50+
let gatewayCost: string | undefined;
51+
let responseId: string | undefined;
52+
53+
// Anthropic: { anthropic: { usage: { service_tier: "standard" } } }
54+
const anthropic = rec(parsed.anthropic);
55+
serviceTier = str(rec(anthropic.usage).service_tier);
56+
57+
// Azure/OpenAI: { azure: { serviceTier: "default" } } or { openai: { serviceTier: "..." } }
58+
const openai = rec(parsed.openai);
59+
if (!serviceTier) {
60+
serviceTier = str(rec(parsed.azure).serviceTier) ?? str(openai.serviceTier);
61+
}
62+
63+
// OpenAI response ID
64+
responseId = str(openai.responseId);
65+
66+
// Gateway: { gateway: { routing: { finalProvider, resolvedProvider }, cost } }
67+
const gateway = rec(parsed.gateway);
68+
const routing = rec(gateway.routing);
69+
resolvedProvider = str(routing.finalProvider) ?? str(routing.resolvedProvider);
70+
gatewayCost = str(gateway.cost);
71+
72+
// OpenRouter: { openrouter: { provider: "xAI" } }
73+
if (!resolvedProvider) {
74+
resolvedProvider = str(rec(parsed.openrouter).provider);
75+
}
76+
77+
if (!serviceTier && !resolvedProvider && !gatewayCost && !responseId) return undefined;
78+
return { serviceTier, resolvedProvider, gatewayCost, responseId };
79+
} catch {
80+
return undefined;
81+
}
82+
}
83+
84+
/**
85+
* Extract user-defined telemetry metadata, coercing non-string values.
86+
* Skips the "prompt" key which is handled separately.
87+
*/
88+
export function extractTelemetryMetadata(raw: unknown): Record<string, string> | undefined {
89+
if (!raw || typeof raw !== "object") return undefined;
90+
91+
const result: Record<string, string> = {};
92+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
93+
if (key === "prompt") continue;
94+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
95+
result[key] = String(value);
96+
}
97+
}
98+
99+
return Object.keys(result).length > 0 ? result : undefined;
100+
}

apps/webapp/app/components/runs/v3/ai/extractAISpanData.ts

Lines changed: 1 addition & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { rec, str, num, parseProviderMetadata, extractTelemetryMetadata } from "./aiHelpers";
12
import type { AISpanData, DisplayItem, ToolDefinition, ToolUse } from "./types";
23

34
/**
@@ -97,22 +98,6 @@ export function extractAISpanData(
9798
};
9899
}
99100

100-
// ---------------------------------------------------------------------------
101-
// Primitive helpers
102-
// ---------------------------------------------------------------------------
103-
104-
function rec(v: unknown): Record<string, unknown> {
105-
return v && typeof v === "object" ? (v as Record<string, unknown>) : {};
106-
}
107-
108-
function str(v: unknown): string | undefined {
109-
return typeof v === "string" ? v : undefined;
110-
}
111-
112-
function num(v: unknown): number | undefined {
113-
return typeof v === "number" ? v : undefined;
114-
}
115-
116101
// ---------------------------------------------------------------------------
117102
// Message → DisplayItem transformation
118103
// ---------------------------------------------------------------------------
@@ -431,49 +416,6 @@ function parseToolDefinitions(raw: unknown): ToolDefinition[] | undefined {
431416
}
432417
}
433418

434-
// ---------------------------------------------------------------------------
435-
// Provider metadata (service tier, inference geo, etc.)
436-
// ---------------------------------------------------------------------------
437-
438-
function parseProviderMetadata(
439-
raw: unknown
440-
): { serviceTier?: string; resolvedProvider?: string; gatewayCost?: string } | undefined {
441-
if (typeof raw !== "string") return undefined;
442-
try {
443-
const parsed = JSON.parse(raw) as Record<string, unknown>;
444-
if (!parsed || typeof parsed !== "object") return undefined;
445-
446-
let serviceTier: string | undefined;
447-
let resolvedProvider: string | undefined;
448-
let gatewayCost: string | undefined;
449-
450-
// Anthropic: { anthropic: { usage: { service_tier: "standard" } } }
451-
const anthropic = rec(parsed.anthropic);
452-
serviceTier = str(rec(anthropic.usage).service_tier);
453-
454-
// Azure/OpenAI: { azure: { serviceTier: "default" } } or { openai: { serviceTier: "..." } }
455-
if (!serviceTier) {
456-
serviceTier = str(rec(parsed.azure).serviceTier) ?? str(rec(parsed.openai).serviceTier);
457-
}
458-
459-
// Gateway: { gateway: { routing: { finalProvider, resolvedProvider }, cost } }
460-
const gateway = rec(parsed.gateway);
461-
const routing = rec(gateway.routing);
462-
resolvedProvider = str(routing.finalProvider) ?? str(routing.resolvedProvider);
463-
gatewayCost = str(gateway.cost);
464-
465-
// OpenRouter: { openrouter: { provider: "xAI" } }
466-
if (!resolvedProvider) {
467-
resolvedProvider = str(rec(parsed.openrouter).provider);
468-
}
469-
470-
if (!serviceTier && !resolvedProvider && !gatewayCost) return undefined;
471-
return { serviceTier, resolvedProvider, gatewayCost };
472-
} catch {
473-
return undefined;
474-
}
475-
}
476-
477419
// ---------------------------------------------------------------------------
478420
// Tool choice parsing
479421
// ---------------------------------------------------------------------------
@@ -508,19 +450,3 @@ function countMessages(raw: unknown): number | undefined {
508450
}
509451
}
510452

511-
// ---------------------------------------------------------------------------
512-
// Telemetry metadata
513-
// ---------------------------------------------------------------------------
514-
515-
function extractTelemetryMetadata(raw: unknown): Record<string, string> | undefined {
516-
if (!raw || typeof raw !== "object") return undefined;
517-
518-
const result: Record<string, string> = {};
519-
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
520-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
521-
result[key] = String(value);
522-
}
523-
}
524-
525-
return Object.keys(result).length > 0 ? result : undefined;
526-
}

0 commit comments

Comments
 (0)