Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/llm-metadata-run-tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---

Propagate run tags to span attributes so they can be extracted server-side for LLM cost attribution metadata.
6 changes: 6 additions & 0 deletions .server-changes/llm-cost-tracking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: feature
---

Add automatic LLM cost calculation for spans with GenAI semantic conventions. When a span arrives with `gen_ai.response.model` and token usage data, costs are calculated from an in-memory pricing registry backed by Postgres and dual-written to both span attributes (`trigger.llm.*`) and a new `llm_usage_v1` ClickHouse table.
12 changes: 12 additions & 0 deletions apps/webapp/app/assets/icons/AnthropicLogoIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function AnthropicLogoIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z" />
</svg>
);
}
5 changes: 5 additions & 0 deletions apps/webapp/app/components/code/QueryResultsChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,11 @@ function createYAxisFormatter(
formatDurationMilliseconds(value * 1000, { style: "short" });
}

if (format === "durationNs") {
return (value: number): string =>
formatDurationMilliseconds(value / 1_000_000, { style: "short" });
}

if (format === "costInDollars" || format === "cost") {
return (value: number): string => {
const dollars = format === "cost" ? value / 100 : value;
Expand Down
20 changes: 20 additions & 0 deletions apps/webapp/app/components/code/TSQLResultsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ function getFormattedValue(value: unknown, column: OutputColumnMetadata): string
return formatDurationMilliseconds(value * 1000, { style: "short" });
}
break;
case "durationNs":
if (typeof value === "number") {
return formatDurationMilliseconds(value / 1_000_000, { style: "short" });
}
break;
case "cost":
if (typeof value === "number") {
return formatCurrencyAccurate(value / 100);
Expand Down Expand Up @@ -282,6 +287,12 @@ function getDisplayLength(value: unknown, column: OutputColumnMetadata): number
return formatted.length;
}
return 10;
case "durationNs":
if (typeof value === "number") {
const formatted = formatDurationMilliseconds(value / 1_000_000, { style: "short" });
return formatted.length;
}
return 10;
case "cost":
case "costInDollars":
// Currency format: "$1,234.56"
Expand Down Expand Up @@ -598,6 +609,15 @@ function CellValue({
);
}
return <span>{String(value)}</span>;
case "durationNs":
if (typeof value === "number") {
return (
<span className="tabular-nums">
{formatDurationMilliseconds(value / 1_000_000, { style: "short" })}
</span>
);
}
return <span>{String(value)}</span>;
case "cost":
if (typeof value === "number") {
return <span className="tabular-nums">{formatCurrencyAccurate(value / 100)}</span>;
Expand Down
3 changes: 3 additions & 0 deletions apps/webapp/app/components/runs/v3/RunIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
TableCellsIcon,
TagIcon,
} from "@heroicons/react/20/solid";
import { AnthropicLogoIcon } from "~/assets/icons/AnthropicLogoIcon";
import { AttemptIcon } from "~/assets/icons/AttemptIcon";
import { TaskIcon } from "~/assets/icons/TaskIcon";
import { cn } from "~/utils/cn";
Expand Down Expand Up @@ -112,6 +113,8 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
return <FunctionIcon className={cn(className, "text-error")} />;
case "streams":
return <StreamsIcon className={cn(className, "text-text-dimmed")} />;
case "tabler-brand-anthropic":
return <AnthropicLogoIcon className={cn(className, "text-text-dimmed")} />;
}

return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;
Expand Down
26 changes: 26 additions & 0 deletions apps/webapp/app/components/runs/v3/SpanTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { TaskEventStyle } from "@trigger.dev/core/v3";
import type { TaskEventLevel } from "@trigger.dev/database";
import { Fragment } from "react";
import { cn } from "~/utils/cn";
import { tablerIcons } from "~/utils/tablerIcons";
import tablerSpritePath from "~/components/primitives/tabler-sprite.svg";

type SpanTitleProps = {
message: string;
Expand Down Expand Up @@ -45,6 +47,15 @@ function SpanAccessory({
/>
);
}
case "pills": {
return (
<div className="flex items-center gap-1">
{accessory.items.map((item, index) => (
<SpanPill key={index} text={item.text} icon={item.icon} />
))}
</div>
);
}
default: {
return (
<div className={cn("flex gap-1")}>
Expand All @@ -59,6 +70,21 @@ function SpanAccessory({
}
}

function SpanPill({ text, icon }: { text: string; icon?: string }) {
const hasIcon = icon && tablerIcons.has(icon);

return (
<span className="inline-flex items-center gap-0.5 rounded-full border border-charcoal-700 bg-charcoal-850 px-1.5 py-px text-xxs text-text-dimmed">
{hasIcon && (
<svg className="size-3 stroke-[1.5] text-text-dimmed/70">
<use xlinkHref={`${tablerSpritePath}#${icon}`} />
</svg>
)}
<span className="truncate">{text}</span>
</span>
);
}

export function SpanCodePathAccessory({
accessory,
className,
Expand Down
Loading
Loading