Compare commits

..
46 changed files with 2044 additions and 600 deletions

No files matched your search

+8
View File
@@ -1,5 +1,12 @@
# Changelog # Changelog
## 2026.1.13-2
### Changes
- Dependencies: bump Pi packages to 0.45.3 and refresh patched pi-ai.
- Testing: update Vitest + browser-playwright to 4.0.17.
- Docs: add Amazon Bedrock provider notes and link from models/FAQ.
## 2026.1.13-1 ## 2026.1.13-1
### Changes ### Changes
@@ -8,6 +15,7 @@
- Agents: add compaction mode config with optional safeguard summarization for long histories. (#700 — thanks @thewilloftheshadow) - Agents: add compaction mode config with optional safeguard summarization for long histories. (#700 — thanks @thewilloftheshadow)
- Agents: support per-agent model fallbacks via `agents.list[].model`. (#583 — thanks @mitschabaude-bot) - Agents: support per-agent model fallbacks via `agents.list[].model`. (#583 — thanks @mitschabaude-bot)
- Tools: add tool profiles plus group shorthands for tool policy allow/deny (global, per-agent, sandbox). - Tools: add tool profiles plus group shorthands for tool policy allow/deny (global, per-agent, sandbox).
- Thinking: allow xhigh for GPT-5.2 + Codex models and downgrade on unsupported switches. (#444 — thanks @grp06)
### Fixes ### Fixes
- Gateway: honor `CLAWDBOT_LAUNCHD_LABEL` / `CLAWDBOT_SYSTEMD_UNIT` overrides when checking or restarting the daemon. - Gateway: honor `CLAWDBOT_LAUNCHD_LABEL` / `CLAWDBOT_SYSTEMD_UNIT` overrides when checking or restarting the daemon.
+1 -1
View File
@@ -247,7 +247,7 @@ Send these in WhatsApp/Telegram/Slack/Microsoft Teams/WebChat (group commands ar
- `/status` — compact session status (model + tokens, cost when available) - `/status` — compact session status (model + tokens, cost when available)
- `/new` or `/reset` — reset the session - `/new` or `/reset` — reset the session
- `/compact` — compact session context (summary) - `/compact` — compact session context (summary)
- `/think <level>` — off|minimal|low|medium|high - `/think <level>` — off|minimal|low|medium|high|xhigh (GPT-5.2 + Codex models only)
- `/verbose on|off` - `/verbose on|off`
- `/cost on|off` — append per-response token/cost usage lines - `/cost on|off` — append per-response token/cost usage lines
- `/restart` — restart the gateway (owner-only in groups) - `/restart` — restart the gateway (owner-only in groups)
+1 -1
View File
@@ -105,7 +105,7 @@ Isolation options (only for `session=isolated`):
### Model and thinking overrides ### Model and thinking overrides
Isolated jobs (`agentTurn`) can override the model and thinking level: Isolated jobs (`agentTurn`) can override the model and thinking level:
- `model`: Provider/model string (e.g., `anthropic/claude-sonnet-4-20250514`) or alias (e.g., `opus`) - `model`: Provider/model string (e.g., `anthropic/claude-sonnet-4-20250514`) or alias (e.g., `opus`)
- `thinking`: Thinking level (`off`, `minimal`, `low`, `medium`, `high`) - `thinking`: Thinking level (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`; GPT-5.2 + Codex models only)
Note: You can set `model` on main-session jobs too, but it changes the shared main Note: You can set `model` on main-session jobs too, but it changes the shared main
session model. We recommend model overrides only for isolated jobs to avoid session model. We recommend model overrides only for isolated jobs to avoid
+71
View File
@@ -0,0 +1,71 @@
---
summary: "Use Amazon Bedrock (Converse API) models with Clawdbot"
read_when:
- You want to use Amazon Bedrock models with Clawdbot
- You need AWS credential/region setup for model calls
---
# Amazon Bedrock
Clawdbot can use **Amazon Bedrock** models via pi‑ai’s **Bedrock Converse**
streaming provider. Bedrock auth uses the **AWS SDK default credential chain**,
not an API key.
## What pi‑ai supports
- Provider: `amazon-bedrock`
- API: `bedrock-converse-stream`
- Auth: AWS credentials (env vars, shared config, or instance role)
- Region: `AWS_REGION` or `AWS_DEFAULT_REGION` (default: `us-east-1`)
## Setup (manual)
1) Ensure AWS credentials are available on the **gateway host**:
```bash
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_REGION="us-east-1"
# Optional:
export AWS_SESSION_TOKEN="..."
export AWS_PROFILE="your-profile"
```
2) Add a Bedrock provider and model to your config:
```json5
{
models: {
providers: {
"amazon-bedrock": {
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
api: "bedrock-converse-stream",
models: [
{
id: "anthropic.claude-3-7-sonnet-20250219-v1:0",
name: "Claude 3.7 Sonnet (Bedrock)",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192
}
]
}
}
},
agents: {
defaults: {
model: { primary: "amazon-bedrock/anthropic.claude-3-7-sonnet-20250219-v1:0" }
}
}
}
```
## Notes
- Bedrock requires **model access** enabled in your AWS account/region.
- If you use profiles, set `AWS_PROFILE` on the gateway host.
- Reasoning support depends on the model; check the Bedrock model card for
current capabilities.
- If you prefer a managed key flow, you can also place an OpenAI‑compatible
proxy in front of Bedrock and configure it as an OpenAI provider instead.
+1 -1
View File
@@ -398,7 +398,7 @@ Required:
Options: Options:
- `--to <dest>` (for session key and optional delivery) - `--to <dest>` (for session key and optional delivery)
- `--session-id <id>` - `--session-id <id>`
- `--thinking <off|minimal|low|medium|high>` - `--thinking <off|minimal|low|medium|high|xhigh>` (GPT-5.2 + Codex models only)
- `--verbose <on|off>` - `--verbose <on|off>`
- `--provider <whatsapp|telegram|discord|slack|signal|imessage>` - `--provider <whatsapp|telegram|discord|slack|signal|imessage>`
- `--local` - `--local`
@@ -39,7 +39,7 @@ Add `hooks.gmail.model` config option to specify an optional cheaper model for G
| Field | Type | Default | Description | | Field | Type | Default | Description |
|-------|------|---------|-------------| |-------|------|---------|-------------|
| `hooks.gmail.model` | `string` | (none) | Model to use for Gmail hook processing. Accepts `provider/model` refs or aliases from `agents.defaults.models`. | | `hooks.gmail.model` | `string` | (none) | Model to use for Gmail hook processing. Accepts `provider/model` refs or aliases from `agents.defaults.models`. |
| `hooks.gmail.thinking` | `string` | (inherited) | Thinking level override (`off`, `minimal`, `low`, `medium`, `high`). If unset, inherits from `agents.defaults.thinkingDefault` or model's default. | | `hooks.gmail.thinking` | `string` | (inherited) | Thinking level override (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`; GPT-5.2 + Codex models only). If unset, inherits from `agents.defaults.thinkingDefault` or model's default. |
### Alias Support ### Alias Support
+1
View File
@@ -31,6 +31,7 @@ model as `provider/model`.
- [Z.AI](/providers/zai) - [Z.AI](/providers/zai)
- [GLM models](/providers/glm) - [GLM models](/providers/glm)
- [MiniMax](/providers/minimax) - [MiniMax](/providers/minimax)
- [Amazon Bedrock](/bedrock)
For the full provider catalog (xAI, Groq, Mistral, etc.) and advanced configuration, For the full provider catalog (xAI, Groq, Mistral, etc.) and advanced configuration,
see [Model providers](/concepts/model-providers). see [Model providers](/concepts/model-providers).
+85 -1
View File
@@ -5,6 +5,90 @@ summary: "Frequently asked questions about Clawdbot setup, configuration, and us
Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, multi-agent, OAuth/API keys, model failover). For runtime diagnostics, see [Troubleshooting](/gateway/troubleshooting). For the full config reference, see [Configuration](/gateway/configuration). Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, multi-agent, OAuth/API keys, model failover). For runtime diagnostics, see [Troubleshooting](/gateway/troubleshooting). For the full config reference, see [Configuration](/gateway/configuration).
## Table of contents
- [What is Clawdbot, in one paragraph?](#what-is-clawdbot-in-one-paragraph)
- [What’s the recommended way to install and set up Clawdbot?](#whats-the-recommended-way-to-install-and-set-up-clawdbot)
- [How do I open the dashboard after onboarding?](#how-do-i-open-the-dashboard-after-onboarding)
- [How do I authenticate the dashboard (token) on localhost vs remote?](#how-do-i-authenticate-the-dashboard-token-on-localhost-vs-remote)
- [What runtime do I need?](#what-runtime-do-i-need)
- [What does the onboarding wizard actually do?](#what-does-the-onboarding-wizard-actually-do)
- [How does Anthropic "setup-token" auth work?](#how-does-anthropic-setup-token-auth-work)
- [Do you support Claude subscription auth (Claude Code OAuth)?](#do-you-support-claude-subscription-auth-claude-code-oauth)
- [Is AWS Bedrock supported?](#is-aws-bedrock-supported)
- [How does Codex auth work?](#how-does-codex-auth-work)
- [Is a local model OK for casual chats?](#is-a-local-model-ok-for-casual-chats)
- [How do I keep hosted model traffic in a specific region?](#how-do-i-keep-hosted-model-traffic-in-a-specific-region)
- [Can I use Bun?](#can-i-use-bun)
- [Telegram: what goes in `allowFrom`?](#telegram-what-goes-in-allowfrom)
- [Can multiple people use one WhatsApp number with different Clawdbots?](#can-multiple-people-use-one-whatsapp-number-with-different-clawdbots)
- [Can I run a "fast chat" agent and an "Opus for coding" agent?](#can-i-run-a-fast-chat-agent-and-an-opus-for-coding-agent)
- [Does Homebrew work on Linux?](#does-homebrew-work-on-linux)
- [Can I switch between npm and git installs later?](#can-i-switch-between-npm-and-git-installs-later)
- [How do I customize skills without keeping the repo dirty?](#how-do-i-customize-skills-without-keeping-the-repo-dirty)
- [Can I load skills from a custom folder?](#can-i-load-skills-from-a-custom-folder)
- [How can I use different models for different tasks?](#how-can-i-use-different-models-for-different-tasks)
- [How do I install skills on Linux?](#how-do-i-install-skills-on-linux)
- [Is there a dedicated sandboxing doc?](#is-there-a-dedicated-sandboxing-doc)
- [How do I bind a host folder into the sandbox?](#how-do-i-bind-a-host-folder-into-the-sandbox)
- [How does memory work?](#how-does-memory-work)
- [Does semantic memory search require an OpenAI API key?](#does-semantic-memory-search-require-an-openai-api-key)
- [Where does Clawdbot store its data?](#where-does-clawdbot-store-its-data)
- [How do I completely uninstall Clawdbot?](#how-do-i-completely-uninstall-clawdbot)
- [Can agents work outside the workspace?](#can-agents-work-outside-the-workspace)
- [I’m in remote mode — where is the session store?](#im-in-remote-mode-where-is-the-session-store)
- [What format is the config? Where is it?](#what-format-is-the-config-where-is-it)
- [I set `gateway.bind: "lan"` (or `"tailnet"`) and now nothing listens / the UI says unauthorized](#i-set-gatewaybind-lan-or-tailnet-and-now-nothing-listens-the-ui-says-unauthorized)
- [Why do I need a token on localhost now?](#why-do-i-need-a-token-on-localhost-now)
- [Do I have to restart after changing config?](#do-i-have-to-restart-after-changing-config)
- [How do I run a central Gateway with specialized workers across devices?](#how-do-i-run-a-central-gateway-with-specialized-workers-across-devices)
- [Can the Clawdbot browser run headless?](#can-the-clawdbot-browser-run-headless)
- [How do commands propagate between Telegram, the gateway, and nodes?](#how-do-commands-propagate-between-telegram-the-gateway-and-nodes)
- [Do nodes run a gateway daemon?](#do-nodes-run-a-gateway-daemon)
- [Is there an API / RPC way to apply config?](#is-there-an-api-rpc-way-to-apply-config)
- [What’s a minimal “sane” config for a first install?](#whats-a-minimal-sane-config-for-a-first-install)
- [How does Clawdbot load environment variables?](#how-does-clawdbot-load-environment-variables)
- [“I started the Gateway via a daemon and my env vars disappeared.” What now?](#i-started-the-gateway-via-a-daemon-and-my-env-vars-disappeared-what-now)
- [How do I start a fresh conversation?](#how-do-i-start-a-fresh-conversation)
- [How do I completely reset Clawdbot (but keep it installed)?](#how-do-i-completely-reset-clawdbot-but-keep-it-installed)
- [Do I need to add a “bot account” to a WhatsApp group?](#do-i-need-to-add-a-bot-account-to-a-whatsapp-group)
- [Why doesn’t Clawdbot reply in a group?](#why-doesnt-clawdbot-reply-in-a-group)
- [Do groups/threads share context with DMs?](#do-groupsthreads-share-context-with-dms)
- [What is the “default model”?](#what-is-the-default-model)
- [How do I switch models on the fly (without restarting)?](#how-do-i-switch-models-on-the-fly-without-restarting)
- [Why do I see “Model … is not allowed” and then no reply?](#why-do-i-see-model-is-not-allowed-and-then-no-reply)
- [Are opus / sonnet / gpt built‑in shortcuts?](#are-opus-sonnet-gpt-builtin-shortcuts)
- [How do I define/override model shortcuts (aliases)?](#how-do-i-defineoverride-model-shortcuts-aliases)
- [How do I add models from other providers like OpenRouter or Z.AI?](#how-do-i-add-models-from-other-providers-like-openrouter-or-zai)
- [How does failover work?](#how-does-failover-work)
- [What does this error mean?](#what-does-this-error-mean)
- [Fix checklist for `No credentials found for profile "anthropic:default"`](#fix-checklist-for-no-credentials-found-for-profile-anthropicdefault)
- [Why did it also try Google Gemini and fail?](#why-did-it-also-try-google-gemini-and-fail)
- [What is an auth profile?](#what-is-an-auth-profile)
- [What are typical profile IDs?](#what-are-typical-profile-ids)
- [Can I control which auth profile is tried first?](#can-i-control-which-auth-profile-is-tried-first)
- [OAuth vs API key: what’s the difference?](#oauth-vs-api-key-whats-the-difference)
- [What port does the Gateway use?](#what-port-does-the-gateway-use)
- [Why does `clawdbot daemon status` say `Runtime: running` but `RPC probe: failed`?](#why-does-clawdbot-daemon-status-say-runtime-running-but-rpc-probe-failed)
- [Why does `clawdbot daemon status` show `Config (cli)` and `Config (daemon)` different?](#why-does-clawdbot-daemon-status-show-config-cli-and-config-daemon-different)
- [What does “another gateway instance is already listening” mean?](#what-does-another-gateway-instance-is-already-listening-mean)
- [How do I run Clawdbot in remote mode (client connects to a Gateway elsewhere)?](#how-do-i-run-clawdbot-in-remote-mode-client-connects-to-a-gateway-elsewhere)
- [The Control UI says “unauthorized” (or keeps reconnecting). What now?](#the-control-ui-says-unauthorized-or-keeps-reconnecting-what-now)
- [I set `gateway.bind: "tailnet"` but it can’t bind / nothing listens](#i-set-gatewaybind-tailnet-but-it-cant-bind-nothing-listens)
- [Can I run multiple Gateways on the same host?](#can-i-run-multiple-gateways-on-the-same-host)
- [Where are logs?](#where-are-logs)
- [How do I start/stop/restart the Gateway daemon?](#how-do-i-startstoprestart-the-gateway-daemon)
- [What’s the fastest way to get more details when something fails?](#whats-the-fastest-way-to-get-more-details-when-something-fails)
- [My skill generated an image/PDF, but nothing was sent](#my-skill-generated-an-imagepdf-but-nothing-was-sent)
- [Is it safe to expose Clawdbot to inbound DMs?](#is-it-safe-to-expose-clawdbot-to-inbound-dms)
- [WhatsApp: will it message my contacts? How does pairing work?](#whatsapp-will-it-message-my-contacts-how-does-pairing-work)
- [How do I stop/cancel a running task?](#how-do-i-stopcancel-a-running-task)
- [Why does it feel like the bot “ignores” rapid‑fire messages?](#why-does-it-feel-like-the-bot-ignores-rapidfire-messages)
- [“All models failed” — what should I check first?](#all-models-failed-what-should-i-check-first)
- [I’m running on my personal WhatsApp number — why is self-chat weird?](#im-running-on-my-personal-whatsapp-number-why-is-self-chat-weird)
- [WhatsApp logged me out. How do I re‑auth?](#whatsapp-logged-me-out-how-do-i-reauth)
- [Build errors on `main` — what’s the standard fix path?](#build-errors-on-main-whats-the-standard-fix-path)
## First 60 seconds if something's broken ## First 60 seconds if something's broken
1) **Quick status (first check)** 1) **Quick status (first check)**
@@ -128,7 +212,7 @@ Yes. Clawdbot can **reuse Claude Code CLI credentials** (OAuth) and also support
### Is AWS Bedrock supported? ### Is AWS Bedrock supported?
Not currently. Clawdbot doesn’t ship a Bedrock provider today. If you must use Bedrock, the common workaround is an OpenAI‑compatible proxy in front of Bedrock, then point Clawdbot at that endpoint. See [Model providers](/providers/models) and [Model providers (full list)](/concepts/model-providers). Yes — via pi‑ai’s **Amazon Bedrock (Converse)** provider with **manual config**. You must supply AWS credentials/region on the gateway host and add a Bedrock provider entry in your models config. See [Amazon Bedrock](/bedrock) and [Model providers](/providers/models). If you prefer a managed key flow, an OpenAI‑compatible proxy in front of Bedrock is still a valid option.
### How does Codex auth work? ### How does Codex auth work?
+1 -1
View File
@@ -38,7 +38,7 @@ clawdbot agent --to +15555550123 --message "Summon reply" --deliver
- `--local`: run locally (requires provider keys in your shell) - `--local`: run locally (requires provider keys in your shell)
- `--deliver`: send the reply to the chosen provider (requires `--to`) - `--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` (default: `whatsapp`)
- `--thinking <off|minimal|low|medium|high>`: persist thinking level - `--thinking <off|minimal|low|medium|high|xhigh>`: persist thinking level (GPT-5.2 + Codex models only)
- `--verbose <on|off>`: persist verbose level - `--verbose <on|off>`: persist verbose level
- `--timeout <seconds>`: override agent timeout - `--timeout <seconds>`: override agent timeout
- `--json`: output structured JSON - `--json`: output structured JSON
+1 -1
View File
@@ -60,7 +60,7 @@ Text + native (when enabled):
- `/activation mention|always` (groups only) - `/activation mention|always` (groups only)
- `/send on|off|inherit` (owner-only) - `/send on|off|inherit` (owner-only)
- `/reset` or `/new` - `/reset` or `/new`
- `/think <level>` (aliases: `/thinking`, `/t`) - `/think <off|minimal|low|medium|high|xhigh>` (GPT-5.2 + Codex models only; aliases: `/thinking`, `/t`)
- `/verbose on|off` (alias: `/v`) - `/verbose on|off` (alias: `/v`)
- `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only) - `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only)
- `/elevated on|off` (alias: `/elev`) - `/elevated on|off` (alias: `/elev`)
+2 -1
View File
@@ -7,11 +7,12 @@ read_when:
## What it does ## What it does
- Inline directive in any inbound body: `/t <level>`, `/think:<level>`, or `/thinking <level>`. - Inline directive in any inbound body: `/t <level>`, `/think:<level>`, or `/thinking <level>`.
- Levels (aliases): `off | minimal | low | medium | high` - Levels (aliases): `off | minimal | low | medium | high | xhigh` (GPT-5.2 + Codex models only)
- minimal → “think” - minimal → “think”
- low → “think hard” - low → “think hard”
- medium → “think harder” - medium → “think harder”
- high → “ultrathink” (max budget) - high → “ultrathink” (max budget)
- xhigh → “ultrathink+” (GPT-5.2 + Codex models only)
- `highest`, `max` map to `high`. - `highest`, `max` map to `high`.
## Resolution order ## Resolution order
+1 -1
View File
@@ -50,7 +50,7 @@ Use SSH tunneling or Tailscale to reach the Gateway WS.
- `/agent <id>` (or `/agents`) - `/agent <id>` (or `/agents`)
- `/session <key>` (or `/sessions`) - `/session <key>` (or `/sessions`)
- `/model <provider/model>` (or `/model list`, `/models`) - `/model <provider/model>` (or `/model list`, `/models`)
- `/think <off|minimal|low|medium|high>` - `/think <off|minimal|low|medium|high|xhigh>` (GPT-5.2 + Codex models only)
- `/verbose <on|off>` - `/verbose <on|off>`
- `/reasoning <on|off|stream>` (stream = Telegram draft only) - `/reasoning <on|off|stream>` (stream = Telegram draft only)
- `/cost <on|off>` - `/cost <on|off>`
+5 -5
View File
@@ -132,10 +132,10 @@
"@grammyjs/runner": "^2.0.3", "@grammyjs/runner": "^2.0.3",
"@grammyjs/transformer-throttler": "^1.2.1", "@grammyjs/transformer-throttler": "^1.2.1",
"@homebridge/ciao": "^1.3.4", "@homebridge/ciao": "^1.3.4",
"@mariozechner/pi-agent-core": "^0.43.0", "@mariozechner/pi-agent-core": "^0.45.3",
"@mariozechner/pi-ai": "^0.43.0", "@mariozechner/pi-ai": "^0.45.3",
"@mariozechner/pi-coding-agent": "^0.43.0", "@mariozechner/pi-coding-agent": "^0.45.3",
"@mariozechner/pi-tui": "^0.43.0", "@mariozechner/pi-tui": "^0.45.3",
"@microsoft/agents-hosting": "^1.1.1", "@microsoft/agents-hosting": "^1.1.1",
"@microsoft/agents-hosting-express": "^1.1.1", "@microsoft/agents-hosting-express": "^1.1.1",
"@microsoft/agents-hosting-extensions-teams": "^1.1.1", "@microsoft/agents-hosting-extensions-teams": "^1.1.1",
@@ -207,7 +207,7 @@
"@sinclair/typebox": "0.34.47" "@sinclair/typebox": "0.34.47"
}, },
"patchedDependencies": { "patchedDependencies": {
"@mariozechner/pi-ai@0.43.0": "patches/@mariozechner__pi-ai@0.43.0.patch" "@mariozechner/pi-ai@0.45.3": "patches/@mariozechner__pi-ai@0.45.3.patch"
} }
}, },
"vitest": { "vitest": {
@@ -1,8 +1,8 @@
diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js
index 12540bb1069087a0d0a2967f792008627b9f79d9..f30b525620e6d8e45146b439ec3733e4053c9d2a 100644 index cc9e0cb..2b18ec4 100644
--- a/dist/providers/google-gemini-cli.js --- a/dist/providers/google-gemini-cli.js
+++ b/dist/providers/google-gemini-cli.js +++ b/dist/providers/google-gemini-cli.js
@@ -248,6 +248,11 @@ export const streamGoogleGeminiCli = (model, context, options) => { @@ -329,6 +329,11 @@ export const streamGoogleGeminiCli = (model, context, options) => {
break; // Success, exit retry loop break; // Success, exit retry loop
} }
const errorText = await response.text(); const errorText = await response.text();
@@ -14,41 +14,8 @@ index 12540bb1069087a0d0a2967f792008627b9f79d9..f30b525620e6d8e45146b439ec3733e4
// Check if retryable // Check if retryable
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) { if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
// Use server-provided delay or exponential backoff // Use server-provided delay or exponential backoff
diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js
index ae4710b0f134ac4a48f5b7053f454d1068bee71f..b1b5bd94586f68461ccc44e4a9cdf3acb4e0d084 100644
--- a/dist/providers/google-shared.js
+++ b/dist/providers/google-shared.js
@@ -42,6 +42,8 @@ export function retainThoughtSignature(existing, incoming) {
export function convertMessages(model, context) {
const contents = [];
const transformedMessages = transformMessages(context.messages, model);
+ const shouldStripFunctionId = typeof model.provider === "string" &&
+ model.provider.startsWith("google");
for (const msg of transformedMessages) {
if (msg.role === "user") {
if (typeof msg.content === "string") {
@@ -113,6 +115,9 @@ export function convertMessages(model, context) {
args: block.arguments,
},
};
+ if (shouldStripFunctionId && part?.functionCall?.id) {
+ delete part.functionCall.id; // Google Gemini/Vertex do not support 'id' in functionCall
+ }
if (block.thoughtSignature) {
part.thoughtSignature = block.thoughtSignature;
}
@@ -155,6 +160,9 @@ export function convertMessages(model, context) {
...(hasImages && supportsMultimodalFunctionResponse && { parts: imageParts }),
},
};
+ if (shouldStripFunctionId && functionResponsePart.functionResponse?.id) {
+ delete functionResponsePart.functionResponse.id; // Google Gemini/Vertex do not support 'id' in functionResponse
+ }
// Cloud Code Assist API requires all function responses to be in a single user turn.
// Check if the last content is already a user turn with function responses and merge.
const lastContent = contents[contents.length - 1];
diff --git a/dist/providers/openai-codex-responses.js b/dist/providers/openai-codex-responses.js diff --git a/dist/providers/openai-codex-responses.js b/dist/providers/openai-codex-responses.js
index ad0a2aabbe10382cee4e463b68a02864dd235e57..8c001acfd0b4e0743181c246f1bedcf8cd2ffb02 100644 index 7488c79..4c34587 100644
--- a/dist/providers/openai-codex-responses.js --- a/dist/providers/openai-codex-responses.js
+++ b/dist/providers/openai-codex-responses.js +++ b/dist/providers/openai-codex-responses.js
@@ -517,7 +517,7 @@ function convertTools(tools) { @@ -517,7 +517,7 @@ function convertTools(tools) {
@@ -61,10 +28,10 @@ index ad0a2aabbe10382cee4e463b68a02864dd235e57..8c001acfd0b4e0743181c246f1bedcf8
} }
function mapStopReason(status) { function mapStopReason(status) {
diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js
index f07085c64390b211340d6a826b28ea9c2e77302f..7f758532246cc7b062df48e9cec4e6c904b76a99 100644 index c4714f4..4d1e6b0 100644
--- a/dist/providers/openai-responses.js --- a/dist/providers/openai-responses.js
+++ b/dist/providers/openai-responses.js +++ b/dist/providers/openai-responses.js
@@ -396,10 +396,16 @@ function convertMessages(model, context) { @@ -400,10 +400,16 @@ function convertMessages(model, context) {
} }
else if (msg.role === "assistant") { else if (msg.role === "assistant") {
const output = []; const output = [];
@@ -81,7 +48,7 @@ index f07085c64390b211340d6a826b28ea9c2e77302f..7f758532246cc7b062df48e9cec4e6c9
const reasoningItem = JSON.parse(block.thinkingSignature); const reasoningItem = JSON.parse(block.thinkingSignature);
output.push(reasoningItem); output.push(reasoningItem);
} }
@@ -434,6 +440,16 @@ function convertMessages(model, context) { @@ -438,6 +444,16 @@ function convertMessages(model, context) {
}); });
} }
} }
+1258 -315
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -10,4 +10,4 @@ onlyBuiltDependencies:
- sharp - sharp
patchedDependencies: patchedDependencies:
'@mariozechner/pi-ai@0.43.0': patches/@mariozechner__pi-ai@0.43.0.patch '@mariozechner/pi-ai@0.45.3': patches/@mariozechner__pi-ai@0.45.3.patch
+1 -1
View File
@@ -57,7 +57,7 @@ describe("resolveAgentConfig", () => {
defaults: { defaults: {
model: { model: {
primary: "anthropic/claude-sonnet-4", primary: "anthropic/claude-sonnet-4",
fallbacks: ["openai/gpt-4.1"], fallbacks: ["openai/gpt-5-nano"],
}, },
}, },
list: [ list: [
+13 -13
View File
@@ -8,7 +8,7 @@ function makeCfg(overrides: Partial<ClawdbotConfig> = {}): ClawdbotConfig {
agents: { agents: {
defaults: { defaults: {
model: { model: {
primary: "openai/gpt-4.1-mini", primary: "openai/gpt-5-nano",
fallbacks: ["anthropic/claude-haiku-3-5"], fallbacks: ["anthropic/claude-haiku-3-5"],
}, },
}, },
@@ -29,7 +29,7 @@ describe("runWithModelFallback", () => {
runWithModelFallback({ runWithModelFallback({
cfg, cfg,
provider: "openai", provider: "openai",
model: "gpt-4.1-mini", model: "gpt-5-nano",
run, run,
}), }),
).rejects.toThrow("bad request"); ).rejects.toThrow("bad request");
@@ -46,7 +46,7 @@ describe("runWithModelFallback", () => {
const result = await runWithModelFallback({ const result = await runWithModelFallback({
cfg, cfg,
provider: "openai", provider: "openai",
model: "gpt-4.1-mini", model: "gpt-5-nano",
run, run,
}); });
@@ -68,7 +68,7 @@ describe("runWithModelFallback", () => {
const result = await runWithModelFallback({ const result = await runWithModelFallback({
cfg, cfg,
provider: "openai", provider: "openai",
model: "gpt-4.1-mini", model: "gpt-5-nano",
run, run,
}); });
@@ -92,7 +92,7 @@ describe("runWithModelFallback", () => {
const result = await runWithModelFallback({ const result = await runWithModelFallback({
cfg, cfg,
provider: "openai", provider: "openai",
model: "gpt-4.1-mini", model: "gpt-5-nano",
run, run,
}); });
@@ -129,7 +129,7 @@ describe("runWithModelFallback", () => {
agents: { agents: {
defaults: { defaults: {
model: { model: {
primary: "openai/gpt-4.1-mini", primary: "openai/gpt-5-nano",
}, },
}, },
}, },
@@ -173,13 +173,13 @@ describe("runWithModelFallback", () => {
cfg, cfg,
provider: "anthropic", provider: "anthropic",
model: "claude-opus-4-5", model: "claude-opus-4-5",
fallbacksOverride: ["openai/gpt-4.1"], fallbacksOverride: ["openai/gpt-5-nano"],
run: async (provider, model) => { run: async (provider, model) => {
calls.push({ provider, model }); calls.push({ provider, model });
if (provider === "anthropic") { if (provider === "anthropic") {
throw Object.assign(new Error("nope"), { status: 401 }); throw Object.assign(new Error("nope"), { status: 401 });
} }
if (provider === "openai" && model === "gpt-4.1") { if (provider === "openai" && model === "gpt-5-nano") {
return "ok"; return "ok";
} }
throw new Error(`unexpected candidate: ${provider}/${model}`); throw new Error(`unexpected candidate: ${provider}/${model}`);
@@ -189,7 +189,7 @@ describe("runWithModelFallback", () => {
expect(res.result).toBe("ok"); expect(res.result).toBe("ok");
expect(calls).toEqual([ expect(calls).toEqual([
{ provider: "anthropic", model: "claude-opus-4-5" }, { provider: "anthropic", model: "claude-opus-4-5" },
{ provider: "openai", model: "gpt-4.1" }, { provider: "openai", model: "gpt-5-nano" },
]); ]);
}); });
@@ -234,7 +234,7 @@ describe("runWithModelFallback", () => {
const result = await runWithModelFallback({ const result = await runWithModelFallback({
cfg, cfg,
provider: "openai", provider: "openai",
model: "gpt-4.1-mini", model: "gpt-5-nano",
run, run,
}); });
@@ -254,7 +254,7 @@ describe("runWithModelFallback", () => {
const result = await runWithModelFallback({ const result = await runWithModelFallback({
cfg, cfg,
provider: "openai", provider: "openai",
model: "gpt-4.1-mini", model: "gpt-5-nano",
run, run,
}); });
@@ -269,7 +269,7 @@ describe("runWithModelFallback", () => {
agents: { agents: {
defaults: { defaults: {
model: { model: {
primary: "openai/gpt-4.1-mini", primary: "openai/gpt-5-nano",
fallbacks: [], fallbacks: [],
}, },
}, },
@@ -292,6 +292,6 @@ describe("runWithModelFallback", () => {
expect(result.result).toBe("ok"); expect(result.result).toBe("ok");
expect(run).toHaveBeenCalledTimes(2); expect(run).toHaveBeenCalledTimes(2);
expect(result.provider).toBe("openai"); expect(result.provider).toBe("openai");
expect(result.model).toBe("gpt-4.1-mini"); expect(result.model).toBe("gpt-5-nano");
}); });
}); });
+12 -8
View File
@@ -13,8 +13,8 @@ import {
const catalog = [ const catalog = [
{ {
provider: "openai", provider: "openai",
id: "gpt-4", id: "gpt-5-nano",
name: "GPT-4", name: "GPT-5 Nano",
}, },
]; ];
@@ -24,7 +24,7 @@ describe("buildAllowedModelSet", () => {
agents: { agents: {
defaults: { defaults: {
models: { models: {
"openai/gpt-4": { alias: "gpt4" }, "openai/gpt-5-nano": { alias: "gpt5nano" },
}, },
}, },
}, },
@@ -38,7 +38,9 @@ describe("buildAllowedModelSet", () => {
}); });
expect(allowed.allowAny).toBe(false); expect(allowed.allowAny).toBe(false);
expect(allowed.allowedKeys.has(modelKey("openai", "gpt-4"))).toBe(true); expect(allowed.allowedKeys.has(modelKey("openai", "gpt-5-nano"))).toBe(
true,
);
expect(allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5"))).toBe( expect(allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5"))).toBe(
true, true,
); );
@@ -57,7 +59,9 @@ describe("buildAllowedModelSet", () => {
}); });
expect(allowed.allowAny).toBe(true); expect(allowed.allowAny).toBe(true);
expect(allowed.allowedKeys.has(modelKey("openai", "gpt-4"))).toBe(true); expect(allowed.allowedKeys.has(modelKey("openai", "gpt-5-nano"))).toBe(
true,
);
expect(allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5"))).toBe( expect(allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5"))).toBe(
true, true,
); );
@@ -236,7 +240,7 @@ describe("resolveAllowedModelRef", () => {
agents: { agents: {
defaults: { defaults: {
models: { models: {
"openai/gpt-4": { alias: "GPT4" }, "openai/gpt-5-nano": { alias: "GPT5NANO" },
}, },
}, },
}, },
@@ -244,12 +248,12 @@ describe("resolveAllowedModelRef", () => {
const resolved = resolveAllowedModelRef({ const resolved = resolveAllowedModelRef({
cfg, cfg,
catalog: [ catalog: [
{ provider: "openai", id: "gpt-4", name: "GPT-4" }, { provider: "openai", id: "gpt-5-nano", name: "GPT-5 Nano" },
{ provider: "anthropic", id: "claude-sonnet-4-1", name: "Sonnet" }, { provider: "anthropic", id: "claude-sonnet-4-1", name: "Sonnet" },
], ],
raw: "anthropic/claude-sonnet-4-1", raw: "anthropic/claude-sonnet-4-1",
defaultProvider: "openai", defaultProvider: "openai",
defaultModel: "gpt-4", defaultModel: "gpt-5-nano",
}); });
expect(resolved).toEqual({ expect(resolved).toEqual({
error: "model not allowed: anthropic/claude-sonnet-4-1", error: "model not allowed: anthropic/claude-sonnet-4-1",
+7 -1
View File
@@ -7,7 +7,13 @@ export type ModelRef = {
model: string; model: string;
}; };
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high"; export type ThinkLevel =
| "off"
| "minimal"
| "low"
| "medium"
| "high"
| "xhigh";
export type ModelAliasIndex = { export type ModelAliasIndex = {
byAlias: Map<string, { alias: string; ref: ModelRef }>; byAlias: Map<string, { alias: string; ref: ModelRef }>;
@@ -207,11 +207,11 @@ describe("resolveExtraParams", () => {
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
it("returns undefined for openai/gpt-4 with no config", () => { it("returns undefined for openai/gpt-5-nano with no config", () => {
const result = resolveExtraParams({ const result = resolveExtraParams({
cfg: undefined, cfg: undefined,
provider: "openai", provider: "openai",
modelId: "gpt-4", modelId: "gpt-5-nano",
}); });
expect(result).toBeUndefined(); expect(result).toBeUndefined();
@@ -223,7 +223,7 @@ describe("resolveExtraParams", () => {
agents: { agents: {
defaults: { defaults: {
models: { models: {
"openai/gpt-4": { "openai/gpt-5-nano": {
params: { params: {
logprobs: true, logprobs: true,
top_logprobs: 5, top_logprobs: 5,
@@ -234,7 +234,7 @@ describe("resolveExtraParams", () => {
}, },
}, },
provider: "openai", provider: "openai",
modelId: "gpt-4", modelId: "gpt-5-nano",
}); });
expect(result).toEqual({ expect(result).toEqual({
+1 -1
View File
@@ -1046,7 +1046,7 @@ export function resolveEmbeddedSessionLane(key: string) {
} }
function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel { function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel {
// pi-agent-core supports "xhigh" too; Clawdbot doesn't surface it for now. // pi-agent-core supports "xhigh"; Clawdbot enables it for specific models.
if (!level) return "off"; if (!level) return "off";
return level; return level;
} }
+1 -1
View File
@@ -42,7 +42,7 @@ describe("block streaming", () => {
piEmbeddedMock.runEmbeddedPiAgent.mockReset(); piEmbeddedMock.runEmbeddedPiAgent.mockReset();
vi.mocked(loadModelCatalog).mockResolvedValue([ vi.mocked(loadModelCatalog).mockResolvedValue([
{ id: "claude-opus-4-5", name: "Opus 4.5", provider: "anthropic" }, { id: "claude-opus-4-5", name: "Opus 4.5", provider: "anthropic" },
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini", provider: "openai" }, { id: "gpt-5-nano", name: "GPT-5 Nano", provider: "openai" },
]); ]);
}); });
+116 -25
View File
@@ -61,7 +61,7 @@ describe("directive behavior", () => {
vi.mocked(loadModelCatalog).mockResolvedValue([ vi.mocked(loadModelCatalog).mockResolvedValue([
{ id: "claude-opus-4-5", name: "Opus 4.5", provider: "anthropic" }, { id: "claude-opus-4-5", name: "Opus 4.5", provider: "anthropic" },
{ id: "claude-sonnet-4-1", name: "Sonnet 4.1", provider: "anthropic" }, { id: "claude-sonnet-4-1", name: "Sonnet 4.1", provider: "anthropic" },
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini", provider: "openai" }, { id: "gpt-5-nano", name: "GPT-5 Nano", provider: "openai" },
]); ]);
}); });
@@ -69,6 +69,97 @@ describe("directive behavior", () => {
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
it("accepts /thinking xhigh for codex models", async () => {
await withTempHome(async (home) => {
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{
Body: "/thinking xhigh",
From: "+1004",
To: "+2000",
},
{},
{
agents: {
defaults: {
model: "openai-codex/gpt-5.2-codex",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: storePath },
},
);
const texts = (Array.isArray(res) ? res : [res])
.map((entry) => entry?.text)
.filter(Boolean);
expect(texts).toContain("Thinking level set to xhigh.");
});
});
it("accepts /thinking xhigh for openai gpt-5.2", async () => {
await withTempHome(async (home) => {
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{
Body: "/thinking xhigh",
From: "+1004",
To: "+2000",
},
{},
{
agents: {
defaults: {
model: "openai/gpt-5.2",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: storePath },
},
);
const texts = (Array.isArray(res) ? res : [res])
.map((entry) => entry?.text)
.filter(Boolean);
expect(texts).toContain("Thinking level set to xhigh.");
});
});
it("rejects /thinking xhigh for non-codex models", async () => {
await withTempHome(async (home) => {
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{
Body: "/thinking xhigh",
From: "+1004",
To: "+2000",
},
{},
{
agents: {
defaults: {
model: "openai/gpt-5-nano",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: storePath },
},
);
const texts = (Array.isArray(res) ? res : [res])
.map((entry) => entry?.text)
.filter(Boolean);
expect(texts).toContain(
'Thinking level "xhigh" is only supported for openai/gpt-5.2, openai-codex/gpt-5.2-codex or openai-codex/gpt-5.1-codex.',
);
});
});
it("keeps reserved command aliases from matching after trimming", async () => { it("keeps reserved command aliases from matching after trimming", async () => {
await withTempHome(async (home) => { await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockReset(); vi.mocked(runEmbeddedPiAgent).mockReset();
@@ -1564,7 +1655,7 @@ describe("directive behavior", () => {
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
models: { models: {
"anthropic/claude-opus-4-5": {}, "anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {}, "openai/gpt-5-nano": {},
}, },
}, },
}, },
@@ -1575,7 +1666,7 @@ describe("directive behavior", () => {
const text = Array.isArray(res) ? res[0]?.text : res?.text; const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("anthropic/claude-opus-4-5"); expect(text).toContain("anthropic/claude-opus-4-5");
expect(text).toContain("Pick: /model <#> or /model <provider/model>"); expect(text).toContain("Pick: /model <#> or /model <provider/model>");
expect(text).toContain("gpt-4.1-mini — openai"); expect(text).toContain("gpt-5-nano — openai");
expect(text).not.toContain("claude-sonnet-4-1"); expect(text).not.toContain("claude-sonnet-4-1");
expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
}); });
@@ -1596,7 +1687,7 @@ describe("directive behavior", () => {
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
models: { models: {
"anthropic/claude-opus-4-5": {}, "anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {}, "openai/gpt-5-nano": {},
}, },
}, },
}, },
@@ -1606,7 +1697,7 @@ describe("directive behavior", () => {
const text = Array.isArray(res) ? res[0]?.text : res?.text; const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("anthropic/claude-opus-4-5"); expect(text).toContain("anthropic/claude-opus-4-5");
expect(text).toContain("openai/gpt-4.1-mini"); expect(text).toContain("openai/gpt-5-nano");
expect(text).not.toContain("claude-sonnet-4-1"); expect(text).not.toContain("claude-sonnet-4-1");
expect(text).toContain("auth:"); expect(text).toContain("auth:");
expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
@@ -1628,7 +1719,7 @@ describe("directive behavior", () => {
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
models: { models: {
"anthropic/claude-opus-4-5": {}, "anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {}, "openai/gpt-5-nano": {},
}, },
}, },
}, },
@@ -1639,7 +1730,7 @@ describe("directive behavior", () => {
const text = Array.isArray(res) ? res[0]?.text : res?.text; const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("Pick: /model <#> or /model <provider/model>"); expect(text).toContain("Pick: /model <#> or /model <provider/model>");
expect(text).toContain("claude-opus-4-5 — anthropic"); expect(text).toContain("claude-opus-4-5 — anthropic");
expect(text).toContain("gpt-4.1-mini — openai"); expect(text).toContain("gpt-5-nano — openai");
expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
}); });
}); });
@@ -1660,7 +1751,7 @@ describe("directive behavior", () => {
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
models: { models: {
"anthropic/claude-opus-4-5": {}, "anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {}, "openai/gpt-5-nano": {},
}, },
}, },
}, },
@@ -1671,7 +1762,7 @@ describe("directive behavior", () => {
const text = Array.isArray(res) ? res[0]?.text : res?.text; const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("Pick: /model <#> or /model <provider/model>"); expect(text).toContain("Pick: /model <#> or /model <provider/model>");
expect(text).toContain("claude-opus-4-5 — anthropic"); expect(text).toContain("claude-opus-4-5 — anthropic");
expect(text).toContain("gpt-4.1-mini — openai"); expect(text).toContain("gpt-5-nano — openai");
expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
}); });
}); });
@@ -1687,7 +1778,7 @@ describe("directive behavior", () => {
id: "claude-opus-4-5", id: "claude-opus-4-5",
name: "Claude Opus 4.5", name: "Claude Opus 4.5",
}, },
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" }, { provider: "openai", id: "gpt-5-nano", name: "GPT-5 Nano" },
]); ]);
const storePath = path.join(home, "sessions.json"); const storePath = path.join(home, "sessions.json");
@@ -1701,7 +1792,7 @@ describe("directive behavior", () => {
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
models: { models: {
"anthropic/claude-opus-4-5": {}, "anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {}, "openai/gpt-5-nano": {},
"minimax/MiniMax-M2.1": { alias: "minimax" }, "minimax/MiniMax-M2.1": { alias: "minimax" },
}, },
}, },
@@ -1722,7 +1813,7 @@ describe("directive behavior", () => {
const text = Array.isArray(res) ? res[0]?.text : res?.text; const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("claude-opus-4-5 — anthropic"); expect(text).toContain("claude-opus-4-5 — anthropic");
expect(text).toContain("gpt-4.1-mini — openai"); expect(text).toContain("gpt-5-nano — openai");
expect(text).toContain("MiniMax-M2.1 — minimax"); expect(text).toContain("MiniMax-M2.1 — minimax");
expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
}); });
@@ -1762,7 +1853,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json"); const storePath = path.join(home, "sessions.json");
await getReplyFromConfig( await getReplyFromConfig(
{ Body: "/model openai/gpt-4.1-mini", From: "+1222", To: "+1222" }, { Body: "/model openai/gpt-5-nano", From: "+1222", To: "+1222" },
{}, {},
{ {
agents: { agents: {
@@ -1771,7 +1862,7 @@ describe("directive behavior", () => {
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
models: { models: {
"anthropic/claude-opus-4-5": {}, "anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {}, "openai/gpt-5-nano": {},
}, },
}, },
}, },
@@ -1780,7 +1871,7 @@ describe("directive behavior", () => {
); );
assertModelSelection(storePath, { assertModelSelection(storePath, {
model: "gpt-4.1-mini", model: "gpt-5-nano",
provider: "openai", provider: "openai",
}); });
expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
@@ -1798,10 +1889,10 @@ describe("directive behavior", () => {
{ {
agents: { agents: {
defaults: { defaults: {
model: { primary: "openai/gpt-4.1-mini" }, model: { primary: "openai/gpt-5-nano" },
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
models: { models: {
"openai/gpt-4.1-mini": {}, "openai/gpt-5-nano": {},
"anthropic/claude-opus-4-5": { alias: "Opus" }, "anthropic/claude-opus-4-5": { alias: "Opus" },
}, },
}, },
@@ -2118,10 +2209,10 @@ describe("directive behavior", () => {
{ {
agents: { agents: {
defaults: { defaults: {
model: { primary: "openai/gpt-4.1-mini" }, model: { primary: "openai/gpt-5-nano" },
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
models: { models: {
"openai/gpt-4.1-mini": {}, "openai/gpt-5-nano": {},
"anthropic/claude-opus-4-5": { alias: "Opus" }, "anthropic/claude-opus-4-5": { alias: "Opus" },
}, },
}, },
@@ -2151,10 +2242,10 @@ describe("directive behavior", () => {
{ {
agents: { agents: {
defaults: { defaults: {
model: { primary: "openai/gpt-4.1-mini" }, model: { primary: "openai/gpt-5-nano" },
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
models: { models: {
"openai/gpt-4.1-mini": {}, "openai/gpt-5-nano": {},
"anthropic/claude-opus-4-5": { alias: "Opus" }, "anthropic/claude-opus-4-5": { alias: "Opus" },
}, },
}, },
@@ -2187,7 +2278,7 @@ describe("directive behavior", () => {
{ {
agents: { agents: {
defaults: { defaults: {
model: { primary: "openai/gpt-4.1-mini" }, model: { primary: "openai/gpt-5-nano" },
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
}, },
}, },
@@ -2218,7 +2309,7 @@ describe("directive behavior", () => {
{ {
agents: { agents: {
defaults: { defaults: {
model: { primary: "openai/gpt-4.1-mini" }, model: { primary: "openai/gpt-5-nano" },
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
}, },
}, },
@@ -2245,7 +2336,7 @@ describe("directive behavior", () => {
const res = await getReplyFromConfig( const res = await getReplyFromConfig(
{ {
Body: "please sync /model openai/gpt-4.1-mini now", Body: "please sync /model openai/gpt-5-nano now",
From: "+1004", From: "+1004",
To: "+2000", To: "+2000",
}, },
@@ -2257,7 +2348,7 @@ describe("directive behavior", () => {
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
models: { models: {
"anthropic/claude-opus-4-5": {}, "anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {}, "openai/gpt-5-nano": {},
}, },
}, },
}, },
+5 -5
View File
@@ -41,7 +41,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
name: "Claude Opus 4.5 (OpenRouter)", name: "Claude Opus 4.5 (OpenRouter)",
contextWindow: 200000, contextWindow: 200000,
}, },
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" }, { provider: "openai", id: "gpt-5-nano", name: "GPT-5 Nano" },
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" }, { provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
{ provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" }, { provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" }, { provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
@@ -320,7 +320,7 @@ describe("trigger handling", () => {
const res = await getReplyFromConfig( const res = await getReplyFromConfig(
{ {
Body: "/model openai/gpt-4.1-mini", Body: "/model openai/gpt-5-nano",
From: "telegram:111", From: "telegram:111",
To: "telegram:111", To: "telegram:111",
ChatType: "direct", ChatType: "direct",
@@ -336,11 +336,11 @@ describe("trigger handling", () => {
); );
const text = Array.isArray(res) ? res[0]?.text : res?.text; const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("Model set to openai/gpt-4.1-mini"); expect(text).toContain("Model set to openai/gpt-5-nano");
const store = loadSessionStore(cfg.session.store); const store = loadSessionStore(cfg.session.store);
expect(store[targetSessionKey]?.providerOverride).toBe("openai"); expect(store[targetSessionKey]?.providerOverride).toBe("openai");
expect(store[targetSessionKey]?.modelOverride).toBe("gpt-4.1-mini"); expect(store[targetSessionKey]?.modelOverride).toBe("gpt-5-nano");
expect(store[slashSessionKey]).toBeUndefined(); expect(store[slashSessionKey]).toBeUndefined();
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
@@ -368,7 +368,7 @@ describe("trigger handling", () => {
expect(vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]).toEqual( expect(vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]).toEqual(
expect.objectContaining({ expect.objectContaining({
provider: "openai", provider: "openai",
model: "gpt-4.1-mini", model: "gpt-5-nano",
}), }),
); );
}); });
+37 -2
View File
@@ -29,7 +29,10 @@ import {
type ClawdbotConfig, type ClawdbotConfig,
loadConfig, loadConfig,
} from "../config/config.js"; } from "../config/config.js";
import { resolveSessionFilePath } from "../config/sessions.js"; import {
resolveSessionFilePath,
saveSessionStore,
} from "../config/sessions.js";
import { logVerbose } from "../globals.js"; import { logVerbose } from "../globals.js";
import { clearCommandLane, getQueueSize } from "../process/command-queue.js"; import { clearCommandLane, getQueueSize } from "../process/command-queue.js";
import { getProviderDock } from "../providers/dock.js"; import { getProviderDock } from "../providers/dock.js";
@@ -94,8 +97,10 @@ import {
import type { MsgContext, TemplateContext } from "./templating.js"; import type { MsgContext, TemplateContext } from "./templating.js";
import { import {
type ElevatedLevel, type ElevatedLevel,
formatXHighModelHint,
normalizeThinkLevel, normalizeThinkLevel,
type ReasoningLevel, type ReasoningLevel,
supportsXHighThinking,
type ThinkLevel, type ThinkLevel,
type VerboseLevel, type VerboseLevel,
} from "./thinking.js"; } from "./thinking.js";
@@ -1187,7 +1192,10 @@ export async function getReplyFromConfig(
if (!resolvedThinkLevel && prefixedCommandBody) { if (!resolvedThinkLevel && prefixedCommandBody) {
const parts = prefixedCommandBody.split(/\s+/); const parts = prefixedCommandBody.split(/\s+/);
const maybeLevel = normalizeThinkLevel(parts[0]); const maybeLevel = normalizeThinkLevel(parts[0]);
if (maybeLevel) { if (
maybeLevel &&
(maybeLevel !== "xhigh" || supportsXHighThinking(provider, model))
) {
resolvedThinkLevel = maybeLevel; resolvedThinkLevel = maybeLevel;
prefixedCommandBody = parts.slice(1).join(" ").trim(); prefixedCommandBody = parts.slice(1).join(" ").trim();
} }
@@ -1195,6 +1203,33 @@ export async function getReplyFromConfig(
if (!resolvedThinkLevel) { if (!resolvedThinkLevel) {
resolvedThinkLevel = await modelState.resolveDefaultThinkingLevel(); resolvedThinkLevel = await modelState.resolveDefaultThinkingLevel();
} }
if (
resolvedThinkLevel === "xhigh" &&
!supportsXHighThinking(provider, model)
) {
const explicitThink =
directives.hasThinkDirective && directives.thinkLevel !== undefined;
if (explicitThink) {
typing.cleanup();
return {
text: `Thinking level "xhigh" is only supported for ${formatXHighModelHint()}. Use /think high or switch to one of those models.`,
};
}
resolvedThinkLevel = "high";
if (
sessionEntry &&
sessionStore &&
sessionKey &&
sessionEntry.thinkingLevel === "xhigh"
) {
sessionEntry.thinkingLevel = "high";
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
if (storePath) {
await saveSessionStore(storePath, sessionStore);
}
}
}
const sessionIdFinal = sessionId ?? crypto.randomUUID(); const sessionIdFinal = sessionId ?? crypto.randomUUID();
const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry); const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry);
const queueBodyBase = transcribedText const queueBodyBase = transcribedText
+156 -120
View File
@@ -37,6 +37,11 @@ import { applyVerboseOverride } from "../../sessions/level-overrides.js";
import { shortenHomePath } from "../../utils.js"; import { shortenHomePath } from "../../utils.js";
import { extractModelDirective } from "../model.js"; import { extractModelDirective } from "../model.js";
import type { MsgContext } from "../templating.js"; import type { MsgContext } from "../templating.js";
import {
formatThinkingLevels,
formatXHighModelHint,
supportsXHighThinking,
} from "../thinking.js";
import type { ReplyPayload } from "../types.js"; import type { ReplyPayload } from "../types.js";
import { import {
type ElevatedLevel, type ElevatedLevel,
@@ -778,6 +783,7 @@ export async function handleDirectiveOnly(params: {
allowedModelCatalog, allowedModelCatalog,
resetModelOverride, resetModelOverride,
provider, provider,
model,
initialModelLabel, initialModelLabel,
formatModelSwitchEvent, formatModelSwitchEvent,
currentThinkLevel, currentThinkLevel,
@@ -943,6 +949,117 @@ export async function handleDirectiveOnly(params: {
} }
} }
let modelSelection: ModelDirectiveSelection | undefined;
let profileOverride: string | undefined;
if (directives.hasModelDirective && directives.rawModelDirective) {
const raw = directives.rawModelDirective.trim();
if (/^[0-9]+$/.test(raw)) {
const resolvedDefault = resolveConfiguredModelRef({
cfg: params.cfg,
defaultProvider,
defaultModel,
});
const pickerCatalog: ModelPickerCatalogEntry[] = (() => {
const keys = new Set<string>();
const out: ModelPickerCatalogEntry[] = [];
const push = (entry: ModelPickerCatalogEntry) => {
const provider = normalizeProviderId(entry.provider);
const id = String(entry.id ?? "").trim();
if (!provider || !id) return;
const key = modelKey(provider, id);
if (keys.has(key)) return;
keys.add(key);
out.push({ provider, id, name: entry.name });
};
for (const entry of allowedModelCatalog) push(entry);
for (const rawKey of Object.keys(
params.cfg.agents?.defaults?.models ?? {},
)) {
const resolved = resolveModelRefFromString({
raw: String(rawKey),
defaultProvider,
aliasIndex,
});
if (!resolved) continue;
push({
provider: resolved.ref.provider,
id: resolved.ref.model,
name: resolved.ref.model,
});
}
if (resolvedDefault.model) {
push({
provider: resolvedDefault.provider,
id: resolvedDefault.model,
name: resolvedDefault.model,
});
}
return out;
})();
const items = buildModelPickerItems(pickerCatalog);
const index = Number.parseInt(raw, 10) - 1;
const item = Number.isFinite(index) ? items[index] : undefined;
if (!item) {
return {
text: `Invalid model selection "${raw}". Use /model to list.`,
};
}
const picked = pickProviderForModel({
item,
preferredProvider: params.provider,
});
if (!picked) {
return {
text: `Invalid model selection "${raw}". Use /model to list.`,
};
}
const key = `${picked.provider}/${picked.model}`;
const aliases = aliasIndex.byKey.get(key);
const alias = aliases && aliases.length > 0 ? aliases[0] : undefined;
modelSelection = {
provider: picked.provider,
model: picked.model,
isDefault:
picked.provider === defaultProvider && picked.model === defaultModel,
...(alias ? { alias } : {}),
};
} else {
const resolved = resolveModelDirectiveSelection({
raw,
defaultProvider,
defaultModel,
aliasIndex,
allowedModelKeys,
});
if (resolved.error) {
return { text: resolved.error };
}
modelSelection = resolved.selection;
}
if (modelSelection && directives.rawModelProfile) {
const profileResolved = resolveProfileOverride({
rawProfile: directives.rawModelProfile,
provider: modelSelection.provider,
cfg: params.cfg,
agentDir,
});
if (profileResolved.error) {
return { text: profileResolved.error };
}
profileOverride = profileResolved.profileId;
}
}
if (directives.rawModelProfile && !modelSelection) {
return { text: "Auth profile override requires a model selection." };
}
const resolvedProvider = modelSelection?.provider ?? provider;
const resolvedModel = modelSelection?.model ?? model;
if (directives.hasThinkDirective && !directives.thinkLevel) { if (directives.hasThinkDirective && !directives.thinkLevel) {
// If no argument was provided, show the current level // If no argument was provided, show the current level
if (!directives.rawThinkLevel) { if (!directives.rawThinkLevel) {
@@ -950,12 +1067,12 @@ export async function handleDirectiveOnly(params: {
return { return {
text: withOptions( text: withOptions(
`Current thinking level: ${level}.`, `Current thinking level: ${level}.`,
"off, minimal, low, medium, high", formatThinkingLevels(resolvedProvider, resolvedModel),
), ),
}; };
} }
return { return {
text: `Unrecognized thinking level "${directives.rawThinkLevel}". Valid levels: off, minimal, low, medium, high.`, text: `Unrecognized thinking level "${directives.rawThinkLevel}". Valid levels: ${formatThinkingLevels(resolvedProvider, resolvedModel)}.`,
}; };
} }
if (directives.hasVerboseDirective && !directives.verboseLevel) { if (directives.hasVerboseDirective && !directives.verboseLevel) {
@@ -1098,126 +1215,25 @@ export async function handleDirectiveOnly(params: {
return { text: errors.join(" ") }; return { text: errors.join(" ") };
} }
let modelSelection: ModelDirectiveSelection | undefined; if (
let profileOverride: string | undefined; directives.hasThinkDirective &&
if (directives.hasModelDirective && directives.rawModelDirective) { directives.thinkLevel === "xhigh" &&
const raw = directives.rawModelDirective.trim(); !supportsXHighThinking(resolvedProvider, resolvedModel)
if (/^[0-9]+$/.test(raw)) { ) {
const resolvedDefault = resolveConfiguredModelRef({ return {
cfg: params.cfg, text: `Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`,
defaultProvider, };
defaultModel,
});
const pickerCatalog: ModelPickerCatalogEntry[] = (() => {
const keys = new Set<string>();
const out: ModelPickerCatalogEntry[] = [];
const push = (entry: ModelPickerCatalogEntry) => {
const provider = normalizeProviderId(entry.provider);
const id = String(entry.id ?? "").trim();
if (!provider || !id) return;
const key = modelKey(provider, id);
if (keys.has(key)) return;
keys.add(key);
out.push({ provider, id, name: entry.name });
};
for (const entry of allowedModelCatalog) push(entry);
for (const rawKey of Object.keys(
params.cfg.agents?.defaults?.models ?? {},
)) {
const resolved = resolveModelRefFromString({
raw: String(rawKey),
defaultProvider,
aliasIndex,
});
if (!resolved) continue;
push({
provider: resolved.ref.provider,
id: resolved.ref.model,
name: resolved.ref.model,
});
}
if (resolvedDefault.model) {
push({
provider: resolvedDefault.provider,
id: resolvedDefault.model,
name: resolvedDefault.model,
});
}
return out;
})();
const items = buildModelPickerItems(pickerCatalog);
const index = Number.parseInt(raw, 10) - 1;
const item = Number.isFinite(index) ? items[index] : undefined;
if (!item) {
return {
text: `Invalid model selection "${raw}". Use /model to list.`,
};
}
const picked = pickProviderForModel({
item,
preferredProvider: params.provider,
});
if (!picked) {
return {
text: `Invalid model selection "${raw}". Use /model to list.`,
};
}
const key = `${picked.provider}/${picked.model}`;
const aliases = aliasIndex.byKey.get(key);
const alias = aliases && aliases.length > 0 ? aliases[0] : undefined;
modelSelection = {
provider: picked.provider,
model: picked.model,
isDefault:
picked.provider === defaultProvider && picked.model === defaultModel,
...(alias ? { alias } : {}),
};
} else {
const resolved = resolveModelDirectiveSelection({
raw,
defaultProvider,
defaultModel,
aliasIndex,
allowedModelKeys,
});
if (resolved.error) {
return { text: resolved.error };
}
modelSelection = resolved.selection;
}
if (modelSelection) {
if (directives.rawModelProfile) {
const profileResolved = resolveProfileOverride({
rawProfile: directives.rawModelProfile,
provider: modelSelection.provider,
cfg: params.cfg,
agentDir,
});
if (profileResolved.error) {
return { text: profileResolved.error };
}
profileOverride = profileResolved.profileId;
}
const nextLabel = `${modelSelection.provider}/${modelSelection.model}`;
if (nextLabel !== initialModelLabel) {
enqueueSystemEvent(
formatModelSwitchEvent(nextLabel, modelSelection.alias),
{
sessionKey,
contextKey: `model:${nextLabel}`,
},
);
}
}
}
if (directives.rawModelProfile && !modelSelection) {
return { text: "Auth profile override requires a model selection." };
} }
const nextThinkLevel = directives.hasThinkDirective
? directives.thinkLevel
: ((sessionEntry?.thinkingLevel as ThinkLevel | undefined) ??
currentThinkLevel);
const shouldDowngradeXHigh =
!directives.hasThinkDirective &&
nextThinkLevel === "xhigh" &&
!supportsXHighThinking(resolvedProvider, resolvedModel);
if (sessionEntry && sessionStore && sessionKey) { if (sessionEntry && sessionStore && sessionKey) {
const prevElevatedLevel = const prevElevatedLevel =
currentElevatedLevel ?? currentElevatedLevel ??
@@ -1239,6 +1255,9 @@ export async function handleDirectiveOnly(params: {
if (directives.thinkLevel === "off") delete sessionEntry.thinkingLevel; if (directives.thinkLevel === "off") delete sessionEntry.thinkingLevel;
else sessionEntry.thinkingLevel = directives.thinkLevel; else sessionEntry.thinkingLevel = directives.thinkLevel;
} }
if (shouldDowngradeXHigh) {
sessionEntry.thinkingLevel = "high";
}
if (directives.hasVerboseDirective && directives.verboseLevel) { if (directives.hasVerboseDirective && directives.verboseLevel) {
applyVerboseOverride(sessionEntry, directives.verboseLevel); applyVerboseOverride(sessionEntry, directives.verboseLevel);
} }
@@ -1295,6 +1314,18 @@ export async function handleDirectiveOnly(params: {
if (storePath) { if (storePath) {
await saveSessionStore(storePath, sessionStore); await saveSessionStore(storePath, sessionStore);
} }
if (modelSelection) {
const nextLabel = `${modelSelection.provider}/${modelSelection.model}`;
if (nextLabel !== initialModelLabel) {
enqueueSystemEvent(
formatModelSwitchEvent(nextLabel, modelSelection.alias),
{
sessionKey,
contextKey: `model:${nextLabel}`,
},
);
}
}
if (elevatedChanged) { if (elevatedChanged) {
const nextElevated = (sessionEntry.elevatedLevel ?? const nextElevated = (sessionEntry.elevatedLevel ??
"off") as ElevatedLevel; "off") as ElevatedLevel;
@@ -1345,6 +1376,11 @@ export async function handleDirectiveOnly(params: {
); );
if (shouldHintDirectRuntime) parts.push(formatElevatedRuntimeHint()); if (shouldHintDirectRuntime) parts.push(formatElevatedRuntimeHint());
} }
if (shouldDowngradeXHigh) {
parts.push(
`Thinking level set to high (xhigh not supported for ${resolvedProvider}/${resolvedModel}).`,
);
}
if (modelSelection) { if (modelSelection) {
const label = `${modelSelection.provider}/${modelSelection.model}`; const label = `${modelSelection.provider}/${modelSelection.model}`;
const labelWithAlias = modelSelection.alias const labelWithAlias = modelSelection.alias
+2 -2
View File
@@ -122,7 +122,7 @@ describe("buildStatusMessage", () => {
sessionId: "override-1", sessionId: "override-1",
updatedAt: 0, updatedAt: 0,
providerOverride: "openai", providerOverride: "openai",
modelOverride: "gpt-4.1-mini", modelOverride: "gpt-5-nano",
modelProvider: "anthropic", modelProvider: "anthropic",
model: "claude-haiku-4-5", model: "claude-haiku-4-5",
contextTokens: 32_000, contextTokens: 32_000,
@@ -133,7 +133,7 @@ describe("buildStatusMessage", () => {
modelAuth: "api-key", modelAuth: "api-key",
}); });
expect(normalizeTestText(text)).toContain("Model: openai/gpt-4.1-mini"); expect(normalizeTestText(text)).toContain("Model: openai/gpt-5-nano");
}); });
it("keeps provider prefix from configured model", () => { it("keeps provider prefix from configured model", () => {
+23 -1
View File
@@ -1,10 +1,32 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { normalizeReasoningLevel, normalizeThinkLevel } from "./thinking.js"; import {
listThinkingLevels,
normalizeReasoningLevel,
normalizeThinkLevel,
} from "./thinking.js";
describe("normalizeThinkLevel", () => { describe("normalizeThinkLevel", () => {
it("accepts mid as medium", () => { it("accepts mid as medium", () => {
expect(normalizeThinkLevel("mid")).toBe("medium"); expect(normalizeThinkLevel("mid")).toBe("medium");
}); });
it("accepts xhigh", () => {
expect(normalizeThinkLevel("xhigh")).toBe("xhigh");
});
});
describe("listThinkingLevels", () => {
it("includes xhigh for codex models", () => {
expect(listThinkingLevels(undefined, "gpt-5.2-codex")).toContain("xhigh");
});
it("includes xhigh for openai gpt-5.2", () => {
expect(listThinkingLevels("openai", "gpt-5.2")).toContain("xhigh");
});
it("excludes xhigh for non-codex models", () => {
expect(listThinkingLevels(undefined, "gpt-5-nano")).not.toContain("xhigh");
});
}); });
describe("normalizeReasoningLevel", () => { describe("normalizeReasoningLevel", () => {
+61 -1
View File
@@ -1,9 +1,30 @@
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high"; export type ThinkLevel =
| "off"
| "minimal"
| "low"
| "medium"
| "high"
| "xhigh";
export type VerboseLevel = "off" | "on"; export type VerboseLevel = "off" | "on";
export type ElevatedLevel = "off" | "on"; export type ElevatedLevel = "off" | "on";
export type ReasoningLevel = "off" | "on" | "stream"; export type ReasoningLevel = "off" | "on" | "stream";
export type UsageDisplayLevel = "off" | "on"; export type UsageDisplayLevel = "off" | "on";
export const XHIGH_MODEL_REFS = [
"openai/gpt-5.2",
"openai-codex/gpt-5.2-codex",
"openai-codex/gpt-5.1-codex",
] as const;
const XHIGH_MODEL_SET = new Set(
XHIGH_MODEL_REFS.map((entry) => entry.toLowerCase()),
);
const XHIGH_MODEL_IDS = new Set(
XHIGH_MODEL_REFS.map((entry) => entry.split("/")[1]?.toLowerCase()).filter(
(entry): entry is string => Boolean(entry),
),
);
// Normalize user-provided thinking level strings to the canonical enum. // Normalize user-provided thinking level strings to the canonical enum.
export function normalizeThinkLevel( export function normalizeThinkLevel(
raw?: string | null, raw?: string | null,
@@ -32,10 +53,49 @@ export function normalizeThinkLevel(
].includes(key) ].includes(key)
) )
return "high"; return "high";
if (["xhigh", "x-high", "x_high"].includes(key)) return "xhigh";
if (["think"].includes(key)) return "minimal"; if (["think"].includes(key)) return "minimal";
return undefined; return undefined;
} }
export function supportsXHighThinking(
provider?: string | null,
model?: string | null,
): boolean {
const modelKey = model?.trim().toLowerCase();
if (!modelKey) return false;
const providerKey = provider?.trim().toLowerCase();
if (providerKey) {
return XHIGH_MODEL_SET.has(`${providerKey}/${modelKey}`);
}
return XHIGH_MODEL_IDS.has(modelKey);
}
export function listThinkingLevels(
provider?: string | null,
model?: string | null,
): ThinkLevel[] {
const levels: ThinkLevel[] = ["off", "minimal", "low", "medium", "high"];
if (supportsXHighThinking(provider, model)) levels.push("xhigh");
return levels;
}
export function formatThinkingLevels(
provider?: string | null,
model?: string | null,
separator = ", ",
): string {
return listThinkingLevels(provider, model).join(separator);
}
export function formatXHighModelHint(): string {
const refs = [...XHIGH_MODEL_REFS] as string[];
if (refs.length === 0) return "unknown model";
if (refs.length === 1) return refs[0];
if (refs.length === 2) return `${refs[0]} or ${refs[1]}`;
return `${refs.slice(0, -1).join(", ")} or ${refs[refs.length - 1]}`;
}
// Normalize verbose flags used to toggle agent verbosity. // Normalize verbose flags used to toggle agent verbosity.
export function normalizeVerboseLevel( export function normalizeVerboseLevel(
raw?: string | null, raw?: string | null,
+3 -3
View File
@@ -152,10 +152,10 @@ describe("agentCommand", () => {
await withTempHome(async (home) => { await withTempHome(async (home) => {
const store = path.join(home, "sessions.json"); const store = path.join(home, "sessions.json");
mockConfig(home, store, { mockConfig(home, store, {
model: { primary: "openai/gpt-4.1-mini" }, model: { primary: "openai/gpt-5-nano" },
models: { models: {
"anthropic/claude-opus-4-5": {}, "anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {}, "openai/gpt-5-nano": {},
}, },
}); });
@@ -163,7 +163,7 @@ describe("agentCommand", () => {
const callArgs = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]; const callArgs = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0];
expect(callArgs?.provider).toBe("openai"); expect(callArgs?.provider).toBe("openai");
expect(callArgs?.model).toBe("gpt-4.1-mini"); expect(callArgs?.model).toBe("gpt-5-nano");
}); });
}); });
+37 -2
View File
@@ -30,8 +30,11 @@ import { hasNonzeroUsage } from "../agents/usage.js";
import { ensureAgentWorkspace } from "../agents/workspace.js"; import { ensureAgentWorkspace } from "../agents/workspace.js";
import type { MsgContext } from "../auto-reply/templating.js"; import type { MsgContext } from "../auto-reply/templating.js";
import { import {
formatThinkingLevels,
formatXHighModelHint,
normalizeThinkLevel, normalizeThinkLevel,
normalizeVerboseLevel, normalizeVerboseLevel,
supportsXHighThinking,
type ThinkLevel, type ThinkLevel,
type VerboseLevel, type VerboseLevel,
} from "../auto-reply/thinking.js"; } from "../auto-reply/thinking.js";
@@ -216,17 +219,26 @@ export async function agentCommand(
ensureBootstrapFiles: !agentCfg?.skipBootstrap, ensureBootstrapFiles: !agentCfg?.skipBootstrap,
}); });
const workspaceDir = workspace.dir; const workspaceDir = workspace.dir;
const configuredModel = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const thinkingLevelsHint = formatThinkingLevels(
configuredModel.provider,
configuredModel.model,
);
const thinkOverride = normalizeThinkLevel(opts.thinking); const thinkOverride = normalizeThinkLevel(opts.thinking);
const thinkOnce = normalizeThinkLevel(opts.thinkingOnce); const thinkOnce = normalizeThinkLevel(opts.thinkingOnce);
if (opts.thinking && !thinkOverride) { if (opts.thinking && !thinkOverride) {
throw new Error( throw new Error(
"Invalid thinking level. Use one of: off, minimal, low, medium, high.", `Invalid thinking level. Use one of: ${thinkingLevelsHint}.`,
); );
} }
if (opts.thinkingOnce && !thinkOnce) { if (opts.thinkingOnce && !thinkOnce) {
throw new Error( throw new Error(
"Invalid one-shot thinking level. Use one of: off, minimal, low, medium, high.", `Invalid one-shot thinking level. Use one of: ${thinkingLevelsHint}.`,
); );
} }
@@ -444,6 +456,29 @@ export async function agentCommand(
catalog: catalogForThinking, catalog: catalogForThinking,
}); });
} }
if (
resolvedThinkLevel === "xhigh" &&
!supportsXHighThinking(provider, model)
) {
const explicitThink = Boolean(thinkOnce || thinkOverride);
if (explicitThink) {
throw new Error(
`Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`,
);
}
resolvedThinkLevel = "high";
if (
sessionEntry &&
sessionStore &&
sessionKey &&
sessionEntry.thinkingLevel === "xhigh"
) {
sessionEntry.thinkingLevel = "high";
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
await saveSessionStore(storePath, sessionStore);
}
}
const sessionFile = resolveSessionFilePath(sessionId, sessionEntry, { const sessionFile = resolveSessionFilePath(sessionId, sessionEntry, {
agentId: sessionAgentId, agentId: sessionAgentId,
}); });
+1 -1
View File
@@ -27,7 +27,7 @@ describe("agents helpers", () => {
name: "Work", name: "Work",
workspace: "/work-ws", workspace: "/work-ws",
agentDir: "/state/agents/work/agent", agentDir: "/state/agents/work/agent",
model: "openai/gpt-4.1", model: "openai/gpt-5-nano",
}, },
], ],
}, },
+6 -6
View File
@@ -157,8 +157,8 @@ describe("models list/status", () => {
}, },
{ {
provider: "openai", provider: "openai",
id: "gpt-4.1-mini", id: "gpt-5-nano",
name: "GPT-4.1 mini", name: "GPT-5 Nano",
input: ["text"], input: ["text"],
baseUrl: "https://api.openai.com/v1", baseUrl: "https://api.openai.com/v1",
contextWindow: 128000, contextWindow: 128000,
@@ -199,8 +199,8 @@ describe("models list/status", () => {
}, },
{ {
provider: "openai", provider: "openai",
id: "gpt-4.1-mini", id: "gpt-5-nano",
name: "GPT-4.1 mini", name: "GPT-5 Nano",
input: ["text"], input: ["text"],
baseUrl: "https://api.openai.com/v1", baseUrl: "https://api.openai.com/v1",
contextWindow: 128000, contextWindow: 128000,
@@ -241,8 +241,8 @@ describe("models list/status", () => {
}, },
{ {
provider: "openai", provider: "openai",
id: "gpt-4.1-mini", id: "gpt-5-nano",
name: "GPT-4.1 mini", name: "GPT-5 Nano",
input: ["text"], input: ["text"],
baseUrl: "https://api.openai.com/v1", baseUrl: "https://api.openai.com/v1",
contextWindow: 128000, contextWindow: 128000,
+6 -6
View File
@@ -1603,10 +1603,10 @@ describe("legacy config detection", () => {
const res = migrateLegacyConfig({ const res = migrateLegacyConfig({
agent: { agent: {
model: "anthropic/claude-opus-4-5", model: "anthropic/claude-opus-4-5",
modelFallbacks: ["openai/gpt-4.1-mini"], modelFallbacks: ["openai/gpt-5-nano"],
imageModel: "openai/gpt-4.1-mini", imageModel: "openai/gpt-5-nano",
imageModelFallbacks: ["anthropic/claude-opus-4-5"], imageModelFallbacks: ["anthropic/claude-opus-4-5"],
allowedModels: ["anthropic/claude-opus-4-5", "openai/gpt-4.1-mini"], allowedModels: ["anthropic/claude-opus-4-5", "openai/gpt-5-nano"],
modelAliases: { Opus: "anthropic/claude-opus-4-5" }, modelAliases: { Opus: "anthropic/claude-opus-4-5" },
}, },
}); });
@@ -1615,10 +1615,10 @@ describe("legacy config detection", () => {
"anthropic/claude-opus-4-5", "anthropic/claude-opus-4-5",
); );
expect(res.config?.agents?.defaults?.model?.fallbacks).toEqual([ expect(res.config?.agents?.defaults?.model?.fallbacks).toEqual([
"openai/gpt-4.1-mini", "openai/gpt-5-nano",
]); ]);
expect(res.config?.agents?.defaults?.imageModel?.primary).toBe( expect(res.config?.agents?.defaults?.imageModel?.primary).toBe(
"openai/gpt-4.1-mini", "openai/gpt-5-nano",
); );
expect(res.config?.agents?.defaults?.imageModel?.fallbacks).toEqual([ expect(res.config?.agents?.defaults?.imageModel?.fallbacks).toEqual([
"anthropic/claude-opus-4-5", "anthropic/claude-opus-4-5",
@@ -1627,7 +1627,7 @@ describe("legacy config detection", () => {
res.config?.agents?.defaults?.models?.["anthropic/claude-opus-4-5"], res.config?.agents?.defaults?.models?.["anthropic/claude-opus-4-5"],
).toMatchObject({ alias: "Opus" }); ).toMatchObject({ alias: "Opus" });
expect( expect(
res.config?.agents?.defaults?.models?.["openai/gpt-4.1-mini"], res.config?.agents?.defaults?.models?.["openai/gpt-5-nano"],
).toBeTruthy(); ).toBeTruthy();
expect(res.config?.agent).toBeUndefined(); expect(res.config?.agent).toBeUndefined();
}); });
+1 -1
View File
@@ -1629,7 +1629,7 @@ export type AgentDefaultsConfig = {
/** Vector memory search configuration (per-agent overrides supported). */ /** Vector memory search configuration (per-agent overrides supported). */
memorySearch?: MemorySearchConfig; memorySearch?: MemorySearchConfig;
/** Default thinking level when no /think directive is present. */ /** Default thinking level when no /think directive is present. */
thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high"; thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
/** Default verbose level when no /verbose directive is present. */ /** Default verbose level when no /verbose directive is present. */
verboseDefault?: "off" | "on"; verboseDefault?: "off" | "on";
/** Default elevated level when no /elevated directive is present. */ /** Default elevated level when no /elevated directive is present. */
+1
View File
@@ -1248,6 +1248,7 @@ const AgentDefaultsSchema = z
z.literal("low"), z.literal("low"),
z.literal("medium"), z.literal("medium"),
z.literal("high"), z.literal("high"),
z.literal("xhigh"),
]) ])
.optional(), .optional(),
verboseDefault: z.union([z.literal("off"), z.literal("on")]).optional(), verboseDefault: z.union([z.literal("off"), z.literal("on")]).optional(),
+2 -2
View File
@@ -213,7 +213,7 @@ describe("runCronIsolatedAgentTurn", () => {
job: makeJob({ job: makeJob({
kind: "agentTurn", kind: "agentTurn",
message: "do it", message: "do it",
model: "openai/gpt-4.1-mini", model: "openai/gpt-5-nano",
}), }),
message: "do it", message: "do it",
sessionKey: "cron:job-1", sessionKey: "cron:job-1",
@@ -226,7 +226,7 @@ describe("runCronIsolatedAgentTurn", () => {
model?: string; model?: string;
}; };
expect(call?.provider).toBe("openai"); expect(call?.provider).toBe("openai");
expect(call?.model).toBe("gpt-4.1-mini"); expect(call?.model).toBe("gpt-5-nano");
}); });
}); });
+10 -1
View File
@@ -32,7 +32,11 @@ import {
DEFAULT_HEARTBEAT_ACK_MAX_CHARS, DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
stripHeartbeatToken, stripHeartbeatToken,
} from "../auto-reply/heartbeat.js"; } from "../auto-reply/heartbeat.js";
import { normalizeThinkLevel } from "../auto-reply/thinking.js"; import {
formatXHighModelHint,
normalizeThinkLevel,
supportsXHighThinking,
} from "../auto-reply/thinking.js";
import type { CliDeps } from "../cli/deps.js"; import type { CliDeps } from "../cli/deps.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { import {
@@ -367,6 +371,11 @@ export async function runCronIsolatedAgentTurn(params: {
catalog: await loadCatalog(), catalog: await loadCatalog(),
}); });
} }
if (thinkLevel === "xhigh" && !supportsXHighThinking(provider, model)) {
throw new Error(
`Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`,
);
}
const timeoutMs = resolveAgentTimeoutMs({ const timeoutMs = resolveAgentTimeoutMs({
cfg: cfgWithAgentDefaults, cfg: cfgWithAgentDefaults,
+2 -2
View File
@@ -46,7 +46,7 @@ describe("hooks mapping", () => {
match: { path: "gmail" }, match: { path: "gmail" },
action: "agent", action: "agent",
messageTemplate: "Subject: {{messages[0].subject}}", messageTemplate: "Subject: {{messages[0].subject}}",
model: "openai/gpt-4.1-mini", model: "openai/gpt-5-nano",
}, },
], ],
}); });
@@ -58,7 +58,7 @@ describe("hooks mapping", () => {
}); });
expect(result?.ok).toBe(true); expect(result?.ok).toBe(true);
if (result?.ok && result.action.kind === "agent") { if (result?.ok && result.action.kind === "agent") {
expect(result.action.model).toBe("openai/gpt-4.1-mini"); expect(result.action.model).toBe("openai/gpt-5-nano");
} }
}); });
+2 -2
View File
@@ -88,7 +88,7 @@ describe("gateway server hooks", () => {
body: JSON.stringify({ body: JSON.stringify({
message: "Do it", message: "Do it",
name: "Email", name: "Email",
model: "openai/gpt-4.1-mini", model: "openai/gpt-5-nano",
}), }),
}); });
expect(res.status).toBe(202); expect(res.status).toBe(202);
@@ -96,7 +96,7 @@ describe("gateway server hooks", () => {
const call = cronIsolatedRun.mock.calls[0]?.[0] as { const call = cronIsolatedRun.mock.calls[0]?.[0] as {
job?: { payload?: { model?: string } }; job?: { payload?: { model?: string } };
}; };
expect(call?.job?.payload?.model).toBe("openai/gpt-4.1-mini"); expect(call?.job?.payload?.model).toBe("openai/gpt-5-nano");
drainSystemEvents(resolveMainKey()); drainSystemEvents(resolveMainKey());
await server.close(); await server.close();
}); });
+31 -1
View File
@@ -8,10 +8,13 @@ import {
} from "../agents/model-selection.js"; } from "../agents/model-selection.js";
import { normalizeGroupActivation } from "../auto-reply/group-activation.js"; import { normalizeGroupActivation } from "../auto-reply/group-activation.js";
import { import {
formatThinkingLevels,
formatXHighModelHint,
normalizeElevatedLevel, normalizeElevatedLevel,
normalizeReasoningLevel, normalizeReasoningLevel,
normalizeThinkLevel, normalizeThinkLevel,
normalizeUsageDisplay, normalizeUsageDisplay,
supportsXHighThinking,
} from "../auto-reply/thinking.js"; } from "../auto-reply/thinking.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import type { SessionEntry } from "../config/sessions.js"; import type { SessionEntry } from "../config/sessions.js";
@@ -95,8 +98,17 @@ export async function applySessionsPatchToStore(params: {
} else if (raw !== undefined) { } else if (raw !== undefined) {
const normalized = normalizeThinkLevel(String(raw)); const normalized = normalizeThinkLevel(String(raw));
if (!normalized) { if (!normalized) {
const resolvedDefault = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const hintProvider =
existing?.providerOverride?.trim() || resolvedDefault.provider;
const hintModel =
existing?.modelOverride?.trim() || resolvedDefault.model;
return invalid( return invalid(
"invalid thinkingLevel (use off|minimal|low|medium|high)", `invalid thinkingLevel (use ${formatThinkingLevels(hintProvider, hintModel, "|")})`,
); );
} }
if (normalized === "off") delete next.thinkingLevel; if (normalized === "off") delete next.thinkingLevel;
@@ -196,6 +208,24 @@ export async function applySessionsPatchToStore(params: {
} }
} }
if (next.thinkingLevel === "xhigh") {
const resolvedDefault = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const effectiveProvider = next.providerOverride ?? resolvedDefault.provider;
const effectiveModel = next.modelOverride ?? resolvedDefault.model;
if (!supportsXHighThinking(effectiveProvider, effectiveModel)) {
if ("thinkingLevel" in patch) {
return invalid(
`thinkingLevel "xhigh" is only supported for ${formatXHighModelHint()}`,
);
}
next.thinkingLevel = "high";
}
}
if ("sendPolicy" in patch) { if ("sendPolicy" in patch) {
const raw = patch.sendPolicy; const raw = patch.sendPolicy;
if (raw === null) { if (raw === null) {
+4 -4
View File
@@ -171,7 +171,7 @@ describe("google-shared convertMessages", () => {
{ {
type: "thinking", type: "thinking",
thinking: "hidden", thinking: "hidden",
thinkingSignature: "sig", thinkingSignature: "c2ln",
}, },
], ],
api: "google-generative-ai", api: "google-generative-ai",
@@ -202,7 +202,7 @@ describe("google-shared convertMessages", () => {
expect(contents[0].role).toBe("model"); expect(contents[0].role).toBe("model");
expect(contents[0].parts?.[0]).toMatchObject({ expect(contents[0].parts?.[0]).toMatchObject({
thought: true, thought: true,
thoughtSignature: "sig", thoughtSignature: "c2ln",
}); });
}); });
@@ -216,7 +216,7 @@ describe("google-shared convertMessages", () => {
{ {
type: "thinking", type: "thinking",
thinking: "structured", thinking: "structured",
thinkingSignature: "sig", thinkingSignature: "c2ln",
}, },
], ],
api: "google-generative-ai", api: "google-generative-ai",
@@ -247,7 +247,7 @@ describe("google-shared convertMessages", () => {
expect(parts).toHaveLength(1); expect(parts).toHaveLength(1);
expect(parts[0]).toMatchObject({ expect(parts[0]).toMatchObject({
thought: true, thought: true,
thoughtSignature: "sig", thoughtSignature: "c2ln",
}); });
}); });
+23 -7
View File
@@ -1,6 +1,9 @@
import type { SlashCommand } from "@mariozechner/pi-tui"; import type { SlashCommand } from "@mariozechner/pi-tui";
import {
formatThinkingLevels,
listThinkingLevels,
} from "../auto-reply/thinking.js";
const THINK_LEVELS = ["off", "minimal", "low", "medium", "high"];
const VERBOSE_LEVELS = ["on", "off"]; const VERBOSE_LEVELS = ["on", "off"];
const REASONING_LEVELS = ["on", "off"]; const REASONING_LEVELS = ["on", "off"];
const ELEVATED_LEVELS = ["on", "off"]; const ELEVATED_LEVELS = ["on", "off"];
@@ -12,6 +15,11 @@ export type ParsedCommand = {
args: string; args: string;
}; };
export type SlashCommandOptions = {
provider?: string;
model?: string;
};
const COMMAND_ALIASES: Record<string, string> = { const COMMAND_ALIASES: Record<string, string> = {
elev: "elevated", elev: "elevated",
}; };
@@ -27,7 +35,10 @@ export function parseCommand(input: string): ParsedCommand {
}; };
} }
export function getSlashCommands(): SlashCommand[] { export function getSlashCommands(
options: SlashCommandOptions = {},
): SlashCommand[] {
const thinkLevels = listThinkingLevels(options.provider, options.model);
return [ return [
{ name: "help", description: "Show slash command help" }, { name: "help", description: "Show slash command help" },
{ name: "status", description: "Show gateway status summary" }, { name: "status", description: "Show gateway status summary" },
@@ -44,9 +55,9 @@ export function getSlashCommands(): SlashCommand[] {
name: "think", name: "think",
description: "Set thinking level", description: "Set thinking level",
getArgumentCompletions: (prefix) => getArgumentCompletions: (prefix) =>
THINK_LEVELS.filter((v) => v.startsWith(prefix.toLowerCase())).map( thinkLevels
(value) => ({ value, label: value }), .filter((v) => v.startsWith(prefix.toLowerCase()))
), .map((value) => ({ value, label: value })),
}, },
{ {
name: "verbose", name: "verbose",
@@ -105,7 +116,12 @@ export function getSlashCommands(): SlashCommand[] {
]; ];
} }
export function helpText(): string { export function helpText(options: SlashCommandOptions = {}): string {
const thinkLevels = formatThinkingLevels(
options.provider,
options.model,
"|",
);
return [ return [
"Slash commands:", "Slash commands:",
"/help", "/help",
@@ -113,7 +129,7 @@ export function helpText(): string {
"/agent <id> (or /agents)", "/agent <id> (or /agents)",
"/session <key> (or /sessions)", "/session <key> (or /sessions)",
"/model <provider/model> (or /models)", "/model <provider/model> (or /models)",
"/think <off|minimal|low|medium|high>", `/think <${thinkLevels}>`,
"/verbose <on|off>", "/verbose <on|off>",
"/reasoning <on|off>", "/reasoning <on|off>",
"/cost <on|off>", "/cost <on|off>",
+30 -6
View File
@@ -7,7 +7,10 @@ import {
TUI, TUI,
} from "@mariozechner/pi-tui"; } from "@mariozechner/pi-tui";
import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import { normalizeUsageDisplay } from "../auto-reply/thinking.js"; import {
formatThinkingLevels,
normalizeUsageDisplay,
} from "../auto-reply/thinking.js";
import { loadConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js";
import { formatAge } from "../infra/provider-summary.js"; import { formatAge } from "../infra/provider-summary.js";
import { import {
@@ -239,6 +242,18 @@ export async function runTui(opts: TuiOptions) {
root.addChild(footer); root.addChild(footer);
root.addChild(editor); root.addChild(editor);
const updateAutocompleteProvider = () => {
editor.setAutocompleteProvider(
new CombinedAutocompleteProvider(
getSlashCommands({
provider: sessionInfo.modelProvider,
model: sessionInfo.model,
}),
process.cwd(),
),
);
};
const tui = new TUI(new ProcessTerminal()); const tui = new TUI(new ProcessTerminal());
tui.addChild(root); tui.addChild(root);
tui.setFocus(editor); tui.setFocus(editor);
@@ -524,6 +539,7 @@ export async function runTui(opts: TuiOptions) {
} catch (err) { } catch (err) {
chatLog.addSystem(`sessions list failed: ${String(err)}`); chatLog.addSystem(`sessions list failed: ${String(err)}`);
} }
updateAutocompleteProvider();
updateFooter(); updateFooter();
tui.requestRender(); tui.requestRender();
}; };
@@ -861,7 +877,12 @@ export async function runTui(opts: TuiOptions) {
if (!name) return; if (!name) return;
switch (name) { switch (name) {
case "help": case "help":
chatLog.addSystem(helpText()); chatLog.addSystem(
helpText({
provider: sessionInfo.modelProvider,
model: sessionInfo.model,
}),
);
break; break;
case "status": case "status":
try { try {
@@ -921,7 +942,12 @@ export async function runTui(opts: TuiOptions) {
break; break;
case "think": case "think":
if (!args) { if (!args) {
chatLog.addSystem("usage: /think <off|minimal|low|medium|high>"); const levels = formatThinkingLevels(
sessionInfo.modelProvider,
sessionInfo.model,
"|",
);
chatLog.addSystem(`usage: /think <${levels}>`);
break; break;
} }
try { try {
@@ -1071,9 +1097,7 @@ export async function runTui(opts: TuiOptions) {
tui.requestRender(); tui.requestRender();
}; };
editor.setAutocompleteProvider( updateAutocompleteProvider();
new CombinedAutocompleteProvider(getSlashCommands(), process.cwd()),
);
editor.onSubmit = (text) => { editor.onSubmit = (text) => {
const value = text.trim(); const value = text.trim();
editor.setText(""); editor.setText("");
+2 -2
View File
@@ -15,10 +15,10 @@
"vite": "7.3.1" "vite": "7.3.1"
}, },
"devDependencies": { "devDependencies": {
"@vitest/browser-playwright": "4.0.16", "@vitest/browser-playwright": "4.0.17",
"playwright": "^1.57.0", "playwright": "^1.57.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "4.0.16" "vitest": "4.0.17"
}, },
"pnpm": { "pnpm": {
"minimumReleaseAge": 2880 "minimumReleaseAge": 2880