Skip to content
2 changes: 1 addition & 1 deletion docs/features/F127-cat-instance-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ community_issue: "#109"
| R-8 | **切换认证方式(订阅↔API key)没有一键切换** — 要一只猫一只猫改 provider profile binding | 高(UX 痛点) | operator想批量切换认证方式时 | 加"一键切换所有猫的 provider profile"功能 |
| R-9 | **nuoda.vip 代理 model name 格式混淆** — API 代理用 `claude-opus-4-6`(Anthropic 原生),但 opencode CLI 需要 `anthropic/claude-opus-4-6`(provider/model 格式),Hub 不知道该用哪个 | 中(配置困惑) | 用第三方 API 代理时 | Hub 编辑器应按 client 类型自动处理 model name 格式 |
| R-10 | **本地反代 `anthropic-proxy.mjs` 的 upstream 配置未初始化** — `start-dev.sh` 启动的反代(端口 9877)依赖 `.cat-cafe/proxy-upstreams.json` 配置上游,但 F127 intake 后 runtime 里该文件不存在。API key profile 创建应自动注册 upstream 到反代 | 中(反代功能不可用) | 配置 API key profile 用本地反代时 | profile 创建/更新时自动写 `proxy-upstreams.json` |
| R-11 | ~~**Hub 缺少结构化、provider-aware 的 `cli.effort` 编辑**~~ — ✅ 已修复(PR #882)。Hub 已提供结构化 effort 字段;Claude=`low/medium/high/max`,Codex=`low/medium/high/xhigh`;保存写 `variant.cli.effort`;只对新 invocation 生效,不强切旧 session;开源跟踪 issue: [clowder-ai#315](https://github.com/zts212653/clowder-ai/issues/315) | ~~高(易错 + UX 差)~~ done | — | — |
| R-11 | ~~**Hub 缺少结构化、provider-aware 的 `cli.effort` 编辑**~~ — ✅ PR #882 提供维护 preset;#315 进一步支持直接输入并原样持久化 provider 原生值(如 Codex `max` / `ultra`)。保存写 `variant.cli.effort`;只对新 invocation 生效,不强切旧 session | ~~高(易错 + UX 差)~~ done | — | — |
| R-12 | ~~**跨项目 homedir legacy 账号污染 runtime 账号配置**~~ — ✅ 已修复(PR #1457)。启动迁移与 installer import 现在只导入项目显式引用的 homedir legacy account;引用源覆盖 `accountRef`、legacy `providerProfileId`、catalog `accounts` keys、credential refs,并保留 installer 内置账号 | ~~中(账号配置 UI 出现 Agent Teams / Local 等外部项目垃圾项)~~ done | — | — |

## AC-B3 验收矩阵(E2E 验证清单)
Expand Down
149 changes: 149 additions & 0 deletions feature-specs/2026-07-10-issue-315-provider-adaptive-effort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Issue #315 Provider-Adaptive CLI Effort Implementation Plan

**Feature:** F127 residual — [docs/features/F127-cat-instance-management.md](../docs/features/F127-cat-instance-management.md)
**Goal:** Let a member retain any non-empty native CLI effort string while continuing to offer maintained client-specific presets and safe adapter-owned argument construction.
**Acceptance Criteria:**
- Hub exposes maintained effort presets and a direct text-entry path.
- Codex accepts native `max` and `ultra` through the structured effort field.
- Each existing effort-aware client adapter translates supported internal presets to its native CLI representation.
- A non-preset value is accepted, persisted, and passed through its selected-client adapter without enum-only rejection.
- Direct input reaches command construction through structured arguments, never unsafe shell concatenation.
- Provider-native validation failures remain visible to the operator.
- New invocations use saved values; existing sessions are not restarted.
- Tests cover preset conversion, passthrough, persistence, and provider-rejection reporting.
**Architecture cell:** `identity-session` (identity-agent subcell)
**Map delta:** none
**Map delta why:** This changes the existing cat-instance configuration contract and its adapters; it introduces no new owner or cross-cell boundary.
**Architecture:** Preserve a single `variant.cli.effort` string in the runtime catalog. The shared module continues to expose the maintained preset vocabulary for UI suggestions and default selection, while the route and loader accept any non-empty string. Claude and Codex retain adapter-owned argv construction; Codex serializes the TOML value rather than interpolating untrusted text.
**Tech Stack:** TypeScript, Zod, Fastify route tests, Vitest component tests, Node test runner.
**前端验证:** Yes — Hub Cat Editor coverage plus an isolated browser preview; formal approval requires interactive verification of the member-editor input and suggestions.

---

## Finish line and exclusions

The Hub member editor can select a maintained preset or enter `max`, `ultra`, or another native value. Saving preserves the exact string in `.cat-cafe/cat-catalog.json`, and the next Claude/Codex invocation receives it via its own argv adapter. We are **not** probing provider versions, maintaining provider-native enums, force-switching running sessions, or repurposing generic `cliConfigArgs`.

## Terminal contract

```ts
type CliEffortPreset = 'low' | 'medium' | 'high' | 'max' | 'xhigh' | 'ultra';
type CliEffortValue = string; // validated at API/schema boundary: trimmed and non-empty

interface CliConfig {
effort?: CliEffortValue;
}
```

`getCliEffortOptionsForProvider(provider, model?)` is the maintained-preset source. It returns
model-aware suggestions when applicable (GPT-5.6 OpenAI models include `max` and `ultra`),
falling back to the base provider list otherwise. Runtime resolution returns a stored non-empty
value unchanged, otherwise the selected client's default; it must never coerce a native value
to a different preset. Effort is only accepted for effort-aware clients (anthropic, openai);
other clients receive HTTP 400 if `cli.effort` is supplied.

## Persistent-state census

| Object | Lifecycle owner | States/events | Invariants |
|---|---|---|---|
| `variant.cli.effort` in runtime catalog | `runtime-cat-catalog` via `/api/cats` | absent → saved preset/native string → edited/cleared | INV-1, INV-2 |
| Hub editor draft `cliEffort` | `HubCatEditor` form state | loaded → edited/selected → serialized | INV-2 |
| Invocation argv | provider adapter | config read → adapter serialization → spawned process | INV-3, INV-4 |

| State | Save preset | Save native `ultra` | Clear field | Client switch without explicit effort |
|---|---|---|---|---|
| Catalog | persist exact preset | persist exact native text | remove `effort` | replace with target-client default |
| New invocation | adapter emits native flag/value | same adapter emits exact native text | client default applies | target-client default applies |

**Invariants:**

- **INV-1:** The runtime catalog stores a user-entered non-empty effort value unchanged; it is not normalized to another provider's preset.
- **INV-2:** Form hydration and payload serialization round-trip both maintained presets and unfamiliar native values unchanged.
- **INV-3:** Only the selected provider adapter constructs CLI args; UI text never becomes a shell command string.
- **INV-4:** A new invocation reads the saved catalog value; pre-existing sessions are not mutated or restarted by this setting change.

**Adversarial scenarios:** blank/whitespace input is rejected or represented as clear; `max` and `ultra` on Codex preserve exactly; a quote-bearing value is TOML-escaped in Codex argv rather than interpreted as a second config; a provider rejects an otherwise accepted native value and its stderr/exit result remains observable.

## Test matrix

| Contract | Test location | Evidence |
|---|---|---|
| Preset suggestions remain client-specific | `hub-cat-editor.test.tsx`, shared effort tests | known preset lists unchanged |
| Native form hydration and payload | `hub-cat-editor.test.tsx` | `ultra` survives load and save |
| API create/patch persistence | `cats-routes-runtime-crud.test.js` | `max`/`ultra` persist without enum rejection |
| Loader passthrough/defaulting | `cat-config-loader.test.js` | native value retained; absent value defaults |
| Safe adapter construction | `codex-agent-service.test.js`, `claude-agent-service.test.js` | exact argv values and escaped TOML |
| Provider failure reporting | existing CLI error-path regression, extended if needed | exit/stderr stays operator-visible |

### Task 1: Make the configuration contract native-value tolerant

**Files:**
- Modify: `packages/shared/src/cli-effort.ts`
- Modify: `packages/shared/src/types/cat-breed.ts`
- Modify: `packages/api/src/config/cat-config-loader.ts`
- Test: `packages/api/test/cat-config-loader.test.js`

**Step 1: Write failing loader tests** for a stored Codex `max` and `ultra` effort, asserting exact retrieval and existing default behavior when absent.

**Step 2: Run the focused test** and confirm it fails because current normalization replaces/rejects the native value.

**Step 3: Implement the smallest contract change**: retain `CliEffortPreset` for maintained suggestions, accept a non-empty string for stored effort, and resolve stored text before applying a provider default.

**Step 4: Run focused shared/loader tests** and confirm all pass.

### Task 2: Accept and persist native values through the Cats API

**Files:**
- Modify: `packages/api/src/routes/cats.ts`
- Test: `packages/api/test/cats-routes-runtime-crud.test.js`

**Step 1: Write failing POST and PATCH tests** that save Codex `max` and `ultra`, then assert API response and catalog persistence retain the submitted strings.

**Step 2: Run those tests** and confirm Zod/provider-enum validation rejects them for the expected reason.

**Step 3: Replace enum-only validation with trimmed non-empty structured text validation.** Keep command/output/default args structured and reject blank values; do not concatenate input into a shell command.

**Step 4: Run focused route tests** and confirm preset/default client-switch regressions remain green.

### Task 3: Provide direct entry in the Hub editor

**Files:**
- Modify: `packages/web/src/components/hub-cat-editor.model.ts`
- Modify: `packages/web/src/components/hub-cat-editor-advanced.tsx`
- Test: `packages/web/src/components/__tests__/hub-cat-editor.test.tsx`

**Step 1: Write failing form/payload tests** showing a persisted `ultra` value is visible and a typed native value serializes in `cli.effort` independently of `cliConfigArgs`.

**Step 2: Run the focused component test** and confirm existing enum filtering clears the unfamiliar value.

**Step 3: Use an editable field with the selected client's preset list as suggestions.** Keep the control visible only for existing effort-aware adapters and retain the "use client default" clear behavior.

**Step 4: Run focused web tests** and confirm preset behavior still passes.

### Task 4: Keep adapter construction safe and observable

**Files:**
- Modify: `packages/api/src/domains/cats/services/agents/providers/CodexAgentService.ts`
- Test: `packages/api/test/codex-agent-service.test.js`
- Test: `packages/api/test/claude-agent-service.test.js`

**Step 1: Write a failing Codex argv regression test** for a quoted native value, asserting exactly one TOML `model_reasoning_effort` argument whose value is serialized safely; add `max`/`ultra` adapter assertions.

**Step 2: Run the targeted tests** and confirm unsafe interpolation or old normalization produces the wrong argv.

**Step 3: Serialize the Codex TOML string through the existing TOML string helper; leave Claude's separate `--effort`, value argv shape intact.** Do not add shell execution or provider-side validation in Cat Café.

**Step 4: Run focused provider tests** and validate the existing non-zero/stderr CLI error path still surfaces provider-native failures.

### Task 5: Verify and hand off

**Files:**
- Modify: all Task 1–4 files only

**Step 1:** Run formatting/type checks and all focused API/web/provider tests.

**Step 2:** Run the relevant package test suites and `pnpm check` with development-safe environment variables.

**Step 3:** Inspect the diff against every acceptance criterion and verify that no runtime configuration file or existing session was changed.

**Step 4:** Commit the plan and implementation in reviewable commits, request cross-individual review, and register the upstream PR for tracking after creation.
22 changes: 8 additions & 14 deletions packages/api/src/config/cat-config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const cliConfigSchema = z.object({
command: z.string().min(1),
outputFormat: z.string().min(1),
defaultArgs: z.array(z.string()).optional(),
effort: z.enum(['low', 'medium', 'high', 'max', 'xhigh']).optional(),
effort: z.string().trim().min(1).optional(),
contextWindow: z.number().positive().int().optional(),
autoCompactTokenLimit: z.number().positive().int().optional(),
});
Expand Down Expand Up @@ -921,8 +921,8 @@ function buildCatIdToVariantIndex(config: CatCafeConfig): Map<string, CatVariant
return index;
}

/** Effort level union across all CLI providers */
export type CliEffortLevel = 'low' | 'medium' | 'high' | 'max' | 'xhigh';
/** Canonical effort string resolved from the catalog or a client default. */
export type CliEffortLevel = string;

/**
* Get CLI effort level for a cat from the resolved cat config.
Expand All @@ -945,17 +945,11 @@ export function getCatEffort(catId: string, config?: CatCafeConfig, fallbackProv

const variant = _catIdToVariant.get(catId);
if (variant?.cli?.effort) {
// Defense-in-depth: validate persisted effort against current provider.
// Stale cross-provider values (e.g. 'max' on openai) are cleaned at write
// time, but historical data may still contain them.
const provider = variant.clientId ?? fallbackProvider;
if (provider) {
const validated = normalizeCliEffortForProvider(provider, variant.cli.effort);
if (validated) return validated;
// Invalid for this provider — fall through to provider default below
} else {
return variant.cli.effort;
}
// Provider CLIs can introduce native effort values faster than our preset
// vocabulary evolves. Preserve a saved non-empty value exactly; the
// selected provider adapter owns native validation at invocation time.
const nativeValue = variant.cli.effort.trim();
if (nativeValue) return nativeValue;
}

// Client-aware defaults: use variant's clientId if found, otherwise fallbackProvider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ function toTomlString(value: string): string {
return `"${escaped}"`;
}

/** Build the structured Codex reasoning-effort config argument. */
export function buildCodexReasoningArgs(effortLevel: string): string[] {
return ['--config', `model_reasoning_effort=${toTomlString(effortLevel)}`];
}

/**
* F203 Phase C — `--config` keys the system controls. User cliConfigArgs
* cannot override these. Currently `developer_instructions` carries the
Expand Down Expand Up @@ -748,7 +753,7 @@ export class CodexAgentService implements AgentService {
const sandboxMode = getCodexSandboxMode();
const approvalPolicy = getCodexApprovalPolicy();
const effortLevel = getCatEffort(this.catId as string, undefined, 'openai');
const reasoningArgs = ['--config', `model_reasoning_effort="${effortLevel}"`];
const reasoningArgs = buildCodexReasoningArgs(effortLevel);
const sandboxConfigArgs = ['--config', `sandbox_mode=${toTomlString(sandboxMode)}`];
const approvalArgs = ['--config', `approval_policy="${approvalPolicy}"`];
const ctxConfig = getCatContextWindowConfig(this.catId as string);
Expand Down
17 changes: 8 additions & 9 deletions packages/api/src/routes/cats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@
import { resolve } from 'node:path';
import {
type CatConfig,
CLI_EFFORT_VALUES,
type CliConfig,
type ClientId,
type ContextBudget,
catRegistry,
getCliEffortOptionsForProvider,
getDefaultCliEffortForProvider,
isValidCliEffortForProvider,
type RosterEntry,
} from '@cat-cafe/shared';
import type { FastifyPluginAsync } from 'fastify';
Expand Down Expand Up @@ -59,7 +57,7 @@ const contextBudgetSchema = z.object({
maxContentLengthPerMsg: z.number().int().positive(),
});

const cliEffortSchema = z.enum(CLI_EFFORT_VALUES);
const cliEffortSchema = z.string().trim().min(1);
Comment thread
zts212653 marked this conversation as resolved.
const cliSchema = z.object({
command: z.string().min(1).optional(),
outputFormat: z.string().min(1).optional(),
Expand Down Expand Up @@ -295,12 +293,13 @@ function buildResolvedCliConfig(client: ClientId, baseCli: CliConfig, patch?: Cl

const effortTouched = patch ? Object.hasOwn(patch, 'effort') : false;
const nextEffort = effortTouched ? patch?.effort : baseCli.effort;
if (nextEffort !== undefined && nextEffort !== null && !isValidCliEffortForProvider(client, nextEffort)) {
const options = getCliEffortOptionsForProvider(client);
if (!options) {
throw new Error(`client "${client}" does not support cli.effort`);
}
throw new Error(`client "${client}" only supports cli.effort ${options.join(' / ')}`);

// Gate: only effort-aware clients (those with a real adapter that consumes
// getCatEffort()) may persist an effort value. Non-effort clients would
// silently ignore the setting and the provider would never perform the
// promised native validation.
if (nextEffort !== undefined && nextEffort !== null && !getCliEffortOptionsForProvider(client)) {
throw new Error(`client "${client}" does not support cli.effort`);
}

return {
Expand Down
24 changes: 18 additions & 6 deletions packages/api/test/cat-config-loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -936,20 +936,32 @@ describe('getCatEffort', () => {
assert.ok(result, 'should return a truthy default effort');
});

it('rejects stale cross-provider effort from historical data (defense-in-depth)', () => {
// Simulates a catalog written before the PATCH write-time cleanup was added:
// an openai cat still carrying anthropic-only effort 'max'.
it('preserves a Codex native effort that is outside the maintained preset vocabulary', () => {
// `max` is not a maintained Codex preset, but newer Codex CLIs may accept it
// natively. The runtime catalog must retain the operator's exact choice.
const cfg = validConfig();
cfg.breeds[0].variants[0].clientId = 'openai';
cfg.breeds[0].variants[0].cli = {
command: 'codex',
outputFormat: 'json',
effort: 'max', // invalid for openai — only anthropic supports 'max'
effort: 'max',
};
const config = loadCatConfig(writeTempConfig(cfg));

// Should fall back to openai default ('xhigh'), not return 'max'
assert.equal(getCatEffort('opus', config), 'xhigh');
assert.equal(getCatEffort('opus', config), 'max');
});

it('preserves an unfamiliar native effort value for a Codex member', () => {
const cfg = validConfig();
cfg.breeds[0].variants[0].clientId = 'openai';
cfg.breeds[0].variants[0].cli = {
command: 'codex',
outputFormat: 'json',
effort: 'ultra',
};
const config = loadCatConfig(writeTempConfig(cfg));

assert.equal(getCatEffort('opus', config), 'ultra');
});
});
describe('F32-b P4c: Sonnet variant in project config', () => {
Expand Down
Loading
Loading