mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 14:22:09 +03:00
Pollinations image requests with no configured apiKey/accessToken (the common free case) were sent with no Authorization header AND no fingerprint headers, so Pollinations' own upstream legitimately rejected them with a real 401 even for a valid OmniRoute key. The chat path already has an anonymous fingerprint-pool fallback (PollinationsExecutor.execute()'s isAnonymous branch); the image path never reused it. Adds open-sse/handlers/imageGeneration/pollinationsAnonAuth.ts, mirroring the chat executor's anonymous session-pool fallback for handleOpenAIImageGeneration, and fixes the pre-existing bug where Authorization was set to the literal string "Bearer undefined" when no token was configured (now correctly gated by if (token)). Regression test: tests/unit/pollinations-image-anon-fallback-8085.test.ts
This commit is contained in:
committed by
GitHub
parent
c252c9d885
commit
e6087c62b2
1
changelog.d/fixes/8085-images-anon-fallback.md
Normal file
1
changelog.d/fixes/8085-images-anon-fallback.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(sse): give keyless Pollinations image requests the same anonymous fingerprint-pool fallback the chat path already uses, instead of a real upstream 401 (#8085)
|
||||
@@ -69,6 +69,10 @@ import { handleSegmindImageGeneration } from "./imageGeneration/providers/segmin
|
||||
import { handleDesignerWebImageGeneration } from "./imageGeneration/providers/designerWeb.ts";
|
||||
import { handleMinimaxImageGeneration } from "./imageGeneration/providers/minimax.ts";
|
||||
import { handleAdobeFireflyImageGeneration } from "./imageGeneration/providers/adobeFirefly.ts";
|
||||
import {
|
||||
applyPollinationsAnonymousFallback,
|
||||
reportPollinationsAnonOutcome,
|
||||
} from "./imageGeneration/pollinationsAnonAuth.ts";
|
||||
|
||||
|
||||
interface KieImageOptions {
|
||||
@@ -1031,17 +1035,30 @@ async function handleOpenAIImageGeneration({
|
||||
}
|
||||
|
||||
// Build headers
|
||||
const headers = {
|
||||
let headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
const token = credentials.apiKey || credentials.accessToken;
|
||||
if (providerConfig.authHeader === "bearer") {
|
||||
if (token && providerConfig.authHeader === "bearer") {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
} else if (providerConfig.authHeader === "x-api-key") {
|
||||
} else if (token && providerConfig.authHeader === "x-api-key") {
|
||||
headers["x-api-key"] = token;
|
||||
}
|
||||
|
||||
// #8085 — keyless Pollinations image requests (the common free case) get
|
||||
// no Authorization header above. Mirror the chat executor's anonymous
|
||||
// fingerprint-pool fallback (open-sse/executors/pollinations.ts) so the
|
||||
// outbound request isn't sent bare and rejected by Pollinations' own 401.
|
||||
let pollinationsAnonSession: Awaited<
|
||||
ReturnType<typeof applyPollinationsAnonymousFallback>
|
||||
>["session"] = null;
|
||||
if (providerConfig.id === "pollinations") {
|
||||
const anon = await applyPollinationsAnonymousFallback(providerConfig.id, token, headers);
|
||||
headers = anon.headers;
|
||||
pollinationsAnonSession = anon.session;
|
||||
}
|
||||
|
||||
if (log) {
|
||||
const promptPreview =
|
||||
typeof body.prompt === "string"
|
||||
@@ -1082,6 +1099,10 @@ async function handleOpenAIImageGeneration({
|
||||
);
|
||||
}
|
||||
|
||||
if (pollinationsAnonSession) {
|
||||
reportPollinationsAnonOutcome(pollinationsAnonSession, result.status);
|
||||
}
|
||||
|
||||
// Save call log after result is determined
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
|
||||
75
open-sse/handlers/imageGeneration/pollinationsAnonAuth.ts
Normal file
75
open-sse/handlers/imageGeneration/pollinationsAnonAuth.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
// #8085 — anonymous fingerprint fallback for keyless Pollinations image requests.
|
||||
//
|
||||
// Chat requests to Pollinations already fall back to a fingerprint-pool
|
||||
// session when no apiKey/accessToken is configured (see
|
||||
// PollinationsExecutor.execute()'s `isAnonymous` branch in
|
||||
// open-sse/executors/pollinations.ts). The image path
|
||||
// (open-sse/handlers/imageGeneration.ts::handleOpenAIImageGeneration) had no
|
||||
// equivalent: a keyless Pollinations image request went out with no
|
||||
// Authorization header AND no fingerprint headers, so Pollinations' own
|
||||
// upstream legitimately rejected it with a 401 — even with a perfectly
|
||||
// valid OmniRoute API key. This module mirrors that same anonymous
|
||||
// session-pool fallback for the image path.
|
||||
|
||||
import { SessionPool } from "../../services/sessionPool/sessionPool.ts";
|
||||
import { DEFAULT_POOL_CONFIG } from "../../services/sessionPool/types.ts";
|
||||
import type { Session } from "../../services/sessionPool/session.ts";
|
||||
|
||||
let pollinationsImagePool: SessionPool | null = null;
|
||||
|
||||
function getPollinationsImagePool(): SessionPool {
|
||||
if (!pollinationsImagePool) {
|
||||
pollinationsImagePool = new SessionPool("pollinations", DEFAULT_POOL_CONFIG);
|
||||
pollinationsImagePool.warmUp(DEFAULT_POOL_CONFIG.minSessions).catch(() => {});
|
||||
}
|
||||
return pollinationsImagePool;
|
||||
}
|
||||
|
||||
/**
|
||||
* When `providerId` is Pollinations and no real key/token is present, acquire
|
||||
* a fingerprint-pool session and return its headers merged over `headers`,
|
||||
* plus the session so the caller can release it once the upstream call is
|
||||
* done. No-op (returns `{ headers, session: null }`) for every other
|
||||
* provider or when a real key is configured.
|
||||
*/
|
||||
export async function applyPollinationsAnonymousFallback(
|
||||
providerId: string,
|
||||
token: string | undefined,
|
||||
headers: Record<string, string>
|
||||
): Promise<{ headers: Record<string, string>; session: Session | null }> {
|
||||
if (providerId !== "pollinations" || token) {
|
||||
return { headers, session: null };
|
||||
}
|
||||
|
||||
const pool = getPollinationsImagePool();
|
||||
let session: Session | null = null;
|
||||
try {
|
||||
session = await pool.acquireBlocking(10_000);
|
||||
} catch {
|
||||
// Pool exhausted — fall through without fingerprint headers rather than
|
||||
// block the request indefinitely.
|
||||
session = null;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return { headers, session: null };
|
||||
}
|
||||
|
||||
return {
|
||||
headers: { ...headers, ...session.buildHeaders() },
|
||||
session,
|
||||
};
|
||||
}
|
||||
|
||||
/** Report the outcome of an anonymous Pollinations image request back to the pool. */
|
||||
export function reportPollinationsAnonOutcome(session: Session | null, status: number | undefined): void {
|
||||
if (!session || !pollinationsImagePool) return;
|
||||
if (status === 429) {
|
||||
pollinationsImagePool.reportCooldown(session);
|
||||
} else if (typeof status === "number" && status >= 500) {
|
||||
pollinationsImagePool.reportDead(session);
|
||||
} else {
|
||||
pollinationsImagePool.reportSuccess(session);
|
||||
}
|
||||
session.release();
|
||||
}
|
||||
64
tests/unit/pollinations-image-anon-fallback-8085.test.ts
Normal file
64
tests/unit/pollinations-image-anon-fallback-8085.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
// Isolated DATA_DIR so this test never touches the real ~/.omniroute DB
|
||||
// (handleImageGeneration's call-log path opens the shared DB singleton).
|
||||
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-images-anon-8085-"));
|
||||
|
||||
// #8085 — Pollinations image-generation requests with NO configured API key
|
||||
// (the common free/keyless case) must reuse the same anonymous fingerprint
|
||||
// session-pool fallback that PollinationsExecutor.execute() already applies
|
||||
// to the chat path (open-sse/executors/pollinations.ts:52-71). Without it,
|
||||
// the outbound request to gen.pollinations.ai carries no Authorization AND
|
||||
// no browser fingerprint, so Pollinations legitimately rejects it with a
|
||||
// real upstream 401 — even though the caller supplied a perfectly valid
|
||||
// OmniRoute API key.
|
||||
const { handleImageGeneration } = await import("../../open-sse/handlers/imageGeneration.ts");
|
||||
|
||||
test("#8085 keyless Pollinations image request includes anonymous fingerprint headers (User-Agent) instead of going out bare", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let captured;
|
||||
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
captured = {
|
||||
url: String(url),
|
||||
headers: options.headers || {},
|
||||
};
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ created: 123, data: [{ url: "https://cdn.example.com/image.png" }] }),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await handleImageGeneration({
|
||||
body: {
|
||||
model: "pollinations/flux",
|
||||
prompt: "a cat",
|
||||
},
|
||||
// No apiKey/accessToken — the common keyless/free Pollinations case.
|
||||
credentials: {},
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.ok(captured, "fetch should have been called");
|
||||
assert.equal(captured.url, "https://gen.pollinations.ai/v1/images/generations");
|
||||
|
||||
// No key was supplied, so no Authorization header is expected — but the
|
||||
// anonymous fallback must inject fingerprint headers (mirroring the chat
|
||||
// executor's isAnonymous branch) so the upstream doesn't see a bare,
|
||||
// headerless request and reject it with a real 401.
|
||||
assert.equal(captured.headers.Authorization, undefined);
|
||||
assert.ok(
|
||||
captured.headers["User-Agent"],
|
||||
"expected anonymous session-pool fingerprint headers (User-Agent) on the outbound Pollinations image request"
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user