@bedrockio/ai 0.3.0 → 0.4.2

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 (60) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +58 -17
  4. package/__mocks__/@anthropic-ai/sdk.js +16 -22
  5. package/__mocks__/@google/generative-ai.js +1 -1
  6. package/__mocks__/openai.js +33 -28
  7. package/dist/cjs/BaseClient.js +242 -182
  8. package/dist/cjs/anthropic.js +115 -93
  9. package/dist/cjs/google.js +74 -80
  10. package/dist/cjs/index.js +23 -75
  11. package/dist/cjs/openai.js +114 -72
  12. package/dist/cjs/package.json +1 -0
  13. package/dist/cjs/utils/code.js +11 -0
  14. package/dist/cjs/utils/json.js +53 -0
  15. package/dist/cjs/utils/templates.js +83 -0
  16. package/dist/cjs/xai.js +11 -20
  17. package/dist/esm/BaseClient.js +243 -0
  18. package/dist/esm/anthropic.js +116 -0
  19. package/dist/esm/google.js +75 -0
  20. package/dist/esm/index.js +25 -0
  21. package/dist/esm/openai.js +113 -0
  22. package/dist/esm/utils/code.js +8 -0
  23. package/dist/esm/utils/json.js +50 -0
  24. package/dist/esm/utils/templates.js +76 -0
  25. package/dist/esm/xai.js +10 -0
  26. package/eslint.config.js +2 -0
  27. package/package.json +18 -17
  28. package/src/BaseClient.js +233 -140
  29. package/src/anthropic.js +96 -56
  30. package/src/google.js +3 -6
  31. package/src/index.js +6 -54
  32. package/src/openai.js +96 -33
  33. package/src/utils/code.js +9 -0
  34. package/src/utils/json.js +58 -0
  35. package/src/utils/templates.js +87 -0
  36. package/src/xai.js +2 -9
  37. package/tsconfig.cjs.json +8 -0
  38. package/tsconfig.esm.json +8 -0
  39. package/tsconfig.types.json +9 -0
  40. package/types/BaseClient.d.ts +67 -26
  41. package/types/BaseClient.d.ts.map +1 -1
  42. package/types/anthropic.d.ts +26 -2
  43. package/types/anthropic.d.ts.map +1 -1
  44. package/types/google.d.ts.map +1 -1
  45. package/types/index.d.ts +4 -11
  46. package/types/index.d.ts.map +1 -1
  47. package/types/openai.d.ts +45 -2
  48. package/types/openai.d.ts.map +1 -1
  49. package/types/utils/code.d.ts +2 -0
  50. package/types/utils/code.d.ts.map +1 -0
  51. package/types/utils/json.d.ts +2 -0
  52. package/types/utils/json.d.ts.map +1 -0
  53. package/types/utils/templates.d.ts +3 -0
  54. package/types/utils/templates.d.ts.map +1 -0
  55. package/types/utils.d.ts +4 -0
  56. package/types/utils.d.ts.map +1 -0
  57. package/types/xai.d.ts.map +1 -1
  58. package/vitest.config.js +10 -0
  59. package/dist/cjs/util.js +0 -62
  60. package/src/util.js +0 -60
@@ -0,0 +1,10 @@
1
+ import { OpenAiClient } from './openai.js';
2
+ export class XAiClient extends OpenAiClient {
3
+ static DEFAULT_MODEL = 'grok-4-fast';
4
+ constructor(options) {
5
+ super({
6
+ ...options,
7
+ baseURL: 'https://api.x.ai/v1',
8
+ });
9
+ }
10
+ }
@@ -0,0 +1,2 @@
1
+ import { nodeImports, recommended } from '@bedrockio/eslint-plugin';
2
+ export default [recommended, nodeImports];
package/package.json CHANGED
@@ -1,20 +1,24 @@
1
1
  {
2
2
  "name": "@bedrockio/ai",
3
- "version": "0.3.0",
3
+ "version": "0.4.2",
4
4
  "description": "Bedrock wrapper for common AI chatbots.",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "test": "node --no-warnings node_modules/.bin/jest",
8
- "types": "tsc",
7
+ "test": "vitest run",
8
+ "test:live": "node --env-file=.env.live ./node_modules/.bin/vitest run",
9
9
  "lint": "eslint",
10
10
  "build": "scripts/build",
11
11
  "eject": "scripts/eject",
12
- "prepublish": "yarn build && yarn types"
12
+ "build:cjs": "tsc -p tsconfig.cjs.json",
13
+ "build:esm": "tsc -p tsconfig.esm.json && tsc-alias -f -p tsconfig.esm.json",
14
+ "build:types": "tsc -p tsconfig.types.json",
15
+ "prepublish": "yarn build"
13
16
  },
14
17
  "types": "types/index.d.ts",
15
18
  "main": "./dist/cjs/index.js",
16
19
  "exports": {
17
20
  ".": {
21
+ "types": "./types/index.d.ts",
18
22
  "import": "./src/index.js",
19
23
  "require": "./dist/cjs/index.js"
20
24
  }
@@ -31,27 +35,24 @@
31
35
  "url": "https://github.com/bedrockio/router"
32
36
  },
33
37
  "dependencies": {
34
- "@anthropic-ai/sdk": "^0.36.3",
38
+ "@anthropic-ai/sdk": "^0.65.0",
35
39
  "@google/generative-ai": "^0.21.0",
36
40
  "glob": "^11.0.1",
37
41
  "mustache": "^4.2.0",
38
- "openai": "^4.83.0"
42
+ "openai": "^6.1.0",
43
+ "partial-json": "^0.1.7"
39
44
  },
40
45
  "devDependencies": {
41
- "@babel/cli": "^7.26.4",
42
- "@babel/core": "^7.26.0",
43
- "@babel/eslint-parser": "^7.26.5",
44
- "@babel/preset-env": "^7.26.0",
46
+ "@bedrockio/eslint-plugin": "^1.2.2",
45
47
  "@bedrockio/prettier-config": "^1.0.2",
46
- "eslint": "^8.33.0",
47
- "eslint-plugin-bedrock": "^1.0.27",
48
- "eslint-plugin-import": "^2.31.0",
49
- "jest": "^29.7.0",
50
- "prettier-eslint": "^16.3.0",
51
- "typescript": "^5.7.3"
48
+ "@bedrockio/yada": "^1.8.0",
49
+ "eslint": "^9.36.0",
50
+ "tsc-alias": "^1.8.16",
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^3.2.4"
52
53
  },
53
54
  "volta": {
54
- "node": "22.12.0",
55
+ "node": "22.20.0",
55
56
  "yarn": "1.22.22"
56
57
  }
57
58
  }
package/src/BaseClient.js CHANGED
@@ -1,195 +1,288 @@
1
- import Mustache from 'mustache';
2
-
3
- import { loadTemplates, loadTemplate } from './util.js';
4
-
5
- const MESSAGES_REG = /(?:^|\n)-{3,}\s*(\w+)\s*-{3,}(.*?)(?=\n-{3,}|$)/gs;
1
+ import { parseCode } from './utils/code.js';
2
+ import { createMessageExtractor } from './utils/json.js';
3
+ import { loadTemplates, renderTemplate } from './utils/templates.js';
6
4
 
7
5
  export default class BaseClient {
8
6
  constructor(options) {
9
- this.options = options;
7
+ this.options = {
8
+ // @ts-ignore
9
+ model: this.constructor.DEFAULT_MODEL,
10
+ ...options,
11
+ };
10
12
  this.templates = null;
11
13
  }
12
14
 
15
+ // Public
16
+
13
17
  /**
14
- * Interpolates vars into the provided template and
15
- * runs the chat completion. The "output" option may
16
- * be omitted and will default to `"text"`.
17
- * {@link https://github.com/bedrockio/ai?tab=readme-ov-file#bedrockioai Documentation}
18
+ * Interpolates vars into the provided template as instructions and runs the
19
+ * prompt.
18
20
  *
19
- * @param {object} options
20
- * @param {string} options.model - The model to use.
21
- * @param {"raw" | "text" | "json" | "messages"} [options.output] - The output to use.
22
- * @param {Object.<string, any>} [options.other] - Additional props
23
- * will be interpolated in the template.
21
+ * @param {PromptOptions} options
24
22
  */
25
23
  async prompt(options) {
26
- options = {
27
- ...this.options,
28
- ...options,
29
- };
24
+ options = await this.normalizeOptions(options);
30
25
 
31
- const messages = await this.getMessages(options);
32
- return await this.getCompletion({
33
- ...options,
34
- messages,
35
- });
26
+ const { input, output, stream, schema } = options;
27
+
28
+ const response = await this.runPrompt(options);
29
+
30
+ if (!stream) {
31
+ this.debug('Response:', response);
32
+ }
33
+
34
+ if (output === 'raw') {
35
+ return response;
36
+ }
37
+
38
+ let result;
39
+ if (schema) {
40
+ result = this.getStructuredResponse(response);
41
+
42
+ // @ts-ignore
43
+ if (options.hasWrappedSchema) {
44
+ result = result.items;
45
+ }
46
+ } else if (output === 'json') {
47
+ result = JSON.parse(parseCode(this.getTextResponse(response)));
48
+ } else {
49
+ result = parseCode(this.getTextResponse(response));
50
+ }
51
+
52
+ if (output === 'messages') {
53
+ return {
54
+ result,
55
+ ...this.getMessagesResponse(input, response),
56
+ };
57
+ } else {
58
+ return result;
59
+ }
36
60
  }
37
61
 
38
62
  /**
39
63
  * Streams the prompt response.
64
+ *
65
+ * @param {PromptOptions & StreamOptions} options
40
66
  * @returns {AsyncIterator}
41
67
  */
42
68
  async *stream(options) {
43
- const stream = await this.getStream(options);
69
+ options = await this.normalizeOptions(options);
44
70
 
45
- let started = false;
71
+ const extractor = this.getMessageExtractor(options);
46
72
 
47
- // @ts-ignore
48
- for await (const chunk of stream) {
49
- const resolved = this.getStreamedChunk(chunk, started);
50
- started = true;
73
+ try {
74
+ const stream = await this.runStream(options);
51
75
 
52
76
  // @ts-ignore
53
- if (resolved) {
54
- yield resolved;
77
+ for await (let event of stream) {
78
+ this.debug('Event:', event);
79
+
80
+ event = this.normalizeStreamEvent(event);
81
+
82
+ if (event) {
83
+ yield event;
84
+ }
85
+
86
+ const extractedMessages = extractor?.(event) || [];
87
+
88
+ for (let message of extractedMessages) {
89
+ const { key, delta, text, done } = message;
90
+
91
+ let extractEvent;
92
+ if (done) {
93
+ extractEvent = {
94
+ type: 'extract:done',
95
+ text,
96
+ key,
97
+ };
98
+ } else {
99
+ extractEvent = {
100
+ type: 'extract:delta',
101
+ delta,
102
+ key,
103
+ };
104
+ }
105
+
106
+ this.debug('Extract:', extractEvent);
107
+
108
+ yield extractEvent;
109
+ }
55
110
  }
111
+ } catch (error) {
112
+ const { message, code } = error;
113
+ yield {
114
+ type: 'error',
115
+ code,
116
+ message,
117
+ };
56
118
  }
57
119
  }
58
120
 
59
- async getMessages(options) {
60
- const { text } = options;
121
+ async buildTemplate(options) {
61
122
  const template = await this.resolveTemplate(options);
123
+ return renderTemplate(template, options);
124
+ }
62
125
 
63
- if (template) {
64
- const raw = render(template, options);
126
+ // Protected
65
127
 
66
- const messages = [];
67
- for (let match of raw.matchAll(MESSAGES_REG)) {
68
- const [, role, content] = match;
69
- messages.push({
70
- role: role.toLowerCase(),
71
- content: content.trim(),
72
- });
73
- }
128
+ runPrompt(options) {
129
+ void options;
130
+ throw new Error('Method not implemented.');
131
+ }
74
132
 
75
- if (!messages.length) {
76
- messages.push({
77
- role: 'user',
78
- content: raw.trim(),
79
- });
133
+ runStream(options) {
134
+ void options;
135
+ throw new Error('Method not implemented.');
136
+ }
137
+
138
+ getTextResponse(response) {
139
+ void response;
140
+ throw new Error('Method not implemented.');
141
+ }
142
+
143
+ /**
144
+ * @returns {Object}
145
+ */
146
+ getStructuredResponse(response) {
147
+ void response;
148
+ throw new Error('Method not implemented.');
149
+ }
150
+
151
+ /**
152
+ * @returns {Object}
153
+ */
154
+ getMessagesResponse(input, response) {
155
+ void response;
156
+ throw new Error('Method not implemented.');
157
+ }
158
+
159
+ /**
160
+ * @returns {Object}
161
+ */
162
+ normalizeStreamEvent(event) {
163
+ void event;
164
+ throw new Error('Method not implemented.');
165
+ }
166
+
167
+ // Private
168
+
169
+ async normalizeOptions(options) {
170
+ options = {
171
+ input: '',
172
+ output: 'text',
173
+ ...this.options,
174
+ ...options,
175
+ };
176
+
177
+ options.input = this.normalizeInput(options);
178
+ options.schema = this.normalizeSchema(options);
179
+ options.instructions ||= await this.resolveInstructions(options);
180
+
181
+ return options;
182
+ }
183
+
184
+ normalizeInput(options) {
185
+ let { input = '', output } = options;
186
+
187
+ if (typeof input === 'string') {
188
+ if (output === 'json') {
189
+ input += '\nOutput only valid JSON.';
80
190
  }
81
191
 
82
- return messages;
83
- } else if (text) {
84
- return [
192
+ input = [
85
193
  {
86
194
  role: 'user',
87
- content: text,
195
+ content: input,
88
196
  },
89
197
  ];
90
- } else {
91
- throw new Error('No input provided.');
92
198
  }
93
- }
94
199
 
95
- async buildTemplate(options) {
96
- const template = await this.resolveTemplate(options);
97
- return render(template, options);
200
+ return input;
98
201
  }
99
202
 
100
- async loadTemplates() {
101
- const { templates } = this.options;
102
- this.templates ||= await loadTemplates(templates);
103
- }
203
+ normalizeSchema(options) {
204
+ let { schema } = options;
104
205
 
105
- async resolveTemplate(options) {
106
- const { template, file } = options;
107
- if (template) {
108
- return template;
109
- } else if (file?.endsWith('.md')) {
110
- return await loadTemplate(file);
111
- } else if (file) {
112
- await this.loadTemplates();
113
- return this.templates[file];
206
+ if (!schema) {
207
+ return;
114
208
  }
115
- }
116
209
 
117
- async getStream(options) {
118
- return await this.prompt({
119
- ...options,
120
- output: 'raw',
121
- stream: true,
122
- });
123
- }
210
+ // Convert to JSON schema.
211
+ schema = schema.toJSON?.() || schema;
124
212
 
125
- getCompletion(options) {
126
- void options;
127
- new Error('Method not implemented.');
128
- }
213
+ if (schema?.type === 'array') {
214
+ schema = {
215
+ type: 'object',
216
+ properties: {
217
+ items: schema,
218
+ },
219
+ required: ['items'],
220
+ additionalProperties: false,
221
+ };
222
+ options.hasWrappedSchema = true;
223
+ }
129
224
 
130
- getStreamedChunk(chunk, started) {
131
- void chunk;
132
- void started;
133
- new Error('Method not implemented.');
225
+ return schema;
134
226
  }
135
- }
136
227
 
137
- function render(template, options) {
138
- let params = {
139
- ...options,
140
- ...options.params,
141
- };
228
+ getMessageExtractor(options) {
229
+ const { extractMessages } = options;
230
+ if (!extractMessages) {
231
+ return;
232
+ }
233
+ const messageExtractor = createMessageExtractor([extractMessages]);
234
+ return (event) => {
235
+ if (event?.type === 'delta') {
236
+ return messageExtractor(event.delta);
237
+ }
238
+ };
239
+ }
142
240
 
143
- params = mapObjects(params);
144
- params = wrapProxy(params);
145
- return Mustache.render(template, params);
146
- }
241
+ debug(message, arg) {
242
+ if (this.options.debug) {
243
+ // TODO: replace with logger when opentelemetry is removed
244
+ // eslint-disable-next-line
245
+ console.debug(`${message}\n${JSON.stringify(arg, null, 2)}\n`);
246
+ }
247
+ }
147
248
 
148
- // Transform arrays and object to versions
149
- // that are more understandable in the context
150
- // of a template that may have meaningful whitespace.
151
- function mapObjects(params) {
152
- const result = {};
153
- for (let [key, value] of Object.entries(params)) {
154
- if (Array.isArray(value)) {
155
- value = mapArray(value);
156
- } else if (typeof value === 'object') {
157
- value = JSON.stringify(value, null, 2);
249
+ async resolveInstructions(options) {
250
+ if (options.template) {
251
+ const template = await this.resolveTemplate(options);
252
+ return renderTemplate(template, options);
158
253
  }
159
- result[key] = value;
160
254
  }
161
- return result;
162
- }
163
255
 
164
- function mapArray(arr) {
165
- // Only map simple arrays of primitives.
166
- if (typeof arr[0] === 'string') {
167
- arr = arr
168
- .map((el) => {
169
- return `- ${el}`;
170
- })
171
- .join('\n');
256
+ async resolveTemplate(options) {
257
+ const { template } = options;
258
+ await this.loadTemplates();
259
+ return this.templates[template] || template;
172
260
  }
173
- return arr;
174
- }
175
261
 
176
- // Wrap params with a proxy object that reports
177
- // as having all properties. If one is accessed
178
- // that does not exist then return the original
179
- // token. This way templates can be partially
180
- // interpolated and re-interpolated later.
181
- function wrapProxy(params) {
182
- return new Proxy(params, {
183
- has() {
184
- return true;
185
- },
186
-
187
- get(target, prop) {
188
- if (prop in target) {
189
- return target[prop];
190
- } else {
191
- return `{{{${prop.toString()}}}}`;
192
- }
193
- },
194
- });
262
+ async loadTemplates() {
263
+ const { templates } = this.options;
264
+ this.templates ||= await loadTemplates(templates);
265
+ }
195
266
  }
267
+
268
+ /**
269
+ * @typedef {Object} PromptOptions
270
+ * @property {string|PromptMessage[]} input - Input to use.
271
+ * @property {string} [model] - The model to use.
272
+ * @property {boolean} stream - Stream response.
273
+ * @property {Object} [schema] - A JSON schema compatible object that defines the output shape.
274
+ * @property {"raw" | "text" | "json" | "messages"} [output] - The return value type.
275
+ * @property {Object} [params] - Params to be interpolated into the template.
276
+ * May also be passed as additional props to options.
277
+ */
278
+
279
+ /**
280
+ * @typedef {Object} StreamOptions
281
+ * @property {string} [extractMessages] - Key in JSON response to extract a message stream from.
282
+ */
283
+
284
+ /**
285
+ * @typedef {Object} PromptMessage
286
+ * @property {"system" | "user" | "assistant"} role
287
+ * @property {string} content
288
+ */