fix: stabilize live probes and docs

This commit is contained in:
Peter Steinberger
2026-01-11 02:24:35 +00:00
parent 6668805aca
commit 20b4e2b859
14 changed files with 149 additions and 89 deletions

View File

@@ -72,7 +72,7 @@ Payload:
- `wakeMode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check.
- `deliver` optional (boolean): If `true`, the agent's response will be sent to the messaging provider. Defaults to `true`. Responses that are only heartbeat acknowledgments are automatically skipped.
- `provider` optional (string): The messaging service for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `msteams`. Defaults to `last`.
- `to` optional (string): The recipient identifier for the provider (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack). Defaults to the last recipient in the main session.
- `to` optional (string): The recipient identifier for the provider (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack, conversation ID for MS Teams). Defaults to the last recipient in the main session.
- `model` optional (string): Model override (e.g., `anthropic/claude-3-5-sonnet` or an alias). Must be in the allowed model list if restricted.
- `thinking` optional (string): Thinking level override (e.g., `low`, `medium`, `high`).
- `timeoutSeconds` optional (number): Maximum duration for the agent run in seconds.

View File

@@ -1,3 +1,11 @@
---
summary: "Broadcast a WhatsApp message to multiple agents"
read_when:
- Configuring broadcast groups
- Debugging multi-agent replies in WhatsApp
status: experimental
---
# Broadcast Groups
**Status:** Experimental
@@ -5,7 +13,9 @@
## Overview
Broadcast Groups enable multiple agents to process and respond to the same message simultaneously. This allows you to create specialized agent teams that work together in a single WhatsApp group, DM, or channel - all using one phone number.
Broadcast Groups enable multiple agents to process and respond to the same message simultaneously. This allows you to create specialized agent teams that work together in a single WhatsApp group or DM — all using one phone number.
Current scope: **WhatsApp only** (web provider).
Broadcast groups are evaluated after provider allowlists and group activation rules. In WhatsApp groups, this means broadcasts happen when Clawdbot would normally reply (for example: on mention, depending on your group settings).
@@ -54,7 +64,9 @@ Agents:
### Basic Setup
Add a top-level `broadcast` section (next to `bindings`):
Add a top-level `broadcast` section (next to `bindings`). Keys are WhatsApp peer ids:
- group chats: group JID (e.g. `120363403215116621@g.us`)
- DMs: E.164 phone number (e.g. `+15551234567`)
```json
{

View File

@@ -39,6 +39,7 @@ Notes:
- `--password <password>`: password override (also sets `CLAWDBOT_GATEWAY_PASSWORD` for the process).
- `--tailscale <off|serve|funnel>`: expose the Gateway via Tailscale.
- `--tailscale-reset-on-exit`: reset Tailscale serve/funnel config on shutdown.
- `--allow-unconfigured`: allow gateway start without `gateway.mode=local` in config.
- `--dev`: create a dev config + workspace if missing (skips BOOTSTRAP.md).
- `--reset`: reset dev config + credentials + sessions + workspace (requires `--dev`).
- `--force`: kill any existing listener on the selected port before starting.

View File

@@ -52,6 +52,7 @@ clawdbot [--dev] [--profile <name>] <command>
providers
list
status
logs
add
remove
login
@@ -74,6 +75,14 @@ clawdbot [--dev] [--profile <name>] <command>
health
status
discover
daemon
status
install
uninstall
start
stop
restart
logs
models
list
status
@@ -83,6 +92,12 @@ clawdbot [--dev] [--profile <name>] <command>
fallbacks list|add|remove|clear
image-fallbacks list|add|remove|clear
scan
auth add|setup-token|paste-token
auth order get|set|clear
sandbox
list
recreate
explain
wake
cron
status
@@ -106,7 +121,8 @@ clawdbot [--dev] [--profile <name>] <command>
run
notify
camera list|snap|clip
canvas snapshot
canvas snapshot|present|hide|navigate|eval
canvas a2ui push|reset
screen record
location get
browser
@@ -180,6 +196,7 @@ Options:
- `--workspace <dir>`
- `--non-interactive`
- `--mode <local|remote>`
- `--flow <quickstart|advanced>`
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|zai-api-key|apiKey|minimax-cloud|minimax-api|minimax|opencode-zen|skip>`
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
- `--token <token>` (non-interactive; used with `--auth-choice token`)
@@ -201,9 +218,12 @@ Options:
- `--tailscale <off|serve|funnel>`
- `--tailscale-reset-on-exit`
- `--install-daemon`
- `--no-install-daemon` (alias: `--skip-daemon`)
- `--daemon-runtime <node|bun>`
- `--skip-providers`
- `--skip-skills`
- `--skip-health`
- `--skip-ui`
- `--node-manager <npm|pnpm|bun>`
- `--json`
@@ -222,19 +242,20 @@ Options:
## Provider helpers
### `providers`
Manage chat provider accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage).
Manage chat provider accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams).
Subcommands:
- `providers list`: show configured chat providers and auth profiles (Claude Code + Codex CLI OAuth sync included).
- `providers status`: check gateway reachability and provider health (`--probe` runs extra checks; use `clawdbot health` or `clawdbot status --deep` for gateway health probes).
- Tip: `providers status` prints warnings with suggested fixes when it can detect common misconfigurations (then points you to `clawdbot doctor`).
- `providers logs`: show recent provider logs from the gateway log file.
- `providers add`: wizard-style setup when no flags are passed; flags switch to non-interactive mode.
- `providers remove`: disable by default; pass `--delete` to remove config entries without prompts.
- `providers login`: interactive provider login (WhatsApp Web only).
- `providers logout`: log out of a provider session (WhatsApp Web only).
Common options:
- `--provider <name>`: `whatsapp|telegram|discord|slack|signal|imessage`
- `--provider <name>`: `whatsapp|telegram|discord|slack|signal|imessage|msteams`
- `--account <id>`: provider account id (default `default`)
- `--name <label>`: display name for the account
@@ -251,6 +272,11 @@ Common options:
- `--no-usage`: skip provider usage/quota snapshots (OAuth/API-backed only).
- `--json`: output JSON (includes usage unless `--no-usage` is set).
`providers logs` options:
- `--provider <name|all>` (default `all`)
- `--lines <n>` (default `200`)
- `--json`
OAuth sync sources:
- Claude Code → `anthropic:claude-cli`
- macOS: Keychain item "Claude Code-credentials" (choose "Always Allow" to avoid launchd prompts)
@@ -336,7 +362,7 @@ Options:
- `--session-id <id>`
- `--thinking <off|minimal|low|medium|high>`
- `--verbose <on|off>`
- `--provider <whatsapp|telegram|discord|slack|signal|imessage>`
- `--provider <whatsapp|telegram|discord|slack|signal|imessage|msteams>`
- `--local`
- `--deliver`
- `--json`
@@ -377,10 +403,12 @@ Show linked session health and recent recipients.
Options:
- `--json`
- `--all` (full diagnosis; read-only, pasteable)
- `--deep` (probe providers)
- `--usage` (show provider usage/quota)
- `--timeout <ms>`
- `--verbose`
- `--debug` (alias for `--verbose`)
### Usage tracking
Clawdbot can surface provider usage/quota when OAuth/API creds are available.
@@ -431,8 +459,11 @@ Options:
- `--reset` (reset dev config + credentials + sessions + workspace)
- `--force` (kill existing listener on port)
- `--verbose`
- `--claude-cli-logs`
- `--ws-log <auto|full|compact>`
- `--compact` (alias for `--ws-log compact`)
- `--raw-stream`
- `--raw-stream-path <path>`
### `daemon`
Manage the Gateway service (launchd/systemd/schtasks).
@@ -476,6 +507,7 @@ Subcommands:
- `gateway call <method> [--params <json>]`
- `gateway health`
- `gateway status`
- `gateway discover`
Common RPCs:
- `config.apply` (validate + write config + restart + wake)
@@ -495,6 +527,10 @@ clawdbot models status
### `models` (root)
`clawdbot models` is an alias for `models status`.
Root options:
- `--status-json` (alias for `models status --json`)
- `--status-plain` (alias for `models status --plain`)
### `models list`
Options:
- `--all`
@@ -545,12 +581,25 @@ Options:
- `--max-candidates <n>`
- `--timeout <ms>`
- `--concurrency <n>`
- `--no-probe`
- `--yes`
- `--no-input`
- `--set-default`
- `--set-image`
- `--json`
### `models auth add|setup-token|paste-token`
Options:
- `add`: interactive auth helper
- `setup-token`: `--provider <name>` (default `anthropic`), `--yes`
- `paste-token`: `--provider <name>`, `--profile-id <id>`, `--expires-in <duration>`
### `models auth order get|set|clear`
Options:
- `get`: `--provider <name>`, `--agent <id>`, `--json`
- `set`: `--provider <name>`, `--agent <id>`, `<profileIds...>`
- `clear`: `--provider <name>`, `--agent <id>`
## Cron + wake
### `wake`

View File

@@ -24,8 +24,8 @@ Provider selection:
Target formats (`--to`):
- WhatsApp: E.164 or group JID
- Telegram: chat id or `@username`
- Discord/Slack: `channel:<id>` or `user:<id>` (raw id is ambiguous for Discord)
- Signal: E.164, `group:<id>`, or `signal:+E.164`
- Discord/Slack: `channel:<id>` or `user:<id>` (raw `channelId` is also accepted)
- Signal: `+E.164`, `group:<id>`, `signal:+E.164`, `signal:group:<id>`, or `username:<name>`/`u:<name>`
- iMessage: handle or `chat_id:<id>`
- MS Teams: conversation id (`19:...@thread.tacv2`) or `conversation:<id>` or `user:<aad-object-id>`
@@ -42,61 +42,79 @@ Target formats (`--to`):
### Core
- `send`
- Providers: WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams
- Required: `--to`, `--message`
- Optional: `--media`, `--reply-to`, `--thread-id`, `--gif-playback`
- Telegram only: `--buttons-json` (requires `"inlineButtons"` in `telegram.capabilities` or `telegram.accounts.<id>.capabilities`)
- Telegram only: `--thread-id` (forum topic id)
- WhatsApp only: `--gif-playback`
- `poll`
- Providers: WhatsApp/Discord/MS Teams
- Required: `--to`, `--poll-question`, `--poll-option` (repeat)
- Optional: `--poll-multi`, `--poll-duration-hours`, `--message`
- Discord only: `--poll-duration-hours`
- `react`
- Providers: Discord/Slack/WhatsApp
- Required: `--to`, `--message-id`
- Optional: `--emoji`, `--remove`, `--participant`, `--from-me`, `--channel-id`
- WhatsApp only: `--participant`, `--from-me`
- `reactions`
- Providers: Discord/Slack
- Required: `--to`, `--message-id`
- Optional: `--limit`, `--channel-id`
- `read`
- Providers: Discord/Slack
- Required: `--to`
- Optional: `--limit`, `--before`, `--after`, `--around`, `--channel-id`
- `edit`
- Providers: Discord/Slack
- Required: `--to`, `--message-id`, `--message`
- Optional: `--channel-id`
- `delete`
- Providers: Discord/Slack
- Required: `--to`, `--message-id`
- Optional: `--channel-id`
- `pin` / `unpin`
- Providers: Discord/Slack
- Required: `--to`, `--message-id`
- Optional: `--channel-id`
- `pins` (list)
- Providers: Discord/Slack
- Required: `--to`
- Optional: `--channel-id`
- `permissions`
- Providers: Discord
- Required: `--to`
- Optional: `--channel-id`
- `search`
- Providers: Discord
- Required: `--guild-id`, `--query`
- Optional: `--channel-id`, `--channel-ids` (repeat), `--author-id`, `--author-ids` (repeat), `--limit`
### Threads
- `thread create`
- Providers: Discord
- Required: `--thread-name`, `--to` (channel id) or `--channel-id`
- Optional: `--message-id`, `--auto-archive-min`
- `thread list`
- Providers: Discord
- Required: `--guild-id`
- Optional: `--channel-id`, `--include-archived`, `--before`, `--limit`
- `thread reply`
- Providers: Discord
- Required: `--to` (thread id), `--message`
- Optional: `--media`, `--reply-to`
@@ -104,18 +122,22 @@ Target formats (`--to`):
- `emoji list`
- Discord: `--guild-id`
- Slack: no extra flags
- `emoji upload`
- Providers: Discord
- Required: `--guild-id`, `--emoji-name`, `--media`
- Optional: `--role-ids` (repeat)
### Stickers
- `sticker send`
- Providers: Discord
- Required: `--to`, `--sticker-id` (repeat)
- Optional: `--message`
- `sticker upload`
- Providers: Discord
- Required: `--guild-id`, `--sticker-name`, `--sticker-desc`, `--sticker-tags`, `--media`
### Roles / Channels / Members / Voice

View File

@@ -107,22 +107,24 @@ clawdbot sandbox recreate --agent alfred
## Configuration
Sandbox settings are in `clawdbot.config.json`:
Sandbox settings live in `~/.clawdbot/clawdbot.json` under `agents.defaults.sandbox` (per-agent overrides go in `agents.list[].sandbox`):
```jsonc
{
"agent": {
"sandbox": {
"mode": "all", // off, non-main, all
"scope": "agent", // session, agent, shared
"docker": {
"image": "clawdbot-sandbox:bookworm-slim",
"containerPrefix": "clawdbot-sbx-"
// ... more Docker options
},
"prune": {
"idleHours": 24, // Auto-prune after 24h idle
"maxAgeDays": 7 // Auto-prune after 7 days
"agents": {
"defaults": {
"sandbox": {
"mode": "all", // off, non-main, all
"scope": "agent", // session, agent, shared
"docker": {
"image": "clawdbot-sandbox:bookworm-slim",
"containerPrefix": "clawdbot-sbx-"
// ... more Docker options
},
"prune": {
"idleHours": 24, // Auto-prune after 24h idle
"maxAgeDays": 7 // Auto-prune after 7 days
}
}
}
}

View File

@@ -2018,7 +2018,7 @@ Mapping notes:
- Templates like `{{messages[0].subject}}` read from the payload.
- `transform` can point to a JS/TS module that returns a hook action.
- `deliver: true` sends the final reply to a provider; `provider` defaults to `last` (falls back to WhatsApp).
- If there is no prior delivery route, set `provider` + `to` explicitly (required for Telegram/Discord/Slack/Signal/iMessage).
- If there is no prior delivery route, set `provider` + `to` explicitly (required for Telegram/Discord/Slack/Signal/iMessage/MS Teams).
- `model` overrides the LLM for this hook run (`provider/model` or alias; must be allowed if `agents.defaults.models` is set).
Gmail helper config (used by `clawdbot hooks gmail setup` / `run`):
@@ -2171,7 +2171,7 @@ Template placeholders are expanded in `tools.audio.transcription.args` (and any
| `{{GroupMembers}}` | Group members preview (best effort) |
| `{{SenderName}}` | Sender display name (best effort) |
| `{{SenderE164}}` | Sender phone number (best effort) |
| `{{Provider}}` | Provider hint (whatsapp|telegram|discord|imessage|webchat|…) |
| `{{Provider}}` | Provider hint (whatsapp|telegram|discord|slack|signal|imessage|msteams|webchat|…) |
## Cron (Gateway scheduler)

View File

@@ -25,4 +25,6 @@ Text is supported everywhere; media and reactions vary by provider.
- Providers can run simultaneously; configure multiple and Clawdbot will route per chat.
- Group behavior varies by provider; see [Groups](/concepts/groups).
- DM pairing and allowlists are enforced for safety; see [Security](/gateway/security).
- Telegram internals: [grammY notes](/providers/grammy).
- Troubleshooting: [Provider troubleshooting](/providers/troubleshooting).
- Model providers are documented separately; see [Model Providers](/providers/models).

View File

@@ -133,7 +133,7 @@ Live tests are split into two layers so we can isolate failures:
- Optional tool-calling stress:
- `CLAWDBOT_LIVE_GATEWAY_TOOL_PROBE=1` enables an extra “bash writes file → read reads it back → echo nonce” check.
- This is specifically meant to catch tool-calling compatibility issues across providers (formatting, history replay, tool_result pairing, etc.).
- Optional image send smoke:
- Optional image send smoke:
- `CLAWDBOT_LIVE_GATEWAY_IMAGE_PROBE=1` sends a real image attachment through the gateway agent pipeline (multimodal message) and asserts the model can read back a per-run code from the image.
- Flow (high level):
- Test generates a tiny PNG with “CAT” + random code (`src/gateway/live-image-probe.ts`)
@@ -142,6 +142,13 @@ Live tests are split into two layers so we can isolate failures:
- Embedded agent forwards a multimodal user message to the model
- Assertion: reply contains `cat` + the code (OCR tolerance: minor mistakes allowed)
Tip: to see what you can test on your machine (and the exact `provider/model` ids), run:
```bash
pnpm clawdbot models list
pnpm clawdbot models list --json
```
## Live: Anthropic setup-token smoke
- Test: `src/agents/anthropic.setup-token.live.test.ts`
@@ -225,7 +232,7 @@ This is the “common models” run we expect to keep working:
- OpenAI (non-Codex): `openai/gpt-5.2` (optional: `openai/gpt-5.1`)
- OpenAI Codex: `openai-codex/gpt-5.2` (optional: `openai-codex/gpt-5.2-codex`)
- Anthropic: `anthropic/claude-opus-4-5` (or `anthropic/claude-sonnet-4-5`)
- Google (Gemini API): `google/gemini-3-pro-preview` and `google/gemini-3-flash-preview`
- Google (Gemini API): `google/gemini-3-pro-preview` and `google/gemini-3-flash-preview` (avoid older Gemini 2.x models)
- Google (Antigravity): `google-antigravity/claude-opus-4-5-thinking` and `google-antigravity/gemini-3-flash`
- Z.AI (GLM): `zai/glm-4.7`
- MiniMax: `minimax/minimax-m2.1`

View File

@@ -37,7 +37,7 @@ clawdbot agent --to +15555550123 --message "Summon reply" --deliver
- `--local`: run locally (requires provider keys in your shell)
- `--deliver`: send the reply to the chosen provider (requires `--to`)
- `--provider`: `whatsapp|telegram|discord|slack|signal|imessage` (default: `whatsapp`)
- `--provider`: `whatsapp|telegram|discord|slack|signal|imessage|msteams` (default: `whatsapp`)
- `--thinking <off|minimal|low|medium|high>`: persist thinking level
- `--verbose <on|off>`: persist verbose level
- `--timeout <seconds>`: override agent timeout

View File

@@ -145,11 +145,11 @@ Notes:
- Uses the image model directly (independent of the main chat model).
### `message`
Send messages and provider actions across Discord/Slack/Telegram/WhatsApp/Signal/iMessage.
Send messages and provider actions across Discord/Slack/Telegram/WhatsApp/Signal/iMessage/MS Teams.
Core actions:
- `send` (text + optional media)
- `poll` (WhatsApp/Discord polls)
- `poll` (WhatsApp/Discord/MS Teams polls)
- `react` / `reactions` / `read` / `edit` / `delete`
- `pin` / `unpin` / `list-pins`
- `permissions`
@@ -166,7 +166,7 @@ Core actions:
Notes:
- `send` routes WhatsApp via the Gateway; other providers go direct.
- `poll` uses the Gateway for WhatsApp and direct Discord API for Discord.
- `poll` uses the Gateway for WhatsApp and MS Teams; Discord polls go direct.
### `cron`
Manage Gateway cron jobs and wakeups.

View File

@@ -18,39 +18,6 @@ diff --git a/dist/providers/openai-codex-responses.js b/dist/providers/openai-co
index 188a8294f26fe1bfe3fb298a7f58e4d8eaf2a529..a3aeb6a7ff53bc4f7f44362adb950b2c55455332 100644
--- a/dist/providers/openai-codex-responses.js
+++ b/dist/providers/openai-codex-responses.js
@@ -433,9 +433,15 @@ function convertMessages(model, context) {
}
else if (msg.role === "assistant") {
const output = [];
+ // OpenAI Responses rejects `reasoning` items that are not followed by a `message`.
+ // Tool-call-only turns (thinking + function_call) are valid assistant turns, but
+ // their stored reasoning items must not be replayed as standalone `reasoning` input.
+ const hasTextBlock = msg.content.some((b) => b.type === "text");
for (const block of msg.content) {
if (block.type === "thinking" && msg.stopReason !== "error") {
if (block.thinkingSignature) {
+ if (!hasTextBlock)
+ continue;
const reasoningItem = JSON.parse(block.thinkingSignature);
output.push(reasoningItem);
}
@@ -470,6 +476,16 @@ function convertMessages(model, context) {
}
if (output.length === 0)
continue;
+ // OpenAI rejects standalone reasoning items when replaying a tool-only turn.
+ // Only submit reasoning items when we also submit an assistant message item.
+ // Repro: pnpm test src/agents/openai-responses.reasoning-replay.test.ts
+ const hasMessage = output.some((item) => item?.type === "message");
+ if (!hasMessage) {
+ for (let i = output.length - 1; i >= 0; i -= 1) {
+ if (output[i]?.type === "reasoning")
+ output.splice(i, 1);
+ }
+ }
messages.push(...output);
}
else if (msg.role === "toolResult") {
@@ -515,7 +531,7 @@ function convertTools(tools) {
name: tool.name,
description: tool.description,
@@ -60,24 +27,3 @@ index 188a8294f26fe1bfe3fb298a7f58e4d8eaf2a529..a3aeb6a7ff53bc4f7f44362adb950b2c
}));
}
function mapStopReason(status) {
diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js
index f07085c64390b211340d6a826b28ea9c2e77302f..523ed38a5a6151d6ff08dd89120315e7aaaf19b6 100644
--- a/dist/providers/openai-responses.js
+++ b/dist/providers/openai-responses.js
@@ -436,6 +436,16 @@ function convertMessages(model, context) {
}
if (output.length === 0)
continue;
+ // OpenAI rejects standalone reasoning items when replaying a tool-only turn.
+ // Only submit reasoning items when we also submit an assistant message item.
+ // Repro: pnpm test src/agents/openai-responses.reasoning-replay.test.ts
+ const hasMessage = output.some((item) => item?.type === "message");
+ if (!hasMessage) {
+ for (let i = output.length - 1; i >= 0; i -= 1) {
+ if (output[i]?.type === "reasoning")
+ output.splice(i, 1);
+ }
+ }
messages.push(...output);
}
else if (msg.role === "toolResult") {

View File

@@ -48,6 +48,16 @@ function isGoogleModelNotFoundError(err: unknown): boolean {
return false;
}
function isModelNotFoundErrorMessage(raw: string): boolean {
const msg = raw.trim();
if (!msg) return false;
if (/\b404\b/.test(msg) && /not[_-]?found/i.test(msg)) return true;
if (/not_found_error/i.test(msg)) return true;
if (/model:\s*[a-z0-9._-]+/i.test(msg) && /not[_-]?found/i.test(msg))
return true;
return false;
}
describeLive("live models (profile keys)", () => {
it(
"completes across configured models",
@@ -187,6 +197,15 @@ describeLive("live models (profile keys)", () => {
},
);
if (res.stopReason === "error") {
const msg = res.errorMessage ?? "";
if (ALL_MODELS && isModelNotFoundErrorMessage(msg)) {
skipped.push({ model: id, reason: msg });
continue;
}
throw new Error(msg || "model returned error with no message");
}
const text = res.content
.filter((block) => block.type === "text")
.map((block) => block.text.trim())

View File

@@ -52,7 +52,7 @@ function installFailingFetchCapture() {
}
describe("openai-responses reasoning replay", () => {
it("handles tool-call-only turns without requiring reasoning replay", async () => {
it("replays reasoning for tool-call-only turns (required by OpenAI)", async () => {
const cap = installFailingFetchCapture();
try {
const model = buildModel();
@@ -141,11 +141,11 @@ describe("openai-responses reasoning replay", () => {
)
.filter((t): t is string => typeof t === "string");
expect(types).toContain("reasoning");
expect(types).toContain("function_call");
const reasoningIndex = types.indexOf("reasoning");
if (reasoningIndex !== -1) {
expect(reasoningIndex).toBeLessThan(types.indexOf("function_call"));
}
expect(types.indexOf("reasoning")).toBeLessThan(
types.indexOf("function_call"),
);
} finally {
cap.restore();
}