Skip to content

Commit b9b3384

Browse files
committed
add stream
1 parent 480270f commit b9b3384

3 files changed

Lines changed: 207 additions & 25 deletions

File tree

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,34 @@ if response.choices[0].message.get("tool_calls"):
7171
print(response.choices[0].message["tool_calls"])
7272
```
7373

74+
### Streaming
75+
76+
```python
77+
for chunk in edgee.stream(
78+
model="gpt-4o",
79+
input="Tell me a story",
80+
):
81+
if chunk.choices[0].delta.content:
82+
print(chunk.choices[0].delta.content, end="", flush=True)
83+
```
84+
85+
### Streaming with Messages
86+
87+
```python
88+
for chunk in edgee.stream(
89+
model="gpt-4o",
90+
input={
91+
"messages": [
92+
{"role": "system", "content": "You are a helpful assistant."},
93+
{"role": "user", "content": "Hello!"},
94+
],
95+
},
96+
):
97+
delta = chunk.choices[0].delta
98+
if delta.content:
99+
print(delta.content, end="", flush=True)
100+
```
101+
74102
## Response
75103

76104
```python
@@ -92,4 +120,24 @@ class Usage:
92120
total_tokens: int
93121
```
94122

123+
### Streaming Response
124+
125+
```python
126+
@dataclass
127+
class StreamChunk:
128+
choices: list[StreamChoice]
129+
130+
@dataclass
131+
class StreamChoice:
132+
index: int
133+
delta: StreamDelta
134+
finish_reason: str | None
135+
136+
@dataclass
137+
class StreamDelta:
138+
role: str | None # Only present in first chunk
139+
content: str | None
140+
tool_calls: list[dict] | None
141+
```
142+
95143
To learn more about this SDK, please refer to the [dedicated documentation](https://www.edgee.cloud/docs/sdk/python).

edgee/__init__.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,25 @@ class SendResponse:
6767
usage: Usage | None = None
6868

6969

70+
@dataclass
71+
class StreamDelta:
72+
role: str | None = None
73+
content: str | None = None
74+
tool_calls: list[dict] | None = None
75+
76+
77+
@dataclass
78+
class StreamChoice:
79+
index: int
80+
delta: StreamDelta
81+
finish_reason: str | None = None
82+
83+
84+
@dataclass
85+
class StreamChunk:
86+
choices: list[StreamChoice]
87+
88+
7089
@dataclass
7190
class EdgeeConfig:
7291
api_key: str | None = None
@@ -159,3 +178,87 @@ def send(
159178
)
160179

161180
return SendResponse(choices=choices, usage=usage)
181+
182+
def stream(
183+
self,
184+
model: str,
185+
input: str | InputObject | dict,
186+
):
187+
"""Stream a completion request from the Edgee AI Gateway.
188+
189+
Yields StreamChunk objects as they arrive from the API.
190+
"""
191+
192+
if isinstance(input, str):
193+
messages = [{"role": "user", "content": input}]
194+
tools = None
195+
tool_choice = None
196+
elif isinstance(input, InputObject):
197+
messages = input.messages
198+
tools = input.tools
199+
tool_choice = input.tool_choice
200+
else:
201+
messages = input.get("messages", [])
202+
tools = input.get("tools")
203+
tool_choice = input.get("tool_choice")
204+
205+
body: dict = {"model": model, "messages": messages, "stream": True}
206+
if tools:
207+
body["tools"] = tools
208+
if tool_choice:
209+
body["tool_choice"] = tool_choice
210+
211+
request = Request(
212+
f"{self.base_url}{API_ENDPOINT}",
213+
data=json.dumps(body).encode("utf-8"),
214+
headers={
215+
"Content-Type": "application/json",
216+
"Authorization": f"Bearer {self.api_key}",
217+
},
218+
method="POST",
219+
)
220+
221+
try:
222+
with urlopen(request) as response:
223+
# Read and parse SSE stream
224+
buffer = ""
225+
for line in response:
226+
decoded_line = line.decode("utf-8")
227+
228+
if decoded_line.strip() == "":
229+
continue
230+
231+
if decoded_line.startswith("data: "):
232+
data_str = decoded_line[6:].strip()
233+
234+
# Check for stream end signal
235+
if data_str == "[DONE]":
236+
break
237+
238+
try:
239+
data = json.loads(data_str)
240+
241+
# Parse choices
242+
choices = []
243+
for c in data.get("choices", []):
244+
delta_data = c.get("delta", {})
245+
delta = StreamDelta(
246+
role=delta_data.get("role"),
247+
content=delta_data.get("content"),
248+
tool_calls=delta_data.get("tool_calls"),
249+
)
250+
choice = StreamChoice(
251+
index=c["index"],
252+
delta=delta,
253+
finish_reason=c.get("finish_reason"),
254+
)
255+
choices.append(choice)
256+
257+
yield StreamChunk(choices=choices)
258+
except json.JSONDecodeError:
259+
# Skip malformed JSON
260+
continue
261+
262+
except HTTPError as e:
263+
error_body = e.read().decode("utf-8")
264+
raise RuntimeError(f"API error {e.code}: {error_body}") from e

example/test.py

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# Test 1: Simple string input
1414
print("Test 1: Simple string input")
1515
response1 = edgee.send(
16-
model="gpt-4o",
16+
model="mistral/mistral-small-latest",
1717
input="What is the capital of France?",
1818
)
1919
print(f"Content: {response1.choices[0].message['content']}")
@@ -23,7 +23,7 @@
2323
# Test 2: Full input object with messages
2424
print("Test 2: Full input object with messages")
2525
response2 = edgee.send(
26-
model="gpt-4o",
26+
model="mistral/mistral-small-latest",
2727
input={
2828
"messages": [
2929
{"role": "system", "content": "You are a helpful assistant."},
@@ -35,29 +35,60 @@
3535
print()
3636

3737
# Test 3: With tools
38-
print("Test 3: With tools")
39-
response3 = edgee.send(
40-
model="gpt-4o",
38+
#print("Test 3: With tools")
39+
#response3 = edgee.send(
40+
# model="gpt-4o",
41+
# input={
42+
# "messages": [{"role": "user", "content": "What is the weather in Paris?"}],
43+
# "tools": [
44+
# {
45+
# "type": "function",
46+
# "function": {
47+
# "name": "get_weather",
48+
# "description": "Get the current weather for a location",
49+
# "parameters": {
50+
# "type": "object",
51+
# "properties": {
52+
# "location": {"type": "string", "description": "City name"},
53+
# },
54+
# "required": ["location"],
55+
# },
56+
# },
57+
# },
58+
# ],
59+
# "tool_choice": "auto",
60+
# },
61+
#)
62+
#print(f"Content: {response3.choices[0].message.get('content')}")
63+
#print(f"Tool calls: {response3.choices[0].message.get('tool_calls')}")
64+
#print()
65+
66+
# Test 4: Streaming
67+
print("Test 4: Streaming")
68+
for chunk in edgee.stream(
69+
model="mistral/mistral-small-latest",
70+
input="Tell me a short story about a robot",
71+
):
72+
if chunk.choices[0].delta.content:
73+
print(chunk.choices[0].delta.content, end="", flush=True)
74+
print("\n")
75+
76+
# Test 5: Streaming with messages
77+
print("Test 5: Streaming with system message")
78+
for chunk in edgee.stream(
79+
model="mistral/mistral-small-latest",
4180
input={
42-
"messages": [{"role": "user", "content": "What is the weather in Paris?"}],
43-
"tools": [
44-
{
45-
"type": "function",
46-
"function": {
47-
"name": "get_weather",
48-
"description": "Get the current weather for a location",
49-
"parameters": {
50-
"type": "object",
51-
"properties": {
52-
"location": {"type": "string", "description": "City name"},
53-
},
54-
"required": ["location"],
55-
},
56-
},
57-
},
81+
"messages": [
82+
{"role": "system", "content": "You are a poetic assistant. Respond in rhyme."},
83+
{"role": "user", "content": "What is Python?"},
5884
],
59-
"tool_choice": "auto",
6085
},
61-
)
62-
print(f"Content: {response3.choices[0].message.get('content')}")
63-
print(f"Tool calls: {response3.choices[0].message.get('tool_calls')}")
86+
):
87+
delta = chunk.choices[0].delta
88+
if delta.role:
89+
print(f"[Role: {delta.role}]")
90+
if delta.content:
91+
print(delta.content, end="", flush=True)
92+
if chunk.choices[0].finish_reason:
93+
print(f"\n[Finished: {chunk.choices[0].finish_reason}]")
94+
print()

0 commit comments

Comments
 (0)