diff --git a/.changeset/llm-metadata-run-tags.md b/.changeset/llm-metadata-run-tags.md
new file mode 100644
index 00000000000..85f04c363b8
--- /dev/null
+++ b/.changeset/llm-metadata-run-tags.md
@@ -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.
diff --git a/.server-changes/llm-cost-tracking.md b/.server-changes/llm-cost-tracking.md
new file mode 100644
index 00000000000..b68302731a7
--- /dev/null
+++ b/.server-changes/llm-cost-tracking.md
@@ -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.
diff --git a/apps/webapp/app/assets/icons/AnthropicLogoIcon.tsx b/apps/webapp/app/assets/icons/AnthropicLogoIcon.tsx
new file mode 100644
index 00000000000..3e647284cce
--- /dev/null
+++ b/apps/webapp/app/assets/icons/AnthropicLogoIcon.tsx
@@ -0,0 +1,12 @@
+export function AnthropicLogoIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/apps/webapp/app/components/code/QueryResultsChart.tsx b/apps/webapp/app/components/code/QueryResultsChart.tsx
index 190304a1e9c..aa6bc6d8898 100644
--- a/apps/webapp/app/components/code/QueryResultsChart.tsx
+++ b/apps/webapp/app/components/code/QueryResultsChart.tsx
@@ -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;
diff --git a/apps/webapp/app/components/code/TSQLResultsTable.tsx b/apps/webapp/app/components/code/TSQLResultsTable.tsx
index 3eb033c1d09..b2caf74dac6 100644
--- a/apps/webapp/app/components/code/TSQLResultsTable.tsx
+++ b/apps/webapp/app/components/code/TSQLResultsTable.tsx
@@ -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);
@@ -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"
@@ -598,6 +609,15 @@ function CellValue({
);
}
return {String(value)};
+ case "durationNs":
+ if (typeof value === "number") {
+ return (
+
+ {formatDurationMilliseconds(value / 1_000_000, { style: "short" })}
+
+ );
+ }
+ return {String(value)};
case "cost":
if (typeof value === "number") {
return {formatCurrencyAccurate(value / 100)};
diff --git a/apps/webapp/app/components/runs/v3/RunIcon.tsx b/apps/webapp/app/components/runs/v3/RunIcon.tsx
index 846d7cae0a4..73963ea09b9 100644
--- a/apps/webapp/app/components/runs/v3/RunIcon.tsx
+++ b/apps/webapp/app/components/runs/v3/RunIcon.tsx
@@ -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";
@@ -112,6 +113,8 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
return ;
case "streams":
return ;
+ case "tabler-brand-anthropic":
+ return ;
}
return ;
diff --git a/apps/webapp/app/components/runs/v3/SpanTitle.tsx b/apps/webapp/app/components/runs/v3/SpanTitle.tsx
index fb0105c45db..fe85fd70c9e 100644
--- a/apps/webapp/app/components/runs/v3/SpanTitle.tsx
+++ b/apps/webapp/app/components/runs/v3/SpanTitle.tsx
@@ -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;
@@ -45,6 +47,15 @@ function SpanAccessory({
/>
);
}
+ case "pills": {
+ return (
+
+ {accessory.items.map((item, index) => (
+
+ ))}
+
+ );
+ }
default: {
return (
@@ -59,6 +70,21 @@ function SpanAccessory({
}
}
+function SpanPill({ text, icon }: { text: string; icon?: string }) {
+ const hasIcon = icon && tablerIcons.has(icon);
+
+ return (
+
+ {hasIcon && (
+
+ )}
+ {text}
+
+ );
+}
+
export function SpanCodePathAccessory({
accessory,
className,
diff --git a/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx b/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
new file mode 100644
index 00000000000..a50ef8ae806
--- /dev/null
+++ b/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
@@ -0,0 +1,285 @@
+import { CheckIcon, ClipboardDocumentIcon, CodeBracketSquareIcon } from "@heroicons/react/20/solid";
+import { lazy, Suspense, useState } from "react";
+import { CodeBlock } from "~/components/code/CodeBlock";
+import { Button } from "~/components/primitives/Buttons";
+import { Header3 } from "~/components/primitives/Headers";
+import { Paragraph } from "~/components/primitives/Paragraph";
+import type { DisplayItem, ToolUse } from "./types";
+
+// Lazy load streamdown to avoid SSR issues
+const StreamdownRenderer = lazy(() =>
+ import("streamdown").then((mod) => ({
+ default: ({ children }: { children: string }) => (
+
+ {children}
+
+ ),
+ }))
+);
+
+export function AIChatMessages({ items }: { items: DisplayItem[] }) {
+ return (
+
+ {items.map((item, i) => {
+ switch (item.type) {
+ case "system":
+ return
;
+ case "user":
+ return
;
+ case "tool-use":
+ return
;
+ case "assistant":
+ return
;
+ }
+ })}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Section header (shared across all sections)
+// ---------------------------------------------------------------------------
+
+function SectionHeader({ label, right }: { label: string; right?: React.ReactNode }) {
+ return (
+
+
{label}
+ {right &&
{right}
}
+
+ );
+}
+
+export function ChatBubble({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// System
+// ---------------------------------------------------------------------------
+
+function SystemSection({ text }: { text: string }) {
+ const [expanded, setExpanded] = useState(false);
+ const isLong = text.length > 150;
+ const preview = isLong ? text.slice(0, 150) + "..." : text;
+
+ return (
+
+
setExpanded(!expanded)}
+ className="text-[10px] text-text-link hover:underline"
+ >
+ {expanded ? "Collapse" : "Expand"}
+
+ ) : undefined
+ }
+ />
+
+
+ {expanded || !isLong ? text : preview}
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// User
+// ---------------------------------------------------------------------------
+
+function UserSection({ text }: { text: string }) {
+ return (
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Assistant response (with markdown/raw toggle)
+// ---------------------------------------------------------------------------
+
+export function AssistantResponse({
+ text,
+ headerLabel = "Assistant",
+}: {
+ text: string;
+ headerLabel?: string;
+}) {
+ const [mode, setMode] = useState<"rendered" | "raw">("rendered");
+ const [copied, setCopied] = useState(false);
+
+ function handleCopy() {
+ navigator.clipboard.writeText(text);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+
+ return (
+
+
+
+
+
+ }
+ />
+ {mode === "rendered" ? (
+
+
+ {text}}>
+ {text}
+
+
+
+ ) : (
+
+ )}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Tool use (merged calls + results)
+// ---------------------------------------------------------------------------
+
+function ToolUseSection({ tools }: { tools: ToolUse[] }) {
+ return (
+
+
+
+
+ {tools.map((tool) => (
+
+ ))}
+
+
+
+ );
+}
+
+type ToolTab = "input" | "output" | "details";
+
+function ToolUseRow({ tool }: { tool: ToolUse }) {
+ const hasInput = tool.inputJson !== "{}";
+ const hasResult = !!tool.resultOutput;
+ const hasDetails = !!tool.description || !!tool.parametersJson;
+
+ const availableTabs: ToolTab[] = [
+ ...(hasInput ? (["input"] as const) : []),
+ ...(hasResult ? (["output"] as const) : []),
+ ...(hasDetails ? (["details"] as const) : []),
+ ];
+
+ const defaultTab: ToolTab | null = hasInput ? "input" : null;
+ const [activeTab, setActiveTab] = useState(defaultTab);
+
+ function handleTabClick(tab: ToolTab) {
+ setActiveTab(activeTab === tab ? null : tab);
+ }
+
+ return (
+
+
+ {tool.toolName}
+ {tool.resultSummary && (
+ {tool.resultSummary}
+ )}
+
+
+ {availableTabs.length > 0 && (
+ <>
+
+ {availableTabs.map((tab) => (
+
+ ))}
+
+
+ {activeTab === "input" && hasInput && (
+
+
+
+ )}
+
+ {activeTab === "output" && hasResult && (
+
+
+
+ )}
+
+ {activeTab === "details" && hasDetails && (
+
+ {tool.description && (
+
{tool.description}
+ )}
+ {tool.parametersJson && (
+
+
+ Parameters schema
+
+
+
+ )}
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx b/apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx
new file mode 100644
index 00000000000..62341fc9041
--- /dev/null
+++ b/apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx
@@ -0,0 +1,113 @@
+import { formatCurrencyAccurate } from "~/utils/numberFormatter";
+import { Header3 } from "~/components/primitives/Headers";
+import type { AISpanData } from "./types";
+
+export function AITagsRow({ aiData }: { aiData: AISpanData }) {
+ return (
+
+
+
+ {aiData.provider !== "unknown" && }
+ {aiData.resolvedProvider && (
+
+ )}
+ {aiData.finishReason && }
+ {aiData.serviceTier && }
+ {aiData.toolChoice && }
+ {aiData.toolCount != null && aiData.toolCount > 0 && (
+
+ )}
+ {aiData.messageCount != null && (
+
+ )}
+ {aiData.telemetryMetadata &&
+ Object.entries(aiData.telemetryMetadata).map(([key, value]) => (
+
+ ))}
+
+
+ );
+}
+
+export function AIStatsSummary({ aiData }: { aiData: AISpanData }) {
+ return (
+
+
Stats
+
+
+
+ {aiData.cachedTokens != null && aiData.cachedTokens > 0 && (
+
+ )}
+ {aiData.cacheCreationTokens != null && aiData.cacheCreationTokens > 0 && (
+
+ )}
+ {aiData.reasoningTokens != null && aiData.reasoningTokens > 0 && (
+
+ )}
+
+
+ {aiData.totalCost != null && (
+
+ )}
+ {aiData.msToFirstChunk != null && (
+
+ )}
+ {aiData.tokensPerSecond != null && (
+
+ )}
+
+
+ );
+}
+
+function MetricRow({
+ label,
+ value,
+ unit,
+ bold,
+}: {
+ label: string;
+ value: string;
+ unit?: string;
+ bold?: boolean;
+}) {
+ return (
+
+ {label}
+
+ {value}
+ {unit && {unit}}
+
+
+ );
+}
+
+function formatTtfc(ms: number): string {
+ if (ms >= 10_000) {
+ return `${(ms / 1000).toFixed(1)}s`;
+ }
+ return `${Math.round(ms)}ms`;
+}
diff --git a/apps/webapp/app/components/runs/v3/ai/AISpanDetails.tsx b/apps/webapp/app/components/runs/v3/ai/AISpanDetails.tsx
new file mode 100644
index 00000000000..4b64da7db38
--- /dev/null
+++ b/apps/webapp/app/components/runs/v3/ai/AISpanDetails.tsx
@@ -0,0 +1,203 @@
+import { CheckIcon, ClipboardDocumentIcon } from "@heroicons/react/20/solid";
+import { useState } from "react";
+import { Button } from "~/components/primitives/Buttons";
+import { Header3 } from "~/components/primitives/Headers";
+import { Paragraph } from "~/components/primitives/Paragraph";
+import { TabButton, TabContainer } from "~/components/primitives/Tabs";
+import { useHasAdminAccess } from "~/hooks/useUser";
+import { AIChatMessages, AssistantResponse, ChatBubble } from "./AIChatMessages";
+import { AIStatsSummary, AITagsRow } from "./AIModelSummary";
+import { AIToolsInventory } from "./AIToolsInventory";
+import type { AISpanData, DisplayItem } from "./types";
+
+type AITab = "overview" | "messages" | "tools";
+
+export function AISpanDetails({
+ aiData,
+ rawProperties,
+}: {
+ aiData: AISpanData;
+ rawProperties?: string;
+}) {
+ const [tab, setTab] = useState("overview");
+ const isAdmin = useHasAdminAccess();
+ const toolCount = aiData.toolCount ?? aiData.toolDefinitions?.length ?? 0;
+
+ return (
+
+ {/* Tab bar */}
+
+
+ setTab("overview")}
+ shortcut={{ key: "o" }}
+ >
+ Overview
+
+ setTab("messages")}
+ shortcut={{ key: "m" }}
+ >
+ Messages
+
+ setTab("tools")}
+ shortcut={{ key: "t" }}
+ >
+
+ Tools
+ {toolCount > 0 && (
+
+ {toolCount}
+
+ )}
+
+
+
+
+
+ {/* Tab content */}
+
+ {tab === "overview" && }
+ {tab === "messages" && }
+ {tab === "tools" && }
+
+
+ {/* Footer: Copy raw (admin only) */}
+ {isAdmin && rawProperties &&
}
+
+ );
+}
+
+function OverviewTab({ aiData }: { aiData: AISpanData }) {
+ const { userText, outputText, outputToolNames } = extractInputOutput(aiData);
+
+ return (
+
+ {/* Tags + Stats */}
+
+
+
+ {/* Input (last user prompt) */}
+ {userText && (
+
+
Input
+
+ {userText}
+
+
+ )}
+
+ {/* Output (assistant response or tool calls) */}
+ {outputText &&
}
+ {outputToolNames.length > 0 && !outputText && (
+
+
Output
+
+
+ Called {outputToolNames.length === 1 ? "tool" : "tools"}:{" "}
+ {outputToolNames.join(", ")}
+
+
+
+ )}
+
+ );
+}
+
+function MessagesTab({ aiData }: { aiData: AISpanData }) {
+ return (
+
+
+ {aiData.items && aiData.items.length > 0 &&
}
+ {aiData.responseText && !hasAssistantItem(aiData.items) && (
+
+ )}
+
+
+ );
+}
+
+function ToolsTab({ aiData }: { aiData: AISpanData }) {
+ return ;
+}
+
+function CopyRawFooter({ rawProperties }: { rawProperties: string }) {
+ const [copied, setCopied] = useState(false);
+
+ function handleCopy() {
+ navigator.clipboard.writeText(rawProperties);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+
+ return (
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+function extractInputOutput(aiData: AISpanData): {
+ userText: string | undefined;
+ outputText: string | undefined;
+ outputToolNames: string[];
+} {
+ let userText: string | undefined;
+ let outputText: string | undefined;
+ const outputToolNames: string[] = [];
+
+ if (aiData.items) {
+ // Find the last user message
+ for (let i = aiData.items.length - 1; i >= 0; i--) {
+ if (aiData.items[i].type === "user") {
+ userText = (aiData.items[i] as { type: "user"; text: string }).text;
+ break;
+ }
+ }
+
+ // Find the last assistant or tool-use item as the output
+ for (let i = aiData.items.length - 1; i >= 0; i--) {
+ const item = aiData.items[i];
+ if (item.type === "assistant") {
+ outputText = item.text;
+ break;
+ }
+ if (item.type === "tool-use") {
+ for (const tool of item.tools) {
+ outputToolNames.push(tool.toolName);
+ }
+ break;
+ }
+ }
+ }
+
+ // Fall back to responseText if no assistant item found
+ if (!outputText && aiData.responseText) {
+ outputText = aiData.responseText;
+ }
+
+ return { userText, outputText, outputToolNames };
+}
+
+function hasAssistantItem(items: DisplayItem[] | undefined): boolean {
+ if (!items) return false;
+ return items.some((item) => item.type === "assistant");
+}
diff --git a/apps/webapp/app/components/runs/v3/ai/AIToolsInventory.tsx b/apps/webapp/app/components/runs/v3/ai/AIToolsInventory.tsx
new file mode 100644
index 00000000000..a329698dd5e
--- /dev/null
+++ b/apps/webapp/app/components/runs/v3/ai/AIToolsInventory.tsx
@@ -0,0 +1,84 @@
+import { useState } from "react";
+import { CodeBlock } from "~/components/code/CodeBlock";
+import type { AISpanData, ToolDefinition } from "./types";
+import { Paragraph } from "~/components/primitives/Paragraph";
+
+export function AIToolsInventory({ aiData }: { aiData: AISpanData }) {
+ const defs = aiData.toolDefinitions ?? [];
+ const calledNames = getCalledToolNames(aiData);
+
+ if (defs.length === 0) {
+ return (
+
+
No tool definitions available for this span.
+
+ );
+ }
+
+ return (
+
+ {defs.map((def) => {
+ const wasCalled = calledNames.has(def.name);
+ return ;
+ })}
+
+ );
+}
+
+function ToolDefRow({ def, wasCalled }: { def: ToolDefinition; wasCalled: boolean }) {
+ const [showSchema, setShowSchema] = useState(false);
+
+ return (
+
+
+
+
{def.name}
+
{wasCalled ? "called" : "not called"}
+
+
+ {def.description && (
+
{def.description}
+ )}
+
+ {def.parametersJson && (
+
+
+ {showSchema && (
+
+
+
+ )}
+
+ )}
+
+ );
+}
+
+function getCalledToolNames(aiData: AISpanData): Set {
+ const names = new Set();
+ if (!aiData.items) return names;
+
+ for (const item of aiData.items) {
+ if (item.type === "tool-use") {
+ for (const tool of item.tools) {
+ names.add(tool.toolName);
+ }
+ }
+ }
+
+ return names;
+}
diff --git a/apps/webapp/app/components/runs/v3/ai/extractAISpanData.ts b/apps/webapp/app/components/runs/v3/ai/extractAISpanData.ts
new file mode 100644
index 00000000000..82c1014d5ab
--- /dev/null
+++ b/apps/webapp/app/components/runs/v3/ai/extractAISpanData.ts
@@ -0,0 +1,512 @@
+import type { AISpanData, DisplayItem, ToolDefinition, ToolUse } from "./types";
+
+/**
+ * Extracts structured AI span data from unflattened OTEL span properties.
+ *
+ * Works with the nested object produced by `unflattenAttributes()` — expects
+ * keys like `gen_ai.response.model`, `ai.prompt.messages`, `trigger.llm.total_cost`, etc.
+ *
+ * @param properties Unflattened span properties object
+ * @param durationMs Span duration in milliseconds
+ * @returns Structured AI data, or undefined if this isn't an AI generation span
+ */
+export function extractAISpanData(
+ properties: Record,
+ durationMs: number
+): AISpanData | undefined {
+ const genAi = properties.gen_ai;
+ if (!genAi || typeof genAi !== "object") return undefined;
+
+ const g = genAi as Record;
+ const ai = rec(properties.ai);
+ const trigger = rec(properties.trigger);
+
+ const gResponse = rec(g.response);
+ const gRequest = rec(g.request);
+ const gUsage = rec(g.usage);
+ const gOperation = rec(g.operation);
+ const aiModel = rec(ai.model);
+ const aiResponse = rec(ai.response);
+ const aiPrompt = rec(ai.prompt);
+ const aiUsage = rec(ai.usage);
+ const triggerLlm = rec(trigger.llm);
+
+ const model = str(gResponse.model) ?? str(gRequest.model) ?? str(aiModel.id);
+ if (!model) return undefined;
+
+ // Prefer ai.usage (richer) over gen_ai.usage.
+ // Gateway/some providers emit promptTokens/completionTokens instead of inputTokens/outputTokens.
+ const inputTokens =
+ num(aiUsage.inputTokens) ?? num(aiUsage.promptTokens) ?? num(gUsage.input_tokens) ?? 0;
+ const outputTokens =
+ num(aiUsage.outputTokens) ?? num(aiUsage.completionTokens) ?? num(gUsage.output_tokens) ?? 0;
+ const totalTokens = num(aiUsage.totalTokens) ?? inputTokens + outputTokens;
+
+ const tokensPerSecond =
+ num(aiResponse.avgOutputTokensPerSecond) ??
+ (outputTokens > 0 && durationMs > 0
+ ? Math.round((outputTokens / (durationMs / 1000)) * 10) / 10
+ : undefined);
+
+ const toolDefs = parseToolDefinitions(aiPrompt.tools);
+ const providerMeta = parseProviderMetadata(aiResponse.providerMetadata);
+ const aiTelemetry = rec(ai.telemetry);
+ const telemetryMeta = extractTelemetryMetadata(aiTelemetry.metadata);
+
+ return {
+ model,
+ provider: str(g.system) ?? "unknown",
+ operationName: str(gOperation.name) ?? str(ai.operationId) ?? "",
+ finishReason: str(aiResponse.finishReason),
+ serviceTier: providerMeta?.serviceTier,
+ resolvedProvider: providerMeta?.resolvedProvider,
+ toolChoice: parseToolChoice(aiPrompt.toolChoice),
+ toolCount: toolDefs?.length,
+ messageCount: countMessages(aiPrompt.messages),
+ telemetryMetadata: telemetryMeta,
+ inputTokens,
+ outputTokens,
+ totalTokens,
+ cachedTokens: num(aiUsage.cachedInputTokens) ?? num(gUsage.cache_read_input_tokens),
+ cacheCreationTokens:
+ num(aiUsage.cacheCreationInputTokens) ?? num(gUsage.cache_creation_input_tokens),
+ reasoningTokens: num(aiUsage.reasoningTokens) ?? num(gUsage.reasoning_tokens),
+ tokensPerSecond,
+ msToFirstChunk: num(aiResponse.msToFirstChunk),
+ durationMs,
+ inputCost: num(triggerLlm.input_cost),
+ outputCost: num(triggerLlm.output_cost),
+ totalCost: num(triggerLlm.total_cost),
+ responseText: str(aiResponse.text) || str(aiResponse.object) || undefined,
+ toolDefinitions: toolDefs,
+ items: buildDisplayItems(aiPrompt.messages, aiResponse.toolCalls, toolDefs),
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Primitive helpers
+// ---------------------------------------------------------------------------
+
+function rec(v: unknown): Record {
+ return v && typeof v === "object" ? (v as Record) : {};
+}
+
+function str(v: unknown): string | undefined {
+ return typeof v === "string" ? v : undefined;
+}
+
+function num(v: unknown): number | undefined {
+ return typeof v === "number" ? v : undefined;
+}
+
+// ---------------------------------------------------------------------------
+// Message → DisplayItem transformation
+// ---------------------------------------------------------------------------
+
+type RawMessage = {
+ role: string;
+ content: unknown;
+ toolCallId?: string;
+ name?: string;
+};
+
+/**
+ * Build display items from prompt messages and optionally response tool calls.
+ * - Parses ai.prompt.messages and merges consecutive tool-call + tool-result pairs
+ * - If ai.response.toolCalls is present (finishReason=tool-calls), appends those too
+ */
+function buildDisplayItems(
+ messagesRaw: unknown,
+ responseToolCallsRaw: unknown,
+ toolDefs?: ToolDefinition[]
+): DisplayItem[] | undefined {
+ const items = parseMessagesToDisplayItems(messagesRaw);
+ const responseToolCalls = parseResponseToolCalls(responseToolCallsRaw);
+
+ if (!items && !responseToolCalls) return undefined;
+
+ const result = items ?? [];
+
+ if (responseToolCalls && responseToolCalls.length > 0) {
+ result.push({ type: "tool-use", tools: responseToolCalls });
+ }
+
+ if (toolDefs && toolDefs.length > 0) {
+ const defsByName = new Map(toolDefs.map((d) => [d.name, d]));
+ for (const item of result) {
+ if (item.type === "tool-use") {
+ for (const tool of item.tools) {
+ const def = defsByName.get(tool.toolName);
+ if (def) {
+ tool.description = def.description;
+ tool.parametersJson = def.parametersJson;
+ }
+ }
+ }
+ }
+ }
+
+ return result.length > 0 ? result : undefined;
+}
+
+function parseMessagesToDisplayItems(raw: unknown): DisplayItem[] | undefined {
+ if (typeof raw !== "string") return undefined;
+
+ let messages: RawMessage[];
+ try {
+ const parsed = JSON.parse(raw);
+ if (!Array.isArray(parsed)) return undefined;
+ messages = parsed.map((item: unknown) => {
+ const m = rec(item);
+ return {
+ role: str(m.role) ?? "user",
+ content: m.content,
+ toolCallId: str(m.toolCallId),
+ name: str(m.name),
+ };
+ });
+ } catch {
+ return undefined;
+ }
+
+ const items: DisplayItem[] = [];
+ let i = 0;
+
+ while (i < messages.length) {
+ const msg = messages[i];
+
+ if (msg.role === "system") {
+ items.push({ type: "system", text: extractTextContent(msg.content) });
+ i++;
+ continue;
+ }
+
+ if (msg.role === "user") {
+ items.push({ type: "user", text: extractTextContent(msg.content) });
+ i++;
+ continue;
+ }
+
+ // Assistant message — check if it contains tool calls
+ if (msg.role === "assistant") {
+ const toolCalls = extractToolCalls(msg.content);
+
+ if (toolCalls.length > 0) {
+ // Collect subsequent tool result messages that match these tool calls
+ const toolCallIds = new Set(toolCalls.map((tc) => tc.toolCallId));
+ let j = i + 1;
+ while (j < messages.length && messages[j].role === "tool") {
+ j++;
+ }
+ // Gather tool result messages between i+1 and j
+ const toolResultMsgs = messages.slice(i + 1, j);
+
+ // Build ToolUse entries by pairing calls with results
+ const tools: ToolUse[] = toolCalls.map((tc) => {
+ const resultMsg = toolResultMsgs.find((m) => {
+ // Match by toolCallId in the message's content parts
+ const results = extractToolResults(m.content);
+ return results.some((r) => r.toolCallId === tc.toolCallId);
+ });
+
+ const result = resultMsg
+ ? extractToolResults(resultMsg.content).find(
+ (r) => r.toolCallId === tc.toolCallId
+ )
+ : undefined;
+
+ return {
+ toolCallId: tc.toolCallId,
+ toolName: tc.toolName,
+ inputJson: JSON.stringify(tc.input, null, 2),
+ resultSummary: result?.summary,
+ resultOutput: result?.formattedOutput,
+ };
+ });
+
+ items.push({ type: "tool-use", tools });
+ i = j; // skip past the tool result messages
+ continue;
+ }
+
+ // Assistant message with just text
+ const text = extractTextContent(msg.content);
+ if (text) {
+ items.push({ type: "assistant", text });
+ }
+ i++;
+ continue;
+ }
+
+ // Skip any other message types (tool messages that weren't consumed above)
+ i++;
+ }
+
+ return items.length > 0 ? items : undefined;
+}
+
+// ---------------------------------------------------------------------------
+// Response tool calls (from ai.response.toolCalls, used when finishReason=tool-calls)
+// ---------------------------------------------------------------------------
+
+/**
+ * Parse ai.response.toolCalls JSON string into ToolUse entries.
+ * These are tool calls the model requested but haven't been executed yet in this span.
+ */
+function parseResponseToolCalls(raw: unknown): ToolUse[] | undefined {
+ if (typeof raw !== "string") return undefined;
+ try {
+ const parsed = JSON.parse(raw);
+ if (!Array.isArray(parsed)) return undefined;
+ const tools: ToolUse[] = [];
+ for (const item of parsed) {
+ const tc = rec(item);
+ if (tc.type === "tool-call" || tc.toolName || tc.toolCallId) {
+ tools.push({
+ toolCallId: str(tc.toolCallId) ?? "",
+ toolName: str(tc.toolName) ?? "",
+ inputJson: JSON.stringify(
+ tc.input && typeof tc.input === "object" ? tc.input : {},
+ null,
+ 2
+ ),
+ });
+ }
+ }
+ return tools.length > 0 ? tools : undefined;
+ } catch {
+ return undefined;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Content part extraction
+// ---------------------------------------------------------------------------
+
+function extractTextContent(content: unknown): string {
+ if (typeof content === "string") return content;
+ if (!Array.isArray(content)) return "";
+
+ const texts: string[] = [];
+ for (const raw of content) {
+ const p = rec(raw);
+ if (p.type === "text" && typeof p.text === "string") {
+ texts.push(p.text);
+ } else if (typeof p.text === "string") {
+ texts.push(p.text);
+ }
+ }
+ return texts.join("\n");
+}
+
+type ParsedToolCall = {
+ toolCallId: string;
+ toolName: string;
+ input: Record;
+};
+
+function extractToolCalls(content: unknown): ParsedToolCall[] {
+ if (!Array.isArray(content)) return [];
+ const calls: ParsedToolCall[] = [];
+ for (const raw of content) {
+ const p = rec(raw);
+ if (p.type === "tool-call") {
+ calls.push({
+ toolCallId: str(p.toolCallId) ?? "",
+ toolName: str(p.toolName) ?? "",
+ input: p.input && typeof p.input === "object" ? (p.input as Record) : {},
+ });
+ }
+ }
+ return calls;
+}
+
+type ParsedToolResult = {
+ toolCallId: string;
+ toolName: string;
+ summary: string;
+ formattedOutput: string;
+};
+
+function extractToolResults(content: unknown): ParsedToolResult[] {
+ if (!Array.isArray(content)) return [];
+ const results: ParsedToolResult[] = [];
+ for (const raw of content) {
+ const p = rec(raw);
+ if (p.type === "tool-result") {
+ const { summary, formattedOutput } = summarizeToolOutput(p.output);
+ results.push({
+ toolCallId: str(p.toolCallId) ?? "",
+ toolName: str(p.toolName) ?? "",
+ summary,
+ formattedOutput,
+ });
+ }
+ }
+ return results;
+}
+
+/**
+ * Summarize a tool output into a short label and a formatted string for display.
+ * Handles the AI SDK's `{ type: "json", value: { status, contentType, body, truncated } }` shape.
+ */
+function summarizeToolOutput(output: unknown): { summary: string; formattedOutput: string } {
+ if (typeof output === "string") {
+ return {
+ summary: output.length > 80 ? output.slice(0, 80) + "..." : output,
+ formattedOutput: output,
+ };
+ }
+
+ if (!output || typeof output !== "object") {
+ return { summary: "result", formattedOutput: JSON.stringify(output, null, 2) };
+ }
+
+ const o = output as Record;
+
+ // AI SDK wraps tool results as { type: "json", value: { status, contentType, body, ... } }
+ if (o.type === "json" && o.value && typeof o.value === "object") {
+ const v = o.value as Record;
+ const parts: string[] = [];
+ if (typeof v.status === "number") parts.push(`${v.status}`);
+ if (typeof v.contentType === "string") parts.push(v.contentType);
+ if (v.truncated === true) parts.push("truncated");
+ return {
+ summary: parts.length > 0 ? parts.join(" · ") : "json result",
+ formattedOutput: JSON.stringify(v, null, 2),
+ };
+ }
+
+ return { summary: "result", formattedOutput: JSON.stringify(output, null, 2) };
+}
+
+// ---------------------------------------------------------------------------
+// Tool definitions (from ai.prompt.tools)
+// ---------------------------------------------------------------------------
+
+/**
+ * Parse ai.prompt.tools — after the array fix, this arrives as a JSON array string
+ * where each element is itself a JSON string of a tool definition.
+ */
+function parseToolDefinitions(raw: unknown): ToolDefinition[] | undefined {
+ if (typeof raw !== "string") return undefined;
+ try {
+ const parsed = JSON.parse(raw);
+ if (!Array.isArray(parsed)) return undefined;
+ const defs: ToolDefinition[] = [];
+ for (const item of parsed) {
+ // Each item is either a JSON string or already an object
+ const obj = typeof item === "string" ? JSON.parse(item) : item;
+ if (!obj || typeof obj !== "object") continue;
+ const o = obj as Record;
+ const name = str(o.name);
+ if (!name) continue;
+ const schema = o.parameters ?? o.inputSchema;
+ defs.push({
+ name,
+ description: str(o.description),
+ parametersJson:
+ schema && typeof schema === "object"
+ ? JSON.stringify(schema, null, 2)
+ : undefined,
+ });
+ }
+ return defs.length > 0 ? defs : undefined;
+ } catch {
+ return undefined;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Provider metadata (service tier, inference geo, etc.)
+// ---------------------------------------------------------------------------
+
+function parseProviderMetadata(
+ raw: unknown
+): { serviceTier?: string; resolvedProvider?: string; gatewayCost?: string } | undefined {
+ if (typeof raw !== "string") return undefined;
+ try {
+ const parsed = JSON.parse(raw) as Record;
+ if (!parsed || typeof parsed !== "object") return undefined;
+
+ let serviceTier: string | undefined;
+ let resolvedProvider: string | undefined;
+ let gatewayCost: string | undefined;
+
+ // Anthropic: { anthropic: { usage: { service_tier: "standard" } } }
+ const anthropic = rec(parsed.anthropic);
+ serviceTier = str(rec(anthropic.usage).service_tier);
+
+ // Azure/OpenAI: { azure: { serviceTier: "default" } } or { openai: { serviceTier: "..." } }
+ if (!serviceTier) {
+ serviceTier = str(rec(parsed.azure).serviceTier) ?? str(rec(parsed.openai).serviceTier);
+ }
+
+ // Gateway: { gateway: { routing: { finalProvider, resolvedProvider }, cost } }
+ const gateway = rec(parsed.gateway);
+ const routing = rec(gateway.routing);
+ resolvedProvider = str(routing.finalProvider) ?? str(routing.resolvedProvider);
+ gatewayCost = str(gateway.cost);
+
+ // OpenRouter: { openrouter: { provider: "xAI" } }
+ if (!resolvedProvider) {
+ resolvedProvider = str(rec(parsed.openrouter).provider);
+ }
+
+ if (!serviceTier && !resolvedProvider && !gatewayCost) return undefined;
+ return { serviceTier, resolvedProvider, gatewayCost };
+ } catch {
+ return undefined;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Tool choice parsing
+// ---------------------------------------------------------------------------
+
+function parseToolChoice(raw: unknown): string | undefined {
+ if (typeof raw !== "string") return undefined;
+ try {
+ const parsed = JSON.parse(raw);
+ if (typeof parsed === "string") return parsed;
+ if (parsed && typeof parsed === "object") {
+ const obj = parsed as Record;
+ if (typeof obj.type === "string") return obj.type;
+ }
+ return undefined;
+ } catch {
+ return undefined;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Message count
+// ---------------------------------------------------------------------------
+
+function countMessages(raw: unknown): number | undefined {
+ if (typeof raw !== "string") return undefined;
+ try {
+ const parsed = JSON.parse(raw);
+ if (!Array.isArray(parsed)) return undefined;
+ return parsed.length > 0 ? parsed.length : undefined;
+ } catch {
+ return undefined;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Telemetry metadata
+// ---------------------------------------------------------------------------
+
+function extractTelemetryMetadata(raw: unknown): Record | undefined {
+ if (!raw || typeof raw !== "object") return undefined;
+
+ const result: Record = {};
+ for (const [key, value] of Object.entries(raw as Record)) {
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
+ result[key] = String(value);
+ }
+ }
+
+ return Object.keys(result).length > 0 ? result : undefined;
+}
diff --git a/apps/webapp/app/components/runs/v3/ai/index.ts b/apps/webapp/app/components/runs/v3/ai/index.ts
new file mode 100644
index 00000000000..7e33a46fb2c
--- /dev/null
+++ b/apps/webapp/app/components/runs/v3/ai/index.ts
@@ -0,0 +1,3 @@
+export { AISpanDetails } from "./AISpanDetails";
+export { extractAISpanData } from "./extractAISpanData";
+export type { AISpanData, DisplayItem, ToolUse } from "./types";
diff --git a/apps/webapp/app/components/runs/v3/ai/types.ts b/apps/webapp/app/components/runs/v3/ai/types.ts
new file mode 100644
index 00000000000..70d75533de2
--- /dev/null
+++ b/apps/webapp/app/components/runs/v3/ai/types.ts
@@ -0,0 +1,103 @@
+// ---------------------------------------------------------------------------
+// Tool use (merged assistant tool-call + tool result)
+// ---------------------------------------------------------------------------
+
+export type ToolDefinition = {
+ name: string;
+ description?: string;
+ /** JSON schema as formatted string */
+ parametersJson?: string;
+};
+
+export type ToolUse = {
+ toolCallId: string;
+ toolName: string;
+ /** Tool description from the definition, if available */
+ description?: string;
+ /** JSON schema of the tool's parameters, pretty-printed */
+ parametersJson?: string;
+ /** Formatted input args as JSON string */
+ inputJson: string;
+ /** Short summary of the result (e.g. "200 · text/html · truncated") */
+ resultSummary?: string;
+ /** Full formatted result for display in a code block */
+ resultOutput?: string;
+};
+
+// ---------------------------------------------------------------------------
+// Display items — what the UI actually renders
+// ---------------------------------------------------------------------------
+
+/** System prompt text (collapsible) */
+export type SystemItem = {
+ type: "system";
+ text: string;
+};
+
+/** User message text */
+export type UserItem = {
+ type: "user";
+ text: string;
+};
+
+/** One or more tool calls with their results, grouped */
+export type ToolUseItem = {
+ type: "tool-use";
+ tools: ToolUse[];
+};
+
+/** Final assistant text response */
+export type AssistantItem = {
+ type: "assistant";
+ text: string;
+};
+
+export type DisplayItem = SystemItem | UserItem | ToolUseItem | AssistantItem;
+
+// ---------------------------------------------------------------------------
+// Span-level AI data
+// ---------------------------------------------------------------------------
+
+export type AISpanData = {
+ model: string;
+ provider: string;
+ operationName: string;
+
+ // Categorical tags
+ finishReason?: string;
+ serviceTier?: string;
+ /** Resolved downstream provider for gateway/openrouter spans (e.g. "xAI", "mistral") */
+ resolvedProvider?: string;
+ toolChoice?: string;
+ toolCount?: number;
+ messageCount?: number;
+ /** User-defined telemetry metadata (from ai.telemetry.metadata) */
+ telemetryMetadata?: Record;
+
+ // Token counts
+ inputTokens: number;
+ outputTokens: number;
+ totalTokens: number;
+ cachedTokens?: number;
+ cacheCreationTokens?: number;
+ reasoningTokens?: number;
+
+ // Performance
+ tokensPerSecond?: number;
+ msToFirstChunk?: number;
+ durationMs: number;
+
+ // Cost
+ inputCost?: number;
+ outputCost?: number;
+ totalCost?: number;
+
+ // Response text (final assistant output)
+ responseText?: string;
+
+ // Tool definitions (from ai.prompt.tools)
+ toolDefinitions?: ToolDefinition[];
+
+ // Display-ready message items (system, user, tool-use groups, assistant text)
+ items?: DisplayItem[];
+};
diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts
index bdfdbea6b3e..05d8f93f66c 100644
--- a/apps/webapp/app/env.server.ts
+++ b/apps/webapp/app/env.server.ts
@@ -1247,6 +1247,12 @@ const EnvironmentSchema = z
// Metric widget concurrency limits
METRIC_WIDGET_DEFAULT_ORG_CONCURRENCY_LIMIT: z.coerce.number().int().default(30),
+ // Admin ClickHouse URL (for admin dashboard queries like missing models)
+ ADMIN_CLICKHOUSE_URL: z
+ .string()
+ .optional()
+ .transform((v) => v ?? process.env.CLICKHOUSE_URL),
+
EVENTS_CLICKHOUSE_URL: z
.string()
.optional()
@@ -1277,6 +1283,11 @@ const EnvironmentSchema = z
EVENTS_CLICKHOUSE_MAX_TRACE_DETAILED_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(5_000),
EVENTS_CLICKHOUSE_MAX_LIVE_RELOADING_SETTING: z.coerce.number().int().default(2000),
+ // LLM cost tracking
+ LLM_COST_TRACKING_ENABLED: BoolEnv.default(true),
+ LLM_PRICING_RELOAD_INTERVAL_MS: z.coerce.number().int().default(5 * 60 * 1000), // 5 minutes
+ LLM_PRICING_SEED_ON_STARTUP: BoolEnv.default(false),
+
// Bootstrap
TRIGGER_BOOTSTRAP_ENABLED: z.string().default("0"),
TRIGGER_BOOTSTRAP_WORKER_GROUP_NAME: z.string().optional(),
diff --git a/apps/webapp/app/presenters/v3/SpanPresenter.server.ts b/apps/webapp/app/presenters/v3/SpanPresenter.server.ts
index ce83c2e242b..9ad94745616 100644
--- a/apps/webapp/app/presenters/v3/SpanPresenter.server.ts
+++ b/apps/webapp/app/presenters/v3/SpanPresenter.server.ts
@@ -24,6 +24,7 @@ import { engine } from "~/v3/runEngine.server";
import { resolveEventRepositoryForStore } from "~/v3/eventRepository/index.server";
import { IEventRepository, SpanDetail } from "~/v3/eventRepository/eventRepository.types";
import { safeJsonParse } from "~/utils/json";
+import { extractAISpanData } from "~/components/runs/v3/ai";
type Result = Awaited>;
export type Span = NonNullable["span"]>;
@@ -543,6 +544,13 @@ export class SpanPresenter extends BasePresenter {
entity: span.entity,
metadata: span.metadata,
triggeredRuns,
+ aiData:
+ span.properties && typeof span.properties === "object"
+ ? extractAISpanData(
+ span.properties as Record,
+ span.duration / 1_000_000
+ )
+ : undefined,
};
switch (span.entity.type) {
@@ -665,6 +673,12 @@ export class SpanPresenter extends BasePresenter {
};
}
default:
+ if (data.aiData) {
+ return {
+ ...data,
+ entity: { type: "ai-generation" as const, object: data.aiData },
+ };
+ }
return { ...data, entity: null };
}
}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/ExamplesContent.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/ExamplesContent.tsx
index 4d027223f14..1235b443348 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/ExamplesContent.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/ExamplesContent.tsx
@@ -118,6 +118,96 @@ LIMIT 100`,
scope: "environment",
table: "metrics",
},
+ {
+ title: "LLM cost by model (past 7d)",
+ description: "Total cost, input tokens, and output tokens grouped by model over the last 7 days.",
+ query: `SELECT
+ response_model,
+ SUM(total_cost) AS total_cost,
+ SUM(input_tokens) AS input_tokens,
+ SUM(output_tokens) AS output_tokens
+FROM llm_usage
+WHERE start_time > now() - INTERVAL 7 DAY
+GROUP BY response_model
+ORDER BY total_cost DESC`,
+ scope: "environment",
+ table: "llm_usage",
+ },
+ {
+ title: "LLM cost over time",
+ description: "Total LLM cost bucketed over time. The bucket size adjusts automatically.",
+ query: `SELECT
+ timeBucket(),
+ SUM(total_cost) AS total_cost
+FROM llm_usage
+GROUP BY timeBucket
+ORDER BY timeBucket
+LIMIT 1000`,
+ scope: "environment",
+ table: "llm_usage",
+ },
+ {
+ title: "Most expensive runs by LLM cost (top 50)",
+ description: "Top 50 runs by total LLM cost with token breakdown.",
+ query: `SELECT
+ run_id,
+ task_identifier,
+ SUM(total_cost) AS llm_cost,
+ SUM(input_tokens) AS input_tokens,
+ SUM(output_tokens) AS output_tokens
+FROM llm_usage
+GROUP BY run_id, task_identifier
+ORDER BY llm_cost DESC
+LIMIT 50`,
+ scope: "environment",
+ table: "llm_usage",
+ },
+ {
+ title: "LLM calls by provider",
+ description: "Count and cost of LLM calls grouped by AI provider.",
+ query: `SELECT
+ gen_ai_system,
+ count() AS call_count,
+ SUM(total_cost) AS total_cost
+FROM llm_usage
+GROUP BY gen_ai_system
+ORDER BY total_cost DESC`,
+ scope: "environment",
+ table: "llm_usage",
+ },
+ {
+ title: "LLM cost by user",
+ description:
+ "Total LLM cost per user from run tags or AI SDK telemetry metadata. Uses metadata.userId which comes from experimental_telemetry metadata or run tags like user:123.",
+ query: `SELECT
+ metadata.userId AS user_id,
+ SUM(total_cost) AS total_cost,
+ SUM(total_tokens) AS total_tokens,
+ count() AS call_count
+FROM llm_usage
+WHERE metadata.userId != ''
+GROUP BY metadata.userId
+ORDER BY total_cost DESC
+LIMIT 50`,
+ scope: "environment",
+ table: "llm_usage",
+ },
+ {
+ title: "LLM cost by metadata key",
+ description:
+ "Browse all metadata keys and their LLM cost. Metadata comes from run tags (key:value) and AI SDK telemetry metadata.",
+ query: `SELECT
+ metadata,
+ response_model,
+ total_cost,
+ total_tokens,
+ run_id
+FROM llm_usage
+ORDER BY start_time DESC
+LIMIT 20`,
+ scope: "environment",
+ table: "llm_usage",
+ },
];
const tableOptions = querySchemas.map((s) => ({ label: s.name, value: s.name }));
diff --git a/apps/webapp/app/routes/admin.api.v1.llm-models.$modelId.ts b/apps/webapp/app/routes/admin.api.v1.llm-models.$modelId.ts
new file mode 100644
index 00000000000..4e8357c886c
--- /dev/null
+++ b/apps/webapp/app/routes/admin.api.v1.llm-models.$modelId.ts
@@ -0,0 +1,152 @@
+import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
+import { z } from "zod";
+import { prisma } from "~/db.server";
+import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
+
+async function requireAdmin(request: Request) {
+ const authResult = await authenticateApiRequestWithPersonalAccessToken(request);
+ if (!authResult) {
+ throw json({ error: "Invalid or Missing API key" }, { status: 401 });
+ }
+
+ const user = await prisma.user.findUnique({ where: { id: authResult.userId } });
+ if (!user?.admin) {
+ throw json({ error: "You must be an admin to perform this action" }, { status: 403 });
+ }
+
+ return user;
+}
+
+export async function loader({ request, params }: LoaderFunctionArgs) {
+ await requireAdmin(request);
+
+ const model = await prisma.llmModel.findUnique({
+ where: { id: params.modelId },
+ include: {
+ pricingTiers: {
+ include: { prices: true },
+ orderBy: { priority: "asc" },
+ },
+ },
+ });
+
+ if (!model) {
+ return json({ error: "Model not found" }, { status: 404 });
+ }
+
+ return json({ model });
+}
+
+const UpdateModelSchema = z.object({
+ modelName: z.string().min(1).optional(),
+ matchPattern: z.string().min(1).optional(),
+ startDate: z.string().nullable().optional(),
+ pricingTiers: z
+ .array(
+ z.object({
+ name: z.string().min(1),
+ isDefault: z.boolean().default(true),
+ priority: z.number().int().default(0),
+ conditions: z
+ .array(
+ z.object({
+ usageDetailPattern: z.string(),
+ operator: z.enum(["gt", "gte", "lt", "lte", "eq", "neq"]),
+ value: z.number(),
+ })
+ )
+ .default([]),
+ prices: z.record(z.string(), z.number()),
+ })
+ )
+ .optional(),
+});
+
+export async function action({ request, params }: ActionFunctionArgs) {
+ await requireAdmin(request);
+
+ const modelId = params.modelId!;
+
+ if (request.method === "DELETE") {
+ const existing = await prisma.llmModel.findUnique({ where: { id: modelId } });
+ if (!existing) {
+ return json({ error: "Model not found" }, { status: 404 });
+ }
+
+ await prisma.llmModel.delete({ where: { id: modelId } });
+ return json({ success: true });
+ }
+
+ if (request.method !== "PUT") {
+ return json({ error: "Method not allowed" }, { status: 405 });
+ }
+
+ let body: unknown;
+ try {
+ body = await request.json();
+ } catch {
+ return json({ error: "Invalid JSON body" }, { status: 400 });
+ }
+
+ const parsed = UpdateModelSchema.safeParse(body);
+
+ if (!parsed.success) {
+ return json({ error: "Invalid request body", details: parsed.error.issues }, { status: 400 });
+ }
+
+ const { modelName, matchPattern, startDate, pricingTiers } = parsed.data;
+
+ // Validate regex if provided — strip (?i) POSIX flag since our registry handles it
+ if (matchPattern) {
+ try {
+ const testPattern = matchPattern.startsWith("(?i)") ? matchPattern.slice(4) : matchPattern;
+ new RegExp(testPattern);
+ } catch {
+ return json({ error: "Invalid regex in matchPattern" }, { status: 400 });
+ }
+ }
+
+ // Update model + tiers atomically
+ const updated = await prisma.$transaction(async (tx) => {
+ await tx.llmModel.update({
+ where: { id: modelId },
+ data: {
+ ...(modelName !== undefined && { modelName }),
+ ...(matchPattern !== undefined && { matchPattern }),
+ ...(startDate !== undefined && { startDate: startDate ? new Date(startDate) : null }),
+ },
+ });
+
+ if (pricingTiers) {
+ await tx.llmPricingTier.deleteMany({ where: { modelId } });
+
+ for (const tier of pricingTiers) {
+ await tx.llmPricingTier.create({
+ data: {
+ modelId,
+ name: tier.name,
+ isDefault: tier.isDefault,
+ priority: tier.priority,
+ conditions: tier.conditions,
+ prices: {
+ create: Object.entries(tier.prices).map(([usageType, price]) => ({
+ modelId,
+ usageType,
+ price,
+ })),
+ },
+ },
+ });
+ }
+ }
+
+ return tx.llmModel.findUnique({
+ where: { id: modelId },
+ include: {
+ pricingTiers: { include: { prices: true }, orderBy: { priority: "asc" } },
+ },
+ });
+ });
+
+ return json({ model: updated });
+}
diff --git a/apps/webapp/app/routes/admin.api.v1.llm-models.missing.ts b/apps/webapp/app/routes/admin.api.v1.llm-models.missing.ts
new file mode 100644
index 00000000000..5ca7077e1cc
--- /dev/null
+++ b/apps/webapp/app/routes/admin.api.v1.llm-models.missing.ts
@@ -0,0 +1,33 @@
+import { type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
+import { prisma } from "~/db.server";
+import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
+import { getMissingLlmModels } from "~/services/admin/missingLlmModels.server";
+
+async function requireAdmin(request: Request) {
+ const authResult = await authenticateApiRequestWithPersonalAccessToken(request);
+ if (!authResult) {
+ throw json({ error: "Invalid or Missing API key" }, { status: 401 });
+ }
+
+ const user = await prisma.user.findUnique({ where: { id: authResult.userId } });
+ if (!user?.admin) {
+ throw json({ error: "You must be an admin to perform this action" }, { status: 403 });
+ }
+
+ return user;
+}
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ await requireAdmin(request);
+
+ const url = new URL(request.url);
+ const lookbackHours = parseInt(url.searchParams.get("lookbackHours") ?? "24", 10);
+
+ if (isNaN(lookbackHours) || lookbackHours < 1 || lookbackHours > 720) {
+ return json({ error: "lookbackHours must be between 1 and 720" }, { status: 400 });
+ }
+
+ const models = await getMissingLlmModels({ lookbackHours });
+
+ return json({ models, lookbackHours });
+}
diff --git a/apps/webapp/app/routes/admin.api.v1.llm-models.reload.ts b/apps/webapp/app/routes/admin.api.v1.llm-models.reload.ts
new file mode 100644
index 00000000000..747722b352a
--- /dev/null
+++ b/apps/webapp/app/routes/admin.api.v1.llm-models.reload.ts
@@ -0,0 +1,24 @@
+import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
+import { prisma } from "~/db.server";
+import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
+import { llmPricingRegistry } from "~/v3/llmPricingRegistry.server";
+
+export async function action({ request }: ActionFunctionArgs) {
+ const authResult = await authenticateApiRequestWithPersonalAccessToken(request);
+ if (!authResult) {
+ return json({ error: "Invalid or Missing API key" }, { status: 401 });
+ }
+
+ const user = await prisma.user.findUnique({ where: { id: authResult.userId } });
+ if (!user?.admin) {
+ return json({ error: "You must be an admin to perform this action" }, { status: 403 });
+ }
+
+ if (!llmPricingRegistry) {
+ return json({ error: "LLM cost tracking is disabled" }, { status: 400 });
+ }
+
+ await llmPricingRegistry.reload();
+
+ return json({ success: true, message: "LLM pricing registry reloaded" });
+}
diff --git a/apps/webapp/app/routes/admin.api.v1.llm-models.seed.ts b/apps/webapp/app/routes/admin.api.v1.llm-models.seed.ts
new file mode 100644
index 00000000000..805f97ad233
--- /dev/null
+++ b/apps/webapp/app/routes/admin.api.v1.llm-models.seed.ts
@@ -0,0 +1,30 @@
+import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
+import { seedLlmPricing } from "@internal/llm-pricing";
+import { prisma } from "~/db.server";
+import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
+import { llmPricingRegistry } from "~/v3/llmPricingRegistry.server";
+
+export async function action({ request }: ActionFunctionArgs) {
+ const authResult = await authenticateApiRequestWithPersonalAccessToken(request);
+ if (!authResult) {
+ return json({ error: "Invalid or Missing API key" }, { status: 401 });
+ }
+
+ const user = await prisma.user.findUnique({ where: { id: authResult.userId } });
+ if (!user?.admin) {
+ return json({ error: "You must be an admin to perform this action" }, { status: 403 });
+ }
+
+ const result = await seedLlmPricing(prisma);
+
+ // Reload the in-memory registry after seeding (if enabled)
+ if (llmPricingRegistry) {
+ await llmPricingRegistry.reload();
+ }
+
+ return json({
+ success: true,
+ ...result,
+ message: `Seeded ${result.modelsCreated} models, skipped ${result.modelsSkipped} existing`,
+ });
+}
diff --git a/apps/webapp/app/routes/admin.api.v1.llm-models.ts b/apps/webapp/app/routes/admin.api.v1.llm-models.ts
new file mode 100644
index 00000000000..6305869c605
--- /dev/null
+++ b/apps/webapp/app/routes/admin.api.v1.llm-models.ts
@@ -0,0 +1,141 @@
+import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
+import { z } from "zod";
+import { prisma } from "~/db.server";
+import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
+import { generateFriendlyId } from "~/v3/friendlyIdentifiers";
+
+async function requireAdmin(request: Request) {
+ const authResult = await authenticateApiRequestWithPersonalAccessToken(request);
+ if (!authResult) {
+ throw json({ error: "Invalid or Missing API key" }, { status: 401 });
+ }
+
+ const user = await prisma.user.findUnique({ where: { id: authResult.userId } });
+ if (!user?.admin) {
+ throw json({ error: "You must be an admin to perform this action" }, { status: 403 });
+ }
+
+ return user;
+}
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ await requireAdmin(request);
+
+ const url = new URL(request.url);
+ const page = parseInt(url.searchParams.get("page") ?? "1");
+ const pageSize = parseInt(url.searchParams.get("pageSize") ?? "50");
+
+ const [models, total] = await Promise.all([
+ prisma.llmModel.findMany({
+ where: { projectId: null },
+ include: {
+ pricingTiers: {
+ include: { prices: true },
+ orderBy: { priority: "asc" },
+ },
+ },
+ orderBy: { modelName: "asc" },
+ skip: (page - 1) * pageSize,
+ take: pageSize,
+ }),
+ prisma.llmModel.count({ where: { projectId: null } }),
+ ]);
+
+ return json({ models, total, page, pageSize });
+}
+
+const CreateModelSchema = z.object({
+ modelName: z.string().min(1),
+ matchPattern: z.string().min(1),
+ startDate: z.string().optional(),
+ source: z.enum(["default", "admin"]).optional().default("admin"),
+ pricingTiers: z.array(
+ z.object({
+ name: z.string().min(1),
+ isDefault: z.boolean().default(true),
+ priority: z.number().int().default(0),
+ conditions: z
+ .array(
+ z.object({
+ usageDetailPattern: z.string(),
+ operator: z.enum(["gt", "gte", "lt", "lte", "eq", "neq"]),
+ value: z.number(),
+ })
+ )
+ .default([]),
+ prices: z.record(z.string(), z.number()),
+ })
+ ),
+});
+
+export async function action({ request }: ActionFunctionArgs) {
+ await requireAdmin(request);
+
+ if (request.method !== "POST") {
+ return json({ error: "Method not allowed" }, { status: 405 });
+ }
+
+ let body: unknown;
+ try {
+ body = await request.json();
+ } catch {
+ return json({ error: "Invalid JSON body" }, { status: 400 });
+ }
+
+ const parsed = CreateModelSchema.safeParse(body);
+
+ if (!parsed.success) {
+ return json({ error: "Invalid request body", details: parsed.error.issues }, { status: 400 });
+ }
+
+ const { modelName, matchPattern, startDate, source, pricingTiers } = parsed.data;
+
+ // Validate regex pattern — strip (?i) POSIX flag since our registry handles it
+ try {
+ const testPattern = matchPattern.startsWith("(?i)") ? matchPattern.slice(4) : matchPattern;
+ new RegExp(testPattern);
+ } catch {
+ return json({ error: "Invalid regex in matchPattern" }, { status: 400 });
+ }
+
+ // Create model + tiers atomically
+ const created = await prisma.$transaction(async (tx) => {
+ const model = await tx.llmModel.create({
+ data: {
+ friendlyId: generateFriendlyId("llm_model"),
+ modelName,
+ matchPattern,
+ startDate: startDate ? new Date(startDate) : null,
+ source,
+ },
+ });
+
+ for (const tier of pricingTiers) {
+ await tx.llmPricingTier.create({
+ data: {
+ modelId: model.id,
+ name: tier.name,
+ isDefault: tier.isDefault,
+ priority: tier.priority,
+ conditions: tier.conditions,
+ prices: {
+ create: Object.entries(tier.prices).map(([usageType, price]) => ({
+ modelId: model.id,
+ usageType,
+ price,
+ })),
+ },
+ },
+ });
+ }
+
+ return tx.llmModel.findUnique({
+ where: { id: model.id },
+ include: {
+ pricingTiers: { include: { prices: true } },
+ },
+ });
+ });
+
+ return json({ model: created }, { status: 201 });
+}
diff --git a/apps/webapp/app/routes/admin.llm-models.$modelId.tsx b/apps/webapp/app/routes/admin.llm-models.$modelId.tsx
new file mode 100644
index 00000000000..e37491a1b4f
--- /dev/null
+++ b/apps/webapp/app/routes/admin.llm-models.$modelId.tsx
@@ -0,0 +1,461 @@
+import { Form, useActionData, useNavigate } from "@remix-run/react";
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
+import { redirect } from "@remix-run/server-runtime";
+import { typedjson, useTypedLoaderData } from "remix-typedjson";
+import { z } from "zod";
+import { useState } from "react";
+import { Button, LinkButton } from "~/components/primitives/Buttons";
+import { Input } from "~/components/primitives/Input";
+import { Paragraph } from "~/components/primitives/Paragraph";
+import { prisma } from "~/db.server";
+import { requireUserId } from "~/services/session.server";
+import { llmPricingRegistry } from "~/v3/llmPricingRegistry.server";
+
+export const loader = async ({ request, params }: LoaderFunctionArgs) => {
+ const userId = await requireUserId(request);
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (!user?.admin) throw redirect("/");
+
+ const model = await prisma.llmModel.findUnique({
+ where: { friendlyId: params.modelId },
+ include: {
+ pricingTiers: { include: { prices: true }, orderBy: { priority: "asc" } },
+ },
+ });
+
+ if (!model) throw new Response("Model not found", { status: 404 });
+
+ // Convert Prisma Decimal to plain numbers for serialization
+ const serialized = {
+ ...model,
+ pricingTiers: model.pricingTiers.map((t) => ({
+ ...t,
+ prices: t.prices.map((p) => ({ ...p, price: Number(p.price) })),
+ })),
+ };
+
+ return typedjson({ model: serialized });
+};
+
+const SaveSchema = z.object({
+ modelName: z.string().min(1),
+ matchPattern: z.string().min(1),
+ pricingTiersJson: z.string(),
+});
+
+export async function action({ request, params }: ActionFunctionArgs) {
+ const userId = await requireUserId(request);
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (!user?.admin) throw redirect("/");
+
+ const friendlyId = params.modelId!;
+ const existing = await prisma.llmModel.findUnique({ where: { friendlyId } });
+ if (!existing) throw new Response("Model not found", { status: 404 });
+ const modelId = existing.id;
+
+ const formData = await request.formData();
+ const _action = formData.get("_action");
+
+ if (_action === "delete") {
+ await prisma.llmModel.delete({ where: { id: modelId } });
+ await llmPricingRegistry?.reload();
+ return redirect("/admin/llm-models");
+ }
+
+ if (_action === "save") {
+ const raw = Object.fromEntries(formData);
+ const parsed = SaveSchema.safeParse(raw);
+
+ if (!parsed.success) {
+ return typedjson({ error: "Invalid form data", details: parsed.error.issues }, { status: 400 });
+ }
+
+ const { modelName, matchPattern, pricingTiersJson } = parsed.data;
+
+ // Validate regex — strip (?i) POSIX flag since our registry handles it
+ try {
+ const testPattern = matchPattern.startsWith("(?i)") ? matchPattern.slice(4) : matchPattern;
+ new RegExp(testPattern);
+ } catch {
+ return typedjson({ error: "Invalid regex in matchPattern" }, { status: 400 });
+ }
+
+ // Parse tiers
+ let pricingTiers: Array<{
+ name: string;
+ isDefault: boolean;
+ priority: number;
+ conditions: Array<{ usageDetailPattern: string; operator: string; value: number }>;
+ prices: Record;
+ }>;
+ try {
+ pricingTiers = JSON.parse(pricingTiersJson) as typeof pricingTiers;
+ } catch {
+ return typedjson({ error: "Invalid pricing tiers JSON" }, { status: 400 });
+ }
+
+ // Update model
+ await prisma.llmModel.update({
+ where: { id: modelId },
+ data: { modelName, matchPattern },
+ });
+
+ // Replace tiers
+ await prisma.llmPricingTier.deleteMany({ where: { modelId } });
+ for (const tier of pricingTiers) {
+ await prisma.llmPricingTier.create({
+ data: {
+ modelId,
+ name: tier.name,
+ isDefault: tier.isDefault,
+ priority: tier.priority,
+ conditions: tier.conditions,
+ prices: {
+ create: Object.entries(tier.prices).map(([usageType, price]) => ({
+ modelId,
+ usageType,
+ price,
+ })),
+ },
+ },
+ });
+ }
+
+ await llmPricingRegistry?.reload();
+ return typedjson({ success: true });
+ }
+
+ return typedjson({ error: "Unknown action" }, { status: 400 });
+}
+
+export default function AdminLlmModelDetailRoute() {
+ const { model } = useTypedLoaderData();
+ const actionData = useActionData<{ success?: boolean; error?: string; details?: unknown[] }>();
+ const navigate = useNavigate();
+
+ const [modelName, setModelName] = useState(model.modelName);
+ const [matchPattern, setMatchPattern] = useState(model.matchPattern);
+ const [testInput, setTestInput] = useState("");
+ const [tiers, setTiers] = useState(() =>
+ model.pricingTiers.map((t) => ({
+ name: t.name,
+ isDefault: t.isDefault,
+ priority: t.priority,
+ conditions: (t.conditions ?? []) as Array<{
+ usageDetailPattern: string;
+ operator: string;
+ value: number;
+ }>,
+ prices: Object.fromEntries(t.prices.map((p) => [p.usageType, p.price])),
+ }))
+ );
+
+ // Test regex match
+ let testResult: boolean | null = null;
+ if (testInput) {
+ try {
+ const pattern = matchPattern.startsWith("(?i)")
+ ? matchPattern.slice(4)
+ : matchPattern;
+ testResult = new RegExp(pattern, "i").test(testInput);
+ } catch {
+ testResult = null;
+ }
+ }
+
+ return (
+
+
+
+
{model.modelName}
+
+
+ {model.source ?? "default"}
+
+
+ Back to list
+
+
+
+
+
+
+ {/* Delete section */}
+
+
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Tier editor sub-component
+// ---------------------------------------------------------------------------
+
+type TierData = {
+ name: string;
+ isDefault: boolean;
+ priority: number;
+ conditions: Array<{ usageDetailPattern: string; operator: string; value: number }>;
+ prices: Record;
+};
+
+const COMMON_USAGE_TYPES = [
+ "input",
+ "output",
+ "input_cached_tokens",
+ "cache_creation_input_tokens",
+ "reasoning_tokens",
+];
+
+function TierEditor({
+ tier,
+ onChange,
+ onRemove,
+}: {
+ tier: TierData;
+ onChange: (t: TierData) => void;
+ onRemove: () => void;
+}) {
+ const [newUsageType, setNewUsageType] = useState("");
+
+ return (
+
+
+
+ {/* Prices */}
+
+
+ Prices (per token)
+
+
+ {Object.entries(tier.prices).map(([usageType, price]) => (
+
+ {usageType}
+ {
+ const val = parseFloat(e.target.value);
+ if (!isNaN(val)) {
+ onChange({
+ ...tier,
+ prices: { ...tier.prices, [usageType]: val },
+ });
+ }
+ }}
+ />
+
+
+ ))}
+
+
+ {/* Add price */}
+
+
+ {newUsageType && (
+
+ )}
+
+
+
+ );
+}
diff --git a/apps/webapp/app/routes/admin.llm-models._index.tsx b/apps/webapp/app/routes/admin.llm-models._index.tsx
new file mode 100644
index 00000000000..fb2f6fdc491
--- /dev/null
+++ b/apps/webapp/app/routes/admin.llm-models._index.tsx
@@ -0,0 +1,346 @@
+import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
+import { Form, useFetcher, Link } from "@remix-run/react";
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
+import { redirect } from "@remix-run/server-runtime";
+import { typedjson, useTypedLoaderData } from "remix-typedjson";
+import { z } from "zod";
+import { Button, LinkButton } from "~/components/primitives/Buttons";
+import { Input } from "~/components/primitives/Input";
+import { PaginationControls } from "~/components/primitives/Pagination";
+import { Paragraph } from "~/components/primitives/Paragraph";
+import {
+ Table,
+ TableBlankRow,
+ TableBody,
+ TableCell,
+ TableHeader,
+ TableHeaderCell,
+ TableRow,
+} from "~/components/primitives/Table";
+import { prisma } from "~/db.server";
+import { requireUserId } from "~/services/session.server";
+import { createSearchParams } from "~/utils/searchParams";
+import { seedLlmPricing } from "@internal/llm-pricing";
+import { llmPricingRegistry } from "~/v3/llmPricingRegistry.server";
+
+const PAGE_SIZE = 50;
+
+const SearchParams = z.object({
+ page: z.coerce.number().optional(),
+ search: z.string().optional(),
+});
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ const userId = await requireUserId(request);
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (!user?.admin) throw redirect("/");
+
+ const searchParams = createSearchParams(request.url, SearchParams);
+ if (!searchParams.success) throw new Error(searchParams.error);
+ const { page: rawPage, search } = searchParams.params.getAll();
+ const page = rawPage ?? 1;
+
+ const where = {
+ projectId: null as string | null,
+ ...(search ? { modelName: { contains: search, mode: "insensitive" as const } } : {}),
+ };
+
+ const [rawModels, total] = await Promise.all([
+ prisma.llmModel.findMany({
+ where,
+ include: {
+ pricingTiers: { include: { prices: true }, orderBy: { priority: "asc" } },
+ },
+ orderBy: { modelName: "asc" },
+ skip: (page - 1) * PAGE_SIZE,
+ take: PAGE_SIZE,
+ }),
+ prisma.llmModel.count({ where }),
+ ]);
+
+ // Convert Prisma Decimal to plain numbers for serialization
+ const models = rawModels.map((m) => ({
+ ...m,
+ pricingTiers: m.pricingTiers.map((t) => ({
+ ...t,
+ prices: t.prices.map((p) => ({ ...p, price: Number(p.price) })),
+ })),
+ }));
+
+ return typedjson({
+ models,
+ total,
+ page,
+ pageCount: Math.ceil(total / PAGE_SIZE),
+ filters: { search },
+ });
+};
+
+export async function action({ request }: ActionFunctionArgs) {
+ const userId = await requireUserId(request);
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (!user?.admin) throw redirect("/");
+
+ const formData = await request.formData();
+ const _action = formData.get("_action");
+
+ if (_action === "seed") {
+ console.log("[admin] seed action started");
+ const result = await seedLlmPricing(prisma);
+ console.log(`[admin] seed complete: ${result.modelsCreated} created, ${result.modelsSkipped} skipped`);
+ await llmPricingRegistry?.reload();
+ console.log("[admin] registry reloaded after seed");
+ return typedjson({
+ success: true,
+ message: `Seeded: ${result.modelsCreated} created, ${result.modelsSkipped} skipped`,
+ });
+ }
+
+ if (_action === "reload") {
+ console.log("[admin] reload action started");
+ await llmPricingRegistry?.reload();
+ console.log("[admin] registry reloaded");
+ return typedjson({ success: true, message: "Registry reloaded" });
+ }
+
+ if (_action === "test") {
+ const modelString = formData.get("modelString");
+ if (typeof modelString !== "string" || !modelString) {
+ return typedjson({ testResult: null });
+ }
+
+ // Use the registry's match() which handles prefix stripping automatically
+ const matched = llmPricingRegistry?.match(modelString) ?? null;
+
+ return typedjson({
+ testResult: {
+ modelString,
+ match: matched
+ ? { friendlyId: matched.friendlyId, modelName: matched.modelName }
+ : null,
+ },
+ });
+ }
+
+ if (_action === "delete") {
+ const modelId = formData.get("modelId");
+ if (typeof modelId === "string") {
+ await prisma.llmModel.delete({ where: { id: modelId } });
+ await llmPricingRegistry?.reload();
+ }
+ return typedjson({ success: true });
+ }
+
+ return typedjson({ error: "Unknown action" }, { status: 400 });
+}
+
+export default function AdminLlmModelsRoute() {
+ const { models, filters, page, pageCount, total } =
+ useTypedLoaderData();
+ const seedFetcher = useFetcher();
+ const reloadFetcher = useFetcher();
+ const testFetcher = useFetcher<{
+ testResult?: {
+ modelString: string;
+ match: { friendlyId: string; modelName: string } | null;
+ } | null;
+ }>();
+
+ const testResult = testFetcher.data?.testResult;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Missing models
+
+
+
+ Add model
+
+
+
+
+ {/* Model tester */}
+
+
+
+
+
+
+
+ {testResult !== undefined && testResult !== null && (
+
+
+ Testing: {testResult.modelString}
+
+ {testResult.match ? (
+
+ Match:{" "}
+
+ {testResult.match.modelName}
+
+
+ ) : (
+
+ No match found — this model has no pricing data
+
+ )}
+
+ )}
+
+
+
+
+ {total} global models (page {page} of {pageCount})
+
+
+
+
+
+
+
+ Model Name
+ Source
+ Input $/tok
+ Output $/tok
+ Other prices
+
+
+
+ {models.length === 0 ? (
+
+ No models found
+
+ ) : (
+ models.map((model) => {
+ // Get default tier prices
+ const defaultTier =
+ model.pricingTiers.find((t) => t.isDefault) ?? model.pricingTiers[0];
+ const priceMap = defaultTier
+ ? Object.fromEntries(defaultTier.prices.map((p) => [p.usageType, p.price]))
+ : {};
+ const inputPrice = priceMap["input"];
+ const outputPrice = priceMap["output"];
+ const otherPrices = defaultTier
+ ? defaultTier.prices.filter(
+ (p) => p.usageType !== "input" && p.usageType !== "output"
+ )
+ : [];
+
+ return (
+
+
+
+ {model.modelName}
+
+
+
+
+ {model.source ?? "default"}
+
+
+
+
+ {inputPrice != null ? formatPrice(inputPrice) : "-"}
+
+
+
+
+ {outputPrice != null ? formatPrice(outputPrice) : "-"}
+
+
+
+ {otherPrices.length > 0 ? (
+ p.usageType).join(", ")}>
+ +{otherPrices.length} more
+
+ ) : (
+ -
+ )}
+
+
+ );
+ })
+ )}
+
+
+
+
+
+
+ );
+}
+
+/** Format a per-token price as $/M tokens for readability */
+function formatPrice(perToken: number): string {
+ const perMillion = perToken * 1_000_000;
+ if (perMillion >= 1) return `$${perMillion.toFixed(2)}/M`;
+ if (perMillion >= 0.01) return `$${perMillion.toFixed(4)}/M`;
+ return `$${perMillion.toFixed(6)}/M`;
+}
diff --git a/apps/webapp/app/routes/admin.llm-models.missing.$model.tsx b/apps/webapp/app/routes/admin.llm-models.missing.$model.tsx
new file mode 100644
index 00000000000..78cb1c4fc91
--- /dev/null
+++ b/apps/webapp/app/routes/admin.llm-models.missing.$model.tsx
@@ -0,0 +1,469 @@
+import { useState } from "react";
+import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
+import { redirect } from "@remix-run/server-runtime";
+import { typedjson, useTypedLoaderData } from "remix-typedjson";
+import { Button, LinkButton } from "~/components/primitives/Buttons";
+import { Paragraph } from "~/components/primitives/Paragraph";
+import { prisma } from "~/db.server";
+import { requireUserId } from "~/services/session.server";
+import {
+ getMissingModelSamples,
+ type MissingModelSample,
+} from "~/services/admin/missingLlmModels.server";
+
+export const loader = async ({ request, params }: LoaderFunctionArgs) => {
+ const userId = await requireUserId(request);
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (!user?.admin) throw redirect("/");
+
+ // Model name is URL-encoded in the URL param
+ const modelName = decodeURIComponent(params.model ?? "");
+ if (!modelName) throw new Response("Missing model param", { status: 400 });
+
+ const url = new URL(request.url);
+ const lookbackHours = parseInt(url.searchParams.get("lookbackHours") ?? "24", 10);
+
+ let samples: MissingModelSample[] = [];
+ let error: string | undefined;
+
+ try {
+ samples = await getMissingModelSamples({ model: modelName, lookbackHours, limit: 10 });
+ } catch (e) {
+ error = e instanceof Error ? e.message : "Failed to query ClickHouse";
+ }
+
+ return typedjson({ modelName, samples, lookbackHours, error });
+};
+
+export default function AdminMissingModelDetailRoute() {
+ const { modelName, samples, lookbackHours, error } = useTypedLoaderData();
+ const [copied, setCopied] = useState(false);
+ const [expandedSpans, setExpandedSpans] = useState>(new Set());
+
+ const providerCosts = extractProviderCosts(samples);
+ const prompt = buildPrompt(modelName, samples, providerCosts);
+
+ function handleCopy() {
+ navigator.clipboard.writeText(prompt).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ });
+ }
+
+ function toggleSpan(spanId: string) {
+ setExpandedSpans((prev) => {
+ const next = new Set(prev);
+ if (next.has(spanId)) next.delete(spanId);
+ else next.add(spanId);
+ return next;
+ });
+ }
+
+ // Extract key token fields from the first sample for quick summary
+ const tokenSummary = samples.length > 0 ? extractTokenTypes(samples) : [];
+
+ return (
+
+
+ {/* Header */}
+
+
+
{modelName}
+
+ Missing pricing — {samples.length} sample span{samples.length !== 1 ? "s" : ""} from
+ last {lookbackHours}h
+
+
+
+
+ Add pricing
+
+
+ Back to missing
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Token types summary */}
+ {tokenSummary.length > 0 && (
+
+
+ Token types seen across samples
+
+
+ {tokenSummary.map((t) => (
+
+ {t.key}
+
+ {t.min === t.max ? t.min.toLocaleString() : `${t.min.toLocaleString()}-${t.max.toLocaleString()}`}
+
+
+ ))}
+
+
+ These are the token usage types that need pricing entries (at minimum: input, output).
+
+
+ )}
+
+ {/* Provider-reported costs */}
+ {providerCosts.length > 0 && (
+
+
+ Provider-reported cost data found in {providerCosts.length} span{providerCosts.length !== 1 ? "s" : ""}
+
+
+ {providerCosts.map((c, i) => (
+
+ {c.source}
+ ${c.cost.toFixed(6)}
+
+ ({c.inputTokens.toLocaleString()} in + {c.outputTokens.toLocaleString()} out)
+
+
+ ))}
+
+ {providerCosts[0]?.estimatedInputPrice != null && (
+
+
+ Estimated per-token rates (assuming ~3x output/input ratio):
+
+
+ input: {providerCosts[0].estimatedInputPrice.toExponential(4)}
+ output: {(providerCosts[0].estimatedOutputPrice ?? 0).toExponential(4)}
+
+
+ Cross-reference with the provider's pricing page before using these estimates.
+
+
+ )}
+
+ )}
+
+ {/* Prompt section */}
+
+
+
+ Claude Code prompt — paste this to have it add pricing for this model
+
+
+
+
+ {prompt}
+
+
+
+ {/* Sample spans */}
+
+
+ Sample spans ({samples.length})
+
+ {samples.map((s) => {
+ const expanded = expandedSpans.has(s.span_id);
+ let parsedAttrs: Record
| null = null;
+ try {
+ parsedAttrs = JSON.parse(s.attributes_text) as Record;
+ } catch {
+ // ignore
+ }
+
+ return (
+
+
+ {expanded && parsedAttrs && (
+
+
+ {JSON.stringify(parsedAttrs, null, 2)}
+
+
+ )}
+
+ );
+ })}
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Extract unique token usage types across all samples
+// ---------------------------------------------------------------------------
+
+type TokenTypeSummary = { key: string; min: number; max: number };
+
+function extractTokenTypes(samples: MissingModelSample[]): TokenTypeSummary[] {
+ const stats = new Map();
+
+ for (const s of samples) {
+ let attrs: Record;
+ try {
+ attrs = JSON.parse(s.attributes_text) as Record;
+ } catch {
+ continue;
+ }
+
+ // Collect from gen_ai.usage.*
+ const genAiUsage = getNestedObj(attrs, ["gen_ai", "usage"]);
+ if (genAiUsage) {
+ for (const [k, v] of Object.entries(genAiUsage)) {
+ if (typeof v === "number" && v > 0) {
+ const existing = stats.get(`gen_ai.usage.${k}`);
+ if (existing) {
+ existing.min = Math.min(existing.min, v);
+ existing.max = Math.max(existing.max, v);
+ } else {
+ stats.set(`gen_ai.usage.${k}`, { min: v, max: v });
+ }
+ }
+ }
+ }
+
+ // Collect from ai.usage.*
+ const aiUsage = getNestedObj(attrs, ["ai", "usage"]);
+ if (aiUsage) {
+ for (const [k, v] of Object.entries(aiUsage)) {
+ if (typeof v === "number" && v > 0) {
+ const existing = stats.get(`ai.usage.${k}`);
+ if (existing) {
+ existing.min = Math.min(existing.min, v);
+ existing.max = Math.max(existing.max, v);
+ } else {
+ stats.set(`ai.usage.${k}`, { min: v, max: v });
+ }
+ }
+ }
+ }
+ }
+
+ return Array.from(stats.entries())
+ .map(([key, { min, max }]) => ({ key, min, max }))
+ .sort((a, b) => a.key.localeCompare(b.key));
+}
+
+function getNestedObj(
+ obj: Record,
+ path: string[]
+): Record | null {
+ let current: unknown = obj;
+ for (const key of path) {
+ if (!current || typeof current !== "object") return null;
+ current = (current as Record)[key];
+ }
+ return current && typeof current === "object" ? (current as Record) : null;
+}
+
+// ---------------------------------------------------------------------------
+// Extract provider-reported costs from providerMetadata
+// ---------------------------------------------------------------------------
+
+type ProviderCostInfo = {
+ source: string; // "gateway" or "openrouter"
+ cost: number;
+ inputTokens: number;
+ outputTokens: number;
+ estimatedInputPrice?: number; // per-token estimate
+ estimatedOutputPrice?: number; // per-token estimate
+};
+
+function extractProviderCosts(samples: MissingModelSample[]): ProviderCostInfo[] {
+ const costs: ProviderCostInfo[] = [];
+
+ for (const s of samples) {
+ let attrs: Record;
+ try {
+ attrs = JSON.parse(s.attributes_text) as Record;
+ } catch {
+ continue;
+ }
+
+ // Parse providerMetadata — could be nested or stringified
+ let providerMeta: Record | null = null;
+ const aiResponse = getNestedObj(attrs, ["ai", "response"]);
+ const rawMeta = aiResponse?.providerMetadata;
+ if (typeof rawMeta === "string") {
+ try { providerMeta = JSON.parse(rawMeta) as Record; } catch {}
+ } else if (rawMeta && typeof rawMeta === "object") {
+ providerMeta = rawMeta as Record;
+ }
+ if (!providerMeta) continue;
+
+ // Get token counts
+ const genAiUsage = getNestedObj(attrs, ["gen_ai", "usage"]);
+ const inputTokens = Number(genAiUsage?.input_tokens ?? 0);
+ const outputTokens = Number(genAiUsage?.output_tokens ?? 0);
+ if (inputTokens === 0 && outputTokens === 0) continue;
+
+ // Gateway: { gateway: { cost: "0.0006615" } }
+ const gw = getNestedObj(providerMeta, ["gateway"]);
+ if (gw) {
+ const cost = parseFloat(String(gw.cost ?? "0"));
+ if (cost > 0) {
+ costs.push({ source: "gateway", cost, inputTokens, outputTokens });
+ continue;
+ }
+ }
+
+ // OpenRouter: { openrouter: { usage: { cost: 0.000135 } } }
+ const or = getNestedObj(providerMeta, ["openrouter"]);
+ const orUsage = or ? getNestedObj(or, ["usage"]) : null;
+ if (orUsage) {
+ const cost = Number(orUsage.cost ?? 0);
+ if (cost > 0) {
+ costs.push({ source: "openrouter", cost, inputTokens, outputTokens });
+ continue;
+ }
+ }
+ }
+
+ // Estimate per-token prices from aggregate costs if we have enough data
+ if (costs.length > 0) {
+ // Use least-squares to estimate input/output price from cost = input*pi + output*po
+ // With 2+ samples we can solve; with 1 we can only estimate a blended rate
+ const totalInput = costs.reduce((s, c) => s + c.inputTokens, 0);
+ const totalOutput = costs.reduce((s, c) => s + c.outputTokens, 0);
+ const totalCost = costs.reduce((s, c) => s + c.cost, 0);
+
+ if (totalInput > 0 && totalOutput > 0) {
+ // Simple approach: assume output is 2-5x input price (common ratio)
+ // Use ratio r where output_price = r * input_price
+ // totalCost = input_price * (totalInput + r * totalOutput)
+ // Try r=3 (common for many models)
+ const r = 3;
+ const estimatedInputPrice = totalCost / (totalInput + r * totalOutput);
+ const estimatedOutputPrice = estimatedInputPrice * r;
+
+ for (const c of costs) {
+ c.estimatedInputPrice = estimatedInputPrice;
+ c.estimatedOutputPrice = estimatedOutputPrice;
+ }
+ }
+ }
+
+ return costs;
+}
+
+// ---------------------------------------------------------------------------
+// Prompt builder — focused on figuring out pricing, not API mechanics
+// ---------------------------------------------------------------------------
+
+function buildPrompt(modelName: string, samples: MissingModelSample[], providerCosts: ProviderCostInfo[]): string {
+ const hasPrefix = modelName.includes("/");
+ const prefix = hasPrefix ? modelName.split("/")[0] : null;
+ const baseName = hasPrefix ? modelName.split("/").slice(1).join("/") : modelName;
+
+ // Extract token types from samples
+ const tokenTypes = extractTokenTypes(samples);
+ const tokenTypeList = tokenTypes.length > 0
+ ? tokenTypes.map((t) => ` - ${t.key}: ${t.min === t.max ? t.min : `${t.min}-${t.max}`}`).join("\n")
+ : " (no token data found in samples)";
+
+ // Get a compact sample of attributes for context
+ let sampleAttrs = "";
+ if (samples.length > 0) {
+ try {
+ const attrs = JSON.parse(samples[0].attributes_text) as Record;
+ const ai = attrs.ai as Record | undefined;
+ const aiResponse = (ai?.response ?? {}) as Record;
+ // Extract just the relevant fields
+ const compact: Record = {};
+ if (attrs.gen_ai) compact.gen_ai = attrs.gen_ai;
+ if (ai?.usage) compact["ai.usage"] = ai.usage;
+ if (aiResponse.providerMetadata) {
+ compact["ai.response.providerMetadata"] = aiResponse.providerMetadata;
+ }
+ sampleAttrs = JSON.stringify(compact, null, 2);
+ } catch {
+ // ignore
+ }
+ }
+
+ // Build suggested regex
+ const escapedBase = baseName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ const suggestedPattern = prefix
+ ? `(?i)^(${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/)?(${escapedBase})$`
+ : `(?i)^(${escapedBase})$`;
+
+ return `I need to add LLM pricing for the model "${modelName}".
+
+## Model info
+- Full model string from spans: \`${modelName}\`
+- Base model name: \`${baseName}\`${prefix ? `\n- Provider prefix: \`${prefix}\`` : ""}
+- This model appears in production spans but has no pricing data.
+
+## Token types seen in spans
+${tokenTypeList}
+
+## What I need you to do
+
+1. **Look up pricing**: Find the current per-token pricing for \`${baseName}\` from the provider's official pricing page. Search the web if needed.
+
+2. **Present the pricing to me** in the following format so I can review before adding:
+
+\`\`\`
+Model name: ${baseName}
+Match pattern: ${suggestedPattern}
+Pricing tier: Standard
+
+Prices (per token):
+ input:
+ output:
+ (add any additional token types if applicable)
+\`\`\`
+
+**IMPORTANT: Do NOT call the admin API or create the model yourself.** Just research the pricing and present it to me. I will add it via the admin dashboard or ask you to proceed once I've reviewed.
+
+## Pricing research notes
+
+- All prices should be in **cost per token** (NOT per million). To convert: divide $/M by 1,000,000.
+ - Example: $3.00/M tokens = 0.000003 per token
+- The \`matchPattern\` regex should match the model name both with and without the provider prefix.
+ - Suggested: \`${suggestedPattern}\`
+ - This matches both \`${baseName}\` and \`${modelName}\`
+- Based on the token types seen in spans, check if the provider charges differently for:
+ - \`input\` and \`output\` — always required
+ - \`input_cached_tokens\` — if the provider offers prompt caching discounts
+ - \`cache_creation_input_tokens\` — if there's a cache write cost
+ - \`reasoning_tokens\` — if the model has chain-of-thought/reasoning tokens${providerCosts.length > 0 ? `
+
+## Provider-reported costs (from ${providerCosts[0].source})
+The gateway/router is reporting costs for this model. Use these to cross-reference your pricing:
+${providerCosts.map((c) => `- $${c.cost.toFixed(6)} for ${c.inputTokens.toLocaleString()} input + ${c.outputTokens.toLocaleString()} output tokens`).join("\n")}${providerCosts[0].estimatedInputPrice != null ? `
+- Estimated per-token rates (rough, assuming ~3x output/input ratio):
+ - input: ${providerCosts[0].estimatedInputPrice.toExponential(4)} (${(providerCosts[0].estimatedInputPrice * 1_000_000).toFixed(4)} $/M)
+ - output: ${(providerCosts[0].estimatedOutputPrice ?? 0).toExponential(4)} (${((providerCosts[0].estimatedOutputPrice ?? 0) * 1_000_000).toFixed(4)} $/M)
+- Verify these against the official pricing page before using.` : ""}` : ""}${sampleAttrs ? `
+
+## Sample span attributes (first span)
+\`\`\`json
+${sampleAttrs}
+\`\`\`` : ""}`;
+}
diff --git a/apps/webapp/app/routes/admin.llm-models.missing._index.tsx b/apps/webapp/app/routes/admin.llm-models.missing._index.tsx
new file mode 100644
index 00000000000..fd933cd22e9
--- /dev/null
+++ b/apps/webapp/app/routes/admin.llm-models.missing._index.tsx
@@ -0,0 +1,158 @@
+import { useSearchParams } from "@remix-run/react";
+import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
+import { redirect } from "@remix-run/server-runtime";
+import { typedjson, useTypedLoaderData } from "remix-typedjson";
+import { z } from "zod";
+import { LinkButton } from "~/components/primitives/Buttons";
+import { Paragraph } from "~/components/primitives/Paragraph";
+import {
+ Table,
+ TableBlankRow,
+ TableBody,
+ TableCell,
+ TableHeader,
+ TableHeaderCell,
+ TableRow,
+} from "~/components/primitives/Table";
+import { prisma } from "~/db.server";
+import { requireUserId } from "~/services/session.server";
+import { getMissingLlmModels } from "~/services/admin/missingLlmModels.server";
+
+const LOOKBACK_OPTIONS = [
+ { label: "1 hour", value: 1 },
+ { label: "6 hours", value: 6 },
+ { label: "24 hours", value: 24 },
+ { label: "7 days", value: 168 },
+ { label: "30 days", value: 720 },
+];
+
+const SearchParams = z.object({
+ lookbackHours: z.coerce.number().optional(),
+});
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ const userId = await requireUserId(request);
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (!user?.admin) throw redirect("/");
+
+ const url = new URL(request.url);
+ const lookbackHours = parseInt(url.searchParams.get("lookbackHours") ?? "24", 10);
+
+ let models: Awaited> = [];
+ let error: string | undefined;
+
+ try {
+ models = await getMissingLlmModels({ lookbackHours });
+ } catch (e) {
+ error = e instanceof Error ? e.message : "Failed to query ClickHouse";
+ }
+
+ return typedjson({ models, lookbackHours, error });
+};
+
+export default function AdminLlmModelsMissingRoute() {
+ const { models, lookbackHours, error } = useTypedLoaderData();
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ return (
+
+
+
+
Missing LLM Models
+
+ Back to models
+
+
+
+
+ Models appearing in spans without cost enrichment. These models need pricing data added.
+
+
+ {/* Lookback selector */}
+
+ Lookback:
+ {LOOKBACK_OPTIONS.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {models.length} unpriced model{models.length !== 1 ? "s" : ""} found in the last{" "}
+ {lookbackHours < 24
+ ? `${lookbackHours}h`
+ : lookbackHours < 168
+ ? `${lookbackHours / 24}d`
+ : `${Math.round(lookbackHours / 24)}d`}
+
+
+
+
+
+ Model Name
+ Provider
+ Span Count
+ Actions
+
+
+
+ {models.length === 0 ? (
+
+ All models have pricing data
+
+ ) : (
+ models.map((m) => (
+
+ ))
+ )}
+
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Row component with link to detail page
+// ---------------------------------------------------------------------------
+
+function MissingModelRow({ model: m }: { model: { model: string; system: string; count: number } }) {
+ return (
+
+
+
+ {m.model}
+
+
+
+ {m.system || "-"}
+
+
+ {m.count.toLocaleString()}
+
+
+
+ Details
+
+
+
+ );
+}
diff --git a/apps/webapp/app/routes/admin.llm-models.new.tsx b/apps/webapp/app/routes/admin.llm-models.new.tsx
new file mode 100644
index 00000000000..20c6e1461f2
--- /dev/null
+++ b/apps/webapp/app/routes/admin.llm-models.new.tsx
@@ -0,0 +1,397 @@
+import { Form, useActionData, useSearchParams } from "@remix-run/react";
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
+import { redirect } from "@remix-run/server-runtime";
+import { typedjson } from "remix-typedjson";
+import { z } from "zod";
+import { useState } from "react";
+import { Button, LinkButton } from "~/components/primitives/Buttons";
+import { Input } from "~/components/primitives/Input";
+import { prisma } from "~/db.server";
+import { requireUserId } from "~/services/session.server";
+import { generateFriendlyId } from "~/v3/friendlyIdentifiers";
+import { llmPricingRegistry } from "~/v3/llmPricingRegistry.server";
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ const userId = await requireUserId(request);
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (!user?.admin) throw redirect("/");
+ return typedjson({});
+};
+
+const CreateSchema = z.object({
+ modelName: z.string().min(1),
+ matchPattern: z.string().min(1),
+ pricingTiersJson: z.string(),
+});
+
+export async function action({ request }: ActionFunctionArgs) {
+ const userId = await requireUserId(request);
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (!user?.admin) throw redirect("/");
+
+ const formData = await request.formData();
+ const raw = Object.fromEntries(formData);
+ console.log("[admin] create model form data:", JSON.stringify(raw).slice(0, 500));
+ const parsed = CreateSchema.safeParse(raw);
+
+ if (!parsed.success) {
+ console.log("[admin] create model validation error:", JSON.stringify(parsed.error.issues));
+ return typedjson({ error: "Invalid form data", details: parsed.error.issues }, { status: 400 });
+ }
+
+ const { modelName, matchPattern, pricingTiersJson } = parsed.data;
+
+ // Validate regex — strip (?i) POSIX flag since our registry handles it
+ try {
+ const testPattern = matchPattern.startsWith("(?i)") ? matchPattern.slice(4) : matchPattern;
+ new RegExp(testPattern);
+ } catch {
+ return typedjson({ error: "Invalid regex in matchPattern" }, { status: 400 });
+ }
+
+ let pricingTiers: Array<{
+ name: string;
+ isDefault: boolean;
+ priority: number;
+ conditions: Array<{ usageDetailPattern: string; operator: string; value: number }>;
+ prices: Record;
+ }>;
+ try {
+ pricingTiers = JSON.parse(pricingTiersJson) as typeof pricingTiers;
+ } catch {
+ return typedjson({ error: "Invalid pricing tiers JSON" }, { status: 400 });
+ }
+
+ const model = await prisma.llmModel.create({
+ data: {
+ friendlyId: generateFriendlyId("llm_model"),
+ modelName,
+ matchPattern,
+ source: "admin",
+ },
+ });
+
+ for (const tier of pricingTiers) {
+ await prisma.llmPricingTier.create({
+ data: {
+ modelId: model.id,
+ name: tier.name,
+ isDefault: tier.isDefault,
+ priority: tier.priority,
+ conditions: tier.conditions,
+ prices: {
+ create: Object.entries(tier.prices).map(([usageType, price]) => ({
+ modelId: model.id,
+ usageType,
+ price,
+ })),
+ },
+ },
+ });
+ }
+
+ await llmPricingRegistry?.reload();
+ return redirect(`/admin/llm-models/${model.friendlyId}`);
+}
+
+export default function AdminLlmModelNewRoute() {
+ const actionData = useActionData<{ error?: string; details?: unknown[] }>();
+ const [params] = useSearchParams();
+ const initialModelName = params.get("modelName") ?? "";
+ const [modelName, setModelName] = useState(initialModelName);
+ const [matchPattern, setMatchPattern] = useState("");
+ const [testInput, setTestInput] = useState("");
+ const [tiers, setTiers] = useState([
+ { name: "Standard", isDefault: true, priority: 0, conditions: [], prices: { input: 0, output: 0 } },
+ ]);
+
+ let testResult: boolean | null = null;
+ if (testInput && matchPattern) {
+ try {
+ const pattern = matchPattern.startsWith("(?i)")
+ ? matchPattern.slice(4)
+ : matchPattern;
+ testResult = new RegExp(pattern, "i").test(testInput);
+ } catch {
+ testResult = null;
+ }
+ }
+
+ // Auto-generate match pattern from model name
+ function autoPattern() {
+ if (modelName) {
+ const escaped = modelName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ setMatchPattern(`(?i)^(${escaped})$`);
+ }
+ }
+
+ return (
+
+
+
+
New LLM Model
+
+ Back to list
+
+
+
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Shared tier editor (duplicated from detail page — could be extracted later)
+// ---------------------------------------------------------------------------
+
+type TierData = {
+ name: string;
+ isDefault: boolean;
+ priority: number;
+ conditions: Array<{ usageDetailPattern: string; operator: string; value: number }>;
+ prices: Record;
+};
+
+const COMMON_USAGE_TYPES = [
+ "input",
+ "output",
+ "input_cached_tokens",
+ "cache_creation_input_tokens",
+ "reasoning_tokens",
+];
+
+function TierEditor({
+ tier,
+ onChange,
+ onRemove,
+}: {
+ tier: TierData;
+ onChange: (t: TierData) => void;
+ onRemove: () => void;
+}) {
+ const [newUsageType, setNewUsageType] = useState("");
+
+ return (
+
+
+
+
+
Prices (per token)
+
+ {Object.entries(tier.prices).map(([usageType, price]) => (
+
+ {usageType}
+ {
+ const val = parseFloat(e.target.value);
+ if (!isNaN(val)) {
+ onChange({ ...tier, prices: { ...tier.prices, [usageType]: val } });
+ }
+ }}
+ />
+
+
+ ))}
+
+
+
+
+ {newUsageType && (
+
+ )}
+
+
+
+ );
+}
diff --git a/apps/webapp/app/routes/admin.tsx b/apps/webapp/app/routes/admin.tsx
index ac8e56c855e..34792e66ee5 100644
--- a/apps/webapp/app/routes/admin.tsx
+++ b/apps/webapp/app/routes/admin.tsx
@@ -32,6 +32,10 @@ export default function Page() {
label: "Concurrency",
to: "/admin/concurrency",
},
+ {
+ label: "LLM Models",
+ to: "/admin/llm-models",
+ },
]}
layoutId={"admin"}
/>
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
index e46eaa5148f..8f7ae61b5d5 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
@@ -19,7 +19,7 @@ import {
taskRunErrorEnhancer,
} from "@trigger.dev/core/v3";
import { assertNever } from "assert-never";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { typedjson, useTypedFetcher } from "remix-typedjson";
import { ExitIcon } from "~/assets/icons/ExitIcon";
import { FlagIcon } from "~/assets/icons/RegionIcons";
@@ -60,6 +60,7 @@ import { RunIcon } from "~/components/runs/v3/RunIcon";
import { RunTag } from "~/components/runs/v3/RunTag";
import { TruncatedCopyableValue } from "~/components/primitives/TruncatedCopyableValue";
import { SpanEvents } from "~/components/runs/v3/SpanEvents";
+import { AISpanDetails } from "~/components/runs/v3/ai";
import { SpanTitle } from "~/components/runs/v3/SpanTitle";
import { TaskRunAttemptStatusCombo } from "~/components/runs/v3/TaskRunAttemptStatus";
import {
@@ -252,6 +253,8 @@ function SpanBody({
span = applySpanOverrides(span, spanOverrides);
+ const isAiGeneration = span.entity?.type === "ai-generation";
+
return (
@@ -276,9 +279,13 @@ function SpanBody({
/>
)}
-
+ {isAiGeneration ? (
-
+ ) : (
+
+
+
+ )}
);
}
@@ -1155,6 +1162,35 @@ function RunError({ error }: { error: TaskRunError }) {
}
}
+function CollapsibleProperties({ code }: { code: string }) {
+ const [open, setOpen] = useState(false);
+ return (
+
+
+ {open && (
+
+
+
+ )}
+
+ );
+}
+
function SpanEntity({ span }: { span: Span }) {
const isAdmin = useHasAdminAccess();
@@ -1352,6 +1388,14 @@ function SpanEntity({ span }: { span: Span }) {
/>
);
}
+ case "ai-generation": {
+ return (
+
+ );
+ }
default: {
assertNever(span.entity);
}
diff --git a/apps/webapp/app/services/admin/missingLlmModels.server.ts b/apps/webapp/app/services/admin/missingLlmModels.server.ts
new file mode 100644
index 00000000000..7ce6bc2ab7e
--- /dev/null
+++ b/apps/webapp/app/services/admin/missingLlmModels.server.ts
@@ -0,0 +1,129 @@
+import { adminClickhouseClient } from "~/services/clickhouseInstance.server";
+import { llmPricingRegistry } from "~/v3/llmPricingRegistry.server";
+
+export type MissingLlmModel = {
+ model: string;
+ system: string;
+ count: number;
+};
+
+export async function getMissingLlmModels(opts: {
+ lookbackHours?: number;
+} = {}): Promise {
+ const lookbackHours = opts.lookbackHours ?? 24;
+ const since = new Date(Date.now() - lookbackHours * 60 * 60 * 1000);
+
+ // queryBuilderFast returns a factory function — call it to get the builder
+ const createBuilder = adminClickhouseClient.reader.queryBuilderFast<{
+ model: string;
+ system: string;
+ cnt: string;
+ }>({
+ name: "missingLlmModels",
+ table: "trigger_dev.task_events_v2",
+ columns: [
+ { name: "model", expression: "attributes.gen_ai.response.model.:String" },
+ { name: "system", expression: "attributes.gen_ai.system.:String" },
+ { name: "cnt", expression: "count()" },
+ ],
+ });
+ const qb = createBuilder();
+
+ // Partition pruning on inserted_at (partition key is toDate(inserted_at))
+ qb.where("inserted_at >= {since: DateTime64(3)}", {
+ since: formatDateTime(since),
+ });
+
+ // Only spans that have a model set
+ qb.where("attributes.gen_ai.response.model.:String != {empty: String}", { empty: "" });
+
+ // Only spans that were NOT cost-enriched (trigger.llm.total_cost is NULL)
+ qb.where("attributes.trigger.llm.total_cost.:Float64 IS NULL", {});
+
+ // Only completed spans
+ qb.where("kind = {kind: String}", { kind: "SPAN" });
+ qb.where("status = {status: String}", { status: "OK" });
+
+ qb.groupBy("model, system");
+ qb.orderBy("cnt DESC");
+ qb.limit(100);
+
+ const [err, rows] = await qb.execute();
+
+ if (err) {
+ throw err;
+ }
+
+ if (!rows) {
+ return [];
+ }
+
+ const candidates = rows
+ .filter((r) => r.model)
+ .map((r) => ({
+ model: r.model,
+ system: r.system,
+ count: parseInt(r.cnt, 10),
+ }));
+
+ if (candidates.length === 0) return [];
+
+ // Filter out models that now have pricing in the database (added after spans were inserted).
+ // The registry's match() handles prefix stripping for gateway/openrouter models.
+ if (!llmPricingRegistry || !llmPricingRegistry.isLoaded) return candidates;
+ const registry = llmPricingRegistry;
+ return candidates.filter((c) => !registry.match(c.model));
+}
+
+export type MissingModelSample = {
+ span_id: string;
+ run_id: string;
+ message: string;
+ attributes_text: string;
+ duration: string;
+ start_time: string;
+};
+
+export async function getMissingModelSamples(opts: {
+ model: string;
+ lookbackHours?: number;
+ limit?: number;
+}): Promise {
+ const lookbackHours = opts.lookbackHours ?? 24;
+ const limit = opts.limit ?? 10;
+ const since = new Date(Date.now() - lookbackHours * 60 * 60 * 1000);
+
+ const createBuilder = adminClickhouseClient.reader.queryBuilderFast({
+ name: "missingModelSamples",
+ table: "trigger_dev.task_events_v2",
+ columns: [
+ "span_id",
+ "run_id",
+ "message",
+ "attributes_text",
+ "duration",
+ "start_time",
+ ],
+ });
+ const qb = createBuilder();
+
+ qb.where("inserted_at >= {since: DateTime64(3)}", { since: formatDateTime(since) });
+ qb.where("attributes.gen_ai.response.model.:String = {model: String}", { model: opts.model });
+ qb.where("attributes.trigger.llm.total_cost.:Float64 IS NULL", {});
+ qb.where("kind = {kind: String}", { kind: "SPAN" });
+ qb.where("status = {status: String}", { status: "OK" });
+ qb.orderBy("start_time DESC");
+ qb.limit(limit);
+
+ const [err, rows] = await qb.execute();
+
+ if (err) {
+ throw err;
+ }
+
+ return rows ?? [];
+}
+
+function formatDateTime(date: Date): string {
+ return date.toISOString().replace("T", " ").replace("Z", "");
+}
diff --git a/apps/webapp/app/services/clickhouseInstance.server.ts b/apps/webapp/app/services/clickhouseInstance.server.ts
index 61494811a0e..9c4941671f3 100644
--- a/apps/webapp/app/services/clickhouseInstance.server.ts
+++ b/apps/webapp/app/services/clickhouseInstance.server.ts
@@ -71,6 +71,34 @@ function initializeLogsClickhouseClient() {
});
}
+export const adminClickhouseClient = singleton(
+ "adminClickhouseClient",
+ initializeAdminClickhouseClient
+);
+
+function initializeAdminClickhouseClient() {
+ if (!env.ADMIN_CLICKHOUSE_URL) {
+ throw new Error("ADMIN_CLICKHOUSE_URL is not set");
+ }
+
+ const url = new URL(env.ADMIN_CLICKHOUSE_URL);
+ url.searchParams.delete("secure");
+
+ return new ClickHouse({
+ url: url.toString(),
+ name: "admin-clickhouse",
+ keepAlive: {
+ enabled: env.CLICKHOUSE_KEEP_ALIVE_ENABLED === "1",
+ idleSocketTtl: env.CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS,
+ },
+ logLevel: env.CLICKHOUSE_LOG_LEVEL,
+ compression: {
+ request: true,
+ },
+ maxOpenConnections: env.CLICKHOUSE_MAX_OPEN_CONNECTIONS,
+ });
+}
+
export const queryClickhouseClient = singleton(
"queryClickhouseClient",
initializeQueryClickhouseClient
diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css
index 2e435a032b7..8ec29b91568 100644
--- a/apps/webapp/app/tailwind.css
+++ b/apps/webapp/app/tailwind.css
@@ -85,6 +85,10 @@
}
@layer utilities {
+ .scrollbar-gutter-stable {
+ scrollbar-gutter: stable;
+ }
+
.animated-gradient-glow {
position: relative;
overflow: visible;
diff --git a/apps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.ts b/apps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.ts
index 9100ad84fec..531c3e307e9 100644
--- a/apps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.ts
+++ b/apps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.ts
@@ -1,5 +1,6 @@
import type {
ClickHouse,
+ LlmUsageV1Input,
TaskEventDetailedSummaryV1Result,
TaskEventDetailsV1Result,
TaskEventSummaryV1Result,
@@ -7,6 +8,7 @@ import type {
TaskEventV2Input,
} from "@internal/clickhouse";
import { Attributes, startSpan, trace, Tracer } from "@internal/tracing";
+
import { createJsonErrorObject } from "@trigger.dev/core/v3/errors";
import { serializeTraceparent } from "@trigger.dev/core/v3/isomorphic";
import {
@@ -94,6 +96,7 @@ export class ClickhouseEventRepository implements IEventRepository {
private _clickhouse: ClickHouse;
private _config: ClickhouseEventRepositoryConfig;
private readonly _flushScheduler: DynamicFlushScheduler;
+ private readonly _llmUsageFlushScheduler: DynamicFlushScheduler;
private _tracer: Tracer;
private _version: "v1" | "v2";
@@ -118,6 +121,17 @@ export class ClickhouseEventRepository implements IEventRepository {
return event.kind === "DEBUG_EVENT";
},
});
+
+ this._llmUsageFlushScheduler = new DynamicFlushScheduler({
+ batchSize: 5000,
+ flushInterval: 2000,
+ callback: this.#flushLlmUsageBatch.bind(this),
+ minConcurrency: 1,
+ maxConcurrency: 2,
+ maxBatchSize: 10000,
+ memoryPressureThreshold: 10000,
+ loadSheddingEnabled: false,
+ });
}
get version() {
@@ -216,6 +230,55 @@ export class ClickhouseEventRepository implements IEventRepository {
});
}
+ async #flushLlmUsageBatch(flushId: string, rows: LlmUsageV1Input[]) {
+
+ const [insertError] = await this._clickhouse.llmUsage.insert(rows, {
+ params: {
+ clickhouse_settings: this.#getClickhouseInsertSettings(),
+ },
+ });
+
+ if (insertError) {
+ throw insertError;
+ }
+
+ logger.info("ClickhouseEventRepository.flushLlmUsageBatch Inserted LLM usage batch", {
+ rows: rows.length,
+ });
+ }
+
+ #createLlmUsageInput(event: CreateEventInput): LlmUsageV1Input {
+ const llmUsage = event._llmUsage!;
+
+ return {
+ organization_id: event.organizationId,
+ project_id: event.projectId,
+ environment_id: event.environmentId,
+ run_id: event.runId,
+ task_identifier: event.taskSlug,
+ trace_id: event.traceId,
+ span_id: event.spanId,
+ gen_ai_system: llmUsage.genAiSystem,
+ request_model: llmUsage.requestModel,
+ response_model: llmUsage.responseModel,
+ matched_model_id: llmUsage.matchedModelId,
+ operation_name: llmUsage.operationName,
+ pricing_tier_id: llmUsage.pricingTierId,
+ pricing_tier_name: llmUsage.pricingTierName,
+ input_tokens: llmUsage.inputTokens,
+ output_tokens: llmUsage.outputTokens,
+ total_tokens: llmUsage.totalTokens,
+ usage_details: llmUsage.usageDetails,
+ input_cost: llmUsage.inputCost,
+ output_cost: llmUsage.outputCost,
+ total_cost: llmUsage.totalCost,
+ cost_details: llmUsage.costDetails,
+ metadata: llmUsage.metadata,
+ start_time: this.#clampAndFormatStartTime(event.startTime.toString()),
+ duration: formatClickhouseUnsignedIntegerString(event.duration ?? 0),
+ };
+ }
+
#getClickhouseInsertSettings() {
if (this._config.insertStrategy === "insert") {
return {};
@@ -236,6 +299,15 @@ export class ClickhouseEventRepository implements IEventRepository {
async insertMany(events: CreateEventInput[]): Promise {
this.addToBatch(events.flatMap((event) => this.createEventToTaskEventV1Input(event)));
+
+ // Dual-write LLM usage records for spans with cost enrichment
+ const llmUsageRows = events
+ .filter((e) => e._llmUsage != null)
+ .map((e) => this.#createLlmUsageInput(e));
+
+ if (llmUsageRows.length > 0) {
+ this._llmUsageFlushScheduler.addToBatch(llmUsageRows);
+ }
}
async insertManyImmediate(events: CreateEventInput[]): Promise {
@@ -1302,19 +1374,21 @@ export class ClickhouseEventRepository implements IEventRepository {
}
}
- if (
- (span.properties == null ||
- (typeof span.properties === "object" && Object.keys(span.properties).length === 0)) &&
- typeof record.attributes_text === "string"
- ) {
- const parsedAttributes = this.#parseAttributes(record.attributes_text);
- const resourceAttributes = parsedAttributes["$resource"];
+ if (typeof record.attributes_text === "string") {
+ const shouldUpdate =
+ span.properties == null ||
+ (typeof span.properties === "object" && Object.keys(span.properties).length === 0) ||
+ (record.kind === "SPAN" && record.status !== "PARTIAL");
+
+ if (shouldUpdate) {
+ const parsedAttributes = this.#parseAttributes(record.attributes_text);
+ const resourceAttributes = parsedAttributes["$resource"];
- // Remove the $resource key from the attributes
- delete parsedAttributes["$resource"];
+ delete parsedAttributes["$resource"];
- span.properties = parsedAttributes;
- span.resourceProperties = resourceAttributes as Record | undefined;
+ span.properties = parsedAttributes;
+ span.resourceProperties = resourceAttributes as Record | undefined;
+ }
}
}
@@ -1525,7 +1599,13 @@ export class ClickhouseEventRepository implements IEventRepository {
}
if (parsedMetadata && "style" in parsedMetadata && parsedMetadata.style) {
- span.data.style = parsedMetadata.style as TaskEventStyle;
+ const newStyle = parsedMetadata.style as TaskEventStyle;
+ // Merge styles: prefer the most complete value for each field
+ span.data.style = {
+ icon: newStyle.icon ?? span.data.style.icon,
+ variant: newStyle.variant ?? span.data.style.variant,
+ accessory: newStyle.accessory ?? span.data.style.accessory,
+ };
}
if (record.kind === "SPAN") {
diff --git a/apps/webapp/app/v3/eventRepository/common.server.ts b/apps/webapp/app/v3/eventRepository/common.server.ts
index 2e3bdf37c50..3ba8a50c7f7 100644
--- a/apps/webapp/app/v3/eventRepository/common.server.ts
+++ b/apps/webapp/app/v3/eventRepository/common.server.ts
@@ -140,7 +140,8 @@ export function createExceptionPropertiesFromError(error: TaskRunError): Excepti
}
}
-// removes keys that start with a $ sign. If there are no keys left, return undefined
+// Removes internal/private attribute keys from span properties.
+// Filters: "$" prefixed keys (private metadata) and "ctx." prefixed keys (Trigger.dev run context)
export function removePrivateProperties(
attributes: Attributes | undefined | null
): Attributes | undefined {
@@ -151,7 +152,7 @@ export function removePrivateProperties(
const result: Attributes = {};
for (const [key, value] of Object.entries(attributes)) {
- if (key.startsWith("$")) {
+ if (key.startsWith("$") || key.startsWith("ctx.")) {
continue;
}
diff --git a/apps/webapp/app/v3/eventRepository/eventRepository.types.ts b/apps/webapp/app/v3/eventRepository/eventRepository.types.ts
index 75664ad0525..69590dc9493 100644
--- a/apps/webapp/app/v3/eventRepository/eventRepository.types.ts
+++ b/apps/webapp/app/v3/eventRepository/eventRepository.types.ts
@@ -21,6 +21,25 @@ export type { ExceptionEventProperties };
// Event Creation Types
// ============================================================================
+export type LlmUsageData = {
+ genAiSystem: string;
+ requestModel: string;
+ responseModel: string;
+ matchedModelId: string;
+ operationName: string;
+ pricingTierId: string;
+ pricingTierName: string;
+ inputTokens: number;
+ outputTokens: number;
+ totalTokens: number;
+ usageDetails: Record;
+ inputCost: number;
+ outputCost: number;
+ totalCost: number;
+ costDetails: Record;
+ metadata: Record;
+};
+
export type CreateEventInput = Omit<
Prisma.TaskEventCreateInput,
| "id"
@@ -57,6 +76,9 @@ export type CreateEventInput = Omit<
metadata: Attributes | undefined;
style: Attributes | undefined;
machineId?: string;
+ runTags?: string[];
+ /** Side-channel data for LLM cost tracking, populated by enrichCreatableEvents */
+ _llmUsage?: LlmUsageData;
};
export type CreatableEventKind = TaskEventKind;
diff --git a/apps/webapp/app/v3/llmPricingRegistry.server.ts b/apps/webapp/app/v3/llmPricingRegistry.server.ts
new file mode 100644
index 00000000000..e90a84689f5
--- /dev/null
+++ b/apps/webapp/app/v3/llmPricingRegistry.server.ts
@@ -0,0 +1,42 @@
+import { ModelPricingRegistry, seedLlmPricing } from "@internal/llm-pricing";
+import { prisma, $replica } from "~/db.server";
+import { env } from "~/env.server";
+import { singleton } from "~/utils/singleton";
+import { setLlmPricingRegistry } from "./utils/enrichCreatableEvents.server";
+
+async function initRegistry(registry: ModelPricingRegistry) {
+ if (env.LLM_PRICING_SEED_ON_STARTUP) {
+ const result = await seedLlmPricing(prisma);
+ }
+
+ await registry.loadFromDatabase();
+}
+
+export const llmPricingRegistry = singleton("llmPricingRegistry", () => {
+ if (!env.LLM_COST_TRACKING_ENABLED) {
+ return null;
+ }
+
+ const registry = new ModelPricingRegistry($replica);
+
+ // Wire up the registry so enrichCreatableEvents can use it
+ setLlmPricingRegistry(registry);
+
+ initRegistry(registry).catch((err) => {
+ console.error("Failed to initialize LLM pricing registry", err);
+ });
+
+ // Periodic reload
+ const reloadInterval = env.LLM_PRICING_RELOAD_INTERVAL_MS;
+ setInterval(() => {
+ registry
+ .reload()
+ .then(() => {
+ })
+ .catch((err) => {
+ console.error("Failed to reload LLM pricing registry", err);
+ });
+ }, reloadInterval);
+
+ return registry;
+});
diff --git a/apps/webapp/app/v3/otlpExporter.server.ts b/apps/webapp/app/v3/otlpExporter.server.ts
index 837071b7de7..0a86fb65ec9 100644
--- a/apps/webapp/app/v3/otlpExporter.server.ts
+++ b/apps/webapp/app/v3/otlpExporter.server.ts
@@ -37,6 +37,9 @@ import type {
} from "./eventRepository/eventRepository.types";
import { startSpan } from "./tracing.server";
import { enrichCreatableEvents } from "./utils/enrichCreatableEvents.server";
+import "./llmPricingRegistry.server"; // Initialize LLM pricing registry on startup
+import { trail } from "agentcrumbs"; // @crumbs
+const crumbOtlp = trail("webapp:otlp-exporter"); // @crumbs
import { env } from "~/env.server";
import { detectBadJsonStrings } from "~/utils/detectBadJsonStrings";
import { singleton } from "~/utils/singleton";
@@ -391,6 +394,24 @@ function convertSpansToCreateableEvents(
SemanticInternalAttributes.METADATA
);
+ const runTags = extractArrayAttribute(span.attributes ?? [], SemanticInternalAttributes.RUN_TAGS);
+
+ // #region @crumbs
+ if (span.attributes) {
+ crumbOtlp("span raw OTEL attrs", {
+ spanName: span.name,
+ spanId: binaryToHex(span.spanId),
+ attrCount: span.attributes.length,
+ attrs: span.attributes.map((a) => ({
+ key: a.key,
+ type: a.value?.stringValue !== undefined ? "string" : a.value?.intValue !== undefined ? "int" : a.value?.doubleValue !== undefined ? "double" : a.value?.boolValue !== undefined ? "bool" : a.value?.arrayValue ? "array" : a.value?.bytesValue ? "bytes" : "unknown",
+ ...(a.value?.arrayValue ? { arrayLen: a.value.arrayValue.values?.length } : {}),
+ ...(a.value?.stringValue !== undefined ? { strLen: a.value.stringValue.length } : {}),
+ })),
+ });
+ }
+ // #endregion @crumbs
+
const properties =
truncateAttributes(
convertKeyValueItemsToMap(span.attributes ?? [], [], undefined, [
@@ -439,6 +460,7 @@ function convertSpansToCreateableEvents(
runId: spanProperties.runId ?? resourceProperties.runId ?? "unknown",
taskSlug: spanProperties.taskSlug ?? resourceProperties.taskSlug ?? "unknown",
machineId: spanProperties.machineId ?? resourceProperties.machineId,
+ runTags,
attemptNumber:
extractNumberAttribute(
span.attributes ?? [],
@@ -708,6 +730,8 @@ function convertKeyValueItemsToMap(
? attribute.value.boolValue
: isBytesValue(attribute.value)
? binaryToHex(attribute.value.bytesValue)
+ : isArrayValue(attribute.value)
+ ? serializeArrayValue(attribute.value.arrayValue!.values)
: undefined;
return map;
@@ -744,6 +768,8 @@ function convertSelectedKeyValueItemsToMap(
? attribute.value.boolValue
: isBytesValue(attribute.value)
? binaryToHex(attribute.value.bytesValue)
+ : isArrayValue(attribute.value)
+ ? serializeArrayValue(attribute.value.arrayValue!.values)
: undefined;
return map;
@@ -1000,6 +1026,21 @@ function extractBooleanAttribute(
return isBoolValue(attribute?.value) ? attribute.value.boolValue : fallback;
}
+function extractArrayAttribute(
+ attributes: KeyValue[],
+ name: string | Array
+): string[] | undefined {
+ const key = Array.isArray(name) ? name.filter(Boolean).join(".") : name;
+
+ const attribute = attributes.find((attribute) => attribute.key === key);
+
+ if (!attribute?.value?.arrayValue?.values) return undefined;
+
+ return attribute.value.arrayValue.values
+ .filter((v): v is { stringValue: string } => isStringValue(v))
+ .map((v) => v.stringValue);
+}
+
function isPartialSpan(span: Span): boolean {
if (!span.attributes) return false;
@@ -1042,6 +1083,31 @@ function isBytesValue(value: AnyValue | undefined): value is { bytesValue: Buffe
return Buffer.isBuffer(value.bytesValue);
}
+function isArrayValue(
+ value: AnyValue | undefined
+): value is { arrayValue: { values: AnyValue[] } } {
+ if (!value) return false;
+
+ return value.arrayValue != null && Array.isArray(value.arrayValue.values);
+}
+
+/**
+ * Serialize an OTEL array value into a JSON string.
+ * For arrays of strings, produces a JSON array: `["item1","item2"]`
+ * For mixed types, extracts primitives and serializes.
+ */
+function serializeArrayValue(values: AnyValue[]): string {
+ const items = values.map((v) => {
+ if (isStringValue(v)) return v.stringValue;
+ if (isIntValue(v)) return Number(v.intValue);
+ if (isDoubleValue(v)) return v.doubleValue;
+ if (isBoolValue(v)) return v.boolValue;
+ return null;
+ });
+
+ return JSON.stringify(items);
+}
+
function binaryToHex(buffer: Buffer | string): string;
function binaryToHex(buffer: Buffer | string | undefined): string | undefined;
function binaryToHex(buffer: Buffer | string | undefined): string | undefined {
diff --git a/apps/webapp/app/v3/querySchemas.ts b/apps/webapp/app/v3/querySchemas.ts
index 53c3be60fa2..7ec23285d0c 100644
--- a/apps/webapp/app/v3/querySchemas.ts
+++ b/apps/webapp/app/v3/querySchemas.ts
@@ -599,7 +599,159 @@ export const metricsSchema: TableSchema = {
/**
* All available schemas for the query editor
*/
-export const querySchemas: TableSchema[] = [runsSchema, metricsSchema];
+/**
+ * Schema definition for the llm_usage table (trigger_dev.llm_usage_v1)
+ */
+export const llmUsageSchema: TableSchema = {
+ name: "llm_usage",
+ clickhouseName: "trigger_dev.llm_usage_v1",
+ description: "LLM token usage and cost data from GenAI spans",
+ timeConstraint: "start_time",
+ tenantColumns: {
+ organizationId: "organization_id",
+ projectId: "project_id",
+ environmentId: "environment_id",
+ },
+ columns: {
+ environment: {
+ name: "environment",
+ clickhouseName: "environment_id",
+ ...column("String", { description: "The environment slug", example: "prod" }),
+ fieldMapping: "environment",
+ customRenderType: "environment",
+ },
+ project: {
+ name: "project",
+ clickhouseName: "project_id",
+ ...column("String", {
+ description: "The project reference, they always start with `proj_`.",
+ example: "proj_howcnaxbfxdmwmxazktx",
+ }),
+ fieldMapping: "project",
+ customRenderType: "project",
+ },
+ run_id: {
+ name: "run_id",
+ ...column("String", {
+ description: "The run ID",
+ customRenderType: "runId",
+ coreColumn: true,
+ }),
+ },
+ task_identifier: {
+ name: "task_identifier",
+ ...column("LowCardinality(String)", {
+ description: "The task identifier",
+ example: "my-task",
+ coreColumn: true,
+ }),
+ },
+ gen_ai_system: {
+ name: "gen_ai_system",
+ ...column("LowCardinality(String)", {
+ description: "AI provider (e.g. openai, anthropic)",
+ example: "openai",
+ coreColumn: true,
+ }),
+ },
+ request_model: {
+ name: "request_model",
+ ...column("String", {
+ description: "The model name requested",
+ example: "gpt-4o",
+ }),
+ },
+ response_model: {
+ name: "response_model",
+ ...column("String", {
+ description: "The model name returned by the provider",
+ example: "gpt-4o-2024-08-06",
+ coreColumn: true,
+ }),
+ },
+ operation_name: {
+ name: "operation_name",
+ ...column("LowCardinality(String)", {
+ description: "Operation type (e.g. chat, completion)",
+ example: "chat",
+ }),
+ },
+ input_tokens: {
+ name: "input_tokens",
+ ...column("UInt64", {
+ description: "Number of input tokens",
+ example: "702",
+ }),
+ },
+ output_tokens: {
+ name: "output_tokens",
+ ...column("UInt64", {
+ description: "Number of output tokens",
+ example: "22",
+ }),
+ },
+ total_tokens: {
+ name: "total_tokens",
+ ...column("UInt64", {
+ description: "Total token count",
+ example: "724",
+ }),
+ },
+ input_cost: {
+ name: "input_cost",
+ ...column("Decimal64(12)", {
+ description: "Input cost in USD",
+ customRenderType: "costInDollars",
+ }),
+ },
+ output_cost: {
+ name: "output_cost",
+ ...column("Decimal64(12)", {
+ description: "Output cost in USD",
+ customRenderType: "costInDollars",
+ }),
+ },
+ total_cost: {
+ name: "total_cost",
+ ...column("Decimal64(12)", {
+ description: "Total cost in USD",
+ customRenderType: "costInDollars",
+ coreColumn: true,
+ }),
+ },
+ pricing_tier_name: {
+ name: "pricing_tier_name",
+ ...column("LowCardinality(String)", {
+ description: "The matched pricing tier name",
+ example: "Standard",
+ }),
+ },
+ start_time: {
+ name: "start_time",
+ ...column("DateTime64(9)", {
+ description: "When the LLM call started",
+ coreColumn: true,
+ }),
+ },
+ duration: {
+ name: "duration",
+ ...column("UInt64", {
+ description: "Span duration in nanoseconds",
+ customRenderType: "durationNs",
+ }),
+ },
+ metadata: {
+ name: "metadata",
+ ...column("Map(LowCardinality(String), String)", {
+ description:
+ "Key-value metadata from run tags (key:value format) and AI SDK telemetry metadata. Access keys with dot notation (metadata.userId) or bracket syntax (metadata['userId']).",
+ example: "{'userId':'user_123','org':'acme'}",
+ }),
+ },
+ },
+};
+
+export const querySchemas: TableSchema[] = [runsSchema, metricsSchema, llmUsageSchema];
/**
* Default query for the query editor
diff --git a/apps/webapp/app/v3/utils/enrichCreatableEvents.server.ts b/apps/webapp/app/v3/utils/enrichCreatableEvents.server.ts
index f718c13d2dd..29be7ae6fad 100644
--- a/apps/webapp/app/v3/utils/enrichCreatableEvents.server.ts
+++ b/apps/webapp/app/v3/utils/enrichCreatableEvents.server.ts
@@ -1,4 +1,28 @@
-import type { CreateEventInput } from "../eventRepository/eventRepository.types";
+import type { CreateEventInput, LlmUsageData } from "../eventRepository/eventRepository.types";
+
+// Registry interface — matches ModelPricingRegistry from @internal/llm-pricing
+type CostRegistry = {
+ isLoaded: boolean;
+ calculateCost(
+ responseModel: string,
+ usageDetails: Record
+ ): {
+ matchedModelId: string;
+ matchedModelName: string;
+ pricingTierId: string;
+ pricingTierName: string;
+ inputCost: number;
+ outputCost: number;
+ totalCost: number;
+ costDetails: Record;
+ } | null;
+};
+
+let _registry: CostRegistry | undefined;
+
+export function setLlmPricingRegistry(registry: CostRegistry): void {
+ _registry = registry;
+}
export function enrichCreatableEvents(events: CreateEventInput[]) {
return events.map((event) => {
@@ -12,9 +36,179 @@ function enrichCreatableEvent(event: CreateEventInput): CreateEventInput {
event.message = message;
event.style = enrichStyle(event);
+ enrichLlmCost(event);
+
return event;
}
+function enrichLlmCost(event: CreateEventInput): void {
+ const props = event.properties;
+ if (!props) return;
+
+ // Only enrich span-like events (INTERNAL, SERVER, CLIENT, CONSUMER, PRODUCER — not LOG, UNSPECIFIED)
+ const enrichableKinds = new Set(["INTERNAL", "SERVER", "CLIENT", "CONSUMER", "PRODUCER"]);
+ if (!enrichableKinds.has(event.kind as string)) return;
+
+ // Skip partial spans (they don't have final token counts)
+ if (event.isPartial) return;
+
+ // Only use gen_ai.* attributes for model resolution to avoid double-counting.
+ // The Vercel AI SDK emits both a parent span (ai.streamText with ai.usage.*)
+ // and a child span (ai.streamText.doStream with gen_ai.*). We only enrich the
+ // child span that has the canonical gen_ai.response.model attribute.
+ const responseModel =
+ typeof props["gen_ai.response.model"] === "string"
+ ? props["gen_ai.response.model"]
+ : typeof props["gen_ai.request.model"] === "string"
+ ? props["gen_ai.request.model"]
+ : null;
+
+ if (!responseModel) {
+ return;
+ }
+
+ // Extract usage details, normalizing attribute names
+ const usageDetails = extractUsageDetails(props);
+
+ // Need at least some token usage
+ const hasTokens = Object.values(usageDetails).some((v) => v > 0);
+ if (!hasTokens) {
+ return;
+ }
+
+ // Add style accessories for model and tokens (even without cost data)
+ const inputTokens = usageDetails["input"] ?? 0;
+ const outputTokens = usageDetails["output"] ?? 0;
+ const totalTokens = usageDetails["total"] ?? inputTokens + outputTokens;
+
+ const pillItems: Array<{ text: string; icon: string }> = [
+ { text: responseModel, icon: "tabler-cube" },
+ { text: formatTokenCount(totalTokens), icon: "tabler-hash" },
+ ];
+
+ // Try cost enrichment if the registry is loaded.
+ // The registry handles prefix stripping (e.g. "mistral/mistral-large-3" → "mistral-large-3")
+ // for gateway/openrouter models automatically in its match() method.
+ let cost: ReturnType["calculateCost"]> | null = null;
+ if (_registry?.isLoaded) {
+ cost = _registry.calculateCost(responseModel, usageDetails);
+ }
+
+ // Fallback: extract cost from provider metadata (gateway/openrouter report per-request cost)
+ let providerCost: { totalCost: number; source: string } | null = null;
+ if (!cost) {
+ providerCost = extractProviderCost(props);
+ }
+
+ if (cost) {
+ // Add trigger.llm.* attributes to the span from our pricing registry
+ event.properties = {
+ ...props,
+ "trigger.llm.input_cost": cost.inputCost,
+ "trigger.llm.output_cost": cost.outputCost,
+ "trigger.llm.total_cost": cost.totalCost,
+ "trigger.llm.matched_model": cost.matchedModelName,
+ "trigger.llm.matched_model_id": cost.matchedModelId,
+ "trigger.llm.pricing_tier": cost.pricingTierName,
+ "trigger.llm.pricing_tier_id": cost.pricingTierId,
+ };
+
+ pillItems.push({ text: formatCost(cost.totalCost), icon: "tabler-currency-dollar" });
+ } else if (providerCost) {
+ // Use provider-reported cost as fallback (no input/output breakdown available)
+ event.properties = {
+ ...props,
+ "trigger.llm.total_cost": providerCost.totalCost,
+ "trigger.llm.cost_source": providerCost.source,
+ };
+
+ pillItems.push({ text: formatCost(providerCost.totalCost), icon: "tabler-currency-dollar" });
+ }
+
+ event.style = {
+ ...(event.style as Record | undefined),
+ accessory: {
+ style: "pills",
+ items: pillItems,
+ },
+ } as unknown as typeof event.style;
+
+ // Only write llm_usage when cost data is available
+ if (!cost && !providerCost) return;
+
+ // Build metadata map from run tags and ai.telemetry.metadata.*
+ const metadata: Record = {};
+
+ if (event.runTags) {
+ for (const tag of event.runTags) {
+ const colonIdx = tag.indexOf(":");
+ if (colonIdx > 0) {
+ metadata[tag.substring(0, colonIdx)] = tag.substring(colonIdx + 1);
+ }
+ }
+ }
+
+ for (const [key, value] of Object.entries(props)) {
+ if (key.startsWith("ai.telemetry.metadata.") && typeof value === "string") {
+ metadata[key.slice("ai.telemetry.metadata.".length)] = value;
+ }
+ }
+
+ // Set _llmUsage side-channel for dual-write to llm_usage_v1
+ const llmUsage: LlmUsageData = {
+ genAiSystem: (props["gen_ai.system"] as string) ?? "unknown",
+ requestModel: (props["gen_ai.request.model"] as string) ?? responseModel,
+ responseModel,
+ matchedModelId: cost?.matchedModelId ?? "",
+ operationName: (props["gen_ai.operation.name"] as string) ?? (props["operation.name"] as string) ?? "",
+ pricingTierId: cost?.pricingTierId ?? (providerCost ? `provider:${providerCost.source}` : ""),
+ pricingTierName: cost?.pricingTierName ?? (providerCost ? `${providerCost.source} reported` : ""),
+ inputTokens: usageDetails["input"] ?? 0,
+ outputTokens: usageDetails["output"] ?? 0,
+ totalTokens: usageDetails["total"] ?? (usageDetails["input"] ?? 0) + (usageDetails["output"] ?? 0),
+ usageDetails,
+ inputCost: cost?.inputCost ?? 0,
+ outputCost: cost?.outputCost ?? 0,
+ totalCost: cost?.totalCost ?? providerCost?.totalCost ?? 0,
+ costDetails: cost?.costDetails ?? {},
+ metadata,
+ };
+
+ event._llmUsage = llmUsage;
+}
+
+function extractUsageDetails(props: Record): Record {
+ const details: Record = {};
+
+ // Only map gen_ai.usage.* attributes — NOT ai.usage.* from parent spans.
+ // This prevents double-counting when both parent (ai.streamText) and child
+ // (ai.streamText.doStream) spans carry token counts.
+ const mappings: Record = {
+ "gen_ai.usage.input_tokens": "input",
+ "gen_ai.usage.output_tokens": "output",
+ "gen_ai.usage.prompt_tokens": "input",
+ "gen_ai.usage.completion_tokens": "output",
+ "gen_ai.usage.total_tokens": "total",
+ "gen_ai.usage.cache_read_input_tokens": "input_cached_tokens",
+ "gen_ai.usage.input_tokens_cache_read": "input_cached_tokens",
+ "gen_ai.usage.cache_creation_input_tokens": "cache_creation_input_tokens",
+ "gen_ai.usage.input_tokens_cache_write": "cache_creation_input_tokens",
+ "gen_ai.usage.reasoning_tokens": "reasoning_tokens",
+ };
+
+ for (const [attrKey, usageKey] of Object.entries(mappings)) {
+ const value = props[attrKey];
+ if (typeof value === "number" && value > 0) {
+ // Don't overwrite if already set (first mapping wins)
+ if (details[usageKey] === undefined) {
+ details[usageKey] = value;
+ }
+ }
+ }
+
+ return details;
+}
+
function enrichStyle(event: CreateEventInput) {
const baseStyle = event.style ?? {};
const props = event.properties;
@@ -27,6 +221,15 @@ function enrichStyle(event: CreateEventInput) {
// GenAI System check
const system = props["gen_ai.system"];
if (typeof system === "string") {
+ // For gateway/openrouter, derive the icon from the model's provider prefix
+ // e.g. "mistral/mistral-large-3" → "mistral", "anthropic/claude-..." → "anthropic"
+ if (system === "gateway" || system === "openrouter") {
+ const modelId = props["gen_ai.request.model"] ?? props["ai.model.id"];
+ if (typeof modelId === "string" && modelId.includes("/")) {
+ const provider = modelId.split("/")[0].replace(/-/g, "");
+ return { ...baseStyle, icon: `tabler-brand-${provider}` };
+ }
+ }
return { ...baseStyle, icon: `tabler-brand-${system.split(".")[0]}` };
}
@@ -49,6 +252,59 @@ function enrichStyle(event: CreateEventInput) {
return baseStyle;
}
+function formatTokenCount(tokens: number): string {
+ if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
+ if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`;
+ return tokens.toString();
+}
+
+/**
+ * Extract provider-reported cost from ai.response.providerMetadata.
+ * Gateway and OpenRouter include per-request cost in their metadata.
+ */
+function extractProviderCost(
+ props: Record
+): { totalCost: number; source: string } | null {
+ const rawMeta = props["ai.response.providerMetadata"];
+ if (typeof rawMeta !== "string") return null;
+
+ let meta: Record;
+ try {
+ meta = JSON.parse(rawMeta) as Record;
+ } catch {
+ return null;
+ }
+
+ if (!meta || typeof meta !== "object") return null;
+
+ // Gateway: { gateway: { cost: "0.0006615" } }
+ const gateway = meta.gateway;
+ if (gateway && typeof gateway === "object") {
+ const gw = gateway as Record;
+ const cost = parseFloat(String(gw.cost ?? "0"));
+ if (cost > 0) return { totalCost: cost, source: "gateway" };
+ }
+
+ // OpenRouter: { openrouter: { usage: { cost: 0.000135 } } }
+ const openrouter = meta.openrouter;
+ if (openrouter && typeof openrouter === "object") {
+ const or = openrouter as Record;
+ const usage = or.usage;
+ if (usage && typeof usage === "object") {
+ const cost = Number((usage as Record).cost ?? 0);
+ if (cost > 0) return { totalCost: cost, source: "openrouter" };
+ }
+ }
+
+ return null;
+}
+
+function formatCost(cost: number): string {
+ if (cost >= 1) return `$${cost.toFixed(2)}`;
+ if (cost >= 0.01) return `$${cost.toFixed(4)}`;
+ return `$${cost.toFixed(6)}`;
+}
+
function repr(value: any): string {
if (typeof value === "string") {
return `'${value}'`;
diff --git a/apps/webapp/package.json b/apps/webapp/package.json
index 139a0ce2d0d..48994376066 100644
--- a/apps/webapp/package.json
+++ b/apps/webapp/package.json
@@ -16,6 +16,7 @@
"start:local": "cross-env node --max-old-space-size=8192 ./build/server.js",
"typecheck": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsc --noEmit -p ./tsconfig.check.json",
"db:seed": "tsx seed.mts",
+ "db:seed:ai-spans": "tsx seed-ai-spans.mts",
"upload:sourcemaps": "bash ./upload-sourcemaps.sh",
"test": "vitest --no-file-parallelism",
"eval:dev": "evalite watch"
@@ -56,6 +57,7 @@
"@heroicons/react": "^2.0.12",
"@jsonhero/schema-infer": "^0.1.5",
"@internal/cache": "workspace:*",
+ "@internal/llm-pricing": "workspace:*",
"@internal/redis": "workspace:*",
"@internal/run-engine": "workspace:*",
"@internal/schedule-engine": "workspace:*",
diff --git a/apps/webapp/seed-ai-spans.mts b/apps/webapp/seed-ai-spans.mts
new file mode 100644
index 00000000000..19a72c23f22
--- /dev/null
+++ b/apps/webapp/seed-ai-spans.mts
@@ -0,0 +1,1564 @@
+import { trail } from "agentcrumbs"; // @crumbs
+const crumb = trail("webapp"); // @crumbs
+import { prisma } from "./app/db.server";
+import { createOrganization } from "./app/models/organization.server";
+import { createProject } from "./app/models/project.server";
+import { ClickHouse } from "@internal/clickhouse";
+import type { TaskEventV2Input, LlmUsageV1Input } from "@internal/clickhouse";
+import {
+ generateTraceId,
+ generateSpanId,
+} from "./app/v3/eventRepository/common.server";
+import {
+ enrichCreatableEvents,
+ setLlmPricingRegistry,
+} from "./app/v3/utils/enrichCreatableEvents.server";
+import { ModelPricingRegistry, seedLlmPricing } from "@internal/llm-pricing";
+import { nanoid } from "nanoid";
+import { unflattenAttributes } from "@trigger.dev/core/v3/utils/flattenAttributes";
+import type { Attributes } from "@opentelemetry/api";
+import type { CreateEventInput } from "./app/v3/eventRepository/eventRepository.types";
+
+const ORG_TITLE = "AI Spans Dev";
+const PROJECT_NAME = "ai-chat-demo";
+const TASK_SLUG = "ai-chat";
+const QUEUE_NAME = "task/ai-chat";
+const WORKER_VERSION = "seed-ai-spans-v1";
+
+// ---------------------------------------------------------------------------
+// ClickHouse formatting helpers (replicated from clickhouseEventRepository)
+// ---------------------------------------------------------------------------
+
+function formatStartTime(startTimeNs: bigint): string {
+ const str = startTimeNs.toString();
+ if (str.length !== 19) return str;
+ return str.substring(0, 10) + "." + str.substring(10);
+}
+
+function formatDuration(value: number | bigint): string {
+ if (value < 0) return "0";
+ if (typeof value === "bigint") return value.toString();
+ return Math.floor(value).toString();
+}
+
+function formatClickhouseDateTime(date: Date): string {
+ return date.toISOString().replace("T", " ").replace("Z", "");
+}
+
+function removePrivateProperties(attributes: Attributes): Attributes | undefined {
+ const result: Attributes = {};
+ for (const [key, value] of Object.entries(attributes)) {
+ if (key.startsWith("$") || key.startsWith("ctx.")) continue;
+ result[key] = value;
+ }
+ return Object.keys(result).length === 0 ? undefined : result;
+}
+
+function eventToClickhouseRow(event: CreateEventInput): TaskEventV2Input {
+ // kind
+ let kind: string;
+ if (event.kind === "UNSPECIFIED") kind = "ANCESTOR_OVERRIDE";
+ else if (event.level === "TRACE") kind = "SPAN";
+ else if (event.isDebug) kind = "DEBUG_EVENT";
+ else kind = `LOG_${(event.level ?? "LOG").toString().toUpperCase()}`;
+
+ // status
+ let status: string;
+ if (event.isPartial) status = "PARTIAL";
+ else if (event.isError) status = "ERROR";
+ else if (event.isCancelled) status = "CANCELLED";
+ else status = "OK";
+
+ // attributes
+ const publicAttrs = removePrivateProperties(event.properties as Attributes);
+ const unflattened = publicAttrs ? unflattenAttributes(publicAttrs) : {};
+ const attributes =
+ unflattened && typeof unflattened === "object" ? { ...unflattened } : {};
+
+ // metadata — mirrors createEventToTaskEventV1InputMetadata
+ const metadataObj: Record = {};
+ if (event.style) {
+ metadataObj.style = unflattenAttributes(event.style as Attributes);
+ }
+ if (event.attemptNumber) {
+ metadataObj.attemptNumber = event.attemptNumber;
+ }
+ // Extract entity from properties (SemanticInternalAttributes)
+ const entityType = event.properties?.["$entity.type"];
+ if (typeof entityType === "string") {
+ metadataObj.entity = {
+ entityType,
+ entityId: event.properties?.["$entity.id"] as string | undefined,
+ entityMetadata: event.properties?.["$entity.metadata"] as string | undefined,
+ };
+ }
+ const metadata = JSON.stringify(metadataObj);
+
+ return {
+ environment_id: event.environmentId,
+ organization_id: event.organizationId,
+ project_id: event.projectId,
+ task_identifier: event.taskSlug,
+ run_id: event.runId,
+ start_time: formatStartTime(BigInt(event.startTime)),
+ duration: formatDuration(event.duration ?? 0),
+ trace_id: event.traceId,
+ span_id: event.spanId,
+ parent_span_id: event.parentId ?? "",
+ message: event.message,
+ kind,
+ status,
+ attributes,
+ metadata,
+ expires_at: formatClickhouseDateTime(
+ new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)
+ ),
+ machine_id: "",
+ };
+}
+
+function eventToLlmUsageRow(event: CreateEventInput): LlmUsageV1Input {
+ const llm = event._llmUsage!;
+ return {
+ organization_id: event.organizationId,
+ project_id: event.projectId,
+ environment_id: event.environmentId,
+ run_id: event.runId,
+ task_identifier: event.taskSlug,
+ trace_id: event.traceId,
+ span_id: event.spanId,
+ gen_ai_system: llm.genAiSystem,
+ request_model: llm.requestModel,
+ response_model: llm.responseModel,
+ matched_model_id: llm.matchedModelId,
+ operation_name: llm.operationName,
+ pricing_tier_id: llm.pricingTierId,
+ pricing_tier_name: llm.pricingTierName,
+ input_tokens: llm.inputTokens,
+ output_tokens: llm.outputTokens,
+ total_tokens: llm.totalTokens,
+ usage_details: llm.usageDetails,
+ input_cost: llm.inputCost,
+ output_cost: llm.outputCost,
+ total_cost: llm.totalCost,
+ cost_details: llm.costDetails,
+ metadata: llm.metadata,
+ start_time: formatStartTime(BigInt(event.startTime)),
+ duration: formatDuration(event.duration ?? 0),
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Main
+// ---------------------------------------------------------------------------
+
+async function seedAiSpans() {
+ crumb("seed started"); // @crumbs
+ console.log("Starting AI span seed...\n");
+
+ // 1. Find user
+ crumb("finding user"); // @crumbs
+ const user = await prisma.user.findUnique({
+ where: { email: "local@trigger.dev" },
+ });
+ if (!user) {
+ console.error("User local@trigger.dev not found. Run `pnpm run db:seed` first.");
+ process.exit(1);
+ }
+ crumb("user found", { userId: user.id }); // @crumbs
+
+ // 2. Find or create org
+ crumb("finding/creating org"); // @crumbs
+ let org = await prisma.organization.findFirst({
+ where: { title: ORG_TITLE, members: { some: { userId: user.id } } },
+ });
+ if (!org) {
+ org = await createOrganization({ title: ORG_TITLE, userId: user.id, companySize: "1-10" });
+ console.log(`Created org: ${org.title} (${org.slug})`);
+ } else {
+ console.log(`Org exists: ${org.title} (${org.slug})`);
+ }
+ crumb("org ready", { orgId: org.id, slug: org.slug }); // @crumbs
+
+ // 3. Find or create project
+ crumb("finding/creating project"); // @crumbs
+ let project = await prisma.project.findFirst({
+ where: { name: PROJECT_NAME, organizationId: org.id },
+ });
+ if (!project) {
+ project = await createProject({
+ organizationSlug: org.slug,
+ name: PROJECT_NAME,
+ userId: user.id,
+ version: "v3",
+ });
+ console.log(`Created project: ${project.name} (${project.externalRef})`);
+ } else {
+ console.log(`Project exists: ${project.name} (${project.externalRef})`);
+ }
+ crumb("project ready", { projectId: project.id, ref: project.externalRef }); // @crumbs
+
+ // 4. Get DEVELOPMENT environment
+ crumb("finding dev environment"); // @crumbs
+ const runtimeEnv = await prisma.runtimeEnvironment.findFirst({
+ where: { projectId: project.id, type: "DEVELOPMENT" },
+ });
+ if (!runtimeEnv) {
+ console.error("No DEVELOPMENT environment found for project.");
+ process.exit(1);
+ }
+ crumb("dev env found", { envId: runtimeEnv.id }); // @crumbs
+
+ // 5. Upsert background worker
+ crumb("upserting worker/task/queue"); // @crumbs
+ const worker = await prisma.backgroundWorker.upsert({
+ where: {
+ projectId_runtimeEnvironmentId_version: {
+ projectId: project.id,
+ runtimeEnvironmentId: runtimeEnv.id,
+ version: WORKER_VERSION,
+ },
+ },
+ update: {},
+ create: {
+ friendlyId: `worker_${nanoid()}`,
+ engine: "V2",
+ contentHash: `seed-ai-spans-${Date.now()}`,
+ sdkVersion: "3.0.0",
+ cliVersion: "3.0.0",
+ projectId: project.id,
+ runtimeEnvironmentId: runtimeEnv.id,
+ version: WORKER_VERSION,
+ metadata: {},
+ },
+ });
+
+ // 6. Upsert task
+ await prisma.backgroundWorkerTask.upsert({
+ where: { workerId_slug: { workerId: worker.id, slug: TASK_SLUG } },
+ update: {},
+ create: {
+ friendlyId: `task_${nanoid()}`,
+ slug: TASK_SLUG,
+ filePath: "src/trigger/ai-chat.ts",
+ exportName: "aiChat",
+ workerId: worker.id,
+ projectId: project.id,
+ runtimeEnvironmentId: runtimeEnv.id,
+ },
+ });
+
+ // 7. Upsert queue
+ await prisma.taskQueue.upsert({
+ where: {
+ runtimeEnvironmentId_name: {
+ runtimeEnvironmentId: runtimeEnv.id,
+ name: QUEUE_NAME,
+ },
+ },
+ update: {},
+ create: {
+ friendlyId: `queue_${nanoid()}`,
+ name: QUEUE_NAME,
+ projectId: project.id,
+ runtimeEnvironmentId: runtimeEnv.id,
+ },
+ });
+
+ crumb("infra upserts done"); // @crumbs
+
+ // 8. Create the TaskRun
+ crumb("creating TaskRun"); // @crumbs
+ const traceId = generateTraceId();
+ const rootSpanId = generateSpanId();
+ const now = Date.now();
+ // Spans start at `now` and extend into the future. completedAt must cover
+ // the full span tree so getSpan's start_time <= completedAt filter works.
+ const startedAt = new Date(now);
+ const completedAt = new Date(now + 150_000); // 2.5 min to cover all spans
+
+ const run = await prisma.taskRun.create({
+ data: {
+ friendlyId: `run_${nanoid()}`,
+ engine: "V2",
+ status: "COMPLETED_SUCCESSFULLY",
+ taskIdentifier: TASK_SLUG,
+ payload: JSON.stringify({
+ message: "What is the current Federal Reserve interest rate?",
+ }),
+ payloadType: "application/json",
+ traceId,
+ spanId: rootSpanId,
+ runtimeEnvironmentId: runtimeEnv.id,
+ projectId: project.id,
+ organizationId: org.id,
+ queue: QUEUE_NAME,
+ lockedToVersionId: worker.id,
+ startedAt,
+ completedAt,
+ runTags: ["user:seed_user_42", "chat:seed_session"],
+ taskEventStore: "clickhouse_v2",
+ },
+ });
+
+ crumb("TaskRun created", { runId: run.friendlyId, traceId }); // @crumbs
+ console.log(`Created TaskRun: ${run.friendlyId}`);
+
+ // 9. Build span tree
+ crumb("building span tree"); // @crumbs
+ const events = buildAiSpanTree({
+ traceId,
+ rootSpanId,
+ runId: run.friendlyId,
+ environmentId: runtimeEnv.id,
+ projectId: project.id,
+ organizationId: org.id,
+ taskSlug: TASK_SLUG,
+ baseTimeMs: now,
+ });
+
+ crumb("span tree built", { spanCount: events.length }); // @crumbs
+ console.log(`Built ${events.length} spans`);
+
+ // 10. Seed LLM pricing and enrich
+ crumb("seeding LLM pricing"); // @crumbs
+ const seedResult = await seedLlmPricing(prisma);
+ console.log(
+ `LLM pricing: ${seedResult.modelsCreated} created, ${seedResult.modelsSkipped} skipped`
+ );
+
+ crumb("loading pricing registry"); // @crumbs
+ const registry = new ModelPricingRegistry(prisma);
+ setLlmPricingRegistry(registry);
+ await registry.loadFromDatabase();
+
+ crumb("enriching events"); // @crumbs
+ const enriched = enrichCreatableEvents(events);
+
+ const enrichedCount = enriched.filter((e) => e._llmUsage != null).length;
+ const totalCost = enriched.reduce((sum, e) => sum + (e._llmUsage?.totalCost ?? 0), 0);
+ console.log(
+ `Enriched ${enrichedCount} spans with LLM cost (total: $${totalCost.toFixed(6)})`
+ );
+
+ crumb("enrichment done", { enrichedCount, totalCost }); // @crumbs
+
+ // 11. Insert into ClickHouse
+ crumb("inserting into ClickHouse"); // @crumbs
+ const clickhouseUrl = process.env.CLICKHOUSE_URL ?? process.env.EVENTS_CLICKHOUSE_URL;
+ if (!clickhouseUrl) {
+ console.error("CLICKHOUSE_URL or EVENTS_CLICKHOUSE_URL not set");
+ process.exit(1);
+ }
+
+ const url = new URL(clickhouseUrl);
+ url.searchParams.delete("secure");
+ const clickhouse = new ClickHouse({ url: url.toString() });
+
+ // Convert to ClickHouse rows and insert
+ const chRows = enriched.map(eventToClickhouseRow);
+ await clickhouse.taskEventsV2.insert(chRows);
+
+ crumb("task events inserted", { rowCount: chRows.length }); // @crumbs
+
+ // Insert LLM usage rows
+ const llmRows = enriched.filter((e) => e._llmUsage != null).map(eventToLlmUsageRow);
+ if (llmRows.length > 0) {
+ await clickhouse.llmUsage.insert(llmRows);
+ crumb("llm usage inserted", { rowCount: llmRows.length }); // @crumbs
+ }
+
+ // 12. Output
+ console.log("\nDone!\n");
+ console.log(
+ `Run URL: http://localhost:3030/orgs/${org.slug}/projects/${project.slug}/env/dev/runs/${run.friendlyId}`
+ );
+ console.log(`Spans: ${events.length}`);
+ console.log(`LLM cost enriched: ${enrichedCount}`);
+ console.log(`Total cost: $${totalCost.toFixed(6)}`);
+ crumb("seed complete"); // @crumbs
+ process.exit(0);
+}
+
+// ---------------------------------------------------------------------------
+// Span tree builder
+// ---------------------------------------------------------------------------
+
+type SpanTreeParams = {
+ traceId: string;
+ rootSpanId: string;
+ runId: string;
+ environmentId: string;
+ projectId: string;
+ organizationId: string;
+ taskSlug: string;
+ baseTimeMs: number;
+};
+
+function buildAiSpanTree(params: SpanTreeParams): CreateEventInput[] {
+ const {
+ traceId,
+ rootSpanId,
+ runId,
+ environmentId,
+ projectId,
+ organizationId,
+ taskSlug,
+ baseTimeMs,
+ } = params;
+
+ const events: CreateEventInput[] = [];
+ const runTags = ["user:seed_user_42", "chat:seed_session"];
+
+ // Timing cursor — each span advances this
+ let cursor = baseTimeMs;
+ function next(durationMs: number) {
+ const start = cursor;
+ cursor += durationMs + 50; // 50ms gap between spans
+ return { startMs: start, durationMs };
+ }
+
+ function makeEvent(opts: {
+ message: string;
+ spanId: string;
+ parentId: string | undefined;
+ startMs: number;
+ durationMs: number;
+ properties: Record;
+ style?: Record;
+ attemptNumber?: number;
+ }): CreateEventInput {
+ const startNs = BigInt(opts.startMs) * BigInt(1_000_000);
+ const durationNs = opts.durationMs * 1_000_000;
+ return {
+ traceId,
+ spanId: opts.spanId,
+ parentId: opts.parentId,
+ message: opts.message,
+ kind: "INTERNAL" as any,
+ status: "OK" as any,
+ level: "TRACE" as any,
+ startTime: startNs,
+ duration: durationNs,
+ isError: false,
+ isPartial: false,
+ isCancelled: false,
+ isDebug: false,
+ runId,
+ environmentId,
+ projectId,
+ organizationId,
+ taskSlug,
+ properties: opts.properties,
+ metadata: undefined,
+ style: opts.style as any,
+ events: undefined,
+ runTags,
+ attemptNumber: opts.attemptNumber,
+ };
+ }
+
+ // --- Shared prompt content ---
+ const userMessage = "What is the current Federal Reserve interest rate?";
+ const systemPrompt = "You are a helpful financial assistant with access to web search tools.";
+ const assistantResponse =
+ "The current Federal Reserve interest rate target range is 4.25% to 4.50%. This was set by the FOMC at their most recent meeting.";
+ const toolCallResult = JSON.stringify({
+ status: 200,
+ contentType: "text/html",
+ body: "...Federal Reserve maintains the target range for the federal funds rate at 4-1/4 to 4-1/2 percent...",
+ truncated: true,
+ });
+ const promptMessages = JSON.stringify([
+ { role: "system", content: systemPrompt },
+ { role: "user", content: userMessage },
+ ]);
+ const toolDefs = JSON.stringify([
+ JSON.stringify({
+ type: "function",
+ name: "webSearch",
+ description: "Search the web for information",
+ inputSchema: {
+ type: "object",
+ properties: {
+ query: { type: "string" },
+ num: { type: "number" },
+ },
+ required: ["query"],
+ },
+ }),
+ ]);
+ const toolCallsJson = JSON.stringify([
+ {
+ id: "call_seed_001",
+ type: "function",
+ function: {
+ name: "webSearch",
+ arguments: '{"query":"federal reserve interest rate 2024","num":5}',
+ },
+ },
+ ]);
+
+ // --- Span IDs ---
+ const attemptId = generateSpanId();
+ const runFnId = generateSpanId();
+
+ // streamText sub-tree IDs
+ const streamWrapId = generateSpanId();
+ const stream1Id = generateSpanId();
+ const toolCall1Id = generateSpanId();
+ const stream2Id = generateSpanId();
+
+ // generateText sub-tree IDs (Anthropic with cache)
+ const genTextWrapId = generateSpanId();
+ const genTextDoId = generateSpanId();
+ const toolCall2Id = generateSpanId();
+
+ // generateObject sub-tree IDs (gateway → xAI)
+ const genObjWrapId = generateSpanId();
+ const genObjDoId = generateSpanId();
+
+ // generateObject sub-tree IDs (Google Gemini)
+ const genObjGeminiWrapId = generateSpanId();
+ const genObjGeminiDoId = generateSpanId();
+
+ // =====================================================================
+ // Structural spans: root → attempt → run()
+ // =====================================================================
+ const rootStart = baseTimeMs;
+ const totalDuration = 120_000; // 2 minutes to cover all ~18 scenarios
+
+ events.push(
+ makeEvent({
+ message: taskSlug,
+ spanId: rootSpanId,
+ parentId: undefined,
+ startMs: rootStart,
+ durationMs: totalDuration,
+ properties: {},
+ })
+ );
+
+ events.push(
+ makeEvent({
+ message: "Attempt 1",
+ spanId: attemptId,
+ parentId: rootSpanId,
+ startMs: rootStart + 30,
+ durationMs: totalDuration - 60,
+ properties: { "$entity.type": "attempt" },
+ style: { icon: "attempt", variant: "cold" },
+ attemptNumber: 1,
+ })
+ );
+
+ events.push(
+ makeEvent({
+ message: "run()",
+ spanId: runFnId,
+ parentId: attemptId,
+ startMs: rootStart + 60,
+ durationMs: totalDuration - 120,
+ properties: {},
+ style: { icon: "task-fn-run" },
+ attemptNumber: 1,
+ })
+ );
+
+ // =====================================================================
+ // 1) ai.streamText — OpenAI gpt-4o-mini with tool use (2 LLM calls)
+ // =====================================================================
+ cursor = rootStart + 100;
+ const stWrap = next(9_500);
+
+ events.push(
+ makeEvent({
+ message: "ai.streamText",
+ spanId: streamWrapId,
+ parentId: runFnId,
+ ...stWrap,
+ properties: {
+ "ai.operationId": "ai.streamText",
+ "ai.model.id": "gpt-4o-mini",
+ "ai.model.provider": "openai.responses",
+ "ai.response.finishReason": "stop",
+ "ai.response.text": assistantResponse,
+ "ai.usage.inputTokens": 807,
+ "ai.usage.outputTokens": 242,
+ "ai.usage.totalTokens": 1049,
+ "ai.telemetry.metadata.userId": "seed_user_42",
+ "ai.telemetry.functionId": "ai-chat",
+ "operation.name": "ai.streamText",
+ },
+ })
+ );
+
+ cursor = stWrap.startMs + 50;
+ const st1 = next(2_500);
+ events.push(
+ makeEvent({
+ message: "ai.streamText.doStream",
+ spanId: stream1Id,
+ parentId: streamWrapId,
+ ...st1,
+ properties: {
+ "gen_ai.system": "openai.responses",
+ "gen_ai.request.model": "gpt-4o-mini",
+ "gen_ai.response.model": "gpt-4o-mini-2024-07-18",
+ "gen_ai.usage.input_tokens": 284,
+ "gen_ai.usage.output_tokens": 55,
+ "ai.model.id": "gpt-4o-mini",
+ "ai.model.provider": "openai.responses",
+ "ai.operationId": "ai.streamText.doStream",
+ "ai.prompt.messages": promptMessages,
+ "ai.prompt.tools": toolDefs,
+ "ai.prompt.toolChoice": '{"type":"auto"}',
+ "ai.settings.maxRetries": 2,
+ "ai.response.finishReason": "tool-calls",
+ "ai.response.toolCalls": toolCallsJson,
+ "ai.response.text": "",
+ "ai.response.id": "resp_seed_001",
+ "ai.response.model": "gpt-4o-mini-2024-07-18",
+ "ai.response.msToFirstChunk": 891.37,
+ "ai.response.msToFinish": 2321.12,
+ "ai.response.timestamp": new Date(st1.startMs + st1.durationMs).toISOString(),
+ "ai.usage.inputTokens": 284,
+ "ai.usage.outputTokens": 55,
+ "ai.usage.totalTokens": 339,
+ "ai.telemetry.metadata.userId": "seed_user_42",
+ "ai.telemetry.functionId": "ai-chat",
+ "operation.name": "ai.streamText.doStream",
+ },
+ })
+ );
+
+ const tc1 = next(3_000);
+ events.push(
+ makeEvent({
+ message: "ai.toolCall",
+ spanId: toolCall1Id,
+ parentId: streamWrapId,
+ ...tc1,
+ properties: {
+ "ai.operationId": "ai.toolCall",
+ "ai.toolCall.name": "webSearch",
+ "ai.toolCall.id": "call_seed_001",
+ "ai.toolCall.args": '{"query":"federal reserve interest rate 2024","num":5}',
+ "ai.toolCall.result": toolCallResult,
+ "operation.name": "ai.toolCall",
+ },
+ })
+ );
+
+ const st2 = next(3_500);
+ events.push(
+ makeEvent({
+ message: "ai.streamText.doStream",
+ spanId: stream2Id,
+ parentId: streamWrapId,
+ ...st2,
+ properties: {
+ "gen_ai.system": "openai.responses",
+ "gen_ai.request.model": "gpt-4o-mini",
+ "gen_ai.response.model": "gpt-4o-mini-2024-07-18",
+ "gen_ai.usage.input_tokens": 523,
+ "gen_ai.usage.output_tokens": 187,
+ "ai.model.id": "gpt-4o-mini",
+ "ai.model.provider": "openai.responses",
+ "ai.operationId": "ai.streamText.doStream",
+ "ai.prompt.messages": promptMessages,
+ "ai.settings.maxRetries": 2,
+ "ai.response.finishReason": "stop",
+ "ai.response.text": assistantResponse,
+ "ai.response.reasoning":
+ "Let me analyze the Federal Reserve data to provide the current rate.",
+ "ai.response.id": "resp_seed_002",
+ "ai.response.model": "gpt-4o-mini-2024-07-18",
+ "ai.response.msToFirstChunk": 672.45,
+ "ai.response.msToFinish": 3412.89,
+ "ai.response.timestamp": new Date(st2.startMs + st2.durationMs).toISOString(),
+ "ai.usage.inputTokens": 523,
+ "ai.usage.outputTokens": 187,
+ "ai.usage.totalTokens": 710,
+ "ai.usage.reasoningTokens": 42,
+ "ai.telemetry.metadata.userId": "seed_user_42",
+ "ai.telemetry.functionId": "ai-chat",
+ "operation.name": "ai.streamText.doStream",
+ },
+ })
+ );
+
+ // =====================================================================
+ // 2) ai.generateText — Anthropic claude-haiku-4-5 with tool call + cache
+ // =====================================================================
+ const gtWrap = next(4_200);
+
+ events.push(
+ makeEvent({
+ message: "ai.generateText",
+ spanId: genTextWrapId,
+ parentId: runFnId,
+ ...gtWrap,
+ properties: {
+ "ai.operationId": "ai.generateText",
+ "ai.model.id": "claude-haiku-4-5",
+ "ai.model.provider": "anthropic.messages",
+ "ai.response.finishReason": "stop",
+ "ai.response.text": "Based on the search results, the current rate is 4.25%-4.50%.",
+ "ai.usage.promptTokens": 9951,
+ "ai.usage.completionTokens": 803,
+ "ai.telemetry.metadata.agentName": "research-agent",
+ "ai.telemetry.functionId": "ai-chat",
+ "operation.name": "ai.generateText",
+ },
+ })
+ );
+
+ cursor = gtWrap.startMs + 50;
+ const gtDo = next(3_200);
+ events.push(
+ makeEvent({
+ message: "ai.generateText.doGenerate",
+ spanId: genTextDoId,
+ parentId: genTextWrapId,
+ ...gtDo,
+ properties: {
+ "gen_ai.system": "anthropic.messages",
+ "gen_ai.request.model": "claude-haiku-4-5",
+ "gen_ai.response.model": "claude-haiku-4-5-20251001",
+ "gen_ai.usage.input_tokens": 9951,
+ "gen_ai.usage.output_tokens": 803,
+ "gen_ai.usage.cache_read_input_tokens": 8200,
+ "gen_ai.usage.cache_creation_input_tokens": 1751,
+ "ai.model.id": "claude-haiku-4-5",
+ "ai.model.provider": "anthropic.messages",
+ "ai.operationId": "ai.generateText.doGenerate",
+ "ai.prompt.messages": promptMessages,
+ "ai.prompt.toolChoice": '{"type":"auto"}',
+ "ai.settings.maxRetries": 2,
+ "ai.response.finishReason": "tool-calls",
+ "ai.response.id": "msg_seed_003",
+ "ai.response.model": "claude-haiku-4-5-20251001",
+ "ai.response.text":
+ "I'll search for the latest Federal Reserve interest rate information.",
+ "ai.response.toolCalls": JSON.stringify([
+ {
+ toolCallId: "toolu_seed_001",
+ toolName: "webSearch",
+ input: '{"query":"federal reserve interest rate current"}',
+ },
+ ]),
+ "ai.response.providerMetadata": JSON.stringify({
+ anthropic: {
+ usage: {
+ input_tokens: 9951,
+ output_tokens: 803,
+ cache_creation_input_tokens: 1751,
+ cache_read_input_tokens: 8200,
+ service_tier: "standard",
+ },
+ },
+ }),
+ "ai.response.timestamp": new Date(gtDo.startMs + gtDo.durationMs).toISOString(),
+ "ai.usage.promptTokens": 9951,
+ "ai.usage.completionTokens": 803,
+ "ai.telemetry.metadata.agentName": "research-agent",
+ "ai.telemetry.functionId": "ai-chat",
+ "operation.name": "ai.generateText.doGenerate",
+ },
+ })
+ );
+
+ const tc2 = next(500);
+ events.push(
+ makeEvent({
+ message: "ai.toolCall",
+ spanId: toolCall2Id,
+ parentId: genTextWrapId,
+ ...tc2,
+ properties: {
+ "ai.operationId": "ai.toolCall",
+ "ai.toolCall.name": "webSearch",
+ "ai.toolCall.id": "toolu_seed_001",
+ "ai.toolCall.args": '{"query":"federal reserve interest rate current"}',
+ "ai.toolCall.result":
+ '[{"title":"Federal Reserve Board - Policy Rate","link":"https://federalreserve.gov/rates","snippet":"The target range is 4.25% to 4.50%"}]',
+ "operation.name": "ai.toolCall",
+ "resource.name": "ai-chat",
+ },
+ })
+ );
+
+ // =====================================================================
+ // 3) ai.generateObject — Gateway → xAI/grok with structured output
+ // =====================================================================
+ const goWrap = next(1_800);
+
+ events.push(
+ makeEvent({
+ message: "ai.generateObject",
+ spanId: genObjWrapId,
+ parentId: runFnId,
+ ...goWrap,
+ properties: {
+ "ai.operationId": "ai.generateObject",
+ "ai.model.id": "xai/grok-4.1-fast-non-reasoning",
+ "ai.model.provider": "gateway",
+ "ai.response.finishReason": "stop",
+ "ai.response.object": JSON.stringify({
+ summary: "Fed rate at 4.25%-4.50%",
+ confidence: 0.95,
+ sources: ["federalreserve.gov"],
+ }),
+ "ai.telemetry.metadata.model": "xai/grok-4.1-fast-non-reasoning",
+ "ai.telemetry.metadata.schemaType": "schema",
+ "ai.telemetry.functionId": "generateObject",
+ "operation.name": "ai.generateObject",
+ },
+ })
+ );
+
+ cursor = goWrap.startMs + 50;
+ const goDo = next(1_600);
+ events.push(
+ makeEvent({
+ message: "ai.generateObject.doGenerate",
+ spanId: genObjDoId,
+ parentId: genObjWrapId,
+ ...goDo,
+ properties: {
+ "gen_ai.system": "gateway",
+ "gen_ai.request.model": "xai/grok-4.1-fast-non-reasoning",
+ "gen_ai.response.model": "xai/grok-4.1-fast-non-reasoning",
+ "gen_ai.usage.input_tokens": 1629,
+ "gen_ai.usage.output_tokens": 158,
+ "ai.model.id": "xai/grok-4.1-fast-non-reasoning",
+ "ai.model.provider": "gateway",
+ "ai.operationId": "ai.generateObject.doGenerate",
+ "ai.prompt.messages": promptMessages,
+ "ai.settings.maxRetries": 3,
+ "ai.response.finishReason": "stop",
+ "ai.response.id": "aiobj_seed_001",
+ "ai.response.model": "xai/grok-4.1-fast-non-reasoning",
+ "ai.response.object": JSON.stringify({
+ summary: "Fed rate at 4.25%-4.50%",
+ confidence: 0.95,
+ sources: ["federalreserve.gov"],
+ }),
+ "ai.response.providerMetadata": JSON.stringify({
+ gateway: {
+ routing: {
+ originalModelId: "xai/grok-4.1-fast-non-reasoning",
+ resolvedProvider: "xai",
+ canonicalSlug: "xai/grok-4.1-fast-non-reasoning",
+ finalProvider: "xai",
+ modelAttemptCount: 1,
+ },
+ cost: "0.0002905",
+ generationId: "gen_seed_001",
+ },
+ }),
+ "ai.response.timestamp": new Date(goDo.startMs + goDo.durationMs).toISOString(),
+ "ai.usage.completionTokens": 158,
+ "ai.usage.promptTokens": 1629,
+ "ai.request.headers.user-agent": "ai/5.0.60",
+ "operation.name": "ai.generateObject.doGenerate",
+ },
+ })
+ );
+
+ // =====================================================================
+ // 4) ai.generateObject — Google Gemini (generative-ai) with thinking tokens
+ // =====================================================================
+ const goGemWrap = next(2_200);
+
+ events.push(
+ makeEvent({
+ message: "ai.generateObject",
+ spanId: genObjGeminiWrapId,
+ parentId: runFnId,
+ ...goGemWrap,
+ properties: {
+ "ai.operationId": "ai.generateObject",
+ "ai.model.id": "gemini-2.5-flash",
+ "ai.model.provider": "google.generative-ai",
+ "ai.response.finishReason": "stop",
+ "ai.response.object": JSON.stringify({
+ category: "financial_data",
+ label: "interest_rate",
+ }),
+ "ai.telemetry.functionId": "classify-content",
+ "operation.name": "ai.generateObject",
+ },
+ })
+ );
+
+ cursor = goGemWrap.startMs + 50;
+ const goGemDo = next(2_000);
+ events.push(
+ makeEvent({
+ message: "ai.generateObject.doGenerate",
+ spanId: genObjGeminiDoId,
+ parentId: genObjGeminiWrapId,
+ ...goGemDo,
+ properties: {
+ "gen_ai.system": "google.generative-ai",
+ "gen_ai.request.model": "gemini-2.5-flash",
+ "gen_ai.response.model": "gemini-2.5-flash",
+ "gen_ai.usage.input_tokens": 898,
+ "gen_ai.usage.output_tokens": 521,
+ "ai.model.id": "gemini-2.5-flash",
+ "ai.model.provider": "google.generative-ai",
+ "ai.operationId": "ai.generateObject.doGenerate",
+ "ai.prompt.messages": JSON.stringify([
+ {
+ role: "user",
+ content: [
+ {
+ type: "text",
+ text: "Classify this content: Federal Reserve interest rate analysis",
+ },
+ ],
+ },
+ ]),
+ "ai.settings.maxRetries": 3,
+ "ai.response.finishReason": "stop",
+ "ai.response.id": "aiobj_seed_gemini",
+ "ai.response.model": "gemini-2.5-flash",
+ "ai.response.object": JSON.stringify({
+ category: "financial_data",
+ label: "interest_rate",
+ }),
+ "ai.response.providerMetadata": JSON.stringify({
+ google: {
+ usageMetadata: {
+ thoughtsTokenCount: 510,
+ promptTokenCount: 898,
+ candidatesTokenCount: 11,
+ totalTokenCount: 1419,
+ },
+ },
+ }),
+ "ai.response.timestamp": new Date(goGemDo.startMs + goGemDo.durationMs).toISOString(),
+ "ai.usage.completionTokens": 521,
+ "ai.usage.promptTokens": 898,
+ "operation.name": "ai.generateObject.doGenerate",
+ },
+ })
+ );
+
+ // =====================================================================
+ // Helper: add a wrapper + doGenerate/doStream pair
+ // =====================================================================
+ function addLlmPair(opts: {
+ wrapperMsg: string; // e.g. "ai.generateText"
+ doMsg: string; // e.g. "ai.generateText.doGenerate"
+ system: string;
+ reqModel: string;
+ respModel: string;
+ inputTokens: number;
+ outputTokens: number;
+ finishReason: string;
+ wrapperDurationMs: number;
+ doDurationMs: number;
+ responseText?: string;
+ responseObject?: string;
+ responseReasoning?: string;
+ toolCallsJson?: string;
+ providerMetadata?: Record;
+ telemetryMetadata?: Record;
+ settings?: Record;
+ /** Use completionTokens/promptTokens instead of inputTokens/outputTokens */
+ useCompletionStyle?: boolean;
+ cacheReadTokens?: number;
+ cacheCreationTokens?: number;
+ reasoningTokens?: number;
+ extraDoProps?: Record;
+ }) {
+ const wId = generateSpanId();
+ const dId = generateSpanId();
+
+ const wrap = next(opts.wrapperDurationMs);
+ const wrapperProps: Record = {
+ "ai.operationId": opts.wrapperMsg,
+ "ai.model.id": opts.reqModel,
+ "ai.model.provider": opts.system,
+ "ai.response.finishReason": opts.finishReason,
+ "operation.name": opts.wrapperMsg,
+ };
+ if (opts.responseText) wrapperProps["ai.response.text"] = opts.responseText;
+ if (opts.responseObject) wrapperProps["ai.response.object"] = opts.responseObject;
+ if (opts.telemetryMetadata) {
+ for (const [k, v] of Object.entries(opts.telemetryMetadata)) {
+ wrapperProps[`ai.telemetry.metadata.${k}`] = v;
+ }
+ }
+
+ events.push(makeEvent({ message: opts.wrapperMsg, spanId: wId, parentId: runFnId, ...wrap, properties: wrapperProps }));
+
+ cursor = wrap.startMs + 50;
+ const doTiming = next(opts.doDurationMs);
+
+ const doProps: Record = {
+ "gen_ai.system": opts.system,
+ "gen_ai.request.model": opts.reqModel,
+ "gen_ai.response.model": opts.respModel,
+ "gen_ai.usage.input_tokens": opts.inputTokens,
+ "gen_ai.usage.output_tokens": opts.outputTokens,
+ "ai.model.id": opts.reqModel,
+ "ai.model.provider": opts.system,
+ "ai.operationId": opts.doMsg,
+ "ai.prompt.messages": promptMessages,
+ "ai.response.finishReason": opts.finishReason,
+ "ai.response.id": `resp_seed_${generateSpanId().slice(0, 8)}`,
+ "ai.response.model": opts.respModel,
+ "ai.response.timestamp": new Date(doTiming.startMs + doTiming.durationMs).toISOString(),
+ "operation.name": opts.doMsg,
+ };
+
+ // Token style
+ if (opts.useCompletionStyle) {
+ doProps["ai.usage.completionTokens"] = opts.outputTokens;
+ doProps["ai.usage.promptTokens"] = opts.inputTokens;
+ } else {
+ doProps["ai.usage.inputTokens"] = opts.inputTokens;
+ doProps["ai.usage.outputTokens"] = opts.outputTokens;
+ doProps["ai.usage.totalTokens"] = opts.inputTokens + opts.outputTokens;
+ }
+
+ if (opts.responseText) doProps["ai.response.text"] = opts.responseText;
+ if (opts.responseObject) doProps["ai.response.object"] = opts.responseObject;
+ if (opts.responseReasoning) doProps["ai.response.reasoning"] = opts.responseReasoning;
+ if (opts.toolCallsJson) doProps["ai.response.toolCalls"] = opts.toolCallsJson;
+ if (opts.cacheReadTokens) {
+ doProps["gen_ai.usage.cache_read_input_tokens"] = opts.cacheReadTokens;
+ }
+ if (opts.cacheCreationTokens) {
+ doProps["gen_ai.usage.cache_creation_input_tokens"] = opts.cacheCreationTokens;
+ }
+ if (opts.reasoningTokens) {
+ doProps["ai.usage.reasoningTokens"] = opts.reasoningTokens;
+ }
+ if (opts.providerMetadata) {
+ doProps["ai.response.providerMetadata"] = JSON.stringify(opts.providerMetadata);
+ }
+ if (opts.settings) {
+ for (const [k, v] of Object.entries(opts.settings)) {
+ doProps[`ai.settings.${k}`] = v;
+ }
+ }
+ if (opts.telemetryMetadata) {
+ for (const [k, v] of Object.entries(opts.telemetryMetadata)) {
+ doProps[`ai.telemetry.metadata.${k}`] = v;
+ }
+ }
+ if (opts.extraDoProps) Object.assign(doProps, opts.extraDoProps);
+
+ events.push(makeEvent({ message: opts.doMsg, spanId: dId, parentId: wId, ...doTiming, properties: doProps }));
+
+ return { wrapperId: wId, doId: dId };
+ }
+
+ // Helper: add a tool call span
+ function addToolCall(parentId: string, name: string, args: string, result: string, durationMs = 500) {
+ const id = generateSpanId();
+ const timing = next(durationMs);
+ events.push(makeEvent({
+ message: "ai.toolCall",
+ spanId: id,
+ parentId,
+ ...timing,
+ properties: {
+ "ai.operationId": "ai.toolCall",
+ "ai.toolCall.name": name,
+ "ai.toolCall.id": `call_${generateSpanId().slice(0, 8)}`,
+ "ai.toolCall.args": args,
+ "ai.toolCall.result": result,
+ "operation.name": "ai.toolCall",
+ },
+ }));
+ return id;
+ }
+
+ // =====================================================================
+ // 5) Gateway → Mistral mistral-large-3
+ // =====================================================================
+ addLlmPair({
+ wrapperMsg: "ai.generateText",
+ doMsg: "ai.generateText.doGenerate",
+ system: "gateway",
+ reqModel: "mistral/mistral-large-3",
+ respModel: "mistral/mistral-large-3",
+ inputTokens: 1179,
+ outputTokens: 48,
+ finishReason: "stop",
+ wrapperDurationMs: 1_400,
+ doDurationMs: 1_200,
+ responseText: "The document discusses quarterly earnings guidance for tech sector.",
+ useCompletionStyle: true,
+ providerMetadata: {
+ gateway: {
+ routing: {
+ originalModelId: "mistral/mistral-large-3",
+ resolvedProvider: "mistral",
+ resolvedProviderApiModelId: "mistral-large-latest",
+ canonicalSlug: "mistral/mistral-large-3",
+ finalProvider: "mistral",
+ modelAttemptCount: 1,
+ },
+ cost: "0.0006615",
+ marketCost: "0.0006615",
+ generationId: "gen_seed_mistral_001",
+ },
+ },
+ extraDoProps: { "ai.request.headers.user-agent": "ai/5.0.60" },
+ });
+
+ // =====================================================================
+ // 6) Gateway → OpenAI gpt-5-mini (with fallback metadata)
+ // =====================================================================
+ addLlmPair({
+ wrapperMsg: "ai.generateText",
+ doMsg: "ai.generateText.doGenerate",
+ system: "gateway",
+ reqModel: "openai/gpt-5-mini",
+ respModel: "openai/gpt-5-mini",
+ inputTokens: 2450,
+ outputTokens: 312,
+ finishReason: "stop",
+ wrapperDurationMs: 5_000,
+ doDurationMs: 4_800,
+ responseText: "NO",
+ useCompletionStyle: true,
+ providerMetadata: {
+ openai: { responseId: "resp_seed_gw_openai", serviceTier: "default" },
+ gateway: {
+ routing: {
+ originalModelId: "openai/gpt-5-mini",
+ resolvedProvider: "openai",
+ resolvedProviderApiModelId: "gpt-5-mini-2025-08-07",
+ canonicalSlug: "openai/gpt-5-mini",
+ finalProvider: "openai",
+ fallbacksAvailable: ["azure"],
+ planningReasoning: "System credentials planned for: openai, azure. Total execution order: openai(system) → azure(system)",
+ modelAttemptCount: 1,
+ },
+ cost: "0.000482",
+ generationId: "gen_seed_gpt5mini_001",
+ },
+ },
+ extraDoProps: { "ai.request.headers.user-agent": "ai/6.0.49" },
+ });
+
+ // =====================================================================
+ // 7) Gateway → DeepSeek deepseek-v3.2 (tool-calls)
+ // =====================================================================
+ const ds = addLlmPair({
+ wrapperMsg: "ai.generateObject",
+ doMsg: "ai.generateObject.doGenerate",
+ system: "gateway",
+ reqModel: "deepseek/deepseek-v3.2",
+ respModel: "deepseek/deepseek-v3.2",
+ inputTokens: 3200,
+ outputTokens: 420,
+ finishReason: "tool-calls",
+ wrapperDurationMs: 2_800,
+ doDurationMs: 2_500,
+ responseObject: JSON.stringify({ action: "search", query: "fed rate history" }),
+ useCompletionStyle: true,
+ providerMetadata: {
+ gateway: {
+ routing: {
+ originalModelId: "deepseek/deepseek-v3.2",
+ resolvedProvider: "deepseek",
+ canonicalSlug: "deepseek/deepseek-v3.2",
+ finalProvider: "deepseek",
+ modelAttemptCount: 1,
+ },
+ cost: "0.000156",
+ generationId: "gen_seed_deepseek_001",
+ },
+ },
+ });
+ addToolCall(ds.wrapperId, "classifyContent", '{"text":"Federal Reserve rate analysis"}', '{"category":"finance","confidence":0.98}');
+
+ // =====================================================================
+ // 8) Gateway → Anthropic claude-haiku via gateway prefix
+ // =====================================================================
+ addLlmPair({
+ wrapperMsg: "ai.generateText",
+ doMsg: "ai.generateText.doGenerate",
+ system: "gateway",
+ reqModel: "anthropic/claude-haiku-4-5-20251001",
+ respModel: "anthropic/claude-haiku-4-5-20251001",
+ inputTokens: 5400,
+ outputTokens: 220,
+ finishReason: "stop",
+ wrapperDurationMs: 1_800,
+ doDurationMs: 1_500,
+ responseText: "The content appears to be a standard financial news article. Classification: SAFE.",
+ useCompletionStyle: true,
+ providerMetadata: {
+ gateway: {
+ routing: {
+ originalModelId: "anthropic/claude-haiku-4-5-20251001",
+ resolvedProvider: "anthropic",
+ canonicalSlug: "anthropic/claude-haiku-4-5-20251001",
+ finalProvider: "anthropic",
+ modelAttemptCount: 1,
+ },
+ cost: "0.00312",
+ generationId: "gen_seed_gw_anthropic_001",
+ },
+ },
+ });
+
+ // =====================================================================
+ // 9) Gateway → Google gemini-3-flash-preview (structured output)
+ // =====================================================================
+ addLlmPair({
+ wrapperMsg: "ai.generateObject",
+ doMsg: "ai.generateObject.doGenerate",
+ system: "gateway",
+ reqModel: "google/gemini-3-flash-preview",
+ respModel: "google/gemini-3-flash-preview",
+ inputTokens: 720,
+ outputTokens: 85,
+ finishReason: "stop",
+ wrapperDurationMs: 1_200,
+ doDurationMs: 1_000,
+ responseObject: JSON.stringify({ sentiment: "neutral", topics: ["monetary_policy", "interest_rates"] }),
+ useCompletionStyle: true,
+ providerMetadata: {
+ gateway: {
+ routing: {
+ originalModelId: "google/gemini-3-flash-preview",
+ resolvedProvider: "google",
+ canonicalSlug: "google/gemini-3-flash-preview",
+ finalProvider: "google",
+ modelAttemptCount: 1,
+ },
+ cost: "0.0000803",
+ generationId: "gen_seed_gw_gemini_001",
+ },
+ },
+ });
+
+ // =====================================================================
+ // 10) OpenRouter → x-ai/grok-4-fast (with reasoning_details)
+ // =====================================================================
+ addLlmPair({
+ wrapperMsg: "ai.generateObject",
+ doMsg: "ai.generateObject.doGenerate",
+ system: "openrouter",
+ reqModel: "x-ai/grok-4-fast",
+ respModel: "x-ai/grok-4-fast",
+ inputTokens: 375,
+ outputTokens: 226,
+ finishReason: "stop",
+ wrapperDurationMs: 1_600,
+ doDurationMs: 1_400,
+ responseObject: JSON.stringify({ hook: "Breaking: Fed holds rates steady", isValidHook: true }),
+ useCompletionStyle: true,
+ telemetryMetadata: { model: "x-ai/grok-4-fast", schemaType: "schema", temperature: "1" },
+ settings: { maxRetries: 2, temperature: 1 },
+ providerMetadata: {
+ openrouter: {
+ provider: "xAI",
+ reasoning_details: [{ type: "reasoning.encrypted", data: "encrypted_seed_data..." }],
+ usage: {
+ promptTokens: 375,
+ promptTokensDetails: { cachedTokens: 343 },
+ completionTokens: 226,
+ completionTokensDetails: { reasoningTokens: 210 },
+ totalTokens: 601,
+ cost: 0.0001351845,
+ costDetails: { upstreamInferenceCost: 0.00013655 },
+ },
+ },
+ },
+ });
+
+ // =====================================================================
+ // 11) OpenRouter → google/gemini-2.5-flash
+ // =====================================================================
+ addLlmPair({
+ wrapperMsg: "ai.generateText",
+ doMsg: "ai.generateText.doGenerate",
+ system: "openrouter",
+ reqModel: "google/gemini-2.5-flash",
+ respModel: "google/gemini-2.5-flash",
+ inputTokens: 1840,
+ outputTokens: 320,
+ finishReason: "stop",
+ wrapperDurationMs: 2_000,
+ doDurationMs: 1_800,
+ responseText: "Based on the latest FOMC minutes, the committee voted unanimously to maintain rates.",
+ useCompletionStyle: true,
+ providerMetadata: {
+ openrouter: {
+ provider: "Google AI Studio",
+ usage: {
+ promptTokens: 1840,
+ completionTokens: 320,
+ totalTokens: 2160,
+ cost: 0.000264,
+ costDetails: { upstreamInferenceCost: 0.000232 },
+ },
+ },
+ },
+ });
+
+ // =====================================================================
+ // 12) OpenRouter → openai/gpt-4.1-mini (req ≠ resp model name)
+ // =====================================================================
+ addLlmPair({
+ wrapperMsg: "ai.generateObject",
+ doMsg: "ai.generateObject.doGenerate",
+ system: "openrouter",
+ reqModel: "openai/gpt-4.1-mini",
+ respModel: "openai/gpt-4.1-mini-2025-04-14",
+ inputTokens: 890,
+ outputTokens: 145,
+ finishReason: "stop",
+ wrapperDurationMs: 1_400,
+ doDurationMs: 1_200,
+ responseObject: JSON.stringify({ summary: "Rate unchanged at 4.25-4.50%", date: "2024-12-18" }),
+ useCompletionStyle: true,
+ providerMetadata: {
+ openrouter: {
+ provider: "OpenAI",
+ usage: {
+ promptTokens: 890,
+ completionTokens: 145,
+ totalTokens: 1035,
+ cost: 0.0000518,
+ },
+ },
+ },
+ });
+
+ // =====================================================================
+ // 13) Azure → gpt-5 with tool-calls
+ // =====================================================================
+ const az = addLlmPair({
+ wrapperMsg: "ai.generateText",
+ doMsg: "ai.generateText.doGenerate",
+ system: "azure.responses",
+ reqModel: "gpt-5-2025-08-07",
+ respModel: "gpt-5-2025-08-07",
+ inputTokens: 2038,
+ outputTokens: 239,
+ finishReason: "tool-calls",
+ wrapperDurationMs: 3_500,
+ doDurationMs: 3_000,
+ responseText: "Let me look up the latest rate decision.",
+ toolCallsJson: JSON.stringify([{
+ toolCallId: "call_azure_001",
+ toolName: "lookupRate",
+ input: '{"source":"federal_reserve","metric":"funds_rate"}',
+ }]),
+ providerMetadata: {
+ azure: { responseId: "resp_seed_azure_001", serviceTier: "default" },
+ },
+ });
+ addToolCall(az.wrapperId, "lookupRate", '{"source":"federal_reserve","metric":"funds_rate"}', '{"rate":"4.25-4.50%","effectiveDate":"2024-12-18"}');
+
+ // =====================================================================
+ // 14) Perplexity → sonar-pro
+ // =====================================================================
+ addLlmPair({
+ wrapperMsg: "ai.generateText",
+ doMsg: "ai.generateText.doGenerate",
+ system: "perplexity",
+ reqModel: "sonar-pro",
+ respModel: "sonar-pro",
+ inputTokens: 151,
+ outputTokens: 428,
+ finishReason: "stop",
+ wrapperDurationMs: 4_500,
+ doDurationMs: 4_200,
+ responseText: "According to the Federal Reserve's most recent announcement on December 18, 2024, the federal funds rate target range was maintained at 4.25% to 4.50%. This decision was made during the December FOMC meeting.",
+ });
+
+ // =====================================================================
+ // 15) openai.chat → gpt-4o-mini (legacy chat completions, mode: "tool")
+ // =====================================================================
+ addLlmPair({
+ wrapperMsg: "ai.generateObject",
+ doMsg: "ai.generateObject.doGenerate",
+ system: "openai.chat",
+ reqModel: "gpt-4o-mini",
+ respModel: "gpt-4o-mini-2024-07-18",
+ inputTokens: 573,
+ outputTokens: 11,
+ finishReason: "stop",
+ wrapperDurationMs: 800,
+ doDurationMs: 600,
+ responseObject: JSON.stringify({ title: "Fed Rate Hold", emoji: "🏦" }),
+ settings: { maxRetries: 2, mode: "tool", temperature: 0.3 },
+ providerMetadata: {
+ openai: { reasoningTokens: 0, cachedPromptTokens: 0 },
+ },
+ });
+
+ // =====================================================================
+ // 16) Anthropic claude-sonnet-4-5 → streamText with reasoning
+ // =====================================================================
+ addLlmPair({
+ wrapperMsg: "ai.streamText",
+ doMsg: "ai.streamText.doStream",
+ system: "anthropic.messages",
+ reqModel: "claude-sonnet-4-5-20250929",
+ respModel: "claude-sonnet-4-5-20250929",
+ inputTokens: 15200,
+ outputTokens: 2840,
+ finishReason: "stop",
+ wrapperDurationMs: 12_000,
+ doDurationMs: 11_500,
+ responseText: "The Federal Reserve has maintained its target range for the federal funds rate at 4.25% to 4.50% since December 2024. This represents a pause in the rate-cutting cycle that began in September 2024. The FOMC has indicated it will continue to assess incoming data, the evolving outlook, and the balance of risks when considering further adjustments.",
+ responseReasoning: "The user is asking about the current Federal Reserve interest rate. Let me provide a comprehensive answer based on the most recent FOMC decision. I should include context about the rate trajectory and forward guidance.",
+ cacheReadTokens: 12400,
+ cacheCreationTokens: 2800,
+ providerMetadata: {
+ anthropic: {
+ usage: {
+ input_tokens: 15200,
+ output_tokens: 2840,
+ cache_creation_input_tokens: 2800,
+ cache_read_input_tokens: 12400,
+ service_tier: "standard",
+ inference_geo: "us-east-1",
+ },
+ },
+ },
+ });
+
+ // =====================================================================
+ // 17) google.vertex.chat → gemini-3.1-pro-preview with tool-calls
+ // =====================================================================
+ const vt = addLlmPair({
+ wrapperMsg: "ai.generateText",
+ doMsg: "ai.generateText.doGenerate",
+ system: "google.vertex.chat",
+ reqModel: "gemini-3.1-pro-preview",
+ respModel: "gemini-3.1-pro-preview",
+ inputTokens: 4200,
+ outputTokens: 680,
+ finishReason: "tool-calls",
+ wrapperDurationMs: 6_000,
+ doDurationMs: 5_500,
+ responseText: "I'll search for the latest FOMC decision and rate information.",
+ toolCallsJson: JSON.stringify([{
+ toolCallId: "call_vertex_001",
+ toolName: "searchFOMC",
+ input: '{"query":"latest FOMC decision december 2024"}',
+ }]),
+ providerMetadata: {
+ google: {
+ usageMetadata: {
+ thoughtsTokenCount: 320,
+ promptTokenCount: 4200,
+ candidatesTokenCount: 680,
+ totalTokenCount: 5200,
+ },
+ },
+ },
+ });
+ addToolCall(vt.wrapperId, "searchFOMC", '{"query":"latest FOMC decision december 2024"}', '{"decision":"hold","rate":"4.25-4.50%","date":"2024-12-18","vote":"unanimous"}', 800);
+
+ // =====================================================================
+ // 18) openai.responses → gpt-5.4 with reasoning tokens
+ // =====================================================================
+ addLlmPair({
+ wrapperMsg: "ai.streamText",
+ doMsg: "ai.streamText.doStream",
+ system: "openai.responses",
+ reqModel: "gpt-5.4",
+ respModel: "gpt-5.4-2026-03-05",
+ inputTokens: 8900,
+ outputTokens: 1250,
+ finishReason: "stop",
+ wrapperDurationMs: 8_000,
+ doDurationMs: 7_500,
+ responseText: "The Federal Reserve's current target range for the federal funds rate is 4.25% to 4.50%, established at the December 2024 FOMC meeting. The committee has signaled a cautious approach to further rate cuts, citing persistent inflation concerns.",
+ responseReasoning: "I need to provide accurate, up-to-date information about the Federal Reserve interest rate. The last FOMC meeting was in December 2024 where they held rates steady after three consecutive cuts.",
+ reasoningTokens: 516,
+ providerMetadata: {
+ openai: {
+ responseId: "resp_seed_gpt54_001",
+ serviceTier: "default",
+ },
+ },
+ extraDoProps: {
+ "ai.response.msToFirstChunk": 1842.5,
+ "ai.response.msToFinish": 7234.8,
+ "ai.response.avgOutputTokensPerSecond": 172.8,
+ },
+ });
+
+ // =====================================================================
+ // 19) Cerebras cerebras-gpt-13b — no pricing, no provider cost
+ // =====================================================================
+ addLlmPair({
+ wrapperMsg: "ai.generateText",
+ doMsg: "ai.generateText.doGenerate",
+ system: "cerebras.chat",
+ reqModel: "cerebras-gpt-13b",
+ respModel: "cerebras-gpt-13b",
+ inputTokens: 450,
+ outputTokens: 120,
+ finishReason: "stop",
+ wrapperDurationMs: 600,
+ doDurationMs: 400,
+ responseText: "The Federal Reserve rate is currently at 4.25-4.50%.",
+ });
+
+ // =====================================================================
+ // 20) Amazon Bedrock — no pricing in registry
+ // =====================================================================
+ addLlmPair({
+ wrapperMsg: "ai.generateText",
+ doMsg: "ai.generateText.doGenerate",
+ system: "amazon-bedrock",
+ reqModel: "us.anthropic.claude-sonnet-4-20250514-v1:0",
+ respModel: "us.anthropic.claude-sonnet-4-20250514-v1:0",
+ inputTokens: 3200,
+ outputTokens: 890,
+ finishReason: "stop",
+ wrapperDurationMs: 4_000,
+ doDurationMs: 3_500,
+ responseText: "Based on the latest FOMC statement, the target rate range remains at 4.25% to 4.50%.",
+ });
+
+ // =====================================================================
+ // 21) Groq — fast inference, no pricing
+ // =====================================================================
+ addLlmPair({
+ wrapperMsg: "ai.generateObject",
+ doMsg: "ai.generateObject.doGenerate",
+ system: "groq.chat",
+ reqModel: "llama-4-scout-17b-16e-instruct",
+ respModel: "llama-4-scout-17b-16e-instruct",
+ inputTokens: 820,
+ outputTokens: 95,
+ finishReason: "stop",
+ wrapperDurationMs: 300,
+ doDurationMs: 200,
+ responseObject: JSON.stringify({ rate: "4.25-4.50%", source: "FOMC", date: "2024-12-18" }),
+ });
+
+ return events;
+}
+
+// ---------------------------------------------------------------------------
+
+seedAiSpans()
+ .catch((e) => {
+ console.error("Seed failed:");
+ console.error(e);
+ process.exit(1);
+ })
+ .finally(async () => {
+ await prisma.$disconnect();
+ });
diff --git a/apps/webapp/test/otlpExporter.test.ts b/apps/webapp/test/otlpExporter.test.ts
index 98380f2a596..6ba54243faf 100644
--- a/apps/webapp/test/otlpExporter.test.ts
+++ b/apps/webapp/test/otlpExporter.test.ts
@@ -1,5 +1,8 @@
-import { describe, it, expect } from "vitest";
-import { enrichCreatableEvents } from "../app/v3/utils/enrichCreatableEvents.server.js";
+import { describe, it, expect, beforeEach, afterEach } from "vitest";
+import {
+ enrichCreatableEvents,
+ setLlmPricingRegistry,
+} from "../app/v3/utils/enrichCreatableEvents.server.js";
import {
RuntimeEnvironmentType,
TaskEventKind,
@@ -83,7 +86,7 @@ describe("OTLPExporter", () => {
const event = $events[0];
expect(event.message).toBe("Responses API with 'gpt-4o'");
- expect(event.style).toEqual({
+ expect(event.style).toMatchObject({
icon: "tabler-brand-openai",
});
});
@@ -161,9 +164,18 @@ describe("OTLPExporter", () => {
const event = $events[0];
expect(event.message).toBe("Responses API with gpt-4o");
- expect(event.style).toEqual({
+ expect(event.style).toMatchObject({
icon: "tabler-brand-openai",
});
+ // Enrichment also adds model/token pills as accessories
+ const style = event.style as Record;
+ expect(style.accessory).toMatchObject({
+ style: "pills",
+ items: expect.arrayContaining([
+ expect.objectContaining({ text: "gpt-4o-2024-08-06" }),
+ expect.objectContaining({ text: "724" }),
+ ]),
+ });
});
it("should handle missing properties gracefully", () => {
@@ -394,4 +406,301 @@ describe("OTLPExporter", () => {
});
});
});
+
+ describe("LLM cost enrichment", () => {
+ const mockRegistry = {
+ isLoaded: true,
+ calculateCost: (responseModel: string, usageDetails: Record) => {
+ if (responseModel.startsWith("gpt-4o")) {
+ const inputCost = (usageDetails["input"] ?? 0) * 0.0000025;
+ const outputCost = (usageDetails["output"] ?? 0) * 0.00001;
+ return {
+ matchedModelId: "llm_model_gpt4o",
+ matchedModelName: "gpt-4o",
+ pricingTierId: "tier-standard",
+ pricingTierName: "Standard",
+ inputCost,
+ outputCost,
+ totalCost: inputCost + outputCost,
+ costDetails: { input: inputCost, output: outputCost },
+ };
+ }
+ return null;
+ },
+ };
+
+ beforeEach(() => {
+ setLlmPricingRegistry(mockRegistry);
+ });
+
+ afterEach(() => {
+ setLlmPricingRegistry(undefined as any);
+ });
+
+ function makeGenAiEvent(overrides: Record = {}) {
+ return {
+ message: "ai.streamText.doStream",
+ traceId: "test-trace",
+ spanId: "test-span",
+ parentId: "test-parent",
+ isPartial: false,
+ isError: false,
+ kind: TaskEventKind.INTERNAL,
+ level: TaskEventLevel.TRACE,
+ status: TaskEventStatus.UNSET,
+ startTime: BigInt(1),
+ duration: 5000000000,
+ style: {},
+ serviceName: "test",
+ environmentId: "env-1",
+ environmentType: RuntimeEnvironmentType.DEVELOPMENT,
+ organizationId: "org-1",
+ projectId: "proj-1",
+ projectRef: "proj_test",
+ runId: "run_test",
+ runIsTest: false,
+ taskSlug: "my-task",
+ metadata: undefined,
+ properties: {
+ "gen_ai.system": "openai",
+ "gen_ai.request.model": "gpt-4o",
+ "gen_ai.response.model": "gpt-4o-2024-08-06",
+ "gen_ai.usage.input_tokens": 702,
+ "gen_ai.usage.output_tokens": 22,
+ "operation.name": "ai.streamText.doStream",
+ ...overrides,
+ },
+ };
+ }
+
+ it("should enrich spans with cost attributes and accessories", () => {
+ const events = [makeGenAiEvent()];
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ const event = $events[0];
+
+ // Cost attributes
+ expect(event.properties["trigger.llm.total_cost"]).toBeCloseTo(0.001975);
+ expect(event.properties["trigger.llm.input_cost"]).toBeCloseTo(0.001755);
+ expect(event.properties["trigger.llm.output_cost"]).toBeCloseTo(0.00022);
+ expect(event.properties["trigger.llm.matched_model"]).toBe("gpt-4o");
+ expect(event.properties["trigger.llm.pricing_tier"]).toBe("Standard");
+
+ // Accessories (pills style)
+ expect(event.style.accessory).toBeDefined();
+ expect(event.style.accessory.style).toBe("pills");
+ expect(event.style.accessory.items).toHaveLength(3);
+ expect(event.style.accessory.items[0]).toEqual({
+ text: "gpt-4o-2024-08-06",
+ icon: "tabler-cube",
+ });
+ expect(event.style.accessory.items[1]).toEqual({
+ text: "724",
+ icon: "tabler-hash",
+ });
+ expect(event.style.accessory.items[2]).toEqual({
+ text: "$0.001975",
+ icon: "tabler-currency-dollar",
+ });
+ });
+
+ it("should set _llmUsage side-channel for dual-write", () => {
+ const events = [makeGenAiEvent()];
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ const event = $events[0];
+
+ expect(event._llmUsage).toBeDefined();
+ expect(event._llmUsage.genAiSystem).toBe("openai");
+ expect(event._llmUsage.responseModel).toBe("gpt-4o-2024-08-06");
+ expect(event._llmUsage.inputTokens).toBe(702);
+ expect(event._llmUsage.outputTokens).toBe(22);
+ expect(event._llmUsage.totalCost).toBeCloseTo(0.001975);
+ expect(event._llmUsage.operationName).toBe("ai.streamText.doStream");
+ });
+
+ it("should skip partial spans", () => {
+ const events = [makeGenAiEvent()];
+ events[0].isPartial = true;
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ expect($events[0].properties["trigger.llm.total_cost"]).toBeUndefined();
+ expect($events[0]._llmUsage).toBeUndefined();
+ });
+
+ it("should skip spans without gen_ai.response.model or gen_ai.request.model", () => {
+ const events = [
+ makeGenAiEvent({
+ "gen_ai.response.model": undefined,
+ "gen_ai.request.model": undefined,
+ }),
+ ];
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ expect($events[0].properties["trigger.llm.total_cost"]).toBeUndefined();
+ });
+
+ it("should fall back to gen_ai.request.model when response.model is missing", () => {
+ const events = [
+ makeGenAiEvent({
+ "gen_ai.response.model": undefined,
+ "gen_ai.request.model": "gpt-4o",
+ }),
+ ];
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ expect($events[0].properties["trigger.llm.matched_model"]).toBe("gpt-4o");
+ });
+
+ it("should skip spans with no token usage", () => {
+ const events = [
+ makeGenAiEvent({
+ "gen_ai.usage.input_tokens": 0,
+ "gen_ai.usage.output_tokens": 0,
+ }),
+ ];
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ expect($events[0].properties["trigger.llm.total_cost"]).toBeUndefined();
+ });
+
+ it("should skip spans with unknown models", () => {
+ const events = [
+ makeGenAiEvent({
+ "gen_ai.response.model": "unknown-model-xyz",
+ "gen_ai.request.model": "unknown-model-xyz",
+ }),
+ ];
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ expect($events[0].properties["trigger.llm.total_cost"]).toBeUndefined();
+ });
+
+ it("should not enrich non-span kinds like SPAN_EVENT or LOG", () => {
+ const events = [makeGenAiEvent()];
+ events[0].kind = "SPAN_EVENT" as any;
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ expect($events[0].properties["trigger.llm.total_cost"]).toBeUndefined();
+ });
+
+ it("should enrich SERVER kind events", () => {
+ const events = [makeGenAiEvent()];
+ events[0].kind = TaskEventKind.SERVER;
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ expect($events[0].properties["trigger.llm.total_cost"]).toBeCloseTo(0.001975);
+ });
+
+ it("should not enrich when registry is not loaded", () => {
+ setLlmPricingRegistry({ isLoaded: false, calculateCost: () => null });
+ const events = [makeGenAiEvent()];
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ expect($events[0].properties["trigger.llm.total_cost"]).toBeUndefined();
+ });
+
+ it("should format token counts with k/M suffixes in accessories", () => {
+ const events = [
+ makeGenAiEvent({
+ "gen_ai.usage.input_tokens": 150000,
+ "gen_ai.usage.output_tokens": 2000,
+ }),
+ ];
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ expect($events[0].style.accessory.items[1].text).toBe("152.0k");
+ });
+
+ it("should normalize alternate token attribute names", () => {
+ const events = [
+ makeGenAiEvent({
+ "gen_ai.usage.input_tokens": undefined,
+ "gen_ai.usage.output_tokens": undefined,
+ "gen_ai.usage.prompt_tokens": 500,
+ "gen_ai.usage.completion_tokens": 100,
+ }),
+ ];
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ expect($events[0]._llmUsage.inputTokens).toBe(500);
+ expect($events[0]._llmUsage.outputTokens).toBe(100);
+ });
+
+ it("should prefer gen_ai.usage.total_tokens over input+output sum", () => {
+ const events = [
+ makeGenAiEvent({
+ "gen_ai.usage.input_tokens": 100,
+ "gen_ai.usage.output_tokens": 50,
+ "gen_ai.usage.total_tokens": 200, // higher than 100+50 (e.g. includes cached/reasoning)
+ }),
+ ];
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ const event = $events[0];
+
+ // Pills should show the explicit total, not input+output
+ expect(event.style.accessory.items[1]).toEqual({
+ text: "200",
+ icon: "tabler-hash",
+ });
+
+ // LLM usage should also use the explicit total
+ expect(event._llmUsage.totalTokens).toBe(200);
+ expect(event._llmUsage.inputTokens).toBe(100);
+ expect(event._llmUsage.outputTokens).toBe(50);
+ });
+
+ it("should fall back to input+output when total_tokens is absent", () => {
+ const events = [
+ makeGenAiEvent({
+ "gen_ai.usage.input_tokens": 300,
+ "gen_ai.usage.output_tokens": 75,
+ }),
+ ];
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ const event = $events[0];
+
+ expect(event.style.accessory.items[1]).toEqual({
+ text: "375",
+ icon: "tabler-hash",
+ });
+ expect(event._llmUsage.totalTokens).toBe(375);
+ });
+
+ it("should use total_tokens when only total is present without input/output breakdown", () => {
+ const events = [
+ makeGenAiEvent({
+ "gen_ai.usage.input_tokens": undefined,
+ "gen_ai.usage.output_tokens": undefined,
+ "gen_ai.usage.total_tokens": 500,
+ }),
+ ];
+
+ // @ts-expect-error
+ const $events = enrichCreatableEvents(events);
+ const event = $events[0];
+
+ // Pills should show 500, not 0
+ expect(event.style.accessory.items[1]).toEqual({
+ text: "500",
+ icon: "tabler-hash",
+ });
+ });
+ });
});
diff --git a/internal-packages/clickhouse/schema/024_create_llm_usage_v1.sql b/internal-packages/clickhouse/schema/024_create_llm_usage_v1.sql
new file mode 100644
index 00000000000..5d7f879fdd3
--- /dev/null
+++ b/internal-packages/clickhouse/schema/024_create_llm_usage_v1.sql
@@ -0,0 +1,48 @@
+-- +goose Up
+CREATE TABLE IF NOT EXISTS trigger_dev.llm_usage_v1
+(
+ organization_id LowCardinality(String),
+ project_id LowCardinality(String),
+ environment_id String CODEC(ZSTD(1)),
+ run_id String CODEC(ZSTD(1)),
+ task_identifier LowCardinality(String),
+ trace_id String CODEC(ZSTD(1)),
+ span_id String CODEC(ZSTD(1)),
+
+ gen_ai_system LowCardinality(String),
+ request_model String CODEC(ZSTD(1)),
+ response_model String CODEC(ZSTD(1)),
+ matched_model_id String CODEC(ZSTD(1)),
+ operation_name LowCardinality(String),
+ pricing_tier_id String CODEC(ZSTD(1)),
+ pricing_tier_name LowCardinality(String),
+
+ input_tokens UInt64 DEFAULT 0,
+ output_tokens UInt64 DEFAULT 0,
+ total_tokens UInt64 DEFAULT 0,
+ usage_details Map(LowCardinality(String), UInt64),
+
+ input_cost Decimal64(12) DEFAULT 0,
+ output_cost Decimal64(12) DEFAULT 0,
+ total_cost Decimal64(12) DEFAULT 0,
+ cost_details Map(LowCardinality(String), Decimal64(12)),
+
+ metadata Map(LowCardinality(String), String),
+
+ start_time DateTime64(9) CODEC(Delta(8), ZSTD(1)),
+ duration UInt64 DEFAULT 0 CODEC(ZSTD(1)),
+ inserted_at DateTime64(3) DEFAULT now64(3),
+
+ INDEX idx_run_id run_id TYPE bloom_filter(0.001) GRANULARITY 1,
+ INDEX idx_span_id span_id TYPE bloom_filter(0.001) GRANULARITY 1,
+ INDEX idx_response_model response_model TYPE bloom_filter(0.01) GRANULARITY 1,
+ INDEX idx_metadata_keys mapKeys(metadata) TYPE bloom_filter(0.01) GRANULARITY 1
+)
+ENGINE = MergeTree
+PARTITION BY toDate(inserted_at)
+ORDER BY (organization_id, project_id, environment_id, toDate(inserted_at), run_id)
+TTL toDateTime(inserted_at) + INTERVAL 365 DAY
+SETTINGS ttl_only_drop_parts = 1;
+
+-- +goose Down
+DROP TABLE IF EXISTS trigger_dev.llm_usage_v1;
diff --git a/internal-packages/clickhouse/src/index.ts b/internal-packages/clickhouse/src/index.ts
index b6fbd92177b..336abf3761f 100644
--- a/internal-packages/clickhouse/src/index.ts
+++ b/internal-packages/clickhouse/src/index.ts
@@ -27,6 +27,7 @@ import {
getLogsSearchListQueryBuilder,
} from "./taskEvents.js";
import { insertMetrics } from "./metrics.js";
+import { insertLlmUsage } from "./llmUsage.js";
import {
getErrorGroups,
getErrorInstances,
@@ -44,6 +45,7 @@ import type { Agent as HttpsAgent } from "https";
export type * from "./taskRuns.js";
export type * from "./taskEvents.js";
export type * from "./metrics.js";
+export type * from "./llmUsage.js";
export type * from "./errors.js";
export type * from "./client/queryBuilder.js";
@@ -225,6 +227,12 @@ export class ClickHouse {
};
}
+ get llmUsage() {
+ return {
+ insert: insertLlmUsage(this.writer),
+ };
+ }
+
get taskEventsV2() {
return {
insert: insertTaskEventsV2(this.writer),
diff --git a/internal-packages/clickhouse/src/llmUsage.ts b/internal-packages/clickhouse/src/llmUsage.ts
new file mode 100644
index 00000000000..e9423962ac4
--- /dev/null
+++ b/internal-packages/clickhouse/src/llmUsage.ts
@@ -0,0 +1,44 @@
+import { z } from "zod";
+import { ClickhouseWriter } from "./client/types.js";
+
+export const LlmUsageV1Input = z.object({
+ organization_id: z.string(),
+ project_id: z.string(),
+ environment_id: z.string(),
+ run_id: z.string(),
+ task_identifier: z.string(),
+ trace_id: z.string(),
+ span_id: z.string(),
+
+ gen_ai_system: z.string(),
+ request_model: z.string(),
+ response_model: z.string(),
+ matched_model_id: z.string(),
+ operation_name: z.string(),
+ pricing_tier_id: z.string(),
+ pricing_tier_name: z.string(),
+
+ input_tokens: z.number(),
+ output_tokens: z.number(),
+ total_tokens: z.number(),
+ usage_details: z.record(z.string(), z.number()),
+
+ input_cost: z.number(),
+ output_cost: z.number(),
+ total_cost: z.number(),
+ cost_details: z.record(z.string(), z.number()),
+
+ metadata: z.record(z.string(), z.string()),
+
+ start_time: z.string(),
+ duration: z.string(),
+});
+
+export type LlmUsageV1Input = z.input;
+
+export function insertLlmUsage(ch: ClickhouseWriter) {
+ return ch.insertUnsafe({
+ name: "insertLlmUsage",
+ table: "trigger_dev.llm_usage_v1",
+ });
+}
diff --git a/internal-packages/database/prisma/migrations/20260310155049_add_llm_pricing_tables/migration.sql b/internal-packages/database/prisma/migrations/20260310155049_add_llm_pricing_tables/migration.sql
new file mode 100644
index 00000000000..286de6eacfb
--- /dev/null
+++ b/internal-packages/database/prisma/migrations/20260310155049_add_llm_pricing_tables/migration.sql
@@ -0,0 +1,67 @@
+-- CreateTable
+CREATE TABLE "public"."llm_models" (
+ "id" TEXT NOT NULL,
+ "friendly_id" TEXT NOT NULL,
+ "project_id" TEXT,
+ "model_name" TEXT NOT NULL,
+ "match_pattern" TEXT NOT NULL,
+ "start_date" TIMESTAMP(3),
+ "source" TEXT NOT NULL DEFAULT 'default',
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "llm_models_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "public"."llm_pricing_tiers" (
+ "id" TEXT NOT NULL,
+ "model_id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "is_default" BOOLEAN NOT NULL DEFAULT true,
+ "priority" INTEGER NOT NULL DEFAULT 0,
+ "conditions" JSONB NOT NULL DEFAULT '[]',
+
+ CONSTRAINT "llm_pricing_tiers_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "public"."llm_prices" (
+ "id" TEXT NOT NULL,
+ "model_id" TEXT NOT NULL,
+ "pricing_tier_id" TEXT NOT NULL,
+ "usage_type" TEXT NOT NULL,
+ "price" DECIMAL(20,12) NOT NULL,
+
+ CONSTRAINT "llm_prices_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "llm_models_friendly_id_key" ON "public"."llm_models"("friendly_id");
+
+-- CreateIndex
+CREATE INDEX "llm_models_project_id_idx" ON "public"."llm_models"("project_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "llm_models_project_id_model_name_start_date_key" ON "public"."llm_models"("project_id", "model_name", "start_date");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "llm_pricing_tiers_model_id_priority_key" ON "public"."llm_pricing_tiers"("model_id", "priority");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "llm_pricing_tiers_model_id_name_key" ON "public"."llm_pricing_tiers"("model_id", "name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "llm_prices_model_id_usage_type_pricing_tier_id_key" ON "public"."llm_prices"("model_id", "usage_type", "pricing_tier_id");
+
+-- AddForeignKey
+ALTER TABLE "public"."llm_models" ADD CONSTRAINT "llm_models_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "public"."Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."llm_pricing_tiers" ADD CONSTRAINT "llm_pricing_tiers_model_id_fkey" FOREIGN KEY ("model_id") REFERENCES "public"."llm_models"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."llm_prices" ADD CONSTRAINT "llm_prices_model_id_fkey" FOREIGN KEY ("model_id") REFERENCES "public"."llm_models"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."llm_prices" ADD CONSTRAINT "llm_prices_pricing_tier_id_fkey" FOREIGN KEY ("pricing_tier_id") REFERENCES "public"."llm_pricing_tiers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma
index f6986be42c0..9e91fc70f14 100644
--- a/internal-packages/database/prisma/schema.prisma
+++ b/internal-packages/database/prisma/schema.prisma
@@ -417,6 +417,7 @@ model Project {
onboardingData Json?
taskScheduleInstances TaskScheduleInstance[]
metricsDashboards MetricsDashboard[]
+ llmModels LlmModel[]
}
enum ProjectVersion {
@@ -2577,3 +2578,59 @@ model MetricsDashboard {
/// Fast lookup for the list
@@index([projectId, createdAt(sort: Desc)])
}
+
+// ====================================================
+// LLM Pricing Models
+// ====================================================
+
+/// A known LLM model or model pattern for cost tracking
+model LlmModel {
+ id String @id @default(cuid())
+ friendlyId String @unique @map("friendly_id")
+ projectId String? @map("project_id")
+ project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
+ modelName String @map("model_name")
+ matchPattern String @map("match_pattern")
+ startDate DateTime? @map("start_date")
+ source String @default("default") // "default", "admin", "project"
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+
+ pricingTiers LlmPricingTier[]
+ prices LlmPrice[]
+
+ @@unique([projectId, modelName, startDate])
+ @@index([projectId])
+ @@map("llm_models")
+}
+
+/// A pricing tier for a model (supports volume-based or conditional pricing)
+model LlmPricingTier {
+ id String @id @default(cuid())
+ modelId String @map("model_id")
+ model LlmModel @relation(fields: [modelId], references: [id], onDelete: Cascade)
+ name String
+ isDefault Boolean @default(true) @map("is_default")
+ priority Int @default(0)
+ conditions Json @default("[]") @db.JsonB
+
+ prices LlmPrice[]
+
+ @@unique([modelId, priority])
+ @@unique([modelId, name])
+ @@map("llm_pricing_tiers")
+}
+
+/// A price point for a usage type within a pricing tier
+model LlmPrice {
+ id String @id @default(cuid())
+ modelId String @map("model_id")
+ model LlmModel @relation(fields: [modelId], references: [id], onDelete: Cascade)
+ pricingTierId String @map("pricing_tier_id")
+ pricingTier LlmPricingTier @relation(fields: [pricingTierId], references: [id], onDelete: Cascade)
+ usageType String @map("usage_type")
+ price Decimal @db.Decimal(20, 12)
+
+ @@unique([modelId, usageType, pricingTierId])
+ @@map("llm_prices")
+}
diff --git a/internal-packages/llm-pricing/package.json b/internal-packages/llm-pricing/package.json
new file mode 100644
index 00000000000..8cf9e366f2c
--- /dev/null
+++ b/internal-packages/llm-pricing/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@internal/llm-pricing",
+ "private": true,
+ "version": "0.0.1",
+ "main": "./src/index.ts",
+ "types": "./src/index.ts",
+ "type": "module",
+ "dependencies": {
+ "@trigger.dev/core": "workspace:*",
+ "@trigger.dev/database": "workspace:*"
+ },
+ "scripts": {
+ "typecheck": "tsc --noEmit",
+ "generate": "echo 'defaultPrices.ts is pre-committed — run sync-prices to update'",
+ "sync-prices": "bash scripts/sync-model-prices.sh",
+ "sync-prices:check": "bash scripts/sync-model-prices.sh --check"
+ }
+}
diff --git a/internal-packages/llm-pricing/scripts/sync-model-prices.sh b/internal-packages/llm-pricing/scripts/sync-model-prices.sh
new file mode 100755
index 00000000000..d72aa6714c6
--- /dev/null
+++ b/internal-packages/llm-pricing/scripts/sync-model-prices.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Sync default model prices from Langfuse's repository and generate the TS module.
+# Usage: ./scripts/sync-model-prices.sh [--check]
+# --check: Exit 1 if prices are outdated (for CI)
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PACKAGE_DIR="$(dirname "$SCRIPT_DIR")"
+JSON_TARGET="$PACKAGE_DIR/src/default-model-prices.json"
+TS_TARGET="$PACKAGE_DIR/src/defaultPrices.ts"
+SOURCE_URL="https://raw.githubusercontent.com/langfuse/langfuse/main/worker/src/constants/default-model-prices.json"
+
+CHECK_MODE=false
+if [[ "${1:-}" == "--check" ]]; then
+ CHECK_MODE=true
+fi
+
+echo "Fetching latest model prices from Langfuse..."
+TMPFILE=$(mktemp)
+trap 'rm -f "$TMPFILE"' EXIT
+
+if ! curl -fsSL "$SOURCE_URL" -o "$TMPFILE"; then
+ echo "ERROR: Failed to fetch from $SOURCE_URL"
+ exit 1
+fi
+
+# Validate it's valid JSON with at least some models
+MODEL_COUNT=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$TMPFILE','utf-8')).length)" 2>/dev/null || echo "0")
+if [[ "$MODEL_COUNT" -lt 10 ]]; then
+ echo "ERROR: Downloaded file has only $MODEL_COUNT models (expected 100+). Aborting."
+ exit 1
+fi
+
+if $CHECK_MODE; then
+ if diff -q "$JSON_TARGET" "$TMPFILE" > /dev/null 2>&1; then
+ echo "Model prices are up to date ($MODEL_COUNT models)"
+ exit 0
+ else
+ echo "Model prices are OUTDATED. Run 'pnpm run sync-prices' in @internal/llm-pricing to update."
+ exit 1
+ fi
+fi
+
+cp "$TMPFILE" "$JSON_TARGET"
+echo "Updated default-model-prices.json ($MODEL_COUNT models)"
+
+# Generate the TypeScript module from the JSON
+echo "Generating defaultPrices.ts..."
+node -e "
+const data = JSON.parse(require('fs').readFileSync('$JSON_TARGET', 'utf-8'));
+const stripped = data.map(e => ({
+ modelName: e.modelName.trim(),
+ matchPattern: e.matchPattern,
+ startDate: e.createdAt,
+ pricingTiers: e.pricingTiers.map(t => ({
+ name: t.name,
+ isDefault: t.isDefault,
+ priority: t.priority,
+ conditions: t.conditions.map(c => ({
+ usageDetailPattern: c.usageDetailPattern,
+ operator: c.operator,
+ value: c.value,
+ })),
+ prices: t.prices,
+ })),
+}));
+
+let out = 'import type { DefaultModelDefinition } from \"./types.js\";\n\n';
+out += '// Auto-generated from Langfuse default-model-prices.json — do not edit manually.\n';
+out += '// Run \`pnpm run sync-prices\` to update from upstream.\n';
+out += '// Source: https://github.com/langfuse/langfuse\n\n';
+out += 'export const defaultModelPrices: DefaultModelDefinition[] = ';
+out += JSON.stringify(stripped, null, 2) + ';\n';
+require('fs').writeFileSync('$TS_TARGET', out);
+console.log('Generated defaultPrices.ts with ' + stripped.length + ' models');
+"
diff --git a/internal-packages/llm-pricing/src/default-model-prices.json b/internal-packages/llm-pricing/src/default-model-prices.json
new file mode 100644
index 00000000000..486c6c512b5
--- /dev/null
+++ b/internal-packages/llm-pricing/src/default-model-prices.json
@@ -0,0 +1,3838 @@
+[
+ {
+ "id": "b9854a5c92dc496b997d99d20",
+ "modelName": "gpt-4o",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4o)$",
+ "createdAt": "2024-05-13T23:15:07.670Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4o",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "b9854a5c92dc496b997d99d20_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000025,
+ "input_cached_tokens": 0.00000125,
+ "input_cache_read": 0.00000125,
+ "output": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "id": "b9854a5c92dc496b997d99d21",
+ "modelName": "gpt-4o-2024-05-13",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4o-2024-05-13)$",
+ "createdAt": "2024-05-13T23:15:07.670Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4o-2024-05-13",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "b9854a5c92dc496b997d99d21_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000005,
+ "output": 0.000015
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrkvq6iq000008ju6c16gynt",
+ "modelName": "gpt-4-1106-preview",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4-1106-preview)$",
+ "createdAt": "2024-04-23T10:37:17.092Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4-1106-preview",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrkvq6iq000008ju6c16gynt_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00001,
+ "output": 0.00003
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrkvx5gp000108juaogs54ea",
+ "modelName": "gpt-4-turbo-vision",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4(-\\d{4})?-vision-preview)$",
+ "createdAt": "2024-01-24T10:19:21.693Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4-vision-preview",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrkvx5gp000108juaogs54ea_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00001,
+ "output": 0.00003
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrkvyzgw000308jue4hse4j9",
+ "modelName": "gpt-4-32k",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4-32k)$",
+ "createdAt": "2024-01-24T10:19:21.693Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4-32k",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrkvyzgw000308jue4hse4j9_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00006,
+ "output": 0.00012
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrkwk4cb000108l5hwwh3zdi",
+ "modelName": "gpt-4-32k-0613",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4-32k-0613)$",
+ "createdAt": "2024-01-24T10:19:21.693Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4-32k-0613",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrkwk4cb000108l5hwwh3zdi_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00006,
+ "output": 0.00012
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrkwk4cb000208l59yvb9yq8",
+ "modelName": "gpt-3.5-turbo-1106",
+ "matchPattern": "(?i)^(openai\/)?(gpt-)(35|3.5)(-turbo-1106)$",
+ "createdAt": "2024-01-24T10:19:21.693Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-3.5-turbo-1106",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrkwk4cb000208l59yvb9yq8_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000001,
+ "output": 0.000002
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrkwk4cc000808l51xmk4uic",
+ "modelName": "gpt-3.5-turbo-0613",
+ "matchPattern": "(?i)^(openai\/)?(gpt-)(35|3.5)(-turbo-0613)$",
+ "createdAt": "2024-01-24T10:19:21.693Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-3.5-turbo-0613",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrkwk4cc000808l51xmk4uic_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000015,
+ "output": 0.000002
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrkwk4cc000908l537kl0rx3",
+ "modelName": "gpt-4-0613",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4-0613)$",
+ "createdAt": "2024-01-24T10:19:21.693Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4-0613",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrkwk4cc000908l537kl0rx3_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00003,
+ "output": 0.00006
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrkwk4cc000a08l562uc3s9g",
+ "modelName": "gpt-3.5-turbo-instruct",
+ "matchPattern": "(?i)^(openai\/)?(gpt-)(35|3.5)(-turbo-instruct)$",
+ "createdAt": "2024-01-24T10:19:21.693Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-3.5-turbo",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrkwk4cc000a08l562uc3s9g_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000015,
+ "output": 0.000002
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrntjt89000108jwcou1af71",
+ "modelName": "text-ada-001",
+ "matchPattern": "(?i)^(text-ada-001)$",
+ "createdAt": "2024-01-24T18:18:50.861Z",
+ "updatedAt": "2024-01-24T18:18:50.861Z",
+ "tokenizerConfig": {
+ "tokenizerModel": "text-ada-001"
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrntjt89000108jwcou1af71_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 0.000004
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrntjt89000208jwawjr894q",
+ "modelName": "text-babbage-001",
+ "matchPattern": "(?i)^(text-babbage-001)$",
+ "createdAt": "2024-01-24T18:18:50.861Z",
+ "updatedAt": "2024-01-24T18:18:50.861Z",
+ "tokenizerConfig": {
+ "tokenizerModel": "text-babbage-001"
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrntjt89000208jwawjr894q_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrntjt89000308jw0jtfa4rs",
+ "modelName": "text-curie-001",
+ "matchPattern": "(?i)^(text-curie-001)$",
+ "createdAt": "2024-01-24T18:18:50.861Z",
+ "updatedAt": "2024-01-24T18:18:50.861Z",
+ "tokenizerConfig": {
+ "tokenizerModel": "text-curie-001"
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrntjt89000308jw0jtfa4rs_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 0.00002
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrntjt89000408jwc2c93h6i",
+ "modelName": "text-davinci-001",
+ "matchPattern": "(?i)^(text-davinci-001)$",
+ "createdAt": "2024-01-24T18:18:50.861Z",
+ "updatedAt": "2024-01-24T18:18:50.861Z",
+ "tokenizerConfig": {
+ "tokenizerModel": "text-davinci-001"
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrntjt89000408jwc2c93h6i_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 0.00002
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrntjt89000508jw192m64qi",
+ "modelName": "text-davinci-002",
+ "matchPattern": "(?i)^(text-davinci-002)$",
+ "createdAt": "2024-01-24T18:18:50.861Z",
+ "updatedAt": "2024-01-24T18:18:50.861Z",
+ "tokenizerConfig": {
+ "tokenizerModel": "text-davinci-002"
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrntjt89000508jw192m64qi_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 0.00002
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrntjt89000608jw4m3x5s55",
+ "modelName": "text-davinci-003",
+ "matchPattern": "(?i)^(text-davinci-003)$",
+ "createdAt": "2024-01-24T18:18:50.861Z",
+ "updatedAt": "2024-01-24T18:18:50.861Z",
+ "tokenizerConfig": {
+ "tokenizerModel": "text-davinci-003"
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrntjt89000608jw4m3x5s55_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 0.00002
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrntjt89000908jwhvkz5crg",
+ "modelName": "text-embedding-ada-002-v2",
+ "matchPattern": "(?i)^(text-embedding-ada-002-v2)$",
+ "createdAt": "2024-01-24T18:18:50.861Z",
+ "updatedAt": "2024-01-24T18:18:50.861Z",
+ "tokenizerConfig": {
+ "tokenizerModel": "text-embedding-ada-002"
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrntjt89000908jwhvkz5crg_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 1e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrntjt89000908jwhvkz5crm",
+ "modelName": "text-embedding-ada-002",
+ "matchPattern": "(?i)^(text-embedding-ada-002)$",
+ "createdAt": "2024-01-24T18:18:50.861Z",
+ "updatedAt": "2024-01-24T18:18:50.861Z",
+ "tokenizerConfig": {
+ "tokenizerModel": "text-embedding-ada-002"
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrntjt89000908jwhvkz5crm_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 1e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrntjt89000a08jw0gcdbd5a",
+ "modelName": "gpt-3.5-turbo-16k-0613",
+ "matchPattern": "(?i)^(openai\/)?(gpt-)(35|3.5)(-turbo-16k-0613)$",
+ "createdAt": "2024-02-03T17:29:57.350Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-3.5-turbo-16k-0613",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrntjt89000a08jw0gcdbd5a_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "output": 0.000004
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrntkjgy000a08jx4e062mr0",
+ "modelName": "gpt-3.5-turbo-0301",
+ "matchPattern": "(?i)^(openai\/)?(gpt-)(35|3.5)(-turbo-0301)$",
+ "createdAt": "2024-01-24T10:19:21.693Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": -1,
+ "tokenizerModel": "gpt-3.5-turbo-0301",
+ "tokensPerMessage": 4
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrntkjgy000a08jx4e062mr0_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000002,
+ "output": 0.000002
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrntkjgy000d08jx0p4y9h4l",
+ "modelName": "gpt-4-32k-0314",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4-32k-0314)$",
+ "createdAt": "2024-01-24T10:19:21.693Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4-32k-0314",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrntkjgy000d08jx0p4y9h4l_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00006,
+ "output": 0.00012
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrntkjgy000e08jx4x6uawoo",
+ "modelName": "gpt-4-0314",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4-0314)$",
+ "createdAt": "2024-01-24T10:19:21.693Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4-0314",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrntkjgy000e08jx4x6uawoo_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00003,
+ "output": 0.00006
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrntkjgy000f08jx79v9g1xj",
+ "modelName": "gpt-4",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4)$",
+ "createdAt": "2024-01-24T10:19:21.693Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrntkjgy000f08jx79v9g1xj_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00003,
+ "output": 0.00006
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrnwb41q000308jsfrac9uh6",
+ "modelName": "claude-instant-1.2",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-instant-1.2)$",
+ "createdAt": "2024-01-30T15:44:13.447Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "clrnwb41q000308jsfrac9uh6_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000163,
+ "output": 0.00000551
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrnwb836000408jsallr6u11",
+ "modelName": "claude-2.0",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-2.0)$",
+ "createdAt": "2024-01-30T15:44:13.447Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "clrnwb836000408jsallr6u11_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000008,
+ "output": 0.000024
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrnwbd1m000508js4hxu6o7n",
+ "modelName": "claude-2.1",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-2.1)$",
+ "createdAt": "2024-01-30T15:44:13.447Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "clrnwbd1m000508js4hxu6o7n_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000008,
+ "output": 0.000024
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrnwbg2b000608jse2pp4q2d",
+ "modelName": "claude-1.3",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-1.3)$",
+ "createdAt": "2024-01-30T15:44:13.447Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "clrnwbg2b000608jse2pp4q2d_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000008,
+ "output": 0.000024
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrnwbi9d000708jseiy44k26",
+ "modelName": "claude-1.2",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-1.2)$",
+ "createdAt": "2024-01-30T15:44:13.447Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "clrnwbi9d000708jseiy44k26_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000008,
+ "output": 0.000024
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrnwblo0000808jsc1385hdp",
+ "modelName": "claude-1.1",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-1.1)$",
+ "createdAt": "2024-01-30T15:44:13.447Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "clrnwblo0000808jsc1385hdp_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000008,
+ "output": 0.000024
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrnwbota000908jsgg9mb1ml",
+ "modelName": "claude-instant-1",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-instant-1)$",
+ "createdAt": "2024-01-30T15:44:13.447Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "clrnwbota000908jsgg9mb1ml_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000163,
+ "output": 0.00000551
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrs2dnql000108l46vo0gp2t",
+ "modelName": "babbage-002",
+ "matchPattern": "(?i)^(babbage-002)$",
+ "createdAt": "2024-01-26T17:35:21.129Z",
+ "updatedAt": "2024-01-26T17:35:21.129Z",
+ "tokenizerConfig": {
+ "tokenizerModel": "babbage-002"
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrs2dnql000108l46vo0gp2t_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 4e-7,
+ "output": 0.0000016
+ }
+ }
+ ]
+ },
+ {
+ "id": "clrs2ds35000208l4g4b0hi3u",
+ "modelName": "davinci-002",
+ "matchPattern": "(?i)^(davinci-002)$",
+ "createdAt": "2024-01-26T17:35:21.129Z",
+ "updatedAt": "2024-01-26T17:35:21.129Z",
+ "tokenizerConfig": {
+ "tokenizerModel": "davinci-002"
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clrs2ds35000208l4g4b0hi3u_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000006,
+ "output": 0.000012
+ }
+ }
+ ]
+ },
+ {
+ "id": "clruwn3pc00010al7bl611c8o",
+ "modelName": "text-embedding-3-small",
+ "matchPattern": "(?i)^(text-embedding-3-small)$",
+ "createdAt": "2024-01-26T17:35:21.129Z",
+ "updatedAt": "2024-01-26T17:35:21.129Z",
+ "tokenizerConfig": {
+ "tokenizerModel": "text-embedding-ada-002"
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clruwn3pc00010al7bl611c8o_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 2e-8
+ }
+ }
+ ]
+ },
+ {
+ "id": "clruwn76700020al7gp8e4g4l",
+ "modelName": "text-embedding-3-large",
+ "matchPattern": "(?i)^(text-embedding-3-large)$",
+ "createdAt": "2024-01-26T17:35:21.129Z",
+ "updatedAt": "2024-01-26T17:35:21.129Z",
+ "tokenizerConfig": {
+ "tokenizerModel": "text-embedding-ada-002"
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clruwn76700020al7gp8e4g4l_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 1.3e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "clruwnahl00030al7ab9rark7",
+ "modelName": "gpt-3.5-turbo-0125",
+ "matchPattern": "(?i)^(openai\/)?(gpt-)(35|3.5)(-turbo-0125)$",
+ "createdAt": "2024-01-26T17:35:21.129Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-3.5-turbo",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clruwnahl00030al7ab9rark7_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 5e-7,
+ "output": 0.0000015
+ }
+ }
+ ]
+ },
+ {
+ "id": "clruwnahl00040al78f1lb0at",
+ "modelName": "gpt-3.5-turbo",
+ "matchPattern": "(?i)^(openai\/)?(gpt-)(35|3.5)(-turbo)$",
+ "createdAt": "2024-02-13T12:00:37.424Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-3.5-turbo",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clruwnahl00040al78f1lb0at_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 5e-7,
+ "output": 0.0000015
+ }
+ }
+ ]
+ },
+ {
+ "id": "clruwnahl00050al796ck3p44",
+ "modelName": "gpt-4-0125-preview",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4-0125-preview)$",
+ "createdAt": "2024-01-26T17:35:21.129Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clruwnahl00050al796ck3p44_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00001,
+ "output": 0.00003
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls08r8sq000308jq14ae96f0",
+ "modelName": "ft:gpt-3.5-turbo-1106",
+ "matchPattern": "(?i)^(ft:)(gpt-3.5-turbo-1106:)(.+)(:)(.*)(:)(.+)$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-3.5-turbo-1106",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cls08r8sq000308jq14ae96f0_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "output": 0.000006
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls08rp99000408jqepxoakjv",
+ "modelName": "ft:gpt-3.5-turbo-0613",
+ "matchPattern": "(?i)^(ft:)(gpt-3.5-turbo-0613:)(.+)(:)(.*)(:)(.+)$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-3.5-turbo-0613",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cls08rp99000408jqepxoakjv_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000012,
+ "output": 0.000016
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls08rv9g000508jq5p4z4nlr",
+ "modelName": "ft:davinci-002",
+ "matchPattern": "(?i)^(ft:)(davinci-002:)(.+)(:)(.*)(:)(.+)$$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "tokenizerConfig": {
+ "tokenizerModel": "davinci-002"
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cls08rv9g000508jq5p4z4nlr_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000012,
+ "output": 0.000012
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls08s2bw000608jq57wj4un2",
+ "modelName": "ft:babbage-002",
+ "matchPattern": "(?i)^(ft:)(babbage-002:)(.+)(:)(.*)(:)(.+)$$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "tokenizerConfig": {
+ "tokenizerModel": "babbage-002"
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cls08s2bw000608jq57wj4un2_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000016,
+ "output": 0.0000016
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls0iv12d000108l251gf3038",
+ "modelName": "chat-bison",
+ "matchPattern": "(?i)^(chat-bison)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "id": "cls0iv12d000108l251gf3038_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls0j33v1000008joagkc4lql",
+ "modelName": "codechat-bison-32k",
+ "matchPattern": "(?i)^(codechat-bison-32k)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "id": "cls0j33v1000008joagkc4lql_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls0jmc9v000008l8ee6r3gsd",
+ "modelName": "codechat-bison",
+ "matchPattern": "(?i)^(codechat-bison)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "id": "cls0jmc9v000008l8ee6r3gsd_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls0jmjt3000108l83ix86w0d",
+ "modelName": "text-bison-32k",
+ "matchPattern": "(?i)^(text-bison-32k)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "id": "cls0jmjt3000108l83ix86w0d_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls0jni4t000008jk3kyy803r",
+ "modelName": "chat-bison-32k",
+ "matchPattern": "(?i)^(chat-bison-32k)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "id": "cls0jni4t000008jk3kyy803r_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls0jungb000208jk12gm4gk1",
+ "modelName": "text-unicorn",
+ "matchPattern": "(?i)^(text-unicorn)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "id": "cls0jungb000208jk12gm4gk1_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000025,
+ "output": 0.0000075
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls0juygp000308jk2a6x9my2",
+ "modelName": "text-bison",
+ "matchPattern": "(?i)^(text-bison)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "id": "cls0juygp000308jk2a6x9my2_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls1nyj5q000208l33ne901d8",
+ "modelName": "textembedding-gecko",
+ "matchPattern": "(?i)^(textembedding-gecko)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "id": "cls1nyj5q000208l33ne901d8_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 1e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls1nyyjp000308l31gxy1bih",
+ "modelName": "textembedding-gecko-multilingual",
+ "matchPattern": "(?i)^(textembedding-gecko-multilingual)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "id": "cls1nyyjp000308l31gxy1bih_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 1e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls1nzjt3000508l3dnwad3g0",
+ "modelName": "code-gecko",
+ "matchPattern": "(?i)^(code-gecko)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "id": "cls1nzjt3000508l3dnwad3g0_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls1nzwx4000608l38va7e4tv",
+ "modelName": "code-bison",
+ "matchPattern": "(?i)^(code-bison)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "id": "cls1nzwx4000608l38va7e4tv_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cls1o053j000708l39f8g4bgs",
+ "modelName": "code-bison-32k",
+ "matchPattern": "(?i)^(code-bison-32k)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-01-31T13:25:02.141Z",
+ "updatedAt": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "id": "cls1o053j000708l39f8g4bgs_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "clsk9lntu000008jwfc51bbqv",
+ "modelName": "gpt-3.5-turbo-16k",
+ "matchPattern": "(?i)^(openai\/)?(gpt-)(35|3.5)(-turbo-16k)$",
+ "createdAt": "2024-02-13T12:00:37.424Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-3.5-turbo-16k",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clsk9lntu000008jwfc51bbqv_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 5e-7,
+ "output": 0.0000015
+ }
+ }
+ ]
+ },
+ {
+ "id": "clsnq07bn000008l4e46v1ll8",
+ "modelName": "gpt-4-turbo-preview",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4-turbo-preview)$",
+ "createdAt": "2024-02-15T21:21:50.947Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clsnq07bn000008l4e46v1ll8_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00001,
+ "output": 0.00003
+ }
+ }
+ ]
+ },
+ {
+ "id": "cltgy0iuw000008le3vod1hhy",
+ "modelName": "claude-3-opus-20240229",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-3-opus-20240229|anthropic\\.claude-3-opus-20240229-v1:0|claude-3-opus@20240229)$",
+ "createdAt": "2024-03-07T17:55:38.139Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cltgy0iuw000008le3vod1hhy_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "output": 0.000075
+ }
+ }
+ ]
+ },
+ {
+ "id": "cltgy0pp6000108le56se7bl3",
+ "modelName": "claude-3-sonnet-20240229",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-3-sonnet-20240229|anthropic\\.claude-3-sonnet-20240229-v1:0|claude-3-sonnet@20240229)$",
+ "createdAt": "2024-03-07T17:55:38.139Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cltgy0pp6000108le56se7bl3_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cltr0w45b000008k1407o9qv1",
+ "modelName": "claude-3-haiku-20240307",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-3-haiku-20240307|anthropic\\.claude-3-haiku-20240307-v1:0|claude-3-haiku@20240307)$",
+ "createdAt": "2024-03-14T09:41:18.736Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cltr0w45b000008k1407o9qv1_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 0.00000125
+ }
+ }
+ ]
+ },
+ {
+ "id": "cluv2sjeo000008ih0fv23hi0",
+ "modelName": "gemini-1.0-pro-latest",
+ "matchPattern": "(?i)^(google\/)?(gemini-1.0-pro-latest)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-04-11T10:27:46.517Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cluv2sjeo000008ih0fv23hi0_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cluv2subq000108ih2mlrga6a",
+ "modelName": "gemini-1.0-pro",
+ "matchPattern": "(?i)^(google\/)?(gemini-1.0-pro)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-04-11T10:27:46.517Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cluv2subq000108ih2mlrga6a_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1.25e-7,
+ "output": 3.75e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cluv2sx04000208ihbek75lsz",
+ "modelName": "gemini-1.0-pro-001",
+ "matchPattern": "(?i)^(google\/)?(gemini-1.0-pro-001)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-04-11T10:27:46.517Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cluv2sx04000208ihbek75lsz_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1.25e-7,
+ "output": 3.75e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cluv2szw0000308ihch3n79x7",
+ "modelName": "gemini-pro",
+ "matchPattern": "(?i)^(google\/)?(gemini-pro)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-04-11T10:27:46.517Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cluv2szw0000308ihch3n79x7_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1.25e-7,
+ "output": 3.75e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cluv2t2x0000408ihfytl45l1",
+ "modelName": "gemini-1.5-pro-latest",
+ "matchPattern": "(?i)^(google\/)?(gemini-1.5-pro-latest)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2024-04-11T10:27:46.517Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cluv2t2x0000408ihfytl45l1_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000025,
+ "output": 0.0000075
+ }
+ }
+ ]
+ },
+ {
+ "id": "cluv2t5k3000508ih5kve9zag",
+ "modelName": "gpt-4-turbo-2024-04-09",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4-turbo-2024-04-09)$",
+ "createdAt": "2024-04-23T10:37:17.092Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4-turbo-2024-04-09",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cluv2t5k3000508ih5kve9zag_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00001,
+ "output": 0.00003
+ }
+ }
+ ]
+ },
+ {
+ "id": "cluvpl4ls000008l6h2gx3i07",
+ "modelName": "gpt-4-turbo",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4-turbo)$",
+ "createdAt": "2024-04-11T21:13:44.989Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4-1106-preview",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cluvpl4ls000008l6h2gx3i07_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00001,
+ "output": 0.00003
+ }
+ }
+ ]
+ },
+ {
+ "id": "clv2o2x0p000008jsf9afceau",
+ "modelName": " gpt-4-preview",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4-preview)$",
+ "createdAt": "2024-04-23T10:37:17.092Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4-turbo-preview",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clv2o2x0p000008jsf9afceau_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00001,
+ "output": 0.00003
+ }
+ }
+ ]
+ },
+ {
+ "id": "clxt0n0m60000pumz1j5b7zsf",
+ "modelName": "claude-3-5-sonnet-20240620",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-3-5-sonnet-20240620|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20240620-v1:0|claude-3-5-sonnet@20240620)$",
+ "createdAt": "2024-06-25T11:47:24.475Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "clxt0n0m60000pumz1j5b7zsf_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "clyrjp56f0000t0mzapoocd7u",
+ "modelName": "gpt-4o-mini",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4o-mini)$",
+ "createdAt": "2024-07-18T17:56:09.591Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4o",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clyrjp56f0000t0mzapoocd7u_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1.5e-7,
+ "output": 6e-7,
+ "input_cached_tokens": 7.5e-8,
+ "input_cache_read": 7.5e-8
+ }
+ }
+ ]
+ },
+ {
+ "id": "clyrjpbe20000t0mzcbwc42rg",
+ "modelName": "gpt-4o-mini-2024-07-18",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4o-mini-2024-07-18)$",
+ "createdAt": "2024-07-18T17:56:09.591Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4o",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clyrjpbe20000t0mzcbwc42rg_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1.5e-7,
+ "input_cached_tokens": 7.5e-8,
+ "input_cache_read": 7.5e-8,
+ "output": 6e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "clzjr85f70000ymmzg7hqffra",
+ "modelName": "gpt-4o-2024-08-06",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4o-2024-08-06)$",
+ "createdAt": "2024-08-07T11:54:31.298Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4o",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "clzjr85f70000ymmzg7hqffra_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000025,
+ "input_cached_tokens": 0.00000125,
+ "input_cache_read": 0.00000125,
+ "output": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm10ivcdp0000gix7lelmbw80",
+ "modelName": "o1-preview",
+ "matchPattern": "(?i)^(openai\/)?(o1-preview)$",
+ "createdAt": "2024-09-13T10:01:35.373Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm10ivcdp0000gix7lelmbw80_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "input_cached_tokens": 0.0000075,
+ "input_cache_read": 0.0000075,
+ "output": 0.00006,
+ "output_reasoning_tokens": 0.00006,
+ "output_reasoning": 0.00006
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm10ivo130000n8x7qopcjjcg",
+ "modelName": "o1-preview-2024-09-12",
+ "matchPattern": "(?i)^(openai\/)?(o1-preview-2024-09-12)$",
+ "createdAt": "2024-09-13T10:01:35.373Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm10ivo130000n8x7qopcjjcg_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "input_cached_tokens": 0.0000075,
+ "input_cache_read": 0.0000075,
+ "output": 0.00006,
+ "output_reasoning_tokens": 0.00006,
+ "output_reasoning": 0.00006
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm10ivwo40000r1x7gg3syjq0",
+ "modelName": "o1-mini",
+ "matchPattern": "(?i)^(openai\/)?(o1-mini)$",
+ "createdAt": "2024-09-13T10:01:35.373Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm10ivwo40000r1x7gg3syjq0_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000011,
+ "input_cached_tokens": 5.5e-7,
+ "input_cache_read": 5.5e-7,
+ "output": 0.0000044,
+ "output_reasoning_tokens": 0.0000044,
+ "output_reasoning": 0.0000044
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm10iw6p20000wgx7it1hlb22",
+ "modelName": "o1-mini-2024-09-12",
+ "matchPattern": "(?i)^(openai\/)?(o1-mini-2024-09-12)$",
+ "createdAt": "2024-09-13T10:01:35.373Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm10iw6p20000wgx7it1hlb22_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000011,
+ "input_cached_tokens": 5.5e-7,
+ "input_cache_read": 5.5e-7,
+ "output": 0.0000044,
+ "output_reasoning_tokens": 0.0000044,
+ "output_reasoning": 0.0000044
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm2krz1uf000208jjg5653iud",
+ "modelName": "claude-3.5-sonnet-20241022",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-3-5-sonnet-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20241022-v2:0|claude-3-5-sonnet-V2@20241022)$",
+ "createdAt": "2024-10-22T18:48:01.676Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cm2krz1uf000208jjg5653iud_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm2ks2vzn000308jjh4ze1w7q",
+ "modelName": "claude-3.5-sonnet-latest",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-3-5-sonnet-latest)$",
+ "createdAt": "2024-10-22T18:48:01.676Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cm2ks2vzn000308jjh4ze1w7q_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm34aq60d000207ml0j1h31ar",
+ "modelName": "claude-3-5-haiku-20241022",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-3-5-haiku-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-haiku-20241022-v1:0|claude-3-5-haiku-V1@20241022)$",
+ "createdAt": "2024-11-05T10:30:50.566Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cm34aq60d000207ml0j1h31ar_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 8e-7,
+ "input_tokens": 8e-7,
+ "output": 0.000004,
+ "output_tokens": 0.000004,
+ "cache_creation_input_tokens": 0.000001,
+ "input_cache_creation": 0.000001,
+ "input_cache_creation_5m": 0.000001,
+ "input_cache_creation_1h": 0.0000016,
+ "cache_read_input_tokens": 8e-8,
+ "input_cache_read": 8e-8
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm34aqb9h000307ml6nypd618",
+ "modelName": "claude-3.5-haiku-latest",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-3-5-haiku-latest)$",
+ "createdAt": "2024-11-05T10:30:50.566Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cm34aqb9h000307ml6nypd618_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 8e-7,
+ "input_tokens": 8e-7,
+ "output": 0.000004,
+ "output_tokens": 0.000004,
+ "cache_creation_input_tokens": 0.000001,
+ "input_cache_creation": 0.000001,
+ "input_cache_creation_5m": 0.000001,
+ "input_cache_creation_1h": 0.0000016,
+ "cache_read_input_tokens": 8e-8,
+ "input_cache_read": 8e-8
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm3x0p8ev000008kyd96800c8",
+ "modelName": "chatgpt-4o-latest",
+ "matchPattern": "(?i)^(chatgpt-4o-latest)$",
+ "createdAt": "2024-11-25T12:47:17.504Z",
+ "updatedAt": "2024-11-25T12:47:17.504Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4o",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cm3x0p8ev000008kyd96800c8_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000005,
+ "output": 0.000015
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm48akqgo000008ldbia24qg0",
+ "modelName": "gpt-4o-2024-11-20",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4o-2024-11-20)$",
+ "createdAt": "2024-12-03T10:06:12.000Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4o",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cm48akqgo000008ldbia24qg0_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000025,
+ "input_cached_tokens": 0.00000125,
+ "input_cache_read": 0.00000125,
+ "output": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm48b2ksh000008l0hn3u0hl3",
+ "modelName": "gpt-4o-audio-preview",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4o-audio-preview)$",
+ "createdAt": "2024-12-03T10:19:56.000Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm48b2ksh000008l0hn3u0hl3_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input_text_tokens": 0.0000025,
+ "output_text_tokens": 0.00001,
+ "input_audio_tokens": 0.0001,
+ "input_audio": 0.0001,
+ "output_audio_tokens": 0.0002,
+ "output_audio": 0.0002
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm48bbm0k000008l69nsdakwf",
+ "modelName": "gpt-4o-audio-preview-2024-10-01",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4o-audio-preview-2024-10-01)$",
+ "createdAt": "2024-12-03T10:19:56.000Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm48bbm0k000008l69nsdakwf_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input_text_tokens": 0.0000025,
+ "output_text_tokens": 0.00001,
+ "input_audio_tokens": 0.0001,
+ "input_audio": 0.0001,
+ "output_audio_tokens": 0.0002,
+ "output_audio": 0.0002
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm48c2qh4000008mhgy4mg2qc",
+ "modelName": "gpt-4o-realtime-preview",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4o-realtime-preview)$",
+ "createdAt": "2024-12-03T10:19:56.000Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm48c2qh4000008mhgy4mg2qc_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input_text_tokens": 0.000005,
+ "input_cached_text_tokens": 0.0000025,
+ "output_text_tokens": 0.00002,
+ "input_audio_tokens": 0.0001,
+ "input_audio": 0.0001,
+ "input_cached_audio_tokens": 0.00002,
+ "output_audio_tokens": 0.0002,
+ "output_audio": 0.0002
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm48cjxtc000008jrcsso3avv",
+ "modelName": "gpt-4o-realtime-preview-2024-10-01",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4o-realtime-preview-2024-10-01)$",
+ "createdAt": "2024-12-03T10:19:56.000Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm48cjxtc000008jrcsso3avv_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input_text_tokens": 0.000005,
+ "input_cached_text_tokens": 0.0000025,
+ "output_text_tokens": 0.00002,
+ "input_audio_tokens": 0.0001,
+ "input_audio": 0.0001,
+ "input_cached_audio_tokens": 0.00002,
+ "output_audio_tokens": 0.0002,
+ "output_audio": 0.0002
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm48cjxtc000108jrcsso3avv",
+ "modelName": "o1",
+ "matchPattern": "(?i)^(openai\/)?(o1)$",
+ "createdAt": "2025-01-17T00:01:35.373Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm48cjxtc000108jrcsso3avv_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "input_cached_tokens": 0.0000075,
+ "input_cache_read": 0.0000075,
+ "output": 0.00006,
+ "output_reasoning_tokens": 0.00006,
+ "output_reasoning": 0.00006
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm48cjxtc000208jrcsso3avv",
+ "modelName": "o1-2024-12-17",
+ "matchPattern": "(?i)^(openai\/)?(o1-2024-12-17)$",
+ "createdAt": "2025-01-17T00:01:35.373Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm48cjxtc000208jrcsso3avv_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "input_cached_tokens": 0.0000075,
+ "input_cache_read": 0.0000075,
+ "output": 0.00006,
+ "output_reasoning_tokens": 0.00006,
+ "output_reasoning": 0.00006
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm6l8j7vs0000tymz9vk7ew8t",
+ "modelName": "o3-mini",
+ "matchPattern": "(?i)^(openai\/)?(o3-mini)$",
+ "createdAt": "2025-01-31T20:41:35.373Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm6l8j7vs0000tymz9vk7ew8t_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000011,
+ "input_cached_tokens": 5.5e-7,
+ "input_cache_read": 5.5e-7,
+ "output": 0.0000044,
+ "output_reasoning_tokens": 0.0000044,
+ "output_reasoning": 0.0000044
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm6l8jan90000tymz52sh0ql8",
+ "modelName": "o3-mini-2025-01-31",
+ "matchPattern": "(?i)^(openai\/)?(o3-mini-2025-01-31)$",
+ "createdAt": "2025-01-31T20:41:35.373Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm6l8jan90000tymz52sh0ql8_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000011,
+ "input_cached_tokens": 5.5e-7,
+ "input_cache_read": 5.5e-7,
+ "output": 0.0000044,
+ "output_reasoning_tokens": 0.0000044,
+ "output_reasoning": 0.0000044
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm6l8jdef0000tymz52sh0ql0",
+ "modelName": "gemini-2.0-flash-001",
+ "matchPattern": "(?i)^(google\/)?(gemini-2.0-flash-001)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2025-02-06T11:11:35.241Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm6l8jdef0000tymz52sh0ql0_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1e-7,
+ "output": 4e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm6l8jfgh0000tymz52sh0ql1",
+ "modelName": "gemini-2.0-flash-lite-preview-02-05",
+ "matchPattern": "(?i)^(google\/)?(gemini-2.0-flash-lite-preview-02-05)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2025-02-06T11:11:35.241Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm6l8jfgh0000tymz52sh0ql1_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 7.5e-8,
+ "output": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7ka7561000108js3t9tb3at",
+ "modelName": "claude-3.7-sonnet-20250219",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-3.7-sonnet-20250219|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3.7-sonnet-20250219-v1:0|claude-3-7-sonnet-V1@20250219)$",
+ "createdAt": "2025-02-25T09:35:39.000Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cm7ka7561000108js3t9tb3at_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7ka7zob000208jsfs9h5ajj",
+ "modelName": "claude-3.7-sonnet-latest",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-3-7-sonnet-latest)$",
+ "createdAt": "2025-02-25T09:35:39.000Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cm7ka7zob000208jsfs9h5ajj_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7nusjvk0000tvmz71o85jwg",
+ "modelName": "gpt-4.5-preview",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4.5-preview)$",
+ "createdAt": "2025-02-27T21:26:54.132Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm7nusjvk0000tvmz71o85jwg_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000075,
+ "input_cached_tokens": 0.0000375,
+ "input_cached_text_tokens": 0.0000375,
+ "input_cache_read": 0.0000375,
+ "output": 0.00015
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7nusn640000tvmzf10z2x65",
+ "modelName": "gpt-4.5-preview-2025-02-27",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4.5-preview-2025-02-27)$",
+ "createdAt": "2025-02-27T21:26:54.132Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm7nusn640000tvmzf10z2x65_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000075,
+ "input_cached_tokens": 0.0000375,
+ "input_cached_text_tokens": 0.0000375,
+ "input_cache_read": 0.0000375,
+ "output": 0.00015
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7nusn643377tvmzh27m33kl",
+ "modelName": "gpt-4.1",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4.1)$",
+ "createdAt": "2025-04-15T10:26:54.132Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cm7nusn643377tvmzh27m33kl_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000002,
+ "input_cached_tokens": 5e-7,
+ "input_cached_text_tokens": 5e-7,
+ "input_cache_read": 5e-7,
+ "output": 0.000008
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7qahw732891bpmzy45r3x70",
+ "modelName": "gpt-4.1-2025-04-14",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4.1-2025-04-14)$",
+ "createdAt": "2025-04-15T10:26:54.132Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cm7qahw732891bpmzy45r3x70_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000002,
+ "input_cached_tokens": 5e-7,
+ "input_cached_text_tokens": 5e-7,
+ "input_cache_read": 5e-7,
+ "output": 0.000008
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7sglt825463kxnza72p6v81",
+ "modelName": "gpt-4.1-mini-2025-04-14",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4.1-mini-2025-04-14)$",
+ "createdAt": "2025-04-15T10:26:54.132Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cm7sglt825463kxnza72p6v81_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 4e-7,
+ "input_cached_tokens": 1e-7,
+ "input_cached_text_tokens": 1e-7,
+ "input_cache_read": 1e-7,
+ "output": 0.0000016
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7vxpz967124dhjtb95w8f92",
+ "modelName": "gpt-4.1-nano-2025-04-14",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4.1-nano-2025-04-14)$",
+ "createdAt": "2025-04-15T10:26:54.132Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cm7vxpz967124dhjtb95w8f92_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1e-7,
+ "input_cached_tokens": 2.5e-8,
+ "input_cached_text_tokens": 2.5e-8,
+ "input_cache_read": 2.5e-8,
+ "output": 4e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7wmny967124dhjtb95w8f81",
+ "modelName": "o3",
+ "matchPattern": "(?i)^(openai\/)?(o3)$",
+ "createdAt": "2025-04-16T23:26:54.132Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm7wmny967124dhjtb95w8f81_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000002,
+ "input_cached_tokens": 5e-7,
+ "input_cache_read": 5e-7,
+ "output": 0.000008,
+ "output_reasoning_tokens": 0.000008,
+ "output_reasoning": 0.000008
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7wopq3327124dhjtb95w8f81",
+ "modelName": "o3-2025-04-16",
+ "matchPattern": "(?i)^(openai\/)?(o3-2025-04-16)$",
+ "createdAt": "2025-04-16T23:26:54.132Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm7wopq3327124dhjtb95w8f81_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000002,
+ "input_cached_tokens": 5e-7,
+ "input_cache_read": 5e-7,
+ "output": 0.000008,
+ "output_reasoning_tokens": 0.000008,
+ "output_reasoning": 0.000008
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7wqrs1327124dhjtb95w8f81",
+ "modelName": "o4-mini",
+ "matchPattern": "(?i)^(o4-mini)$",
+ "createdAt": "2025-04-16T23:26:54.132Z",
+ "updatedAt": "2025-04-16T23:26:54.132Z",
+ "pricingTiers": [
+ {
+ "id": "cm7wqrs1327124dhjtb95w8f81_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000011,
+ "input_cached_tokens": 2.75e-7,
+ "input_cache_read": 2.75e-7,
+ "output": 0.0000044,
+ "output_reasoning_tokens": 0.0000044,
+ "output_reasoning": 0.0000044
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7zqrs1327124dhjtb95w8f82",
+ "modelName": "o4-mini-2025-04-16",
+ "matchPattern": "(?i)^(o4-mini-2025-04-16)$",
+ "createdAt": "2025-04-16T23:26:54.132Z",
+ "updatedAt": "2025-04-16T23:26:54.132Z",
+ "pricingTiers": [
+ {
+ "id": "cm7zqrs1327124dhjtb95w8f82_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000011,
+ "input_cached_tokens": 2.75e-7,
+ "input_cache_read": 2.75e-7,
+ "output": 0.0000044,
+ "output_reasoning_tokens": 0.0000044,
+ "output_reasoning": 0.0000044
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7zsrs1327124dhjtb95w8f74",
+ "modelName": "gemini-2.0-flash",
+ "matchPattern": "(?i)^(google\/)?(gemini-2.0-flash)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2025-04-22T10:11:35.241Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm7zsrs1327124dhjtb95w8f74_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1e-7,
+ "output": 4e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7ztrs1327124dhjtb95w8f19",
+ "modelName": "gemini-2.0-flash-lite-preview",
+ "matchPattern": "(?i)^(google\/)?(gemini-2.0-flash-lite-preview)(@[a-zA-Z0-9]+)?$",
+ "createdAt": "2025-04-22T10:11:35.241Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cm7ztrs1327124dhjtb95w8f19_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 7.5e-8,
+ "output": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7zxrs1327124dhjtb95w8f45",
+ "modelName": "gpt-4.1-nano",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4.1-nano)$",
+ "createdAt": "2025-04-22T10:11:35.241Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cm7zxrs1327124dhjtb95w8f45_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1e-7,
+ "input_cached_tokens": 2.5e-8,
+ "input_cached_text_tokens": 2.5e-8,
+ "input_cache_read": 2.5e-8,
+ "output": 4e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cm7zzrs1327124dhjtb95w8p96",
+ "modelName": "gpt-4.1-mini",
+ "matchPattern": "(?i)^(openai\/)?(gpt-4.1-mini)$",
+ "createdAt": "2025-04-22T10:11:35.241Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cm7zzrs1327124dhjtb95w8p96_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 4e-7,
+ "input_cached_tokens": 1e-7,
+ "input_cached_text_tokens": 1e-7,
+ "input_cache_read": 1e-7,
+ "output": 0.0000016
+ }
+ }
+ ]
+ },
+ {
+ "id": "c5qmrqolku82tra3vgdixmys",
+ "modelName": "claude-sonnet-4-5-20250929",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-sonnet-4-5(-20250929)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-5(-20250929)?-v1(:0)?|claude-sonnet-4-5-V1(@20250929)?|claude-sonnet-4-5(@20250929)?)$",
+ "createdAt": "2025-09-29T00:00:00.000Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "c5qmrqolku82tra3vgdixmys_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ },
+ {
+ "id": "00b65240-047b-4722-9590-808edbc2067f",
+ "name": "Large Context",
+ "isDefault": false,
+ "priority": 1,
+ "conditions": [
+ {
+ "usageDetailPattern": "input",
+ "operator": "gt",
+ "value": 200000,
+ "caseSensitive": false
+ }
+ ],
+ "prices": {
+ "input": 0.000006,
+ "input_tokens": 0.000006,
+ "output": 0.0000225,
+ "output_tokens": 0.0000225,
+ "cache_creation_input_tokens": 0.0000075,
+ "input_cache_creation": 0.0000075,
+ "input_cache_creation_5m": 0.0000075,
+ "input_cache_creation_1h": 0.000012,
+ "cache_read_input_tokens": 6e-7,
+ "input_cache_read": 6e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmazmkzlm00000djp1e1qe4k4",
+ "modelName": "claude-sonnet-4-20250514",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-sonnet-4(-20250514)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4(-20250514)?-v1(:0)?|claude-sonnet-4-V1(@20250514)?|claude-sonnet-4(@20250514)?)$",
+ "createdAt": "2025-05-22T17:09:02.131Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cmazmkzlm00000djp1e1qe4k4_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmazmlbnv00010djpazed91va",
+ "modelName": "claude-sonnet-4-latest",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-sonnet-4-latest)$",
+ "createdAt": "2025-05-22T17:09:02.131Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cmazmlbnv00010djpazed91va_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmazmlm2p00020djpa9s64jw5",
+ "modelName": "claude-opus-4-20250514",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-opus-4(-20250514)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4(-20250514)?-v1(:0)?|claude-opus-4(@20250514)?)$",
+ "createdAt": "2025-05-22T17:09:02.131Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cmazmlm2p00020djpa9s64jw5_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "input_tokens": 0.000015,
+ "output": 0.000075,
+ "output_tokens": 0.000075,
+ "cache_creation_input_tokens": 0.00001875,
+ "input_cache_creation": 0.00001875,
+ "input_cache_creation_5m": 0.00001875,
+ "input_cache_creation_1h": 0.00003,
+ "cache_read_input_tokens": 0.0000015,
+ "input_cache_read": 0.0000015
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmz9x72kq55721pqrs83y4n2bx",
+ "modelName": "o3-pro",
+ "matchPattern": "(?i)^(openai\/)?(o3-pro)$",
+ "createdAt": "2025-06-10T22:26:54.132Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cmz9x72kq55721pqrs83y4n2bx_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00002,
+ "output": 0.00008,
+ "output_reasoning_tokens": 0.00008,
+ "output_reasoning": 0.00008
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmz9x72kq55721pqrs83y4n2by",
+ "modelName": "o3-pro-2025-06-10",
+ "matchPattern": "(?i)^(openai\/)?(o3-pro-2025-06-10)$",
+ "createdAt": "2025-06-10T22:26:54.132Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cmz9x72kq55721pqrs83y4n2by_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00002,
+ "output": 0.00008,
+ "output_reasoning_tokens": 0.00008,
+ "output_reasoning": 0.00008
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmbrold5b000107lbftb9fdoo",
+ "modelName": "o1-pro",
+ "matchPattern": "(?i)^(openai\/)?(o1-pro)$",
+ "createdAt": "2025-06-10T22:26:54.132Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cmbrold5b000107lbftb9fdoo_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00015,
+ "output": 0.0006,
+ "output_reasoning_tokens": 0.0006,
+ "output_reasoning": 0.0006
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmbrolpax000207lb3xkedysz",
+ "modelName": "o1-pro-2025-03-19",
+ "matchPattern": "(?i)^(openai\/)?(o1-pro-2025-03-19)$",
+ "createdAt": "2025-06-10T22:26:54.132Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cmbrolpax000207lb3xkedysz_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00015,
+ "output": 0.0006,
+ "output_reasoning_tokens": 0.0006,
+ "output_reasoning": 0.0006
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmcnjkfwn000107l43bf5e8ax",
+ "modelName": "gemini-2.5-flash",
+ "matchPattern": "(?i)^(google\/)?(gemini-2.5-flash)$",
+ "createdAt": "2025-07-03T13:44:06.964Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "pricingTiers": [
+ {
+ "id": "cmcnjkfwn000107l43bf5e8ax_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 3e-7,
+ "input_text": 3e-7,
+ "input_modality_1": 3e-7,
+ "prompt_token_count": 3e-7,
+ "promptTokenCount": 3e-7,
+ "input_cached_tokens": 3e-8,
+ "cached_content_token_count": 3e-8,
+ "output": 0.0000025,
+ "output_modality_1": 0.0000025,
+ "candidates_token_count": 0.0000025,
+ "candidatesTokenCount": 0.0000025,
+ "thoughtsTokenCount": 0.0000025,
+ "thoughts_token_count": 0.0000025,
+ "output_reasoning": 0.0000025,
+ "input_audio_tokens": 0.000001
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmcnjkrfa000207l4fpnh5mnv",
+ "modelName": "gemini-2.5-flash-lite",
+ "matchPattern": "(?i)^(google\/)?(gemini-2.5-flash-lite)$",
+ "createdAt": "2025-07-03T13:44:06.964Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "pricingTiers": [
+ {
+ "id": "cmcnjkrfa000207l4fpnh5mnv_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1e-7,
+ "input_text": 1e-7,
+ "input_modality_1": 1e-7,
+ "prompt_token_count": 1e-7,
+ "promptTokenCount": 1e-7,
+ "input_cached_tokens": 2.5e-8,
+ "cached_content_token_count": 2.5e-8,
+ "output": 4e-7,
+ "output_modality_1": 4e-7,
+ "candidates_token_count": 4e-7,
+ "candidatesTokenCount": 4e-7,
+ "thoughtsTokenCount": 4e-7,
+ "thoughts_token_count": 4e-7,
+ "output_reasoning": 4e-7,
+ "input_audio_tokens": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmdysde5w0000rkmzbc1g5au3",
+ "modelName": "claude-opus-4-1-20250805",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-opus-4-1(-20250805)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-1(-20250805)?-v1(:0)?|claude-opus-4-1(@20250805)?)$",
+ "createdAt": "2025-08-05T15:00:00.000Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cmdysde5w0000rkmzbc1g5au3_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "input_tokens": 0.000015,
+ "output": 0.000075,
+ "output_tokens": 0.000075,
+ "cache_creation_input_tokens": 0.00001875,
+ "input_cache_creation": 0.00001875,
+ "input_cache_creation_5m": 0.00001875,
+ "input_cache_creation_1h": 0.00003,
+ "cache_read_input_tokens": 0.0000015,
+ "input_cache_read": 0.0000015
+ }
+ }
+ ]
+ },
+ {
+ "id": "38c3822a-09a3-457b-b200-2c6f17f7cf2f",
+ "modelName": "gpt-5",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5)$",
+ "createdAt": "2025-08-07T16:00:00.000Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "38c3822a-09a3-457b-b200-2c6f17f7cf2f_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000125,
+ "input_cached_tokens": 1.25e-7,
+ "output": 0.00001,
+ "input_cache_read": 1.25e-7,
+ "output_reasoning_tokens": 0.00001,
+ "output_reasoning": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "id": "12543803-2d5f-4189-addc-821ad71c8b55",
+ "modelName": "gpt-5-2025-08-07",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5-2025-08-07)$",
+ "createdAt": "2025-08-11T08:00:00.000Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "12543803-2d5f-4189-addc-821ad71c8b55_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000125,
+ "input_cached_tokens": 1.25e-7,
+ "output": 0.00001,
+ "input_cache_read": 1.25e-7,
+ "output_reasoning_tokens": 0.00001,
+ "output_reasoning": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "id": "3d6a975a-a42d-4ea2-a3ec-4ae567d5a364",
+ "modelName": "gpt-5-mini",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5-mini)$",
+ "createdAt": "2025-08-07T16:00:00.000Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "3d6a975a-a42d-4ea2-a3ec-4ae567d5a364_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "input_cached_tokens": 2.5e-8,
+ "output": 0.000002,
+ "input_cache_read": 2.5e-8,
+ "output_reasoning_tokens": 0.000002,
+ "output_reasoning": 0.000002
+ }
+ }
+ ]
+ },
+ {
+ "id": "03b83894-7172-4e1e-8e8b-37d792484efd",
+ "modelName": "gpt-5-mini-2025-08-07",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5-mini-2025-08-07)$",
+ "createdAt": "2025-08-11T08:00:00.000Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "03b83894-7172-4e1e-8e8b-37d792484efd_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "input_cached_tokens": 2.5e-8,
+ "output": 0.000002,
+ "input_cache_read": 2.5e-8,
+ "output_reasoning_tokens": 0.000002,
+ "output_reasoning": 0.000002
+ }
+ }
+ ]
+ },
+ {
+ "id": "f0b40234-b694-4c40-9494-7b0efd860fb9",
+ "modelName": "gpt-5-nano",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5-nano)$",
+ "createdAt": "2025-08-07T16:00:00.000Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "f0b40234-b694-4c40-9494-7b0efd860fb9_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 5e-8,
+ "input_cached_tokens": 5e-9,
+ "output": 4e-7,
+ "input_cache_read": 5e-9,
+ "output_reasoning_tokens": 4e-7,
+ "output_reasoning": 4e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "4489fde4-a594-4011-948b-526989300cd3",
+ "modelName": "gpt-5-nano-2025-08-07",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5-nano-2025-08-07)$",
+ "createdAt": "2025-08-11T08:00:00.000Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "4489fde4-a594-4011-948b-526989300cd3_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 5e-8,
+ "input_cached_tokens": 5e-9,
+ "output": 4e-7,
+ "input_cache_read": 5e-9,
+ "output_reasoning_tokens": 4e-7,
+ "output_reasoning": 4e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "8ba72ee3-ebe8-4110-a614-bf81094447e5",
+ "modelName": "gpt-5-chat-latest",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5-chat-latest)$",
+ "createdAt": "2025-08-07T16:00:00.000Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "8ba72ee3-ebe8-4110-a614-bf81094447e5_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000125,
+ "input_cached_tokens": 1.25e-7,
+ "output": 0.00001,
+ "input_cache_read": 1.25e-7,
+ "output_reasoning_tokens": 0.00001,
+ "output_reasoning": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmgg9zco3000004l258um9xk8",
+ "modelName": "gpt-5-pro",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5-pro)$",
+ "createdAt": "2025-10-07T08:03:54.727Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cmgg9zco3000004l258um9xk8_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "output": 0.00012,
+ "output_reasoning_tokens": 0.00012,
+ "output_reasoning": 0.00012
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmgga0vh9000104l22qe4fes4",
+ "modelName": "gpt-5-pro-2025-10-06",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5-pro-2025-10-06)$",
+ "createdAt": "2025-10-07T08:03:54.727Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cmgga0vh9000104l22qe4fes4_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "output": 0.00012,
+ "output_reasoning_tokens": 0.00012,
+ "output_reasoning": 0.00012
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmgt5gnkv000104jx171tbq4e",
+ "modelName": "claude-haiku-4-5-20251001",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-haiku-4-5-20251001|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-haiku-4-5-20251001-v1:0|claude-4-5-haiku@20251001)$",
+ "createdAt": "2025-10-16T08:20:44.558Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cmgt5gnkv000104jx171tbq4e_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000001,
+ "input_tokens": 0.000001,
+ "output": 0.000005,
+ "output_tokens": 0.000005,
+ "cache_creation_input_tokens": 0.00000125,
+ "input_cache_creation": 0.00000125,
+ "input_cache_creation_5m": 0.00000125,
+ "input_cache_creation_1h": 0.000002,
+ "cache_read_input_tokens": 1e-7,
+ "input_cache_read": 1e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmhymgpym000d04ih34rndvhr",
+ "modelName": "gpt-5.1",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5.1)$",
+ "createdAt": "2025-11-14T08:57:23.481Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cmhymgpym000d04ih34rndvhr_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000125,
+ "input_cached_tokens": 1.25e-7,
+ "output": 0.00001,
+ "input_cache_read": 1.25e-7,
+ "output_reasoning_tokens": 0.00001,
+ "output_reasoning": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmhymgxiw000e04ihh9pw12ef",
+ "modelName": "gpt-5.1-2025-11-13",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5.1-2025-11-13)$",
+ "createdAt": "2025-11-14T08:57:23.481Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cmhymgxiw000e04ihh9pw12ef_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000125,
+ "input_cached_tokens": 1.25e-7,
+ "output": 0.00001,
+ "input_cache_read": 1.25e-7,
+ "output_reasoning_tokens": 0.00001,
+ "output_reasoning": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmieupdva000004l541kwae70",
+ "modelName": "claude-opus-4-5-20251101",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-opus-4-5(-20251101)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-5(-20251101)?-v1(:0)?|claude-opus-4-5(@20251101)?)$",
+ "createdAt": "2025-11-24T20:53:27.571Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerConfig": null,
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "cmieupdva000004l541kwae70_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 5e-6,
+ "input_tokens": 5e-6,
+ "output": 25e-6,
+ "output_tokens": 25e-6,
+ "cache_creation_input_tokens": 6.25e-6,
+ "input_cache_creation": 6.25e-6,
+ "input_cache_creation_5m": 6.25e-6,
+ "input_cache_creation_1h": 10e-6,
+ "cache_read_input_tokens": 0.5e-6,
+ "input_cache_read": 0.5e-6
+ }
+ }
+ ]
+ },
+ {
+ "id": "90ec5ec3-1a48-4ff0-919c-70cdb8f632ed",
+ "modelName": "claude-sonnet-4-6",
+ "matchPattern": "(?i)^(anthropic\\/)?(claude-sonnet-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-6(-v1(:0)?)?|claude-sonnet-4-6)$",
+ "createdAt": "2026-02-18T00:00:00.000Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerConfig": null,
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "90ec5ec3-1a48-4ff0-919c-70cdb8f632ed_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 3e-6,
+ "input_tokens": 3e-6,
+ "output": 15e-6,
+ "output_tokens": 15e-6,
+ "cache_creation_input_tokens": 3.75e-6,
+ "input_cache_creation": 3.75e-6,
+ "input_cache_creation_5m": 3.75e-6,
+ "input_cache_creation_1h": 6e-6,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ },
+ {
+ "id": "7830bfc2-c464-4ffe-b9a2-6e741f6c5486",
+ "name": "Large Context",
+ "isDefault": false,
+ "priority": 1,
+ "conditions": [
+ {
+ "usageDetailPattern": "input",
+ "operator": "gt",
+ "value": 200000,
+ "caseSensitive": false
+ }
+ ],
+ "prices": {
+ "input": 6e-6,
+ "input_tokens": 6e-6,
+ "output": 22.5e-6,
+ "output_tokens": 22.5e-6,
+ "cache_creation_input_tokens": 7.5e-6,
+ "input_cache_creation": 7.5e-6,
+ "input_cache_creation_5m": 7.5e-6,
+ "input_cache_creation_1h": 12e-6,
+ "cache_read_input_tokens": 6e-7,
+ "input_cache_read": 6e-7
+ }
+ }
+ ]
+ },
+ {
+ "id": "13458bc0-1c20-44c2-8753-172f54b67647",
+ "modelName": "claude-opus-4-6",
+ "matchPattern": "(?i)^(anthropic\/)?(claude-opus-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-6-v1(:0)?|claude-opus-4-6)$",
+ "createdAt": "2026-02-09T00:00:00.000Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "tokenizerConfig": null,
+ "tokenizerId": "claude",
+ "pricingTiers": [
+ {
+ "id": "13458bc0-1c20-44c2-8753-172f54b67647_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 5e-6,
+ "input_tokens": 5e-6,
+ "output": 25e-6,
+ "output_tokens": 25e-6,
+ "cache_creation_input_tokens": 6.25e-6,
+ "input_cache_creation": 6.25e-6,
+ "input_cache_creation_5m": 6.25e-6,
+ "input_cache_creation_1h": 10e-6,
+ "cache_read_input_tokens": 0.5e-6,
+ "input_cache_read": 0.5e-6
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmig1hb7i000104l72qrzgc6h",
+ "modelName": "gemini-2.5-pro",
+ "matchPattern": "(?i)^(google\/)?(gemini-2.5-pro)$",
+ "createdAt": "2025-11-26T13:27:53.545Z",
+ "updatedAt": "2026-03-04T00:00:00.000Z",
+ "pricingTiers": [
+ {
+ "id": "cmig1hb7i000104l72qrzgc6h_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1.25e-6,
+ "input_text": 1.25e-6,
+ "input_modality_1": 1.25e-6,
+ "prompt_token_count": 1.25e-6,
+ "promptTokenCount": 1.25e-6,
+ "input_cached_tokens": 0.125e-6,
+ "cached_content_token_count": 0.125e-6,
+ "output": 10e-6,
+ "output_modality_1": 10e-6,
+ "candidates_token_count": 10e-6,
+ "candidatesTokenCount": 10e-6,
+ "thoughtsTokenCount": 10e-6,
+ "thoughts_token_count": 10e-6,
+ "output_reasoning": 10e-6
+ }
+ },
+ {
+ "id": "bcf39e8f-9969-455f-be9a-541a00256092",
+ "name": "Large Context",
+ "isDefault": false,
+ "priority": 1,
+ "conditions": [
+ {
+ "usageDetailPattern": "(input|prompt|cached)",
+ "operator": "gt",
+ "value": 200000,
+ "caseSensitive": false
+ }
+ ],
+ "prices": {
+ "input": 2.5e-6,
+ "input_text": 2.5e-6,
+ "input_modality_1": 2.5e-6,
+ "prompt_token_count": 2.5e-6,
+ "promptTokenCount": 2.5e-6,
+ "input_cached_tokens": 0.25e-6,
+ "cached_content_token_count": 0.25e-6,
+ "output": 15e-6,
+ "output_modality_1": 15e-6,
+ "candidates_token_count": 15e-6,
+ "candidatesTokenCount": 15e-6,
+ "thoughtsTokenCount": 15e-6,
+ "thoughts_token_count": 15e-6,
+ "output_reasoning": 15e-6
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmig1wmep000404l7fh6q5uog",
+ "modelName": "gemini-3-pro-preview",
+ "matchPattern": "(?i)^(google\/)?(gemini-3-pro-preview)$",
+ "createdAt": "2025-11-26T13:27:53.545Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "pricingTiers": [
+ {
+ "id": "cmig1wmep000404l7fh6q5uog_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2e-6,
+ "input_modality_1": 2e-6,
+ "prompt_token_count": 2e-6,
+ "promptTokenCount": 2e-6,
+ "input_cached_tokens": 0.2e-6,
+ "cached_content_token_count": 0.2e-6,
+ "output": 12e-6,
+ "output_modality_1": 12e-6,
+ "candidates_token_count": 12e-6,
+ "candidatesTokenCount": 12e-6,
+ "thoughtsTokenCount": 12e-6,
+ "thoughts_token_count": 12e-6,
+ "output_reasoning": 12e-6
+ }
+ },
+ {
+ "id": "4da930c8-7146-4e27-b66c-b62f2c2ec357",
+ "name": "Large Context",
+ "isDefault": false,
+ "priority": 1,
+ "conditions": [
+ {
+ "usageDetailPattern": "(input|prompt|cached)",
+ "operator": "gt",
+ "value": 200000,
+ "caseSensitive": false
+ }
+ ],
+ "prices": {
+ "input": 4e-6,
+ "input_modality_1": 4e-6,
+ "prompt_token_count": 4e-6,
+ "promptTokenCount": 4e-6,
+ "input_cached_tokens": 0.4e-6,
+ "cached_content_token_count": 0.4e-6,
+ "output": 18e-6,
+ "output_modality_1": 18e-6,
+ "candidates_token_count": 18e-6,
+ "candidatesTokenCount": 18e-6,
+ "thoughtsTokenCount": 18e-6,
+ "thoughts_token_count": 18e-6,
+ "output_reasoning": 18e-6
+ }
+ }
+ ]
+ },
+ {
+ "id": "55106bba-a5dd-441b-bc0d-5652582b349d",
+ "modelName": "gemini-3.1-pro-preview",
+ "matchPattern": "(?i)^(google\/)?(gemini-3.1-pro-preview(-customtools)?)$",
+ "createdAt": "2026-02-19T00:00:00.000Z",
+ "updatedAt": "2026-02-19T00:00:00.000Z",
+ "pricingTiers": [
+ {
+ "id": "55106bba-a5dd-441b-bc0d-5652582b349d_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2e-6,
+ "input_modality_1": 2e-6,
+ "prompt_token_count": 2e-6,
+ "promptTokenCount": 2e-6,
+ "input_cached_tokens": 0.2e-6,
+ "cached_content_token_count": 0.2e-6,
+ "output": 12e-6,
+ "output_modality_1": 12e-6,
+ "candidates_token_count": 12e-6,
+ "candidatesTokenCount": 12e-6,
+ "thoughtsTokenCount": 12e-6,
+ "thoughts_token_count": 12e-6,
+ "output_reasoning": 12e-6
+ }
+ },
+ {
+ "id": "ada11e9f-fe0d-465a-92af-ce334d0eedeb",
+ "name": "Large Context",
+ "isDefault": false,
+ "priority": 1,
+ "conditions": [
+ {
+ "usageDetailPattern": "(input|prompt|cached)",
+ "operator": "gt",
+ "value": 200000,
+ "caseSensitive": false
+ }
+ ],
+ "prices": {
+ "input": 4e-6,
+ "input_modality_1": 4e-6,
+ "prompt_token_count": 4e-6,
+ "promptTokenCount": 4e-6,
+ "input_cached_tokens": 0.4e-6,
+ "cached_content_token_count": 0.4e-6,
+ "output": 18e-6,
+ "output_modality_1": 18e-6,
+ "candidates_token_count": 18e-6,
+ "candidatesTokenCount": 18e-6,
+ "thoughtsTokenCount": 18e-6,
+ "thoughts_token_count": 18e-6,
+ "output_reasoning": 18e-6
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmj2n4f2a000304kz49g4c43u",
+ "modelName": "gpt-5.2",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5.2)$",
+ "createdAt": "2025-12-12T09:00:06.513Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cmj2n4f2a000304kz49g4c43u_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1.75e-6,
+ "input_cached_tokens": 0.175e-6,
+ "input_cache_read": 0.175e-6,
+ "output": 14e-6,
+ "output_reasoning_tokens": 14e-6,
+ "output_reasoning": 14e-6
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmj2muxg6000104kzd2tc8953",
+ "modelName": "gpt-5.2-2025-12-11",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5.2-2025-12-11)$",
+ "createdAt": "2025-12-12T09:00:06.513Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cmj2muxg6000104kzd2tc8953_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1.75e-6,
+ "input_cached_tokens": 0.175e-6,
+ "input_cache_read": 0.175e-6,
+ "output": 14e-6,
+ "output_reasoning_tokens": 14e-6,
+ "output_reasoning": 14e-6
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmj2n6pkq000404kz2s0b6if7",
+ "modelName": "gpt-5.2-pro",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5.2-pro)$",
+ "createdAt": "2025-12-12T09:00:06.513Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cmj2n6pkq000404kz2s0b6if7_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 21e-6,
+ "output": 168e-6,
+ "output_reasoning_tokens": 168e-6,
+ "output_reasoning": 168e-6
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmj2n70oe000504kz21b76mes",
+ "modelName": "gpt-5.2-pro-2025-12-11",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5.2-pro-2025-12-11)$",
+ "createdAt": "2025-12-12T09:00:06.513Z",
+ "updatedAt": "2025-12-12T15:00:06.513Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "cmj2n70oe000504kz21b76mes_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 21e-6,
+ "output": 168e-6,
+ "output_reasoning_tokens": 168e-6,
+ "output_reasoning": 168e-6
+ }
+ }
+ ]
+ },
+ {
+ "id": "bee3c111-fe6f-4641-8775-73ea33b29fca",
+ "modelName": "gpt-5.4",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5.4)$",
+ "createdAt": "2026-03-05T00:00:00.000Z",
+ "updatedAt": "2026-03-05T00:00:00.000Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "bee3c111-fe6f-4641-8775-73ea33b29fca_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-6,
+ "input_cached_tokens": 0.25e-6,
+ "input_cache_read": 0.25e-6,
+ "output": 15e-6,
+ "output_reasoning_tokens": 15e-6,
+ "output_reasoning": 15e-6
+ }
+ }
+ ]
+ },
+ {
+ "id": "68d32054-8748-4d25-9f64-d78d483601bd",
+ "modelName": "gpt-5.4-pro",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5.4-pro)$",
+ "createdAt": "2026-03-05T00:00:00.000Z",
+ "updatedAt": "2026-03-05T00:00:00.000Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "68d32054-8748-4d25-9f64-d78d483601bd_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 30e-6,
+ "output": 180e-6,
+ "output_reasoning_tokens": 180e-6,
+ "output_reasoning": 180e-6
+ }
+ }
+ ]
+ },
+ {
+ "id": "22dfc7e1-1fe1-4286-b1af-928635e7ecb9",
+ "modelName": "gpt-5.4-2026-03-05",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5.4-2026-03-05)$",
+ "createdAt": "2026-03-05T00:00:00.000Z",
+ "updatedAt": "2026-03-05T00:00:00.000Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "22dfc7e1-1fe1-4286-b1af-928635e7ecb9_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-6,
+ "input_cached_tokens": 0.25e-6,
+ "input_cache_read": 0.25e-6,
+ "output": 15e-6,
+ "output_reasoning_tokens": 15e-6,
+ "output_reasoning": 15e-6
+ }
+ }
+ ]
+ },
+ {
+ "id": "d8873413-05ab-4374-8223-e8c9005c4a0e",
+ "modelName": "gpt-5.4-pro-2026-03-05",
+ "matchPattern": "(?i)^(openai\/)?(gpt-5.4-pro-2026-03-05)$",
+ "createdAt": "2026-03-05T00:00:00.000Z",
+ "updatedAt": "2026-03-05T00:00:00.000Z",
+ "tokenizerConfig": {
+ "tokensPerName": 1,
+ "tokenizerModel": "gpt-4",
+ "tokensPerMessage": 3
+ },
+ "tokenizerId": "openai",
+ "pricingTiers": [
+ {
+ "id": "d8873413-05ab-4374-8223-e8c9005c4a0e_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 30e-6,
+ "output": 180e-6,
+ "output_reasoning_tokens": 180e-6,
+ "output_reasoning": 180e-6
+ }
+ }
+ ]
+ },
+ {
+ "id": "cmjfoeykl000004l8ffzra8c7",
+ "modelName": "gemini-3-flash-preview",
+ "matchPattern": "(?i)^(google\/)?(gemini-3-flash-preview)$",
+ "createdAt": "2025-12-21T12:01:42.282Z",
+ "updatedAt": "2025-12-21T12:01:42.282Z",
+ "pricingTiers": [
+ {
+ "id": "cmjfoeykl000004l8ffzra8c7_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.5e-6,
+ "input_modality_1": 0.5e-6,
+ "prompt_token_count": 0.5e-6,
+ "promptTokenCount": 0.5e-6,
+ "input_cached_tokens": 0.05e-6,
+ "cached_content_token_count": 0.05e-6,
+ "output": 3e-6,
+ "output_modality_1": 3e-6,
+ "candidates_token_count": 3e-6,
+ "candidatesTokenCount": 3e-6,
+ "thoughtsTokenCount": 3e-6,
+ "thoughts_token_count": 3e-6,
+ "output_reasoning": 3e-6
+ }
+ }
+ ]
+ },
+ {
+ "id": "0707bef8-10c8-46e2-a871-0436f05f0b92",
+ "modelName": "gemini-3.1-flash-lite-preview",
+ "matchPattern": "(?i)^(google\/)?(gemini-3.1-flash-lite-preview)$",
+ "createdAt": "2026-03-03T00:00:00.000Z",
+ "updatedAt": "2026-03-03T00:00:00.000Z",
+ "pricingTiers": [
+ {
+ "id": "0707bef8-10c8-46e2-a871-0436f05f0b92_tier_default",
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.25e-6,
+ "input_modality_1": 0.25e-6,
+ "prompt_token_count": 0.25e-6,
+ "promptTokenCount": 0.25e-6,
+ "input_cached_tokens": 0.025e-6,
+ "cached_content_token_count": 0.025e-6,
+ "output": 1.5e-6,
+ "output_modality_1": 1.5e-6,
+ "candidates_token_count": 1.5e-6,
+ "candidatesTokenCount": 1.5e-6,
+ "thoughtsTokenCount": 1.5e-6,
+ "thoughts_token_count": 1.5e-6,
+ "output_reasoning": 1.5e-6,
+ "input_audio_tokens": 0.5e-6
+ }
+ }
+ ]
+ }
+]
diff --git a/internal-packages/llm-pricing/src/defaultPrices.ts b/internal-packages/llm-pricing/src/defaultPrices.ts
new file mode 100644
index 00000000000..689944a6432
--- /dev/null
+++ b/internal-packages/llm-pricing/src/defaultPrices.ts
@@ -0,0 +1,2984 @@
+import type { DefaultModelDefinition } from "./types.js";
+
+// Auto-generated from Langfuse default-model-prices.json — do not edit manually.
+// Run `pnpm run sync-prices` to update from upstream.
+// Source: https://github.com/langfuse/langfuse
+
+export const defaultModelPrices: DefaultModelDefinition[] = [
+ {
+ "modelName": "gpt-4o",
+ "matchPattern": "(?i)^(openai/)?(gpt-4o)$",
+ "startDate": "2024-05-13T23:15:07.670Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000025,
+ "input_cached_tokens": 0.00000125,
+ "input_cache_read": 0.00000125,
+ "output": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4o-2024-05-13",
+ "matchPattern": "(?i)^(openai/)?(gpt-4o-2024-05-13)$",
+ "startDate": "2024-05-13T23:15:07.670Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000005,
+ "output": 0.000015
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4-1106-preview",
+ "matchPattern": "(?i)^(openai/)?(gpt-4-1106-preview)$",
+ "startDate": "2024-04-23T10:37:17.092Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00001,
+ "output": 0.00003
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4-turbo-vision",
+ "matchPattern": "(?i)^(openai/)?(gpt-4(-\\d{4})?-vision-preview)$",
+ "startDate": "2024-01-24T10:19:21.693Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00001,
+ "output": 0.00003
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4-32k",
+ "matchPattern": "(?i)^(openai/)?(gpt-4-32k)$",
+ "startDate": "2024-01-24T10:19:21.693Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00006,
+ "output": 0.00012
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4-32k-0613",
+ "matchPattern": "(?i)^(openai/)?(gpt-4-32k-0613)$",
+ "startDate": "2024-01-24T10:19:21.693Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00006,
+ "output": 0.00012
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-3.5-turbo-1106",
+ "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-1106)$",
+ "startDate": "2024-01-24T10:19:21.693Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000001,
+ "output": 0.000002
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-3.5-turbo-0613",
+ "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0613)$",
+ "startDate": "2024-01-24T10:19:21.693Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000015,
+ "output": 0.000002
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4-0613",
+ "matchPattern": "(?i)^(openai/)?(gpt-4-0613)$",
+ "startDate": "2024-01-24T10:19:21.693Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00003,
+ "output": 0.00006
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-3.5-turbo-instruct",
+ "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-instruct)$",
+ "startDate": "2024-01-24T10:19:21.693Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000015,
+ "output": 0.000002
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "text-ada-001",
+ "matchPattern": "(?i)^(text-ada-001)$",
+ "startDate": "2024-01-24T18:18:50.861Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 0.000004
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "text-babbage-001",
+ "matchPattern": "(?i)^(text-babbage-001)$",
+ "startDate": "2024-01-24T18:18:50.861Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "text-curie-001",
+ "matchPattern": "(?i)^(text-curie-001)$",
+ "startDate": "2024-01-24T18:18:50.861Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 0.00002
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "text-davinci-001",
+ "matchPattern": "(?i)^(text-davinci-001)$",
+ "startDate": "2024-01-24T18:18:50.861Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 0.00002
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "text-davinci-002",
+ "matchPattern": "(?i)^(text-davinci-002)$",
+ "startDate": "2024-01-24T18:18:50.861Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 0.00002
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "text-davinci-003",
+ "matchPattern": "(?i)^(text-davinci-003)$",
+ "startDate": "2024-01-24T18:18:50.861Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 0.00002
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "text-embedding-ada-002-v2",
+ "matchPattern": "(?i)^(text-embedding-ada-002-v2)$",
+ "startDate": "2024-01-24T18:18:50.861Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 1e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "text-embedding-ada-002",
+ "matchPattern": "(?i)^(text-embedding-ada-002)$",
+ "startDate": "2024-01-24T18:18:50.861Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 1e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-3.5-turbo-16k-0613",
+ "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-16k-0613)$",
+ "startDate": "2024-02-03T17:29:57.350Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "output": 0.000004
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-3.5-turbo-0301",
+ "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0301)$",
+ "startDate": "2024-01-24T10:19:21.693Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000002,
+ "output": 0.000002
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4-32k-0314",
+ "matchPattern": "(?i)^(openai/)?(gpt-4-32k-0314)$",
+ "startDate": "2024-01-24T10:19:21.693Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00006,
+ "output": 0.00012
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4-0314",
+ "matchPattern": "(?i)^(openai/)?(gpt-4-0314)$",
+ "startDate": "2024-01-24T10:19:21.693Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00003,
+ "output": 0.00006
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4",
+ "matchPattern": "(?i)^(openai/)?(gpt-4)$",
+ "startDate": "2024-01-24T10:19:21.693Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00003,
+ "output": 0.00006
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-instant-1.2",
+ "matchPattern": "(?i)^(anthropic/)?(claude-instant-1.2)$",
+ "startDate": "2024-01-30T15:44:13.447Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000163,
+ "output": 0.00000551
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-2.0",
+ "matchPattern": "(?i)^(anthropic/)?(claude-2.0)$",
+ "startDate": "2024-01-30T15:44:13.447Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000008,
+ "output": 0.000024
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-2.1",
+ "matchPattern": "(?i)^(anthropic/)?(claude-2.1)$",
+ "startDate": "2024-01-30T15:44:13.447Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000008,
+ "output": 0.000024
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-1.3",
+ "matchPattern": "(?i)^(anthropic/)?(claude-1.3)$",
+ "startDate": "2024-01-30T15:44:13.447Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000008,
+ "output": 0.000024
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-1.2",
+ "matchPattern": "(?i)^(anthropic/)?(claude-1.2)$",
+ "startDate": "2024-01-30T15:44:13.447Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000008,
+ "output": 0.000024
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-1.1",
+ "matchPattern": "(?i)^(anthropic/)?(claude-1.1)$",
+ "startDate": "2024-01-30T15:44:13.447Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000008,
+ "output": 0.000024
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-instant-1",
+ "matchPattern": "(?i)^(anthropic/)?(claude-instant-1)$",
+ "startDate": "2024-01-30T15:44:13.447Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000163,
+ "output": 0.00000551
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "babbage-002",
+ "matchPattern": "(?i)^(babbage-002)$",
+ "startDate": "2024-01-26T17:35:21.129Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 4e-7,
+ "output": 0.0000016
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "davinci-002",
+ "matchPattern": "(?i)^(davinci-002)$",
+ "startDate": "2024-01-26T17:35:21.129Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000006,
+ "output": 0.000012
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "text-embedding-3-small",
+ "matchPattern": "(?i)^(text-embedding-3-small)$",
+ "startDate": "2024-01-26T17:35:21.129Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 2e-8
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "text-embedding-3-large",
+ "matchPattern": "(?i)^(text-embedding-3-large)$",
+ "startDate": "2024-01-26T17:35:21.129Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 1.3e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-3.5-turbo-0125",
+ "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0125)$",
+ "startDate": "2024-01-26T17:35:21.129Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 5e-7,
+ "output": 0.0000015
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-3.5-turbo",
+ "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo)$",
+ "startDate": "2024-02-13T12:00:37.424Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 5e-7,
+ "output": 0.0000015
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4-0125-preview",
+ "matchPattern": "(?i)^(openai/)?(gpt-4-0125-preview)$",
+ "startDate": "2024-01-26T17:35:21.129Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00001,
+ "output": 0.00003
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "ft:gpt-3.5-turbo-1106",
+ "matchPattern": "(?i)^(ft:)(gpt-3.5-turbo-1106:)(.+)(:)(.*)(:)(.+)$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "output": 0.000006
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "ft:gpt-3.5-turbo-0613",
+ "matchPattern": "(?i)^(ft:)(gpt-3.5-turbo-0613:)(.+)(:)(.*)(:)(.+)$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000012,
+ "output": 0.000016
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "ft:davinci-002",
+ "matchPattern": "(?i)^(ft:)(davinci-002:)(.+)(:)(.*)(:)(.+)$$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000012,
+ "output": 0.000012
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "ft:babbage-002",
+ "matchPattern": "(?i)^(ft:)(babbage-002:)(.+)(:)(.*)(:)(.+)$$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000016,
+ "output": 0.0000016
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "chat-bison",
+ "matchPattern": "(?i)^(chat-bison)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "codechat-bison-32k",
+ "matchPattern": "(?i)^(codechat-bison-32k)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "codechat-bison",
+ "matchPattern": "(?i)^(codechat-bison)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "text-bison-32k",
+ "matchPattern": "(?i)^(text-bison-32k)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "chat-bison-32k",
+ "matchPattern": "(?i)^(chat-bison-32k)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "text-unicorn",
+ "matchPattern": "(?i)^(text-unicorn)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000025,
+ "output": 0.0000075
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "text-bison",
+ "matchPattern": "(?i)^(text-bison)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "textembedding-gecko",
+ "matchPattern": "(?i)^(textembedding-gecko)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 1e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "textembedding-gecko-multilingual",
+ "matchPattern": "(?i)^(textembedding-gecko-multilingual)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "total": 1e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "code-gecko",
+ "matchPattern": "(?i)^(code-gecko)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "code-bison",
+ "matchPattern": "(?i)^(code-bison)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "code-bison-32k",
+ "matchPattern": "(?i)^(code-bison-32k)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-01-31T13:25:02.141Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-3.5-turbo-16k",
+ "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-16k)$",
+ "startDate": "2024-02-13T12:00:37.424Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 5e-7,
+ "output": 0.0000015
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4-turbo-preview",
+ "matchPattern": "(?i)^(openai/)?(gpt-4-turbo-preview)$",
+ "startDate": "2024-02-15T21:21:50.947Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00001,
+ "output": 0.00003
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-3-opus-20240229",
+ "matchPattern": "(?i)^(anthropic/)?(claude-3-opus-20240229|anthropic\\.claude-3-opus-20240229-v1:0|claude-3-opus@20240229)$",
+ "startDate": "2024-03-07T17:55:38.139Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "output": 0.000075
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-3-sonnet-20240229",
+ "matchPattern": "(?i)^(anthropic/)?(claude-3-sonnet-20240229|anthropic\\.claude-3-sonnet-20240229-v1:0|claude-3-sonnet@20240229)$",
+ "startDate": "2024-03-07T17:55:38.139Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-3-haiku-20240307",
+ "matchPattern": "(?i)^(anthropic/)?(claude-3-haiku-20240307|anthropic\\.claude-3-haiku-20240307-v1:0|claude-3-haiku@20240307)$",
+ "startDate": "2024-03-14T09:41:18.736Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 0.00000125
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-1.0-pro-latest",
+ "matchPattern": "(?i)^(google/)?(gemini-1.0-pro-latest)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-04-11T10:27:46.517Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "output": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-1.0-pro",
+ "matchPattern": "(?i)^(google/)?(gemini-1.0-pro)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-04-11T10:27:46.517Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1.25e-7,
+ "output": 3.75e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-1.0-pro-001",
+ "matchPattern": "(?i)^(google/)?(gemini-1.0-pro-001)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-04-11T10:27:46.517Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1.25e-7,
+ "output": 3.75e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-pro",
+ "matchPattern": "(?i)^(google/)?(gemini-pro)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-04-11T10:27:46.517Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1.25e-7,
+ "output": 3.75e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-1.5-pro-latest",
+ "matchPattern": "(?i)^(google/)?(gemini-1.5-pro-latest)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2024-04-11T10:27:46.517Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000025,
+ "output": 0.0000075
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4-turbo-2024-04-09",
+ "matchPattern": "(?i)^(openai/)?(gpt-4-turbo-2024-04-09)$",
+ "startDate": "2024-04-23T10:37:17.092Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00001,
+ "output": 0.00003
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4-turbo",
+ "matchPattern": "(?i)^(openai/)?(gpt-4-turbo)$",
+ "startDate": "2024-04-11T21:13:44.989Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00001,
+ "output": 0.00003
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4-preview",
+ "matchPattern": "(?i)^(openai/)?(gpt-4-preview)$",
+ "startDate": "2024-04-23T10:37:17.092Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00001,
+ "output": 0.00003
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-3-5-sonnet-20240620",
+ "matchPattern": "(?i)^(anthropic/)?(claude-3-5-sonnet-20240620|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20240620-v1:0|claude-3-5-sonnet@20240620)$",
+ "startDate": "2024-06-25T11:47:24.475Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4o-mini",
+ "matchPattern": "(?i)^(openai/)?(gpt-4o-mini)$",
+ "startDate": "2024-07-18T17:56:09.591Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1.5e-7,
+ "output": 6e-7,
+ "input_cached_tokens": 7.5e-8,
+ "input_cache_read": 7.5e-8
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4o-mini-2024-07-18",
+ "matchPattern": "(?i)^(openai/)?(gpt-4o-mini-2024-07-18)$",
+ "startDate": "2024-07-18T17:56:09.591Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1.5e-7,
+ "input_cached_tokens": 7.5e-8,
+ "input_cache_read": 7.5e-8,
+ "output": 6e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4o-2024-08-06",
+ "matchPattern": "(?i)^(openai/)?(gpt-4o-2024-08-06)$",
+ "startDate": "2024-08-07T11:54:31.298Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000025,
+ "input_cached_tokens": 0.00000125,
+ "input_cache_read": 0.00000125,
+ "output": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o1-preview",
+ "matchPattern": "(?i)^(openai/)?(o1-preview)$",
+ "startDate": "2024-09-13T10:01:35.373Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "input_cached_tokens": 0.0000075,
+ "input_cache_read": 0.0000075,
+ "output": 0.00006,
+ "output_reasoning_tokens": 0.00006,
+ "output_reasoning": 0.00006
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o1-preview-2024-09-12",
+ "matchPattern": "(?i)^(openai/)?(o1-preview-2024-09-12)$",
+ "startDate": "2024-09-13T10:01:35.373Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "input_cached_tokens": 0.0000075,
+ "input_cache_read": 0.0000075,
+ "output": 0.00006,
+ "output_reasoning_tokens": 0.00006,
+ "output_reasoning": 0.00006
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o1-mini",
+ "matchPattern": "(?i)^(openai/)?(o1-mini)$",
+ "startDate": "2024-09-13T10:01:35.373Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000011,
+ "input_cached_tokens": 5.5e-7,
+ "input_cache_read": 5.5e-7,
+ "output": 0.0000044,
+ "output_reasoning_tokens": 0.0000044,
+ "output_reasoning": 0.0000044
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o1-mini-2024-09-12",
+ "matchPattern": "(?i)^(openai/)?(o1-mini-2024-09-12)$",
+ "startDate": "2024-09-13T10:01:35.373Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000011,
+ "input_cached_tokens": 5.5e-7,
+ "input_cache_read": 5.5e-7,
+ "output": 0.0000044,
+ "output_reasoning_tokens": 0.0000044,
+ "output_reasoning": 0.0000044
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-3.5-sonnet-20241022",
+ "matchPattern": "(?i)^(anthropic/)?(claude-3-5-sonnet-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20241022-v2:0|claude-3-5-sonnet-V2@20241022)$",
+ "startDate": "2024-10-22T18:48:01.676Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-3.5-sonnet-latest",
+ "matchPattern": "(?i)^(anthropic/)?(claude-3-5-sonnet-latest)$",
+ "startDate": "2024-10-22T18:48:01.676Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-3-5-haiku-20241022",
+ "matchPattern": "(?i)^(anthropic/)?(claude-3-5-haiku-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-haiku-20241022-v1:0|claude-3-5-haiku-V1@20241022)$",
+ "startDate": "2024-11-05T10:30:50.566Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 8e-7,
+ "input_tokens": 8e-7,
+ "output": 0.000004,
+ "output_tokens": 0.000004,
+ "cache_creation_input_tokens": 0.000001,
+ "input_cache_creation": 0.000001,
+ "input_cache_creation_5m": 0.000001,
+ "input_cache_creation_1h": 0.0000016,
+ "cache_read_input_tokens": 8e-8,
+ "input_cache_read": 8e-8
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-3.5-haiku-latest",
+ "matchPattern": "(?i)^(anthropic/)?(claude-3-5-haiku-latest)$",
+ "startDate": "2024-11-05T10:30:50.566Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 8e-7,
+ "input_tokens": 8e-7,
+ "output": 0.000004,
+ "output_tokens": 0.000004,
+ "cache_creation_input_tokens": 0.000001,
+ "input_cache_creation": 0.000001,
+ "input_cache_creation_5m": 0.000001,
+ "input_cache_creation_1h": 0.0000016,
+ "cache_read_input_tokens": 8e-8,
+ "input_cache_read": 8e-8
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "chatgpt-4o-latest",
+ "matchPattern": "(?i)^(chatgpt-4o-latest)$",
+ "startDate": "2024-11-25T12:47:17.504Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000005,
+ "output": 0.000015
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4o-2024-11-20",
+ "matchPattern": "(?i)^(openai/)?(gpt-4o-2024-11-20)$",
+ "startDate": "2024-12-03T10:06:12.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000025,
+ "input_cached_tokens": 0.00000125,
+ "input_cache_read": 0.00000125,
+ "output": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4o-audio-preview",
+ "matchPattern": "(?i)^(openai/)?(gpt-4o-audio-preview)$",
+ "startDate": "2024-12-03T10:19:56.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input_text_tokens": 0.0000025,
+ "output_text_tokens": 0.00001,
+ "input_audio_tokens": 0.0001,
+ "input_audio": 0.0001,
+ "output_audio_tokens": 0.0002,
+ "output_audio": 0.0002
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4o-audio-preview-2024-10-01",
+ "matchPattern": "(?i)^(openai/)?(gpt-4o-audio-preview-2024-10-01)$",
+ "startDate": "2024-12-03T10:19:56.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input_text_tokens": 0.0000025,
+ "output_text_tokens": 0.00001,
+ "input_audio_tokens": 0.0001,
+ "input_audio": 0.0001,
+ "output_audio_tokens": 0.0002,
+ "output_audio": 0.0002
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4o-realtime-preview",
+ "matchPattern": "(?i)^(openai/)?(gpt-4o-realtime-preview)$",
+ "startDate": "2024-12-03T10:19:56.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input_text_tokens": 0.000005,
+ "input_cached_text_tokens": 0.0000025,
+ "output_text_tokens": 0.00002,
+ "input_audio_tokens": 0.0001,
+ "input_audio": 0.0001,
+ "input_cached_audio_tokens": 0.00002,
+ "output_audio_tokens": 0.0002,
+ "output_audio": 0.0002
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4o-realtime-preview-2024-10-01",
+ "matchPattern": "(?i)^(openai/)?(gpt-4o-realtime-preview-2024-10-01)$",
+ "startDate": "2024-12-03T10:19:56.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input_text_tokens": 0.000005,
+ "input_cached_text_tokens": 0.0000025,
+ "output_text_tokens": 0.00002,
+ "input_audio_tokens": 0.0001,
+ "input_audio": 0.0001,
+ "input_cached_audio_tokens": 0.00002,
+ "output_audio_tokens": 0.0002,
+ "output_audio": 0.0002
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o1",
+ "matchPattern": "(?i)^(openai/)?(o1)$",
+ "startDate": "2025-01-17T00:01:35.373Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "input_cached_tokens": 0.0000075,
+ "input_cache_read": 0.0000075,
+ "output": 0.00006,
+ "output_reasoning_tokens": 0.00006,
+ "output_reasoning": 0.00006
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o1-2024-12-17",
+ "matchPattern": "(?i)^(openai/)?(o1-2024-12-17)$",
+ "startDate": "2025-01-17T00:01:35.373Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "input_cached_tokens": 0.0000075,
+ "input_cache_read": 0.0000075,
+ "output": 0.00006,
+ "output_reasoning_tokens": 0.00006,
+ "output_reasoning": 0.00006
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o3-mini",
+ "matchPattern": "(?i)^(openai/)?(o3-mini)$",
+ "startDate": "2025-01-31T20:41:35.373Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000011,
+ "input_cached_tokens": 5.5e-7,
+ "input_cache_read": 5.5e-7,
+ "output": 0.0000044,
+ "output_reasoning_tokens": 0.0000044,
+ "output_reasoning": 0.0000044
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o3-mini-2025-01-31",
+ "matchPattern": "(?i)^(openai/)?(o3-mini-2025-01-31)$",
+ "startDate": "2025-01-31T20:41:35.373Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000011,
+ "input_cached_tokens": 5.5e-7,
+ "input_cache_read": 5.5e-7,
+ "output": 0.0000044,
+ "output_reasoning_tokens": 0.0000044,
+ "output_reasoning": 0.0000044
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-2.0-flash-001",
+ "matchPattern": "(?i)^(google/)?(gemini-2.0-flash-001)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2025-02-06T11:11:35.241Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1e-7,
+ "output": 4e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-2.0-flash-lite-preview-02-05",
+ "matchPattern": "(?i)^(google/)?(gemini-2.0-flash-lite-preview-02-05)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2025-02-06T11:11:35.241Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 7.5e-8,
+ "output": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-3.7-sonnet-20250219",
+ "matchPattern": "(?i)^(anthropic/)?(claude-3.7-sonnet-20250219|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3.7-sonnet-20250219-v1:0|claude-3-7-sonnet-V1@20250219)$",
+ "startDate": "2025-02-25T09:35:39.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-3.7-sonnet-latest",
+ "matchPattern": "(?i)^(anthropic/)?(claude-3-7-sonnet-latest)$",
+ "startDate": "2025-02-25T09:35:39.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4.5-preview",
+ "matchPattern": "(?i)^(openai/)?(gpt-4.5-preview)$",
+ "startDate": "2025-02-27T21:26:54.132Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000075,
+ "input_cached_tokens": 0.0000375,
+ "input_cached_text_tokens": 0.0000375,
+ "input_cache_read": 0.0000375,
+ "output": 0.00015
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4.5-preview-2025-02-27",
+ "matchPattern": "(?i)^(openai/)?(gpt-4.5-preview-2025-02-27)$",
+ "startDate": "2025-02-27T21:26:54.132Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000075,
+ "input_cached_tokens": 0.0000375,
+ "input_cached_text_tokens": 0.0000375,
+ "input_cache_read": 0.0000375,
+ "output": 0.00015
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4.1",
+ "matchPattern": "(?i)^(openai/)?(gpt-4.1)$",
+ "startDate": "2025-04-15T10:26:54.132Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000002,
+ "input_cached_tokens": 5e-7,
+ "input_cached_text_tokens": 5e-7,
+ "input_cache_read": 5e-7,
+ "output": 0.000008
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4.1-2025-04-14",
+ "matchPattern": "(?i)^(openai/)?(gpt-4.1-2025-04-14)$",
+ "startDate": "2025-04-15T10:26:54.132Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000002,
+ "input_cached_tokens": 5e-7,
+ "input_cached_text_tokens": 5e-7,
+ "input_cache_read": 5e-7,
+ "output": 0.000008
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4.1-mini-2025-04-14",
+ "matchPattern": "(?i)^(openai/)?(gpt-4.1-mini-2025-04-14)$",
+ "startDate": "2025-04-15T10:26:54.132Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 4e-7,
+ "input_cached_tokens": 1e-7,
+ "input_cached_text_tokens": 1e-7,
+ "input_cache_read": 1e-7,
+ "output": 0.0000016
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4.1-nano-2025-04-14",
+ "matchPattern": "(?i)^(openai/)?(gpt-4.1-nano-2025-04-14)$",
+ "startDate": "2025-04-15T10:26:54.132Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1e-7,
+ "input_cached_tokens": 2.5e-8,
+ "input_cached_text_tokens": 2.5e-8,
+ "input_cache_read": 2.5e-8,
+ "output": 4e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o3",
+ "matchPattern": "(?i)^(openai/)?(o3)$",
+ "startDate": "2025-04-16T23:26:54.132Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000002,
+ "input_cached_tokens": 5e-7,
+ "input_cache_read": 5e-7,
+ "output": 0.000008,
+ "output_reasoning_tokens": 0.000008,
+ "output_reasoning": 0.000008
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o3-2025-04-16",
+ "matchPattern": "(?i)^(openai/)?(o3-2025-04-16)$",
+ "startDate": "2025-04-16T23:26:54.132Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000002,
+ "input_cached_tokens": 5e-7,
+ "input_cache_read": 5e-7,
+ "output": 0.000008,
+ "output_reasoning_tokens": 0.000008,
+ "output_reasoning": 0.000008
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o4-mini",
+ "matchPattern": "(?i)^(o4-mini)$",
+ "startDate": "2025-04-16T23:26:54.132Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000011,
+ "input_cached_tokens": 2.75e-7,
+ "input_cache_read": 2.75e-7,
+ "output": 0.0000044,
+ "output_reasoning_tokens": 0.0000044,
+ "output_reasoning": 0.0000044
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o4-mini-2025-04-16",
+ "matchPattern": "(?i)^(o4-mini-2025-04-16)$",
+ "startDate": "2025-04-16T23:26:54.132Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000011,
+ "input_cached_tokens": 2.75e-7,
+ "input_cache_read": 2.75e-7,
+ "output": 0.0000044,
+ "output_reasoning_tokens": 0.0000044,
+ "output_reasoning": 0.0000044
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-2.0-flash",
+ "matchPattern": "(?i)^(google/)?(gemini-2.0-flash)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2025-04-22T10:11:35.241Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1e-7,
+ "output": 4e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-2.0-flash-lite-preview",
+ "matchPattern": "(?i)^(google/)?(gemini-2.0-flash-lite-preview)(@[a-zA-Z0-9]+)?$",
+ "startDate": "2025-04-22T10:11:35.241Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 7.5e-8,
+ "output": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4.1-nano",
+ "matchPattern": "(?i)^(openai/)?(gpt-4.1-nano)$",
+ "startDate": "2025-04-22T10:11:35.241Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1e-7,
+ "input_cached_tokens": 2.5e-8,
+ "input_cached_text_tokens": 2.5e-8,
+ "input_cache_read": 2.5e-8,
+ "output": 4e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-4.1-mini",
+ "matchPattern": "(?i)^(openai/)?(gpt-4.1-mini)$",
+ "startDate": "2025-04-22T10:11:35.241Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 4e-7,
+ "input_cached_tokens": 1e-7,
+ "input_cached_text_tokens": 1e-7,
+ "input_cache_read": 1e-7,
+ "output": 0.0000016
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-sonnet-4-5-20250929",
+ "matchPattern": "(?i)^(anthropic/)?(claude-sonnet-4-5(-20250929)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-5(-20250929)?-v1(:0)?|claude-sonnet-4-5-V1(@20250929)?|claude-sonnet-4-5(@20250929)?)$",
+ "startDate": "2025-09-29T00:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ },
+ {
+ "name": "Large Context",
+ "isDefault": false,
+ "priority": 1,
+ "conditions": [
+ {
+ "usageDetailPattern": "input",
+ "operator": "gt",
+ "value": 200000
+ }
+ ],
+ "prices": {
+ "input": 0.000006,
+ "input_tokens": 0.000006,
+ "output": 0.0000225,
+ "output_tokens": 0.0000225,
+ "cache_creation_input_tokens": 0.0000075,
+ "input_cache_creation": 0.0000075,
+ "input_cache_creation_5m": 0.0000075,
+ "input_cache_creation_1h": 0.000012,
+ "cache_read_input_tokens": 6e-7,
+ "input_cache_read": 6e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-sonnet-4-20250514",
+ "matchPattern": "(?i)^(anthropic/)?(claude-sonnet-4(-20250514)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4(-20250514)?-v1(:0)?|claude-sonnet-4-V1(@20250514)?|claude-sonnet-4(@20250514)?)$",
+ "startDate": "2025-05-22T17:09:02.131Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-sonnet-4-latest",
+ "matchPattern": "(?i)^(anthropic/)?(claude-sonnet-4-latest)$",
+ "startDate": "2025-05-22T17:09:02.131Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-opus-4-20250514",
+ "matchPattern": "(?i)^(anthropic/)?(claude-opus-4(-20250514)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4(-20250514)?-v1(:0)?|claude-opus-4(@20250514)?)$",
+ "startDate": "2025-05-22T17:09:02.131Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "input_tokens": 0.000015,
+ "output": 0.000075,
+ "output_tokens": 0.000075,
+ "cache_creation_input_tokens": 0.00001875,
+ "input_cache_creation": 0.00001875,
+ "input_cache_creation_5m": 0.00001875,
+ "input_cache_creation_1h": 0.00003,
+ "cache_read_input_tokens": 0.0000015,
+ "input_cache_read": 0.0000015
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o3-pro",
+ "matchPattern": "(?i)^(openai/)?(o3-pro)$",
+ "startDate": "2025-06-10T22:26:54.132Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00002,
+ "output": 0.00008,
+ "output_reasoning_tokens": 0.00008,
+ "output_reasoning": 0.00008
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o3-pro-2025-06-10",
+ "matchPattern": "(?i)^(openai/)?(o3-pro-2025-06-10)$",
+ "startDate": "2025-06-10T22:26:54.132Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00002,
+ "output": 0.00008,
+ "output_reasoning_tokens": 0.00008,
+ "output_reasoning": 0.00008
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o1-pro",
+ "matchPattern": "(?i)^(openai/)?(o1-pro)$",
+ "startDate": "2025-06-10T22:26:54.132Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00015,
+ "output": 0.0006,
+ "output_reasoning_tokens": 0.0006,
+ "output_reasoning": 0.0006
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "o1-pro-2025-03-19",
+ "matchPattern": "(?i)^(openai/)?(o1-pro-2025-03-19)$",
+ "startDate": "2025-06-10T22:26:54.132Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00015,
+ "output": 0.0006,
+ "output_reasoning_tokens": 0.0006,
+ "output_reasoning": 0.0006
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-2.5-flash",
+ "matchPattern": "(?i)^(google/)?(gemini-2.5-flash)$",
+ "startDate": "2025-07-03T13:44:06.964Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 3e-7,
+ "input_text": 3e-7,
+ "input_modality_1": 3e-7,
+ "prompt_token_count": 3e-7,
+ "promptTokenCount": 3e-7,
+ "input_cached_tokens": 3e-8,
+ "cached_content_token_count": 3e-8,
+ "output": 0.0000025,
+ "output_modality_1": 0.0000025,
+ "candidates_token_count": 0.0000025,
+ "candidatesTokenCount": 0.0000025,
+ "thoughtsTokenCount": 0.0000025,
+ "thoughts_token_count": 0.0000025,
+ "output_reasoning": 0.0000025,
+ "input_audio_tokens": 0.000001
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-2.5-flash-lite",
+ "matchPattern": "(?i)^(google/)?(gemini-2.5-flash-lite)$",
+ "startDate": "2025-07-03T13:44:06.964Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 1e-7,
+ "input_text": 1e-7,
+ "input_modality_1": 1e-7,
+ "prompt_token_count": 1e-7,
+ "promptTokenCount": 1e-7,
+ "input_cached_tokens": 2.5e-8,
+ "cached_content_token_count": 2.5e-8,
+ "output": 4e-7,
+ "output_modality_1": 4e-7,
+ "candidates_token_count": 4e-7,
+ "candidatesTokenCount": 4e-7,
+ "thoughtsTokenCount": 4e-7,
+ "thoughts_token_count": 4e-7,
+ "output_reasoning": 4e-7,
+ "input_audio_tokens": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-opus-4-1-20250805",
+ "matchPattern": "(?i)^(anthropic/)?(claude-opus-4-1(-20250805)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-1(-20250805)?-v1(:0)?|claude-opus-4-1(@20250805)?)$",
+ "startDate": "2025-08-05T15:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "input_tokens": 0.000015,
+ "output": 0.000075,
+ "output_tokens": 0.000075,
+ "cache_creation_input_tokens": 0.00001875,
+ "input_cache_creation": 0.00001875,
+ "input_cache_creation_5m": 0.00001875,
+ "input_cache_creation_1h": 0.00003,
+ "cache_read_input_tokens": 0.0000015,
+ "input_cache_read": 0.0000015
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5",
+ "matchPattern": "(?i)^(openai/)?(gpt-5)$",
+ "startDate": "2025-08-07T16:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000125,
+ "input_cached_tokens": 1.25e-7,
+ "output": 0.00001,
+ "input_cache_read": 1.25e-7,
+ "output_reasoning_tokens": 0.00001,
+ "output_reasoning": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5-2025-08-07",
+ "matchPattern": "(?i)^(openai/)?(gpt-5-2025-08-07)$",
+ "startDate": "2025-08-11T08:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000125,
+ "input_cached_tokens": 1.25e-7,
+ "output": 0.00001,
+ "input_cache_read": 1.25e-7,
+ "output_reasoning_tokens": 0.00001,
+ "output_reasoning": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5-mini",
+ "matchPattern": "(?i)^(openai/)?(gpt-5-mini)$",
+ "startDate": "2025-08-07T16:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "input_cached_tokens": 2.5e-8,
+ "output": 0.000002,
+ "input_cache_read": 2.5e-8,
+ "output_reasoning_tokens": 0.000002,
+ "output_reasoning": 0.000002
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5-mini-2025-08-07",
+ "matchPattern": "(?i)^(openai/)?(gpt-5-mini-2025-08-07)$",
+ "startDate": "2025-08-11T08:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "input_cached_tokens": 2.5e-8,
+ "output": 0.000002,
+ "input_cache_read": 2.5e-8,
+ "output_reasoning_tokens": 0.000002,
+ "output_reasoning": 0.000002
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5-nano",
+ "matchPattern": "(?i)^(openai/)?(gpt-5-nano)$",
+ "startDate": "2025-08-07T16:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 5e-8,
+ "input_cached_tokens": 5e-9,
+ "output": 4e-7,
+ "input_cache_read": 5e-9,
+ "output_reasoning_tokens": 4e-7,
+ "output_reasoning": 4e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5-nano-2025-08-07",
+ "matchPattern": "(?i)^(openai/)?(gpt-5-nano-2025-08-07)$",
+ "startDate": "2025-08-11T08:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 5e-8,
+ "input_cached_tokens": 5e-9,
+ "output": 4e-7,
+ "input_cache_read": 5e-9,
+ "output_reasoning_tokens": 4e-7,
+ "output_reasoning": 4e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5-chat-latest",
+ "matchPattern": "(?i)^(openai/)?(gpt-5-chat-latest)$",
+ "startDate": "2025-08-07T16:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000125,
+ "input_cached_tokens": 1.25e-7,
+ "output": 0.00001,
+ "input_cache_read": 1.25e-7,
+ "output_reasoning_tokens": 0.00001,
+ "output_reasoning": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5-pro",
+ "matchPattern": "(?i)^(openai/)?(gpt-5-pro)$",
+ "startDate": "2025-10-07T08:03:54.727Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "output": 0.00012,
+ "output_reasoning_tokens": 0.00012,
+ "output_reasoning": 0.00012
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5-pro-2025-10-06",
+ "matchPattern": "(?i)^(openai/)?(gpt-5-pro-2025-10-06)$",
+ "startDate": "2025-10-07T08:03:54.727Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000015,
+ "output": 0.00012,
+ "output_reasoning_tokens": 0.00012,
+ "output_reasoning": 0.00012
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-haiku-4-5-20251001",
+ "matchPattern": "(?i)^(anthropic/)?(claude-haiku-4-5-20251001|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-haiku-4-5-20251001-v1:0|claude-4-5-haiku@20251001)$",
+ "startDate": "2025-10-16T08:20:44.558Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000001,
+ "input_tokens": 0.000001,
+ "output": 0.000005,
+ "output_tokens": 0.000005,
+ "cache_creation_input_tokens": 0.00000125,
+ "input_cache_creation": 0.00000125,
+ "input_cache_creation_5m": 0.00000125,
+ "input_cache_creation_1h": 0.000002,
+ "cache_read_input_tokens": 1e-7,
+ "input_cache_read": 1e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5.1",
+ "matchPattern": "(?i)^(openai/)?(gpt-5.1)$",
+ "startDate": "2025-11-14T08:57:23.481Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000125,
+ "input_cached_tokens": 1.25e-7,
+ "output": 0.00001,
+ "input_cache_read": 1.25e-7,
+ "output_reasoning_tokens": 0.00001,
+ "output_reasoning": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5.1-2025-11-13",
+ "matchPattern": "(?i)^(openai/)?(gpt-5.1-2025-11-13)$",
+ "startDate": "2025-11-14T08:57:23.481Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000125,
+ "input_cached_tokens": 1.25e-7,
+ "output": 0.00001,
+ "input_cache_read": 1.25e-7,
+ "output_reasoning_tokens": 0.00001,
+ "output_reasoning": 0.00001
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-opus-4-5-20251101",
+ "matchPattern": "(?i)^(anthropic/)?(claude-opus-4-5(-20251101)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-5(-20251101)?-v1(:0)?|claude-opus-4-5(@20251101)?)$",
+ "startDate": "2025-11-24T20:53:27.571Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000005,
+ "input_tokens": 0.000005,
+ "output": 0.000025,
+ "output_tokens": 0.000025,
+ "cache_creation_input_tokens": 0.00000625,
+ "input_cache_creation": 0.00000625,
+ "input_cache_creation_5m": 0.00000625,
+ "input_cache_creation_1h": 0.00001,
+ "cache_read_input_tokens": 5e-7,
+ "input_cache_read": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-sonnet-4-6",
+ "matchPattern": "(?i)^(anthropic\\/)?(claude-sonnet-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-6(-v1(:0)?)?|claude-sonnet-4-6)$",
+ "startDate": "2026-02-18T00:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000003,
+ "input_tokens": 0.000003,
+ "output": 0.000015,
+ "output_tokens": 0.000015,
+ "cache_creation_input_tokens": 0.00000375,
+ "input_cache_creation": 0.00000375,
+ "input_cache_creation_5m": 0.00000375,
+ "input_cache_creation_1h": 0.000006,
+ "cache_read_input_tokens": 3e-7,
+ "input_cache_read": 3e-7
+ }
+ },
+ {
+ "name": "Large Context",
+ "isDefault": false,
+ "priority": 1,
+ "conditions": [
+ {
+ "usageDetailPattern": "input",
+ "operator": "gt",
+ "value": 200000
+ }
+ ],
+ "prices": {
+ "input": 0.000006,
+ "input_tokens": 0.000006,
+ "output": 0.0000225,
+ "output_tokens": 0.0000225,
+ "cache_creation_input_tokens": 0.0000075,
+ "input_cache_creation": 0.0000075,
+ "input_cache_creation_5m": 0.0000075,
+ "input_cache_creation_1h": 0.000012,
+ "cache_read_input_tokens": 6e-7,
+ "input_cache_read": 6e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "claude-opus-4-6",
+ "matchPattern": "(?i)^(anthropic/)?(claude-opus-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-6-v1(:0)?|claude-opus-4-6)$",
+ "startDate": "2026-02-09T00:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000005,
+ "input_tokens": 0.000005,
+ "output": 0.000025,
+ "output_tokens": 0.000025,
+ "cache_creation_input_tokens": 0.00000625,
+ "input_cache_creation": 0.00000625,
+ "input_cache_creation_5m": 0.00000625,
+ "input_cache_creation_1h": 0.00001,
+ "cache_read_input_tokens": 5e-7,
+ "input_cache_read": 5e-7
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-2.5-pro",
+ "matchPattern": "(?i)^(google/)?(gemini-2.5-pro)$",
+ "startDate": "2025-11-26T13:27:53.545Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000125,
+ "input_text": 0.00000125,
+ "input_modality_1": 0.00000125,
+ "prompt_token_count": 0.00000125,
+ "promptTokenCount": 0.00000125,
+ "input_cached_tokens": 1.25e-7,
+ "cached_content_token_count": 1.25e-7,
+ "output": 0.00001,
+ "output_modality_1": 0.00001,
+ "candidates_token_count": 0.00001,
+ "candidatesTokenCount": 0.00001,
+ "thoughtsTokenCount": 0.00001,
+ "thoughts_token_count": 0.00001,
+ "output_reasoning": 0.00001
+ }
+ },
+ {
+ "name": "Large Context",
+ "isDefault": false,
+ "priority": 1,
+ "conditions": [
+ {
+ "usageDetailPattern": "(input|prompt|cached)",
+ "operator": "gt",
+ "value": 200000
+ }
+ ],
+ "prices": {
+ "input": 0.0000025,
+ "input_text": 0.0000025,
+ "input_modality_1": 0.0000025,
+ "prompt_token_count": 0.0000025,
+ "promptTokenCount": 0.0000025,
+ "input_cached_tokens": 2.5e-7,
+ "cached_content_token_count": 2.5e-7,
+ "output": 0.000015,
+ "output_modality_1": 0.000015,
+ "candidates_token_count": 0.000015,
+ "candidatesTokenCount": 0.000015,
+ "thoughtsTokenCount": 0.000015,
+ "thoughts_token_count": 0.000015,
+ "output_reasoning": 0.000015
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-3-pro-preview",
+ "matchPattern": "(?i)^(google/)?(gemini-3-pro-preview)$",
+ "startDate": "2025-11-26T13:27:53.545Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000002,
+ "input_modality_1": 0.000002,
+ "prompt_token_count": 0.000002,
+ "promptTokenCount": 0.000002,
+ "input_cached_tokens": 2e-7,
+ "cached_content_token_count": 2e-7,
+ "output": 0.000012,
+ "output_modality_1": 0.000012,
+ "candidates_token_count": 0.000012,
+ "candidatesTokenCount": 0.000012,
+ "thoughtsTokenCount": 0.000012,
+ "thoughts_token_count": 0.000012,
+ "output_reasoning": 0.000012
+ }
+ },
+ {
+ "name": "Large Context",
+ "isDefault": false,
+ "priority": 1,
+ "conditions": [
+ {
+ "usageDetailPattern": "(input|prompt|cached)",
+ "operator": "gt",
+ "value": 200000
+ }
+ ],
+ "prices": {
+ "input": 0.000004,
+ "input_modality_1": 0.000004,
+ "prompt_token_count": 0.000004,
+ "promptTokenCount": 0.000004,
+ "input_cached_tokens": 4e-7,
+ "cached_content_token_count": 4e-7,
+ "output": 0.000018,
+ "output_modality_1": 0.000018,
+ "candidates_token_count": 0.000018,
+ "candidatesTokenCount": 0.000018,
+ "thoughtsTokenCount": 0.000018,
+ "thoughts_token_count": 0.000018,
+ "output_reasoning": 0.000018
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-3.1-pro-preview",
+ "matchPattern": "(?i)^(google/)?(gemini-3.1-pro-preview(-customtools)?)$",
+ "startDate": "2026-02-19T00:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000002,
+ "input_modality_1": 0.000002,
+ "prompt_token_count": 0.000002,
+ "promptTokenCount": 0.000002,
+ "input_cached_tokens": 2e-7,
+ "cached_content_token_count": 2e-7,
+ "output": 0.000012,
+ "output_modality_1": 0.000012,
+ "candidates_token_count": 0.000012,
+ "candidatesTokenCount": 0.000012,
+ "thoughtsTokenCount": 0.000012,
+ "thoughts_token_count": 0.000012,
+ "output_reasoning": 0.000012
+ }
+ },
+ {
+ "name": "Large Context",
+ "isDefault": false,
+ "priority": 1,
+ "conditions": [
+ {
+ "usageDetailPattern": "(input|prompt|cached)",
+ "operator": "gt",
+ "value": 200000
+ }
+ ],
+ "prices": {
+ "input": 0.000004,
+ "input_modality_1": 0.000004,
+ "prompt_token_count": 0.000004,
+ "promptTokenCount": 0.000004,
+ "input_cached_tokens": 4e-7,
+ "cached_content_token_count": 4e-7,
+ "output": 0.000018,
+ "output_modality_1": 0.000018,
+ "candidates_token_count": 0.000018,
+ "candidatesTokenCount": 0.000018,
+ "thoughtsTokenCount": 0.000018,
+ "thoughts_token_count": 0.000018,
+ "output_reasoning": 0.000018
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5.2",
+ "matchPattern": "(?i)^(openai/)?(gpt-5.2)$",
+ "startDate": "2025-12-12T09:00:06.513Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000175,
+ "input_cached_tokens": 1.75e-7,
+ "input_cache_read": 1.75e-7,
+ "output": 0.000014,
+ "output_reasoning_tokens": 0.000014,
+ "output_reasoning": 0.000014
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5.2-2025-12-11",
+ "matchPattern": "(?i)^(openai/)?(gpt-5.2-2025-12-11)$",
+ "startDate": "2025-12-12T09:00:06.513Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00000175,
+ "input_cached_tokens": 1.75e-7,
+ "input_cache_read": 1.75e-7,
+ "output": 0.000014,
+ "output_reasoning_tokens": 0.000014,
+ "output_reasoning": 0.000014
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5.2-pro",
+ "matchPattern": "(?i)^(openai/)?(gpt-5.2-pro)$",
+ "startDate": "2025-12-12T09:00:06.513Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000021,
+ "output": 0.000168,
+ "output_reasoning_tokens": 0.000168,
+ "output_reasoning": 0.000168
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5.2-pro-2025-12-11",
+ "matchPattern": "(?i)^(openai/)?(gpt-5.2-pro-2025-12-11)$",
+ "startDate": "2025-12-12T09:00:06.513Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.000021,
+ "output": 0.000168,
+ "output_reasoning_tokens": 0.000168,
+ "output_reasoning": 0.000168
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5.4",
+ "matchPattern": "(?i)^(openai/)?(gpt-5.4)$",
+ "startDate": "2026-03-05T00:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000025,
+ "input_cached_tokens": 2.5e-7,
+ "input_cache_read": 2.5e-7,
+ "output": 0.000015,
+ "output_reasoning_tokens": 0.000015,
+ "output_reasoning": 0.000015
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5.4-pro",
+ "matchPattern": "(?i)^(openai/)?(gpt-5.4-pro)$",
+ "startDate": "2026-03-05T00:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00003,
+ "output": 0.00018,
+ "output_reasoning_tokens": 0.00018,
+ "output_reasoning": 0.00018
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5.4-2026-03-05",
+ "matchPattern": "(?i)^(openai/)?(gpt-5.4-2026-03-05)$",
+ "startDate": "2026-03-05T00:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.0000025,
+ "input_cached_tokens": 2.5e-7,
+ "input_cache_read": 2.5e-7,
+ "output": 0.000015,
+ "output_reasoning_tokens": 0.000015,
+ "output_reasoning": 0.000015
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gpt-5.4-pro-2026-03-05",
+ "matchPattern": "(?i)^(openai/)?(gpt-5.4-pro-2026-03-05)$",
+ "startDate": "2026-03-05T00:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 0.00003,
+ "output": 0.00018,
+ "output_reasoning_tokens": 0.00018,
+ "output_reasoning": 0.00018
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-3-flash-preview",
+ "matchPattern": "(?i)^(google/)?(gemini-3-flash-preview)$",
+ "startDate": "2025-12-21T12:01:42.282Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 5e-7,
+ "input_modality_1": 5e-7,
+ "prompt_token_count": 5e-7,
+ "promptTokenCount": 5e-7,
+ "input_cached_tokens": 5e-8,
+ "cached_content_token_count": 5e-8,
+ "output": 0.000003,
+ "output_modality_1": 0.000003,
+ "candidates_token_count": 0.000003,
+ "candidatesTokenCount": 0.000003,
+ "thoughtsTokenCount": 0.000003,
+ "thoughts_token_count": 0.000003,
+ "output_reasoning": 0.000003
+ }
+ }
+ ]
+ },
+ {
+ "modelName": "gemini-3.1-flash-lite-preview",
+ "matchPattern": "(?i)^(google/)?(gemini-3.1-flash-lite-preview)$",
+ "startDate": "2026-03-03T00:00:00.000Z",
+ "pricingTiers": [
+ {
+ "name": "Standard",
+ "isDefault": true,
+ "priority": 0,
+ "conditions": [],
+ "prices": {
+ "input": 2.5e-7,
+ "input_modality_1": 2.5e-7,
+ "prompt_token_count": 2.5e-7,
+ "promptTokenCount": 2.5e-7,
+ "input_cached_tokens": 2.5e-8,
+ "cached_content_token_count": 2.5e-8,
+ "output": 0.0000015,
+ "output_modality_1": 0.0000015,
+ "candidates_token_count": 0.0000015,
+ "candidatesTokenCount": 0.0000015,
+ "thoughtsTokenCount": 0.0000015,
+ "thoughts_token_count": 0.0000015,
+ "output_reasoning": 0.0000015,
+ "input_audio_tokens": 5e-7
+ }
+ }
+ ]
+ }
+];
diff --git a/internal-packages/llm-pricing/src/index.ts b/internal-packages/llm-pricing/src/index.ts
new file mode 100644
index 00000000000..3632434c137
--- /dev/null
+++ b/internal-packages/llm-pricing/src/index.ts
@@ -0,0 +1,11 @@
+export { ModelPricingRegistry } from "./registry.js";
+export { seedLlmPricing } from "./seed.js";
+export { defaultModelPrices } from "./defaultPrices.js";
+export type {
+ LlmModelWithPricing,
+ LlmCostResult,
+ LlmPricingTierWithPrices,
+ LlmPriceEntry,
+ PricingCondition,
+ DefaultModelDefinition,
+} from "./types.js";
diff --git a/internal-packages/llm-pricing/src/registry.test.ts b/internal-packages/llm-pricing/src/registry.test.ts
new file mode 100644
index 00000000000..679c8c4cfcf
--- /dev/null
+++ b/internal-packages/llm-pricing/src/registry.test.ts
@@ -0,0 +1,396 @@
+import { describe, it, expect, beforeEach } from "vitest";
+import { ModelPricingRegistry } from "./registry.js";
+import { defaultModelPrices } from "./defaultPrices.js";
+import type { LlmModelWithPricing } from "./types.js";
+
+// Convert POSIX-style (?i) inline flag to JS RegExp 'i' flag
+function compilePattern(pattern: string): RegExp {
+ if (pattern.startsWith("(?i)")) {
+ return new RegExp(pattern.slice(4), "i");
+ }
+ return new RegExp(pattern);
+}
+
+// Create a mock registry that we can load with test data without Prisma
+class TestableRegistry extends ModelPricingRegistry {
+ loadPatterns(models: LlmModelWithPricing[]) {
+ // Access private fields via any cast for testing
+ const self = this as any;
+ self._patterns = models.map((model) => ({
+ regex: compilePattern(model.matchPattern),
+ model,
+ }));
+ self._exactMatchCache = new Map();
+ self._loaded = true;
+ }
+}
+
+const gpt4o: LlmModelWithPricing = {
+ id: "model-gpt4o",
+ friendlyId: "llm_model_gpt4o",
+ modelName: "gpt-4o",
+ matchPattern: "^gpt-4o(-\\d{4}-\\d{2}-\\d{2})?$",
+ startDate: null,
+ pricingTiers: [
+ {
+ id: "tier-gpt4o-standard",
+ name: "Standard",
+ isDefault: true,
+ priority: 0,
+ conditions: [],
+ prices: [
+ { usageType: "input", price: 0.0000025 },
+ { usageType: "output", price: 0.00001 },
+ { usageType: "input_cached_tokens", price: 0.00000125 },
+ ],
+ },
+ ],
+};
+
+const claudeSonnet: LlmModelWithPricing = {
+ id: "model-claude-sonnet",
+ friendlyId: "llm_model_claude_sonnet",
+ modelName: "claude-sonnet-4-0",
+ matchPattern: "^claude-sonnet-4-0(-\\d{8})?$",
+ startDate: null,
+ pricingTiers: [
+ {
+ id: "tier-claude-sonnet-standard",
+ name: "Standard",
+ isDefault: true,
+ priority: 0,
+ conditions: [],
+ prices: [
+ { usageType: "input", price: 0.000003 },
+ { usageType: "output", price: 0.000015 },
+ { usageType: "input_cached_tokens", price: 0.0000015 },
+ ],
+ },
+ ],
+};
+
+describe("ModelPricingRegistry", () => {
+ let registry: TestableRegistry;
+
+ beforeEach(() => {
+ registry = new TestableRegistry(null as any);
+ registry.loadPatterns([gpt4o, claudeSonnet]);
+ });
+
+ describe("match", () => {
+ it("should match exact model name", () => {
+ const result = registry.match("gpt-4o");
+ expect(result).not.toBeNull();
+ expect(result!.modelName).toBe("gpt-4o");
+ });
+
+ it("should match model with date suffix", () => {
+ const result = registry.match("gpt-4o-2024-08-06");
+ expect(result).not.toBeNull();
+ expect(result!.modelName).toBe("gpt-4o");
+ });
+
+ it("should match claude model", () => {
+ const result = registry.match("claude-sonnet-4-0-20250514");
+ expect(result).not.toBeNull();
+ expect(result!.modelName).toBe("claude-sonnet-4-0");
+ });
+
+ it("should return null for unknown model", () => {
+ const result = registry.match("unknown-model-xyz");
+ expect(result).toBeNull();
+ });
+
+ it("should cache exact matches", () => {
+ registry.match("gpt-4o");
+ registry.match("gpt-4o");
+ // Second call should use cache - no way to verify without mocking, but it shouldn't error
+ expect(registry.match("gpt-4o")!.modelName).toBe("gpt-4o");
+ });
+
+ it("should cache misses", () => {
+ expect(registry.match("unknown")).toBeNull();
+ expect(registry.match("unknown")).toBeNull();
+ });
+ });
+
+ describe("calculateCost", () => {
+ it("should calculate cost for input and output tokens", () => {
+ const result = registry.calculateCost("gpt-4o", {
+ input: 1000,
+ output: 100,
+ });
+
+ expect(result).not.toBeNull();
+ expect(result!.matchedModelName).toBe("gpt-4o");
+ expect(result!.pricingTierName).toBe("Standard");
+ expect(result!.inputCost).toBeCloseTo(0.0025); // 1000 * 0.0000025
+ expect(result!.outputCost).toBeCloseTo(0.001); // 100 * 0.00001
+ expect(result!.totalCost).toBeCloseTo(0.0035);
+ });
+
+ it("should include cached token costs", () => {
+ const result = registry.calculateCost("gpt-4o", {
+ input: 500,
+ output: 50,
+ input_cached_tokens: 200,
+ });
+
+ expect(result).not.toBeNull();
+ expect(result!.costDetails["input"]).toBeCloseTo(0.00125); // 500 * 0.0000025
+ expect(result!.costDetails["output"]).toBeCloseTo(0.0005); // 50 * 0.00001
+ expect(result!.costDetails["input_cached_tokens"]).toBeCloseTo(0.00025); // 200 * 0.00000125
+ expect(result!.totalCost).toBeCloseTo(0.002);
+ });
+
+ it("should return null for unknown model", () => {
+ const result = registry.calculateCost("unknown-model", { input: 100, output: 50 });
+ expect(result).toBeNull();
+ });
+
+ it("should handle zero tokens", () => {
+ const result = registry.calculateCost("gpt-4o", { input: 0, output: 0 });
+ expect(result).not.toBeNull();
+ expect(result!.totalCost).toBe(0);
+ });
+
+ it("should handle missing usage types gracefully", () => {
+ const result = registry.calculateCost("gpt-4o", { input: 100 });
+ expect(result).not.toBeNull();
+ expect(result!.inputCost).toBeCloseTo(0.00025);
+ expect(result!.outputCost).toBe(0); // No output tokens
+ expect(result!.totalCost).toBeCloseTo(0.00025);
+ });
+ });
+
+ describe("isLoaded", () => {
+ it("should return false before loading", () => {
+ const freshRegistry = new TestableRegistry(null as any);
+ expect(freshRegistry.isLoaded).toBe(false);
+ });
+
+ it("should return true after loading", () => {
+ expect(registry.isLoaded).toBe(true);
+ });
+ });
+
+ describe("prefix stripping", () => {
+ it("should match gateway-prefixed model names", () => {
+ const result = registry.match("openai/gpt-4o");
+ expect(result).not.toBeNull();
+ expect(result!.modelName).toBe("gpt-4o");
+ });
+
+ it("should match openrouter-prefixed model names with date suffix", () => {
+ const result = registry.match("openai/gpt-4o-2024-08-06");
+ expect(result).not.toBeNull();
+ expect(result!.modelName).toBe("gpt-4o");
+ });
+
+ it("should return null for prefixed unknown model", () => {
+ const result = registry.match("xai/unknown-model");
+ expect(result).toBeNull();
+ });
+ });
+
+ describe("tier matching", () => {
+ const multiTierModel: LlmModelWithPricing = {
+ id: "model-gemini-pro",
+ friendlyId: "llm_model_gemini_pro",
+ modelName: "gemini-2.5-pro",
+ matchPattern: "^gemini-2\\.5-pro$",
+ startDate: null,
+ pricingTiers: [
+ {
+ id: "tier-large-context",
+ name: "Large Context",
+ isDefault: false,
+ priority: 0,
+ conditions: [
+ { usageDetailPattern: "input", operator: "gt" as const, value: 200000 },
+ ],
+ prices: [
+ { usageType: "input", price: 0.0000025 },
+ { usageType: "output", price: 0.00001 },
+ ],
+ },
+ {
+ id: "tier-standard",
+ name: "Standard",
+ isDefault: true,
+ priority: 1,
+ conditions: [],
+ prices: [
+ { usageType: "input", price: 0.00000125 },
+ { usageType: "output", price: 0.000005 },
+ ],
+ },
+ ],
+ };
+
+ it("should use conditional tier when conditions match", () => {
+ const tieredRegistry = new TestableRegistry(null as any);
+ tieredRegistry.loadPatterns([multiTierModel]);
+
+ const result = tieredRegistry.calculateCost("gemini-2.5-pro", {
+ input: 250000,
+ output: 1000,
+ });
+
+ expect(result).not.toBeNull();
+ expect(result!.pricingTierName).toBe("Large Context");
+ expect(result!.inputCost).toBeCloseTo(0.625); // 250000 * 0.0000025
+ });
+
+ it("should fall back to default tier when conditions do not match", () => {
+ const tieredRegistry = new TestableRegistry(null as any);
+ tieredRegistry.loadPatterns([multiTierModel]);
+
+ const result = tieredRegistry.calculateCost("gemini-2.5-pro", {
+ input: 1000,
+ output: 100,
+ });
+
+ expect(result).not.toBeNull();
+ expect(result!.pricingTierName).toBe("Standard");
+ expect(result!.inputCost).toBeCloseTo(0.00125); // 1000 * 0.00000125
+ });
+
+ it("should not let unconditional tier win over conditional match", () => {
+ // Model where unconditional tier has lower priority than conditional
+ const model: LlmModelWithPricing = {
+ ...multiTierModel,
+ pricingTiers: [
+ {
+ id: "tier-unconditional",
+ name: "Unconditional",
+ isDefault: false,
+ priority: 0,
+ conditions: [],
+ prices: [{ usageType: "input", price: 0.001 }],
+ },
+ {
+ id: "tier-conditional",
+ name: "Conditional",
+ isDefault: false,
+ priority: 1,
+ conditions: [
+ { usageDetailPattern: "input", operator: "gt" as const, value: 100 },
+ ],
+ prices: [{ usageType: "input", price: 0.0001 }],
+ },
+ {
+ id: "tier-default",
+ name: "Default",
+ isDefault: true,
+ priority: 2,
+ conditions: [],
+ prices: [{ usageType: "input", price: 0.01 }],
+ },
+ ],
+ };
+
+ const tieredRegistry = new TestableRegistry(null as any);
+ tieredRegistry.loadPatterns([model]);
+
+ // Condition matches — conditional tier should win, not the unconditional one
+ const result = tieredRegistry.calculateCost("gemini-2.5-pro", { input: 500 });
+ expect(result).not.toBeNull();
+ expect(result!.pricingTierName).toBe("Conditional");
+ });
+
+ it("should fall back to isDefault tier when no conditions match", () => {
+ const model: LlmModelWithPricing = {
+ ...multiTierModel,
+ pricingTiers: [
+ {
+ id: "tier-conditional",
+ name: "Conditional",
+ isDefault: false,
+ priority: 0,
+ conditions: [
+ { usageDetailPattern: "input", operator: "gt" as const, value: 999999 },
+ ],
+ prices: [{ usageType: "input", price: 0.001 }],
+ },
+ {
+ id: "tier-default",
+ name: "Default",
+ isDefault: true,
+ priority: 1,
+ conditions: [],
+ prices: [{ usageType: "input", price: 0.0001 }],
+ },
+ ],
+ };
+
+ const tieredRegistry = new TestableRegistry(null as any);
+ tieredRegistry.loadPatterns([model]);
+
+ const result = tieredRegistry.calculateCost("gemini-2.5-pro", { input: 100 });
+ expect(result).not.toBeNull();
+ expect(result!.pricingTierName).toBe("Default");
+ });
+ });
+
+ describe("defaultModelPrices (Langfuse JSON)", () => {
+ it("should load all models from the JSON file", () => {
+ expect(defaultModelPrices.length).toBeGreaterThan(100);
+ });
+
+ it("should compile all match patterns without errors", () => {
+ const langfuseRegistry = new TestableRegistry(null as any);
+ const models: LlmModelWithPricing[] = defaultModelPrices.map((def, i) => ({
+ id: `test-${i}`,
+ friendlyId: `llm_model_test${i}`,
+ modelName: def.modelName,
+ matchPattern: def.matchPattern,
+ startDate: def.startDate ? new Date(def.startDate) : null,
+ pricingTiers: def.pricingTiers.map((tier, j) => ({
+ id: `tier-${i}-${j}`,
+ name: tier.name,
+ isDefault: tier.isDefault,
+ priority: tier.priority,
+ conditions: tier.conditions,
+ prices: Object.entries(tier.prices).map(([usageType, price]) => ({
+ usageType,
+ price,
+ })),
+ })),
+ }));
+
+ // This should not throw — all 141 patterns should compile
+ expect(() => langfuseRegistry.loadPatterns(models)).not.toThrow();
+ expect(langfuseRegistry.isLoaded).toBe(true);
+ });
+
+ it("should match real-world model names from Langfuse patterns", () => {
+ const langfuseRegistry = new TestableRegistry(null as any);
+ const models: LlmModelWithPricing[] = defaultModelPrices.map((def, i) => ({
+ id: `test-${i}`,
+ friendlyId: `llm_model_test${i}`,
+ modelName: def.modelName,
+ matchPattern: def.matchPattern,
+ startDate: null,
+ pricingTiers: def.pricingTiers.map((tier, j) => ({
+ id: `tier-${i}-${j}`,
+ name: tier.name,
+ isDefault: tier.isDefault,
+ priority: tier.priority,
+ conditions: tier.conditions,
+ prices: Object.entries(tier.prices).map(([usageType, price]) => ({
+ usageType,
+ price,
+ })),
+ })),
+ }));
+ langfuseRegistry.loadPatterns(models);
+
+ // Test real model strings that SDKs send
+ expect(langfuseRegistry.match("gpt-4o")).not.toBeNull();
+ expect(langfuseRegistry.match("gpt-4o-mini")).not.toBeNull();
+ expect(langfuseRegistry.match("claude-sonnet-4-5-20250929")).not.toBeNull();
+ expect(langfuseRegistry.match("claude-sonnet-4-20250514")).not.toBeNull();
+ });
+ });
+});
diff --git a/internal-packages/llm-pricing/src/registry.ts b/internal-packages/llm-pricing/src/registry.ts
new file mode 100644
index 00000000000..e1faaaa169f
--- /dev/null
+++ b/internal-packages/llm-pricing/src/registry.ts
@@ -0,0 +1,209 @@
+import type { PrismaClient, PrismaReplicaClient } from "@trigger.dev/database";
+import type {
+ LlmModelWithPricing,
+ LlmCostResult,
+ LlmPricingTierWithPrices,
+ PricingCondition,
+} from "./types.js";
+
+type CompiledPattern = {
+ regex: RegExp;
+ model: LlmModelWithPricing;
+};
+
+// Convert POSIX-style (?i) inline flag to JS RegExp 'i' flag
+function compilePattern(pattern: string): RegExp {
+ if (pattern.startsWith("(?i)")) {
+ return new RegExp(pattern.slice(4), "i");
+ }
+ return new RegExp(pattern);
+}
+
+export class ModelPricingRegistry {
+ private _prisma: PrismaClient | PrismaReplicaClient;
+ private _patterns: CompiledPattern[] = [];
+ private _exactMatchCache: Map = new Map();
+ private _loaded = false;
+
+ constructor(prisma: PrismaClient | PrismaReplicaClient) {
+ this._prisma = prisma;
+ }
+
+ get isLoaded(): boolean {
+ return this._loaded;
+ }
+
+ async loadFromDatabase(): Promise {
+ const models = await this._prisma.llmModel.findMany({
+ where: { projectId: null },
+ include: {
+ pricingTiers: {
+ include: { prices: true },
+ orderBy: { priority: "asc" },
+ },
+ },
+ orderBy: [{ startDate: "desc" }],
+ });
+
+ const compiled: CompiledPattern[] = [];
+
+ for (const model of models) {
+ try {
+ const regex = compilePattern(model.matchPattern);
+ const tiers: LlmPricingTierWithPrices[] = model.pricingTiers.map((tier) => ({
+ id: tier.id,
+ name: tier.name,
+ isDefault: tier.isDefault,
+ priority: tier.priority,
+ conditions: (tier.conditions as PricingCondition[]) ?? [],
+ prices: tier.prices.map((p) => ({
+ usageType: p.usageType,
+ price: Number(p.price),
+ })),
+ }));
+
+ compiled.push({
+ regex,
+ model: {
+ id: model.id,
+ friendlyId: model.friendlyId,
+ modelName: model.modelName,
+ matchPattern: model.matchPattern,
+ startDate: model.startDate,
+ pricingTiers: tiers,
+ },
+ });
+ } catch {
+ // Skip models with invalid regex patterns
+ console.warn(`Invalid regex pattern for model ${model.modelName}: ${model.matchPattern}`);
+ }
+ }
+
+ this._patterns = compiled;
+ this._exactMatchCache.clear();
+ this._loaded = true;
+ }
+
+ async reload(): Promise {
+ await this.loadFromDatabase();
+ }
+
+ match(responseModel: string): LlmModelWithPricing | null {
+ if (!this._loaded) return null;
+
+ // Check exact match cache
+ const cached = this._exactMatchCache.get(responseModel);
+ if (cached !== undefined) return cached;
+
+ // Iterate compiled regex patterns
+ for (const { regex, model } of this._patterns) {
+ if (regex.test(responseModel)) {
+ this._exactMatchCache.set(responseModel, model);
+ return model;
+ }
+ }
+
+ // Fallback: strip provider prefix (e.g. "mistral/mistral-large-3" → "mistral-large-3")
+ // Gateway and OpenRouter prepend the provider to the model name.
+ if (responseModel.includes("/")) {
+ const stripped = responseModel.split("/").slice(1).join("/");
+ for (const { regex, model } of this._patterns) {
+ if (regex.test(stripped)) {
+ this._exactMatchCache.set(responseModel, model);
+ return model;
+ }
+ }
+ }
+
+ // Cache miss
+ this._exactMatchCache.set(responseModel, null);
+ return null;
+ }
+
+ calculateCost(
+ responseModel: string,
+ usageDetails: Record
+ ): LlmCostResult | null {
+ const model = this.match(responseModel);
+ if (!model) return null;
+
+ const tier = this._matchPricingTier(model.pricingTiers, usageDetails);
+ if (!tier) return null;
+
+ const costDetails: Record = {};
+ let totalCost = 0;
+
+ for (const priceEntry of tier.prices) {
+ const tokenCount = usageDetails[priceEntry.usageType] ?? 0;
+ if (tokenCount === 0) continue;
+ const cost = tokenCount * priceEntry.price;
+ costDetails[priceEntry.usageType] = cost;
+ totalCost += cost;
+ }
+
+ const inputCost = costDetails["input"] ?? 0;
+ const outputCost = costDetails["output"] ?? 0;
+
+ return {
+ matchedModelId: model.friendlyId,
+ matchedModelName: model.modelName,
+ pricingTierId: tier.id,
+ pricingTierName: tier.name,
+ inputCost,
+ outputCost,
+ totalCost,
+ costDetails,
+ };
+ }
+
+ private _matchPricingTier(
+ tiers: LlmPricingTierWithPrices[],
+ usageDetails: Record
+ ): LlmPricingTierWithPrices | null {
+ if (tiers.length === 0) return null;
+
+ // Tiers are sorted by priority ascending (lowest first).
+ // First pass: evaluate tiers that have conditions — first match wins.
+ for (const tier of tiers) {
+ if (tier.conditions.length > 0 && this._evaluateConditions(tier.conditions, usageDetails)) {
+ return tier;
+ }
+ }
+
+ // Second pass: fall back to the default tier, or first tier with no conditions
+ const defaultTier = tiers.find((t) => t.isDefault);
+ if (defaultTier) return defaultTier;
+
+ const unconditional = tiers.find((t) => t.conditions.length === 0);
+ return unconditional ?? tiers[0] ?? null;
+ }
+
+ private _evaluateConditions(
+ conditions: PricingCondition[],
+ usageDetails: Record
+ ): boolean {
+ return conditions.every((condition) => {
+ // Find matching usage detail key
+ const regex = new RegExp(condition.usageDetailPattern);
+ const matchingValue = Object.entries(usageDetails).find(([key]) => regex.test(key));
+ const value = matchingValue?.[1] ?? 0;
+
+ switch (condition.operator) {
+ case "gt":
+ return value > condition.value;
+ case "gte":
+ return value >= condition.value;
+ case "lt":
+ return value < condition.value;
+ case "lte":
+ return value <= condition.value;
+ case "eq":
+ return value === condition.value;
+ case "neq":
+ return value !== condition.value;
+ default:
+ return false;
+ }
+ });
+ }
+}
diff --git a/internal-packages/llm-pricing/src/seed.ts b/internal-packages/llm-pricing/src/seed.ts
new file mode 100644
index 00000000000..d068c62a66d
--- /dev/null
+++ b/internal-packages/llm-pricing/src/seed.ts
@@ -0,0 +1,62 @@
+import type { PrismaClient } from "@trigger.dev/database";
+import { generateFriendlyId } from "@trigger.dev/core/v3/isomorphic";
+import { defaultModelPrices } from "./defaultPrices.js";
+
+export async function seedLlmPricing(prisma: PrismaClient): Promise<{
+ modelsCreated: number;
+ modelsSkipped: number;
+}> {
+ let modelsCreated = 0;
+ let modelsSkipped = 0;
+
+ for (const modelDef of defaultModelPrices) {
+ // Check if this model already exists (don't overwrite admin changes)
+ const existing = await prisma.llmModel.findFirst({
+ where: {
+ projectId: null,
+ modelName: modelDef.modelName,
+ },
+ });
+
+ if (existing) {
+ modelsSkipped++;
+ continue;
+ }
+
+ // Create model + tiers atomically so partial models can't be left behind
+ await prisma.$transaction(async (tx) => {
+ const model = await tx.llmModel.create({
+ data: {
+ friendlyId: generateFriendlyId("llm_model"),
+ modelName: modelDef.modelName.trim(),
+ matchPattern: modelDef.matchPattern,
+ startDate: modelDef.startDate ? new Date(modelDef.startDate) : null,
+ source: "default",
+ },
+ });
+
+ for (const tier of modelDef.pricingTiers) {
+ await tx.llmPricingTier.create({
+ data: {
+ modelId: model.id,
+ name: tier.name,
+ isDefault: tier.isDefault,
+ priority: tier.priority,
+ conditions: tier.conditions,
+ prices: {
+ create: Object.entries(tier.prices).map(([usageType, price]) => ({
+ modelId: model.id,
+ usageType,
+ price,
+ })),
+ },
+ },
+ });
+ }
+ });
+
+ modelsCreated++;
+ }
+
+ return { modelsCreated, modelsSkipped };
+}
diff --git a/internal-packages/llm-pricing/src/types.ts b/internal-packages/llm-pricing/src/types.ts
new file mode 100644
index 00000000000..2deec6246ed
--- /dev/null
+++ b/internal-packages/llm-pricing/src/types.ts
@@ -0,0 +1,54 @@
+import type { Decimal } from "@trigger.dev/database";
+
+export type PricingCondition = {
+ usageDetailPattern: string;
+ operator: "gt" | "gte" | "lt" | "lte" | "eq" | "neq";
+ value: number;
+};
+
+export type LlmPriceEntry = {
+ usageType: string;
+ price: number;
+};
+
+export type LlmPricingTierWithPrices = {
+ id: string;
+ name: string;
+ isDefault: boolean;
+ priority: number;
+ conditions: PricingCondition[];
+ prices: LlmPriceEntry[];
+};
+
+export type LlmModelWithPricing = {
+ id: string;
+ friendlyId: string;
+ modelName: string;
+ matchPattern: string;
+ startDate: Date | null;
+ pricingTiers: LlmPricingTierWithPrices[];
+};
+
+export type LlmCostResult = {
+ matchedModelId: string;
+ matchedModelName: string;
+ pricingTierId: string;
+ pricingTierName: string;
+ inputCost: number;
+ outputCost: number;
+ totalCost: number;
+ costDetails: Record;
+};
+
+export type DefaultModelDefinition = {
+ modelName: string;
+ matchPattern: string;
+ startDate?: string;
+ pricingTiers: Array<{
+ name: string;
+ isDefault: boolean;
+ priority: number;
+ conditions: PricingCondition[];
+ prices: Record;
+ }>;
+};
diff --git a/internal-packages/llm-pricing/tsconfig.json b/internal-packages/llm-pricing/tsconfig.json
new file mode 100644
index 00000000000..c64cf33133b
--- /dev/null
+++ b/internal-packages/llm-pricing/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2019",
+ "lib": ["ES2019", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "moduleDetection": "force",
+ "verbatimModuleSyntax": false,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+ "preserveWatchOutput": true,
+ "skipLibCheck": true,
+ "noEmit": true,
+ "strict": true,
+ "resolveJsonModule": true
+ },
+ "exclude": ["node_modules"]
+}
diff --git a/internal-packages/tsql/src/query/printer.ts b/internal-packages/tsql/src/query/printer.ts
index b6e9547db06..d45002f6715 100644
--- a/internal-packages/tsql/src/query/printer.ts
+++ b/internal-packages/tsql/src/query/printer.ts
@@ -2370,6 +2370,21 @@ export class ClickHousePrinter {
// Try to resolve column names through table context
const resolvedChain = this.resolveFieldChain(chainWithPrefix);
+ // For Map columns, convert dot-notation to bracket syntax:
+ // metadata.user -> metadata['user']
+ if (resolvedChain.length > 1) {
+ const rootColumnSchema = this.resolveFieldToColumnSchema([node.chain[0]]);
+ if (rootColumnSchema?.type.startsWith("Map(")) {
+ const rootCol = this.printIdentifierOrIndex(resolvedChain[0]);
+ const mapKeys = resolvedChain.slice(1);
+ let result = rootCol;
+ for (const key of mapKeys) {
+ result = `${result}[${this.context.addValue(String(key))}]`;
+ }
+ return result;
+ }
+ }
+
// Print each chain element
let result = resolvedChain.map((part) => this.printIdentifierOrIndex(part)).join(".");
diff --git a/internal-packages/tsql/src/query/schema.ts b/internal-packages/tsql/src/query/schema.ts
index 2ea81091aad..00a28382de5 100644
--- a/internal-packages/tsql/src/query/schema.ts
+++ b/internal-packages/tsql/src/query/schema.ts
@@ -23,6 +23,9 @@ export type ClickHouseType =
| "Date32"
| "DateTime"
| "DateTime64"
+ | "DateTime64(3)"
+ | "DateTime64(9)"
+ | "Decimal64(12)"
| "UUID"
| "Bool"
| "JSON"
@@ -281,6 +284,7 @@ export type ColumnFormatType =
| "runId"
| "runStatus"
| "duration"
+ | "durationNs"
| "durationSeconds"
| "costInDollars"
| "cost"
diff --git a/packages/core/src/v3/schemas/style.ts b/packages/core/src/v3/schemas/style.ts
index eab62c5b41b..2f833b800ac 100644
--- a/packages/core/src/v3/schemas/style.ts
+++ b/packages/core/src/v3/schemas/style.ts
@@ -11,11 +11,12 @@ const AccessoryItem = z.object({
text: z.string(),
variant: z.string().optional(),
url: z.string().optional(),
+ icon: z.string().optional(),
});
const Accessory = z.object({
items: z.array(AccessoryItem),
- style: z.enum(["codepath"]).optional(),
+ style: z.enum(["codepath", "pills"]).optional(),
});
export type Accessory = z.infer;
diff --git a/packages/core/src/v3/taskContext/otelProcessors.ts b/packages/core/src/v3/taskContext/otelProcessors.ts
index 096f7c0ce76..1c0958d655d 100644
--- a/packages/core/src/v3/taskContext/otelProcessors.ts
+++ b/packages/core/src/v3/taskContext/otelProcessors.ts
@@ -30,6 +30,12 @@ export class TaskContextSpanProcessor implements SpanProcessor {
span.setAttributes(
flattenAttributes(taskContext.attributes, SemanticInternalAttributes.METADATA)
);
+
+ // Set run tags as a proper array attribute (not flattened) so it arrives
+ // as an OTEL ArrayValue and can be extracted on the server side.
+ if (!taskContext.isRunDisabled && taskContext.ctx.run.tags?.length) {
+ span.setAttribute(SemanticInternalAttributes.RUN_TAGS, taskContext.ctx.run.tags);
+ }
}
if (!isPartialSpan(span) && !skipPartialSpan(span)) {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 88ac6ad5421..854d4215447 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -302,6 +302,9 @@ importers:
'@internal/cache':
specifier: workspace:*
version: link:../../internal-packages/cache
+ '@internal/llm-pricing':
+ specifier: workspace:*
+ version: link:../../internal-packages/llm-pricing
'@internal/redis':
specifier: workspace:*
version: link:../../internal-packages/redis
@@ -1125,6 +1128,15 @@ importers:
specifier: 18.2.69
version: 18.2.69
+ internal-packages/llm-pricing:
+ dependencies:
+ '@trigger.dev/core':
+ specifier: workspace:*
+ version: link:../../packages/core
+ '@trigger.dev/database':
+ specifier: workspace:*
+ version: link:../database
+
internal-packages/otlp-importer:
dependencies:
long: