Skip to content

fix(llm): guard GoogleCV (Google Vertex vision) key against non-JSON / JSON non-object - #17464

Open
Harsh23Kashyap wants to merge 2 commits into
infiniflow:mainfrom
Harsh23Kashyap:fix/googlecv-vertex-json-key-fallback
Open

fix(llm): guard GoogleCV (Google Vertex vision) key against non-JSON / JSON non-object#17464
Harsh23Kashyap wants to merge 2 commits into
infiniflow:mainfrom
Harsh23Kashyap:fix/googlecv-vertex-json-key-fallback

Conversation

@Harsh23Kashyap

Copy link
Copy Markdown
Contributor

Summary

Closes #17463. The GoogleCV (Google Cloud Vertex vision) provider in rag/llm/ parses its API key as JSON to extract the service-account fields. The constructor had no try/except around the bare json.loads(key) call:

# rag/llm/cv_model.py:1319 (GoogleCV.__init__)
key = json.loads(key)
access_token = json.loads(base64.b64decode(key.get("google_service_account_key", "")))
project_id = key.get("google_project_id", "")
region = key.get("google_region", "")

The Google Cloud Vertex provider REQUIRES a JSON object key (per the schema in api/apps/llm_app.py:291 and the test fixture in test/testcases/test_web_api/test_llm_app/test_llm_list_unit.py:635-640):

{
  "google_project_id": "<project-id>",
  "google_region": "<region>",
  "google_service_account_key": "<base64-encoded-service-account-json>"
}

A user pasting a non-JSON string or a JSON non-object — e.g. "[1,2,3]", "42", '"hi"', "true", "null" — parses fine and then crashes on the subsequent .get(...) call with AttributeError: 'list' object has no attribute 'get' from inside rag/llm internals — no indication of what the user did wrong.

This is the same class of bug PR #17215 fixed for Azure, PR #17377 fixed for Bedrock, PR #17390 fixed for BaiduYiyan, PR #17457 fixed for VolcEngine/Ark, and PR #17459 fixed for OpenRouter. After this PR, no remaining LLM-provider JSON-decode sites in rag/llm/ that we own crash with AttributeError on JSON non-object keys.

Changes

1 helper, 1 call site, 1 test file:

  • rag/llm/key_utils.py — add _resolve_google_service_account_key(key). Returns a dict {"google_project_id": str, "google_region": str, "google_service_account_key": str}.
    • Python dict or JSON-string-encoded dict → returns the parsed fields (all default to "" if missing).
    • Plain string (or any non-JSON string) → raises ModelException(retryable=False) naming the required schema and pointing at api/apps/llm_app.py:291. Unlike the Bedrock/BaiduYiyan/VolcEngine/OpenRouter helpers, this one does NOT have a plain-key fallback — the Google Vertex provider genuinely requires the full object.
    • JSON top-level non-object (list, string, number, bool, null) → raises ModelException(retryable=False) naming the actual type and the required schema.
    • Non-string non-dict input → raises ModelException.
  • rag/llm/cv_model.pyGoogleCV.__init__ calls the helper instead of the bare json.loads(key).get(...) calls.
  • test/unit_test/rag/llm/test_googlecv_json_key_fallback.py — 19 p0 regression tests:
    • 16 on the helper directly (Python dict, JSON dict with all/missing/empty fields, plain string, empty string, malformed JSON, JSON array/string/number/float/null/bool, non-string non-dict input)
    • 3 on the GoogleCV constructor (JSON array, JSON number, plain string). The Google SDK modules (google.oauth2.service_account, google.auth.transport.requests) are stubbed via sys.modules monkeypatch because GoogleCV.__init__ imports them lazily inside the function body.

Out of scope

  • Chat counterpart GoogleChat (rag/llm/chat_model.py:1284-1289) has the same shape of bug, but PR fix(llm): implement async chat methods for GoogleChat (Vertex AI Gemini) #15994 is open by @glu000 actively working on that class. The chat-side JSON-decode fix is intentionally OUT OF SCOPE here to avoid a conflicting PR.
  • BaiduYiyanEmbed (rag/llm/embedding_model.py:1014) has the same shape — filed separately.
  • The pre-existing access_token = json.loads(base64.b64decode(...)) line still raises JSONDecodeError if google_service_account_key is missing (matching the pre-fix behavior). That is a separate missing-field bug, intentionally out of scope.

Behavior unchanged for the existing happy path

  • JSON dict with all three fields → project_id, region, and access_token populate correctly (unchanged).
  • JSON dict missing any field → that field defaults to "" (unchanged from the pre-fix dict.get(k, "") calls).

No public API change. No data-model change. No migration.

Testing

$ .venv/bin/python -m pytest test/unit_test/rag/llm/test_googlecv_json_key_fallback.py -v
============================== 19 passed in 0.78s ==============================
$ ruff check <3 changed files>
All checks passed!

$ ruff format --check <3 changed files>
3 files already formatted

Cross-references

…niflow#17463)

The GoogleCV (Google Cloud Vertex vision) provider in rag/llm parses
its API key as JSON to extract the service-account fields. The
constructor had no try/except around the bare `json.loads(key)` call:

    # rag/llm/cv_model.py:1319 (GoogleCV.__init__)
    key = json.loads(key)
    access_token = json.loads(base64.b64decode(key.get("google_service_account_key", "")))
    project_id = key.get("google_project_id", "")
    region = key.get("google_region", "")

The Google Cloud Vertex provider REQUIRES a JSON object key (per the
schema in api/apps/llm_app.py:291 and the test fixture in
test/testcases/test_web_api/test_llm_app/test_llm_list_unit.py:635-640):

    {
      "google_project_id": "<project-id>",
      "google_region": "<region>",
      "google_service_account_key": "<base64-encoded-service-account-json>"
    }

A user pasting a non-JSON string or a JSON non-object -- e.g.
`"[1,2,3]"`, `"42"", `'"hello"'`, `"true"", `"null"` --
parses fine and then crashes on the `.get(...)` call with
`AttributeError: 'list' object has no attribute 'get'` (or
`'int'`, `'str'`, `'NoneType'`, `'bool'`) from inside rag/llm
internals -- no indication of what the user did wrong.

Fix: add `_resolve_google_service_account_key` to rag/llm/key_utils
and wire `GoogleCV.__init__` through it. The helper:

- Accepts a dict (returned verbatim) or a JSON-string-encoded dict
  (parsed and returned) with all three fields extracted
  (`google_project_id`, `google_region`, `google_service_account_key`).
- On non-JSON input, on a JSON top-level type that is not a dict
  (list, string, number, bool, null), or on any other input type
  (None, int, list, ...), raises a clear
  `ModelException(retryable=False)` naming the required object shape
  and pointing at the schema in `api/apps/llm_app.py:291`.

Unlike the Bedrock/BaiduYiyan/VolcEngine/OpenRouter helpers, the
Google Vertex helper does NOT have a plain-string-key fallback -- the
provider genuinely requires the full object. The pre-existing
`access_token = json.loads(base64.b64decode(...))` line still
crashes with JSONDecodeError if `google_service_account_key` is
missing, matching the pre-fix behavior; that is a separate
missing-field bug, intentionally left out of scope.

The chat counterpart `GoogleChat` (rag/llm/chat_model.py:1284) has
the same shape of bug but is owned by @glu000 (PR infiniflow#15994) actively
working on that class. The chat-side fix is intentionally OUT OF SCOPE
for this PR to avoid a conflicting change.

After this PR, no remaining LLM-provider JSON-decode sites in
rag/llm/ that we own crash with `AttributeError` on JSON non-object
keys. The only remaining site is the chat-side GoogleChat
(rag/llm/chat_model.py:1284), which is owned by @glu000 via infiniflow#15994.

Fixes infiniflow#17463.
…w#17463)

19 p0 tests in test/unit_test/rag/llm/test_googlecv_json_key_fallback.py:

- TestResolveGoogleServiceAccountKey (16 tests on the helper directly):
  Python dict passes through, JSON dict with all/missing/empty fields
  is tolerated, plain string raises ModelException (the provider
  requires a JSON object), empty string raises, malformed JSON
  raises, JSON top-level non-object types (array, string, number,
  float, null, bool) all raise a clear ModelException naming the
  type, non-string non-dict input raises.

- TestGoogleCVCallSite (3 tests on the GoogleCV constructor):
  JSON array raises ModelException, JSON number raises
  ModelException, plain string raises ModelException. The Google
  SDK modules (google.oauth2.service_account,
  google.auth.transport.requests) are stubbed via sys.modules
  monkeypatch because GoogleCV.__init__ imports them lazily inside
  the function body.

All 19 pass against the fix. The helper raises ModelException
(retryable=False) on every non-JSON-object input with a message
that names the required fields and points at the schema in
api/apps/llm_app.py:291.

Fixes infiniflow#17463.
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. 🌈 python Pull requests that update Python code 🐞 bug Something isn't working, pull request that fix bug. 🧪 test Pull requests that update test cases. labels Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Google Vertex AI service-account keys are now normalized through a shared resolver. Invalid JSON, non-object JSON, and unsupported input types raise non-retryable ModelException errors, with unit and GoogleCV callsite regression tests covering these cases.

Changes

Google service-account key validation

Layer / File(s) Summary
Key resolution and validation
rag/llm/key_utils.py
Adds _resolve_google_service_account_key to parse, validate, and normalize dictionary or JSON-string inputs with explicit non-retryable errors.
GoogleCV integration and regression tests
rag/llm/cv_model.py, test/unit_test/rag/llm/test_googlecv_json_key_fallback.py
Updates GoogleCV to use the resolver and tests valid keys, malformed inputs, JSON primitives, and non-object values.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • infiniflow/ragflow#17463 — Directly covers the GoogleCV JSON non-object AttributeError addressed by the resolver and regression tests.

Poem

I’m a rabbit with a key in my paw,
JSON shapes now follow the law.
No more crashes in the burrowed code,
Clear exceptions mark the rocky road.
GoogleCV hops safely through—
Validated! says the rabbit crew.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed It clearly summarizes the main change: validating GoogleCV keys against non-JSON and non-object values.
Description check ✅ Passed It includes the required Summary and provides solid context, behavior, testing, and scope details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
test/unit_test/rag/llm/test_googlecv_json_key_fallback.py (1)

242-333: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

KEY_VALID_JSON is defined but unused.

Neither TestResolveGoogleServiceAccountKey nor TestGoogleCVCallSite references KEY_VALID_JSON. Either use it in a happy-path GoogleCV constructor test (verifying project_id/region/access_token resolve correctly end-to-end — currently only failure paths are exercised through the constructor), or remove it.

As per path instructions: "Remove dead tests, commented-out code, stale documentation, and 'move later' notes instead of preserving them."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit_test/rag/llm/test_googlecv_json_key_fallback.py` around lines 242 -
333, Remove the unused KEY_VALID_JSON fixture from the test module, since
neither TestResolveGoogleServiceAccountKey nor TestGoogleCVCallSite uses it and
the current tests intentionally cover only failure paths.

Source: Path instructions

rag/llm/key_utils.py (1)

65-94: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider logging before raising in the new validation flow.

None of the three failure branches (malformed JSON, wrong input type, non-object JSON) emit a log line before raising ModelException, making it harder to correlate provider misconfiguration with server logs.

As per path instructions: "Add logging for new flows" (**/*.py).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rag/llm/key_utils.py` around lines 65 - 94, Update the validation branches in
the key-parsing flow to log the provider misconfiguration immediately before
raising ModelException: malformed JSON in the string parser, unsupported input
types, and non-object JSON payloads. Use the module’s existing logging mechanism
and include the relevant error context, while preserving the current exception
messages and retryable=False behavior.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rag/llm/key_utils.py`:
- Around line 68-74: Update the JSON parsing exception handler around json.loads
in the key utility to bind the caught exception and raise ModelException with
explicit exception chaining using from exc. Preserve the existing error message
and retryable=False behavior.

In `@test/unit_test/rag/llm/test_googlecv_json_key_fallback.py`:
- Around line 116-122: The test_json_dict_missing_google_service_account_key
case currently duplicates the missing-region scenario. Update its input to
include google_project_id and google_region while omitting only
google_service_account_key, and assert that the resolver preserves those values
and returns an empty service-account key.

---

Nitpick comments:
In `@rag/llm/key_utils.py`:
- Around line 65-94: Update the validation branches in the key-parsing flow to
log the provider misconfiguration immediately before raising ModelException:
malformed JSON in the string parser, unsupported input types, and non-object
JSON payloads. Use the module’s existing logging mechanism and include the
relevant error context, while preserving the current exception messages and
retryable=False behavior.

In `@test/unit_test/rag/llm/test_googlecv_json_key_fallback.py`:
- Around line 242-333: Remove the unused KEY_VALID_JSON fixture from the test
module, since neither TestResolveGoogleServiceAccountKey nor
TestGoogleCVCallSite uses it and the current tests intentionally cover only
failure paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 09f336b9-7840-4646-9e9c-44e1dcae4df0

📥 Commits

Reviewing files that changed from the base of the PR and between 9b0719f and bfde5b5.

📒 Files selected for processing (3)
  • rag/llm/cv_model.py
  • rag/llm/key_utils.py
  • test/unit_test/rag/llm/test_googlecv_json_key_fallback.py

Comment thread rag/llm/key_utils.py
Comment on lines +68 to +74
try:
payload = json.loads(key)
except (json.JSONDecodeError, TypeError):
raise ModelException(
"Google Vertex AI key is not valid JSON. Expected an object with 'google_project_id', 'google_region', and 'google_service_account_key' (see api/apps/llm_app.py:291 for the schema).",
retryable=False,
)

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Preserve exception chain with from exc.

The except block re-raises ModelException without chaining to the original JSON error, dropping the underlying cause from the traceback.

🔧 Proposed fix
     elif isinstance(key, str):
         try:
             payload = json.loads(key)
-        except (json.JSONDecodeError, TypeError):
+        except (json.JSONDecodeError, TypeError) as exc:
             raise ModelException(
                 "Google Vertex AI key is not valid JSON. Expected an object with 'google_project_id', 'google_region', and 'google_service_account_key' (see api/apps/llm_app.py:291 for the schema).",
                 retryable=False,
-            )
+            ) from exc

Based on learnings (Repo: infiniflow/ragflow PR: 17390, file rag/llm/key_utils.py): "use Python exception chaining: raise ModelException(...) from exc... rather than losing it by re-raising without from exc." This also matches the Ruff B904 hint at lines 71-74.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
payload = json.loads(key)
except (json.JSONDecodeError, TypeError):
raise ModelException(
"Google Vertex AI key is not valid JSON. Expected an object with 'google_project_id', 'google_region', and 'google_service_account_key' (see api/apps/llm_app.py:291 for the schema).",
retryable=False,
)
try:
payload = json.loads(key)
except (json.JSONDecodeError, TypeError) as exc:
raise ModelException(
"Google Vertex AI key is not valid JSON. Expected an object with 'google_project_id', 'google_region', and 'google_service_account_key' (see api/apps/llm_app.py:291 for the schema).",
retryable=False,
) from exc
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 71-74: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rag/llm/key_utils.py` around lines 68 - 74, Update the JSON parsing exception
handler around json.loads in the key utility to bind the caught exception and
raise ModelException with explicit exception chaining using from exc. Preserve
the existing error message and retryable=False behavior.

Sources: Learnings, Linters/SAST tools

Comment on lines +116 to +122
def test_json_dict_missing_google_service_account_key(self):
out = _resolve_google_service_account_key(json.dumps({"google_project_id": "p"}))
assert out == {
"google_project_id": "p",
"google_region": "",
"google_service_account_key": "",
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Duplicate test body — doesn't actually test the missing-google_service_account_key-only case.

This test is byte-for-byte identical to test_json_dict_missing_google_region above it (same input, same assertion), so it provides no coverage of the case where only google_service_account_key is absent.

🔧 Proposed fix
     def test_json_dict_missing_google_service_account_key(self):
-        out = _resolve_google_service_account_key(json.dumps({"google_project_id": "p"}))
+        out = _resolve_google_service_account_key(
+            json.dumps({"google_project_id": "p", "google_region": "us-central1"})
+        )
         assert out == {
             "google_project_id": "p",
-            "google_region": "",
+            "google_region": "us-central1",
             "google_service_account_key": "",
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_json_dict_missing_google_service_account_key(self):
out = _resolve_google_service_account_key(json.dumps({"google_project_id": "p"}))
assert out == {
"google_project_id": "p",
"google_region": "",
"google_service_account_key": "",
}
def test_json_dict_missing_google_service_account_key(self):
out = _resolve_google_service_account_key(
json.dumps({"google_project_id": "p", "google_region": "us-central1"})
)
assert out == {
"google_project_id": "p",
"google_region": "us-central1",
"google_service_account_key": "",
}
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 116-116: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"google_project_id": "p"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit_test/rag/llm/test_googlecv_json_key_fallback.py` around lines 116 -
122, The test_json_dict_missing_google_service_account_key case currently
duplicates the missing-region scenario. Update its input to include
google_project_id and google_region while omitting only
google_service_account_key, and assert that the resolver preserves those values
and returns an empty service-account key.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 bug Something isn't working, pull request that fix bug. 🌈 python Pull requests that update Python code size:M This PR changes 30-99 lines, ignoring generated files. 🧪 test Pull requests that update test cases.

Projects

None yet

1 participant