Skip to content
Closed
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
28 changes: 26 additions & 2 deletions open-sse/config/providers/registry/aihorde/imageModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,32 @@
* 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.
*
* NOTE: this module must NOT statically import the live catalog service
* (open-sse/services/aihordeImageCatalog.ts). That service pulls in the
* network/DB stack (safeOutboundFetch → featureFlags → db/core →
* better-sqlite3), which would drag server-only code into the client bundle
* and fail the production build. Instead, the server-side Horde poller pushes
* refreshed snapshots in through `setAiHordeImageCatalogEntries()`.
*/
import { getCachedAiHordeImageCatalogEntries } from "../../../../services/aihordeImageCatalog.ts";

export interface AiHordeRegistryModelEntry {
id: string;
name: string;
provider: string;
supportedSizes: string[];
inputModalities: string[];
description?: string;
}

// Snapshot of the live AI Horde catalog. Empty until the server-side poller
// pushes a refresh. (The client bundle never populates it — same as before,
// when the client always read an empty in-memory cache.)
let aiHordeCatalogEntries: AiHordeRegistryModelEntry[] = [];

export function setAiHordeImageCatalogEntries(entries: AiHordeRegistryModelEntry[]): void {
aiHordeCatalogEntries = entries;
}

export const AI_HORDE_IMAGE_PROVIDER = {
id: "aihorde",
Expand All @@ -16,7 +40,7 @@ export const AI_HORDE_IMAGE_PROVIDER = {
authHeader: "apikey",
format: "aihorde",
get models() {
return getCachedAiHordeImageCatalogEntries().map((entry) => ({
return aiHordeCatalogEntries.map((entry) => ({
id: entry.id.startsWith("aihorde/") ? entry.id.slice("aihorde/".length) : entry.id,
name: entry.name,
inputModalities: entry.inputModalities,
Expand Down
43 changes: 29 additions & 14 deletions open-sse/services/aihordeImageCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
*/

import { safeOutboundFetch } from "@/shared/network/safeOutboundFetch";
import {
setAiHordeImageCatalogEntries,
type AiHordeRegistryModelEntry,
} from "../config/providers/registry/aihorde/imageModels.ts";

export const AI_HORDE_API_BASE = "https://aihorde.net/api";
export const AI_HORDE_ANONYMOUS_KEY = "0000000000";
Expand Down Expand Up @@ -143,13 +147,15 @@ export class HordeImageCatalog {
} else {
this.lastError = error;
}
syncRegistrySnapshot(this);
}

/** Drop the snapshot so the next `ensureFresh` must hit Horde. */
clear(): void {
this.models = new Map();
this.updatedAt = null;
this.lastError = null;
syncRegistrySnapshot(this);
}

setFetch(fetchImpl: HordeFetch): void {
Expand All @@ -174,7 +180,9 @@ export class HordeImageCatalog {
await this.refresh(options);
}

private async refreshOnce(options: { timeoutMs?: number; signal?: AbortSignal } = {}): Promise<void> {
private async refreshOnce(
options: { timeoutMs?: number; signal?: AbortSignal } = {}
): Promise<void> {
try {
const url = `${AI_HORDE_API_BASE}/v2/status/models?type=image`;
const response = await this.fetchImpl(url, {
Expand All @@ -197,19 +205,13 @@ export class HordeImageCatalog {

export const aiHordeImageCatalog = new HordeImageCatalog();

export function resetAiHordeImageCatalog(): void {
aiHordeImageCatalog.clear();
}

export function getCachedAiHordeImageCatalogEntries(): Array<{
id: string;
name: string;
provider: string;
supportedSizes: string[];
inputModalities: string[];
description?: string;
}> {
return aiHordeImageCatalog.listModels().map((model) => ({
/**
* Map a catalog snapshot to the registry entry shape and push it into
* `AI_HORDE_IMAGE_PROVIDER` (see imageModels.ts). Only the module-level
* singleton drives the registry — injected test instances are ignored.
*/
function registryEntriesFrom(models: HordeImageCatalogModel[]): AiHordeRegistryModelEntry[] {
return models.map((model) => ({
id: `aihorde/${model.name}`,
name: `${model.name} (AI Horde)`,
provider: "aihorde",
Expand All @@ -218,3 +220,16 @@ export function getCachedAiHordeImageCatalogEntries(): Array<{
description: `${model.count} worker${model.count === 1 ? "" : "s"} online`,
}));
}

function syncRegistrySnapshot(from: HordeImageCatalog): void {
if (from !== aiHordeImageCatalog) return;
setAiHordeImageCatalogEntries(registryEntriesFrom(from.listModels()));
}

export function resetAiHordeImageCatalog(): void {
aiHordeImageCatalog.clear();
}

export function getCachedAiHordeImageCatalogEntries(): AiHordeRegistryModelEntry[] {
return registryEntriesFrom(aiHordeImageCatalog.listModels());
}
3 changes: 1 addition & 2 deletions open-sse/services/combo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ import { selectQuotaShareTarget } from "./combo/quotaShareStrategy.ts";
import { makeConnectionConcurrencyResolver, lookupPositiveCap } from "./combo/concurrencyCaps.ts";
import { acquireQuotaShareConcurrencySlot } from "./combo/quotaShareConcurrency.ts";
import { canAffordRequest } from "../../src/lib/quota/quotaScheduler.ts";
import { getCachedProviderConnectionById } from "../../src/lib/localDb.js";
import { getCachedProviderConnectionById } from "../../src/lib/localDb.ts";
import { orderTargetsByEvalScores } from "./evalRouting.ts";

/**
Expand Down Expand Up @@ -1190,7 +1190,6 @@ export async function handleComboChat({
if (i > 0) fallbackCount++;
return stopProtectedPriorityTarget(`Connection capacity reached for ${modelStr}`);
}

}

// #9654 Wave 2: per-target lane-aware admission probe. With virtual
Expand Down
Loading