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
1 change: 0 additions & 1 deletion crates/tw-api/msg-codes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@ control.signin_gone
control.signin_response_unusable
control.store_off
control.store_unavailable
control.unknown_probe_class
control.upstream_in_use
control.upstream_not_found
control.user_empty
Expand Down
37 changes: 16 additions & 21 deletions crates/tw-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,10 +400,8 @@ slug_enum! {
pub enum ProbeMode {
/// 网关自己答
Intercept = "intercept",
/// 照常走路由
Route = "route",
/// 原样转发
Passthrough = "passthrough",
/// 照常走路由,带着类别标记
Forward = "forward",
}
}

Expand Down Expand Up @@ -717,7 +715,13 @@ pub const MSG_CODES: &str = include_str!("../msg-codes.txt");
/// 不给网页调的 `POST /plugins/confirmed`、`PUT /plugins/{id}/confirmed`、
/// `POST /plugins/{id}/approve/confirmed`。只改数据、停用、删、排顺序,装、打开、改、批准不碰
/// 工具调用的插件,都不用点头 —— `CreatePlugin` 和 `ApprovePluginFile` 因此给网页调了。
pub const CONTROL_API_VERSION: u32 = 35;
///
/// **36 起辅助请求只有两档**:[`ProbeMode`] 是 `intercept` / `forward`,`route` 和
/// `passthrough` 合成了 `forward`(照常走规则,带着类别标记,`when.intent` 能命中)。
/// [`DryRunOutcome`] 删了 `passthrough`:转发的那几类照常求值规则。[`RouteSave`] 的
/// `route_probes` 删了,消息码 `control.unknown_probe_class` 跟着删。配置里
/// `client_probes` 的取值同样只剩 `intercept` / `forward`。
pub const CONTROL_API_VERSION: u32 = 36;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
Expand Down Expand Up @@ -1738,15 +1742,14 @@ pub struct ProxyFault {

/// 一类客户端辅助请求的处置。
///
/// **这一段以前在界面上完全不存在,而它的缺席是连锁的**:路由条件
/// `when.intent` 只有在对应那一类被配成 `route` 时才可能命中,所以
/// 界面上那些写了 `intent` 的规则永远不会生效,而用户无从知道为什么。
/// 路由条件 `when.intent` 只对 `forward` 的类别命中 —— `intercept` 的一个字节都不出
/// 本机,到不了规则那一层。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct ProbeView {
/// `health_check` / `warmup` / `titling` / `topic_detect` / `suggestion`
pub id: ProbeClass,
/// `intercept` / `route` / `passthrough`
/// `intercept` / `forward`
pub mode: ProbeMode,
}

Expand Down Expand Up @@ -2904,12 +2907,6 @@ pub struct RouteSave {
/// 不给就不动密钥的选择
#[serde(default, skip_serializing_if = "Option::is_none")]
pub keys: Option<Vec<String>>,
/// 和路由一起写入:把这几类客户端辅助请求设为「交给路由」。
///
/// **规则里的辅助请求条件只对交给路由的类别生效** —— 其余类别的请求在
/// 进路由之前就被本地应答或原样放行,不带类别标记,那个条件永远不满足
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub route_probes: Vec<String>,
}

/// 删除一条路由。
Expand Down Expand Up @@ -4416,8 +4413,6 @@ slug_enum! {
Unavailable = "unavailable",
/// 客户端的辅助请求,网关自己答
Intercepted = "intercepted",
/// 客户端的辅助请求,原样转发
Passthrough = "passthrough",
}
}

Expand All @@ -4434,11 +4429,11 @@ pub struct DryRunResult {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strategy: Option<GroupKind>,
/// `route` | `deny` | `no_match` | `unavailable`(选中的上游都服务不了,
/// 见 `skipped`)| `intercepted` | `passthrough`
/// 见 `skipped`)| `intercepted`
///
/// **后两个说的是这个请求压根没到规则那一层。**客户端自己发的辅助
/// 请求先过 `client_probes`:本地应答的一个字节都不出本机,原样放行的
/// 直接转发 —— 两种情况下 `trace` 都是空的,因为确实一条规则都没求值。
/// **`intercepted` 说的是这个请求压根没到规则那一层。**客户端自己发的辅助
/// 请求先过 `client_probes`:本地应答的一个字节都不出本机,`trace` 是空的,
/// 因为确实一条规则都没求值。转发的那几类照常走规则,和普通请求一样。
pub outcome: DryRunOutcome,
/// 命中的规则名
pub rule: Option<String>,
Expand Down
52 changes: 27 additions & 25 deletions crates/tw-config/src/probes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
//! 省钱,那是把一个功能关掉了。
//!
//! 所以默认只拦 A 类。
//!
//! **只有两档:本地应答,或者转发。**转发的请求照常走路由规则,并且带着
//! 它是哪一类的标记,所以规则里写了 `when.intent` 就生效。以前还有一档
//! 「转发但不带标记」,和带标记的那档只差在 `intent` 条件能不能命中 ——
//! 两档在界面上看着是两种去向,实际走的是同一套规则,只会让人以为其中
//! 一档绕开了路由。

use serde::{Deserialize, Serialize};

Expand All @@ -20,21 +26,17 @@ use serde::{Deserialize, Serialize};
pub enum ProbeAction {
/// 本地应答,一个字节都不发给上游
Intercept,
/// 原样放行
Passthrough,
/// 交给路由规则。**前提是你手里真有一个更便宜的地方** ——
/// 这类请求本来就走客户端的小模型档,在同一个上游内部已经没有更
/// 便宜的可换了。
Route,
/// 照常走路由规则,带着类别标记让 `when.intent` 能命中。规则里没写
/// 这个条件时,它和一个普通请求走的是同一条路
Forward,
}
impl ProbeAction {
/// 写进 YAML 的那个词。**界面写回的是它,不是中文标签** ——
/// 写中文的话下一次加载会因为不是合法取值整份被拒。
pub fn slug(&self) -> &'static str {
match self {
ProbeAction::Intercept => "intercept",
ProbeAction::Passthrough => "passthrough",
ProbeAction::Route => "route",
ProbeAction::Forward => "forward",
}
}
}
Expand All @@ -59,7 +61,7 @@ impl ClientProbes {

/// 五种辅助请求各自怎么处理。
///
/// **默认值的分布本身就是那条判据**:A 类拦、B 类放行。改这个默认值
/// **默认值的分布本身就是那条判据**:A 类拦、B 类转发。改这个默认值
/// 之前先回答「拦掉之后用户会不会少一样东西」。
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
Expand All @@ -70,32 +72,32 @@ pub struct ClientProbes {
/// A 类。正文恰好是 `Warmup`
#[serde(default = "intercept")]
pub warmup: ProbeAction,
/// **B 类,默认放行。**拦了的话每个会话都叫同一个名字。
#[serde(default = "passthrough")]
/// **B 类,默认转发。**拦了的话每个会话都叫同一个名字。
#[serde(default = "forward")]
pub titling: ProbeAction,
/// B 类
#[serde(default = "passthrough")]
#[serde(default = "forward")]
pub topic_detect: ProbeAction,
/// B 类
#[serde(default = "passthrough")]
#[serde(default = "forward")]
pub suggestion: ProbeAction,
}

fn intercept() -> ProbeAction {
ProbeAction::Intercept
}
fn passthrough() -> ProbeAction {
ProbeAction::Passthrough
fn forward() -> ProbeAction {
ProbeAction::Forward
}

impl Default for ClientProbes {
fn default() -> Self {
Self {
health_check: ProbeAction::Intercept,
warmup: ProbeAction::Intercept,
titling: ProbeAction::Passthrough,
topic_detect: ProbeAction::Passthrough,
suggestion: ProbeAction::Passthrough,
titling: ProbeAction::Forward,
topic_detect: ProbeAction::Forward,
suggestion: ProbeAction::Forward,
}
}
}
Expand All @@ -111,24 +113,24 @@ mod tests {
let d = ClientProbes::default();
assert_eq!(d.health_check, ProbeAction::Intercept);
assert_eq!(d.warmup, ProbeAction::Intercept);
assert_eq!(d.titling, ProbeAction::Passthrough);
assert_eq!(d.topic_detect, ProbeAction::Passthrough);
assert_eq!(d.suggestion, ProbeAction::Passthrough);
assert_eq!(d.titling, ProbeAction::Forward);
assert_eq!(d.topic_detect, ProbeAction::Forward);
assert_eq!(d.suggestion, ProbeAction::Forward);
}

#[test]
fn only_the_key_you_wrote_moves_the_rest_stay_default() {
// 配置文件里写一行不该把另外四行也重置掉。
let c: ClientProbes = serde_yaml_ng::from_str("titling: route").unwrap();
assert_eq!(c.titling, ProbeAction::Route);
let c: ClientProbes = serde_yaml_ng::from_str("titling: intercept").unwrap();
assert_eq!(c.titling, ProbeAction::Intercept);
assert_eq!(c.health_check, ProbeAction::Intercept);
assert_eq!(c.suggestion, ProbeAction::Passthrough);
assert_eq!(c.suggestion, ProbeAction::Forward);
}

#[test]
fn a_misspelled_probe_name_is_an_error() {
// `warmup` 写成 `warm_up` 被静默忽略的话,用户会以为预热请求
// 已经被拦住了,而它一直在计费。
assert!(serde_yaml_ng::from_str::<ClientProbes>("warm_up: route").is_err());
assert!(serde_yaml_ng::from_str::<ClientProbes>("warm_up: forward").is_err());
}
}
3 changes: 1 addition & 2 deletions crates/tw-config/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,7 @@ impl From<ProbeAction> for tw_api::ProbeMode {
fn from(a: ProbeAction) -> Self {
match a {
ProbeAction::Intercept => Self::Intercept,
ProbeAction::Route => Self::Route,
ProbeAction::Passthrough => Self::Passthrough,
ProbeAction::Forward => Self::Forward,
}
}
}
23 changes: 10 additions & 13 deletions crates/tw-config/tests/manual/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -889,29 +889,26 @@ pub fn sections() -> Vec<Section> {
row(
"titling",
Kind::Enum(probe_actions),
Def::Is("passthrough"),
Def::Is("forward"),
t(
"Requests that name a session. Passed through by default: intercepting them gives every session the same title.",
"为会话起标题的请求。默认放行:拦下后所有会话都会是同一个标题。",
"Requests that name a session. Forwarded by default: intercepting them gives every session the same title.",
"为会话起标题的请求。默认转发:拦下后所有会话都会是同一个标题。",
),
),
row(
"topic_detect",
Kind::Enum(probe_actions),
Def::Is("passthrough"),
Def::Is("forward"),
t(
"Topic detection. Passed through by default.",
"话题检测。默认放行。",
"Topic detection. Forwarded by default.",
"话题检测。默认转发。",
),
),
row(
"suggestion",
Kind::Enum(probe_actions),
Def::Is("passthrough"),
t(
"Suggestions. Passed through by default.",
"建议。默认放行。",
),
Def::Is("forward"),
t("Suggestions. Forwarded by default.", "建议。默认转发。"),
),
],
},
Expand Down Expand Up @@ -1411,8 +1408,8 @@ pub fn sections() -> Vec<Section> {
Kind::OneOrMany,
Def::Unset,
t(
"A client helper request: `assistant_internal` for any of them, or one class (`titling`). Only classes set to `route` in `client_probes` reach routing.",
"客户端的辅助请求:`assistant_internal` 表示任意一类,也可以写具体的一类(`titling`)。只有在 `client_probes` 中设为 `route` 的类别才会进入路由。",
"A client helper request: `assistant_internal` for any of them, or one class (`titling`). Classes set to `intercept` in `client_probes` are answered locally and never reach routing.",
"客户端的辅助请求:`assistant_internal` 表示任意一类,也可以写具体的一类(`titling`)。在 `client_probes` 中设为 `intercept` 的类别由本地应答,不会进入路由。",
),
),
row(
Expand Down
5 changes: 2 additions & 3 deletions crates/tw-control/src/dryrun.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,8 @@ pub async fn dry_run(
.find(|(class, _)| class.slug() == f.intent)
.and_then(|(_, action)| match action {
tw_config::ProbeAction::Intercept => Some(DryRunOutcome::Intercepted),
tw_config::ProbeAction::Passthrough => Some(DryRunOutcome::Passthrough),
// 交给路由的那些照常往下走,和普通请求一样
tw_config::ProbeAction::Route => None,
// 转发的那些照常往下走,和普通请求一样
tw_config::ProbeAction::Forward => None,
});

// 每条规则的下场。**先走一遍这个,再问结果** —— 顺序反过来的话,
Expand Down
34 changes: 7 additions & 27 deletions crates/tw-control/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ async fn create_route(
if cfg.engine().routes().iter().any(|r| r.name == set.name) {
return Err(name_taken("route", &set.name));
}
let mut out = edit::upsert(text, edit::ROUTES, None, &mapping(&set)?)?;
if let Some(keys) = &req.keys {
out = assign_keys(&out, cfg, None, &set.name, keys)?;
let out = edit::upsert(text, edit::ROUTES, None, &mapping(&set)?)?;
match &req.keys {
Some(keys) => assign_keys(&out, cfg, None, &set.name, keys),
None => Ok(out),
}
route_probes(&out, &req.route_probes)
})
.await
.map_err(apply_fail)?;
Expand Down Expand Up @@ -94,10 +94,10 @@ async fn update_route(
// 的密钥指向一条不存在的路由,会被校验拒掉
out = refs::rename_route(&out, cfg, &name, &set.name)?;
}
if let Some(keys) = &req.keys {
out = assign_keys(&out, cfg, Some(&name), &set.name, keys)?;
match &req.keys {
Some(keys) => assign_keys(&out, cfg, Some(&name), &set.name, keys),
None => Ok(out),
}
route_probes(&out, &req.route_probes)
})
.await
.map_err(apply_fail)?;
Expand Down Expand Up @@ -224,26 +224,6 @@ fn assign_keys(
Ok(out)
}

/// 把这几类辅助请求设为「交给路由」。
fn route_probes(text: &str, probes: &[String]) -> Result<String, ApplyError> {
let mut out = text.to_string();
for p in probes {
// 总称不是一个可以单独设置的类别
if p == "assistant_internal" || !INTENTS.contains(&p.as_str()) {
return Err(invalid(msg!(
"control.unknown_probe_class", class = p =>
"there is no auxiliary-request class `{class}`"
)));
}
out = edit::set(
&out,
&[Step::key("client_probes"), Step::key(p.as_str())],
Some(&Value::String("route".to_string())),
)?;
}
Ok(out)
}

fn route_of(client: usize) -> [Step; 3] {
[
Step::key("clients"),
Expand Down
Loading
Loading