OpenHands / OpenHands/software-agent-sdk
[Bug]: Switching to GPT-5.6 fails when history contains colon-delimited tool-call IDs
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.1k
- Forks
- 539
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 137
Description
Is there an existing issue for the same bug?
- I have searched existing issues and this is not a duplicate.
Bug Description
Switching an existing conversation from Kimi K3 to GPT-5.6 fails when the conversation history contains parallel MCP tool calls whose persisted tool_call_id values contain colons.
In a controlled Agent Canvas reproduction, one GitHub MCP call produced the valid ID github_get_file_contents_0. A later turn with two parallel calls produced github_get_file_contents:1 and github_get_file_contents:2. Both calls and observations completed successfully under Kimi.
The reproduction used Agent Canvas's built-in GitHub marketplace integration. In Agent Canvas 1.15.0, that catalog entry uses GitHub's hosted MCP endpoint at https://api.githubcopilot.com/mcp/. In this run, the tool was registered under the github server namespace as github_get_file_contents.
After switching the same conversation to GPT-5.6, the next message failed before the model could respond. The OpenAI Responses API input ID was constructed as fc_github_get_file_contents:1, preserving the colon, which OpenAI rejects.
This appears to be a cross-provider conversation-history normalization problem. Provider-specific or synthesized historical tool-call IDs need to remain valid when replayed through a different provider.
Expected Behavior
Switching LLM profiles should preserve a valid conversation history. OpenHands should either generate provider-portable tool-call IDs initially or deterministically remap historical IDs when converting the history for the destination provider.
Actual Behavior
The conversation enters execution_status=error with a non-retryable LLMBadRequestError. Every subsequent GPT-5.6 turn encounters the same invalid historical ID.
The failure was reproduced against a local SDK-backed agent server started by Agent Canvas with the equivalent explicit SDK command:
uv tool uvx \
--from openhands-agent-server==1.42.1 \
--with openhands-sdk==1.42.1 \
--with openhands-tools==1.42.1 \
--with openhands-workspace==1.42.1 \
--with 'agent-client-protocol<0.11' \
agent-server --host 127.0.0.1 --port 18000
With that SDK server running, the reproducible API sequence is: create a Kimi conversation with GitHub MCP enabled, request two parallel github_get_file_contents calls, switch the conversation to the gpt-5.6 profile, and send one no-tool message. The final command returns normally at the HTTP layer, but the conversation state changes to error and its next persisted event is the OpenAI LLMBadRequestError shown below.
The unsafe cross-provider serialization can also be reproduced without an MCP server, model credentials, or a live conversation:
uv run --with openhands-sdk==1.42.1 python -c 'from openhands.sdk.llm.message import MessageToolCall; t=MessageToolCall(id="github_get_file_contents:1", name="github_get_file_contents", arguments="{}", origin="completion"); print(t.to_responses_dict())'
It emits:
{'type': 'function_call', 'id': 'fc_github_get_file_contents:1', 'call_id': 'github_get_file_contents:1', 'name': 'github_get_file_contents', 'arguments': '{}'}
The generated Responses item id retains the colon and is therefore rejected by OpenAI. This demonstrates the SDK replay defect independently of the path that originally generated the colon-delimited ID.
Steps to Reproduce
-
Run Agent Canvas with both
openhands/kimi-k3andopenhands/gpt-5.6profiles configured, and install/enable GitHub from the Canvas MCP marketplace. In Agent Canvas 1.15.0, the catalog entry useshttps://api.githubcopilot.com/mcp/. -
Start a new conversation using Kimi K3.
-
Send this prompt:
In a single response, make two GitHub MCP github_get_file_contents calls in parallel: read pyproject.toml and README.md from OpenHands/software-agent-sdk. Do not use any other tool. Then reply only: done. -
Confirm that the persisted actions and observations have IDs resembling:
github_get_file_contents:1 github_get_file_contents:2 -
Switch the same conversation to the GPT-5.6 profile. The reproduction used
POST /api/conversations/{conversation_id}/switch_profilewith{"profile_name":"gpt-5.6"}; switching from the Agent Canvas model picker should exercise the same conversation switch path. -
Send a no-tool message such as
Reply only: switched. -
Observe the OpenAI 400 and
execution_status=error.
Control observation: asking Kimi for exactly one GitHub MCP call generated github_get_file_contents_0, and that turn completed normally. The colon-delimited IDs appeared when two MCP calls were requested in parallel.
Acceptance Criteria
- A conversation containing parallel MCP calls can switch from Kimi K3 to GPT-5.6 without an invalid-ID error.
- Every tool-call/function-call ID sent to OpenAI contains only characters allowed by the OpenAI Responses API.
- Any ID remapping preserves the association between each action and its corresponding observation.
- Add a regression test covering parallel MCP calls followed by a cross-provider LLM switch.
Installation Method
npm install -g @openhands/agent-canvas
If you selected "Other", please specify
Agent Canvas starts the SDK and agent-server through its bundled uvx command.
SDK Version
Agent Canvas 1.15.0 (current npm release at reproduction time), bundling openhands-agent-server==1.42.1, openhands-sdk==1.42.1, and openhands-tools==1.42.1.
PyPI had SDK/agent-server 1.43.1 available, but this reproduction was not rerun against a standalone 1.43.1 server.
Version Confirmation
- I have confirmed this bug exists on the LATEST version of OpenHands SDK
It was confirmed on the SDK version bundled by the latest Agent Canvas release, not on standalone SDK 1.43.1.
Python Version
3.12.12
Model Name (if applicable)
Source: openhands/kimi-k3
Destination: openhands/gpt-5.6
Operating System
MacOS
Logs and Error Messages
Persisted actions before the switch:
{"kind":"ActionEvent","tool_name":"github_get_file_contents","tool_call_id":"github_get_file_contents:1"}
{"kind":"ActionEvent","tool_name":"github_get_file_contents","tool_call_id":"github_get_file_contents:2"}
Corresponding observations used the same IDs and completed successfully.
The first GPT-5.6 turn then failed with:
LLMBadRequestError
Invalid 'input[6].id': 'fc_github_get_file_contents:1'. Expected an ID that contains letters, numbers, underscores, or dashes, but this value contained additional characters.
Minimal Code Sample
The reproduction uses the normal Agent Canvas conversation API rather than direct SDK construction:
POST /api/conversations # create with Kimi K3 + GitHub MCP
POST /api/conversations/{id}/events # request two parallel MCP calls
POST /api/conversations/{id}/switch_profile # {"profile_name":"gpt-5.6"}
POST /api/conversations/{id}/events # send a no-tool message
Screenshots and Additional Context
The issue was reproduced in a fresh disposable conversation after restarting Agent Canvas. No restart occurred between the Kimi MCP calls and the GPT-5.6 failure.
The important persisted transition was:
Kimi parallel MCP ActionEvent ID: github_get_file_contents:1
OpenAI Responses input item ID: fc_github_get_file_contents:1
The destination conversion adds fc_ but leaves the provider-invalid colon intact.
A community reproduction also using the Canvas marketplace GitHub integration instead observed get_file_contents_0 and get_file_contents_1. That useful control did not reproduce the colon IDs and exposed a further difference: their tool name was unnamespaced (get_file_contents), while this reproduction persisted the namespaced form (github_get_file_contents). The reason for that naming/ID difference is not yet established and may be configuration-dependent or nondeterministic. The SDK serialization failure above remains deterministic once such an ID is present in conversation history.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with openhands.sdk.llm.message.MessageToolCall.to_responses_dict() and reproduce the shown colon-delimited ID serialization. Trace the conversation switch endpoints and action/observation ID pairing, then add the requested regression coverage for parallel MCP calls followed by a provider switch. Done means the switch succeeds, emitted IDs meet the stated character requirements, and action-to-observation associations remain intact.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend-api-design, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100