1+ from __future__ import annotations
2+
3+ import json
4+ import re
5+ import time
6+ from dataclasses import dataclass
7+ from typing import Any
8+
9+ import httpx
10+ from django .conf import settings
11+
12+ JSON_OBJECT_PATTERN = re .compile (r"\{.*\}" , re .DOTALL )
13+
14+
15+ @dataclass (slots = True )
16+ class OpenRouterJSONResponse :
17+ payload : dict [str , Any ]
18+ model : str
19+ latency_ms : int
20+
21+
22+ def openrouter_chat_json (* , model : str , system_prompt : str , user_prompt : str ) -> OpenRouterJSONResponse :
23+ if not settings .OPENROUTER_API_KEY :
24+ raise RuntimeError ("OPENROUTER_API_KEY must be configured for OpenRouter chat completions." )
25+
26+ headers = {
27+ "Authorization" : f"Bearer { settings .OPENROUTER_API_KEY } " ,
28+ "Content-Type" : "application/json" ,
29+ }
30+ if settings .OPENROUTER_APP_URL :
31+ headers ["HTTP-Referer" ] = settings .OPENROUTER_APP_URL
32+ if settings .OPENROUTER_APP_NAME :
33+ headers ["X-OpenRouter-Title" ] = settings .OPENROUTER_APP_NAME
34+
35+ started_at = time .perf_counter ()
36+ response = httpx .post (
37+ f"{ settings .OPENROUTER_API_BASE .rstrip ('/' )} /chat/completions" ,
38+ headers = headers ,
39+ json = {
40+ "model" : model ,
41+ "temperature" : 0 ,
42+ "response_format" : {"type" : "json_object" },
43+ "messages" : [
44+ {"role" : "system" , "content" : system_prompt },
45+ {"role" : "user" , "content" : user_prompt },
46+ ],
47+ },
48+ timeout = settings .AI_REQUEST_TIMEOUT_SECONDS ,
49+ )
50+ latency_ms = int ((time .perf_counter () - started_at ) * 1000 )
51+ response .raise_for_status ()
52+
53+ message_content = response .json ()["choices" ][0 ]["message" ]["content" ]
54+ return OpenRouterJSONResponse (
55+ payload = _extract_json_object (message_content ),
56+ model = model ,
57+ latency_ms = latency_ms ,
58+ )
59+
60+
61+ def _extract_json_object (message_content : str ) -> dict [str , Any ]:
62+ try :
63+ payload = json .loads (message_content )
64+ except json .JSONDecodeError :
65+ match = JSON_OBJECT_PATTERN .search (message_content )
66+ if not match :
67+ raise ValueError ("Model response did not contain a JSON object." )
68+ payload = json .loads (match .group (0 ))
69+ if not isinstance (payload , dict ):
70+ raise ValueError ("Model response JSON must be an object." )
71+ return payload
0 commit comments