Skip to content

Commit 674dbeb

Browse files
Merge pull request #8 from Sartify/fix/chat-return-dict
Return chat responses as API JSON dicts by default.
2 parents ac35534 + 70b374f commit 674dbeb

9 files changed

Lines changed: 91 additions & 167 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project are documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Changed
11+
12+
- `client.chat.create` and `client.agents.chat.create` always return the API JSON as a `dict` (no `raw=True` / `.raw`)
13+
- `stream.collect()` returns a dict in the same shape as a non-streaming chat response
14+
815
## [0.2.0] - 2026-08-23
916

1017
First public release on [PyPI](https://pypi.org/project/pawa-ai/).

‎README.md‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export PAWA_AI_API_KEY="your_api_key_here"
2525

2626
Get your key from the [Builders Dashboard](https://builder.pawa-ai.com/dashboard?page=keys).
2727

28-
See **[examples/](examples/)** for a full walkthrough from `pip install` to typed responses.
28+
See **[examples/](examples/)** for a full walkthrough from `pip install` to API responses.
2929

3030
### Chat
3131

@@ -45,8 +45,10 @@ response = client.chat.create(
4545
stream=False,
4646
)
4747

48-
print(response.text) # typed ChatCompletion response
49-
print(response.usage) # token usage when available
48+
# Always a dict matching the API JSON
49+
print(response["success"])
50+
print(response["data"]["request"][0]["message"]["content"])
51+
print(response["data"].get("usage"))
5052
```
5153

5254
### Streaming
@@ -64,7 +66,7 @@ with client.chat.create(
6466

6567
# Or collect the full response after streaming
6668
completion = stream.collect()
67-
print(completion.text)
69+
print(completion["data"]["request"][0]["message"]["content"])
6870
```
6971

7072
Async streaming:
@@ -99,8 +101,6 @@ response = client.vectors.create(
99101
embeddings = response.embeddings
100102
```
101103

102-
Pass `raw=True` on any resource method to get the original JSON dict instead of typed models.
103-
104104
### Retries with exponential backoff
105105

106106
```python
@@ -133,7 +133,7 @@ async def main():
133133
{"role": "user", "content": [{"type": "text", "text": "Habari yako?"}]}
134134
],
135135
)
136-
print(response.text)
136+
print(response["data"]["request"][0]["message"]["content"])
137137

138138
asyncio.run(main())
139139
```
@@ -156,12 +156,12 @@ asyncio.run(main())
156156
## Error handling
157157

158158
```python
159-
from pawa_ai import PawaAI, AuthenticationError, RateLimitError, ChatCompletion
159+
from pawa_ai import PawaAI, AuthenticationError, RateLimitError
160160

161161
client = PawaAI()
162162

163163
try:
164-
completion: ChatCompletion = client.chat.create(model="pawa-v1-ember-20240924", messages=[...])
164+
response = client.chat.create(model="pawa-v1-ember-20240924", messages=[...])
165165
except AuthenticationError as e:
166166
print(f"Auth failed: {e.message}")
167167
except RateLimitError as e:

‎examples/README.md‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,11 @@ This script runs:
4040

4141
| Example | What you get back |
4242
|---------|-------------------|
43-
| Chat | `ChatCompletion` with `.text` and optional `.usage` |
44-
| Streaming | Token deltas printed live, then full text via `.collect()` |
43+
| Chat | API JSON `dict` (`success`, `message`, `data`) |
44+
| Streaming | Token deltas printed live, then full dict via `.collect()` |
4545
| Embeddings | `EmbeddingResponse` with `.embeddings` (list of vectors) |
4646
| Models | `ModelList` with `.models` |
4747
| Error handling | Catches `AuthenticationError`, `RateLimitError`, etc. |
48-
| Raw JSON | Plain `dict` when you pass `raw=True` |
4948
| Async chat | Same as chat, using `AsyncPawaAI` |
5049

5150
## 4. Minimal chat example
@@ -65,7 +64,7 @@ response = client.chat.create(
6564
],
6665
)
6766

68-
print(response.text)
67+
print(response["data"]["request"][0]["message"]["content"])
6968
```
7069

7170
## 5. More capabilities

‎examples/getting_started.py‎

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def require_api_key() -> str:
2929

3030

3131
def example_chat() -> None:
32-
"""Basic chat completion — returns a typed ChatCompletion response."""
32+
"""Basic chat completion — returns the API JSON as a dict."""
3333
from pawa_ai import PawaAI
3434

3535
client = PawaAI()
@@ -56,12 +56,15 @@ def example_chat() -> None:
5656
stream=False,
5757
)
5858

59+
reply = response["data"]["request"][0]["message"]["content"]
60+
usage = response["data"].get("usage") or {}
61+
5962
print("=== Chat completion ===")
60-
print(f"Success: {response.success}")
61-
print(f"Model: {response.model}")
62-
print(f"Reply: {response.text}")
63-
if response.usage:
64-
print(f"Tokens: in={response.usage.tokens_in}, out={response.usage.tokens_out}")
63+
print(f"Success: {response['success']}")
64+
print(f"Model: {response['data'].get('model')}")
65+
print(f"Reply: {reply}")
66+
if usage:
67+
print(f"Tokens: in={usage.get('tokens_in')}, out={usage.get('tokens_out')}")
6568
print()
6669

6770

@@ -88,7 +91,9 @@ def example_streaming() -> None:
8891
print(delta, end="", flush=True)
8992

9093
print()
91-
print(f"Collected: {stream.collect().text[:80]}...")
94+
collected = stream.collect()
95+
reply = collected["data"]["request"][0]["message"]["content"]
96+
print(f"Collected: {reply[:80]}...")
9297
print()
9398

9499

@@ -162,27 +167,7 @@ async def example_async_chat() -> None:
162167
)
163168

164169
print("=== Async chat ===")
165-
print(f"Reply: {response.text}")
166-
print()
167-
168-
169-
def example_raw_response() -> None:
170-
"""Get the original JSON dict instead of typed models."""
171-
from pawa_ai import PawaAI
172-
173-
client = PawaAI()
174-
175-
payload = client.chat.create(
176-
model="pawa-v1-ember-20240924",
177-
messages=[
178-
{"role": "user", "content": [{"type": "text", "text": "Say hi in Swahili."}]}
179-
],
180-
raw=True,
181-
)
182-
183-
print("=== Raw JSON response ===")
184-
print(f"Keys: {list(payload.keys())}")
185-
print(f"Message field: {payload.get('message')}")
170+
print(f"Reply: {response['data']['request'][0]['message']['content']}")
186171
print()
187172

188173

@@ -196,7 +181,6 @@ def main() -> None:
196181
example_embeddings()
197182
example_list_models()
198183
example_error_handling()
199-
example_raw_response()
200184

201185
asyncio.run(example_async_chat())
202186

‎src/pawa_ai/_streaming.py‎

Lines changed: 29 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
from __future__ import annotations
22

33
from collections.abc import AsyncIterator, Iterator
4+
from typing import Any
45

56
from pawa_ai._http import AsyncStream, Stream
6-
from pawa_ai.models.chat import ChatCompletion, ChatStreamChunk
7+
from pawa_ai.models.chat import ChatStreamChunk
8+
9+
10+
def _collected_payload(text: str) -> dict[str, Any]:
11+
return {
12+
"success": True,
13+
"message": "Stream collected",
14+
"data": {
15+
"request": [
16+
{
17+
"finish_reason": "stop",
18+
"message": {"role": "assistant", "content": text},
19+
}
20+
],
21+
"created": "",
22+
"model": "",
23+
"object": "chat.request",
24+
},
25+
}
726

827

928
class ChatCompletionStream:
@@ -26,8 +45,8 @@ def __exit__(self, *_: object) -> None:
2645

2746
def chunks(self) -> Iterator[ChatStreamChunk]:
2847
"""Yield typed stream chunks."""
29-
for raw in self._stream:
30-
yield ChatStreamChunk.from_dict(raw)
48+
for payload in self._stream:
49+
yield ChatStreamChunk.from_dict(payload)
3150

3251
def text_deltas(self) -> Iterator[str]:
3352
"""Yield only the text delta from each chunk."""
@@ -39,26 +58,9 @@ def collect_text(self) -> str:
3958
"""Collect all text deltas into a single string."""
4059
return "".join(self.text_deltas())
4160

42-
def collect(self) -> ChatCompletion:
43-
"""Build a :class:`ChatCompletion` from the full streamed text."""
44-
text = self.collect_text()
45-
return ChatCompletion.from_dict(
46-
{
47-
"success": True,
48-
"message": "Stream collected",
49-
"data": {
50-
"request": [
51-
{
52-
"finish_reason": "stop",
53-
"message": {"role": "assistant", "content": text},
54-
}
55-
],
56-
"created": "",
57-
"model": "",
58-
"object": "chat.request",
59-
},
60-
}
61-
)
61+
def collect(self) -> dict[str, Any]:
62+
"""Build an API-shaped dict from the full streamed text."""
63+
return _collected_payload(self.collect_text())
6264

6365
def close(self) -> None:
6466
self._stream.close()
@@ -74,8 +76,8 @@ def __aiter__(self) -> AsyncIterator[ChatStreamChunk]:
7476
return self.chunks()
7577

7678
async def chunks(self) -> AsyncIterator[ChatStreamChunk]:
77-
async for raw in self._stream:
78-
yield ChatStreamChunk.from_dict(raw)
79+
async for payload in self._stream:
80+
yield ChatStreamChunk.from_dict(payload)
7981

8082
async def text_deltas(self) -> AsyncIterator[str]:
8183
async for chunk in self.chunks():
@@ -88,25 +90,8 @@ async def collect_text(self) -> str:
8890
parts.append(delta)
8991
return "".join(parts)
9092

91-
async def collect(self) -> ChatCompletion:
92-
text = await self.collect_text()
93-
return ChatCompletion.from_dict(
94-
{
95-
"success": True,
96-
"message": "Stream collected",
97-
"data": {
98-
"request": [
99-
{
100-
"finish_reason": "stop",
101-
"message": {"role": "assistant", "content": text},
102-
}
103-
],
104-
"created": "",
105-
"model": "",
106-
"object": "chat.request",
107-
},
108-
}
109-
)
93+
async def collect(self) -> dict[str, Any]:
94+
return _collected_payload(await self.collect_text())
11095

11196
async def close(self) -> None:
11297
await self._stream.close()

‎src/pawa_ai/models/chat.py‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from dataclasses import dataclass, field
3+
from dataclasses import dataclass
44
from typing import Any
55

66

@@ -72,7 +72,6 @@ class ChatCompletion:
7272
object: str
7373
choices: list[ChatChoice]
7474
usage: Usage | None = None
75-
raw: dict[str, Any] = field(repr=False, default_factory=dict)
7675

7776
@classmethod
7877
def from_dict(cls, payload: dict[str, Any]) -> ChatCompletion:
@@ -87,7 +86,6 @@ def from_dict(cls, payload: dict[str, Any]) -> ChatCompletion:
8786
object=str(data.get("object", "")),
8887
choices=choices,
8988
usage=Usage.from_dict(data.get("usage")),
90-
raw=payload,
9189
)
9290

9391
@property
@@ -105,7 +103,6 @@ class ChatStreamChunk:
105103
message: str
106104
delta: str
107105
role: str | None = None
108-
raw: dict[str, Any] = field(repr=False, default_factory=dict)
109106

110107
@classmethod
111108
def from_dict(cls, payload: dict[str, Any]) -> ChatStreamChunk:
@@ -118,5 +115,4 @@ def from_dict(cls, payload: dict[str, Any]) -> ChatStreamChunk:
118115
message=str(payload.get("message", "")),
119116
delta=delta,
120117
role=message_data.get("role"),
121-
raw=payload,
122118
)

‎src/pawa_ai/resources/agents.py‎

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
from typing import TYPE_CHECKING, Any
44

5-
from pawa_ai._http import Stream, raise_for_status
5+
from pawa_ai._http import raise_for_status
66
from pawa_ai._streaming import AsyncChatCompletionStream, ChatCompletionStream
7-
from pawa_ai.models.chat import ChatCompletion
87

98
if TYPE_CHECKING:
109
from pawa_ai._client import AsyncPawaAI, PawaAI
@@ -47,23 +46,17 @@ def __init__(self, client: PawaAI) -> None:
4746

4847
def create(
4948
self,
50-
*,
51-
raw: bool = False,
5249
**params: Any,
53-
) -> ChatCompletion | ChatCompletionStream | Stream | dict[str, Any]:
50+
) -> dict[str, Any] | ChatCompletionStream:
5451
stream = bool(params.get("stream"))
5552
response = self._client._post("/agents/chat/request", json=params)
5653
if stream:
57-
base_stream = Stream(response)
58-
if raw:
59-
return base_stream
60-
return ChatCompletionStream(base_stream)
54+
from pawa_ai._http import Stream
55+
56+
return ChatCompletionStream(Stream(response))
6157

6258
raise_for_status(response)
63-
payload = response.json()
64-
if raw:
65-
return payload
66-
return ChatCompletion.from_dict(payload)
59+
return response.json()
6760

6861

6962
class AsyncAgentsResource:
@@ -103,22 +96,14 @@ def __init__(self, client: AsyncPawaAI) -> None:
10396

10497
async def create(
10598
self,
106-
*,
107-
raw: bool = False,
10899
**params: Any,
109-
) -> ChatCompletion | AsyncChatCompletionStream | dict[str, Any]:
100+
) -> dict[str, Any] | AsyncChatCompletionStream:
110101
stream = bool(params.get("stream"))
111102
response = await self._client._post("/agents/chat/request", json=params)
112103
if stream:
113104
from pawa_ai._http import AsyncStream
114105

115-
base_stream = AsyncStream(response)
116-
if raw:
117-
return base_stream
118-
return AsyncChatCompletionStream(base_stream)
106+
return AsyncChatCompletionStream(AsyncStream(response))
119107

120108
raise_for_status(response)
121-
payload = response.json()
122-
if raw:
123-
return payload
124-
return ChatCompletion.from_dict(payload)
109+
return response.json()

0 commit comments

Comments
 (0)