fix: scope model usage to exact provider ids

This commit is contained in:
Dallin Romney
2026-07-01 01:31:07 -07:00
parent 42dcb23d74
commit e499dd5f5d
4 changed files with 41 additions and 126 deletions

View File

@@ -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.<id>.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.<id>.apiKey`: plugin-level API key convenience field (when supported by the plugin).
- `plugins.entries.<id>.env`: plugin-scoped env var map.

View File

@@ -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.<id>.api` resolve back to
the owning provider plugin.
events for other provider ids are not delivered.
## Plugin shapes

View File

@@ -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<typeof startPluginServices>[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 });

View File

@@ -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<string>;
}): 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));