|
7 | 7 | _T = TypeVar("_T") |
8 | 8 |
|
9 | 9 |
|
10 | | -def to_strict_json_schema(model: type[BaseModel] | TypeAdapter[Any]) -> dict[str, Any]: |
| 10 | +def _model_json_schema(model: type[BaseModel] | TypeAdapter[Any]) -> dict[str, Any]: |
11 | 11 | if isinstance(model, TypeAdapter): |
12 | | - schema = model.json_schema() |
13 | | - else: |
14 | | - schema = model.model_json_schema() |
| 12 | + return model.json_schema() |
| 13 | + return model.model_json_schema() |
| 14 | + |
| 15 | + |
| 16 | +def to_strict_json_schema(model: type[BaseModel] | TypeAdapter[Any]) -> dict[str, Any]: |
| 17 | + schema = _model_json_schema(model) |
| 18 | + |
| 19 | + return _ensure_strict_json_schema(schema, path=(), root=schema) |
| 20 | + |
| 21 | + |
| 22 | +def to_strict_tool_json_schema(model: type[BaseModel] | TypeAdapter[Any]) -> dict[str, Any]: |
| 23 | + schema = _model_json_schema(model) |
| 24 | + _make_defaults_nullable(schema) |
15 | 25 |
|
16 | 26 | return _ensure_strict_json_schema(schema, path=(), root=schema) |
17 | 27 |
|
18 | 28 |
|
| 29 | +def _make_defaults_nullable(json_schema: object) -> None: |
| 30 | + """Encode defaulted tool arguments as nullable fields for strict schemas. |
| 31 | +
|
| 32 | + OpenAI strict schemas require every property and do not support JSON Schema defaults, so |
| 33 | + ``null`` represents "use the Python default". Tool argument preparation replaces that value |
| 34 | + before validation. Response formats intentionally skip this conversion because their output |
| 35 | + is validated directly against the declared Pydantic nullability. |
| 36 | + """ |
| 37 | + if isinstance(json_schema, dict): |
| 38 | + if "default" in json_schema: |
| 39 | + typ = json_schema.get("type") |
| 40 | + if isinstance(typ, str): |
| 41 | + json_schema["type"] = [typ, "null"] |
| 42 | + elif isinstance(typ, list) and "null" not in typ: |
| 43 | + json_schema["type"] = [*typ, "null"] |
| 44 | + |
| 45 | + for value in json_schema.values(): |
| 46 | + _make_defaults_nullable(value) |
| 47 | + elif isinstance(json_schema, list): |
| 48 | + for value in json_schema: |
| 49 | + _make_defaults_nullable(value) |
| 50 | + |
| 51 | + |
19 | 52 | # from https://platform.openai.com/docs/guides/function-calling?api-mode=responses&strict-mode=disabled#strict-mode |
20 | 53 | # Strict mode |
21 | 54 | # Setting strict to true will ensure function calls reliably adhere to the function schema, |
@@ -118,19 +151,6 @@ def _ensure_strict_json_schema( |
118 | 151 | if "default" in json_schema: |
119 | 152 | json_schema.pop("default", None) |
120 | 153 |
|
121 | | - # Treat any parameter with a default value as optional. If the parameter’s type doesn't |
122 | | - # support None, the default will be used instead. |
123 | | - t = json_schema.get("type") |
124 | | - if isinstance(t, str): |
125 | | - json_schema["type"] = [t, "null"] |
126 | | - |
127 | | - elif isinstance(t, list): |
128 | | - types = t.copy() |
129 | | - if "null" not in types: |
130 | | - types.append("null") |
131 | | - |
132 | | - json_schema["type"] = types |
133 | | - |
134 | 154 | json_schema.pop("title", None) |
135 | 155 | json_schema.pop("discriminator", None) |
136 | 156 |
|
|
0 commit comments