Skip to content

core(openaiContentGenerator): protocolTagSanitized has no reader on the non-streaming route, so a suppressed trailing tag is neither logged nor counted #13660

Description

@yiliang114

Summary

RequestContext.protocolTagSanitized has exactly one reader — pipeline.ts:746, inside processStreamWithLogging — so it only ever produces a ProtocolTagSanitizedEvent and a debugLogger.warn on the streaming route. Since #13600 the field is also written on the non-streaming route, where nothing reads it: pipeline.execute() returns without touching the context, so a trailing orphan thinking tag is deleted from the model's answer with no event and no log line.

Source: review thread R2-2 on #13600. The count half of that finding was fixed in 7df8f5c4322ea63a2cd2c054729daee4687a613e; the reader half is filed here because fixing it means adding an emission site to execute(), beyond that PR's per-finding repair ceiling.

The route is live

convertOpenAITextToParts (converter.ts:1249) gates the trailing-tag filter on responseParsingOptions?.contentOnlyThinkingTagLeaks, and provider/default.ts:275 returns contentOnlyThinkingTagLeaks: true for every model. getResponseParsingOptions is not streaming-gated, and taggedThinkingParser is built only when streaming — so createRequestContext(request, isStreaming=false) still carries the flag.

convertOpenAIResponseToLlm is called from pipeline.ts:517, inside execute(). Real non-streaming call sites include hooks/promptHookRunner.ts:328, baseLlmClient.ts:557 and :732, client.ts:5729, and cli/src/serve/hosted-hook-model.ts:74.

Before that PR the field had exactly one producer and it was streaming-only, so this gap is new rather than pre-existing.

Measured

# pipeline.execute, non-streaming, content 'Answer.\n</thinking>', finish_reason 'stop'
BASE 464f486e20 : returnedParts=[{text:"Answer.\n</thinking>"}]  contextProtocolTagSanitized=(absent)                     sanitizedEvents=[]
PR   3179bf197f : returnedParts=[{text:"Answer."}]              contextProtocolTagSanitized={"tagName":"thinking","toolCallCount":0}
                                                                        sanitizedEvents=[]   <- logProtocolTagSanitized never called

A second-order consequence of the same missing reader: the toolCallCount backfill added in 7df8f5c432 lives in convertOpenAIChunkToLlm, so a non-streaming response that carries functionCall parts (built at converter.ts:1428-1444) still stamps toolCallCount: 0.

Proposed scope

Hoist logPendingProtocolTagSanitized out of processStreamWithLogging into a pipeline-level helper and call it in execute() after convertOpenAIResponseToLlm, passing then clearing context.protocolTagSanitized, so one emission site serves both routes. Add a pipeline.test.ts case running pipeline.execute against a mocked non-streaming completion whose content ends in an orphan closer and asserting logProtocolTagSanitized was called once with tagName: 'thinking'. The harness already exists: pipeline.test.ts:46 imports it, :83 mocks it, and :2513/:2539 assert its call count for the streaming route.

Facts a fix must not violate:

  • RequestContext['protocolTagSanitized'] declares toolCallCount as required, not optional (types.ts:128-131), so it cannot be dropped to express "unknown".
  • telemetry/loggers.ts:972 renders the count into a sentence ("preserved N tool call(s)"), so a shared emission site must not pass a count that contradicts the response's own functionCall parts.
  • pipeline.ts:746-749 reads and clears the field immediately after each convertOpenAIChunkToLlm call, and pipeline.ts:799-804 parks it only on a finish-reason response; a deferred stamp must land inside the same call that carries finish_reason.

Related family-level decision: #10559

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    category/coreCore engine and logicpriority/P3Low - Minor, cosmetic, nice-to-fix issuesscope/content-generationAI content generationstatus/blockedBlocked by external dependencytype/bugSomething isn't working as expected

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions