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