1package provider
2
3// Extra third-party providers.
4//
5// Most are OpenAI Chat Completions–compatible, so they reuse `openaiClient`
6// with a different name + base URL. A handful speak the Anthropic Messages
7// API and reuse `anthropicClient` via NewAnthropicCompat below.
8//
9// Providers with a non-trivial protocol (Bedrock Converse, Vertex SSE, Azure
10// Responses, Mistral Conversations) are stubbed so the host wiring compiles.
11// Filling those in is its own work — Bedrock needs SigV4 + Converse stream
12// parsing, Vertex needs ADC, Azure needs the Responses API shape, Mistral
13// needs its bespoke Conversations API.
14
15import (
16 "context"
17 "fmt"
18 "net/http"
19 "os"
20 "strings"
21)
22
23// ----------------------------------------------------------------------
24// OpenAI Chat Completions–compatible providers. All of these just route
25// Chat Completions to a different base URL with a different name. The
26// default Authorization: Bearer header from openaiClient is correct for
27// every one of them.
28// ----------------------------------------------------------------------
29
30// newOpenAICompat is the shared constructor for OpenAI-completions clones.
31func newOpenAICompat(name, apiKey, baseURL, fallbackBaseURL string) Client {
32 if baseURL == "" {
33 baseURL = fallbackBaseURL
34 }
35 return &openaiClient{
36 apiKey: apiKey,
37 baseURL: strings.TrimRight(baseURL, "/"),
38 name: name,
39 http: &http.Client{Timeout: 0},
40 }
41}
42
43// NewMoonshot is the global Moonshot AI endpoint (Kimi-K2 family by id).
44// Provider id is `moonshotai`.
45func NewMoonshot(apiKey, baseURL string) Client {
46 return newOpenAICompat("moonshotai", apiKey, baseURL, "https://api.moonshot.ai/v1")
47}
48
49// NewMoonshotCN is the China-region Moonshot endpoint. Same model ids as
50// the global flavor, different base URL.
51func NewMoonshotCN(apiKey, baseURL string) Client {
52 return newOpenAICompat("moonshotai-cn", apiKey, baseURL, "https://api.moonshot.cn/v1")
53}
54
55// NewCerebras: ultra-fast inference (Llama/Qwen/GPT-OSS/GLM).
56func NewCerebras(apiKey, baseURL string) Client {
57 return newOpenAICompat("cerebras", apiKey, baseURL, "https://api.cerebras.ai/v1")
58}
59
60// NewGroq: LPU inference (Llama/Kimi/Qwen/GPT-OSS).
61func NewGroq(apiKey, baseURL string) Client {
62 return newOpenAICompat("groq", apiKey, baseURL, "https://api.groq.com/openai/v1")
63}
64
65// NewXAI: xAI Grok.
66func NewXAI(apiKey, baseURL string) Client {
67 return newOpenAICompat("xai", apiKey, baseURL, "https://api.x.ai/v1")
68}
69
70// NewTogether: Together.ai aggregator.
71func NewTogether(apiKey, baseURL string) Client {
72 return newOpenAICompat("together", apiKey, baseURL, "https://api.together.ai/v1")
73}
74
75// NewHuggingFace: HF inference router.
76func NewHuggingFace(apiKey, baseURL string) Client {
77 return newOpenAICompat("huggingface", apiKey, baseURL, "https://router.huggingface.co/v1")
78}
79
80// NewZAI: Z.AI GLM family.
81func NewZAI(apiKey, baseURL string) Client {
82 return newOpenAICompat("zai", apiKey, baseURL, "https://api.z.ai/api/coding/paas/v4")
83}
84
85// NewXiaomi: Xiaomi MiMo family (default endpoint).
86func NewXiaomi(apiKey, baseURL string) Client {
87 return newOpenAICompat("xiaomi", apiKey, baseURL, "https://api.xiaomimimo.com/v1")
88}
89
90// NewXiaomiTokenPlan creates a regional Xiaomi token-plan client.
91// region must be "ams", "cn", or "sgp", matching the three
92// `xiaomi-token-plan-*` provider ids.
93func NewXiaomiTokenPlan(region, apiKey, baseURL string) Client {
94 var fallback string
95 var name string
96 switch region {
97 case "ams":
98 fallback = "https://token-plan-ams.xiaomimimo.com/v1"
99 name = "xiaomi-token-plan-ams"
100 case "cn":
101 fallback = "https://token-plan-cn.xiaomimimo.com/v1"
102 name = "xiaomi-token-plan-cn"
103 case "sgp":
104 fallback = "https://token-plan-sgp.xiaomimimo.com/v1"
105 name = "xiaomi-token-plan-sgp"
106 default:
107 panic(fmt.Sprintf("xiaomi token-plan: unknown region %q", region))
108 }
109 return newOpenAICompat(name, apiKey, baseURL, fallback)
110}
111
112// NewOpenRouter: OpenRouter aggregator. Unlocks dozens of upstream
113// models with one key.
114func NewOpenRouter(apiKey, baseURL string) Client {
115 return newOpenAICompat("openrouter", apiKey, baseURL, "https://openrouter.ai/api/v1")
116}
117
118// NewOpenCode is the opencode.ai Zen endpoint. Mixed APIs upstream; this
119// constructor wires the openai-completions flavor only. Models that need
120// the anthropic-messages flavor under the same provider should be built
121// with NewAnthropicCompat against the same base URL.
122func NewOpenCode(apiKey, baseURL string) Client {
123 return newOpenAICompat("opencode", apiKey, baseURL, "https://opencode.ai/zen/v1")
124}
125
126// NewOpenCodeGo is the opencode-go variant.
127func NewOpenCodeGo(apiKey, baseURL string) Client {
128 return newOpenAICompat("opencode-go", apiKey, baseURL, "https://opencode.ai/zen/go/v1")
129}
130
131// NewMinimaxOpenAI is the OpenAI-completions flavor of MiniMax, in case
132// downstream models switch from anthropic-messages. The main MiniMax route
133// uses anthropic-messages; see NewMinimaxAnthropic below.
134func NewMinimaxOpenAI(apiKey, baseURL string) Client {
135 return newOpenAICompat("minimax", apiKey, baseURL, "https://api.minimax.io/v1")
136}
137
138// NewMinimaxCNOpenAI is the CN-region MiniMax (openai-completions).
139func NewMinimaxCNOpenAI(apiKey, baseURL string) Client {
140 return newOpenAICompat("minimax-cn", apiKey, baseURL, "https://api.minimaxi.com/v1")
141}
142
143// NewFireworksOpenAI is the OpenAI-completions flavor of Fireworks.
144// The main route uses the anthropic-messages variant on
145// api.fireworks.ai/inference; see NewFireworksAnthropic.
146func NewFireworksOpenAI(apiKey, baseURL string) Client {
147 return newOpenAICompat("fireworks", apiKey, baseURL, "https://api.fireworks.ai/inference/v1")
148}
149
150// NewVercelGatewayOpenAI is Vercel AI Gateway's OpenAI-compat shim. The
151// main route uses anthropic-messages; see NewVercelGatewayAnthropic.
152func NewVercelGatewayOpenAI(apiKey, baseURL string) Client {
153 return newOpenAICompat("vercel-ai-gateway", apiKey, baseURL, "https://ai-gateway.vercel.sh/v1")
154}
155
156// ----------------------------------------------------------------------
157// Anthropic Messages–compatible providers. These speak Anthropic's wire
158// format but live behind a third party's base URL. They reuse
159// anthropicClient with a custom name.
160// ----------------------------------------------------------------------
161
162// NewAnthropicCompat returns an anthropicClient pinned to a non-default
163// base URL and identifying as `name` for cost / logging purposes. Auth is
164// API key (x-api-key header). For OAuth-fronted compatibles (rare) use
165// NewAnthropicOAuth and rename via NameClient.
166func NewAnthropicCompat(name, apiKey, baseURL string) Client {
167 if baseURL == "" {
168 baseURL = anthropicDefaultBaseURL
169 }
170 return &anthropicClient{
171 apiKey: apiKey,
172 baseURL: strings.TrimRight(baseURL, "/"),
173 name: name,
174 http: &http.Client{Timeout: 0},
175 }
176}
177
178// NewKimiCoding is the Kimi Code client: Kimi behind the Anthropic
179// Messages API at https://api.kimi.com/coding. Replaces the older
180// OpenAI-completions-on-/coding/v1 wiring.
181func NewKimiCoding(apiKey, baseURL string) Client {
182 return NewKimiCodingWithHeaders(apiKey, baseURL, nil)
183}
184
185// NewKimiCodingWithHeaders is the headered variant used by OAuth.
186func NewKimiCodingWithHeaders(apiKey, baseURL string, headers map[string]string) Client {
187 if baseURL == "" {
188 baseURL = "https://api.kimi.com/coding"
189 }
190 return &anthropicClient{
191 apiKey: apiKey,
192 baseURL: strings.TrimRight(baseURL, "/"),
193 name: "kimi",
194 headers: headers,
195 http: &http.Client{Timeout: 0},
196 }
197}
198
199// NewMinimaxAnthropic is the anthropic-messages flavor on
200// api.minimax.io/anthropic, catalogued under provider=minimax.
201func NewMinimaxAnthropic(apiKey, baseURL string) Client {
202 return NewAnthropicCompat("minimax", apiKey, firstNonEmptyString(baseURL, "https://api.minimax.io/anthropic"))
203}
204
205// NewMinimaxCNAnthropic is the CN-region MiniMax (anthropic-messages).
206func NewMinimaxCNAnthropic(apiKey, baseURL string) Client {
207 return NewAnthropicCompat("minimax-cn", apiKey, firstNonEmptyString(baseURL, "https://api.minimaxi.com/anthropic"))
208}
209
210// NewFireworksAnthropic is the main Fireworks route. The
211// anthropic-messages-compatible endpoint at api.fireworks.ai/inference
212// expects Anthropic-style request bodies; use this rather than the
213// OpenAI flavor.
214func NewFireworksAnthropic(apiKey, baseURL string) Client {
215 return NewAnthropicCompat("fireworks", apiKey, firstNonEmptyString(baseURL, "https://api.fireworks.ai/inference"))
216}
217
218// NewVercelGatewayAnthropic — Vercel AI Gateway anthropic-messages route.
219func NewVercelGatewayAnthropic(apiKey, baseURL string) Client {
220 return NewAnthropicCompat("vercel-ai-gateway", apiKey, firstNonEmptyString(baseURL, "https://ai-gateway.vercel.sh"))
221}
222
223// ----------------------------------------------------------------------
224// Stubbed providers — bigger protocol surface, follow-up work.
225// ----------------------------------------------------------------------
226
227type unimplementedClient struct {
228 name string
229 hint string
230}
231
232func (c *unimplementedClient) Name() string { return c.name }
233
234func (c *unimplementedClient) Stream(ctx context.Context, req Request) (<-chan Event, error) {
235 return nil, fmt.Errorf("provider %q not yet implemented: %s", c.name, c.hint)
236}
237
238// NewBedrock returns an AWS Bedrock client. See amazon_bedrock.go for the
239// hand-rolled Converse-Stream wire-format parser. Auth is via
240// AWS_BEARER_TOKEN_BEDROCK (the modern Bedrock API key flow); SigV4
241// signing for IAM access-key + secret credentials is not yet wired.
242func NewBedrock(apiKey, baseURL string) Client {
243 return NewBedrockClient(apiKey, baseURL)
244}
245
246// NewGoogleVertex returns a Vertex AI client. See google_vertex.go for
247// the full auth + URL-rewrite implementation. Requires GOOGLE_CLOUD_PROJECT,
248// GOOGLE_CLOUD_LOCATION, and either GOOGLE_CLOUD_API_KEY or a service-
249// account JSON file pointed to by GOOGLE_APPLICATION_CREDENTIALS (or the
250// default ADC location ~/.config/gcloud/application_default_credentials.json).
251func NewGoogleVertex(apiKey, baseURL string) Client {
252 return NewVertex(apiKey, baseURL)
253}
254
255// NewAzureOpenAIResponses delegates to the real Azure OpenAI client.
256// Despite the provider id mentioning "responses", zot uses Azure's
257// Chat Completions endpoint (older but functionally complete for our
258// agent loop) to avoid duplicating the full openai-responses wire
259// client. Models register under provider id `azure-openai-responses`
260// so user catalogs keep working unchanged.
261func NewAzureOpenAIResponses(apiKey, baseURL string) Client {
262 return NewAzureOpenAI(apiKey, baseURL)
263}
264
265// NewMistral returns a Mistral client using their OpenAI-compatible Chat
266// Completions endpoint at https://api.mistral.ai/v1. Mistral also offers a
267// bespoke "Conversations" API, but the OpenAI-compat endpoint supports
268// the same models with tool calling and streaming, so we use that for
269// simplicity (no extra wire format to maintain).
270func NewMistral(apiKey, baseURL string) Client {
271 return newOpenAICompat("mistral", apiKey, baseURL, "https://api.mistral.ai/v1")
272}
273
274// Cloudflare endpoints carry `{CLOUDFLARE_ACCOUNT_ID}` and (for the AI
275// Gateway) `{CLOUDFLARE_GATEWAY_ID}` placeholders that we substitute
276// from env vars at client-construction time. Workers AI uses standard
277// `Authorization: Bearer <key>`; AI Gateway uses `cf-aig-authorization`
278// (the upstream Authorization header is passed through to whichever
279// downstream provider the gateway forwards to).
280
281func resolveCloudflareURL(template string) (string, error) {
282 out := template
283 for _, key := range []string{"CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_GATEWAY_ID"} {
284 placeholder := "{" + key + "}"
285 if !strings.Contains(out, placeholder) {
286 continue
287 }
288 v := os.Getenv(key)
289 if v == "" {
290 return "", fmt.Errorf("%s is required but not set in env", key)
291 }
292 out = strings.ReplaceAll(out, placeholder, v)
293 }
294 return out, nil
295}
296
297// NewCloudflareWorkersAI returns an OpenAI-compatible client pinned to
298// the Workers AI base URL with {CLOUDFLARE_ACCOUNT_ID} substituted.
299// Returns an erroring client (deferred error on first Stream call) if
300// the env var is missing, so the constructor signature stays the same.
301func NewCloudflareWorkersAI(apiKey, baseURL string) Client {
302 if baseURL == "" {
303 baseURL = "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1"
304 }
305 resolved, err := resolveCloudflareURL(baseURL)
306 if err != nil {
307 return &unimplementedClient{name: "cloudflare-workers-ai", hint: err.Error()}
308 }
309 return newOpenAICompat("cloudflare-workers-ai", apiKey, resolved, "")
310}
311
312// NewCloudflareAIGateway returns the AI Gateway client (OpenAI compat
313// route). Sends `cf-aig-authorization` instead of `Authorization` so
314// the gateway authenticates the caller (downstream-provider auth is
315// configured per-gateway in the Cloudflare dashboard).
316func NewCloudflareAIGateway(apiKey, baseURL string) Client {
317 if baseURL == "" {
318 baseURL = "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat"
319 }
320 resolved, err := resolveCloudflareURL(baseURL)
321 if err != nil {
322 return &unimplementedClient{name: "cloudflare-ai-gateway", hint: err.Error()}
323 }
324 return &openaiClient{
325 apiKey: apiKey,
326 baseURL: strings.TrimRight(resolved, "/"),
327 name: "cloudflare-ai-gateway",
328 headers: map[string]string{"cf-aig-authorization": "Bearer " + apiKey},
329 http: &http.Client{Timeout: 0},
330 }
331}
332
333// NewGithubCopilot returns a GitHub Copilot client. The provided
334// credential must be a GitHub Personal Access Token (PAT) with Copilot
335// access enabled; zot trades it for a short-lived Copilot token on
336// every inference request (cached in memory until ~5min before expiry).
337//
338// Wire format: OpenAI Chat Completions. Copilot-specific headers
339// (X-Initiator, Openai-Intent, Editor-Version, Editor-Plugin-Version,
340// Copilot-Integration-Id, User-Agent) are added by the refresh
341// transport. The model id passes through unchanged.
342//
343// baseURL is ignored: the canonical host is read from `proxy-ep=...` in
344// the short-lived token's value.
345func NewGithubCopilot(apiKey, _ string) Client {
346 if apiKey == "" {
347 return &unimplementedClient{name: "github-copilot", hint: "set COPILOT_GITHUB_TOKEN"}
348 }
349 return NewGithubCopilotClient(apiKey)
350}
351
352// ----------------------------------------------------------------------
353// helpers
354// ----------------------------------------------------------------------
355
356func firstNonEmptyString(vals ...string) string {
357 for _, v := range vals {
358 if v != "" {
359 return v
360 }
361 }
362 return ""
363}