Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -2736,6 +2736,8 @@ engine:
# agent only. This is not a request policy; put patterns such as gpt-* in
# models.allowed or models.blocked. When omitted, the router chooses from the
# models that policy allows (Claude and Codex only from their own model family).
# Lists from imported workflows using the same engine are combined, but if the
# main workflow or any import omits allowed-models, the router has no shortlist.
# GitHub Actions expressions are allowed only when neither models.allowed nor
# models.blocked is set.
# (optional)
Expand Down Expand Up @@ -21336,6 +21338,8 @@ safe-outputs:
# agent only. This is not a request policy; put patterns such as gpt-* in
# models.allowed or models.blocked. When omitted, the router chooses from the
# models that policy allows (Claude and Codex only from their own model family).
# Lists from imported workflows using the same engine are combined, but if the
# main workflow or any import omits allowed-models, the router has no shortlist.
# GitHub Actions expressions are allowed only when neither models.allowed nor
# models.blocked is set.
# (optional)
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/model-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ imports:
Summarize the open pull requests.
```

The importing workflow must either omit `engine` or use its object form, such as `engine: { id: copilot }`. If it also enables routing for the same engine, its `goal` and `mode` apply, and the imported `allowed-models` entries are added to its own. If it enables routing without `allowed-models`, the router chooses from only the imported entries.
The importing workflow must either omit `engine` or use its object form, such as `engine: { id: copilot }`. If it also enables routing for the same engine, its `goal` and `mode` apply. If the importing workflow or any import omits `allowed-models`, the router has no shortlist. Otherwise, the `allowed-models` lists are combined.

### Keeping up with new models

Expand Down
2 changes: 1 addition & 1 deletion pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -15225,7 +15225,7 @@
}
]
},
"description": "Optional exact Copilot model names that narrow the router's choice for the main agent only. This is not a request policy; put patterns such as gpt-* in models.allowed or models.blocked. When omitted, the router chooses from the models that policy allows (Claude and Codex only from their own model family). GitHub Actions expressions are allowed only when neither models.allowed nor models.blocked is set."
"description": "Optional exact Copilot model names that narrow the router's choice for the main agent only. This is not a request policy; put patterns such as gpt-* in models.allowed or models.blocked. When omitted, the router chooses from the models that policy allows (Claude and Codex only from their own model family). Lists from imported workflows using the same engine are combined, but if the main workflow or any import omits allowed-models, the router has no shortlist. GitHub Actions expressions are allowed only when neither models.allowed nor models.blocked is set."
Comment thread
SivaKesava1 marked this conversation as resolved.
Outdated
}
}
},
Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/compiler_orchestrator_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,11 @@ func (c *Compiler) mergeImportedModelRoutingConfigs(engineConfig *EngineConfig,
engineConfig.ModelRouting = &routing
continue
}
// An omitted allowed-models list means "any model", so it wins over any list.
if engineConfig.ModelRouting.AllowedModels == nil || importedConfig.ModelRouting.AllowedModels == nil {
engineConfig.ModelRouting.AllowedModels = nil
continue
}
seen := make(map[string]struct{}, len(engineConfig.ModelRouting.AllowedModels))
for _, model := range engineConfig.ModelRouting.AllowedModels {
seen[model] = struct{}{}
Expand Down
95 changes: 95 additions & 0 deletions pkg/workflow/model_routing_subagents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,98 @@ sandbox:
"github-copilot/claude-haiku-4.5",
}, config["apiProxy"].(map[string]any)["routing"].(map[string]any)["candidateModels"])
}

func TestImportedWorkflowModelRoutingOmittedAllowedModelsMeansAnyModel(t *testing.T) {
routingImport := func(allowedModels string) string {
content := "---\nengine:\n id: copilot\n model-routing:\n goal: cost-speed\n mode: robust\n"
if allowedModels != "" {
content += " allowed-models: " + allowedModels + "\n"
}
return content + "---\n# Shared routing\n"
}
tests := []struct {
name string
mainRouting string
imports map[string]string
importOrder []string
expectedModel []string
}{
{
name: "main omits allowed-models, import lists models",
mainRouting: " model-routing:\n goal: cost\n mode: economy\n",
imports: map[string]string{"routing.md": routingImport("[claude-sonnet-5]")},
importOrder: []string{"routing.md"},
expectedModel: nil,
},
{
name: "main lists models, import omits allowed-models",
mainRouting: " model-routing:\n goal: cost\n mode: economy\n allowed-models: [gpt-6-sol]\n",
imports: map[string]string{"routing.md": routingImport("")},
importOrder: []string{"routing.md"},
expectedModel: nil,
},
{
name: "main without routing, omitted import before listed import",
imports: map[string]string{"omitted.md": routingImport(""), "listed.md": routingImport("[claude-sonnet-5]")},
importOrder: []string{"omitted.md", "listed.md"},
expectedModel: nil,
},
{
name: "main without routing, listed import before omitted import",
imports: map[string]string{"omitted.md": routingImport(""), "listed.md": routingImport("[claude-sonnet-5]")},
importOrder: []string{"listed.md", "omitted.md"},
expectedModel: nil,
},
{
name: "main without routing, import lists models",
imports: map[string]string{"routing.md": routingImport("[claude-sonnet-5]")},
importOrder: []string{"routing.md"},
expectedModel: []string{"claude-sonnet-5"},
},
{
name: "main and import list models",
mainRouting: " model-routing:\n goal: cost\n mode: economy\n allowed-models: [gpt-6-sol]\n",
imports: map[string]string{"routing.md": routingImport("[claude-sonnet-5, gpt-6-sol]")},
importOrder: []string{"routing.md"},
expectedModel: []string{"gpt-6-sol", "claude-sonnet-5"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir()
for name, content := range tt.imports {
require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0600))
}
var importsYAML strings.Builder
for _, name := range tt.importOrder {
importsYAML.WriteString(" - " + name + "\n")
}
source := filepath.Join(dir, "routed.md")
require.NoError(t, os.WriteFile(source, []byte("---\non: workflow_dispatch\npermissions:\n contents: read\n copilot-requests: write\nstrict: false\nimports:\n"+
importsYAML.String()+"engine:\n id: copilot\n"+tt.mainRouting+"---\n# Routed workflow\n"), 0600))

data, err := NewCompiler().ParseWorkflowFile(source)
require.NoError(t, err)
require.NotNil(t, data.EngineConfig.ModelRouting)
require.Equal(t, tt.expectedModel, data.EngineConfig.ModelRouting.AllowedModels)
setRoutingCandidateModelsProxyImage(data, "0.28.33")

configJSON, err := BuildAWFConfigJSON(AWFCommandConfig{
EngineName: "copilot", AllowedDomains: "github.com", WorkflowData: data,
})
require.NoError(t, err)
var config map[string]any
require.NoError(t, json.Unmarshal([]byte(configJSON), &config))
routing := config["apiProxy"].(map[string]any)["routing"].(map[string]any)
if tt.expectedModel == nil {
require.NotContains(t, routing, "candidateModels")
return
}
expected := make([]any, 0, len(tt.expectedModel))
for _, model := range tt.expectedModel {
expected = append(expected, "github-copilot/"+model)
}
require.Equal(t, expected, routing["candidateModels"])
})
}
}
Loading