feat(cross-cutting): added multi-syntax in-band tool-call support for runtime tool conversion
- Added optional Agent and SDK tool-call syntax controls (`toolCallSyntax`, `PI_OWNED_TOOLS`) for owned calls. - Added in-band grammar scanners and renderers for Anthropic, DeepSeek, GLM, Hermes, Kimi, PI, and Qwen3. - Added supportsTools propagation and model schema updates to route unsupported models to fallback syntax. - Replaced stream-markup parsing with syntax-specific in-band scanners and event conversion.
This commit is contained in:
@@ -130,11 +130,13 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
||||
providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "transport">,
|
||||
): Model<TApi> {
|
||||
if (existing) {
|
||||
const supportsTools = model.supportsTools ?? existing.supportsTools;
|
||||
return buildModel({
|
||||
...model,
|
||||
baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
|
||||
headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
|
||||
transport: providerOverride?.transport ?? existing.transport ?? model.transport,
|
||||
...(supportsTools !== undefined ? { supportsTools } : {}),
|
||||
compat: model.compatConfig,
|
||||
} as ModelSpec<TApi>);
|
||||
}
|
||||
@@ -370,6 +372,7 @@ interface ModelPatch {
|
||||
reasoning?: boolean;
|
||||
thinking?: ThinkingConfig;
|
||||
input?: ("text" | "image")[];
|
||||
supportsTools?: boolean;
|
||||
cost?: Partial<Model<Api>["cost"]>;
|
||||
contextWindow?: number;
|
||||
maxTokens?: number;
|
||||
@@ -395,6 +398,7 @@ function applyModelPatch(base: Model<Api>, patch: ModelPatch, transport: ModelTr
|
||||
if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
|
||||
if (patch.thinking !== undefined) result.thinking = patch.thinking;
|
||||
if (patch.input !== undefined) result.input = patch.input;
|
||||
if (patch.supportsTools !== undefined) result.supportsTools = patch.supportsTools;
|
||||
if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
|
||||
if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
|
||||
if (patch.omitMaxOutputTokens !== undefined) result.omitMaxOutputTokens = patch.omitMaxOutputTokens;
|
||||
@@ -506,6 +510,7 @@ function buildCustomModelOverlay(
|
||||
reasoning: modelDef.reasoning,
|
||||
thinking: modelDef.thinking,
|
||||
input: modelDef.input,
|
||||
supportsTools: modelDef.supportsTools,
|
||||
cost: modelDef.cost,
|
||||
contextWindow: modelDef.contextWindow,
|
||||
maxTokens: modelDef.maxTokens,
|
||||
@@ -535,6 +540,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
||||
reference?.cost ??
|
||||
(options.useDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
|
||||
const input = resolvedModel.input ?? reference?.input ?? (options.useDefaults ? ["text"] : undefined);
|
||||
const supportsTools = resolvedModel.supportsTools ?? reference?.supportsTools;
|
||||
return buildModel({
|
||||
id: resolvedModel.id,
|
||||
name: resolvedModel.name ?? (options.useDefaults ? resolvedModel.id : undefined),
|
||||
@@ -544,6 +550,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
||||
reasoning: resolvedModel.reasoning ?? reference?.reasoning ?? (options.useDefaults ? false : undefined),
|
||||
thinking: resolvedModel.thinking ?? reference?.thinking,
|
||||
input: input as ("text" | "image")[],
|
||||
...(supportsTools !== undefined ? { supportsTools } : {}),
|
||||
cost,
|
||||
contextWindow: resolvedModel.contextWindow ?? reference?.contextWindow ?? (options.useDefaults ? 128000 : null),
|
||||
maxTokens: resolvedModel.maxTokens ?? reference?.maxTokens ?? (options.useDefaults ? 16384 : null),
|
||||
@@ -878,10 +885,12 @@ export class ModelRegistry {
|
||||
#mergeResolvedModels(baseModels: Model<Api>[], replacementModels: Model<Api>[]): Model<Api>[] {
|
||||
return mergeByModelKey(baseModels, replacementModels, (existing, replacementModel) => {
|
||||
if (!existing) return replacementModel;
|
||||
const supportsTools = replacementModel.supportsTools ?? existing.supportsTools;
|
||||
return {
|
||||
...replacementModel,
|
||||
contextWindow: replacementModel.contextWindow ?? existing.contextWindow,
|
||||
maxTokens: replacementModel.maxTokens ?? existing.maxTokens,
|
||||
...(supportsTools !== undefined ? { supportsTools } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -2205,6 +2214,7 @@ export interface ProviderConfigInput {
|
||||
reasoning: boolean;
|
||||
thinking?: ThinkingConfig;
|
||||
input: ("text" | "image")[];
|
||||
supportsTools?: boolean;
|
||||
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
|
||||
@@ -133,6 +133,7 @@ const ModelDefinitionSchema = z.object({
|
||||
reasoning: z.boolean().optional(),
|
||||
thinking: ModelThinkingSchema.optional(),
|
||||
input: z.array(z.enum(["text", "image"])).optional(),
|
||||
supportsTools: z.boolean().optional(),
|
||||
cost: z
|
||||
.object({
|
||||
input: z.number(),
|
||||
@@ -155,6 +156,7 @@ export const ModelOverrideSchema = z.object({
|
||||
reasoning: z.boolean().optional(),
|
||||
thinking: ModelThinkingSchema.optional(),
|
||||
input: z.array(z.enum(["text", "image"])).optional(),
|
||||
supportsTools: z.boolean().optional(),
|
||||
cost: z
|
||||
.object({
|
||||
input: z.number().optional(),
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface ProviderValidationModel {
|
||||
id: string;
|
||||
api?: Api;
|
||||
contextWindow?: number;
|
||||
supportsTools?: boolean;
|
||||
maxTokens?: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -1719,6 +1719,48 @@ export const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
|
||||
"tools.format": {
|
||||
type: "enum",
|
||||
values: [
|
||||
"auto",
|
||||
"native",
|
||||
"glm",
|
||||
"hermes",
|
||||
"kimi",
|
||||
"xml",
|
||||
"anthropic",
|
||||
"deepseek",
|
||||
"harmony",
|
||||
"pi",
|
||||
"qwen3",
|
||||
] as const,
|
||||
default: "auto",
|
||||
ui: {
|
||||
tab: "context",
|
||||
group: "Experimental",
|
||||
label: "Tool Call Format",
|
||||
description:
|
||||
"Controls how tools are exposed to the model. Auto uses native tool calls unless the selected model is marked as not supporting tools, then falls back to GLM-style in-band tool calls. Native forces provider-native tools; the other values force the named in-band syntax. Applies on session start.",
|
||||
options: [
|
||||
{
|
||||
value: "auto",
|
||||
label: "Auto",
|
||||
description: "Use native tool calls unless the model is known not to support them.",
|
||||
},
|
||||
{ value: "native", label: "Native", description: "Use provider-native tool calls." },
|
||||
{ value: "glm", label: "GLM", description: "Use GLM-style in-band tool calls." },
|
||||
{ value: "hermes", label: "Hermes", description: "Use Hermes-style in-band tool calls." },
|
||||
{ value: "kimi", label: "Kimi", description: "Use Kimi-style in-band tool calls." },
|
||||
{ value: "xml", label: "XML", description: "Use generic XML in-band tool calls." },
|
||||
{ value: "anthropic", label: "Anthropic", description: "Use Anthropic-style in-band tool calls." },
|
||||
{ value: "deepseek", label: "DeepSeek", description: "Use DeepSeek-style in-band tool calls." },
|
||||
{ value: "harmony", label: "Harmony", description: "Use Harmony-style in-band tool calls." },
|
||||
{ value: "pi", label: "Pi", description: "Use Pi-style in-band tool calls." },
|
||||
{ value: "qwen3", label: "Qwen3", description: "Use Qwen3-style in-band tool calls." },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
"snapcompact.shape": {
|
||||
type: "enum",
|
||||
values: ["auto", ...SHAPE_VARIANT_NAMES] as const,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type SimpleStreamOptions,
|
||||
streamSimple,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import type { ToolCallSyntax } from "@oh-my-pi/pi-ai/grammar";
|
||||
import {
|
||||
getOpenAICodexTransportDetails,
|
||||
prewarmOpenAICodexResponses,
|
||||
@@ -550,6 +551,17 @@ export interface CreateAgentSessionResult {
|
||||
eventBus: EventBus;
|
||||
}
|
||||
|
||||
export type ToolCallFormat = "auto" | "native" | ToolCallSyntax;
|
||||
|
||||
export function resolveToolCallSyntax(
|
||||
format: ToolCallFormat,
|
||||
model: Pick<Model, "supportsTools"> | undefined,
|
||||
): ToolCallSyntax | undefined {
|
||||
if (format === "native") return undefined;
|
||||
if (format === "auto") return model?.supportsTools === false ? "glm" : undefined;
|
||||
return format;
|
||||
}
|
||||
|
||||
// Re-exports
|
||||
|
||||
export type { PromptTemplate } from "./config/prompt-templates";
|
||||
@@ -2489,6 +2501,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
return result;
|
||||
},
|
||||
intentTracing: !!intentField,
|
||||
toolCallSyntax: resolveToolCallSyntax(settings.get("tools.format"), model),
|
||||
getToolChoice: () => session?.nextToolChoice(),
|
||||
telemetry: options.telemetry,
|
||||
appendOnlyContext: model
|
||||
|
||||
Reference in New Issue
Block a user