Unified · Split
| @@ -133,8 +133,8 @@ func (fh *FallbackHandler) WrapHandler(handler gin.HandlerFunc) gin.HandlerFunc · 132 unmodified lines | ||
| 133 | 133 | return |
| 134 | 134 | } |
| 135 | 135 | |
| 136 | − // Normalize model (handles Gemini thinking suffixes) | |
| 137 | − normalizedModel, _ := util.NormalizeGeminiThinkingModel(modelName) | |
| 136 | + // Normalize model (handles dynamic thinking suffixes) | |
| 137 | + normalizedModel, _ := util.NormalizeThinkingModel(modelName) | |
| 138 | 138 | |
| 139 | 139 | // Track resolved model for logging (may change if mapping is applied) |
| 140 | 140 | resolvedModel := normalizedModel |
| @@ -26,60 +26,6 @@ func GetClaudeModels() []*ModelInfo { · 25 unmodified lines | ||
| 26 | 26 | DisplayName: "Claude 4.5 Sonnet", |
| 27 | 27 | ContextLength: 200000, |
| 28 | 28 | MaxCompletionTokens: 64000, |
| 29 | − }, | |
| 30 | − { | |
| 31 | − ID: "claude-sonnet-4-5-thinking", | |
| 32 | − Object: "model", | |
| 33 | − Created: 1759104000, // 2025-09-29 | |
| 34 | − OwnedBy: "anthropic", | |
| 35 | − Type: "claude", | |
| 36 | − DisplayName: "Claude 4.5 Sonnet Thinking", | |
| 37 | − ContextLength: 200000, | |
| 38 | − MaxCompletionTokens: 64000, | |
| 39 | − Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true}, | |
| 40 | − }, | |
| 41 | − { | |
| 42 | − ID: "claude-opus-4-5-thinking", | |
| 43 | − Object: "model", | |
| 44 | − Created: 1761955200, // 2025-11-01 | |
| 45 | − OwnedBy: "anthropic", | |
| 46 | − Type: "claude", | |
| 47 | − DisplayName: "Claude 4.5 Opus Thinking", | |
| 48 | − ContextLength: 200000, | |
| 49 | − MaxCompletionTokens: 64000, | |
| 50 | − Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true}, | |
| 51 | − }, | |
| 52 | − { | |
| 53 | − ID: "claude-opus-4-5-thinking-low", | |
| 54 | − Object: "model", | |
| 55 | − Created: 1761955200, // 2025-11-01 | |
| 56 | − OwnedBy: "anthropic", | |
| 57 | − Type: "claude", | |
| 58 | − DisplayName: "Claude 4.5 Opus Thinking Low", | |
| 59 | − ContextLength: 200000, | |
| 60 | − MaxCompletionTokens: 64000, | |
| 61 | − Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true}, | |
| 62 | − }, | |
| 63 | − { | |
| 64 | − ID: "claude-opus-4-5-thinking-medium", | |
| 65 | − Object: "model", | |
| 66 | − Created: 1761955200, // 2025-11-01 | |
| 67 | − OwnedBy: "anthropic", | |
| 68 | − Type: "claude", | |
| 69 | − DisplayName: "Claude 4.5 Opus Thinking Medium", | |
| 70 | − ContextLength: 200000, | |
| 71 | − MaxCompletionTokens: 64000, | |
| 72 | − Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true}, | |
| 73 | − }, | |
| 74 | − { | |
| 75 | − ID: "claude-opus-4-5-thinking-high", | |
| 76 | − Object: "model", | |
| 77 | − Created: 1761955200, // 2025-11-01 | |
| 78 | − OwnedBy: "anthropic", | |
| 79 | − Type: "claude", | |
| 80 | − DisplayName: "Claude 4.5 Opus Thinking High", | |
| 81 | − ContextLength: 200000, | |
| 82 | − MaxCompletionTokens: 64000, | |
| 83 | 29 | Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true}, |
| 84 | 30 | }, |
| 85 | 31 | { |
| @@ -92,6 +38,7 @@ func GetClaudeModels() []*ModelInfo { · 6 unmodified lines | ||
| 92 | 38 | Description: "Premium model combining maximum intelligence with practical performance", |
| 93 | 39 | ContextLength: 200000, |
| 94 | 40 | MaxCompletionTokens: 64000, |
| 41 | + Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true}, | |
| 95 | 42 | }, |
| 96 | 43 | { |
| 97 | 44 | ID: "claude-opus-4-1-20250805", |
| @@ -530,58 +477,6 @@ func GetOpenAIModels() []*ModelInfo { · 432 unmodified lines | ||
| 530 | 477 | MaxCompletionTokens: 128000, |
| 531 | 478 | SupportedParameters: []string{"tools"}, |
| 532 | 479 | }, |
| 533 | − { | |
| 534 | − ID: "gpt-5-minimal", | |
| 535 | − Object: "model", | |
| 536 | − Created: 1754524800, | |
| 537 | − OwnedBy: "openai", | |
| 538 | − Type: "openai", | |
| 539 | − Version: "gpt-5-2025-08-07", | |
| 540 | − DisplayName: "GPT 5 Minimal", | |
| 541 | − Description: "Stable version of GPT 5, The best model for coding and agentic tasks across domains.", | |
| 542 | − ContextLength: 400000, | |
| 543 | − MaxCompletionTokens: 128000, | |
| 544 | − SupportedParameters: []string{"tools"}, | |
| 545 | − }, | |
| 546 | − { | |
| 547 | − ID: "gpt-5-low", | |
| 548 | − Object: "model", | |
| 549 | − Created: 1754524800, | |
| 550 | − OwnedBy: "openai", | |
| 551 | − Type: "openai", | |
| 552 | − Version: "gpt-5-2025-08-07", | |
| 553 | − DisplayName: "GPT 5 Low", | |
| 554 | − Description: "Stable version of GPT 5, The best model for coding and agentic tasks across domains.", | |
| 555 | − ContextLength: 400000, | |
| 556 | − MaxCompletionTokens: 128000, | |
| 557 | − SupportedParameters: []string{"tools"}, | |
| 558 | − }, | |
| 559 | − { | |
| 560 | − ID: "gpt-5-medium", | |
| 561 | − Object: "model", | |
| 562 | − Created: 1754524800, | |
| 563 | − OwnedBy: "openai", | |
| 564 | − Type: "openai", | |
| 565 | − Version: "gpt-5-2025-08-07", | |
| 566 | − DisplayName: "GPT 5 Medium", | |
| 567 | − Description: "Stable version of GPT 5, The best model for coding and agentic tasks across domains.", | |
| 568 | − ContextLength: 400000, | |
| 569 | − MaxCompletionTokens: 128000, | |
| 570 | − SupportedParameters: []string{"tools"}, | |
| 571 | − }, | |
| 572 | − { | |
| 573 | − ID: "gpt-5-high", | |
| 574 | − Object: "model", | |
| 575 | − Created: 1754524800, | |
| 576 | − OwnedBy: "openai", | |
| 577 | − Type: "openai", | |
| 578 | − Version: "gpt-5-2025-08-07", | |
| 579 | − DisplayName: "GPT 5 High", | |
| 580 | − Description: "Stable version of GPT 5, The best model for coding and agentic tasks across domains.", | |
| 581 | − ContextLength: 400000, | |
| 582 | − MaxCompletionTokens: 128000, | |
| 583 | − SupportedParameters: []string{"tools"}, | |
| 584 | − }, | |
| 585 | 480 | { |
| 586 | 481 | ID: "gpt-5-codex", |
| 587 | 482 | Object: "model", |
| @@ -595,45 +490,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines | ||
| 595 | 490 | MaxCompletionTokens: 128000, |
| 596 | 491 | SupportedParameters: []string{"tools"}, |
| 597 | 492 | }, |
| 598 | − { | |
| 599 | − ID: "gpt-5-codex-low", | |
| 600 | − Object: "model", | |
| 601 | − Created: 1757894400, | |
| 602 | − OwnedBy: "openai", | |
| 603 | − Type: "openai", | |
| 604 | − Version: "gpt-5-2025-09-15", | |
| 605 | − DisplayName: "GPT 5 Codex Low", | |
| 606 | − Description: "Stable version of GPT 5 Codex, The best model for coding and agentic tasks across domains.", | |
| 607 | − ContextLength: 400000, | |
| 608 | − MaxCompletionTokens: 128000, | |
| 609 | − SupportedParameters: []string{"tools"}, | |
| 610 | − }, | |
| 611 | − { | |
| 612 | − ID: "gpt-5-codex-medium", | |
| 613 | − Object: "model", | |
| 614 | − Created: 1757894400, | |
| 615 | − OwnedBy: "openai", | |
| 616 | − Type: "openai", | |
| 617 | − Version: "gpt-5-2025-09-15", | |
| 618 | − DisplayName: "GPT 5 Codex Medium", | |
| 619 | − Description: "Stable version of GPT 5 Codex, The best model for coding and agentic tasks across domains.", | |
| 620 | − ContextLength: 400000, | |
| 621 | − MaxCompletionTokens: 128000, | |
| 622 | − SupportedParameters: []string{"tools"}, | |
| 623 | − }, | |
| 624 | − { | |
| 625 | − ID: "gpt-5-codex-high", | |
| 626 | − Object: "model", | |
| 627 | − Created: 1757894400, | |
| 628 | − OwnedBy: "openai", | |
| 629 | − Type: "openai", | |
| 630 | − Version: "gpt-5-2025-09-15", | |
| 631 | − DisplayName: "GPT 5 Codex High", | |
| 632 | − Description: "Stable version of GPT 5 Codex, The best model for coding and agentic tasks across domains.", | |
| 633 | − ContextLength: 400000, | |
| 634 | − MaxCompletionTokens: 128000, | |
| 635 | − SupportedParameters: []string{"tools"}, | |
| 636 | − }, | |
| 637 | 493 | { |
| 638 | 494 | ID: "gpt-5-codex-mini", |
| 639 | 495 | Object: "model", |
| @@ -647,32 +503,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines | ||
| 647 | 503 | MaxCompletionTokens: 128000, |
| 648 | 504 | SupportedParameters: []string{"tools"}, |
| 649 | 505 | }, |
| 650 | − { | |
| 651 | − ID: "gpt-5-codex-mini-medium", | |
| 652 | − Object: "model", | |
| 653 | − Created: 1762473600, | |
| 654 | − OwnedBy: "openai", | |
| 655 | − Type: "openai", | |
| 656 | − Version: "gpt-5-2025-11-07", | |
| 657 | − DisplayName: "GPT 5 Codex Mini Medium", | |
| 658 | − Description: "Stable version of GPT 5 Codex Mini: cheaper, faster, but less capable version of GPT 5 Codex.", | |
| 659 | − ContextLength: 400000, | |
| 660 | − MaxCompletionTokens: 128000, | |
| 661 | − SupportedParameters: []string{"tools"}, | |
| 662 | − }, | |
| 663 | − { | |
| 664 | − ID: "gpt-5-codex-mini-high", | |
| 665 | − Object: "model", | |
| 666 | − Created: 1762473600, | |
| 667 | − OwnedBy: "openai", | |
| 668 | − Type: "openai", | |
| 669 | − Version: "gpt-5-2025-11-07", | |
| 670 | − DisplayName: "GPT 5 Codex Mini High", | |
| 671 | − Description: "Stable version of GPT 5 Codex Mini: cheaper, faster, but less capable version of GPT 5 Codex.", | |
| 672 | − ContextLength: 400000, | |
| 673 | − MaxCompletionTokens: 128000, | |
| 674 | − SupportedParameters: []string{"tools"}, | |
| 675 | − }, | |
| 676 | 506 | { |
| 677 | 507 | ID: "gpt-5.1", |
| 678 | 508 | Object: "model", |
| @@ -686,58 +516,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines | ||
| 686 | 516 | MaxCompletionTokens: 128000, |
| 687 | 517 | SupportedParameters: []string{"tools"}, |
| 688 | 518 | }, |
| 689 | − { | |
| 690 | − ID: "gpt-5.1-none", | |
| 691 | − Object: "model", | |
| 692 | − Created: 1762905600, | |
| 693 | − OwnedBy: "openai", | |
| 694 | − Type: "openai", | |
| 695 | − Version: "gpt-5.1-2025-11-12", | |
| 696 | − DisplayName: "GPT 5.1 Nothink", | |
| 697 | − Description: "Stable version of GPT 5.1, The best model for coding and agentic tasks across domains.", | |
| 698 | − ContextLength: 400000, | |
| 699 | − MaxCompletionTokens: 128000, | |
| 700 | − SupportedParameters: []string{"tools"}, | |
| 701 | − }, | |
| 702 | − { | |
| 703 | − ID: "gpt-5.1-low", | |
| 704 | − Object: "model", | |
| 705 | − Created: 1762905600, | |
| 706 | − OwnedBy: "openai", | |
| 707 | − Type: "openai", | |
| 708 | − Version: "gpt-5.1-2025-11-12", | |
| 709 | − DisplayName: "GPT 5 Low", | |
| 710 | − Description: "Stable version of GPT 5, The best model for coding and agentic tasks across domains.", | |
| 711 | − ContextLength: 400000, | |
| 712 | − MaxCompletionTokens: 128000, | |
| 713 | − SupportedParameters: []string{"tools"}, | |
| 714 | − }, | |
| 715 | − { | |
| 716 | − ID: "gpt-5.1-medium", | |
| 717 | − Object: "model", | |
| 718 | − Created: 1762905600, | |
| 719 | − OwnedBy: "openai", | |
| 720 | − Type: "openai", | |
| 721 | − Version: "gpt-5.1-2025-11-12", | |
| 722 | − DisplayName: "GPT 5.1 Medium", | |
| 723 | − Description: "Stable version of GPT 5.1, The best model for coding and agentic tasks across domains.", | |
| 724 | − ContextLength: 400000, | |
| 725 | − MaxCompletionTokens: 128000, | |
| 726 | − SupportedParameters: []string{"tools"}, | |
| 727 | − }, | |
| 728 | − { | |
| 729 | − ID: "gpt-5.1-high", | |
| 730 | − Object: "model", | |
| 731 | − Created: 1762905600, | |
| 732 | − OwnedBy: "openai", | |
| 733 | − Type: "openai", | |
| 734 | − Version: "gpt-5.1-2025-11-12", | |
| 735 | − DisplayName: "GPT 5.1 High", | |
| 736 | − Description: "Stable version of GPT 5.1, The best model for coding and agentic tasks across domains.", | |
| 737 | − ContextLength: 400000, | |
| 738 | − MaxCompletionTokens: 128000, | |
| 739 | − SupportedParameters: []string{"tools"}, | |
| 740 | − }, | |
| 741 | 519 | { |
| 742 | 520 | ID: "gpt-5.1-codex", |
| 743 | 521 | Object: "model", |
| @@ -751,45 +529,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines | ||
| 751 | 529 | MaxCompletionTokens: 128000, |
| 752 | 530 | SupportedParameters: []string{"tools"}, |
| 753 | 531 | }, |
| 754 | − { | |
| 755 | − ID: "gpt-5.1-codex-low", | |
| 756 | − Object: "model", | |
| 757 | − Created: 1762905600, | |
| 758 | − OwnedBy: "openai", | |
| 759 | − Type: "openai", | |
| 760 | − Version: "gpt-5.1-2025-11-12", | |
| 761 | − DisplayName: "GPT 5.1 Codex Low", | |
| 762 | − Description: "Stable version of GPT 5.1 Codex, The best model for coding and agentic tasks across domains.", | |
| 763 | − ContextLength: 400000, | |
| 764 | − MaxCompletionTokens: 128000, | |
| 765 | − SupportedParameters: []string{"tools"}, | |
| 766 | − }, | |
| 767 | − { | |
| 768 | − ID: "gpt-5.1-codex-medium", | |
| 769 | − Object: "model", | |
| 770 | − Created: 1762905600, | |
| 771 | − OwnedBy: "openai", | |
| 772 | − Type: "openai", | |
| 773 | − Version: "gpt-5.1-2025-11-12", | |
| 774 | − DisplayName: "GPT 5.1 Codex Medium", | |
| 775 | − Description: "Stable version of GPT 5.1 Codex, The best model for coding and agentic tasks across domains.", | |
| 776 | − ContextLength: 400000, | |
| 777 | − MaxCompletionTokens: 128000, | |
| 778 | − SupportedParameters: []string{"tools"}, | |
| 779 | − }, | |
| 780 | − { | |
| 781 | − ID: "gpt-5.1-codex-high", | |
| 782 | − Object: "model", | |
| 783 | − Created: 1762905600, | |
| 784 | − OwnedBy: "openai", | |
| 785 | − Type: "openai", | |
| 786 | − Version: "gpt-5.1-2025-11-12", | |
| 787 | − DisplayName: "GPT 5.1 Codex High", | |
| 788 | − Description: "Stable version of GPT 5.1 Codex, The best model for coding and agentic tasks across domains.", | |
| 789 | − ContextLength: 400000, | |
| 790 | − MaxCompletionTokens: 128000, | |
| 791 | − SupportedParameters: []string{"tools"}, | |
| 792 | − }, | |
| 793 | 532 | { |
| 794 | 533 | ID: "gpt-5.1-codex-mini", |
| 795 | 534 | Object: "model", |
| @@ -803,33 +542,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines | ||
| 803 | 542 | MaxCompletionTokens: 128000, |
| 804 | 543 | SupportedParameters: []string{"tools"}, |
| 805 | 544 | }, |
| 806 | − { | |
| 807 | − ID: "gpt-5.1-codex-mini-medium", | |
| 808 | − Object: "model", | |
| 809 | − Created: 1762905600, | |
| 810 | − OwnedBy: "openai", | |
| 811 | − Type: "openai", | |
| 812 | − Version: "gpt-5.1-2025-11-12", | |
| 813 | − DisplayName: "GPT 5.1 Codex Mini Medium", | |
| 814 | − Description: "Stable version of GPT 5.1 Codex Mini: cheaper, faster, but less capable version of GPT 5.1 Codex.", | |
| 815 | − ContextLength: 400000, | |
| 816 | − MaxCompletionTokens: 128000, | |
| 817 | − SupportedParameters: []string{"tools"}, | |
| 818 | − }, | |
| 819 | − { | |
| 820 | − ID: "gpt-5.1-codex-mini-high", | |
| 821 | − Object: "model", | |
| 822 | − Created: 1762905600, | |
| 823 | − OwnedBy: "openai", | |
| 824 | − Type: "openai", | |
| 825 | − Version: "gpt-5.1-2025-11-12", | |
| 826 | − DisplayName: "GPT 5.1 Codex Mini High", | |
| 827 | − Description: "Stable version of GPT 5.1 Codex Mini: cheaper, faster, but less capable version of GPT 5.1 Codex.", | |
| 828 | − ContextLength: 400000, | |
| 829 | − MaxCompletionTokens: 128000, | |
| 830 | − SupportedParameters: []string{"tools"}, | |
| 831 | − }, | |
| 832 | − | |
| 833 | 545 | { |
| 834 | 546 | ID: "gpt-5.1-codex-max", |
| 835 | 547 | Object: "model", |
| @@ -843,58 +555,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines | ||
| 843 | 555 | MaxCompletionTokens: 128000, |
| 844 | 556 | SupportedParameters: []string{"tools"}, |
| 845 | 557 | }, |
| 846 | − { | |
| 847 | − ID: "gpt-5.1-codex-max-low", | |
| 848 | − Object: "model", | |
| 849 | − Created: 1763424000, | |
| 850 | − OwnedBy: "openai", | |
| 851 | − Type: "openai", | |
| 852 | − Version: "gpt-5.1-max", | |
| 853 | − DisplayName: "GPT 5.1 Codex Max Low", | |
| 854 | − Description: "Stable version of GPT 5.1 Codex Max Low", | |
| 855 | − ContextLength: 400000, | |
| 856 | − MaxCompletionTokens: 128000, | |
| 857 | − SupportedParameters: []string{"tools"}, | |
| 858 | − }, | |
| 859 | − { | |
| 860 | − ID: "gpt-5.1-codex-max-medium", | |
| 861 | − Object: "model", | |
| 862 | − Created: 1763424000, | |
| 863 | − OwnedBy: "openai", | |
| 864 | − Type: "openai", | |
| 865 | − Version: "gpt-5.1-max", | |
| 866 | − DisplayName: "GPT 5.1 Codex Max Medium", | |
| 867 | − Description: "Stable version of GPT 5.1 Codex Max Medium", | |
| 868 | − ContextLength: 400000, | |
| 869 | − MaxCompletionTokens: 128000, | |
| 870 | − SupportedParameters: []string{"tools"}, | |
| 871 | − }, | |
| 872 | − { | |
| 873 | − ID: "gpt-5.1-codex-max-high", | |
| 874 | − Object: "model", | |
| 875 | − Created: 1763424000, | |
| 876 | − OwnedBy: "openai", | |
| 877 | − Type: "openai", | |
| 878 | − Version: "gpt-5.1-max", | |
| 879 | − DisplayName: "GPT 5.1 Codex Max High", | |
| 880 | − Description: "Stable version of GPT 5.1 Codex Max High", | |
| 881 | − ContextLength: 400000, | |
| 882 | − MaxCompletionTokens: 128000, | |
| 883 | − SupportedParameters: []string{"tools"}, | |
| 884 | − }, | |
| 885 | − { | |
| 886 | − ID: "gpt-5.1-codex-max-xhigh", | |
| 887 | − Object: "model", | |
| 888 | − Created: 1763424000, | |
| 889 | − OwnedBy: "openai", | |
| 890 | − Type: "openai", | |
| 891 | − Version: "gpt-5.1-max", | |
| 892 | − DisplayName: "GPT 5.1 Codex Max XHigh", | |
| 893 | − Description: "Stable version of GPT 5.1 Codex Max XHigh", | |
| 894 | − ContextLength: 400000, | |
| 895 | − MaxCompletionTokens: 128000, | |
| 896 | − SupportedParameters: []string{"tools"}, | |
| 897 | − }, | |
| 898 | 558 | } |
| 899 | 559 | } |
| 900 | 560 | |
| @@ -54,15 +54,22 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r · 53 unmodified lines | ||
| 54 | 54 | // Use streaming translation to preserve function calling, except for claude. |
| 55 | 55 | stream := from != to |
| 56 | 56 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream) |
| 57 | − modelForUpstream := req.Model | |
| 58 | − if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" { | |
| 59 | − body, _ = sjson.SetBytes(body, "model", modelOverride) | |
| 60 | − modelForUpstream = modelOverride | |
| 57 | + upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | |
| 58 | + if upstreamModel == "" { | |
| 59 | + upstreamModel = req.Model | |
| 60 | + } | |
| 61 | + if modelOverride := e.resolveUpstreamModel(upstreamModel, auth); modelOverride != "" { | |
| 62 | + upstreamModel = modelOverride | |
| 63 | + } else if !strings.EqualFold(upstreamModel, req.Model) { | |
| 64 | + if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" { | |
| 65 | + upstreamModel = modelOverride | |
| 66 | + } | |
| 61 | 67 | } |
| 62 | − // Inject thinking config based on model suffix for thinking variants | |
| 63 | − body = e.injectThinkingConfig(req.Model, body) | |
| 68 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 69 | + // Inject thinking config based on model metadata for thinking variants | |
| 70 | + body = e.injectThinkingConfig(req.Model, req.Metadata, body) | |
| 64 | 71 | |
| 65 | − if !strings.HasPrefix(modelForUpstream, "claude-3-5-haiku") { | |
| 72 | + if !strings.HasPrefix(upstreamModel, "claude-3-5-haiku") { | |
| 66 | 73 | body = checkSystemInstructions(body) |
| 67 | 74 | } |
| 68 | 75 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| @@ -161,11 +168,20 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A · 92 unmodified lines | ||
| 161 | 168 | from := opts.SourceFormat |
| 162 | 169 | to := sdktranslator.FromString("claude") |
| 163 | 170 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) |
| 164 | − if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" { | |
| 165 | − body, _ = sjson.SetBytes(body, "model", modelOverride) | |
| 171 | + upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | |
| 172 | + if upstreamModel == "" { | |
| 173 | + upstreamModel = req.Model | |
| 174 | + } | |
| 175 | + if modelOverride := e.resolveUpstreamModel(upstreamModel, auth); modelOverride != "" { | |
| 176 | + upstreamModel = modelOverride | |
| 177 | + } else if !strings.EqualFold(upstreamModel, req.Model) { | |
| 178 | + if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" { | |
| 179 | + upstreamModel = modelOverride | |
| 180 | + } | |
| 166 | 181 | } |
| 167 | − // Inject thinking config based on model suffix for thinking variants | |
| 168 | − body = e.injectThinkingConfig(req.Model, body) | |
| 182 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 183 | + // Inject thinking config based on model metadata for thinking variants | |
| 184 | + body = e.injectThinkingConfig(req.Model, req.Metadata, body) | |
| 169 | 185 | body = checkSystemInstructions(body) |
| 170 | 186 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 171 | 187 | |
| @@ -295,13 +311,20 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut · 123 unmodified lines | ||
| 295 | 311 | // Use streaming translation to preserve function calling, except for claude. |
| 296 | 312 | stream := from != to |
| 297 | 313 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream) |
| 298 | − modelForUpstream := req.Model | |
| 299 | − if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" { | |
| 300 | − body, _ = sjson.SetBytes(body, "model", modelOverride) | |
| 301 | − modelForUpstream = modelOverride | |
| 314 | + upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | |
| 315 | + if upstreamModel == "" { | |
| 316 | + upstreamModel = req.Model | |
| 317 | + } | |
| 318 | + if modelOverride := e.resolveUpstreamModel(upstreamModel, auth); modelOverride != "" { | |
| 319 | + upstreamModel = modelOverride | |
| 320 | + } else if !strings.EqualFold(upstreamModel, req.Model) { | |
| 321 | + if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" { | |
| 322 | + upstreamModel = modelOverride | |
| 323 | + } | |
| 302 | 324 | } |
| 325 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 303 | 326 | |
| 304 | − if !strings.HasPrefix(modelForUpstream, "claude-3-5-haiku") { | |
| 327 | + if !strings.HasPrefix(upstreamModel, "claude-3-5-haiku") { | |
| 305 | 328 | body = checkSystemInstructions(body) |
| 306 | 329 | } |
| 307 | 330 | |
| @@ -427,25 +450,15 @@ func extractAndRemoveBetas(body []byte) ([]string, []byte) { · 119 unmodified lines | ||
| 427 | 450 | return betas, body |
| 428 | 451 | } |
| 429 | 452 | |
| 430 | −// injectThinkingConfig adds thinking configuration based on model name suffix | |
| 431 | −func (e *ClaudeExecutor) injectThinkingConfig(modelName string, body []byte) []byte { | |
| 453 | +// injectThinkingConfig adds thinking configuration based on metadata or legacy suffixes. | |
| 454 | +func (e *ClaudeExecutor) injectThinkingConfig(modelName string, metadata map[string]any, body []byte) []byte { | |
| 432 | 455 | // Only inject if thinking config is not already present |
| 433 | 456 | if gjson.GetBytes(body, "thinking").Exists() { |
| 434 | 457 | return body |
| 435 | 458 | } |
| 436 | 459 | |
| 437 | − var budgetTokens int | |
| 438 | − switch { | |
| 439 | − case strings.HasSuffix(modelName, "-thinking-low"): | |
| 440 | − budgetTokens = 1024 | |
| 441 | − case strings.HasSuffix(modelName, "-thinking-medium"): | |
| 442 | − budgetTokens = 8192 | |
| 443 | − case strings.HasSuffix(modelName, "-thinking-high"): | |
| 444 | − budgetTokens = 24576 | |
| 445 | − case strings.HasSuffix(modelName, "-thinking"): | |
| 446 | − // Default thinking without suffix uses medium budget | |
| 447 | − budgetTokens = 8192 | |
| 448 | − default: | |
| 460 | + budgetTokens, ok := resolveClaudeThinkingBudget(modelName, metadata) | |
| 461 | + if !ok || budgetTokens <= 0 { | |
| 449 | 462 | return body |
| 450 | 463 | } |
| 451 | 464 | |
| @@ -454,6 +467,44 @@ func (e *ClaudeExecutor) injectThinkingConfig(modelName string, body []byte) []b · 2 unmodified lines | ||
| 454 | 467 | return body |
| 455 | 468 | } |
| 456 | 469 | |
| 470 | +func resolveClaudeThinkingBudget(modelName string, metadata map[string]any) (int, bool) { | |
| 471 | + budget, include, effort, matched := util.ThinkingFromMetadata(metadata) | |
| 472 | + if matched { | |
| 473 | + if include != nil && !*include { | |
| 474 | + return 0, false | |
| 475 | + } | |
| 476 | + if budget != nil { | |
| 477 | + normalized := util.NormalizeThinkingBudget(modelName, *budget) | |
| 478 | + if normalized > 0 { | |
| 479 | + return normalized, true | |
| 480 | + } | |
| 481 | + return 0, false | |
| 482 | + } | |
| 483 | + if effort != nil { | |
| 484 | + if derived, ok := util.ThinkingEffortToBudget(modelName, *effort); ok && derived > 0 { | |
| 485 | + return derived, true | |
| 486 | + } | |
| 487 | + } | |
| 488 | + } | |
| 489 | + return claudeBudgetFromSuffix(modelName) | |
| 490 | +} | |
| 491 | + | |
| 492 | +func claudeBudgetFromSuffix(modelName string) (int, bool) { | |
| 493 | + lower := strings.ToLower(strings.TrimSpace(modelName)) | |
| 494 | + switch { | |
| 495 | + case strings.HasSuffix(lower, "-thinking-low"): | |
| 496 | + return 1024, true | |
| 497 | + case strings.HasSuffix(lower, "-thinking-medium"): | |
| 498 | + return 8192, true | |
| 499 | + case strings.HasSuffix(lower, "-thinking-high"): | |
| 500 | + return 24576, true | |
| 501 | + case strings.HasSuffix(lower, "-thinking"): | |
| 502 | + return 8192, true | |
| 503 | + default: | |
| 504 | + return 0, false | |
| 505 | + } | |
| 506 | +} | |
| 507 | + | |
| 457 | 508 | // ensureMaxTokensForThinking ensures max_tokens > thinking.budget_tokens when thinking is enabled. |
| 458 | 509 | // Anthropic API requires this constraint; violating it returns a 400 error. |
| 459 | 510 | // This function should be called after all thinking configuration is finalized. |
| @@ -491,35 +542,45 @@ func ensureMaxTokensForThinking(modelName string, body []byte) []byte { · 31 unmodified lines | ||
| 491 | 542 | } |
| 492 | 543 | |
| 493 | 544 | func (e *ClaudeExecutor) resolveUpstreamModel(alias string, auth *cliproxyauth.Auth) string { |
| 494 | − if alias == "" { | |
| 545 | + trimmed := strings.TrimSpace(alias) | |
| 546 | + if trimmed == "" { | |
| 495 | 547 | return "" |
| 496 | 548 | } |
| 497 | − // Hardcoded mappings for thinking models to actual Claude model names | |
| 498 | − switch alias { | |
| 499 | − case "claude-opus-4-5-thinking", "claude-opus-4-5-thinking-low", "claude-opus-4-5-thinking-medium", "claude-opus-4-5-thinking-high": | |
| 500 | − return "claude-opus-4-5-20251101" | |
| 501 | − case "claude-sonnet-4-5-thinking": | |
| 502 | − return "claude-sonnet-4-5-20250929" | |
| 503 | − } | |
| 549 | + | |
| 504 | 550 | entry := e.resolveClaudeConfig(auth) |
| 505 | 551 | if entry == nil { |
| 506 | 552 | return "" |
| 507 | 553 | } |
| 554 | + | |
| 555 | + normalizedModel, metadata := util.NormalizeThinkingModel(trimmed) | |
| 556 | + | |
| 557 | + // Candidate names to match against configured aliases/names. | |
| 558 | + candidates := []string{strings.TrimSpace(normalizedModel)} | |
| 559 | + if !strings.EqualFold(normalizedModel, trimmed) { | |
| 560 | + candidates = append(candidates, trimmed) | |
| 561 | + } | |
| 562 | + if original := util.ResolveOriginalModel(normalizedModel, metadata); original != "" && !strings.EqualFold(original, normalizedModel) { | |
| 563 | + candidates = append(candidates, original) | |
| 564 | + } | |
| 565 | + | |
| 508 | 566 | for i := range entry.Models { |
| 509 | 567 | model := entry.Models[i] |
| 510 | 568 | name := strings.TrimSpace(model.Name) |
| 511 | 569 | modelAlias := strings.TrimSpace(model.Alias) |
| 512 | − if modelAlias != "" { | |
| 513 | − if strings.EqualFold(modelAlias, alias) { | |
| 570 | + | |
| 571 | + for _, candidate := range candidates { | |
| 572 | + if candidate == "" { | |
| 573 | + continue | |
| 574 | + } | |
| 575 | + if modelAlias != "" && strings.EqualFold(modelAlias, candidate) { | |
| 514 | 576 | if name != "" { |
| 515 | 577 | return name |
| 516 | 578 | } |
| 517 | − return alias | |
| 579 | + return candidate | |
| 580 | + } | |
| 581 | + if name != "" && strings.EqualFold(name, candidate) { | |
| 582 | + return name | |
| 518 | 583 | } |
| 519 | − continue | |
| 520 | − } | |
| 521 | − if name != "" && strings.EqualFold(name, alias) { | |
| 522 | − return name | |
| 523 | 584 | } |
| 524 | 585 | } |
| 525 | 586 | return "" |
| @@ -49,14 +49,14 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re · 48 unmodified lines | ||
| 49 | 49 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 50 | 50 | defer reporter.trackFailure(ctx, &err) |
| 51 | 51 | |
| 52 | + upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | |
| 53 | + | |
| 52 | 54 | from := opts.SourceFormat |
| 53 | 55 | to := sdktranslator.FromString("codex") |
| 54 | 56 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) |
| 55 | − | |
| 56 | − body = e.setReasoningEffortByAlias(req.Model, body) | |
| 57 | − | |
| 57 | + body = applyReasoningEffortMetadata(body, req.Metadata, req.Model) | |
| 58 | 58 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 59 | − | |
| 59 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 60 | 60 | body, _ = sjson.SetBytes(body, "stream", true) |
| 61 | 61 | body, _ = sjson.DeleteBytes(body, "previous_response_id") |
| 62 | 62 | |
| @@ -142,13 +142,16 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au · 79 unmodified lines | ||
| 142 | 142 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 143 | 143 | defer reporter.trackFailure(ctx, &err) |
| 144 | 144 | |
| 145 | + upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | |
| 146 | + | |
| 145 | 147 | from := opts.SourceFormat |
| 146 | 148 | to := sdktranslator.FromString("codex") |
| 147 | 149 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) |
| 148 | 150 | |
| 149 | − body = e.setReasoningEffortByAlias(req.Model, body) | |
| 151 | + body = applyReasoningEffortMetadata(body, req.Metadata, req.Model) | |
| 150 | 152 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 151 | 153 | body, _ = sjson.DeleteBytes(body, "previous_response_id") |
| 154 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 152 | 155 | |
| 153 | 156 | url := strings.TrimSuffix(baseURL, "/") + "/responses" |
| 154 | 157 | httpReq, err := e.cacheHelper(ctx, from, url, req, body) |
| @@ -235,14 +238,16 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au · 80 unmodified lines | ||
| 235 | 238 | } |
| 236 | 239 | |
| 237 | 240 | func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { |
| 241 | + upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | |
| 242 | + | |
| 238 | 243 | from := opts.SourceFormat |
| 239 | 244 | to := sdktranslator.FromString("codex") |
| 240 | 245 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) |
| 241 | 246 | |
| 242 | 247 | modelForCounting := req.Model |
| 243 | 248 | |
| 244 | − body = e.setReasoningEffortByAlias(req.Model, body) | |
| 245 | − | |
| 249 | + body = applyReasoningEffortMetadata(body, req.Metadata, req.Model) | |
| 250 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 246 | 251 | body, _ = sjson.DeleteBytes(body, "previous_response_id") |
| 247 | 252 | body, _ = sjson.SetBytes(body, "stream", false) |
| 248 | 253 | |
| @@ -261,83 +266,6 @@ func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth · 12 unmodified lines | ||
| 261 | 266 | return cliproxyexecutor.Response{Payload: []byte(translated)}, nil |
| 262 | 267 | } |
| 263 | 268 | |
| 264 | −func (e *CodexExecutor) setReasoningEffortByAlias(modelName string, payload []byte) []byte { | |
| 265 | − if util.InArray([]string{"gpt-5", "gpt-5-minimal", "gpt-5-low", "gpt-5-medium", "gpt-5-high"}, modelName) { | |
| 266 | − payload, _ = sjson.SetBytes(payload, "model", "gpt-5") | |
| 267 | − switch modelName { | |
| 268 | − case "gpt-5-minimal": | |
| 269 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "minimal") | |
| 270 | − case "gpt-5-low": | |
| 271 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "low") | |
| 272 | − case "gpt-5-medium": | |
| 273 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium") | |
| 274 | − case "gpt-5-high": | |
| 275 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high") | |
| 276 | − } | |
| 277 | − } else if util.InArray([]string{"gpt-5-codex", "gpt-5-codex-low", "gpt-5-codex-medium", "gpt-5-codex-high"}, modelName) { | |
| 278 | − payload, _ = sjson.SetBytes(payload, "model", "gpt-5-codex") | |
| 279 | − switch modelName { | |
| 280 | − case "gpt-5-codex-low": | |
| 281 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "low") | |
| 282 | − case "gpt-5-codex-medium": | |
| 283 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium") | |
| 284 | − case "gpt-5-codex-high": | |
| 285 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high") | |
| 286 | − } | |
| 287 | − } else if util.InArray([]string{"gpt-5-codex-mini", "gpt-5-codex-mini-medium", "gpt-5-codex-mini-high"}, modelName) { | |
| 288 | − payload, _ = sjson.SetBytes(payload, "model", "gpt-5-codex-mini") | |
| 289 | − switch modelName { | |
| 290 | − case "gpt-5-codex-mini-medium": | |
| 291 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium") | |
| 292 | − case "gpt-5-codex-mini-high": | |
| 293 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high") | |
| 294 | − } | |
| 295 | − } else if util.InArray([]string{"gpt-5.1", "gpt-5.1-none", "gpt-5.1-low", "gpt-5.1-medium", "gpt-5.1-high"}, modelName) { | |
| 296 | − payload, _ = sjson.SetBytes(payload, "model", "gpt-5.1") | |
| 297 | − switch modelName { | |
| 298 | − case "gpt-5.1-none": | |
| 299 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "none") | |
| 300 | − case "gpt-5.1-low": | |
| 301 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "low") | |
| 302 | − case "gpt-5.1-medium": | |
| 303 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium") | |
| 304 | − case "gpt-5.1-high": | |
| 305 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high") | |
| 306 | − } | |
| 307 | − } else if util.InArray([]string{"gpt-5.1-codex", "gpt-5.1-codex-low", "gpt-5.1-codex-medium", "gpt-5.1-codex-high"}, modelName) { | |
| 308 | − payload, _ = sjson.SetBytes(payload, "model", "gpt-5.1-codex") | |
| 309 | − switch modelName { | |
| 310 | − case "gpt-5.1-codex-low": | |
| 311 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "low") | |
| 312 | − case "gpt-5.1-codex-medium": | |
| 313 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium") | |
| 314 | − case "gpt-5.1-codex-high": | |
| 315 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high") | |
| 316 | − } | |
| 317 | − } else if util.InArray([]string{"gpt-5.1-codex-mini", "gpt-5.1-codex-mini-medium", "gpt-5.1-codex-mini-high"}, modelName) { | |
| 318 | − payload, _ = sjson.SetBytes(payload, "model", "gpt-5.1-codex-mini") | |
| 319 | − switch modelName { | |
| 320 | − case "gpt-5.1-codex-mini-medium": | |
| 321 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium") | |
| 322 | − case "gpt-5.1-codex-mini-high": | |
| 323 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high") | |
| 324 | − } | |
| 325 | − } else if util.InArray([]string{"gpt-5.1-codex-max", "gpt-5.1-codex-max-low", "gpt-5.1-codex-max-medium", "gpt-5.1-codex-max-high", "gpt-5.1-codex-max-xhigh"}, modelName) { | |
| 326 | − payload, _ = sjson.SetBytes(payload, "model", "gpt-5.1-codex-max") | |
| 327 | − switch modelName { | |
| 328 | − case "gpt-5.1-codex-max-low": | |
| 329 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "low") | |
| 330 | − case "gpt-5.1-codex-max-medium": | |
| 331 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "medium") | |
| 332 | − case "gpt-5.1-codex-max-high": | |
| 333 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "high") | |
| 334 | − case "gpt-5.1-codex-max-xhigh": | |
| 335 | − payload, _ = sjson.SetBytes(payload, "reasoning.effort", "xhigh") | |
| 336 | − } | |
| 337 | − } | |
| 338 | − return payload | |
| 339 | −} | |
| 340 | − | |
| 341 | 269 | func tokenizerForCodexModel(model string) (tokenizer.Codec, error) { |
| 342 | 270 | sanitized := strings.ToLower(strings.TrimSpace(model)) |
| 343 | 271 | switch { |
| @@ -75,6 +75,8 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r · 74 unmodified lines | ||
| 75 | 75 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 76 | 76 | defer reporter.trackFailure(ctx, &err) |
| 77 | 77 | |
| 78 | + upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | |
| 79 | + | |
| 78 | 80 | // Official Gemini API via API key or OAuth bearer |
| 79 | 81 | from := opts.SourceFormat |
| 80 | 82 | to := sdktranslator.FromString("gemini") |
| @@ -85,6 +87,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r · 4 unmodified lines | ||
| 85 | 87 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) |
| 86 | 88 | body = fixGeminiImageAspectRatio(req.Model, body) |
| 87 | 89 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 90 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 88 | 91 | |
| 89 | 92 | action := "generateContent" |
| 90 | 93 | if req.Metadata != nil { |
| @@ -93,7 +96,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r · 2 unmodified lines | ||
| 93 | 96 | } |
| 94 | 97 | } |
| 95 | 98 | baseURL := resolveGeminiBaseURL(auth) |
| 96 | − url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, req.Model, action) | |
| 99 | + url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, upstreamModel, action) | |
| 97 | 100 | if opts.Alt != "" && action != "countTokens" { |
| 98 | 101 | url = url + fmt.Sprintf("?$alt=%s", opts.Alt) |
| 99 | 102 | } |
| @@ -167,6 +170,8 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A · 67 unmodified lines | ||
| 167 | 170 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 168 | 171 | defer reporter.trackFailure(ctx, &err) |
| 169 | 172 | |
| 173 | + upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | |
| 174 | + | |
| 170 | 175 | from := opts.SourceFormat |
| 171 | 176 | to := sdktranslator.FromString("gemini") |
| 172 | 177 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) |
| @@ -176,9 +181,10 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A · 3 unmodified lines | ||
| 176 | 181 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) |
| 177 | 182 | body = fixGeminiImageAspectRatio(req.Model, body) |
| 178 | 183 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 184 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 179 | 185 | |
| 180 | 186 | baseURL := resolveGeminiBaseURL(auth) |
| 181 | − url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, req.Model, "streamGenerateContent") | |
| 187 | + url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, upstreamModel, "streamGenerateContent") | |
| 182 | 188 | if opts.Alt == "" { |
| 183 | 189 | url = url + "?alt=sse" |
| 184 | 190 | } else { |
| @@ -105,10 +105,12 @@ func (e *GeminiVertexExecutor) CountTokens(ctx context.Context, auth *cliproxyau · 104 unmodified lines | ||
| 105 | 105 | |
| 106 | 106 | // countTokensWithServiceAccount handles token counting using service account credentials. |
| 107 | 107 | func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, projectID, location string, saJSON []byte) (cliproxyexecutor.Response, error) { |
| 108 | + upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | |
| 109 | + | |
| 108 | 110 | from := opts.SourceFormat |
| 109 | 111 | to := sdktranslator.FromString("gemini") |
| 110 | 112 | translatedReq := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) |
| 111 | − if budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(req.Metadata); ok && util.ModelSupportsThinking(req.Model) { | |
| 113 | + if budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(req.Model, req.Metadata); ok && util.ModelSupportsThinking(req.Model) { | |
| 112 | 114 | if budgetOverride != nil { |
| 113 | 115 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) |
| 114 | 116 | budgetOverride = &norm |
| @@ -117,13 +119,14 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context · 2 unmodified lines | ||
| 117 | 119 | } |
| 118 | 120 | translatedReq = util.StripThinkingConfigIfUnsupported(req.Model, translatedReq) |
| 119 | 121 | translatedReq = fixGeminiImageAspectRatio(req.Model, translatedReq) |
| 122 | + translatedReq, _ = sjson.SetBytes(translatedReq, "model", upstreamModel) | |
| 120 | 123 | respCtx := context.WithValue(ctx, "alt", opts.Alt) |
| 121 | 124 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools") |
| 122 | 125 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig") |
| 123 | 126 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "safetySettings") |
| 124 | 127 | |
| 125 | 128 | baseURL := vertexBaseURL(location) |
| 126 | − url := fmt.Sprintf("%s/%s/projects/%s/locations/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, projectID, location, req.Model, "countTokens") | |
| 129 | + url := fmt.Sprintf("%s/%s/projects/%s/locations/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, projectID, location, upstreamModel, "countTokens") | |
| 127 | 130 | |
| 128 | 131 | httpReq, errNewReq := http.NewRequestWithContext(respCtx, http.MethodPost, url, bytes.NewReader(translatedReq)) |
| 129 | 132 | if errNewReq != nil { |
| @@ -191,10 +194,12 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context · 61 unmodified lines | ||
| 191 | 194 | |
| 192 | 195 | // countTokensWithAPIKey handles token counting using API key credentials. |
| 193 | 196 | func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, apiKey, baseURL string) (cliproxyexecutor.Response, error) { |
| 197 | + upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | |
| 198 | + | |
| 194 | 199 | from := opts.SourceFormat |
| 195 | 200 | to := sdktranslator.FromString("gemini") |
| 196 | 201 | translatedReq := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) |
| 197 | − if budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(req.Metadata); ok && util.ModelSupportsThinking(req.Model) { | |
| 202 | + if budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(req.Model, req.Metadata); ok && util.ModelSupportsThinking(req.Model) { | |
| 198 | 203 | if budgetOverride != nil { |
| 199 | 204 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) |
| 200 | 205 | budgetOverride = &norm |
| @@ -203,6 +208,7 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth * · 2 unmodified lines | ||
| 203 | 208 | } |
| 204 | 209 | translatedReq = util.StripThinkingConfigIfUnsupported(req.Model, translatedReq) |
| 205 | 210 | translatedReq = fixGeminiImageAspectRatio(req.Model, translatedReq) |
| 211 | + translatedReq, _ = sjson.SetBytes(translatedReq, "model", upstreamModel) | |
| 206 | 212 | respCtx := context.WithValue(ctx, "alt", opts.Alt) |
| 207 | 213 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools") |
| 208 | 214 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig") |
| @@ -286,10 +292,12 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au · 77 unmodified lines | ||
| 286 | 292 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 287 | 293 | defer reporter.trackFailure(ctx, &err) |
| 288 | 294 | |
| 295 | + upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | |
| 296 | + | |
| 289 | 297 | from := opts.SourceFormat |
| 290 | 298 | to := sdktranslator.FromString("gemini") |
| 291 | 299 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) |
| 292 | − if budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(req.Metadata); ok && util.ModelSupportsThinking(req.Model) { | |
| 300 | + if budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(req.Model, req.Metadata); ok && util.ModelSupportsThinking(req.Model) { | |
| 293 | 301 | if budgetOverride != nil { |
| 294 | 302 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) |
| 295 | 303 | budgetOverride = &norm |
| @@ -301,6 +309,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au · 5 unmodified lines | ||
| 301 | 309 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) |
| 302 | 310 | body = fixGeminiImageAspectRatio(req.Model, body) |
| 303 | 311 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 312 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 304 | 313 | |
| 305 | 314 | action := "generateContent" |
| 306 | 315 | if req.Metadata != nil { |
| @@ -309,7 +318,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au · 2 unmodified lines | ||
| 309 | 318 | } |
| 310 | 319 | } |
| 311 | 320 | baseURL := vertexBaseURL(location) |
| 312 | − url := fmt.Sprintf("%s/%s/projects/%s/locations/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, projectID, location, req.Model, action) | |
| 321 | + url := fmt.Sprintf("%s/%s/projects/%s/locations/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, projectID, location, upstreamModel, action) | |
| 313 | 322 | if opts.Alt != "" && action != "countTokens" { |
| 314 | 323 | url = url + fmt.Sprintf("?$alt=%s", opts.Alt) |
| 315 | 324 | } |
| @@ -383,10 +392,12 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip · 67 unmodified lines | ||
| 383 | 392 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 384 | 393 | defer reporter.trackFailure(ctx, &err) |
| 385 | 394 | |
| 395 | + upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | |
| 396 | + | |
| 386 | 397 | from := opts.SourceFormat |
| 387 | 398 | to := sdktranslator.FromString("gemini") |
| 388 | 399 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) |
| 389 | − if budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(req.Metadata); ok && util.ModelSupportsThinking(req.Model) { | |
| 400 | + if budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(req.Model, req.Metadata); ok && util.ModelSupportsThinking(req.Model) { | |
| 390 | 401 | if budgetOverride != nil { |
| 391 | 402 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) |
| 392 | 403 | budgetOverride = &norm |
| @@ -398,6 +409,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip · 5 unmodified lines | ||
| 398 | 409 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) |
| 399 | 410 | body = fixGeminiImageAspectRatio(req.Model, body) |
| 400 | 411 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 412 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 401 | 413 | |
| 402 | 414 | action := "generateContent" |
| 403 | 415 | if req.Metadata != nil { |
| @@ -410,7 +422,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip · 6 unmodified lines | ||
| 410 | 422 | if baseURL == "" { |
| 411 | 423 | baseURL = "https://generativelanguage.googleapis.com" |
| 412 | 424 | } |
| 413 | − url := fmt.Sprintf("%s/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, req.Model, action) | |
| 425 | + url := fmt.Sprintf("%s/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, upstreamModel, action) | |
| 414 | 426 | if opts.Alt != "" && action != "countTokens" { |
| 415 | 427 | url = url + fmt.Sprintf("?$alt=%s", opts.Alt) |
| 416 | 428 | } |
| @@ -481,10 +493,12 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte · 64 unmodified lines | ||
| 481 | 493 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 482 | 494 | defer reporter.trackFailure(ctx, &err) |
| 483 | 495 | |
| 496 | + upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | |
| 497 | + | |
| 484 | 498 | from := opts.SourceFormat |
| 485 | 499 | to := sdktranslator.FromString("gemini") |
| 486 | 500 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) |
| 487 | − if budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(req.Metadata); ok && util.ModelSupportsThinking(req.Model) { | |
| 501 | + if budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(req.Model, req.Metadata); ok && util.ModelSupportsThinking(req.Model) { | |
| 488 | 502 | if budgetOverride != nil { |
| 489 | 503 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) |
| 490 | 504 | budgetOverride = &norm |
| @@ -496,9 +510,10 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte · 5 unmodified lines | ||
| 496 | 510 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) |
| 497 | 511 | body = fixGeminiImageAspectRatio(req.Model, body) |
| 498 | 512 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 513 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 499 | 514 | |
| 500 | 515 | baseURL := vertexBaseURL(location) |
| 501 | − url := fmt.Sprintf("%s/%s/projects/%s/locations/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, projectID, location, req.Model, "streamGenerateContent") | |
| 516 | + url := fmt.Sprintf("%s/%s/projects/%s/locations/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, projectID, location, upstreamModel, "streamGenerateContent") | |
| 502 | 517 | if opts.Alt == "" { |
| 503 | 518 | url = url + "?alt=sse" |
| 504 | 519 | } else { |
| @@ -595,10 +610,12 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth · 90 unmodified lines | ||
| 595 | 610 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 596 | 611 | defer reporter.trackFailure(ctx, &err) |
| 597 | 612 | |
| 613 | + upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | |
| 614 | + | |
| 598 | 615 | from := opts.SourceFormat |
| 599 | 616 | to := sdktranslator.FromString("gemini") |
| 600 | 617 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) |
| 601 | − if budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(req.Metadata); ok && util.ModelSupportsThinking(req.Model) { | |
| 618 | + if budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(req.Model, req.Metadata); ok && util.ModelSupportsThinking(req.Model) { | |
| 602 | 619 | if budgetOverride != nil { |
| 603 | 620 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) |
| 604 | 621 | budgetOverride = &norm |
| @@ -610,12 +627,13 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth · 5 unmodified lines | ||
| 610 | 627 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) |
| 611 | 628 | body = fixGeminiImageAspectRatio(req.Model, body) |
| 612 | 629 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 630 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 613 | 631 | |
| 614 | 632 | // For API key auth, use simpler URL format without project/location |
| 615 | 633 | if baseURL == "" { |
| 616 | 634 | baseURL = "https://generativelanguage.googleapis.com" |
| 617 | 635 | } |
| 618 | − url := fmt.Sprintf("%s/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, req.Model, "streamGenerateContent") | |
| 636 | + url := fmt.Sprintf("%s/%s/publishers/google/models/%s:%s", baseURL, vertexAPIVersion, upstreamModel, "streamGenerateContent") | |
| 619 | 637 | if opts.Alt == "" { |
| 620 | 638 | url = url + "?alt=sse" |
| 621 | 639 | } else { |
| @@ -57,6 +57,10 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re · 56 unmodified lines | ||
| 57 | 57 | from := opts.SourceFormat |
| 58 | 58 | to := sdktranslator.FromString("openai") |
| 59 | 59 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) |
| 60 | + body = applyReasoningEffortMetadataChatCompletions(body, req.Metadata, req.Model) | |
| 61 | + if upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata); upstreamModel != "" { | |
| 62 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 63 | + } | |
| 60 | 64 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 61 | 65 | |
| 62 | 66 | endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint |
| @@ -139,6 +143,10 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au · 76 unmodified lines | ||
| 139 | 143 | to := sdktranslator.FromString("openai") |
| 140 | 144 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) |
| 141 | 145 | |
| 146 | + body = applyReasoningEffortMetadataChatCompletions(body, req.Metadata, req.Model) | |
| 147 | + if upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata); upstreamModel != "" { | |
| 148 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 149 | + } | |
| 142 | 150 | // Ensure tools array exists to avoid provider quirks similar to Qwen's behaviour. |
| 143 | 151 | toolsResult := gjson.GetBytes(body, "tools") |
| 144 | 152 | if toolsResult.Exists() && toolsResult.IsArray() && len(toolsResult.Array()) == 0 { |
| @@ -58,6 +58,10 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A · 57 unmodified lines | ||
| 58 | 58 | translated = e.overrideModel(translated, modelOverride) |
| 59 | 59 | } |
| 60 | 60 | translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated) |
| 61 | + translated = applyReasoningEffortMetadataChatCompletions(translated, req.Metadata, req.Model) | |
| 62 | + if upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata); upstreamModel != "" { | |
| 63 | + translated, _ = sjson.SetBytes(translated, "model", upstreamModel) | |
| 64 | + } | |
| 61 | 65 | |
| 62 | 66 | url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" |
| 63 | 67 | httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated)) |
| @@ -143,6 +147,10 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy · 79 unmodified lines | ||
| 143 | 147 | translated = e.overrideModel(translated, modelOverride) |
| 144 | 148 | } |
| 145 | 149 | translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated) |
| 150 | + translated = applyReasoningEffortMetadataChatCompletions(translated, req.Metadata, req.Model) | |
| 151 | + if upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata); upstreamModel != "" { | |
| 152 | + translated, _ = sjson.SetBytes(translated, "model", upstreamModel) | |
| 153 | + } | |
| 146 | 154 | |
| 147 | 155 | url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" |
| 148 | 156 | httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated)) |
| @@ -12,8 +12,8 @@ import ( · 11 unmodified lines | ||
| 12 | 12 | // applyThinkingMetadata applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N) |
| 13 | 13 | // for standard Gemini format payloads. It normalizes the budget when the model supports thinking. |
| 14 | 14 | func applyThinkingMetadata(payload []byte, metadata map[string]any, model string) []byte { |
| 15 | − budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(metadata) | |
| 16 | − if !ok { | |
| 15 | + budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata) | |
| 16 | + if !ok || (budgetOverride == nil && includeOverride == nil) { | |
| 17 | 17 | return payload |
| 18 | 18 | } |
| 19 | 19 | if !util.ModelSupportsThinking(model) { |
| @@ -29,17 +29,60 @@ func applyThinkingMetadata(payload []byte, metadata map[string]any, model string · 9 unmodified lines | ||
| 29 | 29 | // applyThinkingMetadataCLI applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N) |
| 30 | 30 | // for Gemini CLI format payloads (nested under "request"). It normalizes the budget when the model supports thinking. |
| 31 | 31 | func applyThinkingMetadataCLI(payload []byte, metadata map[string]any, model string) []byte { |
| 32 | − budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(metadata) | |
| 33 | − if !ok { | |
| 32 | + budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata) | |
| 33 | + if !ok || (budgetOverride == nil && includeOverride == nil) { | |
| 34 | 34 | return payload |
| 35 | 35 | } |
| 36 | − if budgetOverride != nil && util.ModelSupportsThinking(model) { | |
| 36 | + if !util.ModelSupportsThinking(model) { | |
| 37 | + return payload | |
| 38 | + } | |
| 39 | + if budgetOverride != nil { | |
| 37 | 40 | norm := util.NormalizeThinkingBudget(model, *budgetOverride) |
| 38 | 41 | budgetOverride = &norm |
| 39 | 42 | } |
| 40 | 43 | return util.ApplyGeminiCLIThinkingConfig(payload, budgetOverride, includeOverride) |
| 41 | 44 | } |
| 42 | 45 | |
| 46 | +// applyReasoningEffortMetadata applies reasoning effort overrides (reasoning.effort) when present in metadata. | |
| 47 | +// It avoids overwriting an existing reasoning.effort field and only applies to models that support thinking. | |
| 48 | +func applyReasoningEffortMetadata(payload []byte, metadata map[string]any, model string) []byte { | |
| 49 | + if len(metadata) == 0 { | |
| 50 | + return payload | |
| 51 | + } | |
| 52 | + if !util.ModelSupportsThinking(model) { | |
| 53 | + return payload | |
| 54 | + } | |
| 55 | + if gjson.GetBytes(payload, "reasoning.effort").Exists() { | |
| 56 | + return payload | |
| 57 | + } | |
| 58 | + if effort, ok := util.ReasoningEffortFromMetadata(metadata); ok && effort != "" { | |
| 59 | + if updated, err := sjson.SetBytes(payload, "reasoning.effort", effort); err == nil { | |
| 60 | + return updated | |
| 61 | + } | |
| 62 | + } | |
| 63 | + return payload | |
| 64 | +} | |
| 65 | + | |
| 66 | +// applyReasoningEffortMetadataChatCompletions applies reasoning_effort (OpenAI chat completions field) | |
| 67 | +// when present in metadata. It avoids overwriting an existing reasoning_effort field. | |
| 68 | +func applyReasoningEffortMetadataChatCompletions(payload []byte, metadata map[string]any, model string) []byte { | |
| 69 | + if len(metadata) == 0 { | |
| 70 | + return payload | |
| 71 | + } | |
| 72 | + if !util.ModelSupportsThinking(model) { | |
| 73 | + return payload | |
| 74 | + } | |
| 75 | + if gjson.GetBytes(payload, "reasoning_effort").Exists() { | |
| 76 | + return payload | |
| 77 | + } | |
| 78 | + if effort, ok := util.ReasoningEffortFromMetadata(metadata); ok && effort != "" { | |
| 79 | + if updated, err := sjson.SetBytes(payload, "reasoning_effort", effort); err == nil { | |
| 80 | + return updated | |
| 81 | + } | |
| 82 | + } | |
| 83 | + return payload | |
| 84 | +} | |
| 85 | + | |
| 43 | 86 | // applyPayloadConfig applies payload default and override rules from configuration |
| 44 | 87 | // to the given JSON payload for the specified model. |
| 45 | 88 | // Defaults only fill missing fields, while overrides always overwrite existing values. |
| @@ -12,6 +12,7 @@ import ( · 11 unmodified lines | ||
| 12 | 12 | |
| 13 | 13 | qwenauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen" |
| 14 | 14 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| 15 | + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" | |
| 15 | 16 | cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" |
| 16 | 17 | cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" |
| 17 | 18 | sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" |
| @@ -50,6 +51,10 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req · 32 unmodified lines | ||
| 50 | 51 | from := opts.SourceFormat |
| 51 | 52 | to := sdktranslator.FromString("openai") |
| 52 | 53 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) |
| 54 | + body = applyReasoningEffortMetadataChatCompletions(body, req.Metadata, req.Model) | |
| 55 | + if upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata); upstreamModel != "" { | |
| 56 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 57 | + } | |
| 53 | 58 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 54 | 59 | |
| 55 | 60 | url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" |
| @@ -121,6 +126,10 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut · 65 unmodified lines | ||
| 121 | 126 | to := sdktranslator.FromString("openai") |
| 122 | 127 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) |
| 123 | 128 | |
| 129 | + body = applyReasoningEffortMetadataChatCompletions(body, req.Metadata, req.Model) | |
| 130 | + if upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata); upstreamModel != "" { | |
| 131 | + body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 132 | + } | |
| 124 | 133 | toolsResult := gjson.GetBytes(body, "tools") |
| 125 | 134 | // I'm addressing the Qwen3 "poisoning" issue, which is caused by the model needing a tool to be defined. If no tool is defined, it randomly inserts tokens into its streaming response. |
| 126 | 135 | // This will have no real consequences. It's just to scare Qwen3. |
| @@ -1,8 +1,6 @@ | ||
| 1 | 1 | package util |
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | − "encoding/json" | |
| 5 | − "strconv" | |
| 6 | 4 | "strings" |
| 7 | 5 | |
| 8 | 6 | "github.com/tidwall/gjson" |
| @@ -15,80 +13,6 @@ const ( · 6 unmodified lines | ||
| 15 | 13 | GeminiOriginalModelMetadataKey = "gemini_original_model" |
| 16 | 14 | ) |
| 17 | 15 | |
| 18 | −func ParseGeminiThinkingSuffix(model string) (string, *int, *bool, bool) { | |
| 19 | − if model == "" { | |
| 20 | − return model, nil, nil, false | |
| 21 | − } | |
| 22 | − lower := strings.ToLower(model) | |
| 23 | − if !strings.HasPrefix(lower, "gemini-") { | |
| 24 | − return model, nil, nil, false | |
| 25 | − } | |
| 26 | − | |
| 27 | − if strings.HasSuffix(lower, "-nothinking") { | |
| 28 | − base := model[:len(model)-len("-nothinking")] | |
| 29 | − budgetValue := 0 | |
| 30 | − if strings.HasPrefix(lower, "gemini-2.5-pro") { | |
| 31 | − budgetValue = 128 | |
| 32 | − } | |
| 33 | − include := false | |
| 34 | − return base, &budgetValue, &include, true | |
| 35 | − } | |
| 36 | − | |
| 37 | − // Handle "-reasoning" suffix: enables thinking with dynamic budget (-1) | |
| 38 | − // Maps: gemini-2.5-flash-reasoning -> gemini-2.5-flash with thinkingBudget=-1 | |
| 39 | − if strings.HasSuffix(lower, "-reasoning") { | |
| 40 | − base := model[:len(model)-len("-reasoning")] | |
| 41 | − budgetValue := -1 // Dynamic budget | |
| 42 | − include := true | |
| 43 | − return base, &budgetValue, &include, true | |
| 44 | − } | |
| 45 | − | |
| 46 | − idx := strings.LastIndex(lower, "-thinking-") | |
| 47 | − if idx == -1 { | |
| 48 | − return model, nil, nil, false | |
| 49 | − } | |
| 50 | − | |
| 51 | − digits := model[idx+len("-thinking-"):] | |
| 52 | − if digits == "" { | |
| 53 | − return model, nil, nil, false | |
| 54 | − } | |
| 55 | − end := len(digits) | |
| 56 | − for i := 0; i < len(digits); i++ { | |
| 57 | − if digits[i] < '0' || digits[i] > '9' { | |
| 58 | − end = i | |
| 59 | − break | |
| 60 | − } | |
| 61 | − } | |
| 62 | − if end == 0 { | |
| 63 | − return model, nil, nil, false | |
| 64 | − } | |
| 65 | − valueStr := digits[:end] | |
| 66 | − value, err := strconv.Atoi(valueStr) | |
| 67 | − if err != nil { | |
| 68 | − return model, nil, nil, false | |
| 69 | − } | |
| 70 | − base := model[:idx] | |
| 71 | − budgetValue := value | |
| 72 | − return base, &budgetValue, nil, true | |
| 73 | −} | |
| 74 | − | |
| 75 | −func NormalizeGeminiThinkingModel(modelName string) (string, map[string]any) { | |
| 76 | − baseModel, budget, include, matched := ParseGeminiThinkingSuffix(modelName) | |
| 77 | − if !matched { | |
| 78 | − return baseModel, nil | |
| 79 | − } | |
| 80 | − metadata := map[string]any{ | |
| 81 | − GeminiOriginalModelMetadataKey: modelName, | |
| 82 | − } | |
| 83 | − if budget != nil { | |
| 84 | − metadata[GeminiThinkingBudgetMetadataKey] = *budget | |
| 85 | − } | |
| 86 | − if include != nil { | |
| 87 | − metadata[GeminiIncludeThoughtsMetadataKey] = *include | |
| 88 | − } | |
| 89 | − return baseModel, metadata | |
| 90 | −} | |
| 91 | − | |
| 92 | 16 | func ApplyGeminiThinkingConfig(body []byte, budget *int, includeThoughts *bool) []byte { |
| 93 | 17 | if budget == nil && includeThoughts == nil { |
| 94 | 18 | return body |
| @@ -133,80 +57,6 @@ func ApplyGeminiCLIThinkingConfig(body []byte, budget *int, includeThoughts *boo · 38 unmodified lines | ||
| 133 | 57 | return updated |
| 134 | 58 | } |
| 135 | 59 | |
| 136 | −func GeminiThinkingFromMetadata(metadata map[string]any) (*int, *bool, bool) { | |
| 137 | − if len(metadata) == 0 { | |
| 138 | − return nil, nil, false | |
| 139 | − } | |
| 140 | − var ( | |
| 141 | − budgetPtr *int | |
| 142 | − includePtr *bool | |
| 143 | − matched bool | |
| 144 | − ) | |
| 145 | − if rawBudget, ok := metadata[GeminiThinkingBudgetMetadataKey]; ok { | |
| 146 | − switch v := rawBudget.(type) { | |
| 147 | − case int: | |
| 148 | − budget := v | |
| 149 | − budgetPtr = &budget | |
| 150 | − matched = true | |
| 151 | − case int32: | |
| 152 | − budget := int(v) | |
| 153 | − budgetPtr = &budget | |
| 154 | − matched = true | |
| 155 | − case int64: | |
| 156 | − budget := int(v) | |
| 157 | − budgetPtr = &budget | |
| 158 | − matched = true | |
| 159 | − case float64: | |
| 160 | − budget := int(v) | |
| 161 | − budgetPtr = &budget | |
| 162 | − matched = true | |
| 163 | − case json.Number: | |
| 164 | − if val, err := v.Int64(); err == nil { | |
| 165 | − budget := int(val) | |
| 166 | − budgetPtr = &budget | |
| 167 | − matched = true | |
| 168 | − } | |
| 169 | − } | |
| 170 | − } | |
| 171 | − if rawInclude, ok := metadata[GeminiIncludeThoughtsMetadataKey]; ok { | |
| 172 | − switch v := rawInclude.(type) { | |
| 173 | − case bool: | |
| 174 | − include := v | |
| 175 | − includePtr = &include | |
| 176 | − matched = true | |
| 177 | − case string: | |
| 178 | − if parsed, err := strconv.ParseBool(v); err == nil { | |
| 179 | − include := parsed | |
| 180 | − includePtr = &include | |
| 181 | − matched = true | |
| 182 | − } | |
| 183 | − case json.Number: | |
| 184 | − if val, err := v.Int64(); err == nil { | |
| 185 | − include := val != 0 | |
| 186 | − includePtr = &include | |
| 187 | − matched = true | |
| 188 | − } | |
| 189 | − case int: | |
| 190 | − include := v != 0 | |
| 191 | − includePtr = &include | |
| 192 | − matched = true | |
| 193 | − case int32: | |
| 194 | − include := v != 0 | |
| 195 | − includePtr = &include | |
| 196 | − matched = true | |
| 197 | − case int64: | |
| 198 | − include := v != 0 | |
| 199 | − includePtr = &include | |
| 200 | − matched = true | |
| 201 | − case float64: | |
| 202 | − include := v != 0 | |
| 203 | − includePtr = &include | |
| 204 | − matched = true | |
| 205 | − } | |
| 206 | − } | |
| 207 | − return budgetPtr, includePtr, matched | |
| 208 | −} | |
| 209 | − | |
| 210 | 60 | // modelsWithDefaultThinking lists models that should have thinking enabled by default |
| 211 | 61 | // when no explicit thinkingConfig is provided. |
| 212 | 62 | var modelsWithDefaultThinking = map[string]bool{ |
| @@ -0,0 +1,327 @@ | ||
| 1 | +package util | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "encoding/json" | |
| 5 | + "strconv" | |
| 6 | + "strings" | |
| 7 | +) | |
| 8 | + | |
| 9 | +const ( | |
| 10 | + ThinkingBudgetMetadataKey = "thinking_budget" | |
| 11 | + ThinkingIncludeThoughtsMetadataKey = "thinking_include_thoughts" | |
| 12 | + ReasoningEffortMetadataKey = "reasoning_effort" | |
| 13 | + ThinkingOriginalModelMetadataKey = "thinking_original_model" | |
| 14 | +) | |
| 15 | + | |
| 16 | +// NormalizeThinkingModel parses dynamic thinking suffixes on model names and returns | |
| 17 | +// the normalized base model with extracted metadata. Supported patterns: | |
| 18 | +// - "-thinking-<number>" extracts a numeric budget | |
| 19 | +// - "-thinking-<level>" extracts a reasoning effort level (minimal/low/medium/high/xhigh/auto/none) | |
| 20 | +// - "-thinking" maps to a default reasoning effort of "medium" | |
| 21 | +// - "-reasoning" maps to dynamic budget (-1) and include_thoughts=true | |
| 22 | +// - "-nothinking" maps to budget=0 and include_thoughts=false | |
| 23 | +func NormalizeThinkingModel(modelName string) (string, map[string]any) { | |
| 24 | + if modelName == "" { | |
| 25 | + return modelName, nil | |
| 26 | + } | |
| 27 | + | |
| 28 | + lower := strings.ToLower(modelName) | |
| 29 | + baseModel := modelName | |
| 30 | + | |
| 31 | + var ( | |
| 32 | + budgetOverride *int | |
| 33 | + includeThoughts *bool | |
| 34 | + reasoningEffort *string | |
| 35 | + matched bool | |
| 36 | + ) | |
| 37 | + | |
| 38 | + switch { | |
| 39 | + case strings.HasSuffix(lower, "-nothinking"): | |
| 40 | + baseModel = modelName[:len(modelName)-len("-nothinking")] | |
| 41 | + budget := 0 | |
| 42 | + include := false | |
| 43 | + budgetOverride = &budget | |
| 44 | + includeThoughts = &include | |
| 45 | + matched = true | |
| 46 | + case strings.HasSuffix(lower, "-reasoning"): | |
| 47 | + baseModel = modelName[:len(modelName)-len("-reasoning")] | |
| 48 | + budget := -1 | |
| 49 | + include := true | |
| 50 | + budgetOverride = &budget | |
| 51 | + includeThoughts = &include | |
| 52 | + matched = true | |
| 53 | + default: | |
| 54 | + if idx := strings.LastIndex(lower, "-thinking-"); idx != -1 { | |
| 55 | + value := modelName[idx+len("-thinking-"):] | |
| 56 | + if value != "" { | |
| 57 | + if parsed, ok := parseIntPrefix(value); ok { | |
| 58 | + baseModel = modelName[:idx] | |
| 59 | + budgetOverride = &parsed | |
| 60 | + matched = true | |
| 61 | + } else if effort, okEffort := normalizeReasoningEffort(value); okEffort { | |
| 62 | + baseModel = modelName[:idx] | |
| 63 | + reasoningEffort = &effort | |
| 64 | + matched = true | |
| 65 | + } | |
| 66 | + } | |
| 67 | + } else if strings.HasSuffix(lower, "-thinking") { | |
| 68 | + baseModel = modelName[:len(modelName)-len("-thinking")] | |
| 69 | + effort := "medium" | |
| 70 | + reasoningEffort = &effort | |
| 71 | + matched = true | |
| 72 | + } | |
| 73 | + } | |
| 74 | + | |
| 75 | + if !matched { | |
| 76 | + return baseModel, nil | |
| 77 | + } | |
| 78 | + | |
| 79 | + metadata := map[string]any{ | |
| 80 | + ThinkingOriginalModelMetadataKey: modelName, | |
| 81 | + } | |
| 82 | + if budgetOverride != nil { | |
| 83 | + metadata[ThinkingBudgetMetadataKey] = *budgetOverride | |
| 84 | + } | |
| 85 | + if includeThoughts != nil { | |
| 86 | + metadata[ThinkingIncludeThoughtsMetadataKey] = *includeThoughts | |
| 87 | + } | |
| 88 | + if reasoningEffort != nil { | |
| 89 | + metadata[ReasoningEffortMetadataKey] = *reasoningEffort | |
| 90 | + } | |
| 91 | + return baseModel, metadata | |
| 92 | +} | |
| 93 | + | |
| 94 | +// ThinkingFromMetadata extracts thinking overrides from metadata produced by NormalizeThinkingModel. | |
| 95 | +// It accepts both the new generic keys and legacy Gemini-specific keys. | |
| 96 | +func ThinkingFromMetadata(metadata map[string]any) (*int, *bool, *string, bool) { | |
| 97 | + if len(metadata) == 0 { | |
| 98 | + return nil, nil, nil, false | |
| 99 | + } | |
| 100 | + | |
| 101 | + var ( | |
| 102 | + budgetPtr *int | |
| 103 | + includePtr *bool | |
| 104 | + effortPtr *string | |
| 105 | + matched bool | |
| 106 | + ) | |
| 107 | + | |
| 108 | + readBudget := func(key string) { | |
| 109 | + if budgetPtr != nil { | |
| 110 | + return | |
| 111 | + } | |
| 112 | + if raw, ok := metadata[key]; ok { | |
| 113 | + if v, okNumber := parseNumberToInt(raw); okNumber { | |
| 114 | + budget := v | |
| 115 | + budgetPtr = &budget | |
| 116 | + matched = true | |
| 117 | + } | |
| 118 | + } | |
| 119 | + } | |
| 120 | + | |
| 121 | + readInclude := func(key string) { | |
| 122 | + if includePtr != nil { | |
| 123 | + return | |
| 124 | + } | |
| 125 | + if raw, ok := metadata[key]; ok { | |
| 126 | + switch v := raw.(type) { | |
| 127 | + case bool: | |
| 128 | + val := v | |
| 129 | + includePtr = &val | |
| 130 | + matched = true | |
| 131 | + case *bool: | |
| 132 | + if v != nil { | |
| 133 | + val := *v | |
| 134 | + includePtr = &val | |
| 135 | + matched = true | |
| 136 | + } | |
| 137 | + } | |
| 138 | + } | |
| 139 | + } | |
| 140 | + | |
| 141 | + readEffort := func(key string) { | |
| 142 | + if effortPtr != nil { | |
| 143 | + return | |
| 144 | + } | |
| 145 | + if raw, ok := metadata[key]; ok { | |
| 146 | + if val, okStr := raw.(string); okStr && strings.TrimSpace(val) != "" { | |
| 147 | + normalized := strings.ToLower(strings.TrimSpace(val)) | |
| 148 | + effortPtr = &normalized | |
| 149 | + matched = true | |
| 150 | + } | |
| 151 | + } | |
| 152 | + } | |
| 153 | + | |
| 154 | + readBudget(ThinkingBudgetMetadataKey) | |
| 155 | + readBudget(GeminiThinkingBudgetMetadataKey) | |
| 156 | + readInclude(ThinkingIncludeThoughtsMetadataKey) | |
| 157 | + readInclude(GeminiIncludeThoughtsMetadataKey) | |
| 158 | + readEffort(ReasoningEffortMetadataKey) | |
| 159 | + readEffort("reasoning.effort") | |
| 160 | + | |
| 161 | + return budgetPtr, includePtr, effortPtr, matched | |
| 162 | +} | |
| 163 | + | |
| 164 | +// ResolveThinkingConfigFromMetadata derives thinking budget/include overrides, | |
| 165 | +// converting reasoning effort strings into budgets when possible. | |
| 166 | +func ResolveThinkingConfigFromMetadata(model string, metadata map[string]any) (*int, *bool, bool) { | |
| 167 | + budget, include, effort, matched := ThinkingFromMetadata(metadata) | |
| 168 | + if !matched { | |
| 169 | + return nil, nil, false | |
| 170 | + } | |
| 171 | + | |
| 172 | + if budget == nil && effort != nil { | |
| 173 | + if derived, ok := ThinkingEffortToBudget(model, *effort); ok { | |
| 174 | + budget = &derived | |
| 175 | + } | |
| 176 | + } | |
| 177 | + return budget, include, budget != nil || include != nil || effort != nil | |
| 178 | +} | |
| 179 | + | |
| 180 | +// ReasoningEffortFromMetadata resolves a reasoning effort string from metadata, | |
| 181 | +// inferring "auto" and "none" when budgets request dynamic or disabled thinking. | |
| 182 | +func ReasoningEffortFromMetadata(metadata map[string]any) (string, bool) { | |
| 183 | + budget, include, effort, matched := ThinkingFromMetadata(metadata) | |
| 184 | + if !matched { | |
| 185 | + return "", false | |
| 186 | + } | |
| 187 | + if effort != nil && *effort != "" { | |
| 188 | + return *effort, true | |
| 189 | + } | |
| 190 | + if budget != nil { | |
| 191 | + switch *budget { | |
| 192 | + case -1: | |
| 193 | + return "auto", true | |
| 194 | + case 0: | |
| 195 | + return "none", true | |
| 196 | + } | |
| 197 | + } | |
| 198 | + if include != nil && !*include { | |
| 199 | + return "none", true | |
| 200 | + } | |
| 201 | + return "", true | |
| 202 | +} | |
| 203 | + | |
| 204 | +// ThinkingEffortToBudget maps reasoning effort levels to approximate budgets, | |
| 205 | +// clamping the result to the model's supported range. | |
| 206 | +func ThinkingEffortToBudget(model, effort string) (int, bool) { | |
| 207 | + if effort == "" { | |
| 208 | + return 0, false | |
| 209 | + } | |
| 210 | + switch strings.ToLower(effort) { | |
| 211 | + case "none": | |
| 212 | + return 0, true | |
| 213 | + case "auto": | |
| 214 | + return NormalizeThinkingBudget(model, -1), true | |
| 215 | + case "minimal": | |
| 216 | + return NormalizeThinkingBudget(model, 512), true | |
| 217 | + case "low": | |
| 218 | + return NormalizeThinkingBudget(model, 1024), true | |
| 219 | + case "medium": | |
| 220 | + return NormalizeThinkingBudget(model, 8192), true | |
| 221 | + case "high": | |
| 222 | + return NormalizeThinkingBudget(model, 24576), true | |
| 223 | + case "xhigh": | |
| 224 | + return NormalizeThinkingBudget(model, 32768), true | |
| 225 | + default: | |
| 226 | + return 0, false | |
| 227 | + } | |
| 228 | +} | |
| 229 | + | |
| 230 | +// ResolveOriginalModel returns the original model name stored in metadata (if present), | |
| 231 | +// otherwise falls back to the provided model. | |
| 232 | +func ResolveOriginalModel(model string, metadata map[string]any) string { | |
| 233 | + normalize := func(name string) string { | |
| 234 | + if name == "" { | |
| 235 | + return "" | |
| 236 | + } | |
| 237 | + if base, _ := NormalizeThinkingModel(name); base != "" { | |
| 238 | + return base | |
| 239 | + } | |
| 240 | + return strings.TrimSpace(name) | |
| 241 | + } | |
| 242 | + | |
| 243 | + if metadata != nil { | |
| 244 | + if v, ok := metadata[ThinkingOriginalModelMetadataKey]; ok { | |
| 245 | + if s, okStr := v.(string); okStr && strings.TrimSpace(s) != "" { | |
| 246 | + if base := normalize(s); base != "" { | |
| 247 | + return base | |
| 248 | + } | |
| 249 | + } | |
| 250 | + } | |
| 251 | + if v, ok := metadata[GeminiOriginalModelMetadataKey]; ok { | |
| 252 | + if s, okStr := v.(string); okStr && strings.TrimSpace(s) != "" { | |
| 253 | + if base := normalize(s); base != "" { | |
| 254 | + return base | |
| 255 | + } | |
| 256 | + } | |
| 257 | + } | |
| 258 | + } | |
| 259 | + // Fallback: try to re-normalize the model name when metadata was dropped. | |
| 260 | + if base := normalize(model); base != "" { | |
| 261 | + return base | |
| 262 | + } | |
| 263 | + return model | |
| 264 | +} | |
| 265 | + | |
| 266 | +func parseIntPrefix(value string) (int, bool) { | |
| 267 | + if value == "" { | |
| 268 | + return 0, false | |
| 269 | + } | |
| 270 | + digits := strings.TrimLeft(value, "-") | |
| 271 | + if digits == "" { | |
| 272 | + return 0, false | |
| 273 | + } | |
| 274 | + end := len(digits) | |
| 275 | + for i := 0; i < len(digits); i++ { | |
| 276 | + if digits[i] < '0' || digits[i] > '9' { | |
| 277 | + end = i | |
| 278 | + break | |
| 279 | + } | |
| 280 | + } | |
| 281 | + if end == 0 { | |
| 282 | + return 0, false | |
| 283 | + } | |
| 284 | + val, err := strconv.Atoi(digits[:end]) | |
| 285 | + if err != nil { | |
| 286 | + return 0, false | |
| 287 | + } | |
| 288 | + return val, true | |
| 289 | +} | |
| 290 | + | |
| 291 | +func parseNumberToInt(raw any) (int, bool) { | |
| 292 | + switch v := raw.(type) { | |
| 293 | + case int: | |
| 294 | + return v, true | |
| 295 | + case int32: | |
| 296 | + return int(v), true | |
| 297 | + case int64: | |
| 298 | + return int(v), true | |
| 299 | + case float64: | |
| 300 | + return int(v), true | |
| 301 | + case json.Number: | |
| 302 | + if val, err := v.Int64(); err == nil { | |
| 303 | + return int(val), true | |
| 304 | + } | |
| 305 | + case string: | |
| 306 | + if strings.TrimSpace(v) == "" { | |
| 307 | + return 0, false | |
| 308 | + } | |
| 309 | + if parsed, err := strconv.Atoi(strings.TrimSpace(v)); err == nil { | |
| 310 | + return parsed, true | |
| 311 | + } | |
| 312 | + } | |
| 313 | + return 0, false | |
| 314 | +} | |
| 315 | + | |
| 316 | +func normalizeReasoningEffort(value string) (string, bool) { | |
| 317 | + if value == "" { | |
| 318 | + return "", false | |
| 319 | + } | |
| 320 | + effort := strings.ToLower(strings.TrimSpace(value)) | |
| 321 | + switch effort { | |
| 322 | + case "minimal", "low", "medium", "high", "xhigh", "auto", "none": | |
| 323 | + return effort, true | |
| 324 | + default: | |
| 325 | + return "", false | |
| 326 | + } | |
| 327 | +} | |
| @@ -343,18 +343,32 @@ func (h *BaseAPIHandler) getRequestDetails(modelName string) (providers []string · 342 unmodified lines | ||
| 343 | 343 | |
| 344 | 344 | providerName, extractedModelName, isDynamic := h.parseDynamicModel(resolvedModelName) |
| 345 | 345 | |
| 346 | − // First, normalize the model name to handle suffixes like "-thinking-128" | |
| 347 | − // This needs to happen before determining the provider for non-dynamic models. | |
| 348 | − normalizedModel, metadata = normalizeModelMetadata(resolvedModelName) | |
| 346 | + targetModelName := resolvedModelName | |
| 347 | + if isDynamic { | |
| 348 | + targetModelName = extractedModelName | |
| 349 | + } | |
| 350 | + | |
| 351 | + // Normalize the model name to handle dynamic thinking suffixes before determining the provider. | |
| 352 | + normalizedModel, metadata = normalizeModelMetadata(targetModelName) | |
| 349 | 353 | |
| 350 | 354 | if isDynamic { |
| 351 | 355 | providers = []string{providerName} |
| 352 | − // For dynamic models, the extractedModelName is already normalized by parseDynamicModel | |
| 353 | − // so we use it as the final normalizedModel. | |
| 354 | − normalizedModel = extractedModelName | |
| 355 | 356 | } else { |
| 356 | 357 | // For non-dynamic models, use the normalizedModel to get the provider name. |
| 357 | 358 | providers = util.GetProviderName(normalizedModel) |
| 359 | + if len(providers) == 0 && metadata != nil { | |
| 360 | + if originalRaw, ok := metadata[util.ThinkingOriginalModelMetadataKey]; ok { | |
| 361 | + if originalModel, okStr := originalRaw.(string); okStr { | |
| 362 | + originalModel = strings.TrimSpace(originalModel) | |
| 363 | + if originalModel != "" && !strings.EqualFold(originalModel, normalizedModel) { | |
| 364 | + if altProviders := util.GetProviderName(originalModel); len(altProviders) > 0 { | |
| 365 | + providers = altProviders | |
| 366 | + normalizedModel = originalModel | |
| 367 | + } | |
| 368 | + } | |
| 369 | + } | |
| 370 | + } | |
| 371 | + } | |
| 358 | 372 | } |
| 359 | 373 | |
| 360 | 374 | if len(providers) == 0 { |
| @@ -402,7 +416,7 @@ func cloneBytes(src []byte) []byte { · 41 unmodified lines | ||
| 402 | 416 | } |
| 403 | 417 | |
| 404 | 418 | func normalizeModelMetadata(modelName string) (string, map[string]any) { |
| 405 | − return util.NormalizeGeminiThinkingModel(modelName) | |
| 419 | + return util.NormalizeThinkingModel(modelName) | |
| 406 | 420 | } |
| 407 | 421 | |
| 408 | 422 | func cloneMetadata(src map[string]any) map[string]any { |