@@ -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)
},
@@ -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()
@@ -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() {