File size: 4,200 Bytes
cef045d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
"""OB-1 API client — proxies requests to dashboard.openblocklabs.com/api/v1."""

from __future__ import annotations

import httpx
from typing import Any

from ..core import config as _config
from ..core.config import OB1_API_BASE
from ..core.logger import get_logger

log = get_logger("client")

_HEADERS = {
    "HTTP-Referer": "https://github.com/delta-hq/ob1",
    "X-Title": "OB1 CLI",
}


class StreamResponse:
    """Wrapper that keeps httpx client alive during streaming."""

    def __init__(self, resp: httpx.Response, client: httpx.AsyncClient):
        self._resp = resp
        self._client = client

    def __getattr__(self, name):
        return getattr(self._resp, name)

    async def aclose(self):
        await self._resp.aclose()
        await self._client.aclose()


class OB1Client:
    """Async HTTP client to OBL OpenRouter-compatible API."""

    def __init__(self):
        self.base_url = OB1_API_BASE
        self._models_cache: list | None = None

    def _proxy(self) -> str | None:
        url = _config.PROXY_URL
        return url if url else None

    async def fetch_models(self, api_key: str) -> list:
        """Fetch available models from OB-1. Cached after first call."""
        if self._models_cache is not None:
            return self._models_cache
        try:
            log.debug("Fetching models from %s/models", self.base_url)
            async with httpx.AsyncClient(timeout=15, proxy=self._proxy()) as client:
                resp = await client.get(
                    f"{self.base_url}/models",
                    headers={**_HEADERS, "Authorization": f"Bearer {api_key}"},
                )
            if resp.status_code == 200:
                self._models_cache = resp.json().get("data", [])
                log.info("Fetched %d models", len(self._models_cache))
                return self._models_cache
            log.warning("Models fetch returned %d", resp.status_code)
        except Exception as e:
            log.error("Models fetch failed: %s", e)
        return []

    async def chat(
        self,
        api_key: str,
        messages: list,
        model: str = "anthropic/claude-opus-4.6",
        stream: bool = False,
        temperature: float | None = None,
        top_p: float | None = None,
        max_tokens: int | None = None,
        extra_payload: dict[str, Any] | None = None,
    ) -> httpx.Response:
        """Send chat completion request. Returns raw httpx Response."""
        payload = {
            "model": model,
            "messages": messages,
            "stream": stream,
        }
        if temperature is not None:
            payload["temperature"] = temperature
        if top_p is not None:
            payload["top_p"] = top_p
        if max_tokens is not None:
            payload["max_tokens"] = max_tokens
        if stream:
            payload["stream_options"] = {"include_usage": True}
        if extra_payload:
            payload.update(extra_payload)

        headers = {
            **_HEADERS,
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        }

        client = httpx.AsyncClient(timeout=300, proxy=self._proxy())
        try:
            if stream:
                log.debug("Sending stream request to %s model=%s", self.base_url, model)
                req = client.build_request(
                    "POST",
                    f"{self.base_url}/chat/completions",
                    json=payload,
                    headers=headers,
                )
                resp = await client.send(req, stream=True)
                return StreamResponse(resp, client)
            else:
                log.debug("Sending request to %s model=%s", self.base_url, model)
                resp = await client.post(
                    f"{self.base_url}/chat/completions",
                    json=payload,
                    headers=headers,
                )
                log.debug("Response status=%d", resp.status_code)
                await client.aclose()
                return resp
        except Exception as e:
            log.error("Request failed: %s", e)
            await client.aclose()
            raise