@aws-blocks/bb-agent 0.1.1 → 0.1.3
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/DESIGN.md +66 -0
- package/README.md +31 -1
- package/dist/index.test.js +13 -11
- package/dist/model-factory.d.ts.map +1 -1
- package/dist/model-factory.js +19 -45
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -6
- package/src/index.test.ts +12 -9
- package/src/model-factory.ts +20 -44
- package/src/version.ts +1 -1
package/DESIGN.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Agent — Design
|
|
2
|
+
|
|
3
|
+
Design document for the Agent Building Block. For usage, see [README.md](./README.md).
|
|
4
|
+
|
|
5
|
+
**Package:** `@aws-blocks/bb-agent`
|
|
6
|
+
**Type:** Composite (uses DistributedTable, Realtime, AsyncJob, FileBucket internally)
|
|
7
|
+
**AWS Services:** Bedrock, DynamoDB, S3, SQS, AppSync Events
|
|
8
|
+
**Agent Framework:** [Strands Agents SDK](https://strandsagents.com/)
|
|
9
|
+
|
|
10
|
+
## Architecture
|
|
11
|
+
|
|
12
|
+
The Agent BB is a composite Building Block — it creates and manages 4 internal BBs:
|
|
13
|
+
|
|
14
|
+
| Internal BB | Purpose | Created when |
|
|
15
|
+
|-------------|---------|-------------|
|
|
16
|
+
| **FileBucket** | Session persistence (Strands SessionManager) | Always |
|
|
17
|
+
| **DistributedTable** | Frontend message history | `inferenceOnly: false` |
|
|
18
|
+
| **Realtime** | Streaming chunks to caller | Always |
|
|
19
|
+
| **AsyncJob** | Async agent execution (avoids 29s API Gateway timeout) | Always |
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
stream() → AsyncJob.submit() → returns { channelId } immediately
|
|
23
|
+
↓
|
|
24
|
+
AsyncJob consumer
|
|
25
|
+
↓
|
|
26
|
+
runAgent() → Strands agent loop → publishes chunks to Realtime
|
|
27
|
+
→ persists messages to DistributedTable
|
|
28
|
+
→ SessionManager saves state to FileBucket
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Session Persistence
|
|
32
|
+
|
|
33
|
+
Two storage backends, same FileBucket BB:
|
|
34
|
+
- **AWS:** Strands' native `S3Storage` → FileBucket-provisioned S3 bucket
|
|
35
|
+
- **Local:** Custom `FileBucketSnapshotStorage` → FileBucket mock (mirrors S3Storage key layout exactly)
|
|
36
|
+
|
|
37
|
+
## Infrastructure (CDK)
|
|
38
|
+
|
|
39
|
+
The CDK class mirrors the runtime's BB creation:
|
|
40
|
+
- **Bedrock IAM:** `InvokeModel` + `InvokeModelWithResponseStream` on all foundation models and inference profiles
|
|
41
|
+
- **FileBucket:** `${id}-sessions` — session snapshot storage
|
|
42
|
+
- **DistributedTable:** `${id}-messages` — conversation history (only when `inferenceOnly: false`)
|
|
43
|
+
- **Realtime:** `${id}-rt` — streaming namespace `chunks`
|
|
44
|
+
- **AsyncJob:** `${id}-job` — job payload: `{ message, conversationId?, channelId }`
|
|
45
|
+
|
|
46
|
+
> **Note:** Internal Building Blocks are created on the parent scope (not `this`) to ensure correct nested-scope resolution on AWS.
|
|
47
|
+
|
|
48
|
+
## Model Providers
|
|
49
|
+
|
|
50
|
+
All providers are Strands model implementations, mapped from Blocks's `ModelConfig` via `model-factory.ts`:
|
|
51
|
+
|
|
52
|
+
| Provider | Strands Class | Use Case |
|
|
53
|
+
|----------|--------------|----------|
|
|
54
|
+
| `canned` | `CannedProvider` (custom) | Local dev — keyword-based responses with tool call support |
|
|
55
|
+
| `bedrock` | `BedrockModel` | AWS — Amazon Bedrock models |
|
|
56
|
+
| `openai-api` | `OpenAIModel` | Any OpenAI-compatible endpoint (OpenAI, Ollama, vLLM) |
|
|
57
|
+
|
|
58
|
+
## CannedProvider
|
|
59
|
+
|
|
60
|
+
Custom Strands model provider for local development. No network, no API keys, no costs.
|
|
61
|
+
|
|
62
|
+
- Returns instant keyword-based responses (e.g., prompt contains "weather" → weather response, otherwise a default canned response)
|
|
63
|
+
- Streams word by word, matching the same `ModelStreamEvent` protocol as Bedrock/OpenAI
|
|
64
|
+
- Triggers tool calls when the prompt mentions a tool name — splits camelCase names into words (e.g., "weather" matches `getWeather`) and emits Strands `toolUse` events
|
|
65
|
+
- After Strands executes the tool and sends the result back, returns a fixed acknowledgment (`"I called the tool and got a result."`)
|
|
66
|
+
- Token usage reports zeros (no real model call)
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ AI agent with streaming, tool calling, and conversation persistence. Powered by
|
|
|
6
6
|
|
|
7
7
|
**Requires:** `zod` ^4.0.0 as a peer dependency. Tool parameters use Zod schemas for validation. If you see `ZodType missing properties` errors, check your zod version.
|
|
8
8
|
|
|
9
|
+
> Design & mock parity details: [DESIGN.md](./DESIGN.md)
|
|
10
|
+
|
|
9
11
|
## Quick Start
|
|
10
12
|
|
|
11
13
|
```typescript
|
|
@@ -96,6 +98,34 @@ Returned by `stream()`. Provides the Realtime channel and convenience methods:
|
|
|
96
98
|
| `channel` | `Promise<RealtimeChannel>` | Realtime channel handle — `await` it, then call `.subscribe(handler)`. |
|
|
97
99
|
| `complete()` | `Promise<AgentStreamChunk>` | Wait for the done chunk (full text + token usage). |
|
|
98
100
|
|
|
101
|
+
### AgentStreamChunk
|
|
102
|
+
|
|
103
|
+
Each chunk published to the Realtime channel has a `type` and type-specific fields:
|
|
104
|
+
|
|
105
|
+
| Type | Fields | Description |
|
|
106
|
+
|------|--------|-------------|
|
|
107
|
+
| `text-delta` | `text: string` | Incremental text token (in `'token'` streaming mode) or full block (in `'block'` mode). |
|
|
108
|
+
| `tool-call` | `toolName: string`, `input: JSONValue` | Agent is calling a tool. |
|
|
109
|
+
| `tool-result` | `toolName: string`, `text: string` | Tool returned a result. |
|
|
110
|
+
| `done` | `text: string`, `usage: TokenUsage` | Agent finished. `text` contains the full response. `usage` has `{ inputTokens, outputTokens, totalTokens }`. |
|
|
111
|
+
| `error` | `error: string` | Agent encountered an error. |
|
|
112
|
+
| `interrupt` | `interrupts: Array<{ id, name, reason }>` | Agent paused for approval. See [Tool Approval](#tool-approval-human-in-the-loop). |
|
|
113
|
+
|
|
114
|
+
### Message Roles
|
|
115
|
+
|
|
116
|
+
Messages stored in conversation history use these roles:
|
|
117
|
+
|
|
118
|
+
| Role | Description |
|
|
119
|
+
|------|-------------|
|
|
120
|
+
| `user` | User message. |
|
|
121
|
+
| `assistant` | Agent response text. |
|
|
122
|
+
| `tool-call` | Record of a tool invocation (stored for audit). |
|
|
123
|
+
| `tool-result` | Record of a tool's return value. |
|
|
124
|
+
| `approval` | User's approval/denial response to an interrupt. |
|
|
125
|
+
| `interrupt` | Agent paused — snapshot of pending interrupts. |
|
|
126
|
+
|
|
127
|
+
The `useChat` hook only surfaces `user`, `assistant`, and `approval` messages to the UI. Use `agent.getConversation()` directly to access the full history including tool-call/tool-result records.
|
|
128
|
+
|
|
99
129
|
### AgentConfig
|
|
100
130
|
|
|
101
131
|
| Option | Type | Description |
|
|
@@ -630,7 +660,7 @@ const chat = useChat({
|
|
|
630
660
|
resume: (chId, responses, convId) => api.resume(chId, responses, convId),
|
|
631
661
|
},
|
|
632
662
|
subscribe: async (channelId, handler) => {
|
|
633
|
-
const
|
|
663
|
+
const channel = await api.getChannel(channelId);
|
|
634
664
|
return channel.subscribe(handler);
|
|
635
665
|
},
|
|
636
666
|
onMessagesChange: (msgs) => renderMessages(msgs),
|
package/dist/index.test.js
CHANGED
|
@@ -716,13 +716,17 @@ describe('checkModelHealth', () => {
|
|
|
716
716
|
assert.strictEqual(await checkModelHealth({ provider: 'canned' }, log), true);
|
|
717
717
|
});
|
|
718
718
|
test('bedrock foundation model found returns true', async () => {
|
|
719
|
-
|
|
719
|
+
let callCount = 0;
|
|
720
|
+
const mockClient = { send: async () => {
|
|
721
|
+
callCount++;
|
|
722
|
+
if (callCount === 1)
|
|
723
|
+
throw new Error('not an inference profile');
|
|
724
|
+
return { modelDetails: { modelId: 'anthropic.claude-3-haiku' } };
|
|
725
|
+
} };
|
|
720
726
|
assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'anthropic.claude-3-haiku' }, log, mockClient), true);
|
|
721
727
|
});
|
|
722
|
-
test('bedrock
|
|
723
|
-
const
|
|
724
|
-
err.name = 'ValidationException';
|
|
725
|
-
const mockClient = { send: async () => { throw err; } };
|
|
728
|
+
test('bedrock model not found returns false', async () => {
|
|
729
|
+
const mockClient = { send: async () => { throw new Error('not found'); } };
|
|
726
730
|
assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'bad.model' }, log, mockClient), false);
|
|
727
731
|
});
|
|
728
732
|
test('bedrock credential error returns false', async () => {
|
|
@@ -731,15 +735,13 @@ describe('checkModelHealth', () => {
|
|
|
731
735
|
const mockClient = { send: async () => { throw err; } };
|
|
732
736
|
assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'anthropic.claude-3-haiku' }, log, mockClient), false);
|
|
733
737
|
});
|
|
734
|
-
test('bedrock
|
|
738
|
+
test('bedrock inference profile found returns true', async () => {
|
|
735
739
|
const mockClient = { send: async () => ({ inferenceProfileName: 'US Claude Sonnet' }) };
|
|
736
740
|
assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'us.anthropic.claude-sonnet-4' }, log, mockClient), true);
|
|
737
741
|
});
|
|
738
|
-
test('bedrock
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
const mockClient = { send: async () => { throw err; } };
|
|
742
|
-
assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'us.anthropic.claude-sonnet-4' }, log, mockClient), false);
|
|
742
|
+
test('bedrock global inference profile found returns true', async () => {
|
|
743
|
+
const mockClient = { send: async () => ({ inferenceProfileName: 'Global Claude Opus' }) };
|
|
744
|
+
assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'global.anthropic.claude-opus-4-8-v1' }, log, mockClient), true);
|
|
743
745
|
});
|
|
744
746
|
test('openai-api with unreachable endpoint returns false', async () => {
|
|
745
747
|
assert.strictEqual(await checkModelHealth({ provider: 'openai-api', modelId: 'gpt-4', endpoint: 'http://localhost:19999/v1' }, log), false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-factory.d.ts","sourceRoot":"","sources":["../src/model-factory.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAK9C;;;;;;;GAOG;AACH,yDAAyD;AACzD,MAAM,WAAW,mBAAmB;IACnC,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;CACjC;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"model-factory.d.ts","sourceRoot":"","sources":["../src/model-factory.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAK9C;;;;;;;GAOG;AACH,yDAAyD;AACzD,MAAM,WAAW,mBAAmB;IACnC,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;CACjC;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC,CAqHjI;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAkDjH"}
|
package/dist/model-factory.js
CHANGED
|
@@ -16,35 +16,24 @@ export async function checkModelHealth(config, log, _testClient) {
|
|
|
16
16
|
}
|
|
17
17
|
log.info(`Checking model health: ${config.provider}${config.modelId ? ` (${config.modelId})` : ''}`);
|
|
18
18
|
if (config.provider === 'bedrock') {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const client = await getClient();
|
|
30
|
-
const command = _testClient
|
|
31
|
-
? { inferenceProfileIdentifier: config.modelId }
|
|
32
|
-
: new (await import('@aws-sdk/client-bedrock')).GetInferenceProfileCommand({ inferenceProfileIdentifier: config.modelId });
|
|
33
|
-
const res = await client.send(command);
|
|
34
|
-
if (res.inferenceProfileName) {
|
|
35
|
-
log.info(`Inference profile '${config.modelId}' available`);
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
const e = err;
|
|
42
|
-
log.warn(`Inference profile health check failed for '${config.modelId}': ${e.name ?? e.message}`, { provider: config.provider, modelId: config.modelId });
|
|
43
|
-
return false;
|
|
19
|
+
const client = _testClient ?? new (await import('@aws-sdk/client-bedrock')).BedrockClient({});
|
|
20
|
+
// Try GetInferenceProfile first (covers cross-region and global profiles).
|
|
21
|
+
try {
|
|
22
|
+
const command = _testClient
|
|
23
|
+
? { inferenceProfileIdentifier: config.modelId }
|
|
24
|
+
: new (await import('@aws-sdk/client-bedrock')).GetInferenceProfileCommand({ inferenceProfileIdentifier: config.modelId });
|
|
25
|
+
const res = await client.send(command);
|
|
26
|
+
if (res.inferenceProfileName) {
|
|
27
|
+
log.info(`Inference profile '${config.modelId}' available`);
|
|
28
|
+
return true;
|
|
44
29
|
}
|
|
45
30
|
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
const e = err;
|
|
33
|
+
log.debug?.(`Not an inference profile: ${e.name ?? e.message}`, { modelId: config.modelId });
|
|
34
|
+
}
|
|
35
|
+
// Try GetFoundationModel (covers base model IDs).
|
|
46
36
|
try {
|
|
47
|
-
const client = await getClient();
|
|
48
37
|
const command = _testClient
|
|
49
38
|
? { modelIdentifier: config.modelId }
|
|
50
39
|
: new (await import('@aws-sdk/client-bedrock')).GetFoundationModelCommand({ modelIdentifier: config.modelId });
|
|
@@ -53,29 +42,14 @@ export async function checkModelHealth(config, log, _testClient) {
|
|
|
53
42
|
log.info(`Bedrock model '${config.modelId}' exists in catalog`);
|
|
54
43
|
return true;
|
|
55
44
|
}
|
|
56
|
-
return false;
|
|
57
45
|
}
|
|
58
46
|
catch (err) {
|
|
59
47
|
const e = err;
|
|
60
|
-
|
|
61
|
-
if (e.name === 'ValidationException' || e.name === 'ResourceNotFoundException') {
|
|
62
|
-
try {
|
|
63
|
-
const client = await getClient();
|
|
64
|
-
const listCommand = _testClient
|
|
65
|
-
? {}
|
|
66
|
-
: new (await import('@aws-sdk/client-bedrock')).ListFoundationModelsCommand({});
|
|
67
|
-
const list = await client.send(listCommand);
|
|
68
|
-
const available = list.modelSummaries?.map((m) => m.modelId).filter(Boolean) ?? [];
|
|
69
|
-
log.warn(`Bedrock model '${config.modelId}' not found. Available: ${available.slice(0, 10).join(', ')}${available.length > 10 ? ` (+${available.length - 10} more)` : ''}`);
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
log.warn(`Bedrock model '${config.modelId}' not found. Could not list available models.`);
|
|
73
|
-
}
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
log.warn(`Bedrock health check failed: ${e.name ?? e.message}. Verify AWS credentials are configured.`, { provider: config.provider, modelId: config.modelId });
|
|
77
|
-
return false;
|
|
48
|
+
log.debug?.(`Not a foundation model: ${e.name ?? e.message}`, { modelId: config.modelId });
|
|
78
49
|
}
|
|
50
|
+
// Both failed — model not found.
|
|
51
|
+
log.warn(`Bedrock model '${config.modelId}' not found as inference profile or foundation model. Verify the model ID and AWS credentials.`, { provider: config.provider, modelId: config.modelId });
|
|
52
|
+
return false;
|
|
79
53
|
}
|
|
80
54
|
if (config.provider === 'openai-api') {
|
|
81
55
|
const endpoint = config.endpoint ?? 'https://api.openai.com/v1';
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aws-blocks/bb-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"author": "Amazon Web Services",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"dist",
|
|
9
9
|
"MODELS.md",
|
|
10
10
|
"README.md",
|
|
11
|
+
"DESIGN.md",
|
|
11
12
|
"src",
|
|
12
13
|
"LICENSE"
|
|
13
14
|
],
|
|
@@ -33,11 +34,11 @@
|
|
|
33
34
|
"test": "node --test dist/index.test.js"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
36
|
-
"@aws-blocks/bb-async-job": "^0.1.
|
|
37
|
-
"@aws-blocks/bb-distributed-table": "^0.1.
|
|
38
|
-
"@aws-blocks/bb-file-bucket": "^0.1.
|
|
39
|
-
"@aws-blocks/bb-logger": "^0.1.
|
|
40
|
-
"@aws-blocks/bb-realtime": "^0.1.
|
|
37
|
+
"@aws-blocks/bb-async-job": "^0.1.2",
|
|
38
|
+
"@aws-blocks/bb-distributed-table": "^0.1.3",
|
|
39
|
+
"@aws-blocks/bb-file-bucket": "^0.1.2",
|
|
40
|
+
"@aws-blocks/bb-logger": "^0.1.2",
|
|
41
|
+
"@aws-blocks/bb-realtime": "^0.1.2",
|
|
41
42
|
"@aws-blocks/core": "^0.1.1",
|
|
42
43
|
"@aws-sdk/client-bedrock": "^3.700.0",
|
|
43
44
|
"@strands-agents/sdk": "~1.3.0",
|
package/src/index.test.ts
CHANGED
|
@@ -844,13 +844,17 @@ describe('checkModelHealth', () => {
|
|
|
844
844
|
});
|
|
845
845
|
|
|
846
846
|
test('bedrock foundation model found returns true', async () => {
|
|
847
|
-
|
|
847
|
+
let callCount = 0;
|
|
848
|
+
const mockClient = { send: async () => {
|
|
849
|
+
callCount++;
|
|
850
|
+
if (callCount === 1) throw new Error('not an inference profile');
|
|
851
|
+
return { modelDetails: { modelId: 'anthropic.claude-3-haiku' } };
|
|
852
|
+
} };
|
|
848
853
|
assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'anthropic.claude-3-haiku' }, log, mockClient), true);
|
|
849
854
|
});
|
|
850
855
|
|
|
851
|
-
test('bedrock
|
|
852
|
-
const
|
|
853
|
-
const mockClient = { send: async () => { throw err; } };
|
|
856
|
+
test('bedrock model not found returns false', async () => {
|
|
857
|
+
const mockClient = { send: async () => { throw new Error('not found'); } };
|
|
854
858
|
assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'bad.model' }, log, mockClient), false);
|
|
855
859
|
});
|
|
856
860
|
|
|
@@ -860,15 +864,14 @@ describe('checkModelHealth', () => {
|
|
|
860
864
|
assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'anthropic.claude-3-haiku' }, log, mockClient), false);
|
|
861
865
|
});
|
|
862
866
|
|
|
863
|
-
test('bedrock
|
|
867
|
+
test('bedrock inference profile found returns true', async () => {
|
|
864
868
|
const mockClient = { send: async () => ({ inferenceProfileName: 'US Claude Sonnet' }) };
|
|
865
869
|
assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'us.anthropic.claude-sonnet-4' }, log, mockClient), true);
|
|
866
870
|
});
|
|
867
871
|
|
|
868
|
-
test('bedrock
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'us.anthropic.claude-sonnet-4' }, log, mockClient), false);
|
|
872
|
+
test('bedrock global inference profile found returns true', async () => {
|
|
873
|
+
const mockClient = { send: async () => ({ inferenceProfileName: 'Global Claude Opus' }) };
|
|
874
|
+
assert.strictEqual(await checkModelHealth({ provider: 'bedrock', modelId: 'global.anthropic.claude-opus-4-8-v1' }, log, mockClient), true);
|
|
872
875
|
});
|
|
873
876
|
|
|
874
877
|
test('openai-api with unreachable endpoint returns false', async () => {
|
package/src/model-factory.ts
CHANGED
|
@@ -36,36 +36,25 @@ export async function checkModelHealth(config: ModelConfig, log: ChildLogger, _t
|
|
|
36
36
|
}
|
|
37
37
|
log.info(`Checking model health: ${config.provider}${config.modelId ? ` (${config.modelId})` : ''}`);
|
|
38
38
|
if (config.provider === 'bedrock') {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const client = await getClient();
|
|
51
|
-
const command = _testClient
|
|
52
|
-
? { inferenceProfileIdentifier: config.modelId }
|
|
53
|
-
: new (await import('@aws-sdk/client-bedrock')).GetInferenceProfileCommand({ inferenceProfileIdentifier: config.modelId });
|
|
54
|
-
const res = await client.send(command);
|
|
55
|
-
if (res.inferenceProfileName) {
|
|
56
|
-
log.info(`Inference profile '${config.modelId}' available`);
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
return false;
|
|
60
|
-
} catch (err: unknown) {
|
|
61
|
-
const e = err as { name?: string; message?: string };
|
|
62
|
-
log.warn(`Inference profile health check failed for '${config.modelId}': ${e.name ?? e.message}`, { provider: config.provider, modelId: config.modelId });
|
|
63
|
-
return false;
|
|
39
|
+
const client: BedrockHealthClient = _testClient ?? new (await import('@aws-sdk/client-bedrock')).BedrockClient({});
|
|
40
|
+
|
|
41
|
+
// Try GetInferenceProfile first (covers cross-region and global profiles).
|
|
42
|
+
try {
|
|
43
|
+
const command = _testClient
|
|
44
|
+
? { inferenceProfileIdentifier: config.modelId }
|
|
45
|
+
: new (await import('@aws-sdk/client-bedrock')).GetInferenceProfileCommand({ inferenceProfileIdentifier: config.modelId });
|
|
46
|
+
const res = await client.send(command);
|
|
47
|
+
if (res.inferenceProfileName) {
|
|
48
|
+
log.info(`Inference profile '${config.modelId}' available`);
|
|
49
|
+
return true;
|
|
64
50
|
}
|
|
51
|
+
} catch (err: unknown) {
|
|
52
|
+
const e = err as { name?: string; message?: string };
|
|
53
|
+
log.debug?.(`Not an inference profile: ${e.name ?? e.message}`, { modelId: config.modelId });
|
|
65
54
|
}
|
|
66
55
|
|
|
56
|
+
// Try GetFoundationModel (covers base model IDs).
|
|
67
57
|
try {
|
|
68
|
-
const client = await getClient();
|
|
69
58
|
const command = _testClient
|
|
70
59
|
? { modelIdentifier: config.modelId }
|
|
71
60
|
: new (await import('@aws-sdk/client-bedrock')).GetFoundationModelCommand({ modelIdentifier: config.modelId });
|
|
@@ -74,27 +63,14 @@ export async function checkModelHealth(config: ModelConfig, log: ChildLogger, _t
|
|
|
74
63
|
log.info(`Bedrock model '${config.modelId}' exists in catalog`);
|
|
75
64
|
return true;
|
|
76
65
|
}
|
|
77
|
-
return false;
|
|
78
66
|
} catch (err: unknown) {
|
|
79
67
|
const e = err as { name?: string; message?: string };
|
|
80
|
-
|
|
81
|
-
if (e.name === 'ValidationException' || e.name === 'ResourceNotFoundException') {
|
|
82
|
-
try {
|
|
83
|
-
const client = await getClient();
|
|
84
|
-
const listCommand = _testClient
|
|
85
|
-
? {}
|
|
86
|
-
: new (await import('@aws-sdk/client-bedrock')).ListFoundationModelsCommand({});
|
|
87
|
-
const list = await client.send(listCommand);
|
|
88
|
-
const available = list.modelSummaries?.map((m: { modelId?: string }) => m.modelId).filter(Boolean) ?? [];
|
|
89
|
-
log.warn(`Bedrock model '${config.modelId}' not found. Available: ${available.slice(0, 10).join(', ')}${available.length > 10 ? ` (+${available.length - 10} more)` : ''}`);
|
|
90
|
-
} catch {
|
|
91
|
-
log.warn(`Bedrock model '${config.modelId}' not found. Could not list available models.`);
|
|
92
|
-
}
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
log.warn(`Bedrock health check failed: ${e.name ?? e.message}. Verify AWS credentials are configured.`, { provider: config.provider, modelId: config.modelId });
|
|
96
|
-
return false;
|
|
68
|
+
log.debug?.(`Not a foundation model: ${e.name ?? e.message}`, { modelId: config.modelId });
|
|
97
69
|
}
|
|
70
|
+
|
|
71
|
+
// Both failed — model not found.
|
|
72
|
+
log.warn(`Bedrock model '${config.modelId}' not found as inference profile or foundation model. Verify the model ID and AWS credentials.`, { provider: config.provider, modelId: config.modelId });
|
|
73
|
+
return false;
|
|
98
74
|
}
|
|
99
75
|
|
|
100
76
|
if (config.provider === 'openai-api') {
|
package/src/version.ts
CHANGED