@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added configurable `chat-template` thinking support for OpenAI-compatible providers that use `chat_template_kwargs`, such as DeepSeek models behind vLLM ([#5673](https://github.com/earendil-works/pi/issues/5673)).
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed GitHub Copilot OAuth model availability to use the authenticated account's model picker catalog ([#5897](https://github.com/earendil-works/pi/issues/5897)).
|
- Fixed GitHub Copilot OAuth model availability to use the authenticated account's model picker catalog ([#5897](https://github.com/earendil-works/pi/issues/5897)).
|
||||||
|
|||||||
@@ -967,7 +967,8 @@ interface OpenAICompletionsCompat {
|
|||||||
requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false)
|
requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false)
|
||||||
requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false)
|
requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false)
|
||||||
requiresReasoningContentOnAssistantMessages?: boolean; // Whether all replayed assistant messages must include empty reasoning_content when reasoning is enabled (default: auto-detected for DeepSeek)
|
requiresReasoningContentOnAssistantMessages?: boolean; // Whether all replayed assistant messages must include empty reasoning_content when reasoning is enabled (default: auto-detected for DeepSeek)
|
||||||
thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'zai' | 'qwen' | 'qwen-chat-template' | 'string-thinking' | 'ant-ling'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort when supported, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'zai' uses enable_thinking, 'qwen' uses enable_thinking, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking, 'string-thinking' uses top-level thinking, 'ant-ling' uses reasoning: { effort } only for mapped efforts (default: openai)
|
thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'zai' | 'qwen' | 'chat-template' | 'qwen-chat-template' | 'string-thinking' | 'ant-ling'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort when supported, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'zai' uses thinking: { type }, 'qwen' uses enable_thinking, 'chat-template' uses configurable chat_template_kwargs, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking and preserve_thinking, 'string-thinking' uses top-level thinking, 'ant-ling' uses reasoning: { effort } only for mapped efforts (default: openai)
|
||||||
|
chatTemplateKwargs?: Record<string, string | number | boolean | null | { '$var': 'thinking.enabled' | 'thinking.effort'; omitWhenOff?: boolean }>; // chat_template_kwargs values; use $var for pi-controlled thinking values
|
||||||
cacheControlFormat?: 'anthropic'; // Anthropic-style cache_control on system prompt, last tool, and last user/assistant text content
|
cacheControlFormat?: 'anthropic'; // Anthropic-style cache_control on system prompt, last tool, and last user/assistant text content
|
||||||
openRouterRouting?: OpenRouterRouting; // OpenRouter routing preferences (default: {})
|
openRouterRouting?: OpenRouterRouting; // OpenRouter routing preferences (default: {})
|
||||||
vercelGatewayRouting?: VercelGatewayRouting; // Vercel AI Gateway routing preferences (default: {})
|
vercelGatewayRouting?: VercelGatewayRouting; // Vercel AI Gateway routing preferences (default: {})
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { calculateCost, clampThinkingLevel } from "../models.ts";
|
|||||||
import type {
|
import type {
|
||||||
AssistantMessage,
|
AssistantMessage,
|
||||||
CacheRetention,
|
CacheRetention,
|
||||||
|
ChatTemplateKwargValue,
|
||||||
Context,
|
Context,
|
||||||
ImageContent,
|
ImageContent,
|
||||||
Message,
|
Message,
|
||||||
@@ -91,6 +92,8 @@ type ResolvedOpenAICompletionsCompat = Omit<Required<OpenAICompletionsCompat>, "
|
|||||||
cacheControlFormat?: OpenAICompletionsCompat["cacheControlFormat"];
|
cacheControlFormat?: OpenAICompletionsCompat["cacheControlFormat"];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ResolvedChatTemplateKwargValue = string | number | boolean | null;
|
||||||
|
|
||||||
type ChatCompletionInstructionMessageParam = ChatCompletionDeveloperMessageParam | ChatCompletionSystemMessageParam;
|
type ChatCompletionInstructionMessageParam = ChatCompletionDeveloperMessageParam | ChatCompletionSystemMessageParam;
|
||||||
|
|
||||||
type ChatCompletionTextPartWithCacheControl = ChatCompletionContentPartText & {
|
type ChatCompletionTextPartWithCacheControl = ChatCompletionContentPartText & {
|
||||||
@@ -585,6 +588,11 @@ function buildParams(
|
|||||||
enable_thinking: !!options?.reasoningEffort,
|
enable_thinking: !!options?.reasoningEffort,
|
||||||
preserve_thinking: true,
|
preserve_thinking: true,
|
||||||
};
|
};
|
||||||
|
} else if (compat.thinkingFormat === "chat-template" && model.reasoning) {
|
||||||
|
const chatTemplateKwargs = buildChatTemplateKwargs(model, options, compat);
|
||||||
|
if (chatTemplateKwargs) {
|
||||||
|
(params as any).chat_template_kwargs = chatTemplateKwargs;
|
||||||
|
}
|
||||||
} else if (compat.thinkingFormat === "deepseek" && model.reasoning) {
|
} else if (compat.thinkingFormat === "deepseek" && model.reasoning) {
|
||||||
if (options?.reasoningEffort) {
|
if (options?.reasoningEffort) {
|
||||||
(params as any).thinking = { type: "enabled" };
|
(params as any).thinking = { type: "enabled" };
|
||||||
@@ -655,6 +663,44 @@ function buildParams(
|
|||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildChatTemplateKwargs(
|
||||||
|
model: Model<"openai-completions">,
|
||||||
|
options: OpenAICompletionsOptions | undefined,
|
||||||
|
compat: ResolvedOpenAICompletionsCompat,
|
||||||
|
): Record<string, ResolvedChatTemplateKwargValue> | undefined {
|
||||||
|
const kwargs: Record<string, ResolvedChatTemplateKwargValue> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(compat.chatTemplateKwargs)) {
|
||||||
|
const resolved = resolveChatTemplateKwargValue(model, options, value);
|
||||||
|
if (resolved !== undefined) {
|
||||||
|
kwargs[key] = resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(kwargs).length > 0 ? kwargs : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveChatTemplateKwargValue(
|
||||||
|
model: Model<"openai-completions">,
|
||||||
|
options: OpenAICompletionsOptions | undefined,
|
||||||
|
value: ChatTemplateKwargValue,
|
||||||
|
): ResolvedChatTemplateKwargValue | undefined {
|
||||||
|
if (typeof value !== "object" || value === null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reasoningEffort = options?.reasoningEffort;
|
||||||
|
if (!reasoningEffort && value.omitWhenOff) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (value.$var === "thinking.enabled") {
|
||||||
|
return !!reasoningEffort;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedValue = reasoningEffort ? model.thinkingLevelMap?.[reasoningEffort] : model.thinkingLevelMap?.off;
|
||||||
|
return mappedValue === undefined ? reasoningEffort : typeof mappedValue === "string" ? mappedValue : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function getCompatCacheControl(
|
function getCompatCacheControl(
|
||||||
compat: ResolvedOpenAICompletionsCompat,
|
compat: ResolvedOpenAICompletionsCompat,
|
||||||
cacheRetention: CacheRetention,
|
cacheRetention: CacheRetention,
|
||||||
@@ -1167,6 +1213,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
|||||||
: "openai",
|
: "openai",
|
||||||
openRouterRouting: {},
|
openRouterRouting: {},
|
||||||
vercelGatewayRouting: {},
|
vercelGatewayRouting: {},
|
||||||
|
chatTemplateKwargs: {},
|
||||||
zaiToolStream: false,
|
zaiToolStream: false,
|
||||||
supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia,
|
supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia,
|
||||||
cacheControlFormat,
|
cacheControlFormat,
|
||||||
@@ -1205,6 +1252,7 @@ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletion
|
|||||||
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
|
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
|
||||||
openRouterRouting: model.compat.openRouterRouting ?? {},
|
openRouterRouting: model.compat.openRouterRouting ?? {},
|
||||||
vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,
|
vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,
|
||||||
|
chatTemplateKwargs: model.compat.chatTemplateKwargs ?? detected.chatTemplateKwargs,
|
||||||
zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream,
|
zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream,
|
||||||
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
|
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
|
||||||
cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat,
|
cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat,
|
||||||
|
|||||||
@@ -65,6 +65,15 @@ export type ImagesProvider = KnownImagesProvider | string;
|
|||||||
export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh";
|
export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||||
export type ModelThinkingLevel = "off" | ThinkingLevel;
|
export type ModelThinkingLevel = "off" | ThinkingLevel;
|
||||||
export type ThinkingLevelMap = Partial<Record<ModelThinkingLevel, string | null>>;
|
export type ThinkingLevelMap = Partial<Record<ModelThinkingLevel, string | null>>;
|
||||||
|
export type ChatTemplateKwargValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| {
|
||||||
|
$var: "thinking.enabled" | "thinking.effort";
|
||||||
|
omitWhenOff?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/** Token budgets for each thinking level (token-based providers only) */
|
/** Token budgets for each thinking level (token-based providers only) */
|
||||||
export interface ThinkingBudgets {
|
export interface ThinkingBudgets {
|
||||||
@@ -403,7 +412,7 @@ export interface OpenAICompletionsCompat {
|
|||||||
requiresThinkingAsText?: boolean;
|
requiresThinkingAsText?: boolean;
|
||||||
/** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */
|
/** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */
|
||||||
requiresReasoningContentOnAssistantMessages?: boolean;
|
requiresReasoningContentOnAssistantMessages?: boolean;
|
||||||
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort when supported, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses thinking: { type }, "qwen" uses top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking, "string-thinking" uses top-level thinking: string, and "ant-ling" uses reasoning: { effort } only when the mapped effort is non-null. Default: "openai". */
|
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort when supported, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses thinking: { type }, "qwen" uses top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking and preserve_thinking, "chat-template" uses configurable chat_template_kwargs, "string-thinking" uses top-level thinking: string, and "ant-ling" uses reasoning: { effort } only when the mapped effort is non-null. Default: "openai". */
|
||||||
thinkingFormat?:
|
thinkingFormat?:
|
||||||
| "openai"
|
| "openai"
|
||||||
| "openrouter"
|
| "openrouter"
|
||||||
@@ -411,9 +420,12 @@ export interface OpenAICompletionsCompat {
|
|||||||
| "together"
|
| "together"
|
||||||
| "zai"
|
| "zai"
|
||||||
| "qwen"
|
| "qwen"
|
||||||
|
| "chat-template"
|
||||||
| "qwen-chat-template"
|
| "qwen-chat-template"
|
||||||
| "string-thinking"
|
| "string-thinking"
|
||||||
| "ant-ling";
|
| "ant-ling";
|
||||||
|
/** Kwargs to send as `chat_template_kwargs` when `thinkingFormat` is `chat-template`. Use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values. */
|
||||||
|
chatTemplateKwargs?: Record<string, ChatTemplateKwargValue>;
|
||||||
/** OpenRouter-compatible routing preferences sent as the `provider` request field. */
|
/** OpenRouter-compatible routing preferences sent as the `provider` request field. */
|
||||||
openRouterRouting?: OpenRouterRouting;
|
openRouterRouting?: OpenRouterRouting;
|
||||||
/** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */
|
/** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const compat = {
|
|||||||
thinkingFormat: "openai",
|
thinkingFormat: "openai",
|
||||||
openRouterRouting: {},
|
openRouterRouting: {},
|
||||||
vercelGatewayRouting: {},
|
vercelGatewayRouting: {},
|
||||||
|
chatTemplateKwargs: {},
|
||||||
zaiToolStream: false,
|
zaiToolStream: false,
|
||||||
supportsStrictMode: true,
|
supportsStrictMode: true,
|
||||||
cacheControlFormat: undefined,
|
cacheControlFormat: undefined,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import { stream, streamSimple } from "../src/index.ts";
|
import { stream, streamSimple } from "../src/index.ts";
|
||||||
import { getModel } from "../src/models.ts";
|
import { getModel } from "../src/models.ts";
|
||||||
import { convertMessages } from "../src/providers/openai-completions.ts";
|
import { convertMessages } from "../src/providers/openai-completions.ts";
|
||||||
import type { AssistantMessage, Model, Tool, ToolResultMessage } from "../src/types.ts";
|
import type { AssistantMessage, Model, SimpleStreamOptions, Tool, ToolResultMessage } from "../src/types.ts";
|
||||||
|
|
||||||
const mockState = vi.hoisted(() => ({
|
const mockState = vi.hoisted(() => ({
|
||||||
lastParams: undefined as unknown,
|
lastParams: undefined as unknown,
|
||||||
@@ -64,6 +64,46 @@ vi.mock("openai", () => {
|
|||||||
return { default: FakeOpenAI };
|
return { default: FakeOpenAI };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const localOpenAICompletionsModel = {
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "local-vllm",
|
||||||
|
baseUrl: "http://localhost:8000/v1",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
contextWindow: 128000,
|
||||||
|
maxTokens: 8192,
|
||||||
|
} satisfies Omit<Model<"openai-completions">, "id" | "name" | "compat">;
|
||||||
|
|
||||||
|
type CapturedParams = {
|
||||||
|
chat_template_kwargs?: Record<string, unknown>;
|
||||||
|
thinking?: unknown;
|
||||||
|
reasoning_effort?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function captureSimpleParams(
|
||||||
|
model: Model<"openai-completions">,
|
||||||
|
reasoning?: SimpleStreamOptions["reasoning"],
|
||||||
|
): Promise<CapturedParams> {
|
||||||
|
let payload: unknown;
|
||||||
|
|
||||||
|
await streamSimple(
|
||||||
|
model,
|
||||||
|
{
|
||||||
|
messages: [{ role: "user", content: "Hi", timestamp: Date.now() }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiKey: "test",
|
||||||
|
reasoning,
|
||||||
|
onPayload: (params: unknown) => {
|
||||||
|
payload = params;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).result();
|
||||||
|
|
||||||
|
return (payload ?? mockState.lastParams) as CapturedParams;
|
||||||
|
}
|
||||||
|
|
||||||
describe("openai-completions tool_choice", () => {
|
describe("openai-completions tool_choice", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockState.lastParams = undefined;
|
mockState.lastParams = undefined;
|
||||||
@@ -1142,6 +1182,7 @@ describe("openai-completions tool_choice", () => {
|
|||||||
thinkingFormat: "openai",
|
thinkingFormat: "openai",
|
||||||
openRouterRouting: {},
|
openRouterRouting: {},
|
||||||
vercelGatewayRouting: {},
|
vercelGatewayRouting: {},
|
||||||
|
chatTemplateKwargs: {},
|
||||||
zaiToolStream: false,
|
zaiToolStream: false,
|
||||||
supportsStrictMode: true,
|
supportsStrictMode: true,
|
||||||
sendSessionAffinityHeaders: false,
|
sendSessionAffinityHeaders: false,
|
||||||
@@ -1449,6 +1490,77 @@ describe("openai-completions tool_choice", () => {
|
|||||||
expect(params.reasoning_effort).toBeUndefined();
|
expect(params.reasoning_effort).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses configurable chat template boolean thinking kwargs", async () => {
|
||||||
|
const model = {
|
||||||
|
...localOpenAICompletionsModel,
|
||||||
|
id: "deepseek-ai/DeepSeek-V3.1",
|
||||||
|
name: "DeepSeek V3.1 via vLLM",
|
||||||
|
compat: {
|
||||||
|
thinkingFormat: "chat-template",
|
||||||
|
supportsReasoningEffort: false,
|
||||||
|
chatTemplateKwargs: { thinking: { $var: "thinking.enabled" } },
|
||||||
|
},
|
||||||
|
} satisfies Model<"openai-completions">;
|
||||||
|
|
||||||
|
for (const testCase of [
|
||||||
|
{ reasoning: "high" as const, expected: true },
|
||||||
|
{ reasoning: undefined, expected: false },
|
||||||
|
]) {
|
||||||
|
const params = await captureSimpleParams(model, testCase.reasoning);
|
||||||
|
|
||||||
|
expect(params.chat_template_kwargs).toEqual({ thinking: testCase.expected });
|
||||||
|
expect(params.thinking).toBeUndefined();
|
||||||
|
expect(params.reasoning_effort).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses qwen chat template thinking kwargs", async () => {
|
||||||
|
const model = {
|
||||||
|
...localOpenAICompletionsModel,
|
||||||
|
id: "Qwen/Qwen3-Coder",
|
||||||
|
name: "Qwen3 Coder via vLLM",
|
||||||
|
compat: {
|
||||||
|
thinkingFormat: "qwen-chat-template",
|
||||||
|
supportsReasoningEffort: false,
|
||||||
|
},
|
||||||
|
} satisfies Model<"openai-completions">;
|
||||||
|
|
||||||
|
for (const testCase of [
|
||||||
|
{ reasoning: "high" as const, expected: true },
|
||||||
|
{ reasoning: undefined, expected: false },
|
||||||
|
]) {
|
||||||
|
const params = await captureSimpleParams(model, testCase.reasoning);
|
||||||
|
|
||||||
|
expect(params.chat_template_kwargs).toEqual({
|
||||||
|
enable_thinking: testCase.expected,
|
||||||
|
preserve_thinking: true,
|
||||||
|
});
|
||||||
|
expect(params.reasoning_effort).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses configurable chat template effort kwargs with static kwargs", async () => {
|
||||||
|
const model = {
|
||||||
|
...localOpenAICompletionsModel,
|
||||||
|
id: "unsloth/gpt-oss-120b-GGUF",
|
||||||
|
name: "GPT OSS via vLLM",
|
||||||
|
thinkingLevelMap: { xhigh: "max" },
|
||||||
|
compat: {
|
||||||
|
thinkingFormat: "chat-template",
|
||||||
|
supportsReasoningEffort: false,
|
||||||
|
chatTemplateKwargs: {
|
||||||
|
preserve_thinking: true,
|
||||||
|
reasoning_effort: { $var: "thinking.effort", omitWhenOff: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Model<"openai-completions">;
|
||||||
|
|
||||||
|
const params = await captureSimpleParams(model, "xhigh");
|
||||||
|
|
||||||
|
expect(params.chat_template_kwargs).toEqual({ preserve_thinking: true, reasoning_effort: "max" });
|
||||||
|
expect(params.reasoning_effort).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("uses Ant Ling compatibility metadata", async () => {
|
it("uses Ant Ling compatibility metadata", async () => {
|
||||||
const model = getModel("ant-ling", "Ring-2.6-1T")!;
|
const model = getModel("ant-ling", "Ring-2.6-1T")!;
|
||||||
let payload: unknown;
|
let payload: unknown;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const compat: Required<OpenAICompletionsCompat> = {
|
|||||||
thinkingFormat: "openai",
|
thinkingFormat: "openai",
|
||||||
openRouterRouting: {},
|
openRouterRouting: {},
|
||||||
vercelGatewayRouting: {},
|
vercelGatewayRouting: {},
|
||||||
|
chatTemplateKwargs: {},
|
||||||
zaiToolStream: false,
|
zaiToolStream: false,
|
||||||
supportsStrictMode: true,
|
supportsStrictMode: true,
|
||||||
cacheControlFormat: "anthropic",
|
cacheControlFormat: "anthropic",
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added inherited configurable `chat-template` thinking support for OpenAI-compatible providers that use `chat_template_kwargs`, such as DeepSeek models behind vLLM ([#5673](https://github.com/earendil-works/pi/issues/5673)).
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed fuzzy `edit` matches to preserve untouched line blocks instead of rewriting the whole file through normalized content ([#5899](https://github.com/earendil-works/pi/issues/5899)).
|
- Fixed fuzzy `edit` matches to preserve untouched line blocks instead of rewriting the whole file through normalized content ([#5899](https://github.com/earendil-works/pi/issues/5899)).
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ models: [{
|
|||||||
}]
|
}]
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `openrouter` for OpenRouter-style `reasoning: { effort }` controls. Use `together` for Together-style `reasoning: { enabled }` controls; with `supportsReasoningEffort`, it also sends `reasoning_effort`. Use `qwen-chat-template` instead for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`.
|
Use `openrouter` for OpenRouter-style `reasoning: { effort }` controls. Use `together` for Together-style `reasoning: { enabled }` controls; with `supportsReasoningEffort`, it also sends `reasoning_effort`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking` and need `preserve_thinking`.
|
||||||
Use `cacheControlFormat: "anthropic"` for OpenAI-compatible providers that expose Anthropic-style prompt caching via `cache_control` on the system prompt, last tool definition, and last user/assistant text content.
|
Use `cacheControlFormat: "anthropic"` for OpenAI-compatible providers that expose Anthropic-style prompt caching via `cache_control` on the system prompt, last tool definition, and last user/assistant text content.
|
||||||
|
|
||||||
For Anthropic-compatible providers using `api: "anthropic-messages"`, set `compat.forceAdaptiveThinking: true` on models or providers whose upstream model requires adaptive thinking (`thinking.type: "adaptive"` plus `output_config.effort`). Built-in adaptive Claude models set this automatically. Set `compat.allowEmptySignature: true` only for providers that emit empty thinking signatures and expect `signature: ""` on replay.
|
For Anthropic-compatible providers using `api: "anthropic-messages"`, set `compat.forceAdaptiveThinking: true` on models or providers whose upstream model requires adaptive thinking (`thinking.type: "adaptive"` plus `output_config.effort`). Built-in adaptive Claude models set this automatically. Set `compat.allowEmptySignature: true` only for providers that emit empty thinking signatures and expect `signature: ""` on replay.
|
||||||
@@ -718,7 +718,8 @@ interface ProviderModelConfig {
|
|||||||
requiresAssistantAfterToolResult?: boolean;
|
requiresAssistantAfterToolResult?: boolean;
|
||||||
requiresThinkingAsText?: boolean;
|
requiresThinkingAsText?: boolean;
|
||||||
requiresReasoningContentOnAssistantMessages?: boolean;
|
requiresReasoningContentOnAssistantMessages?: boolean;
|
||||||
thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "qwen-chat-template";
|
thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "chat-template" | "qwen-chat-template" | "string-thinking" | "ant-ling";
|
||||||
|
chatTemplateKwargs?: Record<string, string | number | boolean | null | { "$var": "thinking.enabled" | "thinking.effort"; omitWhenOff?: boolean }>;
|
||||||
cacheControlFormat?: "anthropic";
|
cacheControlFormat?: "anthropic";
|
||||||
|
|
||||||
// anthropic-messages
|
// anthropic-messages
|
||||||
@@ -732,5 +733,5 @@ interface ProviderModelConfig {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`openrouter` sends `reasoning: { effort }`. `deepseek` sends `thinking: { type: "enabled" | "disabled" }` and `reasoning_effort` when enabled. `together` sends `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`.
|
`openrouter` sends `reasoning: { effort }`. `deepseek` sends `thinking: { type: "enabled" | "disabled" }` and `reasoning_effort` when enabled. `together` sends `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking` and need `preserve_thinking`. Use `chat-template` for configurable `chat_template_kwargs`, for example DeepSeek V3.x behind vLLM with `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }`.
|
||||||
`cacheControlFormat: "anthropic"` applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user/assistant text content.
|
`cacheControlFormat: "anthropic"` applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user/assistant text content.
|
||||||
|
|||||||
@@ -399,14 +399,15 @@ For providers with partial OpenAI compatibility, use the `compat` field.
|
|||||||
| `requiresAssistantAfterToolResult` | Insert an assistant message before a user message after tool results |
|
| `requiresAssistantAfterToolResult` | Insert an assistant message before a user message after tool results |
|
||||||
| `requiresThinkingAsText` | Convert thinking blocks to plain text |
|
| `requiresThinkingAsText` | Convert thinking blocks to plain text |
|
||||||
| `requiresReasoningContentOnAssistantMessages` | Include empty `reasoning_content` on all replayed assistant messages when reasoning is enabled |
|
| `requiresReasoningContentOnAssistantMessages` | Include empty `reasoning_content` on all replayed assistant messages when reasoning is enabled |
|
||||||
| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `zai`, `qwen`, or `qwen-chat-template` thinking parameters |
|
| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `zai`, `qwen`, `chat-template`, or `qwen-chat-template` thinking parameters |
|
||||||
|
| `chatTemplateKwargs` | `chat_template_kwargs` values for `thinkingFormat: "chat-template"`; use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values |
|
||||||
| `cacheControlFormat` | Use Anthropic-style `cache_control` markers on the system prompt, last tool definition, and last user/assistant text content. Currently only `anthropic` is supported. |
|
| `cacheControlFormat` | Use Anthropic-style `cache_control` markers on the system prompt, last tool definition, and last user/assistant text content. Currently only `anthropic` is supported. |
|
||||||
| `supportsStrictMode` | Include the `strict` field in tool definitions |
|
| `supportsStrictMode` | Include the `strict` field in tool definitions |
|
||||||
| `supportsLongCacheRetention` | Whether the provider accepts long cache retention when cache retention is `long`: `prompt_cache_retention: "24h"` for OpenAI prompt caching, or `cache_control.ttl: "1h"` when `cacheControlFormat` is `anthropic`. Default: `true`. |
|
| `supportsLongCacheRetention` | Whether the provider accepts long cache retention when cache retention is `long`: `prompt_cache_retention: "24h"` for OpenAI prompt caching, or `cache_control.ttl: "1h"` when `cacheControlFormat` is `anthropic`. Default: `true`. |
|
||||||
| `openRouterRouting` | OpenRouter provider routing preferences. This object is sent as-is in the `provider` field of the [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection). |
|
| `openRouterRouting` | OpenRouter provider routing preferences. This object is sent as-is in the `provider` field of the [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection). |
|
||||||
| `vercelGatewayRouting` | Vercel AI Gateway routing config for provider selection (`only`, `order`) |
|
| `vercelGatewayRouting` | Vercel AI Gateway routing config for provider selection (`only`, `order`) |
|
||||||
|
|
||||||
`openrouter` uses `reasoning: { effort }`. `together` uses `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` uses top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that require `chat_template_kwargs.enable_thinking`.
|
`openrouter` uses `reasoning: { effort }`. `together` uses `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` uses top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that require `chat_template_kwargs.enable_thinking` and `preserve_thinking`. Use `chat-template` for vLLM/Hugging Face chat templates that need configurable `chat_template_kwargs`, such as `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }` for DeepSeek V3.x templates.
|
||||||
|
|
||||||
`cacheControlFormat: "anthropic"` is for OpenAI-compatible providers that expose Anthropic-style prompt caching through `cache_control` markers on text content and tool definitions.
|
`cacheControlFormat: "anthropic"` is for OpenAI-compatible providers that expose Anthropic-style prompt caching through `cache_control` markers on text content and tool definitions.
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,13 @@ const ThinkingLevelMapSchema = Type.Object({
|
|||||||
xhigh: Type.Optional(ThinkingLevelMapValueSchema),
|
xhigh: Type.Optional(ThinkingLevelMapValueSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ChatTemplateKwargScalarSchema = Type.Union([Type.String(), Type.Number(), Type.Boolean(), Type.Null()]);
|
||||||
|
const ChatTemplateKwargVariableSchema = Type.Object({
|
||||||
|
$var: Type.Union([Type.Literal("thinking.enabled"), Type.Literal("thinking.effort")]),
|
||||||
|
omitWhenOff: Type.Optional(Type.Boolean()),
|
||||||
|
});
|
||||||
|
const ChatTemplateKwargSchema = Type.Union([ChatTemplateKwargScalarSchema, ChatTemplateKwargVariableSchema]);
|
||||||
|
|
||||||
const OpenAICompletionsCompatSchema = Type.Object({
|
const OpenAICompletionsCompatSchema = Type.Object({
|
||||||
supportsStore: Type.Optional(Type.Boolean()),
|
supportsStore: Type.Optional(Type.Boolean()),
|
||||||
supportsDeveloperRole: Type.Optional(Type.Boolean()),
|
supportsDeveloperRole: Type.Optional(Type.Boolean()),
|
||||||
@@ -114,9 +121,13 @@ const OpenAICompletionsCompatSchema = Type.Object({
|
|||||||
Type.Literal("deepseek"),
|
Type.Literal("deepseek"),
|
||||||
Type.Literal("zai"),
|
Type.Literal("zai"),
|
||||||
Type.Literal("qwen"),
|
Type.Literal("qwen"),
|
||||||
|
Type.Literal("chat-template"),
|
||||||
Type.Literal("qwen-chat-template"),
|
Type.Literal("qwen-chat-template"),
|
||||||
|
Type.Literal("string-thinking"),
|
||||||
|
Type.Literal("ant-ling"),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
chatTemplateKwargs: Type.Optional(Type.Record(Type.String(), ChatTemplateKwargSchema)),
|
||||||
cacheControlFormat: Type.Optional(Type.Literal("anthropic")),
|
cacheControlFormat: Type.Optional(Type.Literal("anthropic")),
|
||||||
openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
|
openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
|
||||||
vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),
|
vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),
|
||||||
@@ -289,6 +300,13 @@ function mergeCompat(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (baseCompletions?.chatTemplateKwargs || overrideCompletions.chatTemplateKwargs) {
|
||||||
|
mergedCompletions.chatTemplateKwargs = {
|
||||||
|
...baseCompletions?.chatTemplateKwargs,
|
||||||
|
...overrideCompletions.chatTemplateKwargs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return merged as Model<Api>["compat"];
|
return merged as Model<Api>["compat"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -434,6 +434,43 @@ describe("ModelRegistry", () => {
|
|||||||
expect(compat?.cacheControlFormat).toBe("anthropic");
|
expect(compat?.cacheControlFormat).toBe("anthropic");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("compat schema accepts chat template thinking configuration", () => {
|
||||||
|
writeRawModelsJson({
|
||||||
|
demo: {
|
||||||
|
baseUrl: "https://example.com/v1",
|
||||||
|
apiKey: "DEMO_KEY",
|
||||||
|
api: "openai-completions",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: "demo-model",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
contextWindow: 1000,
|
||||||
|
maxTokens: 100,
|
||||||
|
compat: {
|
||||||
|
thinkingFormat: "chat-template",
|
||||||
|
chatTemplateKwargs: {
|
||||||
|
preserve_thinking: true,
|
||||||
|
thinking: { $var: "thinking.enabled" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
|
||||||
|
const compat = registry.find("demo", "demo-model")?.compat as OpenAICompletionsCompat | undefined;
|
||||||
|
|
||||||
|
expect(registry.getError()).toBeUndefined();
|
||||||
|
expect(compat?.thinkingFormat).toBe("chat-template");
|
||||||
|
expect(compat?.chatTemplateKwargs).toEqual({
|
||||||
|
preserve_thinking: true,
|
||||||
|
thinking: { $var: "thinking.enabled" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("compat schema accepts Anthropic eager tool input streaming flag", () => {
|
test("compat schema accepts Anthropic eager tool input streaming flag", () => {
|
||||||
writeRawModelsJson({
|
writeRawModelsJson({
|
||||||
demo: {
|
demo: {
|
||||||
|
|||||||
Reference in New Issue
Block a user