Orienter / approach /providers.py
stereoid's picture
Add files using upload-large-folder tool
3f3265f verified
Raw
History Blame Contribute Delete
4.87 kB
import json
from dataclasses import dataclass
from urllib import request
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Union
from approach.config import ProviderConfig
PostFn = Callable[..., Any]
@dataclass(frozen=True)
class EncodedImage:
data: str
media_type: str = "image/jpeg"
class UrlLibResponse:
def __init__(self, status: int, body: bytes):
self.status_code = status
self._body = body
def raise_for_status(self):
if self.status_code >= 400:
raise RuntimeError(f"HTTP request failed with status {self.status_code}")
def json(self):
return json.loads(self._body.decode("utf-8"))
def default_post(url: str, headers: Mapping[str, str], json: Mapping[str, Any], timeout: int):
req = request.Request(
url,
data=json_dumps_bytes(json),
headers=dict(headers),
method="POST",
)
with request.urlopen(req, timeout=timeout) as response:
return UrlLibResponse(response.status, response.read())
def json_dumps_bytes(payload: Mapping[str, Any]) -> bytes:
return json.dumps(payload).encode("utf-8")
def make_multimodal_content(
text: str,
image_b64s: Optional[Iterable[Union[str, EncodedImage]]] = None,
) -> List[Dict[str, Any]]:
content = [{"type": "text", "text": text}]
for image in image_b64s or []:
encoded = image if isinstance(image, EncodedImage) else EncodedImage(data=image)
content.append(
{
"type": "image_url",
"image_url": {
"url": f"data:{encoded.media_type};base64,{encoded.data}"
},
}
)
return content
class OpenAICompatibleChatClient:
def __init__(self, profile: ProviderConfig, post: Optional[PostFn] = None):
self.profile = profile
self.post = post or default_post
def build_payload(
self,
prompt: str,
image_b64s: Optional[Iterable[Union[str, EncodedImage]]] = None,
response_format: Optional[Mapping[str, Any]] = None,
temperature: float = 0,
max_tokens: int = 4096,
) -> Dict[str, Any]:
return self.build_messages_payload(
[
{
"role": "user",
"content": make_multimodal_content(prompt, image_b64s),
}
],
response_format=response_format,
temperature=temperature,
max_tokens=max_tokens,
)
def build_messages_payload(
self,
messages: Iterable[Mapping[str, Any]],
response_format: Optional[Mapping[str, Any]] = None,
temperature: float = 0,
max_tokens: int = 4096,
) -> Dict[str, Any]:
payload: Dict[str, Any] = {
"model": self.profile.model,
"temperature": temperature,
"messages": [dict(message) for message in messages],
"max_tokens": max_tokens,
}
if response_format:
payload["response_format"] = dict(response_format)
if self.profile.router_options:
payload["provider"] = dict(self.profile.router_options)
return payload
def chat_completion(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
if not self.profile.api_key:
raise RuntimeError(f"Missing API key in environment variable {self.profile.api_key_env}")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.profile.api_key}",
}
response = self.post(
f"{self.profile.base_url.rstrip('/')}/chat/completions",
headers=headers,
json=dict(payload),
timeout=120,
)
response.raise_for_status()
return response.json()
def complete_json(
self,
prompt: str,
image_b64s: Optional[Iterable[Union[str, EncodedImage]]] = None,
response_format: Optional[Mapping[str, Any]] = None,
) -> Any:
payload = self.build_payload(prompt, image_b64s, response_format=response_format)
return self.complete_json_payload(payload)
def complete_json_messages(
self,
messages: Iterable[Mapping[str, Any]],
response_format: Optional[Mapping[str, Any]] = None,
) -> Any:
payload = self.build_messages_payload(
messages,
response_format=response_format,
)
return self.complete_json_payload(payload)
def complete_json_payload(self, payload: Mapping[str, Any]) -> Any:
response = self.chat_completion(payload)
content = response["choices"][0]["message"]["content"].strip()
if content.startswith("```"):
content = content.strip("`").split("\n", 1)[-1]
return json.loads(content)