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/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"source": {
"source": "npm",
"package": "@copilotkit/aimock",
"version": "^1.27.2"
"version": "^1.27.3"
},
"description": "Fixture authoring skill for @copilotkit/aimock — LLM, multimedia (image/TTS/transcription/video), MCP, A2A, AG-UI, vector, embeddings, structured output, sequential responses, streaming physics, record/replay, agent loop patterns, and debugging"
}
Expand Down
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.27.2",
"version": "1.27.3",
"description": "Fixture authoring guidance for @copilotkit/aimock — LLM, multimedia, MCP, A2A, AG-UI, vector, and service mocking",
"author": {
"name": "CopilotKit"
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## [Unreleased]

## [1.27.3] - 2026-05-27

### Fixed

- correct OpenAI images endpoint path from `/v1/images/edit` to `/v1/images/edits` (closes [#221](https://github.com/CopilotKit/aimock/issues/221))
- add Ollama `/api/embed` route as alias for `/api/embeddings`; `/api/embed` is the current documented endpoint ([ollama/ollama docs/api.md](https://github.com/ollama/ollama/blob/main/docs/api.md)) used by modern Ollama SDKs, while `/api/embeddings` is retained for backwards-compatibility. Both routes dispatch to the same handler.

## [1.27.2] - 2026-05-26

### Fixed
Expand Down
8 changes: 4 additions & 4 deletions DRIFT.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ When a `critical` drift is detected:
- Google Gemini → `src/gemini.ts` (`buildGeminiTextResponse`, `buildGeminiToolCallResponse`, `buildGeminiTextStreamChunks`, `buildGeminiToolCallStreamChunks`)
- Gemini embedContent → `src/gemini.ts` (embedContent response builder)
- Gemini Interactions → `src/gemini-interactions.ts` (`buildInteractionsTextResponse`, `buildInteractionsToolCallResponse`, `buildInteractionsTextSSEEvents`, `buildInteractionsToolCallSSEEvents`)
- OpenAI Image Edit → `src/images.ts` (multipart `/v1/images/edit` handler)
- OpenAI Image Edit → `src/images.ts` (multipart `/v1/images/edits` handler)
- OpenAI Audio Translation → `src/transcription.ts` (multipart `/v1/audio/translations` handler)
- Ollama Embeddings → `src/ollama.ts` (`/api/embeddings` response builder)
- Ollama Embeddings → `src/ollama.ts` (`/api/embed` + legacy `/api/embeddings` response builder)
- Cohere Embed → `src/cohere.ts` (`/v2/embed` response builder)
- ElevenLabs TTS → `src/elevenlabs-audio.ts` (`/v1/text-to-speech/{voice_id}` response builder)

Expand Down Expand Up @@ -120,9 +120,9 @@ In addition to the 23 existing drift tests (20 HTTP response-shape + 3 model dep
| Endpoint | Provider | Type | Status |
| ---------------------------------------- | ------------- | ----------------- | ------- |
| POST /v1beta/models/{model}:embedContent | Gemini | HTTP | Covered |
| POST /v1/images/edit | OpenAI | HTTP (multipart) | Covered |
| POST /v1/images/edits | OpenAI | HTTP (multipart) | Covered |
| POST /v1/audio/translations | OpenAI | HTTP (multipart) | Covered |
| POST /api/embeddings | Ollama | HTTP | Covered |
| POST /api/embed, /api/embeddings | Ollama | HTTP | Covered |
| POST /v2/embed | Cohere | HTTP | Covered |
| POST /v1/text-to-speech/{voice_id} | ElevenLabs | HTTP | Covered |
| stream_options.include_usage | OpenAI | Streaming feature | Covered |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Run them all on one port with `npx @copilotkit/aimock --config aimock.json`, or
- **Timing-aware recording and replay** — Recorded fixtures capture per-frame arrival timestamps; replay uses recorded timings for approximate timing reproduction based on recorded TTFT and inter-frame cadence (replay chunk count may differ from recording — TTFT and average pace are preserved, not per-token fidelity) with configurable `--replay-speed` multiplier
- **[Multi-turn Conversations](https://aimock.copilotkit.dev/multi-turn)** — Record and replay multi-turn traces with tool rounds; match distinct turns via `turnIndex`, `hasToolResult`, `toolCallId`, `sequenceIndex`, `systemMessage` (gate on host-supplied agent context), or custom predicates
- **[14 LLM Providers](https://aimock.copilotkit.dev/docs)** — OpenAI Chat, OpenAI Responses, OpenAI Realtime (GA + Beta shim), Claude, Gemini (REST + embedContent), Gemini Live, Gemini Interactions, Azure, Bedrock, Vertex AI, Ollama (chat + embeddings), Cohere (chat + embed), ElevenLabs TTS — full streaming support
- **Multimedia APIs** — [image generation](https://aimock.copilotkit.dev/images) (DALL-E, Imagen), [image editing](https://aimock.copilotkit.dev/images) (/v1/images/edit), [text-to-speech](https://aimock.copilotkit.dev/speech) (OpenAI + ElevenLabs), [audio transcription](https://aimock.copilotkit.dev/transcription), [audio translation](https://aimock.copilotkit.dev/transcription) (/v1/audio/translations), [video generation](https://aimock.copilotkit.dev/video), [fal.ai](https://aimock.copilotkit.dev/fal-ai) (image / video / audio with queue lifecycle)
- **Multimedia APIs** — [image generation](https://aimock.copilotkit.dev/images) (DALL-E, Imagen), [image editing](https://aimock.copilotkit.dev/images) (/v1/images/edits), [text-to-speech](https://aimock.copilotkit.dev/speech) (OpenAI + ElevenLabs), [audio transcription](https://aimock.copilotkit.dev/transcription), [audio translation](https://aimock.copilotkit.dev/transcription) (/v1/audio/translations), [video generation](https://aimock.copilotkit.dev/video), [fal.ai](https://aimock.copilotkit.dev/fal-ai) (image / video / audio with queue lifecycle)
- **[MCP](https://aimock.copilotkit.dev/mcp-mock) / [A2A](https://aimock.copilotkit.dev/a2a-mock) / [AG-UI](https://aimock.copilotkit.dev/agui-mock) / [Vector](https://aimock.copilotkit.dev/vector-mock)** — Mock every protocol your AI agents use
- **[Chaos Testing](https://aimock.copilotkit.dev/chaos-testing)** — 500 errors, malformed JSON, mid-stream disconnects at any probability
- **Per-Request Strict Mode** — `X-AIMock-Strict` header overrides the server-level `--strict` flag per request (`true`/`1` = strict, `false`/`0` = lenient)
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.27.2"
appVersion: "1.27.3"
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.27.2",
"version": "1.27.3",
"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
2 changes: 1 addition & 1 deletion scripts/update-competitive-matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const FEATURE_RULES: FeatureRule[] = [
},
{
rowLabel: "Image editing",
keywords: ["/v1/images/edit", "image edit", "image editing", "inpainting", "edit.*image"],
keywords: ["/v1/images/edits", "image edit", "image editing", "inpainting", "edit.*image"],
},
{
rowLabel: "Text-to-Speech",
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/competitive-matrix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const FEATURE_RULES: FeatureRule[] = [
},
{
rowLabel: "Image editing",
keywords: ["/v1/images/edit", "image edit", "image editing", "inpainting", "edit.*image"],
keywords: ["/v1/images/edits", "image edit", "image editing", "inpainting", "edit.*image"],
},
{
rowLabel: "Non-speech audio",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe("image edit endpoint", () => {
formData.append("n", "1");
formData.append("size", "1024x1024");

const res = await fetch(`${mock.url}/v1/images/edit`, {
const res = await fetch(`${mock.url}/v1/images/edits`, {
method: "POST",
headers: { Authorization: "Bearer test" },
body: formData,
Expand All @@ -47,7 +47,7 @@ describe("image edit endpoint", () => {
formData.append("image", new Blob(["fake"]), "image.png");
formData.append("model", "dall-e-2");

const res = await fetch(`${mock.url}/v1/images/edit`, {
const res = await fetch(`${mock.url}/v1/images/edits`, {
method: "POST",
headers: { Authorization: "Bearer test" },
body: formData,
Expand All @@ -72,7 +72,7 @@ describe("image edit endpoint", () => {
formData.append("prompt", "remove background");
formData.append("model", "dall-e-2");

const res = await fetch(`${mock.url}/v1/images/edit`, {
const res = await fetch(`${mock.url}/v1/images/edits`, {
method: "POST",
headers: { Authorization: "Bearer test" },
body: formData,
Expand All @@ -95,7 +95,7 @@ describe("image edit endpoint", () => {
formData.append("image", new Blob(["fake"]), "image.png");
formData.append("prompt", "enhance");

const res = await fetch(`${mock.url}/v1/images/edit`, {
const res = await fetch(`${mock.url}/v1/images/edits`, {
method: "POST",
headers: { Authorization: "Bearer test" },
body: formData,
Expand Down Expand Up @@ -124,7 +124,7 @@ describe("image edit endpoint", () => {
formData.append("image", new Blob(["fake"]), "image.png");
formData.append("prompt", "test prompt");

const editRes = await fetch(`${mock.url}/v1/images/edit`, {
const editRes = await fetch(`${mock.url}/v1/images/edits`, {
method: "POST",
headers: { Authorization: "Bearer test" },
body: formData,
Expand All @@ -146,7 +146,7 @@ describe("image edit endpoint", () => {
formData.append("image", new Blob(["fake"]), "image.png");
formData.append("prompt", "no match");

const res = await fetch(`${mock.url}/v1/images/edit`, {
const res = await fetch(`${mock.url}/v1/images/edits`, {
method: "POST",
headers: { Authorization: "Bearer test" },
body: formData,
Expand All @@ -156,6 +156,40 @@ describe("image edit endpoint", () => {
const data = await res.json();
expect(data.error.code).toBe("no_fixture_match");
});

test("route path matches OpenAI: /v1/images/edits responds, /v1/images/edit is 404 (#221)", async () => {
mock = new LLMock({ port: 0 });
mock.addFixture({
match: { userMessage: "path check", endpoint: "image" },
response: { image: { url: "https://example.com/ok.png" } },
});
await mock.start();

const makeForm = () => {
const fd = new FormData();
fd.append("image", new Blob(["fake"]), "image.png");
fd.append("prompt", "path check");
return fd;
};

// Correct OpenAI path (plural) must succeed
const okRes = await fetch(`${mock.url}/v1/images/edits`, {
method: "POST",
headers: { Authorization: "Bearer test" },
body: makeForm(),
});
expect(okRes.status).toBe(200);

// Legacy singular path must NOT be registered
const badRes = await fetch(`${mock.url}/v1/images/edit`, {
method: "POST",
headers: { Authorization: "Bearer test" },
body: makeForm(),
});
expect(badRes.status).toBe(404);
const badData = await badRes.json();
expect(badData.error?.type).toBe("not_found");
});
});

describe("image variations endpoint", () => {
Expand Down
18 changes: 18 additions & 0 deletions src/__tests__/ollama.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2138,3 +2138,21 @@ describe("POST /api/embeddings — Ollama Embeddings API", () => {
expect(entry.response.status).toBe(200);
});
});

// ─── POST /api/embed (current Ollama endpoint alias) ───────────────────────

describe("POST /api/embed — Ollama Embeddings API (current endpoint alias)", () => {
it("returns a valid embedding response identical to /api/embeddings", async () => {
instance = await createServer([]);
const res = await post(`${instance.url}/api/embed`, {
model: "nomic-embed-text",
prompt: "hello world",
});

expect(res.status).toBe(200);
const body = JSON.parse(res.body);
expect(body.model).toBe("nomic-embed-text");
expect(Array.isArray(body.embedding)).toBe(true);
expect(body.embedding.length).toBe(1536);
});
});
6 changes: 3 additions & 3 deletions src/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ function serializeOpenAIImageResponse(
}

/**
* Handle POST /v1/images/edit — OpenAI Image Edit API.
* Handle POST /v1/images/edits — OpenAI Image Edit API.
*
* Request uses multipart/form-data. We extract text fields (`prompt`, `model`,
* `n`, `size`, `response_format`) and ignore binary fields (`image`, `mask`)
Expand All @@ -319,7 +319,7 @@ export async function handleImageEdit(
setCorsHeaders: (res: http.ServerResponse) => void,
): Promise<void> {
setCorsHeaders(res);
const path = req.url ?? "/v1/images/edit";
const path = req.url ?? "/v1/images/edits";
const method = req.method ?? "POST";

const contentType = Array.isArray(req.headers["content-type"])
Expand Down Expand Up @@ -412,7 +412,7 @@ export async function handleImageEdit(
res,
syntheticReq,
"openai",
req.url ?? "/v1/images/edit",
req.url ?? "/v1/images/edits",
fixtures,
defaults,
raw,
Expand Down
22 changes: 15 additions & 7 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const SEARCH_PATH = "/search";
const RERANK_PATH = "/v2/rerank";
const MODERATIONS_PATH = "/v1/moderations";
const IMAGES_PATH = "/v1/images/generations";
const IMAGES_EDIT_PATH = "/v1/images/edit";
const IMAGES_EDIT_PATH = "/v1/images/edits";
const IMAGES_VARIATIONS_PATH = "/v1/images/variations";
const SPEECH_PATH = "/v1/audio/speech";
const TRANSCRIPTIONS_PATH = "/v1/audio/transcriptions";
Expand Down Expand Up @@ -123,7 +123,7 @@ const COMPAT_SUFFIXES = [
"/audio/transcriptions",
"/audio/translations",
"/images/generations",
"/images/edit",
"/images/edits",
"/images/variations",
];

Expand Down Expand Up @@ -166,6 +166,7 @@ const VERTEX_AI_RE =
const OLLAMA_CHAT_PATH = "/api/chat";
const OLLAMA_GENERATE_PATH = "/api/generate";
const OLLAMA_EMBEDDINGS_PATH = "/api/embeddings";
const OLLAMA_EMBED_PATH = "/api/embed";
const OLLAMA_TAGS_PATH = "/api/tags";

const HEALTH_PATH = "/health";
Expand Down Expand Up @@ -1118,9 +1119,13 @@ export async function createServer(
}

// Ollama /api/* routes must be dispatched BEFORE normalizeCompatPath, which
// rewrites any path ending in /embeddings to /v1/embeddings. The /api/chat
// and /api/generate paths are unaffected (their suffixes aren't in
// COMPAT_SUFFIXES), but /api/embeddings would collide with the OpenAI handler.
// rewrites any path ending in /embeddings to /v1/embeddings. The /api/chat,
// /api/generate, and /api/embed paths are unaffected (their suffixes aren't
// in COMPAT_SUFFIXES), but /api/embeddings would collide with the OpenAI
// handler. /api/embed is the current Ollama endpoint
// (https://github.com/ollama/ollama/blob/main/docs/api.md); /api/embeddings
// is the legacy path kept for backwards-compatibility. Both route to the
// same handler.
if (pathname === OLLAMA_CHAT_PATH && req.method === "POST") {
try {
const raw = await readBody(req);
Expand Down Expand Up @@ -1159,7 +1164,10 @@ export async function createServer(
return;
}

if (pathname === OLLAMA_EMBEDDINGS_PATH && req.method === "POST") {
if (
(pathname === OLLAMA_EMBEDDINGS_PATH || pathname === OLLAMA_EMBED_PATH) &&
req.method === "POST"
) {
try {
const raw = await readBody(req);
await handleOllamaEmbeddings(req, res, raw, fixtures, journal, defaults, setCorsHeaders);
Expand Down Expand Up @@ -1478,7 +1486,7 @@ export async function createServer(
return;
}

// POST /v1/images/edit — OpenAI Image Edit API (multipart/form-data)
// POST /v1/images/edits — OpenAI Image Edit API (multipart/form-data)
if (pathname === IMAGES_EDIT_PATH && req.method === "POST") {
try {
const raw = await readBody(req);
Expand Down
Loading