@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 CHANGED
@@ -231,7 +231,7 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
231
231
 
232
232
  [[package]]
233
233
  name = "codex-chat-bridge"
234
- version = "0.1.3"
234
+ version = "0.1.5"
235
235
  dependencies = [
236
236
  "anyhow",
237
237
  "async-stream",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "codex-chat-bridge"
3
- version = "0.1.3"
3
+ version = "0.1.5"
4
4
  edition = "2024"
5
5
  license = "Apache-2.0"
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heungtae/codex-chat-bridge",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Responses-to-chat/completions bridge for Codex workflows",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",
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(request: &Value) -> Result<BridgeRequest> {
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 normalize_chat_tools(tools: Vec<Value>) -> Vec<Value> {
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
- if tool.get("type").and_then(Value::as_str) != Some("function") {
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);