@saltcorn/large-language-model 1.0.3 → 1.0.5

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)
@@ -611,13 +620,28 @@ const getCompletionOpenAICompatible = async (
611
620
  }
612
621
  if (responses_api) {
613
622
  delete body.tool_choice;
614
- for (const tool of body.tools || []) {
615
- if (tool.type !== "function" || !tool.function) continue;
616
- tool.name = tool.function.name;
617
- tool.description = tool.function.description;
618
- tool.parameters = tool.function.parameters;
619
- if (tool.function.required) tool.required = tool.function.required;
620
- delete tool.function;
623
+ if (body.tools) {
624
+ const newtools = JSON.parse(JSON.stringify(body.tools))
625
+ for (const tool of newtools) {
626
+ if (tool.type !== "function" || !tool.function) continue;
627
+ tool.name = tool.function.name;
628
+ tool.description = tool.function.description;
629
+ tool.parameters = tool.function.parameters;
630
+ if (tool.function.required) tool.required = tool.function.required;
631
+ delete tool.function;
632
+ }
633
+ body.tools = newtools
634
+ }
635
+ if (body.response_format?.type === "json_schema" && !body.text) {
636
+ body.text = {
637
+ format: {
638
+ type: "json_schema",
639
+ name: body.response_format.json_schema.name,
640
+ //strict: true,
641
+ schema: lockDownSchema(body.response_format.json_schema.schema),
642
+ },
643
+ };
644
+ delete body.response_format;
621
645
  }
622
646
  let newChat;
623
647
  if (!appendToChat) {
@@ -1148,6 +1172,99 @@ const getEmbeddingGoogleVertex = async (config, opts, oauth2Client) => {
1148
1172
  return embeddings;
1149
1173
  };
1150
1174
 
1175
+ function lockDownSchema(schema) {
1176
+ if (!schema || typeof schema !== "object") return schema;
1177
+
1178
+ // Handle arrays (e.g., allOf, oneOf, anyOf, items as array)
1179
+ if (Array.isArray(schema)) {
1180
+ schema.forEach((item) => lockDownSchema(item));
1181
+ return schema;
1182
+ }
1183
+
1184
+ // If this subschema defines properties, lock it down
1185
+ if (schema.properties) {
1186
+ schema.additionalProperties = false;
1187
+ }
1188
+
1189
+ // Recurse into properties
1190
+ if (schema.properties) {
1191
+ for (const key of Object.keys(schema.properties)) {
1192
+ lockDownSchema(schema.properties[key]);
1193
+ }
1194
+ }
1195
+
1196
+ // Recurse into additionalProperties if it's a schema (not just a boolean)
1197
+ if (
1198
+ schema.additionalProperties &&
1199
+ typeof schema.additionalProperties === "object"
1200
+ ) {
1201
+ lockDownSchema(schema.additionalProperties);
1202
+ }
1203
+
1204
+ // Recurse into patternProperties
1205
+ if (schema.patternProperties) {
1206
+ for (const key of Object.keys(schema.patternProperties)) {
1207
+ lockDownSchema(schema.patternProperties[key]);
1208
+ }
1209
+ }
1210
+
1211
+ // Recurse into composition keywords
1212
+ for (const keyword of ["allOf", "oneOf", "anyOf"]) {
1213
+ if (Array.isArray(schema[keyword])) {
1214
+ schema[keyword].forEach((sub) => lockDownSchema(sub));
1215
+ }
1216
+ }
1217
+
1218
+ // Recurse into not
1219
+ if (schema.not) {
1220
+ lockDownSchema(schema.not);
1221
+ }
1222
+
1223
+ // Recurse into if/then/else
1224
+ for (const keyword of ["if", "then", "else"]) {
1225
+ if (schema[keyword]) {
1226
+ lockDownSchema(schema[keyword]);
1227
+ }
1228
+ }
1229
+
1230
+ // Recurse into items (tuple or single schema)
1231
+ if (schema.items) {
1232
+ if (Array.isArray(schema.items)) {
1233
+ schema.items.forEach((item) => lockDownSchema(item));
1234
+ } else {
1235
+ lockDownSchema(schema.items);
1236
+ }
1237
+ }
1238
+
1239
+ // Recurse into prefixItems (Draft 2020-12)
1240
+ if (Array.isArray(schema.prefixItems)) {
1241
+ schema.prefixItems.forEach((item) => lockDownSchema(item));
1242
+ }
1243
+
1244
+ // Recurse into $defs / definitions
1245
+ for (const defsKey of ["$defs", "definitions"]) {
1246
+ if (schema[defsKey]) {
1247
+ for (const key of Object.keys(schema[defsKey])) {
1248
+ lockDownSchema(schema[defsKey][key]);
1249
+ }
1250
+ }
1251
+ }
1252
+
1253
+ // Recurse into dependentSchemas
1254
+ if (schema.dependentSchemas) {
1255
+ for (const key of Object.keys(schema.dependentSchemas)) {
1256
+ lockDownSchema(schema.dependentSchemas[key]);
1257
+ }
1258
+ }
1259
+
1260
+ // Recurse into contains
1261
+ if (schema.contains) {
1262
+ lockDownSchema(schema.contains);
1263
+ }
1264
+
1265
+ return schema;
1266
+ }
1267
+
1151
1268
  module.exports = {
1152
1269
  getCompletion,
1153
1270
  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.5",
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
  },
@@ -21,7 +21,7 @@
21
21
  "jest": "^29.7.0"
22
22
  },
23
23
  "scripts": {
24
- "test": "jest tests --runInBand"
24
+ "test": "jest tests --runInBand --verbose"
25
25
  },
26
26
  "author": "Tom Nielsen",
27
27
  "license": "MIT",
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?",
@@ -98,7 +97,7 @@ for (const nameconfig of require("./configs")) {
98
97
  });
99
98
  it("uses tools", async () => {
100
99
  const answer = await getState().functions.llm_generate.run(
101
- "Generate a list of EU capitals in a structured format using the provided tool",
100
+ "Generate a list of all the EU capitals in a structured format using the provided tool",
102
101
  cities_tool,
103
102
  );
104
103
  expect(typeof answer).toBe("object");
@@ -132,7 +131,7 @@ for (const nameconfig of require("./configs")) {
132
131
  it("tool use sequence", async () => {
133
132
  const chat = [];
134
133
  const answer = await getState().functions.llm_generate.run(
135
- "Generate a list of EU capitals in a structured format using the provided tool",
134
+ "Generate a list of all the EU capitals in a structured format using the provided tool",
136
135
  {
137
136
  chat,
138
137
  appendToChat: true,
@@ -165,6 +164,27 @@ 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 all the 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
+
182
+ const json_answer = JSON.parse(answer);
183
+
184
+ expect(json_answer.cities.length).toBe(27);
185
+ expect(!!json_answer.cities[0].city_name).toBe(true);
186
+ expect(!!json_answer.cities[0].country_name).toBe(true);
187
+ });
168
188
  if (name !== "AI SDK Anthropic")
169
189
  it("gets embedding", async () => {
170
190
  const v = await getState().functions.llm_embedding.run(
@@ -186,11 +206,13 @@ const cities_tool = {
186
206
  description: "Provide a list of cities by country and city name",
187
207
  parameters: {
188
208
  type: "object",
209
+ required: ["cities"],
189
210
  properties: {
190
211
  cities: {
191
212
  type: "array",
192
213
  items: {
193
214
  type: "object",
215
+ additionalProperties: false,
194
216
  properties: {
195
217
  country_name: {
196
218
  type: "string",