0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-07 13:19:19 +08:00
discourse/plugins
Rafael dos Santos Silva 87775447c0
FIX: Silent truncation and corruption of AI structured output streams (#41716)
Reported at
https://meta.discourse.org/t/translation-silently-truncated-when-json-stream-parsing-breaks-no-error-raised/407251

## The bug

Some providers stream structured output whose string values were
unescaped by an outer JSON parse, so real newlines appear inside string
values. When that happened, `JsonStreamingTracker` had two failure
modes:

- It marked the stream broken and `StructuredOutput` fell back to
`BestEffortJsonParser`, whose extraction regex (`[^"]+`) cut the value
at the first escaped quote and left `\n` sequences as literal text. A
2,000-char translation could come back as ~50 chars, cut right before
the first quoted word — exactly what the report shows.
- Its escape-and-resume hack (`String#dump` + buffer-growth offset)
miscomputed the resume index whenever the chunk contained non-ASCII,
quotes, or backslashes, silently duplicating or corrupting content
**without ever marking the stream broken**.

Testing a realistic corrupted payload across chunk sizes 1–60: 31
produced the truncated fallback, 27 produced silent corruption, 1
raised, and only 1 came out correct. Either way the result was persisted
as a successful translation with nothing in the logs.

## The fix

Replace the hand-rolled parsing with two gems and keep only glue:

- **json_completer** (pure Ruby): `JsonStreamingTracker` now feeds the
cumulative buffer — with control characters re-escaped — to an
incremental, truncation-tolerant parser and notifies consumers of
changed keys. The corrupted payloads above stream correctly at every
chunk size, so the broken-stream path is only reached for responses that
aren't JSON at all.
- **smarter_json**: `BestEffortJsonParser` becomes a three-attempt chain
(strict-with-completion → control-chars re-escaped → lenient) covering
the quirk shapes the old regexes handled: single quotes, unquoted keys,
markdown fences, prose-wrapped JSON.

This deletes the vendored 668-line SAX parser, the resume hack, and all
manual regex extraction (net −694 lines), and adds a log warning
whenever a response falls back to best-effort parsing.

## Behavior changes

- Scalars now stream progressively: mid-stream
`read_buffered_property(:number)` returns the digits buffered so far
instead of `nil`. Consumers act on final values, so this only affects
mid-stream reads.
- Arrays of objects stream partial objects mid-stream instead of
returning `nil` until finish.
- A trailing comma in an array reads as a `nil` placeholder slot until
the next element arrives.
- Partial tool calls surface a few more progressive updates (the openai
endpoint spec count moved 128 → 134); values still only ever grow.

## Tests

- Regression specs for the report: unescaped control characters with
escaped quotes/emoji streamed across chunk boundaries, fenced +
unescaped responses, truncated JSON, numeric casting.
- 916 examples green across `completions/`, `translation/`,
`modules/ai_helper/`, and `utils/`.
2026-07-15 12:00:24 -03:00
..
automation I18N: Update translations (#41653) 2026-07-13 17:38:52 +02:00
chat FIX: Refresh chat reviewable payload when re-flagging an edited message (#41739) 2026-07-15 11:55:13 -03:00
checklist
discourse-adplugin I18N: Update translations (#41653) 2026-07-13 17:38:52 +02:00
discourse-affiliate
discourse-ai FIX: Silent truncation and corruption of AI structured output streams (#41716) 2026-07-15 12:00:24 -03:00
discourse-apple-auth
discourse-assign FEATURE: schema based workflow (#41684) 2026-07-15 13:22:29 +02:00
discourse-cakeday DEV: Remove queryAll uses in core tests (#41324) 2026-07-01 11:55:39 +01:00
discourse-calendar A11Y: ensure post avatars are hidden when paired with usernames (#41686) 2026-07-14 07:04:22 -04:00
discourse-chat-integration
discourse-data-explorer DEV: Make AI context compaction global (#41317) 2026-07-14 18:41:37 -03:00
discourse-details DEV: Drop unused decorateCooked from discourse-details (#41426) 2026-07-03 17:18:20 +01:00
discourse-gamification I18N: Update translations (#41584) 2026-07-12 17:29:05 +02:00
discourse-github UX: Name the failing repository in GitHub setting validation errors (#41738) 2026-07-15 15:46:18 +02:00
discourse-graphviz
discourse-hcaptcha DEV: Block external requests in system specs via Chrome DNS resolver (#40685) 2026-06-17 12:29:57 +01:00
discourse-lazy-videos
discourse-local-dates I18N: Update translations (#41257) 2026-06-29 15:30:28 +02:00
discourse-login-with-amazon
discourse-lti
discourse-math
discourse-microsoft-auth
discourse-narrative-bot
discourse-oauth2-basic
discourse-openid-connect
discourse-patreon DEV: Drop jquery in patreon plugin (#41425) 2026-07-03 17:13:54 +01:00
discourse-policy
discourse-post-voting SECURITY: Reviewable conversations expose post-voting flag PM excerpts to category moderators outside the PM (#41527) 2026-07-07 15:37:22 -05:00
discourse-presence SECURITY: Authorization bypass in wiki edit presence leaks editor identities (#41474) 2026-07-06 12:53:23 -05:00
discourse-reactions DEV: add upcoming change permanent banner metadata (#41700) 2026-07-15 11:20:02 +04:00
discourse-rewind DEV: Bump eslint/lint-configs, and correct violations (#41321) 2026-07-01 10:39:57 +01:00
discourse-rss-polling I18N: Update translations (#41653) 2026-07-13 17:38:52 +02:00
discourse-solved DEV: add upcoming change for Solved badges (#41613) 2026-07-15 18:15:07 +04:00
discourse-subscriptions UX: Include full state names where previously truncated (#41713) 2026-07-14 22:49:12 -04:00
discourse-templates FIX: Support unloaded categories in template topic options (#41230) 2026-06-29 14:35:07 +08:00
discourse-topic-voting FIX: Show the voting notification prompt when vote limits are disabled (#41731) 2026-07-15 14:54:37 +02:00
discourse-user-notes
discourse-workflows FEATURE: adds user logged in trigger to workflows (#40633) 2026-07-15 16:19:23 +02:00
discourse-zendesk-plugin
footnote UX: prevent nested footnotes (#41325) 2026-07-02 19:55:10 +02:00
poll UX: chart legend styling (#41694) 2026-07-14 11:18:42 +02:00
spoiler-alert FIX: Allow re-blur on spoiler when in details view (#41266) 2026-06-29 12:47:49 -05:00
styleguide DEV: Minor fixes to everyone-based permissions for granular_anonymous_and_logged_in_groups_permissions (#41459) 2026-07-07 09:38:56 +10:00