@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.
- package/.env.example +13 -1
- package/README.md +795 -0
- package/dist/@types/embedding-model-names.d.ts +11 -0
- package/dist/@types/embedding-model-names.d.ts.map +1 -0
- package/dist/@types/embedding-model-names.js +3 -0
- package/dist/@types/embedding-model-names.js.map +1 -0
- package/dist/@types/model-names.d.ts +21 -5
- package/dist/@types/model-names.d.ts.map +1 -1
- package/dist/@types/openrouter-provider.d.ts +25 -0
- package/dist/@types/openrouter-provider.d.ts.map +1 -0
- package/dist/@types/openrouter-provider.js +3 -0
- package/dist/@types/openrouter-provider.js.map +1 -0
- package/dist/ai.d.ts +7 -0
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +56 -5
- package/dist/ai.js.map +1 -1
- package/dist/audio/gemini-transcription.d.ts +4 -0
- package/dist/audio/gemini-transcription.d.ts.map +1 -0
- package/dist/audio/gemini-transcription.js +37 -0
- package/dist/audio/gemini-transcription.js.map +1 -0
- package/dist/audio/index.d.ts +33 -0
- package/dist/audio/index.d.ts.map +1 -0
- package/dist/audio/index.js +43 -0
- package/dist/audio/index.js.map +1 -0
- package/dist/audio/openrouter-client.d.ts +2 -0
- package/dist/audio/openrouter-client.d.ts.map +1 -0
- package/dist/audio/openrouter-client.js +7 -0
- package/dist/audio/openrouter-client.js.map +1 -0
- package/dist/audio/speech-openai.d.ts +4 -0
- package/dist/audio/speech-openai.d.ts.map +1 -0
- package/dist/audio/speech-openai.js +65 -0
- package/dist/audio/speech-openai.js.map +1 -0
- package/dist/audio/speech-openrouter.d.ts +4 -0
- package/dist/audio/speech-openrouter.d.ts.map +1 -0
- package/dist/audio/speech-openrouter.js +89 -0
- package/dist/audio/speech-openrouter.js.map +1 -0
- package/dist/audio/transcription-openai.d.ts +11 -0
- package/dist/audio/transcription-openai.d.ts.map +1 -0
- package/dist/audio/transcription-openai.js +204 -0
- package/dist/audio/transcription-openai.js.map +1 -0
- package/dist/audio/transcription-openrouter.d.ts +4 -0
- package/dist/audio/transcription-openrouter.d.ts.map +1 -0
- package/dist/audio/transcription-openrouter.js +44 -0
- package/dist/audio/transcription-openrouter.js.map +1 -0
- package/dist/audio/types.d.ts +92 -0
- package/dist/audio/types.d.ts.map +1 -0
- package/dist/audio/types.js +6 -0
- package/dist/audio/types.js.map +1 -0
- package/dist/audio/utils.d.ts +5 -0
- package/dist/audio/utils.d.ts.map +1 -0
- package/dist/audio/utils.js +25 -0
- package/dist/audio/utils.js.map +1 -0
- package/dist/embeddings/embed-gemini.d.ts +3 -0
- package/dist/embeddings/embed-gemini.d.ts.map +1 -0
- package/dist/embeddings/embed-gemini.js +38 -0
- package/dist/embeddings/embed-gemini.js.map +1 -0
- package/dist/embeddings/embed-openai.d.ts +3 -0
- package/dist/embeddings/embed-openai.d.ts.map +1 -0
- package/dist/embeddings/embed-openai.js +44 -0
- package/dist/embeddings/embed-openai.js.map +1 -0
- package/dist/embeddings/embed-openrouter.d.ts +3 -0
- package/dist/embeddings/embed-openrouter.d.ts.map +1 -0
- package/dist/embeddings/embed-openrouter.js +47 -0
- package/dist/embeddings/embed-openrouter.js.map +1 -0
- package/dist/embeddings/index.d.ts +19 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +36 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/route.d.ts +12 -0
- package/dist/embeddings/route.d.ts.map +1 -0
- package/dist/embeddings/route.js +42 -0
- package/dist/embeddings/route.js.map +1 -0
- package/dist/embeddings/types.d.ts +37 -0
- package/dist/embeddings/types.d.ts.map +1 -0
- package/dist/embeddings/types.js +6 -0
- package/dist/embeddings/types.js.map +1 -0
- package/dist/embeddings/utils.d.ts +6 -0
- package/dist/embeddings/utils.d.ts.map +1 -0
- package/dist/embeddings/utils.js +25 -0
- package/dist/embeddings/utils.js.map +1 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/langchain/audio-transcription.d.ts +8 -13
- package/dist/langchain/audio-transcription.d.ts.map +1 -1
- package/dist/langchain/audio-transcription.js +8 -92
- package/dist/langchain/audio-transcription.js.map +1 -1
- package/dist/langchain/messages.d.ts +6 -1
- package/dist/langchain/messages.d.ts.map +1 -1
- package/dist/langchain/messages.js +35 -7
- package/dist/langchain/messages.js.map +1 -1
- package/dist/langchain/models.d.ts +38 -0
- package/dist/langchain/models.d.ts.map +1 -1
- package/dist/langchain/models.js +63 -4
- package/dist/langchain/models.js.map +1 -1
- package/dist/langchain/structured-output.d.ts +9 -0
- package/dist/langchain/structured-output.d.ts.map +1 -0
- package/dist/langchain/structured-output.js +70 -0
- package/dist/langchain/structured-output.js.map +1 -0
- package/dist/langchain/tools.d.ts +1 -1
- package/dist/utils/openrouter-client.d.ts +4 -0
- package/dist/utils/openrouter-client.d.ts.map +1 -0
- package/dist/utils/openrouter-client.js +16 -0
- package/dist/utils/openrouter-client.js.map +1 -0
- package/docs/LANGCHAIN_WHISPER_WINDOWS_BUG.md +2 -0
- package/package.json +4 -4
- package/src/@types/embedding-model-names.ts +36 -0
- package/src/@types/model-names.ts +104 -6
- package/src/@types/openrouter-provider.ts +27 -0
- package/src/ai.ts +89 -10
- package/src/audio/gemini-transcription.ts +47 -0
- package/src/audio/index.ts +101 -0
- package/src/audio/openrouter-client.ts +4 -0
- package/src/audio/speech-openai.ts +79 -0
- package/src/audio/speech-openrouter.ts +121 -0
- package/src/audio/transcription-openai.ts +259 -0
- package/src/audio/transcription-openrouter.ts +59 -0
- package/src/audio/types.ts +127 -0
- package/src/audio/utils.ts +20 -0
- package/src/embeddings/embed-gemini.ts +54 -0
- package/src/embeddings/embed-openai.ts +53 -0
- package/src/embeddings/embed-openrouter.ts +64 -0
- package/src/embeddings/index.ts +83 -0
- package/src/embeddings/route.ts +62 -0
- package/src/embeddings/types.ts +53 -0
- package/src/embeddings/utils.ts +21 -0
- package/src/index.ts +52 -5
- package/src/langchain/audio-transcription.ts +19 -90
- package/src/langchain/messages.ts +84 -12
- package/src/langchain/models.ts +130 -3
- package/src/langchain/structured-output.ts +77 -0
- package/src/utils/openrouter-client.ts +10 -0
- package/tests/e2e/README.md +53 -0
- package/tests/e2e/ai-audio-multiprovider.test.ts +118 -0
- package/tests/e2e/ai-deepseek-structured-output.test.ts +133 -0
- package/tests/e2e/ai-embeddings.test.ts +87 -0
- package/tests/e2e/ai-local-models.test.ts +65 -0
- package/tests/unit/audio/index.test.ts +17 -0
- package/tests/unit/audio/speech-openai.test.ts +74 -0
- package/tests/unit/audio/speech-openrouter.test.ts +88 -0
- package/tests/unit/audio/transcription-openai.test.ts +130 -0
- package/tests/unit/audio/transcription-openrouter.test.ts +82 -0
- package/tests/unit/embeddings/embed-gemini.test.ts +52 -0
- package/tests/unit/embeddings/embed-openai.test.ts +68 -0
- package/tests/unit/embeddings/embed-openrouter.test.ts +62 -0
- package/tests/unit/embeddings/index.test.ts +55 -0
- package/tests/unit/index.test.ts +209 -0
- package/tests/unit/langchain/messages.test.ts +37 -0
- package/tests/unit/langchain/models.test.ts +223 -1
- package/tests/unit/langchain/structured-output.test.ts +49 -0
package/tests/unit/index.test.ts
CHANGED
|
@@ -20,7 +20,10 @@ vi.mock("../../src/langchain/models", () => ({
|
|
|
20
20
|
gpt: vi.fn(),
|
|
21
21
|
gemini: vi.fn(),
|
|
22
22
|
openrouter: vi.fn(),
|
|
23
|
+
ollama: vi.fn(),
|
|
24
|
+
openaiCompatible: vi.fn(),
|
|
23
25
|
},
|
|
26
|
+
DEFAULT_OLLAMA_BASE_URL: "http://127.0.0.1:11434",
|
|
24
27
|
}));
|
|
25
28
|
|
|
26
29
|
vi.mock("../../src/langchain/memory", async () => {
|
|
@@ -264,6 +267,77 @@ describe("AI", () => {
|
|
|
264
267
|
).rejects.toThrow("Model not supported");
|
|
265
268
|
});
|
|
266
269
|
|
|
270
|
+
it("deve usar modelo Ollama quando aiModel começa com 'ollama/'", async () => {
|
|
271
|
+
const mockModel = {} as any;
|
|
272
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
273
|
+
const mockResponse = {
|
|
274
|
+
messages: [{ content: "Resposta local" } as any],
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
vi.mocked(AIModels.ollama).mockReturnValue(mockModel);
|
|
278
|
+
vi.mocked(createAgent).mockReturnValue({
|
|
279
|
+
invoke: vi.fn().mockResolvedValue(mockResponse),
|
|
280
|
+
} as any);
|
|
281
|
+
|
|
282
|
+
const localAi = new AI({ ollamaBaseUrl: "http://ollama:11434" });
|
|
283
|
+
|
|
284
|
+
await localAi.call({
|
|
285
|
+
aiModel: "ollama/llama3.2",
|
|
286
|
+
messages: mockMessages,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(AIModels.ollama).toHaveBeenCalledWith({
|
|
290
|
+
model: "llama3.2",
|
|
291
|
+
baseUrl: "http://ollama:11434",
|
|
292
|
+
maxTokens: undefined,
|
|
293
|
+
temperature: undefined,
|
|
294
|
+
numCtx: undefined,
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("deve usar modelo local OpenAI-compatible quando aiModel começa com 'local/'", async () => {
|
|
299
|
+
const mockModel = {} as any;
|
|
300
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
301
|
+
const mockResponse = {
|
|
302
|
+
messages: [{ content: "LM Studio" } as any],
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
vi.mocked(AIModels.openaiCompatible).mockReturnValue(mockModel);
|
|
306
|
+
vi.mocked(createAgent).mockReturnValue({
|
|
307
|
+
invoke: vi.fn().mockResolvedValue(mockResponse),
|
|
308
|
+
} as any);
|
|
309
|
+
|
|
310
|
+
const localAi = new AI({
|
|
311
|
+
localBaseUrl: "http://localhost:1234/v1",
|
|
312
|
+
localApiKey: "test-local-key",
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await localAi.call({
|
|
316
|
+
aiModel: "local/qwen2.5-7b-instruct",
|
|
317
|
+
messages: mockMessages,
|
|
318
|
+
modelConfig: { temperature: 0.3 },
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
expect(AIModels.openaiCompatible).toHaveBeenCalledWith({
|
|
322
|
+
model: "qwen2.5-7b-instruct",
|
|
323
|
+
baseURL: "http://localhost:1234/v1",
|
|
324
|
+
apiKey: "test-local-key",
|
|
325
|
+
maxTokens: undefined,
|
|
326
|
+
temperature: 0.3,
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("deve lançar erro para local/* sem localBaseUrl", async () => {
|
|
331
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
332
|
+
|
|
333
|
+
await expect(
|
|
334
|
+
ai.call({
|
|
335
|
+
aiModel: "local/my-model",
|
|
336
|
+
messages: mockMessages,
|
|
337
|
+
}),
|
|
338
|
+
).rejects.toThrow("localBaseUrl é obrigatório");
|
|
339
|
+
});
|
|
340
|
+
|
|
267
341
|
it("deve passar modelConfig quando fornecido", async () => {
|
|
268
342
|
const mockModel = {} as any;
|
|
269
343
|
const mockMessages = [AIMessages.human("Teste")];
|
|
@@ -388,6 +462,106 @@ describe("AI", () => {
|
|
|
388
462
|
warnSpy.mockRestore();
|
|
389
463
|
});
|
|
390
464
|
|
|
465
|
+
it("deve aplicar provider only deepseek por padrão em openrouter/deepseek/*", async () => {
|
|
466
|
+
const mockModel = {} as any;
|
|
467
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
468
|
+
const mockResponse = {
|
|
469
|
+
messages: [{ content: "Resposta" } as any],
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
vi.mocked(AIModels.openrouter).mockReturnValue(mockModel);
|
|
473
|
+
vi.mocked(createAgent).mockReturnValue({
|
|
474
|
+
invoke: vi.fn().mockResolvedValue(mockResponse),
|
|
475
|
+
} as any);
|
|
476
|
+
|
|
477
|
+
const aiWithOpenRouter = new AI({
|
|
478
|
+
openRouterApiKey: "test-openrouter-key",
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
await aiWithOpenRouter.call({
|
|
482
|
+
aiModel: "openrouter/deepseek/deepseek-v3.2",
|
|
483
|
+
messages: mockMessages,
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
expect(AIModels.openrouter).toHaveBeenCalledWith(
|
|
487
|
+
expect.objectContaining({
|
|
488
|
+
model: "deepseek/deepseek-v3.2",
|
|
489
|
+
apiKey: "test-openrouter-key",
|
|
490
|
+
openRouterProvider: undefined,
|
|
491
|
+
openRouterAllowAllProviders: undefined,
|
|
492
|
+
}),
|
|
493
|
+
);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("deve repassar openRouterProvider e openRouterAllowAllProviders no modelConfig", async () => {
|
|
497
|
+
const mockModel = {} as any;
|
|
498
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
499
|
+
const mockResponse = {
|
|
500
|
+
messages: [{ content: "Resposta" } as any],
|
|
501
|
+
};
|
|
502
|
+
const openRouterProvider = {
|
|
503
|
+
only: ["deepinfra"],
|
|
504
|
+
max_price: { prompt: 1, completion: 2 },
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
vi.mocked(AIModels.openrouter).mockReturnValue(mockModel);
|
|
508
|
+
vi.mocked(createAgent).mockReturnValue({
|
|
509
|
+
invoke: vi.fn().mockResolvedValue(mockResponse),
|
|
510
|
+
} as any);
|
|
511
|
+
|
|
512
|
+
const aiWithOpenRouter = new AI({
|
|
513
|
+
openRouterApiKey: "test-openrouter-key",
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
await aiWithOpenRouter.call({
|
|
517
|
+
aiModel: "openrouter/deepseek/deepseek-r1",
|
|
518
|
+
messages: mockMessages,
|
|
519
|
+
modelConfig: {
|
|
520
|
+
openRouterProvider,
|
|
521
|
+
openRouterAllowAllProviders: false,
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
expect(AIModels.openrouter).toHaveBeenCalledWith(
|
|
526
|
+
expect.objectContaining({
|
|
527
|
+
model: "deepseek/deepseek-r1",
|
|
528
|
+
openRouterProvider,
|
|
529
|
+
openRouterAllowAllProviders: false,
|
|
530
|
+
}),
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("deve repassar openRouterAllowAllProviders true para liberar todos os providers", async () => {
|
|
535
|
+
const mockModel = {} as any;
|
|
536
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
537
|
+
const mockResponse = {
|
|
538
|
+
messages: [{ content: "Resposta" } as any],
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
vi.mocked(AIModels.openrouter).mockReturnValue(mockModel);
|
|
542
|
+
vi.mocked(createAgent).mockReturnValue({
|
|
543
|
+
invoke: vi.fn().mockResolvedValue(mockResponse),
|
|
544
|
+
} as any);
|
|
545
|
+
|
|
546
|
+
const aiWithOpenRouter = new AI({
|
|
547
|
+
openRouterApiKey: "test-openrouter-key",
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
await aiWithOpenRouter.call({
|
|
551
|
+
aiModel: "openrouter/deepseek/deepseek-v3.2",
|
|
552
|
+
messages: mockMessages,
|
|
553
|
+
modelConfig: {
|
|
554
|
+
openRouterAllowAllProviders: true,
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
expect(AIModels.openrouter).toHaveBeenCalledWith(
|
|
559
|
+
expect.objectContaining({
|
|
560
|
+
openRouterAllowAllProviders: true,
|
|
561
|
+
}),
|
|
562
|
+
);
|
|
563
|
+
});
|
|
564
|
+
|
|
391
565
|
it("deve lançar erro quando memory está ativo e threadId não é fornecido", async () => {
|
|
392
566
|
const aiWithMemory = new AI({
|
|
393
567
|
openAIApiKey: "test-key",
|
|
@@ -676,6 +850,41 @@ describe("AI", () => {
|
|
|
676
850
|
expect(result.response).toEqual(mockStructuredResponse);
|
|
677
851
|
});
|
|
678
852
|
|
|
853
|
+
it("deve usar json_object para DeepSeek sem responseFormat no agente", async () => {
|
|
854
|
+
const mockModel = {} as any;
|
|
855
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
856
|
+
const outputSchema = z.object({ sum: z.number(), product: z.number() });
|
|
857
|
+
|
|
858
|
+
vi.mocked(AIModels.openrouter).mockReturnValue(mockModel);
|
|
859
|
+
vi.mocked(createAgent).mockReturnValue({
|
|
860
|
+
invoke: vi.fn().mockResolvedValue({
|
|
861
|
+
messages: [{ content: '{"sum":11,"product":28}' }],
|
|
862
|
+
}),
|
|
863
|
+
} as any);
|
|
864
|
+
|
|
865
|
+
const aiWithOpenRouter = new AI({
|
|
866
|
+
openRouterApiKey: "test-openrouter-key",
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
const result = await aiWithOpenRouter.callStructuredOutput({
|
|
870
|
+
aiModel: "openrouter/deepseek/deepseek-v4-flash",
|
|
871
|
+
messages: mockMessages,
|
|
872
|
+
outputSchema,
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
expect(AIModels.openrouter).toHaveBeenCalledWith(
|
|
876
|
+
expect.objectContaining({
|
|
877
|
+
openRouterForceJsonObject: true,
|
|
878
|
+
}),
|
|
879
|
+
);
|
|
880
|
+
expect(createAgent).toHaveBeenCalledWith(
|
|
881
|
+
expect.objectContaining({
|
|
882
|
+
responseFormat: undefined,
|
|
883
|
+
}),
|
|
884
|
+
);
|
|
885
|
+
expect(result.response).toEqual({ sum: 11, product: 28 });
|
|
886
|
+
});
|
|
887
|
+
|
|
679
888
|
it("deve validar o schema e lançar erro se inválido", async () => {
|
|
680
889
|
const mockModel = {} as any;
|
|
681
890
|
const mockMessages = [AIMessages.human("Teste")];
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { AIMessages } from "../../../src/langchain/messages";
|
|
2
2
|
import { SystemMessage, HumanMessage, AIMessage } from "langchain";
|
|
3
3
|
|
|
4
|
+
vi.mock("../../../src/audio", () => ({
|
|
5
|
+
AIAudio: {
|
|
6
|
+
transcribeWithWhisper: vi.fn().mockResolvedValue("texto whisper"),
|
|
7
|
+
transcribeOpenRouter: vi
|
|
8
|
+
.fn()
|
|
9
|
+
.mockResolvedValue({ text: "texto openrouter" }),
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
|
|
4
13
|
// Mock do langchain
|
|
5
14
|
vi.mock("langchain", () => {
|
|
6
15
|
class MockSystemMessage {
|
|
@@ -174,6 +183,34 @@ describe("AIMessages", () => {
|
|
|
174
183
|
const callArgs = (HumanMessage as any).mock.calls[0][0];
|
|
175
184
|
expect(callArgs.content[0].mime_type).toBe("audio/mp4");
|
|
176
185
|
});
|
|
186
|
+
|
|
187
|
+
it("provider openai usa STT prévio via AIAudio", async () => {
|
|
188
|
+
const { AIAudio } = await import("../../../src/audio");
|
|
189
|
+
const audioBuffer = Buffer.from("fake audio data");
|
|
190
|
+
|
|
191
|
+
const result = await AIMessages.humanAudio({
|
|
192
|
+
audio: { buffer: audioBuffer, mimeType: "audio/mp3" },
|
|
193
|
+
provider: "openai",
|
|
194
|
+
openAIApiKey: "sk-test",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(AIAudio.transcribeWithWhisper).toHaveBeenCalled();
|
|
198
|
+
expect(result.content).toContain("texto whisper");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("provider openrouter usa transcribeOpenRouter", async () => {
|
|
202
|
+
const { AIAudio } = await import("../../../src/audio");
|
|
203
|
+
const audioBuffer = Buffer.from("fake audio data");
|
|
204
|
+
|
|
205
|
+
const result = await AIMessages.humanAudio({
|
|
206
|
+
audio: { buffer: audioBuffer, mimeType: "audio/mp3" },
|
|
207
|
+
provider: "openrouter",
|
|
208
|
+
openRouterApiKey: "or-test",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(AIAudio.transcribeOpenRouter).toHaveBeenCalled();
|
|
212
|
+
expect(result.content).toContain("texto openrouter");
|
|
213
|
+
});
|
|
177
214
|
});
|
|
178
215
|
|
|
179
216
|
describe("humanImage", () => {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AIModels,
|
|
3
|
+
DEFAULT_OLLAMA_BASE_URL,
|
|
4
|
+
LLMModelConfig,
|
|
5
|
+
resolveOpenRouterProvider,
|
|
6
|
+
} from "../../../src/langchain/models";
|
|
2
7
|
import { ChatOpenAI } from "@langchain/openai";
|
|
3
8
|
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
|
9
|
+
import { ChatOllama } from "@langchain/ollama";
|
|
4
10
|
|
|
5
11
|
// Mock das dependências
|
|
6
12
|
vi.mock("@langchain/openai", () => ({
|
|
@@ -11,6 +17,10 @@ vi.mock("@langchain/google-genai", () => ({
|
|
|
11
17
|
ChatGoogleGenerativeAI: vi.fn(),
|
|
12
18
|
}));
|
|
13
19
|
|
|
20
|
+
vi.mock("@langchain/ollama", () => ({
|
|
21
|
+
ChatOllama: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
14
24
|
describe("AIModels", () => {
|
|
15
25
|
beforeEach(() => {
|
|
16
26
|
vi.clearAllMocks();
|
|
@@ -205,4 +215,216 @@ describe("AIModels", () => {
|
|
|
205
215
|
);
|
|
206
216
|
});
|
|
207
217
|
});
|
|
218
|
+
|
|
219
|
+
describe("resolveOpenRouterProvider", () => {
|
|
220
|
+
it("deve retornar only deepseek para modelos deepseek/ sem config", () => {
|
|
221
|
+
expect(
|
|
222
|
+
resolveOpenRouterProvider("deepseek/deepseek-v3.2", {}),
|
|
223
|
+
).toEqual({ only: ["deepseek"] });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("não deve aplicar default para modelos que não são deepseek/", () => {
|
|
227
|
+
expect(
|
|
228
|
+
resolveOpenRouterProvider("openai/gpt-5-nano", {}),
|
|
229
|
+
).toBeUndefined();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("deve retornar undefined quando openRouterAllowAllProviders é true", () => {
|
|
233
|
+
expect(
|
|
234
|
+
resolveOpenRouterProvider("deepseek/deepseek-v3.2", {
|
|
235
|
+
openRouterAllowAllProviders: true,
|
|
236
|
+
}),
|
|
237
|
+
).toBeUndefined();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("deve usar openRouterProvider explícito em vez do default", () => {
|
|
241
|
+
const custom = { only: ["deepinfra"] };
|
|
242
|
+
expect(
|
|
243
|
+
resolveOpenRouterProvider("deepseek/deepseek-r1", {
|
|
244
|
+
openRouterProvider: custom,
|
|
245
|
+
}),
|
|
246
|
+
).toEqual(custom);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("openrouter", () => {
|
|
251
|
+
it("deve criar ChatOpenAI com configurações básicas", () => {
|
|
252
|
+
const config: LLMModelConfig = {
|
|
253
|
+
model: "openai/gpt-5-nano",
|
|
254
|
+
apiKey: "test-openrouter-key",
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
AIModels.openrouter(config);
|
|
258
|
+
|
|
259
|
+
expect(ChatOpenAI).toHaveBeenCalledWith({
|
|
260
|
+
model: "openai/gpt-5-nano",
|
|
261
|
+
apiKey: "test-openrouter-key",
|
|
262
|
+
configuration: {
|
|
263
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("deve aplicar provider only deepseek por padrão em modelos deepseek/", () => {
|
|
269
|
+
const config: LLMModelConfig = {
|
|
270
|
+
model: "deepseek/deepseek-v3.2",
|
|
271
|
+
apiKey: "test-openrouter-key",
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
AIModels.openrouter(config);
|
|
275
|
+
|
|
276
|
+
expect(ChatOpenAI).toHaveBeenCalledWith(
|
|
277
|
+
expect.objectContaining({
|
|
278
|
+
modelKwargs: { provider: { only: ["deepseek"] } },
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("não deve enviar provider em modelos não-deepseek", () => {
|
|
284
|
+
const config: LLMModelConfig = {
|
|
285
|
+
model: "openai/gpt-5-nano",
|
|
286
|
+
apiKey: "test-openrouter-key",
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
AIModels.openrouter(config);
|
|
290
|
+
|
|
291
|
+
expect(ChatOpenAI).toHaveBeenCalledWith(
|
|
292
|
+
expect.not.objectContaining({
|
|
293
|
+
modelKwargs: expect.anything(),
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("não deve aplicar default deepseek quando openRouterAllowAllProviders é true", () => {
|
|
299
|
+
const config: LLMModelConfig = {
|
|
300
|
+
model: "deepseek/deepseek-v3.2",
|
|
301
|
+
apiKey: "test-openrouter-key",
|
|
302
|
+
openRouterAllowAllProviders: true,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
AIModels.openrouter(config);
|
|
306
|
+
|
|
307
|
+
expect(ChatOpenAI).toHaveBeenCalledWith(
|
|
308
|
+
expect.not.objectContaining({
|
|
309
|
+
modelKwargs: expect.anything(),
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("deve enviar openRouterProvider customizado", () => {
|
|
315
|
+
const provider = {
|
|
316
|
+
only: ["deepinfra", "fireworks"],
|
|
317
|
+
max_price: { prompt: 0.5, completion: 1 },
|
|
318
|
+
sort: "price" as const,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
AIModels.openrouter({
|
|
322
|
+
model: "deepseek/deepseek-r1",
|
|
323
|
+
apiKey: "test-openrouter-key",
|
|
324
|
+
openRouterProvider: provider,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(ChatOpenAI).toHaveBeenCalledWith(
|
|
328
|
+
expect.objectContaining({
|
|
329
|
+
modelKwargs: { provider },
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("deve mesclar provider com reasoningEffort", () => {
|
|
335
|
+
AIModels.openrouter({
|
|
336
|
+
model: "deepseek/deepseek-r1",
|
|
337
|
+
apiKey: "test-openrouter-key",
|
|
338
|
+
reasoningEffort: "high",
|
|
339
|
+
openRouterProvider: { only: ["deepseek"] },
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(ChatOpenAI).toHaveBeenCalledWith(
|
|
343
|
+
expect.objectContaining({
|
|
344
|
+
modelKwargs: {
|
|
345
|
+
reasoning_effort: "high",
|
|
346
|
+
provider: { only: ["deepseek"] },
|
|
347
|
+
},
|
|
348
|
+
}),
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("deve lançar erro quando apiKey não é fornecida", () => {
|
|
353
|
+
expect(() =>
|
|
354
|
+
AIModels.openrouter({ model: "openai/gpt-5-nano" }),
|
|
355
|
+
).toThrow("OpenRouter API key is not passed in the model parameters");
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("ollama", () => {
|
|
360
|
+
it("deve criar ChatOllama com configurações básicas e baseUrl padrão", () => {
|
|
361
|
+
AIModels.ollama({ model: "llama3.2" });
|
|
362
|
+
|
|
363
|
+
expect(ChatOllama).toHaveBeenCalledWith({
|
|
364
|
+
model: "llama3.2",
|
|
365
|
+
baseUrl: DEFAULT_OLLAMA_BASE_URL,
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("deve criar ChatOllama com baseUrl, temperature e numCtx customizados", () => {
|
|
370
|
+
AIModels.ollama({
|
|
371
|
+
model: "qwen2.5:7b",
|
|
372
|
+
baseUrl: "http://custom:11434",
|
|
373
|
+
temperature: 0.2,
|
|
374
|
+
numCtx: 8192,
|
|
375
|
+
maxTokens: 512,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
expect(ChatOllama).toHaveBeenCalledWith({
|
|
379
|
+
model: "qwen2.5:7b",
|
|
380
|
+
baseUrl: "http://custom:11434",
|
|
381
|
+
temperature: 0.2,
|
|
382
|
+
numCtx: 8192,
|
|
383
|
+
numPredict: 512,
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe("openaiCompatible", () => {
|
|
389
|
+
it("deve criar ChatOpenAI com baseURL local", () => {
|
|
390
|
+
AIModels.openaiCompatible({
|
|
391
|
+
model: "meta-llama/Llama-3.2-3B-Instruct",
|
|
392
|
+
baseURL: "http://localhost:1234/v1",
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
expect(ChatOpenAI).toHaveBeenCalledWith({
|
|
396
|
+
model: "meta-llama/Llama-3.2-3B-Instruct",
|
|
397
|
+
apiKey: "not-needed",
|
|
398
|
+
configuration: {
|
|
399
|
+
baseURL: "http://localhost:1234/v1",
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("deve aceitar apiKey e parâmetros opcionais", () => {
|
|
405
|
+
AIModels.openaiCompatible({
|
|
406
|
+
model: "my-model",
|
|
407
|
+
baseURL: "http://127.0.0.1:11434/v1",
|
|
408
|
+
apiKey: "lm-studio",
|
|
409
|
+
maxTokens: 1000,
|
|
410
|
+
temperature: 0.5,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
expect(ChatOpenAI).toHaveBeenCalledWith({
|
|
414
|
+
model: "my-model",
|
|
415
|
+
apiKey: "lm-studio",
|
|
416
|
+
maxTokens: 1000,
|
|
417
|
+
temperature: 0.5,
|
|
418
|
+
configuration: {
|
|
419
|
+
baseURL: "http://127.0.0.1:11434/v1",
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("deve lançar erro quando baseURL não é fornecida", () => {
|
|
425
|
+
expect(() =>
|
|
426
|
+
AIModels.openaiCompatible({ model: "x", baseURL: "" }),
|
|
427
|
+
).toThrow("baseURL é obrigatório");
|
|
428
|
+
});
|
|
429
|
+
});
|
|
208
430
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import {
|
|
4
|
+
buildJsonObjectSystemAppend,
|
|
5
|
+
isDeepSeekJsonObjectOnlyModel,
|
|
6
|
+
parseJsonFromAssistantMessages,
|
|
7
|
+
} from "../../../src/langchain/structured-output";
|
|
8
|
+
|
|
9
|
+
describe("structured-output", () => {
|
|
10
|
+
describe("isDeepSeekJsonObjectOnlyModel", () => {
|
|
11
|
+
it("deve detectar openrouter/deepseek/*", () => {
|
|
12
|
+
expect(
|
|
13
|
+
isDeepSeekJsonObjectOnlyModel("openrouter/deepseek/deepseek-v4-pro"),
|
|
14
|
+
).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("não deve detectar openrouter/openai/*", () => {
|
|
18
|
+
expect(
|
|
19
|
+
isDeepSeekJsonObjectOnlyModel("openrouter/openai/gpt-5-nano"),
|
|
20
|
+
).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("buildJsonObjectSystemAppend", () => {
|
|
25
|
+
it("deve incluir a palavra json e o schema", () => {
|
|
26
|
+
const append = buildJsonObjectSystemAppend(
|
|
27
|
+
z.object({ name: z.string() }),
|
|
28
|
+
);
|
|
29
|
+
expect(append.toLowerCase()).toContain("json");
|
|
30
|
+
expect(append).toContain("name");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("parseJsonFromAssistantMessages", () => {
|
|
35
|
+
it("deve parsear JSON direto", () => {
|
|
36
|
+
const result = parseJsonFromAssistantMessages([
|
|
37
|
+
{ content: '{"a":1}' } as any,
|
|
38
|
+
]);
|
|
39
|
+
expect(result).toEqual({ a: 1 });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("deve parsear JSON em fence markdown", () => {
|
|
43
|
+
const result = parseJsonFromAssistantMessages([
|
|
44
|
+
{ content: '```json\n{"b":2}\n```' } as any,
|
|
45
|
+
]);
|
|
46
|
+
expect(result).toEqual({ b: 2 });
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|