From 8195018e3eabfdab41499eaeba502126ccc222de Mon Sep 17 00:00:00 2001 From: "erik.hauan" Date: Wed, 14 Oct 2026 14:33:35 -0700 Subject: [PATCH 1/2] H8: split Ollama-flattened namespaced tool names on registry miss Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Qp4Gg5UQcjWVPZz3y58bWs --- codex-rs/core/src/tools/router.rs | 16 ++++++++++++++++ codex-rs/core/src/tools/router_tests.rs | 18 ++++++++++++++++++ local/FORK_SURFACE.md | 2 +- local/fork-surface.toml | 2 +- 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index baf39f082af7..c21f732774c8 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -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, @@ -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); diff --git a/codex-rs/core/src/tools/router_tests.rs b/codex-rs/core/src/tools/router_tests.rs index 05a1e2e65a3b..d0b691af800e 100644 --- a/codex-rs/core/src/tools/router_tests.rs +++ b/codex-rs/core/src/tools/router_tests.rs @@ -649,3 +649,21 @@ fn namespace_function_names(specs: &[ToolSpec], namespace_name: &str) -> Vec Date: Wed, 14 Oct 2026 14:33:40 -0700 Subject: [PATCH 2/2] H8: correct line count Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Qp4Gg5UQcjWVPZz3y58bWs --- local/FORK_SURFACE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local/FORK_SURFACE.md b/local/FORK_SURFACE.md index 870b7413ff90..a4a10c659e1d 100644 --- a/local/FORK_SURFACE.md +++ b/local/FORK_SURFACE.md @@ -9,7 +9,7 @@ | H5 | TODO | TODO | TODO | | H6 | TODO | TODO | TODO | | H7 | TODO | TODO | 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. 15 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 | +| 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