Skip to content

End streamed answers that generate below an upstream's minimum speed - #384

Merged
fylorn merged 1 commit into
mainfrom
feat/min-speed
Oct 30, 2026
Merged

fylorn merged 1 commit into
mainfrom
feat/min-speed

Conversation

@fylorn

@fylorn fylorn commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

What

A new per-upstream setting, providers[].min_speed:

providers:
  - name: local-vllm
    min_speed: { tokens_per_sec: 15, window_secs: 20 }

From the first token on, a streamed answer (SSE or Bedrock eventstream) is judged once a second. When the average speed over the last window_secs seconds is below tokens_per_sec, the gateway:

  • ends the answer with a 503 error frame in the client's format (Anthropic overloaded_error, Responses response.failed / server_error, Gemini UNAVAILABLE, Chat {"error": …}) and drops the upstream connection;
  • fails the request with gw.upstream.too_slow (upstream, rate, min, secs), source upstream;
  • records one failure for the upstream (counts toward failures_to_pause) and stops the conversation from sticking to it this turn;
  • keeps usage: whatever the upstream had not reported yet is filled with the gateway's estimates, and the request row keeps the measured speed.

Speed uses the same estimate as the live speed. Silences of 5 seconds or more are taken out of the window (the idle timeout handles those), and a second with less than half a window left after that is not judged. Non-streamed answers and WebSocket turns are not judged. Unset means off; behaviour is unchanged.

Contract

  • ProviderView.min_speed?: MinSpeedView { tokens_per_sec, window_secs }, ProviderInput.min_speed?: MinSpeedInput { tokens_per_sec, window_secs }.
  • Event::RequestFailed gains tokens_per_sec?: number, set only for this failure.
  • New codes: gw.upstream.too_slow, config.min_speed_range (upstream, field, value, min, max). One-click repair drops min_speed when the rate is out of range and resets window_secs to 20 when the window is.
  • CONTROL_API_VERSION 57 → 58. Request store schema unchanged.
  • Config packs, sharing and sync carry min_speed with the upstream.
  • StreamConverter::fail_with(status, …) lets converted streams end with a status-specific error; other cut-offs keep using fail.

Tests

  • tw-gateway/tests/min_speed.rs (fake upstreams, the "second" scaled to 50 ms): stays fast, short slowdown, long pause mid-stream, long wait before the first token, upstream without a minimum, non-streamed answer, slowdown for a window (frame, failure, speed, estimated usage, upstream dropped, health), Chat/Responses/Gemini clients, Bedrock eventstream.
  • Unit tests for the window maths, validation, repair, control-plane save/view, pack units, recorder, dialect frames and the 503 mapping.
  • Config manual regenerated.

🤖 Generated with Claude Code

An upstream can now set `min_speed: { tokens_per_sec, window_secs }`
(1-1000 tokens/s, 5-120 s, window defaults to 20 and is always written
back). From the first token on, a streamed answer (SSE or Bedrock
eventstream) is judged once a second: when the average speed over the
last `window_secs` seconds is below the minimum, the answer ends with a
503 error frame in the client's format (Anthropic overloaded_error,
Responses response.failed/server_error, Gemini UNAVAILABLE, Chat error
object), the upstream connection is dropped, the upstream records one
failure and the conversation no longer sticks to it this turn.

- Speed uses the same estimate as the live speed (text, reasoning and
  tool arguments, calibrated by pace). Silences of 5 s or more are taken
  out of the window; a second with less than half a window left after
  that is not judged.
- The request fails with gw.upstream.too_slow (upstream, rate, min,
  secs), source `upstream`. RequestFailed gains `tokens_per_sec`, set
  only for this failure and kept in the request row. Usage the upstream
  had not reported yet is filled with the estimates.
- New validation code config.min_speed_range (upstream, field, value,
  min, max); one-click repair drops min_speed when the rate is out of
  range and resets window_secs to 20 when the window is.
- ProviderView/ProviderInput gain min_speed (MinSpeedView/MinSpeedInput);
  config packs, sharing and sync carry it as part of the upstream.
- Converted streams can now end with a status-specific error
  (StreamConverter::fail_with); other cut-offs are unchanged.
- Control API version 58. Request store schema unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn
fylorn merged commit 30b8be3 into main Oct 30, 2026
6 checks passed
@fylorn
fylorn deleted the feat/min-speed branch October 30, 2026 15:03
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant