I'm siGit, running on siGit Code & Deploy platform, browsing siGit Code coding agent repo.
Add --model to sigit run for picking the cloud tier
Headless runs always used the default cloud tier, so when it was unavailable a script or sandbox runner could only switch by pointing the custom-endpoint override at siGit Code Cloud. --model takes a tier name or its onde-* model id, is validated at parse time (unknown names exit 2), and wins over the provider override and the local-inference setting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seto Elkahfi committed
Oct 26, 2026 at 13:55 UTC
b152ec94a3d54306c349bfb35c615e25a8ac6047
5 files changed
+128
-17
.agents/AGENTS.md
+4
-1
@@ -458,7 +458,10 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
458
clients. The legacy `-p` form reaches the same parser. Every new run gets a UUID session id;
459
`--resume <id>` restores that session through `session_store`, and `--output jsonl` emits a
460
structured session/delta/tool/result stream while logs stay on stderr. Headless permission
461
- prompts collapse to denial unless the tool was pre-approved with `--allow-tool`.
461
+ prompts collapse to denial unless the tool was pre-approved with `--allow-tool`. The backend is
462
+ the provider override, else the default cloud tier when local inference is off; `--model <tier>`
463
+ (validated by `provider::cloud_tier_named`, a tier or its `onde-*` id) picks a cloud tier
464
+ instead and wins over both.
465
- **`crates/cli/src/session_store.rs`** — durable conversations: one JSON-lines history file per session at
466
`$SIGIT_CONFIG_DIR/sessions/<id>.jsonl`, written atomically, restorable into either backend.
467
Each save also writes a `<id>.meta.json` sidecar naming the session's `cwd` and the extra roots
CHANGELOG.md
+10
@@ -1,5 +1,15 @@
1
# Changelog
2
3
+## Unreleased
4
+
5
+### Added
6
+
7
+- **`sigit run --model <tier>` picks the siGit Code Cloud tier for a headless run.** Headless
8
+ runs always used the default tier, so when that tier was unavailable a script or sandbox
9
+ runner had no way to pick another without pointing the custom-endpoint override at siGit Code
10
+ Cloud. The flag takes a tier name (`prism`) or its model id (`onde-prism`) and wins over the
11
+ provider override and the local-inference setting.
12
+
13
## 1.7.6
14
15
A background-task tool for long commands, model pickers grouped by where the
crates/cli/src/headless.rs
+73
-16
@@ -30,7 +30,7 @@ use crate::{permissions, provider, session_store, settings, tools};
30
pub const USAGE: &str = "Usage: sigit run \"<prompt>\" [--cwd <dir>] [--add-dir <dir>]... \
31
[--output text|jsonl] [--quiet] [--resume <session-id>] \
32
[--allow-tool <name>]... [--deny-tool <name>]... \
33
- [--max-tool-rounds <n>]\n \
33
+ [--max-tool-rounds <n>] [--model <tier>]\n \
34
sigit -p \"<prompt>\" [same options]";
35
36
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
@@ -67,6 +67,9 @@ pub struct HeadlessConfig {
67
/// asked for its final message. `--max-tool-rounds`, else
68
/// `SIGIT_MAX_TOOL_ROUNDS`, else the built-in cap.
69
pub max_tool_rounds: usize,
70
+ /// siGit Code Cloud tier named by `--model`. When set it wins over the
71
+ /// provider override and the local-inference setting.
72
+ pub cloud_tier: Option<&'static str>,
73
}
74
75
/// Upper bound for `--max-tool-rounds` / `SIGIT_MAX_TOOL_ROUNDS`.
@@ -147,6 +150,7 @@ pub fn parse_args(args: &[String]) -> Result<Option<HeadlessConfig>, String> {
150
let mut allow_tools: Vec<String> = Vec::new();
151
let mut deny_tools: Vec<String> = Vec::new();
152
let mut max_tool_rounds: Option<usize> = None;
153
+ let mut cloud_tier: Option<&'static str> = None;
154
155
let mut iter = args.iter().skip(usize::from(run_command));
156
while let Some(arg) = iter.next() {
@@ -211,6 +215,21 @@ pub fn parse_args(args: &[String]) -> Result<Option<HeadlessConfig>, String> {
215
.ok_or_else(|| "--max-tool-rounds requires a number".to_string())?;
216
max_tool_rounds = Some(parse_tool_rounds(value, "--max-tool-rounds")?);
217
}
218
+ "--model" => {
219
+ let value = iter
220
+ .next()
221
+ .ok_or_else(|| "--model requires a cloud tier".to_string())?;
222
+ if cloud_tier.is_some() {
223
+ return Err("--model was given more than once".to_string());
224
+ }
225
+ let tier = provider::cloud_tier_named(value).ok_or_else(|| {
226
+ format!(
227
+ "unknown cloud tier: {value} (expected one of: {})",
228
+ provider::CLOUD_TIERS.join(", ")
229
+ )
230
+ })?;
231
+ cloud_tier = Some(tier);
232
+ }
233
// A mistyped flag must not become the prompt. A prompt that
234
// really starts with '-' can still go through -p.
235
other if other.starts_with('-') => return Err(format!("unknown argument: {other}")),
@@ -258,6 +277,7 @@ pub fn parse_args(args: &[String]) -> Result<Option<HeadlessConfig>, String> {
277
allow_tools,
278
deny_tools,
279
max_tool_rounds,
280
+ cloud_tier,
281
}))
282
}
283
@@ -312,22 +332,32 @@ pub async fn run(config: HeadlessConfig) -> i32 {
332
// first, else the signed-in cloud tier when local inference is off (what
333
// `apply_startup_inference_mode` does at every ACP session entry). There is
334
// never an implicit on-device load — a fresh process has no model in memory
315
- // and headless mode must not silently download gigabytes.
316
- let provider_cfg = provider::active_provider().or_else(|| {
317
- if settings::local_inference_enabled() {
318
- None
319
- } else {
320
- provider::cloud_tier_provider(provider::DEFAULT_CLOUD_TIER)
321
- }
322
- });
335
+ // and headless mode must not silently download gigabytes. `--model` is the
336
+ // exception to that order: a tier named on the command line wins over the
337
+ // override and the setting, the way a flag wins over the environment.
338
+ let provider_cfg = match config.cloud_tier {
339
+ Some(tier) => provider::cloud_tier_provider(tier),
340
+ None => provider::active_provider().or_else(|| {
341
+ if settings::local_inference_enabled() {
342
+ None
343
+ } else {
344
+ provider::cloud_tier_provider(provider::DEFAULT_CLOUD_TIER)
345
+ }
346
+ }),
347
+ };
348
let Some(cfg) = provider_cfg else {
324
- emit_error(
325
- &config,
326
- "headless mode needs a remote inference provider — running on-device would require \
327
- loading (and possibly downloading) a local model, which headless mode never does \
328
- implicitly. Set OPENAI_BASE_URL and OPENAI_API_KEY (or configure providers.toml), \
329
- or sign in with `sigit login` and turn local inference off.",
330
- );
349
+ let message = match config.cloud_tier {
350
+ Some(tier) => format!(
351
+ "--model {tier} needs a siGit Code Cloud session. Sign in with `sigit login`."
352
+ ),
353
+ None => "headless mode needs a remote inference provider — running on-device would \
354
+ require loading (and possibly downloading) a local model, which headless \
355
+ mode never does implicitly. Set OPENAI_BASE_URL and OPENAI_API_KEY (or \
356
+ configure providers.toml), or sign in with `sigit login` and turn local \
357
+ inference off."
358
+ .to_string(),
359
+ };
360
+ emit_error(&config, &message);
361
return 1;
362
};
363
@@ -814,6 +844,32 @@ mod tests {
844
assert!(resolve_max_tool_rounds(None, Some("0")).is_err());
845
}
846
847
+ #[test]
848
+ fn model_flag_names_a_cloud_tier() {
849
+ let config = parse_args(&args(&["run", "go"])).unwrap().unwrap();
850
+ assert_eq!(config.cloud_tier, None);
851
+ let config = parse_args(&args(&["run", "go", "--model", "prism"]))
852
+ .unwrap()
853
+ .unwrap();
854
+ assert_eq!(config.cloud_tier, Some("prism"));
855
+ // The wire id works too, so a model id copied from logs is accepted.
856
+ let config = parse_args(&args(&["-p", "go", "--model", "onde-zenith"]))
857
+ .unwrap()
858
+ .unwrap();
859
+ assert_eq!(config.cloud_tier, Some("zenith"));
860
+ }
861
+
862
+ #[test]
863
+ fn model_flag_rejects_unknown_and_repeated_tiers() {
864
+ let error = parse_args(&args(&["run", "go", "--model", "gpt-4o"])).unwrap_err();
865
+ assert!(error.starts_with("unknown cloud tier: gpt-4o"), "{error}");
866
+ assert!(error.contains("prism"), "{error}");
867
+ assert_eq!(
868
+ parse_args(&args(&["run", "go", "--model", "apex", "--model", "flare"])),
869
+ Err("--model was given more than once".to_string())
870
+ );
871
+ }
872
+
873
#[test]
874
fn wrap_up_prompt_names_the_cap_only_when_it_was_reached() {
875
let capped = wrap_up_prompt(true, 24);
@@ -877,6 +933,7 @@ mod tests {
933
assert!(parse_args(&args(&["-p", "x", "--deny-tool"])).is_err());
934
assert!(parse_args(&args(&["run", "x", "--output"])).is_err());
935
assert!(parse_args(&args(&["run", "x", "--resume"])).is_err());
936
+ assert!(parse_args(&args(&["run", "x", "--model"])).is_err());
937
}
938
939
#[test]
crates/cli/src/provider.rs
+24
@@ -115,6 +115,17 @@ pub fn cloud_tier_provider(tier: &str) -> Option<ProviderConfig> {
115
})
116
}
117
118
+/// The cloud tier a name refers to: the tier itself (`prism`) or the model id
119
+/// sent on the wire (`onde-prism`), case-insensitively. `None` for a name that
120
+/// is neither, so a caller can reject it instead of sending it to the cloud.
121
+pub fn cloud_tier_named(name: &str) -> Option<&'static str> {
122
+ let name = name.trim().to_lowercase();
123
+ CLOUD_TIERS
124
+ .iter()
125
+ .copied()
126
+ .find(|tier| *tier == name || tier_to_model(tier) == name)
127
+}
128
+
129
/// Map a neutral tier name to the model id sent on the wire. Unknown values pass
130
/// through unchanged so an explicit model id still works.
131
fn tier_to_model(tier: &str) -> String {
@@ -325,6 +336,19 @@ mod tests {
336
assert_eq!(tier_to_model(DEFAULT_CLOUD_TIER), "onde-nova");
337
}
338
339
+ #[test]
340
+ fn cloud_tier_named_accepts_tiers_and_wire_ids() {
341
+ assert_eq!(cloud_tier_named("prism"), Some("prism"));
342
+ assert_eq!(cloud_tier_named(" Zenith "), Some("zenith"));
343
+ assert_eq!(cloud_tier_named("onde-flare"), Some("flare"));
344
+ // The wire id of `oke` does not follow the `onde-<tier>` pattern.
345
+ assert_eq!(cloud_tier_named("onde-kkk"), Some("oke"));
346
+ assert_eq!(cloud_tier_named("oke"), Some("oke"));
347
+ assert_eq!(cloud_tier_named("gpt-4o-mini"), None);
348
+ assert_eq!(cloud_tier_named("onde-"), None);
349
+ assert_eq!(cloud_tier_named(""), None);
350
+ }
351
+
352
#[test]
353
fn maps_every_cloud_tier_to_its_own_onde_model_id() {
354
for tier in CLOUD_TIERS {
docs/headless.md
+17
@@ -27,6 +27,23 @@ for interactive permission are denied in headless mode unless granted with
27
`--allow-tool <name>`. A repeatable `--deny-tool <name>` takes precedence over grants and
28
settings.
29
30
+## Choosing the model
31
+
32
+With no flag, a run uses the provider override (`OPENAI_BASE_URL` and `OPENAI_API_KEY`, or the
33
+active profile in `providers.toml`) when there is one, and otherwise the default siGit Code Cloud
34
+tier when you are signed in and local inference is off. `--model <tier>` picks a siGit Code
35
+Cloud tier for the run instead:
36
+
37
+```sh
38
+sigit run "Run the focused tests" --model prism --allow-tool run_command
39
+```
40
+
41
+The value is a tier name from `/models` (`prism`) or the model id sent on the wire
42
+(`onde-prism`). An unknown name is a usage error, exit code 2. The flag wins over the provider
43
+override and the local-inference setting, and it needs a signed-in session.
44
+
45
+## Tool rounds
46
+
47
A run makes at most 24 rounds of tool calls. `--max-tool-rounds <n>`, or the
48
`SIGIT_MAX_TOOL_ROUNDS` environment variable, sets a different cap from 1 to 500; the flag wins.
49
When the cap is reached, or the model otherwise ends without saying anything, siGit Code asks it