|
1 | 1 | import { Cline } from '../Cline'; |
2 | 2 | import { ClineProvider } from '../webview/ClineProvider'; |
3 | | -import { ApiConfiguration } from '../../shared/api'; |
| 3 | +import { ApiConfiguration, ModelInfo } from '../../shared/api'; |
4 | 4 | import { ApiStreamChunk } from '../../api/transform/stream'; |
| 5 | +import { Anthropic } from '@anthropic-ai/sdk'; |
5 | 6 | import * as vscode from 'vscode'; |
6 | 7 |
|
7 | 8 | // Mock all MCP-related modules |
@@ -498,6 +499,133 @@ describe('Cline', () => { |
498 | 499 | expect(passedMessage).not.toHaveProperty('ts'); |
499 | 500 | expect(passedMessage).not.toHaveProperty('extraProp'); |
500 | 501 | }); |
| 502 | + |
| 503 | + it('should handle image blocks based on model capabilities', async () => { |
| 504 | + // Create two configurations - one with image support, one without |
| 505 | + const configWithImages = { |
| 506 | + ...mockApiConfig, |
| 507 | + apiModelId: 'claude-3-sonnet' |
| 508 | + }; |
| 509 | + const configWithoutImages = { |
| 510 | + ...mockApiConfig, |
| 511 | + apiModelId: 'gpt-3.5-turbo' |
| 512 | + }; |
| 513 | + |
| 514 | + // Create test conversation history with mixed content |
| 515 | + const conversationHistory: (Anthropic.MessageParam & { ts?: number })[] = [ |
| 516 | + { |
| 517 | + role: 'user' as const, |
| 518 | + content: [ |
| 519 | + { |
| 520 | + type: 'text' as const, |
| 521 | + text: 'Here is an image' |
| 522 | + } satisfies Anthropic.TextBlockParam, |
| 523 | + { |
| 524 | + type: 'image' as const, |
| 525 | + source: { |
| 526 | + type: 'base64' as const, |
| 527 | + media_type: 'image/jpeg', |
| 528 | + data: 'base64data' |
| 529 | + } |
| 530 | + } satisfies Anthropic.ImageBlockParam |
| 531 | + ] |
| 532 | + }, |
| 533 | + { |
| 534 | + role: 'assistant' as const, |
| 535 | + content: [{ |
| 536 | + type: 'text' as const, |
| 537 | + text: 'I see the image' |
| 538 | + } satisfies Anthropic.TextBlockParam] |
| 539 | + } |
| 540 | + ]; |
| 541 | + |
| 542 | + // Test with model that supports images |
| 543 | + const clineWithImages = new Cline( |
| 544 | + mockProvider, |
| 545 | + configWithImages, |
| 546 | + undefined, |
| 547 | + false, |
| 548 | + undefined, |
| 549 | + 'test task' |
| 550 | + ); |
| 551 | + // Mock the model info to indicate image support |
| 552 | + jest.spyOn(clineWithImages.api, 'getModel').mockReturnValue({ |
| 553 | + id: 'claude-3-sonnet', |
| 554 | + info: { |
| 555 | + supportsImages: true, |
| 556 | + supportsPromptCache: true, |
| 557 | + supportsComputerUse: true, |
| 558 | + contextWindow: 200000, |
| 559 | + maxTokens: 4096, |
| 560 | + inputPrice: 0.25, |
| 561 | + outputPrice: 0.75 |
| 562 | + } as ModelInfo |
| 563 | + }); |
| 564 | + clineWithImages.apiConversationHistory = conversationHistory; |
| 565 | + |
| 566 | + // Test with model that doesn't support images |
| 567 | + const clineWithoutImages = new Cline( |
| 568 | + mockProvider, |
| 569 | + configWithoutImages, |
| 570 | + undefined, |
| 571 | + false, |
| 572 | + undefined, |
| 573 | + 'test task' |
| 574 | + ); |
| 575 | + // Mock the model info to indicate no image support |
| 576 | + jest.spyOn(clineWithoutImages.api, 'getModel').mockReturnValue({ |
| 577 | + id: 'gpt-3.5-turbo', |
| 578 | + info: { |
| 579 | + supportsImages: false, |
| 580 | + supportsPromptCache: false, |
| 581 | + supportsComputerUse: false, |
| 582 | + contextWindow: 16000, |
| 583 | + maxTokens: 2048, |
| 584 | + inputPrice: 0.1, |
| 585 | + outputPrice: 0.2 |
| 586 | + } as ModelInfo |
| 587 | + }); |
| 588 | + clineWithoutImages.apiConversationHistory = conversationHistory; |
| 589 | + |
| 590 | + // Create message spy for both instances |
| 591 | + const createMessageSpyWithImages = jest.fn(); |
| 592 | + const createMessageSpyWithoutImages = jest.fn(); |
| 593 | + const mockStream = { |
| 594 | + async *[Symbol.asyncIterator]() { |
| 595 | + yield { type: 'text', text: '' }; |
| 596 | + } |
| 597 | + } as AsyncGenerator<ApiStreamChunk>; |
| 598 | + |
| 599 | + jest.spyOn(clineWithImages.api, 'createMessage').mockImplementation((...args) => { |
| 600 | + createMessageSpyWithImages(...args); |
| 601 | + return mockStream; |
| 602 | + }); |
| 603 | + jest.spyOn(clineWithoutImages.api, 'createMessage').mockImplementation((...args) => { |
| 604 | + createMessageSpyWithoutImages(...args); |
| 605 | + return mockStream; |
| 606 | + }); |
| 607 | + |
| 608 | + // Trigger API requests for both instances |
| 609 | + await clineWithImages.recursivelyMakeClineRequests([{ type: 'text', text: 'test' }]); |
| 610 | + await clineWithoutImages.recursivelyMakeClineRequests([{ type: 'text', text: 'test' }]); |
| 611 | + |
| 612 | + // Verify model with image support preserves image blocks |
| 613 | + const callsWithImages = createMessageSpyWithImages.mock.calls; |
| 614 | + const historyWithImages = callsWithImages[0][1][0]; |
| 615 | + expect(historyWithImages.content).toHaveLength(2); |
| 616 | + expect(historyWithImages.content[0]).toEqual({ type: 'text', text: 'Here is an image' }); |
| 617 | + expect(historyWithImages.content[1]).toHaveProperty('type', 'image'); |
| 618 | + |
| 619 | + // Verify model without image support converts image blocks to text |
| 620 | + const callsWithoutImages = createMessageSpyWithoutImages.mock.calls; |
| 621 | + const historyWithoutImages = callsWithoutImages[0][1][0]; |
| 622 | + expect(historyWithoutImages.content).toHaveLength(2); |
| 623 | + expect(historyWithoutImages.content[0]).toEqual({ type: 'text', text: 'Here is an image' }); |
| 624 | + expect(historyWithoutImages.content[1]).toEqual({ |
| 625 | + type: 'text', |
| 626 | + text: '[Referenced image in conversation]' |
| 627 | + }); |
| 628 | + }); |
501 | 629 | }); |
502 | 630 | }); |
503 | 631 | }); |
0 commit comments