Merge pull request #824 from 0xYiliu/fix/issue-783-fallback-alias-resolution

fix: resolve fallback model alias parsing for issue #783
This commit is contained in:
daming大铭
2026-02-28 00:25:18 +08:00
committed by GitHub
5 changed files with 261 additions and 2 deletions
+41 -1
View File
@@ -92,7 +92,47 @@ func NewAgentInstance(
Primary: model,
Fallbacks: fallbacks,
}
candidates := providers.ResolveCandidates(modelCfg, defaults.Provider)
resolveFromModelList := func(raw string) (string, bool) {
ensureProtocol := func(model string) string {
model = strings.TrimSpace(model)
if model == "" {
return ""
}
if strings.Contains(model, "/") {
return model
}
return "openai/" + model
}
raw = strings.TrimSpace(raw)
if raw == "" {
return "", false
}
if cfg != nil {
if mc, err := cfg.GetModelConfig(raw); err == nil && mc != nil && strings.TrimSpace(mc.Model) != "" {
return ensureProtocol(mc.Model), true
}
for i := range cfg.ModelList {
fullModel := strings.TrimSpace(cfg.ModelList[i].Model)
if fullModel == "" {
continue
}
if fullModel == raw {
return ensureProtocol(fullModel), true
}
_, modelID := providers.ExtractProtocol(fullModel)
if modelID == raw {
return ensureProtocol(fullModel), true
}
}
}
return "", false
}
candidates := providers.ResolveCandidatesWithLookup(modelCfg, defaults.Provider, resolveFromModelList)
return &AgentInstance{
ID: agentID,
+74
View File
@@ -93,3 +93,77 @@ func TestNewAgentInstance_DefaultsTemperatureWhenUnset(t *testing.T) {
t.Fatalf("Temperature = %f, want %f", agent.Temperature, 0.7)
}
}
func TestNewAgentInstance_ResolveCandidatesFromModelListAlias(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-instance-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
Model: "step-3.5-flash",
},
},
ModelList: []config.ModelConfig{
{
ModelName: "step-3.5-flash",
Model: "openrouter/stepfun/step-3.5-flash:free",
APIBase: "https://openrouter.ai/api/v1",
},
},
}
provider := &mockProvider{}
agent := NewAgentInstance(nil, &cfg.Agents.Defaults, cfg, provider)
if len(agent.Candidates) != 1 {
t.Fatalf("len(Candidates) = %d, want 1", len(agent.Candidates))
}
if agent.Candidates[0].Provider != "openrouter" {
t.Fatalf("candidate provider = %q, want %q", agent.Candidates[0].Provider, "openrouter")
}
if agent.Candidates[0].Model != "stepfun/step-3.5-flash:free" {
t.Fatalf("candidate model = %q, want %q", agent.Candidates[0].Model, "stepfun/step-3.5-flash:free")
}
}
func TestNewAgentInstance_ResolveCandidatesFromModelListAliasWithoutProtocol(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-instance-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
Model: "glm-5",
},
},
ModelList: []config.ModelConfig{
{
ModelName: "glm-5",
Model: "glm-5",
APIBase: "https://api.z.ai/api/coding/paas/v4",
},
},
}
provider := &mockProvider{}
agent := NewAgentInstance(nil, &cfg.Agents.Defaults, cfg, provider)
if len(agent.Candidates) != 1 {
t.Fatalf("len(Candidates) = %d, want 1", len(agent.Candidates))
}
if agent.Candidates[0].Provider != "openai" {
t.Fatalf("candidate provider = %q, want %q", agent.Candidates[0].Provider, "openai")
}
if agent.Candidates[0].Model != "glm-5" {
t.Fatalf("candidate model = %q, want %q", agent.Candidates[0].Model, "glm-5")
}
}
+16 -1
View File
@@ -43,11 +43,26 @@ func NewFallbackChain(cooldown *CooldownTracker) *FallbackChain {
// ResolveCandidates parses model config into a deduplicated candidate list.
func ResolveCandidates(cfg ModelConfig, defaultProvider string) []FallbackCandidate {
return ResolveCandidatesWithLookup(cfg, defaultProvider, nil)
}
func ResolveCandidatesWithLookup(
cfg ModelConfig,
defaultProvider string,
lookup func(raw string) (resolved string, ok bool),
) []FallbackCandidate {
seen := make(map[string]bool)
var candidates []FallbackCandidate
addCandidate := func(raw string) {
ref := ParseModelRef(raw, defaultProvider)
candidateRaw := strings.TrimSpace(raw)
if lookup != nil {
if resolved, ok := lookup(candidateRaw); ok {
candidateRaw = resolved
}
}
ref := ParseModelRef(candidateRaw, defaultProvider)
if ref == nil {
return
}
+69
View File
@@ -453,6 +453,75 @@ func TestResolveCandidates_EmptyPrimary(t *testing.T) {
}
}
func TestResolveCandidatesWithLookup_AliasResolvesToNestedModel(t *testing.T) {
cfg := ModelConfig{
Primary: "step-3.5-flash",
Fallbacks: nil,
}
lookup := func(raw string) (string, bool) {
if raw == "step-3.5-flash" {
return "openrouter/stepfun/step-3.5-flash:free", true
}
return "", false
}
candidates := ResolveCandidatesWithLookup(cfg, "", lookup)
if len(candidates) != 1 {
t.Fatalf("candidates = %d, want 1", len(candidates))
}
if candidates[0].Provider != "openrouter" {
t.Fatalf("provider = %q, want openrouter", candidates[0].Provider)
}
if candidates[0].Model != "stepfun/step-3.5-flash:free" {
t.Fatalf("model = %q, want stepfun/step-3.5-flash:free", candidates[0].Model)
}
}
func TestResolveCandidatesWithLookup_DeduplicateAfterLookup(t *testing.T) {
cfg := ModelConfig{
Primary: "step-3.5-flash",
Fallbacks: []string{"openrouter/stepfun/step-3.5-flash:free"},
}
lookup := func(raw string) (string, bool) {
if raw == "step-3.5-flash" {
return "openrouter/stepfun/step-3.5-flash:free", true
}
return "", false
}
candidates := ResolveCandidatesWithLookup(cfg, "", lookup)
if len(candidates) != 1 {
t.Fatalf("candidates = %d, want 1", len(candidates))
}
}
func TestResolveCandidatesWithLookup_AliasWithoutProtocolUsesDefaultProvider(t *testing.T) {
cfg := ModelConfig{
Primary: "glm-5",
Fallbacks: nil,
}
lookup := func(raw string) (string, bool) {
if raw == "glm-5" {
return "glm-5", true
}
return "", false
}
candidates := ResolveCandidatesWithLookup(cfg, "openai", lookup)
if len(candidates) != 1 {
t.Fatalf("candidates = %d, want 1", len(candidates))
}
if candidates[0].Provider != "openai" {
t.Fatalf("provider = %q, want openai", candidates[0].Provider)
}
if candidates[0].Model != "glm-5" {
t.Fatalf("model = %q, want glm-5", candidates[0].Model)
}
}
func TestFallbackExhaustedError_Message(t *testing.T) {
e := &FallbackExhaustedError{
Attempts: []FallbackAttempt{