Skip to main content

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 the Executor 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)
Requests to models from “myprovider” will use 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

Build docs developers (and LLMs) love