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 @@ | |||
| 18 | 18 | Yet another coding agent harness, lightweight and written (vibe-slopped) in go. | |
| 19 | 19 | ||
| 20 | 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 | 22 | - built-in tools (read, write, edit, bash, glob). | |
| 23 | 23 | - three run modes (interactive tui, print, json). | |
| 24 | 24 | - 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. | |||
| 181 | 181 | | Hugging Face | `HF_TOKEN` | `huggingface` | | |
| 182 | 182 | | OpenRouter | `OPENROUTER_API_KEY` | `openrouter` | | |
| 183 | 183 | | Gondola | `GONDOLA_API_KEY` | `gondola` | | |
| 184 | + | | Yolo-Auto | `YOLO_AUTO_API_KEY` | `yolo-auto` | | |
| 184 | 185 | | Mistral | `MISTRAL_API_KEY` | `mistral` | | |
| 185 | 186 | | ZAI | `ZAI_API_KEY` | `zai` | | |
| 186 | 187 | | Xiaomi MiMo | `XIAOMI_API_KEY` | `xiaomi` | | |
| @@ -201,6 +202,14 @@ show instructions and should be configured with environment variables. | |||
| 201 | 202 | When Gondola credentials are available, zot refreshes its public text-model | |
| 202 | 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 | 213 | Example: | |
| 205 | 214 | ||
| 206 | 215 | ```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-model | 202 | 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 | ```bash | 215 | ```bash | ||
M packages/agent/build.go +7 -1
| @@ -193,6 +193,8 @@ func defaultModelForProvider(prov string) string { | |||
| 193 | 193 | return "anthropic/claude-sonnet-4.5" | |
| 194 | 194 | case "gondola": | |
| 195 | 195 | return "kimi-k3" | |
| 196 | + | case "yolo-auto": | |
| 197 | + | return "yolo" | |
| 196 | 198 | case "mistral": | |
| 197 | 199 | return "mistral-large-latest" | |
| 198 | 200 | case "zai": | |
| @@ -234,7 +236,7 @@ func defaultModelForProvider(prov string) string { | |||
| 234 | 236 | var knownProviders = []string{ | |
| 235 | 237 | "anthropic", "openai", "openai-codex", "openai-responses", "kimi", "deepseek", "google", "ollama", provider.LlamaCPPProviderID, provider.LMStudioProviderID, | |
| 236 | 238 | "moonshotai", "moonshotai-cn", | |
| 237 | - | "cerebras", "groq", "xai", "together", "huggingface", "openrouter", "gondola", | |
| 239 | + | "cerebras", "groq", "xai", "together", "huggingface", "openrouter", "gondola", "yolo-auto", | |
| 238 | 240 | "mistral", "zai", | |
| 239 | 241 | "xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "xiaomi-token-plan-sgp", | |
| 240 | 242 | "minimax", "minimax-cn", | |
| @@ -900,6 +902,8 @@ func (r Resolved) NewClient() provider.Client { | |||
| 900 | 902 | return wrap(provider.NewOpenRouter(r.Credential, r.BaseURL)) | |
| 901 | 903 | case "gondola": | |
| 902 | 904 | return wrap(provider.NewGondola(r.Credential, r.BaseURL)) | |
| 905 | + | case "yolo-auto": | |
| 906 | + | return wrap(provider.NewYoloAuto(r.Credential, r.BaseURL)) | |
| 903 | 907 | case "zai": | |
| 904 | 908 | return wrap(provider.NewZAI(r.Credential, r.BaseURL)) | |
| 905 | 909 | case "xiaomi": | |
| @@ -1209,6 +1213,8 @@ func envVarName(provider string) string { | |||
| 1209 | 1213 | return "OPENROUTER" | |
| 1210 | 1214 | case "gondola": | |
| 1211 | 1215 | return "GONDOLA" | |
| 1216 | + | case "yolo-auto": | |
| 1217 | + | return "YOLO_AUTO" | |
| 1212 | 1218 | case "mistral": | |
| 1213 | 1219 | return "MISTRAL" | |
| 1214 | 1220 | 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 | |||
| 466 | 466 | if v := os.Getenv("GONDOLA_API_KEY"); v != "" { | |
| 467 | 467 | 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 | 473 | case "mistral": | |
| 470 | 474 | if v := os.Getenv("MISTRAL_API_KEY"); v != "" { | |
| 471 | 475 | 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", "", nil | 467 | 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", "", nil | 475 | return v, "apikey", "", nil | ||
M packages/agent/modelsync.go +7 -0
| @@ -261,6 +261,13 @@ func refreshModels() { | |||
| 261 | 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 | 273 | // 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 the | 273 | // 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 | |||
| 108 | 108 | return err | |
| 109 | 109 | } | |
| 110 | 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 | 117 | case "huggingface": | |
| 112 | 118 | req, err = http.NewRequestWithContext(ctx, "GET", "https://router.huggingface.co/v1/models", nil) | |
| 113 | 119 | if err != nil { | |
| @@ -108,6 +108,12 @@ func probeAPIKey(ctx context.Context, provider, key string, c *http.Client) erro | |||||
| 108 | return err | 108 | 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 { | |||
| 138 | 138 | return []string{ | |
| 139 | 139 | "anthropic", "openai", "kimi", "deepseek", "google", "llama.cpp", "lmstudio", | |
| 140 | 140 | "moonshotai", "moonshotai-cn", "groq", "cerebras", "xai", "together", | |
| 141 | - | "huggingface", "openrouter", "gondola", "mistral", "zai", | |
| 141 | + | "huggingface", "openrouter", "gondola", "yolo-auto", "mistral", "zai", | |
| 142 | 142 | "xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "xiaomi-token-plan-sgp", | |
| 143 | 143 | "minimax", "minimax-cn", "fireworks", "vercel-ai-gateway", | |
| 144 | 144 | "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 { | |||
| 230 | 230 | const ( | |
| 231 | 231 | openrouterDefaultBaseURL = "https://openrouter.ai/api/v1" | |
| 232 | 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 | 243 | // DiscoverOpenRouter lists models from OpenRouter's public /models | |
| @@ -458,6 +466,70 @@ func DiscoverGondola(ctx context.Context, baseURL string) ([]Model, error) { | |||
| 458 | 466 | 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 | 533 | // perMillionTokens converts a per-token USD price string to USD per 1M | |
| 462 | 534 | // tokens. Empty or unparseable values become 0. | |
| 463 | 535 | 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 /models | 243 | // DiscoverOpenRouter lists models from OpenRouter's public /models | ||
| @@ -458,6 +466,70 @@ func DiscoverGondola(ctx context.Context, baseURL string) ([]Model, error) { | |||||
| 458 | return out, nil | 466 | 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 1M | 533 | // 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 { | |||
| 122 | 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 | 131 | // NewOpenCode is the opencode.ai Zen endpoint. Mixed APIs upstream; this | |
| 126 | 132 | // constructor wires the openai-completions flavor only. Models that need | |
| 127 | 133 | // 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; this | 131 | // NewOpenCode is the opencode.ai Zen endpoint. Mixed APIs upstream; this | ||
| 126 | // constructor wires the openai-completions flavor only. Models that need | 132 | // constructor wires the openai-completions flavor only. Models that need | ||
| 127 | // the anthropic-messages flavor under the same provider should be built | 133 | // 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 { | |||
| 41 | 41 | return "OpenRouter" | |
| 42 | 42 | case "gondola": | |
| 43 | 43 | return "Gondola" | |
| 44 | + | case "yolo-auto": | |
| 45 | + | return "Yolo-Auto" | |
| 44 | 46 | case "mistral": | |
| 45 | 47 | return "Mistral" | |
| 46 | 48 | 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{ | |||
| 272 | 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 | 295 | // ---- Anthropic / newer families ---- | |
| 276 | 296 | { | |
| 277 | 297 | 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) { | |||
| 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 | 535 | func TestOpenAICompatAnthropicReasoningEffort(t *testing.T) { | |
| 515 | 536 | c := NewOpenRouter("token", "").(*openaiClient) | |
| 516 | 537 | 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{ | ||