Name and Version
AI DIAL Core
What steps will reproduce the bug?
-
Create a model with a secret extra data on the upstream:
PUT /v1/models/{bucket}/my-model
{ "upstreams": [ { "endpoint": "https://vendor.example/v1/chat/completions", "secretExtraData": "{\"field\":\"value\"}" } ], ... }
→ 200 OK
-
Later, switch the upstream to a plain-string extraData (intent: replace the pair):
PUT /v1/models/{bucket}/my-model
{ "upstreams": [ { "endpoint": "https://vendor.example/v1/chat/completions", "extraData": "abc" } ], ... }
→ 422 When both extraData and secretExtraData are set, both must be JSON objects
The omitted secretExtraData is silently preserved from the stored entity (omit-preserve in SecretFieldProcessor.mergeInto: current == null || current.isNull() → copy prior ciphertext), so after the merge both fields are set and UpstreamExtraDataMerger.validateNoOverlap (via ConfigResourceController.checkCrossReferences) rejects the non-JSON extraData.
-
Try "secretExtraData": null instead — pre-fix it hits the same preserve branch, so the outcome is identical. No request clears the field; the only workaround is deleting and recreating the model.
What is the expected behavior?
A client must be able to distinguish three update intents for a secret field:
- omitted → keep the stored secret
- explicit
null → erase the secret
- a value → replace the secret
With null meaning erase, step 2 sent as { "extraData": "abc", "secretExtraData": null } should return 200 and leave the upstream with plain extraData: "abc" and no secret extra data.
What do you see instead?
An explicit null is treated identically to an omitted field — the stored secret is silently preserved. There is no request that can remove a secret: GET drops write-only (@EncryptedField) fields entirely, so a client cannot read the value to unset it any other way. Once a JSON secretExtraData is stored, the upstream can never be switched to a plain-string extraData without deleting the entity.
The same applies to Upstream.key: after "key": null the credential is still sent as X-UPSTREAM-KEY on proxied requests. Affected fields: Upstream.key, Upstream.secretExtraData, UpstreamInterface.key, UpstreamInterface.secretExtraData (via Model.upstreams, Route.upstreams, Upstream.interfaces) on per-entity PUT. Key.key is mandatory on PUT (400 on blank/null), so erase does not apply there. An empty string "" incidentally overwrites the secret with "" — undocumented and unpinned by tests.
Name and Version
AI DIAL Core
What steps will reproduce the bug?
Create a model with a secret extra data on the upstream:
→ 200 OK
Later, switch the upstream to a plain-string
extraData(intent: replace the pair):→ 422
When both extraData and secretExtraData are set, both must be JSON objectsThe omitted
secretExtraDatais silently preserved from the stored entity (omit-preserve inSecretFieldProcessor.mergeInto:current == null || current.isNull()→ copy prior ciphertext), so after the merge both fields are set andUpstreamExtraDataMerger.validateNoOverlap(viaConfigResourceController.checkCrossReferences) rejects the non-JSONextraData.Try
"secretExtraData": nullinstead — pre-fix it hits the same preserve branch, so the outcome is identical. No request clears the field; the only workaround is deleting and recreating the model.What is the expected behavior?
A client must be able to distinguish three update intents for a secret field:
null→ erase the secretWith
nullmeaning erase, step 2 sent as{ "extraData": "abc", "secretExtraData": null }should return 200 and leave the upstream with plainextraData: "abc"and no secret extra data.What do you see instead?
An explicit
nullis treated identically to an omitted field — the stored secret is silently preserved. There is no request that can remove a secret: GET drops write-only (@EncryptedField) fields entirely, so a client cannot read the value to unset it any other way. Once a JSONsecretExtraDatais stored, the upstream can never be switched to a plain-stringextraDatawithout deleting the entity.The same applies to
Upstream.key: after"key": nullthe credential is still sent asX-UPSTREAM-KEYon proxied requests. Affected fields:Upstream.key,Upstream.secretExtraData,UpstreamInterface.key,UpstreamInterface.secretExtraData(viaModel.upstreams,Route.upstreams,Upstream.interfaces) on per-entity PUT.Key.keyis mandatory on PUT (400 on blank/null), so erase does not apply there. An empty string "" incidentally overwrites the secret with "" — undocumented and unpinned by tests.