-
-
Notifications
You must be signed in to change notification settings - Fork 6.9k
feat: support Anthropic OAuth tokens in passthrough endpoint #20429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
klaudworks
wants to merge
2
commits into
BerriAI:main
from
klaudworks:feat/anthropic-oauth-passthrough
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -587,19 +587,26 @@ async def anthropic_proxy_route( | |
| base_target_url = os.getenv("ANTHROPIC_API_BASE") or "https://api.anthropic.com" | ||
| encoded_endpoint = httpx.URL(endpoint).path | ||
|
|
||
| # Ensure endpoint starts with '/' for proper URL construction | ||
| if not encoded_endpoint.startswith("/"): | ||
| encoded_endpoint = "/" + encoded_endpoint | ||
|
|
||
| # Construct the full target URL using httpx | ||
| base_url = httpx.URL(base_target_url) | ||
| updated_url = base_url.copy_with(path=encoded_endpoint) | ||
|
|
||
| # Add or update query parameters | ||
| anthropic_api_key = passthrough_endpoint_router.get_credentials( | ||
| custom_llm_provider="anthropic", | ||
| region_name=None, | ||
| ) | ||
| x_api_key_header = request.headers.get("x-api-key", "") | ||
| auth_header = request.headers.get("authorization", "") | ||
|
|
||
| if x_api_key_header or auth_header: | ||
| custom_headers = {} | ||
| else: | ||
| anthropic_api_key = passthrough_endpoint_router.get_credentials( | ||
| custom_llm_provider="anthropic", | ||
| region_name=None, | ||
| ) | ||
| if anthropic_api_key: | ||
| custom_headers = {"x-api-key": anthropic_api_key} | ||
| else: | ||
| custom_headers = {} | ||
|
Comment on lines
+599
to
+609
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nested if-else creates empty dict in multiple paths. Consider simplifying with ternary operator for the inner condition. Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! Prompt To Fix With AIThis is a comment left during a code review.
Path: litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py
Line: 599:609
Comment:
Nested if-else creates empty dict in multiple paths. Consider simplifying with ternary operator for the inner condition.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| ## check for streaming | ||
| is_streaming_request = await is_streaming_request_fn(request) | ||
|
|
@@ -608,7 +615,7 @@ async def anthropic_proxy_route( | |
| endpoint_func = create_pass_through_route( | ||
| endpoint=endpoint, | ||
| target=str(updated_url), | ||
| custom_headers={"x-api-key": "{}".format(anthropic_api_key)}, | ||
| custom_headers=custom_headers, | ||
| _forward_headers=True, | ||
| is_streaming_request=is_streaming_request, | ||
| ) # dynamically construct pass-through endpoint based on incoming path | ||
|
|
@@ -829,10 +836,10 @@ async def handle_bedrock_count_tokens( | |
|
|
||
| except BedrockError as e: | ||
| # Convert BedrockError to HTTPException for FastAPI | ||
| verbose_proxy_logger.error(f"BedrockError in handle_bedrock_count_tokens: {str(e)}") | ||
| raise HTTPException( | ||
| status_code=e.status_code, detail={"error": e.message} | ||
| verbose_proxy_logger.error( | ||
| f"BedrockError in handle_bedrock_count_tokens: {str(e)}" | ||
| ) | ||
| raise HTTPException(status_code=e.status_code, detail={"error": e.message}) | ||
| except HTTPException: | ||
| # Re-raise HTTP exceptions as-is | ||
| raise | ||
|
|
@@ -1041,7 +1048,7 @@ async def bedrock_proxy_route( | |
| target=str(prepped.url), | ||
| custom_headers=prepped.headers, # type: ignore | ||
| is_streaming_request=is_streaming_request, | ||
| _forward_headers=True | ||
| _forward_headers=True, | ||
| ) # dynamically construct pass-through endpoint based on incoming path | ||
| received_value = await endpoint_func( | ||
| request, | ||
|
|
@@ -1063,42 +1070,43 @@ def _resolve_vertex_model_from_router( | |
| ) -> Tuple[str, str, Optional[str], Optional[str]]: | ||
| """ | ||
| Resolve Vertex AI model configuration from router. | ||
|
|
||
| Args: | ||
| model_id: The model ID extracted from the URL (e.g., "gcp/google/gemini-2.5-flash") | ||
| llm_router: The LiteLLM router instance | ||
| encoded_endpoint: The encoded endpoint path | ||
| endpoint: The original endpoint path | ||
| vertex_project: Current vertex project (may be from URL) | ||
| vertex_location: Current vertex location (may be from URL) | ||
|
|
||
| Returns: | ||
| Tuple of (encoded_endpoint, endpoint, vertex_project, vertex_location) | ||
| with resolved values from router config | ||
| """ | ||
| if not llm_router: | ||
| return encoded_endpoint, endpoint, vertex_project, vertex_location | ||
|
|
||
| try: | ||
| deployment = llm_router.get_available_deployment_for_pass_through(model=model_id) | ||
| deployment = llm_router.get_available_deployment_for_pass_through( | ||
| model=model_id | ||
| ) | ||
| if not deployment: | ||
| return encoded_endpoint, endpoint, vertex_project, vertex_location | ||
|
|
||
| litellm_params = deployment.get("litellm_params", {}) | ||
|
|
||
| # Always override with router config values (they take precedence over URL values) | ||
| config_vertex_project = litellm_params.get("vertex_project") | ||
| config_vertex_location = litellm_params.get("vertex_location") | ||
| if config_vertex_project: | ||
| vertex_project = config_vertex_project | ||
| if config_vertex_location: | ||
| vertex_location = config_vertex_location | ||
|
|
||
| # Get the actual Vertex AI model name by stripping the provider prefix | ||
| # e.g., "vertex_ai/gemini-2.0-flash-exp" -> "gemini-2.0-flash-exp" | ||
| model_from_config = litellm_params.get("model", "") | ||
| if model_from_config: | ||
|
|
||
| # get_llm_provider returns (model, custom_llm_provider, dynamic_api_key, api_base) | ||
| # For "vertex_ai/gemini-2.0-flash-exp" it returns: | ||
| # model="gemini-2.0-flash-exp", custom_llm_provider="vertex_ai" | ||
|
|
@@ -1127,12 +1135,12 @@ def _resolve_vertex_model_from_router( | |
| ) | ||
| encoded_endpoint = encoded_endpoint.replace(model_id, actual_model) | ||
| endpoint = endpoint.replace(model_id, actual_model) | ||
|
|
||
| except Exception as e: | ||
| verbose_proxy_logger.debug( | ||
| f"Error resolving vertex model from router for model {model_id}: {e}" | ||
| ) | ||
|
|
||
| return encoded_endpoint, endpoint, vertex_project, vertex_location | ||
|
|
||
|
|
||
|
|
@@ -1597,7 +1605,7 @@ async def _prepare_vertex_auth_headers( | |
| vertex_credentials_str = None | ||
| elif vertex_credentials is not None: | ||
| # Use credentials from vertex_credentials | ||
| # When vertex_credentials are provided (including default credentials), | ||
| # When vertex_credentials are provided (including default credentials), | ||
| # use their project/location values if available | ||
| if vertex_credentials.vertex_project is not None: | ||
| vertex_project = vertex_credentials.vertex_project | ||
|
|
@@ -1703,10 +1711,14 @@ async def _base_vertex_proxy_route( | |
| # Check if model is in router config - always do this to resolve custom model names | ||
| model_id = get_vertex_model_id_from_url(endpoint) | ||
| if model_id: | ||
|
|
||
| if llm_router: | ||
| # Resolve model configuration from router | ||
| encoded_endpoint, endpoint, vertex_project, vertex_location = _resolve_vertex_model_from_router( | ||
| ( | ||
| encoded_endpoint, | ||
| endpoint, | ||
| vertex_project, | ||
| vertex_location, | ||
| ) = _resolve_vertex_model_from_router( | ||
| model_id=model_id, | ||
| llm_router=llm_router, | ||
| encoded_endpoint=encoded_endpoint, | ||
|
|
@@ -1899,25 +1911,25 @@ async def openai_proxy_route( | |
| ): | ||
| """ | ||
| Pass-through endpoint for OpenAI API calls. | ||
|
|
||
| Available on both routes: | ||
| - /openai/{endpoint:path} - Standard OpenAI passthrough route | ||
| - /openai_passthrough/{endpoint:path} - Dedicated passthrough route (recommended for Responses API) | ||
|
|
||
| Use /openai_passthrough/* when you need guaranteed passthrough to OpenAI without conflicts | ||
| with LiteLLM's native implementations (e.g., for the Responses API at /v1/responses). | ||
|
|
||
| Examples: | ||
| Standard route: | ||
| - /openai/v1/chat/completions | ||
| - /openai/v1/assistants | ||
| - /openai/v1/threads | ||
|
|
||
| Dedicated passthrough (for Responses API): | ||
| - /openai_passthrough/v1/responses | ||
| - /openai_passthrough/v1/responses/{response_id} | ||
| - /openai_passthrough/v1/responses/{response_id}/input_items | ||
|
|
||
| [Docs](https://docs.litellm.ai/docs/pass_through/openai_passthrough) | ||
| """ | ||
| base_target_url = os.getenv("OPENAI_API_BASE") or "https://api.openai.com/" | ||
|
|
||
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
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR description claims this detects OAuth tokens (pattern:
Bearer sk-ant-oat...), but the code doesn't check for OAuth tokens specifically. It checks if ANYx-api-keyorauthorizationheader exists, which means all client credentials bypass server credentials. If you specifically want OAuth detection as described, consider checking for the OAuth token pattern.Prompt To Fix With AI