-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathllm_client.py
More file actions
86 lines (70 loc) · 2.7 KB
/
Copy pathllm_client.py
File metadata and controls
86 lines (70 loc) · 2.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#!/usr/bin/env python3
"""OpenAI-compatible chat client for model-to-model experiments."""
from __future__ import annotations
import json
import os
import subprocess
import urllib.error
import urllib.request
class LLMError(RuntimeError):
pass
def _keychain_secret(service: str) -> str:
result = subprocess.run(
[
"/usr/bin/security",
"find-generic-password",
"-a",
os.environ.get("USER", ""),
"-s",
service,
"-w",
],
text=True,
capture_output=True,
)
if result.returncode != 0:
raise LLMError(f"Cannot read Keychain service {service}: {result.stderr.strip()}")
secret = result.stdout.strip()
if not secret:
raise LLMError(f"Keychain service {service} is empty")
return secret
def api_key(model_config: dict) -> str:
env_name = model_config.get("api_key_env")
if env_name and os.environ.get(env_name):
return os.environ[env_name]
keychain_service = model_config.get("keychain_service")
if keychain_service:
return _keychain_secret(keychain_service)
raise LLMError(f"No api_key_env or keychain_service configured for {model_config.get('name')}")
def chat(model_config: dict, messages: list[dict], *, temperature: float = 0.3) -> str:
base_url = (model_config.get("base_url") or "").rstrip("/")
model = model_config.get("model")
if not base_url or not model:
raise LLMError(f"Missing base_url/model for {model_config.get('name')}")
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": model_config.get("max_tokens", 900),
}
request = urllib.request.Request(
f"{base_url}/v1/chat/completions",
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers={
"Authorization": f"Bearer {api_key(model_config)}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=model_config.get("timeout_seconds", 60)) as response:
data = json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
raise LLMError(f"HTTP {exc.code} from {model_config.get('name')}: {body[:500]}") from exc
except urllib.error.URLError as exc:
raise LLMError(f"Request failed for {model_config.get('name')}: {exc}") from exc
text = data.get("choices", [{}])[0].get("message", {}).get("content") or ""
if not text.strip():
raise LLMError(f"Empty response from {model_config.get('name')}")
return text.strip()