@sogni-ai/sogni-client 4.2.0-alpha.21 → 4.2.0-alpha.22

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 (170) hide show
  1. package/CHANGELOG.md +7 -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 +166 -2
  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 +1285 -283
  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 +23 -21
  22. package/dist/CreativeWorkflows/index.js.map +1 -1
  23. package/dist/CreativeWorkflows/types.d.ts +27 -33
  24. package/dist/index.d.ts +19 -4
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/lib/Cache.js +6 -1
  28. package/dist/lib/Cache.js.map +1 -1
  29. package/dist-esm/Account/CurrentAccount.js +59 -0
  30. package/dist-esm/Account/CurrentAccount.js.map +1 -0
  31. package/dist-esm/Account/index.js +543 -0
  32. package/dist-esm/Account/index.js.map +1 -0
  33. package/dist-esm/Account/types.js +2 -0
  34. package/dist-esm/Account/types.js.map +1 -0
  35. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js +327 -0
  36. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js.map +1 -0
  37. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js +222 -0
  38. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js.map +1 -0
  39. package/dist-esm/ApiClient/WebSocketClient/ErrorCode.js +15 -0
  40. package/dist-esm/ApiClient/WebSocketClient/ErrorCode.js.map +1 -0
  41. package/dist-esm/ApiClient/WebSocketClient/eventSubscriptions.js +35 -0
  42. package/dist-esm/ApiClient/WebSocketClient/eventSubscriptions.js.map +1 -0
  43. package/dist-esm/ApiClient/WebSocketClient/events.js +2 -0
  44. package/dist-esm/ApiClient/WebSocketClient/events.js.map +1 -0
  45. package/dist-esm/ApiClient/WebSocketClient/index.js +221 -0
  46. package/dist-esm/ApiClient/WebSocketClient/index.js.map +1 -0
  47. package/dist-esm/ApiClient/WebSocketClient/messages.js +2 -0
  48. package/dist-esm/ApiClient/WebSocketClient/messages.js.map +1 -0
  49. package/dist-esm/ApiClient/WebSocketClient/types.js +2 -0
  50. package/dist-esm/ApiClient/WebSocketClient/types.js.map +1 -0
  51. package/dist-esm/ApiClient/events.js +2 -0
  52. package/dist-esm/ApiClient/events.js.map +1 -0
  53. package/dist-esm/ApiClient/index.js +120 -0
  54. package/dist-esm/ApiClient/index.js.map +1 -0
  55. package/dist-esm/ApiGroup.js +10 -0
  56. package/dist-esm/ApiGroup.js.map +1 -0
  57. package/dist-esm/Chat/ChatStream.js +181 -0
  58. package/dist-esm/Chat/ChatStream.js.map +1 -0
  59. package/dist-esm/Chat/ChatTools.js +562 -0
  60. package/dist-esm/Chat/ChatTools.js.map +1 -0
  61. package/dist-esm/Chat/hostedToolValidation.generated.js +301 -0
  62. package/dist-esm/Chat/hostedToolValidation.generated.js.map +1 -0
  63. package/dist-esm/Chat/index.js +598 -0
  64. package/dist-esm/Chat/index.js.map +1 -0
  65. package/dist-esm/Chat/modelRouting.js +326 -0
  66. package/dist-esm/Chat/modelRouting.js.map +1 -0
  67. package/dist-esm/Chat/sogniHostedTools.generated.json +1531 -0
  68. package/dist-esm/Chat/tools.js +149 -0
  69. package/dist-esm/Chat/tools.js.map +1 -0
  70. package/dist-esm/Chat/types.js +2 -0
  71. package/dist-esm/Chat/types.js.map +1 -0
  72. package/dist-esm/CreativeWorkflows/index.js +256 -0
  73. package/dist-esm/CreativeWorkflows/index.js.map +1 -0
  74. package/dist-esm/CreativeWorkflows/types.js +2 -0
  75. package/dist-esm/CreativeWorkflows/types.js.map +1 -0
  76. package/dist-esm/Projects/Job.js +391 -0
  77. package/dist-esm/Projects/Job.js.map +1 -0
  78. package/dist-esm/Projects/Project.js +295 -0
  79. package/dist-esm/Projects/Project.js.map +1 -0
  80. package/dist-esm/Projects/createJobRequestMessage.js +528 -0
  81. package/dist-esm/Projects/createJobRequestMessage.js.map +1 -0
  82. package/dist-esm/Projects/index.js +1309 -0
  83. package/dist-esm/Projects/index.js.map +1 -0
  84. package/dist-esm/Projects/types/ComfySamplerParams.js +2 -0
  85. package/dist-esm/Projects/types/ComfySamplerParams.js.map +1 -0
  86. package/dist-esm/Projects/types/ControlNetParams.js +2 -0
  87. package/dist-esm/Projects/types/ControlNetParams.js.map +1 -0
  88. package/dist-esm/Projects/types/EstimationResponse.js +2 -0
  89. package/dist-esm/Projects/types/EstimationResponse.js.map +1 -0
  90. package/dist-esm/Projects/types/ModelOptions.js +89 -0
  91. package/dist-esm/Projects/types/ModelOptions.js.map +1 -0
  92. package/dist-esm/Projects/types/ModelTiersRaw.js +13 -0
  93. package/dist-esm/Projects/types/ModelTiersRaw.js.map +1 -0
  94. package/dist-esm/Projects/types/RawProject.js +2 -0
  95. package/dist-esm/Projects/types/RawProject.js.map +1 -0
  96. package/dist-esm/Projects/types/events.js +2 -0
  97. package/dist-esm/Projects/types/events.js.map +1 -0
  98. package/dist-esm/Projects/types/index.js +10 -0
  99. package/dist-esm/Projects/types/index.js.map +1 -0
  100. package/dist-esm/Projects/utils/index.js +229 -0
  101. package/dist-esm/Projects/utils/index.js.map +1 -0
  102. package/dist-esm/Projects/utils/samplers.js +36 -0
  103. package/dist-esm/Projects/utils/samplers.js.map +1 -0
  104. package/dist-esm/Projects/utils/scheduler.js +27 -0
  105. package/dist-esm/Projects/utils/scheduler.js.map +1 -0
  106. package/dist-esm/Stats/index.js +20 -0
  107. package/dist-esm/Stats/index.js.map +1 -0
  108. package/dist-esm/Stats/types.js +2 -0
  109. package/dist-esm/Stats/types.js.map +1 -0
  110. package/dist-esm/events.js +2 -0
  111. package/dist-esm/events.js.map +1 -0
  112. package/dist-esm/index.js +144 -0
  113. package/dist-esm/index.js.map +1 -0
  114. package/dist-esm/lib/AuthManager/ApiKeyAuthManager.js +58 -0
  115. package/dist-esm/lib/AuthManager/ApiKeyAuthManager.js.map +1 -0
  116. package/dist-esm/lib/AuthManager/AuthManagerBase.js +9 -0
  117. package/dist-esm/lib/AuthManager/AuthManagerBase.js.map +1 -0
  118. package/dist-esm/lib/AuthManager/CookieAuthManager.js +48 -0
  119. package/dist-esm/lib/AuthManager/CookieAuthManager.js.map +1 -0
  120. package/dist-esm/lib/AuthManager/TokenAuthManager.js +186 -0
  121. package/dist-esm/lib/AuthManager/TokenAuthManager.js.map +1 -0
  122. package/dist-esm/lib/AuthManager/index.js +5 -0
  123. package/dist-esm/lib/AuthManager/index.js.map +1 -0
  124. package/dist-esm/lib/Cache.js +32 -0
  125. package/dist-esm/lib/Cache.js.map +1 -0
  126. package/dist-esm/lib/DataEntity.js +30 -0
  127. package/dist-esm/lib/DataEntity.js.map +1 -0
  128. package/dist-esm/lib/DefaultLogger.js +32 -0
  129. package/dist-esm/lib/DefaultLogger.js.map +1 -0
  130. package/dist-esm/lib/EIP712Helper.js +37 -0
  131. package/dist-esm/lib/EIP712Helper.js.map +1 -0
  132. package/dist-esm/lib/RestClient.js +90 -0
  133. package/dist-esm/lib/RestClient.js.map +1 -0
  134. package/dist-esm/lib/TypedEventEmitter.js +68 -0
  135. package/dist-esm/lib/TypedEventEmitter.js.map +1 -0
  136. package/dist-esm/lib/base64.js +18 -0
  137. package/dist-esm/lib/base64.js.map +1 -0
  138. package/dist-esm/lib/getUUID.js +7 -0
  139. package/dist-esm/lib/getUUID.js.map +1 -0
  140. package/dist-esm/lib/isNodejs.js +3 -0
  141. package/dist-esm/lib/isNodejs.js.map +1 -0
  142. package/dist-esm/lib/mediaValidation.js +277 -0
  143. package/dist-esm/lib/mediaValidation.js.map +1 -0
  144. package/dist-esm/lib/utils/index.js +30 -0
  145. package/dist-esm/lib/utils/index.js.map +1 -0
  146. package/dist-esm/lib/validation.js +138 -0
  147. package/dist-esm/lib/validation.js.map +1 -0
  148. package/dist-esm/package.json +3 -0
  149. package/dist-esm/types/ErrorData.js +2 -0
  150. package/dist-esm/types/ErrorData.js.map +1 -0
  151. package/dist-esm/types/json.js +2 -0
  152. package/dist-esm/types/json.js.map +1 -0
  153. package/dist-esm/types/token.js +2 -0
  154. package/dist-esm/types/token.js.map +1 -0
  155. package/dist-esm/version.js +3 -0
  156. package/dist-esm/version.js.map +1 -0
  157. package/llms-full.txt +11 -20
  158. package/llms.txt +9 -18
  159. package/package.json +21 -5
  160. package/src/Chat/ChatTools.ts +36 -16
  161. package/src/Chat/hostedToolValidation.generated.ts +453 -0
  162. package/src/Chat/index.ts +198 -3
  163. package/src/Chat/modelRouting.ts +26 -150
  164. package/src/Chat/sogniHostedTools.generated.json +1285 -283
  165. package/src/Chat/tools.ts +106 -41
  166. package/src/Chat/types.ts +131 -3
  167. package/src/CreativeWorkflows/index.ts +22 -41
  168. package/src/CreativeWorkflows/types.ts +69 -42
  169. package/src/index.ts +33 -12
  170. package/src/lib/Cache.ts +8 -1
@@ -0,0 +1,453 @@
1
+ /* eslint-disable */
2
+ // Generated by scripts/sync-hosted-tool-validation.mjs.
3
+ // Source: @sogni/creative-agent/src/backbone/reference/toolValidation.ts
4
+ // Do not edit by hand. Run `npm run sync:hosted-tool-validation` to refresh.
5
+ export interface HostedToolSchemaProperty {
6
+ type?: string | string[];
7
+ const?: unknown;
8
+ enum?: unknown[];
9
+ minimum?: number;
10
+ maximum?: number;
11
+ exclusiveMinimum?: number | boolean;
12
+ exclusiveMaximum?: number | boolean;
13
+ minLength?: number;
14
+ maxLength?: number;
15
+ pattern?: string;
16
+ minItems?: number;
17
+ maxItems?: number;
18
+ items?: HostedToolSchemaProperty | HostedToolSchemaProperty[];
19
+ required?: string[];
20
+ properties?: Record<string, HostedToolSchemaProperty>;
21
+ additionalProperties?: boolean | HostedToolSchemaProperty;
22
+ anyOf?: HostedToolSchemaProperty[];
23
+ oneOf?: HostedToolSchemaProperty[];
24
+ allOf?: HostedToolSchemaProperty[];
25
+ }
26
+
27
+ export interface HostedToolSchema extends HostedToolSchemaProperty {
28
+ type?: string | string[];
29
+ }
30
+
31
+ export interface HostedToolDefinition {
32
+ function: {
33
+ name: string;
34
+ parameters?: HostedToolSchema;
35
+ };
36
+ }
37
+
38
+ export interface ValidateHostedToolArgumentsOptions {
39
+ skipEnumProperties?: string[];
40
+ coercePrimitives?: boolean;
41
+ stripUnknownProperties?: boolean;
42
+ }
43
+
44
+ export interface HostedToolArgumentValidationResult {
45
+ ok: boolean;
46
+ errors: string[];
47
+ }
48
+
49
+ export interface NormalizeHostedToolArgumentsResult extends HostedToolArgumentValidationResult {
50
+ cleaned: Record<string, unknown>;
51
+ warnings: string[];
52
+ }
53
+
54
+ function matchesType(value: unknown, type: string): boolean {
55
+ switch (type) {
56
+ case 'array':
57
+ return Array.isArray(value);
58
+ case 'integer':
59
+ return typeof value === 'number' && Number.isInteger(value);
60
+ case 'number':
61
+ return typeof value === 'number' && Number.isFinite(value);
62
+ case 'boolean':
63
+ return typeof value === 'boolean';
64
+ case 'object':
65
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
66
+ case 'string':
67
+ return typeof value === 'string';
68
+ case 'null':
69
+ return value === null;
70
+ default:
71
+ return true;
72
+ }
73
+ }
74
+
75
+ function typeLabel(type: string | string[] | undefined): string {
76
+ if (Array.isArray(type)) return type.join(' or ');
77
+ return type ?? 'valid value';
78
+ }
79
+
80
+ function typeList(type: string | string[] | undefined): string[] {
81
+ if (!type) return [];
82
+ return Array.isArray(type) ? type : [type];
83
+ }
84
+
85
+ function formatEnum(values: unknown[]): string {
86
+ return values.map(value => JSON.stringify(value)).join(', ');
87
+ }
88
+
89
+ function isRecord(value: unknown): value is Record<string, unknown> {
90
+ return !!value && typeof value === 'object' && !Array.isArray(value);
91
+ }
92
+
93
+ function valuesEqual(left: unknown, right: unknown): boolean {
94
+ if (Object.is(left, right)) return true;
95
+ if (typeof left !== typeof right) return false;
96
+ if (left === null || right === null) return left === right;
97
+ if (typeof left !== 'object') return false;
98
+ try {
99
+ return JSON.stringify(left) === JSON.stringify(right);
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+
105
+ function coerceValueForTypes(
106
+ value: unknown,
107
+ allowedTypes: string[],
108
+ ): { value: unknown; coerced: boolean } {
109
+ if (typeof value !== 'string') return { value, coerced: false };
110
+ const trimmed = value.trim();
111
+ if (!trimmed) return { value, coerced: false };
112
+
113
+ if (allowedTypes.includes('integer') || allowedTypes.includes('number')) {
114
+ const numberValue = Number(trimmed);
115
+ if (
116
+ Number.isFinite(numberValue)
117
+ && (!allowedTypes.includes('integer') || Number.isInteger(numberValue))
118
+ ) {
119
+ return { value: numberValue, coerced: true };
120
+ }
121
+ }
122
+
123
+ if (allowedTypes.includes('boolean')) {
124
+ if (/^(?:true|false)$/i.test(trimmed)) {
125
+ return { value: trimmed.toLowerCase() === 'true', coerced: true };
126
+ }
127
+ }
128
+
129
+ return { value, coerced: false };
130
+ }
131
+
132
+ interface SchemaValidationContext {
133
+ skipEnumProperties: Set<string>;
134
+ coercePrimitives: boolean;
135
+ stripUnknownProperties: boolean;
136
+ errors: string[];
137
+ warnings: string[];
138
+ }
139
+
140
+ function validateNumberBounds(
141
+ path: string,
142
+ value: number,
143
+ schema: HostedToolSchemaProperty,
144
+ errors: string[],
145
+ ): void {
146
+ if (schema.minimum !== undefined && value < schema.minimum) {
147
+ errors.push(`Argument "${path}" must be at least ${schema.minimum}`);
148
+ }
149
+ if (schema.maximum !== undefined && value > schema.maximum) {
150
+ errors.push(`Argument "${path}" must be at most ${schema.maximum}`);
151
+ }
152
+ if (typeof schema.exclusiveMinimum === 'number' && value <= schema.exclusiveMinimum) {
153
+ errors.push(`Argument "${path}" must be greater than ${schema.exclusiveMinimum}`);
154
+ }
155
+ if (typeof schema.exclusiveMaximum === 'number' && value >= schema.exclusiveMaximum) {
156
+ errors.push(`Argument "${path}" must be less than ${schema.exclusiveMaximum}`);
157
+ }
158
+ if (schema.exclusiveMinimum === true && schema.minimum !== undefined && value <= schema.minimum) {
159
+ errors.push(`Argument "${path}" must be greater than ${schema.minimum}`);
160
+ }
161
+ if (schema.exclusiveMaximum === true && schema.maximum !== undefined && value >= schema.maximum) {
162
+ errors.push(`Argument "${path}" must be less than ${schema.maximum}`);
163
+ }
164
+ }
165
+
166
+ function validateStringConstraints(
167
+ path: string,
168
+ value: string,
169
+ schema: HostedToolSchemaProperty,
170
+ errors: string[],
171
+ ): void {
172
+ if (schema.minLength !== undefined && value.length < schema.minLength) {
173
+ errors.push(`Argument "${path}" must contain at least ${schema.minLength} character${schema.minLength === 1 ? '' : 's'}`);
174
+ }
175
+ if (schema.maxLength !== undefined && value.length > schema.maxLength) {
176
+ errors.push(`Argument "${path}" must contain at most ${schema.maxLength} character${schema.maxLength === 1 ? '' : 's'}`);
177
+ }
178
+ if (schema.pattern) {
179
+ try {
180
+ // Hosted-tool schema authors near-universally assume anchored matching
181
+ // for ID-like fields, but JSON Schema `pattern` is unanchored. Auto-anchor
182
+ // to prevent prefix/suffix injection (e.g. `pattern: 'safe-prefix-'` should
183
+ // reject `'evil-safe-prefix-suffix'`). Wrapping with `^(?:...)$` is safe
184
+ // even for already-anchored patterns — `^^…$$` still matches the same set.
185
+ if (!new RegExp(`^(?:${schema.pattern})$`).test(value)) {
186
+ errors.push(`Argument "${path}" must match pattern ${JSON.stringify(schema.pattern)}`);
187
+ }
188
+ } catch {
189
+ errors.push(`Schema for "${path}" contains invalid pattern ${JSON.stringify(schema.pattern)}`);
190
+ }
191
+ }
192
+ }
193
+
194
+ function validateCompositeSchema(
195
+ path: string,
196
+ value: unknown,
197
+ schema: HostedToolSchemaProperty,
198
+ context: SchemaValidationContext,
199
+ ): { matched: boolean; value: unknown } {
200
+ if (schema.allOf) {
201
+ let nextValue = value;
202
+ for (const childSchema of schema.allOf) {
203
+ const result = validateValueAgainstSchema(path, nextValue, childSchema, context);
204
+ nextValue = result.value;
205
+ }
206
+ return { matched: true, value: nextValue };
207
+ }
208
+
209
+ const alternatives = schema.oneOf ?? schema.anyOf;
210
+ if (!alternatives) return { matched: true, value };
211
+
212
+ const matches: unknown[] = [];
213
+ for (const childSchema of alternatives) {
214
+ const trial: SchemaValidationContext = {
215
+ ...context,
216
+ errors: [],
217
+ warnings: [],
218
+ };
219
+ const result = validateValueAgainstSchema(path, value, childSchema, trial);
220
+ if (trial.errors.length === 0) matches.push(result.value);
221
+ }
222
+
223
+ if (schema.oneOf && matches.length !== 1) {
224
+ context.errors.push(`Argument "${path}" must match exactly one allowed schema`);
225
+ return { matched: false, value };
226
+ }
227
+ if (schema.anyOf && matches.length < 1) {
228
+ context.errors.push(`Argument "${path}" must match at least one allowed schema`);
229
+ return { matched: false, value };
230
+ }
231
+
232
+ return { matched: true, value: matches[0] ?? value };
233
+ }
234
+
235
+ function validateObjectAgainstSchema(
236
+ path: string,
237
+ value: Record<string, unknown>,
238
+ schema: HostedToolSchemaProperty,
239
+ context: SchemaValidationContext,
240
+ ): Record<string, unknown> {
241
+ const properties = schema.properties ?? {};
242
+ const cleaned: Record<string, unknown> = {};
243
+
244
+ for (const required of schema.required ?? []) {
245
+ const propValue = value[required];
246
+ // Empty string and empty array are treated as missing — schema authors
247
+ // assume `required` means "has a usable value", not "is non-undefined".
248
+ // Empty object is left untouched (some schemas legitimately accept `{}`).
249
+ const isMissing =
250
+ propValue === undefined
251
+ || propValue === null
252
+ || propValue === ''
253
+ || (Array.isArray(propValue) && propValue.length === 0);
254
+ if (isMissing) {
255
+ context.errors.push(`Missing required argument "${path ? `${path}.` : ''}${required}"`);
256
+ }
257
+ }
258
+
259
+ for (const [name, entryValue] of Object.entries(value)) {
260
+ if (entryValue === undefined || entryValue === null) {
261
+ if (name in properties || !context.stripUnknownProperties) cleaned[name] = entryValue;
262
+ continue;
263
+ }
264
+
265
+ const property = properties[name];
266
+ if (!property) {
267
+ if (schema.additionalProperties === false || context.stripUnknownProperties) {
268
+ context.warnings.push(`Stripped unknown argument "${path ? `${path}.` : ''}${name}"`);
269
+ } else if (isRecord(schema.additionalProperties)) {
270
+ cleaned[name] = validateValueAgainstSchema(
271
+ path ? `${path}.${name}` : name,
272
+ entryValue,
273
+ schema.additionalProperties,
274
+ context,
275
+ ).value;
276
+ } else {
277
+ cleaned[name] = entryValue;
278
+ }
279
+ continue;
280
+ }
281
+
282
+ cleaned[name] = validateValueAgainstSchema(
283
+ path ? `${path}.${name}` : name,
284
+ entryValue,
285
+ property,
286
+ context,
287
+ ).value;
288
+ }
289
+
290
+ return cleaned;
291
+ }
292
+
293
+ function validateArrayAgainstSchema(
294
+ path: string,
295
+ value: unknown[],
296
+ schema: HostedToolSchemaProperty,
297
+ context: SchemaValidationContext,
298
+ ): unknown[] {
299
+ if (schema.minItems !== undefined && value.length < schema.minItems) {
300
+ context.errors.push(`Argument "${path}" must contain at least ${schema.minItems} item${schema.minItems === 1 ? '' : 's'}`);
301
+ }
302
+ if (schema.maxItems !== undefined && value.length > schema.maxItems) {
303
+ context.errors.push(`Argument "${path}" must contain at most ${schema.maxItems} item${schema.maxItems === 1 ? '' : 's'}`);
304
+ }
305
+
306
+ if (!schema.items) return value;
307
+
308
+ if (Array.isArray(schema.items)) {
309
+ const tupleSchemas = schema.items;
310
+ return value.map((item, index) => {
311
+ const itemSchema = tupleSchemas[index];
312
+ return itemSchema
313
+ ? validateValueAgainstSchema(`${path}[${index}]`, item, itemSchema, context).value
314
+ : item;
315
+ });
316
+ }
317
+
318
+ return value.map((item, index) => validateValueAgainstSchema(`${path}[${index}]`, item, schema.items as HostedToolSchemaProperty, context).value);
319
+ }
320
+
321
+ function validateValueAgainstSchema(
322
+ path: string,
323
+ rawValue: unknown,
324
+ schema: HostedToolSchemaProperty,
325
+ context: SchemaValidationContext,
326
+ ): { value: unknown } {
327
+ const composite = validateCompositeSchema(path, rawValue, schema, context);
328
+ if (!composite.matched) return { value: rawValue };
329
+ let value = composite.value;
330
+
331
+ const allowedTypes = typeList(schema.type);
332
+ if (
333
+ context.coercePrimitives
334
+ && allowedTypes.length > 0
335
+ && !allowedTypes.some(type => matchesType(value, type))
336
+ ) {
337
+ const coerced = coerceValueForTypes(value, allowedTypes);
338
+ if (coerced.coerced) {
339
+ context.warnings.push(`Coerced argument "${path}" to ${typeof coerced.value}`);
340
+ value = coerced.value;
341
+ }
342
+ }
343
+
344
+ if (allowedTypes.length > 0 && !allowedTypes.some(type => matchesType(value, type))) {
345
+ context.errors.push(`Argument "${path}" must be ${typeLabel(schema.type)}`);
346
+ return { value };
347
+ }
348
+
349
+ if (schema.const !== undefined && !valuesEqual(value, schema.const)) {
350
+ context.errors.push(`Argument "${path}" must be ${JSON.stringify(schema.const)}`);
351
+ }
352
+
353
+ if (
354
+ schema.enum
355
+ && !context.skipEnumProperties.has(path)
356
+ && !context.skipEnumProperties.has(path.split('.').at(-1) ?? path)
357
+ && !schema.enum.some(candidate => valuesEqual(candidate, value))
358
+ ) {
359
+ context.errors.push(`Argument "${path}" must be one of ${formatEnum(schema.enum)}`);
360
+ }
361
+
362
+ if (typeof value === 'number') {
363
+ validateNumberBounds(path, value, schema, context.errors);
364
+ }
365
+ if (typeof value === 'string') {
366
+ validateStringConstraints(path, value, schema, context.errors);
367
+ }
368
+ if (Array.isArray(value)) {
369
+ value = validateArrayAgainstSchema(path, value, schema, context);
370
+ }
371
+ if (isRecord(value)) {
372
+ value = validateObjectAgainstSchema(path, value, schema, context);
373
+ }
374
+
375
+ return { value };
376
+ }
377
+
378
+ export function validateAndNormalizeHostedToolArguments(
379
+ tools: HostedToolDefinition[],
380
+ toolName: string,
381
+ args: Record<string, unknown>,
382
+ options: ValidateHostedToolArgumentsOptions = {},
383
+ ): NormalizeHostedToolArgumentsResult {
384
+ if (!args || typeof args !== 'object' || Array.isArray(args)) {
385
+ return {
386
+ ok: false,
387
+ errors: ['Tool arguments must be a JSON object'],
388
+ cleaned: {},
389
+ warnings: [],
390
+ };
391
+ }
392
+
393
+ const tool = tools.find(candidate => candidate.function.name === toolName);
394
+ if (!tool) {
395
+ return {
396
+ ok: false,
397
+ errors: [`Unknown hosted Sogni tool "${toolName}"`],
398
+ cleaned: args,
399
+ warnings: [],
400
+ };
401
+ }
402
+
403
+ const schema = tool.function.parameters;
404
+ if (!schema) {
405
+ return { ok: true, errors: [], cleaned: args, warnings: [] };
406
+ }
407
+
408
+ const context: SchemaValidationContext = {
409
+ skipEnumProperties: new Set(options.skipEnumProperties ?? ['model']),
410
+ coercePrimitives: options.coercePrimitives === true,
411
+ stripUnknownProperties: options.stripUnknownProperties === true,
412
+ errors: [],
413
+ warnings: [],
414
+ };
415
+
416
+ const normalizedSchema: HostedToolSchema = {
417
+ ...schema,
418
+ type: schema.type ?? 'object',
419
+ };
420
+ const cleaned = validateValueAgainstSchema('', args, normalizedSchema, context).value;
421
+
422
+ return {
423
+ ok: context.errors.length === 0,
424
+ errors: context.errors,
425
+ cleaned: isRecord(cleaned) ? cleaned : args,
426
+ warnings: context.warnings,
427
+ };
428
+ }
429
+
430
+ export function validateHostedToolArguments(
431
+ tools: HostedToolDefinition[],
432
+ toolName: string,
433
+ args: Record<string, unknown>,
434
+ options: ValidateHostedToolArgumentsOptions = {},
435
+ ): HostedToolArgumentValidationResult {
436
+ const result = validateAndNormalizeHostedToolArguments(tools, toolName, args, options);
437
+ return {
438
+ ok: result.ok,
439
+ errors: result.errors,
440
+ };
441
+ }
442
+
443
+ export function assertHostedToolArguments(
444
+ tools: HostedToolDefinition[],
445
+ toolName: string,
446
+ args: Record<string, unknown>,
447
+ options?: ValidateHostedToolArgumentsOptions,
448
+ ): void {
449
+ const result = validateHostedToolArguments(tools, toolName, args, options);
450
+ if (!result.ok) {
451
+ throw new Error(`Invalid ${toolName} arguments: ${result.errors.join('; ')}`);
452
+ }
453
+ }
package/src/Chat/index.ts CHANGED
@@ -14,11 +14,15 @@ 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';
@@ -136,10 +140,37 @@ class ChatApi extends ApiGroup<ChatApiEvents> {
136
140
  ((params: ChatCompletionParams) => Promise<ChatStream | ChatCompletionResult>);
137
141
  };
138
142
 
143
+ /**
144
+ * Hosted REST chat completion (`POST /v1/chat/completions`). The API
145
+ * executes any eligible hosted tools server-side and returns the
146
+ * final result within the request lifetime.
147
+ */
139
148
  hosted: {
140
149
  create: (params: HostedChatCompletionParams) => Promise<HostedChatCompletionResult>;
141
150
  };
142
151
 
152
+ /**
153
+ * Durable hosted chat runs (`POST /v1/chat/runs`). A run is submitted
154
+ * and persisted server-side; the executor drives the LLM/tool loop
155
+ * without requiring the client to stay connected. Clients can
156
+ * disconnect and reattach via SSE `Last-Event-ID` replay or fetch
157
+ * the final snapshot.
158
+ *
159
+ * - `create` returns the persisted run record immediately (202).
160
+ * - `get` reads the current run state.
161
+ * - `cancel` flips the run to `cancelled` and aborts in-flight work.
162
+ * - `streamEvents` yields `ChatRunEvent`s via SSE.
163
+ */
164
+ runs: {
165
+ create: (params: StartChatRunParams) => Promise<ChatRunRecord>;
166
+ get: (runId: string) => Promise<ChatRunRecord>;
167
+ cancel: (runId: string, reason?: string) => Promise<ChatRunRecord>;
168
+ streamEvents: (
169
+ runId: string,
170
+ options?: StreamChatRunEventsOptions
171
+ ) => AsyncIterableIterator<ChatRunEvent>;
172
+ };
173
+
143
174
  constructor(config: ApiConfig, projects?: ProjectsApi) {
144
175
  super(config);
145
176
 
@@ -157,6 +188,12 @@ class ChatApi extends ApiGroup<ChatApiEvents> {
157
188
  this.hosted = {
158
189
  create: this.createHostedCompletion.bind(this)
159
190
  };
191
+ this.runs = {
192
+ create: this.createChatRun.bind(this),
193
+ get: this.getChatRun.bind(this),
194
+ cancel: this.cancelChatRun.bind(this),
195
+ streamEvents: this.streamChatRunEvents.bind(this)
196
+ };
160
197
 
161
198
  // Set up the tools API (requires ProjectsApi for media generation).
162
199
  // When ProjectsApi is not provided, tool execution methods will throw at runtime.
@@ -364,7 +401,7 @@ class ChatApi extends ApiGroup<ChatApiEvents> {
364
401
  {
365
402
  model: params.model,
366
403
  messages: normalizedMessages,
367
- app_source: params.app_source || params.appSource || this.client.appSource,
404
+ app_source: params.app_source ?? params.appSource ?? this.client.appSource,
368
405
  max_tokens: params.max_tokens,
369
406
  temperature: params.temperature,
370
407
  top_p: params.top_p,
@@ -374,12 +411,12 @@ class ChatApi extends ApiGroup<ChatApiEvents> {
374
411
  frequency_penalty: params.frequency_penalty,
375
412
  presence_penalty: params.presence_penalty,
376
413
  stop: params.stop,
377
- token_type: params.token_type || params.tokenType,
414
+ token_type: params.token_type ?? params.tokenType,
378
415
  tools: params.tools,
379
416
  tool_choice: params.tool_choice,
380
417
  sogni_tools: this.normalizeSogniToolsMode(params.sogni_tools),
381
418
  sogni_tool_execution: params.sogni_tool_execution,
382
- task_profile: params.taskProfile,
419
+ task_profile: params.task_profile ?? params.taskProfile,
383
420
  media_references: params.media_references ?? params.mediaReferences,
384
421
  api_media_references: params.api_media_references ?? params.apiMediaReferences,
385
422
  ...(chatTemplateKwargs && { chat_template_kwargs: chatTemplateKwargs }),
@@ -389,6 +426,164 @@ class ChatApi extends ApiGroup<ChatApiEvents> {
389
426
  );
390
427
  }
391
428
 
429
+ private async chatRunFetch(path: string, options: RequestInit = {}): Promise<Response> {
430
+ const url = new URL(path, this.client.rest.baseUrl).toString();
431
+ const authenticated = await this.client.auth.authenticateRequest(options);
432
+ return fetch(url, authenticated);
433
+ }
434
+
435
+ private async chatRunJson<T>(path: string, options: RequestInit = {}): Promise<T> {
436
+ const response = await this.chatRunFetch(path, options);
437
+ if (!response.ok) {
438
+ const text = await response.text();
439
+ let payload: Record<string, unknown> | undefined;
440
+ try {
441
+ payload = text ? (JSON.parse(text) as Record<string, unknown>) : undefined;
442
+ } catch {
443
+ payload = { message: text };
444
+ }
445
+ const message =
446
+ (payload && typeof payload.message === 'string' && payload.message) ||
447
+ response.statusText ||
448
+ `Chat run request failed with status ${response.status}`;
449
+ const err = new Error(message);
450
+ (err as { status?: number }).status = response.status;
451
+ throw err;
452
+ }
453
+ return (await response.json()) as T;
454
+ }
455
+
456
+ /**
457
+ * Submit a durable hosted chat run. Returns the persisted run record;
458
+ * the executor drives the LLM/tool loop server-side.
459
+ */
460
+ private async createChatRun(params: StartChatRunParams): Promise<ChatRunRecord> {
461
+ const body: Record<string, unknown> = {
462
+ messages: params.messages,
463
+ ...(params.tools ? { tools: params.tools } : {}),
464
+ ...(params.toolChoice !== undefined ? { tool_choice: params.toolChoice } : {}),
465
+ ...(params.model ? { model: params.model } : {}),
466
+ ...(params.sampling ? { sampling: params.sampling } : {}),
467
+ ...(params.mediaReferences ? { media_references: params.mediaReferences } : {}),
468
+ ...(params.mediaContext ? { media_context: params.mediaContext } : {}),
469
+ ...(params.maxEstimatedCapacityUnits !== undefined
470
+ ? { max_estimated_capacity_units: params.maxEstimatedCapacityUnits }
471
+ : {}),
472
+ ...(params.confirmCost !== undefined ? { confirm_cost: params.confirmCost } : {}),
473
+ ...(params.sessionId ? { session_id: params.sessionId } : {}),
474
+ ...(params.clientMessageId ? { client_message_id: params.clientMessageId } : {}),
475
+ ...(params.tokenType ? { token_type: params.tokenType } : {}),
476
+ ...(params.appSource ? { app_source: params.appSource } : {})
477
+ };
478
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
479
+ if (params.idempotencyKey) headers['Idempotency-Key'] = params.idempotencyKey;
480
+ const response = await this.chatRunJson<{
481
+ status: string;
482
+ data: { run: ChatRunRecord; idempotent?: boolean };
483
+ }>('/v1/chat/runs', {
484
+ method: 'POST',
485
+ headers,
486
+ body: JSON.stringify(body)
487
+ });
488
+ return response.data.run;
489
+ }
490
+
491
+ /** Read a persisted chat run record. */
492
+ private async getChatRun(runId: string): Promise<ChatRunRecord> {
493
+ const response = await this.chatRunJson<{ status: string; data: { run: ChatRunRecord } }>(
494
+ `/v1/chat/runs/${encodeURIComponent(runId)}`
495
+ );
496
+ return response.data.run;
497
+ }
498
+
499
+ /** Cancel an in-flight chat run. */
500
+ private async cancelChatRun(runId: string, reason?: string): Promise<ChatRunRecord> {
501
+ const response = await this.chatRunJson<{ status: string; data: { run: ChatRunRecord } }>(
502
+ `/v1/chat/runs/${encodeURIComponent(runId)}/cancel`,
503
+ {
504
+ method: 'POST',
505
+ headers: { 'Content-Type': 'application/json' },
506
+ body: JSON.stringify(reason ? { reason } : {})
507
+ }
508
+ );
509
+ return response.data.run;
510
+ }
511
+
512
+ /**
513
+ * SSE iterator over chat-run events. Honors `Last-Event-ID` for replay.
514
+ * Yields events as `ChatRunEvent`. Caller is responsible for breaking
515
+ * the loop when they see a terminal `run_*` event type they care about.
516
+ */
517
+ private async *streamChatRunEvents(
518
+ runId: string,
519
+ options: StreamChatRunEventsOptions = {}
520
+ ): AsyncIterableIterator<ChatRunEvent> {
521
+ const headers: Record<string, string> = { Accept: 'text/event-stream' };
522
+ if (options.lastEventId !== undefined && Number.isFinite(options.lastEventId)) {
523
+ headers['Last-Event-ID'] = String(options.lastEventId);
524
+ }
525
+ const response = await this.chatRunFetch(
526
+ `/v1/chat/runs/${encodeURIComponent(runId)}/events/stream`,
527
+ { headers, signal: options.signal }
528
+ );
529
+ if (!response.ok || !response.body) {
530
+ throw new Error(`Chat run event stream failed with status ${response.status}`);
531
+ }
532
+ const reader = response.body.getReader();
533
+ const decoder = new TextDecoder();
534
+ let buffer = '';
535
+
536
+ const findFrameBoundary = (source: string): { index: number; length: number } | null => {
537
+ const lf = source.indexOf('\n\n');
538
+ const crlf = source.indexOf('\r\n\r\n');
539
+ if (lf === -1 && crlf === -1) return null;
540
+ if (lf === -1) return { index: crlf, length: 4 };
541
+ if (crlf === -1 || lf < crlf) return { index: lf, length: 2 };
542
+ return { index: crlf, length: 4 };
543
+ };
544
+
545
+ const yieldFrame = (frame: string): ChatRunEvent | null => {
546
+ if (!frame.trim() || frame.startsWith(':')) return null;
547
+ let eventName = 'message';
548
+ let data = '';
549
+ for (const rawLine of frame.split(/\r?\n/)) {
550
+ if (rawLine.startsWith('event:')) eventName = rawLine.slice(6).trim();
551
+ else if (rawLine.startsWith('data:')) data += (data ? '\n' : '') + rawLine.slice(5).trim();
552
+ }
553
+ if (eventName === 'run_status' || !data) return null;
554
+ try {
555
+ return JSON.parse(data) as ChatRunEvent;
556
+ } catch {
557
+ return null;
558
+ }
559
+ };
560
+
561
+ try {
562
+ while (true) {
563
+ const { value, done } = await reader.read();
564
+ if (done) {
565
+ const remaining = buffer.trim();
566
+ if (remaining) {
567
+ const parsed = yieldFrame(remaining);
568
+ if (parsed) yield parsed;
569
+ }
570
+ return;
571
+ }
572
+ buffer += decoder.decode(value, { stream: true });
573
+ let boundary = findFrameBoundary(buffer);
574
+ while (boundary !== null) {
575
+ const frame = buffer.slice(0, boundary.index);
576
+ buffer = buffer.slice(boundary.index + boundary.length);
577
+ boundary = findFrameBoundary(buffer);
578
+ const parsed = yieldFrame(frame);
579
+ if (parsed) yield parsed;
580
+ }
581
+ }
582
+ } finally {
583
+ reader.releaseLock();
584
+ }
585
+ }
586
+
392
587
  /**
393
588
  * Send a single chat completion request (no auto tool execution).
394
589
  */