Repository navigation
feat(console): Add to ThinkWatch Lite; gateway address for every user; model lists follow the key - #100
Merged
Conversation
GET /v1/models and GET /v1beta/models listed every routed model whatever the key, while a request for a model outside the key's allow-list, or outside what its owner's roles grant, is refused. A client that picks a model from the list could pick one it is not allowed to call. The request-time check and the listings now share one decision, GatewayRequestIdentity::may_call (exact or prefix match against the key's allowed_models intersected with the role grants; an empty list allows nothing). The listings put each routed name through the same alias mapping a request would, so a listed model is one a request for it is let through for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ery user The screens that show a key once (a new key, a rotated key, the end of first-run setup) get an "Add to ThinkWatch Lite" button beside Copy. It opens thinkwatch://import?service=thinkwatch&name=thinkwatch&url=...&key=..., the link ThinkWatch Lite 2026.10.16 and later turns into a new upstream for this gateway. The link is built in the browser from the key already on screen; nothing new is sent anywhere. Values Lite would refuse are caught first and the button says why instead: a key without the ai_gateway surface, or a plain-HTTP gateway address other than this machine's. Once clicked, a line links to the download page in case Lite is not installed. Lite is a desktop app, so below the md breakpoint only Copy shows. The button needs the gateway's public address, and so does the Configuration Guide, which read it from /api/admin/settings/system: that needs settings:read, so every other user got a 403 and an address made of the browser's host and port 3000, whatever general.public_* said. GET /api/gateway/info now answers any signed-in user with the base URL, computed server-side from general.public_*; unset parts come from the address the console was opened at (X-Forwarded-Proto, X-Forwarded-Host or Host) and the gateway's listening port, which is what "auto-detect from the browser" meant. Those headers only fill gaps an administrator left, and the answer goes back to the caller who sent them. The query does not sign the user out on a 401, so it can never take a one-time key screen away. The guide gets a ThinkWatch Lite tab, first among the AI gateway clients, whose button opens the create-key dialog prefilled for Lite (name "ThinkWatch Lite", AI gateway only). The dialog's catalogs move into useKeyDialogCatalogs so the guide can open the same dialog, loading them only while it is open. Users without api_keys:create are told how to get a key and add it by hand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ThinkWatch Lite's import-link contract (v2.2) now takes http:// for loopback, private IPv4 ranges (10/8, 172.16/12, 192.168/16, 100.64/10, 169.254/16), private IPv6 ranges (fc00::/7, fe80::/10) and intranet-style names (no dot, or ending in .local, .lan, .internal, .intranet, .corp, .home.arpa, .localhost). Company gateways are often served over plain HTTP inside the network, and clients set up by hand already send the key in plain text there, so refusing the link added no safety. The button's check follows that list exactly; any other http:// host keeps the disabled button and its reason, reworded to say what Lite accepts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
thinkwatch://import?service=thinkwatch&name=thinkwatch&url=<gateway base URL>&key=<key>(each valueencodeURIComponent-encoded), which ThinkWatch Lite 2026.10.16 and later turns into a new upstream for this gateway. It is disabled, with the reason beside it, for a key without theai_gatewaysurface and for a gateway whose public address Lite would refuse (plain HTTP outside this machine and private networks, or anything but a barescheme://host[:port]). Once clicked, a line under it reads "Nothing happened? Install ThinkWatch Lite (2026.10.16 or later) first" (zh: 「没有反应?先安装 ThinkWatch Lite(2026.10.16 或更新版本)」) and links tothinkwat.ch/liteorthinkwat.ch/zh-CN/lite, following the console language. Below themdbreakpoint only Copy shows.ai_gateway) for users withapi_keys:create, and a download link. Users without that permission are told how to get a key and add it by hand.GET /api/gateway/info(console port, any signed-in user):{"base_url": "https://gateway.example.com"}, computed server-side fromgeneral.public_protocol/host/port. Unset parts come from the address the console was opened at (X-Forwarded-Proto, elsehttp;X-Forwarded-Host, elseHost) and the gateway's listening port; a default port is left out; a configured host that is more than a host (a port, a path,${…}) is passed over.Fixes
/api/admin/settings/system, which needssettings:read; everyone else got a 403 andbrowser host + :3000, whatevergeneral.public_*said. It now reads/api/gateway/info./v1/modelsand/v1beta/modelslisted every model whatever the key. They now list only the models a request with that key is let through for: the request-time check and the listings shareGatewayRequestIdentity::may_call(the key'sallowed_modelsintersected with the owner's role grants, exact or prefix match, empty = nothing), applied after the same alias mapping a request goes through. CHANGELOG has this under "Read before upgrading" because clients with narrowed keys will see shorter lists.Credential handling
hrefof an<a>in the same dialog. No request carries it; the download link carries nothing but the page URL (rel="noopener noreferrer"). The key is never stored or logged anywhere new./api/gateway/inforeturns only the public address. The headers it reads fill only what an administrator left unset, and the answer goes back to the same caller, so a forged header misleads nobody else. The console query for it passesno401Redirect, so a 401 can never navigate away from a screen that shows a key once.may_call, whichis_access_allowednow calls).Plain HTTP on private networks
The button follows ThinkWatch Lite's import-link contract v2.2:
https://always;http://for loopback (localhost,127.0.0.1,[::1]), private IPv4 (10/8,172.16/12,192.168/16,100.64/10,169.254/16), private IPv6 (fc00::/7,fe80::/10), and names with no dot or ending in.local,.lan,.internal,.intranet,.corp,.home.arpa,.localhost. Any otherhttp://host keeps a disabled button with the reason. The edges are unit-tested inlite-link.test.ts(172.15/172.16, 172.31/172.32, 100.63/100.64, 100.127/100.128,corp,example.corp,example.com, fc00/fe80/febf/fec0, …).Verification
cargo fmt --all -- --check,cargo clippy --workspace --all-targets -- -D warnings,cargo clippy --workspace --lib --bins -- -D warnings: clean.cargo nextest run --workspace --lib --bins --tests: 806 passed.--run-ignored only, own Postgres 18 / Redis 8 / ClickHouse 26.3 containers): 444 passed, 1 skipped. New:gateway_authz::model_listings_follow_the_key_allow_list,gateway_authz::model_listings_follow_the_owners_roles,console_admin::gateway_info_gives_any_signed_in_user_the_public_gateway_url;openapi_contractcovers the new path.web/:pnpm lint,pnpm test(159 passed; newlite-link.test.ts, Lite cases inapi-keys.test.tsxandsetup.test.tsx),pnpm build(check-i18n parity: 1848 keys), rerun after the private-network change. The Rust side did not change in that commit.🤖 Generated with Claude Code