@luanpoppe/ai 1.1.5 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.env.example +13 -1
  2. package/README.md +795 -0
  3. package/dist/@types/embedding-model-names.d.ts +11 -0
  4. package/dist/@types/embedding-model-names.d.ts.map +1 -0
  5. package/dist/@types/embedding-model-names.js +3 -0
  6. package/dist/@types/embedding-model-names.js.map +1 -0
  7. package/dist/@types/model-names.d.ts +5 -1
  8. package/dist/@types/model-names.d.ts.map +1 -1
  9. package/dist/@types/openrouter-provider.d.ts +25 -0
  10. package/dist/@types/openrouter-provider.d.ts.map +1 -0
  11. package/dist/@types/openrouter-provider.js +3 -0
  12. package/dist/@types/openrouter-provider.js.map +1 -0
  13. package/dist/ai.d.ts +7 -0
  14. package/dist/ai.d.ts.map +1 -1
  15. package/dist/ai.js +56 -5
  16. package/dist/ai.js.map +1 -1
  17. package/dist/audio/gemini-transcription.d.ts +4 -0
  18. package/dist/audio/gemini-transcription.d.ts.map +1 -0
  19. package/dist/audio/gemini-transcription.js +37 -0
  20. package/dist/audio/gemini-transcription.js.map +1 -0
  21. package/dist/audio/index.d.ts +33 -0
  22. package/dist/audio/index.d.ts.map +1 -0
  23. package/dist/audio/index.js +43 -0
  24. package/dist/audio/index.js.map +1 -0
  25. package/dist/audio/openrouter-client.d.ts +2 -0
  26. package/dist/audio/openrouter-client.d.ts.map +1 -0
  27. package/dist/audio/openrouter-client.js +7 -0
  28. package/dist/audio/openrouter-client.js.map +1 -0
  29. package/dist/audio/speech-openai.d.ts +4 -0
  30. package/dist/audio/speech-openai.d.ts.map +1 -0
  31. package/dist/audio/speech-openai.js +65 -0
  32. package/dist/audio/speech-openai.js.map +1 -0
  33. package/dist/audio/speech-openrouter.d.ts +4 -0
  34. package/dist/audio/speech-openrouter.d.ts.map +1 -0
  35. package/dist/audio/speech-openrouter.js +89 -0
  36. package/dist/audio/speech-openrouter.js.map +1 -0
  37. package/dist/audio/transcription-openai.d.ts +11 -0
  38. package/dist/audio/transcription-openai.d.ts.map +1 -0
  39. package/dist/audio/transcription-openai.js +204 -0
  40. package/dist/audio/transcription-openai.js.map +1 -0
  41. package/dist/audio/transcription-openrouter.d.ts +4 -0
  42. package/dist/audio/transcription-openrouter.d.ts.map +1 -0
  43. package/dist/audio/transcription-openrouter.js +44 -0
  44. package/dist/audio/transcription-openrouter.js.map +1 -0
  45. package/dist/audio/types.d.ts +92 -0
  46. package/dist/audio/types.d.ts.map +1 -0
  47. package/dist/audio/types.js +6 -0
  48. package/dist/audio/types.js.map +1 -0
  49. package/dist/audio/utils.d.ts +5 -0
  50. package/dist/audio/utils.d.ts.map +1 -0
  51. package/dist/audio/utils.js +25 -0
  52. package/dist/audio/utils.js.map +1 -0
  53. package/dist/embeddings/embed-gemini.d.ts +3 -0
  54. package/dist/embeddings/embed-gemini.d.ts.map +1 -0
  55. package/dist/embeddings/embed-gemini.js +38 -0
  56. package/dist/embeddings/embed-gemini.js.map +1 -0
  57. package/dist/embeddings/embed-openai.d.ts +3 -0
  58. package/dist/embeddings/embed-openai.d.ts.map +1 -0
  59. package/dist/embeddings/embed-openai.js +44 -0
  60. package/dist/embeddings/embed-openai.js.map +1 -0
  61. package/dist/embeddings/embed-openrouter.d.ts +3 -0
  62. package/dist/embeddings/embed-openrouter.d.ts.map +1 -0
  63. package/dist/embeddings/embed-openrouter.js +47 -0
  64. package/dist/embeddings/embed-openrouter.js.map +1 -0
  65. package/dist/embeddings/index.d.ts +19 -0
  66. package/dist/embeddings/index.d.ts.map +1 -0
  67. package/dist/embeddings/index.js +36 -0
  68. package/dist/embeddings/index.js.map +1 -0
  69. package/dist/embeddings/route.d.ts +12 -0
  70. package/dist/embeddings/route.d.ts.map +1 -0
  71. package/dist/embeddings/route.js +42 -0
  72. package/dist/embeddings/route.js.map +1 -0
  73. package/dist/embeddings/types.d.ts +37 -0
  74. package/dist/embeddings/types.d.ts.map +1 -0
  75. package/dist/embeddings/types.js +6 -0
  76. package/dist/embeddings/types.js.map +1 -0
  77. package/dist/embeddings/utils.d.ts +6 -0
  78. package/dist/embeddings/utils.d.ts.map +1 -0
  79. package/dist/embeddings/utils.js +25 -0
  80. package/dist/embeddings/utils.js.map +1 -0
  81. package/dist/index.d.ts +11 -2
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +16 -3
  84. package/dist/index.js.map +1 -1
  85. package/dist/langchain/audio-transcription.d.ts +8 -13
  86. package/dist/langchain/audio-transcription.d.ts.map +1 -1
  87. package/dist/langchain/audio-transcription.js +8 -92
  88. package/dist/langchain/audio-transcription.js.map +1 -1
  89. package/dist/langchain/messages.d.ts +6 -1
  90. package/dist/langchain/messages.d.ts.map +1 -1
  91. package/dist/langchain/messages.js +35 -7
  92. package/dist/langchain/messages.js.map +1 -1
  93. package/dist/langchain/models.d.ts +38 -0
  94. package/dist/langchain/models.d.ts.map +1 -1
  95. package/dist/langchain/models.js +63 -4
  96. package/dist/langchain/models.js.map +1 -1
  97. package/dist/langchain/structured-output.d.ts +9 -0
  98. package/dist/langchain/structured-output.d.ts.map +1 -0
  99. package/dist/langchain/structured-output.js +70 -0
  100. package/dist/langchain/structured-output.js.map +1 -0
  101. package/dist/langchain/tools.d.ts +1 -1
  102. package/dist/utils/openrouter-client.d.ts +4 -0
  103. package/dist/utils/openrouter-client.d.ts.map +1 -0
  104. package/dist/utils/openrouter-client.js +16 -0
  105. package/dist/utils/openrouter-client.js.map +1 -0
  106. package/docs/LANGCHAIN_WHISPER_WINDOWS_BUG.md +2 -0
  107. package/package.json +4 -4
  108. package/src/@types/embedding-model-names.ts +36 -0
  109. package/src/@types/model-names.ts +12 -1
  110. package/src/@types/openrouter-provider.ts +27 -0
  111. package/src/ai.ts +89 -10
  112. package/src/audio/gemini-transcription.ts +47 -0
  113. package/src/audio/index.ts +101 -0
  114. package/src/audio/openrouter-client.ts +4 -0
  115. package/src/audio/speech-openai.ts +79 -0
  116. package/src/audio/speech-openrouter.ts +121 -0
  117. package/src/audio/transcription-openai.ts +259 -0
  118. package/src/audio/transcription-openrouter.ts +59 -0
  119. package/src/audio/types.ts +127 -0
  120. package/src/audio/utils.ts +20 -0
  121. package/src/embeddings/embed-gemini.ts +54 -0
  122. package/src/embeddings/embed-openai.ts +53 -0
  123. package/src/embeddings/embed-openrouter.ts +64 -0
  124. package/src/embeddings/index.ts +83 -0
  125. package/src/embeddings/route.ts +62 -0
  126. package/src/embeddings/types.ts +53 -0
  127. package/src/embeddings/utils.ts +21 -0
  128. package/src/index.ts +51 -5
  129. package/src/langchain/audio-transcription.ts +19 -90
  130. package/src/langchain/messages.ts +84 -12
  131. package/src/langchain/models.ts +130 -3
  132. package/src/langchain/structured-output.ts +77 -0
  133. package/src/utils/openrouter-client.ts +10 -0
  134. package/tests/e2e/README.md +53 -0
  135. package/tests/e2e/ai-audio-multiprovider.test.ts +118 -0
  136. package/tests/e2e/ai-deepseek-structured-output.test.ts +133 -0
  137. package/tests/e2e/ai-embeddings.test.ts +87 -0
  138. package/tests/e2e/ai-local-models.test.ts +65 -0
  139. package/tests/unit/audio/index.test.ts +17 -0
  140. package/tests/unit/audio/speech-openai.test.ts +74 -0
  141. package/tests/unit/audio/speech-openrouter.test.ts +88 -0
  142. package/tests/unit/audio/transcription-openai.test.ts +130 -0
  143. package/tests/unit/audio/transcription-openrouter.test.ts +82 -0
  144. package/tests/unit/embeddings/embed-gemini.test.ts +52 -0
  145. package/tests/unit/embeddings/embed-openai.test.ts +68 -0
  146. package/tests/unit/embeddings/embed-openrouter.test.ts +62 -0
  147. package/tests/unit/embeddings/index.test.ts +55 -0
  148. package/tests/unit/index.test.ts +209 -0
  149. package/tests/unit/langchain/messages.test.ts +37 -0
  150. package/tests/unit/langchain/models.test.ts +223 -1
  151. package/tests/unit/langchain/structured-output.test.ts +49 -0
@@ -1,6 +1,15 @@
1
1
  import { AIMessages } from "../../../src/langchain/messages";
2
2
  import { SystemMessage, HumanMessage, AIMessage } from "langchain";
3
3
 
4
+ vi.mock("../../../src/audio", () => ({
5
+ AIAudio: {
6
+ transcribeWithWhisper: vi.fn().mockResolvedValue("texto whisper"),
7
+ transcribeOpenRouter: vi
8
+ .fn()
9
+ .mockResolvedValue({ text: "texto openrouter" }),
10
+ },
11
+ }));
12
+
4
13
  // Mock do langchain
5
14
  vi.mock("langchain", () => {
6
15
  class MockSystemMessage {
@@ -174,6 +183,34 @@ describe("AIMessages", () => {
174
183
  const callArgs = (HumanMessage as any).mock.calls[0][0];
175
184
  expect(callArgs.content[0].mime_type).toBe("audio/mp4");
176
185
  });
186
+
187
+ it("provider openai usa STT prévio via AIAudio", async () => {
188
+ const { AIAudio } = await import("../../../src/audio");
189
+ const audioBuffer = Buffer.from("fake audio data");
190
+
191
+ const result = await AIMessages.humanAudio({
192
+ audio: { buffer: audioBuffer, mimeType: "audio/mp3" },
193
+ provider: "openai",
194
+ openAIApiKey: "sk-test",
195
+ });
196
+
197
+ expect(AIAudio.transcribeWithWhisper).toHaveBeenCalled();
198
+ expect(result.content).toContain("texto whisper");
199
+ });
200
+
201
+ it("provider openrouter usa transcribeOpenRouter", async () => {
202
+ const { AIAudio } = await import("../../../src/audio");
203
+ const audioBuffer = Buffer.from("fake audio data");
204
+
205
+ const result = await AIMessages.humanAudio({
206
+ audio: { buffer: audioBuffer, mimeType: "audio/mp3" },
207
+ provider: "openrouter",
208
+ openRouterApiKey: "or-test",
209
+ });
210
+
211
+ expect(AIAudio.transcribeOpenRouter).toHaveBeenCalled();
212
+ expect(result.content).toContain("texto openrouter");
213
+ });
177
214
  });
178
215
 
179
216
  describe("humanImage", () => {
@@ -1,6 +1,12 @@
1
- import { AIModels, LLMModelConfig } from "../../../src/langchain/models";
1
+ import {
2
+ AIModels,
3
+ DEFAULT_OLLAMA_BASE_URL,
4
+ LLMModelConfig,
5
+ resolveOpenRouterProvider,
6
+ } from "../../../src/langchain/models";
2
7
  import { ChatOpenAI } from "@langchain/openai";
3
8
  import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
9
+ import { ChatOllama } from "@langchain/ollama";
4
10
 
5
11
  // Mock das dependências
6
12
  vi.mock("@langchain/openai", () => ({
@@ -11,6 +17,10 @@ vi.mock("@langchain/google-genai", () => ({
11
17
  ChatGoogleGenerativeAI: vi.fn(),
12
18
  }));
13
19
 
20
+ vi.mock("@langchain/ollama", () => ({
21
+ ChatOllama: vi.fn(),
22
+ }));
23
+
14
24
  describe("AIModels", () => {
15
25
  beforeEach(() => {
16
26
  vi.clearAllMocks();
@@ -205,4 +215,216 @@ describe("AIModels", () => {
205
215
  );
206
216
  });
207
217
  });
218
+
219
+ describe("resolveOpenRouterProvider", () => {
220
+ it("deve retornar only deepseek para modelos deepseek/ sem config", () => {
221
+ expect(
222
+ resolveOpenRouterProvider("deepseek/deepseek-v3.2", {}),
223
+ ).toEqual({ only: ["deepseek"] });
224
+ });
225
+
226
+ it("não deve aplicar default para modelos que não são deepseek/", () => {
227
+ expect(
228
+ resolveOpenRouterProvider("openai/gpt-5-nano", {}),
229
+ ).toBeUndefined();
230
+ });
231
+
232
+ it("deve retornar undefined quando openRouterAllowAllProviders é true", () => {
233
+ expect(
234
+ resolveOpenRouterProvider("deepseek/deepseek-v3.2", {
235
+ openRouterAllowAllProviders: true,
236
+ }),
237
+ ).toBeUndefined();
238
+ });
239
+
240
+ it("deve usar openRouterProvider explícito em vez do default", () => {
241
+ const custom = { only: ["deepinfra"] };
242
+ expect(
243
+ resolveOpenRouterProvider("deepseek/deepseek-r1", {
244
+ openRouterProvider: custom,
245
+ }),
246
+ ).toEqual(custom);
247
+ });
248
+ });
249
+
250
+ describe("openrouter", () => {
251
+ it("deve criar ChatOpenAI com configurações básicas", () => {
252
+ const config: LLMModelConfig = {
253
+ model: "openai/gpt-5-nano",
254
+ apiKey: "test-openrouter-key",
255
+ };
256
+
257
+ AIModels.openrouter(config);
258
+
259
+ expect(ChatOpenAI).toHaveBeenCalledWith({
260
+ model: "openai/gpt-5-nano",
261
+ apiKey: "test-openrouter-key",
262
+ configuration: {
263
+ baseURL: "https://openrouter.ai/api/v1",
264
+ },
265
+ });
266
+ });
267
+
268
+ it("deve aplicar provider only deepseek por padrão em modelos deepseek/", () => {
269
+ const config: LLMModelConfig = {
270
+ model: "deepseek/deepseek-v3.2",
271
+ apiKey: "test-openrouter-key",
272
+ };
273
+
274
+ AIModels.openrouter(config);
275
+
276
+ expect(ChatOpenAI).toHaveBeenCalledWith(
277
+ expect.objectContaining({
278
+ modelKwargs: { provider: { only: ["deepseek"] } },
279
+ }),
280
+ );
281
+ });
282
+
283
+ it("não deve enviar provider em modelos não-deepseek", () => {
284
+ const config: LLMModelConfig = {
285
+ model: "openai/gpt-5-nano",
286
+ apiKey: "test-openrouter-key",
287
+ };
288
+
289
+ AIModels.openrouter(config);
290
+
291
+ expect(ChatOpenAI).toHaveBeenCalledWith(
292
+ expect.not.objectContaining({
293
+ modelKwargs: expect.anything(),
294
+ }),
295
+ );
296
+ });
297
+
298
+ it("não deve aplicar default deepseek quando openRouterAllowAllProviders é true", () => {
299
+ const config: LLMModelConfig = {
300
+ model: "deepseek/deepseek-v3.2",
301
+ apiKey: "test-openrouter-key",
302
+ openRouterAllowAllProviders: true,
303
+ };
304
+
305
+ AIModels.openrouter(config);
306
+
307
+ expect(ChatOpenAI).toHaveBeenCalledWith(
308
+ expect.not.objectContaining({
309
+ modelKwargs: expect.anything(),
310
+ }),
311
+ );
312
+ });
313
+
314
+ it("deve enviar openRouterProvider customizado", () => {
315
+ const provider = {
316
+ only: ["deepinfra", "fireworks"],
317
+ max_price: { prompt: 0.5, completion: 1 },
318
+ sort: "price" as const,
319
+ };
320
+
321
+ AIModels.openrouter({
322
+ model: "deepseek/deepseek-r1",
323
+ apiKey: "test-openrouter-key",
324
+ openRouterProvider: provider,
325
+ });
326
+
327
+ expect(ChatOpenAI).toHaveBeenCalledWith(
328
+ expect.objectContaining({
329
+ modelKwargs: { provider },
330
+ }),
331
+ );
332
+ });
333
+
334
+ it("deve mesclar provider com reasoningEffort", () => {
335
+ AIModels.openrouter({
336
+ model: "deepseek/deepseek-r1",
337
+ apiKey: "test-openrouter-key",
338
+ reasoningEffort: "high",
339
+ openRouterProvider: { only: ["deepseek"] },
340
+ });
341
+
342
+ expect(ChatOpenAI).toHaveBeenCalledWith(
343
+ expect.objectContaining({
344
+ modelKwargs: {
345
+ reasoning_effort: "high",
346
+ provider: { only: ["deepseek"] },
347
+ },
348
+ }),
349
+ );
350
+ });
351
+
352
+ it("deve lançar erro quando apiKey não é fornecida", () => {
353
+ expect(() =>
354
+ AIModels.openrouter({ model: "openai/gpt-5-nano" }),
355
+ ).toThrow("OpenRouter API key is not passed in the model parameters");
356
+ });
357
+ });
358
+
359
+ describe("ollama", () => {
360
+ it("deve criar ChatOllama com configurações básicas e baseUrl padrão", () => {
361
+ AIModels.ollama({ model: "llama3.2" });
362
+
363
+ expect(ChatOllama).toHaveBeenCalledWith({
364
+ model: "llama3.2",
365
+ baseUrl: DEFAULT_OLLAMA_BASE_URL,
366
+ });
367
+ });
368
+
369
+ it("deve criar ChatOllama com baseUrl, temperature e numCtx customizados", () => {
370
+ AIModels.ollama({
371
+ model: "qwen2.5:7b",
372
+ baseUrl: "http://custom:11434",
373
+ temperature: 0.2,
374
+ numCtx: 8192,
375
+ maxTokens: 512,
376
+ });
377
+
378
+ expect(ChatOllama).toHaveBeenCalledWith({
379
+ model: "qwen2.5:7b",
380
+ baseUrl: "http://custom:11434",
381
+ temperature: 0.2,
382
+ numCtx: 8192,
383
+ numPredict: 512,
384
+ });
385
+ });
386
+ });
387
+
388
+ describe("openaiCompatible", () => {
389
+ it("deve criar ChatOpenAI com baseURL local", () => {
390
+ AIModels.openaiCompatible({
391
+ model: "meta-llama/Llama-3.2-3B-Instruct",
392
+ baseURL: "http://localhost:1234/v1",
393
+ });
394
+
395
+ expect(ChatOpenAI).toHaveBeenCalledWith({
396
+ model: "meta-llama/Llama-3.2-3B-Instruct",
397
+ apiKey: "not-needed",
398
+ configuration: {
399
+ baseURL: "http://localhost:1234/v1",
400
+ },
401
+ });
402
+ });
403
+
404
+ it("deve aceitar apiKey e parâmetros opcionais", () => {
405
+ AIModels.openaiCompatible({
406
+ model: "my-model",
407
+ baseURL: "http://127.0.0.1:11434/v1",
408
+ apiKey: "lm-studio",
409
+ maxTokens: 1000,
410
+ temperature: 0.5,
411
+ });
412
+
413
+ expect(ChatOpenAI).toHaveBeenCalledWith({
414
+ model: "my-model",
415
+ apiKey: "lm-studio",
416
+ maxTokens: 1000,
417
+ temperature: 0.5,
418
+ configuration: {
419
+ baseURL: "http://127.0.0.1:11434/v1",
420
+ },
421
+ });
422
+ });
423
+
424
+ it("deve lançar erro quando baseURL não é fornecida", () => {
425
+ expect(() =>
426
+ AIModels.openaiCompatible({ model: "x", baseURL: "" }),
427
+ ).toThrow("baseURL é obrigatório");
428
+ });
429
+ });
208
430
  });
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import z from "zod";
3
+ import {
4
+ buildJsonObjectSystemAppend,
5
+ isDeepSeekJsonObjectOnlyModel,
6
+ parseJsonFromAssistantMessages,
7
+ } from "../../../src/langchain/structured-output";
8
+
9
+ describe("structured-output", () => {
10
+ describe("isDeepSeekJsonObjectOnlyModel", () => {
11
+ it("deve detectar openrouter/deepseek/*", () => {
12
+ expect(
13
+ isDeepSeekJsonObjectOnlyModel("openrouter/deepseek/deepseek-v4-pro"),
14
+ ).toBe(true);
15
+ });
16
+
17
+ it("não deve detectar openrouter/openai/*", () => {
18
+ expect(
19
+ isDeepSeekJsonObjectOnlyModel("openrouter/openai/gpt-5-nano"),
20
+ ).toBe(false);
21
+ });
22
+ });
23
+
24
+ describe("buildJsonObjectSystemAppend", () => {
25
+ it("deve incluir a palavra json e o schema", () => {
26
+ const append = buildJsonObjectSystemAppend(
27
+ z.object({ name: z.string() }),
28
+ );
29
+ expect(append.toLowerCase()).toContain("json");
30
+ expect(append).toContain("name");
31
+ });
32
+ });
33
+
34
+ describe("parseJsonFromAssistantMessages", () => {
35
+ it("deve parsear JSON direto", () => {
36
+ const result = parseJsonFromAssistantMessages([
37
+ { content: '{"a":1}' } as any,
38
+ ]);
39
+ expect(result).toEqual({ a: 1 });
40
+ });
41
+
42
+ it("deve parsear JSON em fence markdown", () => {
43
+ const result = parseJsonFromAssistantMessages([
44
+ { content: '```json\n{"b":2}\n```' } as any,
45
+ ]);
46
+ expect(result).toEqual({ b: 2 });
47
+ });
48
+ });
49
+ });