From 185ff00c62853c376d1a7dbeac98d0e60517b2fc Mon Sep 17 00:00:00 2001 From: alkinun Date: Thu, 28 May 2026 12:28:52 +0300 Subject: [PATCH] Fix non-streaming GGUF chat completion usage (#5781) * Fix GGUF non-stream chat completion usage * Handle nullable GGUF completion usage --------- Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com> --- studio/backend/routes/inference.py | 14 +++- .../tests/test_gguf_completion_usage.py | 78 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 studio/backend/tests/test_gguf_completion_usage.py diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 259337616c..7d1c7b2488 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -3091,9 +3091,12 @@ async def openai_chat_completions( else: try: full_text = "" + completion_usage = None for token in gguf_generate(): if isinstance(token, dict): - continue # skip metadata dict in non-streaming path + if token.get("type") == "metadata": + completion_usage = token.get("usage") + continue full_text = token response = ChatCompletion( @@ -3106,6 +3109,15 @@ async def openai_chat_completions( finish_reason = "stop", ) ], + usage = CompletionUsage( + prompt_tokens = (completion_usage or {}).get("prompt_tokens") + or 0, + completion_tokens = (completion_usage or {}).get( + "completion_tokens" + ) + or 0, + total_tokens = (completion_usage or {}).get("total_tokens") or 0, + ), ) return JSONResponse(content = response.model_dump()) diff --git a/studio/backend/tests/test_gguf_completion_usage.py b/studio/backend/tests/test_gguf_completion_usage.py new file mode 100644 index 0000000000..b8cfaee7c7 --- /dev/null +++ b/studio/backend/tests/test_gguf_completion_usage.py @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. + +"""Regression tests for GGUF non-streaming chat completion usage.""" + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from auth.authentication import get_current_subject +import routes.inference as inference_route + + +class _GgufBackend: + is_loaded = True + model_identifier = "test/model.gguf" + _is_audio = False + is_vision = False + supports_tools = False + + def __init__(self, usage): + self.usage = usage + + def generate_chat_completion(self, **kwargs): + yield "answer" + yield { + "type": "metadata", + "usage": self.usage, + "timings": {"prompt_n": 23, "predicted_n": 1283}, + } + + +def _request_completion(monkeypatch, usage): + monkeypatch.setattr( + inference_route, "get_llama_cpp_backend", lambda: _GgufBackend(usage) + ) + monkeypatch.setattr( + inference_route, "_effective_enable_tools", lambda payload: False + ) + + app = FastAPI() + app.include_router(inference_route.router) + app.dependency_overrides[get_current_subject] = lambda: "test-user" + + return TestClient(app).post( + "/chat/completions", + json = { + "messages": [{"role": "user", "content": "Why is the sky blue?"}], + "stream": False, + }, + ) + + +def test_non_streaming_gguf_completion_includes_generated_usage(monkeypatch): + response = _request_completion( + monkeypatch, + {"prompt_tokens": 23, "completion_tokens": 1283, "total_tokens": 1306}, + ) + + assert response.status_code == 200 + assert response.json()["usage"] == { + "prompt_tokens": 23, + "completion_tokens": 1283, + "total_tokens": 1306, + } + + +def test_non_streaming_gguf_completion_defaults_nullable_usage_to_zero(monkeypatch): + response = _request_completion( + monkeypatch, + {"prompt_tokens": None, "completion_tokens": 1283, "total_tokens": None}, + ) + + assert response.status_code == 200 + assert response.json()["usage"] == { + "prompt_tokens": 0, + "completion_tokens": 1283, + "total_tokens": 0, + }