Configuración, personalización, ajustes de plataforma y referencias de API para Pi.

Personalizado Providers

Extensions puede registrar proveedores de modelos personalizados a través de pi.registerProvider(). Esto permite:

  • Proxies: enrute solicitudes a través de proxies corporativos o puertas de enlace API
  • Puntos finales personalizados: utilice implementaciones de modelos privados o autohospedados
  • OAuth/SSO: agregar flujos de autenticación para proveedores empresariales
  • Personalizado APIs: implementar la transmisión para LLM APIs no estándar

Ejemplo Extensions

Vea estos ejemplos completos de proveedores:

Tabla de contenido

Referencia rápida

Extensions puede registrar un pi-ai completo Provider o utilizar el formulario de configuración de proveedor heredado. Prefiera un proveedor completo cuando se requiera autenticación personalizada, filtrado, actualización o comportamiento de transmisión. Pi compone models.json anula los proveedores nativos registrados.

import { createProvider, openAICompletionsApi } from "@earendil-works/pi-ai";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.registerProvider(createProvider({
    id: "native-local",
    name: "Native Local",
    baseUrl: "http://localhost:8080/v1",
    auth: {
      apiKey: {
        name: "Local server API key",
        async login(interaction) {
          return {
            type: "api_key",
            key: await interaction.prompt({ type: "secret", message: "API key" })
          };
        },
        async resolve({ credential }) {
          return credential?.key
            ? { auth: { apiKey: credential.key }, source: "stored API key" }
            : undefined;
        }
      }
    },
    models: [],
    api: openAICompletionsApi()
  }));

  // Legacy provider-config form:
  // Override baseUrl for existing provider
  pi.registerProvider("anthropic", {
    baseUrl: "https://proxy.example.com"
  });

  // Register new provider with models
  pi.registerProvider("my-provider", {
    name: "My Provider",
    baseUrl: "https://api.example.com",
    apiKey: "$MY_API_KEY",
    api: "openai-completions",
    models: [
      {
        id: "my-model",
        name: "My Model",
        reasoning: false,
        input: ["text", "image"],
        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
        contextWindow: 128000,
        maxTokens: 4096
      }
    ]
  });
}

La fábrica de extensiones también puede ser async. Para el descubrimiento dinámico de modelos, busque y registre modelos en la fábrica en lugar de session_start. pi espera a la fábrica antes de que continúe el inicio, por lo que el proveedor está disponible durante el inicio interactivo y para pi --list-models.

Anular proveedor existente

El caso de uso más simple: redirigir a un proveedor existente a través de un proxy.

// All Anthropic requests now go through your proxy
pi.registerProvider("anthropic", {
  baseUrl: "https://proxy.example.com"
});

// Add custom headers to OpenAI requests
pi.registerProvider("openai", {
  headers: {
    "X-Custom-Header": "value"
  }
});

// Both baseUrl and headers
pi.registerProvider("google", {
  baseUrl: "https://ai-gateway.corp.com/google",
  headers: {
    "X-Corp-Auth": "$CORP_AUTH_TOKEN"  // env var or literal
  }
});

Cuando solo se proporcionan baseUrl y/o headers (no models), todos los modelos existentes para ese proveedor se conservan con el nuevo punto final.

Registrar nuevo proveedor

Para agregar un proveedor completamente nuevo, especifique models junto con la configuración requerida.

Si la lista de modelos proviene de un punto final remoto, use una fábrica de extensiones asíncrona:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default async function (pi: ExtensionAPI) {
  const response = await fetch("http://localhost:1234/v1/models");
  const payload = (await response.json()) as {
    data: Array<{
      id: string;
      name?: string;
      context_window?: number;
      max_tokens?: number;
    }>;
  };

  pi.registerProvider("local-openai", {
    baseUrl: "http://localhost:1234/v1",
    apiKey: "$LOCAL_OPENAI_API_KEY",
    api: "openai-completions",
    models: payload.data.map((model) => ({
      id: model.id,
      name: model.name ?? model.id,
      reasoning: false,
      input: ["text"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: model.context_window ?? 128000,
      maxTokens: model.max_tokens ?? 4096,
    })),
  });
}

Esto registra los modelos recuperados antes de que finalice el inicio.

pi.registerProvider("my-llm", {
  baseUrl: "https://api.my-llm.com/v1",
  apiKey: "$MY_LLM_API_KEY",  // env var reference
  api: "openai-completions",  // which streaming API to use
  models: [
    {
      id: "my-llm-large",
      name: "My LLM Large",
      reasoning: true,        // supports extended thinking
      input: ["text", "image"],
      cost: {
        input: 3.0,           // $/million tokens
        output: 15.0,
        cacheRead: 0.3,
        cacheWrite: 3.75
      },
      contextWindow: 200000,
      maxTokens: 16384
    }
  ]
});

Cuando se proporciona models, reemplaza todos los modelos existentes para ese proveedor.

apiKey y los valores de encabezado personalizados usan la misma sintaxis de valor de configuración que models.json: !command al principio ejecuta un comando para el valor completo, $ENV_VAR y ${ENV_VAR} interpolan variables de entorno, $ emite un literal ``apiKeyy los valores de encabezado personalizados usan la misma sintaxis de valor de configuración quemodels.json: !commandal principio ejecuta un comando para el valor completo,$ENV_VARy${ENV_VAR}interpolan variables de entorno,$emite un literal y$!emite un literal!`.

Darse de baja del proveedor

Utilice pi.unregisterProvider(name) para eliminar un proveedor que se registró previamente mediante pi.registerProvider(name,...):

// Register
pi.registerProvider("my-llm", {
  baseUrl: "https://api.my-llm.com/v1",
  apiKey: "$MY_LLM_API_KEY",
  api: "openai-completions",
  models: [
    {
      id: "my-llm-large",
      name: "My LLM Large",
      reasoning: true,
      input: ["text", "image"],
      cost: { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },
      contextWindow: 200000,
      maxTokens: 16384
    }
  ]
});

// Later, remove it
pi.unregisterProvider("my-llm");

Al cancelar el registro se eliminan los modelos dinámicos de ese proveedor, el respaldo API key, el registro de proveedor OAuth y los registros de controladores de flujo personalizados. Se restauran todos los modelos integrados o comportamiento del proveedor que se anularon.

Las llamadas realizadas después de la fase de carga de extensión inicial se aplican inmediatamente, por lo que no se requiere /reload.

API Tipos

El campo api determina qué implementación de transmisión se utiliza:

API Usar para
anthropic-messages Claude antrópico API y compatibles
openai-completions Finalizaciones de OpenAI Chat API y compatibles
openai-responses Respuestas de OpenAI API
azure-openai-responses Respuestas de Azure OpenAI API
openai-codex-responses Respuestas del Códice OpenAI API
mistral-conversations Transmisión de terminaciones de chat nativo de Mistral
google-generative-ai IA generativa de Google API
google-vertex Google Vertex AI API
bedrock-converse-stream converse amazonas API

La mayoría de los proveedores compatibles con OpenAI funcionan con openai-completions. Utilice el nivel de modelo thinkingLevelMap para niveles de pensamiento específicos del modelo y compat para las peculiaridades del proveedor. Los niveles xhigh y max son opcionales, requieren entradas de mapa no nulas y pueden estar separados por agujeros no admitidos:

models: [{
  id: "custom-model",
  // ...
  reasoning: true,
  thinkingLevelMap: {              // map pi levels to provider values; null hides unsupported levels
    minimal: null,
    low: null,
    medium: null,
    high: "default",
    xhigh: null,
    max: "max"
  },
  compat: {
    supportsDeveloperRole: false,   // use "system" instead of "developer"
    supportsReasoningEffort: true,
    maxTokensField: "max_tokens",   // instead of "max_completion_tokens"
    requiresToolResultName: true,   // tool results need name field
    thinkingFormat: "qwen",        // top-level enable_thinking: true
    cacheControlFormat: "anthropic" // Anthropic-style cache_control markers
  }
}]

Utilice openrouter para controles reasoning: { effort } estilo OpenRouter. Utilice together para controles reasoning: { enabled } estilo Together; con supportsReasoningEffort, también envía reasoning_effort. Utilice qwen-chat-template para servidores locales compatibles con Qwen que lean chat_template_kwargs.enable_thinking y necesiten preserve_thinking. Utilice cacheControlFormat: "anthropic" para proveedores compatibles con OpenAI que exponen el almacenamiento en caché de mensajes de estilo Anthropic a través de cache_control en el mensaje del sistema, la última definición de herramienta y el contenido de texto del último usuario, asistente o resultado de la herramienta.

Para proveedores compatibles con Anthropic que utilizan api: "anthropic-messages", establezca compat.forceAdaptiveThinking: true en modelos o proveedores cuyo modelo ascendente requiere pensamiento adaptativo (thinking.type: "adaptive" más output_config.effort). Los modelos Claude adaptables incorporados configuran esto automáticamente. Configure compat.allowEmptySignature: true solo para proveedores que emiten firmas de pensamiento vacías y esperan signature: "" en la reproducción.

Nota de migración: Mistral pasó de openai-completions a mistral-conversations. Utilice mistral-conversations para modelos Mistral nativos. Si enruta intencionalmente puntos finales personalizados/compatibles con Mistral a través de openai-completions, configure los indicadores compat explícitamente según sea necesario.

Encabezado de autenticación

Si su proveedor espera Authorization: Bearer <key> pero no utiliza un estándar API, establezca authHeader: true:

pi.registerProvider("custom-api", {
  baseUrl: "https://api.example.com",
  apiKey: "$MY_API_KEY",
  authHeader: true,  // adds Authorization: Bearer header
  api: "openai-completions",
  models: [...]
});

La clave se resuelve para cada solicitud. Un encabezado de solicitud explícita Authorization tiene prioridad sobre el valor generado.

OAuth Soporte

Agregue autenticación OAuth/SSO que se integra con /login:

import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";

pi.registerProvider("corporate-ai", {
  baseUrl: "https://ai.corp.com/v1",
  api: "openai-responses",
  models: [...],
  oauth: {
    name: "Corporate AI (SSO)",

    async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
      const method = await callbacks.onSelect({
        message: "Select login method:",
        options: [
          { id: "browser", label: "Browser OAuth" },
          { id: "device", label: "Device code" }
        ]
      });
      if (!method) throw new Error("Login cancelled");

      let code: string;
      if (method === "device") {
        callbacks.onDeviceCode({
          userCode: "ABCD-1234",
          verificationUri: "https://sso.corp.com/device",
          intervalSeconds: 5,
          expiresInSeconds: 900
        });
        code = await pollDeviceCodeUntilComplete();
      } else {
        callbacks.onAuth({ url: "https://sso.corp.com/authorize?..." });
        code = await callbacks.onPrompt({ message: "Enter SSO code:" });
      }

      // Exchange for tokens (your implementation)
      const tokens = await exchangeCodeForTokens(code);

      return {
        refresh: tokens.refreshToken,
        access: tokens.accessToken,
        expires: Date.now() + tokens.expiresIn * 1000
      };
    },

    async refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials> {
      const tokens = await refreshAccessToken(credentials.refresh, signal);
      return {
        refresh: tokens.refreshToken ?? credentials.refresh,
        access: tokens.accessToken,
        expires: Date.now() + tokens.expiresIn * 1000
      };
    },

    getApiKey(credentials: OAuthCredentials): string {
      return credentials.access;
    }
  }
});

Después del registro, los usuarios pueden autenticarse a través de /login corporate-ai.

OAuthIniciar sesiónDevoluciones de llamada

El objeto callbacks proporciona interacciones neutrales en la interfaz de usuario para el flujo propiedad del proveedor:

interface OAuthLoginCallbacks {
  // Open URL in browser (for OAuth redirects)
  onAuth(params: { url: string }): void;

  // Show device code (for device authorization flow)
  onDeviceCode(params: {
    userCode: string;
    verificationUri: string;
    intervalSeconds?: number;
    expiresInSeconds?: number;
  }): void;

  // Show transient progress
  onProgress?(message: string): void;

  // Prompt user for input (for manual token entry)
  onPrompt(params: { message: string }): Promise<string>;

  // Show an interactive selector, e.g. to choose browser OAuth vs device code
  onSelect(params: {
    message: string;
    options: { id: string; label: string }[];
  }): Promise<string | undefined>;
}

OAuthCredenciales

Las credenciales persisten en ~/.pi/agent/auth.json:

interface OAuthCredentials {
  refresh: string;   // Refresh token (for refreshToken())
  access: string;    // Access token (returned by getApiKey())
  expires: number;   // Expiration timestamp in milliseconds
}

Transmisión personalizada API

Para proveedores con API no estándar, implemente streamSimple. Estudie las implementaciones de proveedores existentes antes de escribir la suya propia:

Implementaciones de referencia:

Patrón de corriente

Todos los proveedores siguen el mismo patrón:

import {
  type AssistantMessage,
  type AssistantMessageEventStream,
  type Context,
  type Model,
  type SimpleStreamOptions,
  calculateCost,
  createAssistantMessageEventStream,
} from "@earendil-works/pi-ai";

function streamMyProvider(
  model: Model<any>,
  context: Context,
  options?: SimpleStreamOptions
): AssistantMessageEventStream {
  const stream = createAssistantMessageEventStream();

  (async () => {
    // Initialize output message
    const output: AssistantMessage = {
      role: "assistant",
      content: [],
      api: model.api,
      provider: model.provider,
      model: model.id,
      usage: {
        input: 0,
        output: 0,
        cacheRead: 0,
        cacheWrite: 0,
        totalTokens: 0,
        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
      },
      stopReason: "pending",
      timestamp: Date.now(),
    };

    try {
      // Push start event
      stream.push({ type: "start", partial: output });

      // Make API request and process response...
      // Push content events as they arrive and set stopReason from the terminal event.
      if (output.stopReason === "pending") {
        throw new Error("Provider stream ended without a stop reason");
      }
      if (output.stopReason === "error" || output.stopReason === "aborted") {
        throw new Error(output.errorMessage || "An unknown error occurred");
      }

      // Push done event
      stream.push({
        type: "done",
        reason: output.stopReason,
        message: output
      });
      stream.end();
    } catch (error) {
      output.stopReason = options?.signal?.aborted ? "aborted" : "error";
      output.errorMessage = error instanceof Error ? error.message : String(error);
      stream.push({ type: "error", reason: output.stopReason, error: output });
      stream.end();
    }
  })();

  return stream;
}

Tipos de eventos

Envíe eventos a través de stream.push() en este orden:

  1. { type: "start", partial: output } - Transmisión iniciada

  2. Eventos de contenido (repetibles, seguimiento contentIndex para cada bloque):

    • { type: "text_start", contentIndex, partial } - Bloque de texto iniciado
    • { type: "text_delta", contentIndex, delta, partial } - Fragmento de texto
    • { type: "text_end", contentIndex, content, partial } - Bloque de texto finalizado
    • { type: "thinking_start", contentIndex, partial } - El pensamiento comenzó
    • { type: "thinking_delta", contentIndex, delta, partial } - Fragmento de pensamiento
    • { type: "thinking_end", contentIndex, content, partial } - Se acabó el pensamiento
    • { type: "toolcall_start", contentIndex, partial } - Se inició la llamada a la herramienta
    • { type: "toolcall_delta", contentIndex, delta, partial } - Llamada a herramienta JSON fragmento
    • { type: "toolcall_end", contentIndex, toolCall, partial } - Llamada de herramienta finalizada
  3. { type: "done", reason, message } o { type: "error", reason, error } - Transmisión finalizada

El campo partial en cada evento contiene el estado AssistantMessage actual. Actualice output.content a medida que reciba datos, luego incluya output como partial.

Bloques de contenido

Agregue bloques de contenido a output.content a medida que lleguen:

// Text block
output.content.push({ type: "text", text: "" });
stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });

// As text arrives
const block = output.content[contentIndex];
if (block.type === "text") {
  block.text += delta;
  stream.push({ type: "text_delta", contentIndex, delta, partial: output });
}

// When block completes
stream.push({ type: "text_end", contentIndex, content: block.text, partial: output });

Llamadas de herramientas

Las llamadas a herramientas requieren acumular JSON y analizar:

// Start tool call
output.content.push({
  type: "toolCall",
  id: toolCallId,
  name: toolName,
  arguments: {}
});
stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });

// Accumulate JSON
let partialJson = "";
partialJson += jsonDelta;
try {
  block.arguments = JSON.parse(partialJson);
} catch {}
stream.push({ type: "toolcall_delta", contentIndex, delta: jsonDelta, partial: output });

// Complete
stream.push({
  type: "toolcall_end",
  contentIndex,
  toolCall: { type: "toolCall", id, name, arguments: block.arguments },
  partial: output
});

Uso y costo

Actualice el uso desde la respuesta API y calcule el costo:

output.usage.input = response.usage.input_tokens;
output.usage.output = response.usage.output_tokens;
output.usage.cacheRead = response.usage.cache_read_tokens ?? 0;
output.usage.cacheWrite = response.usage.cache_write_tokens ?? 0;
output.usage.totalTokens = output.usage.input + output.usage.output +
                           output.usage.cacheRead + output.usage.cacheWrite;
calculateCost(model, output.usage);

Errores de desbordamiento de contexto

Cuando una solicitud excede la ventana de contexto del modelo, pi puede recuperarse automáticamente compactando la conversación y volviendo a intentarlo. Esta recuperación solo se activa si pi reconoce la falla como un desbordamiento.

La detección se ejecuta en el mensaje del asistente finalizado:

Si su proveedor devuelve errores de desbordamiento con un mensaje que pi no reconoce, normalice el error desde la misma extensión que registra el proveedor. Utilice un controlador message_end para reescribir el mensaje del asistente de modo que errorMessage comience con una frase que pi reconozca. La alternativa genérica context_length_exceeded es la opción más segura.

const MY_PROVIDER_OVERFLOW_PATTERN = /your provider's overflow phrase/i;

export default function (pi: ExtensionAPI) {
  pi.registerProvider("my-provider", { /* ... */ });

  pi.on("message_end", (event, ctx) => {
    const message = event.message;
    if (message.role !== "assistant") return;
    if (message.stopReason !== "error") return;
    if (
      message.provider !== "my-provider" &&
      ctx.model?.provider !== "my-provider"
    )
      return;

    const errorMessage = message.errorMessage ?? "";
    if (errorMessage.includes("context_length_exceeded")) return;
    if (!MY_PROVIDER_OVERFLOW_PATTERN.test(errorMessage)) return;

    return {
      message: {
        ...message,
        errorMessage: `context_length_exceeded: ${errorMessage}`,
      },
    };
  });
}

message_end se ejecuta antes de que pi rastree el mensaje del asistente para la autocompactación, por lo que el errorMessage reescrito es lo que pi verifica. Con esto en su lugar, pi:

  1. Detecta el desbordamiento de errorMessage.
  2. Suelta el mensaje del asistente fallido desde el contexto en vivo.
  3. Ejecute la compactación.
  4. Vuelva a intentar la solicitud una vez.

Guarde la reescritura con cuidado:

  • Ámbite a tu proveedor (message.provider y ctx.model?.provider) para que los errores no relacionados de otros proveedores no se modifiquen.
  • Haga coincidir un patrón específico del proveedor, no los patrones de desbordamiento genéricos de pi. Reescribir los errores de límite de velocidad o limitación (rate limit, too many requests) activaría falsamente la compactación en lugar de la ruta normal de reintento con retroceso de pi.
  • Omita cuando errorMessage ya incluya context_length_exceeded para que el controlador sea idempotente.

Registro

Registre su función de transmisión:

pi.registerProvider("my-provider", {
  baseUrl: "https://api.example.com",
  apiKey: "$MY_API_KEY",
  api: "my-custom-api",
  models: [...],
  streamSimple: streamMyProvider
});

Probando su implementación

Pruebe su proveedor con los mismos conjuntos de pruebas utilizados por los proveedores integrados. Copie y adapte estos archivos de prueba desde packages/ai/test/:

Prueba Objetivo
stream.test.ts Transmisión básica, salida de texto
tokens.test.ts Recuento y uso de tokens
abort.test.ts Abortar Manejo de señales
empty.test.ts Respuestas vacías/mínimas
context-overflow.test.ts Límites de la ventana de contexto
image-limits.test.ts Manejo de entrada de imágenes
unicode-surrogate.test.ts Casos extremos Unicode
tool-call-without-result.test.ts Casos extremos de llamada de herramientas
image-tool-result.test.ts Imágenes en resultados de herramientas
total-tokens.test.ts Cálculo total de tokens
cross-provider-handoff.test.ts Transferencia de contexto entre proveedores

Ejecute pruebas con sus pares de proveedor/modelo para verificar la compatibilidad.

Referencia de configuración

interface ProviderConfig {
  /** Display name for the provider in UI such as /login. */
  name?: string;

  /** API endpoint URL. Required when defining models. */
  baseUrl?: string;

  /** API key literal, env interpolation ($ENV_VAR or ${ENV_VAR}), or !command. Required when defining models (unless oauth). */
  apiKey?: string;

  /** API type for streaming. Required at provider or model level when defining models. */
  api?: Api;

  /** Custom streaming implementation for non-standard APIs. */
  streamSimple?: (
    model: Model<Api>,
    context: Context,
    options?: SimpleStreamOptions
  ) => AssistantMessageEventStream;

  /** Custom headers to include in requests. Values use the same resolution syntax as apiKey. */
  headers?: Record<string, string>;

  /** If true, adds Authorization: Bearer header with the resolved API key. */
  authHeader?: boolean;

  /** Models to register. If provided, replaces all existing models for this provider. */
  models?: ProviderModelConfig[];

  /** OAuth provider for /login support. */
  oauth?: {
    name: string;
    login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>;
    refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials>;
    getApiKey(credentials: OAuthCredentials): string;
  };
}

Referencia de definición de modelo

interface ProviderModelConfig {
  /** Model ID (e.g., "claude-sonnet-4-20250514"). */
  id: string;

  /** Display name (e.g., "Claude 4 Sonnet"). */
  name: string;

  /** API type override for this specific model. */
  api?: Api;

  /** API endpoint URL override for this specific model. */
  baseUrl?: string;

  /** Whether the model supports extended thinking. */
  reasoning: boolean;

  /** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */
  thinkingLevelMap?: Partial<Record<"off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", string | null>>;

  /** Supported input types. */
  input: ("text" | "image")[];

  /** Cost per million tokens (for usage tracking). */
  cost: {
    input: number;
    output: number;
    cacheRead: number;
    cacheWrite: number;
  };

  /** Maximum context window size in tokens. */
  contextWindow: number;

  /** Maximum output tokens. */
  maxTokens: number;

  /** Custom headers for this specific model. */
  headers?: Record<string, string>;

  /** Compatibility settings for the selected API. */
  compat?: {
    // openai-completions
    supportsStore?: boolean;
    supportsDeveloperRole?: boolean;
    supportsReasoningEffort?: boolean;
    supportsUsageInStreaming?: boolean;
    supportsFinishReason?: boolean;
    supportsStrictMode?: boolean;
    supportsOpenAIGrammarTools?: boolean; // openai-completions/openai-responses; false falls back to normal function tools
    maxTokensField?: "max_completion_tokens" | "max_tokens";
    requiresToolResultName?: boolean;
    requiresAssistantAfterToolResult?: boolean;
    requiresThinkingAsText?: boolean;
    requiresReasoningContentOnAssistantMessages?: boolean;
    thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "baseten" | "zai" | "qwen" | "chat-template" | "qwen-chat-template" | "string-thinking" | "ant-ling";
    chatTemplateKwargs?: Record<string, string | number | boolean | null | { "$var": "thinking.enabled" | "thinking.effort"; omitWhenOff?: boolean }>;
    chatTemplateArgs?: Record<string, string | number | boolean | null | { "$var": "thinking.enabled" | "thinking.effort"; omitWhenOff?: boolean }>;
    cacheControlFormat?: "anthropic";
    sessionAffinityFormat?: "openai" | "openai-nosession" | "openrouter";
    sendSessionAffinityHeaders?: boolean;

    // anthropic-messages
    supportsEagerToolInputStreaming?: boolean;
    supportsLongCacheRetention?: boolean;
    sendSessionAffinityHeaders?: boolean;
    supportsCacheControlOnTools?: boolean;
    forceAdaptiveThinking?: boolean;
    allowEmptySignature?: boolean;
    supportsStrictTools?: boolean;
  };
}

openrouter envía reasoning: { effort }. deepseek envía thinking: { type: "enabled" | "disabled" } y reasoning_effort cuando está habilitado. together envía reasoning: { enabled } y también reasoning_effort cuando supportsReasoningEffort está habilitado. qwen es para el nivel superior estilo DashScope enable_thinking. Utilice qwen-chat-template para servidores locales compatibles con Qwen que lean chat_template_kwargs.enable_thinking y necesiten preserve_thinking. Utilice chat-template para chat_template_kwargs configurable, por ejemplo DeepSeek V3.x detrás de vLLM con chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }. Utilice thinkingFormat: "baseten" con chatTemplateArgs cuando el proveedor espere valores de alternancia inferiores a chat_template_args y, opcionalmente, admita el nivel superior reasoning_effort. cacheControlFormat: "anthropic" aplica marcadores cache_control de estilo antrópico al mensaje del sistema, a la última definición de herramienta y al contenido de texto del último usuario, asistente o resultado de herramienta.