Skip to content
C.W.K.
Stream
Lesson 04 of 04 · published

OpenAI Compatibility Tests

~18 min · serving, compatibility, openai

Level 0Downloader
0 XP0/41 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

OpenAI compatibility is a spectrum

Every local engine claims OpenAI-compatible. They're all almost compatible. The divergences are predictable but not standardized:

  • Streaming format. All emit SSE, but exact chunk shapes differ.
  • Tool calling. Native Ollama gives parsed dict args; OpenAI emits JSON string args; some engines wrap differently.
  • Structured output. Ollama uses a format field with JSON Schema. OpenAI uses response_format, and vLLM takes that too — its old guided_json family was removed in v0.12.0 (December 2025), which is why the harness below sends response_format everywhere. Same idea, different keys, and the keys converge over time.
  • Token counting. Slightly different (or missing) token usage fields.

How to test compatibility

Don't trust the label. Run a small test suite that exercises the features you use. The list below is the canonical local-AI compatibility test:

  1. Non-streaming chat with a simple message.
  2. Streaming chat with the same message.
  3. Multi-turn conversation (system + user + assistant).
  4. Tool definition + multi-turn tool loop.
  5. Structured output (JSON Schema).
  6. Token usage in the response.

If all six pass on your engine, you can swap engines without app changes. If items 4 or 5 fail, expect to write engine-specific shims.

Code

Compatibility test harness·python
import httpx, json

TOOLS = [{"type": "function", "function": {
    "name": "say_ok", "description": "Reply with 'ok'.",
    "parameters": {"type": "object", "properties": {}, "required": []},
}}]

PERSON_SCHEMA = {
    "type": "object",
    "properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
    "required": ["name", "age"],
}

def test_compat(base_url: str, model: str, label: str = ""):
    """Six checks, one per feature the prose lists. Returns {n: bool}."""
    print(f"\n=== {label or base_url} ===")
    kw = {"timeout": 120.0}
    results: dict[int, bool] = {}

    def report(n: int, name: str, ok: bool, detail: str = ""):
        results[n] = ok
        print(f"{n}. {name:<18} {'PASS' if ok else 'FAIL'}{'  ' + detail if detail else ''}")

    # 1. Non-streaming chat
    r = httpx.post(f"{base_url}/chat/completions", json={
        "model": model,
        "messages": [{"role": "user", "content": "Say 'ok' and nothing else."}],
        "stream": False,
    }, **kw)
    report(1, "non-stream", r.status_code == 200)

    # 2. Streaming chat
    with httpx.stream("POST", f"{base_url}/chat/completions", json={
        "model": model,
        "messages": [{"role": "user", "content": "Count: 1 2 3"}],
        "stream": True,
    }, timeout=None) as r2:
        chunks = sum(1 for line in r2.iter_lines() if line.startswith("data:"))
    report(2, "stream", chunks > 1, f"({chunks} chunks)")

    # 3. Multi-turn (system + user + assistant + user)
    r3 = httpx.post(f"{base_url}/chat/completions", json={
        "model": model,
        "messages": [
            {"role": "system", "content": "You answer with one word."},
            {"role": "user", "content": "My code word is BANANA. Remember it."},
            {"role": "assistant", "content": "Noted."},
            {"role": "user", "content": "What is my code word?"},
        ],
        "stream": False,
    }, **kw)
    ok3 = r3.status_code == 200 and "banana" in r3.text.lower()
    report(3, "multi-turn", ok3)

    # 4. Tool definition + tool loop (call, then feed the result back)
    r4 = httpx.post(f"{base_url}/chat/completions", json={
        "model": model,
        "messages": [{"role": "user", "content": "Use the say_ok tool."}],
        "tools": TOOLS, "stream": False,
    }, **kw)
    calls = (r4.json().get("choices", [{}])[0].get("message", {}) or {}).get("tool_calls") or []
    ok4 = bool(calls)
    if ok4:
        # The loop half: the engine must accept a tool result back.
        r4b = httpx.post(f"{base_url}/chat/completions", json={
            "model": model,
            "messages": [
                {"role": "user", "content": "Use the say_ok tool."},
                {"role": "assistant", "content": "", "tool_calls": calls},
                {"role": "tool", "tool_call_id": calls[0].get("id", "0"), "content": "ok"},
            ],
            "tools": TOOLS, "stream": False,
        }, **kw)
        ok4 = r4b.status_code == 200
    report(4, "tool loop", ok4)

    # 5. Structured output (JSON Schema)
    r5 = httpx.post(f"{base_url}/chat/completions", json={
        "model": model,
        "messages": [{"role": "user", "content": "Invent a person."}],
        "response_format": {"type": "json_schema", "json_schema": {
            "name": "person", "schema": PERSON_SCHEMA}},
        "stream": False,
    }, **kw)
    ok5 = False
    if r5.status_code == 200:
        try:
            obj = json.loads(r5.json()["choices"][0]["message"]["content"])
            ok5 = isinstance(obj.get("name"), str) and isinstance(obj.get("age"), int)
        except (json.JSONDecodeError, KeyError, AttributeError):
            ok5 = False
    report(5, "structured", ok5)

    # 6. Token usage in the response
    usage = r.json().get("usage") if r.status_code == 200 else None
    ok6 = bool(usage) and usage.get("total_tokens", 0) > 0
    report(6, "usage", ok6, f"({usage})" if usage else "")

    return results

# Run against multiple engines
test_compat("http://localhost:11434/v1", "qwen2.5:7b",   label="Ollama")
test_compat("http://localhost:8080/v1",  "qwen-direct",  label="llama-server")
# test_compat("http://localhost:8000/v1", "Qwen/Qwen2.5-7B-Instruct", label="vLLM")

External links

Exercise

Run the compatibility test harness against Ollama and one other engine. List which of the 6 features pass and which fail. Decide whether the failures matter for your use case — if they do, write the per-engine shim plan.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.