Пользовательский Providers
Extensions может зарегистрировать поставщиков пользовательских моделей через pi.registerProvider(). Это позволяет:
- Прокси – маршрутизация запросов через корпоративные прокси или шлюзы API.
- Пользовательские конечные точки – используйте развертывания локальной или частной модели.
- OAuth/SSO – добавление потоков аутентификации для корпоративных поставщиков.
- Пользовательские APIs — реализация потоковой передачи для нестандартных LLM APIs.
Пример Extensions
См. эти полные примеры поставщиков:
Оглавление
- Example Extensions
- Quick Reference
- Override Existing Provider
- Register New Provider
- Unregister Provider
- OAuth Support
- Custom Streaming API
- Context Overflow Errors
- Testing Your Implementation
- Config Reference
- Model Definition Reference
Краткий справочник
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. Прежде чем писать свою собственную, изучите существующие реализации провайдера:
Эталонные реализации:
- anthropic.ts - Антропные сообщения API
- mistral.ts - Мистральские разговоры API
- openai-completions.ts — Завершения чата OpenAI
- openai-responses.ts - Ответы OpenAI API
- google.ts — Генеративный искусственный интеллект Google
- amazon-bedrock.ts — Основа AWS
Шаблон потока
Все провайдеры следуют одной и той же схеме:
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() в следующем порядке:
{ type: "start", partial: output }— трансляция началась.События контента (повторяемые, трек
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 }— вызов инструмента завершен.
{ 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 будет:
- Обнаружить переполнение от
errorMessage. - Удалите сообщение о сбое помощника из живого контекста.
- Запустите уплотнение.
- Повторите запрос один раз.
Тщательно охраняйте переписывание:
- Присвойте его своему провайдеру (
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 к системному приглашению, последнему определению инструмента и последнему текстовому содержимому пользователя, помощника или результата инструмента.