From b22f98824854c6b75df6db7c148e640bfa1180e7 Mon Sep 17 00:00:00 2001 From: Jason Cook Date: Thu, 23 Apr 2026 10:58:46 -0400 Subject: [PATCH] fix(oauth): jsonify_object() for Prisma Json columns in persist_credential_to_db MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported error after the cross-loop fix: Tokens obtained but DB persist failed: Unable to match input value to any allowed input type for the field. Parse errors: [... ``credential_values`` should be of any of the following types: ``JsonNullValueInput``, ``Json`` ...] Prisma's Json columns ingest pre-serialized JSON *strings*, not raw Python dicts. The ``/credentials`` endpoint wraps the payload with ``jsonify_object(...)`` (in ``litellm/proxy/utils.py``) which does ``json.dumps`` per nested-dict field. My persist helper was skipping that step and passing raw dicts — Prisma's binding layer then couldn't match them to the ``Json`` type. Route the ``credential_values`` + ``credential_info`` dicts through ``jsonify_object`` before the upsert. Same fix in both ChatGPT and Copilot ``db_authenticator.py``. Tests updated to assert the serialized string form (``json.loads(kwargs[...]["credential_values"])``). Co-Authored-By: Claude Opus 4.7 (1M context) --- litellm/llms/chatgpt/db_authenticator.py | 15 +++++++++++---- litellm/llms/github_copilot/db_authenticator.py | 14 ++++++++++---- .../llms/chatgpt/test_chatgpt_db_authenticator.py | 6 ++++-- .../test_copilot_db_authenticator.py | 9 +++++++-- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/litellm/llms/chatgpt/db_authenticator.py b/litellm/llms/chatgpt/db_authenticator.py index 46f6630bb7c..e7098878cc5 100644 --- a/litellm/llms/chatgpt/db_authenticator.py +++ b/litellm/llms/chatgpt/db_authenticator.py @@ -176,6 +176,7 @@ async def persist_credential_to_db(item: CredentialItem) -> None: # would bind the stale None reference rather than the live client. from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value_helper from litellm.proxy.proxy_server import prisma_client + from litellm.proxy.utils import jsonify_object if prisma_client is None: verbose_logger.debug( @@ -191,19 +192,25 @@ async def persist_credential_to_db(item: CredentialItem) -> None: "type": CREDENTIAL_TYPE, "custom_llm_provider": "chatgpt", } + # Prisma's Json columns want JSON-serialized strings, not raw dicts — + # see credential_endpoints/endpoints.py for the canonical pattern. + jsonified = jsonify_object( + { + "credential_values": encrypted_values, + "credential_info": credential_info, + } + ) await prisma_client.db.litellm_credentialstable.upsert( where={"credential_name": item.credential_name}, data={ "create": { "credential_name": item.credential_name, - "credential_values": encrypted_values, - "credential_info": credential_info, + **jsonified, "created_by": "chatgpt_oauth_flow", "updated_by": "chatgpt_oauth_flow", }, "update": { - "credential_values": encrypted_values, - "credential_info": credential_info, + **jsonified, "updated_by": "chatgpt_oauth_flow", }, }, diff --git a/litellm/llms/github_copilot/db_authenticator.py b/litellm/llms/github_copilot/db_authenticator.py index 4d1ddf5e703..8ba126b896f 100644 --- a/litellm/llms/github_copilot/db_authenticator.py +++ b/litellm/llms/github_copilot/db_authenticator.py @@ -180,6 +180,7 @@ async def persist_credential_to_db(item: CredentialItem) -> None: # would bind the stale None reference rather than the live client. from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value_helper from litellm.proxy.proxy_server import prisma_client + from litellm.proxy.utils import jsonify_object if prisma_client is None: verbose_logger.debug( @@ -195,19 +196,24 @@ async def persist_credential_to_db(item: CredentialItem) -> None: "type": CREDENTIAL_TYPE, "custom_llm_provider": "github_copilot", } + # Prisma's Json columns want JSON-serialized strings, not raw dicts. + jsonified = jsonify_object( + { + "credential_values": encrypted_values, + "credential_info": credential_info, + } + ) await prisma_client.db.litellm_credentialstable.upsert( where={"credential_name": item.credential_name}, data={ "create": { "credential_name": item.credential_name, - "credential_values": encrypted_values, - "credential_info": credential_info, + **jsonified, "created_by": "copilot_oauth_flow", "updated_by": "copilot_oauth_flow", }, "update": { - "credential_values": encrypted_values, - "credential_info": credential_info, + **jsonified, "updated_by": "copilot_oauth_flow", }, }, diff --git a/tests/test_litellm/llms/chatgpt/test_chatgpt_db_authenticator.py b/tests/test_litellm/llms/chatgpt/test_chatgpt_db_authenticator.py index aa950abd68d..5d620e1844e 100644 --- a/tests/test_litellm/llms/chatgpt/test_chatgpt_db_authenticator.py +++ b/tests/test_litellm/llms/chatgpt/test_chatgpt_db_authenticator.py @@ -1,3 +1,4 @@ +import json from unittest.mock import MagicMock, patch import pytest @@ -178,11 +179,12 @@ class TestPersistCredentialToDb: assert kwargs["where"] == {"credential_name": "test"} create = kwargs["data"]["create"] assert create["credential_name"] == "test" - assert create["credential_values"] == { + # Prisma Json columns receive pre-serialized JSON strings. + assert json.loads(create["credential_values"]) == { "access_token": "enc(a)", "refresh_token": "enc(r)", } - assert create["credential_info"] == {"type": CREDENTIAL_TYPE} + assert json.loads(create["credential_info"]) == {"type": CREDENTIAL_TYPE} update = kwargs["data"]["update"] assert update["credential_values"] == create["credential_values"] diff --git a/tests/test_litellm/llms/github_copilot/test_copilot_db_authenticator.py b/tests/test_litellm/llms/github_copilot/test_copilot_db_authenticator.py index 95877f8d782..ba0a676b679 100644 --- a/tests/test_litellm/llms/github_copilot/test_copilot_db_authenticator.py +++ b/tests/test_litellm/llms/github_copilot/test_copilot_db_authenticator.py @@ -1,3 +1,4 @@ +import json from unittest.mock import MagicMock, patch import pytest @@ -225,10 +226,14 @@ class TestPersistCredentialToDb: fake_prisma.db.litellm_credentialstable.upsert.assert_called_once() kwargs = fake_prisma.db.litellm_credentialstable.upsert.call_args.kwargs assert kwargs["where"] == {"credential_name": "c"} - assert kwargs["data"]["create"]["credential_values"] == { + # Prisma Json columns receive pre-serialized JSON strings. + assert json.loads(kwargs["data"]["create"]["credential_values"]) == { "access_token": "enc(gho_abc)" } - assert kwargs["data"]["create"]["credential_info"]["type"] == CREDENTIAL_TYPE + assert ( + json.loads(kwargs["data"]["create"]["credential_info"])["type"] + == CREDENTIAL_TYPE + ) class _Awaitable: