Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ Built-in profiles are grouped by provider mix and tier:
- `opencodego` — single OpenCode Go preset (Kimi default, DeepSeek executor/architect, Qwen planner, MiMo critic)
- `claude-opus` — Anthropic OAuth preset centered on `claude-opus-5`
- Single-provider tiers: `glm-{eco,medium,pro}`, `kimi-coding-plan-{eco,medium,pro}`, `mimo-{eco,medium,pro}`, `grok-{eco,medium,pro}`, `cursor-{eco,medium,pro}`, `minimax-{eco,medium,pro}`
- Alibaba Token Plan: `alibaba-token-plan-balanced` preserves the established Qwen/DeepSeek V4 Pro/GLM mix; `alibaba-token-plan-pro` raises execution and independent criticism with DeepSeek V4 Flash 0731 max and GLM xhigh; `alibaba-token-plan-qwenmaxxing` stays Qwen-only; `alibaba-token-plan-qwen-deepseek` keeps Qwen 3.8 Max (`qwen-3.8-max`) on the expensive default (high)/architect (xhigh)/critic (xhigh) roles and spends DeepSeek V4 Flash 0731 on the cheap planner (max) and executor (high) roles; `alibaba-token-plan-glm-deepseek` does the same with GLM 5.2 (`glm-5.2`) as the expensive model
- Alibaba Token Plan: `alibaba-token-plan-balanced` preserves the established Qwen/DeepSeek V4 Pro/GLM mix; `alibaba-token-plan-pro` raises execution and independent criticism with DeepSeek V4 Flash 0731 max and GLM xhigh; `alibaba-token-plan-qwenmaxxing` stays Qwen-only; `alibaba-token-plan-qwen-deepseek` keeps Qwen 3.8 Max (`qwen3.8-max`) on the expensive default (high)/architect (xhigh)/critic (xhigh) roles and spends DeepSeek V4 Flash 0731 on the cheap planner (max) and executor (high) roles; `alibaba-token-plan-glm-deepseek` does the same with GLM 5.2 (`glm-5.2`) as the expensive model
- Combos: `opus-codex`, `codex-opencodego`, and `fable-opus-codex`

The `eco`, `medium`, and `pro` Codex profile mappings are current product judgments: Eco assigns Terra low/Luna low/Luna high/Terra xhigh/Terra high to default/executor/planner/critic/architect; Medium assigns Sol low/Terra low/Terra high/Sol xhigh/Sol high; Pro assigns Sol medium/Terra medium/Sol high/Sol max/Sol xhigh; and LunaMaxxing assigns Luna medium/Luna xhigh/Luna max/Luna max/Luna max. `opus-codex` retains the Medium Codex executor, critic, and architect roles but uses `anthropic/claude-sonnet-5` for planner; `codex-opencodego` retains the Medium Codex default and architect roles; and `fable-opus-codex` uses the Pro Codex executor and architect roles with `anthropic/claude-opus-5:medium` for planner. The descriptive repeated local exact-edit evidence informs only selected executor-style TypeScript tasks; it does not evaluate or prove default, planner, architect, or critic performance. See [GPT-5.6 Codex preset benchmark](./gpt-5.6-codex-preset-benchmark.md). The Alibaba Pro role evidence and its limits are recorded separately in [Alibaba Token Plan Pro profile benchmark](./alibaba-token-plan-pro-profile-benchmark.md). Cursor Eco uses Composer 2.5 for every role; Medium keeps standard Composer for default/planning and spends the Fast premium on execution, criticism, and architecture; Pro uses Composer 2.5 Fast throughout. Composer does not expose a strength value through the current Cursor RPC, so these profiles use exact model IDs without inert generic effort suffixes. See [Cursor Composer profile tiers](./cursor-composer-profile-tiers.md). Effort suffixes are clamped to each model's supported thinking range at preview and activation time. Single-provider tiers pin each provider's current flagship (`zai/glm-5.2`, `kimi-code/kimi-k2.7-code`, `xiaomi/mimo-v2.5-pro`, `xai/grok-4.3`, `cursor/composer-2.5`, `minimax-code/MiniMax-M3`). User-defined profiles override built-ins by exact profile name.
Expand Down
17 changes: 10 additions & 7 deletions docs/tools/todo_write.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
| --- | --- | --- | --- |
| `init` | `list` | None of the other fields are used | Replaces the entire list with `list`; every new task starts `pending` before normalization. |
| `start` | `task` | None | Marks one task `in_progress`; any other `in_progress` task is demoted to `pending`. |
| `done` | `task` or `phase` or neither | None | Marks the target task, phase, or all tasks `completed`. |
| `drop` | `task` or `phase` or neither | None | Marks the target task, phase, or all tasks `abandoned`. |
| `done` | `task` or `phase` | None | Marks the target task or phase `completed`. |
| `drop` | `task` or `phase` | None | Marks the target task or phase `abandoned`. |
| `rm` | `task` or `phase` or neither | None | Removes the target task, clears the phase's task list, or clears all task lists. |
| `append` | `phase`, `items` | None | Appends new `pending` tasks to a phase; creates the phase if missing. |
| `note` | `task`, `text` | None | Appends one trimmed note string to the task's `notes` array. |
Expand All @@ -37,7 +37,7 @@
| `op` | `"init" | "start" | "done" | "rm" | "drop" | "append" | "note"` | Yes | Operation discriminator. |
| `list` | `{ phase: string; items: string[] }[]` | For `init` | Full replacement payload. Each `items` array has `minItems: 1`. |
| `task` | `string` | For `start`; for task-targeted `done`/`drop`/`rm`/`note` | Exact task content match. |
| `phase` | `string` | For `append`; for phase-targeted `done`/`drop`/`rm` | Exact phase name match, except `append` lazily creates a missing phase. |
| `phase` | `string` | For `append`; for phase-targeted `done`/`drop`/`rm`; required when `done`/`drop` omit `task` | Exact phase name match, except `append` lazily creates a missing phase. |
| `items` | `string[]` | For `append` | Tasks to append. `minItems: 1`. |
| `text` | `string` | For `note` | Note text; trailing whitespace is stripped before storing. Empty-after-trim is rejected. |

Expand Down Expand Up @@ -66,7 +66,7 @@ The TUI renderer (`todoWriteToolRenderer`) merges call and result into one trans
3. Each op mutates the working phase array:
- `initPhases(...)` rebuilds the list from scratch.
- `start` resolves a task by exact `content`, demotes every other `in_progress` task to `pending`, then marks the target `in_progress`.
- `done` / `drop` use `getTaskTargets(...)` to target one task, one phase, or every task.
- `done` / `drop` use `getTaskTargets(...)` to target one task or one phase; raw validation rejects either operation when both targets are absent.
- `rm` removes one task, clears one phase's `tasks`, or clears all phases' task arrays.
- `appendItems(...)` resolves or creates the target phase and pushes new `pending` tasks unless the same task content already exists anywhere.
- `note` trims trailing whitespace, rejects empty text, and appends the note to `task.notes`.
Expand All @@ -91,10 +91,13 @@ The TUI renderer (`todoWriteToolRenderer`) merges call and result into one trans
Normalization then re-applies the single-active-task rule after the full op batch.

### Op targeting rules
- `done`, `drop`, `rm`:
- `done`, `drop`:
- `task` set: affect one exact-content task.
- else `phase` set: affect every task in that exact-name phase.
- else: affect every task in every phase.
- else `phase` must be set: affect every task in that exact-name phase.
- `rm`:
- `task` set: remove one exact-content task.
- else `phase` set: clear every task in that exact-name phase.
- else: clear every task in every phase.
- `append` is the only op that creates a missing phase.
- `note` only targets a single task.
- `init` discards previous phases entirely.
Expand Down
3 changes: 3 additions & 0 deletions packages/ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### Fixed

- `todo_write` raw argument rejections now carry bounded, authority-controlled correction codes for each rejected shape: unknown root keys, unknown operation-entry keys, done/drop entries missing a task or phase target, and unknown init list-entry keys. Each code maps to a fixed correction message naming the accepted shape (never echoing the offending input), so invalid calls surface specific guidance while valid payloads keep the existing passthrough/coercion path (#3916).
- Anthropic Sonnet 5 now exposes Anthropic's real `xhigh` and `max` thinking efforts on the Messages API (`minimal`/`low`/`medium`/`high`/`xhigh`/`max`), matching official support. The previous generic `kind === opus` gate excluded it from the full preset range; the capability predicate is now an explicit version-scoped list (Opus 4.7+, Sonnet 5+), so older Sonnet generations and Bedrock Converse routes stay fail-closed at their previously advertised levels (issue #3913).
- Alibaba Token Plan now exposes Qwen 3.8 Max under the provider-supported `qwen3.8-max` wire id instead of the rejected `qwen-3.8-max` spelling; catalog regeneration canonicalizes a legacy discovered alias rather than retaining a broken duplicate (#3909).
- Canonicalized first-class MiniMax M3 catalog ids (issue #3896). The bundled catalog previously shipped stale lowercase `minimax-m3` duplicates (512K) next to the canonical `MiniMax-M3` (1M) on all four first-class MiniMax providers, plus a non-official `minimax-v3` entry under `minimax-code`. The lowercase `minimax-m3` entries and `minimax-v3` are removed; `MiniMax-M3` is the single canonical first-class id (the regen-safe 1M pin in `applyGeneratedModelPolicy` now keys on `MiniMax-M3` / `MiniMax-M3[1m]` instead of the removed lowercase id), `DEFAULT_MODEL_PER_PROVIDER` points at `MiniMax-M3`, and the official Anthropic Token Plan id `MiniMax-M3[1m]` is first-class on the `minimax` / `minimax-cn` Anthropic routes with 1M context semantics. Unrelated catalog providers keep their own `minimax-m3` contracts.
## [0.12.12] - 2026-08-05

Expand Down
8 changes: 7 additions & 1 deletion packages/ai/scripts/generate-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export function injectAlibabaTokenPlanModels(models: Model[]): void {
compat: { supportsDeveloperRole: false },
};
const qwen: Model<"openai-responses"> = {
id: "qwen-3.8-max",
id: "qwen3.8-max",
name: "Qwen3.8 Max",
api: "openai-responses",
provider: "alibaba-token-plan",
Expand All @@ -143,6 +143,12 @@ export function injectAlibabaTokenPlanModels(models: Model[]): void {
maxTokens: 65_536,
compat: { supportsDeveloperRole: false },
};
for (let index = models.length - 1; index >= 0; index--) {
const model = models[index]!;
if (model.provider === "alibaba-token-plan" && model.id === "qwen-3.8-max") {
models.splice(index, 1);
}
}
for (const metadata of [deepseek, qwen]) {
const existing = models.find(model => model.provider === "alibaba-token-plan" && model.id === metadata.id);
if (existing) {
Expand Down
28 changes: 22 additions & 6 deletions packages/ai/src/model-thinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,18 @@ export function supportsAnthropicAdaptiveThinkingDisplay(modelId: string): boole
function anthropicModelHasRealXHighEffort<TApi extends Api>(model: ApiModel<TApi>): boolean {
if (model.api !== "anthropic-messages") return false;
const parsedModel = parseKnownModel(model.id);
if (parsedModel.family !== "anthropic" || parsedModel.kind !== "opus") return false;
return semverGte(parsedModel.version, "4.7");
if (parsedModel.family !== "anthropic") return false;
// Explicit capability predicate instead of a generic `kind === opus` gate:
// Sonnet 5 officially exposes Anthropic's real xhigh and max presets on
// the Messages API just like Opus 4.7+. Older Sonnet generations do not,
// so the predicate stays fail-closed for them.
if (parsedModel.kind === "opus") {
return semverGte(parsedModel.version, "4.7");
}
if (parsedModel.kind === "sonnet") {
return semverGte(parsedModel.version, "5.0");
}
return false;
}

function applyGeneratedModelPolicy(model: ApiModel<Api>): void {
Expand Down Expand Up @@ -691,10 +701,16 @@ function inferAnthropicSupportedEfforts<TApi extends Api>(
// Converse lacks it (same split as Opus 4.7+ below).
return model.api === "anthropic-messages" ? DEFAULT_REASONING_EFFORTS_WITH_XHIGH : DEFAULT_REASONING_EFFORTS;
}
if (parsedModel.kind !== "opus") return DEFAULT_REASONING_EFFORTS;
return anthropicModelHasRealXHighEffort(model)
? DEFAULT_REASONING_EFFORTS_WITH_XHIGH_AND_MAX
: DEFAULT_REASONING_EFFORTS_WITH_MAX;
if (anthropicModelHasRealXHighEffort(model)) {
// Opus 4.7+ and Sonnet 5 expose both Anthropic's real xhigh and
// max presets on the Messages API.
return DEFAULT_REASONING_EFFORTS_WITH_XHIGH_AND_MAX;
}
if (parsedModel.kind === "opus") {
// Opus 4.6 exposes max but not the newer xhigh literal.
return DEFAULT_REASONING_EFFORTS_WITH_MAX;
}
return DEFAULT_REASONING_EFFORTS;
}
return inferFallbackEfforts(model);
}
Expand Down
18 changes: 9 additions & 9 deletions packages/ai/src/models.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@
"maxLevel": "xhigh"
}
},
"qwen-3.8-max": {
"id": "qwen-3.8-max",
"qwen3.8-max": {
"id": "qwen3.8-max",
"name": "Qwen3.8 Max",
"api": "openai-responses",
"provider": "alibaba-token-plan",
Expand Down Expand Up @@ -3952,7 +3952,7 @@
"thinking": {
"mode": "anthropic-adaptive",
"minLevel": "minimal",
"maxLevel": "high"
"maxLevel": "max"
}
},
"claude-opus-5": {
Expand Down Expand Up @@ -4740,7 +4740,7 @@
"thinking": {
"mode": "anthropic-adaptive",
"minLevel": "minimal",
"maxLevel": "high"
"maxLevel": "max"
}
},
"claude-sonnet-4-5": {
Expand Down Expand Up @@ -9866,7 +9866,7 @@
"thinking": {
"mode": "anthropic-adaptive",
"minLevel": "minimal",
"maxLevel": "high"
"maxLevel": "max"
}
},
"gemini-2.5-pro": {
Expand Down Expand Up @@ -60352,7 +60352,7 @@
"thinking": {
"mode": "anthropic-adaptive",
"minLevel": "minimal",
"maxLevel": "high"
"maxLevel": "max"
}
},
"deepseek-v4-flash": {
Expand Down Expand Up @@ -75349,7 +75349,7 @@
"thinking": {
"mode": "anthropic-adaptive",
"minLevel": "minimal",
"maxLevel": "high"
"maxLevel": "max"
}
},
"arcee-ai/trinity-large-preview": {
Expand Down Expand Up @@ -81626,7 +81626,7 @@
"thinking": {
"mode": "anthropic-adaptive",
"minLevel": "minimal",
"maxLevel": "high"
"maxLevel": "max"
}
},
"anthropic/claude-sonnet-5-free": {
Expand All @@ -81651,7 +81651,7 @@
"thinking": {
"mode": "anthropic-adaptive",
"minLevel": "minimal",
"maxLevel": "high"
"maxLevel": "max"
}
},
"baidu/ernie-5.0-thinking-preview": {
Expand Down
6 changes: 5 additions & 1 deletion packages/ai/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,11 @@ export type Static<S> = S extends ZodType ? z.infer<S> : S extends { static: inf
export type RawArgumentRejectionCode =
| "ask-intent-review-requires-positive-round"
| "ask-intent-contract-requires-non-empty-authority"
| "ask-deep-interview-metadata-requires-deep-interview-gate";
| "ask-deep-interview-metadata-requires-deep-interview-gate"
| "todo-write-unknown-root-key"
| "todo-write-unknown-op-entry-key"
| "todo-write-done-drop-requires-target"
| "todo-write-unknown-init-entry-key";

export type RawArgumentValidationResult =
| { outcome: "passthrough" }
Expand Down
5 changes: 5 additions & 0 deletions packages/ai/src/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,11 @@ const RAW_ARGUMENT_REJECTION_MESSAGES: Record<RawArgumentRejectionCode, string>
"deepInterview.intent_contract requires non-empty items and confirmation_options",
"ask-deep-interview-metadata-requires-deep-interview-gate":
"deepInterview metadata cannot be combined with a non-deep-interview workflowGate",
"todo-write-unknown-root-key": "todo_write root accepts only an ops array of operation entries",
"todo-write-unknown-op-entry-key":
"todo_write operation entries accept only op, list, task, phase, items, and text keys",
"todo-write-done-drop-requires-target": "todo_write done and drop entries require a task or phase target",
"todo-write-unknown-init-entry-key": "todo_write init list entries accept only phase and items keys",
};

/**
Expand Down
27 changes: 26 additions & 1 deletion packages/ai/test/generate-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe("injectAlibabaTokenPlanModels", () => {
maxTokens: 384_000,
}),
expect.objectContaining({
id: "qwen-3.8-max",
id: "qwen3.8-max",
name: "Qwen3.8 Max",
api: "openai-responses",
provider: "alibaba-token-plan",
Expand All @@ -60,4 +60,29 @@ describe("injectAlibabaTokenPlanModels", () => {
}),
]);
});

it("removes every legacy Qwen 3.8 Max alias before restoring the canonical model", () => {
const legacy = (): Model<"openai-responses"> => ({
id: "qwen-3.8-max",
name: "Legacy Qwen",
api: "openai-responses",
provider: "alibaba-token-plan",
baseUrl: "https://example.invalid",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1,
maxTokens: 1,
});
const models: Model[] = [legacy(), legacy(), { ...legacy(), id: "qwen3.8-max" }];

injectAlibabaTokenPlanModels(models);

expect(models.filter(model => model.provider === "alibaba-token-plan" && model.id === "qwen-3.8-max")).toEqual(
[],
);
expect(
models.filter(model => model.provider === "alibaba-token-plan" && model.id === "qwen3.8-max"),
).toHaveLength(1);
});
});
22 changes: 21 additions & 1 deletion packages/ai/test/model-thinking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ describe("model thinking metadata", () => {
api: "anthropic-messages",
provider: "anthropic",
});
const sonnet5Bedrock = createModel({
id: "us.anthropic.claude-sonnet-5",
api: "bedrock-converse-stream",
provider: "amazon-bedrock",
});

expect(opus45.thinking?.mode).toBe("anthropic-budget-effort");
expect(opus46.thinking?.mode).toBe("anthropic-adaptive");
Expand All @@ -156,7 +161,7 @@ describe("model thinking metadata", () => {
expect(sonnet5.thinking).toEqual({
mode: "anthropic-adaptive",
minLevel: Effort.Minimal,
maxLevel: Effort.High,
maxLevel: Effort.Max,
});
// Older Opus adaptive models expose max but not the newer xhigh literal.
expect(() => mapEffortToAnthropicAdaptiveEffort(opus46, Effort.XHigh)).toThrow(/not supported/);
Expand All @@ -169,7 +174,22 @@ describe("model thinking metadata", () => {
expect(mapEffortToAnthropicAdaptiveEffort(opus47Bedrock, Effort.Max)).toBe("max");
expect(() => mapEffortToAnthropicAdaptiveEffort(sonnet46, Effort.XHigh)).toThrow(/not supported/);
expect(() => mapEffortToAnthropicAdaptiveEffort(sonnet46, Effort.Max)).toThrow(/not supported/);
// Sonnet 5 officially exposes both Anthropic's real xhigh and max presets.
expect(mapEffortToAnthropicAdaptiveEffort(sonnet5, Effort.High)).toBe("high");
expect(mapEffortToAnthropicAdaptiveEffort(sonnet5, Effort.XHigh)).toBe("xhigh");
expect(mapEffortToAnthropicAdaptiveEffort(sonnet5, Effort.Max)).toBe("max");
expect(requireSupportedEffort(sonnet5, Effort.XHigh)).toBe(Effort.XHigh);
expect(requireSupportedEffort(sonnet5, Effort.Max)).toBe(Effort.Max);
expect(clampThinkingLevelForModel(sonnet5, Effort.XHigh)).toBe(Effort.XHigh);
expect(clampThinkingLevelForModel(sonnet5, Effort.Max)).toBe(Effort.Max);
// Older Sonnet generations stay fail-closed: no xhigh, no max.
expect(() => requireSupportedEffort(sonnet46, Effort.XHigh)).toThrow(/not supported/);
expect(() => requireSupportedEffort(sonnet46, Effort.Max)).toThrow(/not supported/);
// Bedrock Converse lacks the Messages-only xhigh preset, so Bedrock
// Sonnet 5 stays clamped to high (no xhigh, no max).
expect(sonnet5Bedrock.thinking?.maxLevel).toBe(Effort.High);
expect(() => mapEffortToAnthropicAdaptiveEffort(sonnet5Bedrock, Effort.XHigh)).toThrow(/not supported/);
expect(() => mapEffortToAnthropicAdaptiveEffort(sonnet5Bedrock, Effort.Max)).toThrow(/not supported/);
});

it("classifies Fable 5 as adaptive thinking with xhigh support (discovery metadata regression)", () => {
Expand Down
Loading
Loading