Configuration, personnalisation, paramètres de plateforme et références API pour Pi.

Personnalisé Providers

Extensions peut enregistrer des fournisseurs de modèles personnalisés via pi.registerProvider(). Cela permet:

  • Proxies - Acheminer les demandes via des proxys d'entreprise ou des passerelles API
  • Points de terminaison personnalisés – Utilisez des déploiements de modèles auto-hébergés ou privés
  • OAuth/SSO - Ajouter des flux d'authentification pour les fournisseurs d'entreprise
  • ** APIs personnalisés** – Implémenter le streaming pour les LLM APIs non standard

Exemple Extensions

Consultez ces exemples complets de fournisseurs:

Table des matières

Référence rapide

Extensions peut enregistrer soit un pi-ai Provider complet, soit utiliser l'ancien formulaire de configuration du fournisseur. Préférez un fournisseur complet lorsqu’un comportement personnalisé d’authentification, de filtrage, d’actualisation ou de streaming est requis. Pi compose models.json remplace les fournisseurs natifs enregistrés.

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 fabrique d'extensions peut également être async. Pour la découverte dynamique de modèles, récupérez et enregistrez les modèles dans l'usine au lieu de session_start. pi attend l'usine avant que le démarrage ne continue, le fournisseur est donc disponible pendant le démarrage interactif et jusqu'au pi --list-models.

Remplacer le fournisseur existant

Le cas d'utilisation le plus simple: rediriger un fournisseur existant via 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
  }
});

Lorsque seuls baseUrl et/ou headers sont fournis (pas de models), tous les modèles existants pour ce fournisseur sont conservés avec le nouveau point de terminaison.

Enregistrer un nouveau fournisseur

Pour ajouter un tout nouveau fournisseur, spécifiez models avec la configuration requise.

Si la liste de modèles provient d'un point de terminaison distant, utilisez une fabrique d'extensions asynchrone:

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,
    })),
  });
}

Cela enregistre les modèles récupérés avant la fin du démarrage.

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
    }
  ]
});

Lorsque models est fourni, il remplace tous les modèles existants pour ce fournisseur.

apiKey et les valeurs d'en-tête personnalisées utilisent la même syntaxe de valeur de configuration que models.json: !command au début exécute une commande pour la valeur entière, $ENV_VAR et ${ENV_VAR} interpolent les variables d'environnement, $ émet un littéral ``apiKeyet les valeurs d'en-tête personnalisées utilisent la même syntaxe de valeur de configuration quemodels.json: !commandau début exécute une commande pour la valeur entière,$ENV_VARet${ENV_VAR}interpolent les variables d'environnement,$émet un littéral et$!émet un littéral!`.

Désinscrire le fournisseur

Utilisez pi.unregisterProvider(name) pour supprimer un fournisseur précédemment enregistré via 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");

La désinscription supprime les modèles dynamiques de ce fournisseur, le repli API key, l'enregistrement du fournisseur OAuth et les enregistrements de gestionnaires de flux personnalisés. Tous les modèles intégrés ou comportements de fournisseur qui ont été remplacés sont restaurés.

Les appels passés après la phase initiale de chargement de l'extension sont appliqués immédiatement, donc aucun /reload n'est requis.

API Types

Le champ api détermine quelle implémentation de streaming est utilisée:

API Utiliser pour
anthropic-messages Anthropique Claude API et compatibles
openai-completions Complétions de chat OpenAI API et compatibles
openai-responses Réponses OpenAI API
azure-openai-responses Réponses Azure OpenAI API
openai-codex-responses Réponses du Codex OpenAI API
mistral-conversations Achèvements du chat Native Mistral en streaming
google-generative-ai IA générative Google API
google-vertex Google Vertex AI API
bedrock-converse-stream Amazon Bedrock Converse API

La plupart des fournisseurs compatibles OpenAI fonctionnent avec openai-completions. Utilisez le niveau de modèle thinkingLevelMap pour les niveaux de réflexion spécifiques au modèle et compat pour les bizarreries du fournisseur. Les niveaux xhigh et max sont facultatifs, nécessitent des entrées de carte non nulles et peuvent être séparés par des trous non pris en charge:

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
  }
}]

Utilisez openrouter pour les contrôles reasoning: { effort } de style OpenRouter. Utilisez together pour les contrôles reasoning: { enabled } de style Together; avec supportsReasoningEffort, il envoie également reasoning_effort. Utilisez qwen-chat-template pour les serveurs locaux compatibles Qwen qui lisent chat_template_kwargs.enable_thinking et ont besoin de preserve_thinking. Utilisez cacheControlFormat: "anthropic" pour les fournisseurs compatibles OpenAI qui exposent la mise en cache des invites de style Anthropic via cache_control sur l'invite système, la dernière définition d'outil et le contenu textuel du dernier utilisateur, assistant ou résultat de l'outil.

Pour les fournisseurs compatibles Anthropic utilisant api: "anthropic-messages", définissez compat.forceAdaptiveThinking: true sur les modèles ou les fournisseurs dont le modèle en amont nécessite une pensée adaptative (thinking.type: "adaptive" plus output_config.effort). Les modèles Claude adaptatifs intégrés règlent cela automatiquement. Définissez compat.allowEmptySignature: true uniquement pour les fournisseurs qui émettent des signatures de pensée vides et attendent signature: "" lors de la relecture.

Note de migration: Mistral est passé de openai-completions à mistral-conversations. Utilisez mistral-conversations pour les modèles natifs Mistral. Si vous acheminez intentionnellement des points de terminaison compatibles Mistral/personnalisés via openai-completions, définissez explicitement les indicateurs compat si nécessaire.

En-tête d'authentification

Si votre fournisseur attend Authorization: Bearer <key> mais n'utilise pas de API standard, définissez 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 clé est résolue pour chaque demande. Un en-tête de requête explicite Authorization est prioritaire sur la valeur générée.

OAuth Assistance

Ajoutez l'authentification OAuth/SSO qui s'intègre à /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;
    }
  }
});

Après l'inscription, les utilisateurs peuvent s'authentifier via /login corporate-ai.

OAuthConnexionRappels

L'objet callbacks fournit des interactions neutres en termes d'interface utilisateur pour le flux appartenant au fournisseur:

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>;
}

OAuthIdentifiants

Les informations d'identification sont conservées dans ~/.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
}

Diffusion personnalisée API

Pour les fournisseurs avec des API non standard, implémentez streamSimple. Étudiez les implémentations de fournisseurs existantes avant d'écrire la vôtre:

Implémentations de référence:

Modèle de flux

Tous les fournisseurs suivent le même modèle:

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;
}

Types d'événements

Poussez les événements via stream.push() dans cet ordre:

  1. { type: "start", partial: output } – Diffusion démarrée

  2. Événements de contenu (répétables, piste contentIndex pour chaque bloc):

    • { type: "text_start", contentIndex, partial } - Bloc de texte démarré
    • { type: "text_delta", contentIndex, delta, partial } - Morceau de texte
    • { type: "text_end", contentIndex, content, partial } - Bloc de texte terminé
    • { type: "thinking_start", contentIndex, partial } - La réflexion a commencé
    • { type: "thinking_delta", contentIndex, delta, partial } – Morceau de réflexion
    • { type: "thinking_end", contentIndex, content, partial } - La réflexion est terminée
    • { type: "toolcall_start", contentIndex, partial } - L'appel de l'outil a démarré
    • { type: "toolcall_delta", contentIndex, delta, partial } - Appel d'outil JSON morceau
    • { type: "toolcall_end", contentIndex, toolCall, partial } - Appel d'outil terminé
  3. { type: "done", reason, message } ou { type: "error", reason, error } – Diffusion terminée

Le champ partial de chaque événement contient l'état AssistantMessage actuel. Mettez à jour output.content au fur et à mesure que vous recevez des données, puis incluez output comme partial.

Blocs de contenu

Ajoutez des blocs de contenu à output.content dès leur arrivée:

// 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 });

Appels d'outils

Les appels d'outils nécessitent d'accumuler JSON et d'analyser:

// 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
});

Utilisation et coût

Mettez à jour l'utilisation à partir de la réponse API et calculez le coût:

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);

Erreurs de débordement de contexte

Lorsqu'une requête dépasse la fenêtre contextuelle du modèle, pi peut récupérer automatiquement en compactant la conversation et en réessayant. Cette récupération ne démarre que si pi reconnaît l'échec comme un débordement.

La détection s'exécute sur le message finalisé de l'assistant:

Si votre fournisseur renvoie des erreurs de débordement avec un message que pi ne reconnaît pas, normalisez l'erreur à partir de la même extension qui enregistre le fournisseur. Utilisez un gestionnaire message_end pour réécrire le message de l'assistant afin que son errorMessage commence par une phrase que pi reconnaît. La solution de secours générique context_length_exceeded est le choix le plus sûr.

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 s'exécute avant que pi ne suive le message de l'assistant pour le compactage automatique, donc le errorMessage réécrit est ce que pi vérifie. Une fois cela en place, pi:

  1. Détectez le débordement de errorMessage.
  2. Supprimez le message de l'assistant ayant échoué du contexte en direct.
  3. Exécutez le compactage.
  4. Réessayez la demande une fois.

Gardez soigneusement la réécriture:

  • Étendez-le à votre fournisseur (message.provider et ctx.model?.provider) afin que les erreurs non liées provenant d'autres fournisseurs ne soient pas touchées.
  • Faites correspondre un modèle spécifique au fournisseur, et non les modèles de débordement génériques de pi. Les erreurs de réécriture de limite de débit ou de limitation (rate limit, too many requests) déclencheraient faussement le compactage au lieu du chemin normal de nouvelle tentative avec interruption de pi.
  • Ignorer lorsque errorMessage inclut déjà context_length_exceeded afin que le gestionnaire soit idempotent.

Inscription

Enregistrez votre fonction de flux:

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

Tester votre implémentation

Testez votre fournisseur avec les mêmes suites de tests utilisées par les fournisseurs intégrés. Copiez et adaptez ces fichiers de test à partir de packages/ai/test/:

Test But
stream.test.ts Streaming de base, sortie de texte
tokens.test.ts Comptage et utilisation des jetons
abort.test.ts AbandonnerGestion du signal
empty.test.ts Réponses vides/minimales
context-overflow.test.ts Limites de la fenêtre contextuelle
image-limits.test.ts Gestion de la saisie des images
unicode-surrogate.test.ts Cas extrêmes Unicode
tool-call-without-result.test.ts Cas extrêmes d’appel d’outil
image-tool-result.test.ts Images dans les résultats de l'outil
total-tokens.test.ts Calcul total du jeton
cross-provider-handoff.test.ts Transfert de contexte entre fournisseurs

Exécutez des tests avec vos paires fournisseur/modèle pour vérifier la compatibilité.

Référence de configuration

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;
  };
}

Référence de définition du modèle

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 envoie reasoning: { effort }. deepseek envoie thinking: { type: "enabled" | "disabled" } et reasoning_effort lorsqu'il est activé. together envoie reasoning: { enabled } et aussi reasoning_effort lorsque supportsReasoningEffort est activé. qwen est pour le niveau supérieur de style DashScope enable_thinking. Utilisez qwen-chat-template pour les serveurs locaux compatibles Qwen qui lisent chat_template_kwargs.enable_thinking et ont besoin de preserve_thinking. Utilisez chat-template pour chat_template_kwargs configurable, par exemple DeepSeek V3.x derrière vLLM avec chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }. Utilisez thinkingFormat: "baseten" avec chatTemplateArgs lorsque le fournisseur s'attend à basculer les valeurs sous chat_template_args et prend éventuellement en charge reasoning_effort de niveau supérieur. cacheControlFormat: "anthropic" applique des marqueurs cache_control de style anthropique à l'invite système, à la dernière définition d'outil et au contenu textuel du dernier utilisateur, assistant ou résultat de l'outil.