Skip to content

fix(parallel): apply combine_under_n_chars to large PDFs split across workers - #591

Merged
CyMule merged 3 commits into
mainfrom
fix/parallel-combine-option
Oct 7, 2026
Merged

CyMule merged 3 commits into
mainfrom
fix/parallel-combine-option

Conversation

@CyMule

@CyMule CyMule commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

What

combine_under_n_chars now works for large PDFs that are processed in parallel.

Before: request combine_under_n_chars=0 → worker receives combine_text_under_n_chars=0 → ignored → default used
After:  request combine_under_n_chars=0 → worker receives combine_under_n_chars=0 → applied

Why

Internally, the setting is called combine_text_under_n_chars, but workers only accept the public form name combine_under_n_chars. The worker ignored the unknown field and used its default, so short sections could be merged even when the caller asked to keep them separate. Small PDFs processed locally were not affected and are unchanged.

Validation

A test sends a request through a real worker form and checks that a threshold of 0 keeps two sections separate while 100 merges them. It fails on main. Adds a CHANGELOG entry and bumps the version to 0.1.13.

@CyMule
CyMule marked this pull request as ready for review October 7, 2026 15:43
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T15:45:04.475569Z 16dd550 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

Shadow auto-approve: would not auto-approve. This PR does not meet the repository auto-approval settings.

View guided diff | Re-trigger cubic

@CyMule CyMule changed the title Forward the chunk combination threshold in parallel mode fix(parallel): apply combine_under_n_chars to large PDFs split across workers Oct 7, 2026

@paulkarayan paulkarayan left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@paulkarayan

Copy link
Copy Markdown

Automated review, forwarded by @paulkarayan. Findings are code-verified where they name a file and line; the strong-review gpt-pro leg and cubic did not run.

GO -- 0 blocker(s), 0 concern(s), 1 note(s), 1 nit(s).

Parallel PDF workers now receive combine_under_n_chars under its form name; correct, tested red/green, CI green. Only queue fact: 589 and 592 also claim 0.1.12.

Notes (1)

  1. Version collision: unstructured-api 589 and 592 are open and also bump to 0.1.12 (CHANGELOG + version.py); whichever merges second re-bumps. 581 already took 0.1.13.

Nits (1)

  1. general.py:149 worker_kwargs = partition_kwargs.copy() is redundant (**partition_kwargs is already a fresh dict per call), and the test's asserts that options is unmutated were already true on main; harmless.
Full review
# Unstructured-IO/unstructured-api#591 -- fix(parallel): apply combine_under_n_chars to large PDFs split across workers

Asked by pk, DM, https://unstructuredai.slack.com/archives/D0BFPFGDNP7/p1791402891397499
Author: CyMule (Nick Franck). Reviewed at head 16dd5502bc565e25feb586b094e7d02ba03198f3, base main c80d906 (equal to origin/main, no base skew).

**Verdict:** GO
**Blast radius:** 2/5

In parallel mode, `partition_file_via_api` forwarded the library kwarg `combine_text_under_n_chars` to the worker form. The worker form only accepts `combine_under_n_chars`, so it dropped the value and fell back to the default. The PR renames the key on the outbound worker request only. The local small-PDF path still passes the library name to `partition`. Every leg found the fix correct. The new test passes on head and, per the fable leg, fails on base (`assert 1 == 2`); CI `test` is green and ran both new tests. Callers who set the threshold on large PDFs in parallel mode will now get different chunk output. That is the intended fix, and the CHANGELOG discloses it.

## Notes

- Version collision (observed): unstructured-api 589 (https://github.com/Unstructured-IO/unstructured-api/pull/589, "fix(parallel): keep the worker's error status when its error isn't JSON") and unstructured-api 592 (https://github.com/Unstructured-IO/unstructured-api/pull/592, "fix: end multipart responses with the closing boundary") are open and also add `## 0.1.12` and `__version__ = "0.1.12"`. Whichever merges second must re-bump. unstructured-api 581 already claims 0.1.13. This is a fact about the merge queue, not a defect in this PR.

## Findings

None at BLOCKER or CONCERN.

- NIT: `prepline_general/api/general.py:149` `worker_kwargs = partition_kwargs.copy()` is redundant. `**partition_kwargs` is already a fresh dict on each call, and `partial` re-expands it for each `executor.map` item. The old in-place `starting_page_number` write never reached the caller. The test's `assert options[...]` lines were already true on main. Harmless.

## Dismissed

- blast-radius rationale, "stops mutating the caller's kwargs / changes starting_page_number mutation semantics": wrong. The old code mutated only the per-call kwargs dict (see the NIT), so no behavior changed there.
- fable candidate, "other library-vs-form names are still forwarded wrong": refuted, and confirmed by my own check of `general.py:357-381` against `form_params.py:15-40`. Every forwarded key matches a form field except `extract_image_block_to_payload`, which the worker re-derives from `extract_image_block_types` (`general.py:321`). `ocr_languages` goes out as an `"eng+fra"` string and is re-joined to the same value on the worker. This is pre-existing code, outside this diff.
- fable candidate, "an older worker image regresses": refuted. `combine_under_n_chars` is already a field on the worker form, and the old key never was.
- Value encoding of 0 and None: `requests` multipart encoding sends `0` as `"0"` and drops `None`. An unset threshold therefore still reaches the worker as the form default `None`, the same as before.
- review-graph below-bar 581 / 589 / 592 "shared file general.py": each touches a different function in that file. review-graph reported no same-function or merge-tree conflict pair. The only overlap is the version bump, covered in Notes.

## What each leg said

- blast-radius: 2/5, "glance". The rollback is a clean revert. The wire change is limited to one outbound form field that workers already accept.
- cheshire: 0 flags, SIMPLE, no slop, no doc updates implied. It ran on the packaged starter config ("no repos configured") and without AGENTS.md, so its grounding is degraded. Briefing: /Users/pk/.cheshire/briefings/Unstructured-IO-unstructured-api-591.md
- collisions (review-graph push run): `complete: true`, 3 candidates checked, 0 pairs, gh budget 3/25. Its note says 5 open PRs were excluded as candidates by the exclude hook.
- fable (strong-review, report-only, no PR marker): SAFE TO MERGE, no actionable findings. It synced the test group in the scratch checkout and observed both tests pass on head. With `general.py` swapped to base, it observed `test_parallel_worker_honors_nondefault_combine_threshold` fail on `assert 1 == 2`. The bundle's diff omitted the new test file, but fable read the file from the checkout.
- codex (`codex exec review --base origin/main`): no introduced bug. Tests were not run because its read-only sandbox blocked the uv cache.
- ruff 0.15.12 (version from uv.lock): the repo's `check-src` and `check-tests` commands pass, with format and lint clean. `general.py` is excluded by the `[tool.ruff] exclude` config. Run against that file by explicit path, ruff shows the same 4 lint issues and 1 format hunk on main as on head, so the PR adds none.
- CI (`gh pr checks`, 2026-10-07T20:27Z): all pass, including `test` (145 passed; both new tests PASSED in the log), `test_dockerfile`, `lint` and `changelog`. The `claude` check was skipping. The CI-side cubic check passed.
- Not run: the gpt-pro / oracle strong-review leg (it needs a browser on a shared CDP port, so it was excluded from this unattended run). Local cubic was not run (disabled for this run). test-critic and marmalade were not run (outside the requested scope).

## Not covered

- No end-to-end run against a real multi-page PDF through a real parallel worker deployment. The proof is the in-process FastAPI worker test, plus CI.
- I did not run pytest myself. The red/green result above is the fable leg's observation, and the pass on head is also visible in CI.
- No second model-family review (the oracle leg was excluded). test-critic and marmalade were not run.

@CyMule
CyMule merged commit c2f336e into main Oct 7, 2026
14 checks passed
@CyMule
CyMule deleted the fix/parallel-combine-option branch October 7, 2026 23:22
CyMule added a commit that referenced this pull request Oct 7, 2026
## What

List parameters sent as comma-separated values are now split into
separate items.

```
Before: languages=eng,deu → ["eng,deu"]
After:  languages=eng,deu → ["eng", "deu"]
```

This applies to every list form field, including `ocr_languages` and
`skip_infer_table_types`.

## Why

The parser was meant to split on `+` or `,`. A typo meant it always
picked `+`, so a comma list was passed through as a single invalid
language. JSON arrays, `eng+deu`, and repeated fields work as before.
When a value contains both `+` and `,`, `+` still wins.

## Validation

Tests go through a real FastAPI form for three list fields. The comma
cases fail on `main`. Adds a CHANGELOG entry and bumps the version to
0.1.14.

Refreshed against main after #591 merged. The non-retryable worker test
now checks one attempt per PDF split, avoiding an assumption about
executor scheduling. Targeted form-list, worker-error, and
parallel-option tests: 40 passed.
CyMule added a commit that referenced this pull request Oct 8, 2026
## What

`multipart/mixed` responses now end with the closing boundary line that
the MIME format requires.

```
Before (end of response):   After (end of response):
<last part data>            <last part data>
                            --3f9c...--
```

## Why

Without the closing line, strict multipart parsers report the response
as incomplete. If processing fails partway through, the closing line is
still left out, so clients can tell a cut-off response from a complete
one.

## Validation

Tests parse the response with Python's `email` parser (no defects) for
one part, several parts, and no parts, and cover an interrupted stream.
They fail on `main`. Adds a CHANGELOG entry and bumps the version to
0.1.15.

Refreshed against main after #590 and #591 merged. Targeted API tests:
43 passed; response compatibility tests: 4 passed. Changed-source lint
passed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants