Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions internal/llm/fantasy_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,26 +125,40 @@
var err error
switch opts.protocol {
case APIContractAnthropic:
if reasoning := ProviderConfigResponsesReasoning(opts.providerConfig); reasoning != nil && reasoning.Enabled != nil {
client.initErr = fmt.Errorf("provider_config.reasoning.enabled is unsupported for Anthropic")
return client
}
provider, err = anthropic.New(
anthropic.WithBaseURL(strings.TrimRight(opts.baseURL, "/")),
anthropic.WithAPIKey(opts.apiKey),
anthropic.WithHTTPClient(httpClient),
)
case APIContractResponses:
providerOptions, optionsErr := providerConfigSDKOptions(opts.providerConfig, opts.protocol)
if optionsErr != nil {
client.initErr = optionsErr
return client
}
provider, err = openai.New(
openai.WithBaseURL(fantasyOpenAIBaseURL(opts.baseURL, "/v1/responses")),
openai.WithAPIKey(opts.apiKey),
openai.WithHTTPClient(httpClient),
openai.WithUseResponsesAPI(),
openai.WithResponsesAPIFunc(func(string) bool { return true }),
openai.WithSDKOptions(providerConfigSDKOptions(opts.providerConfig, opts.protocol)...),
openai.WithSDKOptions(providerOptions...),
)
default:
providerOptions, optionsErr := providerConfigSDKOptions(opts.providerConfig, opts.protocol)
if optionsErr != nil {
client.initErr = optionsErr
return client
}
provider, err = openai.New(
openai.WithBaseURL(fantasyOpenAIBaseURL(opts.baseURL, opts.chatPath)),
openai.WithAPIKey(opts.apiKey),
openai.WithHTTPClient(httpClient),
openai.WithSDKOptions(providerConfigSDKOptions(opts.providerConfig, opts.protocol)...),
openai.WithSDKOptions(providerOptions...),
)
}
if err != nil {
Expand All @@ -155,10 +169,18 @@
return client
}

func providerConfigSDKOptions(raw, protocol string) []sdkoption.RequestOption {
func providerConfigSDKOptions(raw, protocol string) ([]sdkoption.RequestOption, error) {
var config map[string]json.RawMessage
if json.Unmarshal([]byte(raw), &config) != nil {
return nil
return nil, nil

Check failure on line 175 in internal/llm/fantasy_adapter.go

View workflow job for this annotation

GitHub Actions / Lint

error is not nil (line 174) but it returns nil (nilerr)
}
if reasoning := ProviderConfigResponsesReasoning(raw); reasoning != nil && protocol == APIContractChatCompletions {
if reasoning.Summary != "" || reasoning.BudgetTokens != 0 {
return nil, fmt.Errorf("provider_config.reasoning summary and budget_tokens are unsupported for Chat Completions")
}
}
if reasoning := ProviderConfigResponsesReasoning(raw); reasoning != nil && protocol == APIContractResponses && reasoning.Enabled != nil {
return nil, fmt.Errorf("provider_config.reasoning.enabled is unsupported for Responses")
}
generated := map[string]bool{
"model": true, "messages": true, "input": true, "temperature": true,
Expand All @@ -172,6 +194,18 @@
generated["parallel_tool_calls"] = true
}
var options []sdkoption.RequestOption
if protocol == APIContractChatCompletions {
if reasoning := ProviderConfigResponsesReasoning(raw); reasoning != nil {
value := map[string]any{}
if reasoning.Effort != "" {
value["effort"] = reasoning.Effort
}
if reasoning.Enabled != nil {
value["enabled"] = *reasoning.Enabled
}
options = append(options, sdkoption.WithJSONSet("reasoning", value))
}
}
for key, rawValue := range config {
if reservedProviderConfigKeys[key] || generated[key] {
continue
Expand All @@ -181,7 +215,7 @@
options = append(options, sdkoption.WithJSONSet(key, value))
}
}
return options
return options, nil
}

func fantasyOpenAIBaseURL(baseURL, endpointPath string) string {
Expand Down
14 changes: 12 additions & 2 deletions internal/llm/provider_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ type ResponsesReasoningConfig struct {
Effort string `json:"effort,omitempty"`
Summary string `json:"summary,omitempty"`
BudgetTokens int64 `json:"budget_tokens,omitempty"`
// Enabled is supported by OpenAI-compatible Chat Completions providers,
// but not by the Responses or Anthropic contracts.
Enabled *bool `json:"enabled,omitempty"`
}

// ValidateProviderConfig verifies the per-connection provider_config blob.
Expand Down Expand Up @@ -84,11 +87,18 @@ func ValidateProviderConfig(raw string) error {
}
var reasoning ResponsesReasoningConfig
if err := json.Unmarshal(rawReasoning, &reasoning); err != nil {
return fmt.Errorf("provider_config.reasoning must contain valid effort, summary, and budget_tokens fields")
return fmt.Errorf("provider_config.reasoning must contain valid effort, summary, budget_tokens, and enabled fields")
}
if reasoning.BudgetTokens < 0 {
return fmt.Errorf("provider_config.reasoning.budget_tokens must be non-negative")
}
for key := range reasoningObject {
switch key {
case "effort", "summary", "budget_tokens", "enabled":
default:
return fmt.Errorf("provider_config.reasoning.%s is unsupported", key)
}
}
}
if rawMax, ok := cfg[providerConfigMaxOutputTokensKey]; ok {
var value int
Expand Down Expand Up @@ -212,7 +222,7 @@ func ProviderConfigResponsesReasoning(raw string) *ResponsesReasoningConfig {
}
reasoning.Effort = strings.TrimSpace(reasoning.Effort)
reasoning.Summary = strings.TrimSpace(reasoning.Summary)
if reasoning.Effort == "" && reasoning.Summary == "" && reasoning.BudgetTokens == 0 {
if reasoning.Effort == "" && reasoning.Summary == "" && reasoning.BudgetTokens == 0 && reasoning.Enabled == nil {
return nil
}
return &reasoning
Expand Down
Loading