oauth-codex 2.0.2__tar.gz → 2.0.3__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.
Files changed (63) hide show
  1. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/PKG-INFO +19 -1
  2. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/README.md +18 -0
  3. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/pyproject.toml +1 -1
  4. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_client.py +75 -3
  5. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_version.py +1 -1
  6. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/tooling.py +52 -5
  7. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex.egg-info/PKG-INFO +19 -1
  8. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/tests/test_generate_async.py +33 -0
  9. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/tests/test_generate_sync.py +78 -0
  10. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/setup.cfg +0 -0
  11. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/__init__.py +0 -0
  12. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_base_client.py +0 -0
  13. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_engine.py +0 -0
  14. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_exceptions.py +0 -0
  15. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_models.py +0 -0
  16. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_module_client.py +0 -0
  17. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_resource.py +0 -0
  18. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_types.py +0 -0
  19. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/auth/__init__.py +0 -0
  20. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/auth/config.py +0 -0
  21. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/auth/pkce.py +0 -0
  22. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/auth/store.py +0 -0
  23. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/auth/token_manager.py +0 -0
  24. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/compat_store.py +0 -0
  25. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/core_types.py +0 -0
  26. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/errors.py +0 -0
  27. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/py.typed +0 -0
  28. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/__init__.py +0 -0
  29. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/_wrappers.py +0 -0
  30. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/files.py +0 -0
  31. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/models.py +0 -0
  32. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/responses/__init__.py +0 -0
  33. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/responses/_helpers.py +0 -0
  34. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/responses/input_tokens.py +0 -0
  35. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/responses/responses.py +0 -0
  36. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/vector_stores/__init__.py +0 -0
  37. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/vector_stores/file_batches.py +0 -0
  38. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/vector_stores/files.py +0 -0
  39. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/vector_stores/vector_stores.py +0 -0
  40. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/store.py +0 -0
  41. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/__init__.py +0 -0
  42. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/file_deleted.py +0 -0
  43. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/file_object.py +0 -0
  44. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/responses/__init__.py +0 -0
  45. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/responses/input_token_count_response.py +0 -0
  46. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/responses/response.py +0 -0
  47. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/responses/response_stream_event.py +0 -0
  48. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/shared/__init__.py +0 -0
  49. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/shared/model_capabilities.py +0 -0
  50. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/shared/usage.py +0 -0
  51. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/vector_stores/__init__.py +0 -0
  52. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/vector_stores/vector_store.py +0 -0
  53. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/vector_stores/vector_store_deleted.py +0 -0
  54. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/vector_stores/vector_store_file.py +0 -0
  55. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/vector_stores/vector_store_file_batch.py +0 -0
  56. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/vector_stores/vector_store_search_response.py +0 -0
  57. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/version.py +0 -0
  58. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex.egg-info/SOURCES.txt +0 -0
  59. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex.egg-info/dependency_links.txt +0 -0
  60. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex.egg-info/requires.txt +0 -0
  61. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex.egg-info/top_level.txt +0 -0
  62. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/tests/test_engine_stream_and_continuity.py +0 -0
  63. {oauth_codex-2.0.2 → oauth_codex-2.0.3}/tests/test_public_surface.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oauth-codex
3
- Version: 2.0.2
3
+ Version: 2.0.3
4
4
  Summary: Codex OAuth-based Python SDK with a single Client and generate-first API
5
5
  Author: Codex
6
6
  Requires-Python: >=3.11
@@ -64,6 +64,24 @@ text = client.generate(
64
64
  print(text)
65
65
  ```
66
66
 
67
+ Single-parameter Pydantic tool inputs are also supported.
68
+
69
+ ```python
70
+ from pydantic import BaseModel
71
+
72
+
73
+ class ToolInput(BaseModel):
74
+ query: str
75
+
76
+
77
+ def tool(input: ToolInput) -> str:
78
+ return f"Tool received query: {input.query}"
79
+
80
+
81
+ text = client.generate("Use the tool", tools=[tool])
82
+ print(text)
83
+ ```
84
+
67
85
  If a tool raises an exception, the SDK forwards it to the model as `{\"error\": ...}` and continues the loop.
68
86
 
69
87
  ## Async
@@ -50,6 +50,24 @@ text = client.generate(
50
50
  print(text)
51
51
  ```
52
52
 
53
+ Single-parameter Pydantic tool inputs are also supported.
54
+
55
+ ```python
56
+ from pydantic import BaseModel
57
+
58
+
59
+ class ToolInput(BaseModel):
60
+ query: str
61
+
62
+
63
+ def tool(input: ToolInput) -> str:
64
+ return f"Tool received query: {input.query}"
65
+
66
+
67
+ text = client.generate("Use the tool", tools=[tool])
68
+ print(text)
69
+ ```
70
+
53
71
  If a tool raises an exception, the SDK forwards it to the model as `{\"error\": ...}` and continues the loop.
54
72
 
55
73
  ## Async
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "oauth-codex"
7
- version = "2.0.2"
7
+ version = "2.0.3"
8
8
  description = "Codex OAuth-based Python SDK with a single Client and generate-first API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -6,7 +6,9 @@ import inspect
6
6
  import json
7
7
  import mimetypes
8
8
  from pathlib import Path
9
- from typing import Any, AsyncIterator, Callable, Iterator
9
+ from typing import Any, AsyncIterator, Callable, Iterator, get_type_hints
10
+
11
+ from pydantic import BaseModel
10
12
 
11
13
  from ._base_client import SyncAPIClient
12
14
  from ._engine import OAuthCodexClient as _EngineClient
@@ -371,7 +373,8 @@ class OAuthCodexClient(SyncAPIClient):
371
373
  else:
372
374
  try:
373
375
  kwargs = self._parse_tool_kwargs(call.arguments_json)
374
- value = tool(**kwargs)
376
+ normalized_kwargs = self._normalize_tool_kwargs(tool, kwargs)
377
+ value = tool(**normalized_kwargs)
375
378
  if inspect.isawaitable(value):
376
379
  raise TypeError("async tool is not supported in generate(); use agenerate()")
377
380
  output = self._normalize_tool_output(value)
@@ -394,7 +397,8 @@ class OAuthCodexClient(SyncAPIClient):
394
397
  else:
395
398
  try:
396
399
  kwargs = self._parse_tool_kwargs(call.arguments_json)
397
- value = tool(**kwargs)
400
+ normalized_kwargs = self._normalize_tool_kwargs(tool, kwargs)
401
+ value = tool(**normalized_kwargs)
398
402
  if inspect.isawaitable(value):
399
403
  value = await value
400
404
  output = self._normalize_tool_output(value)
@@ -411,6 +415,74 @@ class OAuthCodexClient(SyncAPIClient):
411
415
  raise TypeError("tool arguments must be a JSON object")
412
416
  return parsed
413
417
 
418
+ def _normalize_tool_kwargs(
419
+ self,
420
+ tool: Callable[..., Any],
421
+ kwargs: dict[str, Any],
422
+ ) -> dict[str, Any]:
423
+ signature = inspect.signature(tool)
424
+ resolved_hints = self._resolve_tool_type_hints(tool)
425
+ params = [
426
+ param
427
+ for param in signature.parameters.values()
428
+ if param.kind not in (param.VAR_POSITIONAL, param.VAR_KEYWORD)
429
+ ]
430
+ if not params:
431
+ return kwargs
432
+
433
+ if len(params) == 1:
434
+ param = params[0]
435
+ model_type = self._resolve_pydantic_model_type(
436
+ resolved_hints.get(param.name, param.annotation)
437
+ )
438
+ if model_type is not None:
439
+ if param.name in kwargs:
440
+ payload = kwargs[param.name]
441
+ if isinstance(payload, model_type):
442
+ return kwargs
443
+ if not isinstance(payload, dict):
444
+ raise TypeError(f"tool argument `{param.name}` must be a JSON object")
445
+ normalized = dict(kwargs)
446
+ normalized[param.name] = model_type.model_validate(payload)
447
+ return normalized
448
+
449
+ if not kwargs and param.default is not inspect._empty:
450
+ return kwargs
451
+
452
+ payload = kwargs
453
+ normalized_payload = (
454
+ payload
455
+ if isinstance(payload, model_type)
456
+ else model_type.model_validate(payload)
457
+ )
458
+ return {param.name: normalized_payload}
459
+
460
+ normalized = dict(kwargs)
461
+ for param in params:
462
+ model_type = self._resolve_pydantic_model_type(
463
+ resolved_hints.get(param.name, param.annotation)
464
+ )
465
+ if model_type is None or param.name not in normalized:
466
+ continue
467
+ payload = normalized[param.name]
468
+ if isinstance(payload, model_type):
469
+ continue
470
+ if not isinstance(payload, dict):
471
+ raise TypeError(f"tool argument `{param.name}` must be a JSON object")
472
+ normalized[param.name] = model_type.model_validate(payload)
473
+ return normalized
474
+
475
+ def _resolve_pydantic_model_type(self, annotation: Any) -> type[BaseModel] | None:
476
+ if isinstance(annotation, type) and issubclass(annotation, BaseModel):
477
+ return annotation
478
+ return None
479
+
480
+ def _resolve_tool_type_hints(self, tool: Callable[..., Any]) -> dict[str, Any]:
481
+ try:
482
+ return get_type_hints(tool)
483
+ except Exception:
484
+ return {}
485
+
414
486
  def _normalize_tool_output(self, output: Any) -> dict[str, Any]:
415
487
  return normalize_tool_output(output)
416
488
 
@@ -1,2 +1,2 @@
1
1
  __title__ = "oauth-codex"
2
- __version__ = "2.0.2"
2
+ __version__ = "2.0.3"
@@ -3,11 +3,32 @@ from __future__ import annotations
3
3
  import inspect
4
4
  import json
5
5
  from types import UnionType
6
- from typing import Any, get_args, get_origin
6
+ from typing import Any, get_args, get_origin, get_type_hints
7
7
 
8
8
  from .core_types import ToolInput, ToolResult, ToolSchema
9
9
  from .errors import SDKRequestError
10
10
 
11
+ try:
12
+ from pydantic import BaseModel
13
+ except Exception: # pragma: no cover - pydantic is a runtime dependency
14
+ BaseModel = None # type: ignore[assignment]
15
+
16
+
17
+ def _is_pydantic_model_type(annotation: Any) -> bool:
18
+ return bool(
19
+ BaseModel is not None
20
+ and isinstance(annotation, type)
21
+ and issubclass(annotation, BaseModel)
22
+ )
23
+
24
+
25
+ def _pydantic_model_to_schema(model_type: type[Any]) -> dict[str, Any]:
26
+ if hasattr(model_type, "model_json_schema"):
27
+ schema = model_type.model_json_schema()
28
+ if isinstance(schema, dict):
29
+ return schema
30
+ return {"type": "object"}
31
+
11
32
 
12
33
  def _python_type_to_schema(annotation: Any) -> dict[str, Any]:
13
34
  if annotation is inspect._empty:
@@ -33,6 +54,8 @@ def _python_type_to_schema(annotation: Any) -> dict[str, Any]:
33
54
  args = get_args(annotation)
34
55
 
35
56
  if origin is None:
57
+ if _is_pydantic_model_type(annotation):
58
+ return _pydantic_model_to_schema(annotation)
36
59
  if annotation is str:
37
60
  return {"type": "string"}
38
61
  if annotation is int:
@@ -67,16 +90,39 @@ def _python_type_to_schema(annotation: Any) -> dict[str, Any]:
67
90
 
68
91
  def callable_to_tool_schema(func: Any) -> ToolSchema:
69
92
  signature = inspect.signature(func)
93
+ try:
94
+ resolved_hints = get_type_hints(func)
95
+ except Exception:
96
+ resolved_hints = {}
97
+
70
98
  doc = inspect.getdoc(func) or ""
71
99
  description = doc.splitlines()[0] if doc else f"Tool `{getattr(func, '__name__', 'tool')}`"
72
100
 
101
+ params = [
102
+ param
103
+ for param in signature.parameters.values()
104
+ if param.kind not in (param.VAR_POSITIONAL, param.VAR_KEYWORD)
105
+ ]
106
+ if len(params) == 1:
107
+ single = params[0]
108
+ single_annotation = resolved_hints.get(single.name, single.annotation)
109
+ if _is_pydantic_model_type(single_annotation):
110
+ model_schema = _python_type_to_schema(single_annotation)
111
+ if model_schema.get("type") == "object":
112
+ return {
113
+ "type": "function",
114
+ "name": getattr(func, "__name__", "tool"),
115
+ "description": description,
116
+ "parameters": model_schema,
117
+ }
118
+
73
119
  properties: dict[str, Any] = {}
74
120
  required: list[str] = []
75
121
 
76
- for name, param in signature.parameters.items():
77
- if param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD):
78
- continue
79
- properties[name] = _python_type_to_schema(param.annotation)
122
+ for param in params:
123
+ name = param.name
124
+ annotation = resolved_hints.get(name, param.annotation)
125
+ properties[name] = _python_type_to_schema(annotation)
80
126
  if param.default is inspect._empty:
81
127
  required.append(name)
82
128
 
@@ -93,6 +139,7 @@ def callable_to_tool_schema(func: Any) -> ToolSchema:
93
139
  }
94
140
 
95
141
 
142
+
96
143
  def _normalize_dict_tool(tool: dict[str, Any]) -> ToolSchema:
97
144
  if tool.get("type") == "function" and "function" in tool and isinstance(tool["function"], dict):
98
145
  fn = tool["function"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oauth-codex
3
- Version: 2.0.2
3
+ Version: 2.0.3
4
4
  Summary: Codex OAuth-based Python SDK with a single Client and generate-first API
5
5
  Author: Codex
6
6
  Requires-Python: >=3.11
@@ -64,6 +64,24 @@ text = client.generate(
64
64
  print(text)
65
65
  ```
66
66
 
67
+ Single-parameter Pydantic tool inputs are also supported.
68
+
69
+ ```python
70
+ from pydantic import BaseModel
71
+
72
+
73
+ class ToolInput(BaseModel):
74
+ query: str
75
+
76
+
77
+ def tool(input: ToolInput) -> str:
78
+ return f"Tool received query: {input.query}"
79
+
80
+
81
+ text = client.generate("Use the tool", tools=[tool])
82
+ print(text)
83
+ ```
84
+
67
85
  If a tool raises an exception, the SDK forwards it to the model as `{\"error\": ...}` and continues the loop.
68
86
 
69
87
  ## Async
@@ -1,12 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import pytest
4
+ from pydantic import BaseModel
4
5
 
5
6
  from conftest import InMemoryTokenStore
6
7
  from oauth_codex import Client
7
8
  from oauth_codex.core_types import GenerateResult, OAuthTokens, StreamEvent, ToolCall
8
9
 
9
10
 
11
+ class ToolInput(BaseModel):
12
+ query: str
13
+
14
+
10
15
  def _client() -> Client:
11
16
  return Client(
12
17
  token_store=InMemoryTokenStore(
@@ -82,3 +87,31 @@ async def test_astream_supports_tool_calls(monkeypatch: pytest.MonkeyPatch) -> N
82
87
  assert calls[1]["previous_response_id"] == "resp_1"
83
88
  tool_results = calls[1]["tool_results"]
84
89
  assert tool_results[0].output == {"product": 12}
90
+
91
+
92
+ @pytest.mark.asyncio
93
+ async def test_agenerate_supports_single_pydantic_tool_input(monkeypatch: pytest.MonkeyPatch) -> None:
94
+ client = _client()
95
+ calls: list[dict[str, object]] = []
96
+
97
+ async def fake_agenerate(**kwargs):
98
+ calls.append(kwargs)
99
+ if len(calls) == 1:
100
+ return GenerateResult(
101
+ text="",
102
+ tool_calls=[ToolCall(id="call_1", name="tool", arguments_json='{"query":"hello"}')],
103
+ finish_reason="tool_calls",
104
+ response_id="resp_1",
105
+ )
106
+ return GenerateResult(text="done", tool_calls=[], finish_reason="stop", response_id="resp_2")
107
+
108
+ monkeypatch.setattr(client._engine, "agenerate", fake_agenerate)
109
+
110
+ def tool(input: ToolInput) -> str:
111
+ return f"Tool received query: {input.query}"
112
+
113
+ out = await client.agenerate("run", tools=[tool])
114
+
115
+ assert out == "done"
116
+ tool_results = calls[1]["tool_results"]
117
+ assert tool_results[0].output == {"output": "Tool received query: hello"}
@@ -1,12 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import pytest
4
+ from pydantic import BaseModel, Field
4
5
 
5
6
  from conftest import InMemoryTokenStore
6
7
  from oauth_codex import Client
7
8
  from oauth_codex.core_types import GenerateResult, OAuthTokens, StreamEvent, ToolCall
8
9
 
9
10
 
11
+ class ToolInputWithDescription(BaseModel):
12
+ query: str = Field(..., description="The query to be processed by the tool.")
13
+
14
+
15
+ class ToolInput(BaseModel):
16
+ query: str
17
+
18
+
10
19
  def _client() -> Client:
11
20
  return Client(
12
21
  token_store=InMemoryTokenStore(
@@ -147,6 +156,75 @@ def test_generate_wraps_string_tool_output_as_dict(monkeypatch: pytest.MonkeyPat
147
156
  assert tool_results[0].output == {"output": "hello"}
148
157
 
149
158
 
159
+ def test_generate_supports_single_pydantic_tool_input_with_flat_payload(
160
+ monkeypatch: pytest.MonkeyPatch,
161
+ ) -> None:
162
+ client = _client()
163
+ calls: list[dict[str, object]] = []
164
+
165
+ def fake_generate(**kwargs):
166
+ calls.append(kwargs)
167
+ if len(calls) == 1:
168
+ return GenerateResult(
169
+ text="",
170
+ tool_calls=[ToolCall(id="call_1", name="tool", arguments_json='{"query":"hello"}')],
171
+ finish_reason="tool_calls",
172
+ response_id="resp_1",
173
+ )
174
+ return GenerateResult(text="done", tool_calls=[], finish_reason="stop", response_id="resp_2")
175
+
176
+ monkeypatch.setattr(client._engine, "generate", fake_generate)
177
+
178
+ def tool(input: ToolInputWithDescription) -> str:
179
+ return f"Tool received query: {input.query}"
180
+
181
+ out = client.generate("run", tools=[tool])
182
+
183
+ assert out == "done"
184
+ first_round_tools = calls[0]["tools"]
185
+ assert isinstance(first_round_tools, list)
186
+ assert first_round_tools[0]["parameters"]["type"] == "object"
187
+ assert "query" in first_round_tools[0]["parameters"]["properties"]
188
+ assert "input" not in first_round_tools[0]["parameters"]["properties"]
189
+ tool_results = calls[1]["tool_results"]
190
+ assert tool_results[0].output == {"output": "Tool received query: hello"}
191
+
192
+
193
+ def test_generate_supports_single_pydantic_tool_input_with_nested_payload(
194
+ monkeypatch: pytest.MonkeyPatch,
195
+ ) -> None:
196
+ client = _client()
197
+ calls: list[dict[str, object]] = []
198
+
199
+ def fake_generate(**kwargs):
200
+ calls.append(kwargs)
201
+ if len(calls) == 1:
202
+ return GenerateResult(
203
+ text="",
204
+ tool_calls=[
205
+ ToolCall(
206
+ id="call_1",
207
+ name="tool",
208
+ arguments_json='{"input":{"query":"hello"}}',
209
+ )
210
+ ],
211
+ finish_reason="tool_calls",
212
+ response_id="resp_1",
213
+ )
214
+ return GenerateResult(text="done", tool_calls=[], finish_reason="stop", response_id="resp_2")
215
+
216
+ monkeypatch.setattr(client._engine, "generate", fake_generate)
217
+
218
+ def tool(input: ToolInput) -> str:
219
+ return f"Tool received query: {input.query}"
220
+
221
+ out = client.generate("run", tools=[tool])
222
+
223
+ assert out == "done"
224
+ tool_results = calls[1]["tool_results"]
225
+ assert tool_results[0].output == {"output": "Tool received query: hello"}
226
+
227
+
150
228
  def test_generate_raises_when_tool_round_limit_exceeded(monkeypatch: pytest.MonkeyPatch) -> None:
151
229
  client = _client()
152
230
  client.max_tool_rounds = 2
File without changes