detached 52c93f8

feat: add Yolo-Auto provider

commit
52c93f81c6c1fe4defdf37f71cdf2c65e4426413
author
Harry Giunta · · 2026-09-23 14:41 UTC
parent
e967eb8
signature
unsigned
13 files · +234 -3
M README.md +1 -1
@@ -18,7 +18,7 @@
1818 Yet another coding agent harness, lightweight and written (vibe-slopped) in go.
1919
2020 - one static binary.
21-- built-in providers for Anthropic, OpenAI/Codex/Responses, Kimi, DeepSeek, Google Gemini/Vertex, GitHub Copilot, Bedrock, Azure OpenAI, OpenRouter, Groq, Cerebras, xAI, Together, Hugging Face, Mistral, Moonshot, Z.AI, Xiaomi, MiniMax, Fireworks, Vercel AI Gateway, OpenCode, Cloudflare AI, and Ollama/local models.
21+- built-in providers for Anthropic, OpenAI/Codex/Responses, Kimi, DeepSeek, Google Gemini/Vertex, GitHub Copilot, Bedrock, Azure OpenAI, OpenRouter, Yolo-Auto, Groq, Cerebras, xAI, Together, Hugging Face, Mistral, Moonshot, Z.AI, Xiaomi, MiniMax, Fireworks, Vercel AI Gateway, OpenCode, Cloudflare AI, and Ollama/local models.
2222 - built-in tools (read, write, edit, bash, glob).
2323 - three run modes (interactive tui, print, json).
2424 - built-in telegram bot.
@@ -18,7 +18,7 @@
18 Yet another coding agent harness, lightweight and written (vibe-slopped) in go.18 Yet another coding agent harness, lightweight and written (vibe-slopped) in go.
19 19
20 - one static binary.20 - one static binary.
21-- built-in providers for Anthropic, OpenAI/Codex/Responses, Kimi, DeepSeek, Google Gemini/Vertex, GitHub Copilot, Bedrock, Azure OpenAI, OpenRouter, Groq, Cerebras, xAI, Together, Hugging Face, Mistral, Moonshot, Z.AI, Xiaomi, MiniMax, Fireworks, Vercel AI Gateway, OpenCode, Cloudflare AI, and Ollama/local models.21+- built-in providers for Anthropic, OpenAI/Codex/Responses, Kimi, DeepSeek, Google Gemini/Vertex, GitHub Copilot, Bedrock, Azure OpenAI, OpenRouter, Yolo-Auto, Groq, Cerebras, xAI, Together, Hugging Face, Mistral, Moonshot, Z.AI, Xiaomi, MiniMax, Fireworks, Vercel AI Gateway, OpenCode, Cloudflare AI, and Ollama/local models.
22 - built-in tools (read, write, edit, bash, glob).22 - built-in tools (read, write, edit, bash, glob).
23 - three run modes (interactive tui, print, json).23 - three run modes (interactive tui, print, json).
24 - built-in telegram bot.24 - built-in telegram bot.
M docs/providers.md +9 -0
@@ -181,6 +181,7 @@ show instructions and should be configured with environment variables.
181181 | Hugging Face | `HF_TOKEN` | `huggingface` |
182182 | OpenRouter | `OPENROUTER_API_KEY` | `openrouter` |
183183 | Gondola | `GONDOLA_API_KEY` | `gondola` |
184+| Yolo-Auto | `YOLO_AUTO_API_KEY` | `yolo-auto` |
184185 | Mistral | `MISTRAL_API_KEY` | `mistral` |
185186 | ZAI | `ZAI_API_KEY` | `zai` |
186187 | Xiaomi MiMo | `XIAOMI_API_KEY` | `xiaomi` |
@@ -201,6 +202,14 @@ show instructions and should be configured with environment variables.
201202 When Gondola credentials are available, zot refreshes its public text-model
202203 catalog in the background and adds the discovered models to `/model`.
203204
205+Yolo-Auto is an OpenAI-compatible gateway with `yolo` and `yolo-small` model
206+aliases and flat-rate plans, so no per-token prices are shown. Those two aliases
207+ship with the provider's documented conservative client limits. With
208+`YOLO_AUTO_API_KEY` set, zot lists `/v1/models` in the background and adds every
209+model the key can reach to `/model`, using the plan-bounded context window the
210+endpoint reports; `yolo` is used when no model is given. Create a key at
211+https://yolo-auto.com and see https://yolo-auto.com/docs for the API reference.
212+
204213 Example:
205214
206215 ```bash
@@ -181,6 +181,7 @@ show instructions and should be configured with environment variables.
181 | Hugging Face | `HF_TOKEN` | `huggingface` |181 | Hugging Face | `HF_TOKEN` | `huggingface` |
182 | OpenRouter | `OPENROUTER_API_KEY` | `openrouter` |182 | OpenRouter | `OPENROUTER_API_KEY` | `openrouter` |
183 | Gondola | `GONDOLA_API_KEY` | `gondola` |183 | Gondola | `GONDOLA_API_KEY` | `gondola` |
184+| Yolo-Auto | `YOLO_AUTO_API_KEY` | `yolo-auto` |
184 | Mistral | `MISTRAL_API_KEY` | `mistral` |185 | Mistral | `MISTRAL_API_KEY` | `mistral` |
185 | ZAI | `ZAI_API_KEY` | `zai` |186 | ZAI | `ZAI_API_KEY` | `zai` |
186 | Xiaomi MiMo | `XIAOMI_API_KEY` | `xiaomi` |187 | Xiaomi MiMo | `XIAOMI_API_KEY` | `xiaomi` |
@@ -201,6 +202,14 @@ show instructions and should be configured with environment variables.
201 When Gondola credentials are available, zot refreshes its public text-model202 When Gondola credentials are available, zot refreshes its public text-model
202 catalog in the background and adds the discovered models to `/model`.203 catalog in the background and adds the discovered models to `/model`.
203 204
205+Yolo-Auto is an OpenAI-compatible gateway with `yolo` and `yolo-small` model
206+aliases and flat-rate plans, so no per-token prices are shown. Those two aliases
207+ship with the provider's documented conservative client limits. With
208+`YOLO_AUTO_API_KEY` set, zot lists `/v1/models` in the background and adds every
209+model the key can reach to `/model`, using the plan-bounded context window the
210+endpoint reports; `yolo` is used when no model is given. Create a key at
211+https://yolo-auto.com and see https://yolo-auto.com/docs for the API reference.
212+
204 Example:213 Example:
205 214
206 ```bash215 ```bash
M packages/agent/build.go +7 -1
@@ -193,6 +193,8 @@ func defaultModelForProvider(prov string) string {
193193 return "anthropic/claude-sonnet-4.5"
194194 case "gondola":
195195 return "kimi-k3"
196+ case "yolo-auto":
197+ return "yolo"
196198 case "mistral":
197199 return "mistral-large-latest"
198200 case "zai":
@@ -234,7 +236,7 @@ func defaultModelForProvider(prov string) string {
234236 var knownProviders = []string{
235237 "anthropic", "openai", "openai-codex", "openai-responses", "kimi", "deepseek", "google", "ollama", provider.LlamaCPPProviderID, provider.LMStudioProviderID,
236238 "moonshotai", "moonshotai-cn",
237- "cerebras", "groq", "xai", "together", "huggingface", "openrouter", "gondola",
239+ "cerebras", "groq", "xai", "together", "huggingface", "openrouter", "gondola", "yolo-auto",
238240 "mistral", "zai",
239241 "xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "xiaomi-token-plan-sgp",
240242 "minimax", "minimax-cn",
@@ -900,6 +902,8 @@ func (r Resolved) NewClient() provider.Client {
900902 return wrap(provider.NewOpenRouter(r.Credential, r.BaseURL))
901903 case "gondola":
902904 return wrap(provider.NewGondola(r.Credential, r.BaseURL))
905+ case "yolo-auto":
906+ return wrap(provider.NewYoloAuto(r.Credential, r.BaseURL))
903907 case "zai":
904908 return wrap(provider.NewZAI(r.Credential, r.BaseURL))
905909 case "xiaomi":
@@ -1209,6 +1213,8 @@ func envVarName(provider string) string {
12091213 return "OPENROUTER"
12101214 case "gondola":
12111215 return "GONDOLA"
1216+ case "yolo-auto":
1217+ return "YOLO_AUTO"
12121218 case "mistral":
12131219 return "MISTRAL"
12141220 case "zai":
@@ -193,6 +193,8 @@ func defaultModelForProvider(prov string) string {
193 return "anthropic/claude-sonnet-4.5"193 return "anthropic/claude-sonnet-4.5"
194 case "gondola":194 case "gondola":
195 return "kimi-k3"195 return "kimi-k3"
196+ case "yolo-auto":
197+ return "yolo"
196 case "mistral":198 case "mistral":
197 return "mistral-large-latest"199 return "mistral-large-latest"
198 case "zai":200 case "zai":
@@ -234,7 +236,7 @@ func defaultModelForProvider(prov string) string {
234 var knownProviders = []string{236 var knownProviders = []string{
235 "anthropic", "openai", "openai-codex", "openai-responses", "kimi", "deepseek", "google", "ollama", provider.LlamaCPPProviderID, provider.LMStudioProviderID,237 "anthropic", "openai", "openai-codex", "openai-responses", "kimi", "deepseek", "google", "ollama", provider.LlamaCPPProviderID, provider.LMStudioProviderID,
236 "moonshotai", "moonshotai-cn",238 "moonshotai", "moonshotai-cn",
237- "cerebras", "groq", "xai", "together", "huggingface", "openrouter", "gondola",239+ "cerebras", "groq", "xai", "together", "huggingface", "openrouter", "gondola", "yolo-auto",
238 "mistral", "zai",240 "mistral", "zai",
239 "xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "xiaomi-token-plan-sgp",241 "xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "xiaomi-token-plan-sgp",
240 "minimax", "minimax-cn",242 "minimax", "minimax-cn",
@@ -900,6 +902,8 @@ func (r Resolved) NewClient() provider.Client {
900 return wrap(provider.NewOpenRouter(r.Credential, r.BaseURL))902 return wrap(provider.NewOpenRouter(r.Credential, r.BaseURL))
901 case "gondola":903 case "gondola":
902 return wrap(provider.NewGondola(r.Credential, r.BaseURL))904 return wrap(provider.NewGondola(r.Credential, r.BaseURL))
905+ case "yolo-auto":
906+ return wrap(provider.NewYoloAuto(r.Credential, r.BaseURL))
903 case "zai":907 case "zai":
904 return wrap(provider.NewZAI(r.Credential, r.BaseURL))908 return wrap(provider.NewZAI(r.Credential, r.BaseURL))
905 case "xiaomi":909 case "xiaomi":
@@ -1209,6 +1213,8 @@ func envVarName(provider string) string {
1209 return "OPENROUTER"1213 return "OPENROUTER"
1210 case "gondola":1214 case "gondola":
1211 return "GONDOLA"1215 return "GONDOLA"
1216+ case "yolo-auto":
1217+ return "YOLO_AUTO"
1212 case "mistral":1218 case "mistral":
1213 return "MISTRAL"1219 return "MISTRAL"
1214 case "zai":1220 case "zai":
M packages/agent/config.go +4 -0
@@ -466,6 +466,10 @@ func resolveCredentialFull(ctx context.Context, provider, explicit string, comma
466466 if v := os.Getenv("GONDOLA_API_KEY"); v != "" {
467467 return v, "apikey", "", nil
468468 }
469+ case "yolo-auto":
470+ if v := os.Getenv("YOLO_AUTO_API_KEY"); v != "" {
471+ return v, "apikey", "", nil
472+ }
469473 case "mistral":
470474 if v := os.Getenv("MISTRAL_API_KEY"); v != "" {
471475 return v, "apikey", "", nil
@@ -466,6 +466,10 @@ func resolveCredentialFull(ctx context.Context, provider, explicit string, comma
466 if v := os.Getenv("GONDOLA_API_KEY"); v != "" {466 if v := os.Getenv("GONDOLA_API_KEY"); v != "" {
467 return v, "apikey", "", nil467 return v, "apikey", "", nil
468 }468 }
469+ case "yolo-auto":
470+ if v := os.Getenv("YOLO_AUTO_API_KEY"); v != "" {
471+ return v, "apikey", "", nil
472+ }
469 case "mistral":473 case "mistral":
470 if v := os.Getenv("MISTRAL_API_KEY"); v != "" {474 if v := os.Getenv("MISTRAL_API_KEY"); v != "" {
471 return v, "apikey", "", nil475 return v, "apikey", "", nil
M packages/agent/modelsync.go +7 -0
@@ -261,6 +261,13 @@ func refreshModels() {
261261 all = append(all, live...)
262262 }
263263 }
264+ if cred, _, err := resolveCredentialForBackground(ctx, "yolo-auto"); err == nil {
265+ // Yolo-Auto's /v1/models answers per key. Gate discovery on a
266+ // credential so its models only fill the picker for key holders.
267+ if live, err := provider.DiscoverYoloAuto(ctx, cred, ""); err == nil {
268+ all = append(all, live...)
269+ }
270+ }
264271 }
265272
266273 // Presets are per-account and require auth. Fetch them even when the
@@ -261,6 +261,13 @@ func refreshModels() {
261 all = append(all, live...)261 all = append(all, live...)
262 }262 }
263 }263 }
264+ if cred, _, err := resolveCredentialForBackground(ctx, "yolo-auto"); err == nil {
265+ // Yolo-Auto's /v1/models answers per key. Gate discovery on a
266+ // credential so its models only fill the picker for key holders.
267+ if live, err := provider.DiscoverYoloAuto(ctx, cred, ""); err == nil {
268+ all = append(all, live...)
269+ }
270+ }
264 }271 }
265 272
266 // Presets are per-account and require auth. Fetch them even when the273 // Presets are per-account and require auth. Fetch them even when the
M packages/provider/auth/probe.go +6 -0
@@ -108,6 +108,12 @@ func probeAPIKey(ctx context.Context, provider, key string, c *http.Client) erro
108108 return err
109109 }
110110 req.Header.Set("authorization", "Bearer "+key)
111+ case "yolo-auto":
112+ req, err = http.NewRequestWithContext(ctx, "GET", "https://yolo-auto.com/v1/models", nil)
113+ if err != nil {
114+ return err
115+ }
116+ req.Header.Set("authorization", "Bearer "+key)
111117 case "huggingface":
112118 req, err = http.NewRequestWithContext(ctx, "GET", "https://router.huggingface.co/v1/models", nil)
113119 if err != nil {
@@ -108,6 +108,12 @@ func probeAPIKey(ctx context.Context, provider, key string, c *http.Client) erro
108 return err108 return err
109 }109 }
110 req.Header.Set("authorization", "Bearer "+key)110 req.Header.Set("authorization", "Bearer "+key)
111+ case "yolo-auto":
112+ req, err = http.NewRequestWithContext(ctx, "GET", "https://yolo-auto.com/v1/models", nil)
113+ if err != nil {
114+ return err
115+ }
116+ req.Header.Set("authorization", "Bearer "+key)
111 case "huggingface":117 case "huggingface":
112 req, err = http.NewRequestWithContext(ctx, "GET", "https://router.huggingface.co/v1/models", nil)118 req, err = http.NewRequestWithContext(ctx, "GET", "https://router.huggingface.co/v1/models", nil)
113 if err != nil {119 if err != nil {
M packages/provider/auth/server.go +1 -1
@@ -138,7 +138,7 @@ func APIKeyProviders() []string {
138138 return []string{
139139 "anthropic", "openai", "kimi", "deepseek", "google", "llama.cpp", "lmstudio",
140140 "moonshotai", "moonshotai-cn", "groq", "cerebras", "xai", "together",
141- "huggingface", "openrouter", "gondola", "mistral", "zai",
141+ "huggingface", "openrouter", "gondola", "yolo-auto", "mistral", "zai",
142142 "xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "xiaomi-token-plan-sgp",
143143 "minimax", "minimax-cn", "fireworks", "vercel-ai-gateway",
144144 "opencode", "opencode-go", "amazon-bedrock", "google-vertex", "azure-openai-responses",
@@ -138,7 +138,7 @@ func APIKeyProviders() []string {
138 return []string{138 return []string{
139 "anthropic", "openai", "kimi", "deepseek", "google", "llama.cpp", "lmstudio",139 "anthropic", "openai", "kimi", "deepseek", "google", "llama.cpp", "lmstudio",
140 "moonshotai", "moonshotai-cn", "groq", "cerebras", "xai", "together",140 "moonshotai", "moonshotai-cn", "groq", "cerebras", "xai", "together",
141- "huggingface", "openrouter", "gondola", "mistral", "zai",141+ "huggingface", "openrouter", "gondola", "yolo-auto", "mistral", "zai",
142 "xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "xiaomi-token-plan-sgp",142 "xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "xiaomi-token-plan-sgp",
143 "minimax", "minimax-cn", "fireworks", "vercel-ai-gateway",143 "minimax", "minimax-cn", "fireworks", "vercel-ai-gateway",
144 "opencode", "opencode-go", "amazon-bedrock", "google-vertex", "azure-openai-responses",144 "opencode", "opencode-go", "amazon-bedrock", "google-vertex", "azure-openai-responses",
M packages/provider/discover.go +72 -0
@@ -230,6 +230,14 @@ func looksLikeChatModel(id string) bool {
230230 const (
231231 openrouterDefaultBaseURL = "https://openrouter.ai/api/v1"
232232 gondolaDefaultBaseURL = "https://api.gondola-ai.com/v1"
233+ yoloAutoDefaultBaseURL = "https://yolo-auto.com/v1"
234+
235+ // Yolo-Auto's documented conservative client limits. The provider is
236+ // flat-rate with plan-bounded windows, so the window is a safe floor
237+ // rather than a ceiling; /v1/models reports the per-key window and
238+ // DiscoverYoloAuto prefers it.
239+ yoloAutoContextWindow = 131072
240+ yoloAutoMaxOutput = 32768
233241 )
234242
235243 // DiscoverOpenRouter lists models from OpenRouter's public /models
@@ -458,6 +466,70 @@ func DiscoverGondola(ctx context.Context, baseURL string) ([]Model, error) {
458466 return out, nil
459467 }
460468
469+// DiscoverYoloAuto lists the models a Yolo-Auto key can reach from the
470+// provider's OpenAI-compatible /v1/models endpoint. Each entry reports the
471+// plan-bounded context window and, when the alias supports them, the
472+// thinking levels it accepts.
473+func DiscoverYoloAuto(ctx context.Context, apiKey, baseURL string) ([]Model, error) {
474+ if baseURL == "" {
475+ baseURL = yoloAutoDefaultBaseURL
476+ }
477+ baseURL = strings.TrimRight(baseURL, "/")
478+ client := &http.Client{Timeout: 15 * time.Second}
479+ req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/models", nil)
480+ if err != nil {
481+ return nil, err
482+ }
483+ if apiKey != "" {
484+ req.Header.Set("authorization", "Bearer "+apiKey)
485+ }
486+ resp, err := client.Do(req)
487+ if err != nil {
488+ return nil, err
489+ }
490+ defer resp.Body.Close()
491+ body, _ := io.ReadAll(resp.Body)
492+ if resp.StatusCode != http.StatusOK {
493+ return nil, fmt.Errorf("yolo-auto discover http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
494+ }
495+ var page struct {
496+ Data []struct {
497+ ID string `json:"id"`
498+ ContextLength int `json:"context_length"`
499+ MaxModelLen int `json:"max_model_len"`
500+ Thinking []string `json:"thinking"`
501+ } `json:"data"`
502+ }
503+ if err := json.Unmarshal(body, &page); err != nil {
504+ return nil, fmt.Errorf("yolo-auto discover parse: %w", err)
505+ }
506+ out := make([]Model, 0, len(page.Data))
507+ for _, d := range page.Data {
508+ id := strings.TrimSpace(d.ID)
509+ if id == "" || strings.Contains(strings.ToLower(id), "embed") {
510+ continue
511+ }
512+ contextWindow := d.ContextLength
513+ if contextWindow == 0 {
514+ contextWindow = d.MaxModelLen
515+ }
516+ if contextWindow == 0 {
517+ contextWindow = yoloAutoContextWindow
518+ }
519+ out = append(out, Model{
520+ Provider: "yolo-auto",
521+ ID: id,
522+ DisplayName: id,
523+ ContextWindow: contextWindow,
524+ MaxOutput: yoloAutoMaxOutput,
525+ Reasoning: len(d.Thinking) > 0,
526+ BaseURL: baseURL,
527+ Source: "live",
528+ })
529+ }
530+ return out, nil
531+}
532+
461533 // perMillionTokens converts a per-token USD price string to USD per 1M
462534 // tokens. Empty or unparseable values become 0.
463535 func perMillionTokens(s string) float64 {
@@ -230,6 +230,14 @@ func looksLikeChatModel(id string) bool {
230 const (230 const (
231 openrouterDefaultBaseURL = "https://openrouter.ai/api/v1"231 openrouterDefaultBaseURL = "https://openrouter.ai/api/v1"
232 gondolaDefaultBaseURL = "https://api.gondola-ai.com/v1"232 gondolaDefaultBaseURL = "https://api.gondola-ai.com/v1"
233+ yoloAutoDefaultBaseURL = "https://yolo-auto.com/v1"
234+
235+ // Yolo-Auto's documented conservative client limits. The provider is
236+ // flat-rate with plan-bounded windows, so the window is a safe floor
237+ // rather than a ceiling; /v1/models reports the per-key window and
238+ // DiscoverYoloAuto prefers it.
239+ yoloAutoContextWindow = 131072
240+ yoloAutoMaxOutput = 32768
233 )241 )
234 242
235 // DiscoverOpenRouter lists models from OpenRouter's public /models243 // DiscoverOpenRouter lists models from OpenRouter's public /models
@@ -458,6 +466,70 @@ func DiscoverGondola(ctx context.Context, baseURL string) ([]Model, error) {
458 return out, nil466 return out, nil
459 }467 }
460 468
469+// DiscoverYoloAuto lists the models a Yolo-Auto key can reach from the
470+// provider's OpenAI-compatible /v1/models endpoint. Each entry reports the
471+// plan-bounded context window and, when the alias supports them, the
472+// thinking levels it accepts.
473+func DiscoverYoloAuto(ctx context.Context, apiKey, baseURL string) ([]Model, error) {
474+ if baseURL == "" {
475+ baseURL = yoloAutoDefaultBaseURL
476+ }
477+ baseURL = strings.TrimRight(baseURL, "/")
478+ client := &http.Client{Timeout: 15 * time.Second}
479+ req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/models", nil)
480+ if err != nil {
481+ return nil, err
482+ }
483+ if apiKey != "" {
484+ req.Header.Set("authorization", "Bearer "+apiKey)
485+ }
486+ resp, err := client.Do(req)
487+ if err != nil {
488+ return nil, err
489+ }
490+ defer resp.Body.Close()
491+ body, _ := io.ReadAll(resp.Body)
492+ if resp.StatusCode != http.StatusOK {
493+ return nil, fmt.Errorf("yolo-auto discover http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
494+ }
495+ var page struct {
496+ Data []struct {
497+ ID string `json:"id"`
498+ ContextLength int `json:"context_length"`
499+ MaxModelLen int `json:"max_model_len"`
500+ Thinking []string `json:"thinking"`
501+ } `json:"data"`
502+ }
503+ if err := json.Unmarshal(body, &page); err != nil {
504+ return nil, fmt.Errorf("yolo-auto discover parse: %w", err)
505+ }
506+ out := make([]Model, 0, len(page.Data))
507+ for _, d := range page.Data {
508+ id := strings.TrimSpace(d.ID)
509+ if id == "" || strings.Contains(strings.ToLower(id), "embed") {
510+ continue
511+ }
512+ contextWindow := d.ContextLength
513+ if contextWindow == 0 {
514+ contextWindow = d.MaxModelLen
515+ }
516+ if contextWindow == 0 {
517+ contextWindow = yoloAutoContextWindow
518+ }
519+ out = append(out, Model{
520+ Provider: "yolo-auto",
521+ ID: id,
522+ DisplayName: id,
523+ ContextWindow: contextWindow,
524+ MaxOutput: yoloAutoMaxOutput,
525+ Reasoning: len(d.Thinking) > 0,
526+ BaseURL: baseURL,
527+ Source: "live",
528+ })
529+ }
530+ return out, nil
531+}
532+
461 // perMillionTokens converts a per-token USD price string to USD per 1M533 // perMillionTokens converts a per-token USD price string to USD per 1M
462 // tokens. Empty or unparseable values become 0.534 // tokens. Empty or unparseable values become 0.
463 func perMillionTokens(s string) float64 {535 func perMillionTokens(s string) float64 {
A packages/provider/discover_yolo_auto_test.go +78 -0
@@ -0,0 +1,78 @@
1+package provider
2+
3+import (
4+ "context"
5+ "fmt"
6+ "net/http"
7+ "net/http/httptest"
8+ "testing"
9+)
10+
11+func TestDiscoverYoloAutoListsKeyModels(t *testing.T) {
12+ const body = `{"object":"list","data":[
13+ {"id":"yolo","object":"model","context_length":262144,"max_model_len":262144,
14+ "thinking":["minimal","low","medium","high","xhigh"]},
15+ {"id":"yolo-small","object":"model","context_length":262144,"max_model_len":262144,
16+ "thinking":null},
17+ {"id":"text-embedding-3-small","object":"model"}
18+ ]}`
19+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20+ if r.URL.Path != "/models" {
21+ t.Fatalf("request path = %q", r.URL.Path)
22+ }
23+ if got := r.Header.Get("authorization"); got != "Bearer yolo_test" {
24+ t.Fatalf("authorization = %q", got)
25+ }
26+ w.Header().Set("content-type", "application/json")
27+ fmt.Fprint(w, body)
28+ }))
29+ defer srv.Close()
30+
31+ models, err := DiscoverYoloAuto(context.Background(), "yolo_test", srv.URL+"/")
32+ if err != nil {
33+ t.Fatal(err)
34+ }
35+ if len(models) != 2 {
36+ t.Fatalf("models = %+v", models)
37+ }
38+
39+ yolo := models[0]
40+ if yolo.Provider != "yolo-auto" || yolo.ID != "yolo" || yolo.DisplayName != "yolo" ||
41+ yolo.ContextWindow != 262144 || yolo.MaxOutput != yoloAutoMaxOutput || !yolo.Reasoning ||
42+ yolo.BaseURL != srv.URL || yolo.Source != "live" {
43+ t.Fatalf("yolo model = %+v", yolo)
44+ }
45+
46+ small := models[1]
47+ if small.ID != "yolo-small" || small.ContextWindow != 262144 || small.Reasoning {
48+ t.Fatalf("second model = %+v", small)
49+ }
50+}
51+
52+func TestDiscoverYoloAutoFallsBackToDocumentedLimits(t *testing.T) {
53+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
54+ w.Header().Set("content-type", "application/json")
55+ fmt.Fprint(w, `{"data":[{"id":"yolo"}]}`)
56+ }))
57+ defer srv.Close()
58+
59+ models, err := DiscoverYoloAuto(context.Background(), "yolo_test", srv.URL)
60+ if err != nil {
61+ t.Fatal(err)
62+ }
63+ if len(models) != 1 || models[0].ContextWindow != yoloAutoContextWindow {
64+ t.Fatalf("models = %+v", models)
65+ }
66+}
67+
68+func TestDiscoverYoloAutoReportsHTTPError(t *testing.T) {
69+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
70+ http.Error(w, "unauthorized", http.StatusUnauthorized)
71+ }))
72+ defer srv.Close()
73+
74+ _, err := DiscoverYoloAuto(context.Background(), "yolo_test", srv.URL)
75+ if err == nil {
76+ t.Fatal("expected discovery error")
77+ }
78+}
@@ -0,0 +1,78 @@
1+package provider
2+
3+import (
4+ "context"
5+ "fmt"
6+ "net/http"
7+ "net/http/httptest"
8+ "testing"
9+)
10+
11+func TestDiscoverYoloAutoListsKeyModels(t *testing.T) {
12+ const body = `{"object":"list","data":[
13+ {"id":"yolo","object":"model","context_length":262144,"max_model_len":262144,
14+ "thinking":["minimal","low","medium","high","xhigh"]},
15+ {"id":"yolo-small","object":"model","context_length":262144,"max_model_len":262144,
16+ "thinking":null},
17+ {"id":"text-embedding-3-small","object":"model"}
18+ ]}`
19+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20+ if r.URL.Path != "/models" {
21+ t.Fatalf("request path = %q", r.URL.Path)
22+ }
23+ if got := r.Header.Get("authorization"); got != "Bearer yolo_test" {
24+ t.Fatalf("authorization = %q", got)
25+ }
26+ w.Header().Set("content-type", "application/json")
27+ fmt.Fprint(w, body)
28+ }))
29+ defer srv.Close()
30+
31+ models, err := DiscoverYoloAuto(context.Background(), "yolo_test", srv.URL+"/")
32+ if err != nil {
33+ t.Fatal(err)
34+ }
35+ if len(models) != 2 {
36+ t.Fatalf("models = %+v", models)
37+ }
38+
39+ yolo := models[0]
40+ if yolo.Provider != "yolo-auto" || yolo.ID != "yolo" || yolo.DisplayName != "yolo" ||
41+ yolo.ContextWindow != 262144 || yolo.MaxOutput != yoloAutoMaxOutput || !yolo.Reasoning ||
42+ yolo.BaseURL != srv.URL || yolo.Source != "live" {
43+ t.Fatalf("yolo model = %+v", yolo)
44+ }
45+
46+ small := models[1]
47+ if small.ID != "yolo-small" || small.ContextWindow != 262144 || small.Reasoning {
48+ t.Fatalf("second model = %+v", small)
49+ }
50+}
51+
52+func TestDiscoverYoloAutoFallsBackToDocumentedLimits(t *testing.T) {
53+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
54+ w.Header().Set("content-type", "application/json")
55+ fmt.Fprint(w, `{"data":[{"id":"yolo"}]}`)
56+ }))
57+ defer srv.Close()
58+
59+ models, err := DiscoverYoloAuto(context.Background(), "yolo_test", srv.URL)
60+ if err != nil {
61+ t.Fatal(err)
62+ }
63+ if len(models) != 1 || models[0].ContextWindow != yoloAutoContextWindow {
64+ t.Fatalf("models = %+v", models)
65+ }
66+}
67+
68+func TestDiscoverYoloAutoReportsHTTPError(t *testing.T) {
69+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
70+ http.Error(w, "unauthorized", http.StatusUnauthorized)
71+ }))
72+ defer srv.Close()
73+
74+ _, err := DiscoverYoloAuto(context.Background(), "yolo_test", srv.URL)
75+ if err == nil {
76+ t.Fatal("expected discovery error")
77+ }
78+}
M packages/provider/extra_providers.go +6 -0
@@ -122,6 +122,12 @@ func NewGondola(apiKey, baseURL string) Client {
122122 return NewOpenAICompat("gondola", apiKey, baseURL, gondolaDefaultBaseURL)
123123 }
124124
125+// NewYoloAuto: Yolo-Auto, an OpenAI Chat Completions-compatible gateway
126+// addressed by the `yolo` and `yolo-small` model aliases.
127+func NewYoloAuto(apiKey, baseURL string) Client {
128+ return NewOpenAICompat("yolo-auto", apiKey, baseURL, yoloAutoDefaultBaseURL)
129+}
130+
125131 // NewOpenCode is the opencode.ai Zen endpoint. Mixed APIs upstream; this
126132 // constructor wires the openai-completions flavor only. Models that need
127133 // the anthropic-messages flavor under the same provider should be built
@@ -122,6 +122,12 @@ func NewGondola(apiKey, baseURL string) Client {
122 return NewOpenAICompat("gondola", apiKey, baseURL, gondolaDefaultBaseURL)122 return NewOpenAICompat("gondola", apiKey, baseURL, gondolaDefaultBaseURL)
123 }123 }
124 124
125+// NewYoloAuto: Yolo-Auto, an OpenAI Chat Completions-compatible gateway
126+// addressed by the `yolo` and `yolo-small` model aliases.
127+func NewYoloAuto(apiKey, baseURL string) Client {
128+ return NewOpenAICompat("yolo-auto", apiKey, baseURL, yoloAutoDefaultBaseURL)
129+}
130+
125 // NewOpenCode is the opencode.ai Zen endpoint. Mixed APIs upstream; this131 // NewOpenCode is the opencode.ai Zen endpoint. Mixed APIs upstream; this
126 // constructor wires the openai-completions flavor only. Models that need132 // constructor wires the openai-completions flavor only. Models that need
127 // the anthropic-messages flavor under the same provider should be built133 // the anthropic-messages flavor under the same provider should be built
M packages/provider/labels.go +2 -0
@@ -41,6 +41,8 @@ func ProviderLabel(id string) string {
4141 return "OpenRouter"
4242 case "gondola":
4343 return "Gondola"
44+ case "yolo-auto":
45+ return "Yolo-Auto"
4446 case "mistral":
4547 return "Mistral"
4648 case "zai":
@@ -41,6 +41,8 @@ func ProviderLabel(id string) string {
41 return "OpenRouter"41 return "OpenRouter"
42 case "gondola":42 case "gondola":
43 return "Gondola"43 return "Gondola"
44+ case "yolo-auto":
45+ return "Yolo-Auto"
44 case "mistral":46 case "mistral":
45 return "Mistral"47 return "Mistral"
46 case "zai":48 case "zai":
M packages/provider/models.go +20 -0
@@ -272,6 +272,26 @@ var Catalog = []Model{
272272 BaseURL: gondolaDefaultBaseURL,
273273 },
274274
275+ // ---- Yolo-Auto ----
276+ // Seed entries only: `yolo` is the default returned by
277+ // defaultModelForProvider so it resolves offline. Yolo-Auto is an
278+ // OpenAI-compatible gateway on flat-rate plans with no per-token
279+ // billing, so no per-token prices are listed. The limits are the
280+ // provider's documented conservative client settings; the full catalog,
281+ // including each key's plan-bounded window, comes from /v1/models
282+ // (DiscoverYoloAuto).
283+ {
284+ Provider: "yolo-auto", ID: "yolo", DisplayName: "Yolo",
285+ ContextWindow: yoloAutoContextWindow, MaxOutput: yoloAutoMaxOutput,
286+ Reasoning: true,
287+ BaseURL: yoloAutoDefaultBaseURL,
288+ },
289+ {
290+ Provider: "yolo-auto", ID: "yolo-small", DisplayName: "Yolo Small",
291+ ContextWindow: yoloAutoContextWindow, MaxOutput: yoloAutoMaxOutput,
292+ BaseURL: yoloAutoDefaultBaseURL,
293+ },
294+
275295 // ---- Anthropic / newer families ----
276296 {
277297 Provider: "anthropic", ID: "claude-opus-4-5", DisplayName: "Claude Opus 4.5 (latest)",
@@ -272,6 +272,26 @@ var Catalog = []Model{
272 BaseURL: gondolaDefaultBaseURL,272 BaseURL: gondolaDefaultBaseURL,
273 },273 },
274 274
275+ // ---- Yolo-Auto ----
276+ // Seed entries only: `yolo` is the default returned by
277+ // defaultModelForProvider so it resolves offline. Yolo-Auto is an
278+ // OpenAI-compatible gateway on flat-rate plans with no per-token
279+ // billing, so no per-token prices are listed. The limits are the
280+ // provider's documented conservative client settings; the full catalog,
281+ // including each key's plan-bounded window, comes from /v1/models
282+ // (DiscoverYoloAuto).
283+ {
284+ Provider: "yolo-auto", ID: "yolo", DisplayName: "Yolo",
285+ ContextWindow: yoloAutoContextWindow, MaxOutput: yoloAutoMaxOutput,
286+ Reasoning: true,
287+ BaseURL: yoloAutoDefaultBaseURL,
288+ },
289+ {
290+ Provider: "yolo-auto", ID: "yolo-small", DisplayName: "Yolo Small",
291+ ContextWindow: yoloAutoContextWindow, MaxOutput: yoloAutoMaxOutput,
292+ BaseURL: yoloAutoDefaultBaseURL,
293+ },
294+
275 // ---- Anthropic / newer families ----295 // ---- Anthropic / newer families ----
276 {296 {
277 Provider: "anthropic", ID: "claude-opus-4-5", DisplayName: "Claude Opus 4.5 (latest)",297 Provider: "anthropic", ID: "claude-opus-4-5", DisplayName: "Claude Opus 4.5 (latest)",
M packages/provider/provider_test.go +21 -0
@@ -511,6 +511,27 @@ func TestGondolaSeedCatalog(t *testing.T) {
511511 }
512512 }
513513
514+func TestYoloAutoSeedCatalog(t *testing.T) {
515+ // `yolo` is the model returned when no --model is given, so it has to
516+ // resolve from the baked-in catalog without a live refresh.
517+ yolo, err := FindModel("yolo-auto", "yolo")
518+ if err != nil {
519+ t.Fatal(err)
520+ }
521+ if yolo.ContextWindow != yoloAutoContextWindow || yolo.MaxOutput != yoloAutoMaxOutput ||
522+ !yolo.Reasoning || yolo.BaseURL != yoloAutoDefaultBaseURL {
523+ t.Fatalf("unexpected Yolo model: %+v", yolo)
524+ }
525+
526+ small, err := FindModel("yolo-auto", "yolo-small")
527+ if err != nil {
528+ t.Fatal(err)
529+ }
530+ if small.BaseURL != yoloAutoDefaultBaseURL || small.Reasoning {
531+ t.Fatalf("unexpected Yolo Small model: %+v", small)
532+ }
533+}
534+
514535 func TestOpenAICompatAnthropicReasoningEffort(t *testing.T) {
515536 c := NewOpenRouter("token", "").(*openaiClient)
516537 wire, err := c.buildRequest(Request{
@@ -511,6 +511,27 @@ func TestGondolaSeedCatalog(t *testing.T) {
511 }511 }
512 }512 }
513 513
514+func TestYoloAutoSeedCatalog(t *testing.T) {
515+ // `yolo` is the model returned when no --model is given, so it has to
516+ // resolve from the baked-in catalog without a live refresh.
517+ yolo, err := FindModel("yolo-auto", "yolo")
518+ if err != nil {
519+ t.Fatal(err)
520+ }
521+ if yolo.ContextWindow != yoloAutoContextWindow || yolo.MaxOutput != yoloAutoMaxOutput ||
522+ !yolo.Reasoning || yolo.BaseURL != yoloAutoDefaultBaseURL {
523+ t.Fatalf("unexpected Yolo model: %+v", yolo)
524+ }
525+
526+ small, err := FindModel("yolo-auto", "yolo-small")
527+ if err != nil {
528+ t.Fatal(err)
529+ }
530+ if small.BaseURL != yoloAutoDefaultBaseURL || small.Reasoning {
531+ t.Fatalf("unexpected Yolo Small model: %+v", small)
532+ }
533+}
534+
514 func TestOpenAICompatAnthropicReasoningEffort(t *testing.T) {535 func TestOpenAICompatAnthropicReasoningEffort(t *testing.T) {
515 c := NewOpenRouter("token", "").(*openaiClient)536 c := NewOpenRouter("token", "").(*openaiClient)
516 wire, err := c.buildRequest(Request{537 wire, err := c.buildRequest(Request{

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