Add TTS endpoint (Phase 1) - #21
Conversation
fd4c407 to
998f11d
Compare
Speech synthesis becomes a fourth routed model kind beside chat, embedding, and classifier, so a catalog entry can declare a text-to-speech model and clients can discover it before shaping requests. A model may now declare the voices it offers, and the declaration is validated at load: entries must be non-empty and unique, and the list is rejected outright on any non-speech kind, symmetric with the existing chat-only field discipline. An empty or absent voice list stays valid, so providers with no fixed voice catalog keep working. The configuration interface gains the new kind as a dropdown choice and a chips editor for the voices that appears only for speech models. - `ModelKind::Speech` joins the kind enum with the serde and Display spelling "speech", a fourth routed catalog kind whose spelling round-trips through TOML and JSON. - `voices` is a private, serde-defaulted string list on the capabilities record, documented as speech-only and surfaced verbatim on the models listing; empty means the model exposes no fixed voice list. - `validate_capabilities` rejects empty voice entries and duplicate voices at load, the error naming the voices field or the duplicated entry. - `validate_kind_scope` rejects a non-empty voices list on chat, embedding, and classifier models, the symmetric half of the chat-only field discipline, so a misplaced list fails loudly naming the field. - `settings-registry.ts` declares a chips editor for voices that renders only when the model kind is speech, and `models-view.ts` gains speech as a fourth kind dropdown option. - `model-detail.test.mjs` pins the four-kind dropdown and a pick-speech, add-chip, save round-trip into the PUT body, while the new config validation tests pin each acceptance and rejection. Design: new surface-growth @ crates/gateway-config/src/config.rs::ModelKind::Speech boundary: wire Design: new surface-growth @ crates/gateway-config/src/config.rs::Capabilities::voices boundary: wire Design: new encapsulated-invariant @ crates/gateway-config/src/config.rs::Capabilities Design: new surface-growth @ crates/gateway-config/src/config/accessors.rs::Capabilities::voices boundary: pub Deferred: launch_options becomes fallible with a refuse-unknown wildcard Plan: vibe/2026-09-07-2-gateway-tts-phase-1.md
A local model whose kind has no launch mode previously fell through a wildcard arm and started as a chat server. Building launch options is now fallible, and speech along with every kind the mapping does not name fails with a dedicated error instead. Because the kind enum is open-ended, a kind added later can never silently inherit the chat default.
- `LocalError::UnsupportedKind` carries the offending `ModelKind` on the public, non-exhaustive error enum and renders "local {kind} models are not yet supported", so the refusal names the kind it rejected.
- `launch_options` now returns `Result<LaunchOptions, LocalError>`; the serve-mode mapping lists chat, embedding, and classifier explicitly, and both the speech arm and the retained wildcard arm fail instead of defaulting to `ServeMode::Chat`. The wildcard stays because `ModelKind` is `#[non_exhaustive]`: a kind added after this mapping fails loudly instead of launching as a chat server.
- `launch_options_for` propagates the new failure with `?` ahead of chat-template resolution.
- `speech_kind_refuses_to_launch_as_chat` pins the refusal: the error is `LocalError::UnsupportedKind` with `ModelKind::Speech` and the exact message text, while the existing launch-options tests unwrap the result.
- No `ServeMode` arm for speech is added; no local speech runtime exists yet, so the error is the entire launch behavior for that kind.
Design: new surface-growth @ crates/gateway-local/src/error.rs::LocalError boundary: pub
Violates: A2 - not determinable from diff
Plan: vibe/2026-09-07-2-gateway-tts-phase-1.md
Add the wire contract for incoming speech synthesis requests to the shared protocol layer. The request carries the named fields an OpenAI-shaped speech call needs and preserves every unnamed field verbatim, so provider-specific dialect fields pass through untouched. Validation at the trust boundary rejects an empty model, empty or over-cap text, out-of-range speed, and reserved keys smuggled into the passthrough map. The audio format set is closed with its mp3 default pinned in the type, so an omitted field resolves to mp3 at deserialization and no route can forget the pin. - `SpeechRequest` names seven fields plus a flattened `rest` map that keeps every unnamed field verbatim; the RESERVED list pins the seven keys the passthrough map must never carry. - `SpeechVoice` accepts both the plain name and the object form; membership in a model's catalog is checked at the route, never in the type, because voice sets are per-checkpoint. - `SpeechResponseFormat` is a closed set whose serde default resolves an omitted field to mp3; provider-only spellings fail deserialization, so no route can forward one. - `validate` returns a static reason string for an empty model, an empty or over-cap input (4096 characters), a speed outside 0.25 to 4.0, or a reserved key in `rest`. - `speech_request_validation_table` and its companion tests pin the cap at 4096 versus 4097 characters, both speed bounds, both voice forms round-tripping, the mp3 default serializing back onto the wire, and verbatim passthrough of unnamed fields. - `SpeechRequest` is constructed only by its tests in this change; no route or upstream consumes it yet. Design: new surface-growth @ crates/shared-protocol/src/wire.rs::SpeechRequest boundary: wire Design: new value-object @ crates/shared-protocol/src/wire.rs::SpeechResponseFormat instead-of: stringly-typed: provider-only spellings stay unrepresentable until the enum is deliberately widened Design: new value-object @ crates/shared-protocol/src/wire.rs::SpeechStreamFormat Design: new pure-function @ crates/shared-protocol/src/wire.rs::SpeechRequest::validate Plan: vibe/2026-09-07-2-gateway-tts-phase-1.md
The upstream abstraction now forwards speech synthesis requests and returns provider audio as a stream of opaque bytes. A speech call substitutes the routed model name, posts to the provider's audio endpoint, forwards the response content type verbatim, and passes the audio through untransformed, because the gateway never inspects or rewrites audio frames. Upstreams with no speech implementation decline the workload with a model-unavailable error rather than fabricate a response. A dedicated client for long-lived audio drops the whole-request timeout that would kill any lengthy stream, bounds stall detection with a per-read idle timeout, and keeps middleboxes from dropping idle connections. - `StreamedAudio` pairs the upstream content type with the raw byte stream; an empty content type is the route's signal to apply its format-to-MIME fallback, and dropping the stream drops the upstream response and aborts the connection. - `Upstream::send_speech` defaults to `ProtocolError::ModelUnavailable` naming the caller's model, so an upstream without a speech implementation declines the workload; the signature stays object-safe through `Arc<dyn Upstream>`. - `audio_streaming_client` carries no whole-request timeout, a 30-second per-read idle timeout, and 60-second TCP keepalive, so a stalled or silently dead audio stream is detected without capping the stream's total length. - `http_audio` is a third client on `OpenAiUpstream` dedicated to the speech path; the chat SSE client is untouched. - `OpenAiUpstream::send_speech` rewrites the request model to the upstream alias, posts to the audio speech endpoint over the audio client, and maps mid-stream read failures to `ProtocolError::upstream_transport` as explicit stream errors rather than silent truncation. Non-success statuses arrive through the existing post helper as `ProtocolError::UpstreamStatus` with a capped body, so no error-path work exists here. - `OpenAiUpstream::with_client` now serves all three client roles from the one injected client, which is how the tests inject a short read deadline against a stalled server. - `bytes = "1"` is pinned once in the workspace manifest and taken up as `bytes.workspace = true`, the house convention for shared external dependencies. - `speech_rewrites_caller_model_and_posts_to_audio_speech` and its companions pin model substitution with no caller-name leak, bearer credential forwarding, the 429 and 503 status shape with capped body, untransformed bytes including invalid UTF-8, the empty content type, stall timeout as a transport error, and the object-safe default refusal. - `crates/shared-protocol/README.md` now names streaming speech among the passthrough workloads the crate describes. - `ProtocolError::classify` and its table test stay frozen: the protocol error type is untouched, the speech-only envelope seam belonging to the gateway crate's own error type. Within this change only the tests exercise `send_speech`; no route consumes it yet. Design: new surface-growth @ crates/shared-protocol/src/upstream.rs::StreamedAudio boundary: pub Design: new surface-growth @ crates/shared-protocol/src/upstream.rs::Upstream::send_speech boundary: pub Design: new surface-growth @ crates/shared-protocol/src/http_util.rs::audio_streaming_client boundary: pub Plan: vibe/2026-09-07-2-gateway-tts-phase-1.md
The gateway gains a speech synthesis route that authenticates the caller before parsing the request body, guards the routed model's kind, and validates the requested voice against the model's catalog before queue admission, so a client error never consumes a queue slot. Because audio frames are opaque bytes that cannot be revalidated chunk by chunk, the reply is a byte passthrough: the provider's stream is forwarded unread under its own content type, with the requested format's MIME type as the fallback and no content length, and the queue permit is held for the stream's entire life. Upstream rate limiting and unavailability map to speech-only retryable error envelopes while every other route keeps its existing mapping, and a mid-stream failure or cancellation surfaces as truncation rather than an error tail. - `audio_speech` runs `check_auth` on the ungated `Caller` parts extractor before extracting `Json<SpeechRequest>` by hand, so an unauthorized caller never makes the gateway parse a body, and it validates the requested voice against the model's catalog before queue admission, so a rejected voice never burns a queue slot. - `relay_audio` re-emits the upstream byte stream unread, holding the queue permit and the in-flight guard inside the stream state for the body's whole lifetime; a mid-stream upstream failure propagates as a body error and a profile switch ends the stream early, so no error envelope ever follows audio bytes. - `GatewayError::UpstreamRateLimited` and `GatewayError::UpstreamUnavailable` are speech-only variants that map an upstream 429 and 503 to retryable 429 and 503 envelopes; every other route keeps the shared `ProtocolError` mapping, so existing envelopes stay bit-identical. - `GatewayError::InvalidVoice` carries the requested voice and the model's catalog list, so the 400 `invalid_voice` envelope names the valid voices. - `build_router` registers `POST /v1/audio/speech` unconditionally, and `admin_status` gains a Speech synthesis row through a table-driven rewrite of the endpoint list. - `speech_mime` maps each response format to its OpenAI MIME spelling and is the content-type fallback when the upstream omits the header or sends an unparsable one; content length is never set, so the response streams chunked. - `crates/gateway/tests/it/speech.rs` adds sixteen integration tests over three fake backends: byte-identical passthrough with no caller-bearer leak, the per-format fallback table, kind and voice rejection, voice-before-admission under a full pool, queue-full 503, the permit held until stream end, disconnect releasing the permit, mid-stream failure with no spliced envelope, the 429 and 503 mappings, and provider internals never reaching the client. - `speech_auth_tests` pins the auth ordering through the real router: a 401 before a malformed body is parsed, and an authenticated malformed body in the OpenAI envelope as `malformed_request`. - `chat.rs`, `embeddings.rs`, and `rerank.rs` each add a configured speech model to the loop that asserts `kind_mismatch` on the wrong route. - `GET /v1/audio/voices` is named in the crate doc header but has no handler or registration in this change. Design: new surface-growth @ crates/gateway/src/lib.rs::audio_speech deps: Caller,Request,State<AppState> boundary: wire Design: new pure-function @ crates/gateway/src/lib.rs::speech_mime deps: SpeechResponseFormat Design: new surface-growth @ crates/gateway/src/error.rs::GatewayError::InvalidVoice boundary: wire Design: new surface-growth @ crates/gateway/src/error.rs::GatewayError::UpstreamRateLimited boundary: wire Design: new surface-growth @ crates/gateway/src/error.rs::GatewayError::UpstreamUnavailable boundary: wire Deferred: the GET /v1/audio/voices handler and its registration Plan: vibe/2026-09-07-2-gateway-tts-phase-1.md
The gateway gains a voice-listing route that answers the union of every speech model's configured voices in the active profile, deduplicated and sorted, so a client can discover which voices it may request before synthesizing. OpenAI has no voice-list route, but the OpenAI-compatible ecosystem converged on one whose entries lead with the voice identifier, so each entry pairs that identifier with a display name that mirrors it. The route is bearer-authenticated and reads only live catalog state; it never calls an upstream.
- `audio_voices` returns id-first `{"id", "name"}` entries under `{"voices": [...]}` with `name` mirroring `id`, because ecosystem clients read the `id` key and the catalog configures voices as bare strings with no separate display name; the entry shape is a compatibility surface pinned on the raw response body.
- `build_router` registers `GET /v1/audio/voices` unconditionally beside `/v1/audio/speech`.
- `audio_voices` authenticates through `check_auth`, holds the publication lock while reading live routing, and collects the speech models' voices into a `BTreeSet`, so the union is deduplicated and sorted.
- `crates/gateway/tests/it/speech.rs` gains the `catalog_gateway` and `voices_body` helpers and four integration tests pinning the id-first union shape, deduplication and ordering across two speech models, the empty union when no speech model is configured, and non-speech models contributing nothing.
Design: new surface-growth @ crates/gateway/src/lib.rs::audio_voices deps: Caller,State<AppState> boundary: wire
Pending: N70 - compounds
Plan: vibe/2026-09-07-2-gateway-tts-phase-1.md
A dev-only parity probe now drives the official OpenAI Python SDK against a real gateway instance and against the live Together AI provider with the same call set, so provider dialect drift surfaces as a failing assertion rather than a stale document. The probe is gated on the vendor key: when the key is absent the script prints a skip message and exits successfully without touching the network, and when present the key travels only through the process environment while the throwaway gateway config carries an interpolation reference. The accompanying verification note records a green run against the live provider, the observed provider dialect, and the behaviors deliberately left to the integration suite. - `tools/gateway-tts-parity.py` is dev-only and gated: with no TOGETHER_API_KEY in the environment or the repo dotenv it prints a skip and exits zero without touching the network, and the key is never printed or written, since the throwaway config carries only the interpolation reference and the gateway subprocess receives the value through its environment. - `run_speech_surface` drives the identical call set against the gateway and the live provider from one body, a boolean selecting the extra gateway-only contract checks; both sides assert the provider-stable subset the first live run observed, while genuinely volatile provider behavior stays on the observe path. - `render_config` emits a throwaway profile on an ephemeral loopback port: one Together endpoint, one speech model upstreaming to Orpheus with the eight voices, and a local-only bearer key that never leaves the loopback listener. - `speech_create` and `voices_call` return observation dicts with HTTP errors as data, so a provider error shape fails or informs an assertion instead of raising. - `main` builds the debug gateway when no binary exists, polls readiness on a 90-second deadline, terminates the subprocess in a finally block, and exits nonzero only when a hard assertion failed. - `design/note-gateway-tts-phase-1-verification.md` records the green live run: the gateway default answers mp3 against a provider whose own default is wav, both sides stream chunked with no content length, emotion tags and unknown fields pass with 200, and the provider has no voices route at all. - `tools/gateway-tts-parity.py` provokes no 429 or 503 envelope on the paid provider and never exercises voice rejection, kind mismatch, or mid-stream disconnect, all of which stay with the Rust integration suite. The script carries no tests of its own; the live run is its verification. Design: new flag-parameter @ tools/gateway-tts-parity.py::run_speech_surface deps: api_key,base_url,gateway,label,model Design: new global-state @ tools/gateway-tts-parity.py::FAILURES Design: new swallowed-exception @ tools/gateway-tts-parity.py::read_dotenv_key Violates: A2 - tools/gateway-tts-parity.py holds TOGETHER_API_KEY outside the gateway and calls the provider directly Deferred: live-provider observation of the 429 and 503 envelopes, which the note leaves to the Rust integration suite Plan: vibe/2026-09-07-2-gateway-tts-phase-1.md
Document the speech synthesis surface: how a speech model is declared in the catalog, how the synthesis route is called, and how voices are enumerated. A new guide chapter carries the material ahead of the profiles chapter, the following chapters shift one number up, and the table of contents, the chapter index, and the compiled guide are regenerated to agree. The gateway crate documentation gains a speech section beside the transcription section, the example configuration gains a commented speech model block, and the kind lists in the remote and local model chapters now include speech. The design report's citation of the serving chapter follows the renumber. - `guide/src/gateway/06-speech-synthesis.md` - New chapter inserted at number 06; the five chapters after it move one number up as pure renames, the table of contents and chapter index are updated to match, and the design report's citation of the serving chapter follows the move. - `guide/promptforge-gateway-guide.md` - Regenerated compiled output committed in the same change, so the single-file guide mirrors the source chapter edits verbatim. - `kind = "speech"` - One canonical catalog example, an endpoint plus a speech model with its voice list, now lives in three places: the crate documentation, the new chapter, and the commented block in the example configuration. - `crates/gateway/README.md` - Documents the route dialect: input capped at 4096 characters, an omitted response format pinned to mp3 in the wire type, an unknown voice rejected as a 400 before queue admission, upstream 429 and 503 answers mapped to distinct client-facing envelopes, and provider audio streamed through with no content length set. - `guide/src/gateway/04-local-models.md` - Documents that a local model with speech kind is refused at launch because no local speech runtime exists yet. Design: new shotgun-surgery @ guide/src/gateway Design: new clone-block @ guide/src/gateway/06-speech-synthesis.md Plan: vibe/2026-09-07-2-gateway-tts-phase-1.md
The speech synthesis feature now has its as-built design record. The new document states what the gateway's first speech phase became as built: a standalone executive summary, sixteen numbered design choices reconciled against the finished work, and the list of items deferred to the next phase. It fixes the design facts and their rationale at the moment of completion, so a later pass over the log does not have to reconstruct them from the feature's eight commit messages. - `design/design-gateway-tts-phase-1.md`: new as-built design document, a title naming the routed speech kind with remote passthrough, a standalone executive summary, sixteen numbered design choices reconciled against the finished work and its decision record, and a deferred-to-phase-2 section. - `design/design-gateway-tts-phase-1.md`: no code, test, or configuration change rides with the document; the commit is prose-only. Plan: vibe/2026-09-07-2-gateway-tts-phase-1.md
Plan: vibe/2026-09-07-2-gateway-tts-phase-1.md
998f11d to
dd2c171
Compare
Gateway TTS Phase 1 Branch Review
Executive summaryDo not merge the current branch. The core routed-speech implementation is substantial and its covered paths are healthy, but the branch has four production defects, six material documentation, verification, governance, or tooling defects, and five smaller record or history defects. The most consequential problems are unsupported local speech models downloading artifacts before refusal, unbounded permit ownership under slow streaming, profile cancellation ending as clean success, and a 30-second read timeout that also limits time to first response headers. The branch adds 3,283 lines and removes 81 across 43 files in ten commits over current master. Forty-one focused TTS tests pass. Formatting, the featureless gateway build, and warnings-denied Clippy for the four owning crates also pass. These results establish that the implemented batch speech path works under its covered conditions. They do not exercise the failure modes identified in this review. The original gripe list was directionally useful but not uniformly correct. Together batch synthesis works, and phase 1 originally excluded Together SSE. The defect is that later documentation advertises an impossible SSE path. The current Vibe Coder schema and pending YAML todo statuses are not valid merge objections because the run closed under an older contract and completion is tracked through step headings. By contrast, stale survey claims became actionable when the branch was rebased onto a base that had removed the named tools and renumbered the architecture. The recommended repair is a history rewrite that amends the owning commits rather than stacking cleanup at the tip. Preserve the phase 1 decision not to implement Together SSE, bound the audio relay, move unsupported-kind checks before provisioning, replace the unmanaged Python probe with a dependency-free Node live-gateway probe, remove direct provider calls, add the missing behavior tests, and reconcile every plan and design claim. Estimated engineering work is 24 to 32 hours, plus live-provider access and complete CI. Confidence is high because the main findings follow directly from control flow, commit history, current provider documentation, and reproducible repository checks. Contents
Decision and required conditionsMerge only after the author completes the following conditions:
This is a conditional no-go rather than a rejection of the routed-speech design. The feature can become merge-ready without changing its core architecture. Scope, method, and limitationsThe review compares local branch The review applied the root The review judged the branch against six criteria: user-visible protocol correctness, architecture and ownership conformance, bounded resource and cancellation behavior, tests that would detect each claimed failure, truthful documentation and provenance, and repairability within the existing commit structure. Local verification ran 14 shared-protocol speech tests, four gateway-config voice tests, one gateway-local refusal test, and 22 gateway speech integration tests. All 41 passed. Formatting, the featureless gateway check, and warnings-denied Clippy for The review did not rerun the credentialed live-provider probe, provoke paid-provider rate limits, or execute the complete cross-platform CI matrix. Repair estimates include implementation, focused tests, and documentation, but exclude queue time on external runners. Provider behavior can drift after the 2026-09-09 documentation snapshot. What the branch gets rightThe routed model-kind decision fits the repository. Speech models use the existing catalog, routing, dominion queue, upstream abstraction, and model discovery instead of adding a parallel service lifecycle. This preserves the gateway as the runtime credential owner and follows the established chat, embedding, and classifier shape. The batch speech path is well covered. The tests prove model rewriting, bearer separation, structural MP3 defaulting, byte-preserving response relay, MIME fallback for six audio formats, voice validation before queue admission, queue-full behavior, permit retention, client-disconnect cleanup, error-body secrecy, 429 and 503 mappings, and voice-union ordering. The branch also handles the new model kind safely in the launch mapping. These strengths narrow the repair. The recommendation does not replace the route, model kind, upstream method, or voices union. Severity-ranked findingsThe findings rank as follows. Estimates overlap where one relay change closes several defects.
F1. Reject local speech before provisioning
The branch promises that a local speech model fails because no local speech runtime exists. The failure happens too late. An operator who accidentally declares a local speech model can download a multi-gigabyte model, provision The strongest counterargument is that prefetching may help phase 2. That behavior is speculative and conflicts with the phase 1 statement that local speech is unsupported. A preparation command must not report success for a model the runtime cannot start. Fix the defect by extracting a side-effect-free supported-kind preflight. Run it before provisioning the shared server and before each model or companion download. Mixed profiles should preserve supported-model progress while returning a per-model failure for unsupported kinds. Add tests proving that an all-speech profile touches neither the server provisioner nor the model store, and that a mixed profile provisions only supported models. Estimated work is 2 to 4 hours across F2. Bound audio relay ownership independently of polling
Architecture invariant A5 requires every stream and wait to be bounded. Invariant A27 separately requires stream permits to survive until body termination and stalled reads to expire. The implementation satisfies A27 under ordinary polling but does not fully satisfy A5.
The request-side 4,096-character cap, queue-depth cap, error-body cap, client-disconnect drop chain, and typical provider behavior all reduce the likelihood of accidental occurrence. They do not create a finite successful-response lifetime. An accepted caller or faulty upstream can occupy every speech dominion slot indefinitely. An endpoint with no dominion avoids permit starvation but permits unbounded concurrent upstream tasks and sockets instead. A short whole-request timeout is not an acceptable fix. A 4,096-character request at slow playback can legitimately produce many minutes of audio, and raw PCM is large. The safer policy is a bounded background relay that owns the upstream stream, permit, and cancellation guard while feeding a small bounded channel to the HTTP body. The task should enforce a generous total lifetime, a byte ceiling, upstream idle, blocked downstream delivery, cancellation, and receiver drop independently. A reasonable static starting envelope is 60 minutes, 1 GiB, 30 seconds of upstream read idle after headers, and 60 seconds blocked on downstream delivery. Together documents Orpheus output as 24 kHz signed 16-bit PCM. One hour of mono audio at that rate is 172,800,000 bytes, about 165 MiB, before SSE framing. The proposed byte limit leaves more than six times that payload. These values still need production validation, named constants, and boundary tests. Configurability should wait until operators demonstrate a need because it would add schema, validation, UI, and documentation surface. Estimated work is 8 to 12 hours for a static bounded pump and deterministic tests. Amend the owning protocol and route commits, then update the plan, guide, and as-built design. Confidence in the need for a finite policy is high. Confidence in the suggested numeric values is medium because production duration distributions are not in the repository. F3. Make profile cancellation fail the body read
The branch correctly notes that no JSON envelope can follow audio bytes. That does not require clean EOF. Emit one body error when profile cancellation wins, then terminate. The bounded relay proposed in F2 should own this behavior so cancellation remains observable even when the downstream has stopped polling. Add a profile-switch integration test that reads one audio chunk, switches profile, and proves the next body read fails, the upstream drops, and the permit becomes available. This requires 1 to 2 hours if implemented alone and less when folded into F2. F4. Separate response-header and body-idle deadlines
The dedicated client applies This matters because Together batch synthesis is not the low-latency SSE mode. The accepted request can contain 4,096 characters, while live verification used about 90. A long request or cold provider can accept and potentially bill work before the gateway reports The strongest counterargument is that production providers usually return headers promptly or emit chunks during generation. No branch evidence establishes the latency distribution, so actual incidence remains uncertain. The code mechanism and resulting error classification are certain. Use a longer explicit first-response deadline, then enforce the shorter body-idle deadline in the relay itself. This design overlaps the bounded pump in F2 and avoids forcing one timeout value to serve two different phases. Add one test where headers arrive after the body-idle threshold but before the first-response threshold, and another where an opened body stalls. Estimated incremental work is 2 to 4 hours, mostly overlapping F2. Amend F5. Stop advertising Together SSE as a phase 1 capability
Together batch speech works. The live note proves MP3 and WAV output through the gateway. Together SSE does not work through this branch. Together's current documentation requires The source report states the correct scope at The practical consequence is false operator guidance. Preserve phase 1 scope and correct the plan, README, guide, parity terminology, verification note, and as-built design. State that OpenAI This correction requires 2 to 4 hours across six or seven documentation and verification files. Actual Together opaque SSE passthrough would require adding One small production defect remains even for an OpenAI SSE-capable upstream. F6. Remove the direct-provider credential path
Architecture invariant A19 says vendor credentials remain inside the gateway. The plan repeats that nothing above the gateway holds a vendor key, then requires a direct provider comparator. This is not an inferred objection. The introducing commit message states: “Violates: A2 - tools/gateway-tts-parity.py holds TOGETHER_API_KEY outside the gateway and calls the provider directly.” The live verification note confirms that the direct calls actually ran. The close commit leaves the admitted violation unresolved. Incremental secret-exposure risk is low because the script is opt-in, does not run in CI, and does not print or newly persist the key. The governance consequence is larger. The script bypasses gateway-owned redaction, routing, accounting, and error handling while the plan simultaneously treats credential isolation as a founding invariant. The strongest counterargument is that architecture invariants govern deployed components, not an operator's local oracle. The written rule contains no such exception, and the branch explicitly labels the implementation a violation. Review cannot silently invent an exemption. Preserve the invariant. Remove the direct Together half and dotenv parsing. The live tool should call only a separately configured gateway using its local bearer. Provider defaults can remain sourced to current documentation, while mocked tests prove exact outbound bodies. If maintainers want direct-provider probes, they should approve a narrow developer-tool exception with host pinning, no persistence, bounded calls, and no CI. Removing the direct half alone takes 1 to 2 hours across the script, plan, note, ledger, commit message, and as-built design. This work should be combined with F7. F7. Replace the unmanaged standalone Python tool
The repository previously used short standard-library Python fragments inside workflows. This branch adds its first tracked standalone The documented no-key skip path is already false on a clean machine. The binary-selection logic creates a second reproducibility defect. Python is not technically required. The repository already requires Node 22 and uses Replace the Python probe with a Node script that contacts only the gateway. Keep exact protocol behavior in Rust integration tests. Record a separate manual official-SDK smoke run against the gateway if compatibility with one client library is an acceptance concern. The replacement is estimated at 4 to 8 hours, including deterministic helper tests, a credentialed live rerun, and updates to the plan, note, ledger, guide references, and as-built design. Retaining Python is the weaker alternative: add pinned dependencies, an installation workflow, offline unit tests, and an explicit tooling exception. That option takes roughly 2 to 4 hours but leaves an additional language environment to maintain. F8. Add route-level tests for claimed behavior
The existing suite is broad, but five claimed boundaries lack direct coverage. First, the product requirements promise a specified unknown-model envelope on the speech route. Only the chat integration suite directly tests Second, every voices integration helper sends a bearer at Third, the plan and as-built design claim profile-switch cancellation truncates speech, but no speech integration test switches profile during an open body. This omission concealed F3 and the unpolled-cancellation portion of F2. Fourth, angle-bracket preservation is tested only through the optional live script. The generic passthrough test uses ordinary text. Offline CI could regress the specific hard requirement without failing. Fifth, a wire test accepts and round-trips The unknown-model, voices-authentication, emotion-tag, and SSE-composition gaps are individually low severity because shared mechanisms already have coverage. Profile-switch cancellation is medium severity because it is bespoke composition and concealed F3. Add all five direct pins in 1.5 to 2 hours. The no-provision and bounded-relay tests belong to F1 and F2 estimates. Tests should amend the commits that introduce each behavior rather than land in a catch-all tip commit. F9. Restore verifiable provenance after the rebase
The branch was rebased after live verification and documentation. Maintainers cannot inspect the exact trees that supposedly passed live verification. The stale-binary behavior in F7 also means the recorded source hash would not prove which executable was exercised even if the commit existed. The rebased content may be equivalent, but the report rulebook and the repository's own evidence discipline require verifiable names and numbers. Rerun the live check against a freshly built executable from a retained rewritten commit. Record the executable hash, commit, and tree hash, because subsequent message-only amendments can change the commit while preserving the tree. Update the note, as-built design, ledger, and commit messages. Estimated repository work is 1 to 2 hours, excluding provider and CI queue time. F14. Dispose architecture records before closure
Immediately before closure, The Vibe Coder contract in force when the TTS run closed required the operator alone to promote, reject, or leave each queue record open. It also required queue dispositions to live in an operator-authored drain commit, never a step or Close commit. The closure carries no disposition rationale and the accepted architecture contains neither record. This has no runtime consequence, but it removes the audit trail for two findings while claiming the plan is closed. Restore the records at the end of the amended implementation series, have the operator dispose each one explicitly in a separate drain commit, and only then replay closure. N69 appears malformed because it calls an undetermined fact a violation; that is a reason to reject it explicitly, not silently delete it. Repository work is under 1 hour. Operator review time is separate. Do not fold the disposition into F10. Make parity claims match actual assertions
The plan says the gateway and provider run the same assertions. The script runs the same call set but not the same checks. WAV, emotion tags, and three passthrough fields are asserted for both targets at Literal symmetry would be wrong because the gateway deliberately changes default format and adds a voices route. The defect is the promise, not the asymmetry. The script still provides useful live evidence for the shared subset. If F6 removes direct provider calls, rename the tool as a live gateway smoke probe and remove parity claims. If maintainers approve direct comparisons instead, represent target expectations explicitly and make known provider baselines fail on drift. Avoid a boolean mode flag whose meaning is spread through the function. The script and document correction is 1 to 2 hours, largely overlapping F6 and F7. F11. Correct the post-rebase survey and CI account
Most stale survey claims were true at the original TTS base The current plan names removed Node tools, claims a removed JSON ceiling file exists, says a removed UI pretest still runs, and cites A102 and A112 through A117. It also says only Do not restore any removed parser, walker, count, or ceiling. Amend the survey to record its original base and a post-rebase validation section against The CI wording needs a separate narrow correction. Plan line 80 calls a feature-focused subset “the exact CI gates,” while line 143 says every step ended with the full suite. Describe focused, component, and final verification accurately. Then obtain one exact-commit integrated CI result after the rewrite. Updating the plan and ledger takes 1 to 2 hours. F12. Complete public model-kind documentation
Five live documentation locations still enumerate the old three-kind set:
The code works because the type already carries F13. Synchronize terminal frontmatter metadata
Six frontmatter todos remain pending at The mismatch cannot reopen work, break closure, or change generated commit messages. It can still mislead a frontmatter-aware viewer or a maintainer reading the plan without knowing which state is authoritative. Repository precedent also exists for synchronizing every todo in the final execution commit. Amend F15. Keep the first retained speech commit safe
Combine Original gripes that do not standCurrent Vibe Coder tags are not retroactiveThe plan does not satisfy the current strict tag schema. That schema was introduced after the original TTS run closed. The run had no contract tags, step tags, exact capitalized Project Survey heading, survey status line, or component-test field because the governing tool did not yet require them. The local rebase happened later, but it replayed a closed run and did not reactivate Do not make current Vibe Coder syntax a merge condition. If maintainers want every newly merged plan file to parse under the current tool regardless of execution date, add a separate post-close migration commit and ledger note. Estimated optional work is 20 to 30 minutes. Confidence is high. Together SSE absence is not a batch-route failureThe source report deliberately scoped Together phase 1 to batch passthrough. MP3 and WAV batch calls succeeded live, and focused tests cover their gateway behavior. The inability to request Together's SSE mode is therefore not evidence that the delivered batch feature fails. F5 remains because user-facing documentation advertises more than the branch implements. Remote PR failures do not describe the rebased TTS codeThe remote pull request currently reports merge conflicts and two failed checks on its pre-rewrite lineage. The core check stopped on an unrelated STT Clippy diagnostic that current local master already fixes. The native lane stopped because the self-hosted runner lacked the required Rust 1.89 directory. The local branch passes the reviewed TTS tests, formatting, featureless check, and owning-crate Clippy. Nevertheless, the rewritten combined tree has no complete exact-commit CI result. The remote failures should be replaced by fresh results after push, not counted as TTS defects. Amendment plan by commitThe branch is already organized into behavior-sized commits. Preserve that structure and amend defects into the commits that introduced them. The recommended chronological rewrite is:
The estimates overlap. The bounded relay, timeout separation, cancellation test, and several missing tests should be implemented as one coherent stream-lifecycle change. The Node rewrite, credential correction, parity correction, and live rerun should likewise be one verification-tool change. Expected total engineering effort is 24 to 32 hours for the recommended scope. A developer already familiar with the gateway test harness is likely to land near the lower bound. External live-provider access and CI queue time are additional. Confidence is medium because deterministic backpressure tests are the largest uncertainty. The rewrite should begin at Verification required before mergeFocused behavior verificationThe amended local-provisioning commit must prove that unsupported kinds perform no artifact or metadata side effects. Test all-speech and mixed supported and unsupported profiles with injected provisioners and stores. The amended stream commit must prove all terminal paths. Required cases are exact byte limit, one byte over limit, total deadline, sub-idle upstream drip, slow downstream channel saturation, upstream idle after headers, delayed but accepted headers, client disconnect, profile cancellation, and upstream body error. Every case must prove permit release and admission of a subsequent request. The route suite must add unknown-model, unauthenticated voices, angle-bracket input, SSE content-type fallback, and the intentionally unsupported Together-native streaming shape. Existing batch MP3, WAV, queue, error, voice, and disconnect tests must remain green. The replacement live tool needs offline tests for gateway startup failure, readiness timeout, request failure, assertion failure, process cleanup, skip behavior, and secret-free output. The credentialed run must contact Together only through the gateway and record the retained commit and tree. Repository gatesRun formatting, warnings-denied Clippy, non-Workshop workspace tests, doctests, warnings-denied documentation, the featureless gateway build, dependency policy, supply-chain checks, both UI packages, and clean-tree verification. Rebuild the generated guide and prove that regeneration leaves no unexplained diff. Run the Workshop Windows and Linux lanes because the rebased base overlaps gateway configuration, routing, and integration harness files. Run MSRV because the branch adds public Rust types and dependency usage. The native Whisper lane remains externally blocked until the self-hosted runner supplies the configured Rust 1.89 binary directory. Do not infer that lane passing from local TTS results. Evidence closureRecord one exact rewritten commit for every local and remote result. Use tree hashes for live observations that survive message-only amendments. Remove or correct every invalid historical object reference. The final pull request must be conflict-free and every required check must report against the pushed head. ConclusionThe routed-speech architecture should proceed, but the current branch is not merge-ready. Its ordinary remote batch path is functional and well tested. The reasons to stop are narrower and concrete: unsupported local models perform side effects, stream ownership can outlive every useful bound, cancellation can look successful, the first-response timeout is too tightly coupled to body idle, documentation advertises an unavailable Together mode, and the verification tool knowingly violates credential ownership while introducing an unmanaged runtime. Amending the existing commit series is preferable to adding a repair stack. The defects align cleanly with the commits that introduced config, local launch behavior, protocol streaming, routing, verification, and documentation. A 24 to 32 hour repair budget is proportionate to the branch's 3,283 added lines and the need for deterministic backpressure tests. Final recommendation: request changes, complete the chronological amendment plan, rerun live verification through the gateway, and require a complete exact-commit CI matrix before merge. Confidence is high in the merge call and medium in the repair duration. ReferencesRepository evidence
External provider evidence
2026-09-09 04:02 - gpt-5.6-sol |
|
I rebased the branch and fixed merge conflicts. Let's go over this report and try to figure out what it means. |
The live speech parity probe ran every call twice, once through the gateway and once directly against the vendor, which placed the vendor credential outside the gateway and contradicted the invariant that the gateway alone holds it. The probe now drives only the gateway, and the vendor key reaches the provider exclusively through the gateway subprocess environment, so provider drift still surfaces through the same assertions. A credential file that exists but cannot be read now fails the run loudly instead of passing silently as a skip. The boolean that selected the gateway-only checks went away with the second call set, and those checks now run unconditionally. The design report and the verification note record the removal and keep the observed provider dialect as a one-run historical record. - `tools/gateway-tts-parity.py` now drives only the gateway: the provider is reached exclusively by the gateway subprocess, and the module docstring states the credential invariant outright. Dev tooling admits no exception to gateway-held vendor credentials. - `run_speech_surface` loses its `gateway` flag parameter with the provider-direct call set gone. The mp3-default, streamed-response, and voices-union checks it selected now run unconditionally against the gateway. - `read_dotenv_key` returns None only for a missing file; any other OSError propagates, and `main` answers with a FAIL line and exit 1. An unreadable credential file no longer reads as an absent key. - `TOGETHER_BASE_URL` is gone as a module constant; the vendor URL survives only as text inside the throwaway gateway config. The direct provider call in `main` that consumed it is removed with its `gateway=False` call site. - `voices_call` drops the `snippet` field from both return shapes; the provider-side observe branch was its only consumer. - `design/note-gateway-tts-phase-1-verification.md` gains an addendum recording the removal: the dialect it preserves stands as a one-run historical record, and re-verification runs through the gateway alone. Design: removes hidden-dependency @ tools/gateway-tts-parity.py::TOGETHER_BASE_URL Design: removes swallowed-exception @ tools/gateway-tts-parity.py::read_dotenv_key deps: path Design: removes flag-parameter @ tools/gateway-tts-parity.py::run_speech_surface deps: api_key,base_url,label,model Plan: none
|
This introduces the first Python source file in the project, are you sure we can't just do this in JS? |
Closes #20