oc compatibility overhaul

This commit is contained in:
Luxferre
2026-09-19 00:19:25 +03:00
parent ebc62b93bf
commit e1efe70d8a
4 changed files with 567 additions and 169 deletions
+239 -65
View File
@@ -248,8 +248,8 @@ func TestDefaultSystemPrompt(t *testing.T) {
if !strings.Contains(defaultSystemPrompt, "You are Bantam, a tiny, powerful AI agent.") {
t.Errorf("expected prompt to contain base description, got: %q", defaultSystemPrompt)
}
if !strings.Contains(defaultSystemPrompt, "shell_exec") || !strings.Contains(defaultSystemPrompt, "write_file") {
t.Errorf("expected prompt to list shell_exec and write_file tools, got: %q", defaultSystemPrompt)
if !strings.Contains(defaultSystemPrompt, "bash") || !strings.Contains(defaultSystemPrompt, "read") || !strings.Contains(defaultSystemPrompt, "write") {
t.Errorf("expected prompt to list bash, read, and write tools, got: %q", defaultSystemPrompt)
}
if strings.Contains(defaultSystemPrompt, "run_subagent") {
t.Errorf("prompt should not mention run_subagent: %q", defaultSystemPrompt)
@@ -1147,15 +1147,16 @@ func TestCol(t *testing.T) {
// ---------- llm / AL / summarize / compact via httptest (no real network) ----------
func TestLLMNonStreamingAndHeaders(t *testing.T) {
var gotPath, gotAuth, gotUA, gotLegacy, gotAffinity, gotOCClient, gotOCReq string
var gotPath, gotAuth, gotUA, gotOCClient, gotOCProject, gotOCSession, gotOCReq, gotLegacy string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
gotUA = r.Header.Get("User-Agent")
gotLegacy = r.Header.Get("X-Session-Id")
gotAffinity = r.Header.Get("x-session-affinity")
gotOCClient = r.Header.Get("x-opencode-client")
gotOCProject = r.Header.Get("x-opencode-project")
gotOCSession = r.Header.Get("x-opencode-session")
gotOCReq = r.Header.Get("x-opencode-request")
gotLegacy = r.Header.Get("X-Session-Id")
w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":"hi","reasoning_content":"think"}}]}`))
}))
defer srv.Close()
@@ -1174,19 +1175,23 @@ func TestLLMNonStreamingAndHeaders(t *testing.T) {
if gotAuth != "Bearer secret" {
t.Errorf("auth = %q", gotAuth)
}
if !strings.Contains(gotUA, "opencode/1.18.31") {
if !strings.Contains(gotUA, "opencode/1.18.31") || !strings.Contains(gotUA, "runtime/bun") {
t.Errorf("user-agent = %q", gotUA)
}
// Non-OpenCode endpoints keep the legacy session-affinity headers and must
// not receive any x-opencode-* headers.
if !isOpencodeSessionID(gotLegacy) {
t.Errorf("X-Session-Id = %q, want a ses_* id", gotLegacy)
if gotOCClient != "cli" {
t.Errorf("x-opencode-client = %q, want cli", gotOCClient)
}
if gotAffinity != gotLegacy {
t.Errorf("x-session-affinity = %q, want %q", gotAffinity, gotLegacy)
if gotOCProject != "global" {
t.Errorf("x-opencode-project = %q, want global", gotOCProject)
}
if gotOCClient != "" || gotOCReq != "" {
t.Errorf("unexpected x-opencode headers: client=%q request=%q", gotOCClient, gotOCReq)
if !isOpencodeSessionID(gotOCSession) {
t.Errorf("x-opencode-session = %q, want ses_* id", gotOCSession)
}
if !strings.HasPrefix(gotOCReq, "msg_") {
t.Errorf("x-opencode-request = %q, want msg_* id", gotOCReq)
}
if gotLegacy != "" {
t.Errorf("legacy X-Session-Id should be absent, got %q", gotLegacy)
}
if m.Content == nil || *m.Content != "hi" || m.ReasoningContent != "think" {
t.Errorf("message = %+v", m)
@@ -1229,9 +1234,14 @@ func TestOcRequestID(t *testing.T) {
if len(id) != 30 {
t.Fatalf("request id length = %d, want 30: %q", len(id), id)
}
for _, c := range id[4:] {
if !strings.ContainsRune(opencodeIDAlphabet, c) {
t.Fatalf("invalid character %c in request id %q", c, id)
for _, c := range id[4:16] {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
t.Fatalf("invalid hex character %c in request id prefix %q", c, id)
}
}
for _, c := range id[16:] {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
t.Fatalf("invalid character %c in request id tail %q", c, id)
}
}
if ocRequestID() == id {
@@ -1239,35 +1249,42 @@ func TestOcRequestID(t *testing.T) {
}
}
func TestApplyLLMHeadersOpencode(t *testing.T) {
cfg := defCfg
cfg.Endpoint = "https://opencode.ai/zen/v1"
cfg.APIKey = "-"
req, err := http.NewRequest("POST", "https://opencode.ai/zen/v1/chat/completions", nil)
if err != nil {
t.Fatalf("NewRequest: %v", err)
func TestApplyLLMHeaders(t *testing.T) {
endpoints := []string{
"https://opencode.ai/zen/v1",
"https://api.openai.com/v1",
"https://api.kilo.ai/api/openrouter",
}
applyLLMHeaders(req, &cfg, "msg_test")
if got := req.Header.Get("User-Agent"); !strings.Contains(got, "opencode/1.18.31") || !strings.Contains(got, "runtime/bun") {
t.Errorf("user-agent = %q", got)
}
if got := req.Header.Get("x-opencode-client"); got != "cli" {
t.Errorf("x-opencode-client = %q, want cli", got)
}
if got := req.Header.Get("x-opencode-project"); got != "global" {
t.Errorf("x-opencode-project = %q, want global", got)
}
if got := req.Header.Get("x-opencode-session"); !isOpencodeSessionID(got) {
t.Errorf("x-opencode-session = %q, want a ses_* id", got)
}
if got := req.Header.Get("x-opencode-request"); got != "msg_test" {
t.Errorf("x-opencode-request = %q, want msg_test", got)
}
if got := req.Header.Get("X-Session-Id"); got != "" {
t.Errorf("legacy X-Session-Id should be absent, got %q", got)
}
if got := req.Header.Get("Authorization"); got != "" {
t.Errorf("Authorization should be absent for '-' key, got %q", got)
for _, ep := range endpoints {
cfg := defCfg
cfg.Endpoint = ep
cfg.APIKey = "-"
req, err := http.NewRequest("POST", ep+"/chat/completions", nil)
if err != nil {
t.Fatalf("NewRequest: %v", err)
}
applyLLMHeaders(req, &cfg, "msg_test")
if got := req.Header.Get("User-Agent"); !strings.Contains(got, "opencode/1.18.31") || !strings.Contains(got, "runtime/bun") {
t.Errorf("[%s] user-agent = %q", ep, got)
}
if got := req.Header.Get("x-opencode-client"); got != "cli" {
t.Errorf("[%s] x-opencode-client = %q, want cli", ep, got)
}
if got := req.Header.Get("x-opencode-project"); got != "global" {
t.Errorf("[%s] x-opencode-project = %q, want global", ep, got)
}
if got := req.Header.Get("x-opencode-session"); !isOpencodeSessionID(got) {
t.Errorf("[%s] x-opencode-session = %q, want a ses_* id", ep, got)
}
if got := req.Header.Get("x-opencode-request"); got != "msg_test" {
t.Errorf("[%s] x-opencode-request = %q, want msg_test", ep, got)
}
if got := req.Header.Get("X-Session-Id"); got != "" {
t.Errorf("[%s] legacy X-Session-Id should be absent, got %q", ep, got)
}
if got := req.Header.Get("Authorization"); got != "" {
t.Errorf("[%s] Authorization should be absent for '-' key, got %q", ep, got)
}
}
}
@@ -1284,9 +1301,14 @@ func TestApplyLLMHeadersRequestID(t *testing.T) {
if len(got) != 30 {
t.Errorf("x-opencode-request length = %d, want 30: %q", len(got), got)
}
for _, c := range got[4:] {
if !strings.ContainsRune(opencodeIDAlphabet, c) {
t.Errorf("invalid character %c in x-opencode-request %q", c, got)
for _, c := range got[4:16] {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
t.Errorf("invalid hex character %c in x-opencode-request timestamp prefix %q", c, got)
}
}
for _, c := range got[16:] {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
t.Errorf("invalid character %c in x-opencode-request tail %q", c, got)
}
}
}
@@ -1302,9 +1324,8 @@ func TestApplyLLMHeadersOpencodeAuth(t *testing.T) {
}
}
// isOpencodeSessionID reports whether sid matches the Zen session id shape
// emitted by opencodeSessionID: "ses_" + 12 lowercase hex chars (ending in
// "ffe") + 14 alphanumeric characters.
// isOpencodeSessionID reports whether sid matches the Zen session id shape:
// "ses_" + 12 lowercase hex chars + 14 alphanumeric characters.
func isOpencodeSessionID(sid string) bool {
if !strings.HasPrefix(sid, "ses_") || len(sid) != 30 {
return false
@@ -1314,9 +1335,6 @@ func isOpencodeSessionID(sid string) bool {
return false
}
}
if sid[13:16] != "ffe" {
return false
}
for _, c := range sid[16:] {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
return false
@@ -1333,17 +1351,13 @@ func TestOpencodeSessionID(t *testing.T) {
if len(sid) != 30 {
t.Fatalf("session id length = %d, want 30: %q", len(sid), sid)
}
// The 12 characters after the "ses_" prefix are lowercase hex (the real
// OpenCode ids carry a millisecond timestamp there, ending in "ffe").
// The 12 characters after the "ses_" prefix are lowercase hex.
hexPart := sid[4:16]
for _, c := range hexPart {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
t.Fatalf("non-hex character %c in session id prefix %q", c, sid)
}
}
if hexPart[9:] != "ffe" {
t.Fatalf("session id hex marker = %q, want suffix ffe: %q", hexPart[9:], sid)
}
// The remaining 14 characters are alphanumeric.
for _, c := range sid[16:] {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
@@ -3106,11 +3120,11 @@ func TestLLMContextTokensIncludesReasoning(t *testing.T) {
func TestMaxToolResDefault(t *testing.T) {
clearBantamEnv(t)
cfg := getCfg(filepath.Join(t.TempDir(), "missing.cfg"))
if cfg.MaxToolRes != 15000 {
t.Errorf("default MaxToolRes = %d, want 15000", cfg.MaxToolRes)
if cfg.MaxToolRes != 65536 {
t.Errorf("default MaxToolRes = %d, want 65536", cfg.MaxToolRes)
}
if cfg.Raw["max_tool_res"] != "15000" {
t.Errorf("default Raw[max_tool_res] = %q, want 15000", cfg.Raw["max_tool_res"])
if cfg.Raw["max_tool_res"] != "65536" {
t.Errorf("default Raw[max_tool_res] = %q, want 65536", cfg.Raw["max_tool_res"])
}
}
@@ -3215,12 +3229,12 @@ func TestOffloadToolResultLargeSpillsAndCleans(t *testing.T) {
}
}
// A non-positive limit must fall back to the 15000 default rather than
// A non-positive limit must fall back to the 65536 default rather than
// spilling every non-empty result.
func TestOffloadToolResultZeroLimitUsesDefault(t *testing.T) {
t.Setenv("TMPDIR", t.TempDir())
var tmps []string
res := strings.Repeat("y", 15000) // exactly the default limit: stays inline
res := strings.Repeat("y", 65536) // exactly the default limit: stays inline
if got := offloadToolResult(res, 0, &tmps); got != res {
t.Errorf("result at default limit should stay inline")
}
@@ -3303,3 +3317,163 @@ func TestBuildSystemPromptMaxToolResHint(t *testing.T) {
t.Errorf("prompt still contains the unexpanded $TMPDIR/bantam placeholder: %q", sp)
}
}
func TestOpencodeIDGeneratorBitwiseInverse(t *testing.T) {
reqID := ocRequestID()
if !strings.HasPrefix(reqID, "msg_") || len(reqID) != 30 {
t.Fatalf("unexpected reqID format: %q", reqID)
}
sesID := opencodeSessionIDFromRequest(reqID)
if !strings.HasPrefix(sesID, "ses_") || len(sesID) != 30 {
t.Fatalf("unexpected sesID format: %q", sesID)
}
// First 12 hex characters (indices 4..16) must be exact bitwise inverse
reqHex := reqID[4:16]
sesHex := sesID[4:16]
reqVal, err1 := strconv.ParseUint(reqHex, 16, 64)
sesVal, err2 := strconv.ParseUint(sesHex, 16, 64)
if err1 != nil || err2 != nil {
t.Fatalf("failed to parse hex values: err1=%v, err2=%v", err1, err2)
}
if (reqVal ^ sesVal) != 0xffffffffffff {
t.Errorf("expected exact bitwise inverse, got reqHex=%s, sesHex=%s, xor=%012x", reqHex, sesHex, reqVal^sesVal)
}
// Verify applyLLMHeaders sets headers with bitwise inverse
cfg := defCfg
cfg.Endpoint = "https://opencode.ai/zen/v1"
req, _ := http.NewRequest("POST", "https://opencode.ai/zen/v1/chat/completions", nil)
applyLLMHeaders(req, &cfg, reqID)
gotReq := req.Header.Get("x-opencode-request")
gotSes := req.Header.Get("x-opencode-session")
if gotReq != reqID {
t.Errorf("x-opencode-request = %q, want %q", gotReq, reqID)
}
rVal, _ := strconv.ParseUint(gotReq[4:16], 16, 64)
sVal, _ := strconv.ParseUint(gotSes[4:16], 16, 64)
if (rVal ^ sVal) != 0xffffffffffff {
t.Errorf("headers not bitwise inverse: gotReq=%s, gotSes=%s", gotReq, gotSes)
}
}
func TestReadToolFileAndDir(t *testing.T) {
tmp := t.TempDir()
file1 := filepath.Join(tmp, "sample.txt")
content := "line one\nline two\nline three\nline four\nline five"
if err := os.WriteFile(file1, []byte(content), 0644); err != nil {
t.Fatal(err)
}
// Reading full file
res, sty := readFileOrDir(file1, 1, 2000)
if sty != 2 {
t.Errorf("expected style 2, got %d (res=%s)", sty, res)
}
expected := "1: line one\n2: line two\n3: line three\n4: line four\n5: line five"
if res != expected {
t.Errorf("got %q, want %q", res, expected)
}
// Reading with offset and limit
res, _ = readFileOrDir(file1, 2, 2)
expectedSub := "2: line two\n3: line three"
if res != expectedSub {
t.Errorf("got %q, want %q", res, expectedSub)
}
// Reading directory
subDir := filepath.Join(tmp, "subdir")
os.Mkdir(subDir, 0755)
dRes, dSty := readFileOrDir(tmp, 1, 100)
if dSty != 2 {
t.Errorf("expected style 2 for dir, got %d", dSty)
}
if !strings.Contains(dRes, "sample.txt") || !strings.Contains(dRes, "subdir/") {
t.Errorf("directory listing missing expected entries: %q", dRes)
}
// Non-existent file
missingRes, missingSty := readFileOrDir(filepath.Join(tmp, "missing.txt"), 1, 100)
if missingSty != 31 || !strings.Contains(missingRes, "no such file") {
t.Errorf("expected missing file error, got sty=%d res=%q", missingSty, missingRes)
}
}
func TestBashToolAndWorkdir(t *testing.T) {
res := shell(context.Background(), "echo $((10 + 25))", 5)
if !strings.Contains(res, "35") || !strings.Contains(res, "exit: 0") {
t.Errorf("unexpected bash output: %q", res)
}
tmp := t.TempDir()
resWorkdir := shellWithWorkdir(context.Background(), "pwd -P", tmp, 5)
if !strings.Contains(resWorkdir, tmp) || !strings.Contains(resWorkdir, "exit: 0") {
t.Errorf("unexpected bash workdir output: %q", resWorkdir)
}
}
func TestALWriteAndReadTools(t *testing.T) {
tmp := t.TempDir()
target := filepath.Join(tmp, "written.txt")
step := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
step++
if step == 1 {
// Step 1: LLM calls write tool
args, _ := json.Marshal(map[string]any{"filePath": target, "content": "alpha\nbeta\ngamma"})
w.Write([]byte(fmt.Sprintf(`{"choices":[{"message":{"role":"assistant","content":null,"tool_calls":[{"id":"c1","type":"function","function":{"name":"write","arguments":%s}}]}}]}`, strconv.Quote(string(args)))))
return
}
if step == 2 {
// Step 2: LLM calls read tool
args, _ := json.Marshal(map[string]any{"filePath": target, "offset": 2, "limit": 1})
w.Write([]byte(fmt.Sprintf(`{"choices":[{"message":{"role":"assistant","content":null,"tool_calls":[{"id":"c2","type":"function","function":{"name":"read","arguments":%s}}]}}]}`, strconv.Quote(string(args)))))
return
}
// Step 3: LLM finishes
w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":"all done"}}]}`))
}))
defer srv.Close()
cfg := defCfg
cfg.Endpoint = srv.URL
cfg.Stream = false
cfg.APIKey = "-"
msgs, _, err := AL(context.Background(), &cfg, []Message{{Role: "user", Content: strp("start")}})
if err != nil {
t.Fatalf("AL failed: %v", err)
}
// Check that file was created and written
data, err := os.ReadFile(target)
if err != nil || string(data) != "alpha\nbeta\ngamma" {
t.Fatalf("expected written file content, got: %q, err: %v", string(data), err)
}
// Check that read tool response was received in conversation
var readToolResult string
for _, m := range msgs {
if m.Role == "tool" && m.ToolCallID == "c2" && m.Content != nil {
readToolResult = *m.Content
}
}
if readToolResult != "2: beta" {
t.Errorf("expected read tool result '2: beta', got %q", readToolResult)
}
}
func TestOffloadToolResultUsesReadPrompt(t *testing.T) {
var tmps []string
big := strings.Repeat("x\n", 10000)
msg := offloadToolResult(big, 100, &tmps)
if !strings.Contains(msg, "Read it with the read tool") {
t.Errorf("expected prompt to reference read tool, got: %q", msg)
}
if !strings.Contains(msg, "offset and limit") {
t.Errorf("expected prompt to reference offset and limit, got: %q", msg)
}
cleanupTemps(tmps)
}