querais 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ # dependencies
2
+ node_modules/
3
+ .pnpm-store/
4
+
5
+ # build output
6
+ dist/
7
+ *.tsbuildinfo
8
+
9
+ # hardhat / contracts
10
+ packages/contracts/artifacts/
11
+ packages/contracts/cache/
12
+ packages/contracts/typechain-types/
13
+ packages/contracts/coverage/
14
+ packages/contracts/coverage.json
15
+ # Slither's symlink-free analysis copy (built fresh by the CI slither job)
16
+ packages/contracts/slither-scratch/
17
+ # Ephemeral local deployment only; real-network manifests (e.g. Sepolia) are committed.
18
+ packages/contracts/deployments/addresses.localhost.json
19
+ packages/contracts/src/abis.ts
20
+
21
+ # env & secrets
22
+ .env
23
+ .env.*
24
+ !.env.example
25
+ *.keystore
26
+
27
+ # logs
28
+ *.log
29
+ npm-debug.log*
30
+ pnpm-debug.log*
31
+
32
+ # editor / os
33
+ .DS_Store
34
+ .idea/
35
+ .vscode/*
36
+ !.vscode/extensions.json
37
+
38
+ # runtime data
39
+ data/
40
+ *.local.json
41
+
42
+ # release artifacts (built by scripts/bundle-daemon.mjs / release.yml)
43
+ # anchored: scripts/release/ (launcher sources) must stay tracked
44
+ /release/
45
+
46
+ # python (sdk-python)
47
+ .venv/
48
+ __pycache__/
49
+ *.egg-info/
50
+ .ruff_cache/
51
+ .pytest_cache/
52
+ sdk-python/dist/
querais-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: querais
3
+ Version: 0.2.0
4
+ Summary: Python client for QueraIS, the decentralized AI inference marketplace. OpenAI-shaped chat/streaming plus QueraIS extras: nodes, stats, model manifest.
5
+ Project-URL: Homepage, https://github.com/ShavitR/querais/tree/main/sdk-python#readme
6
+ Project-URL: Repository, https://github.com/ShavitR/querais
7
+ Project-URL: Issues, https://github.com/ShavitR/querais/issues
8
+ Author: QueraIS contributors
9
+ License-Expression: MIT
10
+ Keywords: ai,arbitrum,decentralized,inference,llm,openai-compatible,querais,web3
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx>=0.27
21
+ Provides-Extra: dev
22
+ Requires-Dist: build>=1.2; extra == 'dev'
23
+ Requires-Dist: pytest>=8; extra == 'dev'
24
+ Requires-Dist: ruff>=0.8; extra == 'dev'
25
+ Provides-Extra: langchain
26
+ Requires-Dist: langchain-openai>=0.2; extra == 'langchain'
27
+ Provides-Extra: llamaindex
28
+ Requires-Dist: llama-index-llms-openai-like>=0.3; extra == 'llamaindex'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # querais
32
+
33
+ Python client for [QueraIS](https://github.com/ShavitR/querais) — the decentralized
34
+ AI inference marketplace. Anyone with a GPU serves models and earns; you buy
35
+ inference through one OpenAI-compatible endpoint.
36
+
37
+ ```bash
38
+ pip install querais
39
+ ```
40
+
41
+ ```python
42
+ from querais import QueraisClient
43
+
44
+ client = QueraisClient("https://querais-gateway.fly.dev", api_key="sk-...")
45
+ result = client.chat([{"role": "user", "content": "hello"}], model="llama3.2")
46
+ print(result.content)
47
+ ```
48
+
49
+ ## OpenAI-compatible
50
+
51
+ The gateway speaks the OpenAI chat-completions protocol, so the official `openai`
52
+ package works too — point it at the gateway:
53
+
54
+ ```python
55
+ from openai import OpenAI
56
+
57
+ client = OpenAI(base_url="https://querais-gateway.fly.dev/v1", api_key="sk-...")
58
+ ```
59
+
60
+ This package is thin sugar over that protocol plus QueraIS-specific helpers.
61
+
62
+ ## Streaming
63
+
64
+ ```python
65
+ for delta in client.chat_stream([{"role": "user", "content": "tell me a story"}],
66
+ model="llama3.2"):
67
+ print(delta, end="", flush=True)
68
+ ```
69
+
70
+ ## QueraIS extras
71
+
72
+ ```python
73
+ client.models() # model ids served by connected nodes
74
+ client.nodes() # public node directory: reputation, prices, dimensions
75
+ client.stats() # network stats
76
+ client.model_manifest() # the gateway's signed model-digest manifest (404 if unpinned)
77
+ ```
78
+
79
+ Routing extensions on `chat()` / `chat_stream()`:
80
+
81
+ ```python
82
+ client.chat(messages, model="llama3.2",
83
+ max_price_per_1k_tokens=0.5, # cap what you pay
84
+ min_reputation=0.7) # floor the node quality
85
+ ```
86
+
87
+ ## LangChain / LlamaIndex
88
+
89
+ The integrations return the **official** LangChain / LlamaIndex OpenAI classes
90
+ configured for the gateway — nothing reimplemented:
91
+
92
+ ```bash
93
+ pip install 'querais[langchain]' # or 'querais[llamaindex]'
94
+ ```
95
+
96
+ ```python
97
+ from querais.langchain import chat_model
98
+ llm = chat_model("https://querais-gateway.fly.dev", api_key="sk-...", model="llama3.2")
99
+
100
+ from querais.llamaindex import llm as qllm
101
+ llm = qllm("https://querais-gateway.fly.dev", api_key="sk-...", model="llama3.2")
102
+ ```
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,76 @@
1
+ # querais
2
+
3
+ Python client for [QueraIS](https://github.com/ShavitR/querais) — the decentralized
4
+ AI inference marketplace. Anyone with a GPU serves models and earns; you buy
5
+ inference through one OpenAI-compatible endpoint.
6
+
7
+ ```bash
8
+ pip install querais
9
+ ```
10
+
11
+ ```python
12
+ from querais import QueraisClient
13
+
14
+ client = QueraisClient("https://querais-gateway.fly.dev", api_key="sk-...")
15
+ result = client.chat([{"role": "user", "content": "hello"}], model="llama3.2")
16
+ print(result.content)
17
+ ```
18
+
19
+ ## OpenAI-compatible
20
+
21
+ The gateway speaks the OpenAI chat-completions protocol, so the official `openai`
22
+ package works too — point it at the gateway:
23
+
24
+ ```python
25
+ from openai import OpenAI
26
+
27
+ client = OpenAI(base_url="https://querais-gateway.fly.dev/v1", api_key="sk-...")
28
+ ```
29
+
30
+ This package is thin sugar over that protocol plus QueraIS-specific helpers.
31
+
32
+ ## Streaming
33
+
34
+ ```python
35
+ for delta in client.chat_stream([{"role": "user", "content": "tell me a story"}],
36
+ model="llama3.2"):
37
+ print(delta, end="", flush=True)
38
+ ```
39
+
40
+ ## QueraIS extras
41
+
42
+ ```python
43
+ client.models() # model ids served by connected nodes
44
+ client.nodes() # public node directory: reputation, prices, dimensions
45
+ client.stats() # network stats
46
+ client.model_manifest() # the gateway's signed model-digest manifest (404 if unpinned)
47
+ ```
48
+
49
+ Routing extensions on `chat()` / `chat_stream()`:
50
+
51
+ ```python
52
+ client.chat(messages, model="llama3.2",
53
+ max_price_per_1k_tokens=0.5, # cap what you pay
54
+ min_reputation=0.7) # floor the node quality
55
+ ```
56
+
57
+ ## LangChain / LlamaIndex
58
+
59
+ The integrations return the **official** LangChain / LlamaIndex OpenAI classes
60
+ configured for the gateway — nothing reimplemented:
61
+
62
+ ```bash
63
+ pip install 'querais[langchain]' # or 'querais[llamaindex]'
64
+ ```
65
+
66
+ ```python
67
+ from querais.langchain import chat_model
68
+ llm = chat_model("https://querais-gateway.fly.dev", api_key="sk-...", model="llama3.2")
69
+
70
+ from querais.llamaindex import llm as qllm
71
+ llm = qllm("https://querais-gateway.fly.dev", api_key="sk-...", model="llama3.2")
72
+ ```
73
+
74
+ ## License
75
+
76
+ MIT
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "querais"
7
+ version = "0.2.0"
8
+ description = "Python client for QueraIS, the decentralized AI inference marketplace. OpenAI-shaped chat/streaming plus QueraIS extras: nodes, stats, model manifest."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "QueraIS contributors" }]
13
+ keywords = [
14
+ "querais",
15
+ "ai",
16
+ "llm",
17
+ "inference",
18
+ "openai-compatible",
19
+ "decentralized",
20
+ "arbitrum",
21
+ "web3",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Intended Audience :: Developers",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
32
+ ]
33
+ dependencies = ["httpx>=0.27"]
34
+
35
+ [project.optional-dependencies]
36
+ # Integrations return the OFFICIAL LangChain / LlamaIndex OpenAI classes pointed at
37
+ # the gateway — nothing is reimplemented, and `pip install querais` stays light.
38
+ langchain = ["langchain-openai>=0.2"]
39
+ llamaindex = ["llama-index-llms-openai-like>=0.3"]
40
+ dev = ["pytest>=8", "ruff>=0.8", "build>=1.2"]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/ShavitR/querais/tree/main/sdk-python#readme"
44
+ Repository = "https://github.com/ShavitR/querais"
45
+ Issues = "https://github.com/ShavitR/querais/issues"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/querais"]
49
+
50
+ [tool.ruff]
51
+ line-length = 100
52
+ target-version = "py310"
53
+
54
+ [tool.ruff.lint]
55
+ # Defaults (pyflakes + pycodestyle errors) plus import sorting, bugbear, pyupgrade.
56
+ select = ["E", "F", "I", "B", "UP"]
57
+
58
+ [tool.pytest.ini_options]
59
+ testpaths = ["tests"]
@@ -0,0 +1,12 @@
1
+ """QueraIS — Python client for the decentralized AI inference marketplace.
2
+
3
+ The gateway is OpenAI-compatible; this package is thin sugar plus QueraIS-specific
4
+ helpers. LangChain / LlamaIndex users: see ``querais.langchain`` /
5
+ ``querais.llamaindex`` (optional extras).
6
+ """
7
+
8
+ from .client import ChatResult, Message, QueraisClient, QueraisError
9
+
10
+ __version__ = "0.1.0"
11
+
12
+ __all__ = ["ChatResult", "Message", "QueraisClient", "QueraisError", "__version__"]
@@ -0,0 +1,201 @@
1
+ """Thin, typed client for the QueraIS gateway.
2
+
3
+ The gateway is OpenAI-compatible, so this is convenience sugar (exactly like the
4
+ TypeScript SDK): chat + SSE streaming plus QueraIS-specific helpers (nodes, stats,
5
+ model manifest). The official ``openai`` package also works against the gateway.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from collections.abc import Iterator
12
+ from dataclasses import dataclass, field
13
+ from typing import Any
14
+
15
+ import httpx
16
+
17
+ Message = dict[str, str]
18
+ """An OpenAI-shaped chat message: ``{"role": "user", "content": "..."}``."""
19
+
20
+
21
+ class QueraisError(Exception):
22
+ """A non-2xx response from the gateway (carries status + body text)."""
23
+
24
+ def __init__(self, status: int, body: str):
25
+ super().__init__(f"QueraIS gateway returned HTTP {status}: {body}")
26
+ self.status = status
27
+ self.body = body
28
+
29
+
30
+ @dataclass
31
+ class ChatResult:
32
+ """A buffered completion: the text, token usage, and the on-chain job id."""
33
+
34
+ content: str
35
+ usage: dict[str, int] = field(default_factory=dict)
36
+ job_id: str | None = None
37
+
38
+
39
+ class QueraisClient:
40
+ """Synchronous client for a QueraIS gateway.
41
+
42
+ >>> client = QueraisClient("https://querais-gateway.fly.dev", api_key="sk-...")
43
+ >>> result = client.chat([{"role": "user", "content": "hello"}], model="llama3.2")
44
+ >>> print(result.content)
45
+
46
+ ``transport`` is injectable for tests (``httpx.MockTransport``) — no network,
47
+ no gateway needed.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ base_url: str,
53
+ *,
54
+ api_key: str,
55
+ timeout: float = 120.0,
56
+ transport: httpx.BaseTransport | None = None,
57
+ ):
58
+ self._http = httpx.Client(
59
+ base_url=base_url.rstrip("/"),
60
+ timeout=timeout,
61
+ headers={"authorization": f"Bearer {api_key}"},
62
+ transport=transport,
63
+ )
64
+
65
+ # ── lifecycle ──────────────────────────────────────────────────────────────
66
+
67
+ def close(self) -> None:
68
+ self._http.close()
69
+
70
+ def __enter__(self) -> QueraisClient:
71
+ return self
72
+
73
+ def __exit__(self, *exc: object) -> None:
74
+ self.close()
75
+
76
+ # ── chat ───────────────────────────────────────────────────────────────────
77
+
78
+ def _chat_body(
79
+ self,
80
+ messages: list[Message],
81
+ model: str,
82
+ stream: bool,
83
+ max_tokens: int | None,
84
+ temperature: float | None,
85
+ max_price_per_1k_tokens: float | None,
86
+ min_reputation: float | None,
87
+ ) -> dict[str, Any]:
88
+ body: dict[str, Any] = {"model": model, "messages": messages, "stream": stream}
89
+ if max_tokens is not None:
90
+ body["max_tokens"] = max_tokens
91
+ if temperature is not None:
92
+ body["temperature"] = temperature
93
+ # QueraIS routing extensions (ignored by other OpenAI-compatible servers).
94
+ if max_price_per_1k_tokens is not None:
95
+ body["max_price_per_1k_tokens"] = max_price_per_1k_tokens
96
+ if min_reputation is not None:
97
+ body["min_reputation"] = min_reputation
98
+ return body
99
+
100
+ def chat(
101
+ self,
102
+ messages: list[Message],
103
+ *,
104
+ model: str,
105
+ max_tokens: int | None = None,
106
+ temperature: float | None = None,
107
+ max_price_per_1k_tokens: float | None = None,
108
+ min_reputation: float | None = None,
109
+ ) -> ChatResult:
110
+ """Buffered chat completion."""
111
+ res = self._http.post(
112
+ "/v1/chat/completions",
113
+ json=self._chat_body(
114
+ messages,
115
+ model,
116
+ False,
117
+ max_tokens,
118
+ temperature,
119
+ max_price_per_1k_tokens,
120
+ min_reputation,
121
+ ),
122
+ )
123
+ if res.status_code >= 400:
124
+ raise QueraisError(res.status_code, res.text)
125
+ data = res.json()
126
+ choices = data.get("choices") or [{}]
127
+ return ChatResult(
128
+ content=(choices[0].get("message") or {}).get("content", ""),
129
+ usage=data.get("usage") or {},
130
+ job_id=res.headers.get("x-querais-job-id"),
131
+ )
132
+
133
+ def chat_stream(
134
+ self,
135
+ messages: list[Message],
136
+ *,
137
+ model: str,
138
+ max_tokens: int | None = None,
139
+ temperature: float | None = None,
140
+ max_price_per_1k_tokens: float | None = None,
141
+ min_reputation: float | None = None,
142
+ ) -> Iterator[str]:
143
+ """Streaming chat completion — yields content deltas as they arrive (SSE)."""
144
+ with self._http.stream(
145
+ "POST",
146
+ "/v1/chat/completions",
147
+ json=self._chat_body(
148
+ messages,
149
+ model,
150
+ True,
151
+ max_tokens,
152
+ temperature,
153
+ max_price_per_1k_tokens,
154
+ min_reputation,
155
+ ),
156
+ ) as res:
157
+ if res.status_code >= 400:
158
+ res.read()
159
+ raise QueraisError(res.status_code, res.text)
160
+ for line in res.iter_lines():
161
+ if not line.startswith("data:"):
162
+ continue
163
+ data = line[len("data:") :].strip()
164
+ if not data or data == "[DONE]":
165
+ continue
166
+ try:
167
+ frame = json.loads(data)
168
+ except ValueError:
169
+ continue # keep-alives / non-JSON frames
170
+ delta = ((frame.get("choices") or [{}])[0].get("delta") or {}).get("content")
171
+ if delta:
172
+ yield delta
173
+
174
+ # ── QueraIS extras ─────────────────────────────────────────────────────────
175
+
176
+ def models(self) -> list[str]:
177
+ """Model ids currently served by at least one connected node."""
178
+ data = self._get_json("/v1/models")
179
+ return [m["id"] for m in data.get("data", [])]
180
+
181
+ def nodes(self) -> list[dict[str, Any]]:
182
+ """The public node directory: wallet, reputation, offers, dimensions."""
183
+ return self._get_json("/v1/nodes").get("data", [])
184
+
185
+ def stats(self) -> dict[str, Any]:
186
+ """Network stats (jobs, volume, nodes)."""
187
+ return self._get_json("/v1/stats")
188
+
189
+ def model_manifest(self) -> dict[str, Any]:
190
+ """The gateway's signed model manifest (Slice 9). Raises 404 when unpinned.
191
+
192
+ The returned object is signed (EIP-191) by the gateway's settler address and
193
+ can be verified offline — see ``GET /v1/credit/info`` for the settler.
194
+ """
195
+ return self._get_json("/v1/models/manifest")
196
+
197
+ def _get_json(self, path: str) -> dict[str, Any]:
198
+ res = self._http.get(path)
199
+ if res.status_code >= 400:
200
+ raise QueraisError(res.status_code, res.text)
201
+ return res.json()
@@ -0,0 +1,30 @@
1
+ """LangChain integration: the OFFICIAL ``ChatOpenAI`` pointed at a QueraIS gateway.
2
+
3
+ Nothing is reimplemented — the gateway speaks the OpenAI protocol, so the genuine
4
+ LangChain class works as-is. Requires the optional extra::
5
+
6
+ pip install 'querais[langchain]'
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+
14
+ def chat_model(base_url: str, *, api_key: str, model: str, **kwargs: Any) -> Any:
15
+ """A ``langchain_openai.ChatOpenAI`` configured for the gateway.
16
+
17
+ Extra ``kwargs`` (temperature, max_tokens, …) pass straight through.
18
+ """
19
+ try:
20
+ from langchain_openai import ChatOpenAI
21
+ except ImportError as err: # pragma: no cover - exercised via the extra
22
+ raise ImportError(
23
+ "LangChain support needs the optional extra: pip install 'querais[langchain]'"
24
+ ) from err
25
+ return ChatOpenAI(
26
+ base_url=f"{base_url.rstrip('/')}/v1",
27
+ api_key=api_key,
28
+ model=model,
29
+ **kwargs,
30
+ )
@@ -0,0 +1,32 @@
1
+ """LlamaIndex integration: the OFFICIAL ``OpenAILike`` pointed at a QueraIS gateway.
2
+
3
+ Nothing is reimplemented — the gateway speaks the OpenAI protocol, so the genuine
4
+ LlamaIndex class works as-is. Requires the optional extra::
5
+
6
+ pip install 'querais[llamaindex]'
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+
14
+ def llm(base_url: str, *, api_key: str, model: str, **kwargs: Any) -> Any:
15
+ """A ``llama_index.llms.openai_like.OpenAILike`` configured for the gateway.
16
+
17
+ ``is_chat_model`` defaults to True (the gateway serves chat completions).
18
+ Extra ``kwargs`` (temperature, max_tokens, …) pass straight through.
19
+ """
20
+ try:
21
+ from llama_index.llms.openai_like import OpenAILike
22
+ except ImportError as err: # pragma: no cover - exercised via the extra
23
+ raise ImportError(
24
+ "LlamaIndex support needs the optional extra: pip install 'querais[llamaindex]'"
25
+ ) from err
26
+ kwargs.setdefault("is_chat_model", True)
27
+ return OpenAILike(
28
+ api_base=f"{base_url.rstrip('/')}/v1",
29
+ api_key=api_key,
30
+ model=model,
31
+ **kwargs,
32
+ )
@@ -0,0 +1,127 @@
1
+ """QueraisClient unit tests on httpx.MockTransport — no network, no gateway."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ import httpx
8
+ import pytest
9
+
10
+ from querais import ChatResult, QueraisClient, QueraisError
11
+
12
+ BASE = "https://gw.test"
13
+
14
+
15
+ def make_client(handler) -> QueraisClient:
16
+ return QueraisClient(BASE, api_key="sk-test", transport=httpx.MockTransport(handler))
17
+
18
+
19
+ def test_chat_returns_content_usage_and_job_id():
20
+ def handler(request: httpx.Request) -> httpx.Response:
21
+ assert request.url.path == "/v1/chat/completions"
22
+ assert request.headers["authorization"] == "Bearer sk-test"
23
+ body = json.loads(request.content)
24
+ assert body["model"] == "mock-model"
25
+ assert body["stream"] is False
26
+ assert body["max_tokens"] == 32
27
+ assert body["max_price_per_1k_tokens"] == 0.5 # QueraIS routing extension
28
+ return httpx.Response(
29
+ 200,
30
+ headers={"x-querais-job-id": "0xjob"},
31
+ json={
32
+ "choices": [{"message": {"role": "assistant", "content": "hello back"}}],
33
+ "usage": {"prompt_tokens": 3, "completion_tokens": 2, "total_tokens": 5},
34
+ },
35
+ )
36
+
37
+ with make_client(handler) as client:
38
+ result = client.chat(
39
+ [{"role": "user", "content": "hello"}],
40
+ model="mock-model",
41
+ max_tokens=32,
42
+ max_price_per_1k_tokens=0.5,
43
+ )
44
+ assert result == ChatResult(
45
+ content="hello back",
46
+ usage={"prompt_tokens": 3, "completion_tokens": 2, "total_tokens": 5},
47
+ job_id="0xjob",
48
+ )
49
+
50
+
51
+ def test_chat_raises_typed_error_with_status_and_body():
52
+ def handler(request: httpx.Request) -> httpx.Response:
53
+ return httpx.Response(
54
+ 503, json={"error": {"message": "no capacity", "type": "no_eligible_nodes"}}
55
+ )
56
+
57
+ with make_client(handler) as client:
58
+ with pytest.raises(QueraisError) as exc:
59
+ client.chat([{"role": "user", "content": "hi"}], model="mock-model")
60
+ assert exc.value.status == 503
61
+ assert "no_eligible_nodes" in exc.value.body
62
+
63
+
64
+ def test_chat_stream_yields_deltas_and_skips_done_and_noise():
65
+ sse = (
66
+ 'data: {"choices":[{"delta":{"content":"Hel"}}]}\n\n'
67
+ "data: \n\n" # keep-alive
68
+ 'data: {"choices":[{"delta":{}}]}\n\n' # finish frame, no content
69
+ 'data: {"choices":[{"delta":{"content":"lo!"}}]}\n\n'
70
+ "data: [DONE]\n\n"
71
+ )
72
+
73
+ def handler(request: httpx.Request) -> httpx.Response:
74
+ assert json.loads(request.content)["stream"] is True
75
+ return httpx.Response(
76
+ 200, content=sse.encode(), headers={"content-type": "text/event-stream"}
77
+ )
78
+
79
+ with make_client(handler) as client:
80
+ chunks = list(client.chat_stream([{"role": "user", "content": "hi"}], model="mock-model"))
81
+ assert chunks == ["Hel", "lo!"]
82
+
83
+
84
+ def test_chat_stream_raises_on_error_status():
85
+ def handler(request: httpx.Request) -> httpx.Response:
86
+ return httpx.Response(401, json={"error": {"message": "bad key"}})
87
+
88
+ with make_client(handler) as client:
89
+ with pytest.raises(QueraisError) as exc:
90
+ list(client.chat_stream([{"role": "user", "content": "hi"}], model="mock-model"))
91
+ assert exc.value.status == 401
92
+
93
+
94
+ def test_models_nodes_stats_and_manifest():
95
+ manifest = {
96
+ "models": {"llama3.2": {"digest": "sha256:" + "a" * 64}},
97
+ "signer": "0x" + "1" * 40,
98
+ "signature": "0x" + "2" * 130,
99
+ }
100
+
101
+ def handler(request: httpx.Request) -> httpx.Response:
102
+ routes = {
103
+ "/v1/models": {"object": "list", "data": [{"id": "llama3.2"}, {"id": "phi3"}]},
104
+ "/v1/nodes": {"data": [{"wallet": "0xabc", "reputation": 0.71}]},
105
+ "/v1/stats": {"jobs24h": 7, "nodes": 1},
106
+ "/v1/models/manifest": manifest,
107
+ }
108
+ payload = routes.get(request.url.path)
109
+ if payload is None:
110
+ return httpx.Response(404, json={"error": "not found"})
111
+ return httpx.Response(200, json=payload)
112
+
113
+ with make_client(handler) as client:
114
+ assert client.models() == ["llama3.2", "phi3"]
115
+ assert client.nodes() == [{"wallet": "0xabc", "reputation": 0.71}]
116
+ assert client.stats() == {"jobs24h": 7, "nodes": 1}
117
+ assert client.model_manifest() == manifest
118
+
119
+
120
+ def test_model_manifest_404_when_gateway_unpinned():
121
+ def handler(request: httpx.Request) -> httpx.Response:
122
+ return httpx.Response(404, json={"error": "no model manifest configured"})
123
+
124
+ with make_client(handler) as client:
125
+ with pytest.raises(QueraisError) as exc:
126
+ client.model_manifest()
127
+ assert exc.value.status == 404
@@ -0,0 +1,27 @@
1
+ """Integration-module tests — skip cleanly when the optional extra isn't installed."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+
8
+ def test_langchain_chat_model_points_at_the_gateway():
9
+ pytest.importorskip("langchain_openai")
10
+ from querais.langchain import chat_model
11
+
12
+ llm = chat_model("https://gw.test/", api_key="sk-test", model="llama3.2", temperature=0)
13
+ # The official class, configured for the gateway (trailing slash normalized).
14
+ assert type(llm).__name__ == "ChatOpenAI"
15
+ assert llm.openai_api_base == "https://gw.test/v1"
16
+ assert llm.model_name == "llama3.2"
17
+
18
+
19
+ def test_llamaindex_llm_points_at_the_gateway():
20
+ pytest.importorskip("llama_index.llms.openai_like")
21
+ from querais.llamaindex import llm
22
+
23
+ model = llm("https://gw.test", api_key="sk-test", model="llama3.2")
24
+ assert type(model).__name__ == "OpenAILike"
25
+ assert model.api_base == "https://gw.test/v1"
26
+ assert model.model == "llama3.2"
27
+ assert model.is_chat_model is True