some opts
This commit is contained in:
@@ -90,7 +90,6 @@ var defCfg = Cfg{"https://api.kilo.ai/api/openrouter", "openrouter/free", "-", 0
|
||||
|
||||
const opencodeAgentVersion = "opencode/1.18.31"
|
||||
const opencodeProviderUA = "ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14"
|
||||
const opencodeIDAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
||||
const opencodeProjectID = "global"
|
||||
|
||||
// opencodeTailAlphabet is the character set used for the 14-character random
|
||||
@@ -110,50 +109,63 @@ func atoiD(s string, d int) int {
|
||||
return d
|
||||
}
|
||||
|
||||
func queryModelsContextWindow(cfg *Cfg) int {
|
||||
func toInt(v any, def int) int {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n
|
||||
case float64:
|
||||
return int(n)
|
||||
case string:
|
||||
return atoiD(n, def)
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func fetchModels(cfg *Cfg, timeoutSec int) ([]map[string]any, error) {
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: proxyFromEnv,
|
||||
DialContext: (&net.Dialer{Timeout: 3 * time.Second}).DialContext,
|
||||
DialContext: (&net.Dialer{Timeout: time.Duration(timeoutSec) * time.Second}).DialContext,
|
||||
},
|
||||
Timeout: 3 * time.Second,
|
||||
Timeout: time.Duration(timeoutSec) * time.Second,
|
||||
}
|
||||
req, err := http.NewRequest("GET", strings.TrimRight(cfg.Endpoint, "/")+"/models", nil)
|
||||
if err != nil {
|
||||
return 0
|
||||
return nil, err
|
||||
}
|
||||
applyLLMHeaders(req, cfg, ocRequestID())
|
||||
resp, err := client.Do(req)
|
||||
if err != nil || resp.StatusCode >= 400 {
|
||||
return 0
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("HTTP %d from /models", resp.StatusCode)
|
||||
}
|
||||
var res struct {
|
||||
Data []map[string]any `json:"data"`
|
||||
Models []map[string]any `json:"models"`
|
||||
}
|
||||
if json.NewDecoder(resp.Body).Decode(&res) != nil {
|
||||
return 0
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := res.Data
|
||||
if len(list) == 0 {
|
||||
list = res.Models
|
||||
if len(res.Data) > 0 {
|
||||
return res.Data, nil
|
||||
}
|
||||
return res.Models, nil
|
||||
}
|
||||
|
||||
func queryModelsContextWindow(cfg *Cfg) int {
|
||||
list, err := fetchModels(cfg, 3)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
for _, item := range list {
|
||||
id, _ := item["id"].(string)
|
||||
if id == cfg.Model || strings.EqualFold(id, cfg.Model) {
|
||||
for _, key := range []string{"context_window", "context_length", "max_context_length", "max_model_len", "context_size", "max_tokens", "max_input_tokens"} {
|
||||
if val, ok := item[key]; ok {
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
if v > 0 {
|
||||
return int(v)
|
||||
}
|
||||
case string:
|
||||
if n := atoiD(v, 0); n > 0 {
|
||||
return n
|
||||
}
|
||||
}
|
||||
if n := toInt(item[key], 0); n > 0 {
|
||||
return n
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,37 +208,10 @@ func listModels(cfg *Cfg) (string, error) {
|
||||
if t < 10 {
|
||||
t = 10
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: proxyFromEnv,
|
||||
DialContext: (&net.Dialer{Timeout: time.Duration(t) * time.Second}).DialContext,
|
||||
},
|
||||
Timeout: time.Duration(t) * time.Second,
|
||||
}
|
||||
req, err := http.NewRequest("GET", strings.TrimRight(cfg.Endpoint, "/")+"/models", nil)
|
||||
list, err := fetchModels(cfg, t)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
applyLLMHeaders(req, cfg, ocRequestID())
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
return "", fmt.Errorf("HTTP %d from /models", resp.StatusCode)
|
||||
}
|
||||
var res struct {
|
||||
Data []map[string]any `json:"data"`
|
||||
Models []map[string]any `json:"models"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||
return "", fmt.Errorf("failed to parse /models response: %v", err)
|
||||
}
|
||||
list := res.Data
|
||||
if len(list) == 0 {
|
||||
list = res.Models
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return "No models returned by the endpoint.", nil
|
||||
}
|
||||
@@ -286,14 +271,20 @@ func parseCfgFile(path string, cfg *Cfg) {
|
||||
}
|
||||
|
||||
func applyEnvCfg(cfg *Cfg) {
|
||||
if v := strings.TrimSpace(os.Getenv("BANTAM_ENDPOINT")); v != "" {
|
||||
cfg.Endpoint = v
|
||||
cfg.Raw["endpoint"] = v
|
||||
set := func(k, env string, dst *string) {
|
||||
if v := strings.TrimSpace(os.Getenv(env)); v != "" {
|
||||
*dst = v
|
||||
cfg.Raw[k] = v
|
||||
}
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("BANTAM_MODEL")); v != "" {
|
||||
cfg.Model = v
|
||||
cfg.Raw["model"] = v
|
||||
setNum := func(k, env string, dst *int) {
|
||||
if v := strings.TrimSpace(os.Getenv(env)); v != "" {
|
||||
*dst = atoiD(v, *dst)
|
||||
cfg.Raw[k] = strconv.Itoa(*dst)
|
||||
}
|
||||
}
|
||||
set("endpoint", "BANTAM_ENDPOINT", &cfg.Endpoint)
|
||||
set("model", "BANTAM_MODEL", &cfg.Model)
|
||||
if v := strings.TrimSpace(os.Getenv("BANTAM_TEMP")); v != "" {
|
||||
if f, e := strconv.ParseFloat(v, 64); e == nil {
|
||||
cfg.Temperature = f
|
||||
@@ -309,38 +300,20 @@ func applyEnvCfg(cfg *Cfg) {
|
||||
cfg.Stream = v == "true" || v == "1" || v == "yes"
|
||||
cfg.Raw["stream"] = strconv.FormatBool(cfg.Stream)
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("BANTAM_COLOR")); v != "" {
|
||||
cfg.Color = v
|
||||
cfg.Raw["color"] = v
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("BANTAM_TIMEOUT")); v != "" {
|
||||
cfg.Timeout = atoiD(v, cfg.Timeout)
|
||||
cfg.Raw["timeout"] = strconv.Itoa(cfg.Timeout)
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("BANTAM_SHELL_TIMEOUT")); v != "" {
|
||||
cfg.ShellTimeout = atoiD(v, cfg.ShellTimeout)
|
||||
cfg.Raw["shell_timeout"] = strconv.Itoa(cfg.ShellTimeout)
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("BANTAM_MAX_AL_ITERATIONS")); v != "" {
|
||||
cfg.MaxALIterations = atoiD(v, cfg.MaxALIterations)
|
||||
cfg.Raw["max_al_iterations"] = strconv.Itoa(cfg.MaxALIterations)
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("BANTAM_CONTEXT_WINDOW")); v != "" {
|
||||
cfg.ContextWindow = atoiD(v, cfg.ContextWindow)
|
||||
cfg.Raw["context_window"] = strconv.Itoa(cfg.ContextWindow)
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("BANTAM_MAX_TOOL_RES")); v != "" {
|
||||
cfg.MaxToolRes = atoiD(v, cfg.MaxToolRes)
|
||||
cfg.Raw["max_tool_res"] = strconv.Itoa(cfg.MaxToolRes)
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("BANTAM_REASONING_EFFORT")); v != "" {
|
||||
cfg.Raw["reasoning_effort"] = v
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("BANTAM_TOOLS_DIR")); v != "" {
|
||||
cfg.Raw["bantam_tools_dir"] = v
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("BANTAM_SKILLS_DIR")); v != "" {
|
||||
cfg.Raw["bantam_skills_dir"] = v
|
||||
set("color", "BANTAM_COLOR", &cfg.Color)
|
||||
setNum("timeout", "BANTAM_TIMEOUT", &cfg.Timeout)
|
||||
setNum("shell_timeout", "BANTAM_SHELL_TIMEOUT", &cfg.ShellTimeout)
|
||||
setNum("max_al_iterations", "BANTAM_MAX_AL_ITERATIONS", &cfg.MaxALIterations)
|
||||
setNum("context_window", "BANTAM_CONTEXT_WINDOW", &cfg.ContextWindow)
|
||||
setNum("max_tool_res", "BANTAM_MAX_TOOL_RES", &cfg.MaxToolRes)
|
||||
for _, pair := range [][2]string{
|
||||
{"reasoning_effort", "BANTAM_REASONING_EFFORT"},
|
||||
{"bantam_tools_dir", "BANTAM_TOOLS_DIR"},
|
||||
{"bantam_skills_dir", "BANTAM_SKILLS_DIR"},
|
||||
} {
|
||||
if v := strings.TrimSpace(os.Getenv(pair[1])); v != "" {
|
||||
cfg.Raw[pair[0]] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,28 +412,21 @@ When generating code:
|
||||
- No emojis in code or documentation.
|
||||
- Respect AGENTS.md contents in the project.`
|
||||
|
||||
func cfgOrEnv(cfg *Cfg, key, env string) string {
|
||||
if v := strings.TrimSpace(cfg.Raw[key]); v != "" {
|
||||
return v
|
||||
}
|
||||
return strings.TrimSpace(os.Getenv(env))
|
||||
}
|
||||
|
||||
func toolsDir(cfg *Cfg) string {
|
||||
// The config key takes priority over the environment variable: a local
|
||||
// .bantam.cfg always overrides BANTAM_TOOLS_DIR.
|
||||
if v := strings.TrimSpace(cfg.Raw["bantam_tools_dir"]); v != "" {
|
||||
return v
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("BANTAM_TOOLS_DIR")); v != "" {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
return cfgOrEnv(cfg, "bantam_tools_dir", "BANTAM_TOOLS_DIR")
|
||||
}
|
||||
|
||||
// skillsDir returns the directory Bantam should scan for skills, preferring the
|
||||
// bantam_skills_dir config key, then the BANTAM_SKILLS_DIR environment variable.
|
||||
func skillsDir(cfg *Cfg) string {
|
||||
if v := strings.TrimSpace(cfg.Raw["bantam_skills_dir"]); v != "" {
|
||||
return v
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("BANTAM_SKILLS_DIR")); v != "" {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
return cfgOrEnv(cfg, "bantam_skills_dir", "BANTAM_SKILLS_DIR")
|
||||
}
|
||||
|
||||
func c(t string, cs ...int) string {
|
||||
@@ -598,9 +564,6 @@ func renderMDLine(line string, st *mdState) string {
|
||||
indent := strings.Repeat(" ", len(line)-len(strings.TrimLeft(line, " ")))
|
||||
return indent + c(m[1]+" ", 33) + renderInline(m[2])
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "|") && strings.HasSuffix(trimmed, "|") {
|
||||
return renderInline(line)
|
||||
}
|
||||
return renderInline(line)
|
||||
}
|
||||
|
||||
@@ -873,11 +836,11 @@ func renderTable(lines []string) []string {
|
||||
|
||||
var res []string
|
||||
|
||||
var topParts []string
|
||||
var horiz []string
|
||||
for _, w := range colWidths {
|
||||
topParts = append(topParts, strings.Repeat("─", w+2))
|
||||
horiz = append(horiz, strings.Repeat("─", w+2))
|
||||
}
|
||||
res = append(res, c("┌"+strings.Join(topParts, "┬")+"┐", 2))
|
||||
res = append(res, c("┌"+strings.Join(horiz, "┬")+"┐", 2))
|
||||
|
||||
if hasHeader {
|
||||
headerCols := make([][]string, numCols)
|
||||
@@ -904,11 +867,7 @@ func renderTable(lines []string) []string {
|
||||
res = append(res, c("│", 2)+strings.Join(hCells, c("│", 2))+c("│", 2))
|
||||
}
|
||||
|
||||
var midParts []string
|
||||
for _, w := range colWidths {
|
||||
midParts = append(midParts, strings.Repeat("─", w+2))
|
||||
}
|
||||
res = append(res, c("├"+strings.Join(midParts, "┼")+"┤", 2))
|
||||
res = append(res, c("├"+strings.Join(horiz, "┼")+"┤", 2))
|
||||
}
|
||||
|
||||
for _, r := range rows {
|
||||
@@ -937,11 +896,7 @@ func renderTable(lines []string) []string {
|
||||
}
|
||||
}
|
||||
|
||||
var botParts []string
|
||||
for _, w := range colWidths {
|
||||
botParts = append(botParts, strings.Repeat("─", w+2))
|
||||
}
|
||||
res = append(res, c("└"+strings.Join(botParts, "┴")+"┘", 2))
|
||||
res = append(res, c("└"+strings.Join(horiz, "┴")+"┘", 2))
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -963,17 +918,7 @@ func renderMD(text string) string {
|
||||
}
|
||||
|
||||
for _, ln := range lines {
|
||||
trimmed := strings.TrimSpace(ln)
|
||||
if strings.HasPrefix(trimmed, "```") {
|
||||
flushTable()
|
||||
out = append(out, renderMDLine(ln, &st))
|
||||
continue
|
||||
}
|
||||
if st.inCode {
|
||||
out = append(out, renderMDLine(ln, &st))
|
||||
continue
|
||||
}
|
||||
if isTableLine(ln) {
|
||||
if !st.inCode && !strings.HasPrefix(strings.TrimSpace(ln), "```") && isTableLine(ln) {
|
||||
tbl = append(tbl, ln)
|
||||
continue
|
||||
}
|
||||
@@ -1115,21 +1060,18 @@ func contextPct(u Usage, cw int) float64 {
|
||||
func formatUsage(u Usage, cw int) string {
|
||||
base := ctxTokens(u)
|
||||
pct := float64(base) * 100.0 / float64(cw)
|
||||
cached := u.Cached()
|
||||
if cached > 0 {
|
||||
pfx := ""
|
||||
if u.Model != "" {
|
||||
pfx = u.Model + ": "
|
||||
}
|
||||
if cached := u.Cached(); cached > 0 {
|
||||
uncached := u.PromptTokens - cached
|
||||
if uncached < 0 {
|
||||
uncached = 0
|
||||
}
|
||||
if u.Model != "" {
|
||||
return fmt.Sprintf("[%s: %d prompt (%d cached, %d uncached) + %d completion | context: %d/%d (%.1f%%)]", u.Model, u.PromptTokens, cached, uncached, u.CompletionTokens, base, cw, pct)
|
||||
}
|
||||
return fmt.Sprintf("[%d prompt (%d cached, %d uncached) + %d completion | context: %d/%d (%.1f%%)]", u.PromptTokens, cached, uncached, u.CompletionTokens, base, cw, pct)
|
||||
return fmt.Sprintf("[%s%d prompt (%d cached, %d uncached) + %d completion | context: %d/%d (%.1f%%)]", pfx, u.PromptTokens, cached, uncached, u.CompletionTokens, base, cw, pct)
|
||||
}
|
||||
if u.Model != "" {
|
||||
return fmt.Sprintf("[%s: %d prompt + %d completion | context: %d/%d (%.1f%%)]", u.Model, u.PromptTokens, u.CompletionTokens, base, cw, pct)
|
||||
}
|
||||
return fmt.Sprintf("[%d prompt + %d completion | context: %d/%d (%.1f%%)]", u.PromptTokens, u.CompletionTokens, base, cw, pct)
|
||||
return fmt.Sprintf("[%s%d prompt + %d completion | context: %d/%d (%.1f%%)]", pfx, u.PromptTokens, u.CompletionTokens, base, cw, pct)
|
||||
}
|
||||
|
||||
type streamDelta struct {
|
||||
@@ -1180,21 +1122,23 @@ func filterText(s string) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func sanitizeToolCalls(tcs []ToolCall) {
|
||||
for j := range tcs {
|
||||
tc := &tcs[j]
|
||||
tc.Function.Arguments = filterText(tc.Function.Arguments)
|
||||
astr := tc.Function.Arguments
|
||||
var a map[string]any
|
||||
if err := json.Unmarshal([]byte(astr), &a); err != nil || a == nil {
|
||||
fixed, _ := json.Marshal(map[string]string{"invalid_raw": astr})
|
||||
tc.Function.Arguments = string(fixed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeMessages(msgs []Message) {
|
||||
for i := range msgs {
|
||||
if msgs[i].Role == "assistant" {
|
||||
if len(msgs[i].ToolCalls) > 0 {
|
||||
for j := range msgs[i].ToolCalls {
|
||||
tc := &msgs[i].ToolCalls[j]
|
||||
tc.Function.Arguments = filterText(tc.Function.Arguments)
|
||||
astr := tc.Function.Arguments
|
||||
var a map[string]any
|
||||
if err := json.Unmarshal([]byte(astr), &a); err != nil || a == nil {
|
||||
fixed, _ := json.Marshal(map[string]string{"invalid_raw": astr})
|
||||
tc.Function.Arguments = string(fixed)
|
||||
}
|
||||
}
|
||||
}
|
||||
sanitizeToolCalls(msgs[i].ToolCalls)
|
||||
if msgs[i].ReasoningContent != "" {
|
||||
msgs[i].ReasoningContent = filterText(msgs[i].ReasoningContent)
|
||||
}
|
||||
@@ -1234,13 +1178,11 @@ func llm(ctx context.Context, cfg *Cfg, msgs []Message, tools []map[string]any)
|
||||
if internalKey(k) {
|
||||
continue
|
||||
}
|
||||
{
|
||||
var jv any
|
||||
if err := json.Unmarshal([]byte(v), &jv); err == nil {
|
||||
p[k] = jv
|
||||
} else {
|
||||
p[k] = v
|
||||
}
|
||||
var jv any
|
||||
if err := json.Unmarshal([]byte(v), &jv); err == nil {
|
||||
p[k] = jv
|
||||
} else {
|
||||
p[k] = v
|
||||
}
|
||||
}
|
||||
body, _ := json.Marshal(p)
|
||||
@@ -1503,13 +1445,7 @@ func parseStream(ctx context.Context, r io.Reader) (Message, Usage, error) {
|
||||
}
|
||||
curLine := lineBuf[:idx]
|
||||
lineBuf = lineBuf[idx+1:]
|
||||
trimmed := strings.TrimSpace(curLine)
|
||||
if strings.HasPrefix(trimmed, "```") {
|
||||
flushTable()
|
||||
fmt.Println(renderMDLine(curLine, &mdSt))
|
||||
} else if mdSt.inCode {
|
||||
fmt.Println(renderMDLine(curLine, &mdSt))
|
||||
} else if isTableLine(curLine) {
|
||||
if !mdSt.inCode && !strings.HasPrefix(strings.TrimSpace(curLine), "```") && isTableLine(curLine) {
|
||||
tblBuf = append(tblBuf, curLine)
|
||||
} else {
|
||||
flushTable()
|
||||
@@ -1663,13 +1599,8 @@ func writeFile(path string, offset, delBytes int, content string) (string, error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
dir := filepath.Dir(path)
|
||||
if dir != "" && dir != "." {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
} else if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if offset > len(data) {
|
||||
padding := make([]byte, offset-len(data))
|
||||
@@ -1711,16 +1642,13 @@ func readFileOrDir(path string, offset, limit int) (string, int) {
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[tool error: read %s: %v]", path, err), 31
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].Name() < entries[j].Name()
|
||||
})
|
||||
var b strings.Builder
|
||||
for _, e := range entries {
|
||||
b.WriteString(e.Name())
|
||||
if e.IsDir() {
|
||||
b.WriteString(e.Name() + "/\n")
|
||||
} else {
|
||||
b.WriteString(e.Name() + "\n")
|
||||
b.WriteByte('/')
|
||||
}
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\n"), 2
|
||||
}
|
||||
@@ -1766,13 +1694,17 @@ func readFileOrDir(path string, offset, limit int) (string, int) {
|
||||
return strings.TrimRight(b.String(), "\n"), 2
|
||||
}
|
||||
|
||||
// toolResDir returns the directory where oversized tool results are spilled.
|
||||
func toolResDir() string {
|
||||
func bantamTmpDir() string {
|
||||
tmp := os.Getenv("TMPDIR")
|
||||
if tmp == "" {
|
||||
tmp = "/tmp"
|
||||
}
|
||||
return filepath.Join(tmp, "bantam", "toolres")
|
||||
return filepath.Join(tmp, "bantam")
|
||||
}
|
||||
|
||||
// toolResDir returns the directory where oversized tool results are spilled.
|
||||
func toolResDir() string {
|
||||
return filepath.Join(bantamTmpDir(), "toolres")
|
||||
}
|
||||
|
||||
// offloadToolResult keeps tool results that fit within maxRes bytes inline.
|
||||
@@ -1818,6 +1750,14 @@ func cleanupTemps(files []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func toolPath(a map[string]any) string {
|
||||
p, _ := a["filePath"].(string)
|
||||
if p == "" {
|
||||
p, _ = a["path"].(string)
|
||||
}
|
||||
return filterText(p)
|
||||
}
|
||||
|
||||
func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error) {
|
||||
done := false
|
||||
var turnUsage Usage
|
||||
@@ -1860,16 +1800,7 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error)
|
||||
if u.Model != "" {
|
||||
turnUsage.Model = u.Model
|
||||
}
|
||||
for j := range m.ToolCalls {
|
||||
tc := &m.ToolCalls[j]
|
||||
tc.Function.Arguments = filterText(tc.Function.Arguments)
|
||||
astr := tc.Function.Arguments
|
||||
var a map[string]any
|
||||
if err := json.Unmarshal([]byte(astr), &a); err != nil || a == nil {
|
||||
fixed, _ := json.Marshal(map[string]string{"invalid_raw": astr})
|
||||
tc.Function.Arguments = string(fixed)
|
||||
}
|
||||
}
|
||||
sanitizeToolCalls(m.ToolCalls)
|
||||
msgs = append(msgs, m)
|
||||
if !cfg.Stream {
|
||||
if m.ReasoningContent != "" {
|
||||
@@ -1910,24 +1841,11 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error)
|
||||
workdir, _ := a["workdir"].(string)
|
||||
workdir = filterText(workdir)
|
||||
to := cfg.ShellTimeout
|
||||
if v, ok := a["timeout"]; ok && v != nil {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
if n > 0 {
|
||||
if n > 1000 {
|
||||
to = int(n / 1000)
|
||||
} else {
|
||||
to = int(n)
|
||||
}
|
||||
}
|
||||
case int:
|
||||
if n > 0 {
|
||||
if n > 1000 {
|
||||
to = n / 1000
|
||||
} else {
|
||||
to = n
|
||||
}
|
||||
}
|
||||
if n := toInt(a["timeout"], 0); n > 0 {
|
||||
if n > 1000 {
|
||||
to = n / 1000
|
||||
} else {
|
||||
to = n
|
||||
}
|
||||
}
|
||||
res = shellWithWorkdir(ctx, cmd, workdir, to)
|
||||
@@ -1935,40 +1853,9 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error)
|
||||
return msgs, turnUsage, err
|
||||
}
|
||||
case "read":
|
||||
path, _ := a["filePath"].(string)
|
||||
if path == "" {
|
||||
path, _ = a["path"].(string)
|
||||
}
|
||||
path = filterText(path)
|
||||
offset := 1
|
||||
if v, ok := a["offset"]; ok && v != nil {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
offset = int(n)
|
||||
case int:
|
||||
offset = n
|
||||
case string:
|
||||
offset = atoiD(n, 1)
|
||||
}
|
||||
}
|
||||
limit := 2000
|
||||
if v, ok := a["limit"]; ok && v != nil {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
limit = int(n)
|
||||
case int:
|
||||
limit = n
|
||||
case string:
|
||||
limit = atoiD(n, 2000)
|
||||
}
|
||||
}
|
||||
res, sty = readFileOrDir(path, offset, limit)
|
||||
res, sty = readFileOrDir(toolPath(a), toInt(a["offset"], 1), toInt(a["limit"], 2000))
|
||||
case "write", "write_file":
|
||||
path, _ := a["filePath"].(string)
|
||||
if path == "" {
|
||||
path, _ = a["path"].(string)
|
||||
}
|
||||
path = filterText(path)
|
||||
path := toolPath(a)
|
||||
contentVal, hasContent := a["content"]
|
||||
var content string
|
||||
if hasContent && contentVal != nil {
|
||||
@@ -1976,63 +1863,32 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error)
|
||||
content = s
|
||||
}
|
||||
}
|
||||
hasOffset := false
|
||||
offset := 0
|
||||
if v, ok := a["offset"]; ok && v != nil {
|
||||
hasOffset = true
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
offset = int(n)
|
||||
case int:
|
||||
offset = n
|
||||
case string:
|
||||
offset = atoiD(n, 0)
|
||||
}
|
||||
}
|
||||
hasOffset := a["offset"] != nil
|
||||
offset := toInt(a["offset"], 0)
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
hasDel := false
|
||||
delBytes := 0
|
||||
if v, ok := a["del_bytes"]; ok {
|
||||
hasDel = true
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
delBytes = int(n)
|
||||
case int:
|
||||
delBytes = n
|
||||
case string:
|
||||
delBytes = atoiD(n, 0)
|
||||
}
|
||||
}
|
||||
hasDel := a["del_bytes"] != nil
|
||||
delBytes := toInt(a["del_bytes"], 0)
|
||||
if strings.TrimSpace(path) == "" {
|
||||
res, sty = fmt.Sprintf("[tool error: %s requires 'filePath' parameter]", fn), 31
|
||||
} else if !hasContent {
|
||||
res, sty = fmt.Sprintf("[tool error: %s requires 'content' parameter]", fn), 31
|
||||
} else if !hasOffset && !hasDel {
|
||||
// Neither offset nor del_bytes was supplied: overwrite the entire
|
||||
// file with the new content. The LLM usually just wants to replace a
|
||||
// file and should not have to know about the tool's offset quirks;
|
||||
// insertion/replace semantics are preserved when either is given.
|
||||
p := strings.TrimSpace(path)
|
||||
if dir := filepath.Dir(p); dir != "" && dir != "." {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
res, sty = fmt.Sprintf("[tool error: %s %s: %v]", fn, p, err), 31
|
||||
}
|
||||
}
|
||||
if res == "" {
|
||||
if err := os.WriteFile(p, []byte(content), 0644); err != nil {
|
||||
res, sty = fmt.Sprintf("[tool error: %s %s: %v]", fn, p, err), 31
|
||||
} else {
|
||||
res, sty = fmt.Sprintf("Successfully wrote %d bytes to %s", len(content), p), 2
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
||||
res, sty = fmt.Sprintf("[tool error: %s %s: %v]", fn, p, err), 31
|
||||
} else if err := os.WriteFile(p, []byte(content), 0644); err != nil {
|
||||
res, sty = fmt.Sprintf("[tool error: %s %s: %v]", fn, p, err), 31
|
||||
} else {
|
||||
res = fmt.Sprintf("Successfully wrote %d bytes to %s", len(content), p)
|
||||
}
|
||||
} else {
|
||||
out, err := writeFile(path, offset, delBytes, content)
|
||||
if err != nil {
|
||||
res, sty = fmt.Sprintf("[tool error: %s %s: %v]", fn, path, err), 31
|
||||
} else {
|
||||
res, sty = out, 2
|
||||
res = out
|
||||
}
|
||||
}
|
||||
default:
|
||||
@@ -2105,6 +1961,21 @@ func projectID() string {
|
||||
// timestamp prefix (6 bytes, big endian) and a 14-character random Base62 tail.
|
||||
// When descending is true (session IDs), the timestamp value is bitwise inverted (~$),
|
||||
// ensuring session IDs and request IDs form bitwise inverse hex prefixes.
|
||||
func ocRandomTail() string {
|
||||
buf := make([]byte, 14)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
h := md5.Sum([]byte(fmt.Sprintf("%d-%d", time.Now().UnixNano(), os.Getpid())))
|
||||
for i := range buf {
|
||||
buf[i] = h[i%len(h)]
|
||||
}
|
||||
}
|
||||
var tail strings.Builder
|
||||
for _, b := range buf {
|
||||
tail.WriteByte(opencodeTailAlphabet[int(b)%len(opencodeTailAlphabet)])
|
||||
}
|
||||
return tail.String()
|
||||
}
|
||||
|
||||
func genOpencodeID(prefix string, descending bool) string {
|
||||
now := time.Now().UnixMilli()
|
||||
ocMu.Lock()
|
||||
@@ -2121,19 +1992,7 @@ func genOpencodeID(prefix string, descending bool) string {
|
||||
val = ^val
|
||||
}
|
||||
hexPart := fmt.Sprintf("%012x", val&0xffffffffffff)
|
||||
|
||||
buf := make([]byte, 14)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
h := md5.Sum([]byte(fmt.Sprintf("%d-%d", time.Now().UnixNano(), os.Getpid())))
|
||||
for i := range buf {
|
||||
buf[i] = h[i%len(h)]
|
||||
}
|
||||
}
|
||||
var tail strings.Builder
|
||||
for _, b := range buf {
|
||||
tail.WriteByte(opencodeTailAlphabet[int(b)%len(opencodeTailAlphabet)])
|
||||
}
|
||||
return prefix + hexPart + tail.String()
|
||||
return prefix + hexPart + ocRandomTail()
|
||||
}
|
||||
|
||||
func opencodeSessionID() string {
|
||||
@@ -2144,22 +2003,8 @@ func opencodeSessionID() string {
|
||||
// is the exact bitwise inverse of the given msg_ request ID.
|
||||
func opencodeSessionIDFromRequest(msgID string) string {
|
||||
if strings.HasPrefix(msgID, "msg_") && len(msgID) >= 16 {
|
||||
hexPart := msgID[4:16]
|
||||
if val, err := strconv.ParseUint(hexPart, 16, 64); err == nil {
|
||||
invVal := (^val) & 0xffffffffffff
|
||||
invHex := fmt.Sprintf("%012x", invVal)
|
||||
buf := make([]byte, 14)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
h := md5.Sum([]byte(fmt.Sprintf("%d-%d", time.Now().UnixNano(), os.Getpid())))
|
||||
for i := range buf {
|
||||
buf[i] = h[i%len(h)]
|
||||
}
|
||||
}
|
||||
var tail strings.Builder
|
||||
for _, b := range buf {
|
||||
tail.WriteByte(opencodeTailAlphabet[int(b)%len(opencodeTailAlphabet)])
|
||||
}
|
||||
return "ses_" + invHex + tail.String()
|
||||
if val, err := strconv.ParseUint(msgID[4:16], 16, 64); err == nil {
|
||||
return fmt.Sprintf("ses_%012x", (^val)&0xffffffffffff) + ocRandomTail()
|
||||
}
|
||||
}
|
||||
return genOpencodeID("ses_", true)
|
||||
@@ -2692,12 +2537,8 @@ func skillPrompt(u string, cfg *Cfg) (string, error) {
|
||||
if rest == "" {
|
||||
return "", fmt.Errorf("usage: /skill <skill_name|absolute_path> [prompt]")
|
||||
}
|
||||
fields := strings.SplitN(rest, " ", 2)
|
||||
name := fields[0]
|
||||
prompt := ""
|
||||
if len(fields) == 2 {
|
||||
prompt = strings.TrimSpace(fields[1])
|
||||
}
|
||||
name, prompt, _ := strings.Cut(rest, " ")
|
||||
prompt = strings.TrimSpace(prompt)
|
||||
sd := skillsDir(cfg)
|
||||
var path string
|
||||
if sd != "" {
|
||||
@@ -2791,14 +2632,31 @@ func doCompact(cfg *Cfg, msgs []Message) []Message {
|
||||
// $TMPDIR/bantam placeholder to a concrete temporary directory, substitutes the
|
||||
// configured max_tool_res value, and appends the optional extra-tools and
|
||||
// skills hints.
|
||||
func buildSystemPrompt(cfg *Cfg) string {
|
||||
sp := defaultSystemPrompt
|
||||
tmp := os.Getenv("TMPDIR")
|
||||
if tmp == "" {
|
||||
tmp = "/tmp"
|
||||
func runTurn(cfg *Cfg, msgs []Message) ([]Message, Usage, error) {
|
||||
sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
resMsgs, usg, err := AL(sigCtx, cfg, msgs)
|
||||
if lastRole(resMsgs) == "tool" {
|
||||
resMsgs = append(resMsgs, Message{Role: "user", Content: strp("continue")})
|
||||
fmt.Println(c("[auto continue: last message was a tool result]", 33))
|
||||
resMsgs, usg, err = AL(sigCtx, cfg, resMsgs)
|
||||
}
|
||||
bantamTmp := filepath.Join(tmp, "bantam")
|
||||
sp = strings.ReplaceAll(sp, "$TMPDIR/bantam", bantamTmp)
|
||||
if err != nil {
|
||||
if sigCtx.Err() != nil || errors.Is(err, context.Canceled) {
|
||||
fmt.Println(c("\n[interrupted]", 33))
|
||||
} else {
|
||||
fmt.Println(c("[error: "+err.Error()+"]", 31))
|
||||
}
|
||||
}
|
||||
return resMsgs, usg, err
|
||||
}
|
||||
|
||||
// buildSystemPrompt renders the built-in system prompt for cfg: it expands the
|
||||
// $TMPDIR/bantam placeholder to a concrete temporary directory, substitutes the
|
||||
// configured max_tool_res value, and appends the optional extra-tools and
|
||||
// skills hints.
|
||||
func buildSystemPrompt(cfg *Cfg) string {
|
||||
sp := strings.ReplaceAll(defaultSystemPrompt, "$TMPDIR/bantam", bantamTmpDir())
|
||||
sp = strings.ReplaceAll(sp, "$MAX_TOOL_RES", strconv.Itoa(cfg.MaxToolRes))
|
||||
if td := toolsDir(cfg); td != "" {
|
||||
sp += "\n\nExtra shell tools can be found at " + td
|
||||
@@ -2829,22 +2687,9 @@ func main() {
|
||||
return
|
||||
}
|
||||
msgs = append(msgs, Message{Role: "user", Content: strp(u)})
|
||||
sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
var usg Usage
|
||||
msgs, usg, err = AL(sigCtx, &cfg, msgs)
|
||||
if lastRole(msgs) == "tool" {
|
||||
msgs = append(msgs, Message{Role: "user", Content: strp("continue")})
|
||||
fmt.Println(c("[auto continue: last message was a tool result]", 33))
|
||||
msgs, usg, err = AL(sigCtx, &cfg, msgs)
|
||||
}
|
||||
interrupted := sigCtx.Err() != nil
|
||||
cancel()
|
||||
msgs, usg, err = runTurn(&cfg, msgs)
|
||||
if err != nil {
|
||||
if interrupted || errors.Is(err, context.Canceled) {
|
||||
fmt.Println(c("\n[interrupted]", 33))
|
||||
} else {
|
||||
fmt.Println(c("[error: "+err.Error()+"]", 31))
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(c(formatUsage(usg, cfg.ContextWindow), 2))
|
||||
@@ -2887,14 +2732,12 @@ func main() {
|
||||
addHistory(u)
|
||||
// Command aliases: /model -> /cfg model, /endpoint -> /cfg endpoint.
|
||||
// (Note: /models is a distinct command and is intentionally not matched.)
|
||||
if u == "/model" {
|
||||
u = "/cfg model"
|
||||
} else if strings.HasPrefix(u, "/model ") {
|
||||
u = "/cfg model " + strings.TrimSpace(strings.TrimPrefix(u, "/model "))
|
||||
} else if u == "/endpoint" {
|
||||
u = "/cfg endpoint"
|
||||
} else if strings.HasPrefix(u, "/endpoint ") {
|
||||
u = "/cfg endpoint " + strings.TrimSpace(strings.TrimPrefix(u, "/endpoint "))
|
||||
for _, cmd := range []string{"model", "endpoint"} {
|
||||
if u == "/"+cmd {
|
||||
u = "/cfg " + cmd
|
||||
} else if strings.HasPrefix(u, "/"+cmd+" ") {
|
||||
u = "/cfg " + cmd + " " + strings.TrimSpace(u[len(cmd)+2:])
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case u == "/quit":
|
||||
@@ -3023,23 +2866,9 @@ func main() {
|
||||
}
|
||||
continue
|
||||
}
|
||||
turnMsgs := append([]Message{}, msgs...)
|
||||
turnMsgs = append(turnMsgs, Message{Role: "user", Content: strp(u)})
|
||||
sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
resMsgs, usg, err := AL(sigCtx, &cfg, turnMsgs)
|
||||
if lastRole(resMsgs) == "tool" {
|
||||
resMsgs = append(resMsgs, Message{Role: "user", Content: strp("continue")})
|
||||
fmt.Println(c("[auto continue: last message was a tool result]", 33))
|
||||
resMsgs, usg, err = AL(sigCtx, &cfg, resMsgs)
|
||||
}
|
||||
interrupted := sigCtx.Err() != nil
|
||||
cancel()
|
||||
turnMsgs := append(append([]Message{}, msgs...), Message{Role: "user", Content: strp(u)})
|
||||
resMsgs, usg, err := runTurn(&cfg, turnMsgs)
|
||||
if err != nil {
|
||||
if interrupted || errors.Is(err, context.Canceled) {
|
||||
fmt.Println(c("\n[interrupted]", 33))
|
||||
} else {
|
||||
fmt.Println(c("[error: "+err.Error()+"]", 31))
|
||||
}
|
||||
continue
|
||||
}
|
||||
msgs = resMsgs
|
||||
|
||||
Reference in New Issue
Block a user