v0.3.93 0192a2e

extra_providers.go RawHistoryFolder

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// NewGondola: Gondola, a USDC-paid marketplace gateway for Venice AI
120// inference. OpenAI Chat Completions-compatible.
121func NewGondola(apiKey, baseURL string) Client {
122	return NewOpenAICompat("gondola", apiKey, baseURL, gondolaDefaultBaseURL)
123}
124
125// NewYoloAuto: Yolo-Auto, an OpenAI Chat Completions-compatible gateway
126// addressed by the `yolo` and `yolo-small` model aliases.
127func NewYoloAuto(apiKey, baseURL string) Client {
128	return NewOpenAICompat("yolo-auto", apiKey, baseURL, yoloAutoDefaultBaseURL)
129}
130
131// NewOpenCode is the opencode.ai Zen endpoint. Mixed APIs upstream; this
132// constructor wires the openai-completions flavor only. Models that need
133// the anthropic-messages flavor under the same provider should be built
134// with NewAnthropicCompat against the same base URL.
135func NewOpenCode(apiKey, baseURL string) Client {
136	return NewOpenAICompat("opencode", apiKey, baseURL, "https://opencode.ai/zen/v1")
137}
138
139// NewOpenCodeGo routes each model through the wire API exposed by OpenCode Go.
140func NewOpenCodeGo(apiKey, baseURL string) Client {
141	const defaultBaseURL = "https://opencode.ai/zen/go/v1"
142	return NewModelRouter("opencode-go",
143		NewOpenAICompat("opencode-go", apiKey, baseURL, defaultBaseURL),
144		map[string]Client{
145			APIResponses: NewOpenAIResponsesNamed(apiKey, firstNonEmptyString(baseURL, defaultBaseURL), "opencode-go"),
146		})
147}
148
149// NewMinimaxOpenAI is the OpenAI-completions flavor of MiniMax, in case
150// downstream models switch from anthropic-messages. The main MiniMax route
151// uses anthropic-messages; see NewMinimaxAnthropic below.
152func NewMinimaxOpenAI(apiKey, baseURL string) Client {
153	return NewOpenAICompat("minimax", apiKey, baseURL, "https://api.minimax.io/v1")
154}
155
156// NewMinimaxCNOpenAI is the CN-region MiniMax (openai-completions).
157func NewMinimaxCNOpenAI(apiKey, baseURL string) Client {
158	return NewOpenAICompat("minimax-cn", apiKey, baseURL, "https://api.minimaxi.com/v1")
159}
160
161// NewFireworksOpenAI is the OpenAI-completions flavor of Fireworks.
162// The main route uses the anthropic-messages variant on
163// api.fireworks.ai/inference; see NewFireworksAnthropic.
164func NewFireworksOpenAI(apiKey, baseURL string) Client {
165	return NewOpenAICompat("fireworks", apiKey, baseURL, "https://api.fireworks.ai/inference/v1")
166}
167
168// NewVercelGatewayOpenAI is Vercel AI Gateway's OpenAI-compat shim. The
169// main route uses anthropic-messages; see NewVercelGatewayAnthropic.
170func NewVercelGatewayOpenAI(apiKey, baseURL string) Client {
171	return NewOpenAICompat("vercel-ai-gateway", apiKey, baseURL, "https://ai-gateway.vercel.sh/v1")
172}
173
174// ----------------------------------------------------------------------
175// Anthropic Messages–compatible providers. These speak Anthropic's wire
176// format but live behind a third party's base URL. They reuse
177// anthropicClient with a custom name.
178// ----------------------------------------------------------------------
179
180// NewAnthropicCompat returns an anthropicClient pinned to a non-default
181// base URL and identifying as `name` for cost / logging purposes. Auth is
182// API key (x-api-key header). For OAuth-fronted compatibles (rare) use
183// NewAnthropicOAuth and rename via NameClient.
184func NewAnthropicCompat(name, apiKey, baseURL string) Client {
185	if baseURL == "" {
186		baseURL = anthropicDefaultBaseURL
187	}
188	return &anthropicClient{
189		apiKey:  apiKey,
190		baseURL: strings.TrimRight(baseURL, "/"),
191		name:    name,
192		http:    &http.Client{Timeout: 0},
193	}
194}
195
196// NewKimiCoding is the Kimi Code client: Kimi behind the Anthropic
197// Messages API at https://api.kimi.com/coding. Replaces the older
198// OpenAI-completions-on-/coding/v1 wiring.
199func NewKimiCoding(apiKey, baseURL string) Client {
200	return NewKimiCodingWithHeaders(apiKey, baseURL, nil)
201}
202
203// NewKimiCodingWithHeaders is the headered variant used by OAuth.
204func NewKimiCodingWithHeaders(apiKey, baseURL string, headers map[string]string) Client {
205	if baseURL == "" {
206		baseURL = "https://api.kimi.com/coding"
207	}
208	return &anthropicClient{
209		apiKey:  apiKey,
210		baseURL: strings.TrimRight(baseURL, "/"),
211		name:    "kimi",
212		headers: headers,
213		http:    &http.Client{Timeout: 0},
214	}
215}
216
217// NewMinimaxAnthropic is the anthropic-messages flavor on
218// api.minimax.io/anthropic, catalogued under provider=minimax.
219func NewMinimaxAnthropic(apiKey, baseURL string) Client {
220	return NewAnthropicCompat("minimax", apiKey, firstNonEmptyString(baseURL, "https://api.minimax.io/anthropic"))
221}
222
223// NewMinimaxCNAnthropic is the CN-region MiniMax (anthropic-messages).
224func NewMinimaxCNAnthropic(apiKey, baseURL string) Client {
225	return NewAnthropicCompat("minimax-cn", apiKey, firstNonEmptyString(baseURL, "https://api.minimaxi.com/anthropic"))
226}
227
228// NewFireworksAnthropic is the main Fireworks route. The
229// anthropic-messages-compatible endpoint at api.fireworks.ai/inference
230// expects Anthropic-style request bodies; use this rather than the
231// OpenAI flavor.
232func NewFireworksAnthropic(apiKey, baseURL string) Client {
233	return NewAnthropicCompat("fireworks", apiKey, firstNonEmptyString(baseURL, "https://api.fireworks.ai/inference"))
234}
235
236// NewVercelGatewayAnthropic — Vercel AI Gateway anthropic-messages route.
237func NewVercelGatewayAnthropic(apiKey, baseURL string) Client {
238	return NewAnthropicCompat("vercel-ai-gateway", apiKey, firstNonEmptyString(baseURL, "https://ai-gateway.vercel.sh"))
239}
240
241// ----------------------------------------------------------------------
242// Stubbed providers — bigger protocol surface, follow-up work.
243// ----------------------------------------------------------------------
244
245type unimplementedClient struct {
246	name string
247	hint string
248}
249
250func (c *unimplementedClient) Name() string { return c.name }
251
252func (c *unimplementedClient) Stream(ctx context.Context, req Request) (<-chan Event, error) {
253	return nil, fmt.Errorf("provider %q not yet implemented: %s", c.name, c.hint)
254}
255
256// NewBedrock returns an AWS Bedrock client. See amazon_bedrock.go for the
257// hand-rolled Converse-Stream wire-format parser. Auth is via
258// AWS_BEARER_TOKEN_BEDROCK (the modern Bedrock API key flow); SigV4
259// signing for IAM access-key + secret credentials is not yet wired.
260func NewBedrock(apiKey, baseURL string) Client {
261	return NewBedrockClient(apiKey, baseURL)
262}
263
264// NewGoogleVertex returns a Vertex AI client. See google_vertex.go for
265// the full auth + URL-rewrite implementation. Requires GOOGLE_CLOUD_PROJECT,
266// GOOGLE_CLOUD_LOCATION, and either GOOGLE_CLOUD_API_KEY or a service-
267// account JSON file pointed to by GOOGLE_APPLICATION_CREDENTIALS (or the
268// default ADC location ~/.config/gcloud/application_default_credentials.json).
269func NewGoogleVertex(apiKey, baseURL string) Client {
270	return NewVertex(apiKey, baseURL)
271}
272
273// NewMistral returns a Mistral client using their OpenAI-compatible Chat
274// Completions endpoint at https://api.mistral.ai/v1. Mistral also offers a
275// bespoke "Conversations" API, but the OpenAI-compat endpoint supports
276// the same models with tool calling and streaming, so we use that for
277// simplicity (no extra wire format to maintain).
278func NewMistral(apiKey, baseURL string) Client {
279	return NewOpenAICompat("mistral", apiKey, baseURL, "https://api.mistral.ai/v1")
280}
281
282// Cloudflare endpoints carry `{CLOUDFLARE_ACCOUNT_ID}` and (for the AI
283// Gateway) `{CLOUDFLARE_GATEWAY_ID}` placeholders that we substitute
284// from env vars at client-construction time. Workers AI uses standard
285// `Authorization: Bearer <key>`; AI Gateway uses `cf-aig-authorization`
286// (the upstream Authorization header is passed through to whichever
287// downstream provider the gateway forwards to).
288
289func resolveCloudflareURL(template string) (string, error) {
290	out := template
291	for _, key := range []string{"CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_GATEWAY_ID"} {
292		placeholder := "{" + key + "}"
293		if !strings.Contains(out, placeholder) {
294			continue
295		}
296		v := os.Getenv(key)
297		if v == "" {
298			return "", fmt.Errorf("%s is required but not set in env", key)
299		}
300		out = strings.ReplaceAll(out, placeholder, v)
301	}
302	return out, nil
303}
304
305// NewCloudflareWorkersAI returns an OpenAI-compatible client pinned to
306// the Workers AI base URL with {CLOUDFLARE_ACCOUNT_ID} substituted.
307// Returns an erroring client (deferred error on first Stream call) if
308// the env var is missing, so the constructor signature stays the same.
309func NewCloudflareWorkersAI(apiKey, baseURL string) Client {
310	if baseURL == "" {
311		baseURL = "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1"
312	}
313	resolved, err := resolveCloudflareURL(baseURL)
314	if err != nil {
315		return &unimplementedClient{name: "cloudflare-workers-ai", hint: err.Error()}
316	}
317	return NewOpenAICompat("cloudflare-workers-ai", apiKey, resolved, "")
318}
319
320// NewCloudflareAIGateway returns the AI Gateway client (OpenAI compat
321// route). Sends `cf-aig-authorization` instead of `Authorization` so
322// the gateway authenticates the caller (downstream-provider auth is
323// configured per-gateway in the Cloudflare dashboard).
324func NewCloudflareAIGateway(apiKey, baseURL string) Client {
325	if baseURL == "" {
326		baseURL = "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat"
327	}
328	resolved, err := resolveCloudflareURL(baseURL)
329	if err != nil {
330		return &unimplementedClient{name: "cloudflare-ai-gateway", hint: err.Error()}
331	}
332	return &openaiClient{
333		apiKey:  apiKey,
334		baseURL: strings.TrimRight(resolved, "/"),
335		name:    "cloudflare-ai-gateway",
336		headers: map[string]string{"cf-aig-authorization": "Bearer " + apiKey},
337		http:    &http.Client{Timeout: 0},
338	}
339}
340
341// NewGithubCopilot returns a GitHub Copilot client. The provided
342// credential must be a GitHub Personal Access Token (PAT) with Copilot
343// access enabled; zot trades it for a short-lived Copilot token on
344// every inference request (cached in memory until ~5min before expiry).
345//
346// Wire formats: Anthropic Messages, OpenAI Responses, and Chat Completions,
347// selected by model family. Copilot-specific headers
348// (X-Initiator, Openai-Intent, Editor-Version, Editor-Plugin-Version,
349// Copilot-Integration-Id, User-Agent) are added by the refresh
350// transport. The model id passes through unchanged.
351//
352// baseURL is ignored: the canonical host is read from `proxy-ep=...` in
353// the short-lived token's value.
354func NewGithubCopilot(apiKey, _ string) Client {
355	if apiKey == "" {
356		return &unimplementedClient{name: "github-copilot", hint: "set COPILOT_GITHUB_TOKEN"}
357	}
358	return NewGithubCopilotClient(apiKey)
359}
360
361// ----------------------------------------------------------------------
362// helpers
363// ----------------------------------------------------------------------
364
365func firstNonEmptyString(vals ...string) string {
366	for _, v := range vals {
367		if v != "" {
368			return v
369		}
370	}
371	return ""
372}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close