@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.
Files changed (178) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/CLAUDE.md +7 -0
  3. package/README.md +45 -25
  4. package/dist/Chat/ChatTools.js +30 -16
  5. package/dist/Chat/ChatTools.js.map +1 -1
  6. package/dist/Chat/hostedToolValidation.generated.d.ts +46 -0
  7. package/dist/Chat/hostedToolValidation.generated.js +306 -0
  8. package/dist/Chat/hostedToolValidation.generated.js.map +1 -0
  9. package/dist/Chat/index.d.ts +41 -1
  10. package/dist/Chat/index.js +285 -26
  11. package/dist/Chat/index.js.map +1 -1
  12. package/dist/Chat/modelRouting.d.ts +8 -26
  13. package/dist/Chat/modelRouting.js +11 -106
  14. package/dist/Chat/modelRouting.js.map +1 -1
  15. package/dist/Chat/sogniHostedTools.generated.json +1596 -304
  16. package/dist/Chat/tools.d.ts +52 -3
  17. package/dist/Chat/tools.js +80 -27
  18. package/dist/Chat/tools.js.map +1 -1
  19. package/dist/Chat/types.d.ts +119 -3
  20. package/dist/CreativeWorkflows/index.d.ts +1 -7
  21. package/dist/CreativeWorkflows/index.js +60 -21
  22. package/dist/CreativeWorkflows/index.js.map +1 -1
  23. package/dist/CreativeWorkflows/types.d.ts +28 -33
  24. package/dist/Projects/index.d.ts +49 -8
  25. package/dist/Projects/index.js +49 -8
  26. package/dist/Projects/index.js.map +1 -1
  27. package/dist/index.d.ts +19 -4
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/lib/Cache.js +6 -1
  31. package/dist/lib/Cache.js.map +1 -1
  32. package/dist/lib/mediaValidation.d.ts +3 -0
  33. package/dist/lib/mediaValidation.js +154 -0
  34. package/dist/lib/mediaValidation.js.map +1 -1
  35. package/dist-esm/Account/CurrentAccount.js +59 -0
  36. package/dist-esm/Account/CurrentAccount.js.map +1 -0
  37. package/dist-esm/Account/index.js +543 -0
  38. package/dist-esm/Account/index.js.map +1 -0
  39. package/dist-esm/Account/types.js +2 -0
  40. package/dist-esm/Account/types.js.map +1 -0
  41. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js +327 -0
  42. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js.map +1 -0
  43. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js +222 -0
  44. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js.map +1 -0
  45. package/dist-esm/ApiClient/WebSocketClient/ErrorCode.js +15 -0
  46. package/dist-esm/ApiClient/WebSocketClient/ErrorCode.js.map +1 -0
  47. package/dist-esm/ApiClient/WebSocketClient/eventSubscriptions.js +35 -0
  48. package/dist-esm/ApiClient/WebSocketClient/eventSubscriptions.js.map +1 -0
  49. package/dist-esm/ApiClient/WebSocketClient/events.js +2 -0
  50. package/dist-esm/ApiClient/WebSocketClient/events.js.map +1 -0
  51. package/dist-esm/ApiClient/WebSocketClient/index.js +221 -0
  52. package/dist-esm/ApiClient/WebSocketClient/index.js.map +1 -0
  53. package/dist-esm/ApiClient/WebSocketClient/messages.js +2 -0
  54. package/dist-esm/ApiClient/WebSocketClient/messages.js.map +1 -0
  55. package/dist-esm/ApiClient/WebSocketClient/types.js +2 -0
  56. package/dist-esm/ApiClient/WebSocketClient/types.js.map +1 -0
  57. package/dist-esm/ApiClient/events.js +2 -0
  58. package/dist-esm/ApiClient/events.js.map +1 -0
  59. package/dist-esm/ApiClient/index.js +120 -0
  60. package/dist-esm/ApiClient/index.js.map +1 -0
  61. package/dist-esm/ApiGroup.js +10 -0
  62. package/dist-esm/ApiGroup.js.map +1 -0
  63. package/dist-esm/Chat/ChatStream.js +181 -0
  64. package/dist-esm/Chat/ChatStream.js.map +1 -0
  65. package/dist-esm/Chat/ChatTools.js +562 -0
  66. package/dist-esm/Chat/ChatTools.js.map +1 -0
  67. package/dist-esm/Chat/hostedToolValidation.generated.js +301 -0
  68. package/dist-esm/Chat/hostedToolValidation.generated.js.map +1 -0
  69. package/dist-esm/Chat/index.js +693 -0
  70. package/dist-esm/Chat/index.js.map +1 -0
  71. package/dist-esm/Chat/modelRouting.js +326 -0
  72. package/dist-esm/Chat/modelRouting.js.map +1 -0
  73. package/dist-esm/Chat/sogniHostedTools.generated.json +1821 -0
  74. package/dist-esm/Chat/tools.js +149 -0
  75. package/dist-esm/Chat/tools.js.map +1 -0
  76. package/dist-esm/Chat/types.js +2 -0
  77. package/dist-esm/Chat/types.js.map +1 -0
  78. package/dist-esm/CreativeWorkflows/index.js +293 -0
  79. package/dist-esm/CreativeWorkflows/index.js.map +1 -0
  80. package/dist-esm/CreativeWorkflows/types.js +2 -0
  81. package/dist-esm/CreativeWorkflows/types.js.map +1 -0
  82. package/dist-esm/Projects/Job.js +391 -0
  83. package/dist-esm/Projects/Job.js.map +1 -0
  84. package/dist-esm/Projects/Project.js +295 -0
  85. package/dist-esm/Projects/Project.js.map +1 -0
  86. package/dist-esm/Projects/createJobRequestMessage.js +528 -0
  87. package/dist-esm/Projects/createJobRequestMessage.js.map +1 -0
  88. package/dist-esm/Projects/index.js +1350 -0
  89. package/dist-esm/Projects/index.js.map +1 -0
  90. package/dist-esm/Projects/types/ComfySamplerParams.js +2 -0
  91. package/dist-esm/Projects/types/ComfySamplerParams.js.map +1 -0
  92. package/dist-esm/Projects/types/ControlNetParams.js +2 -0
  93. package/dist-esm/Projects/types/ControlNetParams.js.map +1 -0
  94. package/dist-esm/Projects/types/EstimationResponse.js +2 -0
  95. package/dist-esm/Projects/types/EstimationResponse.js.map +1 -0
  96. package/dist-esm/Projects/types/ModelOptions.js +89 -0
  97. package/dist-esm/Projects/types/ModelOptions.js.map +1 -0
  98. package/dist-esm/Projects/types/ModelTiersRaw.js +13 -0
  99. package/dist-esm/Projects/types/ModelTiersRaw.js.map +1 -0
  100. package/dist-esm/Projects/types/RawProject.js +2 -0
  101. package/dist-esm/Projects/types/RawProject.js.map +1 -0
  102. package/dist-esm/Projects/types/events.js +2 -0
  103. package/dist-esm/Projects/types/events.js.map +1 -0
  104. package/dist-esm/Projects/types/index.js +10 -0
  105. package/dist-esm/Projects/types/index.js.map +1 -0
  106. package/dist-esm/Projects/utils/index.js +229 -0
  107. package/dist-esm/Projects/utils/index.js.map +1 -0
  108. package/dist-esm/Projects/utils/samplers.js +36 -0
  109. package/dist-esm/Projects/utils/samplers.js.map +1 -0
  110. package/dist-esm/Projects/utils/scheduler.js +27 -0
  111. package/dist-esm/Projects/utils/scheduler.js.map +1 -0
  112. package/dist-esm/Stats/index.js +20 -0
  113. package/dist-esm/Stats/index.js.map +1 -0
  114. package/dist-esm/Stats/types.js +2 -0
  115. package/dist-esm/Stats/types.js.map +1 -0
  116. package/dist-esm/events.js +2 -0
  117. package/dist-esm/events.js.map +1 -0
  118. package/dist-esm/index.js +144 -0
  119. package/dist-esm/index.js.map +1 -0
  120. package/dist-esm/lib/AuthManager/ApiKeyAuthManager.js +58 -0
  121. package/dist-esm/lib/AuthManager/ApiKeyAuthManager.js.map +1 -0
  122. package/dist-esm/lib/AuthManager/AuthManagerBase.js +9 -0
  123. package/dist-esm/lib/AuthManager/AuthManagerBase.js.map +1 -0
  124. package/dist-esm/lib/AuthManager/CookieAuthManager.js +48 -0
  125. package/dist-esm/lib/AuthManager/CookieAuthManager.js.map +1 -0
  126. package/dist-esm/lib/AuthManager/TokenAuthManager.js +186 -0
  127. package/dist-esm/lib/AuthManager/TokenAuthManager.js.map +1 -0
  128. package/dist-esm/lib/AuthManager/index.js +5 -0
  129. package/dist-esm/lib/AuthManager/index.js.map +1 -0
  130. package/dist-esm/lib/Cache.js +32 -0
  131. package/dist-esm/lib/Cache.js.map +1 -0
  132. package/dist-esm/lib/DataEntity.js +30 -0
  133. package/dist-esm/lib/DataEntity.js.map +1 -0
  134. package/dist-esm/lib/DefaultLogger.js +32 -0
  135. package/dist-esm/lib/DefaultLogger.js.map +1 -0
  136. package/dist-esm/lib/EIP712Helper.js +37 -0
  137. package/dist-esm/lib/EIP712Helper.js.map +1 -0
  138. package/dist-esm/lib/RestClient.js +90 -0
  139. package/dist-esm/lib/RestClient.js.map +1 -0
  140. package/dist-esm/lib/TypedEventEmitter.js +68 -0
  141. package/dist-esm/lib/TypedEventEmitter.js.map +1 -0
  142. package/dist-esm/lib/base64.js +18 -0
  143. package/dist-esm/lib/base64.js.map +1 -0
  144. package/dist-esm/lib/getUUID.js +7 -0
  145. package/dist-esm/lib/getUUID.js.map +1 -0
  146. package/dist-esm/lib/isNodejs.js +3 -0
  147. package/dist-esm/lib/isNodejs.js.map +1 -0
  148. package/dist-esm/lib/mediaValidation.js +430 -0
  149. package/dist-esm/lib/mediaValidation.js.map +1 -0
  150. package/dist-esm/lib/utils/index.js +30 -0
  151. package/dist-esm/lib/utils/index.js.map +1 -0
  152. package/dist-esm/lib/validation.js +138 -0
  153. package/dist-esm/lib/validation.js.map +1 -0
  154. package/dist-esm/package.json +3 -0
  155. package/dist-esm/types/ErrorData.js +2 -0
  156. package/dist-esm/types/ErrorData.js.map +1 -0
  157. package/dist-esm/types/json.js +2 -0
  158. package/dist-esm/types/json.js.map +1 -0
  159. package/dist-esm/types/token.js +2 -0
  160. package/dist-esm/types/token.js.map +1 -0
  161. package/dist-esm/version.js +3 -0
  162. package/dist-esm/version.js.map +1 -0
  163. package/llms-full.txt +11 -20
  164. package/llms.txt +9 -18
  165. package/package.json +21 -5
  166. package/src/Chat/ChatTools.ts +36 -16
  167. package/src/Chat/hostedToolValidation.generated.ts +453 -0
  168. package/src/Chat/index.ts +322 -37
  169. package/src/Chat/modelRouting.ts +26 -150
  170. package/src/Chat/sogniHostedTools.generated.json +1596 -304
  171. package/src/Chat/tools.ts +106 -41
  172. package/src/Chat/types.ts +131 -3
  173. package/src/CreativeWorkflows/index.ts +55 -41
  174. package/src/CreativeWorkflows/types.ts +70 -42
  175. package/src/Projects/index.ts +49 -8
  176. package/src/index.ts +33 -12
  177. package/src/lib/Cache.ts +8 -1
  178. 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 { parseInlineMediaDataUri } from '../lib/mediaValidation';
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
- function normalizeVisionImageDataUri(input: string): string {
34
- parseInlineMediaDataUri(input, 'image', {
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 messages.map((msg) => {
44
- if (!Array.isArray(msg.content)) {
45
- return msg;
46
- }
56
+ return Promise.all(
57
+ messages.map(async (msg) => {
58
+ if (!Array.isArray(msg.content)) {
59
+ return msg;
60
+ }
47
61
 
48
- return {
49
- ...msg,
50
- content: msg.content.map((part) => {
51
- if (part.type !== 'image_url') {
52
- return part;
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
- imageCount += 1;
56
- if (imageCount > MAX_VISION_IMAGE_COUNT) {
57
- throw new Error(
58
- `A maximum of ${MAX_VISION_IMAGE_COUNT} vision images is allowed per request`
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
- return {
63
- type: 'image_url' as const,
64
- image_url: {
65
- url: normalizeVisionImageDataUri(part.image_url.url),
66
- ...(part.image_url.detail && { detail: part.image_url.detail })
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 || params.appSource || this.client.appSource,
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 || params.tokenType,
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);
@@ -3,11 +3,28 @@ import { getVideoWorkflowType } from '../Projects/utils';
3
3
  export { getVideoWorkflowType };
4
4
 
5
5
  /**
6
- * Public SDK-local copy of the pure model routing helpers from
7
- * @sogni/creative-agent/backbone/reference. Keep this file self-contained so
8
- * published SDK consumers do not need the private creative-agent package.
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 'sogni_generate_image':
267
+ case 'generate_image':
273
268
  selectors = IMAGE_MODEL_SELECTORS;
274
269
  break;
275
- case 'sogni_edit_image':
270
+ case 'edit_image':
276
271
  selectors = EDIT_IMAGE_MODEL_SELECTORS;
277
272
  break;
278
- case 'sogni_generate_video':
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 'sogni_sound_to_video':
279
+ case 'sound_to_video':
285
280
  selectors = SOUND_TO_VIDEO_MODEL_SELECTORS;
286
281
  break;
287
- case 'sogni_video_to_video':
282
+ case 'video_to_video':
288
283
  selectors = VIDEO_TO_VIDEO_MODEL_SELECTORS;
289
284
  break;
290
- case 'sogni_generate_music':
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
  }