Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
46f3d2c
feat(routing): weights on load-balance group members
fylorn Oct 5, 2026
d01b0dc
Load-balance groups can weigh new conversations by speed and reliability
fylorn Oct 5, 2026
dabcf54
Move a stream that is slow to start on to the next upstream
fylorn Oct 5, 2026
800ffc2
Model specs set by hand: an upstream's context window and output limi…
fylorn Oct 5, 2026
4251aca
Cap concurrent requests per upstream
fylorn Oct 5, 2026
54452f4
Hold a gateway key's permit until the answer has been relayed
fylorn Oct 5, 2026
3e7841e
Usage limits per gateway key: requests, tokens and cost per minute, h…
fylorn Oct 5, 2026
d6e7146
Measure upstream speed from sending to first content; share one wait …
fylorn Oct 5, 2026
174f419
Count each turn on a Responses WebSocket as its own request
fylorn Oct 5, 2026
9c81886
Count WebSocket turns like HTTP hops: one wait deadline, speed and su…
fylorn Oct 5, 2026
0620f53
Control-plane protocol 39 for the routing round
fylorn Oct 5, 2026
07a94ea
Keep a working upstream when the only next ones are full
fylorn Oct 5, 2026
d090016
Weigh load-balance by real speed samples only
fylorn Oct 5, 2026
923f841
Charge the sticky leader even when it is full or paused
fylorn Oct 5, 2026
8aa65f3
Refuse key limits whose records or amounts cannot work
fylorn Oct 5, 2026
9255f47
Say what a load-balance weight shares: requests, not new conversations
fylorn Oct 5, 2026
6149de8
Keep the cache_reads refusal test on a valid cost
fylorn Oct 5, 2026
f7415ba
Count only requests that reached an upstream against key limits
fylorn Oct 5, 2026
dcddf6a
Add a moved day, week or month up again from the records
fylorn Oct 5, 2026
9dc5baa
Fail a WebSocket turn when the upstream closes mid-answer
fylorn Oct 5, 2026
e1586a7
Order WebSocket upgrades like HTTP requests
fylorn Oct 5, 2026
adadbf5
Count a Realtime connection's tokens and cost when it closes
fylorn Oct 5, 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
1 change: 1 addition & 0 deletions Cargo.lock

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

14 changes: 12 additions & 2 deletions bin/twcore/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -820,10 +820,16 @@ fn cmd_serve(path: &Path, port: Option<u16>, safe: bool, parent: Option<u32>) ->
state.pricing.clone(),
body_rx,
run_rx,
tw_control::key_limits::settle_hook(&state),
);
if store.is_some() {
if let Some(rec) = &store {
state.set_body_sink(body_tx);
state.set_plugin_sink(run_tx);
// 密钥的用量上限:这一天、这一周、这个月已经用了多少,从记录里加回来。**在第一个
// 请求之前**(网关还没开始听)
tw_control::key_limits::rebuild(&state, rec.lock().await.db());
// 之后一期的开头变了(到了下一期、换了时区),从记录里把新的那一期加起来
tw_control::key_limits::follow(&state, rec.clone());
}

/*
Expand Down Expand Up @@ -1004,6 +1010,8 @@ fn build_store(
pricing: tw_pricing::Shared,
bodies: tokio::sync::mpsc::Receiver<tw_gateway::bodies::BodyRecord>,
runs: tokio::sync::mpsc::Receiver<tw_gateway::plugin::RunRecord>,
// 每记下一行请求,交给网关的密钥用量上限结算(见 `tw_control::key_limits`)
settled: tw_store::SettleHook,
) -> Option<std::sync::Arc<tokio::sync::Mutex<tw_store::Recorder>>> {
let events = bus.subscribe();
let (db, blobs) = match tw_store::open(dir) {
Expand Down Expand Up @@ -1078,7 +1086,9 @@ fn build_store(
let recorder = tw_store::task::spawn(
// 算完价钱往回报一条 —— 见 `Event::RequestPriced`。这里是唯一
// 同时看得见总线和存储层的地方,所以接线在这儿完成。
tw_store::Recorder::new(db, blobs, pricing).reporting_to(bus),
tw_store::Recorder::new(db, blobs, pricing)
.reporting_to(bus)
.settling_to(settled),
events,
rx,
);
Expand Down
30 changes: 30 additions & 0 deletions crates/tw-api/msg-codes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,24 @@ config.edit.unwritable
config.empty_key
config.empty_models_only
config.failover_range
config.key_limit_cache_reads
config.key_limit_cost_too_small
config.key_limit_duplicate
config.key_limit_empty
config.key_limit_not_positive
config.key_limit_retention
config.key_limit_two_measures
config.model_spec_blank_model
config.model_spec_empty
config.model_spec_wildcard
config.model_spec_zero
config.name_collision
config.no_clients
config.plugin.bad_id
config.plugin.duplicate
config.plugin.file
config.plugin.sha256
config.provider_concurrency_range
config.rejected
config.rejected_at
config.remote_port_is_gateway
Expand All @@ -90,6 +102,7 @@ config.schema_too_new
config.secret.empty_name
config.secret.env_missing
config.secret.unterminated
config.slow_start_too_short
config.store.conflict
config.store.missing
config.store.read_failed
Expand Down Expand Up @@ -126,11 +139,15 @@ control.device_code_unavailable
control.device_code_unreadable
control.disabled_key_cannot_be_default
control.dryrun_needs_target
control.group.balance_not_load_balance
control.group.empty
control.group.name_is_upstream
control.group.no_such_upstream
control.group.preferred_not_member
control.group.upstream_twice
control.group.weight_not_load_balance
control.group.weight_not_member
control.group.weight_out_of_range
control.group_in_use
control.header_no_value
control.internal_error
Expand Down Expand Up @@ -228,8 +245,11 @@ engine.compare.no_operator
engine.duplicate_group
engine.duplicate_route
engine.empty_group
engine.group_balance_not_load_balance
engine.group_unknown_upstream
engine.group_upstream_twice
engine.group_weight_not_load_balance
engine.group_weight_out_of_range
engine.no_action
engine.no_match
engine.phase_two_with_to
Expand All @@ -247,6 +267,8 @@ gw.auth.key_invalid
gw.auth.no_key
gw.auth.source_not_allowed
gw.auth.source_not_allowed_hint
gw.busy_all
gw.busy_upstream
gw.chatgpt.token_missing
gw.chatgpt.token_not_json
gw.chatgpt.token_status
Expand All @@ -266,6 +288,12 @@ gw.convert.tools_unsendable
gw.count_tokens.bedrock_upstream
gw.files.unsupported
gw.internal
gw.key_limit.cost_per_period
gw.key_limit.cost_rolling
gw.key_limit.requests_per_period
gw.key_limit.requests_rolling
gw.key_limit.tokens_per_period
gw.key_limit.tokens_rolling
gw.listen.addr_unavailable
gw.listen.bind_failed
gw.listen.denied
Expand Down Expand Up @@ -348,6 +376,7 @@ gw.route.protocol_mismatch
gw.route.rule_failed
gw.route.selected_upstream_missing
gw.route.upstream_missing
gw.slow_start
gw.toolcall.connection_cut
gw.toolcall.response_cut
gw.toolcall.response_withheld
Expand All @@ -374,6 +403,7 @@ gw.ws.connect_failed
gw.ws.proxy_unsupported
gw.ws.send_failed
gw.ws.upstream_broke
gw.ws.upstream_closed
l1.config.bad_url
l1.config.no_host
l1.config.proxy_addr_form
Expand Down
3 changes: 3 additions & 0 deletions crates/tw-api/src/ep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ endpoints! {
UpdateProvider: PUT "/providers/{name}" [name], api::ProviderSave => api::ConfigWritten;
DeleteProvider: DELETE "/providers/{name}" [name], api::BaseVersion => api::ConfigWritten;
ProviderModels: GET "/providers/{name}/models" [name], () => api::ProviderModelsView;
/// 手写一家上游的一个模型的上下文窗口、输出上限,优先于价目表;两项都空就删掉。
/// **不在 `/providers/` 底下**:写死的一段会盖住 `/providers/{name}`
SetModelSpec: PUT "/provider-model-spec", api::ModelSpecSave => api::ConfigWritten;
RefreshProviderModels: POST "/providers/{name}/models/refresh" [name], () => api::ProviderModelsView;
RefreshStaleModels: POST "/models/refresh", () => api::ModelsRefreshing;
CreateProxy: POST "/proxies", api::ProxySave => api::ConfigWritten;
Expand Down
Loading
Loading