internal/autogen/generate_emit.go
Rama main · réplica de lectura del repositorio original.
package autogen
// YAML emission helpers: the non-model sections of the generated config
// (slot cache, API keys, groups, listeners) plus the small per-model bits
// (display name, profile block, id slug/tag formatting).
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"gopkg.in/yaml.v3"
"github.com/quartermaster-labs/quartermaster/internal/apppaths"
)
// slotKvPath resolves the slot-cache snapshot dir as forward-slash text shared
// by the --slot-save-path flag and the emitted slotCache.path. A blank Path
// falls back to the shared cache directory -- the same one
// config.DefaultSlotCachePath returns, which is what keeps the emitted config
// and the server's own LRU pointed at one directory. Both resolve it through
// internal/apppaths so autogen still needs no internal/config import.
func slotKvPath(sc SlotCacheSettings) string {
p := sc.Path
if p == "" {
p = filepath.Join(apppaths.CacheDir(), "slotkv")
}
return strings.ReplaceAll(p, "\\", "/")
}
// multiResidentOn reports whether VRAM-budget multi-load is enabled (default on).
func multiResidentOn(s Settings) bool {
return s.MultiResident == nil || *s.MultiResident
}
// emitVramBudget writes the top-level vramBudgetGB the router admits against.
// Withheld when multiResident is off, which leaves the router on the legacy
// static swap/exclusive policy even though every model still carries its
// estVramGB (harmless, and it keeps the dashboard's numbers available).
func emitVramBudget(b *strings.Builder, s Settings) {
if !multiResidentOn(s) || s.TargetVramGB <= 0 {
return
}
b.WriteString("# multi-resident: models stay loaded side by side while their\n")
b.WriteString("# estVramGB sum fits this budget; over it, the least-recently-used\n")
b.WriteString("# ones are evicted until the incoming model fits.\n")
fmt.Fprintf(b, "vramBudgetGB: %g\n\n", round2(s.TargetVramGB))
}
// emitRamBudget writes ramBudgetGB, the RAM pool's admission budget (see
// config.Config.RamBudgetGB), from settings.maxRamGB. Same multiResident gate
// as the VRAM one.
func emitRamBudget(b *strings.Builder, s Settings) {
if !multiResidentOn(s) || s.MaxRamGB <= 0 {
return
}
b.WriteString("# CPU-pool models are admitted while every resident's estRamGB fits\n")
b.WriteString("# this budget; over it, the least-recently-used CPU-pool ones go.\n")
fmt.Fprintf(b, "ramBudgetGB: %g\n\n", round2(s.MaxRamGB))
}
// writeEstVram emits a model's predicted VRAM footprint, the admission input for
// vramBudgetGB. Omitted when the caller has no estimate (0), which the router
// reads as "unknown" and handles conservatively.
func writeEstVram(b *strings.Builder, estGB float64) {
if estGB <= 0 {
return
}
fmt.Fprintf(b, " estVramGB: %g\n", round2(estGB))
}
// writeEstRam emits the sizer's predicted RAM share (weights the GPU could not
// take, or the whole footprint of a CPU-device model). It is the admission
// input for ramBudgetGB, and is omitted for a fully-offloaded model rather than
// written as 0.
func writeEstRam(b *strings.Builder, estGB float64) {
if estGB <= 0 {
return
}
fmt.Fprintf(b, " estRamGB: %g\n", round2(estGB))
}
// emitSlotCache writes the slotCache config block (consumed by the server) when
// the feature is enabled. Unset knobs are omitted so the server applies its
// defaults. The path is always emitted so it matches the --slot-save-path flag.
func emitSlotCache(b *strings.Builder, sc SlotCacheSettings) {
if !sc.Enable {
return
}
b.WriteString("slotCache:\n")
b.WriteString(" enable: true\n")
fmt.Fprintf(b, " path: %q\n", slotKvPath(sc))
if sc.MinSaveTokens > 0 {
fmt.Fprintf(b, " minSaveTokens: %d\n", sc.MinSaveTokens)
}
if sc.RecurrentSeeds {
b.WriteString(" recurrentSeeds: true\n")
}
if sc.MaxDiskGB > 0 {
fmt.Fprintf(b, " maxDiskGB: %g\n", sc.MaxDiskGB)
}
if sc.MaxSessions > 0 {
fmt.Fprintf(b, " maxSessions: %d\n", sc.MaxSessions)
}
if sc.MaxIdleDays > 0 {
fmt.Fprintf(b, " maxIdleDays: %d\n", sc.MaxIdleDays)
}
// Only the off case is emitted: absent means on, matching config's *bool.
if sc.PreambleCaches != nil && !*sc.PreambleCaches {
b.WriteString(" preambleCaches: false\n")
}
b.WriteString("\n")
}
// enhancerByID indexes settings.promptEnhancers by lowercased model id, which is
// how both the emitter and the per-model resolver look one up. Later entries win
// over earlier ones with the same id: a duplicate is an edit that was appended
// rather than replaced, and the last word is the one the user just wrote.
//
// Entries naming no model are dropped rather than reported. The list is UI-owned
// (the Settings page appends a blank row before it is filled in), so a half-typed
// entry is a normal intermediate state, not a config error.
func enhancerByID(list []PromptEnhancer) map[string]PromptEnhancer {
if len(list) == 0 {
return nil
}
out := make(map[string]PromptEnhancer, len(list))
for _, e := range list {
id := strings.ToLower(strings.TrimSpace(e.Model))
if id == "" {
continue
}
out[id] = e
}
if len(out) == 0 {
return nil
}
return out
}
// emitPromptEnhancers writes the top-level promptEnhancers map: the rewrite
// models an image model may hand its prompt to, each with the fixed system
// prompt it was trained under.
//
// The body is marshalled by yaml.v3 rather than Fprintf'd like every other block
// in this file, for one reason: a PE system prompt is a multi-KB, multi-line,
// quote-and-backslash-bearing document the publisher ships verbatim, and
// hand-rolling a block scalar for it is exactly the kind of quoting that works on
// the example and corrupts the real thing. The marshaller picks a safe
// representation (block scalar, quoted, whatever the content needs); we only
// indent it.
//
// Emitted in sorted id order so a regeneration with no changes produces an
// identical file - the config hash is what gates a reload.
func emitPromptEnhancers(b *strings.Builder, list []PromptEnhancer) {
byID := enhancerByID(list)
if len(byID) == 0 {
return
}
ids := make([]string, 0, len(byID))
for id := range byID {
ids = append(ids, id)
}
sort.Strings(ids)
b.WriteString("# prompt-rewrite models an image model may delegate to before it renders.\n")
b.WriteString("# Consumed by the CLIENT (the playground's Enhance button), never by the\n")
b.WriteString("# image route: a rewrite you cannot see or edit before it renders is worse\n")
b.WriteString("# than none. A model opts in with its own promptEnhancer: <id>.\n")
b.WriteString("promptEnhancers:\n")
for _, id := range ids {
e := byID[id]
body := struct {
Name string `yaml:"name,omitempty"`
Vision bool `yaml:"vision,omitempty"`
SystemPrompt string `yaml:"systemPrompt,omitempty"`
}{Name: strings.TrimSpace(e.Name), Vision: e.Vision, SystemPrompt: e.SystemPrompt}
out, err := yaml.Marshal(body)
if err != nil {
// Unreachable for a struct of two strings and a bool, but a marshal
// error must not take the whole config with it: skip the entry and say
// so, leaving every other model generated and loadable.
fmt.Fprintf(b, " # SKIPPED prompt enhancer %q: %v\n", e.Model, err)
continue
}
fmt.Fprintf(b, " %q:\n", strings.TrimSpace(e.Model))
b.WriteString(indentYAML(string(out), " "))
}
b.WriteString("\n")
}
// indentYAML shifts a marshalled block right by prefix. Blank lines are left
// EMPTY rather than filled with the prefix: a block scalar's interior blank line
// is part of its value, and padding it with spaces would silently add trailing
// whitespace to a system prompt.
func indentYAML(doc, prefix string) string {
var b strings.Builder
// TrimSuffix, not TrimRight: yaml.Marshal terminates the document with exactly
// one newline, and every newline before that one is CONTENT. A system prompt
// ending in a blank line marshals to a "|+" keep-indicator block, and trimming
// the run would silently drop the bytes that indicator exists to preserve.
for _, line := range strings.Split(strings.TrimSuffix(doc, "\n"), "\n") {
if line == "" {
b.WriteString("\n")
continue
}
b.WriteString(prefix)
b.WriteString(line)
b.WriteString("\n")
}
return b.String()
}
// ovPromptEnhancer reads an override's enhancer id nil-safely. Most emitters in
// this package take *Override and a nil one is the ordinary "no rule matched"
// case, not an error.
func ovPromptEnhancer(ov *Override) string {
if ov == nil {
return ""
}
return ov.PromptEnhancer
}
// ovPromptEnhancerEdit is the img2img half, same nil-safety.
func ovPromptEnhancerEdit(ov *Override) string {
if ov == nil {
return ""
}
return ov.PromptEnhancerEdit
}
// ovEnhancerPrompt / ovEnhancerEditPrompt read the per-model system prompts,
// same nil-safety.
func ovEnhancerPrompt(ov *Override) string {
if ov == nil {
return ""
}
return ov.PromptEnhancerPrompt
}
func ovEnhancerEditPrompt(ov *Override) string {
if ov == nil {
return ""
}
return ov.PromptEnhancerEditPrompt
}
// writePromptEnhancer emits a model's `promptEnhancer:` line - the id of the
// rewrite model it delegates its prompt to.
func writePromptEnhancer(b *strings.Builder, id string, byID map[string]PromptEnhancer) {
writeEnhancerKey(b, "promptEnhancer", id, byID)
}
// writePromptEnhancerEdit emits the img2img half. Written independently of the
// text one: a model may configure either, both or neither, and the client falls
// back to whichever is present.
func writePromptEnhancerEdit(b *strings.Builder, id string, byID map[string]PromptEnhancer) {
writeEnhancerKey(b, "promptEnhancerEdit", id, byID)
}
// writeEnhancerBlock emits a model's whole prompt-enhancer wiring: the id it
// uses per direction, plus the system prompt IT wants that enhancer to run
// under. imageID is the model's discovered id (not its emitted name), which is
// what the detection table is keyed on.
func writeEnhancerBlock(b *strings.Builder, s Settings, imageID, text, edit, textPrompt, editPrompt string) {
text, edit = resolveEnhancerIDs(s, imageID, text, edit)
byID := enhancerByID(s.PromptEnhancers)
writePromptEnhancer(b, text, byID)
writePromptEnhancerEdit(b, edit, byID)
writeEnhancerPrompt(b, "promptEnhancerPrompt", text, textPrompt)
writeEnhancerPrompt(b, "promptEnhancerEditPrompt", edit, editPrompt)
}
// writeEnhancerKey emits one enhancer id, as typed.
//
// A settings.promptEnhancers row is NOT required. It used to be - an id with no
// row was dropped here - and that was wrong in both directions the feature grew:
// an id can now be free text the user typed (the modal says so: "it will be
// called with no system prompt") or one this package detected from a filename,
// and in both cases the system prompt lives on the IMAGE model. Dropping the id
// meant the client showed no button and nothing said why. A row, when there is
// one, now only fixes up the canonical casing of the id it declares.
func writeEnhancerKey(b *strings.Builder, key, id string, byID map[string]PromptEnhancer) {
id = strings.TrimSpace(id)
if id == "" || enhancerDisabled(id) {
return
}
if e, ok := byID[strings.ToLower(id)]; ok {
id = strings.TrimSpace(e.Model)
}
fmt.Fprintf(b, " %s: %q\n", key, id)
}
// writeEnhancerPrompt emits a model's OWN system prompt for one direction: the
// text pasted into the model modal's prompt dialog. Skipped when the model has
// no enhancer in that direction, since the prompt would have nothing to run
// under.
//
// Marshalled rather than Fprintf'd for the same reason emitPromptEnhancers is: a
// rewriter prompt is a multi-line document full of quotes, colons and
// backslashes, and hand-rolling a block scalar for it works on the example and
// corrupts the real thing.
func writeEnhancerPrompt(b *strings.Builder, key, id, prompt string) {
if strings.TrimSpace(id) == "" || enhancerDisabled(id) || prompt == "" {
return
}
out, err := yaml.Marshal(map[string]string{key: prompt})
if err != nil {
// Unreachable for a one-string map, but a marshal error must not take the
// model with it: the enhancer falls back to the shared row's prompt.
fmt.Fprintf(b, " # SKIPPED %s: %v\n", key, err)
return
}
b.WriteString(indentYAML(string(out), " "))
}
// emitAPIKeys writes the apiKeys list and, for any key scoped to a model
// subset, the apiKeyModels map (key => allowed model IDs). Keys with no Models
// are unrestricted and appear only in apiKeys. No keys => nothing emitted.
func emitAPIKeys(b *strings.Builder, keys []APIKeyEntry) {
if len(keys) == 0 {
return
}
b.WriteString("apiKeys:\n")
for _, k := range keys {
fmt.Fprintf(b, " - %q\n", k.Key)
}
scoped := false
for _, k := range keys {
if len(k.Models) > 0 {
scoped = true
break
}
}
if scoped {
b.WriteString("apiKeyModels:\n")
for _, k := range keys {
if len(k.Models) == 0 {
continue
}
fmt.Fprintf(b, " %q:\n", k.Key)
for _, m := range k.Models {
fmt.Fprintf(b, " - %q\n", m)
}
}
}
b.WriteString("\n")
}
// emitGroupsAndListeners writes the groups block and, when settings.groups
// define listen addresses, a listeners block. The mechanism is use-case
// agnostic: each configured group has name-glob patterns (PowerShell -like);
// every emitted model is assigned to the FIRST group whose any pattern matches
// its name, so put specific groups before a "*" catch-all. Models matching no
// group fall into an implicit "default" group with no listener. Every group is
// exclusive, so loading on any listener still evicts whatever the others were
// running (one GPU, VRAM-exclusive); a group marked coexist additionally keeps its
// OWN members loaded side by side (swap:false) instead of swapping between them.
// With no settings.groups the output is a single "exclusive" group over every model
// (upstream default).
func emitGroupsAndListeners(b *strings.Builder, s Settings, emitted []string, cs coexistSets) {
// SAM masks, CPU-only TTS.cpp voices and CPU-only Parakeet ASR models are tiny,
// cost no VRAM, and are wanted ALONGSIDE whatever LLM/image model is loaded — a
// read-aloud click or a dictation must not swap the chat model out. They go in
// their own never-evicting groups (see writeCoexistGroup) and are kept out of
// the exclusive swap groups below.
groups := cs.groups(s)
inCoexist := map[string]bool{}
for _, g := range groups {
for _, n := range g.members {
inCoexist[n] = true
}
}
swappable := emitted
if len(inCoexist) > 0 {
swappable = swappable[:0:0]
for _, n := range emitted {
if !inCoexist[n] {
swappable = append(swappable, n)
}
}
}
if len(s.Groups) == 0 {
b.WriteString("\ngroups:\n")
writeGroup(b, "exclusive", swappable, false)
for _, g := range groups {
writeCoexistGroup(b, g.name, g.members, g.ramPool)
}
return
}
coexist := make(map[string]bool, len(s.Groups))
for _, g := range s.Groups {
if g.Coexist {
coexist[g.Name] = true
}
}
// Assign each model to the first matching group (config order preserved).
members := make(map[string][]string, len(s.Groups)+1)
order := make([]string, 0, len(s.Groups)+1)
seenGroup := map[string]bool{}
addGroup := func(name string) {
if !seenGroup[name] {
seenGroup[name] = true
order = append(order, name)
}
}
for _, g := range s.Groups {
addGroup(g.Name)
}
const defaultGroup = "default"
for _, name := range swappable {
assigned := ""
for _, g := range s.Groups {
for _, pat := range g.Match {
if globLike(pat, name) {
assigned = g.Name
break
}
}
if assigned != "" {
break
}
}
if assigned == "" {
assigned = defaultGroup
addGroup(defaultGroup)
}
members[assigned] = append(members[assigned], name)
}
b.WriteString("\ngroups:\n")
for _, name := range order {
writeGroup(b, name, members[name], coexist[name])
}
for _, g := range groups {
writeCoexistGroup(b, g.name, g.members, g.ramPool)
}
// listeners: address -> the groups it exposes (a group with no Listen binds
// no dedicated port but still groups for eviction). The coexistence groups carry
// no Listen of their own, so they are appended to EVERY listener: a curated port
// must not lose its segmentation / read-aloud models purely because we moved them
// out of the exclusive group.
byAddr := map[string][]string{}
var addrOrder []string
for _, g := range s.Groups {
if g.Listen == "" {
continue
}
if _, ok := byAddr[g.Listen]; !ok {
addrOrder = append(addrOrder, g.Listen)
}
byAddr[g.Listen] = append(byAddr[g.Listen], g.Name)
}
if len(addrOrder) == 0 {
return
}
var everywhere []string
for _, g := range groups {
everywhere = append(everywhere, g.name)
}
b.WriteString("\nlisteners:\n")
for _, addr := range addrOrder {
groups := append(append([]string{}, byAddr[addr]...), everywhere...)
fmt.Fprintf(b, " %q:\n groups: [%s]\n", addr, strings.Join(groups, ", "))
}
}
// coexistSets carries the model classes that must never take part in eviction:
// they are tiny, run outside the VRAM budget, and are used WHILE a chat or image
// model is resident. Sam is GPU-capable but placed live (see liveoffload.go); the
// other two are CPU-only unless the user opts into GPU via extraArgs, an accepted
// under-charge — the alternative is evicting a 27B on every dictation.
type coexistSets struct {
Sam []string // SAM segmentation (*.ggml, sam3_server)
TTS []string // TTS.cpp voices (Kokoro & friends) — CPU-only, no CUDA/ROCm path
ASR []string // Parakeet transcription — CPU-only by default
// CPU holds models the user pinned to the RAM pool (Override.InCpuPool). Not
// a never-evict group like the three above: its members can be big, so they
// are admitted against ramBudgetGB and evict each other LRU-first, but never
// evict, and are never evicted by, a GPU model for VRAM.
CPU []string
}
// coexistGroup is one emitted never-evicting group. Empty ones are dropped, so a
// config with no ASR models carries no "asr" group and no listener reference.
type coexistGroup struct {
name string
members []string
ramPool bool // the RAM-budgeted cpu group, see writeCoexistGroup
}
func (cs coexistSets) groups(s Settings) []coexistGroup {
var out []coexistGroup
for _, g := range []coexistGroup{
{"sam", cs.Sam, false},
{"tts", cs.TTS, false},
{"asr", cs.ASR, false},
{cpuGroupName, cs.CPU, true},
} {
if len(g.members) > 0 {
out = append(out, coexistGroup{freeGroupName(s, g.name), g.members, g.ramPool})
}
}
return out
}
// freeGroupName returns want, or want+"-auto" when a settings group already claims
// that name. The coexistence groups are synthesised by us, and emitting a duplicate
// YAML key would silently drop the user's group of the same name.
func freeGroupName(s Settings, want string) string {
for _, g := range s.Groups {
if strings.EqualFold(strings.TrimSpace(g.Name), want) {
return want + "-auto"
}
}
return want
}
// writeCoexistGroup emits a zero-VRAM coexistence group (SAM segmentation, CPU-only
// TTS.cpp voices, CPU-only Parakeet ASR): exclusive:false so loading a member evicts nothing,
// persistent:true so an exclusive LLM/image load never evicts the members,
// swap:false so several members coexist (they're all tiny). Nothing is emitted when
// the group has no members.
//
// The cpu group (ramPool) differs: swap:true and not persistent, so without a
// RAM budget its members still swap among themselves (one resident) instead of
// piling up in RAM forever, and with one the router admits them by fit. pool:
// ram is what keeps a GPU load from ever evicting them for VRAM.
func writeCoexistGroup(b *strings.Builder, name string, members []string, ramPool bool) {
if len(members) == 0 {
return
}
fmt.Fprintf(b, " %s:\n", name)
if ramPool {
b.WriteString(" swap: true\n")
b.WriteString(" exclusive: false\n")
b.WriteString(" persistent: false\n")
b.WriteString(" pool: ram\n")
} else {
b.WriteString(" swap: false\n")
b.WriteString(" exclusive: false\n")
b.WriteString(" persistent: true\n")
}
b.WriteString(" members:\n")
for _, n := range members {
fmt.Fprintf(b, " - %q\n", n)
}
}
// writeGroup emits one exclusive group with the given members. swap:true (the
// default) means only one member is loaded at a time; coexist emits swap:false so
// the members stay resident together. Either way exclusive:true keeps the group
// evicting the OTHER groups — one GPU.
func writeGroup(b *strings.Builder, name string, members []string, coexist bool) {
fmt.Fprintf(b, " %s:\n", name)
if coexist {
b.WriteString(" swap: false\n")
} else {
b.WriteString(" swap: true\n")
}
b.WriteString(" exclusive: true\n")
b.WriteString(" members:\n")
for _, n := range members {
fmt.Fprintf(b, " - %q\n", n)
}
}
// emitProfile writes one model entry's YAML block.
// resolvePublicName returns the advertised name for a served id when its base
// model was renamed via settings.displayNames: the display name plus the served
// id's variant suffix. The map is keyed by BASE id; the longest matching base
// prefix wins so a renamed "foo" doesn't shadow a separately-renamed "foo-bar".
// "" => not renamed (advertise the real id unchanged).
func resolvePublicName(dn map[string]string, id string) string {
best, bestName := "", ""
for base, name := range dn {
if strings.TrimSpace(name) == "" {
continue
}
if id == base || strings.HasPrefix(id, base+"-") {
if len(base) > len(best) {
best, bestName = base, name
}
}
}
if best == "" {
return ""
}
return bestName + id[len(best):]
}
// writeDisplayName emits the config name: + a routable alias for a renamed served
// id (both = the advertised public name). The real served id stays the config
// key, so it still resolves; the alias makes the public name resolve too. No-op
// when the model wasn't renamed. Indent matches the 4-space model-field block.
func writeDisplayName(b *strings.Builder, s Settings, id string) {
pub := resolvePublicName(s.DisplayNames, id)
if pub == "" || pub == id {
return
}
fmt.Fprintf(b, " name: %q\n", pub)
fmt.Fprintf(b, " aliases: [%q]\n", pub)
}
func emitProfile(b *strings.Builder, s Settings, meta Metadata, row GgufRow, prof profile, ctx, ngl, ncpuMoe int, plan LoadPlan, kvK, kvV string, kvInRam bool, ov *Override) {
fmt.Fprintf(b, "\n # arch=%s size=%gGB blocks=%d moe=%v\n", meta.Architecture, meta.FileSizeGB, meta.BlockCount, meta.IsMoE)
fmt.Fprintf(b, " # est vram=%gGB ram=%gGB\n", plan.EstVramGB, plan.EstRamGB)
fmt.Fprintf(b, " %q:\n", prof.Name)
// The user's custom launch arguments are the last layer of the emitted
// command: a knob the text sets drops the generated flags for that knob. A
// parse error cannot fail generation (this function has no error path), so
// ComposeCmd leaves the text appended and the spawn reports the problem.
composed, _ := ComposeCmd(buildCmdLines(s, meta, row, prof, ctx, ngl, ncpuMoe, kvK, kvV, kvInRam, ov), ov.CustomArgsText())
b.WriteString(" cmd: >\n")
for _, line := range composed.Lines {
fmt.Fprintf(b, " %s\n", line)
}
fmt.Fprintf(b, " ttl: %d\n", s.TtlSec)
// A split is expressed in TELEMETRY device order (nvidia-smi / DXGI), but the
// CUDA runtime enumerates FASTEST_FIRST by default, so on a mismatched pair
// the two orders disagree and the ratio lands on the wrong cards - silently,
// with no error and a model that OOMs the small one. Pinning bus order makes
// the ordinal mean one thing everywhere. Ignored by a Vulkan/ROCm build.
if len(prof.TensorSplit) > 1 {
fmt.Fprintf(b, " env:\n - %q\n", cudaOrderEnv)
}
writeEstVram(b, plan.EstVramGB)
writeEstRam(b, plan.EstRamGB)
writeDisplayName(b, s, prof.Name)
if prof.Unlisted {
b.WriteString(" unlisted: true\n")
}
// Per-model preamble opt-out, read by the server's slot cache. Only meaningful
// for a model that actually persists (it got --slot-save-path above), and only
// the off case is emitted - absent means on.
if ov != nil && ov.SlotCachePreamble != nil && !*ov.SlotCachePreamble &&
s.SlotCache.Enable && ov.SlotCache != nil && *ov.SlotCache {
b.WriteString(" slotCachePreamble: false\n")
}
effort := effortLevels(meta, ov)
if prof.Vision || len(effort) > 0 {
b.WriteString(" capabilities:\n")
if prof.Vision {
b.WriteString(" in: [text, image]\n")
b.WriteString(" out: [text]\n")
}
if len(effort) > 0 {
fmt.Fprintf(b, " reasoningEffort: [%s]\n", strings.Join(effort, ", "))
}
}
}
// effortLevels reports the reasoning-effort ladder to advertise for a model:
// the values the template ACTUALLY BEING RUN accepts. A --chat-template-file
// override replaces the baked template wholesale, so the gguf's ladder says
// nothing about the live renderer — advertising it there would have the server
// translate a client's reasoning_effort into a kwarg the live template ignores.
// The override file is therefore scanned in the gguf template's place: drop-in
// templates are where effort support most often lives (llama.cpp ships no way
// to ask the running server what kwargs its template reads), and the two Qwen
// 3.8 templates in circulation both support the ladder while only one of them
// declares it in a guard. An unreadable file advertises nothing, as before.
//
// With no override set the model runs its own baked template, so whatever the
// gguf declared is what it accepts.
func effortLevels(meta Metadata, ov *Override) []string {
if ov != nil && strings.TrimSpace(ov.ChatTemplateFile) != "" {
return chatTemplateFileEffortLevels(strings.TrimSpace(ov.ChatTemplateFile))
}
return meta.ChatTemplateEffortLevels
}
// chatTemplateFileEffortLevels reads a drop-in .jinja template and pulls its
// ladder out with the same scanner used on baked gguf templates. Results are
// memoized: one template file is typically shared by every ctx variant of every
// model that uses it, and this runs inside config generation.
var chatTemplateEffortCache sync.Map // abs path -> []string
func chatTemplateFileEffortLevels(path string) []string {
key := path
if abs, err := filepath.Abs(path); err == nil {
key = abs
}
if v, ok := chatTemplateEffortCache.Load(key); ok {
return v.([]string)
}
var levels []string
if b, err := os.ReadFile(path); err == nil {
_, levels = scanChatTemplate(string(b))
}
chatTemplateEffortCache.Store(key, levels)
return levels
}
func formatCtxTag(ctx int) string {
if ctx >= 1048576 && ctx%1048576 == 0 {
return fmt.Sprintf("%dm", ctx/1048576)
}
if ctx >= 1024 && ctx%1024 == 0 {
return fmt.Sprintf("%dk", ctx/1024)
}
return fmt.Sprintf("%d", ctx)
}
// slugify lowercases and collapses non-alphanumerics to single dashes.
func slugify(s string) string {
var b strings.Builder
prevDash := false
for _, r := range strings.ToLower(s) {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
prevDash = false
} else if !prevDash {
b.WriteByte('-')
prevDash = true
}
}
return strings.Trim(b.String(), "-")
}
// DefaultNow returns an RFC3339 timestamp for the generation header. Kept
// separate so Generate stays deterministic in tests.
func DefaultNow() string {
return time.Now().Format(time.RFC3339)
}