mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-19 21:52:21 +03:00
fix: complete #3054 dead-provider removal (petals executor + stale tests)
#3054 ("remove 9 dead/unreachable free providers") removed the petals/nanobanana configs, registry entries and validators but left dangling references that broke the build and the unit suite on release/v3.8.8: - open-sse/executors/petals.ts imported the deleted ../config/petals.ts (webpack "Module not found" → `next build` failed). Removed the executor, its registration + re-export in executors/index.ts, and the leftover `providerId === "petals"` branch in providerAllowsOptionalApiKey. - Removed tests for the now-deleted providers: executor-petals.test.ts and poolside-provider.test.ts (REGISTRY.poolside was removed), and the petals / nanobanana validator assertions in provider-validation-specialty.test.ts, plus the stale petals catalog assertions in providers-page-utils.test.ts, proxy-connection-test.test.ts and providers-route-managed-catalog.test.ts. The image/video/embed registries for nanobanana/replicate/nomic are real and untouched — only the dead chat/api-key surfaces were removed. 146/146 affected tests pass; typecheck / build clean.
This commit is contained in:
@@ -26,7 +26,6 @@ import { AzureOpenAIExecutor } from "./azure-openai.ts";
|
||||
import { CommandCodeExecutor } from "./commandCode.ts";
|
||||
import { GitlabExecutor } from "./gitlab.ts";
|
||||
import { NlpCloudExecutor } from "./nlpcloud.ts";
|
||||
import { PetalsExecutor } from "./petals.ts";
|
||||
import { WindsurfExecutor } from "./windsurf.ts";
|
||||
import { DevinCliExecutor } from "./devin-cli.ts";
|
||||
import { DeepSeekWebExecutor } from "./deepseek-web.ts";
|
||||
@@ -70,7 +69,6 @@ const executors = {
|
||||
gitlab: new GitlabExecutor(),
|
||||
"gitlab-duo": new GitlabExecutor("gitlab-duo"),
|
||||
nlpcloud: new NlpCloudExecutor(),
|
||||
petals: new PetalsExecutor(),
|
||||
pollinations: new PollinationsExecutor(),
|
||||
pol: new PollinationsExecutor(), // Alias
|
||||
"cloudflare-ai": new CloudflareAIExecutor(),
|
||||
@@ -177,7 +175,6 @@ export { AzureOpenAIExecutor } from "./azure-openai.ts";
|
||||
export { CommandCodeExecutor } from "./commandCode.ts";
|
||||
export { GitlabExecutor } from "./gitlab.ts";
|
||||
export { NlpCloudExecutor } from "./nlpcloud.ts";
|
||||
export { PetalsExecutor } from "./petals.ts";
|
||||
export { WindsurfExecutor } from "./windsurf.ts";
|
||||
export { DevinCliExecutor } from "./devin-cli.ts";
|
||||
export { CopilotWebExecutor } from "./copilot-web.ts";
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import {
|
||||
BaseExecutor,
|
||||
mergeUpstreamExtraHeaders,
|
||||
type ExecuteInput,
|
||||
type ProviderCredentials,
|
||||
} from "./base.ts";
|
||||
import {
|
||||
PETALS_DEFAULT_BASE_URL,
|
||||
PETALS_DEFAULT_MODEL,
|
||||
normalizePetalsBaseUrl,
|
||||
} from "../config/petals.ts";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
type OpenAIMessage = {
|
||||
role?: string;
|
||||
content?: unknown;
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): JsonRecord {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
|
||||
}
|
||||
|
||||
function extractTextContent(content: unknown): string {
|
||||
if (typeof content === "string") {
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
if (!Array.isArray(content)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return content
|
||||
.map((part) => {
|
||||
if (!part || typeof part !== "object") return "";
|
||||
const item = part as Record<string, unknown>;
|
||||
if (item.type === "text" && typeof item.text === "string") {
|
||||
return item.text;
|
||||
}
|
||||
if (item.type === "input_text" && typeof item.text === "string") {
|
||||
return item.text;
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.filter((text) => text.trim().length > 0)
|
||||
.join("\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function resolvePrompt(body: unknown): string {
|
||||
const payload = asRecord(body);
|
||||
|
||||
const directPrompt = extractTextContent(payload.prompt);
|
||||
if (directPrompt) {
|
||||
return directPrompt;
|
||||
}
|
||||
|
||||
const directInput = extractTextContent(payload.input);
|
||||
if (directInput) {
|
||||
return directInput;
|
||||
}
|
||||
|
||||
const messages = Array.isArray(payload.messages) ? (payload.messages as OpenAIMessage[]) : [];
|
||||
if (messages.length === 0) return "";
|
||||
|
||||
const systemParts: string[] = [];
|
||||
const transcript: string[] = [];
|
||||
let lastRole = "";
|
||||
|
||||
for (const message of messages) {
|
||||
const role = String(message?.role || "user").toLowerCase();
|
||||
const text = extractTextContent(message?.content);
|
||||
if (!text) continue;
|
||||
|
||||
if (role === "system" || role === "developer") {
|
||||
systemParts.push(text);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (role === "assistant") {
|
||||
transcript.push(`Assistant: ${text}`);
|
||||
lastRole = "assistant";
|
||||
continue;
|
||||
}
|
||||
|
||||
transcript.push(`User: ${text}`);
|
||||
lastRole = "user";
|
||||
}
|
||||
|
||||
if (transcript.length === 0) {
|
||||
return systemParts.join("\n\n").trim();
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
if (systemParts.length > 0) {
|
||||
parts.push(`System:\n${systemParts.join("\n\n")}`);
|
||||
}
|
||||
parts.push(transcript.join("\n\n"));
|
||||
if (lastRole !== "assistant") {
|
||||
parts.push("Assistant:");
|
||||
}
|
||||
|
||||
return parts.join("\n\n").trim();
|
||||
}
|
||||
|
||||
function resolveMaxNewTokens(body: unknown): number {
|
||||
const payload = asRecord(body);
|
||||
const candidates = [
|
||||
payload.max_new_tokens,
|
||||
payload.max_completion_tokens,
|
||||
payload.max_output_tokens,
|
||||
payload.max_tokens,
|
||||
];
|
||||
|
||||
for (const value of candidates) {
|
||||
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
||||
return Math.max(1, Math.min(4096, Math.floor(value)));
|
||||
}
|
||||
}
|
||||
|
||||
return 256;
|
||||
}
|
||||
|
||||
function buildRequestPayload(model: string, body: unknown): URLSearchParams | null {
|
||||
const payload = asRecord(body);
|
||||
const prompt = resolvePrompt(payload);
|
||||
if (!prompt) return null;
|
||||
|
||||
const form = new URLSearchParams();
|
||||
form.set("model", model || PETALS_DEFAULT_MODEL);
|
||||
form.set("inputs", prompt);
|
||||
form.set("max_new_tokens", String(resolveMaxNewTokens(payload)));
|
||||
|
||||
const hasSampling =
|
||||
typeof payload.temperature === "number" ||
|
||||
typeof payload.top_k === "number" ||
|
||||
typeof payload.top_p === "number";
|
||||
|
||||
if (hasSampling) {
|
||||
form.set("do_sample", "1");
|
||||
}
|
||||
|
||||
if (typeof payload.temperature === "number") {
|
||||
form.set("temperature", String(payload.temperature));
|
||||
}
|
||||
if (typeof payload.top_k === "number") {
|
||||
form.set("top_k", String(Math.max(1, Math.floor(payload.top_k))));
|
||||
}
|
||||
if (typeof payload.top_p === "number") {
|
||||
form.set("top_p", String(payload.top_p));
|
||||
}
|
||||
if (typeof payload.repetition_penalty === "number") {
|
||||
form.set("repetition_penalty", String(payload.repetition_penalty));
|
||||
}
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
function estimateTokens(text: string): number {
|
||||
return Math.max(1, Math.ceil(text.length / 4));
|
||||
}
|
||||
|
||||
function buildSseChunk(data: unknown): string {
|
||||
return `data: ${JSON.stringify(data)}\n\n`;
|
||||
}
|
||||
|
||||
function buildOpenAiJsonCompletion(
|
||||
content: string,
|
||||
model: string,
|
||||
id: string,
|
||||
created: number
|
||||
): Response {
|
||||
const completionTokens = estimateTokens(content);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id,
|
||||
object: "chat.completion",
|
||||
created,
|
||||
model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: "assistant", content },
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: completionTokens,
|
||||
completion_tokens: completionTokens,
|
||||
total_tokens: completionTokens * 2,
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function buildSynthesizedStream(
|
||||
content: string,
|
||||
model: string,
|
||||
id: string,
|
||||
created: number
|
||||
): Response {
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
buildSseChunk({
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (content) {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
buildSseChunk({
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, delta: { content }, finish_reason: null }],
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
buildSseChunk({
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
})
|
||||
)
|
||||
);
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
|
||||
function toOpenAiError(status: number, message: string): Response {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message,
|
||||
type:
|
||||
status === 401 || status === 403
|
||||
? "authentication_error"
|
||||
: status === 429
|
||||
? "rate_limit_error"
|
||||
: "api_error",
|
||||
},
|
||||
}),
|
||||
{
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export class PetalsExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("petals", PROVIDERS.petals || { format: "openai", baseUrl: PETALS_DEFAULT_BASE_URL });
|
||||
}
|
||||
|
||||
buildUrl(
|
||||
_model: string,
|
||||
_stream: boolean,
|
||||
_urlIndex = 0,
|
||||
credentials: ProviderCredentials | null = null
|
||||
): string {
|
||||
const rawBaseUrl =
|
||||
typeof credentials?.providerSpecificData?.baseUrl === "string"
|
||||
? credentials.providerSpecificData.baseUrl
|
||||
: this.config.baseUrl;
|
||||
return normalizePetalsBaseUrl(rawBaseUrl);
|
||||
}
|
||||
|
||||
buildHeaders(credentials: ProviderCredentials | null): Record<string, string> {
|
||||
const token = credentials?.apiKey || credentials?.accessToken;
|
||||
return {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async execute({ model, body, stream, credentials, signal, upstreamExtraHeaders }: ExecuteInput) {
|
||||
const resolvedModel = model || PETALS_DEFAULT_MODEL;
|
||||
const payload = buildRequestPayload(resolvedModel, body);
|
||||
const url = this.buildUrl(resolvedModel, stream, 0, credentials);
|
||||
const headers = this.buildHeaders(credentials);
|
||||
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
|
||||
|
||||
if (!payload) {
|
||||
return {
|
||||
response: toOpenAiError(400, "Petals requests require at least one user prompt."),
|
||||
url,
|
||||
headers,
|
||||
transformedBody: body,
|
||||
};
|
||||
}
|
||||
|
||||
const transformedBody = Object.fromEntries(payload.entries());
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: payload.toString(),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return {
|
||||
response: toOpenAiError(
|
||||
response.status,
|
||||
`Petals API failed with status ${response.status}: ${errorText || "Unknown error"}`
|
||||
),
|
||||
url,
|
||||
headers,
|
||||
transformedBody,
|
||||
};
|
||||
}
|
||||
|
||||
const json = asRecord(await response.json());
|
||||
if (json.ok === false) {
|
||||
const traceback =
|
||||
typeof json.traceback === "string" && json.traceback.trim()
|
||||
? json.traceback.trim()
|
||||
: "Unknown Petals upstream error";
|
||||
return {
|
||||
response: toOpenAiError(502, `Petals API error: ${traceback}`),
|
||||
url,
|
||||
headers,
|
||||
transformedBody,
|
||||
};
|
||||
}
|
||||
|
||||
const content = typeof json.outputs === "string" ? json.outputs : "";
|
||||
const id = `chatcmpl-petals-${randomUUID()}`;
|
||||
const created = Math.floor(Date.now() / 1000);
|
||||
|
||||
return {
|
||||
response: stream
|
||||
? buildSynthesizedStream(content, resolvedModel, id, created)
|
||||
: buildOpenAiJsonCompletion(content, resolvedModel, id, created),
|
||||
url,
|
||||
headers,
|
||||
transformedBody,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error || "Unknown error");
|
||||
return {
|
||||
response: toOpenAiError(502, `Petals fetch error: ${message}`),
|
||||
url,
|
||||
headers,
|
||||
transformedBody,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PetalsExecutor;
|
||||
@@ -2766,7 +2766,6 @@ export function isSelfHostedChatProvider(providerId: unknown): boolean {
|
||||
export function providerAllowsOptionalApiKey(providerId: unknown): boolean {
|
||||
return (
|
||||
providerId === "searxng-search" ||
|
||||
providerId === "petals" ||
|
||||
providerId === "pollinations" ||
|
||||
providerId === "copilot-web" ||
|
||||
providerId === "duckduckgo-web" ||
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { getExecutor, hasSpecializedExecutor } from "../../open-sse/executors/index.ts";
|
||||
import { PetalsExecutor } from "../../open-sse/executors/petals.ts";
|
||||
|
||||
function jsonResponse(body: unknown, status = 200) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
test("PetalsExecutor is registered in the executor index", () => {
|
||||
assert.equal(hasSpecializedExecutor("petals"), true);
|
||||
assert.ok(getExecutor("petals") instanceof PetalsExecutor);
|
||||
});
|
||||
|
||||
test("PetalsExecutor converts OpenAI messages into form data and wraps JSON responses", async () => {
|
||||
const executor = new PetalsExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
const calls: Array<{
|
||||
url: string;
|
||||
body: URLSearchParams;
|
||||
headers: Record<string, string>;
|
||||
}> = [];
|
||||
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({
|
||||
url: String(url),
|
||||
body: new URLSearchParams(String(init.body || "")),
|
||||
headers: init.headers as Record<string, string>,
|
||||
});
|
||||
|
||||
return jsonResponse({
|
||||
ok: true,
|
||||
outputs: "Hi back from Petals.",
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "stabilityai/StableBeluga2",
|
||||
body: {
|
||||
messages: [
|
||||
{ role: "system", content: "You are concise." },
|
||||
{ role: "user", content: "Hello" },
|
||||
{ role: "assistant", content: "Hi there!" },
|
||||
{ role: "user", content: "How are you?" },
|
||||
],
|
||||
max_tokens: 32,
|
||||
temperature: 0.7,
|
||||
top_p: 0.9,
|
||||
},
|
||||
stream: false,
|
||||
credentials: { apiKey: "" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].url, "https://chat.petals.dev/api/v1/generate");
|
||||
assert.equal(calls[0].headers.Authorization, undefined);
|
||||
assert.equal(calls[0].headers["Content-Type"], "application/x-www-form-urlencoded");
|
||||
assert.equal(calls[0].body.get("model"), "stabilityai/StableBeluga2");
|
||||
assert.equal(calls[0].body.get("max_new_tokens"), "32");
|
||||
assert.equal(calls[0].body.get("temperature"), "0.7");
|
||||
assert.equal(calls[0].body.get("top_p"), "0.9");
|
||||
assert.match(
|
||||
calls[0].body.get("inputs") || "",
|
||||
/System:\nYou are concise\.\n\nUser: Hello\n\nAssistant: Hi there!\n\nUser: How are you\?\n\nAssistant:/
|
||||
);
|
||||
|
||||
assert.deepEqual(result.transformedBody, {
|
||||
model: "stabilityai/StableBeluga2",
|
||||
inputs:
|
||||
"System:\nYou are concise.\n\nUser: Hello\n\nAssistant: Hi there!\n\nUser: How are you?\n\nAssistant:",
|
||||
max_new_tokens: "32",
|
||||
do_sample: "1",
|
||||
temperature: "0.7",
|
||||
top_p: "0.9",
|
||||
});
|
||||
|
||||
const body = (await result.response.json()) as any;
|
||||
assert.equal(body.object, "chat.completion");
|
||||
assert.equal(body.choices[0].message.role, "assistant");
|
||||
assert.equal(body.choices[0].message.content, "Hi back from Petals.");
|
||||
assert.equal(body.model, "stabilityai/StableBeluga2");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("PetalsExecutor synthesizes OpenAI-compatible SSE responses for streaming requests", async () => {
|
||||
const executor = new PetalsExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = async () =>
|
||||
jsonResponse({
|
||||
ok: true,
|
||||
outputs: "Petals stream output",
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "stabilityai/StableBeluga2",
|
||||
body: {
|
||||
messages: [{ role: "user", content: "Say hello" }],
|
||||
},
|
||||
stream: true,
|
||||
credentials: { apiKey: "" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.response.headers.get("Content-Type"), "text/event-stream");
|
||||
const text = await result.response.text();
|
||||
assert.match(text, /data: \{\"id\":\"chatcmpl-petals-/);
|
||||
assert.match(text, /Petals stream output/);
|
||||
assert.match(text, /data: \[DONE\]/);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("PetalsExecutor maps upstream failures to OpenAI-style errors", async () => {
|
||||
const executor = new PetalsExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = async () => jsonResponse({ ok: false, traceback: "petals exploded" }, 200);
|
||||
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "stabilityai/StableBeluga2",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.response.status, 502);
|
||||
const body = (await result.response.json()) as any;
|
||||
assert.match(body.error.message, /Petals API error: petals exploded/);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { REGISTRY } from "../../open-sse/config/providerRegistry.ts";
|
||||
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
|
||||
|
||||
test("poolside registry uses /v1/chat/completions baseUrl consumed directly by default executor", () => {
|
||||
const entry = REGISTRY.poolside;
|
||||
|
||||
assert.ok(entry, "poolside should exist in registry");
|
||||
assert.equal(entry.baseUrl, "https://api.poolside.ai/v1/chat/completions");
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
});
|
||||
|
||||
test("poolside default executor returns the chat endpoint directly", () => {
|
||||
const executor = new DefaultExecutor("poolside");
|
||||
|
||||
assert.equal(
|
||||
executor.buildUrl("poolside-model", true, 0, {}),
|
||||
"https://api.poolside.ai/v1/chat/completions"
|
||||
);
|
||||
});
|
||||
|
||||
test("poolside specialty validator returns valid=true on non-auth chat probe responses", async () => {
|
||||
const calls: Array<{ url: string; status: number }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (url: any, init: any) => {
|
||||
void init;
|
||||
const u = String(url);
|
||||
calls.push({ url: u, status: 400 });
|
||||
// Poolside returns 400 for minimal probe — that means auth passed
|
||||
return new Response(JSON.stringify({ error: { message: "invalid model" } }), {
|
||||
status: 400,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "poolside",
|
||||
apiKey: "sky_validkey",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
assert.equal(result.valid, true);
|
||||
assert.equal(result.error, null);
|
||||
// Should hit /chat/completions only — no /models probe
|
||||
assert.ok(
|
||||
calls.every((c) => c.url.endsWith("/chat/completions")),
|
||||
`expected only /chat/completions probes, got ${JSON.stringify(calls.map((c) => c.url))}`
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("poolside specialty validator returns Invalid API key on 401", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () =>
|
||||
new Response(JSON.stringify({ error: "unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "content-type": "application/json" },
|
||||
})) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "poolside",
|
||||
apiKey: "sky_badkey",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
assert.equal(result.valid, false);
|
||||
assert.equal(result.error, "Invalid API key");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -48,7 +48,7 @@ data:
|
||||
`;
|
||||
}
|
||||
|
||||
test("specialty provider validators cover Deepgram, AssemblyAI, NanoBanana, ElevenLabs and Inworld branches", async () => {
|
||||
test("specialty provider validators cover Deepgram, AssemblyAI, ElevenLabs and Inworld branches", async () => {
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
const target = String(url);
|
||||
const headers = init.headers || {};
|
||||
@@ -61,9 +61,6 @@ test("specialty provider validators cover Deepgram, AssemblyAI, NanoBanana, Elev
|
||||
assert.equal(headers.Authorization, "aa-key");
|
||||
return new Response(JSON.stringify({ error: "unauthorized" }), { status: 403 });
|
||||
}
|
||||
if (target.match(/nanobanana/i)) {
|
||||
return new Response(JSON.stringify({ error: "unauthorized" }), { status: 401 });
|
||||
}
|
||||
if (target.match(/elevenlabs/i)) {
|
||||
return new Response(JSON.stringify({ voices: [] }), { status: 200 });
|
||||
}
|
||||
@@ -76,13 +73,11 @@ test("specialty provider validators cover Deepgram, AssemblyAI, NanoBanana, Elev
|
||||
|
||||
const deepgram = await validateProviderApiKey({ provider: "deepgram", apiKey: "dg-key" });
|
||||
const assembly = await validateProviderApiKey({ provider: "assemblyai", apiKey: "aa-key" });
|
||||
const banana = await validateProviderApiKey({ provider: "nanobanana", apiKey: "nb-key" });
|
||||
const eleven = await validateProviderApiKey({ provider: "elevenlabs", apiKey: "el-key" });
|
||||
const inworld = await validateProviderApiKey({ provider: "inworld", apiKey: "iw-key" });
|
||||
|
||||
assert.equal(deepgram.valid, true);
|
||||
assert.equal(assembly.error, "Invalid API key");
|
||||
assert.equal(banana.error, "Invalid API key");
|
||||
assert.equal(eleven.valid, true);
|
||||
assert.equal(inworld.valid, true);
|
||||
});
|
||||
@@ -127,9 +122,6 @@ test("specialty providers surface network failures and non-auth upstream failure
|
||||
if (target.match(/deepgram/i)) {
|
||||
throw new Error("deepgram offline");
|
||||
}
|
||||
if (target.match(/nanobanana/i)) {
|
||||
throw new Error("nanobanana offline");
|
||||
}
|
||||
if (target.match(/elevenlabs/i)) {
|
||||
return new Response(JSON.stringify({ error: "server" }), { status: 500 });
|
||||
}
|
||||
@@ -143,13 +135,11 @@ test("specialty providers surface network failures and non-auth upstream failure
|
||||
};
|
||||
|
||||
const deepgram = await validateProviderApiKey({ provider: "deepgram", apiKey: "dg-key" });
|
||||
const banana = await validateProviderApiKey({ provider: "nanobanana", apiKey: "nb-key" });
|
||||
const eleven = await validateProviderApiKey({ provider: "elevenlabs", apiKey: "el-key" });
|
||||
const inworld = await validateProviderApiKey({ provider: "inworld", apiKey: "iw-key" });
|
||||
const longcat = await validateProviderApiKey({ provider: "longcat", apiKey: "lc-key" });
|
||||
|
||||
assert.equal(deepgram.error, "deepgram offline");
|
||||
assert.equal(banana.error, "nanobanana offline");
|
||||
assert.equal(eleven.error, "Validation failed: 500");
|
||||
assert.equal(inworld.error, "Invalid API key");
|
||||
assert.equal(longcat.error, "longcat offline");
|
||||
@@ -1123,7 +1113,7 @@ test("registry providers cover remaining OpenAI-like and Claude-like validation
|
||||
assert.equal(calls[1].headers["x-api-key"], "sk-claude");
|
||||
});
|
||||
|
||||
test("specialty validators cover remaining status branches for Deepgram, AssemblyAI, NanoBanana, ElevenLabs, Inworld, Bailian and LongCat", async () => {
|
||||
test("specialty validators cover remaining status branches for Deepgram, AssemblyAI, ElevenLabs, Inworld, Bailian and LongCat", async () => {
|
||||
globalThis.fetch = async (url) => {
|
||||
const target = String(url);
|
||||
if (target.match(/deepgram/i)) {
|
||||
@@ -1132,9 +1122,6 @@ test("specialty validators cover remaining status branches for Deepgram, Assembl
|
||||
if (target.match(/assemblyai/i)) {
|
||||
return new Response(JSON.stringify({ transcripts: [] }), { status: 200 });
|
||||
}
|
||||
if (target.match(/nanobanana/i)) {
|
||||
return new Response(JSON.stringify({ error: "bad request" }), { status: 400 });
|
||||
}
|
||||
if (target.match(/elevenlabs/i)) {
|
||||
return new Response(JSON.stringify({ error: "forbidden" }), { status: 403 });
|
||||
}
|
||||
@@ -1152,7 +1139,6 @@ test("specialty validators cover remaining status branches for Deepgram, Assembl
|
||||
|
||||
const deepgram = await validateProviderApiKey({ provider: "deepgram", apiKey: "dg-key" });
|
||||
const assembly = await validateProviderApiKey({ provider: "assemblyai", apiKey: "aa-key" });
|
||||
const banana = await validateProviderApiKey({ provider: "nanobanana", apiKey: "nb-key" });
|
||||
const eleven = await validateProviderApiKey({ provider: "elevenlabs", apiKey: "el-key" });
|
||||
const inworld = await validateProviderApiKey({ provider: "inworld", apiKey: "iw-key" });
|
||||
const bailian = await validateProviderApiKey({
|
||||
@@ -1179,7 +1165,6 @@ test("specialty validators cover remaining status branches for Deepgram, Assembl
|
||||
|
||||
assert.equal(deepgram.error, "Validation failed: 500");
|
||||
assert.equal(assembly.valid, true);
|
||||
assert.equal(banana.error, "Validation failed: 400");
|
||||
assert.equal(eleven.error, "Invalid API key");
|
||||
assert.equal(inworld.error, "inworld offline");
|
||||
assert.equal(bailian.error, "Validation failed: 500");
|
||||
@@ -1722,54 +1707,6 @@ test("specialty validator rejects invalid Nous Research credentials", async () =
|
||||
assert.equal(nous.error, "Invalid API key");
|
||||
});
|
||||
|
||||
test("specialty validator accepts the public Petals generate endpoint without an API key", async () => {
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
const target = String(url);
|
||||
|
||||
if (target === "https://chat.petals.dev/api/v1/generate") {
|
||||
const headers = init.headers as Record<string, string>;
|
||||
const body = new URLSearchParams(String(init.body));
|
||||
assert.equal(headers.Authorization, undefined);
|
||||
assert.equal(headers["Content-Type"], "application/x-www-form-urlencoded");
|
||||
assert.equal(body.get("model"), "stabilityai/StableBeluga2");
|
||||
assert.equal(body.get("inputs"), "test");
|
||||
assert.equal(body.get("max_new_tokens"), "1");
|
||||
return new Response(JSON.stringify({ ok: true, outputs: "hi" }), { status: 200 });
|
||||
}
|
||||
|
||||
throw new Error(`unexpected fetch: ${target}`);
|
||||
};
|
||||
|
||||
const petals = await validateProviderApiKey({
|
||||
provider: "petals",
|
||||
apiKey: "",
|
||||
});
|
||||
|
||||
assert.equal(petals.valid, true);
|
||||
assert.equal(petals.method, "petals_generate");
|
||||
});
|
||||
|
||||
test("specialty validator surfaces Petals upstream unavailability", async () => {
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
const target = String(url);
|
||||
|
||||
if (target === "https://chat.petals.dev/api/v1/generate") {
|
||||
const headers = init.headers as Record<string, string>;
|
||||
assert.equal(headers.Authorization, undefined);
|
||||
return new Response(JSON.stringify({ error: "unavailable" }), { status: 503 });
|
||||
}
|
||||
|
||||
throw new Error(`unexpected fetch: ${target}`);
|
||||
};
|
||||
|
||||
const petals = await validateProviderApiKey({
|
||||
provider: "petals",
|
||||
apiKey: "",
|
||||
});
|
||||
|
||||
assert.equal(petals.error, "Provider unavailable (503)");
|
||||
});
|
||||
|
||||
test("specialty validator rejects invalid Poe credentials", async () => {
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
const target = String(url);
|
||||
|
||||
@@ -249,7 +249,6 @@ test("static catalog entries resolve local, search, audio, web-cookie and upstre
|
||||
const clarifaiProvider = providerPageUtils.resolveDashboardProviderInfo("clarifai");
|
||||
const empowerProvider = providerPageUtils.resolveDashboardProviderInfo("empower");
|
||||
const nousProvider = providerPageUtils.resolveDashboardProviderInfo("nous-research");
|
||||
const petalsProvider = providerPageUtils.resolveDashboardProviderInfo("petals");
|
||||
const poeProvider = providerPageUtils.resolveDashboardProviderInfo("poe");
|
||||
const azureOpenAiProvider = providerPageUtils.resolveDashboardProviderInfo("azure-openai");
|
||||
const azureAiProvider = providerPageUtils.resolveDashboardProviderInfo("azure-ai");
|
||||
@@ -303,8 +302,6 @@ test("static catalog entries resolve local, search, audio, web-cookie and upstre
|
||||
assert.equal(empowerProvider?.name, providers.APIKEY_PROVIDERS.empower.name);
|
||||
assert.equal(nousProvider?.category, "apikey");
|
||||
assert.equal(nousProvider?.name, providers.APIKEY_PROVIDERS["nous-research"].name);
|
||||
assert.equal(petalsProvider?.category, "apikey");
|
||||
assert.equal(petalsProvider?.name, providers.APIKEY_PROVIDERS.petals.name);
|
||||
assert.equal(poeProvider?.category, "apikey");
|
||||
assert.equal(poeProvider?.name, providers.APIKEY_PROVIDERS.poe.name);
|
||||
assert.equal(azureOpenAiProvider?.category, "apikey");
|
||||
@@ -363,7 +360,6 @@ test("managed provider connection ids include supported static categories and ex
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("clarifai"), true);
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("empower"), true);
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("nous-research"), true);
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("petals"), true);
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("poe"), true);
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("azure-openai"), true);
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("azure-ai"), true);
|
||||
@@ -424,7 +420,6 @@ test("grok-web taxonomy stays web-cookie only and does not leak into api-key ent
|
||||
assert.equal("clarifai" in providers.APIKEY_PROVIDERS, true);
|
||||
assert.equal("empower" in providers.APIKEY_PROVIDERS, true);
|
||||
assert.equal("nous-research" in providers.APIKEY_PROVIDERS, true);
|
||||
assert.equal("petals" in providers.APIKEY_PROVIDERS, true);
|
||||
assert.equal("poe" in providers.APIKEY_PROVIDERS, true);
|
||||
assert.equal("azure-ai" in providers.APIKEY_PROVIDERS, true);
|
||||
assert.equal("bedrock" in providers.APIKEY_PROVIDERS, true);
|
||||
@@ -512,10 +507,6 @@ test("grok-web taxonomy stays web-cookie only and does not leak into api-key ent
|
||||
apiKeyEntries.some((entry) => entry.providerId === "nous-research"),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
apiKeyEntries.some((entry) => entry.providerId === "petals"),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
apiKeyEntries.some((entry) => entry.providerId === "poe"),
|
||||
true
|
||||
|
||||
@@ -110,13 +110,6 @@ test("providers route accepts managed local, audio, web-cookie and search provid
|
||||
name: "Nous Research Primary",
|
||||
},
|
||||
},
|
||||
{
|
||||
provider: "petals",
|
||||
body: {
|
||||
provider: "petals",
|
||||
name: "Petals Public Endpoint",
|
||||
},
|
||||
},
|
||||
{
|
||||
provider: "poe",
|
||||
body: {
|
||||
|
||||
@@ -333,10 +333,6 @@ test("testApiKeyConnection: searxng-search with empty API key does NOT require A
|
||||
assert.equal(providerAllowsOptionalApiKey("searxng-search"), true);
|
||||
});
|
||||
|
||||
test("testApiKeyConnection: petals with empty API key does NOT require API key", () => {
|
||||
assert.equal(providerAllowsOptionalApiKey("petals"), true);
|
||||
});
|
||||
|
||||
test("testApiKeyConnection: self-hosted chat providers with empty API key do NOT require API key", () => {
|
||||
for (const provider of SELF_HOSTED_CHAT_PROVIDER_IDS) {
|
||||
assert.equal(
|
||||
|
||||
Reference in New Issue
Block a user