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
4 changes: 2 additions & 2 deletions .github/workflows/spec-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -516,8 +516,8 @@ jobs:
/v1/ade/parse and /v1/ade/parse/jobs — same name, different level, different route
family — and lifting a `/v1/*` top-level field onto a /v2 operation is the field-level
version of translating a /v1 route into a /v2 URL; (b) inventing a top-level
convenience parameter that folds one wire field into another. Exactly two exist
(`password` on parse, `strict` on extract); they are cross-SDK contract decisions
convenience parameter that folds one wire field into another. Exactly three exist
(`password` on parse, `strict` and `grounding` on extract); they are cross-SDK decisions
shared with ade-typescript and written down in CONTRIBUTING.md, and changing one is a
maintainer's call — do NOT add, remove or re-target an alias. Wire the spec's own
shape, and if a spec change looks like it needs a new one, leave that field unwired
Expand Down
18 changes: 15 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,21 @@ conveniences that fold into nested fields, and these must stay identical in `ade
its key beats the caller's own kwarg. `ade-typescript` carries the mirror-image trap — there
`{password: undefined}` is how JS spells "absent" and `JSON.stringify` drops the key, so testing
key *presence* rather than value would suppress the shorthand **and** send no password at all.
- `strict` on `/v2/extract` and `/v2/extract/jobs` → `options.strict`. No conflict is possible —
neither SDK exposes `options` on extract, so the shorthand is the only way to reach the field. If
either SDK ever exposes it, that becomes a real tie-break and needs a rule here first.
- `strict` and `grounding` on `/v2/extract` and `/v2/extract/jobs` → `options.strict` and
`options.grounding`. No conflict is possible — neither SDK exposes `options` on extract, so the
shorthands are the only way to reach those fields. If either SDK ever exposes it, that becomes a
real tie-break and needs a rule here first. Three rules bind both repos: both shorthands fold into
the **same** `options` object, so build it once and attach it once (a per-shorthand
`body["options"] = {...}` drops whichever landed first); coerce with `bool()` / `Boolean()` rather
than truthiness, because `grounding=False` is the only value that flag is ever passed; and an
omitted or `None`/`undefined` value leaves its key out so the server default applies (`strict`
false, `grounding` true), with `options` omitted entirely when neither is given.

`grounding` was wired in both SDKs together, at the API owners' request, after three snapshots of
arriving/being dropped/being re-added while unwired (it skips the grounding stage, so every
`extraction_metadata` leaf comes back with `ranges: null`). It is a Preview flag: it reached
staging ahead of production, and with grounding off the `extraction` itself can differ slightly
from a grounded run.

**Nothing in CI checks this** — `check-v2-paths` covers routes only, and a hand-written alias is not
spec-derived, so neither repo's CI can see the other's choice. A PR that adds or touches a top-level
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ The response is a `V2ExtractResult`:

By default, unsupported schema fields are skipped and extraction continues. Pass `strict=True` to reject such schemas with an error (HTTP 422) instead.

Pass `grounding=False` to skip the grounding stage: extraction runs as usual and finishes faster, but every `extraction_metadata` field comes back with `ranges: None` instead of pointing into the Markdown. Preview — with grounding off the `extraction` itself can differ slightly from a grounded run. `extract_jobs.create` takes both options too.

## Process Large Documents Asynchronously (Jobs)

For documents that take longer than a synchronous request allows, create a job and wait for it. `client.v2.parse_jobs` and `client.v2.extract_jobs` share the same shape: `create`, `get`, `list`, and `wait`.
Expand Down
8 changes: 4 additions & 4 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,11 @@ Methods:

Blocks, polling `.get(job_id)` with exponential backoff, until the job reaches a terminal status. Raises `JobWaitTimeoutError` if `timeout` seconds elapse first, and `JobFailedError` if `raise_on_failure=True` and the terminal job carries an `error` (not simply every `failed`/`cancelled` status).

- <code title="post /v2/extract">client.v2.<a href="./src/landingai_ade/resources/v2/v2.py">extract</a>(\*, schema, markdown=..., markdown_url=..., model=..., strict=..., save_to=...) -> <a href="./src/landingai_ade/types/v2/extract_response.py">V2ExtractResult</a></code>
- <code title="post /v2/extract">client.v2.<a href="./src/landingai_ade/resources/v2/v2.py">extract</a>(\*, schema, markdown=..., markdown_url=..., model=..., strict=..., grounding=..., save_to=...) -> <a href="./src/landingai_ade/types/v2/extract_response.py">V2ExtractResult</a></code>

Synchronous extract. `schema` accepts a pydantic `BaseModel` subclass, a `dict`, or a JSON-encoded string -- all are coerced to a JSON Schema object. Provide exactly one of `markdown` or `markdown_url`. `strict=True` rejects schemas with unsupported fields (HTTP 422) instead of silently pruning them. Raises `V2SyncTimeoutError` on a 504; use `extract_jobs` for long-running documents.
Synchronous extract. `schema` accepts a pydantic `BaseModel` subclass, a `dict`, or a JSON-encoded string -- all are coerced to a JSON Schema object. Provide exactly one of `markdown` or `markdown_url`. `strict=True` rejects schemas with unsupported fields (HTTP 422) instead of silently pruning them. `grounding=False` skips the grounding stage, so every `extraction_metadata` leaf comes back with `ranges: None` and the request finishes faster -- Preview, and with grounding off the `extraction` itself can differ slightly from a grounded run (empty leaves are not nulled, all-empty array rows are not dropped). Both are sent inside the same nested `options` object and `extract_jobs.create` takes them with the same semantics. Raises `V2SyncTimeoutError` on a 504; use `extract_jobs` for long-running documents.

- <code title="post /v2/extract/jobs">client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">create</a>(\*, schema, markdown=..., markdown_url=..., model=..., strict=..., output_save_url=..., service_tier=...) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
- <code title="post /v2/extract/jobs">client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">create</a>(\*, schema, markdown=..., markdown_url=..., model=..., strict=..., grounding=..., output_save_url=..., service_tier=...) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
- <code title="get /v2/extract/jobs/{job_id}">client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">get</a>(job_id) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
- <code title="get /v2/extract/jobs">client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">list</a>(\*, page=..., page_size=..., status=...) -> JobList[<a href="./src/landingai_ade/types/v2/job.py">Job</a>]</code>
- <code>client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">wait</a>(job_id, \*, timeout=600, poll_interval=None, raise_on_failure=False) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
Expand All @@ -138,5 +138,5 @@ Methods:
Notes:

- `parse_jobs.list` and `extract_jobs.list` both return a `JobList` (a `list[Job]` subclass) carrying pagination metadata: `.has_more`, `.org_id`, `.page`, `.page_size`. The `page_size=` keyword is sent on the wire as the `pageSize` query parameter (the spec renamed it, matching the V1 list routes); the keyword and the response envelope's `page_size` field are unchanged.
- `options.grounding` (added upstream to `/v2/extract` and `/v2/extract/jobs`) is **not** exposed. It is nested inside `options`, which `client.v2.extract` does not surface — reaching it would need a third hand-written top-level shorthand alongside `password` and `strict`, and per CONTRIBUTING.md that is a cross-SDK decision to make with `ade-typescript` first. Until then, send it with `extra_body={"options": {"grounding": False}}` — note that `extra_body` merges at the top level only, so that replaces the whole `options` object and you must repeat `strict` in it rather than passing `strict=`.
- `strict=` and `grounding=` on extract are hand-written top-level shorthands: the spec declares both inside the nested `options` object, which `client.v2.extract` does not surface. They fold into one `options` and `ade-typescript` exposes the same two with the same semantics (CONTRIBUTING.md pins that parity). Omitting one — or passing `None` — leaves its key out, so the server default applies (`strict` false, `grounding` true). An `options` supplied through `extra_body` replaces the assembled one outright rather than merging into it, since `extra_body` merges at the top level only.
- All `client.v2.*` methods accept the usual `extra_headers`, `extra_query`, `extra_body`, and `timeout` overrides; sync methods additionally accept `save_to` (parse/extract only, not the job-creation methods) to write the response to disk, mirroring V1's `save_to`.
89 changes: 45 additions & 44 deletions docs/v2-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,50 +112,51 @@ fields the model could not extract — the extraction is partial), `warnings`
`output_extraction_chars` char counts moved from `billing` onto `metadata`
upstream; both are retained on `V2ExtractBilling` for backward compatibility.

### Extraction options (`strict`)

`/v2/extract` and `/v2/extract/jobs` take a nested `options` object. Unlike parse,
extract does **not** expose `options` as a keyword — the only nested option the SDK
reaches is `strict`, through a hand-written top-level shorthand that folds into
`options.strict`.

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]`.
### Extraction options (`strict`, `grounding`)

`/v2/extract` and `/v2/extract/jobs` take a nested `options` object with exactly two
members, `strict` and `grounding`. Unlike parse, extract does **not** expose `options`
as a keyword: both are reached through hand-written top-level shorthands that fold
into it.

`grounding` is the newer of the two. It is a Preview flag that skips the grounding
stage, so every `extraction_metadata` leaf comes back with `ranges: null` and the
request finishes faster. It arrived, was dropped, and was re-added across three
snapshots while staying unwired; it is wired now because the API owners asked both
SDKs to expose it as a top-level keyword, matching `strict` (the decision, and the
`ade-typescript` half of it, is recorded in CONTRIBUTING.md — that file is where a
shorthand like this has to be agreed before it ships).

Two behaviors are worth knowing:

- **Both shorthands fold into the SAME `options` object**, so `_build_extract_body`
collects them and attaches the object once. Assigning `body["options"]` per
shorthand — which is what the code did while `strict` was alone — would silently
drop whichever landed first. `test_extract_options_carries_only_the_wired_shorthands`
pins the assembled object exactly, which also catches a third key riding along:
`options` is `additionalProperties: false` upstream, so an extra one is a 422.
- **`False` must survive.** The fold is `bool(value)`, not a truthiness test, and an
omitted or `None` value leaves the key out entirely so the server default applies
(`strict` false, `grounding` true). `grounding=False` is the only value anyone
passes this flag for, so a truthiness regression would make the keyword a silent
no-op.

`extra_body` still merges at the **top level only**, so an `options` passed there
replaces the assembled one rather than merging into it
(`test_extract_extra_body_options_replaces_the_shorthands`). Nobody needs it to reach
`grounding` any more, but a caller who mixes the two gets the override, not a union.

The live check is `test_extract_grounding_false_nulls_every_range` in
`tests/contract/test_v2_smoke.py`: it asserts the returned `ranges` are all `null`
rather than asserting a 2xx. A shorthand that never reached the wire — wrong nesting,
a dropped `False` — is not an error staging reports; it just grounds anyway and comes
back with real ranges.

Nothing about the **response** changed — `extraction_metadata` is still an untyped
`Dict[str, object]` on `V2ExtractResult`. Note that with grounding off the
`extraction` itself can differ slightly from a grounded run (empty leaves are not
nulled and all-empty array rows are not dropped), which is upstream behavior, not
something the SDK normalizes.

The async `extract_jobs.create` also accepts `output_save_url` (async jobs only):
when set, the finished result is delivered to that URL and the completed job
Expand Down
Loading