Skip to main content

Passthrough Managed IDs

When you use LiteLLM's passthrough endpoints (e.g. /openai_passthrough/v1/files, /azure/openai/batches) the upstream provider returns its own raw IDs such as file-abc123 or batch_xyz. By default those IDs are returned directly to your client, which means:

  • Any user who guesses or intercepts another user's file-abc123 can use it.
  • You have no proxy-level record of who owns what.
  • Multi-tenant isolation has to be done entirely in your application code.

Passthrough Managed IDs solves this. When the feature is enabled the proxy:

  1. Mints a stable, opaque managed ID for every raw provider ID it sees in a response.
  2. Stores the managed_id → raw_id mapping in the proxy database, tagged with the creating user/team.
  3. Resolves a managed ID back to the raw provider ID just before forwarding any request upstream, after running an ownership/permission check.

Your clients never see raw provider IDs and can never access resources they do not own, even if they guess or forge a managed ID string.

How to enable

Add one line to general_settings in your proxy config:

general_settings:
passthrough_managed_object_ids: true

The feature requires:

  • A database configured for the proxy (Prisma / PostgreSQL).
  • The managed_files enterprise hook to be available.

The feature is active only for OpenAI (/openai_passthrough/...) and Azure OpenAI (/azure/openai/...) passthrough routes.

/openai/v1/files, /openai/v1/batches, and /openai/v1/responses are not passthrough routes

Those three paths are served by LiteLLM's native endpoints, exactly like /v1/files, /v1/batches, and /v1/responses, so passthrough_managed_object_ids never sees them. To isolate tenants there, use require_managed_files for files and batches and the built-in Responses API ownership checks for responses. The passthrough prefix for OpenAI is /openai_passthrough.

Native managed endpoints vs passthrough

Native managed endpointsPassthrough with managed IDs
URL prefix/v1/files, /v1/batches/openai_passthrough/v1/files, /azure/openai/batches
RoutingLiteLLM internal logic; model-based routingDirect forward to upstream provider
Credential resolutionVia model_list routerVia PassthroughEndpointRouter / env vars
Use whenYou want LiteLLM to pick the right deployment automatically, or you need cross-provider batchingYou want to call a provider API directly (e.g. fine-tuning, responses, custom endpoints) but still need proxy-level access control
ID managementAlways managed by LiteLLMManaged IDs only when passthrough_managed_object_ids: true
Streaming ID mintingSupportedSupported for POST /v1/responses with stream: true (the response.id in every event is rewritten)

Supported endpoints

Response ID minting (OUTPUT)

These are the specific routes where LiteLLM will mint a managed ID for raw provider IDs it sees in the response body and swap them before returning to the client.

ProviderMethodPathFields rewritten
OpenAIPOST/v1/filesid (file-)
OpenAIGET/v1/files/{file_id}id (file-)
OpenAIDELETE/v1/files/{file_id}id (file-)
OpenAIPOST/v1/batchesid (batch_), input_file_id, output_file_id, error_file_id
OpenAIGET/v1/batches/{batch_id}id (batch_), input_file_id, output_file_id, error_file_id
OpenAIPOST/v1/batches/{batch_id}/cancelid (batch_), input_file_id, output_file_id, error_file_id
OpenAIPOST/v1/responsesid (resp_)
OpenAIGET/v1/responses/{response_id}id (resp_)
OpenAIDELETE/v1/responses/{response_id}id (resp_)
AzurePOST/v1/filesid (file-)
AzureGET/v1/files/{file_id}id (file-)
AzureDELETE/v1/files/{file_id}id (file-)
AzurePOST/v1/batchesid (batch_), input_file_id, output_file_id, error_file_id
AzureGET/v1/batches/{batch_id}id (batch_), input_file_id, output_file_id, error_file_id
AzurePOST/v1/batches/{batch_id}/cancelid (batch_), input_file_id, output_file_id, error_file_id
AzurePOST/v1/responsesid (resp_)
AzureGET/v1/responses/{response_id}id (resp_)
AzureDELETE/v1/responses/{response_id}id (resp_)

POST /v1/responses with stream: true is covered as well. The proxy records the response as the caller's own from the first response.created event and rewrites the response.id inside every event as the stream is relayed, so a streamed response is owned and protected exactly like a non-streamed one.

Managed ID resolution (INPUT)

This is not route-specific. For every OpenAI or Azure passthrough request, LiteLLM scans the entire request before forwarding it upstream:

LocationWhat is scanned
URL pathEach path segment
Query paramsEvery string-valued parameter
Request bodyAll string values, recursively (works in nested objects and arrays)

This means any endpoint that accepts a file ID, batch ID, or response ID in path, query, or body will automatically resolve managed IDs, including endpoints not listed in the output table above, such as fine-tuning jobs (/v1/fine_tuning/jobs), assistants, or any custom endpoint.

Example, fine-tuning job:

# Client sends managed IDs for training_file and validation_file
response = client.post("/azure/openai/v1/fine_tuning/jobs", json={
"model": "gpt-4o-mini",
"training_file": "bGl0ZWxsbV9wcm94eTpwYXNzdGhyb3VnaDtwcm92...", # managed ID
"validation_file": "bGl0ZWxsbV9wcm94eTpwYXNzdGhyb3VnaDtwcm92...", # managed ID
})

# Proxy resolves both to raw file IDs and forwards:
# POST .../fine_tuning/jobs
# { "model": "gpt-4o-mini", "training_file": "file-2dbc75...", "validation_file": "file-2dbc75..." }

Request flow - any endpoint

This applies to any OpenAI or Azure passthrough endpoint, not just fine-tuning. The same path/query/body scan runs on every request; the example below uses a fine-tuning job with a managed file ID in the body.

On the response path, rewrite_response_ids() mints managed IDs for raw provider IDs, but only on routes listed in the output map (files, batches, responses). Other endpoints (e.g. fine-tuning) return upstream IDs as-is unless they appear in that map.

Permission checks

Every managed ID resolution runs four checks in order. All must pass or the request is rejected.

1. Provider match

The managed ID encodes the provider it was minted for (e.g. azure). If you send an Azure-minted ID on an OpenAI passthrough route (or vice versa), the proxy returns 404 and never forwards the ID upstream.

2. DB existence

The managed ID must map to a real row in the proxy database. A guessed, forged, or base64-crafted string that does not correspond to a real row returns 404. The raw provider ID is never forwarded to the upstream when the DB check fails.

3. Access check - per-request

can_access_resource() decides whether the caller may use a specific resource:

Caller identityAccess granted when
Proxy admin / master keyAlways
Has user_idcreated_by == user_id
Has team_id (service account)team_id == resource.team_id
Has both user_id and team_idEither condition above
NeitherNever (403)

4. Access check - list endpoints

build_owner_filter() scopes the database query for list operations (see below). Same rules, expressed as a Prisma WHERE clause:

CallerWHERE clause
Proxy admin / master key{} (no filter — sees all rows)
user_id onlycreated_by = user_id
team_id onlyteam_id = team_id
Both user_id and team_idcreated_by = user_id OR team_id = team_id
NeitherEmpty list returned immediately — no DB query

How list endpoints work

GET /openai_passthrough/v1/files and GET /openai_passthrough/v1/batches (and their Azure equivalents) are fully intercepted. The request is never forwarded to the upstream provider. Instead, the proxy queries its own database and returns only the rows the caller owns:

GET /openai_passthrough/v1/files
┌─────────────────────────────┐
admin key? │ WHERE {} │
│ (all rows) │
└─────────────────────────────┘
user key? ┌─────────────────────────────┐
│ WHERE created_by = user_id │
│ OR team_id = team_id │
└─────────────────────────────┘


OpenAI-style paginated response
{ "object": "list", "data": [...] }
All IDs in data[] are managed IDs

Pagination parameters limit, after, and before are supported and map directly to a cursor on created_at.

A caller with no user_id and no team_id always receives an empty list; the proxy never falls back to an unscoped query.

Objects the proxy never saw

Ownership is recorded when an object is created through the passthrough route with the feature on. A file, batch, or response created before you enabled it, or created straight against the provider with the same API key, has no owner on the proxy side, so any key allowed on the passthrough route can still reach it by its raw provider ID (see the limitation below). LiteLLM deliberately does not try to claim those objects for anyone.

When those pre-existing objects must be isolated too, the strict option is to stop sharing one provider key across tenants: define one custom passthrough endpoint per team, each carrying that team's own provider API key in its headers, and restrict every team to its endpoint with allowed_passthrough_routes in the team's metadata. The provider then scopes visibility itself, and managed IDs keep working on top of that.

Limitations

Streaming is only rewritten for responses

POST /v1/responses streams are the only SSE responses that carry a mintable ID, so those are the only streams the proxy rewrites. Every other streaming passthrough response is relayed untouched.

Raw IDs only work for their owner

If you send a raw provider ID (e.g. file-abc123) instead of a managed ID, the proxy checks whether it belongs to a managed resource before forwarding it. A raw ID that maps to another caller's resource is refused with 404 (the same answer as an unknown managed ID, so callers cannot probe which IDs exist). The owner's own raw ID is forwarded. A raw ID the proxy has never recorded is forwarded without an ownership check, which is what makes the objects above reachable.

IDs are provider-scoped

A managed ID minted for azure cannot be used on openai passthrough routes and vice versa. Attempting to do so returns 404.