import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import OpenAI from 'openai';
import { isNode } from '../../../__fixtures__/environment.js';
import { OpenAIModel } from '../index.js';
import { ContextWindowOverflowError, ModelThrottledError } from '../../../errors.js';
import { collectIterator } from '../../../__fixtures__/model-test-helpers.js';
import { Message, TextBlock, ToolUseBlock, ToolResultBlock, GuardContentBlock } from '../../../types/messages.js';
import { ImageBlock, DocumentBlock, VideoBlock } from '../../../types/media.js';
import { warnOnce } from '../../../logging/warn-once.js';
import { logger } from '../../../logging/logger.js';
/**
 * Helper to create a mock OpenAI client with streaming support
 */
function createMockClient(streamGenerator) {
    return {
        chat: {
            completions: {
                create: vi.fn(async () => streamGenerator()),
            },
        },
    };
}
// Mock the OpenAI SDK
vi.mock('openai', () => {
    const mockConstructor = vi.fn(function () {
        return {};
    });
    return {
        default: mockConstructor,
    };
});
vi.mock('../../../logging/warn-once.js', () => ({
    warnOnce: vi.fn(),
}));
describe('OpenAIModel', () => {
    beforeEach(() => {
        vi.clearAllMocks();
        vi.restoreAllMocks();
        // Set default env var for most tests using Vitest's stubEnv (Node.js only)
        if (isNode) {
            vi.stubEnv('OPENAI_API_KEY', 'sk-test-env');
        }
    });
    afterEach(() => {
        vi.clearAllMocks();
        // Restore all environment variables to their original state (Node.js only)
        if (isNode) {
            vi.unstubAllEnvs();
        }
    });
    // Shared helper to create a mock OpenAI client that captures the request
    const createMockClientWithCapture = (captureContainer) => {
        return {
            chat: {
                completions: {
                    create: vi.fn(async (request) => {
                        captureContainer.request = request;
                        return (async function* () {
                            yield { choices: [{ delta: { role: 'assistant' }, index: 0 }] };
                            yield { choices: [{ finish_reason: 'stop', delta: {}, index: 0 }] };
                        })();
                    }),
                },
            },
        };
    };
    describe('constructor', () => {
        it('creates an instance with required modelId', () => {
            const provider = new OpenAIModel({ api: 'chat', modelId: 'gpt-5.4', apiKey: 'sk-test' });
            const config = provider.getConfig();
            expect(config.modelId).toBe('gpt-5.4');
        });
        it('uses custom model ID', () => {
            const customModelId = 'gpt-3.5-turbo';
            const provider = new OpenAIModel({ api: 'chat', modelId: customModelId, apiKey: 'sk-test' });
            expect(provider.getConfig()).toStrictEqual({
                modelId: customModelId,
            });
        });
        it('warns when modelId is not explicitly set', () => {
            new OpenAIModel({ api: 'chat', apiKey: 'sk-test' });
            expect(warnOnce).toHaveBeenCalledWith(expect.objectContaining({ warn: expect.any(Function) }), expect.stringContaining('using default modelId'));
        });
        it('does not warn when modelId is explicitly set', () => {
            new OpenAIModel({ api: 'chat', modelId: 'gpt-5.4', apiKey: 'sk-test' });
            expect(warnOnce).not.toHaveBeenCalledWith(expect.objectContaining({ warn: expect.any(Function) }), expect.stringContaining('using default modelId'));
        });
        it('uses API key from constructor parameter', () => {
            const apiKey = 'sk-explicit';
            new OpenAIModel({ api: 'chat', modelId: 'gpt-5.4', apiKey });
            expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({
                apiKey: apiKey,
            }));
        });
        // Node.js-specific test: environment variable usage
        if (isNode) {
            it('uses API key from environment variable', () => {
                vi.stubEnv('OPENAI_API_KEY', 'sk-from-env');
                new OpenAIModel({ api: 'chat', modelId: 'gpt-5.4' });
                // OpenAI client should be called without explicit apiKey (uses env var internally)
                expect(OpenAI).toHaveBeenCalled();
            });
        }
        it('explicit API key takes precedence over environment variable', () => {
            if (isNode) {
                vi.stubEnv('OPENAI_API_KEY', 'sk-from-env');
            }
            const explicitKey = 'sk-explicit';
            new OpenAIModel({ api: 'chat', modelId: 'gpt-5.4', apiKey: explicitKey });
            expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({
                apiKey: explicitKey,
            }));
        });
        it('throws error when no API key is available', () => {
            if (isNode) {
                vi.stubEnv('OPENAI_API_KEY', '');
            }
            expect(() => new OpenAIModel({ api: 'chat', modelId: 'gpt-5.4' })).toThrow("OpenAI API key is required. Provide it via the 'apiKey' option (string or function) or set the OPENAI_API_KEY environment variable.");
        });
        it('uses custom client configuration', () => {
            const timeout = 30000;
            new OpenAIModel({ api: 'chat', modelId: 'gpt-5.4', apiKey: 'sk-test', clientConfig: { timeout } });
            expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({
                timeout: timeout,
            }));
        });
        it('uses provided client instance', () => {
            vi.clearAllMocks();
            const mockClient = {};
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            // Should not create a new OpenAI client
            expect(OpenAI).not.toHaveBeenCalled();
            expect(provider).toBeDefined();
        });
        it('provided client takes precedence over apiKey and clientConfig', () => {
            vi.clearAllMocks();
            const mockClient = {};
            new OpenAIModel({
                api: 'chat',
                modelId: 'gpt-5.4',
                apiKey: 'sk-test',
                client: mockClient,
                clientConfig: { timeout: 30000 },
            });
            // Should not create a new OpenAI client when client is provided
            expect(OpenAI).not.toHaveBeenCalled();
        });
        it('does not require API key when client is provided', () => {
            vi.clearAllMocks();
            if (isNode) {
                vi.stubEnv('OPENAI_API_KEY', '');
            }
            const mockClient = {};
            expect(() => new OpenAIModel({ api: 'chat', client: mockClient })).not.toThrow();
        });
        it('accepts function-based API key', () => {
            const apiKeyFn = vi.fn(async () => 'sk-dynamic');
            new OpenAIModel({
                api: 'chat',
                modelId: 'gpt-5.4',
                apiKey: apiKeyFn,
            });
            expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({
                apiKey: apiKeyFn,
            }));
        });
        it('accepts async function-based API key', () => {
            const apiKeyFn = async () => {
                await new Promise((resolve) => globalThis.setTimeout(resolve, 10));
                return 'sk-async-key';
            };
            new OpenAIModel({
                api: 'chat',
                modelId: 'gpt-5.4',
                apiKey: apiKeyFn,
            });
            expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({
                apiKey: apiKeyFn,
            }));
        });
    });
    describe('updateConfig', () => {
        it('merges new config with existing config', () => {
            const provider = new OpenAIModel({ api: 'chat', modelId: 'gpt-5.4', apiKey: 'sk-test', temperature: 0.5 });
            provider.updateConfig({ modelId: 'gpt-5.4', temperature: 0.8, maxTokens: 2048 });
            expect(provider.getConfig()).toStrictEqual({
                modelId: 'gpt-5.4',
                temperature: 0.8,
                maxTokens: 2048,
                contextWindowLimit: 1_050_000,
            });
        });
        it('preserves fields not included in the update', () => {
            const provider = new OpenAIModel({
                api: 'chat',
                apiKey: 'sk-test',
                modelId: 'gpt-3.5-turbo',
                temperature: 0.5,
                maxTokens: 1024,
            });
            provider.updateConfig({ modelId: 'gpt-3.5-turbo', temperature: 0.8 });
            expect(provider.getConfig()).toStrictEqual({
                modelId: 'gpt-3.5-turbo',
                temperature: 0.8,
                maxTokens: 1024,
            });
        });
        it('re-resolves contextWindowLimit when modelId changes and it was auto-resolved', () => {
            const provider = new OpenAIModel({ api: 'chat', apiKey: 'sk-test' });
            expect(provider.getConfig().contextWindowLimit).toBe(1_050_000); // gpt-5.4 default
            provider.updateConfig({ modelId: 'gpt-4o' });
            expect(provider.getConfig().contextWindowLimit).toBe(128_000); // gpt-4o value
        });
        it('preserves explicit contextWindowLimit when modelId changes', () => {
            const provider = new OpenAIModel({ api: 'chat', apiKey: 'sk-test', contextWindowLimit: 50_000 });
            expect(provider.getConfig().contextWindowLimit).toBe(50_000);
            provider.updateConfig({ modelId: 'gpt-4o' });
            expect(provider.getConfig().contextWindowLimit).toBe(50_000); // preserved
        });
    });
    describe('getConfig', () => {
        it('returns the current configuration', () => {
            const provider = new OpenAIModel({
                api: 'chat',
                modelId: 'gpt-5.4',
                apiKey: 'sk-test',
                maxTokens: 1024,
                temperature: 0.7,
            });
            expect(provider.getConfig()).toStrictEqual({
                modelId: 'gpt-5.4',
                maxTokens: 1024,
                temperature: 0.7,
                contextWindowLimit: 1_050_000,
            });
        });
        it('includes contextWindowLimit in config when provided', () => {
            const provider = new OpenAIModel({
                api: 'chat',
                modelId: 'gpt-4o',
                apiKey: 'sk-test',
                contextWindowLimit: 128_000,
            });
            expect(provider.getConfig()).toStrictEqual({
                modelId: 'gpt-4o',
                contextWindowLimit: 128_000,
            });
        });
        it('auto-populates contextWindowLimit from model ID lookup', () => {
            const provider = new OpenAIModel({ api: 'chat', modelId: 'gpt-4o', apiKey: 'sk-test' });
            expect(provider.getConfig()).toStrictEqual({
                modelId: 'gpt-4o',
                contextWindowLimit: 128_000,
            });
        });
        it('auto-populates contextWindowLimit for default model ID', () => {
            const provider = new OpenAIModel({ api: 'chat', apiKey: 'sk-test' });
            expect(provider.getConfig()).toStrictEqual({
                contextWindowLimit: 1_050_000,
            });
        });
        it('does not override explicit contextWindowLimit', () => {
            const provider = new OpenAIModel({
                api: 'chat',
                modelId: 'gpt-4o',
                apiKey: 'sk-test',
                contextWindowLimit: 50_000,
            });
            expect(provider.getConfig()).toStrictEqual({
                modelId: 'gpt-4o',
                contextWindowLimit: 50_000,
            });
        });
        it('leaves contextWindowLimit undefined for unknown model IDs', () => {
            const provider = new OpenAIModel({ api: 'chat', modelId: 'unknown-model', apiKey: 'sk-test' });
            expect(provider.getConfig()).toStrictEqual({
                modelId: 'unknown-model',
            });
        });
    });
    describe('managed params warning', () => {
        it('warns on construction when params contains provider-managed keys', () => {
            const warnSpy = vi.spyOn(logger, 'warn');
            new OpenAIModel({
                api: 'chat',
                client: {},
                params: { model: 'bad', stream: false },
            });
            expect(warnSpy).toHaveBeenCalledTimes(2);
            expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("'model'"));
            expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("'stream'"));
            warnSpy.mockRestore();
        });
        it('warns on updateConfig when params contains provider-managed keys', () => {
            const model = new OpenAIModel({ api: 'chat', client: {} });
            const warnSpy = vi.spyOn(logger, 'warn');
            model.updateConfig({ params: { stream_options: { include_usage: false } } });
            expect(warnSpy).toHaveBeenCalledTimes(1);
            expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("'stream_options'"));
            warnSpy.mockRestore();
        });
        it('does not warn when params contains only non-managed keys', () => {
            const warnSpy = vi.spyOn(logger, 'warn');
            new OpenAIModel({ api: 'chat', client: {}, params: { seed: 42 } });
            expect(warnSpy).not.toHaveBeenCalled();
            warnSpy.mockRestore();
        });
        it('provider-managed fields in params are overridden and cannot take effect', async () => {
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const warnSpy = vi.spyOn(logger, 'warn');
            const provider = new OpenAIModel({
                api: 'chat',
                modelId: 'gpt-5.4',
                client: mockClient,
                params: {
                    model: 'attacker-model',
                    messages: [{ role: 'user', content: 'hijacked' }],
                    stream: false,
                    stream_options: { include_usage: false },
                },
            });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            await collectIterator(provider.stream(messages));
            expect(captured.request.model).toBe('gpt-5.4');
            expect(captured.request.stream).toBe(true);
            expect(captured.request.stream_options).toEqual({ include_usage: true });
            expect(Array.isArray(captured.request.messages)).toBe(true);
            expect(captured.request.messages[0]).toEqual({ role: 'user', content: [{ type: 'text', text: 'Hi' }] });
            warnSpy.mockRestore();
        });
    });
    describe('stream', () => {
        describe('validation', () => {
            it('throws error when messages array is empty', async () => {
                const mockClient = createMockClient(async function* () { });
                const provider = new OpenAIModel({ api: 'chat', client: mockClient });
                await expect(async () => {
                    await collectIterator(provider.stream([]));
                }).rejects.toThrow('At least one message is required');
            });
            it('validates system prompt is not empty', async () => {
                const mockClient = createMockClient(async function* () {
                    yield {
                        choices: [{ delta: { role: 'assistant', content: 'Hello' }, index: 0 }],
                    };
                    yield {
                        choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
                    };
                });
                const provider = new OpenAIModel({ api: 'chat', client: mockClient });
                const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
                // System prompt that's only whitespace should not be sent
                const events = await collectIterator(provider.stream(messages, { systemPrompt: '   ' }));
                // Should still get valid events
                expect(events.length).toBeGreaterThan(0);
                expect(events[0]?.type).toBe('modelMessageStartEvent');
            });
            it('throws error for streaming with n > 1', async () => {
                const mockClient = createMockClient(async function* () { });
                const provider = new OpenAIModel({
                    api: 'chat',
                    modelId: 'gpt-5.4',
                    client: mockClient,
                    params: { n: 2 },
                });
                const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
                await expect(async () => {
                    for await (const _ of provider.stream(messages)) {
                        // Should not reach here
                    }
                }).rejects.toThrow('Streaming with n > 1 is not supported');
            });
            it('throws error for tool spec without name or description', async () => {
                const mockClient = createMockClient(async function* () { });
                const provider = new OpenAIModel({ api: 'chat', client: mockClient });
                const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
                await expect(async () => {
                    for await (const _ of provider.stream(messages, {
                        toolSpecs: [{ name: '', description: 'test', inputSchema: {} }],
                    })) {
                        // Should not reach here
                    }
                }).rejects.toThrow('Tool specification must have both name and description');
            });
            it('throws error for empty tool result content', async () => {
                const mockClient = createMockClient(async function* () { });
                const provider = new OpenAIModel({ api: 'chat', client: mockClient });
                const messages = [
                    new Message({
                        role: 'user',
                        content: [
                            new ToolResultBlock({
                                toolUseId: 'tool-123',
                                status: 'success',
                                content: [],
                            }),
                        ],
                    }),
                ];
                await expect(async () => {
                    for await (const _ of provider.stream(messages)) {
                        // Should not reach here
                    }
                }).rejects.toThrow('Tool result for toolUseId "tool-123" has empty content');
            });
            it('handles tool result with error status', async () => {
                const mockClient = createMockClient(async function* () {
                    yield {
                        choices: [{ delta: { role: 'assistant', content: 'Ok' }, index: 0 }],
                    };
                    yield {
                        choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
                    };
                });
                const provider = new OpenAIModel({ api: 'chat', client: mockClient });
                const messages = [
                    new Message({ role: 'user', content: [new TextBlock('Run tool')] }),
                    new Message({
                        role: 'assistant',
                        content: [
                            new ToolUseBlock({
                                name: 'calculator',
                                toolUseId: 'tool-123',
                                input: { expr: 'invalid' },
                            }),
                        ],
                    }),
                    new Message({
                        role: 'user',
                        content: [
                            new ToolResultBlock({
                                toolUseId: 'tool-123',
                                status: 'error',
                                content: [new TextBlock('Division by zero')],
                            }),
                        ],
                    }),
                ];
                // Should not throw - error status is handled by prepending [ERROR]
                const events = await collectIterator(provider.stream(messages));
                // Verify we got a response
                expect(events.length).toBeGreaterThan(0);
                expect(events[0]?.type).toBe('modelMessageStartEvent');
            });
            it('throws error for circular reference in tool input', async () => {
                const mockClient = createMockClient(async function* () { });
                const provider = new OpenAIModel({ api: 'chat', client: mockClient });
                const circular = { a: 1 };
                circular.self = circular;
                const messages = [
                    new Message({ role: 'user', content: [new TextBlock('Hi')] }),
                    new Message({
                        role: 'assistant',
                        content: [
                            new ToolUseBlock({
                                name: 'test',
                                toolUseId: 'tool-1',
                                input: circular,
                            }),
                        ],
                    }),
                ];
                await expect(async () => {
                    for await (const _ of provider.stream(messages)) {
                        // Should not reach here
                    }
                }).rejects.toThrow('Failed to serialize tool input');
            });
        });
        describe('basic streaming', () => {
            it('yields correct event sequence for simple text response', async () => {
                const mockClient = createMockClient(async function* () {
                    yield {
                        choices: [{ delta: { role: 'assistant' }, index: 0 }],
                    };
                    yield {
                        choices: [{ delta: { content: 'Hello' }, index: 0 }],
                    };
                    yield {
                        choices: [{ delta: { content: ' world' }, index: 0 }],
                    };
                    yield {
                        choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
                    };
                });
                const provider = new OpenAIModel({ api: 'chat', client: mockClient });
                const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
                const events = await collectIterator(provider.stream(messages));
                // Now includes complete content block lifecycle: start, deltas, stop
                expect(events).toHaveLength(6);
                expect(events[0]).toEqual({ type: 'modelMessageStartEvent', role: 'assistant' });
                expect(events[1]).toEqual({
                    type: 'modelContentBlockStartEvent',
                });
                expect(events[2]).toEqual({
                    type: 'modelContentBlockDeltaEvent',
                    delta: { type: 'textDelta', text: 'Hello' },
                });
                expect(events[3]).toEqual({
                    type: 'modelContentBlockDeltaEvent',
                    delta: { type: 'textDelta', text: ' world' },
                });
                expect(events[4]).toEqual({
                    type: 'modelContentBlockStopEvent',
                });
                expect(events[5]).toEqual({ type: 'modelMessageStopEvent', stopReason: 'endTurn' });
            });
        });
        it('emits modelMetadataEvent with usage information', async () => {
            const mockClient = createMockClient(async function* () {
                yield {
                    choices: [{ delta: { role: 'assistant' }, index: 0 }],
                };
                yield {
                    choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
                };
                yield {
                    choices: [],
                    usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
                };
            });
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            const events = await collectIterator(provider.stream(messages));
            const metadataEvent = events.find((e) => e.type === 'modelMetadataEvent');
            expect(metadataEvent).toBeDefined();
            expect(metadataEvent).toEqual({
                type: 'modelMetadataEvent',
                usage: {
                    inputTokens: 10,
                    outputTokens: 5,
                    totalTokens: 15,
                },
            });
        });
        it('handles usage with undefined properties', async () => {
            const mockClient = createMockClient(async function* () {
                yield {
                    choices: [{ delta: { role: 'assistant' }, index: 0 }],
                };
                yield {
                    choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
                };
                yield {
                    choices: [],
                    usage: {}, // Empty usage object
                };
            });
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            const events = await collectIterator(provider.stream(messages));
            const metadataEvent = events.find((e) => e.type === 'modelMetadataEvent');
            expect(metadataEvent).toBeDefined();
            expect(metadataEvent).toEqual({
                type: 'modelMetadataEvent',
                usage: {
                    inputTokens: 0,
                    outputTokens: 0,
                    totalTokens: 0,
                },
            });
        });
        it('filters out empty string content deltas', async () => {
            const mockClient = createMockClient(async function* () {
                yield {
                    choices: [{ delta: { role: 'assistant' }, index: 0 }],
                };
                yield {
                    choices: [{ delta: { content: '' }, index: 0 }], // Empty content
                };
                yield {
                    choices: [{ delta: { content: 'Hello' }, index: 0 }],
                };
                yield {
                    choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
                };
            });
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            const events = await collectIterator(provider.stream(messages));
            // Should not emit event for empty content
            const contentEvents = events.filter((e) => e.type === 'modelContentBlockDeltaEvent');
            expect(contentEvents).toHaveLength(1);
            expect(contentEvents[0].delta.text).toBe('Hello');
        });
        it('prevents duplicate message start events', async () => {
            const mockClient = createMockClient(async function* () {
                yield {
                    choices: [{ delta: { role: 'assistant' }, index: 0 }],
                };
                yield {
                    choices: [{ delta: { role: 'assistant', content: 'Hello' }, index: 0 }], // Duplicate role
                };
                yield {
                    choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
                };
            });
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            // Suppress console.warn for this test
            vi.spyOn(console, 'warn').mockImplementation(() => { });
            const events = await collectIterator(provider.stream(messages));
            // Should only have one message start event
            const startEvents = events.filter((e) => e.type === 'modelMessageStartEvent');
            expect(startEvents).toHaveLength(1);
        });
    });
    describe('tool calling', () => {
        it('handles tool use request with contentBlockStart and contentBlockStop events', async () => {
            const mockClient = createMockClient(async function* () {
                yield {
                    choices: [{ delta: { role: 'assistant' }, index: 0 }],
                };
                yield {
                    choices: [
                        {
                            delta: {
                                tool_calls: [
                                    {
                                        index: 0,
                                        id: 'call_123',
                                        type: 'function',
                                        function: { name: 'calculator', arguments: '' },
                                    },
                                ],
                            },
                            index: 0,
                        },
                    ],
                };
                yield {
                    choices: [
                        {
                            delta: {
                                tool_calls: [{ index: 0, function: { arguments: '{"expr' } }],
                            },
                            index: 0,
                        },
                    ],
                };
                yield {
                    choices: [
                        {
                            delta: {
                                tool_calls: [{ index: 0, function: { arguments: '":"2+2"}' } }],
                            },
                            index: 0,
                        },
                    ],
                };
                yield {
                    choices: [{ finish_reason: 'tool_calls', delta: {}, index: 0 }],
                };
            });
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Calculate 2+2')] })];
            const events = await collectIterator(provider.stream(messages));
            // Verify key events in sequence
            expect(events[0]).toEqual({ type: 'modelMessageStartEvent', role: 'assistant' });
            expect(events[1]).toEqual({
                type: 'modelContentBlockStartEvent',
                start: {
                    type: 'toolUseStart',
                    name: 'calculator',
                    toolUseId: 'call_123',
                },
            });
            expect(events[2]).toEqual({
                type: 'modelContentBlockDeltaEvent',
                delta: {
                    type: 'toolUseInputDelta',
                    input: '{"expr',
                },
            });
            expect(events[3]).toEqual({
                type: 'modelContentBlockDeltaEvent',
                delta: {
                    type: 'toolUseInputDelta',
                    input: '":"2+2"}',
                },
            });
            expect(events[4]).toEqual({
                type: 'modelContentBlockStopEvent',
            });
            expect(events[5]).toEqual({ type: 'modelMessageStopEvent', stopReason: 'toolUse' });
        });
        it('handles multiple tool calls', async () => {
            const mockClient = createMockClient(async function* () {
                yield {
                    choices: [{ delta: { role: 'assistant' }, index: 0 }],
                };
                yield {
                    choices: [
                        {
                            delta: {
                                tool_calls: [
                                    {
                                        index: 0,
                                        id: 'call_1',
                                        type: 'function',
                                        function: { name: 'tool1', arguments: '{}' },
                                    },
                                ],
                            },
                            index: 0,
                        },
                    ],
                };
                yield {
                    choices: [
                        {
                            delta: {
                                tool_calls: [
                                    {
                                        index: 1,
                                        id: 'call_2',
                                        type: 'function',
                                        function: { name: 'tool2', arguments: '{}' },
                                    },
                                ],
                            },
                            index: 0,
                        },
                    ],
                };
                yield {
                    choices: [{ finish_reason: 'tool_calls', delta: {}, index: 0 }],
                };
            });
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            const events = await collectIterator(provider.stream(messages));
            // Should emit stop events for both tool calls
            const stopEvents = events.filter((e) => e.type === 'modelContentBlockStopEvent');
            expect(stopEvents).toHaveLength(2);
            expect(stopEvents[0]).toEqual({ type: 'modelContentBlockStopEvent' });
            expect(stopEvents[1]).toEqual({ type: 'modelContentBlockStopEvent' });
        });
        it('skips tool calls with invalid index', async () => {
            const mockClient = createMockClient(async function* () {
                yield {
                    choices: [{ delta: { role: 'assistant' }, index: 0 }],
                };
                yield {
                    choices: [
                        {
                            delta: {
                                tool_calls: [
                                    {
                                        index: undefined, // Invalid index
                                        id: 'call_123',
                                        type: 'function',
                                        function: { name: 'tool', arguments: '{}' },
                                    },
                                ],
                            },
                            index: 0,
                        },
                    ],
                };
                yield {
                    choices: [{ finish_reason: 'stop', delta: {}, index: 0 }],
                };
            });
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            // Suppress console.warn for this test
            vi.spyOn(console, 'warn').mockImplementation(() => { });
            const events = await collectIterator(provider.stream(messages));
            // Should not emit any tool-related events
            const toolEvents = events.filter((e) => e.type === 'modelContentBlockStartEvent' || e.type === 'modelContentBlockDeltaEvent');
            expect(toolEvents).toHaveLength(0);
            // The important thing is that invalid tool calls don't crash the stream
            // and are properly skipped
            expect(events.length).toBeGreaterThan(0); // Still got message events
        });
        it('tool argument deltas can be reassembled into valid JSON', async () => {
            const mockClient = createMockClient(async function* () {
                yield { choices: [{ delta: { role: 'assistant' }, index: 0 }] };
                yield {
                    choices: [
                        {
                            delta: {
                                tool_calls: [
                                    {
                                        index: 0,
                                        id: 'call_123',
                                        type: 'function',
                                        function: { name: 'calculator', arguments: '' },
                                    },
                                ],
                            },
                            index: 0,
                        },
                    ],
                };
                // Split JSON across multiple chunks in realistic ways
                yield { choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: '{"' } }] }, index: 0 }] };
                yield { choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: 'x":' } }] }, index: 0 }] };
                yield { choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: '10,' } }] }, index: 0 }] };
                yield { choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: '"y":' } }] }, index: 0 }] };
                yield { choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: '20}' } }] }, index: 0 }] };
                yield { choices: [{ finish_reason: 'tool_calls', delta: {}, index: 0 }] };
            });
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            const events = await collectIterator(provider.stream(messages));
            // Extract and concatenate all tool input deltas
            const inputDeltas = events
                .filter((e) => e.type === 'modelContentBlockDeltaEvent' && e.delta.type === 'toolUseInputDelta')
                .map((e) => e.delta.input);
            const reassembled = inputDeltas.join('');
            // Should be valid JSON
            expect(() => JSON.parse(reassembled)).not.toThrow();
            expect(JSON.parse(reassembled)).toEqual({ x: 10, y: 20 });
        });
        it('handles messages with both text and tool calls', async () => {
            const mockClient = createMockClient(async function* () {
                yield { choices: [{ delta: { role: 'assistant' }, index: 0 }] };
                // Text content first
                yield { choices: [{ delta: { content: 'Let me calculate ' }, index: 0 }] };
                yield { choices: [{ delta: { content: 'that for you.' }, index: 0 }] };
                // Then tool call
                yield {
                    choices: [
                        {
                            delta: {
                                tool_calls: [
                                    {
                                        index: 0,
                                        id: 'call_123',
                                        type: 'function',
                                        function: { name: 'calculator', arguments: '{"expr":"2+2"}' },
                                    },
                                ],
                            },
                            index: 0,
                        },
                    ],
                };
                yield { choices: [{ finish_reason: 'tool_calls', delta: {}, index: 0 }] };
            });
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Calculate 2+2')] })];
            const events = await collectIterator(provider.stream(messages));
            // Should have text deltas followed by tool events
            expect(events[0]?.type).toBe('modelMessageStartEvent');
            // Text content block start
            expect(events[1]?.type).toBe('modelContentBlockStartEvent');
            // Text deltas
            expect(events[2]?.type).toBe('modelContentBlockDeltaEvent');
            expect(events[2].delta.type).toBe('textDelta');
            expect(events[2].delta.text).toBe('Let me calculate ');
            // Tool events should follow
            const toolStartEvent = events.find((e) => e.type === 'modelContentBlockStartEvent' && e.start?.type === 'toolUseStart');
            expect(toolStartEvent).toBeDefined();
            // Both text and tool blocks should have stop events
            const stopEvents = events.filter((e) => e.type === 'modelContentBlockStopEvent');
            expect(stopEvents.length).toBeGreaterThan(0);
        });
    });
    describe('stop reasons', () => {
        it('maps OpenAI stop reasons to SDK stop reasons', async () => {
            const stopReasons = [
                { openai: 'stop', sdk: 'endTurn' },
                { openai: 'tool_calls', sdk: 'toolUse' },
                { openai: 'length', sdk: 'maxTokens' },
                { openai: 'content_filter', sdk: 'contentFiltered' },
            ];
            for (const { openai, sdk } of stopReasons) {
                const mockClient = createMockClient(async function* () {
                    yield {
                        choices: [{ delta: { role: 'assistant' }, index: 0 }],
                    };
                    yield {
                        choices: [{ finish_reason: openai, delta: {}, index: 0 }],
                    };
                });
                const provider = new OpenAIModel({ api: 'chat', client: mockClient });
                const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
                const events = await collectIterator(provider.stream(messages));
                const stopEvent = events.find((e) => e.type === 'modelMessageStopEvent');
                expect(stopEvent).toBeDefined();
                expect(stopEvent.stopReason).toBe(sdk);
            }
        });
        it('handles unknown stop reasons with warning', async () => {
            const mockClient = createMockClient(async function* () {
                yield {
                    choices: [{ delta: { role: 'assistant' }, index: 0 }],
                };
                yield {
                    choices: [{ finish_reason: 'new_unknown_reason', delta: {}, index: 0 }],
                };
            });
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            const events = await collectIterator(provider.stream(messages));
            // Should convert unknown stop reason to camelCase
            const stopEvent = events.find((e) => e.type === 'modelMessageStopEvent');
            expect(stopEvent).toBeDefined();
            expect(stopEvent.stopReason).toBe('newUnknownReason');
            // Note: Warning logging is verified manually/visually since console.warn spying
            // has test isolation issues when running the full test suite
        });
    });
    describe('API request formatting', () => {
        it('formats API request correctly with all options', async () => {
            let capturedRequest = null;
            let callCount = 0;
            const mockClient = {
                chat: {
                    completions: {
                        create: vi.fn(async (request) => {
                            capturedRequest = request;
                            callCount++;
                            // Return an async generator
                            return (async function* () {
                                yield { choices: [{ delta: { role: 'assistant' }, index: 0 }] };
                                yield { choices: [{ finish_reason: 'stop', delta: {}, index: 0 }] };
                            })();
                        }),
                    },
                },
            };
            const provider = new OpenAIModel({
                api: 'chat',
                modelId: 'gpt-5.4',
                client: mockClient,
                temperature: 0.7,
                maxTokens: 1000,
            });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            const toolSpecs = [
                {
                    name: 'calculator',
                    description: 'Calculate expressions',
                    inputSchema: { type: 'object', properties: { expr: { type: 'string' } } },
                },
            ];
            await collectIterator(provider.stream(messages, {
                systemPrompt: 'You are a helpful assistant',
                toolSpecs,
                toolChoice: { auto: {} },
            }));
            // Verify create was called with correct structure
            expect(callCount).toBe(1);
            expect(capturedRequest).toBeDefined();
            expect(capturedRequest).toEqual({
                model: 'gpt-5.4',
                stream: true,
                stream_options: { include_usage: true },
                temperature: 0.7,
                max_completion_tokens: 1000,
                messages: [
                    { role: 'system', content: 'You are a helpful assistant' },
                    { role: 'user', content: [{ type: 'text', text: 'Hi' }] },
                ],
                tools: [
                    {
                        type: 'function',
                        function: {
                            name: 'calculator',
                            description: 'Calculate expressions',
                            parameters: { type: 'object', properties: { expr: { type: 'string' } } },
                        },
                    },
                ],
                tool_choice: 'auto',
            });
        });
    });
    describe('systemPrompt handling', () => {
        it('formats array system prompt with text blocks only', async () => {
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hello')] })];
            await collectIterator(provider.stream(messages, {
                systemPrompt: [
                    { type: 'textBlock', text: 'You are a helpful assistant' },
                    { type: 'textBlock', text: 'Additional context here' },
                ],
            }));
            expect(captured.request).toBeDefined();
            expect(captured.request.messages).toEqual([
                { role: 'system', content: 'You are a helpful assistantAdditional context here' },
                { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
            ]);
        });
        it('formats array system prompt with cache points', async () => {
            const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hello')] })];
            collectIterator(provider.stream(messages, {
                systemPrompt: [
                    { type: 'textBlock', text: 'You are a helpful assistant' },
                    { type: 'textBlock', text: 'Large context document' },
                    { type: 'cachePointBlock', cacheType: 'default' },
                ],
            }));
            // Verify warning was logged
            expect(warnSpy).toHaveBeenCalledWith('cache points are not supported in openai system prompts, ignoring cache points');
            // Verify system message contains only text (cache points ignored)
            expect(captured.request).toBeDefined();
            expect(captured.request.messages).toEqual([
                { role: 'system', content: 'You are a helpful assistantLarge context document' },
                { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
            ]);
            warnSpy.mockRestore();
        });
        it('handles empty array system prompt', async () => {
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hello')] })];
            await collectIterator(provider.stream(messages, {
                systemPrompt: [],
            }));
            // Empty array should not add system message
            expect(captured.request).toBeDefined();
            expect(captured.request.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }]);
        });
        it('formats array system prompt with single text block', async () => {
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hello')] })];
            await collectIterator(provider.stream(messages, {
                systemPrompt: [{ type: 'textBlock', text: 'You are a helpful assistant' }],
            }));
            expect(captured.request).toBeDefined();
            expect(captured.request.messages).toEqual([
                { role: 'system', content: 'You are a helpful assistant' },
                { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
            ]);
        });
        it('warns and filters guard content from system prompt', async () => {
            const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hello')] })];
            await collectIterator(provider.stream(messages, {
                systemPrompt: [
                    { type: 'textBlock', text: 'You are a helpful assistant' },
                    {
                        type: 'guardContentBlock',
                        text: {
                            qualifiers: ['grounding_source'],
                            text: 'Guard content',
                        },
                    },
                ],
            }));
            // Verify warning was logged
            expect(warnSpy).toHaveBeenCalledWith('guard content is not supported in openai system prompts, removing guard content block');
            // Verify guard content is filtered out
            expect(captured.request).toBeDefined();
            expect(captured.request.messages).toEqual([
                { role: 'system', content: 'You are a helpful assistant' },
                { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
            ]);
            warnSpy.mockRestore();
        });
        it('preserves text blocks when filtering guard content', async () => {
            const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hello')] })];
            await collectIterator(provider.stream(messages, {
                systemPrompt: [
                    { type: 'textBlock', text: 'First text' },
                    {
                        type: 'guardContentBlock',
                        text: {
                            qualifiers: ['query'],
                            text: 'Guard content',
                        },
                    },
                    { type: 'textBlock', text: 'Second text' },
                ],
            }));
            // Verify warning was logged
            expect(warnSpy).toHaveBeenCalledWith('guard content is not supported in openai system prompts, removing guard content block');
            // Verify both text blocks preserved, guard content removed
            expect(captured.request).toBeDefined();
            expect(captured.request.messages).toEqual([
                { role: 'system', content: 'First textSecond text' },
                { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
            ]);
            warnSpy.mockRestore();
        });
        it('handles system prompt with only guard content', async () => {
            const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hello')] })];
            await collectIterator(provider.stream(messages, {
                systemPrompt: [
                    {
                        type: 'guardContentBlock',
                        text: {
                            qualifiers: ['guard_content'],
                            text: 'Only guard content',
                        },
                    },
                ],
            }));
            // Verify warning was logged
            expect(warnSpy).toHaveBeenCalledWith('guard content is not supported in openai system prompts, removing guard content block');
            // Verify no system message added (only guard content)
            expect(captured.request).toBeDefined();
            expect(captured.request.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }]);
            warnSpy.mockRestore();
        });
    });
    describe('guard content in messages', () => {
        it('warns and filters guard content from user messages', async () => {
            const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [
                new Message({
                    role: 'user',
                    content: [
                        new TextBlock('Verify this:'),
                        new GuardContentBlock({
                            text: {
                                qualifiers: ['grounding_source'],
                                text: 'Guard content',
                            },
                        }),
                        new TextBlock('Is it correct?'),
                    ],
                }),
            ];
            await collectIterator(provider.stream(messages));
            // Verify warning was logged
            expect(warnSpy).toHaveBeenCalledWith('block_type=<guardContentBlock> | unsupported content type in openai user message | skipping');
            // Verify guard content filtered out
            expect(captured.request).toBeDefined();
            expect(captured.request.messages).toEqual([
                {
                    role: 'user',
                    content: [
                        { type: 'text', text: 'Verify this:' },
                        { type: 'text', text: 'Is it correct?' },
                    ],
                },
            ]);
            warnSpy.mockRestore();
        });
        it('warns and filters guard content with image from user messages', async () => {
            const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const imageBytes = new Uint8Array([1, 2, 3, 4]);
            const messages = [
                new Message({
                    role: 'user',
                    content: [
                        new TextBlock('Check this image:'),
                        new GuardContentBlock({
                            image: {
                                format: 'jpeg',
                                source: { bytes: imageBytes },
                            },
                        }),
                    ],
                }),
            ];
            await collectIterator(provider.stream(messages));
            // Verify warning was logged
            expect(warnSpy).toHaveBeenCalledWith('block_type=<guardContentBlock> | unsupported content type in openai user message | skipping');
            // Verify guard content filtered out
            expect(captured.request).toBeDefined();
            expect(captured.request.messages).toEqual([
                { role: 'user', content: [{ type: 'text', text: 'Check this image:' }] },
            ]);
            warnSpy.mockRestore();
        });
        it('handles message with only guard content', async () => {
            const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [
                new Message({
                    role: 'user',
                    content: [
                        new GuardContentBlock({
                            text: {
                                qualifiers: ['guard_content'],
                                text: 'Only guard content',
                            },
                        }),
                    ],
                }),
            ];
            await collectIterator(provider.stream(messages));
            // Verify warning was logged
            expect(warnSpy).toHaveBeenCalledWith('block_type=<guardContentBlock> | unsupported content type in openai user message | skipping');
            // Verify no user message added (only guard content)
            expect(captured.request).toBeDefined();
            expect(captured.request.messages).toEqual([]);
            warnSpy.mockRestore();
        });
    });
    describe('media blocks', () => {
        it('formats image block in user message as image_url with base64', async () => {
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const imageBytes = new Uint8Array([72, 101, 108, 108, 111]);
            const messages = [
                new Message({
                    role: 'user',
                    content: [
                        new TextBlock('What is in this image?'),
                        new ImageBlock({ format: 'png', source: { bytes: imageBytes } }),
                    ],
                }),
            ];
            await collectIterator(provider.stream(messages));
            const userMsg = captured.request.messages[0];
            expect(userMsg.role).toBe('user');
            expect(userMsg.content).toHaveLength(2);
            expect(userMsg.content[0]).toEqual({ type: 'text', text: 'What is in this image?' });
            expect(userMsg.content[1]).toEqual({
                type: 'image_url',
                image_url: { url: 'data:image/png;base64,SGVsbG8=' },
            });
        });
        it('formats image block in user message with URL source', async () => {
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [
                new Message({
                    role: 'user',
                    content: [new ImageBlock({ format: 'jpeg', source: { url: 'https://example.com/img.jpg' } })],
                }),
            ];
            await collectIterator(provider.stream(messages));
            const userMsg = captured.request.messages[0];
            expect(userMsg.content[0]).toEqual({
                type: 'image_url',
                image_url: { url: 'https://example.com/img.jpg' },
            });
        });
        it('formats document block with bytes source as file in user message', async () => {
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const docBytes = new Uint8Array([1, 2, 3]);
            const messages = [
                new Message({
                    role: 'user',
                    content: [new DocumentBlock({ name: 'report.pdf', format: 'pdf', source: { bytes: docBytes } })],
                }),
            ];
            await collectIterator(provider.stream(messages));
            const userMsg = captured.request.messages[0];
            expect(userMsg.content[0]).toEqual({
                type: 'file',
                file: { file_data: 'data:application/pdf;base64,AQID', filename: 'report.pdf' },
            });
        });
        it('splits image from tool result into separate user message', async () => {
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const imageBytes = new Uint8Array([72, 101, 108, 108, 111]);
            const messages = [
                new Message({
                    role: 'user',
                    content: [
                        new ToolResultBlock({
                            toolUseId: 'tool-1',
                            status: 'success',
                            content: [
                                new TextBlock('Screenshot captured'),
                                new ImageBlock({ format: 'png', source: { bytes: imageBytes } }),
                            ],
                        }),
                    ],
                }),
            ];
            await collectIterator(provider.stream(messages));
            // Tool message with text only
            const toolMsg = captured.request.messages[0];
            expect(toolMsg.role).toBe('tool');
            expect(toolMsg.tool_call_id).toBe('tool-1');
            expect(toolMsg.content).toBe('Screenshot captured');
            // Separate user message with image
            const userMsg = captured.request.messages[1];
            expect(userMsg.role).toBe('user');
            expect(userMsg.content[0]).toEqual({
                type: 'image_url',
                image_url: { url: 'data:image/png;base64,SGVsbG8=' },
            });
        });
        it('injects placeholder text when tool result contains only images', async () => {
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [
                new Message({
                    role: 'user',
                    content: [
                        new ToolResultBlock({
                            toolUseId: 'tool-1',
                            status: 'success',
                            content: [new ImageBlock({ format: 'png', source: { bytes: new Uint8Array([1]) } })],
                        }),
                    ],
                }),
            ];
            await collectIterator(provider.stream(messages));
            const toolMsg = captured.request.messages[0];
            expect(toolMsg.content).toContain('Tool successfully returned an image');
        });
        it('skips document block in tool result with warning', async () => {
            const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [
                new Message({
                    role: 'user',
                    content: [
                        new ToolResultBlock({
                            toolUseId: 'tool-1',
                            status: 'success',
                            content: [
                                new TextBlock('result'),
                                new DocumentBlock({ name: 'doc.pdf', format: 'pdf', source: { bytes: new Uint8Array([1]) } }),
                            ],
                        }),
                    ],
                }),
            ];
            await collectIterator(provider.stream(messages));
            const toolMsg = captured.request.messages[0];
            expect(toolMsg.content).toBe('result');
            expect(warnSpy).toHaveBeenCalled();
            warnSpy.mockRestore();
        });
        it('skips video block in tool result with warning', async () => {
            const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
            const captured = { request: null };
            const mockClient = createMockClientWithCapture(captured);
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [
                new Message({
                    role: 'user',
                    content: [
                        new ToolResultBlock({
                            toolUseId: 'tool-1',
                            status: 'success',
                            content: [
                                new TextBlock('result'),
                                new VideoBlock({ format: 'mp4', source: { bytes: new Uint8Array([1]) } }),
                            ],
                        }),
                    ],
                }),
            ];
            await collectIterator(provider.stream(messages));
            const toolMsg = captured.request.messages[0];
            expect(toolMsg.content).toBe('result');
            expect(warnSpy).toHaveBeenCalled();
            warnSpy.mockRestore();
        });
    });
    describe('error handling', () => {
        it('throws ContextWindowOverflowError for structured error with code', async () => {
            const mockClient = {
                chat: {
                    completions: {
                        create: vi.fn(async () => {
                            const error = new Error('Context length exceeded');
                            error.code = 'context_length_exceeded';
                            throw error;
                        }),
                    },
                },
            };
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            await expect(async () => {
                for await (const _ of provider.stream(messages)) {
                    // Should not reach here
                }
            }).rejects.toThrow(ContextWindowOverflowError);
        });
        it('throws ContextWindowOverflowError for error with message pattern', async () => {
            const mockClient = {
                chat: {
                    completions: {
                        create: vi.fn(async () => {
                            throw new Error('maximum context length exceeded');
                        }),
                    },
                },
            };
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            await expect(async () => {
                for await (const _ of provider.stream(messages)) {
                    // Should not reach here
                }
            }).rejects.toThrow(ContextWindowOverflowError);
        });
        it('throws ContextWindowOverflowError for APIError instance', async () => {
            const mockClient = {
                chat: {
                    completions: {
                        create: vi.fn(async () => {
                            // Simulate APIError from openai package
                            const error = new Error('Context length exceeded');
                            error.name = 'APIError';
                            error.status = 400;
                            error.code = 'context_length_exceeded';
                            // Make it behave like an APIError instance
                            Object.setPrototypeOf(error, Error.prototype);
                            throw error;
                        }),
                    },
                },
            };
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            await expect(async () => {
                for await (const _ of provider.stream(messages)) {
                    // Should not reach here
                }
            }).rejects.toThrow(ContextWindowOverflowError);
        });
        it('passes through other errors unchanged', async () => {
            const mockClient = {
                chat: {
                    completions: {
                        create: vi.fn(async () => {
                            throw new Error('Invalid API key');
                        }),
                    },
                },
            };
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            await expect(async () => {
                for await (const _ of provider.stream(messages)) {
                    // Should not reach here
                }
            }).rejects.toThrow('Invalid API key');
        });
        it('handles stream interruption errors', async () => {
            const mockClient = createMockClient(async function* () {
                yield { choices: [{ delta: { role: 'assistant' }, index: 0 }] };
                yield { choices: [{ delta: { content: 'Hello' }, index: 0 }] };
                // Stream interruption
                throw new Error('Network connection lost');
            });
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            await expect(async () => {
                for await (const _ of provider.stream(messages)) {
                    // Stream will be interrupted
                }
            }).rejects.toThrow('Network connection lost');
        });
        it('throws ModelThrottledError for HTTP 429 status', async () => {
            const originalError = new Error('Too many requests');
            originalError.status = 429;
            const mockClient = {
                chat: {
                    completions: {
                        create: vi.fn(async () => {
                            throw originalError;
                        }),
                    },
                },
            };
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            await expect(async () => {
                for await (const _ of provider.stream(messages)) {
                    // Should not reach here
                }
            }).rejects.toThrow(ModelThrottledError);
        });
        it('throws ModelThrottledError for rate_limit_exceeded error code', async () => {
            const originalError = new Error('Rate limit reached');
            originalError.code = 'rate_limit_exceeded';
            const mockClient = {
                chat: {
                    completions: {
                        create: vi.fn(async () => {
                            throw originalError;
                        }),
                    },
                },
            };
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            await expect(async () => {
                for await (const _ of provider.stream(messages)) {
                    // Should not reach here
                }
            }).rejects.toThrow(ModelThrottledError);
        });
        it('throws ModelThrottledError for error message containing rate limit pattern', async () => {
            const mockClient = {
                chat: {
                    completions: {
                        create: vi.fn(async () => {
                            throw new Error('You have exceeded your rate limit');
                        }),
                    },
                },
            };
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            await expect(async () => {
                for await (const _ of provider.stream(messages)) {
                    // Should not reach here
                }
            }).rejects.toThrow(ModelThrottledError);
        });
        it('throws ModelThrottledError for too many requests message', async () => {
            const mockClient = {
                chat: {
                    completions: {
                        create: vi.fn(async () => {
                            throw new Error('Too many requests, please slow down');
                        }),
                    },
                },
            };
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            await expect(async () => {
                for await (const _ of provider.stream(messages)) {
                    // Should not reach here
                }
            }).rejects.toThrow(ModelThrottledError);
        });
        it('preserves original error as cause in ModelThrottledError', async () => {
            const originalError = new Error('Request too large for gpt-5.4 on tokens per min');
            originalError.status = 429;
            const mockClient = {
                chat: {
                    completions: {
                        create: vi.fn(async () => {
                            throw originalError;
                        }),
                    },
                },
            };
            const provider = new OpenAIModel({ api: 'chat', client: mockClient });
            const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })];
            try {
                for await (const _ of provider.stream(messages)) {
                    // Should not reach here
                }
                expect.fail('Should have thrown');
            }
            catch (error) {
                expect(error).toBeInstanceOf(ModelThrottledError);
                const throttleError = error;
                expect(throttleError.cause).toBe(originalError);
                expect(throttleError.message).toBe('Request too large for gpt-5.4 on tokens per min');
            }
        });
    });
});
//# sourceMappingURL=chat.test.js.map