1package agent
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "path/filepath"
8 "runtime"
9 "strings"
10
11 zotdocs "github.com/patriceckhart/zot"
12 "github.com/patriceckhart/zot/packages/agent/extensions"
13 "github.com/patriceckhart/zot/packages/agent/skills"
14 "github.com/patriceckhart/zot/packages/agent/tools"
15 "github.com/patriceckhart/zot/packages/core"
16 "github.com/patriceckhart/zot/packages/provider"
17)
18
19// ContextFile is an instruction file loaded into the system prompt.
20type ContextFile struct {
21 Path string
22 Content string
23}
24
25// Resolved is the effective configuration after merging CLI, config, defaults.
26type Resolved struct {
27 Provider string
28 Model string
29 Credential string // api key or oauth access token
30 AuthMethod string // "apikey" | "oauth" | "" (no credential yet)
31 AccountID string // ChatGPT account id (for openai oauth), "" otherwise
32 BaseURL string
33 InsecureTLS bool
34 CWD string
35 Reasoning string
36 Temperature *float32
37
38 ToolRegistry core.Registry
39 ToolSummary []ToolSummary
40 SystemPrompt string
41 MaxSteps int
42 Sandbox *tools.Sandbox
43
44 // MaxOutput is the resolved model's maximum output-token budget
45 // (from the catalog). Passed to the agent so each turn requests
46 // the model's full output capacity instead of the provider's
47 // conservative default (e.g. Bedrock's 4096, which truncates
48 // long writes/edits with stopReason=length).
49 MaxOutput int
50
51 // SkillTool is the on-demand skill loader registered with the
52 // agent's tool registry, or nil if no SKILL.md files were
53 // discovered. Exposed so the tui can list / preview skills.
54 SkillTool *skills.Tool
55
56 // SkillDiagnostics records non-fatal skill discovery failures and
57 // shadowing decisions so each host mode can report them appropriately.
58 SkillDiagnostics []string
59
60 // ContextFiles records the AGENTS.md files appended to SystemPrompt,
61 // in effective load order. Interactive mode uses this metadata to make
62 // otherwise invisible startup context inspectable without adding fake
63 // messages to the provider transcript.
64 ContextFiles []ContextFile
65
66 // Bookkeeping for MergeExtensionTools. Captured at Resolve time
67 // so the system prompt can be rebuilt later without re-running
68 // resolve.
69 credentialOptional bool
70 systemAppend []string
71 systemCustom string
72 systemCustomSet bool
73 toolDescriptions map[string]string
74}
75
76// HasCredential reports whether the resolved configuration can create a client.
77// Configured custom endpoints may intentionally omit an API credential.
78func (r Resolved) HasCredential() bool {
79 return r.Credential != "" || r.credentialOptional
80}
81
82// MergeExtensionTools folds every tool registered by an extension
83// into r's ToolRegistry and re-renders the system prompt's tool
84// summary so the model sees both built-in and extension tools.
85//
86// Idempotent: calling twice with the same manager state has no
87// effect on the second pass (existing names are preserved). Built-in
88// tools always win on conflict.
89func (r *Resolved) MergeExtensionTools(mgr ExtensionToolSource) {
90 if mgr == nil {
91 return
92 }
93 infos := mgr.Tools()
94 if len(infos) == 0 {
95 return
96 }
97 changed := false
98 for _, info := range infos {
99 if _, exists := r.ToolRegistry[info.Name]; exists {
100 continue
101 }
102 r.ToolRegistry[info.Name] = mgr.NewExtensionTool(info)
103 changed = true
104 }
105 if !changed {
106 return
107 }
108 // Re-render the system prompt with the merged tool list. Skill
109 // addendum is preserved by walking the existing append slice.
110 append_ := r.systemAppend
111 r.SystemPrompt = BuildSystemPrompt(SystemPromptOpts{
112 CWD: r.CWD,
113 Tools: toolSummariesFromRegistry(r.ToolRegistry, r.toolDescriptions),
114 Custom: r.systemCustom,
115 CustomSet: r.systemCustomSet,
116 Append: append_,
117 ZotDocsDir: filepath.Join(ZotHome(), "docs"),
118 })
119}
120
121// ExtensionToolSource is the slice of the extension manager that
122// MergeExtensionTools needs. Lives here as an interface so the
123// build package doesn't import packages/agent/extensions (which
124// imports core, which imports... avoid the cycle).
125type ExtensionToolSource interface {
126 Tools() []ExtensionToolInfo
127 NewExtensionTool(info ExtensionToolInfo) core.Tool
128}
129
130// ExtensionToolInfo mirrors extensions.ToolInfo so we can declare
131// ExtensionToolSource here without importing the extensions
132// package. The cli wires a tiny adapter to bridge them.
133type ExtensionToolInfo struct {
134 Extension string
135 Name string
136 Description string
137 Schema []byte
138 Deferred bool
139 Interactive bool
140}
141
142// toolSummariesFromRegistry rebuilds the system-prompt tool list
143// from a (possibly extended) registry, using cached descriptions for
144// the human-readable summary text.
145func toolSummariesFromRegistry(reg core.Registry, cached map[string]string) []ToolSummary {
146 out := make([]ToolSummary, 0, len(reg))
147 for name, t := range reg {
148 if d, ok := t.(interface{ Deferred() bool }); ok && d.Deferred() {
149 continue
150 }
151 desc := t.Description()
152 if d, ok := cached[name]; ok && d != "" {
153 desc = d
154 }
155 out = append(out, ToolSummary{Name: name, Description: desc})
156 }
157 return out
158}
159
160// defaultModelForProvider returns the model id zot prefers when the
161// caller didn't pick one. Mirrors the per-provider switch used at
162// multiple points in Resolve; centralised so the unknown-model
163// recovery path and the no-model-configured path can't drift.
164//
165// Returns the empty string for "ollama", which has no built-in
166// default — the caller is expected to special-case ollama and
167// error or use whatever the user passed.
168func defaultModelForProvider(prov string) string {
169 switch prov {
170 case "openai":
171 return "gpt-5"
172 case "openai-codex":
173 return "gpt-5.5"
174 case "openai-responses":
175 return "gpt-5"
176 case "kimi":
177 return "kimi-for-coding"
178 case "deepseek":
179 return "deepseek-v4-pro"
180 case "google":
181 return "gemini-2.5-pro"
182 case "ollama", provider.LlamaCPPProviderID, provider.LMStudioProviderID:
183 return ""
184 case "moonshotai", "moonshotai-cn":
185 return "kimi-k2.6"
186 case "cerebras":
187 return "qwen-3-235b-a22b-instruct-2507"
188 case "groq":
189 return "llama-3.3-70b-versatile"
190 case "xai":
191 return "grok-4.5"
192 case "together":
193 return "Qwen/Qwen3-Coder-480B-A35B-Instruct"
194 case "huggingface":
195 return "moonshotai/Kimi-K2-Instruct"
196 case "openrouter":
197 return "anthropic/claude-sonnet-4.5"
198 case "gondola":
199 return "kimi-k3"
200 case "yolo-auto":
201 return "qwen3.8-flash"
202 case "mistral":
203 return "mistral-large-latest"
204 case "zai":
205 return "glm-4.7"
206 case "xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "xiaomi-token-plan-sgp":
207 return "mimo-v2.5"
208 case "minimax", "minimax-cn":
209 return "MiniMax-M2.7"
210 case "fireworks":
211 return "accounts/fireworks/models/kimi-k2p6"
212 case "vercel-ai-gateway":
213 return "anthropic/claude-sonnet-4.5"
214 case "opencode":
215 return "claude-sonnet-4-5"
216 case "opencode-go":
217 return "kimi-k2.6"
218 case "amazon-bedrock":
219 return "anthropic.claude-sonnet-4-5-20250929-v1:0"
220 case "google-vertex":
221 return "gemini-2.5-pro"
222 case "azure-openai-responses":
223 return "gpt-5"
224 case "github-copilot":
225 return "claude-sonnet-5"
226 default:
227 // Custom providers: pick the first model from the catalog for
228 // that provider, or fall back to the global default.
229 if models := provider.ModelsForProvider(prov); len(models) > 0 {
230 return models[0].ID
231 }
232 return provider.DefaultModel.ID
233 }
234}
235
236// knownProviders is the set of provider ids zot recognises. Used by
237// Resolve to validate args.Provider, by extension-callers, and by the
238// auto-fallback logic that picks any logged-in provider when the user's
239// preferred one has no credentials.
240var knownProviders = []string{
241 "anthropic", "openai", "openai-codex", "openai-responses", "kimi", "deepseek", "google", "ollama", provider.LlamaCPPProviderID, provider.LMStudioProviderID,
242 "moonshotai", "moonshotai-cn",
243 "cerebras", "groq", "xai", "together", "huggingface", "openrouter", "gondola", "yolo-auto",
244 "mistral", "zai",
245 "xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "xiaomi-token-plan-sgp",
246 "minimax", "minimax-cn",
247 "fireworks", "vercel-ai-gateway",
248 "opencode", "opencode-go",
249 "amazon-bedrock", "google-vertex", "azure-openai-responses",
250 "github-copilot", "cloudflare-workers-ai", "cloudflare-ai-gateway",
251}
252
253func isKnownProvider(name string) bool {
254 for _, p := range knownProviders {
255 if p == name {
256 return true
257 }
258 }
259 _, ok := provider.CustomProviders()[name]
260 return ok
261}
262
263// isBuiltinProvider reports whether name is in the hardcoded
264// knownProviders list (not a user-defined custom provider).
265func isBuiltinProvider(name string) bool {
266 for _, p := range knownProviders {
267 if p == name {
268 return true
269 }
270 }
271 return false
272}
273
274// providerAliases maps common short / alternate provider names to the
275// canonical id in knownProviders. Users (and other agents) reach for
276// "bedrock" or "vertex" far more naturally than the fully-qualified
277// "amazon-bedrock" / "google-vertex"; without this mapping an alias is
278// treated as an unknown provider and Resolve silently falls back to
279// anthropic, producing a misleading "no credential for anthropic" error
280// after the user explicitly picked, say, bedrock.
281var providerAliases = map[string]string{
282 "bedrock": "amazon-bedrock",
283 "aws-bedrock": "amazon-bedrock",
284 "amazon": "amazon-bedrock",
285 "vertex": "google-vertex",
286 "gcp-vertex": "google-vertex",
287 "gemini": "google",
288 "googleai": "google",
289 "google-ai": "google",
290 "azure": "azure-openai-responses",
291 "azure-openai": "azure-openai-responses",
292 "copilot": "github-copilot",
293 "github": "github-copilot",
294 "codex": "openai-codex",
295 "moonshot": "moonshotai",
296 "kimi-code": "kimi",
297 "ai-gateway": "vercel-ai-gateway",
298 "vercel": "vercel-ai-gateway",
299 "cloudflare": "cloudflare-workers-ai",
300 "workers-ai": "cloudflare-workers-ai",
301 "hf": "huggingface",
302 "llama": provider.LlamaCPPProviderID,
303 "llamacpp": provider.LlamaCPPProviderID,
304}
305
306// canonicalProvider normalises a user-supplied provider name: trims
307// surrounding whitespace, lower-cases it, and resolves any known alias
308// to its canonical id. Unknown names are returned trimmed/lower-cased
309// and unchanged so the existing unknown-provider handling still runs.
310func canonicalProvider(name string) string {
311 n := strings.ToLower(strings.TrimSpace(name))
312 if n == "" {
313 return n
314 }
315 if canon, ok := providerAliases[n]; ok {
316 return canon
317 }
318 return n
319}
320
321// Resolve merges args, config, and env into a Resolved set.
322//
323// Unlike the earlier version, Resolve NEVER returns an error for
324// missing credentials: the TUI can start without them and launch a
325// login flow. requireCred controls whether missing credentials are a
326// hard error (used by print/json modes).
327func Resolve(args Args, requireCred bool) (Resolved, error) {
328 cfg, _ := LoadConfig()
329
330 // User-requested provider (explicit > config > default).
331 // Normalise common aliases (e.g. "bedrock" -> "amazon-bedrock")
332 // before validation so an alias is never mistaken for an unknown
333 // provider and silently downgraded to anthropic.
334 argProvider := canonicalProvider(args.Provider)
335 provName := firstNonEmpty(argProvider, canonicalProvider(cfg.Provider), "anthropic")
336 if !isKnownProvider(provName) {
337 // Unknown provider (maybe removed or renamed). Fall back to
338 // the first provider that has credentials, or anthropic.
339 // Custom providers (from models.json) are already accepted
340 // by isKnownProvider, so we only reach here for truly unknown
341 // names.
342 provName = "anthropic"
343 if CredentialAvailable("openai") {
344 provName = "openai"
345 }
346 if CredentialAvailable("openai-codex") {
347 provName = "openai-codex"
348 }
349 if CredentialAvailable("kimi") {
350 provName = "kimi"
351 }
352 if CredentialAvailable("deepseek") {
353 provName = "deepseek"
354 }
355 if CredentialAvailable("anthropic") {
356 provName = "anthropic"
357 }
358 // Reset the saved config so this doesn't keep happening.
359 cfg.Provider = provName
360 cfg.Model = ""
361 _ = SaveConfig(cfg)
362 }
363
364 var (
365 cred string
366 method string
367 accountID string
368 credErr error
369 )
370 customCfg, customProvider := provider.CustomProviders()[provName]
371 keylessCustom := customProvider && !isBuiltinProvider(provName) && customCfg.NoAuth
372 if keylessCustom {
373 // Explicit keyless configuration takes precedence over CLI, env, and
374 // stored credentials. Never execute a stored API key command.
375 } else if args.inheritedCredential != "" {
376 cred = args.inheritedCredential
377 method = args.inheritedAuthMethod
378 accountID = args.inheritedAccountID
379 } else if provName == "ollama" {
380 cred = firstNonEmpty(args.APIKey, "ollama")
381 method = "apikey"
382 } else {
383 cred, method, accountID, credErr = ResolveCredentialFull(provName, args.APIKey)
384 }
385
386 // Persist --api-key for custom providers so subsequent runs don't
387 // need to pass it again.
388 if !isBuiltinProvider(provName) && !keylessCustom && args.APIKey != "" {
389 if store := AuthStoreFor(); store != nil {
390 _ = store.SetAPIKey(provName, args.APIKey)
391 }
392 }
393
394 // If the user did NOT explicitly pick a provider (neither via --provider
395 // nor by saving one in config.json) and the default one has no
396 // credentials, auto-fall-back to whichever provider is actually logged
397 // in. That way running plain `zot` after `/login` (any provider) never
398 // shows a "not logged in" banner.
399 //
400 // Critical: when the user HAS saved a provider in config.json (e.g.
401 // "deepseek" with "deepseek-v4-flash"), that choice is respected even
402 // if no credential is found — the tui will show the login dialog or
403 // surface a clear credential error rather than silently switching to
404 // a completely different provider and model.
405 // autoFellBack tracks whether the provider was changed by the
406 // auto-fallback loop (no explicit provider from CLI or config,
407 // and the default had no credentials). When false, the provider
408 // came from the user's explicit choice (--provider or config.json)
409 // and must be respected, including any model id that may belong
410 // to a different provider in the catalog — for example when using
411 // a gateway/router like OpenRouter with "deepseek/deepseek-v4-flash".
412 var autoFellBack bool
413 userPickedProvider := args.Provider != "" || cfg.Provider != ""
414 if credErr != nil && !userPickedProvider && provName != "ollama" {
415 autoFellBack = true
416 // Scan every known provider (not a hardcoded subset) so any
417 // env-based credential is discovered, e.g. an env-only
418 // amazon-bedrock setup (AWS_BEARER_TOKEN_BEDROCK / AWS_PROFILE /
419 // IAM keys) when no config.json pins the provider, such as after
420 // pointing ZOT_HOME at a fresh home dir. Iteration order of
421 // knownProviders defines fallback priority. Local providers without a
422 // default model are skipped because selecting either one here would
423 // fail before the user can choose a model.
424 for _, other := range knownProviders {
425 if other == provName || other == "ollama" || other == provider.LlamaCPPProviderID || other == provider.LMStudioProviderID {
426 continue
427 }
428 if !CredentialAvailable(other) {
429 continue
430 }
431 c, m, a, err := ResolveCredentialFull(other, args.APIKey)
432 provName = other
433 cred, method, accountID, credErr = c, m, a, err
434 break
435 }
436 }
437
438 model := firstNonEmpty(args.Model, cfg.Model)
439 if model == "" {
440 if provName == "ollama" || provName == provider.LlamaCPPProviderID || provName == provider.LMStudioProviderID {
441 return Resolved{}, fmt.Errorf("%s requires --model or a model selected from its manager", provName)
442 }
443 model = defaultModelForProvider(provName)
444 }
445 // If the resolved model belongs to a different provider (e.g. config
446 // says gpt-5 but we auto-fell back to anthropic), pick that provider's
447 // default. This override only fires when the provider was auto-fallback-
448 // changed, NOT when the user explicitly configured the pair — gateway/
449 // router providers (openrouter, vercel-ai-gateway, etc.) can serve
450 // models from any provider in the catalog.
451 if autoFellBack && provName != "ollama" {
452 if m, err := provider.FindModel("", model); err == nil && m.Provider != provName {
453 model = defaultModelForProvider(provName)
454 }
455 }
456 if provName == provider.LMStudioProviderID && LMStudioConfigured() {
457 // Restore transient metadata when resuming a local model after restart.
458 _ = RefreshLMStudioModels(context.Background())
459 }
460 if isDiscoverableCustomProvider(provName) {
461 if _, err := provider.FindModel(provName, model); err != nil {
462 // Same restore for discovery-enabled custom providers. Skipped
463 // when the model is already known to avoid a probe per launch.
464 _ = RefreshCustomProviderModels(context.Background())
465 }
466 }
467 resolvedModel, err := provider.FindModel(provName, model)
468 if err != nil && (provName == "ollama" || provName == provider.LlamaCPPProviderID || provName == provider.LMStudioProviderID) {
469 // Local providers are intentionally open-catalogue: any model id the
470 // configured server understands is valid, even if it is not cached.
471 resolvedModel = provider.Model{
472 Provider: provName,
473 ID: model,
474 DisplayName: model,
475 ContextWindow: 128000,
476 MaxOutput: 16384,
477 BaseURL: args.BaseURL,
478 Source: provName,
479 }
480 if provName == provider.LMStudioProviderID {
481 resolvedModel.ContextWindow = 32768
482 resolvedModel.MaxOutput = 4096
483 }
484 err = nil
485 }
486 // Custom providers are open-catalogue like ollama: any model id the
487 // endpoint understands is valid. Use the provider-level base URL.
488 if err != nil {
489 if cfg, ok := provider.CustomProviders()[provName]; ok {
490 resolvedModel = provider.Model{
491 Provider: provName,
492 ID: model,
493 DisplayName: model,
494 BaseURL: cfg.BaseURL,
495 Source: "user",
496 }
497 err = nil
498 }
499 }
500 if cfg, ok := provider.CustomProviders()[provName]; ok && err == nil && resolvedModel.BaseURL == "" && cfg.BaseURL != "" {
501 // Fall back to the provider-level base URL when the model does
502 // not define its own endpoint.
503 resolvedModel.BaseURL = cfg.BaseURL
504 }
505 if err != nil {
506 // The model the user (or persisted config) asked for is no
507 // longer in the active catalogue — they probably removed it
508 // from their models.json or upgraded zot and the id changed.
509 // Refusing to launch is the wrong move: it strands the user
510 // with no way to even open the TUI and pick a new model.
511 // Fall back to the provider's default, warn on stderr, and,
512 // when the stale id came from the persisted config (not an
513 // explicit --model flag), repair the config so the warning
514 // doesn't repeat on every launch.
515
516 // Gateway providers can accept route-qualified ids that are not in
517 // zot's local catalog yet, for example OpenRouter's
518 // "deepseek/deepseek-v4-flash". Preserve only route-qualified ids;
519 // plain unknown values are likely typos and should still fall back.
520 if isGatewayProvider(provName) && isGatewayRoutedModelID(model) {
521 resolvedModel = provider.Model{
522 Provider: provName,
523 ID: model,
524 DisplayName: model,
525 ContextWindow: 1000000,
526 MaxOutput: 64000,
527 BaseURL: "",
528 Source: "gateway",
529 }
530 err = nil
531 // Don't repair config — the routed model id may be valid upstream.
532 } else {
533 fallback := defaultModelForProvider(provName)
534 fm, ferr := provider.FindModel(provName, fallback)
535 if ferr != nil {
536 // Even the provider default is gone (catastrophic
537 // catalogue trim). Last resort: any model on this
538 // provider, then the global DefaultModel.
539 if candidates := provider.ModelsForProvider(provName); len(candidates) > 0 {
540 fm = candidates[0]
541 ferr = nil
542 } else {
543 fm = provider.DefaultModel
544 ferr = nil
545 }
546 }
547 fmt.Fprintf(os.Stderr,
548 "zot: model %q is not in the active catalogue; using %q instead. Pick a different model with --model or /model.\n",
549 model, fm.ID)
550 if args.Model == "" && cfg.Model == model {
551 cfg.Model = fm.ID
552 _ = SaveConfig(cfg)
553 }
554 resolvedModel = fm
555 model = fm.ID
556 }
557 }
558
559 // Discovered custom-provider models inherit the user's models.json
560 // endpoint, so they count as explicit for --insecure scoping too.
561 discoveredCustom := resolvedModel.Source == "live" && isDiscoverableCustomProvider(provName)
562 explicitBaseURL := args.BaseURL != "" || ((resolvedModel.Source == "user" || discoveredCustom) && resolvedModel.BaseURL != "")
563
564 // If the model defines a base URL (e.g. local ollama) and the
565 // user didn't pass --base-url, use the model's URL. For ollama,
566 // keep http://localhost:11434 as a fallback only after the model
567 // metadata has had a chance to provide a custom baseUrl.
568 if args.BaseURL == "" && resolvedModel.BaseURL != "" && (provName != provider.LMStudioProviderID || resolvedModel.Source == "user") {
569 // LM Studio discovery can retain an older snapshot on failure.
570 // Use its currently registered endpoint below, never a stale URL
571 // with the credentials for a newly registered server.
572 args.BaseURL = resolvedModel.BaseURL
573 }
574 if args.BaseURL == "" && provName == "ollama" {
575 args.BaseURL = "http://localhost:11434"
576 }
577 if args.BaseURL == "" && provName == provider.LMStudioProviderID {
578 root, _, configErr := resolveLMStudioConfig(context.Background(), apiKeyCommandExecute)
579 if configErr != nil {
580 return Resolved{}, configErr
581 }
582 if root == "" {
583 return Resolved{}, fmt.Errorf("LM Studio requires a server URL registered through /login or an explicit --base-url")
584 }
585 args.BaseURL = root + "/v1"
586 }
587 if args.BaseURL == "" && provName == provider.LlamaCPPProviderID {
588 managementURL, _, configErr := ResolveLlamaCPPConfig()
589 if configErr != nil {
590 return Resolved{}, configErr
591 }
592 var baseErr error
593 args.BaseURL, baseErr = provider.LlamaCPPInferenceURL(managementURL)
594 if baseErr != nil {
595 return Resolved{}, baseErr
596 }
597 }
598
599 // Insecure TLS is intentionally scoped to explicit custom endpoints.
600 // Built-in provider base URLs, auth calls, and model discovery keep normal
601 // certificate verification even when --insecure is present.
602 insecureTLS := (args.InsecureTLS || cfg.Insecure) && explicitBaseURL
603
604 // If the model has a base URL, credentials are optional (local
605 // models like ollama don't need real API keys).
606 credentialOptional := false
607 if keylessCustom {
608 if args.BaseURL == "" {
609 return Resolved{}, fmt.Errorf("custom provider %q has auth none but no base URL", provName)
610 }
611 credentialOptional = true
612 } else if resolvedModel.BaseURL != "" && credErr != nil {
613 if _, isCustom := provider.CustomProviders()[provName]; isCustom {
614 // A custom endpoint may be configured for keyless access. Keep the
615 // credential empty so the provider does not send a placeholder token.
616 credentialOptional = true
617 } else {
618 cred = "ollama"
619 if provName == "amazon-bedrock" {
620 // The default AWS CLI profile is resolved by the Bedrock client,
621 // not the general credential resolver. Never pass the local-model
622 // placeholder as a bearer token.
623 cred = "<aws>"
624 }
625 }
626 credErr = nil
627 requireCred = false
628 }
629
630 if credErr != nil && requireCred {
631 return Resolved{}, fmt.Errorf("%w; set %s_API_KEY, pass --api-key, or run `zot` and /login",
632 credErr, envVarName(provName))
633 }
634
635 sandbox := tools.NewSandbox(args.CWD)
636 if cfg.JailByDefault != nil && *cfg.JailByDefault {
637 sandbox.Lock()
638 }
639 if args.PermissionSet != nil {
640 sandbox.SetPermissions(args.PermissionSet)
641 }
642 reg := buildConfiguredToolRegistry(args, args.CWD, sandbox, cfg, runtime.GOOS)
643 if !args.NoTools && provName == "openrouter" && !isOpenRouterPresetModel(model) && OpenRouterServerToolsEnabled() {
644 reg = withOpenRouterServerTools(reg)
645 }
646
647 docsDir, _ := zotdocs.EnsureInstalled(ZotHome())
648
649 // Skill discovery: scan project + global locations + built-in
650 // skills shipped with the binary. If any are found, register
651 // the on-demand `skill` loader tool plus a system-prompt
652 // manifest so the model knows what's available.
653 //
654 // --no-skill bypasses the entire mechanism: no manifest in the
655 // system prompt, no `skill` tool in the registry. Useful for a
656 // clean-room run with zero extra context biasing the model.
657 var (
658 discovered []*skills.Skill
659 skillTool *skills.Tool
660 skillAddendum string
661 skillDiagnostics []string
662 )
663 if !args.NoSkill {
664 homeDir, _ := os.UserHomeDir()
665 var sources []skills.Source
666 if args.WithSkills {
667 for _, loc := range skills.SearchSources(ZotHome(), args.CWD, homeDir) {
668 sources = append(sources, loc)
669 }
670 }
671 if !args.NoExt || len(args.Exts) > 0 {
672 extSources, errs := extensions.PlanSkillSources(ZotHome(), args.CWD, args.Exts, !args.NoExt)
673 sources = append(extSources, sources...)
674 for _, err := range errs {
675 skillDiagnostics = append(skillDiagnostics, err.Error())
676 }
677 }
678 var errs []error
679 discovered, errs = skills.DiscoverSources(sources, true)
680 for _, err := range errs {
681 skillDiagnostics = append(skillDiagnostics, err.Error())
682 }
683 if len(discovered) > 0 {
684 skillTool = skills.NewTool(discovered)
685 reg[skillTool.Name()] = skillTool
686 skillAddendum = skills.SystemPromptAddendum(discovered)
687 }
688 }
689 _ = skillTool
690
691 summaries := toolSummaries(reg, args)
692
693 var contextFiles []ContextFile
694 if !args.NoContextFiles {
695 contextFiles = loadAgentsContext(args.CWD, ZotHome())
696 }
697 append_ := append([]string(nil), args.AppendSystemPrompt...)
698 if agentsAddendum := formatAgentsContext(contextFiles); agentsAddendum != "" {
699 append_ = append(append_, agentsAddendum)
700 }
701 if skillAddendum != "" {
702 append_ = append(append_, skillAddendum)
703 }
704 if AutoSwarmEnabled() {
705 append_ = append(append_, AutoSwarmSystemAddendum)
706 }
707
708 // Custom system prompt resolution order:
709 // 1. --system-prompt flag (highest priority; ad-hoc per run)
710 // 2. $ZOT_HOME/SYSTEM.md (persistent user override)
711 // 3. built-in default (defaultIdentity + defaultGuidelines)
712 custom := args.SystemPrompt
713 customSet := args.SystemPromptSet || custom != ""
714 if !customSet {
715 custom = readUserSystemPrompt(ZotHome())
716 customSet = custom != ""
717 }
718
719 sys := BuildSystemPrompt(SystemPromptOpts{
720 CWD: args.CWD,
721 Tools: summaries,
722 Custom: custom,
723 CustomSet: customSet,
724 Append: append_,
725 ZotDocsDir: docsDir,
726 })
727
728 reasoning := provider.NormalizeReasoning(firstNonEmpty(args.Reasoning, cfg.Reasoning))
729 temperature := args.Temperature
730 if temperature == nil {
731 temperature = cfg.Temperature
732 }
733
734 max := args.MaxSteps // 0 = unlimited
735
736 return Resolved{
737 Provider: provName,
738 Model: model,
739 Credential: cred,
740 AuthMethod: method,
741 AccountID: accountID,
742 BaseURL: args.BaseURL,
743 InsecureTLS: insecureTLS,
744 CWD: args.CWD,
745 Reasoning: reasoning,
746 Temperature: temperature,
747 ToolRegistry: reg,
748 ToolSummary: summaries,
749 SystemPrompt: sys,
750 MaxSteps: max,
751 MaxOutput: resolvedModel.MaxOutput,
752 Sandbox: sandbox,
753 SkillTool: skillTool,
754 SkillDiagnostics: skillDiagnostics,
755 ContextFiles: contextFiles,
756 credentialOptional: credentialOptional,
757 systemAppend: append_,
758 systemCustom: custom,
759 systemCustomSet: customSet,
760 toolDescriptions: descMapFromSummaries(summaries),
761 }, nil
762}
763
764// readUserSystemPrompt looks for $ZOT_HOME/SYSTEM.md and returns its
765// trimmed contents, or "" when the file is missing / unreadable /
766// empty. Errors are intentionally swallowed: the file is optional,
767// and any failure to read it should fall back to the built-in
768// default system prompt rather than crash the run.
769func readUserSystemPrompt(zotHome string) string {
770 if zotHome == "" {
771 return ""
772 }
773 path := filepath.Join(zotHome, "SYSTEM.md")
774 raw, err := os.ReadFile(path)
775 if err != nil {
776 return ""
777 }
778 return strings.TrimSpace(string(raw))
779}
780
781// loadAgentsContext loads optional AGENTS.md instruction files. No
782// default file is created or required: zot only includes files that
783// already exist. Global instructions ($ZOT_HOME/AGENTS.md) come first,
784// followed by project instructions from the filesystem root down to cwd.
785func loadAgentsContext(cwd, zotHome string) []ContextFile {
786 var files []ContextFile
787 seen := map[string]bool{}
788 add := func(path string) {
789 if path == "" {
790 return
791 }
792 abs, err := filepath.Abs(path)
793 if err == nil {
794 path = abs
795 }
796 if seen[path] {
797 return
798 }
799 raw, err := os.ReadFile(path)
800 if err != nil {
801 return
802 }
803 content := strings.TrimSpace(string(raw))
804 if content == "" {
805 return
806 }
807 seen[path] = true
808 files = append(files, ContextFile{Path: path, Content: content})
809 }
810 addFirstFromDir := func(dir string) {
811 if dir == "" {
812 return
813 }
814 for _, name := range []string{"AGENTS.md", "AGENTS.MD"} {
815 path := filepath.Join(dir, name)
816 if _, err := os.Stat(path); err == nil {
817 add(path)
818 return
819 }
820 }
821 }
822
823 addFirstFromDir(zotHome)
824
825 if cwd != "" {
826 abs, err := filepath.Abs(cwd)
827 if err == nil {
828 cwd = abs
829 }
830 var dirs []string
831 for dir := filepath.Clean(cwd); ; dir = filepath.Dir(dir) {
832 dirs = append(dirs, dir)
833 parent := filepath.Dir(dir)
834 if parent == dir {
835 break
836 }
837 }
838 for i := len(dirs) - 1; i >= 0; i-- {
839 addFirstFromDir(dirs[i])
840 }
841 }
842
843 return files
844}
845
846func formatAgentsContext(files []ContextFile) string {
847 if len(files) == 0 {
848 return ""
849 }
850 var sb strings.Builder
851 sb.WriteString("Project context instructions loaded from AGENTS.md files. Follow them when working in this repository. Later files are more specific and may override earlier ones.\n")
852 for _, f := range files {
853 fmt.Fprintf(&sb, "\n## %s\n\n%s\n", f.Path, f.Content)
854 }
855 return strings.TrimSpace(sb.String())
856}
857
858// descMapFromSummaries indexes the human-readable descriptions for
859// the renderToolsSection rebuild path.
860func descMapFromSummaries(summaries []ToolSummary) map[string]string {
861 out := make(map[string]string, len(summaries))
862 for _, s := range summaries {
863 out[s.Name] = s.Description
864 }
865 return out
866}
867
868// NewClient returns a provider.Client for r, choosing the auth mode
869// based on r.AuthMethod. Panics if no credential or usable keyless
870// endpoint is present; callers must check HasCredential() first.
871func (r Resolved) NewClient() provider.Client {
872 if !r.HasCredential() {
873 panic("NewClient called without credential or keyless endpoint; check HasCredential first")
874 }
875 wrap := r.withHTTPClient
876 switch r.Provider {
877 case "ollama":
878 return wrap(provider.NewOpenAI(r.Credential, r.BaseURL))
879 case provider.LlamaCPPProviderID, provider.LMStudioProviderID:
880 credential := r.Credential
881 if credential == "local" {
882 credential = ""
883 }
884 return wrap(provider.NewOpenAI(credential, r.BaseURL))
885 case "kimi":
886 // kimi-coding speaks anthropic-messages on api.kimi.com/coding.
887 // Subscription OAuth (refreshed) wraps the same Anthropic-shaped client.
888 inner := wrap(provider.NewKimiCodingWithHeaders(r.Credential, r.BaseURL, kimiCodeHeaders()))
889 if r.AuthMethod == "oauth" {
890 return r.wrapWithRefresh(inner)
891 }
892 return inner
893 case "moonshotai":
894 return wrap(provider.NewMoonshot(r.Credential, r.BaseURL))
895 case "moonshotai-cn":
896 return wrap(provider.NewMoonshotCN(r.Credential, r.BaseURL))
897 case "deepseek":
898 return wrap(provider.NewDeepSeek(r.Credential, r.BaseURL))
899 case "openai":
900 return provider.NewModelRouter("openai",
901 wrap(provider.NewOpenAI(r.Credential, r.BaseURL)),
902 map[string]provider.Client{
903 provider.APIResponses: wrap(provider.NewOpenAIResponsesNamed(r.Credential, r.BaseURL, "openai")),
904 })
905 case "openai-codex":
906 inner := wrap(provider.NewOpenAICodex(r.Credential, r.AccountID, r.BaseURL))
907 return r.wrapWithRefresh(inner)
908 case "openai-responses":
909 // Public OpenAI Responses API (api.openai.com/v1/responses) via
910 // API key. Separate provider from `openai` (Chat Completions) and
911 // from `openai-codex` (ChatGPT subscription OAuth).
912 return wrap(provider.NewOpenAIResponses(r.Credential, r.BaseURL))
913 case "google":
914 return wrap(provider.NewGemini(r.Credential, r.BaseURL))
915 case "cerebras":
916 return wrap(provider.NewCerebras(r.Credential, r.BaseURL))
917 case "groq":
918 return wrap(provider.NewGroq(r.Credential, r.BaseURL))
919 case "xai":
920 inner := r.newXAIClient(r.Credential)
921 if r.AuthMethod == "oauth" {
922 return r.wrapWithRefresh(inner)
923 }
924 return inner
925 case "together":
926 return wrap(provider.NewTogether(r.Credential, r.BaseURL))
927 case "huggingface":
928 return wrap(provider.NewHuggingFace(r.Credential, r.BaseURL))
929 case "openrouter":
930 return wrap(provider.NewOpenRouter(r.Credential, r.BaseURL))
931 case "gondola":
932 return wrap(provider.NewGondola(r.Credential, r.BaseURL))
933 case "yolo-auto":
934 return wrap(provider.NewYoloAuto(r.Credential, r.BaseURL))
935 case "zai":
936 return wrap(provider.NewZAI(r.Credential, r.BaseURL))
937 case "xiaomi":
938 return wrap(provider.NewXiaomi(r.Credential, r.BaseURL))
939 case "xiaomi-token-plan-ams":
940 return wrap(provider.NewXiaomiTokenPlan("ams", r.Credential, r.BaseURL))
941 case "xiaomi-token-plan-cn":
942 return wrap(provider.NewXiaomiTokenPlan("cn", r.Credential, r.BaseURL))
943 case "xiaomi-token-plan-sgp":
944 return wrap(provider.NewXiaomiTokenPlan("sgp", r.Credential, r.BaseURL))
945 case "opencode":
946 return wrap(provider.NewOpenCode(r.Credential, r.BaseURL))
947 case "opencode-go":
948 return wrap(provider.NewOpenCodeGo(r.Credential, r.BaseURL))
949 case "minimax":
950 return wrap(provider.NewMinimaxAnthropic(r.Credential, r.BaseURL))
951 case "minimax-cn":
952 return wrap(provider.NewMinimaxCNAnthropic(r.Credential, r.BaseURL))
953 case "fireworks":
954 return wrap(provider.NewFireworksAnthropic(r.Credential, r.BaseURL))
955 case "vercel-ai-gateway":
956 return wrap(provider.NewVercelGatewayAnthropic(r.Credential, r.BaseURL))
957 case "mistral":
958 return wrap(provider.NewMistral(r.Credential, r.BaseURL))
959 case "amazon-bedrock":
960 return wrap(provider.NewBedrock(r.Credential, r.BaseURL))
961 case "google-vertex":
962 return wrap(provider.NewGoogleVertex(r.Credential, r.BaseURL))
963 case "azure-openai-responses":
964 return wrap(provider.NewAzureOpenAIResponses(r.Credential, r.BaseURL))
965 case "github-copilot":
966 return wrap(provider.NewGithubCopilot(r.Credential, r.BaseURL))
967 case "cloudflare-workers-ai":
968 return wrap(provider.NewCloudflareWorkersAI(r.Credential, r.BaseURL))
969 case "cloudflare-ai-gateway":
970 return wrap(provider.NewCloudflareAIGateway(r.Credential, r.BaseURL))
971 default:
972 // Custom providers: choose wire format from the models.json api field.
973 if cfg, ok := provider.CustomProviders()[r.Provider]; ok {
974 switch cfg.API {
975 case provider.APIResponses:
976 return wrap(provider.NewOpenAIResponsesNamed(r.Credential, r.BaseURL, r.Provider))
977 case "anthropic":
978 return wrap(provider.NewAnthropicCompat(r.Provider, r.Credential, r.BaseURL))
979 default: // "openai"
980 return wrap(provider.NewOpenAICompat(r.Provider, r.Credential, r.BaseURL, ""))
981 }
982 }
983 if r.AuthMethod == "oauth" {
984 inner := wrap(provider.NewAnthropicOAuth(r.Credential, r.BaseURL))
985 return r.wrapWithRefresh(inner)
986 }
987 return wrap(provider.NewAnthropic(r.Credential, r.BaseURL))
988 }
989}
990
991func (r Resolved) withHTTPClient(c provider.Client) provider.Client {
992 if !r.InsecureTLS {
993 return c
994 }
995 return provider.WithHTTPClient(c, provider.NewHTTPClient(true))
996}
997
998func (r Resolved) newXAIClient(token string) provider.Client {
999 return provider.NewModelRouter("xai",
1000 r.withHTTPClient(provider.NewXAI(token, r.BaseURL)),
1001 map[string]provider.Client{
1002 provider.APIResponses: r.withHTTPClient(provider.NewOpenAIResponsesNamed(token, r.BaseURL, "xai")),
1003 })
1004}
1005
1006// wrapWithRefresh wraps an OAuth client so the access token is
1007// refreshed automatically before each API call. Without this, long
1008// sessions (hours) silently fail when the 1-hour token expires.
1009func (r Resolved) wrapWithRefresh(inner provider.Client) provider.Client {
1010 provName := r.Provider
1011 tokenProvider := provName
1012 if provName == "openai-codex" {
1013 tokenProvider = "openai"
1014 }
1015 baseURL := r.BaseURL
1016 accountID := r.AccountID
1017
1018 refreshFn := func(ctx context.Context) (string, error) {
1019 tok, err := refreshIfExpired(tokenProvider, loadOAuthToken(tokenProvider))
1020 if err != nil {
1021 return "", err
1022 }
1023 return tok.AccessToken, nil
1024 }
1025
1026 factory := func(token string) provider.Client {
1027 switch provName {
1028 case "openai-codex":
1029 return r.withHTTPClient(provider.NewOpenAICodex(token, accountID, baseURL))
1030 case "kimi":
1031 // anthropic-messages on api.kimi.com/coding.
1032 return r.withHTTPClient(provider.NewKimiCodingWithHeaders(token, baseURL, kimiCodeHeaders()))
1033 case "xai":
1034 return r.newXAIClient(token)
1035 default:
1036 return r.withHTTPClient(provider.NewAnthropicOAuth(token, baseURL))
1037 }
1038 }
1039
1040 return provider.NewRefreshingClient(inner, refreshFn, factory)
1041}
1042
1043// UseSandbox replaces the sandbox pointer that every tool in r's
1044// registry references. Used to keep the /jail state stable across
1045// agent rebuilds (e.g. /login, /model switching providers).
1046func (r *Resolved) UseSandbox(s *tools.Sandbox) {
1047 if s == nil || r == nil {
1048 return
1049 }
1050 if r.Sandbox != nil && r.Sandbox.Permissions != nil {
1051 s.Permissions = r.Sandbox.Permissions
1052 }
1053 r.Sandbox = s
1054 for name, t := range r.ToolRegistry {
1055 switch v := t.(type) {
1056 case *tools.ReadTool:
1057 v.Sandbox = s
1058 case *tools.WriteTool:
1059 v.Sandbox = s
1060 case *tools.EditTool:
1061 v.Sandbox = s
1062 case *tools.BashTool:
1063 v.Sandbox = s
1064 case *tools.PowerShellTool:
1065 v.Sandbox = s
1066 case *tools.GlobTool:
1067 v.Sandbox = s
1068 }
1069 _ = name
1070 }
1071}
1072
1073// NewAgent constructs a core.Agent from r. Requires a credential.
1074func (r Resolved) NewAgent() *core.Agent {
1075 a := core.NewAgent(r.NewClient(), r.Model, r.SystemPrompt, r.ToolRegistry)
1076 a.MaxSteps = r.MaxSteps
1077 a.MaxTokens = r.MaxOutput
1078 a.Reasoning = r.Reasoning
1079 a.Temperature = r.Temperature
1080 if hasOpenRouterServerTools(r.ToolRegistry) {
1081 a.MaxToolCalls = openRouterServerToolCallLimit()
1082 }
1083 return a
1084}
1085
1086// openRouterServerToolCallLimit returns the configured call limit from
1087// config.json, falling back to the built-in default (4).
1088func openRouterServerToolCallLimit() int {
1089 cfg, err := LoadConfig()
1090 if err != nil {
1091 return tools.OpenRouterServerToolCallLimit
1092 }
1093 if cfg.OpenRouterServerToolCallLimit != nil && *cfg.OpenRouterServerToolCallLimit > 0 {
1094 return *cfg.OpenRouterServerToolCallLimit
1095 }
1096 return tools.OpenRouterServerToolCallLimit
1097}
1098
1099// OpenRouterServerToolsEnabled reports the persisted preference.
1100// nil/missing and unreadable configuration fail closed to disabled.
1101func OpenRouterServerToolsEnabled() bool {
1102 cfg, err := LoadConfig()
1103 if err != nil {
1104 return false
1105 }
1106 return cfg.OpenRouterServerToolsEnabled != nil && *cfg.OpenRouterServerToolsEnabled
1107}
1108
1109func isOpenRouterPresetModel(model string) bool {
1110 return strings.Contains(strings.ToLower(model), "@preset/")
1111}
1112
1113func hasOpenRouterServerTools(reg core.Registry) bool {
1114 for name := range reg {
1115 if tools.IsOpenRouterServerTool(name) {
1116 return true
1117 }
1118 }
1119 return false
1120}
1121
1122func withOpenRouterServerTools(reg core.Registry) core.Registry {
1123 out := core.Registry{}
1124 for name, t := range reg {
1125 out[name] = t
1126 }
1127 for _, t := range tools.OpenRouterServerTools() {
1128 out[t.Name()] = t
1129 }
1130 return out
1131}
1132
1133func buildConfiguredToolRegistry(args Args, cwd string, sandbox *tools.Sandbox, cfg Config, goos string) core.Registry {
1134 reg := buildToolRegistry(args, cwd, sandbox)
1135 if goos == "windows" && !args.NoTools && len(args.Tools) == 0 && cfg.PowerShellEnabled != nil && *cfg.PowerShellEnabled {
1136 reg["powershell"] = &tools.PowerShellTool{CWD: cwd, Sandbox: sandbox}
1137 }
1138 return reg
1139}
1140
1141func buildToolRegistry(args Args, cwd string, sandbox *tools.Sandbox) core.Registry {
1142 if args.NoTools {
1143 return core.Registry{}
1144 }
1145 all := map[string]core.Tool{
1146 "read": &tools.ReadTool{CWD: cwd, Sandbox: sandbox},
1147 "write": &tools.WriteTool{CWD: cwd, Sandbox: sandbox},
1148 "edit": &tools.EditTool{CWD: cwd, Sandbox: sandbox},
1149 "bash": &tools.BashTool{CWD: cwd, Sandbox: sandbox},
1150 "glob": &tools.GlobTool{CWD: cwd, Sandbox: sandbox},
1151 }
1152 reg := core.Registry{}
1153 if len(args.Tools) == 0 {
1154 for _, t := range all {
1155 reg[t.Name()] = t
1156 }
1157 return reg
1158 }
1159 // PowerShell is explicitly selected, never added to the default tool set.
1160 all["powershell"] = &tools.PowerShellTool{CWD: cwd, Sandbox: sandbox}
1161 for _, name := range args.Tools {
1162 if t, ok := all[name]; ok {
1163 reg[name] = t
1164 }
1165 }
1166 return reg
1167}
1168
1169func toolSummaries(reg core.Registry, args Args) []ToolSummary {
1170 order := []string{"read", "write", "edit", "bash", "powershell", "glob"}
1171 var out []ToolSummary
1172 for _, name := range order {
1173 if t, ok := reg[name]; ok {
1174 out = append(out, ToolSummary{Name: t.Name(), Description: t.Description()})
1175 }
1176 }
1177 return out
1178}
1179
1180func firstNonEmpty(vals ...string) string {
1181 for _, v := range vals {
1182 if v != "" {
1183 return v
1184 }
1185 }
1186 return ""
1187}
1188
1189func kimiCodeHeaders() map[string]string {
1190 host, _ := os.Hostname()
1191 if host == "" {
1192 host = "unknown"
1193 }
1194 deviceID := ""
1195 if home, err := os.UserHomeDir(); err == nil {
1196 if b, err := os.ReadFile(filepath.Join(home, ".kimi", "device_id")); err == nil {
1197 deviceID = strings.TrimSpace(string(b))
1198 }
1199 }
1200 if deviceID == "" {
1201 deviceID = "zot"
1202 }
1203 return map[string]string{
1204 "User-Agent": "KimiCLI/1.41.0",
1205 "X-Msh-Platform": "kimi_cli",
1206 "X-Msh-Version": "1.41.0",
1207 "X-Msh-Device-Name": host,
1208 "X-Msh-Device-Model": runtime.GOOS + "-" + runtime.GOARCH,
1209 "X-Msh-Os-Version": runtime.GOOS,
1210 "X-Msh-Device-Id": deviceID,
1211 }
1212}
1213
1214func envVarName(provider string) string {
1215 switch provider {
1216 case "openai", "openai-codex", "openai-responses":
1217 return "OPENAI"
1218 case "kimi":
1219 return "KIMI"
1220 case "deepseek":
1221 return "DEEPSEEK"
1222 case "google":
1223 return "GEMINI"
1224 case "ollama":
1225 return "OLLAMA"
1226 case "llama.cpp":
1227 return "LLAMA"
1228 case "moonshotai", "moonshotai-cn":
1229 return "MOONSHOT"
1230 case "groq":
1231 return "GROQ"
1232 case "xai":
1233 return "XAI"
1234 case "cerebras":
1235 return "CEREBRAS"
1236 case "together":
1237 return "TOGETHER"
1238 case "huggingface":
1239 return "HF"
1240 case "openrouter":
1241 return "OPENROUTER"
1242 case "gondola":
1243 return "GONDOLA"
1244 case "yolo-auto":
1245 return "YOLO_AUTO"
1246 case "mistral":
1247 return "MISTRAL"
1248 case "zai":
1249 return "ZAI"
1250 case "xiaomi":
1251 return "XIAOMI"
1252 case "xiaomi-token-plan-ams":
1253 return "XIAOMI_TOKEN_PLAN_AMS"
1254 case "xiaomi-token-plan-cn":
1255 return "XIAOMI_TOKEN_PLAN_CN"
1256 case "xiaomi-token-plan-sgp":
1257 return "XIAOMI_TOKEN_PLAN_SGP"
1258 case "minimax":
1259 return "MINIMAX"
1260 case "minimax-cn":
1261 return "MINIMAX_CN"
1262 case "fireworks":
1263 return "FIREWORKS"
1264 case "vercel-ai-gateway":
1265 return "AI_GATEWAY"
1266 case "opencode", "opencode-go":
1267 return "OPENCODE"
1268 case "github-copilot":
1269 return "COPILOT_GITHUB_TOKEN"
1270 case "cloudflare-workers-ai", "cloudflare-ai-gateway":
1271 return "CLOUDFLARE"
1272 case "amazon-bedrock":
1273 return "AWS"
1274 case "google-vertex":
1275 return "GOOGLE_CLOUD"
1276 case "azure-openai-responses":
1277 return "AZURE_OPENAI"
1278 default:
1279 return "ANTHROPIC"
1280 }
1281}