llm-relay 0.32.2 → 0.33.0

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.
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Recovering tool calls a HOST failed to parse.
3
+ *
4
+ * A model emits tool calls in its own dialect; the serving host is supposed to parse that into the
5
+ * OpenAI `message.tool_calls` field. NIM and OpenRouter do. Some free hosts do not, and hand back
6
+ * the raw envelope as ordinary assistant TEXT. Before this module the consequence was silent and
7
+ * total: no `tool_calls` ⇒ `stop_reason: end_turn` ⇒ zero `tool_use` blocks ⇒ the validator finds
8
+ * nothing malformed and PASSES ⇒ repair never engages ⇒ the client receives markup it treats as a
9
+ * final answer. Measured 2026-08-08: a task wrote its output correctly and then emitted
10
+ * `</|DSML|parameter></|DSML|invoke></|DSML|tool_calls>` as its entire visible response.
11
+ * See docs/tool-call-dialect-leak.md.
12
+ *
13
+ * ⚠ This is PARSING, not inference — the same side of the repair boundary as fixing args that
14
+ * violate a schema. The envelope set is CLOSED (same reasoning as the effort vocabulary in
15
+ * sync-tiers.mjs and the alias list in authEnv.ts): prose that merely mentions a tool name is not a
16
+ * tool call, and promoting it to one would be fabricating intent, which is the one thing the repair
17
+ * path must never do. A dialect we do not recognize yields `detected` at most, never a guess.
18
+ */
19
+ /**
20
+ * Marker substrings that mean "a tool-call envelope is present". Detection is deliberately broader
21
+ * than parsing: a truncated envelope must still be recognized as one, because returning its
22
+ * fragment as an answer is what made this failure read as "the job died".
23
+ */
24
+ /**
25
+ * ⚠ Markers deliberately omit the `<` / `</` prefix so a CLOSING tag matches too. The truncated
26
+ * tail of a stream is mostly closing tags — one of the two measured failure bodies was exactly
27
+ * `</|DSML|parameter></|DSML|invoke></|DSML|tool_calls>`, which an open-tag-only marker set
28
+ * reports as ordinary prose. That is the single case where detection matters most, because there
29
+ * is no call left to recover and the alternative is handing the fragment back as an answer.
30
+ */
31
+ const DIALECT_MARKERS = [
32
+ { dialect: "deepseek", markers: ["|tool▁calls▁begin|", "|tool▁call▁begin|", "|tool▁sep|", "|tool▁call▁end|", "|tool▁calls▁end|"] },
33
+ { dialect: "dsml", markers: ["|DSML|tool_calls", "|DSML|invoke", "|DSML|parameter"] },
34
+ { dialect: "hermes", markers: ["<tool_call>", "</tool_call>"] },
35
+ { dialect: "xml-invoke", markers: ["<function_calls>", "</function_calls>", "<invoke name=", "</invoke>"] },
36
+ { dialect: "functionary", markers: ["<function="] },
37
+ ];
38
+ export function detectDialect(text) {
39
+ for (const { dialect, markers } of DIALECT_MARKERS) {
40
+ if (markers.some((m) => text.includes(m)))
41
+ return dialect;
42
+ }
43
+ return null;
44
+ }
45
+ /** Coerce a stringly-typed dialect parameter using the tool's declared schema. */
46
+ function coerce(raw, schema) {
47
+ const t = Array.isArray(schema?.type) ? schema?.type[0] : schema?.type;
48
+ const s = raw.trim();
49
+ switch (t) {
50
+ case "number":
51
+ case "integer": {
52
+ const n = Number(s);
53
+ return Number.isFinite(n) ? n : raw;
54
+ }
55
+ case "boolean":
56
+ return s === "true" ? true : s === "false" ? false : raw;
57
+ case "object":
58
+ case "array":
59
+ try {
60
+ return JSON.parse(s);
61
+ }
62
+ catch {
63
+ return raw;
64
+ }
65
+ case "string":
66
+ return raw;
67
+ default:
68
+ // No declared type: keep the literal. Guessing a shape here would silently rewrite an
69
+ // argument the model meant as text.
70
+ return raw;
71
+ }
72
+ }
73
+ /** `{"name": "x", "arguments": {...}}` — the JSON payload shape shared by several dialects. */
74
+ function fromJsonPayload(raw) {
75
+ let j;
76
+ try {
77
+ j = JSON.parse(raw.trim());
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ if (typeof j !== "object" || j === null)
83
+ return null;
84
+ const o = j;
85
+ const name = typeof o.name === "string" ? o.name : null;
86
+ if (!name)
87
+ return null;
88
+ const args = o.arguments ?? o.parameters ?? o.input ?? {};
89
+ if (typeof args === "string") {
90
+ try {
91
+ return { name, input: JSON.parse(args) };
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ if (typeof args !== "object" || args === null)
98
+ return null;
99
+ return { name, input: args };
100
+ }
101
+ /** `<invoke name="x"><parameter name="p">v</parameter></invoke>`, with or without DSML markers. */
102
+ function fromInvokeForm(text, schemas) {
103
+ const calls = [];
104
+ const invoke = /<(?:|DSML|)?invoke\s+name="([^"]+)"\s*>([\s\S]*?)<\/(?:|DSML|)?invoke>/g;
105
+ for (const m of text.matchAll(invoke)) {
106
+ const name = m[1];
107
+ const bodyText = m[2];
108
+ if (!name || bodyText === undefined)
109
+ continue;
110
+ const input = {};
111
+ const param = /<(?:|DSML|)?parameter\s+name="([^"]+)"\s*>([\s\S]*?)<\/(?:|DSML|)?parameter>/g;
112
+ for (const p of bodyText.matchAll(param)) {
113
+ const key = p[1];
114
+ const value = p[2];
115
+ if (!key || value === undefined)
116
+ continue;
117
+ input[key] = coerce(value, schemas.get(name)?.properties?.[key]);
118
+ }
119
+ calls.push({ name, input });
120
+ }
121
+ return calls;
122
+ }
123
+ /** DeepSeek native: `…begin|>function<|tool▁sep|>NAME\n```json\n{…}\n``` `. */
124
+ function fromDeepSeekForm(text) {
125
+ const calls = [];
126
+ const re = /<|tool▁sep|>([^\n<]+)\n+(?:```(?:json)?\n)?([\s\S]*?)(?:\n?```)?\s*<|tool▁call▁end|>/g;
127
+ for (const m of text.matchAll(re)) {
128
+ const name = m[1];
129
+ const payload = m[2];
130
+ if (!name || payload === undefined)
131
+ continue;
132
+ let input = {};
133
+ try {
134
+ const parsed = JSON.parse(payload.trim());
135
+ if (typeof parsed === "object" && parsed !== null)
136
+ input = parsed;
137
+ }
138
+ catch { /* an unparseable body leaves `detected` to the caller */
139
+ return [];
140
+ }
141
+ calls.push({ name: name.trim(), input });
142
+ }
143
+ return calls;
144
+ }
145
+ /** `<tool_call>{json}</tool_call>` (Hermes/Qwen) and `<function=NAME>{json}</function>`. */
146
+ function fromTaggedJsonForms(text) {
147
+ const calls = [];
148
+ for (const m of text.matchAll(/<tool_call>([\s\S]*?)<\/tool_call>/g)) {
149
+ if (m[1] === undefined)
150
+ continue;
151
+ const c = fromJsonPayload(m[1]);
152
+ if (c)
153
+ calls.push(c);
154
+ }
155
+ for (const m of text.matchAll(/<function=([^>]+)>([\s\S]*?)<\/function>/g)) {
156
+ const name = m[1];
157
+ const payload = m[2];
158
+ if (!name || payload === undefined)
159
+ continue;
160
+ let input = {};
161
+ try {
162
+ const parsed = JSON.parse(payload.trim());
163
+ if (typeof parsed === "object" && parsed !== null)
164
+ input = parsed;
165
+ }
166
+ catch {
167
+ continue;
168
+ }
169
+ calls.push({ name: name.trim(), input });
170
+ }
171
+ return calls;
172
+ }
173
+ /** Everything the envelope occupied, so the surviving prose can stay a text block. */
174
+ function stripEnvelopes(text) {
175
+ return text
176
+ .replace(/<(?:|DSML|)?function_calls>[\s\S]*?<\/(?:|DSML|)?function_calls>/g, "")
177
+ .replace(/<|DSML|tool_calls>[\s\S]*?(?:<\/|DSML|tool_calls>|$)/g, "")
178
+ .replace(/<(?:|DSML|)?invoke\s+name="[^"]*"\s*>[\s\S]*?<\/(?:|DSML|)?invoke>/g, "")
179
+ .replace(/<|tool▁calls▁begin|>[\s\S]*?(?:<|tool▁calls▁end|>|$)/g, "")
180
+ .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, "")
181
+ .replace(/<function=[^>]+>[\s\S]*?<\/function>/g, "")
182
+ .replace(/<\/?think>/g, "")
183
+ .trim();
184
+ }
185
+ /**
186
+ * Recover tool calls from a text block a host failed to parse.
187
+ *
188
+ * `schemas` supplies declared parameter types for the dialects that carry parameters as strings;
189
+ * without it those values stay strings and the validator reports the type error, which is the
190
+ * correct visible failure rather than a silent coercion.
191
+ */
192
+ export function recoverToolCalls(text, schemas = new Map()) {
193
+ const dialect = detectDialect(text);
194
+ if (!dialect)
195
+ return { status: "none" };
196
+ const calls = [
197
+ ...fromInvokeForm(text, schemas),
198
+ ...fromDeepSeekForm(text),
199
+ ...fromTaggedJsonForms(text),
200
+ ].filter((c) => c.name.length > 0);
201
+ if (calls.length === 0)
202
+ return { status: "detected", dialect };
203
+ return { status: "parsed", calls, text: stripEnvelopes(text), dialect };
204
+ }
205
+ //# sourceMappingURL=tool-dialects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-dialects.js","sourceRoot":"","sources":["../src/tool-dialects.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-relay",
3
- "version": "0.32.2",
3
+ "version": "0.33.0",
4
4
  "description": "Loopback bidirectional Anthropic/OpenAI API proxy with tool-call validation and multi-provider routing.",
5
5
  "type": "module",
6
6
  "engines": {