mirror of
https://github.com/openai/codex.git
synced 2026-03-03 05:03:20 +00:00
Compare commits
1 Commits
fix/notify
...
cconger/hi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
160b7ce0d3 |
@@ -344,6 +344,10 @@
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"modelVisible": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -9166,6 +9166,10 @@
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"modelVisible": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": true,
|
||||
"modelVisible": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
|
||||
export type DynamicToolSpec = { name: string, description: string, inputSchema: JsonValue, };
|
||||
export type DynamicToolSpec = { name: string, description: string, inputSchema: JsonValue, modelVisible: boolean, };
|
||||
|
||||
@@ -383,6 +383,12 @@ pub struct DynamicToolSpec {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub input_schema: JsonValue,
|
||||
#[serde(default = "default_model_visible")]
|
||||
pub model_visible: bool,
|
||||
}
|
||||
|
||||
fn default_model_visible() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -4256,6 +4262,26 @@ mod tests {
|
||||
assert_eq!(back_to_v2, v2_policy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_tool_spec_defaults_model_visible_when_omitted() {
|
||||
let spec: DynamicToolSpec = serde_json::from_value(json!({
|
||||
"name": "demo_tool",
|
||||
"description": "Demo dynamic tool",
|
||||
"inputSchema": { "type": "object" }
|
||||
}))
|
||||
.expect("dynamic tool spec should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
spec,
|
||||
DynamicToolSpec {
|
||||
name: "demo_tool".to_string(),
|
||||
description: "Demo dynamic tool".to_string(),
|
||||
input_schema: json!({ "type": "object" }),
|
||||
model_visible: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_policy_round_trips_workspace_write_read_only_access() {
|
||||
let readable_root = test_absolute_path();
|
||||
|
||||
@@ -736,6 +736,8 @@ When the client responds to `item/tool/requestUserInput`, the server emits `serv
|
||||
|
||||
`dynamicTools` on `thread/start` and the corresponding `item/tool/call` request/response flow are experimental APIs. To enable them, set `initialize.params.capabilities.experimentalApi = true`.
|
||||
|
||||
Each `dynamicTools` entry may include `modelVisible`. When omitted, it defaults to `true`. Set `modelVisible: false` to keep the tool callable at runtime without advertising it in the model's `tools` list.
|
||||
|
||||
When a dynamic tool is invoked during a turn, the server sends an `item/tool/call` JSON-RPC request to the client:
|
||||
|
||||
```json
|
||||
|
||||
@@ -2159,6 +2159,7 @@ impl CodexMessageProcessor {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
input_schema: tool.input_schema,
|
||||
model_visible: tool.model_visible,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
@@ -7958,6 +7959,7 @@ mod tests {
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({"type": "null"}),
|
||||
model_visible: true,
|
||||
}];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("invalid schema");
|
||||
assert!(err.contains("my_tool"), "unexpected error: {err}");
|
||||
@@ -7970,6 +7972,7 @@ mod tests {
|
||||
description: "test".to_string(),
|
||||
// Missing `type` is common; core sanitizes these to a supported schema.
|
||||
input_schema: json!({"properties": {}}),
|
||||
model_visible: true,
|
||||
}];
|
||||
validate_dynamic_tools(&tools).expect("valid schema");
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ use wiremock::MockServer;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Ensures dynamic tool specs are serialized into the model request payload.
|
||||
/// Ensures only model-visible dynamic tools are serialized into the model request payload.
|
||||
#[tokio::test]
|
||||
async fn thread_start_injects_dynamic_tools_into_model_requests() -> Result<()> {
|
||||
async fn thread_start_only_injects_model_visible_dynamic_tools_into_model_requests() -> Result<()> {
|
||||
let responses = vec![create_final_assistant_message_sse_response("Done")?];
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
|
||||
@@ -57,16 +57,26 @@ async fn thread_start_injects_dynamic_tools_into_model_requests() -> Result<()>
|
||||
"required": ["city"],
|
||||
"additionalProperties": false,
|
||||
});
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
name: "demo_tool".to_string(),
|
||||
description: "Demo dynamic tool".to_string(),
|
||||
let visible_dynamic_tool = DynamicToolSpec {
|
||||
name: "visible_demo_tool".to_string(),
|
||||
description: "Visible dynamic tool".to_string(),
|
||||
input_schema: input_schema.clone(),
|
||||
model_visible: true,
|
||||
};
|
||||
let hidden_dynamic_tool = DynamicToolSpec {
|
||||
name: "hidden_demo_tool".to_string(),
|
||||
description: "Hidden dynamic tool".to_string(),
|
||||
input_schema: input_schema.clone(),
|
||||
model_visible: false,
|
||||
};
|
||||
|
||||
// Thread start injects dynamic tools into the thread's tool registry.
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
dynamic_tools: Some(vec![dynamic_tool.clone()]),
|
||||
dynamic_tools: Some(vec![
|
||||
visible_dynamic_tool.clone(),
|
||||
hidden_dynamic_tool.clone(),
|
||||
]),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
@@ -106,19 +116,20 @@ async fn thread_start_injects_dynamic_tools_into_model_requests() -> Result<()>
|
||||
let body = bodies
|
||||
.first()
|
||||
.context("expected at least one responses request")?;
|
||||
let tool = find_tool(body, &dynamic_tool.name)
|
||||
.context("expected dynamic tool to be injected into request")?;
|
||||
let tool = find_tool(body, &visible_dynamic_tool.name)
|
||||
.context("expected visible dynamic tool to be injected into request")?;
|
||||
|
||||
assert_eq!(
|
||||
tool.get("description"),
|
||||
Some(&Value::String(dynamic_tool.description.clone()))
|
||||
Some(&Value::String(visible_dynamic_tool.description.clone()))
|
||||
);
|
||||
assert_eq!(tool.get("parameters"), Some(&input_schema));
|
||||
assert!(find_tool(body, &hidden_dynamic_tool.name).is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Exercises the full dynamic tool call path (server request, client response, model output).
|
||||
/// Exercises the full dynamic tool call path for a hidden dynamic tool.
|
||||
#[tokio::test]
|
||||
async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Result<()> {
|
||||
let call_id = "dyn-call-1";
|
||||
@@ -154,6 +165,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
"required": ["city"],
|
||||
"additionalProperties": false,
|
||||
}),
|
||||
model_visible: false,
|
||||
};
|
||||
|
||||
let thread_req = mcp
|
||||
@@ -281,6 +293,10 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
.iter()
|
||||
.find_map(|body| function_call_output_payload(body, call_id))
|
||||
.context("expected function_call_output in follow-up request")?;
|
||||
let initial_request_body = bodies
|
||||
.first()
|
||||
.context("expected initial responses request body")?;
|
||||
assert!(find_tool(initial_request_body, tool_name).is_none());
|
||||
let expected_payload = FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
text: "dynamic-ok".to_string(),
|
||||
@@ -326,6 +342,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
|
||||
"required": ["city"],
|
||||
"additionalProperties": false,
|
||||
}),
|
||||
model_visible: true,
|
||||
};
|
||||
|
||||
let thread_req = mcp
|
||||
|
||||
@@ -2446,6 +2446,7 @@ console.log(out.output?.body?.text ?? "");
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
model_visible: true,
|
||||
}])
|
||||
.await;
|
||||
if !turn
|
||||
|
||||
@@ -1865,16 +1865,19 @@ pub(crate) fn build_specs(
|
||||
|
||||
if !dynamic_tools.is_empty() {
|
||||
for tool in dynamic_tools {
|
||||
match dynamic_tool_to_openai_tool(tool) {
|
||||
Ok(converted_tool) => {
|
||||
builder.push_spec(ToolSpec::Function(converted_tool));
|
||||
builder.register_handler(tool.name.clone(), dynamic_tool_handler.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to convert dynamic tool {:?} to OpenAI tool: {e:?}",
|
||||
tool.name
|
||||
);
|
||||
builder.register_handler(tool.name.clone(), dynamic_tool_handler.clone());
|
||||
|
||||
if tool.model_visible {
|
||||
match dynamic_tool_to_openai_tool(tool) {
|
||||
Ok(converted_tool) => {
|
||||
builder.push_spec(ToolSpec::Function(converted_tool));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to convert dynamic tool {:?} to OpenAI tool: {e:?}",
|
||||
tool.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2798,6 +2801,58 @@ mod tests {
|
||||
assert_eq!(mcp_names, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_dynamic_tools_register_handlers_without_prompt_specs() {
|
||||
let config = test_config();
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
features: &features,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
});
|
||||
|
||||
let dynamic_tools = vec![
|
||||
DynamicToolSpec {
|
||||
name: "visible_tool".to_string(),
|
||||
description: "Visible dynamic tool".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
model_visible: true,
|
||||
},
|
||||
DynamicToolSpec {
|
||||
name: "hidden_tool".to_string(),
|
||||
description: "Hidden dynamic tool".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
model_visible: false,
|
||||
},
|
||||
];
|
||||
|
||||
let (tools, registry) = build_specs(&tools_config, None, None, &dynamic_tools).build();
|
||||
|
||||
assert!(
|
||||
tools
|
||||
.iter()
|
||||
.any(|tool| tool_name(&tool.spec) == "visible_tool")
|
||||
);
|
||||
assert!(
|
||||
!tools
|
||||
.iter()
|
||||
.any(|tool| tool_name(&tool.spec) == "hidden_tool")
|
||||
);
|
||||
assert!(registry.handler("visible_tool").is_some());
|
||||
assert!(registry.handler("hidden_tool").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_tool_description_includes_only_codex_apps_connector_names() {
|
||||
let config = test_config();
|
||||
|
||||
@@ -94,6 +94,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
|
||||
"required": ["city"],
|
||||
"properties": { "city": { "type": "string" } }
|
||||
}),
|
||||
model_visible: true,
|
||||
},
|
||||
DynamicToolSpec {
|
||||
name: "weather_lookup".to_string(),
|
||||
@@ -103,6 +104,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
|
||||
"required": ["zip"],
|
||||
"properties": { "zip": { "type": "string" } }
|
||||
}),
|
||||
model_visible: false,
|
||||
},
|
||||
];
|
||||
let dynamic_tools_for_hook = dynamic_tools.clone();
|
||||
|
||||
@@ -4,12 +4,18 @@ use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use ts_rs::TS;
|
||||
|
||||
fn default_model_visible() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DynamicToolSpec {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub input_schema: JsonValue,
|
||||
#[serde(default = "default_model_visible")]
|
||||
pub model_visible: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
@@ -37,3 +43,30 @@ pub enum DynamicToolCallOutputContentItem {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
InputImage { image_url: String },
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn dynamic_tool_spec_defaults_model_visible_when_omitted() {
|
||||
let spec: DynamicToolSpec = serde_json::from_value(json!({
|
||||
"name": "demo_tool",
|
||||
"description": "Demo dynamic tool",
|
||||
"inputSchema": { "type": "object" }
|
||||
}))
|
||||
.expect("dynamic tool spec should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
spec,
|
||||
DynamicToolSpec {
|
||||
name: "demo_tool".to_string(),
|
||||
description: "Demo dynamic tool".to_string(),
|
||||
input_schema: json!({ "type": "object" }),
|
||||
model_visible: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE thread_dynamic_tools
|
||||
ADD COLUMN model_visible BOOLEAN NOT NULL DEFAULT 1;
|
||||
@@ -42,7 +42,7 @@ WHERE id = ?
|
||||
) -> anyhow::Result<Option<Vec<DynamicToolSpec>>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT name, description, input_schema
|
||||
SELECT name, description, input_schema, model_visible
|
||||
FROM thread_dynamic_tools
|
||||
WHERE thread_id = ?
|
||||
ORDER BY position ASC
|
||||
@@ -62,6 +62,7 @@ ORDER BY position ASC
|
||||
name: row.try_get("name")?,
|
||||
description: row.try_get("description")?,
|
||||
input_schema,
|
||||
model_visible: row.try_get("model_visible")?,
|
||||
});
|
||||
}
|
||||
Ok(Some(tools))
|
||||
@@ -304,8 +305,9 @@ INSERT INTO thread_dynamic_tools (
|
||||
position,
|
||||
name,
|
||||
description,
|
||||
input_schema
|
||||
) VALUES (?, ?, ?, ?, ?)
|
||||
input_schema,
|
||||
model_visible
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(thread_id, position) DO NOTHING
|
||||
"#,
|
||||
)
|
||||
@@ -314,6 +316,7 @@ ON CONFLICT(thread_id, position) DO NOTHING
|
||||
.bind(tool.name.as_str())
|
||||
.bind(tool.description.as_str())
|
||||
.bind(input_schema)
|
||||
.bind(tool.model_visible)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user