@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
package/src/anthropic.js CHANGED
@@ -1,17 +1,15 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
 
3
3
  import BaseClient from './BaseClient.js';
4
- import { transformResponse } from './util.js';
5
4
 
6
- const MODELS_URL = 'https://docs.anthropic.com/en/docs/about-claude/models';
7
- const DEFAULT_MODEL = 'claude-3-5-sonnet-latest';
5
+ const DEFAULT_TOKENS = 4096;
8
6
 
9
7
  export class AnthropicClient extends BaseClient {
8
+ static DEFAULT_MODEL = 'claude-sonnet-4-5';
9
+
10
10
  constructor(options) {
11
11
  super(options);
12
- this.client = new Anthropic({
13
- ...options,
14
- });
12
+ this.client = new Anthropic(options);
15
13
  }
16
14
 
17
15
  /**
@@ -23,75 +21,117 @@ export class AnthropicClient extends BaseClient {
23
21
  return data.map((o) => o.id);
24
22
  }
25
23
 
26
- async getCompletion(options) {
24
+ async runPrompt(options) {
27
25
  const {
28
- model = DEFAULT_MODEL,
29
- max_tokens = 2048,
30
- output = 'text',
26
+ input,
27
+ model,
28
+ temperature,
29
+ instructions,
31
30
  stream = false,
32
- messages,
31
+ tokens = DEFAULT_TOKENS,
33
32
  } = options;
34
- const { client } = this;
35
33
 
36
- const { system, user } = splitMessages(messages);
37
-
38
- if (!model) {
39
- throw new Error(
40
- `No model specified. Available models are here: ${MODELS_URL}.`,
41
- );
42
- }
43
-
44
- const response = await client.messages.create({
45
- max_tokens,
46
- messages: user,
47
- system,
34
+ // @ts-ignore
35
+ return await this.client.messages.create({
48
36
  model,
49
37
  stream,
38
+ temperature,
39
+ max_tokens: tokens,
40
+ system: instructions,
41
+ ...this.getSchemaOptions(options),
42
+ messages: input,
50
43
  });
44
+ }
51
45
 
52
- if (output === 'raw') {
53
- return response;
54
- }
46
+ async runStream(options) {
47
+ return await this.runPrompt({
48
+ ...options,
49
+ output: 'raw',
50
+ stream: true,
51
+ });
52
+ }
55
53
 
56
- // @ts-ignore
57
- const message = response.content[0];
54
+ getTextResponse(response) {
55
+ const textBlock = response.content.find((block) => {
56
+ return block.type === 'text';
57
+ });
58
+ return textBlock?.text || null;
59
+ }
58
60
 
59
- return transformResponse({
60
- ...options,
61
- messages,
62
- message,
61
+ getStructuredResponse(response) {
62
+ const toolBlock = response.content.find((block) => {
63
+ return block.type === 'tool_use';
63
64
  });
65
+ return toolBlock?.input || null;
64
66
  }
65
67
 
66
- getStreamedChunk(chunk) {
67
- // @ts-ignore
68
- let type;
69
- if (chunk.type === 'content_block_start') {
70
- type = 'start';
71
- } else if (chunk.type === 'content_block_delta') {
72
- type = 'chunk';
73
- } else if (chunk.type === 'message_stop') {
74
- type = 'stop';
75
- }
68
+ getMessagesResponse(input, response) {
69
+ return {
70
+ messages: [
71
+ ...input,
72
+ ...response.content
73
+ .filter((item) => {
74
+ return item.type === 'text';
75
+ })
76
+ .map((item) => {
77
+ return {
78
+ role: 'assistant',
79
+ content: item.text,
80
+ };
81
+ }),
82
+ ],
83
+ };
84
+ }
76
85
 
77
- if (type) {
86
+ normalizeStreamEvent(event) {
87
+ let { type } = event;
88
+ if (type === 'content_block_start') {
89
+ return {
90
+ type: 'start',
91
+ };
92
+ } else if (type === 'content_block_stop') {
78
93
  return {
79
- type,
80
- text: chunk.delta?.text || '',
94
+ type: 'stop',
95
+ };
96
+ } else if (type === 'content_block_delta') {
97
+ return {
98
+ type: 'delta',
99
+ text: event.delta.text,
81
100
  };
82
101
  }
83
102
  }
84
- }
85
103
 
86
- function splitMessages(messages) {
87
- const system = [];
88
- const user = [];
89
- for (let message of messages) {
90
- if (message.role === 'system') {
91
- system.push(message);
92
- } else {
93
- user.push(message);
104
+ // Private
105
+
106
+ getSchemaOptions(options) {
107
+ const { output } = options;
108
+ if (output?.type) {
109
+ let schema = output;
110
+
111
+ if (schema.type === 'array') {
112
+ schema = {
113
+ type: 'object',
114
+ properties: {
115
+ items: schema,
116
+ },
117
+ required: ['items'],
118
+ additionalProperties: false,
119
+ };
120
+ }
121
+
122
+ return {
123
+ tools: [
124
+ {
125
+ name: 'schema',
126
+ description: 'Follow the schema for JSON output.',
127
+ input_schema: schema,
128
+ },
129
+ ],
130
+ tool_choice: {
131
+ type: 'tool',
132
+ name: 'schema',
133
+ },
134
+ };
94
135
  }
95
136
  }
96
- return { system: system.join('\n'), user };
97
137
  }
package/src/google.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { GoogleGenerativeAI } from '@google/generative-ai';
2
2
 
3
3
  import BaseClient from './BaseClient.js';
4
- import { transformResponse } from './util.js';
5
4
 
6
5
  const DEFAULT_MODEL = 'models/gemini-2.0-flash-exp';
7
6
 
@@ -33,6 +32,7 @@ export class GoogleClient extends BaseClient {
33
32
  model,
34
33
  });
35
34
 
35
+ // @ts-ignore
36
36
  const messages = await this.getMessages(options);
37
37
 
38
38
  const prompts = messages.map((message) => {
@@ -57,13 +57,10 @@ export class GoogleClient extends BaseClient {
57
57
  });
58
58
  const [message] = parts;
59
59
 
60
- return transformResponse({
61
- ...options,
62
- messages,
63
- message,
64
- });
60
+ return message;
65
61
  }
66
62
  async getStream(options) {
63
+ // @ts-ignore
67
64
  const response = await super.getStream(options);
68
65
  // @ts-ignore
69
66
  return response.stream;
package/src/index.js CHANGED
@@ -1,63 +1,15 @@
1
- import { OpenAiClient } from './openai.js';
2
- import { GoogleClient } from './google.js';
3
1
  import { AnthropicClient } from './anthropic.js';
2
+ import { GoogleClient } from './google.js';
3
+ import { OpenAiClient } from './openai.js';
4
4
  import { XAiClient } from './xai.js';
5
5
 
6
- export class Client {
7
- constructor(options = {}) {
8
- if (!options.platform) {
9
- throw new Error('No platform specified.');
10
- } else if (!options.templates) {
11
- throw new Error('No templates directory specified.');
12
- } else if (!options.apiKey) {
13
- throw new Error('No API key specified.');
14
- }
15
- return getClientForPlatform(options);
16
- }
17
- }
18
-
19
- export class MultiClient {
20
- constructor(options) {
21
- const { platforms } = options;
22
-
23
- this.clients = {};
24
-
25
- for (let platform of platforms) {
26
- const { name, apiKey } = platform;
27
- const client = getClientForPlatform({
28
- ...options,
29
- platform: name,
30
- apiKey,
31
- });
32
- this.clients[name] = client;
33
- this.clients[undefined] ||= client;
34
- }
35
- }
36
-
37
- prompt(options) {
38
- return this.getClient(options).prompt(options);
39
- }
40
-
41
- stream(options) {
42
- return this.getClient(options).stream(options);
43
- }
44
-
45
- buildTemplate(options) {
46
- return this.getClient(options).buildTemplate(options);
47
- }
6
+ export function createClient(options = {}) {
7
+ const { platform } = options;
48
8
 
49
- getClient(options) {
50
- const { platform } = options;
51
- const client = this.clients[platform];
52
- if (!client) {
53
- throw new Error(`Platform "${platform}" not found.`);
54
- }
55
- return client;
9
+ if (!platform) {
10
+ throw new Error('No platform specified.');
56
11
  }
57
- }
58
12
 
59
- function getClientForPlatform(options) {
60
- const { platform } = options;
61
13
  if (platform === 'openai' || platform === 'gpt') {
62
14
  return new OpenAiClient(options);
63
15
  } else if (platform === 'google' || platform === 'gemini') {
package/src/openai.js CHANGED
@@ -1,16 +1,13 @@
1
1
  import OpenAI from 'openai';
2
2
 
3
3
  import BaseClient from './BaseClient.js';
4
- import { transformResponse } from './util.js';
5
-
6
- const DEFAULT_MODEL = 'gpt-4o-mini';
7
4
 
8
5
  export class OpenAiClient extends BaseClient {
6
+ static DEFAULT_MODEL = 'gpt-5-nano';
7
+
9
8
  constructor(options) {
10
9
  super(options);
11
- this.client = new OpenAI({
12
- ...options,
13
- });
10
+ this.client = new OpenAI(options);
14
11
  }
15
12
 
16
13
  /**
@@ -22,49 +19,115 @@ export class OpenAiClient extends BaseClient {
22
19
  return data.map((o) => o.id);
23
20
  }
24
21
 
25
- async getCompletion(options) {
26
- const { model = DEFAULT_MODEL, output = 'text', stream = false } = options;
27
- const { client } = this;
22
+ async runPrompt(options) {
23
+ const {
24
+ input,
25
+ model,
26
+ tools,
27
+ verbosity,
28
+ temperature,
29
+ instructions,
30
+ prevResponseId,
31
+ stream = false,
32
+ } = options;
28
33
 
29
- const messages = await this.getMessages(options);
30
- const response = await client.chat.completions.create({
34
+ const params = {
31
35
  model,
32
- messages,
36
+ input,
37
+ tools,
33
38
  stream,
34
- response_format: {
35
- type: output === 'json' ? 'json_object' : 'text',
39
+ temperature,
40
+ instructions,
41
+ previous_response_id: prevResponseId,
42
+
43
+ text: {
44
+ format: this.getOutputFormat(options),
45
+ verbosity,
36
46
  },
37
- });
47
+ };
38
48
 
39
- if (output === 'raw') {
40
- return response;
41
- }
49
+ this.debug('Params:', params);
42
50
 
43
- const { message } = response.choices[0];
51
+ // @ts-ignore
52
+ return await this.client.responses.create(params);
53
+ }
44
54
 
45
- return transformResponse({
55
+ async runStream(options) {
56
+ return await this.runPrompt({
46
57
  ...options,
47
- messages,
48
- message,
58
+ stream: true,
49
59
  });
50
60
  }
51
61
 
52
- getStreamedChunk(chunk, started) {
53
- const [choice] = chunk.choices;
62
+ getTextResponse(response) {
63
+ return response.output_text;
64
+ }
65
+
66
+ getStructuredResponse(response) {
67
+ return JSON.parse(response.output_text);
68
+ }
69
+
70
+ getMessagesResponse(input, response) {
71
+ return {
72
+ messages: [
73
+ ...input,
74
+ {
75
+ role: 'assistant',
76
+ content: response.output_text,
77
+ },
78
+ ],
79
+ // Note that this ability currently only
80
+ // exists for OpenAI compatible providers.
81
+ prevResponseId: response.id,
82
+ };
83
+ }
84
+
85
+ // Private
54
86
 
55
- let type;
56
- if (!started) {
57
- type = 'start';
58
- } else if (choice.finish_reason === 'stop') {
59
- type = 'stop';
87
+ getOutputFormat(options) {
88
+ let { output, schema } = options;
89
+ if (output === 'json') {
90
+ return {
91
+ type: 'json_object',
92
+ };
93
+ } else if (schema) {
94
+ return {
95
+ type: 'json_schema',
96
+ // Name is required but arbitrary.
97
+ name: 'schema',
98
+ strict: true,
99
+ schema,
100
+ };
60
101
  } else {
61
- type = 'chunk';
102
+ return {
103
+ type: 'text',
104
+ };
62
105
  }
106
+ }
63
107
 
64
- if (type) {
108
+ normalizeStreamEvent(event) {
109
+ const { type } = event;
110
+
111
+ if (type === 'response.created') {
112
+ return {
113
+ type: 'start',
114
+ id: event.response.id,
115
+ };
116
+ } else if (type === 'response.completed') {
117
+ return {
118
+ type: 'stop',
119
+ id: event.response.id,
120
+ usage: event.response.usage,
121
+ };
122
+ } else if (type === 'response.output_text.delta') {
123
+ return {
124
+ type: 'delta',
125
+ delta: event.delta,
126
+ };
127
+ } else if (type === 'response.output_text.done') {
65
128
  return {
66
- type,
67
- text: choice.delta.content || '',
129
+ type: 'done',
130
+ text: event.text,
68
131
  };
69
132
  }
70
133
  }
@@ -0,0 +1,9 @@
1
+ const CODE_REG = /^```\w*(.+)```/s;
2
+
3
+ export function parseCode(content) {
4
+ const match = content.trim().match(CODE_REG);
5
+ if (match) {
6
+ content = match[1].trim();
7
+ }
8
+ return content;
9
+ }
@@ -0,0 +1,58 @@
1
+ import { OBJ, STR, parse } from 'partial-json';
2
+
3
+ export function createMessageExtractor(keys) {
4
+ let buffer = '';
5
+ const extractors = keys.map((key) => {
6
+ return createExtractor(key);
7
+ });
8
+ return (delta) => {
9
+ buffer += delta;
10
+ return extractors
11
+ .map((extractor) => {
12
+ return extractor(buffer);
13
+ })
14
+ .filter((extracted) => {
15
+ return extracted;
16
+ });
17
+ };
18
+ }
19
+
20
+ function createExtractor(key) {
21
+ let lastText = '';
22
+ let done = false;
23
+ return (buffer) => {
24
+ if (done) {
25
+ return;
26
+ }
27
+
28
+ const text = extractText(buffer, key);
29
+
30
+ if (!text) {
31
+ return;
32
+ }
33
+
34
+ // Don't finish while the buffer has whitespace as it
35
+ // may be in the middle of trying to extract.
36
+ if (text === lastText && !buffer.endsWith(' ')) {
37
+ done = true;
38
+ }
39
+ const delta = text.slice(lastText.length);
40
+
41
+ lastText = text;
42
+
43
+ return {
44
+ key,
45
+ text,
46
+ delta,
47
+ done,
48
+ };
49
+ };
50
+ }
51
+
52
+ function extractText(input, key) {
53
+ if (!input) {
54
+ return;
55
+ }
56
+ const parsed = parse(input, STR | OBJ);
57
+ return parsed?.[key] || '';
58
+ }
@@ -0,0 +1,87 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ import { glob } from 'glob';
5
+ import Mustache from 'mustache';
6
+
7
+ export async function loadTemplates(dir) {
8
+ const result = {};
9
+ const files = await glob(path.join(dir, '*.md'));
10
+
11
+ if (!files.length) {
12
+ throw new Error(`No templates found in: ${dir}.`);
13
+ }
14
+
15
+ for (let file of files) {
16
+ const base = path.basename(file, '.md');
17
+ result[base] = await loadTemplate(file);
18
+ }
19
+
20
+ return result;
21
+ }
22
+
23
+ export function renderTemplate(template, options) {
24
+ let params = {
25
+ ...options,
26
+ ...options.params,
27
+ };
28
+
29
+ params = mapObjects(params);
30
+ params = wrapProxy(params);
31
+ return Mustache.render(template, params);
32
+ }
33
+
34
+ // Utils
35
+
36
+ async function loadTemplate(file) {
37
+ return await fs.readFile(file, 'utf-8');
38
+ }
39
+
40
+ // Transform arrays and object to versions
41
+ // that are more understandable in the context
42
+ // of a template that may have meaningful whitespace.
43
+ function mapObjects(params) {
44
+ const result = {};
45
+ for (let [key, value] of Object.entries(params)) {
46
+ if (Array.isArray(value)) {
47
+ value = mapArray(value);
48
+ } else if (typeof value === 'object') {
49
+ value = JSON.stringify(value, null, 2);
50
+ }
51
+ result[key] = value;
52
+ }
53
+ return result;
54
+ }
55
+
56
+ function mapArray(arr) {
57
+ // Only map simple arrays of primitives.
58
+ if (typeof arr[0] === 'string') {
59
+ arr = arr
60
+ .map((el) => {
61
+ return `- ${el}`;
62
+ })
63
+ .join('\n');
64
+ }
65
+ return arr;
66
+ }
67
+
68
+ // Wrap params with a proxy object that reports
69
+ // as having all properties. If one is accessed
70
+ // that does not exist then return the original
71
+ // token. This way templates can be partially
72
+ // interpolated and re-interpolated later.
73
+ function wrapProxy(params) {
74
+ return new Proxy(params, {
75
+ has() {
76
+ return true;
77
+ },
78
+
79
+ get(target, prop) {
80
+ if (prop in target) {
81
+ return target[prop];
82
+ } else {
83
+ return `{{{${prop.toString()}}}}`;
84
+ }
85
+ },
86
+ });
87
+ }
package/src/xai.js CHANGED
@@ -1,19 +1,12 @@
1
1
  import { OpenAiClient } from './openai.js';
2
2
 
3
- const DEFAULT_MODEL = 'grok-2-1212';
4
-
5
3
  export class XAiClient extends OpenAiClient {
4
+ static DEFAULT_MODEL = 'grok-4-fast';
5
+
6
6
  constructor(options) {
7
7
  super({
8
8
  ...options,
9
9
  baseURL: 'https://api.x.ai/v1',
10
10
  });
11
11
  }
12
-
13
- async getCompletion(options) {
14
- return super.getCompletion({
15
- model: DEFAULT_MODEL,
16
- ...options,
17
- });
18
- }
19
12
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "target": "ES2022",
5
+ "module": "CommonJS",
6
+ "outDir": "dist/cjs"
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "ESNext",
5
+ "outDir": "dist/esm",
6
+ "useDefineForClassFields": true
7
+ }
8
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "types",
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "emitDeclarationOnly": true,
8
+ }
9
+ }