token accounting cleanup
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"sync"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -49,6 +50,23 @@ type Cfg struct {
|
||||
Raw map[string]string
|
||||
}
|
||||
|
||||
// internalKey reports whether a model.cfg key is an agent-internal parameter
|
||||
// that must never be forwarded to the chat completions API.
|
||||
func internalKey(k string) bool {
|
||||
switch k {
|
||||
case "endpoint", "model", "temperature", "stream", "api_key", "timeout",
|
||||
"shell_timeout", "max_al_iterations", "color", "context_window":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// llmTransport is a shared HTTP transport reused across all LLM calls so that
|
||||
// connections are pooled instead of recreated per request.
|
||||
var llmTransport = &http.Transport{
|
||||
DialContext: (&net.Dialer{Timeout: 300 * time.Second}).DialContext,
|
||||
}
|
||||
|
||||
var defCfg = Cfg{"https://opencode.ai/zen/v1", "big-pickle", "-", 0.7, 300, 120, 1000, true, "auto", 200000, nil}
|
||||
|
||||
func atoiD(s string, d int) int {
|
||||
@@ -92,8 +110,24 @@ func queryModelsContextWindow(cfg *Cfg) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
var cwCacheMu sync.Mutex
|
||||
var cwCache = map[string]int{}
|
||||
|
||||
func fetchContextWindow(cfg *Cfg) int {
|
||||
// Only values discovered from the /models endpoint are cached, keyed by
|
||||
// endpoint+model. The context_window-override and 200000 default are derived
|
||||
// per call from cfg so they never shadow each other across configs.
|
||||
key := cfg.Endpoint + "\x00" + cfg.Model
|
||||
cwCacheMu.Lock()
|
||||
if cw, ok := cwCache[key]; ok {
|
||||
cwCacheMu.Unlock()
|
||||
return cw
|
||||
}
|
||||
cwCacheMu.Unlock()
|
||||
if cw := queryModelsContextWindow(cfg); cw > 0 {
|
||||
cwCacheMu.Lock()
|
||||
cwCache[key] = cw
|
||||
cwCacheMu.Unlock()
|
||||
return cw
|
||||
}
|
||||
if v, ok := cfg.Raw["context_window"]; ok {
|
||||
@@ -653,6 +687,8 @@ func (u Usage) Cached() int {
|
||||
return u.CachedTokens
|
||||
}
|
||||
|
||||
// estTokens is a coarse chars/4 fallback used only when the provider omits
|
||||
// usage in its response; when real usage is present it is never used.
|
||||
func estTokens(msgs []Message) int {
|
||||
chars := 0
|
||||
for _, m := range msgs {
|
||||
@@ -668,9 +704,13 @@ func estTokens(msgs []Message) int {
|
||||
return t
|
||||
}
|
||||
|
||||
func formatUsage(u Usage, cw int) string {
|
||||
func contextPct(u Usage, cw int) float64 {
|
||||
if cw <= 0 { cw = 200000 }
|
||||
pct := float64(u.PromptTokens) * 100.0 / float64(cw)
|
||||
return float64(u.PromptTokens) * 100.0 / float64(cw)
|
||||
}
|
||||
|
||||
func formatUsage(u Usage, cw int) string {
|
||||
pct := contextPct(u, cw)
|
||||
cached := u.Cached()
|
||||
if cached > 0 {
|
||||
uncached := u.PromptTokens - cached
|
||||
@@ -714,7 +754,7 @@ func filterText(s string) string {
|
||||
for _, r := range s {
|
||||
if r == ' ' || r == '\t' || r == '\n' {
|
||||
b.WriteRune(r)
|
||||
} else if !unicode.Is(unicode.Z, r) && !unicode.IsControl(r) && !unicode.Is(unicode.C, r) && unicode.IsPrint(r) {
|
||||
} else if unicode.IsPrint(r) {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
@@ -741,8 +781,12 @@ func sanitizeMessages(msgs []Message) {
|
||||
}
|
||||
|
||||
func isInvalidAssistantErr(err error) bool {
|
||||
s := err.Error()
|
||||
return strings.Contains(s, "Invalid assistant message") || strings.Contains(s, "content or tool_calls must be set")
|
||||
if err == nil { return false }
|
||||
s := strings.ToLower(err.Error())
|
||||
return strings.Contains(s, "invalid assistant message") ||
|
||||
strings.Contains(s, "content or tool_calls must be set") ||
|
||||
strings.Contains(s, "tool_calls must be set") ||
|
||||
strings.Contains(s, "content must be set")
|
||||
}
|
||||
|
||||
func llm(ctx context.Context, cfg *Cfg, msgs []Message, tools []map[string]any) (Message, Usage, error) {
|
||||
@@ -753,10 +797,10 @@ func llm(ctx context.Context, cfg *Cfg, msgs []Message, tools []map[string]any)
|
||||
if tools != nil { p["tools"] = tools }
|
||||
if cfg.Stream { p["stream_options"] = map[string]any{"include_usage": true} }
|
||||
for k, v := range cfg.Raw {
|
||||
switch k {
|
||||
case "endpoint", "model", "temperature", "stream", "api_key", "timeout", "shell_timeout", "max_al_iterations", "color", "context_window":
|
||||
if internalKey(k) {
|
||||
continue
|
||||
default:
|
||||
}
|
||||
{
|
||||
var jv any
|
||||
if err := json.Unmarshal([]byte(v), &jv); err == nil {
|
||||
p[k] = jv
|
||||
@@ -766,11 +810,8 @@ func llm(ctx context.Context, cfg *Cfg, msgs []Message, tools []map[string]any)
|
||||
}
|
||||
}
|
||||
body, _ := json.Marshal(p)
|
||||
client := &http.Client{Transport: &http.Transport{
|
||||
DialContext: (&net.Dialer{Timeout: time.Duration(cfg.Timeout) * time.Second}).DialContext,
|
||||
ResponseHeaderTimeout: time.Duration(cfg.Timeout) * time.Second,
|
||||
}}
|
||||
defer client.CloseIdleConnections()
|
||||
llmTransport.ResponseHeaderTimeout = time.Duration(cfg.Timeout) * time.Second
|
||||
client := &http.Client{Transport: llmTransport}
|
||||
fib := []int{1, 1, 2, 3, 5, 8, 13, 21, 34}
|
||||
pend := c("...requesting...", 1, 2)
|
||||
var resp *http.Response
|
||||
@@ -1143,8 +1184,14 @@ func saveSession(msgs []Message) (string, string) {
|
||||
path = filepath.Join(d, sid+".json")
|
||||
}
|
||||
s := Session{sid, time.Now().Format("2006-01-02 15:04:05"), summary(msgs), msgs}
|
||||
b, _ := json.MarshalIndent(s, "", " ")
|
||||
os.WriteFile(path, b, 0644)
|
||||
b, err := json.MarshalIndent(s, "", " ")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "saveSession: marshal error: %v\n", err)
|
||||
return sid, s.Summary
|
||||
}
|
||||
if err := os.WriteFile(path, b, 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "saveSession: write error: %v\n", err)
|
||||
}
|
||||
return sid, s.Summary
|
||||
}
|
||||
|
||||
@@ -1188,8 +1235,14 @@ func loadSession(sid string) ([]Message, error) {
|
||||
|
||||
func autosave(msgs []Message) {
|
||||
s := Session{"autosave", time.Now().Format("2006-01-02 15:04:05"), summary(msgs), msgs}
|
||||
b, _ := json.MarshalIndent(s, "", " ")
|
||||
os.WriteFile(filepath.Join(sdir(), "autosave.json"), b, 0644)
|
||||
b, err := json.MarshalIndent(s, "", " ")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "autosave: marshal error: %v\n", err)
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(sdir(), "autosave.json"), b, 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "autosave: write error: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
const compactionPrompt = "You are now acting as a compaction engine. Summarize the preceding conversation concisely but completely, preserving all important facts, decisions, code snippets, tool outputs, errors, and current task state so work can seamlessly continue. Output only the summary."
|
||||
@@ -1602,7 +1655,7 @@ func main() {
|
||||
msgs = resMsgs
|
||||
autosave(msgs)
|
||||
fmt.Println(c(formatUsage(usg, cfg.ContextWindow), 2))
|
||||
pct := float64(usg.PromptTokens) * 100.0 / float64(cfg.ContextWindow)
|
||||
pct := contextPct(usg, cfg.ContextWindow)
|
||||
if pct >= 60.0 && len(msgs) > 1 {
|
||||
fmt.Print(c(fmt.Sprintf("Context usage is at %.1f%% (%d / %d tokens). Compact conversation? [Y/n]: ", pct, usg.PromptTokens, cfg.ContextWindow), 33))
|
||||
if ans, ok := readPlain(""); ok {
|
||||
|
||||
Reference in New Issue
Block a user