Conversation
Scaffold incubator/binding-llm.spec per AGENTS.md conventions and define LlmBeginEx, LlmDataEx, and the LlmFlushEx union, modelled on binding-mcp.spec's idl. LlmBeginEx carries dialect only; model routing is deferred. LlmDataEx has no fields: content flows through the DATA frame's own payload octets and INIT/FIN through its existing flags, so nothing survives in the extension once block identity moves to the FLUSH plane. LlmFlushEx is a 7-case union covering message start, block start/end, finish, usage, keepalive, and an opaque native/raw case for re-encoding events a same-dialect route doesn't recognize. Fixes #2476 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AuZoMsETwEczJx3cbkb8EJ
Scaffolds incubator/binding-llm and incubator/binding-llm.conf, modelled on binding-mcp's SERVER/CLIENT BindingContext structure. LlmBindingInfo is annotated @Incubating so type: llm config loading is gated behind ZILLA_INCUBATOR_ENABLED via FeatureFilter, matching the AmqpBindingInfo/ PgsqlBindingInfo/RisingwaveBindingInfo precedent. Fixes #2477 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0142buJWS7C89AKr9uDJtSy4
…t-type Registers by content-type and hands back a per-stream LlmContentDecoder; stays in an internal, unexported package for now with no concrete implementation registered yet. Fixes #2478 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mw32oxEw24fLt5Ypakj6pH
Closes the non-streaming half of #2478's own scope: "Non-streaming application/json goes through the same abstraction as one event, rather than a special-cased branch." Only text/event-stream had an LlmContentDecoderSpi implementation; application/json requests (non-streaming dialect responses) had no decoder to dispatch to. LlmJsonContentDecoder treats the entire buffered document as a single event (one data + one flush call, no framing loop), mirroring LlmSseContentDecoder's structure and unit-test conventions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mw32oxEw24fLt5Ypakj6pH
Implements LlmContentDecoderSpi for text/event-stream, decoding blank-line- delimited SSE framing (data:/event:/id: fields, comment lines, CRLF/CR/LF line endings) into event DATA + FLUSH frames per the WHATWG SSE parsing algorithm. Framing decode only, independent of any dialect-specific (e.g. OpenAI, Anthropic) payload interpretation. The last-event-id persists across dispatches until overwritten by a subsequent id: field, matching SSE reconnection semantics, and is reported as the flush boundary's associated bytes. A trailing lone CR at the edge of the currently available bytes is deliberately left unconsumed (returned as no progress) since it may still turn out to be part of a CRLF pair once more bytes arrive - avoiding a premature, ambiguous line-terminator decision on a streamed input. Representational-slack note for the round-trip decision this issue flags: this decoder normalizes field ordering and does not preserve field-value whitespace beyond the single optional space after the colon that the SSE spec itself strips, so re-encoding from the decoded data/flush frames alone cannot byte-exactly reproduce arbitrary source framing - only semantic equality should be asserted against this decoder's output. Fixes #2479 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013kK9z4ywuPJe33jm6W1fik
Defines the pluggable-dialect contract for binding-llm, exported from the start (unlike LlmContentDecoderSpi, which stays internal): LlmDialect exposes name()/detect()/contentType() plus supplyDecoder(Kind)/ supplyEncoder(Kind) returning common-json JsonTransform stages, and LlmDialectFactorySpi is the ServiceLoader-registered entry point. HttpHeaders is a minimal read-only accessor for detect(path, headers), since no HTTP header abstraction previously existed in this codebase and pulling in jakarta.ws.rs would add a dependency never otherwise used here. Kind is nested on LlmDialect, distinguishing request/response schemas. No concrete dialect implementations yet (OpenAI/Anthropic land later) -- module-info.java exports the dialect package and declares uses without a corresponding provides. Unit-tested via a stub LlmTestDialect/ LlmTestDialectFactorySpi registered under test-scope META-INF/services, mirroring this module's existing LlmContentDecoderSpi/ LlmTestContentDecoderFactorySpi pattern. Fixes #2480 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016NcAVwRPN1Pwjzpobr75w6
jfallows
force-pushed
the
claude/stoic-sagan-9d81tf
branch
from
September 12, 2026 02:08
b8458bb to
e4f28d0
Compare
…er instance contentType() previously took no parameters, so a dialect could only report one fixed content-type for its lifetime -- insufficient for an API whose response framing (event-stream vs. a single JSON document) depends on a flag in the request body, since neither contentType() nor detect(String, HttpHeaders) offered any way to inspect it. Adds HttpRequestBody, a minimal read-only scalar-member accessor mirroring HttpHeaders, and changes contentType() to contentType(Kind, HttpHeaders, HttpRequestBody): Kind lets request and response resolve independently (a dialect's request body content-type can be fixed while its response varies), and the headers/body context lets that resolution depend on the actual request rather than being fixed at dialect-instance-creation time. Both parameters are nullable for callers without that context available. LlmTestDialect now resolves text/test-event-stream for a streaming response and application/test+json otherwise, exercising the new per-Kind, per-request resolution the stub previously couldn't express. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016NcAVwRPN1Pwjzpobr75w6
…ken detail Extend LlmFlushEx's block-lifecycle skeleton (from #2476) with the two places OpenAI's parallel completions and per-token detail need to survive in the vocabulary, per the issue's scope: - choiceIndex (default 0) on messageStart, blockStart, blockEnd, finish, and native/raw: the (choice, block) compound index's outer half. Anthropic is always choice 0, so every existing dialect mapping is unaffected; OpenAI's n > 1 becomes one messageStart per parallel completion, distinguished by choiceIndex. usage stays choiceIndex-free since every dialect reports it aggregated across choices, never per choice. - logProbability (nullable) on LlmDataEx: the one per-delta detail the vocabulary carries directly, for dialects exposing per-token detail (OpenAI logprobs) without reopening the DATA/FLUSH split from #2476 or growing the vocabulary for the full log-probability structure — richer detail than one value per token stays behind LlmNativeFlushEx. Documents the three lossiness cases as doc comments alongside the fields they concern: choiceIndex and logProbability both drop out on any cross-dialect route to Anthropic (structurally exactly one choice, no per-token detail); message-start input token counts are resolved by decoupling inputTokens into its own deferred usage event rather than emitting a placeholder on messageStart and correcting it later, so a source that discloses tokens late (OpenAI) just emits usage late instead of needing a correction event. Fixes #2481 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AYGZDrytLjmG2AQqUN1hsu
…lects Translates between each dialect's native streaming event sequence and the canonical vocabulary from #2557, in both directions: - LlmAnthropicEventMapper: holds input_tokens from message_start until the paired usage event at message_delta; tracks the currently open block's type to know when content_block_stop needs a canonical blockEnd (tool calls only) and to route content_block_delta payloads (text_delta vs input_json_delta) on encode. - LlmOpenAiEventMapper: translates OpenAI's tool-call-only index space into the canonical (Anthropic-shaped) block index via a per-stream map, and synthesizes blockEnd lazily -- deferred until the next tool call starts or the stream finishes, since OpenAI has no explicit block-close event. Unit-tested against both worked-example tables from the issue (message role/content/tool-call cardinality changes in each direction), plus the held-usage/already-consumed and lazy-blockEnd edge cases. Fixes #2482 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BHBkLjV2tcbNxMEgxkpSw
LlmDialectResolver dispatches path/header detection across every LlmDialect registered via LlmDialectFactorySpi, without hardcoding any dialect's signals. A configured fixed dialect name bypasses detection entirely, including when it matches no registered dialect. When detection matches more than one dialect, or none, resolution is ambiguous and returns null so the caller rejects the request rather than guessing. LlmOptionsConfig adds the optional server-kind `dialect` option (schema, config, and adapter) used to pin a fixed dialect. Fixes #2483 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkteS7C66qiVLGeEDJ2FP
…inEx.dialect Wire llm server (LlmServerFactory) to resolve the inbound dialect via LlmDialectResolver, decode framing through the matching LlmContentDecoderSpi (SSE today), and forward canonical DATA/FLUSH frames to the routed application binding with LlmBeginEx.dialect annotated. When no decoder is registered for the dialect's content-type, the body is forwarded opaquely as DATA followed by a terminal raw FLUSH. The reply direction is a generic, unmodified byte passthrough back to the network. Unroutable or unresolved-dialect requests are rejected by returning null from newStream, relying on the engine's existing auto-reset behavior. Covered by new k3po specs (sse.passthrough, opaque.fallback) with paired runtime (LlmServerIT) and peer-to-peer (NetworkIT/ApplicationIT) coverage, plus a LlmFunctionsTest unit test for the new k3po EL helpers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mHT1vrPATQGSdRcB8NNKM
… of stream LlmServerFactory calls a stream's LlmContentDecoder incrementally, once per network DATA frame, so a fragment-tolerant decoder (SSE) can report event boundaries as they're recognized. LlmJsonContentDecoder didn't tolerate this: it treated whatever bytes any single call received as the complete document, so a JSON body split across more than one DATA frame (arbitrarily likely, never guaranteed to arrive in one frame) produced a premature terminal flush per fragment instead of one flush for the whole document. Fix keeps LlmServerFactory's decode loop unchanged for every decoder: still called incrementally, fragment by fragment, as bytes arrive (no buffering of the whole body). LlmContentDecoder now also gets one call with an empty range after the network stream ends, so a decoder without a self-contained end-of-document marker can defer its terminal event to that call. LlmJsonContentDecoder forwards data on every non-empty call and only reports the terminal event on the empty one. Covered by a new k3po scenario (json.fragmented) whose network side splits the JSON body across two DATA frames via a mid-write flush, verified against a live engine (LlmServerIT), peer-to-peer (NetworkIT/ApplicationIT), and at the decoder unit level (LlmJsonContentDecoderTest). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mHT1vrPATQGSdRcB8NNKM
The dialect stack this branch builds on now resolves content-type per request rather than per dialect instance (contentType() gained Kind, HttpHeaders, and HttpRequestBody parameters), so a dialect's response framing can depend on a flag in the request body instead of being fixed for the dialect's lifetime. LlmServerFactory resolves content-type for the request direction, using the headers already extracted for dialect detection and no request-body context (unavailable at BEGIN time). LlmTestSseDialect/LlmTestJsonDialect report their fixed content-type regardless of kind/headers/body, matching their existing single-purpose role in the sse.passthrough/json.fragmented scenarios. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mHT1vrPATQGSdRcB8NNKM
jfallows
force-pushed
the
claude/stoic-sagan-9d81tf
branch
from
September 12, 2026 04:33
0510c5d to
5f11258
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
LlmServerFactory— thellm serverstream factory. OnBEGIN, resolves the inbound dialect viaLlmDialectResolver(from the:path/headers of the acceptedHttpBeginExFW), selects the matchingLlmContentDecoderfromLlmContentDecoderFactorykeyed by the dialect's content-type, and forwards decoded canonicalDATA/FLUSHframes to the routed application binding, withLlmBeginEx.dialectannotated on the forwardedBEGIN.llm client's job when dialects differ, tracked separately).LlmContentDecoderSpiimplementation exists yet), the body is forwarded opaquely asDATAfollowed by a terminal rawFLUSHonEND, rather than guessing at an undefined decode contract.nullfromnewStream, relying on the engine's existing auto-reset behavior rather than a bespoke reject handler.binding-mcp's server-side stream handling patterns (buffer-slot decode/resume,*Statebitmask, genericdo*frame builders).Covered by new k3po specs (
sse.passthrough,opaque.fallback) understreams/network/streams/application, each with paired runtime IT (LlmServerIT, against a live engine) and peer-to-peer IT (NetworkIT/ApplicationIT, scripts run directly against each other) coverage, plus a unit test for the new k3po EL helper functions (LlmFunctionsTest).Fixes #2484
This branch stacks on #2568 (dialect detection dispatcher and fixed-dialect config, #2483), which itself stacks on the earlier M1 protocol-scaffold PRs — so this diff includes those commits until they merge to
develop, at which point this PR's diff will shrink to just this framing-decode commit.Generated by Claude Code