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
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,16 +480,25 @@ not available on `PATH`.

## Configuration

Select a model for one headless prompt without changing the saved default:
Select a model and reasoning effort for one headless prompt without changing the
saved default:

```bash
zcode --model account:zai-individual-coding-plan/GLM-5.3-Flash --prompt 'Reply with exactly: ok'
zcode --model account:zai-individual-coding-plan/GLM-5.3-Flash --effort high --prompt 'Reply with exactly: ok'
```

`--model` requires a full provider/model ID and `--prompt` (or `-p`). It also
overrides a resumed session's model for that run. Unknown or unavailable models
fail before a prompt is sent, rather than falling back to the default.

`--effort` selects the model's reasoning level for that prompt. Levels are
model-specific (for example `disabled`, `low`, `high` and `max`); run the TUI's
`/effort list` for the current model, or inspect `optionSpecs.reasoningLevel` in
the provider catalog. `--effort auto` keeps the current or resumed default.
Without `--effort`, `--model` uses the model's default level. Both flags require
`--prompt` (or `-p`) and leave the saved default for new sessions unchanged; the
run's model and effort are recorded on that session.

ZCode reads MCP servers, hooks, plugins, permissions, network, storage and display
settings from `~/.zcode/cli/setting.json` (or
`%USERPROFILE%\.zcode\cli\setting.json` on Windows). Project settings use
Expand Down
1 change: 1 addition & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ reported rather than replaced by old settings.
| Providers and model metadata | Shared native `provider_config.json` |
| Default model in `/settings` | Writes the shared default and applies it to the current session |
| `/model`, model cycling and reasoning effort | Change/persist this session's native selection; shared default stays unchanged |
| `--model` / `--effort` on `--prompt` | Per-run model and reasoning-level override; the saved default for new sessions stays unchanged |
| `/new` | Reads the current shared default |
| Resume/restart | Restores the session's model and reasoning options |
| Language and memory | Reads Desktop `localePreference` / `memoryEnabled`; explicit CLI settings override these values |
Expand Down
1 change: 1 addition & 0 deletions docs/CONFIGURATION.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ CLI 专属字段迁移到 `~/.zcode/cli/setting.json`,省略 provider、main/l
| Provider 和模型元数据 | 共享的原生 `provider_config.json` |
| `/settings` 中的默认模型 | 写入共享默认选择,并应用到当前会话 |
| `/model`、模型循环切换、推理强度 | 修改并保存当前会话选择,不改共享默认值 |
| `--prompt` 上的 `--model`/`--effort` | 仅覆盖本次运行的模型和推理强度;不改共享默认值(会话会记录本次选择) |
| `/new` | 读取当前共享默认模型 |
| 恢复会话/重启 | 恢复该会话保存的模型和推理选项 |
| 语言与记忆 | 读取桌面端 `localePreference`/`memoryEnabled`,显式 CLI 设置优先 |
Expand Down
1 change: 1 addition & 0 deletions scripts/check-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ if (patchRuntimeLoginModelDefaults(runtimeSource) !== runtimeSource
|| !runtimeSource.includes('ZCODE_CLI_OAUTH_CALLBACK_STDIN==="1"')
|| !runtimeSource.includes("ZCODE_CLI_CREDENTIALS_FILE")
|| !runtimeSource.includes("ZCODE_CLI_PROMPT_MODEL")
|| !runtimeSource.includes("ZCODE_CLI_PROMPT_EFFORT")
|| !runtimeSource.includes(".loadSessionTranscript=async()=>await(await")
|| !runtimeSource.includes('"loadSessionContextMessages"')
|| !runtimeSource.includes(".readGoal=async()=>await(await")
Expand Down
68 changes: 58 additions & 10 deletions scripts/sync-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1401,22 +1401,69 @@ export function patchRuntimeStreamEofFinishGuard(runtime: string): string {
}

const restoredPromptModelSwitch = /([A-Za-z_$][\w$]*)\.cliDefaultSelection&&\(([A-Za-z_$][\w$]*)\?\.cliDefault&&await \1\.prepareResume\(\),!\2\?\.transient\|\|\2\?\.cliDefault\)\?/u;
const promptModelMarker = "$zCliDefaultPromptModel";
const promptEffortMarker = "$zCliDefaultPromptEffort";
const legacyPromptModelInsertion = /let \$zCliDefaultPromptModel=process\.env\.ZCODE_CLI_PROMPT_MODEL;if\(\$zCliDefaultPromptModel\)await ([A-Za-z_$][\w$]*)\.setModel\(\$zCliDefaultPromptModel,\{transient:true,cliDefault:true\}\);/u;

function promptOverrideInsertion(app: string): string {
// The string selection path intentionally fills a missing reasoning level
// with the model default. An explicit --effort must use the object path so
// the requested level survives; transient avoids a separate persisted
// model-default change.
return [
`let ${promptModelMarker}=process.env.ZCODE_CLI_PROMPT_MODEL;`,
`let ${promptEffortMarker}=process.env.ZCODE_CLI_PROMPT_EFFORT;`,
`if(${promptEffortMarker}&&${promptEffortMarker}.toLowerCase()==="auto")${promptEffortMarker}=void 0;`,
`if(${promptModelMarker}&&!${promptEffortMarker}){await ${app}.setModel(${promptModelMarker},{transient:true,cliDefault:true});}`,
`else if(${promptEffortMarker}){`,
`let $zPromptSelection,$zPromptCurrent;`,
`if(${promptModelMarker}){`,
`let $zPromptSlash=${promptModelMarker}.indexOf("/");`,
`if($zPromptSlash<=0||$zPromptSlash===${promptModelMarker}.length-1)throw new Error("Invalid prompt model selection.");`,
`$zPromptSelection={providerId:${promptModelMarker}.slice(0,$zPromptSlash),modelId:${promptModelMarker}.slice($zPromptSlash+1)};`,
`}else{`,
`await ${app}.readExecutionState?.();`,
`$zPromptCurrent=${app}.getCurrentModelOption?.();`,
`if(!$zPromptCurrent?.ref)throw new Error("Cannot apply --effort without a current model. Pass --model or select a model first.");`,
`$zPromptSelection={...$zPromptCurrent.ref};`,
`}`,
`let $zPromptOption=${app}.getModelOption?.($zPromptSelection);`,
`let $zPromptLevels=($zPromptOption?.reasoning?.levels??$zPromptCurrent?.reasoning?.levels??[]).map($zLevel=>$zLevel.value);`,
`if($zPromptOption&&$zPromptLevels.length===0)throw new Error("Model "+$zPromptSelection.providerId+"/"+$zPromptSelection.modelId+" does not support reasoning effort.");`,
`let $zPromptLevel=$zPromptLevels.find($zLevel=>$zLevel===${promptEffortMarker})??$zPromptLevels.find($zLevel=>$zLevel.toLowerCase()===${promptEffortMarker}.toLowerCase());`,
`if($zPromptLevels.length>0&&!$zPromptLevel)throw new Error("Unsupported reasoning effort: "+${promptEffortMarker}+". Available: "+$zPromptLevels.join(", ")+".");`,
`await ${app}.setModel({...$zPromptSelection,options:{...$zPromptSelection.options,reasoningLevel:$zPromptLevel??${promptEffortMarker}}},{transient:true,cliDefault:true});`,
`}`
].join("");
}

export function patchRuntimePromptModel(runtime: string): string {
const marker = "$zCliDefaultPromptModel";
if (runtime.includes(marker) && restoredPromptModelSwitch.test(runtime)) return runtime;
if (runtime.includes(promptEffortMarker) && restoredPromptModelSwitch.test(runtime)) return runtime;
const anchor = /if\([A-Za-z_$][\w$]*=([A-Za-z_$][\w$]*)\.traceId,[A-Za-z_$][\w$]*\.memoryBench/u.exec(runtime);
if (!anchor) throw new Error("ZCode runtime is incompatible with the prompt model patch.");
// cli-default prompt admission reads CliDefaultSelection, not the runtime
// selection. A transient --model override must update that default without
// selection. A transient prompt override must update that default without
// persisting a session model change.
const switchAnchor = /([A-Za-z_$][\w$]*)\.cliDefaultSelection&&(?:!([A-Za-z_$][\w$]*)\?\.transient|\(!([A-Za-z_$][\w$]*)\?\.transient\|\|\3\?\.cliDefault\))\?/u.exec(runtime);
if (!switchAnchor) throw new Error("ZCode runtime is incompatible with the prompt model patch (cli-default selection anchor missing).");
const insertion = `let ${marker}=process.env.ZCODE_CLI_PROMPT_MODEL;if(${marker})await ${anchor[1]}.setModel(${marker},{transient:true,cliDefault:true});`;
let patched = runtime.includes(marker) ? runtime : runtime.slice(0, anchor.index) + insertion + runtime.slice(anchor.index);
const context = switchAnchor[1];
const options = switchAnchor[2] ?? switchAnchor[3];
patched = patched.replace(switchAnchor[0], `${context}.cliDefaultSelection&&(${options}?.cliDefault&&await ${context}.prepareResume(),!${options}?.transient||${options}?.cliDefault)?`);
const switchAlreadyPatched = restoredPromptModelSwitch.test(runtime);
if (!switchAnchor && !switchAlreadyPatched) {
throw new Error("ZCode runtime is incompatible with the prompt model patch (cli-default selection anchor missing).");
}
const insertion = promptOverrideInsertion(anchor[1]);
let patched: string;
if (legacyPromptModelInsertion.test(runtime)) {
// Upgrade a runtime that only has the earlier --model insertion.
patched = runtime.replace(legacyPromptModelInsertion, insertion);
} else if (runtime.includes(promptModelMarker)) {
throw new Error("ZCode runtime is incompatible with the prompt model patch (partial prompt override).");
} else {
patched = runtime.slice(0, anchor.index) + insertion + runtime.slice(anchor.index);
}
if (switchAnchor) {
const context = switchAnchor[1];
const options = switchAnchor[2] ?? switchAnchor[3];
patched = patched.replace(switchAnchor[0], `${context}.cliDefaultSelection&&(${options}?.cliDefault&&await ${context}.prepareResume(),!${options}?.transient||${options}?.cliDefault)?`);
}
return patched;
}

Expand Down Expand Up @@ -1869,7 +1916,8 @@ export const runtimePatchPlan: readonly RuntimePatchDefinition[] = [
id: "prompt-model",
requirement: "required",
apply: patchRuntimePromptModel,
verify: (runtime) => runtime.includes("$zCliDefaultPromptModel") && runtime.includes("cliDefault:true")
verify: (runtime) => runtime.includes(promptModelMarker) && runtime.includes(promptEffortMarker)
&& runtime.includes("ZCODE_CLI_PROMPT_EFFORT") && runtime.includes("cliDefault:true")
&& restoredPromptModelSwitch.test(runtime)
},
{
Expand Down
11 changes: 8 additions & 3 deletions src/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
import { requestAppServer } from "./app-server-client.ts";
import { runPluginCommand } from "./plugin-cli.ts";
import { missingCodingPlanKey } from "./prompt-preflight.ts";
import { extractPromptModel } from "./prompt-model.ts";
import { extractPromptOverrides } from "./prompt-model.ts";
import {
capabilitiesFromExtractionMetadata,
type RuntimeCliOptionType
Expand Down Expand Up @@ -290,6 +290,7 @@ function runtimeEnvironment(extra: NodeJS.ProcessEnv = {}, inspectConfiguration
delete env.ZCODE_CLI_OAUTH_CALLBACK_STDIN;
delete env.ZCODE_CLI_MIGRATE_CONFIG;
delete env.ZCODE_CLI_PROMPT_MODEL;
delete env.ZCODE_CLI_PROMPT_EFFORT;
const distributionVersion = readDistributionVersion();
const appVersion = readJsonVersion(extractionMetadataPath, "appVersion");
const inherited: NodeJS.ProcessEnv = {
Expand Down Expand Up @@ -488,10 +489,12 @@ export async function main(args: string[]): Promise<number> {
}

let promptModel: string | undefined;
let promptEffort: string | undefined;
try {
const parsed = extractPromptModel(args, readRuntimeCliOptionTypes());
const parsed = extractPromptOverrides(args, readRuntimeCliOptionTypes());
args = parsed.args;
promptModel = parsed.model;
promptEffort = parsed.effort;
} catch (error) {
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
return 1;
Expand Down Expand Up @@ -639,10 +642,12 @@ export async function main(args: string[]): Promise<number> {
const runtimeArgs = withDefaultBrowserUse(login.args);
const code = await runRuntime(node, runtimeArgs, {
...firstRunSetupEnv(setupPending, runtimeArgs),
...(promptModel ? { ZCODE_CLI_PROMPT_MODEL: promptModel } : {})
...(promptModel ? { ZCODE_CLI_PROMPT_MODEL: promptModel } : {}),
...(promptEffort ? { ZCODE_CLI_PROMPT_EFFORT: promptEffort } : {})
});
if (code === 0 && (args.includes("--help") || args.includes("-h"))) {
console.log("\n --model <provider/model> Select a model for this --prompt run without changing the saved default.");
console.log(" --effort <level> Select the model reasoning effort for this --prompt run; auto keeps the default.");
}
return code;
} catch (error) {
Expand Down
65 changes: 51 additions & 14 deletions src/prompt-model.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,57 @@
import { parseArgs } from "node:util";
import type { RuntimeCliOptionType } from "./runtime-capabilities.ts";

export function extractPromptModel(
export interface PromptOverrides {
args: string[];
effort?: string;
model?: string;
}

function removalIndexes(token: { index: number; inlineValue?: boolean }): number[] {
return token.inlineValue ? [token.index] : [token.index, token.index + 1];
}

export function extractPromptOverrides(
args: string[], optionTypes: Readonly<Record<string, RuntimeCliOptionType>>
): { args: string[]; model?: string } {
): PromptOverrides {
const options = Object.fromEntries(Object.entries(optionTypes).map(([name, type]) => [name, { type }]));
const parsed = parseArgs({ args, allowPositionals: true, strict: false, tokens: true,
options: { ...options, prompt: { type: "string", short: "p" }, model: { type: "string" } } });
const tokens = parsed.tokens.filter(token => token.kind === "option" && token.name === "model");
if (tokens.length === 0) return { args };
if (tokens.length !== 1) throw new Error("Specify --model only once.");
if (typeof parsed.values.prompt !== "string") throw new Error("--model requires --prompt or -p.");
const model = String(parsed.values.model).trim();
if (!/^[^/\s]+\/\S+$/u.test(model)) throw new Error("--model must be a provider/model ID.");
const token = tokens[0]!;
const removed = new Set([token.index]);
if (token.kind === "option" && !token.inlineValue) removed.add(token.index + 1);
return { args: args.filter((_, index) => !removed.has(index)), model };
const parsed = parseArgs({
args,
allowPositionals: true,
strict: false,
tokens: true,
options: {
...options,
prompt: { type: "string", short: "p" },
model: { type: "string" },
effort: { type: "string" }
}
});
const modelTokens = parsed.tokens.filter(token => token.kind === "option" && token.name === "model");
const effortTokens = parsed.tokens.filter(token => token.kind === "option" && token.name === "effort");
if (modelTokens.length === 0 && effortTokens.length === 0) return { args };
if (modelTokens.length !== 0 && modelTokens.length !== 1) throw new Error("Specify --model only once.");
if (effortTokens.length !== 0 && effortTokens.length !== 1) throw new Error("Specify --effort only once.");
if (typeof parsed.values.prompt !== "string") {
throw new Error(`${modelTokens.length ? "--model" : "--effort"} requires --prompt or -p.`);
}

const removed = new Set<number>();
let model: string | undefined;
let effort: string | undefined;
if (modelTokens.length === 1) {
if (typeof parsed.values.model !== "string") throw new Error("--model must be a provider/model ID.");
model = parsed.values.model.trim();
if (!/^[^/\s]+\/\S+$/u.test(model)) throw new Error("--model must be a provider/model ID.");
for (const index of removalIndexes(modelTokens[0]!)) removed.add(index);
}
if (effortTokens.length === 1) {
if (typeof parsed.values.effort !== "string") throw new Error("--effort requires a value.");
effort = parsed.values.effort.trim();
if (!effort || effort.startsWith("-") || /\s/u.test(effort)) {
throw new Error("--effort requires a single effort level.");
}
for (const index of removalIndexes(effortTokens[0]!)) removed.add(index);
}
return { args: args.filter((_, index) => !removed.has(index)), model, effort };
}
Loading
Loading