diff --git a/agenta-backend/agenta_backend/routers/projects_router.py b/agenta-backend/agenta_backend/routers/projects_router.py index b8ab51e1f6..bbc5041ed7 100644 --- a/agenta-backend/agenta_backend/routers/projects_router.py +++ b/agenta-backend/agenta_backend/routers/projects_router.py @@ -14,6 +14,9 @@ class ProjectsResponse(BaseModel): + organization_id: Optional[UUID] = None + organization_name: Optional[str] = None + # is_default_organization: Optional[bool] = None workspace_id: Optional[UUID] = None workspace_name: Optional[str] = None # is_default_workspace: Optional[bool] = None @@ -21,6 +24,7 @@ class ProjectsResponse(BaseModel): project_name: str # is_default_project: bool user_role: Optional[str] = None + is_demo: Optional[bool] = None router = APIRouter() @@ -64,11 +68,14 @@ async def get_projects( projects = [ ProjectsResponse( + organization_id=project_membership.project.organization.id, + organization_name=project_membership.project.organization.name, workspace_id=project_membership.project.workspace.id, workspace_name=project_membership.project.workspace.name, project_id=project_membership.project.id, project_name=project_membership.project.project_name, user_role=project_membership.role, + is_demo=project_membership.is_demo, ) for project_membership in _project_memberships ] diff --git a/agenta-web/public/assets/On-boarding.webp b/agenta-web/public/assets/On-boarding.webp new file mode 100644 index 0000000000..2562fd4adc Binary files /dev/null and b/agenta-web/public/assets/On-boarding.webp differ diff --git a/agenta-web/src/components/GenericDrawer/index.tsx b/agenta-web/src/components/GenericDrawer/index.tsx index 8209095527..323aa76f5d 100644 --- a/agenta-web/src/components/GenericDrawer/index.tsx +++ b/agenta-web/src/components/GenericDrawer/index.tsx @@ -7,10 +7,12 @@ type GenericDrawerProps = { headerExtra?: ReactNode mainContent: ReactNode sideContent?: ReactNode + initialWidth?: number } & React.ComponentProps const GenericDrawer = ({...props}: GenericDrawerProps) => { - const [drawerWidth, setDrawerWidth] = useState(1200) + const initialWidth = props.initialWidth || 1200 + const [drawerWidth, setDrawerWidth] = useState(initialWidth) return ( { {props.expandable && ( + + ) +} + +export default NoMobilePageWrapper diff --git a/agenta-web/src/components/NoMobilePageWrapper/assets/constants.ts b/agenta-web/src/components/NoMobilePageWrapper/assets/constants.ts new file mode 100644 index 0000000000..058d997c1f --- /dev/null +++ b/agenta-web/src/components/NoMobilePageWrapper/assets/constants.ts @@ -0,0 +1,9 @@ +// List of routes where the component should be displayed +export const MOBILE_UNOPTIMIZED_APP_ROUTES = [ + "/apps", + "/observability", + "/settings", + "/testsets", + "/evaluations", + "/workspaces", +] diff --git a/agenta-web/src/components/Sidebar/Sidebar.tsx b/agenta-web/src/components/Sidebar/Sidebar.tsx index a90e43fef6..e3c0c403f2 100644 --- a/agenta-web/src/components/Sidebar/Sidebar.tsx +++ b/agenta-web/src/components/Sidebar/Sidebar.tsx @@ -381,21 +381,18 @@ const Sidebar: React.FC = () => { { key: "logout", label: ( -
{ - AlertPopup({ - title: "Logout", - message: - "Are you sure you want to logout?", - onOk: logout, - }) - }} - > +
Logout
), + onClick: () => { + AlertPopup({ + title: "Logout", + message: "Are you sure you want to logout?", + onOk: logout, + }) + }, }, ], selectedKeys: [selectedOrg.id], diff --git a/agenta-web/src/components/pages/observability/ObservabilityDashboard.tsx b/agenta-web/src/components/pages/observability/ObservabilityDashboard.tsx index eef5893e46..46ac3a2c9c 100644 --- a/agenta-web/src/components/pages/observability/ObservabilityDashboard.tsx +++ b/agenta-web/src/components/pages/observability/ObservabilityDashboard.tsx @@ -14,7 +14,7 @@ import {useAppId} from "@/hooks/useAppId" import {useQueryParam} from "@/hooks/useQuery" import {formatCurrency, formatLatency, formatTokenUsage} from "@/lib/helpers/formatters" import {getNodeById} from "@/lib/helpers/observability_helpers" -import {Filter, FilterConditions, JSSTheme} from "@/lib/Types" +import {Filter, FilterConditions, JSSTheme, KeyValuePair} from "@/lib/Types" import {_AgentaRootsResponse} from "@/services/observability/types" import {ReloadOutlined, SwapOutlined} from "@ant-design/icons" import { @@ -35,12 +35,13 @@ import dayjs from "dayjs" import {useRouter} from "next/router" import React, {useCallback, useEffect, useMemo, useState} from "react" import {createUseStyles} from "react-jss" -import {Export} from "@phosphor-icons/react" +import {Database, Export} from "@phosphor-icons/react" import {getAppValues} from "@/contexts/app.context" import {convertToCsv, downloadCsv} from "@/lib/helpers/fileManipulations" import {useUpdateEffect} from "usehooks-ts" import {getStringOrJson} from "@/lib/helpers/utils" import ObservabilityContextProvider, {useObservabilityData} from "@/contexts/observability.context" +import TestsetDrawer, {TestsetTraceData} from "./drawer/TestsetDrawer" const useStyles = createUseStyles((theme: JSSTheme) => ({ title: { @@ -79,6 +80,8 @@ const ObservabilityDashboard = () => { const [selectedTraceId, setSelectedTraceId] = useQueryParam("trace", "") const [editColumns, setEditColumns] = useState(["span_type", "key", "usage"]) const [isFilterColsDropdownOpen, setIsFilterColsDropdownOpen] = useState(false) + const [selectedRowKeys, setSelectedRowKeys] = useState([]) + const [testsetDrawerData, setTestsetDrawerData] = useState([]) const [columns, setColumns] = useState>([ { title: "ID", @@ -263,6 +266,12 @@ const ObservabilityDashboard = () => { return () => clearInterval(interval) }, []) + const rowSelection = { + onChange: (selectedRowKeys: React.Key[]) => { + setSelectedRowKeys(selectedRowKeys) + }, + } + const selectedItem = useMemo( () => (traces?.length ? getNodeById(traces, selected) : null), [selected, traces], @@ -487,6 +496,19 @@ const ObservabilityDashboard = () => { setSort({type, sorted, customRange}) }, []) + const getTestsetTraceData = () => { + if (!traces?.length) return [] + + const extractData = selectedRowKeys.map((key, idx) => { + const node = getNodeById(traces, key as string) + return {data: node?.data as KeyValuePair, key: node?.key, id: idx + 1} + }) + + if (extractData.length > 0) { + setTestsetDrawerData(extractData as TestsetTraceData[]) + } + } + return (
Observability @@ -536,6 +558,13 @@ const ObservabilityDashboard = () => { > Export as CSV + {
[]} dataSource={traces} @@ -615,6 +650,17 @@ const ObservabilityDashboard = () => { /> + {testsetDrawerData.length > 0 && ( + 0} + data={testsetDrawerData} + onClose={() => { + setTestsetDrawerData([]) + setSelectedRowKeys([]) + }} + /> + )} + {activeTrace && !!traces?.length && ( ({ + editor: { + border: `1px solid ${theme.colorBorder}`, + borderRadius: theme.borderRadius, + overflow: "hidden", + "& .monaco-editor": { + width: "0 !important", + }, + }, + drawerHeading: { + fontSize: theme.fontSizeLG, + lineHeight: theme.lineHeightLG, + fontWeight: theme.fontWeightMedium, + }, + container: { + display: "flex", + flexDirection: "column", + gap: 4, + }, + label: { + fontWeight: theme.fontWeightMedium, + }, +})) + +type Mapping = {data: string; column: string; newColumn?: string} +type Preview = {key: string; data: KeyValuePair[]} +export type TestsetTraceData = {key: string; data: KeyValuePair; id: number} +type Props = { + onClose: () => void + data: TestsetTraceData[] +} & React.ComponentProps + +const TestsetDrawer = ({onClose, data, ...props}: Props) => { + const {appTheme} = useAppTheme() + const classes = useStyles() + const {testsets: listOfTestsets, isTestsetsLoading} = useLoadTestsetsList() + const elemRef = useResizeObserver((rect) => { + setIsDrawerExtended(rect.width > 640) + }) + + const [isDrawerExtended, setIsDrawerExtended] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [traceData, setTraceData] = useState(data.length > 0 ? data : []) + const [testset, setTestset] = useState({name: "", id: ""}) + const [newTestsetName, setNewTestsetName] = useState("") + const [editorFormat, setEditorFormat] = useState("JSON") + const [selectedTestsetColumns, setSelectedTestsetColumns] = useState([]) + const [selectedTestsetRows, setSelectedTestsetRows] = useState([]) + const [showLastFiveRows, setShowLastFiveRows] = useState(false) + const [rowDataPreview, setRowDataPreview] = useState(traceData[0]?.key || "") + const [mappingData, setMappingData] = useState([]) + const [preview, setPreview] = useState({key: traceData[0]?.key || "", data: []}) + const [hasDuplicateColumns, setHasDuplicateColumns] = useState(false) + + const isNewTestset = testset.id === "create" + const elementWidth = isDrawerExtended ? 200 * 2 : 200 + const selectedTestsetTestCases = selectedTestsetRows.slice(-5) + const isMapColumnExist = mappingData.some((mapping) => + mapping.column === "create" || !mapping.column ? !!mapping?.newColumn : !!mapping.column, + ) + + // predefind options + const customSelectOptions = (divider = true) => { + return [ + {value: "create", label: "Create New"}, + ...(divider + ? [ + { + value: "divider", + label: , + className: "!p-0 !m-0 !min-h-0.5 !cursor-default", + disabled: true, + }, + ] + : []), + ] + } + + const onTestsetOptionChange = async (option: {label: string; value: string}) => { + const {value, label} = option + + try { + resetStates() + setTestset({name: label, id: value}) + + if (value && value !== "create") { + const data = await fetchTestset(value) + if (data?.csvdata?.length) { + setSelectedTestsetColumns(Object.keys(data.csvdata[0])) + setSelectedTestsetRows(data.csvdata) + } + } + + if (mappingOptions.length > 0 && value) { + setMappingData((prevMappingData) => + mappingOptions.map((item, index) => ({ + ...prevMappingData[index], + data: item.value, + })), + ) + } + } catch (error) { + message.error("Failed to laod Test sets!") + } + } + + const onRemoveTraceData = () => { + const removeTrace = traceData.filter((trace) => trace.key !== rowDataPreview) + setTraceData(removeTrace) + + if (removeTrace.length > 0) { + const currentIndex = traceData.findIndex((trace) => trace.key === rowDataPreview) + // [currentIndex]: Next option in list | [currentIndex - 1]: Previous option if next doesn't exist | [0]: Default to first option + const nextPreview = + removeTrace[currentIndex] || removeTrace[currentIndex - 1] || removeTrace[0] + + setRowDataPreview(nextPreview.key) + + if (rowDataPreview === preview.key) { + onPreviewOptionChange(nextPreview.key) + } + } else { + setRowDataPreview("") + } + } + + const formatDataPreview = useMemo(() => { + if (!traceData?.length) return "" + + const jsonObject = { + data: + traceData.find((trace) => trace?.key === rowDataPreview)?.data || + traceData[0]?.data, + } + if (!jsonObject) return "" + + try { + return editorFormat === "YAML" ? yaml.dump(jsonObject) : getStringOrJson(jsonObject) + } catch (error) { + message.error("Failed to convert JSON to YAML. Please ensure the data is valid.") + return getStringOrJson(jsonObject) + } + }, [editorFormat, traceData, rowDataPreview]) + + const mappingOptions = useMemo(() => { + const uniquePaths = new Set() + + traceData.forEach((traceItem) => { + const traceKeys = collectKeyPathsFromObject(traceItem?.data, "data") + traceKeys.forEach((key) => uniquePaths.add(key)) + }) + + return Array.from(uniquePaths).map((item) => ({value: item})) + }, [traceData]) + + const columnOptions = useMemo(() => { + const selectedColumns = mappingData + .map((item) => item.column) + .filter((col) => col !== "create") + return selectedTestsetColumns.filter((column) => !selectedColumns.includes(column)) + }, [mappingData, selectedTestsetColumns]) + + const onMappingOptionChange = ({ + pathName, + value, + idx, + }: { + pathName: keyof Mapping + value: string + idx: number + }) => { + setMappingData((prev) => { + const newData = [...prev] + newData[idx] = {...newData[idx], [pathName]: value} + return newData + }) + } + + const onPreviewOptionChange = (value: string) => { + let newTestsetData + if (value === "all") { + newTestsetData = mapAndConvertDataInCsvFormat(traceData, "preview") + } else { + const selectedTraceData = traceData.filter((trace) => trace.key === value) + newTestsetData = mapAndConvertDataInCsvFormat(selectedTraceData, "preview") + } + + setPreview({key: value, data: newTestsetData}) + } + + useUpdateEffect(() => { + const duplicatesExist = hasDuplicateColumnNames() + setHasDuplicateColumns(duplicatesExist) + + if (!duplicatesExist && isMapColumnExist) { + onPreviewOptionChange(preview.key) + } + }, [mappingData]) + + const resetStates = () => { + setSelectedTestsetColumns([]) + setSelectedTestsetRows([]) + setShowLastFiveRows(false) + setMappingData((prev) => prev.map((item) => ({...item, column: "", newColumn: ""}))) + setPreview({key: traceData[0]?.key || "", data: []}) + setNewTestsetName("") + } + + const mapAndConvertDataInCsvFormat = ( + traceData: TestsetTraceData[], + type: "preview" | "export", + ) => { + const formattedData = traceData.map((item) => { + const formattedItem: Record = {} + + for (const mapping of mappingData) { + const keys = mapping.data.split(".") + let value = keys.reduce((acc: any, key) => acc?.[key], item) + + const targetKey = + mapping.column === "create" || !mapping.column + ? mapping.newColumn + : mapping.column + + if (targetKey) { + formattedItem[targetKey] = + value === undefined || value === null + ? "" + : typeof value === "string" + ? value + : JSON.stringify(value) + } + } + + for (const column of selectedTestsetColumns) { + if (!(column in formattedItem)) { + formattedItem[column] = "" + } + } + + return formattedItem + }) + + if (type === "export" && !isNewTestset) { + // add all previous test cases + const allKeys = Array.from(new Set(formattedData.flatMap((item) => Object.keys(item)))) + + selectedTestsetRows.forEach((row) => { + const formattedRow: Record = {} + for (const key of allKeys) { + formattedRow[key] = row[key] ?? "" + } + + formattedData.push(formattedRow) + }) + } + + return formattedData + } + + const onSaveTestset = async () => { + try { + setIsLoading(true) + + const newTestsetData = mapAndConvertDataInCsvFormat(traceData, "export") + + if (isNewTestset) { + if (!newTestsetName) { + message.error("Please add a Test set name before saving it") + return + } + + await createNewTestset(newTestsetName, newTestsetData) + message.success("Test set created successfully") + } else { + await updateTestset(testset.id as string, testset.name, newTestsetData) + message.success("Test set updated successfully") + } + + onClose() + } catch (error) { + console.log(error) + message.error("Something went wrong. Please try again later") + } finally { + setIsLoading(false) + } + } + + const hasDuplicateColumnNames = () => { + const seenValues = new Set() + + return mappingData.some((item) => { + const columnValues = [item.column, item.newColumn] + .filter(Boolean) + .filter((value) => value !== "create") + + return columnValues.some((value) => { + if (seenValues.has(value as string)) return true + seenValues.add(value as string) + return false + }) + }) + } + + const tableColumns = useMemo(() => { + const mappedColumns = mappingData.map((data, idx) => { + const columnData = + data.column === "create" || !data.column ? data.newColumn : data.column + + return { + title: columnData, + dataIndex: columnData, + key: idx, + width: 250, + onHeaderCell: () => ({style: {minWidth: 200}}), + } + }) + + const testsetColumns = showLastFiveRows + ? selectedTestsetColumns.map((item) => ({ + title: item, + dataIndex: item, + key: item, + width: 250, + onHeaderCell: () => ({style: {minWidth: 200}}), + })) + : [] + + // Remove duplicate columns and filter out columns without dataIndex + return [...mappedColumns, ...testsetColumns].filter( + (column, index, self) => + column.dataIndex && + self.findIndex((c) => c.dataIndex === column.dataIndex) === index, + ) + }, [mappingData, selectedTestsetColumns, showLastFiveRows]) + + return ( + <> + + + + + } + mainContent={ +
+ + Spans selected {traceData.length} + + +
+ Test set +
+ setNewTestsetName(e.target.value)} + placeholder="Test set name" + /> + +
+ )} +
+ + +
+ + Data preview + + +
+ + onMappingOptionChange({ + pathName: "data", + value, + idx, + }) + } + options={mappingOptions} + /> + +
+ {!isNewTestset && ( + + onMappingOptionChange({ + pathName: "newColumn", + value: e.target.value, + idx, + }) + } + placeholder="Column name" + /> + +
+ ) : null} +
+ +
+ ))} + + + + + ) : ( + + Please select a test set to create mappings + + )} + + +
+ Preview + {isMapColumnExist ? ( + <> +
+
{ + if (showLastFiveRows) { + const totalRows = + preview.data.length + + selectedTestsetTestCases.length + + if ( + index >= + totalRows - selectedTestsetTestCases.length + ) { + return "!bg-[#fafafa]" + } + } + return "" + }} + scroll={{x: "max-content"}} + bordered + pagination={false} + /> + + + ) : ( + + Please select test set to view test set preview. + + )} + + + } + /> + + ) +} + +export default TestsetDrawer diff --git a/agenta-web/src/components/pages/observability/drawer/TraceContent.tsx b/agenta-web/src/components/pages/observability/drawer/TraceContent.tsx index fab6e3e258..bcc1754a6a 100644 --- a/agenta-web/src/components/pages/observability/drawer/TraceContent.tsx +++ b/agenta-web/src/components/pages/observability/drawer/TraceContent.tsx @@ -1,5 +1,5 @@ import ResultTag from "@/components/ResultTag/ResultTag" -import {JSSTheme} from "@/lib/Types" +import {JSSTheme, KeyValuePair} from "@/lib/Types" import {ArrowRight, Database, PlusCircle, Rocket, Timer} from "@phosphor-icons/react" import {Button, Divider, Space, Tabs, TabsProps, Typography} from "antd" import React, {useState} from "react" @@ -11,6 +11,7 @@ import {statusMapper} from "../components/AvatarTreeContent" import {formatCurrency, formatLatency, formatTokenUsage} from "@/lib/helpers/formatters" import StatusRenderer from "../components/StatusRenderer" import AccordionTreePanel from "../components/AccordionTreePanel" +import TestsetDrawer from "./TestsetDrawer" interface TraceContentProps { activeTrace: _AgentaRootsResponse @@ -85,6 +86,7 @@ const TraceContent = ({activeTrace}: TraceContentProps) => { const classes = useStyles() const [tab, setTab] = useState("overview") const {icon, bgColor, color} = statusMapper(activeTrace.node.type) + const [isTestsetDrawerOpen, setIsTestsetDrawerOpen] = useState(false) const transformDataInputs = (data: any) => { return Object.keys(data).reduce((acc, curr) => { @@ -260,21 +262,16 @@ const TraceContent = ({activeTrace}: TraceContentProps) => { {activeTrace.node.name} - {/* - {!activeTrace.parent && activeTrace.refs?.application?.id && ( - - )} - - */} + @@ -356,14 +353,13 @@ const TraceContent = ({activeTrace}: TraceContentProps) => { /> - {/* -
- Evaluation - - - - -
*/} + {isTestsetDrawerOpen && ( + setIsTestsetDrawerOpen(false)} + /> + )} ) } diff --git a/agenta-web/src/hooks/useResizeObserver.ts b/agenta-web/src/hooks/useResizeObserver.ts index 7d87861dc9..476dd227d8 100644 --- a/agenta-web/src/hooks/useResizeObserver.ts +++ b/agenta-web/src/hooks/useResizeObserver.ts @@ -1,26 +1,27 @@ import {useLayoutEffect, useRef} from "react" -function useResizeObserver( - callback: (entry: ResizeObserverEntry["contentRect"]) => void, -) { +const useResizeObserver = ( + callback?: (entry: ResizeObserverEntry["contentRect"]) => void, + element?: HTMLElement, +) => { const ref = useRef(null) useLayoutEffect(() => { - const element = ref?.current + const _element = ref?.current || element - if (!element) { + if (!_element) { return } const observer = new ResizeObserver((entries) => { - callback(entries[0].contentRect) + callback?.(entries[0].contentRect) }) - observer.observe(element) + observer.observe(_element) return () => { observer.disconnect() } - }, [callback, ref]) + }, [callback, element, ref]) return ref } diff --git a/agenta-web/src/lib/helpers/utils.ts b/agenta-web/src/lib/helpers/utils.ts index 01fac1df35..06d1f52738 100644 --- a/agenta-web/src/lib/helpers/utils.ts +++ b/agenta-web/src/lib/helpers/utils.ts @@ -359,3 +359,20 @@ export const filterVariantParameters = ({ export const formatVariantIdWithHash = (variantId: string) => { return `# ${variantId.split("-")[0]}` } + +export const collectKeyPathsFromObject = (obj: any, prefix = ""): string[] => { + const paths: string[] = [] + + for (const [key, value] of Object.entries(obj)) { + const fullPath = prefix ? `${prefix}.${key}` : key + + if (value && typeof value === "object" && !Array.isArray(value)) { + const nestedPaths = collectKeyPathsFromObject(value, fullPath) + paths.push(...nestedPaths) + } else { + paths.push(fullPath) + } + } + + return paths +} diff --git a/agenta-web/src/pages/_app.tsx b/agenta-web/src/pages/_app.tsx index 72e6f5da46..c84846caf9 100644 --- a/agenta-web/src/pages/_app.tsx +++ b/agenta-web/src/pages/_app.tsx @@ -2,12 +2,14 @@ import {useEffect} from "react" import type {AppProps} from "next/app" import {useRouter} from "next/router" import Head from "next/head" +import dynamic from "next/dynamic" import posthog from "posthog-js" import {PostHogProvider} from "posthog-js/react" import "@/styles/globals.css" import Layout from "@/components/Layout/Layout" +import {dynamicComponent} from "@/lib/helpers/dynamic" import ThemeContextProvider from "@/components/Layout/ThemeContextProvider" import AppContextProvider from "@/contexts/app.context" import ProfileContextProvider from "@/contexts/profile.context" @@ -16,6 +18,8 @@ import "ag-grid-community/styles/ag-grid.css" import "ag-grid-community/styles/ag-theme-alpine.css" import {Inter} from "next/font/google" +const NoMobilePageWrapper = dynamicComponent("NoMobilePageWrapper/NoMobilePageWrapper") + const inter = Inter({ subsets: ["latin"], variable: "--font-inter", @@ -60,6 +64,7 @@ export default function App({Component, pageProps}: AppProps) { + diff --git a/docs/blog/main.mdx b/docs/blog/main.mdx index 49c9ac55a5..d65f977510 100644 --- a/docs/blog/main.mdx +++ b/docs/blog/main.mdx @@ -8,6 +8,35 @@ import Image from "@theme/IdealImage"; ```
+### Viewing Traces in the Playground and Authentication for Deployed Applications + +_29 November 2024_ + +**v0.28.0** + +#### Viewing traces in the playground: + +You can now see traces directly in the playground. For simple applications, this means you can view the prompts sent to LLMs. For custom workflows, you get an overview of intermediate steps and outputs. This makes it easier to understand what’s happening under the hood and debug your applications. + +#### Authentication improvements: + +We’ve strengthened authentication for deployed applications. As you know, Agenta lets you either fetch the app’s config or call it with Agenta acting as a proxy. Now, we’ve added authentication to the second method. The APIs we create are now protected and can be called using an API key. You can find code snippets for calling the application in the overview page. + +#### Documentation improvements: + +We’ve added new cookbooks and updated existing documentation: + +- New [cookbook for observability with LangChain](/tutorials/cookbooks/observability_langchain) +- New [cookbook for custom workflows](/tutorials/cookbooks/AI-powered-code-reviews) where we build an AI powered code reviewer +- Updated the [custom workflows documentation](/custom-workflows/overview) and added [reference](/reference/sdk/custom-workflow) +- Updated the [reference for the observability SDK](/reference/sdk/observability) and [for the prompt management SDK](/reference/sdk/configuration-management) + +#### Bug fixes: + +- Fixed an issue with the observability SDK not being compatible with LiteLLM. +- Fixed an issue where cost and token usage were not correctly computed for all calls. + +--- ### Observability and Prompt Management