@code-yeongyu/senpi-ai 2026.10.3 → 2026.10.5

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.
@@ -2,6 +2,16 @@ import type { Api, Model } from "../types.ts";
2
2
  /** The wire identity a forced tool_choice refusal is remembered under. */
3
3
  export type ForcedToolChoiceTarget = Pick<Model<Api>, "api" | "provider" | "baseUrl" | "id">;
4
4
  export declare function isForcedToolChoiceUnsupportedError(error: unknown, sentForcedToolChoice: boolean): boolean;
5
+ /**
6
+ * A 400 to a request that forced a tool and whose message names that very tool: its `tools.N` /
7
+ * `tools[N]` position or its quoted name. Strict-schema gateways refuse the forced tool's schema this
8
+ * way (senpi#2648); the same request without the forced choice is accepted. A 400 that names
9
+ * no tool, or another one, is not this refusal.
10
+ */
11
+ export declare function refusalNamesForcedTool(error: unknown, params: {
12
+ readonly tool_choice?: unknown;
13
+ readonly tools?: unknown;
14
+ }): boolean;
5
15
  export declare function omitToolChoiceParam<TParams extends {
6
16
  tool_choice?: unknown;
7
17
  }>(params: TParams): TParams;
@@ -50,6 +50,44 @@ export function isForcedToolChoiceUnsupportedError(error, sentForcedToolChoice)
50
50
  // cannot force tool use. Use tool_choice 'auto' or 'none'."
51
51
  /tool[_\s-]?choice\s+cannot\s+force\s+tool\s+use/is.test(message));
52
52
  }
53
+ /** The tool a `tool_choice` forces, in the wire shapes senpi sends (Chat Completions, Responses, Messages). */
54
+ function forcedToolName(toolChoice) {
55
+ if (!isRecord(toolChoice))
56
+ return undefined;
57
+ const named = isRecord(toolChoice.function) ? toolChoice.function.name : toolChoice.name;
58
+ return typeof named === "string" && named.length > 0 ? named : undefined;
59
+ }
60
+ function toolNames(tools) {
61
+ if (!Array.isArray(tools))
62
+ return [];
63
+ return tools.map((tool) => {
64
+ if (!isRecord(tool))
65
+ return undefined;
66
+ const named = isRecord(tool.function) ? tool.function.name : tool.name;
67
+ return typeof named === "string" ? named : undefined;
68
+ });
69
+ }
70
+ function escapeRegExp(text) {
71
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
72
+ }
73
+ /**
74
+ * A 400 to a request that forced a tool and whose message names that very tool: its `tools.N` /
75
+ * `tools[N]` position or its quoted name. Strict-schema gateways refuse the forced tool's schema this
76
+ * way (senpi#2648); the same request without the forced choice is accepted. A 400 that names
77
+ * no tool, or another one, is not this refusal.
78
+ */
79
+ export function refusalNamesForcedTool(error, params) {
80
+ if (extractHttpStatus(error) !== 400)
81
+ return false;
82
+ const name = forcedToolName(params.tool_choice);
83
+ if (name === undefined)
84
+ return false;
85
+ const message = errorMessage(error);
86
+ const index = toolNames(params.tools).indexOf(name);
87
+ if (index >= 0 && new RegExp(`\\btools(?:\\.${index}|\\[${index}\\])(?!\\d)`).test(message))
88
+ return true;
89
+ return new RegExp(`['"\`]${escapeRegExp(name)}['"\`]`).test(message);
90
+ }
53
91
  export function omitToolChoiceParam(params) {
54
92
  const nextParams = { ...params };
55
93
  delete nextParams.tool_choice;
@@ -82,7 +120,8 @@ export async function sendWithForcedToolChoiceFallback(request) {
82
120
  return { params: request.params, result: await request.send(request.params) };
83
121
  }
84
122
  catch (error) {
85
- if (!isForcedToolChoiceUnsupportedError(error, forced))
123
+ if (!isForcedToolChoiceUnsupportedError(error, forced) &&
124
+ !(forced && refusalNamesForcedTool(error, request.params)))
86
125
  throw error;
87
126
  const params = omitToolChoiceParam(request.params);
88
127
  const result = await request.send(params);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-ai",
3
- "version": "2026.10.3",
3
+ "version": "2026.10.5",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -88,7 +88,7 @@
88
88
  "dependencies": {
89
89
  "@anthropic-ai/sdk": "0.127.0",
90
90
  "@aws-sdk/client-bedrock-runtime": "3.1136.0",
91
- "@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.3",
91
+ "@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.5",
92
92
  "@google/genai": "2.23.0",
93
93
  "@smithy/node-http-handler": "4.12.1",
94
94
  "http-proxy-agent": "9.1.0",