Skip to content

Commit 1f0b36a

Browse files
committed
feat!: Support Claude 4.6 generation models
1 parent 11a3349 commit 1f0b36a

23 files changed

+925
-492
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ src/ # Source code
1515
├── ai/ # AI model integration and prompts
1616
│ ├── model/ # Bedrock client and rate limiting
1717
│ ├── prompts/ # Prompt templates for various operations
18+
│ ├── tools/ # Tool definitions for model interactions
1819
│ └── mcp/ # Model Context Protocol server
1920
├── check/ # Content validation (lint, links, images)
2021
├── content/ # Content processing

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,17 @@ Toolkit for Markdown uses AWS Bedrock for AI processing. Ensure the following is
398398
- AWS credentials configured
399399
- Access to Bedrock models in the appropriate AWS account
400400

401+
**Supported Models:**
402+
403+
The tool requires Anthropic Claude models via Amazon Bedrock. The following model families are supported:
404+
405+
- Claude Opus 4 (including 4.1, 4.5, 4.6)
406+
- Claude Sonnet 4 (including 4.5, 4.6)
407+
- Claude Haiku 4.5
408+
- Claude 3.7 Sonnet
409+
410+
Model IDs are matched using a prefix, so both standard and cross-region inference profiles (e.g. `us.anthropic.claude-sonnet-4-5-20250929-v1:0`) are accepted. Other model providers and older Claude versions (such as Claude 3.5 Haiku) are not supported.
411+
401412
## Commands
402413

403414
### `review`

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"author": "",
3434
"dependencies": {
3535
"@aws-sdk/client-bedrock-runtime": "^3.985.0",
36-
"@modelcontextprotocol/sdk": "^1.18.2",
36+
"@modelcontextprotocol/sdk": "^1.27.1",
3737
"chalk": "^5.3.0",
3838
"commander": "^14.0.0",
3939
"diff": "^7.0.0",
@@ -50,7 +50,7 @@
5050
"unified": "^11.0.5",
5151
"unist-util-visit": "^5.1.0",
5252
"yaml": "^2.8.0",
53-
"zod": "^3.25.75"
53+
"zod": "^4.3.6"
5454
},
5555
"devDependencies": {
5656
"@biomejs/biome": "^2.0.6",

src/ai/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616

1717
export * from "./model/index.js";
1818
export * from "./prompts/index.js";
19+
export * from "./tools/index.js";

src/ai/model/client.ts

Lines changed: 118 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ import {
2222
CountTokensCommand,
2323
type ImageFormat,
2424
type Message,
25+
type ToolConfiguration,
2526
} from "@aws-sdk/client-bedrock-runtime";
27+
import { z } from "zod";
2628
import type { Prompt } from "../prompts/index.js";
29+
import type { ToolDefinition } from "../tools/types.js";
2730
import {
2831
NoopRateLimiter,
2932
type RateLimiter,
@@ -140,76 +143,102 @@ export class DefaultBedrockClient implements BedrockClient {
140143
*/
141144
async generate(
142145
prompt: Prompt,
146+
tools: ToolDefinition[] = [],
143147
cacheEnabled: boolean = false,
144148
): Promise<BedrockClientGenerateResponse> {
145149
const tokenUsageCounter = new TokenUsageCounter();
146150

147151
const promptText = prompt.prompt;
148152
const promptContext = prompt.context;
149-
let prefill = prompt.prefill ?? "";
150153

151154
let iterations = 0;
152155

153-
while (true) {
154-
// TODO: Emit event about iteration counter
156+
const toolConfig: ToolConfiguration | undefined =
157+
tools.length > 0
158+
? {
159+
tools: tools.map((tool) => ({
160+
toolSpec: {
161+
name: tool.name,
162+
description: tool.description,
163+
inputSchema: {
164+
json: z.toJSONSchema(tool.parameters) as Record<
165+
string,
166+
unknown
167+
>,
168+
},
169+
},
170+
})) as ToolConfiguration["tools"],
171+
}
172+
: undefined;
173+
174+
const toolMap = new Map(tools.map((t) => [t.name, t]));
175+
176+
const messages: Message[] = [
177+
{
178+
role: "user" as ConversationRole,
179+
content: [
180+
...(promptContext ? [{ text: promptContext }] : []),
181+
...(cacheEnabled
182+
? [
183+
{
184+
cachePoint: {
185+
type: "default" as CachePointType,
186+
},
187+
},
188+
]
189+
: []),
190+
{
191+
text: promptText,
192+
},
193+
...(prompt.images || []).map((img) => ({
194+
image: {
195+
format: img.format as ImageFormat,
196+
source: { bytes: img.bytes },
197+
},
198+
})),
199+
],
200+
},
201+
];
155202

203+
while (true) {
156204
if (iterations > this.maxIterations) {
157205
throw new Error("Maximum iterations breached");
158206
}
159207

160-
const conversation = [
161-
{
162-
role: "user" as ConversationRole,
163-
content: [
164-
...(promptContext ? [{ text: promptContext }] : []),
165-
166-
...(cacheEnabled && this.isCachingSupported(this.modelId)
167-
? [
168-
{
169-
cachePoint: {
170-
type: "default" as CachePointType,
171-
},
172-
},
173-
]
174-
: []),
175-
{
176-
text: promptText,
177-
},
178-
...(prompt.images || []).map((img) => ({
179-
image: {
180-
format: img.format as ImageFormat,
181-
source: { bytes: img.bytes },
182-
},
183-
})),
184-
],
185-
},
186-
...(prefill
187-
? [
188-
{
189-
role: "assistant" as ConversationRole,
190-
content: [{ text: prefill }],
191-
},
192-
]
193-
: []),
194-
];
195-
196208
const estimatedTokens = await this.estimateTokens(
197-
conversation,
209+
messages,
198210
prompt.sampleOutput,
199211
);
200-
/*const estimatedTokens = estimateTokens(
201-
promptContext ?? "",
202-
promptText,
203-
prefill,
204-
prompt.sampleOutput ?? "",
205-
);*/
206212

207213
tokenUsageCounter.addEstimated(estimatedTokens);
208214

209215
const command = new ConverseCommand({
210216
modelId: this.modelId,
211-
messages: conversation,
217+
messages,
218+
toolConfig,
219+
additionalModelRequestFields: {
220+
reasoning_config: {
221+
type: "enabled",
222+
budget_tokens: 2048,
223+
},
224+
},
212225
inferenceConfig: { maxTokens: this.maxTokens },
226+
...(prompt.outputSchema && {
227+
outputConfig: {
228+
textFormat: {
229+
type: "json_schema",
230+
structure: {
231+
jsonSchema: {
232+
schema: JSON.stringify(
233+
z.toJSONSchema(prompt.outputSchema.schema),
234+
),
235+
name: prompt.outputSchema.name,
236+
description: prompt.outputSchema.description,
237+
},
238+
},
239+
},
240+
},
241+
}),
213242
});
214243

215244
await Promise.all([
@@ -221,9 +250,6 @@ export class DefaultBedrockClient implements BedrockClient {
221250

222251
const responseObject = await this.client.send(command);
223252

224-
// biome-ignore lint/style/noNonNullAssertion: Need to see if this needs better checks
225-
let response = prefill + responseObject.output!.message!.content![0].text;
226-
227253
this.tokenRateLimiter.consume(
228254
responseObject.usage?.totalTokens || 0,
229255
timestamp,
@@ -233,13 +259,52 @@ export class DefaultBedrockClient implements BedrockClient {
233259
tokenUsageCounter.addUsage(responseObject.usage);
234260
}
235261

236-
if (responseObject.stopReason === "max_tokens") {
237-
prefill = response.trimEnd();
238-
iterations++;
262+
const responseContent = responseObject.output?.message?.content ?? [];
263+
264+
if (responseObject.stopReason === "tool_use") {
265+
messages.push({
266+
role: "assistant" as ConversationRole,
267+
content: responseContent,
268+
});
269+
270+
const toolResultContent = [];
271+
272+
for (const block of responseContent) {
273+
if (!block.toolUse) continue;
239274

275+
const toolUseId = block.toolUse.toolUseId;
276+
const toolName = block.toolUse.name ?? "";
277+
const input = (block.toolUse.input ?? {}) as Record<string, unknown>;
278+
const tool = toolMap.get(toolName);
279+
280+
const resultText = tool
281+
? tool.execute(input)
282+
: `Unknown tool: ${toolName}`;
283+
284+
toolResultContent.push({
285+
toolResult: {
286+
toolUseId,
287+
content: [{ text: resultText }],
288+
},
289+
});
290+
}
291+
292+
messages.push({
293+
role: "user" as ConversationRole,
294+
content: toolResultContent,
295+
});
296+
297+
iterations++;
240298
continue;
241299
}
242300

301+
const textBlock = responseContent.find((block) => block.text);
302+
let response = textBlock?.text ?? "";
303+
304+
if (responseObject.stopReason === "max_tokens") {
305+
throw new Error("Response exceeded maximum token limit");
306+
}
307+
243308
if (responseObject.stopReason !== "end_turn") {
244309
throw new Error(`Unexpected stop reason: ${responseObject.stopReason}`);
245310
}
@@ -286,43 +351,4 @@ export class DefaultBedrockClient implements BedrockClient {
286351

287352
return responseObject.inputTokens ?? 0;
288353
}
289-
290-
/**
291-
* Determines whether prompt caching is supported for the given model ID.
292-
*
293-
* Caching can improve performance and reduce costs by reusing previously
294-
* processed context across multiple requests. This feature is only available
295-
* for specific model versions.
296-
*
297-
* @param modelId - The AWS Bedrock model identifier to check
298-
* @returns True if the model supports prompt caching, false otherwise
299-
*
300-
* @private
301-
*/
302-
private isCachingSupported(modelId: string) {
303-
const validModels = [
304-
"anthropic.claude-opus-4-6",
305-
"anthropic.claude-opus-4-5",
306-
"anthropic.claude-opus-4-1",
307-
"anthropic.claude-opus-4",
308-
"anthropic.claude-sonnet-4-6",
309-
"anthropic.claude-sonnet-4-5",
310-
"anthropic.claude-haiku-4-5",
311-
"anthropic.claude-opus-4",
312-
"anthropic.claude-sonnet-4",
313-
"anthropic.claude-3-7-sonnet",
314-
"anthropic.claude-3-5-haiku",
315-
"amazon.nova-micro-v1:0",
316-
"amazon.nova-lite-v1:0",
317-
"amazon.nova-pro-v1:0",
318-
];
319-
320-
for (const validModel of validModels) {
321-
if (modelId.includes(validModel)) {
322-
return true;
323-
}
324-
}
325-
326-
return false;
327-
}
328354
}

src/ai/model/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import type { Prompt } from "../prompts/index.js";
18+
import type { ToolDefinition } from "../tools/types.js";
1819

1920
export interface TokenUsage {
2021
inputTokens: number;
@@ -33,6 +34,7 @@ export interface BedrockClientGenerateResponse {
3334
export interface BedrockClient {
3435
generate(
3536
prompt: Prompt,
37+
tools: ToolDefinition[],
3638
cacheEnabled: boolean,
3739
): Promise<BedrockClientGenerateResponse>;
3840
}

src/ai/prompts/reviewPrompt.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,7 @@ import { loadImage } from "../../content/utils/markdownUtils.js";
2222
import type { Language } from "../../languages/index.js";
2323
import { buildContextPrompt } from "./contextPrompt.js";
2424
import type { Exemplar, Prompt } from "./types.js";
25-
import {
26-
type ContextStrategy,
27-
extractFileSection,
28-
getContext,
29-
} from "./utils.js";
25+
import { type ContextStrategy, getContext } from "./utils.js";
3026

3127
const template = `Your task is to review the content provided for file "{{file}}" and update it to improve it in terms of style, grammar and syntax.
3228
@@ -56,9 +52,17 @@ For any finding which cannot be reliably remediated, such as missing images or b
5652
[Some screenshot](/pods1.png)
5753
</example_comment>
5854
59-
Write the output as markdown in a similar style to the example content. Respond with the resulting file enclosed in <file></file> including the path to the file as an attribute.
55+
Write the translated content in a similar style to the example content. Use the write_file tool to output the result to "{{currentNode.filePath}}" in chunks:
56+
- Each chunk is a separate call to write_file
57+
- You MUST NOT write more than ~3000 tokens per chunk (roughly 2000-2500 words)
58+
- Break at natural boundaries: section headers, major paragraphs
59+
- First call: mode="create"
60+
- Subsequent calls: mode="append"
61+
- Continue until complete
6062
61-
ONLY respond with the content between the "<file></file>" tags.`;
63+
Write substantial chunks to minimize tool calls while staying well under the output limit.
64+
65+
You're final response to the user MUST simply be "Success".`;
6266

6367
export async function buildReviewPrompt(
6468
tree: ContentTree,
@@ -95,16 +99,6 @@ export async function buildReviewPrompt(
9599
checkIssues: checkIssues && checkIssues.length > 0 ? checkIssues : null,
96100
}),
97101
sampleOutput: currentNode.content || undefined,
98-
prefill: `<file path="${currentNode.filePath}">`,
99-
transform: (input) => {
100-
const fileSection = extractFileSection(input);
101-
102-
if (fileSection.path !== currentNode.filePath) {
103-
throw new Error(`Unexpected file path in output: ${fileSection.path}`);
104-
}
105-
106-
return fileSection.content;
107-
},
108102
};
109103

110104
if (includeImages && currentNode.content) {

0 commit comments

Comments
 (0)