some opts

This commit is contained in:
Luxferre
2026-09-19 00:33:48 +03:00
parent e1efe70d8a
commit 3b5a8a5bfd
+176 -347
View File
@@ -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,54 +109,67 @@ 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 {
if n := toInt(item[key], 0); n > 0 {
return n
}
}
}
}
}
}
return 0
}
@@ -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
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
}
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
}
}
@@ -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("[%s%d prompt (%d cached, %d uncached) + %d completion | context: %d/%d (%.1f%%)]", pfx, 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)
}
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,12 +1122,9 @@ func filterText(s string) string {
return b.String()
}
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]
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
@@ -1194,7 +1133,12 @@ func sanitizeMessages(msgs []Message) {
tc.Function.Arguments = string(fixed)
}
}
}
}
func sanitizeMessages(msgs []Message) {
for i := range msgs {
if msgs[i].Role == "assistant" {
sanitizeToolCalls(msgs[i].ToolCalls)
if msgs[i].ReasoningContent != "" {
msgs[i].ReasoningContent = filterText(msgs[i].ReasoningContent)
}
@@ -1234,7 +1178,6 @@ 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
@@ -1242,7 +1185,6 @@ func llm(ctx context.Context, cfg *Cfg, msgs []Message, tools []map[string]any)
p[k] = v
}
}
}
body, _ := json.Marshal(p)
llmTransport.ResponseHeaderTimeout = time.Duration(cfg.Timeout) * time.Second
client := &http.Client{Transport: llmTransport}
@@ -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,14 +1599,9 @@ 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 {
} else if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return "", err
}
}
}
if offset > len(data) {
padding := make([]byte, offset-len(data))
data = append(data, padding...)
@@ -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,65 +1841,21 @@ 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 := toInt(a["timeout"], 0); n > 0 {
if n > 1000 {
to = n / 1000
} else {
to = n
}
}
}
}
res = shellWithWorkdir(ctx, cmd, workdir, to)
if err := ctx.Err(); err != nil {
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 {
if err := os.MkdirAll(filepath.Dir(p), 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 {
} 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, sty = fmt.Sprintf("Successfully wrote %d bytes to %s", len(content), p), 2
}
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