Repository navigation
feat: namespaced function calls and custom tool calls on the Responses API - #511
Merged
Merged
Conversation
…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.
commit: |
Add a feature bullet linking to the Responses API docs section for namespaced function calls, custom tool calls and toolNamespace matching.
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.
This was referenced Oct 9, 2026
Merged
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #505
Summary
Codex CLI talks to models over the OpenAI Responses API. It routes MCP and sub-agent tools by
(namespace, name), and itsapply_patchtool 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.namespace, emitted asfunction_call.namespace. AtoolCallsentry withtype: "custom"(or acustomToolCallblock) emits acustom_tool_callitem withresponse.custom_tool_call_input.delta/.doneevents. Both work over HTTP SSE, non-streaming and WebSocket. Fixtures that use neither field produce byte-identical output.namespacetools,customtools, and tools delivered inadditional_toolsandtool_search_outputinput items are now visible totoolNameand predicates. A newmatch.toolNamespacekey selects by exact namespace, alone or paired withtoolName.custom_tool_callandcustom_tool_call_outputhistory items count as tool rounds.aimock_unsupported_tool_callin that wire's own error envelope (an error frame on Realtime and Gemini Live). A malformed fixture tool call getsaimock_invalid_fixture_tool_callthe same way, fromtoolCallsor blocks, static or factory. Fixtures that loaded and served before keep working.tool-unknown-nameresolve a Responses call by namespace and name.Spec: https://www.notion.so/3f43aa381852819c9dc0db15a4b40523
Design notes and decisions
mcp__<server>with no trailing__. The docs follow real Codex 0.161.0 output.NamespaceToolParam.namehasminLength: 1).toolNamespacematches any request tool that carries anamespace. Only the Responses API sets one from namespace tools.namespaceis dropped and a custom call is rejected.toolCallsload as on main, with warnings (legacytype: "toolCall", numericid, non-stringname,inputon a function call). The new fields are strict. When non-emptyblocksare served, legacytoolCallsare ignored.type: "custom"witharguments, andtype: "customToolCall"intoolCalls.ToolCallResponse.toolCallsbecomesFixtureToolCall[](a breaking type change, noted in the CHANGELOG).AudioResponse.toolCallsstaysToolCall[].custom_tool_call_outputbecomes empty text (200, as on main).code. Existing fields are unchanged.tool-call-id-duplicatedestination. Output-clearing faults withtool=<custom name>are not applicable.stop-length-mid-toolalways 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), Codexrust-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 assrc/__tests__/fixtures/codex-responses-lite-request.json.Commits
9c0cf717test: 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.4a41f44ffeat: 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.319573b7feat: match namespace, custom and additional_tools tools. Request tool flattening, thetoolNamespacekey (router, journal, validation, aimock-pytest), history conversion, and 400s for malformed tool collections on HTTP and WS.047db367fix(misbehavior): keep faults off custom tool calls and unservable fixtures. Also the namespace-aware schema lookup.16423c12test: drift legs for namespaced and custom calls, streaming and non-streaming.af56cad0docs: Responses API, fixtures, WebSocket and multi-turn pages, and the write-fixtures skill.efd33ae3docs(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.5e36eb2cfix(misbehavior): judgetool-unknown-namewithin the call's own namespace.3acf0955docs(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:exportspass;pnpm test13318 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_KEYlocally). 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, isolatedCODEX_HOME,approval_policy = "never", aimock with--validate-on-load, one aimock process per scenario. Turn 2 fixtures match ontoolCallId. The MCP server is a small stdio server, so the namespace ismcp__stdiodocs.custom_tool_callapply_patchhello.txtwritten with the fixture's content. 2 requests; turn 2 carried the custom call output and matched. Final message "done: patch applied".custom_tool_callapply_patchfunction_callmcp__stdiodocs/lookup_doctools/calllookup_docwith{"id":"505"}. The output came back and matched turn 2. Final message "done: MCP round complete".function_callmcp__stdiodocs/lookup_docOn main before this PR (checked before the rebase),
--validate-on-loadrejects the custom fixture, and Codex fails the MCP call withunsupported call: lookup_doc.Over WS, Codex's empty warm-up
response.creategets 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):
"arguments" must be a string→ 200 withoutput_item.added, input deltas,.done,output_item.done.toolNameon a tool offered only inside a namespace: 404 → 200,namespaceon every item, over HTTP, SSE and WS.toolNamespace+toolNamepair with the wrong namespace registered first: 404 → 200 from the right fixture.custom_tool_callplus output withhasToolResult: true: 404 → 200.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.
toolCallswith non-object entries: uncoded 500 on Responses, 200 nameless call on Chat → codedaimock_invalid_fixture_tool_call, same as static.toolCallsentry: Chat 200function: {}, Responses uncoded 500 → coded 500 on every wire. Array block in the loader: wrong message → "must be an object".typewith no stringargumentson Responses: uncoded 500 → coded error on HTTP and WS.|: false shadow warning → no warning; the true duplicate still warns.toolCallswhenblockswere served → warns only on what is served.FunctionFixtureBlocknot exported: TS2305 in a consumer → compiles./api/generatemisreported unknown blocks as tool calls (400) → same 500 as/api/chat./api/generateblock errors journaled with a null body → body and fixture journaled; response bytes unchanged./api/generatewith a tool call plus an unknown block: 400 → the same 500 as/api/chat, in both orders.applied: true) → 0.tool-call-id-duplicatecould land on a custom call: 22 violations → 0.tool=<custom name>: 16/16 cleared the custom call → 0/16; reported as not applicable.tool-unknown-namechecked the replacement name across all namespaces: wrong 501 and a skipped_v2name → 200, renamed within the call's namespace.toolsvalidation, check order, object arguments and/api/generatewere 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
input. A null item or object input throws a TypeError, sent asserver_errorand not journaled.function_call_outputis not matched bytoolResultContains.additional_toolsdeveloper item becomes an empty system message.nullentry intoolCallsmakesnormalizeResponsethrow a TypeError at file load, not a validation error.blocksthrows a plain Error: uncoded 500 and no journal error. Since this PR only tool-call blocks get the coded error.resolveFixtureBlocksrequires stringarguments, but it stringifies objects.argumentsof null, a number or a boolean pass load validation, becauseJSON.parseaccepts them.argumentsfails load as "not valid JSON: undefined", while the runtime treats it as{}. The same message is given for a mistypedtype, without naming it.toolCallsarguments on the Responses API return an uncoded 500 with journal error null.stream. A request rejected for invalid tool arguments still uses up an interaction id.sequence_number.inputdeltas.item_referencewith notype(ortype: null) is not recognised, so the next tool output merges into the previous turn./api/generaterejects a text-onlyblocksfixture with 400; for{content, blocks:[text]}it returnscontentwhile/api/chatreturns the block text; a valid tool fixture adds a misbehavior summary{applied:false}.argumentsstreams no argument deltas; a function call with noargumentscrashes mid-stream and is journaled as 200.toolCallsarguments make thetool-args-invalid-jsonfault crash with an uncoded 500.""collide (this PR corrected only the comment).describeMatchreports the wrong location.FixtureFileBlockis not exported from the package entry point.openai-live; the "HTTP SSE Text Response" example does not sendstream: true.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
toolNamespace. Bump it at the next aimock release.previous_response_idstate. 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.response.createover WebSocket gets a 404 (on main).namespace, so a recorded Codex session replays as "unsupported call" (PR 2).outputarray (PR 2).agent_messageinput items, which aimock does not convert.openaidev dependency (4.104.0) has no types for custom tool calls ornamespace, and itsResponseStreamignoresoutput_item.doneandresponse.incomplete. Bump it.C. Minor follow-ups in this feature area
Behaviour and validation:
/api/v3/request is labelled "BytePlus ModelArk" in error text, even when BytePlus is not configured.toolCallsis dropped with no error; make it a coded error.namespacetool, non-object and other-type entries are dropped silently, and tools with no stringnameare accepted.toolscounts as absent, butinput[i].tools: nullgets 400.namespaceis dropped and a non-string custominputbecomes "" with no warning.namespacetool with nodescriptionor an emptytoolsarray is accepted; the OpenAI spec rejects both.inputis a warning on atoolCallsfunction entry but an error on atoolCallblock.toolNamespaceload warning says only Responses requests carry namespaces, but a Chat Completions tool with anamespacealso matches.UnsupportedToolCallErrordoes not give thetoolCalls[i]index and can name an empty tool.typeand objectargumentsis 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 }./api/generateuses the OpenAI envelope while/api/chatuses Ollama's; a custom call gets 400 on generate but 500 on chat.statusoncustom_tool_callitems; openai-node's type has no such field. Verify against the real API.FixtureToolCallError,ResponsesInputItemandResponsesToolDefare not exported.Types and names:
FixtureFileToolCallis one interface with optional fields, not a union.ResponsesToolDefhas a catch-all member that stops narrowing and forces casts.resolveFixtureBlocksoverload types are loose.invalidToolArgumentsEnvelopenow serves three error types, so the name is misleading.hasToolCallsreads the rawtoolCalls, not the resolved calls, and has a redundant alias.isPlainObjectin src/helpers.ts is another private object guard; the repo already has several (isObjectin jsonrpc.ts and misbehavior.ts,isPlainObjectin mcp-fakes.ts). Consolidating them is a separate cleanup.Misbehavior:
servesFixturetreats 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.validateFixtureMisbehavioraccepts a fixture fault that can never fire on wires that reject invalid-JSON object arguments.namespace, is not namespace-aware.stop-length-mid-tooldrops custom calls after the cut.Drift tests:
isModelNotFoundtreats a 400 saying "does not exist" as a retired model, so a real shape rejection can show as skipped.JSON.parsewith no guard; the model-listing fetch is not tagged as infra;response.in_progressis dropped from the SDK expectations.Unit tests:
{a:1}, so it never matches.logLevel: "silent".response.completed.tool_search_outputcase.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.