Skip to content

feat: namespaced function calls and custom tool calls on the Responses API - #511

Merged
jpr5 merged 10 commits into
mainfrom
feat/505-responses-namespace-custom-tools
Oct 9, 2026
Merged

jpr5 merged 10 commits into
mainfrom
feat/505-responses-namespace-custom-tools

Conversation

@jpr5

@jpr5 jpr5 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Refs #505

Summary

Codex CLI talks to models over the OpenAI Responses API. It routes MCP and sub-agent tools by (namespace, name), and its apply_patch tool is a freeform custom tool. aimock could not script either shape, so a Codex session against aimock failed on its first tool call. This is PR 1 of 2. Recording is PR 2.

  • Emission. A fixture tool call can carry a namespace, emitted as function_call.namespace. A toolCalls entry with type: "custom" (or a customToolCall block) emits a custom_tool_call item with response.custom_tool_call_input.delta / .done events. Both work over HTTP SSE, non-streaming and WebSocket. Fixtures that use neither field produce byte-identical output.
  • Matching. Function and custom tools inside namespace tools, custom tools, and tools delivered in additional_tools and tool_search_output input items are now visible to toolName and predicates. A new match.toolNamespace key selects by exact namespace, alone or paired with toolName. custom_tool_call and custom_tool_call_output history items count as tool rounds.
  • Validation and errors. File loading validates the new shapes up front. At request time, every other wire rejects a custom call before writing any content, with a 500 aimock_unsupported_tool_call in that wire's own error envelope (an error frame on Realtime and Gemini Live). A malformed fixture tool call gets aimock_invalid_fixture_tool_call the same way, from toolCalls or blocks, static or factory. Fixtures that loaded and served before keep working.
  • Misbehavior (feat: add deterministic model misbehavior faults #508). Custom tool calls are never fault targets and pass through faulted output unchanged. A fixture the wire cannot serve is never faulted, so it gets the same coded error and journal entry as without a misbehavior config. Schema faults and tool-unknown-name resolve a Responses call by namespace and name.
  • Docs and CHANGELOG. Responses API page (shapes, event order, matching, a Codex recipe), fixtures reference, WebSocket and multi-turn pages, the write-fixtures skill, and CHANGELOG entries, including the TypeScript type changes. The README now lists the feature and links to the Responses API docs section.

Spec: https://www.notion.so/3f43aa381852819c9dc0db15a4b40523

Design notes and decisions

  • Codex's MCP namespace is mcp__<server> with no trailing __. The docs follow real Codex 0.161.0 output.
  • An empty namespace name gets a 400, as in the OpenAI spec (NamespaceToolParam.name has minLength: 1).
  • toolNamespace matches any request tool that carries a namespace. Only the Responses API sets one from namespace tools.
  • On non-Responses wires a namespace is dropped and a custom call is rejected.
  • Legacy toolCalls load as on main, with warnings (legacy type: "toolCall", numeric id, non-string name, input on a function call). The new fields are strict. When non-empty blocks are served, legacy toolCalls are ignored.
  • Three shapes that loaded in 1.44.0 are now load errors: an empty namespace, type: "custom" with arguments, and type: "customToolCall" in toolCalls.
  • ToolCallResponse.toolCalls becomes FixtureToolCall[] (a breaking type change, noted in the CHANGELOG). AudioResponse.toolCalls stays ToolCall[].
  • Custom input deltas are split on code points, so no lone surrogates.
  • A malformed custom_tool_call_output becomes empty text (200, as on main).
  • Invalid-arguments envelopes on Ollama, Gemini and Bedrock gain an aimock code. Existing fields are unchanged.
  • Misbehavior: custom calls are never fault targets or the tool-call-id-duplicate destination. Output-clearing faults with tool=<custom name> are not applicable. stop-length-mid-tool always cuts a function call: custom calls before it complete, custom calls after it are dropped, and a custom-only fixture gets 501 not-applicable.

Wire-format sources: the OpenAI OpenAPI spec, openai-node responses.d.ts (7.30/7.31), Codex rust-v0.161.0 (protocol/src/models.rs, codex-api/src/sse/responses.rs), and captured Codex 0.161.0 requests. A trimmed responses-lite request is checked in as src/__tests__/fixtures/codex-responses-lite-request.json.

Commits

  1. 9c0cf717 test: pin Responses tool-call wire output. A golden of the exact HTTP, SSE and WS bytes for tool-only, content plus tools, and ordered blocks, captured before any change.
  2. 4a41f44f feat: emit namespaced function calls and custom tool calls. Fixture types, one emission path for all Responses paths, custom input deltas, coded errors on every other wire, and load-time validation.
  3. 319573b7 feat: match namespace, custom and additional_tools tools. Request tool flattening, the toolNamespace key (router, journal, validation, aimock-pytest), history conversion, and 400s for malformed tool collections on HTTP and WS.
  4. 047db367 fix(misbehavior): keep faults off custom tool calls and unservable fixtures. Also the namespace-aware schema lookup.
  5. 16423c12 test: drift legs for namespaced and custom calls, streaming and non-streaming.
  6. af56cad0 docs: Responses API, fixtures, WebSocket and multi-turn pages, and the write-fixtures skill.
  7. efd33ae3 docs(changelog): the Feature Request: namespaced function calls and custom tool calls in Responses API fixtures #505 entries, including the type changes that can break code compiled against 1.44.0.
  8. 5e36eb2c fix(misbehavior): judge tool-unknown-name within the call's own namespace.
  9. 3acf0955 docs(readme): mention namespaced and custom Responses tool calls. One README feature bullet that links to the Responses API docs section.

Verification

Local gates

Final run on the pushed head (9 commits): prettier, eslint, typecheck, build and test:exports pass; pnpm test 13318 passed, 3 skipped (run after build, as CI does); aimock-pytest 237 passed; commitlint passes on all 9 commits.

The 9th commit changes only the README, so the test, build and typecheck results above still apply. For that commit, the prettier check, commitlint and the CLI test that reads the README passed.

Not run: the live drift legs (no OPENAI_API_KEY locally). They skip with a stated reason when the model does not produce the call.

Codex CLI 0.161.0 end to end

Run on the final tree with no misbehavior faults. Model gpt-5.5, isolated CODEX_HOME, approval_policy = "never", aimock with --validate-on-load, one aimock process per scenario. Turn 2 fixtures match on toolCallId. The MCP server is a small stdio server, so the namespace is mcp__stdiodocs.

# Scenario Transport Codex exit Observed
1 custom_tool_call apply_patch HTTP 0 hello.txt written with the fixture's content. 2 requests; turn 2 carried the custom call output and matched. Final message "done: patch applied".
2 custom_tool_call apply_patch WebSocket 0 Same file and content. WS turn 1 and turn 2 both 200. No HTTPS fallback.
3 namespaced function_call mcp__stdiodocs/lookup_doc HTTP 0 The MCP server received tools/call lookup_doc with {"id":"505"}. The output came back and matched turn 2. Final message "done: MCP round complete".
4 namespaced function_call mcp__stdiodocs/lookup_doc WebSocket 0 Same MCP call received. WS turn 1 and turn 2 both 200. No HTTPS fallback.

On main before this PR (checked before the rebase), --validate-on-load rejects the custom fixture, and Codex fails the MCP call with unsupported call: lookup_doc.

Over WS, Codex's empty warm-up response.create gets a 404 (same on main), and each run spent about 15 s before turn 1.

Red/green evidence

Core feature, on a real server, main vs this branch (measured before the rebase onto #508):

  • Custom call fixture on Responses: 500 "arguments" must be a string → 200 with output_item.added, input deltas, .done, output_item.done.
  • toolName on a tool offered only inside a namespace: 404 → 200, namespace on every item, over HTTP, SSE and WS.
  • toolNamespace + toolName pair with the wrong namespace registered first: 404 → 200 from the right fixture.
  • History custom_tool_call plus output with hasToolResult: true: 404 → 200.
  • The new test files on main: 111 failed, 21 passed (the passes are the byte-identity golden and unchanged-behaviour guards).

Review found the defects below in this branch. Each one was reproduced on a real server before the fix and checked again after it, with a test that fails before and passes after.

  • Custom input deltas split surrogate pairs: 4/5 → 5/5 chunking cases clean.
  • Factory toolCalls with non-object entries: uncoded 500 on Responses, 200 nameless call on Chat → coded aimock_invalid_fixture_tool_call, same as static.
  • Array toolCalls entry: Chat 200 function: {}, Responses uncoded 500 → coded 500 on every wire. Array block in the loader: wrong message → "must be an object".
  • Mistyped custom type with no string arguments on Responses: uncoded 500 → coded error on HTTP and WS.
  • Duplicate-fixture key joined with |: false shadow warning → no warning; the true duplicate still warns.
  • Non-chat custom-call load warning: text false for 16 of 18 endpoints → text true for all 18.
  • Load warning read ignored legacy toolCalls when blocks were served → warns only on what is served.
  • FunctionFixtureBlock not exported: TS2305 in a consumer → compiles.
  • Ollama /api/generate misreported unknown blocks as tool calls (400) → same 500 as /api/chat.
  • /api/generate block errors journaled with a null body → body and fixture journaled; response bytes unchanged.
  • /api/generate with a tool call plus an unknown block: 400 → the same 500 as /api/chat, in both orders.
  • Misbehavior planner threw on an unservable block fixture: Realtime and Gemini Live unjournaled, Gemini Interactions null-body entry → skipped and journaled like a run with no config.
  • Invalid JSON arguments on wires that parse them: 54 off/on mismatches (faults served 200 or journaled applied: true) → 0.
  • tool-call-id-duplicate could land on a custom call: 22 violations → 0.
  • Output-clearing faults with tool=<custom name>: 16/16 cleared the custom call → 0/16; reported as not applicable.
  • Schema faults on a namespaced call used another namespace's schema (served valid arguments) → the call's own schema (arguments violate it).
  • tool-unknown-name checked the replacement name across all namespaces: wrong 501 and a skipped _v2 name → 200, renamed within the call's namespace.
  • Docs: claims about tools validation, check order, object arguments and /api/generate were probed against a real server and corrected to match.

Out of scope / follow-ups

Review raised more than this PR fixes. What is left falls into three groups: behaviour that is the same on main, work for another subject or PR 2, and minor polish in this feature area. None of it blocks Codex use. The full list is below.

A. Same behaviour on main
  • WebSocket Responses does not check the shape of input. A null item or object input throws a TypeError, sent as server_error and not journaled.
  • An array function_call_output is not matched by toolResultContains.
  • An additional_tools developer item becomes an empty system message.
  • A null entry in toolCalls makes normalizeResponse throw a TypeError at file load, not a validation error.
  • A text, unknown-type or non-object entry in blocks throws a plain Error: uncoded 500 and no journal error. Since this PR only tool-call blocks get the coded error.
  • On Gemini Live and Realtime the same malformed (non-tool-call) block errors are journaled as 200.
  • The Gemini Live history rollback does not cover malformed-block errors, so an unanswered user turn stays in history; it also leaves the fixture match counter advanced.
  • A comment says resolveFixtureBlocks requires string arguments, but it stringifies objects.
  • Function-call arguments of null, a number or a boolean pass load validation, because JSON.parse accepts them.
  • A function call with no arguments fails load as "not valid JSON: undefined", while the runtime treats it as {}. The same message is given for a mistyped type, without naming it.
  • Invalid function toolCalls arguments on the Responses API return an uncoded 500 with journal error null.
  • Gemini Interactions and Anthropic streaming send malformed tool arguments as they are, while non-streaming rejects them.
  • Gemini Interactions streams when the request omits stream. A request rejected for invalid tool arguments still uses up an interaction id.
  • Streamed Responses events carry no sequence_number.
  • Text, function-argument and reasoning deltas can split a UTF-16 surrogate pair. This PR fixed it only for custom input deltas.
  • An item_reference with no type (or type: null) is not recognised, so the next tool output merges into the previous turn.
  • Ollama /api/generate rejects a text-only blocks fixture with 400; for {content, blocks:[text]} it returns content while /api/chat returns the block text; a valid tool fixture adds a misbehavior summary {applied:false}.
  • Realtime: a programmatic fixture with object arguments streams no argument deltas; a function call with no arguments crashes mid-stream and is journaled as 200.
  • Object toolCalls arguments make the tool-args-invalid-json fault crash with an uncoded 500.
  • In the duplicate-fixture key an absent field and "" collide (this PR corrected only the comment). describeMatch reports the wrong location.
  • Errors from factory clean-up are labelled "factory threw".
  • FixtureFileBlock is not exported from the package entry point.
  • Skill and docs: the Reasoning row of the provider matrix disagrees with the fixtures doc; the endpoint list omits openai-live; the "HTTP SSE Text Response" example does not send stream: true.
  • Tests: a leftover console.log, weak assertions and console noise in older tests, and a stale comment in the Responses drift file.
B. Another subject or PR 2
  • aimock-pytest pins server 1.44.0 by default, which ignores toolNamespace. Bump it at the next aimock release.
  • WebSocket Responses keeps no previous_response_id state. A follow-up turn that sends only new items cannot match on earlier content, and an outputs-only input makes placeholder tool calls with an empty name in the journal. Same on main with plain function tools. Storing responses by id fixes both.
  • Codex's empty warm-up response.create over WebSocket gets a 404 (on main).
  • Record mode drops custom tool calls and namespace, so a recorded Codex session replays as "unsupported call" (PR 2).
  • Recording a non-streaming Responses body loses the output array (PR 2).
  • Codex sub-agent tasks arrive as agent_message input items, which aimock does not convert.
  • The openai dev dependency (4.104.0) has no types for custom tool calls or namespace, and its ResponseStream ignores output_item.done and response.incomplete. Bump it.
C. Minor follow-ups in this feature area

Behaviour and validation:

  • Any /api/v3/ request is labelled "BytePlus ModelArk" in error text, even when BytePlus is not configured.
  • A non-array Gemini audio toolCalls is dropped with no error; make it a coded error.
  • Inside a namespace tool, non-object and other-type entries are dropped silently, and tools with no string name are accepted.
  • A falsy top-level tools counts as absent, but input[i].tools: null gets 400.
  • In history items, a non-string namespace is dropped and a non-string custom input becomes "" with no warning.
  • A namespace tool with no description or an empty tools array is accepted; the OpenAI spec rejects both.
  • A stray input is a warning on a toolCalls function entry but an error on a toolCall block.
  • The toolNamespace load warning says only Responses requests carry namespaces, but a Chat Completions tool with a namespace also matches.
  • UnsupportedToolCallError does not give the toolCalls[i] index and can name an empty tool.
  • A factory entry with a mistyped type and object arguments is served as a function call, while the same static fixture gets a coded 500. A factory that returns a non-object is spread with { ...raw }.
  • A fixture tool-call error on /api/generate uses the OpenAI envelope while /api/chat uses Ollama's; a custom call gets 400 on generate but 500 on chat.
  • aimock emits status on custom_tool_call items; openai-node's type has no such field. Verify against the real API.
  • Bedrock streaming routes use the non-streaming wire label in error text.
  • FixtureToolCallError, ResponsesInputItem and ResponsesToolDef are not exported.

Types and names:

  • FixtureFileToolCall is one interface with optional fields, not a union.
  • ResponsesToolDef has a catch-all member that stops narrowing and forces casts.
  • Normalized fixture types can still hold null or numeric names and object arguments; the resolveFixtureBlocks overload types are loose.
  • invalidToolArgumentsEnvelope now serves three error types, so the name is misleading.
  • Realtime hasToolCalls reads the raw toolCalls, not the resolved calls, and has a redundant alias.
  • isPlainObject in src/helpers.ts is another private object guard; the repo already has several (isObject in jsonrpc.ts and misbehavior.ts, isPlainObject in mcp-fakes.ts). Consolidating them is a separate cleanup.

Misbehavior:

  • When a fixture cannot be served and misbehavior is skipped, no summary or log is written, even when a header asked for a fault.
  • servesFixture treats any error as "cannot serve", which can hide a helper bug.
  • tool= cannot tell apart two tools with the same name in different namespaces, and the journal target omits the namespace.
  • Blocks rebuilt for a fault drop extra fields.
  • validateFixtureMisbehavior accepts a fixture fault that can never fire on wires that reject invalid-JSON object arguments.
  • The schema lookup for a non-namespaced call on a non-Responses wire, whose Chat request tools carry a namespace, is not namespace-aware.
  • The docs do not say that stop-length-mid-tool drops custom calls after the cut.

Drift tests:

  • isModelNotFound treats a 400 saying "does not exist" as a retired model, so a real shape rejection can show as skipped.
  • The non-streaming Responses leg calls JSON.parse with no guard; the model-listing fetch is not tagged as infra; response.in_progress is dropped from the SDK expectations.

Unit tests:

  • The Gemini audio legacy-compat test never checks that the audio part is present.
  • A fixture-error-parity predicate looks for "PATCH" but the input is {a:1}, so it never matches.
  • The custom-input chunking test does not create the mock with logLevel: "silent".
  • The other-wires test has no rows for Azure streaming, the compatibility-prefix paths, or the BytePlus route.
  • The byte-identity test's 3 s WebSocket deadline can flake and misses frames after response.completed.
  • The emission truncation test pins exactly one delta based on same-tick timing.
  • The invalid-object-arguments misbehavior test table has gaps; streaming pass-through is not tested.
  • Namespaced schema-fault tests cover only missing-required; no wrong-type, extra-property, enum, WS or tool_search_output case.
  • Mutant M07 (key order) survives; the fixture-loader test only checks narrowing.

Base and related PRs

This branch is rebased onto main after #508 (model misbehavior), and it integrates with the misbehavior planner as described above. #509 is still open. If it lands first, this branch will be rebased onto it.

jpr5 added 8 commits October 9, 2026 09:23
…m calls

Record the exact HTTP, SSE and WebSocket bytes the Responses API emits
today for function-call fixtures (tool-only, content plus tool calls, and
ordered blocks). Namespaced and custom tool calls must not change the
output of fixtures that use neither, and this golden makes any drift fail.
…ponses API

Codex and other Responses clients route tool calls by namespace and use
freeform custom tools, and fixtures had no way to produce either.

A fixture tool call may now carry a namespace, and a toolCalls entry with
type "custom" (or a customToolCall block) is a custom tool call with
free-text input. The Responses API emits them as function_call items with
a namespace and as custom_tool_call items over HTTP, SSE and WebSocket.
Custom input deltas are chunked without splitting surrogate pairs, and
fixtures without the new fields keep their exact wire output.

Every other wire rejects a custom tool call before writing any content,
with a coded aimock_unsupported_tool_call 500 in that wire's own error
envelope. A malformed fixture tool call gets aimock_invalid_fixture_tool_call
the same way, whether it is a toolCalls entry or a block, static or from a
factory, and file loading validates the new shapes up front. Fixtures that
loaded and served before keep working.

Refs #505
… requests

Codex sends its tools as namespace tools, custom (freeform) tools, and
tools delivered later in the input (additional_tools items and
tool_search_output results). aimock dropped all of these, so toolName
could not match them and no fixture could select by namespace.

Responses requests now flatten function and custom tools, including those
inside namespace tools and tool-carrying input items, into the normalized
tool list with their namespace. A new toolNamespace match key selects a
fixture by the exact namespace, alone or paired with toolName on the same
tool. custom_tool_call and custom_tool_call_output history items count as
turns and feed text matchers. Malformed tool collections and namespace
tools without a name are rejected with a 400 on HTTP and WebSocket alike.
…xtures

Misbehavior faults now treat custom tool calls as pass-through content, so
the Responses API keeps them and their namespaces unchanged in a faulted
response:

- a custom call is never a fault target or the tool-call-id-duplicate
  destination, and tool=<custom call> selects no output-clearing fault;
- a fault's schema lookup resolves a Responses call by namespace and name;
- a fixture the wire cannot serve (a custom call off Responses, a
  malformed tool call or block, or invalid JSON arguments on a wire that
  sends arguments as an object) is never faulted, and the planner never
  throws on it. The request gets the same coded error and journal entry
  it gets without a misbehavior config.
Compare aimock's namespaced function_call and custom_tool_call output
against the real OpenAI Responses API, streaming and non-streaming, so
drift in either item shape or its events is caught. The legs skip with a
stated reason when the live model does not produce the call, and fail on
any real mismatch in the terminal items.
Describe the namespace field, custom tool calls and customToolCall blocks
in fixtures, the toolNamespace match key, how Responses requests flatten
namespace, custom and additional_tools tools, the coded errors other wires
return for custom or malformed tool calls, and how misbehavior faults
treat them. Update the WebSocket and multi-turn pages and the
write-fixtures skill to match.
Add the #505 entries: the new fixture fields, the toolNamespace match key,
the coded fixture tool-call errors and where each wire carries them, the
new exports, the misbehavior interaction, and the TypeScript type changes
that can break code compiled against 1.44.0.
…pace

On openai-responses the renamed call keeps its namespace, so a replacement
name counts as declared only when a tool with that name is offered in the
target call's own namespace (top-level tools for an un-namespaced call).
This is the same (namespace, name) rule the schema lookup uses.

Before, an explicit name offered only in another namespace was refused with
501 aimock_misbehavior_not_applicable, and the default _v2 naming skipped
names that were free in the call's namespace. Other wires are unchanged.
@pkg-pr-new

pkg-pr-new Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@copilotkit/aimock@511

commit: b7eaf3f

jpr5 added 2 commits October 9, 2026 10:25
Add a feature bullet linking to the Responses API docs section for
namespaced function calls, custom tool calls and toolNamespace matching.
# Conflicts:
#	CHANGELOG.md
@jpr5
jpr5 merged commit 2a16646 into main Oct 9, 2026
33 checks passed
@jpr5
jpr5 deleted the feat/505-responses-namespace-custom-tools branch October 9, 2026 21:30
jpr5 added a commit that referenced this pull request Oct 9, 2026
…dings (#513)

Refs #505

Stacked on #511 (base `feat/505-responses-namespace-custom-tools`).
Merge this after #511, and release the two together.

#509 has merged to main. #511 absorbed main with a merge commit, and
this branch absorbed #511 (with #509) with a merge commit. Neither
branch was rebased.

## Summary

- Record mode now keeps `namespace` on Responses `function_call` items.
Before, the recorder dropped it, so a replayed MCP call came back
without its namespace and Codex could not route it.
- Record mode now keeps `custom_tool_call` items, with their full
`input`. Before, the recorder dropped them, so a turn with only a custom
call (for example `apply_patch`) was saved as `{"content":""}`.
- Interleaved turns record custom calls as `customToolCall` blocks, so
the order of text, function calls and custom calls survives the
recording.
- With this, a Codex session recorded through aimock replays correctly:
`apply_patch` writes its file and the MCP tool is called.

## What changed

**`src/stream-collapse.ts`**
- Function calls and custom calls share one accumulator, keyed by
`output_index`. The ordered-block list gains a custom-call kind.
- `namespace` comes from `response.output_item.added`, or from
`response.output_item.done` if `added` did not have it. An empty string
is ignored.
- `response.custom_tool_call_input.delta` appends to the input.
`response.custom_tool_call_input.done` and `output_item.done` supply the
input only when no delta arrived.
- These handlers run before the generic `response.*` handler.
- At the end, a custom call becomes `{ type: "custom", name, input, id?,
namespace? }` with no `arguments`, and its block becomes
`customToolCall`.

**`src/recorder.ts`**
- `sanitizeRecordedToolCall` sanitizes each kind separately. A function
call gets `arguments ?? "{}"`. A custom call gets `input ?? ""` and
never gets `arguments`.

**Type change (TypeScript only):** `CollapseResult.toolCalls` widens
from `ToolCall[]` to `FixtureToolCall[]`, because `collapseOpenAISSE`
can now return a custom call. Code that reads `.arguments` from a
collapser result needs to narrow on `type` first. The existing collapse
tests read `.arguments` in many places. They now go through a small
helper, `fnArgs` in `src/__tests__/helpers/tool-calls.ts`, which fails
the test if it gets a custom call. That edit is mechanical, and the test
logic does not change.

**Tests:** a new file,
`src/__tests__/responses-record-namespace-custom.test.ts`, has 8
collapse unit tests and one HTTP test that goes record, then disk, then
validate, then replay.

**Docs and CHANGELOG**
- `docs/record-replay/index.html` has a new section on recording
Responses namespaced and custom calls.
- `CHANGELOG.md`, under the existing #505 Unreleased entry: one "Added"
line for the recording behavior, and one line in the breaking type
changes for `CollapseResult.toolCalls`.

## Verification

**Record run, before and after.** Recorder A is a real aimock in record
mode. Its upstream is a second aimock that serves the #511 Responses
stream. A third instance then replays only A's fixtures.
- Before (at #511's head): the custom-only turn was saved as
`{"content":""}`. The namespaced call was saved without `namespace`.
Mixed and interleaved turns lost the custom call. Replay differed from
upstream in 4 of 4 cases.
- After: every fixture keeps `namespace` and the full custom `input`, in
`toolCalls` and in `blocks`. Replay matches upstream in 4 of 4 cases.

**Record, then replay, with Codex CLI 0.161.0.** Codex records a session
through aimock, and a second aimock replays only the recorded fixtures.

| Case | Before | After |
| --- | --- | --- |
| Custom `apply_patch` | `--validate-on-load` rejects the fixture (empty
content). With a plain load, Codex prints an empty message and
`hello.txt` is not created. | Validation passes. On replay,
`apply_patch` writes `hello.txt` and Codex ends on "done: patch
applied". Exit 0. |
| Namespaced MCP tool | Codex logs `unsupported call: lookup_doc` and
the MCP tool is not called. | The fixture keeps `namespace:
"mcp__stdiodocs"`. On replay, the call reaches the MCP server, which
handles it, and Codex ends on "done: MCP round complete". Exit 0. |

In both cases the replay ends on the recorded final-text turn, so Codex
does not loop.

**Local gates:** format, lint, typecheck, build, `test:exports` and
commitlint pass. aimock-pytest: 248 passed. The full vitest suite ran 3
times, and all tests passed each time: 13601 passed, 3 skipped, 0
failed. Each run still exited 1 on one vitest-internal error, `Timeout
calling "onTaskUpdate"`, which no test is attributed to. It comes from
machine load, and the base branch (#511 at b7eaf3f) shows the same
error under the same load.

## Commits

- 3e1b94c feat(record): keep Responses namespace and custom tool calls
in recordings
- 65a7e4d docs(record-replay): document recording of Responses
namespaced and custom calls
- f87b039 docs(record): describe custom tool call blocks in
stream-collapse comments
- 8644e38 chore: merge #505 branch (with #509) into recording branch

## Out of scope and follow-ups

- **Malformed upstream streams in the collapser.** Three cases can drop
data without a warning: a function-call delta and a custom-call delta on
the same `output_index`; a custom-call delta with no `output_item.added`
before it (recorded with an empty name, which the loader then rejects);
and custom-call events with no numeric `output_index`. A well-formed
stream does not trigger any of them.
- **Tests.** The review found two gaps in the new test coverage, and the
new test file leaks a resource in its cleanup. Both go in a follow-up.
- **Non-streaming Responses recording.** A non-streaming Responses call
is still saved as an "unknown format" error fixture. That already
happens on main and is not changed here. WebSocket Responses has no
record path either.
jpr5 added a commit that referenced this pull request Oct 10, 2026
…ecording (#517)

## Why

#509 (MCP fakes recording) changed the published TypeScript surface of
`@copilotkit/aimock@1.44.0`. aimock ships no breaking changes, type-only
ones included, so this PR restores each break additively. The items use
the labels from the 1.44.0 compatibility audit:

- **S1.** The `declare module "./fixture-loader.js"` augmentation in
`src/mcp-fakes.ts` was emitted by tsdown as a literal `sideEffect();`
statement in `dist/mcp-fakes.d.ts` and `.d.cts`. Every consumer that
type-checks with `skipLibCheck: false` failed on every entry point
(TS1036, TS2304). The augmentation is gone. A malformed recorded key now
fails with the 1.44.0 rule `mcp-fakes/bad-block:h`, and the message
names its `record-replay/fa-*` rule.
- **B8.** `McpFakeAddOrigin.kind` had gained `"record"`, which breaks a
custom `Mountable.addMcpFakes` that switches exhaustively on it. The
public type is back to `"file" | "code" | "control-api"`. The recorder
uses an internal origin type and a private `MCPMock` method.
- **B9.** `AimockHandle` in `/vitest` and `/jest` had gained required
`fakesFor()` and `fakesReport()`, which breaks code that constructs or
stubs a handle. `AimockHandle` is restored exactly. `useAimock()` now
returns a new `AimockFakesHandle` that extends it, and both types are
exported from `/vitest` and `/jest`.
- **B11.** `Mountable` had gained `setReplaySpeed?(speed: number):
void`, so a 1.44.0 mount with its own `setReplaySpeed` member of another
type no longer assigned. `Mountable` is back to its exact 1.44.0 shape.
- **R12.** The runtime half of B11: a custom mount with a
`setReplaySpeed` member was called at start, and a non-function member
threw a `TypeError` at `start()`. The server and `LLMock` mount loops
now give the replay speed only to aimock's own `MCPMock`.
- **R13.** A bad recorded key reported `record-replay/fa-*` as the error
rule instead of the 1.44.0 `mcp-fakes/bad-block:h`, and a bad recorded
key together with another error reported a different first rule. The
recorded keys are now checked where 1.44.0 rejected them as unknown
keys, so the first rule matches 1.44.0.

A new gate, `src/__tests__/package-types-compat.test.ts`, runs `npm
pack` on the build, unpacks it into a temporary consumer, and
type-checks every `exports` entry point (ESM and CJS) under `strict` and
`skipLibCheck: false`. The consumer holds the 1.44.0 usage patterns for
B8, B9 and B11.

## The hardening fixes

These fix defects in the MCP recording feature that the review of the
Mastra-parity work found. All of it is new since 1.44.0.

- **Start-time validation and credential redaction.** `llm.record.mcp`
and the record destination are checked in a pre-pass at start, before
anything is mounted. The start fails with an error that names the mount
and the destination for: `null`, `false`, `0` or `""` as
`llm.record.mcp`; mount keys the server can never route (a trailing `/`,
`//`, a query, a fragment, whitespace, a dot segment, an escaped
character), on both `llm.record.mcp` and `--mcp-record` /
`--mcp-proxy-only`; an upstream that is not an http(s) URL; an empty or
non-string `fixturePath`; a broken symbolic link, `EACCES` or a
non-writable directory as the destination; and `maxRecordBufferBytes`
that is not greater than 0 and at most 256 MiB. Credentials in mount
keys are redacted in these errors with the record sanitizer's URL rule.
- **Upstream death mid-body fails loudly on every relay path.** Before,
a truncated upstream answer was relayed as a clean-looking 200, a
`SyntaxError` in the client, or a hang until the client timed out, with
nothing logged. Now the buffered, SSE, pass-through and post-overflow
paths all fail: a 502 before headers, a terminated stream after them,
and a JSON-RPC error event first for an SSE call that never got its
response. Each failure logs one `MCP-RECORD:` line and journals
`recordSkipped: "upstream-aborted"`. A client that disconnects is not
reported as an upstream failure.
- **Config warnings.** `aimock --config` defaulted to the silent log
level, which hid the `MCP-RECORD:` warnings, so a run that recorded
nothing printed nothing. A config whose `llm.record.mcp` has at least
one mount now defaults `logLevel` to `"warn"`. An explicit
`llm.logLevel` still wins, and an empty `llm.record.mcp` stays silent.

## Red/green evidence

Each fix was reproduced on the real surface (the built CLIs, a packed
tarball, real MCP clients `@modelcontextprotocol/sdk` 1.31.0 and
`@modelcontextprotocol/client` 2.2.0) before the change and re-run after
it.

- **S1, B8, B9.** RED: the compat gate's consumer against the base build
failed with `mcp-fakes.d.ts(13,1): error TS1036: Statements are not
allowed in ambient contexts` and `TS2304: Cannot find name 'sideEffect'`
(both `.d.ts` and `.d.cts`), `TS2345: Argument of type '"record"' is not
assignable to parameter of type 'never'`, and `TS2739: ... missing the
following properties from type 'AimockHandle': fakesFor, fakesReport`.
GREEN: the gate passes 2/2. In the packed `dist/`, `sideEffect` and
`declare module` match 0 of 86 `.d.ts` and 86 `.d.cts` files.
- **B11.** RED: a strict consumer with a `Mountable` whose
`setReplaySpeed` is a string fails with `TS2322 ... Types of property
'setReplaySpeed' are incompatible`; npm 1.44.0 compiles it. GREEN: it
compiles, and the `interface Mountable` block in `dist/types.d.ts` and
`.d.cts` is byte-identical to npm 1.44.0.
- **R12.** RED: `LLMock({ replaySpeed: 2 })` with such a mount threw
`TypeError: handler.setReplaySpeed is not a function` at `start()`,
before and after a late mount. GREEN: it starts and the member is never
called, the same as 1.44.0.
- **R13.** RED: a malformed recorded key gave
`"rule":"record-replay/fa-list"`; a bad recorded key with `scope: 5`
gave `bad-block:b` where 1.44.0 gives `bad-block:h`. GREEN:
`"rule":"mcp-fakes/bad-block:h"` with `[record-replay/fa-list]` in the
message. A probe of 722 inputs that 1.44.0 rejects, each loaded both
through `addMcpFakes` and from a file, went from 238 rule mismatches to
0. A separate class of 66 rows, with a recorded key that is valid now,
is listed under the follow-ups below.
- **Start-time validation.** RED: `llm.record.mcp` of `null`, `false`,
`0` and `""`, the mount keys `/mcp/`, `/mcp?x=1`, `/a b` and `//`, an
empty `fixturePath`, a dangling symlink destination and
`maxRecordBufferBytes` of 0, -5 and 314572800 all started (exit 0). A
credentialed URL as a mount key printed
`http://user:tok3nSECRET@host/mcp` in the error. GREEN: each exits 1
with a message that names the mount; the credentialed key prints
`http://host/mcp (credentials removed)`, and the full stderr has 0
secret matches. Valid configs still start.
- **Upstream death mid-body.** RED (killable upstream): SSE record path,
both clients `TIMEOUT 4000ms`; pass-through chunked JSON, a raw 200 with
a truncated body and `SyntaxError: Unterminated string in JSON`;
pass-through content-length, a hang; buffered JSON, `Unexpected content
type: null`; nothing logged. GREEN: SSE gives `McpError -32603 ... the
upstream response failed` at once; the pass-through and post-overflow
paths give `TypeError terminated` at once; buffered JSON gives a 502
with `{"error":"aimock MCP recorder: the upstream response failed"}`.
Each logs `MCP-RECORD: the upstream response failed mid-body
(upstream-aborted) ...` and journals `upstream-aborted`. A client abort
produced no aimock output.
- **Config warnings.** RED: `aimock --config` with an MCP record mount
and a request with no testId printed only the listening line. GREEN: it
prints `MCP-RECORD: forwarded, not recorded: a request on mount /mcp has
no testId or context ...`. `mcp: {}` stays silent, like a config with no
`mcp`.
- The happy path still works: `examples/mcp-record-proof/proof.sh` exits
0, recording server-everything (13 tools, 0 secrets) and replaying it
offline at speed 1 and 100, identical to the recording and to the live
run.

## Not breaking

Every runtime change touches only features added since 1.44.0: MCP
recording (`llm.record.mcp`, `--mcp-record`, `--mcp-proxy-only`), the
recorded `mcpFakes` keys, and the replay-speed hook. The type changes
return the published surface to 1.44.0 and add only new names
(`AimockFakesHandle`).

## Coordination

Another session is fixing the #501, #508, #511 and #513 breaks
separately and will reuse this compat gate. The gate's evidence consumer
still shows their errors (`c-others.ts`, `c508.ts`) and none from #509.
Release PR #516 is held until both land.

## Known follow-ups

- 66 probe rows remain where a recorded key that is now valid, together
with an unrelated error, gives a different first rule than 1.44.0 (for
example `timing` plus `scope: 5` gives `bad-block:b`, not
`bad-block:h`). Both versions reject the input; only the rule code
differs. This waits on decision D4.
- Programmatic `wireMcpRecording` can leave mounts half-wired if a later
step fails.
- `@modelcontextprotocol/client` 2.2.0 maps the 502 to
`CLIENT_HTTP_NOT_IMPLEMENTED` (client side).
- The pass-through SSE and SSE-after-overflow paths terminate the stream
but do not inject a JSON-RPC error event.
- Some test-strength mutants beyond what the new tests cover, and a few
pre-existing log-wording nits.
- `AimockRecordConfig` requires `providers`, but the runtime and docs
accept an `llm.record` with only `mcp`.
- The aimock-cli docs say the `"warn"` default applies when
`llm.record.mcp` is set; the code applies it only when it has at least
one mount.

## Test plan

At the head of this branch, each exit code captured on its own:
`prettier --check .`, `eslint .`, `pnpm typecheck` (three tsconfigs),
`pnpm build`, `package-types-compat.test.ts`, and the recorder,
record-CLI, aimock-cli and MCP fakes test files. The full suite's only
failures are the pre-existing vitest worker `Timeout calling
"onTaskUpdate"` error (it reproduces on the base) and a `validate-cli`
timeout under load that passes alone (96/96); neither file is touched
here.
jpr5 added a commit that referenced this pull request Oct 10, 2026
…strict-args behavior opt-in (#518)

## Summary

With default options, this branch has zero breaking changes compared
with the published `@copilotkit/aimock` 1.44.0. The rule is:

1. An input that 1.44.0 accepted behaves identically.
2. An input that 1.44.0 rejected may now be accepted.
3. New behavior is opt-in.

The changes from #501, #508, #511 and #513 that altered 1.44.0 behavior
now sit behind explicit opt-ins. The 1.44.0 types (`ToolCall`,
`FixtureToolCall`, block unions, `CollapseResult`,
`resolveFixtureBlocks`, WebSocket handler arity,
`FixtureLoadRuleRegistry`) are restored. The new carriers
(`customToolCalls`, `responsesBlocks`, `MisbehaviorConfigError`,
`StartFromConfigOverrides`) are additive.

## Opt-ins

| Feature | Programmatic | CLI flag (`llmock` / `aimock`) | `aimock
--config` key |
|---|---|---|---|
| Extended Responses tool handling (#511/#513): custom tool calls,
namespaces. The fixture keys `match.toolNamespace`, `customToolCalls`
and `responsesBlocks` are honored **only** under `"extended"`; in
`"legacy"` they are ignored, as in 1.44.0 | `responsesTools: "legacy" \|
"extended"` (default `"legacy"`; an invalid value warns and runs
`"legacy"`); aimock-pytest: `AIMockServer(responses_tools="extended")` |
`--responses-tools legacy\|extended`; `aimock validate --responses-tools
legacy\|extended` | `llm.responsesTools` (an invalid value warns and
runs `"legacy"`) |
| Strict invalid tool arguments (#501): 500
`aimock_invalid_tool_arguments` on object wires, authored text on string
wires | `strictToolArguments: true` | `--strict-tool-arguments` |
`llm.strictToolArguments: true` |
| Model misbehavior (#508) | `enableMisbehavior: true` (also passed
through the plugins' `useAimock`) | `--misbehavior` |
`llm.enableMisbehavior: true` (`llm.misbehavior` is read only when
misbehavior is enabled) |
| MCP recording from config | n/a (`MCPMock.enableRecording()` is
unchanged) | n/a (`llmock --mcp-record` / `--mcp-proxy-only` are
unchanged) | `llm.enableMcpRecording: true` (`llm.record.mcp` is read
only with this opt-in; without it the key is ignored with one warning,
as in 1.44.0) |

On the `aimock` bin, a flag overrides the matching config key. If a
boolean flag is absent, the config key applies. Without its opt-in, each
setting keeps the 1.44.0 behavior. For example, invalid fixture tool
arguments are served as `{}` with a warning, and a `misbehavior` key in
a fixture or config is ignored (the config case logs one warning).

## Proof

All comparisons use the packed build of this branch against the npm
1.44.0 tarball.

| Proof | Result |
|---|---|
| Strict consumer matrix: TS 5.0.4 / 5.4.5 / 5.9.3 × bundler /
node16-esm / node16-cjs × 7 entry points (root, mcp, a2a, vector, agui,
vitest, jest), `skipLibCheck: false` | **126/126 cells pass** (exit 0, 0
errors) for both 1.44.0 and this branch |
| Type proofs b01–b12, s01, b07, b10-ws-handler-arity: TS 5.0.4 / 5.4.5
/ 5.9.3 × nodenext / node16-esm / node16-cjs / bundler / node10, strict
| 0 src errors in every cell, **except b12** (see Known open item).
node10 has the same 4 TS1192 lib errors as 1.44.0 |
| `src/__tests__/package-types-compat.test.ts` (strict gate) | 2/2 pass
|
| Runtime A/B, default options: http-ab (all wires × fixtures, stream
and non-stream) | 704 same / 0 diff |
| ws-ab / record-ab / record-ns-ab2 | 28/0, 60/0, 6/0 |
| #501 ab501, 1.44.0 vs default (wire bytes, journal, warn count) |
260/0 |
| #501 ab501, pre-fix main vs `strictToolArguments: true` | 260/0 on
bytes and journal (with strict, Ollama, Interactions and Live also log
the 1.44.0 warning) |
| #508 enabled-ab, main vs `enableMisbehavior: true` | 39/0 |
| Codex CLI 0.161.0 against the packed `dist/cli.js` with
`--responses-tools extended` | **4/4**: custom `apply_patch` over HTTP
and over WS, namespaced MCP over HTTP and over WS. Contrast without the
flag: 404 "No fixture matched", as in 1.44.0 |

There are 3 documented deltas from 1.44.0 with default options. All 3
are allowed by the rule:

1. **`GET /__aimock/misbehavior`**: 1.44.0 returns 404, this branch
returns 200. This is a new additive route (rule 2).
2. **`aimock --config` with a valid or invalid `llm.misbehavior` and no
opt-in**: the exit code (0) and the served responses are identical. The
only difference is one extra stderr warning: `Ignoring llm.misbehavior
because misbehavior is not enabled. ...`
3. **Fixture with invalid JSON tool arguments loaded through `validate`
/ `addFixturesFromJSON`**: 1.44.0 already rejected this input, and this
branch rejects it the same way. The message now ends with a pointer to
`misbehavior: tool-args-invalid-json`.

### Red / green

Every RED was observed on the real failure surface (packed builds driven
over HTTP/WS, the real CLI, and `tsc` consumer projects) before the fix.
Every GREEN was observed afterwards on the same probe.

| Track | RED (before the fix) | GREEN (after the fix) |
|---|---|---|
| #501 invalid tool arguments (ab501: 10 fixtures, 22 HTTP routes in
stream and non-stream modes, 3 WS wires) | 1.44.0 vs pre-fix main: **119
diffs**, 7 on each of the 17 affected wire/mode cells. Controls
identical | 1.44.0 vs fix (default): **260/260 identical** on bytes,
journal and warn count. Audit #501 cells: 30 → 0 (HTTP), 1 → 0 (WS) |
| #508 misbehavior types and WS arity (B7, B10-WS consumer matrix, 15
cells) | Consumer projects fail to compile against main (registry and
4-parameter WS handler breaks) | Same as 1.44.0 in every cell: 0 errors,
except the 4 node10 TS1192 lib errors that 1.44.0 also has |
| #508 `aimock --config` with `llm.misbehavior: "empty-response"`
(fixture `weather` → `sunny`) | 1.44.0 `content="sunny"`, branch before
the fix `content=""` | `content="sunny"` plus the warning. With
`llm.enableMisbehavior: true` or `--misbehavior`: `content=""`. Invalid
value with the opt-in: exit 1 with a rule-prefixed message |
| #511/#513 types (b01–b06, b10) | **14 src errors per cell, 210 in
total** across 15 cells (TS2339 `arguments`, `"function" \| "custom"`,
`CollapseResult.toolCalls`, `resolveFixtureBlocks` arity, ...) | **0 src
errors** in all 15 cells |
| #511/#513 runtime (namespace / custom fixtures F08–F13, F27–F31,
Q01–Q23, loader-ab, record-ns) | Up to 500 on 21/21 routes per fixture,
200 → 400 on Q01–Q06, loader rejects inputs that 1.44.0 accepted, 3/6
recordings differ | Identical to 1.44.0 by default. With
`responsesTools: "extended"`, records `namespace` and `customToolCalls`
|

## Known open item: B12

A consumer object that already has its own **non-fresh** `misbehavior`
key of a different type (for example `misbehavior: string` on a value
assigned to `Fixture`, or `misbehavior: { enabled: true }` on a value
assigned to `MockServerOptions`) no longer assigns to those types: 2 ×
TS2322 per type-proof cell. Adding any optional key has this effect, so
it cannot be avoided without renaming the key. It is the only type proof
that does not compile, and `main` fails it too. This needs a maintainer
decision: accept it, or rename the `misbehavior` key.

## Quality gates (head of this branch)

- `prettier --check .`, `eslint .` and `tsc --noEmit` (`tsconfig.json`,
`tsconfig.test.json`, `scripts/tsconfig.json`) all pass. The `tsdown`
build passes.
- The 65 touched test files, including the strict gate: 3695 passed, 2
skipped (env-gated).
- commitlint over the branch: 0 problems, 0 warnings. All commits are
signed.

## Review fixes (F1, F2, F3, C1, C2, validation)

- **F1**: `match.toolNamespace` is honored only with `responsesTools:
"extended"`. In the default legacy mode it is ignored, as in 1.44.0.
- **F2**: `customToolCalls` and `responsesBlocks` are served only with
`"extended"`. In legacy mode they are dropped on every wire (HTTP, SSE,
Responses WS, Realtime, Gemini Live), as in 1.44.0.
- **F3**: an invalid `responsesTools` (option, constructor or
`llm.responsesTools`) logs one warning and runs `"legacy"` instead of
failing startup. An invalid `--responses-tools` value still exits 1,
because the flag is new.
- **C1**: tests for the `llmock --misbehavior` and `--responses-tools`
pass-through.
- **C2**: a non-boolean `llm.strictToolArguments` or
`llm.enableMisbehavior` logs one warning and the option stays off.
- **Validation**: the validating doors (`validateFixtures`,
`addFixturesFromJSON()`, `POST /__aimock/fixtures`,
`--validate-on-load`, `aimock validate`) check the three keys only in
`"extended"`. New opt-in: **`aimock validate --responses-tools
extended`** (default `legacy` = 1.44.0 parity; an invalid value is a
usage error).
- **aimock-pytest**: new opt-in `AIMockServer(responses_tools=None)`,
which passes `--responses-tools <value>` when set. Unset keeps the
1.44.0 default. The plugin's `toolNamespace` test opts in to
`"extended"`.

| Repro | RED (PR head before the fixes) | GREEN (608a484) |
|---|---|---|
| F1/F2 runtime, default vs 1.44.0 (32 cases + 4 supplementary) | 3/32
identical, 29 differ | **32/32** and **4/4** identical |
| F1/F2 runtime, extended vs PR head extended | n/a | **32/32**
identical (new behavior kept) |
| Validating doors, default vs 1.44.0 (12 inputs x 6 doors) | 20/72
identical, 52 differ | **72/72** identical |
| Validating doors, extended vs PR head | n/a | **48/48** identical |
| F3/C2: `aimock -c` with invalid `responsesTools` / non-boolean flags,
constructor with `responsesTools: "bogus"` | exit 1 / `TypeError`; no
warning for non-boolean flags | starts, one `[aimock] Ignoring ...`
warning each, legacy output identical to 1.44.0 |
| Python plugin suite (CI commands, `AIMOCK_CLI_PATH=dist/cli.js`) | 1
failed (`test_add_fixture_routes_tool_namespace_into_match`: "No fixture
matched"), 247 passed | **248 passed**. Removing the opt-in makes the
test fail again |

Gates at 608a484: full vitest (12 shards) 14,012 passed / 3 skipped;
`package-types-compat.test.ts` 2/2; typecheck, eslint, prettier and
build pass; commitlint clean; all commits signed.

Refs #505 #501 #508 #509 #511 #513


## CI fix: Python Tests (fe5db7c)

All 16 `test (3.10..3.13, 20..26)` jobs failed with `188 failed, 60
passed`. All 188 were in the new
`test_misbehavior_{openai,anthropic,controls}.py` proofs. The pytest
plugin started the CLI without
`--misbehavior`, so faults were ignored, for example
`'{"city":"Paris","units":"metric"}' != '{"city":"Paris",'`,
and `/__aimock/misbehavior` answered `409 Conflict`. No test outside the
misbehavior modules failed, so this is not a
default-behavior regression.

Fix: `AIMockServer(enable_misbehavior=False)` appends `--misbehavior`
when true. The default stays off, as in
1.44.0. The three misbehavior modules override `aimock` with a server
that opts in.

Local proof (Python 3.12, Node 26, `AIMOCK_CLI_PATH=dist/cli.js`, `cd
packages/aimock-pytest && pytest tests/ -v`):

```
RED   (cbcca44): 188 failed, 60 passed, 103 warnings in 71.36s (0:01:11)
GREEN (fe5db7c): 248 passed, 103 warnings in 65.26s (0:01:05)
```

- Bedrock stream default-mode test parses event-stream frames instead of
regex (was flaky on random CRC bytes)

## Review round 2 fixes

- **R2-F1/F2/F3**: in legacy mode, the fixture listing (`GET
/__aimock/fixtures`) and the journal match 1.44.0 byte for byte.
- **Public export parity**: `isContentWithToolCallsResponse`,
`collapseOpenAISSE` and `collapseStreamingResponse` return the 1.44.0
output again (93/93 export probe calls identical to 1.44.0).
- **O7**: when `llm.responsesTools` is invalid, the warning names the
mode that actually runs (`Using "extended" from --responses-tools.`);
with no flag the text is unchanged.
- **record.mcp opt-in**: `llm.record.mcp` is read only with the new
`llm.enableMcpRecording: true`; without it the key is ignored with one
warning, as 1.44.0 ignored it.


## Review round 3 fixes

- **R3-F1**: the stream builders reproduce the 1.44.0 output when
`toolCalls` is not an array.
- **R3-F2**: the `responsesTools` mode is per server, so servers that
share fixtures no longer affect each other.



## Review round 4 fixes

- Default mode keeps caller fixture identity
(addFixture/on/addFixtures/prependFixture) as 1.44.0 did; the per-add
copy happens only with enableMisbehavior
- Extended servers count and journal the caller's fixtures (identity
matches 608a484)
- POST /__aimock/fixtures on an extended server no longer leaks extended
normalization into a shared caller array
- Identity APIs behave the same with enableMisbehavior on
(journal/counts/findByFixture name the caller's fixture; same object
added twice still gives two fault sources)
- Docs: responsesTools mode is fixed at server start
jpr5 added a commit that referenced this pull request Oct 10, 2026
Includes #501, #504, #506, #508, #509, #510, #511, #512, #513, #514, #515, #517, #518.

## [1.45.0] - 2026-10-10

Every fixture, config and request that 1.44.0 accepted behaves the same by default. New behavior
that would change those inputs is opt-in.

### Added

- Record live MCP servers into `mcpFakes` files and replay them offline: `--mcp-record
  <mount>=<url>` and `--mcp-proxy-only <mount>=<url>` (`llmock` bin and Docker image),
  `llm.record.mcp` with the opt-in `llm.enableMcpRecording: true` for `aimock --config` (without the
  opt-in, `llm.record.mcp` is ignored with a warning, as 1.44.0 ignored it), and
  `MCPMock.enableRecording()` / `disableRecording()` (#509).
- Recordings never contain headers and redact known secrets (`AIMOCK_RECORD_SECRET_VALUES`,
  `AIMOCK_MCP_UPSTREAM_AUTH`); a file is not written while a secret remains (#509).
- Recorded MCP fakes replay list order, progress and log notifications, and call timing (scaled by
  `--replay-speed`). The recorded `mcpFakes` keys (`list`, `recorded`, `timing`, `notifications`,
  `durationMs`) need 1.45.0 or later (#509).
- Per-test MCP fake report at `GET /__aimock/mcp/fakes/report` (#509).
- Vitest and Jest: `fakesFor()` and `fakesReport()` on a new `AimockFakesHandle` returned by
  `useAimock()`, and opt-in `useAimock({ fakesReport: "warn" | "fail" })` (default `"off"`); Jest
  option `testIdRoot`. New exports `assertFakesReport`, `AimockFakesReportError` and
  `AimockTestIdCollisionError` (#509).
- aimock-pytest 0.7.0: `fakes_for`, `fakes_report` and `assert_fakes_report` (#509), and
  `AIMockServer` opt-ins `enable_misbehavior` and `responses_tools` (#508, #511).
- Workflow-target examples: a Mastra Workflow and a LangGraph JS graph call MCP fakes with no LLM
  (`examples/mcp-workflow-targets`), plus record and report proofs (#509).
- Model misbehavior faults (invalid tool arguments, schema violations, unknown tools, duplicate IDs,
  length stops, empty responses, refusals, content filtering, reasoning-only) across provider wires,
  with seeded rates, `/__aimock/misbehavior` runtime controls, journal evidence and the
  `aimock_misbehavior_total` metric. Off unless enabled with `enableMisbehavior: true`
  (programmatic, including `useAimock`), `llmock --misbehavior` / `aimock validate --misbehavior`,
  or `aimock --misbehavior`, or the config opt-in `llm.enableMisbehavior: true` (an
  `llm.misbehavior` key alone never enables it, as in 1.44.0). Invalid misbehavior settings throw
  the new `MisbehaviorConfigError` (#508).
- Namespaced function calls and custom tool calls on the OpenAI Responses API, over HTTP, SSE and
  WebSocket, plus `match.toolNamespace` (#511). Like the fixture fields below, `match.toolNamespace`
  applies only with `responsesTools: "extended"`; in the default mode it is ignored, as in 1.44.0.
- Responses namespaced and custom tool calls: with `responsesTools: "extended"` (CLI
  `--responses-tools extended`, also on `aimock validate`; config `llm.responsesTools`), tools
  inside `namespace` tools, `custom` tools and `additional_tools` / `tool_search_output` tools are
  visible to `toolName`, `toolNamespace` and predicates, custom tool rounds count for
  `hasToolResult`, `toolCallId` and `turnIndex`, and fixtures can answer with namespaced calls and
  `customToolCalls` / `responsesBlocks`. The default `"legacy"` keeps 1.44.0 behavior. New types:
  `CustomToolCallMessage`, `ResponsesToolsMode` (#511).
- Record mode keeps Responses namespaces and custom tool calls (`customToolCalls`,
  `responsesBlocks`, `CollapseResult.customToolCalls`) in streamed recordings when `responsesTools:
  "extended"` is set (#513).
- Strict tool arguments: with `strictToolArguments` (CLI `--strict-tool-arguments`, config
  `llm.strictToolArguments`), malformed tool-call argument strings are sent unchanged on string
  wires and return an explicit error on object wires. The default keeps 1.44.0 behavior (#501).
- Vertex and Bedrock modeled-contract comparisons for the misbehavior faults in the daily drift
  checks (#508).

### Fixed

- MCP recording checks its config at start, redacts credentials in errors, fails loudly when the
  upstream dies mid-body (502 or a terminated stream, logged and journaled), and defaults `aimock
  --config` with record mounts to `logLevel: "warn"` (#517).
- Kept 1.44.0 default behavior for strict tool arguments, model misbehavior, Responses namespaced
  and custom tools, `llm.record.mcp`, fixture identity and the exported stream helpers, and made
  each new behavior opt-in (#518).
- Kept the published types compatible with 1.44.0: `AimockHandle`, `Mountable` and
  `McpFakeAddOrigin` keep their 1.44.0 shape, and every entry point type-checks with `skipLibCheck:
  false` (#517).
- Align Cohere streaming length-stop faults with captured provider framing (#508).
- Retain drift evidence across retries and for oversized records (#510, #512).
- Classify `claude-haiku-5-5` in the drift model registry (#515).
- Docs: dark-site font smoothing, consistent homepage comparison headings, the test-suite highlight,
  and the copy-button fallback (#504, #506, #514).
jpr5 added a commit that referenced this pull request Oct 10, 2026
Includes #501, #504, #506, #508, #509, #510, #511, #512, #513, #514, #515, #517, #518.

## [1.45.0] - 2026-10-10

Every fixture, config and request that 1.44.0 accepted behaves the same by default. New behavior
that would change those inputs is opt-in.

### Added

- Record live MCP servers into `mcpFakes` files and replay them offline: `--mcp-record
  <mount>=<url>` and `--mcp-proxy-only <mount>=<url>` (`llmock` bin and Docker image),
  `llm.record.mcp` with the opt-in `llm.enableMcpRecording: true` for `aimock --config` (without the
  opt-in, `llm.record.mcp` is ignored with a warning, as 1.44.0 ignored it), and
  `MCPMock.enableRecording()` / `disableRecording()` (#509).
- Recordings never contain headers and redact known secrets (`AIMOCK_RECORD_SECRET_VALUES`,
  `AIMOCK_MCP_UPSTREAM_AUTH`); a file is not written while a secret remains (#509).
- Recorded MCP fakes replay list order, progress and log notifications, and call timing (scaled by
  `--replay-speed`). The recorded `mcpFakes` keys (`list`, `recorded`, `timing`, `notifications`,
  `durationMs`) need 1.45.0 or later (#509).
- Per-test MCP fake report at `GET /__aimock/mcp/fakes/report` (#509).
- Vitest and Jest: `fakesFor()` and `fakesReport()` on a new `AimockFakesHandle` returned by
  `useAimock()`, and opt-in `useAimock({ fakesReport: "warn" | "fail" })` (default `"off"`); Jest
  option `testIdRoot`. New exports `assertFakesReport`, `AimockFakesReportError` and
  `AimockTestIdCollisionError` (#509).
- aimock-pytest 0.7.0: `fakes_for`, `fakes_report` and `assert_fakes_report` (#509), and
  `AIMockServer` opt-ins `enable_misbehavior` and `responses_tools` (#508, #511).
- Workflow-target examples: a Mastra Workflow and a LangGraph JS graph call MCP fakes with no LLM
  (`examples/mcp-workflow-targets`), plus record and report proofs (#509).
- Model misbehavior faults (invalid tool arguments, schema violations, unknown tools, duplicate IDs,
  length stops, empty responses, refusals, content filtering, reasoning-only) across provider wires,
  with seeded rates, `/__aimock/misbehavior` runtime controls, journal evidence and the
  `aimock_misbehavior_total` metric. Off unless enabled with `enableMisbehavior: true`
  (programmatic, including `useAimock`), `llmock --misbehavior` / `aimock validate --misbehavior`,
  or `aimock --misbehavior`, or the config opt-in `llm.enableMisbehavior: true` (an
  `llm.misbehavior` key alone never enables it, as in 1.44.0). Invalid misbehavior settings throw
  the new `MisbehaviorConfigError` (#508).
- Namespaced function calls and custom tool calls on the OpenAI Responses API, over HTTP, SSE and
  WebSocket, plus `match.toolNamespace` (#511). Like the fixture fields below, `match.toolNamespace`
  applies only with `responsesTools: "extended"`; in the default mode it is ignored, as in 1.44.0.
- Responses namespaced and custom tool calls: with `responsesTools: "extended"` (CLI
  `--responses-tools extended`, also on `aimock validate`; config `llm.responsesTools`), tools
  inside `namespace` tools, `custom` tools and `additional_tools` / `tool_search_output` tools are
  visible to `toolName`, `toolNamespace` and predicates, custom tool rounds count for
  `hasToolResult`, `toolCallId` and `turnIndex`, and fixtures can answer with namespaced calls and
  `customToolCalls` / `responsesBlocks`. The default `"legacy"` keeps 1.44.0 behavior. New types:
  `CustomToolCallMessage`, `ResponsesToolsMode` (#511).
- Record mode keeps Responses namespaces and custom tool calls (`customToolCalls`,
  `responsesBlocks`, `CollapseResult.customToolCalls`) in streamed recordings when `responsesTools:
  "extended"` is set (#513).
- Strict tool arguments: with `strictToolArguments` (CLI `--strict-tool-arguments`, config
  `llm.strictToolArguments`), malformed tool-call argument strings are sent unchanged on string
  wires and return an explicit error on object wires. The default keeps 1.44.0 behavior (#501).
- Vertex and Bedrock modeled-contract comparisons for the misbehavior faults in the daily drift
  checks (#508).

### Fixed

- MCP recording checks its config at start, redacts credentials in errors, fails loudly when the
  upstream dies mid-body (502 or a terminated stream, logged and journaled), and defaults `aimock
  --config` with record mounts to `logLevel: "warn"` (#517).
- Kept 1.44.0 default behavior for strict tool arguments, model misbehavior, Responses namespaced
  and custom tools, `llm.record.mcp`, fixture identity and the exported stream helpers, and made
  each new behavior opt-in (#518).
- Kept the published types compatible with 1.44.0: `AimockHandle`, `Mountable` and
  `McpFakeAddOrigin` keep their 1.44.0 shape, and every entry point type-checks with `skipLibCheck:
  false` (#517).
- Align Cohere streaming length-stop faults with captured provider framing (#508).
- Retain drift evidence across retries and for oversized records (#510, #512).
- Classify `claude-haiku-5-5` in the drift model registry (#515).
- Docs: dark-site font smoothing, consistent homepage comparison headings, the test-suite highlight,
  and the copy-button fallback (#504, #506, #514).
- Invalid opt-in values never stop startup: an invalid `responsesTools` warns once and runs
  `"legacy"`, and a non-boolean `llm.strictToolArguments`, `llm.enableMisbehavior` or
  `llm.enableMcpRecording` warns once and stays off (#518).
jpr5 added a commit that referenced this pull request Oct 10, 2026
Release npm/plugin **1.45.0** and **aimock-pytest 0.7.0** from `main` at
c9c19d1 (#518 merged). Every fixture, config and request that 1.44.0
accepted behaves the same by default; new behavior is opt-in (#518).
This branch was rebuilt as one commit on top of the #518 merge,
replacing the earlier release commit bdfaeef.

> **Prepared, not merged.** `publish-release.yml` runs on every push to
`main`. Merging this PR publishes: npm `@copilotkit/aimock@1.45.0`, the
`v1.45.0` tag, a force-move of the `v1` major tag, the GitHub Release,
the Docker images (`ghcr.io/copilotkit/aimock` and
`ghcr.io/copilotkit/llmock`, `:1.45.0` and `:latest`), `aimock-pytest`
0.7.0 on PyPI and the Slack announcement. The docs site deploys from
`main:/docs` on merge. Merge only when the release is authorized. Do not
dispatch `publish-release.yml` from this branch.

## Versions

| Surface | Before | After |
|---|---|---|
| `package.json` `version` | 1.44.0 | 1.45.0 |
| `.claude-plugin/plugin.json` `version` | 1.44.0 | 1.45.0 |
| `.claude-plugin/marketplace.json` `source.version` | ^1.44.0 | ^1.45.0
|
| `charts/aimock/Chart.yaml` `appVersion` (chart `version` stays 0.1.0)
| 1.44.0 | 1.45.0 |
| `docs/index.html` CLI banner | aimock v1.44.0 | aimock v1.45.0 |
| `packages/aimock-pytest/pyproject.toml` `version` | 0.6.0 | 0.7.0 |
| `packages/aimock-pytest/src/aimock_pytest/_version.py`
`AIMOCK_VERSION` | 1.44.0 | 1.45.0 |
| `packages/aimock-pytest/README.md` `--aimock-version` default | 1.44.0
| 1.45.0 |
| `skills/write-fixtures/SKILL.md` `toolNamespace` pin note | "newer
than its default 1.44.0 pin" | "aimock 1.45.0+, its default pin from
0.7.0" |

"Unreleased" wording now names 1.45.0 in `README.md` (misbehavior note),
`docs/cohere/index.html` (length-stop fidelity) and
`docs/model-misbehavior/index.html` (two places).

The pytest pin bump matters: the new
`AIMockServer(enable_misbehavior=..., responses_tools=...)` options pass
`--misbehavior` / `--responses-tools`, which the 1.44.0 CLI does not
accept. With the pin at 1.45.0, default-download users get a server that
supports them.

**1.45.0 is a minor.** With #518, default behavior and the published
types match 1.44.0. The remaining type additions (the optional
`misbehavior` key, `ChatCompletionRequest.customTools`, `useAimock`
returning `AimockFakesHandle`) were accepted by the maintainer as
additive optional typing (minor).

## CHANGELOG

## [1.45.0] - 2026-10-10

Every fixture, config and request that 1.44.0 accepted behaves the same
by default. New behavior that would change those inputs is opt-in.

### Added

- Record live MCP servers into `mcpFakes` files and replay them offline:
`--mcp-record <mount>=<url>` and `--mcp-proxy-only <mount>=<url>`
(`llmock` bin and Docker image), `llm.record.mcp` with the opt-in
`llm.enableMcpRecording: true` for `aimock --config` (without the
opt-in, `llm.record.mcp` is ignored with a warning, as 1.44.0 ignored
it), and `MCPMock.enableRecording()` / `disableRecording()` (#509).
- Recordings never contain headers and redact known secrets
(`AIMOCK_RECORD_SECRET_VALUES`, `AIMOCK_MCP_UPSTREAM_AUTH`); a file is
not written while a secret remains (#509).
- Recorded MCP fakes replay list order, progress and log notifications,
and call timing (scaled by `--replay-speed`). The recorded `mcpFakes`
keys (`list`, `recorded`, `timing`, `notifications`, `durationMs`) need
1.45.0 or later (#509).
- Per-test MCP fake report at `GET /__aimock/mcp/fakes/report` (#509).
- Vitest and Jest: `fakesFor()` and `fakesReport()` on a new
`AimockFakesHandle` returned by `useAimock()`, and opt-in `useAimock({
fakesReport: "warn" | "fail" })` (default `"off"`); Jest option
`testIdRoot`. New exports `assertFakesReport`, `AimockFakesReportError`
and `AimockTestIdCollisionError` (#509).
- aimock-pytest 0.7.0: `fakes_for`, `fakes_report` and
`assert_fakes_report` (#509), and `AIMockServer` opt-ins
`enable_misbehavior` and `responses_tools` (#508, #511).
- Workflow-target examples: a Mastra Workflow and a LangGraph JS graph
call MCP fakes with no LLM (`examples/mcp-workflow-targets`), plus
record and report proofs (#509).
- Model misbehavior faults (invalid tool arguments, schema violations,
unknown tools, duplicate IDs, length stops, empty responses, refusals,
content filtering, reasoning-only) across provider wires, with seeded
rates, `/__aimock/misbehavior` runtime controls, journal evidence and
the `aimock_misbehavior_total` metric. Off unless enabled with
`enableMisbehavior: true` (programmatic, including `useAimock`), `llmock
--misbehavior` / `aimock validate --misbehavior`, or `aimock
--misbehavior`, or the config opt-in `llm.enableMisbehavior: true` (an
`llm.misbehavior` key alone never enables it, as in 1.44.0). Invalid
misbehavior settings throw the new `MisbehaviorConfigError` (#508).
- Namespaced function calls and custom tool calls on the OpenAI
Responses API, over HTTP, SSE and WebSocket, plus `match.toolNamespace`
(#511). Like the fixture fields below, `match.toolNamespace` applies
only with `responsesTools: "extended"`; in the default mode it is
ignored, as in 1.44.0.
- Responses namespaced and custom tool calls: with `responsesTools:
"extended"` (CLI `--responses-tools extended`, also on `aimock
validate`; config `llm.responsesTools`), tools inside `namespace` tools,
`custom` tools and `additional_tools` / `tool_search_output` tools are
visible to `toolName`, `toolNamespace` and predicates, custom tool
rounds count for `hasToolResult`, `toolCallId` and `turnIndex`, and
fixtures can answer with namespaced calls and `customToolCalls` /
`responsesBlocks`. The default `"legacy"` keeps 1.44.0 behavior. New
types: `CustomToolCallMessage`, `ResponsesToolsMode` (#511).
- Record mode keeps Responses namespaces and custom tool calls
(`customToolCalls`, `responsesBlocks`, `CollapseResult.customToolCalls`)
in streamed recordings when `responsesTools: "extended"` is set (#513).
- Strict tool arguments: with `strictToolArguments` (CLI
`--strict-tool-arguments`, config `llm.strictToolArguments`), malformed
tool-call argument strings are sent unchanged on string wires and return
an explicit error on object wires. The default keeps 1.44.0 behavior
(#501).
- Vertex and Bedrock modeled-contract comparisons for the misbehavior
faults in the daily drift checks (#508).

### Fixed

- MCP recording checks its config at start, redacts credentials in
errors, fails loudly when the upstream dies mid-body (502 or a
terminated stream, logged and journaled), and defaults `aimock --config`
with record mounts to `logLevel: "warn"` (#517).
- Kept 1.44.0 default behavior for strict tool arguments, model
misbehavior, Responses namespaced and custom tools, `llm.record.mcp`,
fixture identity and the exported stream helpers, and made each new
behavior opt-in (#518).
- Kept the published types compatible with 1.44.0: `AimockHandle`,
`Mountable` and `McpFakeAddOrigin` keep their 1.44.0 shape, and every
entry point type-checks with `skipLibCheck: false` (#517).
- Align Cohere streaming length-stop faults with captured provider
framing (#508).
- Retain drift evidence across retries and for oversized records (#510,
#512).
- Classify `claude-haiku-5-5` in the drift model registry (#515).
- Docs: dark-site font smoothing, consistent homepage comparison
headings, the test-suite highlight, and the copy-button fallback (#504,
#506, #514).
- Invalid opt-in values never stop startup: an invalid `responsesTools`
warns once and runs `"legacy"`, and a non-boolean
`llm.strictToolArguments`, `llm.enableMisbehavior` or
`llm.enableMcpRecording` warns once and stays off (#518).

## Release checklist

- [x] Opt-in names for #501, #508, #511, #513 match the merged compat PR
#518 (`strictToolArguments` / `--strict-tool-arguments` /
`llm.strictToolArguments`; `enableMisbehavior` / `--misbehavior` /
`aimock validate --misbehavior` / `llm.enableMisbehavior`;
`responsesTools` / `--responses-tools` / `aimock validate
--responses-tools` / `llm.responsesTools`; `llm.enableMcpRecording`;
pytest `enable_misbehavior` / `responses_tools`), checked against the
source on this branch.
- [x] Pytest pin `AIMOCK_VERSION` = `"1.45.0"` and the README
`--aimock-version` default = 1.45.0.
- [x] aimock-pytest `version` 0.6.0 -> 0.7.0.
- [x] Homepage "Requires 1.45.0+" (MCP scenario fakes card) and the
other "1.45.0 or later" docs notes match this release.
- [x] Version surfaces bumped (table above). `git grep -F 1.44.0`: every
remaining hit is a history comparison ("as in 1.44.0", "up to 1.44.0",
the 1.44.0 CHANGELOG section) or recorded `aimockVersion` provenance in
fixtures/examples.
- [x] "Unreleased" wording removed from README and docs.
- [x] `src/__tests__/package-types-compat.test.ts` on the release head:
2/2 pass.
- [ ] 1.44.0 A/B probe re-run on the release head: not re-run here; this
commit changes only version strings, docs text and the CHANGELOG, and
#518 carries the A/B proof on the same code.

## Local gates (release head)

- `pnpm install --frozen-lockfile`, `format:check`, `lint`, `typecheck`,
`build`, `test:exports` (publint + attw): pass
- Full vitest, 12 shards: 14,069 passed, 3 skipped (env-gated), 0 failed
- aimock-pytest, installed as CI does (hash-pinned hatchling,
`--no-build-isolation`), Python 3.12, `AIMOCK_CLI_PATH=dist/cli.js`: 248
passed
- `npm pack --dry-run`: `copilotkit-aimock-1.45.0.tgz`, 766 files
(1.44.0: 722), dist/, fixtures/, skills/, .claude-plugin/, README,
CHANGELOG, LICENSE; no src or tests
- commitlint: 0 problems, 0 warnings. Commit is SSH-signed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant