Настройка, расширение, параметры платформы и справочник API для Pi.

Пользовательский Providers

Extensions может зарегистрировать поставщиков пользовательских моделей через pi.registerProvider(). Это позволяет:

  • Прокси – маршрутизация запросов через корпоративные прокси или шлюзы API.
  • Пользовательские конечные точки – используйте развертывания локальной или частной модели.
  • OAuth/SSO – добавление потоков аутентификации для корпоративных поставщиков.
  • Пользовательские APIs — реализация потоковой передачи для нестандартных LLM APIs.

Пример Extensions

См. эти полные примеры поставщиков:

Оглавление

Краткий справочник

Extensions может зарегистрировать либо полный pi-ai Provider, либо использовать устаревшую форму конфигурации поставщика. Если требуется настраиваемая проверка подлинности, фильтрация, обновление или потоковая передача, отдайте предпочтение полному поставщику. Pi составляет models.json переопределение над зарегистрированными собственными поставщиками.

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

Фабрикой расширений также может быть async. Для динамического обнаружения моделей извлекайте и регистрируйте модели на фабрике вместо session_start. pi ожидает фабрику перед продолжением запуска, поэтому поставщик доступен во время интерактивного запуска и до pi --list-models.

Переопределить существующего поставщика

Самый простой вариант использования: перенаправить существующего провайдера через прокси.

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

Если указаны только baseUrl и/или headers (нет models), все существующие модели для этого поставщика сохраняются с новой конечной точкой.

Зарегистрировать нового провайдера

Чтобы добавить совершенно нового провайдера, укажите models вместе с необходимой конфигурацией.

Если список моделей поступает из удаленной конечной точки, используйте фабрику асинхронных расширений:

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

Это регистрирует полученные модели до завершения запуска.

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

Если указан models, он заменяет все существующие модели для этого поставщика.

apiKey и значения пользовательского заголовка используют тот же синтаксис значений конфигурации, что и models.json: !command в начале выполняет команду для всего значения, $ENV_VAR и ${ENV_VAR} интерполируют переменные среды, $ выдает литерал ``apiKeyи значения пользовательского заголовка используют тот же синтаксис значений конфигурации, что иmodels.json: !commandв начале выполняет команду для всего значения,$ENV_VARи${ENV_VAR}интерполируют переменные среды,$выдает литерал и$!выдает литерал!`.

Отменить регистрацию поставщика

Используйте pi.unregisterProvider(name), чтобы удалить провайдера, который ранее был зарегистрирован через 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");

При отмене регистрации удаляются динамические модели этого поставщика, резервный вариант API key, регистрация поставщика OAuth и регистрации пользовательского обработчика потока. Любые встроенные модели или поведение поставщика, которые были переопределены, восстанавливаются.

Вызовы, сделанные после начальной фазы загрузки расширения, применяются немедленно, поэтому /reload не требуется.

API Типы

Поле api определяет, какая реализация потоковой передачи используется:

API Используйте для
anthropic-messages Антропный Клод API и совместимые
openai-completions Завершения чата OpenAI API и совместимые
openai-responses Ответы OpenAI API
azure-openai-responses Ответы Azure OpenAI API
openai-codex-responses Ответы Кодекса OpenAI API
mistral-conversations Трансляция нативных завершений чата Mistral
google-generative-ai Генеративный искусственный интеллект Google API
google-vertex Google Вертекс ИИ API
bedrock-converse-stream Конверсы Amazon Bedrock API

Большинство OpenAI-совместимых провайдеров работают с openai-completions. Используйте уровень модели thinkingLevelMap для уровней мышления, специфичных для модели, и compat для особенностей поставщика. Уровни xhigh и max являются добровольными, требуют ненулевых записей карты и могут быть разделены неподдерживаемыми пробелами:

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

Используйте openrouter для элементов управления reasoning: { effort } в стиле OpenRouter. Используйте together для элементов управления reasoning: { enabled } в стиле Together; с supportsReasoningEffort он также отправляет reasoning_effort. Используйте qwen-chat-template для локальных Qwen-совместимых серверов, которые читают chat_template_kwargs.enable_thinking и нуждаются в preserve_thinking. Используйте cacheControlFormat: "anthropic" для поставщиков, совместимых с OpenAI, которые предоставляют кэширование подсказок в стиле Anthropic через cache_control в системном приглашении, последнем определении инструмента и последнем текстовом содержимом пользователя, помощника или результата инструмента.

Для антропосовместимых поставщиков, использующих api: "anthropic-messages", установите compat.forceAdaptiveThinking: true для моделей или поставщиков, чья восходящая модель требует адаптивного мышления (thinking.type: "adaptive" плюс output_config.effort). Встроенные адаптивные модели Клода устанавливают это автоматически. Установите compat.allowEmptySignature: true только для провайдеров, которые излучают пустые мыслительные сигнатуры и ожидают signature: "" при воспроизведении.

Примечание по миграции: Мистраль перемещен с openai-completions на mistral-conversations. Используйте mistral-conversations для родных моделей Mistral. Если вы намеренно маршрутизируете совместимые с Mistral/пользовательские конечные точки через openai-completions, при необходимости явно установите флаги compat.

Заголовок аутентификации

Если ваш провайдер ожидает Authorization: Bearer <key>, но не использует стандартный API, установите 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: [...]
});

Ключ разрешается для каждого запроса. Явный заголовок запроса Authorization имеет приоритет над сгенерированным значением.

OAuth Поддержка

Добавьте аутентификацию OAuth/SSO, которая интегрируется с /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;
    }
  }
});

После регистрации пользователи могут пройти аутентификацию через /login corporate-ai.

OAuthОбратные вызовы для входа в систему

Объект callbacks обеспечивает нейтральное к пользовательскому интерфейсу взаимодействие для потока, принадлежащего провайдеру:

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

OAuthУчетные данные

Учетные данные сохраняются в ~/.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
}

Пользовательская потоковая передача API

Для провайдеров с нестандартными API внедрите streamSimple. Прежде чем писать свою собственную, изучите существующие реализации провайдера:

Эталонные реализации:

Шаблон потока

Все провайдеры следуют одной и той же схеме:

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

Типы событий

Отправьте события через stream.push() в следующем порядке:

  1. { type: "start", partial: output } — трансляция началась.

  2. События контента (повторяемые, трек contentIndex для каждого блока):

    • { type: "text_start", contentIndex, partial } — текстовый блок запущен.
    • { type: "text_delta", contentIndex, delta, partial } — текстовый фрагмент
    • { type: "text_end", contentIndex, content, partial } — текстовый блок завершен.
    • { type: "thinking_start", contentIndex, partial } — Начал думать
    • { type: "thinking_delta", contentIndex, delta, partial } — Мыслящий фрагмент
    • { type: "thinking_end", contentIndex, content, partial } — Раздумья закончились
    • { type: "toolcall_start", contentIndex, partial } — Начался вызов инструмента.
    • { type: "toolcall_delta", contentIndex, delta, partial } — вызов инструмента JSON чанк
    • { type: "toolcall_end", contentIndex, toolCall, partial } — вызов инструмента завершен.
  3. { type: "done", reason, message } или { type: "error", reason, error } — трансляция завершена.

Поле partial в каждом событии содержит текущее состояние AssistantMessage. Обновите output.content по мере получения данных, затем включите output в качестве partial.

Блоки контента

Добавляйте блоки контента в output.content по мере их поступления:

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

Вызовы инструментов

Вызовы инструментов требуют накопления JSON и анализа:

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

Использование и стоимость

Обновите использование из ответа API и рассчитайте стоимость:

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

Ошибки переполнения контекста

Когда запрос превышает контекстное окно модели, pi может автоматически восстановиться, сжимая диалог и повторяя попытку. Это восстановление вступает в силу только в том случае, если pi распознает сбой как переполнение.

Обнаружение выполняется на основе окончательного сообщения помощника:

  • stopReason === "error"
  • errorMessage соответствует одному из известных шаблонов переполнения числа pi (см. packages/ai/src/utils/overflow.ts)

Если ваш провайдер возвращает ошибки переполнения с сообщением, которое pi не распознает, нормализуйте ошибку из того же расширения, которое регистрирует провайдера. Используйте обработчик message_end, чтобы переписать сообщение помощника так, чтобы его errorMessage начиналось с фразы, которую распознает pi. Общий запасной вариант context_length_exceeded — самый безопасный выбор.

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 запускается до того, как pi отслеживает сообщение помощника для автоматического сжатия, поэтому pi проверяет переписанный errorMessage. При этом число pi будет:

  1. Обнаружить переполнение от errorMessage.
  2. Удалите сообщение о сбое помощника из живого контекста.
  3. Запустите уплотнение.
  4. Повторите запрос один раз.

Тщательно охраняйте переписывание:

  • Присвойте его своему провайдеру (message.provider и ctx.model?.provider), чтобы несвязанные ошибки других провайдеров не были затронуты.
  • Сопоставьте шаблон, специфичный для поставщика, а не общие шаблоны переполнения pi. Перезапись ошибок ограничения скорости или регулирования (rate limit, too many requests) будет ложно запускать уплотнение вместо обычного пути повторной попытки с откатом в pi.
  • Пропустить, если errorMessage уже включает в себя context_length_exceeded, поэтому обработчик является идемпотентным.

Регистрация

Зарегистрируйте свою потоковую функцию:

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

Тестирование вашей реализации

Проверьте своего провайдера с помощью тех же наборов тестов, которые используются встроенными провайдерами. Скопируйте и адаптируйте эти тестовые файлы из packages/ai/test/:

Тест Цель
stream.test.ts Базовая потоковая передача, текстовый вывод
tokens.test.ts Подсчет и использование токенов
abort.test.ts Обработка сигнала прерывания
empty.test.ts Пустые/минимальные ответы
context-overflow.test.ts Ограничения контекстного окна
image-limits.test.ts Обработка ввода изображений
unicode-surrogate.test.ts Краевые случаи Юникода
tool-call-without-result.test.ts Краевые случаи вызова инструмента
image-tool-result.test.ts Изображения в результатах инструмента
total-tokens.test.ts Общий расчет токенов
cross-provider-handoff.test.ts Передача контекста между провайдерами

Запустите тесты с парами поставщик/модель, чтобы проверить совместимость.

Справочник по конфигурации

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

Справочник по определению модели

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 отправляет reasoning: { effort }. deepseek отправляет thinking: { type: "enabled" | "disabled" } и reasoning_effort, когда включено. together отправляет reasoning: { enabled }, а также reasoning_effort, когда supportsReasoningEffort включено. qwen соответствует верхнему уровню enable_thinking в стиле DashScope. Используйте qwen-chat-template для локальных Qwen-совместимых серверов, которые читают chat_template_kwargs.enable_thinking и нуждаются в preserve_thinking. Используйте chat-template для настраиваемого chat_template_kwargs, например DeepSeek V3.x за vLLM с chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }. Используйте thinkingFormat: "baseten" с chatTemplateArgs, если поставщик ожидает переключения значений ниже chat_template_args и при необходимости поддерживает reasoning_effort верхнего уровня. cacheControlFormat: "anthropic" применяет маркеры cache_control в стиле Anthropic к системному приглашению, последнему определению инструмента и последнему текстовому содержимому пользователя, помощника или результата инструмента.