harmony: remove special casing in routes.go

Now that we have a built-in parser abstraction, which was introduced in
<https://github.com/ollama/ollama/pull/12248>, we can modify our harmony
parser to match this and then get rid of nearly all of the
harmony-specific logic in routes.go. We do have a small amount of
code that turns the parser on by default if the architecture matches and
no other built-in parser was provided.

The built-in parser interface was modified in order to handle harmony's
prefill and tool name translation requirements.
This commit is contained in:
Devon Rifkin 2025-09-18 14:55:59 -07:00
parent eb0a5d4459
commit e7f56ef3d8
5 changed files with 144 additions and 96 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@
dist dist
build build
.cache .cache
.gocache
*.exe *.exe
.idea .idea
test_data test_data

View File

@ -1,6 +1,7 @@
package harmony package harmony
import ( import (
"encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
"strings" "strings"
@ -265,6 +266,8 @@ type HarmonyMessageHandler struct {
state harmonyMessageState state harmonyMessageState
HarmonyParser *HarmonyParser HarmonyParser *HarmonyParser
FunctionNameMap *FunctionNameMap FunctionNameMap *FunctionNameMap
toolAccumulator *HarmonyToolCallAccumulator
convertedTools map[string]struct{}
} }
// NewHarmonyMessageHandler creates a new message handler // NewHarmonyMessageHandler creates a new message handler
@ -277,6 +280,7 @@ func NewHarmonyMessageHandler() *HarmonyMessageHandler {
HeaderEndTag: "<|message|>", HeaderEndTag: "<|message|>",
}, },
FunctionNameMap: NewFunctionNameMap(), FunctionNameMap: NewFunctionNameMap(),
convertedTools: make(map[string]struct{}),
} }
} }
@ -384,6 +388,79 @@ func NewFunctionNameMap() *FunctionNameMap {
} }
} }
// Init initializes the handler with tools and optional last message
// Implements the Parser interface
func (h *HarmonyMessageHandler) Init(tools []api.Tool, lastMessage *api.Message) []api.Tool {
// Initialize the harmony parser
if h.HarmonyParser == nil {
h.HarmonyParser = &HarmonyParser{
MessageStartTag: "<|start|>",
MessageEndTag: "<|end|>",
HeaderEndTag: "<|message|>",
}
}
// Handle prefill for chat mode
if lastMessage != nil {
h.HarmonyParser.AddImplicitStartOrPrefill(lastMessage)
} else {
h.HarmonyParser.AddImplicitStart()
}
// Initialize tool accumulator
h.toolAccumulator = h.CreateToolParser()
// Process tools and return renamed versions
if len(tools) == 0 {
return tools
}
processedTools := make([]api.Tool, len(tools))
copy(processedTools, tools)
for i, tool := range processedTools {
if tool.Function.Name != "" {
processedTools[i].Function.Name = h.FunctionNameMap.ConvertAndAdd(tool.Function.Name)
h.convertedTools[tool.Function.Name] = struct{}{}
}
}
return processedTools
}
// Add implements the Parser interface - processes streamed content and extracts content, thinking, and tool calls
func (h *HarmonyMessageHandler) Add(s string, done bool) (content string, thinking string, calls []api.ToolCall, err error) {
content, thinking, toolContent := h.AddContent(s, h.toolAccumulator)
if toolContent != "" {
h.toolAccumulator.Add(toolContent)
}
// tool calls always happen one at a time, and always at the end of a message,
// so for simplicity we defer parsing them until we know we're done
if done {
toolName, raw := h.toolAccumulator.Drain()
if toolName != nil {
name := strings.TrimPrefix(*toolName, "functions.")
name = h.FunctionNameMap.OriginalFromConverted(name)
var args api.ToolCallFunctionArguments
if err := json.Unmarshal([]byte(raw), &args); err != nil {
return "", "", nil, fmt.Errorf("error parsing tool call: raw='%s', err=%w", raw, err)
}
calls = append(calls, api.ToolCall{Function: api.ToolCallFunction{Name: name, Arguments: args}})
}
}
return content, thinking, calls, nil
}
// HasToolSupport implements the Parser interface
func (h *HarmonyMessageHandler) HasToolSupport() bool {
return true
}
// HasThinkingSupport implements the Parser interface
func (h *HarmonyMessageHandler) HasThinkingSupport() bool {
return true
}
func (m *FunctionNameMap) ConvertAndAdd(userFunctionName string) string { func (m *FunctionNameMap) ConvertAndAdd(userFunctionName string) string {
harmonyFunctionName := m.deriveName(userFunctionName) harmonyFunctionName := m.deriveName(userFunctionName)
m.userToHarmony[userFunctionName] = harmonyFunctionName m.userToHarmony[userFunctionName] = harmonyFunctionName

View File

@ -2,10 +2,16 @@ package parsers
import ( import (
"github.com/ollama/ollama/api" "github.com/ollama/ollama/api"
"github.com/ollama/ollama/harmony"
) )
type Parser interface { type Parser interface {
Add(s string, tools []api.Tool) (content string, thinking string, calls []api.ToolCall, err error) // Init initializes the parser with tools and optional last message for chat prefill
// Returns processed tools if the parser needs to modify them (e.g., harmony renames them)
Init(tools []api.Tool, lastMessage *api.Message) []api.Tool
// Add processes streamed content and returns parsed content, thinking, and tool calls
// The done flag indicates if this is the last chunk (used for draining accumulators)
Add(s string, done bool) (content string, thinking string, calls []api.ToolCall, err error)
HasToolSupport() bool HasToolSupport() bool
HasThinkingSupport() bool HasThinkingSupport() bool
} }
@ -17,6 +23,8 @@ func ParserForName(name string) Parser {
return parser return parser
case "passthrough": case "passthrough":
return &PassthroughParser{} return &PassthroughParser{}
case "harmony":
return harmony.NewHarmonyMessageHandler()
default: default:
return nil return nil
} }
@ -24,7 +32,11 @@ func ParserForName(name string) Parser {
type PassthroughParser struct{} type PassthroughParser struct{}
func (p *PassthroughParser) Add(s string, tools []api.Tool) (content string, thinking string, calls []api.ToolCall, err error) { func (p *PassthroughParser) Init(tools []api.Tool, lastMessage *api.Message) []api.Tool {
return tools // passthrough doesn't modify tools
}
func (p *PassthroughParser) Add(s string, done bool) (content string, thinking string, calls []api.ToolCall, err error) {
return s, "", nil, nil return s, "", nil, nil
} }

View File

@ -31,6 +31,7 @@ const (
type Qwen3CoderParser struct { type Qwen3CoderParser struct {
state qwenParserState state qwenParserState
acc strings.Builder acc strings.Builder
tools []api.Tool
} }
func (p *Qwen3CoderParser) HasToolSupport() bool { func (p *Qwen3CoderParser) HasToolSupport() bool {
@ -41,7 +42,12 @@ func (p *Qwen3CoderParser) HasThinkingSupport() bool {
return false return false
} }
func (p *Qwen3CoderParser) Add(s string, tools []api.Tool) (content string, thinking string, calls []api.ToolCall, err error) { func (p *Qwen3CoderParser) Init(tools []api.Tool, lastMessage *api.Message) []api.Tool {
p.tools = tools
return tools // Qwen doesn't modify tools
}
func (p *Qwen3CoderParser) Add(s string, done bool) (content string, thinking string, calls []api.ToolCall, err error) {
p.acc.WriteString(s) p.acc.WriteString(s)
events := p.parseEvents() events := p.parseEvents()
@ -51,7 +57,7 @@ func (p *Qwen3CoderParser) Add(s string, tools []api.Tool) (content string, thin
for _, event := range events { for _, event := range events {
switch event := event.(type) { switch event := event.(type) {
case qwenEventRawToolCall: case qwenEventRawToolCall:
toolCall, err := parseToolCall(event, tools) toolCall, err := parseToolCall(event, p.tools)
if err != nil { if err != nil {
slog.Warn("qwen tool call parsing failed", "error", err) slog.Warn("qwen tool call parsing failed", "error", err)
return "", "", nil, err return "", "", nil, err
@ -359,7 +365,7 @@ func parseValue(raw string, paramType api.PropertyType) any {
// Try array // Try array
if typeSet["array"] { if typeSet["array"] {
var arr []interface{} var arr []any
if err := json.Unmarshal([]byte(raw), &arr); err == nil { if err := json.Unmarshal([]byte(raw), &arr); err == nil {
return arr return arr
} }
@ -371,7 +377,7 @@ func parseValue(raw string, paramType api.PropertyType) any {
// Try object // Try object
if typeSet["object"] { if typeSet["object"] {
var obj map[string]interface{} var obj map[string]any
if err := json.Unmarshal([]byte(raw), &obj); err == nil { if err := json.Unmarshal([]byte(raw), &obj); err == nil {
return obj return obj
} }

View File

@ -34,7 +34,6 @@ import (
"github.com/ollama/ollama/envconfig" "github.com/ollama/ollama/envconfig"
"github.com/ollama/ollama/format" "github.com/ollama/ollama/format"
"github.com/ollama/ollama/fs/ggml" "github.com/ollama/ollama/fs/ggml"
"github.com/ollama/ollama/harmony"
"github.com/ollama/ollama/llm" "github.com/ollama/ollama/llm"
"github.com/ollama/ollama/logutil" "github.com/ollama/ollama/logutil"
"github.com/ollama/ollama/model/parsers" "github.com/ollama/ollama/model/parsers"
@ -288,17 +287,21 @@ func (s *Server) GenerateHandler(c *gin.Context) {
return return
} }
useHarmony := shouldUseHarmony(m) && !req.Raw var builtinParser parsers.Parser
var harmonyMessageHandler *harmony.HarmonyMessageHandler if shouldUseHarmony(m) && m.Config.Parser == "" {
var harmonyToolParser *harmony.HarmonyToolCallAccumulator m.Config.Parser = "harmony"
if useHarmony {
harmonyMessageHandler = harmony.NewHarmonyMessageHandler()
harmonyMessageHandler.HarmonyParser.AddImplicitStart()
harmonyToolParser = harmonyMessageHandler.CreateToolParser()
} }
// Validate Think value: string values currently only allowed for gptoss models if !req.Raw && m.Config.Parser != "" {
if req.Think != nil && req.Think.IsString() && !useHarmony { builtinParser = parsers.ParserForName(m.Config.Parser)
if builtinParser != nil {
// no tools or last message for generate endpoint
builtinParser.Init(nil, nil)
}
}
// Validate Think value: string values currently only allowed for harmony/gptoss models
if req.Think != nil && req.Think.IsString() && m.Config.Parser != "harmony" {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("think value %q is not supported for this model", req.Think.String())}) c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("think value %q is not supported for this model", req.Think.String())})
return return
} }
@ -422,7 +425,7 @@ func (s *Server) GenerateHandler(c *gin.Context) {
} }
var thinkingState *thinking.Parser var thinkingState *thinking.Parser
if !useHarmony { if builtinParser == nil {
openingTag, closingTag := thinking.InferTags(m.Template.Template) openingTag, closingTag := thinking.InferTags(m.Template.Template)
if req.Think != nil && req.Think.Bool() && openingTag != "" && closingTag != "" { if req.Think != nil && req.Think.Bool() && openingTag != "" && closingTag != "" {
thinkingState = &thinking.Parser{ thinkingState = &thinking.Parser{
@ -459,11 +462,17 @@ func (s *Server) GenerateHandler(c *gin.Context) {
}, },
} }
if useHarmony { if builtinParser != nil {
content, thinking, toolContent := harmonyMessageHandler.AddContent(cr.Content, harmonyToolParser) content, thinking, toolCalls, err := builtinParser.Add(cr.Content, cr.Done)
if err != nil {
ch <- gin.H{"error": err.Error()}
return
}
res.Response = content res.Response = content
res.Thinking = thinking res.Thinking = thinking
harmonyToolParser.Add(toolContent) if cr.Done && len(toolCalls) > 0 {
res.ToolCalls = toolCalls
}
} else if thinkingState != nil { } else if thinkingState != nil {
thinking, content := thinkingState.AddContent(cr.Content) thinking, content := thinkingState.AddContent(cr.Content)
res.Thinking = thinking res.Thinking = thinking
@ -475,26 +484,6 @@ func (s *Server) GenerateHandler(c *gin.Context) {
} }
if cr.Done { if cr.Done {
if useHarmony {
toolName, toolContent := harmonyToolParser.Drain()
if toolName != nil {
*toolName = strings.TrimPrefix(*toolName, "functions.")
var args api.ToolCallFunctionArguments
if err := json.Unmarshal([]byte(toolContent), &args); err != nil {
errStr := fmt.Sprintf("error parsing tool call: raw='%s', err=%s", toolContent, err.Error())
ch <- gin.H{"error": errStr}
return
}
res.ToolCalls = append(res.ToolCalls, api.ToolCall{
Function: api.ToolCallFunction{
Name: *toolName,
Arguments: args,
},
})
}
}
res.DoneReason = cr.DoneReason.String() res.DoneReason = cr.DoneReason.String()
res.TotalDuration = time.Since(checkpointStart) res.TotalDuration = time.Since(checkpointStart)
res.LoadDuration = checkpointLoaded.Sub(checkpointStart) res.LoadDuration = checkpointLoaded.Sub(checkpointStart)
@ -509,7 +498,7 @@ func (s *Server) GenerateHandler(c *gin.Context) {
} }
} }
if useHarmony { if builtinParser != nil {
// only send messages with meaningful content (empty messages confuse clients) // only send messages with meaningful content (empty messages confuse clients)
if res.Response != "" || res.Thinking != "" || res.Done || len(res.ToolCalls) > 0 { if res.Response != "" || res.Thinking != "" || res.Done || len(res.ToolCalls) > 0 {
ch <- res ch <- res
@ -1853,32 +1842,23 @@ func (s *Server) ChatHandler(c *gin.Context) {
} }
msgs = filterThinkTags(msgs, m) msgs = filterThinkTags(msgs, m)
var builtinParser parsers.Parser if shouldUseHarmony(m) && m.Config.Parser == "" {
if m.Config.Parser != "" { m.Config.Parser = "harmony"
builtinParser = parsers.ParserForName(m.Config.Parser)
} }
var harmonyMessageHandler *harmony.HarmonyMessageHandler var builtinParser parsers.Parser
var harmonyToolParser *harmony.HarmonyToolCallAccumulator
useHarmony := shouldUseHarmony(m) || m.Config.Parser == "harmony"
processedTools := req.Tools processedTools := req.Tools
if useHarmony {
harmonyMessageHandler = harmony.NewHarmonyMessageHandler()
var lastMessage *api.Message
if len(msgs) > 0 {
lastMessage = &msgs[len(msgs)-1]
}
harmonyMessageHandler.HarmonyParser.AddImplicitStartOrPrefill(lastMessage)
harmonyToolParser = harmonyMessageHandler.CreateToolParser()
// make a copy of tools to pass to the chat prompt. Function names may be if m.Config.Parser != "" {
// renamed to be valid Harmony function names. builtinParser = parsers.ParserForName(m.Config.Parser)
processedTools = make([]api.Tool, len(req.Tools)) if builtinParser != nil {
copy(processedTools, req.Tools) // Determine last message for chat prefill
for i, tool := range processedTools { var lastMessage *api.Message
processedTools[i].Function.Name = harmonyMessageHandler.FunctionNameMap.ConvertAndAdd(tool.Function.Name) if len(msgs) > 0 {
lastMessage = &msgs[len(msgs)-1]
}
// Initialize parser and get processed tools
processedTools = builtinParser.Init(req.Tools, lastMessage)
} }
} }
@ -1902,8 +1882,8 @@ func (s *Server) ChatHandler(c *gin.Context) {
return return
} }
// Validate Think value: string values currently only allowed for gptoss models // Validate Think value: string values currently only allowed for harmony/gptoss models
if req.Think != nil && req.Think.IsString() && !useHarmony { if req.Think != nil && req.Think.IsString() && m.Config.Parser != "harmony" {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("think value %q is not supported for this model", req.Think.String())}) c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("think value %q is not supported for this model", req.Think.String())})
return return
} }
@ -1922,7 +1902,7 @@ func (s *Server) ChatHandler(c *gin.Context) {
} }
var toolParser *tools.Parser var toolParser *tools.Parser
if len(req.Tools) > 0 && !useHarmony { if len(req.Tools) > 0 && (builtinParser == nil || !builtinParser.HasToolSupport()) {
toolParser = tools.NewParser(m.Template.Template, req.Tools) toolParser = tools.NewParser(m.Template.Template, req.Tools)
} }
@ -1954,38 +1934,10 @@ func (s *Server) ChatHandler(c *gin.Context) {
res.LoadDuration = checkpointLoaded.Sub(checkpointStart) res.LoadDuration = checkpointLoaded.Sub(checkpointStart)
} }
// TODO(drifkin): fold this as much as possibleinto the generic m.Config.Parser logic if builtinParser != nil {
if useHarmony {
content, thinking, toolContent := harmonyMessageHandler.AddContent(r.Content, harmonyToolParser)
res.Message.Content = content
res.Message.Thinking = thinking
harmonyToolParser.Add(toolContent)
if r.Done {
toolName, toolContent := harmonyToolParser.Drain()
if toolName != nil {
*toolName = strings.TrimPrefix(*toolName, "functions.")
*toolName = harmonyMessageHandler.FunctionNameMap.OriginalFromConverted(*toolName)
var args api.ToolCallFunctionArguments
if err := json.Unmarshal([]byte(toolContent), &args); err != nil {
errStr := fmt.Sprintf("error parsing tool call: raw='%s', err=%s", toolContent, err.Error())
ch <- gin.H{"error": errStr}
return
}
res.Message.ToolCalls = []api.ToolCall{{Function: api.ToolCallFunction{Name: *toolName, Arguments: args}}}
}
}
// only send messages with meaningful content (empty messages confuse clients)
if res.Message.Content != "" || res.Message.Thinking != "" || len(res.Message.ToolCalls) > 0 || res.Done {
ch <- res
}
return
} else if builtinParser != nil {
slog.Log(context.TODO(), logutil.LevelTrace, "builtin parser input", "parser", m.Config.Parser, "content", r.Content) slog.Log(context.TODO(), logutil.LevelTrace, "builtin parser input", "parser", m.Config.Parser, "content", r.Content)
content, thinking, toolCalls, err := builtinParser.Add(r.Content, req.Tools) content, thinking, toolCalls, err := builtinParser.Add(r.Content, r.Done)
if err != nil { if err != nil {
ch <- gin.H{"error": err.Error()} ch <- gin.H{"error": err.Error()}
return return