3048 lines
90 KiB
Go
3048 lines
90 KiB
Go
// th3ist: OpenAI-compatible gateway proxy for t3.chat in Go
|
|
// Created by Luxferre in 2026, released into the public domain
|
|
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
DefaultUserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
|
|
DefaultTargetURL = "https://t3.chat/api/chat"
|
|
DefaultClientContext = ""
|
|
DefaultDeploymentID = ""
|
|
DefaultCookie = ""
|
|
DefaultHcaptchaToken = ""
|
|
DefaultSitekey = "c3102294-b06e-444a-a0c8-79d0f3b04b5a"
|
|
DefaultModel = "gemini-3.5-flash-lite"
|
|
ConfiguredUserAgent string
|
|
cdpCmdCounter int64
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// OpenAI API Data Structures
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type ModelItem struct {
|
|
ID string `json:"id"`
|
|
Object string `json:"object"`
|
|
Created int64 `json:"created"`
|
|
OwnedBy string `json:"owned_by"`
|
|
}
|
|
|
|
type ModelsResponse struct {
|
|
Object string `json:"object"`
|
|
Data []ModelItem `json:"data"`
|
|
}
|
|
|
|
type ToolCallFunction struct {
|
|
Name string `json:"name"`
|
|
Arguments string `json:"arguments"`
|
|
}
|
|
|
|
type ToolCall struct {
|
|
Index *int `json:"index,omitempty"`
|
|
ID string `json:"id,omitempty"`
|
|
Type string `json:"type,omitempty"`
|
|
Function ToolCallFunction `json:"function"`
|
|
}
|
|
|
|
type Tool struct {
|
|
Type string `json:"type"`
|
|
Function interface{} `json:"function"`
|
|
}
|
|
|
|
type ChatMessage struct {
|
|
Role string `json:"role"`
|
|
Content interface{} `json:"content"`
|
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
|
ToolCallID string `json:"tool_call_id,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
}
|
|
|
|
func (m *ChatMessage) GetContentString() string {
|
|
if m.Content == nil {
|
|
return ""
|
|
}
|
|
if str, ok := m.Content.(string); ok {
|
|
return str
|
|
}
|
|
if parts, ok := m.Content.([]interface{}); ok {
|
|
var sb strings.Builder
|
|
for _, p := range parts {
|
|
if str, ok := p.(string); ok {
|
|
sb.WriteString(str)
|
|
} else if itemMap, ok := p.(map[string]interface{}); ok {
|
|
if textVal, ok := itemMap["text"].(string); ok {
|
|
sb.WriteString(textVal)
|
|
}
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
b, err := json.Marshal(m.Content)
|
|
if err == nil {
|
|
return string(b)
|
|
}
|
|
return fmt.Sprintf("%v", m.Content)
|
|
}
|
|
|
|
type ChatCompletionRequest struct {
|
|
Model string `json:"model"`
|
|
Messages []ChatMessage `json:"messages"`
|
|
Tools []Tool `json:"tools,omitempty"`
|
|
ToolChoice interface{} `json:"tool_choice,omitempty"`
|
|
Stream bool `json:"stream"`
|
|
MaxTokens int `json:"max_tokens"`
|
|
MaxCompletionTokens int `json:"max_completion_tokens"`
|
|
Temperature *float64 `json:"temperature,omitempty"`
|
|
TopP *float64 `json:"top_p,omitempty"`
|
|
ReasoningEffort string `json:"reasoning_effort,omitempty"`
|
|
}
|
|
|
|
type ChatCompletionResponseChoice struct {
|
|
Index int `json:"index"`
|
|
Message ChatMessage `json:"message"`
|
|
FinishReason string `json:"finish_reason"`
|
|
}
|
|
|
|
type Usage struct {
|
|
PromptTokens int `json:"prompt_tokens"`
|
|
CompletionTokens int `json:"completion_tokens"`
|
|
TotalTokens int `json:"total_tokens"`
|
|
}
|
|
|
|
type ChatCompletionResponse struct {
|
|
ID string `json:"id"`
|
|
Object string `json:"object"`
|
|
Created int64 `json:"created"`
|
|
Model string `json:"model"`
|
|
Choices []ChatCompletionResponseChoice `json:"choices"`
|
|
Usage Usage `json:"usage"`
|
|
}
|
|
|
|
type StreamDelta struct {
|
|
Role string `json:"role,omitempty"`
|
|
Content string `json:"content,omitempty"`
|
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
|
}
|
|
|
|
type StreamChoice struct {
|
|
Index int `json:"index"`
|
|
Delta StreamDelta `json:"delta"`
|
|
FinishReason *string `json:"finish_reason,omitempty"`
|
|
}
|
|
|
|
type StreamResponse struct {
|
|
ID string `json:"id"`
|
|
Object string `json:"object"`
|
|
Created int64 `json:"created"`
|
|
Model string `json:"model"`
|
|
Choices []StreamChoice `json:"choices"`
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T3 Upstream Request Data Structures
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type T3MessagePart struct {
|
|
Type string `json:"type"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
type T3Message struct {
|
|
ID string `json:"id"`
|
|
Parts []T3MessagePart `json:"parts"`
|
|
Role string `json:"role"`
|
|
Attachments []interface{} `json:"attachments"`
|
|
}
|
|
|
|
type T3ThreadMetadata struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
}
|
|
|
|
type T3ClientAuth struct {
|
|
IsSignedIn bool `json:"isSignedIn"`
|
|
}
|
|
|
|
type T3ModelParams struct {
|
|
ReasoningEffort string `json:"reasoningEffort"`
|
|
IncludeSearch bool `json:"includeSearch"`
|
|
SearchLimit int `json:"searchLimit"`
|
|
}
|
|
|
|
type T3UserConfigParams struct {
|
|
IncludeSearch bool `json:"includeSearch"`
|
|
ReasoningEffort string `json:"reasoningEffort"`
|
|
}
|
|
|
|
type T3UserConfiguration struct {
|
|
CreationTime float64 `json:"_creationTime"`
|
|
CurrentModelParameters T3UserConfigParams `json:"currentModelParameters"`
|
|
CurrentlySelectedModel string `json:"currentlySelectedModel"`
|
|
LatestTOSDate int64 `json:"latestTOSDate"`
|
|
}
|
|
|
|
type T3UserInfo struct {
|
|
Timezone string `json:"timezone"`
|
|
Locale string `json:"locale"`
|
|
}
|
|
|
|
type T3LocalApiKey struct {
|
|
Provider string `json:"provider"`
|
|
Key string `json:"key"`
|
|
DefaultMode string `json:"defaultMode"`
|
|
ModelModes map[string]string `json:"modelModes"`
|
|
}
|
|
|
|
type T3ChatPayload struct {
|
|
Messages []T3Message `json:"messages"`
|
|
ThreadMetadata T3ThreadMetadata `json:"threadMetadata"`
|
|
ClientAuth T3ClientAuth `json:"clientAuth"`
|
|
ResponseMessageID string `json:"responseMessageId"`
|
|
Model string `json:"model"`
|
|
ConvexSessionID string `json:"convexSessionId"`
|
|
ModelParams T3ModelParams `json:"modelParams"`
|
|
Preferences map[string]interface{} `json:"preferences"`
|
|
UserConfiguration T3UserConfiguration `json:"userConfiguration"`
|
|
HcaptchaToken string `json:"hcaptchaToken,omitempty"`
|
|
ApiKey *T3LocalApiKey `json:"apiKey,omitempty"`
|
|
UserInfo T3UserInfo `json:"userInfo"`
|
|
IsEphemeral bool `json:"isEphemeral"`
|
|
}
|
|
|
|
type TokenPayload struct {
|
|
HcaptchaToken string `json:"hcaptchaToken"`
|
|
Cookie string `json:"cookie"`
|
|
DeploymentID string `json:"deploymentId"`
|
|
ClientContext string `json:"clientContext"`
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper Utilities
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func GenerateUUID() string {
|
|
var b [16]byte
|
|
_, err := rand.Read(b[:])
|
|
if err != nil {
|
|
return "00000000-0000-4000-8000-000000000000"
|
|
}
|
|
b[6] = (b[6] & 0x0f) | 0x40
|
|
b[8] = (b[8] & 0x3f) | 0x80
|
|
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
|
|
}
|
|
|
|
func GenerateHex(n int) string {
|
|
b := make([]byte, n)
|
|
_, _ = rand.Read(b)
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
func getFreePort() (string, error) {
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer l.Close()
|
|
_, port, err := net.SplitHostPort(l.Addr().String())
|
|
return port, err
|
|
}
|
|
|
|
func FibonacciDelay(attempt int) time.Duration {
|
|
if attempt <= 0 {
|
|
return 1 * time.Second
|
|
}
|
|
a, b := 1, 1
|
|
for i := 1; i < attempt; i++ {
|
|
a, b = b, a+b
|
|
}
|
|
return time.Duration(a) * time.Second
|
|
}
|
|
|
|
func DoWithFibonacciRetry(client *http.Client, makeReq func() (*http.Request, error), maxRetries int) (*http.Response, error) {
|
|
var lastErr error
|
|
for attempt := 1; attempt <= maxRetries; attempt++ {
|
|
req, err := makeReq()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err == nil && resp.StatusCode == http.StatusOK {
|
|
return resp, nil
|
|
}
|
|
|
|
if resp != nil {
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
|
|
return nil, fmt.Errorf("HTTP status %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
lastErr = fmt.Errorf("HTTP status %d: %s", resp.StatusCode, string(respBody))
|
|
} else {
|
|
lastErr = err
|
|
}
|
|
|
|
if attempt < maxRetries {
|
|
delay := FibonacciDelay(attempt)
|
|
time.Sleep(delay)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("request failed after %d retries: %v", maxRetries, lastErr)
|
|
}
|
|
|
|
func EnableCORS(w http.ResponseWriter) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, api-key, X-User-Agent, X-Hcaptcha-Token, X-Deployment-Id, X-Cookie, X-Client-Context")
|
|
}
|
|
|
|
func ResolveMaxTokens(req ChatCompletionRequest) int {
|
|
mt := req.MaxTokens
|
|
if mt == 0 && req.MaxCompletionTokens > 0 {
|
|
mt = req.MaxCompletionTokens
|
|
}
|
|
if mt <= 0 {
|
|
mt = 131072
|
|
}
|
|
return mt
|
|
}
|
|
|
|
func EffectiveUserAgent(r *http.Request) string {
|
|
if r != nil {
|
|
if c := r.Header.Get("X-User-Agent"); c != "" {
|
|
return c
|
|
}
|
|
}
|
|
if ConfiguredUserAgent != "" {
|
|
return ConfiguredUserAgent
|
|
}
|
|
return DefaultUserAgent
|
|
}
|
|
|
|
func isRateLimited(status int, body string) bool {
|
|
if status == http.StatusTooManyRequests {
|
|
return true
|
|
}
|
|
lower := strings.ToLower(body)
|
|
return strings.Contains(lower, "ratelimit") ||
|
|
strings.Contains(lower, "rate_limit") ||
|
|
strings.Contains(lower, "rate limit") ||
|
|
strings.Contains(lower, "too many requests") ||
|
|
strings.Contains(lower, "quota_exceeded") ||
|
|
strings.Contains(lower, "quota exceeded")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tool and Message Processing
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func BuildToolInstruction(tools []Tool) string {
|
|
if len(tools) == 0 {
|
|
return ""
|
|
}
|
|
toolsBytes, _ := json.MarshalIndent(tools, "", " ")
|
|
return fmt.Sprintf("\n\n# Tool Calling Instructions\n\nYou have access to the following functions:\n<tools>\n%s\n</tools>\n\nWhen you need to call a function, respond ONLY with a <tool_call> block formatted exactly as follows:\n<tool_call>\n{\"name\": \"<function-name>\", \"arguments\": {<args-json-object>}}\n</tool_call>\n\nDo not include conversational filler before or after the tool call.", string(toolsBytes))
|
|
}
|
|
|
|
func TransformMessages(req ChatCompletionRequest) (processed []ChatMessage, toolInstruction string, hasSystem bool) {
|
|
toolInstruction = BuildToolInstruction(req.Tools)
|
|
for _, msg := range req.Messages {
|
|
contentStr := msg.GetContentString()
|
|
m := ChatMessage{Role: msg.Role, Content: contentStr}
|
|
switch msg.Role {
|
|
case "system":
|
|
hasSystem = true
|
|
m.Content = contentStr
|
|
case "assistant":
|
|
var sb strings.Builder
|
|
if contentStr != "" {
|
|
sb.WriteString(contentStr)
|
|
}
|
|
for _, tc := range msg.ToolCalls {
|
|
if sb.Len() > 0 {
|
|
sb.WriteString("\n")
|
|
}
|
|
args := tc.Function.Arguments
|
|
if strings.TrimSpace(args) == "" {
|
|
args = "{}"
|
|
}
|
|
sb.WriteString(fmt.Sprintf("<tool_call>\n{\"name\": %q, \"arguments\": %s}\n</tool_call>", tc.Function.Name, args))
|
|
}
|
|
m.Content = sb.String()
|
|
case "tool", "function":
|
|
m.Role = "user"
|
|
toolName := msg.Name
|
|
if toolName == "" {
|
|
toolName = msg.ToolCallID
|
|
}
|
|
var contentJSON []byte
|
|
if json.Valid([]byte(contentStr)) {
|
|
contentJSON = []byte(contentStr)
|
|
} else {
|
|
contentJSON, _ = json.Marshal(contentStr)
|
|
}
|
|
m.Content = fmt.Sprintf("<tool_response>\n{\"name\": %q, \"content\": %s}\n</tool_response>", toolName, string(contentJSON))
|
|
}
|
|
processed = append(processed, m)
|
|
}
|
|
|
|
if toolInstruction != "" {
|
|
if hasSystem {
|
|
for i, m := range processed {
|
|
if m.Role == "system" {
|
|
processed[i].Content = m.GetContentString() + "\n" + strings.TrimSpace(toolInstruction)
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
processed = append([]ChatMessage{
|
|
{Role: "user", Content: strings.TrimSpace(toolInstruction)},
|
|
}, processed...)
|
|
}
|
|
}
|
|
|
|
return processed, toolInstruction, hasSystem
|
|
}
|
|
|
|
func cleanJSONBlock(input string) string {
|
|
s := strings.TrimSpace(input)
|
|
if strings.HasPrefix(s, "```") {
|
|
lines := strings.Split(s, "\n")
|
|
if len(lines) >= 2 {
|
|
if strings.HasPrefix(lines[len(lines)-1], "```") {
|
|
lines = lines[1 : len(lines)-1]
|
|
} else {
|
|
lines = lines[1:]
|
|
}
|
|
s = strings.TrimSpace(strings.Join(lines, "\n"))
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func sanitizeJSONValue(v interface{}) interface{} {
|
|
switch val := v.(type) {
|
|
case string:
|
|
return strings.TrimSpace(val)
|
|
case map[string]interface{}:
|
|
cleanMap := make(map[string]interface{})
|
|
for k, childV := range val {
|
|
cleanKey := strings.TrimSpace(k)
|
|
cleanMap[cleanKey] = sanitizeJSONValue(childV)
|
|
}
|
|
return cleanMap
|
|
case []interface{}:
|
|
cleanSlice := make([]interface{}, len(val))
|
|
for i, childV := range val {
|
|
cleanSlice[i] = sanitizeJSONValue(childV)
|
|
}
|
|
return cleanSlice
|
|
default:
|
|
return v
|
|
}
|
|
}
|
|
|
|
var toolNameRegex = regexp.MustCompile(`"\s*(?:name|function|action|call)\s*"\s*:\s*"\s*([^"]+?)\s*"`)
|
|
|
|
func repairToolCallJSON(jsonStr string) (ToolCall, bool) {
|
|
nameMatch := toolNameRegex.FindStringSubmatch(jsonStr)
|
|
if len(nameMatch) < 2 {
|
|
return ToolCall{}, false
|
|
}
|
|
nameVal := strings.TrimSpace(nameMatch[1])
|
|
|
|
argsStr := "{}"
|
|
argsKwList := []string{`"arguments"`, `" parameters "`, `"arguments "`, `" parameters"`, `"parameters"`, `"args"`, `"input"`}
|
|
argsIdx := -1
|
|
for _, kw := range argsKwList {
|
|
idx := strings.Index(jsonStr, kw)
|
|
if idx >= 0 {
|
|
argsIdx = idx + len(kw)
|
|
break
|
|
}
|
|
}
|
|
|
|
var targetStr string
|
|
if argsIdx >= 0 {
|
|
targetStr = strings.TrimSpace(jsonStr[argsIdx:])
|
|
if strings.HasPrefix(targetStr, ":") {
|
|
targetStr = strings.TrimSpace(targetStr[1:])
|
|
}
|
|
} else {
|
|
targetStr = jsonStr
|
|
}
|
|
|
|
if strings.HasPrefix(targetStr, "{") {
|
|
endIdx := strings.LastIndex(targetStr, "}")
|
|
if endIdx > 0 {
|
|
objCandidate := targetStr[:endIdx+1]
|
|
var testMap map[string]interface{}
|
|
if json.Unmarshal([]byte(objCandidate), &testMap) == nil {
|
|
b, _ := json.Marshal(sanitizeJSONValue(testMap))
|
|
return ToolCall{
|
|
ID: "call_" + GenerateUUID()[:8],
|
|
Type: "function",
|
|
Function: ToolCallFunction{
|
|
Name: nameVal,
|
|
Arguments: string(b),
|
|
},
|
|
}, true
|
|
}
|
|
}
|
|
} else if strings.HasPrefix(targetStr, `"`) {
|
|
endIdx := strings.LastIndex(targetStr, `"`)
|
|
if endIdx > 0 {
|
|
val := strings.TrimSpace(targetStr[1:endIdx])
|
|
b, _ := json.Marshal(val)
|
|
argsStr = string(b)
|
|
}
|
|
}
|
|
|
|
return ToolCall{
|
|
ID: "call_" + GenerateUUID()[:8],
|
|
Type: "function",
|
|
Function: ToolCallFunction{
|
|
Name: nameVal,
|
|
Arguments: argsStr,
|
|
},
|
|
}, true
|
|
}
|
|
|
|
func parseSingleToolCall(jsonStr string) (ToolCall, bool) {
|
|
cleaned := cleanJSONBlock(jsonStr)
|
|
var raw map[string]interface{}
|
|
if err := json.Unmarshal([]byte(cleaned), &raw); err == nil {
|
|
sanitizedRaw, ok := sanitizeJSONValue(raw).(map[string]interface{})
|
|
if !ok {
|
|
sanitizedRaw = raw
|
|
}
|
|
|
|
for _, wrapperKey := range []string{"function", "function_call", "tool_call"} {
|
|
if fnObj, ok := sanitizedRaw[wrapperKey].(map[string]interface{}); ok {
|
|
if nameVal, ok := fnObj["name"].(string); ok && nameVal != "" {
|
|
argsStr := "{}"
|
|
var argsVal interface{}
|
|
if a, hasA := fnObj["arguments"]; hasA {
|
|
argsVal = a
|
|
} else if p, hasP := fnObj["parameters"]; hasP {
|
|
argsVal = p
|
|
} else if args, hasArgs := fnObj["args"]; hasArgs {
|
|
argsVal = args
|
|
}
|
|
if argsVal != nil {
|
|
if s, isStr := argsVal.(string); isStr {
|
|
var innerObj interface{}
|
|
if json.Unmarshal([]byte(s), &innerObj) == nil {
|
|
b, _ := json.Marshal(sanitizeJSONValue(innerObj))
|
|
argsStr = string(b)
|
|
} else {
|
|
argsStr = strings.TrimSpace(s)
|
|
}
|
|
} else {
|
|
b, _ := json.Marshal(sanitizeJSONValue(argsVal))
|
|
argsStr = string(b)
|
|
}
|
|
}
|
|
return ToolCall{
|
|
ID: "call_" + GenerateUUID()[:8],
|
|
Type: "function",
|
|
Function: ToolCallFunction{
|
|
Name: nameVal,
|
|
Arguments: argsStr,
|
|
},
|
|
}, true
|
|
}
|
|
}
|
|
}
|
|
|
|
nameVal := ""
|
|
for _, key := range []string{"name", "function", "action", "call"} {
|
|
if n, ok := sanitizedRaw[key].(string); ok && n != "" {
|
|
nameVal = n
|
|
break
|
|
}
|
|
}
|
|
|
|
if nameVal != "" {
|
|
argsStr := "{}"
|
|
var argsVal interface{}
|
|
for _, key := range []string{"arguments", "parameters", "args", "input"} {
|
|
if a, ok := sanitizedRaw[key]; ok {
|
|
argsVal = a
|
|
break
|
|
}
|
|
}
|
|
if argsVal != nil {
|
|
if s, isStr := argsVal.(string); isStr {
|
|
var innerObj interface{}
|
|
if json.Unmarshal([]byte(s), &innerObj) == nil {
|
|
b, _ := json.Marshal(sanitizeJSONValue(innerObj))
|
|
argsStr = string(b)
|
|
} else {
|
|
argsStr = strings.TrimSpace(s)
|
|
}
|
|
} else {
|
|
b, _ := json.Marshal(sanitizeJSONValue(argsVal))
|
|
argsStr = string(b)
|
|
}
|
|
} else {
|
|
argsMap := make(map[string]interface{})
|
|
for k, v := range sanitizedRaw {
|
|
if k != "name" && k != "function" && k != "type" && k != "action" && k != "call" {
|
|
argsMap[k] = v
|
|
}
|
|
}
|
|
if len(argsMap) > 0 {
|
|
b, _ := json.Marshal(sanitizeJSONValue(argsMap))
|
|
argsStr = string(b)
|
|
}
|
|
}
|
|
return ToolCall{
|
|
ID: "call_" + GenerateUUID()[:8],
|
|
Type: "function",
|
|
Function: ToolCallFunction{
|
|
Name: nameVal,
|
|
Arguments: argsStr,
|
|
},
|
|
}, true
|
|
}
|
|
}
|
|
|
|
return repairToolCallJSON(cleaned)
|
|
}
|
|
|
|
func parseXMLToolCall(block string) (ToolCall, bool) {
|
|
inner := strings.TrimSpace(block)
|
|
if strings.HasPrefix(inner, "<tool_call>") {
|
|
inner = strings.TrimPrefix(inner, "<tool_call>")
|
|
}
|
|
if strings.HasSuffix(inner, "</tool_call>") {
|
|
inner = strings.TrimSuffix(inner, "</tool_call>")
|
|
}
|
|
inner = cleanJSONBlock(inner)
|
|
|
|
if tc, ok := parseSingleToolCall(inner); ok {
|
|
return tc, true
|
|
}
|
|
|
|
var fnName string
|
|
if strings.Contains(inner, "<name>") && strings.Contains(inner, "</name>") {
|
|
nStart := strings.Index(inner, "<name>") + len("<name>")
|
|
nEnd := strings.Index(inner, "</name>")
|
|
if nStart < nEnd {
|
|
fnName = strings.TrimSpace(inner[nStart:nEnd])
|
|
}
|
|
}
|
|
|
|
var argsStr string
|
|
if strings.Contains(inner, "<arguments>") && strings.Contains(inner, "</arguments>") {
|
|
aStart := strings.Index(inner, "<arguments>") + len("<arguments>")
|
|
aEnd := strings.Index(inner, "</arguments>")
|
|
if aStart < aEnd {
|
|
argsStr = strings.TrimSpace(inner[aStart:aEnd])
|
|
}
|
|
}
|
|
|
|
if fnName != "" {
|
|
if argsStr == "" {
|
|
argsStr = "{}"
|
|
}
|
|
return ToolCall{
|
|
ID: "call_" + GenerateUUID()[:8],
|
|
Type: "function",
|
|
Function: ToolCallFunction{
|
|
Name: fnName,
|
|
Arguments: argsStr,
|
|
},
|
|
}, true
|
|
}
|
|
|
|
return ToolCall{}, false
|
|
}
|
|
|
|
func ExtractToolCallBlocks(content string) (blocks []string, remaining string) {
|
|
s := content
|
|
remaining = content
|
|
|
|
for strings.Contains(s, "<tool_call>") {
|
|
sIdx := strings.Index(s, "<tool_call>")
|
|
rest := s[sIdx+len("<tool_call>"):]
|
|
|
|
relNextSIdx := strings.Index(rest, "<tool_call>")
|
|
var nextSIdx int
|
|
if relNextSIdx != -1 {
|
|
nextSIdx = sIdx + len("<tool_call>") + relNextSIdx
|
|
} else {
|
|
nextSIdx = -1
|
|
}
|
|
|
|
relEIdx := strings.Index(rest, "</tool_call>")
|
|
var eIdx int
|
|
if relEIdx != -1 {
|
|
eIdx = sIdx + len("<tool_call>") + relEIdx
|
|
} else {
|
|
eIdx = -1
|
|
}
|
|
|
|
var blockText string
|
|
var blockEndPos int
|
|
|
|
if eIdx != -1 && (nextSIdx == -1 || eIdx < nextSIdx) {
|
|
blockEndPos = eIdx + len("</tool_call>")
|
|
blockText = s[sIdx:blockEndPos]
|
|
s = s[blockEndPos:]
|
|
} else if nextSIdx != -1 {
|
|
blockEndPos = nextSIdx
|
|
blockText = s[sIdx:blockEndPos]
|
|
s = s[blockEndPos:]
|
|
} else {
|
|
blockText = s[sIdx:]
|
|
s = ""
|
|
}
|
|
|
|
blocks = append(blocks, blockText)
|
|
}
|
|
|
|
for strings.Contains(remaining, "<tool_call>") {
|
|
st := strings.Index(remaining, "<tool_call>")
|
|
rest := remaining[st+len("<tool_call>"):]
|
|
|
|
relNext := strings.Index(rest, "<tool_call>")
|
|
var nextSt int
|
|
if relNext != -1 {
|
|
nextSt = st + len("<tool_call>") + relNext
|
|
} else {
|
|
nextSt = -1
|
|
}
|
|
|
|
relEn := strings.Index(rest, "</tool_call>")
|
|
var en int
|
|
if relEn != -1 {
|
|
en = st + len("<tool_call>") + relEn
|
|
} else {
|
|
en = -1
|
|
}
|
|
|
|
if en != -1 && (nextSt == -1 || en < nextSt) {
|
|
remaining = strings.TrimSpace(remaining[:st] + remaining[en+len("</tool_call>"):])
|
|
} else if nextSt != -1 {
|
|
remaining = strings.TrimSpace(remaining[:st] + remaining[nextSt:])
|
|
} else {
|
|
remaining = strings.TrimSpace(remaining[:st])
|
|
}
|
|
}
|
|
|
|
return blocks, remaining
|
|
}
|
|
|
|
func DetectToolCalls(content string) ([]ToolCall, string, bool) {
|
|
blocks, remaining := ExtractToolCallBlocks(content)
|
|
var calls []ToolCall
|
|
|
|
for _, block := range blocks {
|
|
if toolCall, ok := parseXMLToolCall(block); ok {
|
|
calls = append(calls, toolCall)
|
|
}
|
|
}
|
|
|
|
if len(calls) > 0 {
|
|
return calls, remaining, true
|
|
}
|
|
|
|
if tc, ok := parseSingleToolCall(strings.TrimSpace(content)); ok {
|
|
return []ToolCall{tc}, "", true
|
|
}
|
|
|
|
return nil, content, false
|
|
}
|
|
|
|
func ExtractThinking(content string) (string, string) {
|
|
if strings.Contains(content, "<think>") && strings.Contains(content, "</think>") {
|
|
start := strings.Index(content, "<think>")
|
|
end := strings.Index(content, "</think>")
|
|
if start < end {
|
|
reasoning := content[start+len("<think>") : end]
|
|
rem := content[:start] + content[end+len("</think>"):]
|
|
rem = strings.TrimPrefix(rem, "\n\n")
|
|
rem = strings.TrimPrefix(rem, "\n")
|
|
return rem, reasoning
|
|
}
|
|
}
|
|
return content, ""
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Response Framing & Streamer
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type FinalOutput struct {
|
|
Content interface{}
|
|
ReasoningContent string
|
|
ToolCalls []ToolCall
|
|
FinishReason string
|
|
}
|
|
|
|
func WriteCompletionResponse(w http.ResponseWriter, completionID string, created int64, model string, out FinalOutput) {
|
|
finish := out.FinishReason
|
|
if finish == "" {
|
|
finish = "stop"
|
|
}
|
|
resp := ChatCompletionResponse{
|
|
ID: completionID,
|
|
Object: "chat.completion",
|
|
Created: created,
|
|
Model: model,
|
|
Choices: []ChatCompletionResponseChoice{
|
|
{
|
|
Index: 0,
|
|
Message: ChatMessage{
|
|
Role: "assistant",
|
|
Content: out.Content,
|
|
ReasoningContent: out.ReasoningContent,
|
|
ToolCalls: out.ToolCalls,
|
|
},
|
|
FinishReason: finish,
|
|
},
|
|
},
|
|
Usage: Usage{
|
|
PromptTokens: 0,
|
|
CompletionTokens: 0,
|
|
TotalTokens: 0,
|
|
},
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
type Streamer struct {
|
|
w http.ResponseWriter
|
|
flusher http.Flusher
|
|
id string
|
|
created int64
|
|
model string
|
|
}
|
|
|
|
func NewStreamer(w http.ResponseWriter, flusher http.Flusher, id string, created int64, model string) *Streamer {
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
return &Streamer{w: w, flusher: flusher, id: id, created: created, model: model}
|
|
}
|
|
|
|
func (s *Streamer) Role() {
|
|
sendStreamDelta(s.w, s.flusher, s.id, s.created, s.model, StreamDelta{Role: "assistant"})
|
|
}
|
|
|
|
func (s *Streamer) Reasoning(text string) {
|
|
if text == "" {
|
|
return
|
|
}
|
|
sendStreamDelta(s.w, s.flusher, s.id, s.created, s.model, StreamDelta{ReasoningContent: text})
|
|
}
|
|
|
|
func (s *Streamer) Content(text string) {
|
|
if text == "" {
|
|
return
|
|
}
|
|
sendStreamDelta(s.w, s.flusher, s.id, s.created, s.model, StreamDelta{Content: text})
|
|
}
|
|
|
|
func (s *Streamer) ToolCallDelta(tc ToolCall) {
|
|
sendStreamDelta(s.w, s.flusher, s.id, s.created, s.model, StreamDelta{ToolCalls: []ToolCall{tc}})
|
|
}
|
|
|
|
func (s *Streamer) Finish(reason string) {
|
|
sendStreamChunk(s.w, s.flusher, s.id, s.created, s.model, StreamDelta{}, &reason)
|
|
}
|
|
|
|
func (s *Streamer) Done() {
|
|
fmt.Fprintf(s.w, "data: [DONE]\n\n")
|
|
if s.flusher != nil {
|
|
s.flusher.Flush()
|
|
}
|
|
}
|
|
|
|
func sendStreamDelta(w http.ResponseWriter, flusher http.Flusher, completionID string, createdTime int64, modelName string, delta StreamDelta) {
|
|
sendStreamChunk(w, flusher, completionID, createdTime, modelName, delta, nil)
|
|
}
|
|
|
|
func sendStreamChunk(w http.ResponseWriter, flusher http.Flusher, completionID string, createdTime int64, modelName string, delta StreamDelta, finishReason *string) {
|
|
chunk := StreamResponse{
|
|
ID: completionID,
|
|
Object: "chat.completion.chunk",
|
|
Created: createdTime,
|
|
Model: modelName,
|
|
Choices: []StreamChoice{
|
|
{
|
|
Index: 0,
|
|
Delta: delta,
|
|
FinishReason: finishReason,
|
|
},
|
|
},
|
|
}
|
|
b, _ := json.Marshal(chunk)
|
|
fmt.Fprintf(w, "data: %s\n\n", b)
|
|
if flusher != nil {
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Stateful Line Buffer for Chunked Streaming
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type StreamLineBuffer struct {
|
|
buf string
|
|
}
|
|
|
|
func (b *StreamLineBuffer) Feed(chunk string, onLine func(string)) {
|
|
b.buf += chunk
|
|
for {
|
|
idx := strings.Index(b.buf, "\n")
|
|
if idx == -1 {
|
|
break
|
|
}
|
|
line := b.buf[:idx]
|
|
b.buf = b.buf[idx+1:]
|
|
onLine(line)
|
|
}
|
|
}
|
|
|
|
func (b *StreamLineBuffer) Flush(onLine func(string)) {
|
|
if b.buf != "" {
|
|
onLine(b.buf)
|
|
b.buf = ""
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Stateful Thinking Tag Filter for Streaming
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type StreamThinkingFilter struct {
|
|
inThinking bool
|
|
buf string
|
|
}
|
|
|
|
func NewStreamThinkingFilter() *StreamThinkingFilter {
|
|
return &StreamThinkingFilter{}
|
|
}
|
|
|
|
func hasPrefixOf(target string, prefixes []string) int {
|
|
for _, p := range prefixes {
|
|
if strings.HasSuffix(target, p) {
|
|
return len(p)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (f *StreamThinkingFilter) Feed(chunk string, onContent func(string), onReasoning func(string)) {
|
|
f.buf += chunk
|
|
thinkStartTag := "<think>"
|
|
thinkEndTag := "</think>"
|
|
|
|
thinkStartPrefixes := []string{"<", "<t", "<th", "<thi", "<thin", "<think"}
|
|
thinkEndPrefixes := []string{"<", "</", "</t", "</th", "</thi", "</thin", "</think"}
|
|
|
|
for len(f.buf) > 0 {
|
|
if !f.inThinking {
|
|
if idx := strings.Index(f.buf, thinkStartTag); idx != -1 {
|
|
before := f.buf[:idx]
|
|
if before != "" {
|
|
onContent(before)
|
|
}
|
|
f.inThinking = true
|
|
f.buf = f.buf[idx+len(thinkStartTag):]
|
|
} else if matchLen := hasPrefixOf(f.buf, thinkStartPrefixes); matchLen > 0 {
|
|
safe := f.buf[:len(f.buf)-matchLen]
|
|
if safe != "" {
|
|
onContent(safe)
|
|
}
|
|
f.buf = f.buf[len(f.buf)-matchLen:]
|
|
break
|
|
} else {
|
|
onContent(f.buf)
|
|
f.buf = ""
|
|
break
|
|
}
|
|
} else {
|
|
if idx := strings.Index(f.buf, thinkEndTag); idx != -1 {
|
|
before := f.buf[:idx]
|
|
if before != "" {
|
|
onReasoning(before)
|
|
}
|
|
f.inThinking = false
|
|
f.buf = f.buf[idx+len(thinkEndTag):]
|
|
f.buf = strings.TrimPrefix(f.buf, "\n\n")
|
|
f.buf = strings.TrimPrefix(f.buf, "\n")
|
|
} else if matchLen := hasPrefixOf(f.buf, thinkEndPrefixes); matchLen > 0 {
|
|
safe := f.buf[:len(f.buf)-matchLen]
|
|
if safe != "" {
|
|
onReasoning(safe)
|
|
}
|
|
f.buf = f.buf[len(f.buf)-matchLen:]
|
|
break
|
|
} else {
|
|
onReasoning(f.buf)
|
|
f.buf = ""
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *StreamThinkingFilter) Flush(onContent func(string), onReasoning func(string)) {
|
|
if len(f.buf) > 0 {
|
|
if f.inThinking {
|
|
onReasoning(f.buf)
|
|
} else {
|
|
onContent(f.buf)
|
|
}
|
|
f.buf = ""
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Stateful Tool Call Tag Filter for Streaming
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type StreamToolCallFilter struct {
|
|
inToolCall bool
|
|
buf string
|
|
toolCallBuf string
|
|
toolIndex int
|
|
emittedCall bool
|
|
}
|
|
|
|
func NewStreamToolCallFilter() *StreamToolCallFilter {
|
|
return &StreamToolCallFilter{}
|
|
}
|
|
|
|
func (f *StreamToolCallFilter) Feed(chunk string, onContent func(string), onToolCall func(ToolCall)) {
|
|
f.buf += chunk
|
|
toolStartTag := "<tool_call>"
|
|
toolEndTag := "</tool_call>"
|
|
|
|
startPrefixes := []string{"<", "<t", "<to", "<too", "<tool", "<tool_", "<tool_c", "<tool_ca", "<tool_cal", "<tool_call"}
|
|
endPrefixes := []string{"<", "</", "</t", "</to", "</too", "</tool", "</tool_", "</tool_c", "</tool_ca", "</tool_cal", "</tool_call"}
|
|
|
|
for len(f.buf) > 0 {
|
|
if !f.inToolCall {
|
|
if idx := strings.Index(f.buf, toolStartTag); idx != -1 {
|
|
before := f.buf[:idx]
|
|
if before != "" {
|
|
onContent(before)
|
|
}
|
|
f.inToolCall = true
|
|
f.buf = f.buf[idx+len(toolStartTag):]
|
|
} else if matchLen := hasPrefixOf(f.buf, startPrefixes); matchLen > 0 {
|
|
safe := f.buf[:len(f.buf)-matchLen]
|
|
if safe != "" {
|
|
onContent(safe)
|
|
}
|
|
f.buf = f.buf[len(f.buf)-matchLen:]
|
|
break
|
|
} else {
|
|
onContent(f.buf)
|
|
f.buf = ""
|
|
break
|
|
}
|
|
} else {
|
|
if idx := strings.Index(f.buf, toolEndTag); idx != -1 {
|
|
f.toolCallBuf += f.buf[:idx]
|
|
f.buf = f.buf[idx+len(toolEndTag):]
|
|
f.inToolCall = false
|
|
|
|
if tc, ok := parseSingleToolCall(f.toolCallBuf); ok {
|
|
idxCopy := f.toolIndex
|
|
tc.Index = &idxCopy
|
|
f.toolIndex++
|
|
f.emittedCall = true
|
|
onToolCall(tc)
|
|
} else if tc2, ok2 := parseXMLToolCall("<tool_call>" + f.toolCallBuf + "</tool_call>"); ok2 {
|
|
idxCopy := f.toolIndex
|
|
tc2.Index = &idxCopy
|
|
f.toolIndex++
|
|
f.emittedCall = true
|
|
onToolCall(tc2)
|
|
} else {
|
|
onContent("<tool_call>" + f.toolCallBuf + "</tool_call>")
|
|
}
|
|
f.toolCallBuf = ""
|
|
} else if matchLen := hasPrefixOf(f.buf, endPrefixes); matchLen > 0 {
|
|
safe := f.buf[:len(f.buf)-matchLen]
|
|
f.toolCallBuf += safe
|
|
f.buf = f.buf[len(f.buf)-matchLen:]
|
|
break
|
|
} else {
|
|
f.toolCallBuf += f.buf
|
|
f.buf = ""
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *StreamToolCallFilter) Flush(onContent func(string), onToolCall func(ToolCall)) {
|
|
if f.inToolCall && len(f.toolCallBuf) > 0 {
|
|
if tc, ok := parseSingleToolCall(f.toolCallBuf); ok {
|
|
idxCopy := f.toolIndex
|
|
tc.Index = &idxCopy
|
|
f.emittedCall = true
|
|
onToolCall(tc)
|
|
} else if tc2, ok2 := parseXMLToolCall("<tool_call>" + f.toolCallBuf + "</tool_call>"); ok2 {
|
|
idxCopy := f.toolIndex
|
|
tc2.Index = &idxCopy
|
|
f.emittedCall = true
|
|
onToolCall(tc2)
|
|
} else {
|
|
onContent("<tool_call>" + f.toolCallBuf)
|
|
}
|
|
f.toolCallBuf = ""
|
|
}
|
|
if len(f.buf) > 0 {
|
|
onContent(f.buf)
|
|
f.buf = ""
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Private & Anti-Fingerprinting Chromium CDP Engine & Browser Bridge
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func findChromiumBinary(customPath string) string {
|
|
if customPath != "" {
|
|
if _, err := exec.LookPath(customPath); err == nil {
|
|
return customPath
|
|
}
|
|
}
|
|
candidates := []string{"chromium", "google-chrome", "chromium-browser", "chrome"}
|
|
for _, c := range candidates {
|
|
if p, err := exec.LookPath(c); err == nil {
|
|
return p
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func dialCDPWebSocket(wsURL string) (net.Conn, *bufio.Reader, error) {
|
|
u, err := url.Parse(wsURL)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
conn, err := net.DialTimeout("tcp", u.Host, 5*time.Second)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
key := "dGhlIHNhbXBsZSBub25jZQ=="
|
|
req := fmt.Sprintf("GET %s HTTP/1.1\r\nHost: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: %s\r\nSec-WebSocket-Version: 13\r\n\r\n", u.RequestURI(), u.Host, key)
|
|
if _, err := conn.Write([]byte(req)); err != nil {
|
|
conn.Close()
|
|
return nil, nil, err
|
|
}
|
|
|
|
reader := bufio.NewReader(conn)
|
|
statusLine, err := reader.ReadString('\n')
|
|
if err != nil || !strings.Contains(statusLine, "101") {
|
|
conn.Close()
|
|
return nil, nil, fmt.Errorf("websocket upgrade failed: %s", statusLine)
|
|
}
|
|
|
|
for {
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil || strings.TrimSpace(line) == "" {
|
|
break
|
|
}
|
|
}
|
|
return conn, reader, nil
|
|
}
|
|
|
|
func sendWSFrame(conn net.Conn, payload []byte) error {
|
|
var header []byte
|
|
header = append(header, 0x81)
|
|
|
|
length := len(payload)
|
|
var maskKey [4]byte
|
|
_, _ = rand.Read(maskKey[:])
|
|
|
|
if length < 126 {
|
|
header = append(header, byte(length)|0x80)
|
|
} else if length < 65536 {
|
|
header = append(header, 126|0x80)
|
|
header = append(header, byte(length>>8), byte(length&0xff))
|
|
} else {
|
|
header = append(header, 127|0x80)
|
|
for i := 7; i >= 0; i-- {
|
|
header = append(header, byte((length>>(i*8))&0xff))
|
|
}
|
|
}
|
|
|
|
header = append(header, maskKey[:]...)
|
|
masked := make([]byte, length)
|
|
for i := 0; i < length; i++ {
|
|
masked[i] = payload[i] ^ maskKey[i%4]
|
|
}
|
|
|
|
if _, err := conn.Write(append(header, masked...)); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readWSFrame(conn net.Conn, reader *bufio.Reader) ([]byte, error) {
|
|
conn.SetReadDeadline(time.Now().Add(25 * time.Second))
|
|
b1, err := reader.ReadByte()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opcode := b1 & 0x0f
|
|
if opcode == 0x08 { // Close frame
|
|
return nil, fmt.Errorf("websocket closed by server")
|
|
}
|
|
|
|
b2, err := reader.ReadByte()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
isMasked := (b2 & 0x80) != 0
|
|
length := int(b2 & 0x7f)
|
|
|
|
if length == 126 {
|
|
var extLen uint16
|
|
if err := binary.Read(reader, binary.BigEndian, &extLen); err != nil {
|
|
return nil, err
|
|
}
|
|
length = int(extLen)
|
|
} else if length == 127 {
|
|
var extLen uint64
|
|
if err := binary.Read(reader, binary.BigEndian, &extLen); err != nil {
|
|
return nil, err
|
|
}
|
|
length = int(extLen)
|
|
}
|
|
|
|
var maskKey [4]byte
|
|
if isMasked {
|
|
if _, err := io.ReadFull(reader, maskKey[:]); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
payload := make([]byte, length)
|
|
if _, err := io.ReadFull(reader, payload); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if isMasked {
|
|
for i := 0; i < length; i++ {
|
|
payload[i] ^= maskKey[i%4]
|
|
}
|
|
}
|
|
|
|
return payload, nil
|
|
}
|
|
|
|
func sendCDPCommand(conn net.Conn, reader *bufio.Reader, method string, params map[string]interface{}) (map[string]interface{}, error) {
|
|
cmdID := int(atomic.AddInt64(&cdpCmdCounter, 1))
|
|
msg := map[string]interface{}{
|
|
"id": cmdID,
|
|
"method": method,
|
|
"params": params,
|
|
}
|
|
b, _ := json.Marshal(msg)
|
|
if err := sendWSFrame(conn, b); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for {
|
|
frame, err := readWSFrame(conn, reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var res map[string]interface{}
|
|
if err := json.Unmarshal(frame, &res); err == nil {
|
|
if idVal, ok := res["id"].(float64); ok && int(idVal) == cmdID {
|
|
return res, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// BrowserBridge manages a persistent private headless Chromium session for TLS & Vercel bypass
|
|
type BrowserBridge struct {
|
|
mu sync.Mutex
|
|
cmd *exec.Cmd
|
|
tmpDir string
|
|
port string
|
|
conn net.Conn
|
|
reader *bufio.Reader
|
|
browserBin string
|
|
userAgent string
|
|
headless bool
|
|
display string
|
|
useXvfb bool
|
|
xvfbCmd *exec.Cmd
|
|
}
|
|
|
|
func findFreeXDisplay() string {
|
|
for d := 99; d < 199; d++ {
|
|
lockFile := fmt.Sprintf("/tmp/.X%d-lock", d)
|
|
sockFile := fmt.Sprintf("/tmp/.X11-unix/X%d", d)
|
|
if _, err := os.Stat(lockFile); os.IsNotExist(err) {
|
|
if _, err2 := os.Stat(sockFile); os.IsNotExist(err2) {
|
|
return fmt.Sprintf(":%d", d)
|
|
}
|
|
}
|
|
}
|
|
return ":99"
|
|
}
|
|
|
|
func NewBrowserBridge(browserBin, userAgent string, headless bool, display string, useXvfb bool) *BrowserBridge {
|
|
if userAgent == "" {
|
|
userAgent = DefaultUserAgent
|
|
}
|
|
return &BrowserBridge{
|
|
browserBin: browserBin,
|
|
userAgent: userAgent,
|
|
headless: headless,
|
|
display: display,
|
|
useXvfb: useXvfb,
|
|
}
|
|
}
|
|
|
|
func (bb *BrowserBridge) Start() error {
|
|
bb.mu.Lock()
|
|
defer bb.mu.Unlock()
|
|
|
|
if bb.conn != nil {
|
|
return nil
|
|
}
|
|
|
|
binPath := findChromiumBinary(bb.browserBin)
|
|
if binPath == "" {
|
|
return fmt.Errorf("no chromium or google-chrome binary found on system")
|
|
}
|
|
|
|
// 1. Manage Virtual X Display (Xvfb) if enabled
|
|
effectiveDisplay := bb.display
|
|
if bb.useXvfb {
|
|
xvfbPath, err := exec.LookPath("Xvfb")
|
|
if err != nil {
|
|
xvfbPath, err = exec.LookPath("Xfbdev")
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "warning: Xvfb / Xfbdev not found on system (install via 'xbps-install -S xorg-server-xvfb' or 'apt install xvfb')\n")
|
|
} else {
|
|
vDisplay := findFreeXDisplay()
|
|
xCmd := exec.Command(xvfbPath, vDisplay, "-screen", "0", "1280x800x24", "-ac", "-nolisten", "tcp")
|
|
if err := xCmd.Start(); err == nil {
|
|
bb.xvfbCmd = xCmd
|
|
effectiveDisplay = vDisplay
|
|
time.Sleep(300 * time.Millisecond)
|
|
fmt.Printf("Virtual X server started on display %s for complete window manager isolation.\n", vDisplay)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "warning: failed to start virtual X server: %v\n", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if effectiveDisplay == "" {
|
|
effectiveDisplay = os.Getenv("DISPLAY")
|
|
}
|
|
|
|
tmpDir, err := os.MkdirTemp("", "th3ist_bridge_*")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create temp profile dir: %w", err)
|
|
}
|
|
bb.tmpDir = tmpDir
|
|
|
|
port, err := getFreePort()
|
|
if err != nil {
|
|
port = "9558"
|
|
}
|
|
bb.port = port
|
|
|
|
args := []string{
|
|
"--remote-debugging-port=" + port,
|
|
"--user-data-dir=" + tmpDir,
|
|
"--disable-gpu",
|
|
"--no-sandbox",
|
|
"--no-first-run",
|
|
"--no-default-browser-check",
|
|
"--disable-extensions",
|
|
"--disable-default-apps",
|
|
"--class=th3ist_hidden",
|
|
"--app=https://t3.chat",
|
|
}
|
|
|
|
if bb.headless || effectiveDisplay == "" {
|
|
args = append([]string{"--headless=new"}, args...)
|
|
} else {
|
|
args = append([]string{"--window-position=-3000,-3000", "--window-size=1280,800"}, args...)
|
|
}
|
|
|
|
cmd := exec.Command(binPath, args...)
|
|
if effectiveDisplay != "" {
|
|
cmd.Env = append(os.Environ(), "DISPLAY="+effectiveDisplay)
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
os.RemoveAll(tmpDir)
|
|
return fmt.Errorf("failed to start browser: %w", err)
|
|
}
|
|
bb.cmd = cmd
|
|
|
|
var pageWSURL string
|
|
for i := 0; i < 25; i++ {
|
|
time.Sleep(300 * time.Millisecond)
|
|
resp, err := http.Get("http://127.0.0.1:" + port + "/json/list")
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var pages []struct {
|
|
Type string `json:"type"`
|
|
WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"`
|
|
}
|
|
_ = json.NewDecoder(resp.Body).Decode(&pages)
|
|
resp.Body.Close()
|
|
|
|
for _, p := range pages {
|
|
if p.Type == "page" && p.WebSocketDebuggerURL != "" {
|
|
pageWSURL = p.WebSocketDebuggerURL
|
|
break
|
|
}
|
|
}
|
|
if pageWSURL != "" {
|
|
break
|
|
}
|
|
}
|
|
|
|
if pageWSURL == "" {
|
|
bb.cleanup()
|
|
return fmt.Errorf("timed out waiting for browser page to initialize")
|
|
}
|
|
|
|
conn, reader, err := dialCDPWebSocket(pageWSURL)
|
|
if err != nil {
|
|
bb.cleanup()
|
|
return fmt.Errorf("failed to connect to browser CDP: %w", err)
|
|
}
|
|
bb.conn = conn
|
|
bb.reader = reader
|
|
|
|
// Enable Runtime domain for binding events
|
|
_, _ = sendCDPCommand(conn, reader, "Runtime.enable", nil)
|
|
|
|
// Add binding for real-time streaming chunks
|
|
_, _ = sendCDPCommand(conn, reader, "Runtime.addBinding", map[string]interface{}{
|
|
"name": "th3istStreamChunk",
|
|
})
|
|
|
|
// Inject stealth scripts
|
|
_, _ = sendCDPCommand(conn, reader, "Page.addScriptToEvaluateOnNewDocument", map[string]interface{}{
|
|
"source": `
|
|
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
if (!window.chrome) {
|
|
window.chrome = { runtime: {}, loadTimes: function() {}, csi: function() {}, app: {} };
|
|
}
|
|
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
|
|
Object.defineProperty(navigator, 'plugins', { get: () => [{ name: 'PDF Viewer' }] });
|
|
`,
|
|
})
|
|
|
|
// Simulate mouse movement in browser to prime interaction
|
|
_, _ = sendCDPCommand(conn, reader, "Input.dispatchMouseEvent", map[string]interface{}{
|
|
"type": "mouseMoved",
|
|
"x": 300,
|
|
"y": 400,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (bb *BrowserBridge) cleanup() {
|
|
if bb.conn != nil {
|
|
bb.conn.Close()
|
|
bb.conn = nil
|
|
}
|
|
if bb.cmd != nil && bb.cmd.Process != nil {
|
|
_ = bb.cmd.Process.Kill()
|
|
bb.cmd = nil
|
|
}
|
|
if bb.xvfbCmd != nil && bb.xvfbCmd.Process != nil {
|
|
_ = bb.xvfbCmd.Process.Kill()
|
|
bb.xvfbCmd = nil
|
|
}
|
|
if bb.tmpDir != "" {
|
|
_ = os.RemoveAll(bb.tmpDir)
|
|
bb.tmpDir = ""
|
|
}
|
|
}
|
|
|
|
func (bb *BrowserBridge) Close() {
|
|
bb.mu.Lock()
|
|
defer bb.mu.Unlock()
|
|
bb.cleanup()
|
|
}
|
|
|
|
func (bb *BrowserBridge) GetFreshHcaptchaToken() (string, error) {
|
|
if bb.conn == nil {
|
|
if err := bb.Start(); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
// Simulate user mouse move
|
|
_, _ = sendCDPCommand(bb.conn, bb.reader, "Input.dispatchMouseEvent", map[string]interface{}{
|
|
"type": "mouseMoved",
|
|
"x": 250 + (int(time.Now().UnixNano()%100)),
|
|
"y": 350 + (int(time.Now().UnixNano()%100)),
|
|
})
|
|
|
|
hcaptchaExpr := fmt.Sprintf(`new Promise((resolve) => {
|
|
const sitekey = %q;
|
|
const startTime = Date.now();
|
|
const checkInterval = setInterval(() => {
|
|
if (window.hcaptcha && typeof window.hcaptcha.render === "function" && typeof window.hcaptcha.execute === "function") {
|
|
clearInterval(checkInterval);
|
|
doRender();
|
|
} else if (Date.now() - startTime > 10000) {
|
|
clearInterval(checkInterval);
|
|
resolve({ token: "", error: "window.hcaptcha not loaded in time" });
|
|
}
|
|
}, 150);
|
|
|
|
function doRender() {
|
|
const n = document.createElement("div");
|
|
n.style.position = "absolute";
|
|
n.style.left = "-9999px";
|
|
document.body.appendChild(n);
|
|
|
|
let finished = false;
|
|
const widgetId = window.hcaptcha.render(n, {
|
|
sitekey: sitekey,
|
|
size: "invisible",
|
|
callback: (token) => {
|
|
if (!finished) {
|
|
finished = true;
|
|
resolve({ token });
|
|
}
|
|
},
|
|
"error-callback": (err) => {
|
|
if (!finished) {
|
|
finished = true;
|
|
resolve({ token: "", error: err });
|
|
}
|
|
}
|
|
});
|
|
|
|
try {
|
|
window.hcaptcha.execute(widgetId);
|
|
} catch (e) {
|
|
resolve({ token: "", error: e.message });
|
|
}
|
|
|
|
setTimeout(() => {
|
|
if (!finished) {
|
|
finished = true;
|
|
const token = window.hcaptcha.getResponse ? window.hcaptcha.getResponse(widgetId) : "";
|
|
resolve({ token: token || "" });
|
|
}
|
|
}, 5000);
|
|
}
|
|
})`, DefaultSitekey)
|
|
|
|
evalRes, err := sendCDPCommand(bb.conn, bb.reader, "Runtime.evaluate", map[string]interface{}{
|
|
"expression": hcaptchaExpr,
|
|
"awaitPromise": true,
|
|
"returnByValue": true,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if resMap, ok := evalRes["result"].(map[string]interface{}); ok {
|
|
if valMap, ok := resMap["result"].(map[string]interface{}); ok {
|
|
if valObj, ok := valMap["value"].(map[string]interface{}); ok {
|
|
if tok, ok := valObj["token"].(string); ok && tok != "" {
|
|
return tok, nil
|
|
}
|
|
}
|
|
if tok, ok := valMap["value"].(string); ok && tok != "" {
|
|
return tok, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("failed to extract token from evaluate response: %v", evalRes)
|
|
}
|
|
|
|
func (bb *BrowserBridge) RotateIdentity() (string, error) {
|
|
if bb.conn == nil {
|
|
if err := bb.Start(); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
rotateExpr := `(async () => {
|
|
try {
|
|
let fpModule = null;
|
|
try {
|
|
const scripts = Array.from(document.querySelectorAll('script[src], link[href]'));
|
|
for (const s of scripts) {
|
|
const u = s.src || s.href || '';
|
|
if (u.includes('fp.esm-') || u.includes('/fp.') || u.includes('fingerprint')) {
|
|
fpModule = (await import(u)).default;
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
|
|
if (!fpModule) {
|
|
try {
|
|
const entries = (typeof performance !== "undefined" && performance.getEntriesByType) ? performance.getEntriesByType('resource') : [];
|
|
for (const e of entries) {
|
|
if (e.name && (e.name.includes('fp.esm-') || e.name.includes('/fp.') || e.name.includes('fingerprint'))) {
|
|
fpModule = (await import(e.name)).default;
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
if (!fpModule) {
|
|
try {
|
|
fpModule = (await import("./assets/fp.esm-Bp3Vx1Qv.js")).default;
|
|
} catch (e) {}
|
|
}
|
|
|
|
let comps = {};
|
|
let version = "3.4.2";
|
|
if (fpModule && typeof fpModule.load === "function") {
|
|
try {
|
|
const fp = await fpModule.load();
|
|
const e = await fp.get();
|
|
if (e && e.components) {
|
|
comps = JSON.parse(JSON.stringify(e.components));
|
|
}
|
|
if (e && e.version) {
|
|
version = e.version;
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
const randHex = (len) => {
|
|
const chars = "0123456789abcdef";
|
|
let out = "";
|
|
for (let i = 0; i < len; i++) {
|
|
out += chars[Math.floor(Math.random() * chars.length)];
|
|
}
|
|
return out;
|
|
};
|
|
|
|
const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
|
|
|
|
if (!comps.canvas || !comps.canvas.value) {
|
|
comps.canvas = { value: { geometry: randHex(32), text: randHex(32) }, duration: randInt(1, 4) };
|
|
}
|
|
if (!comps.audio) {
|
|
comps.audio = { value: Math.random() * 80 + 10, duration: randInt(1, 4) };
|
|
}
|
|
if (!comps.platform) {
|
|
comps.platform = { value: "Linux x86_64", duration: 0 };
|
|
}
|
|
if (!comps.vendor) {
|
|
comps.vendor = { value: "Google Inc.", duration: 0 };
|
|
}
|
|
if (!comps.timezone) {
|
|
comps.timezone = { value: "UTC", duration: 0 };
|
|
}
|
|
if (!comps.languages) {
|
|
comps.languages = { value: [["en-US", "en"]], duration: 0 };
|
|
}
|
|
|
|
if (comps.canvas && comps.canvas.value) {
|
|
comps.canvas.value.geometry = randHex(32);
|
|
comps.canvas.value.text = randHex(32);
|
|
}
|
|
if (comps.audio) {
|
|
comps.audio.value = Math.random() * 100 + 0.1;
|
|
}
|
|
|
|
const cpuCores = [4, 6, 8, 12, 16, 24, 32];
|
|
comps.hardwareConcurrency = { value: cpuCores[Math.floor(Math.random() * cpuCores.length)], duration: 0 };
|
|
|
|
const resolutions = [
|
|
[1920, 1080], [2560, 1440], [1680, 1050], [1920, 1200],
|
|
[1440, 900], [3840, 2160], [1366, 768], [1536, 864]
|
|
];
|
|
comps.screenResolution = { value: resolutions[Math.floor(Math.random() * resolutions.length)], duration: 0 };
|
|
|
|
const devMems = [4, 8, 16, 32];
|
|
comps.deviceMemory = { value: devMems[Math.floor(Math.random() * devMems.length)], duration: 0 };
|
|
|
|
const freshVisitorId = "visitor_" + randHex(16) + "_" + Date.now();
|
|
|
|
try {
|
|
if (typeof localStorage !== "undefined") {
|
|
localStorage.removeItem("t3-visitor-id");
|
|
localStorage.removeItem("t3-anon-visitor");
|
|
}
|
|
if (typeof sessionStorage !== "undefined") {
|
|
sessionStorage.clear();
|
|
}
|
|
} catch (e) {}
|
|
|
|
const t = await fetch("/api/identity", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({
|
|
fingerprint: {
|
|
visitorId: freshVisitorId,
|
|
confidence: { score: 0.99 },
|
|
components: comps,
|
|
version: version
|
|
}
|
|
})
|
|
});
|
|
|
|
if (!t.ok) {
|
|
const errText = await t.text();
|
|
return { success: false, error: "HTTP " + t.status + ": " + errText };
|
|
}
|
|
|
|
const data = await t.json();
|
|
return { success: true, visitorId: data.visitorId || freshVisitorId, requiresSignIn: data.requiresSignIn };
|
|
} catch (err) {
|
|
return { success: false, error: err.message || String(err) };
|
|
}
|
|
})()`
|
|
|
|
evalRes, err := sendCDPCommand(bb.conn, bb.reader, "Runtime.evaluate", map[string]interface{}{
|
|
"expression": rotateExpr,
|
|
"awaitPromise": true,
|
|
"returnByValue": true,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if resMap, ok := evalRes["result"].(map[string]interface{}); ok {
|
|
if valMap, ok := resMap["result"].(map[string]interface{}); ok {
|
|
if valObj, ok := valMap["value"].(map[string]interface{}); ok {
|
|
if success, _ := valObj["success"].(bool); success {
|
|
visitorID, _ := valObj["visitorId"].(string)
|
|
return visitorID, nil
|
|
}
|
|
if errMsg, _ := valObj["error"].(string); errMsg != "" {
|
|
return "", fmt.Errorf("identity rotation endpoint error: %s", errMsg)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("failed to rotate identity: %v", evalRes)
|
|
}
|
|
|
|
func (bb *BrowserBridge) ExecuteFetch(payload T3ChatPayload, deploymentID, clientContext string) (int, string, error) {
|
|
bb.mu.Lock()
|
|
defer bb.mu.Unlock()
|
|
|
|
if bb.conn == nil {
|
|
if err := bb.Start(); err != nil {
|
|
return 0, "", fmt.Errorf("failed to start browser bridge: %w", err)
|
|
}
|
|
}
|
|
|
|
maxAttempts := 4
|
|
var lastStatus int
|
|
var lastText string
|
|
|
|
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
|
// If payload has no fresh hcaptcha token, generate one live in-browser
|
|
if payload.HcaptchaToken == "" {
|
|
tok, _ := bb.GetFreshHcaptchaToken()
|
|
if tok != "" {
|
|
payload.HcaptchaToken = tok
|
|
}
|
|
}
|
|
|
|
payloadJSON, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return 0, "", err
|
|
}
|
|
|
|
fetchExpr := fmt.Sprintf(`(async () => {
|
|
try {
|
|
const resp = await fetch("https://t3.chat/api/chat", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"x-client-context": %q,
|
|
"x-deployment-id": %q
|
|
},
|
|
body: JSON.stringify(%s)
|
|
});
|
|
return { status: resp.status, text: await resp.text() };
|
|
} catch (e) {
|
|
return { status: 500, text: e.message || String(e), error: true };
|
|
}
|
|
})()`, clientContext, deploymentID, string(payloadJSON))
|
|
|
|
evalRes, err := sendCDPCommand(bb.conn, bb.reader, "Runtime.evaluate", map[string]interface{}{
|
|
"expression": fetchExpr,
|
|
"awaitPromise": true,
|
|
"returnByValue": true,
|
|
})
|
|
if err != nil {
|
|
return 0, "", fmt.Errorf("cdp evaluate error: %w", err)
|
|
}
|
|
|
|
var status int
|
|
var text string
|
|
if resMap, ok := evalRes["result"].(map[string]interface{}); ok {
|
|
if valMap, ok := resMap["result"].(map[string]interface{}); ok {
|
|
if valObj, ok := valMap["value"].(map[string]interface{}); ok {
|
|
if stVal, ok := valObj["status"].(float64); ok {
|
|
status = int(stVal)
|
|
}
|
|
text, _ = valObj["text"].(string)
|
|
}
|
|
}
|
|
}
|
|
|
|
lastStatus = status
|
|
lastText = text
|
|
|
|
// Return immediately on success
|
|
if status == http.StatusOK {
|
|
return status, text, nil
|
|
}
|
|
|
|
// If rate limited (429 or quota exceeded), auto-rotate hardware identity and retry immediately
|
|
if isRateLimited(status, text) {
|
|
if attempt < maxAttempts {
|
|
fmt.Printf("Rate limit / 429 encountered (attempt %d/%d); immediately rotating identity and retrying...\n", attempt, maxAttempts)
|
|
newID, rotErr := bb.RotateIdentity()
|
|
if rotErr != nil {
|
|
fmt.Fprintf(os.Stderr, "warning: identity rotation failed on attempt %d: %v\n", attempt, rotErr)
|
|
} else {
|
|
fmt.Printf("Rotated hardware fingerprint to new identity: %s\n", newID)
|
|
}
|
|
|
|
// Mint a fresh hCaptcha token for the new visitor identity
|
|
newTok, tokErr := bb.GetFreshHcaptchaToken()
|
|
if tokErr != nil {
|
|
fmt.Fprintf(os.Stderr, "warning: token minting failed after identity rotation: %v\n", tokErr)
|
|
payload.HcaptchaToken = ""
|
|
} else {
|
|
payload.HcaptchaToken = newTok
|
|
}
|
|
|
|
// Regenerate session & response IDs for a clean turn
|
|
payload.ConvexSessionID = GenerateUUID()
|
|
payload.ResponseMessageID = GenerateUUID()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
continue
|
|
}
|
|
} else if (status == http.StatusForbidden || strings.Contains(strings.ToLower(text), "captcha")) && attempt < maxAttempts {
|
|
// Captcha verification failed; mint fresh token and retry immediately
|
|
fmt.Printf("Captcha validation failure encountered (attempt %d/%d); refreshing hCaptcha token and retrying...\n", attempt, maxAttempts)
|
|
newTok, tokErr := bb.GetFreshHcaptchaToken()
|
|
if tokErr == nil && newTok != "" {
|
|
payload.HcaptchaToken = newTok
|
|
payload.ResponseMessageID = GenerateUUID()
|
|
time.Sleep(100 * time.Millisecond)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// If non-retryable client error (e.g. 400 bad request), return immediately
|
|
if status != 0 {
|
|
return status, text, nil
|
|
}
|
|
}
|
|
|
|
if lastStatus != 0 {
|
|
return lastStatus, lastText, nil
|
|
}
|
|
|
|
return 0, "", fmt.Errorf("evaluate failed without valid status: %s", lastText)
|
|
}
|
|
|
|
func (bb *BrowserBridge) ExecuteStreamFetch(payload T3ChatPayload, deploymentID, clientContext string, onStatus func(int), onChunk func(string)) (int, string, error) {
|
|
bb.mu.Lock()
|
|
defer bb.mu.Unlock()
|
|
|
|
if bb.conn == nil {
|
|
if err := bb.Start(); err != nil {
|
|
return 0, "", fmt.Errorf("failed to start browser bridge: %w", err)
|
|
}
|
|
}
|
|
|
|
// Ensure runtime and binding are enabled
|
|
_, _ = sendCDPCommand(bb.conn, bb.reader, "Runtime.enable", nil)
|
|
_, _ = sendCDPCommand(bb.conn, bb.reader, "Runtime.addBinding", map[string]interface{}{
|
|
"name": "th3istStreamChunk",
|
|
})
|
|
|
|
maxAttempts := 4
|
|
var lastStatus int
|
|
var lastText string
|
|
|
|
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
|
// If payload has no fresh hcaptcha token, generate one live in-browser
|
|
if payload.HcaptchaToken == "" {
|
|
tok, _ := bb.GetFreshHcaptchaToken()
|
|
if tok != "" {
|
|
payload.HcaptchaToken = tok
|
|
}
|
|
}
|
|
|
|
payloadJSON, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return 0, "", err
|
|
}
|
|
|
|
streamID := GenerateUUID()
|
|
|
|
fetchExpr := fmt.Sprintf(`(async () => {
|
|
const streamId = %q;
|
|
try {
|
|
const resp = await fetch("https://t3.chat/api/chat", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"x-client-context": %q,
|
|
"x-deployment-id": %q
|
|
},
|
|
body: JSON.stringify(%s)
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
const errText = await resp.text();
|
|
if (window.th3istStreamChunk) {
|
|
window.th3istStreamChunk(JSON.stringify({
|
|
streamId: streamId,
|
|
type: "error",
|
|
status: resp.status,
|
|
text: errText
|
|
}));
|
|
}
|
|
return { status: resp.status, text: errText, error: true };
|
|
}
|
|
|
|
if (window.th3istStreamChunk) {
|
|
window.th3istStreamChunk(JSON.stringify({
|
|
streamId: streamId,
|
|
type: "status",
|
|
status: resp.status
|
|
}));
|
|
}
|
|
|
|
const reader = resp.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
const chunkStr = decoder.decode(value, { stream: true });
|
|
if (window.th3istStreamChunk) {
|
|
window.th3istStreamChunk(JSON.stringify({
|
|
streamId: streamId,
|
|
type: "chunk",
|
|
data: chunkStr
|
|
}));
|
|
}
|
|
}
|
|
|
|
if (window.th3istStreamChunk) {
|
|
window.th3istStreamChunk(JSON.stringify({
|
|
streamId: streamId,
|
|
type: "done"
|
|
}));
|
|
}
|
|
return { status: 200, done: true };
|
|
} catch (e) {
|
|
if (window.th3istStreamChunk) {
|
|
window.th3istStreamChunk(JSON.stringify({
|
|
streamId: streamId,
|
|
type: "error",
|
|
status: 500,
|
|
text: e.message || String(e)
|
|
}));
|
|
}
|
|
return { status: 500, text: e.message || String(e), error: true };
|
|
}
|
|
})()`, streamID, clientContext, deploymentID, string(payloadJSON))
|
|
|
|
cmdID := int(atomic.AddInt64(&cdpCmdCounter, 1))
|
|
msg := map[string]interface{}{
|
|
"id": cmdID,
|
|
"method": "Runtime.evaluate",
|
|
"params": map[string]interface{}{
|
|
"expression": fetchExpr,
|
|
"awaitPromise": true,
|
|
"returnByValue": true,
|
|
},
|
|
}
|
|
b, _ := json.Marshal(msg)
|
|
if err := sendWSFrame(bb.conn, b); err != nil {
|
|
return 0, "", err
|
|
}
|
|
|
|
var streamStatus int
|
|
var streamErrText string
|
|
evalDone := false
|
|
streamDone := false
|
|
|
|
for !evalDone || !streamDone {
|
|
frame, err := readWSFrame(bb.conn, bb.reader)
|
|
if err != nil {
|
|
return 0, "", err
|
|
}
|
|
|
|
var res map[string]interface{}
|
|
if err := json.Unmarshal(frame, &res); err != nil {
|
|
continue
|
|
}
|
|
|
|
if idVal, ok := res["id"].(float64); ok && int(idVal) == cmdID {
|
|
evalDone = true
|
|
if resMap, ok := res["result"].(map[string]interface{}); ok {
|
|
if valMap, ok := resMap["result"].(map[string]interface{}); ok {
|
|
if valObj, ok := valMap["value"].(map[string]interface{}); ok {
|
|
if stVal, ok := valObj["status"].(float64); ok {
|
|
if streamStatus == 0 {
|
|
streamStatus = int(stVal)
|
|
}
|
|
}
|
|
if txt, ok := valObj["text"].(string); ok && streamErrText == "" {
|
|
streamErrText = txt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if method, ok := res["method"].(string); ok && method == "Runtime.bindingCalled" {
|
|
if params, ok := res["params"].(map[string]interface{}); ok {
|
|
if name, _ := params["name"].(string); name == "th3istStreamChunk" {
|
|
if payloadStr, ok := params["payload"].(string); ok {
|
|
var ev struct {
|
|
StreamID string `json:"streamId"`
|
|
Type string `json:"type"`
|
|
Status int `json:"status"`
|
|
Data string `json:"data"`
|
|
Text string `json:"text"`
|
|
}
|
|
if json.Unmarshal([]byte(payloadStr), &ev) == nil {
|
|
if ev.StreamID == streamID {
|
|
switch ev.Type {
|
|
case "status":
|
|
streamStatus = ev.Status
|
|
if onStatus != nil {
|
|
onStatus(ev.Status)
|
|
}
|
|
case "chunk":
|
|
if ev.Data != "" && onChunk != nil {
|
|
onChunk(ev.Data)
|
|
}
|
|
case "done":
|
|
streamDone = true
|
|
case "error":
|
|
streamStatus = ev.Status
|
|
streamErrText = ev.Text
|
|
streamDone = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
lastStatus = streamStatus
|
|
lastText = streamErrText
|
|
|
|
// If 200 OK, stream completed successfully in real time
|
|
if streamStatus == http.StatusOK {
|
|
return streamStatus, "", nil
|
|
}
|
|
|
|
// Rate limit / 429 auto-rotation retry
|
|
if isRateLimited(streamStatus, streamErrText) {
|
|
if attempt < maxAttempts {
|
|
fmt.Printf("Rate limit / 429 encountered in streaming (attempt %d/%d); immediately rotating identity and retrying...\n", attempt, maxAttempts)
|
|
newID, rotErr := bb.RotateIdentity()
|
|
if rotErr != nil {
|
|
fmt.Fprintf(os.Stderr, "warning: identity rotation failed on attempt %d: %v\n", attempt, rotErr)
|
|
} else {
|
|
fmt.Printf("Rotated hardware fingerprint to new identity: %s\n", newID)
|
|
}
|
|
|
|
newTok, tokErr := bb.GetFreshHcaptchaToken()
|
|
if tokErr != nil {
|
|
fmt.Fprintf(os.Stderr, "warning: token minting failed after identity rotation: %v\n", tokErr)
|
|
payload.HcaptchaToken = ""
|
|
} else {
|
|
payload.HcaptchaToken = newTok
|
|
}
|
|
|
|
payload.ConvexSessionID = GenerateUUID()
|
|
payload.ResponseMessageID = GenerateUUID()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
continue
|
|
}
|
|
} else if (streamStatus == http.StatusForbidden || strings.Contains(strings.ToLower(streamErrText), "captcha")) && attempt < maxAttempts {
|
|
fmt.Printf("Captcha validation failure encountered in streaming (attempt %d/%d); refreshing token and retrying...\n", attempt, maxAttempts)
|
|
newTok, tokErr := bb.GetFreshHcaptchaToken()
|
|
if tokErr == nil && newTok != "" {
|
|
payload.HcaptchaToken = newTok
|
|
payload.ResponseMessageID = GenerateUUID()
|
|
time.Sleep(100 * time.Millisecond)
|
|
continue
|
|
}
|
|
}
|
|
|
|
if streamStatus != 0 {
|
|
return streamStatus, streamErrText, nil
|
|
}
|
|
}
|
|
|
|
if lastStatus != 0 {
|
|
return lastStatus, lastText, nil
|
|
}
|
|
|
|
return 0, "", fmt.Errorf("stream failed without status: %s", lastText)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T3 Gateway Service
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type T3Gateway struct {
|
|
mu sync.RWMutex
|
|
targetURL string
|
|
clientContext string
|
|
deploymentID string
|
|
cookie string
|
|
hcaptchaToken string
|
|
defaultModel string
|
|
browserBin string
|
|
autoCapture bool
|
|
client *http.Client
|
|
bridge *BrowserBridge
|
|
}
|
|
|
|
func NewT3Gateway(targetURL, clientContext, deploymentID, cookie, hcaptchaToken, defaultModel, browserBin string, autoCapture bool, headless bool, display string, useXvfb bool) *T3Gateway {
|
|
if targetURL == "" {
|
|
targetURL = DefaultTargetURL
|
|
}
|
|
if clientContext == "" {
|
|
clientContext = DefaultClientContext
|
|
}
|
|
if deploymentID == "" {
|
|
deploymentID = DefaultDeploymentID
|
|
}
|
|
if cookie == "" {
|
|
cookie = DefaultCookie
|
|
}
|
|
if hcaptchaToken == "" {
|
|
hcaptchaToken = DefaultHcaptchaToken
|
|
}
|
|
if defaultModel == "" {
|
|
defaultModel = DefaultModel
|
|
}
|
|
|
|
var bridge *BrowserBridge
|
|
if autoCapture {
|
|
bridge = NewBrowserBridge(browserBin, ConfiguredUserAgent, headless, display, useXvfb)
|
|
}
|
|
|
|
return &T3Gateway{
|
|
targetURL: targetURL,
|
|
clientContext: clientContext,
|
|
deploymentID: deploymentID,
|
|
cookie: cookie,
|
|
hcaptchaToken: hcaptchaToken,
|
|
defaultModel: defaultModel,
|
|
browserBin: browserBin,
|
|
autoCapture: autoCapture,
|
|
client: &http.Client{Timeout: 300 * time.Second},
|
|
bridge: bridge,
|
|
}
|
|
}
|
|
|
|
func (g *T3Gateway) UpdateCredentials(cookie, token, deploymentID, clientContext string) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
if cookie != "" {
|
|
g.cookie = cookie
|
|
}
|
|
if token != "" {
|
|
g.hcaptchaToken = token
|
|
}
|
|
if deploymentID != "" {
|
|
g.deploymentID = deploymentID
|
|
}
|
|
if clientContext != "" {
|
|
g.clientContext = clientContext
|
|
}
|
|
}
|
|
|
|
func (g *T3Gateway) GetCredentials() (cookie, token, deploymentID, clientContext string) {
|
|
g.mu.RLock()
|
|
defer g.mu.RUnlock()
|
|
return g.cookie, g.hcaptchaToken, g.deploymentID, g.clientContext
|
|
}
|
|
|
|
func (g *T3Gateway) ListModels() []ModelItem {
|
|
now := time.Now().Unix()
|
|
return []ModelItem{
|
|
{ID: "gemini-3.5-flash-lite", Object: "model", Created: now, OwnedBy: "google"},
|
|
{ID: "gemini-2.5-pro", Object: "model", Created: now, OwnedBy: "google"},
|
|
{ID: "gemini-2.5-flash", Object: "model", Created: now, OwnedBy: "google"},
|
|
{ID: "claude-3-7-sonnet", Object: "model", Created: now, OwnedBy: "anthropic"},
|
|
{ID: "claude-3-5-sonnet", Object: "model", Created: now, OwnedBy: "anthropic"},
|
|
{ID: "gpt-4o", Object: "model", Created: now, OwnedBy: "openai"},
|
|
{ID: "gpt-4o-mini", Object: "model", Created: now, OwnedBy: "openai"},
|
|
{ID: "o3-mini", Object: "model", Created: now, OwnedBy: "openai"},
|
|
{ID: "deepseek-r1", Object: "model", Created: now, OwnedBy: "deepseek"},
|
|
{ID: "deepseek-v3", Object: "model", Created: now, OwnedBy: "deepseek"},
|
|
}
|
|
}
|
|
|
|
func (g *T3Gateway) extractSessionID(cookieStr string) string {
|
|
if strings.Contains(cookieStr, "convex-session-id=") {
|
|
parts := strings.Split(cookieStr, "convex-session-id=")
|
|
if len(parts) > 1 {
|
|
semiIdx := strings.Index(parts[1], ";")
|
|
if semiIdx != -1 {
|
|
return parts[1][:semiIdx]
|
|
}
|
|
return parts[1]
|
|
}
|
|
}
|
|
return GenerateUUID()
|
|
}
|
|
|
|
func (g *T3Gateway) HandleModels(w http.ResponseWriter, r *http.Request) {
|
|
EnableCORS(w)
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
models := g.ListModels()
|
|
resp := ModelsResponse{
|
|
Object: "list",
|
|
Data: models,
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
func (g *T3Gateway) HandleToken(w http.ResponseWriter, r *http.Request) {
|
|
EnableCORS(w)
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
if r.Method == http.MethodPost {
|
|
var p TokenPayload
|
|
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
|
|
http.Error(w, fmt.Sprintf(`{"error":"Invalid json payload: %v"}`, err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
g.UpdateCredentials(p.Cookie, p.HcaptchaToken, p.DeploymentID, p.ClientContext)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"status":"updated"}`))
|
|
return
|
|
}
|
|
|
|
curCookie, curToken, curDpl, curCtx := g.GetCredentials()
|
|
maskedCookie := ""
|
|
if len(curCookie) > 20 {
|
|
maskedCookie = curCookie[:10] + "..." + curCookie[len(curCookie)-10:]
|
|
}
|
|
maskedToken := ""
|
|
if len(curToken) > 20 {
|
|
maskedToken = curToken[:10] + "..." + curToken[len(curToken)-10:]
|
|
}
|
|
|
|
resp := map[string]interface{}{
|
|
"hasHcaptchaToken": curToken != "",
|
|
"tokenLength": len(curToken),
|
|
"tokenMasked": maskedToken,
|
|
"hasCookie": curCookie != "",
|
|
"cookieMasked": maskedCookie,
|
|
"deploymentId": curDpl,
|
|
"clientContextSet": curCtx != "",
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
func (g *T3Gateway) HandleChatCompletions(w http.ResponseWriter, r *http.Request) {
|
|
EnableCORS(w)
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var req ChatCompletionRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, fmt.Sprintf(`{"error":"Invalid request payload: %v"}`, err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
modelName := req.Model
|
|
if modelName == "" {
|
|
modelName = g.defaultModel
|
|
}
|
|
|
|
curCookie, curToken, curDpl, curCtx := g.GetCredentials()
|
|
|
|
effUA := EffectiveUserAgent(r)
|
|
effCookie := curCookie
|
|
if c := r.Header.Get("X-Cookie"); c != "" {
|
|
effCookie = c
|
|
} else if c := r.Header.Get("Cookie"); c != "" {
|
|
effCookie = c
|
|
}
|
|
|
|
var customApiKey *T3LocalApiKey
|
|
rawKey := r.Header.Get("X-Api-Key")
|
|
if rawKey == "" {
|
|
rawKey = r.Header.Get("api-key")
|
|
}
|
|
if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "Bearer ") {
|
|
token := strings.TrimPrefix(auth, "Bearer ")
|
|
if strings.Contains(token, "=") {
|
|
effCookie = token
|
|
} else if len(token) > 10 {
|
|
rawKey = token
|
|
}
|
|
}
|
|
|
|
if rawKey != "" {
|
|
provider := "OpenAI"
|
|
if strings.HasPrefix(rawKey, "sk-ant-") {
|
|
provider = "Anthropic"
|
|
} else if strings.HasPrefix(rawKey, "sk-or-") {
|
|
provider = "OpenRouter"
|
|
} else if strings.HasPrefix(rawKey, "AIza") {
|
|
provider = "Google"
|
|
} else if strings.HasPrefix(rawKey, "sk-") {
|
|
provider = "OpenAI"
|
|
}
|
|
customApiKey = &T3LocalApiKey{
|
|
Provider: provider,
|
|
Key: rawKey,
|
|
DefaultMode: "priority",
|
|
ModelModes: map[string]string{},
|
|
}
|
|
}
|
|
|
|
effDeploymentID := curDpl
|
|
if d := r.Header.Get("X-Deployment-Id"); d != "" {
|
|
effDeploymentID = d
|
|
}
|
|
|
|
effClientContext := curCtx
|
|
if cc := r.Header.Get("X-Client-Context"); cc != "" {
|
|
effClientContext = cc
|
|
}
|
|
|
|
effHcaptchaToken := curToken
|
|
if ht := r.Header.Get("X-Hcaptcha-Token"); ht != "" {
|
|
effHcaptchaToken = ht
|
|
}
|
|
|
|
sessionID := g.extractSessionID(effCookie)
|
|
threadID := GenerateUUID()
|
|
responseMsgID := GenerateUUID()
|
|
nowMs := float64(time.Now().UnixMilli())
|
|
|
|
processedMsgs, _, _ := TransformMessages(req)
|
|
|
|
var t3Messages []T3Message
|
|
for _, m := range processedMsgs {
|
|
cStr := m.GetContentString()
|
|
role := m.Role
|
|
if role != "user" && role != "assistant" {
|
|
role = "user"
|
|
}
|
|
t3Messages = append(t3Messages, T3Message{
|
|
ID: GenerateUUID(),
|
|
Parts: []T3MessagePart{
|
|
{Type: "text", Text: cStr},
|
|
},
|
|
Role: role,
|
|
Attachments: []interface{}{},
|
|
})
|
|
}
|
|
|
|
effReasoningEffort := "low"
|
|
if req.ReasoningEffort != "" {
|
|
effReasoningEffort = req.ReasoningEffort
|
|
}
|
|
|
|
t3Payload := T3ChatPayload{
|
|
Messages: t3Messages,
|
|
ThreadMetadata: T3ThreadMetadata{
|
|
ID: threadID,
|
|
Title: "Chat",
|
|
},
|
|
ClientAuth: T3ClientAuth{
|
|
IsSignedIn: false,
|
|
},
|
|
ResponseMessageID: responseMsgID,
|
|
Model: modelName,
|
|
ConvexSessionID: sessionID,
|
|
ModelParams: T3ModelParams{
|
|
ReasoningEffort: effReasoningEffort,
|
|
IncludeSearch: false,
|
|
SearchLimit: 1,
|
|
},
|
|
Preferences: map[string]interface{}{},
|
|
UserConfiguration: T3UserConfiguration{
|
|
CreationTime: nowMs,
|
|
CurrentModelParameters: T3UserConfigParams{
|
|
IncludeSearch: false,
|
|
ReasoningEffort: effReasoningEffort,
|
|
},
|
|
CurrentlySelectedModel: modelName,
|
|
LatestTOSDate: int64(nowMs),
|
|
},
|
|
HcaptchaToken: effHcaptchaToken,
|
|
ApiKey: customApiKey,
|
|
UserInfo: T3UserInfo{
|
|
Timezone: "Europe/Kyiv",
|
|
Locale: "en-US",
|
|
},
|
|
IsEphemeral: false,
|
|
}
|
|
|
|
completionID := "chatcmpl-" + GenerateUUID()
|
|
createdTime := time.Now().Unix()
|
|
|
|
// 1. Browser Bridge Execution (TLS & Vercel bypass + auto-generated fresh hCaptcha token)
|
|
if g.bridge != nil {
|
|
if req.Stream {
|
|
var streamer *Streamer
|
|
initStreamer := func() {
|
|
if streamer == nil {
|
|
flusher, _ := w.(http.Flusher)
|
|
streamer = NewStreamer(w, flusher, completionID, createdTime, modelName)
|
|
streamer.Role()
|
|
}
|
|
}
|
|
|
|
thinkingFilter := NewStreamThinkingFilter()
|
|
toolFilter := NewStreamToolCallFilter()
|
|
var fullAccumulatedText strings.Builder
|
|
lastToolCallArgs := map[string]string{}
|
|
emittedToolCallIDs := map[string]bool{}
|
|
lineBuf := &StreamLineBuffer{}
|
|
|
|
status, errBody, err := g.bridge.ExecuteStreamFetch(
|
|
t3Payload,
|
|
effDeploymentID,
|
|
effClientContext,
|
|
func(st int) {
|
|
if st == http.StatusOK {
|
|
initStreamer()
|
|
}
|
|
},
|
|
func(rawChunk string) {
|
|
initStreamer()
|
|
lineBuf.Feed(rawChunk, func(line string) {
|
|
g.processStreamLine(line, streamer, thinkingFilter, toolFilter, &fullAccumulatedText, lastToolCallArgs, emittedToolCallIDs)
|
|
})
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
if streamer == nil {
|
|
http.Error(w, fmt.Sprintf(`{"error":"Browser bridge streaming error: %v"}`, err), http.StatusBadGateway)
|
|
}
|
|
return
|
|
}
|
|
|
|
if status != http.StatusOK {
|
|
if streamer == nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
w.Write([]byte(errBody))
|
|
}
|
|
return
|
|
}
|
|
|
|
initStreamer()
|
|
|
|
lineBuf.Flush(func(line string) {
|
|
g.processStreamLine(line, streamer, thinkingFilter, toolFilter, &fullAccumulatedText, lastToolCallArgs, emittedToolCallIDs)
|
|
})
|
|
|
|
thinkingFilter.Flush(
|
|
func(text string) {
|
|
toolFilter.Feed(text,
|
|
func(t string) {
|
|
fullAccumulatedText.WriteString(t)
|
|
streamer.Content(t)
|
|
},
|
|
func(tc ToolCall) {
|
|
emittedToolCallIDs[tc.ID] = true
|
|
streamer.ToolCallDelta(tc)
|
|
},
|
|
)
|
|
},
|
|
func(reasoning string) {
|
|
streamer.Reasoning(reasoning)
|
|
},
|
|
)
|
|
|
|
toolFilter.Flush(
|
|
func(text string) {
|
|
fullAccumulatedText.WriteString(text)
|
|
streamer.Content(text)
|
|
},
|
|
func(tc ToolCall) {
|
|
emittedToolCallIDs[tc.ID] = true
|
|
streamer.ToolCallDelta(tc)
|
|
},
|
|
)
|
|
|
|
finishReason := "stop"
|
|
if len(emittedToolCallIDs) > 0 || toolFilter.emittedCall {
|
|
finishReason = "tool_calls"
|
|
} else {
|
|
if tc, _, found := DetectToolCalls(fullAccumulatedText.String()); found && len(tc) > 0 {
|
|
finishReason = "tool_calls"
|
|
}
|
|
}
|
|
|
|
streamer.Finish(finishReason)
|
|
streamer.Done()
|
|
return
|
|
}
|
|
|
|
status, responseBody, err := g.bridge.ExecuteFetch(t3Payload, effDeploymentID, effClientContext)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf(`{"error":"Browser bridge error: %v"}`, err), http.StatusBadGateway)
|
|
return
|
|
}
|
|
if status != http.StatusOK {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
w.Write([]byte(responseBody))
|
|
return
|
|
}
|
|
|
|
g.handleNonStreamingResponse(w, strings.NewReader(responseBody), completionID, createdTime, modelName)
|
|
return
|
|
}
|
|
|
|
// 2. Direct HTTP Client fallback
|
|
jsonPayload, err := json.Marshal(t3Payload)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf(`{"error":"Failed to marshal upstream payload: %v"}`, err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
traceHex := GenerateHex(16)
|
|
spanHex := GenerateHex(8)
|
|
b3Header := fmt.Sprintf("%s-%s-1-%s", traceHex, spanHex, spanHex)
|
|
traceparentHeader := fmt.Sprintf("00-%s-%s-01", traceHex, spanHex)
|
|
|
|
makeReq := func() (*http.Request, error) {
|
|
httpReq, err := http.NewRequest("POST", g.targetURL, bytes.NewBuffer(jsonPayload))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
httpReq.Header.Set("User-Agent", effUA)
|
|
httpReq.Header.Set("Accept", "*/*")
|
|
httpReq.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
|
httpReq.Header.Set("Origin", "https://t3.chat")
|
|
httpReq.Header.Set("Referer", fmt.Sprintf("https://t3.chat/chat/%s", threadID))
|
|
httpReq.Header.Set("x-client-context", effClientContext)
|
|
httpReq.Header.Set("x-deployment-id", effDeploymentID)
|
|
httpReq.Header.Set("b3", b3Header)
|
|
httpReq.Header.Set("traceparent", traceparentHeader)
|
|
if effCookie != "" {
|
|
httpReq.Header.Set("Cookie", effCookie)
|
|
}
|
|
return httpReq, nil
|
|
}
|
|
|
|
resp, err := DoWithFibonacciRetry(g.client, makeReq, 3)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf(`{"error":"Upstream call failed: %v"}`, err), http.StatusBadGateway)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if req.Stream {
|
|
g.handleStreamingResponse(w, resp.Body, completionID, createdTime, modelName)
|
|
} else {
|
|
g.handleNonStreamingResponse(w, resp.Body, completionID, createdTime, modelName)
|
|
}
|
|
}
|
|
|
|
func (g *T3Gateway) handleStreamingResponse(w http.ResponseWriter, body io.Reader, completionID string, createdTime int64, modelName string) {
|
|
flusher, _ := w.(http.Flusher)
|
|
streamer := NewStreamer(w, flusher, completionID, createdTime, modelName)
|
|
streamer.Role()
|
|
|
|
scanner := bufio.NewScanner(body)
|
|
thinkingFilter := NewStreamThinkingFilter()
|
|
toolFilter := NewStreamToolCallFilter()
|
|
|
|
var fullAccumulatedText strings.Builder
|
|
lastToolCallArgs := map[string]string{}
|
|
emittedToolCallIDs := map[string]bool{}
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
g.processStreamLine(line, streamer, thinkingFilter, toolFilter, &fullAccumulatedText, lastToolCallArgs, emittedToolCallIDs)
|
|
}
|
|
|
|
thinkingFilter.Flush(
|
|
func(text string) {
|
|
toolFilter.Feed(text,
|
|
func(t string) {
|
|
fullAccumulatedText.WriteString(t)
|
|
streamer.Content(t)
|
|
},
|
|
func(tc ToolCall) {
|
|
emittedToolCallIDs[tc.ID] = true
|
|
streamer.ToolCallDelta(tc)
|
|
},
|
|
)
|
|
},
|
|
func(reasoning string) {
|
|
streamer.Reasoning(reasoning)
|
|
},
|
|
)
|
|
|
|
toolFilter.Flush(
|
|
func(text string) {
|
|
fullAccumulatedText.WriteString(text)
|
|
streamer.Content(text)
|
|
},
|
|
func(tc ToolCall) {
|
|
emittedToolCallIDs[tc.ID] = true
|
|
streamer.ToolCallDelta(tc)
|
|
},
|
|
)
|
|
|
|
finishReason := "stop"
|
|
if len(emittedToolCallIDs) > 0 || toolFilter.emittedCall {
|
|
finishReason = "tool_calls"
|
|
} else {
|
|
if tc, _, found := DetectToolCalls(fullAccumulatedText.String()); found && len(tc) > 0 {
|
|
finishReason = "tool_calls"
|
|
}
|
|
}
|
|
|
|
streamer.Finish(finishReason)
|
|
streamer.Done()
|
|
}
|
|
|
|
func (g *T3Gateway) processStreamLine(line string, streamer *Streamer, filter *StreamThinkingFilter, toolFilter *StreamToolCallFilter, fullText *strings.Builder, lastToolCallArgs map[string]string, emittedToolCallIDs map[string]bool) {
|
|
line = strings.TrimRight(line, "\r\n")
|
|
if line == "" {
|
|
return
|
|
}
|
|
|
|
feedContent := func(chunk string) {
|
|
filter.Feed(chunk,
|
|
func(t string) {
|
|
toolFilter.Feed(t,
|
|
func(plain string) {
|
|
fullText.WriteString(plain)
|
|
streamer.Content(plain)
|
|
},
|
|
func(tc ToolCall) {
|
|
emittedToolCallIDs[tc.ID] = true
|
|
streamer.ToolCallDelta(tc)
|
|
},
|
|
)
|
|
},
|
|
func(r string) {
|
|
streamer.Reasoning(r)
|
|
},
|
|
)
|
|
}
|
|
|
|
// 1. Vercel AI SDK Data Stream protocol lines:
|
|
// 0:"text chunk"
|
|
if strings.HasPrefix(line, "0:") {
|
|
raw := line[2:]
|
|
var textChunk string
|
|
if err := json.Unmarshal([]byte(raw), &textChunk); err == nil {
|
|
feedContent(textChunk)
|
|
} else {
|
|
feedContent(raw)
|
|
}
|
|
return
|
|
}
|
|
|
|
// b:"reasoning chunk" or b:{"type":"reasoning","textDelta":"..."} or g:"..."
|
|
if strings.HasPrefix(line, "b:") || strings.HasPrefix(line, "g:") {
|
|
raw := line[2:]
|
|
var rStr string
|
|
if err := json.Unmarshal([]byte(raw), &rStr); err == nil {
|
|
streamer.Reasoning(rStr)
|
|
} else {
|
|
var rObj struct {
|
|
TextDelta string `json:"textDelta"`
|
|
Reasoning string `json:"reasoning"`
|
|
Text string `json:"text"`
|
|
}
|
|
if json.Unmarshal([]byte(raw), &rObj) == nil {
|
|
if rObj.TextDelta != "" {
|
|
streamer.Reasoning(rObj.TextDelta)
|
|
} else if rObj.Reasoning != "" {
|
|
streamer.Reasoning(rObj.Reasoning)
|
|
} else if rObj.Text != "" {
|
|
streamer.Reasoning(rObj.Text)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// 8:[{"toolCallId":"...","toolName":"...","args":{...}}]
|
|
if strings.HasPrefix(line, "8:") {
|
|
raw := line[2:]
|
|
var rawToolCalls []struct {
|
|
ToolCallID string `json:"toolCallId"`
|
|
ToolName string `json:"toolName"`
|
|
Args interface{} `json:"args"`
|
|
}
|
|
if json.Unmarshal([]byte(raw), &rawToolCalls) == nil {
|
|
for idx, rtc := range rawToolCalls {
|
|
key := rtc.ToolCallID
|
|
if key == "" {
|
|
key = fmt.Sprintf("idx_%d", idx)
|
|
}
|
|
var argsStr string
|
|
if str, ok := rtc.Args.(string); ok {
|
|
argsStr = str
|
|
} else {
|
|
b, _ := json.Marshal(rtc.Args)
|
|
argsStr = string(b)
|
|
}
|
|
prevArgs := lastToolCallArgs[key]
|
|
if !emittedToolCallIDs[key] {
|
|
emittedToolCallIDs[key] = true
|
|
lastToolCallArgs[key] = argsStr
|
|
idxCopy := idx
|
|
streamer.ToolCallDelta(ToolCall{
|
|
Index: &idxCopy,
|
|
ID: rtc.ToolCallID,
|
|
Type: "function",
|
|
Function: ToolCallFunction{
|
|
Name: rtc.ToolName,
|
|
Arguments: argsStr,
|
|
},
|
|
})
|
|
} else if len(argsStr) > len(prevArgs) {
|
|
argDelta := argsStr[len(prevArgs):]
|
|
lastToolCallArgs[key] = argsStr
|
|
idxCopy := idx
|
|
streamer.ToolCallDelta(ToolCall{
|
|
Index: &idxCopy,
|
|
Function: ToolCallFunction{
|
|
Arguments: argDelta,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// 2. Standard SSE lines:
|
|
if strings.HasPrefix(line, "data: ") {
|
|
dataStr := strings.TrimPrefix(line, "data: ")
|
|
if dataStr == "[DONE]" {
|
|
return
|
|
}
|
|
|
|
var sseChunk struct {
|
|
Type string `json:"type"`
|
|
Delta string `json:"delta"`
|
|
Text string `json:"text"`
|
|
ReasoningContent string `json:"reasoning_content"`
|
|
Choices []struct {
|
|
Delta struct {
|
|
Content string `json:"content"`
|
|
ReasoningContent string `json:"reasoning_content"`
|
|
ToolCalls []ToolCall `json:"tool_calls"`
|
|
} `json:"delta"`
|
|
} `json:"choices"`
|
|
}
|
|
|
|
if json.Unmarshal([]byte(dataStr), &sseChunk) == nil {
|
|
if sseChunk.Type == "text-delta" && sseChunk.Delta != "" {
|
|
feedContent(sseChunk.Delta)
|
|
} else if len(sseChunk.Choices) > 0 {
|
|
delta := sseChunk.Choices[0].Delta
|
|
if delta.ReasoningContent != "" {
|
|
streamer.Reasoning(delta.ReasoningContent)
|
|
}
|
|
for _, tc := range delta.ToolCalls {
|
|
emittedToolCallIDs[tc.ID] = true
|
|
streamer.ToolCallDelta(tc)
|
|
}
|
|
if delta.Content != "" {
|
|
feedContent(delta.Content)
|
|
}
|
|
} else if sseChunk.Text != "" {
|
|
feedContent(sseChunk.Text)
|
|
}
|
|
} else {
|
|
var rawStr string
|
|
if json.Unmarshal([]byte(dataStr), &rawStr) == nil {
|
|
feedContent(rawStr)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
func (g *T3Gateway) handleNonStreamingResponse(w http.ResponseWriter, body io.Reader, completionID string, createdTime int64, modelName string) {
|
|
scanner := bufio.NewScanner(body)
|
|
var textBuilder strings.Builder
|
|
var reasoningBuilder strings.Builder
|
|
var parsedToolCalls []ToolCall
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
line = strings.TrimRight(line, "\r\n")
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(line, "0:") {
|
|
raw := line[2:]
|
|
var textChunk string
|
|
if err := json.Unmarshal([]byte(raw), &textChunk); err == nil {
|
|
textBuilder.WriteString(textChunk)
|
|
} else {
|
|
textBuilder.WriteString(raw)
|
|
}
|
|
} else if strings.HasPrefix(line, "b:") || strings.HasPrefix(line, "g:") {
|
|
raw := line[2:]
|
|
var rStr string
|
|
if err := json.Unmarshal([]byte(raw), &rStr); err == nil {
|
|
reasoningBuilder.WriteString(rStr)
|
|
}
|
|
} else if strings.HasPrefix(line, "data: ") {
|
|
dataStr := strings.TrimPrefix(line, "data: ")
|
|
if dataStr == "[DONE]" {
|
|
continue
|
|
}
|
|
var sseChunk struct {
|
|
Type string `json:"type"`
|
|
Delta string `json:"delta"`
|
|
Text string `json:"text"`
|
|
}
|
|
if json.Unmarshal([]byte(dataStr), &sseChunk) == nil {
|
|
if sseChunk.Type == "text-delta" && sseChunk.Delta != "" {
|
|
textBuilder.WriteString(sseChunk.Delta)
|
|
} else if sseChunk.Text != "" {
|
|
textBuilder.WriteString(sseChunk.Text)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
rawContent := textBuilder.String()
|
|
cleanedContent, inTextReasoning := ExtractThinking(rawContent)
|
|
if inTextReasoning != "" {
|
|
if reasoningBuilder.Len() > 0 {
|
|
reasoningBuilder.WriteString("\n")
|
|
}
|
|
reasoningBuilder.WriteString(inTextReasoning)
|
|
}
|
|
|
|
finishReason := "stop"
|
|
var finalToolCalls []ToolCall
|
|
if len(parsedToolCalls) > 0 {
|
|
finalToolCalls = parsedToolCalls
|
|
finishReason = "tool_calls"
|
|
} else {
|
|
if detected, rem, ok := DetectToolCalls(cleanedContent); ok && len(detected) > 0 {
|
|
finalToolCalls = detected
|
|
cleanedContent = rem
|
|
finishReason = "tool_calls"
|
|
}
|
|
}
|
|
|
|
var msgContent interface{} = cleanedContent
|
|
if len(finalToolCalls) > 0 && cleanedContent == "" {
|
|
msgContent = nil
|
|
}
|
|
|
|
WriteCompletionResponse(w, completionID, createdTime, modelName, FinalOutput{
|
|
Content: msgContent,
|
|
ReasoningContent: reasoningBuilder.String(),
|
|
ToolCalls: finalToolCalls,
|
|
FinishReason: finishReason,
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func main() {
|
|
portFlag := flag.String("port", "8080", "Port to listen on")
|
|
targetURLFlag := flag.String("endpoint", DefaultTargetURL, "Upstream T3 chat API endpoint")
|
|
defaultModelFlag := flag.String("default-model", DefaultModel, "Default model to forward")
|
|
cookieFlag := flag.String("cookie", DefaultCookie, "Cookie header to send upstream")
|
|
hcaptchaFlag := flag.String("hcaptcha-token", DefaultHcaptchaToken, "hCaptcha token to send upstream")
|
|
deploymentFlag := flag.String("deployment-id", DefaultDeploymentID, "x-deployment-id header value")
|
|
clientContextFlag := flag.String("client-context", DefaultClientContext, "x-client-context header value")
|
|
browserBinFlag := flag.String("browser-bin", "", "Custom path to Chromium/Chrome binary for auto-capture")
|
|
autoCaptureFlag := flag.Bool("auto-capture", true, "Automatically bridge requests and auto-generate fresh hCaptcha tokens via Chromium (default true)")
|
|
directFlag := flag.Bool("direct", false, "Disable browser bridge and run in direct HTTP client mode")
|
|
noBridgeFlag := flag.Bool("no-bridge", false, "Disable browser bridge (alias for -direct)")
|
|
noAutoCaptureFlag := flag.Bool("no-auto-capture", false, "Disable browser bridge (alias for -direct)")
|
|
headlessFlag := flag.Bool("headless", false, "Force strict headless mode (defaults to offscreen window if display is available)")
|
|
displayFlag := flag.String("display", "", "Custom X11 DISPLAY to run browser on (e.g. :99 for Xvfb / Xephyr / Xnest)")
|
|
xvfbFlag := flag.Bool("xvfb", true, "Automatically spawn and manage an isolated virtual X server (Xvfb) for headless tiling WM environments (default true)")
|
|
xvfbUpperFlag := flag.Bool("Xvfb", false, "Alias for -xvfb")
|
|
noXvfbFlag := flag.Bool("no-xvfb", false, "Disable automatic virtual X server (use active DISPLAY)")
|
|
uaFlag := flag.String("user-agent", DefaultUserAgent, "User-Agent header to send upstream")
|
|
uaShortFlag := flag.String("ua", "", "Alias for -user-agent")
|
|
flag.Parse()
|
|
|
|
if *uaShortFlag != "" {
|
|
ConfiguredUserAgent = *uaShortFlag
|
|
} else if *uaFlag != "" {
|
|
ConfiguredUserAgent = *uaFlag
|
|
}
|
|
|
|
effectiveAutoCapture := *autoCaptureFlag
|
|
if *directFlag || *noBridgeFlag || *noAutoCaptureFlag {
|
|
effectiveAutoCapture = false
|
|
}
|
|
|
|
effectiveXvfb := *xvfbFlag
|
|
if *xvfbUpperFlag {
|
|
effectiveXvfb = true
|
|
}
|
|
if *noXvfbFlag {
|
|
effectiveXvfb = false
|
|
}
|
|
|
|
gateway := NewT3Gateway(
|
|
*targetURLFlag,
|
|
*clientContextFlag,
|
|
*deploymentFlag,
|
|
*cookieFlag,
|
|
*hcaptchaFlag,
|
|
*defaultModelFlag,
|
|
*browserBinFlag,
|
|
effectiveAutoCapture,
|
|
*headlessFlag,
|
|
*displayFlag,
|
|
effectiveXvfb,
|
|
)
|
|
|
|
if effectiveAutoCapture {
|
|
fmt.Println("Initializing private browser bridge (TLS & dynamic hCaptcha generator)...")
|
|
if err := gateway.bridge.Start(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "warning: browser bridge init failed: %v (falling back to direct client)\n", err)
|
|
gateway.bridge = nil
|
|
} else {
|
|
fmt.Println("Browser bridge active! Upstream requests will execute with genuine Chromium TLS fingerprint.")
|
|
}
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
mux.HandleFunc("/v1/models", gateway.HandleModels)
|
|
mux.HandleFunc("/models", gateway.HandleModels)
|
|
mux.HandleFunc("/v1/chat/completions", gateway.HandleChatCompletions)
|
|
mux.HandleFunc("/chat/completions", gateway.HandleChatCompletions)
|
|
|
|
// Token management & ingestion endpoints
|
|
mux.HandleFunc("/v1/token", gateway.HandleToken)
|
|
mux.HandleFunc("/token", gateway.HandleToken)
|
|
|
|
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
EnableCORS(w)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
})
|
|
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
EnableCORS(w)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"service":"th3ist","version":"1.6.0","status":"running"}`))
|
|
})
|
|
|
|
fmt.Printf("th3ist gateway starting on port %s...\n", *portFlag)
|
|
fmt.Printf("Default Model: %s\n", *defaultModelFlag)
|
|
fmt.Printf("Upstream: %s\n", *targetURLFlag)
|
|
fmt.Printf("Auto-Capture / Bridge: %t\n", effectiveAutoCapture)
|
|
fmt.Printf("Virtual X Display (Xvfb): %t\n", effectiveXvfb)
|
|
fmt.Printf("Endpoints:\n")
|
|
fmt.Printf(" GET http://localhost:%s/v1/models\n", *portFlag)
|
|
fmt.Printf(" POST http://localhost:%s/v1/chat/completions\n", *portFlag)
|
|
fmt.Printf(" GET http://localhost:%s/v1/token\n", *portFlag)
|
|
fmt.Printf(" POST http://localhost:%s/v1/token\n", *portFlag)
|
|
|
|
if err := http.ListenAndServe(":"+*portFlag, mux); err != nil {
|
|
fmt.Fprintf(os.Stderr, "server failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|