Summary
With --backend ollama (native capability), forge forwards the verbatim OpenAI
transcript to Ollama. Assistant history messages carry
tool_calls[].function.arguments as a JSON string (OpenAI wire format), but
Ollama's /api/chat requires them as an object (dict). Every multi-turn
tool session fails on the 2nd+ turn with:
400 {"error":"Value looks like object, but can't find closing '}' symbol"}
First turn succeeds (no tool history yet); as soon as an assistant tool_calls
turn enters the history, all subsequent requests 400. Symptom from the client
(opencode) side: the proxy returns a non-OpenAI error chunk and the client's
schema validation crashes.
Minimal repro (directly against Ollama, bypassing forge)
# args as STRING -> 400
curl -s http://localhost:11434/api/chat -d '{"model":"qwen3-coder:30b","stream":false,
"messages":[{"role":"user","content":"x"},
{"role":"assistant","tool_calls":[{"function":{"name":"t","arguments":"{\"x\":\"1\"}"}}]},
{"role":"tool","content":"ok"}],
"tools":[{"type":"function","function":{"name":"t","parameters":{"type":"object","properties":{"x":{"type":"string"}}}}}]}'
# args as DICT -> 200 (same request, arguments as object)
Root cause
proxy/handler.py: in native_passthrough, raw_messages_for_backend = _raw_openai_messages(request_messages) forwards arguments unchanged.
clients/ollama.py send() / send_stream() post these messages to /api/chat without coercing arguments str→dict.
proxy/convert.py already does this coercion on the inbound path (if isinstance(args, str): args = json.loads(args)), but the raw-passthrough path bypasses it.
Suggested fix
Coerce tool_calls[].function.arguments from str→dict for the Ollama backend
(e.g. in OllamaClient.send/send_stream before building the request body, or
suppress raw passthrough for Ollama so the normal format="ollama" serialization
is used).
Env
forge-guardrails 0.7.5, Ollama 0.30.8, model qwen3-coder (custom), client: opencode (OpenAI protocol, streaming).
--- Above message is written by Opus4.8 and it fixed my local env automatically and confirm working. ---
I with this info will help to improve this project.
Summary
With
--backend ollama(native capability), forge forwards the verbatim OpenAItranscript to Ollama. Assistant history messages carry
tool_calls[].function.argumentsas a JSON string (OpenAI wire format), butOllama's
/api/chatrequires them as an object (dict). Every multi-turntool session fails on the 2nd+ turn with:
First turn succeeds (no tool history yet); as soon as an assistant
tool_callsturn enters the history, all subsequent requests 400. Symptom from the client
(opencode) side: the proxy returns a non-OpenAI error chunk and the client's
schema validation crashes.
Minimal repro (directly against Ollama, bypassing forge)
Root cause
proxy/handler.py: innative_passthrough,raw_messages_for_backend = _raw_openai_messages(request_messages)forwards arguments unchanged.clients/ollama.pysend()/send_stream()post these messages to/api/chatwithout coercingargumentsstr→dict.proxy/convert.pyalready does this coercion on the inbound path (if isinstance(args, str): args = json.loads(args)), but the raw-passthrough path bypasses it.Suggested fix
Coerce
tool_calls[].function.argumentsfrom str→dict for the Ollama backend(e.g. in
OllamaClient.send/send_streambefore building the request body, orsuppress raw passthrough for Ollama so the normal
format="ollama"serializationis used).
Env
forge-guardrails 0.7.5, Ollama 0.30.8, model qwen3-coder (custom), client: opencode (OpenAI protocol, streaming).
--- Above message is written by Opus4.8 and it fixed my local env automatically and confirm working. ---
I with this info will help to improve this project.