Summary
A custom openai-chat-completions connection reports fx: InvalidChunk on every reply. The full answer prints first; the failure happens on the final chunk.
The server's finish chunk includes the deprecated delta.function_call field populated with empty strings:
"delta": {"content": "", "role": "assistant", "function_call": {"name": "", "arguments": ""}},
"finish_reason": "stop"fx appears to treat any non-null function_call as a legacy function call and rejects it because name is empty, which aborts the stream. The same stream succeeds when function_call is null or omitted.
This is the same symptom as #1173 but a different trigger: there the offending field is model: ""; here model is populated and the offending field is the empty function_call.
Environment
- fx version: v0.0.16 (latest release), build revision
481104fdbc24, stable channel - Platform: macOS 27.0.1, arm64
- Connection: custom
openai-chat-completionsprovider,auth: none - Real-world source: a local OpenAI-compatible gateway (Magpie) forwarding to an upstream (WorkBuddy) serving
deepseek-v4.1-flashandglm-5.3. Both models emit this finish chunk on every streamed response. Other upstreams behind the same gateway (Anthropic, Codex, Gemini) omitfunction_calland work fine.
Minimal reproduction (no external service needed)
server.py is a ~40-line stdlib-only Python server that streams three chunks plus [DONE]. Its mode argument controls only the final delta's function_call.
server.py
#!/usr/bin/env python3
"""Minimal OpenAI-compatible SSE server reproducing fx InvalidChunk on an empty function_call.
Usage: python3 server.py [bad|null|omit] (default: bad)
"""
import json, sys
from http.server import BaseHTTPRequestHandler, HTTPServer
MODE = sys.argv[1] if len(sys.argv) > 1 else "bad"
MODEL = "repro-model"
def chunk(delta, finish=None, usage=None):
return {"id": "chatcmpl-repro", "object": "chat.completion.chunk", "created": 1793341426,
"model": MODEL, "choices": [{"index": 0, "delta": delta, "finish_reason": finish, "logprobs": None}],
"usage": usage}
class H(BaseHTTPRequestHandler):
def do_POST(self):
self.rfile.read(int(self.headers.get("content-length", 0)))
final_delta = {"content": "", "role": "assistant"}
if MODE == "bad":
final_delta["function_call"] = {"name": "", "arguments": ""}
elif MODE == "null":
final_delta["function_call"] = None
events = [
chunk({"role": "assistant", "content": ""}),
chunk({"content": "Hi!"}),
chunk(final_delta, finish="stop",
usage={"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12}),
]
body = "".join(f"data: {json.dumps(e, separators=(',', ':'))}\n\n" for e in events) + "data: [DONE]\n\n"
data = body.encode()
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, *a): pass
HTTPServer(("127.0.0.1", 3499), H).serve_forever()settings.json:
{
"provider": "repro",
"providers": {
"repro": {
"protocol": "openai-chat-completions",
"base_url": "http://127.0.0.1:3499/v1",
"auth": { "type": "none" },
"model_metadata": {
"repro-model": { "context_window": 128000, "max_output_tokens": 8192, "supports_tool_use": true }
}
}
},
"models": { "repro": "repro-model" }
}Run:
mkdir -p /tmp/fxrepro/home/.fx /tmp/fxrepro/ws
cp settings.json /tmp/fxrepro/home/.fx/settings.json
python3 server.py bad & # or: null | omit
cd /tmp/fxrepro/ws && HOME=/tmp/fxrepro/home fx ask "say hi"Results:
server.py mode | final delta.function_call | fx result |
|---|---|---|
bad | {"name":"","arguments":""} | Hi! then fx: InvalidChunk, exit 1 |
null | null | Hi!, exit 0 |
omit | field absent | Hi!, exit 0 |
Exact stream sent in bad mode
data: {"id":"chatcmpl-repro","object":"chat.completion.chunk","created":1793341426,"model":"repro-model","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"logprobs":null}],"usage":null}
data: {"id":"chatcmpl-repro","object":"chat.completion.chunk","created":1793341426,"model":"repro-model","choices":[{"index":0,"delta":{"content":"Hi!"},"finish_reason":null,"logprobs":null}],"usage":null}
data: {"id":"chatcmpl-repro","object":"chat.completion.chunk","created":1793341426,"model":"repro-model","choices":[{"index":0,"delta":{"content":"","role":"assistant","function_call":{"name":"","arguments":""}},"finish_reason":"stop","logprobs":null}],"usage":{"prompt_tokens":10,"completion_tokens":2,"total_tokens":12}}
data: [DONE]What I ruled out
Against the real gateway, I captured the wire traffic and replayed it to fx from a local server while changing one variable at a time:
- HTTP framing is not the cause. The chunked transfer encoding validated end to end, and serving the same body with
Content-Lengthand no chunking still fails withInvalidChunk. The error name points at the transport layer, but the rejection is SSE/JSON-level. - Removing the leading
: heartbeatSSE comment, merging chunks, droppingdata: [DONE], and trimming the extra non-standardusagefields (prompt_cache_hit_tokens,credit, etc.) each still failed. - Changing only
function_calltonullor removing it made the identical stream succeed.
Expected behavior
Treat a function_call (and, by the same logic, a tool_calls entry) whose name and arguments are both empty as absent, rather than failing the stream. This shape typically comes from servers that serialize zero-valued structs instead of omitting them. fx never requested legacy function calling: its request contains tools (14 entries) and tool_choice, and no functions key.
Suggested fixes, in order of preference
- In the chunk decoder, normalize an all-empty
delta.function_callto null before validation. - More generally, ignore the deprecated
function_calldelta when the request did not use legacyfunctions, or log and skip a malformed delta instead of terminating the stream after content has already arrived. - Include the offending field or chunk (redacted) in the
InvalidChunkerror orstream_errorevent. As with #1173, isolating this required a man-in-the-middle proxy and replay bisection.
Related
- #1173: same
InvalidChunkfailure, triggered bymodel: ""(Bifrost) - #1334: a separate strict-parsing failure (
IncompleteStreamwithoutdata: [DONE])
Activity
0No activity yet.