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:
Luis Pater
2026-06-09 13:39:19 +08:00
parent a2db6a6108
commit 693ce1c55a
22 changed files with 1750 additions and 20 deletions

View File

@@ -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

View File

@@ -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
}