@saltcorn/large-language-model 1.0.3 → 1.0.4

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/generate.js CHANGED
@@ -18,6 +18,7 @@ const {
18
18
  streamText,
19
19
  tool,
20
20
  jsonSchema,
21
+ Output,
21
22
  embed,
22
23
  embedMany,
23
24
  experimental_transcribe,
@@ -518,6 +519,14 @@ const getCompletionAISDK = async (
518
519
  });
519
520
  });
520
521
  }
522
+ if (body.response_format?.type === "json_schema" && !body.output) {
523
+ body.output = Output.object({
524
+ schema: jsonSchema(
525
+ lockDownSchema(body.response_format.json_schema.schema),
526
+ ),
527
+ });
528
+ delete body.response_format;
529
+ }
521
530
 
522
531
  const debugRequest = { ...body, model: use_model_name };
523
532
  if (debugResult)
@@ -619,6 +628,17 @@ const getCompletionOpenAICompatible = async (
619
628
  if (tool.function.required) tool.required = tool.function.required;
620
629
  delete tool.function;
621
630
  }
631
+ if (body.response_format?.type === "json_schema" && !body.text) {
632
+ body.text = {
633
+ format: {
634
+ type: "json_schema",
635
+ name: body.response_format.json_schema.name,
636
+ //strict: true,
637
+ schema: lockDownSchema(body.response_format.json_schema.schema),
638
+ },
639
+ };
640
+ delete body.response_format;
641
+ }
622
642
  let newChat;
623
643
  if (!appendToChat) {
624
644
  newChat = [];
@@ -1148,6 +1168,99 @@ const getEmbeddingGoogleVertex = async (config, opts, oauth2Client) => {
1148
1168
  return embeddings;
1149
1169
  };
1150
1170
 
1171
+ function lockDownSchema(schema) {
1172
+ if (!schema || typeof schema !== "object") return schema;
1173
+
1174
+ // Handle arrays (e.g., allOf, oneOf, anyOf, items as array)
1175
+ if (Array.isArray(schema)) {
1176
+ schema.forEach((item) => lockDownSchema(item));
1177
+ return schema;
1178
+ }
1179
+
1180
+ // If this subschema defines properties, lock it down
1181
+ if (schema.properties) {
1182
+ schema.additionalProperties = false;
1183
+ }
1184
+
1185
+ // Recurse into properties
1186
+ if (schema.properties) {
1187
+ for (const key of Object.keys(schema.properties)) {
1188
+ lockDownSchema(schema.properties[key]);
1189
+ }
1190
+ }
1191
+
1192
+ // Recurse into additionalProperties if it's a schema (not just a boolean)
1193
+ if (
1194
+ schema.additionalProperties &&
1195
+ typeof schema.additionalProperties === "object"
1196
+ ) {
1197
+ lockDownSchema(schema.additionalProperties);
1198
+ }
1199
+
1200
+ // Recurse into patternProperties
1201
+ if (schema.patternProperties) {
1202
+ for (const key of Object.keys(schema.patternProperties)) {
1203
+ lockDownSchema(schema.patternProperties[key]);
1204
+ }
1205
+ }
1206
+
1207
+ // Recurse into composition keywords
1208
+ for (const keyword of ["allOf", "oneOf", "anyOf"]) {
1209
+ if (Array.isArray(schema[keyword])) {
1210
+ schema[keyword].forEach((sub) => lockDownSchema(sub));
1211
+ }
1212
+ }
1213
+
1214
+ // Recurse into not
1215
+ if (schema.not) {
1216
+ lockDownSchema(schema.not);
1217
+ }
1218
+
1219
+ // Recurse into if/then/else
1220
+ for (const keyword of ["if", "then", "else"]) {
1221
+ if (schema[keyword]) {
1222
+ lockDownSchema(schema[keyword]);
1223
+ }
1224
+ }
1225
+
1226
+ // Recurse into items (tuple or single schema)
1227
+ if (schema.items) {
1228
+ if (Array.isArray(schema.items)) {
1229
+ schema.items.forEach((item) => lockDownSchema(item));
1230
+ } else {
1231
+ lockDownSchema(schema.items);
1232
+ }
1233
+ }
1234
+
1235
+ // Recurse into prefixItems (Draft 2020-12)
1236
+ if (Array.isArray(schema.prefixItems)) {
1237
+ schema.prefixItems.forEach((item) => lockDownSchema(item));
1238
+ }
1239
+
1240
+ // Recurse into $defs / definitions
1241
+ for (const defsKey of ["$defs", "definitions"]) {
1242
+ if (schema[defsKey]) {
1243
+ for (const key of Object.keys(schema[defsKey])) {
1244
+ lockDownSchema(schema[defsKey][key]);
1245
+ }
1246
+ }
1247
+ }
1248
+
1249
+ // Recurse into dependentSchemas
1250
+ if (schema.dependentSchemas) {
1251
+ for (const key of Object.keys(schema.dependentSchemas)) {
1252
+ lockDownSchema(schema.dependentSchemas[key]);
1253
+ }
1254
+ }
1255
+
1256
+ // Recurse into contains
1257
+ if (schema.contains) {
1258
+ lockDownSchema(schema.contains);
1259
+ }
1260
+
1261
+ return schema;
1262
+ }
1263
+
1151
1264
  module.exports = {
1152
1265
  getCompletion,
1153
1266
  getEmbedding,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/large-language-model",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Large language models and functionality for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -11,9 +11,9 @@
11
11
  "@google-cloud/vertexai": "^1.9.3",
12
12
  "@google-cloud/aiplatform": "^3.34.0",
13
13
  "googleapis": "^144.0.0",
14
- "ai": "5.0.44",
15
- "@ai-sdk/openai": "2.0.30",
16
- "@ai-sdk/anthropic": "2.0.70",
14
+ "ai": "6.0.116",
15
+ "@ai-sdk/openai": "3.0.41",
16
+ "@ai-sdk/anthropic": "3.0.58",
17
17
  "openai": "6.16.0",
18
18
  "@elevenlabs/elevenlabs-js": "2.31.0"
19
19
  },
package/tests/llm.test.js CHANGED
@@ -29,7 +29,6 @@ for (const nameconfig of require("./configs")) {
29
29
  config,
30
30
  );
31
31
  });
32
-
33
32
  it("generates text", async () => {
34
33
  const answer = await getState().functions.llm_generate.run(
35
34
  "What is the Capital of France?",
@@ -165,6 +164,28 @@ for (const nameconfig of require("./configs")) {
165
164
 
166
165
  expect(cities1.length).toBe(12);
167
166
  });
167
+ it("uses response_format", async () => {
168
+ const answer = await getState().functions.llm_generate.run(
169
+ "Generate a list of EU capitals in JSON format",
170
+ {
171
+ response_format: {
172
+ type: "json_schema",
173
+ json_schema: {
174
+ name: "cities",
175
+ schema: cities_tool.tools[0].function.parameters,
176
+ },
177
+ },
178
+ },
179
+ );
180
+ expect(typeof answer).toBe("string");
181
+ console.log("answer", answer);
182
+
183
+ const json_answer = JSON.parse(answer);
184
+
185
+ expect(json_answer.cities.length).toBe(27);
186
+ expect(!!json_answer.cities[0].city_name).toBe(true);
187
+ expect(!!json_answer.cities[0].country_name).toBe(true);
188
+ });
168
189
  if (name !== "AI SDK Anthropic")
169
190
  it("gets embedding", async () => {
170
191
  const v = await getState().functions.llm_embedding.run(
@@ -186,11 +207,13 @@ const cities_tool = {
186
207
  description: "Provide a list of cities by country and city name",
187
208
  parameters: {
188
209
  type: "object",
210
+ required: ["cities"],
189
211
  properties: {
190
212
  cities: {
191
213
  type: "array",
192
214
  items: {
193
215
  type: "object",
216
+ additionalProperties: false,
194
217
  properties: {
195
218
  country_name: {
196
219
  type: "string",