server: apply format constraint for all thinking parsers when think=false (#15901)

This commit is contained in:
Arkadeep Dutta
2026-07-07 13:54:50 -05:00
committed by GitHub
parent f3d69a3dee
commit 892e7f6be6
2 changed files with 115 additions and 114 deletions
+1 -3
View File
@@ -2766,9 +2766,7 @@ func (s *Server) ChatHandler(c *gin.Context) {
// current approach uses the transition from parsed thinking content to
// parsed non-thinking content as the signal to turn constraining on
// TODO(parthsareen): temporary fix for https://github.com/ollama/ollama/issues/15260.
// To revisit for other models and have a consistent pattern across models through parsers.
forceImmediate := m.Config.Parser == "gemma4" && req.Think != nil && !req.Think.Bool()
forceImmediate := builtinParser != nil && builtinParser.HasThinkingSupport() && req.Think != nil && !req.Think.Bool()
if req.Format != nil && structuredOutputsState == structuredOutputsState_None && !forceImmediate && ((builtinParser != nil || thinkingState != nil) && slices.Contains(m.Capabilities(), model.CapabilityThinking)) {
currentFormat = nil
}
+15 -12
View File
@@ -2811,15 +2811,18 @@ func TestChatWithPromptEndingInThinkTag(t *testing.T) {
}
// TestChatFormatWithThinkFalse verifies that when a model uses a builtin
// parser that supports thinking (e.g. gemma4) and the request explicitly
// disables thinking (think=false), the format constraint is passed to the
// first and only completion call. Previously, format was deferred for all
// thinking-capable parsers and only re-applied after an end-of-thinking
// transition — a transition that never fires when thinking is off. See
// https://github.com/ollama/ollama/issues/15260.
// parser that supports thinking and the request explicitly disables thinking
// (think=false), the format constraint is passed to the first and only
// completion call. Previously, format was deferred for all thinking-capable
// parsers and only re-applied after an end-of-thinking transition -- a
// transition that never fires when thinking is off. See
// https://github.com/ollama/ollama/issues/15260 and
// https://github.com/ollama/ollama/issues/14645.
func TestChatFormatWithThinkFalse(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, parserName := range []string{"gemma4", "qwen3.5", "qwen3-thinking"} {
t.Run(parserName, func(t *testing.T) {
mock := &mockRunner{
CompletionResponse: llm.CompletionResponse{
Done: true,
@@ -2876,13 +2879,11 @@ func TestChatFormatWithThinkFalse(t *testing.T) {
{Name: "output.weight", Shape: []uint64{1}, WriterTo: bytes.NewReader(make([]byte, 4))},
})
// Use the gemma4 builtin parser — it reports HasThinkingSupport=true, which
// adds CapabilityThinking to the model and previously triggered deferral of
// the format even when the user passed think=false.
modelName := "test-" + parserName + "-parser"
w := createRequest(t, s.CreateHandler, api.CreateRequest{
Model: "test-gemma4-parser",
Model: modelName,
Files: map[string]string{"file.gguf": digest},
Parser: "gemma4",
Parser: parserName,
Template: `{{- range .Messages }}{{ .Role }}: {{ .Content }}{{ end }}`,
Stream: &stream,
})
@@ -2916,7 +2917,7 @@ func TestChatFormatWithThinkFalse(t *testing.T) {
streamRequest := false
think := false
w = createRequest(t, s.ChatHandler, api.ChatRequest{
Model: "test-gemma4-parser",
Model: modelName,
Messages: []api.Message{{Role: "user", Content: "Respond in JSON."}},
Think: &api.ThinkValue{Value: think},
Stream: &streamRequest,
@@ -2934,6 +2935,8 @@ func TestChatFormatWithThinkFalse(t *testing.T) {
if !bytes.Equal([]byte(format), []byte(requests[0].Format)) {
t.Errorf("expected first completion format to match the request format, got %q", string(requests[0].Format))
}
})
}
}
func TestGenerateUnload(t *testing.T) {