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
…odecSpi LlmContentDecoderSpi and LlmContentEncoderSpi (the latter previously living downstream in #2571) let a content-type register a decoder with no matching encoder, or vice versa, since each was ServiceLoader-discovered and dispatched independently. LlmContentCodecSpi makes "this content-type is fully supported" one type-enforced fact: a single contentType() key with both supplyDecoder() and supplyEncoder(), one META-INF/services registration per content-type, one LlmContentCodecFactory dispatching both directions. Pulls the internal/encode/ base package and its text/event-stream implementation (LlmContentEncoder, LlmSseContentEncoder) forward from #2571 so the collapse can happen where decode already lives, rather than forking that package ahead of its own introduction there; #2571 will need to rebase on top of this and drop its now-duplicate copies. LlmSseContentDecoder, LlmSseContentEncoder, LlmJsonContentDecoder widen from package-private to public (unchanged otherwise) since their new LlmSseContentCodecSpi/LlmJsonContentCodecSpi providers construct them from the sibling internal.codec package. Adds LlmJsonContentEncoder (new): the application/json inverse of LlmJsonContentDecoder, copying content bytes through unchanged with no framing on flush. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mw32oxEw24fLt5Ypakj6pH
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
…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
ab9987b to
5ca69a5
Compare
|
CI failed on
The robot's captured state at the Run 2 timeout shows the network-side backend script successfully matched the outbound HTTP This doesn't look like a fixture problem: Working hypothesis: I can't reproduce or debug this further from here — this sandbox can't run the k3po Generated by Claude Code Generated by Claude Code |
5ca69a5 to
13e12c5
Compare
…lient Chains an llm:server (auto-detecting dialect: openai from POST /v1/chat/completions) directly into an llm:client (dialect: openai) against a mock OpenAI backend, proving the server→client hop preserves framing byte-for-byte with no payload re-serialization. Adds config/roundtrip.openai.yaml wiring net0 (llm:server) → app0 (llm:client, internal) → net1 (external, mock backend), modeled on binding-tls's bridge.tls1.3.yaml chaining pattern. New streaming and non-streaming network scenarios reuse the wire content already proven by openai.streaming/openai.nonstreaming (including the tool-call streaming chunks spanning DATA and FLUSH), with deliberate insignificant whitespace (e.g. "\"model\": \"gpt-4\"") added to request/response bodies at every hop — this survives only if the body is forwarded unchanged rather than parsed and re-serialized, since same-dialect resolution takes LlmClientFactory's ModelTransform.NONE fast path and the server's json-model pipeline forwards the body unmodified. This scenario chains two different bindings across two different wire legs (net0's pre-server-decode bytes vs net1's post-client-encode bytes) rather than describing one client/server exchange, so — like binding-tls's BridgeIT — it has no NetworkIT peer-to-peer counterpart; only the engine-backed LlmRoundtripIT applies. Could not run `./mvnw verify` for the new LlmRoundtripIT in this sandboxed session: the reactor's maven-notice-plugin license-mapping check fails on unmapped artifacts regardless of network access, the same pre-existing, environment-specific limitation noted in #2576's own test plan. Verified instead: `./mvnw checkstyle:check` (0 violations) and `./mvnw package` (compiles cleanly) for incubator/binding-llm.spec and incubator/binding-llm. CI should confirm the full verify lifecycle. Closes #2491 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UogzKHEA7zC9PgHBkGwu8p
|
Update: rebased onto
This rules out the dialect-detection ambiguity as the cause (that fix landed and made no difference) and rules out ordinary test-order flakiness (reproduces identically across independent CI runs with different code in between). This is a real, deterministic bug in Still can't reproduce or fix this myself (no Generated by Claude Code Generated by Claude Code |
…ng coverage Revert the LlmRoundtripIT approach (chaining llm:server directly into llm:client in one config): it combined two bindings under test in a single IT, which this repo avoids in favor of verifying each binding independently and inferring combinations. PR #2576 already authored the full request/streaming/nonstreaming × network/application script matrix, and ApplicationIT/NetworkIT already prove each application-level and network-level script pair is self-consistent. But engine-backed coverage was split: LlmServerIT only exercised openai.request, and LlmClientIT only exercised openai.streaming/openai.nonstreaming. Composing the already-existing peer-to-peer proofs with LlmServerIT's and LlmClientIT's own per-binding proofs already establishes the round-trip identity claim from #2491 (framing decode/re-encode byte-identical, no payload parsed via the existing .raw() flush-ext assertions, including tool-call streaming spanning DATA and FLUSH) without ever running server+client chained. Adds the two missing LlmServerIT methods (openai.streaming, openai.nonstreaming) using the scripts and config that already exist — no new files needed. Considered adding a symmetric openai.request method to LlmClientIT, but net/openai.request/server.rpt was authored as the peer for the server-detection scenario (path /v1/chat/completions) rather than as what llm:client's own fixed-dialect encoder actually emits (path "/", per the already-passing openai.streaming/openai.nonstreaming network scripts), so pairing them would fail for a script-mismatch reason unrelated to any real defect; left LlmClientIT unchanged rather than author a new script pair for it. Closes #2491 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UogzKHEA7zC9PgHBkGwu8p
|
Root cause found and fixed for the
Fix ( Verified: Generated by Claude Code |
|
Correction to my previous comment: the The actual gap: Fixed properly in Verified directly against Generated by Claude Code |
…lient Chains an llm:server (auto-detecting dialect: openai from POST /v1/chat/completions) directly into an llm:client (dialect: openai) against a mock OpenAI backend, proving the server→client hop preserves framing byte-for-byte with no payload re-serialization. Adds config/roundtrip.openai.yaml wiring net0 (llm:server) → app0 (llm:client, internal) → net1 (external, mock backend), modeled on binding-tls's bridge.tls1.3.yaml chaining pattern. New streaming and non-streaming network scenarios reuse the wire content already proven by openai.streaming/openai.nonstreaming (including the tool-call streaming chunks spanning DATA and FLUSH), with deliberate insignificant whitespace (e.g. "\"model\": \"gpt-4\"") added to request/response bodies at every hop — this survives only if the body is forwarded unchanged rather than parsed and re-serialized, since same-dialect resolution takes LlmClientFactory's ModelTransform.NONE fast path and the server's json-model pipeline forwards the body unmodified. This scenario chains two different bindings across two different wire legs (net0's pre-server-decode bytes vs net1's post-client-encode bytes) rather than describing one client/server exchange, so — like binding-tls's BridgeIT — it has no NetworkIT peer-to-peer counterpart; only the engine-backed LlmRoundtripIT applies. Could not run `./mvnw verify` for the new LlmRoundtripIT in this sandboxed session: the reactor's maven-notice-plugin license-mapping check fails on unmapped artifacts regardless of network access, the same pre-existing, environment-specific limitation noted in #2576's own test plan. Verified instead: `./mvnw checkstyle:check` (0 violations) and `./mvnw package` (compiles cleanly) for incubator/binding-llm.spec and incubator/binding-llm. CI should confirm the full verify lifecycle. Closes #2491 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UogzKHEA7zC9PgHBkGwu8p
…ng coverage Revert the LlmRoundtripIT approach (chaining llm:server directly into llm:client in one config): it combined two bindings under test in a single IT, which this repo avoids in favor of verifying each binding independently and inferring combinations. PR #2576 already authored the full request/streaming/nonstreaming × network/application script matrix, and ApplicationIT/NetworkIT already prove each application-level and network-level script pair is self-consistent. But engine-backed coverage was split: LlmServerIT only exercised openai.request, and LlmClientIT only exercised openai.streaming/openai.nonstreaming. Composing the already-existing peer-to-peer proofs with LlmServerIT's and LlmClientIT's own per-binding proofs already establishes the round-trip identity claim from #2491 (framing decode/re-encode byte-identical, no payload parsed via the existing .raw() flush-ext assertions, including tool-call streaming spanning DATA and FLUSH) without ever running server+client chained. Adds the two missing LlmServerIT methods (openai.streaming, openai.nonstreaming) using the scripts and config that already exist — no new files needed. Considered adding a symmetric openai.request method to LlmClientIT, but net/openai.request/server.rpt was authored as the peer for the server-detection scenario (path /v1/chat/completions) rather than as what llm:client's own fixed-dialect encoder actually emits (path "/", per the already-passing openai.streaming/openai.nonstreaming network scripts), so pairing them would fail for a script-mismatch reason unrelated to any real defect; left LlmClientIT unchanged rather than author a new script pair for it. Closes #2491 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UogzKHEA7zC9PgHBkGwu8p
…lient Chains an llm:server (auto-detecting dialect: openai from POST /v1/chat/completions) directly into an llm:client (dialect: openai) against a mock OpenAI backend, proving the server→client hop preserves framing byte-for-byte with no payload re-serialization. Adds config/roundtrip.openai.yaml wiring net0 (llm:server) → app0 (llm:client, internal) → net1 (external, mock backend), modeled on binding-tls's bridge.tls1.3.yaml chaining pattern. New streaming and non-streaming network scenarios reuse the wire content already proven by openai.streaming/openai.nonstreaming (including the tool-call streaming chunks spanning DATA and FLUSH), with deliberate insignificant whitespace (e.g. "\"model\": \"gpt-4\"") added to request/response bodies at every hop — this survives only if the body is forwarded unchanged rather than parsed and re-serialized, since same-dialect resolution takes LlmClientFactory's ModelTransform.NONE fast path and the server's json-model pipeline forwards the body unmodified. This scenario chains two different bindings across two different wire legs (net0's pre-server-decode bytes vs net1's post-client-encode bytes) rather than describing one client/server exchange, so — like binding-tls's BridgeIT — it has no NetworkIT peer-to-peer counterpart; only the engine-backed LlmRoundtripIT applies. Could not run `./mvnw verify` for the new LlmRoundtripIT in this sandboxed session: the reactor's maven-notice-plugin license-mapping check fails on unmapped artifacts regardless of network access, the same pre-existing, environment-specific limitation noted in #2576's own test plan. Verified instead: `./mvnw checkstyle:check` (0 violations) and `./mvnw package` (compiles cleanly) for incubator/binding-llm.spec and incubator/binding-llm. CI should confirm the full verify lifecycle. Closes #2491 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UogzKHEA7zC9PgHBkGwu8p
…ng coverage Revert the LlmRoundtripIT approach (chaining llm:server directly into llm:client in one config): it combined two bindings under test in a single IT, which this repo avoids in favor of verifying each binding independently and inferring combinations. PR #2576 already authored the full request/streaming/nonstreaming × network/application script matrix, and ApplicationIT/NetworkIT already prove each application-level and network-level script pair is self-consistent. But engine-backed coverage was split: LlmServerIT only exercised openai.request, and LlmClientIT only exercised openai.streaming/openai.nonstreaming. Composing the already-existing peer-to-peer proofs with LlmServerIT's and LlmClientIT's own per-binding proofs already establishes the round-trip identity claim from #2491 (framing decode/re-encode byte-identical, no payload parsed via the existing .raw() flush-ext assertions, including tool-call streaming spanning DATA and FLUSH) without ever running server+client chained. Adds the two missing LlmServerIT methods (openai.streaming, openai.nonstreaming) using the scripts and config that already exist — no new files needed. Considered adding a symmetric openai.request method to LlmClientIT, but net/openai.request/server.rpt was authored as the peer for the server-detection scenario (path /v1/chat/completions) rather than as what llm:client's own fixed-dialect encoder actually emits (path "/", per the already-passing openai.streaming/openai.nonstreaming network scripts), so pairing them would fail for a script-mismatch reason unrelated to any real defect; left LlmClientIT unchanged rather than author a new script pair for it. Closes #2491 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UogzKHEA7zC9PgHBkGwu8p
…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
The sys: namespace's patch contract (Binding.system()/Exporter.system())
lets a component contribute a shared binding (e.g. http_client) but has
no pre-seeded slot for a shared catalog, so a patch adding one has
nothing to append into. Pre-seed catalogs: {} alongside the existing
bindings: {} in the base sys namespace skeleton, the same "pre-seed the
extension point" convention already used for the JSON-schema *-ext
scaffolds, so any component can share a catalog-backed resource the way
bindings are already shared.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mHT1vrPATQGSdRcB8NNKM
…ect schemas Adds LlmDialectFactorySpi.schema(Kind), letting a dialect contribute a URL to its own JSON schema for the request or response direction without knowing anything about catalogs or patches. LlmBinding.system() enumerates every registered dialect, reads each contributed schema, and generates a sys: namespace patch adding one shared inline catalog (llm_dialects) with a <dialect>.request/<dialect>.response subject per schema a dialect contributes -- built once, at engine startup, since the dialect set is ServiceLoader-discovered off the classpath and therefore fixed for the JVM's life, the same way sys: already shares a binding (e.g. http_client) across every binding that references it. LlmDataUrlStreamHandler decodes a base64 data: URL (RFC 2397) in memory, scoped to a single URL via the URL.of(URI, URLStreamHandler) factory -- no temporary file, no globally-registered protocol handler -- used to hand the generated patch to Binding.system()'s URL-returning contract without writing it to disk. LlmBeginEx gains contentType and model fields alongside dialect, for a server-kind stream factory to stamp once it has resolved the dialect and read the request's content-type. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mHT1vrPATQGSdRcB8NNKM
… LlmBeginEx.dialect Implements request/response framing decode-and-forward for the LLM server binding (#2484): LlmServerFactory drives the request body through the resolved dialect's ModelPipeline as bytes arrive off the wire, forwards transformed content to app0 incrementally rather than buffering the whole body, and threads the resolved dialect name onto LlmBeginEx so app0 can see which wire dialect produced the request. Flow control between the client, this binding, and app0 is enforced with dedicated decodeSlot/encodeSlot buffers on each side of the exchange (LlmServer for the network-facing leg, LlmStream for the app-facing leg), each granting credit strictly from its own local slot occupancy rather than copying a peer's sequence numbers across independent byte domains. LlmState tracks per-direction open/closing/closed transitions and end-of-stream deferral while a buffer is still draining. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mHT1vrPATQGSdRcB8NNKM
…fferPool handles DefaultBufferPool.buffer(slot) rewraps and returns a single shared mutable field per pool instance, so holding a buffer reference across a nested call that fetches a different slot from the same pool silently repoints it. LlmServer.decodeNetwork() directly, synchronously calls LlmStream's request-relay staging method as a plain nested Java method call, so a single pool instance backing both the network-decode slot and the app0 request-relay slot would alias between them. Give LlmServerFactory two BufferPool handles instead of one: decodePool (network decode) and encodePool (app0 relay, both directions), obtained via context.bufferPool() and .duplicate() -- matching McpServerFactory's decodePool/encodePool precedent. The two relay directions sharing encodePool (the reply-direction slot on LlmServer and the request-direction slot on LlmStream) never appear in the same call stack: cross-binding accept() is ring-buffer-mediated and dispatched on a later engine tick, not a nested call, so one encodePool instance can't alias between them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mHT1vrPATQGSdRcB8NNKM
…openai dialect
LlmOpenAiRequestTransform renamed a fixed set of top-level fields but never
observed model, so LlmServerFactory.doAppBegin's server.envelope.get("model", 0)
read right after running the request through this transform came back empty and
LlmBeginEx.model was never stamped for real dialect: openai traffic.
Mirrors LlmTestPermissiveDialect's inline ModelExtractTransform (and
KafkaExtractTransform's own pattern): on a FIELD event at $.model, copy the
value into the envelope alongside the existing rename-or-forward decision,
without touching the RENAMES table. LlmOpenAiResponseTransform needs no
equivalent change -- nothing reads model back off a response.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SMybNYCLJEWTwCjDscepZm
…requests CI's LlmServerIT.shouldRejectRequestWithUnresolvedDialect failed: detect() matched on :method/:path alone, so it ambiguously co-matched every existing k3po server fixture -- all of which hit the same /v1/chat/completions path with a test-specific content-type (application/vnd.zilla.test-permissive+json, application/vnd.zilla.test-strict+json) to select a *different* dialect unambiguously. Only one failure surfaced in CI because failsafe stops after the first failure, but the same ambiguity affected every other fixture in that class too -- confirmed by LlmServerIT going from 6 run/1 failed/1 skipped to 8/8 passing with this fix, and LlmClientIT unaffected at 4/4. detect() now also requires content-type: application/json, the only content-type a genuine OpenAI request ever carries, so a request to the same path with a different dialect's own content-type no longer collides. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SMybNYCLJEWTwCjDscepZm
The llm.schema.patch.json allowed options.server on kind:server bindings, but LlmServerFactory never reads binding.options.server — only LlmClientFactory dials it as the upstream endpoint. Restrict the kind:server options schema to dialect (fixed-dialect mode), matching actual runtime usage and the milestone's example configs. Add LlmSchemaValidationTest exercising the full EngineConfigReader pipeline against real zilla.yaml text for both kind:server and kind:client, covering acceptance (bare server, fixed-dialect server, full client) and rejection (server option on kind:server, missing required client fields, malformed server pattern, unknown kind). Fixes #2488 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Aawd4ZFaUE3FgQnzkABj9F
LlmOptionsConfigAdapterTest: round-trip dialect+server together, an explicit server-absent case, and the adapter's silent no-op on a malformed server string (schema is the actual gatekeeper there). LlmSchemaValidationTest: reject additionalProperties on both kind:server and kind:client, reject non-string dialect/server values, and accept an empty options object for kind:server. Closes #2489. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfesxqkqmMKPC87ydouhGR
4901677 to
e7df219
Compare
Adds paired client.rpt/server.rpt k3po scenarios exercising the real `openai` LlmDialect end-to-end (dialect detection, framing decode/encode), rather than the synthetic test-sse/test-conditional dialects existing scenarios use: - openai.request (network+application): llm server detects `dialect: openai` from `POST /v1/chat/completions` alone (no x-llm-dialect header) and forwards the decoded JSON request body, terminated by the JSON content-decoder's single terminal flush. - openai.streaming (network+application): llm client same-dialect round-trip with a `stream:true` request and a realistic OpenAI SSE response sequence (role chunk, content chunk, tool-call start and argument-fragment chunks, finish_reason chunk, usage chunk, [DONE]). - openai.nonstreaming (network+application): llm client same-dialect round-trip with a `stream:false` request and a plain-JSON response carrying top-level usage. New scenarios are wired into LlmServerIT/LlmClientIT (engine-driven) and ApplicationIT/NetworkIT (protocol self-consistency), plus a new client.openai.yaml config pinning `dialect: openai`. All four IT classes compile cleanly and pass checkstyle; the k3po `verify` lifecycle itself could not be exercised in this sandboxed session due to a pre-existing, repo-wide maven-notice-plugin license-mapping gap unrelated to this change (same limitation noted in prior binding-llm PRs' test plans) — CI should confirm. Fixes #2490 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5Pw5eDcghnK7LmZ11QA7u
…p+passthrough server LlmServerFactory no longer decodes request framing into typed DATA/FLUSH events -- it detects the dialect, resolves content-type directly off the real Content-Type header, runs the dialect's decoder through a json-model ModelPipeline, and forwards the pipeline's own output as plain DATA frames, stamping dialect/contentType on the app-facing LlmBeginEx. Update the openai.request client/server pair (network + application) to match: a real content-type header drives LlmBeginEx.contentType, and the terminal LlmNativeFlushEx this scenario previously asserted is gone since the server no longer produces one for plain JSON forwarding. model is intentionally left unasserted here: the real openai dialect does not yet extract it into the envelope (only the test-permissive test dialect does), unlike the design this fixture set was originally written against. Part of the broader LlmDialect/ModelPipeline SPI redesign already landed on this branch's upstream chain; openai.streaming/openai.nonstreaming still need the same treatment for LlmClientFactory's same-dialect path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5Pw5eDcghnK7LmZ11QA7u
…i client fixtures LlmClientFactory always reads content-type off real HTTP headers -- the request's own if the app sets one, else "application/json" -- and its response codec is selected by looking up the backend's real Content-Type response header via LlmContentCodecFactory (registered for "text/event-stream" and "application/json"). A response header carrying neither falls through to opaque forwarding instead of the intended codec. openai.streaming/openai.nonstreaming previously omitted these headers entirely, so despite exercising a real dialect they were never actually routing through the SSE/JSON content codecs LlmClientIT is meant to cover. Add "content-type: application/json" to the outbound request and "content-type: text/event-stream"/"application/json" to the backend response on both the network and application sides, matching every other LlmClientIT-driven fixture's convention (same.dialect, cross.dialect). openai.nonstreaming was also missing the terminal raw LlmFlushEx that LlmJsonContentDecoder always emits alongside its single DATA -- add it to both the read and write side, matching the already-correct openai.streaming pattern. Also make openai.request's application/server.rpt echo the same request body back as its reply (rather than an unrelated "response body" literal), matching request.valid's own client/server symmetry convention, with the network pair's expected reply content updated to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5Pw5eDcghnK7LmZ11QA7u
…eginEx The #2487 session landed model extraction for the real openai dialect (LlmOpenAiRequestTransform now observes $.model and copies it into the ModelEnvelope, mirroring LlmTestPermissiveDialect's ModelExtractTransform). Now that LlmServerFactory.doAppBegin actually stamps LlmBeginEx.model for dialect: openai traffic, assert it in the one fixture pair that exercises server-side model extraction (openai.streaming/openai.nonstreaming never touch model at all -- LlmClientFactory doesn't read or stamp it in either direction). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5Pw5eDcghnK7LmZ11QA7u
… traffic LlmClientFactory always constructed and drove a schema-validating JSON model pipeline for both request and response, even for same-dialect traffic where the design intends pure raw byte relay with no transform. For a dialect that declares no JSON schema for a direction (openai declares none for either), the pipeline's schema resolution always returns NO_SCHEMA_ID, so every same-dialect request or response got REJECTED and the stream was torn down -- reproduced directly against JsonModelHandlerImpl/JsonModelDecoderPipeline with the same no-schema/ModelTransform.NONE configuration LlmClientFactory builds. Skip constructing requestPipeline/responsePipeline entirely when source == target, forwarding request and response bytes directly (still through the content-type codec's own encoder/decoder for framing) instead of driving a pipeline with nothing to validate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5Pw5eDcghnK7LmZ11QA7u
…t client traffic" This reverts commit 45e5e52.
… schemas LlmOpenAiDialectFactorySpi previously declared no schema for either direction, so the shared sys:llm_dialects catalog had no "openai.request"/ "openai.response" subject and the ModelPipeline LlmClientFactory drives for openai traffic (same-dialect included) always resolved NO_SCHEMA_ID and rejected every value, regardless of content -- there was no actual validation happening for openai traffic at all. Add openai.request.schema.json/openai.response.schema.json describing the real Chat Completions wire shapes: request requires model/messages and types the other fields LlmOpenAiRequestTransform's rename table and model-extraction both recognize (stream, max_tokens, top_p, n, presence_penalty, frequency_penalty, tool_choice, response_format, ...); response types choices[]/usage without requiring any top-level member, since a streaming chunk carries only a subset (delta vs message, finish_reason, tool_calls[], usage) of what a single non-streaming completion carries all at once. Verified directly against JsonModelHandlerImpl/JsonModelDecoderPipeline (the same construction LlmClientFactory drives) that all nine request/ response payloads used by the openai.request/openai.streaming/ openai.nonstreaming k3po fixtures validate as COMPLETE against these schemas, not REJECTED. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5Pw5eDcghnK7LmZ11QA7u
…heck Asserting the schema's required fields and specific property keys in a unit test duplicates what the openai.request/openai.streaming/ openai.nonstreaming k3po fixtures already verify against a live engine -- those fixtures are the actual spec for what these schemas must accept, per this repo's test-first discipline. Narrow the unit test to what a factory-level test is actually entitled to check: the schema resource for each Kind exists and parses as a JSON object. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5Pw5eDcghnK7LmZ11QA7u
Engine.java's bootstrap (bindings.stream().map(Binding::system)...) calls LlmBinding.system() -> LlmSystemNamespaceGenerator.generate() for every engine startup in this module, which invokes schema(Kind) for every registered LlmDialectFactorySpi (openai included) for both REQUEST and RESPONSE and reads the resource -- unconditionally, on every LlmServerIT/ LlmClientIT/ApplicationIT/NetworkIT run in the module, not just openai-specific scenarios. A missing or unreadable schema resource would already fail engine bootstrap loudly. The unit test duplicated coverage the k3po ITs already provide for free. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5Pw5eDcghnK7LmZ11QA7u
Adds the negative-path coverage the openai.request/streaming/nonstreaming scenarios never exercised: an invalid document actually gets rejected, not silently forwarded. openai.request.invalid (network only, mirroring request.rejected.schema's existing single-sided pattern): a real openai-dialect request missing the required "messages" field -- llm(server) rejects it before ever opening an app-facing stream, observed as the k3po connect script itself getting aborted. openai.response.invalid (both application and network, mirroring the full openai.streaming/nonstreaming layout): a real backend response with "choices" as a string instead of an array -- llm(client) rejects it and aborts app0 instead of forwarding the malformed document. Wired into LlmServerIT/LlmClientIT (engine-driven) and ApplicationIT/NetworkIT (protocol self-consistency), matching the existing openai.* scenario conventions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5Pw5eDcghnK7LmZ11QA7u
…i traffic llm(client) computed requestContentType from llmBeginEx.contentType() with a wrong null-check: the flyweight accessor itself is never null even when the underlying string16 field is unset, so the fallback to CONTENT_TYPE_JSON never triggered and null flowed into the outbound HTTP begin's content-type header, breaking header matching on the network side and hanging every k3po scenario that omits contentType on the app-side llm:beginEx (which is the common case -- the field is only meant to be set for cross-dialect routes). Check contentType().asString() directly instead, matching the existing idiom one line above for the dialect field. Also stop routing same-dialect responses through a schema-validating ModelPipeline: responsePipeline was unconditionally constructed regardless of dialect, so a same-dialect stream (zero decode/encode/transform by design, per LlmOpenAiResponseTransform's own documented contract) still paid for JSON-schema validation on every response chunk, including the OpenAI SSE `[DONE]` sentinel, which is not JSON and got truncated by the validator. responsePipeline is now null for sameDialect, and forwardResponseContent forwards raw bytes in that case, mirroring how requestPipeline already goes null when no encoder resolves. Removes LlmClientIT.shouldRejectInvalidOpenAiResponse: it asserted schema rejection of an invalid response body on a same-dialect (single-dialect) client config, which cannot validate anything now that same-dialect responses bypass the model pipeline entirely -- this was always the intended contract, just not one this scenario could have exercised. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5Pw5eDcghnK7LmZ11QA7u
…e [DONE] sentinel The previous commit disabled response schema validation entirely for same-dialect llm(client) traffic to work around the OpenAI SSE `[DONE]` sentinel getting mangled by the JSON model pipeline. That was the wrong fix: `[DONE]` isn't JSON at all (it's an SSE-level stream-termination token, not a chat-completion chunk), so no JSON transform can "honor identity" for it -- there's no valid parse to preserve. The actual defect was routing a non-JSON control token into JSON-schema validation at all, not the presence of validation itself. Restores responsePipeline unconditionally (same-dialect responses are schema-validated exactly like cross-dialect ones), and instead adds a narrow, explicit bypass in forwardResponseContent for the literal `[DONE]` bytes specifically, forwarding them raw before they ever reach the model pipeline. Genuine JSON content -- valid or invalid, same-dialect or cross-dialect -- is still schema-validated and rejected on violation. Restores LlmClientIT.shouldRejectInvalidOpenAiResponse, which the previous commit had removed as no longer exercisable; it now passes again since same-dialect responses are validated once more. Also corrects LlmOpenAiResponseTransform's class Javadoc, which overstated the same-dialect bypass as covering the whole path rather than just the non-JSON sentinel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5Pw5eDcghnK7LmZ11QA7u
… responses
LlmServerFactory.LlmStream.onAppBegin forwarded app0's reply BEGIN
extension to net0 verbatim. Since the k3po app-side accept scripts (and
presumably any conformant llm app) never write an explicit begin.ext
before their response body, this extension is empty -- meaning every
llm(server) HTTP response went out with no status line at all, not even
":status 200". That's invalid per HTTP, and any net-side reader
asserting on the response BEGIN (matching ":status") would wait forever
for bytes that would never arrive.
This only ever surfaced as a hang on `shouldDetectOpenAiDialectFromPath`
because the openai scenario's client script is the only one that
actually asserts on the reply BEGIN's ":status" header; existing passing
scenarios (e.g. request.valid) read straight into the body without
checking it, silently tolerating the missing status line.
doNetBegin now builds a real HttpBeginExFW itself (":status 200",
plus "content-type" echoing the same contentType recorded from the
request) instead of relaying whatever the app happened to send.
Also fixes streams/network/openai.request.invalid/client.rpt: it ended
with `read aborted`, which is the accept-side spelling for observing a
peer's abort -- the connect-side script that unilaterally tears down its
own write direction after sending invalid content uses `write abort`,
matching the established sibling scenario (request.rejected.schema).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5Pw5eDcghnK7LmZ11QA7u
…actory into LlmDialect LlmClientFactory hardcoded the literal "[DONE]" bytes as a static constant and compared incoming response chunks against it directly -- OpenAI-specific wire knowledge baked into the dialect-generic client factory. Any other dialect with its own out-of-band stream-termination convention would have needed a second such literal added to the same generic file. Adds LlmDialect.terminator(Kind), returning the literal byte sequence a dialect's kind stream uses to signal completion out of band from any document, or null when it has none. Plain abstract method (no default), matching every other method on this interface -- implemented directly by all seven current dialects: LlmOpenaiDialect returns the real "[DONE]" bytes for RESPONSE (null for REQUEST, since chat completions requests are always a single plain JSON body), the six test dialects all return null. LlmClientFactory resolves target.terminator(Kind.RESPONSE) once per stream at construction (mirroring how requestEncoder/responsePipeline are already resolved once), and forwardResponseContent now wraps the observed slice into a reused comparison flyweight (comparisonRO, following the same reuse-a-field pattern OctetsFW's own valueRO uses) and compares it against the resolved terminator via DirectBufferEx.equals() -- a positive, per-dialect byte match, not an inference from any model-pipeline rejection reason, so a genuinely malformed or truncated chunk still reaches the pipeline and gets rejected rather than being silently forwarded as if it were the terminator. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5Pw5eDcghnK7LmZ11QA7u
151acc0 to
86aed4c
Compare
…lient Chains an llm:server (auto-detecting dialect: openai from POST /v1/chat/completions) directly into an llm:client (dialect: openai) against a mock OpenAI backend, proving the server→client hop preserves framing byte-for-byte with no payload re-serialization. Adds config/roundtrip.openai.yaml wiring net0 (llm:server) → app0 (llm:client, internal) → net1 (external, mock backend), modeled on binding-tls's bridge.tls1.3.yaml chaining pattern. New streaming and non-streaming network scenarios reuse the wire content already proven by openai.streaming/openai.nonstreaming (including the tool-call streaming chunks spanning DATA and FLUSH), with deliberate insignificant whitespace (e.g. "\"model\": \"gpt-4\"") added to request/response bodies at every hop — this survives only if the body is forwarded unchanged rather than parsed and re-serialized, since same-dialect resolution takes LlmClientFactory's ModelTransform.NONE fast path and the server's json-model pipeline forwards the body unmodified. This scenario chains two different bindings across two different wire legs (net0's pre-server-decode bytes vs net1's post-client-encode bytes) rather than describing one client/server exchange, so — like binding-tls's BridgeIT — it has no NetworkIT peer-to-peer counterpart; only the engine-backed LlmRoundtripIT applies. Could not run `./mvnw verify` for the new LlmRoundtripIT in this sandboxed session: the reactor's maven-notice-plugin license-mapping check fails on unmapped artifacts regardless of network access, the same pre-existing, environment-specific limitation noted in #2576's own test plan. Verified instead: `./mvnw checkstyle:check` (0 violations) and `./mvnw package` (compiles cleanly) for incubator/binding-llm.spec and incubator/binding-llm. CI should confirm the full verify lifecycle. Closes #2491 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UogzKHEA7zC9PgHBkGwu8p
…ng coverage Revert the LlmRoundtripIT approach (chaining llm:server directly into llm:client in one config): it combined two bindings under test in a single IT, which this repo avoids in favor of verifying each binding independently and inferring combinations. PR #2576 already authored the full request/streaming/nonstreaming × network/application script matrix, and ApplicationIT/NetworkIT already prove each application-level and network-level script pair is self-consistent. But engine-backed coverage was split: LlmServerIT only exercised openai.request, and LlmClientIT only exercised openai.streaming/openai.nonstreaming. Composing the already-existing peer-to-peer proofs with LlmServerIT's and LlmClientIT's own per-binding proofs already establishes the round-trip identity claim from #2491 (framing decode/re-encode byte-identical, no payload parsed via the existing .raw() flush-ext assertions, including tool-call streaming spanning DATA and FLUSH) without ever running server+client chained. Adds the two missing LlmServerIT methods (openai.streaming, openai.nonstreaming) using the scripts and config that already exist — no new files needed. Considered adding a symmetric openai.request method to LlmClientIT, but net/openai.request/server.rpt was authored as the peer for the server-detection scenario (path /v1/chat/completions) rather than as what llm:client's own fixed-dialect encoder actually emits (path "/", per the already-passing openai.streaming/openai.nonstreaming network scripts), so pairing them would fail for a script-mismatch reason unrelated to any real defect; left LlmClientIT unchanged rather than author a new script pair for it. Closes #2491 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UogzKHEA7zC9PgHBkGwu8p
…lient Chains an llm:server (auto-detecting dialect: openai from POST /v1/chat/completions) directly into an llm:client (dialect: openai) against a mock OpenAI backend, proving the server→client hop preserves framing byte-for-byte with no payload re-serialization. Adds config/roundtrip.openai.yaml wiring net0 (llm:server) → app0 (llm:client, internal) → net1 (external, mock backend), modeled on binding-tls's bridge.tls1.3.yaml chaining pattern. New streaming and non-streaming network scenarios reuse the wire content already proven by openai.streaming/openai.nonstreaming (including the tool-call streaming chunks spanning DATA and FLUSH), with deliberate insignificant whitespace (e.g. "\"model\": \"gpt-4\"") added to request/response bodies at every hop — this survives only if the body is forwarded unchanged rather than parsed and re-serialized, since same-dialect resolution takes LlmClientFactory's ModelTransform.NONE fast path and the server's json-model pipeline forwards the body unmodified. This scenario chains two different bindings across two different wire legs (net0's pre-server-decode bytes vs net1's post-client-encode bytes) rather than describing one client/server exchange, so — like binding-tls's BridgeIT — it has no NetworkIT peer-to-peer counterpart; only the engine-backed LlmRoundtripIT applies. Could not run `./mvnw verify` for the new LlmRoundtripIT in this sandboxed session: the reactor's maven-notice-plugin license-mapping check fails on unmapped artifacts regardless of network access, the same pre-existing, environment-specific limitation noted in #2576's own test plan. Verified instead: `./mvnw checkstyle:check` (0 violations) and `./mvnw package` (compiles cleanly) for incubator/binding-llm.spec and incubator/binding-llm. CI should confirm the full verify lifecycle. Closes #2491 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UogzKHEA7zC9PgHBkGwu8p
…ng coverage Revert the LlmRoundtripIT approach (chaining llm:server directly into llm:client in one config): it combined two bindings under test in a single IT, which this repo avoids in favor of verifying each binding independently and inferring combinations. PR #2576 already authored the full request/streaming/nonstreaming × network/application script matrix, and ApplicationIT/NetworkIT already prove each application-level and network-level script pair is self-consistent. But engine-backed coverage was split: LlmServerIT only exercised openai.request, and LlmClientIT only exercised openai.streaming/openai.nonstreaming. Composing the already-existing peer-to-peer proofs with LlmServerIT's and LlmClientIT's own per-binding proofs already establishes the round-trip identity claim from #2491 (framing decode/re-encode byte-identical, no payload parsed via the existing .raw() flush-ext assertions, including tool-call streaming spanning DATA and FLUSH) without ever running server+client chained. Adds the two missing LlmServerIT methods (openai.streaming, openai.nonstreaming) using the scripts and config that already exist — no new files needed. Considered adding a symmetric openai.request method to LlmClientIT, but net/openai.request/server.rpt was authored as the peer for the server-detection scenario (path /v1/chat/completions) rather than as what llm:client's own fixed-dialect encoder actually emits (path "/", per the already-passing openai.streaming/openai.nonstreaming network scripts), so pairing them would fail for a script-mismatch reason unrelated to any real defect; left LlmClientIT unchanged rather than author a new script pair for it. Closes #2491 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UogzKHEA7zC9PgHBkGwu8p
Description
Closes out issue #2490 (k3po test fixtures for an OpenAI-shaped
/v1/chat/completionsbackend, streaming + non-streaming, for use inllm server/llm clientintegration tests).Per repo convention (
specs/AGENTS.md), the backend side is faked entirely with paired k3poclient.rpt/server.rptscripts — no standalone mock server process and noexamples/llm.proxyexample. Existingbinding-llmscenarios (same.dialect,cross.dialect, etc.) only exercise synthetic test dialects; these new scenarios exercise the realdialect: openaiend-to-end for the first time:openai.request(network + application):llm serverdetectsdialect: openaipurely fromPOST /v1/chat/completions(nox-llm-dialectheader needed), resolvescontent-typedirectly off the real request header, and forwards the request body through ajson-modelModelPipeline— the dialect's decoder observesmodelas a side effect (envelope.set("model", ...)) while the body flows through unchanged.LlmBeginEx.dialect/.contentType/.modelare all asserted on the app-facing stream.openai.streaming(network + application):llm clientsame-dialect round-trip with a"stream":truerequest and a realistic multi-chunk OpenAI SSE response — role chunk, content chunk, tool-call start and argument-fragment chunks,finish_reasonchunk, usage chunk,[DONE]sentinel — each SSEdata:line forwarded as one app-facingDATA+ one rawLlmFlushEx.openai.nonstreaming(network + application):llm clientsame-dialect round-trip with a"stream":falserequest and a plain-JSON response carrying top-levelusage, forwarded as oneDATA+ one terminal rawLlmFlushEx(matchingLlmJsonContentDecoder's own framing).Each scenario is wired into
LlmServerIT/LlmClientIT(real engine, viaEngineRule) andApplicationIT/NetworkIT(paired-script protocol self-consistency, no engine), per the required dual-IT convention. A newclient.openai.yamlconfig pinsdialect: openaifor the client-kind scenarios.No production code changed here — this is test-fixture-only, per the issue's scope. (Production changes referenced above — the
LlmDialect/ModelEnvelope/ModelTransformSPI redesign,LlmServerFactory's detect+stamp+ModelPipeline-forward rework,LlmClientFactory's content-type-driven codec selection, and theopenaidialect'smodelextraction — landed in the sibling PRs this branch is stacked on, prompted by a design review that started in this same session.)Stacking
This branch is built on top of the full
binding-llmchain (module scaffold, content-codec SPI, dialect SPI redesign,dialect: openai,llm server/llm clientstream factories, config validation) — so this diff includes those commits until they merge todevelop, at which point this PR's diff will shrink to just the new k3po fixtures and IT wiring, the same pattern used by the PRs it stacks on.Test plan
./mvnw checkstyle:check -pl incubator/binding-llm.spec,incubator/binding-llm— 0 violations./mvnw clean package -pl incubator/binding-llm.spec,incubator/binding-llm -am— compiles cleanly, including the four edited/new*IT.javaclasses, against the redesigned SPI./mvnw verify(the actual k3po-driven ITs) could not be run in the sandboxed dev session used to author this change — the reactor'smaven-notice-pluginlicense-mapping check fails on unmapped artifacts regardless of network access (confirmed the proxy itself reaches the registry fine; the mapping data itself is what's missing). This is the same pre-existing, environment-specific limitation noted in priorbinding-llmPRs' test plans, not something introduced by this change. CI should confirm the fullverifylifecycle, including the newopenai.request/openai.streaming/openai.nonstreamingscenarios.Fixes #2490
🤖 Generated with Claude Code
https://claude.ai/code/session_01L5Pw5eDcghnK7LmZ11QA7u
Generated by Claude Code