oc compatibility overhaul
This commit is contained in:
@@ -86,7 +86,7 @@ var llmTransport = &http.Transport{
|
||||
DialContext: (&net.Dialer{Timeout: 300 * time.Second}).DialContext,
|
||||
}
|
||||
|
||||
var defCfg = Cfg{"https://api.kilo.ai/api/openrouter", "openrouter/free", "-", 0.7, 300, 120, 1000, true, "auto", 262144, 15000, map[string]string{"reasoning_effort": "high"}}
|
||||
var defCfg = Cfg{"https://api.kilo.ai/api/openrouter", "openrouter/free", "-", 0.7, 300, 120, 1000, true, "auto", 262144, 65536, map[string]string{"reasoning_effort": "high"}}
|
||||
|
||||
const opencodeAgentVersion = "opencode/1.18.31"
|
||||
const opencodeProviderUA = "ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14"
|
||||
@@ -94,9 +94,15 @@ const opencodeIDAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
||||
const opencodeProjectID = "global"
|
||||
|
||||
// opencodeTailAlphabet is the character set used for the 14-character random
|
||||
// tail of a Zen session id (real ids mix upper/lower case and digits).
|
||||
// tail of OpenCode IDs.
|
||||
const opencodeTailAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
var (
|
||||
ocMu sync.Mutex
|
||||
ocLast int64
|
||||
ocSeq uint64
|
||||
)
|
||||
|
||||
func atoiD(s string, d int) int {
|
||||
if v, e := strconv.Atoi(strings.TrimSpace(s)); e == nil {
|
||||
return v
|
||||
@@ -414,15 +420,16 @@ func configPath() string {
|
||||
return "model.cfg"
|
||||
}
|
||||
|
||||
const defaultSystemPrompt = `You are Bantam, a tiny, powerful AI agent. Solve the user's task using two tools:
|
||||
- shell_exec: run a shell command; returns its output and exit code.
|
||||
- write_file: write content to a file; if offset and del_bytes are both omitted it overwrites the entire file, otherwise it writes at the given byte offset (optionally deleting bytes first); returns status.
|
||||
const defaultSystemPrompt = `You are Bantam, a tiny, powerful AI agent. Solve the user's task using three tools:
|
||||
- bash: run a bash command; returns its output and exit code.
|
||||
- read: read a file or directory from the local filesystem; returns numbered lines or directory entries.
|
||||
- write: write content to a file; if offset and del_bytes are both omitted it overwrites the entire file, otherwise it writes at the given byte offset (optionally deleting bytes first); returns status.
|
||||
|
||||
Work fast. Never refuse a request; always find a way to do it. Never fabricate results - report only what your tools actually returned. Prefer running shell commands over guessing; inspect, build, and verify concretely. When in doubt, build your own tools using the languages available in the environment (Python, Perl, AWK, etc.). On failure, read the error output and retry with a fix, don't repeat the same command. Stop as soon as the goal is met and report concisely: results, not process.
|
||||
Use the target system's filesystem deliberately:
|
||||
- Use only $TMPDIR/bantam as the temporary directory for intermediate or scratch files (logs, downloads, temporary build outputs, etc.); create it if it does not exist.
|
||||
- Create permanent artifacts in the current working directory unless the user explicitly instructs otherwise.
|
||||
- Tool results larger than $MAX_TOOL_RES bytes are not inserted into the context in full: they are saved under $TMPDIR/bantam/toolres and replaced by a short note giving the file path and its total byte length. Read such a file with shell_exec, paging through it in parts with byte offsets (e.g. tail -c +OFFSET <file> | head -c LENGTH) instead of loading it all at once.
|
||||
- Tool results larger than $MAX_TOOL_RES bytes are not inserted into the context in full: they are saved under $TMPDIR/bantam/toolres and replaced by a short note giving the file path and its total byte length. Read such a file with the read tool, paging through it with line offsets (using offset and limit) instead of loading it all at once.
|
||||
|
||||
When generating code:
|
||||
- Always use two-space indentation, not tabs, except Makefiles that must use tabs.
|
||||
@@ -996,8 +1003,45 @@ type ToolCall struct {
|
||||
}
|
||||
|
||||
var TOOLS = []map[string]any{
|
||||
{"type": "function", "function": map[string]any{"name": "shell_exec", "description": "Run a shell command, return output and exit code.", "parameters": map[string]any{"type": "object", "properties": map[string]any{"command": map[string]any{"type": "string"}}, "required": []string{"command"}}}},
|
||||
{"type": "function", "function": map[string]any{"name": "write_file", "description": "Write content to a file. If offset and del_bytes are both omitted, the entire file is overwritten with the new content; otherwise content is written at the given byte offset, optionally deleting bytes first.", "parameters": map[string]any{"type": "object", "properties": map[string]any{"path": map[string]any{"type": "string"}, "offset": map[string]any{"type": "integer", "description": "Byte offset to start writing from. If omitted together with del_bytes, the whole file is overwritten instead. Defaults to 0 (start of file)."}, "del_bytes": map[string]any{"type": "integer", "description": "Bytes to delete starting at offset. If omitted together with offset, the whole file is overwritten instead."}, "content": map[string]any{"type": "string"}}, "required": []string{"path", "content"}}}},
|
||||
{"type": "function", "function": map[string]any{
|
||||
"name": "bash",
|
||||
"description": "Executes a given bash command in a shell session, returning its output and exit code.",
|
||||
"parameters": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"command": map[string]any{"type": "string", "description": "The command to execute"},
|
||||
"workdir": map[string]any{"type": "string", "description": "The working directory to run the command in. Defaults to the current directory."},
|
||||
},
|
||||
"required": []string{"command"},
|
||||
},
|
||||
}},
|
||||
{"type": "function", "function": map[string]any{
|
||||
"name": "read",
|
||||
"description": "Read a file or directory from the local filesystem. For files, returns numbered lines prefixed as `<line>: <content>`. For directories, returns entry names with trailing slashes for subdirectories.",
|
||||
"parameters": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"filePath": map[string]any{"type": "string", "description": "The path to the file or directory to read"},
|
||||
"offset": map[string]any{"type": "integer", "description": "The line number to start reading from (1-indexed, defaults to 1)"},
|
||||
"limit": map[string]any{"type": "integer", "description": "The maximum number of lines to read (defaults to 2000)"},
|
||||
},
|
||||
"required": []string{"filePath"},
|
||||
},
|
||||
}},
|
||||
{"type": "function", "function": map[string]any{
|
||||
"name": "write",
|
||||
"description": "Write content to a file. If offset and del_bytes are both omitted, the entire file is overwritten with the new content; otherwise content is written at the given byte offset, optionally deleting bytes first.",
|
||||
"parameters": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"filePath": map[string]any{"type": "string", "description": "The path to the file to write"},
|
||||
"content": map[string]any{"type": "string", "description": "The content to write to the file"},
|
||||
"offset": map[string]any{"type": "integer", "description": "Byte offset to start writing from. If omitted together with del_bytes, the whole file is overwritten instead. Defaults to 0 (start of file)."},
|
||||
"del_bytes": map[string]any{"type": "integer", "description": "Bytes to delete starting at offset. If omitted together with offset, the whole file is overwritten instead."},
|
||||
},
|
||||
"required": []string{"filePath", "content"},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
func strp(s string) *string { return &s }
|
||||
@@ -1177,11 +1221,13 @@ func llm(ctx context.Context, cfg *Cfg, msgs []Message, tools []map[string]any)
|
||||
}
|
||||
cleanMsgs := cleanMessagesForLLM(msgs)
|
||||
sanitizeMessages(cleanMsgs)
|
||||
p := map[string]any{"model": cfg.Model, "temperature": cfg.Temperature, "messages": cleanMsgs, "stream": cfg.Stream}
|
||||
isOC := isOpencodeEndpoint(cfg.Endpoint)
|
||||
stream := cfg.Stream || isOC
|
||||
p := map[string]any{"model": cfg.Model, "temperature": cfg.Temperature, "messages": cleanMsgs, "stream": stream}
|
||||
if tools != nil {
|
||||
p["tools"] = tools
|
||||
}
|
||||
if cfg.Stream {
|
||||
if stream {
|
||||
p["stream_options"] = map[string]any{"include_usage": true}
|
||||
}
|
||||
for k, v := range cfg.Raw {
|
||||
@@ -1262,7 +1308,7 @@ func llm(ctx context.Context, cfg *Cfg, msgs []Message, tools []map[string]any)
|
||||
if COL {
|
||||
fmt.Print("\r\033[K")
|
||||
}
|
||||
if !cfg.Stream {
|
||||
if !stream {
|
||||
var cr struct {
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
@@ -1545,10 +1591,17 @@ func parseStream(ctx context.Context, r io.Reader) (Message, Usage, error) {
|
||||
}
|
||||
|
||||
func shell(ctx context.Context, cmd string, timeout int) string {
|
||||
return shellWithWorkdir(ctx, cmd, "", timeout)
|
||||
}
|
||||
|
||||
func shellWithWorkdir(ctx context.Context, cmd string, workdir string, timeout int) string {
|
||||
cmd = filterText(cmd)
|
||||
cmdCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
c := exec.CommandContext(cmdCtx, "sh", "-c", cmd)
|
||||
c := exec.CommandContext(cmdCtx, "bash", "-c", cmd)
|
||||
if workdir != "" {
|
||||
c.Dir = workdir
|
||||
}
|
||||
c.WaitDelay = 100 * time.Millisecond
|
||||
out, err := c.CombinedOutput()
|
||||
res := strings.TrimSpace(filterText(string(out)))
|
||||
@@ -1639,6 +1692,80 @@ func writeFile(path string, offset, delBytes int, content string) (string, error
|
||||
return fmt.Sprintf("Successfully wrote %d bytes to %s", len(contentBytes), path), nil
|
||||
}
|
||||
|
||||
// readFileOrDir reads a file or directory for the read tool. If path is a
|
||||
// directory, it lists the directory entries sorted alphabetically, appending
|
||||
// a trailing slash for subdirectories. If path is a regular file, it returns
|
||||
// lines between offset and offset+limit-1 (1-indexed), prefixed with line
|
||||
// numbers as "<line>: <content>".
|
||||
func readFileOrDir(path string, offset, limit int) (string, int) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return "[tool error: read requires 'filePath' parameter]", 31
|
||||
}
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[tool error: read %s: %v]", path, err), 31
|
||||
}
|
||||
if fi.IsDir() {
|
||||
entries, err := os.ReadDir(path)
|
||||
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 {
|
||||
if e.IsDir() {
|
||||
b.WriteString(e.Name() + "/\n")
|
||||
} else {
|
||||
b.WriteString(e.Name() + "\n")
|
||||
}
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\n"), 2
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[tool error: read %s: %v]", path, err), 31
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if offset < 1 {
|
||||
offset = 1
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 2000
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
scanner := bufio.NewScanner(f)
|
||||
buf := make([]byte, 64*1024)
|
||||
scanner.Buffer(buf, 1024*1024)
|
||||
|
||||
lineNum := 0
|
||||
linesRead := 0
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
if lineNum < offset {
|
||||
continue
|
||||
}
|
||||
linesRead++
|
||||
text := scanner.Text()
|
||||
if len(text) > 2000 {
|
||||
text = text[:2000]
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("%d: %s\n", lineNum, text))
|
||||
if linesRead >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil && linesRead == 0 {
|
||||
return fmt.Sprintf("[tool error: read %s: %v]", path, err), 31
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\n"), 2
|
||||
}
|
||||
|
||||
// toolResDir returns the directory where oversized tool results are spilled.
|
||||
func toolResDir() string {
|
||||
tmp := os.Getenv("TMPDIR")
|
||||
@@ -1657,7 +1784,7 @@ func toolResDir() string {
|
||||
// returned unchanged so the agent always makes progress.
|
||||
func offloadToolResult(res string, maxRes int, tmps *[]string) string {
|
||||
if maxRes <= 0 {
|
||||
maxRes = 15000
|
||||
maxRes = 65536
|
||||
}
|
||||
if len(res) <= maxRes {
|
||||
return res
|
||||
@@ -1680,7 +1807,7 @@ func offloadToolResult(res string, maxRes int, tmps *[]string) string {
|
||||
return res
|
||||
}
|
||||
*tmps = append(*tmps, name)
|
||||
return fmt.Sprintf("[tool result too large for context: %d bytes (limit %d). The full output was saved to %s. Read it with shell_exec; to read it partially, use a byte offset and length, e.g. `tail -c +OFFSET %s | head -c LENGTH`. The file is %d bytes long.]", n, maxRes, name, name, n)
|
||||
return fmt.Sprintf("[tool result too large for context: %d bytes (limit %d). The full output was saved to %s. Read it with the read tool; to read it partially, use offset and limit. The file is %d bytes long.]", n, maxRes, name, n)
|
||||
}
|
||||
|
||||
// cleanupTemps removes the temporary files created while offloading oversized
|
||||
@@ -1777,15 +1904,70 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error)
|
||||
res, sty = fmt.Sprintf("[tool error: invalid JSON args for %s: %v. Raw: %q]", fn, err, astr), 31
|
||||
} else {
|
||||
switch fn {
|
||||
case "shell_exec":
|
||||
case "bash", "shell_exec":
|
||||
cmd, _ := a["command"].(string)
|
||||
cmd = filterText(cmd)
|
||||
res = shell(ctx, cmd, cfg.ShellTimeout)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res = shellWithWorkdir(ctx, cmd, workdir, to)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return msgs, turnUsage, err
|
||||
}
|
||||
case "write_file":
|
||||
path, _ := a["path"].(string)
|
||||
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)
|
||||
case "write", "write_file":
|
||||
path, _ := a["filePath"].(string)
|
||||
if path == "" {
|
||||
path, _ = a["path"].(string)
|
||||
}
|
||||
path = filterText(path)
|
||||
contentVal, hasContent := a["content"]
|
||||
var content string
|
||||
@@ -1824,9 +2006,9 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(path) == "" {
|
||||
res, sty = "[tool error: write_file requires 'path' parameter]", 31
|
||||
res, sty = fmt.Sprintf("[tool error: %s requires 'filePath' parameter]", fn), 31
|
||||
} else if !hasContent {
|
||||
res, sty = "[tool error: write_file requires 'content' parameter]", 31
|
||||
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
|
||||
@@ -1835,12 +2017,12 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error)
|
||||
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: write_file %s: %v]", p, err), 31
|
||||
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: write_file %s: %v]", p, err), 31
|
||||
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
|
||||
}
|
||||
@@ -1848,7 +2030,7 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error)
|
||||
} else {
|
||||
out, err := writeFile(path, offset, delBytes, content)
|
||||
if err != nil {
|
||||
res, sty = fmt.Sprintf("[tool error: write_file %s: %v]", path, err), 31
|
||||
res, sty = fmt.Sprintf("[tool error: %s %s: %v]", fn, path, err), 31
|
||||
} else {
|
||||
res, sty = out, 2
|
||||
}
|
||||
@@ -1919,17 +2101,27 @@ func projectID() string {
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
// opencodeSessionID returns a Zen-compatible session id of the form
|
||||
// "ses_" + 12 lowercase hex characters + 14 alphanumeric characters. Every
|
||||
// real OpenCode session id observed has this shape (the hex prefix ends in
|
||||
// the literal "ffe"), and the Zen free-tier edge accepts freshly generated
|
||||
// ids in this format, unlike uppercase/ULID-shaped ids.
|
||||
func opencodeSessionID() string {
|
||||
ms := time.Now().UnixNano() / int64(time.Millisecond)
|
||||
var b strings.Builder
|
||||
b.WriteString("ses_")
|
||||
b.WriteString(fmt.Sprintf("%09x", uint64(ms)&0xfffffffff))
|
||||
b.WriteString("ffe")
|
||||
// genOpencodeID generates an OpenCode-style identifier with a 12-hex-character
|
||||
// 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 genOpencodeID(prefix string, descending bool) string {
|
||||
now := time.Now().UnixMilli()
|
||||
ocMu.Lock()
|
||||
if now != ocLast {
|
||||
ocLast = now
|
||||
ocSeq = 0
|
||||
}
|
||||
ocSeq++
|
||||
seq := ocSeq
|
||||
ocMu.Unlock()
|
||||
|
||||
val := uint64(now)*0x1000 + (seq & 0xfff)
|
||||
if descending {
|
||||
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())))
|
||||
@@ -1937,10 +2129,40 @@ func opencodeSessionID() string {
|
||||
buf[i] = h[i%len(h)]
|
||||
}
|
||||
}
|
||||
for _, x := range buf {
|
||||
b.WriteByte(opencodeTailAlphabet[int(x)%len(opencodeTailAlphabet)])
|
||||
var tail strings.Builder
|
||||
for _, b := range buf {
|
||||
tail.WriteByte(opencodeTailAlphabet[int(b)%len(opencodeTailAlphabet)])
|
||||
}
|
||||
return b.String()
|
||||
return prefix + hexPart + tail.String()
|
||||
}
|
||||
|
||||
func opencodeSessionID() string {
|
||||
return genOpencodeID("ses_", true)
|
||||
}
|
||||
|
||||
// opencodeSessionIDFromRequest returns a session ID whose 12-hex timestamp prefix
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
return genOpencodeID("ses_", true)
|
||||
}
|
||||
|
||||
// isOpencodeEndpoint reports whether the endpoint belongs to OpenCode Zen and
|
||||
@@ -1954,41 +2176,24 @@ func isOpencodeEndpoint(endpoint string) bool {
|
||||
return h == "opencode.ai" || strings.HasSuffix(h, ".opencode.ai")
|
||||
}
|
||||
|
||||
// ocRequestID returns a fresh OpenCode-style message id (msg_ followed by 26
|
||||
// base32 characters) matching the ids sent in the x-opencode-request header.
|
||||
// ocRequestID returns a fresh OpenCode-style message id (msg_ followed by 12
|
||||
// hex characters and 14 Base62 characters) matching the ids sent in the
|
||||
// x-opencode-request header.
|
||||
func ocRequestID() string {
|
||||
buf := make([]byte, 26)
|
||||
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 b strings.Builder
|
||||
b.WriteString("msg_")
|
||||
for _, x := range buf {
|
||||
b.WriteByte(opencodeIDAlphabet[int(x)&31])
|
||||
}
|
||||
return b.String()
|
||||
return genOpencodeID("msg_", false)
|
||||
}
|
||||
|
||||
// applyLLMHeaders sets the request headers appropriate for the configured
|
||||
// endpoint. OpenCode (Zen) endpoints receive the x-opencode-* family plus the
|
||||
// full provider User-Agent; all other endpoints keep the legacy
|
||||
// session-affinity headers. msgID is used for the x-opencode-request header.
|
||||
// applyLLMHeaders sets request headers mimicking OpenCode for all endpoints:
|
||||
// the x-opencode-* family, the full provider User-Agent, and Authorization.
|
||||
// msgID is used for the x-opencode-request header, and x-opencode-session is its
|
||||
// bitwise inverse.
|
||||
func applyLLMHeaders(req *http.Request, cfg *Cfg, msgID string) {
|
||||
sid := opencodeSessionID()
|
||||
if isOpencodeEndpoint(cfg.Endpoint) {
|
||||
req.Header.Set("User-Agent", opencodeAgentVersion+" "+opencodeProviderUA)
|
||||
req.Header.Set("x-opencode-client", "cli")
|
||||
req.Header.Set("x-opencode-project", opencodeProjectID)
|
||||
req.Header.Set("x-opencode-session", sid)
|
||||
req.Header.Set("x-opencode-request", msgID)
|
||||
} else {
|
||||
req.Header.Set("User-Agent", opencodeAgentVersion)
|
||||
req.Header.Set("x-session-affinity", sid)
|
||||
req.Header.Set("X-Session-Id", sid)
|
||||
}
|
||||
sid := opencodeSessionIDFromRequest(msgID)
|
||||
req.Header.Set("User-Agent", opencodeAgentVersion+" "+opencodeProviderUA)
|
||||
req.Header.Set("x-opencode-client", "cli")
|
||||
req.Header.Set("x-opencode-project", opencodeProjectID)
|
||||
req.Header.Set("x-opencode-session", sid)
|
||||
req.Header.Set("x-opencode-request", msgID)
|
||||
if cfg.APIKey != "" && cfg.APIKey != "-" {
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
}
|
||||
@@ -2550,11 +2755,11 @@ func runDirectShell(cmd string, timeout int) {
|
||||
return
|
||||
}
|
||||
astr, _ := json.Marshal(map[string]string{"command": cmd})
|
||||
fmt.Println(c(fmt.Sprintf("[tool call: shell_exec(%s)]", string(astr)), 33))
|
||||
fmt.Println(c(fmt.Sprintf("[tool call: bash(%s)]", string(astr)), 33))
|
||||
sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
res := shell(sigCtx, cmd, timeout)
|
||||
cancel()
|
||||
fmt.Println(c("[tool result: shell_exec]", 32) + "\n" + c(res, 2))
|
||||
fmt.Println(c("[tool result: bash]", 32) + "\n" + c(res, 2))
|
||||
}
|
||||
|
||||
func doCompact(cfg *Cfg, msgs []Message) []Message {
|
||||
|
||||
Reference in New Issue
Block a user