summaryrefslogtreecommitdiffhomepage
path: root/js/src/llm/llm.ts
blob: 990c30069efd6aa676e14dfb0e2155d2bc53ee99 (plain)
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import { App } from "../app/app";
import { Log } from "../util/log";
import { mergeDeep } from "remeda";
import path from "path";
import { Provider } from "../provider/provider";

import type { LanguageModel, Provider as ProviderInstance } from "ai";
import { NoSuchModelError } from "ai";
import { Config } from "../config/config";
import { BunProc } from "../bun";
import { Global } from "../global";

export namespace LLM {
  const log = Log.create({ service: "llm" });

  export class ModelNotFoundError extends Error {
    constructor(public readonly model: string) {
      super();
    }
  }

  const NATIVE_PROVIDERS: Record<string, Provider.Info> = {
    anthropic: {
      models: {
        "claude-sonnet-4-20250514": {
          name: "Claude 4 Sonnet",
          cost: {
            input: 3.0 / 1_000_000,
            output: 15.0 / 1_000_000,
            inputCached: 3.75 / 1_000_000,
            outputCached: 0.3 / 1_000_000,
          },
          contextWindow: 200000,
          maxTokens: 50000,
          attachment: true,
        },
      },
    },
    openai: {
      models: {
        "codex-mini-latest": {
          name: "Codex Mini",
          cost: {
            input: 1.5 / 1_000_000,
            inputCached: 0.375 / 1_000_000,
            output: 6.0 / 1_000_000,
            outputCached: 0.0 / 1_000_000,
          },
          contextWindow: 200000,
          maxTokens: 100000,
          attachment: true,
          reasoning: true,
        },
      },
    },
    google: {
      models: {
        "gemini-2.5-pro-preview-03-25": {
          name: "Gemini 2.5 Pro",
          cost: {
            input: 1.25 / 1_000_000,
            inputCached: 0 / 1_000_000,
            output: 10 / 1_000_000,
            outputCached: 0 / 1_000_000,
          },
          contextWindow: 1000000,
          maxTokens: 50000,
          attachment: true,
        },
      },
    },
  };

  const AUTODETECT: Record<string, string[]> = {
    anthropic: ["ANTHROPIC_API_KEY"],
    openai: ["OPENAI_API_KEY"],
    google: ["GOOGLE_GENERATIVE_AI_API_KEY"],
  };

  const state = App.state("llm", async () => {
    const config = await Config.get();
    const providers: Record<
      string,
      {
        info: Provider.Info;
        instance: ProviderInstance;
      }
    > = {};
    const models = new Map<
      string,
      { info: Provider.Model; instance: LanguageModel }
    >();

    const list = mergeDeep(NATIVE_PROVIDERS, config.providers ?? {});

    for (const [providerID, providerInfo] of Object.entries(list)) {
      if (
        !config.providers?.[providerID] &&
        !AUTODETECT[providerID]?.some((env) => process.env[env])
      )
        continue;
      const dir = path.join(
        Global.cache(),
        `node_modules`,
        `@ai-sdk`,
        providerID,
      );
      if (!(await Bun.file(path.join(dir, "package.json")).exists())) {
        BunProc.run(["add", "--exact", `@ai-sdk/${providerID}@alpha`], {
          cwd: Global.cache(),
        });
      }
      const mod = await import(
        path.join(Global.cache(), `node_modules`, `@ai-sdk`, providerID)
      );
      const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!];
      const loaded = fn(providerInfo.options);
      log.info("loaded", { provider: providerID });
      providers[providerID] = {
        info: providerInfo,
        instance: loaded,
      };
    }

    return {
      models,
      providers,
    };
  });

  export async function providers() {
    return state().then((state) => state.providers);
  }

  export async function findModel(providerID: string, modelID: string) {
    const key = `${providerID}/${modelID}`;
    const s = await state();
    if (s.models.has(key)) return s.models.get(key)!;
    const provider = s.providers[providerID];
    if (!provider) throw new ModelNotFoundError(modelID);
    log.info("loading", {
      providerID,
      modelID,
    });
    const info = provider.info.models[modelID];
    if (!info) throw new ModelNotFoundError(modelID);
    try {
      const match = provider.instance.languageModel(modelID);
      log.info("found", { providerID, modelID });
      s.models.set(key, {
        info,
        instance: match,
      });
      return {
        info,
        instance: match,
      };
    } catch (e) {
      if (e instanceof NoSuchModelError) throw new ModelNotFoundError(modelID);
      throw e;
    }
  }
}