feat(binding-llm): LlmDialectFactorySpi / LlmDialect exported SPI - #2556
Merged
Merged
Conversation
jfallows
force-pushed
the
claude/issue-2480-us0ae1
branch
2 times, most recently
from
September 12, 2026 01:39
92d9326 to
c543776
Compare
jfallows
force-pushed
the
claude/issue-2480-us0ae1
branch
from
September 14, 2026 22:59
dc5f206 to
efda042
Compare
This was referenced Sep 18, 2026
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
…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
…velope, not whole-body buffering Replaces LlmDialect's HttpHeaders/HttpRequestBody/contentType() with the engine's existing ModelEnvelope/ModelTransform (runtime/engine/.../model/), reusing machinery this codebase already has instead of inventing LLM-specific buffering to resolve text/event-stream vs. application/json, or a model name, by peeking one field of an otherwise-unbuffered body. detect(ModelEnvelope) folds :path/:method into the same envelope as ordinary headers -- no separate path parameter. supplyDecoder/supplyEncoder now take (Kind, ModelEnvelope) and return ModelTransform: a per-field stage that can extract a field (e.g. a model name) into the envelope while the body still flows through unchanged, mirroring KafkaExtractTransform (runtime/binding-kafka/.../cache/) -- so a caller reads that signal back off the envelope as decoding proceeds rather than buffering the whole body first to inspect it. contentType() is removed entirely: nothing in this shape needs it once the streaming/non-streaming signal is just another envelope entry a caller reads after extraction. common-json/JsonTransform is no longer used anywhere in this module now that the dialect SPI itself doesn't need it, so the dependency comes back out of module-info.java and pom.xml along with it. LlmTestDialect/LlmTestDialectFactorySpi become LlmTestConditionalDialect/ LlmTestConditionalDialectFactorySpi, since what they now demonstrate is exactly this: request detection and model-name extraction conditional on envelope contents, not a fixed per-instance answer. 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 30, 2026 04:24
22d3390 to
5b652f9
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
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(ModelEnvelope), plussupplyDecoder(Kind, ModelEnvelope)/supplyEncoder(Kind, ModelEnvelope)returning the engine's ownModelTransform(native ↔ canonical, field by field).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.ModelEnvelope/ModelTransform(runtime/engine/.../model/) are reused unmodified.detect(ModelEnvelope)folds a request's:method/:pathpseudo-headers into the same envelope as its ordinary headers — no separate path parameter.supplyDecoder/supplyEncoderreceive the per-streamModelEnvelopetoo, so a decoder can extract a field (e.g. a model name, or a streaming flag decidingtext/event-streamvs.application/json) into a named envelope entry while the field still flows through unchanged, mirroringKafkaExtractTransform(runtime/binding-kafka/.../cache/) — a caller reads that signal back off the envelope as decoding proceeds, rather than buffering the whole body up front just to peek at one field.LlmDialectFactorySpi/LlmDialectare exported from the start (unlike the internal content-codec SPI) —module-info.javaexportsthedialectpackage and declaresuses LlmDialectFactorySpi, withrequires transitiveonenginesinceModelEnvelope/ModelTransformappear in the exported public API.LlmDialectFactorySpiTest/LlmTestConditionalDialect/LlmTestConditionalDialectFactorySpi— unit tests exercising the contract via a stub dialect registered under test-scopeMETA-INF/services, mirroring this module's existing internal-SPI stub-test pattern. Coversdetect()reading folded-in:method/:path(includingModelEnvelope.NONE), asupplyDecoder(Kind.REQUEST, envelope)that extracts a field into the envelope while forwarding it unchanged (and correctly ignores an unrelated field), identity behavior for the remainingKind/direction combinations, 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 59 unit tests pass with full (1.00 ratio, 0 missed) jacoco coverage.Rebased directly onto
develop— the module scaffold, internal content-decoder SPI, and both content codecs this dialect SPI sits alongside are now ondevelop(via #2552/#2553), so this diff is scoped to just the dialect SPI's own commits.Fixes #2480
🤖 Generated with Claude Code
https://claude.ai/code/session_016NcAVwRPN1Pwjzpobr75w6