@sogni-ai/sogni-client 4.2.0-alpha.21 → 4.2.0-alpha.23
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/CHANGELOG.md +14 -0
- package/CLAUDE.md +7 -0
- package/README.md +45 -25
- package/dist/Chat/ChatTools.js +30 -16
- package/dist/Chat/ChatTools.js.map +1 -1
- package/dist/Chat/hostedToolValidation.generated.d.ts +46 -0
- package/dist/Chat/hostedToolValidation.generated.js +306 -0
- package/dist/Chat/hostedToolValidation.generated.js.map +1 -0
- package/dist/Chat/index.d.ts +41 -1
- package/dist/Chat/index.js +285 -26
- package/dist/Chat/index.js.map +1 -1
- package/dist/Chat/modelRouting.d.ts +8 -26
- package/dist/Chat/modelRouting.js +11 -106
- package/dist/Chat/modelRouting.js.map +1 -1
- package/dist/Chat/sogniHostedTools.generated.json +1596 -304
- package/dist/Chat/tools.d.ts +52 -3
- package/dist/Chat/tools.js +80 -27
- package/dist/Chat/tools.js.map +1 -1
- package/dist/Chat/types.d.ts +119 -3
- package/dist/CreativeWorkflows/index.d.ts +1 -7
- package/dist/CreativeWorkflows/index.js +60 -21
- package/dist/CreativeWorkflows/index.js.map +1 -1
- package/dist/CreativeWorkflows/types.d.ts +28 -33
- package/dist/Projects/index.d.ts +49 -8
- package/dist/Projects/index.js +49 -8
- package/dist/Projects/index.js.map +1 -1
- package/dist/index.d.ts +19 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/Cache.js +6 -1
- package/dist/lib/Cache.js.map +1 -1
- package/dist/lib/mediaValidation.d.ts +3 -0
- package/dist/lib/mediaValidation.js +154 -0
- package/dist/lib/mediaValidation.js.map +1 -1
- package/dist-esm/Account/CurrentAccount.js +59 -0
- package/dist-esm/Account/CurrentAccount.js.map +1 -0
- package/dist-esm/Account/index.js +543 -0
- package/dist-esm/Account/index.js.map +1 -0
- package/dist-esm/Account/types.js +2 -0
- package/dist-esm/Account/types.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js +327 -0
- package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js +222 -0
- package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/ErrorCode.js +15 -0
- package/dist-esm/ApiClient/WebSocketClient/ErrorCode.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/eventSubscriptions.js +35 -0
- package/dist-esm/ApiClient/WebSocketClient/eventSubscriptions.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/events.js +2 -0
- package/dist-esm/ApiClient/WebSocketClient/events.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/index.js +221 -0
- package/dist-esm/ApiClient/WebSocketClient/index.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/messages.js +2 -0
- package/dist-esm/ApiClient/WebSocketClient/messages.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/types.js +2 -0
- package/dist-esm/ApiClient/WebSocketClient/types.js.map +1 -0
- package/dist-esm/ApiClient/events.js +2 -0
- package/dist-esm/ApiClient/events.js.map +1 -0
- package/dist-esm/ApiClient/index.js +120 -0
- package/dist-esm/ApiClient/index.js.map +1 -0
- package/dist-esm/ApiGroup.js +10 -0
- package/dist-esm/ApiGroup.js.map +1 -0
- package/dist-esm/Chat/ChatStream.js +181 -0
- package/dist-esm/Chat/ChatStream.js.map +1 -0
- package/dist-esm/Chat/ChatTools.js +562 -0
- package/dist-esm/Chat/ChatTools.js.map +1 -0
- package/dist-esm/Chat/hostedToolValidation.generated.js +301 -0
- package/dist-esm/Chat/hostedToolValidation.generated.js.map +1 -0
- package/dist-esm/Chat/index.js +693 -0
- package/dist-esm/Chat/index.js.map +1 -0
- package/dist-esm/Chat/modelRouting.js +326 -0
- package/dist-esm/Chat/modelRouting.js.map +1 -0
- package/dist-esm/Chat/sogniHostedTools.generated.json +1821 -0
- package/dist-esm/Chat/tools.js +149 -0
- package/dist-esm/Chat/tools.js.map +1 -0
- package/dist-esm/Chat/types.js +2 -0
- package/dist-esm/Chat/types.js.map +1 -0
- package/dist-esm/CreativeWorkflows/index.js +293 -0
- package/dist-esm/CreativeWorkflows/index.js.map +1 -0
- package/dist-esm/CreativeWorkflows/types.js +2 -0
- package/dist-esm/CreativeWorkflows/types.js.map +1 -0
- package/dist-esm/Projects/Job.js +391 -0
- package/dist-esm/Projects/Job.js.map +1 -0
- package/dist-esm/Projects/Project.js +295 -0
- package/dist-esm/Projects/Project.js.map +1 -0
- package/dist-esm/Projects/createJobRequestMessage.js +528 -0
- package/dist-esm/Projects/createJobRequestMessage.js.map +1 -0
- package/dist-esm/Projects/index.js +1350 -0
- package/dist-esm/Projects/index.js.map +1 -0
- package/dist-esm/Projects/types/ComfySamplerParams.js +2 -0
- package/dist-esm/Projects/types/ComfySamplerParams.js.map +1 -0
- package/dist-esm/Projects/types/ControlNetParams.js +2 -0
- package/dist-esm/Projects/types/ControlNetParams.js.map +1 -0
- package/dist-esm/Projects/types/EstimationResponse.js +2 -0
- package/dist-esm/Projects/types/EstimationResponse.js.map +1 -0
- package/dist-esm/Projects/types/ModelOptions.js +89 -0
- package/dist-esm/Projects/types/ModelOptions.js.map +1 -0
- package/dist-esm/Projects/types/ModelTiersRaw.js +13 -0
- package/dist-esm/Projects/types/ModelTiersRaw.js.map +1 -0
- package/dist-esm/Projects/types/RawProject.js +2 -0
- package/dist-esm/Projects/types/RawProject.js.map +1 -0
- package/dist-esm/Projects/types/events.js +2 -0
- package/dist-esm/Projects/types/events.js.map +1 -0
- package/dist-esm/Projects/types/index.js +10 -0
- package/dist-esm/Projects/types/index.js.map +1 -0
- package/dist-esm/Projects/utils/index.js +229 -0
- package/dist-esm/Projects/utils/index.js.map +1 -0
- package/dist-esm/Projects/utils/samplers.js +36 -0
- package/dist-esm/Projects/utils/samplers.js.map +1 -0
- package/dist-esm/Projects/utils/scheduler.js +27 -0
- package/dist-esm/Projects/utils/scheduler.js.map +1 -0
- package/dist-esm/Stats/index.js +20 -0
- package/dist-esm/Stats/index.js.map +1 -0
- package/dist-esm/Stats/types.js +2 -0
- package/dist-esm/Stats/types.js.map +1 -0
- package/dist-esm/events.js +2 -0
- package/dist-esm/events.js.map +1 -0
- package/dist-esm/index.js +144 -0
- package/dist-esm/index.js.map +1 -0
- package/dist-esm/lib/AuthManager/ApiKeyAuthManager.js +58 -0
- package/dist-esm/lib/AuthManager/ApiKeyAuthManager.js.map +1 -0
- package/dist-esm/lib/AuthManager/AuthManagerBase.js +9 -0
- package/dist-esm/lib/AuthManager/AuthManagerBase.js.map +1 -0
- package/dist-esm/lib/AuthManager/CookieAuthManager.js +48 -0
- package/dist-esm/lib/AuthManager/CookieAuthManager.js.map +1 -0
- package/dist-esm/lib/AuthManager/TokenAuthManager.js +186 -0
- package/dist-esm/lib/AuthManager/TokenAuthManager.js.map +1 -0
- package/dist-esm/lib/AuthManager/index.js +5 -0
- package/dist-esm/lib/AuthManager/index.js.map +1 -0
- package/dist-esm/lib/Cache.js +32 -0
- package/dist-esm/lib/Cache.js.map +1 -0
- package/dist-esm/lib/DataEntity.js +30 -0
- package/dist-esm/lib/DataEntity.js.map +1 -0
- package/dist-esm/lib/DefaultLogger.js +32 -0
- package/dist-esm/lib/DefaultLogger.js.map +1 -0
- package/dist-esm/lib/EIP712Helper.js +37 -0
- package/dist-esm/lib/EIP712Helper.js.map +1 -0
- package/dist-esm/lib/RestClient.js +90 -0
- package/dist-esm/lib/RestClient.js.map +1 -0
- package/dist-esm/lib/TypedEventEmitter.js +68 -0
- package/dist-esm/lib/TypedEventEmitter.js.map +1 -0
- package/dist-esm/lib/base64.js +18 -0
- package/dist-esm/lib/base64.js.map +1 -0
- package/dist-esm/lib/getUUID.js +7 -0
- package/dist-esm/lib/getUUID.js.map +1 -0
- package/dist-esm/lib/isNodejs.js +3 -0
- package/dist-esm/lib/isNodejs.js.map +1 -0
- package/dist-esm/lib/mediaValidation.js +430 -0
- package/dist-esm/lib/mediaValidation.js.map +1 -0
- package/dist-esm/lib/utils/index.js +30 -0
- package/dist-esm/lib/utils/index.js.map +1 -0
- package/dist-esm/lib/validation.js +138 -0
- package/dist-esm/lib/validation.js.map +1 -0
- package/dist-esm/package.json +3 -0
- package/dist-esm/types/ErrorData.js +2 -0
- package/dist-esm/types/ErrorData.js.map +1 -0
- package/dist-esm/types/json.js +2 -0
- package/dist-esm/types/json.js.map +1 -0
- package/dist-esm/types/token.js +2 -0
- package/dist-esm/types/token.js.map +1 -0
- package/dist-esm/version.js +3 -0
- package/dist-esm/version.js.map +1 -0
- package/llms-full.txt +11 -20
- package/llms.txt +9 -18
- package/package.json +21 -5
- package/src/Chat/ChatTools.ts +36 -16
- package/src/Chat/hostedToolValidation.generated.ts +453 -0
- package/src/Chat/index.ts +322 -37
- package/src/Chat/modelRouting.ts +26 -150
- package/src/Chat/sogniHostedTools.generated.json +1596 -304
- package/src/Chat/tools.ts +106 -41
- package/src/Chat/types.ts +131 -3
- package/src/CreativeWorkflows/index.ts +55 -41
- package/src/CreativeWorkflows/types.ts +70 -42
- package/src/Projects/index.ts +49 -8
- package/src/index.ts +33 -12
- package/src/lib/Cache.ts +8 -1
- package/src/lib/mediaValidation.ts +155 -0
package/src/Chat/index.ts
CHANGED
|
@@ -14,61 +14,154 @@ import {
|
|
|
14
14
|
ChatJobStateEvent,
|
|
15
15
|
ChatRequestMessage,
|
|
16
16
|
ChatMessage,
|
|
17
|
+
ChatRunEvent,
|
|
18
|
+
ChatRunRecord,
|
|
17
19
|
HostedChatCompletionParams,
|
|
18
20
|
HostedChatCompletionResult,
|
|
19
21
|
LLMCostEstimation,
|
|
20
22
|
LLMEstimateResponse,
|
|
21
23
|
LLMModelInfo,
|
|
24
|
+
StartChatRunParams,
|
|
25
|
+
StreamChatRunEventsOptions,
|
|
22
26
|
ToolCall,
|
|
23
27
|
ToolHistoryEntry
|
|
24
28
|
} from './types';
|
|
25
29
|
import getUUID from '../lib/getUUID';
|
|
26
30
|
import type ProjectsApi from '../Projects';
|
|
27
|
-
import {
|
|
31
|
+
import { mediaInputToInlineDataUri } from '../lib/mediaValidation';
|
|
28
32
|
|
|
29
33
|
const MAX_VISION_IMAGE_COUNT = 20;
|
|
30
34
|
const MAX_VISION_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
31
35
|
const MAX_VISION_IMAGE_LONGEST_SIDE = 1024;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
const EXTERNAL_CHAT_RUN_MEDIA_URL = /^https?:\/\//i;
|
|
37
|
+
const INLINE_CHAT_RUN_MEDIA_URL = /^data:/i;
|
|
38
|
+
const CHAT_RUN_MEDIA_CONTEXT_FIELDS = [
|
|
39
|
+
'images',
|
|
40
|
+
'videos',
|
|
41
|
+
'audio',
|
|
42
|
+
'uploadedImages',
|
|
43
|
+
'uploadedVideos',
|
|
44
|
+
'uploadedAudio'
|
|
45
|
+
] as const;
|
|
46
|
+
|
|
47
|
+
async function normalizeVisionImageDataUri(input: string): Promise<string> {
|
|
48
|
+
return mediaInputToInlineDataUri(input, 'image', {
|
|
35
49
|
maxBytes: MAX_VISION_IMAGE_BYTES,
|
|
36
50
|
maxImageLongestSide: MAX_VISION_IMAGE_LONGEST_SIDE
|
|
37
51
|
});
|
|
38
|
-
return input.trim();
|
|
39
52
|
}
|
|
40
53
|
|
|
41
|
-
function normalizeVisionMessages(messages: ChatMessage[]): ChatMessage[] {
|
|
54
|
+
async function normalizeVisionMessages(messages: ChatMessage[]): Promise<ChatMessage[]> {
|
|
42
55
|
let imageCount = 0;
|
|
43
|
-
return
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
return Promise.all(
|
|
57
|
+
messages.map(async (msg) => {
|
|
58
|
+
if (!Array.isArray(msg.content)) {
|
|
59
|
+
return msg;
|
|
60
|
+
}
|
|
47
61
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
return {
|
|
63
|
+
...msg,
|
|
64
|
+
content: await Promise.all(
|
|
65
|
+
msg.content.map(async (part) => {
|
|
66
|
+
if (part.type !== 'image_url') {
|
|
67
|
+
return part;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
imageCount += 1;
|
|
71
|
+
if (imageCount > MAX_VISION_IMAGE_COUNT) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`A maximum of ${MAX_VISION_IMAGE_COUNT} vision images is allowed per request`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
54
76
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
return {
|
|
78
|
+
type: 'image_url' as const,
|
|
79
|
+
image_url: {
|
|
80
|
+
url: await normalizeVisionImageDataUri(part.image_url.url),
|
|
81
|
+
...(part.image_url.detail && { detail: part.image_url.detail })
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
})
|
|
85
|
+
)
|
|
86
|
+
};
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
}
|
|
61
90
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
91
|
+
function chatRunMediaUrlViolation(path: string, value: unknown): string | null {
|
|
92
|
+
if (typeof value !== 'string') return null;
|
|
93
|
+
const trimmed = value.trim();
|
|
94
|
+
if (!trimmed) return null;
|
|
95
|
+
if (INLINE_CHAT_RUN_MEDIA_URL.test(trimmed)) return path;
|
|
96
|
+
if (!EXTERNAL_CHAT_RUN_MEDIA_URL.test(trimmed)) return path;
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function collectChatRunMessageMediaViolations(messages: unknown[]): string[] {
|
|
101
|
+
const violations: string[] = [];
|
|
102
|
+
messages.forEach((message, messageIndex) => {
|
|
103
|
+
if (!message || typeof message !== 'object' || Array.isArray(message)) return;
|
|
104
|
+
const content = (message as { content?: unknown }).content;
|
|
105
|
+
if (!Array.isArray(content)) return;
|
|
106
|
+
content.forEach((part, partIndex) => {
|
|
107
|
+
if (!part || typeof part !== 'object' || Array.isArray(part)) return;
|
|
108
|
+
if ((part as { type?: unknown }).type !== 'image_url') return;
|
|
109
|
+
const imageUrl = (part as { image_url?: unknown }).image_url;
|
|
110
|
+
if (!imageUrl || typeof imageUrl !== 'object' || Array.isArray(imageUrl)) return;
|
|
111
|
+
const violation = chatRunMediaUrlViolation(
|
|
112
|
+
`messages[${messageIndex}].content[${partIndex}].image_url.url`,
|
|
113
|
+
(imageUrl as { url?: unknown }).url
|
|
114
|
+
);
|
|
115
|
+
if (violation) violations.push(violation);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
return violations;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function collectChatRunMediaReferenceViolations(mediaReferences: unknown[] | undefined): string[] {
|
|
122
|
+
if (!Array.isArray(mediaReferences)) return [];
|
|
123
|
+
const violations: string[] = [];
|
|
124
|
+
mediaReferences.forEach((reference, index) => {
|
|
125
|
+
if (!reference || typeof reference !== 'object' || Array.isArray(reference)) return;
|
|
126
|
+
const record = reference as { url?: unknown; dataUri?: unknown; data_uri?: unknown };
|
|
127
|
+
const urlViolation = chatRunMediaUrlViolation(`mediaReferences[${index}].url`, record.url);
|
|
128
|
+
if (urlViolation) violations.push(urlViolation);
|
|
129
|
+
if (typeof record.dataUri === 'string' && record.dataUri.trim()) {
|
|
130
|
+
violations.push(`mediaReferences[${index}].dataUri`);
|
|
131
|
+
}
|
|
132
|
+
if (typeof record.data_uri === 'string' && record.data_uri.trim()) {
|
|
133
|
+
violations.push(`mediaReferences[${index}].data_uri`);
|
|
134
|
+
}
|
|
71
135
|
});
|
|
136
|
+
return violations;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function collectChatRunMediaContextViolations(
|
|
140
|
+
mediaContext: StartChatRunParams['mediaContext'] | undefined
|
|
141
|
+
): string[] {
|
|
142
|
+
if (!mediaContext) return [];
|
|
143
|
+
const violations: string[] = [];
|
|
144
|
+
for (const field of CHAT_RUN_MEDIA_CONTEXT_FIELDS) {
|
|
145
|
+
const values = mediaContext[field];
|
|
146
|
+
if (!Array.isArray(values)) continue;
|
|
147
|
+
values.forEach((value, index) => {
|
|
148
|
+
const violation = chatRunMediaUrlViolation(`mediaContext.${field}[${index}]`, value);
|
|
149
|
+
if (violation) violations.push(violation);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return violations;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function assertChatRunUsesExternalMedia(params: StartChatRunParams): void {
|
|
156
|
+
const violations = [
|
|
157
|
+
...collectChatRunMessageMediaViolations(params.messages),
|
|
158
|
+
...collectChatRunMediaReferenceViolations(params.mediaReferences),
|
|
159
|
+
...collectChatRunMediaContextViolations(params.mediaContext)
|
|
160
|
+
];
|
|
161
|
+
if (violations.length === 0) return;
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Durable chat runs do not support inline base64/data URI media. Upload media first and pass HTTP(S) URLs instead. Offending field(s): ${violations.join(', ')}`
|
|
164
|
+
);
|
|
72
165
|
}
|
|
73
166
|
|
|
74
167
|
export interface ChatApiEvents {
|
|
@@ -136,10 +229,37 @@ class ChatApi extends ApiGroup<ChatApiEvents> {
|
|
|
136
229
|
((params: ChatCompletionParams) => Promise<ChatStream | ChatCompletionResult>);
|
|
137
230
|
};
|
|
138
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Hosted REST chat completion (`POST /v1/chat/completions`). The API
|
|
234
|
+
* executes any eligible hosted tools server-side and returns the
|
|
235
|
+
* final result within the request lifetime.
|
|
236
|
+
*/
|
|
139
237
|
hosted: {
|
|
140
238
|
create: (params: HostedChatCompletionParams) => Promise<HostedChatCompletionResult>;
|
|
141
239
|
};
|
|
142
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Durable hosted chat runs (`POST /v1/chat/runs`). A run is submitted
|
|
243
|
+
* and persisted server-side; the executor drives the LLM/tool loop
|
|
244
|
+
* without requiring the client to stay connected. Clients can
|
|
245
|
+
* disconnect and reattach via SSE `Last-Event-ID` replay or fetch
|
|
246
|
+
* the final snapshot.
|
|
247
|
+
*
|
|
248
|
+
* - `create` returns the persisted run record immediately (202).
|
|
249
|
+
* - `get` reads the current run state.
|
|
250
|
+
* - `cancel` flips the run to `cancelled` and aborts in-flight work.
|
|
251
|
+
* - `streamEvents` yields `ChatRunEvent`s via SSE.
|
|
252
|
+
*/
|
|
253
|
+
runs: {
|
|
254
|
+
create: (params: StartChatRunParams) => Promise<ChatRunRecord>;
|
|
255
|
+
get: (runId: string) => Promise<ChatRunRecord>;
|
|
256
|
+
cancel: (runId: string, reason?: string) => Promise<ChatRunRecord>;
|
|
257
|
+
streamEvents: (
|
|
258
|
+
runId: string,
|
|
259
|
+
options?: StreamChatRunEventsOptions
|
|
260
|
+
) => AsyncIterableIterator<ChatRunEvent>;
|
|
261
|
+
};
|
|
262
|
+
|
|
143
263
|
constructor(config: ApiConfig, projects?: ProjectsApi) {
|
|
144
264
|
super(config);
|
|
145
265
|
|
|
@@ -157,6 +277,12 @@ class ChatApi extends ApiGroup<ChatApiEvents> {
|
|
|
157
277
|
this.hosted = {
|
|
158
278
|
create: this.createHostedCompletion.bind(this)
|
|
159
279
|
};
|
|
280
|
+
this.runs = {
|
|
281
|
+
create: this.createChatRun.bind(this),
|
|
282
|
+
get: this.getChatRun.bind(this),
|
|
283
|
+
cancel: this.cancelChatRun.bind(this),
|
|
284
|
+
streamEvents: this.streamChatRunEvents.bind(this)
|
|
285
|
+
};
|
|
160
286
|
|
|
161
287
|
// Set up the tools API (requires ProjectsApi for media generation).
|
|
162
288
|
// When ProjectsApi is not provided, tool execution methods will throw at runtime.
|
|
@@ -225,7 +351,7 @@ class ChatApi extends ApiGroup<ChatApiEvents> {
|
|
|
225
351
|
think?: boolean;
|
|
226
352
|
taskProfile?: 'general' | 'coding' | 'reasoning';
|
|
227
353
|
}): Promise<LLMCostEstimation> {
|
|
228
|
-
const normalizedMessages = normalizeVisionMessages(params.messages);
|
|
354
|
+
const normalizedMessages = await normalizeVisionMessages(params.messages);
|
|
229
355
|
const tokenType = params.tokenType || 'sogni';
|
|
230
356
|
const inputTokens = Math.ceil(
|
|
231
357
|
JSON.stringify(this.stripImageDataForEstimation(normalizedMessages)).length / 4
|
|
@@ -356,7 +482,7 @@ class ChatApi extends ApiGroup<ChatApiEvents> {
|
|
|
356
482
|
throw new Error('chat.hosted.create currently supports non-streaming requests only.');
|
|
357
483
|
}
|
|
358
484
|
|
|
359
|
-
const normalizedMessages = normalizeVisionMessages(params.messages);
|
|
485
|
+
const normalizedMessages = await normalizeVisionMessages(params.messages);
|
|
360
486
|
const chatTemplateKwargs =
|
|
361
487
|
params.chat_template_kwargs ?? this.buildChatTemplateKwargs(params.think);
|
|
362
488
|
return this.client.rest.post<HostedChatCompletionResult>(
|
|
@@ -364,7 +490,7 @@ class ChatApi extends ApiGroup<ChatApiEvents> {
|
|
|
364
490
|
{
|
|
365
491
|
model: params.model,
|
|
366
492
|
messages: normalizedMessages,
|
|
367
|
-
app_source: params.app_source
|
|
493
|
+
app_source: params.app_source ?? params.appSource ?? this.client.appSource,
|
|
368
494
|
max_tokens: params.max_tokens,
|
|
369
495
|
temperature: params.temperature,
|
|
370
496
|
top_p: params.top_p,
|
|
@@ -374,12 +500,12 @@ class ChatApi extends ApiGroup<ChatApiEvents> {
|
|
|
374
500
|
frequency_penalty: params.frequency_penalty,
|
|
375
501
|
presence_penalty: params.presence_penalty,
|
|
376
502
|
stop: params.stop,
|
|
377
|
-
token_type: params.token_type
|
|
503
|
+
token_type: params.token_type ?? params.tokenType,
|
|
378
504
|
tools: params.tools,
|
|
379
505
|
tool_choice: params.tool_choice,
|
|
380
506
|
sogni_tools: this.normalizeSogniToolsMode(params.sogni_tools),
|
|
381
507
|
sogni_tool_execution: params.sogni_tool_execution,
|
|
382
|
-
task_profile: params.taskProfile,
|
|
508
|
+
task_profile: params.task_profile ?? params.taskProfile,
|
|
383
509
|
media_references: params.media_references ?? params.mediaReferences,
|
|
384
510
|
api_media_references: params.api_media_references ?? params.apiMediaReferences,
|
|
385
511
|
...(chatTemplateKwargs && { chat_template_kwargs: chatTemplateKwargs }),
|
|
@@ -389,6 +515,165 @@ class ChatApi extends ApiGroup<ChatApiEvents> {
|
|
|
389
515
|
);
|
|
390
516
|
}
|
|
391
517
|
|
|
518
|
+
private async chatRunFetch(path: string, options: RequestInit = {}): Promise<Response> {
|
|
519
|
+
const url = new URL(path, this.client.rest.baseUrl).toString();
|
|
520
|
+
const authenticated = await this.client.auth.authenticateRequest(options);
|
|
521
|
+
return fetch(url, authenticated);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private async chatRunJson<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
525
|
+
const response = await this.chatRunFetch(path, options);
|
|
526
|
+
if (!response.ok) {
|
|
527
|
+
const text = await response.text();
|
|
528
|
+
let payload: Record<string, unknown> | undefined;
|
|
529
|
+
try {
|
|
530
|
+
payload = text ? (JSON.parse(text) as Record<string, unknown>) : undefined;
|
|
531
|
+
} catch {
|
|
532
|
+
payload = { message: text };
|
|
533
|
+
}
|
|
534
|
+
const message =
|
|
535
|
+
(payload && typeof payload.message === 'string' && payload.message) ||
|
|
536
|
+
response.statusText ||
|
|
537
|
+
`Chat run request failed with status ${response.status}`;
|
|
538
|
+
const err = new Error(message);
|
|
539
|
+
(err as { status?: number }).status = response.status;
|
|
540
|
+
throw err;
|
|
541
|
+
}
|
|
542
|
+
return (await response.json()) as T;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Submit a durable hosted chat run. Returns the persisted run record;
|
|
547
|
+
* the executor drives the LLM/tool loop server-side.
|
|
548
|
+
*/
|
|
549
|
+
private async createChatRun(params: StartChatRunParams): Promise<ChatRunRecord> {
|
|
550
|
+
assertChatRunUsesExternalMedia(params);
|
|
551
|
+
const body: Record<string, unknown> = {
|
|
552
|
+
messages: params.messages,
|
|
553
|
+
...(params.tools ? { tools: params.tools } : {}),
|
|
554
|
+
...(params.toolChoice !== undefined ? { tool_choice: params.toolChoice } : {}),
|
|
555
|
+
...(params.model ? { model: params.model } : {}),
|
|
556
|
+
...(params.sampling ? { sampling: params.sampling } : {}),
|
|
557
|
+
...(params.mediaReferences ? { media_references: params.mediaReferences } : {}),
|
|
558
|
+
...(params.mediaContext ? { media_context: params.mediaContext } : {}),
|
|
559
|
+
...(params.maxEstimatedCapacityUnits !== undefined
|
|
560
|
+
? { max_estimated_capacity_units: params.maxEstimatedCapacityUnits }
|
|
561
|
+
: {}),
|
|
562
|
+
...(params.confirmCost !== undefined ? { confirm_cost: params.confirmCost } : {}),
|
|
563
|
+
...(params.sessionId ? { session_id: params.sessionId } : {}),
|
|
564
|
+
...(params.clientMessageId ? { client_message_id: params.clientMessageId } : {}),
|
|
565
|
+
...(params.tokenType ? { token_type: params.tokenType } : {}),
|
|
566
|
+
...(params.appSource ? { app_source: params.appSource } : {})
|
|
567
|
+
};
|
|
568
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
569
|
+
if (params.idempotencyKey) headers['Idempotency-Key'] = params.idempotencyKey;
|
|
570
|
+
const response = await this.chatRunJson<{
|
|
571
|
+
status: string;
|
|
572
|
+
data: { run: ChatRunRecord; idempotent?: boolean };
|
|
573
|
+
}>('/v1/chat/runs', {
|
|
574
|
+
method: 'POST',
|
|
575
|
+
headers,
|
|
576
|
+
body: JSON.stringify(body)
|
|
577
|
+
});
|
|
578
|
+
return response.data.run;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** Read a persisted chat run record. */
|
|
582
|
+
private async getChatRun(runId: string): Promise<ChatRunRecord> {
|
|
583
|
+
const response = await this.chatRunJson<{ status: string; data: { run: ChatRunRecord } }>(
|
|
584
|
+
`/v1/chat/runs/${encodeURIComponent(runId)}`
|
|
585
|
+
);
|
|
586
|
+
return response.data.run;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Cancel an in-flight chat run. */
|
|
590
|
+
private async cancelChatRun(runId: string, reason?: string): Promise<ChatRunRecord> {
|
|
591
|
+
const response = await this.chatRunJson<{ status: string; data: { run: ChatRunRecord } }>(
|
|
592
|
+
`/v1/chat/runs/${encodeURIComponent(runId)}/cancel`,
|
|
593
|
+
{
|
|
594
|
+
method: 'POST',
|
|
595
|
+
headers: { 'Content-Type': 'application/json' },
|
|
596
|
+
body: JSON.stringify(reason ? { reason } : {})
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
return response.data.run;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* SSE iterator over chat-run events. Honors `Last-Event-ID` for replay.
|
|
604
|
+
* Yields events as `ChatRunEvent`. Caller is responsible for breaking
|
|
605
|
+
* the loop when they see a terminal `run_*` event type they care about.
|
|
606
|
+
*/
|
|
607
|
+
private async *streamChatRunEvents(
|
|
608
|
+
runId: string,
|
|
609
|
+
options: StreamChatRunEventsOptions = {}
|
|
610
|
+
): AsyncIterableIterator<ChatRunEvent> {
|
|
611
|
+
const headers: Record<string, string> = { Accept: 'text/event-stream' };
|
|
612
|
+
if (options.lastEventId !== undefined && Number.isFinite(options.lastEventId)) {
|
|
613
|
+
headers['Last-Event-ID'] = String(options.lastEventId);
|
|
614
|
+
}
|
|
615
|
+
const response = await this.chatRunFetch(
|
|
616
|
+
`/v1/chat/runs/${encodeURIComponent(runId)}/events/stream`,
|
|
617
|
+
{ headers, signal: options.signal }
|
|
618
|
+
);
|
|
619
|
+
if (!response.ok || !response.body) {
|
|
620
|
+
throw new Error(`Chat run event stream failed with status ${response.status}`);
|
|
621
|
+
}
|
|
622
|
+
const reader = response.body.getReader();
|
|
623
|
+
const decoder = new TextDecoder();
|
|
624
|
+
let buffer = '';
|
|
625
|
+
|
|
626
|
+
const findFrameBoundary = (source: string): { index: number; length: number } | null => {
|
|
627
|
+
const lf = source.indexOf('\n\n');
|
|
628
|
+
const crlf = source.indexOf('\r\n\r\n');
|
|
629
|
+
if (lf === -1 && crlf === -1) return null;
|
|
630
|
+
if (lf === -1) return { index: crlf, length: 4 };
|
|
631
|
+
if (crlf === -1 || lf < crlf) return { index: lf, length: 2 };
|
|
632
|
+
return { index: crlf, length: 4 };
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const yieldFrame = (frame: string): ChatRunEvent | null => {
|
|
636
|
+
if (!frame.trim() || frame.startsWith(':')) return null;
|
|
637
|
+
let eventName = 'message';
|
|
638
|
+
let data = '';
|
|
639
|
+
for (const rawLine of frame.split(/\r?\n/)) {
|
|
640
|
+
if (rawLine.startsWith('event:')) eventName = rawLine.slice(6).trim();
|
|
641
|
+
else if (rawLine.startsWith('data:')) data += (data ? '\n' : '') + rawLine.slice(5).trim();
|
|
642
|
+
}
|
|
643
|
+
if (eventName === 'run_status' || !data) return null;
|
|
644
|
+
try {
|
|
645
|
+
return JSON.parse(data) as ChatRunEvent;
|
|
646
|
+
} catch {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
while (true) {
|
|
653
|
+
const { value, done } = await reader.read();
|
|
654
|
+
if (done) {
|
|
655
|
+
const remaining = buffer.trim();
|
|
656
|
+
if (remaining) {
|
|
657
|
+
const parsed = yieldFrame(remaining);
|
|
658
|
+
if (parsed) yield parsed;
|
|
659
|
+
}
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
buffer += decoder.decode(value, { stream: true });
|
|
663
|
+
let boundary = findFrameBoundary(buffer);
|
|
664
|
+
while (boundary !== null) {
|
|
665
|
+
const frame = buffer.slice(0, boundary.index);
|
|
666
|
+
buffer = buffer.slice(boundary.index + boundary.length);
|
|
667
|
+
boundary = findFrameBoundary(buffer);
|
|
668
|
+
const parsed = yieldFrame(frame);
|
|
669
|
+
if (parsed) yield parsed;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
} finally {
|
|
673
|
+
reader.releaseLock();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
392
677
|
/**
|
|
393
678
|
* Send a single chat completion request (no auto tool execution).
|
|
394
679
|
*/
|
|
@@ -396,7 +681,7 @@ class ChatApi extends ApiGroup<ChatApiEvents> {
|
|
|
396
681
|
params: ChatCompletionParams
|
|
397
682
|
): Promise<ChatStream | ChatCompletionResult> {
|
|
398
683
|
const jobID = getUUID();
|
|
399
|
-
const normalizedMessages = normalizeVisionMessages(params.messages);
|
|
684
|
+
const normalizedMessages = await normalizeVisionMessages(params.messages);
|
|
400
685
|
|
|
401
686
|
// Build chat_template_kwargs from think parameter
|
|
402
687
|
const chatTemplateKwargs = this.buildChatTemplateKwargs(params.think);
|
package/src/Chat/modelRouting.ts
CHANGED
|
@@ -3,11 +3,28 @@ import { getVideoWorkflowType } from '../Projects/utils';
|
|
|
3
3
|
export { getVideoWorkflowType };
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Public SDK-local
|
|
7
|
-
* @sogni/creative-agent/backbone/reference.
|
|
8
|
-
*
|
|
6
|
+
* Public SDK-local routing helpers. The validator surface is generated from
|
|
7
|
+
* @sogni/creative-agent/src/backbone/reference/toolValidation.ts via
|
|
8
|
+
* `npm run sync:hosted-tool-validation` and re-exported below so SDK
|
|
9
|
+
* consumers do not need the private creative-agent package while still
|
|
10
|
+
* inheriting the full validator (pattern auto-anchoring, bounds, oneOf/anyOf,
|
|
11
|
+
* nested recursion, coercion, stripping).
|
|
9
12
|
*/
|
|
10
13
|
|
|
14
|
+
export type {
|
|
15
|
+
HostedToolSchemaProperty,
|
|
16
|
+
HostedToolSchema,
|
|
17
|
+
HostedToolDefinition,
|
|
18
|
+
ValidateHostedToolArgumentsOptions,
|
|
19
|
+
HostedToolArgumentValidationResult,
|
|
20
|
+
NormalizeHostedToolArgumentsResult
|
|
21
|
+
} from './hostedToolValidation.generated';
|
|
22
|
+
export {
|
|
23
|
+
validateAndNormalizeHostedToolArguments,
|
|
24
|
+
validateHostedToolArguments,
|
|
25
|
+
assertHostedToolArguments
|
|
26
|
+
} from './hostedToolValidation.generated';
|
|
27
|
+
|
|
11
28
|
export type BackboneMediaType = 'image' | 'video' | 'audio';
|
|
12
29
|
|
|
13
30
|
export type VideoWorkflow =
|
|
@@ -49,28 +66,6 @@ export interface SelectedBackboneModel {
|
|
|
49
66
|
selectedBy: 'requestedModel' | 'preferredModel' | 'workerCount';
|
|
50
67
|
}
|
|
51
68
|
|
|
52
|
-
export interface HostedToolSchemaProperty {
|
|
53
|
-
type?: string | string[];
|
|
54
|
-
enum?: unknown[];
|
|
55
|
-
items?: HostedToolSchemaProperty;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface HostedToolSchema {
|
|
59
|
-
required?: string[];
|
|
60
|
-
properties?: Record<string, HostedToolSchemaProperty>;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface HostedToolDefinition {
|
|
64
|
-
function: {
|
|
65
|
-
name: string;
|
|
66
|
-
parameters?: HostedToolSchema;
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface ValidateHostedToolArgumentsOptions {
|
|
71
|
-
skipEnumProperties?: string[];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
69
|
export const PREFERRED_MODEL_IDS = {
|
|
75
70
|
image: {
|
|
76
71
|
gptImage2: 'gpt-image-2',
|
|
@@ -269,25 +264,25 @@ export function resolveHostedToolModelSelector(
|
|
|
269
264
|
|
|
270
265
|
let selectors: Record<string, string> | null = null;
|
|
271
266
|
switch (toolName) {
|
|
272
|
-
case '
|
|
267
|
+
case 'generate_image':
|
|
273
268
|
selectors = IMAGE_MODEL_SELECTORS;
|
|
274
269
|
break;
|
|
275
|
-
case '
|
|
270
|
+
case 'edit_image':
|
|
276
271
|
selectors = EDIT_IMAGE_MODEL_SELECTORS;
|
|
277
272
|
break;
|
|
278
|
-
case '
|
|
273
|
+
case 'generate_video':
|
|
279
274
|
selectors =
|
|
280
275
|
isNonEmptyString(args.reference_image_url) || isNonEmptyString(args.reference_image_end_url)
|
|
281
276
|
? IMAGE_VIDEO_MODEL_SELECTORS
|
|
282
277
|
: TEXT_VIDEO_MODEL_SELECTORS;
|
|
283
278
|
break;
|
|
284
|
-
case '
|
|
279
|
+
case 'sound_to_video':
|
|
285
280
|
selectors = SOUND_TO_VIDEO_MODEL_SELECTORS;
|
|
286
281
|
break;
|
|
287
|
-
case '
|
|
282
|
+
case 'video_to_video':
|
|
288
283
|
selectors = VIDEO_TO_VIDEO_MODEL_SELECTORS;
|
|
289
284
|
break;
|
|
290
|
-
case '
|
|
285
|
+
case 'generate_music':
|
|
291
286
|
selectors = MUSIC_MODEL_SELECTORS;
|
|
292
287
|
break;
|
|
293
288
|
default:
|
|
@@ -299,125 +294,6 @@ export function resolveHostedToolModelSelector(
|
|
|
299
294
|
);
|
|
300
295
|
}
|
|
301
296
|
|
|
302
|
-
function matchesType(value: unknown, type: string): boolean {
|
|
303
|
-
switch (type) {
|
|
304
|
-
case 'array':
|
|
305
|
-
return Array.isArray(value);
|
|
306
|
-
case 'integer':
|
|
307
|
-
return typeof value === 'number' && Number.isInteger(value);
|
|
308
|
-
case 'number':
|
|
309
|
-
return typeof value === 'number' && Number.isFinite(value);
|
|
310
|
-
case 'boolean':
|
|
311
|
-
return typeof value === 'boolean';
|
|
312
|
-
case 'object':
|
|
313
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
314
|
-
case 'string':
|
|
315
|
-
return typeof value === 'string';
|
|
316
|
-
case 'null':
|
|
317
|
-
return value === null;
|
|
318
|
-
default:
|
|
319
|
-
return true;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function typeLabel(type: string | string[] | undefined): string {
|
|
324
|
-
if (Array.isArray(type)) return type.join(' or ');
|
|
325
|
-
return type ?? 'valid value';
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function typeList(type: string | string[] | undefined): string[] {
|
|
329
|
-
if (!type) return [];
|
|
330
|
-
return Array.isArray(type) ? type : [type];
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function formatEnum(values: unknown[]): string {
|
|
334
|
-
return values.map((value) => JSON.stringify(value)).join(', ');
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export function validateHostedToolArguments(
|
|
338
|
-
tools: HostedToolDefinition[],
|
|
339
|
-
toolName: string,
|
|
340
|
-
args: Record<string, unknown>,
|
|
341
|
-
options: ValidateHostedToolArgumentsOptions = {}
|
|
342
|
-
): { ok: boolean; errors: string[] } {
|
|
343
|
-
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
344
|
-
return {
|
|
345
|
-
ok: false,
|
|
346
|
-
errors: ['Tool arguments must be a JSON object']
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const tool = tools.find((candidate) => candidate.function.name === toolName);
|
|
351
|
-
if (!tool) {
|
|
352
|
-
return {
|
|
353
|
-
ok: false,
|
|
354
|
-
errors: [`Unknown hosted Sogni tool "${toolName}"`]
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const schema = tool.function.parameters;
|
|
359
|
-
if (!schema) {
|
|
360
|
-
return { ok: true, errors: [] };
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const errors: string[] = [];
|
|
364
|
-
const properties = schema.properties ?? {};
|
|
365
|
-
const skipEnumProperties = new Set(options.skipEnumProperties ?? ['model']);
|
|
366
|
-
|
|
367
|
-
for (const required of schema.required ?? []) {
|
|
368
|
-
if (args[required] === undefined || args[required] === null) {
|
|
369
|
-
errors.push(`Missing required argument "${required}"`);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
for (const [name, value] of Object.entries(args)) {
|
|
374
|
-
if (value === undefined || value === null) continue;
|
|
375
|
-
|
|
376
|
-
const property = properties[name];
|
|
377
|
-
if (!property) continue;
|
|
378
|
-
|
|
379
|
-
const allowedTypes = typeList(property.type);
|
|
380
|
-
if (allowedTypes.length > 0 && !allowedTypes.some((type) => matchesType(value, type))) {
|
|
381
|
-
errors.push(`Argument "${name}" must be ${typeLabel(property.type)}`);
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (
|
|
386
|
-
property.enum &&
|
|
387
|
-
!skipEnumProperties.has(name) &&
|
|
388
|
-
!property.enum.some((candidate) => candidate === value)
|
|
389
|
-
) {
|
|
390
|
-
errors.push(`Argument "${name}" must be one of ${formatEnum(property.enum)}`);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (Array.isArray(value) && property.items?.type) {
|
|
394
|
-
const itemTypes = typeList(property.items.type);
|
|
395
|
-
value.forEach((item, index) => {
|
|
396
|
-
if (!itemTypes.some((type) => matchesType(item, type))) {
|
|
397
|
-
errors.push(`Argument "${name}[${index}]" must be ${typeLabel(property.items?.type)}`);
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return {
|
|
404
|
-
ok: errors.length === 0,
|
|
405
|
-
errors
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
export function assertHostedToolArguments(
|
|
410
|
-
tools: HostedToolDefinition[],
|
|
411
|
-
toolName: string,
|
|
412
|
-
args: Record<string, unknown>,
|
|
413
|
-
options?: ValidateHostedToolArgumentsOptions
|
|
414
|
-
): void {
|
|
415
|
-
const result = validateHostedToolArguments(tools, toolName, args, options);
|
|
416
|
-
if (!result.ok) {
|
|
417
|
-
throw new Error(`Invalid ${toolName} arguments: ${result.errors.join('; ')}`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
297
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
422
298
|
return typeof value === 'object' && value !== null;
|
|
423
299
|
}
|