Add projects

This commit is contained in:
Alois 2026-08-24 00:10:41 +02:00
commit 8b607dd700
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
1802 changed files with 503346 additions and 2 deletions

View file

@ -0,0 +1,502 @@
package claude
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
type webSearchGroundingSupport struct {
StartIndex int64
EndIndex int64
Text string
ChunkURLs []string
ChunkTitle string
}
type webSearchCitedTextBlock struct {
Text string
Citations []map[string]any
}
const antigravityWebSearchSystemInstruction = "You are a search engine bot. You will be given a query from a user. Your task is to search the web for relevant information that will help the user. You MUST perform a web search. Do not respond or interact with the user, please respond as if they typed the query into a search bar."
func antigravitySupportsNativeGoogleSearch(model string) bool {
return registry.AntigravityWebSearchModelFor(model) != ""
}
func isClaudeTypedWebSearchToolType(toolType string) bool {
return toolType == "web_search_20250305" || toolType == "web_search_20260209"
}
func hasClaudeTypedWebSearchTool(payload []byte) bool {
tools := gjson.GetBytes(payload, "tools")
if !tools.IsArray() {
return false
}
for _, tool := range tools.Array() {
if isClaudeTypedWebSearchToolType(tool.Get("type").String()) {
return true
}
}
return false
}
func hasOnlyClaudeTypedWebSearchTools(payload []byte) bool {
tools := gjson.GetBytes(payload, "tools")
if !tools.IsArray() {
return false
}
hasWebSearch := false
for _, tool := range tools.Array() {
if isClaudeTypedWebSearchToolType(tool.Get("type").String()) {
hasWebSearch = true
continue
}
return false
}
return hasWebSearch
}
func allowsClaudeWebSearchToolChoice(payload []byte) bool {
toolChoice := gjson.GetBytes(payload, "tool_choice")
if !toolChoice.Exists() {
return true
}
if toolChoice.Type == gjson.String {
switch toolChoice.String() {
case "", "auto", "any":
return true
case "none":
return false
default:
return false
}
}
if !toolChoice.IsObject() {
return false
}
switch toolChoice.Get("type").String() {
case "", "auto", "any":
return true
case "tool":
return toolChoice.Get("name").String() == "web_search"
default:
return false
}
}
func shouldBuildAntigravityWebSearchRequest(model string, payload []byte) bool {
return antigravitySupportsNativeGoogleSearch(model) &&
hasOnlyClaudeTypedWebSearchTools(payload) &&
allowsClaudeWebSearchToolChoice(payload)
}
func buildAntigravityWebSearchRequest(model string, payload []byte) []byte {
query := extractClaudeWebSearchQuery(payload)
maxResultCount := extractClaudeWebSearchMaxUses(payload)
includedDomains := extractClaudeWebSearchAllowedDomains(payload)
out := []byte(`{"model":"","requestType":"web_search","request":{"contents":[{"role":"user","parts":[{"text":""}]}],"systemInstruction":{"role":"user","parts":[{"text":""}]},"tools":[{"googleSearch":{"enhancedContent":{"imageSearch":{"maxResultCount":5}}}}],"generationConfig":{"candidateCount":1}}}`)
out, _ = sjson.SetBytes(out, "model", model)
out, _ = sjson.SetBytes(out, "request.contents.0.parts.0.text", query)
out, _ = sjson.SetBytes(out, "request.systemInstruction.parts.0.text", antigravityWebSearchSystemInstruction)
out, _ = sjson.SetBytes(out, "request.tools.0.googleSearch.enhancedContent.imageSearch.maxResultCount", maxResultCount)
if len(includedDomains) > 0 {
if domainsJSON, err := json.Marshal(includedDomains); err == nil {
out, _ = sjson.SetRawBytes(out, "request.tools.0.googleSearch.includedDomains", domainsJSON)
}
}
return out
}
func extractClaudeWebSearchMaxUses(payload []byte) int64 {
const defaultMaxResultCount int64 = 5
tools := gjson.GetBytes(payload, "tools")
if !tools.IsArray() {
return defaultMaxResultCount
}
for _, tool := range tools.Array() {
if !isClaudeTypedWebSearchToolType(tool.Get("type").String()) {
continue
}
maxUses := tool.Get("max_uses").Int()
if maxUses > 0 {
return maxUses
}
}
return defaultMaxResultCount
}
func extractClaudeWebSearchAllowedDomains(payload []byte) []string {
tools := gjson.GetBytes(payload, "tools")
if !tools.IsArray() {
return nil
}
for _, tool := range tools.Array() {
if !isClaudeTypedWebSearchToolType(tool.Get("type").String()) {
continue
}
allowedDomains := tool.Get("allowed_domains")
if !allowedDomains.IsArray() {
return nil
}
domains := make([]string, 0, len(allowedDomains.Array()))
for _, domain := range allowedDomains.Array() {
if domain.Type != gjson.String {
continue
}
if trimmed := strings.TrimSpace(domain.String()); trimmed != "" {
domains = append(domains, trimmed)
}
}
return domains
}
return nil
}
func extractClaudeWebSearchQuery(payload []byte) string {
messages := gjson.GetBytes(payload, "messages")
if !messages.IsArray() {
return ""
}
messageResults := messages.Array()
for i := len(messageResults) - 1; i >= 0; i-- {
message := messageResults[i]
if role := message.Get("role").String(); role != "" && role != "user" {
continue
}
if query := extractClaudeTextContent(message.Get("content")); query != "" {
return query
}
}
return ""
}
func extractClaudeTextContent(content gjson.Result) string {
if content.Type == gjson.String {
return strings.TrimSpace(content.String())
}
if !content.IsArray() {
return ""
}
var b strings.Builder
for _, part := range content.Array() {
if text := strings.TrimSpace(part.Get("text").String()); text != "" {
if b.Len() > 0 {
b.WriteByte('\n')
}
b.WriteString(text)
}
}
return strings.TrimSpace(b.String())
}
func hasAntigravityGoogleSearchTool(payload []byte) bool {
tools := gjson.GetBytes(payload, "request.tools")
if !tools.IsArray() {
return false
}
for _, tool := range tools.Array() {
if tool.Get("googleSearch").Exists() {
return true
}
}
return false
}
func shouldTranslateWebSearchGrounding(originalRequestRawJSON, requestRawJSON []byte) bool {
return hasClaudeTypedWebSearchTool(originalRequestRawJSON) && hasAntigravityGoogleSearchTool(requestRawJSON)
}
func antigravityGroundingMetadata(root gjson.Result) gjson.Result {
groundingMetadata := root.Get("response.candidates.0.groundingMetadata")
if groundingMetadata.Exists() {
return groundingMetadata
}
return root.Get("candidates.0.groundingMetadata")
}
func antigravityTextContent(root gjson.Result) string {
var textBuilder strings.Builder
parts := root.Get("response.candidates.0.content.parts")
if !parts.IsArray() {
parts = root.Get("candidates.0.content.parts")
}
if parts.IsArray() {
for _, part := range parts.Array() {
if text := part.Get("text"); text.Exists() {
textBuilder.WriteString(text.String())
}
}
}
return textBuilder.String()
}
func antigravityUsageTokens(root gjson.Result) (int64, int64) {
usage := root.Get("response.usageMetadata")
if !usage.Exists() {
usage = root.Get("usageMetadata")
}
inputTokens := usage.Get("promptTokenCount").Int()
outputTokens := usage.Get("candidatesTokenCount").Int() + usage.Get("thoughtsTokenCount").Int()
if outputTokens == 0 {
totalTokens := usage.Get("totalTokenCount").Int()
if totalTokens > 0 {
outputTokens = totalTokens - inputTokens
if outputTokens < 0 {
outputTokens = 0
}
}
}
return inputTokens, outputTokens
}
func webSearchQueryFromGrounding(groundingMetadata gjson.Result) string {
if queries := groundingMetadata.Get("webSearchQueries"); queries.IsArray() && len(queries.Array()) > 0 {
return queries.Array()[0].String()
}
return ""
}
func webSearchResultsFromGrounding(groundingMetadata gjson.Result) []byte {
results := []byte(`[]`)
groundingChunks := groundingMetadata.Get("groundingChunks")
if !groundingChunks.IsArray() {
return results
}
seenURLs := make(map[string]struct{})
for _, chunk := range groundingChunks.Array() {
web := chunk.Get("web")
if !web.Exists() {
continue
}
uri := strings.TrimSpace(web.Get("uri").String())
if uri == "" {
continue
}
if _, ok := seenURLs[uri]; ok {
continue
}
seenURLs[uri] = struct{}{}
result := []byte(`{"type":"web_search_result","page_age":null}`)
if title := web.Get("title"); title.Exists() {
result, _ = sjson.SetBytes(result, "title", title.String())
}
result, _ = sjson.SetBytes(result, "url", uri)
results, _ = sjson.SetRawBytes(results, "-1", result)
}
return results
}
func parseWebSearchGroundingSupports(groundingMetadata gjson.Result) []webSearchGroundingSupport {
groundingChunks := groundingMetadata.Get("groundingChunks")
if !groundingChunks.IsArray() {
return nil
}
chunks := groundingChunks.Array()
chunkData := make([]struct {
URL string
Title string
}, len(chunks))
for i, chunk := range chunks {
web := chunk.Get("web")
if web.Exists() {
chunkData[i].URL = web.Get("uri").String()
chunkData[i].Title = web.Get("title").String()
}
}
groundingSupports := groundingMetadata.Get("groundingSupports")
if !groundingSupports.IsArray() {
return nil
}
supports := make([]webSearchGroundingSupport, 0, len(groundingSupports.Array()))
for _, support := range groundingSupports.Array() {
segment := support.Get("segment")
if !segment.Exists() {
continue
}
parsed := webSearchGroundingSupport{
StartIndex: segment.Get("startIndex").Int(),
EndIndex: segment.Get("endIndex").Int(),
Text: segment.Get("text").String(),
}
if chunkIndices := support.Get("groundingChunkIndices"); chunkIndices.IsArray() {
for _, idx := range chunkIndices.Array() {
chunkIndex := int(idx.Int())
if chunkIndex < 0 || chunkIndex >= len(chunkData) {
continue
}
parsed.ChunkURLs = append(parsed.ChunkURLs, chunkData[chunkIndex].URL)
if parsed.ChunkTitle == "" {
parsed.ChunkTitle = chunkData[chunkIndex].Title
}
}
}
supports = append(supports, parsed)
}
return supports
}
func buildWebSearchCitedTextBlocks(textContent string, supports []webSearchGroundingSupport) []webSearchCitedTextBlock {
if len(supports) == 0 {
if textContent == "" {
return nil
}
return []webSearchCitedTextBlock{{Text: textContent}}
}
textBytes := []byte(textContent)
blocks := make([]webSearchCitedTextBlock, 0, len(supports)+1)
lastEnd := int64(0)
for _, support := range supports {
if support.EndIndex <= lastEnd {
continue
}
if support.StartIndex > lastEnd {
start := int(lastEnd)
end := min(int(support.StartIndex), len(textBytes))
if start < end {
blocks = append(blocks, webSearchCitedTextBlock{Text: string(textBytes[start:end])})
}
}
citedStart := support.StartIndex
if citedStart < lastEnd {
citedStart = lastEnd
}
citedText := ""
if citedStart < support.EndIndex {
start := min(int(citedStart), len(textBytes))
end := min(int(support.EndIndex), len(textBytes))
if start < end {
citedText = string(textBytes[start:end])
}
}
if citedText != "" && len(support.ChunkURLs) > 0 {
citation := map[string]any{
"type": "web_search_result_location",
"cited_text": citedText,
"url": support.ChunkURLs[0],
"title": support.ChunkTitle,
}
blocks = append(blocks, webSearchCitedTextBlock{
Text: citedText,
Citations: []map[string]any{citation},
})
}
if support.EndIndex > lastEnd {
lastEnd = support.EndIndex
}
}
if int(lastEnd) < len(textBytes) {
blocks = append(blocks, webSearchCitedTextBlock{Text: string(textBytes[lastEnd:])})
}
return blocks
}
func buildClaudeWebSearchContent(toolUseID string, textContent string, groundingMetadata gjson.Result) []byte {
content := []byte(`[]`)
serverToolUse := []byte(`{"type":"server_tool_use","id":"","name":"web_search","input":{}}`)
serverToolUse, _ = sjson.SetBytes(serverToolUse, "id", toolUseID)
if query := webSearchQueryFromGrounding(groundingMetadata); query != "" {
serverToolUse, _ = sjson.SetBytes(serverToolUse, "input.query", query)
}
content, _ = sjson.SetRawBytes(content, "-1", serverToolUse)
webSearchToolResult := []byte(`{"type":"web_search_tool_result","tool_use_id":"","content":[]}`)
webSearchToolResult, _ = sjson.SetBytes(webSearchToolResult, "tool_use_id", toolUseID)
webSearchToolResult, _ = sjson.SetRawBytes(webSearchToolResult, "content", webSearchResultsFromGrounding(groundingMetadata))
content, _ = sjson.SetRawBytes(content, "-1", webSearchToolResult)
for _, block := range buildWebSearchCitedTextBlocks(textContent, parseWebSearchGroundingSupports(groundingMetadata)) {
if block.Text == "" {
continue
}
textBlock := []byte(`{"type":"text","text":""}`)
textBlock, _ = sjson.SetBytes(textBlock, "text", block.Text)
if len(block.Citations) > 0 {
citationsJSON, _ := json.Marshal(block.Citations)
textBlock, _ = sjson.SetRawBytes(textBlock, "citations", citationsJSON)
}
content, _ = sjson.SetRawBytes(content, "-1", textBlock)
}
return content
}
func appendClaudeWebSearchStreamBlocks(appendEvent func(string, string), startIndex int, toolUseID string, textContent string, groundingMetadata gjson.Result) int {
contentIndex := startIndex
serverToolUseStart := fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"server_tool_use","id":"%s","name":"web_search","input":{}}}`,
contentIndex, toolUseID)
appendEvent("content_block_start", serverToolUseStart)
if query := webSearchQueryFromGrounding(groundingMetadata); query != "" {
queryJSON, _ := sjson.Set(`{}`, "query", query)
inputDelta := fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, contentIndex)
inputDelta, _ = sjson.Set(inputDelta, "delta.partial_json", queryJSON)
appendEvent("content_block_delta", inputDelta)
}
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, contentIndex))
contentIndex++
webSearchToolResultStart := fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"web_search_tool_result","tool_use_id":"%s","content":[]}}`,
contentIndex, toolUseID)
webSearchToolResultStart, _ = sjson.SetRaw(webSearchToolResultStart, "content_block.content", string(webSearchResultsFromGrounding(groundingMetadata)))
appendEvent("content_block_start", webSearchToolResultStart)
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, contentIndex))
contentIndex++
for _, block := range buildWebSearchCitedTextBlocks(textContent, parseWebSearchGroundingSupports(groundingMetadata)) {
if block.Text == "" {
continue
}
textBlockStart := fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, contentIndex)
if len(block.Citations) > 0 {
textBlockStart = fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"citations":[],"type":"text","text":""}}`, contentIndex)
}
appendEvent("content_block_start", textBlockStart)
for _, citation := range block.Citations {
citationJSON, _ := json.Marshal(citation)
citationDelta := fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"citations_delta","citation":%s}}`, contentIndex, string(citationJSON))
appendEvent("content_block_delta", citationDelta)
}
for _, chunk := range splitRunesForWebSearch(block.Text, 50) {
textDelta := fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, contentIndex)
textDelta, _ = sjson.Set(textDelta, "delta.text", chunk)
appendEvent("content_block_delta", textDelta)
}
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, contentIndex))
contentIndex++
}
return contentIndex
}
func splitRunesForWebSearch(text string, chunkSize int) []string {
if chunkSize <= 0 || text == "" {
return nil
}
runes := []rune(text)
chunks := make([]string, 0, (len(runes)+chunkSize-1)/chunkSize)
for start := 0; start < len(runes); start += chunkSize {
end := start + chunkSize
if end > len(runes) {
end = len(runes)
}
chunks = append(chunks, string(runes[start:end]))
}
return chunks
}
func newClaudeWebSearchToolUseID() string {
return fmt.Sprintf("srvtoolu_%d", time.Now().UnixNano())
}