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) } }