Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e28bf0d
feat(proto): add get started recommendation contract
appaquet Nov 11, 2026
7b433f8
feat(backend): rename recommendation use cases
appaquet Nov 11, 2026
67751c9
feat(catalog): rename recommendation use cases
appaquet Nov 11, 2026
4b3685c
feat(backend): size recommendations with context per request
appaquet Nov 11, 2026
5c1e28b
feat(catalog): set context per request on recommendations
appaquet Nov 11, 2026
da8142a
feat(backend): add can start now to model recommendations
appaquet Nov 11, 2026
0ca2294
feat(catalog): apply reviewed recommendation lists
appaquet Nov 11, 2026
f7a1a9d
feat(backend): add app targets read
appaquet Nov 12, 2026
ead22aa
feat(catalog): declare app use cases and preferred accelerators
appaquet Nov 12, 2026
c5b8abd
feat(backend): show context and apps in hardware recommend
appaquet Nov 12, 2026
205d1dc
docs(*): describe get started recommendations
appaquet Nov 12, 2026
ecded3d
feat(proto): add get started dismissed preference
appaquet Nov 12, 2026
bac9dea
feat(frontend): add get started page
appaquet Nov 12, 2026
a0ab4de
fix(frontend): apply get started review feedback
appaquet Nov 12, 2026
e42ac63
feat(frontend): open served use cases on their ready step
appaquet Nov 12, 2026
3f70a98
feat(proto): add site-wide console settings
appaquet Nov 12, 2026
4e24557
feat(backend): add site-wide console settings
appaquet Nov 12, 2026
7892ff4
docs(*): describe site-wide console settings
appaquet Nov 12, 2026
4ee000e
feat(proto): drop browser get started preference
appaquet Nov 12, 2026
9ab3c83
feat(frontend): hide get started site-wide
appaquet Nov 12, 2026
ff7e9e0
fix(frontend): open get started from the profile menu without un-hidi…
appaquet Nov 13, 2026
771014b
docs(*): say the profile menu opens get started without un-hiding it
appaquet Nov 13, 2026
928bf0d
feat(frontend): keep applied values when reopening customize
appaquet Nov 13, 2026
b11f9fa
fix(backend): wait for released site stores between route exposure co…
appaquet Nov 13, 2026
2fcf4ce
fix(backend): allow a console update in the pause's watch tick
appaquet Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/add-app/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Each image has a folder under `apps/` (or `engines/` for an engine image) with a
4. Write `catalog/applications/<name>.yaml` from the closest neighbour.
* Reference the package `apps.<name>.docker` with `dockerImage: {}`, and list the platforms the image was pinned for.
* Bind the inference endpoint and published URLs with resolved values rather than literals, and keep any fixed login visible in `notes`.
* Declare `useCases` when the app is the master catalog's app for a use case (one app per use case), and `preferredAccelerators` (vendor names) when it runs best on one vendor's GPUs. Both are advisory metadata.
* Leave out optional fields you cannot support.

5. Write the folder's `README.md` and `AGENTS.md` from the templates in `apps/AGENTS.md`.
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/add-model/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Sizing is owned by the `model-sizing` skill. Catalog loading and validation live
* Size each configuration with the `model-sizing` skill.
* Leave out optional fields you cannot support.

4. Decide whether the model belongs in any hardware class's lists in `catalog/recommendations/`, for each use case, and place it by priority. Lists stay short and best first; add a memory minimum only where the model fits but leaves too little room for context.
4. Decide whether the model belongs in any hardware class's lists in `catalog/recommendations/`, for each use case, and place it by priority. An entry may set `contextTokensPerRequest`: coding entries carry the largest context per request that starts on an idle machine at the entry's minimum; the catalog test checks every entry fits. Lists stay short and best first; add a memory minimum only where the model fits but leaves too little room for context.

5. Validate the declaration and any list you changed with the catalog tests and the area gates.

Expand Down
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,12 @@ in the same change that adds, removes or renames a folder it lists.
An **engine artifact** is one runnable build of it.
- **hardware class**: a kind of execution hardware, nothing model-specific. **Hardware support** is
a configuration's memory minimums and sizing constants for one class.
- **use case**: what an operator runs models for (coding and agents, general assistant,
translation, decision making); a fixed list, distinct from inference request kinds.
- **use case**: what an operator runs Dotcore for (chat assistant, coding, image generation); a
fixed list, distinct from inference request kinds. Each use case has at most one app; models
serve the use cases that have model lists.
- **model recommendation**: a master-catalog entry naming a model profile to run on a hardware
class for a use case, in priority order, with optional memory minimums; advisory, never used for
admission or placement.
class for a use case, in priority order, with optional memory minimums and an optional context
per request (the profile still holds none); advisory, never used for admission or placement.
- **maximum context**: the largest context one request can use with the model's weights; a
configuration may lower it. No "effective" variant.
- **total context** (`total-context`): the KV pool an engine holds, shared by active requests.
Expand Down
162 changes: 141 additions & 21 deletions backend/catalog/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2129,15 +2129,15 @@ artifacts:

const NVIDIA_RECOMMENDATIONS: &str = r#"hardwareClassId: "nvidia"
useCases:
- useCase: "USE_CASE_CODING_AND_AGENTS"
- useCase: "USE_CASE_CODING"
entries:
- modelId: "fixture-model"
profileId: "balanced"
hardwareRequirements:
minimumMemoryBytesPerDevice: "32000000000"
- modelId: "fixture-model"
profileId: "fast"
- useCase: "USE_CASE_TRANSLATION"
- useCase: "USE_CASE_CHAT_ASSISTANT"
entries:
- modelId: "fixture-model"
profileId: "fast"
Expand Down Expand Up @@ -2187,10 +2187,10 @@ useCases:
[
(
"nvidia",
UseCase::CodingAndAgents,
UseCase::Coding,
vec![("balanced", Some(32_000_000_000)), ("fast", None)]
),
("nvidia", UseCase::Translation, vec![("fast", None)]),
("nvidia", UseCase::ChatAssistant, vec![("fast", None)]),
]
);
fs::remove_dir_all(root).expect("Catalog fixture cleanup");
Expand Down Expand Up @@ -2247,6 +2247,108 @@ useCases:
fs::remove_dir_all(root).expect("Catalog fixture cleanup");
}

#[test]
fn master_catalog_applications_serve_distinct_use_cases() {
let root = temporary_catalog_root();
create_empty_catalog_source(&root);
let mut chat = super::test_support::application();
chat.use_cases = vec![UseCase::ChatAssistant as i32];
let mut images = super::test_support::application();
images.application_id = "fixture-images".to_owned();
images.use_cases = vec![UseCase::ImageGeneration as i32];
images.preferred_accelerators = vec!["nvidia".to_owned()];
write_application(&root, "chat.yaml", &chat);
write_application(&root, "images.yaml", &images);

let catalog = Catalog::load_with_policy(
&root,
OciArtifactPolicy::Development,
&ACCEPT_ENGINE_DECLARATIONS,
)
.expect("applications serving distinct use cases load");
assert_eq!(
catalog
.list_application_releases()
.iter()
.map(|application| (
application.application_id.as_str(),
application.use_cases().collect::<Vec<_>>(),
application.preferred_accelerators.clone(),
))
.collect::<Vec<_>>(),
[
(
"fixture-application",
vec![UseCase::ChatAssistant],
Vec::new()
),
(
"fixture-images",
vec![UseCase::ImageGeneration],
vec!["nvidia".to_owned()]
),
]
);

let mut second_chat = images;
second_chat.use_cases = vec![
UseCase::ImageGeneration as i32,
UseCase::ChatAssistant as i32,
];
write_application(&root, "images.yaml", &second_chat);
let error = Catalog::load_with_policy(
&root,
OciArtifactPolicy::Development,
&ACCEPT_ENGINE_DECLARATIONS,
)
.expect_err("a use case served by two applications is rejected");
assert_eq!(
error.to_string(),
"Catalog diagnostics: applications/images.yaml: application \"fixture-images\" serves use case USE_CASE_CHAT_ASSISTANT, already served by application \"fixture-application\" in applications/chat.yaml"
);
fs::remove_dir_all(root).expect("Catalog fixture cleanup");
}

#[test]
fn catalog_layers_cannot_declare_application_use_cases() {
let root = temporary_catalog_root();
let master = root.join("master");
let layer = root.join("layer");
create_empty_catalog_source(&master);
create_empty_catalog_source(&layer);
let mut application = super::test_support::application();
application.use_cases = vec![UseCase::Coding as i32];
write_application(&layer, "coding.yaml", &application);

let development =
Catalog::load_development_sources(&master, &layer, &ACCEPT_ENGINE_DECLARATIONS)
.expect_err("a development layer must not declare application use cases");
let site = Catalog::load_sources_with_policy(
&master,
&layer,
OciArtifactPolicy::Development,
&ACCEPT_ENGINE_DECLARATIONS,
)
.expect_err("a site layer must not declare application use cases");

assert_eq!(
development.to_string(),
"Catalog diagnostics: development catalog applications/coding.yaml: application use cases can only be declared by the master catalog"
);
assert_eq!(
site.to_string(),
"Catalog diagnostics: site catalog applications/coding.yaml: application use cases can only be declared by the master catalog"
);

// A layer application that serves no use case still loads.
application.use_cases.clear();
application.preferred_accelerators = vec!["nvidia".to_owned()];
write_application(&layer, "coding.yaml", &application);
Catalog::load_development_sources(&master, &layer, &ACCEPT_ENGINE_DECLARATIONS)
.expect("a layer application without use cases loads");
fs::remove_dir_all(root).expect("Catalog fixture cleanup");
}

#[test]
fn recommendations_on_wider_and_narrower_classes_than_the_supported_one_are_valid() {
let root = recommendation_fixture(" - hardwareClassId: \"nvidia\"\n");
Expand All @@ -2265,7 +2367,7 @@ useCases:
&root,
class,
&format!(
"hardwareClassId: \"{class}\"\nuseCases:\n - useCase: \"USE_CASE_GENERAL_ASSISTANT\"\n entries:\n - modelId: \"fixture-model\"\n profileId: \"balanced\"\n"
"hardwareClassId: \"{class}\"\nuseCases:\n - useCase: \"USE_CASE_CHAT_ASSISTANT\"\n entries:\n - modelId: \"fixture-model\"\n profileId: \"balanced\"\n"
),
);
}
Expand Down Expand Up @@ -2310,9 +2412,9 @@ useCases:
lists.concat()
)
};
let assistant = |entries: &[(&str, &str)]| list("USE_CASE_GENERAL_ASSISTANT", entries);
let assistant = |entries: &[(&str, &str)]| list("USE_CASE_CHAT_ASSISTANT", entries);
let balanced = [("fixture-model", "balanced")];
let entry = "use case USE_CASE_GENERAL_ASSISTANT entry 1 (model \"fixture-model\" profile \"balanced\")";
let entry = "use case USE_CASE_CHAT_ASSISTANT entry 1 (model \"fixture-model\" profile \"balanced\")";
let nvidia_support = " - hardwareClassId: \"nvidia\"\n";
let minimum_support = " - hardwareClassId: \"nvidia\"\n hardwareRequirements:\n minimumMemoryBytesPerDevice: \"16000000000\"\n";

Expand All @@ -2333,32 +2435,26 @@ useCases:
"repeated use case",
nvidia_support,
vec![("nvidia", file("nvidia", &[assistant(&balanced), assistant(&[("fixture-model", "fast")])]))],
"recommendations/nvidia.yaml: model recommendations for hardware class \"nvidia\" list use case USE_CASE_GENERAL_ASSISTANT more than once".to_owned(),
"recommendations/nvidia.yaml: model recommendations for hardware class \"nvidia\" list use case USE_CASE_CHAT_ASSISTANT more than once".to_owned(),
),
(
"unknown model",
nvidia_support,
vec![("nvidia", file("nvidia", &[assistant(&[("missing", "balanced")])]))],
"recommendations/nvidia.yaml: model recommendations for hardware class \"nvidia\" use case USE_CASE_GENERAL_ASSISTANT entry 1 (model \"missing\" profile \"balanced\") references an unknown model".to_owned(),
"recommendations/nvidia.yaml: model recommendations for hardware class \"nvidia\" use case USE_CASE_CHAT_ASSISTANT entry 1 (model \"missing\" profile \"balanced\") references an unknown model".to_owned(),
),
(
"unknown profile",
nvidia_support,
vec![("nvidia", file("nvidia", &[assistant(&[("fixture-model", "missing")])]))],
"recommendations/nvidia.yaml: model recommendations for hardware class \"nvidia\" use case USE_CASE_GENERAL_ASSISTANT entry 1 (model \"fixture-model\" profile \"missing\") references an unknown profile".to_owned(),
"recommendations/nvidia.yaml: model recommendations for hardware class \"nvidia\" use case USE_CASE_CHAT_ASSISTANT entry 1 (model \"fixture-model\" profile \"missing\") references an unknown profile".to_owned(),
),
(
"unrelated class",
nvidia_support,
vec![("apple", file("apple", &[assistant(&balanced)]))],
format!("recommendations/apple.yaml: model recommendations for hardware class \"apple\" {entry} runs a configuration that supports neither this hardware class, a class containing it, nor a class nested in it"),
),
(
"decision making without decision requests",
nvidia_support,
vec![("nvidia", file("nvidia", &[list("USE_CASE_DECISION_MAKING", &balanced)]))],
"recommendations/nvidia.yaml: model recommendations for hardware class \"nvidia\" use case USE_CASE_DECISION_MAKING entry 1 (model \"fixture-model\" profile \"balanced\") runs a configuration that doesn't accept INFERENCE_REQUEST_KIND_DECISION requests".to_owned(),
),
(
"entry minimum below the configuration's",
minimum_support,
Expand All @@ -2371,11 +2467,35 @@ useCases:
)],
format!("recommendations/nvidia.yaml: model recommendations for hardware class \"nvidia\" {entry} sets minimum memory per device 8000000000 bytes, below its configuration's 16000000000 bytes"),
),
(
"context per request above the maximum context",
nvidia_support,
vec![(
"nvidia",
file("nvidia", &[assistant(&balanced)]).replace(
"profileId: \"balanced\"\n",
"profileId: \"balanced\"\n contextTokensPerRequest: 262145\n",
),
)],
format!("recommendations/nvidia.yaml: model recommendations for hardware class \"nvidia\" {entry} sets context per request 262145 tokens, above its configuration's maximum context of 262144 tokens"),
),
(
"context per request below the minimum context",
nvidia_support,
vec![(
"nvidia",
file("nvidia", &[assistant(&balanced)]).replace(
"profileId: \"balanced\"\n",
"profileId: \"balanced\"\n contextTokensPerRequest: 1023\n",
),
)],
format!("recommendations/nvidia.yaml: model recommendations for hardware class \"nvidia\" {entry} sets context per request 1023 tokens, below the minimum context of 1024 tokens"),
),
(
"repeated entry",
nvidia_support,
vec![("nvidia", file("nvidia", &[assistant(&[balanced[0], balanced[0]])]))],
"recommendations/nvidia.yaml: model recommendations for hardware class \"nvidia\" use case USE_CASE_GENERAL_ASSISTANT entry 2 (model \"fixture-model\" profile \"balanced\") repeats an earlier entry".to_owned(),
"recommendations/nvidia.yaml: model recommendations for hardware class \"nvidia\" use case USE_CASE_CHAT_ASSISTANT entry 2 (model \"fixture-model\" profile \"balanced\") repeats an earlier entry".to_owned(),
),
(
"class declared twice",
Expand Down Expand Up @@ -2420,7 +2540,7 @@ useCases:
}

#[test]
fn chat_use_cases_require_a_configuration_accepting_chat_requests() {
fn coding_entries_require_a_configuration_accepting_chat_requests() {
let root = recommendation_fixture(" - hardwareClassId: \"nvidia\"\n");
let path = root.join("models/fixture.yaml");
let model = fs::read_to_string(&path).expect("fixture model");
Expand All @@ -2435,20 +2555,20 @@ useCases:
super::test_support::write_recommendations(
&root,
"nvidia",
"hardwareClassId: \"nvidia\"\nuseCases:\n - useCase: \"USE_CASE_DECISION_MAKING\"\n entries:\n - modelId: \"fixture-model\"\n profileId: \"balanced\"\n - useCase: \"USE_CASE_TRANSLATION\"\n entries:\n - modelId: \"fixture-model\"\n profileId: \"balanced\"\n",
"hardwareClassId: \"nvidia\"\nuseCases:\n - useCase: \"USE_CASE_CODING\"\n entries:\n - modelId: \"fixture-model\"\n profileId: \"balanced\"\n",
);

let error = Catalog::load_with_policy(
&root,
OciArtifactPolicy::Development,
&ACCEPT_ENGINE_DECLARATIONS,
)
.expect_err("a chat use case on a decision-only configuration must reject")
.expect_err("a coding entry on a decision-only configuration must reject")
.to_string();

assert_eq!(
error,
"Catalog diagnostics: recommendations/nvidia.yaml: model recommendations for hardware class \"nvidia\" use case USE_CASE_TRANSLATION entry 1 (model \"fixture-model\" profile \"balanced\") runs a configuration that doesn't accept INFERENCE_REQUEST_KIND_CHAT requests"
"Catalog diagnostics: recommendations/nvidia.yaml: model recommendations for hardware class \"nvidia\" use case USE_CASE_CODING entry 1 (model \"fixture-model\" profile \"balanced\") runs a configuration that doesn't accept INFERENCE_REQUEST_KIND_CHAT requests"
);
fs::remove_dir_all(root).expect("Catalog fixture cleanup");
}
Expand Down
2 changes: 2 additions & 0 deletions backend/catalog/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ mod tests {
description: String::new(),
notes: String::new(),
version: "1".to_owned(),
use_cases: Vec::new(),
preferred_accelerators: Vec::new(),
graph: Some(ApplicationGraph {
services: vec![ApplicationService {
service_name: "web".to_owned(),
Expand Down
27 changes: 24 additions & 3 deletions backend/catalog/src/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
//!
//! It walks the model, engine, hardware class, recommendation, application, and upstream-template
//! directories and parses every file into a typed record, extracting each configuration's private
//! sizing layers along the way. Only the master catalog may declare model recommendations.
//! sizing layers along the way. Only the master catalog may declare model recommendations and the
//! use cases applications serve.
//! A file that cannot be read or parsed is reported as a located diagnostic rather than
//! aborting the whole load, so one bad file never hides the rest.

Expand Down Expand Up @@ -57,7 +58,13 @@ pub(super) fn load_catalog_source(
&mut records.recommendations,
diagnostics,
);
load_applications(catalog_root, source, &mut records.applications, diagnostics);
load_applications(
catalog_root,
source,
role,
&mut records.applications,
diagnostics,
);
load_upstream_model_templates(
catalog_root,
source,
Expand Down Expand Up @@ -294,14 +301,28 @@ fn load_recommendations(
}
}

/// Loads the applications of one catalog source. Only the master catalog may say which use cases
/// an application serves, since a layer's application would compete with the master's for its use
/// case, so a layer application that names one is reported instead.
fn load_applications(
catalog_root: &Path,
source: &str,
role: CatalogSourceRole,
applications: &mut Vec<(String, Application)>,
diagnostics: &mut Vec<CatalogDiagnostic>,
) {
for file in optional_catalog_files(catalog_root, source, APPLICATION_DIRECTORY, diagnostics) {
match yaml::from_file(&file.path) {
match yaml::from_file::<Application>(&file.path) {
Ok(application)
if role == CatalogSourceRole::Layer && !application.use_cases.is_empty() =>
{
diagnostics.push(CatalogDiagnostic::new(
file.location,
anyhow::anyhow!(
"application use cases can only be declared by the master catalog"
),
));
}
Ok(application) => applications.push((file.location, application)),
Err(error) => diagnostics.push(CatalogDiagnostic::new(
file.location,
Expand Down
Loading
Loading