Skip to content
Closed
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
16 changes: 16 additions & 0 deletions codex-rs/core/src/tools/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,21 @@ impl ToolRouter {
.await
}

/// codex-local H8: some backends (Ollama) flatten `namespace` into the name
/// as `ns.tool`. Split only when the literal name misses but the split hits.
pub(crate) fn unflatten_tool_name(&self, name: ToolName) -> ToolName {
match name.name.split_once('.') {
Some((ns, n))
if name.is_default_namespace()
&& self.registry.tool(&name).is_none()
&& self.registry.tool(&ToolName::namespaced(ns, n)).is_some() =>
{
ToolName::namespaced(ns, n)
}
_ => name,
}
}

#[allow(clippy::too_many_arguments)]
async fn dispatch_tool_call_with_code_mode_result_inner(
&self,
Expand All @@ -361,6 +376,7 @@ impl ToolRouter {
payload,
..
} = call;
let tool_name = self.unflatten_tool_name(tool_name);

// Keep the legacy ToolInvocation.turn field tied to the same request state until handlers migrate.
let turn = Arc::clone(&step_context.turn);
Expand Down
18 changes: 18 additions & 0 deletions codex-rs/core/src/tools/router_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,3 +649,21 @@ fn namespace_function_names(specs: &[ToolSpec], namespace_name: &str) -> Vec<Str
})
.unwrap_or_default()
}

#[tokio::test]
async fn unflatten_tool_name_splits_only_registered_dotted_names() {
let (_, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
let step_context = StepContext::for_test(Arc::clone(&turn));
let router = test_tool_router(
step_context.as_ref(),
Vec::new(),
Vec::new(),
&turn.dynamic_tools,
);

let split = router.unflatten_tool_name(ToolName::plain("functions.exec_command"));
assert_eq!(split, ToolName::namespaced("functions", "exec_command"));
let unknown = ToolName::plain("nope.exec_command");
assert_eq!(router.unflatten_tool_name(unknown.clone()), unknown);
}
2 changes: 1 addition & 1 deletion local/FORK_SURFACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
| H5 | TODO | TODO | TODO |
| H6 | TODO | TODO | TODO |
| H7 | TODO | TODO | TODO |
| H8 | `codex-rs/core/src/tools/router.rs` | When a call misses the registry and the name contains a dot with no namespace, split it into namespace and name. Works around Ollama flattening the Responses `namespace` field into the name. | TODO |
| H8 | `codex-rs/core/src/tools/router.rs` | `ToolRouter::unflatten_tool_name` (+1 call in `dispatch_tool_call_with_code_mode_result_inner`): when a call has no/default namespace, its dotted name misses the registry, and the split at the first dot hits, dispatch under the split `(namespace, name)`. Works around Ollama flattening Responses `namespace` into the name (`multi_agent_v1.spawn_agent`). Unconditional: OpenAI names match `^[a-zA-Z0-9_-]+$`, so stock behaviour is unchanged. History keeps the model's original item. 16 upstream lines in router.rs (tests in router_tests.rs). Re-apply: add the method and call it on `tool_name` right after destructuring `call`. | done |

## H1 re-apply recipe

Expand Down
2 changes: 1 addition & 1 deletion local/fork-surface.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Upstream paths the fork is allowed to modify.
allowlist = ["codex-rs/Cargo.toml", "codex-rs/cli/src/main.rs", "codex-rs/cli/build.rs"]
allowlist = ["codex-rs/Cargo.toml", "codex-rs/cli/src/main.rs", "codex-rs/cli/build.rs", "codex-rs/core/src/tools/router.rs", "codex-rs/core/src/tools/router_tests.rs"]
generated = ["codex-rs/Cargo.lock"]

[budget]
Expand Down