Skip to content

Commit 1fadbfe

Browse files
Merge pull request #3326 from Agenta-AI/feat/showing-model-metrics-on-playgorund
[feat]: show provider model input/output cost in playground
2 parents eda50bf + c4914c1 commit 1fadbfe

File tree

5 files changed

+229
-56
lines changed

5 files changed

+229
-56
lines changed

sdk/agenta/sdk/assets.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
from typing import Dict, Optional, Tuple
2+
3+
from litellm import cost_calculator
4+
5+
16
supported_llm_models = {
27
"anthropic": [
38
"anthropic/claude-sonnet-4-5",
@@ -206,6 +211,58 @@
206211

207212
providers_list = list(supported_llm_models.keys())
208213

214+
215+
def _get_model_costs(model: str) -> Optional[Tuple[float, float]]:
216+
"""
217+
Get the input and output costs per 1M tokens for a model.
218+
219+
Uses litellm's cost_calculator (same as tracing/inline.py) for consistency.
220+
221+
Args:
222+
model: The model name (e.g., "gpt-4o" or "anthropic/claude-3-opus-20240229")
223+
224+
Returns:
225+
Tuple of (input_cost, output_cost) per 1M tokens, or None if not found.
226+
"""
227+
try:
228+
costs = cost_calculator.cost_per_token(
229+
model=model,
230+
prompt_tokens=1_000_000,
231+
completion_tokens=1_000_000,
232+
)
233+
if costs:
234+
input_cost, output_cost = costs
235+
if input_cost > 0 or output_cost > 0:
236+
return (input_cost, output_cost)
237+
except Exception:
238+
pass
239+
return None
240+
241+
242+
def _build_model_metadata() -> Dict[str, Dict[str, Dict[str, float]]]:
243+
"""
244+
Build metadata dictionary with costs for all supported models.
245+
246+
Returns:
247+
Nested dict: {provider: {model: {"input": cost, "output": cost}}}
248+
"""
249+
metadata: Dict[str, Dict[str, Dict[str, float]]] = {}
250+
251+
for provider, models in supported_llm_models.items():
252+
metadata[provider] = {}
253+
for model in models:
254+
costs = _get_model_costs(model)
255+
if costs:
256+
metadata[provider][model] = {
257+
"input": costs[0],
258+
"output": costs[1],
259+
}
260+
261+
return metadata
262+
263+
264+
model_metadata = _build_model_metadata()
265+
209266
model_to_provider_mapping = {
210267
model: provider
211268
for provider, models in supported_llm_models.items()

sdk/agenta/sdk/types.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from starlette.responses import StreamingResponse
99

1010

11-
from agenta.sdk.assets import supported_llm_models
11+
from agenta.sdk.assets import supported_llm_models, model_metadata
1212
from agenta.client.backend.types import AgentaNodesResponse, AgentaNodeDto
1313

1414

@@ -23,7 +23,11 @@ def MCField( # pylint: disable=invalid-name
2323
) -> Field:
2424
# Pydantic 2.12+ no longer allows post-creation mutation of field properties
2525
if isinstance(choices, dict):
26-
json_extra = {"choices": choices, "x-parameter": "grouped_choice"}
26+
json_extra = {
27+
"choices": choices,
28+
"x-parameter": "grouped_choice",
29+
"x-model-metadata": model_metadata,
30+
}
2731
elif isinstance(choices, list):
2832
json_extra = {"choices": choices, "x-parameter": "choice"}
2933
else:

web/oss/src/components/SelectLLMProvider/index.tsx

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import {useMemo, useRef, useState} from "react"
22

33
import {CaretRight, Plus, X} from "@phosphor-icons/react"
4-
import {Select, Input, Button, Divider, InputRef, Popover} from "antd"
4+
import {Button, Divider, Input, InputRef, Popover, Select, Tooltip, Typography} from "antd"
55
import clsx from "clsx"
66

77
import useLazyEffect from "@/oss/hooks/useLazyEffect"
88
import {useVaultSecret} from "@/oss/hooks/useVaultSecret"
99
import {capitalize} from "@/oss/lib/helpers/utils"
10-
import {SecretDTOProvider, PROVIDER_LABELS} from "@/oss/lib/Types"
10+
import {PROVIDER_LABELS, SecretDTOProvider} from "@/oss/lib/Types"
1111

1212
import LLMIcons from "../LLMIcons"
1313
import Anthropic from "../LLMIcons/assets/Anthropic"
@@ -25,6 +25,7 @@ interface ProviderOption {
2525
label: string
2626
value: string
2727
key?: string
28+
metadata?: Record<string, any>
2829
}
2930

3031
interface ProviderGroup {
@@ -169,6 +170,7 @@ const SelectLLMProvider = ({
169170
label: resolvedLabel,
170171
value: resolvedValue,
171172
key: option?.key ?? resolvedValue,
173+
metadata: option?.metadata,
172174
}
173175
})
174176
.filter(Boolean) as ProviderOption[]) ?? [],
@@ -208,6 +210,68 @@ const SelectLLMProvider = ({
208210
setTimeout(() => setOpen(false), 0)
209211
}
210212

213+
const formatCost = (cost: number) => {
214+
const value = Number(cost)
215+
if (isNaN(value)) return "N/A"
216+
return value < 0.01 ? value.toFixed(4) : value.toFixed(2)
217+
}
218+
219+
const renderTooltipContent = (metadata: Record<string, any>) => (
220+
<div className="flex flex-col gap-0.5">
221+
{(metadata.input !== undefined || metadata.output !== undefined) && (
222+
<>
223+
<div className="flex justify-between gap-4">
224+
<Typography.Text className="text-[10px] text-nowrap">
225+
Input:
226+
</Typography.Text>
227+
<Typography.Text className="text-[10px] text-nowrap">
228+
${formatCost(metadata.input)} / 1M
229+
</Typography.Text>
230+
</div>
231+
<div className="flex justify-between gap-4">
232+
<Typography.Text className="text-[10px] text-nowrap">
233+
Output:{" "}
234+
</Typography.Text>
235+
<Typography.Text className="text-[10px] text-nowrap">
236+
${formatCost(metadata.output)} / 1M
237+
</Typography.Text>
238+
</div>
239+
</>
240+
)}
241+
</div>
242+
)
243+
244+
const renderOptionContent = (option: ProviderOption) => {
245+
const Icon = getProviderIcon(option.value) || LLMIcons[option.label]
246+
return (
247+
<div className="flex items-center gap-2 w-full justify-between group h-full">
248+
<div className="flex items-center gap-2 overflow-hidden w-full">
249+
{Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
250+
<span className="truncate">{option.label}</span>
251+
</div>
252+
</div>
253+
)
254+
}
255+
256+
const renderOption = (option: ProviderOption) => {
257+
const content = renderOptionContent(option)
258+
259+
if (option.metadata) {
260+
return (
261+
<Tooltip
262+
title={renderTooltipContent(option.metadata)}
263+
placement="right"
264+
mouseEnterDelay={0.3}
265+
color="white"
266+
>
267+
{content}
268+
</Tooltip>
269+
)
270+
}
271+
272+
return content
273+
}
274+
211275
return (
212276
<>
213277
<Select
@@ -225,6 +289,7 @@ const SelectLLMProvider = ({
225289
placeholder="Select a provider"
226290
style={{width: "100%"}}
227291
virtual={false}
292+
optionLabelProp="label"
228293
className={clsx([
229294
"[&_.ant-select-item-option-content]:flex [&_.ant-select-item-option-content]:items-center [&_.ant-select-item-option-content]:gap-2 [&_.ant-select-selection-item]:!flex [&_.ant-select-selection-item]:!items-center [&_.ant-select-selection-item]:!gap-2",
230295
className,
@@ -292,10 +357,7 @@ const SelectLLMProvider = ({
292357
handleSelect(option.value)
293358
}}
294359
>
295-
{Icon && (
296-
<Icon className="w-4 h-4 flex-shrink-0" />
297-
)}
298-
<span>{option.label}</span>
360+
{renderOption(option)}
299361
</div>
300362
))}
301363
</div>
@@ -369,27 +431,26 @@ const SelectLLMProvider = ({
369431
}
370432
>
371433
{group.options?.map((option) => {
372-
const Icon =
373-
getProviderIcon(group.label || "") || LLMIcons[option.label]
374434
return (
375-
<Option key={option.key ?? option.value} value={option.value}>
376-
<div className="flex items-center gap-2">
377-
{Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
378-
<span>{option.label}</span>
379-
</div>
435+
<Option
436+
key={option.key ?? option.value}
437+
value={option.value}
438+
label={renderOptionContent(option)}
439+
>
440+
{renderOption(option)}
380441
</Option>
381442
)
382443
})}
383444
</OptGroup>
384445
) : (
385446
group.options?.map((option) => {
386-
const Icon = getProviderIcon(option.value) || LLMIcons[option.label]
387447
return (
388-
<Option key={option.key ?? option.value} value={option.value}>
389-
<div className="flex items-center gap-2">
390-
{Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
391-
<span>{option.label}</span>
392-
</div>
448+
<Option
449+
key={option.key ?? option.value}
450+
value={option.value}
451+
label={renderOptionContent(option)}
452+
>
453+
{renderOption(option)}
393454
</Option>
394455
)
395456
})

0 commit comments

Comments
 (0)