mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(ai): default unknown providers to non-strict tools (closes #9816)
This commit is contained in:
parent
63787ee6ba
commit
890f920884
6 changed files with 87 additions and 5 deletions
|
|
@ -6,6 +6,10 @@
|
|||
|
||||
- Added model image-input limit and cache-safe resize metadata to the generated catalog ([#9631](https://github.com/earendil-works/pi/issues/9631)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed unknown OpenAI-compatible Chat Completions endpoints receiving strict tool schemas unless they explicitly advertise support, while preserving strict tools for capable built-in models ([#9816](https://github.com/earendil-works/pi/issues/9816)).
|
||||
|
||||
## [0.86.1] - 2026-09-20
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1213,7 +1213,7 @@ interface OpenAICompletionsCompat {
|
|||
supportsDeveloperRole?: boolean; // Whether provider supports `developer` role vs `system` (default: true)
|
||||
supportsReasoningEffort?: boolean; // Whether provider supports `reasoning_effort` (default: true)
|
||||
supportsUsageInStreaming?: boolean; // Whether provider supports `stream_options: { include_usage: true }` (default: true)
|
||||
supportsStrictMode?: boolean; // Whether provider supports `strict` in tool definitions (default: true)
|
||||
supportsStrictMode?: boolean; // Whether provider supports `strict` in tool definitions (default: false; enabled in metadata for capable built-in models)
|
||||
supportsOpenAIGrammarTools?: boolean; // Whether to emit OpenAI custom Lark/regex grammar tools; false falls back to normal function tools (default: false; the generated catalog enables it for capable models)
|
||||
supportsMidConvoSystemMessages?: boolean; // Whether the model accepts system messages after the conversation started; false folds them into the leading prompt (default: false; the generated catalog enables it for verified models)
|
||||
supportsMidConvoToolAdditions?: boolean; // Whether system messages can add tools mid-conversation via Kimi-style `tools` system messages; requires supportsMidConvoSystemMessages (default: false)
|
||||
|
|
|
|||
|
|
@ -640,7 +640,7 @@ const OPENAI_COMPLETIONS_DEFAULT_COMPAT = {
|
|||
chatTemplateKwargs: {},
|
||||
chatTemplateArgs: {},
|
||||
zaiToolStream: false,
|
||||
supportsStrictMode: true,
|
||||
supportsStrictMode: false,
|
||||
supportsOpenAIGrammarTools: false,
|
||||
supportsMidConvoSystemMessages: false,
|
||||
supportsMidConvoToolAdditions: false,
|
||||
|
|
@ -744,6 +744,7 @@ function detectOpenAICompletionsCompat(model: Model<"openai-completions">): Open
|
|||
chatTemplateKwargs: {},
|
||||
chatTemplateArgs: {},
|
||||
zaiToolStream: false,
|
||||
// Preserve built-in behavior as explicit metadata against the conservative runtime default.
|
||||
supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia && !isCerebras,
|
||||
supportsOpenAIGrammarTools: false,
|
||||
supportsMidConvoSystemMessages: false,
|
||||
|
|
|
|||
|
|
@ -1661,7 +1661,8 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
|||
zaiToolStream: false,
|
||||
supportsThinkingTokenBudget: false,
|
||||
thinkingTokenBudgetField: undefined,
|
||||
supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia && !isCerebras,
|
||||
// OpenAI compatibility alone does not imply strict JSON-schema tool support.
|
||||
supportsStrictMode: false,
|
||||
supportsOpenAIGrammarTools: false,
|
||||
supportsMidConvoSystemMessages: false,
|
||||
supportsMidConvoToolAdditions: false,
|
||||
|
|
|
|||
|
|
@ -731,7 +731,7 @@ export interface OpenAICompletionsCompat {
|
|||
supportsMidConvoSystemMessages?: boolean;
|
||||
/** Whether system messages can introduce additional tools mid-conversation. Requires `supportsMidConvoSystemMessages`. Default: false; the generated model catalog enables it for capable models. */
|
||||
supportsMidConvoToolAdditions?: boolean;
|
||||
/** Whether the provider supports the `strict` field in tool definitions. Default: true. */
|
||||
/** Whether the provider supports the `strict` field in tool definitions. Default: false; generated capable models enable it explicitly. */
|
||||
supportsStrictMode?: boolean;
|
||||
/** Cache control convention for prompt caching. "anthropic" applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user, assistant, or tool-result text content. */
|
||||
cacheControlFormat?: "anthropic";
|
||||
|
|
|
|||
|
|
@ -219,6 +219,82 @@ describe("openai-completions tool_choice", () => {
|
|||
expect("strict" in (tool ?? {})).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults unknown OpenAI-compatible endpoints to non-strict tools", async () => {
|
||||
// Regression test for #9816.
|
||||
const model = {
|
||||
...localOpenAICompletionsModel,
|
||||
id: "local-model",
|
||||
name: "Local Model",
|
||||
} satisfies Model<"openai-completions">;
|
||||
const tool: Tool = {
|
||||
name: "ping",
|
||||
description: "Ping tool",
|
||||
parameters: Type.Object({
|
||||
required: Type.String(),
|
||||
optional: Type.Optional(Type.String()),
|
||||
}),
|
||||
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
||||
};
|
||||
let payload: unknown;
|
||||
|
||||
await streamSimple(
|
||||
model,
|
||||
{
|
||||
messages: [{ role: "user", content: "Call ping", timestamp: Date.now() }],
|
||||
tools: [tool],
|
||||
},
|
||||
{
|
||||
apiKey: "test",
|
||||
onPayload: (params: unknown) => {
|
||||
payload = params;
|
||||
},
|
||||
},
|
||||
).result();
|
||||
|
||||
const params = (payload ?? mockState.lastParams) as {
|
||||
tools?: Array<{ function?: { strict?: boolean; parameters?: { required?: string[] } } }>;
|
||||
};
|
||||
const functionTool = params.tools?.[0]?.function;
|
||||
expect(functionTool).not.toHaveProperty("strict");
|
||||
expect(functionTool?.parameters?.required).toEqual(["required"]);
|
||||
});
|
||||
|
||||
it("preserves strict tools for capable built-in Chat Completions models", async () => {
|
||||
const model = getModel("groq", "openai/gpt-oss-20b")!;
|
||||
expect(model.compat?.supportsStrictMode).toBe(true);
|
||||
const tool: Tool = {
|
||||
name: "ping",
|
||||
description: "Ping tool",
|
||||
parameters: Type.Object({
|
||||
required: Type.String(),
|
||||
optional: Type.Optional(Type.String()),
|
||||
}),
|
||||
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
||||
};
|
||||
let payload: unknown;
|
||||
|
||||
await streamSimple(
|
||||
model,
|
||||
{
|
||||
messages: [{ role: "user", content: "Call ping", timestamp: Date.now() }],
|
||||
tools: [tool],
|
||||
},
|
||||
{
|
||||
apiKey: "test",
|
||||
onPayload: (params: unknown) => {
|
||||
payload = params;
|
||||
},
|
||||
},
|
||||
).result();
|
||||
|
||||
const params = (payload ?? mockState.lastParams) as {
|
||||
tools?: Array<{ function?: { strict?: boolean; parameters?: { required?: string[] } } }>;
|
||||
};
|
||||
const functionTool = params.tools?.[0]?.function;
|
||||
expect(functionTool?.strict).toBe(true);
|
||||
expect(functionTool?.parameters?.required).toEqual(["required", "optional"]);
|
||||
});
|
||||
|
||||
it("maps Groq Qwen reasoning levels to default reasoning_effort", async () => {
|
||||
const model = getModel("groq", "qwen/qwen3.6-27b")!;
|
||||
let payload: unknown;
|
||||
|
|
@ -1799,7 +1875,7 @@ describe("openai-completions tool_choice", () => {
|
|||
thinkingFormat: "ant-ling",
|
||||
supportsLongCacheRetention: false,
|
||||
});
|
||||
expect(model.compat?.supportsStrictMode).toBeUndefined();
|
||||
expect(model.compat?.supportsStrictMode).toBe(true);
|
||||
expect(model.compat?.requiresReasoningContentOnAssistantMessages).toBeUndefined();
|
||||
|
||||
await streamSimple(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue