diff --git a/docs/v2-testing.md b/docs/v2-testing.md index 2080f5b..8c8da88 100644 --- a/docs/v2-testing.md +++ b/docs/v2-testing.md @@ -119,19 +119,40 @@ extract does **not** expose `options` as a keyword — the only nested option th reaches is `strict`, through a hand-written top-level shorthand that folds into `options.strict`. -`strict` is once again the **only** member of that object. A previous snapshot had -briefly added a second, `options.grounding` (a Preview flag that skipped the -grounding stage so every `extraction_metadata` leaf came back with `ranges: null`). -It was deliberately never wired — it is nested, not a top-level `requestBody` -property, so surfacing it would have meant a third hand-written alias next to -`password` and `strict`, which CONTRIBUTING.md pins as a maintainer's call rather -than a spec-sync one. This snapshot drops `grounding` from the spec altogether, so -there is nothing left to decide and no SDK surface to retire. - -One consequence worth knowing when re-running older scratch scripts: the `options` -schema is `additionalProperties: false`, so the `extra_body={"options": {"grounding": -False}}` escape hatch that reached the flag against the previous snapshot is now an -unknown key and is rejected by the gateway. Drop it; pass `strict=` as usual. +That object now has a second member again: this snapshot **re-adds** +`options.grounding`, a Preview flag that skips the grounding stage so every +`extraction_metadata` leaf comes back with `ranges: null` (and the request finishes +faster). It has now been added, dropped, and re-added across three snapshots; the +SDK's answer has not moved, and `strict` remains the only option the SDK itself +folds in. + +`grounding` stays **unwired**, for the same reason as before: it is nested inside +`options`, not a top-level `requestBody` property, so surfacing it would mean a +third hand-written alias next to `password` and `strict`. CONTRIBUTING.md pins that +as a maintainer's call to be made jointly with `ade-typescript`, not a spec-sync +one. Exposing `options` on extract instead is not a way around it — CONTRIBUTING.md +calls that out explicitly, because it would turn the `strict` shorthand into a real +precedence tie-break that needs a written rule in that file first. + +To reach the flag today, use `extra_body`: + +```python +client.v2.extract( + schema=..., markdown="# doc", + extra_body={"options": {"grounding": False, "strict": False}}, +) +``` + +`extra_body` merges at the **top level only**, so it replaces the whole `options` +object — repeat `strict` inside it rather than also passing `strict=`, or the +keyword's value is silently discarded. (The escape hatch was rejected by the gateway +under the previous snapshot, where `grounding` was absent and `options` is +`additionalProperties: false`; it works again now.) + +`test_extract_options_carries_only_strict` pins the exact key set the SDK folds in, +so an unreviewed `grounding` alias cannot start riding along unnoticed, and +`test_extract_grounding_reachable_only_via_extra_body` pins the snippet above — +including the shallow merge that makes repeating `strict` necessary. Nothing about the **response** changed — `extraction_metadata` is unaffected, and `V2ExtractResult` already models it as an untyped `Dict[str, object]`. diff --git a/specs/_generated/v2_models.py b/specs/_generated/v2_models.py index c05e7a3..81c21cf 100644 --- a/specs/_generated/v2_models.py +++ b/specs/_generated/v2_models.py @@ -559,6 +559,11 @@ class V2ExtractOptions(BaseModel): model_config = ConfigDict( extra='forbid', ) + grounding: Optional[bool] = Field( + True, + description='When ``true`` (default), the grounding stage runs and each extracted leaf carries ``ranges`` into the source Markdown where a reference was found (``ranges`` is ``null`` for a value that could not be located). When ``false``, the grounding stage is skipped: every ``extraction_metadata`` leaf has ``ranges: null`` and the request completes faster. Preview.', + title='Grounding', + ) strict: Optional[bool] = Field( False, description='When ``true``, a schema containing fields the model cannot extract fails with a validation error — HTTP 422 on the sync route, or a failed job (``status: "failed"``) on the async ``/jobs`` route. When ``false`` (default), unsupported fields are skipped and extraction continues.', @@ -1893,7 +1898,8 @@ class V2ExtractPostRequest(BaseModel): title='Model', ) options: Optional[V2ExtractOptions] = Field( - None, description='Extraction options (``strict``). Omit for defaults.' + None, + description='Extraction options (``strict``, ``grounding``). Omit for defaults.', ) schema_: dict[str, Any] = Field( ..., @@ -1926,7 +1932,7 @@ class V2ExtractPostRequest1(BaseModel): ) options: Optional[V2ExtractOptions] = Field( None, - description='Extraction options (``strict``). Omit for defaults. JSON-serialized string in form data.', + description='Extraction options (``strict``, ``grounding``). Omit for defaults. JSON-serialized string in form data.', ) schema_: dict[str, Any] = Field( ..., @@ -2047,7 +2053,8 @@ class V2ExtractJobsPostRequest(BaseModel): title='Model', ) options: Optional[V2ExtractOptions] = Field( - None, description='Extraction options (``strict``). Omit for defaults.' + None, + description='Extraction options (``strict``, ``grounding``). Omit for defaults.', ) output_save_url: Optional[str] = Field( None, @@ -2089,7 +2096,7 @@ class V2ExtractJobsPostRequest1(BaseModel): ) options: Optional[V2ExtractOptions] = Field( None, - description='Extraction options (``strict``). Omit for defaults. JSON-serialized string in form data.', + description='Extraction options (``strict``, ``grounding``). Omit for defaults. JSON-serialized string in form data.', ) output_save_url: Optional[str] = Field( None, diff --git a/specs/v2-aide.json b/specs/v2-aide.json index 3e81cec..1bfa2c5 100644 --- a/specs/v2-aide.json +++ b/specs/v2-aide.json @@ -1312,6 +1312,12 @@ "additionalProperties": false, "description": "Extraction options (``docs/extract-v2-contract.md`` → Options).", "properties": { + "grounding": { + "default": true, + "description": "When ``true`` (default), the grounding stage runs and each extracted leaf carries ``ranges`` into the source Markdown where a reference was found (``ranges`` is ``null`` for a value that could not be located). When ``false``, the grounding stage is skipped: every ``extraction_metadata`` leaf has ``ranges: null`` and the request completes faster. Preview.", + "title": "Grounding", + "type": "boolean" + }, "strict": { "default": false, "description": "When ``true``, a schema containing fields the model cannot extract fails with a validation error — HTTP 422 on the sync route, or a failed job (``status: \"failed\"``) on the async ``/jobs`` route. When ``false`` (default), unsupported fields are skipped and extraction continues.", @@ -5240,7 +5246,7 @@ } ], "default": null, - "description": "Extraction options (``strict``). Omit for defaults." + "description": "Extraction options (``strict``, ``grounding``). Omit for defaults." }, "schema": { "additionalProperties": true, @@ -5321,7 +5327,7 @@ } ], "default": null, - "description": "Extraction options (``strict``). Omit for defaults. JSON-serialized string in form data." + "description": "Extraction options (``strict``, ``grounding``). Omit for defaults. JSON-serialized string in form data." }, "schema": { "additionalProperties": true, @@ -5626,7 +5632,7 @@ } ], "default": null, - "description": "Extraction options (``strict``). Omit for defaults." + "description": "Extraction options (``strict``, ``grounding``). Omit for defaults." }, "output_save_url": { "anyOf": [ @@ -5735,7 +5741,7 @@ } ], "default": null, - "description": "Extraction options (``strict``). Omit for defaults. JSON-serialized string in form data." + "description": "Extraction options (``strict``, ``grounding``). Omit for defaults. JSON-serialized string in form data." }, "output_save_url": { "anyOf": [ diff --git a/tests/api_resources/v2/test_extract.py b/tests/api_resources/v2/test_extract.py index fcfefbd..8894ba6 100644 --- a/tests/api_resources/v2/test_extract.py +++ b/tests/api_resources/v2/test_extract.py @@ -53,12 +53,13 @@ def test_extract_sync_strict_option() -> None: @respx.mock def test_extract_options_carries_only_strict() -> None: - # `V2ExtractOptions` is `additionalProperties: false` upstream and `strict` is - # its only member: the spec briefly carried a second option (`grounding`) and - # has since dropped it, so anything the SDK folds in beyond `strict` would now - # be rejected as an unknown key. Pin the exact key set rather than just - # `strict`, so a stray option reintroduced here fails at this seam instead of - # against the live gateway. + # `V2ExtractOptions` is `additionalProperties: false` upstream and now carries + # two members, `strict` and the re-added Preview flag `grounding`. Only + # `strict` is wired: `grounding` is nested rather than a top-level + # `requestBody` property, so reaching it would need a third hand-written alias, + # which CONTRIBUTING.md reserves for a joint call with `ade-typescript`. Pin + # the exact key set rather than just `strict`, so neither a stray option nor an + # unreviewed `grounding` alias can start riding along unnoticed. client = LandingAIADE(apikey=APIKEY) route = respx.post("https://api.ade.landing.ai/v2/extract").mock( return_value=httpx.Response(200, json=EXTRACT_BODY) @@ -68,6 +69,29 @@ def test_extract_options_carries_only_strict() -> None: assert set(req["options"]) == {"strict"} +@respx.mock +def test_extract_grounding_reachable_only_via_extra_body() -> None: + # `options.grounding` is deliberately unwired (nested, not top-level), so + # `extra_body` is the documented escape hatch -- api.md and docs/v2-testing.md + # both point at it. `extra_body` merges at the top level only + # (`_merge_mappings` is a shallow `{**a, **b}`), so the caller's `options` + # replaces the one `_build_extract_body` assembled and must repeat `strict`. + # Pin that here: if the merge ever went deep, the documented snippet would + # start sending a different body than it advertises. + client = LandingAIADE(apikey=APIKEY) + route = respx.post("https://api.ade.landing.ai/v2/extract").mock( + return_value=httpx.Response(200, json=EXTRACT_BODY) + ) + client.v2.extract( + schema={"type": "object"}, + markdown="# doc", + strict=True, + extra_body={"options": {"grounding": False, "strict": False}}, + ) + req = json.loads(route.calls.last.request.content) + assert req["options"] == {"grounding": False, "strict": False} + + def test_extract_job_create_options_carries_only_strict(monkeypatch: pytest.MonkeyPatch) -> None: # Same `additionalProperties: false` contract on the async job route, which # shares `_build_extract_body` with the sync one.