diff --git a/docs/design/llama-stack-config-merge/llama-stack-config-merge.md b/docs/design/llama-stack-config-merge/llama-stack-config-merge.md index cf43a6107..320a7d776 100644 --- a/docs/design/llama-stack-config-merge/llama-stack-config-merge.md +++ b/docs/design/llama-stack-config-merge/llama-stack-config-merge.md @@ -82,7 +82,10 @@ detail that LCORE owns, not an operator-facing artifact. - **R5:** When `llama_stack.config.native_override` overlaps a key set by the high-level section or by the baseline, deep-merge semantics apply with list replacement (maps merge recursively; lists are - replaced wholesale; scalars are replaced). + replaced wholesale; scalars are replaced). The override wins over the + baseline and the high-level expansion; enrichment (R7) applies after + the merge, exactly as in legacy mode, where enrichment always + post-processes the operator's final run.yaml (LCORE-3370). - **R6:** Secrets that LCORE itself emits are never resolved on disk: `apply_high_level_inference` writes `${env.}` references verbatim, and LCORE does not eagerly resolve env refs in the @@ -545,6 +548,7 @@ reference. | Date | Change | Reason | |---|---|---| | 2026-04-23 | Initial version | Spike completion | +| 2026-08-04 | R5: enrichment applies after the `native_override` merge | LCORE-3370 — migrated configs (run.yaml lifted into the override) replaced list-shaped enrichment artifacts wholesale, silently dropping BYOK/Solr providers, registered embedding models, and Azure `model_validation`; ordering now matches legacy, where the operator's run.yaml never beats enrichment | ## Appendix A — Worked example: legacy → unified migration diff --git a/src/llama_stack_configuration.py b/src/llama_stack_configuration.py index 1a563bc33..13c6bc03a 100644 --- a/src/llama_stack_configuration.py +++ b/src/llama_stack_configuration.py @@ -1138,11 +1138,13 @@ def synthesize_configuration( """Synthesize a full Llama Stack ``run.yaml`` dict from a unified config. Implements the unified-mode synthesis pipeline: select a baseline (profile - file, empty, or the built-in default), apply the existing enrichment - (Azure Entra ID, BYOK RAG, Solr/OKP) for parity with legacy mode (R7), - expand the high-level ``inference.providers`` section, ensure the default - MCP tool_runtime provider when the baseline was not empty, and deep-merge - the raw ``native_override`` last (R5). + file, empty, or the built-in default), expand the high-level + ``inference.providers`` section, ensure the default MCP tool_runtime + provider when the baseline was not empty, deep-merge the raw + ``native_override`` (R5: it wins over the baseline and the high-level + expansion), and apply the existing enrichment (Azure Entra ID, BYOK RAG, + vector_store, Solr/OKP) last — matching legacy mode, where enrichment + always post-processes the operator's final run.yaml (R7, LCORE-3370). Parameters: lcs_config: The full ``lightspeed-stack.yaml`` parsed into a dict. @@ -1180,27 +1182,35 @@ def synthesize_configuration( # 3. Normalize duplicated vector_io providers in the baseline. dedupe_providers_vector_io(ls_config) - # 4. Existing enrichment — same calls as legacy generate_configuration so - # unified output matches legacy output for equivalent inputs (R7). - enrich_azure_entra_id_inference(ls_config, lcs_config.get("azure_entra_id")) - enrich_byok_rag(ls_config, lcs_config.get("byok_rag", [])) - enrich_vector_store(ls_config, lcs_config.get("vector_store")) - enrich_solr(ls_config, lcs_config.get("rag", {}), lcs_config.get("okp", {})) - - # 5. High-level inference providers (Decision S5 — a root-level section). + # 4. High-level inference providers (Decision S5 — a root-level section). inference = lcs_config.get("inference") or {} if inference.get("providers"): apply_high_level_inference(ls_config, inference) - # 6. Ensure MCP tool_runtime for default/profile baselines (skipped for + # 5. Ensure MCP tool_runtime for default/profile baselines (skipped for # baseline: empty so migrate round-trips stay lossless). if not baseline_was_empty: ensure_mcp_tool_runtime(ls_config) - # 7. Raw escape hatch, deep-merged last with list replacement (R5). + # 6. Raw escape hatch, deep-merged with list replacement. It wins over the + # baseline and the high-level expansion (R5) but deliberately NOT over + # enrichment (step 7). if unified and unified.get("native_override"): ls_config = deep_merge_list_replace(ls_config, unified["native_override"]) + # 7. Existing enrichment — same calls as legacy generate_configuration so + # unified output matches legacy output for equivalent inputs (R7). + # Applied AFTER the native_override merge (LCORE-3370): in legacy mode + # enrichment always post-processes the operator's final run.yaml, so a + # migrated config (whose native_override IS the lifted run.yaml) must + # get the same treatment or list-shaped enrichment artifacts + # (vector_io providers, registered models, azure model_validation) are + # replaced wholesale by the lifted lists and silently lost. + enrich_azure_entra_id_inference(ls_config, lcs_config.get("azure_entra_id")) + enrich_byok_rag(ls_config, lcs_config.get("byok_rag", [])) + enrich_vector_store(ls_config, lcs_config.get("vector_store")) + enrich_solr(ls_config, lcs_config.get("rag", {}), lcs_config.get("okp", {})) + # 8. Dedupe again in case native_override or enrichment reintroduced dupes. dedupe_providers_vector_io(ls_config) diff --git a/tests/integration/test_unified_synthesis.py b/tests/integration/test_unified_synthesis.py index b0b9a7db0..4833c276d 100644 --- a/tests/integration/test_unified_synthesis.py +++ b/tests/integration/test_unified_synthesis.py @@ -130,7 +130,7 @@ "azure_entra_id": { "tenant_id": "test-tenant", "client_id": "test-client", - "client_secret_path": "/run/secrets/azure", + "client_secret": "test-secret", } } @@ -403,15 +403,6 @@ def test_migrate_then_synthesize_round_trip_without_enrichment( assert legacy == synthesized -@pytest.mark.xfail( - strict=True, - reason="Known defect (LCORE-3370): dumb migration lifts run.yaml " - "into native_override, which deep-merges after enrichment and replaces " - "lists wholesale (R5) — so BYOK/Solr vector_io providers, registered " - "embedding models, and the Azure model_validation enrichment are lost " - "whenever the original run.yaml already carried those list sections. " - "Contradicts migrate_config_dumb's enrichment-keeps-working promise.", -) def test_migrate_then_synthesize_preserves_enrichment_parity( tmp_path: Path, ) -> None: