mirror of
https://github.com/danielmiessler/fabric.git
synced 2026-09-10 07:36:44 -04:00
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:
parent
6e22e55136
commit
283a548e05
7
cmd/generate_changelog/incoming/2021.txt
Normal file
7
cmd/generate_changelog/incoming/2021.txt
Normal 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
346
docs/Azure-AI-Gateway.md
Normal 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/)
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue