Changeset 0.29.1 (#196)

This commit is contained in:
2026-03-17 19:32:20 +00:00
parent 5d637d3a90
commit d4de84f3f1
24 changed files with 3117 additions and 28 deletions

View File

@@ -67,6 +67,8 @@ func (e *Evaluator) Evaluate(ctx *Context, policies []Policy, candidates []Candi
result, matched = applyCostLimit(result, p.Config, ctx)
case PolicyModelAlias:
result, matched = applyModelAlias(result, p.Config, ctx)
case PolicyCapabilityMatch:
result, matched = applyCapabilityMatch(result, p.Config, ctx)
}
if matched {
@@ -218,6 +220,102 @@ func applyModelAlias(candidates []Candidate, cfg PolicyConfig, ctx *Context) ([]
return candidates, true
}
// applyCapabilityMatch filters candidates to those whose model has all
// required capabilities. Optionally sorts remaining candidates by cheapest
// output price when PreferCheapest is set.
//
// Required capabilities map to ModelCapabilities boolean fields:
// "tool_calling", "vision", "thinking", "reasoning",
// "code_optimized", "web_search", "streaming"
func applyCapabilityMatch(candidates []Candidate, cfg PolicyConfig, ctx *Context) ([]Candidate, bool) {
if len(cfg.RequiredCaps) == 0 {
return candidates, false
}
if ctx.Capabilities == nil {
return candidates, false
}
filtered := make([]Candidate, 0, len(candidates))
for _, c := range candidates {
key := c.ConfigID + ":" + c.Model
caps, ok := ctx.Capabilities[key]
if !ok {
continue // no capability data — skip (conservative)
}
if hasAllCaps(caps, cfg.RequiredCaps) {
c.Reason = "capability match"
filtered = append(filtered, c)
}
}
if len(filtered) == 0 {
// No candidates match — fall back to original set rather than
// returning empty (better to try than fail immediately).
return candidates, false
}
// Sort by cheapest output price if requested
if cfg.PreferCheapest && ctx.Pricing != nil {
sort.SliceStable(filtered, func(i, j int) bool {
ki := filtered[i].ConfigID + ":" + filtered[i].Model
kj := filtered[j].ConfigID + ":" + filtered[j].Model
pi, oki := ctx.Pricing[ki]
pj, okj := ctx.Pricing[kj]
if !oki || pi == nil {
return false // unknown price sorts last
}
if !okj || pj == nil {
return true
}
return pi.OutputPerM < pj.OutputPerM
})
}
return filtered, true
}
// hasAllCaps checks whether a ModelCapabilities struct has all requested
// capability flags set. Unrecognized capability names are ignored.
func hasAllCaps(caps *models.ModelCapabilities, required []string) bool {
if caps == nil {
return false
}
for _, req := range required {
switch req {
case "tool_calling":
if !caps.ToolCalling {
return false
}
case "vision":
if !caps.Vision {
return false
}
case "thinking":
if !caps.Thinking {
return false
}
case "reasoning":
if !caps.Reasoning {
return false
}
case "code_optimized":
if !caps.CodeOptimized {
return false
}
case "web_search":
if !caps.WebSearch {
return false
}
case "streaming":
if !caps.Streaming {
return false
}
// Unrecognized caps are ignored (forward-compatible)
}
}
return true
}
// ── Health Sorting ──────────────────────────
var statusOrder = map[models.ProviderStatus]int{