Files
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

80 lines
2.8 KiB
Go

package handler
import (
"encoding/json"
"fmt"
"strings"
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
"github.com/gin-gonic/gin"
)
type grokStandaloneSearchRequest struct {
Query string `json:"query"`
Input string `json:"input"`
MaxResults *int `json:"max_results"`
AllowedXHandles []string `json:"allowed_x_handles"`
ExcludedXHandles []string `json:"excluded_x_handles"`
FromDate string `json:"from_date"`
ToDate string `json:"to_date"`
EnableImageUnderstanding *bool `json:"enable_image_understanding"`
EnableVideoUnderstanding *bool `json:"enable_video_understanding"`
}
// XSearch marks the standalone endpoint so WebSearch can use native x_search
// while retaining its dedicated per-call billing contract.
func (h *GatewayHandler) XSearch(c *gin.Context) {
c.Set("grok_x_search_endpoint", true)
h.WebSearch(c)
}
func resolveGrokStandaloneSearchModel() string {
return xai.ResolveDefaultTextModel(xai.RuntimeModelMappingOptions().DefaultText)
}
func buildGrokXSearchResponsesBody(req grokStandaloneSearchRequest, model string) ([]byte, error) {
input := strings.TrimSpace(req.Query)
if input == "" {
input = strings.TrimSpace(req.Input)
}
tool := map[string]any{"type": "x_search"}
if len(req.AllowedXHandles) > 0 {
tool["allowed_x_handles"] = req.AllowedXHandles
}
if len(req.ExcludedXHandles) > 0 {
tool["excluded_x_handles"] = req.ExcludedXHandles
}
if strings.TrimSpace(req.FromDate) != "" {
tool["from_date"] = strings.TrimSpace(req.FromDate)
}
if strings.TrimSpace(req.ToDate) != "" {
tool["to_date"] = strings.TrimSpace(req.ToDate)
}
if req.EnableImageUnderstanding != nil {
tool["enable_image_understanding"] = *req.EnableImageUnderstanding
}
if req.EnableVideoUnderstanding != nil {
tool["enable_video_understanding"] = *req.EnableVideoUnderstanding
}
maxResults := 0
if req.MaxResults != nil {
maxResults = *req.MaxResults
}
return json.Marshal(map[string]any{
"model": xai.ResolveDefaultTextModel(model),
"input": buildGrokXSearchPrompt(input, maxResults),
"tools": []map[string]any{tool},
"tool_choice": "required",
"include": []string{"x_search_call.action.sources"},
"store": false,
"stream": false,
})
}
func buildGrokXSearchPrompt(query string, maxResults int) string {
return fmt.Sprintf(`Search X for the user query below. Return ONLY valid JSON with this exact shape: {"results":[{"url":"https://...","title":"post or page title","snippet":"concise factual summary"}]}. Return at most %d unique results. Every URL must be an actual x_search source. Populate a non-empty title and snippet for every result. Do not wrap the JSON in markdown.
User query:
%s`, normalizeGrokWebSearchMaxResults(maxResults), query)
}