Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

164 lines
5.9 KiB
Go

//go:build llama
// Package local — chat templates and GBNF grammars.
//
// These are the parts of the old internal/llm/agent and
// internal/llm/svc layers that are specific to a local llama.cpp
// model: how to flatten a provider-neutral []llm.Message into a single
// prompt string with the right turn markers, and how to constrain
// token sampling to a JSON shape via GBNF. The HTTP providers need
// neither — they speak structured messages and json-schema natively.
package local
import (
"fmt"
"strings"
"github.com/zzet/gortex/internal/llm"
)
// chatTemplate describes how to wrap conversation turns for a given
// model family. The wrappers each take raw content and return the
// fully-marked-up turn; assistPrime is the marker appended right
// before a generate call so the model starts an assistant turn.
type chatTemplate struct {
name string
bos string
system func(content string) string
user func(content string) string
tool func(content string) string
assistEnd string // marker appended after a captured assistant emission
assistPrime string
}
// templateChatML covers the Qwen2.5 family and Nous Hermes-3 (which
// re-trains Llama-3 onto ChatML).
var templateChatML = chatTemplate{
name: "chatml",
system: func(c string) string { return "<|im_start|>system\n" + c + "<|im_end|>\n" },
user: func(c string) string { return "<|im_start|>user\n" + c + "<|im_end|>\n" },
tool: func(c string) string { return "<|im_start|>tool\n" + c + "<|im_end|>\n" },
assistEnd: "<|im_end|>\n",
assistPrime: "<|im_start|>assistant\n",
}
// templateLlama3 covers Meta's Llama-3.x stock instruct format. Used
// by models that keep Llama-3's native template (NOT Hermes-3, which
// switches to ChatML).
var templateLlama3 = chatTemplate{
name: "llama3",
bos: "<|begin_of_text|>",
system: func(c string) string {
return "<|start_header_id|>system<|end_header_id|>\n\n" + c + "<|eot_id|>"
},
user: func(c string) string {
return "<|start_header_id|>user<|end_header_id|>\n\n" + c + "<|eot_id|>"
},
tool: func(c string) string {
return "<|start_header_id|>ipython<|end_header_id|>\n\n" + c + "<|eot_id|>"
},
assistEnd: "<|eot_id|>",
assistPrime: "<|start_header_id|>assistant<|end_header_id|>\n\n",
}
// templateByName returns a known chat template by short name. Empty
// falls back to ChatML.
func templateByName(name string) (chatTemplate, error) {
switch name {
case "", "chatml", "qwen", "hermes":
return templateChatML, nil
case "llama3", "llama":
return templateLlama3, nil
}
return chatTemplate{}, fmt.Errorf("local: unknown chat template %q", name)
}
// flatten renders a provider-neutral conversation into a single prompt
// string and primes an assistant turn. RoleAssistant messages are
// wrapped as a complete assistant emission (prime + content + end);
// RoleTool messages use the tool wrapper. The trailing assistPrime is
// what makes the model start generating an assistant turn.
func (t chatTemplate) flatten(msgs []llm.Message) string {
var b strings.Builder
b.WriteString(t.bos)
for _, m := range msgs {
switch m.Role {
case llm.RoleSystem:
b.WriteString(t.system(m.Content))
case llm.RoleUser:
b.WriteString(t.user(m.Content))
case llm.RoleAssistant:
b.WriteString(t.assistPrime)
b.WriteString(m.Content)
b.WriteString(t.assistEnd)
case llm.RoleTool:
b.WriteString(t.tool(m.Content))
}
}
b.WriteString(t.assistPrime)
return b.String()
}
// --- GBNF grammars ----------------------------------------------------------
// The expand / rerank / verify grammars each accept a single-key JSON
// object whose value is a string array. The array body is fully
// optional so the model is always allowed to emit an empty list —
// for verify that empty list is the load-bearing "honest negative".
const (
expandGrammar = `root ::= ws "{" ws "\"terms\"" ws ":" ws "[" ws ( str ( ws "," ws str )* )? ws "]" ws "}" ws
str ::= "\"" ( [^"\\] | "\\" ( ["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] ) )* "\""
ws ::= [ \t\n]*
`
rerankGrammar = `root ::= ws "{" ws "\"order\"" ws ":" ws "[" ws ( str ( ws "," ws str )* )? ws "]" ws "}" ws
str ::= "\"" ( [^"\\] | "\\" ( ["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] ) )* "\""
ws ::= [ \t\n]*
`
verifyGrammar = `root ::= ws "{" ws "\"keep\"" ws ":" ws "[" ws ( str ( ws "," ws str )* )? ws "]" ws "}" ws
str ::= "\"" ( [^"\\] | "\\" ( ["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] ) )* "\""
ws ::= [ \t\n]*
`
)
// buildToolCallGrammar returns a GBNF that accepts
// {"tool":"<one of names>","args":{<arbitrary JSON object>}} with
// whitespace tolerance. names must be non-empty.
func buildToolCallGrammar(names []string) string {
alt := make([]string, len(names))
for i, n := range names {
alt[i] = `"\"" "` + n + `" "\""`
}
toolname := strings.Join(alt, " | ")
return `root ::= ws "{" ws "\"tool\"" ws ":" ws toolname ws "," ws "\"args\"" ws ":" ws object ws "}" ws
toolname ::= ` + toolname + `
object ::= "{" ws ( pair ( ws "," ws pair )* )? ws "}"
pair ::= string ws ":" ws value
array ::= "[" ws ( value ( ws "," ws value )* )? ws "]"
value ::= string | number | object | array | "true" | "false" | "null"
string ::= "\"" ( [^"\\] | "\\" ( ["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] ) )* "\""
number ::= "-"? ( "0" | [1-9] [0-9]* ) ( "." [0-9]+ )? ( [eE] [-+]? [0-9]+ )?
ws ::= [ \t\n]*
`
}
// grammarForShape returns the GBNF for a structured shape. ShapeFreeform
// returns "" (no constraint). ShapeToolCall requires the tool name set.
func grammarForShape(shape llm.JSONShape, tools []llm.ToolSpec) string {
switch shape {
case llm.ShapeExpandTerms:
return expandGrammar
case llm.ShapeRerankOrder:
return rerankGrammar
case llm.ShapeVerifyKeep:
return verifyGrammar
case llm.ShapeToolCall:
names := make([]string, len(tools))
for i, t := range tools {
names[i] = t.Name
}
return buildToolCallGrammar(names)
default:
return ""
}
}