fix(websearch_interception): bill intercepted searches to the calling key - #35708
Conversation
|
|
Greptile SummaryThis PR attributes intercepted web-search calls to the originating proxy key so their costs appear in spend logs and apply to the appropriate key, user, team, and organization budgets.
Confidence Score: 5/5The PR appears safe to merge. The previously reported limiter-reservation issue is resolved because the intercepted search no longer invokes the proxy limiter pre-call hook, and forwarding
|
| Filename | Overview |
|---|---|
| litellm/integrations/websearch_interception/handler.py | The intercepted search now receives authenticated attribution metadata without restoring the previously reported unmatched limiter reservation. |
| tests/test_litellm/integrations/websearch_interception/test_websearch_interception_handler.py | Tests verify attribution fields, spend-gate eligibility, selected search parameters, and metadata-free SDK behavior. |
Reviews (3): Last reviewed commit: "Merge remote-tracking branch 'origin/lit..." | Re-trigger Greptile
… against the calling key An intercepted web search called litellm.asearch() with only the search tool's litellm_params, so the search request carried no owner. The proxy's spend hook skips any call with no key, user or team attached, so the search's provider cost never reached SpendLogs; it was missing from the Logs page and never counted against the caller's budget. The same path never ran the rate limiter either, so an intercepted search was free of the key's RPM/TPM limits. The search now carries the originating key's attribution metadata (key hash, alias, user, team, org, plus model_group set to the resolved search tool) and runs the caller's rate limit checks before hitting the provider, matching what a direct /v1/search request gets. SDK calls with no proxy auth context are unchanged. Resolves LIT-5033 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
fbc7330 to
8466ed0
Compare
|
@greptile review again |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…metadata forwarding Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…evin_ai_lit_5033_websearch_interception_spend # Conflicts: # litellm/integrations/websearch_interception/handler.py
TLDR
Problem this solves:
How it solves it:
User Flow
Before: a developer using a web-search tool through the gateway pays for every search out of band, and their admin cannot see or cap it
"max_budget": 1"tools": [{"type": "web_search_20250305", ...}]and gets back an answer built from live search resultsspend: 0, so the search was free as far as the budget is concernedmax_budgetbecause nothing the search costs is ever counted against itAfter: the same request records the search too, so it shows up in Logs and counts against the key's budget
"max_budget": 1"tools": [{"type": "web_search_20250305", ...}]and gets back an answer built from live search resultsasearchrow for the search, attributed to the same key and userspendmax_budgeton searches is cut off like it would be on any other spendRelevant issues
Linear ticket
Resolves LIT-5033
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
Live proxy against Postgres with real Exa searches and real OpenAI calls, the same request run twice: once on this branch at
24888d56a6, once at that same commit withlitellm/integrations/websearch_interception/handler.pychecked out fromlitellm_internal_stagingat8b16ee1dc2litellm --config lit5033_config.yaml --detailed_debug --port 4000 2>&1 | tee litellm.logKEY=$(curl -s -X POST http://localhost:4000/key/generate -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"user_id":"search-budget-qa","max_budget":1}' | jq -r .key)Web search request through the interception path
curl -s http://localhost:4000/spend/logs -H "Authorization: Bearer sk-1234" | jq '.[] | {call_type, model, model_group, spend, user}'pluscurl -s http://localhost:4000/key/info -H "Authorization: Bearer $KEY" | jq .info.spendWith the handler from
litellm_internal_stagingonly the LLM row lands and the Exa query is freeOn this branch the search gets its own row, priced and attributed to the calling key and user, and the key's spend moves so
max_budgetcovers itFull metadata on that new row, showing where the attribution comes from
{ "request_id": "df37e382-6450-4795-833c-0fbe488117cf", "call_type": "asearch", "model": "exa_ai/search", "model_group": "exa-search", "custom_llm_provider": "exa_ai", "api_base": "https://api.exa.ai/search", "spend": 0.005, "user": "search-budget-qa", "metadata": { "user_api_key": "e49bdcd8c776e87d1a64e58d9dd97ac777223886c1bc8af67ed5e9d96cfee50e", "user_api_key_user_id": "search-budget-qa", "user_api_key_team_id": null, "cost_breakdown": {"input_cost": 0.005, "output_cost": 0, "total_cost": 0.005} } }Both rows also show up on http://localhost:4000/ui/?page=logs
Type
🐛 Bug Fix
Changes
WebSearchInterceptionLogger._execute_search()calledlitellm.asearch()with nothing but the search tool's ownlitellm_params, so the resulting call had no owner._PROXY_track_cost_callbackskips any call whose metadata has no key, user, team or end user, which is why the search's cost was computed correctly and then dropped: no SpendLogs row, nothing on the Logs page, no budget movement. The handler already had the originating request'skwargsin hand, it readsuser_api_key_authout of them for the search tool permission checks, it just never passed the attribution alongThe search now goes out with the calling key's metadata, built from the same
UserAPIKeyAuththe permission checks use, so the row lands on the right key, user, team and org and showsmodel_groupas the resolved search tool the way a direct/v1/searchrequest does.search_tool_nameis resolved off the selected tool rather than only its rawsearch_providerSDK callers keep the old behavior, since there is no key to attribute a search to outside the proxy
Not in scope here: RPM parity. An earlier revision of this PR also invoked the v3 rate limiter's pre-call hook for the intercepted search, which Greptile correctly flagged, since that hook reserves a parallel-request slot and TPM budget that only its own success or failure callback releases. The reservation bookkeeping lives on a ContextVar stash pinned to the originating request's
litellm_call_id, so a nested call reserving through it would both leak its own reservation and clobber the parent request's, which is worse than the gap it closes. Doing this properly means giving the intercepted search its own request context so the limiter's release callbacks match up, which belongs in its own PR. Token usage from the search does still reach the TPM counters through the search's success event now that the metadata is attachedTwo regression tests cover the fix: attribution metadata reaches
asearchand is accepted by the spend gate, and an SDK call with no proxy auth context still sends no proxy metadataFinal Attestation
Link to Devin session: https://app.devin.ai/sessions/8ad0aa5b32ad4848b7fae25718a5a035
Requested by: @mateo-berri