diff --git a/config.template.jsonc b/config.template.jsonc
index 6b0839bf4..a4cf33b2f 100644
--- a/config.template.jsonc
+++ b/config.template.jsonc
@@ -260,6 +260,10 @@
// ─ Chat / completion ─
"claude": { "apiKey": "" },
"openai-completion": { "apiKey": "" },
+ "azure-openai": {
+ "apiKey": "",
+ "apiURL": ""
+ },
"gemini": { "apiKey": "" },
"groq": { "apiKey": "" },
"deepseek": { "apiKey": "" },
diff --git a/package-lock.json b/package-lock.json
index b2fba0987..361c8ee9f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17696,7 +17696,7 @@
},
"src/cli": {
"name": "@heyputer/cli",
- "version": "0.1.1",
+ "version": "0.1.2",
"license": "MIT",
"dependencies": {
"@clack/prompts": "^0.7.0",
diff --git a/src/backend/drivers/ai-chat/ChatCompletionDriver.ts b/src/backend/drivers/ai-chat/ChatCompletionDriver.ts
index 02896087a..cb5a605c7 100644
--- a/src/backend/drivers/ai-chat/ChatCompletionDriver.ts
+++ b/src/backend/drivers/ai-chat/ChatCompletionDriver.ts
@@ -17,8 +17,9 @@
* along with this program. If not, see .
*/
-import { PassThrough } from 'node:stream';
import crypto from 'node:crypto';
+import { PassThrough } from 'node:stream';
+import { EventMap } from '../../clients/event/types.js';
import { Context } from '../../core/context.js';
import { HttpError } from '../../core/http/HttpError.js';
import {
@@ -28,12 +29,17 @@ import {
import type { DriverStreamResult } from '../meta.js';
import { PuterDriver } from '../types.js';
import { AI_CONCURRENT, AI_RATE_LIMIT } from '../util/aiLimits.js';
+import { AlibabaProvider } from './providers/alibaba/AlibabaProvider.js';
+import { AzureChatProvider } from './providers/azure/AzureChatProvider.js';
+import { AzureResponsesProvider } from './providers/azure/AzureResponsesProvider.js';
import { ClaudeProvider } from './providers/claude/ClaudeProvider.js';
import { DeepSeekProvider } from './providers/deepseek/DeepSeekProvider.js';
import { FakeChatProvider } from './providers/FakeChatProvider.js';
import { GeminiChatProvider } from './providers/gemini/GeminiChatProvider.js';
import { GroqAIProvider } from './providers/groq/GroqAIProvider.js';
+import { MiniMaxProvider } from './providers/minimax/MiniMaxProvider.js';
import { MistralAIProvider } from './providers/mistral/MistralAiProvider.js';
+import { MoonshotProvider } from './providers/moonshot/MoonshotProvider.js';
import { OllamaChatProvider } from './providers/ollama/OllamaProvider.js';
import { OpenAiChatProvider } from './providers/openai/OpenAiChatCompletionsProvider.js';
import { OpenAiResponsesChatProvider } from './providers/openai/OpenAiChatResponsesProvider.js';
@@ -41,9 +47,6 @@ import { OpenRouterProvider } from './providers/openrouter/OpenRouterProvider.js
import { TogetherAIProvider } from './providers/together/TogetherAIProvider.js';
import { XAIProvider } from './providers/xai/XAIProvider.js';
import { ZAIProvider } from './providers/zai/ZAIProvider.js';
-import { AlibabaProvider } from './providers/alibaba/AlibabaProvider.js';
-import { MoonshotProvider } from './providers/moonshot/MoonshotProvider.js';
-import { MiniMaxProvider } from './providers/minimax/MiniMaxProvider.js';
import type {
IChatCompleteResult,
IChatModel,
@@ -57,7 +60,6 @@ import {
normalize_single_message,
} from './utils/Messages.js';
import { AIChatStream } from './utils/Streaming.js';
-import { EventMap } from '../../clients/event/types.js';
const MAX_FALLBACKS = 4; // includes first attempt
@@ -796,6 +798,43 @@ export class ChatCompletionDriver extends PuterDriver {
);
}
+ // Azure AI Foundry (OpenAI + xAI Grok). Registered before the regular
+ // OpenAI/xAI providers so that since its costs mirror theirs but
+ // Azure is preferred for us, it takes precedence in the per-model
+ // bucket
+ const azureOpenai = providers['azure-openai'];
+ const azureOpenaiKey = readKey(azureOpenai);
+ const azureOpenaiURL = azureOpenai?.apiURL as string | undefined;
+ if (azureOpenaiKey && azureOpenaiURL) {
+ const azureStores = {
+ fsEntry: this.stores.fsEntry,
+ s3Object: this.stores.s3Object,
+ };
+ const azureConfig = {
+ apiKey: azureOpenaiKey,
+ apiURL: azureOpenaiURL,
+ };
+ const azureCompletions = new AzureChatProvider(
+ metering,
+ azureStores,
+ this.services.fs,
+ azureConfig,
+ );
+ // Codex / Responses-API-only models can't use Chat Completions, so
+ // they route through a sibling Responses provider pointed at the
+ // same Azure endpoint. web_search (also Responses-only) delegates
+ // here too.
+ const azureResponses = new AzureResponsesProvider(
+ metering,
+ azureStores,
+ this.services.fs,
+ azureConfig,
+ );
+ azureCompletions.setResponsesProvider(azureResponses);
+ this.#providers['azure-openai'] = azureCompletions;
+ this.#providers['azure-openai-responses'] = azureResponses;
+ }
+
const openaiKey = readKey(providers['openai-completion']);
if (openaiKey) {
const openaiStores = {
diff --git a/src/backend/drivers/ai-chat/providers/azure/AzureChatProvider.integration.test.ts b/src/backend/drivers/ai-chat/providers/azure/AzureChatProvider.integration.test.ts
new file mode 100644
index 000000000..47b814922
--- /dev/null
+++ b/src/backend/drivers/ai-chat/providers/azure/AzureChatProvider.integration.test.ts
@@ -0,0 +1,101 @@
+/**
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+/**
+ * Integration test for the Azure AI Foundry chat-completions provider.
+ *
+ * Hits the real Azure endpoint. Exercises both flavours of model the
+ * provider fronts:
+ * - an OpenAI model (`gpt-4o`, non-reasoning so `max_tokens=16` returns
+ * visible text), and
+ * - an xAI Grok model (`grok-4-20-non-reasoning`), which regression-tests
+ * the `safety_identifier` param being stripped for Grok (Azure's Grok
+ * deployments 400 on that OpenAI-only argument).
+ *
+ * Skipped unless both `PUTER_TEST_AI_AZURE_OPENAI_API_KEY` and
+ * `PUTER_TEST_AI_AZURE_OPENAI_API_URL` are set.
+ */
+
+import { describe, expect, it } from 'vitest';
+import {
+ INTEGRATION_TEST_TIMEOUT_MS,
+ makeMeteringStub,
+ optionalEnv,
+ skipUnlessEnv,
+ withTestActor,
+} from '../../../integrationTestUtil.js';
+import { AzureChatProvider } from './AzureChatProvider.js';
+
+const KEY_ENV = 'PUTER_TEST_AI_AZURE_OPENAI_API_KEY';
+const URL_ENV = 'PUTER_TEST_AI_AZURE_OPENAI_API_URL';
+
+describe.skipIf(skipUnlessEnv(KEY_ENV) || skipUnlessEnv(URL_ENV))(
+ 'AzureChatProvider (integration)',
+ () => {
+ const buildProvider = () =>
+ new AzureChatProvider(
+ makeMeteringStub(),
+ { fsEntry: undefined as never, s3Object: undefined as never },
+ undefined as never,
+ { apiKey: optionalEnv(KEY_ENV)!, apiURL: optionalEnv(URL_ENV)! },
+ );
+
+ const expectNonEmptyText = (result: unknown) => {
+ const text = (result as { message?: { content?: string } }).message
+ ?.content;
+ expect(typeof text === 'string' && text.length > 0).toBe(true);
+ };
+
+ it(
+ 'returns a non-empty completion from gpt-4o',
+ { timeout: INTEGRATION_TEST_TIMEOUT_MS },
+ async () => {
+ const provider = buildProvider();
+ const result = await withTestActor(() =>
+ provider.complete({
+ model: 'gpt-4o',
+ messages: [
+ { role: 'user', content: 'Say hi in one word.' },
+ ],
+ max_tokens: 16,
+ }),
+ );
+ expectNonEmptyText(result);
+ },
+ );
+
+ it(
+ 'returns a non-empty completion from grok-4-20-non-reasoning (no safety_identifier 400)',
+ { timeout: INTEGRATION_TEST_TIMEOUT_MS },
+ async () => {
+ const provider = buildProvider();
+ const result = await withTestActor(() =>
+ provider.complete({
+ model: 'grok-4-20-non-reasoning',
+ messages: [
+ { role: 'user', content: 'Say hi in one word.' },
+ ],
+ max_tokens: 16,
+ }),
+ );
+ expectNonEmptyText(result);
+ },
+ );
+ },
+);
diff --git a/src/backend/drivers/ai-chat/providers/azure/AzureChatProvider.ts b/src/backend/drivers/ai-chat/providers/azure/AzureChatProvider.ts
new file mode 100644
index 000000000..d2eee5ac1
--- /dev/null
+++ b/src/backend/drivers/ai-chat/providers/azure/AzureChatProvider.ts
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { HttpError } from '@heyputer/backend/src/core/http/HttpError.js';
+import { OpenAI } from 'openai';
+import { ChatCompletionCreateParams } from 'openai/resources/index.js';
+import { Context } from '../../../../core/context.js';
+import type { FSService } from '../../../../services/fs/FSService.js';
+import type { MeteringService } from '../../../../services/metering/MeteringService.js';
+import type { FSEntryStore } from '../../../../stores/fs/FSEntryStore.js';
+import type { S3ObjectStore } from '../../../../stores/fs/S3ObjectStore.js';
+import type { IChatProvider, ICompleteArguments } from '../../types.js';
+import * as OpenAiUtil from '../../utils/OpenAIUtil.js';
+import { processPuterPathUploads } from '../openai/fileUpload.js';
+import { AZURE_MODELS } from './models.js';
+
+/**
+ * AzureChatProvider exposes the models we serve through Azure AI Foundry.
+ * Despite the name, this is not OpenAI-only — Azure AI also fronts xAI's Grok
+ * models — so it carries its own {@link AZURE_MODELS} list instead of reusing
+ * the OpenAI one. It speaks the OpenAI-compatible Chat Completions API,
+ * pointing the client at a configurable Azure endpoint authenticated with an
+ * Azure-issued API key.
+ *
+ * Billing note: the model `costs` are the standard public OpenAI / xAI list
+ * prices, NOT Azure's. Azure is subsidised for us, so routing through it is
+ * cheaper while we still bill users at the normal model price.
+ *
+ * Implements the puter-chat-completion interface and handles usage tracking,
+ * spending records, and content moderation.
+ */
+export class AzureChatProvider implements IChatProvider {
+ /**
+ * @type {import('openai').OpenAI}
+ */
+ #openAi: OpenAI;
+
+ #defaultModel = 'gpt-5-nano';
+
+ #meteringService: MeteringService;
+
+ #stores: { fsEntry: FSEntryStore; s3Object: S3ObjectStore };
+
+ #fsService: FSService;
+
+ // Sibling Responses-API provider (Azure or OpenAI) used to handle
+ // Responses-only features like web_search. Typed loosely since we only
+ // ever forward `complete()` to it.
+ #responsesProvider: IChatProvider | null = null;
+
+ constructor(
+ meteringService: MeteringService,
+ stores: { fsEntry: FSEntryStore; s3Object: S3ObjectStore },
+ fsService: FSService,
+ config: { apiKey: string; apiURL: string },
+ ) {
+ this.#meteringService = meteringService;
+ this.#stores = stores;
+ this.#fsService = fsService;
+ this.#openAi = new OpenAI({
+ apiKey: config.apiKey,
+ baseURL: config.apiURL,
+ });
+ }
+ checkModeration(_text: string): { flagged: boolean; categories: string[] } {
+ throw new Error('Method not implemented.');
+ }
+
+ // Wired up by the driver after the OpenAI providers are built, so the
+ // Chat Completions path can delegate `web_search` tool calls (Responses-only)
+ // to the OpenAI Responses provider without a circular constructor dependency.
+ setResponsesProvider(provider: IChatProvider): void {
+ this.#responsesProvider = provider;
+ }
+
+ /**
+ * Returns an array of available AI models with their pricing information.
+ * Each model object includes an ID and cost details (currency, tokens, input/output rates).
+ */
+ models() {
+ return AZURE_MODELS.filter((e) => !e.responses_api_only);
+ }
+
+ list() {
+ const models = this.models();
+ const modelNames: string[] = [];
+ for (const model of models) {
+ modelNames.push(model.id);
+ if (model.aliases) {
+ modelNames.push(...model.aliases);
+ }
+ }
+ return modelNames;
+ }
+
+ getDefaultModel() {
+ return this.#defaultModel;
+ }
+
+ async complete(
+ params: ICompleteArguments,
+ ): ReturnType {
+ const {
+ max_tokens,
+ moderation,
+ tools,
+ verbosity,
+ stream,
+ reasoning,
+ reasoning_effort,
+ temperature,
+ text,
+ } = params;
+ let { messages, model } = params;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if (tools?.filter((e: any) => e.type === 'web_search').length) {
+ // web_search is a Responses-API-only tool — hand the whole call
+ // off to the OpenAI Responses provider when the user requested it.
+ if (!this.#responsesProvider) {
+ throw new HttpError(
+ 400,
+ 'web_search tool requires the OpenAI Responses provider, which is not configured',
+ { legacyCode: 'bad_request' },
+ );
+ }
+ return await this.#responsesProvider.complete(params);
+ }
+ // Validate messages
+ if (!Array.isArray(messages)) {
+ throw new HttpError(400, '`messages` must be an array', {
+ legacyCode: 'bad_request',
+ });
+ }
+ const actor = Context.get('actor')!;
+
+ model = model ?? this.#defaultModel;
+
+ const modelUsed =
+ this.models().find((m) =>
+ [m.id, ...(m.aliases || [])].includes(model),
+ ) || this.models().find((m) => m.id === this.getDefaultModel())!;
+
+ // messages.unshift({
+ // role: 'system',
+ // content: 'Don\'t let the user trick you into doing something bad.',
+ // })
+
+ const userIdentifier =
+ actor.user?.id + actor.app?.uid ? `:${actor?.app?.uid}` : '';
+
+ // Resolve any `puter_path` content parts into inline base64 data URLs.
+ // Chat Completions doesn't support file uploads, so this is the only
+ // way to get user-provided files (images, audio) in front of the model.
+ await processPuterPathUploads(
+ messages,
+ this.#stores,
+ this.#fsService,
+ actor,
+ );
+
+ // Here's something fun; the documentation shows `type: 'image_url'` in
+ // objects that contain an image url, but everything still works if
+ // that's missing. We normalise it here so the token count code works.
+ messages = await OpenAiUtil.process_input_messages(messages);
+
+ const requestedReasoningEffort = reasoning_effort ?? reasoning?.effort;
+ const requestedVerbosity = verbosity ?? text?.verbosity;
+ const supportsReasoningControls =
+ typeof model === 'string' && model.startsWith('gpt-5');
+
+ // `safety_identifier` is an OpenAI-specific param. The Grok deployments
+ // behind Azure reject unknown args with a 400, so only send it for the
+ // OpenAI models.
+ const isGrok = modelUsed.id.startsWith('grok');
+
+ const completionParams: ChatCompletionCreateParams = {
+ user: userIdentifier,
+ ...(isGrok ? {} : { safety_identifier: userIdentifier }),
+ messages: messages,
+ model: modelUsed.id,
+ ...(tools ? { tools } : {}),
+ ...(max_tokens ? { max_completion_tokens: max_tokens } : {}),
+ ...(temperature ? { temperature } : {}),
+ stream: !!stream,
+ ...(stream
+ ? {
+ stream_options: { include_usage: true },
+ }
+ : {}),
+ ...(supportsReasoningControls
+ ? {}
+ : {
+ ...(requestedReasoningEffort
+ ? { reasoning_effort: requestedReasoningEffort }
+ : {}),
+ ...(requestedVerbosity
+ ? { verbosity: requestedVerbosity }
+ : {}),
+ }),
+ } as ChatCompletionCreateParams;
+
+ const completion =
+ await this.#openAi.chat.completions.create(completionParams);
+
+ return OpenAiUtil.handle_completion_output({
+ usage_calculator: ({ usage }) => {
+ const trackedUsage = {
+ prompt_tokens:
+ (usage.prompt_tokens ?? 0) -
+ (usage.prompt_tokens_details?.cached_tokens ?? 0),
+ completion_tokens: usage.completion_tokens ?? 0,
+ cached_tokens:
+ usage.prompt_tokens_details?.cached_tokens ?? 0,
+ };
+
+ const costsOverrideFromModel = Object.fromEntries(
+ Object.entries(trackedUsage).map(([k, v]) => {
+ return [k, v * modelUsed.costs[k]];
+ }),
+ );
+
+ this.#meteringService.utilRecordUsageObject(
+ trackedUsage,
+ actor!,
+ `azure-openai:${modelUsed?.id}`,
+ costsOverrideFromModel,
+ );
+ return trackedUsage;
+ },
+ stream,
+ completion,
+ moderate: moderation ? this.checkModeration.bind(this) : undefined,
+ });
+ }
+}
diff --git a/src/backend/drivers/ai-chat/providers/azure/AzureResponsesProvider.integration.test.ts b/src/backend/drivers/ai-chat/providers/azure/AzureResponsesProvider.integration.test.ts
new file mode 100644
index 000000000..33b29f0cc
--- /dev/null
+++ b/src/backend/drivers/ai-chat/providers/azure/AzureResponsesProvider.integration.test.ts
@@ -0,0 +1,83 @@
+/**
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+/**
+ * Integration test for the Azure AI Foundry Responses provider.
+ *
+ * Hits the real Azure endpoint with `gpt-5-codex` — a Responses-API-only
+ * (Codex) model that the Chat Completions endpoint rejects. This verifies
+ * the completions/responses split actually routes Codex correctly. Codex is
+ * a reasoning model, so we give it a generous `max_tokens` and low reasoning
+ * effort to leave room for visible output.
+ *
+ * Skipped unless both `PUTER_TEST_AI_AZURE_OPENAI_API_KEY` and
+ * `PUTER_TEST_AI_AZURE_OPENAI_API_URL` are set.
+ */
+
+import { describe, expect, it } from 'vitest';
+import {
+ INTEGRATION_TEST_TIMEOUT_MS,
+ makeMeteringStub,
+ optionalEnv,
+ skipUnlessEnv,
+ withTestActor,
+} from '../../../integrationTestUtil.js';
+import { AzureResponsesProvider } from './AzureResponsesProvider.js';
+
+const KEY_ENV = 'PUTER_TEST_AI_AZURE_OPENAI_API_KEY';
+const URL_ENV = 'PUTER_TEST_AI_AZURE_OPENAI_API_URL';
+
+describe.skipIf(skipUnlessEnv(KEY_ENV) || skipUnlessEnv(URL_ENV))(
+ 'AzureResponsesProvider (integration)',
+ () => {
+ it(
+ 'returns a non-empty completion from gpt-5-codex',
+ { timeout: INTEGRATION_TEST_TIMEOUT_MS },
+ async () => {
+ const provider = new AzureResponsesProvider(
+ makeMeteringStub(),
+ {
+ fsEntry: undefined as never,
+ s3Object: undefined as never,
+ },
+ undefined as never,
+ {
+ apiKey: optionalEnv(KEY_ENV)!,
+ apiURL: optionalEnv(URL_ENV)!,
+ },
+ );
+
+ const result = await withTestActor(() =>
+ provider.complete({
+ model: 'gpt-5-codex',
+ messages: [
+ { role: 'user', content: 'Say hi in one word.' },
+ ],
+ max_tokens: 2048,
+ reasoning: { effort: 'low' },
+ }),
+ );
+
+ const text = (result as { message?: { content?: string } })
+ .message?.content;
+ expect(typeof text === 'string' && text.length > 0).toBe(true);
+ },
+ );
+ },
+);
diff --git a/src/backend/drivers/ai-chat/providers/azure/AzureResponsesProvider.ts b/src/backend/drivers/ai-chat/providers/azure/AzureResponsesProvider.ts
new file mode 100644
index 000000000..1b7a8db62
--- /dev/null
+++ b/src/backend/drivers/ai-chat/providers/azure/AzureResponsesProvider.ts
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { OpenAI } from 'openai';
+import { ResponseCreateParams } from 'openai/resources/responses/responses.mjs';
+import { Context } from '../../../../core/context.js';
+import type { FSService } from '../../../../services/fs/FSService.js';
+import type { MeteringService } from '../../../../services/metering/MeteringService.js';
+import type { FSEntryStore } from '../../../../stores/fs/FSEntryStore.js';
+import type { S3ObjectStore } from '../../../../stores/fs/S3ObjectStore.js';
+import type { IChatProvider, ICompleteArguments } from '../../types.js';
+import * as OpenAiUtil from '../../utils/OpenAIUtil.js';
+import { processPuterPathUploads } from '../openai/fileUpload.js';
+import { AZURE_MODELS } from './models.js';
+import { HttpError } from '@heyputer/backend/src/core/http/HttpError.js';
+
+/**
+ * AzureResponsesProvider serves the Responses-API-only models we expose through
+ * Azure AI Foundry (the Codex family and similar). It mirrors
+ * {@link OpenAiResponsesChatProvider}, but points the OpenAI client at the
+ * configurable Azure endpoint and draws from {@link AZURE_MODELS}.
+ *
+ * Codex / `responses_api_only` models reject the Chat Completions endpoint, so
+ * the sibling {@link AzureChatProvider} (Chat Completions) filters them out and
+ * the driver routes them here instead.
+ *
+ * Billing note: the model `costs` are the standard public OpenAI list prices,
+ * NOT Azure's — Azure is subsidised for us.
+ */
+export class AzureResponsesProvider implements IChatProvider {
+ /**
+ * @type {import('openai').OpenAI}
+ */
+ #openAi: OpenAI;
+
+ #defaultModel = 'gpt-5-codex';
+
+ #meteringService: MeteringService;
+
+ #stores: { fsEntry: FSEntryStore; s3Object: S3ObjectStore };
+
+ #fsService: FSService;
+
+ constructor(
+ meteringService: MeteringService,
+ stores: { fsEntry: FSEntryStore; s3Object: S3ObjectStore },
+ fsService: FSService,
+ config: { apiKey: string; apiURL: string },
+ ) {
+ this.#meteringService = meteringService;
+ this.#stores = stores;
+ this.#fsService = fsService;
+ this.#openAi = new OpenAI({
+ apiKey: config.apiKey,
+ baseURL: config.apiURL,
+ });
+ }
+
+ /**
+ * Returns an array of available AI models with their pricing information.
+ * Each model object includes an ID and cost details (currency, tokens, input/output rates).
+ */
+ models(extra_params?: { no_restrictions?: boolean }) {
+ if (extra_params?.no_restrictions) {
+ return AZURE_MODELS;
+ }
+ return AZURE_MODELS.filter((e) => e.responses_api_only === true);
+ }
+
+ list() {
+ const models = this.models({ no_restrictions: false });
+ const modelNames: string[] = [];
+ for (const model of models) {
+ modelNames.push(model.id);
+ if (model.aliases) {
+ modelNames.push(...model.aliases);
+ }
+ }
+ return modelNames;
+ }
+
+ getDefaultModel() {
+ return this.#defaultModel;
+ }
+
+ async complete({
+ messages,
+ model,
+ max_tokens,
+ moderation,
+ tools,
+ tool_choice,
+ parallel_tool_calls,
+ include,
+ conversation,
+ previous_response_id,
+ instructions,
+ metadata,
+ prompt,
+ prompt_cache_key,
+ prompt_cache_retention,
+ store,
+ top_p,
+ truncation,
+ background,
+ service_tier,
+ verbosity,
+ stream,
+ reasoning,
+ reasoning_effort,
+ temperature,
+ text,
+ }: ICompleteArguments): ReturnType {
+ // Validate messages
+ if (!Array.isArray(messages)) {
+ throw new HttpError(400, '`messages` must be an array', {
+ legacyCode: 'bad_request',
+ });
+ }
+ const actor = Context.get('actor');
+
+ model = model ?? this.#defaultModel;
+
+ const modelUsed =
+ this.models({ no_restrictions: true }).find((m) =>
+ [m.id, ...(m.aliases || [])].includes(model),
+ ) ||
+ this.models({ no_restrictions: true }).find(
+ (m) => m.id === this.getDefaultModel(),
+ )!;
+
+ const userIdentifier =
+ actor?.user.id + actor?.app?.uid ? `:${actor?.app?.uid}` : '';
+
+ // Resolve any `puter_path` content parts into inline base64 data URLs
+ // before the Responses API sees them.
+ await processPuterPathUploads(
+ messages,
+ this.#stores,
+ this.#fsService,
+ actor,
+ );
+
+ if (tools) {
+ // Unravel tools to OpenAI Responses API format
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ tools = (tools as any).map((e) => {
+ if (e.type === 'function') {
+ const tool = e.function;
+ tool.type = 'function';
+ return tool;
+ } else {
+ return e;
+ }
+ });
+ }
+
+ // Here's something fun; the documentation shows `type: 'image_url'` in
+ // objects that contain an image url, but everything still works if
+ // that's missing. We normalise it here so the token count code works.
+ messages =
+ await OpenAiUtil.process_input_messages_responses_api(messages);
+
+ const requestedReasoningEffort = reasoning_effort ?? reasoning?.effort;
+ const requestedVerbosity = verbosity ?? text?.verbosity;
+ const supportsReasoningControls =
+ typeof model === 'string' && model.startsWith('gpt-5');
+
+ const completionParams: ResponseCreateParams = {
+ user: userIdentifier,
+ safety_identifier: userIdentifier,
+ input: messages,
+ model: modelUsed.id,
+ ...(tools ? { tools } : {}),
+ ...(tool_choice !== undefined ? { tool_choice } : {}),
+ ...(parallel_tool_calls !== undefined
+ ? { parallel_tool_calls }
+ : {}),
+ ...(include !== undefined ? { include } : {}),
+ ...(conversation !== undefined ? { conversation } : {}),
+ ...(previous_response_id !== undefined
+ ? { previous_response_id }
+ : {}),
+ ...(instructions !== undefined ? { instructions } : {}),
+ ...(metadata !== undefined ? { metadata } : {}),
+ ...(prompt !== undefined ? { prompt } : {}),
+ ...(prompt_cache_key !== undefined ? { prompt_cache_key } : {}),
+ ...(prompt_cache_retention !== undefined
+ ? { prompt_cache_retention }
+ : {}),
+ ...(store !== undefined ? { store } : {}),
+ ...(max_tokens !== undefined
+ ? { max_output_tokens: max_tokens }
+ : {}),
+ ...(temperature !== undefined ? { temperature } : {}),
+ ...(top_p !== undefined ? { top_p } : {}),
+ ...(truncation !== undefined ? { truncation } : {}),
+ ...(background !== undefined ? { background } : {}),
+ ...(service_tier !== undefined ? { service_tier } : {}),
+ ...(stream !== undefined ? { stream: !!stream } : {}),
+ ...(text !== undefined ? { text } : {}),
+ ...(supportsReasoningControls
+ ? {}
+ : {
+ ...(requestedReasoningEffort
+ ? { reasoning_effort: requestedReasoningEffort }
+ : {}),
+ ...(requestedVerbosity
+ ? { verbosity: requestedVerbosity }
+ : {}),
+ }),
+ ...(supportsReasoningControls && reasoning ? { reasoning } : {}),
+ } as ResponseCreateParams;
+
+ const completion =
+ await this.#openAi.responses.create(completionParams);
+ return OpenAiUtil.handle_completion_output_responses_api({
+ usage_calculator: ({ usage }) => {
+ const trackedUsage = {
+ prompt_tokens:
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ((usage as any).input_tokens ?? 0) -
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ((usage as any).input_tokens_details?.cached_tokens ??
+ 0),
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ completion_tokens: (usage as any).output_tokens ?? 0,
+ cached_tokens:
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (usage as any).input_tokens_details?.cached_tokens ?? 0,
+ };
+
+ const costsOverrideFromModel = Object.fromEntries(
+ Object.entries(trackedUsage).map(([k, v]) => {
+ return [k, v * modelUsed.costs[k]];
+ }),
+ );
+
+ this.#meteringService.utilRecordUsageObject(
+ trackedUsage,
+ actor,
+ `azure-openai:${modelUsed?.id}`,
+ costsOverrideFromModel,
+ );
+ return trackedUsage;
+ },
+ stream,
+ completion,
+ moderate: moderation ? this.checkModeration.bind(this) : undefined,
+ });
+ }
+
+ async checkModeration(text: string) {
+ // create moderation
+ const results = await this.#openAi.moderations.create({
+ model: 'omni-moderation-latest',
+ input: text,
+ });
+
+ let flagged = false;
+
+ for (const result of results?.results ?? []) {
+ // OpenAI does a crazy amount of false positives. We filter by their 80% interval
+ const veryFlaggedEntries = Object.entries(
+ result.category_scores,
+ ).filter((e) => e[1] > 0.8);
+ if (veryFlaggedEntries.length > 0) {
+ flagged = true;
+ break;
+ }
+ }
+
+ return {
+ flagged,
+ results,
+ };
+ }
+}
diff --git a/src/backend/drivers/ai-chat/providers/azure/models.ts b/src/backend/drivers/ai-chat/providers/azure/models.ts
new file mode 100644
index 000000000..3b345d2ac
--- /dev/null
+++ b/src/backend/drivers/ai-chat/providers/azure/models.ts
@@ -0,0 +1,464 @@
+/**
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import type { IChatModel } from '../../types.js';
+
+// Models served through our Azure AI Foundry deployment. This is NOT just
+// OpenAI — Azure AI also fronts xAI's Grok models — so the list lives in its
+// own provider folder rather than sharing the OpenAI list.
+//
+// IMPORTANT: the `costs` below intentionally mirror the public list prices of
+// the equivalent OpenAI / xAI models (see `../openai/models.ts` and
+// `../xai/models.ts`). Azure is subsidised for us, so our actual spend is
+// lower — but we bill users at the standard model price, which is the whole
+// reason we route through Azure. Do NOT replace these with Azure's own rates.
+//
+// `id` is the Azure deployment name and is what we send upstream.
+export const AZURE_MODELS: IChatModel[] = [
+ // -- xAI Grok (via Azure AI Foundry) -----------------------------------
+ {
+ // Costs mirror xai grok-4-1-fast-non-reasoning.
+ puterId: 'azure:x-ai/grok-4-1-fast-non-reasoning',
+ id: 'grok-4-1-fast-non-reasoning',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2025-07',
+ release_date: '2025-11-19',
+ name: 'Grok 4.1 Fast (Non-Reasoning)',
+ aliases: ['x-ai/grok-4-1-fast-non-reasoning'],
+ context: 2_000_000,
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 20,
+ completion_tokens: 50,
+ cached_tokens: 5,
+ },
+ max_tokens: 2_000_000,
+ },
+ {
+ // Costs mirror xai grok-4-1-fast (alias grok-4-1-fast-reasoning).
+ puterId: 'azure:x-ai/grok-4-1-fast-reasoning',
+ id: 'grok-4-1-fast-reasoning',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2025-07',
+ release_date: '2025-11-19',
+ name: 'Grok 4.1 Fast (Reasoning)',
+ aliases: ['x-ai/grok-4-1-fast-reasoning'],
+ context: 2_000_000,
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 20,
+ completion_tokens: 50,
+ cached_tokens: 5,
+ },
+ max_tokens: 2_000_000,
+ },
+ {
+ // Costs mirror xai grok-4.3.
+ puterId: 'azure:x-ai/grok-4.3',
+ id: 'grok-4.3',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ release_date: '2026-05-01',
+ name: 'Grok 4.3',
+ aliases: ['x-ai/grok-4.3'],
+ context: 1_000_000,
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 125,
+ completion_tokens: 250,
+ cached_tokens: 20,
+ },
+ max_tokens: 30_000,
+ },
+ {
+ // Costs mirror xai grok-4.20 (grok-4.20-0309-non-reasoning).
+ puterId: 'azure:x-ai/grok-4-20-non-reasoning',
+ id: 'grok-4-20-non-reasoning',
+ modalities: { input: ['text', 'image', 'pdf'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2025-07',
+ release_date: '2026-03-09',
+ name: 'Grok 4.20 (Non-Reasoning)',
+ aliases: ['x-ai/grok-4-20-non-reasoning'],
+ context: 2_000_000,
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 125,
+ completion_tokens: 250,
+ cached_tokens: 20,
+ },
+ max_tokens: 30_000,
+ },
+ {
+ // Costs mirror xai grok-4.20 (grok-4.20-0309-reasoning).
+ puterId: 'azure:x-ai/grok-4-20-reasoning',
+ id: 'grok-4-20-reasoning',
+ modalities: { input: ['text', 'image', 'pdf'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2025-07',
+ release_date: '2026-03-09',
+ name: 'Grok 4.20 (Reasoning)',
+ aliases: ['x-ai/grok-4-20-reasoning'],
+ context: 2_000_000,
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 125,
+ completion_tokens: 250,
+ cached_tokens: 20,
+ },
+ max_tokens: 30_000,
+ },
+
+ // -- OpenAI (via Azure AI Foundry) -------------------------------------
+ {
+ // Costs mirror openai gpt-5.
+ puterId: 'azure:openai/gpt-5',
+ id: 'gpt-5',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2024-09-30',
+ release_date: '2025-08-07',
+ aliases: ['openai/gpt-5'],
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 125,
+ cached_tokens: 13,
+ completion_tokens: 1000,
+ },
+ context: 128_000,
+ max_tokens: 128000,
+ },
+ {
+ // Costs mirror openai gpt-5-codex (same list price as gpt-5).
+ puterId: 'azure:openai/gpt-5-codex',
+ id: 'gpt-5-codex',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2024-09-30',
+ release_date: '2025-08-07',
+ aliases: ['openai/gpt-5-codex'],
+ // Codex models are Responses-API only on Azure, same as OpenAI.
+ responses_api_only: true,
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 125,
+ cached_tokens: 13,
+ completion_tokens: 1000,
+ },
+ context: 128_000,
+ max_tokens: 128000,
+ },
+ {
+ // Costs mirror openai gpt-5-nano.
+ puterId: 'azure:openai/gpt-5-nano',
+ id: 'gpt-5-nano',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2024-05-30',
+ release_date: '2025-08-07',
+ aliases: ['openai/gpt-5-nano'],
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 5,
+ cached_tokens: 1,
+ completion_tokens: 40,
+ },
+ context: 128_000,
+ max_tokens: 128000,
+ },
+ {
+ // Costs mirror openai gpt-5-mini.
+ puterId: 'azure:openai/gpt-5-mini',
+ id: 'gpt-5-mini',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2024-05-30',
+ release_date: '2025-08-07',
+ aliases: ['openai/gpt-5-mini'],
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 25,
+ cached_tokens: 3,
+ completion_tokens: 200,
+ },
+ context: 128_000,
+ max_tokens: 128000,
+ },
+ {
+ // Costs mirror openai gpt-4o.
+ puterId: 'azure:openai/gpt-4o',
+ id: 'gpt-4o',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2023-09',
+ release_date: '2024-05-13',
+ aliases: ['openai/gpt-4o'],
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 250,
+ cached_tokens: 125,
+ completion_tokens: 1000,
+ },
+ context: 128_000,
+ max_tokens: 16384,
+ },
+ {
+ // Costs mirror openai gpt-5.1-codex-mini.
+ puterId: 'azure:openai/gpt-5.1-codex-mini',
+ id: 'gpt-5.1-codex-mini',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2024-09-30',
+ release_date: '2025-11-13',
+ aliases: ['openai/gpt-5.1-codex-mini'],
+ responses_api_only: true,
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 25,
+ cached_tokens: 3,
+ completion_tokens: 200,
+ },
+ context: 128_000,
+ max_tokens: 128000,
+ },
+ {
+ // Costs mirror openai gpt-5.1.
+ puterId: 'azure:openai/gpt-5.1',
+ id: 'gpt-5.1',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2024-09-30',
+ release_date: '2025-11-13',
+ aliases: ['openai/gpt-5.1'],
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 125,
+ cached_tokens: 13,
+ completion_tokens: 1000,
+ },
+ context: 128_000,
+ max_tokens: 128000,
+ },
+ {
+ // Costs mirror openai gpt-5.1-codex.
+ puterId: 'azure:openai/gpt-5.1-codex',
+ id: 'gpt-5.1-codex',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2024-09-30',
+ release_date: '2025-11-13',
+ aliases: ['openai/gpt-5.1-codex'],
+ responses_api_only: true,
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 125,
+ cached_tokens: 13,
+ completion_tokens: 1000,
+ },
+ context: 128_000,
+ max_tokens: 128000,
+ },
+ {
+ // Costs mirror openai gpt-5.2.
+ puterId: 'azure:openai/gpt-5.2',
+ id: 'gpt-5.2',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2025-08-31',
+ release_date: '2025-12-11',
+ aliases: ['openai/gpt-5.2'],
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 175,
+ cached_tokens: 17.5,
+ completion_tokens: 1400,
+ },
+ context: 128_000,
+ max_tokens: 128000,
+ },
+ {
+ // Costs mirror openai gpt-5.2-codex.
+ puterId: 'azure:openai/gpt-5.2-codex',
+ id: 'gpt-5.2-codex',
+ modalities: { input: ['text', 'image', 'pdf'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2025-08-31',
+ release_date: '2025-12-11',
+ aliases: ['openai/gpt-5.2-codex'],
+ responses_api_only: true,
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 175,
+ cached_tokens: 18,
+ completion_tokens: 1400,
+ },
+ context: 128_000,
+ max_tokens: 128000,
+ },
+ {
+ // Costs mirror openai gpt-5.4-nano.
+ puterId: 'azure:openai/gpt-5.4-nano',
+ id: 'gpt-5.4-nano',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2025-08-31',
+ release_date: '2026-03-19',
+ aliases: ['openai/gpt-5.4-nano'],
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 20,
+ cached_tokens: 2,
+ completion_tokens: 125,
+ },
+ context: 400_000,
+ max_tokens: 128_000,
+ },
+ {
+ // Costs mirror openai gpt-5.4-mini.
+ puterId: 'azure:openai/gpt-5.4-mini',
+ id: 'gpt-5.4-mini',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2025-08-31',
+ aliases: ['openai/gpt-5.4-mini'],
+ responses_api_only: true,
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 75,
+ cached_tokens: 7.5,
+ completion_tokens: 450,
+ },
+ context: 400_000,
+ max_tokens: 128_000,
+ },
+ {
+ // Costs mirror openai gpt-5.3-codex.
+ puterId: 'azure:openai/gpt-5.3-codex',
+ id: 'gpt-5.3-codex',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2025-08-31',
+ aliases: ['openai/gpt-5.3-codex'],
+ responses_api_only: true,
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 175,
+ cached_tokens: 17.5,
+ completion_tokens: 1400,
+ },
+ context: 128_000,
+ max_tokens: 128000,
+ },
+ {
+ // Costs mirror openai gpt-5.4.
+ puterId: 'azure:openai/gpt-5.4',
+ id: 'gpt-5.4',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2025-08-31',
+ release_date: '2026-03-05',
+ aliases: ['openai/gpt-5.4'],
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ prompt_tokens: 250,
+ cached_tokens: 25,
+ completion_tokens: 1500,
+ },
+ context: 1_050_000,
+ max_tokens: 1_050_000,
+ },
+];
diff --git a/src/backend/drivers/ai-chat/providers/openai/models.ts b/src/backend/drivers/ai-chat/providers/openai/models.ts
index c167395c4..22f1aa9b1 100644
--- a/src/backend/drivers/ai-chat/providers/openai/models.ts
+++ b/src/backend/drivers/ai-chat/providers/openai/models.ts
@@ -363,6 +363,30 @@ export const OPEN_AI_MODELS: IChatModel[] = [
context: 128_000,
max_tokens: 128000,
},
+ {
+ puterId: 'openai:openai/gpt-5-codex',
+ id: 'gpt-5-codex',
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2024-09-30',
+ release_date: '2025-09-15',
+ aliases: ['openai/gpt-5-codex'],
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ // models.dev openai: input $1.25 / output $10 / cache_read $0.125
+ // per 1M — identical to gpt-5.
+ tokens: 1_000_000,
+ prompt_tokens: 125,
+ cached_tokens: 13,
+ completion_tokens: 1000,
+ },
+ context: 128_000,
+ max_tokens: 128000,
+ responses_api_only: true,
+ },
{
puterId: 'openai:openai/gpt-5-mini',
id: 'gpt-5-mini-2025-08-07',
diff --git a/src/backend/drivers/ai-chat/providers/xai/models.ts b/src/backend/drivers/ai-chat/providers/xai/models.ts
index 02e2b158f..b6d782b9e 100644
--- a/src/backend/drivers/ai-chat/providers/xai/models.ts
+++ b/src/backend/drivers/ai-chat/providers/xai/models.ts
@@ -198,6 +198,66 @@ export const XAI_MODELS: IChatModel[] = [
},
max_tokens: 30_000,
},
+ {
+ puterId: 'x-ai:x-ai/grok-4-20-reasoning',
+ // xAI exposes this as the dated snapshot id; `grok-4-20-reasoning`
+ // (and dotted forms) are accepted as aliases by callers.
+ id: 'grok-4.20-0309-reasoning',
+ modalities: { input: ['text', 'image', 'pdf'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2025-07',
+ release_date: '2026-03-09',
+ name: 'Grok 4.20 (Reasoning)',
+ aliases: [
+ 'x-ai/grok-4-20-reasoning',
+ 'grok-4-20-reasoning',
+ 'grok-4.20-reasoning',
+ 'x-ai/grok-4.20-reasoning',
+ ],
+ context: 2_000_000,
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ // models.dev xai: input $1.25 / output $2.5 / cache_read $0.2 per 1M.
+ prompt_tokens: 125,
+ completion_tokens: 250,
+ cached_tokens: 20,
+ },
+ max_tokens: 30_000,
+ },
+ {
+ puterId: 'x-ai:x-ai/grok-4-20-non-reasoning',
+ // xAI exposes this as the dated snapshot id; `grok-4-20-non-reasoning`
+ // (and dotted forms) are accepted as aliases by callers.
+ id: 'grok-4.20-0309-non-reasoning',
+ modalities: { input: ['text', 'image', 'pdf'], output: ['text'] },
+ open_weights: false,
+ tool_call: true,
+ knowledge: '2025-07',
+ release_date: '2026-03-09',
+ name: 'Grok 4.20 (Non-Reasoning)',
+ aliases: [
+ 'x-ai/grok-4-20-non-reasoning',
+ 'grok-4-20-non-reasoning',
+ 'grok-4.20-non-reasoning',
+ 'x-ai/grok-4.20-non-reasoning',
+ ],
+ context: 2_000_000,
+ costs_currency: 'usd-cents',
+ input_cost_key: 'prompt_tokens',
+ output_cost_key: 'completion_tokens',
+ costs: {
+ tokens: 1_000_000,
+ // models.dev xai: input $1.25 / output $2.5 / cache_read $0.2 per 1M.
+ prompt_tokens: 125,
+ completion_tokens: 250,
+ cached_tokens: 20,
+ },
+ max_tokens: 30_000,
+ },
{
puterId: 'x-ai:x-ai/grok-4-1-fast',
id: 'grok-4-1-fast',
diff --git a/src/backend/types.ts b/src/backend/types.ts
index dcf295570..87b8d757d 100644
--- a/src/backend/types.ts
+++ b/src/backend/types.ts
@@ -140,6 +140,8 @@ export interface IAIProviderConfig {
apiToken?: string;
/** Override the provider's HTTP base URL (OpenRouter, Cloudflare, ElevenLabs, Ollama). */
apiBaseUrl?: string;
+ /** Azure AI Foundry deployment endpoint (azure-openai). Required alongside `apiKey`. */
+ apiURL?: string;
/** Cloudflare account id. */
accountId?: string;
/** ElevenLabs default voice id. */