@jsonstudio/llms 0.6.1354 → 0.6.1397
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/dist/conversion/compat/profiles/chat-gemini.json +5 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +310 -87
- package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +8 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +6 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_thought_signature_inject/index.d.ts +10 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_thought_signature_inject/index.js +172 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_thought_signature_capture/index.d.ts +10 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_thought_signature_capture/index.js +71 -0
- package/dist/conversion/hub/pipeline/thought-signature/thought-signature-center.d.ts +14 -0
- package/dist/conversion/hub/pipeline/thought-signature/thought-signature-center.js +289 -0
- package/dist/conversion/hub/response/provider-response.js +6 -0
- package/dist/router/virtual-router/bootstrap.js +6 -0
- package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +15 -0
- package/dist/router/virtual-router/engine-selection/alias-selection.js +85 -4
- package/dist/router/virtual-router/engine-selection/tier-selection-select.js +40 -17
- package/dist/router/virtual-router/engine-selection/tier-selection.js +5 -2
- package/dist/router/virtual-router/engine.js +9 -1
- package/dist/router/virtual-router/types.d.ts +14 -1
- package/package.json +1 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { recordStage } from '../../../stages/utils.js';
|
|
2
|
+
import { SKIP_THOUGHT_SIGNATURE, buildThoughtSignatureSessionKey, getCachedThoughtSignature, getLastThoughtSignature, shouldHandleThoughtSignature } from '../../../thought-signature/thought-signature-center.js';
|
|
3
|
+
import { filterInvalidThinkingBlocks, removeTrailingUnsignedThinkingBlocks } from '../../../../../shared/thought-signature-validator.js';
|
|
4
|
+
function isThinkingPart(part) {
|
|
5
|
+
if (part.thought === true)
|
|
6
|
+
return true;
|
|
7
|
+
const type = typeof part.type === 'string' ? String(part.type).toLowerCase() : '';
|
|
8
|
+
return type === 'thinking' || type === 'reasoning' || type === 'redacted_thinking';
|
|
9
|
+
}
|
|
10
|
+
function readThinkingText(part) {
|
|
11
|
+
const text = part.text ?? part.thinking;
|
|
12
|
+
if (typeof text === 'string' && text.trim())
|
|
13
|
+
return text.trim();
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
function hasSignedThinking(parts) {
|
|
17
|
+
return parts.some((part) => {
|
|
18
|
+
if (!isThinkingPart(part))
|
|
19
|
+
return false;
|
|
20
|
+
const sig = part.thoughtSignature ?? part.signature;
|
|
21
|
+
return typeof sig === 'string' && sig.trim().length >= 10;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function hasToolUse(parts) {
|
|
25
|
+
return parts.some((part) => {
|
|
26
|
+
if (part.functionCall || part.function_call)
|
|
27
|
+
return true;
|
|
28
|
+
if (part.type === 'tool_use' || part.type === 'tool_result')
|
|
29
|
+
return true;
|
|
30
|
+
return false;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function injectGeminiPayload(payload, sessionKey) {
|
|
34
|
+
const contents = Array.isArray(payload.contents) ? payload.contents : [];
|
|
35
|
+
for (const content of contents) {
|
|
36
|
+
const parts = Array.isArray(content.parts) ? content.parts : [];
|
|
37
|
+
if (!parts.length)
|
|
38
|
+
continue;
|
|
39
|
+
// Deep-filter invalid thinking parts before injection.
|
|
40
|
+
const cleanedParts = [];
|
|
41
|
+
for (const part of parts) {
|
|
42
|
+
if (!part || typeof part !== 'object') {
|
|
43
|
+
cleanedParts.push(part);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!isThinkingPart(part)) {
|
|
47
|
+
cleanedParts.push(part);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const text = readThinkingText(part);
|
|
51
|
+
const sig = part.thoughtSignature;
|
|
52
|
+
const sigValid = typeof sig === 'string' && sig.trim().length >= 10
|
|
53
|
+
? true
|
|
54
|
+
: Boolean(sig && (!text || !String(text).trim().length));
|
|
55
|
+
if (sigValid) {
|
|
56
|
+
cleanedParts.push(part);
|
|
57
|
+
}
|
|
58
|
+
else if (text && text.trim()) {
|
|
59
|
+
cleanedParts.push({ text });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
content.parts = cleanedParts;
|
|
63
|
+
const effectiveParts = cleanedParts;
|
|
64
|
+
const toolUsePresent = hasToolUse(effectiveParts);
|
|
65
|
+
let signedThinking = hasSignedThinking(effectiveParts);
|
|
66
|
+
for (const part of effectiveParts) {
|
|
67
|
+
if (!isThinkingPart(part))
|
|
68
|
+
continue;
|
|
69
|
+
const text = readThinkingText(part);
|
|
70
|
+
const sig = part.thoughtSignature;
|
|
71
|
+
if (typeof sig === 'string' && sig.trim()) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const cached = text ? getCachedThoughtSignature(sessionKey, text) : undefined;
|
|
75
|
+
if (cached) {
|
|
76
|
+
part.thoughtSignature = cached;
|
|
77
|
+
signedThinking = true;
|
|
78
|
+
}
|
|
79
|
+
else if (text) {
|
|
80
|
+
part.thoughtSignature = SKIP_THOUGHT_SIGNATURE;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (toolUsePresent && !signedThinking) {
|
|
84
|
+
const last = getLastThoughtSignature(sessionKey);
|
|
85
|
+
if (last) {
|
|
86
|
+
effectiveParts.unshift({ thought: true, text: last.text, thoughtSignature: last.signature });
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
effectiveParts.unshift({ thought: true, text: '', thoughtSignature: SKIP_THOUGHT_SIGNATURE });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function injectAnthropicPayload(payload, sessionKey) {
|
|
95
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
|
96
|
+
for (const message of messages) {
|
|
97
|
+
if (message.role !== 'assistant')
|
|
98
|
+
continue;
|
|
99
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
100
|
+
if (!content.length)
|
|
101
|
+
continue;
|
|
102
|
+
// Deep-filter invalid thinking blocks before injection.
|
|
103
|
+
const wrapper = { role: 'assistant', content: [...content] };
|
|
104
|
+
filterInvalidThinkingBlocks([wrapper]);
|
|
105
|
+
if (Array.isArray(wrapper.content)) {
|
|
106
|
+
removeTrailingUnsignedThinkingBlocks(wrapper.content);
|
|
107
|
+
}
|
|
108
|
+
message.content = wrapper.content;
|
|
109
|
+
const effectiveContent = Array.isArray(wrapper.content) ? wrapper.content : content;
|
|
110
|
+
const toolUsePresent = effectiveContent.some((block) => block && typeof block === 'object' && (block.type === 'tool_use' || block.type === 'tool_result'));
|
|
111
|
+
let signedThinking = effectiveContent.some((block) => {
|
|
112
|
+
if (!block || typeof block !== 'object')
|
|
113
|
+
return false;
|
|
114
|
+
const type = typeof block.type === 'string' ? String(block.type).toLowerCase() : '';
|
|
115
|
+
if (type !== 'thinking' && type !== 'redacted_thinking')
|
|
116
|
+
return false;
|
|
117
|
+
const sig = block.signature;
|
|
118
|
+
return typeof sig === 'string' && sig.trim().length >= 10;
|
|
119
|
+
});
|
|
120
|
+
for (const block of effectiveContent) {
|
|
121
|
+
if (!block || typeof block !== 'object')
|
|
122
|
+
continue;
|
|
123
|
+
const type = typeof block.type === 'string' ? String(block.type).toLowerCase() : '';
|
|
124
|
+
if (type !== 'thinking' && type !== 'redacted_thinking')
|
|
125
|
+
continue;
|
|
126
|
+
const text = typeof block.thinking === 'string'
|
|
127
|
+
? String(block.thinking).trim()
|
|
128
|
+
: typeof block.text === 'string'
|
|
129
|
+
? String(block.text).trim()
|
|
130
|
+
: '';
|
|
131
|
+
const sig = block.signature;
|
|
132
|
+
if (typeof sig === 'string' && sig.trim())
|
|
133
|
+
continue;
|
|
134
|
+
const cached = text ? getCachedThoughtSignature(sessionKey, text) : undefined;
|
|
135
|
+
if (cached) {
|
|
136
|
+
block.signature = cached;
|
|
137
|
+
signedThinking = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (toolUsePresent && !signedThinking) {
|
|
141
|
+
const last = getLastThoughtSignature(sessionKey);
|
|
142
|
+
if (last) {
|
|
143
|
+
effectiveContent.unshift({ type: 'thinking', thinking: last.text, signature: last.signature });
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
effectiveContent.unshift({ type: 'thinking', thinking: '', signature: SKIP_THOUGHT_SIGNATURE });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export async function runReqOutboundStage2ThoughtSignatureInject(options) {
|
|
152
|
+
const { payload, adapterContext } = options;
|
|
153
|
+
if (!shouldHandleThoughtSignature(adapterContext)) {
|
|
154
|
+
return payload;
|
|
155
|
+
}
|
|
156
|
+
const sessionKey = buildThoughtSignatureSessionKey(adapterContext, payload);
|
|
157
|
+
if (!sessionKey) {
|
|
158
|
+
return payload;
|
|
159
|
+
}
|
|
160
|
+
const protocol = String(adapterContext.providerProtocol || '').toLowerCase();
|
|
161
|
+
if (protocol === 'gemini-chat') {
|
|
162
|
+
injectGeminiPayload(payload, sessionKey);
|
|
163
|
+
}
|
|
164
|
+
else if (protocol === 'anthropic-messages') {
|
|
165
|
+
injectAnthropicPayload(payload, sessionKey);
|
|
166
|
+
}
|
|
167
|
+
recordStage(options.stageRecorder, 'chat_process.req.stage7.thought_signature_inject', {
|
|
168
|
+
applied: true,
|
|
169
|
+
sessionKey
|
|
170
|
+
});
|
|
171
|
+
return payload;
|
|
172
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AdapterContext } from '../../../../types/chat-envelope.js';
|
|
2
|
+
import type { JsonObject } from '../../../../types/json.js';
|
|
3
|
+
import type { StageRecorder } from '../../../../format-adapters/index.js';
|
|
4
|
+
type ProviderPayload = JsonObject;
|
|
5
|
+
export declare function runRespInboundStage3ThoughtSignatureCapture(options: {
|
|
6
|
+
payload: ProviderPayload;
|
|
7
|
+
adapterContext: AdapterContext;
|
|
8
|
+
stageRecorder?: StageRecorder;
|
|
9
|
+
}): Promise<ProviderPayload>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { recordStage } from '../../../stages/utils.js';
|
|
2
|
+
import { buildThoughtSignatureSessionKey, cacheThoughtSignature, shouldHandleThoughtSignature } from '../../../thought-signature/thought-signature-center.js';
|
|
3
|
+
function captureGeminiPayload(payload, sessionKey) {
|
|
4
|
+
const candidates = Array.isArray(payload.candidates) ? payload.candidates : [];
|
|
5
|
+
for (const candidate of candidates) {
|
|
6
|
+
const content = candidate?.content;
|
|
7
|
+
if (!content || typeof content !== 'object')
|
|
8
|
+
continue;
|
|
9
|
+
const parts = Array.isArray(content.parts) ? content.parts : [];
|
|
10
|
+
if (!parts.length)
|
|
11
|
+
continue;
|
|
12
|
+
let buffer = '';
|
|
13
|
+
for (const part of parts) {
|
|
14
|
+
if (!part || typeof part !== 'object')
|
|
15
|
+
continue;
|
|
16
|
+
if (part.thought === true || String(part.type || '').toLowerCase() === 'thinking') {
|
|
17
|
+
const text = part.text ?? part.thinking;
|
|
18
|
+
if (typeof text === 'string' && text.trim()) {
|
|
19
|
+
buffer += text;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const sig = part.thoughtSignature;
|
|
23
|
+
if (typeof sig === 'string' && sig.trim() && buffer.trim()) {
|
|
24
|
+
cacheThoughtSignature(sessionKey, buffer, sig);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function captureAnthropicPayload(payload, sessionKey) {
|
|
30
|
+
const content = Array.isArray(payload.content) ? payload.content : [];
|
|
31
|
+
if (!content.length)
|
|
32
|
+
return;
|
|
33
|
+
let buffer = '';
|
|
34
|
+
for (const block of content) {
|
|
35
|
+
if (!block || typeof block !== 'object')
|
|
36
|
+
continue;
|
|
37
|
+
const type = typeof block.type === 'string' ? String(block.type).toLowerCase() : '';
|
|
38
|
+
if (type === 'thinking' || type === 'redacted_thinking') {
|
|
39
|
+
const text = block.thinking ?? block.text;
|
|
40
|
+
if (typeof text === 'string' && text.trim()) {
|
|
41
|
+
buffer += text;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const sig = block.signature;
|
|
45
|
+
if (typeof sig === 'string' && sig.trim() && buffer.trim()) {
|
|
46
|
+
cacheThoughtSignature(sessionKey, buffer, sig);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function runRespInboundStage3ThoughtSignatureCapture(options) {
|
|
51
|
+
const { payload, adapterContext } = options;
|
|
52
|
+
if (!shouldHandleThoughtSignature(adapterContext)) {
|
|
53
|
+
return payload;
|
|
54
|
+
}
|
|
55
|
+
const sessionKey = buildThoughtSignatureSessionKey(adapterContext, payload);
|
|
56
|
+
if (!sessionKey) {
|
|
57
|
+
return payload;
|
|
58
|
+
}
|
|
59
|
+
const protocol = String(adapterContext.providerProtocol || '').toLowerCase();
|
|
60
|
+
if (protocol === 'gemini-chat') {
|
|
61
|
+
captureGeminiPayload(payload, sessionKey);
|
|
62
|
+
}
|
|
63
|
+
else if (protocol === 'anthropic-messages') {
|
|
64
|
+
captureAnthropicPayload(payload, sessionKey);
|
|
65
|
+
}
|
|
66
|
+
recordStage(options.stageRecorder, 'chat_process.resp.stage3.thought_signature_capture', {
|
|
67
|
+
applied: true,
|
|
68
|
+
sessionKey
|
|
69
|
+
});
|
|
70
|
+
return payload;
|
|
71
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AdapterContext } from '../../types/chat-envelope.js';
|
|
2
|
+
import type { JsonObject } from '../../types/json.js';
|
|
3
|
+
export declare const SKIP_THOUGHT_SIGNATURE = "skip_thought_signature_validator";
|
|
4
|
+
type LastEntry = {
|
|
5
|
+
text: string;
|
|
6
|
+
signature: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function buildThoughtSignatureSessionKey(ctx: AdapterContext, payload?: JsonObject): string | undefined;
|
|
10
|
+
export declare function cacheThoughtSignature(sessionKey: string, text: unknown, signature: unknown): void;
|
|
11
|
+
export declare function getCachedThoughtSignature(sessionKey: string, text: unknown): string | undefined;
|
|
12
|
+
export declare function getLastThoughtSignature(sessionKey: string): LastEntry | undefined;
|
|
13
|
+
export declare function shouldHandleThoughtSignature(ctx: AdapterContext): boolean;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
export const SKIP_THOUGHT_SIGNATURE = 'skip_thought_signature_validator';
|
|
6
|
+
const MEMORY_TTL_MS = 12 * 60 * 60 * 1000;
|
|
7
|
+
const DISK_TTL_MS = 12 * 60 * 60 * 1000;
|
|
8
|
+
const WRITE_INTERVAL_MS = 60 * 1000;
|
|
9
|
+
const MAX_ENTRIES_PER_SESSION = 50;
|
|
10
|
+
const MAX_SESSIONS = 200;
|
|
11
|
+
const TEXT_HASH_HEX_LEN = 16;
|
|
12
|
+
const sessionStores = new Map();
|
|
13
|
+
const sessionLast = new Map();
|
|
14
|
+
const diskEntries = new Map();
|
|
15
|
+
let dirty = false;
|
|
16
|
+
let writeTimer;
|
|
17
|
+
let cleanupTimer;
|
|
18
|
+
const stats = {
|
|
19
|
+
memoryHits: 0,
|
|
20
|
+
diskHits: 0,
|
|
21
|
+
misses: 0,
|
|
22
|
+
writes: 0,
|
|
23
|
+
lastWrite: 0
|
|
24
|
+
};
|
|
25
|
+
function resolveDefaultCachePath() {
|
|
26
|
+
const env = (process.env.ROUTECODEX_THOUGHT_SIGNATURE_CACHE_PATH ||
|
|
27
|
+
process.env.RCC_THOUGHT_SIGNATURE_CACHE_PATH ||
|
|
28
|
+
'').trim();
|
|
29
|
+
if (env)
|
|
30
|
+
return env;
|
|
31
|
+
const home = process.env.HOME || os.homedir() || '';
|
|
32
|
+
return path.join(home, '.routecodex', 'antigravity-signature-cache.json');
|
|
33
|
+
}
|
|
34
|
+
const persistencePath = resolveDefaultCachePath();
|
|
35
|
+
function now() {
|
|
36
|
+
return Date.now();
|
|
37
|
+
}
|
|
38
|
+
function normalizeText(value) {
|
|
39
|
+
if (typeof value !== 'string')
|
|
40
|
+
return undefined;
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
return trimmed.length ? trimmed : undefined;
|
|
43
|
+
}
|
|
44
|
+
function normalizeSignature(value) {
|
|
45
|
+
if (typeof value !== 'string')
|
|
46
|
+
return undefined;
|
|
47
|
+
const trimmed = value.trim();
|
|
48
|
+
return trimmed.length ? trimmed : undefined;
|
|
49
|
+
}
|
|
50
|
+
function hashText(text) {
|
|
51
|
+
return createHash('sha256').update(text, 'utf8').digest('hex').slice(0, TEXT_HASH_HEX_LEN);
|
|
52
|
+
}
|
|
53
|
+
function makeDiskKey(sessionKey, textHash) {
|
|
54
|
+
return `${sessionKey}:${textHash}`;
|
|
55
|
+
}
|
|
56
|
+
function ensureSession(sessionKey) {
|
|
57
|
+
let store = sessionStores.get(sessionKey);
|
|
58
|
+
if (!store) {
|
|
59
|
+
pruneSessionsIfNeeded();
|
|
60
|
+
store = new Map();
|
|
61
|
+
sessionStores.set(sessionKey, store);
|
|
62
|
+
}
|
|
63
|
+
return store;
|
|
64
|
+
}
|
|
65
|
+
function pruneSession(store) {
|
|
66
|
+
const cutoff = now() - MEMORY_TTL_MS;
|
|
67
|
+
for (const [key, entry] of store) {
|
|
68
|
+
if (entry.timestamp < cutoff) {
|
|
69
|
+
store.delete(key);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (store.size < MAX_ENTRIES_PER_SESSION)
|
|
73
|
+
return;
|
|
74
|
+
const entries = Array.from(store.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
75
|
+
const toRemove = entries.slice(0, Math.floor(MAX_ENTRIES_PER_SESSION / 4));
|
|
76
|
+
for (const [key] of toRemove) {
|
|
77
|
+
store.delete(key);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function ensureCacheDir(filePath) {
|
|
81
|
+
try {
|
|
82
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// best-effort
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function loadPersistedCache() {
|
|
89
|
+
try {
|
|
90
|
+
if (!fs.existsSync(persistencePath))
|
|
91
|
+
return;
|
|
92
|
+
const raw = fs.readFileSync(persistencePath, 'utf-8');
|
|
93
|
+
if (!raw.trim())
|
|
94
|
+
return;
|
|
95
|
+
const parsed = JSON.parse(raw);
|
|
96
|
+
if (!parsed || parsed.version !== '1.0')
|
|
97
|
+
return;
|
|
98
|
+
const cutoff = now() - DISK_TTL_MS;
|
|
99
|
+
for (const [key, entry] of Object.entries(parsed.entries || {})) {
|
|
100
|
+
if (!entry || typeof entry.signature !== 'string')
|
|
101
|
+
continue;
|
|
102
|
+
const ts = typeof entry.timestamp === 'number' ? entry.timestamp : now();
|
|
103
|
+
if (ts < cutoff)
|
|
104
|
+
continue;
|
|
105
|
+
diskEntries.set(key, { signature: entry.signature, timestamp: ts });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// best-effort
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function schedulePersist() {
|
|
113
|
+
if (writeTimer)
|
|
114
|
+
return;
|
|
115
|
+
writeTimer = setInterval(() => {
|
|
116
|
+
if (dirty) {
|
|
117
|
+
persistCache();
|
|
118
|
+
}
|
|
119
|
+
}, WRITE_INTERVAL_MS);
|
|
120
|
+
if (typeof writeTimer.unref === 'function') {
|
|
121
|
+
writeTimer.unref();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function scheduleCleanup() {
|
|
125
|
+
if (cleanupTimer)
|
|
126
|
+
return;
|
|
127
|
+
cleanupTimer = setInterval(() => {
|
|
128
|
+
const cutoff = now() - MEMORY_TTL_MS;
|
|
129
|
+
for (const store of sessionStores.values()) {
|
|
130
|
+
for (const [key, entry] of store.entries()) {
|
|
131
|
+
if (entry.timestamp < cutoff) {
|
|
132
|
+
store.delete(key);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}, 30 * 60 * 1000);
|
|
137
|
+
if (typeof cleanupTimer.unref === 'function') {
|
|
138
|
+
cleanupTimer.unref();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function pruneSessionsIfNeeded() {
|
|
142
|
+
if (sessionStores.size < MAX_SESSIONS)
|
|
143
|
+
return;
|
|
144
|
+
const entries = [];
|
|
145
|
+
for (const key of sessionStores.keys()) {
|
|
146
|
+
const last = sessionLast.get(key);
|
|
147
|
+
entries.push({ key, ts: last?.timestamp ?? 0 });
|
|
148
|
+
}
|
|
149
|
+
entries.sort((a, b) => a.ts - b.ts);
|
|
150
|
+
const removeCount = Math.max(1, sessionStores.size - (MAX_SESSIONS - 1));
|
|
151
|
+
for (const entry of entries.slice(0, removeCount)) {
|
|
152
|
+
sessionStores.delete(entry.key);
|
|
153
|
+
sessionLast.delete(entry.key);
|
|
154
|
+
for (const diskKey of diskEntries.keys()) {
|
|
155
|
+
if (diskKey.startsWith(`${entry.key}:`)) {
|
|
156
|
+
diskEntries.delete(diskKey);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function persistCache() {
|
|
162
|
+
try {
|
|
163
|
+
ensureCacheDir(persistencePath);
|
|
164
|
+
const nowTs = now();
|
|
165
|
+
const validDisk = {};
|
|
166
|
+
for (const [key, entry] of diskEntries.entries()) {
|
|
167
|
+
if (nowTs - entry.timestamp <= DISK_TTL_MS) {
|
|
168
|
+
validDisk[key] = entry;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const merged = { ...validDisk };
|
|
172
|
+
for (const [sessionKey, store] of sessionStores.entries()) {
|
|
173
|
+
for (const [textHash, entry] of store.entries()) {
|
|
174
|
+
const diskKey = makeDiskKey(sessionKey, textHash);
|
|
175
|
+
merged[diskKey] = { signature: entry.signature, timestamp: entry.timestamp };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const data = {
|
|
179
|
+
version: '1.0',
|
|
180
|
+
memory_ttl_seconds: MEMORY_TTL_MS / 1000,
|
|
181
|
+
disk_ttl_seconds: DISK_TTL_MS / 1000,
|
|
182
|
+
entries: merged,
|
|
183
|
+
statistics: {
|
|
184
|
+
memory_hits: stats.memoryHits,
|
|
185
|
+
disk_hits: stats.diskHits,
|
|
186
|
+
misses: stats.misses,
|
|
187
|
+
writes: stats.writes + 1,
|
|
188
|
+
last_write: nowTs
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const tmpPath = path.join(os.tmpdir(), `antigravity-cache-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);
|
|
192
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
193
|
+
try {
|
|
194
|
+
fs.renameSync(tmpPath, persistencePath);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
fs.writeFileSync(persistencePath, fs.readFileSync(tmpPath));
|
|
198
|
+
try {
|
|
199
|
+
fs.unlinkSync(tmpPath);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// ignore cleanup errors
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
stats.writes += 1;
|
|
206
|
+
stats.lastWrite = nowTs;
|
|
207
|
+
dirty = false;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// best-effort
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
loadPersistedCache();
|
|
214
|
+
schedulePersist();
|
|
215
|
+
scheduleCleanup();
|
|
216
|
+
export function buildThoughtSignatureSessionKey(ctx, payload) {
|
|
217
|
+
const sessionId = typeof ctx.sessionId === 'string'
|
|
218
|
+
? String(ctx.sessionId).trim()
|
|
219
|
+
: typeof ctx.conversationId === 'string'
|
|
220
|
+
? String(ctx.conversationId).trim()
|
|
221
|
+
: typeof ctx.requestId === 'string'
|
|
222
|
+
? String(ctx.requestId).trim()
|
|
223
|
+
: '';
|
|
224
|
+
if (!sessionId)
|
|
225
|
+
return undefined;
|
|
226
|
+
const model = (payload && typeof payload.model === 'string' && String(payload.model).trim()) ||
|
|
227
|
+
(typeof ctx.clientModelId === 'string' ? String(ctx.clientModelId).trim() : '') ||
|
|
228
|
+
(typeof ctx.originalModelId === 'string' ? String(ctx.originalModelId).trim() : '');
|
|
229
|
+
const protocol = typeof ctx.providerProtocol === 'string' && ctx.providerProtocol.trim()
|
|
230
|
+
? ctx.providerProtocol.trim()
|
|
231
|
+
: 'unknown';
|
|
232
|
+
const modelKey = model || 'unknown';
|
|
233
|
+
return `${protocol}:${modelKey}:${sessionId}`;
|
|
234
|
+
}
|
|
235
|
+
export function cacheThoughtSignature(sessionKey, text, signature) {
|
|
236
|
+
const normalizedText = normalizeText(text);
|
|
237
|
+
const normalizedSignature = normalizeSignature(signature);
|
|
238
|
+
if (!sessionKey || !normalizedText || !normalizedSignature)
|
|
239
|
+
return;
|
|
240
|
+
const textHash = hashText(normalizedText);
|
|
241
|
+
const store = ensureSession(sessionKey);
|
|
242
|
+
if (store.size >= MAX_ENTRIES_PER_SESSION) {
|
|
243
|
+
pruneSession(store);
|
|
244
|
+
}
|
|
245
|
+
store.set(textHash, { signature: normalizedSignature, timestamp: now() });
|
|
246
|
+
sessionLast.set(sessionKey, { text: normalizedText, signature: normalizedSignature, timestamp: now() });
|
|
247
|
+
dirty = true;
|
|
248
|
+
}
|
|
249
|
+
export function getCachedThoughtSignature(sessionKey, text) {
|
|
250
|
+
if (!sessionKey)
|
|
251
|
+
return undefined;
|
|
252
|
+
const normalizedText = normalizeText(text);
|
|
253
|
+
if (!normalizedText)
|
|
254
|
+
return undefined;
|
|
255
|
+
const textHash = hashText(normalizedText);
|
|
256
|
+
const store = sessionStores.get(sessionKey);
|
|
257
|
+
if (store) {
|
|
258
|
+
const entry = store.get(textHash);
|
|
259
|
+
if (entry) {
|
|
260
|
+
if (now() - entry.timestamp <= MEMORY_TTL_MS) {
|
|
261
|
+
stats.memoryHits += 1;
|
|
262
|
+
return entry.signature;
|
|
263
|
+
}
|
|
264
|
+
store.delete(textHash);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const diskKey = makeDiskKey(sessionKey, textHash);
|
|
268
|
+
const diskEntry = diskEntries.get(diskKey);
|
|
269
|
+
if (diskEntry && now() - diskEntry.timestamp <= DISK_TTL_MS) {
|
|
270
|
+
stats.diskHits += 1;
|
|
271
|
+
const mem = ensureSession(sessionKey);
|
|
272
|
+
mem.set(textHash, { signature: diskEntry.signature, timestamp: now() });
|
|
273
|
+
return diskEntry.signature;
|
|
274
|
+
}
|
|
275
|
+
stats.misses += 1;
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
export function getLastThoughtSignature(sessionKey) {
|
|
279
|
+
if (!sessionKey)
|
|
280
|
+
return undefined;
|
|
281
|
+
return sessionLast.get(sessionKey);
|
|
282
|
+
}
|
|
283
|
+
export function shouldHandleThoughtSignature(ctx) {
|
|
284
|
+
const provider = typeof ctx.providerId === 'string' ? ctx.providerId.toLowerCase() : '';
|
|
285
|
+
if (provider === 'antigravity')
|
|
286
|
+
return true;
|
|
287
|
+
const profile = typeof ctx.profileId === 'string' ? ctx.profileId.toLowerCase() : '';
|
|
288
|
+
return profile.includes('antigravity');
|
|
289
|
+
}
|
|
@@ -8,6 +8,7 @@ import { OpenAIChatResponseMapper, ResponsesResponseMapper, AnthropicResponseMap
|
|
|
8
8
|
import { runRespInboundStage1SseDecode } from '../pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js';
|
|
9
9
|
import { runRespInboundStage2FormatParse } from '../pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js';
|
|
10
10
|
import { runRespInboundStage3SemanticMap } from '../pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js';
|
|
11
|
+
import { runRespInboundStage3ThoughtSignatureCapture } from '../pipeline/stages/resp_inbound/resp_inbound_stage3_thought_signature_capture/index.js';
|
|
11
12
|
import { runRespInboundStageCompatResponse } from '../pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js';
|
|
12
13
|
import { runRespProcessStage1ToolGovernance } from '../pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js';
|
|
13
14
|
import { runRespProcessStage2Finalize } from '../pipeline/stages/resp_process/resp_process_stage2_finalize/index.js';
|
|
@@ -299,6 +300,11 @@ export async function convertProviderResponse(options) {
|
|
|
299
300
|
stageRecorder: options.stageRecorder
|
|
300
301
|
});
|
|
301
302
|
stripInternalPolicyDebugFields(formatEnvelope.payload);
|
|
303
|
+
formatEnvelope.payload = await runRespInboundStage3ThoughtSignatureCapture({
|
|
304
|
+
payload: formatEnvelope.payload,
|
|
305
|
+
adapterContext: options.context,
|
|
306
|
+
stageRecorder: options.stageRecorder
|
|
307
|
+
});
|
|
302
308
|
// Phase 2 (shadow): response tool surface mismatch detection (provider inbound).
|
|
303
309
|
// Only records diffs; does not rewrite payload.
|
|
304
310
|
try {
|
|
@@ -1241,6 +1241,12 @@ function coerceAliasSelectionStrategy(value) {
|
|
|
1241
1241
|
if (normalized === 'sticky-queue' || normalized === 'sticky_queue' || normalized === 'stickyqueue') {
|
|
1242
1242
|
return 'sticky-queue';
|
|
1243
1243
|
}
|
|
1244
|
+
if (normalized === 'best-quota' ||
|
|
1245
|
+
normalized === 'best_quota' ||
|
|
1246
|
+
normalized === 'quota-best' ||
|
|
1247
|
+
normalized === 'quota_best') {
|
|
1248
|
+
return 'best-quota';
|
|
1249
|
+
}
|
|
1244
1250
|
return undefined;
|
|
1245
1251
|
}
|
|
1246
1252
|
function coerceRatio(value) {
|
|
@@ -13,3 +13,18 @@ export declare function pinCandidatesByAliasQueue(opts: {
|
|
|
13
13
|
modelIdOfKey: (providerKey: string) => string | null;
|
|
14
14
|
availabilityCheck: (providerKey: string) => boolean;
|
|
15
15
|
}): string[] | null;
|
|
16
|
+
export declare function pinCandidatesByBestQuota(opts: {
|
|
17
|
+
providerId: string;
|
|
18
|
+
modelId: string;
|
|
19
|
+
candidates: string[];
|
|
20
|
+
orderedTargets: string[];
|
|
21
|
+
aliasOfKey: (providerKey: string) => string | null;
|
|
22
|
+
modelIdOfKey: (providerKey: string) => string | null;
|
|
23
|
+
quotaView: ((providerKey: string) => {
|
|
24
|
+
remainingFraction?: number | null;
|
|
25
|
+
inPool: boolean;
|
|
26
|
+
cooldownUntil?: number | null;
|
|
27
|
+
blacklistUntil?: number | null;
|
|
28
|
+
} | null) | undefined;
|
|
29
|
+
now: number;
|
|
30
|
+
}): string[] | null;
|