@jsonstudio/llms 0.6.1643 → 0.6.1739
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/actions/harvest-tool-calls-from-text.d.ts +10 -0
- package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +121 -0
- package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.d.ts +10 -0
- package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.js +80 -0
- package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.d.ts +7 -0
- package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.js +161 -0
- package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.d.ts +12 -0
- package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.js +67 -0
- package/dist/conversion/compat/actions/iflow-response-body-unwrap.d.ts +9 -0
- package/dist/conversion/compat/actions/iflow-response-body-unwrap.js +140 -0
- package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.d.ts +10 -0
- package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.js +59 -0
- package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.d.ts +14 -0
- package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.js +125 -0
- package/dist/conversion/compat/actions/normalize-tool-call-ids.d.ts +11 -0
- package/dist/conversion/compat/actions/normalize-tool-call-ids.js +140 -0
- package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.d.ts +2 -0
- package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +152 -0
- package/dist/conversion/compat/antigravity-session-signature.d.ts +1 -1
- package/dist/conversion/compat/antigravity-session-signature.js +5 -4
- package/dist/conversion/compat/profiles/chat-iflow.json +6 -0
- package/dist/conversion/compat/profiles/chat-lmstudio.json +7 -1
- package/dist/conversion/hub/operation-table/operation-table-runner.js +1 -1
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +19 -2
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +101 -5
- package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.d.ts +2 -0
- package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +63 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +18 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +1 -1
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +8 -5
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +5 -1
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +113 -0
- package/dist/conversion/hub/pipeline/target-utils.js +3 -0
- package/dist/conversion/hub/response/provider-response.js +27 -1
- package/dist/conversion/responses/responses-openai-bridge.js +32 -6
- package/dist/conversion/shared/anthropic-message-utils.js +20 -5
- package/dist/conversion/shared/bridge-id-utils.d.ts +2 -0
- package/dist/conversion/shared/bridge-id-utils.js +52 -15
- package/dist/conversion/shared/responses-conversation-store.js +40 -5
- package/dist/conversion/shared/responses-output-builder.js +23 -7
- package/dist/conversion/shared/responses-tool-utils.d.ts +1 -0
- package/dist/conversion/shared/responses-tool-utils.js +30 -13
- package/dist/conversion/shared/text-markup-normalizer.d.ts +1 -0
- package/dist/conversion/shared/text-markup-normalizer.js +269 -1
- package/dist/router/virtual-router/bootstrap.js +31 -7
- package/dist/router/virtual-router/classifier.js +1 -1
- package/dist/router/virtual-router/engine/antigravity/alias-lease.d.ts +33 -0
- package/dist/router/virtual-router/engine/antigravity/alias-lease.js +247 -0
- package/dist/router/virtual-router/engine/health/index.d.ts +23 -0
- package/dist/router/virtual-router/engine/health/index.js +720 -0
- package/dist/router/virtual-router/engine/provider-key/parse.d.ts +6 -0
- package/dist/router/virtual-router/engine/provider-key/parse.js +43 -0
- package/dist/router/virtual-router/engine/routing-pools/index.d.ts +13 -0
- package/dist/router/virtual-router/engine/routing-pools/index.js +225 -0
- package/dist/router/virtual-router/engine/routing-state/keys.d.ts +3 -0
- package/dist/router/virtual-router/engine/routing-state/keys.js +30 -0
- package/dist/router/virtual-router/engine/routing-state/metadata.d.ts +6 -0
- package/dist/router/virtual-router/engine/routing-state/metadata.js +132 -0
- package/dist/router/virtual-router/engine/routing-state/store.d.ts +11 -0
- package/dist/router/virtual-router/engine/routing-state/store.js +107 -0
- package/dist/router/virtual-router/engine-health.d.ts +1 -23
- package/dist/router/virtual-router/engine-health.js +1 -720
- package/dist/router/virtual-router/engine-selection/route-utils.js +57 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-select.js +8 -48
- package/dist/router/virtual-router/engine-selection/tier-selection.js +34 -17
- package/dist/router/virtual-router/engine-selection.d.ts +1 -13
- package/dist/router/virtual-router/engine-selection.js +1 -225
- package/dist/router/virtual-router/engine.d.ts +2 -23
- package/dist/router/virtual-router/engine.js +130 -603
- package/dist/router/virtual-router/message-utils.js +15 -5
- package/dist/servertool/engine.js +4 -4
- package/dist/servertool/handlers/followup-request-builder.js +46 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +48 -47
- package/dist/servertool/handlers/stop-message-auto.js +64 -7
- package/dist/servertool/handlers/vision.js +10 -0
- package/dist/servertool/types.d.ts +3 -0
- package/dist/sse/sse-to-json/builders/response-builder.js +6 -0
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +32 -2
- package/dist/sse/sse-to-json/parsers/sse-parser.js +34 -0
- package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -0
- package/dist/sse/sse-to-json/responses-sse-to-json-converter.js +33 -1
- package/dist/tools/apply-patch/args-normalizer/default-actions.d.ts +2 -0
- package/dist/tools/apply-patch/args-normalizer/default-actions.js +12 -0
- package/dist/tools/apply-patch/args-normalizer/extract-patch.d.ts +2 -0
- package/dist/tools/apply-patch/args-normalizer/extract-patch.js +15 -0
- package/dist/tools/apply-patch/args-normalizer/index.d.ts +2 -0
- package/dist/tools/apply-patch/args-normalizer/index.js +164 -0
- package/dist/tools/apply-patch/args-normalizer/structured-builders.d.ts +7 -0
- package/dist/tools/apply-patch/args-normalizer/structured-builders.js +85 -0
- package/dist/tools/apply-patch/args-normalizer/types.d.ts +54 -0
- package/dist/tools/apply-patch/args-normalizer/types.js +1 -0
- package/dist/tools/apply-patch/patch-text/looks-like-patch.js +1 -0
- package/dist/tools/apply-patch/patch-text/normalize.js +104 -5
- package/dist/tools/apply-patch/structured/coercion.js +28 -4
- package/dist/tools/apply-patch/validator.js +7 -146
- package/package.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { normalizeFunctionCallId, normalizeFunctionCallOutputId, normalizeResponsesCallId } from '../../shared/bridge-id-utils.js';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function normalizeToCallId(raw, fallback) {
|
|
6
|
+
const rawStr = typeof raw === 'string' ? raw.trim() : '';
|
|
7
|
+
return normalizeResponsesCallId({ callId: rawStr || undefined, fallback });
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* LM Studio compat:
|
|
11
|
+
* Some LM Studio OpenAI-Responses endpoints accept tool-call items but are picky about ids:
|
|
12
|
+
* - `call_id` should remain `call_*` (used to link tool outputs)
|
|
13
|
+
* - `id` for `function_call` / `function_call_output` should be `fc_*` (OpenAI Responses convention)
|
|
14
|
+
*
|
|
15
|
+
* We apply this in compat (profile: chat:lmstudio) to keep provider layer transport-only.
|
|
16
|
+
*/
|
|
17
|
+
export function enforceLmstudioResponsesFcToolCallIds(payload) {
|
|
18
|
+
try {
|
|
19
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
20
|
+
return payload;
|
|
21
|
+
}
|
|
22
|
+
const root = structuredClone(payload);
|
|
23
|
+
const input = root.input;
|
|
24
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
25
|
+
return root;
|
|
26
|
+
}
|
|
27
|
+
let callCounter = 0;
|
|
28
|
+
for (const item of input) {
|
|
29
|
+
if (!isRecord(item))
|
|
30
|
+
continue;
|
|
31
|
+
const type = typeof item.type === 'string' ? item.type.trim().toLowerCase() : '';
|
|
32
|
+
if (type !== 'function_call' && type !== 'function_call_output') {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const rawCallId = (item.call_id ?? item.tool_call_id ?? item.id);
|
|
36
|
+
const callId = normalizeToCallId(rawCallId, `call_${++callCounter}`);
|
|
37
|
+
item.call_id = callId;
|
|
38
|
+
if (type === 'function_call') {
|
|
39
|
+
const normalizedItemId = normalizeFunctionCallId({
|
|
40
|
+
callId,
|
|
41
|
+
fallback: `fc_${callId}`
|
|
42
|
+
});
|
|
43
|
+
item.id = normalizedItemId;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// function_call_output
|
|
47
|
+
const normalizedOutputId = normalizeFunctionCallOutputId({
|
|
48
|
+
callId,
|
|
49
|
+
fallback: typeof item.id === 'string' && item.id.trim().length ? item.id.trim() : `fc_tool_${callCounter}`
|
|
50
|
+
});
|
|
51
|
+
item.id = normalizedOutputId;
|
|
52
|
+
}
|
|
53
|
+
root.input = input;
|
|
54
|
+
return root;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return payload;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { JsonObject } from '../../hub/types/json.js';
|
|
2
|
+
import type { AdapterContext } from '../../hub/types/chat-envelope.js';
|
|
3
|
+
/**
|
|
4
|
+
* Legacy compatibility shim:
|
|
5
|
+
* Some older LM Studio builds rejected the array form of `input` ("Invalid type for 'input'").
|
|
6
|
+
* Convert canonical Responses input items into a single `input` string.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ Default is OFF (modern LM Studio accepts array input). Enable only if you hit that legacy error:
|
|
9
|
+
* - `LLMSWITCH_LMSTUDIO_STRINGIFY_INPUT=1`
|
|
10
|
+
* - or `ROUTECODEX_LMSTUDIO_STRINGIFY_INPUT=1`
|
|
11
|
+
*
|
|
12
|
+
* This is applied via compat profile `chat:lmstudio` and only when `providerProtocol === 'openai-responses'`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function stringifyLmstudioResponsesInput(payload: JsonObject, adapterContext?: AdapterContext): JsonObject;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function extractTextParts(content) {
|
|
5
|
+
const out = [];
|
|
6
|
+
if (typeof content === 'string' && content.trim().length) {
|
|
7
|
+
out.push(content.trim());
|
|
8
|
+
return out;
|
|
9
|
+
}
|
|
10
|
+
if (!Array.isArray(content)) {
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
for (const part of content) {
|
|
14
|
+
if (!isRecord(part))
|
|
15
|
+
continue;
|
|
16
|
+
const type = typeof part.type === 'string' ? String(part.type).trim().toLowerCase() : '';
|
|
17
|
+
const text = typeof part.text === 'string'
|
|
18
|
+
? part.text
|
|
19
|
+
: typeof part.content === 'string'
|
|
20
|
+
? String(part.content)
|
|
21
|
+
: undefined;
|
|
22
|
+
if (typeof text === 'string' && text.trim().length) {
|
|
23
|
+
out.push(text.trim());
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
// OpenAI Responses content parts often use { type: 'input_text'|'output_text', text: '...' }.
|
|
27
|
+
if ((type === 'input_text' || type === 'output_text') && typeof part.text === 'string') {
|
|
28
|
+
const t = String(part.text).trim();
|
|
29
|
+
if (t.length)
|
|
30
|
+
out.push(t);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function stringifyInputItems(input) {
|
|
36
|
+
if (!Array.isArray(input))
|
|
37
|
+
return null;
|
|
38
|
+
const chunks = [];
|
|
39
|
+
for (const item of input) {
|
|
40
|
+
if (!isRecord(item))
|
|
41
|
+
continue;
|
|
42
|
+
const type = typeof item.type === 'string' ? String(item.type).trim().toLowerCase() : '';
|
|
43
|
+
const roleCandidate = typeof item.role === 'string' ? String(item.role).trim() : '';
|
|
44
|
+
const messageNode = isRecord(item.message) ? item.message : undefined;
|
|
45
|
+
const nestedRoleCandidate = messageNode && typeof messageNode.role === 'string' ? String(messageNode.role).trim() : '';
|
|
46
|
+
// OpenAI Responses supports message-like items without an explicit `type` field:
|
|
47
|
+
// { role: 'user'|'assistant'|'system', content: [...] }
|
|
48
|
+
if (type === 'message' || (!type && (roleCandidate || nestedRoleCandidate))) {
|
|
49
|
+
const role = roleCandidate ||
|
|
50
|
+
nestedRoleCandidate ||
|
|
51
|
+
'user';
|
|
52
|
+
const contentNode = item.content !== undefined ? item.content : messageNode?.content;
|
|
53
|
+
const parts = extractTextParts(contentNode);
|
|
54
|
+
if (parts.length) {
|
|
55
|
+
chunks.push(`${role}: ${parts.join('\n')}`);
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (type === 'function_call') {
|
|
60
|
+
const name = typeof item.name === 'string' ? String(item.name).trim() : 'tool';
|
|
61
|
+
const args = typeof item.arguments === 'string'
|
|
62
|
+
? String(item.arguments)
|
|
63
|
+
: (() => {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.stringify(item.arguments ?? null);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return String(item.arguments ?? '');
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
chunks.push(`assistant tool_call ${name}: ${args}`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (type === 'function_call_output') {
|
|
75
|
+
const output = typeof item.output === 'string'
|
|
76
|
+
? String(item.output)
|
|
77
|
+
: (() => {
|
|
78
|
+
try {
|
|
79
|
+
return JSON.stringify(item.output ?? null);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return String(item.output ?? '');
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
chunks.push(`tool_output: ${output}`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!chunks.length)
|
|
90
|
+
return '';
|
|
91
|
+
return chunks.join('\n\n');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Legacy compatibility shim:
|
|
95
|
+
* Some older LM Studio builds rejected the array form of `input` ("Invalid type for 'input'").
|
|
96
|
+
* Convert canonical Responses input items into a single `input` string.
|
|
97
|
+
*
|
|
98
|
+
* ⚠️ Default is OFF (modern LM Studio accepts array input). Enable only if you hit that legacy error:
|
|
99
|
+
* - `LLMSWITCH_LMSTUDIO_STRINGIFY_INPUT=1`
|
|
100
|
+
* - or `ROUTECODEX_LMSTUDIO_STRINGIFY_INPUT=1`
|
|
101
|
+
*
|
|
102
|
+
* This is applied via compat profile `chat:lmstudio` and only when `providerProtocol === 'openai-responses'`.
|
|
103
|
+
*/
|
|
104
|
+
export function stringifyLmstudioResponsesInput(payload, adapterContext) {
|
|
105
|
+
const enabled = process.env.LLMSWITCH_LMSTUDIO_STRINGIFY_INPUT === '1' ||
|
|
106
|
+
process.env.ROUTECODEX_LMSTUDIO_STRINGIFY_INPUT === '1';
|
|
107
|
+
if (!enabled) {
|
|
108
|
+
return payload;
|
|
109
|
+
}
|
|
110
|
+
if (!adapterContext || adapterContext.providerProtocol !== 'openai-responses') {
|
|
111
|
+
return payload;
|
|
112
|
+
}
|
|
113
|
+
const record = payload;
|
|
114
|
+
const input = record.input;
|
|
115
|
+
if (!Array.isArray(input)) {
|
|
116
|
+
return payload;
|
|
117
|
+
}
|
|
118
|
+
const flattened = stringifyInputItems(input);
|
|
119
|
+
if (flattened === null) {
|
|
120
|
+
return payload;
|
|
121
|
+
}
|
|
122
|
+
const instructions = typeof record.instructions === 'string' ? record.instructions.trim() : '';
|
|
123
|
+
record.input = instructions.length ? `${instructions}\n\n${flattened}`.trim() : flattened;
|
|
124
|
+
return payload;
|
|
125
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { JsonObject } from '../../hub/types/json.js';
|
|
2
|
+
/**
|
|
3
|
+
* Normalize tool-call identifiers across OpenAI-compatible payload variants.
|
|
4
|
+
*
|
|
5
|
+
* Goal: field-level normalization only (no rewriting/rehashing IDs):
|
|
6
|
+
* - Mirror `call_id` ↔ `tool_call_id` when one side is missing.
|
|
7
|
+
* - For Responses `input` / `output` function_call items, ensure `id` and `call_id` are aligned.
|
|
8
|
+
*
|
|
9
|
+
* This is intended to live in compatibility profiles (per-provider), not in host/provider code.
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeToolCallIdsInPlace(root: JsonObject): void;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function ensureRecord(parent, key) {
|
|
5
|
+
const existing = parent[key];
|
|
6
|
+
if (isRecord(existing)) {
|
|
7
|
+
return existing;
|
|
8
|
+
}
|
|
9
|
+
const next = {};
|
|
10
|
+
parent[key] = next;
|
|
11
|
+
return next;
|
|
12
|
+
}
|
|
13
|
+
function pickString(...candidates) {
|
|
14
|
+
for (const c of candidates) {
|
|
15
|
+
if (typeof c === 'string' && c.trim().length) {
|
|
16
|
+
return c.trim();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
function normalizeResponsesInputItem(item) {
|
|
22
|
+
const type = typeof item.type === 'string' ? item.type : '';
|
|
23
|
+
if (type === 'function_call') {
|
|
24
|
+
const id = pickString(item.id);
|
|
25
|
+
const callId = pickString(item.call_id);
|
|
26
|
+
if (callId && !id) {
|
|
27
|
+
item.id = callId;
|
|
28
|
+
}
|
|
29
|
+
else if (id && !callId) {
|
|
30
|
+
item.call_id = id;
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (type === 'function_call_output' || type === 'tool_result' || type === 'tool_message') {
|
|
35
|
+
const id = pickString(item.id);
|
|
36
|
+
const callId = pickString(item.call_id);
|
|
37
|
+
const toolCallId = pickString(item.tool_call_id);
|
|
38
|
+
const resolvedCall = pickString(callId, toolCallId);
|
|
39
|
+
if (resolvedCall && !callId) {
|
|
40
|
+
item.call_id = resolvedCall;
|
|
41
|
+
}
|
|
42
|
+
if (id && !callId && !toolCallId) {
|
|
43
|
+
// Some upstreams only provide an id for tool output items; mirror it to both fields.
|
|
44
|
+
item.call_id = id;
|
|
45
|
+
item.tool_call_id = id;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function normalizeResponsesOutputItem(item) {
|
|
50
|
+
const type = typeof item.type === 'string' ? item.type : '';
|
|
51
|
+
if (type === 'function_call') {
|
|
52
|
+
const id = pickString(item.id, item.item_id);
|
|
53
|
+
const callId = pickString(item.call_id);
|
|
54
|
+
if (callId && !id) {
|
|
55
|
+
item.id = callId;
|
|
56
|
+
}
|
|
57
|
+
else if (id && !callId) {
|
|
58
|
+
item.call_id = id;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function normalizeToolOutputsArray(toolOutputs) {
|
|
63
|
+
if (!Array.isArray(toolOutputs))
|
|
64
|
+
return;
|
|
65
|
+
for (const entry of toolOutputs) {
|
|
66
|
+
if (!isRecord(entry))
|
|
67
|
+
continue;
|
|
68
|
+
const toolCallId = pickString(entry.tool_call_id);
|
|
69
|
+
const callId = pickString(entry.call_id);
|
|
70
|
+
const resolved = pickString(toolCallId, callId, entry.id);
|
|
71
|
+
if (resolved) {
|
|
72
|
+
entry.tool_call_id = resolved;
|
|
73
|
+
entry.call_id = resolved;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function normalizeChatMessages(messages) {
|
|
78
|
+
if (!Array.isArray(messages))
|
|
79
|
+
return;
|
|
80
|
+
for (const msg of messages) {
|
|
81
|
+
if (!isRecord(msg))
|
|
82
|
+
continue;
|
|
83
|
+
// chat tool message uses tool_call_id; mirror to call_id for downstream consumers that only look at call_id.
|
|
84
|
+
const toolCallId = pickString(msg.tool_call_id);
|
|
85
|
+
const callId = pickString(msg.call_id);
|
|
86
|
+
const resolved = pickString(toolCallId, callId);
|
|
87
|
+
if (resolved) {
|
|
88
|
+
msg.tool_call_id = resolved;
|
|
89
|
+
msg.call_id = resolved;
|
|
90
|
+
}
|
|
91
|
+
const toolCalls = msg.tool_calls;
|
|
92
|
+
if (Array.isArray(toolCalls)) {
|
|
93
|
+
for (const call of toolCalls) {
|
|
94
|
+
if (!isRecord(call))
|
|
95
|
+
continue;
|
|
96
|
+
const id = pickString(call.id);
|
|
97
|
+
const callId2 = pickString(call.call_id);
|
|
98
|
+
if (id && !callId2) {
|
|
99
|
+
call.call_id = id;
|
|
100
|
+
}
|
|
101
|
+
else if (callId2 && !id) {
|
|
102
|
+
call.id = callId2;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Normalize tool-call identifiers across OpenAI-compatible payload variants.
|
|
110
|
+
*
|
|
111
|
+
* Goal: field-level normalization only (no rewriting/rehashing IDs):
|
|
112
|
+
* - Mirror `call_id` ↔ `tool_call_id` when one side is missing.
|
|
113
|
+
* - For Responses `input` / `output` function_call items, ensure `id` and `call_id` are aligned.
|
|
114
|
+
*
|
|
115
|
+
* This is intended to live in compatibility profiles (per-provider), not in host/provider code.
|
|
116
|
+
*/
|
|
117
|
+
export function normalizeToolCallIdsInPlace(root) {
|
|
118
|
+
const record = root;
|
|
119
|
+
// Responses request
|
|
120
|
+
if (Array.isArray(record.input)) {
|
|
121
|
+
for (const item of record.input) {
|
|
122
|
+
if (isRecord(item)) {
|
|
123
|
+
normalizeResponsesInputItem(item);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Responses submit_tool_outputs request (common alias)
|
|
128
|
+
normalizeToolOutputsArray(record.tool_outputs);
|
|
129
|
+
normalizeToolOutputsArray(record.toolOutputs);
|
|
130
|
+
// Responses response
|
|
131
|
+
if (Array.isArray(record.output)) {
|
|
132
|
+
for (const item of record.output) {
|
|
133
|
+
if (isRecord(item)) {
|
|
134
|
+
normalizeResponsesOutputItem(item);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Chat request/response
|
|
139
|
+
normalizeChatMessages(record.messages);
|
|
140
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
const ORPHAN_TAG_RE = /^\s*(?:[•*+-]\s*)?<\/?\s*function_calls\s*\/?\s*>\s*$/i;
|
|
5
|
+
function stripOrphanTagLines(text) {
|
|
6
|
+
const raw = String(text ?? '');
|
|
7
|
+
if (!raw)
|
|
8
|
+
return { text: raw, changed: false };
|
|
9
|
+
const lines = raw.split(/\r?\n/);
|
|
10
|
+
const kept = [];
|
|
11
|
+
let changed = false;
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
if (ORPHAN_TAG_RE.test(line)) {
|
|
14
|
+
changed = true;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
kept.push(line);
|
|
18
|
+
}
|
|
19
|
+
return { text: kept.join('\n'), changed };
|
|
20
|
+
}
|
|
21
|
+
function stripInMessageInPlace(message) {
|
|
22
|
+
let changed = false;
|
|
23
|
+
const content = message.content;
|
|
24
|
+
if (typeof content === 'string') {
|
|
25
|
+
const res = stripOrphanTagLines(content);
|
|
26
|
+
if (res.changed) {
|
|
27
|
+
message.content = res.text;
|
|
28
|
+
changed = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (Array.isArray(content)) {
|
|
32
|
+
const next = content.map((part) => {
|
|
33
|
+
if (typeof part === 'string') {
|
|
34
|
+
const res = stripOrphanTagLines(part);
|
|
35
|
+
if (res.changed)
|
|
36
|
+
changed = true;
|
|
37
|
+
return res.text;
|
|
38
|
+
}
|
|
39
|
+
if (!isRecord(part))
|
|
40
|
+
return part;
|
|
41
|
+
const p = { ...part };
|
|
42
|
+
if (typeof p.text === 'string') {
|
|
43
|
+
const res = stripOrphanTagLines(p.text);
|
|
44
|
+
if (res.changed) {
|
|
45
|
+
p.text = res.text;
|
|
46
|
+
changed = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (typeof p.content === 'string') {
|
|
50
|
+
const res = stripOrphanTagLines(p.content);
|
|
51
|
+
if (res.changed) {
|
|
52
|
+
p.content = res.text;
|
|
53
|
+
changed = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return p;
|
|
57
|
+
});
|
|
58
|
+
if (changed) {
|
|
59
|
+
message.content = next;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const key of ['reasoning', 'thinking', 'reasoning_content']) {
|
|
63
|
+
const raw = message[key];
|
|
64
|
+
if (typeof raw === 'string' && raw.trim().length) {
|
|
65
|
+
const res = stripOrphanTagLines(raw);
|
|
66
|
+
if (res.changed) {
|
|
67
|
+
message[key] = res.text;
|
|
68
|
+
changed = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return changed;
|
|
73
|
+
}
|
|
74
|
+
function stripInChatPayloadInPlace(root) {
|
|
75
|
+
const choices = Array.isArray(root.choices) ? root.choices : [];
|
|
76
|
+
if (!choices.length)
|
|
77
|
+
return false;
|
|
78
|
+
let changed = false;
|
|
79
|
+
for (const choice of choices) {
|
|
80
|
+
if (!isRecord(choice))
|
|
81
|
+
continue;
|
|
82
|
+
const message = choice.message;
|
|
83
|
+
if (!isRecord(message))
|
|
84
|
+
continue;
|
|
85
|
+
const role = typeof message.role === 'string' ? String(message.role).trim().toLowerCase() : 'assistant';
|
|
86
|
+
if (role !== 'assistant')
|
|
87
|
+
continue;
|
|
88
|
+
if (stripInMessageInPlace(message)) {
|
|
89
|
+
changed = true;
|
|
90
|
+
choice.message = message;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return changed;
|
|
94
|
+
}
|
|
95
|
+
function stripInResponsesPayloadInPlace(root) {
|
|
96
|
+
const output = Array.isArray(root.output) ? root.output : [];
|
|
97
|
+
if (!output.length)
|
|
98
|
+
return false;
|
|
99
|
+
let changed = false;
|
|
100
|
+
for (const item of output) {
|
|
101
|
+
if (!isRecord(item))
|
|
102
|
+
continue;
|
|
103
|
+
const type = typeof item.type === 'string' ? String(item.type).trim().toLowerCase() : '';
|
|
104
|
+
if (type !== 'message')
|
|
105
|
+
continue;
|
|
106
|
+
const role = typeof item.role === 'string' ? String(item.role).trim().toLowerCase() : 'assistant';
|
|
107
|
+
if (role !== 'assistant')
|
|
108
|
+
continue;
|
|
109
|
+
const content = Array.isArray(item.content) ? item.content : [];
|
|
110
|
+
if (content.length) {
|
|
111
|
+
const next = content.map((part) => {
|
|
112
|
+
if (!isRecord(part))
|
|
113
|
+
return part;
|
|
114
|
+
const p = { ...part };
|
|
115
|
+
for (const key of ['text', 'content', 'value']) {
|
|
116
|
+
if (typeof p[key] === 'string' && String(p[key]).trim().length) {
|
|
117
|
+
const res = stripOrphanTagLines(String(p[key]));
|
|
118
|
+
if (res.changed) {
|
|
119
|
+
p[key] = res.text;
|
|
120
|
+
changed = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return p;
|
|
125
|
+
});
|
|
126
|
+
if (changed) {
|
|
127
|
+
item.content = next;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (typeof item.output_text === 'string' && String(item.output_text).trim().length) {
|
|
131
|
+
const res = stripOrphanTagLines(String(item.output_text));
|
|
132
|
+
if (res.changed) {
|
|
133
|
+
item.output_text = res.text;
|
|
134
|
+
changed = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return changed;
|
|
139
|
+
}
|
|
140
|
+
export function stripOrphanFunctionCallsTag(payload) {
|
|
141
|
+
try {
|
|
142
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
143
|
+
return payload;
|
|
144
|
+
}
|
|
145
|
+
const root = structuredClone(payload);
|
|
146
|
+
const changed = stripInChatPayloadInPlace(root) || stripInResponsesPayloadInPlace(root);
|
|
147
|
+
return (changed ? root : payload);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return payload;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -23,7 +23,7 @@ export declare function getAntigravityRequestSessionMeta(requestId: string): {
|
|
|
23
23
|
} | undefined;
|
|
24
24
|
/**
|
|
25
25
|
* Antigravity-Manager alignment: derive a stable session fingerprint for Gemini native requests.
|
|
26
|
-
* - sha256(first user text parts joined), if
|
|
26
|
+
* - sha256(first user text parts joined), if non-empty and no "<system-reminder>"
|
|
27
27
|
* - else sha256(JSON body)
|
|
28
28
|
* - sid = "sid-" + first 16 hex chars
|
|
29
29
|
*/
|
|
@@ -721,7 +721,7 @@ function findGeminiContentsNode(payload) {
|
|
|
721
721
|
}
|
|
722
722
|
/**
|
|
723
723
|
* Antigravity-Manager alignment: derive a stable session fingerprint for Gemini native requests.
|
|
724
|
-
* - sha256(first user text parts joined), if
|
|
724
|
+
* - sha256(first user text parts joined), if non-empty and no "<system-reminder>"
|
|
725
725
|
* - else sha256(JSON body)
|
|
726
726
|
* - sid = "sid-" + first 16 hex chars
|
|
727
727
|
*/
|
|
@@ -747,9 +747,10 @@ export function extractAntigravityGeminiSessionId(payload) {
|
|
|
747
747
|
}
|
|
748
748
|
}
|
|
749
749
|
const combined = texts.join(' ').trim();
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
|
|
750
|
+
// RouteCodex signature persistence alignment:
|
|
751
|
+
// - Prefer the first user message text as the stable session anchor, even when short.
|
|
752
|
+
// - Skip system-reminder carrier messages (common in tool followups).
|
|
753
|
+
if (combined.length > 0 && !combined.includes('<system-reminder>')) {
|
|
753
754
|
seed = combined;
|
|
754
755
|
break;
|
|
755
756
|
}
|
|
@@ -81,6 +81,9 @@
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
|
+
{ "action": "iflow_kimi_history_media_placeholder" },
|
|
85
|
+
{ "action": "iflow_kimi_cli_defaults" },
|
|
86
|
+
{ "action": "iflow_kimi_thinking_reasoning_fill" },
|
|
84
87
|
{
|
|
85
88
|
"action": "field_map",
|
|
86
89
|
"direction": "incoming",
|
|
@@ -114,6 +117,9 @@
|
|
|
114
117
|
"mappings": [
|
|
115
118
|
{ "action": "snapshot", "phase": "compat-pre" },
|
|
116
119
|
{ "action": "dto_unwrap" },
|
|
120
|
+
{ "action": "iflow_response_body_unwrap" },
|
|
121
|
+
{ "action": "harvest_tool_calls_from_text" },
|
|
122
|
+
{ "action": "strip_orphan_function_calls_tag" },
|
|
117
123
|
{
|
|
118
124
|
"action": "shape_filter",
|
|
119
125
|
"target": "response",
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "chat:lmstudio",
|
|
3
|
-
"protocol": "
|
|
3
|
+
"protocol": "",
|
|
4
4
|
"request": {
|
|
5
5
|
"mappings": [
|
|
6
6
|
{ "action": "snapshot", "phase": "compat-pre" },
|
|
7
7
|
{ "action": "dto_unwrap" },
|
|
8
|
+
{ "action": "lmstudio_responses_input_stringify" },
|
|
9
|
+
{ "action": "normalize_tool_call_ids" },
|
|
10
|
+
{ "action": "lmstudio_responses_fc_ids" },
|
|
8
11
|
{
|
|
9
12
|
"action": "normalize_tool_choice",
|
|
10
13
|
"path": "tool_choice",
|
|
@@ -18,6 +21,9 @@
|
|
|
18
21
|
"mappings": [
|
|
19
22
|
{ "action": "snapshot", "phase": "compat-pre" },
|
|
20
23
|
{ "action": "dto_unwrap" },
|
|
24
|
+
{ "action": "harvest_tool_calls_from_text" },
|
|
25
|
+
{ "action": "strip_orphan_function_calls_tag" },
|
|
26
|
+
{ "action": "normalize_tool_call_ids" },
|
|
21
27
|
{
|
|
22
28
|
"action": "set_default",
|
|
23
29
|
"path": "object",
|
|
@@ -12,7 +12,7 @@ const OUTBOUND_BRIDGE_SPECS = {
|
|
|
12
12
|
'openai-chat': { protocol: 'openai-chat', stage: 'request_outbound', messages: 'format_payload_messages', includeCapturedToolResults: true },
|
|
13
13
|
// Keep parity: openai-responses outbound actions should not touch normalized messages.
|
|
14
14
|
'openai-responses': { protocol: 'openai-responses', stage: 'request_outbound', messages: 'none', moduleType: 'openai-responses' },
|
|
15
|
-
'anthropic-messages': { protocol: 'anthropic-messages', stage: 'request_outbound', messages: '
|
|
15
|
+
'anthropic-messages': { protocol: 'anthropic-messages', stage: 'request_outbound', messages: 'none', includeCapturedToolResults: true },
|
|
16
16
|
// Keep parity with legacy gemini mapper: outbound hooks operate on ChatEnvelope.messages (not Gemini contents).
|
|
17
17
|
'gemini-chat': { protocol: 'gemini-chat', stage: 'request_outbound', messages: 'chat_envelope', includeCapturedToolResults: true }
|
|
18
18
|
};
|
|
@@ -488,7 +488,7 @@ function buildFunctionResponseEntry(output, options) {
|
|
|
488
488
|
}
|
|
489
489
|
};
|
|
490
490
|
if (includeCallId) {
|
|
491
|
-
part.functionResponse.id = output.tool_call_id;
|
|
491
|
+
part.functionResponse.id = sanitizeAntigravityToolCallId(output.tool_call_id);
|
|
492
492
|
}
|
|
493
493
|
return { role: 'user', parts: [part] };
|
|
494
494
|
}
|
|
@@ -795,7 +795,7 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
795
795
|
const functionCall = { name, args: argsJson };
|
|
796
796
|
const part = { functionCall };
|
|
797
797
|
if (includeToolCallIds && typeof tc.id === 'string' && tc.id.trim().length) {
|
|
798
|
-
part.functionCall.id = String(tc.id)
|
|
798
|
+
part.functionCall.id = sanitizeAntigravityToolCallId(String(tc.id));
|
|
799
799
|
}
|
|
800
800
|
// Antigravity-Manager alignment:
|
|
801
801
|
// - Do NOT invent a dummy thoughtSignature in conversion.
|
|
@@ -1092,6 +1092,23 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
1092
1092
|
const compatRequest = applyClaudeThinkingToolSchemaCompat(request, adapterContext);
|
|
1093
1093
|
return compatRequest;
|
|
1094
1094
|
}
|
|
1095
|
+
function sanitizeAntigravityToolCallId(raw) {
|
|
1096
|
+
const trimmed = typeof raw === 'string' ? raw.trim() : '';
|
|
1097
|
+
if (!trimmed) {
|
|
1098
|
+
return trimmed;
|
|
1099
|
+
}
|
|
1100
|
+
// Antigravity (Claude via Gemini) validates tool_use.id against: ^[a-zA-Z0-9_-]+$
|
|
1101
|
+
// Preserve stable IDs when already valid; otherwise sanitize minimally.
|
|
1102
|
+
if (/^[A-Za-z0-9_-]+$/.test(trimmed)) {
|
|
1103
|
+
return trimmed;
|
|
1104
|
+
}
|
|
1105
|
+
const sanitized = trimmed
|
|
1106
|
+
.replace(/[^A-Za-z0-9_-]/g, '_')
|
|
1107
|
+
.replace(/_{2,}/g, '_')
|
|
1108
|
+
.replace(/^_+/, '')
|
|
1109
|
+
.replace(/_+$/, '');
|
|
1110
|
+
return sanitized || `call_${Math.random().toString(36).slice(2, 10)}`;
|
|
1111
|
+
}
|
|
1095
1112
|
function isPlainRecord(value) {
|
|
1096
1113
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
1097
1114
|
}
|