Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
a890511
chore: pin the core dependency to a tag instead of tracking main (#25)
fylorn Sep 23, 2026
a90e9f3
feat!: forward what can be forwarded, convert only what must be (#26)
fylorn Sep 23, 2026
ca7647c
refactor: take the circuit-breaker registry and metric labels back fr…
fylorn Sep 23, 2026
a6678cf
refactor: redact PII with core's guard engine, and restore tool argum…
fylorn Sep 23, 2026
437a671
feat: inspect the tool calls an upstream returns (#29)
fylorn Sep 23, 2026
038796b
build: keep only line tables in dev and test builds (#30)
fylorn Sep 24, 2026
05f52e8
feat: one circuit-breaker state machine, and hidden characters in req…
fylorn Sep 24, 2026
8a76395
fix: make the integration suite pass again (#32)
fylorn Sep 24, 2026
6482690
test: fix the two flaky integration tests (#33)
fylorn Sep 24, 2026
9f46116
fix: bill a Chat stream whose caller did not ask for usage (#34)
fylorn Sep 24, 2026
3c35ab8
fix: a request the upstream refuses no longer fails over or trips bre…
fylorn Sep 24, 2026
234fcce
Merge main (v1.1.0) into dev
fylorn Sep 24, 2026
82f8061
docs: cut releases from a release branch, and keep the tag's headings…
fylorn Sep 24, 2026
bd57b5d
ci: run checks on pull requests into dev, including the integration s…
fylorn Sep 24, 2026
c1b2085
fix: bill cached input at cache prices, estimate missing usage, strip…
fylorn Sep 24, 2026
ad5b75b
refactor: take back what only this side used from core (#41)
fylorn Sep 24, 2026
3a617f3
feat(gateway): take official hosts, output fallback and error shapes …
fylorn Sep 24, 2026
ed1c8b5
feat(gateway): accept Gemini clients, and the Responses API over a We…
fylorn Sep 24, 2026
28d4387
test: count the probes that miss before the streaming cache hit (#44)
fylorn Sep 24, 2026
d3f36fc
refactor(server): move the catalog's SQL into repositories (#45)
fylorn Sep 24, 2026
83e9bcc
refactor(server): move the dashboard, limits and log-forwarding handl…
fylorn Sep 24, 2026
9b5b326
refactor(server): move the MCP handlers' SQL into repositories (#50)
fylorn Sep 24, 2026
86beb82
refactor(gateway): run the request guards on core's tw-guard engines …
fylorn Sep 24, 2026
1c80b83
refactor(server): move the identity handlers' SQL into repositories (…
fylorn Sep 24, 2026
c25b44f
fix(mcp): revoking a default connection no longer fails with a 500 (#51)
fylorn Sep 24, 2026
dbce845
test: leave the cancelled stream after it has started, not on a timer…
fylorn Sep 24, 2026
c3d2d57
refactor(server): move the access handlers' SQL into repositories (#48)
fylorn Sep 24, 2026
d92c2d1
fix(settings): read security.totp_required as a boolean, and seed aut…
fylorn Sep 24, 2026
7e95183
feat(gateway): record a client that leaves before its response exists…
fylorn Sep 24, 2026
367df3c
feat(auth): enforce the TOTP requirement (#55)
fylorn Sep 24, 2026
443de8d
feat(gateway): keep the last response on a Responses WebSocket (#56)
fylorn Sep 24, 2026
6890c72
Merge main (v2.0.0) into dev
fylorn Sep 24, 2026
d4f6d5a
docs(release): CI runs the integration suite on the release PR (#58)
fylorn Sep 24, 2026
ef3de87
docs(contributing): point vulnerability reports at the organization's…
fylorn Sep 25, 2026
c4f8b1c
docs(readme): current description of ThinkWatch Lite and ThinkWatch C…
fylorn Sep 25, 2026
f259962
feat(providers): authenticate Bedrock with an API key (#61)
fylorn Sep 28, 2026
78f9ab5
test: hold the early-cancel client once its key is known, not on a ti…
fylorn Sep 28, 2026
1b752f2
feat(providers): list Bedrock's models, and let the route editor take…
fylorn Sep 28, 2026
1f08421
fix(bedrock): refuse keys that will not decrypt, and keep instance-ro…
fylorn Sep 28, 2026
e480bfd
refactor(bedrock): use core's shared tw-bedrock, and core v0.55.0 (#65)
fylorn Sep 29, 2026
e1e7999
docs(readme): rewrite both READMEs to be short and accurate (#66)
fylorn Sep 30, 2026
af9eb81
Merge main (v2.1.0) into dev
fylorn Sep 30, 2026
ed6dd39
docs(readme): 37 MCP templates, what the setup wizard does, body reda…
fylorn Sep 30, 2026
9afcc08
fix(rbac): make the seeded team_manager work at team scope (#69)
fylorn Sep 30, 2026
fa3a5b9
fix(rbac): require gateway use, and count only grants that give it (#70)
fylorn Sep 30, 2026
a1eed6f
Merge main (v2.2.0) into dev
fylorn Sep 30, 2026
da88b09
Merge main (sponsor line) into dev
fylorn Oct 2, 2026
1a399e7
Allow clippy::double_must_use on the async_trait BlobStore (#73)
fylorn Oct 2, 2026
8d8d96f
Unify the request guards with thinkwatch-core's rule model (#72)
fylorn Oct 3, 2026
2baa123
Fix what the pre-release review of the guard unification found (#75)
fylorn Oct 3, 2026
33c22dc
Merge main (v3.0.0) into dev
fylorn Oct 3, 2026
dcc8616
Merge main (README: ThinkWatch Lite for individual developers) into dev
fylorn Oct 4, 2026
6b73344
Merge main (v3.1.0) into dev
fylorn Oct 5, 2026
03c393c
Usage limits: token limits refuse, key limits count on the key, Retry…
fylorn Oct 5, 2026
4075855
fix(redis): connect over TLS for rediss:// URLs (#82)
fylorn Oct 5, 2026
33234ee
Concurrent startup, body retention on restart, and Helm network polic…
fylorn Oct 5, 2026
b7a34e1
Merge main (v3.2.0) into dev
fylorn Oct 5, 2026
2ce6471
chore(deps): update rust crate rand to v0.10.1 [security] (#77)
renovate[bot] Oct 6, 2026
0682864
chore(deps): update rust crate xxhash-rust to v0.8.16 [security] (#78)
renovate[bot] Oct 6, 2026
1fbe7bd
chore(deps): take the Renovate dashboard upgrades (#85)
fylorn Oct 6, 2026
2e23fd9
chore(deps): pnpm 12, and ClickHouse row validation on (#86)
fylorn Oct 6, 2026
977cab0
ci: save Rust caches only on pushes, and let releases read CI's image…
fylorn Oct 8, 2026
a19ed65
Merge main (v3.2.1) into dev
fylorn Oct 8, 2026
ea2a81f
Merge main (v3.3.0) into dev
fylorn Oct 9, 2026
e8ae1f1
Stream endings and in-stream errors logged like answers; core v0.68.0…
fylorn Oct 10, 2026
1be0683
Release 3.3.1
fylorn Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 129 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,133 @@ target.

## [Unreleased]

## [3.3.1] — 2026-10-10

Streams are logged as they ended. A caller that hangs up once it has read
the whole answer, as Codex does, finished the request; an error the
upstream or Bedrock reports partway through a stream is logged with the
status the same error has as an answer, and counts against the route only
when that answer would; a Responses WebSocket turn ends at its last event.
The thinkwatch-core crates move from v0.67.1 to v0.68.0.

### Read before upgrading

- **Request logs and error rates change for streams.** Codex streams that
were logged as cancelled (`499`) are logged as finished (`200`) and
billed on the reported usage. Streams the upstream failed partway are
logged with a `4xx` or `5xx` `status_code` instead of `200`, so error
rates that count `status_code` ≥ 400 include them; `client_status`
keeps the `200` the caller received. No configuration, database schema,
API or Helm value changes.

### Fixed

- **A caller that hangs up on the answer's last event finished the
request.** Codex closes the connection as soon as it has read
`response.completed`, without waiting for the stream to end, and an
upstream can end its stream a while after that event: the ChatGPT Codex
backend often does, and so can a relay in front of it. A stream forwarded
in its own format whose caller left in that window was logged as
cancelled (`499`, `stream_outcome: client_cancelled`), though the caller
had read the whole answer. Once the answer's last event has gone out —
Responses `response.completed`, `response.incomplete` or
`response.failed`, Anthropic `message_stop`, Chat `[DONE]` — a caller
that leaves is logged as if the stream had run to its end: `200`, billed
on the usage the upstream reported, or, after `response.failed`, the
upstream's failure (see the next entry). Converted streams were not
affected: their last event is written once the upstream's stream has
ended.
- **An error the upstream reports partway through a stream fails the
request.** An upstream that has answered `200` can still report an error
in the stream: an Anthropic `error` event (an `overloaded_error` arrives
this way), Responses `response.failed`, an `error` in a Chat or Gemini
chunk. When the stream then ended normally, the request was logged as a
success (`200`) and counted as one by the route's circuit breaker. It is
now logged as the error it would have been as an answer, with what the
upstream said: `stream_outcome` `upstream_error`, `client_status` `200`
(the status the caller's response went out with), and the `status_code`
and `error_type` that answer would have had, read off the error's own
type or code. A rate limit or a spent quota is `429`
(`UpstreamRateLimited`); a context too long or another invalid request
`400`, an unknown model `404`, an overload `529` or `503`
(`ProviderHttpError`); an error that names no status `502`
(`ProviderError`). The route's health and circuit breaker treat it as
they treat that answer: a `5xx`, a `429` or a refused credential counts
against the route, so a route whose streams keep failing this way opens
its breaker, while a request the upstream refuses, such as one past the
model's context window, does not. This holds for streams forwarded as
sent and converted ones, and for a caller that leaves after the error.
Billing is unchanged: such a request is billed on the usage the upstream
reported.
- Error rates that count `status_code` ≥ 400 now include these requests.
- The rows of other streams that failed after the response went out as
`200` — broken off, cut by tool-call inspection, refused by the
upstream before its first byte — carry `client_status` too.
- **An exception Bedrock reports partway through a stream is logged as
the same exception as an answer.** Bedrock reports a failure in a
stream it has started as an exception event: `throttlingException` when
it throttles partway through, `validationException`,
`internalServerException`, `serviceUnavailableException` and others.
Each was logged as the stream breaking off in transit (`502`,
`error_type` `transport`) and counted against the route's circuit
breaker. Each is now logged with the status the desktop gateway gives
that exception and with what Bedrock said: throttling `429`
(`UpstreamRateLimited`), an invalid request `400`, a model timeout
`504`, the service unavailable `503`, any other exception `500`
(`ProviderHttpError`). Access denied is logged as a refused credential
(`UpstreamAuthError`), without Bedrock's words, which name the IAM
principal, as for a `403` answer. The route's health and circuit breaker treat it
as they treat that answer (see the previous entry), so a request Bedrock
refuses as invalid no longer counts against the route. What the caller
receives is unchanged, and a damaged event-stream frame is still logged
as `transport` `502`.
- **A Responses WebSocket turn ends at its last event.** On
`GET /v1/responses` over a WebSocket, a turn lasted until the upstream's
stream ended, and the connection took the next turn only then, so an
upstream that keeps its stream open after `response.completed` held up
every following turn by that long. The next turn now starts at once.

### Changed

- thinkwatch-core crates (tw-bedrock, tw-breaker, tw-dialect, tw-guard)
v0.67.1 → v0.68.0. tw-dialect changes what the gateway does, tw-guard
only adds to its API, and tw-bedrock and tw-breaker do not change:
- **Long usage objects are read** (tw-dialect). The usage reader skipped
a usage object longer than 8 KB. The ChatGPT Codex backend's usage
carries an `attribution` breakdown that grows with the conversation, so
behind an upstream that passes it on, a longer Codex session's requests
were billed on an estimate (`usage_estimated`) instead of the reported
counts. Usage objects up to 1 MiB are now read. One cut across chunks
is read on from where it stopped, nothing nested inside a usage object
is counted as a second usage, and a stream that arrives in chunks
shorter than the `"usage"` key no longer loses it.
- **A `null` error in a stream chunk is not an error** (tw-dialect).
Some OpenAI-compatible relays and Gemini-format upstreams write
`"error": null` in every streamed chunk, and the Chat and Gemini stream
readers took each such chunk for an error the upstream reported. A
stream converted to another format ended in an error and lost its
text, and for such a stream forwarded as sent, no assembled answer was
cached or captured as the response body. A Gemini caller's tool result
carrying `"error": null` and no `output` went to an upstream of
another format as a failed one. A `null` error is now ignored, and a
Chat chunk's error without a message reaches the caller of a converted
stream as a fixed sentence instead of the whole chunk, which could hold
answer text. The gateway's own check for an error partway through a
stream (see Fixed) ignores a `null` error as well.
- `tw_dialect::convert::ends_answer` recognises an answer's last event.
The gateway uses it to tell a caller that left after the whole answer
from one that left partway, and to end a WebSocket turn (see Fixed).
- tw-guard adds, for the desktop gateway's security log, the `locate`
module, `content::places`, `content::places_text` and `RulePlaces`,
`content::Rule::draw`, `Ledger::placeholder_of`,
`RuleSet::custom_pattern`, `tools::Rule::find_all`, and the fields
`pattern`, `arguments`, `capped`, `places` and `path` on
`tools::wall::Verdict`. Secret redaction, the content filter and
tool-call inspection find, replace, refuse and cut exactly what they
did before. `Verdict::arguments` is a flagged tool call's arguments
with redacted values restored; the gateway does not log it, and its
tool-call audit entries keep the fields they had.

## [3.3.0] — 2026-10-09

Requests converted for Claude now use prompt caching, and Codex works
Expand Down Expand Up @@ -1569,7 +1696,8 @@ unreleased builds should: stop the gateway, run `db/schema.sql`
against PostgreSQL, restart against this tag. The schema is
idempotent end-to-end, so the apply is safe to repeat.

[Unreleased]: https://github.com/ThinkWatchProject/ThinkWatch/compare/v3.3.0...HEAD
[Unreleased]: https://github.com/ThinkWatchProject/ThinkWatch/compare/v3.3.1...HEAD
[3.3.1]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.3.1
[3.3.0]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.3.0
[3.2.1]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.2.1
[3.2.0]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.2.0
Expand Down
28 changes: 14 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ members = [
]

[workspace.package]
version = "3.3.0"
version = "3.3.1"
edition = "2024"
# The MSRV: the newest `rust-version` among the locked dependencies
# (sqlx 0.9 and the aws-smithy crates under aws-sigv4). Without it,
Expand Down Expand Up @@ -70,10 +70,10 @@ opt-level = 3
# never re-exported through a local shim. And the reverse: something only
# this side uses (the at-rest crypto, IMDSv2 credentials, the gateway error)
# lives here, not in core.
tw-bedrock = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.67.1" }
tw-breaker = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.67.1" }
tw-dialect = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.67.1" }
tw-guard = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.67.1" }
tw-bedrock = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.68.0" }
tw-breaker = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.68.0" }
tw-dialect = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.68.0" }
tw-guard = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.68.0" }

# Web framework
axum = { version = "0.8", features = ["macros", "ws"] }
Expand Down
Loading
Loading