@sigitcode-old / sigit / commits / b152ec9
Fork 1

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