@luanpoppe/ai 1.1.4 → 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 +21 -5
  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 +12 -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 +104 -6
  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 +52 -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,65 @@
1
+ import { AI } from "../../src/index";
2
+ import { AIMessages } from "../../src/langchain/messages";
3
+ import "dotenv/config";
4
+
5
+ const ollamaBaseUrl = process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434";
6
+ const ollamaModel = process.env.OLLAMA_MODEL_NAME ?? "llama3.2";
7
+ const localBaseUrl = process.env.LOCAL_BASE_URL;
8
+ const localModel = process.env.LOCAL_MODEL_NAME ?? "local-model";
9
+
10
+ const timeout = 180 * 1000;
11
+
12
+ describe("AI E2E — modelos locais", () => {
13
+ describe.skipIf(!process.env.OLLAMA_E2E)(
14
+ "Ollama (API nativa, prefixo ollama/)",
15
+ () => {
16
+ it(
17
+ "deve responder via ChatOllama",
18
+ { timeout },
19
+ async () => {
20
+ const ai = new AI({ ollamaBaseUrl });
21
+
22
+ const result = await ai.call({
23
+ aiModel: `ollama/${ollamaModel}`,
24
+ messages: [
25
+ AIMessages.human(
26
+ "Responda apenas com: teste ollama ok",
27
+ ),
28
+ ],
29
+ });
30
+
31
+ expect(result.text).toBeDefined();
32
+ expect(result.text.length).toBeGreaterThan(0);
33
+ },
34
+ );
35
+ },
36
+ );
37
+
38
+ describe.skipIf(!localBaseUrl)(
39
+ "OpenAI-compatible (prefixo local/)",
40
+ () => {
41
+ it(
42
+ "deve responder via servidor local (LM Studio, Ollama /v1, etc.)",
43
+ { timeout },
44
+ async () => {
45
+ const ai = new AI({
46
+ localBaseUrl,
47
+ localApiKey: process.env.LOCAL_API_KEY,
48
+ });
49
+
50
+ const result = await ai.call({
51
+ aiModel: `local/${localModel}`,
52
+ messages: [
53
+ AIMessages.human(
54
+ "Responda apenas com: teste local ok",
55
+ ),
56
+ ],
57
+ });
58
+
59
+ expect(result.text).toBeDefined();
60
+ expect(result.text.length).toBeGreaterThan(0);
61
+ },
62
+ );
63
+ },
64
+ );
65
+ });
@@ -0,0 +1,17 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { AIAudio, ANTHROPIC_NO_NATIVE_AUDIO } from "../../../src/index.js";
3
+
4
+ describe("AIAudio facade", () => {
5
+ it("expõe métodos estáticos principais", () => {
6
+ expect(typeof AIAudio.transcribeWithWhisper).toBe("function");
7
+ expect(typeof AIAudio.transcribeDetailedOpenAI).toBe("function");
8
+ expect(typeof AIAudio.transcribeOpenRouter).toBe("function");
9
+ expect(typeof AIAudio.speakOpenAI).toBe("function");
10
+ expect(typeof AIAudio.speakOpenRouter).toBe("function");
11
+ expect(typeof AIAudio.transcribeWithGeminiPrompt).toBe("function");
12
+ });
13
+
14
+ it("exporta constante Anthropic", () => {
15
+ expect(ANTHROPIC_NO_NATIVE_AUDIO).toContain("Anthropic");
16
+ });
17
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { speakOpenAI } from "../../../src/audio/speech-openai.js";
3
+
4
+ const mockSpeechCreate = vi.fn();
5
+
6
+ vi.mock("openai", () => ({
7
+ default: class MockOpenAI {
8
+ audio = {
9
+ speech: {
10
+ create: mockSpeechCreate,
11
+ },
12
+ };
13
+ },
14
+ }));
15
+
16
+ describe("speech-openai", () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ mockSpeechCreate.mockResolvedValue({
20
+ arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
21
+ });
22
+ });
23
+
24
+ it("cria speech com voice e model padrão", async () => {
25
+ const result = await speakOpenAI(
26
+ "Olá",
27
+ { voice: "nova" },
28
+ "sk-test",
29
+ );
30
+
31
+ expect(result.audio).toEqual(Buffer.from([1, 2, 3]));
32
+ expect(result.contentType).toBe("audio/mpeg");
33
+ expect(mockSpeechCreate).toHaveBeenCalledWith(
34
+ expect.objectContaining({
35
+ model: "gpt-4o-mini-tts",
36
+ voice: "nova",
37
+ input: "Olá",
38
+ response_format: "mp3",
39
+ }),
40
+ );
41
+ });
42
+
43
+ it("repassa instructions para gpt-4o-mini-tts", async () => {
44
+ await speakOpenAI(
45
+ "Oi",
46
+ {
47
+ voice: "alloy",
48
+ model: "gpt-4o-mini-tts",
49
+ instructions: "Fale devagar.",
50
+ },
51
+ "sk-test",
52
+ );
53
+
54
+ expect(mockSpeechCreate).toHaveBeenCalledWith(
55
+ expect.objectContaining({ instructions: "Fale devagar." }),
56
+ );
57
+ });
58
+
59
+ it("rejeita instructions em tts-1", async () => {
60
+ await expect(
61
+ speakOpenAI(
62
+ "Oi",
63
+ { voice: "alloy", model: "tts-1", instructions: "x" },
64
+ "sk-test",
65
+ ),
66
+ ).rejects.toThrow("instructions");
67
+ });
68
+
69
+ it("exige API key", async () => {
70
+ await expect(speakOpenAI("Oi", { voice: "nova" })).rejects.toThrow(
71
+ "OpenAI API key",
72
+ );
73
+ });
74
+ });
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { speakOpenRouter } from "../../../src/audio/speech-openrouter.js";
3
+
4
+ const mockSpeechCreate = vi.fn();
5
+ let capturedBaseURL: string | undefined;
6
+
7
+ vi.mock("../../../src/audio/openrouter-client.js", () => ({
8
+ createOpenRouterOpenAIClient: (apiKey: string) => {
9
+ capturedBaseURL = "https://openrouter.ai/api/v1";
10
+ return {
11
+ apiKey,
12
+ audio: {
13
+ speech: { create: mockSpeechCreate },
14
+ },
15
+ };
16
+ },
17
+ }));
18
+
19
+ describe("speech-openrouter", () => {
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ capturedBaseURL = undefined;
23
+ mockSpeechCreate.mockResolvedValue({
24
+ arrayBuffer: async () => new Uint8Array([9, 8]).buffer,
25
+ });
26
+ });
27
+
28
+ it("cria speech com model slug e voice", async () => {
29
+ const result = await speakOpenRouter(
30
+ "Resumo",
31
+ {
32
+ model: "google/gemini-2.5-flash-preview-tts",
33
+ voice: "Kore",
34
+ responseFormat: "mp3",
35
+ },
36
+ "or-key",
37
+ );
38
+
39
+ expect(result.audio).toEqual(Buffer.from([9, 8]));
40
+ expect(mockSpeechCreate).toHaveBeenCalledWith(
41
+ expect.objectContaining({
42
+ model: "google/gemini-2.5-flash-preview-tts",
43
+ voice: "Kore",
44
+ input: "Resumo",
45
+ }),
46
+ );
47
+ });
48
+
49
+ it("mescla instructions em provider.options sem apagar only", async () => {
50
+ await speakOpenRouter(
51
+ "Oi",
52
+ {
53
+ model: "deepseek/some-tts",
54
+ voice: "alloy",
55
+ instructions: "Tom formal.",
56
+ },
57
+ "or-key",
58
+ );
59
+
60
+ const body = mockSpeechCreate.mock.calls[0][0] as Record<string, unknown>;
61
+ const provider = body.provider as Record<string, unknown>;
62
+ expect(provider.only).toEqual(["deepseek"]);
63
+ expect(provider.options).toEqual({
64
+ openai: { instructions: "Tom formal." },
65
+ });
66
+ });
67
+
68
+ it("usa pcm por padrão para modelos Gemini TTS", async () => {
69
+ await speakOpenRouter(
70
+ "Oi",
71
+ {
72
+ model: "google/gemini-3.1-flash-tts-preview",
73
+ voice: "Kore",
74
+ },
75
+ "or-key",
76
+ );
77
+
78
+ expect(mockSpeechCreate).toHaveBeenCalledWith(
79
+ expect.objectContaining({ response_format: "pcm" }),
80
+ );
81
+ });
82
+
83
+ it("exige model e voice", async () => {
84
+ await expect(
85
+ speakOpenRouter("x", { model: "", voice: "a" }, "k"),
86
+ ).rejects.toThrow("model é obrigatório");
87
+ });
88
+ });
@@ -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
+ });