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}