mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
fix(agents): disable usage streaming chunks on non-native openai-completions
This commit is contained in:
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Models/openai-completions streaming compatibility: force `compat.supportsUsageInStreaming=false` for non-native OpenAI-compatible endpoints during model normalization, preventing usage-only stream chunks from triggering `choices[0]` parser crashes in provider streams. (#8714) Thanks @nonanon1.
|
||||
- TUI/token copy-safety rendering: treat long credential-like mixed alphanumeric tokens (including quoted forms) as copy-sensitive in render sanitization so formatter hard-wrap guards no longer inject visible spaces into auth-style values before display. (#26710) Thanks @jasonthane.
|
||||
- WhatsApp/self-chat response prefix fallback: stop forcing `"[openclaw]"` as the implicit outbound response prefix when no identity name or response prefix is configured, so blank/default prefix settings no longer inject branding text unexpectedly in self-chat flows. (#27962) Thanks @ecanmor.
|
||||
- Memory/QMD search result decoding: accept `qmd search` hits that only include `file` URIs (for example `qmd://collection/path.md`) without `docid`, resolve them through managed collection roots, and keep multi-collection results keyed by file fallback so valid QMD hits no longer collapse to empty `memory_search` output. (#28181) Thanks @0x76696265.
|
||||
|
||||
@@ -23,6 +23,11 @@ function supportsDeveloperRole(model: Model<Api>): boolean | undefined {
|
||||
return (model.compat as { supportsDeveloperRole?: boolean } | undefined)?.supportsDeveloperRole;
|
||||
}
|
||||
|
||||
function supportsUsageInStreaming(model: Model<Api>): boolean | undefined {
|
||||
return (model.compat as { supportsUsageInStreaming?: boolean } | undefined)
|
||||
?.supportsUsageInStreaming;
|
||||
}
|
||||
|
||||
function createTemplateModel(provider: string, id: string): Model<Api> {
|
||||
return {
|
||||
id,
|
||||
@@ -82,6 +87,13 @@ function expectSupportsDeveloperRoleForcedOff(overrides?: Partial<Model<Api>>):
|
||||
expect(supportsDeveloperRole(normalized)).toBe(false);
|
||||
}
|
||||
|
||||
function expectSupportsUsageInStreamingForcedOff(overrides?: Partial<Model<Api>>): void {
|
||||
const model = { ...baseModel(), ...overrides };
|
||||
delete (model as { compat?: unknown }).compat;
|
||||
const normalized = normalizeModelCompat(model as Model<Api>);
|
||||
expect(supportsUsageInStreaming(normalized)).toBe(false);
|
||||
}
|
||||
|
||||
function expectResolvedForwardCompat(
|
||||
model: Model<Api> | undefined,
|
||||
expected: { provider: string; id: string },
|
||||
@@ -207,6 +219,13 @@ describe("normalizeModelCompat", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("forces supportsUsageInStreaming off for generic custom openai-completions provider", () => {
|
||||
expectSupportsUsageInStreamingForcedOff({
|
||||
provider: "custom-cpa",
|
||||
baseUrl: "https://cpa.example.com/v1",
|
||||
});
|
||||
});
|
||||
|
||||
it("forces supportsDeveloperRole off for Qwen proxy via openai-completions", () => {
|
||||
expectSupportsDeveloperRoleForcedOff({
|
||||
provider: "qwen-proxy",
|
||||
@@ -243,6 +262,17 @@ describe("normalizeModelCompat", () => {
|
||||
expect(supportsDeveloperRole(normalized)).toBe(false);
|
||||
});
|
||||
|
||||
it("overrides explicit supportsUsageInStreaming true on non-native endpoints", () => {
|
||||
const model = {
|
||||
...baseModel(),
|
||||
provider: "custom-cpa",
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
compat: { supportsUsageInStreaming: true },
|
||||
};
|
||||
const normalized = normalizeModelCompat(model);
|
||||
expect(supportsUsageInStreaming(normalized)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not mutate caller model when forcing supportsDeveloperRole off", () => {
|
||||
const model = {
|
||||
...baseModel(),
|
||||
@@ -253,14 +283,17 @@ describe("normalizeModelCompat", () => {
|
||||
const normalized = normalizeModelCompat(model);
|
||||
expect(normalized).not.toBe(model);
|
||||
expect(supportsDeveloperRole(model)).toBeUndefined();
|
||||
expect(supportsUsageInStreaming(model)).toBeUndefined();
|
||||
expect(supportsDeveloperRole(normalized)).toBe(false);
|
||||
expect(supportsUsageInStreaming(normalized)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not override explicit compat false", () => {
|
||||
const model = baseModel();
|
||||
model.compat = { supportsDeveloperRole: false };
|
||||
model.compat = { supportsDeveloperRole: false, supportsUsageInStreaming: false };
|
||||
const normalized = normalizeModelCompat(model);
|
||||
expect(supportsDeveloperRole(normalized)).toBe(false);
|
||||
expect(supportsUsageInStreaming(normalized)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -52,28 +52,28 @@ export function normalizeModelCompat(model: Model<Api>): Model<Api> {
|
||||
return model;
|
||||
}
|
||||
|
||||
// The `developer` message role is an OpenAI-native convention. All other
|
||||
// openai-completions backends (proxies, Qwen, GLM, DeepSeek, Kimi, etc.)
|
||||
// only recognise `system`. Force supportsDeveloperRole=false for any model
|
||||
// whose baseUrl is not a known native OpenAI endpoint, unless the caller
|
||||
// has already pinned the value explicitly.
|
||||
// The `developer` role and stream usage chunks are OpenAI-native behaviors.
|
||||
// Many OpenAI-compatible backends reject `developer` and/or emit usage-only
|
||||
// chunks that break strict parsers expecting choices[0]. For non-native
|
||||
// openai-completions endpoints, force both compat flags off.
|
||||
const compat = model.compat ?? undefined;
|
||||
if (compat?.supportsDeveloperRole === false) {
|
||||
return model;
|
||||
}
|
||||
// When baseUrl is empty the pi-ai library defaults to api.openai.com, so
|
||||
// leave compat unchanged and let the existing default behaviour apply.
|
||||
// Note: an explicit supportsDeveloperRole: true is intentionally overridden
|
||||
// here for non-native endpoints — those backends would return a 400 if we
|
||||
// sent `developer`, so safety takes precedence over the caller's hint.
|
||||
// leave compat unchanged and let default native behavior apply.
|
||||
// Note: explicit true values are intentionally overridden for non-native
|
||||
// endpoints for safety.
|
||||
const needsForce = baseUrl ? !isOpenAINativeEndpoint(baseUrl) : false;
|
||||
if (!needsForce) {
|
||||
return model;
|
||||
}
|
||||
if (compat?.supportsDeveloperRole === false && compat?.supportsUsageInStreaming === false) {
|
||||
return model;
|
||||
}
|
||||
|
||||
// Return a new object — do not mutate the caller's model reference.
|
||||
return {
|
||||
...model,
|
||||
compat: compat ? { ...compat, supportsDeveloperRole: false } : { supportsDeveloperRole: false },
|
||||
compat: compat
|
||||
? { ...compat, supportsDeveloperRole: false, supportsUsageInStreaming: false }
|
||||
: { supportsDeveloperRole: false, supportsUsageInStreaming: false },
|
||||
} as typeof model;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user