feat: configurable tool pricing, Sub2API channel, and alpha search billing
Add admin-configurable tool-call prices with cross-provider surcharge settlement, Sub2API channel support, /v1/alpha/search relay, and usage-log surcharge UI.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AlphaSearchRequest is the Codex standalone web search request.
|
||||
// RawBody preserves the original JSON so unknown fields are forwarded intact.
|
||||
type AlphaSearchRequest struct {
|
||||
Model string `json:"model"`
|
||||
Id string `json:"id,omitempty"`
|
||||
Stream *bool `json:"stream,omitempty"`
|
||||
RawBody json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
func (r *AlphaSearchRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
combineText := ""
|
||||
if len(r.RawBody) > 0 {
|
||||
combineText = string(r.RawBody)
|
||||
}
|
||||
return &types.TokenCountMeta{
|
||||
CombineText: combineText,
|
||||
TokenType: types.TokenTypeTokenizer,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *AlphaSearchRequest) IsStream(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *AlphaSearchRequest) SetModelName(modelName string) {
|
||||
if modelName != "" {
|
||||
r.Model = modelName
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,7 @@ const (
|
||||
advancedCustomEndpointPathOpenAIChat = "/v1/chat/completions"
|
||||
advancedCustomEndpointPathOpenAIResponses = "/v1/responses"
|
||||
advancedCustomEndpointPathOpenAIResponsesCompact = "/v1/responses/compact"
|
||||
advancedCustomEndpointPathOpenAIAlphaSearch = "/v1/alpha/search"
|
||||
advancedCustomEndpointPathClaudeMessages = "/v1/messages"
|
||||
advancedCustomEndpointPathJinaRerank = "/v1/rerank"
|
||||
advancedCustomEndpointPathImageGeneration = "/v1/images/generations"
|
||||
@@ -204,6 +205,8 @@ func advancedCustomEndpointTypeFromIncomingPath(incomingPath string) (constant.E
|
||||
return constant.EndpointTypeOpenAIResponse, true
|
||||
case advancedCustomEndpointPathOpenAIResponsesCompact:
|
||||
return constant.EndpointTypeOpenAIResponseCompact, true
|
||||
case advancedCustomEndpointPathOpenAIAlphaSearch:
|
||||
return constant.EndpointTypeOpenAIAlphaSearch, true
|
||||
case advancedCustomEndpointPathClaudeMessages:
|
||||
return constant.EndpointTypeAnthropic, true
|
||||
case advancedCustomEndpointPathJinaRerank:
|
||||
@@ -475,6 +478,12 @@ func validateAdvancedCustomUpstreamTarget(index int, upstreamPath string) error
|
||||
}
|
||||
|
||||
func validateAdvancedCustomConverterPath(index int, incomingPath string, converter string) error {
|
||||
if incomingPath == advancedCustomEndpointPathOpenAIAlphaSearch {
|
||||
if converter == advancedCustomConverterNone {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("advanced_custom.advanced_routes[%d].converter does not match incoming_path: %s", index, converter)
|
||||
}
|
||||
switch converter {
|
||||
case advancedCustomConverterNone:
|
||||
return nil
|
||||
|
||||
@@ -477,3 +477,45 @@ func TestAdvancedCustomSupportedEndpointTypesForModel(t *testing.T) {
|
||||
constant.EndpointTypeAnthropic,
|
||||
}, config.SupportedEndpointTypesForModel("other-model"))
|
||||
}
|
||||
|
||||
func TestAdvancedCustomValidateAlphaSearchConverterPath(t *testing.T) {
|
||||
valid := &AdvancedCustomConfig{
|
||||
Routes: []AdvancedCustomRoute{
|
||||
{
|
||||
IncomingPath: "/v1/alpha/search",
|
||||
UpstreamPath: "/v1/alpha/search",
|
||||
Converter: advancedCustomConverterNone,
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, valid.Validate())
|
||||
assert.Equal(t, []constant.EndpointType{
|
||||
constant.EndpointTypeOpenAIAlphaSearch,
|
||||
}, valid.SupportedEndpointTypesForModel("gpt-5.1"))
|
||||
|
||||
nonNoneConverters := []string{
|
||||
advancedCustomConverterClaudeMessagesToOpenAIChat,
|
||||
advancedCustomConverterOpenAIChatToClaudeMessages,
|
||||
advancedCustomConverterOpenAIChatToOpenAIResponses,
|
||||
advancedCustomConverterOpenAIResponsesToOpenAIChat,
|
||||
advancedCustomConverterOpenAIResponsesToGemini,
|
||||
advancedCustomConverterGeminiContentToOpenAIChat,
|
||||
advancedCustomConverterOpenAIChatToGeminiContent,
|
||||
}
|
||||
for _, converter := range nonNoneConverters {
|
||||
t.Run(converter, func(t *testing.T) {
|
||||
config := &AdvancedCustomConfig{
|
||||
Routes: []AdvancedCustomRoute{
|
||||
{
|
||||
IncomingPath: "/v1/alpha/search",
|
||||
UpstreamPath: "/v1/alpha/search",
|
||||
Converter: converter,
|
||||
},
|
||||
},
|
||||
}
|
||||
err := config.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "converter does not match incoming_path")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+9
-4
@@ -438,10 +438,15 @@ func (c *GeminiChatGenerationConfig) UnmarshalJSON(data []byte) error {
|
||||
type MediaResolution string
|
||||
|
||||
type GeminiChatCandidate struct {
|
||||
Content GeminiChatContent `json:"content"`
|
||||
FinishReason *string `json:"finishReason"`
|
||||
Index int64 `json:"index"`
|
||||
SafetyRatings []GeminiChatSafetyRating `json:"safetyRatings"`
|
||||
Content GeminiChatContent `json:"content"`
|
||||
FinishReason *string `json:"finishReason"`
|
||||
Index int64 `json:"index"`
|
||||
SafetyRatings []GeminiChatSafetyRating `json:"safetyRatings"`
|
||||
GroundingMetadata *GeminiGroundingMetadata `json:"groundingMetadata,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiGroundingMetadata struct {
|
||||
WebSearchQueries []string `json:"webSearchQueries,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiChatSafetyRating struct {
|
||||
|
||||
+8
-37
@@ -320,42 +320,6 @@ func (o *OpenAIResponsesResponse) GetOpenAIError() *types.OpenAIError {
|
||||
return GetOpenAIError(o.Error)
|
||||
}
|
||||
|
||||
func (o *OpenAIResponsesResponse) HasImageGenerationCall() bool {
|
||||
if len(o.Output) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, output := range o.Output {
|
||||
if output.Type == ResponsesOutputTypeImageGenerationCall {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (o *OpenAIResponsesResponse) GetQuality() string {
|
||||
if len(o.Output) == 0 {
|
||||
return ""
|
||||
}
|
||||
for _, output := range o.Output {
|
||||
if output.Type == ResponsesOutputTypeImageGenerationCall {
|
||||
return output.Quality
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (o *OpenAIResponsesResponse) GetSize() string {
|
||||
if len(o.Output) == 0 {
|
||||
return ""
|
||||
}
|
||||
for _, output := range o.Output {
|
||||
if output.Type == ResponsesOutputTypeImageGenerationCall {
|
||||
return output.Size
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type IncompleteDetails struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
@@ -368,6 +332,7 @@ type ResponsesOutput struct {
|
||||
Content []ResponsesOutputContent `json:"content"`
|
||||
Quality string `json:"quality"`
|
||||
Size string `json:"size"`
|
||||
Result string `json:"result,omitempty"`
|
||||
CallId string `json:"call_id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Arguments json.RawMessage `json:"arguments,omitempty"`
|
||||
@@ -399,11 +364,17 @@ type ResponsesReasoningSummaryPart struct {
|
||||
|
||||
const (
|
||||
BuildInToolWebSearchPreview = "web_search_preview"
|
||||
BuildInToolWebSearch = "web_search"
|
||||
BuildInToolFileSearch = "file_search"
|
||||
BuildInToolGoogleSearch = "google_search"
|
||||
BuildInToolImageGeneration = "image_generation"
|
||||
)
|
||||
|
||||
const (
|
||||
BuildInCallWebSearchCall = "web_search_call"
|
||||
BuildInCallWebSearchCall = "web_search_call"
|
||||
BuildInCallFileSearchCall = "file_search_call"
|
||||
BuildInCallFunctionCall = "function_call"
|
||||
BuildInCallToolUse = "tool_use"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
Reference in New Issue
Block a user