Repository navigation
Expand file tree
/
Copy pathclient.go
More file actions
228 lines (207 loc) · 6.71 KB
/
Copy pathclient.go
File metadata and controls
228 lines (207 loc) · 6.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
// Package provider composes LLM provider clients for Anthropic, OpenAI,
// and OpenAI-compatible APIs with streaming, retry, and provider detection.
package provider
import (
"context"
"os"
"strings"
"sync"
"time"
"github.com/GrayCodeAI/relay/internal/buildversion"
"github.com/GrayCodeAI/relay/provider/core"
"github.com/GrayCodeAI/relay/catalog"
"github.com/GrayCodeAI/relay/provider/adapters"
"github.com/GrayCodeAI/relay/provider/resilience"
)
// Version mirrors core.Version; the canonical value lives in provider/core so
// feature packages can build User-Agent strings.
// Default is generated from the canonical VERSION file.
var Version = buildversion.Version
// SetVersion wires the canonical version from the VERSION file into the
// provider runtime and provider/core.
func SetVersion(v string) {
Version = v
core.SetVersion(v)
}
// RelayClient is the universal LLM provider.
// It is safe for concurrent use.
type RelayClient struct {
mu sync.RWMutex
defaultProvider string
apiKeys map[string]string
baseURLs map[string]string
customProviders map[string]adapters.ProviderRegistryConfig
providers map[string]core.Provider // cached provider clients
coalescer *resilience.Coalescer // optional request coalescing
}
// Client creates an RelayClient.
func Client(cfg *core.RelayConfig, opts ...core.ClientOption) *RelayClient {
c := &RelayClient{
defaultProvider: DetectProvider(),
apiKeys: make(map[string]string),
baseURLs: make(map[string]string),
customProviders: make(map[string]adapters.ProviderRegistryConfig),
providers: make(map[string]core.Provider),
}
if cfg != nil {
if cfg.Provider != "" {
c.defaultProvider = cfg.Provider
}
if cfg.APIKey != "" {
c.apiKeys[c.defaultProvider] = cfg.APIKey
}
if cfg.BaseURL != "" {
c.baseURLs[c.defaultProvider] = cfg.BaseURL
}
}
// Apply options (including coalescing)
for _, opt := range opts {
opt.ApplyRelay(c)
}
return c
}
// SetCoalescingTTL enables request coalescing with the given reuse TTL.
// Implements core.RelayConfigurable for WithCoalescing.
func (c *RelayClient) SetCoalescingTTL(ttl time.Duration) {
c.coalescer = resilience.NewCoalescer(ttl)
}
// SetAPIKey sets an API key for a provider.
func (c *RelayClient) SetAPIKey(provider, apiKey string) {
c.mu.Lock()
defer c.mu.Unlock()
c.apiKeys[provider] = apiKey
delete(c.providers, provider) // invalidate cached client
}
// Ping checks connectivity to the specified (or default) provider.
func (c *RelayClient) Ping(ctx context.Context, provider string) error {
if provider == "" {
provider = c.defaultProvider
}
p, err := c.getOrCreateProvider(provider)
if err != nil {
return err
}
return p.Ping(ctx)
}
// AnthropicClientConfig holds config for creating an Anthropic provider.
type AnthropicClientConfig struct {
APIKey string `json:"-"`
DefaultHeaders map[string]string `json:"default_headers,omitempty"`
Timeout int `json:"timeout,omitempty"`
MaxRetries int `json:"max_retries,omitempty"`
Provider string `json:"provider,omitempty"`
BaseURL string `json:"base_url,omitempty"`
}
// ParseCustomHeaders parses RELAY_CUSTOM_HEADERS env var into a map.
func ParseCustomHeaders() map[string]string {
result := make(map[string]string)
raw := os.Getenv("RELAY_CUSTOM_HEADERS")
if raw == "" {
return result
}
for _, line := range strings.Split(raw, "\n") {
line = strings.TrimSpace(line)
if idx := strings.Index(line, ":"); idx > 0 {
name := strings.TrimSpace(line[:idx])
value := strings.TrimSpace(line[idx+1:])
// Reject header names/values containing control characters to prevent injection.
if strings.IndexFunc(name, func(r rune) bool { return r < 0x20 }) >= 0 ||
strings.IndexFunc(value, func(r rune) bool { return r < 0x20 }) >= 0 {
continue
}
result[name] = value
}
}
return result
}
var (
cachedCatalog *catalog.CompiledCatalog
catalogLoadOnce sync.Once
)
// NewImageMessage creates a user message with an image from a URL or data URI.
// The url parameter accepts HTTP(S) URLs or data URIs (data:image/png;base64,...).
func NewImageMessage(url string) core.RelayMessage {
return core.RelayMessage{
Role: "user",
ContentParts: []core.ContentPart{
{Type: "image_url", ImageURL: &core.ImageURLPart{URL: url}},
},
}
}
// NewImageMessageWithText creates a user message with text and an image from a URL or data URI.
func NewImageMessageWithText(text, url string) core.RelayMessage {
return core.RelayMessage{
Role: "user",
ContentParts: []core.ContentPart{
{Type: "text", Text: text},
{Type: "image_url", ImageURL: &core.ImageURLPart{URL: url}},
},
}
}
// NewBase64ImageMessage creates a user message with a base64-encoded image.
// mediaType should be a MIME type like "image/png" or "image/jpeg".
func NewBase64ImageMessage(data, mediaType string) core.RelayMessage {
return core.RelayMessage{
Role: "user",
ContentParts: []core.ContentPart{
{Type: "image_url", ImageURL: &core.ImageURLPart{
URL: "data:" + mediaType + ";base64," + data,
}},
},
}
}
// NewBase64ImageMessageWithText creates a user message with text and a base64-encoded image.
func NewBase64ImageMessageWithText(text, data, mediaType string) core.RelayMessage {
return core.RelayMessage{
Role: "user",
ContentParts: []core.ContentPart{
{Type: "text", Text: text},
{Type: "image_url", ImageURL: &core.ImageURLPart{
URL: "data:" + mediaType + ";base64," + data,
}},
},
}
}
// NewAudioMessage creates a user message with base64-encoded audio.
// format should be "wav" or "mp3".
func NewAudioMessage(base64Data, format string) core.RelayMessage {
return core.RelayMessage{
Role: "user",
ContentParts: []core.ContentPart{
{Type: "input_audio", InputAudio: &core.InputAudioPart{
Data: base64Data,
Format: format,
}},
},
}
}
// NewAudioMessageWithText creates a user message with text and base64-encoded audio.
func NewAudioMessageWithText(text, base64Data, format string) core.RelayMessage {
return core.RelayMessage{
Role: "user",
ContentParts: []core.ContentPart{
{Type: "text", Text: text},
{Type: "input_audio", InputAudio: &core.InputAudioPart{
Data: base64Data,
Format: format,
}},
},
}
}
// ResolveDefaultModel resolves the default model for a provider from the catalog.
func ResolveDefaultModel(provider string) string {
catalogLoadOnce.Do(func() {
cat, err := catalog.LoadCatalog(context.Background(), catalog.LoadCatalogOptions{})
if err == nil {
cachedCatalog = cat
}
})
if cachedCatalog == nil {
return ""
}
models := catalog.ModelEntriesForProvider(cachedCatalog, provider)
if len(models) > 0 {
return models[0].ID
}
return ""
}