fix: migration ModelName, reasoning_content, shell regex, loop boundary

1. migration.go: Set ModelName to userModel when provider matches so
   GetModelConfig(userModel) can find the entry. Previously the migration
   created entries with the provider name as ModelName (e.g. "moonshot")
   but lookup used the model name (e.g. "k2p5"), causing "model not found".

2. openai_compat/provider.go: Preserve reasoning_content in conversation
   history. Thinking models (e.g. Kimi K2, DeepSeek-R1) return
   reasoning_content which must be echoed back. Without it, APIs return
   400: "thinking is enabled but reasoning_content is missing".

3. shell.go: Fix deny pattern regex for format/mkfs/diskpart to use
   (?:^|\s) instead of \b to avoid matching --format flags.
   Fix path extraction regex to use submatch to avoid matching flags
   like -rf as paths.

4. loop.go: Adjust forceCompression mid-point to avoid splitting
   tool-call/result message pairs, which causes API errors.
This commit is contained in:
I Putu Eddy Irawan
2026-03-01 08:44:15 +07:00
parent 9c9524f934
commit ee5b61884a
4 changed files with 26 additions and 14 deletions
+8 -1
View File
@@ -980,8 +980,15 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) {
return
}
// Helper to find the mid-point of the conversation
// Find the mid-point of the conversation, avoiding splitting tool call/result pairs.
// A tool-call message (role=assistant with ToolCalls) must be followed by its
// tool-result message (role=tool). Splitting between them causes API errors.
mid := len(conversation) / 2
if mid < len(conversation) && mid > 0 {
if conversation[mid].Role == "tool" {
mid++ // move past the tool result to keep the pair together
}
}
// New history structure:
// 1. System Prompt (with compression note appended)
+3 -1
View File
@@ -367,7 +367,9 @@ func ConvertProvidersToModelList(cfg *Config) []ModelConfig {
// Check if this is the user's configured provider
if slices.Contains(m.providerNames, userProvider) && userModel != "" {
// Use the user's configured model instead of default
// Use the user's configured model instead of default.
// Also set ModelName so GetModelConfig(userModel) can find this entry.
mc.ModelName = userModel
mc.Model = buildModelWithProtocol(m.protocol, userModel)
} else if userProvider == "" && userModel != "" && !legacyModelNameApplied {
// Legacy config: no explicit provider field but model is specified
+10 -8
View File
@@ -289,10 +289,11 @@ func parseResponse(body []byte) (*LLMResponse, error) {
// It mirrors protocoltypes.Message but omits SystemParts, which is an
// internal field that would be unknown to third-party endpoints.
type openaiMessage struct {
Role string `json:"role"`
Content string `json:"content"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
Role string `json:"role"`
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}
// stripSystemParts converts []Message to []openaiMessage, dropping the
@@ -302,10 +303,11 @@ func stripSystemParts(messages []Message) []openaiMessage {
out := make([]openaiMessage, len(messages))
for i, m := range messages {
out[i] = openaiMessage{
Role: m.Role,
Content: m.Content,
ToolCalls: m.ToolCalls,
ToolCallID: m.ToolCallID,
Role: m.Role,
Content: m.Content,
ReasoningContent: m.ReasoningContent,
ToolCalls: m.ToolCalls,
ToolCallID: m.ToolCallID,
}
}
return out
+5 -4
View File
@@ -28,7 +28,7 @@ var defaultDenyPatterns = []*regexp.Regexp{
regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`),
regexp.MustCompile(`\bdel\s+/[fq]\b`),
regexp.MustCompile(`\brmdir\s+/s\b`),
regexp.MustCompile(`\b(format|mkfs|diskpart)\b\s`), // Match disk wiping commands (must be followed by space/args)
regexp.MustCompile(`(?:^|\s)(format|mkfs|diskpart)\s`), // Match disk wiping commands, avoid matching --format flags
regexp.MustCompile(`\bdd\s+if=`),
regexp.MustCompile(`>\s*/dev/sd[a-z]\b`), // Block writes to disk devices (but allow /dev/null)
regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`),
@@ -287,10 +287,11 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
return ""
}
pathPattern := regexp.MustCompile(`[A-Za-z]:\\[^\\\"']+|/[^\s\"']+`)
matches := pathPattern.FindAllString(cmd, -1)
pathPattern := regexp.MustCompile(`(?:^|\s)([A-Za-z]:\\[^\\"']+|/[a-zA-Z][^\s"']*)`)
matches := pathPattern.FindAllStringSubmatch(cmd, -1)
for _, raw := range matches {
for _, match := range matches {
raw := match[1]
p, err := filepath.Abs(raw)
if err != nil {
continue