Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0a79c19f9 | ||
|
|
cb0f6cefa4 | ||
|
|
25ef01b74a | ||
|
|
6db0201fcd | ||
|
|
40e508823f | ||
|
|
18d22aa426 | ||
|
|
a3641526ab |
No files matched your search
@@ -1,5 +1,12 @@
|
||||
# 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
|
||||
|
||||
### Changes
|
||||
@@ -8,6 +15,7 @@
|
||||
- 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)
|
||||
- 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
|
||||
- Gateway: honor `CLAWDBOT_LAUNCHD_LABEL` / `CLAWDBOT_SYSTEMD_UNIT` overrides when checking or restarting the daemon.
|
||||
|
||||
@@ -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)
|
||||
- `/new` or `/reset` — reset the session
|
||||
- `/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`
|
||||
- `/cost on|off` — append per-response token/cost usage lines
|
||||
- `/restart` — restart the gateway (owner-only in groups)
|
||||
|
||||
@@ -105,7 +105,7 @@ Isolation options (only for `session=isolated`):
|
||||
### Model and thinking overrides
|
||||
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`)
|
||||
- `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
|
||||
session model. We recommend model overrides only for isolated jobs to avoid
|
||||
|
||||
@@ -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
@@ -398,7 +398,7 @@ Required:
|
||||
Options:
|
||||
- `--to <dest>` (for session key and optional delivery)
|
||||
- `--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>`
|
||||
- `--provider <whatsapp|telegram|discord|slack|signal|imessage>`
|
||||
- `--local`
|
||||
|
||||
@@ -39,7 +39,7 @@ Add `hooks.gmail.model` config option to specify an optional cheaper model for G
|
||||
| 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.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
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ model as `provider/model`.
|
||||
- [Z.AI](/providers/zai)
|
||||
- [GLM models](/providers/glm)
|
||||
- [MiniMax](/providers/minimax)
|
||||
- [Amazon Bedrock](/bedrock)
|
||||
|
||||
For the full provider catalog (xAI, Groq, Mistral, etc.) and advanced configuration,
|
||||
see [Model providers](/concepts/model-providers).
|
||||
+85
-1
@@ -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).
|
||||
|
||||
## 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
|
||||
|
||||
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?
|
||||
|
||||
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?
|
||||
|
||||
|
||||
@@ -38,7 +38,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`)
|
||||
- `--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
|
||||
- `--timeout <seconds>`: override agent timeout
|
||||
- `--json`: output structured JSON
|
||||
@@ -60,7 +60,7 @@ Text + native (when enabled):
|
||||
- `/activation mention|always` (groups only)
|
||||
- `/send on|off|inherit` (owner-only)
|
||||
- `/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`)
|
||||
- `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only)
|
||||
- `/elevated on|off` (alias: `/elev`)
|
||||
|
||||
@@ -7,11 +7,12 @@ read_when:
|
||||
|
||||
## What it does
|
||||
- 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”
|
||||
- low → “think hard”
|
||||
- medium → “think harder”
|
||||
- high → “ultrathink” (max budget)
|
||||
- xhigh → “ultrathink+” (GPT-5.2 + Codex models only)
|
||||
- `highest`, `max` map to `high`.
|
||||
|
||||
## Resolution order
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ Use SSH tunneling or Tailscale to reach the Gateway WS.
|
||||
- `/agent <id>` (or `/agents`)
|
||||
- `/session <key>` (or `/sessions`)
|
||||
- `/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>`
|
||||
- `/reasoning <on|off|stream>` (stream = Telegram draft only)
|
||||
- `/cost <on|off>`
|
||||
|
||||
+5
-5
@@ -132,10 +132,10 @@
|
||||
"@grammyjs/runner": "^2.0.3",
|
||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||
"@homebridge/ciao": "^1.3.4",
|
||||
"@mariozechner/pi-agent-core": "^0.43.0",
|
||||
"@mariozechner/pi-ai": "^0.43.0",
|
||||
"@mariozechner/pi-coding-agent": "^0.43.0",
|
||||
"@mariozechner/pi-tui": "^0.43.0",
|
||||
"@mariozechner/pi-agent-core": "^0.45.3",
|
||||
"@mariozechner/pi-ai": "^0.45.3",
|
||||
"@mariozechner/pi-coding-agent": "^0.45.3",
|
||||
"@mariozechner/pi-tui": "^0.45.3",
|
||||
"@microsoft/agents-hosting": "^1.1.1",
|
||||
"@microsoft/agents-hosting-express": "^1.1.1",
|
||||
"@microsoft/agents-hosting-extensions-teams": "^1.1.1",
|
||||
@@ -207,7 +207,7 @@
|
||||
"@sinclair/typebox": "0.34.47"
|
||||
},
|
||||
"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": {
|
||||
|
||||
+6
-39
@@ -1,8 +1,8 @@
|
||||
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
|
||||
+++ 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
|
||||
}
|
||||
const errorText = await response.text();
|
||||
@@ -14,41 +14,8 @@ index 12540bb1069087a0d0a2967f792008627b9f79d9..f30b525620e6d8e45146b439ec3733e4
|
||||
// Check if retryable
|
||||
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
|
||||
// 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
|
||||
index ad0a2aabbe10382cee4e463b68a02864dd235e57..8c001acfd0b4e0743181c246f1bedcf8cd2ffb02 100644
|
||||
index 7488c79..4c34587 100644
|
||||
--- a/dist/providers/openai-codex-responses.js
|
||||
+++ b/dist/providers/openai-codex-responses.js
|
||||
@@ -517,7 +517,7 @@ function convertTools(tools) {
|
||||
@@ -61,10 +28,10 @@ index ad0a2aabbe10382cee4e463b68a02864dd235e57..8c001acfd0b4e0743181c246f1bedcf8
|
||||
}
|
||||
function mapStopReason(status) {
|
||||
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
|
||||
+++ 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") {
|
||||
const output = [];
|
||||
@@ -81,7 +48,7 @@ index f07085c64390b211340d6a826b28ea9c2e77302f..7f758532246cc7b062df48e9cec4e6c9
|
||||
const reasoningItem = JSON.parse(block.thinkingSignature);
|
||||
output.push(reasoningItem);
|
||||
}
|
||||
@@ -434,6 +440,16 @@ function convertMessages(model, context) {
|
||||
@@ -438,6 +444,16 @@ function convertMessages(model, context) {
|
||||
});
|
||||
}
|
||||
}
|
||||
Generated
+1258
-315
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -10,4 +10,4 @@ onlyBuiltDependencies:
|
||||
- sharp
|
||||
|
||||
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
|
||||
@@ -57,7 +57,7 @@ describe("resolveAgentConfig", () => {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "anthropic/claude-sonnet-4",
|
||||
fallbacks: ["openai/gpt-4.1"],
|
||||
fallbacks: ["openai/gpt-5-nano"],
|
||||
},
|
||||
},
|
||||
list: [
|
||||
|
||||
@@ -8,7 +8,7 @@ function makeCfg(overrides: Partial<ClawdbotConfig> = {}): ClawdbotConfig {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "openai/gpt-4.1-mini",
|
||||
primary: "openai/gpt-5-nano",
|
||||
fallbacks: ["anthropic/claude-haiku-3-5"],
|
||||
},
|
||||
},
|
||||
@@ -29,7 +29,7 @@ describe("runWithModelFallback", () => {
|
||||
runWithModelFallback({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
run,
|
||||
}),
|
||||
).rejects.toThrow("bad request");
|
||||
@@ -46,7 +46,7 @@ describe("runWithModelFallback", () => {
|
||||
const result = await runWithModelFallback({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
run,
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ describe("runWithModelFallback", () => {
|
||||
const result = await runWithModelFallback({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
run,
|
||||
});
|
||||
|
||||
@@ -92,7 +92,7 @@ describe("runWithModelFallback", () => {
|
||||
const result = await runWithModelFallback({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
run,
|
||||
});
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("runWithModelFallback", () => {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "openai/gpt-4.1-mini",
|
||||
primary: "openai/gpt-5-nano",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -173,13 +173,13 @@ describe("runWithModelFallback", () => {
|
||||
cfg,
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
fallbacksOverride: ["openai/gpt-4.1"],
|
||||
fallbacksOverride: ["openai/gpt-5-nano"],
|
||||
run: async (provider, model) => {
|
||||
calls.push({ provider, model });
|
||||
if (provider === "anthropic") {
|
||||
throw Object.assign(new Error("nope"), { status: 401 });
|
||||
}
|
||||
if (provider === "openai" && model === "gpt-4.1") {
|
||||
if (provider === "openai" && model === "gpt-5-nano") {
|
||||
return "ok";
|
||||
}
|
||||
throw new Error(`unexpected candidate: ${provider}/${model}`);
|
||||
@@ -189,7 +189,7 @@ describe("runWithModelFallback", () => {
|
||||
expect(res.result).toBe("ok");
|
||||
expect(calls).toEqual([
|
||||
{ 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({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
run,
|
||||
});
|
||||
|
||||
@@ -254,7 +254,7 @@ describe("runWithModelFallback", () => {
|
||||
const result = await runWithModelFallback({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
run,
|
||||
});
|
||||
|
||||
@@ -269,7 +269,7 @@ describe("runWithModelFallback", () => {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "openai/gpt-4.1-mini",
|
||||
primary: "openai/gpt-5-nano",
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
@@ -292,6 +292,6 @@ describe("runWithModelFallback", () => {
|
||||
expect(result.result).toBe("ok");
|
||||
expect(run).toHaveBeenCalledTimes(2);
|
||||
expect(result.provider).toBe("openai");
|
||||
expect(result.model).toBe("gpt-4.1-mini");
|
||||
expect(result.model).toBe("gpt-5-nano");
|
||||
});
|
||||
});
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
const catalog = [
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-4",
|
||||
name: "GPT-4",
|
||||
id: "gpt-5-nano",
|
||||
name: "GPT-5 Nano",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("buildAllowedModelSet", () => {
|
||||
agents: {
|
||||
defaults: {
|
||||
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.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(
|
||||
true,
|
||||
);
|
||||
@@ -57,7 +59,9 @@ describe("buildAllowedModelSet", () => {
|
||||
});
|
||||
|
||||
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(
|
||||
true,
|
||||
);
|
||||
@@ -236,7 +240,7 @@ describe("resolveAllowedModelRef", () => {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/gpt-4": { alias: "GPT4" },
|
||||
"openai/gpt-5-nano": { alias: "GPT5NANO" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -244,12 +248,12 @@ describe("resolveAllowedModelRef", () => {
|
||||
const resolved = resolveAllowedModelRef({
|
||||
cfg,
|
||||
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" },
|
||||
],
|
||||
raw: "anthropic/claude-sonnet-4-1",
|
||||
defaultProvider: "openai",
|
||||
defaultModel: "gpt-4",
|
||||
defaultModel: "gpt-5-nano",
|
||||
});
|
||||
expect(resolved).toEqual({
|
||||
error: "model not allowed: anthropic/claude-sonnet-4-1",
|
||||
|
||||
@@ -7,7 +7,13 @@ export type ModelRef = {
|
||||
model: string;
|
||||
};
|
||||
|
||||
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high";
|
||||
export type ThinkLevel =
|
||||
| "off"
|
||||
| "minimal"
|
||||
| "low"
|
||||
| "medium"
|
||||
| "high"
|
||||
| "xhigh";
|
||||
|
||||
export type ModelAliasIndex = {
|
||||
byAlias: Map<string, { alias: string; ref: ModelRef }>;
|
||||
|
||||
@@ -207,11 +207,11 @@ describe("resolveExtraParams", () => {
|
||||
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({
|
||||
cfg: undefined,
|
||||
provider: "openai",
|
||||
modelId: "gpt-4",
|
||||
modelId: "gpt-5-nano",
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
@@ -223,7 +223,7 @@ describe("resolveExtraParams", () => {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/gpt-4": {
|
||||
"openai/gpt-5-nano": {
|
||||
params: {
|
||||
logprobs: true,
|
||||
top_logprobs: 5,
|
||||
@@ -234,7 +234,7 @@ describe("resolveExtraParams", () => {
|
||||
},
|
||||
},
|
||||
provider: "openai",
|
||||
modelId: "gpt-4",
|
||||
modelId: "gpt-5-nano",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
|
||||
@@ -1046,7 +1046,7 @@ export function resolveEmbeddedSessionLane(key: string) {
|
||||
}
|
||||
|
||||
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";
|
||||
return level;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("block streaming", () => {
|
||||
piEmbeddedMock.runEmbeddedPiAgent.mockReset();
|
||||
vi.mocked(loadModelCatalog).mockResolvedValue([
|
||||
{ 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" },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("directive behavior", () => {
|
||||
vi.mocked(loadModelCatalog).mockResolvedValue([
|
||||
{ id: "claude-opus-4-5", name: "Opus 4.5", 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();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
await withTempHome(async (home) => {
|
||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||
@@ -1564,7 +1655,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"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;
|
||||
expect(text).toContain("anthropic/claude-opus-4-5");
|
||||
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(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1596,7 +1687,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"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;
|
||||
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).toContain("auth:");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
@@ -1628,7 +1719,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"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;
|
||||
expect(text).toContain("Pick: /model <#> or /model <provider/model>");
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1660,7 +1751,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"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;
|
||||
expect(text).toContain("Pick: /model <#> or /model <provider/model>");
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1687,7 +1778,7 @@ describe("directive behavior", () => {
|
||||
id: "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");
|
||||
|
||||
@@ -1701,7 +1792,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
"minimax/MiniMax-M2.1": { alias: "minimax" },
|
||||
},
|
||||
},
|
||||
@@ -1722,7 +1813,7 @@ describe("directive behavior", () => {
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
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(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1762,7 +1853,7 @@ describe("directive behavior", () => {
|
||||
const storePath = path.join(home, "sessions.json");
|
||||
|
||||
await getReplyFromConfig(
|
||||
{ Body: "/model openai/gpt-4.1-mini", From: "+1222", To: "+1222" },
|
||||
{ Body: "/model openai/gpt-5-nano", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agents: {
|
||||
@@ -1771,7 +1862,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1780,7 +1871,7 @@ describe("directive behavior", () => {
|
||||
);
|
||||
|
||||
assertModelSelection(storePath, {
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
provider: "openai",
|
||||
});
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
@@ -1798,10 +1889,10 @@ describe("directive behavior", () => {
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
model: { primary: "openai/gpt-5-nano" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
@@ -2118,10 +2209,10 @@ describe("directive behavior", () => {
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
model: { primary: "openai/gpt-5-nano" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
@@ -2151,10 +2242,10 @@ describe("directive behavior", () => {
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
model: { primary: "openai/gpt-5-nano" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
@@ -2187,7 +2278,7 @@ describe("directive behavior", () => {
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
model: { primary: "openai/gpt-5-nano" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
@@ -2218,7 +2309,7 @@ describe("directive behavior", () => {
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
model: { primary: "openai/gpt-5-nano" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
},
|
||||
},
|
||||
@@ -2245,7 +2336,7 @@ describe("directive behavior", () => {
|
||||
|
||||
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",
|
||||
To: "+2000",
|
||||
},
|
||||
@@ -2257,7 +2348,7 @@ describe("directive behavior", () => {
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"openai/gpt-5-nano": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
name: "Claude Opus 4.5 (OpenRouter)",
|
||||
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-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" },
|
||||
{ provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
||||
@@ -320,7 +320,7 @@ describe("trigger handling", () => {
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "/model openai/gpt-4.1-mini",
|
||||
Body: "/model openai/gpt-5-nano",
|
||||
From: "telegram:111",
|
||||
To: "telegram:111",
|
||||
ChatType: "direct",
|
||||
@@ -336,11 +336,11 @@ describe("trigger handling", () => {
|
||||
);
|
||||
|
||||
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);
|
||||
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();
|
||||
|
||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||
@@ -368,7 +368,7 @@ describe("trigger handling", () => {
|
||||
expect(vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
model: "gpt-5-nano",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
+37
-2
@@ -29,7 +29,10 @@ import {
|
||||
type ClawdbotConfig,
|
||||
loadConfig,
|
||||
} from "../config/config.js";
|
||||
import { resolveSessionFilePath } from "../config/sessions.js";
|
||||
import {
|
||||
resolveSessionFilePath,
|
||||
saveSessionStore,
|
||||
} from "../config/sessions.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import { clearCommandLane, getQueueSize } from "../process/command-queue.js";
|
||||
import { getProviderDock } from "../providers/dock.js";
|
||||
@@ -94,8 +97,10 @@ import {
|
||||
import type { MsgContext, TemplateContext } from "./templating.js";
|
||||
import {
|
||||
type ElevatedLevel,
|
||||
formatXHighModelHint,
|
||||
normalizeThinkLevel,
|
||||
type ReasoningLevel,
|
||||
supportsXHighThinking,
|
||||
type ThinkLevel,
|
||||
type VerboseLevel,
|
||||
} from "./thinking.js";
|
||||
@@ -1187,7 +1192,10 @@ export async function getReplyFromConfig(
|
||||
if (!resolvedThinkLevel && prefixedCommandBody) {
|
||||
const parts = prefixedCommandBody.split(/\s+/);
|
||||
const maybeLevel = normalizeThinkLevel(parts[0]);
|
||||
if (maybeLevel) {
|
||||
if (
|
||||
maybeLevel &&
|
||||
(maybeLevel !== "xhigh" || supportsXHighThinking(provider, model))
|
||||
) {
|
||||
resolvedThinkLevel = maybeLevel;
|
||||
prefixedCommandBody = parts.slice(1).join(" ").trim();
|
||||
}
|
||||
@@ -1195,6 +1203,33 @@ export async function getReplyFromConfig(
|
||||
if (!resolvedThinkLevel) {
|
||||
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 sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry);
|
||||
const queueBodyBase = transcribedText
|
||||
|
||||
@@ -37,6 +37,11 @@ import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||
import { shortenHomePath } from "../../utils.js";
|
||||
import { extractModelDirective } from "../model.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import {
|
||||
formatThinkingLevels,
|
||||
formatXHighModelHint,
|
||||
supportsXHighThinking,
|
||||
} from "../thinking.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import {
|
||||
type ElevatedLevel,
|
||||
@@ -778,6 +783,7 @@ export async function handleDirectiveOnly(params: {
|
||||
allowedModelCatalog,
|
||||
resetModelOverride,
|
||||
provider,
|
||||
model,
|
||||
initialModelLabel,
|
||||
formatModelSwitchEvent,
|
||||
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 no argument was provided, show the current level
|
||||
if (!directives.rawThinkLevel) {
|
||||
@@ -950,12 +1067,12 @@ export async function handleDirectiveOnly(params: {
|
||||
return {
|
||||
text: withOptions(
|
||||
`Current thinking level: ${level}.`,
|
||||
"off, minimal, low, medium, high",
|
||||
formatThinkingLevels(resolvedProvider, resolvedModel),
|
||||
),
|
||||
};
|
||||
}
|
||||
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) {
|
||||
@@ -1098,126 +1215,25 @@ export async function handleDirectiveOnly(params: {
|
||||
return { text: errors.join(" ") };
|
||||
}
|
||||
|
||||
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) {
|
||||
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." };
|
||||
if (
|
||||
directives.hasThinkDirective &&
|
||||
directives.thinkLevel === "xhigh" &&
|
||||
!supportsXHighThinking(resolvedProvider, resolvedModel)
|
||||
) {
|
||||
return {
|
||||
text: `Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`,
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
const prevElevatedLevel =
|
||||
currentElevatedLevel ??
|
||||
@@ -1239,6 +1255,9 @@ export async function handleDirectiveOnly(params: {
|
||||
if (directives.thinkLevel === "off") delete sessionEntry.thinkingLevel;
|
||||
else sessionEntry.thinkingLevel = directives.thinkLevel;
|
||||
}
|
||||
if (shouldDowngradeXHigh) {
|
||||
sessionEntry.thinkingLevel = "high";
|
||||
}
|
||||
if (directives.hasVerboseDirective && directives.verboseLevel) {
|
||||
applyVerboseOverride(sessionEntry, directives.verboseLevel);
|
||||
}
|
||||
@@ -1295,6 +1314,18 @@ export async function handleDirectiveOnly(params: {
|
||||
if (storePath) {
|
||||
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) {
|
||||
const nextElevated = (sessionEntry.elevatedLevel ??
|
||||
"off") as ElevatedLevel;
|
||||
@@ -1345,6 +1376,11 @@ export async function handleDirectiveOnly(params: {
|
||||
);
|
||||
if (shouldHintDirectRuntime) parts.push(formatElevatedRuntimeHint());
|
||||
}
|
||||
if (shouldDowngradeXHigh) {
|
||||
parts.push(
|
||||
`Thinking level set to high (xhigh not supported for ${resolvedProvider}/${resolvedModel}).`,
|
||||
);
|
||||
}
|
||||
if (modelSelection) {
|
||||
const label = `${modelSelection.provider}/${modelSelection.model}`;
|
||||
const labelWithAlias = modelSelection.alias
|
||||
|
||||
@@ -122,7 +122,7 @@ describe("buildStatusMessage", () => {
|
||||
sessionId: "override-1",
|
||||
updatedAt: 0,
|
||||
providerOverride: "openai",
|
||||
modelOverride: "gpt-4.1-mini",
|
||||
modelOverride: "gpt-5-nano",
|
||||
modelProvider: "anthropic",
|
||||
model: "claude-haiku-4-5",
|
||||
contextTokens: 32_000,
|
||||
@@ -133,7 +133,7 @@ describe("buildStatusMessage", () => {
|
||||
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", () => {
|
||||
|
||||
@@ -1,10 +1,32 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeReasoningLevel, normalizeThinkLevel } from "./thinking.js";
|
||||
import {
|
||||
listThinkingLevels,
|
||||
normalizeReasoningLevel,
|
||||
normalizeThinkLevel,
|
||||
} from "./thinking.js";
|
||||
|
||||
describe("normalizeThinkLevel", () => {
|
||||
it("accepts mid as 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", () => {
|
||||
|
||||
@@ -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 ElevatedLevel = "off" | "on";
|
||||
export type ReasoningLevel = "off" | "on" | "stream";
|
||||
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.
|
||||
export function normalizeThinkLevel(
|
||||
raw?: string | null,
|
||||
@@ -32,10 +53,49 @@ export function normalizeThinkLevel(
|
||||
].includes(key)
|
||||
)
|
||||
return "high";
|
||||
if (["xhigh", "x-high", "x_high"].includes(key)) return "xhigh";
|
||||
if (["think"].includes(key)) return "minimal";
|
||||
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.
|
||||
export function normalizeVerboseLevel(
|
||||
raw?: string | null,
|
||||
|
||||
@@ -152,10 +152,10 @@ describe("agentCommand", () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
mockConfig(home, store, {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
model: { primary: "openai/gpt-5-nano" },
|
||||
models: {
|
||||
"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];
|
||||
expect(callArgs?.provider).toBe("openai");
|
||||
expect(callArgs?.model).toBe("gpt-4.1-mini");
|
||||
expect(callArgs?.model).toBe("gpt-5-nano");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+37
-2
@@ -30,8 +30,11 @@ import { hasNonzeroUsage } from "../agents/usage.js";
|
||||
import { ensureAgentWorkspace } from "../agents/workspace.js";
|
||||
import type { MsgContext } from "../auto-reply/templating.js";
|
||||
import {
|
||||
formatThinkingLevels,
|
||||
formatXHighModelHint,
|
||||
normalizeThinkLevel,
|
||||
normalizeVerboseLevel,
|
||||
supportsXHighThinking,
|
||||
type ThinkLevel,
|
||||
type VerboseLevel,
|
||||
} from "../auto-reply/thinking.js";
|
||||
@@ -216,17 +219,26 @@ export async function agentCommand(
|
||||
ensureBootstrapFiles: !agentCfg?.skipBootstrap,
|
||||
});
|
||||
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 thinkOnce = normalizeThinkLevel(opts.thinkingOnce);
|
||||
if (opts.thinking && !thinkOverride) {
|
||||
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) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
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, {
|
||||
agentId: sessionAgentId,
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ describe("agents helpers", () => {
|
||||
name: "Work",
|
||||
workspace: "/work-ws",
|
||||
agentDir: "/state/agents/work/agent",
|
||||
model: "openai/gpt-4.1",
|
||||
model: "openai/gpt-5-nano",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -157,8 +157,8 @@ describe("models list/status", () => {
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-4.1-mini",
|
||||
name: "GPT-4.1 mini",
|
||||
id: "gpt-5-nano",
|
||||
name: "GPT-5 Nano",
|
||||
input: ["text"],
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
contextWindow: 128000,
|
||||
@@ -199,8 +199,8 @@ describe("models list/status", () => {
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-4.1-mini",
|
||||
name: "GPT-4.1 mini",
|
||||
id: "gpt-5-nano",
|
||||
name: "GPT-5 Nano",
|
||||
input: ["text"],
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
contextWindow: 128000,
|
||||
@@ -241,8 +241,8 @@ describe("models list/status", () => {
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-4.1-mini",
|
||||
name: "GPT-4.1 mini",
|
||||
id: "gpt-5-nano",
|
||||
name: "GPT-5 Nano",
|
||||
input: ["text"],
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
contextWindow: 128000,
|
||||
|
||||
@@ -1603,10 +1603,10 @@ describe("legacy config detection", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
modelFallbacks: ["openai/gpt-4.1-mini"],
|
||||
imageModel: "openai/gpt-4.1-mini",
|
||||
modelFallbacks: ["openai/gpt-5-nano"],
|
||||
imageModel: "openai/gpt-5-nano",
|
||||
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" },
|
||||
},
|
||||
});
|
||||
@@ -1615,10 +1615,10 @@ describe("legacy config detection", () => {
|
||||
"anthropic/claude-opus-4-5",
|
||||
);
|
||||
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(
|
||||
"openai/gpt-4.1-mini",
|
||||
"openai/gpt-5-nano",
|
||||
);
|
||||
expect(res.config?.agents?.defaults?.imageModel?.fallbacks).toEqual([
|
||||
"anthropic/claude-opus-4-5",
|
||||
@@ -1627,7 +1627,7 @@ describe("legacy config detection", () => {
|
||||
res.config?.agents?.defaults?.models?.["anthropic/claude-opus-4-5"],
|
||||
).toMatchObject({ alias: "Opus" });
|
||||
expect(
|
||||
res.config?.agents?.defaults?.models?.["openai/gpt-4.1-mini"],
|
||||
res.config?.agents?.defaults?.models?.["openai/gpt-5-nano"],
|
||||
).toBeTruthy();
|
||||
expect(res.config?.agent).toBeUndefined();
|
||||
});
|
||||
|
||||
+1
-1
@@ -1629,7 +1629,7 @@ export type AgentDefaultsConfig = {
|
||||
/** Vector memory search configuration (per-agent overrides supported). */
|
||||
memorySearch?: MemorySearchConfig;
|
||||
/** 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. */
|
||||
verboseDefault?: "off" | "on";
|
||||
/** Default elevated level when no /elevated directive is present. */
|
||||
|
||||
@@ -1248,6 +1248,7 @@ const AgentDefaultsSchema = z
|
||||
z.literal("low"),
|
||||
z.literal("medium"),
|
||||
z.literal("high"),
|
||||
z.literal("xhigh"),
|
||||
])
|
||||
.optional(),
|
||||
verboseDefault: z.union([z.literal("off"), z.literal("on")]).optional(),
|
||||
|
||||
@@ -213,7 +213,7 @@ describe("runCronIsolatedAgentTurn", () => {
|
||||
job: makeJob({
|
||||
kind: "agentTurn",
|
||||
message: "do it",
|
||||
model: "openai/gpt-4.1-mini",
|
||||
model: "openai/gpt-5-nano",
|
||||
}),
|
||||
message: "do it",
|
||||
sessionKey: "cron:job-1",
|
||||
@@ -226,7 +226,7 @@ describe("runCronIsolatedAgentTurn", () => {
|
||||
model?: string;
|
||||
};
|
||||
expect(call?.provider).toBe("openai");
|
||||
expect(call?.model).toBe("gpt-4.1-mini");
|
||||
expect(call?.model).toBe("gpt-5-nano");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,11 @@ import {
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
stripHeartbeatToken,
|
||||
} 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 { ClawdbotConfig } from "../config/config.js";
|
||||
import {
|
||||
@@ -367,6 +371,11 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
catalog: await loadCatalog(),
|
||||
});
|
||||
}
|
||||
if (thinkLevel === "xhigh" && !supportsXHighThinking(provider, model)) {
|
||||
throw new Error(
|
||||
`Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const timeoutMs = resolveAgentTimeoutMs({
|
||||
cfg: cfgWithAgentDefaults,
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("hooks mapping", () => {
|
||||
match: { path: "gmail" },
|
||||
action: "agent",
|
||||
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);
|
||||
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");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ describe("gateway server hooks", () => {
|
||||
body: JSON.stringify({
|
||||
message: "Do it",
|
||||
name: "Email",
|
||||
model: "openai/gpt-4.1-mini",
|
||||
model: "openai/gpt-5-nano",
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(202);
|
||||
@@ -96,7 +96,7 @@ describe("gateway server hooks", () => {
|
||||
const call = cronIsolatedRun.mock.calls[0]?.[0] as {
|
||||
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());
|
||||
await server.close();
|
||||
});
|
||||
|
||||
@@ -8,10 +8,13 @@ import {
|
||||
} from "../agents/model-selection.js";
|
||||
import { normalizeGroupActivation } from "../auto-reply/group-activation.js";
|
||||
import {
|
||||
formatThinkingLevels,
|
||||
formatXHighModelHint,
|
||||
normalizeElevatedLevel,
|
||||
normalizeReasoningLevel,
|
||||
normalizeThinkLevel,
|
||||
normalizeUsageDisplay,
|
||||
supportsXHighThinking,
|
||||
} from "../auto-reply/thinking.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
@@ -95,8 +98,17 @@ export async function applySessionsPatchToStore(params: {
|
||||
} else if (raw !== undefined) {
|
||||
const normalized = normalizeThinkLevel(String(raw));
|
||||
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(
|
||||
"invalid thinkingLevel (use off|minimal|low|medium|high)",
|
||||
`invalid thinkingLevel (use ${formatThinkingLevels(hintProvider, hintModel, "|")})`,
|
||||
);
|
||||
}
|
||||
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) {
|
||||
const raw = patch.sendPolicy;
|
||||
if (raw === null) {
|
||||
|
||||
@@ -171,7 +171,7 @@ describe("google-shared convertMessages", () => {
|
||||
{
|
||||
type: "thinking",
|
||||
thinking: "hidden",
|
||||
thinkingSignature: "sig",
|
||||
thinkingSignature: "c2ln",
|
||||
},
|
||||
],
|
||||
api: "google-generative-ai",
|
||||
@@ -202,7 +202,7 @@ describe("google-shared convertMessages", () => {
|
||||
expect(contents[0].role).toBe("model");
|
||||
expect(contents[0].parts?.[0]).toMatchObject({
|
||||
thought: true,
|
||||
thoughtSignature: "sig",
|
||||
thoughtSignature: "c2ln",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -216,7 +216,7 @@ describe("google-shared convertMessages", () => {
|
||||
{
|
||||
type: "thinking",
|
||||
thinking: "structured",
|
||||
thinkingSignature: "sig",
|
||||
thinkingSignature: "c2ln",
|
||||
},
|
||||
],
|
||||
api: "google-generative-ai",
|
||||
@@ -247,7 +247,7 @@ describe("google-shared convertMessages", () => {
|
||||
expect(parts).toHaveLength(1);
|
||||
expect(parts[0]).toMatchObject({
|
||||
thought: true,
|
||||
thoughtSignature: "sig",
|
||||
thoughtSignature: "c2ln",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+23
-7
@@ -1,6 +1,9 @@
|
||||
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 REASONING_LEVELS = ["on", "off"];
|
||||
const ELEVATED_LEVELS = ["on", "off"];
|
||||
@@ -12,6 +15,11 @@ export type ParsedCommand = {
|
||||
args: string;
|
||||
};
|
||||
|
||||
export type SlashCommandOptions = {
|
||||
provider?: string;
|
||||
model?: string;
|
||||
};
|
||||
|
||||
const COMMAND_ALIASES: Record<string, string> = {
|
||||
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 [
|
||||
{ name: "help", description: "Show slash command help" },
|
||||
{ name: "status", description: "Show gateway status summary" },
|
||||
@@ -44,9 +55,9 @@ export function getSlashCommands(): SlashCommand[] {
|
||||
name: "think",
|
||||
description: "Set thinking level",
|
||||
getArgumentCompletions: (prefix) =>
|
||||
THINK_LEVELS.filter((v) => v.startsWith(prefix.toLowerCase())).map(
|
||||
(value) => ({ value, label: value }),
|
||||
),
|
||||
thinkLevels
|
||||
.filter((v) => v.startsWith(prefix.toLowerCase()))
|
||||
.map((value) => ({ value, label: value })),
|
||||
},
|
||||
{
|
||||
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 [
|
||||
"Slash commands:",
|
||||
"/help",
|
||||
@@ -113,7 +129,7 @@ export function helpText(): string {
|
||||
"/agent <id> (or /agents)",
|
||||
"/session <key> (or /sessions)",
|
||||
"/model <provider/model> (or /models)",
|
||||
"/think <off|minimal|low|medium|high>",
|
||||
`/think <${thinkLevels}>`,
|
||||
"/verbose <on|off>",
|
||||
"/reasoning <on|off>",
|
||||
"/cost <on|off>",
|
||||
|
||||
+30
-6
@@ -7,7 +7,10 @@ import {
|
||||
TUI,
|
||||
} from "@mariozechner/pi-tui";
|
||||
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 { formatAge } from "../infra/provider-summary.js";
|
||||
import {
|
||||
@@ -239,6 +242,18 @@ export async function runTui(opts: TuiOptions) {
|
||||
root.addChild(footer);
|
||||
root.addChild(editor);
|
||||
|
||||
const updateAutocompleteProvider = () => {
|
||||
editor.setAutocompleteProvider(
|
||||
new CombinedAutocompleteProvider(
|
||||
getSlashCommands({
|
||||
provider: sessionInfo.modelProvider,
|
||||
model: sessionInfo.model,
|
||||
}),
|
||||
process.cwd(),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const tui = new TUI(new ProcessTerminal());
|
||||
tui.addChild(root);
|
||||
tui.setFocus(editor);
|
||||
@@ -524,6 +539,7 @@ export async function runTui(opts: TuiOptions) {
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`sessions list failed: ${String(err)}`);
|
||||
}
|
||||
updateAutocompleteProvider();
|
||||
updateFooter();
|
||||
tui.requestRender();
|
||||
};
|
||||
@@ -861,7 +877,12 @@ export async function runTui(opts: TuiOptions) {
|
||||
if (!name) return;
|
||||
switch (name) {
|
||||
case "help":
|
||||
chatLog.addSystem(helpText());
|
||||
chatLog.addSystem(
|
||||
helpText({
|
||||
provider: sessionInfo.modelProvider,
|
||||
model: sessionInfo.model,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case "status":
|
||||
try {
|
||||
@@ -921,7 +942,12 @@ export async function runTui(opts: TuiOptions) {
|
||||
break;
|
||||
case "think":
|
||||
if (!args) {
|
||||
chatLog.addSystem("usage: /think <off|minimal|low|medium|high>");
|
||||
const levels = formatThinkingLevels(
|
||||
sessionInfo.modelProvider,
|
||||
sessionInfo.model,
|
||||
"|",
|
||||
);
|
||||
chatLog.addSystem(`usage: /think <${levels}>`);
|
||||
break;
|
||||
}
|
||||
try {
|
||||
@@ -1071,9 +1097,7 @@ export async function runTui(opts: TuiOptions) {
|
||||
tui.requestRender();
|
||||
};
|
||||
|
||||
editor.setAutocompleteProvider(
|
||||
new CombinedAutocompleteProvider(getSlashCommands(), process.cwd()),
|
||||
);
|
||||
updateAutocompleteProvider();
|
||||
editor.onSubmit = (text) => {
|
||||
const value = text.trim();
|
||||
editor.setText("");
|
||||
|
||||
+2
-2
@@ -15,10 +15,10 @@
|
||||
"vite": "7.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/browser-playwright": "4.0.16",
|
||||
"@vitest/browser-playwright": "4.0.17",
|
||||
"playwright": "^1.57.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "4.0.16"
|
||||
"vitest": "4.0.17"
|
||||
},
|
||||
"pnpm": {
|
||||
"minimumReleaseAge": 2880
|
||||
|
||||
Reference in new issue
Block a user