Content comes from the middle json and its paired markdown; the content list is deliberately ignored because it escapes markdown characters. The external-data exception boundary covers both reading and structure parsing so no native exception escapes the OCR client. The layout parser is unchanged.
The service dispatches on the upload filename extension. A PDF sent as image.jpg made /ocr/text return a success response with hallucinated text. Only PDFs are relabelled; everything else keeps the legacy image label.
OcrDocumentElement extends the layout element with text; OcrDocumentResult stays independent because the parent's defaulted call_stats forbids a required markdown field. OcrDocumentTransport is a separate Protocol so existing OcrTransport implementers keep passing isinstance.
Ten tasks covering types and ports, upload sniffing, package parsing, rejected-input classification, transport, client, registration, live checks, docs and verification.
Adds live-service and server-source evidence, structure contract, exception and resource boundaries, corrected breaker analysis, and the Codex review record.
Root-cause analysis from live MonkeyOCR probes: upload label is not detected, output carries no content, and deterministic bad-input 500s are treated as transient. Proposes parse_document with content-sniffed upload type and middle.json-only extraction.
- fidelity cacheability also requires a non-empty projected body, so
think-tag-only answers are no longer cached
- a no-argument tool sent with arguments null only is a deterministic
dialect: ResultInvalid tool_arguments_null instead of retried
- text mode treats choice index null as 0 and no longer lets False pose
as 0
- null-only tool_calls elements no longer produce an empty tool_calls
list; non-stream fidelity checks role before the verdict; warning
paths are length-bounded
- docs: ARCHITECTURE 7.8 field counts, README cache/watchdog wording,
CHANGELOG notes, review record in the design, hook note in the plan
Red: 8 new tests failed before this change (e.g.
test_openai_cacheable_requires_projected_body,
test_null_arguments_tool_is_result_invalid_stream,
test_text_mode_choice_index_null_is_zero[None-True]).
CHANGELOG 1.3.10 (unreleased) with behaviour-change and breaking
sections, README capability rows and a 1.3.10 section, ARCHITECTURE
section 15 plus 6.2/7 updates, schema doc for the stream_end column,
migration impact notes, ROADMAP entry, .env.example MISSING_DONE
semantics, and a revision pointer on the 1.3.9 design. Follow-ups
filed as #32 and #33.
llm_calls gains a nullable stream_end TEXT column (auto backfill,
manual-mode trimming as in 1.3.8). TelemetryRecorder.record_llm_call
takes a new required keyword stream_end; the emitter reads it from the
response on attempt rows and writes NULL for failed, cache-hit and
terminal rows.
Red: 18 tests in tests/unit/test_telemetry.py and test_ports.py failed
against the pre-change source (missing column, 38-field signature,
stream_end absent from emitted rows).
Cacheable is now a strict subset of deliverable: salvaged streams,
content-less results and length/filter/unknown finishes are delivered
but never written, and the read path applies the same verdict to the
stored values before hit overrides (invalid stream_end or non-string
finish read as a miss; 1.3.9 fidelity entries fall back to the
completion's finish). Hits reset stream_end. The legacy
_has_completion_output helper is folded into _is_cacheable.
Red: 13 tests in tests/unit/test_cache.py failed before this change
(uncacheable results were written and replayed; new fields not reset).
Both transport paths now share one verdict: a stream is complete on
[DONE] or on finish_reason followed by measured usage and a clean EOF
(both modes; missing_done only governs real truncation). Outcomes follow
the design's first-match table: upstream_error finishes stay transient,
fidelity mode delivers reasoning-only/length results, text mode raises
ResultInvalid for content-less length/filter endings, and non-stream
fidelity judges before building the snapshot so truncated tools reach
tool_truncated. Projections honour the reasoning alias, dialect warnings
are bounded, and responses carry finish_reason and stream_end.
Red: 39 of 46 tests in tests/unit/test_completion_transport.py failed
before this change (e.g. eof_after_finish raised 保真响应缺 [DONE];
reasoning-only length raised empty_completion; reasoning-only stream hit
the TTFT watchdog, re-confirmed by disabling alias liveness).
Null is treated as absent at every layer; unknown delta, tool and
function fields merge by OpenAI streaming conventions (strings concat,
label keys idempotent, indexed arrays by index) and conflicts drop the
key and record its path instead of failing the call. The accumulator
now tracks finish and usage-after-finish in both modes, exposes the
reasoning alias, and non-stream parsing splits shape checks from
snapshot construction.
Red: 20 of 47 tests in tests/unit/test_openai_completion.py failed
before this change (e.g. test_null_known_and_unknown_fields_are_absent
raised TransientError 未知 delta 字段; finish_seen/raw_message missing).
Pure functions in types.py classify finish reasons, judge stream
completeness and outcome, and decide cacheability; TransportResult and
LLMResponse gain finish_reason and stream_end; snapshots accept
tool_calls null.
Red: tests/unit/test_completion_verdict.py failed at collection
(ImportError: _classify_finish et al. undefined) before this change.
Whole-tree radon blocking let 16 legacy C+ functions block every commit,
including docs-only ones. Touched functions still block; legacy debt is
reported as a warning.
Add faithful single-choice chat results, tool stream activity, error-frame retries, and isolated protocol caching.
Reject empty cached completions with the same value predicate as transport, add regression coverage, and adopt Apache-2.0.
_minimal_fields and the pre_135 acceptance dicts missed the two new
columns, KeyErroring against migrated 39-column physical tables
(caught by the pre-merge integration gate; production path is
signature-gated and unaffected). Verifier gaps P2-1/P2-2 closed:
full suite 1764 passed, PG 30 passed, lint green.
HEDGE__STALL_AFTER_S triggers a cross-source hedge when the primary
leg stalls mid-stream (independent of HEDGE__AFTER_S); two-phase
hedge window with event recheck discipline; watchdog-killed primary
converges on the surviving hedge leg. STREAM__STALL_THRESHOLD_MS
(1000ms default) adjustable via env/settings/constructor. Assembly
guards: ValueError on never-opening windows, embedding/OCR scope
warning (B4). Batch D 14 tests red-to-green, 1688 unit+contracts.
Third approved port change: TelemetryRecorder.record_llm_call gains
stall_ms/stall_count (assembly gate derives from this signature).
Schema five edit points (both DDLs, both backfill decls, COLUMNS)
with INTEGER nullable columns appended last; telemetry_schema_sql
regenerates from the same constants untouched. Emitter forwards the
pair from retry._emit on the success branch only (cancelled hedge
loser rows stay NULL by construction); cache-hit and terminal rows
pass explicit None. Both **fields recorders pick the columns up with
zero signature change.
CallStats gains stall_ms/stall_count (None = no stall observation);
winner sample triple (wall, stall_ms, stall_count) converted once in
_attempt success branch; net = max(0, wall - stall) converged into the
shared _record_winner_sample used by both the single-path guard and
hedge winner registration. Two-fail hedge rounds record nothing
(no winner to attribute). Design section 4.2; issues #27.
B4: assembly-time non-streaming hedge warning operationalized to
embedding/OCR scopes only (chat stream is per-call, no assembly
signal); AFTER_S same-gap backfill included. Plan T1-T5 with real
grep sync lists and fail-first test batches.
B1 chunk = any delta (content or reasoning), B2 telemetry plumbing
via explicit _emit pass, B3 orchestration entry gate for independent
stall hedging; N1-N9 advisory fixes merged. Human decisions recorded
in design section 9.
Findings: industry paradigm research (watchdog-retry is the norm,
mid-stream hedging has no public precedent; pi-tps threshold-based
stall deduction; Gregg off-CPU paired reporting; vLLM exclusion
clauses). Design: generation_ms redefined as net generation time
(wall minus all inter-chunk stalls over threshold), CallStats gains
stall_ms/stall_count, HEDGE__STALL_AFTER_S mid-stream hedge
independent of HEDGE__AFTER_S. Issues #27#28.
Address branch review findings 1-4 on feature/1.3.7-hedged-requests:
- src/polygateway/middleware/retry.py: recheck primary.done() after
hedge admission in _attempt_hedged; release the hedge permit via
settle_and_release(permit, 0) (release_probe for probe entries) and
adjudicate the primary directly instead of firing a billable HTTP
request that would be cancelled immediately
- src/polygateway/config.py: check_hedge_assembly raises a hedge-located
ValueError for empty sources instead of a bare min() error
- tests/unit/test_hedge.py: pin that an injected FakeClock jump of 10^6
seconds does not trigger hedging (design section 8); pin pick(exclude)
counting no gate_rejections and leaving reasons untouched; pin silent
hedge abandonment when the candidate circuit is open; deterministic
regression for the admission-window race (BlockingLimiter harness)
- tests/unit/test_config.py: assert the empty-sources guard message
locates the hedge key
Red-to-green evidence in tests/outputs/137/review-fixes/
CallStats gains hedges/generation_ms/hedge_won (all defaulted, appended
after total_latency_ms); _CallContext counts them via record_generation
(overwrite for chat/OCR, accumulate for embedding batches) and
register_hedge, and snapshot carries them out. All three _attempt
implementations time only the transport call itself on the same injected
clock as total_latency_ms; the chat sink is recorded by the orchestrator
so hedge winner attribution stays with T3. Hedge counters stay 0/False
until the T3 orchestration lands.
Red-green evidence: tests/outputs/137/t2/ (10 new tests AttributeError
red, then green; full unit+contracts 1621 passed).
Transport.complete gains the keyword-only first_token_event (no default,
per the port convention): streaming sets it on the first delta, the
non-streaming path accepts it but never sets it, None means the caller
does not observe the first token. All fake/wrapping transports and the
three direct call sites follow the signature; the e2e wrapper forwards.
Red-green evidence: tests/outputs/137/t1/ (batch A TypeError red, then
147 file tests + 1550 unit tests green).