mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-07 09:17:12 +08:00
fix: scope model usage to exact provider ids
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user