@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,83 @@
1
+ import type { AIEmbeddingModelNames } from "../@types/embedding-model-names";
2
+ import { embedOpenAI } from "./embed-openai";
3
+ import { embedOpenRouter } from "./embed-openrouter";
4
+ import { embedGemini } from "./embed-gemini";
5
+ import {
6
+ embedWithModel,
7
+ type EmbedOptions,
8
+ } from "./route";
9
+ import type {
10
+ EmbedApiKeys,
11
+ EmbeddingResult,
12
+ GeminiEmbeddingOptions,
13
+ GeminiEmbeddingTaskType,
14
+ OpenAIEmbeddingOptions,
15
+ OpenRouterEmbeddingOptions,
16
+ } from "./types";
17
+
18
+ export type {
19
+ AIEmbeddingModelNames,
20
+ OpenAIEmbeddingModel,
21
+ GeminiEmbeddingModel,
22
+ OpenRouterEmbeddingModel,
23
+ OpenRouterPrefixedEmbeddingModel,
24
+ } from "../@types/embedding-model-names";
25
+
26
+ export type {
27
+ EmbeddingResult,
28
+ EmbeddingUsage,
29
+ OpenAIEmbeddingOptions,
30
+ OpenRouterEmbeddingOptions,
31
+ GeminiEmbeddingOptions,
32
+ GeminiEmbeddingTaskType,
33
+ EmbedApiKeys,
34
+ } from "./types";
35
+
36
+ export type { EmbedOptions } from "./route";
37
+
38
+ export { ANTHROPIC_NO_NATIVE_EMBEDDINGS } from "./types";
39
+ export { embedWithModel };
40
+
41
+ export class AIEmbeddings {
42
+ static embedOpenAI = embedOpenAI;
43
+ static embedOpenRouter = embedOpenRouter;
44
+ static embedGemini = embedGemini;
45
+
46
+ static async embed(
47
+ input: string | string[],
48
+ options: EmbedOptions,
49
+ keys: EmbedApiKeys,
50
+ ): Promise<EmbeddingResult> {
51
+ return embedWithModel(input, options, keys);
52
+ }
53
+
54
+ static async embedDocuments(
55
+ texts: string[],
56
+ options: EmbedOptions,
57
+ keys: EmbedApiKeys,
58
+ ): Promise<number[][]> {
59
+ const taskType =
60
+ options.taskType ??
61
+ (options.model.startsWith("gemini-embedding-")
62
+ ? ("RETRIEVAL_DOCUMENT" as const)
63
+ : undefined);
64
+
65
+ const result = await embedWithModel(texts, { ...options, taskType }, keys);
66
+ return result.embeddings;
67
+ }
68
+
69
+ static async embedQuery(
70
+ text: string,
71
+ options: EmbedOptions,
72
+ keys: EmbedApiKeys,
73
+ ): Promise<number[]> {
74
+ const taskType =
75
+ options.taskType ??
76
+ (options.model.startsWith("gemini-embedding-")
77
+ ? ("RETRIEVAL_QUERY" as const)
78
+ : undefined);
79
+
80
+ const result = await embedWithModel(text, { ...options, taskType }, keys);
81
+ return result.embeddings[0] ?? [];
82
+ }
83
+ }
@@ -0,0 +1,62 @@
1
+ import type {
2
+ EmbedApiKeys,
3
+ EmbeddingResult,
4
+ GeminiEmbeddingOptions,
5
+ OpenAIEmbeddingOptions,
6
+ OpenRouterEmbeddingOptions,
7
+ } from "./types";
8
+ import type { AIEmbeddingModelNames } from "../@types/embedding-model-names";
9
+ import { embedOpenAI } from "./embed-openai";
10
+ import { embedOpenRouter } from "./embed-openrouter";
11
+ import { embedGemini } from "./embed-gemini";
12
+ import {
13
+ isGeminiEmbeddingModel,
14
+ isOpenAIEmbeddingModel,
15
+ isOpenRouterEmbeddingModel,
16
+ } from "./utils";
17
+
18
+ export type EmbedOptions = {
19
+ model: AIEmbeddingModelNames;
20
+ dimensions?: number;
21
+ encodingFormat?: "float" | "base64";
22
+ taskType?: GeminiEmbeddingOptions["taskType"];
23
+ openRouterProvider?: OpenRouterEmbeddingOptions["openRouterProvider"];
24
+ openRouterAllowAllProviders?: boolean;
25
+ };
26
+
27
+ export async function embedWithModel(
28
+ input: string | string[],
29
+ options: EmbedOptions,
30
+ keys: EmbedApiKeys,
31
+ ): Promise<EmbeddingResult> {
32
+ const { model } = options;
33
+
34
+ if (isOpenRouterEmbeddingModel(model)) {
35
+ const orOptions: OpenRouterEmbeddingOptions = { model };
36
+ if (options.dimensions !== undefined) orOptions.dimensions = options.dimensions;
37
+ if (options.encodingFormat) orOptions.encodingFormat = options.encodingFormat;
38
+ if (options.openRouterProvider) {
39
+ orOptions.openRouterProvider = options.openRouterProvider;
40
+ }
41
+ if (options.openRouterAllowAllProviders !== undefined) {
42
+ orOptions.openRouterAllowAllProviders = options.openRouterAllowAllProviders;
43
+ }
44
+ return embedOpenRouter(input, orOptions, keys.openRouterApiKey);
45
+ }
46
+
47
+ if (isOpenAIEmbeddingModel(model)) {
48
+ const oaiOptions: OpenAIEmbeddingOptions = { model };
49
+ if (options.dimensions !== undefined) oaiOptions.dimensions = options.dimensions;
50
+ if (options.encodingFormat) oaiOptions.encodingFormat = options.encodingFormat;
51
+ return embedOpenAI(input, oaiOptions, keys.openAIApiKey);
52
+ }
53
+
54
+ if (isGeminiEmbeddingModel(model)) {
55
+ const geminiOptions: GeminiEmbeddingOptions = { model };
56
+ if (options.dimensions !== undefined) geminiOptions.dimensions = options.dimensions;
57
+ if (options.taskType) geminiOptions.taskType = options.taskType;
58
+ return embedGemini(input, geminiOptions, keys.googleGeminiToken);
59
+ }
60
+
61
+ throw new Error(`[@luanpoppe/ai] Modelo de embedding não suportado: ${model}`);
62
+ }
@@ -0,0 +1,53 @@
1
+ import type { OpenRouterProviderPreferences } from "../@types/openrouter-provider";
2
+ import type { AIEmbeddingModelNames } from "../@types/embedding-model-names";
3
+
4
+ export type GeminiEmbeddingTaskType =
5
+ | "RETRIEVAL_DOCUMENT"
6
+ | "RETRIEVAL_QUERY"
7
+ | "SEMANTIC_SIMILARITY"
8
+ | "CLASSIFICATION"
9
+ | "CLUSTERING"
10
+ | "QUESTION_ANSWERING"
11
+ | "FACT_VERIFICATION"
12
+ | "CODE_RETRIEVAL_QUERY";
13
+
14
+ export type EmbeddingUsage = {
15
+ prompt_tokens?: number;
16
+ total_tokens?: number;
17
+ };
18
+
19
+ export type EmbeddingResult = {
20
+ embeddings: number[][];
21
+ model: string;
22
+ usage?: EmbeddingUsage;
23
+ };
24
+
25
+ export type OpenAIEmbeddingOptions = {
26
+ model: AIEmbeddingModelNames;
27
+ dimensions?: number;
28
+ encodingFormat?: "float" | "base64";
29
+ };
30
+
31
+ export type OpenRouterEmbeddingOptions = {
32
+ model: AIEmbeddingModelNames;
33
+ dimensions?: number;
34
+ encodingFormat?: "float" | "base64";
35
+ openRouterProvider?: OpenRouterProviderPreferences;
36
+ openRouterAllowAllProviders?: boolean;
37
+ };
38
+
39
+ export type GeminiEmbeddingOptions = {
40
+ model: AIEmbeddingModelNames;
41
+ dimensions?: number;
42
+ taskType?: GeminiEmbeddingTaskType;
43
+ };
44
+
45
+ export type EmbedApiKeys = {
46
+ openAIApiKey?: string;
47
+ googleGeminiToken?: string;
48
+ openRouterApiKey?: string;
49
+ };
50
+
51
+ /** A API pública da Anthropic não oferece embeddings nativos (recomenda Voyage AI para RAG com Claude). */
52
+ export const ANTHROPIC_NO_NATIVE_EMBEDDINGS =
53
+ "A API da Anthropic não suporta embeddings nativos. Use OpenAI, Gemini ou OpenRouter para vetorizar texto antes de AI.call com modelos anthropic." as const;
@@ -0,0 +1,21 @@
1
+ export function normalizeInput(input: string | string[]): string[] {
2
+ return Array.isArray(input) ? input : [input];
3
+ }
4
+
5
+ export function stripOpenRouterPrefix(model: string): string {
6
+ return model.startsWith("openrouter/")
7
+ ? model.slice("openrouter/".length)
8
+ : model;
9
+ }
10
+
11
+ export function isOpenAIEmbeddingModel(model: string): boolean {
12
+ return model.startsWith("text-embedding-");
13
+ }
14
+
15
+ export function isGeminiEmbeddingModel(model: string): boolean {
16
+ return model.startsWith("gemini-embedding-");
17
+ }
18
+
19
+ export function isOpenRouterEmbeddingModel(model: string): boolean {
20
+ return model.startsWith("openrouter/");
21
+ }
package/src/index.ts CHANGED
@@ -15,9 +15,23 @@ export type {
15
15
  AICallStructuredOutputParams,
16
16
  AICallStructuredOutputReturn,
17
17
  } from "./@types/ai-call";
18
+ export type { AIModelNames } from "./@types/model-names";
18
19
 
19
20
  export { AIModels } from "./langchain/models";
20
- export type { LLMModelConfig, ReasoningEffort } from "./langchain/models";
21
+ export {
22
+ DEFAULT_OLLAMA_BASE_URL,
23
+ DEFAULT_LOCAL_API_KEY,
24
+ } from "./langchain/models";
25
+ export type {
26
+ LLMModelConfig,
27
+ OllamaModelConfig,
28
+ OpenAICompatibleModelConfig,
29
+ ReasoningEffort,
30
+ OpenRouterMaxPrice,
31
+ OpenRouterProviderPreferences,
32
+ OpenRouterProviderSort,
33
+ } from "./langchain/models";
34
+ export { resolveOpenRouterProvider } from "./langchain/models";
21
35
  export { AIMessages } from "./langchain/messages";
22
36
  export { AITools } from "./langchain/tools";
23
37
  export { AIMemory } from "./langchain/memory";
@@ -34,6 +48,41 @@ export type {
34
48
  HistoryMessageItem,
35
49
  GetHistoryResult,
36
50
  } from "./langchain/memory";
51
+ export { AIAudio } from "./audio";
52
+ export type {
53
+ WhisperModel,
54
+ OpenAITranscriptionOptions,
55
+ OpenAITranscriptionResponseFormat,
56
+ OpenAITranscriptionDetailed,
57
+ OpenRouterTranscriptionOptions,
58
+ OpenRouterTranscriptionResult,
59
+ OpenAISpeechOptions,
60
+ OpenAISpeechModel,
61
+ OpenRouterSpeechOptions,
62
+ SpeechResult,
63
+ TranscriptionWord,
64
+ TranscriptionSegment,
65
+ DiarizedSegment,
66
+ GeminiTranscriptionOptions,
67
+ } from "./audio";
68
+ export { ANTHROPIC_NO_NATIVE_AUDIO } from "./audio";
69
+ export { AIEmbeddings, ANTHROPIC_NO_NATIVE_EMBEDDINGS } from "./embeddings";
70
+ export type {
71
+ AIEmbeddingModelNames,
72
+ OpenAIEmbeddingModel,
73
+ GeminiEmbeddingModel,
74
+ OpenRouterEmbeddingModel,
75
+ OpenRouterPrefixedEmbeddingModel,
76
+ EmbeddingResult,
77
+ EmbeddingUsage,
78
+ OpenAIEmbeddingOptions,
79
+ OpenRouterEmbeddingOptions,
80
+ GeminiEmbeddingOptions,
81
+ GeminiEmbeddingTaskType,
82
+ EmbedApiKeys,
83
+ EmbedOptions,
84
+ } from "./embeddings";
85
+ /** @deprecated Use AIAudio instead */
37
86
  export { AIAudioTranscription } from "./langchain/audio-transcription";
38
87
  export { AudioUtils } from "./utils/audio-utils";
39
88
  export { ImageUtils } from "./utils/image-utils";
@@ -45,10 +94,8 @@ export type {
45
94
  ImageContentBlock,
46
95
  HumanMessageWithImageOptions,
47
96
  } from "./langchain/messages";
48
- export type {
49
- WhisperModel,
50
- WhisperTranscriptionOptions,
51
- } from "./langchain/audio-transcription";
97
+ /** @deprecated Use OpenAITranscriptionOptions from AIAudio */
98
+ export type { WhisperTranscriptionOptions } from "./langchain/audio-transcription";
52
99
 
53
100
  // Aliases para compatibilidade (deprecated - serão removidos em 2.0.0)
54
101
  /** @deprecated Use AI instead. Will be removed in 2.0.0 */
@@ -1,105 +1,34 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import OpenAI from "openai";
4
- import { toFile } from "openai";
5
1
  import type { AudioBuffer, AudioMimeType } from "../@types/audio";
6
- import { MIME_TO_EXTENSION } from "../@types/audio";
2
+ import { AIAudio } from "../audio";
3
+ import type {
4
+ OpenAITranscriptionOptions,
5
+ WhisperModel,
6
+ } from "../audio/types";
7
7
 
8
- /** Modelos disponíveis na API de transcrição OpenAI (Speech-to-Text) */
9
- export type WhisperModel =
10
- | "whisper-1"
11
- | "gpt-4o-transcribe"
12
- | "gpt-4o-mini-transcribe"
13
- | "gpt-4o-mini-transcribe-2025-12-15"
14
- | "gpt-4o-transcribe-diarize";
8
+ export type { WhisperModel };
15
9
 
16
- export type WhisperTranscriptionOptions = {
17
- /** Modelo de transcrição. Padrão: "whisper-1". gpt-4o-transcribe e gpt-4o-mini-transcribe têm maior qualidade. */
18
- model?: WhisperModel;
19
- languageIn2Digits?: string;
20
- prompt?: string;
21
- responseFormat?: "json" | "text" | "srt" | "verbose_json" | "vtt";
22
- temperature?: number;
23
- timestampGranularities?: ("word" | "segment")[];
24
- /** Formato do áudio: extensão ("mp3", "wav", "webm") ou MIME type ("audio/wav", "audio/webm") */
25
- format?: string | AudioMimeType;
26
- };
27
-
28
- function getExtension(format?: string | AudioMimeType): string {
29
- if (!format) return "mp3";
30
- if (format.startsWith("audio/")) {
31
- return MIME_TO_EXTENSION[format as AudioMimeType] ?? "mp3";
32
- }
33
- return format.replace(/^\./, "");
34
- }
35
-
36
- function toBuffer(audioBuffer: AudioBuffer): Buffer {
37
- if (audioBuffer instanceof Buffer) return audioBuffer;
38
- if (audioBuffer instanceof ArrayBuffer) return Buffer.from(audioBuffer);
39
- return Buffer.from(audioBuffer as Uint8Array);
40
- }
10
+ /** @deprecated Use OpenAITranscriptionOptions from AIAudio */
11
+ export type WhisperTranscriptionOptions = OpenAITranscriptionOptions;
41
12
 
13
+ /**
14
+ * @deprecated Use {@link AIAudio.transcribeWithWhisper} instead.
15
+ */
42
16
  export class AIAudioTranscription {
43
- static async transcribeWithWhisper(
17
+ static transcribeWithWhisper(
44
18
  audioBuffer: AudioBuffer,
45
19
  options: WhisperTranscriptionOptions = {},
46
- openAIApiKey?: string
20
+ openAIApiKey?: string,
47
21
  ): Promise<string> {
48
- if (openAIApiKey) {
49
- process.env.OPENAI_API_KEY = openAIApiKey;
50
- }
51
-
52
- const buffer = toBuffer(audioBuffer);
53
- const extension = getExtension(options.format);
54
- const fileName = `whisper-${Date.now()}.${extension}`;
55
-
56
- const file = await toFile(buffer, fileName);
57
-
58
- const openai = new OpenAI();
59
-
60
- const transcriptionParams: OpenAI.Audio.TranscriptionCreateParams = {
61
- file,
62
- model: options.model ?? "whisper-1",
63
- response_format: options.responseFormat ?? "text",
64
- };
65
-
66
- if (options.languageIn2Digits) {
67
- transcriptionParams.language = options.languageIn2Digits;
68
- }
69
- if (options.prompt) {
70
- transcriptionParams.prompt = options.prompt;
71
- }
72
- if (options.temperature !== undefined) {
73
- transcriptionParams.temperature = options.temperature;
74
- }
75
- if (options.timestampGranularities) {
76
- transcriptionParams.timestamp_granularities =
77
- options.timestampGranularities;
78
- }
79
-
80
- const response = await openai.audio.transcriptions.create(
81
- transcriptionParams
82
- );
83
-
84
- return typeof response === "string" ? response : response.text;
22
+ return AIAudio.transcribeWithWhisper(audioBuffer, options, openAIApiKey);
85
23
  }
86
24
 
87
- static async transcribeFileWithWhisper(
25
+ static transcribeFileWithWhisper(
88
26
  filePath: string,
89
27
  options: WhisperTranscriptionOptions = {},
90
- openAIApiKey?: string
28
+ openAIApiKey?: string,
91
29
  ): Promise<string> {
92
- if (!fs.existsSync(filePath)) {
93
- throw new Error(`Arquivo não encontrado: ${filePath}`);
94
- }
95
-
96
- const audioBuffer = fs.readFileSync(filePath);
97
- const format =
98
- options.format ?? (path.extname(filePath).replace(/^\./, "") || "mp3");
99
- return this.transcribeWithWhisper(
100
- audioBuffer,
101
- { ...options, format },
102
- openAIApiKey
103
- );
30
+ return AIAudio.transcribeFileWithWhisper(filePath, options, openAIApiKey);
104
31
  }
105
32
  }
33
+
34
+ export type { AudioBuffer, AudioMimeType };
@@ -3,7 +3,7 @@ import { AudioUtils } from "../utils/audio-utils";
3
3
  import { ImageUtils } from "../utils/image-utils";
4
4
  import type { AudioBuffer, AudioMimeType } from "../@types/audio";
5
5
  import type { ImageBuffer, ImageMimeType } from "../@types/image";
6
- import { AIAudioTranscription } from "./audio-transcription";
6
+ import { AIAudio } from "../audio";
7
7
 
8
8
  export type MessageInput = SystemMessage | HumanMessage | AIMessage;
9
9
 
@@ -39,8 +39,11 @@ export type HumanMessageWithAudioOptions = {
39
39
  filename?: string;
40
40
  };
41
41
  text?: string;
42
- provider?: "openai" | "gemini" | "auto";
42
+ provider?: "openai" | "gemini" | "openrouter" | "auto";
43
43
  openAIApiKey?: string;
44
+ openRouterApiKey?: string;
45
+ /** Modelo OpenRouter STT (ex.: openai/whisper-1) quando provider é openrouter ou auto com OR */
46
+ openRouterTranscriptionModel?: string;
44
47
  };
45
48
 
46
49
  export class AIMessages {
@@ -83,16 +86,31 @@ export class AIMessages {
83
86
  static async humanAudio(
84
87
  options: HumanMessageWithAudioOptions
85
88
  ): Promise<HumanMessage> {
86
- const { audio, text, provider = "auto", openAIApiKey } = options;
89
+ const {
90
+ audio,
91
+ text,
92
+ provider = "auto",
93
+ openAIApiKey,
94
+ openRouterApiKey,
95
+ openRouterTranscriptionModel,
96
+ } = options;
87
97
  const { buffer, mimeType, filename } = audio;
88
98
 
99
+ const resolvedProvider = this.resolveAudioProvider(
100
+ provider,
101
+ openAIApiKey,
102
+ openRouterApiKey,
103
+ );
104
+
89
105
  return this.createHumanMessageWithAudio(
90
106
  buffer,
91
107
  text,
92
108
  mimeType,
93
109
  filename,
94
- provider,
95
- openAIApiKey
110
+ resolvedProvider,
111
+ openAIApiKey,
112
+ openRouterApiKey,
113
+ openRouterTranscriptionModel,
96
114
  );
97
115
  }
98
116
 
@@ -100,22 +118,45 @@ export class AIMessages {
100
118
  return new AIMessage(message);
101
119
  }
102
120
 
121
+ private static resolveAudioProvider(
122
+ provider: "openai" | "gemini" | "openrouter" | "auto",
123
+ openAIApiKey?: string,
124
+ openRouterApiKey?: string,
125
+ ): "openai" | "gemini" | "openrouter" {
126
+ if (provider !== "auto") return provider;
127
+ if (openAIApiKey) return "openai";
128
+ if (openRouterApiKey) return "openrouter";
129
+ return "gemini";
130
+ }
131
+
103
132
  private static async createHumanMessageWithAudio(
104
133
  audioBuffer: AudioBuffer,
105
134
  text?: string,
106
135
  mimeType?: AudioMimeType,
107
136
  filename?: string,
108
- provider: "openai" | "gemini" | "auto" = "auto",
109
- openAIApiKey?: string
137
+ provider: "openai" | "gemini" | "openrouter" = "gemini",
138
+ openAIApiKey?: string,
139
+ openRouterApiKey?: string,
140
+ openRouterTranscriptionModel = "openai/whisper-1",
110
141
  ): Promise<HumanMessage> {
111
- // Para OpenAI, faz transcrição prévia automaticamente devido à limitação da API
112
- if (provider === "openai")
142
+ if (provider === "openai") {
113
143
  return await this.processAudioOpenAi(
114
144
  audioBuffer,
115
145
  mimeType,
116
146
  text,
117
- openAIApiKey
147
+ openAIApiKey,
148
+ );
149
+ }
150
+
151
+ if (provider === "openrouter") {
152
+ return await this.processAudioOpenRouter(
153
+ audioBuffer,
154
+ mimeType,
155
+ text,
156
+ openRouterApiKey,
157
+ openRouterTranscriptionModel,
118
158
  );
159
+ }
119
160
 
120
161
  // Para Gemini e outros, usa multimodal direto (trabalho síncrono, mas retorna Promise para compatibilidade)
121
162
  const base64Data = AudioUtils.bufferToBase64(audioBuffer);
@@ -168,10 +209,10 @@ export class AIMessages {
168
209
  ? { format: mimeType }
169
210
  : { language: "pt" };
170
211
 
171
- const transcribedText = await AIAudioTranscription.transcribeWithWhisper(
212
+ const transcribedText = await AIAudio.transcribeWithWhisper(
172
213
  audioBuffer,
173
214
  transcriptionOptions,
174
- openAIApiKey
215
+ openAIApiKey,
175
216
  );
176
217
 
177
218
  // Combina o texto original (se fornecido) com a transcrição
@@ -192,4 +233,35 @@ export class AIMessages {
192
233
  );
193
234
  }
194
235
  }
236
+
237
+ private static async processAudioOpenRouter(
238
+ audioBuffer: AudioBuffer,
239
+ mimeType?: AudioMimeType,
240
+ text?: string,
241
+ openRouterApiKey?: string,
242
+ openRouterTranscriptionModel = "openai/whisper-1",
243
+ ) {
244
+ if (!openRouterApiKey) {
245
+ throw new Error(
246
+ "openRouterApiKey é necessária quando provider é 'openrouter'.",
247
+ );
248
+ }
249
+
250
+ const format = mimeType ?? AudioUtils.detectAudioMimeType(audioBuffer);
251
+ const { text: transcribedText } = await AIAudio.transcribeOpenRouter(
252
+ audioBuffer,
253
+ {
254
+ model: openRouterTranscriptionModel,
255
+ format,
256
+ ...(text ? {} : { language: "pt" }),
257
+ },
258
+ openRouterApiKey,
259
+ );
260
+
261
+ const finalText = text
262
+ ? `${text}\n\nÁudio transcrito: ${transcribedText}`
263
+ : `Áudio transcrito: ${transcribedText}`;
264
+
265
+ return new HumanMessage(finalText);
266
+ }
195
267
  }