Skip to content

Commit 32cdfc5

Browse files
authored
Merge pull request #1 from Traigor/fix-local-agents-claude
Fix local agents claude
2 parents d83789d + 58b0414 commit 32cdfc5

9 files changed

Lines changed: 708 additions & 20 deletions

File tree

‎.gitignore‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,9 @@ target
2323
.claude
2424
.codex
2525
.opencode
26+
27+
docs/superpowers/*
28+
29+
docs/superpowers/plans/*
30+
31+
docs/superpowers/specs/*

‎README.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,12 @@ runtime = "codex"
9595
model = "gpt-5.4-mini"
9696
temperature = "0.1"
9797
max_output_tokens = 4096
98+
thinking_level = "extra_high"
9899
```
99100

100-
`codex_exec` writes a temporary JSON Schema file, runs `codex exec --output-schema <schema-file> --output-last-message <result-file>`, and returns the parsed result file as `parsed_json`. `claude_code_print` runs `claude -p --output-format json --allowedTools Read,Grep,Glob` and treats schema adherence as prompt-guided JSON rather than strict schema enforcement.
101+
`thinking_level` is optional and only supported by local CLI-agent drivers. For `codex_exec`, supported values are `low`, `medium`, `high`, `extra_high`, and `xhigh`; both `extra_high` and `xhigh` run Codex with `model_reasoning_effort="xhigh"`. For `claude_code_print`, supported values are Claude Code's native effort names: `low`, `medium`, `high`, `xhigh`, and `max`.
102+
103+
`codex_exec` writes a temporary JSON Schema file, runs `codex exec --output-schema <schema-file> --output-last-message <result-file>`, and returns the parsed result file as `parsed_json`. `claude_code_print` runs `claude -p --model <model> --output-format json --input-format text --json-schema <schema> --allowedTools Read,Grep,Glob`, writes the combined prompt to stdin, and returns Claude Code's JSON output as `parsed_json`. When `thinking_level` is present, it is passed as provider-specific CLI configuration. The `--json-schema` argument is included when the inference request metadata contains `json_schema`.
101104

102105
## How Bitloops calls it
103106

‎crates/bitloops-inference/src/config.rs‎

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,43 @@ pub struct ProfileConfig {
9696
pub temperature: Option<f32>,
9797
pub timeout_secs: u64,
9898
pub max_output_tokens: Option<u32>,
99+
pub thinking_level: Option<ThinkingLevel>,
99100
pub runtime_command: Option<String>,
100101
pub runtime_args: Vec<String>,
101102
pub startup_timeout_secs: u64,
102103
}
103104

105+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
106+
pub enum ThinkingLevel {
107+
Low,
108+
Medium,
109+
High,
110+
ExtraHigh,
111+
Max,
112+
}
113+
114+
impl ThinkingLevel {
115+
pub const fn codex_reasoning_effort(self) -> Option<&'static str> {
116+
match self {
117+
Self::Low => Some("low"),
118+
Self::Medium => Some("medium"),
119+
Self::High => Some("high"),
120+
Self::ExtraHigh => Some("xhigh"),
121+
Self::Max => None,
122+
}
123+
}
124+
125+
pub const fn claude_effort(self) -> &'static str {
126+
match self {
127+
Self::Low => "low",
128+
Self::Medium => "medium",
129+
Self::High => "high",
130+
Self::ExtraHigh => "xhigh",
131+
Self::Max => "max",
132+
}
133+
}
134+
}
135+
104136
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105137
pub enum ProfileTask {
106138
TextGeneration,
@@ -148,6 +180,7 @@ impl ProfileConfig {
148180
"api_key",
149181
"temperature",
150182
"max_output_tokens",
183+
"thinking_level",
151184
],
152185
)?;
153186

@@ -190,6 +223,13 @@ impl ProfileConfig {
190223
"profile '{profile_name}' field 'max_output_tokens' must be greater than 0"
191224
)));
192225
}
226+
let thinking_level = optional_thinking_level(
227+
profile_name,
228+
raw,
229+
"thinking_level",
230+
driver_spec.kind,
231+
lookup,
232+
)?;
193233

194234
let runtime_name = required_profile_string(profile_name, raw, "runtime", lookup)?;
195235
let runtime = runtimes.get(&runtime_name).ok_or_else(|| {
@@ -239,6 +279,7 @@ impl ProfileConfig {
239279
temperature: Some(temperature),
240280
timeout_secs,
241281
max_output_tokens: Some(max_output_tokens),
282+
thinking_level,
242283
runtime_command,
243284
runtime_args,
244285
startup_timeout_secs,
@@ -454,6 +495,62 @@ fn required_profile_u32(
454495
}
455496
}
456497

498+
fn optional_thinking_level(
499+
profile_name: &str,
500+
table: &Table,
501+
field_name: &str,
502+
kind: ProviderKind,
503+
lookup: &impl Fn(&str) -> Option<String>,
504+
) -> Result<Option<ThinkingLevel>, ConfigError> {
505+
let Some(raw) = optional_profile_string(profile_name, table, field_name, lookup)? else {
506+
return Ok(None);
507+
};
508+
509+
match kind {
510+
ProviderKind::CodexExec => parse_codex_thinking_level(profile_name, field_name, &raw),
511+
ProviderKind::ClaudeCodePrint => parse_claude_thinking_level(profile_name, field_name, &raw),
512+
ProviderKind::OpenAiChatCompletions | ProviderKind::OllamaChat => {
513+
Err(ConfigError::Validation(format!(
514+
"profile '{profile_name}' field '{field_name}' is only supported for local CLI-agent drivers"
515+
)))
516+
}
517+
}
518+
.map(Some)
519+
}
520+
521+
fn parse_codex_thinking_level(
522+
profile_name: &str,
523+
field_name: &str,
524+
raw: &str,
525+
) -> Result<ThinkingLevel, ConfigError> {
526+
match raw {
527+
"low" => Ok(ThinkingLevel::Low),
528+
"medium" => Ok(ThinkingLevel::Medium),
529+
"high" => Ok(ThinkingLevel::High),
530+
"extra_high" | "xhigh" => Ok(ThinkingLevel::ExtraHigh),
531+
other => Err(ConfigError::Validation(format!(
532+
"profile '{profile_name}' field '{field_name}' has unsupported value '{other}' for driver 'codex_exec'; supported values are low, medium, high, extra_high, xhigh"
533+
))),
534+
}
535+
}
536+
537+
fn parse_claude_thinking_level(
538+
profile_name: &str,
539+
field_name: &str,
540+
raw: &str,
541+
) -> Result<ThinkingLevel, ConfigError> {
542+
match raw {
543+
"low" => Ok(ThinkingLevel::Low),
544+
"medium" => Ok(ThinkingLevel::Medium),
545+
"high" => Ok(ThinkingLevel::High),
546+
"xhigh" => Ok(ThinkingLevel::ExtraHigh),
547+
"max" => Ok(ThinkingLevel::Max),
548+
other => Err(ConfigError::Validation(format!(
549+
"profile '{profile_name}' field '{field_name}' has unsupported value '{other}' for driver 'claude_code_print'; supported values are low, medium, high, xhigh, max"
550+
))),
551+
}
552+
}
553+
457554
fn validate_non_empty(
458555
profile_name: &str,
459556
field_name: &str,
@@ -802,6 +899,142 @@ mod tests {
802899
assert_eq!(profile.timeout_secs, 300);
803900
}
804901

902+
#[test]
903+
fn accepts_codex_thinking_level_extra_high() {
904+
let config = parse_config(
905+
r#"
906+
[inference.runtimes.codex]
907+
command = "codex"
908+
startup_timeout_secs = 5
909+
request_timeout_secs = 300
910+
911+
[inference.profiles.local_agent]
912+
task = "structured_generation"
913+
driver = "codex_exec"
914+
runtime = "codex"
915+
model = "gpt-5.4-mini"
916+
temperature = "0.1"
917+
max_output_tokens = 4096
918+
thinking_level = "extra_high"
919+
"#,
920+
&|_| None,
921+
)
922+
.expect("config should parse");
923+
924+
let profile = config.profile("local_agent").expect("profile should exist");
925+
assert_eq!(profile.thinking_level, Some(ThinkingLevel::ExtraHigh));
926+
}
927+
928+
#[test]
929+
fn accepts_codex_native_xhigh_alias() {
930+
let config = parse_config(
931+
r#"
932+
[inference.runtimes.codex]
933+
command = "codex"
934+
request_timeout_secs = 300
935+
936+
[inference.profiles.local_agent]
937+
task = "structured_generation"
938+
driver = "codex_exec"
939+
runtime = "codex"
940+
model = "gpt-5.4-mini"
941+
temperature = "0.1"
942+
max_output_tokens = 4096
943+
thinking_level = "xhigh"
944+
"#,
945+
&|_| None,
946+
)
947+
.expect("config should parse");
948+
949+
let profile = config.profile("local_agent").expect("profile should exist");
950+
assert_eq!(profile.thinking_level, Some(ThinkingLevel::ExtraHigh));
951+
}
952+
953+
#[test]
954+
fn accepts_claude_thinking_level_max() {
955+
let config = parse_config(
956+
r#"
957+
[inference.runtimes.claude]
958+
command = "claude"
959+
startup_timeout_secs = 5
960+
request_timeout_secs = 300
961+
962+
[inference.profiles.local_agent]
963+
task = "structured_generation"
964+
driver = "claude_code_print"
965+
runtime = "claude"
966+
model = "claude-opus-4-7"
967+
temperature = "0.1"
968+
max_output_tokens = 4096
969+
thinking_level = "max"
970+
"#,
971+
&|_| None,
972+
)
973+
.expect("config should parse");
974+
975+
let profile = config.profile("local_agent").expect("profile should exist");
976+
assert_eq!(profile.thinking_level, Some(ThinkingLevel::Max));
977+
}
978+
979+
#[test]
980+
fn rejects_unsupported_codex_thinking_level() {
981+
let error = parse_config(
982+
r#"
983+
[inference.runtimes.codex]
984+
command = "codex"
985+
request_timeout_secs = 300
986+
987+
[inference.profiles.local_agent]
988+
task = "structured_generation"
989+
driver = "codex_exec"
990+
runtime = "codex"
991+
model = "gpt-5.4-mini"
992+
temperature = "0.1"
993+
max_output_tokens = 4096
994+
thinking_level = "max"
995+
"#,
996+
&|_| None,
997+
)
998+
.expect_err("config should fail");
999+
1000+
assert!(error.to_string().contains("thinking_level"));
1001+
assert!(error.to_string().contains("codex_exec"));
1002+
assert!(
1003+
error
1004+
.to_string()
1005+
.contains("low, medium, high, extra_high, xhigh")
1006+
);
1007+
}
1008+
1009+
#[test]
1010+
fn rejects_thinking_level_for_http_profiles() {
1011+
let error = parse_config(
1012+
r#"
1013+
[inference.runtimes.bitloops_inference]
1014+
request_timeout_secs = 120
1015+
1016+
[inference.profiles.summary_local]
1017+
task = "text_generation"
1018+
driver = "openai_chat_completions"
1019+
runtime = "bitloops_inference"
1020+
model = "gpt-4.1-mini"
1021+
base_url = "https://example.com/v1/chat/completions"
1022+
temperature = "0.1"
1023+
max_output_tokens = 200
1024+
thinking_level = "high"
1025+
"#,
1026+
&|_| None,
1027+
)
1028+
.expect_err("config should fail");
1029+
1030+
assert!(error.to_string().contains("thinking_level"));
1031+
assert!(
1032+
error
1033+
.to_string()
1034+
.contains("only supported for local CLI-agent drivers")
1035+
);
1036+
}
1037+
8051038
#[test]
8061039
fn fails_when_environment_variable_is_missing() {
8071040
let error = parse_config(

‎crates/bitloops-inference/src/perf.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ impl PerfSettings {
287287
temperature: Some(self.temperature),
288288
timeout_secs: self.timeout_secs,
289289
max_output_tokens: Some(self.max_output_tokens),
290+
thinking_level: None,
290291
runtime_command: None,
291292
runtime_args: Vec::new(),
292293
startup_timeout_secs: 60,

0 commit comments

Comments
 (0)