first commit
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
// This file implements the /btw model/thinking override config (US-005, #282):
|
||||
// an optional per-command config that lets a side thread use a different model
|
||||
// and/or reasoning effort than the main session, without touching the main
|
||||
// session's settings (mirrors pi-btw's pi-btw.json).
|
||||
//
|
||||
// The config lives at $PIGO_HOME/btw.json (or ~/.pigo/btw.json). It is read
|
||||
// fresh on every /btw invocation, so editing it takes effect on the next call
|
||||
// with no restart. A missing file, an empty object, or an absent field all mean
|
||||
// "inherit the session default" silently — only a malformed file or an
|
||||
// unusable model override produces a (non-fatal) warning.
|
||||
package btw
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/smallnest/pigo/internal/agentcore"
|
||||
"github.com/smallnest/pigo/internal/cli"
|
||||
"github.com/smallnest/pigo/internal/cli/run"
|
||||
"github.com/smallnest/pigo/internal/cli/ui"
|
||||
"github.com/smallnest/pigo/internal/provider"
|
||||
)
|
||||
|
||||
// btwConfig is the on-disk shape of ~/.pigo/btw.json. Both fields are optional;
|
||||
// an absent field (nil / empty) inherits the session default. Pointers/empty
|
||||
// strings distinguish "not set" from a real value so a partial file still falls
|
||||
// back per-field.
|
||||
type btwConfig struct {
|
||||
Model string `json:"model,omitempty"`
|
||||
ThinkingLevel string `json:"thinkingLevel,omitempty"`
|
||||
}
|
||||
|
||||
// BtwRunSettings is the resolved model/provider/thinking a side run uses. It is
|
||||
// computed once per /btw invocation from the session defaults overlaid with
|
||||
// btw.json, and passed down to AskSide so every turn of that invocation uses
|
||||
// the same settings.
|
||||
type BtwRunSettings struct {
|
||||
Model string
|
||||
ProviderName string
|
||||
Provider provider.Provider
|
||||
ThinkingLevel agentcore.ThinkingLevel
|
||||
}
|
||||
|
||||
// btwConfigPath returns the path to the /btw override config, or "" when the
|
||||
// config directory cannot be resolved (then the config is treated as absent).
|
||||
func btwConfigPath() string {
|
||||
dir := run.ConfigDir()
|
||||
if dir == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(dir, "btw.json")
|
||||
}
|
||||
|
||||
// loadBtwConfig reads and parses btw.json. A missing file returns a zero config
|
||||
// with no error (inherit everything). A malformed file returns an error so the
|
||||
// caller can warn and fall back. An empty object parses to a zero config.
|
||||
func loadBtwConfig(path string) (btwConfig, error) {
|
||||
if path == "" {
|
||||
return btwConfig{}, nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return btwConfig{}, nil
|
||||
}
|
||||
return btwConfig{}, err
|
||||
}
|
||||
var cfg btwConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return btwConfig{}, fmt.Errorf("parse %s: %w", path, err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// ResolveBtwSettings computes the model/provider/thinking a side run should use.
|
||||
// It starts from the session defaults (host.Live()) and overlays btw.json:
|
||||
//
|
||||
// - No config / empty object / absent fields → inherit the session values.
|
||||
// - thinkingLevel set → validate and override (invalid value warns, falls back).
|
||||
// - model set → resolve its provider (reusing resolveProvider like /model);
|
||||
// if the model cannot be resolved/authenticated, warn on one line and fall
|
||||
// back to the session model+provider.
|
||||
//
|
||||
// A malformed config file warns once and inherits everything. Nothing here
|
||||
// mutates the session live config, so the override is confined to the side
|
||||
// thread (FR-8).
|
||||
func ResolveBtwSettings(out io.Writer, host cli.Host) BtwRunSettings {
|
||||
live := host.Live()
|
||||
s := BtwRunSettings{
|
||||
Model: live.Model,
|
||||
ProviderName: live.ProviderName,
|
||||
Provider: live.Provider,
|
||||
ThinkingLevel: live.ThinkingLevel,
|
||||
}
|
||||
|
||||
cfg, err := loadBtwConfig(btwConfigPath())
|
||||
if err != nil {
|
||||
fmt.Fprintf(out, "%s\n", ui.Colorize(ui.Enabled(), ui.Dim, "btw: ignoring invalid btw.json: "+err.Error()))
|
||||
return s
|
||||
}
|
||||
|
||||
if lvl := strings.TrimSpace(cfg.ThinkingLevel); lvl != "" {
|
||||
if v, ok := validThinkingLevel(lvl); ok {
|
||||
s.ThinkingLevel = v
|
||||
} else {
|
||||
fmt.Fprintf(out, "%s\n", ui.Colorize(ui.Enabled(), ui.Dim, fmt.Sprintf("btw: ignoring invalid thinkingLevel %q, using %q", lvl, s.ThinkingLevel)))
|
||||
}
|
||||
}
|
||||
|
||||
if model := strings.TrimSpace(cfg.Model); model != "" && model != s.Model {
|
||||
prov, providerName, perr := provider.ResolveProvider(model, live.BaseURL, live.Protocol, "", os.Getenv)
|
||||
if perr != nil {
|
||||
fmt.Fprintf(out, "%s\n", ui.Colorize(ui.Enabled(), ui.Dim, fmt.Sprintf("btw: cannot use model %q (%v), falling back to %q", model, perr, s.Model)))
|
||||
} else {
|
||||
s.Model = model
|
||||
s.ProviderName = providerName
|
||||
s.Provider = prov
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// validThinkingLevel reports whether s is one of the known reasoning-effort
|
||||
// levels and returns the typed value. It mirrors the enum in agentcore so an
|
||||
// invalid btw.json value can be rejected without importing the config layer.
|
||||
func validThinkingLevel(s string) (agentcore.ThinkingLevel, bool) {
|
||||
switch agentcore.ThinkingLevel(s) {
|
||||
case agentcore.ThinkingOff, agentcore.ThinkingMinimal, agentcore.ThinkingLow,
|
||||
agentcore.ThinkingMedium, agentcore.ThinkingHigh, agentcore.ThinkingXHigh, agentcore.ThinkingMax:
|
||||
return agentcore.ThinkingLevel(s), true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user