@qvac/sdk 0.17.1 → 0.18.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.
- package/NOTICE +124 -64
- package/README.md +19 -0
- package/dist/client/api/audio-gen.d.ts +17 -0
- package/dist/client/api/audio-gen.js +17 -0
- package/dist/client/api/finetune.d.ts +2 -0
- package/dist/client/api/finetune.js +7 -1
- package/dist/client/api/index.d.ts +1 -1
- package/dist/client/api/index.js +1 -1
- package/dist/client/api/load-model.d.ts +8 -0
- package/dist/client/api/transcribe.d.ts +19 -2
- package/dist/client/api/transcribe.js +66 -27
- package/dist/client/api/translate.d.ts +1 -0
- package/dist/client/api/translate.js +40 -19
- package/dist/client/api/vla.d.ts +42 -1
- package/dist/client/api/vla.js +50 -1
- package/dist/commands/bundle/entry-gen.d.ts +4 -0
- package/dist/commands/bundle/entry-gen.js +6 -0
- package/dist/commands/bundle/index.js +19 -12
- package/dist/examples/asr/parakeet-indic-conformer-filesystem.d.ts +2 -0
- package/dist/examples/asr/parakeet-indic-conformer-filesystem.js +52 -0
- package/dist/examples/audiogen/generate-cover.d.ts +2 -0
- package/dist/examples/audiogen/generate-cover.js +100 -0
- package/dist/examples/llamacpp-fallback.d.ts +2 -0
- package/dist/examples/llamacpp-fallback.js +34 -0
- package/dist/examples/multi-job-completion.d.ts +28 -0
- package/dist/examples/multi-job-completion.js +128 -0
- package/dist/examples/profiling/basic.js +32 -1
- package/dist/examples/system-resources.d.ts +2 -0
- package/dist/examples/system-resources.js +86 -0
- package/dist/examples/tts/audio8.d.ts +2 -0
- package/dist/examples/tts/audio8.js +54 -0
- package/dist/examples/tts/cosyvoice3.d.ts +2 -0
- package/dist/examples/tts/cosyvoice3.js +52 -0
- package/dist/examples/vla-groot.js +92 -49
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/models/registry/models.d.ts +782 -6
- package/dist/models/registry/models.js +2664 -1859
- package/dist/schemas/audio-gen.d.ts +103 -0
- package/dist/schemas/audio-gen.js +135 -4
- package/dist/schemas/common.d.ts +550 -11
- package/dist/schemas/constants-registry.d.ts +18 -4
- package/dist/schemas/constants-registry.js +16 -3
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/llamacpp-config.d.ts +10 -10
- package/dist/schemas/llamacpp-config.js +168 -82
- package/dist/schemas/load-model.d.ts +3678 -323
- package/dist/schemas/load-model.js +21 -3
- package/dist/schemas/public.d.ts +3 -0
- package/dist/schemas/public.js +6 -0
- package/dist/schemas/sdk-config.d.ts +60 -24
- package/dist/schemas/sdk-errors-server.d.ts +1 -0
- package/dist/schemas/sdk-errors-server.js +5 -0
- package/dist/schemas/text-to-speech.d.ts +1086 -110
- package/dist/schemas/text-to-speech.js +194 -4
- package/dist/schemas/tools.d.ts +0 -16
- package/dist/schemas/tools.js +0 -15
- package/dist/schemas/transcription-config.d.ts +2 -0
- package/dist/schemas/transcription-config.js +10 -4
- package/dist/schemas/transcription.d.ts +3 -0
- package/dist/schemas/vla.d.ts +83 -0
- package/dist/schemas/vla.js +105 -2
- package/dist/scripts/link-workspace-inference.d.ts +2 -0
- package/dist/scripts/link-workspace-inference.js +31 -0
- package/dist/server/bare/ops/cancel.d.ts +1 -1
- package/dist/server/bare/ops/cancel.js +1 -1
- package/dist/server/bare/ops/kv-cache-retention.js +11 -0
- package/dist/server/bare/ops/kv-cache-utils.d.ts +1 -1
- package/dist/server/bare/ops/kv-cache-utils.js +40 -12
- package/dist/server/bare/ops/transcribe.d.ts +2 -2
- package/dist/server/bare/ops/transcribe.js +54 -48
- package/dist/server/bare/ops/translate-stats.d.ts +5 -0
- package/dist/server/bare/ops/translate-stats.js +11 -0
- package/dist/server/bare/ops/translate.js +66 -38
- package/dist/server/bare/ops/unload-model.js +30 -24
- package/dist/server/bare/plugins/audiogen-ggml/ops/audio-gen-input.d.ts +20 -0
- package/dist/server/bare/plugins/audiogen-ggml/ops/audio-gen-input.js +153 -0
- package/dist/server/bare/plugins/audiogen-ggml/ops/audio-gen-stream.js +52 -1
- package/dist/server/bare/plugins/audiogen-ggml/plugin.d.ts +28 -0
- package/dist/server/bare/plugins/ggml-ocr/plugin.js +3 -1
- package/dist/server/bare/plugins/ggml-vla/ops/vla-set-embodiment.d.ts +3 -0
- package/dist/server/bare/plugins/ggml-vla/ops/vla-set-embodiment.js +9 -0
- package/dist/server/bare/plugins/ggml-vla/plugin.d.ts +37 -0
- package/dist/server/bare/plugins/ggml-vla/plugin.js +21 -4
- package/dist/server/bare/plugins/llamacpp-completion/ops/batch-completion-stream.js +16 -15
- package/dist/server/bare/plugins/llamacpp-completion/ops/cache-logger.d.ts +1 -1
- package/dist/server/bare/plugins/llamacpp-completion/ops/cache-logger.js +2 -2
- package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stats.d.ts +6 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stats.js +10 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stream.d.ts +1 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stream.js +131 -123
- package/dist/server/bare/plugins/llamacpp-completion/ops/finetune.js +110 -20
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-session.d.ts +49 -23
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-session.js +308 -143
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-state.d.ts +1 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-state.js +2 -2
- package/dist/server/bare/plugins/llamacpp-completion/plugin.d.ts +6 -6
- package/dist/server/bare/plugins/llamacpp-completion/plugin.js +55 -36
- package/dist/server/bare/plugins/llamacpp-completion/transform.js +1 -7
- package/dist/server/bare/plugins/llamacpp-embedding/plugin.js +2 -3
- package/dist/server/bare/plugins/nmtcpp-translation/plugin.js +6 -5
- package/dist/server/bare/plugins/parakeet-transcription/plugin.js +27 -16
- package/dist/server/bare/plugins/tts-ggml/ops/parler-options.js +23 -3
- package/dist/server/bare/plugins/tts-ggml/plugin.d.ts +302 -4
- package/dist/server/bare/plugins/tts-ggml/plugin.js +131 -0
- package/dist/server/bare/plugins/whispercpp-transcription/plugin.js +48 -39
- package/dist/server/bare/registry/model-registry.d.ts +1 -0
- package/dist/server/bare/runtime/request-registry-singleton.js +29 -15
- package/dist/server/bare/runtime/request-registry.d.ts +27 -0
- package/dist/server/bare/runtime/request-registry.js +246 -76
- package/dist/server/bare/types/addon-responses.d.ts +4 -12
- package/dist/server/rpc/handlers/load-model/handler.js +2 -2
- package/dist/server/rpc/handlers/load-model/resolve-session.d.ts +1 -1
- package/dist/server/rpc/handlers/load-model/resolve-session.js +3 -3
- package/dist/server/rpc/handlers/load-model/resolve.d.ts +12 -2
- package/dist/server/rpc/handlers/load-model/resolve.js +90 -12
- package/dist/server/utils/audio/decoder.d.ts +37 -1
- package/dist/server/utils/audio/decoder.js +81 -51
- package/dist/server/utils/config-transform.d.ts +3 -0
- package/dist/server/utils/config-transform.js +6 -0
- package/dist/server/utils/index.d.ts +0 -1
- package/dist/server/utils/index.js +0 -1
- package/dist/server/utils/shard-utils.d.ts +1 -0
- package/dist/server/utils/shard-utils.js +14 -0
- package/dist/server/utils/tool-integration.d.ts +3 -10
- package/dist/server/utils/tool-integration.js +3 -12
- package/dist/utils/errors-server.d.ts +3 -0
- package/dist/utils/errors-server.js +5 -0
- package/package.json +17 -14
- package/dist/examples/llamacpp-dynamic-tools.d.ts +0 -2
- package/dist/examples/llamacpp-dynamic-tools.js +0 -152
- package/dist/models/update-models/codegen.d.ts +0 -3
- package/dist/models/update-models/codegen.js +0 -196
- package/dist/models/update-models/companions.d.ts +0 -30
- package/dist/models/update-models/companions.js +0 -296
- package/dist/models/update-models/history.d.ts +0 -24
- package/dist/models/update-models/history.js +0 -107
- package/dist/models/update-models/index.d.ts +0 -2
- package/dist/models/update-models/index.js +0 -123
- package/dist/models/update-models/naming.d.ts +0 -4
- package/dist/models/update-models/naming.js +0 -396
- package/dist/models/update-models/processing.d.ts +0 -8
- package/dist/models/update-models/processing.js +0 -65
- package/dist/models/update-models/registry.d.ts +0 -5
- package/dist/models/update-models/registry.js +0 -87
- package/dist/models/update-models/shards.d.ts +0 -4
- package/dist/models/update-models/shards.js +0 -57
- package/dist/models/update-models/types.d.ts +0 -88
- package/dist/models/update-models/types.js +0 -1
- package/dist/models/update-models/utils.d.ts +0 -3
- package/dist/models/update-models/utils.js +0 -21
- package/dist/server/bare/plugins/llamacpp-completion/ops/batch-cancelled.d.ts +0 -23
- package/dist/server/bare/plugins/llamacpp-completion/ops/batch-cancelled.js +0 -30
- package/dist/server/utils/expand-gguf-shards.d.ts +0 -3
- package/dist/server/utils/expand-gguf-shards.js +0 -22
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { TOOLS_MODE } from '../../../../../schemas/tools.js';
|
|
2
1
|
import { logCacheDisabled, logCacheInit, logCacheSave, logMessagesToAddon } from '../../../../../server/bare/plugins/llamacpp-completion/ops/cache-logger.js';
|
|
3
2
|
import { extractSystemPrompt, getCurrentCacheInfo } from '../../../../../server/bare/ops/kv-cache-utils.js';
|
|
4
3
|
import { getModel, getModelConfig } from '../../../../../server/bare/registry/model-registry.js';
|
|
5
4
|
import { decideCachedHistorySlice, shouldCommitCachedTurn } from '../../../../../server/bare/plugins/llamacpp-completion/ops/kv-cache-state.js';
|
|
6
5
|
import { createKvCacheSession, generateConfigHash } from '../../../../../server/bare/plugins/llamacpp-completion/ops/kv-cache-session.js';
|
|
7
|
-
import {
|
|
6
|
+
import { detectToolDialect, prependToolsToHistory } from '../../../../../server/utils/tool-integration.js';
|
|
8
7
|
import { parseToolCalls } from '../../../../../server/utils/tools/index.js';
|
|
9
8
|
import { getResponseFormatJsonSchema } from '../../../../../server/utils/response-format.js';
|
|
10
9
|
import { buildAutoCacheSaveHistory } from '../../../../../server/utils/index.js';
|
|
@@ -56,29 +55,60 @@ export function transformMessages(messages) {
|
|
|
56
55
|
}
|
|
57
56
|
return transformed;
|
|
58
57
|
}
|
|
59
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Prime the cache prefix with the system prompt only.
|
|
60
|
+
*
|
|
61
|
+
* Tools deliberately stay out of the prefix: a message list with no user turn
|
|
62
|
+
* is not a renderable conversation for every chat template (Qwen3.5 raises
|
|
63
|
+
* `No user query found in messages.` because it anchors its tool block on the
|
|
64
|
+
* last user query), and a template failure degrades the whole render rather
|
|
65
|
+
* than just the tool block. Tools travel with the turn instead.
|
|
66
|
+
*/
|
|
67
|
+
async function initSystemPromptCache(model, cachePathToUse, systemPromptToUse, cacheKey, onResponse) {
|
|
60
68
|
const primeMessages = [{ role: 'system', content: systemPromptToUse }];
|
|
61
|
-
|
|
62
|
-
if (tools && tools.length > 0) {
|
|
63
|
-
const transformedTools = transformMessages(tools);
|
|
64
|
-
primeMessages.push(...transformedTools);
|
|
65
|
-
toolCount = tools.length;
|
|
66
|
-
}
|
|
67
|
-
logCacheInit(cacheKey, systemPromptToUse, toolCount);
|
|
69
|
+
logCacheInit(cacheKey, systemPromptToUse);
|
|
68
70
|
logMessagesToAddon(primeMessages, 'CACHE_INIT');
|
|
69
71
|
const primeResponse = await runModel(model, primeMessages, {
|
|
70
72
|
cacheKey: cachePathToUse,
|
|
71
73
|
saveCacheToDisk: true,
|
|
72
74
|
prefill: true
|
|
73
75
|
});
|
|
76
|
+
// Register the prime so an abort during a cold-cache prefill cancels it.
|
|
77
|
+
onResponse?.(primeResponse);
|
|
74
78
|
await primeResponse.await();
|
|
75
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Attach the tool block ahead of a turn payload, mirroring the no-kv-cache
|
|
82
|
+
* path (`prependToolsToHistory`).
|
|
83
|
+
*/
|
|
84
|
+
function withToolBlock(messages, toolBlock) {
|
|
85
|
+
if (toolBlock.length === 0)
|
|
86
|
+
return messages;
|
|
87
|
+
return [...toolBlock, ...messages];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Whether a payload carrying this tool block actually gets it in front of the
|
|
91
|
+
* model. Qwen-family templates anchor their tool section on the last user
|
|
92
|
+
* query and raise without one, and the addon answers that by re-rendering the
|
|
93
|
+
* turn with tools stripped — a usable prompt with no tools in it. Recording
|
|
94
|
+
* such a turn as "the block is cached now" would suppress the block for the
|
|
95
|
+
* rest of the session, so require a user message before believing it landed.
|
|
96
|
+
*/
|
|
97
|
+
function rendersToolBlock(messages, toolBlock) {
|
|
98
|
+
if (toolBlock.length === 0)
|
|
99
|
+
return false;
|
|
100
|
+
return messages.some((msg) => msg.role === 'user');
|
|
101
|
+
}
|
|
76
102
|
/**
|
|
77
103
|
* Pick the messages that need to reach the model for the next turn.
|
|
78
104
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
105
|
+
* Tools are never baked into the primed prefix — a prefix with no user turn is
|
|
106
|
+
* not a renderable conversation for every template — so they travel with a
|
|
107
|
+
* turn instead.
|
|
108
|
+
*
|
|
109
|
+
* - Empty history: nothing to slice; send whatever non-system messages
|
|
110
|
+
* exist. (The call site always reports the cache as existing, so this
|
|
111
|
+
* is the only way into this branch.)
|
|
82
112
|
* - Cache hit with a recorded `savedCount`: send only the unsaved tail
|
|
83
113
|
* (`history.slice(savedCount)`), so a multi-message turn (e.g. a
|
|
84
114
|
* consumer pushing both an assistant transcript and a follow-up user
|
|
@@ -86,66 +116,46 @@ async function initSystemPromptCache(model, cachePathToUse, systemPromptToUse, c
|
|
|
86
116
|
* - Cache hit with a stale/missing `savedCount`: fall back to the full
|
|
87
117
|
* non-system history. The session is told (`dropStaleSavedCount`) so
|
|
88
118
|
* the bad boundary doesn't propagate into the next turn.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* - The addon anchors the tool block after the last user message and
|
|
92
|
-
* trims tools + the assistant's tool-call output from the cache once
|
|
93
|
-
* the chain resolves. After that trim, the cache only holds messages
|
|
94
|
-
* up to the last user turn, so the SDK has to ship the right slice
|
|
95
|
-
* plus the (possibly new) tool set:
|
|
96
|
-
* * tool-chain continuation (last role is "tool"): send the trailing
|
|
97
|
-
* consecutive tool messages, no tool block — tools are still
|
|
98
|
-
* anchored in the cache from the previous round.
|
|
99
|
-
* * new user turn after a chain (prev role is "assistant"): send
|
|
100
|
-
* [assistant, user] so the model sees its own final reply before
|
|
101
|
-
* the new prompt, then re-anchor the tool block.
|
|
102
|
-
* * otherwise: send just the last message + tool block.
|
|
119
|
+
* - The tool block travels only with the turn that writes it into the
|
|
120
|
+
* cache; see `skipToolBlock` below.
|
|
103
121
|
*/
|
|
104
|
-
function prepareMessagesForCache(session, turn, cacheExists, history, tools) {
|
|
105
|
-
const
|
|
106
|
-
const dynamic = addTools.length > 0;
|
|
122
|
+
function prepareMessagesForCache(session, turn, cacheExists, history, tools, toolBlockEvictable = false) {
|
|
123
|
+
const toolBlock = tools?.length ? transformMessages(tools) : [];
|
|
107
124
|
if (!(cacheExists && history.length > 0)) {
|
|
108
125
|
const historyWithoutSystem = history.filter((msg) => msg.role !== 'system');
|
|
109
|
-
return
|
|
126
|
+
return {
|
|
127
|
+
messages: withToolBlock(transformMessages(historyWithoutSystem), toolBlock),
|
|
128
|
+
toolBlockCached: rendersToolBlock(historyWithoutSystem, toolBlock)
|
|
129
|
+
};
|
|
110
130
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
session.dropStaleSavedCount(turn);
|
|
124
|
-
}
|
|
125
|
-
return transformMessages(messages);
|
|
126
|
-
}
|
|
127
|
-
// Dynamic path. The addon trimmed tools after the previous round, so the
|
|
128
|
-
// cache no longer holds the saved-count we'd rely on for slicing — pick
|
|
129
|
-
// the right fragment based on the role of the last history message.
|
|
130
|
-
const lastMsg = history[history.length - 1];
|
|
131
|
-
if (lastMsg.role === 'tool') {
|
|
132
|
-
const trailingTools = [];
|
|
133
|
-
for (let i = history.length - 1; i >= 0; i--) {
|
|
134
|
-
const msg = history[i];
|
|
135
|
-
if (msg.role !== 'tool')
|
|
136
|
-
break;
|
|
137
|
-
trailingTools.unshift(msg);
|
|
138
|
-
}
|
|
139
|
-
return transformMessages(trailingTools);
|
|
131
|
+
// Slice from the turn's `savedCount` so callers can
|
|
132
|
+
// stage multiple messages between completions. `decideCachedHistorySlice`
|
|
133
|
+
// also guards against the QVAC-17780 stale-count regression: if the
|
|
134
|
+
// saved boundary would slice the history down to an empty payload
|
|
135
|
+
// (e.g. after a cancelled mid-decode), it falls back to the full
|
|
136
|
+
// non-system history and signals the caller to drop the bad entry.
|
|
137
|
+
// The session owns the entry; `dropStaleSavedCount` clears it
|
|
138
|
+
// without touching the on-disk file (the file is still trustworthy
|
|
139
|
+
// — only the boundary count is wrong).
|
|
140
|
+
const { messages, clearStaleCount } = decideCachedHistorySlice(turn.savedCount, cacheExists, history);
|
|
141
|
+
if (clearStaleCount) {
|
|
142
|
+
session.dropStaleSavedCount(turn);
|
|
140
143
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
// The block is never trimmed back out of the cache, so re-sending it every
|
|
145
|
+
// turn would leave one copy per turn and grow the prefix with the
|
|
146
|
+
// conversation. Skip it only when the prefix is known to hold a rendered
|
|
147
|
+
// one: `toolBlockCached` records that a previous turn actually got it into
|
|
148
|
+
// the cache, which a committed message count does not prove. A stale
|
|
149
|
+
// boundary means we are resending the whole conversation anyway, and an
|
|
150
|
+
// evictable block can no longer be assumed present.
|
|
151
|
+
const skipToolBlock = turn.toolBlockCached && !clearStaleCount && !toolBlockEvictable;
|
|
152
|
+
const blockToSend = skipToolBlock ? [] : toolBlock;
|
|
153
|
+
return {
|
|
154
|
+
messages: withToolBlock(transformMessages(messages), blockToSend),
|
|
155
|
+
toolBlockCached: skipToolBlock || rendersToolBlock(messages, blockToSend)
|
|
156
|
+
};
|
|
147
157
|
}
|
|
148
|
-
async function* processModelResponse(model, messagesToSend, tools, generationParams, cacheOptions, dialect) {
|
|
158
|
+
async function* processModelResponse(model, messagesToSend, tools, generationParams, cacheOptions, dialect, onResponse) {
|
|
149
159
|
const runOptions = {
|
|
150
160
|
...(generationParams && { generationParams }),
|
|
151
161
|
...(cacheOptions?.cacheKey !== undefined && {
|
|
@@ -158,6 +168,8 @@ async function* processModelResponse(model, messagesToSend, tools, generationPar
|
|
|
158
168
|
const hasRunOptions = Object.keys(runOptions).length > 0;
|
|
159
169
|
const modelStart = nowMs();
|
|
160
170
|
const response = await runModel(model, messagesToSend, hasRunOptions ? runOptions : undefined);
|
|
171
|
+
// Hand the admitted response back so the caller can cancel just this job.
|
|
172
|
+
onResponse?.(response);
|
|
161
173
|
let accumulatedText = '';
|
|
162
174
|
let producedTokens = false;
|
|
163
175
|
let emittedPieces = 0;
|
|
@@ -185,7 +197,8 @@ async function* processModelResponse(model, messagesToSend, tools, generationPar
|
|
|
185
197
|
...buildStreamResult(modelExecutionMs, stats),
|
|
186
198
|
toolCalls: toolCallsResult,
|
|
187
199
|
responseText: accumulatedText,
|
|
188
|
-
producedTokens
|
|
200
|
+
producedTokens,
|
|
201
|
+
stoppedAtContextBoundary: responseWithStats.stats?.stopReason === 'contextOverflow'
|
|
189
202
|
};
|
|
190
203
|
}
|
|
191
204
|
export async function* completion(params, opts) {
|
|
@@ -194,9 +207,13 @@ export async function* completion(params, opts) {
|
|
|
194
207
|
const requestLogger = opts.logger ?? logger;
|
|
195
208
|
const modelConfig = getModelConfig(modelId);
|
|
196
209
|
const toolsEnabled = modelConfig.tools === true;
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
210
|
+
const toolsActive = !!tools?.length && toolsEnabled;
|
|
211
|
+
// Sliding is opt-in (`n_discarded` defaults to 0). Once on, the addon's
|
|
212
|
+
// discard window opens at the end of the primed prefix — which is where the
|
|
213
|
+
// tool block sits, since the prime is the system prompt alone — and nothing
|
|
214
|
+
// protects it. So while sliding is possible the block cannot be assumed to
|
|
215
|
+
// survive, and it has to travel with every turn.
|
|
216
|
+
const toolBlockEvictable = (modelConfig.n_discarded ?? 0) > 0;
|
|
200
217
|
const dialect = tools && tools.length > 0 ? (params.toolDialect ?? detectToolDialect(modelId)) : undefined;
|
|
201
218
|
// `responseFormat` is forwarded to the addon as a per-request
|
|
202
219
|
// `generationParams.json_schema`, which the addon converts to GBNF and
|
|
@@ -216,56 +233,45 @@ export async function* completion(params, opts) {
|
|
|
216
233
|
}
|
|
217
234
|
}
|
|
218
235
|
const model = getModel(modelId);
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
requestLogger.warn(`[cancel] addon.cancel() rejected during abort for modelId=${modelId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
236
|
-
});
|
|
237
|
-
}
|
|
236
|
+
// Per-request hard cancel: under continuous batching the model runs several
|
|
237
|
+
// jobs at once, so an abort must cancel only THIS request's job, not the
|
|
238
|
+
// whole model — `response.cancel()` routes to the addon's per-job cancel.
|
|
239
|
+
// `.catch(...)` keeps the fire-and-forget cancel from leaking a rejection.
|
|
240
|
+
let activeResponse = null;
|
|
241
|
+
const cancelActive = () => {
|
|
242
|
+
activeResponse?.cancel().catch((err) => {
|
|
243
|
+
requestLogger.warn(`[cancel] response.cancel() rejected during abort for modelId=${modelId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
// Publish each run's response as it's admitted; the `signal.aborted` re-check
|
|
247
|
+
// cancels it if the abort landed while run() was still being admitted.
|
|
248
|
+
const setActiveResponse = (response) => {
|
|
249
|
+
activeResponse = response;
|
|
250
|
+
if (signal.aborted)
|
|
251
|
+
cancelActive();
|
|
238
252
|
};
|
|
253
|
+
const onAbort = () => cancelActive();
|
|
239
254
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
240
|
-
// `
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
// already aborted at `begin(...)`. Without this fall-through, the
|
|
244
|
-
// addon would keep decoding until the post-loop check notices.
|
|
245
|
-
// Re-using `onAbort` here keeps the listener body as the single
|
|
246
|
-
// source of truth for "what cancel does."
|
|
255
|
+
// `{ once: true }` won't fire for an already-aborted signal (e.g. an
|
|
256
|
+
// already-aborted parentSignal at begin), so fire once here; a no-op before
|
|
257
|
+
// any response exists.
|
|
247
258
|
if (signal.aborted)
|
|
248
259
|
onAbort();
|
|
249
|
-
// Detach the abort listener on every exit path (happy, throw, generator
|
|
250
|
-
// `return()` from upstream). `{ once: true }` already removes the
|
|
251
|
-
// listener if the signal fires, so the `removeEventListener` here is
|
|
252
|
-
// the cleanup hook for the signal-never-fired path.
|
|
253
260
|
scope.defer(() => {
|
|
254
261
|
signal.removeEventListener('abort', onAbort);
|
|
262
|
+
// Drop the ref so a late abort can't cancel an already-settled response.
|
|
263
|
+
activeResponse = null;
|
|
255
264
|
});
|
|
256
265
|
if (!kvCache) {
|
|
257
266
|
// KV-cache disabled — straight passthrough, no session involvement.
|
|
258
267
|
let historyWithTools = history;
|
|
259
|
-
if (
|
|
268
|
+
if (toolsActive && tools) {
|
|
260
269
|
historyWithTools = prependToolsToHistory(history, tools);
|
|
261
270
|
}
|
|
262
|
-
else if (dynamicTools && tools) {
|
|
263
|
-
historyWithTools = appendToolsToHistory(history, tools);
|
|
264
|
-
}
|
|
265
271
|
const transformedHistory = transformMessages(historyWithTools);
|
|
266
272
|
logCacheDisabled();
|
|
267
273
|
logMessagesToAddon(transformedHistory, 'NO_CACHE');
|
|
268
|
-
return yield* processModelResponse(model, transformedHistory, tools, mergedGenerationParams, undefined, dialect);
|
|
274
|
+
return yield* processModelResponse(model, transformedHistory, tools, mergedGenerationParams, undefined, dialect, setActiveResponse);
|
|
269
275
|
}
|
|
270
276
|
// ---- KV-cache path. The session owns all three bookkeeping layers
|
|
271
277
|
// (on-disk `.bin`, `initializedCaches`, `cachedMessageCounts`). The
|
|
@@ -275,19 +281,15 @@ export async function* completion(params, opts) {
|
|
|
275
281
|
// unwind through the same `scope.defer` hook. ----
|
|
276
282
|
const session = createKvCacheSession(modelId, { logger: requestLogger });
|
|
277
283
|
const systemPromptFromHistory = extractSystemPrompt(history);
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
const configHash = generateConfigHash(systemPromptFromHistory,
|
|
284
|
+
// The tool block is baked into the cache on the turn that first sends it and
|
|
285
|
+
// never trimmed, so a late or changed tool set has to land on a fresh cache
|
|
286
|
+
// rather than a warm prefix holding the old block.
|
|
287
|
+
const configHash = generateConfigHash(systemPromptFromHistory, toolsActive ? tools : undefined);
|
|
282
288
|
const systemPromptToUse = systemPromptFromHistory ||
|
|
283
289
|
modelConfig.system_prompt ||
|
|
284
290
|
'You are a helpful assistant.';
|
|
285
291
|
const primeIfMissing = async (cachePath) => {
|
|
286
|
-
await initSystemPromptCache(model, cachePath, systemPromptToUse, typeof kvCache === 'string' ? kvCache : 'auto',
|
|
287
|
-
// Static-mode tools are baked into the system-prompt cache so
|
|
288
|
-
// they're shared across the session. Dynamic-mode tools belong
|
|
289
|
-
// to a per-turn anchor and must not enter the system cache.
|
|
290
|
-
staticTools ? tools : undefined);
|
|
292
|
+
await initSystemPromptCache(model, cachePath, systemPromptToUse, typeof kvCache === 'string' ? kvCache : 'auto', setActiveResponse);
|
|
291
293
|
};
|
|
292
294
|
let turn;
|
|
293
295
|
if (typeof kvCache === 'string') {
|
|
@@ -295,7 +297,8 @@ export async function* completion(params, opts) {
|
|
|
295
297
|
kind: 'custom',
|
|
296
298
|
customKey: kvCache,
|
|
297
299
|
configHash,
|
|
298
|
-
primeIfMissing
|
|
300
|
+
primeIfMissing,
|
|
301
|
+
signal
|
|
299
302
|
});
|
|
300
303
|
}
|
|
301
304
|
else {
|
|
@@ -308,7 +311,8 @@ export async function* completion(params, opts) {
|
|
|
308
311
|
kind: 'auto',
|
|
309
312
|
configHash,
|
|
310
313
|
history: cacheMessages,
|
|
311
|
-
primeIfMissing
|
|
314
|
+
primeIfMissing,
|
|
315
|
+
signal
|
|
312
316
|
});
|
|
313
317
|
}
|
|
314
318
|
// Single cleanup hook for every non-success exit path. `commitTurn`
|
|
@@ -320,15 +324,17 @@ export async function* completion(params, opts) {
|
|
|
320
324
|
// `cacheExists` is implied by `beginTurn` — the session either found
|
|
321
325
|
// an existing cache or just primed one. Pass `true` to the message
|
|
322
326
|
// selector so the slicing branches engage.
|
|
323
|
-
const
|
|
324
|
-
/* cacheExists */ true, history,
|
|
327
|
+
const payload = prepareMessagesForCache(session, turn,
|
|
328
|
+
/* cacheExists */ true, history, toolsActive ? tools : undefined, toolBlockEvictable);
|
|
329
|
+
const messagesToSend = payload.messages;
|
|
325
330
|
logMessagesToAddon(messagesToSend, 'PROMPT_SEND');
|
|
326
|
-
const result = yield* processModelResponse(model, messagesToSend, tools, mergedGenerationParams, { cacheKey: turn.cachePath, saveCacheToDisk: true }, dialect);
|
|
331
|
+
const result = yield* processModelResponse(model, messagesToSend, tools, mergedGenerationParams, { cacheKey: turn.cachePath, saveCacheToDisk: true }, dialect, setActiveResponse);
|
|
327
332
|
const shouldCommitTurn = shouldCommitCachedTurn({
|
|
328
333
|
aborted: signal.aborted,
|
|
329
334
|
producedTokens: result.producedTokens,
|
|
330
335
|
generatedTokens: result.stats?.generatedTokens,
|
|
331
|
-
predict: mergedGenerationParams?.predict ?? modelConfig.predict
|
|
336
|
+
predict: mergedGenerationParams?.predict ?? modelConfig.predict,
|
|
337
|
+
stoppedAtContextBoundary: result.stoppedAtContextBoundary
|
|
332
338
|
});
|
|
333
339
|
if (typeof kvCache === 'string') {
|
|
334
340
|
// Custom-key path: the addon wrote the new cache state inline at
|
|
@@ -337,7 +343,8 @@ export async function* completion(params, opts) {
|
|
|
337
343
|
if (shouldCommitTurn) {
|
|
338
344
|
await session.commitTurn(turn, {
|
|
339
345
|
kind: 'static',
|
|
340
|
-
messageCount: history.length + 1
|
|
346
|
+
messageCount: history.length + 1,
|
|
347
|
+
toolBlockCached: payload.toolBlockCached
|
|
341
348
|
});
|
|
342
349
|
}
|
|
343
350
|
return result;
|
|
@@ -368,7 +375,8 @@ export async function* completion(params, opts) {
|
|
|
368
375
|
await session.commitTurn(turn, {
|
|
369
376
|
kind: 'autoRename',
|
|
370
377
|
targetCachePath: postResponseCacheInfo.cachePath,
|
|
371
|
-
messageCount: savedHistory.length
|
|
378
|
+
messageCount: savedHistory.length,
|
|
379
|
+
toolBlockCached: payload.toolBlockCached
|
|
372
380
|
});
|
|
373
381
|
return result;
|
|
374
382
|
}
|
|
@@ -57,15 +57,52 @@ import { getRequestRegistry, withRequestContext } from '../../../../../server/ba
|
|
|
57
57
|
import { generateServerRequestId } from '../../../../../server/bare/runtime/request-id.js';
|
|
58
58
|
import { getServerLogger } from '../../../../../logging/index.js';
|
|
59
59
|
const PAUSE_CHECKPOINT_PREFIX = 'pause_checkpoint_step_';
|
|
60
|
-
|
|
60
|
+
// Ref-count, not a Set: with an exclusive writer a second finetune queues behind
|
|
61
|
+
// the first, and both register before admission. A Set let the first's cleanup
|
|
62
|
+
// clear the key while the second was still training, so getFinetuneState()
|
|
63
|
+
// reported IDLE mid-run. Each call owns one ref and releases exactly one.
|
|
64
|
+
const finetuneRuntimeState = new Map();
|
|
61
65
|
function getRunningFinetuneState(modelId) {
|
|
62
|
-
return finetuneRuntimeState.
|
|
66
|
+
return (finetuneRuntimeState.get(modelId) ?? 0) > 0;
|
|
63
67
|
}
|
|
64
68
|
function registerRunningFinetune(modelId) {
|
|
65
|
-
finetuneRuntimeState.
|
|
69
|
+
finetuneRuntimeState.set(modelId, (finetuneRuntimeState.get(modelId) ?? 0) + 1);
|
|
66
70
|
}
|
|
67
71
|
export function clearFinetuneRuntimeState(modelId) {
|
|
68
|
-
finetuneRuntimeState.
|
|
72
|
+
const count = finetuneRuntimeState.get(modelId);
|
|
73
|
+
if (count === undefined)
|
|
74
|
+
return;
|
|
75
|
+
if (count <= 1)
|
|
76
|
+
finetuneRuntimeState.delete(modelId);
|
|
77
|
+
else
|
|
78
|
+
finetuneRuntimeState.set(modelId, count - 1);
|
|
79
|
+
}
|
|
80
|
+
// modelIds whose single in-flight finetune currently HOLDS the exclusive lane
|
|
81
|
+
// (admitted, past the queued-abort check). One finetune per model (see the
|
|
82
|
+
// reject-second guard in startFinetune), so this is a plain membership set. Only an
|
|
83
|
+
// admitted finetune may be paused through the addon-global model.pause(); a finetune
|
|
84
|
+
// still queued behind a reader is cancelled instead, so pause never touches an
|
|
85
|
+
// unrelated reader.
|
|
86
|
+
const admittedFinetunes = new Set();
|
|
87
|
+
// The single in-flight finetune's requestId per model. A QUEUED finetune is cancelled
|
|
88
|
+
// by this id, not by a broad cancel({ modelId, kind }): the registry's grant→register
|
|
89
|
+
// handoff leaves a brief window where the request is in neither the wait queue nor the
|
|
90
|
+
// active set — a broad cancel misses it, but cancel({ requestId }) covers it via the
|
|
91
|
+
// cancel-before-begin tripwire.
|
|
92
|
+
const finetuneRequestIds = new Map();
|
|
93
|
+
// Models whose admitted finetune is mid-pause: model.pause() has been called but has
|
|
94
|
+
// not settled. A new finetune is rejected during this window even if the runtime ref
|
|
95
|
+
// has already cleared, because handle.await() can settle before model.pause() resolves.
|
|
96
|
+
const pausingModels = new Set();
|
|
97
|
+
// Cancel the model's in-flight finetune by its tracked requestId (covers the
|
|
98
|
+
// registry grant→register handoff window via the cancel-before-begin tripwire),
|
|
99
|
+
// falling back to a broad cancel if the id is unknown.
|
|
100
|
+
function cancelTrackedFinetune(modelId) {
|
|
101
|
+
const requestId = finetuneRequestIds.get(modelId);
|
|
102
|
+
if (requestId !== undefined)
|
|
103
|
+
getRequestRegistry().cancel({ requestId });
|
|
104
|
+
else
|
|
105
|
+
getRequestRegistry().cancel({ modelId, kind: 'finetune' });
|
|
69
106
|
}
|
|
70
107
|
export function getFinetuneStateFromCheckpoints(options) {
|
|
71
108
|
const checkpointDirectory = options.checkpointSaveDir ?? './checkpoints';
|
|
@@ -105,24 +142,35 @@ export async function startFinetune(request, onProgress) {
|
|
|
105
142
|
try {
|
|
106
143
|
const model = getModel(request.modelId);
|
|
107
144
|
validateExplicitFinetuneOperation(request);
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
145
|
+
// One finetune per model: reject a second start/resume while one is pending or
|
|
146
|
+
// running. With no queued finetune peers, pause/cancel never has to disambiguate
|
|
147
|
+
// multiple finetunes, and a finetune that arrives mid-pause is simply rejected
|
|
148
|
+
// because the pausing one still occupies the model until it unwinds.
|
|
149
|
+
if (getRunningFinetuneState(request.modelId) || pausingModels.has(request.modelId)) {
|
|
150
|
+
throw new CompletionFailedError(`Model "${request.modelId}" already has an active finetune; pause or cancel it before starting another`);
|
|
151
|
+
}
|
|
152
|
+
// Stable id, used for registry scoping and for targeted cancellation of this
|
|
153
|
+
// finetune while it is queued (see pauseFinetune / cancelFinetune).
|
|
154
|
+
const requestId = request.requestId ?? generateServerRequestId();
|
|
155
|
+
// Take a ref before the async begin() so an immediate getFinetuneState() poll
|
|
156
|
+
// observes RUNNING, not IDLE, and a concurrent start is rejected above. Released
|
|
157
|
+
// on a failed begin() or on scope unwind below.
|
|
113
158
|
registerRunningFinetune(request.modelId);
|
|
159
|
+
finetuneRequestIds.set(request.modelId, requestId);
|
|
114
160
|
// Scope the run into the registry so cancel({ requestId }) and
|
|
115
161
|
// cancel({ modelId, kind: "finetune" }) reach it; onAbort forwards to
|
|
116
162
|
// model.cancel().
|
|
117
163
|
const ctx = __addDisposableResource(env_1, await getRequestRegistry()
|
|
118
164
|
.begin({
|
|
119
|
-
requestId
|
|
165
|
+
requestId,
|
|
120
166
|
kind: 'finetune',
|
|
121
167
|
modelId: request.modelId
|
|
122
168
|
})
|
|
123
169
|
.catch((err) => {
|
|
124
|
-
|
|
125
|
-
|
|
170
|
+
clearFinetuneRuntimeState(request.modelId);
|
|
171
|
+
if (finetuneRequestIds.get(request.modelId) === requestId) {
|
|
172
|
+
finetuneRequestIds.delete(request.modelId);
|
|
173
|
+
}
|
|
126
174
|
throw err;
|
|
127
175
|
}), true);
|
|
128
176
|
const requestLogger = withRequestContext(getServerLogger(), ctx);
|
|
@@ -130,7 +178,30 @@ export async function startFinetune(request, onProgress) {
|
|
|
130
178
|
// removes the listener first.
|
|
131
179
|
ctx.scope.defer(() => {
|
|
132
180
|
clearFinetuneRuntimeState(request.modelId);
|
|
181
|
+
if (finetuneRequestIds.get(request.modelId) === requestId) {
|
|
182
|
+
finetuneRequestIds.delete(request.modelId);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
// A finetune cancelled while queued resolves aborted without the exclusive
|
|
186
|
+
// slot, so completions may still hold the lane. Stop before the global cancel
|
|
187
|
+
// or any native finetune so neither runs against them.
|
|
188
|
+
if (ctx.signal.aborted) {
|
|
189
|
+
return { type: 'finetune', status: 'CANCELLED' };
|
|
190
|
+
}
|
|
191
|
+
// Past the queued-abort check: this finetune now holds the exclusive lane. Mark
|
|
192
|
+
// it admitted so pauseFinetune pauses THIS finetune via model.pause() rather than
|
|
193
|
+
// global-pausing (and killing a reader) while it is merely queued.
|
|
194
|
+
admittedFinetunes.add(request.modelId);
|
|
195
|
+
ctx.scope.defer(() => {
|
|
196
|
+
admittedFinetunes.delete(request.modelId);
|
|
133
197
|
});
|
|
198
|
+
// The pre-admission validation above ran before this request queued for the
|
|
199
|
+
// exclusive lane, so a peer finetune could have changed the checkpoint state
|
|
200
|
+
// while we waited. Re-validate now that we hold the lane exclusively and the
|
|
201
|
+
// state is stable.
|
|
202
|
+
validateExplicitFinetuneOperation(request);
|
|
203
|
+
// Global cancel is finetune's only stop; it runs exclusively, so nothing else
|
|
204
|
+
// is on the model when this fires.
|
|
134
205
|
const onAbort = () => {
|
|
135
206
|
model.cancel().catch((err) => {
|
|
136
207
|
requestLogger.warn(`[cancel] model.cancel() rejected during abort for modelId=${request.modelId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -167,19 +238,38 @@ export async function startFinetune(request, onProgress) {
|
|
|
167
238
|
}
|
|
168
239
|
}
|
|
169
240
|
export async function pauseFinetune(modelId) {
|
|
241
|
+
// Validate the model is loaded on every path (matches main; getModel throws
|
|
242
|
+
// ModelNotFoundError for an unknown/unloaded model) before reporting any state.
|
|
170
243
|
const model = getModel(modelId);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
244
|
+
if (admittedFinetunes.has(modelId)) {
|
|
245
|
+
// The single finetune holds the model exclusively, so the addon-global pause is
|
|
246
|
+
// safe (nothing else runs on the model). Bar a new finetune until the native
|
|
247
|
+
// pause settles: the runtime ref can clear when handle.await() resolves, which
|
|
248
|
+
// may precede model.pause()'s resolution.
|
|
249
|
+
pausingModels.add(modelId);
|
|
250
|
+
try {
|
|
251
|
+
await model.pause();
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
pausingModels.delete(modelId);
|
|
255
|
+
}
|
|
256
|
+
return { type: 'finetune', status: 'PAUSED' };
|
|
257
|
+
}
|
|
258
|
+
if (getRunningFinetuneState(modelId)) {
|
|
259
|
+
// A finetune is pending but not yet admitted (queued behind a reader). A global
|
|
260
|
+
// pause would cancel that unrelated reader, so cancel the queued finetune by its
|
|
261
|
+
// own requestId. Nothing has trained, so report CANCELLED rather than a false
|
|
262
|
+
// PAUSED.
|
|
263
|
+
cancelTrackedFinetune(modelId);
|
|
264
|
+
return { type: 'finetune', status: 'CANCELLED' };
|
|
265
|
+
}
|
|
266
|
+
// No finetune on this model: a no-op pause, but the model was validated above.
|
|
267
|
+
return { type: 'finetune', status: 'PAUSED' };
|
|
176
268
|
}
|
|
177
269
|
// Routes cancellation through the registry; the model.cancel() forward is
|
|
178
270
|
// installed by startFinetune, so never call model.cancel() here.
|
|
179
271
|
export function cancelFinetune(modelId) {
|
|
180
|
-
|
|
181
|
-
// return shape.
|
|
182
|
-
getRequestRegistry().cancel({ modelId, kind: 'finetune' });
|
|
272
|
+
cancelTrackedFinetune(modelId);
|
|
183
273
|
return Promise.resolve({
|
|
184
274
|
type: 'finetune',
|
|
185
275
|
status: 'CANCELLED'
|