package pluginhost import ( "bytes" "context" "encoding/json" "io" "net/http" "reflect" "strings" "testing" sdkaccess "github.com/router-for-me/CLIProxyAPI/v8/sdk/access" coreusage "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/usage" "github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginabi" "github.com/router-for-me/CLIProxyAPI/v8/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"]) } if decoded["frontend_auth_provider_omit_body"] != nil && decoded["frontend_auth_provider_omit_body"] != false { t.Fatalf("frontend_auth_provider_omit_body = %#v, want omitted or false", decoded["frontend_auth_provider_omit_body"]) } if decoded["frontend_auth_omit_body"] != nil && decoded["frontend_auth_omit_body"] != false { t.Fatalf("frontend_auth_omit_body = %#v, want omitted or false", decoded["frontend_auth_omit_body"]) } } func TestRPCCapabilitiesIncludeFrontendAuthProviderOmitBody(t *testing.T) { for _, tc := range []struct { name string caps pluginapi.Capabilities }{ { name: "FrontendAuthProviderOmitBody", caps: pluginapi.Capabilities{ FrontendAuthProvider: frontendAuthProviderFunc{identifier: "omit-auth"}, FrontendAuthProviderOmitBody: true, }, }, { name: "FrontendAuthOmitBody", caps: pluginapi.Capabilities{ FrontendAuthProvider: frontendAuthProviderFunc{identifier: "omit-auth"}, FrontendAuthOmitBody: true, }, }, { name: "method hook", caps: pluginapi.Capabilities{ FrontendAuthProvider: omitBodyFrontendAuthProvider{ frontendAuthProviderFunc: frontendAuthProviderFunc{identifier: "omit-auth"}, }, }, }, } { t.Run(tc.name, func(t *testing.T) { plugin := pluginapi.Plugin{Capabilities: tc.caps} caps := rpcCapabilitiesFromPlugin(plugin) if !caps.FrontendAuthProvider { t.Fatal("FrontendAuthProvider = false, want true") } if !caps.FrontendAuthProviderOmitBody || !caps.FrontendAuthOmitBody { t.Fatalf("omit body flags = provider:%v short:%v, want both true", caps.FrontendAuthProviderOmitBody, caps.FrontendAuthOmitBody) } raw, errMarshal := json.Marshal(caps) if errMarshal != nil { t.Fatalf("Marshal() error = %v", errMarshal) } var decoded map[string]any if errUnmarshal := json.Unmarshal(raw, &decoded); errUnmarshal != nil { t.Fatalf("Unmarshal() error = %v", errUnmarshal) } if decoded["frontend_auth_provider_omit_body"] != true { t.Fatalf("frontend_auth_provider_omit_body = %#v, want true", decoded["frontend_auth_provider_omit_body"]) } if decoded["frontend_auth_omit_body"] != true { t.Fatalf("frontend_auth_omit_body = %#v, want true", decoded["frontend_auth_omit_body"]) } }) } } func TestRPCFrontendAuthOmitsBodyOnTheWireForSchema7(t *testing.T) { for _, tc := range []struct { name string schemaVersion uint32 omitBody bool shortFlagOnly bool wantBodyLen int }{ {name: "schema 7 omit body", schemaVersion: pluginabi.SchemaVersionFrontendAuthOmitBody, omitBody: true, wantBodyLen: 0}, {name: "schema 7 short flag omit body", schemaVersion: pluginabi.SchemaVersionFrontendAuthOmitBody, omitBody: true, shortFlagOnly: true, wantBodyLen: 0}, {name: "schema 7 default includes body", schemaVersion: pluginabi.SchemaVersionFrontendAuthOmitBody, omitBody: false, wantBodyLen: len("request")}, {name: "schema 6 ignores omit body", schemaVersion: 6, omitBody: true, wantBodyLen: len("request")}, } { t.Run(tc.name, func(t *testing.T) { const key = "plugin:auth-wire:quota" sdkaccess.UnregisterProvider(key) defer sdkaccess.UnregisterProvider(key) var wireGot pluginapi.FrontendAuthRequest var wireCalls int plugin := validTestPlugin("auth-wire") plugin.Capabilities.FrontendAuthProvider = frontendAuthProviderFunc{ identifier: "quota", authenticate: func(_ context.Context, req pluginapi.FrontendAuthRequest) (pluginapi.FrontendAuthResponse, error) { wireCalls++ wireGot = req return pluginapi.FrontendAuthResponse{Authenticated: true, Principal: "wire-user"}, nil }, } if tc.omitBody && !tc.shortFlagOnly { plugin.Capabilities.FrontendAuthProviderOmitBody = true } lookup := newTestSymbolLookup(&testPlugin{registerResult: plugin}) lookup.schemaVersion = tc.schemaVersion var client pluginClient = lookup if tc.shortFlagOnly { client = &shortFrontendAuthOmitBodyClient{inner: lookup} } registered, errRegister := registerRPCPlugin(context.Background(), nil, "auth-wire", client, pluginabi.MethodPluginRegister, nil) if errRegister != nil { t.Fatalf("registerRPCPlugin() error = %v", errRegister) } if registered.SchemaVersion != tc.schemaVersion { t.Fatalf("registered SchemaVersion = %d, want %d", registered.SchemaVersion, tc.schemaVersion) } if registered.Capabilities.FrontendAuthProviderOmitBody != tc.omitBody || registered.Capabilities.FrontendAuthOmitBody != tc.omitBody { t.Fatalf("registered omit flags = provider:%v short:%v, want %v", registered.Capabilities.FrontendAuthProviderOmitBody, registered.Capabilities.FrontendAuthOmitBody, tc.omitBody) } host := newHostWithRecords(capabilityRecord{id: "auth-wire", plugin: registered}) host.RegisterFrontendAuthProviders() var provider sdkaccess.Provider for _, candidate := range sdkaccess.RegisteredProviders() { if candidate.Identifier() == key { provider = candidate break } } if provider == nil { t.Fatalf("provider %q was not registered", key) } counter := &countingAuthBodyReader{reader: strings.NewReader("request")} req, errNewReq := http.NewRequest(http.MethodPost, "http://example.test/v1/responses", io.NopCloser(counter)) if errNewReq != nil { t.Fatalf("NewRequest() error = %v", errNewReq) } req.Header.Set("Authorization", "Bearer wire-token") res, authErr := provider.Authenticate(context.Background(), req) if authErr != nil { t.Fatalf("Authenticate() error = %v, want nil", authErr) } if res == nil || res.Principal != "wire-user" { t.Fatalf("Authenticate() result = %#v, want wire-user", res) } if wireCalls != 1 { t.Fatalf("wire calls = %d, want 1", wireCalls) } if len(wireGot.Body) != tc.wantBodyLen { t.Fatalf("wire body len = %d, want %d", len(wireGot.Body), tc.wantBodyLen) } if counter.readBytes != tc.wantBodyLen { t.Fatalf("counter.readBytes = %d, want %d", counter.readBytes, tc.wantBodyLen) } }) } } type shortFrontendAuthOmitBodyClient struct { inner pluginClient } func (c *shortFrontendAuthOmitBodyClient) Call(ctx context.Context, method string, request []byte) ([]byte, error) { if method == pluginabi.MethodPluginRegister || method == pluginabi.MethodPluginReconfigure { if _, errCall := c.inner.Call(ctx, method, request); errCall != nil { return nil, errCall } return []byte(`{"ok":true,"result":{"schema_version":7,"metadata":{"Name":"auth-wire","Version":"1.0.0","Author":"test","GitHubRepository":"https://github.com/router-for-me/CLIProxyAPI"},"capabilities":{"frontend_auth_provider":true,"frontend_auth_omit_body":true}}}`), nil } return c.inner.Call(ctx, method, request) } func (c *shortFrontendAuthOmitBodyClient) Shutdown() { c.inner.Shutdown() } func TestRPCCapabilitiesIncludeScheduler(t *testing.T) { plugin := pluginapi.Plugin{ Capabilities: pluginapi.Capabilities{ Scheduler: schedulerFunc(func(context.Context, pluginapi.SchedulerPickRequest) (pluginapi.SchedulerPickResponse, error) { return pluginapi.SchedulerPickResponse{}, nil }), }, } caps := rpcCapabilitiesFromPlugin(plugin) if !caps.Scheduler { t.Fatal("Scheduler = 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["scheduler"] != true { t.Fatalf("scheduler = %#v, want true", decoded["scheduler"]) } if decoded["scheduler_across_priorities"] != nil && decoded["scheduler_across_priorities"] != false { t.Fatalf("scheduler_across_priorities = %#v, want omitted or false", decoded["scheduler_across_priorities"]) } } func TestRPCCapabilitiesIncludeSchedulerAcrossPriorities(t *testing.T) { plugin := pluginapi.Plugin{ Capabilities: pluginapi.Capabilities{ Scheduler: schedulerFunc(func(context.Context, pluginapi.SchedulerPickRequest) (pluginapi.SchedulerPickResponse, error) { return pluginapi.SchedulerPickResponse{}, nil }), SchedulerAcrossPriorities: true, }, } caps := rpcCapabilitiesFromPlugin(plugin) if !caps.Scheduler { t.Fatal("Scheduler = false, want true") } if !caps.SchedulerAcrossPriorities { t.Fatal("SchedulerAcrossPriorities = false, want true") } raw, errMarshal := json.Marshal(caps) if errMarshal != nil { t.Fatalf("Marshal() error = %v", errMarshal) } var decoded map[string]any if errUnmarshal := json.Unmarshal(raw, &decoded); errUnmarshal != nil { t.Fatalf("Unmarshal() error = %v", errUnmarshal) } if decoded["scheduler_across_priorities"] != true { t.Fatalf("scheduler_across_priorities = %#v, want true", decoded["scheduler_across_priorities"]) } } type testAcrossPrioritiesSchedulerFunc struct { schedulerFunc } func (testAcrossPrioritiesSchedulerFunc) SchedulerWantsAcrossPriorities() bool { return true } func TestRPCCapabilitiesIncludeSchedulerAcrossPrioritiesViaMethod(t *testing.T) { plugin := pluginapi.Plugin{ Capabilities: pluginapi.Capabilities{ Scheduler: testAcrossPrioritiesSchedulerFunc{ schedulerFunc: schedulerFunc(func(context.Context, pluginapi.SchedulerPickRequest) (pluginapi.SchedulerPickResponse, error) { return pluginapi.SchedulerPickResponse{}, nil }), }, }, } caps := rpcCapabilitiesFromPlugin(plugin) if !caps.SchedulerAcrossPriorities { t.Fatal("SchedulerAcrossPriorities = false, want true") } } func TestRPCCapabilitiesIncludeModelRouter(t *testing.T) { plugin := pluginapi.Plugin{ Capabilities: pluginapi.Capabilities{ ModelRouter: modelRouterFunc(func(context.Context, pluginapi.ModelRouteRequest) (pluginapi.ModelRouteResponse, error) { return pluginapi.ModelRouteResponse{}, nil }), }, } caps := rpcCapabilitiesFromPlugin(plugin) if !caps.ModelRouter { t.Fatal("ModelRouter = 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["model_router"] != true { t.Fatalf("model_router = %#v, want true", decoded["model_router"]) } } func TestRPCCapabilitiesIncludeRequestInterceptorOmitBody(t *testing.T) { plugin := pluginapi.Plugin{ Capabilities: pluginapi.Capabilities{ RequestInterceptor: requestInterceptorFunc(func(context.Context, pluginapi.RequestInterceptRequest) (pluginapi.RequestInterceptResponse, error) { return pluginapi.RequestInterceptResponse{}, nil }), RequestInterceptorOmitBodyBefore: true, RequestInterceptorOmitBodyAfter: true, }, } caps := rpcCapabilitiesFromPlugin(plugin) if !caps.RequestInterceptor { t.Fatal("RequestInterceptor = false, want true") } if !caps.RequestInterceptorOmitBodyBefore || !caps.RequestInterceptorOmitBodyAfter { t.Fatal("RequestInterceptorOmitBody flags not propagated") } raw, errMarshal := json.Marshal(caps) if errMarshal != nil { t.Fatalf("Marshal() error = %v", errMarshal) } var decoded map[string]any if errUnmarshal := json.Unmarshal(raw, &decoded); errUnmarshal != nil { t.Fatalf("Unmarshal() error = %v", errUnmarshal) } if decoded["request_interceptor_omit_body_before"] != true { t.Fatalf("request_interceptor_omit_body_before = %#v, want true", decoded["request_interceptor_omit_body_before"]) } if decoded["request_interceptor_omit_body_after"] != true { t.Fatalf("request_interceptor_omit_body_after = %#v, want true", decoded["request_interceptor_omit_body_after"]) } } func TestRPCRequestInterceptorOmitsBodyOnTheWireForSchema7(t *testing.T) { for _, tc := range []struct { name string omitBefore bool omitAfter bool wantBefore int wantAfter int }{ {name: "both phases", omitBefore: true, omitAfter: true, wantBefore: 0, wantAfter: 0}, {name: "before only", omitBefore: true, wantBefore: 0, wantAfter: len("request")}, {name: "after only", omitAfter: true, wantBefore: len("request"), wantAfter: 0}, } { t.Run(tc.name, func(t *testing.T) { recorder := &phaseRecordingInterceptor{} plugin := validTestPlugin("omit-wire") plugin.Capabilities.RequestInterceptor = recorder plugin.Capabilities.RequestInterceptorOmitBodyBefore = tc.omitBefore plugin.Capabilities.RequestInterceptorOmitBodyAfter = tc.omitAfter lookup := newTestSymbolLookup(&testPlugin{registerResult: plugin}) lookup.schemaVersion = pluginabi.SchemaVersionRequestInterceptorOmitBody registered, errRegister := registerRPCPlugin(context.Background(), nil, "omit-wire", lookup, pluginabi.MethodPluginRegister, nil) if errRegister != nil { t.Fatalf("registerRPCPlugin() error = %v", errRegister) } if registered.SchemaVersion != pluginabi.SchemaVersionRequestInterceptorOmitBody { t.Fatalf("registered SchemaVersion = %d, want %d", registered.SchemaVersion, pluginabi.SchemaVersionRequestInterceptorOmitBody) } if registered.Capabilities.RequestInterceptorOmitBodyBefore != tc.omitBefore || registered.Capabilities.RequestInterceptorOmitBodyAfter != tc.omitAfter { t.Fatalf("registered omit flags = before:%v after:%v, want before:%v after:%v", registered.Capabilities.RequestInterceptorOmitBodyBefore, registered.Capabilities.RequestInterceptorOmitBodyAfter, tc.omitBefore, tc.omitAfter) } host := newHostWithRecords(capabilityRecord{id: "omit-wire", plugin: registered}) _ = host.InterceptRequestBeforeAuth(context.Background(), pluginapi.RequestInterceptRequest{RequestID: "req-before", Body: []byte("request")}) if recorder.beforeCalls != 1 { t.Fatalf("wire before calls = %d, want 1", recorder.beforeCalls) } if recorder.beforeGot.RequestID != "req-before" { t.Fatalf("wire before RequestID = %q, want req-before", recorder.beforeGot.RequestID) } if len(recorder.beforeGot.Body) != tc.wantBefore { t.Fatalf("wire before body len = %d, want %d", len(recorder.beforeGot.Body), tc.wantBefore) } _ = host.InterceptRequestAfterAuth(context.Background(), pluginapi.RequestInterceptRequest{RequestID: "req-after", Body: []byte("request")}) if recorder.afterCalls != 1 { t.Fatalf("wire after calls = %d, want 1", recorder.afterCalls) } if recorder.afterGot.RequestID != "req-after" { t.Fatalf("wire after RequestID = %q, want req-after", recorder.afterGot.RequestID) } if len(recorder.afterGot.Body) != tc.wantAfter { t.Fatalf("wire after body len = %d, want %d", len(recorder.afterGot.Body), tc.wantAfter) } }) } } type phaseRecordingInterceptor struct { beforeGot pluginapi.RequestInterceptRequest afterGot pluginapi.RequestInterceptRequest beforeCalls int afterCalls int } func (p *phaseRecordingInterceptor) InterceptRequestBeforeAuth(_ context.Context, req pluginapi.RequestInterceptRequest) (pluginapi.RequestInterceptResponse, error) { p.beforeCalls++ p.beforeGot = req return pluginapi.RequestInterceptResponse{}, nil } func (p *phaseRecordingInterceptor) InterceptRequestAfterAuth(_ context.Context, req pluginapi.RequestInterceptRequest) (pluginapi.RequestInterceptResponse, error) { p.afterCalls++ p.afterGot = req return pluginapi.RequestInterceptResponse{}, nil } func TestRPCCapabilitiesIncludeQuotaProvider(t *testing.T) { plugin := pluginapi.Plugin{ Capabilities: pluginapi.Capabilities{ QuotaProvider: &testQuotaProvider{identifier: "quota-test"}, }, } caps := rpcCapabilitiesFromPlugin(plugin) if !caps.QuotaProvider { t.Fatal("QuotaProvider = 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["quota_provider"] != true { t.Fatalf("quota_provider = %#v, want true", decoded["quota_provider"]) } } func TestRegisterRPCPluginSendsHostSchemaVersion(t *testing.T) { lookup := newTestSymbolLookup(&testPlugin{ registerResult: validTestPlugin("schema"), }) registered, errRegister := registerRPCPlugin(context.Background(), nil, "schema", lookup, pluginabi.MethodPluginRegister, []byte("mode: test")) if errRegister != nil { t.Fatalf("registerRPCPlugin() error = %v", errRegister) } if lookup.lastLifecycle.SchemaVersion != pluginabi.SchemaVersion { t.Fatalf("lifecycle schema_version = %d, want %d", lookup.lastLifecycle.SchemaVersion, pluginabi.SchemaVersion) } if !lookup.lastLifecycle.MultiAuthProviders { t.Fatal("lifecycle multi_auth_providers = false, want true") } if registered.SchemaVersion != pluginabi.SchemaVersion { t.Fatalf("registered SchemaVersion = %d, want %d", registered.SchemaVersion, pluginabi.SchemaVersion) } if string(lookup.lastLifecycle.ConfigYAML) != "mode: test" { t.Fatalf("lifecycle config = %q, want input config", lookup.lastLifecycle.ConfigYAML) } } func TestRPCAuthIdentifierEntriesAndMetadataFallback(t *testing.T) { lookup := newTestSymbolLookup(&testPlugin{ registerResult: pluginapi.Plugin{ Metadata: pluginapi.Metadata{ Name: "OMC ZCode OAuth", Version: "1.0.0", Author: "test", GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI", Logo: "https://default.example/zcode.svg", }, Capabilities: pluginapi.Capabilities{ AuthProvider: fakeAuthProvider{ identifier: " ZCode-ZAI ", entries: []pluginapi.AuthProviderEntry{ {Identifier: " ZCode-ZAI ", Name: "Z.ai", Logo: "https://chat.z.ai/logo.svg"}, {Identifier: "zcode-bigmodel", Name: "", Logo: ""}, }, }, }, }, }) plugin, errRegister := registerRPCPlugin(context.Background(), nil, "omc-zcode-oauth", lookup, pluginabi.MethodPluginRegister, nil) if errRegister != nil { t.Fatalf("registerRPCPlugin() error = %v", errRegister) } host := newHostWithRecords(capabilityRecord{ id: "omc-zcode-oauth", priority: 10, meta: plugin.Metadata, plugin: plugin, }) infos := host.RegisteredPlugins() if len(infos) != 1 { t.Fatalf("RegisteredPlugins() len = %d, want 1", len(infos)) } if infos[0].OAuthProvider != "zcode-zai" { t.Fatalf("OAuthProvider = %q, want zcode-zai", infos[0].OAuthProvider) } wantProviders := []string{"zcode-zai", "zcode-bigmodel"} if !reflect.DeepEqual(infos[0].OAuthProviders, wantProviders) { t.Fatalf("OAuthProviders = %#v, want %#v", infos[0].OAuthProviders, wantProviders) } wantEntries := []pluginapi.AuthProviderEntry{ {Identifier: "zcode-zai", Name: "Z.ai", Logo: "https://chat.z.ai/logo.svg"}, {Identifier: "zcode-bigmodel", Name: "OMC ZCode OAuth", Logo: "https://default.example/zcode.svg"}, } if !reflect.DeepEqual(infos[0].OAuthProviderEntries, wantEntries) { t.Fatalf("OAuthProviderEntries = %#v, want %#v", infos[0].OAuthProviderEntries, wantEntries) } // Verify wire-level tolerance when auth.identifier returns only entries (empty identifier/identifiers) and unknown keys. rawWireClient := &lifecycleTestClient{call: func(_ context.Context, method string, _ []byte) ([]byte, error) { switch method { case pluginabi.MethodPluginRegister: return marshalRPCResult(rpcRegistration{ SchemaVersion: pluginabi.SchemaVersion, Metadata: pluginapi.Metadata{ Name: "OMC ZCode OAuth", Version: "1.0.0", Author: "test", GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI", Logo: "https://default.example/zcode.svg", }, Capabilities: rpcCapabilities{ AuthProvider: true, }, }) case pluginabi.MethodAuthIdentifier: return []byte(`{"ok":true,"result":{"identifier":"","entries":[{"identifier":" zcode-zai ","name":"Z.ai","logo":"https://chat.z.ai/logo.svg","extra_future_field":true},{"identifier":"zcode-bigmodel","unknown_key":"ignored"}],"future_top_level":"ignored"}}`), nil default: return marshalRPCResult(rpcEmptyResponse{}) } }} wirePlugin, errWire := registerRPCPlugin(context.Background(), nil, "omc-wire", rawWireClient, pluginabi.MethodPluginRegister, nil) if errWire != nil { t.Fatalf("registerRPCPlugin(wirePlugin) error = %v", errWire) } wireHost := newHostWithRecords(capabilityRecord{ id: "omc-wire", priority: 10, meta: wirePlugin.Metadata, plugin: wirePlugin, }) wireInfos := wireHost.RegisteredPlugins() if len(wireInfos) != 1 || wireInfos[0].OAuthProvider != "zcode-zai" || !reflect.DeepEqual(wireInfos[0].OAuthProviders, wantProviders) || !reflect.DeepEqual(wireInfos[0].OAuthProviderEntries, wantEntries) { t.Fatalf("wireInfos = %#v, want primary=zcode-zai providers=%#v entries=%#v", wireInfos, wantProviders, wantEntries) } } func TestRegisterRPCPluginRejectsFutureSchemaVersion(t *testing.T) { lookup := newTestSymbolLookup(&testPlugin{ registerResult: validTestPlugin("future-schema"), }) lookup.schemaVersion = pluginabi.SchemaVersion + 1 _, errRegister := registerRPCPlugin(context.Background(), nil, "future-schema", lookup, pluginabi.MethodPluginRegister, nil) if errRegister == nil || !strings.Contains(errRegister.Error(), "schema version") { t.Fatalf("registerRPCPlugin() error = %v, want unsupported schema version", errRegister) } } func TestRegisterRPCPluginAcceptsModelRouterOnSchema1(t *testing.T) { plugin := validTestPlugin("router-schema1") plugin.Capabilities.ModelRouter = modelRouterFunc(func(context.Context, pluginapi.ModelRouteRequest) (pluginapi.ModelRouteResponse, error) { return pluginapi.ModelRouteResponse{}, nil }) lookup := newTestSymbolLookup(&testPlugin{registerResult: plugin}) lookup.schemaVersion = 1 registered, errRegister := registerRPCPlugin(context.Background(), nil, "router-schema1", lookup, pluginabi.MethodPluginRegister, nil) if errRegister != nil { t.Fatalf("registerRPCPlugin() error = %v, want model_router on schema 1", errRegister) } if registered.Capabilities.ModelRouter == nil { t.Fatal("ModelRouter = nil, want adapter") } if registered.SchemaVersion != 1 { t.Fatalf("registered SchemaVersion = %d, want 1", registered.SchemaVersion) } } func TestRPCModelRouteUsesAdapter(t *testing.T) { var routeCalls int var gotReq pluginapi.ModelRouteRequest lookup := newTestSymbolLookup(&testPlugin{ registerResult: pluginapi.Plugin{ Metadata: pluginapi.Metadata{ Name: "router", Version: "1.0.0", Author: "test", GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI", }, Capabilities: pluginapi.Capabilities{ ModelRouter: modelRouterFunc(func(ctx context.Context, req pluginapi.ModelRouteRequest) (pluginapi.ModelRouteResponse, error) { routeCalls++ gotReq = req return pluginapi.ModelRouteResponse{ Handled: true, TargetKind: pluginapi.ModelRouteTargetExecutor, Target: "claude-websearch-plugin", Reason: "typed websearch", }, nil }), }, }, }) plugin, errRegister := registerRPCPlugin(context.Background(), nil, "router", lookup, pluginabi.MethodPluginRegister, nil) if errRegister != nil { t.Fatalf("registerRPCPlugin() error = %v", errRegister) } if plugin.Capabilities.ModelRouter == nil { t.Fatal("ModelRouter = nil, want adapter") } req := pluginapi.ModelRouteRequest{ SourceFormat: "anthropic", RequestedModel: "claude-sonnet", Stream: true, Headers: map[string][]string{"X-Test": {"one", "two"}}, Query: map[string][]string{"beta": {"true"}}, Body: []byte(`{"tools":[{"type":"web_search_20250305","name":"web_search"}]}`), Metadata: map[string]any{ "keep": "value", }, } resp, errRoute := plugin.Capabilities.ModelRouter.RouteModel(context.Background(), req) if errRoute != nil { t.Fatalf("ModelRouter.RouteModel() error = %v", errRoute) } if !resp.Handled || resp.Target != "claude-websearch-plugin" || resp.Reason != "typed websearch" { t.Fatalf("ModelRouter.RouteModel() response = %#v", resp) } if routeCalls != 1 { t.Fatalf("route calls = %d, want 1", routeCalls) } if gotReq.SourceFormat != req.SourceFormat || gotReq.RequestedModel != req.RequestedModel || gotReq.Stream != req.Stream || string(gotReq.Body) != string(req.Body) { t.Fatalf("route request main fields = %#v, want %#v", gotReq, req) } if !reflect.DeepEqual(gotReq.Headers, req.Headers) { t.Fatalf("route request headers = %#v, want %#v", gotReq.Headers, req.Headers) } if !reflect.DeepEqual(gotReq.Query, req.Query) { t.Fatalf("route request query = %#v, want %#v", gotReq.Query, req.Query) } if gotReq.Metadata["keep"] != "value" { t.Fatalf("route request metadata = %#v", gotReq.Metadata) } } func TestRPCSchedulerPickUsesAdapter(t *testing.T) { var pickCalls int var gotReq pluginapi.SchedulerPickRequest lookup := newTestSymbolLookup(&testPlugin{ registerResult: pluginapi.Plugin{ Metadata: pluginapi.Metadata{ Name: "scheduler", Version: "1.0.0", Author: "test", GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI", }, Capabilities: pluginapi.Capabilities{ Scheduler: schedulerFunc(func(ctx context.Context, req pluginapi.SchedulerPickRequest) (pluginapi.SchedulerPickResponse, error) { pickCalls++ gotReq = req return pluginapi.SchedulerPickResponse{ AuthID: "auth-2", Handled: true, }, nil }), }, }, }) plugin, errRegister := registerRPCPlugin(context.Background(), nil, "scheduler", lookup, pluginabi.MethodPluginRegister, nil) if errRegister != nil { t.Fatalf("registerRPCPlugin() error = %v", errRegister) } if plugin.Capabilities.Scheduler == nil { t.Fatal("Scheduler = nil, want adapter") } req := pluginapi.SchedulerPickRequest{ Provider: "openai", Providers: []string{"openai", "codex"}, Model: "gpt-5.4", Stream: true, Options: pluginapi.SchedulerOptions{ Headers: map[string][]string{"X-Test": {"one", "two"}}, }, Candidates: []pluginapi.SchedulerAuthCandidate{ { ID: "auth-1", Provider: "openai", Priority: 10, Status: "ready", Attributes: map[string]string{"region": "us"}, }, { ID: "auth-2", Provider: "codex", Priority: 20, Status: "ready", Attributes: map[string]string{"region": "eu"}, }, }, } resp, errPick := plugin.Capabilities.Scheduler.Pick(context.Background(), req) if errPick != nil { t.Fatalf("Scheduler.Pick() error = %v", errPick) } if resp.AuthID != "auth-2" || !resp.Handled { t.Fatalf("Scheduler.Pick() response = %#v, want auth-2 handled", resp) } if pickCalls != 1 { t.Fatalf("scheduler pick calls = %d, want 1", pickCalls) } if gotReq.Provider != req.Provider || !reflect.DeepEqual(gotReq.Providers, req.Providers) || gotReq.Model != req.Model || gotReq.Stream != req.Stream { t.Fatalf("scheduler request main fields = %#v, want %#v", gotReq, req) } if !reflect.DeepEqual(gotReq.Options.Headers, req.Options.Headers) { t.Fatalf("scheduler request headers = %#v, want %#v", gotReq.Options.Headers, req.Options.Headers) } if len(gotReq.Candidates) != len(req.Candidates) { t.Fatalf("scheduler candidates len = %d, want %d", len(gotReq.Candidates), len(req.Candidates)) } for index := range req.Candidates { gotCandidate := gotReq.Candidates[index] wantCandidate := req.Candidates[index] if gotCandidate.ID != wantCandidate.ID || gotCandidate.Provider != wantCandidate.Provider || gotCandidate.Priority != wantCandidate.Priority || gotCandidate.Status != wantCandidate.Status || !reflect.DeepEqual(gotCandidate.Attributes, wantCandidate.Attributes) { t.Fatalf("scheduler candidate[%d] = %#v, want %#v", index, gotCandidate, wantCandidate) } } } func TestSanitizePluginRequestScheduler(t *testing.T) { req := pluginapi.SchedulerPickRequest{ Provider: "openai", Providers: []string{"openai", "codex"}, Model: "gpt-5.4", Stream: true, Options: pluginapi.SchedulerOptions{ Headers: map[string][]string{"X-Test": {"one", "two"}}, Metadata: map[string]any{ "keep": "value", "drop": make(chan struct{}), }, }, Candidates: []pluginapi.SchedulerAuthCandidate{ { ID: "auth-1", Provider: "openai", Priority: 10, Status: "ready", Attributes: map[string]string{"region": "us"}, Metadata: map[string]any{ "keep": "candidate", "drop": make(chan struct{}), }, }, }, } raw, errMarshal := json.Marshal(sanitizePluginRequest(req)) if errMarshal != nil { t.Fatalf("Marshal(sanitized scheduler request) error = %v", errMarshal) } var decoded pluginapi.SchedulerPickRequest if errUnmarshal := json.Unmarshal(raw, &decoded); errUnmarshal != nil { t.Fatalf("Unmarshal(sanitized scheduler request) error = %v", errUnmarshal) } if decoded.Provider != req.Provider || !reflect.DeepEqual(decoded.Providers, req.Providers) || decoded.Model != req.Model || decoded.Stream != req.Stream { t.Fatalf("scheduler request main fields = %#v, want %#v", decoded, req) } if !reflect.DeepEqual(decoded.Options.Headers, req.Options.Headers) { t.Fatalf("scheduler request headers = %#v, want %#v", decoded.Options.Headers, req.Options.Headers) } if decoded.Options.Metadata["keep"] != "value" { t.Fatalf("scheduler options metadata keep = %#v, want value", decoded.Options.Metadata["keep"]) } if _, ok := decoded.Options.Metadata["drop"]; ok { t.Fatalf("scheduler options metadata drop survived sanitize: %#v", decoded.Options.Metadata) } if len(decoded.Candidates) != 1 { t.Fatalf("scheduler candidates len = %d, want 1", len(decoded.Candidates)) } gotCandidate := decoded.Candidates[0] wantCandidate := req.Candidates[0] if gotCandidate.ID != wantCandidate.ID || gotCandidate.Provider != wantCandidate.Provider || gotCandidate.Priority != wantCandidate.Priority || gotCandidate.Status != wantCandidate.Status || !reflect.DeepEqual(gotCandidate.Attributes, wantCandidate.Attributes) { t.Fatalf("scheduler candidate = %#v, want %#v", gotCandidate, wantCandidate) } if gotCandidate.Metadata["keep"] != "candidate" { t.Fatalf("scheduler candidate metadata keep = %#v, want candidate", gotCandidate.Metadata["keep"]) } if _, ok := gotCandidate.Metadata["drop"]; ok { t.Fatalf("scheduler candidate metadata drop survived sanitize: %#v", gotCandidate.Metadata) } } func TestRPCSchedulerAndUsageSelectionIDCompatibility(t *testing.T) { pickReq := pluginapi.SchedulerPickRequest{ Provider: "openai", Model: "gpt-5.4", Options: pluginapi.SchedulerOptions{ Metadata: map[string]any{ pluginapi.SelectionIDMetadataKey: "sel-rpc-123", }, }, } rawPick, errMarshalPick := json.Marshal(sanitizePluginRequest(pickReq)) if errMarshalPick != nil { t.Fatalf("Marshal(SchedulerPickRequest) error = %v", errMarshalPick) } var decodedPick pluginapi.SchedulerPickRequest if errUnmarshalPick := json.Unmarshal(rawPick, &decodedPick); errUnmarshalPick != nil { t.Fatalf("Unmarshal(SchedulerPickRequest) error = %v", errUnmarshalPick) } if got := decodedPick.Options.Metadata[pluginapi.SelectionIDMetadataKey]; got != "sel-rpc-123" { t.Fatalf("decodedPick selection_id = %#v, want %q", got, "sel-rpc-123") } usageRec := pluginapi.UsageRecord{ Provider: "openai", Model: "gpt-5.4", RequestID: "req-1", TraceID: "trace-1", SelectionID: "sel-rpc-123", } rawUsage, errMarshalUsage := json.Marshal(usageRec) if errMarshalUsage != nil { t.Fatalf("Marshal(UsageRecord) error = %v", errMarshalUsage) } var decodedUsage pluginapi.UsageRecord if errUnmarshalUsage := json.Unmarshal(rawUsage, &decodedUsage); errUnmarshalUsage != nil { t.Fatalf("Unmarshal(UsageRecord) error = %v", errUnmarshalUsage) } if decodedUsage.SelectionID != "sel-rpc-123" { t.Fatalf("decodedUsage.SelectionID = %q, want %q", decodedUsage.SelectionID, "sel-rpc-123") } // Old plugins compiled against a legacy UsageRecord without SelectionID must still unmarshal cleanly. type legacyUsageRecord struct { Provider string Model string RequestID string TraceID string } var legacy legacyUsageRecord if errLegacy := json.Unmarshal(rawUsage, &legacy); errLegacy != nil { t.Fatalf("Unmarshal(legacyUsageRecord) error = %v", errLegacy) } if legacy.Provider != "openai" || legacy.RequestID != "req-1" || legacy.TraceID != "trace-1" { t.Fatalf("legacyUsageRecord = %+v", legacy) } } func TestUsageRecordRPCModalityTokenCounters(t *testing.T) { var deliveredRaw []byte var deliveredRecord pluginapi.UsageRecord rpcAdapter := &rpcPluginAdapter{ id: "rpc-usage-modality", client: &lifecycleTestClient{call: func(_ context.Context, method string, request []byte) ([]byte, error) { if method != pluginabi.MethodUsageHandle { return nil, io.ErrUnexpectedEOF } deliveredRaw = bytes.Clone(request) if errUnmarshal := json.Unmarshal(request, &deliveredRecord); errUnmarshal != nil { return nil, errUnmarshal } return marshalRPCResult(rpcEmptyResponse{}) }}, } host := newHostWithRecords(capabilityRecord{ id: "rpc-usage-modality", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ UsagePlugin: rpcAdapter, }}, }) rpcAdapter.host = host adapter := &usageAdapter{ host: host, pluginID: "rpc-usage-modality", plugin: rpcAdapter, } adapter.HandleUsage(context.Background(), coreusage.Record{ Provider: "openai", Model: "gpt-image-2.5-flare", Detail: coreusage.Detail{ InputTokens: 80, OutputTokens: 1056, CachedTokens: 40, CacheReadTokens: 40, TotalTokens: 1136, TextInputTokens: coreusage.Int64Ptr(30), ImageInputTokens: coreusage.Int64Ptr(50), CachedTextTokens: coreusage.Int64Ptr(25), CachedImageTokens: coreusage.Int64Ptr(15), TextOutputTokens: coreusage.Int64Ptr(0), ImageOutputTokens: coreusage.Int64Ptr(1056), }, }) got := deliveredRecord.Detail if got.InputTokens != 80 || got.OutputTokens != 1056 || got.CachedTokens != 40 || got.CacheReadTokens != 40 || got.TotalTokens != 1136 { t.Fatalf("delivered aggregate Detail = %+v", got) } if got.TextInputTokens == nil || *got.TextInputTokens != 30 { t.Fatalf("delivered TextInputTokens = %v, want 30", got.TextInputTokens) } if got.ImageInputTokens == nil || *got.ImageInputTokens != 50 { t.Fatalf("delivered ImageInputTokens = %v, want 50", got.ImageInputTokens) } if got.CachedTextTokens == nil || *got.CachedTextTokens != 25 { t.Fatalf("delivered CachedTextTokens = %v, want 25", got.CachedTextTokens) } if got.CachedImageTokens == nil || *got.CachedImageTokens != 15 { t.Fatalf("delivered CachedImageTokens = %v, want 15", got.CachedImageTokens) } if got.TextOutputTokens == nil || *got.TextOutputTokens != 0 { t.Fatalf("delivered TextOutputTokens = %v, want explicit 0", got.TextOutputTokens) } if got.ImageOutputTokens == nil || *got.ImageOutputTokens != 1056 { t.Fatalf("delivered ImageOutputTokens = %v, want 1056", got.ImageOutputTokens) } if got.AudioInputTokens != nil || got.VideoInputTokens != nil || got.FileInputTokens != nil || got.CachedAudioTokens != nil || got.AudioOutputTokens != nil { t.Fatalf("expected absent modality counters to remain nil over RPC, got %+v", got) } // Legacy plugins compiled against the 7-field UsageDetail must still unmarshal cleanly. type legacyUsageDetail struct { InputTokens int64 OutputTokens int64 ReasoningTokens int64 CachedTokens int64 CacheReadTokens int64 CacheCreationTokens int64 TotalTokens int64 } type legacyUsageRecordWithDetail struct { Provider string Model string Detail legacyUsageDetail } var legacyRec legacyUsageRecordWithDetail if errLegacy := json.Unmarshal(deliveredRaw, &legacyRec); errLegacy != nil { t.Fatalf("Unmarshal(legacyUsageRecordWithDetail) error = %v", errLegacy) } if legacyRec.Detail.InputTokens != 80 || legacyRec.Detail.OutputTokens != 1056 || legacyRec.Detail.TotalTokens != 1136 { t.Fatalf("legacyUsageRecordWithDetail.Detail = %+v", legacyRec.Detail) } } type recordingRawPatchClient struct { schemaVersion uint32 methods []string rawRequests [][]byte rawRequestPtr []uintptr patchFunc func(method string, rawBody []byte) (pluginapi.RequestBodyPatchResponse, error) } func (c *recordingRawPatchClient) Call(_ context.Context, method string, request []byte) ([]byte, error) { c.methods = append(c.methods, method) c.rawRequests = append(c.rawRequests, bytes.Clone(request)) if len(request) > 0 { c.rawRequestPtr = append(c.rawRequestPtr, uintptr(reflect.ValueOf(request).Pointer())) } else { c.rawRequestPtr = append(c.rawRequestPtr, 0) } switch method { case pluginabi.MethodPluginRegister: return marshalRPCResult(rpcRegistration{ Metadata: pluginapi.Metadata{ Name: "Timezone Patcher", Version: "1.0.0", Author: "test", GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI", }, SchemaVersion: c.schemaVersion, Capabilities: rpcCapabilities{ RequestInterceptorPatchBodyBefore: true, RequestInterceptorPatchBodyAfter: true, }, }) case pluginabi.MethodRequestPatchBodyBefore, pluginabi.MethodRequestPatchBodyAfter: resp, err := c.patchFunc(method, request) if err != nil { return nil, err } return marshalRPCResult(resp) case pluginabi.MethodRequestInterceptBefore, pluginabi.MethodRequestInterceptAfter: var req pluginapi.RequestInterceptRequest if err := json.Unmarshal(request, &req); err != nil { return nil, err } return marshalRPCResult(pluginapi.RequestInterceptResponse{ Body: bytes.ReplaceAll(req.Body, []byte("UTC"), []byte("Legacy")), }) default: return marshalRPCResult(struct{}{}) } } func (c *recordingRawPatchClient) Shutdown() {} func (c *recordingRawPatchClient) Close() error { return nil } func TestRPCRequestBodyPatchRawCABIAndSchemaVersionGate(t *testing.T) { rawInput := []byte(`{"model":"gpt-5","input":[{"role":"developer","content":[{"type":"input_text","text":"\n 2026-04-19\n UTC\n"}]}]}`) clientV8 := &recordingRawPatchClient{ schemaVersion: pluginabi.SchemaVersionRequestInterceptorBodyPatch, patchFunc: func(method string, rawBody []byte) (pluginapi.RequestBodyPatchResponse, error) { if method == pluginabi.MethodRequestPatchBodyBefore { target := []byte("UTC") idx := bytes.Index(rawBody, target) if idx < 0 { return pluginapi.RequestBodyPatchResponse{}, nil } return pluginapi.RequestBodyPatchResponse{ Edits: []pluginapi.RequestBodyEdit{ { Start: idx, End: idx + len(target), Replacement: "Asia/Shanghai", }, }, }, nil } return pluginapi.RequestBodyPatchResponse{}, nil }, } host := New() pluginV8, errReg := registerRPCPlugin(context.Background(), host, "tz-patcher", clientV8, pluginabi.MethodPluginRegister, nil) if errReg != nil { t.Fatalf("registerRPCPlugin(v8) error = %v", errReg) } if !validPlugin(pluginV8) { t.Fatalf("validPlugin(patch-only pluginV8) = false, want true") } setHostSnapshotForTest(host, true, capabilityRecord{id: "tz-patcher", plugin: pluginV8}) if !host.HasRequestInterceptors() { t.Fatal("HasRequestInterceptors() = false, want true for patch-only plugin") } resp := host.InterceptRequestBeforeAuth(context.Background(), pluginapi.RequestInterceptRequest{ Model: "gpt-5", Body: rawInput, }) if len(clientV8.methods) != 2 || clientV8.methods[1] != pluginabi.MethodRequestPatchBodyBefore { t.Fatalf("called methods = %v, want [%s %s]", clientV8.methods, pluginabi.MethodPluginRegister, pluginabi.MethodRequestPatchBodyBefore) } // Crucial requirement 1: C ABI receives the raw request body directly, not a JSON/Base64 envelope, // and without a pre-call bytes.Clone on the host side. if !bytes.Equal(clientV8.rawRequests[1], rawInput) { t.Fatalf("raw C ABI request = %s, want exact rawInput %s", clientV8.rawRequests[1], rawInput) } if gotPtr, wantPtr := clientV8.rawRequestPtr[1], uintptr(reflect.ValueOf(rawInput).Pointer()); gotPtr != wantPtr { t.Fatalf("raw C ABI request slice pointer = %#x, want original rawInput pointer %#x (zero pre-call copy)", gotPtr, wantPtr) } wantPatched := `{"model":"gpt-5","input":[{"role":"developer","content":[{"type":"input_text","text":"\n 2026-04-19\n Asia/Shanghai\n"}]}]}` if string(resp.Body) != wantPatched { t.Fatalf("patched body = %s, want %s", resp.Body, wantPatched) } // Crucial requirement 3: When plugin returns empty edits, host reuses original body (resp.Body == nil). afterResp := host.InterceptRequestAfterAuth(context.Background(), pluginapi.RequestInterceptRequest{ Model: "gpt-5", Body: rawInput, }) if afterResp.Body != nil { t.Fatalf("no-op patch returned non-nil Body = %s, want nil to reuse original body", afterResp.Body) } // Schema v7 must ignore request_interceptor_patch_body_before. clientV7 := &recordingRawPatchClient{ schemaVersion: pluginabi.SchemaVersionRequestInterceptorOmitBody, patchFunc: func(string, []byte) (pluginapi.RequestBodyPatchResponse, error) { t.Fatal("patchFunc must not be called on schema v7") return pluginapi.RequestBodyPatchResponse{}, nil }, } pluginV7, errRegV7 := registerRPCPlugin(context.Background(), host, "tz-patcher-v7", clientV7, pluginabi.MethodPluginRegister, nil) if errRegV7 != nil { t.Fatalf("registerRPCPlugin(v7) error = %v", errRegV7) } if pluginV7.Capabilities.RequestBodyPatcherBefore != nil || pluginV7.Capabilities.RequestInterceptorPatchBodyBefore { t.Fatalf("schema v7 kept patch capability: %+v", pluginV7.Capabilities) } }