SPB Git

Repository Public

index.ts 3.1 KB 74 lines typescript

1/**2 * `GET /models`: live listing per provider (when its API key is configured) merged over the3 * static catalogue, cached 10 min. A provider whose listing fails, times out or has no key is4 * answered from the catalogue with `available: false` and a `stale` note — never a guess.5 */6import type { FastifyInstance } from 'fastify';7import { z } from 'zod';8import { Provider } from '@aos/shared';9import { ModelInfoEx } from '../../dto.js';10import { authOf, type Ctx } from '../../http.js';1112const PROVIDERS = Provider.options;13/** A failed listing is retried sooner than a good one expires. */14const FAILURE_CACHE_MS = 60_000;1516export class ModelCatalogue {17  private cache: { at: number; ttl: number; models: ModelInfoEx[] } | null = null;18  private inflight: Promise<ModelInfoEx[]> | null = null;19  constructor(private readonly ctx: Ctx) {}2021  /** Catalogue only (no network): used to validate a mission's model id. */22  known(): ModelInfoEx[] {23    return this.ctx.providers?.catalogue() ?? [];24  }2526  async list(): Promise<ModelInfoEx[]> {27    const now = this.ctx.now();28    if (this.cache && now - this.cache.at < this.cache.ttl) return this.cache.models;29    this.inflight ??= this.load().finally(() => (this.inflight = null));30    return this.inflight;31  }3233  private async load(): Promise<ModelInfoEx[]> {34    const { providers, config, log } = this.ctx;35    if (!providers) return [];36    const catalogue = providers.catalogue();37    let failed = false;38    const parts = await Promise.all(39      PROVIDERS.map(async (p): Promise<ModelInfoEx[]> => {40        const fallback = (note: string): ModelInfoEx[] => catalogue.filter((m) => m.provider === p).map((m) => ({ ...m, available: false, stale: true, note }));41        if (!config.providerKeys[p]) return fallback('API key not configured');42        let timer: NodeJS.Timeout | undefined;43        try {44          const live = await Promise.race([45            providers.listModels(p),46            new Promise<never>((_, reject) => {47              timer = setTimeout(() => reject(new Error('timeout')), config.modelsTimeoutMs);48            }),49          ]);50          return live.filter((m) => m.provider === p);51        } catch (err) {52          failed = true;53          // The message of a provider error may quote request details: log the class only.54          log.warn({ provider: p, error: (err as Error).name }, 'model listing failed, serving the static catalogue');55          return fallback('live model listing unavailable');56        } finally {57          clearTimeout(timer);58        }59      }),60    );61    const models = parts.flat();62    this.cache = { at: this.ctx.now(), ttl: failed ? Math.min(FAILURE_CACHE_MS, config.modelsCacheMs) : config.modelsCacheMs, models };63    return models;64  }65}6667export default async function modelsPlugin(app: FastifyInstance, ctx: Ctx): Promise<void> {68  const catalogue = new ModelCatalogue(ctx);69  app.get('/models', { config: { doc: { summary: 'Models of every provider with prices and availability (cached 10 min)', response: z.array(ModelInfoEx), responseName: 'ModelInfoList' } } }, async (req) => {70    authOf(req);71    return catalogue.list();72  });73}74
Files main
Menu

Theme