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			openai.WithSkipUserAgent(),
 35		},
 36		languageModelOptions: []openai.LanguageModelOption{
 37			openai.WithLanguageModelPrepareCallFunc(languagePrepareModelCall),
 38			openai.WithLanguageModelUsageFunc(languageModelUsage),
 39			openai.WithLanguageModelStreamUsageFunc(languageModelStreamUsage),
 40			openai.WithLanguageModelStreamExtraFunc(languageModelStreamExtra),
 41			openai.WithLanguageModelExtraContentFunc(languageModelExtraContent),
 42			openai.WithLanguageModelToPromptFunc(languageModelToPrompt),
 43		},
 44		objectMode: fantasy.ObjectModeTool, // Default to tool mode for openrouter
 45	}
 46	for _, o := range opts {
 47		o(&providerOptions)
 48	}
 49
 50	// Handle object mode: convert unsupported modes to tool
 51	// OpenRouter doesn't support native JSON mode, so we use tool or text
 52	objectMode := providerOptions.objectMode
 53	if objectMode == fantasy.ObjectModeAuto || objectMode == fantasy.ObjectModeJSON {
 54		objectMode = fantasy.ObjectModeTool
 55	}
 56
 57	providerOptions.openaiOptions = append(
 58		providerOptions.openaiOptions,
 59		openai.WithLanguageModelOptions(providerOptions.languageModelOptions...),
 60		openai.WithObjectMode(objectMode),
 61	)
 62	return openai.New(providerOptions.openaiOptions...)
 63}
 64
 65// WithAPIKey sets the API key for the OpenRouter provider.
 66func WithAPIKey(apiKey string) Option {
 67	return func(o *options) {
 68		o.openaiOptions = append(o.openaiOptions, openai.WithAPIKey(apiKey))
 69	}
 70}
 71
 72// WithName sets the name for the OpenRouter provider.
 73func WithName(name string) Option {
 74	return func(o *options) {
 75		o.openaiOptions = append(o.openaiOptions, openai.WithName(name))
 76	}
 77}
 78
 79// WithHeaders sets the headers for the OpenRouter provider.
 80func WithHeaders(headers map[string]string) Option {
 81	return func(o *options) {
 82		o.openaiOptions = append(o.openaiOptions, openai.WithHeaders(headers))
 83	}
 84}
 85
 86// WithHTTPClient sets the HTTP client for the OpenRouter provider.
 87func WithHTTPClient(client option.HTTPClient) Option {
 88	return func(o *options) {
 89		o.openaiOptions = append(o.openaiOptions, openai.WithHTTPClient(client))
 90	}
 91}
 92
 93// WithUserAgent sets an explicit User-Agent header, overriding the default and any
 94// value set via WithHeaders.
 95func WithUserAgent(ua string) Option {
 96	return func(o *options) {
 97		o.openaiOptions = append(o.openaiOptions, openai.WithUserAgent(ua))
 98	}
 99}
100
101// WithObjectMode sets the object generation mode for the OpenRouter provider.
102// Supported modes: ObjectModeTool, ObjectModeText.
103// ObjectModeAuto and ObjectModeJSON are automatically converted to ObjectModeTool
104// since OpenRouter doesn't support native JSON mode.
105func WithObjectMode(om fantasy.ObjectMode) Option {
106	return func(o *options) {
107		o.objectMode = om
108	}
109}
110
111func structToMapJSON(s any) (map[string]any, error) {
112	var result map[string]any
113	jsonBytes, err := json.Marshal(s)
114	if err != nil {
115		return nil, err
116	}
117	err = json.Unmarshal(jsonBytes, &result)
118	if err != nil {
119		return nil, err
120	}
121	return result, nil
122}