feat(binding-llm): LlmDialectFactorySpi / LlmDialect exported SPI - #2556
Open
jfallows wants to merge 7 commits into
Open
feat(binding-llm): LlmDialectFactorySpi / LlmDialect exported SPI#2556jfallows wants to merge 7 commits into
jfallows wants to merge 7 commits into
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
jfallows
force-pushed
the
claude/issue-2480-us0ae1
branch
from
September 11, 2026 20:20
a98acbf to
92d9326
Compare
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/issue-2480-us0ae1
branch
from
September 12, 2026 01:39
92d9326 to
c543776
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
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
Defines and exports the pluggable-dialect SPI for
binding-llm, modelled onbinding-mcp's within-binding SPI precedent (module-info.javaexportsthe SPI package,usesthe SPI, with built-inprovidesimplementations added later).LlmDialect(io.aklivity.zilla.runtime.binding.llm.dialect) —name(),detect(path, headers),contentType(Kind, HttpHeaders, HttpRequestBody), plussupplyDecoder(Kind)/supplyEncoder(Kind)returningcommon-json'sJsonTransform(native ↔ canonical).Kindis nested onLlmDialect, distinguishing the request and response directions since each has its own schema and mapping.LlmDialectFactorySpi— theServiceLoader-registered entry point (name()+create()), registered inMETA-INF/services/io.aklivity.zilla.runtime.binding.llm.dialect.LlmDialectFactorySpi.HttpHeaders— a minimal read-only header accessor fordetect(path, headers)andcontentType(...). No such abstraction previously existed in this codebase (nojakarta.ws.rsdependency anywhere), so this is a small Zilla-owned contract rather than pulling in JAX-RS for one method.HttpRequestBody— a minimal read-only scalar-member accessor for a request body, mirroringHttpHeaders. Added socontentType()can resolve per request (e.g. a streaming-capable dialect choosingtext/event-streamvs.application/jsonfrom astreamfield in the request body) instead of being fixed once when the dialect instance is created — a capability gap surfaced by a downstream dialect implementation, folded back into this SPI issue while its own PR is still open rather than needing a second signature change later.contentType()also takesKind, since a dialect's request and response content-types can differ and must resolve independently;headers/bodyare nullable for callers without that context.LlmDialectFactorySpi/LlmDialectare exported from the start (unlikeLlmContentDecoderSpi, which stays internal) —module-info.javanowexportsthedialectpackage and declaresuses LlmDialectFactorySpi, withrequires transitiveoncommon-jsonsinceJsonTransformappears in the exported public API.LlmDialectFactorySpiTest/LlmTestDialect/LlmTestDialectFactorySpi— unit tests exercising the contract via a stub dialect registered under test-scopeMETA-INF/services, mirroring this module's existingLlmContentDecoderSpi/LlmTestContentDecoderFactorySpipattern. Covers name/detect, per-Kind/per-requestcontentType()resolution (streaming vs. non-streaming, including a null body), identity-transform forwarding for bothKindvalues, andKind.values()/valueOf().No concrete dialect implementations yet (OpenAI/Anthropic land later, per the issue's acceptance criteria) —
usesis declared without a correspondingprovides.Verified
./mvnw clean verify -pl incubator/binding-llmpasses end-to-end: checkstyle (0 violations), license headers, and all 45 unit tests pass with full (1.00 ratio, 0 missed) jacoco coverage.This branch is built on top of
claude/issue-2479-2qhkfm(#2554, the SSE/JSON content decoders), so this diff includes #2554's commits until that merges todevelop— at which point this PR's diff will shrink to just this dialect SPI's commits.Fixes #2480
🤖 Generated with Claude Code
https://claude.ai/code/session_016NcAVwRPN1Pwjzpobr75w6