1+ import os
2+ import logging
3+ from typing import Dict , Any
4+ from urllib .parse import quote
5+
6+ import httpx
7+ from fastapi import APIRouter , Depends , HTTPException , Request
8+ from fastapi .responses import StreamingResponse , Response
9+
10+ from api .auth import api_key_auth
11+ from api .setting import AWS_REGION , DEBUG
12+
13+ logger = logging .getLogger (__name__ )
14+
15+ router = APIRouter (prefix = "/bedrock" )
16+
17+ # Get AWS bearer token from environment
18+ AWS_BEARER_TOKEN = os .environ .get ("AWS_BEARER_TOKEN_BEDROCK" )
19+
20+ if not AWS_BEARER_TOKEN :
21+ logger .warning ("AWS_BEARER_TOKEN_BEDROCK not set - bedrock proxy endpoints will not work" )
22+
23+
24+ def get_aws_url (model_id : str , endpoint_path : str ) -> str :
25+ """Convert proxy path to AWS Bedrock URL"""
26+ encoded_model_id = quote (model_id , safe = '' )
27+ base_url = f"https://bedrock-runtime.{ AWS_REGION } .amazonaws.com"
28+ return f"{ base_url } /model/{ encoded_model_id } /{ endpoint_path } "
29+
30+
31+ def get_proxy_headers (request : Request ) -> Dict [str , str ]:
32+ """Get headers to forward to AWS, replacing Authorization"""
33+ headers = dict (request .headers )
34+
35+ # Remove proxy authorization and add AWS bearer token
36+ headers .pop ("authorization" , None )
37+ headers .pop ("host" , None ) # Let httpx set the correct host
38+
39+ if AWS_BEARER_TOKEN :
40+ headers ["Authorization" ] = f"Bearer { AWS_BEARER_TOKEN } "
41+
42+ return headers
43+
44+
45+ @router .api_route ("/model/{model_id}/{endpoint_path:path}" , methods = ["GET" , "POST" , "PUT" , "DELETE" , "PATCH" ])
46+ async def transparent_proxy (
47+ request : Request ,
48+ model_id : str ,
49+ endpoint_path : str ,
50+ _ : None = Depends (api_key_auth )
51+ ):
52+ """
53+ Transparent HTTP proxy to AWS Bedrock.
54+ Forwards all requests as-is, only changing auth and URL.
55+ """
56+ if not AWS_BEARER_TOKEN :
57+ raise HTTPException (
58+ status_code = 503 ,
59+ detail = "AWS_BEARER_TOKEN_BEDROCK not configured"
60+ )
61+
62+ # Build AWS URL
63+ aws_url = get_aws_url (model_id , endpoint_path )
64+
65+ # Get headers to forward
66+ proxy_headers = get_proxy_headers (request )
67+
68+ # Get request body
69+ body = await request .body ()
70+
71+ if DEBUG :
72+ logger .info (f"Proxying { request .method } to: { aws_url } " )
73+ logger .info (f"Headers: { dict (proxy_headers )} " )
74+ if body :
75+ logger .info (f"Body length: { len (body )} bytes" )
76+
77+ try :
78+ async with httpx .AsyncClient () as client :
79+ # Forward the request to AWS
80+ response = await client .request (
81+ method = request .method ,
82+ url = aws_url ,
83+ headers = proxy_headers ,
84+ content = body ,
85+ params = request .query_params ,
86+ timeout = 120.0
87+ )
88+
89+ # Check if response is streaming
90+ content_type = response .headers .get ("content-type" , "" )
91+ if "text/event-stream" in content_type or "stream" in content_type :
92+ # Stream the response
93+ return StreamingResponse (
94+ content = response .aiter_bytes (),
95+ status_code = response .status_code ,
96+ headers = dict (response .headers ),
97+ media_type = content_type
98+ )
99+
100+ # Regular response
101+ return Response (
102+ content = response .content ,
103+ status_code = response .status_code ,
104+ headers = dict (response .headers )
105+ )
106+
107+ except httpx .RequestError as e :
108+ logger .error (f"Proxy request failed: { e } " )
109+ raise HTTPException (status_code = 502 , detail = f"Upstream request failed: { str (e )} " )
110+ except httpx .HTTPStatusError as e :
111+ logger .error (f"AWS returned error: { e .response .status_code } " )
112+ raise HTTPException (status_code = e .response .status_code , detail = e .response .text )
113+ except Exception as e :
114+ logger .error (f"Proxy error: { e } " )
115+ raise HTTPException (status_code = 500 , detail = "Proxy error" )
0 commit comments