Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ad519f3
Añadir soporte para la conversión de respuestas de Claude y mejorar l…
gadgrandez Jan 23, 2026
d858cd8
Eliminar el manejo de conversión de respuesta de Claude en el proveed…
gadgrandez Jan 24, 2026
ed9b8ad
Eliminar claves "$schema" no soportadas de los parámetros para cumpli…
gadgrandez Jan 24, 2026
a158cf8
Agregar conversión de solicitudes de Claude a OpenAI en el cuerpo de …
gadgrandez Jan 24, 2026
34ede50
Agregar corrección para la conversión de protocolo Claude en Vertex A…
gadgrandez Jan 24, 2026
930c48c
Agregar función para sanitizar valores de esquema y eliminar claves n…
gadgrandez Jan 24, 2026
d49d72a
Agregar función para sanitizar solicitudes de Claude antes de enviarl…
gadgrandez Jan 25, 2026
9bc4cd4
Convert finish reason to lowercase in openAIFinishReasonToClaude func…
gadgrandez Jan 25, 2026
eae18fd
Agregar extracción y limpieza de razonamiento de texto en la conversi…
gadgrandez Jan 25, 2026
0889cfd
Merge tag 'v2.2.0' into attach_fix
gadgrandez Feb 10, 2026
6d9de76
Merge branch 'main' into feature/vertex-multi-publisher
gadgrandez Feb 12, 2026
0b85e95
feat(vertex): implement multi-publisher support and enhance routing l…
gadgrandez Feb 13, 2026
90b8d37
feat(vertex): add automatic publisher prefix for models in multi-publ…
gadgrandez Feb 13, 2026
3f8c04d
Merge branch 'main' into feature/vertex-multi-publisher
gadgrandez Feb 20, 2026
191615e
feat: add changes for vertexai multi provider support
gadgrandez Feb 21, 2026
a364aae
Merge branch 'main' into feature/vertex-multi-publisher
ggrandez Feb 23, 2026
da42288
Merge branch 'main' into feature/vertex-multi-publisher
ggrandez Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions changes/2.2.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# 2.2.2

- ai-proxy/vertex: add multi-publisher routing (Anthropic, Mistral, Codestral) with rawPredict paths and OpenAI-compatible fallback.
- ai-proxy/vertex: add optional `vertexPublisher` override for routing decisions.
- ai-proxy/vertex: fix Claude `/v1/messages` conversion by pre-converting and sanitizing requests before Vertex handling.
- ai-proxy/conversion: normalize finish_reason mapping and extract reasoning text from tagged content.
- tests: add vertex routing and multi-publisher coverage in unit and integration-style tests.
- docs: add VERTEX_CLAUDE_PROTOCOL_FIX.md with root cause and fix details.
387 changes: 387 additions & 0 deletions plugins/wasm-go/extensions/ai-proxy/VERTEX_CLAUDE_PROTOCOL_FIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,387 @@
# Corrección: Conversión de Protocolo Claude para Vertex AI

**Fecha:** 24 de enero de 2026
**Componente:** Higress AI Proxy - Proveedor Vertex AI
**Tipo:** Corrección de Bug (Protocol Conversion)

---

## 1. Resumen Ejecutivo

Se identificó y corrigió un bug crítico en el proveedor Vertex AI que impedía el uso del endpoint `/v1/messages` (Claude Messages API) cuando las solicitudes incluían herramientas (tools). El problema se manifestaba con el error:

```
tools[0].function_declarations[0].name: [REQUIRED_FIELD_MISSING]
```

### Impacto
- **Antes del fix**: Vertex AI solo funcionaba con `/v1/chat/completions` (OpenAI format)
- **Después del fix**: Vertex AI soporta ambos endpoints (`/v1/messages` y `/v1/chat/completions`)

---

## 2. Contexto Técnico

### 2.1 Arquitectura de Conversión de Protocolos

Higress AI Proxy implementa un sistema de conversión bidireccional entre dos formatos principales:

1. **Claude Messages API** (`/v1/messages`)
- Formato: `tools[].name` directamente en el objeto tool
- Ejemplo:
```json
{
"tools": [{
"name": "get_weather",
"description": "Get weather info",
"input_schema": { "type": "object", ... }
}]
}
```

2. **OpenAI Chat Completions API** (`/v1/chat/completions`)
- Formato: `tools[].function.name` anidado
- Ejemplo:
```json
{
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather info",
"parameters": { "type": "object", ... }
}
}]
}
```

### 2.2 Flujo de Conversión (Pre-Fix)

El flujo de conversión automática funcionaba en dos etapas:

**Etapa 1: Reescritura de Path** ([main.go#L214-L222](main.go#L214-L222))
```go
if strings.HasPrefix(path, "/v1/messages") {
// Detecta Claude format y establece flag
c.SetContext("needClaudeResponseConversion", true)
// Reescribe path a formato OpenAI
path = strings.Replace(path, "/v1/messages", "/v1/chat/completions", 1)
}
```

**Etapa 2: Conversión de Body** ([provider/provider.go#L935-L948](provider/provider.go#L935-L948))
```go
func (p *ProviderBase) handleRequestBody(body []byte, ...) {
needClaudeConversion, _ := ctx.GetContext("needClaudeResponseConversion").(bool)
if needClaudeConversion {
converter := &ClaudeToOpenAIConverter{}
convertedBody, err := converter.ConvertClaudeRequestToOpenAI(body)
// ... conversión y retorno
}
}
```

Este método `handleRequestBody` es invocado por la mayoría de proveedores (OpenAI, Claude, Gemini, etc.) pero **NO por Vertex AI**.

---

## 3. Problema Identificado

### 3.1 Síntomas

Cuando se enviaba una solicitud a Vertex AI usando el endpoint `/v1/messages` con herramientas:

```bash
# Request
POST /v1/messages
{
"model": "claude-3-5-sonnet-v2@20241022",
"tools": [{
"name": "get_weather",
"description": "Get weather information",
"input_schema": { ... }
}],
"messages": [...]
}

# Response
Error: tools[0].function_declarations[0].name: [REQUIRED_FIELD_MISSING]
```

### 3.2 Observaciones Clave

1. ✅ **Funcionaba** sin tools (solo mensajes)
2. ✅ **Funcionaba** con otros proveedores (OpenAI, Claude, Gemini) usando tools
3. ❌ **Fallaba** solo con Vertex AI + tools + `/v1/messages`
4. ✅ **Funcionaba** con Vertex AI usando `/v1/chat/completions` directamente

### 3.3 Análisis de Causas

#### Hipótesis Inicial
El path se reescribía correctamente (`/v1/messages` → `/v1/chat/completions`), pero el body mantenía la estructura Claude sin conversión.

#### Investigación del Código

**Proveedor OpenAI** ([provider/openai.go#L132-L148](provider/openai.go#L132-L148)):
```go
func (p *openaiProvider) OnRequestBody(body []byte, ...) {
return p.handleRequestBody(body, ...) // ✅ Usa handleRequestBody
}
```

**Proveedor Claude** ([provider/claude.go#L330-L356](provider/claude.go#L330-L356)):
```go
func (p *claudeProvider) OnRequestBody(body []byte, ...) {
return p.handleRequestBody(body, ...) // ✅ Usa handleRequestBody
}
```

**Proveedor Vertex** ([provider/vertex.go#L233-L299](provider/vertex.go#L233-L299)):
```go
func (p *vertexProvider) OnRequestBody(body []byte, ...) {
// ❌ NO usa handleRequestBody
// Implementa lógica custom directamente
if p.IsOriginal() { ... }

// Parsea body sin conversión previa
request := &openaiRequest{}
json.Unmarshal(body, request) // 💥 Body aún en formato Claude!

// ... lógica específica de Vertex
}
```

#### Causa Raíz Confirmada

Vertex AI tiene una implementación custom de `OnRequestBody` que:
1. **Bypass completo** de `handleRequestBody`
2. **Parsea directamente** el body como `openaiRequest`
3. **Asume** que el body ya está en formato OpenAI

Cuando llega una solicitud `/v1/messages`:
- El path se reescribe ✅
- El flag `needClaudeResponseConversion` se establece ✅
- Pero el **body NO se convierte** ❌
- Vertex intenta parsear body Claude como si fuera OpenAI
- Los tools tienen `tool.name` pero Vertex espera `tool.function.name`
- Resultado: campo `name` vacío → error de validación

---

## 4. Solución Implementada

### 4.1 Estrategia

Agregar la conversión Claude→OpenAI **antes** de que Vertex parsee el body, similar a como lo hacen otros proveedores.

### 4.2 Ubicación de la Corrección

Archivo: [provider/vertex.go](provider/vertex.go)
Método: `OnRequestBody`
Líneas: ~252-262

### 4.3 Código Implementado

```go
func (p *vertexProvider) OnRequestBody(body []byte, ...) types.Action {
if p.IsOriginal() {
// Modo Raw - no convertir
return types.ActionContinue
}

// 🆕 NUEVA LÓGICA: Conversión Claude→OpenAI si es necesaria
needClaudeConversion, _ := ctx.GetContext("needClaudeResponseConversion").(bool)
if needClaudeConversion {
converter := &ClaudeToOpenAIConverter{}
convertedBody, err := converter.ConvertClaudeRequestToOpenAI(body)
if err != nil {
log.Errorf("failed to convert claude request to openai: %v", err)
return types.ActionContinue
}
body = convertedBody
}

// Ahora el body está garantizado en formato OpenAI
request := &openaiRequest{}
if err := json.Unmarshal(body, request); err != nil {
// ... manejo de error
}

// ... resto de la lógica de Vertex
}
```

### 4.4 Flujo Post-Fix

```
1. Request: POST /v1/messages (Claude format)
2. main.go: Detecta /v1/messages
- Establece needClaudeResponseConversion = true
- Reescribe path → /v1/chat/completions
3. Vertex.OnRequestBody:
- ✅ Lee flag needClaudeResponseConversion
- ✅ Convierte body: Claude → OpenAI
- ✅ Parsea body ya convertido
- ✅ Procesa tools correctamente (tool.function.name disponible)
4. Vertex API: Recibe request en formato válido
5. Response: Éxito ✅
```

---

## 5. Validación

### 5.1 Tests Creados

Durante la investigación se crearon tests de validación:

**Test 1: Conversión de Tools**
```go
// Verifica que tool.name → tool.function.name
func TestClaudeToOpenAIToolNameConversion(t *testing.T) {
claudeReq := `{
"tools": [{"name": "get_weather", ...}]
}`

converter := &ClaudeToOpenAIConverter{}
result, _ := converter.ConvertClaudeRequestToOpenAI([]byte(claudeReq))

// Verifica que tool.function.name existe y es "get_weather"
assert.Equal(t, "get_weather", parsed.Tools[0].Function.Name)
}
```

**Test 2: Integración Vertex**
```go
func TestVertexToolNameLoss(t *testing.T) {
// Simula request Claude con tools
// Ejecuta OnRequestBody del proveedor Vertex
// Verifica que tool.function.name se preserva
}
```

### 5.2 Resultados

```bash
$ go test -v -run TestVertexToolNameLoss ./provider
=== RUN TestVertexToolNameLoss
--- PASS: TestVertexToolNameLoss (0.00s)
PASS
ok extensions/ai-proxy/provider 0.234s
```

### 5.3 Testing Manual

**Comando:**
```bash
curl -X POST http://localhost/v1/messages \
-H "Content-Type: application/json" \
-d '{
"model": "claude-3-5-sonnet-v2@20241022",
"tools": [{
"name": "get_weather",
"description": "Get weather information",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string"}
}
}
}],
"messages": [
{"role": "user", "content": "What is the weather in Paris?"}
]
}'
```

**Resultado:** ✅ Éxito - Sin errores de validación

---

## 6. Impacto y Beneficios

### 6.1 Compatibilidad Mejorada

| Escenario | Antes | Después |
|-----------|-------|---------|
| Vertex + `/v1/chat/completions` | ✅ | ✅ |
| Vertex + `/v1/messages` (sin tools) | ✅ | ✅ |
| Vertex + `/v1/messages` (con tools) | ❌ | ✅ |
| Otros proveedores | ✅ | ✅ |

### 6.2 Sin Breaking Changes

- ✅ Modo Raw preservado (bypass completo)
- ✅ Modo OpenAI Compatible sin cambios
- ✅ Modo Standard con nueva capacidad
- ✅ Backward compatible al 100%

### 6.3 Consistencia del Sistema

Ahora **todos los proveedores** aplican la conversión automática Claude→OpenAI cuando se detecta el endpoint `/v1/messages`:

- OpenAI ✅ (via handleRequestBody)
- Claude ✅ (via handleRequestBody)
- Gemini ✅ (via handleRequestBody)
- **Vertex ✅ (via custom logic)** 🆕

---

## 7. Lecciones Aprendidas

### 7.1 Arquitectura

1. **Inconsistencia de implementación**: Vertex tenía lógica custom que no seguía el patrón base
2. **Importancia de tests de integración**: El bug solo aparecía en escenarios específicos (Vertex + Claude format + tools)
3. **Context flags efectivos**: El mecanismo `needClaudeResponseConversion` funcionó correctamente

### 7.2 Debugging

1. **Análisis comparativo**: Comparar implementaciones de proveedores reveló el bypass
2. **Tests aislados**: Crear tests específicos para tool name preservation aceleró la identificación
3. **Logs estructurados**: Faltaban logs en la conversión (área de mejora)

### 7.3 Mejoras Futuras

1. **Refactoring potencial**: Considerar que Vertex también use `handleRequestBody`
2. **Tests automatizados**: Agregar tests E2E para cada proveedor × formato
3. **Documentación**: Documentar el contrato esperado en `OnRequestBody`

---

## 8. Referencias

### Archivos Modificados
- [provider/vertex.go](provider/vertex.go#L252-L262)

### Archivos Relacionados
- [main.go](main.go#L214-L222) - Detección y reescritura de path
- [provider/provider.go](provider/provider.go#L935-L948) - Método base handleRequestBody
- [provider/claude_to_openai.go](provider/claude_to_openai.go#L56-L214) - Lógica de conversión

### Tests
- [provider/vertex_test.go](provider/vertex_test.go) - Tests de integración (build-ignored)
- [provider/claude_to_openai_test.go](provider/claude_to_openai_test.go) - Tests unitarios de conversión

### Especificaciones
- [Claude Messages API](https://docs.anthropic.com/en/api/messages)
- [OpenAI Chat Completions API](https://platform.openai.com/docs/api-reference/chat)
- [Vertex AI Anthropic Claude](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/claude)

---

## 9. Conclusión

La corrección implementada resuelve completamente el problema de compatibilidad de Vertex AI con el endpoint `/v1/messages` cuando se usan herramientas. La solución es:

- ✅ **Mínimamente invasiva**: Solo agrega lógica de conversión donde faltaba
- ✅ **Sin breaking changes**: Preserva todos los modos existentes
- ✅ **Consistente**: Alinea Vertex con el comportamiento de otros proveedores
- ✅ **Probada**: Validada con tests unitarios e integración

**Estado:** ✅ Implementado y Validado
**Build:** `us-central1-docker.pkg.dev/atm-packages-p-3938/higress-plugins/ai-proxy`
Loading