/**
 * @license
 * Copyright 2025 Qwen
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { GenerateContentResponse, Type, FinishReason } from '@google/genai';
import { ContentGenerationPipeline } from './pipeline.js';
import { OpenAIContentConverter } from './converter.js';
// Mock dependencies
vi.mock('./converter.js');
vi.mock('openai');
describe('ContentGenerationPipeline', () => {
    let pipeline;
    let mockConfig;
    let mockProvider;
    let mockClient;
    let mockConverter;
    let mockTelemetryService;
    let mockErrorHandler;
    let mockContentGeneratorConfig;
    let mockCliConfig;
    beforeEach(() => {
        // Reset all mocks
        vi.clearAllMocks();
        // Mock OpenAI client
        mockClient = {
            chat: {
                completions: {
                    create: vi.fn(),
                },
            },
        };
        // Mock converter
        mockConverter = {
            convertGeminiRequestToOpenAI: vi.fn(),
            convertOpenAIResponseToGemini: vi.fn(),
            convertOpenAIChunkToGemini: vi.fn(),
            convertGeminiToolsToOpenAI: vi.fn(),
            resetStreamingToolCalls: vi.fn(),
        };
        // Mock provider
        mockProvider = {
            buildClient: vi.fn().mockReturnValue(mockClient),
            buildRequest: vi.fn().mockImplementation((req) => req),
            buildHeaders: vi.fn().mockReturnValue({}),
        };
        // Mock telemetry service
        mockTelemetryService = {
            logSuccess: vi.fn().mockResolvedValue(undefined),
            logError: vi.fn().mockResolvedValue(undefined),
            logStreamingSuccess: vi.fn().mockResolvedValue(undefined),
        };
        // Mock error handler
        mockErrorHandler = {
            handle: vi.fn().mockImplementation((error) => {
                throw error;
            }),
            shouldSuppressErrorLogging: vi.fn().mockReturnValue(false),
        };
        // Mock configs
        mockCliConfig = {};
        mockContentGeneratorConfig = {
            model: 'test-model',
            authType: 'openai',
            samplingParams: {
                temperature: 0.7,
                top_p: 0.9,
                max_tokens: 1000,
            },
        };
        // Mock the OpenAIContentConverter constructor
        OpenAIContentConverter.mockImplementation(() => mockConverter);
        mockConfig = {
            cliConfig: mockCliConfig,
            provider: mockProvider,
            contentGeneratorConfig: mockContentGeneratorConfig,
            telemetryService: mockTelemetryService,
            errorHandler: mockErrorHandler,
        };
        pipeline = new ContentGenerationPipeline(mockConfig);
    });
    describe('constructor', () => {
        it('should initialize with correct configuration', () => {
            expect(mockProvider.buildClient).toHaveBeenCalled();
            expect(OpenAIContentConverter).toHaveBeenCalledWith('test-model');
        });
    });
    describe('execute', () => {
        it('should successfully execute non-streaming request', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
            };
            const userPromptId = 'test-prompt-id';
            const mockMessages = [
                { role: 'user', content: 'Hello' },
            ];
            const mockOpenAIResponse = {
                id: 'response-id',
                choices: [
                    { message: { content: 'Hello response' }, finish_reason: 'stop' },
                ],
                created: Date.now(),
                model: 'test-model',
                usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
            };
            const mockGeminiResponse = new GenerateContentResponse();
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue(mockMessages);
            mockConverter.convertOpenAIResponseToGemini.mockReturnValue(mockGeminiResponse);
            mockClient.chat.completions.create.mockResolvedValue(mockOpenAIResponse);
            // Act
            const result = await pipeline.execute(request, userPromptId);
            // Assert
            expect(result).toBe(mockGeminiResponse);
            expect(mockConverter.convertGeminiRequestToOpenAI).toHaveBeenCalledWith(request);
            expect(mockClient.chat.completions.create).toHaveBeenCalledWith(expect.objectContaining({
                model: 'test-model',
                messages: mockMessages,
                temperature: 0.7,
                top_p: 0.9,
                max_tokens: 1000,
            }), expect.objectContaining({
                signal: undefined,
            }));
            expect(mockConverter.convertOpenAIResponseToGemini).toHaveBeenCalledWith(mockOpenAIResponse);
            expect(mockTelemetryService.logSuccess).toHaveBeenCalledWith(expect.objectContaining({
                userPromptId,
                model: 'test-model',
                authType: 'openai',
                isStreaming: false,
            }), mockGeminiResponse, expect.any(Object), mockOpenAIResponse);
        });
        it('should handle tools in request', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
                config: {
                    tools: [
                        {
                            functionDeclarations: [
                                {
                                    name: 'test-function',
                                    description: 'Test function',
                                    parameters: { type: Type.OBJECT, properties: {} },
                                },
                            ],
                        },
                    ],
                },
            };
            const userPromptId = 'test-prompt-id';
            const mockMessages = [
                { role: 'user', content: 'Hello' },
            ];
            const mockTools = [
                { type: 'function', function: { name: 'test-function' } },
            ];
            const mockOpenAIResponse = {
                id: 'response-id',
                choices: [
                    { message: { content: 'Hello response' }, finish_reason: 'stop' },
                ],
            };
            const mockGeminiResponse = new GenerateContentResponse();
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue(mockMessages);
            mockConverter.convertGeminiToolsToOpenAI.mockResolvedValue(mockTools);
            mockConverter.convertOpenAIResponseToGemini.mockReturnValue(mockGeminiResponse);
            mockClient.chat.completions.create.mockResolvedValue(mockOpenAIResponse);
            // Act
            const result = await pipeline.execute(request, userPromptId);
            // Assert
            expect(result).toBe(mockGeminiResponse);
            expect(mockConverter.convertGeminiToolsToOpenAI).toHaveBeenCalledWith(request.config.tools);
            expect(mockClient.chat.completions.create).toHaveBeenCalledWith(expect.objectContaining({
                tools: mockTools,
            }), expect.objectContaining({
                signal: undefined,
            }));
        });
        it('should handle errors and log them', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
            };
            const userPromptId = 'test-prompt-id';
            const testError = new Error('API Error');
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue([]);
            mockClient.chat.completions.create.mockRejectedValue(testError);
            // Act & Assert
            await expect(pipeline.execute(request, userPromptId)).rejects.toThrow('API Error');
            expect(mockTelemetryService.logError).toHaveBeenCalledWith(expect.objectContaining({
                userPromptId,
                model: 'test-model',
                authType: 'openai',
                isStreaming: false,
            }), testError, expect.any(Object));
            expect(mockErrorHandler.handle).toHaveBeenCalledWith(testError, expect.any(Object), request);
        });
        it('should pass abort signal to OpenAI client when provided', async () => {
            const abortController = new AbortController();
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
                config: { abortSignal: abortController.signal },
            };
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue([]);
            mockConverter.convertOpenAIResponseToGemini.mockReturnValue(new GenerateContentResponse());
            mockClient.chat.completions.create.mockResolvedValue({
                choices: [{ message: { content: 'response' } }],
            });
            await pipeline.execute(request, 'test-id');
            expect(mockClient.chat.completions.create).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ signal: abortController.signal }));
        });
    });
    describe('executeStream', () => {
        it('should successfully execute streaming request', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
            };
            const userPromptId = 'test-prompt-id';
            const mockChunk1 = {
                id: 'chunk-1',
                choices: [{ delta: { content: 'Hello' }, finish_reason: null }],
            };
            const mockChunk2 = {
                id: 'chunk-2',
                choices: [{ delta: { content: ' response' }, finish_reason: 'stop' }],
            };
            const mockStream = {
                async *[Symbol.asyncIterator]() {
                    yield mockChunk1;
                    yield mockChunk2;
                },
            };
            const mockGeminiResponse1 = new GenerateContentResponse();
            const mockGeminiResponse2 = new GenerateContentResponse();
            mockGeminiResponse1.candidates = [
                { content: { parts: [{ text: 'Hello' }], role: 'model' } },
            ];
            mockGeminiResponse2.candidates = [
                { content: { parts: [{ text: ' response' }], role: 'model' } },
            ];
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue([]);
            mockConverter.convertOpenAIChunkToGemini
                .mockReturnValueOnce(mockGeminiResponse1)
                .mockReturnValueOnce(mockGeminiResponse2);
            mockClient.chat.completions.create.mockResolvedValue(mockStream);
            // Act
            const resultGenerator = await pipeline.executeStream(request, userPromptId);
            const results = [];
            for await (const result of resultGenerator) {
                results.push(result);
            }
            // Assert
            expect(results).toHaveLength(2);
            expect(results[0]).toBe(mockGeminiResponse1);
            expect(results[1]).toBe(mockGeminiResponse2);
            expect(mockConverter.resetStreamingToolCalls).toHaveBeenCalled();
            expect(mockClient.chat.completions.create).toHaveBeenCalledWith(expect.objectContaining({
                stream: true,
                stream_options: { include_usage: true },
            }), expect.objectContaining({
                signal: undefined,
            }));
            expect(mockTelemetryService.logStreamingSuccess).toHaveBeenCalledWith(expect.objectContaining({
                userPromptId,
                model: 'test-model',
                authType: 'openai',
                isStreaming: true,
            }), [mockGeminiResponse1, mockGeminiResponse2], expect.any(Object), [mockChunk1, mockChunk2]);
        });
        it('should filter empty responses', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
            };
            const userPromptId = 'test-prompt-id';
            const mockChunk1 = {
                id: 'chunk-1',
                choices: [{ delta: { content: '' }, finish_reason: null }],
            };
            const mockChunk2 = {
                id: 'chunk-2',
                choices: [
                    { delta: { content: 'Hello response' }, finish_reason: 'stop' },
                ],
            };
            const mockStream = {
                async *[Symbol.asyncIterator]() {
                    yield mockChunk1;
                    yield mockChunk2;
                },
            };
            const mockEmptyResponse = new GenerateContentResponse();
            mockEmptyResponse.candidates = [
                { content: { parts: [], role: 'model' } },
            ];
            const mockValidResponse = new GenerateContentResponse();
            mockValidResponse.candidates = [
                { content: { parts: [{ text: 'Hello response' }], role: 'model' } },
            ];
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue([]);
            mockConverter.convertOpenAIChunkToGemini
                .mockReturnValueOnce(mockEmptyResponse)
                .mockReturnValueOnce(mockValidResponse);
            mockClient.chat.completions.create.mockResolvedValue(mockStream);
            // Act
            const resultGenerator = await pipeline.executeStream(request, userPromptId);
            const results = [];
            for await (const result of resultGenerator) {
                results.push(result);
            }
            // Assert
            expect(results).toHaveLength(1); // Empty response should be filtered out
            expect(results[0]).toBe(mockValidResponse);
        });
        it('should handle streaming errors and reset tool calls', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
            };
            const userPromptId = 'test-prompt-id';
            const testError = new Error('Stream Error');
            const mockStream = {
                /* eslint-disable-next-line */
                async *[Symbol.asyncIterator]() {
                    throw testError;
                },
            };
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue([]);
            mockClient.chat.completions.create.mockResolvedValue(mockStream);
            // Act
            const resultGenerator = await pipeline.executeStream(request, userPromptId);
            // Assert
            // The stream should handle the error internally - errors during iteration don't propagate to the consumer
            // Instead, they are handled internally by the pipeline
            const results = [];
            try {
                for await (const result of resultGenerator) {
                    results.push(result);
                }
            }
            catch (error) {
                // This is expected - the error should propagate from the stream processing
                expect(error).toBe(testError);
            }
            expect(results).toHaveLength(0); // No results due to error
            expect(mockConverter.resetStreamingToolCalls).toHaveBeenCalledTimes(2); // Once at start, once on error
            expect(mockTelemetryService.logError).toHaveBeenCalledWith(expect.objectContaining({
                userPromptId,
                model: 'test-model',
                authType: 'openai',
                isStreaming: true,
            }), testError, expect.any(Object));
            expect(mockErrorHandler.handle).toHaveBeenCalledWith(testError, expect.any(Object), request);
        });
        it('should pass abort signal to OpenAI client for streaming requests', async () => {
            const abortController = new AbortController();
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
                config: { abortSignal: abortController.signal },
            };
            const mockStream = {
                async *[Symbol.asyncIterator]() {
                    yield {
                        id: 'chunk-1',
                        choices: [{ delta: { content: 'Hello' }, finish_reason: 'stop' }],
                    };
                },
            };
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue([]);
            mockConverter.convertOpenAIChunkToGemini.mockReturnValue(new GenerateContentResponse());
            mockClient.chat.completions.create.mockResolvedValue(mockStream);
            const resultGenerator = await pipeline.executeStream(request, 'test-id');
            for await (const _result of resultGenerator) {
                // Consume stream
            }
            expect(mockClient.chat.completions.create).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ signal: abortController.signal }));
        });
        it('should merge finishReason and usageMetadata from separate chunks', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
            };
            const userPromptId = 'test-prompt-id';
            // Content chunk
            const mockChunk1 = {
                id: 'chunk-1',
                choices: [
                    { delta: { content: 'Hello response' }, finish_reason: null },
                ],
            };
            // Finish reason chunk (empty content, has finish_reason)
            const mockChunk2 = {
                id: 'chunk-2',
                choices: [{ delta: { content: '' }, finish_reason: 'stop' }],
            };
            // Usage metadata chunk (empty candidates, has usage)
            const mockChunk3 = {
                id: 'chunk-3',
                object: 'chat.completion.chunk',
                created: Date.now(),
                model: 'test-model',
                choices: [],
                usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
            };
            const mockStream = {
                async *[Symbol.asyncIterator]() {
                    yield mockChunk1;
                    yield mockChunk2;
                    yield mockChunk3;
                },
            };
            // Mock converter responses
            const mockContentResponse = new GenerateContentResponse();
            mockContentResponse.candidates = [
                { content: { parts: [{ text: 'Hello response' }], role: 'model' } },
            ];
            const mockFinishResponse = new GenerateContentResponse();
            mockFinishResponse.candidates = [
                {
                    content: { parts: [], role: 'model' },
                    finishReason: FinishReason.STOP,
                },
            ];
            const mockUsageResponse = new GenerateContentResponse();
            mockUsageResponse.candidates = [];
            mockUsageResponse.usageMetadata = {
                promptTokenCount: 10,
                candidatesTokenCount: 20,
                totalTokenCount: 30,
            };
            // Expected merged response (finishReason + usageMetadata combined)
            const mockMergedResponse = new GenerateContentResponse();
            mockMergedResponse.candidates = [
                {
                    content: { parts: [], role: 'model' },
                    finishReason: FinishReason.STOP,
                },
            ];
            mockMergedResponse.usageMetadata = {
                promptTokenCount: 10,
                candidatesTokenCount: 20,
                totalTokenCount: 30,
            };
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue([]);
            mockConverter.convertOpenAIChunkToGemini
                .mockReturnValueOnce(mockContentResponse)
                .mockReturnValueOnce(mockFinishResponse)
                .mockReturnValueOnce(mockUsageResponse);
            mockClient.chat.completions.create.mockResolvedValue(mockStream);
            // Act
            const resultGenerator = await pipeline.executeStream(request, userPromptId);
            const results = [];
            for await (const result of resultGenerator) {
                results.push(result);
            }
            // Assert
            expect(results).toHaveLength(2); // Content chunk + merged finish/usage chunk
            expect(results[0]).toBe(mockContentResponse);
            // The last result should have both finishReason and usageMetadata
            const lastResult = results[1];
            expect(lastResult.candidates?.[0]?.finishReason).toBe(FinishReason.STOP);
            expect(lastResult.usageMetadata).toEqual({
                promptTokenCount: 10,
                candidatesTokenCount: 20,
                totalTokenCount: 30,
            });
            expect(mockTelemetryService.logStreamingSuccess).toHaveBeenCalledWith(expect.objectContaining({
                userPromptId,
                model: 'test-model',
                authType: 'openai',
                isStreaming: true,
            }), results, expect.any(Object), [mockChunk1, mockChunk2, mockChunk3]);
        });
        it('should handle ideal case where last chunk has both finishReason and usageMetadata', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
            };
            const userPromptId = 'test-prompt-id';
            // Content chunk
            const mockChunk1 = {
                id: 'chunk-1',
                choices: [
                    { delta: { content: 'Hello response' }, finish_reason: null },
                ],
            };
            // Final chunk with both finish_reason and usage (ideal case)
            const mockChunk2 = {
                id: 'chunk-2',
                choices: [{ delta: { content: '' }, finish_reason: 'stop' }],
                usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
            };
            const mockStream = {
                async *[Symbol.asyncIterator]() {
                    yield mockChunk1;
                    yield mockChunk2;
                },
            };
            // Mock converter responses
            const mockContentResponse = new GenerateContentResponse();
            mockContentResponse.candidates = [
                { content: { parts: [{ text: 'Hello response' }], role: 'model' } },
            ];
            const mockFinalResponse = new GenerateContentResponse();
            mockFinalResponse.candidates = [
                {
                    content: { parts: [], role: 'model' },
                    finishReason: FinishReason.STOP,
                },
            ];
            mockFinalResponse.usageMetadata = {
                promptTokenCount: 10,
                candidatesTokenCount: 20,
                totalTokenCount: 30,
            };
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue([]);
            mockConverter.convertOpenAIChunkToGemini
                .mockReturnValueOnce(mockContentResponse)
                .mockReturnValueOnce(mockFinalResponse);
            mockClient.chat.completions.create.mockResolvedValue(mockStream);
            // Act
            const resultGenerator = await pipeline.executeStream(request, userPromptId);
            const results = [];
            for await (const result of resultGenerator) {
                results.push(result);
            }
            // Assert
            expect(results).toHaveLength(2);
            expect(results[0]).toBe(mockContentResponse);
            expect(results[1]).toBe(mockFinalResponse);
            // The last result should have both finishReason and usageMetadata
            const lastResult = results[1];
            expect(lastResult.candidates?.[0]?.finishReason).toBe(FinishReason.STOP);
            expect(lastResult.usageMetadata).toEqual({
                promptTokenCount: 10,
                candidatesTokenCount: 20,
                totalTokenCount: 30,
            });
        });
        it('should handle providers that send zero usage in finish chunk (like modelscope)', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
            };
            const userPromptId = 'test-prompt-id';
            // Content chunk with zero usage (typical for modelscope)
            const mockChunk1 = {
                id: 'chunk-1',
                choices: [
                    { delta: { content: 'Hello response' }, finish_reason: null },
                ],
                usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
            };
            // Finish chunk with zero usage (has finishReason but usage is all zeros)
            const mockChunk2 = {
                id: 'chunk-2',
                choices: [{ delta: { content: '' }, finish_reason: 'stop' }],
                usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
            };
            // Final usage chunk with actual usage data
            const mockChunk3 = {
                id: 'chunk-3',
                object: 'chat.completion.chunk',
                created: Date.now(),
                model: 'test-model',
                choices: [],
                usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
            };
            const mockStream = {
                async *[Symbol.asyncIterator]() {
                    yield mockChunk1;
                    yield mockChunk2;
                    yield mockChunk3;
                },
            };
            // Mock converter responses
            const mockContentResponse = new GenerateContentResponse();
            mockContentResponse.candidates = [
                { content: { parts: [{ text: 'Hello response' }], role: 'model' } },
            ];
            // Content chunk has zero usage metadata (should be filtered or ignored)
            mockContentResponse.usageMetadata = {
                promptTokenCount: 0,
                candidatesTokenCount: 0,
                totalTokenCount: 0,
            };
            const mockFinishResponseWithZeroUsage = new GenerateContentResponse();
            mockFinishResponseWithZeroUsage.candidates = [
                {
                    content: { parts: [], role: 'model' },
                    finishReason: FinishReason.STOP,
                },
            ];
            // Finish chunk has zero usage metadata (should be treated as no usage)
            mockFinishResponseWithZeroUsage.usageMetadata = {
                promptTokenCount: 0,
                candidatesTokenCount: 0,
                totalTokenCount: 0,
            };
            const mockUsageResponse = new GenerateContentResponse();
            mockUsageResponse.candidates = [];
            mockUsageResponse.usageMetadata = {
                promptTokenCount: 10,
                candidatesTokenCount: 20,
                totalTokenCount: 30,
            };
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue([]);
            mockConverter.convertOpenAIChunkToGemini
                .mockReturnValueOnce(mockContentResponse)
                .mockReturnValueOnce(mockFinishResponseWithZeroUsage)
                .mockReturnValueOnce(mockUsageResponse);
            mockClient.chat.completions.create.mockResolvedValue(mockStream);
            // Act
            const resultGenerator = await pipeline.executeStream(request, userPromptId);
            const results = [];
            for await (const result of resultGenerator) {
                results.push(result);
            }
            // Assert
            expect(results).toHaveLength(2); // Content chunk + merged finish/usage chunk
            expect(results[0]).toBe(mockContentResponse);
            // The last result should have both finishReason and valid usageMetadata
            const lastResult = results[1];
            expect(lastResult.candidates?.[0]?.finishReason).toBe(FinishReason.STOP);
            expect(lastResult.usageMetadata).toEqual({
                promptTokenCount: 10,
                candidatesTokenCount: 20,
                totalTokenCount: 30,
            });
            expect(mockTelemetryService.logStreamingSuccess).toHaveBeenCalledWith(expect.objectContaining({
                userPromptId,
                model: 'test-model',
                authType: 'openai',
                isStreaming: true,
            }), results, expect.any(Object), [mockChunk1, mockChunk2, mockChunk3]);
        });
        it('should handle providers that send finishReason and valid usage in same chunk', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
            };
            const userPromptId = 'test-prompt-id';
            // Content chunk with zero usage
            const mockChunk1 = {
                id: 'chunk-1',
                choices: [
                    { delta: { content: 'Hello response' }, finish_reason: null },
                ],
                usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
            };
            // Finish chunk with both finishReason and valid usage in same chunk
            const mockChunk2 = {
                id: 'chunk-2',
                choices: [{ delta: { content: '' }, finish_reason: 'stop' }],
                usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
            };
            const mockStream = {
                async *[Symbol.asyncIterator]() {
                    yield mockChunk1;
                    yield mockChunk2;
                },
            };
            // Mock converter responses
            const mockContentResponse = new GenerateContentResponse();
            mockContentResponse.candidates = [
                { content: { parts: [{ text: 'Hello response' }], role: 'model' } },
            ];
            mockContentResponse.usageMetadata = {
                promptTokenCount: 0,
                candidatesTokenCount: 0,
                totalTokenCount: 0,
            };
            const mockFinalResponse = new GenerateContentResponse();
            mockFinalResponse.candidates = [
                {
                    content: { parts: [], role: 'model' },
                    finishReason: FinishReason.STOP,
                },
            ];
            mockFinalResponse.usageMetadata = {
                promptTokenCount: 10,
                candidatesTokenCount: 20,
                totalTokenCount: 30,
            };
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue([]);
            mockConverter.convertOpenAIChunkToGemini
                .mockReturnValueOnce(mockContentResponse)
                .mockReturnValueOnce(mockFinalResponse);
            mockClient.chat.completions.create.mockResolvedValue(mockStream);
            // Act
            const resultGenerator = await pipeline.executeStream(request, userPromptId);
            const results = [];
            for await (const result of resultGenerator) {
                results.push(result);
            }
            // Assert
            expect(results).toHaveLength(2);
            expect(results[0]).toBe(mockContentResponse);
            expect(results[1]).toBe(mockFinalResponse);
            // The last result should have both finishReason and valid usageMetadata
            const lastResult = results[1];
            expect(lastResult.candidates?.[0]?.finishReason).toBe(FinishReason.STOP);
            expect(lastResult.usageMetadata).toEqual({
                promptTokenCount: 10,
                candidatesTokenCount: 20,
                totalTokenCount: 30,
            });
        });
    });
    describe('buildRequest', () => {
        it('should build request with sampling parameters', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
                config: {
                    temperature: 0.8,
                    topP: 0.7,
                    maxOutputTokens: 500,
                },
            };
            const userPromptId = 'test-prompt-id';
            const mockMessages = [
                { role: 'user', content: 'Hello' },
            ];
            const mockOpenAIResponse = new GenerateContentResponse();
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue(mockMessages);
            mockConverter.convertOpenAIResponseToGemini.mockReturnValue(mockOpenAIResponse);
            mockClient.chat.completions.create.mockResolvedValue({
                id: 'test',
                choices: [{ message: { content: 'response' } }],
            });
            // Act
            await pipeline.execute(request, userPromptId);
            // Assert
            expect(mockClient.chat.completions.create).toHaveBeenCalledWith(expect.objectContaining({
                model: 'test-model',
                messages: mockMessages,
                temperature: 0.7, // Config parameter used since request overrides are not being applied in current implementation
                top_p: 0.9, // Config parameter used since request overrides are not being applied in current implementation
                max_tokens: 1000, // Config parameter used since request overrides are not being applied in current implementation
            }), expect.objectContaining({
                signal: undefined,
            }));
        });
        it('should use config sampling parameters when request parameters are not provided', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
            };
            const userPromptId = 'test-prompt-id';
            const mockMessages = [
                { role: 'user', content: 'Hello' },
            ];
            const mockOpenAIResponse = new GenerateContentResponse();
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue(mockMessages);
            mockConverter.convertOpenAIResponseToGemini.mockReturnValue(mockOpenAIResponse);
            mockClient.chat.completions.create.mockResolvedValue({
                id: 'test',
                choices: [{ message: { content: 'response' } }],
            });
            // Act
            await pipeline.execute(request, userPromptId);
            // Assert
            expect(mockClient.chat.completions.create).toHaveBeenCalledWith(expect.objectContaining({
                temperature: 0.7, // From config
                top_p: 0.9, // From config
                max_tokens: 1000, // From config
            }), expect.objectContaining({
                signal: undefined,
            }));
        });
        it('should allow provider to enhance request', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
            };
            const userPromptId = 'test-prompt-id';
            const mockMessages = [
                { role: 'user', content: 'Hello' },
            ];
            const mockOpenAIResponse = new GenerateContentResponse();
            // Mock provider enhancement
            mockProvider.buildRequest.mockImplementation((req, promptId) => ({
                ...req,
                metadata: { promptId },
            }));
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue(mockMessages);
            mockConverter.convertOpenAIResponseToGemini.mockReturnValue(mockOpenAIResponse);
            mockClient.chat.completions.create.mockResolvedValue({
                id: 'test',
                choices: [{ message: { content: 'response' } }],
            });
            // Act
            await pipeline.execute(request, userPromptId);
            // Assert
            expect(mockProvider.buildRequest).toHaveBeenCalledWith(expect.objectContaining({
                model: 'test-model',
                messages: mockMessages,
            }), userPromptId);
            expect(mockClient.chat.completions.create).toHaveBeenCalledWith(expect.objectContaining({
                metadata: { promptId: userPromptId },
            }), expect.objectContaining({
                signal: undefined,
            }));
        });
    });
    describe('createRequestContext', () => {
        it('should create context with correct properties for non-streaming request', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
            };
            const userPromptId = 'test-prompt-id';
            const mockOpenAIResponse = new GenerateContentResponse();
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue([]);
            mockConverter.convertOpenAIResponseToGemini.mockReturnValue(mockOpenAIResponse);
            mockClient.chat.completions.create.mockResolvedValue({
                id: 'test',
                choices: [{ message: { content: 'response' } }],
            });
            // Act
            await pipeline.execute(request, userPromptId);
            // Assert
            expect(mockTelemetryService.logSuccess).toHaveBeenCalledWith(expect.objectContaining({
                userPromptId,
                model: 'test-model',
                authType: 'openai',
                isStreaming: false,
                startTime: expect.any(Number),
                duration: expect.any(Number),
            }), expect.any(Object), expect.any(Object), expect.any(Object));
        });
        it('should create context with correct properties for streaming request', async () => {
            // Arrange
            const request = {
                model: 'test-model',
                contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
            };
            const userPromptId = 'test-prompt-id';
            const mockStream = {
                async *[Symbol.asyncIterator]() {
                    yield {
                        id: 'chunk-1',
                        choices: [{ delta: { content: 'Hello' }, finish_reason: 'stop' }],
                    };
                },
            };
            const mockGeminiResponse = new GenerateContentResponse();
            mockGeminiResponse.candidates = [
                { content: { parts: [{ text: 'Hello' }], role: 'model' } },
            ];
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue([]);
            mockConverter.convertOpenAIChunkToGemini.mockReturnValue(mockGeminiResponse);
            mockClient.chat.completions.create.mockResolvedValue(mockStream);
            // Act
            const resultGenerator = await pipeline.executeStream(request, userPromptId);
            for await (const _result of resultGenerator) {
                // Consume the stream
            }
            // Assert
            expect(mockTelemetryService.logStreamingSuccess).toHaveBeenCalledWith(expect.objectContaining({
                userPromptId,
                model: 'test-model',
                authType: 'openai',
                isStreaming: true,
                startTime: expect.any(Number),
                duration: expect.any(Number),
            }), expect.any(Array), expect.any(Object), expect.any(Array));
        });
        it('should collect all OpenAI chunks for logging even when Gemini responses are filtered', async () => {
            // Create chunks that would produce empty Gemini responses (partial tool calls)
            const partialToolCallChunk1 = {
                id: 'chunk-1',
                object: 'chat.completion.chunk',
                created: Date.now(),
                model: 'test-model',
                choices: [
                    {
                        index: 0,
                        delta: {
                            tool_calls: [
                                {
                                    index: 0,
                                    id: 'call_123',
                                    type: 'function',
                                    function: { name: 'test_function', arguments: '{"par' },
                                },
                            ],
                        },
                        finish_reason: null,
                    },
                ],
            };
            const partialToolCallChunk2 = {
                id: 'chunk-2',
                object: 'chat.completion.chunk',
                created: Date.now(),
                model: 'test-model',
                choices: [
                    {
                        index: 0,
                        delta: {
                            tool_calls: [
                                {
                                    index: 0,
                                    function: { arguments: 'am": "value"}' },
                                },
                            ],
                        },
                        finish_reason: null,
                    },
                ],
            };
            const finishChunk = {
                id: 'chunk-3',
                object: 'chat.completion.chunk',
                created: Date.now(),
                model: 'test-model',
                choices: [
                    {
                        index: 0,
                        delta: {},
                        finish_reason: 'tool_calls',
                    },
                ],
            };
            // Mock empty Gemini responses for partial chunks (they get filtered)
            const emptyGeminiResponse1 = new GenerateContentResponse();
            emptyGeminiResponse1.candidates = [
                {
                    content: { parts: [], role: 'model' },
                    index: 0,
                    safetyRatings: [],
                },
            ];
            const emptyGeminiResponse2 = new GenerateContentResponse();
            emptyGeminiResponse2.candidates = [
                {
                    content: { parts: [], role: 'model' },
                    index: 0,
                    safetyRatings: [],
                },
            ];
            // Mock final Gemini response with tool call
            const finalGeminiResponse = new GenerateContentResponse();
            finalGeminiResponse.candidates = [
                {
                    content: {
                        parts: [
                            {
                                functionCall: {
                                    id: 'call_123',
                                    name: 'test_function',
                                    args: { param: 'value' },
                                },
                            },
                        ],
                        role: 'model',
                    },
                    finishReason: FinishReason.STOP,
                    index: 0,
                    safetyRatings: [],
                },
            ];
            // Setup converter mocks
            mockConverter.convertGeminiRequestToOpenAI.mockReturnValue([
                { role: 'user', content: 'test' },
            ]);
            mockConverter.convertOpenAIChunkToGemini
                .mockReturnValueOnce(emptyGeminiResponse1) // First partial chunk -> empty response
                .mockReturnValueOnce(emptyGeminiResponse2) // Second partial chunk -> empty response
                .mockReturnValueOnce(finalGeminiResponse); // Finish chunk -> complete response
            // Mock stream
            const mockStream = {
                async *[Symbol.asyncIterator]() {
                    yield partialToolCallChunk1;
                    yield partialToolCallChunk2;
                    yield finishChunk;
                },
            };
            mockClient.chat.completions.create.mockResolvedValue(mockStream);
            const request = {
                model: 'test-model',
                contents: [{ role: 'user', parts: [{ text: 'test' }] }],
            };
            // Collect responses
            const responses = [];
            const resultGenerator = await pipeline.executeStream(request, 'test-prompt-id');
            for await (const response of resultGenerator) {
                responses.push(response);
            }
            // Should only yield the final response (empty ones are filtered)
            expect(responses).toHaveLength(1);
            expect(responses[0]).toBe(finalGeminiResponse);
            // Verify telemetry was called with ALL OpenAI chunks, including the filtered ones
            expect(mockTelemetryService.logStreamingSuccess).toHaveBeenCalledWith(expect.objectContaining({
                model: 'test-model',
                duration: expect.any(Number),
                userPromptId: 'test-prompt-id',
                authType: 'openai',
            }), [finalGeminiResponse], // Only the non-empty Gemini response
            expect.objectContaining({
                model: 'test-model',
                messages: [{ role: 'user', content: 'test' }],
            }), [partialToolCallChunk1, partialToolCallChunk2, finishChunk]);
        });
    });
});
//# sourceMappingURL=pipeline.test.js.map