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:
backryun
2026-06-30 14:01:32 +09:00
committed by GitHub
parent b09875d3b3
commit 2ce25e7da8
39 changed files with 52 additions and 509 deletions

View File

@@ -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`.**

View File

@@ -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 |
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |

View File

@@ -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 1595% 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)

View File

@@ -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…

View File

@@ -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. |

View File

@@ -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,

View File

@@ -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)" },
],
};

View File

@@ -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(),

View File

@@ -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,
};
}
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -4804,8 +4804,6 @@
"checkWebToken": "Проверить токен",
"huggingchatLabel": "HuggingChat (Бесплатно)",
"huggingchatDesc": "Бесплатный LLM чат через huggingface.co/chat",
"phindLabel": "Phind (Бесплатно)",
"phindDesc": "Бесплатный AI-чат для разработчиков",
"poeWebLabel": "Poe Web",
"poeWebDesc": "Многофункциональный чат через poe.com",
"veniceWebLabel": "Венеция Веб",

View File

@@ -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",

View File

@@ -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",

View File

@@ -4660,8 +4660,6 @@
"checkWebToken": "检查令牌",
"huggingchatLabel": "HuggingChat免费",
"huggingchatDesc": "通过 huggingface.co/chat 免费使用 LLM 聊天",
"phindLabel": "Phind免费",
"phindDesc": "免费的开发者专用 AI 聊天",
"poeWebLabel": "Poe 网页",
"poeWebDesc": "通过 poe.com 进行多模型聊天",
"veniceWebLabel": "威尼斯网络",

View File

@@ -132,7 +132,6 @@ const VENDOR_PREFIXES = [
"alibaba/",
"nvidia/",
"01-ai/",
"phind/",
"zerox/",
"together/",
"fireworks/",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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 () => {

View File

@@ -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
}
});
});

View File

@@ -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",

View File

@@ -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)"
);
});

View File

@@ -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", () => {

View File

@@ -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(),