fix(oauth): jsonify_object() for Prisma Json columns in persist_credential_to_db

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) <noreply@anthropic.com>
This commit is contained in:
Jason Cook 2026-04-23 10:58:46 -04:00
parent c3932e06d4
commit b22f988248
4 changed files with 32 additions and 12 deletions

View file

@ -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",
},
},

View file

@ -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",
},
},

View file

@ -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"]

View file

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