mirror of https://github.com/openclaw/openclaw.git
fix: Disable strict mode tools for non-native openai-completions compatible APIs (#45497)
Merged via squash.
Prepared head SHA: 20fe05fe74
Co-authored-by: sahancava <57447079+sahancava@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
This commit is contained in:
parent
a2d73be3a4
commit
9616d1e8ba
|
|
@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
|
||||||
- Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`.
|
- Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`.
|
||||||
- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. Thanks @vincentkoc.
|
- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. Thanks @vincentkoc.
|
||||||
- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
|
- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
|
||||||
|
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@ function supportsUsageInStreaming(model: Model<Api>): boolean | undefined {
|
||||||
?.supportsUsageInStreaming;
|
?.supportsUsageInStreaming;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function supportsStrictMode(model: Model<Api>): boolean | undefined {
|
||||||
|
return (model.compat as { supportsStrictMode?: boolean } | undefined)?.supportsStrictMode;
|
||||||
|
}
|
||||||
|
|
||||||
function createTemplateModel(provider: string, id: string): Model<Api> {
|
function createTemplateModel(provider: string, id: string): Model<Api> {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
|
@ -94,6 +98,13 @@ function expectSupportsUsageInStreamingForcedOff(overrides?: Partial<Model<Api>>
|
||||||
expect(supportsUsageInStreaming(normalized)).toBe(false);
|
expect(supportsUsageInStreaming(normalized)).toBe(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expectSupportsStrictModeForcedOff(overrides?: Partial<Model<Api>>): void {
|
||||||
|
const model = { ...baseModel(), ...overrides };
|
||||||
|
delete (model as { compat?: unknown }).compat;
|
||||||
|
const normalized = normalizeModelCompat(model as Model<Api>);
|
||||||
|
expect(supportsStrictMode(normalized)).toBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
function expectResolvedForwardCompat(
|
function expectResolvedForwardCompat(
|
||||||
model: Model<Api> | undefined,
|
model: Model<Api> | undefined,
|
||||||
expected: { provider: string; id: string },
|
expected: { provider: string; id: string },
|
||||||
|
|
@ -226,6 +237,17 @@ describe("normalizeModelCompat", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("forces supportsStrictMode off for z.ai models", () => {
|
||||||
|
expectSupportsStrictModeForcedOff();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forces supportsStrictMode off for custom openai-completions provider", () => {
|
||||||
|
expectSupportsStrictModeForcedOff({
|
||||||
|
provider: "custom-cpa",
|
||||||
|
baseUrl: "https://cpa.example.com/v1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("forces supportsDeveloperRole off for Qwen proxy via openai-completions", () => {
|
it("forces supportsDeveloperRole off for Qwen proxy via openai-completions", () => {
|
||||||
expectSupportsDeveloperRoleForcedOff({
|
expectSupportsDeveloperRoleForcedOff({
|
||||||
provider: "qwen-proxy",
|
provider: "qwen-proxy",
|
||||||
|
|
@ -283,6 +305,18 @@ describe("normalizeModelCompat", () => {
|
||||||
const normalized = normalizeModelCompat(model);
|
const normalized = normalizeModelCompat(model);
|
||||||
expect(supportsDeveloperRole(normalized)).toBe(false);
|
expect(supportsDeveloperRole(normalized)).toBe(false);
|
||||||
expect(supportsUsageInStreaming(normalized)).toBe(false);
|
expect(supportsUsageInStreaming(normalized)).toBe(false);
|
||||||
|
expect(supportsStrictMode(normalized)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects explicit supportsStrictMode true on non-native endpoints", () => {
|
||||||
|
const model = {
|
||||||
|
...baseModel(),
|
||||||
|
provider: "custom-cpa",
|
||||||
|
baseUrl: "https://proxy.example.com/v1",
|
||||||
|
compat: { supportsStrictMode: true },
|
||||||
|
};
|
||||||
|
const normalized = normalizeModelCompat(model);
|
||||||
|
expect(supportsStrictMode(normalized)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not mutate caller model when forcing supportsDeveloperRole off", () => {
|
it("does not mutate caller model when forcing supportsDeveloperRole off", () => {
|
||||||
|
|
@ -296,16 +330,23 @@ describe("normalizeModelCompat", () => {
|
||||||
expect(normalized).not.toBe(model);
|
expect(normalized).not.toBe(model);
|
||||||
expect(supportsDeveloperRole(model)).toBeUndefined();
|
expect(supportsDeveloperRole(model)).toBeUndefined();
|
||||||
expect(supportsUsageInStreaming(model)).toBeUndefined();
|
expect(supportsUsageInStreaming(model)).toBeUndefined();
|
||||||
|
expect(supportsStrictMode(model)).toBeUndefined();
|
||||||
expect(supportsDeveloperRole(normalized)).toBe(false);
|
expect(supportsDeveloperRole(normalized)).toBe(false);
|
||||||
expect(supportsUsageInStreaming(normalized)).toBe(false);
|
expect(supportsUsageInStreaming(normalized)).toBe(false);
|
||||||
|
expect(supportsStrictMode(normalized)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not override explicit compat false", () => {
|
it("does not override explicit compat false", () => {
|
||||||
const model = baseModel();
|
const model = baseModel();
|
||||||
model.compat = { supportsDeveloperRole: false, supportsUsageInStreaming: false };
|
model.compat = {
|
||||||
|
supportsDeveloperRole: false,
|
||||||
|
supportsUsageInStreaming: false,
|
||||||
|
supportsStrictMode: false,
|
||||||
|
};
|
||||||
const normalized = normalizeModelCompat(model);
|
const normalized = normalizeModelCompat(model);
|
||||||
expect(supportsDeveloperRole(normalized)).toBe(false);
|
expect(supportsDeveloperRole(normalized)).toBe(false);
|
||||||
expect(supportsUsageInStreaming(normalized)).toBe(false);
|
expect(supportsUsageInStreaming(normalized)).toBe(false);
|
||||||
|
expect(supportsStrictMode(normalized)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,10 @@ export function normalizeModelCompat(model: Model<Api>): Model<Api> {
|
||||||
|
|
||||||
// The `developer` role and stream usage chunks are OpenAI-native behaviors.
|
// The `developer` role and stream usage chunks are OpenAI-native behaviors.
|
||||||
// Many OpenAI-compatible backends reject `developer` and/or emit usage-only
|
// Many OpenAI-compatible backends reject `developer` and/or emit usage-only
|
||||||
// chunks that break strict parsers expecting choices[0]. For non-native
|
// chunks that break strict parsers expecting choices[0]. Additionally, the
|
||||||
// openai-completions endpoints, force both compat flags off — unless the
|
// `strict` boolean inside tools validation is rejected by several providers
|
||||||
// user has explicitly opted in via their model config.
|
// causing tool calls to be ignored. For non-native openai-completions endpoints,
|
||||||
|
// default these compat flags off unless explicitly opted in.
|
||||||
const compat = model.compat ?? undefined;
|
const compat = model.compat ?? undefined;
|
||||||
// When baseUrl is empty the pi-ai library defaults to api.openai.com, so
|
// When baseUrl is empty the pi-ai library defaults to api.openai.com, so
|
||||||
// leave compat unchanged and let default native behavior apply.
|
// leave compat unchanged and let default native behavior apply.
|
||||||
|
|
@ -64,13 +65,14 @@ export function normalizeModelCompat(model: Model<Api>): Model<Api> {
|
||||||
if (!needsForce) {
|
if (!needsForce) {
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Respect explicit user overrides: if the user has set a compat flag to
|
|
||||||
// true in their model definition, they know their endpoint supports it.
|
|
||||||
const forcedDeveloperRole = compat?.supportsDeveloperRole === true;
|
const forcedDeveloperRole = compat?.supportsDeveloperRole === true;
|
||||||
const forcedUsageStreaming = compat?.supportsUsageInStreaming === true;
|
const forcedUsageStreaming = compat?.supportsUsageInStreaming === true;
|
||||||
|
const targetStrictMode = compat?.supportsStrictMode ?? false;
|
||||||
if (forcedDeveloperRole && forcedUsageStreaming) {
|
if (
|
||||||
|
compat?.supportsDeveloperRole !== undefined &&
|
||||||
|
compat?.supportsUsageInStreaming !== undefined &&
|
||||||
|
compat?.supportsStrictMode !== undefined
|
||||||
|
) {
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,7 +84,12 @@ export function normalizeModelCompat(model: Model<Api>): Model<Api> {
|
||||||
...compat,
|
...compat,
|
||||||
supportsDeveloperRole: forcedDeveloperRole || false,
|
supportsDeveloperRole: forcedDeveloperRole || false,
|
||||||
supportsUsageInStreaming: forcedUsageStreaming || false,
|
supportsUsageInStreaming: forcedUsageStreaming || false,
|
||||||
|
supportsStrictMode: targetStrictMode,
|
||||||
}
|
}
|
||||||
: { supportsDeveloperRole: false, supportsUsageInStreaming: false },
|
: {
|
||||||
|
supportsDeveloperRole: false,
|
||||||
|
supportsUsageInStreaming: false,
|
||||||
|
supportsStrictMode: false,
|
||||||
|
},
|
||||||
} as typeof model;
|
} as typeof model;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue