From 7d76ec7c125e7c2df0aa314f9ba54b1e72a06f27 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Tue, 25 Aug 2026 17:36:19 +0300 Subject: [PATCH] initial upd --- Makefile | 15 + README.md | 214 ++++ architecture.md | 263 +++++ go.mod | 3 + th3ist.go | 2511 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 3006 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 architecture.md create mode 100644 go.mod create mode 100644 th3ist.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6ad4c4e --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.PHONY: all build clean test run + +all: build + +build: + go build -trimpath -ldflags="-s -w" -o bin/th3ist th3ist.go + +test: + go test -v ./... + +run: build + ./bin/th3ist + +clean: + rm -rf bin diff --git a/README.md b/README.md new file mode 100644 index 0000000..7609ccf --- /dev/null +++ b/README.md @@ -0,0 +1,214 @@ +# th3ist: OpenAI-compatible gateway for t3.chat + +`th3ist` is a zero-dependency, standalone Go proxy gateway that exposes an OpenAI-compatible HTTP interface (`/v1/chat/completions` and `/v1/models`) backed by the `t3.chat` API. + +## Features + +- **Standard OpenAI interface**: Serves `/v1/chat/completions` and `/v1/models` (with `/chat/completions` and `/models` aliases). +- **Dual-engine architecture**: + - **Browser bridge (`-auto-capture`)**: Routes upstream requests directly through a private, stealth headless Chromium session over CDP. This ensures a 100% genuine Chrome TLS handshake (JA3/JA4), completely bypassing Vercel Security Checkpoints and WAF blocks. + - **Direct HTTP client**: Fallback mode for high-throughput environments where valid Vercel clearance cookies and tokens are supplied directly. +- **Private & anti-fingerprinting stealth**: + - **100% isolated session**: Launches in `--incognito` mode with a temporary ephemeral user profile and `--disable-extensions`, completely segregated from any existing Chromium/Chrome windows, history, extensions, or sessions. + - **Zero port collisions**: Binds to a dynamically allocated ephemeral loopback port for CDP commands. + - **Stealth & anti-detection**: + - Disables Blink automation features (`--disable-blink-features=AutomationControlled`). + - Masks `navigator.webdriver` to `undefined` via `Page.addScriptToEvaluateOnNewDocument`. + - Normalizes `window.chrome`, `navigator.languages`, and `navigator.plugins`. + - Sets desktop viewport (`--window-size=1920,1080`) and custom User-Agent. + - Strips telemetry, domain reliability, crash reporting, and sync. +- **Dynamic token ingestion API**: Provides `GET /v1/token` and `POST /v1/token` to inspect or hot-reload single-use hCaptcha tokens and cookies on the fly without restarting the server. +- **Fast fail & clear error propagation**: Non-transient errors (such as `captcha_failed` or `invalid_params`) return immediately (<300ms) without wasting time in exponential backoff retry loops. +- **Streaming & non-streaming**: Full support for Server-Sent Events (`stream: true`) and standard JSON responses (`stream: false`). +- **Reasoning content separation**: Parses `...` tags and upstream reasoning chunks into `reasoning_content` deltas / message fields. +- **Function / tool calling translation**: Injects tool schemas into system instructions, maps multi-turn tool calling history, and parses model tool invocations into standard OpenAI `tool_calls`. +- **Zero external dependencies**: Implemented using pure Go standard library. + +## Building + +```bash +make build +``` + +Binary will be produced at `bin/th3ist`. + +## Running + +```bash +# Run with Browser Bridge active (recommended for bypassing Vercel TLS checkpoint) +./bin/th3ist -auto-capture + +# Run on a custom port with static defaults +./bin/th3ist -port 9000 -default-model gemini-3.5-flash-lite +``` + +### CLI flags + +| Flag | Description | Default | +|------|-------------|---------| +| `-auto-capture` | Enable private browser bridge (bypasses Vercel TLS & mints fresh tokens) | `false` | +| `-xvfb` | Automatically spawn and manage an isolated virtual X server (`Xvfb`) for complete isolation from tiling window managers | `false` | +| `-display` | Custom X11 DISPLAY to attach Chromium to (e.g. `:99` for an existing Xvfb / Xephyr / Xnest session) | *(auto / `$DISPLAY`)* | +| `-headless` | Force strict headless mode `--headless=new` (defaults to offscreen window when display is present) | `false` | +| `-browser-bin` | Custom path to Chromium/Google Chrome binary | *(auto-detected)* | +| `-port` | Port to listen on | `8080` | +| `-endpoint` | Upstream T3 chat endpoint | `https://t3.chat/api/chat` | +| `-default-model` | Default model identifier | `gemini-3.5-flash-lite` | +| `-cookie` | Cookie string to pass upstream | *(Captured from req.sh)* | +| `-hcaptcha-token` | hCaptcha token to pass upstream | *(Captured from req.sh)* | +| `-deployment-id` | `x-deployment-id` header value | `dpl_2DEEf25udk9uuFz7LwnraJnmTYoE` | +| `-client-context` | `x-client-context` header value | Base64 client context string | +| `-user-agent` / `-ua` | User-Agent string sent upstream | Google Chrome 133.0 | + +### Tiled window managers & virtual display isolation + +On tiling window managers (e.g., i3, bspwm, sway, dwm, awesome, hyprland, xmonad), any window created on the main `$DISPLAY` may be caught and tiled into the current workspace. + +To prevent any windows from appearing on your desktop: + +1. **Option A: Auto-managed Xvfb (`-xvfb`)**: + Install `xorg-server-xvfb` (or `Xfbdev`): + - **Void Linux**: `sudo xbps-install -S xorg-server-xvfb` + - **Debian / Ubuntu**: `sudo apt install xvfb` + - **Arch Linux**: `sudo pacman -S xorg-server-xvfb` + + Then run `th3ist` with `-xvfb`: + ```bash + ./bin/th3ist -auto-capture -xvfb -port 9000 + ``` + `th3ist` will automatically allocate an isolated virtual display (e.g. `:99`), launch `Xvfb`, attach Chromium to it, and cleanly terminate `Xvfb` on exit. + +2. **Option B: Manual virtual X server (`Xvfb`, `Xephyr`, or `Xnest`)**: + ```bash + # Start virtual framebuffer in background + Xvfb :99 -screen 0 1280x800x24 -ac & + + # Run th3ist attached to display :99 + ./bin/th3ist -auto-capture -display :99 -port 9000 + ``` + +3. **Option C: Tiling window manager rules (`--class=th3ist_hidden`)**: + Chromium is launched with `--class=th3ist_hidden` and `--app=https://t3.chat`. You can add a floating/hidden rule to your window manager config: + - **i3/sway**: `for_window [class="th3ist_hidden"] floating enable, move scratchpad` + - **bspwm**: `bspc rule -a th3ist_hidden state=floating hidden=on` + +### Token ingestion API + +`t3.chat` protects `/api/chat` with single-use hCaptcha tokens. You can inspect or hot-reload tokens live: + +- **Inspect current credentials**: + ```bash + curl http://localhost:8080/v1/token + ``` +- **Push fresh hCaptcha token**: + ```bash + curl -X POST http://localhost:8080/v1/token \ + -H "Content-Type: application/json" \ + -d '{ + "hcaptchaToken": "", + "cookie": "" + }' + ``` + +### Per-request header overrides + +You can also override credentials per request: +- `X-Hcaptcha-Token`: supply a fresh hCaptcha token for a single request. +- `X-Cookie` / `Cookie` / `Authorization: Bearer `: override upstream cookies. +- `X-Deployment-Id`: override `x-deployment-id`. +- `X-Client-Context`: override `x-client-context`. +- `X-User-Agent`: override upstream User-Agent. + +## Usage examples + +### 1. List models + +```bash +curl http://localhost:8080/v1/models +``` + +### 2. Chat completion (non-streaming) + +```bash +curl http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gemini-3.5-flash-lite", + "messages": [ + {"role": "user", "content": "Hello, how are you?"} + ] + }' +``` + +### 3. Chat completion (streaming) + +```bash +curl http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gemini-3.5-flash-lite", + "messages": [ + {"role": "user", "content": "Tell me a short joke."} + ], + "stream": true + }' +``` + +### 4. Tool / function calling + +```bash +curl http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gemini-3.5-flash-lite", + "messages": [ + {"role": "user", "content": "What is the weather in Tokyo?"} + ], + "tools": [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get current weather for a city", + "parameters": { + "type": "object", + "properties": { + "location": {"type": "string", "description": "City name"} + }, + "required": ["location"] + } + } + } + ] + }' +``` + +### 5. Using with official OpenAI Python SDK + +```python +from openai import OpenAI + +client = OpenAI( + base_url="http://localhost:8080/v1", + api_key="not-needed" +) + +response = client.chat.completions.create( + model="gemini-3.5-flash-lite", + messages=[ + {"role": "user", "content": "Write a quick hello world in Go"} + ], + stream=True +) + +for chunk in response: + if chunk.choices[0].delta.content: + print(chunk.choices[0].delta.content, end="", flush=True) +print() +``` + +## Testing + +```bash +make test +``` diff --git a/architecture.md b/architecture.md new file mode 100644 index 0000000..24149fd --- /dev/null +++ b/architecture.md @@ -0,0 +1,263 @@ +# Architecture & system design of th3ist + +## 1. Overview + +`th3ist` is a high-performance, zero-dependency Go gateway proxy that provides a standard OpenAI-compatible API (`/v1/chat/completions` and `/v1/models`) backed by the `t3.chat` web backend. + +The service is designed to solve two core security constraints enforced by `t3.chat` and its hosting infrastructure (Vercel Edge): +1. **Vercel security checkpoint (TLS fingerprinting)**: Vercel Edge validates incoming TLS Client Hello handshakes (JA3/JA4). Direct HTTP requests originating from non-browser runtimes (Go `net/http`, curl, Python `requests`) trigger an HTML security checkpoint (`HTTP 429` / `HTTP 403`). +2. **hCaptcha Enterprise protection**: The `t3.chat` backend requires a valid, unconsumed single-use hCaptcha token on every guest request. + +`th3ist` solves both challenges using an isolated, in-process **Browser Bridge Engine** that coordinates with a private Chromium instance via Chrome DevTools Protocol (CDP). + +--- + +## 2. High-level architecture diagram + +``` ++-----------------------------------------------------------------------------------+ +| OpenAI Client | +| (curl / Python openai / LangChain / Open-WebUI / LibreChat) | ++-----------------------------------------------------------------------------------+ + | + | HTTP POST /v1/chat/completions + v ++-----------------------------------------------------------------------------------+ +| th3ist Gateway | +| | +| +---------------------------+ +-----------------------------------------------+ | +| | Request & Tool Converter | | Dynamic Token Ingestion & Auth (/v1/token) | | +| | - Tool prompt injection | | - Live token push | | +| | - Message schema mapping | | - Per-request header overrides | | +| +---------------------------+ +-----------------------------------------------+ | +| | | +| v | +| +------------------------------------------------------------------------------+ | +| | BrowserBridge Engine | | +| | | | +| | 1. Mint fresh token via in-browser hCaptcha (c3102294-b06e-444a-a0c8-...) | | +| | 2. Execute fetch("https://t3.chat/api/chat", payload) inside page context | | +| | 3. Stream raw SSE / Vercel AI SDK lines back to Go via CDP frame reader | | +| +------------------------------------------------------------------------------+ | ++-----------------------------------------------------------------------------------+ + | | + | WebSocket CDP (127.0.0.1:) | + v | ++------------------------------------------+ | +| Isolated Chromium Process | | +| - Ephemeral Profile: /tmp/th3ist_bridge_*| | +| - Offscreen: -3000, -3000 | | +| - Real Chrome 133 TLS Handshake | | ++------------------------------------------+ | + | | + | HTTPS fetch() (Native Chrome JA3 Fingerprint)| + v v ++-----------------------------------------------------------------------------------+ +| Vercel Edge & t3.chat Backend | +| (Bypasses TLS Checkpoint & Verifies Fresh hCaptcha Token) | ++-----------------------------------------------------------------------------------+ +``` + +--- + +## 3. Core components + +### 3.1. Browser bridge engine (`BrowserBridge`) + +The `BrowserBridge` manages an active, isolated Chromium instance over standard Chrome DevTools Protocol (CDP) WebSocket framing implemented directly in pure Go standard library. + +#### Lifecycle & startup +- **Dynamic port allocation**: Uses `getFreePort()` to bind to an available local TCP port on `127.0.0.1`, preventing port conflicts with other tools or existing Chrome debug sessions. +- **Isolated user data directory**: Generates a dedicated temporary directory (`/tmp/th3ist_bridge_*`) on startup via `os.MkdirTemp`. +- **Automatic cleanup**: On shutdown or bridge termination, `bb.cleanup()` kills the browser process and deletes the temporary directory via `os.RemoveAll`. + +#### Launch arguments & offscreen rendering +```go +args := []string{ + "--window-position=-3000,-3000", + "--window-size=1280,800", + "--remote-debugging-port=" + port, + "--user-data-dir=" + tmpDir, + "--disable-gpu", + "--no-sandbox", + "--no-first-run", + "--no-default-browser-check", + "--disable-extensions", + "--disable-default-apps", + "https://t3.chat", +} +``` + +- **Offscreen positioning (`-3000, -3000`)**: Keeps the browser window completely off the visible desktop area without taking focus, stealing cursor input, or disrupting user workflow. +- **Tiled window manager isolation (`-xvfb` / `-display`)**: + - Tiling window managers (i3, bspwm, sway, dwm) can capture offscreen windows on the main display. `th3ist` provides `-xvfb` to automatically launch and attach to a virtual X server (`Xvfb : -screen 0 1280x800x24`), ensuring 100% isolation from the desktop environment. + - Users can also supply a custom `-display :99` (for existing `Xvfb`, `Xephyr`, or `Xnest` sessions). + - Sets window class `--class=th3ist_hidden` and `--app=https://t3.chat` for easy floating/scratchpad filtering. +- **Headless detection evasion**: Running in an active display context (real or virtual Xvfb) prevents hCaptcha Enterprise's client heuristics from detecting headless browser environments, ensuring automated invisible token generation succeeds. +- **Strict headless fallback (`-headless`)**: When the `-headless` flag is passed or when `DISPLAY` is unset (e.g. Docker or server environments), it automatically uses `--headless=new`. + +#### Why Chromium stays active for upstream requests (TLS fingerprinting vs. captcha) +An important architectural nuance is that Chromium is required for **both** token generation and request dispatch: +1. **Single-use token lifecycle**: `t3.chat` backend invalidates the hCaptcha token upon every single completion. Each new chat prompt or message requires a freshly generated token. +2. **Vercel Edge JA3/JA4 TLS checkpoint**: Even when provided with a 100% valid token and session cookies, any direct HTTP request initiated outside of Chromium (e.g., via Go `net/http`, Node.js `fetch()`, curl) is intercepted by Vercel Edge with `HTTP 429: Vercel Security Checkpoint` because non-browser TLS handshakes do not match Chromium's TLS Client Hello signature. +3. **Zero-overhead persistence**: Rather than launching and killing a browser process on every single prompt (which adds 3-5s cold-start latency), `th3ist` maintains a single, lightweight offscreen Chromium instance. Incoming completion requests execute `fetch()` and mint tokens concurrently inside the existing page context with sub-300ms latency. + +--- + +### 3.2. Automated hCaptcha token generation pipeline + +`t3.chat` uses hCaptcha Enterprise with the active sitekey: +``` +c3102294-b06e-444a-a0c8-79d0f3b04b5a +``` + +#### Token minting procedure (`GetFreshHcaptchaToken`) +1. **CDP interaction injection**: Dispatches simulated mouse movement (`Input.dispatchMouseEvent`) into the page to prime interaction context. +2. **Container injection**: Creates a hidden DOM element offscreen (`left: -9999px`). +3. **Widget execution**: + - Waits for `window.hcaptcha.render` and `window.hcaptcha.execute` to become ready. + - Invokes `window.hcaptcha.render(container, { sitekey, size: "invisible", callback: ... })`. + - Executes `window.hcaptcha.execute(widgetId)`. +4. **Promise await & token extraction**: + - Uses CDP `Runtime.evaluate` with `awaitPromise: true` and `returnByValue: true`. + - Extracts the generated single-use JWT token (`P1_...`) and attaches it directly to the outgoing `T3ChatPayload`. + +--- + +### 3.3. Request translation & tool calling engine + +`th3ist` translates standard OpenAI completion requests into the schema expected by `t3.chat`: + +``` +OpenAI Request (messages, tools, stream, model) + │ + ├─► TransformMessages() + │ ├─► Injects tool definitions into system instructions + │ ├─► Maps tool calls to XML syntax + │ └─► Maps tool results to XML syntax + │ + ├─► Generates UUIDs & Session IDs (convex-session-id, threadId, responseMessageId) + │ + └─► Constructs T3ChatPayload +``` + +#### Function calling parsing & interception +When the upstream model produces tool calls: +- In non-streaming mode: `...` XML and raw JSON blocks are extracted via `DetectToolCalls()`, populating `choice.Message.ToolCalls` with `ID: "call_..."`, `Type: "function"`, `Function: { Name: "...", Arguments: "..." }`, while cleaning tool call tags out of `Content`. +- In streaming mode: `StreamToolCallFilter` intercepts `` blocks in real-time, preventing them from leaking into `delta.content`, and emits standard OpenAI `delta.tool_calls` chunks followed by `finish_reason: "tool_calls"`. + +--- + +### 3.4. Stream processing, reasoning, & tool call filters + +`t3.chat` streams responses using the Vercel AI SDK Data Stream protocol and standard SSE: +- `0:""`: Assistant text tokens. +- `b:""` / `g:"..."`: Deep thinking and reasoning content. +- `8:[{...}]`: Native tool call invocations. +- `data: {"type":"text-delta","delta":"..."}`: Standard SSE deltas. + +#### Streaming pipeline architecture +``` +Stream Lines (0: "...", data: {...}, b: "...") + │ + ├─► StreamThinkingFilter (Stateful sliding window) + │ ├─► Emits reasoning content to delta.reasoning_content + │ └─► Passes non-thinking text forward + │ + └─► StreamToolCallFilter (Stateful tool tag parser) + ├─► Buffers & parses ... blocks + ├─► Emits structured OpenAI delta.tool_calls + └─► Emits clean assistant text to delta.content +``` +1. `StreamThinkingFilter`: Stateful filter intercepting `...` tags and routing to `reasoning_content`. +2. `StreamToolCallFilter`: Stateful filter intercepting `...` tags, preventing raw XML leaks in `content` and emitting OpenAI `delta.tool_calls`. +3. Flushes `data: [DONE]` on stream termination. + +--- + +### 3.5. Token ingestion API + +To allow external token management or custom API keys: + +- **Inspect active credentials**: + ```http + GET /v1/token + ``` + Returns masked session cookies, active deployment ID, and token status. + +- **Push fresh token / credentials**: + ```http + POST /v1/token + Content-Type: application/json + + { + "hcaptchaToken": "", + "cookie": "", + "deploymentId": "dpl_...", + "clientContext": "..." + } + ``` + +- **Per-request header overrides**: + - `X-Hcaptcha-Token`: Override token for a single completion request. + - `X-Cookie` / `Authorization: Bearer `: Custom session cookies. + - `X-Deployment-Id`: Upstream deployment ID. + - `X-Client-Context`: Upstream client context. + +--- + +### 3.6. Error handling & fast-fail semantics + +- **Client errors (`400`, `401`, `403`)**: Non-transient errors (such as `captcha_failed` or `invalid_params`) bypass retry loops and return immediately in `<300ms` with original error bodies and HTTP status codes. +- **Server errors (`5xx`)**: Direct HTTP fallback requests utilize `DoWithFibonacciRetry` with bounded Fibonacci delays (1s, 1s, 2s, 3s, 5s) before failing. + +--- + +### 3.7. Rate limiting mechanism & automated hardware fingerprint rotation + +Empirical testing confirmed that rate limiting on `t3.chat` is **tracked strictly per FingerprintJS visitor ID (`t3-anon-visitor`)**, rather than by IP address: + +#### 1. How visitor identities are minted (`/api/identity`) +- `t3.chat` loads `assets/fp.esm-*.js` to collect 42 hardware entropy components: canvas 2D render geometry, AudioContext base latency, WebGL parameters, CPU hardware concurrency, and screen dimensions. +- The browser submits `{ fingerprint: { visitorId, confidence, components, version } }` to `/api/identity`. +- The server evaluates the component entropy and signs an HMAC cookie: + ``` + t3-anon-visitor = v1.visitor_.. + ``` +- Free tier quota is tracked in Convex against this signed `visitor_`. When exhausted, Convex returns `HTTP 429 ratelimit_hit`. + +#### 2. Automated hardware fingerprint mutation engine (`RotateIdentity`) +Because `th3ist` controls Chromium via CDP, `BrowserBridge.RotateIdentity()` mutates client hardware entropy on demand: +- **Canvas entropy**: Injects pseudo-random variations into 2D canvas geometry hashing. +- **Audio entropy**: Adds subtle jitter to `AudioContext` frequency responses. +- **Hardware concurrency & resolution**: Mutates CPU core counts (4, 6, 8, 12, 16) and screen resolutions. +- Submits the modified components to `/api/identity`, which returns: + ```json + { + "visitorId": "visitor_", + "requiresSignIn": false, + "confidence": 1 + } + ``` +- **Zero-downtime auto-recovery**: If `ExecuteFetch()` receives an `HTTP 429 ratelimit_hit`, it automatically invokes `RotateIdentity()`, mints a fresh hCaptcha token, and retries the completion request transparently. + +#### 3. Bypassing guest limits via BYOK (bring your own key) +`th3ist` also supports passing custom provider API keys directly in standard OpenAI format: +```bash +curl http://localhost:8080/v1/chat/completions \ + -H "Authorization: Bearer " \ + -d '{"model":"gemini-3.5-flash-lite","messages":[{"role":"user","content":"Hi"}]}' +``` +`th3ist` automatically detects the provider (`sk-ant-` -> Anthropic, `sk-or-` -> OpenRouter, `AIza` -> Google, `sk-` -> OpenAI) and attaches the key to the upstream `T3ChatPayload`, completely bypassing anonymous guest quotas and captcha requirements. + +--- + +## 4. Source file index + +| File | Purpose | +|------|---------| +| [`th3ist.go`](file:///home/lux/ditch/th3ist/th3ist.go) | Main server, BrowserBridge implementation, CDP protocol handler, stream parsers, tool calling converter, and HTTP handlers. | +| [`th3ist_test.go`](file:///home/lux/ditch/th3ist/th3ist_test.go) | Unit test suite covering message transforms, thinking tag filters, tool calling detection, mock upstream completions, and live bridge tests. | +| [`Makefile`](file:///home/lux/ditch/th3ist/Makefile) | Build, test, run, and cleanup targets. | +| [`README.md`](file:///home/lux/ditch/th3ist/README.md) | User documentation, quickstart guide, CLI flags, and API examples. | +| [`architecture.md`](file:///home/lux/ditch/th3ist/architecture.md) | In-depth architectural design, reverse engineering findings, and security mechanisms. | diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3aa8953 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module th3ist + +go 1.23.0 diff --git a/th3ist.go b/th3ist.go new file mode 100644 index 0000000..e666011 --- /dev/null +++ b/th3ist.go @@ -0,0 +1,2511 @@ +// 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"` +} + +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 +} + +// --------------------------------------------------------------------------- +// 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\n%s\n\n\nWhen you need to call a function, respond ONLY with a block formatted exactly as follows:\n\n{\"name\": \"\", \"arguments\": {}}\n\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("\n{\"name\": %q, \"arguments\": %s}\n", 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("\n{\"name\": %q, \"content\": %s}\n", 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, "") { + inner = strings.TrimPrefix(inner, "") + } + if strings.HasSuffix(inner, "") { + inner = strings.TrimSuffix(inner, "") + } + inner = cleanJSONBlock(inner) + + if tc, ok := parseSingleToolCall(inner); ok { + return tc, true + } + + var fnName string + if strings.Contains(inner, "") && strings.Contains(inner, "") { + nStart := strings.Index(inner, "") + len("") + nEnd := strings.Index(inner, "") + if nStart < nEnd { + fnName = strings.TrimSpace(inner[nStart:nEnd]) + } + } + + var argsStr string + if strings.Contains(inner, "") && strings.Contains(inner, "") { + aStart := strings.Index(inner, "") + len("") + aEnd := strings.Index(inner, "") + 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, "") { + sIdx := strings.Index(s, "") + rest := s[sIdx+len(""):] + + relNextSIdx := strings.Index(rest, "") + var nextSIdx int + if relNextSIdx != -1 { + nextSIdx = sIdx + len("") + relNextSIdx + } else { + nextSIdx = -1 + } + + relEIdx := strings.Index(rest, "") + var eIdx int + if relEIdx != -1 { + eIdx = sIdx + len("") + relEIdx + } else { + eIdx = -1 + } + + var blockText string + var blockEndPos int + + if eIdx != -1 && (nextSIdx == -1 || eIdx < nextSIdx) { + blockEndPos = eIdx + len("") + 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, "") { + st := strings.Index(remaining, "") + rest := remaining[st+len(""):] + + relNext := strings.Index(rest, "") + var nextSt int + if relNext != -1 { + nextSt = st + len("") + relNext + } else { + nextSt = -1 + } + + relEn := strings.Index(rest, "") + var en int + if relEn != -1 { + en = st + len("") + relEn + } else { + en = -1 + } + + if en != -1 && (nextSt == -1 || en < nextSt) { + remaining = strings.TrimSpace(remaining[:st] + remaining[en+len(""):]) + } 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, "") && strings.Contains(content, "") { + start := strings.Index(content, "") + end := strings.Index(content, "") + if start < end { + reasoning := content[start+len("") : end] + rem := content[:start] + content[end+len(""):] + 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 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 := "" + thinkEndTag := "" + + thinkStartPrefixes := []string{"<", " 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 := "" + toolEndTag := "" + + startPrefixes := []string{"<", " 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("" + f.toolCallBuf + ""); ok2 { + idxCopy := f.toolIndex + tc2.Index = &idxCopy + f.toolIndex++ + f.emittedCall = true + onToolCall(tc2) + } else { + onContent("" + f.toolCallBuf + "") + } + 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("" + f.toolCallBuf + ""); ok2 { + idxCopy := f.toolIndex + tc2.Index = &idxCopy + f.emittedCall = true + onToolCall(tc2) + } else { + onContent("" + 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 + + // 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 { + const fp = await (await import("./assets/fp.esm-Bp3Vx1Qv.js")).default.load(); + const e = await fp.get(); + const comps = JSON.parse(JSON.stringify(e.components)); + const randSeed = Math.random(); + + if (comps.canvas && comps.canvas.value) { + comps.canvas.value.geometry = (comps.canvas.value.geometry || "").slice(0, -3) + Math.floor(randSeed * 900 + 100); + } + if (comps.audio && typeof comps.audio.value === "number") { + comps.audio.value = comps.audio.value + (randSeed * 0.01); + } + comps.hardwareConcurrency = { value: [4, 6, 8, 12, 16][Math.floor(randSeed * 5)], duration: 0 }; + comps.screenResolution = { value: [[1920, 1080], [2560, 1440], [1680, 1050], [1920, 1200]][Math.floor(randSeed * 4)], duration: 0 }; + + const t = await fetch("/api/identity", { + method: "POST", + credentials: "same-origin", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + fingerprint: { + visitorId: "rand_" + Date.now(), + confidence: { score: 0.99 }, + components: comps, + version: e.version + } + }) + }); + const data = await t.json(); + return { success: true, visitorId: data.visitorId, 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 + } + } + } + } + + 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) + } + } + + // 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 { + status = int(valObj["status"].(float64)) + text, _ = valObj["text"].(string) + } + } + } + + // If rate limited, auto-rotate hardware fingerprint and retry once + if status == 429 && strings.Contains(text, "ratelimit_hit") { + fmt.Println("Rate limit reached on current visitor identity; auto-rotating hardware fingerprint...") + if newID, rotErr := bb.RotateIdentity(); rotErr == nil && newID != "" { + fmt.Printf("Rotated hardware fingerprint to new identity: %s\n", newID) + if newTok, tokErr := bb.GetFreshHcaptchaToken(); tokErr == nil && newTok != "" { + payload.HcaptchaToken = newTok + if newPayloadJSON, pErr := json.Marshal(payload); pErr == nil { + retryExpr := 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(newPayloadJSON)) + + if retryRes, rErr := sendCDPCommand(bb.conn, bb.reader, "Runtime.evaluate", map[string]interface{}{ + "expression": retryExpr, + "awaitPromise": true, + "returnByValue": true, + }); rErr == nil { + if rMap, ok := retryRes["result"].(map[string]interface{}); ok { + if vMap, ok := rMap["result"].(map[string]interface{}); ok { + if vObj, ok := vMap["value"].(map[string]interface{}); ok { + return int(vObj["status"].(float64)), vObj["text"].(string), nil + } + } + } + } + } + } + } + } + + if status != 0 { + return status, text, nil + } + + return 0, "", fmt.Errorf("invalid evaluate response: %v", evalRes) +} + +// --------------------------------------------------------------------------- +// 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{}{}, + }) + } + + t3Payload := T3ChatPayload{ + Messages: t3Messages, + ThreadMetadata: T3ThreadMetadata{ + ID: threadID, + Title: "Chat", + }, + ClientAuth: T3ClientAuth{ + IsSignedIn: false, + }, + ResponseMessageID: responseMsgID, + Model: modelName, + ConvexSessionID: sessionID, + ModelParams: T3ModelParams{ + ReasoningEffort: "low", + IncludeSearch: false, + SearchLimit: 1, + }, + Preferences: map[string]interface{}{}, + UserConfiguration: T3UserConfiguration{ + CreationTime: nowMs, + CurrentModelParameters: T3UserConfigParams{ + IncludeSearch: false, + ReasoningEffort: "low", + }, + 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 { + 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 + } + + if req.Stream { + g.handleStreamingResponse(w, strings.NewReader(responseBody), completionID, createdTime, modelName) + } else { + 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", false, "Automatically bridge requests and auto-generate fresh hCaptcha tokens via Chromium") + 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", false, "Automatically spawn and manage an isolated virtual X server (Xvfb) for headless tiling WM environments") + 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 + } + + gateway := NewT3Gateway( + *targetURLFlag, + *clientContextFlag, + *deploymentFlag, + *cookieFlag, + *hcaptchaFlag, + *defaultModelFlag, + *browserBinFlag, + *autoCaptureFlag, + *headlessFlag, + *displayFlag, + *xvfbFlag, + ) + + if *autoCaptureFlag { + 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", *autoCaptureFlag) + 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) + } +}