mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 17:09:13 -10:00
fix(exec): read the account model listing before the first request
Account listings (the Codewhale account, Baseten and plugin account catalogs) are kept only in the memory of the process that read them, by design: they are credential-scoped and are never reloaded from disk (provider_catalog_live::maybe_load_persisted_cache_for_config forgets them). The interactive TUI refreshes its listing at launch and the desktop app asks its Engine to, but exec, review and the workflow tool never did, so a fresh CLI process resolved an account model with no route limits and sent the model-only default. exec, review and the workflow tool now run one awaited account-listing refresh before their first request when the route is account-scoped, off the async runtime (spawn_blocking + block_on, as the Runtime API refresh route does). If the listing cannot be read the run continues with built-in limits and prints one stderr line, naming the refresh failure with the model picker's labels (catalog_refresh_error_label, now pub(crate)) rather than the receipt's "cache_write_failed", which a rejected listing also produced. The account-scope rule now has one definition (route_is_account_scoped). Not covered: auto and inventory model selection, which runs before the refresh; Runtime API and app-server clients that never ask for a refresh; saved non-account rosters, which exec still does not load. docs/PROVIDERS.md (both languages) says so; the CHANGELOG line also coversfe2f1d8d03, which carried the parser half without one. Verification: checked once on the final batch tree (parentdb89057d84): cargo fmt --check clean; tui lib nextest across route_budget, provider_catalog_live, provider_lake, mounted_history_tests, session_manager, client, core::engine, compaction, tools::subagent, runtime_api, purge, commands::contract, tui::model_picker and whats_new 2757 passed, 0 failed; tui integration exec_turn_usage 11 passed, 0 failed; feature_registry 2 passed, 0 failed; clippy -p codewhale-tui --lib --tests -D warnings with the CI allow-list clean; check-blocking- calls-budget 540 sites, within budget. Not run: hosted CI and the loopback product captures (those follow on the release build). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f35645acbc
commit
d92c96a25e
9 files changed
+83
-8
No files matched your search
@@ -106,6 +106,13 @@ guidance.
|
||||
counted, instead of rewriting the file and then reporting that nothing
|
||||
changed. Refs #6955. Thanks @7jrxt42BxFZo4iAnN4CX.
|
||||
|
||||
- The input, output and context limits your Codewhale account states for a
|
||||
model now apply to its requests. The desktop app and interactive sessions
|
||||
take them from the account's model listing, and `codewhale exec`,
|
||||
`codewhale review` and the workflow tool read that listing once before
|
||||
their first request; if it cannot be read, the run continues with built-in
|
||||
limits and says so on stderr.
|
||||
|
||||
- When a route states an output limit larger than half its context window,
|
||||
as a server that shares one window between input and output does,
|
||||
Codewhale compacts the conversation to keep a quarter of the window (at
|
||||
|
||||
@@ -106,6 +106,13 @@ guidance.
|
||||
counted, instead of rewriting the file and then reporting that nothing
|
||||
changed. Refs #6955. Thanks @7jrxt42BxFZo4iAnN4CX.
|
||||
|
||||
- The input, output and context limits your Codewhale account states for a
|
||||
model now apply to its requests. The desktop app and interactive sessions
|
||||
take them from the account's model listing, and `codewhale exec`,
|
||||
`codewhale review` and the workflow tool read that listing once before
|
||||
their first request; if it cannot be read, the run continues with built-in
|
||||
limits and says so on stderr.
|
||||
|
||||
- When a route states an output limit larger than half its context window,
|
||||
as a server that shares one window between input and output does,
|
||||
Codewhale compacts the conversation to keep a quarter of the window (at
|
||||
|
||||
@@ -378,6 +378,7 @@ pub(crate) async fn run_exec_agent(
|
||||
|
||||
let route = resolve_cli_exec_route(config, model, prompt, force_configured_route).await?;
|
||||
let execution_config = config_for_cli_route(config, &route)?;
|
||||
crate::provider_lake::refresh_account_catalog_before_first_request(&execution_config).await;
|
||||
let auto_model = route.auto_model;
|
||||
let effective_identity = execution_config
|
||||
.active_provider_identity()
|
||||
|
||||
@@ -10370,6 +10370,7 @@ async fn run_review(config: &Config, args: ReviewArgs) -> Result<()> {
|
||||
.expect("review has at least one prompt");
|
||||
let route = resolve_cli_exec_route(config, &model, route_input, force_configured_route).await?;
|
||||
let execution_config = config_for_cli_route(config, &route)?;
|
||||
crate::provider_lake::refresh_account_catalog_before_first_request(&execution_config).await;
|
||||
let route_provider = execution_config
|
||||
.active_provider_identity()
|
||||
.map_err(anyhow::Error::msg)?
|
||||
@@ -14317,6 +14318,7 @@ async fn run_workflow_tool_command_inner(
|
||||
)
|
||||
.await?;
|
||||
let execution_config = config_for_cli_route(&config, &route)?;
|
||||
crate::provider_lake::refresh_account_catalog_before_first_request(&execution_config).await;
|
||||
let route_identity = execution_config
|
||||
.active_provider_identity()
|
||||
.map_err(anyhow::Error::msg)
|
||||
|
||||
@@ -683,6 +683,19 @@ fn is_account_scoped_scope(provider: &str, fingerprint: &str) -> bool {
|
||||
== base_url_fingerprint(ProviderKind::Codewhale.provider().default_base_url())
|
||||
}
|
||||
|
||||
pub(crate) fn route_is_account_scoped(
|
||||
config: &Config,
|
||||
identity: &crate::config::ProviderIdentity,
|
||||
) -> bool {
|
||||
is_account_scoped_scope(
|
||||
&storage_provider(
|
||||
identity.provider,
|
||||
&canonical_provider_scope(identity.key.as_str()),
|
||||
),
|
||||
&base_url_fingerprint(&config.base_url_for_route(identity)),
|
||||
)
|
||||
}
|
||||
|
||||
fn cache_lock_path(path: &Path) -> PathBuf {
|
||||
let mut name = path
|
||||
.file_name()
|
||||
@@ -1248,10 +1261,7 @@ pub fn maybe_load_persisted_cache_for_config(config: &Config) -> usize {
|
||||
let provider = identity.provider;
|
||||
let provider_identity = canonical_provider_scope(identity.key.as_str());
|
||||
let fingerprint = base_url_fingerprint(&config.base_url_for_route(&identity));
|
||||
if is_account_scoped_scope(
|
||||
&storage_provider(provider, &provider_identity),
|
||||
&fingerprint,
|
||||
) {
|
||||
if route_is_account_scoped(config, &identity) {
|
||||
forget_account_scoped_provider(provider, &provider_identity);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1868,6 +1868,50 @@ pub(crate) async fn update_provider_catalog(
|
||||
receipt
|
||||
}
|
||||
|
||||
pub(crate) async fn refresh_account_catalog_before_first_request(config: &Config) {
|
||||
let Ok(identity) = config.active_provider_identity() else {
|
||||
return;
|
||||
};
|
||||
if !crate::provider_catalog_live::route_is_account_scoped(config, &identity)
|
||||
|| live_catalog_unavailable(config, &identity)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let handle = tokio::runtime::Handle::current();
|
||||
let refresh_config = config.clone();
|
||||
let refresh_identity = identity.clone();
|
||||
let receipt = tokio::task::spawn_blocking(move || {
|
||||
handle.block_on(update_provider_catalog(&refresh_config, &refresh_identity))
|
||||
})
|
||||
.await
|
||||
.ok();
|
||||
let status = crate::provider_catalog_live::status_for_route(
|
||||
identity.provider,
|
||||
identity.key.as_str(),
|
||||
&config.active_route_base_url(),
|
||||
);
|
||||
if !matches!(status, CatalogStatus::Fresh) {
|
||||
let reason = match &status {
|
||||
CatalogStatus::Failed { reason } => {
|
||||
crate::tui::model_picker::catalog_refresh_error_label(*reason)
|
||||
}
|
||||
_ => receipt.map_or("refresh_failed", |receipt| {
|
||||
receipt.error.unwrap_or(receipt.outcome)
|
||||
}),
|
||||
};
|
||||
tracing::warn!(
|
||||
target: "provider_catalog",
|
||||
provider = %identity.key,
|
||||
reason,
|
||||
"account model listing unavailable before the first request"
|
||||
);
|
||||
eprintln!(
|
||||
"codewhale: could not read the {} account model listing ({reason}); requesting with built-in model limits.",
|
||||
identity.key
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run_models(
|
||||
config: &Config,
|
||||
update: bool,
|
||||
|
||||
@@ -3952,7 +3952,7 @@ fn provider_catalog_source_label(receipt: Option<&(CatalogStatus, bool)>) -> Str
|
||||
}
|
||||
}
|
||||
|
||||
fn catalog_refresh_error_label(error: CatalogRefreshError) -> &'static str {
|
||||
pub(crate) fn catalog_refresh_error_label(error: CatalogRefreshError) -> &'static str {
|
||||
match error {
|
||||
CatalogRefreshError::Unauthorized => "unauthorized",
|
||||
CatalogRefreshError::Forbidden => "forbidden",
|
||||
|
||||
+6
-2
@@ -177,8 +177,12 @@ that invocation.
|
||||
Plain `models` lists the active provider's saved catalog without provider requests.
|
||||
For ChatGPT, it validates the selected registration locally before reading
|
||||
that account's saved roster. Successful refreshes are saved under Codewhale's
|
||||
catalog directory and used by the model/provider pickers. Cache files are
|
||||
scoped to provider identity and endpoint; a failed refresh preserves prior
|
||||
catalog directory and used by the model/provider pickers, except account
|
||||
listings (the Codewhale account, Baseten and plugin account catalogs), which
|
||||
stay in the memory of the process that read them; `exec`, `review` and the
|
||||
workflow tool therefore read the account listing once before their first
|
||||
request, and continue with built-in limits if it cannot be read. Cache files
|
||||
are scoped to provider identity and endpoint; a failed refresh preserves prior
|
||||
rows. Text output reports source, last successful fetch time (Unix seconds),
|
||||
and freshness. `--update --json` adds per-source receipts and aggregate counts;
|
||||
partial failures return a nonzero exit code after writing those receipts.
|
||||
|
||||
@@ -85,7 +85,7 @@ codewhale models --provider openai --json
|
||||
|
||||
`models --update`(也可写作 `--refresh`)会更新共享的 Models.dev 元数据,并用每个已配置提供商各自的凭据调用其现有的 `/models` 端点。`--provider ID` 把刷新限定在该提供商,包括具名的自定义端点。它不发起任何推理请求,也不会更改已保存的提供商或模型。命令行传入的 API 密钥只限于当前提供商;本次调用中其他路由会被报告为已跳过。
|
||||
|
||||
不带 `--update` 的 `models` 会列出当前提供商已保存的目录,不发起提供商请求。ChatGPT 路由会先在本地验证所选注册,再读取该账户的缓存目录。刷新成功后,结果保存在 Codewhale 的目录文件夹下,供模型/提供商选择器使用。缓存文件按提供商身份和端点划分;刷新失败时会保留原有行。文本输出报告来源、最近一次成功获取的时间(Unix 秒)和新鲜度。`--update --json` 会额外给出每个来源的回执和汇总计数;部分失败时,会在写完这些回执后返回非零退出码。普通的 `models --json` 仍保持模型数组格式。随附的或已配置的回退项并不能证明账户可以使用列出的每个模型。
|
||||
不带 `--update` 的 `models` 会列出当前提供商已保存的目录,不发起提供商请求。ChatGPT 路由会先在本地验证所选注册,再读取该账户的缓存目录。刷新成功后,结果保存在 Codewhale 的目录文件夹下,供模型/提供商选择器使用;账户目录(Codewhale 账户、Baseten 和插件账户目录)除外,它们只保留在读取它们的进程内存中,因此 `exec`、`review` 和工作流工具会在第一次请求前读取一次账户模型列表,读取失败时使用内置限制继续运行。缓存文件按提供商身份和端点划分;刷新失败时会保留原有行。文本输出报告来源、最近一次成功获取的时间(Unix 秒)和新鲜度。`--update --json` 会额外给出每个来源的回执和汇总计数;部分失败时,会在写完这些回执后返回非零退出码。普通的 `models --json` 仍保持模型数组格式。随附的或已配置的回退项并不能证明账户可以使用列出的每个模型。
|
||||
|
||||
### 使用 ChatGPT 登录
|
||||
|
||||
|
||||
Reference in new issue
Block a user