使用 Ollama Cloud 取得 Token 用量:釐清 Chat Completions 與 Responses API 的串流差異

最近我在整理「頁問」串接 Ollama Cloud 的 Token 用量資訊,先釐清了一個容易混淆的問題:使用 deepseek-v4.1-flash:1017 時,是不是一定要傳入 stream_options.include_usage: true,才拿得到輸入與輸出的 Token 數?

接著延伸到 Responses API,才發現這個問題不能只看模型名稱。同樣是串流回應,Chat Completions 與 Responses API 的用量回傳方式並不相同。如果把兩套 API 當成同一種格式處理,就可能收到完整回答,卻漏掉最後的用量資訊。

這篇文章整理我查核官方文件與原始碼後確認的差異,並提供請求與解析範例。重點是先辨識端點,再決定要送什麼參數,以及從哪個事件讀取用量。

Ollama Cloud 的兩條串流分別以獨立用量區塊與完成回應回傳 Token 統計的概念插畫


先確認你呼叫的是哪一個 API

Chat Completions 串流應明確要求回傳用量;Responses API 則不需要 stream_options.include_usage,用量放在完成回應中。

我會先用下表判斷應該檢查哪一段程式:

API 與模式 是否需要 stream_options.include_usage: true 用量位置 輸入/輸出欄位
/v1/chat/completions
串流
需要明確設定 最後的用量區塊之 usage prompt_tokens
completion_tokens
/v1/chat/completions
非串流
不需要 JSON 回應的 usage prompt_tokens
completion_tokens
/v1/responses
串流
不需要 response.completed 事件的 response.usage input_tokens
output_tokens
/v1/responses
非串流
不需要 JSON 回應的 usage input_tokens
output_tokens
原生 /api/chat,串流 不需要此參數 done: true 的最後一筆物件 prompt_eval_count
eval_count

前三種用量格式不要混用。尤其 Responses 串流的取值路徑是 event.response.usage,並不是 event.usage。

依照 Ollama 的 OpenAI 相容性文件,直接呼叫雲端時,Base URL 使用 https://ollama.com/v1,也支援無狀態的 Responses 請求。不過,支援 OpenAI 相容介面並不代表所有 OpenAI 功能都能使用;目前文件仍列有雲端 Responses 的功能限制。


使用 Chat Completions 時,明確要求串流用量

假設已將 Ollama API 金鑰設定在 OLLAMA_API_KEY 環境變數,請求可以這樣寫:

curl --no-buffer --fail-with-body https://ollama.com/v1/chat/completions \
    -H "Authorization: Bearer ${OLLAMA_API_KEY}" \
    -H 'Content-Type: application/json' \
    -d '{
        "model": "deepseek-v4.1-flash:1017",
        "messages": [
            { "role": "user", "content": "請用一句話介紹 Ollama。" }
        ],
        "stream": true,
        "stream_options": {
            "include_usage": true
        }
    }'

這裡真正影響串流用量回傳的是 stream_options.include_usage。Ollama 在 Chat Completions 支援欄位中列出了這個選項;我會明確傳入,避免依賴 SDK 或中介服務替我補上參數。

回應中最後的用量區塊可能像這樣,以下僅保留重點欄位:

data: {"choices":[],"usage":{"prompt_tokens":20,"completion_tokens":12,"total_tokens":32}}

data: [DONE]

choices 是空陣列,仍然可能是一筆重要的用量回應。 如果程式一看到 choices.length === 0 就跳過,最後的統計就會被自己丟掉。

另一個細節是,文字輸出結束的 finish_reason 與用量區塊可能分開送達。收到 finish_reason 時就停止讀取,也可能漏掉後面的 usage。這個行為可從 Ollama 的 Chat Completions 串流轉換實作確認。


改用 Responses API 時,讀取完成事件

Responses API 的請求不必加上 stream_options.include_usage:

curl --no-buffer --fail-with-body https://ollama.com/v1/responses \
    -H "Authorization: Bearer ${OLLAMA_API_KEY}" \
    -H 'Content-Type: application/json' \
    -d '{
        "model": "deepseek-v4.1-flash:1017",
        "input": "請用一句話介紹 Ollama。",
        "stream": true
    }'

這套 API 使用具名的 Server-Sent Events(SSE,伺服器傳送事件)。文字增量與完成事件有不同的 type,完成事件中的用量格式如下,同樣只保留重點欄位:

event: response.completed
data: {"type":"response.completed","response":{"status":"completed","usage":{"input_tokens":20,"output_tokens":12,"total_tokens":32}}}

我查到的 Ollama Responses 實作會在 processCompletion 建立用量物件,再放進 response.completed 事件的 response 中,這段流程沒有以 include_usage 作為開關。

因此,如果 Responses API 的完成事件沒有用量,我會先檢查原始事件、SDK 與中介轉換流程,確認欄位是否被丟掉。加入 Chat Completions 的用量參數,並不是 Responses API 的標準解法。


讓解析程式分別處理文字與用量

下面示範的是 SSE 已經完成分幀、data 已解析成 JSON 之後的事件處理。它不是完整的網路串流解析器:單次 reader.read() 取得的資料,可能只有半筆事件,也可能包含多筆事件,不能直接對每次讀到的字串呼叫 JSON.parse()。

如果專案已有 SSE 解析器,我會在事件處理層保留 API 差異,再統一成應用程式使用的欄位:

let tokenUsage = null;

function handleChatCompletionChunk(chunk) {
    // 先處理用量,避免 choices 為空時漏掉統計。
    if (chunk.usage) {
        tokenUsage = {
            inputTokens: chunk.usage.prompt_tokens,
            outputTokens: chunk.usage.completion_tokens,
            totalTokens: chunk.usage.total_tokens
        };
    }

    const text = chunk.choices?.[0]?.delta?.content;
    if (typeof text === 'string') {
        console.log(text);
    }
}

function handleResponsesEvent(event) {
    if (event.type === 'response.output_text.delta') {
        console.log(event.delta);
    }

    if (event.type === 'response.completed' && event.response?.usage) {
        const usage = event.response.usage;
        tokenUsage = {
            inputTokens: usage.input_tokens,
            outputTokens: usage.output_tokens,
            totalTokens: usage.total_tokens
        };
    }
}

這兩個函式是兩種 API 的獨立處理入口,每次請求開始時都應重設 tokenUsage。Chat Completions 的 [DONE] 則交由外層 SSE 讀取流程辨識,不要當成 JSON 傳進來。

我會讓「尚未取得用量」保留為 null。如果一開始就填入 0,串流中斷、使用者取消,或服務沒有回傳統計時,畫面就會把未知用量顯示成零。

完整回答文字已到齊,不代表用量資訊也已到齊。 取消或中斷時可能收不到最後的統計,介面應顯示「未取得用量」,避免把缺少資料當成沒有消耗 Token。


使用原生 API 時,改看 Ollama 的計數欄位

如果呼叫的是原生 /api/chat,就不適用上述兩套 OpenAI 用量欄位。

Ollama 原生 Chat API 文件列出的欄位是:

  • prompt_eval_count:輸入 Token 數。
  • eval_count:輸出 Token 數。
  • done:是否已完成。

原生串流使用 NDJSON,每行是一筆 JSON。用量應從最後完成的物件讀取,不要套用 SSE 的 data: 或 event: 解析規則。若兩個計數都有提供,才將它們相加作為總 Token 數。


驗證時,同時檢查有資料與缺少資料的情境

實際整合時,我會先保存已遮蔽敏感資訊的原始回應,確認伺服器有沒有回傳用量,再對照應用程式解析後的結果。可以依照下表檢查:

情境 檢查重點
Chat Completions 串流,帶入 include_usage: true 完整讀完,確認最後是否有 usage
Chat Completions 串流,省略該選項 記錄實際行為;SDK 或代理可能自動補入,不能只看呼叫端程式碼判斷
Chat Completions 用量區塊的 choices 為空 仍須保存用量
Responses 串流,未加入 include_usage 確認 response.completed.response.usage
Responses 非串流 確認 JSON 根層的 usage
使用者取消,或未收到最終用量就斷線 保留未知狀態,不能顯示成零
連續發出兩次請求 第二次不能沿用第一次的統計

這份表格是後續實測清單,並不是本文已完成的測試紀錄。若要確認特定模型與端點的實際行為,仍須使用自己的帳號執行請求。


總結

這次整理最值得記錄的地方,是取得 Token 用量需要同時對齊請求設定與回應解析方式。對 Ollama 的 Chat Completions 串流,我會明確設定 stream_options.include_usage: true,並持續讀取到最後的用量區塊。

改用 Responses API 時,就直接處理 response.completed 中的 response.usage。兩套 API 的欄位名稱也不同,要分別讀取後再轉成應用程式的共用格式。

相關連結

留言評論