courier-encode 0.1.0__tar.gz → 0.1.2__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.
- courier_encode-0.1.2/.idea/workspace.xml +83 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/PKG-INFO +2 -2
- {courier_encode-0.1.0 → courier_encode-0.1.2}/docs.md +73 -5
- {courier_encode-0.1.0 → courier_encode-0.1.2}/pyproject.toml +2 -2
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/__init__.py +2 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/_http.py +11 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/_streaming.py +43 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/messages.py +12 -5
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/relay.py +693 -27
- courier_encode-0.1.2/tests/test_streaming.py +338 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/uv.lock +1 -1
- courier_encode-0.1.0/.idea/workspace.xml +0 -122
- courier_encode-0.1.0/tests/test_streaming.py +0 -68
- {courier_encode-0.1.0 → courier_encode-0.1.2}/.gitignore +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/.idea/.gitignore +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/.idea/encode.iml +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/.idea/inspectionProfiles/Project_Default.xml +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/.idea/misc.xml +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/.idea/modules.xml +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/.idea/vcs.xml +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/LICENSE +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/README.md +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/examples/basic_chat.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/examples/intercept.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/examples/responses_endpoint.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/examples/structured_output.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/examples/tools_loop.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/examples/whisper_transcribe.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/_config.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/_schema.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/_version.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/client.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/errors.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/py.typed +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/responses.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/session.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/tools.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/src/encode/whisper.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/tests/__init__.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/tests/conftest.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/tests/test_async.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/tests/test_errors.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/tests/test_messages.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/tests/test_messages_obj.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/tests/test_relay_chat.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/tests/test_relay_responses.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/tests/test_response_format.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/tests/test_session.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/tests/test_tool_loop.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/tests/test_tools_schema.py +0 -0
- {courier_encode-0.1.0 → courier_encode-0.1.2}/tests/test_whisper.py +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="AutoImportSettings">
|
|
4
|
+
<option name="autoReloadType" value="ALL" />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="ChangeListManager">
|
|
7
|
+
<list default="true" id="9259bfb5-884c-4336-83fd-f640b45e88c5" name="Changes" comment="">
|
|
8
|
+
<change beforePath="$PROJECT_DIR$/pyproject.toml" beforeDir="false" afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
|
|
9
|
+
<change beforePath="$PROJECT_DIR$/src/encode/_http.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/encode/_http.py" afterDir="false" />
|
|
10
|
+
<change beforePath="$PROJECT_DIR$/src/encode/relay.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/encode/relay.py" afterDir="false" />
|
|
11
|
+
<change beforePath="$PROJECT_DIR$/tests/test_streaming.py" beforeDir="false" afterPath="$PROJECT_DIR$/tests/test_streaming.py" afterDir="false" />
|
|
12
|
+
<change beforePath="$PROJECT_DIR$/uv.lock" beforeDir="false" afterPath="$PROJECT_DIR$/uv.lock" afterDir="false" />
|
|
13
|
+
</list>
|
|
14
|
+
<option name="SHOW_DIALOG" value="false" />
|
|
15
|
+
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
16
|
+
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
17
|
+
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
18
|
+
</component>
|
|
19
|
+
<component name="EmbeddingIndexingInfo">
|
|
20
|
+
<option name="cachedIndexableFilesCount" value="3" />
|
|
21
|
+
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
|
22
|
+
</component>
|
|
23
|
+
<component name="Git.Settings">
|
|
24
|
+
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
25
|
+
</component>
|
|
26
|
+
<component name="McpProjectServerCommands">
|
|
27
|
+
<commands />
|
|
28
|
+
<urls />
|
|
29
|
+
</component>
|
|
30
|
+
<component name="ProjectColorInfo"><![CDATA[{
|
|
31
|
+
"associatedIndex": 8,
|
|
32
|
+
"fromUser": false
|
|
33
|
+
}]]></component>
|
|
34
|
+
<component name="ProjectId" id="3DRfPMmybWPzhvemSx3K8mYqPeH" />
|
|
35
|
+
<component name="ProjectViewState">
|
|
36
|
+
<option name="hideEmptyMiddlePackages" value="true" />
|
|
37
|
+
<option name="showLibraryContents" value="true" />
|
|
38
|
+
</component>
|
|
39
|
+
<component name="PropertiesComponent"><![CDATA[{
|
|
40
|
+
"keyToString": {
|
|
41
|
+
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
|
42
|
+
"RunOnceActivity.MCP Project settings loaded": "true",
|
|
43
|
+
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
44
|
+
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
|
45
|
+
"RunOnceActivity.git.unshallow": "true",
|
|
46
|
+
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
|
47
|
+
"codeWithMe.voiceChat.enabledByDefault": "false",
|
|
48
|
+
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
|
49
|
+
"git-widget-placeholder": "main",
|
|
50
|
+
"junie.onboarding.icon.badge.shown": "true",
|
|
51
|
+
"last_opened_file_path": "/Users/jacksonoaks/Documents/business/recursion_ai/encode",
|
|
52
|
+
"node.js.detected.package.eslint": "true",
|
|
53
|
+
"node.js.detected.package.tslint": "true",
|
|
54
|
+
"node.js.selected.package.eslint": "(autodetect)",
|
|
55
|
+
"node.js.selected.package.tslint": "(autodetect)",
|
|
56
|
+
"nodejs_package_manager_path": "npm",
|
|
57
|
+
"to.speed.mode.migration.done": "true",
|
|
58
|
+
"vue.rearranger.settings.migration": "true"
|
|
59
|
+
}
|
|
60
|
+
}]]></component>
|
|
61
|
+
<component name="SharedIndexes">
|
|
62
|
+
<attachedChunks>
|
|
63
|
+
<set>
|
|
64
|
+
<option value="bundled-js-predefined-d6986cc7102b-3bd3a6803838-JavaScript-PY-261.22158.340" />
|
|
65
|
+
<option value="bundled-python-sdk-b63d5a1f7c97-b61e75351b1f-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-261.22158.340" />
|
|
66
|
+
</set>
|
|
67
|
+
</attachedChunks>
|
|
68
|
+
</component>
|
|
69
|
+
<component name="TaskManager">
|
|
70
|
+
<task active="true" id="Default" summary="Default task">
|
|
71
|
+
<changelist id="9259bfb5-884c-4336-83fd-f640b45e88c5" name="Changes" comment="" />
|
|
72
|
+
<created>1778249777914</created>
|
|
73
|
+
<option name="number" value="Default" />
|
|
74
|
+
<option name="presentableId" value="Default" />
|
|
75
|
+
<updated>1778249777914</updated>
|
|
76
|
+
<workItem from="1778249778942" duration="11261000" />
|
|
77
|
+
</task>
|
|
78
|
+
<servers />
|
|
79
|
+
</component>
|
|
80
|
+
<component name="TypeScriptGeneratedFilesManager">
|
|
81
|
+
<option name="version" value="3" />
|
|
82
|
+
</component>
|
|
83
|
+
</project>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: courier-encode
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Python SDK for OpenAI-compatible inference endpoints (Courier and friends) with auto tool-call loops, structured outputs, and Whisper.
|
|
5
5
|
Project-URL: Homepage, https://getcourier.ai
|
|
6
6
|
Project-URL: Documentation, https://getcourier.ai/docs
|
|
@@ -211,7 +211,7 @@ License-File: LICENSE
|
|
|
211
211
|
Keywords: agents,courier,llm,openai,sdk,tool-calling,whisper
|
|
212
212
|
Classifier: Development Status :: 3 - Alpha
|
|
213
213
|
Classifier: Intended Audience :: Developers
|
|
214
|
-
Classifier: License :: OSI Approved ::
|
|
214
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
215
215
|
Classifier: Programming Language :: Python :: 3
|
|
216
216
|
Classifier: Programming Language :: Python :: 3.10
|
|
217
217
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -232,7 +232,7 @@ print(out.tool_calls[0].name) # "get_weather"
|
|
|
232
232
|
print(out.tool_calls[0].result) # {"city": "Denver", "temp_f": 72}
|
|
233
233
|
```
|
|
234
234
|
|
|
235
|
-
The loop runs until the model stops calling tools
|
|
235
|
+
The loop runs until the model stops calling tools. There is no cap by default — pass `max_tool_iterations=N` if you want to bail on a runaway model. See [Capping iterations](#capping-iterations-optional) below.
|
|
236
236
|
|
|
237
237
|
### What the SDK does for you
|
|
238
238
|
|
|
@@ -406,7 +406,7 @@ The tool loop and intercept work identically across both endpoints — `function
|
|
|
406
406
|
|
|
407
407
|
## Streaming
|
|
408
408
|
|
|
409
|
-
Set `stream=True` and iterate the handle
|
|
409
|
+
Set `stream=True` and iterate the handle. The handle yields `StreamEvent` objects — parsed Python values, not raw SSE bytes. The upstream HTTP connection is held open with `httpx.stream()`, so backpressure works end-to-end.
|
|
410
410
|
|
|
411
411
|
```python
|
|
412
412
|
handle = encode.relay(
|
|
@@ -419,14 +419,82 @@ for event in handle:
|
|
|
419
419
|
print(event.data, end="", flush=True)
|
|
420
420
|
```
|
|
421
421
|
|
|
422
|
-
|
|
422
|
+
### `StreamEvent`
|
|
423
|
+
|
|
424
|
+
Every event carries three fields:
|
|
425
|
+
|
|
426
|
+
| field | type | meaning |
|
|
427
|
+
| ------- | ---------------- | ----------------------------------------------------------------------- |
|
|
428
|
+
| `type` | `str` | event kind (see tables below) |
|
|
429
|
+
| `data` | `Any` | the parsed payload — a `str` for text deltas, `dict`/`list` otherwise |
|
|
430
|
+
| `raw` | `dict` \| `None` | the full upstream chunk as parsed JSON (re-serialize this if proxying) |
|
|
431
|
+
|
|
432
|
+
### Event types — `/v1/chat/completions`
|
|
433
|
+
|
|
434
|
+
| `event.type` | `event.data` |
|
|
435
|
+
| -------------------- | --------------------------------------------------------------------- |
|
|
436
|
+
| `content.delta` | `str` — the next token of assistant text |
|
|
437
|
+
| `tool_calls.delta` | `list[dict]` — partial tool-call fragments (raw upstream deltas) |
|
|
438
|
+
| `tool_call.start` | `{id, name, arguments: dict, iteration}` — fully assembled tool call about to run |
|
|
439
|
+
| `tool_call.result` | `{id, result, result_serialized, duration_ms, iteration}` — tool returned successfully |
|
|
440
|
+
| `tool_call.error` | `{id, error, iteration}` — tool raised; loop continues |
|
|
441
|
+
| `iteration.end` | `{iteration, had_tool_calls}` — one tool-loop iteration completed |
|
|
442
|
+
| `finish` | `str` — final finish reason (`"stop"`, `"length"`, `"tool_calls"`, …) |
|
|
443
|
+
|
|
444
|
+
The new `tool_call.*` events fire **only when `tools=` is set**. Without tools, you only see `content.delta` / `finish`. The raw `tool_calls.delta` events still fire when the model emits tool-call fragments — most chat-UI consumers can ignore them and key off `tool_call.start` / `tool_call.result` instead.
|
|
445
|
+
|
|
446
|
+
### Event types — `/v1/responses`
|
|
447
|
+
|
|
448
|
+
For the responses endpoint, upstream events are passed through with their `type` field intact (`response.output_text.delta`, `response.completed`, …) and `event.data` set to the entire parsed event dict. When `tools=` is set, encode also synthesizes `content.delta` (mapped from `response.output_text.delta`) and the same `tool_call.start` / `tool_call.result` / `tool_call.error` / `iteration.end` events as chat, so a single consumer can handle both endpoints uniformly.
|
|
423
449
|
|
|
424
450
|
```python
|
|
425
451
|
for event in encode.relay(model="m", input="hi", stream=True):
|
|
426
452
|
print(event.type, event.data)
|
|
427
453
|
```
|
|
428
454
|
|
|
429
|
-
|
|
455
|
+
### Streaming with tools (auto-loop)
|
|
456
|
+
|
|
457
|
+
Pass `tools=` and `stream=True` together. The SDK runs the same auto-tool-loop as `stream=False` but yields events as the iteration proceeds — content tokens forward to the consumer in real time, and each tool dispatch fires `tool_call.start` / `tool_call.result` (or `.error`) events the consumer can render in a chat UI.
|
|
458
|
+
|
|
459
|
+
```python
|
|
460
|
+
def get_weather(city: str) -> dict:
|
|
461
|
+
"""Get current weather by city."""
|
|
462
|
+
return {"city": city, "temp_f": 72}
|
|
463
|
+
|
|
464
|
+
for ev in encode.relay(
|
|
465
|
+
model="gpt-4o-mini",
|
|
466
|
+
messages=[{"role": "user", "content": "What's the weather in Denver?"}],
|
|
467
|
+
tools=[get_weather],
|
|
468
|
+
stream=True,
|
|
469
|
+
):
|
|
470
|
+
if ev.type == "content.delta":
|
|
471
|
+
print(ev.data, end="", flush=True)
|
|
472
|
+
elif ev.type == "tool_call.start":
|
|
473
|
+
print(f"\n[calling {ev.data['name']}({ev.data['arguments']})]")
|
|
474
|
+
elif ev.type == "tool_call.result":
|
|
475
|
+
print(f"[result: {ev.data['result']}]")
|
|
476
|
+
elif ev.type == "tool_call.error":
|
|
477
|
+
print(f"[tool error: {ev.data['error']}]")
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
`max_tool_iterations` still works — passing a cap raises `MaxToolIterationsError` (with `.partial` carrying the streamed-so-far state) the moment the cap is exceeded.
|
|
481
|
+
|
|
482
|
+
`Messages` containers passed as `messages=` are mutated when the stream finishes, just like the non-stream path. If the consumer abandons the iterator early (breaks out of the loop), the container is **not** updated — drain the iterator if you want the absorption.
|
|
483
|
+
|
|
484
|
+
### Async streaming
|
|
485
|
+
|
|
486
|
+
Use `relay_async` with `async for`:
|
|
487
|
+
|
|
488
|
+
```python
|
|
489
|
+
handle = encode.relay_async(model="m", messages=[...], tools=[get_weather], stream=True)
|
|
490
|
+
async for event in handle:
|
|
491
|
+
if event.type == "content.delta":
|
|
492
|
+
print(event.data, end="", flush=True)
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Restrictions
|
|
496
|
+
|
|
497
|
+
- **`response_format` is not supported when streaming.** Combining the two raises `ValueError` immediately — structured output isn't meaningful mid-stream.
|
|
430
498
|
|
|
431
499
|
---
|
|
432
500
|
|
|
@@ -691,7 +759,7 @@ async for event in AsyncRelayHandle: ...
|
|
|
691
759
|
# Models
|
|
692
760
|
encode.Message, Messages, Conversation, TextContent, ImageContent, AudioContent
|
|
693
761
|
encode.RelayResponse, WhisperResponse, ToolCallRecord, AssistantTurn, Usage
|
|
694
|
-
encode.InterceptEvent
|
|
762
|
+
encode.InterceptEvent, StreamEvent
|
|
695
763
|
|
|
696
764
|
# Errors (all inherit CourierError)
|
|
697
765
|
encode.AuthError, InvalidRequestError, InvalidToolCallError, InvalidToolChoiceError,
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "courier-encode"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Python SDK for OpenAI-compatible inference endpoints (Courier and friends) with auto tool-call loops, structured outputs, and Whisper."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -14,7 +14,7 @@ keywords = ["openai", "courier", "llm", "sdk", "agents", "tool-calling", "whispe
|
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Development Status :: 3 - Alpha",
|
|
16
16
|
"Intended Audience :: Developers",
|
|
17
|
-
"License :: OSI Approved ::
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
18
|
"Programming Language :: Python :: 3",
|
|
19
19
|
"Programming Language :: Python :: 3.10",
|
|
20
20
|
"Programming Language :: Python :: 3.11",
|
|
@@ -22,6 +22,7 @@ from . import _config
|
|
|
22
22
|
# Auto-load .env once on import (opt-out via ENCODE_DISABLE_DOTENV=1).
|
|
23
23
|
_config.load_dotenv_once()
|
|
24
24
|
|
|
25
|
+
from ._streaming import StreamEvent
|
|
25
26
|
from ._version import __version__
|
|
26
27
|
from .client import AsyncClient, Client
|
|
27
28
|
from .errors import (
|
|
@@ -77,6 +78,7 @@ __all__ = [
|
|
|
77
78
|
"RelayHandle",
|
|
78
79
|
"AsyncRelayHandle",
|
|
79
80
|
"InterceptEvent",
|
|
81
|
+
"StreamEvent",
|
|
80
82
|
"whisper",
|
|
81
83
|
"whisper_async",
|
|
82
84
|
"Message",
|
|
@@ -47,6 +47,17 @@ def parse_body(resp: httpx.Response) -> Any:
|
|
|
47
47
|
def raise_for_status(resp: httpx.Response) -> None:
|
|
48
48
|
if resp.is_success:
|
|
49
49
|
return
|
|
50
|
+
# Idempotent for already-buffered responses; required for sync streaming.
|
|
51
|
+
resp.read()
|
|
52
|
+
body = parse_body(resp)
|
|
53
|
+
raise errors.from_envelope(body, status=resp.status_code)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def araise_for_status(resp: httpx.Response) -> None:
|
|
57
|
+
if resp.is_success:
|
|
58
|
+
return
|
|
59
|
+
# Required for responses obtained via async_client.stream(...).
|
|
60
|
+
await resp.aread()
|
|
50
61
|
body = parse_body(resp)
|
|
51
62
|
raise errors.from_envelope(body, status=resp.status_code)
|
|
52
63
|
|
|
@@ -95,3 +95,46 @@ async def aiter_responses(resp: httpx.Response) -> AsyncIterator[StreamEvent]:
|
|
|
95
95
|
continue
|
|
96
96
|
etype = event.get("type", "unknown")
|
|
97
97
|
yield StreamEvent(type=etype, data=event, raw=event)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# Chat tool-call delta accumulator
|
|
102
|
+
#
|
|
103
|
+
# Chat completions emits tool calls as a sequence of partial deltas keyed by
|
|
104
|
+
# `index`. Each chunk may contribute id/type/function.name on first arrival
|
|
105
|
+
# and append more bytes to function.arguments on subsequent chunks. The relay
|
|
106
|
+
# tool-loop needs the assembled list at end-of-stream to dispatch tools.
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def accumulate_chat_tool_calls(
|
|
111
|
+
buf: dict[int, dict[str, Any]],
|
|
112
|
+
deltas: list[dict[str, Any]] | None,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Merge a chunk's `tool_calls` delta list into ``buf`` keyed by index.
|
|
115
|
+
|
|
116
|
+
Mutates ``buf`` in place. Safe to call with a ``None`` or empty deltas list.
|
|
117
|
+
"""
|
|
118
|
+
if not deltas:
|
|
119
|
+
return
|
|
120
|
+
for d in deltas:
|
|
121
|
+
idx = d.get("index", 0)
|
|
122
|
+
slot = buf.setdefault(
|
|
123
|
+
idx, {"id": "", "type": "function", "function": {"name": "", "arguments": ""}}
|
|
124
|
+
)
|
|
125
|
+
if d.get("id"):
|
|
126
|
+
slot["id"] = d["id"]
|
|
127
|
+
if d.get("type"):
|
|
128
|
+
slot["type"] = d["type"]
|
|
129
|
+
fn = d.get("function") or {}
|
|
130
|
+
if fn.get("name"):
|
|
131
|
+
slot["function"]["name"] = fn["name"]
|
|
132
|
+
if fn.get("arguments"):
|
|
133
|
+
slot["function"]["arguments"] += fn["arguments"]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def finalize_chat_tool_calls(
|
|
137
|
+
buf: dict[int, dict[str, Any]],
|
|
138
|
+
) -> list[dict[str, Any]]:
|
|
139
|
+
"""Return the buffered tool calls sorted by index, in /v1/chat shape."""
|
|
140
|
+
return [buf[k] for k in sorted(buf)]
|
|
@@ -14,7 +14,7 @@ import mimetypes
|
|
|
14
14
|
from collections.abc import Iterable, Iterator, Sequence
|
|
15
15
|
from os import PathLike
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import TYPE_CHECKING, Any, Literal
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
18
18
|
|
|
19
19
|
from pydantic import BaseModel, ConfigDict
|
|
20
20
|
|
|
@@ -193,15 +193,16 @@ def _coerce_content(content: Any) -> Any:
|
|
|
193
193
|
return content
|
|
194
194
|
|
|
195
195
|
|
|
196
|
-
class Messages:
|
|
196
|
+
class Messages(Sequence[dict[str, Any]]):
|
|
197
197
|
"""Mutable conversation container.
|
|
198
198
|
|
|
199
199
|
Pass to ``relay()`` / ``relay_async()`` as ``messages=`` and the SDK will
|
|
200
200
|
append the new turns in place after the loop completes. Plain lists work
|
|
201
201
|
too — they are not mutated.
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
Implements :class:`collections.abc.Sequence`, so it satisfies
|
|
204
|
+
``Sequence[Any]`` parameter types and supports ``len()``, iteration,
|
|
205
|
+
indexing, slicing, ``in``, ``index()``, ``count()``, etc.
|
|
205
206
|
|
|
206
207
|
Example:
|
|
207
208
|
m = (
|
|
@@ -282,7 +283,13 @@ class Messages:
|
|
|
282
283
|
def __iter__(self) -> Iterator[dict[str, Any]]:
|
|
283
284
|
return iter(self._items)
|
|
284
285
|
|
|
285
|
-
|
|
286
|
+
@overload
|
|
287
|
+
def __getitem__(self, idx: int) -> dict[str, Any]: ...
|
|
288
|
+
@overload
|
|
289
|
+
def __getitem__(self, idx: slice) -> list[dict[str, Any]]: ...
|
|
290
|
+
def __getitem__(
|
|
291
|
+
self, idx: int | slice
|
|
292
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
286
293
|
return self._items[idx]
|
|
287
294
|
|
|
288
295
|
def __bool__(self) -> bool:
|