Browse Source

feat(ai-monitoring): Add cost information to the AI monitoring screens (#70381)

colin-sentry 10 months ago
parent
commit
81a99f9055

+ 4 - 0
static/app/utils/discover/fields.tsx

@@ -127,6 +127,10 @@ export type PercentageUnit = 'percentage';
 
 export type PercentChangeUnit = 'percent_change';
 
+export enum CurrencyUnit {
+  USD = 'usd',
+}
+
 export enum DurationUnit {
   NANOSECOND = 'nanosecond',
   MICROSECOND = 'microsecond',

+ 63 - 18
static/app/views/aiMonitoring/PipelinesTable.tsx

@@ -9,6 +9,8 @@ import Link from 'sentry/components/links/link';
 import type {CursorHandler} from 'sentry/components/pagination';
 import Pagination from 'sentry/components/pagination';
 import SearchBar from 'sentry/components/searchBar';
+import {Tooltip} from 'sentry/components/tooltip';
+import {IconInfo} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {Organization} from 'sentry/types/organization';
@@ -37,10 +39,15 @@ type Row = Pick<
   | 'avg(span.duration)'
   | 'sum(span.duration)'
   | 'ai_total_tokens_used()'
+  | 'ai_total_tokens_used(c:spans/ai.total_cost@none)'
 >;
 
 type Column = GridColumnHeader<
-  'span.description' | 'spm()' | 'avg(span.duration)' | 'ai_total_tokens_used()'
+  | 'span.description'
+  | 'spm()'
+  | 'avg(span.duration)'
+  | 'ai_total_tokens_used()'
+  | 'ai_total_tokens_used(c:spans/ai.total_cost@none)'
 >;
 
 const COLUMN_ORDER: Column[] = [
@@ -54,6 +61,11 @@ const COLUMN_ORDER: Column[] = [
     name: t('Total tokens used'),
     width: 180,
   },
+  {
+    key: 'ai_total_tokens_used(c:spans/ai.total_cost@none)',
+    name: t('Total cost'),
+    width: 180,
+  },
   {
     key: `avg(span.duration)`,
     name: t('Pipeline Duration'),
@@ -87,6 +99,7 @@ export function PipelinesTable() {
   if (!sort) {
     sort = {field: 'spm()', kind: 'desc'};
   }
+
   const {data, isLoading, meta, pageLinks, error} = useSpanMetrics({
     search: MutableSearch.fromQueryObject({
       'span.category': 'ai.pipeline',
@@ -99,7 +112,6 @@ export function PipelinesTable() {
       'spm()',
       'avg(span.duration)',
       'sum(span.duration)',
-      'ai_total_tokens_used()', // this is zero initially and overwritten below.
     ],
     sorts: [sort],
     limit: 25,
@@ -109,24 +121,37 @@ export function PipelinesTable() {
 
   const {
     data: tokensUsedData,
-    isLoading: tokensUsedLoading,
     error: tokensUsedError,
+    isLoading: tokensUsedLoading,
   } = useSpanMetrics({
-    search: MutableSearch.fromQueryObject({
-      'span.ai.pipeline.group': (data as Row[])?.map(x => x['span.group']).join(','),
-      'span.category': 'ai',
-    }),
-    fields: ['span.ai.pipeline.group', 'ai_total_tokens_used()'],
+    search: new MutableSearch(
+      `span.category:ai span.ai.pipeline.group:[${(data as Row[])?.map(x => x['span.group']).join(',')}]`
+    ),
+    fields: [
+      'span.ai.pipeline.group',
+      'ai_total_tokens_used()',
+      'ai_total_tokens_used(c:spans/ai.total_cost@none)',
+    ],
   });
-  if (!tokensUsedLoading) {
-    for (const tokenUsedRow of tokensUsedData) {
-      const groupId = tokenUsedRow['span.ai.pipeline.group'];
-      const tokensUsed = tokenUsedRow['ai_total_tokens_used()'];
-      data
-        .filter(x => x['span.group'] === groupId)
-        .forEach(x => (x['ai_total_tokens_used()'] = tokensUsed));
+
+  const rows: Row[] = (data as Row[]).map(baseRow => {
+    const row: Row = {
+      ...baseRow,
+      'ai_total_tokens_used()': 0,
+      'ai_total_tokens_used(c:spans/ai.total_cost@none)': 0,
+    };
+    if (!tokensUsedLoading) {
+      const tokenUsedDataPoint = tokensUsedData.find(
+        tokenRow => tokenRow['span.ai.pipeline.group'] === row['span.group']
+      );
+      if (tokenUsedDataPoint) {
+        row['ai_total_tokens_used()'] = tokenUsedDataPoint['ai_total_tokens_used()'];
+        row['ai_total_tokens_used(c:spans/ai.total_cost@none)'] =
+          tokenUsedDataPoint['ai_total_tokens_used(c:spans/ai.total_cost@none)'];
+      }
     }
-  }
+    return row;
+  });
 
   const handleCursor: CursorHandler = (newCursor, pathname, query) => {
     browserHistory.push({
@@ -149,7 +174,7 @@ export function PipelinesTable() {
   return (
     <VisuallyCompleteWithData
       id="PipelinesTable"
-      hasData={data.length > 0}
+      hasData={rows.length > 0}
       isLoading={isLoading}
     >
       <Container>
@@ -161,7 +186,7 @@ export function PipelinesTable() {
         <GridEditable
           isLoading={isLoading}
           error={error ?? tokensUsedError}
-          data={data}
+          data={rows}
           columnOrder={COLUMN_ORDER}
           columnSortBy={[
             {
@@ -212,6 +237,26 @@ function renderBodyCell(
       </Link>
     );
   }
+  if (column.key === 'ai_total_tokens_used(c:spans/ai.total_cost@none)') {
+    const cost = row['ai_total_tokens_used(c:spans/ai.total_cost@none)'];
+    if (cost) {
+      if (cost < 0.01) {
+        return <span>US {cost * 100}¢</span>;
+      }
+      return <span>US${cost}</span>;
+    }
+    return (
+      <span>
+        Unknown{' '}
+        <Tooltip
+          title="Cost can only be calculated for certain OpenAI and Anthropic models, other providers aren't yet supported."
+          isHoverable
+        >
+          <IconInfo size="xs" />
+        </Tooltip>
+      </span>
+    );
+  }
 
   if (!meta || !meta?.fields) {
     return row[column.key];

+ 17 - 2
static/app/views/aiMonitoring/aiMonitoringDetailsPage.tsx

@@ -13,7 +13,7 @@ import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilt
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
-import {DurationUnit, RateUnit} from 'sentry/utils/discover/fields';
+import {CurrencyUnit, DurationUnit, RateUnit} from 'sentry/utils/discover/fields';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import useOrganization from 'sentry/utils/useOrganization';
 import {normalizeUrl} from 'sentry/utils/withDomainRequired';
@@ -73,7 +73,10 @@ export default function AiMonitoringPage({params}: Props) {
       'span.category': 'ai',
       'span.ai.pipeline.group': groupId,
     }),
-    fields: ['ai_total_tokens_used()'],
+    fields: [
+      'ai_total_tokens_used()',
+      'ai_total_tokens_used(c:spans/ai.total_cost@none)',
+    ],
     enabled: Boolean(groupId),
     referrer: 'api.ai-pipelines.view',
   });
@@ -130,6 +133,17 @@ export default function AiMonitoringPage({params}: Props) {
                             isLoading={isTotalTokenDataLoading}
                           />
 
+                          <MetricReadout
+                            title={t('Total Cost')}
+                            value={
+                              tokenUsedMetric[
+                                'ai_total_tokens_used(c:spans/ai.total_cost@none)'
+                              ]
+                            }
+                            unit={CurrencyUnit.USD}
+                            isLoading={isTotalTokenDataLoading}
+                          />
+
                           <MetricReadout
                             title={t('Pipeline Duration')}
                             value={
@@ -175,6 +189,7 @@ const SpaceBetweenWrap = styled('div')`
   display: flex;
   justify-content: space-between;
   flex-wrap: wrap;
+  gap: ${space(2)};
 `;
 
 const MetricsRibbon = styled('div')`

+ 25 - 6
static/app/views/performance/metricReadout.tsx

@@ -8,12 +8,15 @@ import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {PercentChange} from 'sentry/components/percentChange';
 import {Tooltip} from 'sentry/components/tooltip';
 import {defined} from 'sentry/utils';
-import type {
-  CountUnit,
-  PercentageUnit,
-  PercentChangeUnit,
+import {
+  type CountUnit,
+  CurrencyUnit,
+  DurationUnit,
+  type PercentageUnit,
+  type PercentChangeUnit,
+  RateUnit,
+  SizeUnit,
 } from 'sentry/utils/discover/fields';
-import {DurationUnit, RateUnit, SizeUnit} from 'sentry/utils/discover/fields';
 import {
   formatAbbreviatedNumber,
   formatPercentage,
@@ -27,7 +30,8 @@ type Unit =
   | RateUnit
   | CountUnit
   | PercentageUnit
-  | PercentChangeUnit;
+  | PercentChangeUnit
+  | CurrencyUnit;
 
 interface Props {
   title: string;
@@ -101,6 +105,21 @@ function ReadoutContent({unit, value, tooltip, align = 'right', isLoading}: Prop
     );
   }
 
+  if (unit === CurrencyUnit.USD) {
+    const numericValue = typeof value === 'string' ? parseFloat(value) : value;
+    if (numericValue < 0.01) {
+      renderedValue = (
+        <NumberContainer align={align}>US {numericValue * 100}¢</NumberContainer>
+      );
+    } else if (numericValue >= 1) {
+      renderedValue = (
+        <NumberContainer align={align}>
+          US ${formatAbbreviatedNumber(numericValue)}
+        </NumberContainer>
+      );
+    }
+  }
+
   if (unit === 'percentage') {
     renderedValue = (
       <NumberContainer align={align}>

+ 2 - 0
static/app/views/starfish/types.tsx

@@ -131,6 +131,8 @@ export type SpanMetricsResponse = {
   'http_response_rate(3)': number;
   'http_response_rate(4)': number;
   'http_response_rate(5)': number;
+} & {
+  'ai_total_tokens_used(c:spans/ai.total_cost@none)': number;
 } & {
   ['project']: string;
   ['project.id']: number;