Custom Executors
Executors handle the actual communication with AI service providers. You can create custom executors to integrate new providers or modify existing behavior.Executor Interface
To create a custom executor, implement theExecutor interface:
package executor
type Executor interface {
// Identifier returns the unique provider key
Identifier() string
// Execute handles non-streaming requests
Execute(ctx context.Context, auth *coreauth.Auth, req Request, opts Options) (Response, error)
// ExecuteStream handles streaming requests
ExecuteStream(ctx context.Context, auth *coreauth.Auth, req Request, opts Options) (*StreamResult, error)
// PrepareRequest injects credentials into HTTP requests
PrepareRequest(req *http.Request, auth *coreauth.Auth) error
// HttpRequest executes arbitrary HTTP requests with auth
HttpRequest(ctx context.Context, auth *coreauth.Auth, req *http.Request) (*http.Response, error)
// Refresh refreshes authentication credentials
Refresh(ctx context.Context, auth *coreauth.Auth) (*coreauth.Auth, error)
// CountTokens estimates token usage (optional)
CountTokens(ctx context.Context, auth *coreauth.Auth, req Request, opts Options) (Response, error)
}
Executor Types
The SDK defines key types for request/response handling:// Request encapsulates the translated payload
type Request struct {
Model string // Upstream model identifier
Payload []byte // Provider-specific JSON payload
Format sdktranslator.Format // Payload schema
Metadata map[string]any // Execution hints
}
// Options controls execution behavior
type Options struct {
Stream bool // Enable streaming
Alt string // Alternate format hint
Headers http.Header // Request headers
Query url.Values // Query parameters
OriginalRequest []byte // Inbound request bytes
SourceFormat sdktranslator.Format // Inbound schema
Metadata map[string]any // Execution hints
}
// Response wraps provider response
type Response struct {
Payload []byte // Provider response
Metadata map[string]any // Structured data
Headers http.Header // Response headers
}
// StreamResult wraps streaming response
type StreamResult struct {
Headers http.Header // HTTP headers
Chunks <-chan StreamChunk // Chunk channel
}
// StreamChunk represents a single streaming payload
type StreamChunk struct {
Payload []byte // Raw chunk data
Err error // Terminal error
}
Complete Executor Example
Here’s a complete custom executor implementation:package main
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
clipexec "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
)
const providerKey = "myprovider"
// MyExecutor implements a custom AI provider
type MyExecutor struct{}
// Identifier returns the unique provider identifier
func (MyExecutor) Identifier() string {
return providerKey
}
// PrepareRequest injects authentication credentials
func (MyExecutor) PrepareRequest(req *http.Request, auth *coreauth.Auth) error {
if req == nil || auth == nil {
return nil
}
// Inject API key from auth attributes
if auth.Attributes != nil {
if apiKey := strings.TrimSpace(auth.Attributes["api_key"]); apiKey != "" {
req.Header.Set("Authorization", "Bearer "+apiKey)
}
}
// Add custom headers
req.Header.Set("X-Provider", providerKey)
return nil
}
// Execute handles non-streaming requests
func (MyExecutor) Execute(
ctx context.Context,
auth *coreauth.Auth,
req clipexec.Request,
opts clipexec.Options,
) (clipexec.Response, error) {
// Get upstream endpoint
endpoint := "https://api.myprovider.com/v1/chat/completions"
if auth != nil && auth.Attributes != nil {
if ep := auth.Attributes["endpoint"]; ep != "" {
endpoint = ep
}
}
// Create HTTP request
httpReq, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
endpoint,
bytes.NewReader(req.Payload),
)
if err != nil {
return clipexec.Response{}, err
}
httpReq.Header.Set("Content-Type", "application/json")
// Inject credentials
if err := (MyExecutor{}).PrepareRequest(httpReq, auth); err != nil {
return clipexec.Response{}, err
}
// Execute request
client := http.DefaultClient
resp, err := client.Do(httpReq)
if err != nil {
return clipexec.Response{}, err
}
defer resp.Body.Close()
// Read response
body, err := io.ReadAll(resp.Body)
if err != nil {
return clipexec.Response{}, err
}
// Check for errors
if resp.StatusCode != http.StatusOK {
return clipexec.Response{}, &statusError{
code: resp.StatusCode,
message: string(body),
}
}
return clipexec.Response{
Payload: body,
Headers: resp.Header,
}, nil
}
// ExecuteStream handles streaming requests
func (MyExecutor) ExecuteStream(
ctx context.Context,
auth *coreauth.Auth,
req clipexec.Request,
opts clipexec.Options,
) (*clipexec.StreamResult, error) {
// Get endpoint
endpoint := "https://api.myprovider.com/v1/chat/completions"
if auth != nil && auth.Attributes != nil {
if ep := auth.Attributes["endpoint"]; ep != "" {
endpoint = ep
}
}
// Create request
httpReq, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
endpoint,
bytes.NewReader(req.Payload),
)
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "text/event-stream")
if err := (MyExecutor{}).PrepareRequest(httpReq, auth); err != nil {
return nil, err
}
// Execute request
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
// Create chunk channel
chunks := make(chan clipexec.StreamChunk, 16)
go func() {
defer close(chunks)
defer resp.Body.Close()
// Read streaming response
buf := make([]byte, 4096)
for {
n, err := resp.Body.Read(buf)
if n > 0 {
// Send chunk
chunk := make([]byte, n)
copy(chunk, buf[:n])
select {
case chunks <- clipexec.StreamChunk{Payload: chunk}:
case <-ctx.Done():
return
}
}
if err != nil {
if err != io.EOF {
chunks <- clipexec.StreamChunk{Err: err}
}
return
}
}
}()
return &clipexec.StreamResult{
Headers: resp.Header,
Chunks: chunks,
}, nil
}
// HttpRequest executes arbitrary HTTP requests
func (MyExecutor) HttpRequest(
ctx context.Context,
auth *coreauth.Auth,
req *http.Request,
) (*http.Response, error) {
if req == nil {
return nil, errors.New("request is nil")
}
httpReq := req.WithContext(ctx)
if err := (MyExecutor{}).PrepareRequest(httpReq, auth); err != nil {
return nil, err
}
return http.DefaultClient.Do(httpReq)
}
// Refresh refreshes authentication (if needed)
func (MyExecutor) Refresh(
ctx context.Context,
auth *coreauth.Auth,
) (*coreauth.Auth, error) {
// Return unchanged if no refresh needed
return auth, nil
}
// CountTokens estimates token usage (optional)
func (MyExecutor) CountTokens(
ctx context.Context,
auth *coreauth.Auth,
req clipexec.Request,
opts clipexec.Options,
) (clipexec.Response, error) {
return clipexec.Response{}, errors.New("token counting not implemented")
}
// statusError implements StatusError for HTTP status codes
type statusError struct {
code int
message string
}
func (e *statusError) Error() string {
return fmt.Sprintf("status %d: %s", e.code, e.message)
}
func (e *statusError) StatusCode() int {
return e.code
}
Registering Executors
Register your executor with the core auth manager:import coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
coreManager := coreauth.NewManager(tokenStore, selector, nil)
coreManager.RegisterExecutor(MyExecutor{})
builder.WithCoreAuthManager(coreManager)
Using Executors
Executors are automatically selected based on the auth provider:// Auth record with matching provider
auth := &coreauth.Auth{
ID: "my-auth-1",
Provider: "myprovider", // Matches executor Identifier()
Attributes: map[string]string{
"api_key": "sk-...",
"endpoint": "https://custom.api.com/v1",
},
}
coreManager.Register(ctx, auth)
MyExecutor.
Custom Translators
Translators convert requests and responses between different API formats (e.g., OpenAI ↔ Custom Provider).Translator Interface
Register translators between format pairs:import sdktr "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
// Define formats
const (
fOpenAI = sdktr.Format("openai.chat")
fMyProv = sdktr.Format("myprov.chat")
)
// Register translator
sdktr.Register(
fOpenAI, // Source format
fMyProv, // Target format
requestTransform,
responseTransform,
)
Request Transformation
requestTransform := func(model string, raw []byte, stream bool) []byte {
// Parse OpenAI format
var openAIReq struct {
Model string `json:"model"`
Messages []map[string]interface{} `json:"messages"`
Stream bool `json:"stream"`
}
json.Unmarshal(raw, &openAIReq)
// Convert to provider format
providerReq := map[string]interface{}{
"model": model,
"prompt": convertMessages(openAIReq.Messages),
"stream": stream,
}
result, _ := json.Marshal(providerReq)
return result
}
Response Transformation
responseTransform := sdktr.ResponseTransform{
// Non-streaming response
NonStream: func(
ctx context.Context,
model string,
originalReq, translatedReq, raw []byte,
param *any,
) string {
// Parse provider response
var provResp struct {
Text string `json:"text"`
}
json.Unmarshal(raw, &provResp)
// Convert to OpenAI format
openAIResp := map[string]interface{}{
"id": "chatcmpl-" + generateID(),
"object": "chat.completion",
"created": time.Now().Unix(),
"model": model,
"choices": []map[string]interface{}{
{
"index": 0,
"message": map[string]interface{}{
"role": "assistant",
"content": provResp.Text,
},
"finish_reason": "stop",
},
},
}
result, _ := json.Marshal(openAIResp)
return string(result)
},
// Streaming response
Stream: func(
ctx context.Context,
model string,
originalReq, translatedReq, raw []byte,
param *any,
) []string {
// Parse streaming chunk
var provChunk struct {
Delta string `json:"delta"`
Done bool `json:"done"`
}
json.Unmarshal(raw, &provChunk)
// Convert to OpenAI SSE format
if provChunk.Done {
return []string{"data: [DONE]\n\n"}
}
openAIChunk := map[string]interface{}{
"id": "chatcmpl-" + generateID(),
"object": "chat.completion.chunk",
"created": time.Now().Unix(),
"model": model,
"choices": []map[string]interface{}{
{
"index": 0,
"delta": map[string]interface{}{
"content": provChunk.Delta,
},
},
},
}
data, _ := json.Marshal(openAIChunk)
return []string{"data: " + string(data) + "\n\n"}
},
}
Complete Translator Example
package main
import (
"context"
"encoding/json"
"time"
sdktr "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
)
const (
fOpenAI = sdktr.Format("openai.chat")
fMyProv = sdktr.Format("myprov.chat")
)
func init() {
sdktr.Register(fOpenAI, fMyProv, transformRequest, transformResponse)
}
func transformRequest(model string, raw []byte, stream bool) []byte {
var req struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
Temperature float64 `json:"temperature,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
}
if err := json.Unmarshal(raw, &req); err != nil {
return raw
}
// Build prompt from messages
var prompt string
for _, msg := range req.Messages {
prompt += msg.Role + ": " + msg.Content + "\n"
}
// Create provider request
provReq := map[string]interface{}{
"model": model,
"prompt": prompt,
"stream": stream,
"temperature": req.Temperature,
}
if req.MaxTokens > 0 {
provReq["max_tokens"] = req.MaxTokens
}
result, _ := json.Marshal(provReq)
return result
}
var transformResponse = sdktr.ResponseTransform{
NonStream: func(ctx context.Context, model string, origReq, transReq, raw []byte, param *any) string {
var provResp struct {
Text string `json:"text"`
FinishReason string `json:"finish_reason"`
}
if err := json.Unmarshal(raw, &provResp); err != nil {
return string(raw)
}
resp := map[string]interface{}{
"id": "chat-" + generateID(),
"object": "chat.completion",
"created": time.Now().Unix(),
"model": model,
"choices": []map[string]interface{}{
{
"index": 0,
"message": map[string]string{
"role": "assistant",
"content": provResp.Text,
},
"finish_reason": provResp.FinishReason,
},
},
}
result, _ := json.Marshal(resp)
return string(result)
},
Stream: func(ctx context.Context, model string, origReq, transReq, raw []byte, param *any) []string {
var chunk struct {
Delta string `json:"delta"`
Done bool `json:"done"`
}
if err := json.Unmarshal(raw, &chunk); err != nil {
return []string{string(raw)}
}
if chunk.Done {
return []string{"data: [DONE]\n\n"}
}
sseChunk := map[string]interface{}{
"id": "chat-" + generateID(),
"object": "chat.completion.chunk",
"created": time.Now().Unix(),
"model": model,
"choices": []map[string]interface{}{
{
"index": 0,
"delta": map[string]string{
"content": chunk.Delta,
},
},
},
}
data, _ := json.Marshal(sseChunk)
return []string{"data: " + string(data) + "\n\n"}
},
}
func generateID() string {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
Next Steps
Access Providers
Implement custom authentication
File Watching
Configuration and auth file watching
Usage Tracking
Monitor API usage
Examples
Browse complete examples