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 , BackgroundTasks
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+ background_tasks : BackgroundTasks ,
49+ model_id : str ,
50+ endpoint_path : str ,
51+ _ : None = Depends (api_key_auth )
52+ ):
53+ """
54+ Transparent HTTP proxy to AWS Bedrock.
55+ Forwards all requests as-is, only changing auth and URL.
56+ """
57+ if not AWS_BEARER_TOKEN :
58+ raise HTTPException (
59+ status_code = 503 ,
60+ detail = "AWS_BEARER_TOKEN_BEDROCK not configured"
61+ )
62+
63+ # Build AWS URL
64+ aws_url = get_aws_url (model_id , endpoint_path )
65+
66+ # Get headers to forward
67+ proxy_headers = get_proxy_headers (request )
68+
69+ # Get request body
70+ body = await request .body ()
71+
72+ if DEBUG :
73+ logger .info (f"Proxying { request .method } to: { aws_url } " )
74+ logger .info (f"Headers: { dict (proxy_headers )} " )
75+ if body :
76+ logger .info (f"Body length: { len (body )} bytes" )
77+
78+ try :
79+ # Always use streaming for transparent pass-through
80+ client = httpx .AsyncClient ()
81+
82+ # Add cleanup task
83+ async def cleanup_client ():
84+ await client .aclose ()
85+
86+ background_tasks .add_task (cleanup_client )
87+
88+ async def stream_generator ():
89+ async with client .stream (
90+ method = request .method ,
91+ url = aws_url ,
92+ headers = proxy_headers ,
93+ content = body ,
94+ params = request .query_params ,
95+ timeout = 120.0
96+ ) as response :
97+ async for chunk in response .aiter_bytes ():
98+ if chunk : # Only yield non-empty chunks
99+ yield chunk
100+
101+ return StreamingResponse (content = stream_generator ())
102+
103+ except httpx .RequestError as e :
104+ logger .error (f"Proxy request failed: { e } " )
105+ raise HTTPException (status_code = 502 , detail = f"Upstream request failed: { str (e )} " )
106+ except httpx .HTTPStatusError as e :
107+ logger .error (f"AWS returned error: { e .response .status_code } " )
108+ raise HTTPException (status_code = e .response .status_code , detail = e .response .text )
109+ except Exception as e :
110+ logger .error (f"Proxy error: { e } " )
111+ raise HTTPException (status_code = 500 , detail = "Proxy error" )
0 commit comments