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:
parent
57d9c09f6e
commit
9e857e6154
230
docs/providers/azure.md
Normal file
230
docs/providers/azure.md
Normal 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)
|
||||||
69
src/agents/azure-url-fix.test.ts
Normal file
69
src/agents/azure-url-fix.test.ts
Normal 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",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
74
src/agents/azure-url-fix.ts
Normal file
74
src/agents/azure-url-fix.ts
Normal 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;
|
||||||
|
}
|
||||||
51
src/agents/model-auth.azure.test.ts
Normal file
51
src/agents/model-auth.azure.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -229,6 +229,18 @@ export async function resolveApiKeyForProvider(params: {
|
|||||||
export type EnvApiKeyResult = { apiKey: string; source: string };
|
export type EnvApiKeyResult = { apiKey: string; source: string };
|
||||||
export type ModelAuthMode = "api-key" | "oauth" | "token" | "mixed" | "aws-sdk" | "unknown";
|
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 {
|
export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
||||||
const normalized = normalizeProviderId(provider);
|
const normalized = normalizeProviderId(provider);
|
||||||
const applied = new Set(getShellEnvAppliedKeys());
|
const applied = new Set(getShellEnvAppliedKeys());
|
||||||
@ -271,6 +283,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
|||||||
|
|
||||||
const envMap: Record<string, string> = {
|
const envMap: Record<string, string> = {
|
||||||
openai: "OPENAI_API_KEY",
|
openai: "OPENAI_API_KEY",
|
||||||
|
azure: "AZURE_API_KEY",
|
||||||
google: "GEMINI_API_KEY",
|
google: "GEMINI_API_KEY",
|
||||||
groq: "GROQ_API_KEY",
|
groq: "GROQ_API_KEY",
|
||||||
deepgram: "DEEPGRAM_API_KEY",
|
deepgram: "DEEPGRAM_API_KEY",
|
||||||
|
|||||||
109
src/agents/models-config.providers.azure.test.ts
Normal file
109
src/agents/models-config.providers.azure.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -5,7 +5,13 @@ import {
|
|||||||
resolveCopilotApiToken,
|
resolveCopilotApiToken,
|
||||||
} from "../providers/github-copilot-token.js";
|
} from "../providers/github-copilot-token.js";
|
||||||
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.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 { discoverBedrockModels } from "./bedrock-discovery.js";
|
||||||
import {
|
import {
|
||||||
buildSyntheticModelDefinition,
|
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: {
|
export async function resolveImplicitProviders(params: {
|
||||||
agentDir: string;
|
agentDir: string;
|
||||||
}): Promise<ModelsConfig["providers"]> {
|
}): Promise<ModelsConfig["providers"]> {
|
||||||
@ -418,6 +481,13 @@ export async function resolveImplicitProviders(params: {
|
|||||||
providers.ollama = { ...(await buildOllamaProvider()), apiKey: ollamaKey };
|
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;
|
return providers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -85,6 +85,7 @@ import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
|
|||||||
import { MAX_IMAGE_BYTES } from "../../../media/constants.js";
|
import { MAX_IMAGE_BYTES } from "../../../media/constants.js";
|
||||||
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js";
|
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js";
|
||||||
import { detectAndLoadPromptImages } from "./images.js";
|
import { detectAndLoadPromptImages } from "./images.js";
|
||||||
|
import { installAzureUrlFix, isAzureUrlFixInstalled } from "../../azure-url-fix.js";
|
||||||
|
|
||||||
export function injectHistoryImagesIntoMessages(
|
export function injectHistoryImagesIntoMessages(
|
||||||
messages: AgentMessage[],
|
messages: AgentMessage[],
|
||||||
@ -133,6 +134,11 @@ export function injectHistoryImagesIntoMessages(
|
|||||||
export async function runEmbeddedAttempt(
|
export async function runEmbeddedAttempt(
|
||||||
params: EmbeddedRunAttemptParams,
|
params: EmbeddedRunAttemptParams,
|
||||||
): Promise<EmbeddedRunAttemptResult> {
|
): Promise<EmbeddedRunAttemptResult> {
|
||||||
|
// Install Azure URL fix once per process
|
||||||
|
if (!isAzureUrlFixInstalled()) {
|
||||||
|
installAzureUrlFix();
|
||||||
|
}
|
||||||
|
|
||||||
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
||||||
const prevCwd = process.cwd();
|
const prevCwd = process.cwd();
|
||||||
const runAbortController = new AbortController();
|
const runAbortController = new AbortController();
|
||||||
|
|||||||
@ -72,6 +72,7 @@ export function resolveTranscriptPolicy(params: {
|
|||||||
const modelId = params.modelId ?? "";
|
const modelId = params.modelId ?? "";
|
||||||
const isGoogle = isGoogleModelApi(params.modelApi);
|
const isGoogle = isGoogleModelApi(params.modelApi);
|
||||||
const isAnthropic = isAnthropicApi(params.modelApi, provider);
|
const isAnthropic = isAnthropicApi(params.modelApi, provider);
|
||||||
|
const isAzure = provider === "azure";
|
||||||
const isOpenAi = isOpenAiProvider(provider) || (!provider && isOpenAiApi(params.modelApi));
|
const isOpenAi = isOpenAiProvider(provider) || (!provider && isOpenAiApi(params.modelApi));
|
||||||
const isMistral = isMistralModel({ provider, modelId });
|
const isMistral = isMistralModel({ provider, modelId });
|
||||||
const isOpenRouterGemini =
|
const isOpenRouterGemini =
|
||||||
@ -85,7 +86,7 @@ export function resolveTranscriptPolicy(params: {
|
|||||||
|
|
||||||
const needsNonImageSanitize = isGoogle || isAnthropic || isMistral || isOpenRouterGemini;
|
const needsNonImageSanitize = isGoogle || isAnthropic || isMistral || isOpenRouterGemini;
|
||||||
|
|
||||||
const sanitizeToolCallIds = isGoogle || isMistral;
|
const sanitizeToolCallIds = isGoogle || isMistral || isAzure;
|
||||||
const toolCallIdMode: ToolCallIdMode | undefined = isMistral
|
const toolCallIdMode: ToolCallIdMode | undefined = isMistral
|
||||||
? "strict9"
|
? "strict9"
|
||||||
: sanitizeToolCallIds
|
: sanitizeToolCallIds
|
||||||
@ -98,12 +99,17 @@ export function resolveTranscriptPolicy(params: {
|
|||||||
const normalizeAntigravityThinkingBlocks = isAntigravityClaudeModel;
|
const normalizeAntigravityThinkingBlocks = isAntigravityClaudeModel;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sanitizeMode: isOpenAi ? "images-only" : needsNonImageSanitize ? "full" : "images-only",
|
sanitizeMode:
|
||||||
sanitizeToolCallIds: !isOpenAi && sanitizeToolCallIds,
|
isOpenAi && !isAzure
|
||||||
|
? "images-only"
|
||||||
|
: needsNonImageSanitize || isAzure
|
||||||
|
? "full"
|
||||||
|
: "images-only",
|
||||||
|
sanitizeToolCallIds: (isAzure || !isOpenAi) && sanitizeToolCallIds,
|
||||||
toolCallIdMode,
|
toolCallIdMode,
|
||||||
repairToolUseResultPairing: !isOpenAi && repairToolUseResultPairing,
|
repairToolUseResultPairing: !isOpenAi && repairToolUseResultPairing,
|
||||||
preserveSignatures: isAntigravityClaudeModel,
|
preserveSignatures: isAntigravityClaudeModel,
|
||||||
sanitizeThoughtSignatures: isOpenAi ? undefined : sanitizeThoughtSignatures,
|
sanitizeThoughtSignatures: isOpenAi && !isAzure ? undefined : sanitizeThoughtSignatures,
|
||||||
normalizeAntigravityThinkingBlocks,
|
normalizeAntigravityThinkingBlocks,
|
||||||
applyGoogleTurnOrdering: !isOpenAi && isGoogle,
|
applyGoogleTurnOrdering: !isOpenAi && isGoogle,
|
||||||
validateGeminiTurns: !isOpenAi && isGoogle,
|
validateGeminiTurns: !isOpenAi && isGoogle,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export type AuthChoiceOption = {
|
|||||||
|
|
||||||
export type AuthChoiceGroupId =
|
export type AuthChoiceGroupId =
|
||||||
| "openai"
|
| "openai"
|
||||||
|
| "azure"
|
||||||
| "anthropic"
|
| "anthropic"
|
||||||
| "google"
|
| "google"
|
||||||
| "copilot"
|
| "copilot"
|
||||||
@ -41,6 +42,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
|||||||
hint: "Codex OAuth + API key",
|
hint: "Codex OAuth + API key",
|
||||||
choices: ["openai-codex", "openai-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",
|
value: "anthropic",
|
||||||
label: "Anthropic",
|
label: "Anthropic",
|
||||||
@ -134,6 +141,11 @@ export function buildAuthChoiceOptions(params: {
|
|||||||
});
|
});
|
||||||
options.push({ value: "chutes", label: "Chutes (OAuth)" });
|
options.push({ value: "chutes", label: "Chutes (OAuth)" });
|
||||||
options.push({ value: "openai-api-key", label: "OpenAI API key" });
|
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: "openrouter-api-key", label: "OpenRouter API key" });
|
||||||
options.push({
|
options.push({
|
||||||
value: "ai-gateway-api-key",
|
value: "ai-gateway-api-key",
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
} from "./google-gemini-model-default.js";
|
} from "./google-gemini-model-default.js";
|
||||||
import {
|
import {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
|
applyAzureConfig,
|
||||||
applyKimiCodeConfig,
|
applyKimiCodeConfig,
|
||||||
applyKimiCodeProviderConfig,
|
applyKimiCodeProviderConfig,
|
||||||
applyMoonshotConfig,
|
applyMoonshotConfig,
|
||||||
@ -28,12 +29,14 @@ import {
|
|||||||
applyVercelAiGatewayConfig,
|
applyVercelAiGatewayConfig,
|
||||||
applyVercelAiGatewayProviderConfig,
|
applyVercelAiGatewayProviderConfig,
|
||||||
applyZaiConfig,
|
applyZaiConfig,
|
||||||
|
AZURE_DEFAULT_MODEL_REF,
|
||||||
KIMI_CODE_MODEL_REF,
|
KIMI_CODE_MODEL_REF,
|
||||||
MOONSHOT_DEFAULT_MODEL_REF,
|
MOONSHOT_DEFAULT_MODEL_REF,
|
||||||
OPENROUTER_DEFAULT_MODEL_REF,
|
OPENROUTER_DEFAULT_MODEL_REF,
|
||||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||||
VENICE_DEFAULT_MODEL_REF,
|
VENICE_DEFAULT_MODEL_REF,
|
||||||
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||||
|
setAzureApiKey,
|
||||||
setGeminiApiKey,
|
setGeminiApiKey,
|
||||||
setKimiCodeApiKey,
|
setKimiCodeApiKey,
|
||||||
setMoonshotApiKey,
|
setMoonshotApiKey,
|
||||||
@ -321,6 +324,38 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
return { config: nextConfig, agentModelOverride };
|
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") {
|
if (authChoice === "gemini-api-key") {
|
||||||
let hasCredential = false;
|
let hasCredential = false;
|
||||||
|
|
||||||
|
|||||||
139
src/commands/onboard-auth.config-azure.ts
Normal file
139
src/commands/onboard-auth.config-azure.ts
Normal 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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -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) {
|
export async function setGeminiApiKey(key: string, agentDir?: string) {
|
||||||
// Write to resolved agent dir so gateway finds credentials on startup.
|
// Write to resolved agent dir so gateway finds credentials on startup.
|
||||||
upsertAuthProfile({
|
upsertAuthProfile({
|
||||||
|
|||||||
@ -32,9 +32,16 @@ export {
|
|||||||
applyOpencodeZenConfig,
|
applyOpencodeZenConfig,
|
||||||
applyOpencodeZenProviderConfig,
|
applyOpencodeZenProviderConfig,
|
||||||
} from "./onboard-auth.config-opencode.js";
|
} from "./onboard-auth.config-opencode.js";
|
||||||
|
export {
|
||||||
|
applyAzureConfig,
|
||||||
|
applyAzureProviderConfig,
|
||||||
|
AZURE_DEFAULT_MODEL_ID,
|
||||||
|
AZURE_DEFAULT_MODEL_REF,
|
||||||
|
} from "./onboard-auth.config-azure.js";
|
||||||
export {
|
export {
|
||||||
OPENROUTER_DEFAULT_MODEL_REF,
|
OPENROUTER_DEFAULT_MODEL_REF,
|
||||||
setAnthropicApiKey,
|
setAnthropicApiKey,
|
||||||
|
setAzureApiKey,
|
||||||
setGeminiApiKey,
|
setGeminiApiKey,
|
||||||
setKimiCodeApiKey,
|
setKimiCodeApiKey,
|
||||||
setMinimaxApiKey,
|
setMinimaxApiKey,
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export type AuthChoice =
|
|||||||
| "chutes"
|
| "chutes"
|
||||||
| "openai-codex"
|
| "openai-codex"
|
||||||
| "openai-api-key"
|
| "openai-api-key"
|
||||||
|
| "azure-api-key"
|
||||||
| "openrouter-api-key"
|
| "openrouter-api-key"
|
||||||
| "ai-gateway-api-key"
|
| "ai-gateway-api-key"
|
||||||
| "moonshot-api-key"
|
| "moonshot-api-key"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user