diff --git a/pkg/providers/factory.go b/pkg/providers/factory.go index ab27cdfc6..e261fc960 100644 --- a/pkg/providers/factory.go +++ b/pkg/providers/factory.go @@ -309,7 +309,7 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) { if sel.apiBase == "" { sel.apiBase = "https://api.mistral.ai/v1" } - case cfg.Providers.Opencode.APIKey != "" && cfg.Providers.Opencode.APIBase != "": + case cfg.Providers.Opencode.APIKey != "": sel.apiKey = cfg.Providers.Opencode.APIKey sel.apiBase = cfg.Providers.Opencode.APIBase sel.proxy = cfg.Providers.Opencode.Proxy diff --git a/pkg/providers/factory_provider.go b/pkg/providers/factory_provider.go index 3354c199b..47775e86c 100644 --- a/pkg/providers/factory_provider.go +++ b/pkg/providers/factory_provider.go @@ -170,6 +170,9 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err return provider, modelID, nil case "opencode", "opencode-zen": + if cfg.APIKey == "" && cfg.APIBase == "" { + return nil, "", fmt.Errorf("api_key or api_base is required for opencode protocol") + } apiBase := cfg.APIBase if apiBase == "" { apiBase = "https://opencode.ai/zen/v1" diff --git a/pkg/providers/opencode/provider.go b/pkg/providers/opencode/provider.go index ae461495b..77b7ce7e0 100644 --- a/pkg/providers/opencode/provider.go +++ b/pkg/providers/opencode/provider.go @@ -434,6 +434,18 @@ func (p *Provider) chatAnthropicMessages( "role": "assistant", "content": content, }) + case "tool": + // Tool result - same format as user message with tool_use_id + anthropicMessages = append(anthropicMessages, map[string]any{ + "role": "user", + "content": []map[string]any{ + { + "type": "tool_result", + "tool_use_id": msg.ToolCallID, + "content": msg.Content, + }, + }, + }) } } @@ -527,6 +539,8 @@ func (p *Provider) chatGeminiModels( // Build contents from messages (Gemini format) var contents []map[string]any var systemParts []string + // Track tool call ID to function name mapping for proper functionResponse + toolCallIDToName := make(map[string]string) for _, msg := range messages { switch msg.Role { @@ -539,35 +553,20 @@ func (p *Provider) chatGeminiModels( systemParts = append(systemParts, msg.Content) } case "user": - if msg.ToolCallID != "" { - // Tool result - contents = append(contents, map[string]any{ - "role": "user", - "parts": []map[string]any{ - { - "functionResponse": map[string]any{ - "name": msg.ToolCallID, - "response": map[string]any{ - "result": msg.Content, - }, - }, - }, - }, - }) - } else { - contents = append(contents, map[string]any{ - "role": "user", - "parts": []map[string]any{ - {"text": msg.Content}, - }, - }) - } + contents = append(contents, map[string]any{ + "role": "user", + "parts": []map[string]any{ + {"text": msg.Content}, + }, + }) case "assistant": parts := []map[string]any{} if msg.Content != "" { parts = append(parts, map[string]any{"text": msg.Content}) } for _, tc := range msg.ToolCalls { + // Track the mapping from tool call ID to function name + toolCallIDToName[tc.ID] = tc.Name parts = append(parts, map[string]any{ "functionCall": map[string]any{ "name": tc.Name, @@ -579,6 +578,27 @@ func (p *Provider) chatGeminiModels( "role": "model", "parts": parts, }) + case "tool": + // Tool result - use mapped function name, not ToolCallID + funcName := toolCallIDToName[msg.ToolCallID] + if funcName == "" { + // Fallback: if no mapping found, this shouldn't happen in normal flow + // but we need to handle it gracefully + funcName = "unknown_function" + } + contents = append(contents, map[string]any{ + "role": "user", + "parts": []map[string]any{ + { + "functionResponse": map[string]any{ + "name": funcName, + "response": map[string]any{ + "result": msg.Content, + }, + }, + }, + }, + }) } } @@ -708,13 +728,13 @@ func parseGeminiResponse(body []byte) (*LLMResponse, error) { var content strings.Builder var toolCalls []ToolCall - for _, part := range candidate.Content.Parts { + for i, part := range candidate.Content.Parts { if part.Text != "" { content.WriteString(part.Text) } if part.FunctionCall != nil { toolCalls = append(toolCalls, ToolCall{ - ID: fmt.Sprintf("call_%s_%d", part.FunctionCall.Name, time.Now().UnixNano()), + ID: fmt.Sprintf("call_%s_%d", part.FunctionCall.Name, i), Name: part.FunctionCall.Name, Arguments: part.FunctionCall.Args, })