mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
`normalizeExecutorResult()` has always accepted `Response | { response, url, headers,
transformedBody }` — the bare arm is what the web/scraping executors return from their
error and passthrough paths, and `chatcore-upstream-timeouts.test.ts` already covers
that both shapes are handled. But `BaseExecutor.execute` has no explicit return type,
so TypeScript inferred it from the method's single `return` — the object shape alone.
Every override returning a bare `Response` was therefore reported as incompatible:
* 14 × TS2739 in `duckduckgo-web.ts`, whose `execute()` additionally pinned its own
signature to just the object shape while returning `errorResponse()` /
`processResponse()` (both `Response`) from 14 valid paths
* TS2416 in `felo-web.ts` and `gitlab.ts`, which declare `Promise<Response>`
Fix the declaration rather than the call sites: export `ExecutorExecuteResult` from
`base.ts` — the same union `normalizeExecutorResult()` accepts — and annotate
`BaseExecutor.execute` with it. `duckduckgo-web.ts` then drops its over-narrow
annotation, matching BaseExecutor and the ~38 other executors that let the return type
be inferred.
Two subclasses read `.response` straight off `super.execute()` and now narrow first:
* `github.ts` — the existing `!result.response` guard already meant "bare Response,
nothing to materialize"; it is now expressed as `result instanceof Response`, which
is the same branch for every input (bare / object / nullish)
* `pollinations.ts` — reads the status through both arms for its pool bookkeeping
Wrapping DuckDuckGo's 14 returns would have been the wrong fix: the values are already
correct, and `normalizeExecutorResult()` produces exactly `{ response, url: "",
headers: {}, transformedBody: null }` for them.
Validation: full tsc error-set diff against the base config — 335 -> 319, **zero new
errors** (line-number-agnostic diff is empty; the two `duckduckgo-web.ts` TS2345s that
appear to move are the same two pre-existing errors renumbered by added comments, and
are left for a later slice). `typecheck:core` clean, `check:type-coverage` 92.17% ->
94.17%, and 49 of the 50 existing test files importing a touched executor pass —
`plan3-p0.test.ts` fails identically with and without this change (it reads the
developer's real ~/.omniroute DB rather than a test-scoped DATA_DIR).
The new test pins the runtime behavior of the narrowing so a later simplification
cannot quietly drop the bare-Response arm.
122 lines
4.0 KiB
TypeScript
122 lines
4.0 KiB
TypeScript
import { BaseExecutor } from "./base.ts";
|
|
import { PROVIDERS } from "../config/constants.ts";
|
|
import { DEFAULT_POOL_CONFIG } from "../services/sessionPool/types.ts";
|
|
import type { ExecuteInput } from "./base.ts";
|
|
|
|
export class PollinationsExecutor extends BaseExecutor {
|
|
constructor() {
|
|
super("pollinations", PROVIDERS["pollinations"] || { format: "openai" });
|
|
this.poolConfig = DEFAULT_POOL_CONFIG;
|
|
}
|
|
|
|
buildUrl(_model: string, _stream: boolean, urlIndex = 0, _credentials = null): string {
|
|
const baseUrls = this.getBaseUrls();
|
|
return (
|
|
baseUrls[urlIndex] || baseUrls[0] || "https://gen.pollinations.ai/v1/chat/completions"
|
|
);
|
|
}
|
|
|
|
buildHeaders(credentials: any, stream = true): Record<string, string> {
|
|
const key = credentials?.apiKey || credentials?.accessToken;
|
|
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
};
|
|
|
|
if (key) {
|
|
headers.Authorization = `Bearer ${key}`;
|
|
}
|
|
|
|
if (stream) {
|
|
headers["Accept"] = "text/event-stream";
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
transformRequest(model: string, body: any, stream: boolean, _credentials: any): any {
|
|
if (typeof body === "object" && body !== null) {
|
|
body.model = model;
|
|
body.stream = stream;
|
|
// #3981: Pollinations treats jsonMode=true as "the model MUST return JSON"
|
|
// and rejects (HTTP 400) any request whose messages don't mention "json".
|
|
// Only enable it when the caller actually asked for JSON output.
|
|
const responseFormatType = body.response_format?.type;
|
|
if (responseFormatType === "json_object" || responseFormatType === "json_schema") {
|
|
body.jsonMode = true;
|
|
}
|
|
}
|
|
return body;
|
|
}
|
|
|
|
async execute(input: ExecuteInput) {
|
|
const isAnonymous = !input.credentials?.apiKey && !input.credentials?.accessToken;
|
|
|
|
if (!isAnonymous) {
|
|
return super.execute(input);
|
|
}
|
|
|
|
const pool = this.getPool();
|
|
|
|
// Use acquireBlocking for anonymous requests to wait for available session
|
|
let session;
|
|
try {
|
|
session = pool ? await pool.acquireBlocking(10_000) : null;
|
|
} catch {
|
|
// Pool exhausted — fall through to direct request without fingerprint
|
|
session = null;
|
|
}
|
|
|
|
if (session) {
|
|
const fpHeaders = session.buildHeaders();
|
|
input.upstreamExtraHeaders = {
|
|
...fpHeaders,
|
|
...input.upstreamExtraHeaders,
|
|
};
|
|
}
|
|
|
|
try {
|
|
const result = await super.execute(input);
|
|
|
|
if (session && pool) {
|
|
// execute() contracts for `Response | { response, ... }`; both arms carry the
|
|
// status this pool bookkeeping needs.
|
|
const status = (result instanceof Response ? result : result.response).status;
|
|
if (status === 429) {
|
|
pool.reportCooldown(session);
|
|
} else if (status >= 500) {
|
|
pool.reportDead(session);
|
|
} else {
|
|
pool.reportSuccess(session);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
} catch (err: any) {
|
|
if (session && pool) {
|
|
pool.reportCooldown(session);
|
|
}
|
|
// Enhance 401 errors with actionable guidance
|
|
if (err?.status === 401 || err?.statusCode === 401) {
|
|
const premiumModels = ["claude", "claude-fast", "claude-large", "gemini", "gemini-fast", "midijourney", "midijourney-large"];
|
|
const model = input.model || "";
|
|
if (premiumModels.includes(model)) {
|
|
const enhanced = new Error(
|
|
`Pollinations model "${model}" requires an API key. ` +
|
|
`Free keyless models: openai, openai-fast, openai-large, qwen-coder, mistral, deepseek, grok, gemini-flash-lite-3.1, perplexity-fast, perplexity-reasoning. ` +
|
|
`Get a Pollinations API key at https://enter.pollinations.ai and add it in Settings → API Keys.`
|
|
);
|
|
(enhanced as any).status = 401;
|
|
(enhanced as any).type = "authentication_error";
|
|
throw enhanced;
|
|
}
|
|
}
|
|
throw err;
|
|
} finally {
|
|
session?.release();
|
|
}
|
|
}
|
|
}
|
|
|
|
export default PollinationsExecutor;
|