Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 12 additions & 2 deletions src/components/ImportLinkBuilder.astro
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const t = zh
awsId: "大写字母与数字,16 到 128 个字符。",
headers: "每行一个「名称: 值」,最多 16 行,名称不重复,值为可打印 ASCII;鉴权、Cookie、Host、内容与连接相关的请求头、User-Agent 及 proxy-*、sec-* 开头的请求头不接受。",
concurrency: "1 到 1000 之间的整数。",
limits:
"每行一个 per|measure|max|every|cache_reads,最多 16 行:per 为 minute、hour、day、week 或 month;measure 为 requests、tokens 或 cost;max 为不以 0 开头的整数,cost 为至少 0.01 的美元金额,最多 6 位小数;every 只用于 minute(1 到 60)与 hour(1 到 24);cache_reads 写 y,只用于 tokens;末尾留空的几项可以省略;各行的 per、every、measure 与 cache_reads 不能完全相同。",
models: "模型 ID 以逗号分隔,只含字母、数字与 - _ . : / @ +,最多 64 项。",
modelsOnly: "模型 ID 或含 * ? 的通配规则,以逗号分隔,最多 64 项。",
specs: "每行一个 model|context|max_output|reasoning|image:数值为 1 到 10000000,reasoning 与 image 为 y 或 n,至少填写一项。",
Expand Down Expand Up @@ -75,6 +77,8 @@ const t = zh
awsId: "Uppercase letters and digits, 16 to 128 characters.",
headers: "One Name: Value per line, at most 16, no repeated names, printable ASCII values; authentication, cookie, host, content and connection headers, User-Agent, and names starting with proxy- or sec- are not accepted.",
concurrency: "A whole number from 1 to 1000.",
limits:
"One per|measure|max|every|cache_reads per line, at most 16: per is minute, hour, day, week or month; measure is requests, tokens or cost; max is a whole number without leading zeros, or for cost an amount in USD of at least 0.01 with up to six decimals; every is for minute (1 to 60) and hour (1 to 24) only; cache_reads is y, for tokens only; empty fields at the end may be left out; no two lines with the same per, every, measure and cache_reads.",
models: "Comma-separated model IDs of letters, digits and - _ . : / @ +, at most 64.",
modelsOnly: "Comma-separated model IDs or patterns with * and ?, at most 64.",
specs: "One model|context|max_output|reasoning|image per line: numbers from 1 to 10000000, y or n, at least one field filled.",
Expand All @@ -98,6 +102,7 @@ const ruleOf: Record<string, keyof typeof t.rules> = {
aws_access_key_id: "awsId",
headers: "headers",
concurrency: "concurrency",
limits: "limits",
models: "models",
models_only: "modelsOnly",
specs: "specs",
Expand Down Expand Up @@ -227,6 +232,11 @@ const fieldsetCls = "grid gap-4 border-t border-[var(--color-border)] pt-5";
{L.clientIdentity}
</label>
</div>
<label data-field="limits">
<span class={labelCls}>{L.limits}{t.optional}</span>
<textarea class={input} name="limits" rows="2" spellcheck="false" placeholder="minute|tokens|10000000|5"></textarea>
<span class={hintCls}>{t.perLine} per|measure|max|every|cache_reads</span>
</label>
</fieldset>

<fieldset class={fieldsetCls}>
Expand Down Expand Up @@ -412,7 +422,7 @@ const fieldsetCls = "grid gap-4 border-t border-[var(--color-border)] pt-5";
let v: string;
if (p === "client_identity") v = field.checked ? "on" : "";
else if (p === "headers") v = lines(field.value).join(LF);
else if (p === "specs" || p === "prices") v = lines(field.value).join(";");
else if (p === "specs" || p === "prices" || p === "limits") v = lines(field.value).join(";");
else if (p === "models" || p === "models_only")
v = field.value
.split(",")
Expand Down Expand Up @@ -440,7 +450,7 @@ const fieldsetCls = "grid gap-4 border-t border-[var(--color-border)] pt-5";
if (r.ok) {
code("app").textContent = appLink(r.link);
code("web").textContent = webLink(r.link);
version.textContent = r.link.v2 ? s.needsVersion : s.worksBefore;
version.textContent = r.link.since ? s.needsVersion.replace("{v}", r.link.since) : s.worksBefore;
output.hidden = false;
message.hidden = true;
return;
Expand Down
10 changes: 7 additions & 3 deletions src/components/pages/ImportPage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ const value = "min-w-0 break-all font-mono text-[15px] text-[var(--color-text)]"
>
{t.open}
</button>
<p class="mt-3 text-sm text-[var(--color-muted)]" data-version hidden>{shared.needsVersion}</p>
<p class="mt-3 text-sm text-[var(--color-muted)]" data-version hidden></p>
</div>

<div class="mt-12 border-t border-[var(--color-border)] pt-8">
Expand Down Expand Up @@ -232,7 +232,7 @@ const value = "min-w-0 break-all font-mono text-[15px] text-[var(--color-text)]"
showableParam,
type ImportLink,
} from "~/lib/import-link";
import type { ImportLinkText } from "~/lib/import-link-text";
import { limitPhrase, type ImportLinkText } from "~/lib/import-link-text";

type Strings = ImportLinkText & {
tokenEndpoint: string;
Expand Down Expand Up @@ -364,6 +364,7 @@ const value = "min-w-0 break-all font-mono text-[15px] text-[var(--color-text)]"
if (l.headers.length) advanced.push([L.headers, l.headers.map((h) => `${h.name}: ${h.value}`)]);
if (l.clientIdentity) advanced.push([L.clientIdentity, s.on]);
if (l.concurrency !== undefined) advanced.push([L.concurrency, String(l.concurrency)]);
if (l.limits.length) advanced.push([L.limits, l.limits.map((x) => limitPhrase(s.limit, x))]);
if (l.balance) advanced.push([L.balance, s.balances[l.balance]]);
addGroup(s.groups.advanced, advanced);

Expand Down Expand Up @@ -437,7 +438,10 @@ const value = "min-w-0 break-all font-mono text-[15px] text-[var(--color-text)]"
$("[data-update]").hidden = false;
}
show(l);
$("[data-version]").hidden = !l.v2;
if (l.since) {
$("[data-version]").textContent = s.needsVersion.replace("{v}", l.since);
$("[data-version]").hidden = false;
}
// The only place this page navigates to, built from the checked values
// (and the parameters of a newer version, re-encoded).
const target = appLink(l, result.unknown);
Expand Down
15 changes: 9 additions & 6 deletions src/content/docs-lite/en/failover-and-load-balancing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ ThinkWatch Lite turns each relay key into an upstream and puts several upstreams

## Before you start

- ThinkWatch Lite, [installed](/lite/#install), with a client connected on the Clients page. This guide follows version 2026.10.11.
- ThinkWatch Lite, [installed](/lite/#install), with a client connected on the Clients page. This guide follows version 2026.10.26.
- The base URL and API keys of each relay.

## Steps
Expand All @@ -26,12 +26,15 @@ ThinkWatch Lite turns each relay key into an upstream and puts several upstreams

A strategy only sets the order; every member remains available for failover.

- **When the next upstream is tried.** Before anything has reached the client, the gateway moves on when the upstream cannot be reached or its credential cannot be read; answers 5xx, 429, 401, 403, 402 or 404; answers 400 or 422 with an error about an insufficient balance, a used-up quota or an unavailable model; in a streamed answer, reports an error before the first content, such as an overload; or sends no content within the no-response timeout. Other 4xx responses, and the last member's 4xx other than 429, go back to the client unchanged.
- **When the next upstream is tried.** Before anything has reached the client, the gateway moves on when the upstream cannot be reached or its credential cannot be read; answers 5xx, 401, 403, 402 or 404; answers 400 or 422 with an error about an insufficient balance, a used-up quota or an unavailable model; in a streamed answer, reports an error before the first content, such as an overload; or sends no content within the no-response timeout. A 429 that reports a used-up quota counts as **Quota used up**; any other 429 is a rate limit, which pauses the upstream instead of counting as a failure, and the request moves on or waits (see **Rate limits**). Other 4xx responses, and the last member's 4xx, go back to the client unchanged.
- **Weights and distribution.** In a **Round robin** group, a member's weight, from 1 to 100, is its long-run share of requests: weights 7 and 3 send seven requests in ten to the first, interleaved with the other three. **By ratio** uses the weights alone; **By speed** multiplies each by how much faster than the members' median the upstream starts answering (time to first token, squared, kept between 0.1 and 10), **By reliability** by its success rate over its last 50 outcomes within 30 minutes (squared, at least 0.05), and **By speed and reliability** by both. An upstream with too few samples counts as average, requests that stay with an upstream for their conversation count toward its share, and paused or full members sit out.
- **No response.** An upstream that sends no content for the **No-response timeout** in Settings › Failover, 300 seconds by default (30 to 3,600), is given up on. Only content counts: text, reasoning or a tool call, not keep-alive pings. The clock starts when the request is sent to that upstream, so time spent waiting for a free slot does not count, and starts again with every piece of content. Before anything has reached the client, the request moves to the next upstream and the silent one counts as a failure toward its pause; when none is left, the client receives a timeout error in its own format. Once the answer has started, it ends with an error event the client understands. A whole, non-streamed answer has the same time to arrive. A streamed answer is held back for at most 15 seconds; after that the client receives the response headers and keep-alive comments (Gemini clients get the headers only), so it does not give up while the gateway can still move to the next upstream. The silent upstream's time to first token is recorded as the full wait, which ranks it lower for **Lowest latency** and **By speed**. The request's **Routing** tab marks the attempt **No response · 300 s** with the input the upstream may have billed for it. A model that thinks for a long time without sending anything may need a longer timeout.
- **Concurrency limits.** An upstream with a **Concurrency limit** takes at most that many requests at once. When it is full, a conversation that stays on it waits for a free slot and then moves on, and other requests go straight to the next member; when every member is full, a request waits for the first free slot for up to **Wait for a free slot at most** in Settings › Failover, 30 seconds by default, and then receives a 429 with `Retry-After`, or the error of an earlier attempt when one was sent. The same wait covers a key's per-minute and per-hour usage limits, and the Traffic page shows each skip and the time queued.
- **Pauses.** A failing upstream is paused for as long as Settings › Failover sets: by default 60 seconds after 3 **Consecutive failures**, doubling up to 600; 30 minutes for **Insufficient balance**; until the reset time, or 60 minutes, for **Quota used up**; the wait a rate-limited upstream asks for, up to 60 minutes. A rule with a single upstream is never held back, and when every member is paused they are tried anyway. A request aborted on the Traffic page does not count against its upstream.
- **Sessions and prompt cache.** Within a turn, while the client sends tool results back, requests keep the rule chosen at the start of the turn and the upstream that answered. Across turns, a conversation stays with the upstream that answered last if that answer read or wrote at least 1,024 cached tokens within the last five minutes; moving would rebuild the cache at full price. Otherwise, or when that upstream is paused, the strategy orders the members again, which is when **Round robin** moves on. The **Conversation** line on a request's **Routing** tab shows when a request stayed.
- **No response.** An upstream that sends no content for the **No-response timeout** in Settings › Failover, 300 seconds by default (30 to 3,600), is given up on. Only content counts: text, reasoning or a tool call, not keep-alive pings. The clock starts when the request is sent to that upstream, so time spent waiting before it is sent does not count, and starts again with every piece of content. Before anything has reached the client, the request moves to the next upstream and the silent one counts as a failure toward its pause; when none is left, the client receives a timeout error in its own format. Once the answer has started, it ends with an error event the client understands. A whole, non-streamed answer has the same time to arrive. A streamed answer is held back for at most 15 seconds; after that the client receives the response headers and keep-alive comments (Gemini clients get the headers only), so it does not give up while the gateway can still move to the next upstream. The silent upstream's time to first token is recorded as the full wait, which ranks it lower for **Lowest latency** and **By speed**. The request's **Routing** tab marks the attempt **No response · 300 s** with the input the upstream may have billed for it. A model that thinks for a long time without sending anything may need a longer timeout.
- **Concurrency limits.** An upstream with a **Concurrency limit** takes at most that many requests at once. When it is full, a conversation that stays on it waits for it, and other requests go straight to the next member; how long a request waits is described under **Waiting**.
- **Rate limits.** An upstream that answers 429 is paused for the time it asks for, given in a `retry-after-ms` or `Retry-After` header or in the error body. When it gives none, the pause lasts 5, 10, 20 and 40 seconds, then 60 seconds for each further 429 in a row; any other answer starts the count over. **Rate limit** in Settings › Failover caps the pause, 60 minutes by default. A rate limit is not a failure: it does not count toward **Consecutive failures** or toward the success rate used by **By reliability**. The request that received the 429 goes to the next member when one can take it at once, and otherwise waits for the pause to end; the client sees nothing of the 429. When the pause ends, one request goes first, and the others follow once it gets a normal answer. A rule with a single upstream waits for it as well. The Upstreams page tags a paused upstream **Rate limited · back in 42 s**, and the request's **Routing** tab marks the attempt **429 · Paused 30 s**.
- **Usage limits.** **Usage limits** under **Advanced settings** in the upstream dialog (for a ChatGPT account, below its **Concurrency limit**) keep an upstream within the quota of the relay or plan behind it. Each caps the requests, tokens or cost in USD per minute, hour, day, week or month; a minute or hour limit can also span several, from 1 to 60 minutes or 1 to 24 hours, such as 10,000,000 tokens every 5 minutes. When a minute or hour limit is full, the upstream is passed over like a full one: a conversation that stays on it waits for room, and other requests go to the next member. When a day, week or month limit is used up, requests go to the next member until it resets, at local midnight, on Monday or on the 1st; none waits for it. Every request sent to the upstream counts, except one it answered with 429. Tokens and cost come from the answer: tokens are uncached input, cache writes and output, and **Count cache reads** adds cache reads to a token limit; a free upstream, and a model without a price, count as $0 toward a cost limit. Being at a limit is not a failure. The Upstreams page tags such an upstream **Usage limit reached · back in 1 min 10 s**, or **Daily usage limit reached** (weekly, monthly) for a used-up period, and hovering over the tag or the **Quota / billing** cell lists each limit with its use. After a restart, day, week and month limits are added up again from the request records, which must therefore be kept for at least 1, 7 or 31 days; minute and hour limits start empty.
- **Waiting.** A request waits for upstreams that cannot take it yet, because they are full, rate limited or at a minute or hour usage limit, for at most **Wait at most** in Settings › Failover: 120 seconds by default, from 0 to 600, with 0 for no wait. The same time also covers a key's per-minute and per-hour usage limits. A conversation that stays on an upstream for its prompt cache waits for it when it can recover within that time, and otherwise moves on, where its cache starts over; any other request goes to the next member at once. When no member is left, the request waits for the first one that becomes available. A streamed request receives the response headers and keep-alive comments meanwhile (Gemini clients get the headers only), so the client sees only a slower first token. If none becomes available in time, the client receives a 429 with `Retry-After` set to when the first one will, or the error of an earlier attempt when one was sent. When every member has used up a day, week or month limit, `Retry-After` gives the earliest reset and the response tells the client not to retry before then. On the Traffic page, a waiting request is **Queued**, with the upstreams it waits for in place of the upstream and, on hover, how long it waits at most; its **Routing** tab gives each wait, such as **Queued 12 s**.
- **Pauses.** A failing upstream is paused for as long as Settings › Failover sets: by default 60 seconds after 3 **Consecutive failures**, doubling up to 600; 30 minutes for **Insufficient balance**; until the reset time, or 60 minutes, for **Quota used up**. A rule with a single upstream is never held back by these pauses, and when every member is paused they are tried anyway. A request aborted on the Traffic page does not count against its upstream.
- **Sessions and prompt cache.** Within a turn, while the client sends tool results back, requests keep the rule chosen at the start of the turn and the upstream that answered. Across turns, a conversation stays with the upstream that answered last if that answer read or wrote at least 1,024 cached tokens within the last five minutes; moving would rebuild the cache at full price. Otherwise, or when that upstream is paused after failures, is rate limited past the wait limit, or has used up a day, week or month limit, the strategy orders the members again, which is when **Round robin** moves on. The **Conversation** line on a request's **Routing** tab shows when a request stayed.
- **Same model name.** Every member is asked for the model the client sent, or the name a rule rewrote it to. A member whose model list lacks it is skipped; one without a list is tried, and its 404 moves the request on. A model that a member serves but leaves out of its list can be added with **Add models…** in the member's model list on the Upstreams page, or in the **Models** section of its edit dialog.
- **Falling back to another model.** Add a rule with the condition **Selected upstream** set to the backup upstream, **On match** set to **Continue matching**, and its model in **Change model to** under **Parameter rewrites**. Such a rule is evaluated for each upstream as it is tried, failover included. The changed model no longer hits the cached prompt, and the request is priced by the name sent.

Expand Down
Loading
Loading