AkurAI Build
Menu

popagent

public

Latest change dc4b371554df3a78c40c29085ead1db3de7e24f8 - Add model routing policy, companyStatus tool, Markdown channel output, compact Runtime settings by AkurAI Build

import {
  DEFAULT_MODEL_ROUTING,
  modelSpec,
  parseModelSpec,
  ROUTING_MODEL_ID,
  type AgentRuntimeSettings,
  type ModelRouting,
} from "./api-types";
import { agentRuntimeSettings } from "./agent-runtime-settings";
import { appLogger } from "./observability";

type RoutingSource = { get(): Promise<Pick<AgentRuntimeSettings, "defaultModel" | "modelRouting">> };

export type ModelRouteHealth = {
  spec: string;
  model: string;
  reasoningEffort: string;
  cooldownUntil: string | null;
  lastError: string | null;
  picks: number;
};

/**
 * Resolves the `policy/routing` pseudo model to one concrete `model@effort`
 * spec. `fallback` always prefers the first healthy route in order;
 * `round-robin` rotates through healthy routes. A route that fails with a
 * rate limit or an unknown-model error leaves rotation for `cooldownMs`.
 * State is per process: a restart clears cooldowns, which is the desired
 * behaviour for a bounded quota window.
 */
export class ModelRouter {
  private cooldowns = new Map<string, { until: number; error: string }>();
  private picks = new Map<string, number>();
  private cursor = 0;

  constructor(private readonly settings: RoutingSource = agentRuntimeSettings, private readonly now: () => number = Date.now) {}

  /** True when the requested model must be resolved through the policy. */
  static isRouted(requested: string | null | undefined): boolean {
    return requested === ROUTING_MODEL_ID;
  }

  /** Effective `model@effort` spec for one attempt. Non-routed ids pass through untouched. */
  async pick(requested: string): Promise<string> {
    if (!ModelRouter.isRouted(requested)) return requested;
    const settings = await this.settings.get();
    const routing = settings.modelRouting ?? DEFAULT_MODEL_ROUTING;
    const routes = routing.enabled ? routing.routes : [];
    if (routes.length === 0) {
      appLogger().warn("model.routing.unconfigured", { fallback: settings.defaultModel });
      return settings.defaultModel;
    }
    const specs = routes.map(modelSpec);
    const healthy = specs.filter((spec) => !this.cooling(spec));
    // Every route is cooling down: take the one whose cooldown ends first
    // rather than refusing to work at all.
    const candidates = healthy.length ? healthy : [...specs].sort((a, b) => (this.cooldowns.get(a)?.until ?? 0) - (this.cooldowns.get(b)?.until ?? 0)).slice(0, 1);
    const chosen = routing.strategy === "round-robin"
      ? candidates[this.cursor++ % candidates.length]!
      : candidates[0]!;
    this.picks.set(chosen, (this.picks.get(chosen) ?? 0) + 1);
    return chosen;
  }

  /**
   * Record a failed attempt for one effective spec. Only quota and
   * unknown-model failures take a route out of rotation; other errors are
   * the task's problem, not the route's.
   */
  async reportFailure(spec: string, error: unknown): Promise<boolean> {
    const message = error instanceof Error ? error.message : String(error);
    const status = Number((error as { statusCode?: unknown; status?: unknown } | null)?.statusCode ?? (error as { status?: unknown } | null)?.status);
    const rateLimited = status === 429 || /\brate[- ]?limit|\btoo many requests|\busage limit|\bquota\b/i.test(message);
    const unknownModel = status === 404 || /\bunknown model|\bmodel .* not found|\bno such model|\bunsupported model/i.test(message);
    if (!rateLimited && !unknownModel) return false;
    const routing = (await this.settings.get()).modelRouting ?? DEFAULT_MODEL_ROUTING;
    const specs = new Set(routing.routes.map(modelSpec));
    if (!specs.has(spec)) return false;
    const resetMinutes = /reset after (\d+)\s*m/i.exec(message);
    const cooldownMs = resetMinutes ? Math.max(routing.cooldownMs, Number(resetMinutes[1]) * 60_000) : routing.cooldownMs;
    this.cooldowns.set(spec, { until: this.now() + cooldownMs, error: message.slice(0, 300) });
    appLogger().warn("model.routing.cooldown", { spec, cooldownMs, reason: rateLimited ? "rate-limit" : "unknown-model" });
    return true;
  }

  /** Per-route state for the settings page. */
  async health(): Promise<{ strategy: ModelRouting["strategy"]; enabled: boolean; routes: ModelRouteHealth[] }> {
    const routing = (await this.settings.get()).modelRouting ?? DEFAULT_MODEL_ROUTING;
    return {
      strategy: routing.strategy,
      enabled: routing.enabled,
      routes: routing.routes.map((route) => {
        const spec = modelSpec(route);
        const cooling = this.cooling(spec) ? this.cooldowns.get(spec)! : undefined;
        const parsed = parseModelSpec(spec);
        return {
          spec,
          model: parsed.model,
          reasoningEffort: parsed.reasoningEffort,
          cooldownUntil: cooling ? new Date(cooling.until).toISOString() : null,
          lastError: this.cooldowns.get(spec)?.error ?? null,
          picks: this.picks.get(spec) ?? 0,
        };
      }),
    };
  }

  /** Test seam: forget every cooldown and counter. */
  reset() {
    this.cooldowns.clear();
    this.picks.clear();
    this.cursor = 0;
  }

  private cooling(spec: string): boolean {
    const entry = this.cooldowns.get(spec);
    return entry !== undefined && entry.until > this.now();
  }
}

export const modelRouter = new ModelRouter();