Skip to content

Commit bb06dc7

Browse files
authored
fix(agents): restore usage tracking for non-native openai-completions providers
Fixes openclaw#46142 Stop forcing supportsUsageInStreaming=false on non-native openai-completions endpoints. Most OpenAI-compatible APIs (DashScope, DeepSeek, Groq, Together, etc.) handle stream_options: { include_usage: true } correctly. The blanket disable broke usage/cost tracking for all non-OpenAI providers. supportsDeveloperRole is still forced off for non-native endpoints since the developer message role is genuinely OpenAI-specific. Users on backends that reject stream_options can opt out with compat.supportsUsageInStreaming: false in their model config. Fixes openclaw#46142
1 parent d33f3f8 commit bb06dc7

File tree

3 files changed

+30
-19
lines changed

3 files changed

+30
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
1919

2020
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. Thanks @vincentkoc.
2121
- CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob.
22+
- Agents/usage tracking: stop forcing `supportsUsageInStreaming: false` on non-native openai-completions endpoints so providers like DashScope, DeepSeek, and other OpenAI-compatible backends report token usage and cost instead of showing all zeros. (#46142)
2223

2324
## 2026.3.13
2425

src/agents/model-compat.test.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,16 @@ describe("normalizeModelCompat", () => {
219219
});
220220
});
221221

222-
it("forces supportsUsageInStreaming off for generic custom openai-completions provider", () => {
223-
expectSupportsUsageInStreamingForcedOff({
222+
it("leaves supportsUsageInStreaming at default for generic custom openai-completions provider", () => {
223+
const model = {
224+
...baseModel(),
224225
provider: "custom-cpa",
225226
baseUrl: "https://cpa.example.com/v1",
226-
});
227+
};
228+
delete (model as { compat?: unknown }).compat;
229+
const normalized = normalizeModelCompat(model as Model<Api>);
230+
// supportsUsageInStreaming is no longer forced off — pi-ai's default (true) applies
231+
expect(supportsUsageInStreaming(normalized)).toBeUndefined();
227232
});
228233

229234
it("forces supportsDeveloperRole off for Qwen proxy via openai-completions", () => {
@@ -273,7 +278,7 @@ describe("normalizeModelCompat", () => {
273278
expect(supportsUsageInStreaming(normalized)).toBe(true);
274279
});
275280

276-
it("still forces flags off when not explicitly set by user", () => {
281+
it("forces supportsDeveloperRole off but leaves supportsUsageInStreaming unset for non-native endpoints", () => {
277282
const model = {
278283
...baseModel(),
279284
provider: "custom-cpa",
@@ -282,7 +287,8 @@ describe("normalizeModelCompat", () => {
282287
delete (model as { compat?: unknown }).compat;
283288
const normalized = normalizeModelCompat(model);
284289
expect(supportsDeveloperRole(normalized)).toBe(false);
285-
expect(supportsUsageInStreaming(normalized)).toBe(false);
290+
// supportsUsageInStreaming is no longer forced off — pi-ai default applies
291+
expect(supportsUsageInStreaming(normalized)).toBeUndefined();
286292
});
287293

288294
it("does not mutate caller model when forcing supportsDeveloperRole off", () => {
@@ -297,7 +303,8 @@ describe("normalizeModelCompat", () => {
297303
expect(supportsDeveloperRole(model)).toBeUndefined();
298304
expect(supportsUsageInStreaming(model)).toBeUndefined();
299305
expect(supportsDeveloperRole(normalized)).toBe(false);
300-
expect(supportsUsageInStreaming(normalized)).toBe(false);
306+
// supportsUsageInStreaming is not set by normalizeModelCompat — pi-ai default applies
307+
expect(supportsUsageInStreaming(normalized)).toBeUndefined();
301308
});
302309

303310
it("does not override explicit compat false", () => {

src/agents/model-compat.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,16 @@ export function normalizeModelCompat(model: Model<Api>): Model<Api> {
5252
return model;
5353
}
5454

55-
// The `developer` role and stream usage chunks are OpenAI-native behaviors.
56-
// Many OpenAI-compatible backends reject `developer` and/or emit usage-only
57-
// chunks that break strict parsers expecting choices[0]. For non-native
58-
// openai-completions endpoints, force both compat flags off — unless the
59-
// user has explicitly opted in via their model config.
55+
// The `developer` role is an OpenAI-native behavior that most compatible
56+
// backends reject. Force it off for non-native endpoints unless the user
57+
// has explicitly opted in via their model config.
58+
//
59+
// `supportsUsageInStreaming` is NOT forced off — most OpenAI-compatible
60+
// backends (DashScope, DeepSeek, Groq, Together, etc.) handle
61+
// `stream_options: { include_usage: true }` correctly, and disabling it
62+
// silently breaks usage/cost tracking for all non-native providers.
63+
// Users can still opt out with `compat.supportsUsageInStreaming: false`
64+
// if their backend rejects the parameter.
6065
const compat = model.compat ?? undefined;
6166
// When baseUrl is empty the pi-ai library defaults to api.openai.com, so
6267
// leave compat unchanged and let default native behavior apply.
@@ -65,24 +70,22 @@ export function normalizeModelCompat(model: Model<Api>): Model<Api> {
6570
return model;
6671
}
6772

68-
// Respect explicit user overrides: if the user has set a compat flag to
69-
// true in their model definition, they know their endpoint supports it.
73+
// Respect explicit user overrides.
7074
const forcedDeveloperRole = compat?.supportsDeveloperRole === true;
71-
const forcedUsageStreaming = compat?.supportsUsageInStreaming === true;
7275

73-
if (forcedDeveloperRole && forcedUsageStreaming) {
76+
if (forcedDeveloperRole) {
7477
return model;
7578
}
7679

77-
// Return a new object — do not mutate the caller's model reference.
80+
// Only force supportsDeveloperRole off. Leave supportsUsageInStreaming
81+
// at whatever the user set or pi-ai's default (true).
7882
return {
7983
...model,
8084
compat: compat
8185
? {
8286
...compat,
83-
supportsDeveloperRole: forcedDeveloperRole || false,
84-
supportsUsageInStreaming: forcedUsageStreaming || false,
87+
supportsDeveloperRole: false,
8588
}
86-
: { supportsDeveloperRole: false, supportsUsageInStreaming: false },
89+
: { supportsDeveloperRole: false },
8790
} as typeof model;
8891
}

0 commit comments

Comments
 (0)