Skip to content

Commit 1b6f8fb

Browse files
Merge pull request #26 from dedalus-labs/release-please--branches--main--changes--next
release: 0.1.0
2 parents eb5af24 + fba54e5 commit 1b6f8fb

File tree

20 files changed

+949
-164
lines changed

20 files changed

+949
-164
lines changed

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "0.0.1"
2+
".": "0.1.0"
33
}

.stats.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 12
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/dedalus-labs--inc-dash%2Fdedalus-sdk-a559d4a84f39e2e0e3708da9afe7d88870466bbab48b20582593498a74241f5a.yml
3-
openapi_spec_hash: 1f3614678b91b9838bf7fbc8d2f83fb1
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/dedalus-labs--inc-dash%2Fdedalus-sdk-256027f9a68dd74ecb9dd7878c64541d9dc9c5335990624281899c7ddce331ea.yml
3+
openapi_spec_hash: 11645039804f2325591a5363712ae158
44
config_hash: 7282c3956a1a1d9c0f957d1d5e2ee097

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## 0.1.0 (2025-11-09)
4+
5+
Full Changelog: [v0.0.1...v0.1.0](https://github.com/dedalus-labs/dedalus-sdk-python/compare/v0.0.1...v0.1.0)
6+
7+
### Features
8+
9+
* **api:** messages param nullable ([e905235](https://github.com/dedalus-labs/dedalus-sdk-python/commit/e9052357b7efa9d49b4f8b8d4c7dfc026d69414b))
10+
* flexible input params for .parse() ([b208fbe](https://github.com/dedalus-labs/dedalus-sdk-python/commit/b208fbed8300526b323ac7c935d6d50bb652f0d3))
11+
* structured outputs for tools ([b0434ca](https://github.com/dedalus-labs/dedalus-sdk-python/commit/b0434ca32e43dc5ef254e3fecb5493a2d3896384))
12+
313
## 0.0.1 (2025-11-08)
414

515
Full Changelog: [v0.1.0-alpha.10...v0.0.1](https://github.com/dedalus-labs/dedalus-sdk-python/compare/v0.1.0-alpha.10...v0.0.1)

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ client = Dedalus(
3535
)
3636

3737
completion = client.chat.completions.create(
38+
model="openai/gpt-5",
3839
messages=[
3940
{
4041
"role": "user",
4142
"content": "Hello, how are you today?",
4243
}
4344
],
44-
model="openai/gpt-5",
4545
)
4646
print(completion.id)
4747
```
@@ -69,13 +69,13 @@ client = AsyncDedalus(
6969

7070
async def main() -> None:
7171
completion = await client.chat.completions.create(
72+
model="openai/gpt-5",
7273
messages=[
7374
{
7475
"role": "user",
7576
"content": "Hello, how are you today?",
7677
}
7778
],
78-
model="openai/gpt-5",
7979
)
8080
print(completion.id)
8181

@@ -110,13 +110,13 @@ async def main() -> None:
110110
http_client=DefaultAioHttpClient(),
111111
) as client:
112112
completion = await client.chat.completions.create(
113+
model="openai/gpt-5",
113114
messages=[
114115
{
115116
"role": "user",
116117
"content": "Hello, how are you today?",
117118
}
118119
],
119-
model="openai/gpt-5",
120120
)
121121
print(completion.id)
122122

@@ -134,6 +134,8 @@ from dedalus_labs import Dedalus
134134
client = Dedalus()
135135

136136
stream = client.chat.completions.create(
137+
model="openai/gpt-5",
138+
stream=True,
137139
messages=[
138140
{
139141
"role": "system",
@@ -144,8 +146,6 @@ stream = client.chat.completions.create(
144146
"content": "What do you think of artificial intelligence?",
145147
},
146148
],
147-
model="openai/gpt-5",
148-
stream=True,
149149
)
150150
for completion in stream:
151151
print(completion.id)
@@ -159,6 +159,8 @@ from dedalus_labs import AsyncDedalus
159159
client = AsyncDedalus()
160160

161161
stream = await client.chat.completions.create(
162+
model="openai/gpt-5",
163+
stream=True,
162164
messages=[
163165
{
164166
"role": "system",
@@ -169,8 +171,6 @@ stream = await client.chat.completions.create(
169171
"content": "What do you think of artificial intelligence?",
170172
},
171173
],
172-
model="openai/gpt-5",
173-
stream=True,
174174
)
175175
async for completion in stream:
176176
print(completion.id)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "dedalus_labs"
3-
version = "0.0.1"
3+
version = "0.1.0"
44
description = "The official Python library for the Dedalus API"
55
dynamic = ["readme"]
66
license = "MIT"

src/dedalus_labs/_compat.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,20 @@ def model_parse(model: type[_ModelT], data: Any) -> _ModelT:
165165
return model.model_validate(data)
166166

167167

168+
def model_parse_json(model: type[_ModelT], data: str | bytes) -> _ModelT:
169+
"""Parse JSON string/bytes into Pydantic model."""
170+
if PYDANTIC_V1:
171+
return model.parse_raw(data) # pyright: ignore[reportDeprecated]
172+
return model.model_validate_json(data)
173+
174+
175+
def model_json_schema(model: type[pydantic.BaseModel]) -> dict[str, Any]:
176+
"""Get JSON schema from Pydantic model."""
177+
if PYDANTIC_V1:
178+
return model.schema() # pyright: ignore[reportDeprecated]
179+
return model.model_json_schema()
180+
181+
168182
# generic models
169183
if TYPE_CHECKING:
170184

src/dedalus_labs/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
__title__ = "dedalus_labs"
4-
__version__ = "0.0.1" # x-release-please-version
4+
__version__ = "0.1.0" # x-release-please-version
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from __future__ import annotations
2+
3+
from ._completions import (
4+
ResponseFormatT as ResponseFormatT,
5+
parse_chat_completion as parse_chat_completion,
6+
type_to_response_format_param as type_to_response_format_param,
7+
validate_input_tools as validate_input_tools,
8+
)
9+
10+
__all__ = [
11+
"ResponseFormatT",
12+
"parse_chat_completion",
13+
"type_to_response_format_param",
14+
"validate_input_tools",
15+
]
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from typing import TYPE_CHECKING, Any, Dict, Iterable, cast
5+
from typing_extensions import TypeVar, TypeGuard
6+
7+
import pydantic
8+
9+
from .._tools import PydanticFunctionTool
10+
from ..._types import Omit, omit
11+
from ..._utils import is_dict, is_given
12+
from ..._compat import PYDANTIC_V1, model_parse_json
13+
from ..._models import construct_type_unchecked
14+
from .._pydantic import is_basemodel_type, to_strict_json_schema, is_dataclass_like_type
15+
16+
if TYPE_CHECKING:
17+
from ...types.chat.completion_create_params import ResponseFormat as ResponseFormatParam
18+
from ...types.chat.completion import ChoiceMessageToolCallChatCompletionMessageToolCallFunction as Function
19+
20+
ResponseFormatT = TypeVar("ResponseFormatT")
21+
22+
23+
def type_to_response_format_param(
24+
response_format: type | ResponseFormatParam | Omit,
25+
) -> ResponseFormatParam | Omit:
26+
"""Convert Pydantic model to API response_format parameter."""
27+
if not is_given(response_format):
28+
return omit
29+
30+
if is_dict(response_format):
31+
return response_format
32+
33+
response_format = cast(type, response_format)
34+
35+
if is_basemodel_type(response_format):
36+
name = response_format.__name__
37+
json_schema_type = response_format
38+
elif is_dataclass_like_type(response_format):
39+
name = response_format.__name__
40+
json_schema_type = pydantic.TypeAdapter(response_format)
41+
else:
42+
raise TypeError(f"Unsupported response_format type - {response_format}")
43+
44+
return {
45+
"type": "json_schema",
46+
"json_schema": {
47+
"schema": to_strict_json_schema(json_schema_type),
48+
"name": name,
49+
"strict": True,
50+
},
51+
}
52+
53+
54+
def validate_input_tools(tools: Iterable[Dict[str, Any]] | Omit = omit) -> Iterable[Dict[str, Any]] | Omit:
55+
"""Validate tools for strict parsing support."""
56+
if not is_given(tools):
57+
return omit
58+
59+
for tool in tools:
60+
if tool.get("type") != "function":
61+
raise ValueError(f"Only function tools support auto-parsing; got {tool.get('type')}")
62+
63+
strict = tool.get("function", {}).get("strict")
64+
if strict is not True:
65+
name = tool.get("function", {}).get("name", "unknown")
66+
raise ValueError(f"Tool '{name}' is not strict. Only strict function tools can be auto-parsed")
67+
68+
return cast(Iterable[Dict[str, Any]], tools)
69+
70+
71+
def parse_chat_completion(
72+
*,
73+
response_format: type[ResponseFormatT] | ResponseFormatParam | Omit,
74+
chat_completion: Any,
75+
input_tools: Iterable[Dict[str, Any]] | Omit = omit,
76+
) -> Any:
77+
"""Parse completion: response content and tool call arguments into Pydantic models."""
78+
from ...types.chat.parsed_chat_completion import (
79+
ParsedChatCompletion,
80+
ParsedChoice,
81+
ParsedChatCompletionMessage,
82+
)
83+
from ...types.chat.parsed_function_tool_call import ParsedFunctionToolCall, ParsedFunction
84+
85+
tool_list = list(input_tools) if is_given(input_tools) else []
86+
87+
choices = []
88+
for choice in chat_completion.choices:
89+
message = choice.message
90+
91+
# Parse tool calls if present
92+
tool_calls = []
93+
if hasattr(message, "tool_calls") and message.tool_calls:
94+
for tool_call in message.tool_calls:
95+
if tool_call.type == "function":
96+
parsed_args = _parse_function_tool_arguments(
97+
input_tools=tool_list, function=tool_call.function
98+
)
99+
tool_calls.append(
100+
construct_type_unchecked(
101+
value={
102+
**tool_call.to_dict(),
103+
"function": {
104+
**tool_call.function.to_dict(),
105+
"parsed_arguments": parsed_args,
106+
},
107+
},
108+
type_=ParsedFunctionToolCall,
109+
)
110+
)
111+
else:
112+
tool_calls.append(tool_call)
113+
114+
# Parse response content
115+
parsed_content = None
116+
if is_given(response_format) and not is_dict(response_format):
117+
if message.content and not getattr(message, "refusal", None):
118+
parsed_content = _parse_content(response_format, message.content)
119+
120+
choices.append(
121+
construct_type_unchecked(
122+
type_=cast(Any, ParsedChoice),
123+
value={
124+
**choice.to_dict(),
125+
"message": {
126+
**message.to_dict(),
127+
"parsed": parsed_content,
128+
"tool_calls": tool_calls if tool_calls else None,
129+
},
130+
},
131+
)
132+
)
133+
134+
return construct_type_unchecked(
135+
type_=cast(Any, ParsedChatCompletion),
136+
value={
137+
**chat_completion.to_dict(),
138+
"choices": choices,
139+
},
140+
)
141+
142+
143+
def _parse_function_tool_arguments(*, input_tools: list[Dict[str, Any]], function: Function) -> object | None:
144+
"""Parse tool call arguments using Pydantic if tool schema is available."""
145+
input_tool = next(
146+
(t for t in input_tools if t.get("type") == "function" and t.get("function", {}).get("name") == function.name),
147+
None,
148+
)
149+
if not input_tool:
150+
return None
151+
152+
input_fn = input_tool.get("function")
153+
if isinstance(input_fn, PydanticFunctionTool):
154+
return model_parse_json(input_fn.model, function.arguments)
155+
156+
if input_fn and input_fn.get("strict"):
157+
return json.loads(function.arguments)
158+
159+
return None
160+
161+
162+
def _parse_content(response_format: type[ResponseFormatT], content: str) -> ResponseFormatT:
163+
"""Deserialize JSON string into typed Pydantic model."""
164+
if is_basemodel_type(response_format):
165+
return cast(ResponseFormatT, model_parse_json(response_format, content))
166+
167+
if is_dataclass_like_type(response_format):
168+
if PYDANTIC_V1:
169+
raise TypeError(f"Non BaseModel types are only supported with Pydantic v2 - {response_format}")
170+
return pydantic.TypeAdapter(response_format).validate_json(content)
171+
172+
raise TypeError(f"Unable to automatically parse response format type {response_format}")

0 commit comments

Comments
 (0)