From e499dd5f5dfd9cd3378f0228d26640ae1fefdfe5 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Wed, 1 Jul 2026 01:31:07 -0700 Subject: [PATCH] fix: scope model usage to exact provider ids --- docs/gateway/configuration-reference.md | 3 +- docs/plugins/sdk-entrypoints.md | 3 +- src/plugins/services.test.ts | 127 +++++++----------------- src/plugins/services.ts | 34 +------ 4 files changed, 41 insertions(+), 126 deletions(-) diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index ba98a095def1..6840f8cbca4f 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -268,8 +268,7 @@ See [MCP](/cli/mcp#openclaw-as-an-mcp-client-registry) and - `allow`: optional allowlist (only listed plugins load). `deny` wins. - `plugins.modelUsage.enabled`: explicitly enables model usage accounting events for provider plugin service subscriptions. Events are scoped to provider ids - registered by the same plugin, including configured aliases resolved through - `models.providers..api`. Default: `false`. This is separate from + registered by the same plugin. Default: `false`. This is separate from `diagnostics.enabled`; enabling it does not expose raw diagnostics. - `plugins.entries..apiKey`: plugin-level API key convenience field (when supported by the plugin). - `plugins.entries..env`: plugin-scoped env var map. diff --git a/docs/plugins/sdk-entrypoints.md b/docs/plugins/sdk-entrypoints.md index b6e388aaabbf..bc3ee1c1c47a 100644 --- a/docs/plugins/sdk-entrypoints.md +++ b/docs/plugins/sdk-entrypoints.md @@ -354,8 +354,7 @@ agent/session ids, token buckets (`input`, `output`, `cacheRead`, `cacheWrite`, estimated `costUsd`, and run duration. The surface exposes only model usage accounting; raw prompts, responses, tool payloads, and other diagnostics are not included. Events are scoped to the service plugin's registered provider ids; -configured provider aliases such as `models.providers..api` resolve back to -the owning provider plugin. +events for other provider ids are not delivered. ## Plugin shapes diff --git a/src/plugins/services.test.ts b/src/plugins/services.test.ts index 6362d1c8db34..b25e99bfa927 100644 --- a/src/plugins/services.test.ts +++ b/src/plugins/services.test.ts @@ -82,6 +82,9 @@ function createServiceConfig() { function createModelUsageServiceConfig() { return { + diagnostics: { + enabled: false, + }, plugins: { modelUsage: { enabled: true, @@ -401,7 +404,7 @@ describe("startPluginServices", () => { expect(contexts[0]?.modelUsage).toBeUndefined(); }); - it("exposes trusted model usage events to plugin services when enabled", async () => { + it("exposes matching-provider usage when enabled without diagnostics", async () => { const seen: PluginModelUsageEvent[] = []; let unsubscribe: (() => void) | undefined; const config = createModelUsageServiceConfig(); @@ -449,38 +452,40 @@ describe("startPluginServices", () => { usage: { total: 1 }, }, ); - emitModelUsageEvent( - { diagnostics: { enabled: false }, plugins: { modelUsage: { enabled: true } } }, - { - sessionKey: "agent:main:slack:channel:c1", - sessionId: "session-1", - channel: "slack", - agentId: "main", - provider: "openai", - model: "gpt-5.5", - usage: { - input: 10, - output: 5, - cacheRead: 2, - cacheWrite: 1, - promptTokens: 13, - total: 18, - }, - lastCallUsage: { - input: 4, - output: 5, - cacheRead: 2, - cacheWrite: 1, - total: 12, - }, - context: { - limit: 100, - used: 13, - }, - costUsd: 0.00042, - durationMs: 123, + emitModelUsageEvent(config, { + sessionKey: "other-provider", + provider: "anthropic", + usage: { total: 1 }, + }); + emitModelUsageEvent(config, { + sessionKey: "agent:main:slack:channel:c1", + sessionId: "session-1", + channel: "slack", + agentId: "main", + provider: "openai", + model: "gpt-5.5", + usage: { + input: 10, + output: 5, + cacheRead: 2, + cacheWrite: 1, + promptTokens: 13, + total: 18, }, - ); + lastCallUsage: { + input: 4, + output: 5, + cacheRead: 2, + cacheWrite: 1, + total: 12, + }, + context: { + limit: 100, + used: 13, + }, + costUsd: 0.00042, + durationMs: 123, + }); expect(seen).toEqual([ { @@ -524,64 +529,6 @@ describe("startPluginServices", () => { expect(seen).toHaveLength(1); }); - it("scopes model usage events to provider owner services", async () => { - const seen: PluginModelUsageEvent[] = []; - let unsubscribe: (() => void) | undefined; - const config = { - ...createModelUsageServiceConfig(), - models: { - providers: { - "custom-openai": { - api: "openai", - }, - }, - }, - } as Parameters[0]["config"]; - const usageService: OpenClawPluginService = { - id: "usage-reader", - start: (ctx) => { - if (!ctx.modelUsage) { - throw new Error("expected model usage subscription"); - } - unsubscribe = ctx.modelUsage.onEvent((event) => { - seen.push(event); - }); - }, - stop: () => unsubscribe?.(), - }; - const handle = await startPluginServices({ - registry: createRegistry([usageService], "openai-plugin", "workspace", false, [ - { pluginId: "openai-plugin", provider: createProvider("openai") }, - { pluginId: "anthropic-plugin", provider: createProvider("anthropic") }, - ]), - config, - }); - - emitModelUsageEvent(config, { - sessionKey: "other-provider", - provider: "anthropic", - usage: { total: 1 }, - }); - emitModelUsageEvent(config, { - sessionKey: "direct-provider", - provider: "openai", - usage: { total: 2 }, - }); - emitModelUsageEvent(config, { - sessionKey: "configured-alias", - provider: "custom-openai", - usage: { total: 3 }, - }); - emitModelUsageEvent(config, { - sessionKey: "missing-provider", - usage: { total: 4 }, - }); - - expect(seen.map((event) => event.sessionKey)).toEqual(["direct-provider", "configured-alias"]); - - await handle.stop(); - }); - it("grants internal diagnostics only to trusted diagnostics exporter services", async () => { const contexts: OpenClawPluginServiceContext[] = []; const diagnosticsService = createTrackingService("diagnostics-otel", { contexts }); diff --git a/src/plugins/services.ts b/src/plugins/services.ts index 3f4b91c25ca1..ba0def7f120b 100644 --- a/src/plugins/services.ts +++ b/src/plugins/services.ts @@ -1,8 +1,5 @@ /** Starts, stops, and inspects plugin service registrations. */ -import { - findNormalizedProviderValue, - normalizeProviderId, -} from "@openclaw/model-catalog-core/provider-id"; +import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; import { STATE_DIR } from "../config/paths.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { @@ -72,27 +69,6 @@ function createModelUsageProviderIds( return providerIds; } -function modelUsageEventMatchesProviderIds(params: { - config: OpenClawConfig; - event: DiagnosticUsageEvent; - providerIds: ReadonlySet; -}): boolean { - const normalizedProvider = normalizeProviderId(params.event.provider ?? ""); - if (!normalizedProvider) { - return false; - } - if (params.providerIds.has(normalizedProvider)) { - return true; - } - const providerConfig = findNormalizedProviderValue( - params.config.models?.providers, - normalizedProvider, - ); - const api = - typeof providerConfig?.api === "string" ? normalizeProviderId(providerConfig.api) : ""; - return Boolean(api && params.providerIds.has(api)); -} - function createServiceContext(params: { config: OpenClawConfig; registry: PluginRegistry; @@ -122,13 +98,7 @@ function createServiceContext(params: { modelUsage: { onEvent: (listener) => onCoreModelUsageEvent((event) => { - if ( - !modelUsageEventMatchesProviderIds({ - config: params.config, - event, - providerIds: modelUsageProviderIds, - }) - ) { + if (!modelUsageProviderIds.has(normalizeProviderId(event.provider ?? ""))) { return; } listener(toPluginModelUsageEvent(event));