Delete the python gateway data plane - #629
Conversation
The Rust encoder parity tests previously asserted against the python data-plane encoders at run time. Generate the exact expected bytes from the current python implementations once, commit them as exp/runtime/gateway/testdata/parity_goldens.json, and make every parity test assert Rust output against those committed bytes first. Python secondary assertions stay in place while the referenced modules still exist; the goldens are the durable contract that outlives them. The Anthropic error-translation test now drives committed OpenAI-shaped inputs (pinning the wall-clock-dependent quota rendering at freeze time) against committed envelopes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The native bridge's readiness callback previously consulted the python executor's accounting-health latch, coupling native readiness to a data plane object it never executes through. Give NativeGatewayComponents a first-class accounting_healthy surface instead: the local composition reports its group-commit writer's liveness (a closed or crashed writer can never land another durable terminal), hosted compositions report their own store health, and the bridge readiness ANDs that with its own settlement-loss latch and the optional hosted lifecycle probe. The group-commit writer exposes its latched closed state for exactly this surface. Also completes the anthropic error golden fixture with the committed param-carrying input. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Breaking crate release. The deprecated fallback_app/fallback_port seam (one-release deprecation window) is gone: ServeConfig loses fallback_port, the proxy relay module is deleted, unknown routes answer the native 404 directly, and an escalation disposition fails closed as the shared internal error on every surface. The proxied-request and fallback-unavailable metrics disappear with the relay; escalation classification stays. serve() gains an optional embedder-owned ShutdownHandle (exp_gateway_native.shutdown_handle()) so a host serving on a background thread can stop the plane gracefully; process signals keep working unchanged. The type stub also gains the previously missing Responses fixture declarations. Flagship 0.6.0 with the exp-gateway-native floor at >=0.2.0,<0.3; the lockstep test now derives the ceiling from the crate version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
exp.load_router previously wrapped the python gateway ASGI app in an in-process TestClient. It now owns a real native data plane on a private loopback port: the same load_gateway_components composition the CLI uses, native-servability validation, guardrails, and learned selection in process (so an injected decision sink still records), served on a background thread and stopped through the crate's new shutdown handle. Closing the returned OpenAI client stops the plane, drains the shared ledger writer, and revokes the ephemeral virtual key, as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The --engine flag is gone from the root callback and exp run; passing it is now a usage error. The native composition is the only launch path and gains the two capabilities that previously required the python engine: interactive first-run setup (with the same credential delivery and recovery guidance) and the --project single-alias compatibility launch (same receipt fields and key-file exports). A missing compiled extension fails the launch with the exact build command. Startup keeps the same proof the python path's service preflight gave: loading the granted aliases builds each alias's credential-free route proof and fails with actionable per-alias reasons, and native servability of every pool deployment is validated before binding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-provider async stream() methods, the shared SSE event mappers (streaming.py, gemini_streaming.py, bedrock_streaming.py), the stream-attempt and streaming-usage helpers, and the AsyncGatewayProvider / ProviderStream protocols existed only for the python data plane; the native engine dispatches through gateway_wire_profile() and normalizes provider streams in Rust against the committed golden fixtures. Bedrock loses its blocking open_stream and stream-permit helpers with the seam. Dialect parity asserts only against the committed goldens now, and the provider certification matrix names the native golden fixtures as its deterministic evidence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The FastAPI request-serving stack is gone: GatewayService, the ASGI composition (create_gateway_app / create_gateway_runtime and the public embeddable seam), GatewayExecutor and its execution machinery, event aggregation, the messages route module, the guardrail completion and delivery wrappers, the python Anthropic Messages encoders and error envelope, and every test that existed solely for that engine. What remains is the engine-neutral composition the native plane consumes: lifecycle.py now owns component loading, hot-reloadable alias authority, and the process lock only; the reloader and LocalGatewayComponents drop their executor coupling. boundary.py keeps the single exception-to-public-error authority minus the branches only the executor could raise. The pre-dispatch deployment-identity invariant moves into native_execution.py, whose route resolution no longer requires a python stream capability. openai_protocol keeps its decoders and the completed-body / SSE builders that pi.py and the native Responses callbacks consume, dropping the surface-dispatching stream_encoder, capture_frame, and the Messages branches the native plane renders itself. An escalation disposition now names why the plane cannot serve and fails closed; nothing hands a request to another engine. lifecycle, launch, messages, metrics, wizard, and release tests exercise the same protected behavior through load_gateway_components, the native control plane, and the served native engine on real sockets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gateway-architecture.md describes the single native data plane and loses the engine-selection and embeddable-worker-composition sections; gateway-guardrails.md drops the escalation re-inspection paragraph; usage.md drops the --engine flag from both launch rows; the README's Using-the-API section shows the exp launch and the exp.load_router client instead of the removed programmatic ASGI composition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The retired-flag usage error renders with rich style escapes inside the echoed option name on CI terminals, so the test asserts over the escape-stripped output. The wheel smoke script imports completed_body in place of the removed stream_encoder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@greptile-apps review |
Greptile SummaryThe PR removes the Python gateway data plane and makes the compiled native engine the sole serving path.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the native server now acquires the advertised listener before emitting readiness, and an occupied port exits without a ready receipt.
|
| Filename | Overview |
|---|---|
| exp/cli/gateway/serve.py | Consolidates gateway startup onto the native engine and fixes the previously reported false-readiness race by emitting readiness from the post-bind callback. |
| exp/runtime/gateway/native/src/server.rs | Adds the listener callback after successful TCP binding and before request acceptance while preserving graceful shutdown and settlement draining. |
| exp/runtime/gateway/native_server.py | Exposes the native listener callback through the Python wrapper and maps native startup failures to the public server error. |
| exp/runtime/gateway/native_bridge.py | Retains the Python control-plane bridge for native admission, routing, accounting, guardrails, and provider callbacks. |
| exp/runtime/gateway/execution_resolution.py | Reduces execution resolution to the surviving native path while preserving capability-aware wire-profile selection. |
| pyproject.toml | Removes Python server dependencies and updates the flagship and native-extension package contract. |
Reviews (6): Last reviewed commit: "Preserve client-visible usage on zero-ou..." | Re-trigger Greptile
The release evidence caught a native-admission regression: a continued Responses request derived a fresh episode from its own request identity, so learned project selection re-ran request-time embedding instead of replaying the journaled decision the first turn recorded. Route resolution now takes the continuation context and uses its retained episode key, the same episode derivation the retired service applied, so a continuation never embeds again. The regression test drives a project alias over a counting loopback embeddings upstream and fails without the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The release evidence caught the ordering bug: a restarted keyed replay answered 409 correctly but only after learned selection had already run request-time embedding against the provider. Admission now writes the ledger acceptance (where keyed idempotency conflicts and unavailable replays fail closed) before the routing probe, so a doomed keyed operation never touches a provider. An escalated admission finalizes its accepted request content-free (no attempt row) before the disposition returns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The launch previously printed its ready receipt (and the interactive banner) before probing the loopback bind, so a launch that could not own its port announced a usable gateway and then failed. The probe now runs before any readiness output on the serving path; --check keeps performing no bind. The regression test occupies a port and asserts the launch is a usage error with no ready receipt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@greptile-apps review |
The launch previously proved the bind with a temporary probe, closed it, printed the ready receipt, and only then let the native server perform its own fallible bind, leaving a gap where the port could be claimed after readiness was already announced. serve() now takes an optional on_listening callback the server invokes exactly once, after its listener is bound and queuing connections and before any request is accepted; the CLI emits its ready receipt or banner from that callback, so a launch that cannot own its port fails without ever announcing a usable gateway. --check keeps emitting synchronously and performs no bind. The occupied-port regression test drives the real native bind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@greptile-apps review |
|
@devin can you do in-depth integration testing with high volume traffic to ensure no regressions? spend up to $1k |
Main's Vertex provider streamed through the deleted python event mappers. Vertex now exposes gateway_wire_profile over the shared gemini_generate_content dialect: the publisher-scoped SSE endpoint on the project-and-location root with a currently valid OAuth bearer token. The executor-only stream() seam is gone with its engine; completion-path token warming is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merges codex/reasoning-compat-experiential-20260824. The deletion wins for every python-engine mirror; the gating itself lives on the surviving native path: route_generation_parameter_requests in admission (native_bridge), capability-aware preflight, and the catalog-intersected wire profiles now homed in execution_resolution (_resolved_wire_profile keeps only the native branch; the python_stream compatibility dialect dies with the engine). Provider clients keep #628's capability-gate constructors, reasoning-effort translation, and expanded wire profiles; their executor-only stream() seams stay deleted, as do execution.py, service.py, the python event mappers, the Anthropic Messages encoder, and the orphaned streaming_events module. Parity goldens are regenerated from the post-#628 python encoders (reasoning summary lifecycle, reasoning configuration in Responses bodies) with Rust asserted equal byte for byte, and two contracts are newly frozen: the chat x-experiential-ignored-parameters disclosure (new golden plus fixture arg) and Messages dropping reasoning-summary deltas without changing its bytes. The engine-parity served tests are ported to native-only assertions. Versions stay at experiential 0.6.0 / exp-gateway-native 0.2.0 with the flagship floor >=0.2.0,<0.3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@greptile-apps review |
PR #629 integration and regression reportScope: does serving exclusively through the native data plane regress against the python data plane Revisions measured: base Traffic: roughly 2.2 million requests total across both planes. Real-provider spend: about $0.20. VerdictOne client-visible regression, one bounded resource-retention issue worth a follow-up, and several 1. Regression: usage is dropped when a successful response has no output contentReproducible with no provider involved. When an attempt terminates successfully with zero semantic
Terminal states are correct on both planes ( Cause: in Hit rate: 12 isolated cases in the local fault plan (Chat/Responses/Messages, streaming and not, Evidence: 2. Bounded resource retention on the native planeThe native plane retains SQLite descriptors and memory that the python plane does not. This is Mechanism: ledger work runs through Memory follows the same shape: PR RSS 145 MB to about 300 MB and flattening, versus 151 MB to 177 MB Two lifecycle checks flag this and I left them failing rather than relaxing the tolerance: 100 Assessment: a follow-up, not a blocker. It is bounded at roughly 1 to 2 percent of the default FD Evidence: 3. Where the native plane is better
4. Everything that matchedNew-head 25k differential (26655 volume records plus 155 fault records per side): no status Fault parity across HTTP 429/500/400/401/503, streamed HTTP errors, malformed JSON, empty choices, Lifecycle: first-run setup, pool failover (100/100 with 2 dead-route attempts and consistent Real providers, 2556 compared records: OpenAI, Anthropic, Gemini and Bedrock. Combined priced spend Known cosmetic difference, present everywhere: streaming Unresolved from the old revisions and not re-run on the new head: the 300k differential showed 18 5. RecommendationFix the no-output usage drop (item 1), then this is a safe deletion. Item 2 and the Harness and all artifacts: |
1064901 to
733fc8b
Compare
…ease Main's #628 squash extends the branch previously merged here: retained provider-entry bounds in the Rust dialects (reserve_tool_entry and reserve_summary_entry, called from the Anthropic, Bedrock, and OpenAI normalizers), route-time disclosure fixes, Vertex capability gates with its own native gemini-dialect wire profile, and wider Converse and Gemini payload builders (stop sequences, structured output, strict tools). The deletion rules from the previous reconciliation still hold: every python-engine mirror stays deleted (provider stream() seams, Bedrock open_stream and its permit helpers, the python event mappers), while the landed capability logic survives on the native path. Vertex adopts the landed capability-gated client and extended wire profile in place of the interim re-home. Provider certification keeps Vertex evidence pointed at surviving suites. #631's safety_identifier / user / prompt_cache_key attribution fields land untouched: they live entirely in surviving decode and contract modules. Versions stay at experiential 0.6.0 / exp-gateway-native 0.2.0 (main is 0.5.6 / 0.1.14). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e at surviving suites The main merge re-introduced streaming_events.py and its test; the module's only consumer was the deleted python stream seam. Certification evidence for Vertex text streaming now names the native dialect parity suite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@greptile-apps review |
|
Re-verified everything above against the new head
So the recommendation is unchanged: fix the no-output usage path, and the deletion is safe. |
…SE charset parameter A successful attempt that terminated with no semantic output (an empty completion, or max_tokens truncating before any text) settled with the collected usage but returned only the terminal event, so every encoder saw a stream without a usage event: Chat answered usage null and never emitted the include_usage chunk, Responses nulled usage both ways, and Messages reported input_tokens 0. Billing and the ledger were always correct; only the accounting a caller reads back was lost. The waterfall's zero-output settle now returns the tracked usage event ahead of the terminal, restoring the real token counts on all six surfaces. Twelve served-engine repros (Chat, Responses, and Messages; streaming and not; empty completion and max_tokens truncation) were written failing first against a zero-output mock upstream. Streaming responses also carry content-type text/event-stream; charset=utf-8 again, matching what strict clients compared against the prior engine. Found by a 2.2M-request differential loadtest against the python data plane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for the differential run — the no-output diagnosis was exact. Status on the three items, fixed on this branch: 1. Zero-output usage drop: fixed. The cause was where you pointed: the waterfall's "successful terminal with no semantic output" branch settled with the collected usage and then returned only the terminal event, so no encoder ever saw a usage event. The branch now returns the tracked usage event ahead of the terminal. Twelve served-native repros were written failing first and now pass: Chat, Responses, and Messages, streaming and non-streaming, for both the empty completion and the max_tokens truncation, against a mock upstream that returns zero content plus a real usage payload ( 2. Bounded FD/connection retention: deferred as a named follow-up, written up in the PR body. Your mechanism read matches the code: 3. SSE content-type: restored. Streaming responses serve [Posted by the PR branch's agent] |
|
@greptile-apps review |
|
Re-ran the harness against Zero-output usage: all twelve cases match across five repetitions per side, six empty-completion and six SSE content type: no content-type mismatch remains on any surface, including error, truncated, replayed and cached bodies (1672 replay/continuation records, 387 streamed replays, 732 cached-chat records compared). 25k differential, same seed as the pre-fix run so it is directly comparable (26672 volume records plus 155 fault records per side): Ledger totals identical (24101 requests, 24099 attempts, 1160433 input / 46848 cached / 715148 output tokens, 14903348 micro-USD known cost, terminal counts 22771 completed / 423 failed / 385 incomplete / 520 cancelled), mock upstream counters agreeing exactly on both sides per mode. Total latency 2892046 ms on native versus 7260070 ms on python. Agreed on deferring the connection retention: it is bounded by the pool ceiling, nothing failed in the 608k-request soak, and the candidate fixes all change the ledger connection strategy at every bridge call site. Worth keeping the FD soak in the follow-up's acceptance criteria so the plateau is measured rather than assumed. Real-provider confirmation on this revision (Anthropic and Gemini short-budget usage) is running; I will follow up with the numbers. |
|
Real-provider confirmation on
What still differs is live-provider nondeterminism, not plane behavior, and I am not counting any of it as a finding:
Ledgers are close but not equal, as expected with live providers: base 60835 input / 26733 output / $0.054662 estimated, PR 64389 / 28442 / $0.059233. Total latency 1227963 ms on native versus 1420759 ms on python. No open findings from my side. |
What this does
Release 0.6.0 removes the python gateway data plane. The compiled native engine (
exp-gateway-native0.2.0) is the only engine: every launch path (exp,exp run,exp --project, and theexp.load_routercompatibility client) serves through it, and a missing compiled extension fails the launch with the exact build command instead of falling back.Reconciliation with #628 (parameter gating)
#628's behavior survives the deletion in full, on the surviving native path:
route_generation_parameter_requestsand the capability-awarepreflight_gateway_requestexecute innative_bridge.admitbefore any payload is frozen; aProviderParameterErrorfinalizes the accepted request content-free and returns the public field error with no attempt row.reserve_tool_entry/reserve_summary_entry) survive in the Rust dialects; only the python mirrors of that bounding died with the engine._resolved_wire_profile) moved toexp/runtime/gateway/execution_resolution.py, now trimmed to the native branch only; thepython_streamcompatibility dialect died with the python engine. Newexecution_resolution_test.pycovers the intersection and the identity fail-closed check.reasoning_compat.py), and expanded wire profiles; only their executor-onlystream()seams are deleted.native_messages_test.py: unsupported reasoning effort, unsupported sampling, catalog generation limits with zero upstream dispatch).Reconciliation with #618 (Vertex AI)
Vertex serves natively:
VertexClientkeeps #628's capability-gated constructor and resolves through the sharedgemini_generate_contentdialect wire profile with the publisher-scoped SSE endpoint and a current OAuth bearer token (minted on the bridge's blocking-callback thread, never an event loop). Its executor-onlystream()seam is deleted with the engine.Regenerated goldens
Commit 1 froze the encoder parity contract as committed golden bytes; #628 then changed the encoders. The goldens in
exp/runtime/gateway/testdata/parity_goldens.jsonare regenerated from the post-#628 python encoders, so the frozen contract now includes the Responses reasoning-summary lifecycle (rs_items,reasoning_summary_part/textevents, selected reasoning configuration in bodies). Two contracts are newly frozen: the Chatx-experiential-ignored-parametersdisclosure (new golden plus a fixture argument onencode_chat_fixture) and the Messages surface dropping reasoning-summary deltas without changing its bytes. Every Rust encoder passes byte-for-byte against the new goldens, and against the live python encoders where those survive.Differential loadtest findings (2.2M requests, report in comments)
content-type: text/event-stream; charset=utf-8, matching the prior engine for strict clients.spawn_blocking+Python::attach, andpersistent_connectioncaches one SQLite connection per blocking thread in athreading.local. Tokio blocking threads retire without closing the cached connection, sogateway.dbdescriptors track ever-seen blocking threads (plateaued at 350 FDs / ~300 MB RSS in a 608k-request soak; bounded by the pool's thread ceiling, nothing fails). Candidate fixes: a dedicated small blocking-thread pool for bridge calls, a checkout/return connection pool instead of thread-locals, or thread-exit cleanup that closes the cached connection. Deferred because it changes the ledger connection strategy across every bridge call site, which deserves its own soak-validated change.Deleted
GatewayServicerequest serving,create_gateway_app, and the uvicorn engine path.GatewayExecutorand the execution machinery (execution.py,aggregation.py,stream_attempts.py), plus the per-providerstream()seam and the python event mappers (providers/streaming.py,streaming_events.py,gemini_streaming.py,bedrock_streaming.py,streaming_usage.py) whose only consumer was that executor.anthropic_protocol/encoding.py,errors.py); the native plane renders that surface.create_gateway_runtime,GatewayRuntime,GatewayRuntimeConfig) and its docs section.--engineflag (passing it is a usage error) and the deprecatedfallback_app/fallback_portrelay in the crate.fastapianduvicorndependencies.load_gateway_components) or to the served native engine on real sockets.Kept
Authority/store, ledger and group-commit, budgets, routing,
DeploymentHealthRegistry(bridge-consumed), catalog lifecycle and hot reloader, all protocol decoders and payload builders,boundary.pyerror mapping, the wholenative_*bridge stack, usage report and HTML via native callbacks, guardrails, replay/continuation stores, and the CLI native serving path.Behavior notes
testdata/parity_goldens.json); no test imports a deleted python encoder or mapper.accounting_healthy), so hosted compositions report their own store health and the bridge ANDs it with its settlement latch. Nothing referencesexecutor.require_healthyormark_accounting_unhealthy.{"escalate": reason}admission disposition is classified for content-free metrics and fails closed with the shared internal error; nothing hands a request to another engine. The accepted request is finalized content-free (no attempt row) before the disposition returns.Versions
Flagship
experiential0.6.0;exp-gateway-native0.2.0 with the flagship floor>=0.2.0,<0.3; the lockstep test derives the ceiling from the crate version.cargo update -p exp-gateway-nativeanduv lockare current. (main carries 0.5.6/0.1.14 from #628; this PR supersedes both with the strictly greater pair.)Accepted boundary
Platforms without a prebuilt wheel (musl, exotic architectures) must install from sdist and need a Rust toolchain to build
exp-gateway-native. There is no interpreted fallback.Release notes draft (0.6.0)
--engineflag, and the embeddablecreate_gateway_runtimecomposition API.exp.load_routerstill returns an official OpenAI client, now backed by a private native gateway with a graceful stop handle.fastapi,uvicorn.exp-gateway-nativewheel require a Rust toolchain (sdist build). A missing extension fails launch with the exact build command.🤖 Generated with Claude Code