Merge pull request #187 from CJackHwang/codex/investigate-unrecognized-issue-in-ds2api

fix: use schema-correct exec parameter examples in tool prompt
This commit is contained in:
CJACK.
2026-04-02 20:15:30 +08:00
committed by GitHub
6 changed files with 257 additions and 7 deletions

View File

@@ -183,7 +183,7 @@ func findToolSegmentStart(s string) int {
return -1
}
lower := strings.ToLower(s)
keywords := []string{"tool_calls", "\"function\"", "function.name:", "functioncall", "\"tool_use\""}
keywords := []string{"tool_calls", "\"function\"", "function.name:", "\"tool_use\""}
bestKeyIdx := -1
for _, kw := range keywords {
idx := strings.Index(lower, kw)
@@ -191,6 +191,9 @@ func findToolSegmentStart(s string) int {
bestKeyIdx = idx
}
}
if fnKeyIdx := findQuotedFunctionCallKeyStart(s); fnKeyIdx >= 0 && (bestKeyIdx < 0 || fnKeyIdx < bestKeyIdx) {
bestKeyIdx = fnKeyIdx
}
// Also detect XML tool call tags.
for _, tag := range xmlToolTagsToDetect {
idx := strings.Index(lower, tag)
@@ -240,13 +243,16 @@ func consumeToolCapture(state *toolStreamSieveState, toolNames []string) (prefix
lower := strings.ToLower(captured)
keyIdx := -1
keywords := []string{"tool_calls", "\"function\"", "function.name:", "functioncall", "\"tool_use\""}
keywords := []string{"tool_calls", "\"function\"", "function.name:", "\"tool_use\""}
for _, kw := range keywords {
idx := strings.Index(lower, kw)
if idx >= 0 && (keyIdx < 0 || idx < keyIdx) {
keyIdx = idx
}
}
if fnKeyIdx := findQuotedFunctionCallKeyStart(captured); fnKeyIdx >= 0 && (keyIdx < 0 || fnKeyIdx < keyIdx) {
keyIdx = fnKeyIdx
}
if keyIdx < 0 {
return "", nil, "", false

View File

@@ -0,0 +1,100 @@
package openai
import "strings"
func findQuotedFunctionCallKeyStart(s string) int {
lower := strings.ToLower(s)
quotedIdx := findFunctionCallKeyStart(lower, `"functioncall"`)
bareIdx := findFunctionCallKeyStart(lower, "functioncall")
// Prefer the quoted JSON key whenever we have a structural match.
// Bare-key detection is only for loose payloads where the quoted form
// is absent.
if quotedIdx >= 0 {
return quotedIdx
}
return bareIdx
}
func findFunctionCallKeyStart(lower, key string) int {
for from := 0; from < len(lower); {
rel := strings.Index(lower[from:], key)
if rel < 0 {
return -1
}
idx := from + rel
if isInsideJSONString(lower, idx) {
from = idx + 1
continue
}
if !hasJSONObjectContextPrefix(lower[:idx]) {
from = idx + 1
continue
}
if !hasJSONKeyBoundary(lower, idx, len(key)) {
from = idx + 1
continue
}
j := idx + len(key)
for j < len(lower) && (lower[j] == ' ' || lower[j] == '\t' || lower[j] == '\r' || lower[j] == '\n') {
j++
}
if j < len(lower) && lower[j] == ':' {
k := j + 1
for k < len(lower) && (lower[k] == ' ' || lower[k] == '\t' || lower[k] == '\r' || lower[k] == '\n') {
k++
}
if k < len(lower) && lower[k] != '{' {
from = idx + 1
continue
}
return idx
}
from = idx + 1
}
return -1
}
func isInsideJSONString(s string, idx int) bool {
inString := false
escaped := false
for i := 0; i < idx; i++ {
c := s[i]
if escaped {
escaped = false
continue
}
if c == '\\' && inString {
escaped = true
continue
}
if c == '"' {
inString = !inString
}
}
return inString
}
func hasJSONObjectContextPrefix(prefix string) bool {
return strings.LastIndex(prefix, "{") >= 0
}
func hasJSONKeyBoundary(s string, idx, keyLen int) bool {
if idx > 0 {
prev := s[idx-1]
if isLowerAlphaNumeric(prev) {
return false
}
}
if end := idx + keyLen; end < len(s) {
next := s[end]
if isLowerAlphaNumeric(next) {
return false
}
}
return true
}
func isLowerAlphaNumeric(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9') || b == '_'
}

View File

@@ -0,0 +1,23 @@
package openai
import "testing"
func TestFindQuotedFunctionCallKeyStart_PrefersEarlierBareKey(t *testing.T) {
input := `{functionCall:{"name":"a","arguments":"{}"},"message":"literal text: \"functionCall\": not a key"}`
got := findQuotedFunctionCallKeyStart(input)
want := 1
if got != want {
t.Fatalf("findQuotedFunctionCallKeyStart() = %d, want %d", got, want)
}
}
func TestFindQuotedFunctionCallKeyStart_PrefersEarlierQuotedKey(t *testing.T) {
input := `{"functionCall":{"name":"a","arguments":"{}"},"note":"functionCall appears in prose"}`
got := findQuotedFunctionCallKeyStart(input)
want := 1
if got != want {
t.Fatalf("findQuotedFunctionCallKeyStart() = %d, want %d", got, want)
}
}

View File

@@ -120,6 +120,60 @@ func TestFindToolSegmentStartDetectsXMLToolCalls(t *testing.T) {
}
}
func TestFindToolSegmentStartIgnoresFunctionCallProse(t *testing.T) {
input := "Please explain the functionCall API field and how clients should parse it."
if got := findToolSegmentStart(input); got != -1 {
t.Fatalf("expected no tool segment start for prose, got %d", got)
}
}
func TestFindToolSegmentStartDetectsQuotedFunctionCallKey(t *testing.T) {
input := `prefix {"functionCall": {"name":"search_web","args":{"query":"x"}}}`
want := strings.Index(input, "{")
if got := findToolSegmentStart(input); got != want {
t.Fatalf("expected JSON object start %d, got %d", want, got)
}
}
func TestFindToolSegmentStartDetectsLooseFunctionCallKey(t *testing.T) {
input := `prefix {functionCall: {"name":"search_web","args":{"query":"x"}}}`
want := strings.Index(input, "{")
if got := findToolSegmentStart(input); got != want {
t.Fatalf("expected JSON object start %d, got %d", want, got)
}
}
func TestFindToolSegmentStartPrefersQuotedFunctionCallOverEarlierBareProse(t *testing.T) {
input := `prefix {note} functionCall: docs hint {"functionCall":{"name":"search_web","args":{"query":"x"}}}`
want := strings.Index(input, `{"functionCall"`)
if got := findToolSegmentStart(input); got != want {
t.Fatalf("expected quoted functionCall JSON start %d, got %d", want, got)
}
}
func TestFindToolSegmentStartIgnoresLooseFunctionCallProse(t *testing.T) {
input := "Please explain why functionCall: is used in documentation examples."
if got := findToolSegmentStart(input); got != -1 {
t.Fatalf("expected no tool segment start for prose, got %d", got)
}
}
func TestProcessToolSieveDoesNotBufferFunctionCallProse(t *testing.T) {
var state toolStreamSieveState
chunk := "Please explain the functionCall API field and keep streaming this sentence."
events := processToolSieveChunk(&state, chunk, []string{"search_web"})
var text string
for _, evt := range events {
text += evt.Content
if len(evt.ToolCalls) > 0 {
t.Fatalf("expected no tool calls for prose, got %#v", evt.ToolCalls)
}
}
if text != chunk {
t.Fatalf("expected prose to pass through immediately, got %q", text)
}
}
func TestProcessToolSieveDetectsGeminiFunctionCallPayload(t *testing.T) {
var state toolStreamSieveState
events := processToolSieveChunk(&state, `{"functionCall":{"name":"search_web","args":{"query":"latest"}}}`, []string{"search_web"})

View File

@@ -1,5 +1,7 @@
package util
import "strings"
// BuildToolCallInstructions generates the unified tool-calling instruction block
// used by all adapters (OpenAI, Claude, Gemini). It uses attention-optimized
// structure: rules → negative examples → positive examples → anchor.
@@ -19,7 +21,7 @@ func BuildToolCallInstructions(toolNames []string) string {
ex1 = n
used["ex1"] = true
// Write/execute-type tools
case !used["ex2"] && matchAny(n, "write_to_file", "apply_diff", "execute_command", "Write", "Edit", "MultiEdit", "Bash"):
case !used["ex2"] && matchAny(n, "write_to_file", "apply_diff", "execute_command", "exec_command", "Write", "Edit", "MultiEdit", "Bash"):
ex2 = n
used["ex2"] = true
// Interactive/meta tools
@@ -28,6 +30,9 @@ func BuildToolCallInstructions(toolNames []string) string {
used["ex3"] = true
}
}
ex1Params := exampleReadParams(ex1)
ex2Params := exampleWriteOrExecParams(ex2)
ex3Params := exampleInteractiveParams(ex3)
return `TOOL CALL FORMAT — FOLLOW EXACTLY:
@@ -47,6 +52,7 @@ RULES:
4) Do NOT wrap the XML in markdown code fences (no triple backticks).
5) After receiving a tool result, use it directly. Only call another tool if the result is insufficient.
6) If you want to say something AND call a tool, output text first, then the XML block on its own.
7) Parameters MUST use the exact field names from the selected tool schema.
❌ WRONG — Do NOT do these:
Wrong 1 — mixed text and XML:
@@ -62,7 +68,7 @@ Example A — Single tool:
<tool_calls>
<tool_call>
<tool_name>` + ex1 + `</tool_name>
<parameters>{"path":"src/main.go"}</parameters>
<parameters>` + ex1Params + `</parameters>
</tool_call>
</tool_calls>
@@ -70,11 +76,11 @@ Example B — Two tools in parallel:
<tool_calls>
<tool_call>
<tool_name>` + ex1 + `</tool_name>
<parameters>{"path":"config.json"}</parameters>
<parameters>` + ex1Params + `</parameters>
</tool_call>
<tool_call>
<tool_name>` + ex2 + `</tool_name>
<parameters>{"path":"output.txt","content":"Hello world"}</parameters>
<parameters>` + ex2Params + `</parameters>
</tool_call>
</tool_calls>
@@ -82,7 +88,7 @@ Example C — Tool with complex nested JSON parameters:
<tool_calls>
<tool_call>
<tool_name>` + ex3 + `</tool_name>
<parameters>{"question":"Which approach do you prefer?","follow_up":[{"text":"Option A"},{"text":"Option B"}]}</parameters>
<parameters>` + ex3Params + `</parameters>
</tool_call>
</tool_calls>
@@ -97,3 +103,38 @@ func matchAny(name string, candidates ...string) bool {
}
return false
}
func exampleReadParams(name string) string {
switch strings.TrimSpace(name) {
case "Read":
return `{"file_path":"README.md"}`
case "Glob":
return `{"pattern":"**/*.go","path":"."}`
default:
return `{"path":"src/main.go"}`
}
}
func exampleWriteOrExecParams(name string) string {
switch strings.TrimSpace(name) {
case "Bash", "execute_command":
return `{"command":"pwd"}`
case "exec_command":
return `{"cmd":"pwd"}`
case "Edit":
return `{"file_path":"README.md","old_string":"foo","new_string":"bar"}`
case "MultiEdit":
return `{"file_path":"README.md","edits":[{"old_string":"foo","new_string":"bar"}]}`
default:
return `{"path":"output.txt","content":"Hello world"}`
}
}
func exampleInteractiveParams(name string) string {
switch strings.TrimSpace(name) {
case "Task":
return `{"description":"Investigate flaky tests","prompt":"Run targeted tests and summarize failures"}`
default:
return `{"question":"Which approach do you prefer?","follow_up":[{"text":"Option A"},{"text":"Option B"}]}`
}
}

View File

@@ -0,0 +1,26 @@
package util
import (
"strings"
"testing"
)
func TestBuildToolCallInstructions_ExecCommandUsesCmdExample(t *testing.T) {
out := BuildToolCallInstructions([]string{"exec_command"})
if !strings.Contains(out, `<tool_name>exec_command</tool_name>`) {
t.Fatalf("expected exec_command in examples, got: %s", out)
}
if !strings.Contains(out, `<parameters>{"cmd":"pwd"}</parameters>`) {
t.Fatalf("expected cmd parameter example for exec_command, got: %s", out)
}
}
func TestBuildToolCallInstructions_ExecuteCommandUsesCommandExample(t *testing.T) {
out := BuildToolCallInstructions([]string{"execute_command"})
if !strings.Contains(out, `<tool_name>execute_command</tool_name>`) {
t.Fatalf("expected execute_command in examples, got: %s", out)
}
if !strings.Contains(out, `<parameters>{"command":"pwd"}</parameters>`) {
t.Fatalf("expected command parameter example for execute_command, got: %s", out)
}
}