From 4f7489717b6e400ef7b504767d150258bd439b47 Mon Sep 17 00:00:00 2001 From: kujtimiihoxha Date: Fri, 17 Oct 2025 18:02:03 +0200 Subject: [PATCH] feat: add responses API support --- internal/agent/agent.go | 6 ++++++ internal/agent/coordinator.go | 18 +++++++++++++++--- internal/message/content.go | 29 ++++++++++++++++++++++++----- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 9a237954179f83e9a60295162f3225b98a0cdbe3..cbfe5683b66c57e74fd4a74ccac20bf24bfa6720 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -12,6 +12,7 @@ import ( "charm.land/fantasy" "charm.land/fantasy/providers/anthropic" + "charm.land/fantasy/providers/openai" "github.com/charmbracelet/catwalk/pkg/catwalk" "github.com/charmbracelet/crush/internal/agent/tools" "github.com/charmbracelet/crush/internal/config" @@ -241,6 +242,11 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy currentAssistant.AppendReasoningSignature(reasoning.Signature) } } + if openaiData, ok := reasoning.ProviderMetadata[openai.Name]; ok { + if reasoning, ok := openaiData.(*openai.ResponsesReasoningMetadata); ok { + currentAssistant.SetReasoningResponsesData(reasoning) + } + } currentAssistant.FinishThinking() return a.messages.Update(genCtx, *currentAssistant) }, diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 241d90d813d150cc3632e8731b83ed6a4ab6fa9b..533142b4720d819abc6bac2cafa395744e93e7c5 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -176,9 +176,20 @@ func getProviderOptions(model Model, tp catwalk.Type) fantasy.ProviderOptions { if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" { mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort } - parsed, err := openai.ParseOptions(mergedOptions) - if err == nil { - options[openai.Name] = parsed + if openai.IsResponsesModel(model.CatwalkCfg.ID) { + if openai.IsResponsesReasoningModel(model.CatwalkCfg.ID) { + mergedOptions["reasoning_summary"] = "auto" + mergedOptions["include"] = []openai.IncludeType{openai.IncludeReasoningEncryptedContent} + } + parsed, err := openai.ParseResponsesOptions(mergedOptions) + if err == nil { + options[openai.Name] = parsed + } + } else { + parsed, err := openai.ParseOptions(mergedOptions) + if err == nil { + options[openai.Name] = parsed + } } case anthropic.Name: _, hasThink := mergedOptions["thinking"] @@ -433,6 +444,7 @@ func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) fantasy.Provider { opts := []openai.Option{ openai.WithAPIKey(apiKey), + openai.WithUseResponsesAPI(), } if c.cfg.Options.Debug { httpClient := log.NewHTTPClient() diff --git a/internal/message/content.go b/internal/message/content.go index 6c95aea9e94b5730858361f77e36d35ec7cd1a2e..84187ed5c830d0a9a5a00e94c04b2b6c6b5c2440 100644 --- a/internal/message/content.go +++ b/internal/message/content.go @@ -9,6 +9,7 @@ import ( "charm.land/fantasy" "charm.land/fantasy/providers/anthropic" + "charm.land/fantasy/providers/openai" "github.com/charmbracelet/catwalk/pkg/catwalk" ) @@ -40,10 +41,11 @@ type ContentPart interface { } type ReasoningContent struct { - Thinking string `json:"thinking"` - Signature string `json:"signature"` - StartedAt int64 `json:"started_at,omitempty"` - FinishedAt int64 `json:"finished_at,omitempty"` + Thinking string `json:"thinking"` + Signature string `json:"signature"` + ResponsesData *openai.ResponsesReasoningMetadata `json:"responses_data"` + StartedAt int64 `json:"started_at,omitempty"` + FinishedAt int64 `json:"finished_at,omitempty"` } func (tc ReasoningContent) String() string { @@ -272,6 +274,20 @@ func (m *Message) AppendReasoningSignature(signature string) { m.Parts = append(m.Parts, ReasoningContent{Signature: signature}) } +func (m *Message) SetReasoningResponsesData(data *openai.ResponsesReasoningMetadata) { + for i, part := range m.Parts { + if c, ok := part.(ReasoningContent); ok { + m.Parts[i] = ReasoningContent{ + Thinking: c.Thinking, + ResponsesData: data, + StartedAt: c.StartedAt, + FinishedAt: c.FinishedAt, + } + return + } + } +} + func (m *Message) FinishThinking() { for i, part := range m.Parts { if c, ok := part.(ReasoningContent); ok { @@ -420,10 +436,13 @@ func (m *Message) ToAIMessage() []fantasy.Message { if reasoning.Thinking != "" { reasoningPart := fantasy.ReasoningPart{Text: reasoning.Thinking, ProviderOptions: fantasy.ProviderOptions{}} if reasoning.Signature != "" { - reasoningPart.ProviderOptions["anthropic"] = &anthropic.ReasoningOptionMetadata{ + reasoningPart.ProviderOptions[anthropic.Name] = &anthropic.ReasoningOptionMetadata{ Signature: reasoning.Signature, } } + if reasoning.ResponsesData != nil { + reasoningPart.ProviderOptions[openai.Name] = reasoning.ResponsesData + } parts = append(parts, reasoningPart) } for _, call := range m.ToolCalls() {