Merge branch 'main' into discuss-extension-submit-frame
9 files · +191 -37
M go.mod +2 -2
| @@ -1,13 +1,13 @@ | |||
| 1 | 1 | module github.com/patriceckhart/zot | |
| 2 | 2 | ||
| 3 | - | go 1.22 | |
| 3 | + | go 1.25.0 | |
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | 6 | github.com/alecthomas/chroma/v2 v2.23.1 | |
| 7 | 7 | github.com/google/uuid v1.6.0 | |
| 8 | 8 | github.com/mattn/go-runewidth v0.0.16 | |
| 9 | 9 | github.com/sahilm/fuzzy v0.1.1 | |
| 10 | - | golang.org/x/image v0.18.0 | |
| 10 | + | golang.org/x/image v0.43.0 | |
| 11 | 11 | golang.org/x/sys v0.26.0 | |
| 12 | 12 | golang.org/x/term v0.25.0 | |
| 13 | 13 | ) | |
| @@ -1,13 +1,13 @@ | |||||
| 1 | module github.com/patriceckhart/zot | 1 | module github.com/patriceckhart/zot | ||
| 2 | 2 | ||||
| 3 | - | go 1.22 | 3 | + | go 1.25.0 |
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | github.com/alecthomas/chroma/v2 v2.23.1 | 6 | github.com/alecthomas/chroma/v2 v2.23.1 | ||
| 7 | github.com/google/uuid v1.6.0 | 7 | github.com/google/uuid v1.6.0 | ||
| 8 | github.com/mattn/go-runewidth v0.0.16 | 8 | github.com/mattn/go-runewidth v0.0.16 | ||
| 9 | github.com/sahilm/fuzzy v0.1.1 | 9 | github.com/sahilm/fuzzy v0.1.1 | ||
| 10 | - | golang.org/x/image v0.18.0 | 10 | + | golang.org/x/image v0.43.0 |
| 11 | golang.org/x/sys v0.26.0 | 11 | golang.org/x/sys v0.26.0 | ||
| 12 | golang.org/x/term v0.25.0 | 12 | golang.org/x/term v0.25.0 | ||
| 13 | ) | 13 | ) | ||
M go.sum +2 -2
| @@ -18,8 +18,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= | |||
| 18 | 18 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | |
| 19 | 19 | github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= | |
| 20 | 20 | github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= | |
| 21 | - | golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= | |
| 22 | - | golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= | |
| 21 | + | golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY= | |
| 22 | + | golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q= | |
| 23 | 23 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= | |
| 24 | 24 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | |
| 25 | 25 | golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= | |
| @@ -18,8 +18,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= | |||||
| 18 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | 18 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||
| 19 | github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= | 19 | github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= | ||
| 20 | github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= | 20 | github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= | ||
| 21 | - | golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= | 21 | + | golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY= |
| 22 | - | golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= | 22 | + | golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q= |
| 23 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= | 23 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= | ||
| 24 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | 24 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||
| 25 | golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= | 25 | golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= | ||
M packages/agent/build.go +69 -29
| @@ -348,12 +348,28 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 348 | 348 | } | |
| 349 | 349 | } | |
| 350 | 350 | ||
| 351 | - | // If the user did NOT explicitly pick a provider and the default one | |
| 352 | - | // has no credentials, auto-fall-back to whichever provider is actually | |
| 353 | - | // logged in. That way running plain `zot` after `/login` (any provider) | |
| 354 | - | // never shows a "not logged in" banner. | |
| 355 | - | userPickedProvider := args.Provider != "" | |
| 351 | + | // If the user did NOT explicitly pick a provider (neither via --provider | |
| 352 | + | // nor by saving one in config.json) and the default one has no | |
| 353 | + | // credentials, auto-fall-back to whichever provider is actually logged | |
| 354 | + | // in. That way running plain `zot` after `/login` (any provider) never | |
| 355 | + | // shows a "not logged in" banner. | |
| 356 | + | // | |
| 357 | + | // Critical: when the user HAS saved a provider in config.json (e.g. | |
| 358 | + | // "deepseek" with "deepseek-v4-flash"), that choice is respected even | |
| 359 | + | // if no credential is found — the tui will show the login dialog or | |
| 360 | + | // surface a clear credential error rather than silently switching to | |
| 361 | + | // a completely different provider and model. | |
| 362 | + | // autoFellBack tracks whether the provider was changed by the | |
| 363 | + | // auto-fallback loop (no explicit provider from CLI or config, | |
| 364 | + | // and the default had no credentials). When false, the provider | |
| 365 | + | // came from the user's explicit choice (--provider or config.json) | |
| 366 | + | // and must be respected, including any model id that may belong | |
| 367 | + | // to a different provider in the catalog — for example when using | |
| 368 | + | // a gateway/router like OpenRouter with "deepseek/deepseek-v4-flash". | |
| 369 | + | var autoFellBack bool | |
| 370 | + | userPickedProvider := args.Provider != "" || cfg.Provider != "" | |
| 356 | 371 | if credErr != nil && !userPickedProvider && provName != "ollama" { | |
| 372 | + | autoFellBack = true | |
| 357 | 373 | // Scan every known provider (not a hardcoded subset) so any | |
| 358 | 374 | // env-based credential is discovered, e.g. an env-only | |
| 359 | 375 | // amazon-bedrock setup (AWS_BEARER_TOKEN_BEDROCK / AWS_PROFILE / | |
| @@ -381,8 +397,12 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 381 | 397 | model = defaultModelForProvider(provName) | |
| 382 | 398 | } | |
| 383 | 399 | // If the resolved model belongs to a different provider (e.g. config | |
| 384 | - | // says gpt-5 but we fell back to anthropic), pick that provider's default. | |
| 385 | - | if provName != "ollama" { | |
| 400 | + | // says gpt-5 but we auto-fell back to anthropic), pick that provider's | |
| 401 | + | // default. This override only fires when the provider was auto-fallback- | |
| 402 | + | // changed, NOT when the user explicitly configured the pair — gateway/ | |
| 403 | + | // router providers (openrouter, vercel-ai-gateway, etc.) can serve | |
| 404 | + | // models from any provider in the catalog. | |
| 405 | + | if autoFellBack && provName != "ollama" { | |
| 386 | 406 | if m, err := provider.FindModel("", model); err == nil && m.Provider != provName { | |
| 387 | 407 | model = defaultModelForProvider(provName) | |
| 388 | 408 | } | |
| @@ -416,7 +436,8 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 416 | 436 | } | |
| 417 | 437 | err = nil | |
| 418 | 438 | } | |
| 419 | - | } else if cfg, ok := provider.CustomProviders()[provName]; ok && resolvedModel.BaseURL == "" && cfg.BaseURL != "" { | |
| 439 | + | } | |
| 440 | + | if cfg, ok := provider.CustomProviders()[provName]; ok && err == nil && resolvedModel.BaseURL == "" && cfg.BaseURL != "" { | |
| 420 | 441 | // Fall back to the provider-level base URL when the model does | |
| 421 | 442 | // not define its own endpoint. | |
| 422 | 443 | resolvedModel.BaseURL = cfg.BaseURL | |
| @@ -431,29 +452,48 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 431 | 452 | // when the stale id came from the persisted config (not an | |
| 432 | 453 | // explicit --model flag), repair the config so the warning | |
| 433 | 454 | // doesn't repeat on every launch. | |
| 434 | - | fallback := defaultModelForProvider(provName) | |
| 435 | - | fm, ferr := provider.FindModel(provName, fallback) | |
| 436 | - | if ferr != nil { | |
| 437 | - | // Even the provider default is gone (catastrophic | |
| 438 | - | // catalogue trim). Last resort: any model on this | |
| 439 | - | // provider, then the global DefaultModel. | |
| 440 | - | if candidates := provider.ModelsForProvider(provName); len(candidates) > 0 { | |
| 441 | - | fm = candidates[0] | |
| 442 | - | ferr = nil | |
| 443 | - | } else { | |
| 444 | - | fm = provider.DefaultModel | |
| 445 | - | ferr = nil | |
| 455 | + | ||
| 456 | + | // Gateway providers can accept route-qualified ids that are not in | |
| 457 | + | // zot's local catalog yet, for example OpenRouter's | |
| 458 | + | // "deepseek/deepseek-v4-flash". Preserve only route-qualified ids; | |
| 459 | + | // plain unknown values are likely typos and should still fall back. | |
| 460 | + | if isGatewayProvider(provName) && isGatewayRoutedModelID(model) { | |
| 461 | + | resolvedModel = provider.Model{ | |
| 462 | + | Provider: provName, | |
| 463 | + | ID: model, | |
| 464 | + | DisplayName: model, | |
| 465 | + | ContextWindow: 1000000, | |
| 466 | + | MaxOutput: 64000, | |
| 467 | + | BaseURL: "", | |
| 468 | + | Source: "gateway", | |
| 446 | 469 | } | |
| 470 | + | err = nil | |
| 471 | + | // Don't repair config — the routed model id may be valid upstream. | |
| 472 | + | } else { | |
| 473 | + | fallback := defaultModelForProvider(provName) | |
| 474 | + | fm, ferr := provider.FindModel(provName, fallback) | |
| 475 | + | if ferr != nil { | |
| 476 | + | // Even the provider default is gone (catastrophic | |
| 477 | + | // catalogue trim). Last resort: any model on this | |
| 478 | + | // provider, then the global DefaultModel. | |
| 479 | + | if candidates := provider.ModelsForProvider(provName); len(candidates) > 0 { | |
| 480 | + | fm = candidates[0] | |
| 481 | + | ferr = nil | |
| 482 | + | } else { | |
| 483 | + | fm = provider.DefaultModel | |
| 484 | + | ferr = nil | |
| 485 | + | } | |
| 486 | + | } | |
| 487 | + | fmt.Fprintf(os.Stderr, | |
| 488 | + | "zot: model %q is not in the active catalogue; using %q instead. Pick a different model with --model or /model.\n", | |
| 489 | + | model, fm.ID) | |
| 490 | + | if args.Model == "" && cfg.Model == model { | |
| 491 | + | cfg.Model = fm.ID | |
| 492 | + | _ = SaveConfig(cfg) | |
| 493 | + | } | |
| 494 | + | resolvedModel = fm | |
| 495 | + | model = fm.ID | |
| 447 | 496 | } | |
| 448 | - | fmt.Fprintf(os.Stderr, | |
| 449 | - | "zot: model %q is not in the active catalogue; using %q instead. Pick a different model with --model or /model.\n", | |
| 450 | - | model, fm.ID) | |
| 451 | - | if args.Model == "" && cfg.Model == model { | |
| 452 | - | cfg.Model = fm.ID | |
| 453 | - | _ = SaveConfig(cfg) | |
| 454 | - | } | |
| 455 | - | resolvedModel = fm | |
| 456 | - | model = fm.ID | |
| 457 | 497 | } | |
| 458 | 498 | ||
| 459 | 499 | explicitBaseURL := args.BaseURL != "" || (resolvedModel.Source == "user" && resolvedModel.BaseURL != "") | |
| @@ -348,12 +348,28 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 348 | } | 348 | } | ||
| 349 | } | 349 | } | ||
| 350 | 350 | ||||
| 351 | - | // If the user did NOT explicitly pick a provider and the default one | 351 | + | // If the user did NOT explicitly pick a provider (neither via --provider |
| 352 | - | // has no credentials, auto-fall-back to whichever provider is actually | 352 | + | // nor by saving one in config.json) and the default one has no |
| 353 | - | // logged in. That way running plain `zot` after `/login` (any provider) | 353 | + | // credentials, auto-fall-back to whichever provider is actually logged |
| 354 | - | // never shows a "not logged in" banner. | 354 | + | // in. That way running plain `zot` after `/login` (any provider) never |
| 355 | - | userPickedProvider := args.Provider != "" | 355 | + | // shows a "not logged in" banner. |
| 356 | + | // | |||
| 357 | + | // Critical: when the user HAS saved a provider in config.json (e.g. | |||
| 358 | + | // "deepseek" with "deepseek-v4-flash"), that choice is respected even | |||
| 359 | + | // if no credential is found — the tui will show the login dialog or | |||
| 360 | + | // surface a clear credential error rather than silently switching to | |||
| 361 | + | // a completely different provider and model. | |||
| 362 | + | // autoFellBack tracks whether the provider was changed by the | |||
| 363 | + | // auto-fallback loop (no explicit provider from CLI or config, | |||
| 364 | + | // and the default had no credentials). When false, the provider | |||
| 365 | + | // came from the user's explicit choice (--provider or config.json) | |||
| 366 | + | // and must be respected, including any model id that may belong | |||
| 367 | + | // to a different provider in the catalog — for example when using | |||
| 368 | + | // a gateway/router like OpenRouter with "deepseek/deepseek-v4-flash". | |||
| 369 | + | var autoFellBack bool | |||
| 370 | + | userPickedProvider := args.Provider != "" || cfg.Provider != "" | |||
| 356 | if credErr != nil && !userPickedProvider && provName != "ollama" { | 371 | if credErr != nil && !userPickedProvider && provName != "ollama" { | ||
| 372 | + | autoFellBack = true | |||
| 357 | // Scan every known provider (not a hardcoded subset) so any | 373 | // Scan every known provider (not a hardcoded subset) so any | ||
| 358 | // env-based credential is discovered, e.g. an env-only | 374 | // env-based credential is discovered, e.g. an env-only | ||
| 359 | // amazon-bedrock setup (AWS_BEARER_TOKEN_BEDROCK / AWS_PROFILE / | 375 | // amazon-bedrock setup (AWS_BEARER_TOKEN_BEDROCK / AWS_PROFILE / | ||
| @@ -381,8 +397,12 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 381 | model = defaultModelForProvider(provName) | 397 | model = defaultModelForProvider(provName) | ||
| 382 | } | 398 | } | ||
| 383 | // If the resolved model belongs to a different provider (e.g. config | 399 | // If the resolved model belongs to a different provider (e.g. config | ||
| 384 | - | // says gpt-5 but we fell back to anthropic), pick that provider's default. | 400 | + | // says gpt-5 but we auto-fell back to anthropic), pick that provider's |
| 385 | - | if provName != "ollama" { | 401 | + | // default. This override only fires when the provider was auto-fallback- |
| 402 | + | // changed, NOT when the user explicitly configured the pair — gateway/ | |||
| 403 | + | // router providers (openrouter, vercel-ai-gateway, etc.) can serve | |||
| 404 | + | // models from any provider in the catalog. | |||
| 405 | + | if autoFellBack && provName != "ollama" { | |||
| 386 | if m, err := provider.FindModel("", model); err == nil && m.Provider != provName { | 406 | if m, err := provider.FindModel("", model); err == nil && m.Provider != provName { | ||
| 387 | model = defaultModelForProvider(provName) | 407 | model = defaultModelForProvider(provName) | ||
| 388 | } | 408 | } | ||
| @@ -416,7 +436,8 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 416 | } | 436 | } | ||
| 417 | err = nil | 437 | err = nil | ||
| 418 | } | 438 | } | ||
| 419 | - | } else if cfg, ok := provider.CustomProviders()[provName]; ok && resolvedModel.BaseURL == "" && cfg.BaseURL != "" { | 439 | + | } |
| 440 | + | if cfg, ok := provider.CustomProviders()[provName]; ok && err == nil && resolvedModel.BaseURL == "" && cfg.BaseURL != "" { | |||
| 420 | // Fall back to the provider-level base URL when the model does | 441 | // Fall back to the provider-level base URL when the model does | ||
| 421 | // not define its own endpoint. | 442 | // not define its own endpoint. | ||
| 422 | resolvedModel.BaseURL = cfg.BaseURL | 443 | resolvedModel.BaseURL = cfg.BaseURL | ||
| @@ -431,29 +452,48 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 431 | // when the stale id came from the persisted config (not an | 452 | // when the stale id came from the persisted config (not an | ||
| 432 | // explicit --model flag), repair the config so the warning | 453 | // explicit --model flag), repair the config so the warning | ||
| 433 | // doesn't repeat on every launch. | 454 | // doesn't repeat on every launch. | ||
| 434 | - | fallback := defaultModelForProvider(provName) | 455 | + | |
| 435 | - | fm, ferr := provider.FindModel(provName, fallback) | 456 | + | // Gateway providers can accept route-qualified ids that are not in |
| 436 | - | if ferr != nil { | 457 | + | // zot's local catalog yet, for example OpenRouter's |
| 437 | - | // Even the provider default is gone (catastrophic | 458 | + | // "deepseek/deepseek-v4-flash". Preserve only route-qualified ids; |
| 438 | - | // catalogue trim). Last resort: any model on this | 459 | + | // plain unknown values are likely typos and should still fall back. |
| 439 | - | // provider, then the global DefaultModel. | 460 | + | if isGatewayProvider(provName) && isGatewayRoutedModelID(model) { |
| 440 | - | if candidates := provider.ModelsForProvider(provName); len(candidates) > 0 { | 461 | + | resolvedModel = provider.Model{ |
| 441 | - | fm = candidates[0] | 462 | + | Provider: provName, |
| 442 | - | ferr = nil | 463 | + | ID: model, |
| 443 | - | } else { | 464 | + | DisplayName: model, |
| 444 | - | fm = provider.DefaultModel | 465 | + | ContextWindow: 1000000, |
| 445 | - | ferr = nil | 466 | + | MaxOutput: 64000, |
| 467 | + | BaseURL: "", | |||
| 468 | + | Source: "gateway", | |||
| 446 | } | 469 | } | ||
| 470 | + | err = nil | |||
| 471 | + | // Don't repair config — the routed model id may be valid upstream. | |||
| 472 | + | } else { | |||
| 473 | + | fallback := defaultModelForProvider(provName) | |||
| 474 | + | fm, ferr := provider.FindModel(provName, fallback) | |||
| 475 | + | if ferr != nil { | |||
| 476 | + | // Even the provider default is gone (catastrophic | |||
| 477 | + | // catalogue trim). Last resort: any model on this | |||
| 478 | + | // provider, then the global DefaultModel. | |||
| 479 | + | if candidates := provider.ModelsForProvider(provName); len(candidates) > 0 { | |||
| 480 | + | fm = candidates[0] | |||
| 481 | + | ferr = nil | |||
| 482 | + | } else { | |||
| 483 | + | fm = provider.DefaultModel | |||
| 484 | + | ferr = nil | |||
| 485 | + | } | |||
| 486 | + | } | |||
| 487 | + | fmt.Fprintf(os.Stderr, | |||
| 488 | + | "zot: model %q is not in the active catalogue; using %q instead. Pick a different model with --model or /model.\n", | |||
| 489 | + | model, fm.ID) | |||
| 490 | + | if args.Model == "" && cfg.Model == model { | |||
| 491 | + | cfg.Model = fm.ID | |||
| 492 | + | _ = SaveConfig(cfg) | |||
| 493 | + | } | |||
| 494 | + | resolvedModel = fm | |||
| 495 | + | model = fm.ID | |||
| 447 | } | 496 | } | ||
| 448 | - | fmt.Fprintf(os.Stderr, | |||
| 449 | - | "zot: model %q is not in the active catalogue; using %q instead. Pick a different model with --model or /model.\n", | |||
| 450 | - | model, fm.ID) | |||
| 451 | - | if args.Model == "" && cfg.Model == model { | |||
| 452 | - | cfg.Model = fm.ID | |||
| 453 | - | _ = SaveConfig(cfg) | |||
| 454 | - | } | |||
| 455 | - | resolvedModel = fm | |||
| 456 | - | model = fm.ID | |||
| 457 | } | 497 | } | ||
| 458 | 498 | ||||
| 459 | explicitBaseURL := args.BaseURL != "" || (resolvedModel.Source == "user" && resolvedModel.BaseURL != "") | 499 | explicitBaseURL := args.BaseURL != "" || (resolvedModel.Source == "user" && resolvedModel.BaseURL != "") | ||
M packages/agent/build_test.go +43 -0
| @@ -112,6 +112,49 @@ func TestResolveExplicitFlagStaleDoesNotRepairConfig(t *testing.T) { | |||
| 112 | 112 | } | |
| 113 | 113 | } | |
| 114 | 114 | ||
| 115 | + | func TestResolveOpenRouterPreservesSavedRoutedModelID(t *testing.T) { | |
| 116 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 117 | + | t.Setenv("OPENROUTER_API_KEY", "test-key") | |
| 118 | + | want := "deepseek/deepseek-v4-flash" | |
| 119 | + | if err := SaveConfig(Config{Provider: "openrouter", Model: want}); err != nil { | |
| 120 | + | t.Fatal(err) | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | r, err := Resolve(Args{}, true) | |
| 124 | + | if err != nil { | |
| 125 | + | t.Fatalf("Resolve failed: %v", err) | |
| 126 | + | } | |
| 127 | + | if r.Provider != "openrouter" { | |
| 128 | + | t.Fatalf("provider = %q, want openrouter", r.Provider) | |
| 129 | + | } | |
| 130 | + | if r.Model != want { | |
| 131 | + | t.Fatalf("model = %q, want %q", r.Model, want) | |
| 132 | + | } | |
| 133 | + | if r.MaxOutput != 64000 { | |
| 134 | + | t.Fatalf("MaxOutput = %d, want synthetic gateway default 64000", r.MaxOutput) | |
| 135 | + | } | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | func TestResolveGatewayPlainUnknownModelFallsBack(t *testing.T) { | |
| 139 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 140 | + | t.Setenv("OPENROUTER_API_KEY", "test-key") | |
| 141 | + | stale := "not-a-routed-model" | |
| 142 | + | if err := SaveConfig(Config{Provider: "openrouter", Model: stale}); err != nil { | |
| 143 | + | t.Fatal(err) | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | r, err := Resolve(Args{}, true) | |
| 147 | + | if err != nil { | |
| 148 | + | t.Fatalf("Resolve failed: %v", err) | |
| 149 | + | } | |
| 150 | + | if r.Provider != "openrouter" { | |
| 151 | + | t.Fatalf("provider = %q, want openrouter", r.Provider) | |
| 152 | + | } | |
| 153 | + | if r.Model == stale || r.Model == "" { | |
| 154 | + | t.Fatalf("model = %q, want repaired gateway default", r.Model) | |
| 155 | + | } | |
| 156 | + | } | |
| 157 | + | ||
| 115 | 158 | // TestResolveEnvOnlyBedrockDiscoveredWithoutConfig reproduces issue | |
| 116 | 159 | // #15: pointing ZOT_HOME at a fresh dir drops the persisted | |
| 117 | 160 | // config.json (which pinned provider=amazon-bedrock). Resolve must | |
| @@ -112,6 +112,49 @@ func TestResolveExplicitFlagStaleDoesNotRepairConfig(t *testing.T) { | |||||
| 112 | } | 112 | } | ||
| 113 | } | 113 | } | ||
| 114 | 114 | ||||
| 115 | + | func TestResolveOpenRouterPreservesSavedRoutedModelID(t *testing.T) { | |||
| 116 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |||
| 117 | + | t.Setenv("OPENROUTER_API_KEY", "test-key") | |||
| 118 | + | want := "deepseek/deepseek-v4-flash" | |||
| 119 | + | if err := SaveConfig(Config{Provider: "openrouter", Model: want}); err != nil { | |||
| 120 | + | t.Fatal(err) | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | r, err := Resolve(Args{}, true) | |||
| 124 | + | if err != nil { | |||
| 125 | + | t.Fatalf("Resolve failed: %v", err) | |||
| 126 | + | } | |||
| 127 | + | if r.Provider != "openrouter" { | |||
| 128 | + | t.Fatalf("provider = %q, want openrouter", r.Provider) | |||
| 129 | + | } | |||
| 130 | + | if r.Model != want { | |||
| 131 | + | t.Fatalf("model = %q, want %q", r.Model, want) | |||
| 132 | + | } | |||
| 133 | + | if r.MaxOutput != 64000 { | |||
| 134 | + | t.Fatalf("MaxOutput = %d, want synthetic gateway default 64000", r.MaxOutput) | |||
| 135 | + | } | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | func TestResolveGatewayPlainUnknownModelFallsBack(t *testing.T) { | |||
| 139 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |||
| 140 | + | t.Setenv("OPENROUTER_API_KEY", "test-key") | |||
| 141 | + | stale := "not-a-routed-model" | |||
| 142 | + | if err := SaveConfig(Config{Provider: "openrouter", Model: stale}); err != nil { | |||
| 143 | + | t.Fatal(err) | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | r, err := Resolve(Args{}, true) | |||
| 147 | + | if err != nil { | |||
| 148 | + | t.Fatalf("Resolve failed: %v", err) | |||
| 149 | + | } | |||
| 150 | + | if r.Provider != "openrouter" { | |||
| 151 | + | t.Fatalf("provider = %q, want openrouter", r.Provider) | |||
| 152 | + | } | |||
| 153 | + | if r.Model == stale || r.Model == "" { | |||
| 154 | + | t.Fatalf("model = %q, want repaired gateway default", r.Model) | |||
| 155 | + | } | |||
| 156 | + | } | |||
| 157 | + | ||||
| 115 | // TestResolveEnvOnlyBedrockDiscoveredWithoutConfig reproduces issue | 158 | // TestResolveEnvOnlyBedrockDiscoveredWithoutConfig reproduces issue | ||
| 116 | // #15: pointing ZOT_HOME at a fresh dir drops the persisted | 159 | // #15: pointing ZOT_HOME at a fresh dir drops the persisted | ||
| 117 | // config.json (which pinned provider=amazon-bedrock). Resolve must | 160 | // config.json (which pinned provider=amazon-bedrock). Resolve must | ||
M packages/agent/modelsync.go +32 -1
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "fmt" | |
| 6 | 6 | "os" | |
| 7 | 7 | "path/filepath" | |
| 8 | + | "strings" | |
| 8 | 9 | "time" | |
| 9 | 10 | ||
| 10 | 11 | "github.com/patriceckhart/zot/packages/provider" | |
| @@ -46,6 +47,28 @@ func LoadUserModels() { | |||
| 46 | 47 | provider.SetUserModels(models) | |
| 47 | 48 | } | |
| 48 | 49 | ||
| 50 | + | // isGatewayProvider returns true for providers whose OpenAI-compatible | |
| 51 | + | // endpoint can accept routed model IDs that are not present in zot's local | |
| 52 | + | // catalog. Vercel AI Gateway is intentionally not listed here: zot currently | |
| 53 | + | // talks to it through the Anthropic-compatible client, which still requires | |
| 54 | + | // catalog metadata for request shaping. | |
| 55 | + | func isGatewayProvider(prov string) bool { | |
| 56 | + | switch prov { | |
| 57 | + | case "openrouter", "cloudflare-ai-gateway": | |
| 58 | + | return true | |
| 59 | + | default: | |
| 60 | + | return false | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // isGatewayRoutedModelID reports whether a model looks like the routed IDs | |
| 65 | + | // used by gateway providers, for example "deepseek/deepseek-v4-flash". | |
| 66 | + | // Non-routed typos like "deepseek-v4-flashh" should still be repaired to a | |
| 67 | + | // known default instead of being silently accepted. | |
| 68 | + | func isGatewayRoutedModelID(model string) bool { | |
| 69 | + | return strings.Contains(strings.TrimSpace(model), "/") | |
| 70 | + | } | |
| 71 | + | ||
| 49 | 72 | // ValidateAndRepairConfig checks the persisted config.json's | |
| 50 | 73 | // (Provider, Model) pair against the active catalog and repairs any | |
| 51 | 74 | // mismatch in-place (and on disk) before any UI renders. Three failure | |
| @@ -57,6 +80,9 @@ func LoadUserModels() { | |||
| 57 | 80 | // (e.g. provider=anthropic + model=kimi-for-coding from a stale | |
| 58 | 81 | // half-applied switch) -> reset model to the provider's default. | |
| 59 | 82 | // | |
| 83 | + | // Gateway providers are exempt from the cross-provider model check for routed | |
| 84 | + | // model IDs because those IDs can be valid even when absent from zot's catalog. | |
| 85 | + | // | |
| 60 | 86 | // Silent on success; one stderr line per repair. Errors loading or | |
| 61 | 87 | // saving the file are non-fatal — the caller continues with defaults. | |
| 62 | 88 | func ValidateAndRepairConfig() { | |
| @@ -76,7 +102,12 @@ func ValidateAndRepairConfig() { | |||
| 76 | 102 | ||
| 77 | 103 | if cfg.Provider != "" && cfg.Model != "" { | |
| 78 | 104 | if _, err := provider.FindModel(cfg.Provider, cfg.Model); err != nil { | |
| 79 | - | if m, err := provider.FindModel("", cfg.Model); err == nil { | |
| 105 | + | // Gateway providers can serve routed model ids like | |
| 106 | + | // "deepseek/deepseek-v4-flash" even when the local catalog does not | |
| 107 | + | // know them. Preserve only routed ids; plain typos are still repaired. | |
| 108 | + | if isGatewayProvider(cfg.Provider) && isGatewayRoutedModelID(cfg.Model) { | |
| 109 | + | // Provider is a router and the id is route-qualified; preserve it. | |
| 110 | + | } else if m, err := provider.FindModel("", cfg.Model); err == nil { | |
| 80 | 111 | fix := defaultModelForProvider(cfg.Provider) | |
| 81 | 112 | fmt.Fprintf(os.Stderr, | |
| 82 | 113 | "zot: config.json: model %q belongs to provider %q (config has provider=%q); switched model to %q\n", | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "os" | 6 | "os" | ||
| 7 | "path/filepath" | 7 | "path/filepath" | ||
| 8 | + | "strings" | |||
| 8 | "time" | 9 | "time" | ||
| 9 | 10 | ||||
| 10 | "github.com/patriceckhart/zot/packages/provider" | 11 | "github.com/patriceckhart/zot/packages/provider" | ||
| @@ -46,6 +47,28 @@ func LoadUserModels() { | |||||
| 46 | provider.SetUserModels(models) | 47 | provider.SetUserModels(models) | ||
| 47 | } | 48 | } | ||
| 48 | 49 | ||||
| 50 | + | // isGatewayProvider returns true for providers whose OpenAI-compatible | |||
| 51 | + | // endpoint can accept routed model IDs that are not present in zot's local | |||
| 52 | + | // catalog. Vercel AI Gateway is intentionally not listed here: zot currently | |||
| 53 | + | // talks to it through the Anthropic-compatible client, which still requires | |||
| 54 | + | // catalog metadata for request shaping. | |||
| 55 | + | func isGatewayProvider(prov string) bool { | |||
| 56 | + | switch prov { | |||
| 57 | + | case "openrouter", "cloudflare-ai-gateway": | |||
| 58 | + | return true | |||
| 59 | + | default: | |||
| 60 | + | return false | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // isGatewayRoutedModelID reports whether a model looks like the routed IDs | |||
| 65 | + | // used by gateway providers, for example "deepseek/deepseek-v4-flash". | |||
| 66 | + | // Non-routed typos like "deepseek-v4-flashh" should still be repaired to a | |||
| 67 | + | // known default instead of being silently accepted. | |||
| 68 | + | func isGatewayRoutedModelID(model string) bool { | |||
| 69 | + | return strings.Contains(strings.TrimSpace(model), "/") | |||
| 70 | + | } | |||
| 71 | + | ||||
| 49 | // ValidateAndRepairConfig checks the persisted config.json's | 72 | // ValidateAndRepairConfig checks the persisted config.json's | ||
| 50 | // (Provider, Model) pair against the active catalog and repairs any | 73 | // (Provider, Model) pair against the active catalog and repairs any | ||
| 51 | // mismatch in-place (and on disk) before any UI renders. Three failure | 74 | // mismatch in-place (and on disk) before any UI renders. Three failure | ||
| @@ -57,6 +80,9 @@ func LoadUserModels() { | |||||
| 57 | // (e.g. provider=anthropic + model=kimi-for-coding from a stale | 80 | // (e.g. provider=anthropic + model=kimi-for-coding from a stale | ||
| 58 | // half-applied switch) -> reset model to the provider's default. | 81 | // half-applied switch) -> reset model to the provider's default. | ||
| 59 | // | 82 | // | ||
| 83 | + | // Gateway providers are exempt from the cross-provider model check for routed | |||
| 84 | + | // model IDs because those IDs can be valid even when absent from zot's catalog. | |||
| 85 | + | // | |||
| 60 | // Silent on success; one stderr line per repair. Errors loading or | 86 | // Silent on success; one stderr line per repair. Errors loading or | ||
| 61 | // saving the file are non-fatal — the caller continues with defaults. | 87 | // saving the file are non-fatal — the caller continues with defaults. | ||
| 62 | func ValidateAndRepairConfig() { | 88 | func ValidateAndRepairConfig() { | ||
| @@ -76,7 +102,12 @@ func ValidateAndRepairConfig() { | |||||
| 76 | 102 | ||||
| 77 | if cfg.Provider != "" && cfg.Model != "" { | 103 | if cfg.Provider != "" && cfg.Model != "" { | ||
| 78 | if _, err := provider.FindModel(cfg.Provider, cfg.Model); err != nil { | 104 | if _, err := provider.FindModel(cfg.Provider, cfg.Model); err != nil { | ||
| 79 | - | if m, err := provider.FindModel("", cfg.Model); err == nil { | 105 | + | // Gateway providers can serve routed model ids like |
| 106 | + | // "deepseek/deepseek-v4-flash" even when the local catalog does not | |||
| 107 | + | // know them. Preserve only routed ids; plain typos are still repaired. | |||
| 108 | + | if isGatewayProvider(cfg.Provider) && isGatewayRoutedModelID(cfg.Model) { | |||
| 109 | + | // Provider is a router and the id is route-qualified; preserve it. | |||
| 110 | + | } else if m, err := provider.FindModel("", cfg.Model); err == nil { | |||
| 80 | fix := defaultModelForProvider(cfg.Provider) | 111 | fix := defaultModelForProvider(cfg.Provider) | ||
| 81 | fmt.Fprintf(os.Stderr, | 112 | fmt.Fprintf(os.Stderr, | ||
| 82 | "zot: config.json: model %q belongs to provider %q (config has provider=%q); switched model to %q\n", | 113 | "zot: config.json: model %q belongs to provider %q (config has provider=%q); switched model to %q\n", | ||
M packages/agent/modelsync_test.go +37 -0
| @@ -101,6 +101,43 @@ func TestValidateAndRepairConfig_DuplicateModelIDValidForConfiguredProvider(t *t | |||
| 101 | 101 | } | |
| 102 | 102 | } | |
| 103 | 103 | ||
| 104 | + | func TestValidateAndRepairConfig_OpenRouterPreservesRoutedModelID(t *testing.T) { | |
| 105 | + | home := t.TempDir() | |
| 106 | + | t.Setenv("ZOT_HOME", home) | |
| 107 | + | ||
| 108 | + | want := "deepseek/deepseek-v4-flash" | |
| 109 | + | b, _ := json.Marshal(Config{Provider: "openrouter", Model: want}) | |
| 110 | + | _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644) | |
| 111 | + | ||
| 112 | + | ValidateAndRepairConfig() | |
| 113 | + | ||
| 114 | + | out, _ := LoadConfig() | |
| 115 | + | if out.Provider != "openrouter" { | |
| 116 | + | t.Errorf("provider mutated: %q", out.Provider) | |
| 117 | + | } | |
| 118 | + | if out.Model != want { | |
| 119 | + | t.Errorf("routed model id mutated: got %q, want %q", out.Model, want) | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | func TestValidateAndRepairConfig_GatewayPlainUnknownModelStillRepairs(t *testing.T) { | |
| 124 | + | home := t.TempDir() | |
| 125 | + | t.Setenv("ZOT_HOME", home) | |
| 126 | + | ||
| 127 | + | b, _ := json.Marshal(Config{Provider: "openrouter", Model: "not-a-routed-model"}) | |
| 128 | + | _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644) | |
| 129 | + | ||
| 130 | + | ValidateAndRepairConfig() | |
| 131 | + | ||
| 132 | + | out, _ := LoadConfig() | |
| 133 | + | if out.Provider != "openrouter" { | |
| 134 | + | t.Errorf("provider mutated: %q", out.Provider) | |
| 135 | + | } | |
| 136 | + | if out.Model == "not-a-routed-model" || out.Model == "" { | |
| 137 | + | t.Errorf("plain unknown gateway model was not repaired: %q", out.Model) | |
| 138 | + | } | |
| 139 | + | } | |
| 140 | + | ||
| 104 | 141 | // TestValidateAndRepairConfig_HappyPath leaves a valid config alone. | |
| 105 | 142 | func TestValidateAndRepairConfig_HappyPath(t *testing.T) { | |
| 106 | 143 | home := t.TempDir() | |
| @@ -101,6 +101,43 @@ func TestValidateAndRepairConfig_DuplicateModelIDValidForConfiguredProvider(t *t | |||||
| 101 | } | 101 | } | ||
| 102 | } | 102 | } | ||
| 103 | 103 | ||||
| 104 | + | func TestValidateAndRepairConfig_OpenRouterPreservesRoutedModelID(t *testing.T) { | |||
| 105 | + | home := t.TempDir() | |||
| 106 | + | t.Setenv("ZOT_HOME", home) | |||
| 107 | + | ||||
| 108 | + | want := "deepseek/deepseek-v4-flash" | |||
| 109 | + | b, _ := json.Marshal(Config{Provider: "openrouter", Model: want}) | |||
| 110 | + | _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644) | |||
| 111 | + | ||||
| 112 | + | ValidateAndRepairConfig() | |||
| 113 | + | ||||
| 114 | + | out, _ := LoadConfig() | |||
| 115 | + | if out.Provider != "openrouter" { | |||
| 116 | + | t.Errorf("provider mutated: %q", out.Provider) | |||
| 117 | + | } | |||
| 118 | + | if out.Model != want { | |||
| 119 | + | t.Errorf("routed model id mutated: got %q, want %q", out.Model, want) | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | func TestValidateAndRepairConfig_GatewayPlainUnknownModelStillRepairs(t *testing.T) { | |||
| 124 | + | home := t.TempDir() | |||
| 125 | + | t.Setenv("ZOT_HOME", home) | |||
| 126 | + | ||||
| 127 | + | b, _ := json.Marshal(Config{Provider: "openrouter", Model: "not-a-routed-model"}) | |||
| 128 | + | _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644) | |||
| 129 | + | ||||
| 130 | + | ValidateAndRepairConfig() | |||
| 131 | + | ||||
| 132 | + | out, _ := LoadConfig() | |||
| 133 | + | if out.Provider != "openrouter" { | |||
| 134 | + | t.Errorf("provider mutated: %q", out.Provider) | |||
| 135 | + | } | |||
| 136 | + | if out.Model == "not-a-routed-model" || out.Model == "" { | |||
| 137 | + | t.Errorf("plain unknown gateway model was not repaired: %q", out.Model) | |||
| 138 | + | } | |||
| 139 | + | } | |||
| 140 | + | ||||
| 104 | // TestValidateAndRepairConfig_HappyPath leaves a valid config alone. | 141 | // TestValidateAndRepairConfig_HappyPath leaves a valid config alone. | ||
| 105 | func TestValidateAndRepairConfig_HappyPath(t *testing.T) { | 142 | func TestValidateAndRepairConfig_HappyPath(t *testing.T) { | ||
| 106 | home := t.TempDir() | 143 | home := t.TempDir() | ||
M packages/provider/auth/callback.go +2 -1
| @@ -2,6 +2,7 @@ package auth | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "errors" | |
| 5 | 6 | "fmt" | |
| 6 | 7 | "net" | |
| 7 | 8 | "net/http" | |
| @@ -91,7 +92,7 @@ func (cs *CallbackServer) handle(w http.ResponseWriter, r *http.Request) { | |||
| 91 | 92 | } | |
| 92 | 93 | w.WriteHeader(http.StatusBadRequest) | |
| 93 | 94 | _, _ = w.Write([]byte(oauthErrorHTML(msg))) | |
| 94 | - | cs.deliver(CallbackResult{Err: fmt.Errorf(msg), RawPath: r.URL.RequestURI()}) | |
| 95 | + | cs.deliver(CallbackResult{Err: errors.New(msg), RawPath: r.URL.RequestURI()}) | |
| 95 | 96 | return | |
| 96 | 97 | } | |
| 97 | 98 | code := q.Get("code") | |
| @@ -2,6 +2,7 @@ package auth | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "errors" | |||
| 5 | "fmt" | 6 | "fmt" | ||
| 6 | "net" | 7 | "net" | ||
| 7 | "net/http" | 8 | "net/http" | ||
| @@ -91,7 +92,7 @@ func (cs *CallbackServer) handle(w http.ResponseWriter, r *http.Request) { | |||||
| 91 | } | 92 | } | ||
| 92 | w.WriteHeader(http.StatusBadRequest) | 93 | w.WriteHeader(http.StatusBadRequest) | ||
| 93 | _, _ = w.Write([]byte(oauthErrorHTML(msg))) | 94 | _, _ = w.Write([]byte(oauthErrorHTML(msg))) | ||
| 94 | - | cs.deliver(CallbackResult{Err: fmt.Errorf(msg), RawPath: r.URL.RequestURI()}) | 95 | + | cs.deliver(CallbackResult{Err: errors.New(msg), RawPath: r.URL.RequestURI()}) |
| 95 | return | 96 | return | ||
| 96 | } | 97 | } | ||
| 97 | code := q.Get("code") | 98 | code := q.Get("code") | ||
M packages/provider/auth/manager.go +2 -1
| @@ -2,6 +2,7 @@ package auth | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "errors" | |
| 5 | 6 | "fmt" | |
| 6 | 7 | "net/url" | |
| 7 | 8 | "os" | |
| @@ -81,7 +82,7 @@ func (m *Manager) Close() { | |||
| 81 | 82 | // StartAPIKey launches the API-key login flow. | |
| 82 | 83 | func (m *Manager) StartAPIKey(provider string) (string, error) { | |
| 83 | 84 | if !isKnownAPIKeyProvider(provider) { | |
| 84 | - | return "", fmt.Errorf(apiKeyProviderMessage()) | |
| 85 | + | return "", errors.New(apiKeyProviderMessage()) | |
| 85 | 86 | } | |
| 86 | 87 | if err := m.ensureKeyServer(); err != nil { | |
| 87 | 88 | return "", err | |
| @@ -2,6 +2,7 @@ package auth | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "errors" | |||
| 5 | "fmt" | 6 | "fmt" | ||
| 6 | "net/url" | 7 | "net/url" | ||
| 7 | "os" | 8 | "os" | ||
| @@ -81,7 +82,7 @@ func (m *Manager) Close() { | |||||
| 81 | // StartAPIKey launches the API-key login flow. | 82 | // StartAPIKey launches the API-key login flow. | ||
| 82 | func (m *Manager) StartAPIKey(provider string) (string, error) { | 83 | func (m *Manager) StartAPIKey(provider string) (string, error) { | ||
| 83 | if !isKnownAPIKeyProvider(provider) { | 84 | if !isKnownAPIKeyProvider(provider) { | ||
| 84 | - | return "", fmt.Errorf(apiKeyProviderMessage()) | 85 | + | return "", errors.New(apiKeyProviderMessage()) |
| 85 | } | 86 | } | ||
| 86 | if err := m.ensureKeyServer(); err != nil { | 87 | if err := m.ensureKeyServer(); err != nil { | ||
| 87 | return "", err | 88 | return "", err | ||
M packages/provider/auth/server.go +2 -1
| @@ -2,6 +2,7 @@ package auth | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "errors" | |
| 5 | 6 | "fmt" | |
| 6 | 7 | "html/template" | |
| 7 | 8 | "net" | |
| @@ -204,7 +205,7 @@ func (s *Server) handleCallback(w http.ResponseWriter, r *http.Request) { | |||
| 204 | 205 | msg += ": " + d | |
| 205 | 206 | } | |
| 206 | 207 | s.errorPage(w, msg) | |
| 207 | - | s.results <- LoginResult{Provider: provider, Method: "oauth", Err: fmt.Errorf(msg)} | |
| 208 | + | s.results <- LoginResult{Provider: provider, Method: "oauth", Err: errors.New(msg)} | |
| 208 | 209 | return | |
| 209 | 210 | } | |
| 210 | 211 | code := q.Get("code") | |
| @@ -2,6 +2,7 @@ package auth | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "errors" | |||
| 5 | "fmt" | 6 | "fmt" | ||
| 6 | "html/template" | 7 | "html/template" | ||
| 7 | "net" | 8 | "net" | ||
| @@ -204,7 +205,7 @@ func (s *Server) handleCallback(w http.ResponseWriter, r *http.Request) { | |||||
| 204 | msg += ": " + d | 205 | msg += ": " + d | ||
| 205 | } | 206 | } | ||
| 206 | s.errorPage(w, msg) | 207 | s.errorPage(w, msg) | ||
| 207 | - | s.results <- LoginResult{Provider: provider, Method: "oauth", Err: fmt.Errorf(msg)} | 208 | + | s.results <- LoginResult{Provider: provider, Method: "oauth", Err: errors.New(msg)} |
| 208 | return | 209 | return | ||
| 209 | } | 210 | } | ||
| 210 | code := q.Get("code") | 211 | code := q.Get("code") | ||