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}