feat: enhance OpenAI compatibility for tool calling
This commit is contained in:
parent
4583f88626
commit
bb4d6fb610
@ -2336,6 +2336,11 @@ Select the model via `agents.defaults.model.primary` (provider/model).
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Tool calling note (OpenAI-compatible servers):
|
||||||
|
- Some servers only support tool calling via `POST /v1/chat/completions` when the client sends
|
||||||
|
`tools` + `tool_choice`. If your `openai-completions` model never emits tool calls, set
|
||||||
|
`compat: { openaiCompletionsTools: true }` on that model entry to force Moltbot to include tools.
|
||||||
|
|
||||||
### OpenCode Zen (multi-model proxy)
|
### OpenCode Zen (multi-model proxy)
|
||||||
|
|
||||||
OpenCode Zen is a multi-model gateway with per-model endpoints. Moltbot uses
|
OpenCode Zen is a multi-model gateway with per-model endpoints. Moltbot uses
|
||||||
|
|||||||
@ -120,6 +120,7 @@ describe("splitSdkTools", () => {
|
|||||||
const { builtInTools, customTools } = splitSdkTools({
|
const { builtInTools, customTools } = splitSdkTools({
|
||||||
tools,
|
tools,
|
||||||
sandboxEnabled: true,
|
sandboxEnabled: true,
|
||||||
|
modelApi: "openai-responses",
|
||||||
});
|
});
|
||||||
expect(builtInTools).toEqual([]);
|
expect(builtInTools).toEqual([]);
|
||||||
expect(customTools.map((tool) => tool.name)).toEqual([
|
expect(customTools.map((tool) => tool.name)).toEqual([
|
||||||
@ -134,6 +135,7 @@ describe("splitSdkTools", () => {
|
|||||||
const { builtInTools, customTools } = splitSdkTools({
|
const { builtInTools, customTools } = splitSdkTools({
|
||||||
tools,
|
tools,
|
||||||
sandboxEnabled: false,
|
sandboxEnabled: false,
|
||||||
|
modelApi: "openai-responses",
|
||||||
});
|
});
|
||||||
expect(builtInTools).toEqual([]);
|
expect(builtInTools).toEqual([]);
|
||||||
expect(customTools.map((tool) => tool.name)).toEqual([
|
expect(customTools.map((tool) => tool.name)).toEqual([
|
||||||
@ -144,4 +146,21 @@ describe("splitSdkTools", () => {
|
|||||||
"browser",
|
"browser",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("can route tools to builtInTools for openai-completions when enabled", () => {
|
||||||
|
const { builtInTools, customTools } = splitSdkTools({
|
||||||
|
tools,
|
||||||
|
sandboxEnabled: false,
|
||||||
|
modelApi: "openai-completions",
|
||||||
|
openaiCompletionsTools: true,
|
||||||
|
});
|
||||||
|
expect(builtInTools.map((tool) => tool.name)).toEqual([
|
||||||
|
"read",
|
||||||
|
"exec",
|
||||||
|
"edit",
|
||||||
|
"write",
|
||||||
|
"browser",
|
||||||
|
]);
|
||||||
|
expect(customTools).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -378,9 +378,16 @@ export async function compactEmbeddedPiSessionDirect(
|
|||||||
model,
|
model,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const openaiCompletionsTools =
|
||||||
|
model.api === "openai-completions" &&
|
||||||
|
(model.compat as { openaiCompletionsTools?: unknown } | undefined)
|
||||||
|
?.openaiCompletionsTools === true;
|
||||||
|
|
||||||
const { builtInTools, customTools } = splitSdkTools({
|
const { builtInTools, customTools } = splitSdkTools({
|
||||||
tools,
|
tools,
|
||||||
sandboxEnabled: !!sandbox?.enabled,
|
sandboxEnabled: !!sandbox?.enabled,
|
||||||
|
modelApi: model.api,
|
||||||
|
openaiCompletionsTools,
|
||||||
});
|
});
|
||||||
|
|
||||||
let session: Awaited<ReturnType<typeof createAgentSession>>["session"];
|
let session: Awaited<ReturnType<typeof createAgentSession>>["session"];
|
||||||
|
|||||||
@ -432,9 +432,16 @@ export async function runEmbeddedAttempt(
|
|||||||
model: params.model,
|
model: params.model,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const openaiCompletionsTools =
|
||||||
|
params.model.api === "openai-completions" &&
|
||||||
|
(params.model.compat as { openaiCompletionsTools?: unknown } | undefined)
|
||||||
|
?.openaiCompletionsTools === true;
|
||||||
|
|
||||||
const { builtInTools, customTools } = splitSdkTools({
|
const { builtInTools, customTools } = splitSdkTools({
|
||||||
tools,
|
tools,
|
||||||
sandboxEnabled: !!sandbox?.enabled,
|
sandboxEnabled: !!sandbox?.enabled,
|
||||||
|
modelApi: params.model.api,
|
||||||
|
openaiCompletionsTools,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add client tools (OpenResponses hosted tools) to customTools
|
// Add client tools (OpenResponses hosted tools) to customTools
|
||||||
|
|||||||
@ -2,15 +2,36 @@ import type { AgentTool } from "@mariozechner/pi-agent-core";
|
|||||||
|
|
||||||
import { toToolDefinitions } from "../pi-tool-definition-adapter.js";
|
import { toToolDefinitions } from "../pi-tool-definition-adapter.js";
|
||||||
|
|
||||||
// We always pass tools via `customTools` so our policy filtering, sandbox integration,
|
|
||||||
// and extended toolset remain consistent across providers.
|
|
||||||
type AnyAgentTool = AgentTool;
|
type AnyAgentTool = AgentTool;
|
||||||
|
|
||||||
export function splitSdkTools(options: { tools: AnyAgentTool[]; sandboxEnabled: boolean }): {
|
function isOpenAiCompletionsToolsEnabled(options: {
|
||||||
|
modelApi?: string;
|
||||||
|
openaiCompletionsTools?: boolean;
|
||||||
|
}): boolean {
|
||||||
|
return options.modelApi === "openai-completions" && options.openaiCompletionsTools === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitSdkTools(options: {
|
||||||
|
tools: AnyAgentTool[];
|
||||||
|
sandboxEnabled: boolean;
|
||||||
|
modelApi?: string;
|
||||||
|
openaiCompletionsTools?: boolean;
|
||||||
|
}): {
|
||||||
builtInTools: AnyAgentTool[];
|
builtInTools: AnyAgentTool[];
|
||||||
customTools: ReturnType<typeof toToolDefinitions>;
|
customTools: ReturnType<typeof toToolDefinitions>;
|
||||||
} {
|
} {
|
||||||
const { tools } = options;
|
const { tools } = options;
|
||||||
|
// Default behavior: route all tools through `customTools` so our policy filtering,
|
||||||
|
// sandbox integration, and extended toolset remain consistent across providers.
|
||||||
|
//
|
||||||
|
// Some OpenAI-compatible servers (notably local vLLM) only support tool calling on
|
||||||
|
// /v1/chat/completions when tools are passed via the SDK tool path.
|
||||||
|
if (isOpenAiCompletionsToolsEnabled(options)) {
|
||||||
|
return {
|
||||||
|
builtInTools: tools,
|
||||||
|
customTools: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
builtInTools: [],
|
builtInTools: [],
|
||||||
customTools: toToolDefinitions(tools),
|
customTools: toToolDefinitions(tools),
|
||||||
|
|||||||
@ -11,6 +11,14 @@ export type ModelCompatConfig = {
|
|||||||
supportsDeveloperRole?: boolean;
|
supportsDeveloperRole?: boolean;
|
||||||
supportsReasoningEffort?: boolean;
|
supportsReasoningEffort?: boolean;
|
||||||
maxTokensField?: "max_completion_tokens" | "max_tokens";
|
maxTokensField?: "max_completion_tokens" | "max_tokens";
|
||||||
|
/**
|
||||||
|
* For some OpenAI-compatible servers (e.g. local vLLM), tool calling is only
|
||||||
|
* supported via `POST /v1/chat/completions` with `tools` + `tool_choice`.
|
||||||
|
*
|
||||||
|
* When enabled for an `openai-completions` model, Moltbot routes the agent's
|
||||||
|
* toolset through the SDK tool path so tools are included in the request.
|
||||||
|
*/
|
||||||
|
openaiCompletionsTools?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModelProviderAuthMode = "api-key" | "aws-sdk" | "oauth" | "token";
|
export type ModelProviderAuthMode = "api-key" | "aws-sdk" | "oauth" | "token";
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export const ModelCompatSchema = z
|
|||||||
maxTokensField: z
|
maxTokensField: z
|
||||||
.union([z.literal("max_completion_tokens"), z.literal("max_tokens")])
|
.union([z.literal("max_completion_tokens"), z.literal("max_tokens")])
|
||||||
.optional(),
|
.optional(),
|
||||||
|
openaiCompletionsTools: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user