@qvac/sdk 0.10.0 → 0.10.1

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.
Files changed (27) hide show
  1. package/NOTICE +28 -28
  2. package/dist/examples/{llamacpp-native-tools.js → tools/llamacpp-native-tools.js} +4 -47
  3. package/dist/examples/tools/llamacpp-tools-harmony-multiturn.d.ts +2 -0
  4. package/dist/examples/tools/llamacpp-tools-harmony-multiturn.js +81 -0
  5. package/dist/examples/{llamacpp-tools-pythonic.js → tools/llamacpp-tools-pythonic.js} +1 -20
  6. package/dist/examples/tools/shared.d.ts +21 -0
  7. package/dist/examples/tools/shared.js +42 -0
  8. package/dist/models/registry/models.d.ts +312 -1231
  9. package/dist/models/registry/models.js +2081 -3025
  10. package/dist/models/update-models/registry.d.ts +1 -0
  11. package/dist/models/update-models/registry.js +19 -1
  12. package/dist/schemas/common.d.ts +1 -0
  13. package/dist/schemas/completion-stream.d.ts +4 -0
  14. package/dist/schemas/completion-stream.js +2 -1
  15. package/dist/server/bare/plugins/llamacpp-completion/plugin.d.ts +1 -0
  16. package/dist/server/bare/plugins/llamacpp-completion/plugin.js +3 -3
  17. package/dist/server/bare/plugins/nmtcpp-translation/plugin.js +1 -82
  18. package/dist/server/bare/plugins/nmtcpp-translation/resolve-vocab.d.ts +32 -0
  19. package/dist/server/bare/plugins/nmtcpp-translation/resolve-vocab.js +117 -0
  20. package/dist/server/utils/completion-normalizer.js +168 -56
  21. package/dist/server/utils/tools/dialect.js +2 -0
  22. package/dist/server/utils/tools/parser.js +6 -0
  23. package/dist/server/utils/tools/parsers/harmony.d.ts +4 -0
  24. package/dist/server/utils/tools/parsers/harmony.js +59 -0
  25. package/package.json +1 -1
  26. /package/dist/examples/{llamacpp-native-tools.d.ts → tools/llamacpp-native-tools.d.ts} +0 -0
  27. /package/dist/examples/{llamacpp-tools-pythonic.d.ts → tools/llamacpp-tools-pythonic.d.ts} +0 -0
@@ -1,14 +1,14 @@
1
1
  import { parseToolCalls } from "./tools/index.js";
2
2
  const DIALECT_SPECS = {
3
3
  hermes: {
4
- frames: [{ open: "<tool_call>", close: "</tool_call>" }],
4
+ toolFrames: [{ open: "<tool_call>", close: "</tool_call>" }],
5
5
  },
6
6
  // No streaming framing — tool calls are only recovered at finalization.
7
7
  json: {
8
- frames: [],
8
+ toolFrames: [],
9
9
  },
10
10
  pythonic: {
11
- frames: [
11
+ toolFrames: [
12
12
  { open: "<|tool_call_start|>", close: "<|tool_call_end|>" },
13
13
  {
14
14
  open: "<|start_header_id|>tool_call<|end_header_id|>",
@@ -16,36 +16,90 @@ const DIALECT_SPECS = {
16
16
  },
17
17
  ],
18
18
  },
19
+ harmony: {
20
+ toolFrames: [
21
+ { open: "<|channel|>commentary to=functions.", close: "<|call|>" },
22
+ ],
23
+ thinkingFrames: [
24
+ { open: "<|channel|>analysis<|message|>", close: "<|end|>" },
25
+ ],
26
+ contentFrames: [
27
+ { open: "<|channel|>final<|message|>", close: "<|return|>" },
28
+ ],
29
+ // Role headers between channel frames + stray `<|return|>` closes.
30
+ dropTokens: [
31
+ "<|start|>assistant",
32
+ "<|start|>system",
33
+ "<|start|>user",
34
+ "<|return|>",
35
+ ],
36
+ },
19
37
  };
20
- const THINK_OPEN = "<think>";
21
- const THINK_CLOSE = "</think>";
38
+ // Capture-gated reasoning marker — the generic `<think>...</think>`
39
+ // convention used by reasoning-tuned models. When the gate is off the
40
+ // markers pass through into `contentDelta` unchanged (preserves the
41
+ // pre-dialect behavior).
42
+ const BUILTIN_THINKING = { open: "<think>", close: "</think>" };
22
43
  export function createCompletionNormalizer(config) {
23
44
  let seq = 0;
24
45
  let state = "content";
25
46
  let rawText = "";
26
47
  const contentParts = [];
27
48
  const thinkingParts = [];
28
- // Frame buffer keeps the open + close markers in place so parseToolCalls
29
- // can match its own dialect markers.
49
+ // Tool frame: buffer keeps the open + close markers in place so
50
+ // `parseToolCalls` can match its own dialect markers.
30
51
  let toolCallBuffer = "";
31
- let activeFrame = null;
52
+ let activeToolFrame = null;
53
+ // Thinking frame: tracks which frame's close marker we're scanning for
54
+ // (could be the built-in `<think>` or a dialect-specific frame like
55
+ // Harmony's `<|channel|>analysis<|message|>...<|end|>`).
56
+ let activeThinkingFrame = null;
57
+ // Content frame: inner streamed as it arrives; partial close tail buffered.
58
+ let activeContentFrame = null;
32
59
  // Trailing chars that could be a prefix of a watched marker, so markers
33
60
  // split across token pushes aren't missed by indexOf.
34
61
  let contentTail = "";
35
- let toolTail = "";
36
- let thinkingTail = "";
62
+ let toolFramingTail = "";
63
+ let thinkingFramingTail = "";
64
+ let contentFramingTail = "";
37
65
  const streamedToolCallKeys = new Set();
38
66
  const toolsEnabled = config.tools.length > 0 && config.capabilities.toolCalling !== "none";
39
- const useFramedParsing = toolsEnabled && config.capabilities.toolCalling === "textParse";
40
- const useThinkingParsing = config.captureThinking && config.capabilities.thinkingFraming === "thinkTags";
67
+ const useToolFraming = toolsEnabled && config.capabilities.toolCalling === "textParse";
68
+ // Register `<think>` only when both capability + capture opt in.
69
+ // Dialect protocol markers always register; capture only gates emission.
70
+ const useBuiltinThinkTags = config.captureThinking &&
71
+ config.capabilities.thinkingFraming === "thinkTags";
41
72
  const dialect = config.toolDialect ?? "hermes";
42
73
  const dialectSpec = DIALECT_SPECS[dialect];
43
- const frameSpecs = useFramedParsing ? dialectSpec.frames : [];
74
+ const toolFrames = useToolFraming
75
+ ? dialectSpec.toolFrames
76
+ : [];
77
+ const thinkingFrames = [
78
+ ...(useBuiltinThinkTags ? [BUILTIN_THINKING] : []),
79
+ ...(dialectSpec.thinkingFrames ?? []),
80
+ ];
81
+ const contentFrames = dialectSpec.contentFrames ?? [];
82
+ const dropTokens = dialectSpec.dropTokens ?? [];
83
+ const markerToEntry = new Map();
84
+ const dropTokenSet = new Set();
44
85
  const contentMarkers = [];
45
- if (useThinkingParsing)
46
- contentMarkers.push(THINK_OPEN);
47
- for (const f of frameSpecs)
48
- contentMarkers.push(f.open);
86
+ function registerFrames(frames, kind) {
87
+ for (const f of frames) {
88
+ if (markerToEntry.has(f.open))
89
+ continue;
90
+ markerToEntry.set(f.open, { kind, spec: f });
91
+ contentMarkers.push(f.open);
92
+ }
93
+ }
94
+ registerFrames(toolFrames, "tool");
95
+ registerFrames(thinkingFrames, "thinking");
96
+ registerFrames(contentFrames, "content");
97
+ for (const t of dropTokens) {
98
+ if (markerToEntry.has(t) || dropTokenSet.has(t))
99
+ continue;
100
+ dropTokenSet.add(t);
101
+ contentMarkers.push(t);
102
+ }
49
103
  function nextSeq() {
50
104
  return seq++;
51
105
  }
@@ -55,11 +109,14 @@ export function createCompletionNormalizer(config) {
55
109
  contentParts.push(text);
56
110
  events.push({ type: "contentDelta", seq: nextSeq(), text });
57
111
  }
112
+ // Capture-gated: thinking frames may register even when `captureThinking`
113
+ // is false (e.g. harmony's analysis channel, to strip markers from
114
+ // `contentDelta`) — drop the inner here instead of buffering it.
58
115
  function emitThinking(events, text) {
59
116
  if (!text)
60
117
  return;
61
- thinkingParts.push(text);
62
118
  if (config.captureThinking) {
119
+ thinkingParts.push(text);
63
120
  events.push({ type: "thinkingDelta", seq: nextSeq(), text });
64
121
  }
65
122
  }
@@ -102,10 +159,10 @@ export function createCompletionNormalizer(config) {
102
159
  }
103
160
  function flushToolBuffer(events, closed) {
104
161
  const raw = toolCallBuffer;
105
- const frame = activeFrame;
162
+ const frame = activeToolFrame;
106
163
  toolCallBuffer = "";
107
- activeFrame = null;
108
- toolTail = "";
164
+ activeToolFrame = null;
165
+ toolFramingTail = "";
109
166
  if (!frame)
110
167
  return;
111
168
  // Empty-payload guard. Strip frame markers and check the inner is
@@ -150,64 +207,110 @@ export function createCompletionNormalizer(config) {
150
207
  emitContent(events, raw);
151
208
  }
152
209
  }
210
+ function getStateTail() {
211
+ switch (state) {
212
+ case "content":
213
+ return contentTail;
214
+ case "thinkingFraming":
215
+ return thinkingFramingTail;
216
+ case "toolFraming":
217
+ return toolFramingTail;
218
+ case "contentFraming":
219
+ return contentFramingTail;
220
+ }
221
+ }
153
222
  function processSegment(events, text) {
154
- let buf = (state === "content"
155
- ? contentTail
156
- : state === "thinking"
157
- ? thinkingTail
158
- : toolTail) + text;
223
+ let buf = getStateTail() + text;
159
224
  contentTail = "";
160
- thinkingTail = "";
161
- toolTail = "";
225
+ thinkingFramingTail = "";
226
+ toolFramingTail = "";
227
+ contentFramingTail = "";
162
228
  while (buf.length > 0) {
163
- if (state === "toolBuffering") {
164
- if (!activeFrame) {
229
+ if (state === "toolFraming") {
230
+ if (!activeToolFrame) {
165
231
  state = "content";
166
232
  continue;
167
233
  }
168
- const hit = buf.indexOf(activeFrame.close);
234
+ const hit = buf.indexOf(activeToolFrame.close);
169
235
  if (hit >= 0) {
170
- toolCallBuffer += buf.slice(0, hit + activeFrame.close.length);
171
- buf = buf.slice(hit + activeFrame.close.length);
236
+ toolCallBuffer += buf.slice(0, hit + activeToolFrame.close.length);
237
+ buf = buf.slice(hit + activeToolFrame.close.length);
172
238
  state = "content";
173
239
  flushToolBuffer(events, true);
174
240
  continue;
175
241
  }
176
- const tailLen = trailingPartialMarkerLen(buf, [activeFrame.close]);
242
+ const tailLen = trailingPartialMarkerLen(buf, [activeToolFrame.close]);
177
243
  const safeLen = buf.length - tailLen;
178
244
  toolCallBuffer += buf.slice(0, safeLen);
179
- toolTail = buf.slice(safeLen);
245
+ toolFramingTail = buf.slice(safeLen);
180
246
  buf = "";
181
247
  continue;
182
248
  }
183
- if (state === "thinking") {
184
- const closeIdx = buf.indexOf(THINK_CLOSE);
249
+ if (state === "thinkingFraming") {
250
+ if (!activeThinkingFrame) {
251
+ state = "content";
252
+ continue;
253
+ }
254
+ const closeMarker = activeThinkingFrame.close;
255
+ const closeIdx = buf.indexOf(closeMarker);
185
256
  if (closeIdx >= 0) {
186
257
  emitThinking(events, buf.slice(0, closeIdx));
187
- buf = buf.slice(closeIdx + THINK_CLOSE.length);
258
+ buf = buf.slice(closeIdx + closeMarker.length);
259
+ activeThinkingFrame = null;
188
260
  state = "content";
189
261
  continue;
190
262
  }
191
- const tailLen = trailingPartialMarkerLen(buf, [THINK_CLOSE]);
263
+ const tailLen = trailingPartialMarkerLen(buf, [closeMarker]);
192
264
  const safeLen = buf.length - tailLen;
193
265
  emitThinking(events, buf.slice(0, safeLen));
194
- thinkingTail = buf.slice(safeLen);
266
+ thinkingFramingTail = buf.slice(safeLen);
267
+ buf = "";
268
+ continue;
269
+ }
270
+ if (state === "contentFraming") {
271
+ if (!activeContentFrame) {
272
+ state = "content";
273
+ continue;
274
+ }
275
+ const closeMarker = activeContentFrame.close;
276
+ const closeIdx = buf.indexOf(closeMarker);
277
+ if (closeIdx >= 0) {
278
+ emitContent(events, buf.slice(0, closeIdx));
279
+ activeContentFrame = null;
280
+ contentFramingTail = "";
281
+ buf = buf.slice(closeIdx + closeMarker.length);
282
+ state = "content";
283
+ continue;
284
+ }
285
+ const tailLen = trailingPartialMarkerLen(buf, [closeMarker]);
286
+ const safeLen = buf.length - tailLen;
287
+ emitContent(events, buf.slice(0, safeLen));
288
+ contentFramingTail = buf.slice(safeLen);
195
289
  buf = "";
196
290
  continue;
197
291
  }
198
292
  const hit = findEarliestMarker(buf, contentMarkers);
199
293
  if (hit) {
200
294
  emitContent(events, buf.slice(0, hit.idx));
201
- if (hit.marker === THINK_OPEN) {
202
- buf = buf.slice(hit.idx + hit.marker.length);
203
- state = "thinking";
295
+ buf = buf.slice(hit.idx + hit.marker.length);
296
+ if (dropTokenSet.has(hit.marker)) {
297
+ // Glue token between frames — strip and stay in content state.
204
298
  continue;
205
299
  }
206
- const spec = frameSpecs.find((f) => f.open === hit.marker);
207
- toolCallBuffer = hit.marker;
208
- activeFrame = spec;
209
- buf = buf.slice(hit.idx + hit.marker.length);
210
- state = "toolBuffering";
300
+ const entry = markerToEntry.get(hit.marker);
301
+ if (entry.kind === "tool") {
302
+ toolCallBuffer = hit.marker;
303
+ activeToolFrame = entry.spec;
304
+ state = "toolFraming";
305
+ }
306
+ else if (entry.kind === "thinking") {
307
+ activeThinkingFrame = entry.spec;
308
+ state = "thinkingFraming";
309
+ }
310
+ else {
311
+ activeContentFrame = entry.spec;
312
+ state = "contentFraming";
313
+ }
211
314
  continue;
212
315
  }
213
316
  const tailLen = trailingPartialMarkerLen(buf, contentMarkers);
@@ -234,24 +337,33 @@ export function createCompletionNormalizer(config) {
234
337
  emitContent(events, contentTail);
235
338
  contentTail = "";
236
339
  }
237
- else if (state === "thinking" && thinkingTail) {
238
- emitThinking(events, thinkingTail);
239
- thinkingTail = "";
340
+ else if (state === "thinkingFraming" && thinkingFramingTail) {
341
+ emitThinking(events, thinkingFramingTail);
342
+ thinkingFramingTail = "";
240
343
  }
241
- else if (state === "toolBuffering" && toolTail) {
242
- toolCallBuffer += toolTail;
243
- toolTail = "";
344
+ else if (state === "toolFraming" && toolFramingTail) {
345
+ toolCallBuffer += toolFramingTail;
346
+ toolFramingTail = "";
244
347
  }
245
- if (state === "toolBuffering" && activeFrame) {
348
+ else if (state === "contentFraming" && contentFramingTail) {
349
+ // Cutoff inside a partial close marker — emit tail as content.
350
+ emitContent(events, contentFramingTail);
351
+ contentFramingTail = "";
352
+ }
353
+ if (state === "toolFraming" && activeToolFrame) {
246
354
  if (opts?.error) {
247
355
  emitContent(events, toolCallBuffer);
248
356
  toolCallBuffer = "";
249
- activeFrame = null;
357
+ activeToolFrame = null;
250
358
  }
251
359
  else {
252
360
  flushToolBuffer(events, false);
253
361
  }
254
362
  }
363
+ else if (state === "contentFraming") {
364
+ activeContentFrame = null;
365
+ }
366
+ activeThinkingFrame = null;
255
367
  state = "content";
256
368
  if (!opts?.error) {
257
369
  if (opts?.toolCalls) {
@@ -6,6 +6,8 @@
6
6
  export function detectToolDialectFromName(name, path) {
7
7
  const basename = path.toLowerCase().split(/[/\\]/).pop() ?? "";
8
8
  const tag = `${(name ?? "").toLowerCase()}|${basename}`;
9
+ if (/gpt[_-]?oss/.test(tag))
10
+ return "harmony";
9
11
  if (/lfm[_-]?\d/.test(tag))
10
12
  return "pythonic";
11
13
  return "hermes";
@@ -2,6 +2,7 @@ import { stripThinkingBlocks, } from "./shared.js";
2
2
  import { parseGemmaFormat, parseGenericFormat, parseLlamacppFormat, } from "./parsers/json.js";
3
3
  import { parseHermesFormat } from "./parsers/hermes.js";
4
4
  import { parsePythonicFormat } from "./parsers/pythonic.js";
5
+ import { parseHarmonyFormat } from "./parsers/harmony.js";
5
6
  function pickFormatParsers(dialect) {
6
7
  switch (dialect) {
7
8
  case "pythonic":
@@ -12,10 +13,15 @@ function pickFormatParsers(dialect) {
12
13
  return [parseHermesFormat, parseGemmaFormat, parseLlamacppFormat];
13
14
  case "json":
14
15
  return [parseGemmaFormat, parseLlamacppFormat];
16
+ case "harmony":
17
+ return [parseHarmonyFormat];
15
18
  default:
19
+ // Harmony first: `to=functions.` is uniquely Harmony and can't
20
+ // false-match other dialects.
16
21
  // Pythonic last: its bare `[name(...)]` form can match payloads that
17
22
  // look like other dialects.
18
23
  return [
24
+ parseHarmonyFormat,
19
25
  parseHermesFormat,
20
26
  parseGemmaFormat,
21
27
  parseLlamacppFormat,
@@ -0,0 +1,4 @@
1
+ import type { Tool } from "../../../../schemas/index";
2
+ import { type ParserResult } from "../shared";
3
+ export declare function parseHarmonyFormat(text: string, tools: Tool[]): ParserResult;
4
+ //# sourceMappingURL=harmony.d.ts.map
@@ -0,0 +1,59 @@
1
+ import { generateStableToolCallId, validateToolArguments, } from "../shared.js";
2
+ const HARMONY_FRAME_RE = /to=functions\.([\w-]+)\s+<\|constrain\|>json<\|message\|>([\s\S]*?)(?=<\|call\|>|$)/g;
3
+ export function parseHarmonyFormat(text, tools) {
4
+ const toolCalls = [];
5
+ const errors = [];
6
+ if (!text.includes("to=functions.")) {
7
+ return { matched: false, toolCalls, errors };
8
+ }
9
+ HARMONY_FRAME_RE.lastIndex = 0;
10
+ let m;
11
+ while ((m = HARMONY_FRAME_RE.exec(text)) !== null) {
12
+ const name = m[1];
13
+ const payload = m[2]?.trim();
14
+ const raw = m[0];
15
+ if (!name || payload === undefined)
16
+ continue;
17
+ let parsedArgs;
18
+ try {
19
+ parsedArgs = JSON.parse(payload);
20
+ }
21
+ catch (error) {
22
+ errors.push({
23
+ code: "PARSE_ERROR",
24
+ message: `Failed to parse Harmony tool call JSON for ${name}: ${error instanceof Error ? error.message : String(error)}`,
25
+ raw,
26
+ });
27
+ continue;
28
+ }
29
+ if (typeof parsedArgs !== "object" ||
30
+ parsedArgs === null ||
31
+ Array.isArray(parsedArgs)) {
32
+ errors.push({
33
+ code: "PARSE_ERROR",
34
+ message: `Harmony tool call payload for ${name} is not a JSON object`,
35
+ raw,
36
+ });
37
+ continue;
38
+ }
39
+ const args = parsedArgs;
40
+ const validation = validateToolArguments(name, args, tools);
41
+ if (!validation.isValid && validation.error) {
42
+ errors.push({ ...validation.error, raw });
43
+ continue;
44
+ }
45
+ toolCalls.push({
46
+ id: generateStableToolCallId(name, args),
47
+ name,
48
+ arguments: args,
49
+ raw,
50
+ });
51
+ }
52
+ // Defer `matched` to extraction so partial Harmony shapes fall through
53
+ // to the next parser instead of short-circuiting the chain.
54
+ return {
55
+ matched: toolCalls.length > 0 || errors.length > 0,
56
+ toolCalls,
57
+ errors,
58
+ };
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/sdk",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",