mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 08:04:25 -10:00
Elect the local Ollama default from completion-capable native tags
Refs #5848 (kept open: see "Not verified").
The alphabetical-first election in the runtime resolver is deleted. The
provider lake stays the single catalog and default authority.
- /api/tags is read natively, carrying completion, tools and thinking
capabilities. Tags that omit capabilities reuse the bounded /api/show pass
with the listing client's authorization and redirect policy. Only rows with
positive completion capability become chat rows; embedding rows never reach
the picker, and choose_chat_tag ranks the filtered roster and marks
default_for_provider. valid_catalog_model_id runs before election, known
secret-bearing tags are rejected before any authenticated profile call, and
the whole profile-future error is redacted before shared logging.
- The first-run probe builds the same delta and returns the elected default,
so there is no second election. A daemon reporting no completion capability
gets no automatic default; an explicit real model still passes through.
- /provider ollama refreshes before resolving. The CLI exec route and the
Runtime API (tasks, threads, stream), including an explicit
model_provider/model_provider_id and blank, auto or placeholder models, await
a targeted refresh. refresh_local_ollama_catalog serializes through one
async mutex whose guard moves into the blocking worker, so a cancelled caller
cannot release it before publication; a queued call reuses only an exact
successful cache entry fetched since it queued, with an elected default and
an authoritative same-endpoint partition (this includes A to B to A). Startup
gets a background refresh; no config read waits on the network.
- Served context is the positive loaded /api/ps context_length, never the
trained /api/show maximum. The observation is current for 60 seconds, then
only that context fact clears; the 24h roster TTL is unchanged. The Ollama
runtime resolver cache is bypassed so an expired fact cannot survive in it.
Unloaded or unknown context stays the explicitly unverified 8192 floor;
configured context overrides keep precedence. Frozen active routes remain
snapshots: no mid-turn revocation, warmup, num_ctx change or polling.
The 60 s window is a proposal, not release qualification.
Verified locally (macOS; Ollama 0.40.9 on localhost, qwen3:4b
completion/tools/thinking and nomic-embed-text embedding; no hosted CI, no
paid route, no Linux/FreeBSD/Windows compile; every check below ran once on
the combined tree of this commit and the preceding browser/OAuth commit, on
base 62bbcebf2b, before the two were split for landing):
- nextest -p codewhale-runtime -p codewhale-tui --lib, focused filters
(local_ollama, route_runtime, model_inventory, provider_catalog_live,
unresolved_ollama_client_waits_for_catalog_but_probe_can_bootstrap,
api_provider_default_and_model_list_follow_exact_local_catalog,
api_config_reports_local_default_availability_without_blocking_config_reads,
oauth::, plugins::providers::, browser::): 251 run, 251 passed, 0 failed,
15315 skipped. Every filter matched nonzero tests (10, 37, 28, 25, 1, 1, 1).
Six existing fixtures now carry default_for_provider because the implicit
election they leaned on no longer exists.
- Built binary, isolated HOME and CODEWHALE_HOME, env -i:
* real daemon, fresh home, exec --provider ollama with no model: wire model
qwen3:4b (not the embedding tag), turn completed with output "OK".
* real daemon, Runtime API ready in 0.23 s, immediate no-model, blank,
explicit model_provider, and model_provider plus id admissions all
resolved qwen3:4b with the model unloaded; /v1/providers/ollama/models
listed only qwen3:4b at the 8192 fallback. With qwen3:4b loaded the same
list showed 32768 (from /api/ps) against a trained 262144. A no-model task
and a no-model stream both ran to completion on qwen3:4b.
* first-run TUI (pty, no config): "Provider switched: deepseek -> ollama,
Model: deepseek-flash -> qwen3:4b"; configured deepseek then /provider
ollama switched to qwen3:4b. Embedding tag never adopted.
* mock daemon with 1 s per request (race forced): first admission waited
2.0 s and succeeded; two concurrent fresh admissions both succeeded with
exactly one /api/tags and one /api/ps fetch; an explicit Ollama target
while deepseek was active did the same. Embedding-only roster: 400
"no fresh model catalog", empty model list, model_available false. Legacy
tags without capabilities used /api/show and still elected the chat tag.
Context 16384 from /api/ps held for the 60 s window, cleared to 8192 at
about 65 s with the default still available, and returned on the next
admission. Endpoint A to B to A within 60 s: B showed 8192, A 16384.
- dead-code 212 (at budget), blocking calls 543 sites / 165 files,
module_graph --check 121 closure modules, reqwest builder, provider registry
and changelog sync checks pass.
Not verified: a baseline binary was not built, so the admission race is shown
passing on this tree rather than failing on the previous one. An unreachable or
stalled local endpoint during /provider ollama (the switch awaits a bounded
refresh) was not exercised. Configured context override precedence was not run
in the product. The embedding-only error text reads "no fresh model catalog",
which is accurate but unhelpful; improving it is left open. Model execution was
shown only for exec, API task and API stream on qwen3:4b; this is not release
qualification.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
cd6203e1d2
commit
b4103aa7bb
14 files changed
+491
-134
No files matched your search
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Local Ollama defaults now come from chat-capable tags, and `/provider ollama`
|
||||
refreshes the live catalog before switching. Exec and Runtime API defaults use
|
||||
the same discovery path. Fresh context observations come from loaded models'
|
||||
served windows; unloaded models retain an explicitly unverified fallback (#5848).
|
||||
- `codewhale --provider <name> resume <id>` now moves the saved session to the
|
||||
named provider, as `exec --resume` already did, so a session whose provider
|
||||
table was deleted can be reopened on a provider you still have. It takes
|
||||
|
||||
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Local Ollama defaults now come from chat-capable tags, and `/provider ollama`
|
||||
refreshes the live catalog before switching. Exec and Runtime API defaults use
|
||||
the same discovery path. Fresh context observations come from loaded models'
|
||||
served windows; unloaded models retain an explicitly unverified fallback (#5848).
|
||||
- `codewhale --provider <name> resume <id>` now moves the saved session to the
|
||||
named provider, as `exec --resume` already did, so a session whose provider
|
||||
table was deleted can be reopened on a provider you still have. It takes
|
||||
|
||||
@@ -999,8 +999,8 @@ pub(super) enum ErrorBodyDisclosure {
|
||||
/// the connection and never answers hangs model-list, catalog refresh, and
|
||||
/// health checks forever (ops R4).
|
||||
pub(super) const NON_STREAMING_HTTP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const PROVIDER_CATALOG_MAX_RESPONSE_BYTES: usize = 8 * 1024 * 1024;
|
||||
const PROVIDER_CATALOG_MAX_ROWS: usize = 10_000;
|
||||
pub(crate) const PROVIDER_CATALOG_MAX_RESPONSE_BYTES: usize = 8 * 1024 * 1024;
|
||||
pub(crate) const PROVIDER_CATALOG_MAX_ROWS: usize = 10_000;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ModelsFetchLimits {
|
||||
@@ -1200,7 +1200,7 @@ pub(super) async fn bounded_error_text(response: reqwest::Response, max_bytes: u
|
||||
String::from_utf8_lossy(&buf).into_owned()
|
||||
}
|
||||
|
||||
async fn bounded_provider_catalog_text(
|
||||
pub(crate) async fn bounded_provider_catalog_text(
|
||||
response: reqwest::Response,
|
||||
max_bytes: usize,
|
||||
) -> Result<String, CatalogRefreshError> {
|
||||
@@ -3780,6 +3780,15 @@ impl CodewhaleClient {
|
||||
// Go is unpaginated. A Messages generation dialect or a custom identity
|
||||
// resembling a built-in provider does not establish this list contract.
|
||||
let cursor_query = (self.api_provider == ProviderKind::Anthropic).then_some("after_id");
|
||||
self.models_document_at(endpoint, cursor_query, mode).await
|
||||
}
|
||||
|
||||
async fn models_document_at(
|
||||
&self,
|
||||
endpoint: reqwest::Url,
|
||||
cursor_query: Option<&str>,
|
||||
mode: ModelsRequestMode,
|
||||
) -> Result<(String, tokio::time::Instant), ModelsFetchError> {
|
||||
collect_models_document(
|
||||
endpoint,
|
||||
cursor_query,
|
||||
@@ -3854,17 +3863,101 @@ impl CodewhaleClient {
|
||||
catalog_endpoint_is_baseten(self.api_provider, &self.base_url)
|
||||
}
|
||||
|
||||
/// Fetch the provider's live `/models` listing as a secret-free
|
||||
/// Fetch the provider's live model listing as a secret-free
|
||||
/// [`ProviderCatalogDelta`] (#3385).
|
||||
///
|
||||
/// Uses the same URL construction and auth client as [`Self::list_models`],
|
||||
/// but fetches pages without `send_with_retry` so a refresh
|
||||
/// Uses the listing auth client, with native catalog routes for Ollama,
|
||||
/// and fetches pages without `send_with_retry` so a refresh
|
||||
/// failure stays typed and non-fatal — bundled / saved / static rows are
|
||||
/// untouched. The delta is scoped to the base-URL fingerprint and stamped
|
||||
/// with the fetch time; the API key authorizes the request but is **never**
|
||||
/// persisted into the delta or cache. Unknown live rows carry no canonical
|
||||
/// model, capabilities, or pricing, per the #3385 contract.
|
||||
pub async fn fetch_catalog_delta(&self) -> Result<ProviderCatalogDelta, CatalogRefreshError> {
|
||||
if self.api_provider == ProviderKind::Ollama {
|
||||
let origin = crate::local_ollama::ollama_native_origin(&self.base_url);
|
||||
let tags_url = reqwest::Url::parse(&format!("{origin}/api/tags"))
|
||||
.map_err(|_| CatalogRefreshError::InvalidResponse)?;
|
||||
let (body, deadline) = self
|
||||
.models_document_at(tags_url, None, ModelsRequestMode::Refresh)
|
||||
.await
|
||||
.map_err(ModelsFetchError::into_catalog)?;
|
||||
let (tags, mut profiles) = crate::local_ollama::parse_ollama_tags_with_profiles(&body)
|
||||
.map_err(|_| CatalogRefreshError::InvalidResponse)?;
|
||||
if tags.iter().any(|tag| {
|
||||
self.model_bound_secret_values
|
||||
.iter()
|
||||
.any(|secret| !secret.is_empty() && tag.contains(secret))
|
||||
}) {
|
||||
return Err(CatalogRefreshError::InvalidResponse);
|
||||
}
|
||||
let fetched = tokio::time::timeout_at(
|
||||
deadline,
|
||||
crate::local_ollama::fetch_tag_profiles(&tags, &profiles, |tag| {
|
||||
let origin = &origin;
|
||||
async move {
|
||||
let result = async {
|
||||
let request = self
|
||||
.models_http_client
|
||||
.post(format!("{origin}/api/show"))
|
||||
.json(&serde_json::json!({ "model": tag }))
|
||||
.timeout(Duration::from_secs(2));
|
||||
let response =
|
||||
self.authorize_plugin_request(request).await?.send().await?;
|
||||
anyhow::ensure!(
|
||||
response.status().is_success(),
|
||||
"Ollama profile request failed"
|
||||
);
|
||||
let body = bounded_provider_catalog_text(
|
||||
response,
|
||||
PROVIDER_CATALOG_MAX_RESPONSE_BYTES,
|
||||
)
|
||||
.await
|
||||
.map_err(|reason| {
|
||||
anyhow::anyhow!("Ollama profile rejected: {reason:?}")
|
||||
})?;
|
||||
crate::local_ollama::parse_ollama_show_response(&body)
|
||||
}
|
||||
.await;
|
||||
result.map_err(|error: anyhow::Error| {
|
||||
anyhow::anyhow!(self.redact_model_bound_text(&error.to_string()))
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| CatalogRefreshError::Network)?;
|
||||
profiles.extend(fetched);
|
||||
let running_url = reqwest::Url::parse(&format!("{origin}/api/ps"))
|
||||
.map_err(|_| CatalogRefreshError::InvalidResponse)?;
|
||||
let running_deadline =
|
||||
deadline.min(tokio::time::Instant::now() + Duration::from_secs(2));
|
||||
let running = tokio::time::timeout_at(
|
||||
running_deadline,
|
||||
self.models_document_at(running_url, None, ModelsRequestMode::Refresh),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(Result::ok)
|
||||
.map(|(body, _)| body);
|
||||
let delta = crate::local_ollama::ollama_catalog_delta(
|
||||
&self.catalog_provider_id(),
|
||||
&self.base_url,
|
||||
&tags,
|
||||
&profiles,
|
||||
running.as_deref(),
|
||||
);
|
||||
if tokio::time::Instant::now() >= deadline
|
||||
|| delta.offerings.iter().any(|row| {
|
||||
self.model_bound_secret_values
|
||||
.iter()
|
||||
.any(|secret| !secret.is_empty() && row.wire_model_id.contains(secret))
|
||||
})
|
||||
{
|
||||
return Err(CatalogRefreshError::InvalidResponse);
|
||||
}
|
||||
return Ok(delta);
|
||||
}
|
||||
let (body, deadline) = self
|
||||
.models_document(ModelsRequestMode::Refresh)
|
||||
.await
|
||||
|
||||
@@ -1100,6 +1100,7 @@
|
||||
provider: "ollama".into(),
|
||||
wire_model_id: id.into(),
|
||||
endpoint_key: "chat".into(),
|
||||
default_for_provider: id == "alpha:tag",
|
||||
source: CatalogSource::Live {
|
||||
base_url_fingerprint: fingerprint.clone(),
|
||||
fetched_at,
|
||||
|
||||
@@ -13958,6 +13958,22 @@ async fn resolve_cli_exec_route(
|
||||
prompt: &str,
|
||||
force_configured_route: bool,
|
||||
) -> Result<CliAutoRoute> {
|
||||
let mut local_default = None;
|
||||
if let Ok(identity) = config.active_provider_identity()
|
||||
&& identity.provider == crate::config::ProviderKind::Ollama
|
||||
{
|
||||
crate::provider_lake::refresh_local_ollama_catalog(config, &identity).await;
|
||||
if crate::config::is_unresolved_local_ollama_model(model)
|
||||
|| (force_configured_route && model.trim().eq_ignore_ascii_case("auto"))
|
||||
{
|
||||
local_default = Some(
|
||||
crate::route_runtime::resolve_runtime_route_for_identity(config, &identity, None)
|
||||
.map_err(anyhow::Error::msg)?
|
||||
.model,
|
||||
);
|
||||
}
|
||||
}
|
||||
let model = local_default.as_deref().unwrap_or(model);
|
||||
if force_configured_route && !model.trim().eq_ignore_ascii_case("auto") {
|
||||
let reasoning_effort = config
|
||||
.reasoning_effort()
|
||||
|
||||
+201
-91
@@ -1,4 +1,4 @@
|
||||
//! Unconfigured first-run adoption of a live local Ollama catalog.
|
||||
//! Live local Ollama catalog facts and unconfigured first-run adoption.
|
||||
//!
|
||||
//! Virgin sessions default to the DeepSeek costume (`deepseek-flash`). When a
|
||||
//! real local daemon answers `GET /api/tags` (or the OpenAI-compat
|
||||
@@ -186,67 +186,156 @@ struct OllamaTagModel {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
model: String,
|
||||
#[serde(default)]
|
||||
capabilities: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
details: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
context_length: Option<u64>,
|
||||
}
|
||||
|
||||
/// Parse native Ollama `GET /api/tags` JSON into sorted unique tag ids.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn parse_ollama_tags_response(payload: &str) -> anyhow::Result<Vec<String>> {
|
||||
let parsed: OllamaTagsResponse = serde_json::from_str(payload)
|
||||
.map_err(|err| anyhow::anyhow!("Failed to parse Ollama /api/tags JSON: {err}"))?;
|
||||
let mut tags: Vec<String> = parsed
|
||||
.models
|
||||
.into_iter()
|
||||
.filter_map(|row| {
|
||||
let name = row.name.trim();
|
||||
if !name.is_empty() {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
let model = row.model.trim();
|
||||
if !model.is_empty() {
|
||||
Some(model.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
tags.sort();
|
||||
tags.dedup();
|
||||
Ok(tags)
|
||||
parse_ollama_tags_with_profiles(payload).map(|(tags, _)| tags)
|
||||
}
|
||||
|
||||
fn record_ollama_tags_into_lake(endpoint_v1: &str, tags: &[String]) {
|
||||
if tags.is_empty() {
|
||||
return;
|
||||
pub(crate) fn parse_ollama_tags_with_profiles(
|
||||
payload: &str,
|
||||
) -> anyhow::Result<(
|
||||
Vec<String>,
|
||||
std::collections::HashMap<String, OllamaTagProfile>,
|
||||
)> {
|
||||
let parsed: OllamaTagsResponse = serde_json::from_str(payload)
|
||||
.map_err(|err| anyhow::anyhow!("Failed to parse Ollama /api/tags JSON: {err}"))?;
|
||||
anyhow::ensure!(
|
||||
parsed.models.len() <= crate::client::PROVIDER_CATALOG_MAX_ROWS,
|
||||
"Ollama catalog exceeds the model row limit"
|
||||
);
|
||||
let mut profiles = std::collections::HashMap::new();
|
||||
for row in parsed.models {
|
||||
let tag = if row.name.trim().is_empty() {
|
||||
row.model.trim()
|
||||
} else {
|
||||
row.name.trim()
|
||||
};
|
||||
if !crate::provider_lake::valid_catalog_model_id(tag) {
|
||||
continue;
|
||||
}
|
||||
let profile = OllamaTagProfile {
|
||||
capabilities: row.capabilities,
|
||||
context_length: row
|
||||
.details
|
||||
.as_ref()
|
||||
.and_then(|details| details.get("context_length"))
|
||||
.and_then(serde_json::Value::as_u64),
|
||||
};
|
||||
if let Some(previous) = profiles.insert(tag.to_string(), profile.clone()) {
|
||||
anyhow::ensure!(
|
||||
previous == profile,
|
||||
"Ollama reported conflicting tag capabilities"
|
||||
);
|
||||
}
|
||||
}
|
||||
let fingerprint = base_url_fingerprint(endpoint_v1);
|
||||
let fetched_at = now_unix();
|
||||
let offerings = tags
|
||||
.iter()
|
||||
.map(|tag| CatalogOffering {
|
||||
provider: "ollama".into(),
|
||||
wire_model_id: tag.clone(),
|
||||
endpoint_key: "chat".into(),
|
||||
source: CatalogSource::Live {
|
||||
base_url_fingerprint: fingerprint.clone(),
|
||||
fetched_at,
|
||||
},
|
||||
default_for_provider: false,
|
||||
..Default::default()
|
||||
let mut tags: Vec<String> = profiles.keys().cloned().collect();
|
||||
tags.sort();
|
||||
Ok((tags, profiles))
|
||||
}
|
||||
|
||||
pub(crate) fn ollama_catalog_delta(
|
||||
provider: &str,
|
||||
endpoint_v1: &str,
|
||||
tags: &[String],
|
||||
profiles: &std::collections::HashMap<String, OllamaTagProfile>,
|
||||
running_body: Option<&str>,
|
||||
) -> ProviderCatalogDelta {
|
||||
let running: std::collections::HashMap<String, u64> = running_body
|
||||
.and_then(|body| serde_json::from_str::<OllamaTagsResponse>(body).ok())
|
||||
.filter(|response| response.models.len() <= crate::client::PROVIDER_CATALOG_MAX_ROWS)
|
||||
.into_iter()
|
||||
.flat_map(|response| response.models)
|
||||
.filter_map(|row| {
|
||||
let context = row.context_length.filter(|value| *value > 0)?;
|
||||
let context = u32::try_from(context).ok().map(u64::from)?;
|
||||
let tag = if row.name.trim().is_empty() {
|
||||
row.model.trim()
|
||||
} else {
|
||||
row.name.trim()
|
||||
};
|
||||
(!tag.is_empty()).then(|| (tag.to_string(), context))
|
||||
})
|
||||
.collect();
|
||||
let chat_tags: Vec<String> = tags
|
||||
.iter()
|
||||
.filter(|tag| crate::provider_lake::valid_catalog_model_id(tag))
|
||||
.filter(|tag| {
|
||||
profiles
|
||||
.get(*tag)
|
||||
.is_some_and(|profile| profile.has_capability("completion") == Some(true))
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
let preferred = choose_chat_tag(&chat_tags, profiles);
|
||||
let fingerprint = base_url_fingerprint(endpoint_v1);
|
||||
let fetched_at = now_unix();
|
||||
let offerings = chat_tags
|
||||
.iter()
|
||||
.filter_map(|tag| {
|
||||
let profile = profiles.get(tag)?;
|
||||
Some(CatalogOffering {
|
||||
provider: provider.to_string(),
|
||||
wire_model_id: tag.clone(),
|
||||
endpoint_key: "chat".into(),
|
||||
default_for_provider: preferred.as_deref() == Some(tag.as_str()),
|
||||
tool_call: profile.has_capability("tools"),
|
||||
reasoning: profile.has_capability("thinking"),
|
||||
limit: running.get(tag).copied().map(|context| {
|
||||
codewhale_config::models_dev::ModelsDevLimit {
|
||||
context: Some(context),
|
||||
..Default::default()
|
||||
}
|
||||
}),
|
||||
source: CatalogSource::Live {
|
||||
base_url_fingerprint: fingerprint.clone(),
|
||||
fetched_at,
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
ProviderCatalogDelta {
|
||||
provider: provider.to_string(),
|
||||
base_url_fingerprint: fingerprint,
|
||||
fetched_at,
|
||||
offerings,
|
||||
}
|
||||
}
|
||||
|
||||
fn record_ollama_tags_into_lake(
|
||||
endpoint_v1: &str,
|
||||
tags: &[String],
|
||||
profiles: &std::collections::HashMap<String, OllamaTagProfile>,
|
||||
running_body: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let delta = ollama_catalog_delta("ollama", endpoint_v1, tags, profiles, running_body);
|
||||
let chat_tag = delta
|
||||
.offerings
|
||||
.iter()
|
||||
.find(|offering| offering.default_for_provider)
|
||||
.map(|offering| offering.wire_model_id.clone());
|
||||
let ticket = crate::provider_catalog_live::begin_refresh_for_identity(
|
||||
ProviderKind::Ollama,
|
||||
"ollama",
|
||||
endpoint_v1,
|
||||
);
|
||||
let _ = crate::provider_catalog_live::record_success_if_current(
|
||||
&ticket,
|
||||
ProviderCatalogDelta {
|
||||
provider: "ollama".into(),
|
||||
base_url_fingerprint: fingerprint,
|
||||
fetched_at,
|
||||
offerings,
|
||||
},
|
||||
);
|
||||
if matches!(
|
||||
crate::provider_catalog_live::record_success_if_current(&ticket, delta),
|
||||
Some(codewhale_config::catalog::CatalogStatus::Fresh)
|
||||
) {
|
||||
chat_tag
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn probe_client() -> anyhow::Result<reqwest::Client> {
|
||||
@@ -263,7 +352,12 @@ async fn fetch_text(url: &str) -> anyhow::Result<String> {
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!("HTTP {}", response.status());
|
||||
}
|
||||
Ok(response.text().await?)
|
||||
crate::client::bounded_provider_catalog_text(
|
||||
response,
|
||||
crate::client::PROVIDER_CATALOG_MAX_RESPONSE_BYTES,
|
||||
)
|
||||
.await
|
||||
.map_err(|reason| anyhow::anyhow!("Ollama catalog response rejected: {reason:?}"))
|
||||
}
|
||||
|
||||
async fn fetch_tag_profile(origin: &str, tag: &str) -> anyhow::Result<OllamaTagProfile> {
|
||||
@@ -275,36 +369,45 @@ async fn fetch_tag_profile(origin: &str, tag: &str) -> anyhow::Result<OllamaTagP
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!("HTTP {}", response.status());
|
||||
}
|
||||
parse_ollama_show_response(&response.text().await?)
|
||||
let body = crate::client::bounded_provider_catalog_text(
|
||||
response,
|
||||
crate::client::PROVIDER_CATALOG_MAX_RESPONSE_BYTES,
|
||||
)
|
||||
.await
|
||||
.map_err(|reason| anyhow::anyhow!("Ollama profile response rejected: {reason:?}"))?;
|
||||
parse_ollama_show_response(&body)
|
||||
}
|
||||
|
||||
/// Ask `/api/show` about the plausible chat tags. Failures leave a tag
|
||||
/// unprofiled, so the name heuristic decides for it.
|
||||
async fn fetch_tag_profiles(
|
||||
origin: &str,
|
||||
/// Ask `/api/show` about plausible tags whose capabilities are still unknown.
|
||||
/// An unprofiled tag cannot become an executable catalog default.
|
||||
pub(crate) async fn fetch_tag_profiles<F, Fut>(
|
||||
tags: &[String],
|
||||
) -> std::collections::HashMap<String, OllamaTagProfile> {
|
||||
profiles: &std::collections::HashMap<String, OllamaTagProfile>,
|
||||
mut fetch: F,
|
||||
) -> std::collections::HashMap<String, OllamaTagProfile>
|
||||
where
|
||||
F: FnMut(String) -> Fut,
|
||||
Fut: std::future::Future<Output = anyhow::Result<OllamaTagProfile>>,
|
||||
{
|
||||
let candidates: Vec<&String> = tags
|
||||
.iter()
|
||||
.filter(|tag| !looks_like_non_chat_tag(tag))
|
||||
.filter(|tag| {
|
||||
profiles
|
||||
.get(*tag)
|
||||
.is_none_or(|profile| profile.capabilities.is_none())
|
||||
&& !looks_like_non_chat_tag(tag)
|
||||
})
|
||||
.take(SHOW_PROBE_LIMIT)
|
||||
.collect();
|
||||
let lookups = candidates
|
||||
.iter()
|
||||
.map(|tag| fetch_tag_profile(origin, tag.as_str()));
|
||||
let results = futures_util::future::join_all(lookups).await;
|
||||
let results =
|
||||
futures_util::future::join_all(candidates.iter().map(|tag| fetch((*tag).clone()))).await;
|
||||
candidates
|
||||
.into_iter()
|
||||
.zip(results)
|
||||
.filter_map(|(tag, result)| match result {
|
||||
Ok(profile) => Some((tag.clone(), profile)),
|
||||
Err(err) => {
|
||||
tracing::debug!(
|
||||
target: "local_ollama",
|
||||
error = %err,
|
||||
tag = %tag,
|
||||
"POST /api/show probe failed"
|
||||
);
|
||||
tracing::debug!(target: "local_ollama", error = %err, tag = %tag, "POST /api/show probe failed");
|
||||
None
|
||||
}
|
||||
})
|
||||
@@ -320,37 +423,27 @@ pub(crate) async fn probe_live_local_ollama_catalog(
|
||||
let endpoint_v1 = ollama_v1_base_url(config);
|
||||
let origin = ollama_native_origin(&endpoint_v1);
|
||||
let tags_url = format!("{origin}/api/tags");
|
||||
|
||||
let tags = match fetch_text(&tags_url).await {
|
||||
Ok(body) => match parse_ollama_tags_response(&body) {
|
||||
Ok(tags) if !tags.is_empty() => tags,
|
||||
let (tags, mut profiles) = match fetch_text(&tags_url).await {
|
||||
Ok(body) => match parse_ollama_tags_with_profiles(&body) {
|
||||
Ok((tags, profiles)) if !tags.is_empty() => (tags, profiles),
|
||||
Ok(_) => return None,
|
||||
Err(err) => {
|
||||
tracing::debug!(
|
||||
target: "local_ollama",
|
||||
error = %err,
|
||||
"GET /api/tags returned unusable body"
|
||||
);
|
||||
Vec::new()
|
||||
tracing::debug!(target: "local_ollama", error = %err, "GET /api/tags returned unusable body");
|
||||
(Vec::new(), std::collections::HashMap::new())
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::debug!(
|
||||
target: "local_ollama",
|
||||
error = %err,
|
||||
url = %tags_url,
|
||||
"GET /api/tags probe failed"
|
||||
);
|
||||
Vec::new()
|
||||
tracing::debug!(target: "local_ollama", error = %err, url = %tags_url, "GET /api/tags probe failed");
|
||||
(Vec::new(), std::collections::HashMap::new())
|
||||
}
|
||||
};
|
||||
|
||||
let tags = if tags.is_empty() {
|
||||
// Fallback: OpenAI-compat roster (same tags, different shape).
|
||||
let models_url = format!("{}/models", endpoint_v1.trim_end_matches('/'));
|
||||
match fetch_text(&models_url).await {
|
||||
Ok(body) => match crate::client::parse_models_response(&body) {
|
||||
Ok(models) if !models.is_empty() => models.into_iter().map(|m| m.id).collect(),
|
||||
Ok(models) if !models.is_empty() => {
|
||||
models.into_iter().map(|model| model.id).collect()
|
||||
}
|
||||
_ => return None,
|
||||
},
|
||||
Err(_) => return None,
|
||||
@@ -358,10 +451,27 @@ pub(crate) async fn probe_live_local_ollama_catalog(
|
||||
} else {
|
||||
tags
|
||||
};
|
||||
|
||||
record_ollama_tags_into_lake(&endpoint_v1, &tags);
|
||||
let profiles = fetch_tag_profiles(&origin, &tags).await;
|
||||
let chat_tag = choose_chat_tag(&tags, &profiles);
|
||||
let fetched = fetch_tag_profiles(&tags, &profiles, |tag| {
|
||||
let origin = &origin;
|
||||
async move { fetch_tag_profile(origin, &tag).await }
|
||||
})
|
||||
.await;
|
||||
for (tag, profile) in fetched {
|
||||
profiles
|
||||
.entry(tag)
|
||||
.and_modify(|known| {
|
||||
if known.capabilities.is_none() {
|
||||
known.capabilities = profile.capabilities.clone();
|
||||
}
|
||||
if known.context_length.is_none() {
|
||||
known.context_length = profile.context_length;
|
||||
}
|
||||
})
|
||||
.or_insert(profile);
|
||||
}
|
||||
let running_body = fetch_text(&format!("{origin}/api/ps")).await.ok();
|
||||
let chat_tag =
|
||||
record_ollama_tags_into_lake(&endpoint_v1, &tags, &profiles, running_body.as_deref());
|
||||
Some(LiveLocalOllamaCatalog {
|
||||
endpoint_v1,
|
||||
tags,
|
||||
@@ -561,7 +671,7 @@ mod tests {
|
||||
use std::io::{Read, Write};
|
||||
let mut buf = [0u8; 1024];
|
||||
let _ = stream.read(&mut buf);
|
||||
let body = br#"{"models":[{"name":"qwen2.5:0.5b","model":"qwen2.5:0.5b"}]}"#;
|
||||
let body = br#"{"models":[{"name":"qwen2.5:0.5b","model":"qwen2.5:0.5b","capabilities":["completion"]}]}"#;
|
||||
let header = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
body.len()
|
||||
|
||||
@@ -1587,6 +1587,7 @@ mod tests {
|
||||
provider: "ollama".into(),
|
||||
wire_model_id: "qwen2.5:0.5b".into(),
|
||||
endpoint_key: "chat".into(),
|
||||
default_for_provider: true,
|
||||
source: CatalogSource::Live {
|
||||
base_url_fingerprint: fingerprint.clone(),
|
||||
fetched_at: now,
|
||||
|
||||
@@ -904,6 +904,24 @@ fn row_matches_endpoint_fingerprint(row: &CatalogOffering, fingerprint: &str) ->
|
||||
)
|
||||
}
|
||||
|
||||
fn current_local_context(provider: ProviderKind, mut row: CatalogOffering) -> CatalogOffering {
|
||||
if provider != ProviderKind::Ollama {
|
||||
return row;
|
||||
}
|
||||
let now = codewhale_config::catalog::now_unix();
|
||||
let current = matches!(
|
||||
row.fact_source("limit.context"),
|
||||
CatalogSource::Live { base_url_fingerprint, fetched_at }
|
||||
if row_matches_endpoint_fingerprint(&row, base_url_fingerprint)
|
||||
&& *fetched_at <= now
|
||||
&& now - *fetched_at < 60
|
||||
);
|
||||
if !current && let Some(limit) = row.limit.as_mut() {
|
||||
limit.context = None;
|
||||
}
|
||||
row
|
||||
}
|
||||
|
||||
/// Build or reuse the runtime resolver for an exact provider identity.
|
||||
///
|
||||
/// Only a fresh provider-owned partition whose source fingerprint matches the
|
||||
@@ -933,7 +951,8 @@ pub(crate) fn runtime_catalog_resolver_for_identity(
|
||||
fingerprint
|
||||
);
|
||||
|
||||
if let Ok(cache) = RUNTIME_RESOLVER_CACHE.read()
|
||||
if provider != ProviderKind::Ollama
|
||||
&& let Ok(cache) = RUNTIME_RESOLVER_CACHE.read()
|
||||
&& let Some(cached) = cache.get(&cache_key)
|
||||
&& cached.generation == generation
|
||||
&& cached.cloud_generation == cloud_generation
|
||||
@@ -1066,6 +1085,9 @@ pub(crate) fn runtime_catalog_resolver_for_identity(
|
||||
}
|
||||
}
|
||||
}
|
||||
if provider == ProviderKind::Ollama {
|
||||
route_offerings.retain(|_, offering| offering.provider.as_str() != catalog_id.as_ref());
|
||||
}
|
||||
if endpoint_catalog_authoritative {
|
||||
let transport_provider = if provider == ProviderKind::Custom {
|
||||
ProviderKind::Custom.as_str()
|
||||
@@ -1079,7 +1101,7 @@ pub(crate) fn runtime_catalog_resolver_for_identity(
|
||||
row.complete_from(lower, Some(base_url));
|
||||
}
|
||||
row.provider = transport_provider.to_string();
|
||||
let offering = row.to_offering();
|
||||
let offering = current_local_context(provider, row).to_offering();
|
||||
route_offerings.insert(offering_key(&offering), offering);
|
||||
}
|
||||
// The roster replaced its whole scope, but an id it explicitly attests
|
||||
@@ -1094,30 +1116,17 @@ pub(crate) fn runtime_catalog_resolver_for_identity(
|
||||
)
|
||||
.into_values()
|
||||
{
|
||||
let offering = row.to_offering();
|
||||
let offering = current_local_context(provider, row).to_offering();
|
||||
route_offerings
|
||||
.entry(offering_key(&offering))
|
||||
.or_insert(offering);
|
||||
}
|
||||
}
|
||||
|
||||
// Ollama's tag list does not mark a provider default. In the absence of
|
||||
// an explicit tag, elect a stable row only from this fresh exact endpoint.
|
||||
// Other providers retain their reported or curated default semantics.
|
||||
if provider == ProviderKind::Ollama
|
||||
&& endpoint_catalog_authoritative
|
||||
&& !route_offerings.values().any(|offering| {
|
||||
offering.provider.as_str() == catalog_id.as_ref() && offering.default_for_provider
|
||||
})
|
||||
&& let Some(offering) = route_offerings
|
||||
.values_mut()
|
||||
.find(|offering| offering.provider.as_str() == catalog_id.as_ref())
|
||||
{
|
||||
offering.default_for_provider = true;
|
||||
}
|
||||
|
||||
let resolver = RouteResolver::from_offerings(route_offerings.into_values().collect());
|
||||
if let Ok(mut cache) = RUNTIME_RESOLVER_CACHE.write() {
|
||||
if provider != ProviderKind::Ollama
|
||||
&& let Ok(mut cache) = RUNTIME_RESOLVER_CACHE.write()
|
||||
{
|
||||
cache.retain(|_, entry| {
|
||||
entry.generation == generation && entry.cloud_generation == cloud_generation
|
||||
});
|
||||
@@ -1299,6 +1308,7 @@ pub fn catalog_offering_for_model_identity(
|
||||
needle,
|
||||
)
|
||||
.cloned()
|
||||
.map(|row| current_local_context(provider, row))
|
||||
}
|
||||
|
||||
/// What one exact-route lookup found in the provider's own catalog entry.
|
||||
@@ -1366,7 +1376,7 @@ pub(crate) fn catalog_offering_for_route(
|
||||
row.complete_from(lower, Some(base_url));
|
||||
}
|
||||
}
|
||||
return Some(*row);
|
||||
return Some(current_local_context(provider, *row));
|
||||
}
|
||||
// A successful fetch that does not list this id is authoritative about
|
||||
// its absence. Only an explicitly attested unlisted row may still name
|
||||
@@ -1375,7 +1385,8 @@ pub(crate) fn catalog_offering_for_route(
|
||||
Ok(Some(RouteCatalogLookup::NotListed)) => {
|
||||
return cloud_unlisted_offerings_for_route(provider, identity, base_url)
|
||||
.into_values()
|
||||
.find(|row| row.wire_model_id == model);
|
||||
.find(|row| row.wire_model_id == model)
|
||||
.map(|row| current_local_context(provider, row));
|
||||
}
|
||||
Ok(Some(RouteCatalogLookup::NoRoster)) | Ok(None) | Err(_) => {}
|
||||
}
|
||||
@@ -1393,7 +1404,7 @@ pub(crate) fn catalog_offering_for_route(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(offering)
|
||||
Some(current_local_context(provider, offering))
|
||||
}
|
||||
|
||||
/// Config declarations take precedence only for the exact selected route.
|
||||
@@ -1924,6 +1935,59 @@ pub(crate) async fn update_provider_catalog(
|
||||
receipt
|
||||
}
|
||||
|
||||
pub(crate) async fn refresh_local_ollama_catalog(config: &Config, identity: &ProviderIdentity) {
|
||||
#[cfg(test)]
|
||||
let _ = (config, identity);
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
if identity.provider != ProviderKind::Ollama {
|
||||
return;
|
||||
}
|
||||
static REFRESH_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
|
||||
let queued_at = codewhale_config::catalog::now_unix();
|
||||
let refresh_guard = REFRESH_LOCK.lock().await;
|
||||
let handle = tokio::runtime::Handle::current();
|
||||
let config = config.clone();
|
||||
let identity = identity.clone();
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
let _refresh_guard = refresh_guard;
|
||||
let now = codewhale_config::catalog::now_unix();
|
||||
let base_url = config.base_url_for_route(&identity);
|
||||
let recent = crate::provider_catalog_live::with_cached_entry_for_route(
|
||||
identity.provider,
|
||||
identity.key.as_str(),
|
||||
&base_url,
|
||||
|entry| {
|
||||
entry.is_fresh(now)
|
||||
&& matches!(
|
||||
entry.status,
|
||||
codewhale_config::catalog::CatalogStatus::Fresh
|
||||
)
|
||||
&& entry.fetched_at >= queued_at
|
||||
&& entry.fetched_at <= now
|
||||
&& entry.offerings.iter().any(|row| row.default_for_provider)
|
||||
},
|
||||
)
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
if recent
|
||||
&& runtime_catalog_resolver_for_identity(
|
||||
identity.provider,
|
||||
Some(identity.key.as_str()),
|
||||
&base_url,
|
||||
codewhale_config::catalog::CatalogStatus::Fresh,
|
||||
)
|
||||
.endpoint_catalog_authoritative
|
||||
{
|
||||
return;
|
||||
}
|
||||
handle.block_on(update_provider_catalog(&config, &identity));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn refresh_account_catalog_before_first_request(config: &Config) {
|
||||
let Ok(identity) = config.active_provider_identity() else {
|
||||
return;
|
||||
@@ -2358,7 +2422,9 @@ pub fn provider_capability_with_wire(
|
||||
// Provider-owned wire IDs can have exact catalog facts without a legacy
|
||||
// model-only row. Reuse those facts before conservative fallback budgets.
|
||||
let offering = bundled_catalog_offering_for_model(provider, resolved_model);
|
||||
let context_window = if official_deepseek {
|
||||
let context_window = if provider == ProviderKind::Ollama {
|
||||
8192
|
||||
} else if official_deepseek {
|
||||
codewhale_models::DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS
|
||||
} else if let Some(window) = codewhale_models::context_window_for_model(resolved_model) {
|
||||
window
|
||||
@@ -2369,8 +2435,6 @@ pub fn provider_capability_with_wire(
|
||||
.and_then(|window| u32::try_from(window).ok())
|
||||
{
|
||||
window
|
||||
} else if matches!(provider, ProviderKind::Ollama) {
|
||||
8192
|
||||
} else {
|
||||
codewhale_models::LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS
|
||||
};
|
||||
|
||||
@@ -2496,7 +2496,7 @@ mod tests {
|
||||
.into_iter()
|
||||
.map(|model| {
|
||||
let mut row = live_catalog_offering("ollama", model, endpoint);
|
||||
row.default_for_provider = false; // Real Ollama tags have no default flag.
|
||||
row.default_for_provider = model == "alpha:tag";
|
||||
row
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -1737,6 +1737,15 @@ pub(crate) fn open_runtime_threads_for_host(
|
||||
// its capabilities available to route resolution.
|
||||
crate::models_dev_live::maybe_load_persisted_cache();
|
||||
crate::provider_catalog_live::maybe_load_persisted_cache_for_config(config);
|
||||
if let Ok(identity) = config.active_provider_identity()
|
||||
&& identity.provider == ProviderKind::Ollama
|
||||
&& let Ok(handle) = tokio::runtime::Handle::try_current()
|
||||
{
|
||||
let config = config.clone();
|
||||
handle.spawn(async move {
|
||||
crate::provider_lake::refresh_local_ollama_catalog(&config, &identity).await;
|
||||
});
|
||||
}
|
||||
crate::models_dev_live::spawn_background_refresh();
|
||||
Ok((manager, workshop_activation))
|
||||
}
|
||||
@@ -3317,6 +3326,33 @@ async fn health() -> Json<HealthResponse> {
|
||||
})
|
||||
}
|
||||
|
||||
async fn refresh_local_default_catalog(
|
||||
state: &RuntimeApiState,
|
||||
requested: Option<&str>,
|
||||
provider: Option<&str>,
|
||||
provider_id: Option<&str>,
|
||||
) {
|
||||
if requested.is_some_and(|model| {
|
||||
!model.trim().is_empty()
|
||||
&& !model.trim().eq_ignore_ascii_case("auto")
|
||||
&& !crate::config::is_unresolved_local_ollama_model(model)
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
let config = state.config.read().clone();
|
||||
let provider = provider.map(str::trim).filter(|value| !value.is_empty());
|
||||
let identity = if provider.is_some() || provider_id.is_some() {
|
||||
config.resolve_persisted_provider_identity(provider, provider_id)
|
||||
} else {
|
||||
config.active_provider_identity()
|
||||
};
|
||||
if let Ok(identity) = identity
|
||||
&& identity.provider == ProviderKind::Ollama
|
||||
{
|
||||
crate::provider_lake::refresh_local_ollama_catalog(&config, &identity).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_request_model(config: &Config, requested: Option<&str>) -> Result<String, ApiError> {
|
||||
if let Some(model) = requested {
|
||||
return Ok(model.to_string());
|
||||
@@ -3343,6 +3379,13 @@ async fn create_task(
|
||||
if req.workspace.is_none() {
|
||||
req.workspace = Some(state.workspace.clone());
|
||||
}
|
||||
refresh_local_default_catalog(
|
||||
&state,
|
||||
req.model.as_deref(),
|
||||
req.model_provider.as_deref(),
|
||||
req.model_provider_id.as_deref(),
|
||||
)
|
||||
.await;
|
||||
if req.model.is_none() && req.model_provider.is_none() && req.model_provider_id.is_none() {
|
||||
req.model = Some(runtime_request_model(&state.config.read(), None)?);
|
||||
}
|
||||
@@ -3364,6 +3407,13 @@ async fn create_thread(
|
||||
if req.mode.as_ref().is_none_or(|m| m.trim().is_empty()) {
|
||||
req.mode = Some("agent".to_string());
|
||||
}
|
||||
refresh_local_default_catalog(
|
||||
&state,
|
||||
req.model.as_deref(),
|
||||
req.model_provider.as_deref(),
|
||||
req.model_provider_id.as_deref(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let thread = state
|
||||
.runtime_threads
|
||||
@@ -9775,6 +9825,7 @@ async fn stream_turn(
|
||||
|
||||
crate::image_attach::prepare_runtime_images(&req.images).map_err(map_thread_err)?;
|
||||
|
||||
refresh_local_default_catalog(&state, req.model.as_deref(), None, None).await;
|
||||
let model = runtime_request_model(&state.config.read(), req.model.as_deref())?;
|
||||
if req.max_output_tokens.is_some() {
|
||||
let config = state.config.read();
|
||||
|
||||
@@ -19098,6 +19098,7 @@ fn api_provider_default_and_model_list_follow_exact_local_catalog() {
|
||||
provider: "ollama".into(),
|
||||
wire_model_id: "local-default:tag".into(),
|
||||
endpoint_key: "chat".into(),
|
||||
default_for_provider: true,
|
||||
source: CatalogSource::Live {
|
||||
base_url_fingerprint: fingerprint.clone(),
|
||||
fetched_at,
|
||||
@@ -19216,6 +19217,7 @@ async fn api_config_reports_local_default_availability_without_blocking_config_r
|
||||
provider: "ollama".into(),
|
||||
wire_model_id: "available:tag".into(),
|
||||
endpoint_key: "chat".into(),
|
||||
default_for_provider: true,
|
||||
source: CatalogSource::Live {
|
||||
base_url_fingerprint: fingerprint,
|
||||
fetched_at,
|
||||
|
||||
@@ -673,6 +673,7 @@ pub(crate) async fn switch_provider(
|
||||
config.default_text_model = None;
|
||||
}
|
||||
|
||||
crate::provider_lake::refresh_local_ollama_catalog(config, &identity).await;
|
||||
let resolved_route =
|
||||
match resolve_runtime_route_for_identity(config, &identity, model_override.as_deref()) {
|
||||
Ok(route) => route,
|
||||
|
||||
+11
-1
@@ -1282,7 +1282,7 @@ provider = "ollama"
|
||||
|
||||
[profiles.ollama.providers.ollama]
|
||||
base_url = "http://localhost:11434/v1"
|
||||
model = "qwen2.5-coder:7b" # any tag `ollama list` shows
|
||||
model = "qwen2.5-coder:7b" # an installed chat-capable tag
|
||||
|
||||
[profiles.ollama-cloud]
|
||||
provider = "ollama-cloud"
|
||||
@@ -1299,6 +1299,16 @@ Select a profile with:
|
||||
|
||||
If a profile is selected but missing, codewhale exits with an error listing available profiles.
|
||||
|
||||
For local Ollama, omit `providers.ollama.model` to select a tag whose native
|
||||
catalog confirms completion support. `/provider ollama`, exec, and Runtime API
|
||||
default-model admission refresh that catalog; embedding-only catalogs have no
|
||||
automatic chat default. An explicit model keeps its exact tag.
|
||||
|
||||
A loaded model's `/api/ps` context observation is usable for 60 seconds.
|
||||
`/api/show` reports the trained maximum, which does not establish the served
|
||||
window. An unloaded model or an expired observation uses the unverified 8192-token
|
||||
fallback; explicit context-window configuration still takes precedence.
|
||||
|
||||
## Legacy top-level `base_url` and `api_key`
|
||||
|
||||
Older releases kept DeepSeek's endpoint and key at the top of `config.toml`,
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "e6a3b9066cf7d2240ccc2380d02b34635c7b1d1d04a1e32c39c284426a27f26f",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. A file over 100 KiB is cut to its first 100 KiB\n and ends with a `[…truncated: N of M bytes omitted …]` marker, and\n `/context report` names the file. Instruction and rules files share one\n 48 KiB budget, and instruction files claim it first. Instruction text past\n the budget is dropped from the front, broadest scope first (the global\n file, then the repository root), so a file larger than the budget keeps\n only its end;Line truncated
|
||||
"sha256": "407bf73507b95a8010582d742e8c365f959cc9cdda32c771067a96aa4fa83825",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. A file over 100 KiB is cut to its first 100 KiB\n and ends with a `[…truncated: N of M bytes omitted …]` marker, and\n `/context report` names the file. Instruction and rules files share one\n 48 KiB budget, and instruction files claim it first. Instruction text past\n the budget is dropped from the front, broadest scope first (the global\n file, then the repository root), so a file larger than the budget keeps\n only its end;Line truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/CODEWHALE_AGENT.md",
|
||||
@@ -64,8 +64,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "e6a3b9066cf7d2240ccc2380d02b34635c7b1d1d04a1e32c39c284426a27f26f",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. A file over 100 KiB is cut to its first 100 KiB\n and ends with a `[…truncated: N of M bytes omitted …]` marker, and\n `/context report` names the file. Instruction and rules files share one\n 48 KiB budget, and instruction files claim it first. Instruction text past\n the budget is dropped from the front, broadest scope first (the global\n file, then the repository root), so a file larger than the budget keeps\n only its end;Line truncated
|
||||
"sha256": "407bf73507b95a8010582d742e8c365f959cc9cdda32c771067a96aa4fa83825",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. A file over 100 KiB is cut to its first 100 KiB\n and ends with a `[…truncated: N of M bytes omitted …]` marker, and\n `/context report` names the file. Instruction and rules files share one\n 48 KiB budget, and instruction files claim it first. Instruction text past\n the budget is dropped from the front, broadest scope first (the global\n file, then the repository root), so a file larger than the budget keeps\n only its end;Line truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/LEGACY_PATHS.md",
|
||||
@@ -96,8 +96,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "e6a3b9066cf7d2240ccc2380d02b34635c7b1d1d04a1e32c39c284426a27f26f",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. A file over 100 KiB is cut to its first 100 KiB\n and ends with a `[…truncated: N of M bytes omitted …]` marker, and\n `/context report` names the file. Instruction and rules files share one\n 48 KiB budget, and instruction files claim it first. Instruction text past\n the budget is dropped from the front, broadest scope first (the global\n file, then the repository root), so a file larger than the budget keeps\n only its end;Line truncated
|
||||
"sha256": "407bf73507b95a8010582d742e8c365f959cc9cdda32c771067a96aa4fa83825",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. A file over 100 KiB is cut to its first 100 KiB\n and ends with a `[…truncated: N of M bytes omitted …]` marker, and\n `/context report` names the file. Instruction and rules files share one\n 48 KiB budget, and instruction files claim it first. Instruction text past\n the budget is dropped from the front, broadest scope first (the global\n file, then the repository root), so a file larger than the budget keeps\n only its end;Line truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -224,8 +224,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "e6a3b9066cf7d2240ccc2380d02b34635c7b1d1d04a1e32c39c284426a27f26f",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. A file over 100 KiB is cut to its first 100 KiB\n and ends with a `[…truncated: N of M bytes omitted …]` marker, and\n `/context report` names the file. Instruction and rules files share one\n 48 KiB budget, and instruction files claim it first. Instruction text past\n the budget is dropped from the front, broadest scope first (the global\n file, then the repository root), so a file larger than the budget keeps\n only its end;Line truncated
|
||||
"sha256": "407bf73507b95a8010582d742e8c365f959cc9cdda32c771067a96aa4fa83825",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. A file over 100 KiB is cut to its first 100 KiB\n and ends with a `[…truncated: N of M bytes omitted …]` marker, and\n `/context report` names the file. Instruction and rules files share one\n 48 KiB budget, and instruction files claim it first. Instruction text past\n the budget is dropped from the front, broadest scope first (the global\n file, then the repository root), so a file larger than the budget keeps\n only its end;Line truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -256,8 +256,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "e6a3b9066cf7d2240ccc2380d02b34635c7b1d1d04a1e32c39c284426a27f26f",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. A file over 100 KiB is cut to its first 100 KiB\n and ends with a `[…truncated: N of M bytes omitted …]` marker, and\n `/context report` names the file. Instruction and rules files share one\n 48 KiB budget, and instruction files claim it first. Instruction text past\n the budget is dropped from the front, broadest scope first (the global\n file, then the repository root), so a file larger than the budget keeps\n only its end;Line truncated
|
||||
"sha256": "407bf73507b95a8010582d742e8c365f959cc9cdda32c771067a96aa4fa83825",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. A file over 100 KiB is cut to its first 100 KiB\n and ends with a `[…truncated: N of M bytes omitted …]` marker, and\n `/context report` names the file. Instruction and rules files share one\n 48 KiB budget, and instruction files claim it first. Instruction text past\n the budget is dropped from the front, broadest scope first (the global\n file, then the repository root), so a file larger than the budget keeps\n only its end;Line truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user