fabro/lib/crates/fabro-workflow/src/run_materialization.rs
Bryan Helmkamp 34d83db801
feat(model): support open provider catalog data (#245)
## Summary

This PR moves Fabro’s provider/model catalog toward settings-driven
provider identity by replacing the closed provider schema at the
API/auth/model boundary with `ProviderId`, then loading built-in
provider and model metadata from embedded per-provider TOML files.

The immediate result is that built-ins now use the same settings-shaped
catalog data that custom providers will use later, while request-serving
paths still keep the existing bootstrap/default catalog behavior until
the resolved-catalog plumbing lands.

## Changes

- Replaces API-facing provider enum usage with string-backed
`ProviderId`, including OpenAPI/progenitor replacements and regenerated
TypeScript client models.
- Routes model, auth, billing, CLI, server, and workflow call sites
through provider IDs where they cross product identity boundaries.
- Builds `Catalog` from settings-shaped provider/model data with
validation for adapter keys, OpenAI-compatible `base_url`, duplicate
aliases, provider defaults, disabled entries, model controls, and
per-speed cost rows.
- Replaces `catalog.json` with embedded provider TOML files under
`lib/crates/fabro-model/src/catalog/providers/`.
- Adds an explicit `fabro_model::bootstrap_catalog` hatch for
setup/install paths and extends the dev policy test to keep bootstrap
access contained.
- Preserves public training and knowledge-cutoff labels in LLM model
settings while still accepting bare TOML dates.

## Verification

- `cargo nextest run -p fabro-model -p fabro-config -p fabro-api` — 416
passed
- `cargo nextest run -p fabro-dev --features dev
bootstrap_catalog_references_stay_in_allowlist` — 1 passed
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo build --workspace`
- `git diff --check`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
2026-05-12 15:42:49 -04:00

75 lines
2.1 KiB
Rust

use fabro_graphviz::graph::Graph;
use fabro_model::{Catalog, ProviderId};
use fabro_types::WorkflowSettings;
use fabro_types::settings::InterpString;
use fabro_types::settings::run::RunGoal;
pub fn materialize_run(
mut settings: WorkflowSettings,
graph: &Graph,
catalog: &Catalog,
configured_providers: &[ProviderId],
) -> WorkflowSettings {
let configured_model = settings
.run
.model
.name
.as_ref()
.map(InterpString::as_source);
let configured_provider = settings
.run
.model
.provider
.as_ref()
.map(InterpString::as_source);
let graph_provider = graph
.attrs
.get("default_provider")
.and_then(|value| value.as_str())
.map(str::to_string);
let graph_model = graph
.attrs
.get("default_model")
.and_then(|value| value.as_str())
.map(str::to_string);
let provider = configured_provider.or(graph_provider);
let model = configured_model.or(graph_model).unwrap_or_else(|| {
provider
.as_deref()
.map(ProviderId::from)
.and_then(|provider| catalog.default_for_provider(&provider))
.unwrap_or_else(|| catalog.default_for_configured_ids(configured_providers))
.id
.clone()
});
let (resolved_model, resolved_provider) = match catalog.get(&model) {
Some(info) => (
info.id.clone(),
provider.or(Some(info.provider.to_string())),
),
None => (model, provider),
};
settings.run.model.name = Some(InterpString::parse(&resolved_model));
settings.run.model.provider = resolved_provider.as_deref().map(InterpString::parse);
let goal = graph.goal().to_string();
settings.run.goal = if goal.is_empty() {
None
} else {
Some(RunGoal::Inline(InterpString::parse(&goal)))
};
if settings
.run
.pull_request
.as_ref()
.is_some_and(|pull_request| !pull_request.enabled)
{
settings.run.pull_request = None;
}
settings
}