@oh-my-pi/pi-ai 15.1.8 → 15.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +2 -2
- package/src/providers/ollama.ts +26 -1
- package/src/providers/openai-completions.ts +21 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.1.9] - 2026-05-21
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed Ollama named tool forcing to send only the requested tool when the caller passes a named `toolChoice`, preserving `tool_choice: "required"` while preventing local models from selecting a different tool. ([#1236](https://github.com/can1357/oh-my-pi/issues/1236))
|
|
10
|
+
- Fixed `/btw` (and IRC background replies) returning a `BedrockException` 400 (`The toolConfig field must be defined when using toolUse and toolResult content blocks.`) on LiteLLM → Bedrock once the session has tool-call history. Two source fixes in `buildParams`: (1) `if (context.tools)` → `if (context.tools?.length)` so an explicit `context.tools = []` (the /btw opt-out) never routes through `convertTools` and never emits an empty `"tools"` array; (2) `else if (hasToolHistory(...))` → `else if (context.tools === undefined && hasToolHistory(...))` so the Anthropic-proxy sentinel that injects `tools: []` for tool-history turns is suppressed when the caller explicitly opted out, preventing it from re-introducing the empty array. As defence-in-depth, `tool_choice: "none"` is also dropped when the resolved tools list is missing or empty. ([#1227](https://github.com/can1357/oh-my-pi/issues/1227))
|
|
11
|
+
|
|
5
12
|
## [15.1.8] - 2026-05-20
|
|
6
13
|
### Added
|
|
7
14
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "15.1.
|
|
4
|
+
"version": "15.1.9",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
45
45
|
"@bufbuild/protobuf": "^2.12.0",
|
|
46
|
-
"@oh-my-pi/pi-utils": "15.1.
|
|
46
|
+
"@oh-my-pi/pi-utils": "15.1.9",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
package/src/providers/ollama.ts
CHANGED
|
@@ -116,6 +116,29 @@ function mapToolChoice(toolChoice: ToolChoice | undefined): "auto" | "none" | "r
|
|
|
116
116
|
return undefined;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
function getNamedToolChoiceName(toolChoice: ToolChoice | undefined): string | undefined {
|
|
120
|
+
if (!toolChoice || typeof toolChoice === "string") {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
if ("function" in toolChoice) {
|
|
124
|
+
return toolChoice.function.name;
|
|
125
|
+
}
|
|
126
|
+
return toolChoice.name;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function selectToolsForToolChoice(tools: Tool[] | undefined, toolChoice: ToolChoice | undefined): Tool[] | undefined {
|
|
130
|
+
const toolName = getNamedToolChoiceName(toolChoice);
|
|
131
|
+
if (!toolName || !tools) {
|
|
132
|
+
return tools;
|
|
133
|
+
}
|
|
134
|
+
for (const tool of tools) {
|
|
135
|
+
if (tool.name === toolName) {
|
|
136
|
+
return [tool];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
|
|
119
142
|
function toPlainContent(content: string | Array<{ type: "text" | "image"; text?: string; data?: string }>): {
|
|
120
143
|
content: string;
|
|
121
144
|
images?: string[];
|
|
@@ -231,10 +254,12 @@ function convertTools(tools: Tool[] | undefined): OllamaFunctionTool[] | undefin
|
|
|
231
254
|
function createChatBody(model: Model<"ollama-chat">, context: Context, options: OllamaChatOptions | undefined) {
|
|
232
255
|
const think = mapReasoning(options?.reasoning);
|
|
233
256
|
const toolChoice = mapToolChoice(options?.toolChoice);
|
|
257
|
+
const selectedTools = selectToolsForToolChoice(context.tools, options?.toolChoice);
|
|
258
|
+
const tools = convertTools(selectedTools);
|
|
234
259
|
return {
|
|
235
260
|
model: model.id,
|
|
236
261
|
messages: convertMessages(model, context),
|
|
237
|
-
...(
|
|
262
|
+
...(tools ? { tools } : {}),
|
|
238
263
|
...(think !== undefined ? { think } : {}),
|
|
239
264
|
...(toolChoice !== undefined ? { tool_choice: toolChoice } : {}),
|
|
240
265
|
...(options?.maxTokens !== undefined ? { options: { num_predict: options.maxTokens } } : {}),
|
|
@@ -1110,12 +1110,18 @@ function buildParams(
|
|
|
1110
1110
|
}
|
|
1111
1111
|
}
|
|
1112
1112
|
|
|
1113
|
-
if (context.tools) {
|
|
1113
|
+
if (context.tools?.length) {
|
|
1114
1114
|
const builtTools = convertTools(context.tools, compat, toolStrictModeOverride);
|
|
1115
1115
|
params.tools = builtTools.tools;
|
|
1116
1116
|
toolStrictMode = builtTools.toolStrictMode;
|
|
1117
|
-
} else if (hasToolHistory(context.messages)) {
|
|
1118
|
-
// Anthropic (via LiteLLM/proxy) requires tools param when conversation
|
|
1117
|
+
} else if (context.tools === undefined && hasToolHistory(context.messages)) {
|
|
1118
|
+
// Anthropic (via LiteLLM/proxy) requires the `tools` param when the conversation
|
|
1119
|
+
// contains tool_calls/tool_results, even when no tools are offered this turn.
|
|
1120
|
+
// Only inject the sentinel when the caller passed `context.tools = undefined`
|
|
1121
|
+
// (i.e. tools were not specified at all). An explicit `context.tools = []` means
|
|
1122
|
+
// the caller opted out of tools for this turn (as /btw and IRC background replies
|
|
1123
|
+
// do via AgentSession.runEphemeralTurn) — honour that intent and emit nothing,
|
|
1124
|
+
// so LiteLLM → Bedrock never sees an empty `toolConfig` block.
|
|
1119
1125
|
params.tools = [];
|
|
1120
1126
|
}
|
|
1121
1127
|
|
|
@@ -1123,6 +1129,18 @@ function buildParams(
|
|
|
1123
1129
|
params.tool_choice = mapToOpenAICompletionsToolChoice(options.toolChoice);
|
|
1124
1130
|
}
|
|
1125
1131
|
|
|
1132
|
+
if (params.tool_choice === "none" && (!Array.isArray(params.tools) || params.tools.length === 0)) {
|
|
1133
|
+
// `tool_choice: "none"` with no tools to gate is redundant and also
|
|
1134
|
+
// trips LiteLLM → Bedrock: the proxy serializes the directive into a
|
|
1135
|
+
// `toolConfig` block, and Bedrock requires `toolConfig.tools` to be
|
|
1136
|
+
// non-empty whenever the conversation already holds `toolUse`/`toolResult`
|
|
1137
|
+
// content. Drop it whenever the resolved tools list is missing or empty.
|
|
1138
|
+
// Side-channel turns hit this: `/btw` and IRC background replies route
|
|
1139
|
+
// through `AgentSession.runEphemeralTurn`, which sets `context.tools = []`
|
|
1140
|
+
// and `toolChoice: "none"` (see packages/coding-agent/src/session/agent-session.ts).
|
|
1141
|
+
delete params.tool_choice;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1126
1144
|
if (supportsReasoningParams && compat.thinkingFormat === "zai" && model.reasoning) {
|
|
1127
1145
|
// Z.ai uses binary thinking: { type: "enabled" | "disabled" }
|
|
1128
1146
|
// Must explicitly disable since z.ai defaults to thinking enabled.
|