diff --git a/plugins/ai/openai/chat_completions.go b/plugins/ai/openai/chat_completions.go new file mode 100644 index 00000000..ede434d9 --- /dev/null +++ b/plugins/ai/openai/chat_completions.go @@ -0,0 +1,102 @@ +package openai + +// This file contains helper methods for the Chat Completions API. +// These methods are used as fallbacks for OpenAI-compatible providers +// that don't support the newer Responses API (e.g., Groq, Mistral, etc.). + +import ( + "context" + "strings" + + "github.com/danielmiessler/fabric/chat" + "github.com/danielmiessler/fabric/common" + openai "github.com/openai/openai-go" + "github.com/openai/openai-go/shared" +) + +// sendChatCompletions sends a request using the Chat Completions API +func (o *Client) sendChatCompletions(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions) (ret string, err error) { + req := o.buildChatCompletionParams(msgs, opts) + + var resp *openai.ChatCompletion + if resp, err = o.ApiClient.Chat.Completions.New(ctx, req); err != nil { + return + } + if len(resp.Choices) > 0 { + ret = resp.Choices[0].Message.Content + } + return +} + +// sendStreamChatCompletions sends a streaming request using the Chat Completions API +func (o *Client) sendStreamChatCompletions( + msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions, channel chan string, +) (err error) { + req := o.buildChatCompletionParams(msgs, opts) + stream := o.ApiClient.Chat.Completions.NewStreaming(context.Background(), req) + for stream.Next() { + chunk := stream.Current() + if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" { + channel <- chunk.Choices[0].Delta.Content + } + } + if stream.Err() == nil { + channel <- "\n" + } + close(channel) + return stream.Err() +} + +// buildChatCompletionParams builds parameters for the Chat Completions API +func (o *Client) buildChatCompletionParams( + inputMsgs []*chat.ChatCompletionMessage, opts *common.ChatOptions, +) (ret openai.ChatCompletionNewParams) { + + messages := make([]openai.ChatCompletionMessageParamUnion, len(inputMsgs)) + for i, msgPtr := range inputMsgs { + msg := *msgPtr + if strings.Contains(opts.Model, "deepseek") && len(inputMsgs) == 1 && msg.Role == chat.ChatMessageRoleSystem { + msg.Role = chat.ChatMessageRoleUser + } + messages[i] = o.convertChatMessage(msg) + } + + ret = openai.ChatCompletionNewParams{ + Model: shared.ChatModel(opts.Model), + Messages: messages, + } + + if !opts.Raw { + ret.Temperature = openai.Float(opts.Temperature) + ret.TopP = openai.Float(opts.TopP) + if opts.MaxTokens != 0 { + ret.MaxTokens = openai.Int(int64(opts.MaxTokens)) + } + if opts.PresencePenalty != 0 { + ret.PresencePenalty = openai.Float(opts.PresencePenalty) + } + if opts.FrequencyPenalty != 0 { + ret.FrequencyPenalty = openai.Float(opts.FrequencyPenalty) + } + if opts.Seed != 0 { + ret.Seed = openai.Int(int64(opts.Seed)) + } + } + return +} + +// convertChatMessage converts fabric chat message to OpenAI chat completion message +func (o *Client) convertChatMessage(msg chat.ChatCompletionMessage) openai.ChatCompletionMessageParamUnion { + // For now, simplify to text-only messages to get the basic functionality working + // Multi-content support can be added later if needed + switch msg.Role { + case chat.ChatMessageRoleSystem: + return openai.SystemMessage(msg.Content) + case chat.ChatMessageRoleUser: + return openai.UserMessage(msg.Content) + case chat.ChatMessageRoleAssistant: + return openai.AssistantMessage(msg.Content) + default: + return openai.UserMessage(msg.Content) + } +} diff --git a/plugins/ai/openai/openai.go b/plugins/ai/openai/openai.go index 10f37908..635999bb 100644 --- a/plugins/ai/openai/openai.go +++ b/plugins/ai/openai/openai.go @@ -30,6 +30,17 @@ func NewClientCompatible(vendorName string, defaultBaseUrl string, configureCust return } +func NewClientCompatibleWithResponses(vendorName string, defaultBaseUrl string, implementsResponses bool, configureCustom func() error) (ret *Client) { + ret = NewClientCompatibleNoSetupQuestions(vendorName, configureCustom) + + ret.ApiKey = ret.AddSetupQuestion("API Key", true) + ret.ApiBaseURL = ret.AddSetupQuestion("API Base URL", false) + ret.ApiBaseURL.Value = defaultBaseUrl + ret.ImplementsResponses = implementsResponses + + return +} + func NewClientCompatibleNoSetupQuestions(vendorName string, configureCustom func() error) (ret *Client) { ret = &Client{} @@ -48,9 +59,10 @@ func NewClientCompatibleNoSetupQuestions(vendorName string, configureCustom func type Client struct { *plugins.PluginBase - ApiKey *plugins.SetupQuestion - ApiBaseURL *plugins.SetupQuestion - ApiClient *openai.Client + ApiKey *plugins.SetupQuestion + ApiBaseURL *plugins.SetupQuestion + ApiClient *openai.Client + ImplementsResponses bool // Whether this provider supports the Responses API } func (o *Client) configure() (ret error) { @@ -76,6 +88,16 @@ func (o *Client) ListModels() (ret []string, err error) { func (o *Client) SendStream( msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions, channel chan string, +) (err error) { + // Use Responses API for OpenAI, Chat Completions API for other providers + if o.supportsResponsesAPI() { + return o.sendStreamResponses(msgs, opts, channel) + } + return o.sendStreamChatCompletions(msgs, opts, channel) +} + +func (o *Client) sendStreamResponses( + msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions, channel chan string, ) (err error) { req := o.buildResponseParams(msgs, opts) stream := o.ApiClient.Responses.NewStreaming(context.Background(), req) @@ -96,6 +118,14 @@ func (o *Client) SendStream( } func (o *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions) (ret string, err error) { + // Use Responses API for OpenAI, Chat Completions API for other providers + if o.supportsResponsesAPI() { + return o.sendResponses(ctx, msgs, opts) + } + return o.sendChatCompletions(ctx, msgs, opts) +} + +func (o *Client) sendResponses(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *common.ChatOptions) (ret string, err error) { req := o.buildResponseParams(msgs, opts) var resp *responses.Response @@ -106,6 +136,19 @@ func (o *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, o return } +// supportsResponsesAPI determines if the provider supports the new Responses API +func (o *Client) supportsResponsesAPI() bool { + // For the main OpenAI client, check the base URL (backward compatibility) + if o.ApiBaseURL != nil { + baseURL := o.ApiBaseURL.Value + if baseURL == "" || baseURL == "https://api.openai.com/v1" { + return true + } + } + // For OpenAI-compatible providers, use the explicit flag + return o.ImplementsResponses +} + func (o *Client) NeedsRawMode(modelName string) bool { openaiModelsPrefixes := []string{ "o1", @@ -154,6 +197,21 @@ func (o *Client) buildResponseParams( if opts.MaxTokens != 0 { ret.MaxOutputTokens = openai.Int(int64(opts.MaxTokens)) } + + // Add parameters not officially supported by Responses API as extra fields + extraFields := make(map[string]any) + if opts.PresencePenalty != 0 { + extraFields["presence_penalty"] = opts.PresencePenalty + } + if opts.FrequencyPenalty != 0 { + extraFields["frequency_penalty"] = opts.FrequencyPenalty + } + if opts.Seed != 0 { + extraFields["seed"] = opts.Seed + } + if len(extraFields) > 0 { + ret.SetExtraFields(extraFields) + } } return } diff --git a/plugins/ai/openai_compatible/providers_config.go b/plugins/ai/openai_compatible/providers_config.go index 828e1940..a15f5cb2 100644 --- a/plugins/ai/openai_compatible/providers_config.go +++ b/plugins/ai/openai_compatible/providers_config.go @@ -9,8 +9,9 @@ import ( // ProviderConfig defines the configuration for an OpenAI-compatible API provider type ProviderConfig struct { - Name string - BaseURL string + Name string + BaseURL string + ImplementsResponses bool // Whether the provider supports OpenAI's new Responses API } // Client is the common structure for all OpenAI-compatible providers @@ -21,51 +22,66 @@ type Client struct { // NewClient creates a new OpenAI-compatible client for the specified provider func NewClient(providerConfig ProviderConfig) *Client { client := &Client{} - client.Client = openai.NewClientCompatible(providerConfig.Name, providerConfig.BaseURL, nil) + client.Client = openai.NewClientCompatibleWithResponses( + providerConfig.Name, + providerConfig.BaseURL, + providerConfig.ImplementsResponses, + nil, + ) return client } // ProviderMap is a map of provider name to ProviderConfig for O(1) lookup var ProviderMap = map[string]ProviderConfig{ "AIML": { - Name: "AIML", - BaseURL: "https://api.aimlapi.com/v1", + Name: "AIML", + BaseURL: "https://api.aimlapi.com/v1", + ImplementsResponses: false, }, "Cerebras": { - Name: "Cerebras", - BaseURL: "https://api.cerebras.ai/v1", + Name: "Cerebras", + BaseURL: "https://api.cerebras.ai/v1", + ImplementsResponses: false, }, "DeepSeek": { - Name: "DeepSeek", - BaseURL: "https://api.deepseek.com", + Name: "DeepSeek", + BaseURL: "https://api.deepseek.com", + ImplementsResponses: false, }, "GrokAI": { - Name: "GrokAI", - BaseURL: "https://api.x.ai/v1", + Name: "GrokAI", + BaseURL: "https://api.x.ai/v1", + ImplementsResponses: false, }, "Groq": { - Name: "Groq", - BaseURL: "https://api.groq.com/openai/v1", + Name: "Groq", + BaseURL: "https://api.groq.com/openai/v1", + ImplementsResponses: false, }, "Langdock": { - Name: "Langdock", - BaseURL: "https://api.langdock.com/openai/{{REGION=us}}/v1", + Name: "Langdock", + BaseURL: "https://api.langdock.com/openai/{{REGION=us}}/v1", + ImplementsResponses: false, }, "LiteLLM": { - Name: "LiteLLM", - BaseURL: "http://localhost:4000", + Name: "LiteLLM", + BaseURL: "http://localhost:4000", + ImplementsResponses: false, }, "Mistral": { - Name: "Mistral", - BaseURL: "https://api.mistral.ai/v1", + Name: "Mistral", + BaseURL: "https://api.mistral.ai/v1", + ImplementsResponses: false, }, "OpenRouter": { - Name: "OpenRouter", - BaseURL: "https://openrouter.ai/api/v1", + Name: "OpenRouter", + BaseURL: "https://openrouter.ai/api/v1", + ImplementsResponses: false, }, "SiliconCloud": { - Name: "SiliconCloud", - BaseURL: "https://api.siliconflow.cn/v1", + Name: "SiliconCloud", + BaseURL: "https://api.siliconflow.cn/v1", + ImplementsResponses: false, }, }