import{createProvider,openAICompletionsApi}from"@earendil-works/pi-ai";importtype{ExtensionAPI}from"@earendil-works/pi-coding-agent";exportdefaultfunction(pi: ExtensionAPI){pi.registerProvider(createProvider({id:"native-local",name:"Native Local",baseUrl:"http://localhost:8080/v1",auth:{apiKey:{name:"Local server API key",asynclogin(interaction){return{type:"api_key",key: awaitinteraction.prompt({type:"secret",message:"API key"})};},asyncresolve({credential}){returncredential?.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
}});
interfaceOAuthLoginCallbacks{// 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>;}
OAuthCredentials
凭据会持久化到 ~/.pi/agent/auth.json:
interfaceOAuthCredentials{refresh: string;// Refresh token (for refreshToken())
access: string;// Access token (returned by getApiKey())
expires: number;// Expiration timestamp in milliseconds
}
自定义流式 API
对于使用非标准 API 的提供商,可实现 streamSimple。编写自己的实现之前,请先研究现有的 API 实现:
// Text block
output.content.push({type:"text",text:""});stream.push({type:"text_start",contentIndex: output.content.length-1,partial: output});// As text arrives
constblock=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});
interfaceProviderConfig{/** 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;};}
模型定义参考
interfaceProviderModelConfig{/** 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"|"thinking.budget";omitWhenOff?:boolean}>;chatTemplateArgs?: Record<string,string|number|boolean|null|{"$var":"thinking.enabled"|"thinking.effort"|"thinking.budget";omitWhenOff?:boolean}>;thinkingTokenBudgetField?:"thinking_token_budget"|"thinking_budget"|"thinking_budget_tokens";supportsThinkingTokenBudget?: 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;};}