@jsonstudio/llms 0.6.230 → 0.6.467
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/README.md +2 -0
- package/dist/conversion/codecs/gemini-openai-codec.js +24 -2
- package/dist/conversion/compat/actions/gemini-web-search.d.ts +17 -0
- package/dist/conversion/compat/actions/gemini-web-search.js +68 -0
- package/dist/conversion/compat/actions/glm-image-content.d.ts +2 -0
- package/dist/conversion/compat/actions/glm-image-content.js +83 -0
- package/dist/conversion/compat/actions/glm-vision-prompt.d.ts +11 -0
- package/dist/conversion/compat/actions/glm-vision-prompt.js +177 -0
- package/dist/conversion/compat/actions/glm-web-search.js +25 -28
- package/dist/conversion/compat/actions/iflow-web-search.d.ts +18 -0
- package/dist/conversion/compat/actions/iflow-web-search.js +87 -0
- package/dist/conversion/compat/actions/universal-shape-filter.js +11 -0
- package/dist/conversion/compat/profiles/chat-gemini.json +17 -0
- package/dist/conversion/compat/profiles/chat-glm.json +194 -184
- package/dist/conversion/compat/profiles/chat-iflow.json +199 -195
- package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
- package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
- package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
- package/dist/conversion/config/sample-config.json +1 -1
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +24 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +32 -1
- package/dist/conversion/hub/pipeline/session-identifiers.d.ts +9 -0
- package/dist/conversion/hub/pipeline/session-identifiers.js +76 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +31 -2
- package/dist/conversion/hub/pipeline/target-utils.js +6 -0
- package/dist/conversion/hub/process/chat-process.js +186 -40
- package/dist/conversion/hub/response/provider-response.d.ts +13 -1
- package/dist/conversion/hub/response/provider-response.js +84 -35
- package/dist/conversion/hub/response/server-side-tools.js +61 -4
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +123 -3
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +17 -1
- package/dist/conversion/hub/standardized-bridge.js +14 -0
- package/dist/conversion/responses/responses-openai-bridge.js +110 -6
- package/dist/conversion/shared/anthropic-message-utils.js +133 -9
- package/dist/conversion/shared/bridge-message-utils.js +137 -10
- package/dist/conversion/shared/errors.d.ts +20 -0
- package/dist/conversion/shared/errors.js +28 -0
- package/dist/conversion/shared/responses-conversation-store.js +30 -3
- package/dist/conversion/shared/responses-output-builder.js +111 -8
- package/dist/conversion/shared/tool-filter-pipeline.js +1 -0
- package/dist/filters/special/request-toolcalls-stringify.d.ts +13 -0
- package/dist/filters/special/request-toolcalls-stringify.js +103 -3
- package/dist/filters/special/response-tool-text-canonicalize.d.ts +16 -0
- package/dist/filters/special/response-tool-text-canonicalize.js +27 -3
- package/dist/router/virtual-router/bootstrap.js +44 -12
- package/dist/router/virtual-router/classifier.js +13 -17
- package/dist/router/virtual-router/engine.d.ts +39 -0
- package/dist/router/virtual-router/engine.js +755 -55
- package/dist/router/virtual-router/features.js +1 -1
- package/dist/router/virtual-router/message-utils.js +36 -24
- package/dist/router/virtual-router/provider-registry.d.ts +15 -0
- package/dist/router/virtual-router/provider-registry.js +42 -1
- package/dist/router/virtual-router/routing-instructions.d.ts +34 -0
- package/dist/router/virtual-router/routing-instructions.js +383 -0
- package/dist/router/virtual-router/sticky-session-store.d.ts +3 -0
- package/dist/router/virtual-router/sticky-session-store.js +110 -0
- package/dist/router/virtual-router/token-counter.js +14 -3
- package/dist/router/virtual-router/tool-signals.js +0 -22
- package/dist/router/virtual-router/types.d.ts +80 -0
- package/dist/router/virtual-router/types.js +2 -1
- package/dist/servertool/engine.d.ts +27 -0
- package/dist/servertool/engine.js +101 -0
- package/dist/servertool/flow-types.d.ts +40 -0
- package/dist/servertool/flow-types.js +1 -0
- package/dist/servertool/handlers/vision.d.ts +1 -0
- package/dist/servertool/handlers/vision.js +194 -0
- package/dist/servertool/handlers/web-search.d.ts +1 -0
- package/dist/servertool/handlers/web-search.js +791 -0
- package/dist/servertool/orchestration-types.d.ts +33 -0
- package/dist/servertool/orchestration-types.js +1 -0
- package/dist/servertool/registry.d.ts +18 -0
- package/dist/servertool/registry.js +27 -0
- package/dist/servertool/server-side-tools.d.ts +8 -0
- package/dist/servertool/server-side-tools.js +208 -0
- package/dist/servertool/types.d.ts +94 -0
- package/dist/servertool/types.js +1 -0
- package/dist/servertool/vision-tool.d.ts +2 -0
- package/dist/servertool/vision-tool.js +185 -0
- package/dist/sse/sse-to-json/builders/response-builder.js +6 -3
- package/package.json +1 -1
|
@@ -78,11 +78,22 @@ function encodeContent(content, encoder) {
|
|
|
78
78
|
total += encodeText(part, encoder);
|
|
79
79
|
}
|
|
80
80
|
else if (part && typeof part === 'object') {
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
const record = part;
|
|
82
|
+
const typeValue = typeof record.type === 'string' ? record.type.toLowerCase() : '';
|
|
83
|
+
// Large binary/image payloads (data URIs, base64, etc.) should not
|
|
84
|
+
// dominate context estimation. For image-like blocks, only count a
|
|
85
|
+
// small textual placeholder instead of the full JSON/body.
|
|
86
|
+
if (typeValue.startsWith('image')) {
|
|
87
|
+
const caption = typeof record.caption === 'string' && record.caption.trim().length
|
|
88
|
+
? record.caption
|
|
89
|
+
: '[image]';
|
|
90
|
+
total += encodeText(caption, encoder);
|
|
91
|
+
}
|
|
92
|
+
else if (typeof record.text === 'string') {
|
|
93
|
+
total += encodeText(record.text, encoder);
|
|
83
94
|
}
|
|
84
95
|
else {
|
|
85
|
-
total += encodeText(JSON.stringify(
|
|
96
|
+
total += encodeText(JSON.stringify(record), encoder);
|
|
86
97
|
}
|
|
87
98
|
}
|
|
88
99
|
}
|
|
@@ -85,22 +85,6 @@ const SHELL_WRITE_PATTERNS = [
|
|
|
85
85
|
'go install',
|
|
86
86
|
'make install'
|
|
87
87
|
];
|
|
88
|
-
const SHELL_SEARCH_PATTERNS = [
|
|
89
|
-
'rg ',
|
|
90
|
-
'rg-',
|
|
91
|
-
'grep ',
|
|
92
|
-
'grep-',
|
|
93
|
-
'ripgrep',
|
|
94
|
-
'find ',
|
|
95
|
-
'fd ',
|
|
96
|
-
'locate ',
|
|
97
|
-
'search ',
|
|
98
|
-
'ack ',
|
|
99
|
-
'ag ',
|
|
100
|
-
'where ',
|
|
101
|
-
'which ',
|
|
102
|
-
'codesearch'
|
|
103
|
-
];
|
|
104
88
|
const SHELL_READ_PATTERNS = [
|
|
105
89
|
'ls',
|
|
106
90
|
'dir ',
|
|
@@ -362,9 +346,6 @@ function classifyShellCommand(command) {
|
|
|
362
346
|
if (segments.some((segment) => matchesAnyPattern(segment, SHELL_WRITE_PATTERNS))) {
|
|
363
347
|
return 'write';
|
|
364
348
|
}
|
|
365
|
-
if (segments.some((segment) => matchesAnyPattern(segment, SHELL_SEARCH_PATTERNS))) {
|
|
366
|
-
return 'search';
|
|
367
|
-
}
|
|
368
349
|
if (segments.some((segment) => matchesAnyPattern(segment, SHELL_READ_PATTERNS))) {
|
|
369
350
|
return 'read';
|
|
370
351
|
}
|
|
@@ -372,9 +353,6 @@ function classifyShellCommand(command) {
|
|
|
372
353
|
if (matchesAnyPattern(stripped, SHELL_WRITE_PATTERNS)) {
|
|
373
354
|
return 'write';
|
|
374
355
|
}
|
|
375
|
-
if (matchesAnyPattern(stripped, SHELL_SEARCH_PATTERNS)) {
|
|
376
|
-
return 'search';
|
|
377
|
-
}
|
|
378
356
|
if (matchesAnyPattern(stripped, SHELL_READ_PATTERNS)) {
|
|
379
357
|
return 'read';
|
|
380
358
|
}
|
|
@@ -5,11 +5,19 @@ import type { StandardizedRequest } from '../../conversion/hub/types/standardize
|
|
|
5
5
|
export declare const DEFAULT_MODEL_CONTEXT_TOKENS = 200000;
|
|
6
6
|
export declare const DEFAULT_ROUTE = "default";
|
|
7
7
|
export declare const ROUTE_PRIORITY: string[];
|
|
8
|
+
export type RoutingInstructionMode = 'force' | 'sticky' | 'none';
|
|
8
9
|
export interface RoutePoolTier {
|
|
9
10
|
id: string;
|
|
10
11
|
targets: string[];
|
|
11
12
|
priority: number;
|
|
12
13
|
backup?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Optional force flag for this route pool.
|
|
16
|
+
* Currently interpreted for:
|
|
17
|
+
* - routing.vision: force dedicated vision backend handling.
|
|
18
|
+
* - routing.web_search / routing.search: force server-side web_search flow.
|
|
19
|
+
*/
|
|
20
|
+
force?: boolean;
|
|
13
21
|
}
|
|
14
22
|
export type RoutingPools = Record<string, RoutePoolTier[]>;
|
|
15
23
|
export type StreamingPreference = 'auto' | 'always' | 'never';
|
|
@@ -42,6 +50,13 @@ export interface ProviderProfile {
|
|
|
42
50
|
responsesConfig?: ResponsesProviderConfig;
|
|
43
51
|
streaming?: StreamingPreference;
|
|
44
52
|
maxContextTokens?: number;
|
|
53
|
+
/**
|
|
54
|
+
* When true, this provider must be skipped for any request that
|
|
55
|
+
* requires server-side tool orchestration (e.g. web_search).
|
|
56
|
+
* Normal chat routing (without servertool injection) may still
|
|
57
|
+
* use this provider as usual.
|
|
58
|
+
*/
|
|
59
|
+
serverToolsDisabled?: boolean;
|
|
45
60
|
}
|
|
46
61
|
export interface ProviderRuntimeProfile {
|
|
47
62
|
runtimeKey: string;
|
|
@@ -61,6 +76,12 @@ export interface ProviderRuntimeProfile {
|
|
|
61
76
|
modelContextTokens?: Record<string, number>;
|
|
62
77
|
defaultContextTokens?: number;
|
|
63
78
|
maxContextTokens?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Provider-level flag propagated from virtualrouter.providers[*].
|
|
81
|
+
* When true, VirtualRouterEngine will skip this runtime for any
|
|
82
|
+
* request that declares serverToolRequired=true in routing metadata.
|
|
83
|
+
*/
|
|
84
|
+
serverToolsDisabled?: boolean;
|
|
64
85
|
}
|
|
65
86
|
export interface VirtualRouterClassifierConfig {
|
|
66
87
|
longContextThresholdTokens?: number;
|
|
@@ -83,10 +104,21 @@ export interface VirtualRouterWebSearchEngineConfig {
|
|
|
83
104
|
providerKey: string;
|
|
84
105
|
description?: string;
|
|
85
106
|
default?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* When true, this engine will never be used by server-side tools
|
|
109
|
+
* (e.g. web_search). It will also be omitted from injected tool
|
|
110
|
+
* schemas so main models cannot select it for servertool flows.
|
|
111
|
+
*/
|
|
112
|
+
serverToolsDisabled?: boolean;
|
|
86
113
|
}
|
|
87
114
|
export interface VirtualRouterWebSearchConfig {
|
|
88
115
|
engines: VirtualRouterWebSearchEngineConfig[];
|
|
89
116
|
injectPolicy?: 'always' | 'selective';
|
|
117
|
+
/**
|
|
118
|
+
* When true, always prefer server-side web_search orchestration
|
|
119
|
+
* over upstream builtin behaviours (e.g. OpenAI Responses builtin web_search).
|
|
120
|
+
*/
|
|
121
|
+
force?: boolean;
|
|
90
122
|
}
|
|
91
123
|
export interface VirtualRouterConfig {
|
|
92
124
|
routing: RoutingPools;
|
|
@@ -129,6 +161,47 @@ export interface RouterMetadataInput {
|
|
|
129
161
|
providerProtocol?: string;
|
|
130
162
|
stage?: 'inbound' | 'outbound' | 'response';
|
|
131
163
|
routeHint?: string;
|
|
164
|
+
/**
|
|
165
|
+
* Indicates that current routing decision is for a request which
|
|
166
|
+
* expects server-side tools orchestration (e.g. web_search).
|
|
167
|
+
* Virtual Router should skip providers that opt out via
|
|
168
|
+
* serverToolsDisabled when this flag is true.
|
|
169
|
+
*/
|
|
170
|
+
serverToolRequired?: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* 强制路由模式,从消息中的 <**...**> 指令解析得出
|
|
173
|
+
*/
|
|
174
|
+
routingMode?: RoutingInstructionMode;
|
|
175
|
+
/**
|
|
176
|
+
* 允许的 provider 白名单
|
|
177
|
+
*/
|
|
178
|
+
allowedProviders?: string[];
|
|
179
|
+
/**
|
|
180
|
+
* 强制使用的 provider model (格式: provider.model)
|
|
181
|
+
*/
|
|
182
|
+
forcedProviderModel?: string;
|
|
183
|
+
/**
|
|
184
|
+
* 强制使用的 provider keyAlias
|
|
185
|
+
*/
|
|
186
|
+
forcedProviderKeyAlias?: string;
|
|
187
|
+
/**
|
|
188
|
+
* 强制使用的 provider keyIndex (从 1 开始)
|
|
189
|
+
*/
|
|
190
|
+
forcedProviderKeyIndex?: number;
|
|
191
|
+
/**
|
|
192
|
+
* 禁用的 provider model 列表
|
|
193
|
+
*/
|
|
194
|
+
disabledProviderModels?: string[];
|
|
195
|
+
/**
|
|
196
|
+
* 禁用的 provider keyAlias 列表
|
|
197
|
+
*/
|
|
198
|
+
disabledProviderKeyAliases?: string[];
|
|
199
|
+
/**
|
|
200
|
+
* 禁用的 provider keyIndex 列表 (从 1 开始)
|
|
201
|
+
*/
|
|
202
|
+
disabledProviderKeyIndexes?: number[];
|
|
203
|
+
sessionId?: string;
|
|
204
|
+
conversationId?: string;
|
|
132
205
|
responsesResume?: {
|
|
133
206
|
previousRequestId?: string;
|
|
134
207
|
restoredFromResponseId?: string;
|
|
@@ -182,6 +255,13 @@ export interface TargetMetadata {
|
|
|
182
255
|
responsesConfig?: ResponsesProviderConfig;
|
|
183
256
|
streaming?: StreamingPreference;
|
|
184
257
|
maxContextTokens?: number;
|
|
258
|
+
/**
|
|
259
|
+
* Route-level flags propagated from the virtual router.
|
|
260
|
+
* These are derived from routing pools and webSearch config and
|
|
261
|
+
* are used by hub pipeline/process layers (web_search / vision).
|
|
262
|
+
*/
|
|
263
|
+
forceWebSearch?: boolean;
|
|
264
|
+
forceVision?: boolean;
|
|
185
265
|
}
|
|
186
266
|
export interface ResponsesProviderConfig {
|
|
187
267
|
toolCallIdStyle?: 'fc' | 'preserve';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AdapterContext } from '../conversion/hub/types/chat-envelope.js';
|
|
2
|
+
import type { JsonObject } from '../conversion/hub/types/json.js';
|
|
3
|
+
import type { ProviderInvoker } from './types.js';
|
|
4
|
+
export interface ServerToolOrchestrationOptions {
|
|
5
|
+
chat: JsonObject;
|
|
6
|
+
adapterContext: AdapterContext;
|
|
7
|
+
requestId: string;
|
|
8
|
+
entryEndpoint: string;
|
|
9
|
+
providerProtocol: string;
|
|
10
|
+
reenterPipeline?: (options: {
|
|
11
|
+
entryEndpoint: string;
|
|
12
|
+
requestId: string;
|
|
13
|
+
body: JsonObject;
|
|
14
|
+
metadata?: JsonObject;
|
|
15
|
+
}) => Promise<{
|
|
16
|
+
body?: JsonObject;
|
|
17
|
+
__sse_responses?: unknown;
|
|
18
|
+
format?: string;
|
|
19
|
+
}>;
|
|
20
|
+
providerInvoker?: ProviderInvoker;
|
|
21
|
+
}
|
|
22
|
+
export interface ServerToolOrchestrationResult {
|
|
23
|
+
chat: JsonObject;
|
|
24
|
+
executed: boolean;
|
|
25
|
+
flowId?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function runServerToolOrchestration(options: ServerToolOrchestrationOptions): Promise<ServerToolOrchestrationResult>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { runServerSideToolEngine } from './server-side-tools.js';
|
|
2
|
+
export async function runServerToolOrchestration(options) {
|
|
3
|
+
const engineOptions = {
|
|
4
|
+
chatResponse: options.chat,
|
|
5
|
+
adapterContext: options.adapterContext,
|
|
6
|
+
entryEndpoint: options.entryEndpoint,
|
|
7
|
+
requestId: options.requestId,
|
|
8
|
+
providerProtocol: options.providerProtocol,
|
|
9
|
+
providerInvoker: options.providerInvoker,
|
|
10
|
+
reenterPipeline: options.reenterPipeline
|
|
11
|
+
};
|
|
12
|
+
const engineResult = await runServerSideToolEngine(engineOptions);
|
|
13
|
+
if (engineResult.mode === 'passthrough' || !engineResult.execution) {
|
|
14
|
+
return {
|
|
15
|
+
chat: engineResult.finalChatResponse,
|
|
16
|
+
executed: false
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (!engineResult.execution.followup || !options.reenterPipeline) {
|
|
20
|
+
return {
|
|
21
|
+
chat: engineResult.finalChatResponse,
|
|
22
|
+
executed: true,
|
|
23
|
+
flowId: engineResult.execution.flowId
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const routeHint = resolveRouteHint(options.adapterContext, engineResult.execution.flowId);
|
|
27
|
+
const metadata = {
|
|
28
|
+
serverToolFollowup: true,
|
|
29
|
+
stream: false,
|
|
30
|
+
...(engineResult.execution.followup.metadata ?? {})
|
|
31
|
+
};
|
|
32
|
+
if (routeHint && typeof metadata.routeHint !== 'string') {
|
|
33
|
+
metadata.routeHint = routeHint;
|
|
34
|
+
}
|
|
35
|
+
const followup = await options.reenterPipeline({
|
|
36
|
+
entryEndpoint: '/v1/chat/completions',
|
|
37
|
+
requestId: `${options.requestId}${engineResult.execution.followup.requestIdSuffix}`,
|
|
38
|
+
body: engineResult.execution.followup.payload,
|
|
39
|
+
metadata
|
|
40
|
+
});
|
|
41
|
+
const followupBody = followup.body && typeof followup.body === 'object'
|
|
42
|
+
? followup.body
|
|
43
|
+
: engineResult.finalChatResponse;
|
|
44
|
+
const decorated = decorateFinalChatWithServerToolContext(followupBody, engineResult.execution);
|
|
45
|
+
return {
|
|
46
|
+
chat: decorated,
|
|
47
|
+
executed: true,
|
|
48
|
+
flowId: engineResult.execution.flowId
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function decorateFinalChatWithServerToolContext(chat, execution) {
|
|
52
|
+
if (!execution || !execution.context) {
|
|
53
|
+
return chat;
|
|
54
|
+
}
|
|
55
|
+
// 目前仅对 web_search flow 附加原文摘要,避免影响其它 ServerTool。
|
|
56
|
+
if (execution.flowId !== 'web_search_flow') {
|
|
57
|
+
return chat;
|
|
58
|
+
}
|
|
59
|
+
const ctx = execution.context;
|
|
60
|
+
const web = ctx.web_search;
|
|
61
|
+
const summary = web && typeof web.summary === 'string' && web.summary.trim().length
|
|
62
|
+
? web.summary.trim()
|
|
63
|
+
: '';
|
|
64
|
+
if (!summary) {
|
|
65
|
+
return chat;
|
|
66
|
+
}
|
|
67
|
+
const engineId = web && typeof web.engineId === 'string' && web.engineId.trim().length
|
|
68
|
+
? web.engineId.trim()
|
|
69
|
+
: undefined;
|
|
70
|
+
const label = engineId
|
|
71
|
+
? `【web_search 原文 | engine: ${engineId}】`
|
|
72
|
+
: '【web_search 原文】';
|
|
73
|
+
const cloned = JSON.parse(JSON.stringify(chat));
|
|
74
|
+
const choices = Array.isArray(cloned.choices) ? cloned.choices : [];
|
|
75
|
+
if (!choices.length) {
|
|
76
|
+
return cloned;
|
|
77
|
+
}
|
|
78
|
+
const first = choices[0] && typeof choices[0] === 'object' ? choices[0] : null;
|
|
79
|
+
if (!first || !first.message || typeof first.message !== 'object') {
|
|
80
|
+
return cloned;
|
|
81
|
+
}
|
|
82
|
+
const message = first.message;
|
|
83
|
+
const baseContent = typeof message.content === 'string' ? message.content : '';
|
|
84
|
+
const suffix = `${label}\n${summary}`;
|
|
85
|
+
message.content =
|
|
86
|
+
baseContent && baseContent.trim().length
|
|
87
|
+
? `${baseContent}\n\n${suffix}`
|
|
88
|
+
: suffix;
|
|
89
|
+
return cloned;
|
|
90
|
+
}
|
|
91
|
+
function resolveRouteHint(adapterContext, flowId) {
|
|
92
|
+
const rawRoute = adapterContext.routeId;
|
|
93
|
+
const routeId = typeof rawRoute === 'string' && rawRoute.trim() ? rawRoute.trim() : '';
|
|
94
|
+
if (!routeId) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
if (flowId && routeId.toLowerCase() === flowId.toLowerCase()) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
return routeId;
|
|
101
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { JsonObject } from '../conversion/hub/types/json.js';
|
|
2
|
+
import type { ServerToolOrchestrationOptions, ServerToolOrchestrationResult } from './orchestration-types.js';
|
|
3
|
+
export interface ServerToolFlowContext {
|
|
4
|
+
options: ServerToolOrchestrationOptions;
|
|
5
|
+
baseChatResponse: JsonObject;
|
|
6
|
+
capturedChatRequest?: JsonObject;
|
|
7
|
+
routeId?: string;
|
|
8
|
+
cache: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface ServerToolHopResult {
|
|
11
|
+
body?: JsonObject;
|
|
12
|
+
__sse_responses?: unknown;
|
|
13
|
+
format?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ServerToolReenterCallOptions {
|
|
16
|
+
requestIdSuffix: string;
|
|
17
|
+
body: JsonObject;
|
|
18
|
+
entryEndpoint?: string;
|
|
19
|
+
metadata?: JsonObject;
|
|
20
|
+
}
|
|
21
|
+
export interface ServerToolProviderCallOptions {
|
|
22
|
+
requestIdSuffix: string;
|
|
23
|
+
providerKey: string;
|
|
24
|
+
providerProtocol: string;
|
|
25
|
+
entryEndpoint: string;
|
|
26
|
+
payload: JsonObject;
|
|
27
|
+
modelId?: string;
|
|
28
|
+
routeHint?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ServerToolFlowHelpers {
|
|
31
|
+
makeRequestId: (suffix: string) => string;
|
|
32
|
+
callReenterHop: (options: ServerToolReenterCallOptions) => Promise<ServerToolHopResult | null>;
|
|
33
|
+
callProviderHop: (options: ServerToolProviderCallOptions) => Promise<ServerToolHopResult | null>;
|
|
34
|
+
getRouteHintForFollowup: (exclude?: string) => string | undefined;
|
|
35
|
+
}
|
|
36
|
+
export interface ServerToolFlow {
|
|
37
|
+
id: string;
|
|
38
|
+
shouldRun: (context: ServerToolFlowContext) => Promise<boolean> | boolean;
|
|
39
|
+
run: (context: ServerToolFlowContext, helpers: ServerToolFlowHelpers) => Promise<ServerToolOrchestrationResult | null>;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { registerServerToolHandler } from '../registry.js';
|
|
2
|
+
import { cloneJson, extractTextFromChatLike } from '../server-side-tools.js';
|
|
3
|
+
const FLOW_ID = 'vision_flow';
|
|
4
|
+
const handler = async (ctx) => {
|
|
5
|
+
if (!ctx.options.reenterPipeline) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
if (!shouldRunVisionFlow(ctx)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const captured = getCapturedRequest(ctx.adapterContext);
|
|
12
|
+
if (!captured) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const analysisPayload = buildVisionAnalysisPayload(captured);
|
|
16
|
+
if (!analysisPayload) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const visionResponse = await ctx.options.reenterPipeline({
|
|
20
|
+
entryEndpoint: '/v1/chat/completions',
|
|
21
|
+
requestId: `${ctx.options.requestId}:vision`,
|
|
22
|
+
body: analysisPayload,
|
|
23
|
+
metadata: {
|
|
24
|
+
routeHint: 'vision',
|
|
25
|
+
serverToolFollowup: true,
|
|
26
|
+
stream: false
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
const visionBody = visionResponse.body && typeof visionResponse.body === 'object'
|
|
30
|
+
? visionResponse.body
|
|
31
|
+
: null;
|
|
32
|
+
if (!visionBody) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const visionSummary = extractTextFromChatLike(visionBody);
|
|
36
|
+
if (!visionSummary) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const followupPayload = buildVisionFollowupPayload(captured, visionSummary);
|
|
40
|
+
if (!followupPayload) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const execution = {
|
|
44
|
+
flowId: FLOW_ID,
|
|
45
|
+
followup: {
|
|
46
|
+
requestIdSuffix: ':vision_followup',
|
|
47
|
+
payload: followupPayload,
|
|
48
|
+
metadata: buildFollowupMetadata(ctx.adapterContext, 'vision')
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
chatResponse: ctx.base,
|
|
53
|
+
execution
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
registerServerToolHandler('vision_auto', handler, { trigger: 'auto' });
|
|
57
|
+
function shouldRunVisionFlow(ctx) {
|
|
58
|
+
const record = ctx.adapterContext;
|
|
59
|
+
const followupFlag = record.serverToolFollowup === true || record.serverToolFollowup === 'true';
|
|
60
|
+
if (followupFlag) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return record.hasImageAttachment === true || record.hasImageAttachment === 'true';
|
|
64
|
+
}
|
|
65
|
+
function getCapturedRequest(adapterContext) {
|
|
66
|
+
if (!adapterContext || typeof adapterContext !== 'object') {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const captured = adapterContext.capturedChatRequest;
|
|
70
|
+
if (!captured || typeof captured !== 'object' || Array.isArray(captured)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return captured;
|
|
74
|
+
}
|
|
75
|
+
function buildVisionAnalysisPayload(source) {
|
|
76
|
+
if (!source || typeof source !== 'object') {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const payload = {};
|
|
80
|
+
if (typeof source.model === 'string' && source.model.trim()) {
|
|
81
|
+
payload.model = source.model.trim();
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(source.messages)) {
|
|
84
|
+
payload.messages = cloneJson(source.messages);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(source.tools) && source.tools.length) {
|
|
90
|
+
payload.tools = cloneJson(source.tools);
|
|
91
|
+
}
|
|
92
|
+
const parameters = source.parameters;
|
|
93
|
+
if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
|
|
94
|
+
const params = cloneJson(parameters);
|
|
95
|
+
Object.assign(payload, params);
|
|
96
|
+
}
|
|
97
|
+
return payload;
|
|
98
|
+
}
|
|
99
|
+
function buildVisionFollowupPayload(source, summary) {
|
|
100
|
+
if (!source || typeof source !== 'object') {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const payload = {};
|
|
104
|
+
if (typeof source.model === 'string' && source.model.trim()) {
|
|
105
|
+
payload.model = source.model.trim();
|
|
106
|
+
}
|
|
107
|
+
payload.messages = injectVisionSummary(source.messages, summary);
|
|
108
|
+
if (Array.isArray(source.tools) && source.tools.length) {
|
|
109
|
+
payload.tools = cloneJson(source.tools);
|
|
110
|
+
}
|
|
111
|
+
const parameters = source.parameters;
|
|
112
|
+
if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
|
|
113
|
+
const params = cloneJson(parameters);
|
|
114
|
+
Object.assign(payload, params);
|
|
115
|
+
}
|
|
116
|
+
return payload;
|
|
117
|
+
}
|
|
118
|
+
function injectVisionSummary(source, summary) {
|
|
119
|
+
const messages = Array.isArray(source) ? cloneJson(source) : [];
|
|
120
|
+
let injected = false;
|
|
121
|
+
for (const message of messages) {
|
|
122
|
+
if (!message || typeof message !== 'object')
|
|
123
|
+
continue;
|
|
124
|
+
const content = message.content;
|
|
125
|
+
if (!Array.isArray(content))
|
|
126
|
+
continue;
|
|
127
|
+
const nextParts = [];
|
|
128
|
+
let removed = false;
|
|
129
|
+
for (const part of content) {
|
|
130
|
+
if (part && typeof part === 'object') {
|
|
131
|
+
const typeValue = typeof part.type === 'string'
|
|
132
|
+
? part.type.toLowerCase()
|
|
133
|
+
: '';
|
|
134
|
+
if (typeValue.includes('image')) {
|
|
135
|
+
removed = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
nextParts.push(part);
|
|
140
|
+
}
|
|
141
|
+
if (removed) {
|
|
142
|
+
nextParts.push({
|
|
143
|
+
type: 'text',
|
|
144
|
+
text: `[Vision] ${summary}`
|
|
145
|
+
});
|
|
146
|
+
message.content = nextParts;
|
|
147
|
+
injected = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!injected) {
|
|
151
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
152
|
+
const msg = messages[i];
|
|
153
|
+
if (!msg || typeof msg !== 'object')
|
|
154
|
+
continue;
|
|
155
|
+
const role = typeof msg.role === 'string'
|
|
156
|
+
? msg.role.toLowerCase()
|
|
157
|
+
: '';
|
|
158
|
+
if (role !== 'user')
|
|
159
|
+
continue;
|
|
160
|
+
const content = msg.content;
|
|
161
|
+
if (Array.isArray(content)) {
|
|
162
|
+
content.push({
|
|
163
|
+
type: 'text',
|
|
164
|
+
text: `[Vision] ${summary}`
|
|
165
|
+
});
|
|
166
|
+
injected = true;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
if (typeof content === 'string' && content.length) {
|
|
170
|
+
msg.content = `${content}\n[Vision] ${summary}`;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
msg.content = `[Vision] ${summary}`;
|
|
174
|
+
}
|
|
175
|
+
injected = true;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!injected) {
|
|
180
|
+
messages.push({
|
|
181
|
+
role: 'system',
|
|
182
|
+
content: `[Vision] ${summary}`
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return messages;
|
|
186
|
+
}
|
|
187
|
+
function buildFollowupMetadata(adapterContext, toolName) {
|
|
188
|
+
const ctx = adapterContext && typeof adapterContext === 'object' ? adapterContext : null;
|
|
189
|
+
const routeId = ctx && typeof ctx.routeId === 'string' && ctx.routeId.trim() ? ctx.routeId.trim() : '';
|
|
190
|
+
if (!routeId || routeId.toLowerCase() === toolName.toLowerCase()) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
return { routeHint: routeId };
|
|
194
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|