package renderers
import (
"fmt"
"strings"
"github.com/ollama/ollama/api"
)
const (
qwen35ThinkOpenTag = ""
qwen35ThinkCloseTag = ""
qwen35ToolPostamble = `
If you choose to call a function ONLY reply in the following format with NO suffix:
value_1
This is the value for the second parameter
that can span
multiple lines
Reminder:
- Function calls MUST follow the specified format: an inner block must be nested within XML tags
- Required parameters MUST be specified
- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after
- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls
`
)
type Qwen35Renderer struct {
isThinking bool
emitEmptyThinkOnNoThink bool
useImgTags bool
}
func (r *Qwen35Renderer) renderContent(content api.Message, imageOffset int) (string, int) {
// This assumes all images are at the front of the message - same assumption as ollama/ollama/runner.go
var subSb strings.Builder
for range content.Images {
if r.useImgTags {
subSb.WriteString(fmt.Sprintf("[img-%d]", imageOffset))
imageOffset++
} else {
subSb.WriteString("<|vision_start|><|image_pad|><|vision_end|>")
}
}
// TODO: support videos
subSb.WriteString(content.Content)
return subSb.String(), imageOffset
}
func splitQwen35ReasoningContent(content, messageThinking string, isThinking bool) (reasoning string, remaining string) {
if isThinking && messageThinking != "" {
return strings.TrimSpace(messageThinking), content
}
if idx := strings.Index(content, qwen35ThinkCloseTag); idx != -1 {
before := content[:idx]
if open := strings.LastIndex(before, qwen35ThinkOpenTag); open != -1 {
reasoning = before[open+len(qwen35ThinkOpenTag):]
} else {
reasoning = before
}
content = strings.TrimLeft(content[idx+len(qwen35ThinkCloseTag):], "\n")
}
return strings.TrimSpace(reasoning), content
}
func (r *Qwen35Renderer) Render(messages []api.Message, tools []api.Tool, think *api.ThinkValue) (string, error) {
var sb strings.Builder
isThinking := r.isThinking
if think != nil {
isThinking = think.Bool()
}
if len(tools) > 0 {
sb.WriteString(imStartTag + "system\n")
sb.WriteString("# Tools\n\nYou have access to the following functions:\n\n")
for _, tool := range tools {
sb.WriteString("\n")
if b, err := marshalWithSpaces(tool); err == nil {
sb.Write(b)
}
}
sb.WriteString(qwen35ToolPostamble)
if len(messages) > 0 && messages[0].Role == "system" {
systemContent, _ := r.renderContent(messages[0], 0)
systemContent = strings.TrimSpace(systemContent)
if systemContent != "" {
sb.WriteString("\n\n")
sb.WriteString(systemContent)
}
}
sb.WriteString(imEndTag + "\n")
} else if len(messages) > 0 && messages[0].Role == "system" {
systemContent, _ := r.renderContent(messages[0], 0)
sb.WriteString(imStartTag + "system\n" + strings.TrimSpace(systemContent) + imEndTag + "\n")
}
multiStepTool := true
lastQueryIndex := len(messages) - 1 // so this is the last user message
for i := len(messages) - 1; i >= 0; i-- {
message := messages[i]
if multiStepTool && message.Role == "user" {
content, _ := r.renderContent(message, 0)
content = strings.TrimSpace(content)
if !(strings.HasPrefix(content, "") && strings.HasSuffix(content, "")) {
multiStepTool = false
lastQueryIndex = i
}
}
}
imageOffset := 0
for i, message := range messages {
content, nextImageOffset := r.renderContent(message, imageOffset)
imageOffset = nextImageOffset
content = strings.TrimSpace(content)
lastMessage := i == len(messages)-1
prefill := lastMessage && message.Role == "assistant"
if message.Role == "user" || (message.Role == "system" && i != 0) {
sb.WriteString(imStartTag + message.Role + "\n" + content + imEndTag + "\n")
} else if message.Role == "assistant" {
contentReasoning, content := splitQwen35ReasoningContent(content, message.Thinking, isThinking)
if isThinking && i > lastQueryIndex {
sb.WriteString(imStartTag + message.Role + "\n\n" + contentReasoning + "\n\n\n" + content)
} else {
sb.WriteString(imStartTag + message.Role + "\n" + content)
}
if len(message.ToolCalls) > 0 {
for j, toolCall := range message.ToolCalls {
if j == 0 {
if strings.TrimSpace(content) != "" {
sb.WriteString("\n\n")
}
} else {
sb.WriteString("\n")
}
sb.WriteString("\n\n")
for name, value := range toolCall.Function.Arguments.All() {
sb.WriteString("\n")
sb.WriteString(formatToolCallArgument(value))
sb.WriteString("\n\n")
}
sb.WriteString("\n")
}
}
if !prefill {
sb.WriteString(imEndTag + "\n")
}
} else if message.Role == "tool" {
if i == 0 || messages[i-1].Role != "tool" {
sb.WriteString(imStartTag + "user")
}
sb.WriteString("\n\n" + content + "\n")
if i == len(messages)-1 || messages[i+1].Role != "tool" {
sb.WriteString(imEndTag + "\n")
}
}
// prefill at the end
if lastMessage && !prefill {
sb.WriteString(imStartTag + "assistant\n")
if isThinking {
sb.WriteString("\n")
} else if r.emitEmptyThinkOnNoThink {
sb.WriteString("\n\n\n\n")
}
}
}
return sb.String(), nil
}