@heungtae/codex-chat-bridge 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/main.rs +56 -13
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/package.json
CHANGED
package/src/main.rs
CHANGED
|
@@ -25,6 +25,7 @@ use serde::Serialize;
|
|
|
25
25
|
use serde_json::Value;
|
|
26
26
|
use serde_json::json;
|
|
27
27
|
use std::collections::BTreeMap;
|
|
28
|
+
use std::collections::HashSet;
|
|
28
29
|
use std::fs::File;
|
|
29
30
|
use std::fs::{self};
|
|
30
31
|
use std::io::Write;
|
|
@@ -66,6 +67,14 @@ struct Args {
|
|
|
66
67
|
|
|
67
68
|
#[arg(long)]
|
|
68
69
|
http_shutdown: bool,
|
|
70
|
+
|
|
71
|
+
#[arg(
|
|
72
|
+
long = "drop-tool-type",
|
|
73
|
+
value_name = "TYPE",
|
|
74
|
+
action = clap::ArgAction::Append,
|
|
75
|
+
help = "drop tool entries whose `type` matches this value; can be repeated"
|
|
76
|
+
)]
|
|
77
|
+
drop_tool_types: Vec<String>,
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
#[derive(Debug, Clone, Default, Deserialize)]
|
|
@@ -76,6 +85,7 @@ struct FileConfig {
|
|
|
76
85
|
api_key_env: Option<String>,
|
|
77
86
|
server_info: Option<PathBuf>,
|
|
78
87
|
http_shutdown: Option<bool>,
|
|
88
|
+
drop_tool_types: Option<Vec<String>>,
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
#[derive(Debug, Clone)]
|
|
@@ -86,6 +96,7 @@ struct ResolvedConfig {
|
|
|
86
96
|
api_key_env: String,
|
|
87
97
|
server_info: Option<PathBuf>,
|
|
88
98
|
http_shutdown: bool,
|
|
99
|
+
drop_tool_types: Vec<String>,
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
const DEFAULT_CONFIG_TEMPLATE: &str = r#"# codex-chat-bridge runtime configuration
|
|
@@ -98,6 +109,7 @@ const DEFAULT_CONFIG_TEMPLATE: &str = r#"# codex-chat-bridge runtime configurati
|
|
|
98
109
|
# api_key_env = "OPENAI_API_KEY"
|
|
99
110
|
# server_info = "/tmp/codex-chat-bridge-info.json"
|
|
100
111
|
# http_shutdown = false
|
|
112
|
+
# drop_tool_types = ["web_search", "web_search_preview"]
|
|
101
113
|
"#;
|
|
102
114
|
|
|
103
115
|
#[derive(Clone)]
|
|
@@ -106,6 +118,7 @@ struct AppState {
|
|
|
106
118
|
upstream_url: String,
|
|
107
119
|
api_key: String,
|
|
108
120
|
http_shutdown: bool,
|
|
121
|
+
drop_tool_types: HashSet<String>,
|
|
109
122
|
}
|
|
110
123
|
|
|
111
124
|
#[derive(Serialize)]
|
|
@@ -224,6 +237,7 @@ async fn main() -> Result<()> {
|
|
|
224
237
|
upstream_url: config.upstream_url.clone(),
|
|
225
238
|
api_key,
|
|
226
239
|
http_shutdown: config.http_shutdown,
|
|
240
|
+
drop_tool_types: config.drop_tool_types.into_iter().collect(),
|
|
227
241
|
});
|
|
228
242
|
|
|
229
243
|
let app = Router::new()
|
|
@@ -293,6 +307,9 @@ fn ensure_default_config_file(path: &Path) -> Result<()> {
|
|
|
293
307
|
|
|
294
308
|
fn resolve_config(args: Args, file_config: Option<FileConfig>) -> ResolvedConfig {
|
|
295
309
|
let file_config = file_config.unwrap_or_default();
|
|
310
|
+
let mut drop_tool_types = file_config.drop_tool_types.unwrap_or_default();
|
|
311
|
+
drop_tool_types.extend(args.drop_tool_types);
|
|
312
|
+
drop_tool_types.retain(|v| !v.trim().is_empty());
|
|
296
313
|
|
|
297
314
|
ResolvedConfig {
|
|
298
315
|
host: args
|
|
@@ -310,6 +327,7 @@ fn resolve_config(args: Args, file_config: Option<FileConfig>) -> ResolvedConfig
|
|
|
310
327
|
.unwrap_or_else(|| "OPENAI_API_KEY".to_string()),
|
|
311
328
|
server_info: args.server_info.or(file_config.server_info),
|
|
312
329
|
http_shutdown: args.http_shutdown || file_config.http_shutdown.unwrap_or(false),
|
|
330
|
+
drop_tool_types,
|
|
313
331
|
}
|
|
314
332
|
}
|
|
315
333
|
|
|
@@ -363,7 +381,8 @@ async fn handle_responses(
|
|
|
363
381
|
}
|
|
364
382
|
};
|
|
365
383
|
|
|
366
|
-
let bridge_request = match map_responses_to_chat_request(&request_value)
|
|
384
|
+
let bridge_request = match map_responses_to_chat_request(&request_value, &state.drop_tool_types)
|
|
385
|
+
{
|
|
367
386
|
Ok(v) => v,
|
|
368
387
|
Err(err) => return sse_error_response("invalid_request", &err.to_string()),
|
|
369
388
|
};
|
|
@@ -660,7 +679,10 @@ fn sse_error_response(code: &str, message: &str) -> Response {
|
|
|
660
679
|
.into_response()
|
|
661
680
|
}
|
|
662
681
|
|
|
663
|
-
fn map_responses_to_chat_request(
|
|
682
|
+
fn map_responses_to_chat_request(
|
|
683
|
+
request: &Value,
|
|
684
|
+
drop_tool_types: &HashSet<String>,
|
|
685
|
+
) -> Result<BridgeRequest> {
|
|
664
686
|
let model = request
|
|
665
687
|
.get("model")
|
|
666
688
|
.and_then(Value::as_str)
|
|
@@ -773,7 +795,7 @@ fn map_responses_to_chat_request(request: &Value) -> Result<BridgeRequest> {
|
|
|
773
795
|
}
|
|
774
796
|
}
|
|
775
797
|
|
|
776
|
-
let chat_tools = normalize_chat_tools(tools);
|
|
798
|
+
let chat_tools = normalize_chat_tools(tools, drop_tool_types);
|
|
777
799
|
let chat_tool_choice = normalize_tool_choice(tool_choice);
|
|
778
800
|
|
|
779
801
|
let response_id = format!("resp_bridge_{}", Uuid::now_v7());
|
|
@@ -828,11 +850,16 @@ fn function_output_to_text(value: &Value) -> String {
|
|
|
828
850
|
}
|
|
829
851
|
}
|
|
830
852
|
|
|
831
|
-
fn normalize_chat_tools(tools: Vec<Value>) -> Vec<Value> {
|
|
853
|
+
fn normalize_chat_tools(tools: Vec<Value>, drop_tool_types: &HashSet<String>) -> Vec<Value> {
|
|
832
854
|
tools
|
|
833
855
|
.into_iter()
|
|
834
856
|
.filter_map(|tool| {
|
|
835
|
-
|
|
857
|
+
let tool_type = tool.get("type").and_then(Value::as_str);
|
|
858
|
+
if tool_type.is_some_and(|t| drop_tool_types.contains(t)) {
|
|
859
|
+
return None;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if tool_type != Some("function") {
|
|
836
863
|
return Some(tool);
|
|
837
864
|
}
|
|
838
865
|
|
|
@@ -958,7 +985,7 @@ mod tests {
|
|
|
958
985
|
"parallel_tool_calls": true
|
|
959
986
|
});
|
|
960
987
|
|
|
961
|
-
let req = map_responses_to_chat_request(&input).expect("should map");
|
|
988
|
+
let req = map_responses_to_chat_request(&input, &HashSet::new()).expect("should map");
|
|
962
989
|
let messages = req
|
|
963
990
|
.chat_request
|
|
964
991
|
.get("messages")
|
|
@@ -1011,7 +1038,7 @@ mod tests {
|
|
|
1011
1038
|
#[test]
|
|
1012
1039
|
fn normalize_chat_tools_passes_non_function_tool() {
|
|
1013
1040
|
let tools = vec![json!({"type": "web_search_preview"})];
|
|
1014
|
-
let out = normalize_chat_tools(tools);
|
|
1041
|
+
let out = normalize_chat_tools(tools, &HashSet::new());
|
|
1015
1042
|
assert_eq!(out, vec![json!({"type": "web_search_preview"})]);
|
|
1016
1043
|
}
|
|
1017
1044
|
|
|
@@ -1039,7 +1066,7 @@ mod tests {
|
|
|
1039
1066
|
"tools": []
|
|
1040
1067
|
});
|
|
1041
1068
|
|
|
1042
|
-
let req = map_responses_to_chat_request(&input).expect("should map");
|
|
1069
|
+
let req = map_responses_to_chat_request(&input, &HashSet::new()).expect("should map");
|
|
1043
1070
|
let messages = req
|
|
1044
1071
|
.chat_request
|
|
1045
1072
|
.get("messages")
|
|
@@ -1059,14 +1086,14 @@ mod tests {
|
|
|
1059
1086
|
"tool_choice": 123
|
|
1060
1087
|
});
|
|
1061
1088
|
|
|
1062
|
-
let req = map_responses_to_chat_request(&input).expect("should map");
|
|
1089
|
+
let req = map_responses_to_chat_request(&input, &HashSet::new()).expect("should map");
|
|
1063
1090
|
assert_eq!(req.chat_request["tool_choice"], "auto");
|
|
1064
1091
|
}
|
|
1065
1092
|
|
|
1066
1093
|
#[test]
|
|
1067
1094
|
fn map_requires_input_array() {
|
|
1068
1095
|
let input = json!({"model":"gpt-4.1"});
|
|
1069
|
-
let err = map_responses_to_chat_request(&input).expect_err("must fail");
|
|
1096
|
+
let err = map_responses_to_chat_request(&input, &HashSet::new()).expect_err("must fail");
|
|
1070
1097
|
assert!(err.to_string().contains("missing `input` array"));
|
|
1071
1098
|
}
|
|
1072
1099
|
|
|
@@ -1086,7 +1113,7 @@ mod tests {
|
|
|
1086
1113
|
"input": [{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}],
|
|
1087
1114
|
"tools": []
|
|
1088
1115
|
});
|
|
1089
|
-
let req = map_responses_to_chat_request(&input).expect("ok");
|
|
1116
|
+
let req = map_responses_to_chat_request(&input, &HashSet::new()).expect("ok");
|
|
1090
1117
|
let obj = req.chat_request.as_object().expect("object");
|
|
1091
1118
|
assert!(!obj.contains_key("tools"));
|
|
1092
1119
|
assert!(!obj.contains_key("tool_choice"));
|
|
@@ -1111,7 +1138,7 @@ mod tests {
|
|
|
1111
1138
|
"type": "function",
|
|
1112
1139
|
"function": {"name":"f", "parameters": {"type":"object"}}
|
|
1113
1140
|
})];
|
|
1114
|
-
let out = normalize_chat_tools(tools.clone());
|
|
1141
|
+
let out = normalize_chat_tools(tools.clone(), &HashSet::new());
|
|
1115
1142
|
assert_eq!(out, tools);
|
|
1116
1143
|
}
|
|
1117
1144
|
|
|
@@ -1135,11 +1162,24 @@ mod tests {
|
|
|
1135
1162
|
"input": [{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}],
|
|
1136
1163
|
"tools": []
|
|
1137
1164
|
});
|
|
1138
|
-
let req = map_responses_to_chat_request(&input).expect("ok");
|
|
1165
|
+
let req = map_responses_to_chat_request(&input, &HashSet::new()).expect("ok");
|
|
1139
1166
|
let messages = req.chat_request["messages"].as_array().expect("array");
|
|
1140
1167
|
assert_eq!(messages[0]["role"], "system");
|
|
1141
1168
|
}
|
|
1142
1169
|
|
|
1170
|
+
#[test]
|
|
1171
|
+
fn normalize_chat_tools_drops_configured_tool_types() {
|
|
1172
|
+
let tools = vec![
|
|
1173
|
+
json!({"type": "web_search_preview"}),
|
|
1174
|
+
json!({"type": "function", "name": "f", "parameters": {"type":"object"}}),
|
|
1175
|
+
];
|
|
1176
|
+
let mut drop = HashSet::new();
|
|
1177
|
+
drop.insert("web_search_preview".to_string());
|
|
1178
|
+
let out = normalize_chat_tools(tools, &drop);
|
|
1179
|
+
assert_eq!(out.len(), 1);
|
|
1180
|
+
assert_eq!(out[0]["type"], "function");
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1143
1183
|
#[tokio::test]
|
|
1144
1184
|
async fn stream_emits_output_item_added_before_text_delta() {
|
|
1145
1185
|
let upstream = stream::iter(vec![Ok::<Bytes, reqwest::Error>(Bytes::from(
|
|
@@ -1172,6 +1212,7 @@ mod tests {
|
|
|
1172
1212
|
api_key_env: Some("CLI_API_KEY".to_string()),
|
|
1173
1213
|
server_info: None,
|
|
1174
1214
|
http_shutdown: true,
|
|
1215
|
+
drop_tool_types: vec![],
|
|
1175
1216
|
};
|
|
1176
1217
|
let file = FileConfig {
|
|
1177
1218
|
host: Some("127.0.0.1".to_string()),
|
|
@@ -1180,6 +1221,7 @@ mod tests {
|
|
|
1180
1221
|
api_key_env: Some("FILE_API_KEY".to_string()),
|
|
1181
1222
|
server_info: Some(PathBuf::from("/tmp/server.json")),
|
|
1182
1223
|
http_shutdown: Some(false),
|
|
1224
|
+
drop_tool_types: None,
|
|
1183
1225
|
};
|
|
1184
1226
|
|
|
1185
1227
|
let resolved = resolve_config(args, Some(file));
|
|
@@ -1204,6 +1246,7 @@ mod tests {
|
|
|
1204
1246
|
api_key_env: None,
|
|
1205
1247
|
server_info: None,
|
|
1206
1248
|
http_shutdown: false,
|
|
1249
|
+
drop_tool_types: vec![],
|
|
1207
1250
|
};
|
|
1208
1251
|
|
|
1209
1252
|
let resolved = resolve_config(args, None);
|