Provider-neutral tool calling repair layer for OpenAI-compatible model proxies, local agents, and developer tools.
It helps projects that expose /v1/chat/completions or /v1/responses behave more like a mature OpenAI-compatible API when the upstream model is weak at function calling.
Many local agents and model proxies fail in the same boring ways:
- The model says "I will read the file" but never emits a tool call.
- Tool calls are returned as XML-ish text instead of OpenAI
tool_calls. - Streaming clients wait forever when the upstream fails.
- Repeated paragraphs make the agent look stuck.
/v1/responsesclients needresponse.*SSE events, not raw chat chunks.
This package is a small adapter layer for those cases.
- Parse XML-style tool calls into OpenAI-compatible
tool_calls. - Hide incomplete tool markup during streaming.
- Repair local filesystem action narratives into read-only shell tool calls.
- Drop blocked or unsupported tool names.
- Deduplicate repeated tool calls and repeated visible paragraphs.
- Convert chat/completions SSE chunks into Responses API-style events.
- Emit
response.failedplus[DONE]on upstream errors. - Provider-neutral by design: no provider-specific access layer or public relay features.
pip install agent-toolcall-adapterFor local development:
pip install -e .from agent_toolcall_adapter import adapt_assistant_text
raw_text = '''
<tool_calls>
<invoke name="PowerShell">
<parameter name="command"><![CDATA[Get-ChildItem -Force .]]></parameter>
</invoke>
</tool_calls>
'''
result = adapt_assistant_text(raw_text, allowed_tool_names={"PowerShell"})
print(result.finish_reason)
print(result.to_openai_message())Output shape:
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_xxx",
"type": "function",
"function": {
"name": "PowerShell",
"arguments": "{\"command\":\"Get-ChildItem -Force .\"}"
}
}
]
}Use it at the boundary where your upstream model text becomes an OpenAI response:
from agent_toolcall_adapter import adapt_assistant_text
def build_chat_completion(upstream_text: str, model: str, tools: list[dict]):
allowed = {
tool["function"]["name"]
for tool in tools
if tool.get("type") == "function"
}
adapted = adapt_assistant_text(upstream_text, allowed_tool_names=allowed)
return {
"id": "chatcmpl_local",
"object": "chat.completion",
"model": model,
"choices": [{
"index": 0,
"message": adapted.to_openai_message(),
"finish_reason": adapted.finish_reason,
}],
}For models that need a text tool protocol:
from agent_toolcall_adapter import build_tool_prompt
system_tool_prompt = build_tool_prompt(tools)
messages.insert(0, {"role": "system", "content": system_tool_prompt})from agent_toolcall_adapter import ResponsesStreamAdapter
adapter = ResponsesStreamAdapter(model="my-model")
for event in adapter.start():
yield event
try:
for chat_chunk in upstream_chat_sse:
for event in adapter.feed(chat_chunk):
yield event
except Exception as exc:
for event in adapter.fail(str(exc)):
yield eventThis repository is for interoperability and tool-call formatting.
It intentionally does not include:
- Provider-specific access implementations.
- Public relay server features.
- Traffic aggregation features.
- Provider-specific automation tutorials.
- Third-party service redistribution workflows.
If you use it inside a proxy, keep provider access, auth, rate limits, and user data handling in your own compliant provider layer.
OpenAI-compatible API, function calling, tool calling, Responses API, local agents, Codex, WorkBuddy, MCP, LLM proxy, XML tool calls, streaming adapter.
MIT.