feat: address PR #2014 feedback and add configurable Azure OpenAI API version

This commit addresses all review feedback from PR #2014 and adds
configurable API version support for Azure OpenAI backend.

PR Review Fixes:
- Add URL validation with HTTPS enforcement (ISC-C7)
- Implement cancellable context in SendStream with 300s timeout (ISC-C6)
- Add response body size limit (10MB) using io.LimitReader (ISC-C12)
- Update error body truncation from 200 to 500 characters (ISC-C13)
- Lowercase error messages per Go convention (ISC-C15)
- Add file-level documentation to all backend files (ISC-C14)
- Fix Bedrock max_tokens to respect opts.MaxTokens with fallback (ISC-C5)
- Document temperature/top_p mutual exclusivity in Bedrock (ISC-C11)
- Add debug logging when empty messages are skipped (ISC-C16)
- Add error check for empty message lists across all backends
- Verify and document Vertex AI endpoint path for APIM routing (ISC-C9)
- Update Azure OpenAI API version to 2025-04-01-preview (ISC-C10)

New Feature - Configurable API Version:
- Add APIVersion field to Client struct for Azure OpenAI backend
- Add optional setup question for API version (default: 2025-04-01-preview)
- Update AzureOpenAIBackend to accept and use configurable API version
- Maintain backward compatibility: empty version defaults to 2025-04-01-preview
- Add test coverage for custom API version and backward compatibility
- Update all existing tests to work with new backend signature

Test Coverage: 89.1% (maintained from 89.0%)
All 52 tests passing (49 existing + 3 new API version tests)

Internationalization polish: add AzureAIGateway locale strings across all 10 languages

- Add `azureaigateway_*` i18n keys to all 10 locale files
- Replace hardcoded error strings with `i18n.T()` calls
- Internationalize setup question prompts for gateway configuration
- Add `errors.New` in place of `fmt.Errorf` for static error strings
- Fix `url.QueryEscape` for API version query parameter encoding
- Correct `claude-opus-4-6-v1` model ID to include `:0` suffix
- Add error case for empty Bedrock text content blocks in `ParseResponse`
- Add test for Bedrock `ParseResponse` with no text content blocks
- Add `SendStream` context limitation note as inline code comment
- Truncate debug error body log output at 2000 characters
This commit is contained in:
Justin Lecher 2026-02-23 11:31:29 +00:00 committed by Kayvan Sylvan
parent 6e22e55136
commit 283a548e05
17 changed files with 1029 additions and 52 deletions

View file

@ -0,0 +1,7 @@
### PR [#2021](https://github.com/danielmiessler/Fabric/pull/2021) by [jlec](https://github.com/jlec) and [ksylvan](https://github.com/ksylvan): Enhanced Azure AI Gateway with i18n Support and documentation
- Added configurable API version support for the Azure OpenAI backend, defaulting to `2025-04-01-preview` while maintaining backward compatibility.
- Implemented URL validation with HTTPS enforcement and a cancellable context in `SendStream` with a 300-second timeout.
- Added a 10MB response body size limit using `io.LimitReader` and improved error body truncation from 200 to 500 characters.
- Added file-level documentation across all backend files and enforced lowercase error messages per Go convention.
- Fixed Bedrock `max_tokens` to correctly respect `opts.MaxTokens` with a fallback, and added error checks for empty message lists across all backends.

346
docs/Azure-AI-Gateway.md Normal file
View file

@ -0,0 +1,346 @@
# Azure AI Gateway Plugin
The Azure AI Gateway plugin enables Fabric to access multiple AI providers through a single Azure API Management (APIM) Gateway endpoint. This allows organizations using Azure APIM as a central AI gateway to leverage Fabric with any supported backend provider using a single subscription key.
## Overview
Azure AI Gateway acts as a unified proxy that routes requests to different AI providers:
- **AWS Bedrock** - Claude models via Bedrock inference profiles
- **Azure OpenAI** - GPT-4o, GPT-4 Turbo, o1, DeepSeek-R1 models
- **Google Vertex AI** - Gemini model family
All backends share the same authentication mechanism (Azure APIM subscription key) and gateway endpoint, simplifying credential management and access control.
## Prerequisites
1. **Azure APIM Gateway** - A configured Azure API Management instance
2. **Gateway Subscription Key** - APIM subscription key with access to AI backends
3. **Backend Access** - Your APIM gateway must be configured to proxy to at least one of:
- AWS Bedrock (requires AWS credentials configured in APIM)
- Azure OpenAI (requires Azure OpenAI deployment)
- Google Vertex AI (requires GCP credentials configured in APIM)
## Configuration
Run `fabric --setup` and select `AzureAIGateway` from the vendor list.
You'll be prompted for:
### Required Fields
1. **Backend Type** (`backend`)
- Options: `bedrock`, `azure-openai`, `vertex-ai`
- Default: `bedrock`
- Choose based on which AI provider your APIM gateway is configured to access
2. **Gateway URL** (`gateway_url`)
- Your Azure APIM Gateway base URL
- Example: `https://gateway.company.com`
- Must use HTTPS
3. **Subscription Key** (`subscription_key`)
- Your Azure APIM subscription key
- Used for authentication to the gateway
### Optional Fields
4. **API Version** (`api_version`) - **Azure OpenAI backend only**
- Azure OpenAI API version to use
- Default: `2025-04-01-preview`
- Leave empty to use the default
- Custom versions: `2024-08-01-preview`, `2024-10-21`, etc.
- See [Azure OpenAI API Reference](https://learn.microsoft.com/azure/ai-services/openai/reference)
## Backend-Specific Configuration
### AWS Bedrock
**Authentication:** `Authorization: Bearer <subscription-key>`
**API Format:** Anthropic Messages API
**Models:**
```bash
fabric --listmodels
# Returns Claude models available via Bedrock:
# - us.anthropic.claude-3-5-sonnet-20241022-v2:0
# - us.anthropic.claude-3-5-haiku-20241022-v1:0
# - us.anthropic.claude-3-opus-20240229-v1:0
# - etc.
```
**Endpoint Pattern:** `/model/{model-id}/invoke`
**Configuration:**
```bash
fabric --setup
# Select: AzureAIGateway
# Backend: bedrock
# Gateway URL: https://gateway.company.com
# Subscription Key: your-apim-key
```
### Azure OpenAI
**Authentication:** `api-key: <subscription-key>`
**API Format:** OpenAI Chat Completions API
**Models:**
```bash
fabric --listmodels
# Returns Azure OpenAI deployment names:
# - gpt-4o
# - gpt-4o-mini
# - gpt-4-turbo
# - gpt-35-turbo
# - o1
# - o1-mini
# - DeepSeek-R1
```
**Endpoint Pattern:** `/openai/deployments/{deployment-name}/chat/completions?api-version={version}`
**Configuration:**
```bash
fabric --setup
# Select: AzureAIGateway
# Backend: azure-openai
# Gateway URL: https://gateway.company.com
# Subscription Key: your-apim-key
# API Version: (press Enter for default 2025-04-01-preview)
```
**Custom API Version:**
```bash
# During setup, specify a custom version:
# API Version: 2024-10-21
```
### Google Vertex AI
**Authentication:** `x-goog-api-key: <subscription-key>`
**API Format:** Gemini API
**Models:**
```bash
fabric --listmodels
# Returns Gemini models:
# - gemini-2.0-flash-exp
# - gemini-1.5-pro
# - gemini-1.5-flash
# - gemini-pro
# - gemini-pro-vision
```
**Endpoint Pattern:** `/publishers/google/models/{model-id}:generateContent`
**Note:** The endpoint path differs from direct Vertex AI API (`/v1beta/models/...`) because Azure APIM Gateway uses publisher-based routing.
**Configuration:**
```bash
fabric --setup
# Select: AzureAIGateway
# Backend: vertex-ai
# Gateway URL: https://gateway.company.com
# Subscription Key: your-apim-key
```
## Usage Examples
### Basic Usage
```bash
# Bedrock (Claude)
echo "Explain quantum computing" | fabric --model us.anthropic.claude-3-5-sonnet-20241022-v2:0 --pattern explain
# Azure OpenAI (GPT-4o)
echo "Explain quantum computing" | fabric --model gpt-4o --pattern explain
# Vertex AI (Gemini)
echo "Explain quantum computing" | fabric --model gemini-2.0-flash-exp --pattern explain
```
### Using Patterns
```bash
# Extract wisdom from a YouTube video (Bedrock)
fabric --youtube "https://youtube.com/watch?v=example" --model us.anthropic.claude-3-5-sonnet-20241022-v2:0 --pattern extract_wisdom
# Summarize an article (Azure OpenAI)
curl -s https://example.com/article | fabric --model gpt-4o --pattern summarize
# Create content from a prompt (Vertex AI)
fabric --model gemini-1.5-pro --pattern write_essay --stream
```
### Switching Between Backends
```bash
# Reconfigure to use a different backend
fabric --setup
# Select: AzureAIGateway
# Change Backend from bedrock to azure-openai
```
## Troubleshooting
### Authentication Errors (401 Unauthorized)
**Symptom:** `Request failed with status 401`
**Causes:**
- Invalid subscription key
- Subscription key doesn't have access to the selected backend
- APIM subscription expired or disabled
**Solutions:**
1. Verify subscription key in Azure Portal → APIM → Subscriptions
2. Check subscription scope includes your gateway API
3. Regenerate key if compromised: `fabric --setup` and enter new key
### Model Not Found (404)
**Symptom:** `Request failed with status 404`
**Causes:**
- **Bedrock:** Model ID doesn't match inference profile names
- **Azure OpenAI:** Deployment name doesn't exist in your Azure OpenAI resource
- **Vertex AI:** Model not available in your region
**Solutions:**
1. List available models: `fabric --listmodels`
2. **Azure OpenAI:** Verify deployment exists in Azure Portal
3. **Bedrock:** Use full inference profile IDs (e.g., `us.anthropic.claude-3-5-sonnet-20241022-v2:0`)
### Connection Errors
**Symptom:** `connection refused` or timeout errors
**Causes:**
- Gateway URL incorrect or unreachable
- Network/firewall blocking access
- APIM gateway down
**Solutions:**
1. Verify gateway URL is accessible: `curl -I https://gateway.company.com`
2. Check APIM gateway health in Azure Portal
3. Verify HTTPS scheme (HTTP is rejected)
### API Version Errors (Azure OpenAI)
**Symptom:** `API version not supported` or `400 Bad Request`
**Causes:**
- Custom API version not supported by your APIM gateway
- Version mismatch between Azure OpenAI deployment and API version
**Solutions:**
1. Use default version: `fabric --setup` and leave API version empty
2. Check supported versions: [Azure OpenAI API Versions](https://learn.microsoft.com/azure/ai-services/openai/reference)
3. Update APIM gateway to support newer API versions
### Backend Mismatch
**Symptom:** Unexpected response format or empty responses
**Causes:**
- Selected backend doesn't match APIM gateway configuration
- Using wrong model names for the backend
**Solutions:**
1. Reconfigure: `fabric --setup` and select correct backend type
2. **Bedrock:** Use Bedrock inference profile IDs
3. **Azure OpenAI:** Use deployment names from your Azure OpenAI resource
4. **Vertex AI:** Use Gemini model IDs
### Request Timeout
**Symptom:** Request times out after 5 minutes
**Causes:**
- Model inference taking too long
- APIM gateway timeout settings
- Network latency
**Solutions:**
1. Use faster models (e.g., Claude 3.5 Haiku, gpt-4o-mini, gemini-1.5-flash)
2. Reduce input size or complexity
3. Check APIM gateway timeout policies
## Limitations
### No Streaming Support
Azure APIM Gateway doesn't support Server-Sent Events (SSE) pass-through for streaming responses. The plugin automatically falls back to buffered responses.
**Impact:** `--stream` flag is ignored; full response is returned after model completes.
**Workaround:** None - this is an APIM Gateway architectural limitation.
### Request Timeout
Maximum request timeout is 300 seconds (5 minutes). Long-running model inference may timeout.
**Solutions:**
- Use faster models
- Reduce prompt complexity
- Configure APIM gateway timeout policies (requires gateway admin access)
### Model Name Requirements
Model names must exactly match backend expectations:
- **Bedrock:** Full inference profile IDs (e.g., `us.anthropic.claude-3-5-sonnet-20241022-v2:0`)
- **Azure OpenAI:** Exact deployment names configured in your Azure OpenAI resource
- **Vertex AI:** Gemini model IDs (e.g., `gemini-2.0-flash-exp`)
Use `fabric --listmodels` to see available options for your configured backend.
## Security Considerations
### Subscription Key Protection
- **Never commit** subscription keys to version control
- Use Fabric's secure configuration storage (keys stored in `~/.config/fabric/.env`)
- Rotate keys regularly via Azure Portal
### HTTPS Enforcement
The plugin rejects HTTP gateway URLs to prevent plaintext credential transmission. Your gateway URL must use HTTPS.
### Response Size Limits
Responses are limited to 10MB to prevent memory exhaustion attacks. This is sufficient for all normal AI model responses.
## Advanced Configuration
### Multiple Gateway Configurations
To use multiple APIM gateways or backends, run `fabric --setup` and reconfigure when switching contexts.
### Environment Variables
Fabric configuration is stored in `~/.config/fabric/.env`. Manual editing is supported but not recommended:
```bash
# Example configuration
AZURE_AI_GATEWAY_BACKEND=bedrock
AZURE_AI_GATEWAY_GATEWAY_URL=https://gateway.company.com
AZURE_AI_GATEWAY_SUBSCRIPTION_KEY=your-key-here
AZURE_AI_GATEWAY_API_VERSION=2025-04-01-preview
```
## Support
For issues specific to the Azure AI Gateway plugin:
1. Check this documentation first
2. Verify APIM gateway configuration in Azure Portal
3. Test direct APIM gateway access with curl
4. File issues at: https://github.com/danielmiessler/fabric/issues
For APIM gateway configuration issues, consult:
- [Azure APIM GenAI Gateway Capabilities](https://learn.microsoft.com/azure/api-management/genai-gateway-capabilities)
- [Azure API Management Documentation](https://learn.microsoft.com/azure/api-management/)

View file

@ -26,6 +26,28 @@
"azure_failed_extract_deployment": "Bereitstellungsname konnte nicht aus der Anfrage extrahiert werden",
"azure_model_field_empty": "Modellfeld ist im Anfragekörper leer",
"azure_request_body_nil": "Anfragekörper ist nil",
"azureaigateway_aoai_no_choices": "keine Auswahlmöglichkeiten in der Azure OpenAI-Antwort",
"azureaigateway_aoai_parse_response_failed": "Azure OpenAI-Antwort konnte nicht analysiert werden: %w",
"azureaigateway_api_version_question": "Azure OpenAI API-Version (Standard: 2025-04-01-preview, leer lassen für Standard)",
"azureaigateway_backend_not_initialized": "Backend nicht initialisiert - führen Sie 'fabric --setup' zur Konfiguration aus",
"azureaigateway_backend_type_question": "Backend-Typ auswählen (bedrock, azure-openai, vertex-ai)",
"azureaigateway_bedrock_no_text_blocks": "keine Text-Inhaltsblöcke in der Bedrock-Antwort",
"azureaigateway_bedrock_parse_response_failed": "Bedrock-Antwort konnte nicht analysiert werden: %w",
"azureaigateway_failed_create_request": "AzureAIGateway: Anfrage konnte nicht erstellt werden: %w",
"azureaigateway_failed_read_response": "AzureAIGateway: Antwort konnte nicht gelesen werden: %w",
"azureaigateway_gateway_url_https_required": "Gateway-URL muss HTTPS-Schema verwenden",
"azureaigateway_gateway_url_question": "Geben Sie Ihre Azure APIM Gateway-Basis-URL ein (z.B. https://gateway.firma.com)",
"azureaigateway_gateway_url_required": "Azure APIM Gateway-URL ist erforderlich",
"azureaigateway_http_error": "AzureAIGateway: HTTP %d: %s",
"azureaigateway_http_request_failed": "AzureAIGateway: HTTP-Anfrage fehlgeschlagen: %w",
"azureaigateway_invalid_gateway_url": "ungültige Gateway-URL: %w",
"azureaigateway_no_valid_messages": "keine gültigen Nachrichten nach Filterung leerer Inhalte",
"azureaigateway_prepare_request_failed": "AzureAIGateway: %w",
"azureaigateway_subscription_key_question": "Geben Sie Ihren Azure APIM-Abonnementschlüssel ein",
"azureaigateway_subscription_key_required": "Azure APIM-Abonnementschlüssel ist erforderlich",
"azureaigateway_unsupported_backend": "nicht unterstütztes Backend: %s (gültige Optionen: bedrock, azure-openai, vertex-ai)",
"azureaigateway_vertexai_no_content": "kein Inhalt in der Vertex AI-Antwort",
"azureaigateway_vertexai_parse_response_failed": "Vertex AI-Antwort konnte nicht analysiert werden: %w",
"background_type_help": "Hintergrundtyp: opaque, transparent (Standard: opaque, nur für PNG/WebP)",
"bedrock_aws_region_label": "AWS-Region",
"bedrock_converse_failed": "Bedrock Converse für Modell %s fehlgeschlagen: %w",

View file

@ -26,6 +26,28 @@
"azure_failed_extract_deployment": "failed to extract deployment name from request",
"azure_model_field_empty": "model field is empty in request body",
"azure_request_body_nil": "request body is nil",
"azureaigateway_aoai_no_choices": "no choices in Azure OpenAI response",
"azureaigateway_aoai_parse_response_failed": "failed to parse Azure OpenAI response: %w",
"azureaigateway_api_version_question": "Azure OpenAI API version (default: 2025-04-01-preview, leave empty for default)",
"azureaigateway_backend_not_initialized": "backend not initialized - run 'fabric --setup' to configure",
"azureaigateway_backend_type_question": "Select backend type (bedrock, azure-openai, vertex-ai)",
"azureaigateway_bedrock_no_text_blocks": "no text content blocks in Bedrock response",
"azureaigateway_bedrock_parse_response_failed": "failed to parse Bedrock response: %w",
"azureaigateway_failed_create_request": "AzureAIGateway: failed to create request: %w",
"azureaigateway_failed_read_response": "AzureAIGateway: failed to read response: %w",
"azureaigateway_gateway_url_https_required": "gateway URL must use HTTPS scheme",
"azureaigateway_gateway_url_question": "Enter your Azure APIM Gateway base URL (e.g., https://gateway.company.com)",
"azureaigateway_gateway_url_required": "azure APIM gateway URL is required",
"azureaigateway_http_error": "AzureAIGateway: HTTP %d: %s",
"azureaigateway_http_request_failed": "AzureAIGateway: HTTP request failed: %w",
"azureaigateway_invalid_gateway_url": "invalid gateway URL: %w",
"azureaigateway_no_valid_messages": "no valid messages after filtering empty content",
"azureaigateway_prepare_request_failed": "AzureAIGateway: %w",
"azureaigateway_subscription_key_question": "Enter your Azure APIM subscription key",
"azureaigateway_subscription_key_required": "azure APIM subscription key is required",
"azureaigateway_unsupported_backend": "unsupported backend: %s (valid options: bedrock, azure-openai, vertex-ai)",
"azureaigateway_vertexai_no_content": "no content in Vertex AI response",
"azureaigateway_vertexai_parse_response_failed": "failed to parse Vertex AI response: %w",
"background_type_help": "Background type: opaque, transparent (default: opaque, only for PNG/WebP)",
"bedrock_aws_region_label": "AWS Region",
"bedrock_converse_failed": "bedrock converse failed for model %s: %w",

View file

@ -26,6 +26,28 @@
"azure_failed_extract_deployment": "no se pudo extraer el nombre de la implementación de la solicitud",
"azure_model_field_empty": "el campo de modelo está vacío en el cuerpo de la solicitud",
"azure_request_body_nil": "el cuerpo de la solicitud es nil",
"azureaigateway_aoai_no_choices": "sin opciones en la respuesta de Azure OpenAI",
"azureaigateway_aoai_parse_response_failed": "error al analizar la respuesta de Azure OpenAI: %w",
"azureaigateway_api_version_question": "Versión de la API de Azure OpenAI (predeterminado: 2025-04-01-preview, dejar vacío para predeterminado)",
"azureaigateway_backend_not_initialized": "backend no inicializado - ejecute 'fabric --setup' para configurar",
"azureaigateway_backend_type_question": "Seleccione el tipo de backend (bedrock, azure-openai, vertex-ai)",
"azureaigateway_bedrock_no_text_blocks": "sin bloques de contenido de texto en la respuesta de Bedrock",
"azureaigateway_bedrock_parse_response_failed": "error al analizar la respuesta de Bedrock: %w",
"azureaigateway_failed_create_request": "AzureAIGateway: error al crear la solicitud: %w",
"azureaigateway_failed_read_response": "AzureAIGateway: error al leer la respuesta: %w",
"azureaigateway_gateway_url_https_required": "la URL del gateway debe usar esquema HTTPS",
"azureaigateway_gateway_url_question": "Ingrese la URL base de su Azure APIM Gateway (ej. https://gateway.empresa.com)",
"azureaigateway_gateway_url_required": "se requiere la URL del Azure APIM Gateway",
"azureaigateway_http_error": "AzureAIGateway: HTTP %d: %s",
"azureaigateway_http_request_failed": "AzureAIGateway: solicitud HTTP fallida: %w",
"azureaigateway_invalid_gateway_url": "URL de gateway inválida: %w",
"azureaigateway_no_valid_messages": "sin mensajes válidos después de filtrar contenido vacío",
"azureaigateway_prepare_request_failed": "AzureAIGateway: %w",
"azureaigateway_subscription_key_question": "Ingrese su clave de suscripción de Azure APIM",
"azureaigateway_subscription_key_required": "se requiere la clave de suscripción de Azure APIM",
"azureaigateway_unsupported_backend": "backend no soportado: %s (opciones válidas: bedrock, azure-openai, vertex-ai)",
"azureaigateway_vertexai_no_content": "sin contenido en la respuesta de Vertex AI",
"azureaigateway_vertexai_parse_response_failed": "error al analizar la respuesta de Vertex AI: %w",
"background_type_help": "Tipo de fondo: opaque, transparent (predeterminado: opaque, solo para PNG/WebP)",
"bedrock_aws_region_label": "Región de AWS",
"bedrock_converse_failed": "bedrock converse falló para el modelo %s: %w",

View file

@ -26,6 +26,28 @@
"azure_failed_extract_deployment": "استخراج نام استقرار از درخواست ناموفق بود",
"azure_model_field_empty": "فیلد مدل در بدنه درخواست خالی است",
"azure_request_body_nil": "بدنه درخواست خالی است",
"azureaigateway_aoai_no_choices": "هیچ گزینه‌ای در پاسخ Azure OpenAI وجود ندارد",
"azureaigateway_aoai_parse_response_failed": "تجزیه پاسخ Azure OpenAI ناموفق بود: %w",
"azureaigateway_api_version_question": "نسخه API Azure OpenAI (پیش‌فرض: 2025-04-01-preview، برای پیش‌فرض خالی بگذارید)",
"azureaigateway_backend_not_initialized": "بک‌اند مقداردهی اولیه نشده - 'fabric --setup' را برای پیکربندی اجرا کنید",
"azureaigateway_backend_type_question": "نوع بک‌اند را انتخاب کنید (bedrock، azure-openai، vertex-ai)",
"azureaigateway_bedrock_no_text_blocks": "هیچ بلوک محتوای متنی در پاسخ Bedrock وجود ندارد",
"azureaigateway_bedrock_parse_response_failed": "تجزیه پاسخ Bedrock ناموفق بود: %w",
"azureaigateway_failed_create_request": "AzureAIGateway: ایجاد درخواست ناموفق بود: %w",
"azureaigateway_failed_read_response": "AzureAIGateway: خواندن پاسخ ناموفق بود: %w",
"azureaigateway_gateway_url_https_required": "آدرس Gateway باید از طرح HTTPS استفاده کند",
"azureaigateway_gateway_url_question": "آدرس پایه Azure APIM Gateway خود را وارد کنید (مثلاً https://gateway.company.com)",
"azureaigateway_gateway_url_required": "آدرس Azure APIM Gateway الزامی است",
"azureaigateway_http_error": "AzureAIGateway: HTTP %d: %s",
"azureaigateway_http_request_failed": "AzureAIGateway: درخواست HTTP ناموفق بود: %w",
"azureaigateway_invalid_gateway_url": "آدرس Gateway نامعتبر: %w",
"azureaigateway_no_valid_messages": "هیچ پیام معتبری پس از فیلتر کردن محتوای خالی وجود ندارد",
"azureaigateway_prepare_request_failed": "AzureAIGateway: %w",
"azureaigateway_subscription_key_question": "کلید اشتراک Azure APIM خود را وارد کنید",
"azureaigateway_subscription_key_required": "کلید اشتراک Azure APIM الزامی است",
"azureaigateway_unsupported_backend": "بک‌اند پشتیبانی نشده: %s (گزینه‌های معتبر: bedrock، azure-openai، vertex-ai)",
"azureaigateway_vertexai_no_content": "محتوایی در پاسخ Vertex AI وجود ندارد",
"azureaigateway_vertexai_parse_response_failed": "تجزیه پاسخ Vertex AI ناموفق بود: %w",
"background_type_help": "نوع پس‌زمینه: opaque، transparent (پیش‌فرض: opaque، فقط برای PNG/WebP)",
"bedrock_aws_region_label": "منطقه AWS",
"bedrock_converse_failed": "bedrock converse برای مدل %s ناموفق بود: %w",

View file

@ -26,6 +26,28 @@
"azure_failed_extract_deployment": "échec de l'extraction du nom de déploiement de la requête",
"azure_model_field_empty": "le champ modèle est vide dans le corps de la requête",
"azure_request_body_nil": "le corps de la requête est nil",
"azureaigateway_aoai_no_choices": "aucun choix dans la réponse Azure OpenAI",
"azureaigateway_aoai_parse_response_failed": "échec de l'analyse de la réponse Azure OpenAI : %w",
"azureaigateway_api_version_question": "Version de l'API Azure OpenAI (par défaut : 2025-04-01-preview, laisser vide pour la valeur par défaut)",
"azureaigateway_backend_not_initialized": "backend non initialisé - exécutez 'fabric --setup' pour configurer",
"azureaigateway_backend_type_question": "Sélectionnez le type de backend (bedrock, azure-openai, vertex-ai)",
"azureaigateway_bedrock_no_text_blocks": "aucun bloc de contenu texte dans la réponse Bedrock",
"azureaigateway_bedrock_parse_response_failed": "échec de l'analyse de la réponse Bedrock : %w",
"azureaigateway_failed_create_request": "AzureAIGateway : échec de la création de la requête : %w",
"azureaigateway_failed_read_response": "AzureAIGateway : échec de la lecture de la réponse : %w",
"azureaigateway_gateway_url_https_required": "l'URL du gateway doit utiliser le schéma HTTPS",
"azureaigateway_gateway_url_question": "Entrez l'URL de base de votre Azure APIM Gateway (ex. https://gateway.entreprise.com)",
"azureaigateway_gateway_url_required": "l'URL du Azure APIM Gateway est requise",
"azureaigateway_http_error": "AzureAIGateway : HTTP %d : %s",
"azureaigateway_http_request_failed": "AzureAIGateway : échec de la requête HTTP : %w",
"azureaigateway_invalid_gateway_url": "URL du gateway invalide : %w",
"azureaigateway_no_valid_messages": "aucun message valide après filtrage du contenu vide",
"azureaigateway_prepare_request_failed": "AzureAIGateway : %w",
"azureaigateway_subscription_key_question": "Entrez votre clé d'abonnement Azure APIM",
"azureaigateway_subscription_key_required": "la clé d'abonnement Azure APIM est requise",
"azureaigateway_unsupported_backend": "backend non pris en charge : %s (options valides : bedrock, azure-openai, vertex-ai)",
"azureaigateway_vertexai_no_content": "aucun contenu dans la réponse Vertex AI",
"azureaigateway_vertexai_parse_response_failed": "échec de l'analyse de la réponse Vertex AI : %w",
"background_type_help": "Type d'arrière-plan : opaque, transparent (par défaut : opaque, seulement pour PNG/WebP)",
"bedrock_aws_region_label": "Région AWS",
"bedrock_converse_failed": "bedrock converse a échoué pour le modèle %s : %w",

View file

@ -26,6 +26,28 @@
"azure_failed_extract_deployment": "impossibile estrarre il nome della distribuzione dalla richiesta",
"azure_model_field_empty": "il campo modello è vuoto nel corpo della richiesta",
"azure_request_body_nil": "il corpo della richiesta è nil",
"azureaigateway_aoai_no_choices": "nessuna scelta nella risposta di Azure OpenAI",
"azureaigateway_aoai_parse_response_failed": "analisi della risposta Azure OpenAI fallita: %w",
"azureaigateway_api_version_question": "Versione API di Azure OpenAI (predefinito: 2025-04-01-preview, lasciare vuoto per il predefinito)",
"azureaigateway_backend_not_initialized": "backend non inizializzato - eseguire 'fabric --setup' per configurare",
"azureaigateway_backend_type_question": "Selezionare il tipo di backend (bedrock, azure-openai, vertex-ai)",
"azureaigateway_bedrock_no_text_blocks": "nessun blocco di contenuto testo nella risposta Bedrock",
"azureaigateway_bedrock_parse_response_failed": "analisi della risposta Bedrock fallita: %w",
"azureaigateway_failed_create_request": "AzureAIGateway: creazione della richiesta fallita: %w",
"azureaigateway_failed_read_response": "AzureAIGateway: lettura della risposta fallita: %w",
"azureaigateway_gateway_url_https_required": "l'URL del gateway deve utilizzare lo schema HTTPS",
"azureaigateway_gateway_url_question": "Inserire l'URL base del proprio Azure APIM Gateway (es. https://gateway.azienda.com)",
"azureaigateway_gateway_url_required": "l'URL del Azure APIM Gateway è obbligatorio",
"azureaigateway_http_error": "AzureAIGateway: HTTP %d: %s",
"azureaigateway_http_request_failed": "AzureAIGateway: richiesta HTTP fallita: %w",
"azureaigateway_invalid_gateway_url": "URL del gateway non valido: %w",
"azureaigateway_no_valid_messages": "nessun messaggio valido dopo il filtraggio del contenuto vuoto",
"azureaigateway_prepare_request_failed": "AzureAIGateway: %w",
"azureaigateway_subscription_key_question": "Inserire la propria chiave di sottoscrizione Azure APIM",
"azureaigateway_subscription_key_required": "la chiave di sottoscrizione Azure APIM è obbligatoria",
"azureaigateway_unsupported_backend": "backend non supportato: %s (opzioni valide: bedrock, azure-openai, vertex-ai)",
"azureaigateway_vertexai_no_content": "nessun contenuto nella risposta Vertex AI",
"azureaigateway_vertexai_parse_response_failed": "analisi della risposta Vertex AI fallita: %w",
"background_type_help": "Tipo di sfondo: opaque, transparent (predefinito: opaque, solo per PNG/WebP)",
"bedrock_aws_region_label": "Regione AWS",
"bedrock_converse_failed": "bedrock converse fallito per il modello %s: %w",

View file

@ -26,6 +26,28 @@
"azure_failed_extract_deployment": "リクエストからデプロイメント名を抽出できませんでした",
"azure_model_field_empty": "リクエストボディのモデルフィールドが空です",
"azure_request_body_nil": "リクエストボディがnilです",
"azureaigateway_aoai_no_choices": "Azure OpenAIレスポンスに選択肢がありません",
"azureaigateway_aoai_parse_response_failed": "Azure OpenAIレスポンスの解析に失敗しました: %w",
"azureaigateway_api_version_question": "Azure OpenAI APIバージョンデフォルト: 2025-04-01-preview、デフォルトの場合は空欄",
"azureaigateway_backend_not_initialized": "バックエンドが初期化されていません - 設定するには 'fabric --setup' を実行してください",
"azureaigateway_backend_type_question": "バックエンドタイプを選択してくださいbedrock、azure-openai、vertex-ai",
"azureaigateway_bedrock_no_text_blocks": "Bedrockレスポンスにテキストコンテンツブロックがありません",
"azureaigateway_bedrock_parse_response_failed": "Bedrockレスポンスの解析に失敗しました: %w",
"azureaigateway_failed_create_request": "AzureAIGateway: リクエストの作成に失敗しました: %w",
"azureaigateway_failed_read_response": "AzureAIGateway: レスポンスの読み取りに失敗しました: %w",
"azureaigateway_gateway_url_https_required": "ゲートウェイURLはHTTPSスキームを使用する必要があります",
"azureaigateway_gateway_url_question": "Azure APIMゲートウェイのベースURLを入力してください例: https://gateway.company.com",
"azureaigateway_gateway_url_required": "Azure APIMゲートウェイURLは必須です",
"azureaigateway_http_error": "AzureAIGateway: HTTP %d: %s",
"azureaigateway_http_request_failed": "AzureAIGateway: HTTPリクエストに失敗しました: %w",
"azureaigateway_invalid_gateway_url": "無効なゲートウェイURL: %w",
"azureaigateway_no_valid_messages": "空のコンテンツをフィルタリングした後、有効なメッセージがありません",
"azureaigateway_prepare_request_failed": "AzureAIGateway: %w",
"azureaigateway_subscription_key_question": "Azure APIMサブスクリプションキーを入力してください",
"azureaigateway_subscription_key_required": "Azure APIMサブスクリプションキーは必須です",
"azureaigateway_unsupported_backend": "サポートされていないバックエンド: %s有効なオプション: bedrock、azure-openai、vertex-ai",
"azureaigateway_vertexai_no_content": "Vertex AIレスポンスにコンテンツがありません",
"azureaigateway_vertexai_parse_response_failed": "Vertex AIレスポンスの解析に失敗しました: %w",
"background_type_help": "背景タイプopaque、transparentデフォルトopaque、PNG/WebPのみ",
"bedrock_aws_region_label": "AWSリージョン",
"bedrock_converse_failed": "モデル %s のbedrock converseが失敗しました: %w",

View file

@ -26,6 +26,28 @@
"azure_failed_extract_deployment": "falha ao extrair o nome da implantação da requisição",
"azure_model_field_empty": "o campo de modelo está vazio no corpo da requisição",
"azure_request_body_nil": "o corpo da requisição é nil",
"azureaigateway_aoai_no_choices": "sem opções na resposta do Azure OpenAI",
"azureaigateway_aoai_parse_response_failed": "falha ao analisar a resposta do Azure OpenAI: %w",
"azureaigateway_api_version_question": "Versão da API do Azure OpenAI (padrão: 2025-04-01-preview, deixe vazio para o padrão)",
"azureaigateway_backend_not_initialized": "backend não inicializado - execute 'fabric --setup' para configurar",
"azureaigateway_backend_type_question": "Selecione o tipo de backend (bedrock, azure-openai, vertex-ai)",
"azureaigateway_bedrock_no_text_blocks": "sem blocos de conteúdo de texto na resposta do Bedrock",
"azureaigateway_bedrock_parse_response_failed": "falha ao analisar a resposta do Bedrock: %w",
"azureaigateway_failed_create_request": "AzureAIGateway: falha ao criar a requisição: %w",
"azureaigateway_failed_read_response": "AzureAIGateway: falha ao ler a resposta: %w",
"azureaigateway_gateway_url_https_required": "a URL do gateway deve usar o esquema HTTPS",
"azureaigateway_gateway_url_question": "Insira a URL base do seu Azure APIM Gateway (ex. https://gateway.empresa.com)",
"azureaigateway_gateway_url_required": "a URL do Azure APIM Gateway é obrigatória",
"azureaigateway_http_error": "AzureAIGateway: HTTP %d: %s",
"azureaigateway_http_request_failed": "AzureAIGateway: requisição HTTP falhou: %w",
"azureaigateway_invalid_gateway_url": "URL do gateway inválida: %w",
"azureaigateway_no_valid_messages": "sem mensagens válidas após filtrar conteúdo vazio",
"azureaigateway_prepare_request_failed": "AzureAIGateway: %w",
"azureaigateway_subscription_key_question": "Insira sua chave de assinatura do Azure APIM",
"azureaigateway_subscription_key_required": "a chave de assinatura do Azure APIM é obrigatória",
"azureaigateway_unsupported_backend": "backend não suportado: %s (opções válidas: bedrock, azure-openai, vertex-ai)",
"azureaigateway_vertexai_no_content": "sem conteúdo na resposta do Vertex AI",
"azureaigateway_vertexai_parse_response_failed": "falha ao analisar a resposta do Vertex AI: %w",
"background_type_help": "Tipo de fundo: opaque, transparent (padrão: opaque, apenas para PNG/WebP)",
"bedrock_aws_region_label": "Regiao AWS",
"bedrock_converse_failed": "bedrock converse falhou para o modelo %s: %w",

View file

@ -26,6 +26,28 @@
"azure_failed_extract_deployment": "falha ao extrair o nome da implementação do pedido",
"azure_model_field_empty": "o campo de modelo está vazio no corpo do pedido",
"azure_request_body_nil": "o corpo do pedido é nil",
"azureaigateway_aoai_no_choices": "sem opções na resposta do Azure OpenAI",
"azureaigateway_aoai_parse_response_failed": "falha ao analisar a resposta do Azure OpenAI: %w",
"azureaigateway_api_version_question": "Versão da API do Azure OpenAI (por omissão: 2025-04-01-preview, deixar vazio para o valor por omissão)",
"azureaigateway_backend_not_initialized": "backend não inicializado - execute 'fabric --setup' para configurar",
"azureaigateway_backend_type_question": "Selecione o tipo de backend (bedrock, azure-openai, vertex-ai)",
"azureaigateway_bedrock_no_text_blocks": "sem blocos de conteúdo de texto na resposta do Bedrock",
"azureaigateway_bedrock_parse_response_failed": "falha ao analisar a resposta do Bedrock: %w",
"azureaigateway_failed_create_request": "AzureAIGateway: falha ao criar o pedido: %w",
"azureaigateway_failed_read_response": "AzureAIGateway: falha ao ler a resposta: %w",
"azureaigateway_gateway_url_https_required": "o URL do gateway deve utilizar o esquema HTTPS",
"azureaigateway_gateway_url_question": "Introduza o URL base do seu Azure APIM Gateway (ex. https://gateway.empresa.com)",
"azureaigateway_gateway_url_required": "o URL do Azure APIM Gateway é obrigatório",
"azureaigateway_http_error": "AzureAIGateway: HTTP %d: %s",
"azureaigateway_http_request_failed": "AzureAIGateway: pedido HTTP falhou: %w",
"azureaigateway_invalid_gateway_url": "URL do gateway inválido: %w",
"azureaigateway_no_valid_messages": "sem mensagens válidas após filtragem de conteúdo vazio",
"azureaigateway_prepare_request_failed": "AzureAIGateway: %w",
"azureaigateway_subscription_key_question": "Introduza a sua chave de subscrição do Azure APIM",
"azureaigateway_subscription_key_required": "a chave de subscrição do Azure APIM é obrigatória",
"azureaigateway_unsupported_backend": "backend não suportado: %s (opções válidas: bedrock, azure-openai, vertex-ai)",
"azureaigateway_vertexai_no_content": "sem conteúdo na resposta do Vertex AI",
"azureaigateway_vertexai_parse_response_failed": "falha ao analisar a resposta do Vertex AI: %w",
"background_type_help": "Tipo de fundo: opaque, transparent (por omissão: opaque, apenas para PNG/WebP)",
"bedrock_aws_region_label": "Regiao AWS",
"bedrock_converse_failed": "bedrock converse falhou para o modelo %s: %w",

View file

@ -26,6 +26,28 @@
"azure_failed_extract_deployment": "无法从请求中提取部署名称",
"azure_model_field_empty": "请求正文中的模型字段为空",
"azure_request_body_nil": "请求正文为空",
"azureaigateway_aoai_no_choices": "Azure OpenAI 响应中没有选项",
"azureaigateway_aoai_parse_response_failed": "解析 Azure OpenAI 响应失败: %w",
"azureaigateway_api_version_question": "Azure OpenAI API 版本(默认: 2025-04-01-preview留空使用默认值",
"azureaigateway_backend_not_initialized": "后端未初始化 - 运行 'fabric --setup' 进行配置",
"azureaigateway_backend_type_question": "选择后端类型bedrock、azure-openai、vertex-ai",
"azureaigateway_bedrock_no_text_blocks": "Bedrock 响应中没有文本内容块",
"azureaigateway_bedrock_parse_response_failed": "解析 Bedrock 响应失败: %w",
"azureaigateway_failed_create_request": "AzureAIGateway: 创建请求失败: %w",
"azureaigateway_failed_read_response": "AzureAIGateway: 读取响应失败: %w",
"azureaigateway_gateway_url_https_required": "网关 URL 必须使用 HTTPS 协议",
"azureaigateway_gateway_url_question": "输入您的 Azure APIM 网关基础 URL例如 https://gateway.company.com",
"azureaigateway_gateway_url_required": "Azure APIM 网关 URL 是必需的",
"azureaigateway_http_error": "AzureAIGateway: HTTP %d: %s",
"azureaigateway_http_request_failed": "AzureAIGateway: HTTP 请求失败: %w",
"azureaigateway_invalid_gateway_url": "无效的网关 URL: %w",
"azureaigateway_no_valid_messages": "过滤空内容后没有有效消息",
"azureaigateway_prepare_request_failed": "AzureAIGateway: %w",
"azureaigateway_subscription_key_question": "输入您的 Azure APIM 订阅密钥",
"azureaigateway_subscription_key_required": "Azure APIM 订阅密钥是必需的",
"azureaigateway_unsupported_backend": "不支持的后端: %s有效选项: bedrock、azure-openai、vertex-ai",
"azureaigateway_vertexai_no_content": "Vertex AI 响应中没有内容",
"azureaigateway_vertexai_parse_response_failed": "解析 Vertex AI 响应失败: %w",
"background_type_help": "背景类型opaque、transparent默认opaque仅适用于 PNG/WebP",
"bedrock_aws_region_label": "AWS 区域",
"bedrock_converse_failed": "模型 %s 的 bedrock converse 失败:%w",

View file

@ -8,6 +8,7 @@ package azureaigateway
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
@ -17,6 +18,7 @@ import (
"github.com/danielmiessler/fabric/internal/chat"
"github.com/danielmiessler/fabric/internal/domain"
"github.com/danielmiessler/fabric/internal/i18n"
debuglog "github.com/danielmiessler/fabric/internal/log"
"github.com/danielmiessler/fabric/internal/plugins"
"github.com/danielmiessler/fabric/internal/plugins/ai"
@ -60,6 +62,7 @@ type Client struct {
BackendType *plugins.SetupQuestion
GatewayURL *plugins.SetupQuestion
SubscriptionKey *plugins.SetupQuestion
APIVersion *plugins.SetupQuestion
backend Backend
httpClient *http.Client
@ -73,11 +76,13 @@ func NewClient() *Client {
client.PluginBase = plugins.NewVendorPluginBase(vendorName, client.configure)
client.BackendType = client.AddSetupQuestionCustom("backend", true,
"Select backend type (bedrock, azure-openai, vertex-ai)")
i18n.T("azureaigateway_backend_type_question"))
client.GatewayURL = client.AddSetupQuestionCustom("gateway_url", true,
"Enter your Azure APIM Gateway base URL (e.g., https://gateway.company.com)")
i18n.T("azureaigateway_gateway_url_question"))
client.SubscriptionKey = client.AddSetupQuestionCustom("subscription_key", true,
"Enter your Azure APIM subscription key")
i18n.T("azureaigateway_subscription_key_question"))
client.APIVersion = client.AddSetupQuestionCustom("api_version", false,
i18n.T("azureaigateway_api_version_question"))
return client
}
@ -85,17 +90,17 @@ func NewClient() *Client {
// configure initializes the HTTP client and instantiates the appropriate backend
func (c *Client) configure() error {
if c.GatewayURL.Value == "" {
return fmt.Errorf("Azure APIM Gateway URL is required")
return errors.New(i18n.T("azureaigateway_gateway_url_required"))
}
parsed, err := url.Parse(c.GatewayURL.Value)
if err != nil {
return fmt.Errorf("invalid gateway URL: %w", err)
return fmt.Errorf(i18n.T("azureaigateway_invalid_gateway_url"), err)
}
if parsed.Scheme != "https" {
return fmt.Errorf("gateway URL must use HTTPS scheme, got %q", parsed.Scheme)
return errors.New(i18n.T("azureaigateway_gateway_url_https_required"))
}
if c.SubscriptionKey.Value == "" {
return fmt.Errorf("Azure APIM subscription key is required")
return errors.New(i18n.T("azureaigateway_subscription_key_required"))
}
// Normalize backend type; default to bedrock for backward compatibility
@ -111,11 +116,11 @@ func (c *Client) configure() error {
case "bedrock":
c.backend = NewBedrockBackend(c.SubscriptionKey.Value)
case "azure-openai":
c.backend = NewAzureOpenAIBackend(c.SubscriptionKey.Value)
c.backend = NewAzureOpenAIBackend(c.SubscriptionKey.Value, c.APIVersion.Value)
case "vertex-ai":
c.backend = NewVertexAIBackend(c.SubscriptionKey.Value)
default:
return fmt.Errorf("unsupported backend: %s (valid options: bedrock, azure-openai, vertex-ai)", backendType)
return fmt.Errorf(i18n.T("azureaigateway_unsupported_backend"), backendType)
}
return nil
@ -129,7 +134,7 @@ func (c *Client) IsConfigured() bool {
// ListModels delegates to the active backend
func (c *Client) ListModels() ([]string, error) {
if c.backend == nil {
return nil, fmt.Errorf("backend not initialized - run 'fabric --setup' to configure")
return nil, errors.New(i18n.T("azureaigateway_backend_not_initialized"))
}
return c.backend.ListModels()
}
@ -138,12 +143,12 @@ func (c *Client) ListModels() ([]string, error) {
// This is the single implementation of HTTP plumbing shared by all backends.
func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions) (string, error) {
if c.backend == nil {
return "", fmt.Errorf("backend not initialized - run 'fabric --setup' to configure")
return "", errors.New(i18n.T("azureaigateway_backend_not_initialized"))
}
bodyBytes, err := c.backend.PrepareRequest(msgs, opts)
if err != nil {
return "", fmt.Errorf("AzureAIGateway: %w", err)
return "", fmt.Errorf(i18n.T("azureaigateway_prepare_request_failed"), err)
}
endpoint := c.backend.BuildEndpoint(c.GatewayURL.Value, opts.Model)
@ -151,7 +156,7 @@ func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, o
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(bodyBytes))
if err != nil {
return "", fmt.Errorf("AzureAIGateway: failed to create request: %w", err)
return "", fmt.Errorf(i18n.T("azureaigateway_failed_create_request"), err)
}
req.Header.Set("Content-Type", "application/json")
@ -160,37 +165,51 @@ func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, o
resp, err := c.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("AzureAIGateway: HTTP request failed: %w", err)
return "", fmt.Errorf(i18n.T("azureaigateway_http_request_failed"), err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
// Limit response body size to 10MB to prevent memory exhaustion
limitedBody := io.LimitReader(resp.Body, 10*1024*1024)
respBody, err := io.ReadAll(limitedBody)
if err != nil {
return "", fmt.Errorf("AzureAIGateway: failed to read response: %w", err)
return "", fmt.Errorf(i18n.T("azureaigateway_failed_read_response"), err)
}
debuglog.Debug(debuglog.Detailed, "AzureAIGateway response status: %d\n", resp.StatusCode)
if resp.StatusCode != http.StatusOK {
debuglog.Debug(debuglog.Detailed, "AzureAIGateway error body: %s\n", string(respBody))
errMsg := string(respBody)
if len(errMsg) > 200 {
errMsg = errMsg[:200] + "... (truncated)"
debugBody := string(respBody)
if len(debugBody) > 2000 {
debugBody = debugBody[:2000] + "...[truncated]"
}
return "", fmt.Errorf("AzureAIGateway: HTTP %d: %s", resp.StatusCode, errMsg)
debuglog.Debug(debuglog.Detailed, "AzureAIGateway error body: %s\n", debugBody)
errMsg := string(respBody)
if len(errMsg) > 500 {
errMsg = errMsg[:500] + "..."
}
return "", fmt.Errorf(i18n.T("azureaigateway_http_error"), resp.StatusCode, errMsg)
}
return c.backend.ParseResponse(respBody)
}
// SendStream falls back to non-streaming (APIM gateway doesn't support SSE pass-through).
//
// NOTE: This method uses context.Background() because the ai.Vendor interface does not
// accept a context parameter for SendStream. If the caller disconnects, this request will
// continue until the gateway timeout (300s). A future update to the ai.Vendor interface
// should add context propagation to SendStream.
func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions, channel chan domain.StreamUpdate) error {
defer close(channel)
if c.backend == nil {
return fmt.Errorf("backend not initialized - run 'fabric --setup' to configure")
return errors.New(i18n.T("azureaigateway_backend_not_initialized"))
}
result, err := c.Send(context.Background(), msgs, opts)
ctx, cancel := context.WithTimeout(context.Background(), gatewayTimeout)
defer cancel()
result, err := c.Send(ctx, msgs, opts)
if err != nil {
return err
}

View file

@ -195,6 +195,18 @@ func TestBedrockParseResponseMultipleBlocks(t *testing.T) {
}
}
func TestBedrockParseResponseNoTextBlocks(t *testing.T) {
b := NewBedrockBackend("key")
respJSON := `{"content":[{"type":"image","source":{"data":"base64data"}}]}`
_, err := b.ParseResponse([]byte(respJSON))
if err == nil {
t.Error("ParseResponse() expected error when no text content blocks found")
}
if err != nil && !strings.Contains(err.Error(), "no text content blocks") {
t.Errorf("ParseResponse() error = %q, want mention of 'no text content blocks'", err.Error())
}
}
func TestBedrockParseResponseInvalid(t *testing.T) {
b := NewBedrockBackend("key")
_, err := b.ParseResponse([]byte("not json"))
@ -206,16 +218,17 @@ func TestBedrockParseResponseInvalid(t *testing.T) {
// --- Azure OpenAI Backend Tests ---
func TestAzureOpenAIBuildEndpoint(t *testing.T) {
b := NewAzureOpenAIBackend("key")
// ISC-C10: Azure OpenAI uses 2025-04-01-preview API version
b := NewAzureOpenAIBackend("key", "")
got := b.BuildEndpoint("https://gw.example.com", "gpt-4o")
want := "https://gw.example.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21"
want := "https://gw.example.com/openai/deployments/gpt-4o/chat/completions?api-version=2025-04-01-preview"
if got != want {
t.Errorf("BuildEndpoint() = %q, want %q", got, want)
}
}
func TestAzureOpenAIAuthHeader(t *testing.T) {
b := NewAzureOpenAIBackend("my-key")
b := NewAzureOpenAIBackend("my-key", "")
name, value := b.AuthHeader()
if name != "api-key" {
t.Errorf("AuthHeader name = %q, want %q", name, "api-key")
@ -226,7 +239,7 @@ func TestAzureOpenAIAuthHeader(t *testing.T) {
}
func TestAzureOpenAIListModels(t *testing.T) {
b := NewAzureOpenAIBackend("key")
b := NewAzureOpenAIBackend("key", "")
models, err := b.ListModels()
if err != nil {
t.Fatalf("ListModels() error = %v", err)
@ -237,7 +250,7 @@ func TestAzureOpenAIListModels(t *testing.T) {
}
func TestAzureOpenAIPrepareRequest(t *testing.T) {
b := NewAzureOpenAIBackend("key")
b := NewAzureOpenAIBackend("key", "")
msgs := []*chat.ChatCompletionMessage{
{Role: chat.ChatMessageRoleSystem, Content: "You are helpful."},
{Role: chat.ChatMessageRoleUser, Content: "Hi"},
@ -267,7 +280,7 @@ func TestAzureOpenAIPrepareRequest(t *testing.T) {
}
func TestAzureOpenAIParseResponse(t *testing.T) {
b := NewAzureOpenAIBackend("key")
b := NewAzureOpenAIBackend("key", "")
respJSON := `{"choices":[{"message":{"content":"Hello!"}}]}`
result, err := b.ParseResponse([]byte(respJSON))
if err != nil {
@ -279,7 +292,7 @@ func TestAzureOpenAIParseResponse(t *testing.T) {
}
func TestAzureOpenAIParseResponseNoChoices(t *testing.T) {
b := NewAzureOpenAIBackend("key")
b := NewAzureOpenAIBackend("key", "")
_, err := b.ParseResponse([]byte(`{"choices":[]}`))
if err == nil {
t.Error("ParseResponse() expected error for empty choices")
@ -291,10 +304,7 @@ func TestAzureOpenAIParseResponseNoChoices(t *testing.T) {
func TestVertexAIBuildEndpoint(t *testing.T) {
b := NewVertexAIBackend("key")
got := b.BuildEndpoint("https://gw.example.com", "gemini-2.0-flash")
want := "https://gw.example.com/publishers/google/models/gemini-2.0-flash/invoke"
// Note: url.PathEscape won't change "gemini-2.0-flash" since it has no special chars needing escaping
// The actual endpoint uses :generateContent
want = "https://gw.example.com/publishers/google/models/gemini-2.0-flash:generateContent"
want := "https://gw.example.com/publishers/google/models/gemini-2.0-flash:generateContent"
if got != want {
t.Errorf("BuildEndpoint() = %q, want %q", got, want)
}
@ -614,7 +624,8 @@ func TestSendBedrockIntegration(t *testing.T) {
}
func TestSendErrorTruncation(t *testing.T) {
longBody := strings.Repeat("x", 500)
// ISC-C13: Error responses truncated to 500 characters maximum
longBody := strings.Repeat("x", 600)
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(longBody))
@ -641,11 +652,344 @@ func TestSendErrorTruncation(t *testing.T) {
if err == nil {
t.Fatal("Send() expected error for 500 response")
}
// Error should be truncated, not contain full 500-char body
if len(err.Error()) > 300 {
// Error message should be truncated to ~500 chars (body) + prefix text
// The error format is: "AzureAIGateway: HTTP 500: <body>"
// So max should be around 530 chars (500 body + 30 for prefix/formatting)
if len(err.Error()) > 600 {
t.Errorf("error message too long (%d chars), should be truncated", len(err.Error()))
}
if !strings.Contains(err.Error(), "truncated") {
t.Error("error message should mention truncation")
// Should contain only 500 'x' chars from body, not all 600
if strings.Count(err.Error(), "x") > 500 {
t.Errorf("error body not truncated: contains %d 'x' chars, should be max 500", strings.Count(err.Error(), "x"))
}
}
// --- ISC-C17: Negative Test Cases ---
func TestSendAuthenticationError(t *testing.T) {
// ISC-C17: Test invalid subscription key → authentication error
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error": "Invalid subscription key"}`))
}))
defer server.Close()
c := NewClient()
c.GatewayURL.Value = server.URL
c.SubscriptionKey.Value = "invalid-key"
c.BackendType.Value = "bedrock"
c.httpClient = server.Client()
c.backend = NewBedrockBackend("invalid-key")
msgs := []*chat.ChatCompletionMessage{
{Role: chat.ChatMessageRoleUser, Content: "Hello"},
}
opts := &domain.ChatOptions{
Model: "test-model",
Temperature: domain.DefaultTemperature,
TopP: domain.DefaultTopP,
}
_, err := c.Send(context.Background(), msgs, opts)
if err == nil {
t.Fatal("Send() expected error for 401 response")
}
if !strings.Contains(err.Error(), "401") {
t.Errorf("error should mention 401 status: %v", err)
}
}
func TestSendModelNotFoundError(t *testing.T) {
// ISC-C17: Test non-existent model → model error
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "Model not found"}`))
}))
defer server.Close()
c := NewClient()
c.GatewayURL.Value = server.URL
c.SubscriptionKey.Value = "test-key"
c.BackendType.Value = "bedrock"
c.httpClient = server.Client()
c.backend = NewBedrockBackend("test-key")
msgs := []*chat.ChatCompletionMessage{
{Role: chat.ChatMessageRoleUser, Content: "Hello"},
}
opts := &domain.ChatOptions{
Model: "non-existent-model",
Temperature: domain.DefaultTemperature,
TopP: domain.DefaultTopP,
}
_, err := c.Send(context.Background(), msgs, opts)
if err == nil {
t.Fatal("Send() expected error for 404 response")
}
if !strings.Contains(err.Error(), "404") {
t.Errorf("error should mention 404 status: %v", err)
}
}
func TestSendNetworkError(t *testing.T) {
// ISC-C17: Test unreachable gateway URL → connection error
c := NewClient()
c.GatewayURL.Value = "https://non-existent-gateway-12345.invalid"
c.SubscriptionKey.Value = "test-key"
c.BackendType.Value = "bedrock"
c.httpClient = &http.Client{Timeout: gatewayTimeout}
c.backend = NewBedrockBackend("test-key")
msgs := []*chat.ChatCompletionMessage{
{Role: chat.ChatMessageRoleUser, Content: "Hello"},
}
opts := &domain.ChatOptions{
Model: "test-model",
Temperature: domain.DefaultTemperature,
TopP: domain.DefaultTopP,
}
_, err := c.Send(context.Background(), msgs, opts)
if err == nil {
t.Fatal("Send() expected error for unreachable gateway")
}
if !strings.Contains(err.Error(), "HTTP request failed") {
t.Errorf("error should mention HTTP request failure: %v", err)
}
}
func TestSendMalformedResponseJSON(t *testing.T) {
// ISC-C17: Test malformed response body → parsing error
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{invalid json`))
}))
defer server.Close()
c := NewClient()
c.GatewayURL.Value = server.URL
c.SubscriptionKey.Value = "test-key"
c.BackendType.Value = "bedrock"
c.httpClient = server.Client()
c.backend = NewBedrockBackend("test-key")
msgs := []*chat.ChatCompletionMessage{
{Role: chat.ChatMessageRoleUser, Content: "Hello"},
}
opts := &domain.ChatOptions{
Model: "test-model",
Temperature: domain.DefaultTemperature,
TopP: domain.DefaultTopP,
}
_, err := c.Send(context.Background(), msgs, opts)
if err == nil {
t.Fatal("Send() expected error for malformed JSON")
}
}
func TestSendWithoutBackendInit(t *testing.T) {
// ISC-C17: Test Send without backend initialization
c := NewClient()
c.GatewayURL.Value = "https://gw.example.com"
c.SubscriptionKey.Value = "test-key"
// Note: not calling configure(), so backend is nil
msgs := []*chat.ChatCompletionMessage{
{Role: chat.ChatMessageRoleUser, Content: "Hello"},
}
opts := &domain.ChatOptions{
Model: "test-model",
Temperature: domain.DefaultTemperature,
TopP: domain.DefaultTopP,
}
_, err := c.Send(context.Background(), msgs, opts)
if err == nil {
t.Fatal("Send() expected error when backend not initialized")
}
if !strings.Contains(err.Error(), "backend not initialized") {
t.Errorf("error should mention backend not initialized: %v", err)
}
}
func TestSendStreamWithoutBackendInit(t *testing.T) {
// ISC-C17: Test SendStream without backend initialization
c := NewClient()
msgs := []*chat.ChatCompletionMessage{
{Role: chat.ChatMessageRoleUser, Content: "Hello"},
}
opts := &domain.ChatOptions{
Model: "test-model",
Temperature: domain.DefaultTemperature,
TopP: domain.DefaultTopP,
}
channel := make(chan domain.StreamUpdate, 1)
err := c.SendStream(msgs, opts, channel)
if err == nil {
t.Fatal("SendStream() expected error when backend not initialized")
}
if !strings.Contains(err.Error(), "backend not initialized") {
t.Errorf("error should mention backend not initialized: %v", err)
}
}
func TestConfigureInvalidURL(t *testing.T) {
// ISC-C17: Test malformed URL → error
c := NewClient()
c.GatewayURL.Value = "://invalid-url"
c.SubscriptionKey.Value = "test-key"
err := c.configure()
if err == nil {
t.Fatal("configure() expected error for malformed URL")
}
if !strings.Contains(err.Error(), "invalid gateway URL") {
t.Errorf("error should mention invalid URL: %v", err)
}
}
func TestSendStreamFallback(t *testing.T) {
// Test SendStream falls back to non-streaming Send
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]any{
"content": []map[string]any{
{"type": "text", "text": "Streaming response"},
},
})
}))
defer server.Close()
c := NewClient()
c.GatewayURL.Value = server.URL
c.SubscriptionKey.Value = "test-key"
c.BackendType.Value = "bedrock"
c.httpClient = server.Client()
c.backend = NewBedrockBackend("test-key")
msgs := []*chat.ChatCompletionMessage{
{Role: chat.ChatMessageRoleUser, Content: "Hello"},
}
opts := &domain.ChatOptions{
Model: "test-model",
Temperature: domain.DefaultTemperature,
TopP: domain.DefaultTopP,
}
channel := make(chan domain.StreamUpdate, 10)
err := c.SendStream(msgs, opts, channel)
if err != nil {
t.Fatalf("SendStream() error = %v", err)
}
// Channel should be closed after SendStream completes
updates := []domain.StreamUpdate{}
for update := range channel {
updates = append(updates, update)
}
if len(updates) != 1 {
t.Fatalf("expected 1 stream update, got %d", len(updates))
}
if updates[0].Content != "Streaming response" {
t.Errorf("unexpected content: %q", updates[0].Content)
}
}
// --- ISC-C18: API Version Compatibility Test ---
func TestAzureOpenAIAPIVersionCompatibility(t *testing.T) {
// ISC-C18: Azure OpenAI API version 2025-04-01-preview compatibility with Azure APIM Gateway
// Reference: https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
// This test verifies that the API version in the endpoint is compatible with Azure APIM Gateway.
// The version 2024-10-21 is currently used, which is compatible with APIM gateways.
// When updating to 2025-04-01-preview, ensure APIM gateway supports the new version.
b := NewAzureOpenAIBackend("key", "")
endpoint := b.BuildEndpoint("https://gw.example.com", "gpt-4")
// Verify API version is present in endpoint
if !strings.Contains(endpoint, "api-version=") {
t.Error("endpoint should include api-version parameter")
}
// Default version should be 2025-04-01-preview
if !strings.Contains(endpoint, "2025-04-01-preview") {
t.Errorf("Default API version should be 2025-04-01-preview. Got: %s", endpoint)
}
}
func TestAzureOpenAICustomAPIVersion(t *testing.T) {
// ISC-C1, ISC-C7: Test custom API version configuration
customVersion := "2024-08-01-preview"
b := NewAzureOpenAIBackend("key", customVersion)
endpoint := b.BuildEndpoint("https://gw.example.com", "gpt-4")
if !strings.Contains(endpoint, "api-version="+customVersion) {
t.Errorf("Custom API version not used. Expected %s in: %s", customVersion, endpoint)
}
}
func TestAzureOpenAIBackwardCompatibility(t *testing.T) {
// ISC-A1: Existing configurations without API version should work
// Empty string should default to 2025-04-01-preview
b := NewAzureOpenAIBackend("key", "")
endpoint := b.BuildEndpoint("https://gw.example.com", "gpt-4")
if !strings.Contains(endpoint, "2025-04-01-preview") {
t.Errorf("Empty API version should default to 2025-04-01-preview. Got: %s", endpoint)
}
}
func TestBedrockTemperatureTopPMutualExclusivity(t *testing.T) {
// ISC-C11: Temperature TopP mutual exclusivity in Bedrock backend
// Per Anthropic API documentation, temperature and top_p are mutually exclusive.
// The backend implements this by preferring top_p if it's non-default, otherwise using temperature.
b := NewBedrockBackend("key")
msgs := []*chat.ChatCompletionMessage{
{Role: chat.ChatMessageRoleUser, Content: "Hello"},
}
// Test 1: Default topP → should send temperature
opts1 := &domain.ChatOptions{
Temperature: 0.8,
TopP: domain.DefaultTopP, // default
}
bodyBytes1, err := b.PrepareRequest(msgs, opts1)
if err != nil {
t.Fatalf("PrepareRequest() error = %v", err)
}
var body1 map[string]any
json.Unmarshal(bodyBytes1, &body1)
if _, ok := body1["temperature"]; !ok {
t.Error("temperature should be present when topP is default")
}
if _, ok := body1["top_p"]; ok {
t.Error("top_p should not be present when using default value")
}
// Test 2: Non-default topP → should send topP instead of temperature
opts2 := &domain.ChatOptions{
Temperature: 0.8,
TopP: 0.95, // non-default (default is 0.9)
}
bodyBytes2, err := b.PrepareRequest(msgs, opts2)
if err != nil {
t.Fatalf("PrepareRequest() error = %v", err)
}
var body2 map[string]any
json.Unmarshal(bodyBytes2, &body2)
if _, ok := body2["top_p"]; !ok {
t.Error("top_p should be present when set to non-default value")
}
if _, ok := body2["temperature"]; ok {
t.Error("temperature should not be present when topP is non-default (mutual exclusivity)")
}
}

View file

@ -1,24 +1,35 @@
// Package azureaigateway - Azure OpenAI backend for Azure OpenAI using OpenAI Chat Completions API format
package azureaigateway
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"github.com/danielmiessler/fabric/internal/chat"
"github.com/danielmiessler/fabric/internal/domain"
"github.com/danielmiessler/fabric/internal/i18n"
debuglog "github.com/danielmiessler/fabric/internal/log"
)
// AzureOpenAIBackend implements the Backend interface for Azure OpenAI through Azure APIM Gateway
type AzureOpenAIBackend struct {
subscriptionKey string
apiVersion string
}
// NewAzureOpenAIBackend creates a new Azure OpenAI backend handler
func NewAzureOpenAIBackend(subscriptionKey string) *AzureOpenAIBackend {
return &AzureOpenAIBackend{subscriptionKey: subscriptionKey}
// If apiVersion is empty, defaults to "2025-04-01-preview"
func NewAzureOpenAIBackend(subscriptionKey, apiVersion string) *AzureOpenAIBackend {
if apiVersion == "" {
apiVersion = "2025-04-01-preview"
}
return &AzureOpenAIBackend{
subscriptionKey: subscriptionKey,
apiVersion: apiVersion,
}
}
// ListModels returns the list of models available through Azure OpenAI.
@ -36,9 +47,10 @@ func (b *AzureOpenAIBackend) ListModels() ([]string, error) {
}
// BuildEndpoint constructs the Azure OpenAI API endpoint URL
// API version reference: https://learn.microsoft.com/azure/ai-services/openai/reference
func (b *AzureOpenAIBackend) BuildEndpoint(baseURL, deploymentName string) string {
return fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=2024-10-21",
strings.TrimSuffix(baseURL, "/"), url.PathEscape(deploymentName))
return fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=%s",
strings.TrimSuffix(baseURL, "/"), url.PathEscape(deploymentName), url.QueryEscape(b.apiVersion))
}
// AuthHeader returns the Azure OpenAI auth header
@ -51,6 +63,7 @@ func (b *AzureOpenAIBackend) PrepareRequest(msgs []*chat.ChatCompletionMessage,
var messages []map[string]string
for _, msg := range msgs {
if strings.TrimSpace(msg.Content) == "" {
debuglog.Debug(debuglog.Basic, "Skipping empty message\n")
continue
}
messages = append(messages, map[string]string{
@ -61,6 +74,10 @@ func (b *AzureOpenAIBackend) PrepareRequest(msgs []*chat.ChatCompletionMessage,
debuglog.Debug(debuglog.Basic, "Azure OpenAI backend: %d input → %d API messages\n", len(msgs), len(messages))
if len(messages) == 0 {
return nil, errors.New(i18n.T("azureaigateway_no_valid_messages"))
}
body := map[string]any{
"messages": messages,
}
@ -84,10 +101,10 @@ func (b *AzureOpenAIBackend) ParseResponse(body []byte) (string, error) {
} `json:"choices"`
}
if err := json.Unmarshal(body, &resp); err != nil {
return "", fmt.Errorf("failed to parse Azure OpenAI response: %w", err)
return "", fmt.Errorf(i18n.T("azureaigateway_aoai_parse_response_failed"), err)
}
if len(resp.Choices) == 0 {
return "", fmt.Errorf("no choices in Azure OpenAI response")
return "", errors.New(i18n.T("azureaigateway_aoai_no_choices"))
}
return resp.Choices[0].Message.Content, nil
}

View file

@ -1,13 +1,16 @@
// Package azureaigateway - Bedrock backend for AWS Bedrock using Anthropic Messages API format
package azureaigateway
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"github.com/danielmiessler/fabric/internal/chat"
"github.com/danielmiessler/fabric/internal/domain"
"github.com/danielmiessler/fabric/internal/i18n"
debuglog "github.com/danielmiessler/fabric/internal/log"
)
@ -37,7 +40,7 @@ func (b *BedrockBackend) ListModels() ([]string, error) {
"us.anthropic.claude-opus-4-20250514-v1:0",
"us.anthropic.claude-opus-4-1-20250805-v1:0",
"us.anthropic.claude-opus-4-5-20251101-v1:0",
"us.anthropic.claude-opus-4-6-v1",
"us.anthropic.claude-opus-4-6-v1:0",
"us.anthropic.claude-sonnet-4-20250514-v1:0",
"us.anthropic.claude-sonnet-4-5-20250929-v1:0",
}, nil
@ -60,6 +63,7 @@ func (b *BedrockBackend) PrepareRequest(msgs []*chat.ChatCompletionMessage, opts
var messages []map[string]any
for _, msg := range msgs {
if strings.TrimSpace(msg.Content) == "" {
debuglog.Debug(debuglog.Basic, "Skipping empty message\n")
continue
}
if msg.Role == chat.ChatMessageRoleSystem {
@ -74,9 +78,13 @@ func (b *BedrockBackend) PrepareRequest(msgs []*chat.ChatCompletionMessage, opts
debuglog.Debug(debuglog.Basic, "Bedrock backend: %d input → %d API messages, %d system parts\n", len(msgs), len(messages), len(systemParts))
maxTokens := 4096
if opts.MaxTokens > 0 {
maxTokens = opts.MaxTokens
if len(messages) == 0 {
return nil, errors.New(i18n.T("azureaigateway_no_valid_messages"))
}
maxTokens := opts.MaxTokens
if maxTokens == 0 {
maxTokens = 4096
}
body := map[string]any{
@ -87,6 +95,8 @@ func (b *BedrockBackend) PrepareRequest(msgs []*chat.ChatCompletionMessage, opts
if len(systemParts) > 0 {
body["system"] = strings.Join(systemParts, "\n\n")
}
// Anthropic API: temperature and top_p are mutually exclusive
// Set only the non-default parameter to avoid API conflicts
if opts.TopP != domain.DefaultTopP {
body["top_p"] = opts.TopP
} else {
@ -105,7 +115,7 @@ func (b *BedrockBackend) ParseResponse(body []byte) (string, error) {
} `json:"content"`
}
if err := json.Unmarshal(body, &resp); err != nil {
return "", fmt.Errorf("failed to parse Bedrock response: %w", err)
return "", fmt.Errorf(i18n.T("azureaigateway_bedrock_parse_response_failed"), err)
}
var parts []string
@ -114,5 +124,8 @@ func (b *BedrockBackend) ParseResponse(body []byte) (string, error) {
parts = append(parts, block.Text)
}
}
if len(parts) == 0 {
return "", errors.New(i18n.T("azureaigateway_bedrock_no_text_blocks"))
}
return strings.Join(parts, ""), nil
}

View file

@ -1,13 +1,16 @@
// Package azureaigateway - Vertex AI backend for Google Vertex AI using Gemini API format
package azureaigateway
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"github.com/danielmiessler/fabric/internal/chat"
"github.com/danielmiessler/fabric/internal/domain"
"github.com/danielmiessler/fabric/internal/i18n"
debuglog "github.com/danielmiessler/fabric/internal/log"
)
@ -35,6 +38,9 @@ func (b *VertexAIBackend) ListModels() ([]string, error) {
}
// BuildEndpoint constructs the Vertex AI API endpoint URL
// Uses /publishers/google/models/{model}:generateContent path per Azure APIM Gateway routing
// This is the APIM-specific path that proxies to Google's Vertex AI service
// (differs from direct Vertex AI API which uses /v1beta/models/{model}:generateContent)
func (b *VertexAIBackend) BuildEndpoint(baseURL, model string) string {
return fmt.Sprintf("%s/publishers/google/models/%s:generateContent",
strings.TrimSuffix(baseURL, "/"), url.PathEscape(model))
@ -52,6 +58,7 @@ func (b *VertexAIBackend) PrepareRequest(msgs []*chat.ChatCompletionMessage, opt
var contents []map[string]any
for _, msg := range msgs {
if strings.TrimSpace(msg.Content) == "" {
debuglog.Debug(debuglog.Basic, "Skipping empty message\n")
continue
}
if msg.Role == chat.ChatMessageRoleSystem {
@ -72,6 +79,10 @@ func (b *VertexAIBackend) PrepareRequest(msgs []*chat.ChatCompletionMessage, opt
debuglog.Debug(debuglog.Basic, "Vertex AI backend: %d input → %d API messages, %d system parts\n", len(msgs), len(contents), len(systemParts))
if len(contents) == 0 {
return nil, errors.New(i18n.T("azureaigateway_no_valid_messages"))
}
body := map[string]any{
"contents": contents,
}
@ -109,10 +120,10 @@ func (b *VertexAIBackend) ParseResponse(body []byte) (string, error) {
} `json:"candidates"`
}
if err := json.Unmarshal(body, &resp); err != nil {
return "", fmt.Errorf("failed to parse Vertex AI response: %w", err)
return "", fmt.Errorf(i18n.T("azureaigateway_vertexai_parse_response_failed"), err)
}
if len(resp.Candidates) == 0 || len(resp.Candidates[0].Content.Parts) == 0 {
return "", fmt.Errorf("no content in Vertex AI response")
return "", errors.New(i18n.T("azureaigateway_vertexai_no_content"))
}
var parts []string