Skip to content

[spring-ai] Streaming responses ending with CJK punctuation (。!?) are misclassified as partial and never persisted to the session #1608

Description

@caps-xia

Describe the Bug

In the Spring AI bridge (contrib/spring-ai), MessageConverter#isPartialResponse
decides whether a streaming chunk is partial by checking only ASCII sentence-ending
punctuation: ., !, ?, \n. CJK terminal punctuation (。, !, ?) is not
included.

As a result, when a model's final streaming response ends with a Chinese sentence
mark (which is the normal case for Chinese users), it is classified as partial=true
and therefore never persisted to the session — the reply silently disappears from
conversation history, breaking multi-turn context and session replay.

Steps to Reproduce

  1. Use the official com.google.adk.models.springai.SpringAI model bridge with any
    OpenAI-compatible model, streaming mode (RunConfig SSE).
  2. Send a question that produces a Chinese answer, e.g. 根因是库存扣减为 0 未拦截。
  3. Observe the final LlmResponse: partial is true, and the session event store
    never receives this response.

A minimal unit-level repro (calling the converter directly) is provided below.
No exception/stacktrace is produced — the failure is silent, which is what makes it
hard to notice in production.

Expected Behavior

A response ending with 。 (or ! / ?) is a complete final response in Chinese
and should be partial=false, persisted to the session like its ASCII-. counterpart.

Observed Behavior

Verified against google-adk-spring-ai:1.9.0 on our classpath:

[PROBE] chinese-ending partial = true    <-- bug: final reply treated as partial
[PROBE] ascii-ending   partial = false

Downstream effect: Runner-driven sessions lose the assistant's final answer for
CJK conversations; Session.events() replay shows the question without the reply.
Live streaming looks fine (partials are forwarded to the client), so the loss only
shows up in history, multi-turn follow-ups and session replay.

Environment Details

  • ADK Library Version: verified on 1.9.0, and confirmed still present in
    1.11.0 (latest release) and on current main
    (contrib/spring-ai/src/main/java/com/google/adk/models/springai/MessageConverter.java#isPartialResponse)
  • OS: macOS (behavior is OS-independent — pure string logic)
  • TS Version: N/A (Java)

Model Information

Model-independent. Observed with GLM via an OpenAI-compatible gateway; the
classification happens in MessageConverter before any model-specific handling.

Regression

N/A — the heuristic has been present since we started using the bridge (1.9.0),
and is unchanged through 1.11.0.

Additional Context

Suggested fix, from minimal to better:

  1. Add CJK terminal punctuation to the check: 。!? (plus …, )).
  2. Better: don't infer completeness from punctuation at all. Text legitimately ends
    without sentence-ending characters (lists, code blocks, tables). Partial-ness
    could be derived from chunk position / stream state instead of the last character.

This affects every CJK-language user of the official SpringAI bridge — and because
partials are still forwarded live, the missing persistence only surfaces later
(history / follow-up turns), which makes it easy to misdiagnose as a session-store
problem.

Minimal Reproduction Code

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.adk.models.springai.MessageConverter;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;

import java.util.List;

MessageConverter converter = new MessageConverter(new ObjectMapper());

ChatResponse chineseEnding = new ChatResponse(List.of(new Generation(
        AssistantMessage.builder().content("根因是库存扣减为 0 未拦截。").build())));
ChatResponse asciiEnding = new ChatResponse(List.of(new Generation(
        AssistantMessage.builder().content("Root cause: zero-stock deduction is not blocked.").build())));

boolean chinesePartial = converter.toLlmResponse(chineseEnding, true).partial().orElse(false); // true  (bug)
boolean asciiPartial   = converter.toLlmResponse(asciiEnding, true).partial().orElse(false);   // false

How often has this issue occurred?

Always (100%) — every Chinese final response ending with 。, which is essentially
every conversation.

Activity

  1. self-assigned this
    on Oct 5, 2026
  2. added theissue type on Oct 5, 2026
  3. caps-xia commented on Oct 5, 2026

    @caps-xia
    Author

    Thanks for triaging, @hirematha!

    I hit this in production and currently work around it with a custom BaseLlm bridge that aggregates the final response itself, so I have solid reproduction coverage. Happy to contribute a PR unless one is already in progress — I'd just like to align on direction first:

    1. Minimal fix — add CJK terminal punctuation (。, !, ?) to the check. Low risk, but it stays a locale-dependent heuristic: any script whose punctuation isn't in the list remains broken.

    2. Root fix — stop inferring "partial vs final" from punctuation altogether: stream chunks as partials, and emit the aggregated response as the single final event on stream completion. Locale-independent by construction, and mostly just wiring — StreamingResponseAggregator#getFinalResponse() already builds exactly this event, but the streaming path never calls it (the completion callback only calls emitter.onComplete()). I'd avoid anchoring the end detection on finishReason instead: it differs across providers (stop/end_turn/STOP), tool-call turns use tool_calls, and some OpenAI-compatible gateways drop it entirely.

    My preference is 2, with 1 fine as a short-term patch. Would you be open to a PR for either? (Happy to cover persistence-timing and error-path details in the PR description.)

  4. added
    waiting on reporterWaiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale.
    on Oct 6, 2026
  5. hirematha commented on Oct 6, 2026

    @hirematha

    Hi @caps-xia, thank you for reporting this issue, I have been able to reproduce it using your inputs. It would be great if you could contribute a robust fix to address this issue.

  6. added 5 commits that reference this issue on Oct 6, 2026
    4c8aaee
    689154e
    47b7c41
    8faafcc
    6dd8e0d
  7. hirematha commented on Oct 8, 2026

    @hirematha

    Hi @caps-xia, thank you for your contribution and quick fix, I have verified through local testing that your fix addresses the issue.
    Currently, the issue and the associated PR are under review by our team and we will keep you updated if any additional information is required. Thank you.

  8. added and removed
    waiting on reporterWaiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale.
    on Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions