@heungtae/codex-chat-bridge 0.1.3 → 0.1.5
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 +128 -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)
|
|
@@ -722,6 +744,40 @@ fn map_responses_to_chat_request(request: &Value) -> Result<BridgeRequest> {
|
|
|
722
744
|
}));
|
|
723
745
|
}
|
|
724
746
|
}
|
|
747
|
+
"function_call" => {
|
|
748
|
+
let name = item
|
|
749
|
+
.get("name")
|
|
750
|
+
.and_then(Value::as_str)
|
|
751
|
+
.unwrap_or_default();
|
|
752
|
+
if name.is_empty() {
|
|
753
|
+
warn!("ignoring function_call item with empty name");
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
let call_id = item
|
|
758
|
+
.get("call_id")
|
|
759
|
+
.and_then(Value::as_str)
|
|
760
|
+
.filter(|v| !v.trim().is_empty())
|
|
761
|
+
.map(ToString::to_string)
|
|
762
|
+
.unwrap_or_else(|| format!("call_{}", Uuid::now_v7()));
|
|
763
|
+
let arguments = item
|
|
764
|
+
.get("arguments")
|
|
765
|
+
.map(function_arguments_to_text)
|
|
766
|
+
.unwrap_or_else(|| "{}".to_string());
|
|
767
|
+
|
|
768
|
+
messages.push(json!({
|
|
769
|
+
"role": "assistant",
|
|
770
|
+
"content": "",
|
|
771
|
+
"tool_calls": [{
|
|
772
|
+
"id": call_id,
|
|
773
|
+
"type": "function",
|
|
774
|
+
"function": {
|
|
775
|
+
"name": name,
|
|
776
|
+
"arguments": arguments,
|
|
777
|
+
}
|
|
778
|
+
}]
|
|
779
|
+
}));
|
|
780
|
+
}
|
|
725
781
|
"function_call_output" => {
|
|
726
782
|
let call_id = item
|
|
727
783
|
.get("call_id")
|
|
@@ -773,7 +829,7 @@ fn map_responses_to_chat_request(request: &Value) -> Result<BridgeRequest> {
|
|
|
773
829
|
}
|
|
774
830
|
}
|
|
775
831
|
|
|
776
|
-
let chat_tools = normalize_chat_tools(tools);
|
|
832
|
+
let chat_tools = normalize_chat_tools(tools, drop_tool_types);
|
|
777
833
|
let chat_tool_choice = normalize_tool_choice(tool_choice);
|
|
778
834
|
|
|
779
835
|
let response_id = format!("resp_bridge_{}", Uuid::now_v7());
|
|
@@ -828,11 +884,23 @@ fn function_output_to_text(value: &Value) -> String {
|
|
|
828
884
|
}
|
|
829
885
|
}
|
|
830
886
|
|
|
831
|
-
fn
|
|
887
|
+
fn function_arguments_to_text(value: &Value) -> String {
|
|
888
|
+
match value {
|
|
889
|
+
Value::String(s) => s.clone(),
|
|
890
|
+
other => other.to_string(),
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
fn normalize_chat_tools(tools: Vec<Value>, drop_tool_types: &HashSet<String>) -> Vec<Value> {
|
|
832
895
|
tools
|
|
833
896
|
.into_iter()
|
|
834
897
|
.filter_map(|tool| {
|
|
835
|
-
|
|
898
|
+
let tool_type = tool.get("type").and_then(Value::as_str);
|
|
899
|
+
if tool_type.is_some_and(|t| drop_tool_types.contains(t)) {
|
|
900
|
+
return None;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if tool_type != Some("function") {
|
|
836
904
|
return Some(tool);
|
|
837
905
|
}
|
|
838
906
|
|
|
@@ -958,7 +1026,7 @@ mod tests {
|
|
|
958
1026
|
"parallel_tool_calls": true
|
|
959
1027
|
});
|
|
960
1028
|
|
|
961
|
-
let req = map_responses_to_chat_request(&input).expect("should map");
|
|
1029
|
+
let req = map_responses_to_chat_request(&input, &HashSet::new()).expect("should map");
|
|
962
1030
|
let messages = req
|
|
963
1031
|
.chat_request
|
|
964
1032
|
.get("messages")
|
|
@@ -1011,7 +1079,7 @@ mod tests {
|
|
|
1011
1079
|
#[test]
|
|
1012
1080
|
fn normalize_chat_tools_passes_non_function_tool() {
|
|
1013
1081
|
let tools = vec![json!({"type": "web_search_preview"})];
|
|
1014
|
-
let out = normalize_chat_tools(tools);
|
|
1082
|
+
let out = normalize_chat_tools(tools, &HashSet::new());
|
|
1015
1083
|
assert_eq!(out, vec![json!({"type": "web_search_preview"})]);
|
|
1016
1084
|
}
|
|
1017
1085
|
|
|
@@ -1039,7 +1107,7 @@ mod tests {
|
|
|
1039
1107
|
"tools": []
|
|
1040
1108
|
});
|
|
1041
1109
|
|
|
1042
|
-
let req = map_responses_to_chat_request(&input).expect("should map");
|
|
1110
|
+
let req = map_responses_to_chat_request(&input, &HashSet::new()).expect("should map");
|
|
1043
1111
|
let messages = req
|
|
1044
1112
|
.chat_request
|
|
1045
1113
|
.get("messages")
|
|
@@ -1050,6 +1118,37 @@ mod tests {
|
|
|
1050
1118
|
assert_eq!(messages[0]["tool_call_id"], "call_1");
|
|
1051
1119
|
}
|
|
1052
1120
|
|
|
1121
|
+
#[test]
|
|
1122
|
+
fn map_supports_function_call_to_assistant_tool_call() {
|
|
1123
|
+
let input = json!({
|
|
1124
|
+
"model": "gpt-4.1",
|
|
1125
|
+
"input": [
|
|
1126
|
+
{
|
|
1127
|
+
"type": "function_call",
|
|
1128
|
+
"call_id": "call_1",
|
|
1129
|
+
"name": "get_weather",
|
|
1130
|
+
"arguments": "{\"city\":\"seoul\"}"
|
|
1131
|
+
}
|
|
1132
|
+
],
|
|
1133
|
+
"tools": []
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
let req = map_responses_to_chat_request(&input, &HashSet::new()).expect("should map");
|
|
1137
|
+
let messages = req
|
|
1138
|
+
.chat_request
|
|
1139
|
+
.get("messages")
|
|
1140
|
+
.and_then(Value::as_array)
|
|
1141
|
+
.expect("messages");
|
|
1142
|
+
assert_eq!(messages.len(), 1);
|
|
1143
|
+
assert_eq!(messages[0]["role"], "assistant");
|
|
1144
|
+
assert_eq!(messages[0]["tool_calls"][0]["id"], "call_1");
|
|
1145
|
+
assert_eq!(messages[0]["tool_calls"][0]["type"], "function");
|
|
1146
|
+
assert_eq!(
|
|
1147
|
+
messages[0]["tool_calls"][0]["function"]["name"],
|
|
1148
|
+
"get_weather"
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1053
1152
|
#[test]
|
|
1054
1153
|
fn map_defaults_tool_choice_when_invalid() {
|
|
1055
1154
|
let input = json!({
|
|
@@ -1059,14 +1158,14 @@ mod tests {
|
|
|
1059
1158
|
"tool_choice": 123
|
|
1060
1159
|
});
|
|
1061
1160
|
|
|
1062
|
-
let req = map_responses_to_chat_request(&input).expect("should map");
|
|
1161
|
+
let req = map_responses_to_chat_request(&input, &HashSet::new()).expect("should map");
|
|
1063
1162
|
assert_eq!(req.chat_request["tool_choice"], "auto");
|
|
1064
1163
|
}
|
|
1065
1164
|
|
|
1066
1165
|
#[test]
|
|
1067
1166
|
fn map_requires_input_array() {
|
|
1068
1167
|
let input = json!({"model":"gpt-4.1"});
|
|
1069
|
-
let err = map_responses_to_chat_request(&input).expect_err("must fail");
|
|
1168
|
+
let err = map_responses_to_chat_request(&input, &HashSet::new()).expect_err("must fail");
|
|
1070
1169
|
assert!(err.to_string().contains("missing `input` array"));
|
|
1071
1170
|
}
|
|
1072
1171
|
|
|
@@ -1086,7 +1185,7 @@ mod tests {
|
|
|
1086
1185
|
"input": [{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}],
|
|
1087
1186
|
"tools": []
|
|
1088
1187
|
});
|
|
1089
|
-
let req = map_responses_to_chat_request(&input).expect("ok");
|
|
1188
|
+
let req = map_responses_to_chat_request(&input, &HashSet::new()).expect("ok");
|
|
1090
1189
|
let obj = req.chat_request.as_object().expect("object");
|
|
1091
1190
|
assert!(!obj.contains_key("tools"));
|
|
1092
1191
|
assert!(!obj.contains_key("tool_choice"));
|
|
@@ -1111,7 +1210,7 @@ mod tests {
|
|
|
1111
1210
|
"type": "function",
|
|
1112
1211
|
"function": {"name":"f", "parameters": {"type":"object"}}
|
|
1113
1212
|
})];
|
|
1114
|
-
let out = normalize_chat_tools(tools.clone());
|
|
1213
|
+
let out = normalize_chat_tools(tools.clone(), &HashSet::new());
|
|
1115
1214
|
assert_eq!(out, tools);
|
|
1116
1215
|
}
|
|
1117
1216
|
|
|
@@ -1135,11 +1234,24 @@ mod tests {
|
|
|
1135
1234
|
"input": [{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}],
|
|
1136
1235
|
"tools": []
|
|
1137
1236
|
});
|
|
1138
|
-
let req = map_responses_to_chat_request(&input).expect("ok");
|
|
1237
|
+
let req = map_responses_to_chat_request(&input, &HashSet::new()).expect("ok");
|
|
1139
1238
|
let messages = req.chat_request["messages"].as_array().expect("array");
|
|
1140
1239
|
assert_eq!(messages[0]["role"], "system");
|
|
1141
1240
|
}
|
|
1142
1241
|
|
|
1242
|
+
#[test]
|
|
1243
|
+
fn normalize_chat_tools_drops_configured_tool_types() {
|
|
1244
|
+
let tools = vec![
|
|
1245
|
+
json!({"type": "web_search_preview"}),
|
|
1246
|
+
json!({"type": "function", "name": "f", "parameters": {"type":"object"}}),
|
|
1247
|
+
];
|
|
1248
|
+
let mut drop = HashSet::new();
|
|
1249
|
+
drop.insert("web_search_preview".to_string());
|
|
1250
|
+
let out = normalize_chat_tools(tools, &drop);
|
|
1251
|
+
assert_eq!(out.len(), 1);
|
|
1252
|
+
assert_eq!(out[0]["type"], "function");
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1143
1255
|
#[tokio::test]
|
|
1144
1256
|
async fn stream_emits_output_item_added_before_text_delta() {
|
|
1145
1257
|
let upstream = stream::iter(vec![Ok::<Bytes, reqwest::Error>(Bytes::from(
|
|
@@ -1172,6 +1284,7 @@ mod tests {
|
|
|
1172
1284
|
api_key_env: Some("CLI_API_KEY".to_string()),
|
|
1173
1285
|
server_info: None,
|
|
1174
1286
|
http_shutdown: true,
|
|
1287
|
+
drop_tool_types: vec![],
|
|
1175
1288
|
};
|
|
1176
1289
|
let file = FileConfig {
|
|
1177
1290
|
host: Some("127.0.0.1".to_string()),
|
|
@@ -1180,6 +1293,7 @@ mod tests {
|
|
|
1180
1293
|
api_key_env: Some("FILE_API_KEY".to_string()),
|
|
1181
1294
|
server_info: Some(PathBuf::from("/tmp/server.json")),
|
|
1182
1295
|
http_shutdown: Some(false),
|
|
1296
|
+
drop_tool_types: None,
|
|
1183
1297
|
};
|
|
1184
1298
|
|
|
1185
1299
|
let resolved = resolve_config(args, Some(file));
|
|
@@ -1204,6 +1318,7 @@ mod tests {
|
|
|
1204
1318
|
api_key_env: None,
|
|
1205
1319
|
server_info: None,
|
|
1206
1320
|
http_shutdown: false,
|
|
1321
|
+
drop_tool_types: vec![],
|
|
1207
1322
|
};
|
|
1208
1323
|
|
|
1209
1324
|
let resolved = resolve_config(args, None);
|