@iinm/plain-agent 1.3.2 → 1.4.0

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.
@@ -835,6 +835,52 @@
835
835
  "model": "qwen/qwen3-next-80b-a3b-thinking-maas"
836
836
  }
837
837
  }
838
+ },
839
+
840
+ {
841
+ "name": "nova-2-lite",
842
+ "variant": "bedrock",
843
+ "platform": {
844
+ "name": "bedrock",
845
+ "variant": "default"
846
+ },
847
+ "model": {
848
+ "format": "bedrock-converse",
849
+ "config": {
850
+ "model": "global.amazon.nova-2-lite-v1:0",
851
+ "enablePromptCaching": true,
852
+ "additionalModelRequestFields": {
853
+ "reasoningConfig": {
854
+ "type": "enabled",
855
+ "maxReasoningEffort": "medium"
856
+ }
857
+ }
858
+ }
859
+ }
860
+ },
861
+ {
862
+ "name": "claude-haiku-4-5",
863
+ "variant": "thinking-16k-bedrock-converse",
864
+ "platform": {
865
+ "name": "bedrock",
866
+ "variant": "default"
867
+ },
868
+ "model": {
869
+ "format": "bedrock-converse",
870
+ "config": {
871
+ "model": "global.anthropic.claude-haiku-4-5-20251001-v1:0",
872
+ "enablePromptCaching": true,
873
+ "inferenceConfig": {
874
+ "max_tokens": 32768
875
+ },
876
+ "additionalModelRequestFields": {
877
+ "reasoning_config": {
878
+ "type": "enabled",
879
+ "budget_tokens": 16384
880
+ }
881
+ }
882
+ }
883
+ }
838
884
  }
839
885
  ]
840
886
  }
package/README.md CHANGED
@@ -50,6 +50,8 @@ Create the configuration.
50
50
  "name": "anthropic",
51
51
  "variant": "default",
52
52
  "apiKey": "FIXME"
53
+ // Or
54
+ // "apiKey": { "$env": "ANTHROPIC_API_KEY" }
53
55
  },
54
56
  {
55
57
  "name": "gemini",
@@ -603,11 +605,13 @@ policy='{
603
605
  "Effect": "Allow",
604
606
  "Action": [
605
607
  "bedrock:InvokeModel",
606
- "bedrock:InvokeModelWithResponseStream"
608
+ "bedrock:InvokeModelWithResponseStream",
609
+ "bedrock:ListInferenceProfiles"
607
610
  ],
608
611
  "Resource": [
609
- "arn:aws:bedrock:*::foundation-model/*",
610
- "arn:aws:bedrock:*:*:inference-profile/*"
612
+ "arn:aws:bedrock:*:*:foundation-model/*",
613
+ "arn:aws:bedrock:*:*:inference-profile/*",
614
+ "arn:aws:bedrock:*:*:application-inference-profile/*"
611
615
  ]
612
616
  }
613
617
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/main.mjs CHANGED
@@ -60,7 +60,7 @@ if (cliArgs.subcommand.type === "install-claude-code-plugins") {
60
60
  (async () => {
61
61
  const startTime = new Date();
62
62
  const sessionId = [
63
- startTime.toISOString().slice(0, 10),
63
+ `${startTime.getFullYear()}-${`0${startTime.getMonth() + 1}`.slice(-2)}-${`0${startTime.getDate()}`.slice(-2)}`,
64
64
  `0${startTime.getHours()}`.slice(-2) +
65
65
  `0${startTime.getMinutes()}`.slice(-2),
66
66
  ].join("-");
@@ -1,4 +1,5 @@
1
1
  import { callAnthropicModel } from "./providers/anthropic.mjs";
2
+ import { callBedrockConverseModel } from "./providers/bedrock.mjs";
2
3
  import { createCacheEnabledGeminiModelCaller } from "./providers/gemini.mjs";
3
4
  import { callOpenAIModel } from "./providers/openai.mjs";
4
5
  import { callOpenAICompatibleModel } from "./providers/openaiCompatible.mjs";
@@ -25,5 +26,7 @@ export function createModelCaller(modelDef) {
25
26
  case "openai-messages":
26
27
  return (input) =>
27
28
  callOpenAICompatibleModel(platform, model.config, input);
29
+ case "bedrock-converse":
30
+ return (input) => callBedrockConverseModel(platform, model.config, input);
28
31
  }
29
32
  }
@@ -2,6 +2,7 @@ import { AnthropicModelConfig } from "./providers/anthropic";
2
2
  import { GeminiModelConfig } from "./providers/gemini";
3
3
  import { OpenAIModelConfig } from "./providers/openai";
4
4
  import { OpenAICompatibleModelConfig } from "./providers/openaiCompatible";
5
+ import { BedrockConverseModelConfig } from "./providers/bedrock";
5
6
 
6
7
  export type ModelDefinition = {
7
8
  name: string;
@@ -70,4 +71,8 @@ export type ModelConfig =
70
71
  | {
71
72
  format: "openai-messages";
72
73
  config: OpenAICompatibleModelConfig;
74
+ }
75
+ | {
76
+ format: "bedrock-converse";
77
+ config: BedrockConverseModelConfig;
73
78
  };
package/src/prompt.mjs CHANGED
@@ -112,6 +112,12 @@ If skill matches task: read full file and apply the workflow
112
112
 
113
113
  ${skillDescriptions}
114
114
 
115
+ # Claude Code Compatibility Notes
116
+
117
+ When using a Claude Code-compatible command, agent, or skill, follow these rules:
118
+ - Subagents cannot run in parallel. Delegate to them one at a time.
119
+ - If a Claude Code prompt mentions CLAUDE.md for project rules or conventions, use AGENTS.md instead when CLAUDE.md is absent.
120
+
115
121
  # Environment
116
122
 
117
123
  - User name: ${username}
@@ -0,0 +1,249 @@
1
+ /* Model Configuration */
2
+ export type BedrockConverseModelConfig = {
3
+ model: string;
4
+ inferenceConfig?: {
5
+ maxTokens?: number;
6
+ temperature?: number;
7
+ topP?: number;
8
+ };
9
+ additionalModelRequestFields?: Record<string, unknown>;
10
+ enablePromptCaching?: boolean;
11
+ };
12
+
13
+ /* Request */
14
+ export type BedrockConverseRequest = {
15
+ modelId?: string;
16
+ messages: BedrockMessage[];
17
+ system?: BedrockSystemContentBlock[];
18
+ toolConfig?: BedrockToolConfig;
19
+ additionalModelRequestFields?: Record<string, unknown>;
20
+ inferenceConfig?: {
21
+ maxTokens?: number;
22
+ temperature?: number;
23
+ };
24
+ };
25
+
26
+ /* Message */
27
+ export type BedrockMessage = BedrockUserMessage | BedrockAssistantMessage;
28
+
29
+ export type BedrockUserMessage = {
30
+ role: "user";
31
+ content: BedrockContentBlock[];
32
+ };
33
+
34
+ export type BedrockAssistantMessage = {
35
+ role: "assistant";
36
+ content: BedrockAssistantContentBlock[];
37
+ };
38
+
39
+ export type BedrockSystemContentBlock =
40
+ | {
41
+ text: string;
42
+ }
43
+ | BedrockCachePointBlock;
44
+
45
+ /* Content Block */
46
+ export type BedrockContentBlock =
47
+ | BedrockTextBlock
48
+ | BedrockImageBlock
49
+ | BedrockToolUseBlock
50
+ | BedrockToolResultBlock
51
+ | BedrockCachePointBlock;
52
+
53
+ export type BedrockAssistantContentBlock =
54
+ | BedrockTextBlock
55
+ | BedrockToolUseBlock
56
+ | BedrockReasoningContentBlock
57
+ | BedrockCachePointBlock;
58
+
59
+ export type BedrockTextBlock = {
60
+ text: string;
61
+ cachePoint?: {
62
+ type: "default";
63
+ };
64
+ };
65
+
66
+ export type BedrockImageBlock = {
67
+ image: {
68
+ format: "png" | "jpeg" | "gif" | "webp";
69
+ source: {
70
+ bytes?: string; // base64 encoded
71
+ };
72
+ };
73
+ };
74
+
75
+ export type BedrockToolUseBlock = {
76
+ toolUse: {
77
+ toolUseId: string;
78
+ name: string;
79
+ input: Record<string, unknown>;
80
+ };
81
+ };
82
+
83
+ export type BedrockToolResultBlock = {
84
+ toolResult: {
85
+ toolUseId: string;
86
+ content: BedrockToolResultContent[];
87
+ status?: "success" | "error";
88
+ };
89
+ };
90
+
91
+ export type BedrockToolResultContent =
92
+ | { text: string }
93
+ | { image: BedrockImageBlock["image"] };
94
+
95
+ // Message history type - used when sending back to API
96
+ // Claude Haiku 4.5 and Nova use this format
97
+ // Note: reasoningText and redactedContent are mutually exclusive (union type)
98
+ export type BedrockReasoningContentBlock = {
99
+ reasoningContent:
100
+ | {
101
+ reasoningText: {
102
+ text: string;
103
+ signature?: string;
104
+ };
105
+ }
106
+ | {
107
+ redactedContent: string; // Base64-encoded binary
108
+ };
109
+ };
110
+
111
+ // Internal type for accumulating reasoning content during streaming
112
+ // Note: Streaming API uses flat structure (reasoningContent.text, reasoningContent.redactedContent)
113
+ // but message history uses nested structure (reasoningContent.reasoningText.text, reasoningContent.redactedContent)
114
+ export type BedrockReasoningContentAccumulator = {
115
+ reasoningContent: {
116
+ text?: string;
117
+ signature?: string;
118
+ redactedContent?: string; // Base64-encoded binary
119
+ };
120
+ };
121
+
122
+ // Internal type for accumulating partial content during streaming
123
+ // Note: reasoningContent uses flat structure during streaming, but nested structure in message history
124
+ export type BedrockAssistantContentBlockWithPartial = {
125
+ text?: string;
126
+ toolUse?: {
127
+ toolUseId?: string;
128
+ name?: string;
129
+ input?: unknown;
130
+ };
131
+ reasoningContent?: {
132
+ text?: string;
133
+ signature?: string;
134
+ redactedContent?: string; // Base64-encoded binary
135
+ };
136
+ cachePoint?: {
137
+ type: "default";
138
+ };
139
+ _partialInput?: string;
140
+ };
141
+
142
+ export type BedrockCachePointBlock = {
143
+ cachePoint: {
144
+ type: "default";
145
+ };
146
+ };
147
+
148
+ /* Tool Configuration */
149
+ export type BedrockToolConfig = {
150
+ tools: BedrockTool[];
151
+ toolChoice?: BedrockToolChoice;
152
+ };
153
+
154
+ export type BedrockTool = {
155
+ toolSpec: {
156
+ name: string;
157
+ description?: string;
158
+ inputSchema: {
159
+ json: Record<string, unknown>;
160
+ };
161
+ };
162
+ };
163
+
164
+ export type BedrockToolChoice =
165
+ | { auto: Record<string, never> }
166
+ | { any: Record<string, never> }
167
+ | { tool: { name: string } };
168
+
169
+ /* Response */
170
+ export type BedrockConverseResponse = {
171
+ metrics: {
172
+ latencyMs: number;
173
+ };
174
+ output: {
175
+ message: BedrockAssistantMessage;
176
+ };
177
+ stopReason: "end_turn" | "tool_use" | "max_tokens" | "stop_sequence";
178
+ usage: BedrockUsage;
179
+ };
180
+
181
+ /* Usage */
182
+ export type BedrockUsage = {
183
+ inputTokens: number;
184
+ outputTokens: number;
185
+ totalTokens: number;
186
+ cacheReadInputTokens?: number;
187
+ cacheWriteInputTokens?: number;
188
+ };
189
+
190
+ /* Stream Event */
191
+ export type BedrockStreamEvent =
192
+ | BedrockStreamMessageStartEvent
193
+ | BedrockStreamContentBlockStartEvent
194
+ | BedrockStreamContentBlockDeltaEvent
195
+ | BedrockStreamContentBlockStopEvent
196
+ | BedrockStreamMessageStopEvent
197
+ | BedrockStreamMetadataEvent;
198
+
199
+ export type BedrockStreamMessageStartEvent = {
200
+ messageStart: {
201
+ requestId: string;
202
+ };
203
+ };
204
+
205
+ export type BedrockStreamContentBlockStartEvent = {
206
+ contentBlockIndex: number;
207
+ start: {
208
+ text?: string;
209
+ toolUse?: {
210
+ toolUseId: string;
211
+ name: string;
212
+ };
213
+ };
214
+ };
215
+
216
+ export type BedrockStreamContentBlockDeltaEvent = {
217
+ contentBlockIndex: number;
218
+ delta: {
219
+ text?: string;
220
+ toolUse?: {
221
+ toolUseId?: string;
222
+ name?: string;
223
+ input?: string; // partial JSON
224
+ };
225
+ reasoningContent?: {
226
+ text?: string;
227
+ signature?: string;
228
+ redactedContent?: string; // Base64-encoded binary
229
+ };
230
+ };
231
+ };
232
+
233
+ export type BedrockStreamContentBlockStopEvent = {
234
+ contentBlockStop: {
235
+ contentBlockIndex: number;
236
+ };
237
+ };
238
+
239
+ export type BedrockStreamMessageStopEvent = {
240
+ stopReason: "end_turn" | "tool_use" | "max_tokens" | "stop_sequence";
241
+ additionalModelResponseFields?: Record<string, unknown>;
242
+ };
243
+
244
+ export type BedrockStreamMetadataEvent = {
245
+ usage: BedrockUsage;
246
+ metrics: {
247
+ latencyMs: number;
248
+ };
249
+ };
@@ -0,0 +1,709 @@
1
+ /**
2
+ * @import { ModelInput, Message, AssistantMessage, ModelOutput, PartialMessageContent } from "../model";
3
+ * @import { ToolDefinition } from "../tool";
4
+ * @import { BedrockConverseModelConfig, BedrockMessage, BedrockContentBlock, BedrockAssistantContentBlock, BedrockAssistantContentBlockWithPartial, BedrockTool, BedrockStreamEvent, BedrockConverseRequest, BedrockUsage, BedrockToolResultContent } from "./bedrock";
5
+ */
6
+
7
+ import { styleText } from "node:util";
8
+ import { Sha256 } from "@aws-crypto/sha256-js";
9
+ import { fromIni } from "@aws-sdk/credential-providers";
10
+ import { HttpRequest } from "@smithy/protocol-http";
11
+ import { SignatureV4 } from "@smithy/signature-v4";
12
+ import { noThrow } from "../utils/noThrow.mjs";
13
+ import { readBedrockStreamEvents } from "./platform/bedrock.mjs";
14
+
15
+ /**
16
+ * @param {import("../modelDefinition").PlatformConfig} platformConfig
17
+ * @param {BedrockConverseModelConfig} modelConfig
18
+ * @param {ModelInput} input
19
+ * @param {number} [retryCount]
20
+ * @returns {Promise<ModelOutput | Error>}
21
+ */
22
+ export async function callBedrockConverseModel(
23
+ platformConfig,
24
+ modelConfig,
25
+ input,
26
+ retryCount = 0,
27
+ ) {
28
+ return await noThrow(async () => {
29
+ const messages = convertGenericMessageToBedrockFormat(input.messages);
30
+ const cachedMessages = modelConfig.enablePromptCaching
31
+ ? enablePromptCaching(messages)
32
+ : messages;
33
+ const tools = convertGenericToolDefinitionToBedrockFormat(
34
+ input.tools || [],
35
+ );
36
+
37
+ const url = (() => {
38
+ const baseURL = platformConfig.baseURL;
39
+ if (platformConfig.name !== "bedrock") {
40
+ throw new Error(`Unsupported platform: ${platformConfig.name}`);
41
+ }
42
+ return `${baseURL}/model/${modelConfig.model}/converse-stream`;
43
+ })();
44
+
45
+ const region = extractRegionFromBaseURL(platformConfig.baseURL);
46
+
47
+ /** @type {BedrockConverseRequest} */
48
+ const request = {
49
+ messages: cachedMessages,
50
+ ...(modelConfig.inferenceConfig && {
51
+ inferenceConfig: modelConfig.inferenceConfig,
52
+ }),
53
+ ...(modelConfig.additionalModelRequestFields && {
54
+ additionalModelRequestFields: modelConfig.additionalModelRequestFields,
55
+ }),
56
+ };
57
+
58
+ // Add system messages if present
59
+ const systemMessages = extractSystemMessages(
60
+ input.messages,
61
+ modelConfig.enablePromptCaching,
62
+ );
63
+ if (systemMessages.length > 0) {
64
+ request.system = systemMessages;
65
+ }
66
+
67
+ // Add tools if present
68
+ if (tools.length > 0) {
69
+ request.toolConfig = {
70
+ tools: tools,
71
+ };
72
+ }
73
+
74
+ const payload = JSON.stringify(request);
75
+
76
+ // Sign request with AWS Signature V4
77
+ const signer = new SignatureV4({
78
+ credentials: fromIni({ profile: platformConfig.awsProfile }),
79
+ region,
80
+ service: "bedrock",
81
+ sha256: Sha256,
82
+ });
83
+
84
+ const urlParsed = new URL(url);
85
+ const { hostname, pathname } = urlParsed;
86
+
87
+ const req = new HttpRequest({
88
+ protocol: "https:",
89
+ method: "POST",
90
+ hostname,
91
+ path: pathname,
92
+ headers: {
93
+ host: hostname,
94
+ "Content-Type": "application/json",
95
+ },
96
+ body: payload,
97
+ });
98
+
99
+ const signed = await signer.sign(req);
100
+
101
+ const response = await fetch(url, {
102
+ method: signed.method,
103
+ headers: signed.headers,
104
+ body: signed.body,
105
+ signal: AbortSignal.timeout(120 * 1000),
106
+ });
107
+
108
+ if (response.status !== 200) {
109
+ const errorText = await response.text();
110
+ console.error(
111
+ styleText("red", `Bedrock API error: ${response.status} ${errorText}`),
112
+ );
113
+
114
+ // Retry on throttling or server errors
115
+ if (
116
+ (response.status === 429 ||
117
+ response.status === 502 ||
118
+ response.status === 503) &&
119
+ retryCount < 3
120
+ ) {
121
+ const retryInterval = Math.min(2 * 2 ** retryCount, 16);
122
+ console.error(
123
+ styleText(
124
+ "yellow",
125
+ `Retrying in ${retryInterval} seconds... (attempt ${retryCount + 1})`,
126
+ ),
127
+ );
128
+ await new Promise((resolve) =>
129
+ setTimeout(resolve, retryInterval * 1000),
130
+ );
131
+ return callBedrockConverseModel(
132
+ platformConfig,
133
+ modelConfig,
134
+ input,
135
+ retryCount + 1,
136
+ );
137
+ }
138
+
139
+ throw new Error(`Bedrock API error: ${response.status} ${errorText}`);
140
+ }
141
+
142
+ if (!response.body) {
143
+ throw new Error("Response body is empty");
144
+ }
145
+
146
+ const reader = response.body.getReader();
147
+
148
+ /** @type {BedrockAssistantContentBlockWithPartial[]} */
149
+ const contentBlocks = [];
150
+ /** @type {Record<number, BedrockAssistantContentBlockWithPartial>} */
151
+ const contentBlockMap = {};
152
+ /** @type {BedrockUsage | undefined} */
153
+ let usage;
154
+
155
+ // Process stream events
156
+ for await (const event of readBedrockStreamEvents(reader)) {
157
+ const bedrockEvent = /** @type {BedrockStreamEvent} */ (event);
158
+
159
+ if (input.onPartialMessageContent) {
160
+ const partialContents = convertBedrockStreamEventToPartialContent(
161
+ bedrockEvent,
162
+ contentBlockMap,
163
+ );
164
+ for (const partialContent of partialContents) {
165
+ input.onPartialMessageContent(partialContent);
166
+ }
167
+ }
168
+
169
+ // Handle Converse API events (flat structure)
170
+ // Check for start event first
171
+ if ("contentBlockIndex" in bedrockEvent && "start" in bedrockEvent) {
172
+ const index = bedrockEvent.contentBlockIndex;
173
+ const start = bedrockEvent.start;
174
+
175
+ if (start.toolUse) {
176
+ contentBlockMap[index] = {
177
+ toolUse: {
178
+ toolUseId: start.toolUse.toolUseId || "",
179
+ name: start.toolUse.name || "",
180
+ input: {},
181
+ },
182
+ };
183
+ }
184
+ }
185
+
186
+ if ("contentBlockIndex" in bedrockEvent && "delta" in bedrockEvent) {
187
+ const index = bedrockEvent.contentBlockIndex;
188
+ const delta = bedrockEvent.delta;
189
+
190
+ // Initialize content block if not exists
191
+ if (!contentBlockMap[index]) {
192
+ if (delta.text !== undefined) {
193
+ contentBlockMap[index] = { text: "" };
194
+ } else if (delta.toolUse) {
195
+ contentBlockMap[index] = {
196
+ toolUse: {
197
+ toolUseId: delta.toolUse.toolUseId || "",
198
+ name: delta.toolUse.name || "",
199
+ input: {},
200
+ },
201
+ };
202
+ } else if (delta.reasoningContent) {
203
+ contentBlockMap[index] = {
204
+ reasoningContent: {
205
+ text: undefined,
206
+ signature: undefined,
207
+ redactedContent: undefined,
208
+ },
209
+ };
210
+ }
211
+ }
212
+
213
+ const block = contentBlockMap[index];
214
+
215
+ // Accumulate content
216
+ if (block && delta.text !== undefined && "text" in block) {
217
+ block.text += delta.text;
218
+ } else if (
219
+ block &&
220
+ delta.toolUse &&
221
+ "toolUse" in block &&
222
+ block.toolUse
223
+ ) {
224
+ // Accumulate tool input as JSON string
225
+ if (!block._partialInput) {
226
+ block._partialInput = "";
227
+ }
228
+ block._partialInput += delta.toolUse.input || "";
229
+ } else if (
230
+ block &&
231
+ delta.reasoningContent &&
232
+ "reasoningContent" in block &&
233
+ block.reasoningContent
234
+ ) {
235
+ if (delta.reasoningContent.text) {
236
+ block.reasoningContent.text =
237
+ (block.reasoningContent.text || "") + delta.reasoningContent.text;
238
+ }
239
+ if (delta.reasoningContent.signature) {
240
+ block.reasoningContent.signature = delta.reasoningContent.signature;
241
+ }
242
+ if (delta.reasoningContent.redactedContent) {
243
+ block.reasoningContent.redactedContent =
244
+ delta.reasoningContent.redactedContent;
245
+ }
246
+ }
247
+ }
248
+
249
+ // Handle message stop
250
+ if ("stopReason" in bedrockEvent) {
251
+ // Finalize all content blocks
252
+ for (const [_index, block] of Object.entries(contentBlockMap)) {
253
+ // Parse accumulated tool input JSON
254
+ if (
255
+ block &&
256
+ "toolUse" in block &&
257
+ block.toolUse &&
258
+ block._partialInput
259
+ ) {
260
+ try {
261
+ block.toolUse.input = JSON.parse(block._partialInput);
262
+ } catch (err) {
263
+ console.error(
264
+ styleText(
265
+ "red",
266
+ `Failed to parse tool input JSON for tool "${block.toolUse.name}": ${block._partialInput}`,
267
+ ),
268
+ );
269
+ block.toolUse.input = {
270
+ err: String(err),
271
+ raw: block._partialInput,
272
+ };
273
+ }
274
+ delete block._partialInput;
275
+ }
276
+ contentBlocks.push(block);
277
+ }
278
+ }
279
+
280
+ // Handle metadata
281
+ if ("usage" in bedrockEvent && "metrics" in bedrockEvent) {
282
+ usage = bedrockEvent.usage;
283
+ }
284
+ }
285
+
286
+ const message =
287
+ convertBedrockContentBlocksToAssistantMessage(contentBlocks);
288
+
289
+ const providerTokenUsage = usage
290
+ ? {
291
+ inputTokens: usage.inputTokens,
292
+ outputTokens: usage.outputTokens,
293
+ totalTokens: usage.totalTokens,
294
+ ...(usage.cacheReadInputTokens && {
295
+ cacheReadInputTokens: usage.cacheReadInputTokens,
296
+ }),
297
+ ...(usage.cacheWriteInputTokens && {
298
+ cacheWriteInputTokens: usage.cacheWriteInputTokens,
299
+ }),
300
+ }
301
+ : {};
302
+
303
+ return {
304
+ message,
305
+ providerTokenUsage,
306
+ };
307
+ });
308
+ }
309
+
310
+ /**
311
+ * @param {Message[]} messages
312
+ * @returns {BedrockMessage[]}
313
+ */
314
+ function convertGenericMessageToBedrockFormat(messages) {
315
+ /** @type {BedrockMessage[]} */
316
+ const bedrockMessages = [];
317
+
318
+ for (const message of messages) {
319
+ if (message.role === "system") {
320
+ // System messages handled separately
321
+ continue;
322
+ }
323
+
324
+ if (message.role === "user") {
325
+ /** @type {BedrockContentBlock[]} */
326
+ const content = [];
327
+
328
+ for (const part of message.content) {
329
+ if (part.type === "text" && part.text) {
330
+ // Only include non-empty text blocks
331
+ content.push({ text: part.text });
332
+ } else if (part.type === "image") {
333
+ content.push({
334
+ image: {
335
+ format: /** @type {"png" | "jpeg" | "gif" | "webp"} */ (
336
+ part.mimeType.split("/")[1]
337
+ ),
338
+ source: {
339
+ bytes: part.data,
340
+ },
341
+ },
342
+ });
343
+ } else if (part.type === "tool_result") {
344
+ /** @type {BedrockToolResultContent[]} */
345
+ const toolResultContent = [];
346
+ for (const resultPart of part.content) {
347
+ if (resultPart.type === "text") {
348
+ toolResultContent.push({ text: resultPart.text });
349
+ } else if (resultPart.type === "image") {
350
+ toolResultContent.push({
351
+ image: {
352
+ format: /** @type {"png" | "jpeg" | "gif" | "webp"} */ (
353
+ resultPart.mimeType.split("/")[1]
354
+ ),
355
+ source: {
356
+ bytes: resultPart.data,
357
+ },
358
+ },
359
+ });
360
+ }
361
+ }
362
+
363
+ content.push({
364
+ toolResult: {
365
+ toolUseId: part.toolUseId,
366
+ content: toolResultContent,
367
+ status: part.isError ? "error" : "success",
368
+ },
369
+ });
370
+ }
371
+ }
372
+
373
+ bedrockMessages.push({ role: "user", content });
374
+ } else if (message.role === "assistant") {
375
+ /** @type {BedrockAssistantContentBlock[]} */
376
+ const content = [];
377
+
378
+ for (const part of message.content) {
379
+ if (part.type === "text") {
380
+ content.push({ text: part.text });
381
+ } else if (part.type === "thinking") {
382
+ // Extended thinking requires signature for multi-turn conversations
383
+ const signature = /** @type {string | undefined} */ (
384
+ part.provider?.fields?.signature
385
+ );
386
+ if (signature) {
387
+ content.push({
388
+ reasoningContent: {
389
+ reasoningText: {
390
+ text: part.thinking,
391
+ signature,
392
+ },
393
+ },
394
+ });
395
+ }
396
+ } else if (part.type === "redacted_thinking") {
397
+ // Redacted thinking must be included in message history
398
+ const data = /** @type {string | undefined} */ (
399
+ part.provider?.fields?.data
400
+ );
401
+ if (data) {
402
+ content.push({
403
+ reasoningContent: {
404
+ redactedContent: data,
405
+ },
406
+ });
407
+ }
408
+ } else if (part.type === "tool_use") {
409
+ content.push({
410
+ toolUse: {
411
+ toolUseId: part.toolUseId,
412
+ name: part.toolName,
413
+ input: part.input,
414
+ },
415
+ });
416
+ }
417
+ }
418
+
419
+ bedrockMessages.push({ role: "assistant", content });
420
+ }
421
+ }
422
+
423
+ return bedrockMessages;
424
+ }
425
+
426
+ /**
427
+ * @param {Message[]} messages
428
+ * @param {boolean} [enablePromptCaching]
429
+ * @returns {import("./bedrock").BedrockSystemContentBlock[]}
430
+ */
431
+ function extractSystemMessages(messages, enablePromptCaching = false) {
432
+ /** @type {import("./bedrock").BedrockSystemContentBlock[]} */
433
+ const systemBlocks = [];
434
+
435
+ for (const message of messages) {
436
+ if (message.role === "system") {
437
+ for (const part of message.content) {
438
+ systemBlocks.push({ text: part.text });
439
+ }
440
+ }
441
+ }
442
+
443
+ // Add cache point at the end of system messages if enabled
444
+ if (enablePromptCaching && systemBlocks.length > 0) {
445
+ systemBlocks.push({ cachePoint: { type: "default" } });
446
+ }
447
+
448
+ return systemBlocks;
449
+ }
450
+
451
+ /**
452
+ * @param {ToolDefinition[]} tools
453
+ * @returns {BedrockTool[]}
454
+ */
455
+ function convertGenericToolDefinitionToBedrockFormat(tools) {
456
+ return tools.map((tool) => ({
457
+ toolSpec: {
458
+ name: tool.name,
459
+ description: tool.description,
460
+ inputSchema: {
461
+ json: tool.inputSchema,
462
+ },
463
+ },
464
+ }));
465
+ }
466
+
467
+ /**
468
+ * @param {BedrockMessage[]} messages
469
+ * @returns {BedrockMessage[]}
470
+ */
471
+ function enablePromptCaching(messages) {
472
+ // Find user message indices
473
+ const userMessageIndices = messages
474
+ .map((msg, index) => (msg.role === "user" ? index : -1))
475
+ .filter((index) => index !== -1);
476
+
477
+ // Target last two user messages for caching
478
+ const cacheTargetIndices = [
479
+ userMessageIndices.at(-1),
480
+ userMessageIndices.at(-2),
481
+ ].filter((index) => index !== undefined);
482
+
483
+ const cachedMessages = messages.map((message, index) => {
484
+ if (cacheTargetIndices.includes(index)) {
485
+ // Add cache point as a separate block at the end
486
+ // Only add to messages without tool results (tool results don't support cachePoint)
487
+ if (message.role === "user") {
488
+ const content = /** @type {BedrockContentBlock[]} */ ([
489
+ ...message.content,
490
+ ]);
491
+ // Check if content contains toolResult
492
+ const hasToolResult = content.some(
493
+ (block) => "toolResult" in block && block.toolResult,
494
+ );
495
+ if (!hasToolResult) {
496
+ content.push({ cachePoint: { type: "default" } });
497
+ return { ...message, content };
498
+ }
499
+ }
500
+ if (message.role === "assistant") {
501
+ const content = /** @type {BedrockAssistantContentBlock[]} */ ([
502
+ ...message.content,
503
+ ]);
504
+ content.push({ cachePoint: { type: "default" } });
505
+ return { ...message, content };
506
+ }
507
+ }
508
+ return message;
509
+ });
510
+
511
+ return cachedMessages;
512
+ }
513
+
514
+ /**
515
+ * @param {BedrockStreamEvent} event
516
+ * @param {Record<number, import("./bedrock").BedrockAssistantContentBlockWithPartial>} contentBlockMap
517
+ * @returns {PartialMessageContent[]}
518
+ */
519
+ function convertBedrockStreamEventToPartialContent(event, contentBlockMap) {
520
+ /** @type {PartialMessageContent[]} */
521
+ const partialContents = [];
522
+
523
+ // Handle Converse API events (flat structure)
524
+ // Note: Don't send message start event here
525
+ // Each content block will send its own start event
526
+
527
+ // Handle tool use start event
528
+ if ("contentBlockIndex" in event && "start" in event) {
529
+ const index = event.contentBlockIndex;
530
+ const start = event.start;
531
+
532
+ // Send stop event for previous block if exists
533
+ if (index > 0 && contentBlockMap[index - 1]) {
534
+ const prevBlock = contentBlockMap[index - 1];
535
+ const prevType = prevBlock.text
536
+ ? "text"
537
+ : prevBlock.toolUse
538
+ ? "tool_use"
539
+ : prevBlock.reasoningContent
540
+ ? "thinking"
541
+ : "unknown";
542
+
543
+ partialContents.push({
544
+ type: prevType,
545
+ position: "stop",
546
+ });
547
+ }
548
+
549
+ if (start.toolUse) {
550
+ partialContents.push({
551
+ type: "tool_use",
552
+ position: "start",
553
+ content: JSON.stringify({
554
+ toolUseId: start.toolUse.toolUseId,
555
+ name: start.toolUse.name,
556
+ }),
557
+ });
558
+ }
559
+ }
560
+
561
+ if ("contentBlockIndex" in event && "delta" in event) {
562
+ const delta = event.delta;
563
+ const index = event.contentBlockIndex;
564
+
565
+ // Check if this is a new block (no entry in contentBlockMap)
566
+ // If so, send stop event for previous block first
567
+ if (!contentBlockMap[index] && index > 0 && contentBlockMap[index - 1]) {
568
+ const prevBlock = contentBlockMap[index - 1];
569
+ const prevType = prevBlock.text
570
+ ? "text"
571
+ : prevBlock.toolUse
572
+ ? "tool_use"
573
+ : prevBlock.reasoningContent
574
+ ? "thinking"
575
+ : "unknown";
576
+
577
+ partialContents.push({
578
+ type: prevType,
579
+ position: "stop",
580
+ });
581
+ }
582
+
583
+ if (delta.text !== undefined) {
584
+ // Send start event if this is a new text block
585
+ if (!contentBlockMap[index]) {
586
+ partialContents.push({
587
+ type: "text",
588
+ position: "start",
589
+ content: "",
590
+ });
591
+ }
592
+ partialContents.push({
593
+ type: "text",
594
+ position: "delta",
595
+ content: delta.text,
596
+ });
597
+ } else if (delta.toolUse) {
598
+ // Don't send tool input deltas to onPartialMessageContent
599
+ // Tool input will be shown when tool call is complete
600
+ } else if (delta.reasoningContent) {
601
+ // Send start event if this is a new reasoningContent block
602
+ if (!contentBlockMap[index]) {
603
+ partialContents.push({
604
+ type: "thinking",
605
+ position: "start",
606
+ content: "",
607
+ });
608
+ }
609
+ // Reasoning content (text or redactedContent)
610
+ if (delta.reasoningContent.text) {
611
+ partialContents.push({
612
+ type: "thinking",
613
+ position: "delta",
614
+ content: delta.reasoningContent.text,
615
+ });
616
+ }
617
+ // Note: redactedContent is encrypted, so we don't display it
618
+ // but we still need to track it for the final message
619
+ }
620
+ }
621
+
622
+ if ("stopReason" in event) {
623
+ // Message stop event
624
+ const blocks = Object.values(contentBlockMap);
625
+ if (blocks.length > 0) {
626
+ const lastBlock = blocks[blocks.length - 1];
627
+ const type =
628
+ lastBlock && "text" in lastBlock
629
+ ? "text"
630
+ : lastBlock && "toolUse" in lastBlock
631
+ ? "tool_use"
632
+ : lastBlock && "reasoningContent" in lastBlock
633
+ ? "thinking"
634
+ : "unknown";
635
+
636
+ partialContents.push({
637
+ type,
638
+ position: "stop",
639
+ });
640
+ }
641
+ }
642
+
643
+ return partialContents;
644
+ }
645
+
646
+ /**
647
+ * @param {BedrockAssistantContentBlockWithPartial[]} contentBlocks
648
+ * @returns {AssistantMessage}
649
+ */
650
+ function convertBedrockContentBlocksToAssistantMessage(contentBlocks) {
651
+ /** @type {AssistantMessage["content"]} */
652
+ const content = [];
653
+
654
+ for (const block of contentBlocks) {
655
+ if (block.text) {
656
+ // Only include non-empty text blocks
657
+ content.push({
658
+ type: "text",
659
+ text: block.text,
660
+ });
661
+ } else if (block.toolUse) {
662
+ content.push({
663
+ type: "tool_use",
664
+ toolUseId: block.toolUse.toolUseId || "",
665
+ toolName: block.toolUse.name || "",
666
+ input:
667
+ /** @type {Record<string, unknown>} */ (block.toolUse.input) ??
668
+ /** @type {Record<string, unknown>} */ ({}),
669
+ });
670
+ } else if (block.reasoningContent) {
671
+ // Reasoning content
672
+ if (block.reasoningContent.text) {
673
+ content.push({
674
+ type: "thinking",
675
+ thinking: block.reasoningContent.text,
676
+ ...(block.reasoningContent.signature && {
677
+ provider: {
678
+ fields: { signature: block.reasoningContent.signature },
679
+ },
680
+ }),
681
+ });
682
+ } else if (block.reasoningContent.redactedContent) {
683
+ content.push({
684
+ type: "redacted_thinking",
685
+ provider: {
686
+ fields: { data: block.reasoningContent.redactedContent },
687
+ },
688
+ });
689
+ }
690
+ }
691
+ }
692
+
693
+ return {
694
+ role: "assistant",
695
+ content,
696
+ };
697
+ }
698
+
699
+ /**
700
+ * @param {string} baseURL
701
+ * @returns {string}
702
+ */
703
+ function extractRegionFromBaseURL(baseURL) {
704
+ const match = baseURL.match(/bedrock-runtime\.([^.]+)\.amazonaws\.com/);
705
+ if (!match) {
706
+ throw new Error(`Failed to extract region from baseURL: ${baseURL}`);
707
+ }
708
+ return match[1];
709
+ }
@@ -44,6 +44,7 @@ export async function* readBedrockStreamEvents(reader) {
44
44
  try {
45
45
  const payloadParsed = JSON.parse(payloadDecoded);
46
46
  if (payloadParsed.bytes) {
47
+ // Invoke API format (base64 encoded event)
47
48
  const event = Buffer.from(payloadParsed.bytes, "base64").toString(
48
49
  "utf-8",
49
50
  );
@@ -56,6 +57,9 @@ export async function* readBedrockStreamEvents(reader) {
56
57
  `Bedrock message received: ${JSON.stringify(payloadParsed.message)}`,
57
58
  ),
58
59
  );
60
+ } else {
61
+ // Converse API format (direct event data)
62
+ yield payloadParsed;
59
63
  }
60
64
  } catch (err) {
61
65
  if (err instanceof Error) {
@@ -16,6 +16,30 @@ export function evalJSONConfig(configItem) {
16
16
  return new RegExp(configItem.$regex);
17
17
  }
18
18
 
19
+ if (
20
+ Object.keys(configItem).length === 1 &&
21
+ "$env" in configItem &&
22
+ typeof configItem.$env === "string"
23
+ ) {
24
+ const value = process.env[configItem.$env];
25
+ if (value === undefined) {
26
+ throw new Error(
27
+ `Environment variable '${configItem.$env}' is not defined`,
28
+ );
29
+ }
30
+ return value;
31
+ }
32
+
33
+ if (
34
+ Object.keys(configItem).length === 1 &&
35
+ "$env" in configItem &&
36
+ typeof configItem.$env !== "string"
37
+ ) {
38
+ throw new Error(
39
+ `The value of '$env' must be a string, got: ${typeof configItem.$env}`,
40
+ );
41
+ }
42
+
19
43
  if (Object.keys(configItem).length === 1 && "$has" in configItem) {
20
44
  const pattern = evalJSONConfig(configItem.$has);
21
45
  /** @param {unknown} value */