fix(fantasy): replace panics with error returns and harden provider logic
- google: WithVertex validates project/location at New() instead of panicking; toGooglePrompt returns error for unsupported message roles - anthropic/google: emit CallWarning for skipped system messages instead of silent drops - openai responses: remove unused streamErr variable, simplify error/finish branching in streamObjectWithJSONMode — error events now return immediately without checking a stale flag - openai/openrouter: remove resolved TODO comments, fix import ordering
This commit is contained in:
parent
dc84a4b57f
commit
f14dfed1b4
5 changed files with 33 additions and 27 deletions
|
|
@ -7,11 +7,12 @@ import (
|
|||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"io"
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"charm.land/fantasy/object"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
|
|
@ -542,8 +543,10 @@ func toPrompt(prompt fantasy.Prompt, sendReasoningData bool) ([]anthropic.TextBl
|
|||
switch block.Role {
|
||||
case fantasy.MessageRoleSystem:
|
||||
if finishedSystemBlock {
|
||||
// skip multiple system messages that are separated by user/assistant messages
|
||||
// TODO: see if we need to send error here?
|
||||
warnings = append(warnings, fantasy.CallWarning{
|
||||
Type: fantasy.CallWarningTypeOther,
|
||||
Message: "anthropic: additional system message after user/assistant messages was skipped",
|
||||
})
|
||||
continue
|
||||
}
|
||||
finishedSystemBlock = true
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"maps"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"charm.land/fantasy/object"
|
||||
"charm.land/fantasy/providers/anthropic"
|
||||
|
|
@ -62,6 +63,10 @@ func New(opts ...Option) (fantasy.Provider, error) {
|
|||
|
||||
options.name = cmp.Or(options.name, Name)
|
||||
|
||||
if options.backend == genai.BackendVertexAI && (options.project == "" || options.location == "") {
|
||||
return nil, errors.New("google: WithVertex requires non-empty project and location")
|
||||
}
|
||||
|
||||
return &provider{
|
||||
options: options,
|
||||
}, nil
|
||||
|
|
@ -85,10 +90,8 @@ func WithGeminiAPIKey(apiKey string) Option {
|
|||
}
|
||||
|
||||
// WithVertex configures the Google provider to use Vertex AI.
|
||||
// Both project and location are validated when New() is called.
|
||||
func WithVertex(project, location string) Option {
|
||||
if project == "" || location == "" {
|
||||
panic("project and location must be provided")
|
||||
}
|
||||
return func(o *options) {
|
||||
o.backend = genai.BackendVertexAI
|
||||
o.apiKey = ""
|
||||
|
|
@ -220,7 +223,10 @@ func (g languageModel) prepareParams(call fantasy.Call) (*genai.GenerateContentC
|
|||
}
|
||||
}
|
||||
|
||||
systemInstructions, content, warnings := toGooglePrompt(call.Prompt)
|
||||
systemInstructions, content, warnings, err := toGooglePrompt(call.Prompt)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
if providerOptions.ThinkingConfig != nil {
|
||||
if providerOptions.ThinkingConfig.IncludeThoughts != nil &&
|
||||
|
|
@ -324,7 +330,7 @@ func (g languageModel) prepareParams(call fantasy.Call) (*genai.GenerateContentC
|
|||
return config, content, warnings, nil
|
||||
}
|
||||
|
||||
func toGooglePrompt(prompt fantasy.Prompt) (*genai.Content, []*genai.Content, []fantasy.CallWarning) { //nolint: unparam
|
||||
func toGooglePrompt(prompt fantasy.Prompt) (*genai.Content, []*genai.Content, []fantasy.CallWarning, error) {
|
||||
var systemInstructions *genai.Content
|
||||
var content []*genai.Content
|
||||
var warnings []fantasy.CallWarning
|
||||
|
|
@ -334,8 +340,10 @@ func toGooglePrompt(prompt fantasy.Prompt) (*genai.Content, []*genai.Content, []
|
|||
switch msg.Role {
|
||||
case fantasy.MessageRoleSystem:
|
||||
if finishedSystemBlock {
|
||||
// skip multiple system messages that are separated by user/assistant messages
|
||||
// TODO: see if we need to send error here?
|
||||
warnings = append(warnings, fantasy.CallWarning{
|
||||
Type: fantasy.CallWarningTypeOther,
|
||||
Message: "google: additional system message after user/assistant messages was skipped",
|
||||
})
|
||||
continue
|
||||
}
|
||||
finishedSystemBlock = true
|
||||
|
|
@ -514,10 +522,10 @@ func toGooglePrompt(prompt fantasy.Prompt) (*genai.Content, []*genai.Content, []
|
|||
})
|
||||
}
|
||||
default:
|
||||
panic("unsupported message role: " + msg.Role)
|
||||
return nil, nil, nil, fmt.Errorf("google: unsupported message role: %s", msg.Role)
|
||||
}
|
||||
}
|
||||
return systemInstructions, content, warnings
|
||||
return systemInstructions, content, warnings, nil
|
||||
}
|
||||
|
||||
// Generate implements fantasy.LanguageModel.
|
||||
|
|
|
|||
|
|
@ -539,7 +539,6 @@ func DefaultToPrompt(prompt fantasy.Prompt, _, _ string) ([]openai.ChatCompletio
|
|||
}
|
||||
messages = append(messages, openai.ToolMessage(output.Text, toolResultPart.ToolCallID))
|
||||
case fantasy.ToolResultContentTypeError:
|
||||
// TODO: check if better handling is needed
|
||||
output, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](toolResultPart.Output)
|
||||
if !ok {
|
||||
warnings = append(warnings, fantasy.CallWarning{
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import (
|
|||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"charm.land/fantasy/object"
|
||||
"charm.land/fantasy/schema"
|
||||
|
|
@ -1232,7 +1233,6 @@ func (o responsesLanguageModel) streamObjectWithJSONMode(ctx context.Context, ca
|
|||
var lastParsedObject any
|
||||
var usage fantasy.Usage
|
||||
var finishReason fantasy.FinishReason
|
||||
var streamErr error
|
||||
hasFunctionCall := false
|
||||
|
||||
for stream.Next() {
|
||||
|
|
@ -1299,13 +1299,10 @@ func (o responsesLanguageModel) streamObjectWithJSONMode(ctx context.Context, ca
|
|||
|
||||
case "error":
|
||||
errorEvent := event.AsError()
|
||||
streamErr = fmt.Errorf("response error: %s (code: %s)", errorEvent.Message, errorEvent.Code)
|
||||
if !yield(fantasy.ObjectStreamPart{
|
||||
yield(fantasy.ObjectStreamPart{
|
||||
Type: fantasy.ObjectStreamPartTypeError,
|
||||
Error: streamErr,
|
||||
}) {
|
||||
return
|
||||
}
|
||||
Error: fmt.Errorf("response error: %s (code: %s)", errorEvent.Message, errorEvent.Code),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -1320,14 +1317,13 @@ func (o responsesLanguageModel) streamObjectWithJSONMode(ctx context.Context, ca
|
|||
}
|
||||
|
||||
// Final validation and emit
|
||||
if streamErr == nil && lastParsedObject != nil {
|
||||
if lastParsedObject != nil {
|
||||
yield(fantasy.ObjectStreamPart{
|
||||
Type: fantasy.ObjectStreamPartTypeFinish,
|
||||
Usage: usage,
|
||||
FinishReason: finishReason,
|
||||
})
|
||||
} else if streamErr == nil && lastParsedObject == nil {
|
||||
// No object was generated
|
||||
} else {
|
||||
yield(fantasy.ObjectStreamPart{
|
||||
Type: fantasy.ObjectStreamPartTypeError,
|
||||
Error: &fantasy.NoObjectGeneratedError{
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ package openrouter
|
|||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"charm.land/fantasy/providers/anthropic"
|
||||
"charm.land/fantasy/providers/google"
|
||||
|
|
@ -1010,7 +1011,6 @@ func languageModelToPrompt(prompt fantasy.Prompt, _, model string) ([]openaisdk.
|
|||
}
|
||||
messages = append(messages, tr)
|
||||
case fantasy.ToolResultContentTypeError:
|
||||
// TODO: check if better handling is needed
|
||||
output, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](toolResultPart.Output)
|
||||
if !ok {
|
||||
warnings = append(warnings, fantasy.CallWarning{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue