@iinm/plain-agent 1.3.3 → 1.4.1

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.3",
3
+ "version": "1.4.1",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/mcp.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
+ * @import { Client } from "@modelcontextprotocol/client";
2
3
  * @import { StructuredToolResultContent, Tool, ToolImplementation } from "./tool";
3
4
  * @import { MCPServerConfig } from "./config";
4
5
  */
5
6
 
6
7
  import { mkdir, open } from "node:fs/promises";
7
8
  import path from "node:path";
8
- import { Client } from "@modelcontextprotocol/client";
9
9
  import { AGENT_PROJECT_METADATA_DIR } from "./env.mjs";
10
10
  import { writeTmpFile } from "./tmpfile.mjs";
11
11
  import { noThrow } from "./utils/noThrow.mjs";
@@ -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}
@@ -5,10 +5,6 @@
5
5
  */
6
6
 
7
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
8
  import { noThrow } from "../utils/noThrow.mjs";
13
9
  import { readBedrockStreamEvents } from "./platform/bedrock.mjs";
14
10
  import { getGoogleCloudAccessToken } from "./platform/googleCloud.mjs";
@@ -112,6 +108,11 @@ export async function callAnthropicModel(
112
108
 
113
109
  // bedrock + sso profile
114
110
  const runFetchForBedrock = async () => {
111
+ const { Sha256 } = await import("@aws-crypto/sha256-js");
112
+ const { fromIni } = await import("@aws-sdk/credential-providers");
113
+ const { HttpRequest } = await import("@smithy/protocol-http");
114
+ const { SignatureV4 } = await import("@smithy/signature-v4");
115
+
115
116
  const region =
116
117
  url.match(/bedrock-runtime\.([\w-]+)\.amazonaws\.com/)?.[1] ?? "";
117
118
  const urlParsed = new URL(url);
@@ -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,710 @@
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 { noThrow } from "../utils/noThrow.mjs";
9
+ import { readBedrockStreamEvents } from "./platform/bedrock.mjs";
10
+
11
+ /**
12
+ * @param {import("../modelDefinition").PlatformConfig} platformConfig
13
+ * @param {BedrockConverseModelConfig} modelConfig
14
+ * @param {ModelInput} input
15
+ * @param {number} [retryCount]
16
+ * @returns {Promise<ModelOutput | Error>}
17
+ */
18
+ export async function callBedrockConverseModel(
19
+ platformConfig,
20
+ modelConfig,
21
+ input,
22
+ retryCount = 0,
23
+ ) {
24
+ const { Sha256 } = await import("@aws-crypto/sha256-js");
25
+ const { fromIni } = await import("@aws-sdk/credential-providers");
26
+ const { HttpRequest } = await import("@smithy/protocol-http");
27
+ const { SignatureV4 } = await import("@smithy/signature-v4");
28
+
29
+ return await noThrow(async () => {
30
+ const messages = convertGenericMessageToBedrockFormat(input.messages);
31
+ const cachedMessages = modelConfig.enablePromptCaching
32
+ ? enablePromptCaching(messages)
33
+ : messages;
34
+ const tools = convertGenericToolDefinitionToBedrockFormat(
35
+ input.tools || [],
36
+ );
37
+
38
+ const url = (() => {
39
+ const baseURL = platformConfig.baseURL;
40
+ if (platformConfig.name !== "bedrock") {
41
+ throw new Error(`Unsupported platform: ${platformConfig.name}`);
42
+ }
43
+ return `${baseURL}/model/${modelConfig.model}/converse-stream`;
44
+ })();
45
+
46
+ const region = extractRegionFromBaseURL(platformConfig.baseURL);
47
+
48
+ /** @type {BedrockConverseRequest} */
49
+ const request = {
50
+ messages: cachedMessages,
51
+ ...(modelConfig.inferenceConfig && {
52
+ inferenceConfig: modelConfig.inferenceConfig,
53
+ }),
54
+ ...(modelConfig.additionalModelRequestFields && {
55
+ additionalModelRequestFields: modelConfig.additionalModelRequestFields,
56
+ }),
57
+ };
58
+
59
+ // Add system messages if present
60
+ const systemMessages = extractSystemMessages(
61
+ input.messages,
62
+ modelConfig.enablePromptCaching,
63
+ );
64
+ if (systemMessages.length > 0) {
65
+ request.system = systemMessages;
66
+ }
67
+
68
+ // Add tools if present
69
+ if (tools.length > 0) {
70
+ request.toolConfig = {
71
+ tools: tools,
72
+ };
73
+ }
74
+
75
+ const payload = JSON.stringify(request);
76
+
77
+ // Sign request with AWS Signature V4
78
+ const signer = new SignatureV4({
79
+ credentials: fromIni({ profile: platformConfig.awsProfile }),
80
+ region,
81
+ service: "bedrock",
82
+ sha256: Sha256,
83
+ });
84
+
85
+ const urlParsed = new URL(url);
86
+ const { hostname, pathname } = urlParsed;
87
+
88
+ const req = new HttpRequest({
89
+ protocol: "https:",
90
+ method: "POST",
91
+ hostname,
92
+ path: pathname,
93
+ headers: {
94
+ host: hostname,
95
+ "Content-Type": "application/json",
96
+ },
97
+ body: payload,
98
+ });
99
+
100
+ const signed = await signer.sign(req);
101
+
102
+ const response = await fetch(url, {
103
+ method: signed.method,
104
+ headers: signed.headers,
105
+ body: signed.body,
106
+ signal: AbortSignal.timeout(120 * 1000),
107
+ });
108
+
109
+ if (response.status !== 200) {
110
+ const errorText = await response.text();
111
+ console.error(
112
+ styleText("red", `Bedrock API error: ${response.status} ${errorText}`),
113
+ );
114
+
115
+ // Retry on throttling or server errors
116
+ if (
117
+ (response.status === 429 ||
118
+ response.status === 502 ||
119
+ response.status === 503) &&
120
+ retryCount < 3
121
+ ) {
122
+ const retryInterval = Math.min(2 * 2 ** retryCount, 16);
123
+ console.error(
124
+ styleText(
125
+ "yellow",
126
+ `Retrying in ${retryInterval} seconds... (attempt ${retryCount + 1})`,
127
+ ),
128
+ );
129
+ await new Promise((resolve) =>
130
+ setTimeout(resolve, retryInterval * 1000),
131
+ );
132
+ return callBedrockConverseModel(
133
+ platformConfig,
134
+ modelConfig,
135
+ input,
136
+ retryCount + 1,
137
+ );
138
+ }
139
+
140
+ throw new Error(`Bedrock API error: ${response.status} ${errorText}`);
141
+ }
142
+
143
+ if (!response.body) {
144
+ throw new Error("Response body is empty");
145
+ }
146
+
147
+ const reader = response.body.getReader();
148
+
149
+ /** @type {BedrockAssistantContentBlockWithPartial[]} */
150
+ const contentBlocks = [];
151
+ /** @type {Record<number, BedrockAssistantContentBlockWithPartial>} */
152
+ const contentBlockMap = {};
153
+ /** @type {BedrockUsage | undefined} */
154
+ let usage;
155
+
156
+ // Process stream events
157
+ for await (const event of readBedrockStreamEvents(reader)) {
158
+ const bedrockEvent = /** @type {BedrockStreamEvent} */ (event);
159
+
160
+ if (input.onPartialMessageContent) {
161
+ const partialContents = convertBedrockStreamEventToPartialContent(
162
+ bedrockEvent,
163
+ contentBlockMap,
164
+ );
165
+ for (const partialContent of partialContents) {
166
+ input.onPartialMessageContent(partialContent);
167
+ }
168
+ }
169
+
170
+ // Handle Converse API events (flat structure)
171
+ // Check for start event first
172
+ if ("contentBlockIndex" in bedrockEvent && "start" in bedrockEvent) {
173
+ const index = bedrockEvent.contentBlockIndex;
174
+ const start = bedrockEvent.start;
175
+
176
+ if (start.toolUse) {
177
+ contentBlockMap[index] = {
178
+ toolUse: {
179
+ toolUseId: start.toolUse.toolUseId || "",
180
+ name: start.toolUse.name || "",
181
+ input: {},
182
+ },
183
+ };
184
+ }
185
+ }
186
+
187
+ if ("contentBlockIndex" in bedrockEvent && "delta" in bedrockEvent) {
188
+ const index = bedrockEvent.contentBlockIndex;
189
+ const delta = bedrockEvent.delta;
190
+
191
+ // Initialize content block if not exists
192
+ if (!contentBlockMap[index]) {
193
+ if (delta.text !== undefined) {
194
+ contentBlockMap[index] = { text: "" };
195
+ } else if (delta.toolUse) {
196
+ contentBlockMap[index] = {
197
+ toolUse: {
198
+ toolUseId: delta.toolUse.toolUseId || "",
199
+ name: delta.toolUse.name || "",
200
+ input: {},
201
+ },
202
+ };
203
+ } else if (delta.reasoningContent) {
204
+ contentBlockMap[index] = {
205
+ reasoningContent: {
206
+ text: undefined,
207
+ signature: undefined,
208
+ redactedContent: undefined,
209
+ },
210
+ };
211
+ }
212
+ }
213
+
214
+ const block = contentBlockMap[index];
215
+
216
+ // Accumulate content
217
+ if (block && delta.text !== undefined && "text" in block) {
218
+ block.text += delta.text;
219
+ } else if (
220
+ block &&
221
+ delta.toolUse &&
222
+ "toolUse" in block &&
223
+ block.toolUse
224
+ ) {
225
+ // Accumulate tool input as JSON string
226
+ if (!block._partialInput) {
227
+ block._partialInput = "";
228
+ }
229
+ block._partialInput += delta.toolUse.input || "";
230
+ } else if (
231
+ block &&
232
+ delta.reasoningContent &&
233
+ "reasoningContent" in block &&
234
+ block.reasoningContent
235
+ ) {
236
+ if (delta.reasoningContent.text) {
237
+ block.reasoningContent.text =
238
+ (block.reasoningContent.text || "") + delta.reasoningContent.text;
239
+ }
240
+ if (delta.reasoningContent.signature) {
241
+ block.reasoningContent.signature = delta.reasoningContent.signature;
242
+ }
243
+ if (delta.reasoningContent.redactedContent) {
244
+ block.reasoningContent.redactedContent =
245
+ delta.reasoningContent.redactedContent;
246
+ }
247
+ }
248
+ }
249
+
250
+ // Handle message stop
251
+ if ("stopReason" in bedrockEvent) {
252
+ // Finalize all content blocks
253
+ for (const [_index, block] of Object.entries(contentBlockMap)) {
254
+ // Parse accumulated tool input JSON
255
+ if (
256
+ block &&
257
+ "toolUse" in block &&
258
+ block.toolUse &&
259
+ block._partialInput
260
+ ) {
261
+ try {
262
+ block.toolUse.input = JSON.parse(block._partialInput);
263
+ } catch (err) {
264
+ console.error(
265
+ styleText(
266
+ "red",
267
+ `Failed to parse tool input JSON for tool "${block.toolUse.name}": ${block._partialInput}`,
268
+ ),
269
+ );
270
+ block.toolUse.input = {
271
+ err: String(err),
272
+ raw: block._partialInput,
273
+ };
274
+ }
275
+ delete block._partialInput;
276
+ }
277
+ contentBlocks.push(block);
278
+ }
279
+ }
280
+
281
+ // Handle metadata
282
+ if ("usage" in bedrockEvent && "metrics" in bedrockEvent) {
283
+ usage = bedrockEvent.usage;
284
+ }
285
+ }
286
+
287
+ const message =
288
+ convertBedrockContentBlocksToAssistantMessage(contentBlocks);
289
+
290
+ const providerTokenUsage = usage
291
+ ? {
292
+ inputTokens: usage.inputTokens,
293
+ outputTokens: usage.outputTokens,
294
+ totalTokens: usage.totalTokens,
295
+ ...(usage.cacheReadInputTokens && {
296
+ cacheReadInputTokens: usage.cacheReadInputTokens,
297
+ }),
298
+ ...(usage.cacheWriteInputTokens && {
299
+ cacheWriteInputTokens: usage.cacheWriteInputTokens,
300
+ }),
301
+ }
302
+ : {};
303
+
304
+ return {
305
+ message,
306
+ providerTokenUsage,
307
+ };
308
+ });
309
+ }
310
+
311
+ /**
312
+ * @param {Message[]} messages
313
+ * @returns {BedrockMessage[]}
314
+ */
315
+ function convertGenericMessageToBedrockFormat(messages) {
316
+ /** @type {BedrockMessage[]} */
317
+ const bedrockMessages = [];
318
+
319
+ for (const message of messages) {
320
+ if (message.role === "system") {
321
+ // System messages handled separately
322
+ continue;
323
+ }
324
+
325
+ if (message.role === "user") {
326
+ /** @type {BedrockContentBlock[]} */
327
+ const content = [];
328
+
329
+ for (const part of message.content) {
330
+ if (part.type === "text" && part.text) {
331
+ // Only include non-empty text blocks
332
+ content.push({ text: part.text });
333
+ } else if (part.type === "image") {
334
+ content.push({
335
+ image: {
336
+ format: /** @type {"png" | "jpeg" | "gif" | "webp"} */ (
337
+ part.mimeType.split("/")[1]
338
+ ),
339
+ source: {
340
+ bytes: part.data,
341
+ },
342
+ },
343
+ });
344
+ } else if (part.type === "tool_result") {
345
+ /** @type {BedrockToolResultContent[]} */
346
+ const toolResultContent = [];
347
+ for (const resultPart of part.content) {
348
+ if (resultPart.type === "text") {
349
+ toolResultContent.push({ text: resultPart.text });
350
+ } else if (resultPart.type === "image") {
351
+ toolResultContent.push({
352
+ image: {
353
+ format: /** @type {"png" | "jpeg" | "gif" | "webp"} */ (
354
+ resultPart.mimeType.split("/")[1]
355
+ ),
356
+ source: {
357
+ bytes: resultPart.data,
358
+ },
359
+ },
360
+ });
361
+ }
362
+ }
363
+
364
+ content.push({
365
+ toolResult: {
366
+ toolUseId: part.toolUseId,
367
+ content: toolResultContent,
368
+ status: part.isError ? "error" : "success",
369
+ },
370
+ });
371
+ }
372
+ }
373
+
374
+ bedrockMessages.push({ role: "user", content });
375
+ } else if (message.role === "assistant") {
376
+ /** @type {BedrockAssistantContentBlock[]} */
377
+ const content = [];
378
+
379
+ for (const part of message.content) {
380
+ if (part.type === "text") {
381
+ content.push({ text: part.text });
382
+ } else if (part.type === "thinking") {
383
+ // Extended thinking requires signature for multi-turn conversations
384
+ const signature = /** @type {string | undefined} */ (
385
+ part.provider?.fields?.signature
386
+ );
387
+ if (signature) {
388
+ content.push({
389
+ reasoningContent: {
390
+ reasoningText: {
391
+ text: part.thinking,
392
+ signature,
393
+ },
394
+ },
395
+ });
396
+ }
397
+ } else if (part.type === "redacted_thinking") {
398
+ // Redacted thinking must be included in message history
399
+ const data = /** @type {string | undefined} */ (
400
+ part.provider?.fields?.data
401
+ );
402
+ if (data) {
403
+ content.push({
404
+ reasoningContent: {
405
+ redactedContent: data,
406
+ },
407
+ });
408
+ }
409
+ } else if (part.type === "tool_use") {
410
+ content.push({
411
+ toolUse: {
412
+ toolUseId: part.toolUseId,
413
+ name: part.toolName,
414
+ input: part.input,
415
+ },
416
+ });
417
+ }
418
+ }
419
+
420
+ bedrockMessages.push({ role: "assistant", content });
421
+ }
422
+ }
423
+
424
+ return bedrockMessages;
425
+ }
426
+
427
+ /**
428
+ * @param {Message[]} messages
429
+ * @param {boolean} [enablePromptCaching]
430
+ * @returns {import("./bedrock").BedrockSystemContentBlock[]}
431
+ */
432
+ function extractSystemMessages(messages, enablePromptCaching = false) {
433
+ /** @type {import("./bedrock").BedrockSystemContentBlock[]} */
434
+ const systemBlocks = [];
435
+
436
+ for (const message of messages) {
437
+ if (message.role === "system") {
438
+ for (const part of message.content) {
439
+ systemBlocks.push({ text: part.text });
440
+ }
441
+ }
442
+ }
443
+
444
+ // Add cache point at the end of system messages if enabled
445
+ if (enablePromptCaching && systemBlocks.length > 0) {
446
+ systemBlocks.push({ cachePoint: { type: "default" } });
447
+ }
448
+
449
+ return systemBlocks;
450
+ }
451
+
452
+ /**
453
+ * @param {ToolDefinition[]} tools
454
+ * @returns {BedrockTool[]}
455
+ */
456
+ function convertGenericToolDefinitionToBedrockFormat(tools) {
457
+ return tools.map((tool) => ({
458
+ toolSpec: {
459
+ name: tool.name,
460
+ description: tool.description,
461
+ inputSchema: {
462
+ json: tool.inputSchema,
463
+ },
464
+ },
465
+ }));
466
+ }
467
+
468
+ /**
469
+ * @param {BedrockMessage[]} messages
470
+ * @returns {BedrockMessage[]}
471
+ */
472
+ function enablePromptCaching(messages) {
473
+ // Find user message indices
474
+ const userMessageIndices = messages
475
+ .map((msg, index) => (msg.role === "user" ? index : -1))
476
+ .filter((index) => index !== -1);
477
+
478
+ // Target last two user messages for caching
479
+ const cacheTargetIndices = [
480
+ userMessageIndices.at(-1),
481
+ userMessageIndices.at(-2),
482
+ ].filter((index) => index !== undefined);
483
+
484
+ const cachedMessages = messages.map((message, index) => {
485
+ if (cacheTargetIndices.includes(index)) {
486
+ // Add cache point as a separate block at the end
487
+ // Only add to messages without tool results (tool results don't support cachePoint)
488
+ if (message.role === "user") {
489
+ const content = /** @type {BedrockContentBlock[]} */ ([
490
+ ...message.content,
491
+ ]);
492
+ // Check if content contains toolResult
493
+ const hasToolResult = content.some(
494
+ (block) => "toolResult" in block && block.toolResult,
495
+ );
496
+ if (!hasToolResult) {
497
+ content.push({ cachePoint: { type: "default" } });
498
+ return { ...message, content };
499
+ }
500
+ }
501
+ if (message.role === "assistant") {
502
+ const content = /** @type {BedrockAssistantContentBlock[]} */ ([
503
+ ...message.content,
504
+ ]);
505
+ content.push({ cachePoint: { type: "default" } });
506
+ return { ...message, content };
507
+ }
508
+ }
509
+ return message;
510
+ });
511
+
512
+ return cachedMessages;
513
+ }
514
+
515
+ /**
516
+ * @param {BedrockStreamEvent} event
517
+ * @param {Record<number, import("./bedrock").BedrockAssistantContentBlockWithPartial>} contentBlockMap
518
+ * @returns {PartialMessageContent[]}
519
+ */
520
+ function convertBedrockStreamEventToPartialContent(event, contentBlockMap) {
521
+ /** @type {PartialMessageContent[]} */
522
+ const partialContents = [];
523
+
524
+ // Handle Converse API events (flat structure)
525
+ // Note: Don't send message start event here
526
+ // Each content block will send its own start event
527
+
528
+ // Handle tool use start event
529
+ if ("contentBlockIndex" in event && "start" in event) {
530
+ const index = event.contentBlockIndex;
531
+ const start = event.start;
532
+
533
+ // Send stop event for previous block if exists
534
+ if (index > 0 && contentBlockMap[index - 1]) {
535
+ const prevBlock = contentBlockMap[index - 1];
536
+ const prevType = prevBlock.text
537
+ ? "text"
538
+ : prevBlock.toolUse
539
+ ? "tool_use"
540
+ : prevBlock.reasoningContent
541
+ ? "thinking"
542
+ : "unknown";
543
+
544
+ partialContents.push({
545
+ type: prevType,
546
+ position: "stop",
547
+ });
548
+ }
549
+
550
+ if (start.toolUse) {
551
+ partialContents.push({
552
+ type: "tool_use",
553
+ position: "start",
554
+ content: JSON.stringify({
555
+ toolUseId: start.toolUse.toolUseId,
556
+ name: start.toolUse.name,
557
+ }),
558
+ });
559
+ }
560
+ }
561
+
562
+ if ("contentBlockIndex" in event && "delta" in event) {
563
+ const delta = event.delta;
564
+ const index = event.contentBlockIndex;
565
+
566
+ // Check if this is a new block (no entry in contentBlockMap)
567
+ // If so, send stop event for previous block first
568
+ if (!contentBlockMap[index] && index > 0 && contentBlockMap[index - 1]) {
569
+ const prevBlock = contentBlockMap[index - 1];
570
+ const prevType = prevBlock.text
571
+ ? "text"
572
+ : prevBlock.toolUse
573
+ ? "tool_use"
574
+ : prevBlock.reasoningContent
575
+ ? "thinking"
576
+ : "unknown";
577
+
578
+ partialContents.push({
579
+ type: prevType,
580
+ position: "stop",
581
+ });
582
+ }
583
+
584
+ if (delta.text !== undefined) {
585
+ // Send start event if this is a new text block
586
+ if (!contentBlockMap[index]) {
587
+ partialContents.push({
588
+ type: "text",
589
+ position: "start",
590
+ content: "",
591
+ });
592
+ }
593
+ partialContents.push({
594
+ type: "text",
595
+ position: "delta",
596
+ content: delta.text,
597
+ });
598
+ } else if (delta.toolUse) {
599
+ // Don't send tool input deltas to onPartialMessageContent
600
+ // Tool input will be shown when tool call is complete
601
+ } else if (delta.reasoningContent) {
602
+ // Send start event if this is a new reasoningContent block
603
+ if (!contentBlockMap[index]) {
604
+ partialContents.push({
605
+ type: "thinking",
606
+ position: "start",
607
+ content: "",
608
+ });
609
+ }
610
+ // Reasoning content (text or redactedContent)
611
+ if (delta.reasoningContent.text) {
612
+ partialContents.push({
613
+ type: "thinking",
614
+ position: "delta",
615
+ content: delta.reasoningContent.text,
616
+ });
617
+ }
618
+ // Note: redactedContent is encrypted, so we don't display it
619
+ // but we still need to track it for the final message
620
+ }
621
+ }
622
+
623
+ if ("stopReason" in event) {
624
+ // Message stop event
625
+ const blocks = Object.values(contentBlockMap);
626
+ if (blocks.length > 0) {
627
+ const lastBlock = blocks[blocks.length - 1];
628
+ const type =
629
+ lastBlock && "text" in lastBlock
630
+ ? "text"
631
+ : lastBlock && "toolUse" in lastBlock
632
+ ? "tool_use"
633
+ : lastBlock && "reasoningContent" in lastBlock
634
+ ? "thinking"
635
+ : "unknown";
636
+
637
+ partialContents.push({
638
+ type,
639
+ position: "stop",
640
+ });
641
+ }
642
+ }
643
+
644
+ return partialContents;
645
+ }
646
+
647
+ /**
648
+ * @param {BedrockAssistantContentBlockWithPartial[]} contentBlocks
649
+ * @returns {AssistantMessage}
650
+ */
651
+ function convertBedrockContentBlocksToAssistantMessage(contentBlocks) {
652
+ /** @type {AssistantMessage["content"]} */
653
+ const content = [];
654
+
655
+ for (const block of contentBlocks) {
656
+ if (block.text) {
657
+ // Only include non-empty text blocks
658
+ content.push({
659
+ type: "text",
660
+ text: block.text,
661
+ });
662
+ } else if (block.toolUse) {
663
+ content.push({
664
+ type: "tool_use",
665
+ toolUseId: block.toolUse.toolUseId || "",
666
+ toolName: block.toolUse.name || "",
667
+ input:
668
+ /** @type {Record<string, unknown>} */ (block.toolUse.input) ??
669
+ /** @type {Record<string, unknown>} */ ({}),
670
+ });
671
+ } else if (block.reasoningContent) {
672
+ // Reasoning content
673
+ if (block.reasoningContent.text) {
674
+ content.push({
675
+ type: "thinking",
676
+ thinking: block.reasoningContent.text,
677
+ ...(block.reasoningContent.signature && {
678
+ provider: {
679
+ fields: { signature: block.reasoningContent.signature },
680
+ },
681
+ }),
682
+ });
683
+ } else if (block.reasoningContent.redactedContent) {
684
+ content.push({
685
+ type: "redacted_thinking",
686
+ provider: {
687
+ fields: { data: block.reasoningContent.redactedContent },
688
+ },
689
+ });
690
+ }
691
+ }
692
+ }
693
+
694
+ return {
695
+ role: "assistant",
696
+ content,
697
+ };
698
+ }
699
+
700
+ /**
701
+ * @param {string} baseURL
702
+ * @returns {string}
703
+ */
704
+ function extractRegionFromBaseURL(baseURL) {
705
+ const match = baseURL.match(/bedrock-runtime\.([^.]+)\.amazonaws\.com/);
706
+ if (!match) {
707
+ throw new Error(`Failed to extract region from baseURL: ${baseURL}`);
708
+ }
709
+ return match[1];
710
+ }
@@ -5,10 +5,6 @@
5
5
  */
6
6
 
7
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
8
  import { noThrow } from "../utils/noThrow.mjs";
13
9
  import { retryOnError } from "../utils/retryOnError.mjs";
14
10
  import { readBedrockStreamEvents } from "./platform/bedrock.mjs";
@@ -112,6 +108,11 @@ export async function callOpenAICompatibleModel(
112
108
 
113
109
  // bedrock + sso profile
114
110
  const runFetchForBedrock = async () => {
111
+ const { Sha256 } = await import("@aws-crypto/sha256-js");
112
+ const { fromIni } = await import("@aws-sdk/credential-providers");
113
+ const { HttpRequest } = await import("@smithy/protocol-http");
114
+ const { SignatureV4 } = await import("@smithy/signature-v4");
115
+
115
116
  const region =
116
117
  url.match(/bedrock-runtime\.([\w-]+)\.amazonaws\.com/)?.[1] ?? "";
117
118
  const urlParsed = new URL(url);
@@ -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 */