mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-03 06:35:00 +08:00
feat(pluginhost, scheduler): introduce Go-based plugin with scheduler capabilities
- Added a Go scheduler plugin demonstrating CLIProxyAPI capabilities, such as `plugin.register`, `plugin.reconfigure`, and `scheduler.pick`. - Implemented methods for plugin configuration, built-in scheduler delegation (`fill-first`, `round-robin`), dynamic candidate selection, and error handling. - Extended `pluginhost` with scheduler handling, candidate normalization, and fallback mechanisms. - Included examples, tests, and detailed documentation for scheduler usage and implementation.
This commit is contained in:
@@ -76,6 +76,8 @@ type Capabilities struct {
|
||||
FrontendAuthProvider FrontendAuthProvider
|
||||
// FrontendAuthProviderExclusive makes this frontend auth provider the only active request auth provider when selected.
|
||||
FrontendAuthProviderExclusive bool
|
||||
// Scheduler chooses an auth candidate before the built-in scheduler runs.
|
||||
Scheduler Scheduler
|
||||
// Executor sends requests to an upstream provider or local backend.
|
||||
Executor ProviderExecutor
|
||||
// ExecutorModelScope declares whether Executor serves static models, OAuth auth models, or both.
|
||||
@@ -441,6 +443,70 @@ type FrontendAuthResponse struct {
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
const (
|
||||
// SchedulerBuiltinRoundRobin delegates auth selection to the built-in round-robin scheduler.
|
||||
SchedulerBuiltinRoundRobin = "round-robin"
|
||||
// SchedulerBuiltinFillFirst delegates auth selection to the built-in fill-first scheduler.
|
||||
SchedulerBuiltinFillFirst = "fill-first"
|
||||
)
|
||||
|
||||
// Scheduler chooses an auth candidate before the built-in scheduler runs.
|
||||
type Scheduler interface {
|
||||
Pick(context.Context, SchedulerPickRequest) (SchedulerPickResponse, error)
|
||||
}
|
||||
|
||||
// SchedulerPickRequest describes the routing context offered to a scheduler plugin.
|
||||
type SchedulerPickRequest struct {
|
||||
// Plugin is the metadata of the plugin being executed.
|
||||
Plugin Metadata
|
||||
// Provider is the primary provider key requested by the route.
|
||||
Provider string
|
||||
// Providers contains every provider key accepted by the route.
|
||||
Providers []string
|
||||
// Model is the requested model identifier.
|
||||
Model string
|
||||
// Stream reports whether the request expects streaming output.
|
||||
Stream bool
|
||||
// Options contains request-scoped scheduler inputs.
|
||||
Options SchedulerOptions
|
||||
// Candidates contains auth records available for selection.
|
||||
Candidates []SchedulerAuthCandidate
|
||||
}
|
||||
|
||||
// SchedulerOptions carries request-scoped scheduler inputs.
|
||||
type SchedulerOptions struct {
|
||||
// Headers contains request headers relevant to scheduling.
|
||||
Headers map[string][]string
|
||||
// Metadata carries host-provided scheduler context.
|
||||
Metadata map[string]any
|
||||
}
|
||||
|
||||
// SchedulerAuthCandidate describes one auth candidate available to a scheduler.
|
||||
type SchedulerAuthCandidate struct {
|
||||
// ID identifies the auth record.
|
||||
ID string
|
||||
// Provider identifies the auth provider.
|
||||
Provider string
|
||||
// Priority is the host priority assigned to the auth record.
|
||||
Priority int
|
||||
// Status is the current host-visible auth status.
|
||||
Status string
|
||||
// Attributes contains immutable routing and provider attributes.
|
||||
Attributes map[string]string
|
||||
// Metadata contains mutable host-managed auth metadata.
|
||||
Metadata map[string]any
|
||||
}
|
||||
|
||||
// SchedulerPickResponse returns a scheduler plugin routing decision.
|
||||
type SchedulerPickResponse struct {
|
||||
// AuthID identifies the selected auth record.
|
||||
AuthID string
|
||||
// DelegateBuiltin asks the host to use a named built-in scheduler.
|
||||
DelegateBuiltin string
|
||||
// Handled reports whether the plugin made a scheduling decision.
|
||||
Handled bool
|
||||
}
|
||||
|
||||
// ProviderExecutor handles model execution, streaming, HTTP bridging, and token counting.
|
||||
type ProviderExecutor interface {
|
||||
Identifier() string
|
||||
|
||||
@@ -13,6 +13,7 @@ var _ ModelRegistrar = (*compileTimePlugin)(nil)
|
||||
var _ ModelProvider = (*compileTimePlugin)(nil)
|
||||
var _ AuthProvider = (*compileTimePlugin)(nil)
|
||||
var _ FrontendAuthProvider = (*compileTimePlugin)(nil)
|
||||
var _ Scheduler = (*compileTimePlugin)(nil)
|
||||
var _ ProviderExecutor = (*compileTimePlugin)(nil)
|
||||
var _ HostHTTPClient = (*compileTimePlugin)(nil)
|
||||
var _ RequestTranslator = (*compileTimePlugin)(nil)
|
||||
@@ -113,6 +114,71 @@ func TestHostInjectedHTTPClientIsNotEncodedInPluginJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchedulerTypesExposeRoutingFields(t *testing.T) {
|
||||
request := SchedulerPickRequest{
|
||||
Plugin: Metadata{Name: "scheduler-plugin"},
|
||||
Provider: "openai",
|
||||
Providers: []string{"openai", "gemini"},
|
||||
Model: "gpt-test",
|
||||
Stream: true,
|
||||
Options: SchedulerOptions{
|
||||
Headers: map[string][]string{"X-Test": []string{"1"}},
|
||||
Metadata: map[string]any{"tenant": "demo"},
|
||||
},
|
||||
Candidates: []SchedulerAuthCandidate{{
|
||||
ID: "auth-1",
|
||||
Provider: "openai",
|
||||
Priority: 10,
|
||||
Status: "ready",
|
||||
Attributes: map[string]string{"region": "us"},
|
||||
Metadata: map[string]any{"load": float64(0.5)},
|
||||
}},
|
||||
}
|
||||
response := SchedulerPickResponse{
|
||||
AuthID: request.Candidates[0].ID,
|
||||
DelegateBuiltin: SchedulerBuiltinRoundRobin,
|
||||
Handled: true,
|
||||
}
|
||||
|
||||
if request.Plugin.Name != "scheduler-plugin" {
|
||||
t.Fatalf("Plugin.Name = %q", request.Plugin.Name)
|
||||
}
|
||||
if request.Provider != "openai" {
|
||||
t.Fatalf("Provider = %q", request.Provider)
|
||||
}
|
||||
if len(request.Providers) != 2 || request.Providers[1] != "gemini" {
|
||||
t.Fatalf("Providers = %#v", request.Providers)
|
||||
}
|
||||
if request.Model != "gpt-test" {
|
||||
t.Fatalf("Model = %q", request.Model)
|
||||
}
|
||||
if !request.Stream {
|
||||
t.Fatalf("Stream = %v", request.Stream)
|
||||
}
|
||||
if got := request.Options.Headers["X-Test"]; len(got) != 1 || got[0] != "1" {
|
||||
t.Fatalf("Options.Headers = %#v", request.Options.Headers)
|
||||
}
|
||||
if request.Options.Metadata["tenant"] != "demo" {
|
||||
t.Fatalf("Options.Metadata = %#v", request.Options.Metadata)
|
||||
}
|
||||
if len(request.Candidates) != 1 {
|
||||
t.Fatalf("Candidates = %#v", request.Candidates)
|
||||
}
|
||||
candidate := request.Candidates[0]
|
||||
if candidate.ID != "auth-1" || candidate.Provider != "openai" || candidate.Priority != 10 || candidate.Status != "ready" {
|
||||
t.Fatalf("Candidate = %#v", candidate)
|
||||
}
|
||||
if candidate.Attributes["region"] != "us" {
|
||||
t.Fatalf("Candidate.Attributes = %#v", candidate.Attributes)
|
||||
}
|
||||
if candidate.Metadata["load"] != float64(0.5) {
|
||||
t.Fatalf("Candidate.Metadata = %#v", candidate.Metadata)
|
||||
}
|
||||
if response.AuthID != "auth-1" || response.DelegateBuiltin != SchedulerBuiltinRoundRobin || !response.Handled {
|
||||
t.Fatalf("SchedulerPickResponse = %#v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func (compileTimePlugin) RegisterModels(context.Context, ModelRegistrationRequest) (ModelRegistrationResponse, error) {
|
||||
return ModelRegistrationResponse{}, nil
|
||||
}
|
||||
@@ -147,6 +213,10 @@ func (compileTimePlugin) Authenticate(context.Context, FrontendAuthRequest) (Fro
|
||||
return FrontendAuthResponse{}, nil
|
||||
}
|
||||
|
||||
func (compileTimePlugin) Pick(context.Context, SchedulerPickRequest) (SchedulerPickResponse, error) {
|
||||
return SchedulerPickResponse{}, nil
|
||||
}
|
||||
|
||||
func (compileTimePlugin) Execute(context.Context, ExecutorRequest) (ExecutorResponse, error) {
|
||||
return ExecutorResponse{}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user