diff --git a/crates/agent/src/lib.rs b/crates/agent/src/lib.rs index 19c4bdf272..51e6accfe5 100644 --- a/crates/agent/src/lib.rs +++ b/crates/agent/src/lib.rs @@ -2690,13 +2690,23 @@ mod tests { } #[test] - fn ollama_default_uses_small_local_model_id() { + fn ollama_default_is_unavailable_until_the_local_catalog_answers() { + // Y-2: `DEFAULT_OLLAMA_MODEL` is deliberately "unknown". The real + // default comes from the live local catalog, so the header never names + // a model the session cannot reach; without that catalog the registry + // must say so instead of resolving a costume. let registry = ModelRegistry::default(); - let resolved = registry.resolve_ok(None, Some(ProviderKind::Ollama)); + let error = registry + .resolve(None, Some(ProviderKind::Ollama)) + .expect_err("the placeholder default must not resolve"); - assert_eq!(resolved.resolved.provider, ProviderKind::Ollama); - assert_eq!(resolved.resolved.id, "deepseek-v4-flash"); - assert!(resolved.resolved.supports_reasoning); + assert!(matches!( + error, + ModelResolutionError::ProviderDefaultUnavailable { + provider: ProviderKind::Ollama, + ref default_model, + } if default_model == "unknown" + )); } #[test] diff --git a/crates/config/src/provider_defaults.rs b/crates/config/src/provider_defaults.rs index 996b34d16d..dce191402f 100644 --- a/crates/config/src/provider_defaults.rs +++ b/crates/config/src/provider_defaults.rs @@ -120,7 +120,10 @@ pub(crate) const DEFAULT_SGLANG_BASE_URL: &str = "http://localhost:30000/v1"; pub(crate) const DEFAULT_VLLM_MODEL: &str = "deepseek-ai/DeepSeek-V4-Pro"; pub(crate) const DEFAULT_VLLM_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash"; pub(crate) const DEFAULT_VLLM_BASE_URL: &str = "http://localhost:8000/v1"; -pub(crate) const DEFAULT_OLLAMA_MODEL: &str = "deepseek-v4-flash"; +/// Unresolved local-Ollama default. A live `GET /v1/models` (Ollama's +/// OpenAI-compat catalog, same tags as `/api/tags`) must supply the real id; +/// this marker must never be sent as a model name. +pub(crate) const DEFAULT_OLLAMA_MODEL: &str = "unknown"; pub(crate) const DEFAULT_OLLAMA_BASE_URL: &str = "http://localhost:11434/v1"; pub(crate) const DEFAULT_OLLAMA_CLOUD_MODEL: &str = "gpt-oss:120b"; pub(crate) const DEFAULT_OLLAMA_CLOUD_BASE_URL: &str = "https://ollama.com/v1"; diff --git a/crates/config/src/route/providers-export.golden.json b/crates/config/src/route/providers-export.golden.json index ecad82b837..ab155da498 100644 --- a/crates/config/src/route/providers-export.golden.json +++ b/crates/config/src/route/providers-export.golden.json @@ -688,7 +688,7 @@ "label": "Ollama", "endpoint": "http://localhost:11434/v1", "wire": "chat-completions", - "defaultModel": "deepseek-v4-flash", + "defaultModel": "unknown", "envVars": [ "OLLAMA_API_KEY" ], diff --git a/crates/config/src/tests.rs b/crates/config/src/tests.rs index 94bf88613d..56cfcac073 100644 --- a/crates/config/src/tests.rs +++ b/crates/config/src/tests.rs @@ -6291,7 +6291,7 @@ fn vllm_provider_defaults_to_local_endpoint_and_model() { } #[test] -fn ollama_provider_defaults_to_local_endpoint_and_small_model() { +fn ollama_provider_defaults_to_local_endpoint_and_unresolved_model() { let _lock = env_lock(); let _env = EnvGuard::without_deepseek_runtime_overrides(); let config = ConfigToml { @@ -6303,6 +6303,9 @@ fn ollama_provider_defaults_to_local_endpoint_and_small_model() { assert_eq!(resolved.provider, ProviderKind::Ollama); assert_eq!(resolved.base_url, DEFAULT_OLLAMA_BASE_URL); + // Pre-refresh window: do not invent a hosted DeepSeek id. A live + // `GET /v1/models` (same tags as `/api/tags`) must replace this marker. + assert_eq!(resolved.model, "unknown"); assert_eq!(resolved.model, DEFAULT_OLLAMA_MODEL); assert_eq!(resolved.api_key, None); } diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index e66f7235eb..633c91c53d 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -2625,16 +2625,22 @@ impl DeepSeekClient { /// snapshot via `provider_lake::merge_live_offerings`, preserving rows /// from other sources. /// - /// Currently activated for providers whose model list is not covered by the - /// Models.dev catalog (e.g. TelecomJS TokenHub). The refresh is non-fatal: - /// on failure, existing/bundled rows remain available. + /// Activated for providers whose model list is not covered by the + /// Models.dev catalog (TelecomJS TokenHub, Eden AI, Concentrate, local + /// Ollama). + /// Ollama's OpenAI-compat `GET /v1/models` returns the same tags as + /// native `GET /api/tags`. The refresh is non-fatal: on failure, + /// existing/bundled rows remain available. pub fn spawn_active_provider_catalog_refresh(config: &Config) { let provider = config.api_provider(); // Only refresh for providers that serve their own model list and are // not already covered by the Models.dev catalog. if !matches!( provider, - ApiProvider::Telecomjs | ApiProvider::Edenai | ApiProvider::Concentrate + ApiProvider::Telecomjs + | ApiProvider::Edenai + | ApiProvider::Concentrate + | ApiProvider::Ollama ) { return; } diff --git a/crates/tui/src/config/models.rs b/crates/tui/src/config/models.rs index 9ebc57b09d..6f323785c4 100644 --- a/crates/tui/src/config/models.rs +++ b/crates/tui/src/config/models.rs @@ -137,7 +137,16 @@ pub const DEFAULT_SGLANG_BASE_URL: &str = "http://localhost:30000/v1"; pub const DEFAULT_VLLM_MODEL: &str = "deepseek-ai/DeepSeek-V4-Pro"; pub const DEFAULT_VLLM_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash"; pub const DEFAULT_VLLM_BASE_URL: &str = "http://localhost:8000/v1"; -pub const DEFAULT_OLLAMA_MODEL: &str = "deepseek-v4-flash"; +/// Unresolved local-Ollama default. A live provider-catalog refresh must +/// replace this with a tag `GET /v1/models` actually returned. Do not send +/// this string as a model id. +pub const DEFAULT_OLLAMA_MODEL: &str = "unknown"; + +/// True when `model` is the pre-refresh local-Ollama placeholder, not a tag. +#[must_use] +pub fn is_unresolved_local_ollama_model(model: &str) -> bool { + model.trim().eq_ignore_ascii_case(DEFAULT_OLLAMA_MODEL) +} pub const DEFAULT_OLLAMA_BASE_URL: &str = "http://localhost:11434/v1"; pub const DEFAULT_OLLAMA_CLOUD_MODEL: &str = "gpt-oss:120b"; pub const DEFAULT_OLLAMA_CLOUD_BASE_URL: &str = codewhale_config::provider::OLLAMA_CLOUD_BASE_URL; diff --git a/crates/tui/src/model_inventory.rs b/crates/tui/src/model_inventory.rs index 12b4b16652..04c88d07de 100644 --- a/crates/tui/src/model_inventory.rs +++ b/crates/tui/src/model_inventory.rs @@ -366,6 +366,30 @@ fn configured_model_for_provider(config: &Config, provider: ApiProvider) -> Opti } fn provider_default_model(config: &Config, provider: ApiProvider) -> String { + if provider == ApiProvider::Ollama { + let configured = if provider == config.api_provider() { + Some(config.default_model()) + } else { + configured_model_for_provider(config, provider) + }; + let unresolved = configured.as_deref().is_none_or(|model| { + model.trim().eq_ignore_ascii_case("auto") + || crate::config::is_unresolved_local_ollama_model(model) + }); + if unresolved + && let Some(live) = crate::provider_lake::live_per_provider_models(provider) + .into_iter() + .next() + { + return live; + } + if let Some(model) = configured.filter(|model| { + !model.trim().eq_ignore_ascii_case("auto") + && !crate::config::is_unresolved_local_ollama_model(model) + }) { + return model; + } + } if provider == config.api_provider() { let model = config.default_model(); if !model.trim().eq_ignore_ascii_case("auto") { @@ -1107,4 +1131,33 @@ mod tests { }; assert!(!ModelInventory::from_config(&deepseek).router_available); } + + #[test] + fn ollama_default_prefers_live_local_tags_over_the_unresolved_marker() { + let _live = crate::provider_lake::lock_live_snapshot(); + crate::provider_lake::clear_live_snapshot(); + let config = Config { + provider: Some("ollama".to_string()), + ..Default::default() + }; + assert_eq!( + provider_default_model(&config, ApiProvider::Ollama), + crate::config::DEFAULT_OLLAMA_MODEL + ); + + crate::provider_lake::merge_live_offerings(vec![ + codewhale_config::catalog::CatalogOffering { + provider: "ollama".to_string(), + wire_model_id: "qwen2.5:0.5b".to_string(), + endpoint_key: "chat".to_string(), + default_for_provider: true, + ..Default::default() + }, + ]); + assert_eq!( + provider_default_model(&config, ApiProvider::Ollama), + "qwen2.5:0.5b" + ); + crate::provider_lake::clear_live_snapshot(); + } } diff --git a/crates/tui/src/provider_lake.rs b/crates/tui/src/provider_lake.rs index 596fa8e257..6805556744 100644 --- a/crates/tui/src/provider_lake.rs +++ b/crates/tui/src/provider_lake.rs @@ -372,6 +372,23 @@ fn catalog_models_from_offerings<'a>( models } +/// Tags from the provider's own live `/v1/models` partition. +/// +/// Models.dev rows must not satisfy a LOCAL default (Ollama). This reads only +/// the PerProvider snapshot so a cross-provider catalog cannot costume a +/// machine that has not answered with its own tags. +#[must_use] +pub fn live_per_provider_models(provider: ApiProvider) -> Vec { + let catalog_id = catalog_provider_id(provider).to_ascii_lowercase(); + let Ok(guard) = LIVE_SNAPSHOT.read() else { + return Vec::new(); + }; + let Some(snapshot) = guard.per_provider.get(&catalog_id) else { + return Vec::new(); + }; + catalog_models_from_offerings(&snapshot.offerings) +} + /// Catalog-backed model ids for one provider (#4188). /// /// Precedence: live Models.dev rows (when published) override bundled offline @@ -606,6 +623,43 @@ mod tests { // the lake must still return empty rather than inventing rows. assert!(all_catalog_models_for_provider(ApiProvider::Ollama).is_empty()); assert!(model_completion_names_for_provider(ApiProvider::Ollama).is_empty()); + assert!(live_per_provider_models(ApiProvider::Ollama).is_empty()); + } + + #[test] + fn ollama_live_default_uses_per_provider_tags_not_models_dev() { + let _live = lock_live_snapshot(); + clear_live_snapshot(); + + set_live_snapshot( + CatalogSnapshot { + offerings: vec![CatalogOffering { + provider: "ollama".to_string(), + wire_model_id: "deepseek-v4-flash".to_string(), + endpoint_key: "chat".to_string(), + default_for_provider: true, + ..Default::default() + }], + }, + LiveSource::ModelsDev, + ); + assert!( + live_per_provider_models(ApiProvider::Ollama).is_empty(), + "Models.dev must not satisfy a local Ollama default" + ); + + merge_live_offerings(vec![CatalogOffering { + provider: "ollama".to_string(), + wire_model_id: "qwen2.5:0.5b".to_string(), + endpoint_key: "chat".to_string(), + default_for_provider: true, + ..Default::default() + }]); + assert_eq!( + live_per_provider_models(ApiProvider::Ollama), + vec!["qwen2.5:0.5b".to_string()] + ); + clear_live_snapshot(); } /// #4116 / #4188 (AC): a provider with no bundled/live catalog coverage must diff --git a/crates/tui/src/tui/goldens/startup_100x30.txt b/crates/tui/src/tui/goldens/startup_100x30.txt index cba44ebfac..2490d4c084 100644 --- a/crates/tui/src/tui/goldens/startup_100x30.txt +++ b/crates/tui/src/tui/goldens/startup_100x30.txt @@ -1,11 +1,11 @@ - ██▄ ▄██ - ███████▄ ▄███████ - ▀████████ ████████▀ - ▀███████████████▀ - ▀▀▀█████▀▀▀ - ███ - █████ - ███████ + ▄████████> + ▄████████████████▄ + ███████████████████ + ▀████▀ ▀████▀ + ▀▀▀▀▀▀▀ + ▀▀▀ + ▀ + What are we working on? welcome back · 4 saved sessions in this workspace diff --git a/crates/tui/src/tui/goldens/startup_120x32.txt b/crates/tui/src/tui/goldens/startup_120x32.txt index 7d8e234325..d9df4ce88a 100644 --- a/crates/tui/src/tui/goldens/startup_120x32.txt +++ b/crates/tui/src/tui/goldens/startup_120x32.txt @@ -1,11 +1,11 @@ - ██▄ ▄██ - ███████▄ ▄███████ - ▀████████ ████████▀ - ▀███████████████▀ - ▀▀▀█████▀▀▀ - ███ - █████ - ███████ + ▄████████> + ▄████████████████▄ + ███████████████████ + ▀████▀ ▀████▀ + ▀▀▀▀▀▀▀ + ▀▀▀ + ▀ + What are we working on? welcome back · 4 saved sessions in this workspace diff --git a/crates/tui/src/tui/goldens/startup_160x40.txt b/crates/tui/src/tui/goldens/startup_160x40.txt index d23d3fa28c..ff8add23cc 100644 --- a/crates/tui/src/tui/goldens/startup_160x40.txt +++ b/crates/tui/src/tui/goldens/startup_160x40.txt @@ -1,13 +1,13 @@ - ██▄ ▄██ - ███████▄ ▄███████ - ▀████████ ████████▀ - ▀███████████████▀ - ▀▀▀█████▀▀▀ - ███ - █████ - ███████ + ▄████████> + ▄████████████████▄ + ███████████████████ + ▀████▀ ▀████▀ + ▀▀▀▀▀▀▀ + ▀▀▀ + ▀ + What are we working on? welcome back · 4 saved sessions in this workspace diff --git a/crates/tui/src/tui/goldens/startup_80x24.txt b/crates/tui/src/tui/goldens/startup_80x24.txt index cfb82baf85..a851ffc24a 100644 --- a/crates/tui/src/tui/goldens/startup_80x24.txt +++ b/crates/tui/src/tui/goldens/startup_80x24.txt @@ -1,9 +1,9 @@ - █▄ ▄█ - █████▄ ▄█████ - ███████████ - ▀▀███▀▀ - ███ - █████ + ▄██████> + ▄██████████▄ + █████████████ + ▀███▀ ▀██▀ + ▀▀▀▀▀ + ▀ What are we working on? welcome back · 4 saved sessions in this workspace diff --git a/crates/tui/src/tui/goldens/startup_ink_100x30.txt b/crates/tui/src/tui/goldens/startup_ink_100x30.txt index 6ee38aff34..18c4133c6a 100644 --- a/crates/tui/src/tui/goldens/startup_ink_100x30.txt +++ b/crates/tui/src/tui/goldens/startup_ink_100x30.txt @@ -1,11 +1,11 @@ -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbaaaaaaaaaaaaabbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbaaabbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbabbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbaaaaabbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacccccccccccccccccccccccaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddaaaaaaaaaaaaaaaaaaaaaaaaaa diff --git a/crates/tui/src/tui/goldens/startup_ink_120x32.txt b/crates/tui/src/tui/goldens/startup_ink_120x32.txt index 28373b0eee..ec44edb636 100644 --- a/crates/tui/src/tui/goldens/startup_ink_120x32.txt +++ b/crates/tui/src/tui/goldens/startup_ink_120x32.txt @@ -1,11 +1,11 @@ -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbaaaaaaaaaaaaabbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbaaabbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbabbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbaaaaabbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacccccccccccccccccccccccaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa diff --git a/crates/tui/src/tui/goldens/startup_ink_160x40.txt b/crates/tui/src/tui/goldens/startup_ink_160x40.txt index 852ca517f9..b3ecc25e71 100644 --- a/crates/tui/src/tui/goldens/startup_ink_160x40.txt +++ b/crates/tui/src/tui/goldens/startup_ink_160x40.txt @@ -1,13 +1,13 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbaaaaaaaaaaaaabbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbaaabbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbabbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbaaaaabbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacccccccccccccccccccccccaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa diff --git a/crates/tui/src/tui/goldens/startup_ink_80x24.txt b/crates/tui/src/tui/goldens/startup_ink_80x24.txt index 18651d0e1c..20f40c554a 100644 --- a/crates/tui/src/tui/goldens/startup_ink_80x24.txt +++ b/crates/tui/src/tui/goldens/startup_ink_80x24.txt @@ -1,9 +1,9 @@ -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbaaaaaaaaabbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbabbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbaaabbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaacccccccccccccccccccccccaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaadddddddddddddddddddddddddddddddddddddddddddddddddaaaaaaaaaaaaaaaa diff --git a/crates/tui/src/tui/mark.rs b/crates/tui/src/tui/mark.rs index 61cd6fb408..f1e0c16d30 100644 --- a/crates/tui/src/tui/mark.rs +++ b/crates/tui/src/tui/mark.rs @@ -1,18 +1,18 @@ -//! The Codewhale fluke — the identity mark, as cells. +//! The Codewhale mark — the identity silhouette, as cells. //! -//! Two marks, two jobs: the fluke (here) is the small/chrome mark, the -//! variant-B silhouette is the large raster one. Geometry is measured from -//! mockup `tideline-05`, symmetrized, half-block downsampled. +//! Side-view whale facing left, `>` prompt-eye, upward fluke. The old +//! symmetric fluke read as a fish at 16px; this mark cannot be mirrored +//! about its vertical axis. //! //! ASCII rungs are drawn, not transliterated — `glyphs::ascii_fallback` maps -//! every block glyph to `#`, which would flatten the fluke to a blob. +//! every block glyph to `#`, which would flatten the whale to a blob. use ratatui::{buffer::Buffer, layout::Rect, style::Color, style::Style}; /// Rungs of the mark's scale ladder, each sampled at its own size. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MarkSize { - /// 9x4 — the smallest rung that still reads as a fluke. + /// 9x4 — the smallest rung that still reads as a side-view whale. Small, /// 13x6 — the default hero mark. Medium, @@ -21,58 +21,58 @@ pub enum MarkSize { } const SMALL_ROWS: [&str; 4] = [ - "█▄▄▄ ▄▄▄█", // - "▀███▄███▀", - " ▀█▀", - " ███", + "▄█████> ", // + "████████▄", + "▀██▀ ▀██ ", + " ▀▀▀ ", ]; const MEDIUM_ROWS: [&str; 6] = [ - "█▄ ▄█", // - "█████▄ ▄█████", - " ███████████", - " ▀▀███▀▀", - " ███", - " █████", + " ▄██████> ", // + "▄██████████▄ ", + "█████████████", + "▀███▀ ▀██▀ ", + " ▀▀▀▀▀ ", + " ▀ ", ]; const LARGE_ROWS: [&str; 8] = [ - "██▄ ▄██", // - "███████▄ ▄███████", - "▀████████ ████████▀", - " ▀███████████████▀", - " ▀▀▀█████▀▀▀", - " ███", - " █████", - " ███████", + " ▄████████> ", // + "▄████████████████▄ ", + "███████████████████", + "▀████▀ ▀████▀ ", + " ▀▀▀▀▀▀▀ ", + " ▀▀▀ ", + " ▀ ", + " ", ]; /// `'` and `,` carry the half-cell the block glyphs carried, so lobes slope. const SMALL_ASCII: [&str; 4] = [ - "#,,, ,,,#", // - "'###,###'", - " '#'", - " ###", + ",#####> ", // + "########,", + "'##' '## ", + " ''' ", ]; const MEDIUM_ASCII: [&str; 6] = [ - "#, ,#", // - "#####, ,#####", - " ###########", - " ''###''", - " ###", - " #####", + " ,######> ", // + ",##########, ", + "#############", + "'###' '##' ", + " ''''' ", + " ' ", ]; const LARGE_ASCII: [&str; 8] = [ - "##, ,##", // - "#######, ,#######", - "'######## ########'", - " '###############'", - " '''#####'''", - " ###", - " #####", - " #######", + " ,########> ", // + ",################, ", + "###################", + "'####' '####' ", + " ''''''' ", + " ''' ", + " ' ", + " ", ]; impl MarkSize { @@ -247,27 +247,18 @@ mod tests { } #[test] - fn the_mark_is_symmetric_about_its_vertical_axis() { + fn the_mark_has_a_prompt_eye() { for size in [MarkSize::Small, MarkSize::Medium, MarkSize::Large] { - let (cols, _) = size.cells(); - for line in size.rows(false) { - let mut cells: Vec = line.chars().collect(); - cells.resize(usize::from(cols), ' '); - let mirrored: Vec = cells.iter().rev().copied().map(mirror_glyph).collect(); - assert_eq!(cells, mirrored, "{size:?} row {line:?} is not symmetric"); + for ascii_safe in [false, true] { + let art = size.rows(ascii_safe); + assert!( + art.iter().any(|line| line.contains('>')), + "{size:?} ascii={ascii_safe} is missing the `>` prompt-eye" + ); } } } - fn mirror_glyph(glyph: char) -> char { - // Half-blocks are left-right symmetric; this hook catches a future - // asymmetric glyph rather than letting it pass silently. - match glyph { - '█' | '▀' | '▄' | ' ' => glyph, - other => other, - } - } - #[test] fn for_area_never_returns_a_rung_that_would_clip() { for width in 0u16..40 { @@ -352,12 +343,12 @@ mod tests { false, ); assert_eq!(painted, Rect::new(4, 0, 13, 6)); - // Row 0 is `█▄ ▄█`; the gap keeps the field beneath it. - assert_eq!(buf.cell((4, 0)).map(|c| c.symbol()), Some("█")); - assert_eq!(buf.cell((5, 0)).map(|c| c.symbol()), Some("▄")); - assert_eq!(buf.cell((6, 0)).map(|c| c.symbol()), Some("~")); - assert_eq!(buf.cell((15, 0)).map(|c| c.symbol()), Some("▄")); - assert_eq!(buf.cell((16, 0)).map(|c| c.symbol()), Some("█")); + // Row 0 is ` ▄██████> `; leading blanks keep the field. + assert_eq!(buf.cell((4, 0)).map(|c| c.symbol()), Some("~")); + assert_eq!(buf.cell((5, 0)).map(|c| c.symbol()), Some("~")); + assert_eq!(buf.cell((6, 0)).map(|c| c.symbol()), Some("▄")); + assert_eq!(buf.cell((13, 0)).map(|c| c.symbol()), Some(">")); + assert_eq!(buf.cell((14, 0)).map(|c| c.symbol()), Some("~")); } #[test] diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 6d15f38e76..db42d157d7 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -8192,9 +8192,12 @@ fn first_run_ollama_choice_survives_restart_without_rewriting_config() { ); assert!(app.pending_route_save.is_none()); assert!( - app.status_message - .as_deref() - .is_some_and(|message| message.contains("Remembered ollama/deepseek-v4-flash")), + app.status_message.as_deref().is_some_and(|message| { + message.contains(&format!( + "Remembered ollama/{}", + crate::config::DEFAULT_OLLAMA_MODEL + )) + }), "onboarding must truthfully receipt the durable startup route: {:?}", app.status_message, ); diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 74b3256098..f2c11ff4f7 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -327,7 +327,7 @@ table. | Runner | Default base URL | Default model | Base URL override | | --- | --- | --- | --- | -| `ollama` | `http://localhost:11434/v1` | `deepseek-v4-flash` | `OLLAMA_BASE_URL` | +| `ollama` | `http://localhost:11434/v1` | live tag from `GET /v1/models` (pre-refresh: `unknown`) | `OLLAMA_BASE_URL` | | `vllm` | `http://localhost:8000/v1` | `deepseek-ai/DeepSeek-V4-Pro` | `VLLM_BASE_URL` | | `sglang` | `http://localhost:30000/v1` | `deepseek-ai/DeepSeek-V4-Pro` | `SGLANG_BASE_URL` | @@ -589,7 +589,7 @@ overlay and lets DSH resolve its own keys. | `minimax-anthropic` | `[providers.minimax_anthropic]` | `MINIMAX_API_KEY` | `MINIMAX_ANTHROPIC_BASE_URL`; default `https://api.minimax.io/anthropic`; China `https://api.minimaxi.com/anthropic` | `MiniMax-M3`, `MiniMax-M2.7` | MiniMax direct Anthropic-compatible Messages route. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`; the route uses `x-api-key`. M3 supports adaptive or disabled thinking. M2.7 always keeps thinking enabled. | | `sglang` | `[providers.sglang]` | Optional `SGLANG_API_KEY` | `SGLANG_BASE_URL`; default `http://localhost:30000/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Self-hosted OpenAI-compatible route. Localhost deployments commonly omit auth. `SGLANG_MODEL` is accepted. | | `vllm` | `[providers.vllm]` | Optional `VLLM_API_KEY` | `VLLM_BASE_URL`; default `http://localhost:8000/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Self-hosted vLLM OpenAI-compatible route. Localhost deployments commonly omit auth. `VLLM_MODEL` is accepted. | -| `ollama` | `[providers.ollama]` | Local optional `OLLAMA_API_KEY` | `OLLAMA_BASE_URL`; default `http://localhost:11434/v1` | `deepseek-v4-flash`; provider-hinted custom tags pass through | Local Ollama is keyless by default. `OLLAMA_MODEL` is accepted. | +| `ollama` | `[providers.ollama]` | Local optional `OLLAMA_API_KEY` | `OLLAMA_BASE_URL`; default `http://localhost:11434/v1` | live tag from the local catalog; pre-refresh placeholder `unknown`; provider-hinted custom tags pass through | Local Ollama is keyless by default. `OLLAMA_MODEL` is accepted. The header must not paint a hosted id the local daemon did not list. | | `ollama-cloud` | `[providers.ollama_cloud]` | `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY` | `OLLAMA_CLOUD_BASE_URL`; default `https://ollama.com/v1` | `gpt-oss:120b`; arbitrary provider-owned IDs pass through | Hosted OpenAI-compatible `/v1/chat/completions` route. Save credentials under `ollama-cloud`; the exact released `ollama` + Cloud URL tuple has bounded read-only in-memory compatibility with its legacy table and secret slot. `OLLAMA_CLOUD_MODEL` is accepted. | | `huggingface` | `[providers.huggingface]` | `HUGGINGFACE_API_KEY`, `HF_TOKEN` | `HUGGINGFACE_BASE_URL`, `HF_BASE_URL`; default `https://router.huggingface.co/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Hugging Face Inference Providers OpenAI-compatible router route. Accepted aliases: `huggingface`, `hugging-face`, `hugging_face`, `hf`. Org-prefixed model IDs pass through. `HUGGINGFACE_MODEL` and `HF_MODEL` are accepted. Hub browsing/export are separate future features. | | `deepinfra` | `[providers.deepinfra]` | `DEEPINFRA_API_KEY`, `DEEPINFRA_TOKEN` | `DEEPINFRA_BASE_URL`; default `https://api.deepinfra.com/v1/openai` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | DeepInfra OpenAI-compatible route. Drop-in replacement for OpenAI SDK. | @@ -825,7 +825,7 @@ endpoint when the endpoint supports model listing. | `minimax-anthropic` | `MiniMax-M3`, `MiniMax-M2.7` | yes | yes | | `sglang` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | yes | | `vllm` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | yes | -| `ollama` | `deepseek-v4-flash`; custom tags pass through when provider hint is `ollama` | yes | no | +| `ollama` | live local tag; custom tags pass through when provider hint is `ollama` | yes | no | | `ollama-cloud` | `gpt-oss:120b`; arbitrary provider-owned model IDs pass through | yes | yes | | `huggingface` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | no | | `deepinfra` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | yes | diff --git a/docs/zh_hans/PROVIDERS.md b/docs/zh_hans/PROVIDERS.md index 1ae2ad7d7b..6b0244329d 100644 --- a/docs/zh_hans/PROVIDERS.md +++ b/docs/zh_hans/PROVIDERS.md @@ -210,7 +210,7 @@ model = "qwen3.8-max" | 运行时 | 默认 base URL | 默认模型 | Base URL 覆盖 | | --- | --- | --- | --- | -| `ollama` | `http://localhost:11434/v1` | `deepseek-v4-flash` | `OLLAMA_BASE_URL` | +| `ollama` | `http://localhost:11434/v1` | 来自 `GET /v1/models` 的实况标签(刷新前:`unknown`) | `OLLAMA_BASE_URL` | | `vllm` | `http://localhost:8000/v1` | `deepseek-ai/DeepSeek-V4-Pro` | `VLLM_BASE_URL` | | `sglang` | `http://localhost:30000/v1` | `deepseek-ai/DeepSeek-V4-Pro` | `SGLANG_BASE_URL` | @@ -401,7 +401,7 @@ Kimi 仍然仅支持 API 密钥;对 Kimi 的外部授权被拒绝。 | `minimax-anthropic` | `[providers.minimax_anthropic]` | `MINIMAX_API_KEY` | `MINIMAX_ANTHROPIC_BASE_URL`;默认 `https://api.minimax.io/anthropic`;中国 `https://api.minimaxi.com/anthropic` | `MiniMax-M3`, `MiniMax-M2.7` | MiniMax 直连 Anthropic 兼容 Messages 路由。保留 `/anthropic` 后缀,因为 Codewhale 会追加 `/v1/messages`;该路由使用 `x-api-key`。M3 支持自适应或禁用思考。M2.7 始终启用思考。 | | `sglang` | `[providers.sglang]` | 可选 `SGLANG_API_KEY` | `SGLANG_BASE_URL`;默认 `http://localhost:30000/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | 自托管 OpenAI 兼容路由。本地部署通常省略认证。接受 `SGLANG_MODEL`。 | | `vllm` | `[providers.vllm]` | 可选 `VLLM_API_KEY` | `VLLM_BASE_URL`;默认 `http://localhost:8000/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | 自托管 vLLM OpenAI 兼容路由。本地部署通常省略认证。接受 `VLLM_MODEL`。 | -| `ollama` | `[providers.ollama]` | 本地可选 `OLLAMA_API_KEY` | `OLLAMA_BASE_URL`;默认 `http://localhost:11434/v1` | `deepseek-v4-flash`;提供商提示的自定义标签直通 | 本地 Ollama 默认无密钥。接受 `OLLAMA_MODEL`。 | +| `ollama` | `[providers.ollama]` | 本地可选 `OLLAMA_API_KEY` | `OLLAMA_BASE_URL`;默认 `http://localhost:11434/v1` | 本地目录的实况标签;刷新前占位 `unknown`;提供商提示的自定义标签直通 | 本地 Ollama 默认无密钥。接受 `OLLAMA_MODEL`。标题栏不得绘制本机守护进程未列出的托管 id。 | | `ollama-cloud` | `[providers.ollama_cloud]` | `OLLAMA_CLOUD_API_KEY`, 然后是 `OLLAMA_API_KEY` | `OLLAMA_CLOUD_BASE_URL`;默认 `https://ollama.com/v1` | `gpt-oss:120b`;任意的提供商自有 ID 直通 | 托管 OpenAI 兼容 `/v1/chat/completions` 路由。在 `ollama-cloud` 下保存凭据;精确的已发布 `ollama` + Cloud URL 元组与其遗留表和秘密槽位具有有界的只读内存兼容性。接受 `OLLAMA_CLOUD_MODEL`。 | | `huggingface` | `[providers.huggingface]` | `HUGGINGFACE_API_KEY`, `HF_TOKEN` | `HUGGINGFACE_BASE_URL`, `HF_BASE_URL`;默认 `https://router.huggingface.co/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Hugging Face Inference Providers OpenAI 兼容路由器路由。接受的别名:`huggingface`, `hugging-face`, `hugging_face`, `hf`。Org 前缀的模型 ID 直通。接受 `HUGGINGFACE_MODEL` 和 `HF_MODEL`。Hub 浏览/导出是未来的独立功能。 | | `deepinfra` | `[providers.deepinfra]` | `DEEPINFRA_API_KEY`, `DEEPINFRA_TOKEN` | `DEEPINFRA_BASE_URL`;默认 `https://api.deepinfra.com/v1/openai` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | DeepInfra OpenAI 兼容路由。OpenAI SDK 的直接替代品。 | @@ -514,7 +514,7 @@ OpenRouter completions 和静态注册表行包括自 2026 年 4 月起通过 Op | `minimax-anthropic` | `MiniMax-M3`, `MiniMax-M2.7` | 是 | 是 | | `sglang` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | 是 | 是 | | `vllm` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | 是 | 是 | -| `ollama` | `deepseek-v4-flash`;提供商提示为 `ollama` 时自定义标签原样通过 | 是 | 否 | +| `ollama` | 实况本地标签;提供商提示为 `ollama` 时自定义标签原样通过 | 是 | 否 | | `ollama-cloud` | `gpt-oss:120b`;任意的提供商自有模型 ID 原样通过 | 是 | 是 | | `huggingface` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | 是 | 否 | | `deepinfra` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | 是 | 是 | diff --git a/web/app/icon.svg b/web/app/icon.svg index 759ab94dd8..01975ae715 100644 --- a/web/app/icon.svg +++ b/web/app/icon.svg @@ -1,12 +1,20 @@ - - - - - - - - - - + + + + diff --git a/web/brand/mark/README.md b/web/brand/mark/README.md new file mode 100644 index 0000000000..d6ad427c5f --- /dev/null +++ b/web/brand/mark/README.md @@ -0,0 +1,11 @@ +# Codewhale mark + +Canonical mark: white side-view whale facing left, `>` prompt-eye cutout +(even-odd), small belly crease, upward fluke. + +- `whale.svg` — silhouette only (`currentColor`). In-page chrome source. +- `tile-*.svg` — squircle tiles. `tile-electric.svg` (#0066FF) is the + website app icon source (`web/app/icon.svg`). +- Rasters in `web/public/brand/` and `web/public/icon-{192,512}.png`. + +Do not restore the symmetric fluke. At 16px that mark reads as a fish. diff --git a/web/brand/mark/tile-black.svg b/web/brand/mark/tile-black.svg new file mode 100644 index 0000000000..df8faebb63 --- /dev/null +++ b/web/brand/mark/tile-black.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/brand/mark/tile-circle.svg b/web/brand/mark/tile-circle.svg new file mode 100644 index 0000000000..714138f4da --- /dev/null +++ b/web/brand/mark/tile-circle.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/brand/mark/tile-cyan.svg b/web/brand/mark/tile-cyan.svg new file mode 100644 index 0000000000..f9bd8d8cdb --- /dev/null +++ b/web/brand/mark/tile-cyan.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/brand/mark/tile-electric.svg b/web/brand/mark/tile-electric.svg new file mode 100644 index 0000000000..bc9c55441b --- /dev/null +++ b/web/brand/mark/tile-electric.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/brand/mark/tile-gold.svg b/web/brand/mark/tile-gold.svg new file mode 100644 index 0000000000..9d4924c1e8 --- /dev/null +++ b/web/brand/mark/tile-gold.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/brand/mark/tile-green.svg b/web/brand/mark/tile-green.svg new file mode 100644 index 0000000000..60e8f9e588 --- /dev/null +++ b/web/brand/mark/tile-green.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/brand/mark/tile-ivory.svg b/web/brand/mark/tile-ivory.svg new file mode 100644 index 0000000000..badd1ad3ac --- /dev/null +++ b/web/brand/mark/tile-ivory.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/brand/mark/tile-magenta.svg b/web/brand/mark/tile-magenta.svg new file mode 100644 index 0000000000..b8d8d277cb --- /dev/null +++ b/web/brand/mark/tile-magenta.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/brand/mark/tile-navy.svg b/web/brand/mark/tile-navy.svg new file mode 100644 index 0000000000..208630d4db --- /dev/null +++ b/web/brand/mark/tile-navy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/brand/mark/tile-orange.svg b/web/brand/mark/tile-orange.svg new file mode 100644 index 0000000000..d91001148d --- /dev/null +++ b/web/brand/mark/tile-orange.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/brand/mark/tile-outline.svg b/web/brand/mark/tile-outline.svg new file mode 100644 index 0000000000..134e795238 --- /dev/null +++ b/web/brand/mark/tile-outline.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/brand/mark/tile-seafoam.svg b/web/brand/mark/tile-seafoam.svg new file mode 100644 index 0000000000..0a6eeda9b2 --- /dev/null +++ b/web/brand/mark/tile-seafoam.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/brand/mark/tile-tide.svg b/web/brand/mark/tile-tide.svg new file mode 100644 index 0000000000..52d1786a40 --- /dev/null +++ b/web/brand/mark/tile-tide.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/brand/mark/tile-white.svg b/web/brand/mark/tile-white.svg new file mode 100644 index 0000000000..80ea20c082 --- /dev/null +++ b/web/brand/mark/tile-white.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/brand/mark/whale.svg b/web/brand/mark/whale.svg new file mode 100644 index 0000000000..c9290d35f6 --- /dev/null +++ b/web/brand/mark/whale.svg @@ -0,0 +1,19 @@ + + + + diff --git a/web/components/whale.tsx b/web/components/whale.tsx index ebd4a167cb..9f4b2c16eb 100644 --- a/web/components/whale.tsx +++ b/web/components/whale.tsx @@ -1,17 +1,11 @@ /** - * The Codewhale brand mark — the white whale silhouette traced from the - * 1254×1254 logo master (rounded-square tile lives in app/icon.svg; this is - * the bare silhouette for in-page chrome). The path keeps the master's - * coordinate frame: potrace units, y-up, offset by the silhouette's bounding - * origin, framed by the viewBox below. + * The Codewhale brand mark — side-view whale facing left, `>` prompt-eye + * cutout (even-odd), belly crease, upward fluke. Matches `web/brand/mark/whale.svg`. */ const WHALE_MARK = - "M5351 8953 c-174 -358 -474 -544 -1148 -713 -707 -176 -1031 -417 -1164 -862 -11 -40 -23 -77 -26 -82 -3 -4 -42 29 -87 76 -318 327 -591 401 -1321 353 -711 -47 -1078 48 -1421 367 -93 87 -93 87 -120 57 -28 -31 -24 -169 10 -334 143 -690 584 -1142 1240 -1270 165 -33 322 -43 721 -50 467 -7 597 -24 753 -101 252 -123 333 -343 298 -814 -39 -532 -41 -696 -15 -1050 55 -732 286 -1413 681 -2002 70 -104 69 -95 11 -102 -912 -117 -927 -123 -767 -332 258 -338 747 -544 1302 -550 l252 -2 103 -94 c1220 -1119 2384 -1604 3220 -1342 326 102 482 241 577 509 312 887 49 1756 -785 2591 -514 516 -1014 849 -2035 1357 -910 452 -1205 640 -1540 976 -426 427 -512 802 -248 1072 89 92 205 165 483 306 771 390 1070 709 1186 1263 75 362 -55 990 -160 773z"; + "M10 50 C10 34 24 26 44 28 C58 29 70 36 76 46 C80 52 78 60 70 66 L86 48 C92 42 100 48 96 62 C92 78 78 86 62 82 C54 88 36 88 20 74 C8 64 10 56 10 50 Z M22 40 L36 50 L22 60 L26 50 Z M30 70 C38 74 48 74 52 70 C46 72 38 72 30 70 Z"; -/* Silhouette bounds in master coordinates (x 201–1054, y 177–1072), with a - small margin so the fluke never kisses the frame at favicon sizes. */ -const VIEW_BOX = "170 148 915 950"; -const MASTER_TRANSFORM = "translate(197 173) translate(0 904) scale(0.1 -0.1)"; +const VIEW_BOX = "0 0 100 100"; export function Whale({ size = 36, @@ -40,9 +34,7 @@ export function Whale({ {caustic ? ( - - - + @@ -52,24 +44,21 @@ export function Whale({ ) : null} - - - + - {/* The light is clipped to the mark itself, so it lands on the whale and - never on a rectangle of footer behind it. The clip lives on a STATIC - group and the highlight moves inside it — put the clip on the moving - rect and the whale-shaped window slides along with the light, which - is a very quiet way to render nothing at all. Parked off-canvas at - rest, which is also where reduced motion leaves it. */} {caustic ? ( diff --git a/web/public/brand/codewhale-mark-black.png b/web/public/brand/codewhale-mark-black.png new file mode 100644 index 0000000000..963203c1b4 Binary files /dev/null and b/web/public/brand/codewhale-mark-black.png differ diff --git a/web/public/brand/codewhale-mark-circle.png b/web/public/brand/codewhale-mark-circle.png new file mode 100644 index 0000000000..fd0ccdbb86 Binary files /dev/null and b/web/public/brand/codewhale-mark-circle.png differ diff --git a/web/public/brand/codewhale-mark-cyan.png b/web/public/brand/codewhale-mark-cyan.png new file mode 100644 index 0000000000..fb7d2b0b37 Binary files /dev/null and b/web/public/brand/codewhale-mark-cyan.png differ diff --git a/web/public/brand/codewhale-mark-electric.png b/web/public/brand/codewhale-mark-electric.png new file mode 100644 index 0000000000..441ca15c50 Binary files /dev/null and b/web/public/brand/codewhale-mark-electric.png differ diff --git a/web/public/brand/codewhale-mark-gold.png b/web/public/brand/codewhale-mark-gold.png new file mode 100644 index 0000000000..5db2e1306e Binary files /dev/null and b/web/public/brand/codewhale-mark-gold.png differ diff --git a/web/public/brand/codewhale-mark-green.png b/web/public/brand/codewhale-mark-green.png new file mode 100644 index 0000000000..ad9423f322 Binary files /dev/null and b/web/public/brand/codewhale-mark-green.png differ diff --git a/web/public/brand/codewhale-mark-ivory.png b/web/public/brand/codewhale-mark-ivory.png new file mode 100644 index 0000000000..314f85d1f5 Binary files /dev/null and b/web/public/brand/codewhale-mark-ivory.png differ diff --git a/web/public/brand/codewhale-mark-magenta.png b/web/public/brand/codewhale-mark-magenta.png new file mode 100644 index 0000000000..aa27a03d29 Binary files /dev/null and b/web/public/brand/codewhale-mark-magenta.png differ diff --git a/web/public/brand/codewhale-mark-navy.png b/web/public/brand/codewhale-mark-navy.png new file mode 100644 index 0000000000..79bed3c5db Binary files /dev/null and b/web/public/brand/codewhale-mark-navy.png differ diff --git a/web/public/brand/codewhale-mark-orange.png b/web/public/brand/codewhale-mark-orange.png new file mode 100644 index 0000000000..63f99e9556 Binary files /dev/null and b/web/public/brand/codewhale-mark-orange.png differ diff --git a/web/public/brand/codewhale-mark-outline.png b/web/public/brand/codewhale-mark-outline.png new file mode 100644 index 0000000000..a3a8271ce2 Binary files /dev/null and b/web/public/brand/codewhale-mark-outline.png differ diff --git a/web/public/brand/codewhale-mark-seafoam.png b/web/public/brand/codewhale-mark-seafoam.png new file mode 100644 index 0000000000..15f30aaa79 Binary files /dev/null and b/web/public/brand/codewhale-mark-seafoam.png differ diff --git a/web/public/brand/codewhale-mark-tide.png b/web/public/brand/codewhale-mark-tide.png new file mode 100644 index 0000000000..43c9875219 Binary files /dev/null and b/web/public/brand/codewhale-mark-tide.png differ diff --git a/web/public/brand/codewhale-mark-white.png b/web/public/brand/codewhale-mark-white.png new file mode 100644 index 0000000000..5da930cab7 Binary files /dev/null and b/web/public/brand/codewhale-mark-white.png differ diff --git a/web/public/icon-192.png b/web/public/icon-192.png index 9e34b5111f..57d0dfac6b 100644 Binary files a/web/public/icon-192.png and b/web/public/icon-192.png differ diff --git a/web/public/icon-512.png b/web/public/icon-512.png index f2895b0a96..57b8de194a 100644 Binary files a/web/public/icon-512.png and b/web/public/icon-512.png differ