mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 08:04:25 -10:00
fix(models): read the resolved catalog, and stop the transport seam erasing sourced facts (#6396 items 5 and 11)
Two named defects from the models.dev repair checklist, both about consumers
reading a different set of facts than the rest of the product.
Item 11 — `codewhale model list` built its own `ModelRegistry::default()`,
which is the bundled reviewed seed and nothing else: no models.dev live rows,
no per-provider roster, no cloud overlay. The plural `codewhale models` path
consumes the provider lake. The singular command now reads
`all_catalog_models_for_provider` — the same reader the picker, subagent model
validation and fleet command already use — after the same
`maybe_load_persisted_cache()` hydration the product's own models path
performs. Two crate-root re-exports expose those two items rather than making
the 4,360-line `provider_lake` module public.
`ModelCommand::Resolve`/`Set` keep `ModelRegistry`: they need registry and
config semantics (route hints, rejection metadata, route-preference writes),
not the catalog.
Item 5 — the runtime catalog resolver let the curated transport seam overwrite
whole rows. 108 of the 123 curated transport rows state only provider, id,
endpoint_key and default_for_provider, and `ReviewedTransport::to_offering`
hardcodes `UnknownOrStale` pricing, so inserting them whole erased the limits,
capabilities, canonical identity and price of any models.dev row for the same
`(provider, wire_model_id)` — while the picker kept reading the models.dev row,
so picker and runtime disagreed about the same model. The insert is now
field-aware: the transport row still wins every fact it states (`false` stays
`false`), and genuine absence is filled from the offering already in the map.
This is what `RouteResolver::new` already does for opencode-go — "The transport
seam must not erase already sourced Go limits/prices" — generalized to the
route resolver instead of carved out for one provider. Pricing is deliberately
not filled: opencode-zen has a competing provider-owned rate authority, which
is item 6's call and landed separately in 22a6695ae.
The third site the audit named, `provider_lake.rs:956`'s neighbouring insert,
was already correct: `row.complete_from(lower, Some(base_url))` runs first, and
`complete_facts_from` fills only `None` fields. `CatalogCompiler::compile`'s
bundled→live insert is left alone deliberately: both layers are the same
authority, `complete_from` refuses to complete a `ModelsDevLive` self row by
design (filling a fresh upstream row from the stale seed would re-assert facts
upstream removed, which item 3 forbids), and `compile` has no production caller.
Evidence:
- nextest -p codewhale-config -p codewhale-tui -p codewhale-cli with the
provider_lake, models_dev_live, pricing, route::conformance, catalog,
model_command and provider_catalog_live selectors:
Summary [6.246s] 519 tests run: 519 passed, 0 failed
- nextest -p codewhale-cli -E 'test(model_command) | test(model_set_)':
5 tests run, 5 passed (proves the new re-exports resolve and the CLI builds)
Still open on #6396: item 9 (context/output budgeting through the runtime),
item 13 (shrink catalog_corrections.json), the `app-server` projection still
building its own registry, the docs that still describe the static registry,
and the installed acceptance.
This commit is contained in:
1 parent
c9b8e40991
commit
7a2ec8f7ee
3 files changed
+63
-7
No files matched your search
+10
-6
@@ -5694,19 +5694,23 @@ fn run_model_command(
|
||||
top_level_provider: Option<ProviderKind>,
|
||||
resolved_runtime: &ResolvedRuntimeOptions,
|
||||
) -> Result<()> {
|
||||
let registry = ModelRegistry::default();
|
||||
match command {
|
||||
ModelCommand::List { provider } => {
|
||||
let filter = model_command_provider_hint(provider, top_level_provider);
|
||||
for model in registry.list().into_iter().filter(|m| match filter {
|
||||
Some(p) => m.provider == p,
|
||||
None => true,
|
||||
}) {
|
||||
println!("{} ({})", model.id, model.provider.as_str());
|
||||
codewhale_tui::maybe_load_persisted_cache();
|
||||
let providers: &[ProviderKind] = match &filter {
|
||||
Some(provider) => std::slice::from_ref(provider),
|
||||
None => ProviderKind::all(),
|
||||
};
|
||||
for provider in providers {
|
||||
for model in codewhale_tui::all_catalog_models_for_provider(*provider) {
|
||||
println!("{model} ({})", provider.as_str());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ModelCommand::Resolve { model, provider } => {
|
||||
let registry = ModelRegistry::default();
|
||||
// Only `model resolve --provider X` is a hypothetical. The
|
||||
// top-level `--provider` is the route this process is actually on,
|
||||
// and it is already folded into `resolved_runtime` — treating it as
|
||||
|
||||
@@ -73,6 +73,7 @@ mod model_profile;
|
||||
mod model_relevance;
|
||||
mod model_routing;
|
||||
mod models_dev_live;
|
||||
pub use models_dev_live::maybe_load_persisted_cache;
|
||||
mod network_policy;
|
||||
mod notify;
|
||||
mod oauth;
|
||||
@@ -86,6 +87,7 @@ mod project_context_cache;
|
||||
mod prompts;
|
||||
mod provider_catalog_live;
|
||||
mod provider_lake;
|
||||
pub use provider_lake::all_catalog_models_for_provider;
|
||||
mod provider_readiness;
|
||||
mod purge;
|
||||
pub mod reasoning_preference;
|
||||
|
||||
@@ -785,6 +785,51 @@ fn offering_key(offering: &ProviderModelOffering) -> (String, String) {
|
||||
)
|
||||
}
|
||||
|
||||
fn offering_with_absent_facts_filled(
|
||||
mut offering: ProviderModelOffering,
|
||||
lower: &ProviderModelOffering,
|
||||
) -> ProviderModelOffering {
|
||||
if offering.canonical_model.is_none() {
|
||||
offering.canonical_model.clone_from(&lower.canonical_model);
|
||||
}
|
||||
if offering.limits.context_tokens.is_none() {
|
||||
offering.limits.context_tokens = lower.limits.context_tokens;
|
||||
}
|
||||
if offering.limits.input_tokens.is_none() {
|
||||
offering.limits.input_tokens = lower.limits.input_tokens;
|
||||
}
|
||||
if offering.limits.output_tokens.is_none() {
|
||||
offering.limits.output_tokens = lower.limits.output_tokens;
|
||||
}
|
||||
for (target, source) in [
|
||||
(
|
||||
&mut offering.capabilities.attachments,
|
||||
lower.capabilities.attachments,
|
||||
),
|
||||
(
|
||||
&mut offering.capabilities.image_input,
|
||||
lower.capabilities.image_input,
|
||||
),
|
||||
(
|
||||
&mut offering.capabilities.reasoning,
|
||||
lower.capabilities.reasoning,
|
||||
),
|
||||
(
|
||||
&mut offering.capabilities.native_tool_calls,
|
||||
lower.capabilities.native_tool_calls,
|
||||
),
|
||||
(
|
||||
&mut offering.capabilities.structured_output,
|
||||
lower.capabilities.structured_output,
|
||||
),
|
||||
] {
|
||||
if *target == codewhale_config::route::CapabilityState::Unknown {
|
||||
*target = source;
|
||||
}
|
||||
}
|
||||
offering
|
||||
}
|
||||
|
||||
fn row_matches_endpoint_fingerprint(row: &CatalogOffering, fingerprint: &str) -> bool {
|
||||
matches!(
|
||||
&row.source,
|
||||
@@ -903,7 +948,12 @@ pub(crate) fn runtime_catalog_resolver_for_identity(
|
||||
// Curated transport facts win ordinary Models.dev collisions, exactly as
|
||||
// in RouteResolver::new(). A fresh exact roster replaces its whole scope.
|
||||
for offering in bundled_offerings() {
|
||||
route_offerings.insert(offering_key(&offering), offering);
|
||||
let key = offering_key(&offering);
|
||||
let enriched = match route_offerings.get(&key) {
|
||||
Some(lower) => offering_with_absent_facts_filled(offering, lower),
|
||||
None => offering,
|
||||
};
|
||||
route_offerings.insert(key, enriched);
|
||||
}
|
||||
// Keep curated transport identity, applying only fields explicitly signed
|
||||
// at the lower cloud layer. Hidden rows must not be resurrected here.
|
||||
|
||||
Reference in new issue
Block a user