diff --git a/Cargo.lock b/Cargo.lock index 37b882fa..49743cf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3670,7 +3670,7 @@ dependencies = [ [[package]] name = "tw-api" -version = "0.82.0" +version = "0.83.0" dependencies = [ "serde", "serde_json", @@ -3681,7 +3681,7 @@ dependencies = [ [[package]] name = "tw-bedrock" -version = "0.82.0" +version = "0.83.0" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -3702,7 +3702,7 @@ dependencies = [ [[package]] name = "tw-breaker" -version = "0.82.0" +version = "0.83.0" dependencies = [ "serde", "serde_json", @@ -3710,7 +3710,7 @@ dependencies = [ [[package]] name = "tw-config" -version = "0.82.0" +version = "0.83.0" dependencies = [ "blake3", "libc", @@ -3736,7 +3736,7 @@ dependencies = [ [[package]] name = "tw-control" -version = "0.82.0" +version = "0.83.0" dependencies = [ "axum", "base64 0.22.1", @@ -3782,7 +3782,7 @@ dependencies = [ [[package]] name = "tw-dialect" -version = "0.82.0" +version = "0.83.0" dependencies = [ "memchr", "serde", @@ -3791,7 +3791,7 @@ dependencies = [ [[package]] name = "tw-engine" -version = "0.82.0" +version = "0.83.0" dependencies = [ "chrono", "serde", @@ -3805,7 +3805,7 @@ dependencies = [ [[package]] name = "tw-gateway" -version = "0.82.0" +version = "0.83.0" dependencies = [ "arc-swap", "async-stream", @@ -3856,7 +3856,7 @@ dependencies = [ [[package]] name = "tw-guard" -version = "0.82.0" +version = "0.83.0" dependencies = [ "aho-corasick", "base64 0.22.1", @@ -3874,7 +3874,7 @@ dependencies = [ [[package]] name = "tw-handover" -version = "0.82.0" +version = "0.83.0" dependencies = [ "libc", "serde", @@ -3887,7 +3887,7 @@ dependencies = [ [[package]] name = "tw-link" -version = "0.82.0" +version = "0.83.0" dependencies = [ "serde", "serde_json", @@ -3901,7 +3901,7 @@ dependencies = [ [[package]] name = "tw-observe" -version = "0.82.0" +version = "0.83.0" dependencies = [ "tokio", "tracing", @@ -3910,7 +3910,7 @@ dependencies = [ [[package]] name = "tw-pack" -version = "0.82.0" +version = "0.83.0" dependencies = [ "age", "blake3", @@ -3934,7 +3934,7 @@ dependencies = [ [[package]] name = "tw-plugin" -version = "0.82.0" +version = "0.83.0" dependencies = [ "libc", "rand 0.10.2", @@ -3950,7 +3950,7 @@ dependencies = [ [[package]] name = "tw-pricing" -version = "0.82.0" +version = "0.83.0" dependencies = [ "arc-swap", "flate2", @@ -3963,14 +3963,14 @@ dependencies = [ [[package]] name = "tw-secret" -version = "0.82.0" +version = "0.83.0" dependencies = [ "thiserror 2.0.20", ] [[package]] name = "tw-store" -version = "0.82.0" +version = "0.83.0" dependencies = [ "blake3", "bytes", @@ -3990,7 +3990,7 @@ dependencies = [ [[package]] name = "tw-sync" -version = "0.82.0" +version = "0.83.0" dependencies = [ "age", "axum", @@ -4018,7 +4018,7 @@ dependencies = [ [[package]] name = "tw-types" -version = "0.82.0" +version = "0.83.0" dependencies = [ "serde", "serde_json", @@ -4027,7 +4027,7 @@ dependencies = [ [[package]] name = "tw-watch" -version = "0.82.0" +version = "0.83.0" dependencies = [ "libc", "notify", @@ -4038,7 +4038,7 @@ dependencies = [ [[package]] name = "tw-yaml" -version = "0.82.0" +version = "0.83.0" dependencies = [ "saphyr-parser", "serde_yaml_ng", @@ -4048,7 +4048,7 @@ dependencies = [ [[package]] name = "twcore" -version = "0.82.0" +version = "0.83.0" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index 89dbfb48..ebb6a463 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ categories = ["network-programming", "web-programming::http-server"] # 二进制走 CalVer,crate 走 SemVer —— 两者是两回事,卖给不同的人。 # 这里是 crate 的版本。 -version = "0.82.0" +version = "0.83.0" [workspace.dependencies] # ── 内部 crate ─────────────────────────────────────────── diff --git a/release-notes/0.83.0.md b/release-notes/0.83.0.md new file mode 100644 index 00000000..c394e291 --- /dev/null +++ b/release-notes/0.83.0.md @@ -0,0 +1,29 @@ +This release retries upstreams that fail for a moment before any answer content, including a single upstream, and lets routing rules set the reasoning effort, adapting when an upstream refuses one. + +**Upgrade notes** + +- The control-plane protocol version goes from 52 to 54. The request store's schema is unchanged; nothing is cleared. +- `set.thinking` in routing rules is removed and replaced by `set.reasoning`. A configuration that still has `thinking:` under a rule's `set` does not load: core starts in safe mode and the one-click repair removes the field. `thinking: false` corresponds to `reasoning: off`. +- `failover.wait_secs` now limits queueing and retrying together. Its default (120 seconds) and range are unchanged; `0` turns both off. + +**Temporary upstream failures are retried.** When every candidate has been tried and the last attempt failed temporarily before any answer content reached the client, the gateway backs off and tries again instead of passing the error on. With a single upstream, the request is sent to it again. + +- Temporary failures: statuses 500, 502, 503, 504 and 529, unless the body names a request error; in-stream errors with a listed code or type (`server_error`, `api_error`, `overloaded_error`, `server_is_overloaded`, `slow_down`, Gemini `UNAVAILABLE` / `INTERNAL`, Bedrock `serviceUnavailableException` / `internalServerException`); a connection that cannot be made or breaks before the response head; a stream that breaks, or ends after only its opening events, before content (new code `gw.upstream.stream_ended_early`). +- Not retried: other statuses, unlisted in-stream errors, rate limits (they keep using the throttle queue), used-up quota, no balance, rejected credentials, a missing model, idle timeouts, and any failure after content. Failover to another candidate is unchanged. +- Pauses are 2, 4 and 8 seconds, then 16 seconds each time, with up to 20% jitter either way. `retry-after-ms`, `Retry-After` or a retry delay in the body can lengthen a pause but not shorten it. Retrying stops when the next pause would end after the request's wait deadline; the last error is then handed over. +- Each round retries the candidates that failed temporarily in the previous round. A conversation kept on an upstream (session affinity, encrypted history) retries only that upstream. Each retry is a normal hop: request hooks, content filtering, redaction and parameter rewrites run again. Token-counting requests are not retried. +- The last candidate's streamed answer is held until its first content while the request can still wait, so an error at the start can be retried. After 15 seconds the client receives the response head and keep-alives as before. Whole answers from the last candidate are passed on as they arrive. +- Hits in content written by plugins are reported once per client request, not once per attempt. +- New event `RequestRetrying` (paired with `RequestDequeued`); `AttemptView` gains `retry` and `backoff_ms`. + +**Routing rules can set the reasoning effort.** `set.reasoning` takes `off` or one of `low`, `medium`, `high`, `xhigh`, `max`. + +- A level changes the effort of requests that already have reasoning on. Requests without reasoning are left as they are; reasoning is never turned on. +- `off` turns reasoning off: OpenAI Chat and Responses requests get `none`, and other formats lose the reasoning configuration together with the effort. Anthropic's `output_config.effort` and the GLM-style Chat `thinking` switch are now removed as well. +- Rules apply per hop, so a rule with a `provider_would_be` condition changes only that upstream. Gemini budgets are capped at the model's limit. + +**Refused efforts are resent with one the upstream accepts.** When an upstream answers 400 because of the effort level, the request is sent again once to the same upstream with a level it accepts (the ceiling, or the nearest level the error lists), or without the effort when the error names none. + +- After a successful resend the choice is remembered per upstream, model, format and level for 7 days (`effort-memory.json` in the data directory), so later requests are right the first time. Entries are dropped when the upstream's address, key or protocol changes, or when it is removed. +- The resend does not count toward retries. Requests that set only a budget, and WebSocket turns, are not resent. +- `AttemptView.effort` (`asked`, `sent`, `reason`: `rule`, `rejected` or `remembered`) records every change on the attempt chain.