on main help sign in
feat: add responses API support
4f74897kujtimiihoxha
internal/agent/agent.go+6
@@ -12,6 +12,7 @@ import (
12
12
13 "charm.land/fantasy"
13 "charm.land/fantasy"
14 "charm.land/fantasy/providers/anthropic"
14 "charm.land/fantasy/providers/anthropic"
15+ "charm.land/fantasy/providers/openai"
15 "github.com/charmbracelet/catwalk/pkg/catwalk"
16 "github.com/charmbracelet/catwalk/pkg/catwalk"
16 "github.com/charmbracelet/crush/internal/agent/tools"
17 "github.com/charmbracelet/crush/internal/agent/tools"
17 "github.com/charmbracelet/crush/internal/config"
18 "github.com/charmbracelet/crush/internal/config"
@@ -241,6 +242,11 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
241 currentAssistant.AppendReasoningSignature(reasoning.Signature)
242 currentAssistant.AppendReasoningSignature(reasoning.Signature)
242 }
243 }
243 }
244 }
245+ if openaiData, ok := reasoning.ProviderMetadata[openai.Name]; ok {
246+ if reasoning, ok := openaiData.(*openai.ResponsesReasoningMetadata); ok {
247+ currentAssistant.SetReasoningResponsesData(reasoning)
248+ }
249+ }
244 currentAssistant.FinishThinking()
250 currentAssistant.FinishThinking()
245 return a.messages.Update(genCtx, *currentAssistant)
251 return a.messages.Update(genCtx, *currentAssistant)
246 },
252 },
internal/agent/coordinator.go+15−3
@@ -176,9 +176,20 @@ func getProviderOptions(model Model, tp catwalk.Type) fantasy.ProviderOptions {
176 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
176 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
177 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
177 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
178 }
178 }
179+ if openai.IsResponsesModel(model.CatwalkCfg.ID) {
180+ if openai.IsResponsesReasoningModel(model.CatwalkCfg.ID) {
181+ mergedOptions["reasoning_summary"] = "auto"
182+ mergedOptions["include"] = []openai.IncludeType{openai.IncludeReasoningEncryptedContent}
183+ }
184+ parsed, err := openai.ParseResponsesOptions(mergedOptions)
185+ if err == nil {
186+ options[openai.Name] = parsed
187+ }
188+ } else {
179− parsed, err := openai.ParseOptions(mergedOptions)
189+ parsed, err := openai.ParseOptions(mergedOptions)
180− if err == nil {
190+ if err == nil {
181− options[openai.Name] = parsed
191+ options[openai.Name] = parsed
192+ }
182 }
193 }
183 case anthropic.Name:
194 case anthropic.Name:
184 _, hasThink := mergedOptions["thinking"]
195 _, hasThink := mergedOptions["thinking"]
@@ -433,6 +444,7 @@ func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map
433func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) fantasy.Provider {
444func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) fantasy.Provider {
434 opts := []openai.Option{
445 opts := []openai.Option{
435 openai.WithAPIKey(apiKey),
446 openai.WithAPIKey(apiKey),
447+ openai.WithUseResponsesAPI(),
436 }
448 }
437 if c.cfg.Options.Debug {
449 if c.cfg.Options.Debug {
438 httpClient := log.NewHTTPClient()
450 httpClient := log.NewHTTPClient()
internal/message/content.go+24−5
@@ -9,6 +9,7 @@ import (
9
9
10 "charm.land/fantasy"
10 "charm.land/fantasy"
11 "charm.land/fantasy/providers/anthropic"
11 "charm.land/fantasy/providers/anthropic"
12+ "charm.land/fantasy/providers/openai"
12 "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/catwalk/pkg/catwalk"
13)
14)
14
15
@@ -40,10 +41,11 @@ type ContentPart interface {
40}
41}
41
42
42type ReasoningContent struct {
43type ReasoningContent struct {
43− Thinking string `json:"thinking"`
44+ Thinking string `json:"thinking"`
44− Signature string `json:"signature"`
45+ Signature string `json:"signature"`
46+ ResponsesData *openai.ResponsesReasoningMetadata `json:"responses_data"`
45− StartedAt int64 `json:"started_at,omitempty"`
47+ StartedAt int64 `json:"started_at,omitempty"`
46− FinishedAt int64 `json:"finished_at,omitempty"`
48+ FinishedAt int64 `json:"finished_at,omitempty"`
47}
49}
48
50
49func (tc ReasoningContent) String() string {
51func (tc ReasoningContent) String() string {
@@ -272,6 +274,20 @@ func (m *Message) AppendReasoningSignature(signature string) {
272 m.Parts = append(m.Parts, ReasoningContent{Signature: signature})
274 m.Parts = append(m.Parts, ReasoningContent{Signature: signature})
273}
275}
274
276
277+func (m *Message) SetReasoningResponsesData(data *openai.ResponsesReasoningMetadata) {
278+ for i, part := range m.Parts {
279+ if c, ok := part.(ReasoningContent); ok {
280+ m.Parts[i] = ReasoningContent{
281+ Thinking: c.Thinking,
282+ ResponsesData: data,
283+ StartedAt: c.StartedAt,
284+ FinishedAt: c.FinishedAt,
285+ }
286+ return
287+ }
288+ }
289+}
290+
275func (m *Message) FinishThinking() {
291func (m *Message) FinishThinking() {
276 for i, part := range m.Parts {
292 for i, part := range m.Parts {
277 if c, ok := part.(ReasoningContent); ok {
293 if c, ok := part.(ReasoningContent); ok {
@@ -420,10 +436,13 @@ func (m *Message) ToAIMessage() []fantasy.Message {
420 if reasoning.Thinking != "" {
436 if reasoning.Thinking != "" {
421 reasoningPart := fantasy.ReasoningPart{Text: reasoning.Thinking, ProviderOptions: fantasy.ProviderOptions{}}
437 reasoningPart := fantasy.ReasoningPart{Text: reasoning.Thinking, ProviderOptions: fantasy.ProviderOptions{}}
422 if reasoning.Signature != "" {
438 if reasoning.Signature != "" {
423− reasoningPart.ProviderOptions["anthropic"] = &anthropic.ReasoningOptionMetadata{
439+ reasoningPart.ProviderOptions[anthropic.Name] = &anthropic.ReasoningOptionMetadata{
424 Signature: reasoning.Signature,
440 Signature: reasoning.Signature,
425 }
441 }
426 }
442 }
443+ if reasoning.ResponsesData != nil {
444+ reasoningPart.ProviderOptions[openai.Name] = reasoning.ResponsesData
445+ }
427 parts = append(parts, reasoningPart)
446 parts = append(parts, reasoningPart)
428 }
447 }
429 for _, call := range m.ToolCalls() {
448 for _, call := range m.ToolCalls() {