GPT-4o RAG citation format error after adding GPU node

Problem Description

After adding a fourth GPU node to a distributed inference cluster for OpenAI GPT‑4o with Retrieval‑Augmented Generation (RAG), the citations field in the model response became malformed. Typical symptoms observed in production logs include:

  • JSON parsing errors such as "Error parsing citation: unexpected token ''".
  • Missing or truncated brackets in the citation array, e.g. "Citation format violation: missing 'source' key in response object".
  • Interleaved citation IDs when the round‑robin GPU scheduler is used.
  • Final output containing broken markdown links (e.g., [1] https://example.com without closing brackets).

These errors prevent downstream consumers from reliably linking generated content to source documents, breaking compliance requirements and user experience.

Root Cause Analysis

The issue originates from the interaction of three components:

  1. GPT‑4o response schema: The official OpenAI API reference specifies that RAG responses must contain a top‑level citations array of objects, each with source, page, and id fields (OpenAI API reference for GPT‑4o).
  2. Distributed inference streaming: When scaling across multiple GPUs, the OpenAI multi‑instance deployment guide notes that each worker streams partial JSON fragments. The load balancer may split HTTP responses, and workers must flush complete JSON objects before the next chunk (OpenAI documentation on multi‑instance deployment and GPU scaling).
  3. Response aggregation middleware: In the existing pipeline, an async aggregator concatenates streamed fragments from each GPU worker. A race condition in this aggregator caused the final closing brace of the citations block to be dropped on the last GPU, as reported in the GitHub issue openai/openai-python #8421 and the Stack Overflow answer for 78543219.

Consequently, the aggregated payload sometimes ends with an incomplete JSON object (e.g., {"citations":[{"source":"doc1") or with duplicated citation entries, leading to the parsing errors listed above.

Investigation and Debugging

The following steps were used to isolate the failure:

1. Examine worker logs

kubectl logs -l app=gpt4o-worker -c inference --tail=200 | grep -i citation
2024-09-08T12:34:56.789Z worker-2 INFO Streaming response chunk: {"citations":[
2024-09-08T12:34:56.791Z worker-2 INFO Streaming response chunk: {"source":"doc42","page":3,"id":"c7"}
2024-09-08T12:34:56.793Z worker-2 WARN Incomplete JSON detected, awaiting more data

2. Capture the raw HTTP stream

tcpdump -i eth0 -s 0 -w /tmp/trace.pcap port 443
# later decode with tshark
tshark -r /tmp/trace.pcap -Y http.response -T fields -e http.file_data | grep citations

3. Verify aggregation output

# Python snippet reproducing the bug
from aggregator import aggregate_responses

chunks = [
    '{"citations":[{"source":"doc1","page":2,"id":"c1"}',
    ',{"source":"doc2","page":5,"id":"c2"}]}',  # correct closing brace
    '{"citations":[{"source":"doc3","page":1,"id":"c3"}'  # missing closing
]

result = aggregate_responses(chunks)
print(result)
# Output:
# {"citations":[{"source":"doc1","page":2,"id":"c1"},{"source":"doc2","page":5,"id":"c2"}]}
# Error parsing citation: unexpected token ''

4. Correlate with load balancer behavior

Azure OpenAI incident logs showed that the load balancer split the HTTP response at a 4 KB boundary, cutting the citation block in half. This matches the pattern observed in the trace capture.

Resolution

The fix consists of three coordinated changes:

1. Enforce function‑calling schema on each worker

By using OpenAI’s function calling feature, each worker returns a fully‑validated JSON object, preventing partial streams.

# Before (raw text streaming)
response = openai.ChatCompletion.create(
    model="gpt-4o",
    messages=messages,
    stream=True
)

# After (function call with strict schema)
citation_schema = {
    "type": "object",
    "properties": {
        "citations": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "source": {"type": "string"},
                    "page": {"type": "integer"},
                    "id": {"type": "string"}
                },
                "required": ["source", "page", "id"]
            }
        }
    },
    "required": ["citations"]
}

response = openai.ChatCompletion.create(
    model="gpt-4o",
    messages=messages,
    functions=[{"name":"return_citations","parameters":citation_schema}],
    function_call={"name":"return_citations"},
    stream=False   # disable raw streaming, rely on function payload
)

This change aligns with the OpenAI guide on function calling and structured outputs, guaranteeing that each worker emits a complete JSON payload.

2. Introduce a robust aggregation middleware

The new middleware buffers complete JSON objects per worker and merges them only after all workers signal completion.

# aggregator.py (before)
def aggregate_responses(chunks):
    return "".join(chunks)   # naive concatenation

# aggregator.py (after)
import json

def aggregate_responses(chunks):
    merged = {"citations": []}
    for chunk in chunks:
        try:
            obj = json.loads(chunk)
            merged["citations"].extend(obj.get("citations", []))
        except json.JSONDecodeError:
            # Log and skip malformed chunk
            logger.warning("Malformed JSON from worker: %s", chunk)
    # De‑duplicate by citation id
    unique = {c["id"]: c for c in merged["citations"]}.values()
    return json.dumps({"citations": list(unique)})

3. Adjust the GPU scheduler to preserve ordering

Switch from round‑robin to a deterministic queue that waits for each GPU to finish before releasing its payload. This eliminates interleaved citation arrays reported in the “race condition” incident.

# launch with torch.distributed
torchrun --nproc_per_node=4 --rdzv_id=gpt4o_rag \
    --rdzv_backend=c10d --rdzv_endpoint=host:29500 \
    --max_restarts=0 \
    inference_worker.py --scheduler=sequential

Validation

After deploying the fixes, the following checks confirm correct behavior:

1. JSON schema validation

curl -s http://inference-service/v1/completions \
    -H "Content-Type: application/json" \
    -d @request.json | jq .citations
[
  {"source":"doc1","page":2,"id":"c1"},
  {"source":"doc2","page":5,"id":"c2"},
  {"source":"doc3","page":1,"id":"c3"}
]

2. Log inspection

kubectl logs -l app=gpt4o-worker -c inference | grep "Citation format violation"
# No output – indicates all citations conform to schema

3. End‑to‑end functional test

A test harness queries the RAG endpoint with a known document set and asserts that each generated citation resolves to an existing source URL. All assertions pass.

Operational Experience

  • Misleading symptom: Initial alerts showed only a spike in StreamingError: citation block not closed properly, leading teams to suspect network packet loss. In reality, the root cause was the aggregator’s naive concatenation.
  • Incorrect assumption: It was assumed that disabling streaming would reduce latency. However, the function‑calling approach added ~30 ms overhead, which was acceptable given the gain in data integrity.
  • Production edge case: When the load balancer performed TLS termination, it introduced an extra 2 KB buffer that split the JSON payload exactly at the citation array boundary. The new middleware’s buffering logic absorbed this split without error.
  • Lesson learned: Always validate structured outputs at the worker level before any cross‑node aggregation, especially when using streaming APIs in a distributed setting.

Best Practices and Prevention

  • Use OpenAI function calls for any structured RAG output; avoid raw streaming when JSON integrity is required.
  • Implement per‑worker JSON schema validation (e.g., with jsonschema) and fail fast on malformed payloads.
  • Configure the load balancer to disable HTTP/2 multiplexing for RAG endpoints, preventing mid‑stream splits.
  • Instrument metrics:
    • gpt4o.rag.citation_parse_errors – counter of JSON parse failures.
    • gpt4o.rag.aggregation_latency_ms – histogram of aggregation time.
  • Set alerts on any non‑zero citation_parse_errors within a 5‑minute window.
  • Prefer sequential or deterministic scheduling for GPU workers when the response order matters.

Related Topic Hub: LLM Systems Troubleshooting Hub

FAQ

  1. Why does the citation format break only after adding a GPU node?
    Because the additional node introduces a new streaming source. The original aggregator concatenated fragments without ensuring each fragment formed a complete JSON object, so the extra stream increased the chance of a split at the citation block.
  2. Can I keep streaming and still get valid citations?
    Yes, but you must enforce a strict JSON schema per chunk and buffer incomplete fragments until a closing delimiter is received. Using function calls is the recommended approach.
  3. How do I detect duplicate citation IDs?
    After aggregation, de‑duplicate using the citation id field as shown in the corrected aggregate_responses implementation. Monitoring for sudden spikes in the number of citations per response can also surface duplicates.
  4. Is there a performance impact when disabling streaming?
    The function‑calling path adds a small latency overhead (≈30 ms in our tests) due to full JSON serialization on the worker side, but it eliminates parsing errors and downstream retries, resulting in overall faster end‑to‑end latency.
  5. What should I monitor to catch similar issues early?
    Track gpt4o.rag.citation_parse_errors, response size variance, and aggregation latency. Alert on any parse error or when aggregation latency exceeds the 95th percentile of the baseline.