/** * Azure AI Foundry OpenAI-compatible provider. * * Azure Foundry endpoints used by this repo expose an OpenAI-compatible * `/chat/completions` API when `LLM_BASE_URL` points at the resource's * `/openai/v1` base URL. Request/stream parsing is therefore identical to the * OpenAI provider. Azure resource keys use the `api-key` header, and there is * no safe global default host for Azure deployments, so baseUrl must be * injected by the key provider. * * Filename `llm.azure_foundry.js` registers provider key `azure_foundry`. */ import { LLMError } from '../errors.js' import OpenAIProvider from './llm.openai.js' export default class AzureFoundryProvider extends OpenAIProvider { /** * Azure 将内容安全拦截作为 HTTP 400 返回。对剧情运行时,这通常是供应商对完整 prompt * 或生成方向的拒绝,不等价于玩家请求参数错误;允许路由层切到下一个供应商。 * @protected * @param {Response} res * @param {unknown} body * @param {string} reason */ httpErrorInfo(res, body, reason) { if (isContentFilterError(body)) { return { reason: 'contentFilter', retryable: true, message: '[azure_foundry] content filtered', provider_request_id: res.headers.get('x-request-id'), } } return super.httpErrorInfo(res, body, reason) } /** * @protected * @param {import('../index.js').KeyLease} lease * @returns {Record} */ chatCompletionsHeaders(lease) { return { 'content-type': 'application/json', 'api-key': lease.api_key, } } /** * @protected * @param {string|undefined} model * @returns {string} */ maxTokensParamName(model) { return isGpt5Model(model) ? 'max_completion_tokens' : 'max_tokens' } /** * Azure Foundry OpenAI v1 rejects the non-standard `thinking` parameter. * @protected * @returns {boolean} */ supportsThinkingParam() { return false } /** @returns {string} */ defaultBaseUrl() { throw new LLMError({ provider: this.provider, reason: 'badRequest', retryable: false, message: 'azure_foundry requires LLM_BASE_URL, for example https://.services.ai.azure.com/openai/v1', }) } } /** * @param {string|undefined} model * @returns {boolean} */ function isGpt5Model(model) { return /^gpt-5(?:[.-]|$)/i.test(String(model || '')) } function isContentFilterError(body) { if (!body || Object.getPrototypeOf(body) !== Object.prototype) return false const error = body.error if (!error || Object.getPrototypeOf(error) !== Object.prototype) return false return error.code === 'content_filter' }