📦 EqualifyEverything / equalify-iris

📄 index.ts · 75 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75import type { Capability, IrisConfig, ProviderBlock } from "../config.ts";
import { BedrockProvider } from "./bedrock.ts";
import { OpenRouterProvider } from "./openrouter.ts";
import type { CompletionResult, Image, Message, ModelProvider } from "./types.ts";

export type { Image, Message, CompletionResult } from "./types.ts";

// The router maps (agent, capability) -> concrete provider + model using the
// deployment config (PRD §10.3). Providers are constructed lazily so a
// deployment only needs credentials for the providers it actually references.
export class ProviderRouter {
  private cfg: IrisConfig["providers"];
  private cache = new Map<string, ModelProvider>();

  constructor(cfg: IrisConfig) {
    this.cfg = cfg.providers;
  }

  private build(name: string): ModelProvider {
    const cached = this.cache.get(name);
    if (cached) return cached;
    const block = this.cfg[name] as ProviderBlock | undefined;
    if (!block) throw new Error(`provider "${name}" is referenced but not configured`);
    let provider: ModelProvider;
    switch (name) {
      case "openrouter":
        provider = new OpenRouterProvider(block);
        break;
      case "bedrock":
        provider = new BedrockProvider(block);
        break;
      default:
        throw new Error(`unknown provider "${name}"`);
    }
    this.cache.set(name, provider);
    return provider;
  }

  // Normalize a per_agent entry (string shorthand or object) to its parts.
  private agentOverride(agentName: string): { provider?: string; model?: string } {
    const entry = this.cfg.per_agent?.[agentName];
    if (entry == null) return {};
    return typeof entry === "string" ? { provider: entry } : entry;
  }

  // Resolve the provider name for an agent: per-agent override, else default.
  private providerNameFor(agentName: string): string {
    return this.agentOverride(agentName).provider ?? this.cfg.default;
  }

  // Resolve the concrete model with fallbacks: per-agent model override ->
  // provider's per_capability model -> provider's default_model.
  private modelFor(agentName: string, providerName: string, capability: Capability): string {
    const override = this.agentOverride(agentName).model;
    if (override) return override;
    const block = this.cfg[providerName] as ProviderBlock | undefined;
    if (!block) throw new Error(`provider "${providerName}" is not configured`);
    return block.per_capability?.[capability] ?? block.default_model;
  }

  // Run a completion for a given agent + capability. The agent declares the
  // capability; the deployment config decides the provider and concrete model.
  async complete(
    agentName: string,
    capability: Capability,
    messages: Message[],
    opts: { images?: Image[]; schema?: Record<string, unknown> } = {},
  ): Promise<CompletionResult> {
    const providerName = this.providerNameFor(agentName);
    const provider = this.build(providerName);
    const model = this.modelFor(agentName, providerName, capability);
    return provider.complete({ capability, messages, model, images: opts.images, schema: opts.schema });
  }
}