Lab 04-02: OpenAI-compatible clients¶
Objective¶
Talk to llama serve from your own program, without curl: first with the official openai SDK in Python
(full response and streaming), then with native Node.js fetch and no dependencies at all. By the end you know
where llama.cpp's "extra" fields (timings, chat_template_kwargs) go when you use an SDK
designed for another provider, and you know how to measure time to first token.
Prerequisites¶
- Lab 04-01 completed (you can start the server and read
timings). - Python 3.10+ with
pip; Node.js 18 or later (for nativefetchandTextDecoderStream; the examples use 22). - Ubuntu with the installer binary:
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt.
Materials¶
| What | Where | Size |
|---|---|---|
ggml-org/Qwen3.5-0.8B-GGUF (file Qwen3.5-0.8B-Q8_0.gguf, Apache 2.0) |
Hugging Face, via -hf |
785 MiB |
chat.py: Python client with the openai SDK (--stream for streaming) |
lab folder | 3 KB |
chat.mjs: Node.js client with fetch, hand-written SSE parser (--stream) |
lab folder | 2 KB |
openai package (PyPI) |
pip install openai (version 3.18.0) |
~10 MB with dependencies |
Steps¶
-
Start the server in a terminal and leave it open (one slot is enough):
Wait for
srv llama_server: listening on http://127.0.0.1:8080(orcurl -s localhost:8080/health). -
Set up the Python environment in a second terminal, inside the lab folder:
-
Read
chat.pybefore running it. The three lines that matter:client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="no-key-required") # (1)! EXTRA = {"chat_template_kwargs": {"enable_thinking": False}} # (2)! r = client.chat.completions.create(model="local", messages=..., extra_body=EXTRA) # (3)!- Compared with "real" OpenAI code only
base_urlchanges;api_keymust have some value (the SDK rejects an empty string), or the key passed to the server with--api-key. - Parameters the OpenAI API does not know (here: turning off Qwen3.5's thinking) go in
extra_body: the SDK copies them into the JSON as they are. modelis ignored by a single-model server (the-aalias applies); it becomes mandatory in router mode.
- Compared with "real" OpenAI code only
-
Full response:
Output (b11046, 6 vCPU machine without GPU):
Llamà.cpp è una libreria C++ per la gestione di chiamate di sistema, progettata per semplificare l'uso … [finish_reason=stop prompt=41 tok, completion=39 tok, 12.7s] [server: cache_n=0 prompt 12.4 t/s, generazione 4.2 t/s]The second line comes from
r.usage; the third from thetimingsfield, which is not in the SDK's data model: Pydantic keeps it inr.model_extra["timings"]. (The answer is made up: a 0.8 B model does not know llama.cpp; what matters here is how the data travels, not the content.) -
Streaming:
Llama.cpp è un framework Python per la generazione di modelli di linguaggio … [39 chunk, primo token dopo 1.11s, totale 15.4s]With
stream=Truethe SDK returns an iterator: each item is an already decoded SSE chunk, withchunk.choices[0].delta.content(often a word or part of a word). The script measures time to first token (TTFT): it is the prompt processing time plus the first generation step, and it is what the user perceives as "responsiveness". The first chunk hasdelta.role = "assistant"and a nullcontent: that is why the code checksif delta.content. -
Node.js without dependencies. Read
chat.mjs: the request is afetchwithJSON.stringify(body), and streaming is a ten-line hand-written SSE parser (it accumulates text in a buffer, splits on lines, keeps only those that start withdata:, stops on[DONE]). Try it:Ecco tre usi principali di un server locale per modelli di linguaggio (LLM): 1. **Analisi e Generazione di Contenuti**: … [80 token, 4.3 t/s]With
--streamthe text appears piece by piece, then[80 token, 4.5 t/s](from thetimingsof the last chunk) and[fine]whendata: [DONE]arrives.finish_reasonhere islength: 80 tokens are not enough for three points. -
Switch server without touching the code: both scripts read
LLAMA_URL. -
With an API key. Restart the server with
--api-key segreta123and runpython chat.pyagain: you getopenai.AuthenticationError: Error code: 401 - {'error': {'message': 'Invalid API Key', 'type': 'authentication_error', 'code': 401}}. SetAPI_KEY = "segreta123"inchat.pyand inchat.mjsuncomment the headerAuthorization: Bearer …. If you remove theapi_keyargument from the constructor, the SDK reads theOPENAI_API_KEYenvironment variable by itself:OPENAI_API_KEY=segreta123 python chat.pygoes through. -
Stop the server with Ctrl+C.
sequenceDiagram
participant P as chat.py (openai SDK)
participant S as llama serve
P->>S: POST /v1/chat/completions {messages, stream:true, chat_template_kwargs}
S-->>P: data: {"delta":{"role":"assistant","content":null}}
S-->>P: data: {"delta":{"content":"Llama"}} ← TTFT measured here
S-->>P: data: {"delta":{"content":".cpp"}} … (one chunk per token)
S-->>P: data: {"finish_reason":"stop","timings":{…}}
S-->>P: data: [DONE]Check the result¶
-
python chat.pyprints the answer, thenusageand the line[server: cache_n=… t/s]taken frommodel_extra. -
python chat.py --streamshows the text growing and a TTFT of 1-3 s on CPU (it depends on the machine). -
node chat.mjs --streamends with[fine]afterdata: [DONE]. - With
--api-keyon the server, the client without a key gets401and with the key it works.
Questions and variants¶
- Run
python chat.pytwice in a row:cache_ngoes from 0 to a value close to theprompttokens. Why? (reread § 6 of module 04). - Raise
max_tokensto 300 and comparefinish_reason: when is itlengthand whenstop? - In the Node client send
"stream_options": {"include_usage": true}:usagealso appears in the last chunk. - Remove
chat_template_kwargsand run again withmax_tokens: 80: the model "thinks" andcontentcan stay empty, while the Python SDK exposes the reasoning indelta.model_extra["reasoning_content"]. - Anthropic variant: the same server answers on
/v1/messages; if you have theanthropicSDK, tryAnthropic(base_url="http://127.0.0.1:8080", api_key="nokey")with the mandatorymax_tokens. - Browser variant:
chat.mjsalso runs in an HTML page (samefetch); if the page is served from another origin remember--cors-origins.
Deliverable¶
A file lab-04-02.md with: the outputs of chat.py (full and stream, with the TTFT), those of chat.mjs, the error
line you got in step 8 and a three-line answer to the question: "which lines would you change to move the
same script from the local server to the real OpenAI API?".