Skip to content

Commit 6780890

Browse files
authored
Merge pull request #1155 from org2AI/dev/gpt56-effort-levels
fix(models): preserve GPT-5.6 effort semantics
2 parents 1a53200 + 37c3678 commit 6780890

15 files changed

Lines changed: 521 additions & 28 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# GPT-5.6 effort handling
2+
3+
## Source and behavior
4+
5+
OpenAI's [model documentation](https://learn.chatgpt.com/docs/models#know-when-to-use-max-or-ultra)
6+
distinguishes Max reasoning from Ultra's delegation mode. The desktop app may
7+
hide Max until it is enabled in settings. The public API's
8+
[GPT-5.6 model documentation](https://developers.openai.com/api/docs/models/gpt-5.6-sol)
9+
supports `xhigh` and `max`; `ultra` is not a public API effort.
10+
11+
The saved account's model variants and the local Codex catalog were inspected
12+
read-only. Both contained `xhigh`, `max`, and `ultra` for Sol/Terra, and `xhigh`
13+
and `max` for Luna. No credentials, preferences, or historical sessions were
14+
changed. No destructive remediation or schema migration is needed for those
15+
records. Live and persisted capability metadata continue to take precedence
16+
over fallback variants.
17+
18+
The producing paths had three inconsistencies:
19+
20+
- Fallback model metadata omitted Max for GPT-5.6. The fallback now emits Max
21+
for Sol, Terra, and Luna, with Ultra additionally available for Sol/Terra.
22+
- The frontend slider placed Ultra before Max, while the table/default ranking
23+
omitted Ultra. All three rankings now place Ultra after Max. Separately,
24+
the requested GPT-5.6 picker policy hides the standalone Max step by default:
25+
Sol/Terra show Light, Medium, High, Extra High, Ultra; Luna stops at Extra High
26+
because its catalog does not advertise Ultra. This policy is shared across
27+
GPT-5.6 pickers, independent of the account supplying the variants.
28+
- Rust parsing erased Ultra into Max, while the public OpenAI effort mapper
29+
also lowered `xhigh` and `max` to `high`. Parsing now retains Ultra; public
30+
API requests preserve the selected `xhigh`/`max` value. Unsupported explicit
31+
selections can fail at the provider instead of silently running lower effort.
32+
33+
Max is real capability data, not malformed data. Its UI exclusion is the
34+
requested product behavior, not data cleanup. The catalog and request resolver
35+
retain Max, and an already-applied Max selection remains visible when editing
36+
it. Opening, dismissing, or changing Fast cannot silently turn it into another
37+
effort or enable Ultra delegation. After selecting a different level, the normal
38+
menu no longer offers Max. Other model families' Max options remain unchanged.
39+
40+
Correctly applying a previously downgraded effort can increase active-request
41+
latency and usage. Ultra can additionally use the existing worker allowance;
42+
it does not change that allowance or create idle work.
43+
44+
Native Codex Ultra requests send `max` reasoning and add bounded delegation
45+
guidance to that request's instructions. This reuses existing subagent tools,
46+
permissions, worker limits, and cancellation behavior. It does not enable tools,
47+
spawn workers itself, change cached prompts, or override user restrictions.
48+
This implements ORGII's delegation guidance; it does not establish full parity
49+
with Codex's internal orchestration.
50+
51+
Ultra's slider fill, label, and focus ring use the existing purple theme token.
52+
Other levels retain the primary accent. The popup retains its existing
53+
Apply/Cancel workflow in this change.
54+
55+
## Architecture coverage
56+
57+
Layers 1–10 were considered within the changed call chain: compilation;
58+
existing shared effort mapping; names; Max/Ultra semantics; default handling;
59+
provider-specific delegation guidance; explanatory comments; serialized wire
60+
bodies; streaming/non-streaming parity; and live/persisted/fallback precedence.
61+
Unrelated session initialization, database schema, and architecture cleanup
62+
were intentionally excluded.
63+
64+
## Lifecycle checks
65+
66+
| Area | Verdict | Evidence | Change or reason kept | Verification |
67+
| ------------------ | ------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------- |
68+
| Background work | keep | Existing slider-scoped visibility listener and CSS animation | No new timers, listeners, workers, or polling | Hidden/visible and repeated-open listener tests |
69+
| Memory | keep | Fixed 18 decorative comets; per-request instruction string | No retained mode state or growing buffers | Source inspection and request tests |
70+
| Scope/isolation | keep | Ultra instructions are constructed from the current request's parsed mode | No account writes, cached-prompt mutation, or tool-permission changes | Serialized request tests preserve input and tool absence |
71+
| Rendering/hot path | keep | One derived accent state; existing native range and CSS motion | No new subscriptions; reduced-motion rules retained | Slider tests and SCSS compilation |
72+
73+
## Verification
74+
75+
- `pnpm exec vitest run --config config/vitest.config.ts src/components/ModelPropertiesDropdown/EffortSlider.test.ts src/util/__tests__/modelVariants.test.ts src/util/__tests__/variantEditOptions.test.ts`: 20 tests passed.
76+
- `pnpm exec eslint src/components/ModelPropertiesDropdown/index.tsx src/components/ModelPropertiesDropdown/EffortSlider.tsx src/components/ModelPropertiesDropdown/EffortSlider.test.ts src/util/__tests__/variantEditOptions.test.ts src/util/variantEditOptions.ts src/util/defaultModelVariant.ts src/modules/MainApp/Integrations/KeyVault/shared/ModelTable/ModelVariantInlineCard.tsx --max-warnings 0`: passed with zero warnings.
77+
- `pnpm run typecheck`: passed after the final picker policy change. The first
78+
attempt was terminated with SIGTERM before diagnostics and is not counted
79+
as a pass.
80+
- `cargo test -p agent_core -p key_vault --lib gpt_5_6` from `src-tauri`: 4 passed,
81+
including actual mocked HTTP requests through both chat transports and the
82+
fallback catalog producing boundary.
83+
- `~/.cargo/shared-target/debug/deps/agent_core-ed837004ecb79b26 core::providers:: --quiet`:
84+
the freshly built provider test executable passed 446 tests, including native
85+
Codex and public Responses serialization. Direct execution avoids rebuilding
86+
or contending for the shared Cargo target lock.
87+
- `~/.cargo/shared-target/debug/deps/key_vault-2ea898359e39b97e codex --quiet`:
88+
36 Codex catalog/discovery/credential-handling tests passed. Together with the
89+
provider suite, 482 distinct Rust tests passed.
90+
- SCSS compilation verified the purple token, matching focus ring, and retained
91+
reduced-motion rules.
92+
- `node scripts/quality/check-test-placement.mjs`: passed across 440 directories.
93+
- `rustfmt --check --edition 2021` on the six changed Rust files: passed.
94+
- `git diff --check`: passed.
95+
96+
Verification used no desktop control or live LLM requests from ORGII. Mock HTTP
97+
requests and serialized request bodies verify the integration boundary, but do
98+
not prove provider acceptance or observed automatic delegation. Full-app visual
99+
inspection and CPU/RSS measurement were not run; no runtime performance gain is
100+
claimed.
101+
102+
Performance verdict: **pass for this scoped change**. No new idle/background
103+
resources or retained state were introduced; listener cleanup and visibility
104+
gating are regression-tested. Active Ultra delegation can consume more usage
105+
within existing limits; its real-world latency and resource cost remain unmeasured.

src-tauri/crates/agent-core/src/core/providers/codex_native/client.rs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ impl CodexNativeClient {
106106
ReasoningLevel::Medium => "medium",
107107
ReasoningLevel::High => "high",
108108
ReasoningLevel::ExtraHigh => "xhigh",
109-
ReasoningLevel::Max | ReasoningLevel::Ultracode => "max",
109+
ReasoningLevel::Max | ReasoningLevel::Ultra | ReasoningLevel::Ultracode => "max",
110110
ReasoningLevel::Baseline | ReasoningLevel::None => return None,
111111
})
112112
}
@@ -146,14 +146,29 @@ impl CodexNativeClient {
146146
);
147147
let reasoning = Self::codex_reasoning_effort(parsed.level)
148148
.map(|effort| serde_json::json!({ "effort": effort }));
149+
let mut instructions = Self::required_instructions(instructions);
150+
// Ultra is Max reasoning plus a delegation policy, not an API effort
151+
// named `ultra`. Apply it per request so changing effort cannot leave
152+
// stale mode instructions in the session's cached system prompt.
153+
if parsed.level == Some(crate::providers::thinking_mode::ReasoningLevel::Ultra) {
154+
instructions.push_str(
155+
"\n\n## Ultra mode\n\n\
156+
Use the available subagent tools to delegate concrete, independent subtasks \
157+
in parallel when that helps complete the user's request. Keep the main agent \
158+
responsible for integration and verification, and do not duplicate delegated work. \
159+
Keep simple or tightly coupled work local. Respect all user restrictions, tool \
160+
permissions, worker limits, and cancellation rules. If delegation is unavailable \
161+
or disallowed, work directly; never enable tools or bypass restrictions to delegate.",
162+
);
163+
}
149164
let service_tier = (parsed.fast
150165
&& Self::codex_supports_fast_service_tier(&parsed.base_model))
151166
.then(|| CODEX_FAST_SERVICE_TIER.to_string());
152167

153168
ResponsesRequest {
154169
model: parsed.base_model,
155170
input,
156-
instructions: Self::required_instructions(instructions),
171+
instructions,
157172
tools: converted_tools,
158173
tool_choice,
159174
reasoning,
@@ -331,5 +346,43 @@ mod tests {
331346
assert_eq!(req.model, "gpt-5.6-sol");
332347
assert_eq!(req.reasoning.as_ref().unwrap()["effort"], "max");
333348
assert_eq!(req.service_tier.as_deref(), Some("priority"));
349+
assert!(req.instructions.contains("## Ultra mode"));
350+
assert!(req.instructions.contains("Respect all user restrictions"));
351+
}
352+
353+
#[test]
354+
fn build_responses_request_preserves_effort_and_ultra_mode_on_the_wire() {
355+
let messages = [json!({"role": "system", "content": "Keep workspace edits scoped."})];
356+
for base in ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] {
357+
let efforts: &[&str] = if base == "gpt-5.6-luna" {
358+
&["low", "medium", "high", "xhigh", "max"]
359+
} else {
360+
&["low", "medium", "high", "xhigh", "max", "ultra"]
361+
};
362+
for effort in efforts {
363+
for (fast, stream) in [(false, false), (true, true)] {
364+
let suffix = if fast { "-fast" } else { "" };
365+
let model = format!("openai/{base}-{effort}{suffix}");
366+
let req =
367+
CodexNativeClient::build_responses_request(&messages, None, &model, stream);
368+
let body = serde_json::to_value(req).unwrap();
369+
assert_eq!(body["model"], base);
370+
assert_eq!(
371+
body["reasoning"]["effort"],
372+
if *effort == "ultra" { "max" } else { effort }
373+
);
374+
assert_eq!(body["stream"], stream);
375+
assert_eq!(body.get("service_tier").is_some(), fast);
376+
let instructions = body["instructions"].as_str().unwrap();
377+
assert!(instructions.starts_with("Keep workspace edits scoped."));
378+
assert_eq!(instructions.contains("## Ultra mode"), *effort == "ultra");
379+
assert!(body.get("max_output_tokens").is_none());
380+
assert!(body.get("temperature").is_none());
381+
assert!(body.get("tools").is_none(), "Ultra must not enable tools");
382+
}
383+
}
384+
}
385+
// The mode belongs to the current request, not a mutated/cached prompt.
386+
assert_eq!(messages[0]["content"], "Keep workspace edits scoped.");
334387
}
335388
}

src-tauri/crates/agent-core/src/core/providers/openai_compat/streaming/chat.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,71 @@ mod tests {
269269
use wiremock::matchers::{method, path};
270270
use wiremock::{Mock, MockServer, ResponseTemplate};
271271

272+
#[tokio::test]
273+
async fn gpt_5_6_upper_efforts_reach_both_chat_transports() {
274+
crate::test_support::install_crypto_provider_for_tests();
275+
let server = MockServer::start().await;
276+
Mock::given(method("POST"))
277+
.and(path("/chat/completions"))
278+
.respond_with(|request: &wiremock::Request| {
279+
let body: Value = request.body_json().unwrap();
280+
if body["stream"] == true {
281+
ResponseTemplate::new(200)
282+
.insert_header("content-type", "text/event-stream")
283+
.set_body_string(concat!(
284+
"data: {\"choices\":[{\"delta\":{\"content\":\"ok\"},\"finish_reason\":\"stop\"}]}\n\n",
285+
"data: [DONE]\n\n"
286+
))
287+
} else {
288+
ResponseTemplate::new(200).set_body_json(serde_json::json!({
289+
"choices": [{"message": {"content": "ok"}, "finish_reason": "stop"}]
290+
}))
291+
}
292+
})
293+
.expect(12)
294+
.mount(&server)
295+
.await;
296+
297+
let client = OpenAICompatClient::new(
298+
ProviderConfig {
299+
api_key: "test-key".to_string(),
300+
api_base: Some(server.uri()),
301+
extra_headers: HashMap::new(),
302+
is_azure: false,
303+
},
304+
find_by_name(provider_id::OPENAI).unwrap(),
305+
"gpt-5.6-sol".to_string(),
306+
);
307+
let messages = [serde_json::json!({"role": "user", "content": "hello"})];
308+
for base in ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] {
309+
for effort in ["xhigh", "max"] {
310+
for stream in [false, true] {
311+
let model = format!("{base}-{effort}");
312+
if stream {
313+
client
314+
.chat_streaming(&messages, None, &model, 1024, 0.0, &|_| {}, None)
315+
.await
316+
.unwrap();
317+
} else {
318+
client
319+
.chat(&messages, None, &model, 1024, 0.0)
320+
.await
321+
.unwrap();
322+
}
323+
let requests = server.received_requests().await.unwrap();
324+
let body: Value = requests.last().unwrap().body_json().unwrap();
325+
assert_eq!(body["model"], base);
326+
assert_eq!(body["reasoning_effort"], effort);
327+
assert_eq!(
328+
body.get("stream").and_then(Value::as_bool).unwrap_or(false),
329+
stream
330+
);
331+
assert!(body.get("thinking").is_none());
332+
}
333+
}
334+
}
335+
}
336+
272337
#[tokio::test]
273338
async fn non_streaming_standard_usage_normalizes_cached_tokens() {
274339
crate::test_support::install_crypto_provider_for_tests();

src-tauri/crates/agent-core/src/core/providers/openai_responses/client.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,34 @@ mod tests {
153153
assert!(req.reasoning.is_none());
154154
}
155155

156+
#[test]
157+
fn build_responses_request_preserves_upper_efforts_on_the_wire() {
158+
for (base, efforts) in [
159+
("gpt-5.4", &["xhigh"][..]),
160+
("gpt-5.5", &["xhigh"][..]),
161+
("gpt-5.6-sol", &["xhigh", "max"][..]),
162+
("gpt-5.6-terra", &["xhigh", "max"][..]),
163+
("gpt-5.6-luna", &["xhigh", "max"][..]),
164+
] {
165+
for effort in efforts {
166+
for stream in [false, true] {
167+
let req = OpenAIResponsesClient::build_responses_request(
168+
&[],
169+
None,
170+
&format!("{base}-{effort}"),
171+
4096,
172+
0.0,
173+
stream,
174+
);
175+
let body = serde_json::to_value(req).unwrap();
176+
assert_eq!(body["model"], base);
177+
assert_eq!(body["reasoning"]["effort"], *effort);
178+
assert_eq!(body["stream"], stream);
179+
}
180+
}
181+
}
182+
}
183+
156184
#[test]
157185
fn build_responses_request_non_reasoning_omits_reasoning() {
158186
let req =

0 commit comments

Comments
 (0)