Skip to content

fix(managed_files): skip managed file rows with no file_object when listing files - #35368

Closed
devin-ai-integration[bot] wants to merge 1 commit into
litellm_internal_stagingfrom
devin_ai_fix_files_list_null_file_object_35361
Closed

fix(managed_files): skip managed file rows with no file_object when listing files#35368
devin-ai-integration[bot] wants to merge 1 commit into
litellm_internal_stagingfrom
devin_ai_fix_files_list_null_file_object_35361

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • GET /v1/files 500s on one null file_object row
  • Poller-registered batch output/error ids create those rows
  • Whole listing is lost, not just the row

How it solves it:

  • Parse each managed file row defensively, skip unparseable ones
  • Handles the JSON-string form the writer stores too

Relevant issues

Fixes #35361

Linear ticket

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • 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

I could not stand up the DB-backed proxy needed for the end to end repro in this environment: managed files require Postgres, and both apt and the Docker registry are unreachable from here, so there is no way to get a database up. Reviewer runbook for a live repro is below; the regression test stands in as the automated proof (fails before the fix with the reported OpenAIFileObject() argument after ** must be a mapping, not NoneType, passes after)

Before the fix (71b825a7f0 plus the test only):

$ PYTHONPATH=./enterprise LITELLM_LOCAL_MODEL_COST_MAP=True uv run --no-sync pytest \
    tests/test_litellm/enterprise/proxy/test_managed_files_hook.py -q -k skips_rows
FAILED tests/test_litellm/enterprise/proxy/test_managed_files_hook.py::test_get_user_created_file_ids_skips_rows_without_file_object
E   TypeError: litellm.types.llms.openai.OpenAIFileObject() argument after ** must be a mapping, not NoneType
1 failed

After the fix (88aea5306f):

$ PYTHONPATH=./enterprise LITELLM_LOCAL_MODEL_COST_MAP=True uv run --no-sync pytest \
    tests/test_litellm/enterprise/proxy/test_managed_files_hook.py -q -k skips_rows
1 passed

The four other failures in that file (test_should_pass_credentials_to_afile_retrieve, test_should_fallback_when_no_router, test_should_not_double_wrap_already_unified_output_file_id, test_afile_content_bedrock_unified_id_end_to_end) fail identically on the unmodified base commit in this environment; they are missing optional deps here, not regressions from this change

Live repro for a reviewer with a database, on a proxy with enable_preview_features: true, database_url set and CheckBatchCost enabled:

  1. curl -X POST http://localhost:4000/v1/files -H "Authorization: Bearer sk-1234" -F purpose=batch -F 'target_model_names=my-gpt' -F file=@batch.jsonl
  2. curl -X POST http://localhost:4000/v1/batches -H "Authorization: Bearer sk-1234" -H 'Content-Type: application/json' -d '{"input_file_id": "<id from step 1>", "endpoint": "/v1/chat/completions", "completion_window": "24h"}'
  3. Wait for the batch to complete and for one CheckBatchCost cycle, which registers the batch's output_file_id and error_file_id as managed ids with file_object null
  4. curl "http://localhost:4000/v1/files?target_model_names=my-gpt" -H "Authorization: Bearer sk-1234"; before this change that returns a 500 naming NoneType, after it returns 200 with the parseable files and without the poller-registered rows

Type

🐛 Bug Fix

Changes

store_unified_file_id deliberately accepts file_object=None (LiteLLM_ManagedFileTable.file_object is Optional, and the writer only sets the column when the value is non-null), and the batch cost poller uses exactly that path when it re-registers a completed batch's output_file_id / error_file_id as managed ids. The reader disagreed: get_user_created_file_ids did OpenAIFileObject(**row.file_object) for every row, so one null column raised TypeError and the whole GET /v1/files response became a 500 for that caller

Rows are now parsed through a small helper that returns None for a null or unparseable column and logs a warning, and the listing skips those, matching how alist_batches already tolerates rows it cannot parse. The helper also accepts the JSON-string form, which is what the writer actually persists (file_object.model_dump_json() into a Json column), so a string column no longer depends on Prisma decoding it back into a dict

def _parse_managed_file_object(raw_file_object: object, unified_file_id: str) -> Optional[OpenAIFileObject]:
    if not raw_file_object:
        return None
    try:
        return (
            OpenAIFileObject.model_validate_json(raw_file_object)
            if isinstance(raw_file_object, str)
            else OpenAIFileObject.model_validate(raw_file_object)
        )
    except Exception as e:
        verbose_logger.warning(f"Failed to parse managed file object {unified_file_id}: {e}")
        return None

No backfill is needed; a poller-registered output row carries no provider metadata worth showing

One nearby inconsistency I did not touch, since it is a separate bug: delete_unified_file_id returns initial_value.file_object annotated as OpenAIFileObject while the DB gives back the raw column, so DELETE /v1/files/{id} can hand a JSON string or None straight to the response model

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

…isting files

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes managed-file listing so one null or malformed stored file object does not fail the entire request.

  • Adds defensive parsing for both JSON-string and object representations of OpenAIFileObject.
  • Omits rows whose file objects are absent or cannot be parsed.
  • Adds regression coverage for valid string/object rows alongside null and malformed rows.

Confidence Score: 5/5

The PR appears safe to merge, with the reported listing failure handled defensively and covered by a targeted regression test.

The parser supports the repository’s required Pydantic version and both persisted file-object shapes, while isolating invalid rows instead of allowing one row to fail the complete file listing.

Important Files Changed

Filename Overview
enterprise/litellm_enterprise/proxy/hooks/managed_files.py Adds per-row parsing and omission of unusable managed-file objects, consistently handling the persisted JSON-string representation without introducing an actionable defect.
tests/test_litellm/enterprise/proxy/test_managed_files_hook.py Adds focused mock-based regression coverage confirming valid rows remain listed while null and malformed rows are skipped.

Reviews (1): Last reviewed commit: "fix(managed_files): skip managed file ro..." | Re-trigger Greptile

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@mateo-berri

mateo-berri commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Closing since #35365 merged the same null file_object skip in get_user_created_file_ids, so this is superseded

@mateo-berri mateo-berri closed this Aug 6, 2026
@mateo-berri
mateo-berri deleted the devin_ai_fix_files_list_null_file_object_35361 branch August 6, 2026 17:14
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.

[Bug]: GET /v1/files 500s with "argument after ** must be a mapping, not NoneType" when a managed row has a null file_object

2 participants