맞춤 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.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를 사용하세요. xhigh 및 max 수준은 선택 가능하고 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를 구현하세요. 직접 작성하기 전에 기존 공급자 구현을 연구하십시오.
참조 구현:
- anthropic.ts - 인류학적 메시지 API
- mistral.ts - 미스트랄 대화 API
- openai-completions.ts - OpenAI 채팅 완료
- openai-responses.ts - OpenAI 응답 API
- google.ts - Google 생성 AI
- 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가 자동 압축을 위한 보조 메시지를 추적하기 전에 실행되므로 다시 작성된 errorMessage가 pi가 확인하는 것입니다. 이를 적용하면 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 |
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;
};
}openrouter가 reasoning: { effort }을 보냅니다. deepseek는 활성화되면 thinking: { type: "enabled" | "disabled" } 및 reasoning_effort를 보냅니다. together는 reasoning: { 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 마커를 시스템 프롬프트, 마지막 도구 정의, 마지막 사용자, 보조자 또는 도구 결과 텍스트 콘텐츠에 적용합니다.