@saltcorn/large-language-model 1.0.2 → 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,
@@ -423,17 +424,20 @@ const getCompletion = async (config, opts) => {
423
424
  }
424
425
  };
425
426
 
426
- const getAiSdkModel = ({
427
- provider,
428
- api_key,
429
- model_name,
430
- anthropic_api_key,
431
- }) => {
427
+ const getAiSdkModel = (
428
+ { provider, api_key, model_name, anthropic_api_key },
429
+ isEmbedding,
430
+ ) => {
432
431
  switch (provider) {
433
432
  case "OpenAI":
434
433
  const openai = createOpenAI({ apiKey: api_key });
435
- return openai(model_name);
434
+ return isEmbedding
435
+ ? openai.textEmbeddingModel(model_name)
436
+ : openai(model_name);
437
+
436
438
  case "Anthropic":
439
+ if (isEmbedding)
440
+ throw new Error("Anthropic does not provide embedding models");
437
441
  const anthropic = createAnthropic({
438
442
  apiKey: anthropic_api_key,
439
443
  });
@@ -515,6 +519,14 @@ const getCompletionAISDK = async (
515
519
  });
516
520
  });
517
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
+ }
518
530
 
519
531
  const debugRequest = { ...body, model: use_model_name };
520
532
  if (debugResult)
@@ -616,6 +628,17 @@ const getCompletionOpenAICompatible = async (
616
628
  if (tool.function.required) tool.required = tool.function.required;
617
629
  delete tool.function;
618
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
+ }
619
642
  let newChat;
620
643
  if (!appendToChat) {
621
644
  newChat = [];
@@ -949,19 +972,17 @@ const getEmbeddingOpenAICompatible = async (
949
972
 
950
973
  const getEmbeddingAISDK = async (config, { prompt, model, debugResult }) => {
951
974
  const { provider, apiKey, embed_model } = config;
952
- let model_obj,
953
- providerOptions = {};
954
- const model_name = model || embed_model;
955
-
956
- switch (provider) {
957
- case "OpenAI":
958
- const openai = createOpenAI({ apiKey: apiKey });
959
- model_obj = openai.textEmbeddingModel(
960
- model_name || "text-embedding-3-small",
961
- );
962
- //providerOptions.openai = {};
963
- break;
964
- }
975
+ let providerOptions = {};
976
+ const model_name = model || embed_model || "text-embedding-3-small";
977
+ let model_obj = getAiSdkModel(
978
+ {
979
+ ...config,
980
+ model_name,
981
+ api_key: apiKey,
982
+ provider,
983
+ },
984
+ true,
985
+ );
965
986
  const body = {
966
987
  model: model_obj,
967
988
  providerOptions,
@@ -1147,6 +1168,99 @@ const getEmbeddingGoogleVertex = async (config, opts, oauth2Client) => {
1147
1168
  return embeddings;
1148
1169
  };
1149
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
+
1150
1264
  module.exports = {
1151
1265
  getCompletion,
1152
1266
  getEmbedding,
package/index.js CHANGED
@@ -126,7 +126,7 @@ ${domReady(`
126
126
  label: "Embedding model", //gpt-3.5-turbo
127
127
  type: "String",
128
128
  required: true,
129
- showIf: { backend: "AI SDK" },
129
+ showIf: { backend: "AI SDK", ai_sdk_provider: ["OpenAI"] },
130
130
  attributes: {
131
131
  calcOptions: [
132
132
  "ai_sdk_provider",
@@ -136,14 +136,6 @@ ${domReady(`
136
136
  "text-embedding-3-large",
137
137
  "text-embedding-ada-002",
138
138
  ],
139
- Anthropic: [
140
- "voyage-3-large",
141
- "voyage-3",
142
- "voyage-3-lite",
143
- "voyage-code-3",
144
- "voyage-finance-2",
145
- "voyage-law-2",
146
- ],
147
139
  },
148
140
  ],
149
141
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/large-language-model",
3
- "version": "1.0.2",
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/configs.js CHANGED
@@ -36,7 +36,6 @@ module.exports = [
36
36
  model: "claude-sonnet-4-6",
37
37
  api_key: process.env.ANTHROPIC_API_KEY,
38
38
  backend: "AI SDK",
39
- embed_model: "text-embedding-3-small",
40
39
  image_model: "gpt-image-1",
41
40
  temperature: 0.7,
42
41
  ai_sdk_provider: "Anthropic",
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,37 @@ 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
+ });
189
+ if (name !== "AI SDK Anthropic")
190
+ it("gets embedding", async () => {
191
+ const v = await getState().functions.llm_embedding.run(
192
+ "The quick brown fox jumps over the lazy dog",
193
+ );
194
+ expect(Array.isArray(v)).toBe(true);
195
+ expect(v.length).toBeGreaterThan(50);
196
+ expect(typeof v[0]).toBe("number");
197
+ });
168
198
  });
169
199
  }
170
200
 
@@ -177,11 +207,13 @@ const cities_tool = {
177
207
  description: "Provide a list of cities by country and city name",
178
208
  parameters: {
179
209
  type: "object",
210
+ required: ["cities"],
180
211
  properties: {
181
212
  cities: {
182
213
  type: "array",
183
214
  items: {
184
215
  type: "object",
216
+ additionalProperties: false,
185
217
  properties: {
186
218
  country_name: {
187
219
  type: "string",