package renderers
import (
"encoding/json"
"fmt"
"strings"
"github.com/ollama/ollama/api"
)
type Nemotron3NanoRenderer struct {
IsThinking bool
}
func (r *Nemotron3NanoRenderer) 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
enableThinking := r.IsThinking && (thinkValue != nil && thinkValue.Bool())
// Extract system message if present
var systemMessage string
var loopMessages []api.Message
if len(messages) > 0 && messages[0].Role == "system" {
systemMessage = messages[0].Content
loopMessages = messages[1:]
} else {
loopMessages = messages
}
// Find last user message index for thinking truncation
lastUserIdx := -1
for i, msg := range loopMessages {
if msg.Role == "user" {
lastUserIdx = i
}
}
sb.WriteString("<|im_start|>system\n")
if systemMessage != "" {
sb.WriteString(systemMessage)
}
if len(tools) > 0 {
if systemMessage != "" {
sb.WriteString("\n\n")
}
sb.WriteString(r.renderTools(tools))
}
sb.WriteString("<|im_end|>\n")
for i, message := range loopMessages {
switch message.Role {
case "assistant":
// Build content with thinking tags
content := r.buildContent(message)
shouldTruncate := i < lastUserIdx
if len(message.ToolCalls) > 0 {
sb.WriteString("<|im_start|>assistant\n")
sb.WriteString(r.formatContent(content, shouldTruncate, true))
r.writeToolCalls(&sb, message.ToolCalls)
sb.WriteString("<|im_end|>\n")
} else {
formatted := r.formatContent(content, shouldTruncate, false)
sb.WriteString("<|im_start|>assistant\n" + formatted + "<|im_end|>\n")
}
case "user", "system":
sb.WriteString("<|im_start|>" + message.Role + "\n")
sb.WriteString(message.Content)
sb.WriteString("<|im_end|>\n")
case "tool":
// Check if previous message was also a tool message
prevWasTool := i > 0 && loopMessages[i-1].Role == "tool"
nextIsTool := i+1 < len(loopMessages) && loopMessages[i+1].Role == "tool"
if !prevWasTool {
sb.WriteString("<|im_start|>user\n")
}
sb.WriteString("\n")
sb.WriteString(message.Content)
sb.WriteString("\n\n")
if !nextIsTool {
sb.WriteString("<|im_end|>\n")
}
default:
sb.WriteString("<|im_start|>" + message.Role + "\n" + message.Content + "<|im_end|>\n")
}
}
// Add generation prompt
if enableThinking {
sb.WriteString("<|im_start|>assistant\n\n")
} else {
sb.WriteString("<|im_start|>assistant\n")
}
return sb.String(), nil
}
func (r *Nemotron3NanoRenderer) renderTools(tools []api.Tool) string {
var sb strings.Builder
sb.WriteString("# Tools\n\nYou have access to the following functions:\n\n")
for _, tool := range tools {
fn := tool.Function
sb.WriteString("\n\n" + fn.Name + "")
if fn.Description != "" {
sb.WriteString("\n" + strings.TrimSpace(fn.Description) + "")
}
sb.WriteString("\n")
if fn.Parameters.Properties != nil {
for paramName, paramFields := range fn.Parameters.Properties {
sb.WriteString("\n")
sb.WriteString("\n" + paramName + "")
if len(paramFields.Type) > 0 {
sb.WriteString("\n" + strings.Join(paramFields.Type, ", ") + "")
}
if paramFields.Description != "" {
sb.WriteString("\n" + strings.TrimSpace(paramFields.Description) + "")
}
if len(paramFields.Enum) > 0 {
enumJSON, _ := json.Marshal(paramFields.Enum)
sb.WriteString("\n" + string(enumJSON) + "")
}
sb.WriteString("\n")
}
}
if len(fn.Parameters.Required) > 0 {
reqJSON, _ := json.Marshal(fn.Parameters.Required)
sb.WriteString("\n" + string(reqJSON) + "")
}
sb.WriteString("\n")
sb.WriteString("\n")
}
sb.WriteString("\n")
sb.WriteString("\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n" +
"\n\n\nvalue_1\n\n" +
"\nThis is the value for the second parameter\nthat can span\nmultiple lines\n" +
"\n\n\n\n\nReminder:\n" +
"- Function calls MUST follow the specified format: an inner block must be nested within XML tags\n" +
"- Required parameters MUST be specified\n" +
"- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n" +
"- 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\n")
return sb.String()
}
func (r *Nemotron3NanoRenderer) buildContent(message api.Message) string {
// The parser always extracts thinking into the Thinking field,
// so Content will never have tags embedded
if message.Thinking != "" {
return "\n" + message.Thinking + "\n\n" + message.Content
}
return "" + message.Content
}
func (r *Nemotron3NanoRenderer) formatContent(content string, truncate bool, addNewline bool) string {
if content == "" {
return ""
}
if !truncate {
if addNewline {
return strings.TrimSpace(content) + "\n"
}
return strings.TrimSpace(content)
}
// Truncate thinking - keep only content after
c := content
if strings.Contains(c, "") {
parts := strings.Split(c, "")
c = parts[len(parts)-1]
} else if strings.Contains(c, "") {
parts := strings.Split(c, "")
c = parts[0]
}
c = "" + strings.TrimSpace(c)
if addNewline && len(c) > len("") {
return c + "\n"
}
if c == "" {
return c
}
return strings.TrimSpace(c)
}
func (r *Nemotron3NanoRenderer) writeToolCalls(sb *strings.Builder, toolCalls []api.ToolCall) {
for _, tc := range toolCalls {
sb.WriteString("\n\n")
for name, value := range tc.Function.Arguments {
sb.WriteString("\n" + r.formatArgValue(value) + "\n\n")
}
sb.WriteString("\n\n")
}
}
func (r *Nemotron3NanoRenderer) formatArgValue(value any) string {
switch v := value.(type) {
case map[string]any, []any:
jsonBytes, _ := json.Marshal(v)
return string(jsonBytes)
default:
return fmt.Sprintf("%v", v)
}
}