feat: add Azure provider support

Add support for Azure-hosted OpenAI-compatible models including OpenAI (GPT-4, GPT-5.2), DeepSeek, and other compatible models.

Implementation:
- Add Azure provider configuration with auto-discovery from environment
- Environment variables: AZURE_ENDPOINT, AZURE_API_KEY, AZURE_DEPLOYMENT, AZURE_API_VERSION
- URL fix middleware to handle Azure's specific URL format
- Tool call ID sanitization for Azure's 40-character limit
- Onboarding wizard support for interactive Azure setup
- Comprehensive tests for configuration and URL handling
- Documentation with setup guide and troubleshooting

Technical details:
- Azure uses different URL structure: {endpoint}/openai/deployments/{deployment}/chat/completions?api-version={version}
- OpenAI SDK constructs URLs incorrectly for Azure, placing query params before path
- URL fix middleware intercepts and corrects malformed URLs transparently
- Supports max_completion_tokens for newer models via compat config
- Tool call IDs automatically truncated to 40 characters for Azure compatibility

Onboard integration:
- Added Azure to auth choice groups (appears after OpenAI)
- Interactive prompts for endpoint, deployment name, API key, and API version
- Auto-discovery from environment variables if already configured
- Supports both manual configuration and environment variable detection

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Li Hongmin 2026-01-28 16:27:00 +09:00
parent 57d9c09f6e
commit 9e857e6154
15 changed files with 840 additions and 5 deletions

230
docs/providers/azure.md Normal file
View File

@ -0,0 +1,230 @@
# Azure Provider
Azure supports deploying OpenAI-compatible models through Azure infrastructure. Moltbot's Azure provider allows you to use various models deployed on Azure, including OpenAI models (GPT-4, GPT-3.5), DeepSeek, and other compatible models.
## Prerequisites
Before using Azure with Moltbot, you need:
1. An active Azure subscription with Azure AI access
2. An Azure resource with model deployments
3. Your Azure API key and endpoint
4. A deployed model (OpenAI, DeepSeek, or other compatible models)
## Configuration
### Environment Variables
The easiest way to configure Azure is through environment variables:
```bash
export AZURE_ENDPOINT="https://your-resource.cognitiveservices.azure.com"
export AZURE_API_KEY="your-api-key-here"
export AZURE_DEPLOYMENT="your-deployment-name"
export AZURE_API_VERSION="2024-02-01" # Optional, defaults to 2024-08-01-preview
```
#### Required Variables
- `AZURE_ENDPOINT`: Your Azure resource endpoint URL
- Format: `https://{resource-name}.cognitiveservices.azure.com` or `https://{region}.api.cognitive.microsoft.com`
- Find this in the Azure Portal under your resource's "Keys and Endpoint" section
- `AZURE_API_KEY`: Your Azure API key
- Find this in the Azure Portal under "Keys and Endpoint"
- Either KEY 1 or KEY 2 will work
- `AZURE_DEPLOYMENT`: The name of your model deployment
- This is the deployment name you configured in Azure AI Studio
- Must match exactly as configured in Azure
- Examples: `gpt-4`, `gpt-5.2`, `deepseek-chat`
#### Optional Variables
- `AZURE_API_VERSION`: Azure API version
- Default: `2024-08-01-preview`
- Use a stable API version for production workloads
- Common versions: `2024-02-01`, `2024-08-01-preview`
### models.json Configuration
Alternatively, you can configure Azure in your `models.json` file:
```json
{
"providers": {
"azure": {
"baseUrl": "https://your-resource.cognitiveservices.azure.com/openai/deployments/your-deployment/chat/completions?api-version=2024-02-01",
"apiKey": "AZURE_API_KEY",
"api": "openai-completions",
"headers": {
"api-key": "${AZURE_API_KEY}"
},
"models": [
{
"id": "",
"name": "Azure GPT-4",
"reasoning": false,
"input": ["text"],
"cost": {
"input": 10,
"output": 30,
"cacheRead": 2.5,
"cacheWrite": 12.5
},
"contextWindow": 200000,
"maxTokens": 16384,
"compat": {
"maxTokensField": "max_completion_tokens"
}
}
]
}
}
}
```
## Supported Models
Azure supports various OpenAI-compatible models:
### OpenAI Models
- **GPT-4 Series**: `gpt-4`, `gpt-4-turbo`, `gpt-4-vision`
- **GPT-5.2**: Latest models with extended context
- **GPT-3.5**: `gpt-35-turbo` (note the hyphen instead of dot)
- **o1/o3 Models**: Reasoning models with extended thinking
### DeepSeek Models
- **DeepSeek-V3**: Latest DeepSeek model
- **DeepSeek-Chat**: General conversation model
### Other Compatible Models
Any OpenAI API-compatible model deployed on Azure will work with this provider.
## Usage
### With Environment Variables
```bash
# Configure Azure
export AZURE_ENDPOINT="https://eastus2.api.cognitive.microsoft.com"
export AZURE_API_KEY="your-key-here"
export AZURE_DEPLOYMENT="gpt-5.2"
export AZURE_API_VERSION="2024-02-01"
# Use with Moltbot
moltbot agent --message "Hello" --model azure/gpt-5.2
```
### List Available Models
```bash
moltbot models list | grep azure
```
Expected output:
```
azure/{deployment-name} Azure {deployment-name} ...
```
## Deployment Name Mapping
Azure uses deployment names instead of model IDs. When you configure `AZURE_DEPLOYMENT=gpt-5.2`, Moltbot will expose this as:
```
azure/gpt-5.2
```
The deployment name becomes the model identifier in Moltbot.
## Important Notes
### URL Construction
Azure has a specific URL format that differs from standard OpenAI:
```
Standard OpenAI: https://api.openai.com/v1/chat/completions
Azure: https://{endpoint}/openai/deployments/{deployment}/chat/completions?api-version={version}
```
Moltbot automatically handles URL construction through an internal URL fix middleware, so you don't need to worry about the format differences.
### API Compatibility
Azure's API is compatible with OpenAI's API format but uses:
- Header: `api-key` instead of `Authorization: Bearer`
- Parameter: `max_completion_tokens` instead of `max_tokens` (for newer models)
- Query parameter: `api-version` is required
## Troubleshooting
### HTTP 404 Errors
**Problem**: Getting 404 errors when making API calls
**Solutions**:
1. Verify your `AZURE_ENDPOINT` is correct (should not include `/openai/deployments/`)
2. Verify your `AZURE_DEPLOYMENT` name matches exactly in Azure Portal
3. Check that `AZURE_API_VERSION` is supported by your deployment
4. Ensure your Azure resource has the model deployed
### Authentication Errors
**Problem**: 401 Unauthorized errors
**Solutions**:
1. Verify `AZURE_API_KEY` is correct
2. Check that the API key hasn't been regenerated in Azure Portal
3. Ensure the key matches the endpoint (don't mix keys from different resources)
### Unsupported Parameter Errors
**Problem**: `Unsupported parameter: 'max_tokens'` error
**Solution**: Newer Azure models require `max_completion_tokens` instead of `max_tokens`. Moltbot handles this automatically through the `compat.maxTokensField` setting.
### Model Not Found
**Problem**: Model doesn't appear in `moltbot models list`
**Solutions**:
1. Check all required environment variables are set
2. Verify `AZURE_DEPLOYMENT` is exactly as configured in Azure
3. Restart Moltbot after changing environment variables
## Security Best Practices
1. **Never commit API keys**: Use environment variables or Azure Key Vault
2. **Rotate keys regularly**: Regenerate your API keys periodically
3. **Use RBAC**: Configure role-based access control in Azure
4. **Monitor usage**: Enable Azure Monitor for usage tracking
5. **Set spending limits**: Configure budgets in Azure Cost Management
## API Versions
Azure uses API versioning. Common versions:
- `2024-02-01`: Stable production version
- `2024-08-01-preview`: Preview with latest features
- `2023-12-01`: Older stable version
Check [Azure OpenAI API documentation](https://learn.microsoft.com/azure/ai-services/openai/reference) for the latest versions.
## Comparison with Standard OpenAI
| Feature | Azure | Standard OpenAI |
|---------|-------|-----------------|
| Endpoint | Regional (e.g., eastus2) | Global (api.openai.com) |
| Authentication | api-key header | Bearer token |
| Deployment | Named deployments | Model IDs |
| Billing | Azure subscription | OpenAI account |
| Data residency | Regional | Global |
| Enterprise features | Azure integration | OpenAI org settings |
## Additional Resources
- [Azure AI Services Documentation](https://learn.microsoft.com/azure/ai-services/)
- [Azure OpenAI Quickstart](https://learn.microsoft.com/azure/ai-services/openai/quickstart)
- [Model Deployments](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource)
- [API Reference](https://learn.microsoft.com/azure/ai-services/openai/reference)

View File

@ -0,0 +1,69 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { installAzureUrlFix, resetAzureUrlFixForTesting } from "./azure-url-fix.js";
describe("Azure URL Fix", () => {
let originalFetch: typeof fetch;
beforeEach(() => {
originalFetch = globalThis.fetch;
resetAzureUrlFixForTesting(); // Reset state for each test
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it("should fix malformed Azure URLs with query params before path", async () => {
// Mock fetch to capture the fixed URL
let capturedUrl = "";
globalThis.fetch = async (input: RequestInfo | URL) => {
capturedUrl = typeof input === "string" ? input : (input as any).url || input.toString();
return new Response("{}");
};
installAzureUrlFix();
// Simulate OpenAI SDK's malformed URL construction
await fetch(
"https://eastus2.api.cognitive.microsoft.com/openai/deployments/gpt-5.2/chat/completions?api-version=2024-02-01/chat/completions",
);
// Verify URL was fixed
expect(capturedUrl).toBe(
"https://eastus2.api.cognitive.microsoft.com/openai/deployments/gpt-5.2/chat/completions?api-version=2024-02-01",
);
});
it("should not modify non-Azure URLs", async () => {
let capturedUrl = "";
globalThis.fetch = async (input: RequestInfo | URL) => {
capturedUrl = typeof input === "string" ? input : (input as any).url;
return new Response("{}");
};
installAzureUrlFix();
const testUrl = "https://api.openai.com/v1/chat/completions";
await fetch(testUrl);
expect(capturedUrl).toBe(testUrl);
});
it("should handle URLs with different Azure endpoint formats", async () => {
let capturedUrl = "";
globalThis.fetch = async (input: RequestInfo | URL) => {
capturedUrl = typeof input === "string" ? input : (input as any).url;
return new Response("{}");
};
installAzureUrlFix();
await fetch(
"https://test.openai.azure.com/openai/deployments/gpt-4?api-version=2024-08-01-preview/chat/completions",
);
expect(capturedUrl).toBe(
"https://test.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2024-08-01-preview",
);
});
});

View File

@ -0,0 +1,74 @@
/**
* Azure OpenAI URL fix utility
*
* Problem: OpenAI SDK constructs URLs as: baseURL + path
* For Azure with query params: https://endpoint/deployments/name?api-version=xxx + /chat/completions
* Result: https://endpoint/deployments/name?api-version=xxx/chat/completions (WRONG)
*
* This utility intercepts fetch calls and fixes Azure URLs before sending.
*/
let azureUrlFixInstalled = false;
export function installAzureUrlFix(): void {
if (azureUrlFixInstalled) return;
const originalFetch = globalThis.fetch;
globalThis.fetch = async function azureCompatibleFetch(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> {
// Extract URL string from input
let url =
typeof input === "string"
? input
: input instanceof URL
? input.href
: (input as Request).url;
// Detect malformed Azure URLs
// Pattern: https://endpoint/.../chat/completions?api-version={version}/chat/completions
// The SDK appends /chat/completions after the query string, which is wrong
// Fix to: https://endpoint/.../chat/completions?api-version={version}
const azureUrlPattern = /(https:\/\/[^?]+)(\?[^/]+)(\/.*)/;
const match = url.match(azureUrlPattern);
if (match) {
const [, basePath, queryParam, wrongPath] = match;
// Only fix if this looks like an Azure URL with deployments
if (basePath.includes("/deployments/") && queryParam.includes("api-version=")) {
// Check if basePath already contains the wrongPath (duplicate case)
const fixedUrl = basePath.endsWith(wrongPath)
? `${basePath}${queryParam}` // Already has path, just remove duplicate
: `${basePath}${wrongPath}${queryParam}`; // Need to move path before query
// Reconstruct input with fixed URL
if (typeof input === "string") {
return originalFetch(fixedUrl, init);
} else if (input instanceof URL) {
return originalFetch(new URL(fixedUrl), init);
} else if (input instanceof Request) {
return originalFetch(new Request(fixedUrl, input), init);
}
}
}
// Non-Azure URLs pass through unchanged
return originalFetch(input, init);
} as typeof fetch;
azureUrlFixInstalled = true;
}
export function isAzureUrlFixInstalled(): boolean {
return azureUrlFixInstalled;
}
/**
* Reset the URL fix installation state (for testing only)
* @internal
*/
export function resetAzureUrlFixForTesting(): void {
azureUrlFixInstalled = false;
}

View File

@ -0,0 +1,51 @@
import { describe, it, expect } from "vitest";
import {
resolveAzureEndpoint,
resolveAzureApiVersion,
resolveAzureDeployment,
} from "./model-auth.js";
describe("Azure provider configuration", () => {
it("should resolve Azure endpoint from environment", () => {
const endpoint = resolveAzureEndpoint({
AZURE_ENDPOINT: "https://eastus2.api.cognitive.microsoft.com",
});
expect(endpoint).toBe("https://eastus2.api.cognitive.microsoft.com");
});
it("should resolve Azure API version with default", () => {
const version = resolveAzureApiVersion({});
expect(version).toBe("2024-08-01-preview");
});
it("should resolve Azure API version from environment", () => {
const version = resolveAzureApiVersion({
AZURE_API_VERSION: "2024-02-01",
});
expect(version).toBe("2024-02-01");
});
it("should resolve Azure deployment from environment", () => {
const deployment = resolveAzureDeployment({
AZURE_DEPLOYMENT: "gpt-5.2",
});
expect(deployment).toBe("gpt-5.2");
});
it("should return undefined for missing endpoint", () => {
const endpoint = resolveAzureEndpoint({});
expect(endpoint).toBeUndefined();
});
it("should return undefined for missing deployment", () => {
const deployment = resolveAzureDeployment({});
expect(deployment).toBeUndefined();
});
it("should trim whitespace from values", () => {
const endpoint = resolveAzureEndpoint({
AZURE_ENDPOINT: " https://test.com ",
});
expect(endpoint).toBe("https://test.com");
});
});

View File

@ -229,6 +229,18 @@ export async function resolveApiKeyForProvider(params: {
export type EnvApiKeyResult = { apiKey: string; source: string };
export type ModelAuthMode = "api-key" | "oauth" | "token" | "mixed" | "aws-sdk" | "unknown";
export function resolveAzureEndpoint(env = process.env): string | undefined {
return env.AZURE_ENDPOINT?.trim() || undefined;
}
export function resolveAzureApiVersion(env = process.env): string {
return env.AZURE_API_VERSION?.trim() || "2024-08-01-preview";
}
export function resolveAzureDeployment(env = process.env): string | undefined {
return env.AZURE_DEPLOYMENT?.trim() || undefined;
}
export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
const normalized = normalizeProviderId(provider);
const applied = new Set(getShellEnvAppliedKeys());
@ -271,6 +283,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
const envMap: Record<string, string> = {
openai: "OPENAI_API_KEY",
azure: "AZURE_API_KEY",
google: "GEMINI_API_KEY",
groq: "GROQ_API_KEY",
deepgram: "DEEPGRAM_API_KEY",

View File

@ -0,0 +1,109 @@
import { describe, it, expect } from "vitest";
import { resolveImplicitAzureProvider } from "./models-config.providers.js";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
describe("Azure provider auto-discovery", () => {
it("should return null when environment variables are missing", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "moltbot-test-"));
const provider = await resolveImplicitAzureProvider({
agentDir: tempDir,
env: {},
});
expect(provider).toBeNull();
});
it("should auto-discover Azure provider from environment variables", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "moltbot-test-"));
const provider = await resolveImplicitAzureProvider({
agentDir: tempDir,
env: {
AZURE_ENDPOINT: "https://eastus2.api.cognitive.microsoft.com",
AZURE_API_KEY: "test-key",
AZURE_DEPLOYMENT: "gpt-5.2",
AZURE_API_VERSION: "2024-02-01",
},
});
expect(provider).not.toBeNull();
expect(provider?.apiKey).toBe("test-key");
expect(provider?.api).toBe("openai-completions");
expect(provider?.headers).toEqual({ "api-key": "test-key" });
});
it("should construct correct baseUrl with deployment and API version", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "moltbot-test-"));
const provider = await resolveImplicitAzureProvider({
agentDir: tempDir,
env: {
AZURE_ENDPOINT: "https://eastus2.api.cognitive.microsoft.com",
AZURE_API_KEY: "test-key",
AZURE_DEPLOYMENT: "gpt-5.2",
AZURE_API_VERSION: "2024-02-01",
},
});
expect(provider?.baseUrl).toBe(
"https://eastus2.api.cognitive.microsoft.com/openai/deployments/gpt-5.2/chat/completions?api-version=2024-02-01",
);
});
it("should create model definition with empty ID", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "moltbot-test-"));
const provider = await resolveImplicitAzureProvider({
agentDir: tempDir,
env: {
AZURE_ENDPOINT: "https://test.com",
AZURE_API_KEY: "key",
AZURE_DEPLOYMENT: "gpt-4",
},
});
expect(provider?.models).toHaveLength(1);
expect(provider?.models[0].id).toBe("");
expect(provider?.models[0].name).toBe("Azure gpt-4");
});
it("should detect reasoning models (o1/o3)", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "moltbot-test-"));
const provider = await resolveImplicitAzureProvider({
agentDir: tempDir,
env: {
AZURE_ENDPOINT: "https://test.com",
AZURE_API_KEY: "key",
AZURE_DEPLOYMENT: "gpt-o1-preview",
},
});
expect(provider?.models[0].reasoning).toBe(true);
});
it("should detect vision models", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "moltbot-test-"));
const provider = await resolveImplicitAzureProvider({
agentDir: tempDir,
env: {
AZURE_ENDPOINT: "https://test.com",
AZURE_API_KEY: "key",
AZURE_DEPLOYMENT: "gpt-4-vision",
},
});
expect(provider?.models[0].input).toEqual(["text", "image"]);
});
it("should use max_completion_tokens compatibility", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "moltbot-test-"));
const provider = await resolveImplicitAzureProvider({
agentDir: tempDir,
env: {
AZURE_ENDPOINT: "https://test.com",
AZURE_API_KEY: "key",
AZURE_DEPLOYMENT: "gpt-5.2",
},
});
expect(provider?.models[0].compat?.maxTokensField).toBe("max_completion_tokens");
});
});

View File

@ -5,7 +5,13 @@ import {
resolveCopilotApiToken,
} from "../providers/github-copilot-token.js";
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
import {
resolveAwsSdkEnvVarName,
resolveEnvApiKey,
resolveAzureEndpoint,
resolveAzureApiVersion,
resolveAzureDeployment,
} from "./model-auth.js";
import { discoverBedrockModels } from "./bedrock-discovery.js";
import {
buildSyntheticModelDefinition,
@ -359,6 +365,63 @@ async function buildOllamaProvider(): Promise<ProviderConfig> {
};
}
export async function resolveImplicitAzureProvider(params: {
agentDir: string;
env?: NodeJS.ProcessEnv;
}): Promise<ProviderConfig | null> {
const env = params.env ?? process.env;
const endpoint = resolveAzureEndpoint(env);
const deployment = resolveAzureDeployment(env);
const apiVersion = resolveAzureApiVersion(env);
const authStore = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
const apiKey =
resolveEnvApiKeyVarName("azure") ??
resolveApiKeyFromProfiles({ provider: "azure", store: authStore });
if (!endpoint || !deployment || !apiKey) {
return null;
}
// Construct Azure-compatible baseUrl
// Note: OpenAI SDK will append /chat/completions to this URL, resulting in:
// {endpoint}/openai/deployments/{deployment}/chat/completions?api-version={version}/chat/completions
// Our URL fix middleware (azure-url-fix.ts) intercepts fetch calls and corrects this to:
// {endpoint}/openai/deployments/{deployment}/chat/completions?api-version={version}
const baseUrl = `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}`;
// Create model definition for the Azure deployment
const models: ModelDefinitionConfig[] = [
{
id: deployment,
name: `Azure ${deployment}`,
reasoning: deployment.toLowerCase().includes("o1") || deployment.toLowerCase().includes("o3"),
input: deployment.toLowerCase().includes("vision") ? ["text", "image"] : ["text"],
cost: {
input: 10,
output: 30,
cacheRead: 2.5,
cacheWrite: 12.5,
},
contextWindow: 200000,
maxTokens: 16384,
compat: {
maxTokensField: "max_completion_tokens",
},
},
];
return {
baseUrl,
apiKey,
api: "openai-completions",
headers: {
"api-key": apiKey,
},
models,
};
}
export async function resolveImplicitProviders(params: {
agentDir: string;
}): Promise<ModelsConfig["providers"]> {
@ -418,6 +481,13 @@ export async function resolveImplicitProviders(params: {
providers.ollama = { ...(await buildOllamaProvider()), apiKey: ollamaKey };
}
// Azure provider - auto-discover from environment variables
// Supports OpenAI-compatible APIs (OpenAI, DeepSeek, etc.) on Azure
const azureProvider = await resolveImplicitAzureProvider(params);
if (azureProvider) {
providers.azure = azureProvider;
}
return providers;
}

View File

@ -85,6 +85,7 @@ import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import { MAX_IMAGE_BYTES } from "../../../media/constants.js";
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js";
import { detectAndLoadPromptImages } from "./images.js";
import { installAzureUrlFix, isAzureUrlFixInstalled } from "../../azure-url-fix.js";
export function injectHistoryImagesIntoMessages(
messages: AgentMessage[],
@ -133,6 +134,11 @@ export function injectHistoryImagesIntoMessages(
export async function runEmbeddedAttempt(
params: EmbeddedRunAttemptParams,
): Promise<EmbeddedRunAttemptResult> {
// Install Azure URL fix once per process
if (!isAzureUrlFixInstalled()) {
installAzureUrlFix();
}
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
const prevCwd = process.cwd();
const runAbortController = new AbortController();

View File

@ -72,6 +72,7 @@ export function resolveTranscriptPolicy(params: {
const modelId = params.modelId ?? "";
const isGoogle = isGoogleModelApi(params.modelApi);
const isAnthropic = isAnthropicApi(params.modelApi, provider);
const isAzure = provider === "azure";
const isOpenAi = isOpenAiProvider(provider) || (!provider && isOpenAiApi(params.modelApi));
const isMistral = isMistralModel({ provider, modelId });
const isOpenRouterGemini =
@ -85,7 +86,7 @@ export function resolveTranscriptPolicy(params: {
const needsNonImageSanitize = isGoogle || isAnthropic || isMistral || isOpenRouterGemini;
const sanitizeToolCallIds = isGoogle || isMistral;
const sanitizeToolCallIds = isGoogle || isMistral || isAzure;
const toolCallIdMode: ToolCallIdMode | undefined = isMistral
? "strict9"
: sanitizeToolCallIds
@ -98,12 +99,17 @@ export function resolveTranscriptPolicy(params: {
const normalizeAntigravityThinkingBlocks = isAntigravityClaudeModel;
return {
sanitizeMode: isOpenAi ? "images-only" : needsNonImageSanitize ? "full" : "images-only",
sanitizeToolCallIds: !isOpenAi && sanitizeToolCallIds,
sanitizeMode:
isOpenAi && !isAzure
? "images-only"
: needsNonImageSanitize || isAzure
? "full"
: "images-only",
sanitizeToolCallIds: (isAzure || !isOpenAi) && sanitizeToolCallIds,
toolCallIdMode,
repairToolUseResultPairing: !isOpenAi && repairToolUseResultPairing,
preserveSignatures: isAntigravityClaudeModel,
sanitizeThoughtSignatures: isOpenAi ? undefined : sanitizeThoughtSignatures,
sanitizeThoughtSignatures: isOpenAi && !isAzure ? undefined : sanitizeThoughtSignatures,
normalizeAntigravityThinkingBlocks,
applyGoogleTurnOrdering: !isOpenAi && isGoogle,
validateGeminiTurns: !isOpenAi && isGoogle,

View File

@ -9,6 +9,7 @@ export type AuthChoiceOption = {
export type AuthChoiceGroupId =
| "openai"
| "azure"
| "anthropic"
| "google"
| "copilot"
@ -41,6 +42,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
hint: "Codex OAuth + API key",
choices: ["openai-codex", "openai-api-key"],
},
{
value: "azure",
label: "Azure",
hint: "OpenAI, DeepSeek, and compatible models on Azure",
choices: ["azure-api-key"],
},
{
value: "anthropic",
label: "Anthropic",
@ -134,6 +141,11 @@ export function buildAuthChoiceOptions(params: {
});
options.push({ value: "chutes", label: "Chutes (OAuth)" });
options.push({ value: "openai-api-key", label: "OpenAI API key" });
options.push({
value: "azure-api-key",
label: "Azure API key",
hint: "OpenAI, DeepSeek, and compatible models on Azure",
});
options.push({ value: "openrouter-api-key", label: "OpenRouter API key" });
options.push({
value: "ai-gateway-api-key",

View File

@ -13,6 +13,7 @@ import {
} from "./google-gemini-model-default.js";
import {
applyAuthProfileConfig,
applyAzureConfig,
applyKimiCodeConfig,
applyKimiCodeProviderConfig,
applyMoonshotConfig,
@ -28,12 +29,14 @@ import {
applyVercelAiGatewayConfig,
applyVercelAiGatewayProviderConfig,
applyZaiConfig,
AZURE_DEFAULT_MODEL_REF,
KIMI_CODE_MODEL_REF,
MOONSHOT_DEFAULT_MODEL_REF,
OPENROUTER_DEFAULT_MODEL_REF,
SYNTHETIC_DEFAULT_MODEL_REF,
VENICE_DEFAULT_MODEL_REF,
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
setAzureApiKey,
setGeminiApiKey,
setKimiCodeApiKey,
setMoonshotApiKey,
@ -321,6 +324,38 @@ export async function applyAuthChoiceApiProviders(
return { config: nextConfig, agentModelOverride };
}
if (authChoice === "azure-api-key") {
const azureConfig = await applyAzureConfig({
prompter: params.prompter,
agentDir: params.agentDir,
});
nextConfig = { ...nextConfig, ...azureConfig };
if (params.setDefaultModel) {
nextConfig = {
...nextConfig,
agents: {
...nextConfig.agents,
defaults: {
...nextConfig.agents?.defaults,
model: {
...nextConfig.agents?.defaults?.model,
primary: AZURE_DEFAULT_MODEL_REF,
},
},
},
};
await params.prompter.note(
`Default model set to ${AZURE_DEFAULT_MODEL_REF}`,
"Model configured",
);
} else {
agentModelOverride = AZURE_DEFAULT_MODEL_REF;
await noteAgentModel(AZURE_DEFAULT_MODEL_REF);
}
return { config: nextConfig, agentModelOverride };
}
if (authChoice === "gemini-api-key") {
let hasCredential = false;

View File

@ -0,0 +1,139 @@
import type { MoltbotConfig } from "../config/config.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import { setAzureApiKey } from "./onboard-auth.credentials.js";
export const AZURE_DEFAULT_MODEL_ID = "gpt-5.2";
export const AZURE_DEFAULT_MODEL_REF = `azure/${AZURE_DEFAULT_MODEL_ID}`;
export async function applyAzureConfig(params: {
prompter: WizardPrompter;
agentDir?: string;
}): Promise<MoltbotConfig> {
const { prompter, agentDir } = params;
await prompter.note(
[
"Azure provider supports OpenAI-compatible models on Azure infrastructure:",
"",
"• OpenAI models: GPT-4, GPT-5.2, o1/o3",
"• DeepSeek models: DeepSeek-V3, DeepSeek-Chat",
"• Other compatible models deployed on Azure",
"",
"You'll need:",
"1. Azure endpoint (e.g., https://eastus2.api.cognitive.microsoft.com)",
"2. Azure API key",
"3. Deployment name (e.g., gpt-5.2, deepseek-chat)",
"4. API version (default: 2024-08-01-preview)",
"",
"Find these in Azure Portal under your resource's 'Keys and Endpoint'.",
].join("\n"),
"Azure Setup",
);
const endpoint = await prompter.text({
message: "Azure endpoint URL",
placeholder: "https://your-resource.cognitiveservices.azure.com",
validate: (value) => {
if (!value?.trim()) return "Endpoint is required";
try {
new URL(value);
return undefined;
} catch {
return "Invalid URL format";
}
},
});
const deployment = await prompter.text({
message: "Deployment name",
placeholder: "gpt-5.2",
validate: (value) => (!value?.trim() ? "Deployment name is required" : undefined),
});
const apiKey = await prompter.text({
message: "Azure API key",
validate: (value: string) => (!value?.trim() ? "API key is required" : undefined),
});
const apiVersion = await prompter.text({
message: "API version (optional)",
placeholder: "2024-08-01-preview",
initialValue: "2024-08-01-preview",
});
await setAzureApiKey(apiKey, agentDir);
const baseUrl = `${endpoint.trim()}/openai/deployments/${deployment.trim()}?api-version=${apiVersion.trim()}`;
return {
models: {
providers: {
azure: {
baseUrl,
apiKey: "AZURE_API_KEY",
api: "openai-completions",
headers: {
"api-key": "${AZURE_API_KEY}",
},
models: [
{
id: deployment.trim(),
name: `Azure ${deployment.trim()}`,
reasoning:
deployment.toLowerCase().includes("o1") || deployment.toLowerCase().includes("o3"),
input: deployment.toLowerCase().includes("vision") ? ["text", "image"] : ["text"],
cost: {
input: 10,
output: 30,
cacheRead: 2.5,
cacheWrite: 12.5,
},
contextWindow: 200000,
maxTokens: 16384,
compat: {
maxTokensField: "max_completion_tokens",
},
},
],
},
},
},
};
}
export function applyAzureProviderConfig(): MoltbotConfig {
return {
models: {
providers: {
azure: {
baseUrl:
"${AZURE_ENDPOINT}/openai/deployments/${AZURE_DEPLOYMENT}?api-version=${AZURE_API_VERSION}",
apiKey: "AZURE_API_KEY",
api: "openai-completions",
headers: {
"api-key": "${AZURE_API_KEY}",
},
models: [
{
id: "${AZURE_DEPLOYMENT}",
name: "Azure ${AZURE_DEPLOYMENT}",
reasoning: false,
input: ["text"],
cost: {
input: 10,
output: 30,
cacheRead: 2.5,
cacheWrite: 12.5,
},
contextWindow: 200000,
maxTokens: 16384,
compat: {
maxTokensField: "max_completion_tokens",
},
},
],
},
},
},
};
}

View File

@ -34,6 +34,19 @@ export async function setAnthropicApiKey(key: string, agentDir?: string) {
});
}
export async function setAzureApiKey(key: string, agentDir?: string) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "azure:default",
credential: {
type: "api_key",
provider: "azure",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setGeminiApiKey(key: string, agentDir?: string) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({

View File

@ -32,9 +32,16 @@ export {
applyOpencodeZenConfig,
applyOpencodeZenProviderConfig,
} from "./onboard-auth.config-opencode.js";
export {
applyAzureConfig,
applyAzureProviderConfig,
AZURE_DEFAULT_MODEL_ID,
AZURE_DEFAULT_MODEL_REF,
} from "./onboard-auth.config-azure.js";
export {
OPENROUTER_DEFAULT_MODEL_REF,
setAnthropicApiKey,
setAzureApiKey,
setGeminiApiKey,
setKimiCodeApiKey,
setMinimaxApiKey,

View File

@ -11,6 +11,7 @@ export type AuthChoice =
| "chutes"
| "openai-codex"
| "openai-api-key"
| "azure-api-key"
| "openrouter-api-key"
| "ai-gateway-api-key"
| "moonshot-api-key"