@qvac/sdk 1.0.0 → 1.1.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.
@@ -15,6 +15,7 @@ export declare const transcribeParamsSchema: z.ZodObject<{
15
15
  type: z.ZodLiteral<"filePath">;
16
16
  value: z.ZodString;
17
17
  }, z.core.$strip>], "type">;
18
+ prompt: z.ZodOptional<z.ZodString>;
18
19
  }, z.core.$strip>;
19
20
  export declare const transcribeStreamRequestSchema: z.ZodObject<{
20
21
  modelId: z.ZodString;
@@ -25,6 +26,7 @@ export declare const transcribeStreamRequestSchema: z.ZodObject<{
25
26
  type: z.ZodLiteral<"filePath">;
26
27
  value: z.ZodString;
27
28
  }, z.core.$strip>], "type">;
29
+ prompt: z.ZodOptional<z.ZodString>;
28
30
  type: z.ZodLiteral<"transcribeStream">;
29
31
  }, z.core.$strip>;
30
32
  export declare const transcribeStreamResponseSchema: z.ZodObject<{
@@ -38,6 +40,7 @@ export type TranscribeParams = z.infer<typeof transcribeParamsSchema>;
38
40
  export type TranscribeClientParams = {
39
41
  modelId: string;
40
42
  audioChunk: string | Buffer;
43
+ prompt?: string;
41
44
  };
42
45
  export type TranscribeStreamRequest = z.infer<typeof transcribeStreamRequestSchema>;
43
46
  export type TranscribeStreamResponse = z.infer<typeof transcribeStreamResponseSchema>;
@@ -12,6 +12,7 @@ export const audioInputSchema = z.discriminatedUnion("type", [
12
12
  export const transcribeParamsSchema = z.object({
13
13
  modelId: z.string(),
14
14
  audioChunk: audioInputSchema,
15
+ prompt: z.string().optional(),
15
16
  });
16
17
  // Streaming Transcribe Schema (for real-time audio streaming)
17
18
  export const transcribeStreamRequestSchema = transcribeParamsSchema.extend({
@@ -9,16 +9,65 @@ export declare const whisperConfigSchema: z.ZodObject<{
9
9
  json: "json";
10
10
  }>>;
11
11
  min_seconds: z.ZodDefault<z.ZodNumber>;
12
- max_seconds: z.ZodDefault<z.ZodNumber>;
13
- audio_format: z.ZodDefault<z.ZodString>;
14
12
  update_frequency: z.ZodDefault<z.ZodString>;
15
- vadConfig: z.ZodOptional<z.ZodObject<{
16
- vad_threshold: z.ZodOptional<z.ZodNumber>;
17
- vad_min_speech_duration_ms: z.ZodOptional<z.ZodNumber>;
18
- vad_min_silence_duration_ms: z.ZodOptional<z.ZodNumber>;
19
- vad_max_speech_duration_s: z.ZodOptional<z.ZodNumber>;
20
- vad_speech_pad_ms: z.ZodOptional<z.ZodNumber>;
21
- vad_samples_overlap: z.ZodOptional<z.ZodNumber>;
13
+ strategy: z.ZodOptional<z.ZodEnum<{
14
+ greedy: "greedy";
15
+ beam_search: "beam_search";
16
+ }>>;
17
+ n_threads: z.ZodOptional<z.ZodNumber>;
18
+ n_max_text_ctx: z.ZodOptional<z.ZodNumber>;
19
+ offset_ms: z.ZodOptional<z.ZodNumber>;
20
+ duration_ms: z.ZodOptional<z.ZodNumber>;
21
+ audio_ctx: z.ZodOptional<z.ZodNumber>;
22
+ translate: z.ZodOptional<z.ZodBoolean>;
23
+ no_context: z.ZodOptional<z.ZodBoolean>;
24
+ no_timestamps: z.ZodOptional<z.ZodBoolean>;
25
+ single_segment: z.ZodOptional<z.ZodBoolean>;
26
+ print_special: z.ZodOptional<z.ZodBoolean>;
27
+ print_progress: z.ZodOptional<z.ZodBoolean>;
28
+ print_realtime: z.ZodOptional<z.ZodBoolean>;
29
+ print_timestamps: z.ZodOptional<z.ZodBoolean>;
30
+ token_timestamps: z.ZodOptional<z.ZodBoolean>;
31
+ thold_pt: z.ZodOptional<z.ZodNumber>;
32
+ thold_ptsum: z.ZodOptional<z.ZodNumber>;
33
+ max_len: z.ZodOptional<z.ZodNumber>;
34
+ split_on_word: z.ZodOptional<z.ZodBoolean>;
35
+ max_tokens: z.ZodOptional<z.ZodNumber>;
36
+ debug_mode: z.ZodOptional<z.ZodBoolean>;
37
+ tdrz_enable: z.ZodOptional<z.ZodBoolean>;
38
+ suppress_regex: z.ZodOptional<z.ZodString>;
39
+ initial_prompt: z.ZodOptional<z.ZodString>;
40
+ language: z.ZodOptional<z.ZodString>;
41
+ detect_language: z.ZodOptional<z.ZodBoolean>;
42
+ suppress_blank: z.ZodOptional<z.ZodBoolean>;
43
+ suppress_nst: z.ZodOptional<z.ZodBoolean>;
44
+ temperature: z.ZodOptional<z.ZodNumber>;
45
+ length_penalty: z.ZodOptional<z.ZodNumber>;
46
+ temperature_inc: z.ZodOptional<z.ZodNumber>;
47
+ entropy_thold: z.ZodOptional<z.ZodNumber>;
48
+ logprob_thold: z.ZodOptional<z.ZodNumber>;
49
+ greedy_best_of: z.ZodOptional<z.ZodNumber>;
50
+ beam_search_beam_size: z.ZodOptional<z.ZodNumber>;
51
+ vadModelPath: z.ZodOptional<z.ZodString>;
52
+ vad_params: z.ZodOptional<z.ZodObject<{
53
+ threshold: z.ZodOptional<z.ZodNumber>;
54
+ min_speech_duration_ms: z.ZodOptional<z.ZodNumber>;
55
+ min_silence_duration_ms: z.ZodOptional<z.ZodNumber>;
56
+ max_speech_duration_s: z.ZodOptional<z.ZodNumber>;
57
+ speech_pad_ms: z.ZodOptional<z.ZodNumber>;
58
+ samples_overlap: z.ZodOptional<z.ZodNumber>;
59
+ }, z.core.$strip>>;
60
+ max_seconds: z.ZodOptional<z.ZodNumber>;
61
+ chunk_length_s: z.ZodOptional<z.ZodNumber>;
62
+ audio_format: z.ZodOptional<z.ZodString>;
63
+ contextParams: z.ZodOptional<z.ZodObject<{
64
+ model: z.ZodOptional<z.ZodString>;
65
+ use_gpu: z.ZodOptional<z.ZodBoolean>;
66
+ flash_attn: z.ZodOptional<z.ZodBoolean>;
67
+ gpu_device: z.ZodOptional<z.ZodNumber>;
68
+ }, z.core.$strip>>;
69
+ miscConfig: z.ZodOptional<z.ZodObject<{
70
+ caption_enabled: z.ZodOptional<z.ZodBoolean>;
22
71
  }, z.core.$strip>>;
23
72
  }, z.core.$strip>;
24
73
  export type WhisperConfig = z.infer<typeof whisperConfigSchema>;
@@ -1,18 +1,72 @@
1
1
  import { z } from "zod";
2
- const vadConfigSchema = z.object({
3
- vad_threshold: z.number().optional(),
4
- vad_min_speech_duration_ms: z.number().optional(),
5
- vad_min_silence_duration_ms: z.number().optional(),
6
- vad_max_speech_duration_s: z.number().optional(),
7
- vad_speech_pad_ms: z.number().optional(),
8
- vad_samples_overlap: z.number().optional(),
9
- });
2
+ const vadParamsSchema = z
3
+ .object({
4
+ threshold: z.number().optional(),
5
+ min_speech_duration_ms: z.number().optional(),
6
+ min_silence_duration_ms: z.number().optional(),
7
+ max_speech_duration_s: z.number().optional(),
8
+ speech_pad_ms: z.number().optional(),
9
+ samples_overlap: z.number().optional(),
10
+ })
11
+ .optional();
12
+ const contextParamsSchema = z
13
+ .object({
14
+ model: z.string().optional(),
15
+ use_gpu: z.boolean().optional(),
16
+ flash_attn: z.boolean().optional(),
17
+ gpu_device: z.number().optional(),
18
+ })
19
+ .optional();
20
+ const miscConfigSchema = z
21
+ .object({
22
+ caption_enabled: z.boolean().optional(),
23
+ })
24
+ .optional();
10
25
  export const whisperConfigSchema = z.object({
11
26
  mode: z.enum(["caption", "batch"]).default("caption"),
12
27
  output_format: z.enum(["plaintext", "json"]).default("plaintext"),
13
28
  min_seconds: z.number().default(1),
14
- max_seconds: z.number().default(30),
15
- audio_format: z.string().default("wav"),
16
29
  update_frequency: z.string().default("on_end"),
17
- vadConfig: vadConfigSchema.optional(),
30
+ strategy: z.enum(["greedy", "beam_search"]).optional(),
31
+ n_threads: z.number().int().optional(),
32
+ n_max_text_ctx: z.number().int().optional(),
33
+ offset_ms: z.number().int().optional(),
34
+ duration_ms: z.number().int().optional(),
35
+ audio_ctx: z.number().int().optional(),
36
+ translate: z.boolean().optional(),
37
+ no_context: z.boolean().optional(),
38
+ no_timestamps: z.boolean().optional(),
39
+ single_segment: z.boolean().optional(),
40
+ print_special: z.boolean().optional(),
41
+ print_progress: z.boolean().optional(),
42
+ print_realtime: z.boolean().optional(),
43
+ print_timestamps: z.boolean().optional(),
44
+ token_timestamps: z.boolean().optional(),
45
+ thold_pt: z.number().optional(),
46
+ thold_ptsum: z.number().optional(),
47
+ max_len: z.number().int().optional(),
48
+ split_on_word: z.boolean().optional(),
49
+ max_tokens: z.number().int().optional(),
50
+ debug_mode: z.boolean().optional(),
51
+ tdrz_enable: z.boolean().optional(),
52
+ suppress_regex: z.string().optional(),
53
+ initial_prompt: z.string().optional(),
54
+ language: z.string().optional(),
55
+ detect_language: z.boolean().optional(),
56
+ suppress_blank: z.boolean().optional(),
57
+ suppress_nst: z.boolean().optional(),
58
+ temperature: z.number().optional(),
59
+ length_penalty: z.number().optional(),
60
+ temperature_inc: z.number().optional(),
61
+ entropy_thold: z.number().optional(),
62
+ logprob_thold: z.number().optional(),
63
+ greedy_best_of: z.number().int().optional(),
64
+ beam_search_beam_size: z.number().int().optional(),
65
+ vadModelPath: z.string().optional(),
66
+ vad_params: vadParamsSchema,
67
+ max_seconds: z.number().optional(),
68
+ chunk_length_s: z.number().optional(),
69
+ audio_format: z.string().optional(),
70
+ contextParams: contextParamsSchema,
71
+ miscConfig: miscConfigSchema,
18
72
  });
@@ -2,7 +2,8 @@ import {} from "../../../../../schemas/index.js";
2
2
  import { getModel, getModelConfig, } from "../../../registry/model-registry.js";
3
3
  import { findMatchingCache, getCurrentCacheInfo, renameCacheFile, getCacheFilePath, extractSystemPrompt, generateConfigHash, customCacheExists, } from "../kv-cache-utils";
4
4
  import fs from "bare-fs";
5
- import { parseToolCalls, convertToolsToGrammar, } from "../../../../utils/tool-parser.js";
5
+ import { insertToolsIntoHistory, setupToolGrammar, checkForToolEvents, } from "../../../../utils/tool-integration.js";
6
+ import { parseToolCalls } from "../../../../utils/tool-parser.js";
6
7
  function transformMessage(message) {
7
8
  const transformed = [];
8
9
  // Check if it's a tool definition (has type: "function")
@@ -74,7 +75,6 @@ function prepareMessagesForCache(cachePathToUse, cacheExists, history) {
74
75
  }
75
76
  async function* processModelResponse(model, messagesToSend, shouldSaveCache, tools) {
76
77
  const response = await model.run(messagesToSend);
77
- // Create a promise that rejects when onError is triggered
78
78
  let rejectOnError = null;
79
79
  const errorPromise = new Promise((_, reject) => {
80
80
  rejectOnError = reject;
@@ -86,43 +86,23 @@ async function* processModelResponse(model, messagesToSend, shouldSaveCache, too
86
86
  });
87
87
  const iterator = response.iterate();
88
88
  let accumulatedText = "";
89
- const emittedToolCalls = new Set();
89
+ const emittedToolCallPositions = new Set();
90
+ let toolCallsResult = [];
90
91
  try {
91
92
  while (true) {
92
- // Race between getting the next token and an error occurring
93
93
  const result = await Promise.race([iterator.next(), errorPromise]);
94
94
  if (result.done) {
95
95
  break;
96
96
  }
97
97
  const token = result.value;
98
98
  accumulatedText += token;
99
+ yield { token };
99
100
  if (tools && tools.length > 0) {
100
- const { toolCalls, errors } = parseToolCalls(accumulatedText, tools);
101
- for (const call of toolCalls) {
102
- if (!emittedToolCalls.has(call.id)) {
103
- emittedToolCalls.add(call.id);
104
- const toolCallEvent = {
105
- type: "toolCall",
106
- call,
107
- };
108
- yield { token, toolCallEvent };
109
- continue;
110
- }
111
- }
112
- for (const error of errors) {
113
- const errorKey = `error:${error.code}:${error.message}`;
114
- if (!emittedToolCalls.has(errorKey)) {
115
- emittedToolCalls.add(errorKey);
116
- const toolCallEvent = {
117
- type: "toolCallError",
118
- error,
119
- };
120
- yield { token, toolCallEvent };
121
- continue;
122
- }
101
+ const toolEvents = checkForToolEvents(accumulatedText, token, tools, emittedToolCallPositions);
102
+ for (const toolEvent of toolEvents) {
103
+ yield { token: "", toolCallEvent: toolEvent };
123
104
  }
124
105
  }
125
- yield { token };
126
106
  }
127
107
  }
128
108
  catch (error) {
@@ -133,6 +113,10 @@ async function* processModelResponse(model, messagesToSend, shouldSaveCache, too
133
113
  throw new Error(String(error));
134
114
  }
135
115
  }
116
+ if (tools && tools.length > 0) {
117
+ const { toolCalls } = parseToolCalls(accumulatedText, tools);
118
+ toolCallsResult = toolCalls;
119
+ }
136
120
  if (shouldSaveCache) {
137
121
  await model.run([{ role: "session", content: "save" }]);
138
122
  }
@@ -142,30 +126,16 @@ async function* processModelResponse(model, messagesToSend, shouldSaveCache, too
142
126
  tokensPerSecond: responseWithStats.stats?.TPS ?? 0,
143
127
  cacheTokens: responseWithStats.stats?.CacheTokens ?? 0,
144
128
  };
145
- const { toolCalls } = parseToolCalls(accumulatedText, tools || []);
146
- return { stats, toolCalls };
129
+ return { stats, toolCalls: toolCallsResult };
147
130
  }
148
131
  export async function* completion(params) {
149
132
  const { history, modelId, kvCache, tools } = params;
150
- // Check if model was configured with tools support
151
133
  const modelConfig = getModelConfig(modelId);
152
134
  const toolsEnabled = modelConfig.tools === true;
153
135
  let historyWithTools = history;
154
136
  if (tools && tools.length > 0 && toolsEnabled) {
155
- // Insert tools after system message (if exists) or at the beginning
156
- const systemMsgIndex = history.findIndex((msg) => msg.role === "system");
157
- if (systemMsgIndex >= 0) {
158
- historyWithTools = [
159
- ...history.slice(0, systemMsgIndex + 1),
160
- ...tools,
161
- ...history.slice(systemMsgIndex + 1),
162
- ];
163
- }
164
- else {
165
- historyWithTools = [...tools, ...history];
166
- }
167
- const grammar = convertToolsToGrammar(tools);
168
- modelConfig.grammar = grammar;
137
+ historyWithTools = insertToolsIntoHistory(history, tools);
138
+ setupToolGrammar(modelConfig, tools);
169
139
  }
170
140
  const transformedHistory = transformMessages(historyWithTools);
171
141
  const model = getModel(modelId);
@@ -23,11 +23,12 @@ function createFilesystemDLMimic(options) {
23
23
  }
24
24
  export function createWhisperModel(modelId, modelPath, whisperConfig, vadModelPath) {
25
25
  const { dirPath, basePath } = parseModelPath(modelPath);
26
- // Handle VAD model path - now uses vadModelPath parameter for consistency
26
+ // Handle VAD model path
27
27
  let vadModelName = "";
28
28
  let vadDirPath = "./";
29
- if (vadModelPath) {
30
- const vadParsed = parseModelPath(vadModelPath);
29
+ const effectiveVadPath = vadModelPath || whisperConfig.vadModelPath;
30
+ if (effectiveVadPath) {
31
+ const vadParsed = parseModelPath(effectiveVadPath);
31
32
  vadDirPath = vadParsed.dirPath;
32
33
  vadModelName = vadParsed.basePath;
33
34
  }
@@ -45,11 +46,11 @@ export function createWhisperModel(modelId, modelPath, whisperConfig, vadModelPa
45
46
  stats: false,
46
47
  },
47
48
  };
49
+ const { contextParams, miscConfig, ...whisperParams } = whisperConfig;
48
50
  const config = {
49
- whisperConfig: {
50
- ...whisperConfig,
51
- vad_model: vadModelPath || "",
52
- },
51
+ whisperConfig: whisperParams,
52
+ ...(contextParams && { contextParams }),
53
+ ...(miscConfig && { miscConfig }),
53
54
  };
54
55
  return new TranscriptionWhispercpp(args, config);
55
56
  }
@@ -1,10 +1,24 @@
1
- import { getModel } from "../../../registry/model-registry.js";
1
+ import { getModel, getModelConfig, updateModelConfig, } from "../../../registry/model-registry.js";
2
2
  import { Readable } from "bare-stream";
3
3
  import fs from "bare-fs";
4
4
  import { needsDecoding, decodeAudioToStream } from "../../../../utils/index.js";
5
5
  import {} from "../../../../../schemas/index.js";
6
6
  export async function* transcribe(params) {
7
7
  const model = getModel(params.modelId);
8
+ if (params.prompt && typeof model.reload === "function") {
9
+ const existingConfig = getModelConfig(params.modelId);
10
+ const updatedConfig = {
11
+ ...existingConfig,
12
+ initial_prompt: params.prompt,
13
+ };
14
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
15
+ const { contextParams: _, miscConfig, ...whisperParams } = updatedConfig;
16
+ await model.reload({
17
+ whisperConfig: whisperParams,
18
+ ...(miscConfig && { miscConfig }),
19
+ });
20
+ updateModelConfig(params.modelId, updatedConfig);
21
+ }
8
22
  let audioStream;
9
23
  const audioChunk = params.audioChunk;
10
24
  switch (audioChunk.type) {
@@ -25,6 +25,7 @@ export interface AnyModel {
25
25
  }>>;
26
26
  await(): Promise<void>;
27
27
  }>;
28
+ reload?(config: unknown): Promise<void>;
28
29
  unload?(): Promise<void>;
29
30
  addon?: AddonInterface;
30
31
  }
@@ -66,6 +67,7 @@ export declare function getModelInfo(id: string): {
66
67
  config: unknown;
67
68
  } | null;
68
69
  export declare function getModelConfig(id: string): unknown;
70
+ export declare function updateModelConfig(id: string, config: unknown): void;
69
71
  export declare function clearRegistry(): void;
70
72
  export declare function getRegistryStats(): {
71
73
  totalModels: number;
@@ -80,6 +80,13 @@ export function getModelConfig(id) {
80
80
  }
81
81
  return entry.local.config;
82
82
  }
83
+ export function updateModelConfig(id, config) {
84
+ const entry = modelRegistry.get(id);
85
+ if (!entry || !entry.local) {
86
+ throw new Error(`Model with ID "${id}" not found or is delegated`);
87
+ }
88
+ entry.local.config = config;
89
+ }
83
90
  export function clearRegistry() {
84
91
  modelRegistry.clear();
85
92
  console.log("Model registry cleared");
@@ -23,7 +23,6 @@ export async function* handleCompletionStream(request) {
23
23
  done = true;
24
24
  }
25
25
  else {
26
- buffer += result.value.token;
27
26
  if (request.stream) {
28
27
  yield {
29
28
  type: "completionStream",
@@ -31,11 +30,14 @@ export async function* handleCompletionStream(request) {
31
30
  toolCallEvent: result.value.toolCallEvent,
32
31
  };
33
32
  }
33
+ else {
34
+ buffer += result.value.token;
35
+ }
34
36
  }
35
37
  }
36
38
  yield {
37
39
  type: "completionStream",
38
- token: buffer,
40
+ token: request.stream ? "" : buffer,
39
41
  done: true,
40
42
  stats,
41
43
  toolCalls,
@@ -27,11 +27,10 @@ async function validateCachedFile(modelPath, url, signal) {
27
27
  expectedSize = parseInt(response.headers.get("content-length") || "0");
28
28
  }
29
29
  catch (error) {
30
- console.log(`⚠️ Could not get expected file size from HEAD request: ${String(error)}`);
31
- console.log(`✅ Using cached HTTP model: ${modelPath}`);
32
- return modelPath;
30
+ console.log(`⚠️ Could not get expected file size from HEAD request, will re-download: ${String(error)}`);
31
+ return null;
33
32
  }
34
- if (expectedSize > 0 && localSize !== expectedSize) {
33
+ if (localSize !== expectedSize) {
35
34
  console.log(`📥 Cached file size mismatch. Expected: ${expectedSize}, Found: ${localSize}. Re-downloading...`);
36
35
  return null;
37
36
  }
@@ -64,6 +63,13 @@ async function performHttpDownload(url, modelPath, downloadKey, progressCallback
64
63
  const headers = {
65
64
  "User-Agent": "qvac-sdk",
66
65
  };
66
+ const isPresignedUrl = url.includes("X-Amz-") || // AWS S3
67
+ url.includes("X-Goog-") || // Google Cloud Storage
68
+ (url.includes("sig=") && url.includes("se=")); // Azure Blob Storage SAS
69
+ if (isPresignedUrl) {
70
+ const urlObj = new URL(url);
71
+ headers["host"] = urlObj.hostname; // Override to prevent :443 from being appended
72
+ }
67
73
  if (startOffset > 0) {
68
74
  headers["Range"] = `bytes=${startOffset}-`;
69
75
  }
@@ -1,9 +1,13 @@
1
1
  import { transcribeStream } from "../../bare/addons/whispercpp/index.js";
2
2
  export async function* handleTranscribeStream(request) {
3
- const { modelId, audioChunk } = request;
3
+ const { modelId, audioChunk, prompt } = request;
4
4
  try {
5
5
  // Stream transcription results in real-time
6
- for await (const textChunk of transcribeStream({ audioChunk, modelId })) {
6
+ for await (const textChunk of transcribeStream({
7
+ audioChunk,
8
+ modelId,
9
+ prompt,
10
+ })) {
7
11
  yield {
8
12
  type: "transcribeStream",
9
13
  text: textChunk,
@@ -0,0 +1,13 @@
1
+ import type { Tool, ToolCallEvent } from "../../schemas/index";
2
+ interface HistoryMessage {
3
+ role: string;
4
+ content: string;
5
+ attachments?: {
6
+ path: string;
7
+ }[] | undefined;
8
+ }
9
+ export declare function insertToolsIntoHistory(history: HistoryMessage[], tools: Tool[]): Array<HistoryMessage | Tool>;
10
+ export declare function setupToolGrammar(modelConfig: Record<string, unknown>, tools: Tool[]): void;
11
+ export declare function checkForToolEvents(accumulatedText: string, currentToken: string, tools: Tool[], emittedToolCallPositions: Set<number>): ToolCallEvent[];
12
+ export {};
13
+ //# sourceMappingURL=tool-integration.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { parseToolCalls, convertToolsToGrammar, } from "./tool-parser.js";
2
+ export function insertToolsIntoHistory(history, tools) {
3
+ const systemMsgIndex = history.findIndex((msg) => msg.role === "system");
4
+ if (systemMsgIndex >= 0) {
5
+ return [
6
+ ...history.slice(0, systemMsgIndex + 1),
7
+ ...tools,
8
+ ...history.slice(systemMsgIndex + 1),
9
+ ];
10
+ }
11
+ return [...tools, ...history];
12
+ }
13
+ export function setupToolGrammar(modelConfig, tools) {
14
+ const grammar = convertToolsToGrammar(tools);
15
+ modelConfig["grammar"] = grammar;
16
+ }
17
+ export function checkForToolEvents(accumulatedText, currentToken, tools, emittedToolCallPositions) {
18
+ const events = [];
19
+ if (currentToken.includes("</tool_call>") || currentToken.includes("}")) {
20
+ const { toolCalls, errors } = parseToolCalls(accumulatedText, tools);
21
+ for (const call of toolCalls) {
22
+ const callPosition = accumulatedText.indexOf(call.raw || "");
23
+ if (callPosition >= 0 && !emittedToolCallPositions.has(callPosition)) {
24
+ emittedToolCallPositions.add(callPosition);
25
+ events.push({
26
+ type: "toolCall",
27
+ call,
28
+ });
29
+ }
30
+ }
31
+ for (const error of errors) {
32
+ const errorPosition = accumulatedText.indexOf(error.raw || "");
33
+ if (errorPosition >= 0 && !emittedToolCallPositions.has(errorPosition)) {
34
+ emittedToolCallPositions.add(errorPosition);
35
+ events.push({
36
+ type: "toolCallError",
37
+ error,
38
+ });
39
+ }
40
+ }
41
+ }
42
+ return events;
43
+ }
@@ -1,5 +1,14 @@
1
- function generateToolCallId() {
2
- return `call_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1
+ let toolCallSequence = 0;
2
+ function generateStableToolCallId(name, args) {
3
+ const content = `${name}:${JSON.stringify(args)}`;
4
+ let hash = 0;
5
+ for (let i = 0; i < content.length; i++) {
6
+ const char = content.charCodeAt(i);
7
+ hash = (hash << 5) - hash + char;
8
+ hash = hash & hash;
9
+ }
10
+ const sequence = toolCallSequence++;
11
+ return `call_${Math.abs(hash).toString(36)}_${sequence}`;
3
12
  }
4
13
  function isValidToolCall(obj) {
5
14
  if (!obj || typeof obj !== "object") {
@@ -63,7 +72,7 @@ function parseGemmaFormat(text, tools) {
63
72
  continue;
64
73
  }
65
74
  toolCalls.push({
66
- id: call.id || generateToolCallId(),
75
+ id: call.id || generateStableToolCallId(call.name, call.arguments),
67
76
  name: call.name,
68
77
  arguments: call.arguments,
69
78
  raw: JSON.stringify(call),
@@ -80,13 +89,14 @@ function parseQwenFormat(text, tools) {
80
89
  const toolCalls = [];
81
90
  const errors = [];
82
91
  const toolCallRegex = /<tool_call>\s*({[\s\S]*?})\s*<\/tool_call>/g;
83
- let match;
84
- while ((match = toolCallRegex.exec(text)) !== null) {
92
+ const matches = Array.from(text.matchAll(toolCallRegex));
93
+ for (const match of matches) {
85
94
  try {
86
95
  const callJson = match[1];
87
96
  if (!callJson)
88
97
  continue;
89
- const callItem = JSON.parse(callJson);
98
+ const trimmedJson = callJson.trim();
99
+ const callItem = JSON.parse(trimmedJson);
90
100
  if (!isValidToolCall(callItem)) {
91
101
  continue;
92
102
  }
@@ -95,15 +105,15 @@ function parseQwenFormat(text, tools) {
95
105
  if (!validation.isValid && validation.error) {
96
106
  errors.push({
97
107
  ...validation.error,
98
- raw: callJson,
108
+ raw: trimmedJson,
99
109
  });
100
110
  continue;
101
111
  }
102
112
  toolCalls.push({
103
- id: call.id || generateToolCallId(),
113
+ id: call.id || generateStableToolCallId(call.name, call.arguments),
104
114
  name: call.name,
105
115
  arguments: call.arguments,
106
- raw: callJson,
116
+ raw: trimmedJson,
107
117
  });
108
118
  }
109
119
  catch (error) {
@@ -132,7 +142,8 @@ function parseLlamacppFormat(text, tools) {
132
142
  }
133
143
  else {
134
144
  toolCalls.push({
135
- id: parsed.id || generateToolCallId(),
145
+ id: parsed.id ||
146
+ generateStableToolCallId(parsed.name, parsed.arguments),
136
147
  name: parsed.name,
137
148
  arguments: parsed.arguments,
138
149
  raw: text,
@@ -164,7 +175,7 @@ function parseGenericFormat(text, tools) {
164
175
  continue;
165
176
  }
166
177
  toolCalls.push({
167
- id: obj.id || generateToolCallId(),
178
+ id: obj.id || generateStableToolCallId(obj.name, obj.arguments),
168
179
  name: obj.name,
169
180
  arguments: obj.arguments,
170
181
  raw: match[0],
@@ -186,17 +197,20 @@ export function parseToolCalls(text, tools) {
186
197
  return { toolCalls: [], errors: [] };
187
198
  }
188
199
  let result = parseGemmaFormat(text, tools);
189
- if (result.toolCalls.length > 0 || result.errors.length > 0) {
200
+ if (result.toolCalls.length > 0) {
190
201
  return result;
191
202
  }
192
203
  result = parseQwenFormat(text, tools);
193
- if (result.toolCalls.length > 0 || result.errors.length > 0) {
204
+ if (result.toolCalls.length > 0) {
194
205
  return result;
195
206
  }
196
207
  result = parseLlamacppFormat(text, tools);
197
- if (result.toolCalls.length > 0 || result.errors.length > 0) {
208
+ if (result.toolCalls.length > 0) {
198
209
  return result;
199
210
  }
211
+ if (text.includes("<tool_call>")) {
212
+ return { toolCalls: [], errors: [] };
213
+ }
200
214
  result = parseGenericFormat(text, tools);
201
215
  return result;
202
216
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/sdk",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -60,7 +60,7 @@
60
60
  "@qvac/langdetect-text": "^0.1.0",
61
61
  "@qvac/logging": "^0.1.0",
62
62
  "@qvac/rag": "^0.2.1",
63
- "@qvac/transcription-whispercpp": "^0.1.8",
63
+ "@qvac/transcription-whispercpp": "^0.1.14",
64
64
  "@qvac/translation-nmtcpp": "^0.1.2",
65
65
  "@qvac/tts-onnx": "^0.2.1",
66
66
  "fast-safe-stringify": "2.1.1",