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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,29 @@ versioning when releases are cut.
and keep scheduled public runs inert so public publishing stays downstream of
the internal source-of-truth release.

## [0.10.88] - 2026-09-13

### Added

- Managed DeepSeek V4.1 Flash, Qwen 3.8 Max, and Gemini 3.8 Flash model choices.
Availability requires an enabled gateway route, provider enrollment, and model credits.
GLM-5.3 remains the default; managed requests support text and tools.

### Improved

- Show the upstream provider and “Model credits” in the model picker so managed
routes are distinct from routes that use your own provider keys. Selection
still uses the exact managed route; the picker does not claim live availability.

### Fixed

- Include Gemini thinking tokens in gateway output charges and require final
native usage before settling a streamed request.
- Preserve each managed model's provider and credential through helper calls and continuation.
- Stop retrying exhausted provider quotas reported as HTTP 429. Insufficient
managed credits also produce a terminal quota result; temporary rate limits
retain their existing retry behavior.

## [0.10.86] - 2026-09-11

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions docs/MODELS.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,8 @@ generator, so you can use them out of the box:
`https://api.groq.com/openai/v1/responses`.
- **OpenAI Codex (Codex app-server + ChatGPT sign-in):** `gpt-5.1`,
`gpt-5.2`, `gpt-5.2-codex`, `gpt-5.3-codex`,
`gpt-5.3-codex-spark`, `gpt-5.4`, `gpt-5.4-mini`, and `gpt-5.5`
(the live-catalog default for OpenAI) under the `openai-codex`
`gpt-5.3-codex-spark`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`,
and `gpt-5.6` (the live-catalog default for OpenAI) under the `openai-codex`
provider. These use `api: "openai-codex-app-server"` and require
`maestro codex login` to Sign in with ChatGPT through Codex app-server.
Published Maestro installs use the packaged `@openai/codex` app-server first
Expand Down
12 changes: 6 additions & 6 deletions docs/protocols/codex-subagent-dispatch-table-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@
"opus": {
"anthropic": "claude-opus-4-6",
"openai": "gpt-5.2",
"openai-codex": "gpt-5.5",
"openai-codex": "gpt-5.6",
"google": "gemini-2.0-flash-thinking-exp"
},
"sonnet": {
"anthropic": "claude-sonnet-4-5-20250929",
"openai": "gpt-4o",
"openai-codex": "gpt-5.4",
"openai-codex": "gpt-5.6",
"google": "gemini-2.0-flash-exp"
},
"haiku": {
"anthropic": "claude-haiku-4-5-20251001",
"openai": "gpt-4o-mini",
"openai-codex": "gpt-5.4-mini",
"openai-codex": "gpt-5.6",
"google": "gemini-2.0-flash-lite-exp"
}
},
Expand Down Expand Up @@ -139,7 +139,7 @@
"explorer": { "model": "haiku", "reasoningEffort": "low" },
"planner": { "model": "opus", "reasoningEffort": "high" },
"coder": {
"model": "openai-codex/gpt-5.5",
"model": "openai-codex/gpt-5.6",
"reasoningEffort": "medium"
},
"reviewer": { "model": "sonnet", "reasoningEffort": "medium" },
Expand Down Expand Up @@ -190,11 +190,11 @@
"explorer": { "model": "sonnet", "reasoningEffort": "medium" },
"planner": { "model": "opus", "reasoningEffort": "xhigh" },
"coder": {
"model": "openai-codex/gpt-5.5",
"model": "openai-codex/gpt-5.6",
"reasoningEffort": "high"
},
"reviewer": {
"model": "openai-codex/gpt-5.5",
"model": "openai-codex/gpt-5.6",
"reasoningEffort": "medium"
},
"researcher": { "model": "sonnet", "reasoningEffort": "medium" },
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@evalops/deixic-code",
"description": "Deixic Code — native Rust coding agent, CLI, TUI, and web runtime gateway",
"version": "0.10.86",
"version": "0.10.88",
"private": false,
"type": "module",
"bin": {
Expand Down
22 changes: 13 additions & 9 deletions packages/ai-rs/src/model_capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,15 +205,16 @@ pub fn openai_request_capabilities(
let name = stripped.rsplit('/').next().unwrap_or(stripped);
if name.starts_with("gpt-5.6-") {
"max"
} else if ["gpt-5.2", "gpt-5.3", "gpt-5.4", "gpt-5.5"]
.iter()
.any(|prefix| {
name == *prefix
|| name
.strip_prefix(prefix)
.is_some_and(|rest| rest.starts_with("-"))
})
{
} else if [
"gpt-5.2", "gpt-5.3", "gpt-5.4", "gpt-5.5", "gpt-5.6", "gpt-5.7",
]
.iter()
.any(|prefix| {
name == *prefix
|| name
.strip_prefix(prefix)
.is_some_and(|rest| rest.starts_with("-"))
}) {
"xhigh"
} else {
"high"
Expand Down Expand Up @@ -323,7 +324,10 @@ mod tests {
#[test]
fn maximum_effort_is_provider_and_model_specific() {
for (model, expected) in [
("gpt-5.4", "xhigh"),
("gpt-5.5", "xhigh"),
("gpt-5.6", "xhigh"),
("gpt-5.7", "xhigh"),
("gpt-5.6-luna", "max"),
("unknown", "high"),
] {
Expand Down
106 changes: 97 additions & 9 deletions packages/ai-rs/src/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,16 @@ fn responses_item_id(id: &str) -> Option<&str> {
id.split_once('|').map(|(_, item_id)| item_id)
}

// These are the Google-protocol provider IDs and aliases accepted by the
// model-provider-core catalog. This classifies opaque replay metadata only;
// signed route validation and provider selection remain owned by the gateway.
fn supports_gemini_tool_history(provider: Option<&str>) -> bool {
matches!(
provider,
Some("google" | "googleai" | "google-ai" | "gemini" | "vertex-ai" | "vertex_ai")
)
Comment thread
haasonsaas marked this conversation as resolved.
}

/// Extract function call from a `ResponseItem`
///
/// # Pattern: Option Chaining
Expand Down Expand Up @@ -1375,6 +1385,37 @@ impl OpenAiClient {
.context("managed gateway request body must be an object")?;
if let Some((authorization, _)) = authorization {
project_managed_inference_route(object, &authorization)?;
// Provider history follows the signed route, not the legacy
// constructor's provider. Keep native Gemini context only when
// the admitted route can replay it; it is never tool authority.
let gemini_route = object
.get("provider_candidates")
.and_then(serde_json::Value::as_array)
.is_some_and(|candidates| {
candidates.iter().any(|candidate| {
Comment thread
haasonsaas marked this conversation as resolved.
supports_gemini_tool_history(
candidate
.pointer("/provider_ref/provider")
.and_then(serde_json::Value::as_str),
)
})
});
if !gemini_route {
if let Some(input) = object
.get_mut("input")
.and_then(serde_json::Value::as_array_mut)
{
for item in input {
if item.get("type").and_then(serde_json::Value::as_str)
== Some("function_call")
{
if let Some(item) = item.as_object_mut() {
item.remove("extra_content");
}
}
}
}
}
object.insert("managed_inference_authorization".to_string(), authorization);
}
if let Some(context) = managed_context {
Expand Down Expand Up @@ -1891,13 +1932,13 @@ impl OpenAiClient {
function_call["id"] = serde_json::json!(item_id);
}
if self.managed_gateway
&& matches!(
self.request_extensions
.get("provider_ref")
.and_then(|provider| provider.get("provider"))
.and_then(serde_json::Value::as_str),
Some("gemini" | "vertex_ai")
)
&& (self.managed_inference_authorization.is_some()
|| supports_gemini_tool_history(
self.request_extensions
.get("provider_ref")
.and_then(|provider| provider.get("provider"))
.and_then(serde_json::Value::as_str),
))
{
if let Some(context) = gemini_context {
function_call["extra_content"] =
Expand Down Expand Up @@ -4764,7 +4805,7 @@ data: {"type":"response.completed","response":{"output":[{"type":"message","cont
"openrouter/anthropic/claude-sonnet-4.5",
"openrouter/google/gemini-2.5-pro",
"openrouter/meta-llama/llama-4-maverick",
"openrouter/openai/gpt-5.4",
"openrouter/openai/gpt-5.5",
"openrouter/openai/o3-mini",
"openrouter/openrouter/auto",
"evalops/openrouter/gpt-5.6",
Expand Down Expand Up @@ -5418,7 +5459,15 @@ data: {"type":"response.completed","response":{"output":[{"type":"message","cont
model: "gemini-3.6-flash".into(),
..Default::default()
};
for provider in ["gemini", "vertex_ai", "openai"] {
for provider in [
"vertex-ai",
"vertex_ai",
"gemini",
"google",
"google-ai",
"googleai",
"openai",
] {
let client = OpenAiClient::new("test-key")
.unwrap()
.with_managed_gateway_context(
Expand All @@ -5436,6 +5485,45 @@ data: {"type":"response.completed","response":{"output":[{"type":"message","cont
assert_eq!(body["input"][0]["call_id"], body["input"][1]["call_id"]);
assert_eq!(body["input"][1]["output"], "wrote 6 bytes\n");
}
// Hosted turns select the provider from signed candidates. The
// legacy constructor route can name a different provider.
for provider in [
"vertex-ai",
"vertex_ai",
"gemini",
"google",
"google-ai",
"googleai",
"openai",
] {
let mut client = OpenAiClient::new("test-key")
.unwrap()
.with_managed_gateway_context(
"org_123",
"workspace_456",
serde_json::json!({"provider": if provider == "openai" { "vertex_ai" } else { "openrouter" }}),
)
.unwrap();
let mut authorization: serde_json::Value =
serde_json::from_str(&managed_authorization_fixture("gemini-history")).unwrap();
authorization["claims"]["providerCandidates"][0]["provider"] =
serde_json::json!(provider);
client.set_managed_inference_authorization(Some(authorization.to_string()));
let body = client
.managed_request(client.build_responses_request_body(&restored, &config))
.unwrap();
assert!(body.get("provider_ref").is_none());
assert_eq!(
body["provider_candidates"][0]["provider_ref"]["provider"],
provider
);
if provider == "openai" {
assert!(body["input"][0].get("extra_content").is_none());
} else {
assert_eq!(body["input"][0]["extra_content"], item["extra_content"]);
}
assert_eq!(body["input"][0]["call_id"], body["input"][1]["call_id"]);
}
let other = OpenAiClient::new("test-key")
.unwrap()
.build_responses_request_body(&restored, &config);
Expand Down
8 changes: 4 additions & 4 deletions packages/ambient-agent-rs/src/pr_creator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -742,11 +742,11 @@ mod tests {
#[test]
fn builds_explicit_follow_up_approval_trailer_when_missing() {
let metadata =
PrCreator::build_authorship_metadata(&event_with_extra(HashMap::new()), "gpt-5.4")
PrCreator::build_authorship_metadata(&event_with_extra(HashMap::new()), "gpt-5.6")
.unwrap();

assert_eq!(metadata.prompt_id, "prompt_123");
assert_eq!(metadata.model_identifier, "gpt-5.4");
assert_eq!(metadata.model_identifier, "gpt-5.6");
assert_eq!(
metadata.approvals_id,
"missing:approval-follow-up-required:prompt_123"
Expand All @@ -757,7 +757,7 @@ mod tests {
fn fallback_approval_trailer_uses_trimmed_prompt_id() {
let mut event = event_with_extra(HashMap::new());
event.id = " prompt_123 ".to_string();
let metadata = PrCreator::build_authorship_metadata(&event, "gpt-5.4").unwrap();
let metadata = PrCreator::build_authorship_metadata(&event, "gpt-5.6").unwrap();

assert_eq!(metadata.prompt_id, "prompt_123");
assert_eq!(
Expand All @@ -775,7 +775,7 @@ mod tests {
);

let metadata =
PrCreator::build_authorship_metadata(&event_with_extra(extra), "gpt-5.4").unwrap();
PrCreator::build_authorship_metadata(&event_with_extra(extra), "gpt-5.6").unwrap();

assert_eq!(metadata.approvals_id, "approval_789");
}
Expand Down
26 changes: 18 additions & 8 deletions packages/local-host-rs/src/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,10 +522,12 @@ fn resolve_native_client(
}

let (credential_mode, identity) = crate::credential_mode::require_ready_with_identity(model)?;
let identity_scope = crate::telemetry::TelemetryIdentityScope::new(
&identity.organization_id,
identity.workspace_id.as_deref(),
);
let identity_scope = identity.as_ref().and_then(|identity| {
crate::telemetry::TelemetryIdentityScope::new(
&identity.organization_id,
identity.workspace_id.as_deref(),
)
});
if let crate::codex_auth::CodexModelRoute::AppServer { model_id } = route {
return Ok((
NativeResolvedClient {
Expand Down Expand Up @@ -738,14 +740,15 @@ fn relay_runtime_events(
}

/// Bind a model transition before publishing its identity to turn telemetry.
/// Automatic routing cannot replace the actor's active tenant or proceed
/// without a complete verified tenant; explicit model changes refresh it.
/// Automatic routing cannot replace the actor's active tenant. Direct-provider
/// BYOK may have no tenant; staying unbound is not a tenant change. Explicit
/// model changes refresh the scope.
fn update_model_identity_scope(
active: &mut Option<crate::telemetry::TelemetryIdentityScope>,
resolved: Option<crate::telemetry::TelemetryIdentityScope>,
preserve_scope: bool,
) -> Result<(), String> {
if preserve_scope && (resolved.is_none() || resolved != *active) {
if preserve_scope && resolved != *active {
return Err(
"Automatic routing cannot change the active organization or workspace".to_owned(),
);
Expand Down Expand Up @@ -813,8 +816,15 @@ mod identity_transition_tests {
assert_eq!(active, initial);
}
let mut unbound = None;
assert!(update_model_identity_scope(&mut unbound, None, true).is_err());
update_model_identity_scope(&mut unbound, None, true).unwrap();
assert!(unbound.is_none());
let mut still_unbound = None;
assert_eq!(
update_model_identity_scope(&mut still_unbound, scope("org-a", "workspace-a"), true)
.unwrap_err(),
"Automatic routing cannot change the active organization or workspace"
);
assert!(still_unbound.is_none());
}

#[test]
Expand Down
Loading