diff --git a/api/pyproject.toml b/api/pyproject.toml index 049c6c83fe..d635fc77dd 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "api" -version = "0.72.3" +version = "0.72.4" description = "Agenta API" authors = [ { name = "Mahmoud Mabrouk", email = "mahmoud@agenta.ai" }, diff --git a/sdk/agenta/sdk/assets.py b/sdk/agenta/sdk/assets.py index 4457ab357b..a584371842 100644 --- a/sdk/agenta/sdk/assets.py +++ b/sdk/agenta/sdk/assets.py @@ -1,3 +1,8 @@ +from typing import Dict, Optional, Tuple + +from litellm import cost_calculator + + supported_llm_models = { "anthropic": [ "anthropic/claude-sonnet-4-5", @@ -206,6 +211,58 @@ providers_list = list(supported_llm_models.keys()) + +def _get_model_costs(model: str) -> Optional[Tuple[float, float]]: + """ + Get the input and output costs per 1M tokens for a model. + + Uses litellm's cost_calculator (same as tracing/inline.py) for consistency. + + Args: + model: The model name (e.g., "gpt-4o" or "anthropic/claude-3-opus-20240229") + + Returns: + Tuple of (input_cost, output_cost) per 1M tokens, or None if not found. + """ + try: + costs = cost_calculator.cost_per_token( + model=model, + prompt_tokens=1_000_000, + completion_tokens=1_000_000, + ) + if costs: + input_cost, output_cost = costs + if input_cost > 0 or output_cost > 0: + return (input_cost, output_cost) + except Exception: + pass + return None + + +def _build_model_metadata() -> Dict[str, Dict[str, Dict[str, float]]]: + """ + Build metadata dictionary with costs for all supported models. + + Returns: + Nested dict: {provider: {model: {"input": cost, "output": cost}}} + """ + metadata: Dict[str, Dict[str, Dict[str, float]]] = {} + + for provider, models in supported_llm_models.items(): + metadata[provider] = {} + for model in models: + costs = _get_model_costs(model) + if costs: + metadata[provider][model] = { + "input": costs[0], + "output": costs[1], + } + + return metadata + + +model_metadata = _build_model_metadata() + model_to_provider_mapping = { model: provider for provider, models in supported_llm_models.items() diff --git a/sdk/agenta/sdk/middleware/config.py b/sdk/agenta/sdk/middleware/config.py index cda301ff07..98d960a411 100644 --- a/sdk/agenta/sdk/middleware/config.py +++ b/sdk/agenta/sdk/middleware/config.py @@ -224,6 +224,7 @@ async def _parse_variant_ref( baggage.get("ag.refs.variant.slug") # ALTERNATIVE or request.query_params.get("variant_slug") + or body.get("variant_slug") # LEGACY or baggage.get("variant_slug") or request.query_params.get("config") @@ -234,6 +235,7 @@ async def _parse_variant_ref( baggage.get("ag.refs.variant.version") # ALTERNATIVE or request.query_params.get("variant_version") + or body.get("variant_version") # LEGACY or baggage.get("variant_version") ) @@ -244,7 +246,7 @@ async def _parse_variant_ref( return Reference( id=variant_id, slug=variant_slug, - version=variant_version, + version=str(variant_version) if variant_version is not None else None, ) async def _parse_environment_ref( diff --git a/sdk/agenta/sdk/types.py b/sdk/agenta/sdk/types.py index f5953a142f..243886ac31 100644 --- a/sdk/agenta/sdk/types.py +++ b/sdk/agenta/sdk/types.py @@ -8,7 +8,7 @@ from starlette.responses import StreamingResponse -from agenta.sdk.assets import supported_llm_models +from agenta.sdk.assets import supported_llm_models, model_metadata from agenta.client.backend.types import AgentaNodesResponse, AgentaNodeDto @@ -23,7 +23,11 @@ def MCField( # pylint: disable=invalid-name ) -> Field: # Pydantic 2.12+ no longer allows post-creation mutation of field properties if isinstance(choices, dict): - json_extra = {"choices": choices, "x-parameter": "grouped_choice"} + json_extra = { + "choices": choices, + "x-parameter": "grouped_choice", + "x-model-metadata": model_metadata, + } elif isinstance(choices, list): json_extra = {"choices": choices, "x-parameter": "choice"} else: diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index 52b4199539..dcb029f9d8 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "agenta" -version = "0.72.3" +version = "0.72.4" description = "The SDK for agenta is an open-source LLMOps platform." readme = "README.md" authors = [ diff --git a/web/ee/package.json b/web/ee/package.json index 4ad070355a..d65072a444 100644 --- a/web/ee/package.json +++ b/web/ee/package.json @@ -1,6 +1,6 @@ { "name": "@agenta/ee", - "version": "0.72.3", + "version": "0.72.4", "private": true, "engines": { "node": ">=18" diff --git a/web/oss/package.json b/web/oss/package.json index 7d2b91f069..2120d78cda 100644 --- a/web/oss/package.json +++ b/web/oss/package.json @@ -1,6 +1,6 @@ { "name": "@agenta/oss", - "version": "0.72.3", + "version": "0.72.4", "private": true, "engines": { "node": ">=18" diff --git a/web/oss/src/code_snippets/endpoints/fetch_config/python.ts b/web/oss/src/code_snippets/endpoints/fetch_config/python.ts index ff594c9c64..3be4fb23c9 100644 --- a/web/oss/src/code_snippets/endpoints/fetch_config/python.ts +++ b/web/oss/src/code_snippets/endpoints/fetch_config/python.ts @@ -1,18 +1,17 @@ import {getEnv} from "@/oss/lib/helpers/dynamicEnv" export default function pythonCode(appName: string, env_name: string, apiKey: string): string { - return ` -import os + return `import os import agenta as ag -os.environ["AGENTA_API_KEY"] = "${apiKey}" # Add your API key here +os.environ["AGENTA_API_KEY"] = "${apiKey}" os.environ["AGENTA_HOST"] = "${getEnv("NEXT_PUBLIC_AGENTA_API_URL")}" ag.init() config = ag.ConfigManager.get_from_registry( app_slug="${appName}", - environment_slug="${env_name}" - ) + environment_slug="${env_name}", +) print(config) ` } diff --git a/web/oss/src/code_snippets/endpoints/fetch_config/typescript.ts b/web/oss/src/code_snippets/endpoints/fetch_config/typescript.ts index b6b134ce9d..39e742436d 100644 --- a/web/oss/src/code_snippets/endpoints/fetch_config/typescript.ts +++ b/web/oss/src/code_snippets/endpoints/fetch_config/typescript.ts @@ -22,8 +22,8 @@ const getConfig = async (appName: string, environmentSlug: string) => { }, }, { headers: { - 'Content-Type': 'application/json', - 'Authorization': "ApiKey ${apiKey}", // Add your API key here + 'Content-Type': 'application/json', + 'Authorization': "ApiKey ${apiKey}", }, }); diff --git a/web/oss/src/code_snippets/endpoints/fetch_variant/curl.ts b/web/oss/src/code_snippets/endpoints/fetch_variant/curl.ts index 35cdc05cb7..b950a62422 100644 --- a/web/oss/src/code_snippets/endpoints/fetch_variant/curl.ts +++ b/web/oss/src/code_snippets/endpoints/fetch_variant/curl.ts @@ -1,13 +1,14 @@ +import {getEnv} from "@/oss/lib/helpers/dynamicEnv" + export const buildCurlSnippet = ( appSlug: string, variantSlug: string, variantVersion: number, apiKey: string, ) => { - return `# Fetch configuration by variant -curl -X POST "https://cloud.agenta.ai/api/variants/configs/fetch" \\ + return `curl -X POST "${getEnv("NEXT_PUBLIC_AGENTA_API_URL")}/variants/configs/fetch" \\ -H "Content-Type: application/json" \\ - -H "Authorization: Bearer ${apiKey}" \\ + -H "Authorization: ApiKey ${apiKey}" \\ -d '{ "variant_ref": { "slug": "${variantSlug}", diff --git a/web/oss/src/code_snippets/endpoints/fetch_variant/python.ts b/web/oss/src/code_snippets/endpoints/fetch_variant/python.ts index c6c744f3ce..ac86c5d77f 100644 --- a/web/oss/src/code_snippets/endpoints/fetch_variant/python.ts +++ b/web/oss/src/code_snippets/endpoints/fetch_variant/python.ts @@ -3,13 +3,12 @@ export const buildPythonSnippet = ( variantSlug: string, variantVersion: number, ) => { - return `# Fetch configuration by variant -import agenta as ag + return `import agenta as ag config = ag.ConfigManager.get_from_registry( app_slug="${appSlug}", variant_slug="${variantSlug}", - variant_version=${variantVersion} # Optional: If not provided, fetches the latest version + variant_version=${variantVersion}, ) print("Fetched configuration:") diff --git a/web/oss/src/code_snippets/endpoints/fetch_variant/typescript.ts b/web/oss/src/code_snippets/endpoints/fetch_variant/typescript.ts index 122581f586..0953ac4f7d 100644 --- a/web/oss/src/code_snippets/endpoints/fetch_variant/typescript.ts +++ b/web/oss/src/code_snippets/endpoints/fetch_variant/typescript.ts @@ -1,32 +1,33 @@ +import {getEnv} from "@/oss/lib/helpers/dynamicEnv" + export const buildTypescriptSnippet = ( appSlug: string, variantSlug: string, variantVersion: number, apiKey: string, ) => { - return `// Fetch configuration by variant - const fetchResponse = await fetch('https://cloud.agenta.ai/api/variants/configs/fetch', { + return `const fetchResponse = await fetch('${getEnv("NEXT_PUBLIC_AGENTA_API_URL")}/variants/configs/fetch', { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': 'Bearer ${apiKey}' + 'Authorization': 'ApiKey ${apiKey}', }, body: JSON.stringify({ variant_ref: { - slug: '${variantSlug}', - version: ${variantVersion}, - id: null + slug: '${variantSlug}', + version: ${variantVersion}, + id: null, }, application_ref: { - slug: '${appSlug}', - version: null, - id: null - } - }) - }); + slug: '${appSlug}', + version: null, + id: null, + }, + }), +}); - const config = await fetchResponse.json(); - console.log('Fetched configuration:'); - console.log(config); - ` +const config = await fetchResponse.json(); +console.log('Fetched configuration:'); +console.log(config); +` } diff --git a/web/oss/src/code_snippets/endpoints/invoke_llm_app/curl.ts b/web/oss/src/code_snippets/endpoints/invoke_llm_app/curl.ts index 5169dcf0e2..12106a466c 100644 --- a/web/oss/src/code_snippets/endpoints/invoke_llm_app/curl.ts +++ b/web/oss/src/code_snippets/endpoints/invoke_llm_app/curl.ts @@ -2,10 +2,9 @@ export default function cURLCode(uri: string, params: string, apiKey: string): s const parsedParams = JSON.parse(params) const isChat = parsedParams.messages !== undefined - return `# Add your API key to the Authorization header -curl -X POST "${uri}" \\ + return `curl -X POST "${uri}" \\ -H "Content-Type: application/json" \\ --H "Authorization: ApiKey ${apiKey}" \\${isChat ? '\n-H "Baggage: ag.session.id=your_session_id" \\ # Optional: track chat sessions' : ""} +-H "Authorization: ApiKey ${apiKey}" \\${isChat ? '\n-H "Baggage: ag.session.id=your_session_id" \\' : ""} -d '${params}' ` } diff --git a/web/oss/src/code_snippets/endpoints/invoke_llm_app/python.ts b/web/oss/src/code_snippets/endpoints/invoke_llm_app/python.ts index d8eca52722..897deb0352 100644 --- a/web/oss/src/code_snippets/endpoints/invoke_llm_app/python.ts +++ b/web/oss/src/code_snippets/endpoints/invoke_llm_app/python.ts @@ -8,8 +8,8 @@ import json url = "${uri}" params = ${params} headers = { - "Content-Type": "application/json", - "Authorization": "ApiKey ${apiKey}", # Add your API key here${isChat ? '\n "Baggage": "ag.session.id=your_session_id", # Optional: track chat sessions' : ""} + "Content-Type": "application/json", + "Authorization": "ApiKey ${apiKey}",${isChat ? '\n "Baggage": "ag.session.id=your_session_id",' : ""} } response = requests.post(url, json=params, headers=headers) diff --git a/web/oss/src/code_snippets/endpoints/invoke_llm_app/typescript.ts b/web/oss/src/code_snippets/endpoints/invoke_llm_app/typescript.ts index d3ab9f8b4e..fa2fdeccd0 100644 --- a/web/oss/src/code_snippets/endpoints/invoke_llm_app/typescript.ts +++ b/web/oss/src/code_snippets/endpoints/invoke_llm_app/typescript.ts @@ -11,7 +11,7 @@ const generate = async () => { const data = ${params}; const headers = { "Content-Type": "application/json", - "Authorization": "ApiKey ${apiKey}", // Add your API key here${isChat ? '\n "Baggage": "ag.session.id=your_session_id" // Optional: track chat sessions' : ""} + "Authorization": "ApiKey ${apiKey}",${isChat ? '\n "Baggage": "ag.session.id=your_session_id",' : ""} }; const response = await axios.post(url, data, { headers }); diff --git a/web/oss/src/components/DeploymentsDashboard/assets/VariantUseApiContent.tsx b/web/oss/src/components/DeploymentsDashboard/assets/VariantUseApiContent.tsx index 915b8a7396..b68f4fdf08 100644 --- a/web/oss/src/components/DeploymentsDashboard/assets/VariantUseApiContent.tsx +++ b/web/oss/src/components/DeploymentsDashboard/assets/VariantUseApiContent.tsx @@ -1,19 +1,23 @@ import {useCallback, useEffect, useMemo, useState} from "react" import {PythonOutlined} from "@ant-design/icons" -import {FileCodeIcon, FileTsIcon} from "@phosphor-icons/react" -import {Tabs, Typography} from "antd" +import {FileCode, FileTs} from "@phosphor-icons/react" +import {Spin, Tabs, Typography} from "antd" import {useAtomValue} from "jotai" import dynamic from "next/dynamic" import {buildCurlSnippet} from "@/oss/code_snippets/endpoints/fetch_variant/curl" import {buildPythonSnippet} from "@/oss/code_snippets/endpoints/fetch_variant/python" import {buildTypescriptSnippet} from "@/oss/code_snippets/endpoints/fetch_variant/typescript" -import CopyButton from "@/oss/components/CopyButton/CopyButton" -import CodeBlock from "@/oss/components/DynamicCodeBlock/CodeBlock" +import invokeLlmAppcURLCode from "@/oss/code_snippets/endpoints/invoke_llm_app/curl" +import invokeLlmApppythonCode from "@/oss/code_snippets/endpoints/invoke_llm_app/python" +import invokeLlmApptsCode from "@/oss/code_snippets/endpoints/invoke_llm_app/typescript" +import LanguageCodeBlock from "@/oss/components/pages/overview/deployments/DeploymentDrawer/assets/LanguageCodeBlock" import SelectVariant from "@/oss/components/Playground/Components/Menus/SelectVariant" import VariantDetailsWithStatus from "@/oss/components/VariantDetailsWithStatus" -import {currentAppAtom} from "@/oss/state/app" +import {useAppId} from "@/oss/hooks/useAppId" +import {currentAppAtom, useURI} from "@/oss/state/app" +import {stablePromptVariablesAtomFamily} from "@/oss/state/newPlayground/core/prompts" import {revisionsByVariantIdAtomFamily, variantsAtom} from "@/oss/state/variant/atoms/fetcher" import { latestRevisionInfoByVariantIdAtomFamily, @@ -29,13 +33,8 @@ interface VariantUseApiContentProps { initialRevisionId?: string } -interface CodeSnippets { - python: string - typescript: string - bash: string -} - const VariantUseApiContent = ({initialRevisionId}: VariantUseApiContentProps) => { + const appId = useAppId() const variants = useAtomValue(variantsAtom) const revisionList = useAtomValue(revisionListAtom) const currentApp = useAtomValue(currentAppAtom) @@ -45,6 +44,15 @@ const VariantUseApiContent = ({initialRevisionId}: VariantUseApiContentProps) => const [selectedLang, setSelectedLang] = useState("python") const [apiKeyValue, setApiKeyValue] = useState("") + // Get URI for the selected variant + const {data: uri, isLoading: isUriQueryLoading} = useURI(appId, selectedVariantId) + const isLoading = Boolean(selectedVariantId) && isUriQueryLoading + + // Get variable names for the selected revision + const variableNames = useAtomValue( + stablePromptVariablesAtomFamily(selectedRevisionId || ""), + ) as string[] + const initialRevision = useMemo( () => revisionList.find((rev) => rev.id === initialRevisionId), [initialRevisionId, revisionList], @@ -120,13 +128,52 @@ const VariantUseApiContent = ({initialRevisionId}: VariantUseApiContentProps) => const variantSlug = (selectedVariant as any)?.variantSlug || selectedVariant?.variantName || - selectedRevision?.variantName || + (selectedRevision as any)?.variantName || "my-variant-slug" const variantVersion = selectedRevision?.revision ?? latestRevision?.revision ?? 1 const appSlug = (currentApp as any)?.app_slug || currentApp?.app_name || "my-app-slug" const apiKey = apiKeyValue || "YOUR_API_KEY" - const codeSnippets: CodeSnippets = useMemo( + const invokeLlmUrl = uri ?? "" + + // Build params for invoke LLM (with variant refs instead of environment) + const params = useMemo(() => { + const synthesized = variableNames.map((name) => ({name, input: name === "messages"})) + + const mainParams: Record = {} + const secondaryParams: Record = {} + + synthesized.forEach((item) => { + if (item.input) { + mainParams[item.name] = "add_a_value" + } else { + secondaryParams[item.name] = "add_a_value" + } + }) + + const hasMessagesParam = synthesized.some((p) => p?.name === "messages") + const isChat = currentApp?.app_type === "chat" || hasMessagesParam + if (isChat) { + mainParams["messages"] = [ + { + role: "user", + content: "", + }, + ] + mainParams["inputs"] = secondaryParams + } else if (Object.keys(secondaryParams).length > 0) { + mainParams["inputs"] = secondaryParams + } + + // Use variant refs instead of environment + mainParams["app"] = appSlug + mainParams["variant_slug"] = variantSlug + mainParams["variant_version"] = variantVersion + + return JSON.stringify(mainParams, null, 2) + }, [variableNames, currentApp?.app_type, appSlug, variantSlug, variantVersion]) + + const fetchConfigCodeSnippet = useMemo( () => ({ python: buildPythonSnippet(appSlug, variantSlug, variantVersion), typescript: buildTypescriptSnippet(appSlug, variantSlug, variantVersion, apiKey), @@ -135,48 +182,48 @@ const VariantUseApiContent = ({initialRevisionId}: VariantUseApiContentProps) => [apiKey, appSlug, variantSlug, variantVersion], ) - const renderTabChildren = useCallback(() => { - const activeSnippet = codeSnippets[selectedLang as keyof CodeSnippets] + const invokeLlmAppCodeSnippet = useMemo( + () => ({ + python: invokeLlmApppythonCode(invokeLlmUrl, params, apiKeyValue || "x.xxxxxxxx"), + bash: invokeLlmAppcURLCode(invokeLlmUrl, params, apiKeyValue || "x.xxxxxxxx"), + typescript: invokeLlmApptsCode(invokeLlmUrl, params, apiKeyValue || "x.xxxxxxxx"), + }), + [apiKeyValue, invokeLlmUrl, params], + ) + const renderTabChildren = useCallback(() => { return ( -
-
- Use API - -
- -
+ + {}} + invokeLlmUrl={invokeLlmUrl} + /> + ) - }, [ - apiKeyValue, - codeSnippets, - revisionList, - selectedLang, - selectedRevision?.id, - selectedRevision?.isLatestRevision, - selectedRevision?.revision, - selectedRevisionId, - ]) + }, [fetchConfigCodeSnippet, invokeLlmAppCodeSnippet, invokeLlmUrl, isLoading, selectedLang]) const tabItems = useMemo( () => [ { key: "python", label: "Python", - icon: , children: renderTabChildren(), + icon: , }, { key: "typescript", label: "TypeScript", - icon: , children: renderTabChildren(), + icon: , }, { key: "bash", label: "cURL", - icon: , children: renderTabChildren(), + icon: , }, ], [renderTabChildren], @@ -221,10 +268,10 @@ const VariantUseApiContent = ({initialRevisionId}: VariantUseApiContentProps) => ) diff --git a/web/oss/src/components/SelectLLMProvider/index.tsx b/web/oss/src/components/SelectLLMProvider/index.tsx index 15bbb04df7..3a0bf30556 100644 --- a/web/oss/src/components/SelectLLMProvider/index.tsx +++ b/web/oss/src/components/SelectLLMProvider/index.tsx @@ -1,13 +1,13 @@ import {useMemo, useRef, useState} from "react" import {CaretRight, Plus, X} from "@phosphor-icons/react" -import {Select, Input, Button, Divider, InputRef, Popover} from "antd" +import {Button, Divider, Input, InputRef, Popover, Select, Tooltip, Typography} from "antd" import clsx from "clsx" import useLazyEffect from "@/oss/hooks/useLazyEffect" import {useVaultSecret} from "@/oss/hooks/useVaultSecret" import {capitalize} from "@/oss/lib/helpers/utils" -import {SecretDTOProvider, PROVIDER_LABELS} from "@/oss/lib/Types" +import {PROVIDER_LABELS, SecretDTOProvider} from "@/oss/lib/Types" import LLMIcons from "../LLMIcons" import Anthropic from "../LLMIcons/assets/Anthropic" @@ -25,6 +25,7 @@ interface ProviderOption { label: string value: string key?: string + metadata?: Record } interface ProviderGroup { @@ -169,6 +170,7 @@ const SelectLLMProvider = ({ label: resolvedLabel, value: resolvedValue, key: option?.key ?? resolvedValue, + metadata: option?.metadata, } }) .filter(Boolean) as ProviderOption[]) ?? [], @@ -208,6 +210,68 @@ const SelectLLMProvider = ({ setTimeout(() => setOpen(false), 0) } + const formatCost = (cost: number) => { + const value = Number(cost) + if (isNaN(value)) return "N/A" + return value < 0.01 ? value.toFixed(4) : value.toFixed(2) + } + + const renderTooltipContent = (metadata: Record) => ( +
+ {(metadata.input !== undefined || metadata.output !== undefined) && ( + <> +
+ + Input: + + + ${formatCost(metadata.input)} / 1M + +
+
+ + Output:{" "} + + + ${formatCost(metadata.output)} / 1M + +
+ + )} +
+ ) + + const renderOptionContent = (option: ProviderOption) => { + const Icon = getProviderIcon(option.value) || LLMIcons[option.label] + return ( +
+
+ {Icon && } + {option.label} +
+
+ ) + } + + const renderOption = (option: ProviderOption) => { + const content = renderOptionContent(option) + + if (option.metadata) { + return ( + + {content} + + ) + } + + return content + } + return ( <>