Pi 구성, 확장, 플랫폼 설정 및 API 참조.

맞춤 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.registerProvider(name,...)를 통해 등록된 제공자를 제거하려면 pi.unregisterProvider(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 Anthropic Claude 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 생성 AI API
google-vertex Google Vertex AI API
bedrock-converse-stream 아마존 베드락 컨버스 API

대부분의 OpenAI 호환 공급자는 openai-completions와 함께 작동합니다. 모델별 사고 수준에는 모델 수준 thinkingLevelMap을 사용하고 공급자의 특이 사항에는 compat를 사용하세요. xhighmax 수준은 선택 가능하고 null이 아닌 지도 항목이 필요하며 지원되지 않는 구멍으로 구분될 수 있습니다.

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도 전송됩니다. chat_template_kwargs.enable_thinking을 읽고 preserve_thinking이 필요한 로컬 Qwen 호환 서버에는 qwen-chat-template를 사용하세요. 시스템 프롬프트, 마지막 도구 정의, 마지막 사용자, 보조자 또는 도구 결과 텍스트 콘텐츠에서 cache_control를 통해 Anthropic 스타일 프롬프트 캐싱을 노출하는 OpenAI 호환 공급자의 경우 cacheControlFormat: "anthropic"를 사용하세요.

api: "anthropic-messages"를 사용하는 인류 호환 제공자의 경우 업스트림 모델에 적응적 사고가 필요한 모델 또는 제공자에 compat.forceAdaptiveThinking: true를 설정합니다(thinking.type: "adaptive" + output_config.effort). 내장된 적응형 Claude 모델은 이를 자동으로 설정합니다. 빈 생각 서명을 내보내고 재생 시 signature: ""를 기대하는 공급자에 대해서만 compat.allowEmptySignature: true를 설정합니다.

마이그레이션 참고 사항: Mistral이 openai-completions에서 mistral-conversations로 이동했습니다. 기본 Mistral 모델에는 mistral-conversations를 사용하세요. openai-completions를 통해 의도적으로 Mistral 호환/사용자 지정 엔드포인트를 라우팅하는 경우 필요에 따라 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 지원

/login와 통합되는 OAuth/SSO 인증을 추가하세요.

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 객체는 공급자 소유 흐름에 대해 UI 중립적 상호 작용을 제공합니다.

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를 업데이트한 다음 outputpartial로 포함합니다.

콘텐츠 블록

콘텐츠 블록이 도착하면 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가 실패를 오버플로로 인식하는 경우에만 시작됩니다.

최종 어시스턴트 메시지에서 감지가 실행됩니다.

공급자가 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가 자동 압축을 위한 보조 메시지를 추적하기 전에 실행되므로 다시 작성된 errorMessage가 pi가 확인하는 것입니다. 이를 적용하면 pi는 다음을 수행합니다.

  1. errorMessage에서 오버플로를 감지합니다.
  2. 라이브 컨텍스트에서 실패한 어시스턴트 메시지를 삭제하세요.
  3. 압축을 실행합니다.
  4. 요청을 한 번 다시 시도하세요.

재작성 시 주의 깊게 보호하세요.

  • 범위를 제공업체(message.providerctx.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 Abort신호 처리
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;
  };
}

openrouterreasoning: { effort }을 보냅니다. deepseek는 활성화되면 thinking: { type: "enabled" | "disabled" }reasoning_effort를 보냅니다. togetherreasoning: { enabled }를 전송하고 supportsReasoningEffort가 활성화되면 reasoning_effort도 전송합니다. qwen는 DashScope 스타일 최상위 enable_thinking용입니다. chat_template_kwargs.enable_thinking를 읽고 preserve_thinking이 필요한 로컬 Qwen 호환 서버에는 qwen-chat-template를 사용하세요. chat_template_kwargs를 구성하려면 chat-template를 사용하세요. 예를 들어 chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }이 있는 vLLM 뒤의 DeepSeek V3.x입니다. 공급자가 chat_template_args 아래의 토글 값을 예상하고 선택적으로 최상위 reasoning_effort를 지원하는 경우 thinkingFormat: "baseten"chatTemplateArgs와 함께 사용하세요. cacheControlFormat: "anthropic"는 인류 스타일 cache_control 마커를 시스템 프롬프트, 마지막 도구 정의, 마지막 사용자, 보조자 또는 도구 결과 텍스트 콘텐츠에 적용합니다.