Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aimock",
"version": "1.19.2",
"version": "1.19.4",
"description": "Fixture authoring guidance for @copilotkit/aimock — LLM, multimedia, MCP, A2A, AG-UI, vector, and service mocking",
"author": {
"name": "CopilotKit"
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# @copilotkit/aimock

## [1.19.4] - 2026-05-08

### Fixed

- **Converse stream: spurious `type` field in contentBlockDelta and contentBlockStart** — `delta` objects contained a Claude Messages API `type` field (`text_delta`, `thinking_delta`) that is not a member of the Converse API's tagged union. botocore's single-member union parser rejected the extra field with `ResponseParserError`. Also fixed reasoning deltas to use `reasoningContent` (Converse format) instead of `thinking` (Claude format). (Issue #165, reported by @KMiya84377)
- **Converse: `inferenceConfig.maxTokens` silently dropped** — `converseToCompletionRequest` now forwards `maxTokens` to `max_tokens`
- **Converse: non-streaming responses missing `metrics`** — Added `metrics: { latencyMs: 0 }` to all 3 non-streaming converse response builders, matching the streaming path and AWS ConverseResponse spec

### Added

- **Bedrock drift test expansion** — invoke-with-response-stream drift test with binary frame parsing and Anthropic-native event shape comparison. Converse-stream SDK shapes for tool call (`toolUse` start/delta) and reasoning (`reasoningContent` start/delta) variants. Three-way triangulate comparisons for all variants.

## [1.19.3] - 2026-05-08

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion charts/aimock/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ name: aimock
description: Mock infrastructure for AI application testing (OpenAI, Anthropic, Gemini, MCP, A2A, vector)
type: application
version: 0.1.0
appVersion: "1.19.2"
appVersion: "1.19.4"
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@copilotkit/aimock",
"version": "1.19.3",
"version": "1.19.4",
"description": "Mock infrastructure for AI application testing — LLM APIs, image generation, text-to-speech, transcription, audio generation, video generation, MCP tools, A2A agents, AG-UI event streams, vector databases, search, rerank, and moderation. One package, one port, zero dependencies.",
"license": "MIT",
"keywords": [
Expand Down
66 changes: 59 additions & 7 deletions src/__tests__/bedrock-stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,7 @@ describe("POST /model/{modelId}/converse (non-streaming)", () => {
expect(body.output.message.content[0].text).toBe("Hi there!");
expect(body.stopReason).toBe("end_turn");
expect(body.usage).toEqual({ inputTokens: 0, outputTokens: 0, totalTokens: 0 });
expect(typeof body.metrics.latencyMs).toBe("number");
});

it("returns tool call response in Converse format", async () => {
Expand All @@ -719,6 +720,18 @@ describe("POST /model/{modelId}/converse (non-streaming)", () => {
expect(body.output.message.content[0].toolUse.input).toEqual({ city: "SF" });
expect(body.output.message.content[0].toolUse.toolUseId).toBeDefined();
expect(body.stopReason).toBe("tool_use");
expect(typeof body.metrics.latencyMs).toBe("number");
});

it("returns metrics field in content+toolCalls response", async () => {
instance = await createServer(allFixtures);
const res = await post(`${instance.url}/model/${MODEL_ID}/converse`, {
messages: [{ role: "user", content: [{ text: "search-and-explain" }] }],
});

expect(res.status).toBe(200);
const body = JSON.parse(res.body);
expect(typeof body.metrics.latencyMs).toBe("number");
});

it("returns 404 when no fixture matches", async () => {
Expand Down Expand Up @@ -771,8 +784,18 @@ describe("POST /model/{modelId}/converse-stream", () => {
expect(frames[0].payload).toEqual({ role: "assistant" });

expect(frames[1].eventType).toBe("contentBlockStart");
// Text contentBlockStart has an empty start object (no spurious `type` field)
const startPayloadText = frames[1].payload as { start: Record<string, unknown> };
expect(startPayloadText.start).not.toHaveProperty("type");
expect(Object.keys(startPayloadText.start)).toEqual([]);

const deltas = frames.filter((f) => f.eventType === "contentBlockDelta");
// Verify no spurious `type` field in any text delta
for (const d of deltas) {
const delta = (d.payload as { delta: Record<string, unknown> }).delta;
expect(delta).not.toHaveProperty("type");
expect(Object.keys(delta)).toEqual(["text"]);
}
const fullText = deltas
.map((f) => (f.payload as { delta: { text: string } }).delta.text)
.join("");
Expand Down Expand Up @@ -881,21 +904,32 @@ describe("POST /model/{modelId}/converse-stream (content + toolCalls)", () => {
expect(frames[0].eventType).toBe("messageStart");
expect(frames[0].payload).toEqual({ role: "assistant" });

// Text contentBlockStart
// Text contentBlockStart — start is an empty object (no spurious `type` field)
const textBlockStart = frames.find(
(f) =>
f.eventType === "contentBlockStart" &&
(f.payload as { start?: { type?: string } }).start?.type === "text",
!(f.payload as { start?: { toolUse?: unknown; reasoningContent?: unknown } }).start
?.toolUse &&
!(f.payload as { start?: { toolUse?: unknown; reasoningContent?: unknown } }).start
?.reasoningContent,
);
expect(textBlockStart).toBeDefined();
const textStart = (textBlockStart!.payload as { start: Record<string, unknown> }).start;
expect(textStart).not.toHaveProperty("type");
expect(Object.keys(textStart)).toEqual([]);

// Text deltas
// Text deltas — no spurious `type` field in delta
const textDeltas = frames.filter(
(f) =>
f.eventType === "contentBlockDelta" &&
(f.payload as { delta?: { text?: string } }).delta?.text !== undefined,
);
expect(textDeltas.length).toBeGreaterThanOrEqual(1);
for (const td of textDeltas) {
const delta = (td.payload as { delta: Record<string, unknown> }).delta;
expect(delta).not.toHaveProperty("type");
expect(Object.keys(delta)).toEqual(["text"]);
}
const fullText = textDeltas
.map((f) => (f.payload as { delta: { text: string } }).delta.text)
.join("");
Expand Down Expand Up @@ -1055,10 +1089,14 @@ describe("POST /model/{modelId}/converse-stream (contentWithToolCalls full struc
expect(blockStarts.length).toBe(2); // one text, one tool

// 3. Text content block appears before tool call block
// Text block start has an empty `start` object (no `type` field)
const textBlockStartIdx = frames.findIndex(
(f) =>
f.eventType === "contentBlockStart" &&
(f.payload as { start?: { type?: string } }).start?.type === "text",
!(f.payload as { start?: { toolUse?: unknown; reasoningContent?: unknown } }).start
?.toolUse &&
!(f.payload as { start?: { toolUse?: unknown; reasoningContent?: unknown } }).start
?.reasoningContent,
);
const toolBlockStartIdx = frames.findIndex(
(f) =>
Expand Down Expand Up @@ -1146,9 +1184,10 @@ describe("POST /model/{modelId}/converse-stream (contentWithToolCalls full struc
);
expect(indices).toEqual([0, 1, 2]);

// Text block at index 0
const textStart = blockStarts[0].payload as { start: { type: string } };
expect(textStart.start.type).toBe("text");
// Text block at index 0 — start is empty (no `type` field)
const textStartPayload = blockStarts[0].payload as { start: Record<string, unknown> };
expect(Object.keys(textStartPayload.start)).toEqual([]);
expect(textStartPayload.start).not.toHaveProperty("type");

// Tool blocks at indices 1 and 2
const tool1Start = blockStarts[1].payload as {
Expand Down Expand Up @@ -1337,6 +1376,18 @@ describe("converseToCompletionRequest", () => {
expect(result.temperature).toBe(0.7);
});

it("forwards inferenceConfig.maxTokens as max_tokens", () => {
const result = converseToCompletionRequest(
{
messages: [{ role: "user", content: [{ text: "hi" }] }],
inferenceConfig: { maxTokens: 100 },
},
"model-id",
);

expect(result.max_tokens).toBe(100);
});

it("sets model from modelId parameter", () => {
const result = converseToCompletionRequest(
{
Expand Down Expand Up @@ -1783,6 +1834,7 @@ describe("converseToCompletionRequest (edge cases)", () => {
"model",
);
expect(result.temperature).toBeUndefined();
expect(result.max_tokens).toBe(100);
});

it("handles assistant text blocks with missing text alongside toolUse (text ?? '')", () => {
Expand Down
Loading
Loading