@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,10 @@
|
|
|
1
|
+
import type { JsonObject } from '../../hub/types/json.js';
|
|
2
|
+
/**
|
|
3
|
+
* Harvest tool calls from assistant textual markup into OpenAI `tool_calls`.
|
|
4
|
+
*
|
|
5
|
+
* Some upstreams (including certain iFlow/hosted models) emit tool calls as plain text tokens
|
|
6
|
+
* (e.g. `<|tool_call_begin|> ...`) instead of structured `tool_calls`.
|
|
7
|
+
*
|
|
8
|
+
* This action is response-only and provider-scoped via compatibility profiles.
|
|
9
|
+
*/
|
|
10
|
+
export declare function harvestToolCallsFromText(payload: JsonObject): JsonObject;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { normalizeAssistantTextToToolCalls } from '../../shared/text-markup-normalizer.js';
|
|
2
|
+
import { normalizeFunctionCallId } from '../../shared/bridge-id-utils.js';
|
|
3
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
function pickString(...candidates) {
|
|
5
|
+
for (const c of candidates) {
|
|
6
|
+
if (typeof c === 'string' && c.trim().length)
|
|
7
|
+
return c.trim();
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
function extractResponsesMessageText(item) {
|
|
12
|
+
const parts = [];
|
|
13
|
+
const content = Array.isArray(item.content) ? item.content : [];
|
|
14
|
+
for (const part of content) {
|
|
15
|
+
if (!part || typeof part !== 'object' || Array.isArray(part))
|
|
16
|
+
continue;
|
|
17
|
+
const text = pickString(part.text, part.content, part.value);
|
|
18
|
+
if (text)
|
|
19
|
+
parts.push(text);
|
|
20
|
+
}
|
|
21
|
+
if (typeof item.output_text === 'string' && item.output_text.trim().length) {
|
|
22
|
+
parts.push(String(item.output_text));
|
|
23
|
+
}
|
|
24
|
+
return parts.join('\n').trim();
|
|
25
|
+
}
|
|
26
|
+
function harvestToolCallsFromResponsesPayloadInPlace(root) {
|
|
27
|
+
const output = Array.isArray(root.output) ? root.output : [];
|
|
28
|
+
if (!output.length)
|
|
29
|
+
return false;
|
|
30
|
+
let changed = false;
|
|
31
|
+
const nextOutput = [];
|
|
32
|
+
for (const item of output) {
|
|
33
|
+
if (!isRecord(item)) {
|
|
34
|
+
nextOutput.push(item);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const type = typeof item.type === 'string' ? String(item.type).trim().toLowerCase() : '';
|
|
38
|
+
if (type !== 'message') {
|
|
39
|
+
nextOutput.push(item);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const role = typeof item.role === 'string' ? String(item.role).trim().toLowerCase() : 'assistant';
|
|
43
|
+
if (role !== 'assistant') {
|
|
44
|
+
nextOutput.push(item);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const text = extractResponsesMessageText(item);
|
|
48
|
+
if (!text) {
|
|
49
|
+
nextOutput.push(item);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const normalized = normalizeAssistantTextToToolCalls({ role: 'assistant', content: text });
|
|
53
|
+
const toolCalls = Array.isArray(normalized.tool_calls) ? normalized.tool_calls : [];
|
|
54
|
+
if (!toolCalls.length) {
|
|
55
|
+
nextOutput.push(item);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
// Drop the original assistant message item (it usually only contains the tool markup),
|
|
59
|
+
// and append canonical Responses function_call output items.
|
|
60
|
+
changed = true;
|
|
61
|
+
for (const call of toolCalls) {
|
|
62
|
+
if (!call || typeof call !== 'object')
|
|
63
|
+
continue;
|
|
64
|
+
const fn = call.function && typeof call.function === 'object' ? call.function : null;
|
|
65
|
+
const name = typeof fn?.name === 'string' ? String(fn.name).trim() : '';
|
|
66
|
+
if (!name)
|
|
67
|
+
continue;
|
|
68
|
+
const args = typeof fn?.arguments === 'string' ? String(fn.arguments) : '{}';
|
|
69
|
+
const callId = pickString(call.call_id, call.id) ?? `call_${Math.random().toString(36).slice(2, 10)}`;
|
|
70
|
+
const itemId = normalizeFunctionCallId({ callId, fallback: `fc_${callId}` });
|
|
71
|
+
nextOutput.push({
|
|
72
|
+
type: 'function_call',
|
|
73
|
+
id: itemId,
|
|
74
|
+
call_id: callId,
|
|
75
|
+
name,
|
|
76
|
+
arguments: args
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (changed) {
|
|
81
|
+
root.output = nextOutput;
|
|
82
|
+
}
|
|
83
|
+
return changed;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Harvest tool calls from assistant textual markup into OpenAI `tool_calls`.
|
|
87
|
+
*
|
|
88
|
+
* Some upstreams (including certain iFlow/hosted models) emit tool calls as plain text tokens
|
|
89
|
+
* (e.g. `<|tool_call_begin|> ...`) instead of structured `tool_calls`.
|
|
90
|
+
*
|
|
91
|
+
* This action is response-only and provider-scoped via compatibility profiles.
|
|
92
|
+
*/
|
|
93
|
+
export function harvestToolCallsFromText(payload) {
|
|
94
|
+
const root = structuredClone(payload);
|
|
95
|
+
const choices = Array.isArray(root.choices) ? root.choices : [];
|
|
96
|
+
// Responses provider payload: harvest into canonical Responses output items first.
|
|
97
|
+
// This allows the normal semantic mapper (Responses → Chat) to surface tool_calls for servertool orchestration.
|
|
98
|
+
if (!choices.length) {
|
|
99
|
+
harvestToolCallsFromResponsesPayloadInPlace(root);
|
|
100
|
+
return root;
|
|
101
|
+
}
|
|
102
|
+
for (const choice of choices) {
|
|
103
|
+
if (!isRecord(choice))
|
|
104
|
+
continue;
|
|
105
|
+
const message = choice.message;
|
|
106
|
+
if (!isRecord(message))
|
|
107
|
+
continue;
|
|
108
|
+
const normalized = normalizeAssistantTextToToolCalls(message);
|
|
109
|
+
if (normalized !== message) {
|
|
110
|
+
choice.message = normalized;
|
|
111
|
+
const hasToolCalls = Array.isArray(normalized.tool_calls) && normalized.tool_calls.length > 0;
|
|
112
|
+
if (hasToolCalls) {
|
|
113
|
+
const finish = typeof choice.finish_reason === 'string' ? String(choice.finish_reason).trim() : '';
|
|
114
|
+
if (!finish || finish === 'stop') {
|
|
115
|
+
choice.finish_reason = 'tool_calls';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return root;
|
|
121
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { JsonObject } from '../../hub/types/json.js';
|
|
2
|
+
/**
|
|
3
|
+
* iFlow/Kimi request defaults aligned with iflow-cli:
|
|
4
|
+
* - thinking enabled path: temperature=1
|
|
5
|
+
* - thinking disabled path: temperature=0.6
|
|
6
|
+
* - shared defaults: top_p=0.95, n=1, penalties=0, max_new_tokens=max_tokens
|
|
7
|
+
*
|
|
8
|
+
* RouteCodex keeps thinking enabled by default for kimi-k2.5 unless explicitly disabled.
|
|
9
|
+
*/
|
|
10
|
+
export declare function applyIflowKimiCliDefaults(payload: JsonObject): JsonObject;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function normalizeModel(value) {
|
|
5
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
6
|
+
}
|
|
7
|
+
function isKimiK25Model(value) {
|
|
8
|
+
const model = normalizeModel(value);
|
|
9
|
+
return model === 'kimi-k2.5' || model.startsWith('kimi-k2.5-');
|
|
10
|
+
}
|
|
11
|
+
function mirrorMaxTokens(record) {
|
|
12
|
+
const maxTokens = record.max_tokens;
|
|
13
|
+
if (typeof maxTokens === 'number' && Number.isFinite(maxTokens) && maxTokens > 0) {
|
|
14
|
+
record.max_new_tokens = Math.floor(maxTokens);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function isThinkingExplicitlyDisabled(value) {
|
|
18
|
+
if (value === false) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (!isRecord(value)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const enabled = value.enabled;
|
|
25
|
+
if (enabled === false) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const type = typeof value.type === 'string' ? value.type.trim().toLowerCase() : '';
|
|
29
|
+
return type === 'disabled' || type === 'off';
|
|
30
|
+
}
|
|
31
|
+
function normalizeThinkingForKimi(record) {
|
|
32
|
+
const current = record.thinking;
|
|
33
|
+
if (current === undefined || current === null) {
|
|
34
|
+
record.thinking = { type: 'enabled' };
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (current === true) {
|
|
38
|
+
record.thinking = { type: 'enabled' };
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (isThinkingExplicitlyDisabled(current)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (isRecord(current)) {
|
|
45
|
+
if (typeof current.type !== 'string' || current.type.trim().length === 0) {
|
|
46
|
+
current.type = 'enabled';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* iFlow/Kimi request defaults aligned with iflow-cli:
|
|
53
|
+
* - thinking enabled path: temperature=1
|
|
54
|
+
* - thinking disabled path: temperature=0.6
|
|
55
|
+
* - shared defaults: top_p=0.95, n=1, penalties=0, max_new_tokens=max_tokens
|
|
56
|
+
*
|
|
57
|
+
* RouteCodex keeps thinking enabled by default for kimi-k2.5 unless explicitly disabled.
|
|
58
|
+
*/
|
|
59
|
+
export function applyIflowKimiCliDefaults(payload) {
|
|
60
|
+
try {
|
|
61
|
+
if (!isRecord(payload)) {
|
|
62
|
+
return payload;
|
|
63
|
+
}
|
|
64
|
+
const root = structuredClone(payload);
|
|
65
|
+
if (!isKimiK25Model(root.model)) {
|
|
66
|
+
return root;
|
|
67
|
+
}
|
|
68
|
+
const thinkingEnabled = normalizeThinkingForKimi(root);
|
|
69
|
+
root.temperature = thinkingEnabled ? 1 : 0.6;
|
|
70
|
+
root.top_p = 0.95;
|
|
71
|
+
root.n = 1;
|
|
72
|
+
root.presence_penalty = 0;
|
|
73
|
+
root.frequency_penalty = 0;
|
|
74
|
+
mirrorMaxTokens(root);
|
|
75
|
+
return root;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return payload;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JsonObject } from '../../hub/types/json.js';
|
|
2
|
+
/**
|
|
3
|
+
* iFlow/Kimi multimodal compat:
|
|
4
|
+
* - Preserve latest user turn media payload.
|
|
5
|
+
* - Replace historical inline-base64 image/video parts with text placeholders.
|
|
6
|
+
*/
|
|
7
|
+
export declare function applyIflowKimiHistoryMediaPlaceholder(payload: JsonObject): JsonObject;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function detectMediaKind(part) {
|
|
5
|
+
const typeValue = typeof part.type === 'string' ? part.type.trim().toLowerCase() : '';
|
|
6
|
+
if (!typeValue) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
if (typeValue.includes('video')) {
|
|
10
|
+
return 'video';
|
|
11
|
+
}
|
|
12
|
+
if (typeValue.includes('image')) {
|
|
13
|
+
return 'image';
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function isInlineBase64(value) {
|
|
18
|
+
const normalized = value.trim().toLowerCase();
|
|
19
|
+
if (!normalized) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (normalized.startsWith('data:') && normalized.includes(';base64,')) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (normalized.startsWith('base64,')) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function mediaPartCarriesInlineBase64(part) {
|
|
31
|
+
const candidates = [];
|
|
32
|
+
const imageUrl = part.image_url;
|
|
33
|
+
const videoUrl = part.video_url;
|
|
34
|
+
if (typeof imageUrl === 'string') {
|
|
35
|
+
candidates.push(imageUrl);
|
|
36
|
+
}
|
|
37
|
+
if (typeof videoUrl === 'string') {
|
|
38
|
+
candidates.push(videoUrl);
|
|
39
|
+
}
|
|
40
|
+
if (isRecord(imageUrl)) {
|
|
41
|
+
const url = typeof imageUrl.url === 'string' ? imageUrl.url : '';
|
|
42
|
+
const data = typeof imageUrl.data === 'string' ? String(imageUrl.data) : '';
|
|
43
|
+
const base64 = typeof imageUrl.base64 === 'string'
|
|
44
|
+
? String(imageUrl.base64)
|
|
45
|
+
: '';
|
|
46
|
+
if (url)
|
|
47
|
+
candidates.push(url);
|
|
48
|
+
if (data)
|
|
49
|
+
candidates.push(data);
|
|
50
|
+
if (base64)
|
|
51
|
+
candidates.push(`base64,${base64}`);
|
|
52
|
+
}
|
|
53
|
+
if (isRecord(videoUrl)) {
|
|
54
|
+
const url = typeof videoUrl.url === 'string' ? videoUrl.url : '';
|
|
55
|
+
const data = typeof videoUrl.data === 'string' ? String(videoUrl.data) : '';
|
|
56
|
+
const base64 = typeof videoUrl.base64 === 'string'
|
|
57
|
+
? String(videoUrl.base64)
|
|
58
|
+
: '';
|
|
59
|
+
if (url)
|
|
60
|
+
candidates.push(url);
|
|
61
|
+
if (data)
|
|
62
|
+
candidates.push(data);
|
|
63
|
+
if (base64)
|
|
64
|
+
candidates.push(`base64,${base64}`);
|
|
65
|
+
}
|
|
66
|
+
const url = typeof part.url === 'string' ? String(part.url) : '';
|
|
67
|
+
const uri = typeof part.uri === 'string' ? String(part.uri) : '';
|
|
68
|
+
const data = typeof part.data === 'string' ? String(part.data) : '';
|
|
69
|
+
const base64 = typeof part.base64 === 'string' ? String(part.base64) : '';
|
|
70
|
+
if (url)
|
|
71
|
+
candidates.push(url);
|
|
72
|
+
if (uri)
|
|
73
|
+
candidates.push(uri);
|
|
74
|
+
if (data)
|
|
75
|
+
candidates.push(data);
|
|
76
|
+
if (base64)
|
|
77
|
+
candidates.push(`base64,${base64}`);
|
|
78
|
+
return candidates.some((value) => isInlineBase64(value));
|
|
79
|
+
}
|
|
80
|
+
function buildPlaceholderPart(kind) {
|
|
81
|
+
return {
|
|
82
|
+
type: 'text',
|
|
83
|
+
text: kind === 'video' ? '[history_video_base64_omitted]' : '[history_image_base64_omitted]'
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* iFlow/Kimi multimodal compat:
|
|
88
|
+
* - Preserve latest user turn media payload.
|
|
89
|
+
* - Replace historical inline-base64 image/video parts with text placeholders.
|
|
90
|
+
*/
|
|
91
|
+
export function applyIflowKimiHistoryMediaPlaceholder(payload) {
|
|
92
|
+
try {
|
|
93
|
+
if (!isRecord(payload)) {
|
|
94
|
+
return payload;
|
|
95
|
+
}
|
|
96
|
+
const root = structuredClone(payload);
|
|
97
|
+
const model = typeof root.model === 'string' ? root.model.trim().toLowerCase() : '';
|
|
98
|
+
if (model !== 'kimi-k2.5') {
|
|
99
|
+
return root;
|
|
100
|
+
}
|
|
101
|
+
const messagesValue = root.messages;
|
|
102
|
+
if (!Array.isArray(messagesValue) || !messagesValue.length) {
|
|
103
|
+
return root;
|
|
104
|
+
}
|
|
105
|
+
const messages = messagesValue.map((item) => (isRecord(item) ? item : null));
|
|
106
|
+
let latestUserIndex = -1;
|
|
107
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
108
|
+
const message = messages[index];
|
|
109
|
+
if (!message)
|
|
110
|
+
continue;
|
|
111
|
+
const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
|
|
112
|
+
if (role === 'user') {
|
|
113
|
+
latestUserIndex = index;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (latestUserIndex <= 0) {
|
|
118
|
+
return root;
|
|
119
|
+
}
|
|
120
|
+
const nextMessages = [];
|
|
121
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
122
|
+
const message = messages[index];
|
|
123
|
+
if (!message)
|
|
124
|
+
continue;
|
|
125
|
+
if (index >= latestUserIndex) {
|
|
126
|
+
nextMessages.push(message);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const contentValue = message.content;
|
|
130
|
+
if (!Array.isArray(contentValue)) {
|
|
131
|
+
nextMessages.push(message);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
let changed = false;
|
|
135
|
+
const nextContent = contentValue.map((part) => {
|
|
136
|
+
if (!isRecord(part)) {
|
|
137
|
+
return part;
|
|
138
|
+
}
|
|
139
|
+
const mediaKind = detectMediaKind(part);
|
|
140
|
+
if (!mediaKind) {
|
|
141
|
+
return part;
|
|
142
|
+
}
|
|
143
|
+
if (!mediaPartCarriesInlineBase64(part)) {
|
|
144
|
+
return part;
|
|
145
|
+
}
|
|
146
|
+
changed = true;
|
|
147
|
+
return buildPlaceholderPart(mediaKind);
|
|
148
|
+
});
|
|
149
|
+
if (!changed) {
|
|
150
|
+
nextMessages.push(message);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
nextMessages.push({ ...message, content: nextContent });
|
|
154
|
+
}
|
|
155
|
+
root.messages = nextMessages;
|
|
156
|
+
return root;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return payload;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { JsonObject } from '../../hub/types/json.js';
|
|
2
|
+
/**
|
|
3
|
+
* iFlow/Kimi compat:
|
|
4
|
+
* When thinking is active, iFlow requires reasoning_content to exist (and be non-empty)
|
|
5
|
+
* on assistant tool-call messages. Some tool-call surfaces omit it, causing 400 validation errors.
|
|
6
|
+
*
|
|
7
|
+
* We only apply this for kimi-k2.5 to avoid affecting other models.
|
|
8
|
+
*
|
|
9
|
+
* Note: responses→chat bridging may omit top-level thinking while upstream Kimi still
|
|
10
|
+
* enforces this contract. So for kimi-k2.5 we inject unless thinking is explicitly disabled.
|
|
11
|
+
*/
|
|
12
|
+
export declare function fillIflowKimiThinkingReasoningContent(payload: JsonObject): JsonObject;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function hasNonEmptyArray(value) {
|
|
5
|
+
return Array.isArray(value) && value.length > 0;
|
|
6
|
+
}
|
|
7
|
+
function isThinkingDisabled(value) {
|
|
8
|
+
if (value === false)
|
|
9
|
+
return true;
|
|
10
|
+
if (!isRecord(value))
|
|
11
|
+
return false;
|
|
12
|
+
const enabled = value.enabled;
|
|
13
|
+
if (enabled === false)
|
|
14
|
+
return true;
|
|
15
|
+
const type = typeof value.type === 'string' ? String(value.type).trim().toLowerCase() : '';
|
|
16
|
+
if (type === 'disabled' || type === 'off')
|
|
17
|
+
return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* iFlow/Kimi compat:
|
|
22
|
+
* When thinking is active, iFlow requires reasoning_content to exist (and be non-empty)
|
|
23
|
+
* on assistant tool-call messages. Some tool-call surfaces omit it, causing 400 validation errors.
|
|
24
|
+
*
|
|
25
|
+
* We only apply this for kimi-k2.5 to avoid affecting other models.
|
|
26
|
+
*
|
|
27
|
+
* Note: responses→chat bridging may omit top-level thinking while upstream Kimi still
|
|
28
|
+
* enforces this contract. So for kimi-k2.5 we inject unless thinking is explicitly disabled.
|
|
29
|
+
*/
|
|
30
|
+
export function fillIflowKimiThinkingReasoningContent(payload) {
|
|
31
|
+
try {
|
|
32
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
33
|
+
return payload;
|
|
34
|
+
}
|
|
35
|
+
const root = structuredClone(payload);
|
|
36
|
+
const model = typeof root.model === 'string' ? root.model.trim().toLowerCase() : '';
|
|
37
|
+
if (!(model === 'kimi-k2.5' || model.startsWith('kimi-k2.5-'))) {
|
|
38
|
+
return root;
|
|
39
|
+
}
|
|
40
|
+
if (isThinkingDisabled(root.thinking)) {
|
|
41
|
+
return root;
|
|
42
|
+
}
|
|
43
|
+
const messages = Array.isArray(root.messages) ? root.messages : [];
|
|
44
|
+
if (!messages.length) {
|
|
45
|
+
return root;
|
|
46
|
+
}
|
|
47
|
+
for (const msg of messages) {
|
|
48
|
+
if (!isRecord(msg))
|
|
49
|
+
continue;
|
|
50
|
+
const role = typeof msg.role === 'string' ? msg.role.trim().toLowerCase() : '';
|
|
51
|
+
if (role !== 'assistant')
|
|
52
|
+
continue;
|
|
53
|
+
if (!hasNonEmptyArray(msg.tool_calls))
|
|
54
|
+
continue;
|
|
55
|
+
const rc = msg.reasoning_content;
|
|
56
|
+
const rcText = typeof rc === 'string' ? rc : '';
|
|
57
|
+
if (!rcText || !rcText.trim()) {
|
|
58
|
+
msg.reasoning_content = '.';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
root.messages = messages;
|
|
62
|
+
return root;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return payload;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { JsonObject } from '../../hub/types/json.js';
|
|
2
|
+
/**
|
|
3
|
+
* iFlow compatibility: some iFlow backends wrap OpenAI-compatible payloads inside:
|
|
4
|
+
* { status, msg, body, request_id }
|
|
5
|
+
* where `body` is the actual OpenAI-chat/Responses JSON (or a JSON string).
|
|
6
|
+
*
|
|
7
|
+
* This action unwraps `body` so hub semantic mapping and tool harvesting can proceed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function unwrapIflowResponseBodyEnvelope(payload: JsonObject): JsonObject;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
2
|
+
function looksLikeKnownProviderResponseShape(value) {
|
|
3
|
+
if (!isRecord(value))
|
|
4
|
+
return false;
|
|
5
|
+
if (Array.isArray(value.choices))
|
|
6
|
+
return true; // openai-chat
|
|
7
|
+
if (Array.isArray(value.output) || value.object === 'response')
|
|
8
|
+
return true; // openai-responses
|
|
9
|
+
if (typeof value.type === 'string' && String(value.type).toLowerCase() === 'message' && Array.isArray(value.content)) {
|
|
10
|
+
return true; // anthropic-messages
|
|
11
|
+
}
|
|
12
|
+
if (Array.isArray(value.candidates))
|
|
13
|
+
return true; // gemini-chat
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
function stripJsonTextPrefix(text) {
|
|
17
|
+
let out = String(text || '').trimStart();
|
|
18
|
+
// anti-XSSI prefix: ")]}',\n{...}"
|
|
19
|
+
out = out.replace(/^\)\]\}',?\s*/u, '');
|
|
20
|
+
// single-line SSE-like wrapper sometimes returned as plain text
|
|
21
|
+
out = out.replace(/^data:\s*/iu, '');
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function tryParseJsonRecord(text) {
|
|
25
|
+
try {
|
|
26
|
+
const trimmed = stripJsonTextPrefix(text).trim();
|
|
27
|
+
if (!trimmed)
|
|
28
|
+
return null;
|
|
29
|
+
if (trimmed.length > 10 * 1024 * 1024)
|
|
30
|
+
return null;
|
|
31
|
+
const first = trimmed.charAt(0);
|
|
32
|
+
const last = trimmed.charAt(trimmed.length - 1);
|
|
33
|
+
if ((first !== '{' && first !== '[') || (last !== '}' && last !== ']')) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const parsed = JSON.parse(trimmed);
|
|
37
|
+
return isRecord(parsed) ? parsed : null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function tryParseJsonRecordFromMaybeSseText(text) {
|
|
44
|
+
if (typeof text !== 'string') {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const raw = stripJsonTextPrefix(text);
|
|
48
|
+
const direct = tryParseJsonRecord(raw);
|
|
49
|
+
if (direct) {
|
|
50
|
+
return direct;
|
|
51
|
+
}
|
|
52
|
+
// Multi-line SSE text occasionally shows up as a plain string inside iFlow envelopes.
|
|
53
|
+
// Best-effort: parse the last valid `data:` JSON object chunk.
|
|
54
|
+
if (raw.length > 10 * 1024 * 1024) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const lines = raw.split(/\r?\n/u);
|
|
58
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
59
|
+
const line = lines[i]?.trim();
|
|
60
|
+
if (!line)
|
|
61
|
+
continue;
|
|
62
|
+
const lower = line.toLowerCase();
|
|
63
|
+
if (lower === 'data: [done]' || lower === '[done]')
|
|
64
|
+
continue;
|
|
65
|
+
const candidate = lower.startsWith('data:') ? line.slice(5).trim() : line;
|
|
66
|
+
const parsed = tryParseJsonRecord(candidate);
|
|
67
|
+
if (parsed) {
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function tryUnwrapKnownShapeFromBody(value, depth) {
|
|
74
|
+
if (depth < 0)
|
|
75
|
+
return null;
|
|
76
|
+
if (looksLikeKnownProviderResponseShape(value)) {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
if (isRecord(value)) {
|
|
80
|
+
// nested body/data wrappers
|
|
81
|
+
for (const key of ['data', 'body', 'response', 'payload', 'result']) {
|
|
82
|
+
const nested = value[key];
|
|
83
|
+
const unwrapped = tryUnwrapKnownShapeFromBody(nested, depth - 1);
|
|
84
|
+
if (unwrapped)
|
|
85
|
+
return unwrapped;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (typeof value === 'string') {
|
|
90
|
+
const parsed = tryParseJsonRecord(value) ?? tryParseJsonRecordFromMaybeSseText(value);
|
|
91
|
+
if (!parsed)
|
|
92
|
+
return null;
|
|
93
|
+
return tryUnwrapKnownShapeFromBody(parsed, depth - 1);
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* iFlow compatibility: some iFlow backends wrap OpenAI-compatible payloads inside:
|
|
99
|
+
* { status, msg, body, request_id }
|
|
100
|
+
* where `body` is the actual OpenAI-chat/Responses JSON (or a JSON string).
|
|
101
|
+
*
|
|
102
|
+
* This action unwraps `body` so hub semantic mapping and tool harvesting can proceed.
|
|
103
|
+
*/
|
|
104
|
+
export function unwrapIflowResponseBodyEnvelope(payload) {
|
|
105
|
+
try {
|
|
106
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
107
|
+
return payload;
|
|
108
|
+
}
|
|
109
|
+
const root = structuredClone(payload);
|
|
110
|
+
// Only unwrap when it looks like the iFlow envelope.
|
|
111
|
+
if (!('body' in root) || !('status' in root) || !('msg' in root)) {
|
|
112
|
+
return root;
|
|
113
|
+
}
|
|
114
|
+
// If the root is already a known provider response, don't touch it.
|
|
115
|
+
if (looksLikeKnownProviderResponseShape(root)) {
|
|
116
|
+
return root;
|
|
117
|
+
}
|
|
118
|
+
const body = root.body;
|
|
119
|
+
const unwrapped = tryUnwrapKnownShapeFromBody(body, 6);
|
|
120
|
+
if (unwrapped) {
|
|
121
|
+
return unwrapped;
|
|
122
|
+
}
|
|
123
|
+
// Fallback: unwrap to `body` even when we can't yet recognize the nested shape.
|
|
124
|
+
// This avoids treating the iFlow envelope as the provider payload and improves
|
|
125
|
+
// downstream diagnostics / error handling.
|
|
126
|
+
if (isRecord(body)) {
|
|
127
|
+
return body;
|
|
128
|
+
}
|
|
129
|
+
if (typeof body === 'string') {
|
|
130
|
+
const parsed = tryParseJsonRecordFromMaybeSseText(body);
|
|
131
|
+
if (parsed) {
|
|
132
|
+
return parsed;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return root;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return payload;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { JsonObject } from '../../hub/types/json.js';
|
|
2
|
+
/**
|
|
3
|
+
* LM Studio compat:
|
|
4
|
+
* Some LM Studio OpenAI-Responses endpoints accept tool-call items but are picky about ids:
|
|
5
|
+
* - `call_id` should remain `call_*` (used to link tool outputs)
|
|
6
|
+
* - `id` for `function_call` / `function_call_output` should be `fc_*` (OpenAI Responses convention)
|
|
7
|
+
*
|
|
8
|
+
* We apply this in compat (profile: chat:lmstudio) to keep provider layer transport-only.
|
|
9
|
+
*/
|
|
10
|
+
export declare function enforceLmstudioResponsesFcToolCallIds(payload: JsonObject): JsonObject;
|