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: 1 addition & 0 deletions crates/tw-dialect/src/anthropic/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ pub fn encode_request(r: &Request, t: &Target, dropped: &mut Dropped) -> Value {
(r.seed.is_some(), Feature::Seed),
(r.presence_penalty.is_some(), Feature::PresencePenalty),
(r.frequency_penalty.is_some(), Feature::FrequencyPenalty),
(r.verbosity.is_some(), Feature::Verbosity),
] {
if present {
dropped.feature(f);
Expand Down
3 changes: 3 additions & 0 deletions crates/tw-dialect/src/bedrock/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ pub fn encode_request(r: &Request, t: &Target, dropped: &mut Dropped) -> Value {
if r.format.is_some() {
dropped.feature(Feature::Format);
}
if r.verbosity.is_some() {
dropped.feature(Feature::Verbosity);
}

Value::Object(out)
}
Expand Down
14 changes: 13 additions & 1 deletion crates/tw-dialect/src/caller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ fn chat(c: &mut Collect, v: &Value) {

/// 见 `responses::request::decode_request`:`input` 是一个字符串时整个是用户的话;
/// 是数组时看 `message`(`system`、`developer`、`assistant` 之外的角色,没写的算
/// `user`)和两种工具结果
/// `user`)、别的代理发来的 `agent_message` 和两种工具结果
fn responses(c: &mut Collect, v: &Value) {
let Some(input) = v.get("input") else {
return;
Expand Down Expand Up @@ -351,6 +351,18 @@ fn responses_item(c: &mut Collect, item: &Value) {
c.key("content", |c| responses_content(c, content, false));
}
}
// 只认 `input_text`:`encrypted_content` 只有 OpenAI 读得懂,解码时丢掉了
"agent_message" => {
if let Some(Value::Array(parts)) = item.get("content") {
c.key("content", |c| {
for (i, p) in parts.iter().enumerate() {
if str_of(p, "type") == Some("input_text") {
c.index(i, |c| c.field(p, "text", false));
}
}
});
}
}
"function_call_output" | "custom_tool_call_output" => {
if let Some(output) = item.get("output") {
c.key("output", |c| responses_content(c, output, true));
Expand Down
16 changes: 15 additions & 1 deletion crates/tw-dialect/src/chat/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ pub fn decode_request(
_ => None,
};

if has(v, "verbosity") {
r.verbosity = str_of(v, "verbosity").and_then(Verbosity::parse);
if r.verbosity.is_none() {
dropped.path("verbosity");
}
}

if u64_of(v, "n").is_some_and(|n| n > 1) {
dropped.path("n");
}
Expand All @@ -194,7 +201,6 @@ pub fn decode_request(
"prediction",
"audio",
"web_search_options",
"verbosity",
"moderation",
"functions",
"function_call",
Expand Down Expand Up @@ -397,6 +403,14 @@ pub fn encode_request(r: &Request, t: &Target, dropped: &mut Dropped) -> Value {
None => {}
}

match r.verbosity {
Some(x) if Verbosity::understood_by(&r.model) => {
out.insert("verbosity".into(), json!(x.as_str()));
}
Some(_) => dropped.feature(Feature::Verbosity),
None => {}
}

match &r.format {
Some(Format::JsonObject) => {
out.insert("response_format".into(), json!({ "type": "json_object" }));
Expand Down
6 changes: 6 additions & 0 deletions crates/tw-dialect/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,11 @@ impl Session {
self.shape.namespaced.get(name)
}

/// 这个工具是 Responses 客户端自己执行的工具搜索:调用写回 `tool_search_call`
pub(crate) fn is_tool_search(&self, name: &str) -> bool {
self.shape.tool_search.as_deref() == Some(name)
}

/// 上游的整包响应 → 客户端的整包响应。上游返回的不是 JSON 时是 `None`
pub fn response(&self, body: &[u8]) -> Option<Vec<u8>> {
let v: Value = serde_json::from_slice(body).ok()?;
Expand Down Expand Up @@ -367,6 +372,7 @@ impl Session {
namespaced: HashMap::new(),
include_usage: false,
gemini_sse: true,
tool_search: None,
},
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/tw-dialect/src/gemini/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,9 @@ pub fn encode_request(r: &Request, _t: &Target, dropped: &mut Dropped) -> Value
Some(_) => dropped.feature(Feature::Reasoning),
None => {}
}
if r.verbosity.is_some() {
dropped.feature(Feature::Verbosity);
}
if !g.is_empty() {
out.insert("generationConfig".into(), Value::Object(g));
}
Expand Down
50 changes: 50 additions & 0 deletions crates/tw-dialect/src/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ pub struct Request {
pub frequency_penalty: Option<f64>,
pub reasoning: Option<Reasoning>,
pub format: Option<Format>,
/// 回答写多写少(OpenAI GPT-5 系列的 `verbosity`)
pub verbosity: Option<Verbosity>,
pub stream: bool,
/// 提示缓存的断点,按出现顺序
pub cache: Vec<CachePoint>,
Expand Down Expand Up @@ -147,6 +149,9 @@ pub struct ClientShape {
pub include_usage: bool,
/// Gemini 客户端要 SSE(`alt=sse`);否则流是一个逐步写出的 JSON 数组
pub gemini_sse: bool,
/// Responses 客户端自己执行的工具搜索(Codex 的 `tool_search`,`execution: client`)
/// 转成的函数工具叫什么。上游调用它时,写回去的是 `tool_search_call`,不是函数调用
pub tool_search: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -339,6 +344,47 @@ pub enum Effort {
Max,
}

/// 回答写多写少。**只有 OpenAI 的 GPT-5 系列认**:Chat 的 `verbosity`、Responses 的
/// `text.verbosity`。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verbosity {
Low,
Medium,
High,
}

impl Verbosity {
pub fn parse(s: &str) -> Option<Verbosity> {
Some(match s {
"low" => Verbosity::Low,
"medium" => Verbosity::Medium,
"high" => Verbosity::High,
_ => return None,
})
}

pub fn as_str(self) -> &'static str {
match self {
Verbosity::Low => "low",
Verbosity::Medium => "medium",
Verbosity::High => "high",
}
}

/// 上游的这个模型认不认 `verbosity`:OpenAI 的 GPT-5 及以后(`gpt-5.4`、`openai/gpt-5`)。
///
/// **按模型名判断,不按格式**:说 Chat 格式的绝大多数是别家的模型,不认的参数有的
/// 忽略、有的直接 400;GPT-4.1 这种 OpenAI 自己的老模型也是 400
pub fn understood_by(model: &str) -> bool {
let model = model.to_ascii_lowercase();
let Some((_, rest)) = model.split_once("gpt-") else {
return false;
};
let major: String = rest.chars().take_while(char::is_ascii_digit).collect();
major.parse::<u32>().is_ok_and(|m| m >= 5)
}
}

#[derive(Debug, Clone, PartialEq)]
pub enum Format {
JsonObject,
Expand Down Expand Up @@ -585,6 +631,8 @@ pub enum Feature {
FreeformFormat,
/// 提示缓存的断点,目标模型不认
Cache,
/// 回答写多写少,目标模型不认(见 [`Verbosity::understood_by`])
Verbosity,
}

impl Feature {
Expand Down Expand Up @@ -643,6 +691,8 @@ impl Feature {
(FreeformFormat, _) => "tools.custom.format",
(Cache, Bedrock) => "cachePoint",
(Cache, _) => "cache_control",
(Feature::Verbosity, Responses) => "text.verbosity",
(Feature::Verbosity, _) => "verbosity",
}
}
}
Expand Down
Loading
Loading