Skip to content
Merged
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
90 changes: 90 additions & 0 deletions devlog/_plan/260912_muse_tool_name_alias/000_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Meta Muse 64-char MCP tool-name aliasing (#4410)

## Problem

Meta Muse (https://api.meta.ai/v1, openai-responses adapter) rejects any request
whose function tool name exceeds 64 characters: HTTP 400
'name' must be at most 64 characters, got 66. Real ZCode sessions carry
fully-namespaced MCP names (20 of 93 tools over the limit), so the whole turn
dies before any tool call. Repro: 66-char placeholder name -> 400, 64-char -> 200.
Only the name length matters; schemas, arguments, and message bodies are fine.

## Prior art in this tree

- src/adapters/kiro-wire.ts kiroToolName - deterministic, collision-safe
normalization to ^[a-zA-Z0-9_-]{1,64}$ with a nameMap that restores original
names on the way back. Same shape of problem, different transport.
- src/adapters/openai-responses.ts (~line 2465) - existing Muse-scoped outbound
transform stripMuseSparkUnsupportedWebSearchFields(outBody, parsed.modelId, url),
gated on the api.meta.ai Responses URL. The aliasing hook belongs at the same
seam so no other provider path changes behavior.
- src/responses/tool-name-aliases.ts plus src/responses/namespace-tool-compat.ts
and custom-tool-compat.ts - the existing alias/restore machinery for namespace,
custom-tool, and tool-search wire names. Inbound restore should reuse this
layer rather than inventing a second mapping channel.

## Design

1. Outbound (meta-muse / api.meta.ai Responses only): before the request body
leaves, rewrite every function tool name longer than 64 chars to a
deterministic collision-safe wire name: keep a readable prefix, append a
short stable hash suffix, clamp to 64, sanitize to the safe charset, and
dedupe within the request (same input -> same output across turns).
2. Record the alias map on the turn/request context.
3. Inbound: restore original names in streamed and non-streamed function_call /
tool_call outputs, in tool_choice echo, and in any history items that carry
the aliased name back upstream, using the existing alias-restore machinery.
4. Arguments, user text, and schema property names are never rewritten. Other
providers see zero behavioral change (scope strictly to the meta-muse
baseUrl / provider id).

## Regression coverage

- Unit: alias function - 64 passes through verbatim, 65/66/93-char names map
deterministically, collision-safe, charset-safe.
- Adapter-level: meta-muse outbound request with the issue's 93-tool catalog
fixture sends only <=64-char names; a second provider keeps names verbatim.
- Inbound: tool_call with aliased name restores the original MCP name;
tool_choice round-trips.

## Delivery

- Branch codex/260912-muse-64-tool-alias from dev (aa0dd50864), PR to dev with
full template, close #4410 manually after merge (PRs target dev; GitHub
auto-close only fires on main).
- Implementation and verification delegated to xai/grok-4.6 spawned subagents;
local suite NOT run; pushes use --no-verify; exact-head remote CI is the
passing evidence.

## Audit amendments (grok-4.6 explorer, near-pass — blocking findings folded in)

1. Do NOT copy stripMuseSparkUnsupportedWebSearchFields predicates (contributor-model
+ URL set incl. Zen). Gate the new sibling transform on destination host api.meta.ai
so the default muse-spark-1.3 model is covered; place it at the same call site
(after namespace flattening ~openai-responses.ts:2453, before stringify).
2. Existing alias types cannot carry Map<wireName, originalName>. Add a new sidecar
on AdapterRequest (e.g. convertedMuseToolNameAliases) and a new restore helper in
src/responses/ (e.g. muse-tool-name-alias.ts); wire restore at core.ts sites:
stream payload rewrites 6098-6107 (Muse rewrite BEFORE namespace restore),
block rewrites 6164 / undeclared guard 6150, non-stream 6362-6380, continuation
cache 5102-5104, inspection 5081, and every failover/rebuild refresh of
routed aliases (4808, 4905, 5357, 5474, 5595, 5822, 7270, 7415).
3. Outbound rewrite covers tools[] PLUS history function_call/custom_tool_call names,
tool_choice ({type:function|custom, name} and allowed_tools.tools[].name),
additional_tools, and chat-shaped tool.function.name (use wireToolInnerName).
4. Restore order is load-bearing: Muse hashed->original BEFORE namespace restore and
BEFORE the undeclared-tool guard (continuation turns declare only client originals).
5. Do not import kiro-wire.ts into src/responses/; copy the algorithm into a new
helper. Hash the ORIGINAL name (55-char prefix + _ + 8 hex sha256 = 64), charset
[^a-zA-Z0-9_-] -> _, two-phase claim (pass-through <=64 names claimed first),
salt loop wireName#N on collision, declaration-order processing.
6. Structure docs to update in the same change: structure/transports/responses.md
(alias contract), plus other owners of touched areas (runtime.md,
transports/inventory.md, data-planes/inbound-compat.md, providers/chat-compat.md,
adapters/registry.md as applicable).
7. Tests: unit helper tests/responses/responses-muse-tool-name-alias.test.ts;
adapter outbound (93-tool catalog fixture + second provider unchanged)
tests/providers/muse-tool-name-alias.test.ts; inbound restore/SSE in
tests/responses/ near openai-responses-passthrough/namespace-tool-compat.
New files need entries in BOTH scripts/test-layout/layout.json explicit and
tests/fixtures/test-layout-expected.json. Never put muse-* under tests/responses/.
2 changes: 2 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,7 @@
"muse-passive-quota-observation.test.ts": "providers",
"muse-spark-web-search-compat.test.ts": "providers",
"muse-subscription-usage.test.ts": "providers",
"muse-tool-name-alias.test.ts": "providers",
"namespace-tool-compat.test.ts": "responses",
"native-alias-maintainer-regressions.test.ts": "codex-integration",
"native-claude-code-toggle.test.ts": "codex-integration",
Expand Down Expand Up @@ -1104,6 +1105,7 @@
"responses-inbound-store-default.test.ts": "responses",
"responses-item-id-repair.test.ts": "responses",
"responses-json-events.test.ts": "responses",
"responses-muse-tool-name-alias.test.ts": "responses",
"responses-native-main-refresh.test.ts": "responses",
"responses-opaque-blob-recovery.test.ts": "responses",
"responses-parser-agent-message.test.ts": "responses",
Expand Down
2 changes: 2 additions & 0 deletions src/adapters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export interface AdapterRequest {
convertedRoutedToolSearchNames?: ReadonlySet<string>;
/** Upstream-only aliases for namespace tools flattened in this request. */
convertedRoutedNamespaceToolAliases?: ReadonlyMap<string, { namespace: string; name: string; kind: "function" | "custom" }>;
/** Upstream-only <=64-char aliases for Meta Muse tool names rewritten in this request. */
convertedMuseToolNameAliases?: ReadonlyMap<string, string>;
/** Releases observation of a serialized request body after its final fetch attempt settles. */
releaseBodyObservation?: () => void;
/** Exact reasoning parameter emitted by the adapter, for request-log diagnostics only. */
Expand Down
11 changes: 11 additions & 0 deletions src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { TranslatorBudget } from "../lib/translator-budget";
import { rewriteRoutedCustomToolsForUpstream } from "../responses/custom-tool-compat";
import { rewriteRoutedToolSearchForUpstream } from "../responses/tool-search-compat";
import { rewriteRoutedNamespaceToolsForUpstream } from "../responses/namespace-tool-compat";
import { isMetaAiResponsesDestination, rewriteMuseToolNamesForUpstream } from "../responses/muse-tool-name-alias";
import { openaiResponsesUrl } from "./openai-responses-url";
import { normalizeResponsesCodeMode } from "./responses-code-mode";
import { stripUnicodePropertyPatterns } from "./responses-tool-schema";
Expand Down Expand Up @@ -2370,6 +2371,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
let routedCustomToolRepairNames: Set<string> | undefined;
let convertedRoutedToolSearchNames: Set<string> | undefined;
let convertedRoutedNamespaceToolAliases: Map<string, { namespace: string; name: string; kind: "function" | "custom" }> | undefined;
let convertedMuseToolNameAliases: Map<string, string> | undefined;
const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
let outBody = stripPreviousResponseId(
parsed._rawBody,
Expand Down Expand Up @@ -2463,6 +2465,14 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
outBody = stripOpenAiOnlyWebSearchFields(outBody);
}
outBody = stripMuseSparkUnsupportedWebSearchFields(outBody, parsed.modelId, url);
// Host-only: api.meta.ai rejects function names over 64 chars on every Muse model,
// including default muse-spark-1.3. Do not reuse the contributor/Zen web_search
// predicates. Namespace flattening has already produced the public wire names.
if (isMetaAiResponsesDestination(url)) {
const rewritten = rewriteMuseToolNamesForUpstream(outBody);
outBody = rewritten.body;
convertedMuseToolNameAliases = rewritten.aliases;
}
// Last, so promoted namespace children are also cleared of Codex-private fields.
outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false);
}
Expand Down Expand Up @@ -2571,6 +2581,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
...(routedCustomToolRepairNames ? { routedCustomToolRepairNames } : {}),
...(convertedRoutedToolSearchNames ? { convertedRoutedToolSearchNames } : {}),
...(convertedRoutedNamespaceToolAliases ? { convertedRoutedNamespaceToolAliases } : {}),
...(convertedMuseToolNameAliases ? { convertedMuseToolNameAliases } : {}),
...(tierLog ? { tierLog } : {}),
};
},
Expand Down
Loading
Loading