feat: add Bedrock request optimizer (PRE-SEND thinking + cache injection) (#1301)

* feat: add Bedrock request optimizer (PRE-SEND thinking + cache injection)

Add a PRE-SEND request optimizer that enhances Bedrock API requests
before forwarding, complementing the existing POST-ERROR rectifier system.

New modules:
- thinking_optimizer: 3-path model detection (adaptive/legacy/skip)
  - Opus 4.6/Sonnet 4.6: adaptive thinking + effort max + 1M context beta
  - Legacy models: inject extended thinking with max budget
  - Haiku: skip (no modification)
- cache_injector: auto-inject cache_control breakpoints (max 4)
  - Injects at tools/system/assistant message positions
  - TTL upgrade for existing breakpoints (5m → 1h)

Gate: only activates for Bedrock providers (CLAUDE_CODE_USE_BEDROCK=1)
Config: stored in SQLite settings table, default OFF, user opt-in
UI: new Optimizer section in RectifierConfigPanel with 3 toggles + TTL

18 unit tests covering all paths. Verified against live Bedrock API.

* chore: remove docs/plans directory

* fix: address code review findings for Bedrock request optimizer

P0 fixes:
- Replace hardcoded Chinese with i18n t() calls in optimizer panel,
  add translation keys to zh/en/ja locale files
- Fix u64 underflow: max_tokens - 1 → max_tokens.saturating_sub(1)
- Move optimizer from before retry loop to per-provider with body
  cloning, preventing Bedrock fields leaking to non-Bedrock providers

P1 fixes:
- Replace .map() side-effect pattern with idiomatic if-let (clippy)
- Fix module alphabetical ordering in mod.rs
- Add cache_ttl whitelist validation in set_optimizer_config
- Remove #[allow(unused_assignments)] and dead budget decrement

---------

Co-authored-by: Keith (via OpenClaw) <keithyt06@users.noreply.github.com>
Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
Keith Yu
2026-03-07 18:57:21 +08:00
committed by GitHub
parent 27d21c23ac
commit 8217bfff50
14 changed files with 969 additions and 9 deletions
@@ -3,7 +3,11 @@ import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { settingsApi, type RectifierConfig } from "@/lib/api/settings";
import {
settingsApi,
type RectifierConfig,
type OptimizerConfig,
} from "@/lib/api/settings";
export function RectifierConfigPanel() {
const { t } = useTranslation();
@@ -12,6 +16,12 @@ export function RectifierConfigPanel() {
requestThinkingSignature: true,
requestThinkingBudget: true,
});
const [optimizerConfig, setOptimizerConfig] = useState<OptimizerConfig>({
enabled: false,
thinkingOptimizer: true,
cacheInjection: true,
cacheTtl: "1h",
});
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
@@ -20,6 +30,10 @@ export function RectifierConfigPanel() {
.then(setConfig)
.catch((e) => console.error("Failed to load rectifier config:", e))
.finally(() => setIsLoading(false));
settingsApi
.getOptimizerConfig()
.then(setOptimizerConfig)
.catch((e) => console.error("Failed to load optimizer config:", e));
}, []);
const handleChange = async (updates: Partial<RectifierConfig>) => {
@@ -34,6 +48,18 @@ export function RectifierConfigPanel() {
}
};
const handleOptimizerChange = async (updates: Partial<OptimizerConfig>) => {
const newConfig = { ...optimizerConfig, ...updates };
setOptimizerConfig(newConfig);
try {
await settingsApi.setOptimizerConfig(newConfig);
} catch (e) {
console.error("Failed to save optimizer config:", e);
toast.error(String(e));
setOptimizerConfig(optimizerConfig);
}
};
if (isLoading) return null;
return (
@@ -86,6 +112,94 @@ export function RectifierConfigPanel() {
/>
</div>
</div>
<div className="border-t pt-6 mt-6">
<div className="space-y-1 mb-4">
<h3 className="text-sm font-medium">
{t("settings.advanced.optimizer.title")}
</h3>
<p className="text-xs text-muted-foreground">
{t("settings.advanced.optimizer.description")}
</p>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>{t("settings.advanced.optimizer.enabled")}</Label>
</div>
<Switch
checked={optimizerConfig.enabled}
onCheckedChange={(checked) =>
handleOptimizerChange({ enabled: checked })
}
/>
</div>
<div className="space-y-4 pl-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>
{t("settings.advanced.optimizer.thinkingOptimizer")}
</Label>
<p className="text-xs text-muted-foreground">
{t(
"settings.advanced.optimizer.thinkingOptimizerDescription",
)}
</p>
</div>
<Switch
checked={optimizerConfig.thinkingOptimizer}
disabled={!optimizerConfig.enabled}
onCheckedChange={(checked) =>
handleOptimizerChange({ thinkingOptimizer: checked })
}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>{t("settings.advanced.optimizer.cacheInjection")}</Label>
<p className="text-xs text-muted-foreground">
{t("settings.advanced.optimizer.cacheInjectionDescription")}
</p>
</div>
<Switch
checked={optimizerConfig.cacheInjection}
disabled={!optimizerConfig.enabled}
onCheckedChange={(checked) =>
handleOptimizerChange({ cacheInjection: checked })
}
/>
</div>
{optimizerConfig.cacheInjection && (
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>{t("settings.advanced.optimizer.cacheTtl")}</Label>
</div>
<select
className="h-9 rounded-md border border-input bg-background px-3 text-sm"
value={optimizerConfig.cacheTtl}
disabled={
!optimizerConfig.enabled || !optimizerConfig.cacheInjection
}
onChange={(e) =>
handleOptimizerChange({ cacheTtl: e.target.value })
}
>
<option value="5m">
{t("settings.advanced.optimizer.cacheTtl5m")}
</option>
<option value="1h">
{t("settings.advanced.optimizer.cacheTtl1h")}
</option>
</select>
</div>
)}
</div>
</div>
</div>
</div>
);
}
+12
View File
@@ -257,6 +257,18 @@
"thinkingBudget": "Thinking Budget Rectification",
"thinkingBudgetDescription": "When an Anthropic-type provider returns budget_tokens constraint errors (such as at least 1024), automatically normalizes thinking to enabled, sets thinking budget to 32000, and raises max_tokens to 64000 if needed, then retries once"
},
"optimizer": {
"title": "Bedrock Request Optimizer",
"description": "Automatically optimize Thinking and Cache configuration before sending requests (only applies to Bedrock providers)",
"enabled": "Enable Optimizer",
"thinkingOptimizer": "Thinking Optimization",
"thinkingOptimizerDescription": "Automatically enable Adaptive Thinking for Opus/Sonnet, and inject Extended Thinking for legacy models",
"cacheInjection": "Cache Injection",
"cacheInjectionDescription": "Automatically inject cache breakpoints at key positions in requests to reduce duplicate token billing",
"cacheTtl": "Cache TTL",
"cacheTtl5m": "5 minutes",
"cacheTtl1h": "1 hour"
},
"logConfig": {
"title": "Log Management",
"description": "Control log output level",
+12
View File
@@ -257,6 +257,18 @@
"thinkingBudget": "Thinking Budget 整流",
"thinkingBudgetDescription": "Anthropic タイプのプロバイダーが budget_tokens 制約エラー(例: 1024 以上)を返した場合、thinking を enabled に正規化し、thinking 予算を 32000 に設定し、必要に応じて max_tokens を 64000 に引き上げて 1 回リトライします"
},
"optimizer": {
"title": "Bedrock リクエストオプティマイザー",
"description": "リクエスト送信前に Thinking と Cache の設定を自動最適化(Bedrock プロバイダーのみ有効)",
"enabled": "オプティマイザーを有効化",
"thinkingOptimizer": "Thinking 最適化",
"thinkingOptimizerDescription": "Opus/Sonnet に Adaptive Thinking を自動的に有効化し、レガシーモデルに Extended Thinking を注入",
"cacheInjection": "Cache 注入",
"cacheInjectionDescription": "リクエストの重要な位置に Cache ブレークポイントを自動注入し、重複トークンの課金を削減",
"cacheTtl": "Cache TTL",
"cacheTtl5m": "5 分",
"cacheTtl1h": "1 時間"
},
"logConfig": {
"title": "ログ管理",
"description": "ログ出力レベルを制御",
+12
View File
@@ -257,6 +257,18 @@
"thinkingBudget": "Thinking Budget 整流",
"thinkingBudgetDescription": "当 Anthropic 类型供应商返回 budget_tokens 约束错误(如至少 1024)时,自动将 thinking 规范为 enabled 并将 budget 设为 32000,同时在需要时将 max_tokens 设为 64000,然后重试一次"
},
"optimizer": {
"title": "Bedrock 请求优化器",
"description": "在请求发送前自动优化 Thinking 和 Cache 配置(仅 Bedrock 供应商生效)",
"enabled": "启用优化器",
"thinkingOptimizer": "Thinking 优化",
"thinkingOptimizerDescription": "自动为 Opus/Sonnet 启用 Adaptive Thinking,为旧模型注入 Extended Thinking",
"cacheInjection": "Cache 注入",
"cacheInjectionDescription": "自动在请求关键位置注入 Cache 断点,减少重复 token 计费",
"cacheTtl": "Cache TTL",
"cacheTtl5m": "5 分钟",
"cacheTtl1h": "1 小时"
},
"logConfig": {
"title": "日志管理",
"description": "控制日志输出级别",
+15
View File
@@ -196,6 +196,14 @@ export const settingsApi = {
return await invoke("set_rectifier_config", { config });
},
async getOptimizerConfig(): Promise<OptimizerConfig> {
return await invoke("get_optimizer_config");
},
async setOptimizerConfig(config: OptimizerConfig): Promise<boolean> {
return await invoke("set_optimizer_config", { config });
},
async getLogConfig(): Promise<LogConfig> {
return await invoke("get_log_config");
},
@@ -211,6 +219,13 @@ export interface RectifierConfig {
requestThinkingBudget: boolean;
}
export interface OptimizerConfig {
enabled: boolean;
thinkingOptimizer: boolean;
cacheInjection: boolean;
cacheTtl: string;
}
export interface LogConfig {
enabled: boolean;
level: "error" | "warn" | "info" | "debug" | "trace";