mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(providers): remove dead Phind provider + dedupe HuggingChat catalog listing (#5530)
Integrated into release/v3.8.42 (round 3). Dead Phind removal + HuggingChat dedupe, verified complete.
This commit is contained in:
@@ -3,12 +3,12 @@
|
||||
## Project
|
||||
|
||||
Unified AI proxy/router — route any LLM through one endpoint. Multi-provider support
|
||||
with **237 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
|
||||
with **236 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
|
||||
Cohere, NVIDIA, Cerebras, Pollinations, Puter, Cloudflare AI, HuggingFace, DeepInfra,
|
||||
SambaNova, Meta Llama API, Moonshot AI, AI21 Labs, Databricks, Snowflake, and many more)
|
||||
with **MCP Server** (94 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
|
||||
|
||||
> **Live counts (v3.8.40)**: providers 237 · MCP tools 94 · MCP scopes 30 · A2A skills 6 ·
|
||||
> **Live counts (v3.8.40)**: providers 236 · MCP tools 94 · MCP scopes 30 · A2A skills 6 ·
|
||||
> open-sse services 298 · routing strategies 17 · auto-combo scoring factors 12 ·
|
||||
> DB modules 94 · DB migrations 106 · base tables 17 · search providers 11 ·
|
||||
> i18n locales 42. **Refresh with `npm run check:docs-all`.**
|
||||
|
||||
@@ -35,7 +35,7 @@ For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep archit
|
||||
|
||||
## Project at a Glance
|
||||
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 237 LLM providers, auto-fallback.
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 236 LLM providers, auto-fallback.
|
||||
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
|
||||
12
README.md
12
README.md
@@ -6,7 +6,7 @@
|
||||
|
||||
# 🚀 OmniRoute — The Free AI Gateway
|
||||
|
||||
### Never stop coding. Connect every AI tool to **237 providers** — **50+ free** — through one endpoint.
|
||||
### Never stop coding. Connect every AI tool to **236 providers** — **50+ free** — through one endpoint.
|
||||
|
||||
**Plug Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini. Auto-fallback.**
|
||||
<br/>
|
||||
@@ -138,11 +138,11 @@
|
||||
|
||||
</div>
|
||||
|
||||
> One endpoint. **237 providers.** Never stop building — and let OmniRoute pick the cheapest one that works.
|
||||
> One endpoint. **236 providers.** Never stop building — and let OmniRoute pick the cheapest one that works.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="33%" valign="top"><b>🚫 Never hit limits</b><br/><sub>Auto-fallback across 237 providers in milliseconds. Quota out? Next provider takes over — zero downtime.</sub></td>
|
||||
<td width="33%" valign="top"><b>🚫 Never hit limits</b><br/><sub>Auto-fallback across 236 providers in milliseconds. Quota out? Next provider takes over — zero downtime.</sub></td>
|
||||
<td width="33%" valign="top"><b>💸 Save up to 95% tokens</b><br/><sub>RTK + Caveman stacked compression cuts 15–95% of eligible tokens (~89% avg on tool-heavy sessions).</sub></td>
|
||||
<td width="33%" valign="top"><b>🆓 $0 to start</b><br/><sub>50+ providers with a free tier, 11 free <i>forever</i> (Kiro, Qoder, Pollinations, LongCat…). No card needed.</sub></td>
|
||||
</tr>
|
||||
@@ -301,7 +301,7 @@ Result: 4 layers of fallback = zero downtime
|
||||
- **💸 Cost telemetry everywhere** — `X-OmniRoute-*` cost/usage headers on every endpoint (including media), a non-token cost engine, a cache-HIT `X-OmniRoute-Cost-Saved` header, and per-key USD spend quotas. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🧠 Memory you control** — opt-in int8 vector quantization (Qdrant + sqlite-vec), memory off by default, and a per-request `x-omniroute-no-memory` header. → [Memory](docs/frameworks/MEMORY.md)
|
||||
- **🛡️ Security** — a prompt-injection guard across every LLM route (backed by a red-team suite), plus a free DuckDuckGo last-resort web search. → [Guardrails](docs/security/GUARDRAILS.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 237-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech / transcription / music / video), and one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`). → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 236-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech / transcription / music / video), and one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`). → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel), one-click **Cloudflare Workers** and **Deno Deploy** relay deployers wired into the proxy pool, and an optional Bifrost Go sidecar that offloads the hottest relay path (`BIFROST_BASE_URL`, with automatic fallback to the TypeScript path on timeout) — now with a relay-backend selector (`OMNIROUTE_RELAY_BACKEND=ts|bifrost|auto`) so the `/v1/relay` endpoint stays the stable surface while choosing the fastest backend internally. → [Environment](docs/reference/ENVIRONMENT.md)
|
||||
|
||||
<br/>
|
||||
@@ -348,7 +348,7 @@ Result: 4 layers of fallback = zero downtime
|
||||
|
||||
</div>
|
||||
|
||||
> The most complete catalog of any open-source router: **237 providers**, **50+ with a free tier**, **11 free forever**.
|
||||
> The most complete catalog of any open-source router: **236 providers**, **50+ with a free tier**, **11 free forever**.
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -804,7 +804,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
|
||||
**Will I be charged by OmniRoute?** No — it's free, open-source software on your machine. You only pay paid providers directly. OmniRoute has no billing system.
|
||||
**Are FREE providers really unlimited?** Mostly — Qoder, Pollinations, LongCat, and Cloudflare are free with no per-account credit cap. Kiro is free too but capped at ~50 credits/month per account. Stack multiple free providers in a combo and auto-fallback keeps you serving for $0.
|
||||
**Will compression hurt quality?** No — it only compresses the **input**; code, URLs, JSON are always protected.
|
||||
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 237 providers.
|
||||
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 236 providers.
|
||||
|
||||
📖 [User Guide](docs/guides/USER_GUIDE.md) · [API Reference](docs/reference/API_REFERENCE.md) · [Environment Config](docs/reference/ENVIRONMENT.md)
|
||||
|
||||
|
||||
@@ -159,7 +159,6 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve
|
||||
| `freemodel-dev` | unknown | The Terms of Service page (freemodel.dev/terms) returned only a header with no readable content via WebFetch; no clause… |
|
||||
| `gitlawb` | unknown | No ToS or acceptable-use policy found; proxy/resale restrictions unknown — assume caution for self-hosted proxy use. |
|
||||
| `liquid` | unknown | No hosted API exists to proxy; open-source model commercial use is free for orgs under $10M annual revenue. No self-hos… |
|
||||
| `phind` | unknown | Service is discontinued; ToS is no longer relevant. No proxy use is possible. |
|
||||
| `theoldllm` | unknown | No terms of service document was found on the site; proxying, resale, or self-hosted use policy is entirely undocumente… |
|
||||
| `yi` | unknown | ToS not publicly accessible without login; no proxy/resale clauses could be reviewed. Self-hosted personal proxy use st… |
|
||||
| `comfyui` | ok | GPL-3.0 open-source license explicitly permits self-hosted personal proxy use; Comfy Org ToS confirms commercial use of… |
|
||||
@@ -308,7 +307,7 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve
|
||||
- **`nvidia`** — The "40 RPM, 70+ models" rate limit element matches the catalog, but the freeNote framing as a simple dev-access tier undersells that the old one-time credit pool has been removed — access is now tru…
|
||||
- **`ollama-cloud`** — Our shipped freeNote is "(none)" — this is stale. Ollama Cloud launched a cloud inference product with a genuine free tier that provides light weekly GPU-time-based access to hosted open models.
|
||||
- **`openrouter`** — RPD tightened from 200 to 50 for zero-credit accounts (RPM unchanged at 20). The catalog note was accurate on RPM but overstated the RPD by 4x for the no-credits baseline tier.
|
||||
- **`phind`** — Our shipped freeNote describes an active free chat/code-search service — but Phind is fully discontinued as of January 16, 2026. The catalog entry should be marked discontinued and removed from activ…
|
||||
- **`phind`** — Phind shut down on January 16, 2026. The provider has now been **fully removed** from the catalog (registry, executor, and both the web-cookie and API-key catalog entries) — matching the dead-service-removal precedent (#5246 Gemini CLI).
|
||||
- **`pollinations`** — Partially matches — the "no API key required" claim is still true for anonymous access, but the catalog freeNote omits that: (1) rate limits do apply (interval throttle of ~1 req/6-15s for anonymous …
|
||||
- **`predibase`** — The shipped freeNote ($25 free trial credits, 30-day validity) still matches current documentation. However, the catalog omits the concurrent 20,000 tokens/day serverless rate limit that applies duri…
|
||||
- **`publicai`** — The shipped freeNote ("Free community inference tier") is broadly accurate but understates the specificity: the 20 RPM rate limit is now documented. No major tightening found; the service remains fre…
|
||||
|
||||
@@ -10,7 +10,7 @@ lastUpdated: 2026-06-30
|
||||
> Regenerate with: `npm run gen:provider-reference`
|
||||
> **Last generated:** 2026-06-30
|
||||
|
||||
Total providers: **237**. See category breakdown below.
|
||||
Total providers: **236**. See category breakdown below.
|
||||
|
||||
## Categories
|
||||
|
||||
@@ -56,7 +56,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `windsurf` | `ws` | Windsurf (Devin CLI) | OAuth | [link](https://windsurf.com) | In the Windsurf / VS Code IDE, open the command palette and run `Windsurf: Provide Auth Token` (or click the Jupyter "Get Windsurf Authentication Token" button), then copy the shown token and paste it here. Note: opening windsurf.com/show-auth-token directly only renders a "Redirecting" page — the IDE must initiate the flow (it adds a `?state=...` param) for the token to appear. |
|
||||
| `zed` | `zd` | Zed IDE | OAuth | [link](https://zed.dev) | Zed stores LLM provider credentials (OpenAI, Anthropic, Google, Mistral, xAI) in the OS keychain. Use the Import button below to discover and import them automatically. |
|
||||
|
||||
## Web Cookie Providers (24)
|
||||
## Web Cookie Providers (23)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
@@ -75,9 +75,8 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `inner-ai` | `in-ai` | Inner.ai (Subscription) | Web cookie | [link](https://app.innerai.com) | Paste your token cookie and email separated by a space: open DevTools → Application → Cookies → .innerai.com, copy the token value, then append a space and your Inner.ai login email. Example: eyJhbG... user@example.com |
|
||||
| `kimi-web` | `kimi-web` | Kimi Web (Moonshot AI) | Web cookie | [link](https://kimi.moonshot.cn) | Paste your session cookie from kimi.moonshot.cn (DevTools → Application → Cookies) |
|
||||
| `lmarena` | `lma` | LMArena (Free) | Web cookie | [link](https://lmarena.ai) | Paste the full Cookie header from lmarena.ai (DevTools → Network → request → Cookie). The session is now split across arena-auth-prod-v1.0, .1, … — copy the whole header. Optional — works with free tier for basic comparisons. |
|
||||
| `muse-spark-web` | `ms-web` | Muse Spark Web (Meta AI) | Web cookie | [link](https://www.meta.ai) | Paste your abra_sess value or full cookie header from meta.ai |
|
||||
| `muse-spark-web` | `ms-web` | Muse Spark Web (Meta AI) | Web cookie | [link](https://www.meta.ai) | Paste your ecto_1_sess value or full cookie header from meta.ai |
|
||||
| `perplexity-web` | `pplx-web` | Perplexity Web (Pro/Max) | Web cookie | [link](https://www.perplexity.ai) | Paste your __Secure-next-auth.session-token cookie value from perplexity.ai |
|
||||
| `phind` | `ph` | Phind (Free) | Web cookie | [link](https://www.phind.com) | ⚠️ **DEPRECATED.** Phind shut down its API (2026-01); the /api/chat endpoint no longer serves (sweep 2026-06-19). |
|
||||
| `poe-web` | `poe` | Poe Web (Subscription) | Web cookie | [link](https://poe.com) | Paste your p-b cookie value from poe.com (DevTools → Application → Cookies → p-b) |
|
||||
| `qwen-web` | `qwen-web` | Qwen Web (Free) | Web cookie | [link](https://chat.qwen.ai) | Open chat.qwen.ai, log in, then open DevTools → Application → Local Storage → copy the "token" value (or use tongyi_sso_ticket cookie as Bearer token). |
|
||||
| `t3-web` | `t3chat` | t3.chat (Pro/Free) | Web cookie | [link](https://t3.chat) | Open t3.chat in your browser, log in, then open DevTools → Application → Local Storage → https://t3.chat. Copy the value of 'convex-session-id'. Also open DevTools → Network, copy the Cookie header from any request. Paste both values here. See provider setup docs for a step-by-step guide. |
|
||||
@@ -85,7 +84,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `venice-web` | `ven` | Venice Web (Privacy) | Web cookie | [link](https://venice.ai) | Paste your session cookie from venice.ai (DevTools → Application → Cookies) |
|
||||
| `zenmux-free` | `zmf` | ZenMux Free (Web) | Web cookie | [link](https://zenmux.ai) | Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days. |
|
||||
|
||||
## API Key Providers (paid / paid-with-free-credits) (160)
|
||||
## API Key Providers (paid / paid-with-free-credits) (158)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
@@ -155,7 +154,6 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `hackclub` | `hc` | Hackclub AI | API key, aggregator | [link](https://ai.hackclub.com) | Sign in with your Hack Club account at ai.hackclub.com. |
|
||||
| `haiper` | `hp` | Haiper | API key, video | [link](https://haiper.ai) | Get API key at haiper.ai/haiper-api |
|
||||
| `heroku` | `heroku` | Heroku AI | API key, enterprise | [link](https://www.heroku.com) | — |
|
||||
| `huggingchat` | `huggingchat` | HuggingChat | API key | [link](https://huggingface.co/chat) | No API key required for basic access. |
|
||||
| `huggingface` | `hf` | HuggingFace | API key | [link](https://huggingface.co) | Free Inference API for thousands of models (Whisper, VITS, SDXL…) |
|
||||
| `hyperbolic` | `hyp` | Hyperbolic | API key | [link](https://hyperbolic.xyz) | $1-5 trial credits on signup for serverless inference |
|
||||
| `ideogram` | `ideo` | Ideogram | API key | [link](https://ideogram.ai) | Get API key at ideogram.ai/docs/api |
|
||||
@@ -203,7 +201,6 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `orcarouter` | `orcarouter` | OrcaRouter | API key | [link](https://www.orcarouter.ai) | — |
|
||||
| `ovhcloud` | `ovh` | OVHcloud AI | API key | [link](https://www.ovhcloud.com) | — |
|
||||
| `perplexity` | `pplx` | Perplexity | API key | [link](https://www.perplexity.ai) | — |
|
||||
| `phind` | `phind` | Phind | API key | [link](https://phind.com) | Get API key at phind.com |
|
||||
| `piapi` | `pi` | PiAPI | API key, aggregator | [link](https://piapi.ai) | — |
|
||||
| `pioneer` | `pn` | Pioneer AI | API key | [link](https://pioneer.ai) | $75 free usage credits — no credit card required |
|
||||
| `poe` | `poe` | Poe | API key, aggregator | [link](https://creator.poe.com/api-reference) | Bearer API key for the Poe OpenAI-compatible API. |
|
||||
|
||||
@@ -81,7 +81,6 @@ import { monsterapiProvider } from "./registry/monsterapi/index.ts";
|
||||
import { sensenovaProvider } from "./registry/sensenova/index.ts";
|
||||
import { hyperbolicProvider } from "./registry/hyperbolic/index.ts";
|
||||
import { lambda_aiProvider } from "./registry/lambda-ai/index.ts";
|
||||
import { phindProvider } from "./registry/phind/index.ts";
|
||||
import { t3_webProvider } from "./registry/t3-web/index.ts";
|
||||
import { klusterProvider } from "./registry/kluster/index.ts";
|
||||
import { iflytekProvider } from "./registry/iflytek/index.ts";
|
||||
@@ -253,7 +252,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
sensenova: sensenovaProvider,
|
||||
hyperbolic: hyperbolicProvider,
|
||||
"lambda-ai": lambda_aiProvider,
|
||||
phind: phindProvider,
|
||||
"t3-web": t3_webProvider,
|
||||
kluster: klusterProvider,
|
||||
iflytek: iflytekProvider,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const phindProvider: RegistryEntry = {
|
||||
id: "phind",
|
||||
alias: "ph",
|
||||
format: "openai",
|
||||
executor: "phind",
|
||||
baseUrl: "https://www.phind.com/api/chat",
|
||||
authType: "apikey",
|
||||
authHeader: "cookie",
|
||||
models: [
|
||||
{ id: "phind-model", name: "Phind Model (Auto)" },
|
||||
{ id: "gpt-4o", name: "GPT-4o (via Phind)" },
|
||||
{ id: "claude-3.5-sonnet", name: "Claude 3.5 Sonnet (via Phind)" },
|
||||
],
|
||||
};
|
||||
@@ -40,7 +40,6 @@ import { T3ChatWebExecutor } from "./t3-chat-web.ts";
|
||||
import { ClaudeWebExecutor } from "./claude-web.ts";
|
||||
import { InnerAiExecutor } from "./inner-ai.ts";
|
||||
import { HuggingChatExecutor } from "./huggingchat.ts";
|
||||
import { PhindExecutor } from "./phind.ts";
|
||||
import { PoeWebExecutor } from "./poe-web.ts";
|
||||
import { VeniceWebExecutor } from "./venice-web.ts";
|
||||
import { V0VercelWebExecutor } from "./v0-vercel-web.ts";
|
||||
@@ -128,8 +127,6 @@ const executors = {
|
||||
"in-ai": new InnerAiExecutor(), // Alias
|
||||
huggingchat: new HuggingChatExecutor(),
|
||||
hc: new HuggingChatExecutor(), // Alias
|
||||
phind: new PhindExecutor(),
|
||||
ph: new PhindExecutor(), // Alias
|
||||
"poe-web": new PoeWebExecutor(),
|
||||
poe: new PoeWebExecutor(), // Alias
|
||||
"venice-web": new VeniceWebExecutor(),
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
/**
|
||||
* PhindExecutor — Free Dev-Focused AI Chat via phind.com
|
||||
*
|
||||
* Routes requests through Phind's chat API.
|
||||
* Free tier available. Uses session cookie for auth.
|
||||
*
|
||||
* Endpoint: POST https://www.phind.com/api/agent
|
||||
* Auth: Session cookie from phind.com
|
||||
* SSE response with data: prefixed JSON chunks (OpenAI-compatible delta format)
|
||||
*/
|
||||
import { BaseExecutor, type ExecuteInput } from "./base.ts";
|
||||
import { makeExecutorErrorResult as makeErrorResult, normalizeCookie } from "../utils/error.ts";
|
||||
|
||||
const BASE_URL = "https://www.phind.com";
|
||||
const CHAT_URL = `${BASE_URL}/api/agent`;
|
||||
const USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
|
||||
|
||||
export class PhindExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("phind", { id: "phind", baseUrl: "https://www.phind.com" });
|
||||
}
|
||||
|
||||
async execute(input: ExecuteInput) {
|
||||
const { body, credentials, signal, stream: wantStream } = input;
|
||||
const bodyObj = (body || {}) as Record<string, unknown>;
|
||||
const rawCookie = String(credentials?.apiKey ?? "").trim();
|
||||
const cookie = normalizeCookie(rawCookie);
|
||||
|
||||
// Build Phind-format messages
|
||||
const messages = (bodyObj.messages as Array<{ role: string; content: string }>) || [];
|
||||
const phindMessages = messages.map((m) => ({ role: m.role, content: m.content }));
|
||||
const modelId = (bodyObj.model as string) || "phind-model";
|
||||
// Last user message as userInput (Phind expects this field)
|
||||
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
||||
const userInput = lastUserMsg?.content || "";
|
||||
|
||||
const reqBody = {
|
||||
userInput,
|
||||
messages: phindMessages,
|
||||
requestedModel: modelId,
|
||||
webSearchMode: "auto",
|
||||
isChromeExtension: false,
|
||||
language: "en-US",
|
||||
date: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const reqHeaders: Record<string, string> = {
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
"User-Agent": USER_AGENT,
|
||||
Accept: "text/event-stream",
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
};
|
||||
if (cookie) reqHeaders.Cookie = cookie;
|
||||
|
||||
let upstream: Response;
|
||||
try {
|
||||
upstream = await fetch(CHAT_URL, {
|
||||
method: "POST",
|
||||
headers: reqHeaders,
|
||||
body: JSON.stringify(reqBody),
|
||||
signal,
|
||||
});
|
||||
} catch (err) {
|
||||
return makeErrorResult(
|
||||
502,
|
||||
`Phind fetch failed: ${err instanceof Error ? err.message : "unknown"}`,
|
||||
body,
|
||||
CHAT_URL
|
||||
);
|
||||
}
|
||||
|
||||
if (!upstream.ok) {
|
||||
const errText = await upstream.text().catch(() => "");
|
||||
return makeErrorResult(upstream.status, `Phind error: ${errText}`, body, CHAT_URL);
|
||||
}
|
||||
|
||||
// Phind always returns SSE — parse it for both streaming and non-streaming
|
||||
if (!upstream.body) {
|
||||
return makeErrorResult(502, "Phind returned empty response body", body, CHAT_URL);
|
||||
}
|
||||
|
||||
if (!wantStream) {
|
||||
// Collect all SSE chunks into a single response
|
||||
const reader = upstream.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
let fullText = "";
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data:")) continue;
|
||||
const data = line.slice(5).trim();
|
||||
if (data === "[DONE]") continue;
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const text = parsed.choices?.[0]?.delta?.content || parsed.content || "";
|
||||
if (text) fullText += text;
|
||||
} catch {
|
||||
// Skip unparseable chunks
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (!signal?.aborted) {
|
||||
return makeErrorResult(
|
||||
502,
|
||||
`Phind stream read failed: ${err instanceof Error ? err.message : "unknown"}`,
|
||||
body,
|
||||
CHAT_URL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
response: new Response(
|
||||
JSON.stringify({
|
||||
id: `chatcmpl-ph-${Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: modelId,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: "assistant", content: fullText },
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: Math.ceil((userInput || "").length / 4),
|
||||
completion_tokens: Math.ceil(fullText.length / 4),
|
||||
total_tokens: Math.ceil(((userInput || "").length + fullText.length) / 4),
|
||||
},
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
),
|
||||
url: CHAT_URL,
|
||||
headers: reqHeaders,
|
||||
transformedBody: reqBody,
|
||||
};
|
||||
}
|
||||
|
||||
// Streaming: transform Phind SSE to OpenAI format
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
const sseStream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const reader = upstream.body!.getReader();
|
||||
let buffer = "";
|
||||
try {
|
||||
// Initial role chunk
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
`data: ${JSON.stringify({
|
||||
id: `chatcmpl-ph-${Date.now()}`,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: modelId,
|
||||
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
|
||||
})}\n\n`
|
||||
)
|
||||
);
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data:")) continue;
|
||||
const data = line.slice(5).trim();
|
||||
if (data === "[DONE]") {
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const text = parsed.choices?.[0]?.delta?.content || parsed.content || "";
|
||||
if (text) {
|
||||
const chunk = {
|
||||
id: `chatcmpl-ph-${Date.now()}`,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: modelId,
|
||||
choices: [{ index: 0, delta: { content: text }, finish_reason: null }],
|
||||
};
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
||||
}
|
||||
} catch {
|
||||
// Skip unparseable chunks
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (!signal?.aborted) {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
`data: ${JSON.stringify({
|
||||
id: `chatcmpl-ph-${Date.now()}`,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: modelId,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
content: `[Stream error: ${err instanceof Error ? err.message : String(err)}]`,
|
||||
},
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
})}\n\n`
|
||||
)
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
response: new Response(sseStream, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
}),
|
||||
url: CHAT_URL,
|
||||
headers: reqHeaders,
|
||||
transformedBody: reqBody,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4818,8 +4818,6 @@
|
||||
"checkWebToken": "Check token",
|
||||
"huggingchatLabel": "HuggingChat (Free)",
|
||||
"huggingchatDesc": "Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "Phind (Free)",
|
||||
"phindDesc": "Free dev-focused AI chat",
|
||||
"poeWebLabel": "Poe Web",
|
||||
"poeWebDesc": "Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "Venice Web",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "Controlla il gettone",
|
||||
"huggingchatLabel": "__MISSING__:HuggingChat (Free)",
|
||||
"huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "__MISSING__:Phind (Free)",
|
||||
"phindDesc": "__MISSING__:Free dev-focused AI chat",
|
||||
"poeWebLabel": "__MISSING__:Poe Web",
|
||||
"poeWebDesc": "__MISSING__:Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "__MISSING__:Venice Web",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "トークンをチェックする",
|
||||
"huggingchatLabel": "__MISSING__:HuggingChat (Free)",
|
||||
"huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "__MISSING__:Phind (Free)",
|
||||
"phindDesc": "__MISSING__:Free dev-focused AI chat",
|
||||
"poeWebLabel": "__MISSING__:Poe Web",
|
||||
"poeWebDesc": "__MISSING__:Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "__MISSING__:Venice Web",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "토큰 확인",
|
||||
"huggingchatLabel": "__MISSING__:HuggingChat (Free)",
|
||||
"huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "__MISSING__:Phind (Free)",
|
||||
"phindDesc": "__MISSING__:Free dev-focused AI chat",
|
||||
"poeWebLabel": "__MISSING__:Poe Web",
|
||||
"poeWebDesc": "__MISSING__:Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "__MISSING__:Venice Web",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "टोकन तपासा",
|
||||
"huggingchatLabel": "__MISSING__:HuggingChat (Free)",
|
||||
"huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "__MISSING__:Phind (Free)",
|
||||
"phindDesc": "__MISSING__:Free dev-focused AI chat",
|
||||
"poeWebLabel": "__MISSING__:Poe Web",
|
||||
"poeWebDesc": "__MISSING__:Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "__MISSING__:Venice Web",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "Semak token",
|
||||
"huggingchatLabel": "__MISSING__:HuggingChat (Free)",
|
||||
"huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "__MISSING__:Phind (Free)",
|
||||
"phindDesc": "__MISSING__:Free dev-focused AI chat",
|
||||
"poeWebLabel": "__MISSING__:Poe Web",
|
||||
"poeWebDesc": "__MISSING__:Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "__MISSING__:Venice Web",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "Controleer token",
|
||||
"huggingchatLabel": "__MISSING__:HuggingChat (Free)",
|
||||
"huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "__MISSING__:Phind (Free)",
|
||||
"phindDesc": "__MISSING__:Free dev-focused AI chat",
|
||||
"poeWebLabel": "__MISSING__:Poe Web",
|
||||
"poeWebDesc": "__MISSING__:Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "__MISSING__:Venice Web",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "Sjekk token",
|
||||
"huggingchatLabel": "__MISSING__:HuggingChat (Free)",
|
||||
"huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "__MISSING__:Phind (Free)",
|
||||
"phindDesc": "__MISSING__:Free dev-focused AI chat",
|
||||
"poeWebLabel": "__MISSING__:Poe Web",
|
||||
"poeWebDesc": "__MISSING__:Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "__MISSING__:Venice Web",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "Suriin ang token",
|
||||
"huggingchatLabel": "__MISSING__:HuggingChat (Free)",
|
||||
"huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "__MISSING__:Phind (Free)",
|
||||
"phindDesc": "__MISSING__:Free dev-focused AI chat",
|
||||
"poeWebLabel": "__MISSING__:Poe Web",
|
||||
"poeWebDesc": "__MISSING__:Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "__MISSING__:Venice Web",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "Sprawdź token",
|
||||
"huggingchatLabel": "__MISSING__:HuggingChat (Free)",
|
||||
"huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "__MISSING__:Phind (Free)",
|
||||
"phindDesc": "__MISSING__:Free dev-focused AI chat",
|
||||
"poeWebLabel": "__MISSING__:Poe Web",
|
||||
"poeWebDesc": "__MISSING__:Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "__MISSING__:Venice Web",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "Verifique o token",
|
||||
"huggingchatLabel": "__MISSING__:HuggingChat (Free)",
|
||||
"huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "__MISSING__:Phind (Free)",
|
||||
"phindDesc": "__MISSING__:Free dev-focused AI chat",
|
||||
"poeWebLabel": "__MISSING__:Poe Web",
|
||||
"poeWebDesc": "__MISSING__:Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "__MISSING__:Venice Web",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "Verifique o token",
|
||||
"huggingchatLabel": "__MISSING__:HuggingChat (Free)",
|
||||
"huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "__MISSING__:Phind (Free)",
|
||||
"phindDesc": "__MISSING__:Free dev-focused AI chat",
|
||||
"poeWebLabel": "__MISSING__:Poe Web",
|
||||
"poeWebDesc": "__MISSING__:Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "__MISSING__:Venice Web",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "Verificați jetonul",
|
||||
"huggingchatLabel": "__MISSING__:HuggingChat (Free)",
|
||||
"huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "__MISSING__:Phind (Free)",
|
||||
"phindDesc": "__MISSING__:Free dev-focused AI chat",
|
||||
"poeWebLabel": "__MISSING__:Poe Web",
|
||||
"poeWebDesc": "__MISSING__:Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "__MISSING__:Venice Web",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "Проверить токен",
|
||||
"huggingchatLabel": "HuggingChat (Бесплатно)",
|
||||
"huggingchatDesc": "Бесплатный LLM чат через huggingface.co/chat",
|
||||
"phindLabel": "Phind (Бесплатно)",
|
||||
"phindDesc": "Бесплатный AI-чат для разработчиков",
|
||||
"poeWebLabel": "Poe Web",
|
||||
"poeWebDesc": "Многофункциональный чат через poe.com",
|
||||
"veniceWebLabel": "Венеция Веб",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "Kontrolný token",
|
||||
"huggingchatLabel": "__MISSING__:HuggingChat (Free)",
|
||||
"huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "__MISSING__:Phind (Free)",
|
||||
"phindDesc": "__MISSING__:Free dev-focused AI chat",
|
||||
"poeWebLabel": "__MISSING__:Poe Web",
|
||||
"poeWebDesc": "__MISSING__:Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "__MISSING__:Venice Web",
|
||||
|
||||
@@ -4804,8 +4804,6 @@
|
||||
"checkWebToken": "Kontrollera token",
|
||||
"huggingchatLabel": "__MISSING__:HuggingChat (Free)",
|
||||
"huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat",
|
||||
"phindLabel": "__MISSING__:Phind (Free)",
|
||||
"phindDesc": "__MISSING__:Free dev-focused AI chat",
|
||||
"poeWebLabel": "__MISSING__:Poe Web",
|
||||
"poeWebDesc": "__MISSING__:Multi-model chat via poe.com",
|
||||
"veniceWebLabel": "__MISSING__:Venice Web",
|
||||
|
||||
@@ -4660,8 +4660,6 @@
|
||||
"checkWebToken": "检查令牌",
|
||||
"huggingchatLabel": "HuggingChat(免费)",
|
||||
"huggingchatDesc": "通过 huggingface.co/chat 免费使用 LLM 聊天",
|
||||
"phindLabel": "Phind(免费)",
|
||||
"phindDesc": "免费的开发者专用 AI 聊天",
|
||||
"poeWebLabel": "Poe 网页",
|
||||
"poeWebDesc": "通过 poe.com 进行多模型聊天",
|
||||
"veniceWebLabel": "威尼斯网络",
|
||||
|
||||
@@ -132,7 +132,6 @@ const VENDOR_PREFIXES = [
|
||||
"alibaba/",
|
||||
"nvidia/",
|
||||
"01-ai/",
|
||||
"phind/",
|
||||
"zerox/",
|
||||
"together/",
|
||||
"fireworks/",
|
||||
|
||||
@@ -118,7 +118,6 @@ import OpenCodeMonoIcon from "@lobehub/icons/es/OpenCode/components/Mono";
|
||||
import OpenRouterMonoIcon from "@lobehub/icons/es/OpenRouter/components/Mono";
|
||||
import PerplexityColorIcon from "@lobehub/icons/es/Perplexity/components/Color";
|
||||
import PerplexityMonoIcon from "@lobehub/icons/es/Perplexity/components/Mono";
|
||||
import PhindMonoIcon from "@lobehub/icons/es/Phind/components/Mono";
|
||||
import PoeColorIcon from "@lobehub/icons/es/Poe/components/Color";
|
||||
import PoeMonoIcon from "@lobehub/icons/es/Poe/components/Mono";
|
||||
import PollinationsMonoIcon from "@lobehub/icons/es/Pollinations/components/Mono";
|
||||
@@ -268,7 +267,6 @@ const LOBE_ICON_COMPONENTS = {
|
||||
OpenCode: { mono: OpenCodeMonoIcon },
|
||||
OpenRouter: { mono: OpenRouterMonoIcon },
|
||||
Perplexity: { mono: PerplexityMonoIcon, color: PerplexityColorIcon },
|
||||
Phind: { mono: PhindMonoIcon },
|
||||
Poe: { mono: PoeMonoIcon, color: PoeColorIcon },
|
||||
Pollinations: { mono: PollinationsMonoIcon },
|
||||
Qoder: { mono: QoderMonoIcon, color: QoderColorIcon },
|
||||
@@ -426,7 +424,6 @@ const LOBE_PROVIDER_ALIASES = {
|
||||
"perplexity-search": "Perplexity",
|
||||
"perplexity-web": "Perplexity",
|
||||
poe: "Poe",
|
||||
phind: "Phind",
|
||||
pollinations: "Pollinations",
|
||||
qoder: "Qoder",
|
||||
qwen: "Qwen",
|
||||
|
||||
@@ -223,19 +223,6 @@ export const APIKEY_PROVIDERS_FRONTIER = {
|
||||
hasFree: true,
|
||||
freeNote: "Free tier: 50 RPM, 500,000 TPM — no credit card",
|
||||
},
|
||||
phind: {
|
||||
id: "phind",
|
||||
alias: "phind",
|
||||
name: "Phind",
|
||||
icon: "search",
|
||||
color: "#EC4899",
|
||||
textIcon: "PH",
|
||||
website: "https://phind.com",
|
||||
hasFree: false,
|
||||
freeNote: "Discontinued 2026 — phind.com shut down (2026-01); no free tier.",
|
||||
passthroughModels: true,
|
||||
authHint: "Get API key at phind.com",
|
||||
},
|
||||
"arcee-ai": {
|
||||
id: "arcee-ai",
|
||||
alias: "arcee",
|
||||
|
||||
@@ -249,19 +249,6 @@ export const APIKEY_PROVIDERS_INFERENCE = {
|
||||
hasFree: true,
|
||||
freeNote: "$1 free credits, refreshes every 4 weeks",
|
||||
},
|
||||
huggingchat: {
|
||||
id: "huggingchat",
|
||||
alias: "huggingchat",
|
||||
name: "HuggingChat",
|
||||
icon: "chat",
|
||||
color: "#FFD21E",
|
||||
textIcon: "HC",
|
||||
website: "https://huggingface.co/chat",
|
||||
hasFree: true,
|
||||
freeNote: "Free chat with open models (Llama, Mistral, etc.).",
|
||||
passthroughModels: true,
|
||||
authHint: "No API key required for basic access.",
|
||||
},
|
||||
monsterapi: {
|
||||
id: "monsterapi",
|
||||
alias: "monster",
|
||||
|
||||
@@ -200,24 +200,6 @@ export const WEB_COOKIE_PROVIDERS = {
|
||||
"Paste your hf-chat cookie value from huggingface.co/chat (DevTools → Application → Cookies → hf-chat). Optional — works without auth for basic use.",
|
||||
riskNoticeVariant: "webCookie",
|
||||
},
|
||||
phind: {
|
||||
id: "phind",
|
||||
alias: "ph",
|
||||
name: "Phind (Free)",
|
||||
icon: "auto_awesome",
|
||||
color: "#000000",
|
||||
textIcon: "PH",
|
||||
website: "https://www.phind.com",
|
||||
hasFree: false,
|
||||
freeNote: "Discontinued 2026 — phind.com shut down (2026-01); no free tier.",
|
||||
authHint:
|
||||
"Paste your session cookie from phind.com (DevTools → Application → Cookies). Optional — works with free tier.",
|
||||
subscriptionRisk: true,
|
||||
riskNoticeVariant: "deprecated",
|
||||
deprecated: true,
|
||||
deprecationReason:
|
||||
"Phind shut down its API (2026-01); the /api/chat endpoint no longer serves (sweep 2026-06-19).",
|
||||
},
|
||||
"poe-web": {
|
||||
id: "poe-web",
|
||||
alias: "poe",
|
||||
|
||||
@@ -129,13 +129,6 @@ export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = {
|
||||
acceptsFullCookieHeader: true,
|
||||
storageKeys: ["cookie", "hf-chat"],
|
||||
},
|
||||
phind: {
|
||||
kind: "cookie",
|
||||
credentialName: "phind_session",
|
||||
placeholder: "phind_session=... or full Cookie header from phind.com",
|
||||
acceptsFullCookieHeader: true,
|
||||
storageKeys: ["cookie", "phind_session"],
|
||||
},
|
||||
"poe-web": {
|
||||
kind: "cookie",
|
||||
credentialName: "p-b",
|
||||
|
||||
@@ -3175,29 +3175,6 @@
|
||||
"stream": "https://www.perplexity.ai/rest/sse/perplexity_ask"
|
||||
}
|
||||
},
|
||||
"phind": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://www.phind.com/api/chat",
|
||||
"stream": "https://www.phind.com/api/chat"
|
||||
}
|
||||
},
|
||||
"pioneer": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
|
||||
@@ -9,18 +9,23 @@ import assert from "node:assert";
|
||||
describe("2026 discontinued free tiers — providers.ts hasFree reconciliation", () => {
|
||||
it("APIKEY_PROVIDERS dead tiers no longer advertise a free tier", async () => {
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
||||
for (const id of ["chutes", "kluster", "glhf", "phind", "gitlawb", "gitlawb-gmi", "aimlapi", "yi"]) {
|
||||
// These providers still operate (an API key works) but lost their free tier, so
|
||||
// they are KEPT with hasFree:false. phind is NOT here: the whole phind.com service
|
||||
// shut down 2026-01-16, so it was removed entirely (registry/executor/catalogs),
|
||||
// matching the dead-service-removal precedent (#5246 Gemini CLI).
|
||||
for (const id of ["chutes", "kluster", "glhf", "gitlawb", "gitlawb-gmi", "aimlapi", "yi"]) {
|
||||
const p = (APIKEY_PROVIDERS as Record<string, { hasFree?: boolean }>)[id];
|
||||
assert.ok(p, `${id} should still exist in APIKEY_PROVIDERS (provider not removed, only its free flag)`);
|
||||
assert.strictEqual(p.hasFree, false, `${id} should have hasFree:false (discontinued in 2026)`);
|
||||
}
|
||||
});
|
||||
|
||||
it("WEB_COOKIE_PROVIDERS phind (web/cookie path) no longer advertises a free tier", async () => {
|
||||
const { WEB_COOKIE_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
||||
const p = (WEB_COOKIE_PROVIDERS as Record<string, { hasFree?: boolean }>)["phind"];
|
||||
assert.ok(p, "phind should still exist in WEB_COOKIE_PROVIDERS");
|
||||
assert.strictEqual(p.hasFree, false, "phind web/cookie should have hasFree:false (phind.com shut down 2026-01)");
|
||||
it("phind is fully removed (service shut down 2026-01) from both catalogs", async () => {
|
||||
const { APIKEY_PROVIDERS, WEB_COOKIE_PROVIDERS } = await import(
|
||||
"../../src/shared/constants/providers.ts"
|
||||
);
|
||||
assert.ok(!("phind" in APIKEY_PROVIDERS), "phind must not be in APIKEY_PROVIDERS");
|
||||
assert.ok(!("phind" in WEB_COOKIE_PROVIDERS), "phind must not be in WEB_COOKIE_PROVIDERS");
|
||||
});
|
||||
|
||||
it("intentionally-kept providers still advertise free (genuinely free / ToS-flagged, not flipped)", async () => {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const mod = await import("../../open-sse/executors/phind.ts");
|
||||
|
||||
describe("PhindExecutor", () => {
|
||||
it("can be instantiated", () => {
|
||||
const executor = new mod.PhindExecutor();
|
||||
assert.ok(executor);
|
||||
});
|
||||
|
||||
it("execute returns proper shape on missing cookie (fetch fails)", async () => {
|
||||
const executor = new mod.PhindExecutor();
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "phind-model",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "" },
|
||||
signal: null,
|
||||
});
|
||||
assert.ok(result.response instanceof Response);
|
||||
assert.ok(typeof result.url === "string");
|
||||
assert.ok(typeof result.headers === "object");
|
||||
} catch {
|
||||
// Network error expected in test env
|
||||
}
|
||||
});
|
||||
|
||||
it("execute builds correct URL", async () => {
|
||||
const executor = new mod.PhindExecutor();
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "test",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "fake" },
|
||||
signal: null,
|
||||
});
|
||||
assert.ok(result.url.includes("phind.com/api/agent"));
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -60,7 +60,6 @@ const FAKE_CREDS: Record<string, string> = {
|
||||
"inner-ai": "fake-audit-sweep user@example.com",
|
||||
"adapta-web": "__client=fake-audit-sweep",
|
||||
huggingchat: "hf-chat=fake-audit-sweep",
|
||||
phind: "fake-audit-sweep",
|
||||
"poe-web": "p-b=fake-audit-sweep",
|
||||
"venice-web": "fake-audit-sweep",
|
||||
"v0-vercel-web": "fake-audit-sweep",
|
||||
|
||||
@@ -18,7 +18,12 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { PROVIDER_ID_TO_ALIAS } from "../../open-sse/config/providerModels.ts";
|
||||
import { resolveProviderId, getProviderAlias } from "../../src/shared/constants/providers.ts";
|
||||
import {
|
||||
resolveProviderId,
|
||||
getProviderAlias,
|
||||
APIKEY_PROVIDERS,
|
||||
WEB_COOKIE_PROVIDERS,
|
||||
} from "../../src/shared/constants/providers.ts";
|
||||
|
||||
test("no two provider IDs share the same alias in the open-sse registry", () => {
|
||||
const aliasToIds = new Map<string, string[]>();
|
||||
@@ -62,3 +67,19 @@ test("src/shared providers map resolves the same aliases unambiguously", () => {
|
||||
assert.equal(getProviderAlias("kimi"), "kimi");
|
||||
assert.equal(getProviderAlias("hackclub"), "hc");
|
||||
});
|
||||
|
||||
test("no provider id is registered in both the API-key and web-cookie catalogs", () => {
|
||||
// A provider belongs to exactly one auth category; the same id in both catalogs
|
||||
// renders the provider twice in the dashboard (once per section). huggingchat
|
||||
// regressed this way (its API-key counterpart is the separate `huggingface`
|
||||
// Inference API id), so it must live ONLY in WEB_COOKIE_PROVIDERS.
|
||||
const apikeyIds = new Set(Object.keys(APIKEY_PROVIDERS));
|
||||
const overlap = Object.keys(WEB_COOKIE_PROVIDERS).filter((id) => apikeyIds.has(id));
|
||||
assert.deepEqual(overlap, [], `Providers duplicated across catalogs: ${overlap.join(", ")}`);
|
||||
|
||||
assert.ok("huggingchat" in WEB_COOKIE_PROVIDERS, "huggingchat must be in the web-cookie catalog");
|
||||
assert.ok(
|
||||
!("huggingchat" in APIKEY_PROVIDERS),
|
||||
"huggingchat must NOT be in the API-key catalog (use `huggingface` for the API key path)"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -31,12 +31,12 @@ test("barrel still exports every catalog + key helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("APIKEY_PROVIDERS merges the 6 family files into 160 entries (no loss / no dup)", async () => {
|
||||
test("APIKEY_PROVIDERS merges the 6 family files into 158 entries (no loss / no dup)", async () => {
|
||||
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
|
||||
assert.equal(keys.length, 160);
|
||||
assert.equal(new Set(keys).size, 160, "duplicate keys after spread-merge");
|
||||
assert.equal(keys.length, 158);
|
||||
assert.equal(new Set(keys).size, 158, "duplicate keys after spread-merge");
|
||||
// the merged object's entry-count equals the sum of the 6 semantic family files; families are a
|
||||
// strict partition (every provider in exactly one), so the sum must be exactly 160.
|
||||
// strict partition (every provider in exactly one), so the sum must be exactly 158.
|
||||
const families: [string, string][] = [
|
||||
["gateways", "APIKEY_PROVIDERS_GATEWAYS"],
|
||||
["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"],
|
||||
@@ -56,7 +56,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 160 entries (no loss / no
|
||||
seen.add(k);
|
||||
}
|
||||
}
|
||||
assert.equal(famTotal, 160, "families must partition all 160 providers");
|
||||
assert.equal(famTotal, 158, "families must partition all 158 providers");
|
||||
});
|
||||
|
||||
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { HuggingChatExecutor } = await import("../../open-sse/executors/huggingchat.ts");
|
||||
const { PhindExecutor } = await import("../../open-sse/executors/phind.ts");
|
||||
const { PoeWebExecutor } = await import("../../open-sse/executors/poe-web.ts");
|
||||
const { VeniceWebExecutor } = await import("../../open-sse/executors/venice-web.ts");
|
||||
const { V0VercelWebExecutor } = await import("../../open-sse/executors/v0-vercel-web.ts");
|
||||
@@ -97,13 +96,6 @@ test("HuggingChat executor is registered", () => {
|
||||
assert.ok(executor instanceof HuggingChatExecutor);
|
||||
});
|
||||
|
||||
test("Phind executor is registered", () => {
|
||||
assert.ok(hasSpecializedExecutor("phind"));
|
||||
assert.ok(hasSpecializedExecutor("ph"));
|
||||
const executor = getExecutor("phind");
|
||||
assert.ok(executor instanceof PhindExecutor);
|
||||
});
|
||||
|
||||
test("Poe Web executor is registered", () => {
|
||||
assert.ok(hasSpecializedExecutor("poe-web"));
|
||||
assert.ok(hasSpecializedExecutor("poe"));
|
||||
@@ -149,11 +141,6 @@ test("HuggingChat sets correct provider", () => {
|
||||
assert.equal(executor.getProvider(), "huggingchat");
|
||||
});
|
||||
|
||||
test("Phind sets correct provider", () => {
|
||||
const executor = new PhindExecutor();
|
||||
assert.equal(executor.getProvider(), "phind");
|
||||
});
|
||||
|
||||
test("Poe Web sets correct provider", () => {
|
||||
const executor = new PoeWebExecutor();
|
||||
assert.equal(executor.getProvider(), "poe-web");
|
||||
@@ -314,41 +301,6 @@ test("HuggingChat: fetch failure returns 502", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Phind Execution Tests ────────────────────────────────────────────────────
|
||||
|
||||
test("Phind: streaming returns SSE chunks", async () => {
|
||||
const sseData = [
|
||||
'data: {"choices":[{"delta":{"content":"Hello "}}]}',
|
||||
'data: {"choices":[{"delta":{"content":"world"}}]}',
|
||||
];
|
||||
const restore = mockFetchCapture(200, mockSSEStream(sseData));
|
||||
try {
|
||||
const executor = new PhindExecutor();
|
||||
const result = await executor.execute({
|
||||
...noopExecuteInput,
|
||||
model: "phind-model",
|
||||
});
|
||||
assert.ok(result.response instanceof Response);
|
||||
assert.ok(result.url.includes("phind.com"));
|
||||
const text = await result.response.text();
|
||||
assert.ok(text.includes("data:"));
|
||||
} finally {
|
||||
restore.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("Phind: error response returns error result", async () => {
|
||||
const restore = mockFetchCapture(403, "Forbidden");
|
||||
try {
|
||||
const executor = new PhindExecutor();
|
||||
const result = await executor.execute(noopExecuteInput);
|
||||
assert.ok(result.response instanceof Response);
|
||||
assert.equal(result.response.status, 403);
|
||||
} finally {
|
||||
restore.restore();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Poe Web Execution Tests ──────────────────────────────────────────────────
|
||||
|
||||
test("Poe Web: non-streaming returns JSON completion", async () => {
|
||||
@@ -515,7 +467,6 @@ test("Doubao Web: error response returns error result", async () => {
|
||||
test("All executors handle Cookie: prefix", async () => {
|
||||
const executors = [
|
||||
new HuggingChatExecutor(),
|
||||
new PhindExecutor(),
|
||||
new PoeWebExecutor(),
|
||||
new VeniceWebExecutor(),
|
||||
new V0VercelWebExecutor(),
|
||||
@@ -553,7 +504,6 @@ test("All executors handle Cookie: prefix", async () => {
|
||||
test("All executors handle bare cookie value", async () => {
|
||||
const executors = [
|
||||
new HuggingChatExecutor(),
|
||||
new PhindExecutor(),
|
||||
new PoeWebExecutor(),
|
||||
new VeniceWebExecutor(),
|
||||
new V0VercelWebExecutor(),
|
||||
|
||||
Reference in New Issue
Block a user