Konfigurasi, kustomisasi, pengaturan platform, dan referensi API untuk Pi.

Kustom Providers

Extensions dapat mendaftarkan penyedia model khusus melalui pi.registerProvider(). Hal ini memungkinkan:

  • Proxy - Merutekan permintaan melalui proxy perusahaan atau gateway API
  • Titik akhir khusus - Gunakan penerapan model yang dihosting sendiri atau pribadi
  • OAuth/SSO - Tambahkan alur autentikasi untuk penyedia perusahaan
  • Kustom APIs - Terapkan streaming untuk LLM non-standar APIs

Contoh Extensions

Lihat contoh penyedia lengkap ini:

Daftar isi

Referensi Cepat

Extensions dapat mendaftarkan pi-ai lengkap Provider atau menggunakan formulir konfigurasi penyedia lama. Pilih penyedia yang lengkap ketika autentikasi khusus, pemfilteran, penyegaran, atau perilaku streaming diperlukan. Pi menyusun models.json menimpa penyedia asli terdaftar di atas.

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

Pabrik ekstensi juga bisa async. Untuk penemuan model dinamis, ambil dan daftarkan model di pabrik, bukan di session_start. pi menunggu pabrik sebelum pengaktifan dilanjutkan, sehingga penyedia tersedia selama pengaktifan interaktif dan ke pi --list-models.

Ganti Penyedia yang Ada

Kasus penggunaan paling sederhana: mengarahkan ulang penyedia yang ada melalui 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
  }
});

Jika hanya baseUrl dan/atau headers yang disediakan (tidak ada models), semua model yang ada untuk penyedia tersebut akan dipertahankan dengan titik akhir baru.

Daftarkan Penyedia Baru

Untuk menambahkan penyedia yang benar-benar baru, tentukan models beserta konfigurasi yang diperlukan.

Jika daftar model berasal dari titik akhir jarak jauh, gunakan pabrik ekstensi async:

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

Ini mendaftarkan model yang diambil sebelum startup selesai.

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

Jika models disediakan, ini menggantikan semua model yang ada untuk penyedia tersebut.

apiKey dan nilai header khusus menggunakan sintaks nilai konfigurasi yang sama seperti models.json: !command di awal menjalankan perintah untuk seluruh nilai, $ENV_VAR dan ${ENV_VAR} menginterpolasi variabel lingkungan, $ memancarkan ``apiKeydan nilai header khusus menggunakan sintaks nilai konfigurasi yang sama sepertimodels.json: !commanddi awal menjalankan perintah untuk seluruh nilai,$ENV_VARdan${ENV_VAR}menginterpolasi variabel lingkungan,$memancarkan literal, dan$!memancarkan!` literal.

Batalkan Pendaftaran Penyedia

Gunakan pi.unregisterProvider(name) untuk menghapus penyedia yang sebelumnya terdaftar melalui 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");

Membatalkan pendaftaran akan menghapus model dinamis penyedia tersebut, API key fallback, OAuth pendaftaran penyedia, dan pendaftaran pengendali aliran khusus. Model bawaan atau perilaku penyedia apa pun yang diganti akan dipulihkan.

Panggilan yang dilakukan setelah fase beban ekstensi awal diterapkan segera, jadi tidak diperlukan /reload.

API Jenis

Bidang api menentukan implementasi streaming mana yang digunakan:

API Gunakan untuk
anthropic-messages Claude Antropik API dan yang kompatibel
openai-completions Penyelesaian Obrolan OpenAI API dan yang kompatibel
openai-responses Respons OpenAI API
azure-openai-responses Respons Azure OpenAI API
openai-codex-responses Respons Kodeks OpenAI API
mistral-conversations Streaming Penyelesaian Obrolan Mistral Asli
google-generative-ai AI Generatif Google API
google-vertex Google Vertex AI API
bedrock-converse-stream Batu Dasar Amazon Converse API

Sebagian besar penyedia yang kompatibel dengan OpenAI bekerja dengan openai-completions. Gunakan tingkat model thinkingLevelMap untuk tingkat pemikiran khusus model, dan compat untuk kebiasaan penyedia layanan. Level xhigh dan max bersifat opt-in, memerlukan entri peta bukan nol, dan dapat dipisahkan oleh lubang yang tidak didukung:

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

Gunakan openrouter untuk kontrol reasoning: { effort } gaya OpenRouter. Gunakan together untuk kontrol gaya Together reasoning: { enabled }; dengan supportsReasoningEffort, ia juga mengirimkan reasoning_effort. Gunakan qwen-chat-template untuk server lokal yang kompatibel dengan Qwen yang membaca chat_template_kwargs.enable_thinking dan memerlukan preserve_thinking. Gunakan cacheControlFormat: "anthropic" untuk penyedia yang kompatibel dengan OpenAI yang mengekspos cache cepat gaya Antropik melalui cache_control pada perintah sistem, definisi alat terakhir, dan konten teks pengguna, asisten, atau hasil alat terakhir.

Untuk penyedia yang kompatibel dengan Antropik yang menggunakan api: "anthropic-messages", tetapkan compat.forceAdaptiveThinking: true pada model atau penyedia yang model hulunya memerlukan pemikiran adaptif (thinking.type: "adaptive" plus output_config.effort). Model Claude adaptif bawaan mengatur ini secara otomatis. Tetapkan compat.allowEmptySignature: true hanya untuk penyedia yang mengeluarkan tanda tangan berpikir kosong dan mengharapkan signature: "" diputar ulang.

Catatan migrasi: Mistral berpindah dari openai-completions ke mistral-conversations. Gunakan mistral-conversations untuk model Mistral asli. Jika Anda sengaja merutekan titik akhir yang kompatibel/khusus Mistral melalui openai-completions, setel tanda compat secara eksplisit sesuai kebutuhan.

Tajuk Otentikasi

Jika penyedia Anda mengharapkan Authorization: Bearer <key> tetapi tidak menggunakan standar API, tetapkan 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: [...]
});

Kuncinya diselesaikan untuk setiap permintaan. Permintaan eksplisit Authorization header lebih diutamakan daripada nilai yang dihasilkan.

OAuth Dukungan

Tambahkan autentikasi OAuth/SSO yang terintegrasi dengan /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;
    }
  }
});

Setelah registrasi, pengguna dapat mengautentikasi melalui /login corporate-ai.

OAuthLoginCallback

Objek callbacks menyediakan interaksi UI-netral untuk aliran milik penyedia:

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

OAuthKredensial

Kredensial dipertahankan di ~/.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
}

Streaming Khusus API

Untuk penyedia dengan API non-standar, terapkan streamSimple. Pelajari implementasi penyedia yang ada sebelum menulis implementasi Anda sendiri:

Implementasi referensi:

Pola Aliran

Semua penyedia mengikuti pola yang sama:

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

Jenis Acara

Dorong acara melalui stream.push() dalam urutan ini:

  1. { type: "start", partial: output } - Streaming dimulai

  2. Peristiwa konten (dapat diulang, lacak contentIndex untuk setiap blok):

    • { type: "text_start", contentIndex, partial } - Blok teks dimulai
    • { type: "text_delta", contentIndex, delta, partial } - Potongan teks
    • { type: "text_end", contentIndex, content, partial } - Blok teks berakhir
    • { type: "thinking_start", contentIndex, partial } - Pemikiran dimulai
    • { type: "thinking_delta", contentIndex, delta, partial } - Potongan pemikiran
    • { type: "thinking_end", contentIndex, content, partial } - Pemikiran berakhir
    • { type: "toolcall_start", contentIndex, partial } - Panggilan alat dimulai
    • { type: "toolcall_delta", contentIndex, delta, partial } - Panggilan alat JSON potongan
    • { type: "toolcall_end", contentIndex, toolCall, partial } - Panggilan alat berakhir
  3. { type: "done", reason, message } atau { type: "error", reason, error } - Streaming berakhir

Bidang partial di setiap peristiwa berisi status AssistantMessage saat ini. Perbarui output.content saat Anda menerima data, lalu sertakan output sebagai partial.

Blok Konten

Tambahkan blok konten ke output.content saat blok tersebut tiba:

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

Panggilan Alat

Panggilan alat memerlukan akumulasi JSON dan penguraian:

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

Penggunaan dan Biaya

Perbarui penggunaan dari API respons dan hitung biaya:

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

Kesalahan Meluap Konteks

Ketika permintaan melebihi jendela konteks model, pi dapat pulih secara otomatis dengan memadatkan percakapan dan mencoba lagi. Pemulihan ini hanya dimulai jika pi mengenali kegagalan sebagai luapan.

Deteksi berjalan pada pesan asisten yang diselesaikan:

Jika penyedia Anda mengembalikan kesalahan overflow dengan pesan yang tidak dikenali pi, normalkan kesalahan dari ekstensi yang sama yang mendaftarkan penyedia tersebut. Gunakan pengendali message_end untuk menulis ulang pesan asisten sehingga errorMessage dimulai dengan frasa yang dikenali pi. Penggantian umum context_length_exceeded adalah pilihan paling aman.

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 berjalan sebelum pi melacak pesan asisten untuk pemadatan otomatis, jadi errorMessage yang ditulis ulang itulah yang diperiksa pi. Dengan ini, pi akan:

  1. Deteksi luapan dari errorMessage.
  2. Hapus pesan asisten yang gagal dari konteks langsung.
  3. Jalankan pemadatan.
  4. Coba lagi permintaan tersebut satu kali.

Jaga penulisan ulang dengan hati-hati:

  • Cakupannya ke penyedia Anda (message.provider dan ctx.model?.provider) sehingga kesalahan yang tidak terkait dari penyedia lain tidak tersentuh.
  • Cocokkan pola khusus penyedia, bukan pola luapan umum pi. Kesalahan penulisan ulang batas kecepatan atau pelambatan (rate limit, too many requests) akan memicu pemadatan secara salah, bukan jalur percobaan ulang dengan kemunduran normal pi.
  • Lewati ketika errorMessage sudah menyertakan context_length_exceeded sehingga handlernya idempoten.

Pendaftaran

Daftarkan fungsi streaming Anda:

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

Menguji Implementasi Anda

Uji penyedia Anda terhadap rangkaian pengujian yang sama dengan yang digunakan oleh penyedia bawaan. Salin dan sesuaikan file pengujian ini dari packages/ai/test/:

Tes Tujuan
stream.test.ts Streaming dasar, keluaran teks
tokens.test.ts Penghitungan dan penggunaan token
abort.test.ts Batalkan penanganan sinyal
empty.test.ts Respons kosong/minimal
context-overflow.test.ts Batas jendela konteks
image-limits.test.ts Penanganan masukan gambar
unicode-surrogate.test.ts Kasus tepi Unicode
tool-call-without-result.test.ts Kasus tepi panggilan alat
image-tool-result.test.ts Gambar dalam hasil alat
total-tokens.test.ts Perhitungan total token
cross-provider-handoff.test.ts Penyerahan konteks antar penyedia

Jalankan pengujian dengan pasangan penyedia/model Anda untuk memverifikasi kompatibilitas.

Referensi Konfigurasi

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

Referensi Definisi Model

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 mengirim reasoning: { effort }. deepseek mengirim thinking: { type: "enabled" | "disabled" } dan reasoning_effort saat diaktifkan. together mengirim reasoning: { enabled } dan juga reasoning_effort ketika supportsReasoningEffort diaktifkan. qwen adalah untuk tingkat atas gaya DashScope enable_thinking. Gunakan qwen-chat-template untuk server lokal yang kompatibel dengan Qwen yang membaca chat_template_kwargs.enable_thinking dan memerlukan preserve_thinking. Gunakan chat-template untuk chat_template_kwargs yang dapat dikonfigurasi, misalnya DeepSeek V3.x di belakang vLLM dengan chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }. Gunakan thinkingFormat: "baseten" dengan chatTemplateArgs ketika penyedia mengharapkan nilai peralihan di bawah chat_template_args dan secara opsional mendukung reasoning_effort tingkat atas. cacheControlFormat: "anthropic" menerapkan penanda gaya Antropis cache_control ke perintah sistem, definisi alat terakhir, dan konten teks pengguna, asisten, atau hasil alat terakhir.