fix(llm): redact credentials from re-raised provider error messages#435
Open
VoidChecksum wants to merge 1 commit into
Open
fix(llm): redact credentials from re-raised provider error messages#435VoidChecksum wants to merge 1 commit into
VoidChecksum wants to merge 1 commit into
Conversation
The actionable-error translation in `decepticon/llm/factory.py`
(`_reraise_with_actionable_message` and `_reraise_if_connection_error`)
interpolated the raw upstream exception text (`str(exc)`) into the
user-facing `RuntimeError` (e.g. `... Underlying: {msg}`).
For LiteLLM/openai errors `str(exc)` is constructed from the provider's
HTTP response body, which can echo back the `Authorization` / `x-api-key`
/ `Bearer` value that was sent on the request. Those RuntimeErrors
propagate to the CLI and into logs, so a transient upstream failure could
disclose a live API key to anyone reading the output.
Add `_redact_secrets(text)`, a small regex-based scrubber applied to every
place these reraise paths interpolate the exception text. It scrubs:
- `Bearer <token>` -> `Bearer [REDACTED]`
- `sk-ant-*` / `sk-*` provider key prefixes -> `[REDACTED]`
- `authorization` / `api_key` / `x-api-key` header or kwarg values
(JSON `"authorization": "..."` and `api_key=...` forms) -> value
replaced with `[REDACTED]`, key name kept for readability
- generic long opaque tokens (>=24 chars) -> `[REDACTED]`
Branch matching still runs against the raw message (HTTP status codes and
provider keywords are not secret-shaped), so the actionable guidance
(status code, offending model id, remediation hint) is preserved verbatim;
only secret material is removed. Non-secret text such as
`model_group=anthropic/claude-opus-4-7` is left intact.
Update `TestActionableErrorTranslation` to add coverage proving an
exception carrying `Authorization: Bearer sk-ant-...` (and one with
`api_key=sk-...`) is re-raised with the secret redacted while the
status-code/guidance remains, plus a non-secret passthrough assertion.
Tests use obviously-fake placeholder tokens only.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a credential-disclosure-into-logs bug in the LLM factory's error
translation path.
_reraise_with_actionable_messageand_reraise_if_connection_errorinpackages/decepticon/decepticon/llm/factory.pyinterpolated the raw upstreamexception text (
str(exc)) straight into the user-facingRuntimeError(
... \nUnderlying: {msg}). For LiteLLM/openai errors,str(exc)is builtfrom the provider's HTTP response body, which can reflect the
Authorization/x-api-key/Bearervalue that was sent on the request.Those
RuntimeErrors propagate to the CLI and into logs — so a transientupstream 4xx could leak a live API key to anyone reading the output.
Fix
Add
_redact_secrets(text), a small regex scrubber applied to every placethese reraise paths interpolate the exception text. It redacts:
Bearer <token>→Bearer [REDACTED]sk-ant-*/sk-*provider key prefixes →[REDACTED]authorization/api_key/x-api-keyheader values (JSON"authorization": "..."and kwargapi_key=...forms) → value replaced,key name kept for readability
[REDACTED]Branch matching still runs on the raw message (HTTP status codes and
provider keywords aren't secret-shaped), so the actionable guidance — status
code, offending model id, and remediation hint — is preserved verbatim. Only
secret material is removed; non-secret text such as
model_group=anthropic/claude-opus-4-7is left intact.Tests
TestActionableErrorTranslationgains three cases:Authorization: Bearer sk-ant-<fake>→ secret redacted,credentials (401)guidance + model id still present
api_key=sk-<fake>→ secret redacted,rejected the request (400)guidancestill present
_redact_secretsAll placeholder tokens are obviously fake. Existing assertions (which check
guidance strings, not secret text) are unchanged.
Verification
uv run ruff check(both files) → cleanuv run ruff format --check(both files) → cleanuv run basedpyright packages/decepticon/decepticon/llm/factory.py→ 0 errorsuv run pytest packages/decepticon/tests/unit/llm -q→ 216 passed, 2 skipped