Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 34 additions & 13 deletions docs/v2-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]`.
Expand Down
15 changes: 11 additions & 4 deletions specs/_generated/v2_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down Expand Up @@ -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(
...,
Expand Down Expand Up @@ -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(
...,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 10 additions & 4 deletions specs/v2-aide.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -5240,7 +5246,7 @@
}
],
"default": null,
"description": "Extraction options (``strict``). Omit for defaults."
"description": "Extraction options (``strict``, ``grounding``). Omit for defaults."
},
"schema": {
"additionalProperties": true,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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": [
Expand Down
36 changes: 30 additions & 6 deletions tests/api_resources/v2/test_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
Expand Down