Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3cf3956
feat: support OpenAI transcription replay
jpr5 Jul 30, 2026
cfbbe0a
test: cover transcription replay contracts
jpr5 Jul 30, 2026
89d4e5c
chore: release v1.38.0
jpr5 Aug 3, 2026
ead88d4
fix: preserve transcription terminal replay contracts
jpr5 Aug 3, 2026
c45bc66
fix: preserve realtime output audio configuration
jpr5 Aug 3, 2026
4b6cedb
test: make replay speed timing deterministic
jpr5 Aug 3, 2026
b5ce7df
test: strengthen replay timing contracts
jpr5 Aug 3, 2026
cf983c3
feat: validate inbound API keys
jpr5 Aug 3, 2026
52c9240
feat: support API keys in pytest helper
jpr5 Aug 3, 2026
7c6262e
docs: describe API key validation
jpr5 Aug 3, 2026
f3a21fb
test: stabilize CLI integration deadlines
jpr5 Aug 3, 2026
1451855
fix: scope API key egress policy to requests
jpr5 Aug 3, 2026
bb31f5c
test: cover API key client configuration
jpr5 Aug 3, 2026
d2e346c
ci: enforce npm release before pytest publish
jpr5 Aug 3, 2026
c26dcc3
test: isolate pytest subprocess plugin loading
jpr5 Aug 3, 2026
29645e1
ci: disable cache in pytest publish workflow
jpr5 Aug 3, 2026
0e74e57
fix: sequence pytest release and redact credentials
jpr5 Aug 4, 2026
bb356ac
docs: merge duplicate changelog heading
jpr5 Aug 4, 2026
19b5404
fix(realtime): input transcription config no longer hijacks a convers…
jpr5 Aug 4, 2026
3b84ac6
fix(transcription): terminate the streaming response with data: [DONE]
jpr5 Aug 4, 2026
ac7ffcc
fix(realtime): match live GA session, usage and match-count semantics
jpr5 Aug 4, 2026
09d370f
Merge origin/main into fix/live-transcribe-support-pr
jpr5 Aug 4, 2026
1c9008f
docs(changelog): record the journal credential-redaction fix in 1.38.0
jpr5 Aug 4, 2026
fcd8d56
fix(realtime): make synthesized transcription usage model-aware
jpr5 Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"source": {
"source": "npm",
"package": "@copilotkit/aimock",
"version": "^1.35.1"
"version": "^1.38.0"
},
"description": "Fixture authoring skill for @copilotkit/aimock — LLM, multimedia (image/TTS/transcription/video), MCP, A2A, AG-UI, vector, embeddings, structured output, sequential responses, streaming physics, record/replay, agent loop patterns, and debugging"
}
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aimock",
"version": "1.35.1",
"version": "1.38.0",
"description": "Fixture authoring guidance for @copilotkit/aimock — LLM, multimedia, MCP, A2A, AG-UI, vector, and service mocking",
"author": {
"name": "CopilotKit"
Expand Down
48 changes: 0 additions & 48 deletions .github/workflows/publish-pytest.yml

This file was deleted.

50 changes: 50 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,53 @@ jobs:
curl -s -X POST "$SLACK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"

publish-pytest:
needs: [build, publish]
if: >-
always() &&
(needs.publish.result == 'success' || needs.build.outputs.published == 'true')
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { persist-credentials: false }
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
package-manager-cache: false

- name: Verify pinned npm release is published
run: |
VERSION=$(python -c "import sys; sys.path.insert(0, 'packages/aimock-pytest/src'); from aimock_pytest._version import AIMOCK_VERSION; print(AIMOCK_VERSION)")
npm view "@copilotkit/aimock@${VERSION}" version

- name: Install build tools
run: pip install hatch

- name: Check if pytest version is already published
id: check
run: |
VERSION=$(python -c "import tomllib; print(tomllib.load(open('packages/aimock-pytest/pyproject.toml', 'rb'))['project']['version'])")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if pip install "aimock-pytest==$VERSION" --dry-run --no-deps 2>/dev/null; then
echo "published=true" >> "$GITHUB_OUTPUT"
else
echo "published=false" >> "$GITHUB_OUTPUT"
fi

- name: Build pytest package
if: steps.check.outputs.published == 'false'
run: cd packages/aimock-pytest && hatch build

- name: Publish pytest package to PyPI
if: steps.check.outputs.published == 'false'
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: packages/aimock-pytest/dist/
1 change: 1 addition & 0 deletions .github/workflows/test-unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm test
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@

## [Unreleased]

## [1.38.0] - 2026-08-03

### Added

- OpenAI transcription replay for `gpt-transcribe` HTTP streams and `gpt-live-transcribe` Realtime sessions. Recorded responses retain transcript languages and usage metadata; replay supports progressive transcript events, timing controls, strict fixture matching, and interruption behavior.
- Opt-in inbound API-key validation for HTTP, control, mount, and WebSocket boundaries. Validated test credentials are never forwarded by the generic record/proxy path; auth-enabled proxying requires a configured provider credential.
- API-key egress isolation is request-scoped across generic, AG-UI, fal, and video record paths. Authorization schemes are case-insensitive, and OpenRouter off-origin unsigned URLs never receive a configured provider credential.
- `aimock-pytest` `0.5.2` is released by the Release workflow after npm `@copilotkit/aimock` `1.38.0`; the PyPI job verifies the pinned npm package is available before building a wheel.
- **Reasoning `encrypted_content` on the Responses API.** aimock now synthesizes an opaque (base64) `reasoning.encrypted_content` blob on the TERMINAL reasoning item (`response.output_item.done` and non-streaming `output[]`). Emitted only when the request wants it: `include: ["reasoning.encrypted_content"]` OR a stateless request (`store: false` / ZDR). Stored / opted-out replays stay byte-identical. A request carrying the EXPLICIT `include: ["reasoning.encrypted_content"]` opt-in, against a reasoning-CAPABLE model, now gets a reasoning item even when the fixture declares NO `reasoning` summary — a `summary: []` item that exists purely to carry the blob, matching what real OpenAI returns when summaries were never requested. This is the shape a fixture recorded from the real stateless agent-framework flow has, so gating the item on a declared summary starved the exact case the feature exists to serve. The synthesized item leads `output[]` / takes `output_index` 0 and shifts the rest, emits `output_item.added` → `output_item.done` with no summary-text events (there is no summary part to describe), and is suppressed for non-reasoning models (gpt-4o etc.), which have no reasoning channel at all. Note the two gates differ in width on purpose: `store: false` still attaches the blob to a reasoning item that a declared summary already produces, but it does NOT synthesize an item where the fixture declares none — creating an output item that did not previously exist is a bigger behavior change than adding a field to one already on the wire, so it takes the gate that is directly observable in the request. `agent-framework-openai` >= 1.11.0 sends `include` and never `store`, so the narrower gate costs the feature nothing. This lets `agent-framework-openai` >= 1.11.0 replay reasoning-paired tool calls on a stateless reasoning + multi-tool chain (related: microsoft/agent-framework#7233). Applies to both the HTTP and WebSocket Responses transports. Known limitations: the blob is withheld from the in-progress `added` item, which is aimock's own simplification rather than upstream parity — real OpenAI populates `added` too (recorded captures carry a shorter blob there beside an empty summary, re-encrypted by `done`), but the field is `anyOf: [string, null]` and non-required, so omitting it is contract-legal and no known consumer requires it; and aimock skips inbound reasoning items, so it cannot reproduce OpenAI's `invalid_encrypted_content` rejection of a mismatched blob/id.
- **OpenRouter chat / LLM router simulation.** Requests whose original path starts with `/api/v1/` (point any OpenAI SDK at a `baseURL` ending `/api/v1`) are detected as OpenRouter and shaped to match real OpenRouter bytes: a `gen-` id prefix (a fixture `id` override still wins), a top-level `provider` (default = the winning model slug's author, fixture-overridable via `provider`), both `finish_reason` and `native_finish_reason` on every choice/delta, an always-present `system_fingerprint` and `service_tier` (null by default), `message.reasoning`, and a rich `usage` with a **fixture-scriptable** `cost` + `cost_details` (emitted only when a fixture supplies a cost — never fabricated), plus `is_byok` / `prompt_tokens_details` / `completion_tokens_details` when overridden. Callers on the plain OpenAI `/v1/...` base are byte-for-byte unchanged.
- **`models[]` fallback (router failover) simulation.** When the request body carries `models: [...]`, aimock walks `[model, ...models]` in order and serves the first fixture returning a NON-error response; a `429`/`503` error fixture on a candidate simulates a runtime provider failure and falls through to the next. The winning slug is echoed back as the top-level `model`, so a test asserts failover by reading `response.model`. (Deliberate non-goal: an unknown/invalid model is a fixture miss, not OpenRouter's up-front invalid-model 400.)
Expand All @@ -12,6 +18,10 @@
- **Opt-in `: OPENROUTER PROCESSING` SSE keepalive** (fixture option `openRouterProcessing`, default off): one comment line emitted before the first data frame, matching real OpenRouter streams.
- OpenRouter request extensions (`provider`, `models`, `route`, `reasoning`, `plugins`, `prediction`, `usage`) and attribution headers (`HTTP-Referer`, `X-OpenRouter-Title`, legacy `X-Title`) are accepted and journaled, never required or rejected.

### Fixed

- **`GET /__aimock/journal` no longer leaks accepted inbound credentials.** Journal header redaction is now derived from the same registry that decides which headers are accepted as credentials, so the two can no longer drift apart. Previously only `authorization`, `x-api-key` and `api-key` were redacted while `x-goog-api-key` and `xi-api-key` were accepted as credentials and journaled **in plaintext** — so a real Gemini / Veo / ElevenLabs key sent on those headers appeared verbatim in the journal, which is unauthenticated unless inbound API-key validation is enabled. Affects 1.37.4 and earlier. A test asserts every accepted header redacts, so adding a new accepted header without redacting it now fails.

## [1.37.4] - 2026-07-20

### Fixed
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,24 @@ Private and link-local addresses (loopback, RFC1918, CGNAT, cloud metadata, ULA,

On replay, `turnIndex` is a non-fatal disambiguator, not a hard reject gate: a content-matching fixture is served even when its scripted `turnIndex` differs from the request's assistant-message count. This kills false "no fixture matched" misses for multi-bubble agent runs (multi-step agents emit several assistant bubbles per logical turn). When a served fixture diverges from its scripted `turnIndex`, the match diagnostic carries `turnIndexRelaxed: true` and aimock logs a one-shot warning (at the `warn` log level — silent by default). To restore the legacy strict behavior where a defined `turnIndex` must equal the assistant count exactly, set `AIMOCK_STRICT_TURN_INDEX=1`. The record path is always strict regardless of this flag.

## API-key validation

By default aimock accepts all requests. Opt into inbound test-client validation with a programmatic option, top-level `aimock.json` field, or environment-only key list:

```ts
await createServer(fixtures, { auth: { apiKeys: ["test-key"] } });
```

```json
{ "auth": { "apiKeys": ["test-key"] } }
```

```bash
AIMOCK_API_KEYS=test-key,rotated-key npx @copilotkit/aimock --config aimock.json
```

Use `Authorization: Bearer <key>`, `Authorization: Key <key>`, `x-api-key`, `x-goog-api-key`, `api-key`, or `xi-api-key`. Every supplied credential must resolve to one configured key; mismatches return `401` with an OpenAI-compatible authentication error. HTTP routes, control APIs, mounts, and WebSocket upgrades are protected. Genuine CORS preflights plus `GET /health`, `GET /ready`, and `GET /metrics` remain public. This is inbound test access control, distinct from `record.providerKeys`; when enabled, proxying strips test credentials and requires a configured static provider credential before egress.

### aimock-owned upstream keys — `AIMOCK_PROVIDER_*_KEY`

In record or `--proxy-only` mode, aimock forwards the caller's auth header to the real provider unchanged. If your tests can only send a dummy placeholder key (e.g. an SDK that refuses to start without a non-empty API key), aimock can inject its own configured upstream key on a fixture-miss passthrough so the proxied call actually authenticates. Each provider has an independent env var, and the key is applied with the provider-correct wire scheme:
Expand Down
2 changes: 1 addition & 1 deletion charts/aimock/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ name: aimock
description: Mock infrastructure for AI application testing (OpenAI, Anthropic, Gemini, MCP, A2A, vector)
type: application
version: 0.1.0
appVersion: "1.35.1"
appVersion: "1.38.0"
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@copilotkit/aimock",
"version": "1.37.4",
"version": "1.38.0",
"description": "Mock infrastructure for AI application testing — LLM APIs, image generation, image editing, text-to-speech, transcription, audio translation, audio generation, video generation, embeddings, MCP tools, A2A agents, AG-UI event streams, vector databases, search, rerank, and moderation. One package, one port, zero dependencies.",
"license": "MIT",
"keywords": [
Expand Down
13 changes: 10 additions & 3 deletions packages/aimock-pytest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,14 @@ aimock.reset() # alias for reset_fixtures()

```
--aimock-node PATH Path to node binary
--aimock-version VER aimock npm version (default: 1.35.1)
--aimock-version VER aimock npm version (default: 1.38.0)
--aimock-api-key KEY Inbound API key for the aimock child process
```

## API-key validation

Pass `pytest --aimock-api-key test-key` to protect the aimock child. The helper sends this key on all control API calls, and the child receives it through `AIMOCK_API_KEYS`, never through process arguments. Direct client calls must use `Authorization: Bearer test-key`. For direct construction, use `AIMockServer(node_manager, api_key="test-key")`.

## Environment Variables

| Variable | Description |
Expand Down Expand Up @@ -127,8 +132,10 @@ The `test-pytest.yml` workflow:

Tests run across a matrix of Python 3.10--3.13 and Node 20/22.

The `publish-pytest.yml` workflow publishes to PyPI on pushes to `main` when
the version in `pyproject.toml` has not already been published.
The Release workflow publishes `aimock-pytest` to PyPI after its npm publish
job succeeds. Its PyPI job verifies that the `AIMOCK_VERSION` pin exists on
npm before building a wheel, so npm publication completes before the
corresponding `aimock-pytest` release.

## License

Expand Down
2 changes: 1 addition & 1 deletion packages/aimock-pytest/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "aimock-pytest"
version = "0.5.0"
version = "0.5.2"
description = "pytest fixtures for aimock — mock LLM APIs, multimedia, MCP, A2A, AG-UI, vector DBs, and more"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
Loading
Loading