llm.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { App } from "../app/app";
  2. import { Log } from "../util/log";
  3. import { mergeDeep } from "remeda";
  4. import path from "path";
  5. import { Provider } from "../provider/provider";
  6. import type { LanguageModel, Provider as ProviderInstance } from "ai";
  7. import { NoSuchModelError } from "ai";
  8. import { Config } from "../config/config";
  9. import { BunProc } from "../bun";
  10. import { Global } from "../global";
  11. export namespace LLM {
  12. const log = Log.create({ service: "llm" });
  13. export class ModelNotFoundError extends Error {
  14. constructor(public readonly model: string) {
  15. super();
  16. }
  17. }
  18. const NATIVE_PROVIDERS: Record<string, Provider.Info> = {
  19. anthropic: {
  20. models: {
  21. "claude-sonnet-4-20250514": {
  22. name: "Claude 4 Sonnet",
  23. cost: {
  24. input: 3.0 / 1_000_000,
  25. output: 15.0 / 1_000_000,
  26. inputCached: 3.75 / 1_000_000,
  27. outputCached: 0.3 / 1_000_000,
  28. },
  29. contextWindow: 200000,
  30. maxTokens: 50000,
  31. attachment: true,
  32. },
  33. },
  34. },
  35. openai: {
  36. models: {
  37. "codex-mini-latest": {
  38. name: "Codex Mini",
  39. cost: {
  40. input: 1.5 / 1_000_000,
  41. inputCached: 0.375 / 1_000_000,
  42. output: 6.0 / 1_000_000,
  43. outputCached: 0.0 / 1_000_000,
  44. },
  45. contextWindow: 200000,
  46. maxTokens: 100000,
  47. attachment: true,
  48. reasoning: true,
  49. },
  50. },
  51. },
  52. google: {
  53. models: {
  54. "gemini-2.5-pro-preview-03-25": {
  55. name: "Gemini 2.5 Pro",
  56. cost: {
  57. input: 1.25 / 1_000_000,
  58. inputCached: 0 / 1_000_000,
  59. output: 10 / 1_000_000,
  60. outputCached: 0 / 1_000_000,
  61. },
  62. contextWindow: 1000000,
  63. maxTokens: 50000,
  64. attachment: true,
  65. },
  66. },
  67. },
  68. };
  69. const AUTODETECT: Record<string, string[]> = {
  70. anthropic: ["ANTHROPIC_API_KEY"],
  71. openai: ["OPENAI_API_KEY"],
  72. google: ["GOOGLE_GENERATIVE_AI_API_KEY"],
  73. };
  74. const state = App.state("llm", async () => {
  75. const config = await Config.get();
  76. const providers: Record<
  77. string,
  78. {
  79. info: Provider.Info;
  80. instance: ProviderInstance;
  81. }
  82. > = {};
  83. const models = new Map<
  84. string,
  85. { info: Provider.Model; instance: LanguageModel }
  86. >();
  87. const list = mergeDeep(NATIVE_PROVIDERS, config.providers ?? {});
  88. for (const [providerID, providerInfo] of Object.entries(list)) {
  89. if (
  90. !config.providers?.[providerID] &&
  91. !AUTODETECT[providerID]?.some((env) => process.env[env])
  92. )
  93. continue;
  94. const dir = path.join(
  95. Global.cache(),
  96. `node_modules`,
  97. `@ai-sdk`,
  98. providerID,
  99. );
  100. if (!(await Bun.file(path.join(dir, "package.json")).exists())) {
  101. BunProc.run(["add", "--exact", `@ai-sdk/${providerID}@alpha`], {
  102. cwd: Global.cache(),
  103. });
  104. }
  105. const mod = await import(
  106. path.join(Global.cache(), `node_modules`, `@ai-sdk`, providerID)
  107. );
  108. const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!];
  109. const loaded = fn(providerInfo.options);
  110. log.info("loaded", { provider: providerID });
  111. providers[providerID] = {
  112. info: providerInfo,
  113. instance: loaded,
  114. };
  115. }
  116. return {
  117. models,
  118. providers,
  119. };
  120. });
  121. export async function providers() {
  122. return state().then((state) => state.providers);
  123. }
  124. export async function findModel(providerID: string, modelID: string) {
  125. const key = `${providerID}/${modelID}`;
  126. const s = await state();
  127. if (s.models.has(key)) return s.models.get(key)!;
  128. const provider = s.providers[providerID];
  129. if (!provider) throw new ModelNotFoundError(modelID);
  130. log.info("loading", {
  131. providerID,
  132. modelID,
  133. });
  134. const info = provider.info.models[modelID];
  135. if (!info) throw new ModelNotFoundError(modelID);
  136. try {
  137. const match = provider.instance.languageModel(modelID);
  138. log.info("found", { providerID, modelID });
  139. s.models.set(key, {
  140. info,
  141. instance: match,
  142. });
  143. return {
  144. info,
  145. instance: match,
  146. };
  147. } catch (e) {
  148. if (e instanceof NoSuchModelError) throw new ModelNotFoundError(modelID);
  149. throw e;
  150. }
  151. }
  152. }