| 638 | } |
| 639 | |
| 640 | func ResolveModelWithProviderHint(modelName, providerHint string) (provider string, model string, err error) { |
| 641 | modelName = strings.TrimSpace(modelName) |
| 642 | if modelName == "" { |
| 643 | return "", "", xerrors.New("model is required") |
| 644 | } |
| 645 | |
| 646 | // Gateway providers (vercel, openrouter, openai-compat) treat the |
| 647 | // "<provider>/<model>" slash as part of the upstream model ID, so |
| 648 | // parseCanonicalModelRef would incorrectly strip the prefix and |
| 649 | // route to the embedded provider name instead. Honor an explicit |
| 650 | // gateway hint before attempting canonical-ref parsing. |
| 651 | if normalized := NormalizeProvider(providerHint); normalized != "" && isGatewayProvider(normalized) { |
| 652 | return normalized, modelName, nil |
| 653 | } |
| 654 | |
| 655 | if provider, modelID, ok := parseCanonicalModelRef(modelName); ok { |
| 656 | return provider, modelID, nil |
| 657 | } |
| 658 | |
| 659 | if provider := NormalizeProvider(providerHint); provider != "" { |
| 660 | return provider, modelName, nil |
| 661 | } |
| 662 | |
| 663 | normalized := strings.ToLower(modelName) |
| 664 | switch normalized { |
| 665 | case "claude-opus-4-6": |
| 666 | return fantasyanthropic.Name, "claude-opus-4-6", nil |
| 667 | case "gpt-5.2": |
| 668 | return fantasyopenai.Name, "gpt-5.2", nil |
| 669 | case "gemini-2.5-flash": |
| 670 | return fantasygoogle.Name, "gemini-2.5-flash", nil |
| 671 | } |
| 672 | |
| 673 | if isChatModelForProvider(fantasyanthropic.Name, normalized) { |
| 674 | return fantasyanthropic.Name, modelName, nil |
| 675 | } |
| 676 | if isChatModelForProvider(fantasyopenai.Name, normalized) { |
| 677 | return fantasyopenai.Name, modelName, nil |
| 678 | } |
| 679 | |
| 680 | return "", "", xerrors.Errorf("unknown model %q", modelName) |
| 681 | } |
| 682 | |
| 683 | func parseCanonicalModelRef(modelRef string) (provider string, model string, ok bool) { |
| 684 | modelRef = strings.TrimSpace(modelRef) |