mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-08 01:16:47 +08:00
feat(access, pluginhost): add support for exclusive frontend auth providers
- Introduced `FrontendAuthProviderExclusive` capability to restrict authentication to a single selected provider. - Added `SetExclusiveProvider` and `ClearExclusiveProvider` methods for managing exclusive providers in the access registry. - Updated `pluginhost` to prioritize and enforce exclusive providers based on plugin priority and ID. - Enhanced RPC capabilities schema to include `FrontendAuthProviderExclusive` field. - Added example plugin and tests for exclusive frontend auth behavior.
This commit is contained in:
@@ -8,6 +8,7 @@ This directory contains standard dynamic library plugin examples for the CLIProx
|
||||
- `model/`: model capability only.
|
||||
- `auth/`: auth provider capability only.
|
||||
- `frontend-auth/`: frontend auth provider capability only.
|
||||
- `frontend-auth-exclusive/`: frontend auth provider that becomes the only request authentication provider when selected.
|
||||
- `executor/`: executor capability only.
|
||||
- `protocol-format/`: minimal executor focused on input/output format declarations.
|
||||
- `request-translator/`: request translation capability only.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
- `model/`:只演示模型能力。
|
||||
- `auth/`:只演示认证提供方能力。
|
||||
- `frontend-auth/`:只演示前端认证提供方能力。
|
||||
- `frontend-auth-exclusive/`:演示被选中后成为唯一请求认证方式的前端认证提供方。
|
||||
- `executor/`:只演示执行器能力。
|
||||
- `protocol-format/`:使用最小执行器重点演示输入和输出格式声明。
|
||||
- `request-translator/`:只演示请求转换能力。
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Frontend Auth Exclusive Plugin Example
|
||||
|
||||
This example registers a frontend auth provider with `frontend_auth_provider_exclusive: true`.
|
||||
|
||||
When enabled and selected, this provider becomes the only request authentication provider. Built-in config API keys and other frontend auth providers do not authenticate requests while this provider is active.
|
||||
|
||||
The example accepts requests that include:
|
||||
|
||||
```http
|
||||
X-Example-Frontend-Auth: exclusive
|
||||
```
|
||||
|
||||
Build:
|
||||
|
||||
```bash
|
||||
cd examples/plugin/frontend-auth-exclusive/go
|
||||
go build -buildmode=c-shared -o /tmp/cliproxy-frontend-auth-exclusive.dylib .
|
||||
```
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module github.com/router-for-me/CLIProxyAPI/v7/examples/plugin/frontend-auth-exclusive/go
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require github.com/router-for-me/CLIProxyAPI/v7 v7.0.0
|
||||
|
||||
replace github.com/router-for-me/CLIProxyAPI/v7 => ../../../..
|
||||
@@ -0,0 +1,194 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef struct {
|
||||
void* ptr;
|
||||
size_t len;
|
||||
} cliproxy_buffer;
|
||||
|
||||
typedef int (*cliproxy_host_call_fn)(void*, const char*, const uint8_t*, size_t, cliproxy_buffer*);
|
||||
typedef void (*cliproxy_host_free_fn)(void*, size_t);
|
||||
|
||||
typedef struct {
|
||||
uint32_t abi_version;
|
||||
void* host_ctx;
|
||||
cliproxy_host_call_fn call;
|
||||
cliproxy_host_free_fn free_buffer;
|
||||
} cliproxy_host_api;
|
||||
|
||||
typedef int (*cliproxy_plugin_call_fn)(char*, uint8_t*, size_t, cliproxy_buffer*);
|
||||
typedef void (*cliproxy_plugin_free_fn)(void*, size_t);
|
||||
typedef void (*cliproxy_plugin_shutdown_fn)(void);
|
||||
|
||||
typedef struct {
|
||||
uint32_t abi_version;
|
||||
cliproxy_plugin_call_fn call;
|
||||
cliproxy_plugin_free_fn free_buffer;
|
||||
cliproxy_plugin_shutdown_fn shutdown;
|
||||
} cliproxy_plugin_api;
|
||||
|
||||
extern int cliproxyPluginCall(char*, uint8_t*, size_t, cliproxy_buffer*);
|
||||
extern void cliproxyPluginFree(void*, size_t);
|
||||
extern void cliproxyPluginShutdown(void);
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"unsafe"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
|
||||
)
|
||||
|
||||
type envelope struct {
|
||||
OK bool `json:"ok"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Error *envelopeError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type envelopeError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type registration struct {
|
||||
SchemaVersion uint32 `json:"schema_version"`
|
||||
Metadata pluginapi.Metadata `json:"metadata"`
|
||||
Capabilities capabilities `json:"capabilities"`
|
||||
}
|
||||
|
||||
type capabilities struct {
|
||||
FrontendAuthProvider bool `json:"frontend_auth_provider"`
|
||||
FrontendAuthProviderExclusive bool `json:"frontend_auth_provider_exclusive"`
|
||||
}
|
||||
|
||||
type identifierResponse struct {
|
||||
Identifier string `json:"identifier"`
|
||||
}
|
||||
|
||||
func main() {}
|
||||
|
||||
//export cliproxy_plugin_init
|
||||
func cliproxy_plugin_init(host *C.cliproxy_host_api, plugin *C.cliproxy_plugin_api) C.int {
|
||||
_ = host
|
||||
if plugin == nil {
|
||||
return 1
|
||||
}
|
||||
plugin.abi_version = C.uint32_t(pluginabi.ABIVersion)
|
||||
plugin.call = C.cliproxy_plugin_call_fn(C.cliproxyPluginCall)
|
||||
plugin.free_buffer = C.cliproxy_plugin_free_fn(C.cliproxyPluginFree)
|
||||
plugin.shutdown = C.cliproxy_plugin_shutdown_fn(C.cliproxyPluginShutdown)
|
||||
return 0
|
||||
}
|
||||
|
||||
//export cliproxyPluginCall
|
||||
func cliproxyPluginCall(method *C.char, request *C.uint8_t, requestLen C.size_t, response *C.cliproxy_buffer) C.int {
|
||||
if response != nil {
|
||||
response.ptr = nil
|
||||
response.len = 0
|
||||
}
|
||||
if method == nil {
|
||||
writeResponse(response, errorEnvelope("invalid_method", "method is required"))
|
||||
return 1
|
||||
}
|
||||
var requestBytes []byte
|
||||
if request != nil && requestLen > 0 {
|
||||
requestBytes = C.GoBytes(unsafe.Pointer(request), C.int(requestLen))
|
||||
}
|
||||
raw, errHandle := handleMethod(C.GoString(method), requestBytes)
|
||||
if errHandle != nil {
|
||||
writeResponse(response, errorEnvelope("plugin_error", errHandle.Error()))
|
||||
return 1
|
||||
}
|
||||
writeResponse(response, raw)
|
||||
return 0
|
||||
}
|
||||
|
||||
//export cliproxyPluginFree
|
||||
func cliproxyPluginFree(ptr unsafe.Pointer, len C.size_t) {
|
||||
if ptr != nil {
|
||||
C.free(ptr)
|
||||
}
|
||||
_ = len
|
||||
}
|
||||
|
||||
//export cliproxyPluginShutdown
|
||||
func cliproxyPluginShutdown() {}
|
||||
|
||||
func handleMethod(method string, request []byte) ([]byte, error) {
|
||||
switch method {
|
||||
case pluginabi.MethodPluginRegister, pluginabi.MethodPluginReconfigure:
|
||||
return okEnvelope(exampleRegistration())
|
||||
case pluginabi.MethodFrontendAuthIdentifier:
|
||||
return okEnvelope(identifierResponse{Identifier: "example-frontend-auth-exclusive-go"})
|
||||
case pluginabi.MethodFrontendAuthAuthenticate:
|
||||
return authenticate(request)
|
||||
default:
|
||||
return errorEnvelope("unknown_method", "unknown method: "+method), nil
|
||||
}
|
||||
}
|
||||
|
||||
func exampleRegistration() registration {
|
||||
return registration{
|
||||
SchemaVersion: pluginabi.SchemaVersion,
|
||||
Metadata: pluginapi.Metadata{
|
||||
Name: "example-frontend-auth-exclusive-go",
|
||||
Version: "0.1.0",
|
||||
Author: "router-for-me",
|
||||
GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI",
|
||||
Logo: "https://example.invalid/example-frontend-auth-exclusive-go.png",
|
||||
ConfigFields: []pluginapi.ConfigField{},
|
||||
},
|
||||
Capabilities: capabilities{
|
||||
FrontendAuthProvider: true,
|
||||
FrontendAuthProviderExclusive: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func authenticate(request []byte) ([]byte, error) {
|
||||
var req pluginapi.FrontendAuthRequest
|
||||
if errUnmarshal := json.Unmarshal(request, &req); errUnmarshal != nil {
|
||||
return okEnvelope(pluginapi.FrontendAuthResponse{Authenticated: false})
|
||||
}
|
||||
if req.Headers.Get("X-Example-Frontend-Auth") != "exclusive" {
|
||||
return okEnvelope(pluginapi.FrontendAuthResponse{Authenticated: false})
|
||||
}
|
||||
return okEnvelope(pluginapi.FrontendAuthResponse{
|
||||
Authenticated: true,
|
||||
Principal: "example-frontend-auth-exclusive-go",
|
||||
Metadata: map[string]string{
|
||||
"mode": "exclusive",
|
||||
"provider": "example-frontend-auth-exclusive-go",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func okEnvelope(v any) ([]byte, error) {
|
||||
raw, errMarshal := json.Marshal(v)
|
||||
if errMarshal != nil {
|
||||
return nil, errMarshal
|
||||
}
|
||||
return json.Marshal(envelope{OK: true, Result: raw})
|
||||
}
|
||||
|
||||
func errorEnvelope(code, message string) []byte {
|
||||
raw, _ := json.Marshal(envelope{OK: false, Error: &envelopeError{Code: code, Message: message}})
|
||||
return raw
|
||||
}
|
||||
|
||||
func writeResponse(response *C.cliproxy_buffer, raw []byte) {
|
||||
if response == nil || len(raw) == 0 {
|
||||
return
|
||||
}
|
||||
ptr := C.CBytes(raw)
|
||||
if ptr == nil {
|
||||
return
|
||||
}
|
||||
response.ptr = ptr
|
||||
response.len = C.size_t(len(raw))
|
||||
}
|
||||
@@ -1037,7 +1037,14 @@ func (h *Host) RegisterFrontendAuthProviders() {
|
||||
return
|
||||
}
|
||||
|
||||
type exclusiveFrontendAuthCandidate struct {
|
||||
key string
|
||||
pluginID string
|
||||
priority int
|
||||
}
|
||||
|
||||
nextKeys := make(map[string]struct{})
|
||||
var bestExclusive exclusiveFrontendAuthCandidate
|
||||
for _, record := range h.Snapshot().records {
|
||||
provider := record.plugin.Capabilities.FrontendAuthProvider
|
||||
if provider == nil || h.isPluginFused(record.id) {
|
||||
@@ -1054,8 +1061,25 @@ func (h *Host) RegisterFrontendAuthProviders() {
|
||||
}
|
||||
sdkaccess.RegisterProvider(key, adapter)
|
||||
nextKeys[key] = struct{}{}
|
||||
if record.plugin.Capabilities.FrontendAuthProviderExclusive {
|
||||
candidate := exclusiveFrontendAuthCandidate{
|
||||
key: key,
|
||||
pluginID: record.id,
|
||||
priority: record.priority,
|
||||
}
|
||||
if bestExclusive.key == "" ||
|
||||
candidate.priority > bestExclusive.priority ||
|
||||
(candidate.priority == bestExclusive.priority && candidate.pluginID < bestExclusive.pluginID) {
|
||||
bestExclusive = candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bestExclusive.key != "" {
|
||||
sdkaccess.SetExclusiveProvider(bestExclusive.key)
|
||||
} else {
|
||||
sdkaccess.ClearExclusiveProvider()
|
||||
}
|
||||
h.pruneStaleAccessProviders(nextKeys)
|
||||
}
|
||||
|
||||
|
||||
@@ -2051,6 +2051,177 @@ func TestRegisterFrontendAuthProvidersIdentifierPanicFusesPlugin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterFrontendAuthProvidersSelectsHighestPriorityExclusiveProvider(t *testing.T) {
|
||||
lowKey := "plugin:exclusive-low:custom-auth"
|
||||
highKey := "plugin:exclusive-high:custom-auth"
|
||||
normalKey := "plugin:normal-auth:custom-auth"
|
||||
for _, key := range []string{lowKey, highKey, normalKey} {
|
||||
sdkaccess.UnregisterProvider(key)
|
||||
defer sdkaccess.UnregisterProvider(key)
|
||||
}
|
||||
sdkaccess.ClearExclusiveProvider()
|
||||
defer sdkaccess.ClearExclusiveProvider()
|
||||
|
||||
host := newHostWithRecords(
|
||||
capabilityRecord{
|
||||
id: "exclusive-low",
|
||||
priority: 1,
|
||||
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
|
||||
FrontendAuthProvider: frontendAuthProviderFunc{identifier: "custom-auth"},
|
||||
FrontendAuthProviderExclusive: true,
|
||||
}},
|
||||
},
|
||||
capabilityRecord{
|
||||
id: "exclusive-high",
|
||||
priority: 10,
|
||||
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
|
||||
FrontendAuthProvider: frontendAuthProviderFunc{identifier: "custom-auth"},
|
||||
FrontendAuthProviderExclusive: true,
|
||||
}},
|
||||
},
|
||||
capabilityRecord{
|
||||
id: "normal-auth",
|
||||
priority: 20,
|
||||
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
|
||||
FrontendAuthProvider: frontendAuthProviderFunc{identifier: "custom-auth"},
|
||||
}},
|
||||
},
|
||||
)
|
||||
|
||||
host.RegisterFrontendAuthProviders()
|
||||
|
||||
providers := sdkaccess.RegisteredProviders()
|
||||
if len(providers) != 1 {
|
||||
t.Fatalf("RegisteredProviders() len = %d, want 1", len(providers))
|
||||
}
|
||||
if providers[0].Identifier() != highKey {
|
||||
t.Fatalf("exclusive provider = %q, want %q", providers[0].Identifier(), highKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterFrontendAuthProvidersSelectsExclusiveProviderByPluginIDWhenPriorityTies(t *testing.T) {
|
||||
alphaKey := "plugin:alpha-auth:custom-auth"
|
||||
betaKey := "plugin:beta-auth:custom-auth"
|
||||
for _, key := range []string{alphaKey, betaKey} {
|
||||
sdkaccess.UnregisterProvider(key)
|
||||
defer sdkaccess.UnregisterProvider(key)
|
||||
}
|
||||
sdkaccess.ClearExclusiveProvider()
|
||||
defer sdkaccess.ClearExclusiveProvider()
|
||||
|
||||
host := newHostWithRecords(
|
||||
capabilityRecord{
|
||||
id: "beta-auth",
|
||||
priority: 5,
|
||||
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
|
||||
FrontendAuthProvider: frontendAuthProviderFunc{identifier: "custom-auth"},
|
||||
FrontendAuthProviderExclusive: true,
|
||||
}},
|
||||
},
|
||||
capabilityRecord{
|
||||
id: "alpha-auth",
|
||||
priority: 5,
|
||||
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
|
||||
FrontendAuthProvider: frontendAuthProviderFunc{identifier: "custom-auth"},
|
||||
FrontendAuthProviderExclusive: true,
|
||||
}},
|
||||
},
|
||||
)
|
||||
|
||||
host.RegisterFrontendAuthProviders()
|
||||
|
||||
providers := sdkaccess.RegisteredProviders()
|
||||
if len(providers) != 1 {
|
||||
t.Fatalf("RegisteredProviders() len = %d, want 1", len(providers))
|
||||
}
|
||||
if providers[0].Identifier() != alphaKey {
|
||||
t.Fatalf("exclusive provider = %q, want %q", providers[0].Identifier(), alphaKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterFrontendAuthProvidersClearsExclusiveProviderWhenExclusivePluginRemoved(t *testing.T) {
|
||||
exclusiveKey := "plugin:exclusive-auth:custom-auth"
|
||||
normalKey := "plugin:normal-auth:custom-auth"
|
||||
for _, key := range []string{exclusiveKey, normalKey} {
|
||||
sdkaccess.UnregisterProvider(key)
|
||||
defer sdkaccess.UnregisterProvider(key)
|
||||
}
|
||||
sdkaccess.ClearExclusiveProvider()
|
||||
defer sdkaccess.ClearExclusiveProvider()
|
||||
|
||||
host := newHostWithRecords(
|
||||
capabilityRecord{
|
||||
id: "exclusive-auth",
|
||||
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
|
||||
FrontendAuthProvider: frontendAuthProviderFunc{identifier: "custom-auth"},
|
||||
FrontendAuthProviderExclusive: true,
|
||||
}},
|
||||
},
|
||||
capabilityRecord{
|
||||
id: "normal-auth",
|
||||
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
|
||||
FrontendAuthProvider: frontendAuthProviderFunc{identifier: "custom-auth"},
|
||||
}},
|
||||
},
|
||||
)
|
||||
|
||||
host.RegisterFrontendAuthProviders()
|
||||
if got := sdkaccess.RegisteredProviders(); len(got) != 1 || got[0].Identifier() != exclusiveKey {
|
||||
t.Fatalf("exclusive RegisteredProviders() = %#v, want only %q", got, exclusiveKey)
|
||||
}
|
||||
|
||||
host.snapshot.Store(&Snapshot{enabled: true, records: []capabilityRecord{
|
||||
{
|
||||
id: "normal-auth",
|
||||
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
|
||||
FrontendAuthProvider: frontendAuthProviderFunc{identifier: "custom-auth"},
|
||||
}},
|
||||
},
|
||||
}})
|
||||
host.RegisterFrontendAuthProviders()
|
||||
|
||||
providers := sdkaccess.RegisteredProviders()
|
||||
if len(providers) != 1 {
|
||||
t.Fatalf("RegisteredProviders() len = %d, want 1", len(providers))
|
||||
}
|
||||
if providers[0].Identifier() != normalKey {
|
||||
t.Fatalf("restored provider = %q, want %q", providers[0].Identifier(), normalKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterFrontendAuthProvidersIgnoresExclusiveWithoutFrontendAuthProvider(t *testing.T) {
|
||||
normalKey := "plugin:normal-auth:custom-auth"
|
||||
sdkaccess.UnregisterProvider(normalKey)
|
||||
sdkaccess.ClearExclusiveProvider()
|
||||
defer sdkaccess.UnregisterProvider(normalKey)
|
||||
defer sdkaccess.ClearExclusiveProvider()
|
||||
|
||||
host := newHostWithRecords(
|
||||
capabilityRecord{
|
||||
id: "exclusive-without-provider",
|
||||
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
|
||||
FrontendAuthProviderExclusive: true,
|
||||
}},
|
||||
},
|
||||
capabilityRecord{
|
||||
id: "normal-auth",
|
||||
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
|
||||
FrontendAuthProvider: frontendAuthProviderFunc{identifier: "custom-auth"},
|
||||
}},
|
||||
},
|
||||
)
|
||||
|
||||
host.RegisterFrontendAuthProviders()
|
||||
|
||||
providers := sdkaccess.RegisteredProviders()
|
||||
if len(providers) != 1 {
|
||||
t.Fatalf("RegisteredProviders() len = %d, want 1", len(providers))
|
||||
}
|
||||
if providers[0].Identifier() != normalKey {
|
||||
t.Fatalf("provider = %q, want %q", providers[0].Identifier(), normalKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsageAdapterUsesCurrentSnapshotCapability(t *testing.T) {
|
||||
oldCalls := 0
|
||||
newCalls := 0
|
||||
|
||||
@@ -52,9 +52,10 @@ func registerRPCPlugin(ctx context.Context, host *Host, id string, client plugin
|
||||
plugin := pluginapi.Plugin{
|
||||
Metadata: resp.Metadata,
|
||||
Capabilities: pluginapi.Capabilities{
|
||||
ExecutorModelScope: resp.Capabilities.ExecutorModelScope,
|
||||
ExecutorInputFormats: append([]string(nil), resp.Capabilities.ExecutorInputFormats...),
|
||||
ExecutorOutputFormats: append([]string(nil), resp.Capabilities.ExecutorOutputFormats...),
|
||||
FrontendAuthProviderExclusive: resp.Capabilities.FrontendAuthProvider && resp.Capabilities.FrontendAuthProviderExclusive,
|
||||
ExecutorModelScope: resp.Capabilities.ExecutorModelScope,
|
||||
ExecutorInputFormats: append([]string(nil), resp.Capabilities.ExecutorInputFormats...),
|
||||
ExecutorOutputFormats: append([]string(nil), resp.Capabilities.ExecutorOutputFormats...),
|
||||
},
|
||||
}
|
||||
if resp.Capabilities.ModelRegistrar {
|
||||
|
||||
@@ -18,26 +18,27 @@ type rpcRegistration struct {
|
||||
}
|
||||
|
||||
type rpcCapabilities struct {
|
||||
ModelRegistrar bool `json:"model_registrar"`
|
||||
ModelProvider bool `json:"model_provider"`
|
||||
AuthProvider bool `json:"auth_provider"`
|
||||
FrontendAuthProvider bool `json:"frontend_auth_provider"`
|
||||
Executor bool `json:"executor"`
|
||||
ExecutorModelScope pluginapi.ExecutorModelScope `json:"executor_model_scope"`
|
||||
ExecutorInputFormats []string `json:"executor_input_formats,omitempty"`
|
||||
ExecutorOutputFormats []string `json:"executor_output_formats,omitempty"`
|
||||
RequestTranslator bool `json:"request_translator"`
|
||||
RequestNormalizer bool `json:"request_normalizer"`
|
||||
RequestInterceptor bool `json:"request_interceptor"`
|
||||
ResponseTranslator bool `json:"response_translator"`
|
||||
ResponseBeforeTranslator bool `json:"response_before_translator"`
|
||||
ResponseAfterTranslator bool `json:"response_after_translator"`
|
||||
ResponseInterceptor bool `json:"response_interceptor"`
|
||||
StreamChunkInterceptor bool `json:"response_stream_interceptor"`
|
||||
ThinkingApplier bool `json:"thinking_applier"`
|
||||
UsagePlugin bool `json:"usage_plugin"`
|
||||
CommandLinePlugin bool `json:"command_line_plugin"`
|
||||
ManagementAPI bool `json:"management_api"`
|
||||
ModelRegistrar bool `json:"model_registrar"`
|
||||
ModelProvider bool `json:"model_provider"`
|
||||
AuthProvider bool `json:"auth_provider"`
|
||||
FrontendAuthProvider bool `json:"frontend_auth_provider"`
|
||||
FrontendAuthProviderExclusive bool `json:"frontend_auth_provider_exclusive"`
|
||||
Executor bool `json:"executor"`
|
||||
ExecutorModelScope pluginapi.ExecutorModelScope `json:"executor_model_scope"`
|
||||
ExecutorInputFormats []string `json:"executor_input_formats,omitempty"`
|
||||
ExecutorOutputFormats []string `json:"executor_output_formats,omitempty"`
|
||||
RequestTranslator bool `json:"request_translator"`
|
||||
RequestNormalizer bool `json:"request_normalizer"`
|
||||
RequestInterceptor bool `json:"request_interceptor"`
|
||||
ResponseTranslator bool `json:"response_translator"`
|
||||
ResponseBeforeTranslator bool `json:"response_before_translator"`
|
||||
ResponseAfterTranslator bool `json:"response_after_translator"`
|
||||
ResponseInterceptor bool `json:"response_interceptor"`
|
||||
StreamChunkInterceptor bool `json:"response_stream_interceptor"`
|
||||
ThinkingApplier bool `json:"thinking_applier"`
|
||||
UsagePlugin bool `json:"usage_plugin"`
|
||||
CommandLinePlugin bool `json:"command_line_plugin"`
|
||||
ManagementAPI bool `json:"management_api"`
|
||||
}
|
||||
|
||||
type rpcIdentifierResponse struct {
|
||||
@@ -94,26 +95,27 @@ type rpcEmptyResponse struct{}
|
||||
func rpcCapabilitiesFromPlugin(plugin pluginapi.Plugin) rpcCapabilities {
|
||||
caps := plugin.Capabilities
|
||||
return rpcCapabilities{
|
||||
ModelRegistrar: caps.ModelRegistrar != nil,
|
||||
ModelProvider: caps.ModelProvider != nil,
|
||||
AuthProvider: caps.AuthProvider != nil,
|
||||
FrontendAuthProvider: caps.FrontendAuthProvider != nil,
|
||||
Executor: caps.Executor != nil,
|
||||
ExecutorModelScope: normalizedExecutorModelScope(caps),
|
||||
ExecutorInputFormats: append([]string(nil), caps.ExecutorInputFormats...),
|
||||
ExecutorOutputFormats: append([]string(nil), caps.ExecutorOutputFormats...),
|
||||
RequestTranslator: caps.RequestTranslator != nil,
|
||||
RequestNormalizer: caps.RequestNormalizer != nil,
|
||||
RequestInterceptor: caps.RequestInterceptor != nil,
|
||||
ResponseTranslator: caps.ResponseTranslator != nil,
|
||||
ResponseBeforeTranslator: caps.ResponseBeforeTranslator != nil,
|
||||
ResponseAfterTranslator: caps.ResponseAfterTranslator != nil,
|
||||
ResponseInterceptor: caps.ResponseInterceptor != nil,
|
||||
StreamChunkInterceptor: caps.StreamChunkInterceptor != nil,
|
||||
ThinkingApplier: caps.ThinkingApplier != nil,
|
||||
UsagePlugin: caps.UsagePlugin != nil,
|
||||
CommandLinePlugin: caps.CommandLinePlugin != nil,
|
||||
ManagementAPI: caps.ManagementAPI != nil,
|
||||
ModelRegistrar: caps.ModelRegistrar != nil,
|
||||
ModelProvider: caps.ModelProvider != nil,
|
||||
AuthProvider: caps.AuthProvider != nil,
|
||||
FrontendAuthProvider: caps.FrontendAuthProvider != nil,
|
||||
FrontendAuthProviderExclusive: caps.FrontendAuthProvider != nil && caps.FrontendAuthProviderExclusive,
|
||||
Executor: caps.Executor != nil,
|
||||
ExecutorModelScope: normalizedExecutorModelScope(caps),
|
||||
ExecutorInputFormats: append([]string(nil), caps.ExecutorInputFormats...),
|
||||
ExecutorOutputFormats: append([]string(nil), caps.ExecutorOutputFormats...),
|
||||
RequestTranslator: caps.RequestTranslator != nil,
|
||||
RequestNormalizer: caps.RequestNormalizer != nil,
|
||||
RequestInterceptor: caps.RequestInterceptor != nil,
|
||||
ResponseTranslator: caps.ResponseTranslator != nil,
|
||||
ResponseBeforeTranslator: caps.ResponseBeforeTranslator != nil,
|
||||
ResponseAfterTranslator: caps.ResponseAfterTranslator != nil,
|
||||
ResponseInterceptor: caps.ResponseInterceptor != nil,
|
||||
StreamChunkInterceptor: caps.StreamChunkInterceptor != nil,
|
||||
ThinkingApplier: caps.ThinkingApplier != nil,
|
||||
UsagePlugin: caps.UsagePlugin != nil,
|
||||
CommandLinePlugin: caps.CommandLinePlugin != nil,
|
||||
ManagementAPI: caps.ManagementAPI != nil,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package pluginhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
|
||||
)
|
||||
|
||||
func TestRPCCapabilitiesIncludeFrontendAuthProviderExclusive(t *testing.T) {
|
||||
plugin := pluginapi.Plugin{
|
||||
Capabilities: pluginapi.Capabilities{
|
||||
FrontendAuthProvider: frontendAuthProviderFunc{identifier: "exclusive-auth"},
|
||||
FrontendAuthProviderExclusive: true,
|
||||
},
|
||||
}
|
||||
|
||||
caps := rpcCapabilitiesFromPlugin(plugin)
|
||||
if !caps.FrontendAuthProvider {
|
||||
t.Fatal("FrontendAuthProvider = false, want true")
|
||||
}
|
||||
if !caps.FrontendAuthProviderExclusive {
|
||||
t.Fatal("FrontendAuthProviderExclusive = false, want true")
|
||||
}
|
||||
|
||||
raw, errMarshal := json.Marshal(caps)
|
||||
if errMarshal != nil {
|
||||
t.Fatalf("Marshal() error = %v", errMarshal)
|
||||
}
|
||||
if !json.Valid(raw) {
|
||||
t.Fatalf("marshaled capabilities are invalid JSON: %s", raw)
|
||||
}
|
||||
var decoded map[string]any
|
||||
if errUnmarshal := json.Unmarshal(raw, &decoded); errUnmarshal != nil {
|
||||
t.Fatalf("Unmarshal() error = %v", errUnmarshal)
|
||||
}
|
||||
if decoded["frontend_auth_provider_exclusive"] != true {
|
||||
t.Fatalf("frontend_auth_provider_exclusive = %#v, want true", decoded["frontend_auth_provider_exclusive"])
|
||||
}
|
||||
}
|
||||
+25
-3
@@ -21,9 +21,10 @@ type Result struct {
|
||||
}
|
||||
|
||||
var (
|
||||
registryMu sync.RWMutex
|
||||
registry = make(map[string]Provider)
|
||||
order []string
|
||||
registryMu sync.RWMutex
|
||||
registry = make(map[string]Provider)
|
||||
order []string
|
||||
exclusiveProvider string
|
||||
)
|
||||
|
||||
// RegisterProvider registers a pre-built provider instance for a given type identifier.
|
||||
@@ -63,6 +64,21 @@ func UnregisterProvider(typ string) {
|
||||
registryMu.Unlock()
|
||||
}
|
||||
|
||||
// SetExclusiveProvider restricts RegisteredProviders to a single provider key when present.
|
||||
func SetExclusiveProvider(typ string) {
|
||||
normalizedType := strings.TrimSpace(typ)
|
||||
registryMu.Lock()
|
||||
exclusiveProvider = normalizedType
|
||||
registryMu.Unlock()
|
||||
}
|
||||
|
||||
// ClearExclusiveProvider removes any active provider restriction.
|
||||
func ClearExclusiveProvider() {
|
||||
registryMu.Lock()
|
||||
exclusiveProvider = ""
|
||||
registryMu.Unlock()
|
||||
}
|
||||
|
||||
// RegisteredProviders returns the global provider instances in registration order.
|
||||
func RegisteredProviders() []Provider {
|
||||
registryMu.RLock()
|
||||
@@ -70,6 +86,12 @@ func RegisteredProviders() []Provider {
|
||||
registryMu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
if exclusiveProvider != "" {
|
||||
if provider, exists := registry[exclusiveProvider]; exists && provider != nil {
|
||||
registryMu.RUnlock()
|
||||
return []Provider{provider}
|
||||
}
|
||||
}
|
||||
providers := make([]Provider, 0, len(order))
|
||||
for _, providerType := range order {
|
||||
provider, exists := registry[providerType]
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testProvider struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (p testProvider) Identifier() string {
|
||||
return p.id
|
||||
}
|
||||
|
||||
func (p testProvider) Authenticate(context.Context, *http.Request) (*Result, *AuthError) {
|
||||
return &Result{Provider: p.id, Principal: p.id}, nil
|
||||
}
|
||||
|
||||
func TestRegisteredProvidersReturnsOnlyExclusiveProvider(t *testing.T) {
|
||||
UnregisterProvider("test-a")
|
||||
UnregisterProvider("test-b")
|
||||
ClearExclusiveProvider()
|
||||
defer UnregisterProvider("test-a")
|
||||
defer UnregisterProvider("test-b")
|
||||
defer ClearExclusiveProvider()
|
||||
|
||||
RegisterProvider("test-a", testProvider{id: "test-a"})
|
||||
RegisterProvider("test-b", testProvider{id: "test-b"})
|
||||
SetExclusiveProvider("test-b")
|
||||
|
||||
providers := RegisteredProviders()
|
||||
if len(providers) != 1 {
|
||||
t.Fatalf("RegisteredProviders() len = %d, want 1", len(providers))
|
||||
}
|
||||
if providers[0].Identifier() != "test-b" {
|
||||
t.Fatalf("RegisteredProviders()[0] = %q, want test-b", providers[0].Identifier())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisteredProvidersRestoresAllProvidersAfterExclusiveCleared(t *testing.T) {
|
||||
UnregisterProvider("test-a")
|
||||
UnregisterProvider("test-b")
|
||||
ClearExclusiveProvider()
|
||||
defer UnregisterProvider("test-a")
|
||||
defer UnregisterProvider("test-b")
|
||||
defer ClearExclusiveProvider()
|
||||
|
||||
RegisterProvider("test-a", testProvider{id: "test-a"})
|
||||
RegisterProvider("test-b", testProvider{id: "test-b"})
|
||||
SetExclusiveProvider("test-b")
|
||||
ClearExclusiveProvider()
|
||||
|
||||
providers := RegisteredProviders()
|
||||
if len(providers) != 2 {
|
||||
t.Fatalf("RegisteredProviders() len = %d, want 2", len(providers))
|
||||
}
|
||||
if providers[0].Identifier() != "test-a" || providers[1].Identifier() != "test-b" {
|
||||
t.Fatalf("RegisteredProviders() = [%q, %q], want [test-a, test-b]", providers[0].Identifier(), providers[1].Identifier())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisteredProvidersIgnoresStaleExclusiveProvider(t *testing.T) {
|
||||
UnregisterProvider("test-a")
|
||||
UnregisterProvider("missing")
|
||||
ClearExclusiveProvider()
|
||||
defer UnregisterProvider("test-a")
|
||||
defer ClearExclusiveProvider()
|
||||
|
||||
RegisterProvider("test-a", testProvider{id: "test-a"})
|
||||
SetExclusiveProvider("missing")
|
||||
|
||||
providers := RegisteredProviders()
|
||||
if len(providers) != 1 {
|
||||
t.Fatalf("RegisteredProviders() len = %d, want 1", len(providers))
|
||||
}
|
||||
if providers[0].Identifier() != "test-a" {
|
||||
t.Fatalf("RegisteredProviders()[0] = %q, want test-a", providers[0].Identifier())
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,8 @@ type Capabilities struct {
|
||||
AuthProvider AuthProvider
|
||||
// FrontendAuthProvider authenticates frontend requests before proxy handling.
|
||||
FrontendAuthProvider FrontendAuthProvider
|
||||
// FrontendAuthProviderExclusive makes this frontend auth provider the only active request auth provider when selected.
|
||||
FrontendAuthProviderExclusive bool
|
||||
// Executor sends requests to an upstream provider or local backend.
|
||||
Executor ProviderExecutor
|
||||
// ExecutorModelScope declares whether Executor serves static models, OAuth auth models, or both.
|
||||
|
||||
Reference in New Issue
Block a user