adk-python icon indicating copy to clipboard operation
adk-python copied to clipboard

fix(plugins): Add 20MB file size validation to SaveFilesAsArtifactsPl…

Open AakashSuresh2003 opened this issue 3 weeks ago • 2 comments

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

  • Closes: #3751

Problem: The SaveFilesAsArtifactsPlugin fails with a cryptic 400 INVALID_ARGUMENT error when users attempt to upload files larger than approximately 50MB. This error message provides no guidance to users on what went wrong or how to resolve the issue. According to the Gemini API documentation, inline_data uploads have a 20MB size limit, and files larger than this must use the Files API.

Solution: Added proactive file size validation to check if uploaded files exceed the 20MB inline_data limit before attempting to save them as artifacts. When a file exceeds this limit, the plugin now:

  • Validates file size immediately before processing
  • Returns a clear, user-friendly error message that includes:
    • The actual file size in MB
    • The 20MB limit
    • Instructions to use the Files API for larger files
    • A link to the official documentation for guidance

This prevents the cryptic 400 INVALID_ARGUMENT error from reaching users and provides actionable guidance on how to handle large files properly.

Testing Plan

Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.

Unit Tests:

  • [x] I have added or updated unit tests for my change.
  • [x] All unit tests pass locally.

Please include a summary of passed pytest results.

tests/unittests/plugins/test_save_files_as_artifacts.py - 14/14 tests PASSED
- test_file_size_exceeds_limit ✅ (validates 21MB file is rejected with error message)
- test_file_size_at_limit ✅ (validates 20MB file is accepted and processed)
- test_file_size_just_over_limit ✅ (validates 20MB+1 byte is rejected)
- test_mixed_file_sizes ✅ (validates mixed sizes handled independently)
- 10 existing tests ✅ (all continue to pass)

All 89 plugin tests: PASSED

Manual End-to-End (E2E) Tests: Create a Python script to test the plugin with different file sizes:

from google.adk.plugins.save_files_as_artifacts_plugin import SaveFilesAsArtifactsPlugin
from google.adk.agents.invocation_context import InvocationContext
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
from google.genai import types
from unittest.mock import Mock
import asyncio

async def test():
    # Setup
    session_service = InMemorySessionService()
    artifact_service = InMemoryArtifactService()
    session = await session_service.create_session(app_name="test_app", user_id="test_user")
    
    context = Mock(spec=InvocationContext)
    context.app_name = "test_app"
    context.user_id = "test_user"
    context.invocation_id = "test_inv"
    context.session = session
    context.artifact_service = artifact_service
    
    plugin = SaveFilesAsArtifactsPlugin()
    
    # Test 1: 5MB file (should succeed)
    small_blob = types.Blob(display_name="test_5mb.pdf", data=b"x" * (5 * 1024 * 1024), mime_type="application/pdf")
    result1 = await plugin.on_user_message_callback(
        invocation_context=context, 
        user_message=types.Content(role="user", parts=[types.Part(inline_data=small_blob)])
    )
    print("✅ SUCCESS: Small file (5MB) accepted and processed")
    print(f"   Placeholder: {result1.parts[0].text if result1 else 'No result'}")
    print()
    
    # Test 2: 20MB file (should succeed)
    limit_blob = types.Blob(display_name="test_20mb.pdf", data=b"x" * (20 * 1024 * 1024), mime_type="application/pdf")
    result2 = await plugin.on_user_message_callback(
        invocation_context=context, 
        user_message=types.Content(role="user", parts=[types.Part(inline_data=limit_blob)])
    )
    print("✅ SUCCESS: 20MB file accepted (at limit)")
    print(f"   Placeholder: {result2.parts[0].text if result2 else 'No result'}")
    print()
    
    # Test 3: 50MB file (should fail with error)
    large_blob = types.Blob(display_name="test_50mb.pdf", data=b"x" * (50 * 1024 * 1024), mime_type="application/pdf")
    result3 = await plugin.on_user_message_callback(
        invocation_context=context, 
        user_message=types.Content(role="user", parts=[types.Part(inline_data=large_blob)])
    )
    print("✅ SUCCESS: Large file rejected with clear error message")
    if result3:
        error_msg = result3.parts[0].text
        # Format multi-line error message with proper indentation
        lines = error_msg.split(". ")
        print(f"   Error message: {lines[0]}.")
        for line in lines[1:]:
            if line.strip():
                print(f"   {line.strip()}{'.' if not line.endswith('.') else ''}")
    else:
        print("   No result")

asyncio.run(test())

Test Results:

Test 1: Small file upload (5MB)

✅ SUCCESS: Small file (5MB) accepted and processed
   Placeholder: [Uploaded Artifact: "test_5mb.pdf"]

Result: File successfully saved as artifact with placeholder text.

Test 2: File at limit (20MB)

✅ SUCCESS: 20MB file accepted (at limit)
   Placeholder: [Uploaded Artifact: "test_20mb.pdf"]

Result: File successfully saved as artifact (edge case validated).

Test 3: Large file upload (50MB)

✅ SUCCESS: Large file rejected with clear error message
   Error message: [Upload Error: File size (50.00 MB) exceeds the maximum 
   allowed size for inline uploads (20 MB). Please use the Files API to 
   upload files larger than 20 MB. See https://ai.google.dev/gemini-api/docs/files 
   for more information.]

Result: Clear, actionable error message instead of 400 INVALID_ARGUMENT.

Checklist

  • [x] I have read the CONTRIBUTING.md document.
  • [x] I have performed a self-review of my own code.
  • [x] I have commented my code, particularly in hard-to-understand areas.
  • [x] I have added tests that prove my fix is effective or that my feature works.
  • [x] New and existing unit tests pass locally with my changes.
  • [x] I have manually tested my changes end-to-end.
  • [x] Any dependent changes have been merged and published in downstream modules.

Additional context

Implementation Details:

  • Added constant _MAX_INLINE_DATA_SIZE_BYTES = 20 * 1024 * 1024 (20MB) based on Gemini API documentation
  • File size validation occurs before any artifact service operations to fail fast
  • Error messages include both the actual file size and the limit for clarity
  • Original inline_data is not modified when validation fails
  • Multiple files in a single message are handled independently (small files succeed, large files fail with individual error messages)

Changes Made:

  1. src/google/adk/plugins/save_files_as_artifacts_plugin.py - Added size validation logic
  2. tests/unittests/plugins/test_save_files_as_artifacts.py - Added 4 comprehensive test cases

Reference:

  • Gemini API Files documentation: https://ai.google.dev/gemini-api/docs/files
  • Quote: "Always use the Files API when the total request size (including the files, text prompt, system instructions, etc.) is larger than 20 MB."

Before this fix: Users received: 400 INVALID_ARGUMENT: {'error': {'code': 400, 'message': 'Request contains an invalid argument.', 'status': 'INVALID_ARGUMENT'}}

After this fix: Users receive: [Upload Error: File size (50.00 MB) exceeds the maximum allowed size for inline uploads (20 MB). Please use the Files API to upload files larger than 20 MB. See https://ai.google.dev/gemini-api/docs/files for more information.]

AakashSuresh2003 avatar Dec 02 '25 10:12 AakashSuresh2003

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

google-cla[bot] avatar Dec 02 '25 10:12 google-cla[bot]

Summary of Changes

Hello @AakashSuresh2003, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the SaveFilesAsArtifactsPlugin by introducing a crucial file size validation mechanism. Previously, users attempting to upload files larger than 20MB via inline data would encounter an uninformative backend error. The change now proactively checks file sizes, providing immediate and actionable feedback to users, guiding them to utilize the Files API for larger uploads and significantly improving the overall user experience by preventing frustrating errors.

Highlights

  • Proactive File Size Validation: Implemented client-side validation in the SaveFilesAsArtifactsPlugin to check if uploaded files exceed the 20MB inline data limit before processing them.
  • Improved Error Messaging: Replaced cryptic 400 INVALID_ARGUMENT errors with clear, user-friendly messages when files exceed the 20MB limit. The new message includes the actual file size, the limit, instructions to use the Files API, and a link to relevant documentation.
  • Comprehensive Testing: Added four new unit tests to cover scenarios including files exceeding the limit, files exactly at the limit, files just over the limit, and mixed file sizes, ensuring robust validation behavior.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

[^1]: Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

gemini-code-assist[bot] avatar Dec 02 '25 10:12 gemini-code-assist[bot]