Saltar al contenido principal

Documentation Index

Fetch the complete documentation index at: https://docs.mka1.com/llms.txt

Use this file to discover all available pages before exploring further.

En MKA1, el límite práctico de inquilino es:
  • una clave API de cuenta distinta
  • un ID de usuario final delegado opcional vía X-On-Behalf-Of
  • la propiedad y uso de recursos aguas abajo registrados contra ese contexto autenticado
Utiliza la siguiente guía paso a paso cuando quieras verificar que dos inquilinos están realmente aislados entre sí. Te proporciona un flujo exacto de Inquilino A / Inquilino B que puedes ejecutar de principio a fin.

Verificar el aislamiento de inquilinos en la práctica

Trata cada inquilino como una cuenta separada con su propia clave API. El objetivo es mostrar cuatro cosas:
  1. El Inquilino A y el Inquilino B usan claves API diferentes.
  2. El Inquilino A y el Inquilino B pueden tener configuraciones de cuota diferentes.
  3. El Inquilino A y el Inquilino B pueden tener políticas diferentes.
  4. Un recurso creado bajo el Inquilino A no es accesible bajo el Inquilino B.

Paso 1: definir dos inquilinos

Comienza con dos claves API separadas.
export TENANT_A_KEY="<tenant-a-api-key>"
export TENANT_B_KEY="<tenant-b-api-key>"
export TENANT_A_USER="tenant_a_user"
export TENANT_B_USER="tenant_b_user"
Si ya tienes dos claves de producción o staging, puedes usarlas directamente. Si necesitas aprovisionar dos claves con límites diferentes para la demostración, créalas por separado.
curl https://apigw.mka1.com/api/v1/authentication/api-key/create/rate-limited \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Cookie: better-auth.session_token=<tenant-a-session>' \
  --data '{
    "name": "Tenant A demo key",
    "rateLimitEnabled": true,
    "rateLimitTimeWindow": 3600000,
    "rateLimitMax": 60
  }'
curl https://apigw.mka1.com/api/v1/authentication/api-key/create/rate-limited \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Cookie: better-auth.session_token=<tenant-b-session>' \
  --data '{
    "name": "Tenant B demo key",
    "rateLimitEnabled": true,
    "rateLimitTimeWindow": 3600000,
    "rateLimitMax": 600
  }'
Esto establece inmediatamente las dos primeras piezas de aislamiento:
  • El Inquilino A y el Inquilino B no comparten una clave.
  • El Inquilino A y el Inquilino B no comparten la misma configuración de cuota.

Paso 2: configurar políticas diferentes para cada inquilino

Ahora asigna a los dos inquilinos políticas de guardrails diferentes. Configura el Inquilino A para bloquear la palabra confidential.
curl https://apigw.mka1.com/api/v1/llm/guardrails \
  --request PUT \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer ${TENANT_A_KEY}" \
  --data '{
    "guardrails": [
      {
        "mode": "ban_words",
        "enabled": true,
        "config": {
          "words": ["confidential"]
        }
      }
    ]
  }'
Configura el Inquilino B con una política diferente.
curl https://apigw.mka1.com/api/v1/llm/guardrails \
  --request PUT \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer ${TENANT_B_KEY}" \
  --data '{
    "guardrails": [
      {
        "mode": "ban_words",
        "enabled": true,
        "config": {
          "words": ["internal-only"]
        }
      }
    ]
  }'
Luego prueba el mismo contenido contra ambos inquilinos. El Inquilino A debería bloquear esto:
curl https://apigw.mka1.com/api/v1/llm/guardrails/test \
  --request POST \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer ${TENANT_A_KEY}" \
  --data '{
    "content": "Summarize this confidential roadmap."
  }'
El Inquilino B debería evaluar el mismo contenido contra su propia política en vez de la del Inquilino A:
curl https://apigw.mka1.com/api/v1/llm/guardrails/test \
  --request POST \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer ${TENANT_B_KEY}" \
  --data '{
    "content": "Summarize this confidential roadmap."
  }'
El resultado esperado es:
  • El Inquilino A falla porque confidential está bloqueado en la política del Inquilino A.
  • El Inquilino B no hereda la política del Inquilino A.
Esta es la demostración práctica más clara de políticas independientes por inquilino.

Paso 3: demostrar cuotas separadas

Después de crear dos claves con diferentes límites, envía el mismo tipo de solicitud a través de ambas claves. El Inquilino A usa la clave de bajo límite:
for i in $(seq 1 70); do
  curl https://apigw.mka1.com/api/v1/llm/responses \
    --request POST \
    --silent \
    --output /dev/null \
    --write-out "tenant-a request $i -> %{http_code}\n" \
    --header 'Content-Type: application/json' \
    --header "Authorization: Bearer ${TENANT_A_KEY}" \
    --header "X-On-Behalf-Of: ${TENANT_A_USER}" \
    --data '{
      "model": "meetkai:functionary",
      "input": "Reply with the word ok."
    }'
done
El Inquilino B usa la clave de límite más alto:
for i in $(seq 1 70); do
  curl https://apigw.mka1.com/api/v1/llm/responses \
    --request POST \
    --silent \
    --output /dev/null \
    --write-out "tenant-b request $i -> %{http_code}\n" \
    --header 'Content-Type: application/json' \
    --header "Authorization: Bearer ${TENANT_B_KEY}" \
    --header "X-On-Behalf-Of: ${TENANT_B_USER}" \
    --data '{
      "model": "meetkai:functionary-pt",
      "input": "Reply with the word ok."
    }'
done
El resultado esperado es:
  • El Inquilino A comienza a recibir 429 antes.
  • El Inquilino B sigue teniendo éxito porque tiene una clave y una cuota diferente.
Esto muestra que los inquilinos no comparten un limitador global.

Paso 4: demostrar aislamiento de recursos

Crea un recurso bajo el Inquilino A. Una conversación es un ejemplo sencillo porque es visible a través de la API pública.
curl https://apigw.mka1.com/api/v1/llm/conversations \
  --request POST \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer ${TENANT_A_KEY}" \
  --header "X-On-Behalf-Of: ${TENANT_A_USER}" \
  --data '{
    "metadata": {
      "tenant": "A",
      "demo": "segregated-tenants"
    }
  }'
Supón que la respuesta devuelve conv_tenant_a_123. El Inquilino A puede leerlo:
curl https://apigw.mka1.com/api/v1/llm/conversations/conv_tenant_a_123 \
  --header "Authorization: Bearer ${TENANT_A_KEY}" \
  --header "X-On-Behalf-Of: ${TENANT_A_USER}"
El Inquilino B no debería poder leer el mismo recurso:
curl https://apigw.mka1.com/api/v1/llm/conversations/conv_tenant_a_123 \
  --header "Authorization: Bearer ${TENANT_B_KEY}" \
  --header "X-On-Behalf-Of: ${TENANT_B_USER}"
También puedes mostrar el aislamiento en el listado:
curl 'https://apigw.mka1.com/api/v1/llm/conversations?limit=20' \
  --header "Authorization: Bearer ${TENANT_B_KEY}" \
  --header "X-On-Behalf-Of: ${TENANT_B_USER}"
El Inquilino B no debería ver la conversación del Inquilino A en su lista. Esta es la prueba práctica de enclave de inquilino:
  • se usa la misma superficie de API
  • se usa la misma plataforma
  • pero la identidad, política, cuota y propiedad de recursos se aplican por separado

Paso opcional 5: mostrar libros de uso separados

Si quieres una prueba visible adicional, consulta el uso por separado para cada inquilino.
curl 'https://apigw.mka1.com/api/v1/llm/usage/responses?start_time=1704067200&end_time=1704153600&bucket_width=1d&group_by=api_key_id' \
  --header "Authorization: Bearer ${TENANT_A_KEY}"
curl 'https://apigw.mka1.com/api/v1/llm/usage/responses?start_time=1704067200&end_time=1704153600&bucket_width=1d&group_by=api_key_id' \
  --header "Authorization: Bearer ${TENANT_B_KEY}"
El objetivo de este paso no es solo la facturación. Demuestra que la plataforma registra la actividad contra el contexto autenticado del inquilino en vez de fusionar todo el tráfico en una sola cuenta indiferenciada.

Qué demuestra esta guía paso a paso

Si ejecutas la guía anterior, puedes hacer estas afirmaciones exactas:
  • Claves independientes: El Inquilino A y el Inquilino B se autentican con diferentes claves API.
  • Cuotas independientes: El Inquilino A y el Inquilino B pueden tener configuraciones de límite de tasa diferentes y recibir comportamientos 429 distintos.
  • Políticas independientes: El Inquilino A y el Inquilino B pueden establecer diferentes guardrails y obtener resultados distintos sobre el mismo contenido.
  • Aislamiento real: Un recurso creado bajo el Inquilino A no es legible bajo el Inquilino B.
La superficie de la API es compartida, pero el contexto autenticado del inquilino, la aplicación de cuotas, la configuración de políticas y la propiedad de recursos no se comparten.

Cómo funciona el modelo de identidad internamente

La autenticación en la API de MKA1 tiene tres capas:
  • Tu clave API identifica tu cuenta.
  • X-On-Behalf-Of identifica al usuario final para el que actúa tu servidor.
  • Un JWT intercambiado otorga a un servicio aguas abajo una credencial de corta duración derivada de esa clave API y del contexto de usuario final.
La versión corta es simple:
  • Envía Authorization: Bearer <mka1-api-key> en cada solicitud del lado del servidor.
  • Añade X-On-Behalf-Of cuando la solicitud pertenezca a uno de tus usuarios finales.
  • Usa POST /api/v1/authentication/api-key/exchange-token cuando otro servicio deba recibir un token de corta duración en vez de tu clave API real.
Si solo necesitas el patrón de uso, comienza con la guía de autenticación. Esta página explica lo que ocurre después de que la solicitud llega al gateway.

El camino de la solicitud

La API de MKA1 no pide a los servicios aguas abajo que validen los tokens bearer por sí mismos. Las solicitudes pasan primero por el gateway, y el gateway inyecta cabeceras de identidad confiables para el resto de la plataforma. Cuando también envías X-On-Behalf-Of, el gateway mantiene esa identidad de usuario final delegada con la solicitud: El intercambio JWT añade un paso extra:

Los tres patrones principales

Patrón 1: solicitudes solo backend

Usa esto cuando tu backend llama a la API de MKA1 para su propio flujo de trabajo y no hay un usuario final separado que rastrear.
curl https://apigw.mka1.com/api/v1/llm/responses \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <mka1-api-key>' \
  --data '{
    "model": "meetkai:functionary-pt",
    "input": "Write a short release note for our API update."
  }'
const response = await fetch("https://apigw.mka1.com/api/v1/llm/responses", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.MKA1_API_KEY}`,
  },
  body: JSON.stringify({
    model: "meetkai:functionary-pt",
    input: "Write a short release note for our API update.",
  }),
});

Patrón 2: integración de servidor multiusuario

Usa esto cuando tu backend realiza la solicitud para uno de los usuarios de tu propia aplicación.
curl https://apigw.mka1.com/api/v1/llm/responses \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <mka1-api-key>' \
  --header 'X-On-Behalf-Of: user_123' \
  --data '{
    "model": "meetkai:functionary-pt",
    "input": "Summarize this support ticket."
  }'
const response = await fetch("https://apigw.mka1.com/api/v1/llm/responses", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.MKA1_API_KEY}`,
    "X-On-Behalf-Of": "user_123",
  },
  body: JSON.stringify({
    model: "meetkai:functionary-pt",
    input: "Summarize this support ticket.",
  }),
});
Este es el patrón correcto cuando quieres que solicitudes, archivos, respuestas, conversaciones y uso permanezcan asociados a un ID de usuario final estable de tu propio sistema.

Patrón 3: intercambia tu clave API por un JWT de corta duración

Usa esto cuando otro servicio deba recibir un token de tiempo limitado en vez de tu clave API de larga duración.
curl https://apigw.mka1.com/api/v1/authentication/api-key/exchange-token \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <mka1-api-key>' \
  --data '{
    "audience": "https://my-service.example.com",
    "externalUserId": "user_123",
    "expiresIn": 3600,
    "permissions": ["agent:create", "agent:read"]
  }'
const response = await fetch("https://apigw.mka1.com/api/v1/authentication/api-key/exchange-token", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.MKA1_API_KEY}`,
  },
  body: JSON.stringify({
    audience: "https://my-service.example.com",
    externalUserId: "user_123",
    expiresIn: 3600,
    permissions: ["agent:create", "agent:read"],
  }),
});

const { token } = await response.json();

Qué cabeceras envías vs cuáles inyecta la plataforma

Cabeceras que envías

CabeceraObligatoriaSignificado
AuthorizationTu clave API de MKA1 o JWT intercambiado en formato bearer
X-On-Behalf-OfNoTu identificador de usuario final estable para solicitudes delegadas del lado del servidor

Cabeceras confiables inyectadas dentro de la plataforma

CabeceraProducida porSignificado
X-User-IDGatewayEl ID de usuario autenticado de la cuenta
X-Api-Key-IDGatewayEl ID de la clave API resuelta
X-User-RoleGatewayEl contexto de rol autenticado
X-Api-Key-PermissionsGatewayPermisos adjuntos a la clave API
X-Exchange-JWT-External-User-IDGatewayEl ID de usuario final delegado
X-Exchange-JWT-PermissionsGatewayPermisos embebidos en el JWT intercambiado
Los clientes envían Authorization y a veces X-On-Behalf-Of. Los clientes no envían directamente las cabeceras internas X-User-ID o X-Api-Key-ID. Estas son derivadas por el gateway tras la validación.

Cómo funciona el intercambio JWT

POST /api/v1/authentication/api-key/exchange-token convierte una clave API de larga duración en un token de corta duración para otro servicio. El cuerpo de la solicitud tiene cuatro campos significativos:
  • audience: la URL exacta del servicio que debe aceptar el JWT
  • externalUserId: el ID de usuario final colocado en el subject del JWT
  • expiresIn: tiempo de vida del token en segundos, de 300 a 2592000
  • permissions: un subconjunto opcional de los permisos de la clave API
La respuesta es:
{
  "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Un payload decodificado del token se ve así:
{
  "ak": "cm4xqw8ij0001w8lcmj9tn6v7",
  "sub": "user_123",
  "iat": 1734624000,
  "exp": 1734627600,
  "iss": "https://exchange.meetkai.com",
  "aud": "https://my-service.example.com",
  "permissions": ["agent:create", "agent:read"]
}
Estas claims son las más importantes:
  • ak: el ID de la clave API usada para lookup y aplicación de límites de tasa
  • sub: tu identidad de usuario final delegada
  • aud: el servicio que debe aceptar el token
  • permissions: el conjunto de capacidades permitidas para ese token

Cómo funciona el alcance de inquilino en la práctica

La decisión de diseño más importante es que la identidad de usuario final es explícita. Si envías X-On-Behalf-Of: user_123, los servicios aguas abajo pueden mantener recursos y uso asociados a ese usuario. Esto es importante para:
  • conversaciones guardadas
  • respuestas almacenadas
  • archivos y almacenes vectoriales
  • uso y auditoría por usuario
  • comprobaciones de autorización delegadas
Cuando omites X-On-Behalf-Of, las solicitudes se ejecutan como trabajo de cuenta solo backend. Cuando lo incluyes, la solicitud se convierte en una solicitud delegada para un usuario final específico. Usa un identificador estable de tu propio sistema. No uses un nombre visible mutable a menos que ya sea tu ID de usuario canónico.

Límites de tasa y qué deben esperar los llamadores

Las claves API pueden tener configuraciones de límite de tasa personalizadas. La ruta de autenticación aplica los límites de tasa por clave API antes de que la solicitud llegue a los servicios aguas abajo. En la práctica, esto significa:
  • dos claves API diferentes pueden tener límites diferentes
  • una clave API sobrecargada no implica que otra clave esté agotada
  • un 429 es parte de la ruta de autenticación, no un error de modelo aguas abajo
El flujo de intercambio de token también permanece vinculado a la clave API de origen. Eso significa que un JWT aguas abajo aún hereda el comportamiento de límite de tasa a nivel de cuenta asociado a la clave fuente. Si quieres grupos de tráfico separados para diferentes aplicaciones o cargas de trabajo, usa claves API separadas.

Errores comunes

Enviar X-On-Behalf-Of desde código en el navegador

No expongas tu clave API en código de cliente de navegador o móvil. Tu servidor debe llamar a la API de MKA1 y adjuntar X-On-Behalf-Of allí.

Usar un identificador de usuario final inestable

Usa un ID interno duradero como user_123. No alternes entre correos electrónicos, nombres de usuario y nombres visibles para el mismo usuario.

Usar el audience incorrecto en JWTs intercambiados

El servicio aguas abajo debe validar el token para la audiencia prevista. Establece audience en la URL real del servicio que debe aceptar el JWT.

Solicitar permisos más amplios que los de la clave API

El endpoint de intercambio de token solo permite un subconjunto de los permisos de la clave API. Si solicitas un permiso que la clave API no tiene, el intercambio falla.

Tratar cabeceras internas propagadas como cabeceras públicas de cliente

Cabeceras como X-User-ID y X-Api-Key-ID son parte de la ruta de solicitud interna confiable. No son un sustituto de Authorization.

Apéndice de rutas de código

Los siguientes fragmentos muestran la forma principal de implementación detrás del comportamiento público.

Kong valida el token bearer e inyecta cabeceras confiables

authorizationHeader = utils.StripBearer(authorizationHeader)

if utils.IsApiKey(authorizationHeader) {
    resp, err := c.authenticationClient.ValidateApiKey(ctx, authorizationHeader)
    if err != nil {
        c.handleAuthenticationServiceError(kong, err)
        return
    }

    c.writeApiKeyInfoToUnderlyingHeaders(kong, resp.GetApiKeyInfo())
    c.writeUserInfoToUnderlyingHeaders(kong, resp.GetUserInfo())

    onBehalfOfHeader, _ := kong.Request.GetHeader(headerOnBehalfOf)
    if onBehalfOfHeader != "" {
        c.writeExchangeJwtInfoToUnderlyingHeaders(kong, &authentication.ExchangeJWTInfo{
            ExternalUserId: onBehalfOfHeader,
            Permissions:    resp.GetApiKeyInfo().GetPermissions(),
        })
    }

    return
}
Por eso los servicios aguas abajo pueden confiar en X-User-ID, X-Api-Key-ID y X-Exchange-JWT-External-User-ID después de la validación del gateway.

El endpoint de intercambio de token verifica la clave API, comprueba permisos y firma el JWT

const apiKeyResult = await auth.api.verifyApiKey({
  body: {
    key: apiKey,
  },
});

if (!apiKeyResult.valid || !apiKeyResult.key) {
  ctx.set.status = 401;
  return { message: "Invalid API key" };
}

const apiKeyPermissions = Object.entries(verifiedKey.permissions ?? {}).map(
  ([key, value]) => `${key}:${value}`,
);
const permissions = validatedBody.permissions ?? apiKeyPermissions;

if (permissions.some((permission) => !apiKeyPermissions.includes(permission))) {
  ctx.set.status = 401;
  return { message: "Permissions mismatch" };
}
Luego firma un payload que incluye el ID de la clave API, el subject de usuario final delegado y los permisos filtrados:
payload: {
  ak: verifiedKey.id,
  sub: validatedBody.externalUserId,
  iat: DateTime.now().toUTC().toUnixInteger(),
  permissions: permissions,
}

Los servicios aguas abajo leen la identidad de las cabeceras confiables

const apiKey = reqHeaders.get(KONG_API_KEY_HEADER)?.trim();
const userId = reqHeaders.get(KONG_USER_ID_HEADER)?.trim();
const externalUserId = reqHeaders.get(KONG_EXTERNAL_USER_ID_HEADER)?.trim() ?? "";

if (!apiKey?.length || !userId?.length) {
  throw KONG_AUTH_ERROR;
}

return { apiKey, userId, externalUserId, userRole };
Por eso los servicios detrás del gateway normalmente no analizan los tokens bearer por sí mismos. Operan sobre un contexto de identidad validado.

Próximos pasos

Utiliza la guía de autenticación para la versión corta y ejemplos para copiar y pegar. Utiliza la visión general de la API para inspeccionar la referencia generada y el OpenAPI spec en vivo. Luego revisa las páginas de endpoints relevantes en la Referencia de API para los detalles exactos de solicitud y respuesta.