Skip to content

Commit 7ae61df

Browse files
committed
fix(trace): extract normalizeToolId to client-safe module, consolidate log-details formatting utils
1 parent ad5f50f commit 7ae61df

5 files changed

Lines changed: 100 additions & 137 deletions

File tree

apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx

Lines changed: 6 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ import {
2222
import { AgentSkillsIcon, WorkflowIcon } from '@/components/icons'
2323
import { cn } from '@/lib/core/utils/cn'
2424
import type { TraceSpan } from '@/lib/logs/types'
25+
import {
26+
formatTokensSummary,
27+
formatTps,
28+
formatTtft,
29+
parseTime,
30+
} from '@/app/workspace/[workspaceId]/logs/components/log-details/utils'
2531
import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config'
2632
import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config'
2733
import { getBlock, getBlockByToolName } from '@/blocks'
@@ -59,53 +65,12 @@ function useSetToggle() {
5965
)
6066
}
6167

62-
/**
63-
* Formats a token count with locale-aware thousands separators.
64-
* Returns `undefined` for missing or non-positive counts so callers can
65-
* filter them out before rendering.
66-
*/
67-
function formatTokenCount(value: number | undefined): string | undefined {
68-
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined
69-
return value.toLocaleString('en-US')
70-
}
71-
72-
/**
73-
* Builds a compact, dot-separated token summary for a span:
74-
* `"1,234 in · 567 out · 1,801 total"` with cache/reasoning appended when
75-
* present. Returns `undefined` when the span has no meaningful token data.
76-
*/
77-
function formatTokensSummary(tokens: TraceSpan['tokens']): string | undefined {
78-
if (!tokens) return undefined
79-
const parts: string[] = []
80-
const input = formatTokenCount(tokens.input)
81-
const output = formatTokenCount(tokens.output)
82-
const total = formatTokenCount(tokens.total)
83-
const cacheRead = formatTokenCount(tokens.cacheRead)
84-
const cacheWrite = formatTokenCount(tokens.cacheWrite)
85-
const reasoning = formatTokenCount(tokens.reasoning)
86-
if (input) parts.push(`${input} in`)
87-
if (cacheRead) parts.push(`${cacheRead} cached`)
88-
if (cacheWrite) parts.push(`${cacheWrite} cache write`)
89-
if (output) parts.push(`${output} out`)
90-
if (reasoning) parts.push(`${reasoning} reasoning`)
91-
if (total) parts.push(`${total} total`)
92-
return parts.length > 0 ? parts.join(' · ') : undefined
93-
}
94-
95-
/**
96-
* Formats a USD cost value for display. Shows `<$0.0001` for non-zero sub-cent
97-
* amounts so the user sees it was counted.
98-
*/
9968
function formatCostAmount(value: number | undefined): string | undefined {
10069
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined
10170
if (value < 0.0001) return '<$0.0001'
10271
return `$${value.toFixed(4)}`
10372
}
10473

105-
/**
106-
* Builds a compact cost summary: `"$0.0023 · $0.0001 in · $0.0022 out"`.
107-
* Falls back to whichever parts are present.
108-
*/
10974
function formatCostSummary(cost: TraceSpan['cost']): string | undefined {
11075
if (!cost) return undefined
11176
const parts: string[] = []
@@ -118,36 +83,6 @@ function formatCostSummary(cost: TraceSpan['cost']): string | undefined {
11883
return parts.length > 0 ? parts.join(' · ') : undefined
11984
}
12085

121-
/**
122-
* Derives tokens-per-second from output tokens over segment duration.
123-
* Returns `undefined` when inputs are missing or non-positive.
124-
*/
125-
function formatTps(outputTokens: number | undefined, durationMs: number): string | undefined {
126-
if (typeof outputTokens !== 'number' || !(outputTokens > 0)) return undefined
127-
if (!(durationMs > 0)) return undefined
128-
const tps = Math.round(outputTokens / (durationMs / 1000))
129-
if (!(tps > 0)) return undefined
130-
return `${tps.toLocaleString('en-US')} tok/s`
131-
}
132-
133-
/**
134-
* Formats time-to-first-token. Uses `ms` below 1000, `s` above.
135-
*/
136-
function formatTtft(ms: number | undefined): string | undefined {
137-
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) return undefined
138-
if (ms < 1000) return `${Math.round(ms)}ms`
139-
return `${(ms / 1000).toFixed(2)}s`
140-
}
141-
142-
/**
143-
* Parses a time value to milliseconds
144-
*/
145-
function parseTime(value?: string | number | null): number {
146-
if (!value) return 0
147-
const ms = typeof value === 'number' ? value : new Date(value).getTime()
148-
return Number.isFinite(ms) ? ms : 0
149-
}
150-
15186
/**
15287
* Checks if a span or any of its descendants has an error (any error).
15388
*/

apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,18 @@ import { AgentSkillsIcon, WorkflowIcon } from '@/components/icons'
3333
import { dollarsToCredits } from '@/lib/billing/credits/conversion'
3434
import { cn } from '@/lib/core/utils/cn'
3535
import type { TraceSpan } from '@/lib/logs/types'
36+
import {
37+
formatTokenCount,
38+
formatTps,
39+
formatTtft,
40+
parseTime,
41+
} from '@/app/workspace/[workspaceId]/logs/components/log-details/utils'
3642
import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config'
3743
import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config'
3844
import { getBlock, getBlockByToolName } from '@/blocks'
3945
import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'
4046
import { PROVIDER_DEFINITIONS } from '@/providers/models'
41-
import { normalizeToolId } from '@/tools'
47+
import { normalizeToolId } from '@/tools/normalize'
4248

4349
const DEFAULT_BLOCK_COLOR = '#6b7280'
4450
const DEFAULT_TREE_PANE_WIDTH = 360
@@ -64,15 +70,6 @@ interface BlockAppearance {
6470
bgColor: string
6571
}
6672

67-
/**
68-
* Parses a timestamp or numeric ms into milliseconds since epoch.
69-
*/
70-
function parseTime(value?: string | number | null): number {
71-
if (!value) return 0
72-
const ms = typeof value === 'number' ? value : new Date(value).getTime()
73-
return Number.isFinite(ms) ? ms : 0
74-
}
75-
7673
/**
7774
* Whether a span type represents a loop or parallel iteration container.
7875
*/
@@ -187,31 +184,13 @@ function iconColorClass(bgColor: string): string {
187184
return r * 299 + g * 587 + b * 114 > 160_000 ? 'text-[#111111]' : 'text-white'
188185
}
189186

190-
function formatTokenCount(value: number | undefined): string | undefined {
191-
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined
192-
return value.toLocaleString('en-US')
193-
}
194-
195187
function formatCostAmount(value: number | undefined): string | undefined {
196188
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined
197189
const credits = dollarsToCredits(value)
198190
if (credits <= 0) return '<1 credit'
199191
return `${credits.toLocaleString('en-US')} ${credits === 1 ? 'credit' : 'credits'}`
200192
}
201193

202-
function formatTtft(ms: number | undefined): string | undefined {
203-
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) return undefined
204-
if (ms < 1000) return `${Math.round(ms)}ms`
205-
return `${(ms / 1000).toFixed(2)}s`
206-
}
207-
208-
function formatTps(outputTokens: number | undefined, durationMs: number): string | undefined {
209-
if (typeof outputTokens !== 'number' || !(outputTokens > 0)) return undefined
210-
if (!(durationMs > 0)) return undefined
211-
const tps = Math.round(outputTokens / (durationMs / 1000))
212-
return tps > 0 ? `${tps.toLocaleString('en-US')} tok/s` : undefined
213-
}
214-
215194
function getDisplayName(span: TraceSpan): string {
216195
if (span.type?.toLowerCase() === 'tool') return normalizeToolId(span.name)
217196
return span.name
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { TraceSpan } from '@/lib/logs/types'
2+
3+
export function parseTime(value?: string | number | null): number {
4+
if (!value) return 0
5+
const ms = typeof value === 'number' ? value : new Date(value).getTime()
6+
return Number.isFinite(ms) ? ms : 0
7+
}
8+
9+
export function formatTokenCount(value: number | undefined): string | undefined {
10+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined
11+
return value.toLocaleString('en-US')
12+
}
13+
14+
export function formatTtft(ms: number | undefined): string | undefined {
15+
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) return undefined
16+
if (ms < 1000) return `${Math.round(ms)}ms`
17+
return `${(ms / 1000).toFixed(2)}s`
18+
}
19+
20+
export function formatTps(
21+
outputTokens: number | undefined,
22+
durationMs: number
23+
): string | undefined {
24+
if (typeof outputTokens !== 'number' || !(outputTokens > 0)) return undefined
25+
if (!(durationMs > 0)) return undefined
26+
const tps = Math.round(outputTokens / (durationMs / 1000))
27+
return tps > 0 ? `${tps.toLocaleString('en-US')} tok/s` : undefined
28+
}
29+
30+
export function formatTokensSummary(tokens: TraceSpan['tokens']): string | undefined {
31+
if (!tokens) return undefined
32+
const parts: string[] = []
33+
const input = formatTokenCount(tokens.input)
34+
const output = formatTokenCount(tokens.output)
35+
const total = formatTokenCount(tokens.total)
36+
const cacheRead = formatTokenCount(tokens.cacheRead)
37+
const cacheWrite = formatTokenCount(tokens.cacheWrite)
38+
const reasoning = formatTokenCount(tokens.reasoning)
39+
if (input) parts.push(`${input} in`)
40+
if (cacheRead) parts.push(`${cacheRead} cached`)
41+
if (cacheWrite) parts.push(`${cacheWrite} cache write`)
42+
if (output) parts.push(`${output} out`)
43+
if (reasoning) parts.push(`${reasoning} reasoning`)
44+
if (total) parts.push(`${total} total`)
45+
return parts.length > 0 ? parts.join(' · ') : undefined
46+
}

apps/sim/tools/index.ts

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -550,44 +550,7 @@ async function applyHostedKeyCostToResult(
550550
}
551551
}
552552

553-
/**
554-
* Normalizes a tool ID by stripping resource ID suffix (UUID/tableId).
555-
* Workflow tools: 'workflow_executor_<uuid>' -> 'workflow_executor'
556-
* Knowledge tools: 'knowledge_search_<uuid>' -> 'knowledge_search'
557-
* Table tools: 'table_query_rows_<tableId>' -> 'table_query_rows'
558-
*/
559-
export function normalizeToolId(toolId: string): string {
560-
if (toolId.startsWith('workflow_executor_') && toolId.length > 'workflow_executor_'.length) {
561-
return 'workflow_executor'
562-
}
563-
564-
const knowledgeOps = ['knowledge_search', 'knowledge_upload_chunk', 'knowledge_create_document']
565-
for (const op of knowledgeOps) {
566-
if (toolId.startsWith(`${op}_`) && toolId.length > op.length + 1) {
567-
return op
568-
}
569-
}
570-
571-
const tableOps = [
572-
'table_query_rows',
573-
'table_insert_row',
574-
'table_batch_insert_rows',
575-
'table_update_row',
576-
'table_update_rows_by_filter',
577-
'table_delete_rows_by_filter',
578-
'table_upsert_row',
579-
'table_get_row',
580-
'table_delete_row',
581-
'table_get_schema',
582-
]
583-
for (const op of tableOps) {
584-
if (toolId.startsWith(`${op}_`) && toolId.length > op.length + 1) {
585-
return op
586-
}
587-
}
588-
589-
return toolId
590-
}
553+
export { normalizeToolId } from '@/tools/normalize'
591554

592555
/**
593556
* Maximum request body size in bytes before we warn/error about size limits.

apps/sim/tools/normalize.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Normalizes a tool ID by stripping resource ID suffix (UUID/tableId).
3+
* Workflow tools: 'workflow_executor_<uuid>' -> 'workflow_executor'
4+
* Knowledge tools: 'knowledge_search_<uuid>' -> 'knowledge_search'
5+
* Table tools: 'table_query_rows_<tableId>' -> 'table_query_rows'
6+
*
7+
* Pure string utility — no server dependencies, safe to import in client components.
8+
*/
9+
export function normalizeToolId(toolId: string): string {
10+
if (toolId.startsWith('workflow_executor_') && toolId.length > 'workflow_executor_'.length) {
11+
return 'workflow_executor'
12+
}
13+
14+
const knowledgeOps = ['knowledge_search', 'knowledge_upload_chunk', 'knowledge_create_document']
15+
for (const op of knowledgeOps) {
16+
if (toolId.startsWith(`${op}_`) && toolId.length > op.length + 1) {
17+
return op
18+
}
19+
}
20+
21+
const tableOps = [
22+
'table_query_rows',
23+
'table_insert_row',
24+
'table_batch_insert_rows',
25+
'table_update_row',
26+
'table_update_rows_by_filter',
27+
'table_delete_rows_by_filter',
28+
'table_upsert_row',
29+
'table_get_row',
30+
'table_delete_row',
31+
'table_get_schema',
32+
]
33+
for (const op of tableOps) {
34+
if (toolId.startsWith(`${op}_`) && toolId.length > op.length + 1) {
35+
return op
36+
}
37+
}
38+
39+
return toolId
40+
}

0 commit comments

Comments
 (0)