#18394·ollama

Include the served manifest digest in local /api/chat responses (tested v0.34.0 patch)

Author: Belphareon-bakCreated Sep 11, 2026Updated Sep 17, 2026

Local evaluation clients need to identify the exact artifact that served a chat response. /api/tags before and after inference cannot distinguish an A -> B -> A tag replacement during the request. A model name alone is therefore insufficient to bind a score to an artifact.

Proposed additive response fields for local /api/chat:

  • digest: full local manifest digest from the same *Model selected for this request, in streaming and non-streaming responses, covering native and template paths.
  • provider_version: serving Ollama version, useful for keeping measurements from different inference-engine versions separate. This field could be considered separately if preferred.

The patch below is based on v0.34.0 (d8ab4b4f0ca24b51d3a46b3bf4f462e58ce66b1f). It adds no inventory polling or client lease requirement. Remote/cloud proxy responses are not relabeled as a locally served artifact.

Validation on Linux x86_64, RTX 3090, Go 1.26.7, with the matching v0.34.0 native payload:

  • go test ./server -run '^(TestGenerateChat|TestChatHandlerChatTemplateRoute)$' -count=1 passes with an isolated model directory.
  • Phi-4 14B and Qwen3.5 27B return the expected full manifest digest and serving version in non-streaming responses and every streaming frame.
  • Both models completed actual GPU inference; the local evaluation client rejects a missing/mismatched digest or serving version.

This is a tested implementation proposal, not a claim that a response field protects against an untrusted server. The intended boundary is accidental/concurrent artifact replacement on a trusted local provider.

Applicable git-am patch against v0.34.0
From e8c2d86c3a056b58031c77ec5141a8ecd7cf923a Mon Sep 17 00:00:00 2001
From: Belphareon <[email protected]>
Date: Fri, 11 Sep 2026 21:00:00 +0000
Subject: [PATCH] server: attest chat manifest digest and serving provider
 version

---
 api/types.go                   |  6 ++++
 server/routes.go               | 60 ++++++++++++++++++++--------------
 server/routes_generate_test.go | 45 +++++++++++++++++++++++--
 3 files changed, 85 insertions(+), 26 deletions(-)

diff --git a/api/types.go b/api/types.go
index 7ea9022..beeeb85 100644
--- a/api/types.go
+++ b/api/types.go
@@ -521,6 +521,12 @@ type ChatResponse struct {
 	// Model is the model name that generated the response.
 	Model string `json:"model"`
 
+	// Digest is the exact local manifest digest selected for this response.
+	Digest string `json:"digest,omitempty"`
+
+	// ProviderVersion identifies the Ollama build serving this response.
+	ProviderVersion string `json:"provider_version,omitempty"`
+
 	// RemoteModel is the name of the upstream model that generated the response.
 	RemoteModel string `json:"remote_model,omitempty"`
 
diff --git a/server/routes.go b/server/routes.go
index 9b73139..eed328d 100644
--- a/server/routes.go
+++ b/server/routes.go
@@ -2520,11 +2520,13 @@ func (s *Server) ChatHandler(c *gin.Context) {
 		s.sched.expireRunner(m)
 
 		c.JSON(http.StatusOK, api.ChatResponse{
-			Model:      req.Model,
-			CreatedAt:  time.Now().UTC(),
-			Message:    api.Message{Role: "assistant"},
-			Done:       true,
-			DoneReason: "unload",
+			Digest:          m.Digest,
+			ProviderVersion: version.Version,
+			Model:           req.Model,
+			CreatedAt:       time.Now().UTC(),
+			Message:         api.Message{Role: "assistant"},
+			Done:            true,
+			DoneReason:      "unload",
 		})
 		return
 	}
@@ -2658,11 +2660,13 @@ func (s *Server) ChatHandler(c *gin.Context) {
 
 	if len(req.Messages) == 0 {
 		c.JSON(http.StatusOK, api.ChatResponse{
-			Model:      req.Model,
-			CreatedAt:  time.Now().UTC(),
-			Message:    api.Message{Role: "assistant"},
-			Done:       true,
-			DoneReason: "load",
+			Digest:          m.Digest,
+			ProviderVersion: version.Version,
+			Model:           req.Model,
+			CreatedAt:       time.Now().UTC(),
+			Message:         api.Message{Role: "assistant"},
+			Done:            true,
+			DoneReason:      "load",
 		})
 		return
 	}
@@ -2721,8 +2725,10 @@ func (s *Server) ChatHandler(c *gin.Context) {
 	// If debug mode is enabled, return the rendered template instead of calling the model
 	if req.DebugRenderOnly {
 		c.JSON(http.StatusOK, api.ChatResponse{
-			Model:     req.Model,
-			CreatedAt: time.Now().UTC(),
+			Digest:          m.Digest,
+			ProviderVersion: version.Version,
+			Model:           req.Model,
+			CreatedAt:       time.Now().UTC(),
 			DebugInfo: &api.DebugInfo{
 				RenderedTemplate: prompt,
 				ImageCount:       len(media),
@@ -2823,12 +2829,14 @@ func (s *Server) ChatHandler(c *gin.Context) {
 				}
 
 				res := api.ChatResponse{
-					Model:     req.Model,
-					CreatedAt: time.Now().UTC(),
-					Message:   api.Message{Role: "assistant", Content: r.Content},
-					Done:      r.Done,
-					Metrics:   metrics,
-					Logprobs:  toAPILogprobs(r.Logprobs),
+					Digest:          m.Digest,
+					ProviderVersion: version.Version,
+					Model:           req.Model,
+					CreatedAt:       time.Now().UTC(),
+					Message:         api.Message{Role: "assistant", Content: r.Content},
+					Done:            r.Done,
+					Metrics:         metrics,
+					Logprobs:        toAPILogprobs(r.Logprobs),
 				}
 
 				if r.Done {
@@ -3015,8 +3023,10 @@ func (s *Server) handleNativeChat(c *gin.Context, req api.ChatRequest, m *Model,
 		}
 
 		c.JSON(http.StatusOK, api.ChatResponse{
-			Model:     req.Model,
-			CreatedAt: time.Now().UTC(),
+			Digest:          m.Digest,
+			ProviderVersion: version.Version,
+			Model:           req.Model,
+			CreatedAt:       time.Now().UTC(),
 			DebugInfo: &api.DebugInfo{
 				RenderedTemplate: prompt,
 				ImageCount:       countChatImages(msgs),
@@ -3031,10 +3041,12 @@ func (s *Server) handleNativeChat(c *gin.Context, req api.ChatRequest, m *Model,
 
 		err := r.Chat(c.Request.Context(), nativeReq, func(r llm.ChatResponse) {
 			res := api.ChatResponse{
-				Model:     req.Model,
-				CreatedAt: time.Now().UTC(),
-				Message:   r.Message,
-				Done:      r.Done,
+				Digest:          m.Digest,
+				ProviderVersion: version.Version,
+				Model:           req.Model,
+				CreatedAt:       time.Now().UTC(),
+				Message:         r.Message,
+				Done:            r.Done,
 				Metrics: api.Metrics{
 					PromptEvalCount:       r.PromptEvalCount,
 					PromptEvalCachedCount: r.PromptEvalCachedCount,
diff --git a/server/routes_generate_test.go b/server/routes_generate_test.go
index f3decb8..3f81d9a 100644
--- a/server/routes_generate_test.go
+++ b/server/routes_generate_test.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"github.com/ollama/ollama/version"
 	"io"
 	"net/http"
 	"net/http/httptest"
@@ -312,6 +313,20 @@ func TestChatHandlerChatTemplateRoute(t *testing.T) {
 	if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil {
 		t.Fatal(err)
 	}
+	existingName, err := getExistingName(model.ParseName("chat-template"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	servedModel, err := GetModel(existingName.String())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if actual.ProviderVersion != version.Version {
+		t.Fatalf("provider version = %q, want %q", actual.ProviderVersion, version.Version)
+	}
+	if actual.Digest != servedModel.Digest {
+		t.Fatalf("response digest = %q, want exact manifest digest %q", actual.Digest, servedModel.Digest)
+	}
 	if actual.Message.Content != "chat template response" {
 		t.Fatalf("expected chat template response, got %q", actual.Message.Content)
 	}
@@ -833,6 +848,18 @@ func TestGenerateChat(t *testing.T) {
 	if w.Code != http.StatusOK {
 		t.Fatalf("expected status 200, got %d", w.Code)
 	}
+	expectedDigest := func(t *testing.T, modelName string) string {
+		t.Helper()
+		existingName, err := getExistingName(model.ParseName(modelName))
+		if err != nil {
+			t.Fatal(err)
+		}
+		servedModel, err := GetModel(existingName.String())
+		if err != nil {
+			t.Fatal(err)
+		}
+		return servedModel.Digest
+	}
 
 	t.Run("missing body", func(t *testing.T) {
 		w := createRequest(t, s.ChatHandler, nil)
@@ -935,6 +962,13 @@ func TestGenerateChat(t *testing.T) {
 		if actual.Model != "test" {
 			t.Errorf("expected model test, got %s", actual.Model)
 		}
+		wantDigest := expectedDigest(t, "test")
+		if actual.ProviderVersion != version.Version {
+			t.Fatalf("provider version = %q, want %q", actual.ProviderVersion, version.Version)
+		}
+		if actual.Digest != wantDigest {
+			t.Errorf("response digest = %q, want exact manifest digest %q", actual.Digest, wantDigest)
+		}
 
 		if !actual.Done {
 			t.Errorf("expected done true, got false")
@@ -945,7 +979,7 @@ func TestGenerateChat(t *testing.T) {
 		}
 	})
 
-	checkChatResponse := func(t *testing.T, body io.Reader, model, content string) {
+	checkChatResponse := func(t *testing.T, body io.Reader, modelName, content string) {
 		t.Helper()
 
 		var actual api.ChatResponse
@@ -953,9 +987,16 @@ func TestGenerateChat(t *testing.T) {
 			t.Fatal(err)
 		}
 
-		if actual.Model != model {
+		if actual.Model != modelName {
 			t.Errorf("expected model test, got %s", actual.Model)
 		}
+		wantDigest := expectedDigest(t, modelName)
+		if actual.ProviderVersion != version.Version {
+			t.Fatalf("provider version = %q, want %q", actual.ProviderVersion, version.Version)
+		}
+		if actual.Digest != wantDigest {
+			t.Errorf("response digest = %q, want exact manifest digest %q", actual.Digest, wantDigest)
+		}
 
 		if !actual.Done {
 			t.Errorf("expected done false, got true")
-- 
2.43.0