openclaw - ✅(Solved) Fix [Bug]: OpenAI Responses replay can send overlong tool call_id values after tool use [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#74665Fetched 2026-04-30 06:21:32
View on GitHub
Comments
1
Participants
2
Timeline
2
Reactions
2
Timeline (top)
commented ×1cross-referenced ×1

OpenAI Responses replay can send overlong or malformed call_id values back to the provider after a tool call, causing the next request to be rejected.

Error Message

throw new Error('stop after payload capture'); if (event.type === 'error') console.error(event.error.errorMessage);

Root Cause

Severity: High for affected runs, because the conversation breaks immediately after tool execution even though the tool itself completed.

Fix Action

Fix / Workaround

A local downstream patch fixes this by normalizing Responses call ids and item ids at all three boundaries:

The patch keeps ids deterministic, strips unsafe characters, caps length, and ensures Responses item ids have the expected fc prefix shape.

PR fix notes

PR #74850: fix(openai): normalize responses replay tool ids

Description (problem / solution / changelog)

Summary

  • normalize malformed or overlong OpenAI Responses replay tool ids before pi-ai splits call_id|fc_id pairs
  • keep canonical call_... and fc_... pairings unchanged for same-model reasoning replay
  • add regression coverage for overlong call ids and non-fc item ids from #74665

Fixes #74665

Tests

  • pnpm test src/agents/pi-embedded-runner.openai-tool-id-preservation.test.ts src/agents/openai-responses.reasoning-replay.test.ts extensions/openai/openai-provider.test.ts
  • pnpm exec oxfmt --check --threads=1 src/agents/pi-embedded-helpers/openai.ts src/agents/pi-embedded-helpers.ts src/agents/pi-embedded-runner/replay-history.ts src/agents/pi-embedded-runner.openai-tool-id-preservation.test.ts

Notes

  • pnpm check:changed was attempted locally but fails due unrelated pre-existing dirty workspace files such as "... 2.ts" copies with missing imports; the focused tests and formatter for this change pass.

Changed files

  • docs/reference/transcript-hygiene.md (modified, +1/-1)
  • src/agents/pi-embedded-helpers.ts (modified, +1/-0)
  • src/agents/pi-embedded-helpers/openai.ts (modified, +191/-0)
  • src/agents/pi-embedded-runner.openai-tool-id-preservation.test.ts (modified, +37/-0)
  • src/agents/pi-embedded-runner/replay-history.ts (modified, +6/-3)
  • src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.test.ts (modified, +65/-1)
  • src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts (modified, +13/-1)
  • src/agents/pi-embedded-runner/run/attempt.ts (modified, +2/-6)

Code Example

TMP=$(mktemp -d)
cd "$TMP"
npm init -y >/dev/null
npm install @mariozechner/pi-ai@0.70.5 >/dev/null
node --input-type=module <<'NODE'
import { streamOpenAIResponses } from '@mariozechner/pi-ai/openai-responses';

const longCallId = 'call_' + 'x'.repeat(120);
const longItemId = 'notfc_' + 'y'.repeat(120);
const toolCallId = `${longCallId}|${longItemId}`;

const model = {
  api: 'openai-responses',
  provider: 'openai-codex',
  id: 'gpt-test',
  baseUrl: 'https://example.invalid/v1',
  input: ['text'],
  output: ['text'],
  headers: {},
};

const context = {
  systemPrompt: 'test',
  messages: [
    {
      role: 'assistant',
      api: 'openai-responses',
      provider: 'openai-codex',
      model: 'gpt-test',
      content: [{ type: 'toolCall', id: toolCallId, name: 'noop', arguments: {} }],
    },
    { role: 'toolResult', toolCallId, content: [{ type: 'text', text: 'ok' }] },
    { role: 'user', content: 'continue' },
  ],
};

const stream = streamOpenAIResponses(model, context, {
  apiKey: 'x',
  onPayload(params) {
    console.log(JSON.stringify(params.input, null, 2));
    throw new Error('stop after payload capture');
  },
});

for await (const event of stream) {
  if (event.type === 'error') console.error(event.error.errorMessage);
}
NODE

---

[
  {
    "role": "system",
    "content": "test"
  },
  {
    "type": "function_call",
    "id": "notfc_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "call_id": "call_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "name": "noop",
    "arguments": "{}"
  },
  {
    "type": "function_call_output",
    "call_id": "call_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "output": "ok"
  },
  {
    "role": "user",
    "content": [
      {
        "type": "input_text",
        "text": "continue"
      }
    ]
  }
]

---

$ npm view openclaw@2026.4.27-beta.1 dependencies.@mariozechner/pi-ai
0.70.5

$ npm view @mariozechner/pi-ai@0.70.5 version
0.70.5

# Latest @mariozechner/pi-ai still has the same replay shape at 0.70.6:
# openai-responses-shared.js still splits toolCall.id and msg.toolCallId directly,
# and still stores stream function-call ids as `${item.call_id}|${item.id}`.

# Repro output shows overlong call_id and non-fc item id in the generated payload.

---

const [callId, itemIdRaw] = toolCall.id.split("|");
...
call_id: callId
...
const [callId] = msg.toolCallId.split("|");
...
call_id: callId
...
id: `${item.call_id}|${item.id}`
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

OpenAI Responses replay can send overlong or malformed call_id values back to the provider after a tool call, causing the next request to be rejected.

Steps to reproduce

  1. Install [email protected], which depends on @mariozechner/[email protected], or install @mariozechner/[email protected] directly.
  2. Use an OpenAI Responses provider path (openai-responses / openai-codex-responses) with a prior assistant tool call whose internal tool-call id is stored as {call_id}|{response_item_id}.
  3. Let the stored call_id or item id contain a long provider/internal id.
  4. Inspect the next Responses payload generated for replay after the tool result.

Minimal payload-capture repro:

TMP=$(mktemp -d)
cd "$TMP"
npm init -y >/dev/null
npm install @mariozechner/[email protected] >/dev/null
node --input-type=module <<'NODE'
import { streamOpenAIResponses } from '@mariozechner/pi-ai/openai-responses';

const longCallId = 'call_' + 'x'.repeat(120);
const longItemId = 'notfc_' + 'y'.repeat(120);
const toolCallId = `${longCallId}|${longItemId}`;

const model = {
  api: 'openai-responses',
  provider: 'openai-codex',
  id: 'gpt-test',
  baseUrl: 'https://example.invalid/v1',
  input: ['text'],
  output: ['text'],
  headers: {},
};

const context = {
  systemPrompt: 'test',
  messages: [
    {
      role: 'assistant',
      api: 'openai-responses',
      provider: 'openai-codex',
      model: 'gpt-test',
      content: [{ type: 'toolCall', id: toolCallId, name: 'noop', arguments: {} }],
    },
    { role: 'toolResult', toolCallId, content: [{ type: 'text', text: 'ok' }] },
    { role: 'user', content: 'continue' },
  ],
};

const stream = streamOpenAIResponses(model, context, {
  apiKey: 'x',
  onPayload(params) {
    console.log(JSON.stringify(params.input, null, 2));
    throw new Error('stop after payload capture');
  },
});

for await (const event of stream) {
  if (event.type === 'error') console.error(event.error.errorMessage);
}
NODE

Expected behavior

OpenClaw should normalize Responses tool identifiers before replaying them:

  • function_call.call_id and matching function_call_output.call_id should be valid Responses call ids, not raw overlong internal ids.
  • function_call.id should be a valid Responses item id and keep the required fc prefix/shape.
  • The replayed tool result should be accepted by OpenAI-compatible Responses APIs.

Actual behavior

The generated replay payload includes the raw overlong call id and raw item id:

[
  {
    "role": "system",
    "content": "test"
  },
  {
    "type": "function_call",
    "id": "notfc_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "call_id": "call_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "name": "noop",
    "arguments": "{}"
  },
  {
    "type": "function_call_output",
    "call_id": "call_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "output": "ok"
  },
  {
    "role": "user",
    "content": [
      {
        "type": "input_text",
        "text": "continue"
      }
    ]
  }
]

In real provider runs, this class of payload fails when the Responses API validates input[*].call_id / function-call ids.

OpenClaw version

2026.4.27-beta.1

Operating system

Ubuntu 24.04 / Linux

Install method

npm package / npm global beta path; isolated repro installed @mariozechner/[email protected], which [email protected] depends on.

Model

OpenAI Responses / OpenAI Codex Responses style model using tool calls.

Provider / routing chain

OpenClaw -> @mariozechner/pi-ai Responses provider -> OpenAI-compatible Responses endpoint.

Additional provider/model setup details

The repro uses onPayload to stop before network send, so no API credentials or live provider are required. The failure is in request construction/replay: stored tool-call ids are split and re-emitted without robust normalization.

Logs, screenshots, and evidence

$ npm view [email protected] dependencies.@mariozechner/pi-ai
0.70.5

$ npm view @mariozechner/[email protected] version
0.70.5

# Latest @mariozechner/pi-ai still has the same replay shape at 0.70.6:
# openai-responses-shared.js still splits toolCall.id and msg.toolCallId directly,
# and still stores stream function-call ids as `${item.call_id}|${item.id}`.

# Repro output shows overlong call_id and non-fc item id in the generated payload.

Relevant implementation shape in openai-responses-shared.js:

const [callId, itemIdRaw] = toolCall.id.split("|");
...
call_id: callId
...
const [callId] = msg.toolCallId.split("|");
...
call_id: callId
...
id: `${item.call_id}|${item.id}`

Impact and severity

Affected: users of OpenAI Responses / Codex Responses provider paths when a conversation continues after tool use and stored tool ids contain provider/internal ids that exceed Responses validation limits.

Severity: High for affected runs, because the conversation breaks immediately after tool execution even though the tool itself completed.

Frequency: Always reproduced for replay payload construction with the overlong ids shown above.

Consequence: Tool-using conversations fail on the follow-up request with provider-side validation errors such as invalid/overlong input[*].call_id.

Additional information

A local downstream patch fixes this by normalizing Responses call ids and item ids at all three boundaries:

  1. When converting stored assistant toolCall.id back into function_call input items.
  2. When converting toolResult.toolCallId into function_call_output.call_id.
  3. When storing streamed Responses function_call ids as OpenClaw tool-call ids.

The patch keeps ids deterministic, strips unsafe characters, caps length, and ensures Responses item ids have the expected fc prefix shape.

extent analysis

TL;DR

To fix the issue, normalize the call_id and item_id values in the OpenAI Responses payload to ensure they comply with the validation limits of the OpenAI-compatible Responses API.

Guidance

  1. Identify the source of overlong ids: Determine where the overlong call_id and item_id values are originating from, which could be from the toolCall.id or msg.toolCallId in the openai-responses-shared.js file.
  2. Implement id normalization: Develop a function to normalize these ids, ensuring they are within the allowed length and format for the OpenAI Responses API. This may involve stripping unsafe characters, truncating the length, and prefixing item ids with 'fc' as needed.
  3. Apply normalization at all boundaries: Ensure that the normalization function is applied at all points where call_id and item_id are converted or stored, including when converting toolCall.id to function_call input items, toolResult.toolCallId to function_call_output.call_id, and when storing streamed Responses function_call ids as OpenClaw tool-call ids.

Example

A simple example of id normalization could involve using JavaScript's string methods to truncate and prefix the ids:

function normalizeId(id, maxLength, prefix = '') {
  // Remove unsafe characters and truncate to maxLength
  const safeId = id.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, maxLength);
  return prefix + safeId;
}

// Example usage
const callId = 'call_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
const normalizedCallId = normalizeId(callId, 64); // Assuming 64 character limit
const itemId = 'notfc_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

OpenClaw should normalize Responses tool identifiers before replaying them:

  • function_call.call_id and matching function_call_output.call_id should be valid Responses call ids, not raw overlong internal ids.
  • function_call.id should be a valid Responses item id and keep the required fc prefix/shape.
  • The replayed tool result should be accepted by OpenAI-compatible Responses APIs.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: OpenAI Responses replay can send overlong tool call_id values after tool use [1 pull requests, 1 comments, 2 participants]