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
30 changes: 24 additions & 6 deletions crates/tw-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -940,7 +940,16 @@ pub const MSG_CODES: &str = include_str!("../msg-codes.txt");
/// `replaced`,[`OutcomeDetail`] 的 `replaced` 多了可选的 `segments`(换了几段)和
/// `replacement`(换成的文字),回答里的命中 `direction` 是 `response`;[`SecurityCounts`] 多了
/// `content_replaced`。照 47 写的界面不认 `placeholder`、`replace` 这两个动作。
pub const CONTROL_API_VERSION: u32 = 48;
///
/// **49 起 `/live` 的速度是此刻的**:[`LiveView::tokens_per_sec`] 从「最近一分钟跑完的请求」
/// 改成在跑的请求最近 [`LIVE_RATE_WINDOW_MS`] 毫秒的合计生成速度,按流里收到的内容现估;
/// 没有在跑的请求是 null,有请求却都还没开始生成是 0。[`RunningView`] 多了 `tokens_per_sec`
/// (这一个请求此刻的速度)和 `output_tokens`(到目前为止估的输出 token),第一个 token 之前、
/// 非流式的是 null。
pub const CONTROL_API_VERSION: u32 = 49;

/// `/live` 的速度看最近多久(见 [`LiveView::tokens_per_sec`]),毫秒
pub const LIVE_RATE_WINDOW_MS: u64 = 3_000;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
Expand Down Expand Up @@ -1956,7 +1965,7 @@ impl Event {
}
}

/// 此刻的实时读数(`GET /live`):在跑的请求,和最近跑完的请求生成得多快。
/// 此刻的实时读数(`GET /live`):在跑的请求,和它们此刻生成得多快。
///
/// **按事件数出来的,core 里只数一份。**以前菜单栏自己听事件流、自己记在跑的和
/// 跑完的:半路才开始听的那一段要拿 `/in-flight` 对账,丢过事件要重对,而这些
Expand All @@ -1966,10 +1975,13 @@ impl Event {
pub struct LiveView {
/// 开始了、还没有结局的请求,开始得早的在前。和 `/in-flight` 是同一批
pub running: Vec<RunningView>,
/// 最近一分钟跑完的请求平均每秒生成多少 token:每个请求的速度(见
/// `RequestFinished::tokens_per_sec`)按它生成用的时间加权,也就是这些请求
/// 吐出的 token 合计除以生成用的时间合计。**只数有速度的**:非流式的不算。这一分钟里
/// 没有的是空,不是 0
/// 此刻每秒生成多少 token:在跑的请求各自的速度([`RunningView::tokens_per_sec`])加起来。
/// **没有在跑的请求是空**;有请求在跑、却都还在等第一个 token(或者是非流式的)是 0。
///
/// 是估的:流里每到一段正文、推理文字、工具参数就按字数估 token(同输入的估算),再乘上
/// 这家上游这个模型此前实测的比例 —— 每个流式请求结束时,拿上游报的输出 token 数校准
/// 一次。上游在流里报累计数的(Gemini)用它报的。**请求结束后记下的速度
/// (`RequestFinished::tokens_per_sec`)照旧按上游报的实数算**,和这里不是一个数
pub tokens_per_sec: Option<u32>,
}

Expand Down Expand Up @@ -2000,6 +2012,12 @@ pub struct RunningView {
/// 实际接下它的上游:尝试链里接下了的那一跳。**路由报出结论之前没有**
/// (还在等上游的响应头,或者正在故障转移)
pub upstream: Option<String>,
/// 这个请求此刻每秒生成多少 token:最近 [`LIVE_RATE_WINDOW_MS`] 毫秒(刚开口不到这么久
/// 的,从第一个 token 算起)里估出的输出除以这段时间,估法见 [`LiveView::tokens_per_sec`]。
/// 停下来不吐字的时候往下掉到 0。**第一个 token 之前、非流式的没有**
pub tokens_per_sec: Option<u32>,
/// 到目前为止估的输出 token(同上的估法)。**第一个 token 之前、非流式的没有**
pub output_tokens: Option<u64>,
}

/// 此刻还在跑的请求(`GET /in-flight`):每一个到目前为止的事件,和 core 此刻的
Expand Down
18 changes: 14 additions & 4 deletions crates/tw-control/tests/in_flight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,13 @@ async fn live(router: axum::Router) -> serde_json::Value {
serde_json::from_slice(&body).unwrap()
}

/// `/live`:菜单栏要的在跑的请求和生成速率,由 core 数好。
/// `/live`:菜单栏要的在跑的请求和此刻的生成速度,由 core 数好。
#[tokio::test]
async fn live_gives_the_running_requests_and_the_generation_rate() {
let (_d, bus, router) = app();
let idle = live(router.clone()).await;
assert_eq!(idle["running"], serde_json::json!([]));
// 这一分钟里没有跑完的:是空,不是 0
// 没有在跑的:是空,不是 0
assert!(idle["tokens_per_sec"].is_null(), "{idle}");

bus.emit(started(1, "claude-sonnet-5"));
Expand Down Expand Up @@ -199,12 +199,22 @@ async fn live_gives_the_running_requests_and_the_generation_rate() {
answered_model: None,
});
bus.emit(started(2, "gpt-5.5"));
bus.emit(started(3, "gpt-5.5"));
// 3 开口了、记了 40 个;2 还在等第一个 token
bus.meter(3).unwrap().add(40);

let v = live(router).await;
assert_eq!(v["tokens_per_sec"], 50, "{v}");
assert_eq!(v["running"].as_array().unwrap().len(), 1);
// **跑完的不算**,只看在跑的:2 没开口不算速度,合计就是 3 的。3 刚开口,40 个除以
// 开口以来的时间(至少一格 250 毫秒,所以最多 160);跑得慢的机器上读得晚一点,数小一点
let rate = v["running"][1]["tokens_per_sec"].as_u64().unwrap();
assert!((1..=160).contains(&rate), "{v}");
assert_eq!(v["tokens_per_sec"], rate, "{v}");
assert_eq!(v["running"].as_array().unwrap().len(), 2);
assert_eq!(v["running"][1]["output_tokens"], 40, "{v}");
let r = &v["running"][0];
assert_eq!(r["id"], 2);
assert!(r["tokens_per_sec"].is_null(), "{v}");
assert!(r["output_tokens"].is_null(), "{v}");
assert_eq!(r["client"], "我");
assert_eq!(r["client_hint"], "claude-code");
assert_eq!(r["model"], "gpt-5.5");
Expand Down
14 changes: 9 additions & 5 deletions crates/tw-engine/src/facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,15 +235,19 @@ fn part(text: &mut Text, p: &Part) -> u64 {
0
}

/// 攒着的文字:ASCII 字节数和其余字符数分开数。
#[derive(Default)]
struct Text {
/// 攒着的文字:ASCII 字节数和其余字符数分开数。估 token 和输入同一个算法(见
/// [`estimate_tokens`])。
///
/// **一段一段喂、最后一起算**:流里的一段常常只有两三个字符,每段各自取整的话,英文会
/// 被估高好几倍。网关读着回答的流估已经生成了多少,用的也是它
#[derive(Debug, Default, Clone, Copy)]
pub struct Text {
ascii: u64,
other: u64,
}

impl Text {
fn add(&mut self, s: &str) {
pub fn add(&mut self, s: &str) {
self.add_bytes(s.as_bytes());
}

Expand Down Expand Up @@ -275,7 +279,7 @@ impl Text {
let _ = serde_json::to_writer(Count(self), v);
}

fn tokens(&self) -> u64 {
pub fn tokens(&self) -> u64 {
self.ascii.div_ceil(4) + self.other
}
}
Expand Down
4 changes: 3 additions & 1 deletion crates/tw-engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub use engine::{
RuleNotes, SetAction, Target, balance_factors, has_catch_all, is_builtin_group, notes,
order_by, scalar_name,
};
pub use facts::{RequestFacts, TokenParts, estimate_parts, estimate_strings, estimate_tokens};
pub use facts::{
RequestFacts, Text as TextTokens, TokenParts, estimate_parts, estimate_strings, estimate_tokens,
};
pub use time::{LocalTime, Window};
pub use weighted::Member;
Loading
Loading