Something went wrong. Try again.
sloproxy
Something went wrong. Try again.
21 kB · 557 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558package pluginapi
import ( "context" "encoding/json" "net/http" "net/url" "strings" "testing")
type compileTimePlugin struct{}
var _ ModelRegistrar = (*compileTimePlugin)(nil)var _ ModelProvider = (*compileTimePlugin)(nil)var _ AuthProvider = (*compileTimePlugin)(nil)var _ FrontendAuthProvider = (*compileTimePlugin)(nil)var _ Scheduler = (*compileTimePlugin)(nil)var _ ModelRouter = (*compileTimePlugin)(nil)var _ ProviderExecutor = (*compileTimePlugin)(nil)var _ HostHTTPClient = (*compileTimePlugin)(nil)var _ RequestTranslator = (*compileTimePlugin)(nil)var _ RequestNormalizer = (*compileTimePlugin)(nil)var _ ResponseTranslator = (*compileTimePlugin)(nil)var _ ResponseNormalizer = (*compileTimePlugin)(nil)var _ RequestInterceptor = (*compileTimePlugin)(nil)var _ RequestLifecyclePlugin = (*compileTimePlugin)(nil)var _ ResponseInterceptor = (*compileTimePlugin)(nil)var _ StreamChunkInterceptor = (*compileTimePlugin)(nil)var _ WebSocketResponseObserver = (*compileTimePlugin)(nil)var _ ThinkingApplier = (*compileTimePlugin)(nil)var _ UsagePlugin = (*compileTimePlugin)(nil)var _ CommandLinePlugin = (*compileTimePlugin)(nil)var _ ManagementAPI = (*compileTimePlugin)(nil)var _ ManagementHandler = (*compileTimePlugin)(nil)
func TestMetadataConfigFieldsExposePluginSchema(t *testing.T) { meta := Metadata{ Name: "example", Version: "1.0.0", Author: "test", GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI", Logo: "https://example.com/logo.svg", ConfigFields: []ConfigField{{ Name: "mode", Type: ConfigFieldTypeEnum, EnumValues: []string{"safe", "fast"}, Description: "Execution mode.", }}, } if meta.Logo == "" || len(meta.ConfigFields) != 1 { t.Fatalf("metadata missing logo or config fields: %#v", meta) }}
func TestAuthParseResponseSupportsMultipleAuths(t *testing.T) { resp := AuthParseResponse{ Handled: true, Auth: AuthData{ Provider: "gemini-cli", ID: "primary.json", }, Auths: []AuthData{ {Provider: "gemini-cli", ID: "primary.json"}, {Provider: "gemini-cli", ID: "primary-project-a.json"}, }, }
raw, errMarshal := json.Marshal(resp) if errMarshal != nil { t.Fatalf("Marshal() error = %v", errMarshal) } var decoded AuthParseResponse if errUnmarshal := json.Unmarshal(raw, &decoded); errUnmarshal != nil { t.Fatalf("Unmarshal() error = %v", errUnmarshal) } if !decoded.Handled || len(decoded.Auths) != 2 || decoded.Auths[1].ID != "primary-project-a.json" { t.Fatalf("decoded response = %#v, want two auths", decoded) } if decoded.Auth.ID != "primary.json" { t.Fatalf("decoded Auth.ID = %q, want primary.json", decoded.Auth.ID) }}
func TestAuthLoginPollResponseSupportsMultipleAuths(t *testing.T) { resp := AuthLoginPollResponse{ Status: AuthLoginStatusSuccess, Auth: AuthData{ Provider: "gemini-cli", ID: "primary.json", }, Auths: []AuthData{ {Provider: "gemini-cli", ID: "primary.json"}, {Provider: "gemini-cli", ID: "primary-project-a.json"}, }, }
raw, errMarshal := json.Marshal(resp) if errMarshal != nil { t.Fatalf("Marshal() error = %v", errMarshal) } var decoded AuthLoginPollResponse if errUnmarshal := json.Unmarshal(raw, &decoded); errUnmarshal != nil { t.Fatalf("Unmarshal() error = %v", errUnmarshal) } if decoded.Status != AuthLoginStatusSuccess || len(decoded.Auths) != 2 { t.Fatalf("decoded response = %#v, want success with two auths", decoded) }}
func TestResourceRouteMenuFieldsExposeManagementUIHints(t *testing.T) { route := ResourceRoute{ Path: "/status", Menu: "Example Status", Description: "Shows example plugin status.", Handler: compileTimePlugin{}, } if route.Menu == "" || route.Description == "" { t.Fatalf("resource route missing menu fields: %#v", route) }}
func TestHostInjectedHTTPClientIsNotEncodedInPluginJSON(t *testing.T) { requests := []struct { name string req any dst any }{ { name: "auth login start", req: AuthLoginStartRequest{Provider: "plugin-example", HTTPClient: compileTimePlugin{}}, dst: &AuthLoginStartRequest{}, }, { name: "auth login poll", req: AuthLoginPollRequest{Provider: "plugin-example", HTTPClient: compileTimePlugin{}}, dst: &AuthLoginPollRequest{}, }, { name: "auth refresh", req: AuthRefreshRequest{AuthID: "auth-1", HTTPClient: compileTimePlugin{}}, dst: &AuthRefreshRequest{}, }, { name: "auth model", req: AuthModelRequest{AuthID: "auth-1", HTTPClient: compileTimePlugin{}}, dst: &AuthModelRequest{}, }, { name: "executor request", req: ExecutorRequest{Model: "model-1", HTTPClient: compileTimePlugin{}}, dst: &ExecutorRequest{}, }, { name: "executor http request", req: ExecutorHTTPRequest{AuthID: "auth-1", HTTPClient: compileTimePlugin{}}, dst: &ExecutorHTTPRequest{}, }, }
for _, tt := range requests { raw, errMarshal := json.Marshal(tt.req) if errMarshal != nil { t.Fatalf("%s marshal error = %v", tt.name, errMarshal) } if strings.Contains(string(raw), "HTTPClient") { t.Fatalf("%s JSON contains host HTTPClient: %s", tt.name, raw) } withLegacyHTTPClient := append(raw[:len(raw)-1], []byte(`,"HTTPClient":{}}`)...) if errUnmarshal := json.Unmarshal(withLegacyHTTPClient, tt.dst); errUnmarshal != nil { t.Fatalf("%s unmarshal with legacy HTTPClient object error = %v", tt.name, errUnmarshal) } }}
func TestHostModelTypesPreserveFields(t *testing.T) { request := HostModelExecutionRequest{ EntryProtocol: "openai", ExitProtocol: "claude", Model: "gpt-test", Stream: true, Body: []byte(`{"input":"hello"}`), Headers: http.Header{"X-Test": []string{"one", "two"}}, Query: url.Values{"alt": []string{"beta"}}, Alt: "chat", } rawRequest, errMarshalRequest := json.Marshal(request) if errMarshalRequest != nil { t.Fatalf("marshal HostModelExecutionRequest: %v", errMarshalRequest) } requestJSON := string(rawRequest) for _, field := range []string{"entry_protocol", "exit_protocol", "model", "stream", "body", "headers", "query", "alt"} { if !strings.Contains(requestJSON, `"`+field+`"`) { t.Fatalf("HostModelExecutionRequest JSON missing field %q: %s", field, requestJSON) } } var decodedRequest HostModelExecutionRequest if errUnmarshalRequest := json.Unmarshal(rawRequest, &decodedRequest); errUnmarshalRequest != nil { t.Fatalf("unmarshal HostModelExecutionRequest: %v", errUnmarshalRequest) } if decodedRequest.EntryProtocol != request.EntryProtocol || decodedRequest.ExitProtocol != request.ExitProtocol || decodedRequest.Model != request.Model || decodedRequest.Stream != request.Stream || string(decodedRequest.Body) != string(request.Body) || decodedRequest.Headers.Get("X-Test") != "one" || decodedRequest.Query.Get("alt") != "beta" || decodedRequest.Alt != request.Alt { t.Fatalf("HostModelExecutionRequest round trip = %#v", decodedRequest) } if got := decodedRequest.Headers.Values("X-Test"); len(got) != 2 || got[1] != "two" { t.Fatalf("HostModelExecutionRequest headers = %#v", decodedRequest.Headers) }
response := HostModelExecutionResponse{ StatusCode: http.StatusAccepted, Headers: http.Header{"Content-Type": []string{"application/json"}}, Body: []byte(`{"ok":true}`), } rawResponse, errMarshalResponse := json.Marshal(response) if errMarshalResponse != nil { t.Fatalf("marshal HostModelExecutionResponse: %v", errMarshalResponse) } responseJSON := string(rawResponse) for _, field := range []string{"status_code", "headers", "body"} { if !strings.Contains(responseJSON, `"`+field+`"`) { t.Fatalf("HostModelExecutionResponse JSON missing field %q: %s", field, responseJSON) } } var decodedResponse HostModelExecutionResponse if errUnmarshalResponse := json.Unmarshal(rawResponse, &decodedResponse); errUnmarshalResponse != nil { t.Fatalf("unmarshal HostModelExecutionResponse: %v", errUnmarshalResponse) } if decodedResponse.StatusCode != response.StatusCode || decodedResponse.Headers.Get("Content-Type") != "application/json" || string(decodedResponse.Body) != string(response.Body) { t.Fatalf("HostModelExecutionResponse round trip = %#v", decodedResponse) }
streamResponse := HostModelStreamResponse{ StatusCode: http.StatusOK, Headers: http.Header{"Content-Type": []string{"text/event-stream"}}, StreamID: "stream-1", } rawStreamResponse, errMarshalStreamResponse := json.Marshal(streamResponse) if errMarshalStreamResponse != nil { t.Fatalf("marshal HostModelStreamResponse: %v", errMarshalStreamResponse) } streamResponseJSON := string(rawStreamResponse) for _, field := range []string{"status_code", "headers", "stream_id"} { if !strings.Contains(streamResponseJSON, `"`+field+`"`) { t.Fatalf("HostModelStreamResponse JSON missing field %q: %s", field, streamResponseJSON) } } var decodedStreamResponse HostModelStreamResponse if errUnmarshalStreamResponse := json.Unmarshal(rawStreamResponse, &decodedStreamResponse); errUnmarshalStreamResponse != nil { t.Fatalf("unmarshal HostModelStreamResponse: %v", errUnmarshalStreamResponse) } if decodedStreamResponse.StatusCode != streamResponse.StatusCode || decodedStreamResponse.Headers.Get("Content-Type") != "text/event-stream" || decodedStreamResponse.StreamID != streamResponse.StreamID { t.Fatalf("HostModelStreamResponse round trip = %#v", decodedStreamResponse) }
readRequest := HostModelStreamReadRequest{StreamID: "stream-1"} rawReadRequest, errMarshalReadRequest := json.Marshal(readRequest) if errMarshalReadRequest != nil { t.Fatalf("marshal HostModelStreamReadRequest: %v", errMarshalReadRequest) } if !strings.Contains(string(rawReadRequest), `"stream_id"`) { t.Fatalf("HostModelStreamReadRequest JSON missing stream_id: %s", rawReadRequest) } var decodedReadRequest HostModelStreamReadRequest if errUnmarshalReadRequest := json.Unmarshal(rawReadRequest, &decodedReadRequest); errUnmarshalReadRequest != nil { t.Fatalf("unmarshal HostModelStreamReadRequest: %v", errUnmarshalReadRequest) } if decodedReadRequest.StreamID != readRequest.StreamID { t.Fatalf("HostModelStreamReadRequest round trip = %#v", decodedReadRequest) }
readResponse := HostModelStreamReadResponse{ Payload: []byte("data: test\n\n"), Error: "temporary stream error", Done: true, } rawReadResponse, errMarshalReadResponse := json.Marshal(readResponse) if errMarshalReadResponse != nil { t.Fatalf("marshal HostModelStreamReadResponse: %v", errMarshalReadResponse) } readResponseJSON := string(rawReadResponse) for _, field := range []string{"payload", "error", "done"} { if !strings.Contains(readResponseJSON, `"`+field+`"`) { t.Fatalf("HostModelStreamReadResponse JSON missing field %q: %s", field, readResponseJSON) } } var decodedReadResponse HostModelStreamReadResponse if errUnmarshalReadResponse := json.Unmarshal(rawReadResponse, &decodedReadResponse); errUnmarshalReadResponse != nil { t.Fatalf("unmarshal HostModelStreamReadResponse: %v", errUnmarshalReadResponse) } if string(decodedReadResponse.Payload) != string(readResponse.Payload) || decodedReadResponse.Error != readResponse.Error || decodedReadResponse.Done != readResponse.Done { t.Fatalf("HostModelStreamReadResponse round trip = %#v", decodedReadResponse) }
closeRequest := HostModelStreamCloseRequest{StreamID: "stream-1"} rawCloseRequest, errMarshalCloseRequest := json.Marshal(closeRequest) if errMarshalCloseRequest != nil { t.Fatalf("marshal HostModelStreamCloseRequest: %v", errMarshalCloseRequest) } if !strings.Contains(string(rawCloseRequest), `"stream_id"`) { t.Fatalf("HostModelStreamCloseRequest JSON missing stream_id: %s", rawCloseRequest) } var decodedCloseRequest HostModelStreamCloseRequest if errUnmarshalCloseRequest := json.Unmarshal(rawCloseRequest, &decodedCloseRequest); errUnmarshalCloseRequest != nil { t.Fatalf("unmarshal HostModelStreamCloseRequest: %v", errUnmarshalCloseRequest) } if decodedCloseRequest.StreamID != closeRequest.StreamID { t.Fatalf("HostModelStreamCloseRequest round trip = %#v", decodedCloseRequest) }}
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 TestModelRouteTypesExposeRoutingFields(t *testing.T) { request := ModelRouteRequest{ Plugin: Metadata{Name: "router-plugin"}, PluginID: "router-plugin-id", SourceFormat: "anthropic", RequestedModel: "claude-sonnet", Stream: true, Headers: http.Header{"X-Test": []string{"1"}}, Query: url.Values{"beta": []string{"true"}}, Body: []byte(`{"model":"claude-sonnet"}`), Metadata: map[string]any{"tenant": "demo"}, } response := ModelRouteResponse{ Handled: true, TargetKind: ModelRouteTargetExecutor, Target: "claude-websearch-plugin", Reason: "typed websearch", }
if request.Plugin.Name != "router-plugin" { t.Fatalf("Plugin.Name = %q", request.Plugin.Name) } if request.PluginID != "router-plugin-id" { t.Fatalf("PluginID = %q", request.PluginID) } if request.SourceFormat != "anthropic" || request.RequestedModel != "claude-sonnet" || !request.Stream { t.Fatalf("request main fields = %#v", request) } if request.Headers.Get("X-Test") != "1" { t.Fatalf("Headers = %#v", request.Headers) } if request.Query.Get("beta") != "true" { t.Fatalf("Query = %#v", request.Query) } if string(request.Body) != `{"model":"claude-sonnet"}` { t.Fatalf("Body = %q", request.Body) } if request.Metadata["tenant"] != "demo" { t.Fatalf("Metadata = %#v", request.Metadata) } if !response.Handled || response.Target != "claude-websearch-plugin" || response.Reason != "typed websearch" { t.Fatalf("ModelRouteResponse = %#v", response) }}
func (compileTimePlugin) RegisterModels(context.Context, ModelRegistrationRequest) (ModelRegistrationResponse, error) { return ModelRegistrationResponse{}, nil}
func (compileTimePlugin) StaticModels(context.Context, StaticModelRequest) (ModelResponse, error) { return ModelResponse{}, nil}
func (compileTimePlugin) ModelsForAuth(context.Context, AuthModelRequest) (ModelResponse, error) { return ModelResponse{}, nil}
func (compileTimePlugin) Identifier() string { return "compile-time" }
func (compileTimePlugin) ParseAuth(context.Context, AuthParseRequest) (AuthParseResponse, error) { return AuthParseResponse{}, nil}
func (compileTimePlugin) StartLogin(context.Context, AuthLoginStartRequest) (AuthLoginStartResponse, error) { return AuthLoginStartResponse{}, nil}
func (compileTimePlugin) PollLogin(context.Context, AuthLoginPollRequest) (AuthLoginPollResponse, error) { return AuthLoginPollResponse{}, nil}
func (compileTimePlugin) RefreshAuth(context.Context, AuthRefreshRequest) (AuthRefreshResponse, error) { return AuthRefreshResponse{}, nil}
func (compileTimePlugin) Authenticate(context.Context, FrontendAuthRequest) (FrontendAuthResponse, error) { return FrontendAuthResponse{}, nil}
func (compileTimePlugin) Pick(context.Context, SchedulerPickRequest) (SchedulerPickResponse, error) { return SchedulerPickResponse{}, nil}
func (compileTimePlugin) RouteModel(context.Context, ModelRouteRequest) (ModelRouteResponse, error) { return ModelRouteResponse{}, nil}
func (compileTimePlugin) Execute(context.Context, ExecutorRequest) (ExecutorResponse, error) { return ExecutorResponse{}, nil}
func (compileTimePlugin) ExecuteStream(context.Context, ExecutorRequest) (ExecutorStreamResponse, error) { return ExecutorStreamResponse{}, nil}
func (compileTimePlugin) CountTokens(context.Context, ExecutorRequest) (ExecutorResponse, error) { return ExecutorResponse{}, nil}
func (compileTimePlugin) HttpRequest(context.Context, ExecutorHTTPRequest) (ExecutorHTTPResponse, error) { return ExecutorHTTPResponse{}, nil}
func (compileTimePlugin) Do(context.Context, HTTPRequest) (HTTPResponse, error) { return HTTPResponse{}, nil}
func (compileTimePlugin) DoStream(context.Context, HTTPRequest) (HTTPStreamResponse, error) { return HTTPStreamResponse{}, nil}
func (compileTimePlugin) TranslateRequest(context.Context, RequestTransformRequest) (PayloadResponse, error) { return PayloadResponse{}, nil}
func (compileTimePlugin) NormalizeRequest(context.Context, RequestTransformRequest) (PayloadResponse, error) { return PayloadResponse{}, nil}
func (compileTimePlugin) TranslateResponse(context.Context, ResponseTransformRequest) (PayloadResponse, error) { return PayloadResponse{}, nil}
func (compileTimePlugin) NormalizeResponse(context.Context, ResponseTransformRequest) (PayloadResponse, error) { return PayloadResponse{}, nil}
func (compileTimePlugin) InterceptRequestBeforeAuth(context.Context, RequestInterceptRequest) (RequestInterceptResponse, error) { return RequestInterceptResponse{}, nil}
func (compileTimePlugin) InterceptRequestAfterAuth(context.Context, RequestInterceptRequest) (RequestInterceptResponse, error) { return RequestInterceptResponse{}, nil}
func (compileTimePlugin) HandleRequestComplete(context.Context, RequestCompletion) error { return nil }
func (compileTimePlugin) InterceptResponse(context.Context, ResponseInterceptRequest) (ResponseInterceptResponse, error) { return ResponseInterceptResponse{}, nil}
func (compileTimePlugin) InterceptStreamChunk(context.Context, StreamChunkInterceptRequest) (StreamChunkInterceptResponse, error) { return StreamChunkInterceptResponse{}, nil}
func (compileTimePlugin) ObserveWebSocketResponseEvent(context.Context, WebSocketResponseEvent) error { return nil}
func (compileTimePlugin) ApplyThinking(context.Context, ThinkingApplyRequest) (PayloadResponse, error) { return PayloadResponse{}, nil}
func (compileTimePlugin) HandleUsage(context.Context, UsageRecord) {}
func (compileTimePlugin) RegisterCommandLine(context.Context, CommandLineRegistrationRequest) (CommandLineRegistrationResponse, error) { return CommandLineRegistrationResponse{}, nil}
func (compileTimePlugin) ExecuteCommandLine(context.Context, CommandLineExecutionRequest) (CommandLineExecutionResponse, error) { return CommandLineExecutionResponse{}, nil}
func (compileTimePlugin) RegisterManagement(context.Context, ManagementRegistrationRequest) (ManagementRegistrationResponse, error) { return ManagementRegistrationResponse{}, nil}
func (compileTimePlugin) HandleManagement(context.Context, ManagementRequest) (ManagementResponse, error) { return ManagementResponse{}, nil}