Litellm oss staging 04 01 2026 - #24958
Conversation
…error traces (#24943) * fix(proxy): use actual request start_time for failed spend logs async_post_call_failure_hook was calling datetime.now() for both start_time and end_time, making every failed request show Duration: 0.000s. litellm_logging_obj (already fetched in the same method for trace ID propagation) carries the real request start_time — use it as actual_start_time with a datetime.now() fallback when absent. Add two regression tests covering the fix and the fallback path. Fixes #24888 * fix(llm translation): redact Gemini API key from URL query params in error traces Gemini API requests authenticate via a ?key=<api_key> URL query param. When a provider call fails, httpx.Response.raise_for_status() embeds the full URL in the error message, leaking the key in exception traces and logs. Changes: - Extract secret-redaction logic from litellm/_logging.py into a new public utility module litellm/litellm_core_utils/secret_redaction.py, exposing redact_string() as a proper public API instead of a private helper - Add (?<=[?&])key=[^\s&'"]{8,} pattern to _SECRET_RE so ?key=VALUE and &key=VALUE fragments are caught by the existing SecretRedactionFilter - Apply redact_string() to error_str in exception_mapping_utils.py so the key is also stripped from the mapped exception message surfaced to callers - Add 5 regression tests covering: ?key=, &key=, short-value no-op, httpx raise_for_status path, and end-to-end logger output - Keep _redact_string = redact_string alias in _logging.py for backward compat Fixes #24902 * revert: undo start_time fix for failed spend logs * fix: gate exception redaction on _ENABLE_SECRET_REDACTION opt-out flag - Apply redact_string() conditionally in exception_mapping_utils.py, matching the same _ENABLE_SECRET_REDACTION guard used by SecretRedactionFilter so that LITELLM_DISABLE_REDACT_SECRETS=true is honoured for exception messages - Rewrite test_redact_string_applied_to_httpx_error_message to use pytest.raises so assertions cannot be silently skipped if raise_for_status() doesn't raise - Add test_exception_mapping_respects_redaction_opt_out to verify the flag is respected end-to-end through exception_type()
* feat: added Qohash Nexus guardrail hook * fix: ui_friendly_name of Qostodian Nexus * Update litellm/proxy/guardrails/guardrail_hooks/qohash/qohash.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update litellm/proxy/guardrails/guardrail_hooks/qohash/qohash.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
…ion (#24926) service_tier (priority/flex) was not forwarded to generic_cost_per_token for azure and azure_ai providers, so tier-specific pricing was ignored and standard pricing was always returned. Other providers (openai, bedrock, gemini, vertex_ai) already pass it correctly.
…PM (#24925) Join LiteLLM_BudgetTable as b_tm on team membership budget_id and select team_member_tpm_limit / team_member_rpm_limit so virtual key auth populates limits for parallel_request_limiter_v3. Add test_team_member_rate_limits_v3_raises_429_when_over_limit mirroring existing key-level OVER_LIMIT / HTTP 429 coverage. Made-with: Cursor
* fix(gemini): handle Gemini Files API URIs without fetching Fixes #24907 When a file is uploaded via the Gemini Files API, the returned URI (https://generativelanguage.googleapis.com/v1beta/files/...) starts with 'https://' and hits the generic HTTPS handler in _process_gemini_media(). That handler calls _get_image_mime_type_from_url() which tries to fetch the URL — but Gemini Files API URLs return 403 when accessed directly, causing: 'Unable to determine mime type for file_id: ...' Fix: add an early elif that matches Gemini Files API URLs and passes them through as file_data without trying to fetch the URL. When an explicit format is provided it's included; otherwise the Gemini API infers the MIME type from its stored metadata. Exactly matches the fix direction suggested by the issue reporter (rodriciru). * fix: anchor Gemini Files API URL check with startswith Address greptile P2: replace `in` substring check with `startswith` to prevent query-string injection bypass (e.g. `https://evil.com/?ref=https://generativelanguage...`). Also adds trailing slash to match only valid file URIs. --------- Co-authored-by: voidborne-d <voidborne-d@users.noreply.github.com>
…v1 endpoints (#24911) When aembedding=True, api_version was not passed to self.aembedding(), causing get_azure_openai_client() to receive None instead of "v1". This made _is_azure_v1_api_version() return False, so AsyncAzureOpenAI was selected instead of AsyncOpenAI, constructing the wrong request URL and returning 404. Fixes #24848 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
| r"(?:aws_secret_access_key|aws_session_token|aws_access_key_id)" | ||
| r"\s*[:=]\s*[A-Za-z0-9/+=]{20,}", |
Check warning
Code scanning / CodeQL
Implicit string concatenation in a list Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
In general, to fix this type of issue, any place where two adjacent string literals are meant to be concatenated should use an explicit + operator (or other explicit joining, like ''.join([...])). This preserves the runtime string value but makes the intention clear and avoids confusion with missing commas in lists or tuples.
For this specific case in litellm/litellm_core_utils/secret_redaction.py, we want to keep the regex pattern as a single element in the patterns list, but make its multi-line nature explicit. On lines 20–21, we should add a trailing + at the end of the first line so it reads like:
r"(?:aws_secret_access_key|aws_session_token|aws_access_key_id)" +
r"\s*[:=]\s*[A-Za-z0-9/+=]{20,}",This leaves the compiled regex unchanged, avoids any missing-comma confusion, and satisfies the CodeQL recommendation. No additional imports, methods, or definitions are required, and no other parts of the file need modification.
| @@ -17,7 +17,7 @@ | ||
| # AWS access key IDs | ||
| r"(?:AKIA|ASIA)[0-9A-Z]{16}", | ||
| # AWS secrets / session tokens / access key IDs (key=value) | ||
| r"(?:aws_secret_access_key|aws_session_token|aws_access_key_id)" | ||
| r"(?:aws_secret_access_key|aws_session_token|aws_access_key_id)" + | ||
| r"\s*[:=]\s*[A-Za-z0-9/+=]{20,}", | ||
| # Bearer tokens (OAuth, JWT, etc.) | ||
| r"Bearer\s+[A-Za-z0-9\-._~+/]{10,}=*", |
| r"\w*(?:password|passwd|client_secret|secret_key|_secret)" | ||
| r"['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]+", |
Check warning
Code scanning / CodeQL
Implicit string concatenation in a list Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
Use explicit concatenation (+) for intentionally split string literals inside the patterns list.
Best fix here: in litellm/litellm_core_utils/secret_redaction.py, update the pattern at lines 40–41 so the two raw string literals are joined with +. This preserves exact functionality while making intent explicit and removing the CodeQL finding. No imports, methods, or new definitions are needed.
| @@ -37,7 +37,7 @@ | ||
| # full "key=<secret>" fragment so the value is redacted regardless of format. | ||
| r"(?<=[?&])key=[^\s&'\"]{8,}", | ||
| # Password / secret params (handles key=value and 'key': 'value') | ||
| r"\w*(?:password|passwd|client_secret|secret_key|_secret)" | ||
| r"\w*(?:password|passwd|client_secret|secret_key|_secret)" + | ||
| r"['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]+", | ||
| # Database connection string credentials (scheme://user:pass@host) | ||
| r"(?<=://)[^\s'\"]*:[^\s'\"@]+(?=@)", |
| r"(?:master_key|database_url|db_url|connection_string|" | ||
| r"private_key|signing_key|encryption_key|" | ||
| r"auth_token|access_token|refresh_token|" | ||
| r"slack_webhook_url|webhook_url|" | ||
| r"database_connection_string|" | ||
| r"huggingface_token|jwt_secret)" | ||
| r"""['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]+""", |
Check warning
Code scanning / CodeQL
Implicit string concatenation in a list Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
In general, to fix “implicit string concatenation in a list” when the concatenation is intentional, explicitly join the string literals with + so it’s clear that they form one element, rather than being separate elements accidentally merged by omission of a comma. If the concatenation is not intended, instead insert the missing comma to create distinct list elements.
For this specific case in litellm/litellm_core_utils/secret_redaction.py, the long regex for key-name-based redaction (lines 50–56) is meant to be a single pattern. To retain behavior while satisfying the recommendation and avoiding ambiguity, we should convert the multiple adjacent raw string literals into a single concatenated expression using +, optionally keeping line breaks for readability. We must preserve the exact pattern text, including the closing parenthesis and subsequent character-class portion starting at line 56. The change is confined to the patterns list in _build_secret_patterns; no new imports, methods, or external dependencies are needed.
Concretely, replace the current block:
- from line 50:
r"(?:master_key|database_url|db_url|connection_string|" - through line 56:
r"""['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]+""",
with a single expression where each part is joined by +, keeping the exact same regex content, e.g.:
r"(?:master_key|database_url|db_url|connection_string|"
+ r"private_key|signing_key|encryption_key|"
+ r"auth_token|access_token|refresh_token|"
+ r"slack_webhook_url|webhook_url|"
+ r"database_connection_string|"
+ r"huggingface_token|jwt_secret)"
+ r"""['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]+""",No other lines in the file need adjustment.
| @@ -48,12 +48,12 @@ | ||
| # regardless of what the value looks like. | ||
| # e.g. 'master_key': 'any-value-here', "database_url": "postgres://..." | ||
| r"(?:master_key|database_url|db_url|connection_string|" | ||
| r"private_key|signing_key|encryption_key|" | ||
| r"auth_token|access_token|refresh_token|" | ||
| r"slack_webhook_url|webhook_url|" | ||
| r"database_connection_string|" | ||
| r"huggingface_token|jwt_secret)" | ||
| r"""['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]+""", | ||
| + r"private_key|signing_key|encryption_key|" | ||
| + r"auth_token|access_token|refresh_token|" | ||
| + r"slack_webhook_url|webhook_url|" | ||
| + r"database_connection_string|" | ||
| + r"huggingface_token|jwt_secret)" | ||
| + r"""['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]+""", | ||
| ] | ||
| return re.compile("|".join(patterns), re.IGNORECASE) | ||
|
|
Greptile SummaryThis PR bundles several fixes and a new Qostodian Nexus guardrail integration: Gemini Files API 403 fix, Azure AI Foundry embedding api_version forwarding, service_tier cost propagation, team-member rate limit data in the key-lookup SQL join, and a new standalone secret-redaction module that scrubs API keys from exception messages.
Confidence Score: 4/5Not safe to merge — two P0 bugs prevent LiteLLM from starting up. Two P0 findings: a SyntaxError in guardrails.py (unclosed parenthesis in import) and a NameError in _logging.py (import re removed but still used at definition time). Both are confirmed to crash module loading. The fixes are one-liners each, but must be applied before this can ship. litellm/types/guardrails.py (line 35, SyntaxError) and litellm/_logging.py (line 27, NameError on missing import re)
|
| Filename | Overview |
|---|---|
| litellm/types/guardrails.py | P0 SyntaxError: missing ) in qohash import on line 35-37 prevents the module from loading entirely. |
| litellm/_logging.py | P0: import re removed but _build_secret_patterns() still references re.Pattern (return annotation) and re.compile() (body), causing NameError at module load; redact_string imported from secret_redaction but unused. |
| litellm/litellm_core_utils/secret_redaction.py | New standalone redaction utility module; adds URL query-param key pattern to fix Gemini API key exposure in httpx error messages. |
| litellm/proxy/guardrails/guardrail_hooks/qohash/qohash.py | New Qostodian Nexus guardrail wrapping GenericGuardrailAPI; defaults to HTTP for the API base; pass-through apply_guardrail override needed for proxy/utils.py detection is well-commented. |
| litellm/llms/vertex_ai/gemini/transformation.py | Adds early-exit branch for Gemini Files API URIs to avoid 403 HTTP fetch; correctly passes file_uri with optional mime_type through to the Gemini API. |
| litellm/proxy/utils.py | Extends existing raw SQL to LEFT JOIN LiteLLM_BudgetTable for team-member TPM/RPM limits; JOIN condition matches schema (budget_id exists on LiteLLM_TeamMembership). |
| litellm/llms/azure/cost_calculation.py | Adds service_tier parameter to enable priority/flex pricing tiers; threaded through from cost_calculator.py correctly. |
| litellm/llms/azure/azure.py | Fixes missing api_version forwarding to aembedding() call for Azure AI Foundry v1 endpoints. |
| litellm/litellm_core_utils/exception_mapping_utils.py | Applies redact_string() to exception messages when secret redaction is enabled, fixing the Gemini API key leak via raised exceptions. |
Class Diagram
%%{init: {'theme': 'neutral'}}%%
classDiagram
class GenericGuardrailAPI {
+api_base: str
+extra_headers: list
+apply_guardrail(inputs, request_data, input_type, logging_obj)
+get_config_model() Type
}
class QostodianNexus {
+GUARDRAIL_NAME: str
+__init__(api_base, **kwargs)
+apply_guardrail(inputs, request_data, input_type, logging_obj)
+get_config_model() QostodianNexusConfigModel
}
class QostodianNexusConfigModel {
+api_base: Optional[str]
+ui_friendly_name() str
}
class GuardrailConfigModel {
<<base>>
}
class SupportedGuardrailIntegrations {
<<enum>>
QOSTODIAN_NEXUS
}
GenericGuardrailAPI <|-- QostodianNexus
GuardrailConfigModel <|-- QostodianNexusConfigModel
QostodianNexus ..> QostodianNexusConfigModel : get_config_model()
QostodianNexus ..> SupportedGuardrailIntegrations : registered as
Comments Outside Diff (1)
-
litellm/_logging.py, line 27 (link)NameErrorat module load —import reremoved but still usedThis PR removes
import refrom_logging.py, but_build_secret_patterns()still usesre.Patternin the return-type annotation (line 27) andre.compile/re.IGNORECASEin the body (line 82). In Python 3.11, function annotations are evaluated eagerly at definition time, so the module will crash withNameError: name 're' is not definedthe moment Python processes line 27 — before any LiteLLM code can run.Verified with
exec:def foo() -> re.Pattern: pass # → NameError: name 're' is not defined
The fix is simply to restore the removed import, or quote the annotation:
Or add
import reback to the imports section. Theredact_stringimport fromsecret_redaction(added on line 11) is currently unused in this module — if the intent was to delegate to that function, the existing_redact_string/_SECRET_REmust also be removed or kept alongside the restoredimport re.
Reviews (3): Last reviewed commit: "Merge branch 'main' into litellm_oss_sta..." | Re-trigger Greptile
| api_base: Optional[str] = None, | ||
| **kwargs, | ||
| ): | ||
| api_base = api_base or os.environ.get("QOSTODIAN_NEXUS_API_BASE", "http://nexus:8800") |
There was a problem hiding this comment.
Plaintext HTTP as default API base
The default URL http://nexus:8800 uses unencrypted HTTP. Even for an internal service, plaintext HTTP exposes the full request body (which may contain user prompts and LLM responses) to network observers on the path between LiteLLM and the Nexus container.
Consider defaulting to https:// and documenting that HTTP may be used for local/container deployments where TLS is terminated at a higher layer.
| api_base = api_base or os.environ.get("QOSTODIAN_NEXUS_API_BASE", "http://nexus:8800") | |
| api_base = api_base or os.environ.get("QOSTODIAN_NEXUS_API_BASE", "https://nexus:8800") |
| self, | ||
| inputs: GenericGuardrailAPIInputs, | ||
| request_data: dict, | ||
| input_type: Literal["request", "response"], | ||
| logging_obj: Optional["LiteLLMLoggingObj"] = None, | ||
| ) -> GenericGuardrailAPIInputs: | ||
| """ | ||
| Apply Qostodian Nexus to the given inputs. | ||
|
|
||
| NOTE: This override is intentionally a pass-through. It must be present | ||
| directly in this class's __dict__ so that LiteLLM's unified guardrail | ||
| routing check (`"apply_guardrail" in type(callback).__dict__` in | ||
| litellm/proxy/utils.py) routes calls correctly. Do not remove. | ||
| """ | ||
| return await super().apply_guardrail( | ||
| inputs=inputs, |
There was a problem hiding this comment.
Pass-through override tightly couples to internal detection logic
The apply_guardrail override is a no-op pass-through that exists solely to ensure the method appears in type(callback).__dict__, satisfying the detection check at litellm/proxy/utils.py:868:
use_unified = "apply_guardrail" in type(callback).__dict__The comment documents this correctly, but the class now has a hidden dependency on an implementation detail deep in proxy/utils.py. If that detection logic ever changes (e.g. using hasattr or getattr instead), this override becomes dead weight silently, with no compiler error.
Consider converting the detection check to use hasattr() or a registry-based approach to remove this coupling entirely. Alternatively, a class-level marker attribute (e.g. _uses_unified_guardrail = True) would be a more explicit and fragile-proof contract.
|
Hi @krrish-berri-2, just saw more recent staging branches merged recently. Just wanted to make sure this one was not forgotten 😁 Thanks! |
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 29203053 | Triggered | Generic Password | e6fb699 | .circleci/config.yml | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
There was a problem hiding this comment.
High: Syntax error disables all guardrail enforcement
This PR adds a Qostodian Nexus guardrail integration, secret redaction improvements, cost calculation fixes, and SQL query enhancements. However, a missing closing parenthesis in litellm/types/guardrails.py introduces a SyntaxError that prevents the guardrails type module from loading, which would silently disable every configured guardrail (content filtering, PII detection, prompt injection blocking, etc.) at proxy startup.
| ToolPermissionGuardrailConfigModel, | ||
| ) | ||
| from litellm.types.proxy.guardrails.guardrail_hooks.qohash import ( | ||
| QostodianNexusConfigModel, |
There was a problem hiding this comment.
High: Guardrail bypass via import failure
The import of QostodianNexusConfigModel is missing its closing ). This causes a SyntaxError on this module, which is imported by every guardrail hook in the proxy. At startup, all guardrails (PII masking, prompt injection detection, content filtering, etc.) will fail to load. An attacker who knows the proxy is running this version can send requests that bypass all configured guardrails.
| QostodianNexusConfigModel, | |
| QostodianNexusConfigModel, | |
| ) | |
| from litellm.types.proxy.guardrails.guardrail_hooks.hiddenlayer import ( |
|
|
||
| from litellm.litellm_core_utils.safe_json_dumps import safe_dumps | ||
| from litellm.litellm_core_utils.safe_json_loads import safe_json_loads | ||
| from litellm.litellm_core_utils.secret_redaction import redact_string |
Check notice
Code scanning / CodeQL
Unused import Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
The best fix is to remove the unused import line from litellm/_logging.py and leave behavior unchanged.
- General approach: delete imports that are not referenced.
- Specific change: in the import block at the top of
litellm/_logging.py, remove:from litellm.litellm_core_utils.secret_redaction import redact_string
- No additional methods, definitions, or dependency changes are required.
| @@ -8,7 +8,6 @@ | ||
|
|
||
| from litellm.litellm_core_utils.safe_json_dumps import safe_dumps | ||
| from litellm.litellm_core_utils.safe_json_loads import safe_json_loads | ||
| from litellm.litellm_core_utils.secret_redaction import redact_string | ||
|
|
||
| set_verbose = False | ||
|
|
| from litellm.types.proxy.guardrails.guardrail_hooks.qohash import ( | ||
| QostodianNexusConfigModel, | ||
| from litellm.types.proxy.guardrails.guardrail_hooks.hiddenlayer import ( | ||
| HiddenlayerGuardrailConfigModel | ||
| ) |
There was a problem hiding this comment.
SyntaxError —
( was never closed
The opening parenthesis on line 35 is never closed before the next from statement starts on line 37. Python will raise SyntaxError: '(' was never closed when loading the module, breaking every import of litellm.types.guardrails (and therefore the entire proxy).
Confirmed with ast.parse:
SyntaxError: '(' was never closed
File "litellm/types/guardrails.py", line 35
from litellm.types.proxy.guardrails.guardrail_hooks.qohash import (
Fix — add the missing ) and reorder the HiddenlayerGuardrailConfigModel import:
| from litellm.types.proxy.guardrails.guardrail_hooks.qohash import ( | |
| QostodianNexusConfigModel, | |
| from litellm.types.proxy.guardrails.guardrail_hooks.hiddenlayer import ( | |
| HiddenlayerGuardrailConfigModel | |
| ) | |
| from litellm.types.proxy.guardrails.guardrail_hooks.qohash import ( | |
| QostodianNexusConfigModel, | |
| ) | |
| from litellm.types.proxy.guardrails.guardrail_hooks.hiddenlayer import ( | |
| HiddenlayerGuardrailConfigModel, | |
| ) |
|
Too many files changed for review. ( |
|
clean PR: #25856 |
Relevant issues
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
tests/test_litellm/directory, Adding at least 1 test is a hard requirement - see detailsmake test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewDelays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
CI (LiteLLM team)
Branch creation CI run
Link:
CI run for the last commit
Link:
Merge / cherry-pick CI run
Links:
Type
🆕 New Feature
🐛 Bug Fix
🧹 Refactoring
📖 Documentation
🚄 Infrastructure
✅ Test
Changes