Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- **feat(providers):** AI Horde accepts an optional registered API key and advertises only live image models that currently have workers ([#10542](https://github.com/diegosouzapw/OmniRoute/pull/10542))
- **fix(providers):** AI Horde Check validates keys via `/v2/find_user` instead of the unauthenticated OpenAI models list ([#10542](https://github.com/diegosouzapw/OmniRoute/pull/10542))
2 changes: 2 additions & 0 deletions open-sse/config/imageRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ADOBE_FIREFLY_IMAGE_ROUTING_ALIASES,
toRegistryImageModels,
} from "../services/adobeFireflyModels.ts";
import { AI_HORDE_IMAGE_PROVIDER } from "./providers/registry/aihorde/imageModels.ts";

interface ImageModelEntry {
id: string;
Expand Down Expand Up @@ -841,6 +842,7 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
// still pass supported 4K dimensions through the permissive request schema.
supportedSizes: ["1024x1024", "2048x2048"],
},
aihorde: AI_HORDE_IMAGE_PROVIDER,
};

/**
Expand Down
26 changes: 26 additions & 0 deletions open-sse/config/providers/registry/aihorde/imageModels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* AI Horde image-generation provider entry.
*
* Chat still goes through oai.aihorde.net. Image jobs use the native Horde
* async API (`/v2/generate/async`). `models` is a live getter so
* imageRegistry stays under the file-size cap and zero-worker names are
* never advertised.
*/
import { getCachedAiHordeImageCatalogEntries } from "../../../../services/aihordeImageCatalog.ts";

export const AI_HORDE_IMAGE_PROVIDER = {
id: "aihorde",
alias: "horde",
baseUrl: "https://aihorde.net/api",
authType: "apikey",
authHeader: "apikey",
format: "aihorde",
get models() {
return getCachedAiHordeImageCatalogEntries().map((entry) => ({
id: entry.id.startsWith("aihorde/") ? entry.id.slice("aihorde/".length) : entry.id,
name: entry.name,
inputModalities: entry.inputModalities,
}));
},
supportedSizes: ["512x512", "768x768", "1024x1024", "1024x768", "768x1024"],
};
11 changes: 8 additions & 3 deletions open-sse/config/providers/registry/aihorde/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
* the free catalog registers it as `recurring-uncapped` (never summed into
* the token headline) rather than inventing an RPM/RPD figure.
*
* Model list changes as workers come and go, so the live catalog is fetched via
* passthrough; the entries below are the ones that have carried steady worker
* threads and only serve as a fallback when discovery fails.
* Chat model list changes as workers come and go, so the live chat catalog is
* fetched via passthrough; the entries below are the ones that have carried
* steady worker threads and only serve as a fallback when discovery fails.
*
* Image models are a separate native Horde API (`/v2/generate/async`). They
* are discovered by polling `/v2/status/models?type=image` and only advertised
* while `count > 0`. An optional registered API key is stored as a normal
* connection and sent as the Horde `apikey` header for both chat and images.
*/
export const aihordeProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "aihorde",
Expand Down
34 changes: 14 additions & 20 deletions open-sse/handlers/imageGeneration.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
import { randomUUID } from "crypto";
/**
* Image Generation Handler
*
* Handles POST /v1/images/generations requests.
* Proxies to upstream image generation providers using OpenAI-compatible format.
*
* Request format (OpenAI-compatible):
* {
* "model": "openai/gpt-image-2",
* "prompt": "a beautiful sunset over mountains",
* "n": 1,
* "size": "1024x1024",
* "quality": "standard", // optional: "standard" | "hd"
* "response_format": "url" // optional: "url" | "b64_json"
* }
*/
/** Image generation handler for POST /v1/images/generations (OpenAI-compatible). */

import { getImageProvider, parseImageModel } from "../config/imageRegistry.ts";
import { HTTP_STATUS } from "../config/constants.ts";
Expand Down Expand Up @@ -51,10 +36,6 @@ import {
} from "@/shared/utils/fetchTimeout";
import { sanitizeErrorMessage, sanitizeUpstreamDetails } from "../utils/error.ts";

// --- Per-provider handlers (extracted to co-located files in PR-#4582-batch) ---
// Imported locally so internal callers (handleImageGeneration / handleImageEdit)
// resolve to a real binding. extractMarkdownImageUrls + CHATGPT_WEB_IMAGE_ID_RE
// are still used by handleImageEdit below, so they are imported (not re-defined).
import { handleSDWebUIImageGeneration } from "./imageGeneration/providers/sdWebUI.ts";
import { handleHyperbolicImageGeneration } from "./imageGeneration/providers/hyperbolic.ts";
import { handleHuggingFaceImageGeneration } from "./imageGeneration/providers/huggingface.ts";
Expand All @@ -76,6 +57,7 @@ import { handleDesignerWebImageGeneration } from "./imageGeneration/providers/de
import { handleMinimaxImageGeneration } from "./imageGeneration/providers/minimax.ts";
import { handleAdobeFireflyImageGeneration } from "./imageGeneration/providers/adobeFirefly.ts";
import { handleAlibabaImageGeneration } from "./imageGeneration/providers/alibabaImage.ts";
import { handleAiHordeImageGeneration } from "./imageGeneration/providers/aihorde.ts";
import {
applyPollinationsAnonymousFallback,
reportPollinationsAnonOutcome,
Expand Down Expand Up @@ -373,6 +355,18 @@ export async function handleImageGeneration({
});
}

if (providerConfig.format === "aihorde") {
return handleAiHordeImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
signal,
});
}

if (providerConfig.format === "gemini-image") {
return handleGeminiImageGeneration({ model, providerConfig, body, credentials, log });
}
Expand Down
Loading