/**
 * OpenAI model provider implementation.
 *
 * Supports both the Responses API (default) and the Chat Completions API.
 * Selected via the `api` option at construction time.
 *
 * @see https://platform.openai.com/docs/api-reference/responses
 * @see https://platform.openai.com/docs/api-reference/chat
 */
import OpenAI from 'openai';
import { Model, resolveConfigMetadata } from '../model.js';
import { ContextWindowOverflowError, ModelThrottledError } from '../../errors.js';
import { logger } from '../../logging/logger.js';
import { warnOnce } from '../../logging/warn-once.js';
import { MODEL_DEFAULTS, defaultModelWarningMessage } from '../defaults.js';
import { bedrockMantleBaseUrl, createMantleApiKeySetter, resolveMantleRegion } from './mantle.js';
import { classifyOpenAIError } from './errors.js';
import { formatChatRequest, mapChatChunkToEvents, warnManagedParams as warnChatManagedParams } from './chat-adapter.js';
import { createResponsesStreamState, finalizeResponsesStream, formatResponsesRequest, mapResponsesEventToSDK, warnManagedParams as warnResponsesManagedParams, } from './responses-adapter.js';
/**
 * OpenAI model provider.
 *
 * Defaults to the Responses API. Pass `api: 'chat'` to use Chat Completions.
 * The `api` field is construction-only — it cannot be changed via
 * {@link OpenAIModel.updateConfig}.
 *
 * @example
 * ```typescript
 * // Responses API (default)
 * const model = new OpenAIModel({ modelId: 'gpt-5.4', apiKey: 'sk-...' })
 * ```
 *
 * @example
 * ```typescript
 * // Chat Completions
 * const model = new OpenAIModel({ api: 'chat', modelId: 'gpt-5.4', apiKey: 'sk-...' })
 * ```
 *
 * @example
 * ```typescript
 * // Responses API with built-in web search
 * const model = new OpenAIModel({
 *   modelId: 'gpt-5.4',
 *   params: { tools: [{ type: 'web_search' }] },
 * })
 * ```
 */
export class OpenAIModel extends Model {
    _api;
    _config;
    _client;
    constructor(options) {
        super();
        const { apiKey, client, clientConfig, bedrockMantleConfig, api = 'responses', ...modelConfig } = options;
        if (api !== 'chat' && api !== 'responses') {
            throw new Error(`Unsupported OpenAI API: '${api}'. Supported values: 'chat', 'responses'`);
        }
        this._api = api;
        // `stateful` only exists on the responses branch of the discriminated union.
        // Storing as the merged OpenAIModelConfig matches what `getConfig` returns.
        this._config = modelConfig;
        if (modelConfig.modelId === undefined) {
            warnOnce(logger, defaultModelWarningMessage(MODEL_DEFAULTS.openai.modelId));
        }
        if (api === 'responses') {
            warnResponsesManagedParams(modelConfig.params);
        }
        else {
            warnChatManagedParams(modelConfig.params);
        }
        if (bedrockMantleConfig && client) {
            throw new Error("'bedrockMantleConfig' cannot be combined with a pre-built 'client'.");
        }
        if (client) {
            this._client = client;
        }
        else if (bedrockMantleConfig) {
            this._client = buildMantleClient(bedrockMantleConfig, apiKey, clientConfig);
        }
        else {
            const hasEnvKey = typeof process !== 'undefined' && typeof process.env !== 'undefined' && process.env.OPENAI_API_KEY;
            if (!apiKey && !hasEnvKey) {
                throw new Error("OpenAI API key is required. Provide it via the 'apiKey' option (string or function) or set the OPENAI_API_KEY environment variable.");
            }
            this._client = new OpenAI({
                ...(apiKey ? { apiKey } : {}),
                ...clientConfig,
            });
        }
    }
    /**
     * The OpenAI API mode this model operates in (`'chat'` or `'responses'`).
     * Set at construction and immutable; exposed for debugging and serialization.
     */
    get api() {
        return this._api;
    }
    /**
     * Whether this model manages conversation state server-side.
     *
     * `true` only for `api: 'responses'` with `stateful === true`. Chat Completions
     * is always stateless, and Responses defaults to stateless.
     */
    get stateful() {
        return this._api === 'responses' && this._config.stateful === true;
    }
    /**
     * Updates the model configuration.
     *
     * `api` and `stateful` are construction-only — if present in `modelConfig`,
     * they are stripped with a warning. Changing either at runtime would
     * invalidate the invariants the agent builds on top of `stateful` (message
     * history management, `previous_response_id` chaining).
     */
    updateConfig(modelConfig) {
        const { api, stateful, ...rest } = modelConfig;
        if (api !== undefined) {
            logger.warn(`api=<${api}> | 'api' is construction-only and cannot be changed via updateConfig — ignoring`);
        }
        if (stateful !== undefined) {
            logger.warn(`stateful=<${stateful}> | 'stateful' is construction-only and cannot be changed via updateConfig — ignoring`);
        }
        if (this._api === 'responses') {
            warnResponsesManagedParams(rest.params);
        }
        else {
            warnChatManagedParams(rest.params);
        }
        this._config = { ...this._config, ...rest };
    }
    getConfig() {
        return resolveConfigMetadata(this._config, this._config.modelId ?? MODEL_DEFAULTS.openai.modelId);
    }
    async *stream(messages, options) {
        if (!messages || messages.length === 0) {
            throw new Error('At least one message is required');
        }
        if (this._api === 'chat') {
            yield* this._streamChat(messages, options);
        }
        else {
            yield* this._streamResponses(messages, options);
        }
    }
    async *_streamChat(messages, options) {
        try {
            const request = formatChatRequest(this._config, messages, options);
            const stream = await this._client.chat.completions.create(request);
            const streamState = {
                messageStarted: false,
                textContentBlockStarted: false,
            };
            const activeToolCalls = new Map();
            let bufferedUsage = null;
            for await (const chunk of stream) {
                if (!chunk.choices || chunk.choices.length === 0) {
                    if (chunk.usage) {
                        bufferedUsage = {
                            type: 'modelMetadataEvent',
                            usage: {
                                inputTokens: chunk.usage.prompt_tokens ?? 0,
                                outputTokens: chunk.usage.completion_tokens ?? 0,
                                totalTokens: chunk.usage.total_tokens ?? 0,
                            },
                        };
                    }
                    continue;
                }
                const events = mapChatChunkToEvents(chunk, streamState, activeToolCalls);
                for (const event of events) {
                    if (event.type === 'modelMessageStopEvent' && bufferedUsage) {
                        yield bufferedUsage;
                        bufferedUsage = null;
                    }
                    yield event;
                }
            }
            if (bufferedUsage) {
                yield bufferedUsage;
            }
        }
        catch (error) {
            throw this._rewrapError(error);
        }
    }
    async *_streamResponses(messages, options) {
        try {
            const request = formatResponsesRequest(this._config, messages, options, this.stateful);
            const stream = await this._client.responses.create(request);
            const state = createResponsesStreamState();
            for await (const event of stream) {
                for (const sdkEvent of mapResponsesEventToSDK(event, state, this.stateful, options?.modelState)) {
                    yield sdkEvent;
                }
            }
            for (const sdkEvent of finalizeResponsesStream(state)) {
                yield sdkEvent;
            }
        }
        catch (error) {
            throw this._rewrapError(error);
        }
    }
    _rewrapError(error) {
        const err = error;
        const kind = classifyOpenAIError(err);
        if (kind === 'throttling') {
            const message = err.message ?? 'Request was throttled by the model provider';
            logger.debug(`throttled | error_message=<${message}>`);
            return new ModelThrottledError(message, { cause: err });
        }
        if (kind === 'contextOverflow') {
            return new ContextWindowOverflowError(err.message);
        }
        return error;
    }
}
function buildMantleClient(bedrockMantleConfig, apiKey, clientConfig) {
    if (apiKey !== undefined) {
        throw new Error("'apiKey' cannot be combined with 'bedrockMantleConfig'; the API key is derived from the Mantle config automatically.");
    }
    const conflicting = [];
    if (clientConfig?.apiKey !== undefined)
        conflicting.push('apiKey');
    if (clientConfig?.baseURL !== undefined)
        conflicting.push('baseURL');
    if (conflicting.length > 0) {
        throw new Error(`clientConfig must not contain ${conflicting.join(', ')} when bedrockMantleConfig is set; ` +
            'these are derived from the Mantle config automatically.');
    }
    // Resolve the region eagerly so missing-region configuration fails fast.
    const region = resolveMantleRegion(bedrockMantleConfig);
    return new OpenAI({
        ...clientConfig,
        baseURL: bedrockMantleBaseUrl(region),
        apiKey: createMantleApiKeySetter(bedrockMantleConfig, region),
    });
}
//# sourceMappingURL=model.js.map