Skip to content

fix(proxy): keep the raw client model out of spend logs for rejections outside the router - #41943

Merged
mateo-berri merged 6 commits into
mainfrom
litellm_unknown_model_spend_logs_outside_router
Sep 19, 2026
Merged

mateo-berri merged 6 commits into
mainfrom
litellm_unknown_model_spend_logs_outside_router

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Rejected unknown-model calls outside the router save the raw model string
  • Moderations rows store it as model and model_group
  • Batches, files, and adapter rows keep it in the saved error text
  • With prompt storage on, the saved request body keeps it too

How it solves it:

  • The placeholder keys on a resolved deployment id, not on model_group
  • Placeholdered rows get the raw string scrubbed from their error text
  • Their saved request body stores unknown-model and the scrubbed metadata
  • Files, batches, and the adapter route raise the shared unknown-model error

User Flow

Before: a developer's client puts prompt text into the model field by mistake, the gateway rejects the call, and the prompt text is still saved in the spend logs for anyone who can read them

  1. They send POST https://litellm-domain/v1/moderations with "input": "hi" and "model": "opus-4.6 Please summarize my medical records\nPatient has diabetes", and get a 400 carrying OpenAI's Invalid value for 'model' error
  2. They send POST https://litellm-domain/v1/batches with the same model and get a 400 saying Model '<that string>' not found in model_list. Please check your config.yaml.
  3. They send POST https://litellm-domain/v1/files as multipart with purpose=batch, the same model, and a JSONL file, and get a 400 with the same not found in model_list text
  4. They send POST https://litellm-domain/anthropic-adapter/v1/messages (an adapter pass-through route from the config) with the same model and get a 400 saying completion: Invalid model name passed in model=<that string>
  5. The admin reads each call with GET https://litellm-domain/spend/logs?request_id=. The moderations row shows the whole string, medical text included, in model, in model_group, and in metadata.error_information.error_message. The batches and adapter rows show "model": "unknown-model", but their error_message still holds the whole string. The files call returns no call id, and its row (found through GET https://litellm-domain/spend/logs?start_date=...&end_date=...) holds the string in error_message too
  6. With store_prompts_in_spend_logs: true in the config, even next to turn_off_message_logging: true, each of those rows also stores the whole string as the model of the saved request body, and the moderations row stores it twice more inside that body's metadata (model_group and error_information.error_message)
  7. Anyone with access to the logs page, /spend/logs, or the spend logs table can read that user's prompt text

After: the same calls are rejected with 400s, and none of the saved rows holds the prompt text

  1. They send the same POST https://litellm-domain/v1/moderations and get the same 400 with OpenAI's Invalid value for 'model' error
  2. They send the same POST https://litellm-domain/v1/batches and get a 400 that now reads batch creation: Invalid model name passed in model=<that string>. Call /v1/models to view available models for your key.
  3. They send the same POST https://litellm-domain/v1/files and get a 400 that now reads file upload: Invalid model name passed in model=<that string>. Call /v1/models to view available models for your key.
  4. They send the same POST https://litellm-domain/anthropic-adapter/v1/messages and get the same 400 with "type": "invalid_request_error" and "code": "400", with the /v1/models hint added to the message
  5. The admin reads each row the same way. Every one shows "model": "unknown-model", an empty model_group, and an error_message with the string cut out: Invalid value for 'model' = unknown-model for moderations, batch creation: Invalid model name passed in. Call /v1/models ... for batches, file upload: ... for files, and completion: ... for the adapter, with no client string anywhere in the row
  6. With prompt storage on, the saved request body of those rows reads "model": "unknown-model", and the metadata copied into that body shows the same empty model_group and the same cut-down error_information.error_message as the row; a healthy call's saved body keeps its real model name
  7. Someone with access to the logs can no longer read the prompt text; the caller still sees their own string echoed back in the 400, as before

Design decisions

The placeholder gate moved from "no model_group" to "the call failed, no deployment id was resolved, the model does not look like a model name, and the group is not a configured one". model_group is stamped before a deployment is picked, and for calls that never reach the router it is just the client's own string, so it says nothing about whether the model is real. A deployment id only exists once a deployment was chosen. The alternative, rejecting unknown moderation models at the proxy, was left out on purpose because moderations is allowed to pass a model the config does not list straight to the provider

"Looks like a model name" means at most 256 characters with no whitespace. That test alone would also placeholder a configured group whose name has spaces (Broken GPT Mini) when every deployment is cooling down and the router fails before picking one, so the gate also asks the router whether the group is configured, by exact membership in the model list or the team-public names. Wildcard patterns are excluded on purpose: a regex match against a prompt-shaped string is exactly the guess this PR removes. When no router is in hand (the GCS pub/sub v1 path builds the same payload without one) the gate falls back to placeholdering

The persisted model_group is blanked only when the row is placeholdered, the group equals the raw client string, and that string does not look like a model name. Blanking it on every placeholdered row was rejected because router-rejected rows keep a sane unknown model in model_group today and tests pin that. router_metadata.requested_model follows the same value

The error text scrub runs where the spend log payload is built, since that is the only place that knows the row was placeholdered. It replaces three spellings of the raw string with unknown-model: the literal one, the Python repr-escaped one (\n written as two characters, which is how a provider error dict or an exception detail gets stringified), and the JSON-escaped one. Scrubbing only the literal spelling was rejected because the moderations repro stores the repr-escaped form. Only error_message and traceback are scrubbed; the other error fields never hold free text. The scrub builds a new mapping instead of editing the one the logging object handed over, so Prometheus, Datadog, and OTel keep reading the unscrubbed error text they already receive

Error text is truncated to 2048 characters (35% head, a marker, 65% tail) before the payload is built, so the raw string can be cut in half by the marker. The scrub also removes the longest suffix of the head and the longest prefix of the tail that overlap a spelling. The alternatives were moving the scrub ahead of truncation, which would put it before the placeholder verdict exists, or dropping truncated error text entirely, which loses debugging value

With store_prompts_in_spend_logs on, the saved request body of a placeholdered row gets its model replaced by unknown-model. The proxy also injects its metadata dict into that body, and the logging pipeline later writes model_group and error_information into the same dict, so the saved body carried the client string a second and third time under metadata.model_group and metadata.error_information.error_message (found by the live QA on the moderations path, which is the one whose provider 400 echoes the model). Those two keys now get the same values the row itself stores, the persisted group and the scrubbed error text, and every other key in that metadata stays as it was. Dropping the whole saved body for those rows was rejected because the rest of the body (headers minus authorization, metadata, the other fields) is what an operator debugs a rejected call with. turn_off_message_logging was not extended to cover model because it never has, and the placeholder is what the rest of the row already says. The request body sanitizer now descends into any Mapping, not only dict, which its own signature already promised; without that the read-only metadata copy was stored as its str()

The adapter route raises the shared unknown-model error with route completion, which keeps the client message prefix and the invalid_request_error / null / "400" contract it has today. Using the request path as the route was rejected because it changes the client message shape for no gain. retryable_with_model_read_through is False for both the adapter and the files and batches helper, because only the router's request path acts on that flag and neither of these goes through it. The a2a route already does the same

The files and batches credential lookup raises the same error with its operation_context as the route, so the client still gets a 400 that names the model, and the saved message is the fixed <operation>: Invalid model name passed in. text. One caller built its operation_context with the file's model inside it, which would have put a client string back into the saved message, so it now reads file operation (file created with model) like its sibling call sites. This changes the client-facing wording on those routes from Model '<x>' not found in model_list. Please check your config.yaml. to the shared wording, and a sane unknown model on /v1/batches now saves unknown-model like the chat routes do instead of the name. Keeping the old wording with a separate exception class was rejected because it would need a second special case in the spend log sanitizer. The helper imports the error inside the function because its module is imported by SDK-only installs where fastapi is absent, and a module-level import broke that install test in CI

Relevant issues

Affected release

Linear ticket

Resolves LIT-7620

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Shared setup for both sides: a proxy booted from the commit named in each heading with --num_workers 2 and --use_v2_migration_resolver, its own Postgres database, real OpenAI and Anthropic keys, master key sk-lit7620-qa, and custom_adapter.py next to the config. Every row below is read back through GET /spend/logs?request_id=<x-litellm-call-id> (the jq '<row fields>' filter picks request_id, call_type, status, model, model_group, model_id, metadata.error_information.error_message, and two booleans that grep the traceback and the whole JSON row for the prompt text medical records|Patient has diabetes). The sweep cases read GET /spend/logs?start_date=...&summarize=false at the merge base and GET /spend/logs/v2?... at the tip. The five Prompt storage on cases ran on a second boot of the same commit with qa_prompts.yaml; their filter adds stored_request_body_model (the model of the saved request body, first 80 characters), stored_request_body_holds_prompt_text, and stored_messages. The files case uploads a one-line batch.jsonl ({"custom_id":"r1","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-5.4-mini","messages":[{"role":"user","content":"hi"}]}}). The hostile-input case sends a 3948-character, 32-line model string (long_model.txt) and then tests every 40-character and every 12-character slice of it against the JSON-encoded row, its string values, and its string values with literal \n turned into newlines. The After side is the PR's tip, which merges main at 1f6e5b6: #41934 landed a per-key grant check ahead of the files and batches credential lookup after this branch's merge base, so the three Merged tip only cases generate a virtual key restricted to one model and send the same prompt-shaped model through it, where that grant check rejects the call with a 403 that names the model before the unknown-model error ever runs; that 403 carries no x-litellm-call-id header, so those rows are read back through GET /spend/logs/v2?status=failure&start_date=...&end_date=... filtered on the key alias. The merge base has no such check, so those cases have no Before side

The output under each step is abbreviated to fit GitHub's description limit: string values inside a row are cut at 100 characters and other output lines at 160, each cut ending in ... (+N chars), the row's request_id is left out because the command that read it carries the same id, and the cooldown case shows one of its twelve calls. The complete, unabridged transcripts of both sides, including the cases left out here, are in this gist. Left out: the healthy-call controls on a configured model (moderations, chat completions, responses, messages, the adapter route, the group with spaces in its name, and the prompt-storage boot's sane unknown name), the control where the provider rejects a call on a configured model, and the four router-path cases (/v1/chat/completions, /v1/responses, and /v1/messages with prompt text in model, and /v1/chat/completions with a sane unknown model name), which this PR does not touch and whose rows read the same on both sides. Every one of those rows still counts in the sweep case of its side

qa.yaml:

model_list:
  - model_name: opus-4.6
    litellm_params:
      model: anthropic/claude-opus-4-6
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: claude-haiku-4-5
    litellm_params:
      model: anthropic/claude-haiku-4-5
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: gpt-5.4-mini
    litellm_params:
      model: openai/gpt-5.4-mini
      api_key: os.environ/OPENAI_API_KEY
  - model_name: omni-moderation-latest
    litellm_params:
      model: openai/omni-moderation-latest
      api_key: os.environ/OPENAI_API_KEY
  - model_name: "Team GPT Mini"
    litellm_params:
      model: openai/gpt-5.4-mini
      api_key: os.environ/OPENAI_API_KEY
  - model_name: "Broken GPT Mini"
    litellm_params:
      model: openai/gpt-5.4-mini
      api_key: sk-lit7620-invalid-key-a
  - model_name: "Broken GPT Mini"
    litellm_params:
      model: openai/gpt-5.4-mini
      api_key: sk-lit7620-invalid-key-b
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  pass_through_endpoints:
    - path: "/anthropic-adapter/v1/messages"
      target: custom_adapter.anthropic_adapter
router_settings:
  num_retries: 0
  cooldown_time: 300
litellm_settings:
  drop_params: true

qa_prompts.yaml:

model_list:
  - model_name: opus-4.6
    litellm_params:
      model: anthropic/claude-opus-4-6
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: claude-haiku-4-5
    litellm_params:
      model: anthropic/claude-haiku-4-5
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: gpt-5.4-mini
    litellm_params:
      model: openai/gpt-5.4-mini
      api_key: os.environ/OPENAI_API_KEY
  - model_name: omni-moderation-latest
    litellm_params:
      model: openai/omni-moderation-latest
      api_key: os.environ/OPENAI_API_KEY
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  store_prompts_in_spend_logs: true
  pass_through_endpoints:
    - path: "/anthropic-adapter/v1/messages"
      target: custom_adapter.anthropic_adapter
router_settings:
  num_retries: 0
litellm_settings:
  drop_params: true
  turn_off_message_logging: true

custom_adapter.py:

from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import AnthropicAdapter

anthropic_adapter = AnthropicAdapter()

Before (5fc510a)

POST /v1/moderations with prompt text in model

  1. curl -sS -X POST http://127.0.0.1:23858/v1/moderations -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"input":"hi","model":"opus-4.6 Please summarize my medical records\nPatient has diabetes"}'

    HTTP 400
    x-litellm-call-id: 8ae826e9-0806-4791-be26-1a75d88c2bab
    {"error":{"message":"Error code: 400 - {'error': {'message': \"Invalid value for 'model' = opus-4.6 Please summarize my medical records\\nPatient has diabetes. ... (+184 chars)
    
  2. curl -sS 'http://127.0.0.1:23858/spend/logs?request_id=8ae826e9-0806-4791-be26-1a75d88c2bab' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "",
     "status": "failure",
     "model": "opus-4.6 Please summarize my medical records\nPatient has diabetes",
     "model_group": "opus-4.6 Please summarize my medical records\nPatient has diabetes",
     "model_id": "",
     "error_message": "Error code: 400 - {'error': {'message': \"Invalid value for 'model' = opus-4.6 Please summarize my me... (+157 chars)",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": true
    }
    

POST /v1/batches with prompt text in model

  1. curl -sS -X POST http://127.0.0.1:23858/v1/batches -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"input_file_id":"file-lit7620-does-not-exist","endpoint":"/v1/chat/completions","completion_window":"24h","model":"opus-4.6 Please summarize my medical records\nPatient has diabetes"}'

    HTTP 400
    x-litellm-call-id: ac051450-b83d-4cdf-84b3-7a98eef54da5
    {"error":{"message":"{'error': \"Model 'opus-4.6 Please summarize my medical records\\nPatient has diabetes' not found in model_list. Please check your config.y... (+67 chars)
    
  2. curl -sS 'http://127.0.0.1:23858/spend/logs?request_id=ac051450-b83d-4cdf-84b3-7a98eef54da5' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acreate_batch",
     "status": "failure",
     "model": "unknown-model",
     "model_group": "",
     "model_id": "",
     "error_message": "400: {'error': \"Model 'opus-4.6 Please summarize my medical records\\nPatient has diabetes' not found... (+48 chars)",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": true
    }
    

POST /anthropic-adapter/v1/messages with prompt text in model

  1. curl -sS -X POST http://127.0.0.1:23858/anthropic-adapter/v1/messages -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"model":"opus-4.6 Please summarize my medical records\nPatient has diabetes","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'

    HTTP 400
    x-litellm-call-id: 0f2c5b14-20f8-41f7-b91a-b1a0e8fd975c
    {"error":{"message":"400: {'error': 'completion: Invalid model name passed in model=opus-4.6 Please summarize my medical records\\nPatient has diabetes'}","type... (+53 chars)
    
  2. curl -sS 'http://127.0.0.1:23858/spend/logs?request_id=0f2c5b14-20f8-41f7-b91a-b1a0e8fd975c' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acompletion",
     "status": "failure",
     "model": "unknown-model",
     "model_group": "",
     "model_id": "",
     "error_message": "400: {'error': 'completion: Invalid model name passed in model=opus-4.6 Please summarize my medical ... (+31 chars)",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": true
    }
    

POST /v1/files with prompt text in model

  1. curl -sS -X POST http://127.0.0.1:23858/v1/files -H 'Authorization: Bearer sk-lit7620-qa' -F purpose=batch -F 'model=<the same prompt-shaped string, with a real newline>' -F file=@batch.jsonl

    HTTP 400
    x-litellm-call-id: 
    {"error":{"message":"{'error': \"Model 'opus-4.6 Please summarize my medical records\\nPatient has diabetes' not found in model_list. Please check your config.y... (+67 chars)
    
    NO SPEND LOG ROW FOUND after 90s for ids:
    

GET /v1/batches?model=

  1. curl -sS 'http://127.0.0.1:23858/v1/batches?model=opus-4.6%20Please%20summarize%20my%20medical%20records%0APatient%20has%20diabetes' -H 'Authorization: Bearer sk-lit7620-qa'

    HTTP 200
    x-litellm-call-id: 
    {"object":"list","data":[],"first_id":null,"last_id":null,"has_more":false}
    
    NO SPEND LOG ROW FOUND after 90s for ids:
    

Sane unknown model name on POST /v1/moderations

  1. curl -sS -X POST http://127.0.0.1:23858/v1/moderations -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"input":"hi","model":"text-moderation-nonexistent-9"}'

    HTTP 400
    x-litellm-call-id: 5cb75e80-10e0-4961-b81e-eb6a1d00a202
    {"error":{"message":"Error code: 400 - {'error': {'message': \"Invalid value for 'model' = text-moderation-nonexistent-9. Please check the OpenAI documentation ... (+146 chars)
    
  2. curl -sS 'http://127.0.0.1:23858/spend/logs?request_id=5cb75e80-10e0-4961-b81e-eb6a1d00a202' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "",
     "status": "failure",
     "model": "text-moderation-nonexistent-9",
     "model_group": "text-moderation-nonexistent-9",
     "model_id": "",
     "error_message": "Error code: 400 - {'error': {'message': \"Invalid value for 'model' = text-moderation-nonexistent-9. ... (+120 chars)",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": false
    }
    

Sane unknown model name on POST /v1/batches

  1. curl -sS -X POST http://127.0.0.1:23858/v1/batches -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"input_file_id":"file-lit7620-does-not-exist","endpoint":"/v1/chat/completions","completion_window":"24h","model":"gpt-nonexistent-9"}'

    HTTP 400
    x-litellm-call-id: 21c6436a-66e1-4575-8033-8f4449bd0aa1
    {"error":{"message":"{'error': \"Model 'gpt-nonexistent-9' not found in model_list. Please check your config.yaml.\"}","type":"internal_server_error","param":nu... (+17 chars)
    
  2. curl -sS 'http://127.0.0.1:23858/spend/logs?request_id=21c6436a-66e1-4575-8033-8f4449bd0aa1' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acreate_batch",
     "status": "failure",
     "model": "gpt-nonexistent-9",
     "model_group": "",
     "model_id": "",
     "error_message": "400: {'error': \"Model 'gpt-nonexistent-9' not found in model_list. Please check your config.yaml.\"}",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": false
    }
    

Configured model group with spaces in its name, the provider rejects the call

  1. curl -sS -X POST http://127.0.0.1:23858/v1/chat/completions -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"model":"Team GPT Mini","messages":[{"role":"narrator","content":"hi"}]}'

    HTTP 400
    x-litellm-call-id: e6876c31-343f-4739-9eac-40ea58aa33dd
    {"error":{"message":"litellm.BadRequestError: OpenAIException - Invalid value: 'narrator'. Supported values are: 'system', 'assistant', 'user', 'function', 'too... (+168 chars)
    
  2. curl -sS 'http://127.0.0.1:23858/spend/logs?request_id=e6876c31-343f-4739-9eac-40ea58aa33dd' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acompletion",
     "status": "failure",
     "model": "openai/gpt-5.4-mini",
     "model_group": "Team GPT Mini",
     "model_id": "1c1f423cd7f44bd5d09819eac0748e17af1ce23d27a66ac0eead407e8bcdf7b5",
     "error_message": "litellm.BadRequestError: OpenAIException - Invalid value: 'narrator'. Supported values are: 'system'... (+132 chars)",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": false
    }
    

Configured model group with spaces in its name, every deployment has a bad key

Twelve identical calls; statuses in order: 3 x 401, 9 x 429. Call 4 (every deployment already cooling down, so the router fails before picking one) shown; the full transcript holds all twelve

  1. curl -sS -X POST http://127.0.0.1:23858/v1/chat/completions -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"model":"Broken GPT Mini","messages":[{"role":"user","content":"hi"}]}'

    HTTP 429
    x-litellm-call-id: 5d88b136-4e3a-4368-909c-0cc11dd6caac
    {"error":{"message":"No deployments available for selected model, Try again in 300 seconds. All deployments for selected model are in cooldown. Passed model=Bro... (+253 chars)
    
  2. curl -sS 'http://127.0.0.1:23858/spend/logs?request_id=5d88b136-4e3a-4368-909c-0cc11dd6caac' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acompletion",
     "status": "failure",
     "model": "Broken GPT Mini",
     "model_group": "Broken GPT Mini",
     "model_id": "",
     "error_message": "No deployments available for selected model, Try again in 300 seconds. All deployments for selected ... (+226 chars)",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": false
    }
    

Every spend log row of the run, grouped by call_type, status, model, and model_group, and how many rows hold the prompt text

  1. curl -sS 'http://127.0.0.1:23858/spend/logs?start_date=2026-09-18&end_date=2026-09-20&summarize=false' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<group and count>'

    {"total_rows":30}
    {"call_type":"","status":"failure","model":"text-moderation-nonexistent-9","model_group":"text-moderation-nonexistent-9","holds_prompt_text":false,"rows":1}
    {"call_type":"","status":"failure","model":"opus-4.6 Please summarize my medical records\nPatient has diabetes","model_group":"opus-4.6 Please summarize my medi... (+69 chars)
    {"call_type":"IGNORE_THIS","status":"failure","model":"","model_group":"","holds_prompt_text":true,"rows":2}
    {"call_type":"acompletion","status":"failure","model":"Broken GPT Mini","model_group":"Broken GPT Mini","holds_prompt_text":false,"rows":9}
    {"call_type":"acompletion","status":"failure","model":"openai/gpt-5.4-mini","model_group":"Broken GPT Mini","holds_prompt_text":false,"rows":3}
    {"call_type":"acompletion","status":"failure","model":"openai/gpt-5.4-mini","model_group":"Team GPT Mini","holds_prompt_text":false,"rows":1}
    {"call_type":"acompletion","status":"success","model":"openai/gpt-5.4-mini","model_group":"Team GPT Mini","holds_prompt_text":false,"rows":1}
    {"call_type":"acompletion","status":"failure","model":"openai/gpt-5.4-mini","model_group":"gpt-5.4-mini","holds_prompt_text":false,"rows":1}
    {"call_type":"acompletion","status":"success","model":"openai/gpt-5.4-mini","model_group":"gpt-5.4-mini","holds_prompt_text":false,"rows":1}
    {"call_type":"acompletion","status":"failure","model":"unknown-model","model_group":"","holds_prompt_text":false,"rows":3}
    {"call_type":"acompletion","status":"failure","model":"unknown-model","model_group":"","holds_prompt_text":true,"rows":1}
    {"call_type":"acreate_batch","status":"failure","model":"gpt-nonexistent-9","model_group":"","holds_prompt_text":false,"rows":1}
    {"call_type":"acreate_batch","status":"failure","model":"unknown-model","model_group":"","holds_prompt_text":true,"rows":1}
    {"call_type":"amoderation","status":"success","model":"openai/omni-moderation-latest","model_group":"omni-moderation-latest","holds_prompt_text":false,"rows":1}
    {"call_type":"anthropic_messages","status":"success","model":"anthropic/claude-haiku-4-5","model_group":"claude-haiku-4-5","holds_prompt_text":false,"rows":1}
    {"call_type":"aresponses","status":"success","model":"openai/gpt-5.4-mini","model_group":"gpt-5.4-mini","holds_prompt_text":false,"rows":1}
    {"call_type":"aresponses","status":"failure","model":"unknown-model","model_group":"","holds_prompt_text":false,"rows":1}
    

The rows written by POST /v1/files (call_type IGNORE_THIS), read back one by one

  1. curl -sS 'http://127.0.0.1:23858/spend/logs?start_date=2026-09-18&end_date=2026-09-20&summarize=false' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<rows with call_type IGNORE_THIS>'

    {
      "request_id": "a1b8d9fd-8163-4a96-b38a-06df18744919",
      "startTime": "2026-09-19T09:33:35.561000Z",
      "call_type": "IGNORE_THIS",
      "status": "failure",
      "model": "",
      "model_group": "",
      "model_id": "",
      "error_message": "400: {'error': \"Model 'opus-4.6 Please summarize my medical records\\nPatient has diabetes' not found in model_list. Please check your conf... (+13 chars)
      "traceback_holds_prompt_text": false,
      "field_paths_holding_prompt_text": [
        "metadata.error_information.error_message"
      ]
    }
    {
      "request_id": "c5fdafa0-4583-418c-bd6a-0770efcc23b3",
      "startTime": "2026-09-19T09:26:14.288000Z",
      "call_type": "IGNORE_THIS",
      "status": "failure",
      "model": "",
      "model_group": "",
      "model_id": "",
      "error_message": "400: {'error': \"Model 'opus-4.6 Please summarize my medical records\\nPatient has diabetes' not found in model_list. Please check your conf... (+13 chars)
      "traceback_holds_prompt_text": false,
      "field_paths_holding_prompt_text": [
        "metadata.error_information.error_message"
      ]
    }
    

Hostile input: a 3948-character prompt-shaped model on POST /v1/moderations

model string: 3948 characters, 32 lines. First 2 lines and last line:
opus-4.6 Please summarize the medical records below for the insurance appeal
Visit 001 on ward D07: patient Jordan Example reported symptom code 137 and the nurse wrote dosage note 23 about insulin
Visit 032 on ward S24: patient Jordan Example reported symptom code 384 and the nurse wrote dosage note 66 about insulin
  1. curl -sS -X POST http://127.0.0.1:33372/v1/moderations -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d @long_body.json # {"input":"hi","model":<the string above>}

    HTTP 400
    x-litellm-call-id: 2026ae06-57a2-4d71-8a7e-0c30d0b60903
    response body, first 400 characters of 4289:
    {"error":{"message":"Error code: 400 - {'error': {'message': \"Invalid value for 'model' = opus-4.6 Please summarize the medical records below for the insurance... (+239 chars)
    
  2. curl -sS 'http://127.0.0.1:33372/spend/logs?request_id=2026ae06-57a2-4d71-8a7e-0c30d0b60903' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "",
     "status": "failure",
     "model": "opus-4.6 Please summarize the medical records below for the insurance appeal\nVisit 001 on ward D07: ... (+3848 chars)",
     "model_group": "opus-4.6 Please summarize the medical records below for the insurance appeal\nVisit 001 on ward D07: ... (+3848 chars)",
     "model_id": "",
     "error_message_length": 2293,
     "error_message": "Error code: 400 - {'error': {'message': \"Invalid value for 'model' = opus-4.6 Please summarize the m... (+2193 chars)",
     "traceback_length": 0
    }
    
  3. ... | jq --rawfile m long_model.txt -f slices.jq # every 40-character slice of the model string (offsets 0 to length-40) tested against the JSON-encoded row, its string values, and its string values with literal backslash-n turned into newlines

    {
     "model_string_length": 3948,
     "slices_tested": 3909,
     "slices_found_in_row": 3909,
     "first_hit_offsets": [
      0,
      1,
      2,
      3,
      4
     ],
     "row_holds_Jordan_Example": true,
     "row_holds_insulin": true,
     "row_holds_insurance_appeal": true,
     "row_holds_Visit_NNN": true
    }
    
  4. ... | jq -r '.[0].metadata.error_information.traceback' | grep -n -i -E 'truncat' | cut -c1-300 # truncation markers in the traceback

    (end of marker search)
    
  5. ... | jq --rawfile m long_model.txt '<same test with 12-character slices against the JSON-encoded row>'

    {
     "slices_tested": 3937,
     "slices_found_in_row": 3937
    }
    

Prompt storage on: POST /v1/moderations with prompt text in model

  1. curl -sS -D - -X POST http://127.0.0.1:29160/v1/moderations -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"input":"hi","model":"opus-4.6 Please summarize my medical records\nPatient has diabetes"}'

    HTTP 400
    x-litellm-call-id: e9e34900-9a74-49fb-aa85-2399adbf5682
    {"error":{"message":"Error code: 400 - {'error': {'message': \"Invalid value for 'model' = opus-4.6 Please summarize my medical records\\nPatient has diabetes. ... (+184 chars)
    
  2. curl -sS 'http://127.0.0.1:29160/spend/logs?request_id=e9e34900-9a74-49fb-aa85-2399adbf5682' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "",
     "status": "failure",
     "model": "opus-4.6 Please summarize my medical records\nPatient has diabetes",
     "model_group": "opus-4.6 Please summarize my medical records\nPatient has diabetes",
     "model_id": "",
     "stored_request_body_model": "opus-4.6 Please summarize my medical records\nPatient has diabetes",
     "stored_request_body_holds_prompt_text": true,
     "stored_messages": {},
     "error_message": "Error code: 400 - {'error': {'message': \"Invalid value for 'model' = opus-4.6 Please summarize my me... (+157 chars)",
     "whole_row_holds_prompt_text": true
    }
    

Prompt storage on: POST /v1/batches with prompt text in model

  1. curl -sS -D - -X POST http://127.0.0.1:29160/v1/batches -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"input_file_id":"file-lit7620-does-not-exist","endpoint":"/v1/chat/completions","completion_window":"24h","model":"opus-4.6 Please summarize my medical records\nPatient has diabetes"}'

    HTTP 400
    x-litellm-call-id: ebbb1ef1-19df-445d-8396-954c4a14cb4f
    {"error":{"message":"{'error': \"Model 'opus-4.6 Please summarize my medical records\\nPatient has diabetes' not found in model_list. Please check your config.y... (+67 chars)
    
  2. curl -sS 'http://127.0.0.1:29160/spend/logs?request_id=ebbb1ef1-19df-445d-8396-954c4a14cb4f' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acreate_batch",
     "status": "failure",
     "model": "unknown-model",
     "model_group": "",
     "model_id": "",
     "stored_request_body_model": "opus-4.6 Please summarize my medical records\nPatient has diabetes",
     "stored_request_body_holds_prompt_text": true,
     "stored_messages": {},
     "error_message": "400: {'error': \"Model 'opus-4.6 Please summarize my medical records\\nPatient has diabetes' not found... (+48 chars)",
     "whole_row_holds_prompt_text": true
    }
    

Prompt storage on: POST /anthropic-adapter/v1/messages with prompt text in model

  1. curl -sS -D - -X POST http://127.0.0.1:29160/anthropic-adapter/v1/messages -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"model":"opus-4.6 Please summarize my medical records\nPatient has diabetes","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'

    HTTP 400
    x-litellm-call-id: 4f848255-6968-47a6-8f1b-7b003699a0ef
    {"error":{"message":"400: {'error': 'completion: Invalid model name passed in model=opus-4.6 Please summarize my medical records\\nPatient has diabetes'}","type... (+53 chars)
    
  2. curl -sS 'http://127.0.0.1:29160/spend/logs?request_id=4f848255-6968-47a6-8f1b-7b003699a0ef' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acompletion",
     "status": "failure",
     "model": "unknown-model",
     "model_group": "",
     "model_id": "",
     "stored_request_body_model": "opus-4.6 Please summarize my medical records\nPatient has diabetes",
     "stored_request_body_holds_prompt_text": true,
     "stored_messages": {},
     "error_message": "400: {'error': 'completion: Invalid model name passed in model=opus-4.6 Please summarize my medical ... (+31 chars)",
     "whole_row_holds_prompt_text": true
    }
    

Prompt storage on, control: healthy POST /v1/chat/completions on a configured model

  1. curl -sS -D - -X POST http://127.0.0.1:29160/v1/chat/completions -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"model":"gpt-5.4-mini","messages":[{"role":"user","content":"Reply with the single word ok"}],"max_tokens":5}'

    HTTP 200
    x-litellm-call-id: 16225467-c3a0-46a7-b5a5-32a9bac1cbf0
    {"id":"chatcmpl-EPmZultW5iqrtKixZ2l1rqud1nBF8","created":1789813642,"model":"gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index"... (+240 chars)
    
  2. curl -sS 'http://127.0.0.1:29160/spend/logs?request_id=16225467-c3a0-46a7-b5a5-32a9bac1cbf0' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acompletion",
     "status": "success",
     "model": "openai/gpt-5.4-mini",
     "model_group": "gpt-5.4-mini",
     "model_id": "fbb196c899041a89ba318c64bd6f9d4ca15e6d1b6831cecc0c6fde81870b6e4c",
     "stored_request_body_model": "gpt-5.4-mini",
     "stored_request_body_holds_prompt_text": false,
     "stored_messages": {},
     "error_message": null,
     "whole_row_holds_prompt_text": false
    }
    

After (aadadc6)

POST /v1/moderations with prompt text in model

  1. curl -sS -X POST http://127.0.0.1:22193/v1/moderations -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"input":"hi","model":"opus-4.6 Please summarize my medical records\nPatient has diabetes"}'

    HTTP 400
    x-litellm-call-id: 9831eb04-1e5b-47a2-8df2-d3d879d0ba3d
    {"error":{"message":"Error code: 400 - {'error': {'message': \"Invalid value for 'model' = opus-4.6 Please summarize my medical records\\nPatient has diabetes. ... (+184 chars)
    
  2. curl -sS 'http://127.0.0.1:22193/spend/logs?request_id=9831eb04-1e5b-47a2-8df2-d3d879d0ba3d' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "",
     "status": "failure",
     "model": "unknown-model",
     "model_group": "",
     "model_id": "",
     "error_message": "Error code: 400 - {'error': {'message': \"Invalid value for 'model' = unknown-model. Please check the... (+104 chars)",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": false
    }
    

POST /v1/batches with prompt text in model

  1. curl -sS -X POST http://127.0.0.1:22193/v1/batches -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"input_file_id":"file-lit7620-does-not-exist","endpoint":"/v1/chat/completions","completion_window":"24h","model":"opus-4.6 Please summarize my medical records\nPatient has diabetes"}'

    HTTP 400
    x-litellm-call-id: 894237ec-bcbb-41fa-a9c9-d42909ddc2e7
    {"error":{"message":"{'error': 'batch creation: Invalid model name passed in model=opus-4.6 Please summarize my medical records\\nPatient has diabetes. Call `/v... (+110 chars)
    
  2. curl -sS 'http://127.0.0.1:22193/spend/logs?request_id=894237ec-bcbb-41fa-a9c9-d42909ddc2e7' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acreate_batch",
     "status": "failure",
     "model": "unknown-model",
     "model_group": "",
     "model_id": "",
     "error_message": "batch creation: Invalid model name passed in. Call `/v1/models` to view available models for your ke... (+2 chars)",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": false
    }
    

POST /anthropic-adapter/v1/messages with prompt text in model

  1. curl -sS -X POST http://127.0.0.1:22193/anthropic-adapter/v1/messages -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"model":"opus-4.6 Please summarize my medical records\nPatient has diabetes","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'

    HTTP 400
    x-litellm-call-id: 9921634c-cdde-4dd4-b859-3cb19b16aef6
    {"error":{"message":"400: {'error': 'completion: Invalid model name passed in model=opus-4.6 Please summarize my medical records\\nPatient has diabetes. Call `/... (+111 chars)
    
  2. curl -sS 'http://127.0.0.1:22193/spend/logs?request_id=9921634c-cdde-4dd4-b859-3cb19b16aef6' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acompletion",
     "status": "failure",
     "model": "unknown-model",
     "model_group": "",
     "model_id": "",
     "error_message": "completion: Invalid model name passed in. Call `/v1/models` to view available models for your key.",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": false
    }
    

POST /v1/files with prompt text in model

  1. curl -sS -X POST http://127.0.0.1:22193/v1/files -H 'Authorization: Bearer sk-lit7620-qa' -F purpose=batch -F 'model=<the same prompt-shaped string, with a real newline>' -F file=@batch.jsonl

    HTTP 400
    x-litellm-call-id: 
    {"error":{"message":"{'error': 'file upload: Invalid model name passed in model=opus-4.6 Please summarize my medical records\\nPatient has diabetes. Call `/v1/m... (+107 chars)
    
    NO SPEND LOG ROW FOUND after 90s for ids:
    

GET /v1/batches?model=

  1. curl -sS 'http://127.0.0.1:22193/v1/batches?model=opus-4.6%20Please%20summarize%20my%20medical%20records%0APatient%20has%20diabetes' -H 'Authorization: Bearer sk-lit7620-qa'

    HTTP 200
    x-litellm-call-id: 
    {"object":"list","data":[],"first_id":null,"last_id":null,"has_more":false}
    
    NO SPEND LOG ROW FOUND after 90s for ids:
    

Sane unknown model name on POST /v1/moderations

  1. curl -sS -X POST http://127.0.0.1:22193/v1/moderations -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"input":"hi","model":"text-moderation-nonexistent-9"}'

    HTTP 400
    x-litellm-call-id: 8a8461c5-55dd-4334-bccb-68e0e79172ad
    {"error":{"message":"Error code: 400 - {'error': {'message': \"Invalid value for 'model' = text-moderation-nonexistent-9. Please check the OpenAI documentation ... (+146 chars)
    
  2. curl -sS 'http://127.0.0.1:22193/spend/logs?request_id=8a8461c5-55dd-4334-bccb-68e0e79172ad' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "",
     "status": "failure",
     "model": "text-moderation-nonexistent-9",
     "model_group": "text-moderation-nonexistent-9",
     "model_id": "",
     "error_message": "Error code: 400 - {'error': {'message': \"Invalid value for 'model' = text-moderation-nonexistent-9. ... (+120 chars)",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": false
    }
    

Sane unknown model name on POST /v1/batches

  1. curl -sS -X POST http://127.0.0.1:22193/v1/batches -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"input_file_id":"file-lit7620-does-not-exist","endpoint":"/v1/chat/completions","completion_window":"24h","model":"gpt-nonexistent-9"}'

    HTTP 400
    x-litellm-call-id: cf87088c-955f-454d-a811-d3ebabfc7131
    {"error":{"message":"{'error': 'batch creation: Invalid model name passed in model=gpt-nonexistent-9. Call `/v1/models` to view available models for your key.'}... (+60 chars)
    
  2. curl -sS 'http://127.0.0.1:22193/spend/logs?request_id=cf87088c-955f-454d-a811-d3ebabfc7131' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acreate_batch",
     "status": "failure",
     "model": "unknown-model",
     "model_group": "",
     "model_id": "",
     "error_message": "batch creation: Invalid model name passed in. Call `/v1/models` to view available models for your ke... (+2 chars)",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": false
    }
    

Configured model group with spaces in its name, the provider rejects the call

  1. curl -sS -X POST http://127.0.0.1:22193/v1/chat/completions -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"model":"Team GPT Mini","messages":[{"role":"narrator","content":"hi"}]}'

    HTTP 400
    x-litellm-call-id: 726dd001-e4f1-4ef1-bcfe-27d8b0729af5
    {"error":{"message":"litellm.BadRequestError: OpenAIException - Invalid value: 'narrator'. Supported values are: 'system', 'assistant', 'user', 'function', 'too... (+168 chars)
    
  2. curl -sS 'http://127.0.0.1:22193/spend/logs?request_id=726dd001-e4f1-4ef1-bcfe-27d8b0729af5' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acompletion",
     "status": "failure",
     "model": "openai/gpt-5.4-mini",
     "model_group": "Team GPT Mini",
     "model_id": "1c1f423cd7f44bd5d09819eac0748e17af1ce23d27a66ac0eead407e8bcdf7b5",
     "error_message": "litellm.BadRequestError: OpenAIException - Invalid value: 'narrator'. Supported values are: 'system'... (+132 chars)",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": false
    }
    

Configured model group with spaces in its name, every deployment has a bad key

Twelve identical calls; statuses in order: 3 x 401, 9 x 429. Call 4 (every deployment already cooling down, so the router fails before picking one) shown; the full transcript holds all twelve

  1. curl -sS -X POST http://127.0.0.1:22193/v1/chat/completions -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"model":"Broken GPT Mini","messages":[{"role":"user","content":"hi"}]}'

    HTTP 429
    x-litellm-call-id: 97e54ae0-3d24-4e1e-89a9-e47f4790e08a
    {"error":{"message":"No deployments available for selected model, Try again in 300 seconds. All deployments for selected model are in cooldown. Passed model=Bro... (+253 chars)
    
  2. curl -sS 'http://127.0.0.1:22193/spend/logs?request_id=97e54ae0-3d24-4e1e-89a9-e47f4790e08a' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acompletion",
     "status": "failure",
     "model": "Broken GPT Mini",
     "model_group": "Broken GPT Mini",
     "model_id": "",
     "error_message": "No deployments available for selected model, Try again in 300 seconds. All deployments for selected ... (+226 chars)",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": false
    }
    

Every spend log row of the run, grouped by call_type, status, model, and model_group, and how many rows hold the prompt text

  1. curl -sS 'http://127.0.0.1:22193/spend/logs/v2?start_date=2026-09-19%2000:00:00&end_date=2026-09-20%2023:59:59&page=1&page_size=100' -H 'Authorization: Bearer sk-lit7620-qa' | jq '{total, rows: (.data|length)}'

    {
     "total": 30,
     "rows": 30
    }
    
  2. ... | jq -r '.data[] | [.call_type, .status, .model, .model_group] | @tsv' | sort | uniq -c

       1 	failure	text-moderation-nonexistent-9	text-moderation-nonexistent-9
       1 	failure	unknown-model	
       9 acompletion	failure	Broken GPT Mini	Broken GPT Mini
       3 acompletion	failure	openai/gpt-5.4-mini	Broken GPT Mini
       1 acompletion	failure	openai/gpt-5.4-mini	gpt-5.4-mini
       1 acompletion	failure	openai/gpt-5.4-mini	Team GPT Mini
       4 acompletion	failure	unknown-model	
       1 acompletion	success	openai/gpt-5.4-mini	gpt-5.4-mini
       1 acompletion	success	openai/gpt-5.4-mini	Team GPT Mini
       2 acreate_batch	failure	unknown-model	
       1 amoderation	success	openai/omni-moderation-latest	omni-moderation-latest
       1 anthropic_messages	success	anthropic/claude-haiku-4-5	claude-haiku-4-5
       1 aresponses	failure	unknown-model	
       1 aresponses	success	openai/gpt-5.4-mini	gpt-5.4-mini
       2 IGNORE_THIS	failure	unknown-model
    
  3. ... | jq '[.data[] | select(tostring | test("medical records|Patient has diabetes|summarize my"))] | length' # rows holding the prompt text anywhere

    0
    
  4. ... | jq '[.data[] | select((.call_type|test("file|list_batches|alist")) )] | map({request_id, call_type, model, model_group})' # rows from the files or list-batches routes

    []
    

The rows written by POST /v1/files (call_type IGNORE_THIS), read back one by one

  1. ... | jq '.data[] | select(.call_type=="IGNORE_THIS") | {request_id, startTime, call_type, status, model, model_group, model_id, api_base}'

    {
      "request_id": "ff819b50-1df6-4e2e-b6d6-eabba8e80b5c",
      "startTime": "2026-09-19T11:52:01.871+00:00",
      "call_type": "IGNORE_THIS",
      "status": "failure",
      "model": "unknown-model",
      "model_group": "",
      "model_id": "",
      "api_base": ""
    }
    {
      "request_id": "1afc278d-a79a-440b-a540-f2c687fa44e9",
      "startTime": "2026-09-19T11:46:08.761+00:00",
      "call_type": "IGNORE_THIS",
      "status": "failure",
      "model": "unknown-model",
      "model_group": "",
      "model_id": "",
      "api_base": ""
    }
    
  2. curl -sS 'http://127.0.0.1:22193/spend/logs?request_id=ff819b50-1df6-4e2e-b6d6-eabba8e80b5c' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields, plus route and traceback head>'

    {
     "startTime": "2026-09-19T11:52:01.871000Z",
     "call_type": "IGNORE_THIS",
     "status": "failure",
     "model": "unknown-model",
     "model_group": "",
     "model_id": "",
     "user_api_key_request_route": null,
     "error_message": "file upload: Invalid model name passed in. Call `/v1/models` to view available models for your key.",
     "error_class": "ProxyModelNotFoundError",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": false
    }
    
  3. curl -sS 'http://127.0.0.1:22193/spend/logs?request_id=1afc278d-a79a-440b-a540-f2c687fa44e9' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields, plus route and traceback head>'

    {
     "startTime": "2026-09-19T11:46:08.761000Z",
     "call_type": "IGNORE_THIS",
     "status": "failure",
     "model": "unknown-model",
     "model_group": "",
     "model_id": "",
     "user_api_key_request_route": null,
     "error_message": "file upload: Invalid model name passed in. Call `/v1/models` to view available models for your key.",
     "error_class": "ProxyModelNotFoundError",
     "traceback_holds_prompt_text": false,
     "whole_row_holds_prompt_text": false
    }
    

Hostile input: a 3948-character prompt-shaped model on POST /v1/moderations

model string: 3948 characters, 32 lines. First 2 lines and last line:
opus-4.6 Please summarize the medical records below for the insurance appeal
Visit 001 on ward D07: patient Jordan Example reported symptom code 137 and the nurse wrote dosage note 23 about insulin
Visit 032 on ward S24: patient Jordan Example reported symptom code 384 and the nurse wrote dosage note 66 about insulin
  1. curl -sS -X POST http://127.0.0.1:22193/v1/moderations -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d @long_body.json # {"input":"hi","model":<the string above>}

    HTTP 400
    x-litellm-call-id: ae3557c3-64c8-476e-9171-4df1a124f1fc
    response body, first 400 characters of 4289:
    {"error":{"message":"Error code: 400 - {'error': {'message': \"Invalid value for 'model' = opus-4.6 Please summarize the medical records below for the insurance... (+239 chars)
    
  2. curl -sS 'http://127.0.0.1:22193/spend/logs?request_id=ae3557c3-64c8-476e-9171-4df1a124f1fc' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "",
     "status": "failure",
     "model": "unknown-model",
     "model_group": "",
     "model_id": "",
     "error_message_length": 463,
     "error_message": "Error code: 400 - {'error': {'message': \"Invalid value for 'model' = unknown-model... (litellm_trunc... (+363 chars)",
     "traceback_length": 0
    }
    
  3. ... | jq --rawfile m long_model.txt -f slices.jq # every 40-character slice of the model string (offsets 0 to length-40) tested against the JSON-encoded row, its string values, and its string values with literal backslash-n turned into newlines

    {
     "model_string_length": 3948,
     "slices_tested": 3909,
     "slices_found_in_row": 0,
     "first_hit_offsets": [],
     "row_holds_Jordan_Example": false,
     "row_holds_insulin": false,
     "row_holds_insurance_appeal": false,
     "row_holds_Visit_NNN": false
    }
    
  4. ... | jq -r '.[0].metadata.error_information.traceback' | grep -n -i -E 'truncat' | cut -c1-300 # truncation markers in the traceback

    (end of marker search)
    
  5. ... | jq --rawfile m long_model.txt '<same test with 12-character slices against the JSON-encoded row>'

    {
     "slices_tested": 3937,
     "slices_found_in_row": 0
    }
    

Merged tip only: a virtual key restricted to gpt-5.4-mini

  1. curl -sS -X POST http://127.0.0.1:27326/key/generate -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"models": ["gpt-5.4-mini"], "key_alias": "lit7620-restricted"}' | jq '{key: (.key | .[0:8] + "..."), models}'

    {
     "key": "sk-jEoQF...",
     "models": [
      "gpt-5.4-mini"
     ]
    }
    

Merged tip only: the restricted key sends POST /v1/batches with prompt text in model

  1. curl -sS -X POST http://127.0.0.1:27326/v1/batches -H 'Authorization: Bearer <restricted key>' -H 'Content-Type: application/json' -d '{"input_file_id": "file-abc123", "endpoint": "/v1/chat/completions", "completion_window": "24h", "model": "opus-4.6 Please summarize my medical records\nPatient has diabetes"}'

    HTTP 403
    x-litellm-call-id: 
    {"error":{"message":"The requested model 'opus-4.6 Please summarize my medical records\nPatient has diabetes' is not available for this API key, or the model na... (+128 chars)
    
  2. curl -sS 'http://127.0.0.1:27326/spend/logs/v2?status=failure&start_date=2026-09-19%2000:00:00&end_date=2026-09-20%2023:59:59&page=1&page_size=100' -H 'Authorization: Bearer sk-lit7620-qa' | jq '[.data[] | select(.metadata.user_api_key_alias == "lit7620-restricted")] | map(<row fields>)'

    [
     {
      "call_type": "acreate_batch",
      "status": "failure",
      "model": "unknown-model",
      "model_group": "",
      "model_id": "",
      "key_alias": "lit7620-restricted",
      "error_message": "The requested model 'unknown-model' is not available for this API key, or the model name is invalid.... (+49 chars)",
      "whole_row_holds_prompt_text": false
     }
    ]
    

Merged tip only: the restricted key sends POST /v1/files with prompt text in model

  1. curl -sS -X POST http://127.0.0.1:27326/v1/files -H 'Authorization: Bearer <restricted key>' -F purpose=batch -F 'model=<the same prompt-shaped string, with a real newline>' -F file=@batch.jsonl

    HTTP 403
    x-litellm-call-id: 
    {"error":{"message":"The requested model 'opus-4.6 Please summarize my medical records\nPatient has diabetes' is not available for this API key, or the model na... (+128 chars)
    
  2. curl -sS 'http://127.0.0.1:27326/spend/logs/v2?status=failure&start_date=2026-09-19%2000:00:00&end_date=2026-09-20%2023:59:59&page=1&page_size=100' -H 'Authorization: Bearer sk-lit7620-qa' | jq '[.data[] | select(.metadata.user_api_key_alias == "lit7620-restricted")] | map(<row fields>)'

    [
     {
      "call_type": "/v1/files",
      "status": "failure",
      "model": "unknown-model",
      "model_group": "",
      "model_id": "",
      "key_alias": "lit7620-restricted",
      "error_message": "The requested model 'unknown-model' is not available for this API key, or the model name is invalid.... (+49 chars)",
      "whole_row_holds_prompt_text": false
     },
     {
      "call_type": "acreate_batch",
      "status": "failure",
      "model": "unknown-model",
      "model_group": "",
      "model_id": "",
      "key_alias": "lit7620-restricted",
      "error_message": "The requested model 'unknown-model' is not available for this API key, or the model name is invalid.... (+49 chars)",
      "whole_row_holds_prompt_text": false
     }
    ]
    

Prompt storage on: POST /v1/moderations with prompt text in model

  1. curl -sS -D - -X POST http://127.0.0.1:28229/v1/moderations -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"input":"hi","model":"opus-4.6 Please summarize my medical records\nPatient has diabetes"}'

    HTTP 400
    x-litellm-call-id: 620127d9-b1f8-486d-9d81-cd8971f50ab2
    {"error":{"message":"Error code: 400 - {'error': {'message': \"Invalid value for 'model' = opus-4.6 Please summarize my medical records\\nPatient has diabetes. ... (+184 chars)
    
  2. curl -sS 'http://127.0.0.1:28229/spend/logs?request_id=620127d9-b1f8-486d-9d81-cd8971f50ab2' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "",
     "status": "failure",
     "model": "unknown-model",
     "model_group": "",
     "model_id": "",
     "stored_request_body_model": "unknown-model",
     "stored_request_body_holds_prompt_text": false,
     "stored_messages": {},
     "error_message": "Error code: 400 - {'error': {'message': \"Invalid value for 'model' = unknown-model. Please check the... (+104 chars)",
     "whole_row_holds_prompt_text": false
    }
    

Prompt storage on: POST /v1/batches with prompt text in model

  1. curl -sS -D - -X POST http://127.0.0.1:28229/v1/batches -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"input_file_id":"file-lit7620-does-not-exist","endpoint":"/v1/chat/completions","completion_window":"24h","model":"opus-4.6 Please summarize my medical records\nPatient has diabetes"}'

    HTTP 400
    x-litellm-call-id: 8c01bccd-1d31-45e9-a216-e7188734007f
    {"error":{"message":"{'error': 'batch creation: Invalid model name passed in model=opus-4.6 Please summarize my medical records\\nPatient has diabetes. Call `/v... (+110 chars)
    
  2. curl -sS 'http://127.0.0.1:28229/spend/logs?request_id=8c01bccd-1d31-45e9-a216-e7188734007f' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acreate_batch",
     "status": "failure",
     "model": "unknown-model",
     "model_group": "",
     "model_id": "",
     "stored_request_body_model": "unknown-model",
     "stored_request_body_holds_prompt_text": false,
     "stored_messages": {},
     "error_message": "batch creation: Invalid model name passed in. Call `/v1/models` to view available models for your ke... (+2 chars)",
     "whole_row_holds_prompt_text": false
    }
    

Prompt storage on: POST /anthropic-adapter/v1/messages with prompt text in model

  1. curl -sS -D - -X POST http://127.0.0.1:28229/anthropic-adapter/v1/messages -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"model":"opus-4.6 Please summarize my medical records\nPatient has diabetes","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'

    HTTP 400
    x-litellm-call-id: bd08bf0e-3772-44ab-91a8-662441026100
    {"error":{"message":"400: {'error': 'completion: Invalid model name passed in model=opus-4.6 Please summarize my medical records\\nPatient has diabetes. Call `/... (+111 chars)
    
  2. curl -sS 'http://127.0.0.1:28229/spend/logs?request_id=bd08bf0e-3772-44ab-91a8-662441026100' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acompletion",
     "status": "failure",
     "model": "unknown-model",
     "model_group": "",
     "model_id": "",
     "stored_request_body_model": "unknown-model",
     "stored_request_body_holds_prompt_text": false,
     "stored_messages": {},
     "error_message": "completion: Invalid model name passed in. Call `/v1/models` to view available models for your key.",
     "whole_row_holds_prompt_text": false
    }
    

Prompt storage on, control: healthy POST /v1/chat/completions on a configured model

  1. curl -sS -D - -X POST http://127.0.0.1:28229/v1/chat/completions -H 'Authorization: Bearer sk-lit7620-qa' -H 'Content-Type: application/json' -d '{"model":"gpt-5.4-mini","messages":[{"role":"user","content":"Reply with the single word ok"}],"max_tokens":5}'

    HTTP 200
    x-litellm-call-id: feadec69-04b6-4253-8a59-1696e8b8eba4
    {"id":"chatcmpl-EPnyOc43TcUF1wOVS6AZRBk1U47yK","created":1789819004,"model":"gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index"... (+240 chars)
    
  2. curl -sS 'http://127.0.0.1:28229/spend/logs?request_id=feadec69-04b6-4253-8a59-1696e8b8eba4' -H 'Authorization: Bearer sk-lit7620-qa' | jq '<row fields>'

    {
     "call_type": "acompletion",
     "status": "success",
     "model": "openai/gpt-5.4-mini",
     "model_group": "gpt-5.4-mini",
     "model_id": "fbb196c899041a89ba318c64bd6f9d4ca15e6d1b6831cecc0c6fde81870b6e4c",
     "stored_request_body_model": "gpt-5.4-mini",
     "stored_request_body_holds_prompt_text": false,
     "stored_messages": {},
     "error_message": null,
     "whole_row_holds_prompt_text": false
    }
    

Type

Bug Fix

Caveats (if any)

Medium

  • Cold storage copies of the request body are not scrubbed; the log drawer prefers that copy when the row's fields are empty. Assumed from reading the drawer override code, not driven live; cold storage keeps full prompts by design

Low

  • CircleCI at the tip (pipeline 89869) has two reds, neither in this diff. proxy_e2e_anthropic_messages_tests is red on test_bedrock_invoke_messages_with_all_beta_headers (Bedrock 400 invalid beta flag, opus-4.5 and sonnet-4.5 converse) here, at the previous tip (89863), and on the last two main pipelines (89818, 89834), both older than the main this tip merges; no fix on main yet. google_generate_content_endpoint_testing hit Vertex 429 Resource exhausted on the three test_proxy_genai_sdk_* tests, as did every pipeline across eight branches from 10:27Z to 11:54Z (89859 to 89870); green at 10:17Z and on main 89834
  • The two reds of the previous tip that were fixes this branch predated cleared with the main merge: litellm_utils_testing test_models_by_provider (fix(proxy): register transcribe as a known provider for model grants #41926) and the local_testing_part2 MockValSer flake are green at 89869
  • The Before side ran at 5fc510a, the merge base before main was merged in; the merge moved it to 1f6e5b6, and none of the 53 main commits in between touches the spend log payload builder, the moderations route, or the adapter route. The files and batches helper gained feat(batches): support Mistral files/batches and per-page OCR batch cost tracking (internal copy of #40484) #41934's grant check, which passes the master key through to the same not found in model_list error the Before side shows
  • /spend/logs/v2 model_group and error_message filters no longer find these rows by the client string; filter on model=unknown-model instead
  • Operators no longer see the rejected string in the row; the caller's 400 still echoes it
  • A sane unknown model on /v1/batches now saves unknown-model instead of the name, matching the chat routes
  • The client-facing 400 wording on files and batches changes to the shared Invalid model name passed in text
  • GET /v1/batches?model=<x> writes no spend row at all, before and after; left alone
  • POST /v1/files returns no x-litellm-call-id header and saves a call_type: IGNORE_THIS row, before and after; left alone
  • A sane unknown model on /v1/moderations still saves its name, since the provider rejects it and the name is not prompt-shaped; left alone
  • The adapter route's happy path 500s with No matching adapter given, before and after (LIT-7515); left alone
  • Daily spend rows that already exist for unknown-model keep their first-inserted model_group
  • Without Redis, cooldowns are per worker, so the all-deployments-cooling case takes more calls with two workers; left alone
  • GCS pub/sub v1 success payloads placeholder without a router-based group rescue; its failure path never runs
  • Assumption: a wildcard-only configured group whose name holds whitespace is placeholdered when every deployment is cooling down. If wrong, such rows save unknown-model with an empty group instead of the group name

Final Attestation

  • I have read the entire PR description and it is up to date with the latest commits, and I have gone through the entire PR diff line-by-line and it has no bugs, no issues, no regressions, no vulnerabilities, no drops in quality of code (e.g., DRY, code smells, magic strings, etc.), and no low quality tests. The PR meets the bar of a senior engineer at a top-tier tech company

  • aadadc6 passes /live-pr-risk

@devin-ai-integration

devin-ai-integration Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no outstanding blocking or non-blocking defects were identified.

Summary

This PR adds security hardening to rejected requests and preserves existing client-facing error behavior.

  • Uses the shared unknown-model error for files, batches, and adapter requests.
  • Sanitizes placeholdered spend-log fields and stored request bodies.
  • Preserves configured whitespace-containing model groups and routed failures.
  • Adds regression coverage for error sanitization, truncated values, prompt storage, and endpoint behavior.

Reviews (5) · Last reviewed commit: "Merge remote-tracking branch 'origin/mai..."

Comment thread litellm/proxy/spend_tracking/spend_tracking_utils.py
Comment thread litellm/proxy/openai_files_endpoints/common_utils.py
Comment thread tests/test_litellm/proxy/openai_files_endpoint/test_files_common_utils.py Outdated
@codecov

codecov Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.24561% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...tellm/proxy/spend_tracking/spend_tracking_utils.py 98.11% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_unknown_model_spend_logs_outside_router (aadadc6) with main (1f6e5b6)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (dad8c32) during the generation of this report, so 1f6e5b6 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

…when a spend row is placeholdered

With store_prompts_in_spend_logs on, the persisted request body kept the client's model string even when the row's model, model_group, and error text had been replaced by the unknown-model placeholder. The body's model now takes the same placeholder on those rows. Also annotates the new test locals with Final and wraps the four test lines that ran past 120 characters.
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 19, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

…_spend_logs_outside_router

# Conflicts:
#	litellm/proxy/openai_files_endpoints/common_utils.py
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 19, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit aadadc6. Configure here.

@mateo-berri mateo-berri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mateo-berri
mateo-berri merged commit f15dba2 into main Sep 19, 2026
144 of 147 checks passed
@mateo-berri
mateo-berri deleted the litellm_unknown_model_spend_logs_outside_router branch September 19, 2026 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant