DeepseekV3 family renderer (#13180)

This commit is contained in:
Grace 2025-12-15 14:50:52 -08:00 committed by GitHub
parent aacd1cb394
commit 2c639431b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 616 additions and 0 deletions

View File

@ -0,0 +1,121 @@
package renderers
import (
"encoding/json"
"strings"
"github.com/ollama/ollama/api"
)
type DeepSeek3Variant int
const (
Deepseek31 DeepSeek3Variant = iota
)
type DeepSeek3Renderer struct {
IsThinking bool
Variant DeepSeek3Variant
}
func (r *DeepSeek3Renderer) Render(messages []api.Message, tools []api.Tool, thinkValue *api.ThinkValue) (string, error) {
var sb strings.Builder
// thinking is enabled: model must support it AND user must request it
thinking := r.IsThinking && (thinkValue != nil && thinkValue.Bool())
// extract system messages first
var systemPrompt strings.Builder
isFirstSystemPrompt := true
for _, message := range messages {
if message.Role == "system" {
if isFirstSystemPrompt {
systemPrompt.WriteString(message.Content)
isFirstSystemPrompt = false
} else {
systemPrompt.WriteString("\n\n" + message.Content)
}
}
}
sb.WriteString("<begin▁of▁sentence>" + systemPrompt.String())
// state tracking
isTool := false
isLastUser := false
for _, message := range messages {
switch message.Role {
case "user":
isTool = false
isLastUser = true
sb.WriteString("<User>" + message.Content)
case "assistant":
if len(message.ToolCalls) > 0 {
if isLastUser {
sb.WriteString("<Assistant></think>")
}
isLastUser = false
isTool = false
if message.Content != "" {
sb.WriteString(message.Content)
}
sb.WriteString("<tool▁calls▁begin>")
for _, toolCall := range message.ToolCalls {
sb.WriteString("<tool▁call▁begin>" + toolCall.Function.Name + "<tool▁sep>")
argsJSON, _ := json.Marshal(toolCall.Function.Arguments)
sb.WriteString(string(argsJSON))
sb.WriteString("<tool▁call▁end>")
}
sb.WriteString("<tool▁calls▁end><end▁of▁sentence>")
} else {
if isLastUser {
sb.WriteString("<Assistant>")
// message["prefix"] is defined and message["prefix"] and thinking
// message.Thinking != "" represents message["prefix"] being defined
if message.Thinking != "" && thinking {
sb.WriteString("<think>")
} else {
sb.WriteString("</think>")
}
}
isLastUser = false
content := message.Content
if isTool {
sb.WriteString(content + "<end▁of▁sentence>")
isTool = false
} else {
if strings.Contains(content, "</think>") {
parts := strings.SplitN(content, "</think>", 2)
if len(parts) > 1 {
content = parts[1]
}
}
sb.WriteString(content + "<end▁of▁sentence>")
}
}
case "tool":
isLastUser = false
isTool = true
sb.WriteString("<tool▁output▁begin>" + message.Content + "<tool▁output▁end>")
}
}
if isLastUser && !isTool {
sb.WriteString("<Assistant>")
if thinking {
sb.WriteString("<think>")
} else {
sb.WriteString("</think>")
}
}
return sb.String(), nil
}

View File

@ -0,0 +1,492 @@
package renderers
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/ollama/ollama/api"
)
func TestDeepSeekRenderer(t *testing.T) {
tests := []struct {
name string
messages []api.Message
tools []api.Tool
thinkValue *api.ThinkValue
expected string
}{
{
name: "basic user message",
messages: []api.Message{
{Role: "user", Content: "Hello, how are you?"},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>Hello, how are you?<Assistant></think>`,
},
{
name: "basic with system message",
messages: []api.Message{
{Role: "system", Content: "You are a helpful assistant."},
{Role: "user", Content: "Hello, how are you?"},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence>You are a helpful assistant.<User>Hello, how are you?<Assistant></think>`,
},
{
name: "multiple system messages",
messages: []api.Message{
{Role: "system", Content: "First instruction"},
{Role: "system", Content: "Second instruction"},
{Role: "user", Content: "Hello"},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<beginofsentence>First instruction
Second instruction<User>Hello<Assistant></think>`,
},
{
name: "thinking enabled",
messages: []api.Message{
{Role: "user", Content: "Hello, how are you?"},
},
thinkValue: &api.ThinkValue{Value: true},
expected: `<begin▁of▁sentence><User>Hello, how are you?<Assistant><think>`,
},
{
name: "thinking enabled with system",
messages: []api.Message{
{Role: "system", Content: "You are a helpful assistant."},
{Role: "user", Content: "Hello, how are you?"},
},
thinkValue: &api.ThinkValue{Value: true},
expected: `<begin▁of▁sentence>You are a helpful assistant.<User>Hello, how are you?<Assistant><think>`,
},
{
name: "conversation with assistant response",
messages: []api.Message{
{Role: "user", Content: "What is the capital of France?"},
{Role: "assistant", Content: "The capital of France is Paris."},
{Role: "user", Content: "Fantastic!"},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>What is the capital of France?<Assistant></think>The capital of France is Paris.<end▁of▁sentence><User>Fantastic!<Assistant></think>`,
},
{
name: "assistant with tool calls",
messages: []api.Message{
{Role: "user", Content: "What's the weather?"},
{
Role: "assistant",
ToolCalls: []api.ToolCall{
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
"location": "Paris",
},
},
},
},
},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>What's the weather?<Assistant></think><tool▁calls▁begin><tool▁call▁begin>get_weather<tool▁sep>{"location":"Paris"}<tool▁call▁end><tool▁calls▁end><end▁of▁sentence>`,
},
{
name: "assistant with content and tool calls",
messages: []api.Message{
{Role: "user", Content: "What's the weather in Paris?"},
{
Role: "assistant",
Content: "I'll check the weather for you.",
ToolCalls: []api.ToolCall{
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
"location": "Paris",
},
},
},
},
},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>What's the weather in Paris?<Assistant></think>I'll check the weather for you.<tool▁calls▁begin><tool▁call▁begin>get_weather<tool▁sep>{"location":"Paris"}<tool▁call▁end><tool▁calls▁end><end▁of▁sentence>`,
},
{
name: "tool response",
messages: []api.Message{
{Role: "user", Content: "What's the weather?"},
{
Role: "assistant",
ToolCalls: []api.ToolCall{
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
"location": "Paris",
},
},
},
},
},
{Role: "tool", Content: "Temperature: 22°C, Sunny"},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>What's the weather?<Assistant></think><tool▁calls▁begin><tool▁call▁begin>get_weather<tool▁sep>{"location":"Paris"}<tool▁call▁end><tool▁calls▁end><end▁of▁sentence><tool▁output▁begin>Temperature: 22°C, Sunny<tool▁output▁end>`,
},
{
name: "multiple tool calls",
messages: []api.Message{
{Role: "user", Content: "Get weather for Paris and London"},
{
Role: "assistant",
ToolCalls: []api.ToolCall{
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
"location": "Paris",
},
},
},
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
"location": "London",
},
},
},
},
},
{Role: "tool", Content: "Paris: 22°C, Sunny"},
{Role: "tool", Content: "London: 18°C, Cloudy"},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>Get weather for Paris and London<Assistant></think><tool▁calls▁begin><tool▁call▁begin>get_weather<tool▁sep>{"location":"Paris"}<tool▁call▁end><tool▁call▁begin>get_weather<tool▁sep>{"location":"London"}<tool▁call▁end><tool▁calls▁end><end▁of▁sentence><tool▁output▁begin>Paris: 22°C, Sunny<tool▁output▁end><tool▁output▁begin>London: 18°C, Cloudy<tool▁output▁end>`,
},
{
name: "content with </think> tag removal",
messages: []api.Message{
{Role: "user", Content: "Think about this"},
{Role: "assistant", Content: "I'm thinking about this.</think>The answer is 42."},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>Think about this<Assistant></think>The answer is 42.<end▁of▁sentence>`,
},
{
name: "empty system message",
messages: []api.Message{
{Role: "system", Content: ""},
{Role: "user", Content: "Hello"},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>Hello<Assistant></think>`,
},
{
name: "empty assistant content",
messages: []api.Message{
{Role: "user", Content: "Hello"},
{Role: "assistant", Content: ""},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>Hello<Assistant></think><end▁of▁sentence>`,
},
{
name: "special characters",
messages: []api.Message{
{Role: "user", Content: "What about <|special|> tokens and \"quotes\"?"},
{Role: "assistant", Content: "They're handled normally."},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>What about <|special|> tokens and "quotes"?<Assistant></think>They're handled normally.<end▁of▁sentence>`,
},
{
name: "tool calls with null content",
messages: []api.Message{
{Role: "user", Content: "Get weather"},
{
Role: "assistant",
ToolCalls: []api.ToolCall{
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
"location": "Paris",
},
},
},
},
},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>Get weather<Assistant></think><tool▁calls▁begin><tool▁call▁begin>get_weather<tool▁sep>{"location":"Paris"}<tool▁call▁end><tool▁calls▁end><end▁of▁sentence>`,
},
{
name: "assistant after tool context",
messages: []api.Message{
{Role: "user", Content: "Process data"},
{
Role: "assistant",
ToolCalls: []api.ToolCall{
{
Function: api.ToolCallFunction{
Name: "process",
Arguments: api.ToolCallFunctionArguments{
"data": "test",
},
},
},
},
},
{Role: "tool", Content: "Success"},
{Role: "assistant", Content: "Done"},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>Process data<Assistant></think><tool▁calls▁begin><tool▁call▁begin>process<tool▁sep>{"data":"test"}<tool▁call▁end><tool▁calls▁end><end▁of▁sentence><tool▁output▁begin>Success<tool▁output▁end>Done<end▁of▁sentence>`,
},
{
name: "no messages",
messages: []api.Message{},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence>`,
},
{
name: "only system messages",
messages: []api.Message{
{Role: "system", Content: "System instruction"},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence>System instruction`,
},
{
name: "multiple think tags in content",
messages: []api.Message{
{Role: "user", Content: "Complex question"},
{Role: "assistant", Content: "First thought</think>Second thought</think>Final answer"},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>Complex question<Assistant></think>Second thought</think>Final answer<end▁of▁sentence>`,
},
{
name: "thinking enabled after tool call - should render thinking",
messages: []api.Message{
{Role: "user", Content: "What's the weather in Paris?"},
{
Role: "assistant",
ToolCalls: []api.ToolCall{
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
"location": "Paris",
},
},
},
},
},
{Role: "tool", Content: "Temperature: 22°C, Sunny"},
{Role: "assistant", Content: "Based on the weather data, it's sunny in Paris."},
{Role: "user", Content: "Now tell me about London weather too."},
},
thinkValue: &api.ThinkValue{Value: true},
expected: `<begin▁of▁sentence><User>What's the weather in Paris?<Assistant></think><tool▁calls▁begin><tool▁call▁begin>get_weather<tool▁sep>{"location":"Paris"}<tool▁call▁end><tool▁calls▁end><end▁of▁sentence><tool▁output▁begin>Temperature: 22°C, Sunny<tool▁output▁end>Based on the weather data, it's sunny in Paris.<end▁of▁sentence><User>Now tell me about London weather too.<Assistant><think>`,
},
{
name: "thinking disabled after tool call - should not render thinking",
messages: []api.Message{
{Role: "user", Content: "What's the weather in Paris?"},
{
Role: "assistant",
ToolCalls: []api.ToolCall{
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
"location": "Paris",
},
},
},
},
},
{Role: "tool", Content: "Temperature: 22°C, Sunny"},
{Role: "assistant", Content: "Based on the weather data, it's sunny in Paris."},
{Role: "user", Content: "Now tell me about London weather too."},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>What's the weather in Paris?<Assistant></think><tool▁calls▁begin><tool▁call▁begin>get_weather<tool▁sep>{"location":"Paris"}<tool▁call▁end><tool▁calls▁end><end▁of▁sentence><tool▁output▁begin>Temperature: 22°C, Sunny<tool▁output▁end>Based on the weather data, it's sunny in Paris.<end▁of▁sentence><User>Now tell me about London weather too.<Assistant></think>`,
},
{
name: "thinking enabled but messages without thinking content",
messages: []api.Message{
{Role: "user", Content: "First question about cats"},
{Role: "assistant", Content: "Cats are wonderful pets."},
{Role: "user", Content: "What about dogs?"},
{Role: "assistant", Content: "Dogs are loyal companions."},
{Role: "user", Content: "Final question about birds"},
},
thinkValue: &api.ThinkValue{Value: true},
expected: `<begin▁of▁sentence><User>First question about cats<Assistant></think>Cats are wonderful pets.<end▁of▁sentence><User>What about dogs?<Assistant></think>Dogs are loyal companions.<end▁of▁sentence><User>Final question about birds<Assistant><think>`,
},
{
name: "thinking disabled for all assistant responses",
messages: []api.Message{
{Role: "user", Content: "First question about cats"},
{Role: "assistant", Content: "Cats are wonderful pets."},
{Role: "user", Content: "What about dogs?"},
{Role: "assistant", Content: "Dogs are loyal companions."},
{Role: "user", Content: "Final question about birds"},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>First question about cats<Assistant></think>Cats are wonderful pets.<end▁of▁sentence><User>What about dogs?<Assistant></think>Dogs are loyal companions.<end▁of▁sentence><User>Final question about birds<Assistant></think>`,
},
{
name: "complex conversation with tool calls and thinking enabled",
messages: []api.Message{
{Role: "user", Content: "Tell me about the weather"},
{Role: "assistant", Content: "I'll check the weather for you."},
{Role: "user", Content: "Actually, get Paris weather specifically"},
{
Role: "assistant",
ToolCalls: []api.ToolCall{
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
"location": "Paris",
},
},
},
},
},
{Role: "tool", Content: "Paris: 22°C, Sunny"},
{Role: "assistant", Content: "The weather in Paris is great!"},
{Role: "user", Content: "What about the forecast for tomorrow?"},
},
thinkValue: &api.ThinkValue{Value: true},
expected: `<begin▁of▁sentence><User>Tell me about the weather<Assistant></think>I'll check the weather for you.<end▁of▁sentence><User>Actually, get Paris weather specifically<Assistant></think><tool▁calls▁begin><tool▁call▁begin>get_weather<tool▁sep>{"location":"Paris"}<tool▁call▁end><tool▁calls▁end><end▁of▁sentence><tool▁output▁begin>Paris: 22°C, Sunny<tool▁output▁end>The weather in Paris is great!<end▁of▁sentence><User>What about the forecast for tomorrow?<Assistant><think>`,
},
{
name: "tool call without subsequent user message - no thinking",
messages: []api.Message{
{Role: "user", Content: "Get the weather"},
{
Role: "assistant",
ToolCalls: []api.ToolCall{
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
"location": "Paris",
},
},
},
},
},
{Role: "tool", Content: "22°C, Sunny"},
},
thinkValue: &api.ThinkValue{Value: true},
expected: `<begin▁of▁sentence><User>Get the weather<Assistant></think><tool▁calls▁begin><tool▁call▁begin>get_weather<tool▁sep>{"location":"Paris"}<tool▁call▁end><tool▁calls▁end><end▁of▁sentence><tool▁output▁begin>22°C, Sunny<tool▁output▁end>`,
},
{
name: "messages with thinking content, no thinking in render",
messages: []api.Message{
{Role: "user", Content: "Solve this math problem: 15 * 23"},
{
Role: "assistant",
Content: "The answer is 345.",
Thinking: "Let me calculate 15 * 23. I can break this down: 15 * 20 = 300, and 15 * 3 = 45, so 300 + 45 = 345.",
},
{Role: "user", Content: "What about 12 * 34?"},
},
thinkValue: &api.ThinkValue{Value: false},
expected: `<begin▁of▁sentence><User>Solve this math problem: 15 * 23<Assistant></think>The answer is 345.<end▁of▁sentence><User>What about 12 * 34?<Assistant></think>`,
},
{
name: "conversation with mix of thinking and no thinking",
messages: []api.Message{
{Role: "user", Content: "Explain quantum physics"},
{
Role: "assistant",
Content: "Quantum physics is the study of matter and energy at the smallest scales.",
Thinking: "This is a complex topic. I should start with basic concepts and avoid overwhelming technical details.",
},
{Role: "user", Content: "What about photons?"},
{
Role: "assistant",
Content: "Photons are particles of light with no mass.",
},
{Role: "user", Content: "How do they interact with matter?"},
},
thinkValue: &api.ThinkValue{Value: true},
expected: `<begin▁of▁sentence><User>Explain quantum physics<Assistant><think>Quantum physics is the study of matter and energy at the smallest scales.<end▁of▁sentence><User>What about photons?<Assistant></think>Photons are particles of light with no mass.<end▁of▁sentence><User>How do they interact with matter?<Assistant><think>`,
},
{
name: "tool call with thinking content in response",
messages: []api.Message{
{Role: "user", Content: "What's the weather in Tokyo and New York?"},
{
Role: "assistant",
Content: "I'll check the weather for both cities.",
Thinking: "I need to call the weather API for two different cities. Let me make parallel calls.",
ToolCalls: []api.ToolCall{
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
"location": "Tokyo",
},
},
},
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
"location": "New York",
},
},
},
},
},
{Role: "tool", Content: "Tokyo: 18°C, Cloudy"},
{Role: "tool", Content: "New York: 22°C, Sunny"},
{
Role: "assistant",
Content: "Based on the weather data: Tokyo is cloudy at 18°C, while New York is sunny at 22°C.",
Thinking: "The data shows a nice contrast between the two cities. Tokyo is cooler and overcast while NYC has better weather.",
},
},
thinkValue: &api.ThinkValue{Value: true},
expected: `<begin▁of▁sentence><User>What's the weather in Tokyo and New York?<Assistant></think>I'll check the weather for both cities.<tool▁calls▁begin><tool▁call▁begin>get_weather<tool▁sep>{"location":"Tokyo"}<tool▁call▁end><tool▁call▁begin>get_weather<tool▁sep>{"location":"New York"}<tool▁call▁end><tool▁calls▁end><end▁of▁sentence><tool▁output▁begin>Tokyo: 18°C, Cloudy<tool▁output▁end><tool▁output▁begin>New York: 22°C, Sunny<tool▁output▁end>Based on the weather data: Tokyo is cloudy at 18°C, while New York is sunny at 22°C.<end▁of▁sentence>`,
},
{
name: "empty thinking field",
messages: []api.Message{
{Role: "user", Content: "Simple question"},
{
Role: "assistant",
Content: "Simple answer.",
Thinking: "", // Empty thinking content
},
},
thinkValue: &api.ThinkValue{Value: true},
expected: `<begin▁of▁sentence><User>Simple question<Assistant></think>Simple answer.<end▁of▁sentence>`,
},
}
renderer := &DeepSeek3Renderer{IsThinking: true, Variant: Deepseek31}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rendered, err := renderer.Render(tt.messages, tt.tools, tt.thinkValue)
if err != nil {
t.Fatalf("Render() error = %v", err)
}
if diff := cmp.Diff(tt.expected, rendered); diff != "" {
t.Errorf("Render() mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@ -59,6 +59,9 @@ func rendererForName(name string) Renderer {
case "cogito":
renderer := &CogitoRenderer{isThinking: true}
return renderer
case "deepseek-v3.1":
renderer := &DeepSeek3Renderer{IsThinking: true, Variant: Deepseek31}
return renderer
case "olmo3":
renderer := &Olmo3Renderer{UseExtendedSystemMessage: false}
return renderer