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.
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/PKG-INFO +19 -1
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/README.md +18 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/pyproject.toml +1 -1
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_client.py +75 -3
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_version.py +1 -1
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/tooling.py +52 -5
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex.egg-info/PKG-INFO +19 -1
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/tests/test_generate_async.py +33 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/tests/test_generate_sync.py +78 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/setup.cfg +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/__init__.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_base_client.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_engine.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_exceptions.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_models.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_module_client.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_resource.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/_types.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/auth/__init__.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/auth/config.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/auth/pkce.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/auth/store.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/auth/token_manager.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/compat_store.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/core_types.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/errors.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/py.typed +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/__init__.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/_wrappers.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/files.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/models.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/responses/__init__.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/responses/_helpers.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/responses/input_tokens.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/responses/responses.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/vector_stores/__init__.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/vector_stores/file_batches.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/vector_stores/files.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/vector_stores/vector_stores.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/store.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/__init__.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/file_deleted.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/file_object.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/responses/__init__.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/responses/input_token_count_response.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/responses/response.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/responses/response_stream_event.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/shared/__init__.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/shared/model_capabilities.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/shared/usage.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/vector_stores/__init__.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/vector_stores/vector_store.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/vector_stores/vector_store_deleted.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/vector_stores/vector_store_file.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/vector_stores/vector_store_file_batch.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/vector_stores/vector_store_search_response.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/version.py +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex.egg-info/SOURCES.txt +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex.egg-info/dependency_links.txt +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex.egg-info/requires.txt +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex.egg-info/top_level.txt +0 -0
- {oauth_codex-2.0.2 → oauth_codex-2.0.3}/tests/test_engine_stream_and_continuity.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
__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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
properties[name] = _python_type_to_schema(
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/vector_stores/file_batches.py
RENAMED
|
File without changes
|
|
File without changes
|
{oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/resources/vector_stores/vector_stores.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/responses/response_stream_event.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/vector_stores/vector_store_deleted.py
RENAMED
|
File without changes
|
{oauth_codex-2.0.2 → oauth_codex-2.0.3}/src/oauth_codex/types/vector_stores/vector_store_file.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|