Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
46 changes: 46 additions & 0 deletions db_scripts/backfill_daily_tool_spend.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
-- One-shot backfill of the LiteLLM_DailyToolSpend rollup from the per-request
-- LiteLLM_SpendLogToolIndex x LiteLLM_SpendLogs tables.
--
-- This is an opt-in, manual operation. New deployments do not need it: the
-- rollup is written at request time from the moment the release is deployed.
-- Run it only if you want the Cost Optimization "Spend by tool" card to show
-- history from before the deploy, and only once.
--
-- IMPORTANT caveats before running:
--
-- 1. Pre-deploy index rows may include tools that were merely DECLARED in a
-- request body but never invoked (the release this ships with stops
-- recording those). For agentic clients that declare many tools per
-- request, backfilled history attributes each request's full spend to
-- every declared tool, overstating per-tool spend. Post-deploy rows do not
-- have this problem. If your traffic is mostly such clients, consider not
-- backfilling.
--
-- 2. Coverage is bounded by spend-log retention: rows older than
-- maximum_spend_logs_retention_period are already gone.
--
-- 3. Replace the cutover timestamp below with the time you deployed the
-- release, so backfilled per-request rows cannot double-count on top of
-- rollup rows the new writer already created. ON CONFLICT DO NOTHING is a
-- second guard for (date, tool_name) buckets the writer already touched:
-- such buckets keep the writer's numbers and skip the backfill's.
--
-- Usage:
-- psql "$DATABASE_URL" -v cutover="'2026-07-25T00:00:00Z'" -f db_scripts/backfill_daily_tool_spend.sql

SET TIME ZONE 'UTC';

INSERT INTO "LiteLLM_DailyToolSpend" (date, tool_name, spend, total_tokens, request_count, created_at, updated_at)
SELECT
to_char(ti.start_time, 'YYYY-MM-DD') AS date,
ti.tool_name,
COALESCE(SUM(sl.spend), 0) AS spend,
COALESCE(SUM(sl.total_tokens), 0) AS total_tokens,
COUNT(*) AS request_count,
now() AS created_at,
now() AS updated_at
FROM "LiteLLM_SpendLogToolIndex" ti
JOIN "LiteLLM_SpendLogs" sl ON sl.request_id = ti.request_id
WHERE ti.start_time < :cutover::timestamptz
GROUP BY 1, 2
ON CONFLICT (date, tool_name) DO NOTHING;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogToolIndex_start_time_idx" ON "LiteLLM_SpendLogToolIndex"("start_time");
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE IF NOT EXISTS "LiteLLM_DailyToolSpend" (
"date" TEXT NOT NULL,
"tool_name" TEXT NOT NULL,
"spend" DOUBLE PRECISION NOT NULL DEFAULT 0.0,
"total_tokens" BIGINT NOT NULL DEFAULT 0,
"request_count" BIGINT NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,

CONSTRAINT "LiteLLM_DailyToolSpend_pkey" PRIMARY KEY ("date","tool_name")
);
14 changes: 14 additions & 0 deletions litellm-proxy-extras/litellm_proxy_extras/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,20 @@ model LiteLLM_SpendLogToolIndex {

@@id([request_id, tool_name])
@@index([tool_name, start_time])
@@index([start_time])
}

// Daily tool spend rollup (one row per tool per day) – the Cost Optimization card reads this, never SpendLogs
model LiteLLM_DailyToolSpend {
date String
tool_name String
spend Float @default(0.0)
total_tokens BigInt @default(0)
request_count BigInt @default(0)
created_at DateTime @default(now())
updated_at DateTime @updatedAt

@@id([date, tool_name])
}

// Prompt table for storing prompt configurations
Expand Down
4 changes: 2 additions & 2 deletions litellm-proxy-extras/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "litellm-proxy-extras"
version = "0.4.79.post1"
version = "0.4.79.post2"
description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package."
readme = "README.md"
requires-python = ">=3.9"
Expand All @@ -26,7 +26,7 @@ required-version = ">=0.10.9"
module-root = ""

[tool.commitizen]
version = "0.4.79.post1"
version = "0.4.79.post2"
version_files = [
"pyproject.toml:^version",
"../pyproject.toml:litellm-proxy-extras==",
Expand Down
1 change: 1 addition & 0 deletions litellm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,7 @@
SPEND_LOG_CLEANUP_BATCH_FAILURE_BACKOFF_SECONDS = float(
os.getenv("SPEND_LOG_CLEANUP_BATCH_FAILURE_BACKOFF_SECONDS", 0.5)
)
TOOL_SPEND_TOP_TOOLS = 100
SPEND_LOG_PARTITION_INTERVAL = os.getenv("SPEND_LOG_PARTITION_INTERVAL", "day")
SPEND_LOG_PARTITION_PRECREATE_AHEAD = int(os.getenv("SPEND_LOG_PARTITION_PRECREATE_AHEAD", 7))
SPEND_LOG_QUEUE_SIZE_THRESHOLD = int(os.getenv("SPEND_LOG_QUEUE_SIZE_THRESHOLD", 100))
Expand Down
32 changes: 24 additions & 8 deletions litellm/litellm_core_utils/prompt_templates/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import re
import xml.etree.ElementTree as ET
from enum import Enum
from collections.abc import Mapping
from typing import Any, Dict, List, Optional, Set, Tuple, TypedDict, Union, cast, overload

from jinja2.sandbox import ImmutableSandboxedEnvironment
Expand Down Expand Up @@ -5350,7 +5351,9 @@ def prompt_factory(
def get_attribute_or_key(tool_or_function, attribute, default=None):
if hasattr(tool_or_function, attribute):
return getattr(tool_or_function, attribute)
return tool_or_function.get(attribute, default)
if isinstance(tool_or_function, Mapping):
return tool_or_function.get(attribute, default)
return default


class NormalizedToolCall(TypedDict):
Expand Down Expand Up @@ -5379,14 +5382,18 @@ def _parse_tool_call_arguments(raw: Any, tool_name: Optional[str], context: str)
return parsed if isinstance(parsed, dict) else {}


def _tool_calls_from_chat_completion_response(response: Any) -> list[NormalizedToolCall]:
def _tool_calls_from_chat_completion_response(
response: Any, include_all_choices: bool = False
) -> list[NormalizedToolCall]:
choices = get_attribute_or_key(response, "choices", None)
if not (isinstance(choices, list) and choices):
return []
message = get_attribute_or_key(choices[0], "message", None)
tool_calls = get_attribute_or_key(message, "tool_calls", None) if message else None
if not isinstance(tool_calls, list):
return []
tool_calls: list[Any] = []
for choice in choices if include_all_choices else choices[:1]:
message = get_attribute_or_key(choice, "message", None)
choice_tool_calls = get_attribute_or_key(message, "tool_calls", None) if message else None
if isinstance(choice_tool_calls, list):
tool_calls.extend(choice_tool_calls)
result: list[NormalizedToolCall] = []
for tc in tool_calls:
fn = get_attribute_or_key(tc, "function", None)
Expand Down Expand Up @@ -5449,19 +5456,28 @@ def _tool_calls_from_anthropic_messages_response(response: Any) -> list[Normaliz
return result


def get_tool_calls_from_response(response: Any) -> list[NormalizedToolCall]:
def get_tool_calls_from_response(response: Any, include_all_choices: bool = False) -> list[NormalizedToolCall]:
"""
Extract tool/function calls from a response object into a normalized
``{"id", "name", "arguments"}`` shape, regardless of which API surface
produced it: chat completions (``choices[].message.tool_calls``),
the Responses API (``output`` items of type ``function_call``), or the
Anthropic Messages API (``content`` blocks of type ``tool_use``).

``include_all_choices`` decides the chat-completions scope: the default
reads only ``choices[0]``, which is what consumers that act on THE reply
(e.g. guardrails rebuilding the primary assistant message) want; usage
accounting passes True because every choice of an ``n>1`` request costs
money and its tool calls really ran. The other surfaces have a single
output, so the flag has no effect on them.

Callers that only care about a specific tool should filter the result by
``name`` themselves -- this returns every tool call found.
"""
chat_tool_calls = _tool_calls_from_chat_completion_response(response, include_all_choices=include_all_choices)
if chat_tool_calls:
return chat_tool_calls
for extractor in (
_tool_calls_from_chat_completion_response,
_tool_calls_from_responses_api_response,
_tool_calls_from_anthropic_messages_response,
):
Expand Down
2 changes: 1 addition & 1 deletion litellm/proxy/_experimental/out/404.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion litellm/proxy/_experimental/out/404/index.html

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
1:"$Sreact.fragment"
2:I[347257,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/1uwy4yks0f1c7.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"ClientPageRoot"]
3:I[871135,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/1uwy4yks0f1c7.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js","/litellm-asset-prefix/_next/static/chunks/1zr7rrk4wkmju.js","/litellm-asset-prefix/_next/static/chunks/4055s0-dom81s.js","/litellm-asset-prefix/_next/static/chunks/0zduf1gntl_f8.js","/litellm-asset-prefix/_next/static/chunks/20af1m4f_wo-4.js","/litellm-asset-prefix/_next/static/chunks/34z87tuwl-2tk.js","/litellm-asset-prefix/_next/static/chunks/2ovpxj_l6k1df.js","/litellm-asset-prefix/_next/static/chunks/1egl1w6n7et4v.js","/litellm-asset-prefix/_next/static/chunks/3u1stln-nfstx.js","/litellm-asset-prefix/_next/static/chunks/09il7w0a0afcs.js","/litellm-asset-prefix/_next/static/chunks/1vquuz09jxl5_.js","/litellm-asset-prefix/_next/static/chunks/04a80e3a1m-iy.js","/litellm-asset-prefix/_next/static/chunks/0dbvgsc7ha049.js","/litellm-asset-prefix/_next/static/chunks/00c27bg0wy4ve.js","/litellm-asset-prefix/_next/static/chunks/0coq3c60p1b7b.js","/litellm-asset-prefix/_next/static/chunks/3htra5tr7po1s.js","/litellm-asset-prefix/_next/static/chunks/3_6d0cxt0bjl-.js","/litellm-asset-prefix/_next/static/chunks/39dhse7b9znhd.js","/litellm-asset-prefix/_next/static/chunks/2hu1vyy-5pv13.js","/litellm-asset-prefix/_next/static/chunks/2hanmoxc51y5l.js","/litellm-asset-prefix/_next/static/chunks/0rxrzom5haz4h.js","/litellm-asset-prefix/_next/static/chunks/199uwr871eene.js","/litellm-asset-prefix/_next/static/chunks/099m1uce-fb8r.js","/litellm-asset-prefix/_next/static/chunks/1uz3jt-tj9lkf.js","/litellm-asset-prefix/_next/static/chunks/0nd2fff333ozp.js","/litellm-asset-prefix/_next/static/chunks/1agyescpxom9_.js","/litellm-asset-prefix/_next/static/chunks/296166z--6fpm.js","/litellm-asset-prefix/_next/static/chunks/12wsfsljxg4xv.js","/litellm-asset-prefix/_next/static/chunks/1y_oq-jkyyahr.js","/litellm-asset-prefix/_next/static/chunks/3-952v5v-4lvz.js","/litellm-asset-prefix/_next/static/chunks/05i7gtmth4p52.js","/litellm-asset-prefix/_next/static/chunks/23-g73xaw3kap.js","/litellm-asset-prefix/_next/static/chunks/19bxlez8idou2.js","/litellm-asset-prefix/_next/static/chunks/17arud1mz8ipv.js","/litellm-asset-prefix/_next/static/chunks/0enpuqjw6najv.js","/litellm-asset-prefix/_next/static/chunks/1h_layxqih-1d.js","/litellm-asset-prefix/_next/static/chunks/09l_m9l1emin2.js"],"default"]
6:I[897367,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/1uwy4yks0f1c7.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"OutletBoundary"]
2:I[347257,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/1tgw6wg8vpjb_.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"ClientPageRoot"]
3:I[871135,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/1tgw6wg8vpjb_.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js","/litellm-asset-prefix/_next/static/chunks/1zr7rrk4wkmju.js","/litellm-asset-prefix/_next/static/chunks/4055s0-dom81s.js","/litellm-asset-prefix/_next/static/chunks/0zduf1gntl_f8.js","/litellm-asset-prefix/_next/static/chunks/04a80e3a1m-iy.js","/litellm-asset-prefix/_next/static/chunks/34z87tuwl-2tk.js","/litellm-asset-prefix/_next/static/chunks/1vquuz09jxl5_.js","/litellm-asset-prefix/_next/static/chunks/3wlz1dk9uk_5l.js","/litellm-asset-prefix/_next/static/chunks/3u1stln-nfstx.js","/litellm-asset-prefix/_next/static/chunks/09il7w0a0afcs.js","/litellm-asset-prefix/_next/static/chunks/0dbvgsc7ha049.js","/litellm-asset-prefix/_next/static/chunks/00c27bg0wy4ve.js","/litellm-asset-prefix/_next/static/chunks/0coq3c60p1b7b.js","/litellm-asset-prefix/_next/static/chunks/29zy8xbte5bx8.js","/litellm-asset-prefix/_next/static/chunks/20af1m4f_wo-4.js","/litellm-asset-prefix/_next/static/chunks/2ti12o47v07vt.js","/litellm-asset-prefix/_next/static/chunks/3_6d0cxt0bjl-.js","/litellm-asset-prefix/_next/static/chunks/2hanmoxc51y5l.js","/litellm-asset-prefix/_next/static/chunks/2hu1vyy-5pv13.js","/litellm-asset-prefix/_next/static/chunks/1uz3jt-tj9lkf.js","/litellm-asset-prefix/_next/static/chunks/0nd2fff333ozp.js","/litellm-asset-prefix/_next/static/chunks/199uwr871eene.js","/litellm-asset-prefix/_next/static/chunks/099m1uce-fb8r.js","/litellm-asset-prefix/_next/static/chunks/19bxlez8idou2.js","/litellm-asset-prefix/_next/static/chunks/0ob4xjai8e6ym.js","/litellm-asset-prefix/_next/static/chunks/09l_m9l1emin2.js","/litellm-asset-prefix/_next/static/chunks/296166z--6fpm.js","/litellm-asset-prefix/_next/static/chunks/12wsfsljxg4xv.js","/litellm-asset-prefix/_next/static/chunks/1y_oq-jkyyahr.js","/litellm-asset-prefix/_next/static/chunks/3-952v5v-4lvz.js","/litellm-asset-prefix/_next/static/chunks/05i7gtmth4p52.js","/litellm-asset-prefix/_next/static/chunks/23-g73xaw3kap.js","/litellm-asset-prefix/_next/static/chunks/2s5k4mt99snqf.js","/litellm-asset-prefix/_next/static/chunks/17arud1mz8ipv.js","/litellm-asset-prefix/_next/static/chunks/10hqekmjsynkf.js","/litellm-asset-prefix/_next/static/chunks/1h_layxqih-1d.js"],"default"]
6:I[897367,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/1tgw6wg8vpjb_.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"OutletBoundary"]
7:"$Sreact.suspense"
0:{"rsc":["$","$1","c",{"children":[["$","$L2",null,{"Component":"$3","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@4","$@5"]}}],[["$","script","script-0",{"src":"/litellm-asset-prefix/_next/static/chunks/3_6d0cxt0bjl-.js","async":true}],["$","script","script-1",{"src":"/litellm-asset-prefix/_next/static/chunks/39dhse7b9znhd.js","async":true}],["$","script","script-2",{"src":"/litellm-asset-prefix/_next/static/chunks/2hu1vyy-5pv13.js","async":true}],["$","script","script-3",{"src":"/litellm-asset-prefix/_next/static/chunks/2hanmoxc51y5l.js","async":true}],["$","script","script-4",{"src":"/litellm-asset-prefix/_next/static/chunks/0rxrzom5haz4h.js","async":true}],["$","script","script-5",{"src":"/litellm-asset-prefix/_next/static/chunks/199uwr871eene.js","async":true}],["$","script","script-6",{"src":"/litellm-asset-prefix/_next/static/chunks/099m1uce-fb8r.js","async":true}],["$","script","script-7",{"src":"/litellm-asset-prefix/_next/static/chunks/1uz3jt-tj9lkf.js","async":true}],["$","script","script-8",{"src":"/litellm-asset-prefix/_next/static/chunks/0nd2fff333ozp.js","async":true}],["$","script","script-9",{"src":"/litellm-asset-prefix/_next/static/chunks/1agyescpxom9_.js","async":true}],["$","script","script-10",{"src":"/litellm-asset-prefix/_next/static/chunks/296166z--6fpm.js","async":true}],["$","script","script-11",{"src":"/litellm-asset-prefix/_next/static/chunks/12wsfsljxg4xv.js","async":true}],["$","script","script-12",{"src":"/litellm-asset-prefix/_next/static/chunks/1y_oq-jkyyahr.js","async":true}],["$","script","script-13",{"src":"/litellm-asset-prefix/_next/static/chunks/3-952v5v-4lvz.js","async":true}],["$","script","script-14",{"src":"/litellm-asset-prefix/_next/static/chunks/05i7gtmth4p52.js","async":true}],["$","script","script-15",{"src":"/litellm-asset-prefix/_next/static/chunks/23-g73xaw3kap.js","async":true}],["$","script","script-16",{"src":"/litellm-asset-prefix/_next/static/chunks/19bxlez8idou2.js","async":true}],["$","script","script-17",{"src":"/litellm-asset-prefix/_next/static/chunks/17arud1mz8ipv.js","async":true}],["$","script","script-18",{"src":"/litellm-asset-prefix/_next/static/chunks/0enpuqjw6najv.js","async":true}],["$","script","script-19",{"src":"/litellm-asset-prefix/_next/static/chunks/1h_layxqih-1d.js","async":true}],["$","script","script-20",{"src":"/litellm-asset-prefix/_next/static/chunks/09l_m9l1emin2.js","async":true}]],["$","$L6",null,{"children":["$","$7",null,{"name":"Next.MetadataOutlet","children":"$@8"}]}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"tjyp94vnPkfmefK0pvYUs"}
0:{"rsc":["$","$1","c",{"children":[["$","$L2",null,{"Component":"$3","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@4","$@5"]}}],[["$","script","script-0",{"src":"/litellm-asset-prefix/_next/static/chunks/3_6d0cxt0bjl-.js","async":true}],["$","script","script-1",{"src":"/litellm-asset-prefix/_next/static/chunks/2hanmoxc51y5l.js","async":true}],["$","script","script-2",{"src":"/litellm-asset-prefix/_next/static/chunks/2hu1vyy-5pv13.js","async":true}],["$","script","script-3",{"src":"/litellm-asset-prefix/_next/static/chunks/1uz3jt-tj9lkf.js","async":true}],["$","script","script-4",{"src":"/litellm-asset-prefix/_next/static/chunks/0nd2fff333ozp.js","async":true}],["$","script","script-5",{"src":"/litellm-asset-prefix/_next/static/chunks/199uwr871eene.js","async":true}],["$","script","script-6",{"src":"/litellm-asset-prefix/_next/static/chunks/099m1uce-fb8r.js","async":true}],["$","script","script-7",{"src":"/litellm-asset-prefix/_next/static/chunks/19bxlez8idou2.js","async":true}],["$","script","script-8",{"src":"/litellm-asset-prefix/_next/static/chunks/0ob4xjai8e6ym.js","async":true}],["$","script","script-9",{"src":"/litellm-asset-prefix/_next/static/chunks/09l_m9l1emin2.js","async":true}],["$","script","script-10",{"src":"/litellm-asset-prefix/_next/static/chunks/296166z--6fpm.js","async":true}],["$","script","script-11",{"src":"/litellm-asset-prefix/_next/static/chunks/12wsfsljxg4xv.js","async":true}],["$","script","script-12",{"src":"/litellm-asset-prefix/_next/static/chunks/1y_oq-jkyyahr.js","async":true}],["$","script","script-13",{"src":"/litellm-asset-prefix/_next/static/chunks/3-952v5v-4lvz.js","async":true}],["$","script","script-14",{"src":"/litellm-asset-prefix/_next/static/chunks/05i7gtmth4p52.js","async":true}],["$","script","script-15",{"src":"/litellm-asset-prefix/_next/static/chunks/23-g73xaw3kap.js","async":true}],["$","script","script-16",{"src":"/litellm-asset-prefix/_next/static/chunks/2s5k4mt99snqf.js","async":true}],["$","script","script-17",{"src":"/litellm-asset-prefix/_next/static/chunks/17arud1mz8ipv.js","async":true}],["$","script","script-18",{"src":"/litellm-asset-prefix/_next/static/chunks/10hqekmjsynkf.js","async":true}],["$","script","script-19",{"src":"/litellm-asset-prefix/_next/static/chunks/1h_layxqih-1d.js","async":true}]],["$","$L6",null,{"children":["$","$7",null,{"name":"Next.MetadataOutlet","children":"$@8"}]}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"42XogNvS9fAzsVum7mqhy"}
4:{}
5:"$0:rsc:props:children:0:props:serverProvidedParams:params"
8:null
Loading
Loading