feat: add Azure Anthropic (Claude) provider support

- Add "Azure Anthropic (Claude)" to API_PROVIDERS in registry.py
  with ANTHROPIC_API_KEY auth (required for Claude CLI to route
  through custom base URL instead of default Anthropic endpoint)
- Add Azure env var template to .env.example
- Show Base URL input field for Azure provider in Settings UI
  with "Configured" state and Azure-specific placeholder
- Widen Settings modal for better readability with long URLs
- Add Azure endpoint detection and "Azure Mode" log label
- Rename misleading "GLM Mode" fallback label to "Alternative API"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
nogataka
2026-02-10 21:29:05 +09:00
parent 55064945a4
commit d2b3ba9aee
4 changed files with 62 additions and 22 deletions

View File

@@ -30,11 +30,18 @@
# ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-3-5-haiku@20241022
# ===================
# Alternative API Providers (GLM, Ollama, Kimi, Custom)
# Alternative API Providers (Azure, GLM, Ollama, Kimi, Custom)
# ===================
# Configure via Settings UI (recommended) or set env vars below.
# When both are set, env vars take precedence.
#
# Azure Anthropic (Claude):
# ANTHROPIC_BASE_URL=https://your-resource.services.ai.azure.com/anthropic
# ANTHROPIC_API_KEY=your-azure-api-key
# ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-6
# ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-5
# ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-haiku-4-5
#
# GLM (Zhipu AI):
# ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
# ANTHROPIC_AUTH_TOKEN=your-glm-api-key

View File

@@ -463,6 +463,7 @@ def create_client(
is_vertex = sdk_env.get("CLAUDE_CODE_USE_VERTEX") == "1"
is_alternative_api = bool(base_url) or is_vertex
is_ollama = "localhost:11434" in base_url or "127.0.0.1:11434" in base_url
is_azure = "services.ai.azure.com" in base_url
model = convert_model_for_vertex(model)
if sdk_env:
print(f" - API overrides: {', '.join(sdk_env.keys())}")
@@ -472,8 +473,10 @@ def create_client(
print(f" - Vertex AI Mode: Using GCP project '{project_id}' with model '{model}' in region '{region}'")
elif is_ollama:
print(" - Ollama Mode: Using local models")
elif is_azure:
print(f" - Azure Mode: Using {base_url}")
elif "ANTHROPIC_BASE_URL" in sdk_env:
print(f" - GLM Mode: Using {sdk_env['ANTHROPIC_BASE_URL']}")
print(f" - Alternative API: Using {sdk_env['ANTHROPIC_BASE_URL']}")
# Create a wrapper for bash_security_hook that passes project_dir via context
async def bash_hook_with_context(input_data, tool_use_id=None, context=None):

View File

@@ -676,6 +676,18 @@ API_PROVIDERS: dict[str, dict[str, Any]] = {
],
"default_model": "glm-4.7",
},
"azure": {
"name": "Azure Anthropic (Claude)",
"base_url": "",
"requires_auth": True,
"auth_env_var": "ANTHROPIC_API_KEY",
"models": [
{"id": "claude-opus-4-6", "name": "Claude Opus"},
{"id": "claude-sonnet-4-5", "name": "Claude Sonnet"},
{"id": "claude-haiku-4-5", "name": "Claude Haiku"},
],
"default_model": "claude-opus-4-6",
},
"ollama": {
"name": "Ollama (Local)",
"base_url": "http://localhost:11434",

View File

@@ -83,8 +83,10 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
}
const handleSaveCustomBaseUrl = () => {
if (customBaseUrlInput.trim() && !updateSettings.isPending) {
updateSettings.mutate({ api_base_url: customBaseUrlInput.trim() })
const effectiveBaseUrl = customBaseUrlInput || settings?.api_base_url || ''
if (effectiveBaseUrl.trim() && !updateSettings.isPending) {
updateSettings.mutate({ api_base_url: effectiveBaseUrl.trim() })
setCustomBaseUrlInput('')
}
}
@@ -102,12 +104,12 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
const currentProviderInfo: ProviderInfo | undefined = providers.find(p => p.id === currentProvider)
const isAlternativeProvider = currentProvider !== 'claude'
const showAuthField = isAlternativeProvider && currentProviderInfo?.requires_auth
const showBaseUrlField = currentProvider === 'custom'
const showBaseUrlField = currentProvider === 'custom' || currentProvider === 'azure'
const showCustomModelInput = currentProvider === 'custom' || currentProvider === 'ollama'
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent aria-describedby={undefined} className="sm:max-w-sm max-h-[85vh] overflow-y-auto">
<DialogContent aria-describedby={undefined} className="sm:max-w-lg max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Settings
@@ -289,22 +291,38 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
{showBaseUrlField && (
<div className="space-y-2 pt-1">
<Label className="text-sm">Base URL</Label>
<div className="flex gap-2">
<input
type="text"
value={customBaseUrlInput || settings.api_base_url || ''}
onChange={(e) => setCustomBaseUrlInput(e.target.value)}
placeholder="https://api.example.com/v1"
className="flex-1 py-1.5 px-3 text-sm border rounded-md bg-background"
/>
<Button
size="sm"
onClick={handleSaveCustomBaseUrl}
disabled={!customBaseUrlInput.trim() || isSaving}
>
Save
</Button>
</div>
{settings.api_base_url && !customBaseUrlInput && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<ShieldCheck size={14} className="text-green-500" />
<span className="truncate">{settings.api_base_url}</span>
<Button
variant="ghost"
size="sm"
className="h-auto py-0.5 px-2 text-xs shrink-0"
onClick={() => setCustomBaseUrlInput(settings.api_base_url || ' ')}
>
Change
</Button>
</div>
)}
{(!settings.api_base_url || customBaseUrlInput) && (
<div className="flex gap-2">
<input
type="text"
value={customBaseUrlInput.trim()}
onChange={(e) => setCustomBaseUrlInput(e.target.value)}
placeholder={currentProvider === 'azure' ? 'https://your-resource.services.ai.azure.com/anthropic' : 'https://api.example.com/v1'}
className="flex-1 py-1.5 px-3 text-sm border rounded-md bg-background"
/>
<Button
size="sm"
onClick={handleSaveCustomBaseUrl}
disabled={!customBaseUrlInput.trim() || isSaving}
>
Save
</Button>
</div>
)}
</div>
)}
</div>