@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,259 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import OpenAI from "openai";
4
+ import { toFile } from "openai";
5
+ import type { AudioBuffer } from "../@types/audio";
6
+ import type {
7
+ OpenAITranscriptionDetailed,
8
+ OpenAITranscriptionOptions,
9
+ TranscriptionSegment,
10
+ TranscriptionWord,
11
+ DiarizedSegment,
12
+ } from "./types";
13
+ import { getAudioExtension, toAudioBuffer } from "./utils";
14
+
15
+ function createOpenAIClient(apiKey?: string): OpenAI {
16
+ if (apiKey) {
17
+ return new OpenAI({ apiKey });
18
+ }
19
+ return new OpenAI();
20
+ }
21
+
22
+ async function createAudioFile(
23
+ audioBuffer: AudioBuffer,
24
+ format?: OpenAITranscriptionOptions["format"],
25
+ ) {
26
+ const buffer = toAudioBuffer(audioBuffer);
27
+ const extension = getAudioExtension(format);
28
+ const fileName = `whisper-${Date.now()}.${extension}`;
29
+ return toFile(buffer, fileName);
30
+ }
31
+
32
+ function validateTranscriptionOptions(options: OpenAITranscriptionOptions): void {
33
+ const model = options.model ?? "whisper-1";
34
+ const format = options.responseFormat ?? "text";
35
+
36
+ if (
37
+ options.timestampGranularities?.length &&
38
+ (model !== "whisper-1" || format !== "verbose_json")
39
+ ) {
40
+ throw new Error(
41
+ "[@luanpoppe/ai] timestampGranularities só é suportado com model whisper-1 e responseFormat verbose_json.",
42
+ );
43
+ }
44
+
45
+ if (model === "gpt-4o-transcribe-diarize" && format === "diarized_json") {
46
+ if (!options.chunkingStrategy) {
47
+ options.chunkingStrategy = "auto";
48
+ }
49
+ }
50
+ }
51
+
52
+ function mapVerboseResponse(
53
+ response: Record<string, unknown>,
54
+ ): OpenAITranscriptionDetailed {
55
+ const words = Array.isArray(response.words)
56
+ ? (response.words as TranscriptionWord[])
57
+ : undefined;
58
+ const segments = Array.isArray(response.segments)
59
+ ? (response.segments as TranscriptionSegment[])
60
+ : undefined;
61
+
62
+ const detailed: OpenAITranscriptionDetailed = {
63
+ text: String(response.text ?? ""),
64
+ };
65
+ if (typeof response.language === "string") {
66
+ detailed.language = response.language;
67
+ }
68
+ if (typeof response.duration === "number") {
69
+ detailed.duration = response.duration;
70
+ }
71
+ if (words) detailed.words = words;
72
+ if (segments) detailed.segments = segments;
73
+ return detailed;
74
+ }
75
+
76
+ function mapDiarizedResponse(
77
+ response: Record<string, unknown>,
78
+ ): OpenAITranscriptionDetailed {
79
+ const segments = Array.isArray(response.segments)
80
+ ? (response.segments as Array<Record<string, unknown>>)
81
+ : [];
82
+
83
+ const speakers: DiarizedSegment[] = segments.map((seg) => ({
84
+ speaker: String(seg.speaker ?? "unknown"),
85
+ start: Number(seg.start ?? 0),
86
+ end: Number(seg.end ?? 0),
87
+ text: String(seg.text ?? ""),
88
+ }));
89
+
90
+ return {
91
+ text: String(response.text ?? speakers.map((s) => s.text).join(" ")),
92
+ speakers,
93
+ segments: speakers.map((s, id) => ({
94
+ id,
95
+ start: s.start,
96
+ end: s.end,
97
+ text: s.text,
98
+ })),
99
+ };
100
+ }
101
+
102
+ function mapTranscriptionResponse(
103
+ response: unknown,
104
+ responseFormat: OpenAITranscriptionOptions["responseFormat"],
105
+ ): OpenAITranscriptionDetailed {
106
+ if (typeof response === "string") {
107
+ return { text: response };
108
+ }
109
+
110
+ const obj = response as Record<string, unknown>;
111
+ if (responseFormat === "diarized_json") {
112
+ return mapDiarizedResponse(obj);
113
+ }
114
+ if (responseFormat === "verbose_json") {
115
+ return mapVerboseResponse(obj);
116
+ }
117
+ return { text: String(obj.text ?? "") };
118
+ }
119
+
120
+ export async function transcribeDetailedOpenAI(
121
+ audioBuffer: AudioBuffer,
122
+ options: OpenAITranscriptionOptions = {},
123
+ openAIApiKey?: string,
124
+ ): Promise<OpenAITranscriptionDetailed> {
125
+ validateTranscriptionOptions(options);
126
+
127
+ const openai = createOpenAIClient(openAIApiKey);
128
+ const file = await createAudioFile(audioBuffer, options.format);
129
+ const responseFormat = options.responseFormat ?? "verbose_json";
130
+ const model = options.model ?? "whisper-1";
131
+
132
+ const params: OpenAI.Audio.TranscriptionCreateParams = {
133
+ file,
134
+ model,
135
+ response_format: responseFormat as OpenAI.Audio.TranscriptionCreateParams["response_format"],
136
+ };
137
+
138
+ if (options.languageIn2Digits) params.language = options.languageIn2Digits;
139
+ if (options.prompt) params.prompt = options.prompt;
140
+ if (options.temperature !== undefined) params.temperature = options.temperature;
141
+ if (options.timestampGranularities) {
142
+ params.timestamp_granularities = options.timestampGranularities;
143
+ }
144
+ if (options.chunkingStrategy) {
145
+ (params as OpenAI.Audio.TranscriptionCreateParams & {
146
+ chunking_strategy?: unknown;
147
+ }).chunking_strategy = options.chunkingStrategy;
148
+ }
149
+
150
+ const response = await openai.audio.transcriptions.create(params);
151
+ return mapTranscriptionResponse(response, responseFormat);
152
+ }
153
+
154
+ export async function transcribeWithWhisperOpenAI(
155
+ audioBuffer: AudioBuffer,
156
+ options: OpenAITranscriptionOptions = {},
157
+ openAIApiKey?: string,
158
+ ): Promise<string> {
159
+ const detailed = await transcribeDetailedOpenAI(
160
+ audioBuffer,
161
+ { ...options, responseFormat: options.responseFormat ?? "text" },
162
+ openAIApiKey,
163
+ );
164
+ return detailed.text;
165
+ }
166
+
167
+ export async function transcribeToSrtOpenAI(
168
+ audioBuffer: AudioBuffer,
169
+ options: Omit<OpenAITranscriptionOptions, "responseFormat"> = {},
170
+ openAIApiKey?: string,
171
+ ): Promise<string> {
172
+ const detailed = await transcribeDetailedOpenAI(
173
+ audioBuffer,
174
+ { ...options, model: options.model ?? "whisper-1", responseFormat: "srt" },
175
+ openAIApiKey,
176
+ );
177
+ return detailed.text;
178
+ }
179
+
180
+ export async function transcribeToVttOpenAI(
181
+ audioBuffer: AudioBuffer,
182
+ options: Omit<OpenAITranscriptionOptions, "responseFormat"> = {},
183
+ openAIApiKey?: string,
184
+ ): Promise<string> {
185
+ const detailed = await transcribeDetailedOpenAI(
186
+ audioBuffer,
187
+ { ...options, model: options.model ?? "whisper-1", responseFormat: "vtt" },
188
+ openAIApiKey,
189
+ );
190
+ return detailed.text;
191
+ }
192
+
193
+ export async function transcribeDiarizedOpenAI(
194
+ audioBuffer: AudioBuffer,
195
+ options: Omit<OpenAITranscriptionOptions, "model" | "responseFormat"> = {},
196
+ openAIApiKey?: string,
197
+ ): Promise<OpenAITranscriptionDetailed> {
198
+ return transcribeDetailedOpenAI(
199
+ audioBuffer,
200
+ {
201
+ ...options,
202
+ model: "gpt-4o-transcribe-diarize",
203
+ responseFormat: "diarized_json",
204
+ chunkingStrategy: options.chunkingStrategy ?? "auto",
205
+ },
206
+ openAIApiKey,
207
+ );
208
+ }
209
+
210
+ export async function translateOpenAI(
211
+ audioBuffer: AudioBuffer,
212
+ options: Omit<OpenAITranscriptionOptions, "responseFormat"> = {},
213
+ openAIApiKey?: string,
214
+ ): Promise<string> {
215
+ const openai = createOpenAIClient(openAIApiKey);
216
+ const file = await createAudioFile(audioBuffer, options.format);
217
+
218
+ const response = await openai.audio.translations.create({
219
+ file,
220
+ model: "whisper-1",
221
+ ...(options.prompt ? { prompt: options.prompt } : {}),
222
+ ...(options.temperature !== undefined
223
+ ? { temperature: options.temperature }
224
+ : {}),
225
+ });
226
+
227
+ return typeof response === "string" ? response : response.text;
228
+ }
229
+
230
+ export async function transcribeFileDetailedOpenAI(
231
+ filePath: string,
232
+ options: OpenAITranscriptionOptions = {},
233
+ openAIApiKey?: string,
234
+ ): Promise<OpenAITranscriptionDetailed> {
235
+ if (!fs.existsSync(filePath)) {
236
+ throw new Error(`Arquivo não encontrado: ${filePath}`);
237
+ }
238
+ const audioBuffer = fs.readFileSync(filePath);
239
+ const format =
240
+ options.format ?? (path.extname(filePath).replace(/^\./, "") || "mp3");
241
+ return transcribeDetailedOpenAI(
242
+ audioBuffer,
243
+ { ...options, format },
244
+ openAIApiKey,
245
+ );
246
+ }
247
+
248
+ export async function transcribeFileWithWhisperOpenAI(
249
+ filePath: string,
250
+ options: OpenAITranscriptionOptions = {},
251
+ openAIApiKey?: string,
252
+ ): Promise<string> {
253
+ const detailed = await transcribeFileDetailedOpenAI(
254
+ filePath,
255
+ options,
256
+ openAIApiKey,
257
+ );
258
+ return detailed.text;
259
+ }
@@ -0,0 +1,59 @@
1
+ import type { AudioBuffer } from "../@types/audio";
2
+ import { resolveOpenRouterProvider } from "../langchain/models";
3
+ import type {
4
+ OpenRouterTranscriptionOptions,
5
+ OpenRouterTranscriptionResult,
6
+ } from "./types";
7
+ import { bufferToBase64, getAudioExtension } from "./utils";
8
+
9
+ const OPENROUTER_TRANSCRIPTIONS_URL =
10
+ "https://openrouter.ai/api/v1/audio/transcriptions";
11
+
12
+ export async function transcribeOpenRouter(
13
+ audioBuffer: AudioBuffer,
14
+ options: OpenRouterTranscriptionOptions,
15
+ openRouterApiKey?: string,
16
+ ): Promise<OpenRouterTranscriptionResult> {
17
+ if (!openRouterApiKey) {
18
+ throw new Error(
19
+ "OpenRouter API key is not passed in the transcription parameters",
20
+ );
21
+ }
22
+
23
+ const format = getAudioExtension(options.format);
24
+ const provider = resolveOpenRouterProvider(options.model, {
25
+ openRouterProvider: options.openRouterProvider,
26
+ openRouterAllowAllProviders: options.openRouterAllowAllProviders,
27
+ });
28
+
29
+ const body: Record<string, unknown> = {
30
+ model: options.model,
31
+ input_audio: {
32
+ data: bufferToBase64(audioBuffer),
33
+ format,
34
+ },
35
+ };
36
+
37
+ if (options.language) body.language = options.language;
38
+ if (options.temperature !== undefined) body.temperature = options.temperature;
39
+ if (provider) body.provider = provider;
40
+
41
+ const response = await fetch(OPENROUTER_TRANSCRIPTIONS_URL, {
42
+ method: "POST",
43
+ headers: {
44
+ Authorization: `Bearer ${openRouterApiKey}`,
45
+ "Content-Type": "application/json",
46
+ },
47
+ body: JSON.stringify(body),
48
+ });
49
+
50
+ if (!response.ok) {
51
+ const errText = await response.text();
52
+ throw new Error(
53
+ `[@luanpoppe/ai] OpenRouter STT falhou (${response.status}): ${errText}`,
54
+ );
55
+ }
56
+
57
+ const data = (await response.json()) as OpenRouterTranscriptionResult;
58
+ return data;
59
+ }
@@ -0,0 +1,127 @@
1
+ import type { AudioMimeType } from "../@types/audio";
2
+ import type { OpenRouterProviderPreferences } from "../@types/openrouter-provider";
3
+
4
+ export type WhisperModel =
5
+ | "whisper-1"
6
+ | "gpt-4o-transcribe"
7
+ | "gpt-4o-mini-transcribe"
8
+ | "gpt-4o-mini-transcribe-2025-12-15"
9
+ | "gpt-4o-transcribe-diarize";
10
+
11
+ export type OpenAITranscriptionResponseFormat =
12
+ | "json"
13
+ | "text"
14
+ | "srt"
15
+ | "verbose_json"
16
+ | "vtt"
17
+ | "diarized_json";
18
+
19
+ export type OpenAITranscriptionOptions = {
20
+ model?: WhisperModel;
21
+ languageIn2Digits?: string;
22
+ prompt?: string;
23
+ responseFormat?: OpenAITranscriptionResponseFormat;
24
+ temperature?: number;
25
+ timestampGranularities?: ("word" | "segment")[];
26
+ format?: string | AudioMimeType;
27
+ /** Obrigatório para `gpt-4o-transcribe-diarize` em áudio > 30s */
28
+ chunkingStrategy?: "auto" | { type: "server_vad" };
29
+ };
30
+
31
+ export type TranscriptionWord = {
32
+ word: string;
33
+ start: number;
34
+ end: number;
35
+ };
36
+
37
+ export type TranscriptionSegment = {
38
+ id?: number;
39
+ start: number;
40
+ end: number;
41
+ text: string;
42
+ };
43
+
44
+ export type DiarizedSegment = {
45
+ speaker: string;
46
+ start: number;
47
+ end: number;
48
+ text: string;
49
+ };
50
+
51
+ export type OpenAITranscriptionDetailed = {
52
+ text: string;
53
+ language?: string;
54
+ duration?: number;
55
+ words?: TranscriptionWord[];
56
+ segments?: TranscriptionSegment[];
57
+ speakers?: DiarizedSegment[];
58
+ };
59
+
60
+ export type OpenRouterTranscriptionOptions = {
61
+ model: string;
62
+ format?: string | AudioMimeType;
63
+ language?: string;
64
+ temperature?: number;
65
+ openRouterProvider?: OpenRouterProviderPreferences;
66
+ /** Quando true, não aplica default de provider (ex.: deepseek only) */
67
+ openRouterAllowAllProviders?: boolean;
68
+ };
69
+
70
+ export type OpenRouterTranscriptionResult = {
71
+ text: string;
72
+ usage?: {
73
+ seconds?: number;
74
+ cost?: number;
75
+ input_tokens?: number;
76
+ output_tokens?: number;
77
+ total_tokens?: number;
78
+ };
79
+ };
80
+
81
+ export type OpenAISpeechModel =
82
+ | "tts-1"
83
+ | "tts-1-hd"
84
+ | "gpt-4o-mini-tts"
85
+ | "gpt-4o-mini-tts-2025-12-15";
86
+
87
+ export type OpenAISpeechResponseFormat =
88
+ | "mp3"
89
+ | "opus"
90
+ | "aac"
91
+ | "flac"
92
+ | "wav"
93
+ | "pcm";
94
+
95
+ export type OpenAISpeechOptions = {
96
+ model?: OpenAISpeechModel;
97
+ voice: string;
98
+ responseFormat?: OpenAISpeechResponseFormat;
99
+ speed?: number;
100
+ /** Apenas modelos gpt-4o-mini-tts* */
101
+ instructions?: string;
102
+ };
103
+
104
+ export type OpenRouterSpeechOptions = {
105
+ model: string;
106
+ voice: string;
107
+ responseFormat?: "mp3" | "pcm";
108
+ speed?: number;
109
+ instructions?: string;
110
+ openRouterProvider?: OpenRouterProviderPreferences;
111
+ openRouterAllowAllProviders?: boolean;
112
+ };
113
+
114
+ export type SpeechResult = {
115
+ audio: Buffer;
116
+ contentType: string;
117
+ };
118
+
119
+ export type GeminiTranscriptionOptions = {
120
+ model?: string;
121
+ prompt?: string;
122
+ googleGeminiToken?: string;
123
+ };
124
+
125
+ /** A API pública da Anthropic não oferece STT/TTS nativo (apenas texto/imagem/documento). */
126
+ export const ANTHROPIC_NO_NATIVE_AUDIO =
127
+ "A API da Anthropic não suporta áudio nativo. Use STT (OpenAI/OpenRouter) e depois AI.call com modelos anthropic." as const;
@@ -0,0 +1,20 @@
1
+ import type { AudioBuffer, AudioMimeType } from "../@types/audio";
2
+ import { MIME_TO_EXTENSION } from "../@types/audio";
3
+
4
+ export function getAudioExtension(format?: string | AudioMimeType): string {
5
+ if (!format) return "mp3";
6
+ if (format.startsWith("audio/")) {
7
+ return MIME_TO_EXTENSION[format as AudioMimeType] ?? "mp3";
8
+ }
9
+ return format.replace(/^\./, "");
10
+ }
11
+
12
+ export function toAudioBuffer(audioBuffer: AudioBuffer): Buffer {
13
+ if (audioBuffer instanceof Buffer) return audioBuffer;
14
+ if (audioBuffer instanceof ArrayBuffer) return Buffer.from(audioBuffer);
15
+ return Buffer.from(audioBuffer as Uint8Array);
16
+ }
17
+
18
+ export function bufferToBase64(audioBuffer: AudioBuffer): string {
19
+ return toAudioBuffer(audioBuffer).toString("base64");
20
+ }
@@ -0,0 +1,54 @@
1
+ import { GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";
2
+ import type { EmbeddingResult, GeminiEmbeddingOptions } from "./types";
3
+ import { normalizeInput } from "./utils";
4
+
5
+ export async function embedGemini(
6
+ input: string | string[],
7
+ options: GeminiEmbeddingOptions,
8
+ googleGeminiToken?: string,
9
+ ): Promise<EmbeddingResult> {
10
+ if (!googleGeminiToken) {
11
+ throw new Error(
12
+ "googleGeminiToken é obrigatório para embedGemini.",
13
+ );
14
+ }
15
+
16
+ const texts = normalizeInput(input);
17
+ const modelParams: ConstructorParameters<typeof GoogleGenerativeAIEmbeddings>[0] =
18
+ {
19
+ apiKey: googleGeminiToken,
20
+ model: options.model,
21
+ };
22
+ if (options.taskType) {
23
+ modelParams.taskType = options.taskType as NonNullable<
24
+ typeof modelParams.taskType
25
+ >;
26
+ }
27
+
28
+ const embeddingsModel = new GoogleGenerativeAIEmbeddings(modelParams);
29
+
30
+ const embeddings =
31
+ texts.length === 1
32
+ ? [await embeddingsModel.embedQuery(texts[0]!)]
33
+ : await embeddingsModel.embedDocuments(texts);
34
+
35
+ const result: EmbeddingResult = {
36
+ embeddings,
37
+ model: options.model,
38
+ };
39
+
40
+ if (options.dimensions !== undefined && options.model === "gemini-embedding-001") {
41
+ result.embeddings = result.embeddings.map((vec) =>
42
+ truncateAndNormalizeL2(vec, options.dimensions!),
43
+ );
44
+ }
45
+
46
+ return result;
47
+ }
48
+
49
+ function truncateAndNormalizeL2(vector: number[], dimensions: number): number[] {
50
+ const truncated = vector.slice(0, dimensions);
51
+ const norm = Math.sqrt(truncated.reduce((sum, v) => sum + v * v, 0));
52
+ if (norm === 0) return truncated;
53
+ return truncated.map((v) => v / norm);
54
+ }
@@ -0,0 +1,53 @@
1
+ import OpenAI from "openai";
2
+ import type { OpenAIEmbeddingOptions, EmbeddingResult } from "./types";
3
+ import { normalizeInput } from "./utils";
4
+
5
+ function createOpenAIClient(apiKey?: string): OpenAI {
6
+ if (!apiKey) {
7
+ throw new Error("OpenAI API key is not passed in the embedding parameters");
8
+ }
9
+ return new OpenAI({ apiKey });
10
+ }
11
+
12
+ function mapEmbeddingResponse(
13
+ response: OpenAI.CreateEmbeddingResponse,
14
+ model: string,
15
+ ): EmbeddingResult {
16
+ const embeddings = response.data
17
+ .sort((a, b) => a.index - b.index)
18
+ .map((item) => item.embedding as number[]);
19
+
20
+ const result: EmbeddingResult = {
21
+ embeddings,
22
+ model: response.model ?? model,
23
+ };
24
+
25
+ if (response.usage) {
26
+ result.usage = {
27
+ prompt_tokens: response.usage.prompt_tokens,
28
+ total_tokens: response.usage.total_tokens,
29
+ };
30
+ }
31
+
32
+ return result;
33
+ }
34
+
35
+ export async function embedOpenAI(
36
+ input: string | string[],
37
+ options: OpenAIEmbeddingOptions,
38
+ openAIApiKey?: string,
39
+ ): Promise<EmbeddingResult> {
40
+ const openai = createOpenAIClient(openAIApiKey);
41
+ const texts = normalizeInput(input);
42
+
43
+ const response = await openai.embeddings.create({
44
+ model: options.model,
45
+ input: texts.length === 1 ? texts[0]! : texts,
46
+ ...(options.dimensions !== undefined ? { dimensions: options.dimensions } : {}),
47
+ ...(options.encodingFormat
48
+ ? { encoding_format: options.encodingFormat }
49
+ : {}),
50
+ });
51
+
52
+ return mapEmbeddingResponse(response, options.model);
53
+ }
@@ -0,0 +1,64 @@
1
+ import type OpenAI from "openai";
2
+ import { resolveOpenRouterProvider } from "../langchain/models";
3
+ import { createOpenRouterOpenAIClient } from "../utils/openrouter-client";
4
+ import type { EmbeddingResult, OpenRouterEmbeddingOptions } from "./types";
5
+ import { normalizeInput, stripOpenRouterPrefix } from "./utils";
6
+
7
+ function mapEmbeddingResponse(
8
+ response: OpenAI.CreateEmbeddingResponse,
9
+ model: string,
10
+ ): EmbeddingResult {
11
+ const embeddings = response.data
12
+ .sort((a, b) => a.index - b.index)
13
+ .map((item) => item.embedding as number[]);
14
+
15
+ const result: EmbeddingResult = {
16
+ embeddings,
17
+ model: response.model ?? model,
18
+ };
19
+
20
+ if (response.usage) {
21
+ result.usage = {
22
+ prompt_tokens: response.usage.prompt_tokens,
23
+ total_tokens: response.usage.total_tokens,
24
+ };
25
+ }
26
+
27
+ return result;
28
+ }
29
+
30
+ export async function embedOpenRouter(
31
+ input: string | string[],
32
+ options: OpenRouterEmbeddingOptions,
33
+ openRouterApiKey?: string,
34
+ ): Promise<EmbeddingResult> {
35
+ if (!openRouterApiKey) {
36
+ throw new Error(
37
+ "OpenRouter API key is not passed in the embedding parameters",
38
+ );
39
+ }
40
+
41
+ const openRouterModel = stripOpenRouterPrefix(options.model);
42
+ const client = createOpenRouterOpenAIClient(openRouterApiKey);
43
+ const texts = normalizeInput(input);
44
+
45
+ const provider = resolveOpenRouterProvider(openRouterModel, {
46
+ openRouterProvider: options.openRouterProvider,
47
+ openRouterAllowAllProviders: options.openRouterAllowAllProviders,
48
+ });
49
+
50
+ const body: Record<string, unknown> = {
51
+ model: openRouterModel,
52
+ input: texts.length === 1 ? texts[0]! : texts,
53
+ };
54
+
55
+ if (options.dimensions !== undefined) body.dimensions = options.dimensions;
56
+ if (options.encodingFormat) body.encoding_format = options.encodingFormat;
57
+ if (provider) body.provider = provider;
58
+
59
+ const response = await client.embeddings.create(
60
+ body as unknown as OpenAI.EmbeddingCreateParams,
61
+ );
62
+
63
+ return mapEmbeddingResponse(response, openRouterModel);
64
+ }