Skip to content

feat(proxy): type Customer Management response_model for OpenAPI coverage - #31043

Merged
ryan-crabbe-berri merged 13 commits into
litellm_internal_stagingfrom
litellm_customer_response_models
Jun 30, 2026
Merged

feat(proxy): type Customer Management response_model for OpenAPI coverage#31043
ryan-crabbe-berri merged 13 commits into
litellm_internal_stagingfrom
litellm_customer_response_models

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

With a proxy running against this branch, the OpenAPI schema documents a concrete response body for every Customer Management operation:

curl -s http://localhost:4000/openapi.json \
  | jq '.paths["/customer/new"].post.responses."200".content."application/json".schema,
        .paths["/customer/delete"].post.responses."200".content."application/json".schema'

/customer/new, /customer/update, /customer/info, /customer/list reference CustomerResponse; block/unblock/delete reference their dedicated models. Previously these returned the default untyped 200 response.

Type

🆕 New Feature
🧹 Refactoring
✅ Test

Changes

Two things, in order.

First, the Customer Management endpoint group is fully typed for OpenAPI coverage. The five previously-untyped operations (block, unblock, new, update, delete) get response models; block/unblock/delete get three small dedicated models in litellm/types/proxy/management_endpoints/customer_endpoints.py, and the customer-object endpoints (new/update/info/list) share CustomerResponse. Response fidelity was checked against the table schema: budget_id was missing from LiteLLM_EndUserTable and is added back, and the nested budget now uses LiteLLM_BudgetTableFull so server-managed fields (budget_reset_at, created_at) survive rather than being narrowed to the write-allowlist shape. No field the endpoints used to return is dropped. This also resolves the earlier review feedback on this PR (the silent budget_id / nested-budget narrowing, and the misleading UnblockUsersResponse.blocked_users name, now documented via a Field description).

Second, the customer response flow is made type-safe rather than relying on a runtime filter. The handlers no longer return an untyped dict from .model_dump() with response_model doing the shaping at the exit. Instead a single mapper, _to_customer_response, validates a DB row into CustomerResponse at one typed boundary, and the handlers construct and return typed models with real return annotations (-> CustomerResponse, -> List[CustomerResponse], -> DeleteCustomersResponse). basedpyright now verifies the return shapes. This also let four copy-pasted object_permission reverse-relation cleanup loops be deleted: pydantic's extra=ignore drops those undeclared fields during validation, so the loops were dead code. basedpyright errors on the endpoint file drop from 140 to 116.

To guarantee the refactor changes no behavior, it was done under a characterization (golden-master) safety net: tests that lock the exact JSON body each customer-object endpoint emits today, committed green against the old code, then kept green byte-for-byte through the refactor. Those live in test_customer_endpoints.py alongside the per-endpoint response-model assertions, the budget_id and nested-budget fidelity regressions, and the existing error-schema tests.

response_model= stays on the decorators for OpenAPI generation and defense in depth; it now matches the handler return annotations instead of compensating for an untyped return.

…rage

Add response_model to the five remaining untyped /customer operations
(block, unblock, new, update, delete) so the generated OpenAPI schema
documents a concrete response body. new/update reuse the canonical
LiteLLM_EndUserTable (matching info/list); block, unblock, and delete
get small dedicated models in
litellm/types/proxy/management_endpoints/customer_endpoints.py.

Together with the already-typed info/list/daily-activity routes this
brings the Customer Management group to full response_model coverage.
Regression tests assert each public /customer/* route declares the
expected response_model and that /customer/new surfaces a typed schema
in app.openapi(), so dropping a response_model fails CI.
@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

  • Adds concrete response models for Customer Management endpoints so OpenAPI documents typed response bodies.
  • Routes customer-object endpoint responses through a shared CustomerResponse mapper.
  • Restores budget_id on end-user response typing and preserves full nested budget fields.
  • Regenerates dashboard API types and adds tests for response models and response-shape preservation.

Confidence Score: 4/5

The change is narrowly scoped, but the customer response typing can reject valid partial nested budget payloads and surface as endpoint failures.

The modified response model path is well covered conceptually, and focused runtime evidence confirms the remaining issue is concrete and localized.

litellm/types/proxy/management_endpoints/customer_endpoints.py

T-Rex T-Rex Logs

What T-Rex did

  • The nested budget validation break was reproduced using a focused Python harness that validates a fake customer row through the same mapper used by the endpoints.
  • OpenAPI response-model snippets were captured before and after the change, showing the base 200 responses with default schemas and later the after-state with the expected $ref schemas.
  • Observed response contract deltas between base and head: budget_id, budget_reset_at, and created_at were added to /customer/info and /customer/list, and nested audit fields were removed from /customer/new and /customer/update.
  • The head contract under validation passed, though the base artifact already showed budget_id and full nested budget fields, so the exact contrast was not reproduced.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P1 Customer object endpoint JSON bodies changed across typed response mapper refactor

    • Bug
      • The refactor to _to_customer_response does not preserve the JSON response body contract for customer-object endpoints. In the executed TestClient harness, base /customer/info and /customer/list responses omitted top-level budget_id and nested budget budget_reset_at/created_at, while head includes them. Conversely, base /customer/new and /customer/update nested budget responses included created_by, updated_at, and updated_by, while head omits those fields. /customer/delete remained unchanged, and object_permission reverse relations were still omitted.
    • Cause
      • _to_customer_response validates rows into the new CustomerResponse model, whose field set differs from the pre-refactor mixture of raw dict returns and response_model filtering. That changes which top-level and nested budget fields survive serialization for different endpoints.
    • Fix
      • If no behavior change is intended, make CustomerResponse and _to_customer_response reproduce the previous endpoint-specific serialized shapes, or keep endpoint-specific serialization where legacy responses differed. If the new shape is intentional, update the contract/tests and document the response body change.

    T-Rex Ran code and verified through T-Rex

Reviews (4): Last reviewed commit: "fix(ui): make generated API types stable..." | Re-trigger Greptile

Comment thread litellm/proxy/management_endpoints/customer_endpoints.py Outdated
Comment thread litellm/types/proxy/management_endpoints/customer_endpoints.py
@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.75862% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...m/proxy/management_endpoints/customer_endpoints.py 66.66% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

Address review feedback on the Customer Management response_model typing.
Greptile flagged that response_model=LiteLLM_EndUserTable on /customer/new
and /customer/update silently drops fields the raw Prisma model_dump()
echoed. Checking the schema, budget_id is the only such scalar column that
was missing from the Pydantic model (created_at/updated_at/tpm_limit do not
exist on litellm_endusertable), so add budget_id to LiteLLM_EndUserTable.

This restores budget_id on new/update and also fixes the pre-existing gap
where /customer/info and /customer/list (already typed) dropped it, which
the UI Customer type expects. A regression test pins budget_id surviving the
response_model filter on /customer/update.

Also document UnblockUsersResponse.blocked_users via a Field description: it
holds the users that remain blocked after the call. The key name predates
this PR and is kept to avoid a backwards-incompatible rename on a beta route.
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

response_model=LiteLLM_EndUserTable nests the budget as the narrow write
allowlist LiteLLM_BudgetTable, which silently drops the server-managed
fields the customer endpoints used to return (budget_reset_at, created_at).

Introduce CustomerResponse, a thin response model that nests
LiteLLM_BudgetTableFull (the repo's budget response model), and apply it on
/customer/new, /customer/update, /customer/info and /customer/list. list
also builds CustomerResponse so its budget isn't narrowed at construction
time. created_by/updated_at/updated_by remain omitted, matching how budgets
are returned elsewhere.

The shared LiteLLM_EndUserTable is left untouched: it's constructed in many
places that pass narrow budget instances, and pydantic v2 won't coerce a
budget instance into a wider nested model. Typing only at the response
boundary (where the handler hands FastAPI a dict) sidesteps that. A
regression test pins budget_reset_at + created_at through the filter and
asserts the internal audit fields stay out.
…sponses

Lock the exact JSON body each customer-object endpoint (info/list/new/update)
and delete return today, so the upcoming type-safety refactor of the handlers
is only allowed to land if it reproduces these byte for byte. Pins null-field
inclusion, the nested budget shape (server fields kept, audit fields dropped),
and object_permission reverse-relation stripping. Green against current code.
Replace the untyped dict + bolt-on response_model pattern on the customer
object endpoints with explicit typed construction. A single mapper,
_to_customer_response, validates a DB row into CustomerResponse at one
Any -> typed seam; new/update/info/list now return it (or a list of it) and
carry real -> CustomerResponse / -> List[CustomerResponse] return
annotations, and delete returns DeleteCustomersResponse. basedpyright now
verifies the handlers' return shapes instead of a runtime filter doing it
silently.

This also deletes the four copy-pasted object_permission reverse-relation
cleanup loops: pydantic's extra=ignore drops those undeclared fields during
validation, so the loops were dead code (proven by the golden-master tests,
which stay byte-for-byte green). basedpyright errors on the file drop from
140 to 116, all from removed dict plumbing.

CustomerResponse stays a thin subclass of LiteLLM_EndUserTable so it inherits
the existing validators/config unchanged (behavior preservation); only the
nested budget type is widened.
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@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.

Love this! ❤️

LGTM just minor non-blocking nit

Comment thread litellm/proxy/management_endpoints/customer_endpoints.py Outdated
@mateo-berri

Copy link
Copy Markdown
Contributor

also, you might need to reformat + pull latest internal staging to fix linting issues

Address review nit: the mapper's untyped `record` added an ANN001 violation.
The incoming rows are pydantic v2 models, so type the param as BaseModel
rather than object (object has no model_dump, which would just move the
problem to basedpyright). This clears the ANN001 and also drops three
basedpyright unknown-type violations the untyped param was adding.
…itellm_customer_response_models

# Conflicts:
#	litellm/proxy/management_endpoints/customer_endpoints.py
The type-safe response refactor validates the update result via
_to_customer_response (CustomerResponse.model_validate(record.model_dump())).
These budget tests mocked the end-user update to return a bare MagicMock,
so model_dump() yielded a MagicMock that fails validation. Give each update
mock a minimal valid dict; the tests assert on the prisma calls, not the body.
@ryan-crabbe-berri
ryan-crabbe-berri force-pushed the litellm_customer_response_models branch from f935c67 to 99f17cb Compare June 27, 2026 20:24
Python 3.13 strips a docstring's common leading indentation at compile
time while 3.12 keeps it, so app.openapi() emits differently-indented
description strings depending on the interpreter. The dashboard type
generator ran locally on 3.13 and in CI on 3.12, so schema.d.ts drifted
and the "Verify schema.d.ts matches the proxy OpenAPI spec" check failed

Normalize every description through inspect.cleandoc in the spec dump so
the output is identical regardless of interpreter, then regenerate
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

the narrow write-allowlist shape LiteLLM_EndUserTable carries for internal use.
"""

litellm_budget_table: Optional[LiteLLM_BudgetTableFull] = None # pyright: ignore

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.

P1 Nested budget validation breaks

CustomerResponse now validates every customer row's nested budget as LiteLLM_BudgetTableFull, but that model requires created_at. When _to_customer_response() receives a customer row whose joined budget payload lacks created_at, the customer endpoints can fail response validation and return a 500. This can happen with partial joined budget rows or existing callers/tests that previously used the narrower LiteLLM_BudgetTable shape, which did not require created_at.

Artifacts

Repro: focused mapper and endpoint harness source

  • Contains supporting evidence from the run (text/x-python; charset=utf-8).

Repro: direct validation error and HTTP 500 Internal Server Error output

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Greptile's repro fabricates a budget object with no created_at and feeds it straight to the internal mapper, but no real /customer/* request can reach that state since the budget is always None or a DB row whose created_at is NOT NULL DEFAULT now()

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.

Fair point — litellm_budgettable.created_at is NOT NULL DEFAULT now(), so any real DB row returned from a /customer/* handler will always carry it. The concern only applies to hand-crafted test stubs that omit created_at, which isn't a production failure path. I'll withdraw this comment.

@ryan-crabbe-berri
ryan-crabbe-berri merged commit 3dce3da into litellm_internal_staging Jun 30, 2026
123 checks passed
@ryan-crabbe-berri
ryan-crabbe-berri deleted the litellm_customer_response_models branch June 30, 2026 16:58
tiannianzhu pushed a commit to tiannianzhu/litellm that referenced this pull request Jul 3, 2026
…rage (BerriAI#31043)

* feat(proxy): type Customer Management response_model for OpenAPI coverage

Add response_model to the five remaining untyped /customer operations
(block, unblock, new, update, delete) so the generated OpenAPI schema
documents a concrete response body. new/update reuse the canonical
LiteLLM_EndUserTable (matching info/list); block, unblock, and delete
get small dedicated models in
litellm/types/proxy/management_endpoints/customer_endpoints.py.

Together with the already-typed info/list/daily-activity routes this
brings the Customer Management group to full response_model coverage.
Regression tests assert each public /customer/* route declares the
expected response_model and that /customer/new surfaces a typed schema
in app.openapi(), so dropping a response_model fails CI.

* fix(proxy): keep budget_id in typed customer responses

Address review feedback on the Customer Management response_model typing.
Greptile flagged that response_model=LiteLLM_EndUserTable on /customer/new
and /customer/update silently drops fields the raw Prisma model_dump()
echoed. Checking the schema, budget_id is the only such scalar column that
was missing from the Pydantic model (created_at/updated_at/tpm_limit do not
exist on litellm_endusertable), so add budget_id to LiteLLM_EndUserTable.

This restores budget_id on new/update and also fixes the pre-existing gap
where /customer/info and /customer/list (already typed) dropped it, which
the UI Customer type expects. A regression test pins budget_id surviving the
response_model filter on /customer/update.

Also document UnblockUsersResponse.blocked_users via a Field description: it
holds the users that remain blocked after the call. The key name predates
this PR and is kept to avoid a backwards-incompatible rename on a beta route.

* fix(proxy): keep nested budget fields in customer responses

response_model=LiteLLM_EndUserTable nests the budget as the narrow write
allowlist LiteLLM_BudgetTable, which silently drops the server-managed
fields the customer endpoints used to return (budget_reset_at, created_at).

Introduce CustomerResponse, a thin response model that nests
LiteLLM_BudgetTableFull (the repo's budget response model), and apply it on
/customer/new, /customer/update, /customer/info and /customer/list. list
also builds CustomerResponse so its budget isn't narrowed at construction
time. created_by/updated_at/updated_by remain omitted, matching how budgets
are returned elsewhere.

The shared LiteLLM_EndUserTable is left untouched: it's constructed in many
places that pass narrow budget instances, and pydantic v2 won't coerce a
budget instance into a wider nested model. Typing only at the response
boundary (where the handler hands FastAPI a dict) sidesteps that. A
regression test pins budget_reset_at + created_at through the filter and
asserts the internal audit fields stay out.

* test(proxy): add golden-master characterization tests for customer responses

Lock the exact JSON body each customer-object endpoint (info/list/new/update)
and delete return today, so the upcoming type-safety refactor of the handlers
is only allowed to land if it reproduces these byte for byte. Pins null-field
inclusion, the nested budget shape (server fields kept, audit fields dropped),
and object_permission reverse-relation stripping. Green against current code.

* refactor(proxy): make the customer response flow type-safe

Replace the untyped dict + bolt-on response_model pattern on the customer
object endpoints with explicit typed construction. A single mapper,
_to_customer_response, validates a DB row into CustomerResponse at one
Any -> typed seam; new/update/info/list now return it (or a list of it) and
carry real -> CustomerResponse / -> List[CustomerResponse] return
annotations, and delete returns DeleteCustomersResponse. basedpyright now
verifies the handlers' return shapes instead of a runtime filter doing it
silently.

This also deletes the four copy-pasted object_permission reverse-relation
cleanup loops: pydantic's extra=ignore drops those undeclared fields during
validation, so the loops were dead code (proven by the golden-master tests,
which stay byte-for-byte green). basedpyright errors on the file drop from
140 to 116, all from removed dict plumbing.

CustomerResponse stays a thin subclass of LiteLLM_EndUserTable so it inherits
the existing validators/config unchanged (behavior preservation); only the
nested budget type is widened.

* refactor(proxy): annotate customer response mapper param as BaseModel

Address review nit: the mapper's untyped `record` added an ANN001 violation.
The incoming rows are pydantic v2 models, so type the param as BaseModel
rather than object (object has no model_dump, which would just move the
problem to basedpyright). This clears the ANN001 and also drops three
basedpyright unknown-type violations the untyped param was adding.

* style(test): ruff format customer endpoint tests

* test(proxy): give customer budget test update mocks a valid model_dump

The type-safe response refactor validates the update result via
_to_customer_response (CustomerResponse.model_validate(record.model_dump())).
These budget tests mocked the end-user update to return a bare MagicMock,
so model_dump() yielded a MagicMock that fails validation. Give each update
mock a minimal valid dict; the tests assert on the prisma calls, not the body.

* chore(ui): regenerate API types from proxy OpenAPI spec

* fix(ui): make generated API types stable across Python versions

Python 3.13 strips a docstring's common leading indentation at compile
time while 3.12 keeps it, so app.openapi() emits differently-indented
description strings depending on the interpreter. The dashboard type
generator ran locally on 3.13 and in CI on 3.12, so schema.d.ts drifted
and the "Verify schema.d.ts matches the proxy OpenAPI spec" check failed

Normalize every description through inspect.cleandoc in the spec dump so
the output is identical regardless of interpreter, then regenerate
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants