ValasaiChander commited on
Commit
e332f37
·
verified ·
1 Parent(s): 6aa956c

Upload services/openrouter_client.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. services/openrouter_client.py +127 -0
services/openrouter_client.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal OpenRouter chat-completions client used by the AI service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import urllib.error
9
+ import urllib.request
10
+ from typing import Any
11
+
12
+
13
+ class OpenRouterClient:
14
+ """Small helper for calling the OpenRouter OpenAI-compatible API."""
15
+
16
+ def __init__(self, api_key: str, model: str):
17
+ api_key = (api_key or "").strip()
18
+ if not api_key:
19
+ raise ValueError("OPENROUTER_API_KEY is required")
20
+
21
+ self.api_key = api_key
22
+ self.model = model
23
+ self.base_url = os.getenv(
24
+ "OPENROUTER_BASE_URL",
25
+ "https://openrouter.ai/api/v1/chat/completions",
26
+ )
27
+ self.app_name = os.getenv("OPENROUTER_APP_NAME", "Debatra AI Service")
28
+ self.site_url = os.getenv("OPENROUTER_SITE_URL", "http://localhost")
29
+
30
+ async def chat_completion(
31
+ self,
32
+ messages: list[dict[str, str]],
33
+ *,
34
+ temperature: float = 0.4,
35
+ max_tokens: int = 1024,
36
+ ) -> str:
37
+ payload = {
38
+ "model": self.model,
39
+ "messages": messages,
40
+ "temperature": temperature,
41
+ "max_tokens": max_tokens,
42
+ }
43
+
44
+ data = await asyncio.to_thread(self._post_json, payload)
45
+ return self._extract_content(data)
46
+
47
+ async def chat_completion_json(
48
+ self,
49
+ messages: list[dict[str, str]],
50
+ *,
51
+ temperature: float = 0.4,
52
+ max_tokens: int = 1024,
53
+ ) -> dict[str, Any]:
54
+ text = await self.chat_completion(
55
+ messages,
56
+ temperature=temperature,
57
+ max_tokens=max_tokens,
58
+ )
59
+ text = self._strip_code_fences(text)
60
+
61
+ if not text:
62
+ raise ValueError("OpenRouter returned an empty response")
63
+
64
+ try:
65
+ return json.loads(text)
66
+ except json.JSONDecodeError as exc:
67
+ raise ValueError("OpenRouter response was not valid JSON") from exc
68
+
69
+ def _post_json(self, payload: dict[str, Any]) -> dict[str, Any]:
70
+ request = urllib.request.Request(
71
+ self.base_url,
72
+ data=json.dumps(payload).encode("utf-8"),
73
+ headers={
74
+ "Authorization": f"Bearer {self.api_key}",
75
+ "Content-Type": "application/json",
76
+ "HTTP-Referer": self.site_url,
77
+ "X-Title": self.app_name,
78
+ },
79
+ method="POST",
80
+ )
81
+
82
+ try:
83
+ with urllib.request.urlopen(request, timeout=60) as response:
84
+ body = response.read().decode("utf-8")
85
+ except urllib.error.HTTPError as error:
86
+ error_body = error.read().decode("utf-8", errors="replace") if error.fp else ""
87
+ raise RuntimeError(
88
+ f"OpenRouter API error {error.code}: {error_body or error.reason}"
89
+ ) from error
90
+ except urllib.error.URLError as error:
91
+ raise RuntimeError(f"OpenRouter request failed: {error.reason}") from error
92
+
93
+ try:
94
+ return json.loads(body)
95
+ except json.JSONDecodeError as exc:
96
+ raise RuntimeError("OpenRouter returned invalid JSON") from exc
97
+
98
+ @staticmethod
99
+ def _extract_content(response: dict[str, Any]) -> str:
100
+ choices = response.get("choices") or []
101
+ if not choices:
102
+ raise ValueError("OpenRouter returned no choices")
103
+
104
+ message = choices[0].get("message") or {}
105
+ content = message.get("content") or ""
106
+
107
+ if isinstance(content, list):
108
+ fragments: list[str] = []
109
+ for fragment in content:
110
+ if isinstance(fragment, dict):
111
+ fragments.append(str(fragment.get("text", "")))
112
+ else:
113
+ fragments.append(str(fragment))
114
+ content = "".join(fragments)
115
+
116
+ return str(content).strip()
117
+
118
+ @staticmethod
119
+ def _strip_code_fences(text: str) -> str:
120
+ text = text.strip()
121
+ if text.startswith("```"):
122
+ parts = text.split("```", 2)
123
+ if len(parts) >= 3:
124
+ text = parts[1]
125
+ if text.startswith("json"):
126
+ text = text[4:]
127
+ return text.strip()