diff --git a/config/README.md b/config/README.md index 003cafb840..2aef392163 100644 --- a/config/README.md +++ b/config/README.md @@ -18,6 +18,7 @@ Inside canonical `config.yaml`: - `routing.projections.partitions` is the canonical runtime home for exclusive domain or embedding partitions; DSL authoring uses `PROJECTION partition` - `routing.projections.scores` and `routing.projections.mappings` let maintained configs turn learned and heuristic signals into named routing bands that decisions can reference with `type: projection` - `routing.decisions[].candidateIterations` carries bounded DSL `FOR ... IN` metadata for candidate-model authoring; it is declarative selection policy input, not a general scripting runtime +- `routing.decisions[].modelRefs[].quality_score` optionally overrides the model-card quality score for that candidate in the owning decision; `multi_factor` falls back to the model-card value only when this field is omitted - `routing.decisions[].emits[]` carries typed side-effect directives from DSL `EMIT` blocks; the current supported kind is `retention`, where `drop: true` skips response-side semantic-cache writes and the remaining fields stay structured/auditable for follow-up runtime consumers such as turn-aware cache TTL, current-model affinity, prefix/KV-cache warmth, and session transition telemetry - request-shape detectors such as `routing.signals.structure` stay in the signal layer as typed named facts; numeric thresholds live inside the detector config instead of turning decisions into a free-form expression language - `routing.signals.embeddings[].query_modality` declares which modality of incoming request payload the embedding rule's query is computed from. Defaults to `"text"`; `"image"` and `"audio"` require `global.model_catalog.embeddings.semantic.embedding_config.model_type=multimodal` so the query and candidate embeddings land in the same shared space. See `website/docs/tutorials/signal/learned/embedding.md` for the worked multimodal example. @@ -46,7 +47,7 @@ Inside canonical `config.yaml`: - `not/`: exclusion examples - `composite/`: nested AND/OR/NOT cases -Decision fragments may reference `modelRefs[].lora_name`, but those adapter names must be declared in the base config's `routing.modelCards[].loras`. +Decision fragments may reference `modelRefs[].lora_name`, but those adapter names must be declared in the base config's `routing.modelCards[].loras`. A `modelRefs[].quality_score` value is decision-scoped, must be between `0` and `1`, and preserves an explicit `0`. Candidate iteration fragments must stay bounded to `decision.candidates` or an explicit model list and feed existing decision outputs such as `MODEL `. `config/algorithm/` is organized by routing policy: diff --git a/config/config.yaml b/config/config.yaml index 46e64c5376..a04f1852a9 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -571,6 +571,7 @@ routing: - model: qwen3-8b lora_name: business-adapter weight: 1.0 + quality_score: 0.83 use_reasoning: false reasoning_description: Candidate iteration reference model for DSL authoring coverage. reasoning_effort: low @@ -1135,7 +1136,9 @@ routing: name: other modelRefs: - model: qwen3-8b + quality_score: 0.72 - model: qwen3-32b + quality_score: 0.94 algorithm: type: multi_factor multi_factor: diff --git a/dashboard/frontend/src/pages/ConfigPageDecisionModelQuality.test.tsx b/dashboard/frontend/src/pages/ConfigPageDecisionModelQuality.test.tsx new file mode 100644 index 0000000000..3cd5815ebb --- /dev/null +++ b/dashboard/frontend/src/pages/ConfigPageDecisionModelQuality.test.tsx @@ -0,0 +1,15 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' + +describe('decision model quality editor', () => { + it('preserves and edits decision-scoped quality scores', () => { + const source = readFileSync( + new URL('./ConfigPageDecisionsSection.tsx', import.meta.url), + 'utf8', + ) + + expect(source).toContain('Decision quality score') + expect(source).toContain("| 'quality_score'") + expect(source).toContain('modelRef.quality_score = modelRefValue.quality_score') + }) +}) diff --git a/dashboard/frontend/src/pages/ConfigPageDecisionsSection.tsx b/dashboard/frontend/src/pages/ConfigPageDecisionsSection.tsx index b0afae745e..b967e61377 100644 --- a/dashboard/frontend/src/pages/ConfigPageDecisionsSection.tsx +++ b/dashboard/frontend/src/pages/ConfigPageDecisionsSection.tsx @@ -114,6 +114,7 @@ export default function ConfigPageDecisionsSection({ ref.reasoning_effort ? `Effort: ${ref.reasoning_effort}` : null, ref.lora_name ? `LoRA: ${ref.lora_name}` : null, typeof ref.weight === 'number' ? `Weight: ${ref.weight}` : null, + typeof ref.quality_score === 'number' ? `Quality: ${ref.quality_score}` : null, ].filter((value): value is string => Boolean(value)) const details = [ @@ -355,6 +356,7 @@ export default function ConfigPageDecisionsSection({ reasoning_effort: ref.reasoning_effort || '', lora_name: ref.lora_name || '', weight: typeof ref.weight === 'number' ? ref.weight : undefined, + quality_score: typeof ref.quality_score === 'number' ? ref.quality_score : undefined, })), plugins: (decision.plugins || []).map((plugin) => ({ type: plugin.type, @@ -471,7 +473,8 @@ export default function ConfigPageDecisionsSection({ | 'reasoning_description' | 'reasoning_effort' | 'lora_name' - | 'weight', + | 'weight' + | 'quality_score', val: string | boolean | number | undefined, ) => { const next = rows.map((item, idx) => (idx === index ? { ...item, [key]: val } : item)) @@ -592,6 +595,27 @@ export default function ConfigPageDecisionsSection({ className={decisionStyles.editorInput} /> +