diff --git a/internal/pluginhost/adapters_test.go b/internal/pluginhost/adapters_test.go index de62918ec..5b40f84cf 100644 --- a/internal/pluginhost/adapters_test.go +++ b/internal/pluginhost/adapters_test.go @@ -2283,6 +2283,49 @@ func TestUsageAdapterPreservesExplicitGenerateFalse(t *testing.T) { } } +func TestUsageAdapterDetachesContext(t *testing.T) { + var receivedCtx context.Context + plugin := usagePluginFunc(func(ctx context.Context, record pluginapi.UsageRecord) { + receivedCtx = ctx + }) + host := newHostWithRecords(capabilityRecord{ + id: "usage-detach", + plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ + UsagePlugin: plugin, + }}, + }) + adapter := &usageAdapter{ + host: host, + pluginID: "usage-detach", + } + + canceledCtx, cancel := context.WithCancel(context.Background()) + cancel() + + adapter.HandleUsage(canceledCtx, coreusage.Record{ + Provider: "provider", + Model: "gpt-5.4", + }) + if receivedCtx == nil { + t.Fatal("plugin did not receive context") + } + if errCtx := receivedCtx.Err(); errCtx != nil { + t.Fatalf("expected detached context without error, got ctx.Err() = %v", errCtx) + } + + receivedCtx = nil + adapter.HandleUsage(nil, coreusage.Record{ + Provider: "provider", + Model: "gpt-5.4", + }) + if receivedCtx == nil { + t.Fatal("plugin did not receive context for nil input") + } + if errCtx := receivedCtx.Err(); errCtx != nil { + t.Fatalf("expected non-nil context without error for nil input, got ctx.Err() = %v", errCtx) + } +} + func TestUsageManagerRegisterNamedReplacesWithoutDuplicateDispatch(t *testing.T) { manager := coreusage.NewManager(0) defer manager.Stop() diff --git a/internal/pluginhost/adapters_usage_translation.go b/internal/pluginhost/adapters_usage_translation.go index 2201eb6c8..a257db3e2 100644 --- a/internal/pluginhost/adapters_usage_translation.go +++ b/internal/pluginhost/adapters_usage_translation.go @@ -136,6 +136,11 @@ func (a *usageAdapter) HandleUsage(ctx context.Context, record coreusage.Record) if plugin == nil { return } + if ctx == nil { + ctx = context.Background() + } else { + ctx = context.WithoutCancel(ctx) + } defer func() { if recovered := recover(); recovered != nil { a.host.fusePlugin(a.pluginID, "UsagePlugin.HandleUsage", recovered) diff --git a/internal/pluginhost/rpc_client.go b/internal/pluginhost/rpc_client.go index 1425d4757..845a4c204 100644 --- a/internal/pluginhost/rpc_client.go +++ b/internal/pluginhost/rpc_client.go @@ -11,6 +11,7 @@ import ( "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi" "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi" + log "github.com/sirupsen/logrus" ) type rpcPluginAdapter struct { @@ -539,7 +540,9 @@ func (a rpcThinkingApplier) ApplyThinking(ctx context.Context, req pluginapi.Thi } func (a *rpcPluginAdapter) HandleUsage(ctx context.Context, record pluginapi.UsageRecord) { - _, _ = callPlugin[rpcEmptyResponse](ctx, a.client, pluginabi.MethodUsageHandle, record) + if _, errCall := callPlugin[rpcEmptyResponse](ctx, a.client, pluginabi.MethodUsageHandle, record); errCall != nil { + log.Debugf("pluginhost: usage.handle to %s failed: %v", a.id, errCall) + } } func (a *rpcPluginAdapter) RegisterCommandLine(ctx context.Context, req pluginapi.CommandLineRegistrationRequest) (pluginapi.CommandLineRegistrationResponse, error) {