This commit is contained in:
CJACK
2026-03-01 07:15:35 +08:00
parent 321b8a89ee
commit 93e9fb531d
24 changed files with 391 additions and 357 deletions

View File

@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"ds2api/internal/sse"
@@ -67,6 +68,7 @@ func TestGoCompatToolcallFixtures(t *testing.T) {
var fixture struct {
Text string `json:"text"`
ToolNames []string `json:"tool_names"`
Mode string `json:"mode"`
}
mustLoadJSON(t, fixturePath, &fixture)
@@ -75,7 +77,13 @@ func TestGoCompatToolcallFixtures(t *testing.T) {
}
mustLoadJSON(t, expectedPath, &expected)
got := util.ParseToolCalls(fixture.Text, fixture.ToolNames)
var got []util.ParsedToolCall
switch strings.ToLower(strings.TrimSpace(fixture.Mode)) {
case "standalone":
got = util.ParseStandaloneToolCalls(fixture.Text, fixture.ToolNames)
default:
got = util.ParseToolCalls(fixture.Text, fixture.ToolNames)
}
if len(got) == 0 && len(expected.Calls) == 0 {
continue
}

View File

@@ -10,8 +10,10 @@ const {
} = require('./sse_parse');
const {
resolveToolcallPolicy,
formatIncrementalToolCallDeltas,
normalizePreparedToolNames,
boolDefaultTrue,
filterIncrementalToolCallDeltasByAllowed,
} = require('./toolcall_policy');
const {
estimateTokens,
@@ -82,7 +84,9 @@ module.exports.__test = {
shouldSkipPath,
asString,
resolveToolcallPolicy,
formatIncrementalToolCallDeltas,
normalizePreparedToolNames,
boolDefaultTrue,
filterIncrementalToolCallDeltasByAllowed,
estimateTokens,
};

View File

@@ -68,6 +68,47 @@ function formatIncrementalToolCallDeltas(deltas, idStore) {
return out;
}
function filterIncrementalToolCallDeltasByAllowed(deltas, allowedNames, seenNames) {
if (!Array.isArray(deltas) || deltas.length === 0) {
return [];
}
const seen = seenNames instanceof Map ? seenNames : new Map();
const allowed = new Set((allowedNames || []).filter((name) => asString(name) !== ''));
if (allowed.size === 0) {
for (const d of deltas) {
if (d && typeof d === 'object' && asString(d.name)) {
const index = Number.isInteger(d.index) ? d.index : 0;
seen.set(index, '__blocked__');
}
}
return [];
}
const out = [];
for (const d of deltas) {
if (!d || typeof d !== 'object') {
continue;
}
const index = Number.isInteger(d.index) ? d.index : 0;
const name = asString(d.name);
if (name) {
if (!allowed.has(name)) {
seen.set(index, '__blocked__');
continue;
}
seen.set(index, name);
out.push(d);
continue;
}
const existing = asString(seen.get(index));
if (!existing || existing === '__blocked__') {
continue;
}
out.push(d);
}
return out;
}
function ensureStreamToolCallID(idStore, index) {
const key = Number.isInteger(index) ? index : 0;
const existing = idStore.get(key);
@@ -104,4 +145,5 @@ module.exports = {
normalizePreparedToolNames,
boolDefaultTrue,
formatIncrementalToolCallDeltas,
filterIncrementalToolCallDeltasByAllowed,
};

View File

@@ -5,7 +5,7 @@ const {
createToolSieveState,
processToolSieveChunk,
flushToolSieve,
parseToolCalls,
parseStandaloneToolCalls,
formatOpenAIStreamToolCalls,
} = require('../helpers/stream-tool-sieve');
const {
@@ -24,7 +24,6 @@ const {
} = require('./token_usage');
const {
resolveToolcallPolicy,
formatIncrementalToolCallDeltas,
} = require('./toolcall_policy');
const {
createChatCompletionEmitter,
@@ -130,7 +129,6 @@ async function handleVercelStream(req, res, rawBody, payload) {
let thinkingText = '';
let outputText = '';
const toolSieveEnabled = toolPolicy.toolSieveEnabled;
const emitEarlyToolDeltas = toolPolicy.emitEarlyToolDeltas;
const toolSieveState = createToolSieveState();
let toolCallsEmitted = false;
const streamToolCallIDs = new Map();
@@ -155,13 +153,18 @@ async function handleVercelStream(req, res, rawBody, payload) {
await releaseLease();
return;
}
const detected = parseToolCalls(outputText, toolNames);
const detected = parseStandaloneToolCalls(outputText, toolNames);
if (detected.length > 0 && !toolCallsEmitted) {
toolCallsEmitted = true;
sendDeltaFrame({ tool_calls: formatOpenAIStreamToolCalls(detected) });
sendDeltaFrame({ tool_calls: formatOpenAIStreamToolCalls(detected, streamToolCallIDs) });
} else if (toolSieveEnabled) {
const tailEvents = flushToolSieve(toolSieveState, toolNames);
for (const evt of tailEvents) {
if (evt.type === 'tool_calls' && Array.isArray(evt.calls) && evt.calls.length > 0) {
toolCallsEmitted = true;
sendDeltaFrame({ tool_calls: formatOpenAIStreamToolCalls(evt.calls, streamToolCallIDs) });
continue;
}
if (evt.text) {
sendDeltaFrame({ content: evt.text });
}
@@ -252,17 +255,9 @@ async function handleVercelStream(req, res, rawBody, payload) {
}
const events = processToolSieveChunk(toolSieveState, p.text, toolNames);
for (const evt of events) {
if (evt.type === 'tool_call_deltas' && Array.isArray(evt.deltas) && evt.deltas.length > 0) {
if (!emitEarlyToolDeltas) {
continue;
}
toolCallsEmitted = true;
sendDeltaFrame({ tool_calls: formatIncrementalToolCallDeltas(evt.deltas, streamToolCallIDs) });
continue;
}
if (evt.type === 'tool_calls') {
toolCallsEmitted = true;
sendDeltaFrame({ tool_calls: formatOpenAIStreamToolCalls(evt.calls) });
sendDeltaFrame({ tool_calls: formatOpenAIStreamToolCalls(evt.calls, streamToolCallIDs) });
continue;
}
if (evt.text) {

View File

@@ -2,13 +2,13 @@
const crypto = require('crypto');
function formatOpenAIStreamToolCalls(calls) {
function formatOpenAIStreamToolCalls(calls, idStore) {
if (!Array.isArray(calls) || calls.length === 0) {
return [];
}
return calls.map((c, idx) => ({
index: idx,
id: `call_${newCallID()}`,
id: ensureStreamToolCallID(idStore, idx),
type: 'function',
function: {
name: c.name,
@@ -17,6 +17,20 @@ function formatOpenAIStreamToolCalls(calls) {
}));
}
function ensureStreamToolCallID(idStore, index) {
if (!(idStore instanceof Map)) {
return `call_${newCallID()}`;
}
const key = Number.isInteger(index) ? index : 0;
const existing = idStore.get(key);
if (existing) {
return existing;
}
const next = `call_${newCallID()}`;
idStore.set(key, next);
return next;
}
function newCallID() {
if (typeof crypto.randomUUID === 'function') {
return crypto.randomUUID().replace(/-/g, '');

View File

@@ -1,226 +0,0 @@
'use strict';
const {
looksLikeToolExampleContext,
insideCodeFence,
} = require('./state');
const {
findObjectFieldValueStart,
parseJSONStringLiteral,
skipSpaces,
} = require('./jsonscan');
function buildIncrementalToolDeltas(state) {
const captured = state.capture || '';
if (!captured) {
return [];
}
if (looksLikeToolExampleContext(state.recentTextTail)) {
return [];
}
const lower = captured.toLowerCase();
const keyIdx = lower.indexOf('tool_calls');
if (keyIdx < 0) {
return [];
}
const start = captured.slice(0, keyIdx).lastIndexOf('{');
if (start < 0) {
return [];
}
if (insideCodeFence((state.recentTextTail || '') + captured.slice(0, start))) {
return [];
}
const callStart = findFirstToolCallObjectStart(captured, keyIdx);
if (callStart < 0) {
return [];
}
const deltas = [];
if (!state.toolName) {
const name = extractToolCallName(captured, callStart);
if (!name) {
return [];
}
state.toolName = name;
}
if (state.toolArgsStart < 0) {
const args = findToolCallArgsStart(captured, callStart);
if (args) {
state.toolArgsString = Boolean(args.stringMode);
state.toolArgsStart = state.toolArgsString ? args.start + 1 : args.start;
state.toolArgsSent = state.toolArgsStart;
}
}
if (!state.toolNameSent) {
if (state.toolArgsStart < 0) {
return [];
}
state.toolNameSent = true;
deltas.push({ index: 0, name: state.toolName });
}
if (state.toolArgsStart < 0 || state.toolArgsDone) {
return deltas;
}
const progress = scanToolCallArgsProgress(captured, state.toolArgsStart, state.toolArgsString);
if (!progress) {
return deltas;
}
if (progress.end > state.toolArgsSent) {
deltas.push({
index: 0,
arguments: captured.slice(state.toolArgsSent, progress.end),
});
state.toolArgsSent = progress.end;
}
if (progress.complete) {
state.toolArgsDone = true;
}
return deltas;
}
function findFirstToolCallObjectStart(text, keyIdx) {
const arrStart = findToolCallsArrayStart(text, keyIdx);
if (arrStart < 0) {
return -1;
}
const i = skipSpaces(text, arrStart + 1);
if (i >= text.length || text[i] !== '{') {
return -1;
}
return i;
}
function findToolCallsArrayStart(text, keyIdx) {
let i = keyIdx + 'tool_calls'.length;
while (i < text.length && text[i] !== ':') {
i += 1;
}
if (i >= text.length) {
return -1;
}
i = skipSpaces(text, i + 1);
if (i >= text.length || text[i] !== '[') {
return -1;
}
return i;
}
function extractToolCallName(text, callStart) {
let valueStart = findObjectFieldValueStart(text, callStart, ['name']);
if (valueStart < 0 || text[valueStart] !== '"') {
const fnStart = findFunctionObjectStart(text, callStart);
if (fnStart < 0) {
return '';
}
valueStart = findObjectFieldValueStart(text, fnStart, ['name']);
if (valueStart < 0 || text[valueStart] !== '"') {
return '';
}
}
const parsed = parseJSONStringLiteral(text, valueStart);
if (!parsed) {
return '';
}
return parsed.value;
}
function findToolCallArgsStart(text, callStart) {
const keys = ['input', 'arguments', 'args', 'parameters', 'params'];
let valueStart = findObjectFieldValueStart(text, callStart, keys);
if (valueStart < 0) {
const fnStart = findFunctionObjectStart(text, callStart);
if (fnStart < 0) {
return null;
}
valueStart = findObjectFieldValueStart(text, fnStart, keys);
if (valueStart < 0) {
return null;
}
}
if (valueStart >= text.length) {
return null;
}
const ch = text[valueStart];
if (ch === '{' || ch === '[') {
return { start: valueStart, stringMode: false };
}
if (ch === '"') {
return { start: valueStart, stringMode: true };
}
return null;
}
function scanToolCallArgsProgress(text, start, stringMode) {
if (start < 0 || start > text.length) {
return null;
}
if (stringMode) {
let escaped = false;
for (let i = start; i < text.length; i += 1) {
const ch = text[i];
if (escaped) {
escaped = false;
continue;
}
if (ch === '\\') {
escaped = true;
continue;
}
if (ch === '"') {
return { end: i, complete: true };
}
}
return { end: text.length, complete: false };
}
if (start >= text.length || (text[start] !== '{' && text[start] !== '[')) {
return null;
}
let depth = 0;
let quote = '';
let escaped = false;
for (let i = start; i < text.length; i += 1) {
const ch = text[i];
if (quote) {
if (escaped) {
escaped = false;
continue;
}
if (ch === '\\') {
escaped = true;
continue;
}
if (ch === quote) {
quote = '';
}
continue;
}
if (ch === '"' || ch === "'") {
quote = ch;
continue;
}
if (ch === '{' || ch === '[') {
depth += 1;
continue;
}
if (ch === '}' || ch === ']') {
depth -= 1;
if (depth === 0) {
return { end: i + 1, complete: true };
}
}
}
return { end: text.length, complete: false };
}
function findFunctionObjectStart(text, callStart) {
const valueStart = findObjectFieldValueStart(text, callStart, ['function']);
if (valueStart < 0 || valueStart >= text.length || text[valueStart] !== '{') {
return -1;
}
return valueStart;
}
module.exports = {
buildIncrementalToolDeltas,
};

View File

@@ -10,7 +10,9 @@ const {
const {
extractToolNames,
parseToolCalls,
parseToolCallsDetailed,
parseStandaloneToolCalls,
parseStandaloneToolCallsDetailed,
} = require('./parse');
const {
formatOpenAIStreamToolCalls,
@@ -22,6 +24,8 @@ module.exports = {
processToolSieveChunk,
flushToolSieve,
parseToolCalls,
parseToolCallsDetailed,
parseStandaloneToolCalls,
parseStandaloneToolCallsDetailed,
formatOpenAIStreamToolCalls,
};

View File

@@ -29,25 +29,38 @@ function extractToolNames(tools) {
}
function parseToolCalls(text, toolNames) {
return parseToolCallsDetailed(text, toolNames).calls;
}
function parseToolCallsDetailed(text, toolNames) {
const result = emptyParseResult();
if (!toStringSafe(text)) {
return [];
return result;
}
const sanitized = stripFencedCodeBlocks(text);
if (!toStringSafe(sanitized)) {
return [];
return result;
}
result.sawToolCallSyntax = sanitized.toLowerCase().includes('tool_calls');
const candidates = buildToolCallCandidates(sanitized);
let parsed = [];
for (const c of candidates) {
parsed = parseToolCallsPayload(c);
if (parsed.length > 0) {
result.sawToolCallSyntax = true;
break;
}
}
if (parsed.length === 0) {
return [];
return result;
}
return filterToolCalls(parsed, toolNames);
const filtered = filterToolCallsDetailed(parsed, toolNames);
result.calls = filtered.calls;
result.rejectedToolNames = filtered.rejectedToolNames;
result.rejectedByPolicy = filtered.rejectedToolNames.length > 0 && filtered.calls.length === 0;
return result;
}
function stripFencedCodeBlocks(text) {
@@ -59,37 +72,43 @@ function stripFencedCodeBlocks(text) {
}
function parseStandaloneToolCalls(text, toolNames) {
return parseStandaloneToolCallsDetailed(text, toolNames).calls;
}
function parseStandaloneToolCallsDetailed(text, toolNames) {
const result = emptyParseResult();
const trimmed = toStringSafe(text);
if (!trimmed) {
return [];
}
if ((trimmed.startsWith('```') && trimmed.endsWith('```')) || trimmed.includes('```')) {
return [];
return result;
}
if (looksLikeToolExampleContext(trimmed)) {
return [];
return result;
}
const candidates = [trimmed];
if (trimmed.startsWith('```') && trimmed.endsWith('```')) {
const m = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
if (m && m[1]) {
candidates.push(toStringSafe(m[1]));
}
result.sawToolCallSyntax = trimmed.toLowerCase().includes('tool_calls');
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
return result;
}
for (const candidate of candidates) {
const c = toStringSafe(candidate);
if (!c) {
continue;
}
if (!c.startsWith('{') && !c.startsWith('[')) {
continue;
}
const parsed = parseToolCallsPayload(c);
if (parsed.length > 0) {
return filterToolCalls(parsed, toolNames);
}
const parsed = parseToolCallsPayload(trimmed);
if (parsed.length === 0) {
return result;
}
return [];
result.sawToolCallSyntax = true;
const filtered = filterToolCallsDetailed(parsed, toolNames);
result.calls = filtered.calls;
result.rejectedToolNames = filtered.rejectedToolNames;
result.rejectedByPolicy = filtered.rejectedToolNames.length > 0 && filtered.calls.length === 0;
return result;
}
function emptyParseResult() {
return {
calls: [],
sawToolCallSyntax: false,
rejectedByPolicy: false,
rejectedToolNames: [],
};
}
function buildToolCallCandidates(text) {
@@ -251,23 +270,69 @@ function parseToolCallInput(v) {
return {};
}
function filterToolCalls(parsed, toolNames) {
const allowed = new Set((toolNames || []).filter(Boolean));
const out = [];
function filterToolCallsDetailed(parsed, toolNames) {
const allowed = new Set();
const allowedCanonical = new Map();
for (const item of toolNames || []) {
const name = toStringSafe(item);
if (!name) {
continue;
}
allowed.add(name);
const lower = name.toLowerCase();
if (!allowedCanonical.has(lower)) {
allowedCanonical.set(lower, name);
}
}
if (allowed.size === 0) {
const rejected = [];
const seen = new Set();
for (const tc of parsed) {
if (!tc || !tc.name) {
continue;
}
if (seen.has(tc.name)) {
continue;
}
seen.add(tc.name);
rejected.push(tc.name);
}
return { calls: [], rejectedToolNames: rejected };
}
const calls = [];
const rejected = [];
const seenRejected = new Set();
for (const tc of parsed) {
if (!tc || !tc.name) {
continue;
}
if (allowed.size > 0 && !allowed.has(tc.name)) {
let matchedName = '';
if (allowed.has(tc.name)) {
matchedName = tc.name;
} else {
matchedName = allowedCanonical.get(tc.name.toLowerCase()) || '';
}
if (!matchedName) {
if (!seenRejected.has(tc.name)) {
seenRejected.add(tc.name);
rejected.push(tc.name);
}
continue;
}
out.push({ name: tc.name, input: tc.input || {} });
calls.push({
name: matchedName,
input: tc.input && typeof tc.input === 'object' && !Array.isArray(tc.input) ? tc.input : {},
});
}
return out;
return { calls, rejectedToolNames: rejected };
}
module.exports = {
extractToolNames,
parseToolCalls,
parseToolCallsDetailed,
parseStandaloneToolCalls,
parseStandaloneToolCallsDetailed,
};

View File

@@ -6,10 +6,7 @@ const {
insideCodeFence,
} = require('./state');
const {
buildIncrementalToolDeltas,
} = require('./incremental');
const {
parseStandaloneToolCalls,
parseStandaloneToolCallsDetailed,
} = require('./parse');
const {
extractJSONObjectFrom,
@@ -23,6 +20,21 @@ function processToolSieveChunk(state, chunk, toolNames) {
state.pending += chunk;
}
const events = [];
if (Array.isArray(state.pendingToolCalls) && state.pendingToolCalls.length > 0) {
const pending = state.pending || '';
if (pending.trim() !== '') {
const content = (state.pendingToolRaw || '') + pending;
state.pending = '';
state.pendingToolRaw = '';
state.pendingToolCalls = [];
noteText(state, content);
events.push({ type: 'text', text: content });
} else {
return events;
}
}
// eslint-disable-next-line no-constant-condition
while (true) {
if (state.capturing) {
@@ -30,49 +42,50 @@ function processToolSieveChunk(state, chunk, toolNames) {
state.capture += state.pending;
state.pending = '';
}
const deltas = buildIncrementalToolDeltas(state);
if (deltas.length > 0) {
events.push({ type: 'tool_call_deltas', deltas });
}
const consumed = consumeToolCapture(state, toolNames);
if (!consumed.ready) {
break;
}
const captured = state.capture;
state.capture = '';
state.capturing = false;
resetIncrementalToolState(state);
if (Array.isArray(consumed.calls) && consumed.calls.length > 0) {
state.pendingToolRaw = captured;
state.pendingToolCalls = consumed.calls;
continue;
}
if (consumed.prefix) {
noteText(state, consumed.prefix);
events.push({ type: 'text', text: consumed.prefix });
}
if (Array.isArray(consumed.calls) && consumed.calls.length > 0) {
events.push({ type: 'tool_calls', calls: consumed.calls });
}
if (consumed.suffix) {
state.pending += consumed.suffix;
}
continue;
}
if (!state.pending) {
const pending = state.pending || '';
if (!pending) {
break;
}
const start = findToolSegmentStart(state.pending);
const start = findToolSegmentStart(pending);
if (start >= 0) {
const prefix = state.pending.slice(0, start);
const prefix = pending.slice(0, start);
if (prefix) {
noteText(state, prefix);
events.push({ type: 'text', text: prefix });
}
state.capture = state.pending.slice(start);
state.pending = '';
state.capture += pending.slice(start);
state.capturing = true;
resetIncrementalToolState(state);
continue;
}
const [safe, hold] = splitSafeContentForToolDetection(state.pending);
const [safe, hold] = splitSafeContentForToolDetection(pending);
if (!safe) {
break;
}
@@ -88,6 +101,13 @@ function flushToolSieve(state, toolNames) {
return [];
}
const events = processToolSieveChunk(state, '', toolNames);
if (Array.isArray(state.pendingToolCalls) && state.pendingToolCalls.length > 0) {
events.push({ type: 'tool_calls', calls: state.pendingToolCalls });
state.pendingToolRaw = '';
state.pendingToolCalls = [];
}
if (state.capturing) {
const consumed = consumeToolCapture(state, toolNames);
if (consumed.ready) {
@@ -110,11 +130,13 @@ function flushToolSieve(state, toolNames) {
state.capturing = false;
resetIncrementalToolState(state);
}
if (state.pending) {
noteText(state, state.pending);
events.push({ type: 'text', text: state.pending });
state.pending = '';
}
return events;
}
@@ -154,11 +176,10 @@ function findToolSegmentStart(s) {
let offset = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
const keyRel = lower.indexOf('tool_calls', offset);
if (keyRel < 0) {
const keyIdx = lower.indexOf('tool_calls', offset);
if (keyIdx < 0) {
return -1;
}
const keyIdx = keyRel;
const start = s.slice(0, keyIdx).lastIndexOf('{');
const candidateStart = start >= 0 ? start : keyIdx;
if (!insideCodeFence(s.slice(0, candidateStart))) {
@@ -169,7 +190,7 @@ function findToolSegmentStart(s) {
}
function consumeToolCapture(state, toolNames) {
const captured = state.capture;
const captured = state.capture || '';
if (!captured) {
return { ready: false, prefix: '', calls: [], suffix: '' };
}
@@ -186,8 +207,10 @@ function consumeToolCapture(state, toolNames) {
if (!obj.ok) {
return { ready: false, prefix: '', calls: [], suffix: '' };
}
const prefixPart = captured.slice(0, start);
const suffixPart = captured.slice(obj.end);
if (insideCodeFence((state.recentTextTail || '') + prefixPart)) {
return {
ready: true,
@@ -196,18 +219,19 @@ function consumeToolCapture(state, toolNames) {
suffix: '',
};
}
const rawParsed = parseStandaloneToolCalls(captured.slice(start, obj.end), []);
const parsed = parseStandaloneToolCalls(captured.slice(start, obj.end), toolNames);
if (parsed.length === 0) {
if (rawParsed.length > 0 && Array.isArray(toolNames) && toolNames.length > 0) {
return {
ready: true,
prefix: prefixPart,
calls: [],
suffix: suffixPart,
};
}
if (state.toolNameSent) {
if ((state.recentTextTail || '').trim() !== '' || prefixPart.trim() !== '' || suffixPart.trim() !== '') {
return {
ready: true,
prefix: captured,
calls: [],
suffix: '',
};
}
const parsed = parseStandaloneToolCallsDetailed(captured.slice(start, obj.end), toolNames);
if (!Array.isArray(parsed.calls) || parsed.calls.length === 0) {
if (parsed.sawToolCallSyntax && parsed.rejectedByPolicy) {
return {
ready: true,
prefix: prefixPart,
@@ -222,26 +246,11 @@ function consumeToolCapture(state, toolNames) {
suffix: '',
};
}
if (state.toolNameSent) {
if (parsed.length > 1) {
return {
ready: true,
prefix: prefixPart,
calls: parsed.slice(1),
suffix: suffixPart,
};
}
return {
ready: true,
prefix: prefixPart,
calls: [],
suffix: suffixPart,
};
}
return {
ready: true,
prefix: prefixPart,
calls: parsed,
calls: parsed.calls,
suffix: suffixPart,
};
}

View File

@@ -8,6 +8,9 @@ function createToolSieveState() {
capture: '',
capturing: false,
recentTextTail: '',
pendingToolRaw: '',
pendingToolCalls: [],
disableDeltas: false,
toolNameSent: false,
toolName: '',
toolArgsStart: -1,
@@ -18,6 +21,7 @@ function createToolSieveState() {
}
function resetIncrementalToolState(state) {
state.disableDeltas = false;
state.toolNameSent = false;
state.toolName = '';
state.toolArgsStart = -1;