openrouter.go

  1// Package openrouter provides an implementation of the fantasy AI SDK for OpenRouter's language models.
  2package openrouter
  3
  4import (
  5	"encoding/json"
  6
  7	"charm.land/fantasy"
  8	"charm.land/fantasy/providers/openai"
  9	"github.com/openai/openai-go/v2/option"
 10)
 11
 12type options struct {
 13	openaiOptions        []openai.Option
 14	languageModelOptions []openai.LanguageModelOption
 15	objectMode           fantasy.ObjectMode
 16}
 17
 18const (
 19	// DefaultURL is the default URL for the OpenRouter API.
 20	DefaultURL = "https://openrouter.ai/api/v1"
 21	// Name is the name of the OpenRouter provider.
 22	Name = "openrouter"
 23)
 24
 25// Option defines a function that configures OpenRouter provider options.
 26type Option = func(*options)
 27
 28// New creates a new OpenRouter provider with the given options.
 29func New(opts ...Option) (fantasy.Provider, error) {
 30	providerOptions := options{
 31		openaiOptions: []openai.Option{
 32			openai.WithName(Name),
 33			openai.WithBaseURL(DefaultURL),
 34		},
 35		languageModelOptions: []openai.LanguageModelOption{
 36			openai.WithLanguageModelPrepareCallFunc(languagePrepareModelCall),
 37			openai.WithLanguageModelUsageFunc(languageModelUsage),
 38			openai.WithLanguageModelStreamUsageFunc(languageModelStreamUsage),
 39			openai.WithLanguageModelStreamExtraFunc(languageModelStreamExtra),
 40			openai.WithLanguageModelExtraContentFunc(languageModelExtraContent),
 41			openai.WithLanguageModelToPromptFunc(languageModelToPrompt),
 42		},
 43		objectMode: fantasy.ObjectModeTool, // Default to tool mode for openrouter
 44	}
 45	for _, o := range opts {
 46		o(&providerOptions)
 47	}
 48
 49	// Handle object mode: convert unsupported modes to tool
 50	// OpenRouter doesn't support native JSON mode, so we use tool or text
 51	objectMode := providerOptions.objectMode
 52	if objectMode == fantasy.ObjectModeAuto || objectMode == fantasy.ObjectModeJSON {
 53		objectMode = fantasy.ObjectModeTool
 54	}
 55
 56	providerOptions.openaiOptions = append(
 57		providerOptions.openaiOptions,
 58		openai.WithLanguageModelOptions(providerOptions.languageModelOptions...),
 59		openai.WithObjectMode(objectMode),
 60	)
 61	return openai.New(providerOptions.openaiOptions...)
 62}
 63
 64// WithAPIKey sets the API key for the OpenRouter provider.
 65func WithAPIKey(apiKey string) Option {
 66	return func(o *options) {
 67		o.openaiOptions = append(o.openaiOptions, openai.WithAPIKey(apiKey))
 68	}
 69}
 70
 71// WithName sets the name for the OpenRouter provider.
 72func WithName(name string) Option {
 73	return func(o *options) {
 74		o.openaiOptions = append(o.openaiOptions, openai.WithName(name))
 75	}
 76}
 77
 78// WithHeaders sets the headers for the OpenRouter provider.
 79func WithHeaders(headers map[string]string) Option {
 80	return func(o *options) {
 81		o.openaiOptions = append(o.openaiOptions, openai.WithHeaders(headers))
 82	}
 83}
 84
 85// WithHTTPClient sets the HTTP client for the OpenRouter provider.
 86func WithHTTPClient(client option.HTTPClient) Option {
 87	return func(o *options) {
 88		o.openaiOptions = append(o.openaiOptions, openai.WithHTTPClient(client))
 89	}
 90}
 91
 92// WithObjectMode sets the object generation mode for the OpenRouter provider.
 93// Supported modes: ObjectModeTool, ObjectModeText.
 94// ObjectModeAuto and ObjectModeJSON are automatically converted to ObjectModeTool
 95// since OpenRouter doesn't support native JSON mode.
 96func WithObjectMode(om fantasy.ObjectMode) Option {
 97	return func(o *options) {
 98		o.objectMode = om
 99	}
100}
101
102func structToMapJSON(s any) (map[string]any, error) {
103	var result map[string]any
104	jsonBytes, err := json.Marshal(s)
105	if err != nil {
106		return nil, err
107	}
108	err = json.Unmarshal(jsonBytes, &result)
109	if err != nil {
110		return nil, err
111	}
112	return result, nil
113}