ollie/fs/session/files.go

990 lines
24 KiB
Go

package fs
import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"ollie/agent"
"ollie/backend"
olog "ollie/log"
"ollie/toolsrv"
)
// SessionFileList defines session-level files (shared across agents).
var SessionFileList = []struct {
Name string
OneShot bool
Async bool
}{
{"env", false, false},
{"ctl", false, true},
}
// AgentFileList defines agent-level files.
var AgentFileList = []struct {
Name string
OneShot bool
Async bool
}{
{"cwd", false, false},
{"plan", false, false},
{"ctl", false, true},
{"prompt", false, true},
{"fifo.in", false, true},
{"fifo.out", true, false},
{"log", false, false},
{"chat", false, false},
{"offset", false, false},
{"cfg", false, false},
{"state", false, false},
{"statewait", false, false},
{"usage", false, false},
{"cost", false, false},
{"ctxsz", false, false},
{"models", false, false},
{"systemprompt", false, false},
{"tail", false, false},
{"prompt.prev", false, false},
{"context", false, false},
{"toolsrv.loaded", false, false},
{"toolsrv.rev", false, false},
{"tools", false, false},
}
// FileList is the combined list (for backward compat in tests).
var FileList = append(SessionFileList, AgentFileList...)
// sessionFilePerms returns the permission registry for per-session files.
func sessionFilePerms() map[string]os.FileMode {
return Perms[PathSessionFile].Files
}
func NewSessionTree(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error, invalidateModels func(), toolRegistry *toolsrv.Registry) *Tree {
h := &sessionHelper{sess: sess, log: log, kill: kill, rename: rename, saveTranscript: saveTranscript, invalidateModels: invalidateModels, toolRegistry: toolRegistry}
perms := sessionFilePerms()
specs := make([]FileSpec, len(SessionFileList))
for i, f := range SessionFileList {
specs[i] = h.fileSpec(f.Name, perms[f.Name])
specs[i].OneShot = f.OneShot
specs[i].Async = f.Async
}
return NewSynth(specs)
}
// NewAgentTree builds the file tree for a single agent within a session.
func NewAgentTree(sess *Session, al *AgentLog, log *olog.Logger, saveTranscript func([]byte) error, invalidateModels func(), toolRegistry *toolsrv.Registry) *Tree {
h := &agentHelper{al: al, sess: sess, log: log, saveTranscript: saveTranscript, invalidateModels: invalidateModels, toolRegistry: toolRegistry}
perms := sessionFilePerms()
specs := make([]FileSpec, len(AgentFileList))
for i, f := range AgentFileList {
specs[i] = h.fileSpec(f.Name, perms[f.Name])
specs[i].OneShot = f.OneShot
specs[i].Async = f.Async
}
return NewSynth(specs)
}
// sessionHelper holds the dependencies needed to build session FileSpecs.
type sessionHelper struct {
sess *Session
log *olog.Logger
kill func()
rename func(newID string) error
saveTranscript func([]byte) error
invalidateModels func()
toolRegistry *toolsrv.Registry
}
// agentHelper holds the dependencies needed to build agent FileSpecs.
type agentHelper struct {
al *AgentLog
sess *Session
log *olog.Logger
saveTranscript func([]byte) error
invalidateModels func()
toolRegistry *toolsrv.Registry
}
func (h *sessionHelper) fileSpec(name string, mode os.FileMode) FileSpec {
fs := FileSpec{Name: name, Mode: mode}
switch name {
case "env":
fs.Read = func() ([]byte, error) { return []byte(h.envContent()), nil }
case "ctl":
fs.Read = func() ([]byte, error) { return nil, nil }
fs.Write = func(data []byte) error {
input := strings.TrimSpace(string(data))
if input == "" {
return nil
}
return h.handleCtl(input)
}
}
return fs
}
func (h *sessionHelper) envContent() string {
var sb strings.Builder
fmt.Fprintf(&sb, "OLLIE_SESSION_ID=%s\n", h.sess.RunnableID())
for _, e := range os.Environ() {
if strings.HasPrefix(e, "OLLIE_") && !strings.HasPrefix(e, "OLLIE_SESSION_ID=") {
sb.WriteString(e)
sb.WriteByte('\n')
}
}
return sb.String()
}
func (h *sessionHelper) handleCtl(input string) error {
cmd := strings.Fields(input)
if len(cmd) == 0 {
return fmt.Errorf("empty ctl command")
}
switch cmd[0] {
case "kill", ".":
h.kill()
case "rn":
if name := strings.TrimSpace(input[3:]); name != "" {
if err := h.rename(name); err != nil {
h.log.Error("rename: %v", err)
}
}
case "save":
h.sess.Core.SaveSession("") // triggers session persistence
case "invalidate":
if h.invalidateModels != nil {
h.invalidateModels()
}
h.sess.InvalidateModelsCache()
default:
return fmt.Errorf("unknown session ctl command: %s", cmd[0])
}
return nil
}
func (h *agentHelper) fileSpec(name string, mode os.FileMode) FileSpec {
fs := FileSpec{Name: name, Mode: mode}
// Read
switch name {
case "cwd":
fs.Read = func() ([]byte, error) {
return []byte(h.sess.Core.CWD() + "\n"), nil
}
case "ctl":
fs.Read = func() ([]byte, error) { return nil, nil }
case "plan":
fs.Read = func() ([]byte, error) {
h.al.mu.RLock()
data := make([]byte, len(h.al.plan))
copy(data, h.al.plan)
h.al.mu.RUnlock()
return data, nil
}
fs.Size = func() int64 {
h.al.mu.RLock()
n := len(h.al.plan)
h.al.mu.RUnlock()
return int64(n)
}
case "prompt.prev":
fs.Read = func() ([]byte, error) {
h.al.mu.RLock()
data := make([]byte, len(h.al.prevPrompt))
copy(data, h.al.prevPrompt)
h.al.mu.RUnlock()
return data, nil
}
case "log":
fs.Read = func() ([]byte, error) {
const maxWindow = 64 * 1024
h.al.mu.RLock()
log := h.al.log
start := 0
if len(log) > maxWindow {
start = len(log) - maxWindow
}
data := make([]byte, len(log)-start)
copy(data, log[start:])
h.al.mu.RUnlock()
return data, nil
}
fs.Size = func() int64 {
const maxWindow = 64 * 1024
h.al.mu.RLock()
n := len(h.al.log)
h.al.mu.RUnlock()
if n > maxWindow {
return int64(maxWindow)
}
return int64(n)
}
case "fifo.out":
fs.Read = func() ([]byte, error) {
item, ok := h.sess.Core.Agent().PopQueue()
if !ok {
return nil, nil
}
return []byte(item), nil
}
case "cfg":
fs.Read = func() ([]byte, error) { return []byte(h.cfgContent()), nil }
default:
fs.Read = func() ([]byte, error) { return []byte(h.content(name)), nil }
}
// Write
switch name {
case "cwd":
fs.Write = func(data []byte) error {
input := strings.TrimSpace(string(data))
if input == "" {
return nil
}
return h.sess.Core.SetCWD(input)
}
case "plan":
fs.Write = func(data []byte) error {
h.al.mu.Lock()
h.al.plan = make([]byte, len(data))
copy(h.al.plan, data)
h.al.mu.Unlock()
return nil
}
case "log":
fs.Write = func(data []byte) error {
input := strings.TrimSpace(string(data))
if input == "" {
return nil
}
return h.saveTranscript([]byte(input))
}
case "prompt":
fs.Write = func(data []byte) error {
input := strings.TrimSpace(string(data))
if input == "" {
return nil
}
// Handle /invalidate locally before passing to core
if input == "/invalidate" {
if h.invalidateModels != nil {
h.invalidateModels()
}
h.sess.InvalidateModelsCache()
return nil
}
h.al.mu.Lock()
h.al.prevPrompt = []byte(input)
h.al.mu.Unlock()
go func() {
h.sess.Core.Agent().Submit(h.sess.SessionCtx, input)
h.al.EnsureTrailingNewline()
}()
return nil
}
case "fifo.in":
fs.Write = func(data []byte) error {
input := strings.TrimSpace(string(data))
if input == "" {
return nil
}
h.sess.Core.Agent().Queue(input)
return nil
}
case "cfg":
fs.Write = func(data []byte) error {
input := strings.TrimSpace(string(data))
if input == "" {
return nil
}
return h.handleCfg(input)
}
case "ctl":
fs.Write = func(data []byte) error {
input := strings.TrimSpace(string(data))
if input == "" {
return nil
}
return h.handleCtl(input)
}
case "tools":
fs.Write = func(data []byte) error {
input := strings.TrimSpace(string(data))
if input == "" {
return nil
}
if h.toolRegistry == nil {
return fmt.Errorf("registry not available")
}
return h.toolRegistry.Load(h.sess.id, input)
}
}
// Wait
if name == "statewait" {
fs.Wait = func(connCtx context.Context, base string) ([]byte, string, error) {
ctx, cancel := context.WithCancel(connCtx)
defer cancel()
context.AfterFunc(h.sess.SessionCtx, cancel)
if base == "" {
base = h.sess.Core.Agent().State()
}
v, ok := h.sess.Core.WaitChange(ctx, agent.WatchState, base)
if !ok {
s := h.sess.Core.Agent().State()
return []byte(s + "\n"), s, nil
}
return []byte(v + "\n"), v, nil
}
}
// Chat stream: blocking read that delivers new log data as it arrives.
// Per-fid offset tracked via waitBase (string-encoded int64).
if name == "chat" {
fs.LongBlock = true
fs.Wait = func(connCtx context.Context, base string) ([]byte, string, error) {
ctx, cancel := context.WithCancel(connCtx)
defer cancel()
context.AfterFunc(h.sess.SessionCtx, cancel)
// Wake up on context cancellation (e.g. shutdown).
context.AfterFunc(ctx, func() { h.al.chatCond.Broadcast() })
// Parse offset from base; empty = start from current end ("now")
var offset int
if base != "" {
fmt.Sscanf(base, "%d", &offset)
} else {
h.al.mu.RLock()
offset = len(h.al.log)
h.al.mu.RUnlock()
}
// Block until new data is available or context cancelled
h.al.mu.RLock()
for len(h.al.log) <= offset {
if ctx.Err() != nil {
h.al.mu.RUnlock()
return nil, "", nil
}
h.al.chatCond.Wait()
}
// Copy new data
data := make([]byte, len(h.al.log)-offset)
copy(data, h.al.log[offset:])
newOffset := len(h.al.log)
h.al.mu.RUnlock()
return data, fmt.Sprintf("%d", newOffset), nil
}
}
return fs
}
func (h *sessionHelper) content(name string) string {
h.sess.mu.RLock()
defer h.sess.mu.RUnlock()
switch name {
case "env":
return h.envContent()
}
return ""
}
func (h *agentHelper) content(name string) string {
h.al.mu.RLock()
defer h.al.mu.RUnlock()
switch name {
case "usage":
return h.sess.Core.Agent().UsageStr() + "\n"
case "cost":
return h.sess.Core.Agent().CostStr()
case "ctxsz":
return h.sess.Core.Agent().CtxSz() + "\n"
case "models":
h.log.Debug("content(models) called")
return h.sess.CachedListModels() + "\n"
case "systemprompt":
return h.sess.Core.Agent().SystemPrompt()
case "state":
return h.sess.Core.Agent().State() + "\n"
case "offset":
return fmt.Sprintf("%d\n", h.al.ChatOffset)
case "tail":
return "#!/bin/sh\nexec tail -f \"$(dirname \"$0\")/chat\"\n"
case "context":
var sb strings.Builder
enc := json.NewEncoder(&sb)
enc.SetEscapeHTML(false)
for _, m := range h.sess.Core.Agent().Context() {
enc.Encode(m)
}
return sb.String()
case "toolsrv.loaded":
if h.toolRegistry == nil {
return "registry not available\n"
}
loaded := h.toolRegistry.Loaded(h.sess.id)
if len(loaded) == 0 {
return "(no tools loaded)\n"
}
var sb strings.Builder
for _, ti := range loaded {
if ti.Description != "" {
fmt.Fprintf(&sb, "%-20s %s\n", ti.Name, ti.Description)
} else {
sb.WriteString(ti.Name + "\n")
}
}
return sb.String()
case "toolsrv.rev":
if h.toolRegistry == nil {
return "0\n"
}
return fmt.Sprintf("%d\n", h.toolRegistry.Revision(h.sess.id))
case "tools":
if h.toolRegistry == nil {
return "registry not available\n"
}
var sb strings.Builder
for _, ti := range h.toolRegistry.Summaries() {
if ti.Description != "" {
fmt.Fprintf(&sb, "%-20s %s\n", ti.Name, ti.Description)
} else {
sb.WriteString(ti.Name + "\n")
}
}
return sb.String()
}
return ""
}
func (h *agentHelper) cfgContent() string {
h.al.mu.RLock()
defer h.al.mu.RUnlock()
p := h.sess.Core.Agent().GenParams()
var sb strings.Builder
fmt.Fprintf(&sb, "name=%s\n", h.sess.id)
fmt.Fprintf(&sb, "backend=%s\n", h.sess.Core.Agent().BackendName())
fmt.Fprintf(&sb, "model=%s\n", h.sess.Core.Agent().ModelName())
fmt.Fprintf(&sb, "agent=%s\n", h.sess.Core.Agent().Name())
fmt.Fprintf(&sb, "cwd=%s\n", h.sess.Core.CWD())
fmt.Fprintf(&sb, "remote=%s\n", h.al.remote)
fmt.Fprintf(&sb, "maxTokens=%d\n", p.MaxTokens)
fmt.Fprintf(&sb, "maxCompletionTokens=%d\n", p.MaxCompletionTokens)
if p.Temperature != nil {
fmt.Fprintf(&sb, "temperature=%g\n", *p.Temperature)
} else {
sb.WriteString("temperature=\n")
}
if p.TopP != nil {
fmt.Fprintf(&sb, "topP=%g\n", *p.TopP)
} else {
sb.WriteString("topP=\n")
}
if p.TopK != nil {
fmt.Fprintf(&sb, "topK=%d\n", *p.TopK)
} else {
sb.WriteString("topK=\n")
}
if p.MinP != nil {
fmt.Fprintf(&sb, "minP=%g\n", *p.MinP)
} else {
sb.WriteString("minP=\n")
}
if p.TopA != nil {
fmt.Fprintf(&sb, "topA=%g\n", *p.TopA)
} else {
sb.WriteString("topA=\n")
}
if p.FrequencyPenalty != nil {
fmt.Fprintf(&sb, "frequencyPenalty=%g\n", *p.FrequencyPenalty)
} else {
sb.WriteString("frequencyPenalty=\n")
}
if p.PresencePenalty != nil {
fmt.Fprintf(&sb, "presencePenalty=%g\n", *p.PresencePenalty)
} else {
sb.WriteString("presencePenalty=\n")
}
if p.RepetitionPenalty != nil {
fmt.Fprintf(&sb, "repetitionPenalty=%g\n", *p.RepetitionPenalty)
} else {
sb.WriteString("repetitionPenalty=\n")
}
fmt.Fprintf(&sb, "reasoning=%d\n", p.ThinkingBudget)
if p.ReasoningEffort != "" {
fmt.Fprintf(&sb, "reasoningEffort=%s\n", p.ReasoningEffort)
} else {
sb.WriteString("reasoningEffort=\n")
}
if p.IncludeReasoning != nil {
fmt.Fprintf(&sb, "includeReasoning=%t\n", *p.IncludeReasoning)
} else {
sb.WriteString("includeReasoning=\n")
}
if p.ResponseFormat != "" {
fmt.Fprintf(&sb, "responseFormat=%s\n", p.ResponseFormat)
} else {
sb.WriteString("responseFormat=\n")
}
if len(p.Stop) > 0 {
fmt.Fprintf(&sb, "stop=%s\n", strings.Join(p.Stop, ","))
} else {
sb.WriteString("stop=\n")
}
if p.Verbosity != "" {
fmt.Fprintf(&sb, "verbosity=%s\n", p.Verbosity)
} else {
sb.WriteString("verbosity=\n")
}
fmt.Fprintf(&sb, "compactionModel=%s\n", h.sess.Core.Agent().CompactionModel())
return sb.String()
}
func (h *agentHelper) handleCfg(input string) error {
p := h.sess.Core.Agent().GenParams()
hasParams := false
var deferredBackend, deferredModel string
for _, line := range strings.Split(input, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
k, v = strings.TrimSpace(k), strings.TrimSpace(v)
switch k {
case "name":
if v == "" {
continue
}
if h.sess.Core.Agent().IsRunning() {
return fmt.Errorf("cannot rename while agent is running")
}
return fmt.Errorf("rename not supported via agent cfg; use session/ctl rn")
case "agent":
if v == "" {
continue
}
if h.sess.Core.Agent().IsRunning() {
return fmt.Errorf("cannot switch %s while agent is running", k)
}
h.sess.Core.Agent().Submit(h.sess.SessionCtx, "/agent "+v)
case "backend", "model":
if v == "" {
continue
}
if h.sess.Core.Agent().IsRunning() {
return fmt.Errorf("cannot switch %s while agent is running", k)
}
// Defer backend/model until after the loop so they override
// any values set by agent=.
if k == "backend" {
deferredBackend = v
} else {
deferredModel = v
}
case "cwd":
if v == "" {
continue
}
return h.sess.Core.SetCWD(v)
case "maxTokens":
hasParams = true
if v == "" {
p.MaxTokens = 0
} else if n, err := strconv.Atoi(v); err == nil {
p.MaxTokens = n
}
case "maxCompletionTokens":
hasParams = true
if v == "" {
p.MaxCompletionTokens = 0
} else if n, err := strconv.Atoi(v); err == nil {
p.MaxCompletionTokens = n
}
case "temperature":
hasParams = true
if v == "" {
p.Temperature = nil
} else if f, err := strconv.ParseFloat(v, 64); err == nil {
p.Temperature = &f
}
case "topP":
hasParams = true
if v == "" {
p.TopP = nil
} else if f, err := strconv.ParseFloat(v, 64); err == nil {
p.TopP = &f
}
case "topK":
hasParams = true
if v == "" {
p.TopK = nil
} else if n, err := strconv.Atoi(v); err == nil {
p.TopK = &n
}
case "minP":
hasParams = true
if v == "" {
p.MinP = nil
} else if f, err := strconv.ParseFloat(v, 64); err == nil {
p.MinP = &f
}
case "topA":
hasParams = true
if v == "" {
p.TopA = nil
} else if f, err := strconv.ParseFloat(v, 64); err == nil {
p.TopA = &f
}
case "frequencyPenalty":
hasParams = true
if v == "" {
p.FrequencyPenalty = nil
} else if f, err := strconv.ParseFloat(v, 64); err == nil {
p.FrequencyPenalty = &f
}
case "presencePenalty":
hasParams = true
if v == "" {
p.PresencePenalty = nil
} else if f, err := strconv.ParseFloat(v, 64); err == nil {
p.PresencePenalty = &f
}
case "repetitionPenalty":
hasParams = true
if v == "" {
p.RepetitionPenalty = nil
} else if f, err := strconv.ParseFloat(v, 64); err == nil {
p.RepetitionPenalty = &f
}
case "reasoning":
hasParams = true
if v == "" {
p.ThinkingBudget = 0
} else if n, err := strconv.Atoi(v); err == nil {
p.ThinkingBudget = n
}
case "reasoningEffort":
hasParams = true
p.ReasoningEffort = v
case "includeReasoning":
hasParams = true
if v == "" {
p.IncludeReasoning = nil
} else {
b := v == "true"
p.IncludeReasoning = &b
}
case "responseFormat":
hasParams = true
p.ResponseFormat = v
case "stop":
hasParams = true
if v == "" {
p.Stop = nil
} else {
p.Stop = strings.Split(v, ",")
}
case "verbosity":
hasParams = true
p.Verbosity = v
case "compactionModel":
h.sess.Core.Agent().SetCompactionModel(v)
// state → read-only, silently ignored
}
}
// Apply backend/model after agent= so manual overrides win.
if deferredBackend != "" {
h.sess.Core.Agent().Submit(h.sess.SessionCtx, "/backend "+deferredBackend)
}
if deferredModel != "" {
h.sess.Core.Agent().Submit(h.sess.SessionCtx, "/model "+deferredModel)
}
if hasParams {
if h.sess.Core.Agent().IsRunning() {
return fmt.Errorf("cannot change params while agent is running")
}
h.sess.Core.Agent().SetGenParams(p)
}
return nil
}
func (h *agentHelper) handleCtl(input string) error {
cmd := strings.Fields(input)
if len(cmd) == 0 {
return fmt.Errorf("empty ctl command")
}
switch cmd[0] {
case "stop":
h.sess.Core.Agent().Interrupt(agent.ErrInterrupted)
case "kill", ".":
// FIXME: kill currently kills the entire session because we only support
// one agent per session. When multi-agent-per-session lands, kill should
// kill the agent only; session and chat stream persist.
h.sess.Cancel()
case "detach":
if !h.sess.Core.Agent().Detach() {
return fmt.Errorf("no running process to detach")
}
case "signal":
if len(cmd) < 2 {
return fmt.Errorf("usage: signal <pid> [TERM|KILL]")
}
pid, err := strconv.Atoi(cmd[1])
if err != nil {
return fmt.Errorf("invalid pid: %s", cmd[1])
}
sig := 15 // SIGTERM default
if len(cmd) >= 3 && strings.ToUpper(cmd[2]) == "KILL" {
sig = 9
}
if err := h.sess.Core.Agent().SignalDetached(pid, sig); err != nil {
return err
}
case "invalidate":
if h.invalidateModels != nil {
h.invalidateModels()
}
h.sess.InvalidateModelsCache()
case "compact", "clear", "backend", "model", "models",
"agents", "agent", "sessions", "cwd", "skills",
"tools", "context", "usage", "cost", "history",
"irw", "help":
h.sess.Core.Agent().Submit(h.sess.SessionCtx, "/"+input)
default:
return fmt.Errorf("unknown ctl command: %s", cmd[0])
}
return nil
}
// FormatParams formats generation parameters as key=value lines.
func FormatParams(p backend.GenerationParams) string {
var sb strings.Builder
fmt.Fprintf(&sb, "maxTokens=%d\n", p.MaxTokens)
fmt.Fprintf(&sb, "maxCompletionTokens=%d\n", p.MaxCompletionTokens)
if p.Temperature != nil {
fmt.Fprintf(&sb, "temperature=%g\n", *p.Temperature)
} else {
fmt.Fprintf(&sb, "temperature=\n")
}
if p.TopP != nil {
fmt.Fprintf(&sb, "topP=%g\n", *p.TopP)
} else {
fmt.Fprintf(&sb, "topP=\n")
}
if p.TopK != nil {
fmt.Fprintf(&sb, "topK=%d\n", *p.TopK)
} else {
fmt.Fprintf(&sb, "topK=\n")
}
if p.MinP != nil {
fmt.Fprintf(&sb, "minP=%g\n", *p.MinP)
} else {
fmt.Fprintf(&sb, "minP=\n")
}
if p.TopA != nil {
fmt.Fprintf(&sb, "topA=%g\n", *p.TopA)
} else {
fmt.Fprintf(&sb, "topA=\n")
}
if p.FrequencyPenalty != nil {
fmt.Fprintf(&sb, "frequencyPenalty=%g\n", *p.FrequencyPenalty)
} else {
fmt.Fprintf(&sb, "frequencyPenalty=\n")
}
if p.PresencePenalty != nil {
fmt.Fprintf(&sb, "presencePenalty=%g\n", *p.PresencePenalty)
} else {
fmt.Fprintf(&sb, "presencePenalty=\n")
}
if p.RepetitionPenalty != nil {
fmt.Fprintf(&sb, "repetitionPenalty=%g\n", *p.RepetitionPenalty)
} else {
fmt.Fprintf(&sb, "repetitionPenalty=\n")
}
fmt.Fprintf(&sb, "reasoning=%d\n", p.ThinkingBudget)
if p.ReasoningEffort != "" {
fmt.Fprintf(&sb, "reasoningEffort=%s\n", p.ReasoningEffort)
} else {
fmt.Fprintf(&sb, "reasoningEffort=\n")
}
if p.IncludeReasoning != nil {
fmt.Fprintf(&sb, "includeReasoning=%t\n", *p.IncludeReasoning)
} else {
fmt.Fprintf(&sb, "includeReasoning=\n")
}
if p.ResponseFormat != "" {
fmt.Fprintf(&sb, "responseFormat=%s\n", p.ResponseFormat)
} else {
fmt.Fprintf(&sb, "responseFormat=\n")
}
if len(p.Stop) > 0 {
fmt.Fprintf(&sb, "stop=%s\n", strings.Join(p.Stop, ","))
} else {
fmt.Fprintf(&sb, "stop=\n")
}
if p.Verbosity != "" {
fmt.Fprintf(&sb, "verbosity=%s\n", p.Verbosity)
} else {
fmt.Fprintf(&sb, "verbosity=\n")
}
return sb.String()
}
// ParseParams parses key=value lines into generation parameters.
func ParseParams(input string, current backend.GenerationParams) (backend.GenerationParams, error) {
p := current
for _, line := range strings.Split(input, "\n") {
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
switch k {
case "maxTokens":
if v == "" {
p.MaxTokens = 0
} else {
n, err := strconv.Atoi(v)
if err != nil {
return p, fmt.Errorf("invalid maxTokens: %s", v)
}
p.MaxTokens = n
}
case "maxCompletionTokens":
if v == "" {
p.MaxCompletionTokens = 0
} else {
n, err := strconv.Atoi(v)
if err != nil {
return p, fmt.Errorf("invalid maxCompletionTokens: %s", v)
}
p.MaxCompletionTokens = n
}
case "temperature":
if v == "" {
p.Temperature = nil
} else {
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return p, fmt.Errorf("invalid temperature: %s", v)
}
p.Temperature = &f
}
case "topP":
if v == "" {
p.TopP = nil
} else {
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return p, fmt.Errorf("invalid topP: %s", v)
}
p.TopP = &f
}
case "topK":
if v == "" {
p.TopK = nil
} else {
n, err := strconv.Atoi(v)
if err != nil {
return p, fmt.Errorf("invalid topK: %s", v)
}
p.TopK = &n
}
case "minP":
if v == "" {
p.MinP = nil
} else {
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return p, fmt.Errorf("invalid minP: %s", v)
}
p.MinP = &f
}
case "topA":
if v == "" {
p.TopA = nil
} else {
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return p, fmt.Errorf("invalid topA: %s", v)
}
p.TopA = &f
}
case "frequencyPenalty":
if v == "" {
p.FrequencyPenalty = nil
} else {
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return p, fmt.Errorf("invalid frequencyPenalty: %s", v)
}
p.FrequencyPenalty = &f
}
case "presencePenalty":
if v == "" {
p.PresencePenalty = nil
} else {
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return p, fmt.Errorf("invalid presencePenalty: %s", v)
}
p.PresencePenalty = &f
}
case "repetitionPenalty":
if v == "" {
p.RepetitionPenalty = nil
} else {
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return p, fmt.Errorf("invalid repetitionPenalty: %s", v)
}
p.RepetitionPenalty = &f
}
case "reasoning":
if v == "" {
p.ThinkingBudget = 0
} else {
n, err := strconv.Atoi(v)
if err != nil {
return p, fmt.Errorf("invalid reasoning: %s", v)
}
p.ThinkingBudget = n
}
case "reasoningEffort":
p.ReasoningEffort = v
case "includeReasoning":
if v == "" {
p.IncludeReasoning = nil
} else {
b := v == "true"
p.IncludeReasoning = &b
}
case "responseFormat":
p.ResponseFormat = v
case "stop":
if v == "" {
p.Stop = nil
} else {
p.Stop = strings.Split(v, ",")
}
case "verbosity":
p.Verbosity = v
}
}
return p, nil
}