From dafcb7c2992c4f540afe57fe69e3c54271d799ec Mon Sep 17 00:00:00 2001 From: almahirp Date: Tue, 7 Jul 2026 16:46:59 -0500 Subject: [PATCH] feat(ai): add support for Snowflake Cortex and OpenAI gateways Added environment variables to allow routing traffic to custom endpoints like Snowflake Cortex, without altering the default behavior for standard OpenAI users. Changes included: - Support for OPENAI_BASE_URL and OPENAI_EXTRA_HEADERS_JSON to override the default api.openai.com endpoint and pass required custom headers. - Added FINALRUN_OPENAI_SNOWFLAKE_COMPAT=1 flag to intercept the underlying fetch call and map max_tokens to max_completion_tokens (required by Snowflake Cortex). - Added FINALRUN_OPENAI_USE_CHAT=1 flag to force the use of Chat Completions (openai.chat) instead of the default Responses API (openai.responses), avoiding 403 errors on /v1/responses for gateways that only support the chat-compatible endpoint. The default behavior for standard OpenAI usage is kept exactly the same. --- packages/goal-executor/src/ai/AIAgent.ts | 68 +++++++++++++++++++++--- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/packages/goal-executor/src/ai/AIAgent.ts b/packages/goal-executor/src/ai/AIAgent.ts index 6a72dabc..293185d4 100644 --- a/packages/goal-executor/src/ai/AIAgent.ts +++ b/packages/goal-executor/src/ai/AIAgent.ts @@ -663,12 +663,68 @@ export class AIAgent { let client: unknown; switch (resolved.provider) { case 'openai': { - const openai = createOpenAI({ apiKey }); - // Use the Responses API (not Chat Completions) so that - // `providerOptions.openai.reasoningEffort` is honored by reasoning - // models like gpt-5.4-mini. `openai(modelId)` defaults to Chat - // Completions and silently ignores reasoning effort. - client = openai.responses(resolved.modelName); + const openaiOptions: Parameters[0] = { apiKey }; + + // Explicit endpoint override — required when talking to Snowflake + // Cortex (or any OpenAI-compatible gateway) so we don't silently hit + // api.openai.com. `@ai-sdk/openai` also reads OPENAI_BASE_URL from env, + // but setting it here makes the routing explicit. + const baseURL = process.env.OPENAI_BASE_URL; + if (baseURL) { + openaiOptions.baseURL = baseURL; + } + + const extraHeadersJson = process.env.OPENAI_EXTRA_HEADERS_JSON; + if (extraHeadersJson) { + try { + const parsed = JSON.parse(extraHeadersJson); + if (parsed && typeof parsed === 'object') { + openaiOptions.headers = parsed as Record; + } + } catch (err) { + console.error( + 'OPENAI_EXTRA_HEADERS_JSON is not valid JSON; ignoring.', + err, + ); + } + } + + if (process.env.FINALRUN_OPENAI_SNOWFLAKE_COMPAT === '1') { + const baseFetch: typeof fetch = + (openaiOptions.fetch as typeof fetch | undefined) ?? fetch; + openaiOptions.fetch = (async (input, init) => { + if (init && typeof init.body === 'string') { + try { + const payload = JSON.parse(init.body); + if ( + payload && + typeof payload === 'object' && + 'max_tokens' in payload && + !('max_completion_tokens' in payload) + ) { + payload.max_completion_tokens = payload.max_tokens; + delete payload.max_tokens; + init = { ...init, body: JSON.stringify(payload) }; + } + } catch { + // body is not JSON; pass through unchanged + } + } + return baseFetch(input as Parameters[0], init); + }) as typeof fetch; + } + + const openai = createOpenAI(openaiOptions); + + // Responses API is the default so reasoning models (gpt-5.4-mini, etc.) + // honor `providerOptions.openai.reasoningEffort`. Set + // FINALRUN_OPENAI_USE_CHAT=1 to force Chat Completions instead — needed + // for Snowflake Cortex, which only exposes the OpenAI-chat-compatible + // endpoint and 403s on /v1/responses. + client = + process.env.FINALRUN_OPENAI_USE_CHAT === '1' + ? openai.chat(resolved.modelName) + : openai.responses(resolved.modelName); break; } case 'google': {