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
1 change: 1 addition & 0 deletions Cargo.lock

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

29 changes: 28 additions & 1 deletion crates/xgeny-cli/src/composition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ fn compatibility_probe_config(
.and_then(|config| config.with_timeout(limits.timeout()))
.and_then(|config| config.with_response_format(options.response_format))
.and_then(|config| config.with_thinking(options.thinking))
.and_then(artifact_schema_from_environment)
.map_err(map_model_check_config)
}

Expand Down Expand Up @@ -504,6 +505,9 @@ const fn model_rejection_code(reason: ModelCallRejectionReason) -> &'static str
"model_rejected.planner_invalid_response"
}
ModelCallRejectionReason::ProviderLimit => "model_rejected.provider_limit",
ModelCallRejectionReason::RequestTooLarge => "model_rejected.request_too_large",
ModelCallRejectionReason::RateLimited => "model_rejected.rate_limited",
ModelCallRejectionReason::OutputTruncated => "model_rejected.output_truncated",
ModelCallRejectionReason::ProviderRejected => "model_rejected.provider_rejected",
ModelCallRejectionReason::ProposalRejected => "model_rejected.proposal_rejected",
ModelCallRejectionReason::MaterializationFailed => "model_rejected.materialization_failed",
Expand Down Expand Up @@ -1734,6 +1738,7 @@ fn planner_config(
.and_then(|config| config.with_timeout(limits.timeout()))
.and_then(|config| config.with_response_format(options.response_format))
.and_then(|config| config.with_thinking(options.thinking))
.and_then(artifact_schema_from_environment)
.map_err(map_provider_config)?;
if planning_constraints_required {
config
Expand All @@ -1744,6 +1749,20 @@ fn planner_config(
}
}

// Per-invocation host contract, not a model-profile default. Resume checks the
// resulting schema-bound request digest before sending anything.
fn artifact_schema_from_environment(
config: OpenAiPlannerConfig,
) -> Result<OpenAiPlannerConfig, OpenAiPlannerConfigError> {
match std::env::var("XGENY_OPENAI_ARTIFACT_SCHEMA") {
Ok(encoded) => config.with_artifact_schema(&encoded),
Err(std::env::VarError::NotPresent) => Ok(config),
Err(std::env::VarError::NotUnicode(_)) => Err(
OpenAiPlannerConfigError::InvalidProfileField("artifact_schema"),
),
}
}

fn remote_planner(
config: OpenAiPlannerConfig,
credential: Option<BearerCredential>,
Expand Down Expand Up @@ -1855,6 +1874,9 @@ fn map_planner_unavailable(run_id: String, failure: PlannerPortFailure) -> Local
}
PlannerPortFailure::InvalidResponse => ModelCallRejectionReason::PlannerInvalidResponse,
PlannerPortFailure::ProviderLimit => ModelCallRejectionReason::ProviderLimit,
PlannerPortFailure::RequestTooLarge => ModelCallRejectionReason::RequestTooLarge,
PlannerPortFailure::RateLimited => ModelCallRejectionReason::RateLimited,
PlannerPortFailure::OutputTruncated => ModelCallRejectionReason::OutputTruncated,
PlannerPortFailure::ProviderRejected => ModelCallRejectionReason::ProviderRejected,
};
LocalCommandResult::Rejected {
Expand Down Expand Up @@ -2812,11 +2834,16 @@ mod tests {
#[test]
fn explicit_wire_options_have_identical_probe_and_planner_digests() {
use xgeny_provider_openai::{ResponseFormat, ThinkingMode};
for response_format in [ResponseFormat::JsonSchema, ResponseFormat::JsonObject] {
for response_format in [
ResponseFormat::JsonSchema,
ResponseFormat::JsonObject,
ResponseFormat::JsonSchemaAtomicJson,
] {
for thinking in [
ThinkingMode::Default,
ThinkingMode::Disabled,
ThinkingMode::Enabled,
ThinkingMode::ChatTemplateDisabled,
] {
let options = RequestOptions {
response_format,
Expand Down
13 changes: 10 additions & 3 deletions crates/xgeny-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ struct RunArgs {
#[command(flatten)]
request_options: RequestOptionArgs,
/// Goal sent to the bounded planner.
#[arg(help = format!("Goal sent to the bounded planner. XGENY_MAX_GOAL_BYTES={}", xgeny_cli::MAX_GOAL_BYTES))]
#[arg(help = format!("Goal sent to the bounded planner. XGENY_MAX_GOAL_BYTES={} XGENY_OPENAI_ARTIFACT_SCHEMA=atomic-json-schema-v1 (optional per-invocation JSON Schema; also required unchanged on resume)", xgeny_cli::MAX_GOAL_BYTES))]
goal: String,
/// Workspace root opened as the local filesystem capability.
#[arg(long, default_value = ".")]
Expand Down Expand Up @@ -287,10 +287,10 @@ struct ResumeArgs {
#[derive(Debug, Args, Default)]
struct RequestOptionArgs {
/// Structured output transport; `json_object` is validated by `XGENy`, not server-enforced schema.
#[arg(long, value_parser = ["json_schema", "json_object"])]
#[arg(long, value_parser = ["json_schema", "json_object", "json_schema_atomic_json"])]
response_format: Option<String>,
/// Explicit provider thinking setting; default omits the provider-specific setting.
#[arg(long, value_parser = ["default", "disabled", "enabled"])]
#[arg(long, value_parser = ["default", "disabled", "enabled", "chat_template_disabled"])]
thinking: Option<String>,
}

Expand Down Expand Up @@ -1206,6 +1206,7 @@ fn resolve_request_options(
None => base.response_format,
Some("json_schema") => ResponseFormat::JsonSchema,
Some("json_object") => ResponseFormat::JsonObject,
Some("json_schema_atomic_json") => ResponseFormat::JsonSchemaAtomicJson,
Some(_) => return Err(ModelCliError::InvalidRequestOptions),
};
let thinking = match explicit
Expand All @@ -1217,6 +1218,7 @@ fn resolve_request_options(
Some("default") => ThinkingMode::Default,
Some("disabled") => ThinkingMode::Disabled,
Some("enabled") => ThinkingMode::Enabled,
Some("chat_template_disabled") => ThinkingMode::ChatTemplateDisabled,
Some(_) => return Err(ModelCliError::InvalidRequestOptions),
};
Ok(RequestOptions {
Expand All @@ -1229,6 +1231,7 @@ const fn response_format_label(format: ResponseFormat) -> &'static str {
match format {
ResponseFormat::JsonSchema => "json_schema",
ResponseFormat::JsonObject => "json_object",
ResponseFormat::JsonSchemaAtomicJson => "json_schema_atomic_json",
}
}

Expand All @@ -1237,12 +1240,16 @@ const fn thinking_label(thinking: ThinkingMode) -> &'static str {
ThinkingMode::Default => "default",
ThinkingMode::Disabled => "disabled",
ThinkingMode::Enabled => "enabled",
ThinkingMode::ChatTemplateDisabled => "chat_template_disabled",
}
}

const fn compatibility_label(options: RequestOptions) -> &'static str {
match options.response_format {
ResponseFormat::JsonSchema => "strict JSON compatible",
ResponseFormat::JsonSchemaAtomicJson => {
"atomic JSON wire compatible (native write still verified)"
}
ResponseFormat::JsonObject => {
"JSON object compatible (host-validated; no server schema guarantee)"
}
Expand Down
7 changes: 6 additions & 1 deletion crates/xgeny-cli/src/model_profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1159,11 +1159,16 @@ mod tests {

#[test]
fn request_options_round_trip_and_reject_unknown_modes() {
for response_format in [ResponseFormat::JsonSchema, ResponseFormat::JsonObject] {
for response_format in [
ResponseFormat::JsonSchema,
ResponseFormat::JsonObject,
ResponseFormat::JsonSchemaAtomicJson,
] {
for thinking in [
ThinkingMode::Default,
ThinkingMode::Disabled,
ThinkingMode::Enabled,
ThinkingMode::ChatTemplateDisabled,
] {
let mut original = profile("wire");
let options = RequestOptions {
Expand Down
9 changes: 9 additions & 0 deletions crates/xgeny-cli/tests/live_go50902_public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,15 @@ fn require_workspace_completion(output: &Output, state_root: &Path) {
ModelCallRejectionReason::ProviderLimit => {
"live workspace provider response exceeded a limit"
}
ModelCallRejectionReason::RequestTooLarge => {
"live workspace request too large"
}
ModelCallRejectionReason::RateLimited => {
"live workspace provider rate limited request"
}
ModelCallRejectionReason::OutputTruncated => {
"live workspace output truncated"
}
ModelCallRejectionReason::ProviderRejected => {
"live workspace provider rejected the request"
}
Expand Down
1 change: 1 addition & 0 deletions crates/xgeny-provider-openai/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ repository.workspace = true
publish = false

[dependencies]
jsonschema.workspace = true
serde.workspace = true
serde_jcs.workspace = true
serde_json.workspace = true
Expand Down
Loading