mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
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:
+8
-1
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user