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"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
jsonv2 "github.com/go-json-experiment/json"
|
|
||||||
"io"
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
jsonv2 "github.com/go-json-experiment/json"
|
||||||
|
|
||||||
"charm.land/fantasy"
|
"charm.land/fantasy"
|
||||||
"charm.land/fantasy/object"
|
"charm.land/fantasy/object"
|
||||||
"github.com/aws/aws-sdk-go-v2/config"
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
|
@ -542,8 +543,10 @@ func toPrompt(prompt fantasy.Prompt, sendReasoningData bool) ([]anthropic.TextBl
|
||||||
switch block.Role {
|
switch block.Role {
|
||||||
case fantasy.MessageRoleSystem:
|
case fantasy.MessageRoleSystem:
|
||||||
if finishedSystemBlock {
|
if finishedSystemBlock {
|
||||||
// skip multiple system messages that are separated by user/assistant messages
|
warnings = append(warnings, fantasy.CallWarning{
|
||||||
// TODO: see if we need to send error here?
|
Type: fantasy.CallWarningTypeOther,
|
||||||
|
Message: "anthropic: additional system message after user/assistant messages was skipped",
|
||||||
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
finishedSystemBlock = true
|
finishedSystemBlock = true
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
jsonv2 "github.com/go-json-experiment/json"
|
|
||||||
"maps"
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
jsonv2 "github.com/go-json-experiment/json"
|
||||||
|
|
||||||
"charm.land/fantasy"
|
"charm.land/fantasy"
|
||||||
"charm.land/fantasy/object"
|
"charm.land/fantasy/object"
|
||||||
"charm.land/fantasy/providers/anthropic"
|
"charm.land/fantasy/providers/anthropic"
|
||||||
|
|
@ -62,6 +63,10 @@ func New(opts ...Option) (fantasy.Provider, error) {
|
||||||
|
|
||||||
options.name = cmp.Or(options.name, Name)
|
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{
|
return &provider{
|
||||||
options: options,
|
options: options,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
@ -85,10 +90,8 @@ func WithGeminiAPIKey(apiKey string) Option {
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithVertex configures the Google provider to use Vertex AI.
|
// 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 {
|
func WithVertex(project, location string) Option {
|
||||||
if project == "" || location == "" {
|
|
||||||
panic("project and location must be provided")
|
|
||||||
}
|
|
||||||
return func(o *options) {
|
return func(o *options) {
|
||||||
o.backend = genai.BackendVertexAI
|
o.backend = genai.BackendVertexAI
|
||||||
o.apiKey = ""
|
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 != nil {
|
||||||
if providerOptions.ThinkingConfig.IncludeThoughts != nil &&
|
if providerOptions.ThinkingConfig.IncludeThoughts != nil &&
|
||||||
|
|
@ -324,7 +330,7 @@ func (g languageModel) prepareParams(call fantasy.Call) (*genai.GenerateContentC
|
||||||
return config, content, warnings, nil
|
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 systemInstructions *genai.Content
|
||||||
var content []*genai.Content
|
var content []*genai.Content
|
||||||
var warnings []fantasy.CallWarning
|
var warnings []fantasy.CallWarning
|
||||||
|
|
@ -334,8 +340,10 @@ func toGooglePrompt(prompt fantasy.Prompt) (*genai.Content, []*genai.Content, []
|
||||||
switch msg.Role {
|
switch msg.Role {
|
||||||
case fantasy.MessageRoleSystem:
|
case fantasy.MessageRoleSystem:
|
||||||
if finishedSystemBlock {
|
if finishedSystemBlock {
|
||||||
// skip multiple system messages that are separated by user/assistant messages
|
warnings = append(warnings, fantasy.CallWarning{
|
||||||
// TODO: see if we need to send error here?
|
Type: fantasy.CallWarningTypeOther,
|
||||||
|
Message: "google: additional system message after user/assistant messages was skipped",
|
||||||
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
finishedSystemBlock = true
|
finishedSystemBlock = true
|
||||||
|
|
@ -514,10 +522,10 @@ func toGooglePrompt(prompt fantasy.Prompt) (*genai.Content, []*genai.Content, []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
default:
|
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.
|
// 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))
|
messages = append(messages, openai.ToolMessage(output.Text, toolResultPart.ToolCallID))
|
||||||
case fantasy.ToolResultContentTypeError:
|
case fantasy.ToolResultContentTypeError:
|
||||||
// TODO: check if better handling is needed
|
|
||||||
output, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](toolResultPart.Output)
|
output, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](toolResultPart.Output)
|
||||||
if !ok {
|
if !ok {
|
||||||
warnings = append(warnings, fantasy.CallWarning{
|
warnings = append(warnings, fantasy.CallWarning{
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
jsonv2 "github.com/go-json-experiment/json"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
jsonv2 "github.com/go-json-experiment/json"
|
||||||
|
|
||||||
"charm.land/fantasy"
|
"charm.land/fantasy"
|
||||||
"charm.land/fantasy/object"
|
"charm.land/fantasy/object"
|
||||||
"charm.land/fantasy/schema"
|
"charm.land/fantasy/schema"
|
||||||
|
|
@ -1232,7 +1233,6 @@ func (o responsesLanguageModel) streamObjectWithJSONMode(ctx context.Context, ca
|
||||||
var lastParsedObject any
|
var lastParsedObject any
|
||||||
var usage fantasy.Usage
|
var usage fantasy.Usage
|
||||||
var finishReason fantasy.FinishReason
|
var finishReason fantasy.FinishReason
|
||||||
var streamErr error
|
|
||||||
hasFunctionCall := false
|
hasFunctionCall := false
|
||||||
|
|
||||||
for stream.Next() {
|
for stream.Next() {
|
||||||
|
|
@ -1299,13 +1299,10 @@ func (o responsesLanguageModel) streamObjectWithJSONMode(ctx context.Context, ca
|
||||||
|
|
||||||
case "error":
|
case "error":
|
||||||
errorEvent := event.AsError()
|
errorEvent := event.AsError()
|
||||||
streamErr = fmt.Errorf("response error: %s (code: %s)", errorEvent.Message, errorEvent.Code)
|
yield(fantasy.ObjectStreamPart{
|
||||||
if !yield(fantasy.ObjectStreamPart{
|
|
||||||
Type: fantasy.ObjectStreamPartTypeError,
|
Type: fantasy.ObjectStreamPartTypeError,
|
||||||
Error: streamErr,
|
Error: fmt.Errorf("response error: %s (code: %s)", errorEvent.Message, errorEvent.Code),
|
||||||
}) {
|
})
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1320,14 +1317,13 @@ func (o responsesLanguageModel) streamObjectWithJSONMode(ctx context.Context, ca
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final validation and emit
|
// Final validation and emit
|
||||||
if streamErr == nil && lastParsedObject != nil {
|
if lastParsedObject != nil {
|
||||||
yield(fantasy.ObjectStreamPart{
|
yield(fantasy.ObjectStreamPart{
|
||||||
Type: fantasy.ObjectStreamPartTypeFinish,
|
Type: fantasy.ObjectStreamPartTypeFinish,
|
||||||
Usage: usage,
|
Usage: usage,
|
||||||
FinishReason: finishReason,
|
FinishReason: finishReason,
|
||||||
})
|
})
|
||||||
} else if streamErr == nil && lastParsedObject == nil {
|
} else {
|
||||||
// No object was generated
|
|
||||||
yield(fantasy.ObjectStreamPart{
|
yield(fantasy.ObjectStreamPart{
|
||||||
Type: fantasy.ObjectStreamPartTypeError,
|
Type: fantasy.ObjectStreamPartTypeError,
|
||||||
Error: &fantasy.NoObjectGeneratedError{
|
Error: &fantasy.NoObjectGeneratedError{
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@ package openrouter
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
jsonv2 "github.com/go-json-experiment/json"
|
|
||||||
"maps"
|
"maps"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
jsonv2 "github.com/go-json-experiment/json"
|
||||||
|
|
||||||
"charm.land/fantasy"
|
"charm.land/fantasy"
|
||||||
"charm.land/fantasy/providers/anthropic"
|
"charm.land/fantasy/providers/anthropic"
|
||||||
"charm.land/fantasy/providers/google"
|
"charm.land/fantasy/providers/google"
|
||||||
|
|
@ -1010,7 +1011,6 @@ func languageModelToPrompt(prompt fantasy.Prompt, _, model string) ([]openaisdk.
|
||||||
}
|
}
|
||||||
messages = append(messages, tr)
|
messages = append(messages, tr)
|
||||||
case fantasy.ToolResultContentTypeError:
|
case fantasy.ToolResultContentTypeError:
|
||||||
// TODO: check if better handling is needed
|
|
||||||
output, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](toolResultPart.Output)
|
output, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](toolResultPart.Output)
|
||||||
if !ok {
|
if !ok {
|
||||||
warnings = append(warnings, fantasy.CallWarning{
|
warnings = append(warnings, fantasy.CallWarning{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue