@crvouga/mockingbird-service-openai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/DISCOVERY.md +55 -0
- package/README.md +193 -0
- package/SUPPORT.md +26 -0
- package/dist/chunk-I6E3KEW7.js +818 -0
- package/dist/chunk-I6E3KEW7.js.map +7 -0
- package/dist/chunk-JAE7QQJM.js +4098 -0
- package/dist/chunk-JAE7QQJM.js.map +7 -0
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +1134 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +7 -0
- package/dist/server.d.ts +1571 -0
- package/dist/server.js +12 -0
- package/dist/server.js.map +7 -0
- package/openapi.yaml +3656 -0
- package/package.json +131 -0
package/CHANGELOG.md
ADDED
package/DISCOVERY.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @crvouga/mockingbird-service-openai discovery
|
|
2
|
+
|
|
3
|
+
This is the installed-package index for coding agents and tooling. All relative links resolve
|
|
4
|
+
inside `node_modules/@crvouga/mockingbird-service-openai/`; no repository checkout is needed to discover the mock's
|
|
5
|
+
supported surface or documented behavior.
|
|
6
|
+
|
|
7
|
+
## Capability and behavior sources
|
|
8
|
+
|
|
9
|
+
| Question | Authoritative file | What it contains |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Behaviour and integration | [`README.md`](README.md) | Routes, state transitions, auth, webhooks, controls, presets and deliberate omissions. |
|
|
12
|
+
| Exact capabilities | [`SUPPORT.md`](SUPPORT.md) | Supported, unsupported and parity-covered operations or commands, including reasons for gaps. |
|
|
13
|
+
| Wire contract | [`openapi.yaml`](openapi.yaml) | Machine-readable paths, methods, schemas, responses and parity annotations. |
|
|
14
|
+
| Public API | [`dist/index.d.ts`](dist/index.d.ts) | The installed package's exact TypeScript exports and signatures. |
|
|
15
|
+
| Package metadata | [`package.json`](package.json) | Runtime/entry-point claims, vendor links, parity scope/tier and `mockingbird.discovery`. |
|
|
16
|
+
|
|
17
|
+
Read these together: the contract/capability matrix says *what* is available, while the README
|
|
18
|
+
defines stateful behavior, lifecycle rules, test controls, and intentional oracle differences.
|
|
19
|
+
If prose and an executable surface disagree, report a parity mismatch instead of adding a
|
|
20
|
+
consumer-side workaround.
|
|
21
|
+
|
|
22
|
+
## Parity and oracle
|
|
23
|
+
|
|
24
|
+
- Declared parity surface: **Scripted chat/tool calls, SSE, embeddings, models, files and uploads**.
|
|
25
|
+
- Parity tier: **cold** (the repository controls when live checks run).
|
|
26
|
+
- Oracle: **Official documentation and local SDK oracle**.
|
|
27
|
+
- Repository command: `bun run parity:service -- openai`.
|
|
28
|
+
- Evidence model: Live billed inference is disabled by repository policy. The package parity runner exits 2; tests verify official SDKs locally.
|
|
29
|
+
|
|
30
|
+
The npm package contains evidence summaries and the exact contract, not credentials or the
|
|
31
|
+
repository-only parity harness. Self-parity/property and acceptance tests run in the Mockingbird
|
|
32
|
+
repository; live parity is an additional oracle check, not a substitute for the packaged matrix.
|
|
33
|
+
|
|
34
|
+
## Runtime introspection
|
|
35
|
+
|
|
36
|
+
- `GET /__admin/health`
|
|
37
|
+
- `GET /__admin`
|
|
38
|
+
- `GET /__admin/state`
|
|
39
|
+
- `GET /__admin/requests`
|
|
40
|
+
- `GET /__admin/metrics`
|
|
41
|
+
- `GET /__admin/faults/presets`
|
|
42
|
+
- `GET /__admin/ui`
|
|
43
|
+
|
|
44
|
+
For HTTP services, use `x-mockingbird-namespace` (or the documented credential/path carrier) so
|
|
45
|
+
parallel tests do not share state. Admin state, journal, metrics and fault-preset endpoints are
|
|
46
|
+
designed for assertions and diagnosis by consuming test suites.
|
|
47
|
+
|
|
48
|
+
## Report a mismatch or missing capability
|
|
49
|
+
|
|
50
|
+
Follow the [agent reporting contract](https://github.com/crvouga/mockingbird/blob/main/docs/REPORTING_ISSUES.md). Include package version,
|
|
51
|
+
operation/command, a minimal redacted request, actual mock result, expected oracle result or vendor
|
|
52
|
+
documentation, and whether the mismatch appears in the matrix. Never include keys, tokens,
|
|
53
|
+
customer data, prompts, PHI, card data, or unredacted recordings.
|
|
54
|
+
|
|
55
|
+
Service key: `openai`.
|
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# @crvouga/mockingbird-service-openai
|
|
2
|
+
|
|
3
|
+
> Familiar calls. Faithful echoes. Part of [Mockingbird](https://github.com/crvouga/mockingbird).
|
|
4
|
+
|
|
5
|
+
A **wip**, portable, entirely local OpenAI mock for Chat Completions, function tool loops,
|
|
6
|
+
Chat SSE streams, embeddings, models, files and staged uploads. Verified with unmodified
|
|
7
|
+
**openai 7.27.0**, **@ai-sdk/openai 4.0.83** and **ai 7.0.127** over Node HTTP.
|
|
8
|
+
It makes no inference calls and requires no vendor account or billed API key.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
bun add @crvouga/mockingbird-service-openai
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { createRuntime, DEFAULT_TOKEN } from "@crvouga/mockingbird-service-openai"
|
|
20
|
+
|
|
21
|
+
const mock = createRuntime({
|
|
22
|
+
scripts: [{
|
|
23
|
+
kind: "chat",
|
|
24
|
+
message: { role: "assistant", content: "Fixture answer" },
|
|
25
|
+
usage: { prompt_tokens: 12, completion_tokens: 3, cached_tokens: 8 },
|
|
26
|
+
}],
|
|
27
|
+
})
|
|
28
|
+
const response = await mock.fetch(new Request("http://mock.local/v1/chat/completions", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { authorization: `Bearer ${DEFAULT_TOKEN}`, "content-type": "application/json" },
|
|
31
|
+
body: JSON.stringify({ model: "fixture-chat", messages: [{ role: "user", content: "Fixture question" }] }),
|
|
32
|
+
}))
|
|
33
|
+
console.log(await response.json())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For this optional SDK example, also install `@ai-sdk/openai@4.0.83` and `ai@7.0.127`.
|
|
37
|
+
Use the explicit `.chat()` provider: the provider's default model uses the Responses API.
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { createOpenAI } from "@ai-sdk/openai"
|
|
41
|
+
import { generateText } from "ai"
|
|
42
|
+
import { createServer } from "@crvouga/mockingbird-service-openai/server"
|
|
43
|
+
import { DEFAULT_TOKEN } from "@crvouga/mockingbird-service-openai"
|
|
44
|
+
|
|
45
|
+
const server = await createServer({
|
|
46
|
+
scripts: [{ kind: "chat", message: { role: "assistant", content: "Fixture answer" } }],
|
|
47
|
+
})
|
|
48
|
+
try {
|
|
49
|
+
const provider = createOpenAI({ baseURL: `${server.url}/v1`, apiKey: DEFAULT_TOKEN })
|
|
50
|
+
console.log((await generateText({ model: provider.chat("fixture-chat"), prompt: "Fixture", maxRetries: 0 })).text)
|
|
51
|
+
} finally {
|
|
52
|
+
await server.close()
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`openai` also accepts `baseURL: server.url + "/v1"` and the fixture `apiKey`.
|
|
57
|
+
The CLI is `mockingbird-openai serve --port 8813`. `createServer()` defaults to an ephemeral port.
|
|
58
|
+
|
|
59
|
+
## Routes and persisted behavior
|
|
60
|
+
|
|
61
|
+
| Endpoint | Behavior |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `POST /v1/chat/completions` | Scripted assistant JSON or Chat SSE; tools, finish reasons and exact usage |
|
|
64
|
+
| `GET /v1/chat/completions` | Stored completions only, model filter, `after`, `limit`, `order` |
|
|
65
|
+
| `GET` / `DELETE /v1/chat/completions/{completion_id}` | Retrieve/delete a `store: true` completion |
|
|
66
|
+
| `POST /v1/embeddings` | Exact scripted vectors, string or token inputs, float/base64 encoding and dimensions |
|
|
67
|
+
| `GET /v1/models` / `GET /v1/models/{model}` | Configured synthetic model fixtures |
|
|
68
|
+
| `POST` / `GET /v1/files` | Multipart file creation; metadata listing with purpose, order, after and limit |
|
|
69
|
+
| `GET` / `DELETE /v1/files/{file_id}` | Metadata read and deletion |
|
|
70
|
+
| `GET /v1/files/{file_id}/content` | Exact binary bytes |
|
|
71
|
+
| `POST /v1/uploads` | Create a pending staged Upload, expires after one hour |
|
|
72
|
+
| `POST /v1/uploads/{upload_id}/parts` | Multipart `data`, pending parts remain unpublished |
|
|
73
|
+
| `POST /v1/uploads/{upload_id}/complete` | Ordered `part_ids`; exact declared byte count and optional MD5 check |
|
|
74
|
+
| `POST /v1/uploads/{upload_id}/cancel` | Cancel a pending Upload |
|
|
75
|
+
|
|
76
|
+
`DEFAULT_TOKEN` is `fixture-openai-token`; `tokens: [{ token, models?: string[] }]` configures
|
|
77
|
+
synthetic bearer credentials and optional model access. Invalid/missing credentials return 401,
|
|
78
|
+
model-scope denial 403, unknown model/resource 404, malformed requests 400, with
|
|
79
|
+
`{ error: { message, type, param, code } }` and `x-request-id`. Default models are `fixture-chat`
|
|
80
|
+
and `fixture-embedding` (three dimensions). `models` accepts `{ id, kind: "chat" | "embedding",
|
|
81
|
+
dimensions?, created?, owned_by? }`. No model name implies real inference or vendor availability.
|
|
82
|
+
|
|
83
|
+
Script selection consumes the first unconsumed script matching its kind and optional
|
|
84
|
+
`match: { model?, lastRole?, contentIncludes?, toolCallId? }`. Unmatched scripts stay queued.
|
|
85
|
+
`ChatScript` has `kind: "chat"`, `message: { role: "assistant", content: string | null,
|
|
86
|
+
tool_calls?, refusal? }`, optional `finish_reason`, `usage`, `chunk_size` and `stream_delay_ms`.
|
|
87
|
+
Function calls use `{ id?, type: "function", function: { name, arguments: string } }`;
|
|
88
|
+
arguments must be valid JSON **strings**. Missing IDs are generated once and persist. Assistant
|
|
89
|
+
null content is preserved with tools/refusal; a `role: "tool"` reply must refer to an earlier
|
|
90
|
+
assistant call ID. Finish reasons are `stop`, `tool_calls`, `length`, `content_filter`.
|
|
91
|
+
`EmbeddingScript` has `kind: "embedding"`, `vectors: number[][]`, optional match and usage.
|
|
92
|
+
|
|
93
|
+
Exact usage is `{ prompt_tokens, completion_tokens?, cached_tokens? }` (nonnegative integers,
|
|
94
|
+
cached at most prompt). Chat replies include total tokens and `prompt_tokens_details.cached_tokens`.
|
|
95
|
+
Without a script, the local default is `Fixture response`; embeddings use deterministic hash
|
|
96
|
+
vectors. Default token estimates count UTF-8 bytes divided by four, rounded up, with a minimum
|
|
97
|
+
of one. These defaults are synthetic policies; supply usage/vectors for precise consumer tests.
|
|
98
|
+
They do not approximate semantic inference or claim tokenizer parity.
|
|
99
|
+
|
|
100
|
+
SSE emits `chat.completion.chunk` JSON with indexed content/tool argument deltas, a final
|
|
101
|
+
finish reason, and `[DONE]`. `stream_options.include_usage: true` adds a final empty-choices
|
|
102
|
+
usage chunk; earlier chunks carry null usage. `chunk_size` counts Unicode characters;
|
|
103
|
+
`stream_delay_ms` delays frame delivery for cancellation tests. Aborting/canceling closes the
|
|
104
|
+
stream and clears its pending timer; a begun completion remains a created resource.
|
|
105
|
+
|
|
106
|
+
Only `store: true` completions are available through vendor reads. Chat ordering defaults to
|
|
107
|
+
ascending creation time. Its local page bounds are default 20, maximum 100. File ordering defaults locally to descending
|
|
108
|
+
creation time, limit/max 10,000; callers can set order explicitly. Stable insertion order breaks
|
|
109
|
+
creation-time ties. `after` is the last resource ID; all pages expose `first_id`, `last_id`,
|
|
110
|
+
`has_more`. Repeating `prompt_cache_key`, headers or request IDs does not deduplicate creations:
|
|
111
|
+
the documented Chat contract does not promise mutation idempotency. Upload completion only
|
|
112
|
+
accepts pending uploads; completed/cancelled/expired uploads reject further mutation.
|
|
113
|
+
|
|
114
|
+
File multipart bodies use `purpose` and `file`. Supported purposes are `assistants`, `batch`,
|
|
115
|
+
`fine-tune`, `vision`, `user_data`; staged Uploads use the first four. Batch files expire after
|
|
116
|
+
30 days. Optional `expires_after[anchor]=created_at` and `expires_after[seconds]` accept
|
|
117
|
+
3,600–2,592,000 seconds. Staged Upload completion creates the file atomically in caller part
|
|
118
|
+
order, verifies byte count/checksum, and never publishes partial data. `maxFileBytes` defaults
|
|
119
|
+
to 20 MiB and bounds files/uploads/parts locally; it is not a production quota.
|
|
120
|
+
|
|
121
|
+
All records use SQLite Collections and IdSequence; lifecycle timestamps/expiry use the shared
|
|
122
|
+
clock. Each namespace receives constructor `scripts`, `models`, `files` fixtures; reset restores
|
|
123
|
+
them. Files accept `{ id?, filename, purpose, bytes: string | number[], expires_at? }`.
|
|
124
|
+
Use `x-mockingbird-namespace`, `/__admin/ns/{namespace}/v1/...`, or shared credential registration
|
|
125
|
+
for SDKs. Timeline branches/checkpoints reuse the shared runtime and keep their state isolated.
|
|
126
|
+
|
|
127
|
+
## Controls, failures and privacy
|
|
128
|
+
|
|
129
|
+
`DEFAULT_ADMIN_KEY` is `fixture-openai-admin`. Admin data routes require
|
|
130
|
+
`x-mockingbird-admin-key`; configure a nonempty `adminKey` to replace it. A custom `adminPrefix`
|
|
131
|
+
relocates all controls and the namespace path carrier. Shared clock, fault, reset, journal,
|
|
132
|
+
state, credential, metrics and Timeline routes remain available.
|
|
133
|
+
|
|
134
|
+
| Control | Purpose |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `POST /__admin/scripts` | Strictly validate and queue one exact Chat/embedding script |
|
|
137
|
+
| `GET /__admin/scripts` | IDs, kinds and consumption state only |
|
|
138
|
+
| `POST /__admin/raw-faults` | Explicit raw `{ body: string, status?, content_type?, model? }`; allows malformed vendor data |
|
|
139
|
+
| `GET /__admin/request-metadata` | Model, counts/roles, byte length, hashes and response/script IDs |
|
|
140
|
+
| `POST /__admin/request-assertions` | `{ fingerprints: string[] }`, compare ordered SHA-256 request hashes |
|
|
141
|
+
| `GET /__admin/stream-metadata` | Open/closed/aborted stream status and chunk counts |
|
|
142
|
+
| `GET /__admin/upload-metadata` | Pending/completed/cancelled/expired Upload metadata |
|
|
143
|
+
|
|
144
|
+
Normal scripts reject unknown fields and malformed response shapes atomically. Raw-fault controls
|
|
145
|
+
are the sole escape hatch. `fingerprint(body)` computes the exact JSON-serialization SHA-256 hash
|
|
146
|
+
for request assertions; property order matters. No incoming prompts/tool results are retained.
|
|
147
|
+
Scripts, replies, file/part bytes and raw fault bodies are AES-GCM encrypted in SQLite with a
|
|
148
|
+
private runtime key and logical namespace binding. Generic state snapshots show ciphertext and
|
|
149
|
+
metadata, never plaintext bodies or keys. Vendor resource reads decrypt the requested output.
|
|
150
|
+
The shared request journal/logs contain metadata only; API keys are never logged.
|
|
151
|
+
|
|
152
|
+
Presets fire once: `rate_limited` (429, Retry-After and retry-after-ms), `server_error` (500),
|
|
153
|
+
`service_unavailable` (503), `network_reset`, `invalid_json`, `malformed_stream`, `content_refusal`,
|
|
154
|
+
`slow_response` (50 ms before dispatch). Canned errors/drop do not consume scripts.
|
|
155
|
+
The reported raw client retries only network errors, 429 and 5xx. Official OpenAI SDK additionally
|
|
156
|
+
retries 408/409 by its own policy; configure `maxRetries` when asserting attempts. Transport
|
|
157
|
+
latency uses wall time, while resource lifecycle uses the mock clock.
|
|
158
|
+
|
|
159
|
+
## API
|
|
160
|
+
|
|
161
|
+
Root exports `OpenAIAPI`, `createRuntime`, `document`, `supportedOperationIds`, `DEFAULT_MODELS`,
|
|
162
|
+
`DEFAULT_TOKEN`, `DEFAULT_ADMIN_KEY`, `OPENAI_NAMESPACE`, `OPENAI_PRESETS`, `fingerprint`,
|
|
163
|
+
`validateScript`, `createVaultKey`, `vendorError`, and exported fixture/script/options/runtime types.
|
|
164
|
+
The API exposes `fetch`, `reset`, `ensureSeeded`, `queue`, `queueRaw`, `seedFile`, `state`, `app`,
|
|
165
|
+
`sqlite`. Node `./server` exports `createServer`, `DEFAULT_PORT`, `serveTarget` and server types.
|
|
166
|
+
Optional `vaultKey` is caller-supplied private key material, never persisted in state.
|
|
167
|
+
|
|
168
|
+
## Oracle and verification
|
|
169
|
+
|
|
170
|
+
The contract follows official [Chat creation](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create),
|
|
171
|
+
[function calling](https://developers.openai.com/api/docs/guides/function-calling),
|
|
172
|
+
[stored Chat listing](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/list),
|
|
173
|
+
[embeddings](https://developers.openai.com/api/reference/resources/embeddings/methods/create),
|
|
174
|
+
[models](https://developers.openai.com/api/reference/resources/models/methods/list),
|
|
175
|
+
[files](https://developers.openai.com/api/reference/resources/files/methods/create),
|
|
176
|
+
[file listing](https://developers.openai.com/api/reference/resources/files/methods/list),
|
|
177
|
+
[Uploads](https://developers.openai.com/api/reference/resources/uploads/methods/create),
|
|
178
|
+
[completion](https://developers.openai.com/api/reference/resources/uploads/methods/complete), and
|
|
179
|
+
[errors](https://developers.openai.com/api/docs/guides/error-codes).
|
|
180
|
+
|
|
181
|
+
`bun test` verifies unmodified SDKs over Node HTTP, the reported nonstream tool loop with exact
|
|
182
|
+
retry counts, SSE/abort handling, exact bytes and staged atomicity, namespaces/reset/Timeline,
|
|
183
|
+
privacy, pagination, every preset, every enabled JSON operation via independent-instance walks,
|
|
184
|
+
and a deliberately divergent instance. Multipart/binary operations have served acceptance tests.
|
|
185
|
+
`bun run parity` exits 2: repository policy prohibits billed inference. No live OpenAI result is
|
|
186
|
+
claimed; official documentation and local SDK execution are the oracle evidence.
|
|
187
|
+
|
|
188
|
+
## Deliberately not modelled
|
|
189
|
+
|
|
190
|
+
Responses API, realtime/audio/image/batch/fine-tuning APIs, built-in hosted tools, multimodal
|
|
191
|
+
content interpretation, real inference/tokenization, account billing, production quotas,
|
|
192
|
+
production opaque IDs/cursor encodings and undocumented request-ID deduplication. This package
|
|
193
|
+
never calls OpenAI. Encrypted persistence is private runtime state, not a production key vault.
|
package/SUPPORT.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Local scripted OpenAI API — operation support
|
|
2
|
+
|
|
3
|
+
Generated from `openapi.yaml`; do not edit by hand.
|
|
4
|
+
|
|
5
|
+
- operations in spec: **16**
|
|
6
|
+
- supported by the mock: **16**
|
|
7
|
+
- parity enabled: **13**
|
|
8
|
+
|
|
9
|
+
| operationId | route | mock | parity | notes |
|
|
10
|
+
| --- | --- | --- | --- | --- |
|
|
11
|
+
| `ListChatCompletions` | `GET /v1/chat/completions` | ✅ supported | ✅ | |
|
|
12
|
+
| `CreateChatCompletion` | `POST /v1/chat/completions` | ✅ supported | ⚠️ unsafe (opt-in) | |
|
|
13
|
+
| `GetChatCompletion` | `GET /v1/chat/completions/{completion_id}` | ✅ supported | ✅ | |
|
|
14
|
+
| `DeleteChatCompletion` | `DELETE /v1/chat/completions/{completion_id}` | ✅ supported | ⚠️ unsafe (opt-in) | |
|
|
15
|
+
| `CreateEmbeddings` | `POST /v1/embeddings` | ✅ supported | ⚠️ unsafe (opt-in) | |
|
|
16
|
+
| `ListModels` | `GET /v1/models` | ✅ supported | ✅ | |
|
|
17
|
+
| `GetModel` | `GET /v1/models/{model}` | ✅ supported | ✅ | |
|
|
18
|
+
| `ListFiles` | `GET /v1/files` | ✅ supported | ✅ | |
|
|
19
|
+
| `CreateFile` | `POST /v1/files` | ✅ supported | ❌ disabled | Multipart/binary byte shapes are verified by served SDK and acceptance tests. |
|
|
20
|
+
| `GetFile` | `GET /v1/files/{file_id}` | ✅ supported | ✅ | |
|
|
21
|
+
| `DeleteFile` | `DELETE /v1/files/{file_id}` | ✅ supported | ⚠️ unsafe (opt-in) | |
|
|
22
|
+
| `DownloadFile` | `GET /v1/files/{file_id}/content` | ✅ supported | ❌ disabled | Multipart/binary byte shapes are verified by served SDK and acceptance tests. |
|
|
23
|
+
| `CreateUpload` | `POST /v1/uploads` | ✅ supported | ⚠️ unsafe (opt-in) | |
|
|
24
|
+
| `CreateUploadPart` | `POST /v1/uploads/{upload_id}/parts` | ✅ supported | ❌ disabled | Multipart/binary byte shapes are verified by served SDK and acceptance tests. |
|
|
25
|
+
| `CompleteUpload` | `POST /v1/uploads/{upload_id}/complete` | ✅ supported | ⚠️ unsafe (opt-in) | |
|
|
26
|
+
| `CancelUpload` | `POST /v1/uploads/{upload_id}/cancel` | ✅ supported | ⚠️ unsafe (opt-in) | |
|