@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
@@ -0,0 +1,130 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import {
3
+ transcribeDetailedOpenAI,
4
+ transcribeToSrtOpenAI,
5
+ transcribeToVttOpenAI,
6
+ transcribeDiarizedOpenAI,
7
+ translateOpenAI,
8
+ transcribeWithWhisperOpenAI,
9
+ } from "../../../src/audio/transcription-openai.js";
10
+
11
+ const mockTranscriptionsCreate = vi.fn();
12
+ const mockTranslationsCreate = vi.fn();
13
+
14
+ vi.mock("openai", () => ({
15
+ default: class MockOpenAI {
16
+ audio = {
17
+ transcriptions: { create: mockTranscriptionsCreate },
18
+ translations: { create: mockTranslationsCreate },
19
+ };
20
+ },
21
+ toFile: async (buffer: Buffer, filename: string) =>
22
+ new File([new Uint8Array(buffer)], filename, { type: "audio/mpeg" }),
23
+ }));
24
+
25
+ describe("transcription-openai", () => {
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ });
29
+
30
+ it("transcribeDetailedOpenAI mapeia verbose_json com words", async () => {
31
+ mockTranscriptionsCreate.mockResolvedValue({
32
+ text: "olá",
33
+ language: "pt",
34
+ duration: 1.2,
35
+ words: [{ word: "olá", start: 0, end: 0.5 }],
36
+ segments: [{ id: 0, start: 0, end: 0.5, text: "olá" }],
37
+ });
38
+
39
+ const result = await transcribeDetailedOpenAI(
40
+ Buffer.from("audio"),
41
+ {
42
+ model: "whisper-1",
43
+ responseFormat: "verbose_json",
44
+ timestampGranularities: ["word"],
45
+ },
46
+ "sk-test",
47
+ );
48
+
49
+ expect(result.text).toBe("olá");
50
+ expect(result.words).toHaveLength(1);
51
+ expect(result.segments).toHaveLength(1);
52
+ });
53
+
54
+ it("transcribeToSrtOpenAI usa response_format srt", async () => {
55
+ mockTranscriptionsCreate.mockResolvedValue("1\n00:00:00,000 --> 00:00:01,000\nolá");
56
+
57
+ const srt = await transcribeToSrtOpenAI(
58
+ Buffer.from("audio"),
59
+ { languageIn2Digits: "pt" },
60
+ "sk-test",
61
+ );
62
+
63
+ expect(srt).toContain("olá");
64
+ expect(mockTranscriptionsCreate).toHaveBeenCalledWith(
65
+ expect.objectContaining({ response_format: "srt", model: "whisper-1" }),
66
+ );
67
+ });
68
+
69
+ it("transcribeToVttOpenAI usa response_format vtt", async () => {
70
+ mockTranscriptionsCreate.mockResolvedValue("WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nolá");
71
+
72
+ const vtt = await transcribeToVttOpenAI(Buffer.from("audio"), {}, "sk-test");
73
+
74
+ expect(vtt).toContain("WEBVTT");
75
+ expect(mockTranscriptionsCreate).toHaveBeenCalledWith(
76
+ expect.objectContaining({ response_format: "vtt" }),
77
+ );
78
+ });
79
+
80
+ it("transcribeDiarizedOpenAI mapeia speakers", async () => {
81
+ mockTranscriptionsCreate.mockResolvedValue({
82
+ text: "fala a fala b",
83
+ segments: [
84
+ { speaker: "A", start: 0, end: 1, text: "fala a" },
85
+ { speaker: "B", start: 1, end: 2, text: "fala b" },
86
+ ],
87
+ });
88
+
89
+ const result = await transcribeDiarizedOpenAI(Buffer.from("audio"), {}, "sk-test");
90
+
91
+ expect(result.speakers).toHaveLength(2);
92
+ expect(result.speakers?.[0].speaker).toBe("A");
93
+ expect(mockTranscriptionsCreate).toHaveBeenCalledWith(
94
+ expect.objectContaining({
95
+ model: "gpt-4o-transcribe-diarize",
96
+ response_format: "diarized_json",
97
+ }),
98
+ );
99
+ });
100
+
101
+ it("translateOpenAI chama translations.create", async () => {
102
+ mockTranslationsCreate.mockResolvedValue({ text: "hello" });
103
+
104
+ const text = await translateOpenAI(Buffer.from("audio"), {}, "sk-test");
105
+
106
+ expect(text).toBe("hello");
107
+ expect(mockTranslationsCreate).toHaveBeenCalled();
108
+ });
109
+
110
+ it("rejeita timestampGranularities fora de whisper-1 verbose_json", async () => {
111
+ await expect(
112
+ transcribeDetailedOpenAI(Buffer.from("x"), {
113
+ model: "gpt-4o-transcribe",
114
+ responseFormat: "json",
115
+ timestampGranularities: ["word"],
116
+ }),
117
+ ).rejects.toThrow("timestampGranularities");
118
+ });
119
+
120
+ it("transcribeWithWhisperOpenAI retorna string", async () => {
121
+ mockTranscriptionsCreate.mockResolvedValue("texto simples");
122
+
123
+ const text = await transcribeWithWhisperOpenAI(Buffer.from("audio"), {}, "sk-test");
124
+
125
+ expect(text).toBe("texto simples");
126
+ expect(mockTranscriptionsCreate).toHaveBeenCalledWith(
127
+ expect.objectContaining({ response_format: "text" }),
128
+ );
129
+ });
130
+ });
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { transcribeOpenRouter } from "../../../src/audio/transcription-openrouter.js";
3
+
4
+ const mockFetch = vi.fn();
5
+
6
+ vi.stubGlobal("fetch", mockFetch);
7
+
8
+ describe("transcription-openrouter", () => {
9
+ beforeEach(() => {
10
+ vi.clearAllMocks();
11
+ mockFetch.mockResolvedValue({
12
+ ok: true,
13
+ json: async () => ({
14
+ text: "transcrito via OR",
15
+ usage: { seconds: 2, cost: 0.001 },
16
+ }),
17
+ });
18
+ });
19
+
20
+ it("envia input_audio base64 e model", async () => {
21
+ const buf = Buffer.from("fake-audio");
22
+ const result = await transcribeOpenRouter(
23
+ buf,
24
+ { model: "openai/whisper-1", format: "mp3", language: "pt" },
25
+ "or-key",
26
+ );
27
+
28
+ expect(result.text).toBe("transcrito via OR");
29
+ expect(mockFetch).toHaveBeenCalledWith(
30
+ "https://openrouter.ai/api/v1/audio/transcriptions",
31
+ expect.objectContaining({
32
+ method: "POST",
33
+ headers: expect.objectContaining({
34
+ Authorization: "Bearer or-key",
35
+ }),
36
+ }),
37
+ );
38
+
39
+ const body = JSON.parse(
40
+ (mockFetch.mock.calls[0][1] as RequestInit).body as string,
41
+ );
42
+ expect(body.model).toBe("openai/whisper-1");
43
+ expect(body.input_audio.format).toBe("mp3");
44
+ expect(body.input_audio.data).toBe(buf.toString("base64"));
45
+ expect(body.language).toBe("pt");
46
+ });
47
+
48
+ it("aplica provider default deepseek para modelos deepseek", async () => {
49
+ await transcribeOpenRouter(
50
+ Buffer.from("x"),
51
+ { model: "deepseek/some-stt" },
52
+ "or-key",
53
+ );
54
+
55
+ const body = JSON.parse(
56
+ (mockFetch.mock.calls[0][1] as RequestInit).body as string,
57
+ );
58
+ expect(body.provider).toEqual({ only: ["deepseek"] });
59
+ });
60
+
61
+ it("exige openRouterApiKey", async () => {
62
+ await expect(
63
+ transcribeOpenRouter(Buffer.from("x"), { model: "openai/whisper-1" }),
64
+ ).rejects.toThrow("OpenRouter API key");
65
+ });
66
+
67
+ it("propaga erro HTTP", async () => {
68
+ mockFetch.mockResolvedValue({
69
+ ok: false,
70
+ status: 400,
71
+ text: async () => "bad request",
72
+ });
73
+
74
+ await expect(
75
+ transcribeOpenRouter(
76
+ Buffer.from("x"),
77
+ { model: "openai/whisper-1" },
78
+ "or-key",
79
+ ),
80
+ ).rejects.toThrow("OpenRouter STT falhou");
81
+ });
82
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { embedGemini } from "../../../src/embeddings/embed-gemini.js";
3
+
4
+ const mockEmbedQuery = vi.fn();
5
+ const mockEmbedDocuments = vi.fn();
6
+
7
+ vi.mock("@langchain/google-genai", () => ({
8
+ GoogleGenerativeAIEmbeddings: class MockGoogleGenerativeAIEmbeddings {
9
+ constructor(public params: Record<string, unknown>) {}
10
+ embedQuery = mockEmbedQuery;
11
+ embedDocuments = mockEmbedDocuments;
12
+ },
13
+ }));
14
+
15
+ describe("embed-gemini", () => {
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+ mockEmbedQuery.mockResolvedValue([0.5, 0.6]);
19
+ mockEmbedDocuments.mockResolvedValue([
20
+ [0.1, 0.2],
21
+ [0.3, 0.4],
22
+ ]);
23
+ });
24
+
25
+ it("embedQuery para input único", async () => {
26
+ const result = await embedGemini(
27
+ "hello",
28
+ { model: "gemini-embedding-001" },
29
+ "gemini-key",
30
+ );
31
+
32
+ expect(result.embeddings).toEqual([[0.5, 0.6]]);
33
+ expect(mockEmbedQuery).toHaveBeenCalledWith("hello");
34
+ });
35
+
36
+ it("embedDocuments para lote", async () => {
37
+ const result = await embedGemini(
38
+ ["a", "b"],
39
+ { model: "gemini-embedding-001", taskType: "RETRIEVAL_DOCUMENT" },
40
+ "gemini-key",
41
+ );
42
+
43
+ expect(result.embeddings).toHaveLength(2);
44
+ expect(mockEmbedDocuments).toHaveBeenCalledWith(["a", "b"]);
45
+ });
46
+
47
+ it("exige googleGeminiToken", async () => {
48
+ await expect(
49
+ embedGemini("x", { model: "gemini-embedding-001" }),
50
+ ).rejects.toThrow("googleGeminiToken");
51
+ });
52
+ });
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { embedOpenAI } from "../../../src/embeddings/embed-openai.js";
3
+
4
+ const mockEmbeddingsCreate = vi.fn();
5
+
6
+ vi.mock("openai", () => ({
7
+ default: class MockOpenAI {
8
+ embeddings = { create: mockEmbeddingsCreate };
9
+ },
10
+ }));
11
+
12
+ describe("embed-openai", () => {
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ mockEmbeddingsCreate.mockResolvedValue({
16
+ model: "text-embedding-3-small",
17
+ data: [
18
+ { index: 0, embedding: [0.1, 0.2] },
19
+ { index: 1, embedding: [0.3, 0.4] },
20
+ ],
21
+ usage: { prompt_tokens: 10, total_tokens: 10 },
22
+ });
23
+ });
24
+
25
+ it("cria embeddings para string única", async () => {
26
+ mockEmbeddingsCreate.mockResolvedValueOnce({
27
+ model: "text-embedding-3-small",
28
+ data: [{ index: 0, embedding: [0.1, 0.2] }],
29
+ usage: { prompt_tokens: 5, total_tokens: 5 },
30
+ });
31
+
32
+ const result = await embedOpenAI(
33
+ "hello",
34
+ { model: "text-embedding-3-small" },
35
+ "sk-test",
36
+ );
37
+
38
+ expect(result.embeddings).toEqual([[0.1, 0.2]]);
39
+ expect(mockEmbeddingsCreate).toHaveBeenCalledWith(
40
+ expect.objectContaining({
41
+ model: "text-embedding-3-small",
42
+ input: "hello",
43
+ }),
44
+ );
45
+ });
46
+
47
+ it("cria embeddings em lote", async () => {
48
+ const result = await embedOpenAI(
49
+ ["a", "b"],
50
+ { model: "text-embedding-3-large", dimensions: 256 },
51
+ "sk-test",
52
+ );
53
+
54
+ expect(result.embeddings).toHaveLength(2);
55
+ expect(mockEmbeddingsCreate).toHaveBeenCalledWith(
56
+ expect.objectContaining({
57
+ input: ["a", "b"],
58
+ dimensions: 256,
59
+ }),
60
+ );
61
+ });
62
+
63
+ it("exige API key", async () => {
64
+ await expect(
65
+ embedOpenAI("x", { model: "text-embedding-3-small" }),
66
+ ).rejects.toThrow("OpenAI API key");
67
+ });
68
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { embedOpenRouter } from "../../../src/embeddings/embed-openrouter.js";
3
+
4
+ const mockEmbeddingsCreate = vi.fn();
5
+
6
+ vi.mock("../../../src/utils/openrouter-client.js", () => ({
7
+ createOpenRouterOpenAIClient: () => ({
8
+ embeddings: { create: mockEmbeddingsCreate },
9
+ }),
10
+ }));
11
+
12
+ describe("embed-openrouter", () => {
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ mockEmbeddingsCreate.mockResolvedValue({
16
+ model: "openai/text-embedding-3-small",
17
+ data: [{ index: 0, embedding: [1, 2, 3] }],
18
+ usage: { prompt_tokens: 5, total_tokens: 5 },
19
+ });
20
+ });
21
+
22
+ it("remove prefixo openrouter/ do model", async () => {
23
+ const result = await embedOpenRouter(
24
+ "test",
25
+ { model: "openrouter/openai/text-embedding-3-small" },
26
+ "or-key",
27
+ );
28
+
29
+ expect(result.embeddings[0]).toEqual([1, 2, 3]);
30
+ expect(mockEmbeddingsCreate).toHaveBeenCalledWith(
31
+ expect.objectContaining({
32
+ model: "openai/text-embedding-3-small",
33
+ input: "test",
34
+ }),
35
+ );
36
+ });
37
+
38
+ it("repassa provider quando informado", async () => {
39
+ await embedOpenRouter(
40
+ "test",
41
+ {
42
+ model: "openrouter/openai/text-embedding-3-small",
43
+ openRouterProvider: { sort: "price" },
44
+ },
45
+ "or-key",
46
+ );
47
+
48
+ expect(mockEmbeddingsCreate).toHaveBeenCalledWith(
49
+ expect.objectContaining({
50
+ provider: { sort: "price" },
51
+ }),
52
+ );
53
+ });
54
+
55
+ it("exige openRouterApiKey", async () => {
56
+ await expect(
57
+ embedOpenRouter("x", {
58
+ model: "openrouter/openai/text-embedding-3-small",
59
+ }),
60
+ ).rejects.toThrow("OpenRouter API key");
61
+ });
62
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import {
3
+ AIEmbeddings,
4
+ ANTHROPIC_NO_NATIVE_EMBEDDINGS,
5
+ } from "../../../src/index.js";
6
+
7
+ vi.mock("../../../src/embeddings/embed-openai.js", () => ({
8
+ embedOpenAI: vi.fn().mockResolvedValue({
9
+ embeddings: [[1]],
10
+ model: "text-embedding-3-small",
11
+ }),
12
+ }));
13
+
14
+ vi.mock("../../../src/embeddings/embed-openrouter.js", () => ({
15
+ embedOpenRouter: vi.fn().mockResolvedValue({
16
+ embeddings: [[2]],
17
+ model: "openai/text-embedding-3-small",
18
+ }),
19
+ }));
20
+
21
+ vi.mock("../../../src/embeddings/embed-gemini.js", () => ({
22
+ embedGemini: vi.fn().mockResolvedValue({
23
+ embeddings: [[3]],
24
+ model: "gemini-embedding-001",
25
+ }),
26
+ }));
27
+
28
+ describe("AIEmbeddings facade", () => {
29
+ it("expõe métodos estáticos", () => {
30
+ expect(typeof AIEmbeddings.embedOpenAI).toBe("function");
31
+ expect(typeof AIEmbeddings.embedOpenRouter).toBe("function");
32
+ expect(typeof AIEmbeddings.embedGemini).toBe("function");
33
+ expect(typeof AIEmbeddings.embedDocuments).toBe("function");
34
+ expect(typeof AIEmbeddings.embedQuery).toBe("function");
35
+ });
36
+
37
+ it("exporta constante Anthropic", () => {
38
+ expect(ANTHROPIC_NO_NATIVE_EMBEDDINGS).toContain("Anthropic");
39
+ });
40
+
41
+ it("embedQuery roteia por prefixo openrouter/", async () => {
42
+ const { embedOpenRouter } = await import(
43
+ "../../../src/embeddings/embed-openrouter.js"
44
+ );
45
+
46
+ const vec = await AIEmbeddings.embedQuery(
47
+ "q",
48
+ { model: "openrouter/openai/text-embedding-3-small" },
49
+ { openRouterApiKey: "k" },
50
+ );
51
+
52
+ expect(vec).toEqual([2]);
53
+ expect(embedOpenRouter).toHaveBeenCalled();
54
+ });
55
+ });
@@ -20,7 +20,10 @@ vi.mock("../../src/langchain/models", () => ({
20
20
  gpt: vi.fn(),
21
21
  gemini: vi.fn(),
22
22
  openrouter: vi.fn(),
23
+ ollama: vi.fn(),
24
+ openaiCompatible: vi.fn(),
23
25
  },
26
+ DEFAULT_OLLAMA_BASE_URL: "http://127.0.0.1:11434",
24
27
  }));
25
28
 
26
29
  vi.mock("../../src/langchain/memory", async () => {
@@ -264,6 +267,77 @@ describe("AI", () => {
264
267
  ).rejects.toThrow("Model not supported");
265
268
  });
266
269
 
270
+ it("deve usar modelo Ollama quando aiModel começa com 'ollama/'", async () => {
271
+ const mockModel = {} as any;
272
+ const mockMessages = [AIMessages.human("Teste")];
273
+ const mockResponse = {
274
+ messages: [{ content: "Resposta local" } as any],
275
+ };
276
+
277
+ vi.mocked(AIModels.ollama).mockReturnValue(mockModel);
278
+ vi.mocked(createAgent).mockReturnValue({
279
+ invoke: vi.fn().mockResolvedValue(mockResponse),
280
+ } as any);
281
+
282
+ const localAi = new AI({ ollamaBaseUrl: "http://ollama:11434" });
283
+
284
+ await localAi.call({
285
+ aiModel: "ollama/llama3.2",
286
+ messages: mockMessages,
287
+ });
288
+
289
+ expect(AIModels.ollama).toHaveBeenCalledWith({
290
+ model: "llama3.2",
291
+ baseUrl: "http://ollama:11434",
292
+ maxTokens: undefined,
293
+ temperature: undefined,
294
+ numCtx: undefined,
295
+ });
296
+ });
297
+
298
+ it("deve usar modelo local OpenAI-compatible quando aiModel começa com 'local/'", async () => {
299
+ const mockModel = {} as any;
300
+ const mockMessages = [AIMessages.human("Teste")];
301
+ const mockResponse = {
302
+ messages: [{ content: "LM Studio" } as any],
303
+ };
304
+
305
+ vi.mocked(AIModels.openaiCompatible).mockReturnValue(mockModel);
306
+ vi.mocked(createAgent).mockReturnValue({
307
+ invoke: vi.fn().mockResolvedValue(mockResponse),
308
+ } as any);
309
+
310
+ const localAi = new AI({
311
+ localBaseUrl: "http://localhost:1234/v1",
312
+ localApiKey: "test-local-key",
313
+ });
314
+
315
+ await localAi.call({
316
+ aiModel: "local/qwen2.5-7b-instruct",
317
+ messages: mockMessages,
318
+ modelConfig: { temperature: 0.3 },
319
+ });
320
+
321
+ expect(AIModels.openaiCompatible).toHaveBeenCalledWith({
322
+ model: "qwen2.5-7b-instruct",
323
+ baseURL: "http://localhost:1234/v1",
324
+ apiKey: "test-local-key",
325
+ maxTokens: undefined,
326
+ temperature: 0.3,
327
+ });
328
+ });
329
+
330
+ it("deve lançar erro para local/* sem localBaseUrl", async () => {
331
+ const mockMessages = [AIMessages.human("Teste")];
332
+
333
+ await expect(
334
+ ai.call({
335
+ aiModel: "local/my-model",
336
+ messages: mockMessages,
337
+ }),
338
+ ).rejects.toThrow("localBaseUrl é obrigatório");
339
+ });
340
+
267
341
  it("deve passar modelConfig quando fornecido", async () => {
268
342
  const mockModel = {} as any;
269
343
  const mockMessages = [AIMessages.human("Teste")];
@@ -388,6 +462,106 @@ describe("AI", () => {
388
462
  warnSpy.mockRestore();
389
463
  });
390
464
 
465
+ it("deve aplicar provider only deepseek por padrão em openrouter/deepseek/*", async () => {
466
+ const mockModel = {} as any;
467
+ const mockMessages = [AIMessages.human("Teste")];
468
+ const mockResponse = {
469
+ messages: [{ content: "Resposta" } as any],
470
+ };
471
+
472
+ vi.mocked(AIModels.openrouter).mockReturnValue(mockModel);
473
+ vi.mocked(createAgent).mockReturnValue({
474
+ invoke: vi.fn().mockResolvedValue(mockResponse),
475
+ } as any);
476
+
477
+ const aiWithOpenRouter = new AI({
478
+ openRouterApiKey: "test-openrouter-key",
479
+ });
480
+
481
+ await aiWithOpenRouter.call({
482
+ aiModel: "openrouter/deepseek/deepseek-v3.2",
483
+ messages: mockMessages,
484
+ });
485
+
486
+ expect(AIModels.openrouter).toHaveBeenCalledWith(
487
+ expect.objectContaining({
488
+ model: "deepseek/deepseek-v3.2",
489
+ apiKey: "test-openrouter-key",
490
+ openRouterProvider: undefined,
491
+ openRouterAllowAllProviders: undefined,
492
+ }),
493
+ );
494
+ });
495
+
496
+ it("deve repassar openRouterProvider e openRouterAllowAllProviders no modelConfig", async () => {
497
+ const mockModel = {} as any;
498
+ const mockMessages = [AIMessages.human("Teste")];
499
+ const mockResponse = {
500
+ messages: [{ content: "Resposta" } as any],
501
+ };
502
+ const openRouterProvider = {
503
+ only: ["deepinfra"],
504
+ max_price: { prompt: 1, completion: 2 },
505
+ };
506
+
507
+ vi.mocked(AIModels.openrouter).mockReturnValue(mockModel);
508
+ vi.mocked(createAgent).mockReturnValue({
509
+ invoke: vi.fn().mockResolvedValue(mockResponse),
510
+ } as any);
511
+
512
+ const aiWithOpenRouter = new AI({
513
+ openRouterApiKey: "test-openrouter-key",
514
+ });
515
+
516
+ await aiWithOpenRouter.call({
517
+ aiModel: "openrouter/deepseek/deepseek-r1",
518
+ messages: mockMessages,
519
+ modelConfig: {
520
+ openRouterProvider,
521
+ openRouterAllowAllProviders: false,
522
+ },
523
+ });
524
+
525
+ expect(AIModels.openrouter).toHaveBeenCalledWith(
526
+ expect.objectContaining({
527
+ model: "deepseek/deepseek-r1",
528
+ openRouterProvider,
529
+ openRouterAllowAllProviders: false,
530
+ }),
531
+ );
532
+ });
533
+
534
+ it("deve repassar openRouterAllowAllProviders true para liberar todos os providers", async () => {
535
+ const mockModel = {} as any;
536
+ const mockMessages = [AIMessages.human("Teste")];
537
+ const mockResponse = {
538
+ messages: [{ content: "Resposta" } as any],
539
+ };
540
+
541
+ vi.mocked(AIModels.openrouter).mockReturnValue(mockModel);
542
+ vi.mocked(createAgent).mockReturnValue({
543
+ invoke: vi.fn().mockResolvedValue(mockResponse),
544
+ } as any);
545
+
546
+ const aiWithOpenRouter = new AI({
547
+ openRouterApiKey: "test-openrouter-key",
548
+ });
549
+
550
+ await aiWithOpenRouter.call({
551
+ aiModel: "openrouter/deepseek/deepseek-v3.2",
552
+ messages: mockMessages,
553
+ modelConfig: {
554
+ openRouterAllowAllProviders: true,
555
+ },
556
+ });
557
+
558
+ expect(AIModels.openrouter).toHaveBeenCalledWith(
559
+ expect.objectContaining({
560
+ openRouterAllowAllProviders: true,
561
+ }),
562
+ );
563
+ });
564
+
391
565
  it("deve lançar erro quando memory está ativo e threadId não é fornecido", async () => {
392
566
  const aiWithMemory = new AI({
393
567
  openAIApiKey: "test-key",
@@ -676,6 +850,41 @@ describe("AI", () => {
676
850
  expect(result.response).toEqual(mockStructuredResponse);
677
851
  });
678
852
 
853
+ it("deve usar json_object para DeepSeek sem responseFormat no agente", async () => {
854
+ const mockModel = {} as any;
855
+ const mockMessages = [AIMessages.human("Teste")];
856
+ const outputSchema = z.object({ sum: z.number(), product: z.number() });
857
+
858
+ vi.mocked(AIModels.openrouter).mockReturnValue(mockModel);
859
+ vi.mocked(createAgent).mockReturnValue({
860
+ invoke: vi.fn().mockResolvedValue({
861
+ messages: [{ content: '{"sum":11,"product":28}' }],
862
+ }),
863
+ } as any);
864
+
865
+ const aiWithOpenRouter = new AI({
866
+ openRouterApiKey: "test-openrouter-key",
867
+ });
868
+
869
+ const result = await aiWithOpenRouter.callStructuredOutput({
870
+ aiModel: "openrouter/deepseek/deepseek-v4-flash",
871
+ messages: mockMessages,
872
+ outputSchema,
873
+ });
874
+
875
+ expect(AIModels.openrouter).toHaveBeenCalledWith(
876
+ expect.objectContaining({
877
+ openRouterForceJsonObject: true,
878
+ }),
879
+ );
880
+ expect(createAgent).toHaveBeenCalledWith(
881
+ expect.objectContaining({
882
+ responseFormat: undefined,
883
+ }),
884
+ );
885
+ expect(result.response).toEqual({ sum: 11, product: 28 });
886
+ });
887
+
679
888
  it("deve validar o schema e lançar erro se inválido", async () => {
680
889
  const mockModel = {} as any;
681
890
  const mockMessages = [AIMessages.human("Teste")];