fix(ui): rotate model credentials in a dedicated modal so a normal save can't overwrite secrets - #28089
Conversation
Provider API keys / auth could previously only be changed by hand-editing
the raw litellm_params JSON, so there was no first-class way to rotate a
model's key. Adds an Authentication section that renders the correct
provider-specific fields (reusing ProviderSpecificFields) keyed off the
model's custom_llm_provider; fields are blank ("leave blank to keep
current") so untouched secrets are preserved and only entered values are
PATCHed and encrypted at rest.
Resolves LIT-3169
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR fixes a silent credential-corruption bug where the model edit form re-sent masked secret placeholders (e.g.
Confidence Score: 5/5Safe to merge — the change is UI-only, tightly scoped to the model credential edit path, and introduces no new backend calls or auth boundary changes. The corruption fix is correctly layered: masked secrets are stripped at both the form-seed stage and the final PATCH assembly, so neither a user typing nor a code-path regression can re-introduce the bug. The query-key invalidation (["models", "list"] prefix) correctly matches the useModelsInfo query. destroyOnHidden is valid in the project's antd 5.29.3. The regression test actively catches the masked-secret-on-wire scenario. No new network handlers or auth changes are introduced. No files require special attention.
|
| Filename | Overview |
|---|---|
| ui/litellm-dashboard/src/components/model_info_view.tsx | Core fix: adds isMaskedSecret/stripMaskedSecrets guards that strip masked credential placeholders from both the textarea seed and the PATCH payload, plus toolbar migration from Tremor to antd and wiring of the new credentials modal. |
| ui/litellm-dashboard/src/components/update_model_credentials_modal.tsx | New modal that sends a minimal { litellm_params: { api_key }, model_info: { id } } PATCH for key rotation; uses antd Form with required validation, destroyOnHidden (valid in antd 5.29.3), and proper error handling. |
| ui/litellm-dashboard/src/components/update_model_credentials_modal.test.tsx | New test suite covering the minimal-PATCH contract and the no-op-on-blank guard for the credentials modal; correctly mocks networking and notification dependencies. |
| ui/litellm-dashboard/src/components/model_info_view.test.tsx | Adds a regression test asserting that a masked secret in litellm_params never appears in the save PATCH; the test is well-targeted and would catch re-introduction of the corruption bug. |
| ui/litellm-dashboard/src/components/networking.tsx | Removes two console.log calls that echoed the full PATCH payload (including freshly-entered provider secrets) before and after the model update request. |
| ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx | Removes debug console.log statements (vertex credential upload handler and upload onChange) that were added in an earlier revision; the component interface is simplified back to its pre-PR state. |
Reviews (4): Last reviewed commit: "refactor(ui): convert the model detail t..." | Re-trigger Greptile
PR overviewProvider auth editing in model edit viewThis PR adds provider-specific authentication fields to the model edit form, omits blank secret inputs from update payloads, and avoids copying newly entered auth fields into the local read-only params view after save. I checked the form merge order, credential-name branch, provider field key resolution, and PATCH helper for secret exposure or authorization issues. Security review
Risk: 2/10 |
Drop the onFieldsResolved/authFieldKeys round trip: the parent now resolves provider auth field keys itself via the new useProviderAuthFieldKeys hook (same metadata ProviderSpecificFields renders), removing the report-up effect and its stable-reference footgun. ProviderSpecificFields keeps only excludeKeys (real need: suppress duplicate visible inputs). Fix the stale Authentication branch: derive it from the live litellm_credential_name form value (Form.useWatch) instead of the server snapshot, so clearing/adding a credential mid-edit shows the right UI. Also skip inline auth updates entirely when a named credential is selected, so we never submit a credential name and raw inline auth together.
|
@greptile re review |
…sole The auth values a user types are still sent in the PATCH request, but: - strip them from the locally-stored litellm_params after save so the read-only LiteLLM Params JSON doesn't render the plaintext key - remove the debug console.log in modelPatchUpdateCall that dumped the full update payload (incl. api_key / vertex_credentials) to the browser console on every model update Backend stores these encrypted and returns them masked on refetch.
Auth fields render blank ('leave blank to keep'), but required metadata
(e.g. OpenAI api_key) added a required validation rule that blocked
onFinish entirely — making it impossible to save any unrelated edit
without re-entering the secret. Add a disableRequired prop to
ProviderSpecificFields and set it in the model edit Authentication section.
|
@greptileai re review |
…itellm_model_auth_fields
…itellm_model_auth_fields # Conflicts: # ui/litellm-dashboard/src/components/add_model/provider_specific_fields.test.tsx # ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx
…ve can't overwrite secrets
The model edit form seeded the read-only LiteLLM Params textarea with the whole
litellm_params blob and re-sent all of it on every save. Because /model/info
redacts secrets by masking them ("azur****BBCC") rather than removing them, any
save re-encrypted the asterisk mask over the real value and silently destroyed
credentials such as azure_ad_token, aws_session_token, watsonx token/zen_api_key
and the OCI key fields. api_key, client_secret, vertex_credentials and the AWS
access/secret keys were safe only because the backend strips those entirely
Credential rotation now lives in a dedicated UpdateModelCredentialsModal that
PATCHes only the fields the user types, decoupled from the params blob; the
backend already merges partial litellm_params, so the rest of the deployment is
left untouched. The general edit form drops masked values from both the textarea
seed and the outbound payload, so a normal save can never carry a redacted secret
Also removes the now-unused inline auth section and its excludeKeys and
useProviderAuthFieldKeys plumbing, strips secret-leaking console.logs from the
provider upload handler and the model-update response, and fixes a
react-hooks/use-memo error that was failing the frontend-lint CI job
Removing the credential-echoing console.log (and its info: any param) from the provider upload handler dropped the tracked count by one; update the committed baseline so the Check lint budgets CI step is not stale
Narrows UpdateModelCredentialsModal to a single API Key field. On submit it
PATCHes only { api_key }, so the backend merge leaves every other deployment
param untouched; a model authed via azure_ad_token, AWS keys, or a Vertex JSON
won't have anything to rotate here yet, which is the intended scope for now.
Drops the multi-field provider rendering this added earlier, which also removes
the now-unused disableRequired prop from ProviderSpecificFields and reverts that
shared component to its prior shape. The "Update API Key" trigger button is now
an antd Button rather than a TremorButton, so the feature introduces no tremor.
… antd Switches Test Connection, Re-use Credentials and Delete Model to antd Button so the toolbar matches the Update API Key button and no longer mixes libraries; Delete Model uses antd's danger styling instead of hand-rolled red classes
|
@greptileai review Reworked credential editing into a dedicated api-key rotation modal that PATCHes only the new key, stopped the model edit form from re-sending masked secrets (the corruption fix), and converted the model detail toolbar to antd |
…ve can't overwrite secrets (BerriAI#28089) * feat(ui): add provider auth editing to the model edit view Provider API keys / auth could previously only be changed by hand-editing the raw litellm_params JSON, so there was no first-class way to rotate a model's key. Adds an Authentication section that renders the correct provider-specific fields (reusing ProviderSpecificFields) keyed off the model's custom_llm_provider; fields are blank ("leave blank to keep current") so untouched secrets are preserved and only entered values are PATCHed and encrypted at rest. Resolves LIT-3169 * refactor(ui): simplify model auth editing; fix stale credential branch Drop the onFieldsResolved/authFieldKeys round trip: the parent now resolves provider auth field keys itself via the new useProviderAuthFieldKeys hook (same metadata ProviderSpecificFields renders), removing the report-up effect and its stable-reference footgun. ProviderSpecificFields keeps only excludeKeys (real need: suppress duplicate visible inputs). Fix the stale Authentication branch: derive it from the live litellm_credential_name form value (Form.useWatch) instead of the server snapshot, so clearing/adding a credential mid-edit shows the right UI. Also skip inline auth updates entirely when a named credential is selected, so we never submit a credential name and raw inline auth together. * fix(ui): don't leak freshly-entered model auth secrets to display/console The auth values a user types are still sent in the PATCH request, but: - strip them from the locally-stored litellm_params after save so the read-only LiteLLM Params JSON doesn't render the plaintext key - remove the debug console.log in modelPatchUpdateCall that dumped the full update payload (incl. api_key / vertex_credentials) to the browser console on every model update Backend stores these encrypted and returns them masked on refetch. * fix(ui): don't require blank auth fields in model edit context Auth fields render blank ('leave blank to keep'), but required metadata (e.g. OpenAI api_key) added a required validation rule that blocked onFinish entirely — making it impossible to save any unrelated edit without re-entering the secret. Add a disableRequired prop to ProviderSpecificFields and set it in the model edit Authentication section. * fix(ui): rotate model credentials in a dedicated modal so a normal save can't overwrite secrets The model edit form seeded the read-only LiteLLM Params textarea with the whole litellm_params blob and re-sent all of it on every save. Because /model/info redacts secrets by masking them ("azur****BBCC") rather than removing them, any save re-encrypted the asterisk mask over the real value and silently destroyed credentials such as azure_ad_token, aws_session_token, watsonx token/zen_api_key and the OCI key fields. api_key, client_secret, vertex_credentials and the AWS access/secret keys were safe only because the backend strips those entirely Credential rotation now lives in a dedicated UpdateModelCredentialsModal that PATCHes only the fields the user types, decoupled from the params blob; the backend already merges partial litellm_params, so the rest of the deployment is left untouched. The general edit form drops masked values from both the textarea seed and the outbound payload, so a normal save can never carry a redacted secret Also removes the now-unused inline auth section and its excludeKeys and useProviderAuthFieldKeys plumbing, strips secret-leaking console.logs from the provider upload handler and the model-update response, and fixes a react-hooks/use-memo error that was failing the frontend-lint CI job * chore(ui): ratchet no-explicit-any lint metric to 2013 Removing the credential-echoing console.log (and its info: any param) from the provider upload handler dropped the tracked count by one; update the committed baseline so the Check lint budgets CI step is not stale * refactor(ui): scope the model credential modal to api-key rotation only Narrows UpdateModelCredentialsModal to a single API Key field. On submit it PATCHes only { api_key }, so the backend merge leaves every other deployment param untouched; a model authed via azure_ad_token, AWS keys, or a Vertex JSON won't have anything to rotate here yet, which is the intended scope for now. Drops the multi-field provider rendering this added earlier, which also removes the now-unused disableRequired prop from ProviderSpecificFields and reverts that shared component to its prior shape. The "Update API Key" trigger button is now an antd Button rather than a TremorButton, so the feature introduces no tremor. * refactor(ui): convert the model detail toolbar buttons from tremor to antd Switches Test Connection, Re-use Credentials and Delete Model to antd Button so the toolbar matches the Update API Key button and no longer mixes libraries; Delete Model uses antd's danger styling instead of hand-rolled red classes * style(ui): make the api-key modal submit button primary and drop the Need Help link
Relevant issues
Linear ticket
Resolves LIT-3169
Summary
Adds a dedicated "Update API Key" modal so a model's key can be rotated without hand-editing raw
litellm_params, and closes a silent credential-corruption bug in the model edit formThe bug this fixes
/v1/model/inforedacts secrets by masking them (e.g.azur****BBCC), not by removing them. The edit form seeded the read-only LiteLLM Params textarea with the entirelitellm_paramsblob, masked secrets included, and re-sent the whole blob on every save. Saving any unrelated field (TPM, tags, cost) therefore re-sent the masked placeholder, which the backend encrypted straight over the real secret. The masked-but-kept fields areazure_ad_token,aws_session_token, watsonxtoken/zen_api_keyand the OCI key fields;api_key,client_secret,vertex_credentialsand the AWS access/secret keys were safe only because the backend removes those from the snapshot entirely. An earlier revision of this PR added an inline auth section that sat on top of that broken blob path, which made the footgun easier to triggerWhat changed
API-key rotation lives in
UpdateModelCredentialsModal, opened from an "Update API Key" button on the model. It is a single field that PATCHes only{ api_key }; the backend already merges partiallitellm_params, so everything else on the deployment is untouched. The modal and its trigger button are antd, so the feature introduces no new tremorThe general edit form no longer touches secrets at all: masked values are stripped from both the textarea seed and the outbound payload, so a normal save can never carry a redacted secret. Two stray
console.logs that echoed credentials (the provider upload handler and the model-update response) are removed, and areact-hooks/use-memoerror that was failing the frontend-lint CI job is fixedScreenshots / Proof of Fix
Driven through the Admin UI at
http://localhost:4000/ui/models-and-endpoints, capturing the outbound PATCH from the browser and reading the stored secret back from the DB by decrypting the row (the API only ever shows it masked)Before the fix, a plain save destroyed a masked-but-kept secret:
After the fix, the same plain save leaves it alone, and the modal rotates only the key:
To re-capture screenshots: go to http://localhost:4000/ui/models-and-endpoints, open a DB model, click "Update API Key", enter a new key, submit; then Edit Settings and Save without touching anything and confirm the key still works. Browsers cache old JS chunks across rebuilds, so hard-refresh if the UI looks stale
Test plan
model_info_view.test.tsx: regression test asserting a masked secret inlitellm_paramsis never present in the save PATCH (verified it fails when the strip guard is disabled)update_model_credentials_modal.test.tsx: the modal sends a minimal{ api_key }PATCH and is a no-op when the field is blankprovider_specific_fields.test.tsxplus the Add Model and credential-modal suites still pass (the shared component is back to its pre-PR render path)Type
🐛 Bug Fix
🆕 New Feature
Changes
UI only. New
UpdateModelCredentialsModal(single api-key field, antd);model_info_viewwires the antd trigger button, strips masked secrets on seed and on submit, and drops the inline auth section;provider_specific_fieldsloses the unusedexcludeKeys/disableRequired/useProviderAuthFieldKeysadditions;networkingdrops a credential-echoing log