@luanpoppe/ai 1.1.3 → 1.1.4
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.
- package/dist/@types/image.d.ts +4 -0
- package/dist/@types/image.d.ts.map +1 -0
- package/dist/@types/image.js +10 -0
- package/dist/@types/image.js.map +1 -0
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +6 -0
- package/dist/ai.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/langchain/messages.d.ts +17 -0
- package/dist/langchain/messages.d.ts.map +1 -1
- package/dist/langchain/messages.js +21 -0
- package/dist/langchain/messages.js.map +1 -1
- package/dist/langchain/models.d.ts +4 -0
- package/dist/langchain/models.d.ts.map +1 -1
- package/dist/langchain/models.js +14 -2
- package/dist/langchain/models.js.map +1 -1
- package/dist/utils/image-utils.d.ts +8 -0
- package/dist/utils/image-utils.d.ts.map +1 -0
- package/dist/utils/image-utils.js +91 -0
- package/dist/utils/image-utils.js.map +1 -0
- package/package.json +1 -1
- package/src/@types/image.ts +14 -0
- package/src/ai.ts +11 -0
- package/src/index.ts +5 -0
- package/src/langchain/messages.ts +47 -0
- package/src/langchain/models.ts +19 -2
- package/src/utils/image-utils.ts +113 -0
- package/tests/e2e/@fixtures/imagem-teste-bateria.jpg +0 -0
- package/tests/e2e/ai-audio.test.ts +3 -3
- package/tests/e2e/ai-image.test.ts +173 -0
- package/tests/e2e/ai.test.ts +44 -4
- package/tests/unit/index.test.ts +95 -0
- package/tests/unit/langchain/image-utils.test.ts +127 -0
- package/tests/unit/langchain/messages.test.ts +78 -0
- package/tests/unit/langchain/models.test.ts +16 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { ImageBuffer, ImageMimeType } from "../@types/image";
|
|
2
|
+
|
|
3
|
+
export class ImageUtils {
|
|
4
|
+
static bufferToBase64(buffer: ImageBuffer): string {
|
|
5
|
+
if (buffer instanceof Buffer) {
|
|
6
|
+
return buffer.toString("base64");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (buffer instanceof ArrayBuffer) {
|
|
10
|
+
const uint8Array = new Uint8Array(buffer);
|
|
11
|
+
return Buffer.from(uint8Array).toString("base64");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (buffer instanceof Uint8Array) {
|
|
15
|
+
return Buffer.from(buffer).toString("base64");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
throw new Error("Tipo de buffer não suportado");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static detectMimeTypeFromExtension(filename?: string): ImageMimeType | null {
|
|
22
|
+
if (!filename) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const extension = filename.toLowerCase().split(".").pop();
|
|
27
|
+
|
|
28
|
+
const mimeTypeMap: Record<string, ImageMimeType> = {
|
|
29
|
+
jpg: "image/jpeg",
|
|
30
|
+
jpeg: "image/jpeg",
|
|
31
|
+
png: "image/png",
|
|
32
|
+
gif: "image/gif",
|
|
33
|
+
webp: "image/webp",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return extension ? mimeTypeMap[extension] ?? null : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static detectMimeTypeFromBuffer(buffer: ImageBuffer): ImageMimeType | null {
|
|
40
|
+
let bytes: Uint8Array;
|
|
41
|
+
|
|
42
|
+
if (buffer instanceof Buffer) {
|
|
43
|
+
bytes = new Uint8Array(buffer.slice(0, 16));
|
|
44
|
+
} else if (buffer instanceof ArrayBuffer) {
|
|
45
|
+
bytes = new Uint8Array(buffer.slice(0, 16));
|
|
46
|
+
} else {
|
|
47
|
+
bytes = buffer.slice(0, 16);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// JPEG: FF D8 FF
|
|
51
|
+
if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
|
|
52
|
+
return "image/jpeg";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
|
56
|
+
if (
|
|
57
|
+
bytes[0] === 0x89 &&
|
|
58
|
+
bytes[1] === 0x50 &&
|
|
59
|
+
bytes[2] === 0x4e &&
|
|
60
|
+
bytes[3] === 0x47 &&
|
|
61
|
+
bytes[4] === 0x0d &&
|
|
62
|
+
bytes[5] === 0x0a &&
|
|
63
|
+
bytes[6] === 0x1a &&
|
|
64
|
+
bytes[7] === 0x0a
|
|
65
|
+
) {
|
|
66
|
+
return "image/png";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// GIF: 47 49 46 38 (GIF8)
|
|
70
|
+
if (
|
|
71
|
+
bytes[0] === 0x47 &&
|
|
72
|
+
bytes[1] === 0x49 &&
|
|
73
|
+
bytes[2] === 0x46 &&
|
|
74
|
+
bytes[3] === 0x38
|
|
75
|
+
) {
|
|
76
|
+
return "image/gif";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// WebP: 52 49 46 46 (RIFF) ... 57 45 42 50 (WEBP)
|
|
80
|
+
if (
|
|
81
|
+
bytes[0] === 0x52 &&
|
|
82
|
+
bytes[1] === 0x49 &&
|
|
83
|
+
bytes[2] === 0x46 &&
|
|
84
|
+
bytes[3] === 0x46 &&
|
|
85
|
+
bytes[8] === 0x57 &&
|
|
86
|
+
bytes[9] === 0x45 &&
|
|
87
|
+
bytes[10] === 0x42 &&
|
|
88
|
+
bytes[11] === 0x50
|
|
89
|
+
) {
|
|
90
|
+
return "image/webp";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static detectImageMimeType(
|
|
97
|
+
buffer: ImageBuffer,
|
|
98
|
+
filename?: string
|
|
99
|
+
): ImageMimeType {
|
|
100
|
+
const mimeFromExtension =
|
|
101
|
+
ImageUtils.detectMimeTypeFromExtension(filename);
|
|
102
|
+
if (mimeFromExtension) {
|
|
103
|
+
return mimeFromExtension;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const mimeFromBuffer = ImageUtils.detectMimeTypeFromBuffer(buffer);
|
|
107
|
+
if (mimeFromBuffer) {
|
|
108
|
+
return mimeFromBuffer;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return "image/jpeg";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
Binary file
|
|
@@ -58,7 +58,7 @@ describe("AI Audio E2E Tests", () => {
|
|
|
58
58
|
}
|
|
59
59
|
);
|
|
60
60
|
|
|
61
|
-
it(
|
|
61
|
+
it.skip(
|
|
62
62
|
"deve processar áudio diretamente com Gemini (multimodal)",
|
|
63
63
|
{ timeout },
|
|
64
64
|
async () => {
|
|
@@ -132,7 +132,7 @@ describe("AI Audio E2E Tests", () => {
|
|
|
132
132
|
}
|
|
133
133
|
);
|
|
134
134
|
|
|
135
|
-
it(
|
|
135
|
+
it.skip(
|
|
136
136
|
"deve processar áudio com OpenRouter usando Gemini (multimodal)",
|
|
137
137
|
{ timeout },
|
|
138
138
|
async () => {
|
|
@@ -168,7 +168,7 @@ describe("AI Audio E2E Tests", () => {
|
|
|
168
168
|
}
|
|
169
169
|
);
|
|
170
170
|
|
|
171
|
-
it(
|
|
171
|
+
it.skip(
|
|
172
172
|
"deve processar áudio com OpenRouter usando GPT (transcrição prévia com Whisper)",
|
|
173
173
|
{ timeout },
|
|
174
174
|
async () => {
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { AI } from "../../src/index";
|
|
4
|
+
import { AIMessages } from "../../src/langchain/messages";
|
|
5
|
+
import "dotenv/config";
|
|
6
|
+
|
|
7
|
+
const IMAGE_FIXTURE_PATH = path.join(
|
|
8
|
+
process.cwd(),
|
|
9
|
+
"tests",
|
|
10
|
+
"e2e",
|
|
11
|
+
"@fixtures",
|
|
12
|
+
"imagem-teste-bateria.jpg"
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const PROMPT =
|
|
16
|
+
"Descreva brevemente o que há nesta imagem. Responda em uma frase.";
|
|
17
|
+
|
|
18
|
+
const EXPECTED_TERMS = [
|
|
19
|
+
"bateria",
|
|
20
|
+
"baterista",
|
|
21
|
+
"pratos",
|
|
22
|
+
"tambor",
|
|
23
|
+
"drum",
|
|
24
|
+
"cymbal",
|
|
25
|
+
"palco",
|
|
26
|
+
"stage",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function responseRelatesToImage(text: string): boolean {
|
|
30
|
+
const lower = text.toLowerCase();
|
|
31
|
+
return EXPECTED_TERMS.some((term) => lower.includes(term));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("AI Image E2E Tests", () => {
|
|
35
|
+
const openAIApiKey = process.env.OPENAI_API_KEY;
|
|
36
|
+
const googleGeminiToken = process.env.GOOGLE_GEMINI_TOKEN;
|
|
37
|
+
const openRouterApiKey = process.env.OPENROUTER_API_KEY;
|
|
38
|
+
|
|
39
|
+
const timeout = 180 * 1000;
|
|
40
|
+
|
|
41
|
+
describe("Suporte a imagens", () => {
|
|
42
|
+
it(
|
|
43
|
+
"deve processar imagem com OpenAI (gpt-4o)",
|
|
44
|
+
{ timeout },
|
|
45
|
+
async () => {
|
|
46
|
+
if (!openAIApiKey) {
|
|
47
|
+
console.log("OPENAI_API_KEY não está configurada");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const ai = new AI({
|
|
51
|
+
openAIApiKey: openAIApiKey!,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const imageBuffer = fs.readFileSync(IMAGE_FIXTURE_PATH);
|
|
55
|
+
const message = AIMessages.humanImage({
|
|
56
|
+
image: {
|
|
57
|
+
buffer: imageBuffer,
|
|
58
|
+
filename: "imagem-teste-bateria.jpg",
|
|
59
|
+
},
|
|
60
|
+
text: PROMPT,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const result = await ai.call({
|
|
64
|
+
aiModel: "gpt-4o",
|
|
65
|
+
messages: [message],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(result.text).toBeDefined();
|
|
69
|
+
expect(result.text.length).toBeGreaterThan(0);
|
|
70
|
+
expect(result.text).not.toContain("Model call failed");
|
|
71
|
+
expect(result.text).not.toContain("Invalid");
|
|
72
|
+
expect(responseRelatesToImage(result.text)).toBe(true);
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
it.skip(
|
|
77
|
+
"deve processar imagem com Gemini (gemini-2.5-flash)",
|
|
78
|
+
{ timeout },
|
|
79
|
+
async () => {
|
|
80
|
+
if (!googleGeminiToken) {
|
|
81
|
+
console.log("GOOGLE_GEMINI_TOKEN não está configurada");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const ai = new AI({
|
|
85
|
+
googleGeminiToken: googleGeminiToken!,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const imageBuffer = fs.readFileSync(IMAGE_FIXTURE_PATH);
|
|
89
|
+
const message = AIMessages.humanImage({
|
|
90
|
+
image: {
|
|
91
|
+
buffer: imageBuffer,
|
|
92
|
+
filename: "imagem-teste-bateria.jpg",
|
|
93
|
+
},
|
|
94
|
+
text: PROMPT,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const result = await ai.call({
|
|
98
|
+
aiModel: "gemini-2.5-flash",
|
|
99
|
+
messages: [message],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result.text).toBeDefined();
|
|
103
|
+
expect(result.text.length).toBeGreaterThan(0);
|
|
104
|
+
expect(responseRelatesToImage(result.text)).toBe(true);
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
it.skip(
|
|
109
|
+
"deve processar imagem com OpenRouter (openrouter/openai/gpt-4o)",
|
|
110
|
+
{ timeout },
|
|
111
|
+
async () => {
|
|
112
|
+
if (!openRouterApiKey) {
|
|
113
|
+
console.log("OPENROUTER_API_KEY não está configurada");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const ai = new AI({
|
|
117
|
+
openRouterApiKey: openRouterApiKey!,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const imageBuffer = fs.readFileSync(IMAGE_FIXTURE_PATH);
|
|
121
|
+
const message = AIMessages.humanImage({
|
|
122
|
+
image: {
|
|
123
|
+
buffer: imageBuffer,
|
|
124
|
+
filename: "imagem-teste-bateria.jpg",
|
|
125
|
+
},
|
|
126
|
+
text: PROMPT,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const result = await ai.call({
|
|
130
|
+
aiModel: "openrouter/openai/gpt-4o",
|
|
131
|
+
messages: [message],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result.text).toBeDefined();
|
|
135
|
+
expect(result.text.length).toBeGreaterThan(0);
|
|
136
|
+
expect(result.text).not.toContain("Model call failed");
|
|
137
|
+
expect(responseRelatesToImage(result.text)).toBe(true);
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
it.skip(
|
|
142
|
+
"deve processar imagem com OpenRouter (openrouter/google/gemini-2.5-flash)",
|
|
143
|
+
{ timeout },
|
|
144
|
+
async () => {
|
|
145
|
+
if (!openRouterApiKey) {
|
|
146
|
+
console.log("OPENROUTER_API_KEY não está configurada");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const ai = new AI({
|
|
150
|
+
openRouterApiKey: openRouterApiKey!,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const imageBuffer = fs.readFileSync(IMAGE_FIXTURE_PATH);
|
|
154
|
+
const message = AIMessages.humanImage({
|
|
155
|
+
image: {
|
|
156
|
+
buffer: imageBuffer,
|
|
157
|
+
filename: "imagem-teste-bateria.jpg",
|
|
158
|
+
},
|
|
159
|
+
text: PROMPT,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const result = await ai.call({
|
|
163
|
+
aiModel: "openrouter/google/gemini-2.5-flash",
|
|
164
|
+
messages: [message],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(result.text).toBeDefined();
|
|
168
|
+
expect(result.text.length).toBeGreaterThan(0);
|
|
169
|
+
expect(responseRelatesToImage(result.text)).toBe(true);
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
});
|
package/tests/e2e/ai.test.ts
CHANGED
|
@@ -45,7 +45,7 @@ describe("AI E2E Tests", () => {
|
|
|
45
45
|
},
|
|
46
46
|
);
|
|
47
47
|
|
|
48
|
-
it(
|
|
48
|
+
it.skip(
|
|
49
49
|
"deve fazer uma chamada real para Gemini e retornar resposta",
|
|
50
50
|
{ timeout },
|
|
51
51
|
async () => {
|
|
@@ -139,7 +139,7 @@ describe("AI E2E Tests", () => {
|
|
|
139
139
|
},
|
|
140
140
|
);
|
|
141
141
|
|
|
142
|
-
it(
|
|
142
|
+
it.skip(
|
|
143
143
|
"deve criar múltiplas mensagens e fazer chamada",
|
|
144
144
|
{ timeout },
|
|
145
145
|
async () => {
|
|
@@ -348,7 +348,7 @@ describe("AI E2E Tests", () => {
|
|
|
348
348
|
},
|
|
349
349
|
);
|
|
350
350
|
|
|
351
|
-
it(
|
|
351
|
+
it.skip(
|
|
352
352
|
"deve retornar resposta estruturada com Gemini",
|
|
353
353
|
{ timeout },
|
|
354
354
|
async () => {
|
|
@@ -484,8 +484,9 @@ describe("AI E2E Tests", () => {
|
|
|
484
484
|
AIMessages.human("Explique o que é TypeScript em uma frase curta."),
|
|
485
485
|
];
|
|
486
486
|
|
|
487
|
+
// gpt-4o suporta temperature; gpt-5-nano (reasoning) só aceita default
|
|
487
488
|
const result = await ai.call({
|
|
488
|
-
aiModel: "gpt-
|
|
489
|
+
aiModel: "gpt-4o",
|
|
489
490
|
messages,
|
|
490
491
|
modelConfig: {
|
|
491
492
|
maxTokens: 50,
|
|
@@ -499,6 +500,45 @@ describe("AI E2E Tests", () => {
|
|
|
499
500
|
},
|
|
500
501
|
);
|
|
501
502
|
|
|
503
|
+
// Níveis suportados pela API OpenAI (o1, gpt-5-nano, etc.): low, medium, high.
|
|
504
|
+
// Modelos mais recentes (gpt-5.2) podem suportar também: none, minimal, xhigh.
|
|
505
|
+
it.each([
|
|
506
|
+
{ reasoningEffort: "low" as const },
|
|
507
|
+
{ reasoningEffort: "medium" as const },
|
|
508
|
+
{ reasoningEffort: "high" as const },
|
|
509
|
+
])(
|
|
510
|
+
"deve usar reasoningEffort=$reasoningEffort em chamada real ao GPT",
|
|
511
|
+
{ timeout },
|
|
512
|
+
async ({ reasoningEffort }) => {
|
|
513
|
+
if (!openAIApiKey) {
|
|
514
|
+
console.log("OPENAI_API_KEY não está configurada");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const ai = new AI({
|
|
518
|
+
openAIApiKey: openAIApiKey!,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const messages = [
|
|
522
|
+
AIMessages.human(
|
|
523
|
+
"Responda apenas com a palavra 'OK' (modelo de reasoning).",
|
|
524
|
+
),
|
|
525
|
+
];
|
|
526
|
+
|
|
527
|
+
const result = await ai.call({
|
|
528
|
+
aiModel: "gpt-5-nano",
|
|
529
|
+
messages,
|
|
530
|
+
modelConfig: {
|
|
531
|
+
reasoningEffort,
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
expect(result.text).toBeDefined();
|
|
536
|
+
expect(result.text.length).toBeGreaterThan(0);
|
|
537
|
+
expect(result.messages).toBeDefined();
|
|
538
|
+
expect(result.messages.length).toBeGreaterThan(0);
|
|
539
|
+
},
|
|
540
|
+
);
|
|
541
|
+
|
|
502
542
|
it(
|
|
503
543
|
"deve usar maxTokens e temperature corretamente com OpenRouter",
|
|
504
544
|
{ timeout },
|
package/tests/unit/index.test.ts
CHANGED
|
@@ -293,6 +293,101 @@ describe("AI", () => {
|
|
|
293
293
|
});
|
|
294
294
|
});
|
|
295
295
|
|
|
296
|
+
it("deve passar reasoningEffort no modelConfig", async () => {
|
|
297
|
+
const mockModel = {} as any;
|
|
298
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
299
|
+
const mockResponse = {
|
|
300
|
+
messages: [{ content: "Resposta" } as any],
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
vi.mocked(AIModels.gpt).mockReturnValue(mockModel);
|
|
304
|
+
vi.mocked(createAgent).mockReturnValue({
|
|
305
|
+
invoke: vi.fn().mockResolvedValue(mockResponse),
|
|
306
|
+
} as any);
|
|
307
|
+
|
|
308
|
+
await ai.call({
|
|
309
|
+
aiModel: "gpt-5-nano",
|
|
310
|
+
messages: mockMessages,
|
|
311
|
+
modelConfig: {
|
|
312
|
+
reasoningEffort: "medium",
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(AIModels.gpt).toHaveBeenCalledWith({
|
|
317
|
+
model: "gpt-5-nano",
|
|
318
|
+
apiKey: mockTokens.openAIApiKey,
|
|
319
|
+
reasoningEffort: "medium",
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("deve exibir warning quando reasoningEffort é passado com modelo Gemini", async () => {
|
|
324
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
325
|
+
const mockModel = {} as any;
|
|
326
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
327
|
+
const mockResponse = {
|
|
328
|
+
messages: [{ content: "Resposta" } as any],
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
vi.mocked(AIModels.gemini).mockReturnValue(mockModel);
|
|
332
|
+
vi.mocked(createAgent).mockReturnValue({
|
|
333
|
+
invoke: vi.fn().mockResolvedValue(mockResponse),
|
|
334
|
+
} as any);
|
|
335
|
+
|
|
336
|
+
const aiWithGemini = new AI({
|
|
337
|
+
openAIApiKey: "test-key",
|
|
338
|
+
googleGeminiToken: "test-gemini-token",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
await aiWithGemini.call({
|
|
342
|
+
aiModel: "gemini-2.5-flash",
|
|
343
|
+
messages: mockMessages,
|
|
344
|
+
modelConfig: {
|
|
345
|
+
reasoningEffort: "high",
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
350
|
+
expect.stringContaining('O modelo "gemini-2.5-flash" não suporta reasoningEffort'),
|
|
351
|
+
);
|
|
352
|
+
expect(AIModels.gemini).toHaveBeenCalledWith(
|
|
353
|
+
expect.not.objectContaining({ reasoningEffort: expect.anything() }),
|
|
354
|
+
);
|
|
355
|
+
warnSpy.mockRestore();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("deve exibir warning quando reasoningEffort é passado com OpenRouter não-OpenAI", async () => {
|
|
359
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
360
|
+
const mockModel = {} as any;
|
|
361
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
362
|
+
const mockResponse = {
|
|
363
|
+
messages: [{ content: "Resposta" } as any],
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
vi.mocked(AIModels.openrouter).mockReturnValue(mockModel);
|
|
367
|
+
vi.mocked(createAgent).mockReturnValue({
|
|
368
|
+
invoke: vi.fn().mockResolvedValue(mockResponse),
|
|
369
|
+
} as any);
|
|
370
|
+
|
|
371
|
+
const aiWithOpenRouter = new AI({
|
|
372
|
+
openRouterApiKey: "test-openrouter-key",
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
await aiWithOpenRouter.call({
|
|
376
|
+
aiModel: "openrouter/google/gemini-2.5-flash",
|
|
377
|
+
messages: mockMessages,
|
|
378
|
+
modelConfig: {
|
|
379
|
+
reasoningEffort: "high",
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
384
|
+
expect.stringContaining(
|
|
385
|
+
'O modelo "openrouter/google/gemini-2.5-flash" não suporta reasoningEffort',
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
warnSpy.mockRestore();
|
|
389
|
+
});
|
|
390
|
+
|
|
296
391
|
it("deve lançar erro quando memory está ativo e threadId não é fornecido", async () => {
|
|
297
392
|
const aiWithMemory = new AI({
|
|
298
393
|
openAIApiKey: "test-key",
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { ImageUtils } from "../../../src/utils/image-utils";
|
|
3
|
+
|
|
4
|
+
describe("ImageUtils", () => {
|
|
5
|
+
describe("bufferToBase64", () => {
|
|
6
|
+
it("deve converter Buffer para base64", () => {
|
|
7
|
+
const buffer = Buffer.from("test data");
|
|
8
|
+
const result = ImageUtils.bufferToBase64(buffer);
|
|
9
|
+
expect(result).toBe("dGVzdCBkYXRh");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("deve converter ArrayBuffer para base64", () => {
|
|
13
|
+
const arrayBuffer = new TextEncoder().encode("test data").buffer;
|
|
14
|
+
const result = ImageUtils.bufferToBase64(arrayBuffer);
|
|
15
|
+
expect(result).toBe("dGVzdCBkYXRh");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("deve converter Uint8Array para base64", () => {
|
|
19
|
+
const uint8Array = new TextEncoder().encode("test data");
|
|
20
|
+
const result = ImageUtils.bufferToBase64(uint8Array);
|
|
21
|
+
expect(result).toBe("dGVzdCBkYXRh");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("deve lançar erro para tipo não suportado", () => {
|
|
25
|
+
expect(() => {
|
|
26
|
+
ImageUtils.bufferToBase64("invalid" as any);
|
|
27
|
+
}).toThrow("Tipo de buffer não suportado");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("detectMimeTypeFromExtension", () => {
|
|
32
|
+
it("deve detectar JPEG pela extensão", () => {
|
|
33
|
+
expect(ImageUtils.detectMimeTypeFromExtension("imagem.jpg")).toBe(
|
|
34
|
+
"image/jpeg"
|
|
35
|
+
);
|
|
36
|
+
expect(ImageUtils.detectMimeTypeFromExtension("foto.jpeg")).toBe(
|
|
37
|
+
"image/jpeg"
|
|
38
|
+
);
|
|
39
|
+
expect(ImageUtils.detectMimeTypeFromExtension("foto.JPG")).toBe(
|
|
40
|
+
"image/jpeg"
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("deve detectar PNG pela extensão", () => {
|
|
45
|
+
expect(ImageUtils.detectMimeTypeFromExtension("imagem.png")).toBe(
|
|
46
|
+
"image/png"
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("deve detectar GIF pela extensão", () => {
|
|
51
|
+
expect(ImageUtils.detectMimeTypeFromExtension("animacao.gif")).toBe(
|
|
52
|
+
"image/gif"
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("deve detectar WebP pela extensão", () => {
|
|
57
|
+
expect(ImageUtils.detectMimeTypeFromExtension("foto.webp")).toBe(
|
|
58
|
+
"image/webp"
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("deve retornar null para extensão desconhecida", () => {
|
|
63
|
+
expect(
|
|
64
|
+
ImageUtils.detectMimeTypeFromExtension("arquivo.unknown")
|
|
65
|
+
).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("deve retornar null quando filename não é fornecido", () => {
|
|
69
|
+
expect(ImageUtils.detectMimeTypeFromExtension(undefined)).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("detectMimeTypeFromBuffer", () => {
|
|
74
|
+
it("deve detectar JPEG pelos magic bytes", () => {
|
|
75
|
+
const buffer = Buffer.from([0xff, 0xd8, 0xff, 0x00, 0x00]);
|
|
76
|
+
expect(ImageUtils.detectMimeTypeFromBuffer(buffer)).toBe("image/jpeg");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("deve detectar PNG pelos magic bytes", () => {
|
|
80
|
+
const buffer = Buffer.from([
|
|
81
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00,
|
|
82
|
+
0x00, 0x00, 0x00, 0x00, 0x00,
|
|
83
|
+
]);
|
|
84
|
+
expect(ImageUtils.detectMimeTypeFromBuffer(buffer)).toBe("image/png");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("deve detectar GIF pelos magic bytes", () => {
|
|
88
|
+
const buffer = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x00, 0x00]);
|
|
89
|
+
expect(ImageUtils.detectMimeTypeFromBuffer(buffer)).toBe("image/gif");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("deve detectar WebP pelos magic bytes", () => {
|
|
93
|
+
const buffer = Buffer.from([
|
|
94
|
+
0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50,
|
|
95
|
+
]);
|
|
96
|
+
expect(ImageUtils.detectMimeTypeFromBuffer(buffer)).toBe("image/webp");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("deve retornar null para buffer desconhecido", () => {
|
|
100
|
+
const buffer = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
|
101
|
+
expect(ImageUtils.detectMimeTypeFromBuffer(buffer)).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("detectImageMimeType", () => {
|
|
106
|
+
it("deve priorizar extensão sobre magic bytes", () => {
|
|
107
|
+
const buffer = Buffer.from([0xff, 0xd8, 0xff]); // Magic bytes de JPEG
|
|
108
|
+
const result = ImageUtils.detectImageMimeType(buffer, "imagem.png");
|
|
109
|
+
expect(result).toBe("image/png");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("deve usar magic bytes quando extensão não está disponível", () => {
|
|
113
|
+
const buffer = Buffer.from([
|
|
114
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00,
|
|
115
|
+
0x00, 0x00, 0x00, 0x00, 0x00,
|
|
116
|
+
]);
|
|
117
|
+
const result = ImageUtils.detectImageMimeType(buffer);
|
|
118
|
+
expect(result).toBe("image/png");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("deve usar fallback para JPEG quando não consegue detectar", () => {
|
|
122
|
+
const buffer = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
|
123
|
+
const result = ImageUtils.detectImageMimeType(buffer);
|
|
124
|
+
expect(result).toBe("image/jpeg");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|