Angshul's picture
Upload 16 files
ea9e5f8 verified
Raw History Blame Contribute Delete
4.16 kB
from __future__ import annotations
import asyncio
import time
from dataclasses import dataclass
from typing import Any
import httpx
from .config import Model
class UpstreamError(RuntimeError):
pass
@dataclass
class ChatResult:
text: str
prompt_tokens: int
output_tokens: int
elapsed_ms: float
price_usd: float | None
usage_source: str
class RemoteClient:
def __init__(self, timeout_sec: float = 90.0, max_connections: int = 128,
transport: httpx.AsyncBaseTransport | None = None):
self.http = httpx.AsyncClient(timeout=timeout_sec,
limits=httpx.Limits(max_connections=max_connections, max_keepalive_connections=min(max_connections,32)),
transport=transport, follow_redirects=False)
async def close(self) -> None:
await self.http.aclose()
async def chat(self, model: Model, messages: list[dict], max_tokens: int, timeout_sec: float) -> ChatResult:
if max_tokens < 1: raise ValueError("max_tokens must be positive")
headers={"content-type":"application/json"}
if model.api_key:headers["authorization"]="Bearer "+model.api_key
payload={"model":model.model_id,"messages":messages,"max_tokens":max_tokens,"temperature":0}
began=time.perf_counter()
try:
response=await self.http.post(model.base_url + "/chat/completions",json=payload,
headers=headers,timeout=max(.05,timeout_sec))
response.raise_for_status()
parsed=response.json()
content=parsed["choices"][0]["message"].get("content")
if isinstance(content,list):
content=" ".join(p.get("text","") for p in content if isinstance(p,dict))
if not isinstance(content,str): raise ValueError("No text content returned")
usage=parsed.get("usage") or {}
# Missing usage is reported as unknown, not silently replaced with a fictitious charge.
pt=usage.get("prompt_tokens")
ot=usage.get("completion_tokens")
known=isinstance(pt,int) and isinstance(ot,int)
price=(pt*model.price_input_per_1m+ot*model.price_output_per_1m)/1e6 if known and model.priced else None
return ChatResult(content, int(pt) if known else 0,int(ot) if known else 0,
(time.perf_counter()-began)*1000,price,"provider" if known else "missing")
except (httpx.HTTPError, ValueError, KeyError, TypeError, IndexError) as e:
raise UpstreamError(f"Model backend '{model.alias}' failed: {type(e).__name__}") from e
async def retrieve(self, url: str, api_key: str, query: str, top_k: int,
timeout_sec: float) -> list[dict[str,Any]]:
if not url.startswith("https://") and not (url.startswith("http://") and __import__("os").getenv("ALLOW_HTTP_ENDPOINTS")=="1"):
raise ValueError("Retriever URL must use https")
headers={"content-type":"application/json"}
if api_key:headers["authorization"]="Bearer "+api_key
try:
r=await self.http.post(url,json={"query":query,"top_k":top_k},headers=headers,
timeout=max(.05,timeout_sec))
r.raise_for_status()
raw=r.json()
docs=raw["documents"] if isinstance(raw,dict) else raw
if not isinstance(docs,list):raise ValueError("Retriever must return documents array")
clean=[]
for d in docs[:top_k]:
if not isinstance(d,dict) or not isinstance(d.get("text"),str):continue
score=d.get("score",0.0)
clean.append({"id":str(d.get("id",len(clean)))[:100],"text":d["text"][:5000],
"score":float(score) if isinstance(score,(int,float)) else 0.0,
"title":str(d.get("title",""))[:180]})
return clean
except (httpx.HTTPError, ValueError, KeyError, TypeError) as e:
raise UpstreamError(f"Retriever backend failed: {type(e).__name__}") from e