#17294

fix(tools): Read shim rewrites filePath -> file_path for every client, breaking Kilo/OpenCode on /v1/messages

Open70nyITopened 2d ago

OmniRoute Version

3.8.52 (npm global)

Installation Method

npm (npm i -g omniroute), bare-metal Linux

Operating System

Linux (Ubuntu)

Node.js Version

v22.23.1

Provider(s) Involved

kiro and ollama-cloud

Model(s) Involved

claude-opus-5.5, deepseek-v4.1-flash:1017

Client Tool

Kilo Code 7.8.15 (VS Code extension and CLI), using POST /v1/messages (sourceFormat: claude, targetFormat: openai)

Description

Since upgrading to 3.8.52, every read tool call made through OmniRoute comes back with the argument renamed from filePath to file_path. Kilo declares filePath as the required field, so it rejects the call with:

The read tool was called with invalid arguments: ["filePath"]: is missing and is required.

The model then retries, producing an endless tool-call loop. This happens with every model, including real Claude models (claude-opus-5.5), not just DeepSeek.

Kilo is not the only affected client: any Anthropic-format client that does not use Claude Code's parameter names (Kilo, OpenCode, Cline, etc.) is broken by this.

Steps to Reproduce

  1. Point Kilo (or any OpenCode-style client) at OmniRoute POST /v1/messages.

  2. Send a request whose tools[] declares:

    {
      "name": "read",
      "input_schema": {
        "type": "object",
        "properties": { "filePath": { "type": "string" } },
        "required": ["filePath"]
      }
    }
  3. Ask the model to read a file.

  4. Observe the tool call that comes back:

    content_block_start: {"type":"tool_use","name":"Read","input":{}}
    input_json_delta:    {"file_path":"/tmp/test.txt"}
  5. The client rejects it because filePath is absent, then retries.

Minimal replay (no client involved) with a captured real Kilo request:

curl -N http://localhost:20128/v1/messages \
  -H 'content-type: application/json' \
  -H 'x-api-key: <key>' \
  -H 'anthropic-version: 2023-06-01' \
  --data-binary @request.json

request.json declares read + required:["filePath"]; the response still contains name:"Read" and {"file_path": ...}.

Expected Behavior

A tool-call argument rewrite must be reversed before the response reaches the client, or must not be applied at all when the client did not declare that parameter name. A client that declared filePath must receive filePath.

Actual Behavior

The response shim always converts filePath -> file_path and deletes the original key, regardless of the client.

Root Cause

open-sse/translator/helpers/toolCallShim.ts:48:

// Some models name the Read path `path` / `filePath`; Claude Code requires `file_path`.
function sanitizeFilePathArg(args: Record<string, unknown>): void {
  if (typeof args.file_path === "string" && args.file_path) return;
  const alias = [args.path, args.filePath].find((v) => typeof v === "string" && v);
  if (!alias) return;
  args.file_path = alias;
  if (typeof args.path === "string") delete args.path;
  if (typeof args.filePath === "string") delete args.filePath;   // <- destroys the client's field
}

It is registered as the Read shim (toolCallShim.ts:88) and resolveToolCallShim matches case-insensitively (toolCallShim.ts:114), so it also fires for Kilo's lowercase read. openai-to-claude.ts:938 applies it unconditionally on the response path.

Version bisect:

  • v3.8.50, v3.8.51: toolCallShim.ts = 4886 bytes, no sanitizeFilePathArg.
  • v3.8.52: 5447 bytes, sanitizeFilePathArg added.

/v1/messages is only the transport envelope; tool schemas are client-defined. The request side already knows how to distinguish Claude Code clients (executors/base.ts:991, x-app / user-agent), but the response shim does not use that check.

Suggested Fix

Preferred: apply the Claude Code alias only when the request actually came from a Claude Code client (same detection as executors/base.ts:991), otherwise leave the arguments untouched.

Minimal safe fallback: do not delete the original key - set file_path but keep filePath when it was supplied. Clients decode only the keys they declared, so both Claude Code and Kilo would work.

Related Issues

  • #13942 - same class of bug (response-side rename leaking arg keys to clients), closed as not planned.
  • #2695, #2889, #2935 - origin of the Read shim (the pages cleanup), which is why the shim exists.

Test Impact

Needs a new unit test.

Error Logs / Output

The read tool was called with invalid arguments: ["filePath"]: is missing and is required.
Please rewrite the input so it satisfies the expected schema.

Client-side tool-call record:

{
  "tool": "read",
  "state": {
    "status": "error",
    "input": { "file_path": "/home/user/repo/.projenrc.ts" },
    "error": "The read tool was called with invalid arguments: [\"filePath\"]: is missing and is required."
  }
}

Additional Context

  • Reproduced through a Kiro-hosted Claude Opus target (claude-opus-5.5) and an ollama-cloud-hosted DeepSeek Flash target (deepseek-v4.1-flash), so it is model-agnostic. Local routing aliases are intentionally omitted; they are private configuration labels and irrelevant to the bug.
  • Current workaround for users: downgrade to 3.8.51, patch out the delete args.filePath line, or normalize file_path -> filePath in the client.

Validation Plan

  • Client declaring filePath receives filePath on POST /v1/messages (streaming and non-streaming).
  • Claude Code client still receives file_path (no regression).
  • Non-Read tools and MCP tools are byte-identical (no shim applied).
  • claude-opus-5.5 (Kiro) and deepseek-v4.1-flash (ollama-cloud) both complete a read without a retry loop.

Activity

0

No activity yet.