Skip to content

refactor(translate): translate returned streams through internal SSE IR - #594

Open
Menci wants to merge 91 commits into
mainfrom
feat/fit-ir-sse
Open

Menci wants to merge 91 commits into
mainfrom
feat/fit-ir-sse

Conversation

@Menci

@Menci Menci commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Returned generation streams use a private Fit IR inside packages/translate. Three upstream readers and four client writers compose the nine existing A via B routes. Native adapters and usage mappings remain under each protocol's as-target / as-source; IR is not exported from the translate package.

Readers map native events directly to self-describing IR records. initialize creates the minimal terminal-valid structure and starts item/part lifecycle, assign carries complete non-append values, and append carries string fragments only for statically declared fields. IRNone distinguishes a determined absent call ID from a value that may still arrive. Response failure uses the error event; finish only represents completed or incomplete generation. Records stream directly without a batching/atomicity wrapper.

The schema is the single source of truth for legal paths, optional/nullable value shape, append and IRNone traits, and choice/item/part lifecycle units. Its inferred value type is checked against IR at compile time. The accumulator owns plain copy-on-write state, validates operation classes and closed lifecycles, and exposes narrow reads for native snapshots and pending opens.

Writers react to the current record and retain state only for facts their target receives out of sync: ordered pending opens, generated identifiers, native ordinal mapping, emitted annotation progress and buffered complete tool calls. Native done and terminal snapshots are constructed from the accumulator only when emitted. There is no protocol namespace, publication index, filtered projection, rebuild-then-diff reader, writer-side scan to re-derive changes, or growing writer copy of IR content.

Responses done snapshots use per-field reconciliation: empty snapshots do not erase streamed content, equal values are ignored, append targets may add a missing suffix, non-prefix text throws, and assign fields may only be supplied once or match. Image generation items allocate independently at output_item.added and receive their image part at done. Late ChatCompletions citations are clamped to text that remains open after nonstandard item interruption; closed-only spans are warned and dropped. Lazy direction-specific coordinate indexes follow later text appends and preserve split surrogate boundaries. Responses delta logprobs append at consecutive IR array indexes before terminal reconciliation, nullable native annotation indexes compact into contiguous IR arrays, and closing a lifecycle unit validates every required schema field. GenerateContent completion tracks every observed candidate independently, so one finished candidate cannot terminate later candidates or usage.

The complete design evolution—from ChatCompletions reasoning dialects and the discarded Fat IR direction through Fit IR and the stacked sidecar/IAT/thin-turn design—is recorded in the design archaeology. Detailed retained evidence covers accounting/policy, final performance, citation coordinates, ChatCompletions reasoning aliases and mixed deltas, and real ChatCompletions producer deviations.

Authenticated sidecars, replay checks and complete assistant-turn round trips remain scoped to dependent draft #599. Thin references remain scoped to its dependent draft #624.

Validation on current head 451205fb7: all five PR checks passed (lint, workspace/root typechecks, generated assets, web build, and 683 Vitest files / 7,976 passed tests with 3 expected skips). Focused local validation also passed the Gemini protocol/gateway suite, Responses snapshot-policy tests, all three owning TypeScript projects, and changed-file lint. The linked performance workload and results apply to the reviewed 291a728fc base.

Menci added 3 commits October 10, 2026 07:20
Introduce fit IR assign/append batches and lifecycle events with three inbound and four outbound SSE adapters. Preserve string boundaries and report final projections for a standalone IAT and per-protocol CBOR thin item implementation.

Retain citation coordinate experiments and distinguish measured Responses code-point offsets from the provisional ChatCompletions interpretation. Cover incremental delivery, interleaving, Unicode ranges and lossless thin restoration in 34 tests.
Complete ordered image and text projection, finish IAT callbacks before terminal events, and preserve audio replay metadata and usage details. Keep selective thin references aligned with protocol item discriminators and retain unsafe JSON integers and original argument spelling. Add regression coverage for terminal consumption, pending images, JSON restoration, and invalid state propagation.
Centralize IR lifecycle traversal and defer audio classification until its streamed fields are known. Preserve generated image formats and out-of-range numeric lexemes, and keep empty text from claiming projection targets. Expose completed unnamed tools and invalid logprob ownership. Extend the regressions to 66 focused tests.
Menci added 5 commits October 10, 2026 16:30
Preserve the full JSON wrapper as freeform tool input when its input field is absent or not a string, so clients can decide how to handle the call. Log the extraction failure without logging tool contents.
Replace nine pairwise return mappers with three native readers and four native writers inside translate. Keep request builders and public contracts unchanged, preserve source item identity and native error/refusal semantics, and keep IAT/thin utilities out of this change for a stacked draft.

Move stream coverage to native async pipelines and cover the confirmed signature, refusal, custom input fallback, usage and terminal policies.
…pletion

Keep structured ChatCompletions reasoning groups and source identities through late compatibility carriers. Preserve surrounding message order and add post-finish regression coverage. Remove test-only IR transport plumbing and redundant terminal checks after the private integration cleanup.
Use native response start and explicit choice completion in request-translation fixtures. Expect the upstream classifier explanation unchanged in translated Responses policy failures.
@Menci

Menci commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner Author

Codex: citation-coordinate investigation and probe evidence.

Citation coordinate evidence

In A via B, A is the client protocol and B is the upstream protocol; requests flow → and responses flow ←.

Current interpretation

Protocol Coordinate referent Unit End convention Evidence status
ChatCompletions Generated message text Unicode code point, provisionally Exclusive Real captures exclude UTF-8 bytes and establish half-open ranges; code point versus UTF-16 remains an inference.
Responses Generated output text Unicode code point Exclusive Direct Copilot gpt-6-luna probes distinguish all three units using supplementary characters. Applies to the tested URL citation paths.
Messages Source document text for char_location citations Unicode code point Exclusive A captured response and its source document distinguish code points from UTF-16 with 📖.

The IR coordinate unit is UTF-16 code units, preserving JavaScript string contents including lone surrogates. Converting an upstream code-point range into IR therefore requires calculating its corresponding UTF-16 offsets. The IR range type is IRUTF16TextRange.

ChatCompletions' code-point unit remains inferred. Responses' measured behavior is limited to the tested provider and URL-citation paths.

Direct Copilot experiments

The probes used $probing-copilot against the token-advertised Copilot endpoint, with provider authentication headers, without traversing Floway.

The experiments ran on 2026-10-10 (Asia/Singapore). HTTP /models returned 200 and listed the exact requested model IDs. The observations below retain selected response fields, not complete raw HTTP recordings. Missing request or response details must not be reconstructed as if recorded.

Retained Responses request

The successful Unicode-prefix probe used this exact body at /responses:

{
  "model": "gpt-6-luna",
  "input": [
    {
      "role": "user",
      "content": [
        {
          "type": "input_text",
          "text": "Use the built-in web search tool now to find the official Unicode character name for U+1F4D6. Begin the final answer exactly with the literal prefix A😀中B followed by one space. Then state the code point and official Unicode name in one short sentence, and include the literal symbol 📖 within that factual sentence. Do not type a URL, footnote, bracketed citation, or citation JSON; let the API provide server-generated source annotations."
        }
      ]
    }
  ],
  "tools": [{ "type": "web_search", "search_context_size": "low" }],
  "tool_choice": "required",
  "include": ["web_search_call.action.sources"],
  "reasoning": { "effort": "low" },
  "text": { "verbosity": "low" },
  "max_output_tokens": 1400
}

The web_search_preview request differed only in tools[0].type. The ASCII-prefix control retained the same settings with web_search, replacing input with:

[
  {
    "role": "user",
    "content": "Use the built-in web search tool to find the official Unicode source confirming the name for U+1F4D6. Begin the final answer exactly with the ASCII prefix ASCII followed by one space. Then state the code point and official Unicode name in one short sentence that contains the literal symbol 📖. Do not type a URL, footnote, bracketed citation, or citation JSON; allow the server to attach its source annotation."
  }
]

Responses: supplementary-character probe

Exact request model: gpt-6-luna; endpoint: /responses; native tool: web_search. The request returned HTTP 200 and a completed native search call with a server-generated url_citation.

{
  "text": "A😀中B U+1F4D6 is officially named OPEN BOOK 📖. ([unicode.org](https://www.unicode.org/charts/PDF/U1F300.pdf?utm_source=openai))",
  "annotation": {
    "type": "url_citation",
    "start_index": 46,
    "end_index": 126,
    "title": "The Unicode Standard, Version 17.0",
    "url": "https://www.unicode.org/charts/PDF/U1F300.pdf?utm_source=openai"
  }
}
Measurement Result
Full text, Unicode code points 126
Full text, UTF-16 code units 128
Full text, UTF-8 bytes 134
Code-point range [46,126) Complete parenthesized Markdown citation link
Corresponding UTF-16 range [48,128)
Corresponding UTF-8 range [54,134)

The actual JavaScript code-unit slice at the reported coordinates was:

. ([unicode.org](https://www.unicode.org/charts/PDF/U1F300.pdf?utm_source=openai

Code-point slicing selected:

([unicode.org](https://www.unicode.org/charts/PDF/U1F300.pdf?utm_source=openai))

Thus the reported offsets count code points, and the reported end equals the exclusive code-point text length. The supplementary characters precede the citation; no supplementary character inside the generated citation link was observed.

Responses: independent checks

Tool / output Reported range Code-point length UTF-16 length UTF-8 length Observation
web_search_preview, Unicode-prefix variant [46,146) 146 148 154 Code-point slicing selected the complete citation link; code-unit and byte slicing did not.
web_search, ASCII-prefix control [49,129) 129 130 132 Code-point slicing again selected the complete citation link.

The ASCII-prefix control began ASCII U+1F4D6 is officially named OPEN BOOK (📖). , so a supplementary character still occurred before the citation. These are independent returned outputs, not synthetic annotations supplied by the model in its message text.

Retained output and annotation for web_search_preview:

{
  "text": "A😀中B U+1F4D6 is officially named OPEN BOOK 📖. ([unicode.org](https://www.unicode.org/Public/UCD/latest/charts/nameslist/1f300/?utm_source=openai))",
  "annotation": {
    "type": "url_citation",
    "start_index": 46,
    "end_index": 146,
    "title": "Miscellaneous Symbols and Pictographs – Unicode 18.0.0",
    "url": "https://www.unicode.org/Public/UCD/latest/charts/nameslist/1f300/?utm_source=openai"
  }
}

Retained output and annotation for the ASCII-prefix control:

{
  "text": "ASCII U+1F4D6 is officially named OPEN BOOK (📖). ([unicode.org](https://www.unicode.org/charts/PDF/U1F300.pdf?utm_source=openai))",
  "annotation": {
    "type": "url_citation",
    "start_index": 49,
    "end_index": 129,
    "title": "The Unicode Standard, Version 17.0",
    "url": "https://www.unicode.org/charts/PDF/U1F300.pdf?utm_source=openai"
  }
}

ChatCompletions: normal models

Requested model Returned model Normal generation Search-option observations
gpt-4o gpt-4o-2024-11-20 HTTP 200 Three attempts with {}, search_context_size: "low", and search_context_size: "medium" returned no native annotations. The first two stated that live search was unavailable; the third returned plain factual text.
gpt-5-mini gpt-5-mini HTTP 200, READY, finish reason stop {} and search_context_size: "low" returned no native annotations and stated that live search was unavailable.
gpt-4.1 gpt-4.1-2025-04-14 HTTP 200, READY, finish reason stop {} and search_context_size: "low" returned plain text without native annotations.

The successful normal gpt-5-mini request used reasoning_effort: "low" and max_completion_tokens: 512. An earlier 32-token budget ended with empty content and finish reason length; it did not establish endpoint failure.

The normal-generation prompt was Reply with only the word READY.. The successful normal gpt-4.1 request used max_tokens: 64.

The search attempts for gpt-5-mini and gpt-4.1 used this shared message:

{
  "role": "user",
  "content": "Use live web search now for an official Unicode source confirming the character name for U+1F4D6. Begin the response exactly with the literal prefix A😀中B followed by one space. State the code point and official Unicode name in one short factual sentence that includes the literal symbol 📖. Do not type a URL, footnote, Markdown citation, or citation JSON. If live search is unavailable, say so plainly."
}

Their bodies set model, messages: [the message above], and one of the two tested web_search_options values. gpt-5-mini additionally used reasoning_effort: "low" and max_completion_tokens: 1024; gpt-4.1 used max_tokens: 256.

All three gpt-4o search attempts used max_tokens: 180. Their retained prompts were:

  • Low-context attempt: Search the web now for the official Unicode character name for U+1F4D6. Begin the answer exactly with the literal prefix A😀中B followed by one space. Then state the code point and official Unicode name in one short sentence, and include the literal symbol 📖 within that factual sentence. Do not type any URL, footnote, bracketed citation, or citation JSON. Use the search result rather than prior knowledge.
  • Empty-options attempt: retained as the same prompt with an ending asking If live search unavailable, say that plainly. The exact replacement versus addition of the final sentence was not retained unambiguously.
  • Medium-context attempt: Use web search to check the official Unicode name for U+1F4D6. Begin the answer exactly with A😀中B followed by a space; state the code point and official name in one short sentence, including 📖. Do not type a URL, footnote, bracketed citation, or citation JSON; use live search if available, and say plainly if it is unavailable.

These experiments establish ChatCompletions endpoint availability for the tested models. They do not measure citation offsets or prove absence of other citation mechanisms.

Native ChatCompletions citation trigger

The inspected official web-search guide documents a dedicated search-model path:

{
  "model": "gpt-5-search-api",
  "web_search_options": {},
  "messages": [
    { "role": "user", "content": "What was a positive news story from today?" }
  ]
}

Native returned citations are under choices[].message.annotations[].url_citation. Ordinary gpt-4o, gpt-5-mini, and gpt-4.1 requests with web_search_options did not exercise that documented search-model path. Access to the dedicated search model was unavailable for further probes.

The pinned official SDK associates message annotations with web search and declares the nested URL citation shape. Its request definition declares web_search_options separately from function/custom tools. The documentation's character-index wording does not resolve code point versus UTF-16. Model-authored citation JSON in message.content is not evidence about native response-envelope annotations.

Public response captures

ChatCompletions

Actual SSE fixture, pinned revision: returned model gpt-5-search-api-2025-10-14.

After concatenating choices[0].delta.content, the text lengths are 538 code points, 538 UTF-16 units, and 542 UTF-8 bytes. Native annotations are delivered in a later delta.annotations.

Reported range Code-point / UTF-16 slice UTF-8 byte slice
[73,162) Complete parenthesized axios.com link Same, because preceding text is ASCII
[231,341) Complete parenthesized axios.com link Misaligned after vóór; begins with preceding punctuation and truncates the link
[434,538) Complete final parenthesized axios.com link Misaligned after non-ASCII text; truncates the link

The first two ends are followed by spaces; the final end equals the full character length. This establishes end-exclusive behavior in the capture. There are no supplementary characters in the body, so code point versus UTF-16 cannot be distinguished.

A second ChatCompletions-shaped capture, with model: "openai/gpt-5.5" and provider: "OpenAI", was routed through OpenRouter. Its [328,421) citation selects the complete fidelity.com Markdown link by character offsets, and position 421 is the following newline. Preceding ’ and — invalidate byte slicing. It likewise has no supplementary characters; forwarding through OpenRouter also limits provenance.

Responses

Official Cookbook captured output, pinned revision: U+2019 appears at code-point position 531. Citation [625,753) selects the complete rifnote.com Markdown link; position 753 is a newline. UTF-8 slicing is displaced by two bytes. All five citations align with half-open character ranges. The body has no supplementary characters, so this capture alone does not distinguish code points from UTF-16; the live experiment above does.

Messages

Captured char_location citation and matching source document: range [732,1080) selects a quote containing 📖.

Measurement Result
Reported range width 348
Quote code-point length 348
Quote UTF-16 length 349
Quote UTF-8 length 353
Code-point slice matches quote Yes
UTF-16 slice at reported offsets matches No
Corresponding UTF-16 range [732,1081)

These coordinates refer to the source document, not the generated assistant text. They cannot be copied into an IR output-text range. A Python consumer and another consumer slice source text using these character coordinates.

Client and gateway interpretations

Explicit code-point interpretation: new-api

ChatCompletions annotation conversion:

runes := []rune(text)
return string(runes[start:end])

The caller reads nested annotation["url_citation"]. This explicitly interprets ChatCompletions coordinates as code points and end-exclusive. It is a gateway implementation assumption, not an upstream specification.

Conflicting direct client: Promptfoo

Native ChatCompletions annotation consumer:

output.slice(citation.start_index, citation.end_index + 1).trim()

The call site supplies message.annotations. This assumes UTF-16 and an inclusive end. Its test uses a hand-authored ASCII response Current answer, start 0, end 6, and expects Current. It does not test supplementary characters or validate a real server response. The inclusive-end assumption conflicts with the captures above; .trim() can conceal an extra space or newline. This is not sufficient evidence for a UTF-16 upstream contract.

Pass-through official client

OpenAI Agents JS ChatCompletions streaming adapter validates and retains start_index and end_index without slicing or coordinate conversion. Its implementation language does not establish the upstream coordinate unit.

Other consumers and limitations

  • LobeHub extracts native ChatCompletions citation URL/title and discards coordinates.
  • Mattermost UI inserts markers with JavaScript slicing, and its type describes UTF-16. However, the inspected adapter consumes normalized Responses annotations. Tests use ASCII. This is not direct ChatCompletions evidence and conflicts with the tested Responses behavior.
  • SymbolicAI consumes Responses output annotations and uses Python code-point lengths; its renderer uses half-open Python slices. It is not ChatCompletions evidence.
  • Tinfoil's producer converts byte offsets to rune offsets when emitting ChatCompletions-style annotations. Its test distinguishes bytes from characters using BMP text, not code points from UTF-16. It records another implementation assumption.

Research included global web searches and global GitHub code searches across annotation field names, protocol names, slicing operations, coordinate encodings, and language-specific consumers. Schema declarations, pass-through adapters, URL-only displays, hand-authored annotations, and consumers of another protocol were not treated as proof of native ChatCompletions units. No inspected direct ChatCompletions client supplied a supplementary-character test establishing its coordinate interpretation.

Remaining uncertainty

ChatCompletions' unit remains inferred. Settling it requires an authentic native annotation response with a supplementary character before its range, or a primary upstream contract explicitly specifying the unit. The measured Responses behavior does not establish every provider or annotation type's contract.

Menci added 2 commits October 10, 2026 19:52
Move the detailed citation-coordinate investigation into the PR discussion and link the owning coordinate conversion comment directly to that evidence.
Keep core types and stream operations at the IR root and shared adapter support under ir/shared. Own native error conversion in each writer. Keep custom tool wrapping in the existing Responses translation helper outside IR.
@Menci Menci changed the title feat(ir): translate generated output through a shared stream refactor(translate): translate returned streams through internal SSE IR Oct 10, 2026
@Menci
Menci marked this pull request as ready for review October 10, 2026 11:57
Menci added 6 commits October 10, 2026 20:01
Stream scalar reasoning with native item interruptions, keep independent tools live, and retain text arrival order through buffered projections. Ignore incoming ChatCompletions reasoning_items and wait for stable tool identities.

Preserve raw truncated arguments with native length/incomplete/max_tokens outcomes. GenerateContent keeps tool order, omits incomplete calls, and ends failures with compact Google JSON trailers. Recheck cancellation after writes and keep captured-stream copying consistent with the wire format.

Validate with the complete repository verify pipeline (606 suites, 6761 tests), gated protocol-pair audits, actual SDK consumers, and converged comment/structure/defensive reviews.
# Conflicts:
#	apps/web/__tests__/components/requests/stream-render_test.ts
#	apps/web/src/components/requests/stream-render.ts
Expose known initial accounting before native start events and consume late cache breakdowns as whole snapshots. Preserve text arrival order across concurrent items and ordered tool buffering, rebase citations across interrupted source text, and keep logprobs with their original text owner.

Normalize GenerateContent tier aliases at its output boundary and restore reasoning/output count validation. Add gated lifecycle, accounting, and ownership regressions; keep IR private to translate.
Emit the initial ChatCompletions role from the initialized start state and attach same-chunk citations and logprobs before tools close their text item. Verify both properties before pulling another upstream frame or relying on the terminal response snapshot.
@Menci

Menci commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner Author

Codex: accounting, timing and policy decisions with client evidence.

  • Initial ChatCompletions role and Messages message_start are emitted immediately. Responses initial snapshots retain known usage and tier; unknown usage is null. Earlier envelopes do not promise earlier visible text.
  • Messages retains cumulative realtime usage, including repeated terminal counts. The native cumulative contract takes precedence over clients that add snapshots.
  • Messages usage members update independently. A present cache_creation object replaces the complete snapshot; null clears it and omission preserves it. Independent maxima would retain stale corrected counts.
  • OpenAI fast passes through. GenerateContent maps fast to priority and default to standard; candidate counts exclude reasoning and reject reasoning counts above inclusive output counts.
  • Policy errors preserve nonblank upstream explanations verbatim, including spacing. Missing/null/blank explanations receive a truthful nonblank message, e.g. The upstream declined this request under the bio policy category. The translation does not inject an OpenAI enrollment discriminator.

Downstream ChatCompletions supports continuous_usage_stats only together with include_usage. The mode follows vLLM's flag contract: latest reported cumulative usage is attached to actual choice chunks, including role, text, reasoning, tool, affinity carrier and finish chunks. It does not create usage progress frames. Unknown statistics stay null until upstream reports them. Responses frequently reports statistics only at completion, so this mode cannot invent intermediate counts. Per-choice counters remain distinct from the aggregate trailer. Default mode strips body statistics and emits only the latest empty-choices carrier immediately before DONE or normal EOF; failures do not flush pending statistics.

Messages usage compatibility was reproduced with output 0 → 2 and progress/final snapshots of 2: @langchain/anthropic 1.5.12 / @langchain/core 1.2.17 reported 4 in both aggregation and handleLLMEnd, while the official SDK reported 2. Its event conversion and aggregation explain the additive behavior. The native contract defines cumulative counts; LangChain.js #10249 also acknowledges cumulative cache statistics from the official upstream. Its cache fix does not establish that output counts are increments.

Messages permits explanation:null; Responses requires error.message:string. Codex’s decoder supplies an OpenAI enrollment discriminator for absent/blank explanations; the truthful nonblank fallback prevents this provider-inappropriate UI.

Initial non-null usage is accepted by the official SDK and appears in its own ResponseStream snapshots. The official SSE examples use null, but do not require discarding already-reported usage. Codex's catalog declares priority plus the additional fast spelling. Google has a native serviceTier field in its discovery schema.

Nested cache replacement follows the nonstandard sender’s snapshot semantics. new-api constructs both TTL values, omits zero JSON members, and replaces the whole object including zero corrections, with a specific regression test. The Copilot gateway consumer also shallow-merges usage. Bifrost instead uses independent per-counter maxima; that can retain stale corrections and is a different accounting model.

Native Messages delta usage does not declare the nested cache_creation field. TS/Python typed accumulators ignore this late extension; Go raw accumulation replaces each usage member rather than recursively merging it. The official TypeScript accumulator assigns cumulative output_tokens, so complete usage snapshots retain that count.

Earlier envelopes are accepted, but do not inherently display earlier text. Real OpenAI 7.32.0 and Anthropic 0.133.0 gated SSE probes delivered one initial event callback and zero text callbacks; empty-body completion was accepted. Cline filters metadata/start parts, and its first-chunk timing measures normalized content. The start-before-block constraint has real client evidence in CLIProxyAPI #563. Copilot-api and CLIProxyAPI emit an immediate Messages start for response.created, while CLIProxyAPI's Chat translator waits for the first content delta. Both schedules are in use; this does not establish one required upstream-relative timing policy.

Repeated cumulative ChatCompletions usage is unsafe as a universal extension. A mocked HTTP stream containing usage 10/0, 10/2, and final 10/4 produced correct final 10/4/14 in OpenAI Python 3.28.0 and Vercel OpenAI 4.0.91, but LangChain OpenAI 1.7.0 reported 30/6/36 both in its aggregate and on_llm_end callback. It converts each usage chunk and adds usage on aggregation. Official Chat stream usage is a final additional chunk. CLIProxyAPI emits repeated cumulative usage, but that example cannot override the reproduced client incompatibility.

GenerateContent has a different consumer contract. Google JS 2.28.0 models and chats accepted usage-only frames, continued text AB, and retained chat history. LangChain Google 4.4.1 subtracts previous cumulative usage on content chunks; usage-only chunks instead produce empty-response warnings and are not exposed as application chunks. Vercel Google 4.0.93 records usage but exposes it only at final finish. This supports cumulative usage alongside actual content, but does not demonstrate a user-visible benefit from inventing pure usage frames.

Known initial Responses usage is safe in the tested consumers: LangChain OpenAI 1.7.0 accepted created/in_progress 10/0/10 and final completed 10/4/14 without double counting. It only converts terminal usage into usage_metadata. This distinguishes initial response snapshots from repeated Chat usage chunks.

The biology-prefix rule’s original rationale was Codex-specific UI. An injected prefix has a specific UX cost. Codex's special notice replaces the upstream explanation with fixed OpenAI Trusted Access enrollment links; its rendered snapshot confirms the displayed result. Anthropic has a separate Life Sciences Verification Program. An OpenAI enrollment cannot change the actual Anthropic classifier. Without the injected prefix, Codex still classifies bio_policy as non-retryable and retains the explanation. Copilot-api and Bifrost use content_filter rather than injecting the Codex prefix; no independently found Anthropic-to-Responses gateway used this prefix injection. Promptfoo and Inspect AI classify by code, without requiring it.

The normative contract should not be confused with a universal first-party reporting cadence. Examined source recordings include genuine api.anthropic.com/v1/messages recordings in Braintrust's Claude Agent SDK cassettes, Anthropic SDK cassettes, and LangChain cassettes. All 25 examined streams carried one final usage delta after content blocks; the largest observed response was 475 output tokens. These third-party recordings support that common cadence, not a claim that long first-party generations can never report intermediate statistics. Streaming usage remains supported independent of that observation.

Menci added 7 commits October 11, 2026 05:51
Publish native initial envelopes immediately and capture downstream usage preferences before upstream accounting rewrites. Opted-in ChatCompletions choice chunks carry the latest reported cumulative counters, including affinity carriers and finish frames. Standard usage is coalesced into one terminal carrier.

Validate both flag combinations through HTTP, per-trip preference ownership, unknown counters, gated initial envelopes, and per-choice affinity projections.
Preserve cumulative Messages usage progress. Supply nonblank policy error explanations when the upstream has none, and publish legacy reasoning carriers as each item completes before finish_reason.

Reject replacements or parent-array deletions of text already emitted to scalar destinations, including closed blocks. Reconcile buffered text replacements and removals before delivery. Cover these policies with gated lifecycle and native-source snapshot regressions.
Introduce explicit initialize operations for unpublished skeletons and restrict append to strings. Array creation uses contiguous indexed initialize or assign operations. Track actual field publication independently from structural state so native writers never infer content from placeholders or frame grouping.

Adapt all readers and writers, preserve native lifecycle envelopes, and cover skeleton suppression, actual empty values, strict array indexing, and regrouped frames.
@Menci

Menci commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner Author

Codex: final IR stream performance evidence.

Workload: 4,000 ChatCompletions chunks with one streamed text token and one unique logprob token per chunk, plus 20 unique top_logprobs alternatives per token. Runtime: Node v26.8.1. Each pipeline runs in its own fresh process. Within that process, each measurement starts after forced GC, creates a fresh reader/accumulator/writer pipeline, and pauses the upstream after all 4,000 token chunks have been consumed but before terminal frames. Retained heap is the forced-GC heapUsed delta from the pre-run baseline at that pause. Time per token is pipeline-start-to-pause divided by 4,000. Each row is the median of three fresh pipeline runs.

Pipeline Retained heap delta Time per token
Reader + accumulator 38.44 MiB 32.1 µs
ChatCompletions writer 38.57 MiB 34.6 µs
Responses writer 38.50 MiB 37.1 µs
Messages writer 38.55 MiB 37.0 µs
GenerateContent writer 38.52 MiB 37.6 µs

The workload retains roughly 84,000 unique token/top-logprob objects. IR does not deep-copy those opaque leaf objects: the producer record and the two copy-on-write collectors share them. Most retained heap is therefore the source object graph whose lifetime is extended until terminal snapshots, plus array references and collector containers. Writer-specific retained state adds about 0.06–0.13 MiB.

Separate 4k/16k measurements on the same architecture kept time per token broadly stable and retained heap proportional to token count, so the ordinary record path is linear. The final implementation also removes the semantically empty IRFrame, resolves schema paths with one cursor, converts only newly arrived annotations, and uses lazy direction-specific coordinate indexes that follow later text appends. Citation-heavy inputs no longer repeatedly scan/re-encode the complete accumulated text for every range.

This evidence describes the final head 291a728fc; it is CPU/heap behavior of in-memory translation and excludes network I/O and JSON wire serialization.

Menci added 25 commits October 13, 2026 06:35
Goal: replace untyped native events in the Responses writer.
Result: events, output items, content parts, annotations and usage are
protocol types; reasoning part events come from small builders instead of
conditional key spreads, and logprob tokens are read from IR state. One
cast remains per documented gap: IR logprobs may omit fields the wire type
requires, and a cache write can be reported without cache reads.
…ders

Goal: drop the untyped usage input of the two remaining usage readers.
Result: ChatCompletions reads the protocol usage type and Responses reads
a local view of the fields it consumes. The invalid-counter test calls
each converter with a literal, since the converters no longer share one
loosely typed parameter.
… kind

Goal: replace the long if/else chains over unit kinds in the Responses
writer.
Result: message, image, reasoning and tool call units each have an opener
returning the added item and its follow-up events, and a handler for
operations on an open unit. openPendingUnits keeps the loop, the wait for a
tool call ID and the dequeue; justOpened semantics are unchanged.
Goal: keep every statement in the IR, reader, writer and usage files
readable.
Result: long conditions, object literals and nested ternaries are split
over several lines with named intermediates; behavior is unchanged.
Goal: replace the single-letter builder name in the three stream readers.
Result: the builder is named ir in the ChatCompletions, Messages and
Responses readers; no existing identifier used that name.
Goal: remove the near-identical IRJSONValue and IRValue definitions.
Result: both derive from one generic value type; IRJSONValue has no extra
leaf and IRValue adds the IRNone marker at any depth. Exported names are
unchanged.
The custom tool input done event carries item_id, output_index and input only; the namespace restorer already strips a name when families change. Remove the name field the Responses writer added to it and the matching protocol type member.
A scratch script was staged by a bulk add and broke the lint job, which parses every TypeScript file against a project.
A response.failed event may carry a null error, and the previous translators answered it with a default message. Emit the same IR error instead of throwing.
The previous translators dropped citations that are not web or search results when writing Responses output. The conversion to file_citation was new behavior with an invented file index.
…served IR fields

No writer reads the granularity of a quoted citation source, which left source_text a one-field wrapper; store the quoted text as a string. Note that choice-scoped logprobs and the audio MIME type are reserved for a GenerateContent upstream.
Declare the structure of the IR once, with the append, IRNone and lifecycle unit traits on its nodes, and check the value type inferred from it against the IR types at compile time. The path predicates, unit path builders, end event paths, record owners and the collector's validation now walk that schema instead of matching path shapes. Values written into the IR are checked against it, so a field the schema does not declare is rejected.
Goal: let upstream readers own writes through builder-managed unit handles instead of reconstructing lifecycle paths and state.\n\nResult: item, part, and choice handles build schema-derived paths, track open units, support idempotent closure and one-time initialization, and expose the two scoped accumulated-value reads needed by readers. Focused handle tests cover paths, lifecycle, and reads.
Goal: make the Messages upstream reader produce IR exclusively through builder-owned item, part, and choice handles.\n\nResult: block state now owns its write handles and citation count, all unit writes use relative handle paths, and handle lifecycle closure preserves the existing record order and errors.
Goal: remove accumulated IR reads and hand-built unit paths from the ChatCompletions upstream reader.\n\nResult: choice state now owns item, part, tool, annotation, and logprob bookkeeping through write handles; only the dedicated concatenated-text view is read for citation coordinates, while record order and lifecycle behavior remain unchanged.
Goal: make the Responses upstream reader own item, part, and reconciliation writes without inspecting accumulated IR structure.\n\nResult: reader state tracks content, reasoning, logprob, tool, image, and audio units through handles; snapshot comparisons use the scoped current-value API, full error paths resolve through the builder, and focused plus full translation tests preserve behavior.
Goal: stop rediscovering immutable IR unit kinds and per-record token values from accumulated state.\n\nDecision: retain only item, part, and logprob owner metadata at initialization; keep complete-state reads for envelopes, audio replay, terminal reasoning, citations, and late tool metadata.\n\nResult: per-record dispatch uses record values and registered discriminants without caching growing IR content.
Goal: avoid accumulated-state reads that only rediscover an initialized unit kind.\n\nDecision: register immutable content-block kinds from initialize records while keeping complete-state reads for ordered delayed opening and terminal reasoning or tool snapshots.\n\nResult: opened-unit append dispatch uses registry metadata without caching source content.
Goal: remove accumulated-state reads that only recover initialized unit kinds, grouping, or the current token.\n\nDecision: retain immutable part and logprob-owner metadata while preserving complete-state reads for delayed opening, coordinates, and terminal output snapshots.\n\nResult: registration and per-record emission use record values without mirroring growing IR content.
Goal: remove redundant accumulated-state dispatch and the writer-owned copy of pending IR text.\n\nDecision: retain immutable unit kinds and pending source offsets, then reconstruct delayed fragments from the collector when their target part can open.\n\nResult: original chunk boundaries and grounding mappings remain intact without mirroring growing source content.
Goal: share the target-independent rule that truncated tool completions do not require valid JSON.\n\nDecision: keep each protocol's waiting and streaming control flow local, and centralize only completion-time parsing and repair.\n\nResult: Messages, Responses, and GenerateContent preserve their distinct boundaries while using one tested completion helper.
Goal: prevent GenerateContent output from inventing inline-media MIME types absent from IR.\n\nDecision: remove unowned image and audio MIME fallbacks; retain Chat audio replay identity because it belongs to the newly emitted target stream.\n\nResult: reliable reader-provided MIME metadata is forwarded, and missing MIME continues to fail at the writer boundary.
Goal: keep the write-handle implementation compliant with repository lint.\n\nResult: equivalent optional-chain and nullish-assignment expressions remove the final lint violations without changing reader behavior.
Stream private IR records directly so grouping carries no false atomicity while preserving upstream backpressure. Resolve schema paths with one state cursor and replace repeated citation scans with lazy direction-specific coordinate indexes that follow later text appends.\n\nTrack native and emitted annotation progress separately, cover split surrogate boundaries and incremental citations, and retain the existing complete logprob state. Verified with translate typecheck, task-owned lint, and all 1,013 translate tests.
@Menci

Menci commented Oct 13, 2026

Copy link
Copy Markdown
Owner Author

Codex: design archaeology for reasoning translation, Fit IR, and carried assistant-turn round trips.

This comment records the design path that led to #594 and the dependent #599. It summarizes the decisions rather than preserving discussion transcripts. Earlier alternatives are included where they explain the final boundaries.

The problem first appeared as ChatCompletions reasoning dialects

ChatCompletions has no official reasoning-text or encrypted-reasoning contract. Real producers and clients independently use scalar text fields such as reasoning, reasoning_text, and reasoning_content, and structured/opaque carriers such as reasoning_details, reasoning_opaque, reasoning_items, and thinking_blocks.

The first proposal was a configurable ChatCompletions codec: normalize an upstream-selected text/data dialect into one private message representation, then project it to the client's selected dialect. This was broader than pairwise translation because native ChatCompletions → ChatCompletions also needed normalization. The implementation used a private enumerable Symbol for normalized reasoning and exposed explicit source/target codec options.

Client round-trip research showed why field renaming was not enough:

  • OpenRouter-oriented adapters preserve reasoning_details but commonly discard reasoning_opaque.
  • Copilot-specific adapters may preserve reasoning_opaque and ignore reasoning_details.
  • Generic OpenAI-compatible adapters may discard both because neither field is in the official ChatCompletions schema.
  • Accepting a field without throwing is not evidence that the next request will contain it.
  • Older clients could fail on particular structured-detail combinations even when newer versions accepted them.

The resulting canonical split was conceptual rather than wire-specific:

  • readable reasoning text, meant to be shown to the user;
  • upstream opaque/encrypted reasoning, needed for native replay;
  • a gateway-owned sidecar, authenticated and encrypted so only the issuing deployment can restore it;
  • unrecognized residue, retained by the native protocol representation until a later stage decides whether it must be carried.

For scalar text aliases, the first recognized field is selected per choice for the complete turn. Empty strings still establish the selected field. Simultaneous aliases use the stable preference reasoning, reasoning_text, reasoning_content; the implementation does not switch aliases from frame to frame. Mixed readable reasoning/content deltas are emitted reasoning-first, following the observed producer convention. The detailed client/producer evidence remains in the dedicated reasoning comment.

The early codec PR was closed because the general problem was larger than ChatCompletions reasoning. Every protocol has native assistant-output structure another protocol cannot replay, and client compatibility can vary by API key rather than only by model/upstream. Its useful decisions survived in the later IR and sidecar designs; its protocol-specific carrier machinery did not.

From a reasoning sidecar to a translation-wide sidecar

The original sidecar RFC was ChatCompletions-centric. It proposed extracting readable reasoning, retaining opaque fields, and encoding a compact encrypted object into reasoning_details or reasoning_opaque. For Responses and Messages sources it introduced "thin items/blocks": preserve the native item/block structure but replace strings already exposed in the ChatCompletions assistant message with text-range references.

That idea was generalized in two steps.

First, the sidecar became a property of the complete translation system, not a reasoning codec. Any client protocol may carry one encrypted sidecar, and any upstream protocol may contribute native structure to it. The carrier is protocol-native:

  • Responses: a dedicated opaque reasoning item;
  • Messages: a dedicated redacted-thinking block;
  • GenerateContent: a dedicated thought-signature part;
  • ChatCompletions: an encrypted entry in the configured reasoning carrier.

Upstream opaque values are stored inside the authenticated sidecar when native replay needs them. The sidecar carrier is the only gateway-generated opaque value returned for one assistant turn; multiple sidecars make the turn unrecoverable.

Second, "thin item" was generalized to a per-protocol thin assistant turn. A turn is the whole assistant result in that protocol: one assistant message for ChatCompletions/Messages, an interleaved output-item sequence for Responses, and potentially several model messages for GenerateContent. The protocol-specific code creates an intermediate thin turn containing references; generic code resolves it to a finalized thin turn.

Fat IR and Fit IR

Two competing intermediate-representation directions were considered.

Fat IR attempted to losslessly model the union of all protocol-native structures, possibly with unknown/native payloads embedded directly. In its strongest form, an A via B path would be B SSE → Fat IR SSE → A SSE, and the IR could be blindly collected. It promised fewer sidecar concepts, but made the IR itself contain every protocol's residual structure and every future event family. It was difficult to validate, difficult to emit correctly, and especially fragile for Responses, whose event vocabulary evolves continuously. Unknown-event passthrough was also not enough: a target usually cannot give meaningful lifecycle/order semantics to an unknown source event.

Fit IR contains only the union of mutually translatable generated-output semantics. It is deliberately private to packages/translate, covers assistant output rather than request bodies, and preserves the source protocol's meaningful structure even where targets flatten it. For example, reasoning summary/content strings remain separate IR entries although another protocol may concatenate them.

The initial Fit-IR discussion considered target-dependent gate-in: admit a semantic only if the selected target can express it. That was rejected. A reader should describe the source once; a writer describes what it actually emitted. The later IAT records which IR strings reached the client, so target capability does not change the source IR.

The boundary is semantic equivalence or equivalent use, not shared spelling. Examples include encrypted reasoning versus thinking signatures. Source-only server tools, MCP/server-owned result objects, request-only configuration, and fields that no other output protocol can meaningfully consume stay outside IR and are retained only by thin capture where native replay needs them.

Why the IR is an operation stream

The design started from the observation that most streamed output changes are either complete replacement or string accumulation. Numeric increments can always be represented as assigning the new cumulative value. Lifecycle cannot be represented as a value update, so records divide into operations and events.

The current operation contract is:

initialize(path, value) // create minimal structure at an absent path
assign(path, value)     // publish a complete actual value
append(path, string)    // publish an actual string fragment

initialize exists because creating a terminal-valid skeleton is not the same thing as publishing empty content. An array initialize must use exactly the current array length. append is string-only; it has no competing array-push meaning. The schema is the single source of truth for legal paths, optional/nullable fields, append targets, IRNone targets, and choice/item/part lifecycle units, with a compile-time proof that the schema's inferred value type matches IR.

IRNone is distinct from absence and null: it means an optional call ID is now known never to arrive. Messages and Responses writers can stop waiting and synthesize an ID; an absent field still means "not known yet".

Lifecycle records are start, part_end, item_end, choice_end, finish, error, and ping. Initializing an item or part starts its lifecycle. Operations after the corresponding end fail. finish represents normal completed/incomplete generation; internal failures use the single error primitive and each writer chooses native error framing.

An early IRFrame { records[] } grouped records derived from one native frame. Review established that no atomicity existed: writers already observed and emitted each record before the next. The wrapper was removed; readers now stream IRRecord directly while normal async-generator backpressure still prevents pulling the next upstream frame until the current records are consumed.

Reader, accumulator, and writer ownership

Readers map native events directly into IR records. They keep only native facts IR does not hold or cannot yet place: source indexes, selected aliases, incomplete native wrapper text, and snapshot identity. Write handles remember the unit and initialized fields, so readers do not read IR back to discover what they wrote. Two reads remain intentional: ChatCompletions citation conversion reads accumulated generated text, and the Responses reader compares native done snapshots with accumulated IR values.

Each producer and consumer owns its own copy-on-write accumulator. Record payloads are immutable; collectors borrow them until a write first touches the containing path. This preserves old record snapshots without repeated deep cloning.

Writers react to the current record and retain only target-owned/out-of-sync facts: generated IDs, native ordinal indexes, pending-open ordering, emitted annotation progress, and target lifecycle. They read complete IR state only when the native protocol requires a complete opening/terminal snapshot or a coordinate calculation. Growing source strings, arguments, annotations, logprobs, and output items are not mirrored in writer state.

Target ordering is explicit. A unit missing a target-required fact may delay later native starts. The queue stores units, not replayable IR records. When it opens, it reads the current accumulated value once. GenerateContent pending text stores source paths/ranges rather than copied strings.

Protocol-specific lifecycle decisions

ChatCompletions has no item lifecycle, so the reader infers it. Readable reasoning/text transitions close the previous scalar item. A function call closes when a later tool index starts or when the choice ends; syntactically complete JSON is not an end marker because real streams can append malformed/concatenated objects. A native custom call remains open until choice end. Updates for already closed calls are warned and ignored. Tool arguments remain strings in IR; targets requiring objects parse them at their own completion boundary, and truncation never forces successful JSON parsing.

Messages blocks may be simultaneously open in IR. Client-specific reordering is an independent outer concern. Messages usage is cumulative; members update independently, while its nested cache snapshot follows the sender's snapshot semantics.

Responses done events are snapshots, not unconditional replacements. Reconciliation is per field:

  • an empty/missing done value does not erase already streamed content;
  • an equal value produces no operation;
  • an append target may add a missing suffix;
  • a non-prefix replacement fails because already-emitted scalar text cannot be retracted;
  • a previously absent assign field may be supplied, but a conflicting replacement fails.

Done snapshots reconcile before closing their part/item. Terminal output may introduce an unannounced item, which is initialized and closed in output order. Type/ID mismatches and missing terminal items fail. Image generation allocates its output item independently and receives the image part when result data arrives.

GenerateContent may require complete media/tool-call values and therefore holds later units behind an incomplete earlier unit. It preserves arrival order for delayed text. Its stream errors use a first-class raw frame so Google's compact bare-JSON error follows ordinary SSE chunks without pretending to be an SSE data field.

Citations and coordinates

IR text ranges use UTF-16 code-unit offsets because IR stores JavaScript strings exactly, including isolated surrogates. Native protocols may use code points or UTF-8 bytes. The current implementation creates a direction-specific typed coordinate index lazily on the first citation and extends it on later text appends, including a surrogate pair split across deltas.

ChatCompletions citation indices are treated as half-open code-point offsets based on the retained capture/community evidence. Responses probe evidence establishes code-point offsets. GenerateContent grounding uses UTF-8 byte offsets. Late ChatCompletions citations are intersected with the text that remains open after item interruption; a closed-only span is warned and dropped.

The detailed probe evidence remains in the dedicated citation comment.

Thin turns, IAT, and sidecar finalization (#599)

#594 intentionally stops at returned-stream translation. #599 adds carried history restoration on top.

While B SSE becomes Fit IR, protocol-B code also collects a native B assistant turn and replaces eligible source strings with intermediate references. Every IR-origin string is registered in an Intermediate Association Table (IAT). As A's writer emits content, it records how IR source ranges map onto A's client-visible strings. Concatenating or flattening several IR strings therefore produces ranges into the final A string rather than duplicating text in the sidecar.

At completion, generic code resolves intermediate references through the IAT:

  • content exposed and expected to round-trip through A becomes a reference;
  • content not exposed, or known not to round-trip through A, remains literal in the thin turn;
  • object/string crossings reference the canonical string only when exact stringify/parse round-trip is proven; otherwise the original representation remains literal.

Final references use a predeclared table of binary hashes. A reference stores the table index and optional UTF-16 range, not a repeated hash string. Repeated strings share one hash entry. CBOR keeps binary hashes compact. Raw JSON numeric tokens use a dedicated tag, and isolated UTF-16 surrogates use WTF-8 in a dedicated tag; ordinary well-formed strings remain normal CBOR text.

The sidecar contains the finalized thin assistant turn, referenced-content hashes, and a versioned replay check. It is signed/encrypted with deployment-owned keys. Versioning starts at 1 because the feature was not previously published; after publication, validators for historical promised versions must remain.

On the next request, protocol-A code splits user and assistant turns according to A's native rules, strips/extracts the single sidecar, and constructs A's client-visible text/object view. The replay check covers the body the gateway actually returned and expects back: visible text/reasoning and tool-call ID/name/arguments in order, plus the item-kind sequence. Response-only metadata is excluded. User turns always take normal bare translation.

The two assistant paths are mutually exclusive:

valid sidecar + matching replay check
    -> hydrate the native B thin turn

missing/invalid sidecar or client-visible turn changed
    -> remove gateway-private carriers and translate A's actual turn bare

Authentication failure or replay mismatch is treated as untrusted client history and falls back to bare translation. A sidecar that authenticates but cannot be decoded/hydrated indicates an implementation invariant failure and must surface an error rather than silently degrading.

The early LiteLLM-style reasoning_items/thinking_blocks replay carriers, ciphertext-plus-ID signature packing, direct pairwise opaque projections, and optional-codec paths were removed. Returned output has one path: B SSE → Fit IR records → A SSE; #599 adds thin capture/sidecar alongside that path instead of introducing a second translation pipeline.

PR layering and final boundaries

The long-lived architecture is therefore:

returned generation:
    B SSE -> B reader -> Fit IR records -> A writer -> A SSE

assistant-history round trip (stacked work):
    B native turn + IR-to-A associations -> finalized thin B turn + signed sidecar
    A replayed assistant turn + sidecar -> hydrated B turn OR bare A-to-B translation

The IR remains deliberately fit: it describes the translatable generated-output semantics. Native residual structure belongs to the thin turn; client-visible integrity and reconstruction belong to IAT/replay; vendor/provider wire deviations are handled at their owning input boundary rather than becoming IR concepts.

Detailed retained evidence:

Menci added a commit that referenced this pull request Oct 13, 2026
Adopt direct IR record streams, schema-derived write handles, shared unit lifecycle, and the latest protocol adapter architecture from PR #594.

# Conflicts:
#	packages/gateway/__tests__/data-plane/chat/openai-chat-completions/affinity/egress_test.ts
#	packages/translate/AGENTS.md
#	packages/translate/__tests__/ir/helpers.ts
#	packages/translate/__tests__/ir/initialize_test.ts
#	packages/translate/__tests__/ir/lifecycle_test.ts
#	packages/translate/__tests__/ir/streams_test.ts
#	packages/translate/__tests__/protocols/anthropic-messages/via-openai-chat-completions/request_test.ts
#	packages/translate/__tests__/protocols/anthropic-messages/via-openai-responses/request_test.ts
#	packages/translate/__tests__/protocols/gemini-generate-content/via-anthropic-messages/request_test.ts
#	packages/translate/__tests__/protocols/gemini-generate-content/via-openai-chat-completions/request_test.ts
#	packages/translate/__tests__/protocols/gemini-generate-content/via-openai-responses/request_test.ts
#	packages/translate/__tests__/protocols/openai-chat-completions/via-openai-responses/request_test.ts
#	packages/translate/__tests__/protocols/openai-responses/via-anthropic-messages/request_test.ts
#	packages/translate/__tests__/protocols/openai-responses/via-openai-chat-completions/request_test.ts
#	packages/translate/src/protocols/anthropic-messages/as-source/sse-to.ts
#	packages/translate/src/protocols/anthropic-messages/as-target/sse-from.ts
#	packages/translate/src/protocols/gemini-generate-content/as-source/sse-to.ts
#	packages/translate/src/protocols/gemini-generate-content/via-openai-chat-completions/request.ts
#	packages/translate/src/protocols/gemini-generate-content/via-openai-responses/request.ts
#	packages/translate/src/protocols/openai-chat-completions/as-source/sse-to.ts
#	packages/translate/src/protocols/openai-chat-completions/as-target/sse-from.ts
#	packages/translate/src/protocols/openai-responses/as-source/sse-to.ts
#	packages/translate/src/protocols/openai-responses/as-target/sse-from.ts
#	packages/translate/src/shared/json/object.ts
Menci added 3 commits October 16, 2026 16:26
Emit each candidate termination at IR choice_end and send late usage at global finish without buffering terminal candidates.

Keep protocol collection and gateway forwarding open after candidate completion so later candidates and usage survive. Preserve error, prompt blocking, and missing-completion behavior.
Append Responses delta logprobs at new IR array indexes while reconciling terminal snapshots. Compact nullable annotation indexes and validate required schema fields when units close. Track GenerateContent completion independently for every observed candidate in protocol collection and gateway streaming.
Model output text and refusal parts as distinct states so annotation, coordinate, and logprob bookkeeping cannot leak into refusal handling. Reject content-part type changes at their owning slot and remove the obsolete GenerateContent candidate-finished helper from public exports.
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.

1 participant