diff --git a/th3ist.go b/th3ist.go index 2236b29..a98dc6a 100644 --- a/th3ist.go +++ b/th3ist.go @@ -1771,6 +1771,130 @@ func (bb *BrowserBridge) RotateIdentity() (string, error) { return "", fmt.Errorf("failed to rotate identity: %v", evalRes) } +func (bb *BrowserBridge) EvaluateScript(js string) (string, error) { + bb.mu.Lock() + defer bb.mu.Unlock() + + if bb.conn == nil { + if err := bb.Start(); err != nil { + return "", fmt.Errorf("failed to start browser bridge: %w", err) + } + } + + evalRes, err := sendCDPCommand(bb.conn, bb.reader, "Runtime.evaluate", map[string]interface{}{ + "expression": js, + "awaitPromise": true, + "returnByValue": true, + }) + if err != nil { + return "", err + } + + resJSON, _ := json.Marshal(evalRes) + return string(resJSON), nil +} + +func (bb *BrowserBridge) FetchFreeModels() ([]ModelItem, error) { + js := `(async () => { + let targetUrl = null; + for (let attempt = 0; attempt < 10; attempt++) { + const entries = performance.getEntriesByType('resource') + .map(r => r.name) + .filter(n => n.includes('model-helpers') || n.includes('model-selector')); + targetUrl = entries.find(n => n.includes('model-helpers')); + if (targetUrl) break; + + const scripts = Array.from(document.querySelectorAll('script[src]')).map(s => s.src); + targetUrl = scripts.find(s => s.includes('model-helpers')); + if (targetUrl) break; + + await new Promise(r => setTimeout(r, 500)); + } + + if (!targetUrl) return { error: "model-helpers bundle not found in active session" }; + + const resp = await fetch(targetUrl); + const code = await resp.text(); + const startIdx = code.indexOf('var dE=['); + if (startIdx === -1) return { error: "model registry definition (var dE) not found" }; + + const sandbox = 'var Z = function(d){return new Date(d)}; function uE(){return{}}; function $(e){return e}; var fast={},images={},reasoning={},reasoningEffort={},toolCalling={},imageGeneration={},nativePDFs={}; ' + code.substring(0, startIdx) + code.substring(startIdx, code.indexOf(';function ', startIdx)) + '; dE;'; + const models = eval(sandbox); + const now = new Date(); + const freeList = []; + + for (const m of models) { + const isRetired = m.retiredOn && new Date(m.retiredOn) <= now; + const isLegacy = !!m.legacy; + const hasExplicitFree = m.limits && m.limits.app && m.limits.app.free !== undefined && m.limits.app.free !== null; + const isPro = m.tier === 'pro' || m.tier === 'premier' || !!m.requiresPro || !!m.premium || !!m.disabled; + + if (!isLegacy && !isRetired && !isPro && hasExplicitFree) { + let dev = (m.developer || m.provider || "t3").toLowerCase(); + if (dev.includes("google")) dev = "google"; + else if (dev.includes("openai")) dev = "openai"; + else if (dev.includes("anthropic")) dev = "anthropic"; + else if (dev.includes("meta")) dev = "meta"; + else if (dev.includes("moonshot")) dev = "moonshot"; + else if (dev.includes("alibaba") || dev.includes("qwen")) dev = "alibaba"; + else if (dev.includes("deepseek")) dev = "deepseek"; + else if (dev.includes("xai") || dev.includes("grok")) dev = "xai"; + else if (dev.includes("z.ai") || dev.includes("glm")) dev = "zai"; + else if (dev.includes("xiaomi")) dev = "xiaomi"; + else if (dev.includes("minimax")) dev = "minimax"; + + freeList.push({ + id: m.id, + owned_by: dev + }); + } + } + return { models: freeList }; + })()` + + res, err := bb.EvaluateScript(js) + if err != nil { + return nil, err + } + + var cdpRes struct { + Result struct { + Result struct { + Value struct { + Error string `json:"error"` + Models []struct { + ID string `json:"id"` + OwnedBy string `json:"owned_by"` + } `json:"models"` + } `json:"value"` + } `json:"result"` + } `json:"result"` + } + + if err := json.Unmarshal([]byte(res), &cdpRes); err != nil { + return nil, err + } + + if cdpRes.Result.Result.Value.Error != "" { + return nil, fmt.Errorf("%s", cdpRes.Result.Result.Value.Error) + } + + var items []ModelItem + now := time.Now().Unix() + for _, m := range cdpRes.Result.Result.Value.Models { + if m.ID != "" { + items = append(items, ModelItem{ + ID: m.ID, + Object: "model", + Created: now, + OwnedBy: m.OwnedBy, + }) + } + } + + return items, nil +} + func (bb *BrowserBridge) ExecuteFetch(payload T3ChatPayload, deploymentID, clientContext string) (int, string, error) { bb.mu.Lock() defer bb.mu.Unlock() @@ -2162,6 +2286,24 @@ type T3Gateway struct { autoCapture bool client *http.Client bridge *BrowserBridge + models []ModelItem +} + +func DefaultFreeModels() []ModelItem { + now := time.Now().Unix() + return []ModelItem{ + // Google (Verified Unlocked Free) + {ID: "gemini-3.5-flash-lite", Object: "model", Created: now, OwnedBy: "google"}, + {ID: "gemini-3.1-flash-lite", Object: "model", Created: now, OwnedBy: "google"}, + {ID: "gemini-3-flash", Object: "model", Created: now, OwnedBy: "google"}, + + // OpenAI (Verified Unlocked Free) + {ID: "gpt-5.4-mini", Object: "model", Created: now, OwnedBy: "openai"}, + + // Moonshot (Verified Unlocked Free) + {ID: "kimi-k2-0905", Object: "model", Created: now, OwnedBy: "moonshot"}, + {ID: "kimi-k2.5", Object: "model", Created: now, OwnedBy: "moonshot"}, + } } func NewT3Gateway(targetURL, clientContext, deploymentID, cookie, hcaptchaToken, defaultModel, browserBin string, autoCapture bool, headless bool, display string, useXvfb bool) *T3Gateway { @@ -2200,6 +2342,7 @@ func NewT3Gateway(targetURL, clientContext, deploymentID, cookie, hcaptchaToken, autoCapture: autoCapture, client: &http.Client{Timeout: 300 * time.Second}, bridge: bridge, + models: DefaultFreeModels(), } } @@ -2226,20 +2369,35 @@ func (g *T3Gateway) GetCredentials() (cookie, token, deploymentID, clientContext 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) RefreshModels() error { + if g.bridge != nil { + items, err := g.bridge.FetchFreeModels() + if err != nil { + return err + } + if len(items) > 0 { + g.mu.Lock() + g.models = items + g.mu.Unlock() + return nil + } } + return nil +} + +func (g *T3Gateway) ListModels() []ModelItem { + g.mu.RLock() + defer g.mu.RUnlock() + if len(g.models) == 0 { + return DefaultFreeModels() + } + now := time.Now().Unix() + res := make([]ModelItem, len(g.models)) + for i, m := range g.models { + res[i] = m + res[i].Created = now + } + return res } func (g *T3Gateway) extractSessionID(cookieStr string) string { @@ -2999,6 +3157,13 @@ func main() { gateway.bridge = nil } else { fmt.Println("Browser bridge active! Upstream requests will execute with genuine Chromium TLS fingerprint.") + fmt.Println("Fetching live free-tier models to populate /v1/models...") + if err := gateway.RefreshModels(); err != nil { + fmt.Fprintf(os.Stderr, "warning: live model discovery failed: %v (using default free-tier model catalog)\n", err) + } else { + models := gateway.ListModels() + fmt.Printf("Successfully populated %d active free-tier models into /v1/models\n", len(models)) + } } }