Unified · Split
| @@ -133,8 +133,8 @@ func (fh *FallbackHandler) WrapHandler(handler gin.HandlerFunc) gin.HandlerFunc · 132 unmodified lines | |||
| 133 | return | 133 | return |
| 134 | } | 134 | } |
| 135 | 135 | ||
| 136 | // Normalize model (handles Gemini thinking suffixes) | 136 | // Normalize model (handles dynamic thinking suffixes) |
| 137 | normalizedModel, _ := util.NormalizeGeminiThinkingModel(modelName) | 137 | normalizedModel, _ := util.NormalizeThinkingModel(modelName) |
| 138 | 138 | ||
| 139 | // Track resolved model for logging (may change if mapping is applied) | 139 | // Track resolved model for logging (may change if mapping is applied) |
| 140 | resolvedModel := normalizedModel | 140 | resolvedModel := normalizedModel |
| @@ -26,60 +26,6 @@ func GetClaudeModels() []*ModelInfo { · 25 unmodified lines | |||
| 26 | DisplayName: "Claude 4.5 Sonnet", | 26 | DisplayName: "Claude 4.5 Sonnet", |
| 27 | ContextLength: 200000, | 27 | ContextLength: 200000, |
| 28 | MaxCompletionTokens: 64000, | 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 | Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true}, | 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 | Description: "Premium model combining maximum intelligence with practical performance", | 38 | Description: "Premium model combining maximum intelligence with practical performance", |
| 93 | ContextLength: 200000, | 39 | ContextLength: 200000, |
| 94 | MaxCompletionTokens: 64000, | 40 | MaxCompletionTokens: 64000, |
| 41 | Thinking: &ThinkingSupport{Min: 1024, Max: 100000, ZeroAllowed: false, DynamicAllowed: true}, | ||
| 95 | }, | 42 | }, |
| 96 | { | 43 | { |
| 97 | ID: "claude-opus-4-1-20250805", | 44 | ID: "claude-opus-4-1-20250805", |
| @@ -530,58 +477,6 @@ func GetOpenAIModels() []*ModelInfo { · 432 unmodified lines | |||
| 530 | MaxCompletionTokens: 128000, | 477 | MaxCompletionTokens: 128000, |
| 531 | SupportedParameters: []string{"tools"}, | 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 | ID: "gpt-5-codex", | 481 | ID: "gpt-5-codex", |
| 587 | Object: "model", | 482 | Object: "model", |
| @@ -595,45 +490,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines | |||
| 595 | MaxCompletionTokens: 128000, | 490 | MaxCompletionTokens: 128000, |
| 596 | SupportedParameters: []string{"tools"}, | 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 | ID: "gpt-5-codex-mini", | 494 | ID: "gpt-5-codex-mini", |
| 639 | Object: "model", | 495 | Object: "model", |
| @@ -647,32 +503,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines | |||
| 647 | MaxCompletionTokens: 128000, | 503 | MaxCompletionTokens: 128000, |
| 648 | SupportedParameters: []string{"tools"}, | 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 | ID: "gpt-5.1", | 507 | ID: "gpt-5.1", |
| 678 | Object: "model", | 508 | Object: "model", |
| @@ -686,58 +516,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines | |||
| 686 | MaxCompletionTokens: 128000, | 516 | MaxCompletionTokens: 128000, |
| 687 | SupportedParameters: []string{"tools"}, | 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 | ID: "gpt-5.1-codex", | 520 | ID: "gpt-5.1-codex", |
| 743 | Object: "model", | 521 | Object: "model", |
| @@ -751,45 +529,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines | |||
| 751 | MaxCompletionTokens: 128000, | 529 | MaxCompletionTokens: 128000, |
| 752 | SupportedParameters: []string{"tools"}, | 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 | ID: "gpt-5.1-codex-mini", | 533 | ID: "gpt-5.1-codex-mini", |
| 795 | Object: "model", | 534 | Object: "model", |
| @@ -803,33 +542,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines | |||
| 803 | MaxCompletionTokens: 128000, | 542 | MaxCompletionTokens: 128000, |
| 804 | SupportedParameters: []string{"tools"}, | 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 | ID: "gpt-5.1-codex-max", | 546 | ID: "gpt-5.1-codex-max", |
| 835 | Object: "model", | 547 | Object: "model", |
| @@ -843,58 +555,6 @@ func GetOpenAIModels() []*ModelInfo { · 7 unmodified lines | |||
| 843 | MaxCompletionTokens: 128000, | 555 | MaxCompletionTokens: 128000, |
| 844 | SupportedParameters: []string{"tools"}, | 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 | // Use streaming translation to preserve function calling, except for claude. | 54 | // Use streaming translation to preserve function calling, except for claude. |
| 55 | stream := from != to | 55 | stream := from != to |
| 56 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream) | 56 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream) |
| 57 | modelForUpstream := req.Model | 57 | upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) |
| 58 | if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" { | 58 | if upstreamModel == "" { |
| 59 | body, _ = sjson.SetBytes(body, "model", modelOverride) | 59 | upstreamModel = req.Model |
| 60 | modelForUpstream = modelOverride | 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 | 68 | body, _ = sjson.SetBytes(body, "model", upstreamModel) |
| 63 | body = e.injectThinkingConfig(req.Model, body) | 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 | body = checkSystemInstructions(body) | 73 | body = checkSystemInstructions(body) |
| 67 | } | 74 | } |
| 68 | body = applyPayloadConfig(e.cfg, req.Model, body) | 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 | from := opts.SourceFormat | 168 | from := opts.SourceFormat |
| 162 | to := sdktranslator.FromString("claude") | 169 | to := sdktranslator.FromString("claude") |
| 163 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) | 170 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) |
| 164 | if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" { | 171 | upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) |
| 165 | body, _ = sjson.SetBytes(body, "model", modelOverride) | 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 | 182 | body, _ = sjson.SetBytes(body, "model", upstreamModel) |
| 168 | body = e.injectThinkingConfig(req.Model, body) | 183 | // Inject thinking config based on model metadata for thinking variants |
| 184 | body = e.injectThinkingConfig(req.Model, req.Metadata, body) | ||
| 169 | body = checkSystemInstructions(body) | 185 | body = checkSystemInstructions(body) |
| 170 | body = applyPayloadConfig(e.cfg, req.Model, body) | 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 | // Use streaming translation to preserve function calling, except for claude. | 311 | // Use streaming translation to preserve function calling, except for claude. |
| 296 | stream := from != to | 312 | stream := from != to |
| 297 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream) | 313 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream) |
| 298 | modelForUpstream := req.Model | 314 | upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) |
| 299 | if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" { | 315 | if upstreamModel == "" { |
| 300 | body, _ = sjson.SetBytes(body, "model", modelOverride) | 316 | upstreamModel = req.Model |
| 301 | modelForUpstream = modelOverride | 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 | body = checkSystemInstructions(body) | 328 | body = checkSystemInstructions(body) |
| 306 | } | 329 | } |
| 307 | 330 | ||
| @@ -427,25 +450,15 @@ func extractAndRemoveBetas(body []byte) ([]string, []byte) { · 119 unmodified lines | |||
| 427 | return betas, body | 450 | return betas, body |
| 428 | } | 451 | } |
| 429 | 452 | ||
| 430 | // injectThinkingConfig adds thinking configuration based on model name suffix | 453 | // injectThinkingConfig adds thinking configuration based on metadata or legacy suffixes. |
| 431 | func (e *ClaudeExecutor) injectThinkingConfig(modelName string, body []byte) []byte { | 454 | func (e *ClaudeExecutor) injectThinkingConfig(modelName string, metadata map[string]any, body []byte) []byte { |
| 432 | // Only inject if thinking config is not already present | 455 | // Only inject if thinking config is not already present |
| 433 | if gjson.GetBytes(body, "thinking").Exists() { | 456 | if gjson.GetBytes(body, "thinking").Exists() { |
| 434 | return body | 457 | return body |
| 435 | } | 458 | } |
| 436 | 459 | ||
| 437 | var budgetTokens int | 460 | budgetTokens, ok := resolveClaudeThinkingBudget(modelName, metadata) |
| 438 | switch { | 461 | if !ok || budgetTokens <= 0 { |
| 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: | ||
| 449 | return body | 462 | return body |
| 450 | } | 463 | } |
| 451 | 464 | ||
| @@ -454,6 +467,44 @@ func (e *ClaudeExecutor) injectThinkingConfig(modelName string, body []byte) []b · 2 unmodified lines | |||
| 454 | return body | 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 | // ensureMaxTokensForThinking ensures max_tokens > thinking.budget_tokens when thinking is enabled. | 508 | // ensureMaxTokensForThinking ensures max_tokens > thinking.budget_tokens when thinking is enabled. |
| 458 | // Anthropic API requires this constraint; violating it returns a 400 error. | 509 | // Anthropic API requires this constraint; violating it returns a 400 error. |
| 459 | // This function should be called after all thinking configuration is finalized. | 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 | func (e *ClaudeExecutor) resolveUpstreamModel(alias string, auth *cliproxyauth.Auth) string { | 544 | func (e *ClaudeExecutor) resolveUpstreamModel(alias string, auth *cliproxyauth.Auth) string { |
| 494 | if alias == "" { | 545 | trimmed := strings.TrimSpace(alias) |
| 546 | if trimmed == "" { | ||
| 495 | return "" | 547 | return "" |
| 496 | } | 548 | } |
| 497 | // Hardcoded mappings for thinking models to actual Claude model names | 549 | |
| 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 | } | ||
| 504 | entry := e.resolveClaudeConfig(auth) | 550 | entry := e.resolveClaudeConfig(auth) |
| 505 | if entry == nil { | 551 | if entry == nil { |
| 506 | return "" | 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 | for i := range entry.Models { | 566 | for i := range entry.Models { |
| 509 | model := entry.Models[i] | 567 | model := entry.Models[i] |
| 510 | name := strings.TrimSpace(model.Name) | 568 | name := strings.TrimSpace(model.Name) |
| 511 | modelAlias := strings.TrimSpace(model.Alias) | 569 | modelAlias := strings.TrimSpace(model.Alias) |
| 512 | if modelAlias != "" { | 570 | |
| 513 | if strings.EqualFold(modelAlias, alias) { | 571 | for _, candidate := range candidates { |
| 572 | if candidate == "" { | ||
| 573 | continue | ||
| 574 | } | ||
| 575 | if modelAlias != "" && strings.EqualFold(modelAlias, candidate) { | ||
| 514 | if name != "" { | 576 | if name != "" { |
| 515 | return name | 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 | return "" | 586 | return "" |
| @@ -49,14 +49,14 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re · 48 unmodified lines | |||
| 49 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) | 49 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 50 | defer reporter.trackFailure(ctx, &err) | 50 | defer reporter.trackFailure(ctx, &err) |
| 51 | 51 | ||
| 52 | upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | ||
| 53 | |||
| 52 | from := opts.SourceFormat | 54 | from := opts.SourceFormat |
| 53 | to := sdktranslator.FromString("codex") | 55 | to := sdktranslator.FromString("codex") |
| 54 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) | 56 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) |
| 55 | 57 | body = applyReasoningEffortMetadata(body, req.Metadata, req.Model) | |
| 56 | body = e.setReasoningEffortByAlias(req.Model, body) | ||
| 57 | |||
| 58 | body = applyPayloadConfig(e.cfg, req.Model, body) | 58 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 59 | 59 | body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 60 | body, _ = sjson.SetBytes(body, "stream", true) | 60 | body, _ = sjson.SetBytes(body, "stream", true) |
| 61 | body, _ = sjson.DeleteBytes(body, "previous_response_id") | 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 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) | 142 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 143 | defer reporter.trackFailure(ctx, &err) | 143 | defer reporter.trackFailure(ctx, &err) |
| 144 | 144 | ||
| 145 | upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | ||
| 146 | |||
| 145 | from := opts.SourceFormat | 147 | from := opts.SourceFormat |
| 146 | to := sdktranslator.FromString("codex") | 148 | to := sdktranslator.FromString("codex") |
| 147 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) | 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 | body = applyPayloadConfig(e.cfg, req.Model, body) | 152 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 151 | body, _ = sjson.DeleteBytes(body, "previous_response_id") | 153 | body, _ = sjson.DeleteBytes(body, "previous_response_id") |
| 154 | body, _ = sjson.SetBytes(body, "model", upstreamModel) | ||
| 152 | 155 | ||
| 153 | url := strings.TrimSuffix(baseURL, "/") + "/responses" | 156 | url := strings.TrimSuffix(baseURL, "/") + "/responses" |
| 154 | httpReq, err := e.cacheHelper(ctx, from, url, req, body) | 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 | func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { | 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 | from := opts.SourceFormat | 243 | from := opts.SourceFormat |
| 239 | to := sdktranslator.FromString("codex") | 244 | to := sdktranslator.FromString("codex") |
| 240 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) | 245 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) |
| 241 | 246 | ||
| 242 | modelForCounting := req.Model | 247 | modelForCounting := req.Model |
| 243 | 248 | ||
| 244 | body = e.setReasoningEffortByAlias(req.Model, body) | 249 | body = applyReasoningEffortMetadata(body, req.Metadata, req.Model) |
| 245 | 250 | body, _ = sjson.SetBytes(body, "model", upstreamModel) | |
| 246 | body, _ = sjson.DeleteBytes(body, "previous_response_id") | 251 | body, _ = sjson.DeleteBytes(body, "previous_response_id") |
| 247 | body, _ = sjson.SetBytes(body, "stream", false) | 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 | return cliproxyexecutor.Response{Payload: []byte(translated)}, nil | 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 | func tokenizerForCodexModel(model string) (tokenizer.Codec, error) { | 269 | func tokenizerForCodexModel(model string) (tokenizer.Codec, error) { |
| 342 | sanitized := strings.ToLower(strings.TrimSpace(model)) | 270 | sanitized := strings.ToLower(strings.TrimSpace(model)) |
| 343 | switch { | 271 | switch { |
| @@ -75,6 +75,8 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r · 74 unmodified lines | |||
| 75 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) | 75 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 76 | defer reporter.trackFailure(ctx, &err) | 76 | defer reporter.trackFailure(ctx, &err) |
| 77 | 77 | ||
| 78 | upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | ||
| 79 | |||
| 78 | // Official Gemini API via API key or OAuth bearer | 80 | // Official Gemini API via API key or OAuth bearer |
| 79 | from := opts.SourceFormat | 81 | from := opts.SourceFormat |
| 80 | to := sdktranslator.FromString("gemini") | 82 | to := sdktranslator.FromString("gemini") |
| @@ -85,6 +87,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r · 4 unmodified lines | |||
| 85 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) | 87 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) |
| 86 | body = fixGeminiImageAspectRatio(req.Model, body) | 88 | body = fixGeminiImageAspectRatio(req.Model, body) |
| 87 | body = applyPayloadConfig(e.cfg, req.Model, body) | 89 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 90 | body, _ = sjson.SetBytes(body, "model", upstreamModel) | ||
| 88 | 91 | ||
| 89 | action := "generateContent" | 92 | action := "generateContent" |
| 90 | if req.Metadata != nil { | 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 | baseURL := resolveGeminiBaseURL(auth) | 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 | if opts.Alt != "" && action != "countTokens" { | 100 | if opts.Alt != "" && action != "countTokens" { |
| 98 | url = url + fmt.Sprintf("?$alt=%s", opts.Alt) | 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 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) | 170 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 168 | defer reporter.trackFailure(ctx, &err) | 171 | defer reporter.trackFailure(ctx, &err) |
| 169 | 172 | ||
| 173 | upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | ||
| 174 | |||
| 170 | from := opts.SourceFormat | 175 | from := opts.SourceFormat |
| 171 | to := sdktranslator.FromString("gemini") | 176 | to := sdktranslator.FromString("gemini") |
| 172 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) | 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 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) | 181 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) |
| 177 | body = fixGeminiImageAspectRatio(req.Model, body) | 182 | body = fixGeminiImageAspectRatio(req.Model, body) |
| 178 | body = applyPayloadConfig(e.cfg, req.Model, body) | 183 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 184 | body, _ = sjson.SetBytes(body, "model", upstreamModel) | ||
| 179 | 185 | ||
| 180 | baseURL := resolveGeminiBaseURL(auth) | 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 | if opts.Alt == "" { | 188 | if opts.Alt == "" { |
| 183 | url = url + "?alt=sse" | 189 | url = url + "?alt=sse" |
| 184 | } else { | 190 | } else { |
| @@ -105,10 +105,12 @@ func (e *GeminiVertexExecutor) CountTokens(ctx context.Context, auth *cliproxyau · 104 unmodified lines | |||
| 105 | 105 | ||
| 106 | // countTokensWithServiceAccount handles token counting using service account credentials. | 106 | // countTokensWithServiceAccount handles token counting using service account credentials. |
| 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) { | 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 | from := opts.SourceFormat | 110 | from := opts.SourceFormat |
| 109 | to := sdktranslator.FromString("gemini") | 111 | to := sdktranslator.FromString("gemini") |
| 110 | translatedReq := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) | 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 | if budgetOverride != nil { | 114 | if budgetOverride != nil { |
| 113 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) | 115 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) |
| 114 | budgetOverride = &norm | 116 | budgetOverride = &norm |
| @@ -117,13 +119,14 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context · 2 unmodified lines | |||
| 117 | } | 119 | } |
| 118 | translatedReq = util.StripThinkingConfigIfUnsupported(req.Model, translatedReq) | 120 | translatedReq = util.StripThinkingConfigIfUnsupported(req.Model, translatedReq) |
| 119 | translatedReq = fixGeminiImageAspectRatio(req.Model, translatedReq) | 121 | translatedReq = fixGeminiImageAspectRatio(req.Model, translatedReq) |
| 122 | translatedReq, _ = sjson.SetBytes(translatedReq, "model", upstreamModel) | ||
| 120 | respCtx := context.WithValue(ctx, "alt", opts.Alt) | 123 | respCtx := context.WithValue(ctx, "alt", opts.Alt) |
| 121 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools") | 124 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools") |
| 122 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig") | 125 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig") |
| 123 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "safetySettings") | 126 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "safetySettings") |
| 124 | 127 | ||
| 125 | baseURL := vertexBaseURL(location) | 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 | httpReq, errNewReq := http.NewRequestWithContext(respCtx, http.MethodPost, url, bytes.NewReader(translatedReq)) | 131 | httpReq, errNewReq := http.NewRequestWithContext(respCtx, http.MethodPost, url, bytes.NewReader(translatedReq)) |
| 129 | if errNewReq != nil { | 132 | if errNewReq != nil { |
| @@ -191,10 +194,12 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context · 61 unmodified lines | |||
| 191 | 194 | ||
| 192 | // countTokensWithAPIKey handles token counting using API key credentials. | 195 | // countTokensWithAPIKey handles token counting using API key credentials. |
| 193 | func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, apiKey, baseURL string) (cliproxyexecutor.Response, error) { | 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 | from := opts.SourceFormat | 199 | from := opts.SourceFormat |
| 195 | to := sdktranslator.FromString("gemini") | 200 | to := sdktranslator.FromString("gemini") |
| 196 | translatedReq := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) | 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 | if budgetOverride != nil { | 203 | if budgetOverride != nil { |
| 199 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) | 204 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) |
| 200 | budgetOverride = &norm | 205 | budgetOverride = &norm |
| @@ -203,6 +208,7 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth * · 2 unmodified lines | |||
| 203 | } | 208 | } |
| 204 | translatedReq = util.StripThinkingConfigIfUnsupported(req.Model, translatedReq) | 209 | translatedReq = util.StripThinkingConfigIfUnsupported(req.Model, translatedReq) |
| 205 | translatedReq = fixGeminiImageAspectRatio(req.Model, translatedReq) | 210 | translatedReq = fixGeminiImageAspectRatio(req.Model, translatedReq) |
| 211 | translatedReq, _ = sjson.SetBytes(translatedReq, "model", upstreamModel) | ||
| 206 | respCtx := context.WithValue(ctx, "alt", opts.Alt) | 212 | respCtx := context.WithValue(ctx, "alt", opts.Alt) |
| 207 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools") | 213 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools") |
| 208 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig") | 214 | translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig") |
| @@ -286,10 +292,12 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au · 77 unmodified lines | |||
| 286 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) | 292 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 287 | defer reporter.trackFailure(ctx, &err) | 293 | defer reporter.trackFailure(ctx, &err) |
| 288 | 294 | ||
| 295 | upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | ||
| 296 | |||
| 289 | from := opts.SourceFormat | 297 | from := opts.SourceFormat |
| 290 | to := sdktranslator.FromString("gemini") | 298 | to := sdktranslator.FromString("gemini") |
| 291 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) | 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 | if budgetOverride != nil { | 301 | if budgetOverride != nil { |
| 294 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) | 302 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) |
| 295 | budgetOverride = &norm | 303 | budgetOverride = &norm |
| @@ -301,6 +309,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au · 5 unmodified lines | |||
| 301 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) | 309 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) |
| 302 | body = fixGeminiImageAspectRatio(req.Model, body) | 310 | body = fixGeminiImageAspectRatio(req.Model, body) |
| 303 | body = applyPayloadConfig(e.cfg, req.Model, body) | 311 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 312 | body, _ = sjson.SetBytes(body, "model", upstreamModel) | ||
| 304 | 313 | ||
| 305 | action := "generateContent" | 314 | action := "generateContent" |
| 306 | if req.Metadata != nil { | 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 | baseURL := vertexBaseURL(location) | 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 | if opts.Alt != "" && action != "countTokens" { | 322 | if opts.Alt != "" && action != "countTokens" { |
| 314 | url = url + fmt.Sprintf("?$alt=%s", opts.Alt) | 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 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) | 392 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 384 | defer reporter.trackFailure(ctx, &err) | 393 | defer reporter.trackFailure(ctx, &err) |
| 385 | 394 | ||
| 395 | upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | ||
| 396 | |||
| 386 | from := opts.SourceFormat | 397 | from := opts.SourceFormat |
| 387 | to := sdktranslator.FromString("gemini") | 398 | to := sdktranslator.FromString("gemini") |
| 388 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) | 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 | if budgetOverride != nil { | 401 | if budgetOverride != nil { |
| 391 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) | 402 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) |
| 392 | budgetOverride = &norm | 403 | budgetOverride = &norm |
| @@ -398,6 +409,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip · 5 unmodified lines | |||
| 398 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) | 409 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) |
| 399 | body = fixGeminiImageAspectRatio(req.Model, body) | 410 | body = fixGeminiImageAspectRatio(req.Model, body) |
| 400 | body = applyPayloadConfig(e.cfg, req.Model, body) | 411 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 412 | body, _ = sjson.SetBytes(body, "model", upstreamModel) | ||
| 401 | 413 | ||
| 402 | action := "generateContent" | 414 | action := "generateContent" |
| 403 | if req.Metadata != nil { | 415 | if req.Metadata != nil { |
| @@ -410,7 +422,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip · 6 unmodified lines | |||
| 410 | if baseURL == "" { | 422 | if baseURL == "" { |
| 411 | baseURL = "https://generativelanguage.googleapis.com" | 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 | if opts.Alt != "" && action != "countTokens" { | 426 | if opts.Alt != "" && action != "countTokens" { |
| 415 | url = url + fmt.Sprintf("?$alt=%s", opts.Alt) | 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 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) | 493 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 482 | defer reporter.trackFailure(ctx, &err) | 494 | defer reporter.trackFailure(ctx, &err) |
| 483 | 495 | ||
| 496 | upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | ||
| 497 | |||
| 484 | from := opts.SourceFormat | 498 | from := opts.SourceFormat |
| 485 | to := sdktranslator.FromString("gemini") | 499 | to := sdktranslator.FromString("gemini") |
| 486 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) | 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 | if budgetOverride != nil { | 502 | if budgetOverride != nil { |
| 489 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) | 503 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) |
| 490 | budgetOverride = &norm | 504 | budgetOverride = &norm |
| @@ -496,9 +510,10 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte · 5 unmodified lines | |||
| 496 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) | 510 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) |
| 497 | body = fixGeminiImageAspectRatio(req.Model, body) | 511 | body = fixGeminiImageAspectRatio(req.Model, body) |
| 498 | body = applyPayloadConfig(e.cfg, req.Model, body) | 512 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 513 | body, _ = sjson.SetBytes(body, "model", upstreamModel) | ||
| 499 | 514 | ||
| 500 | baseURL := vertexBaseURL(location) | 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 | if opts.Alt == "" { | 517 | if opts.Alt == "" { |
| 503 | url = url + "?alt=sse" | 518 | url = url + "?alt=sse" |
| 504 | } else { | 519 | } else { |
| @@ -595,10 +610,12 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth · 90 unmodified lines | |||
| 595 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) | 610 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) |
| 596 | defer reporter.trackFailure(ctx, &err) | 611 | defer reporter.trackFailure(ctx, &err) |
| 597 | 612 | ||
| 613 | upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata) | ||
| 614 | |||
| 598 | from := opts.SourceFormat | 615 | from := opts.SourceFormat |
| 599 | to := sdktranslator.FromString("gemini") | 616 | to := sdktranslator.FromString("gemini") |
| 600 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) | 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 | if budgetOverride != nil { | 619 | if budgetOverride != nil { |
| 603 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) | 620 | norm := util.NormalizeThinkingBudget(req.Model, *budgetOverride) |
| 604 | budgetOverride = &norm | 621 | budgetOverride = &norm |
| @@ -610,12 +627,13 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth · 5 unmodified lines | |||
| 610 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) | 627 | body = util.StripThinkingConfigIfUnsupported(req.Model, body) |
| 611 | body = fixGeminiImageAspectRatio(req.Model, body) | 628 | body = fixGeminiImageAspectRatio(req.Model, body) |
| 612 | body = applyPayloadConfig(e.cfg, req.Model, body) | 629 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 630 | body, _ = sjson.SetBytes(body, "model", upstreamModel) | ||
| 613 | 631 | ||
| 614 | // For API key auth, use simpler URL format without project/location | 632 | // For API key auth, use simpler URL format without project/location |
| 615 | if baseURL == "" { | 633 | if baseURL == "" { |
| 616 | baseURL = "https://generativelanguage.googleapis.com" | 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 | if opts.Alt == "" { | 637 | if opts.Alt == "" { |
| 620 | url = url + "?alt=sse" | 638 | url = url + "?alt=sse" |
| 621 | } else { | 639 | } else { |
| @@ -57,6 +57,10 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re · 56 unmodified lines | |||
| 57 | from := opts.SourceFormat | 57 | from := opts.SourceFormat |
| 58 | to := sdktranslator.FromString("openai") | 58 | to := sdktranslator.FromString("openai") |
| 59 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) | 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 | body = applyPayloadConfig(e.cfg, req.Model, body) | 64 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 61 | 65 | ||
| 62 | endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint | 66 | endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint |
| @@ -139,6 +143,10 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au · 76 unmodified lines | |||
| 139 | to := sdktranslator.FromString("openai") | 143 | to := sdktranslator.FromString("openai") |
| 140 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) | 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 | // Ensure tools array exists to avoid provider quirks similar to Qwen's behaviour. | 150 | // Ensure tools array exists to avoid provider quirks similar to Qwen's behaviour. |
| 143 | toolsResult := gjson.GetBytes(body, "tools") | 151 | toolsResult := gjson.GetBytes(body, "tools") |
| 144 | if toolsResult.Exists() && toolsResult.IsArray() && len(toolsResult.Array()) == 0 { | 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 | translated = e.overrideModel(translated, modelOverride) | 58 | translated = e.overrideModel(translated, modelOverride) |
| 59 | } | 59 | } |
| 60 | translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated) | 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 | url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" | 66 | url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" |
| 63 | httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated)) | 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 | translated = e.overrideModel(translated, modelOverride) | 147 | translated = e.overrideModel(translated, modelOverride) |
| 144 | } | 148 | } |
| 145 | translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated) | 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 | url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" | 155 | url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" |
| 148 | httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated)) | 156 | httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated)) |
| @@ -12,8 +12,8 @@ import ( · 11 unmodified lines | |||
| 12 | // applyThinkingMetadata applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N) | 12 | // applyThinkingMetadata applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N) |
| 13 | // for standard Gemini format payloads. It normalizes the budget when the model supports thinking. | 13 | // for standard Gemini format payloads. It normalizes the budget when the model supports thinking. |
| 14 | func applyThinkingMetadata(payload []byte, metadata map[string]any, model string) []byte { | 14 | func applyThinkingMetadata(payload []byte, metadata map[string]any, model string) []byte { |
| 15 | budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(metadata) | 15 | budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata) |
| 16 | if !ok { | 16 | if !ok || (budgetOverride == nil && includeOverride == nil) { |
| 17 | return payload | 17 | return payload |
| 18 | } | 18 | } |
| 19 | if !util.ModelSupportsThinking(model) { | 19 | if !util.ModelSupportsThinking(model) { |
| @@ -29,17 +29,60 @@ func applyThinkingMetadata(payload []byte, metadata map[string]any, model string · 9 unmodified lines | |||
| 29 | // applyThinkingMetadataCLI applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N) | 29 | // applyThinkingMetadataCLI applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N) |
| 30 | // for Gemini CLI format payloads (nested under "request"). It normalizes the budget when the model supports thinking. | 30 | // for Gemini CLI format payloads (nested under "request"). It normalizes the budget when the model supports thinking. |
| 31 | func applyThinkingMetadataCLI(payload []byte, metadata map[string]any, model string) []byte { | 31 | func applyThinkingMetadataCLI(payload []byte, metadata map[string]any, model string) []byte { |
| 32 | budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(metadata) | 32 | budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata) |
| 33 | if !ok { | 33 | if !ok || (budgetOverride == nil && includeOverride == nil) { |
| 34 | return payload | 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 | norm := util.NormalizeThinkingBudget(model, *budgetOverride) | 40 | norm := util.NormalizeThinkingBudget(model, *budgetOverride) |
| 38 | budgetOverride = &norm | 41 | budgetOverride = &norm |
| 39 | } | 42 | } |
| 40 | return util.ApplyGeminiCLIThinkingConfig(payload, budgetOverride, includeOverride) | 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 | // applyPayloadConfig applies payload default and override rules from configuration | 86 | // applyPayloadConfig applies payload default and override rules from configuration |
| 44 | // to the given JSON payload for the specified model. | 87 | // to the given JSON payload for the specified model. |
| 45 | // Defaults only fill missing fields, while overrides always overwrite existing values. | 88 | // Defaults only fill missing fields, while overrides always overwrite existing values. |
| @@ -12,6 +12,7 @@ import ( · 11 unmodified lines | |||
| 12 | 12 | ||
| 13 | qwenauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen" | 13 | qwenauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen" |
| 14 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" | 14 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| 15 | "github.com/router-for-me/CLIProxyAPI/v6/internal/util" | ||
| 15 | cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" | 16 | cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" |
| 16 | cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" | 17 | cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" |
| 17 | sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" | 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 | from := opts.SourceFormat | 51 | from := opts.SourceFormat |
| 51 | to := sdktranslator.FromString("openai") | 52 | to := sdktranslator.FromString("openai") |
| 52 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) | 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 | body = applyPayloadConfig(e.cfg, req.Model, body) | 58 | body = applyPayloadConfig(e.cfg, req.Model, body) |
| 54 | 59 | ||
| 55 | url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" | 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 | to := sdktranslator.FromString("openai") | 126 | to := sdktranslator.FromString("openai") |
| 122 | body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) | 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 | toolsResult := gjson.GetBytes(body, "tools") | 133 | toolsResult := gjson.GetBytes(body, "tools") |
| 125 | // 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. | 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 | // This will have no real consequences. It's just to scare Qwen3. | 135 | // This will have no real consequences. It's just to scare Qwen3. |
| @@ -1,8 +1,6 @@ | |||
| 1 | package util | 1 | package util |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "encoding/json" | ||
| 5 | "strconv" | ||
| 6 | "strings" | 4 | "strings" |
| 7 | 5 | ||
| 8 | "github.com/tidwall/gjson" | 6 | "github.com/tidwall/gjson" |
| @@ -15,80 +13,6 @@ const ( · 6 unmodified lines | |||
| 15 | GeminiOriginalModelMetadataKey = "gemini_original_model" | 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 | func ApplyGeminiThinkingConfig(body []byte, budget *int, includeThoughts *bool) []byte { | 16 | func ApplyGeminiThinkingConfig(body []byte, budget *int, includeThoughts *bool) []byte { |
| 93 | if budget == nil && includeThoughts == nil { | 17 | if budget == nil && includeThoughts == nil { |
| 94 | return body | 18 | return body |
| @@ -133,80 +57,6 @@ func ApplyGeminiCLIThinkingConfig(body []byte, budget *int, includeThoughts *boo · 38 unmodified lines | |||
| 133 | return updated | 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 | // modelsWithDefaultThinking lists models that should have thinking enabled by default | 60 | // modelsWithDefaultThinking lists models that should have thinking enabled by default |
| 211 | // when no explicit thinkingConfig is provided. | 61 | // when no explicit thinkingConfig is provided. |
| 212 | var modelsWithDefaultThinking = map[string]bool{ | 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 | providerName, extractedModelName, isDynamic := h.parseDynamicModel(resolvedModelName) | 344 | providerName, extractedModelName, isDynamic := h.parseDynamicModel(resolvedModelName) |
| 345 | 345 | ||
| 346 | // First, normalize the model name to handle suffixes like "-thinking-128" | 346 | targetModelName := resolvedModelName |
| 347 | // This needs to happen before determining the provider for non-dynamic models. | 347 | if isDynamic { |
| 348 | normalizedModel, metadata = normalizeModelMetadata(resolvedModelName) | 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 | if isDynamic { | 354 | if isDynamic { |
| 351 | providers = []string{providerName} | 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 | } else { | 356 | } else { |
| 356 | // For non-dynamic models, use the normalizedModel to get the provider name. | 357 | // For non-dynamic models, use the normalizedModel to get the provider name. |
| 357 | providers = util.GetProviderName(normalizedModel) | 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 | if len(providers) == 0 { | 374 | if len(providers) == 0 { |
| @@ -402,7 +416,7 @@ func cloneBytes(src []byte) []byte { · 41 unmodified lines | |||
| 402 | } | 416 | } |
| 403 | 417 | ||
| 404 | func normalizeModelMetadata(modelName string) (string, map[string]any) { | 418 | func normalizeModelMetadata(modelName string) (string, map[string]any) { |
| 405 | return util.NormalizeGeminiThinkingModel(modelName) | 419 | return util.NormalizeThinkingModel(modelName) |
| 406 | } | 420 | } |
| 407 | 421 | ||
| 408 | func cloneMetadata(src map[string]any) map[string]any { | 422 | func cloneMetadata(src map[string]any) map[string]any { |