11from typing import Annotated
22
3- from fastapi import APIRouter , Body , Depends
3+ from fastapi import APIRouter , Body , Depends , Header , Request
44from fastapi .responses import StreamingResponse
5+ from langfuse .decorators import langfuse_context , observe
56
67from api .auth import api_key_auth
78from api .models .bedrock import BedrockModel
1516)
1617
1718
19+ def extract_langfuse_metadata (chat_request : ChatRequest , headers : dict ) -> dict :
20+ """Extract Langfuse tracing metadata from request body and headers.
21+
22+ Metadata can be provided via:
23+ 1. extra_body.langfuse_metadata dict in the request
24+ 2. HTTP headers: X-Chat-Id, X-User-Id, X-Session-Id, X-Message-Id
25+ 3. user field in the request (for user_id)
26+
27+ Returns a dict with: user_id, session_id, chat_id, message_id, and any custom metadata
28+ """
29+ metadata = {}
30+
31+ # Extract from extra_body if present
32+ if chat_request .extra_body and isinstance (chat_request .extra_body , dict ):
33+ langfuse_meta = chat_request .extra_body .get ("langfuse_metadata" , {})
34+ if isinstance (langfuse_meta , dict ):
35+ metadata .update (langfuse_meta )
36+
37+ # Extract from headers
38+ headers_lower = {k .lower (): v for k , v in headers .items ()}
39+
40+ # Map headers to metadata fields - support both standard and OpenWebUI-prefixed headers
41+ header_mapping = {
42+ "x-chat-id" : "chat_id" ,
43+ "x-openwebui-chat-id" : "chat_id" , # OpenWebUI sends this format
44+ "x-user-id" : "user_id" ,
45+ "x-openwebui-user-id" : "user_id" , # OpenWebUI sends this format
46+ "x-session-id" : "session_id" ,
47+ "x-openwebui-session-id" : "session_id" , # OpenWebUI sends this format
48+ "x-message-id" : "message_id" ,
49+ "x-openwebui-message-id" : "message_id" , # OpenWebUI sends this format
50+ }
51+
52+ for header_key , meta_key in header_mapping .items ():
53+ if header_key in headers_lower and headers_lower [header_key ]:
54+ # Don't override if already set (standard headers take precedence)
55+ if meta_key not in metadata :
56+ metadata [meta_key ] = headers_lower [header_key ]
57+
58+ # Use the 'user' field from request as user_id if not already set
59+ if "user_id" not in metadata and chat_request .user :
60+ metadata ["user_id" ] = chat_request .user
61+
62+ return metadata
63+
64+
1865@router .post (
1966 "/completions" , response_model = ChatResponse | ChatStreamResponse | Error , response_model_exclude_unset = True
2067)
68+ @observe (as_type = "generation" , name = "chat_completion" )
2169async def chat_completions (
70+ request : Request ,
2271 chat_request : Annotated [
2372 ChatRequest ,
2473 Body (
@@ -34,12 +83,42 @@ async def chat_completions(
3483 ),
3584 ],
3685):
37- if chat_request .model .lower ().startswith ("gpt-" ):
38- chat_request .model = DEFAULT_MODEL
86+ # Extract metadata for Langfuse tracing
87+ metadata = extract_langfuse_metadata (chat_request , dict (request .headers ))
88+
89+ # Create trace name using chat_id if available
90+ trace_name = f"chat:{ metadata .get ('chat_id' , 'unknown' )} "
91+
92+ # Update trace with metadata, user_id, and session_id
93+ langfuse_context .update_current_trace (
94+ name = trace_name ,
95+ user_id = metadata .get ("user_id" ),
96+ session_id = metadata .get ("session_id" ),
97+ metadata = metadata ,
98+ input = {
99+ "model" : chat_request .model ,
100+ "messages" : [msg .model_dump () for msg in chat_request .messages ],
101+ "temperature" : chat_request .temperature ,
102+ "max_tokens" : chat_request .max_tokens ,
103+ "tools" : [tool .model_dump () for tool in chat_request .tools ] if chat_request .tools else None ,
104+ }
105+ )
39106
40107 # Exception will be raised if model not supported.
41108 model = BedrockModel ()
42109 model .validate (chat_request )
110+
43111 if chat_request .stream :
44112 return StreamingResponse (content = model .chat_stream (chat_request ), media_type = "text/event-stream" )
45- return await model .chat (chat_request )
113+
114+ response = await model .chat (chat_request )
115+
116+ # Update trace with output for non-streaming
117+ langfuse_context .update_current_trace (
118+ output = {
119+ "message" : response .choices [0 ].message .model_dump () if response .choices else None ,
120+ "finish_reason" : response .choices [0 ].finish_reason if response .choices else None ,
121+ }
122+ )
123+
124+ return response
0 commit comments