Skip to content
Merged
20 changes: 20 additions & 0 deletions crates/aisix-admin/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3231,6 +3231,13 @@ fn add_variant_titles(doc: &mut Value) {
"/components/schemas/KeywordPattern/oneOf",
&["Literal", "Regex"],
),
(
// Model's top-level direct/routing/ensemble mutual-exclusion
// `oneOf` (injected by `aisix_core::models::schema::model_root_schema`).
// Order must match `aisix_core::models::model::model_one_of`.
"/components/schemas/Model/oneOf",
&["Routing model", "Direct model", "Ensemble model"],
),
(
"/components/schemas/ObjectStoreAuthMode/oneOf",
&["Credential reference", "Cloud identity"],
Expand Down Expand Up @@ -4023,6 +4030,13 @@ mod tests {
}

for (key, child) in map {
// `if`/`then`/`else` are cross-field constraint subschemas
// (e.g. object_store's cloud-identity rule), not ReDoc-
// rendered property definitions, so their inner properties
// need no descriptions.
if matches!(key.as_str(), "if" | "then" | "else") {
continue;
}
collect_missing_property_descriptions(child, format!("{path}/{key}"), missing);
}
}
Expand Down Expand Up @@ -4136,6 +4150,12 @@ mod tests {
}

for (key, child) in map {
// `not` subschemas are negative constraints (e.g. Model's
// direct/routing/ensemble mutual exclusion), never rendered
// as ReDoc tabs, so their inner variants need no titles.
if key == "not" {
continue;
}
collect_untitled_schema_variants(child, format!("{path}/{key}"), missing);
}
}
Expand Down
54 changes: 42 additions & 12 deletions crates/aisix-core/src/bin/dump-schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,59 @@ use std::path::{Path, PathBuf};

use schemars::JsonSchema;

use aisix_core::models::{
ApiKey, CachePolicy, EnsembleConfig, Guardrail, Model, ObservabilityExporter, ProviderKey,
RateLimit, RateLimitPolicy, Routing,
};
use aisix_core::models::schema;
use aisix_core::models::{EnsembleConfig, RateLimit, Routing};

fn main() {
let out_dir = workspace_root().join("schemas").join("resources");
fs::create_dir_all(&out_dir).expect("create schemas/resources dir");

dump::<ApiKey>(&out_dir, "api_key");
dump::<CachePolicy>(&out_dir, "cache_policy");
// Every resource with a runtime validator goes through the SAME
// `*_root_schema()` producer the validator uses, so the published schema ==
// the enforced schema by construction. `ensemble`/`rate_limit`/`routing`
// have no standalone validator (they are nested struct types) so they dump
// straight from the struct via `schema_for!`.
dump_value(&out_dir, "api_key", schema::apikey_root_schema());
dump_value(&out_dir, "cache_policy", schema::cache_policy_root_schema());
dump_value(&out_dir, "model", schema::model_root_schema());
dump_value(
&out_dir,
"rate_limit_policy",
schema::rate_limit_policy_root_schema(),
);
dump_value(&out_dir, "provider_key", schema::provider_key_root_schema());
dump_value(
&out_dir,
"observability_exporter",
schema::observability_exporter_root_schema(),
);
dump_value(&out_dir, "guardrail", schema::guardrail_root_schema());
dump_value(
&out_dir,
"guardrail_attachment",
schema::guardrail_attachment_root_schema(),
);

dump::<EnsembleConfig>(&out_dir, "ensemble");
dump::<Guardrail>(&out_dir, "guardrail");
dump::<Model>(&out_dir, "model");
dump::<ObservabilityExporter>(&out_dir, "observability_exporter");
dump::<ProviderKey>(&out_dir, "provider_key");
dump::<RateLimit>(&out_dir, "rate_limit");
dump::<RateLimitPolicy>(&out_dir, "rate_limit_policy");
dump::<Routing>(&out_dir, "routing");
}

fn dump<T: JsonSchema>(out_dir: &Path, name: &str) {
let schema = schemars::schema_for!(T);
// Serialize the `RootSchema` directly to preserve schemars' native key
// ordering. (Routing through `serde_json::Value` would re-sort keys.)
let mut json =
serde_json::to_string_pretty(&schemars::schema_for!(T)).expect("serialize schema");
json.push('\n');
let path = out_dir.join(format!("{name}.schema.json"));
fs::write(&path, json).unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
println!("wrote {}", path.display());
}

/// Write a pre-assembled schema `Value`. Used for resources whose canonical
/// schema is built by a dedicated producer rather than a bare `schema_for!`
/// (e.g. `model`, which injects the cross-field `oneOf`).
fn dump_value(out_dir: &Path, name: &str, schema: serde_json::Value) {
let mut json = serde_json::to_string_pretty(&schema).expect("serialize schema");
json.push('\n');
let path = out_dir.join(format!("{name}.schema.json"));
Expand Down
6 changes: 3 additions & 3 deletions crates/aisix-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ pub use models::{
validate_observability_exporter, validate_provider_key, validate_rate_limit_policy, Adapter,
AisixSnapshot, ApiKey, AppliedGuardrail, CachePolicy, CooldownConfig, ExporterKind, Guardrail,
GuardrailHookPoint, GuardrailKind, KeywordConfig, KeywordPattern, Model, ObservabilityExporter,
OnAllFilteredPolicy, ParamConstraints, ProviderKey, RateLimit, RateLimitPolicy,
RequestOverrides, ResponseOverrides, Routing, RoutingStrategy, RoutingTarget, SchemaError,
StreamDoneMarker, TelemetryTags, DEFAULT_COOLDOWN_TRIGGER_STATUSES,
OnAllFilteredPolicy, ParamConstraints, PolicyScope, PolicyWindow, ProviderKey, RateLimit,
RateLimitPolicy, RequestOverrides, ResponseOverrides, Routing, RoutingStrategy, RoutingTarget,
SchemaError, StreamDoneMarker, TelemetryKind, TelemetryTags, DEFAULT_COOLDOWN_TRIGGER_STATUSES,
};
pub use resource::{Resource, ResourceEntry};
pub use snapshot::{ResourceTable, SnapshotHandle};
3 changes: 3 additions & 0 deletions crates/aisix-core/src/models/apikey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::resource::Resource;
pub struct ApiKey {
/// SHA-256 hexadecimal hash of the plaintext bearer. The proxy hashes
/// incoming bearer tokens before lookup.
#[schemars(length(min = 1))]
pub key_hash: String,

/// Model identifiers this key may use. An empty array denies access to every model.
Expand All @@ -31,11 +32,13 @@ pub struct ApiKey {
/// Team this API key belongs to. Used for matching team-scope
/// rate limit policies.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(length(min = 1))]
pub team_id: Option<String>,

/// Org member who owns this key. Used for matching member-scope
/// rate limit policies.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(length(min = 1))]
pub user_id: Option<String>,

/// etcd-key uuid. Filled by the loader and never included in the JSON payload.
Expand Down
3 changes: 3 additions & 0 deletions crates/aisix-core/src/models/cache_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub enum CacheBackend {
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, PartialEq)]
pub struct CachePolicy {
/// Operator-facing name that surfaces in metric labels and cache headers.
#[schemars(length(min = 1, max = 120))]
pub name: String,

/// When false, the cache gate skips this policy. Allows operators
Expand All @@ -43,11 +44,13 @@ pub struct CachePolicy {

/// Cache entry TTL in seconds.
#[serde(default = "default_ttl_seconds")]
#[schemars(range(min = 1, max = 604800))]
pub ttl_seconds: u32,

/// Free-form scope. Supports `"all"`, `"model:<name>"`, and
/// `"api_key:<id>"`. See `parsed_applies_to`.
#[serde(default = "default_applies_to")]
#[schemars(length(min = 1, max = 255))]
pub applies_to: String,

/// Set by the loader from the kine path's UUID segment. The DP
Expand Down
6 changes: 6 additions & 0 deletions crates/aisix-core/src/models/ensemble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ use serde::{Deserialize, Serialize};
#[serde(deny_unknown_fields)]
pub struct PanelMember {
/// Model alias for a direct model that receives one panel request.
#[schemars(length(min = 1))]
pub model: String,
/// Sampling temperature for this panel member. Omit it to keep the request's temperature.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0.0))]
pub temperature: Option<f32>,
/// Sampling seed for this panel member.
#[serde(default, skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -48,9 +50,11 @@ impl PanelMember {
#[serde(deny_unknown_fields)]
pub struct Judge {
/// Model alias for the direct model that synthesizes panel responses.
#[schemars(length(min = 1))]
pub model: String,
/// Override for the built-in synthesis prompt template.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(length(min = 1))]
pub synthesis_prompt: Option<String>,
}

Expand All @@ -73,11 +77,13 @@ const DEFAULT_MIN_RESPONSES: usize = 2;
#[serde(deny_unknown_fields)]
pub struct EnsembleConfig {
/// Direct models called concurrently for each ensemble request.
#[schemars(length(min = 1))]
pub panel: Vec<PanelMember>,
/// Direct model that combines successful panel responses.
pub judge: Judge,
/// Minimum successful panel responses required before judge synthesis. When omitted, the gateway requires the smaller of 2 and the panel size.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 1))]
pub min_responses: Option<u32>,
/// Per-call upstream deadline applied to each panel member and the judge. Set `0` or omit it to disable the ensemble-level deadline.
#[serde(default, skip_serializing_if = "Option::is_none")]
Expand Down
28 changes: 26 additions & 2 deletions crates/aisix-core/src/models/guardrail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ pub enum GuardrailHookPoint {
#[serde(tag = "kind", content = "value", rename_all = "lowercase")]
pub enum KeywordPattern {
/// Literal string to match.
Literal(String),
Literal(#[schemars(length(min = 1))] String),
/// Regular expression pattern to match.
Regex(String),
Regex(#[schemars(length(min = 1))] String),
}

/// Config block for `kind: "keyword"`.
Expand All @@ -83,10 +83,12 @@ pub struct KeywordConfig {
pub enum BedrockAWSCredentials {
Static {
/// AWS access key ID for static Bedrock guardrail credentials.
#[schemars(length(min = 1))]
access_key_id: String,
/// Decrypted before projection. Plaintext is held in memory only
/// and is not logged. The data plane passes it to the
/// AWS SDK's static credentials provider.
#[schemars(length(min = 1))]
secret_access_key: String,
},
}
Expand All @@ -98,6 +100,7 @@ pub enum BedrockLatencyMode {
Serial,
Timed {
/// Maximum time in milliseconds to wait for the Bedrock guardrail response.
#[schemars(range(min = 100, max = 5000))]
timeout_ms: u32,
},
}
Expand All @@ -115,12 +118,15 @@ pub struct AzureContentSafetyConfig {
/// Azure Cognitive Services resource endpoint, e.g.
/// `https://my-resource.cognitiveservices.azure.com`.
/// The data plane appends `/contentsafety/text:shieldPrompt?api-version=2024-09-01`.
#[schemars(length(min = 1))]
pub endpoint: String,
/// Azure subscription key sent with the `Ocp-Apim-Subscription-Key` header. Decrypted before
/// projection. Plaintext is held in memory only and is not logged.
#[schemars(length(min = 1))]
pub api_key: String,
/// HTTP call timeout in milliseconds. A value of `0` triggers the timeout immediately.
#[serde(default = "default_acs_timeout_ms")]
#[schemars(range(max = 4_294_967_295u32))]
pub timeout_ms: u32,
}

Expand All @@ -142,14 +148,17 @@ fn default_acs_timeout_ms() -> u32 {
pub struct AzureContentSafetyTextModerationConfig {
/// Azure Cognitive Services resource endpoint. The data plane appends
/// `/contentsafety/text:analyze?api-version=2024-09-01`.
#[schemars(length(min = 1))]
pub endpoint: String,
/// Azure subscription key sent with the `Ocp-Apim-Subscription-Key` header. Plaintext is held in
/// memory only and is not logged.
#[schemars(length(min = 1))]
pub api_key: String,
/// HTTP call timeout in milliseconds. `fail_open` and `output_fail_open`
/// govern the verdict when it elapses. A value of `0` triggers the timeout
/// immediately.
#[serde(default = "default_acs_timeout_ms")]
#[schemars(range(max = 4_294_967_295u32))]
pub timeout_ms: u32,

// --- moderation parameters ---
Expand All @@ -161,6 +170,7 @@ pub struct AzureContentSafetyTextModerationConfig {
pub categories: Vec<String>,
/// General severity threshold. A category at or above it blocks.
#[serde(default = "default_acs_severity_threshold")]
#[schemars(range(max = 7))]
pub severity_threshold: u8,
/// Per-category threshold overrides. These take precedence over the general threshold.
#[serde(default)]
Expand All @@ -182,12 +192,14 @@ pub struct AzureContentSafetyTextModerationConfig {
pub stream_processing_mode: String,
/// Sliding-window size in characters for window mode.
#[serde(default = "default_acs_window_size")]
#[schemars(range(min = 1, max = 10_000))]
pub window_size: u32,
/// Chars carried between windows so a span split across a boundary is still caught.
#[serde(default = "default_acs_window_overlap_size")]
pub window_overlap_size: u32,
/// Max bytes buffered in `buffer_full` mode before `on_buffer_exceeded` applies.
#[serde(default = "default_acs_max_buffer_bytes")]
#[schemars(range(min = 1))]
pub max_buffer_bytes: u64,
/// Buffer-overflow policy. Use `fail_open` to allow output when the buffer cap is hit.
#[serde(default = "default_acs_on_buffer_exceeded")]
Expand Down Expand Up @@ -257,14 +269,18 @@ fn default_acs_on_buffer_exceeded() -> String {
pub struct AliyunTextModerationConfig {
/// Aliyun region the guardrail lives in, e.g. `cn-shanghai`. The data plane
/// builds the endpoint `https://green-cip.<region>.aliyuncs.com`.
#[schemars(length(min = 1))]
pub region: String,
/// Explicit endpoint override as a full URL with no trailing slash. When set, it takes precedence over `region`.
#[serde(default)]
#[schemars(length(min = 1))]
pub endpoint: Option<String>,
/// Aliyun AccessKey ID.
#[schemars(length(min = 1))]
pub access_key_id: String,
/// Aliyun AccessKey secret. Decrypted before projection. Plaintext is held
/// in memory only and is not logged. Used to sign the request.
#[schemars(length(min = 1))]
pub access_key_secret: String,
/// Minimum risk level that triggers a block: `low`, `medium`, or `high`. A returned level at or above this blocks.
#[serde(default = "default_aliyun_risk_level_threshold")]
Expand All @@ -273,6 +289,7 @@ pub struct AliyunTextModerationConfig {
/// govern the verdict when it elapses. A value of `0` triggers the timeout
/// immediately.
#[serde(default = "default_acs_timeout_ms")]
#[schemars(range(max = 4_294_967_295u32))]
pub timeout_ms: u32,
/// Fail-open policy for the output hook. When disabled, an Aliyun outage does not release unscanned model output.
#[serde(default)]
Expand All @@ -285,12 +302,14 @@ pub struct AliyunTextModerationConfig {
pub stream_processing_mode: String,
/// Sliding-window size in characters when window mode is used. Aliyun limits each `llm_response_moderation` call to 2,000 characters.
#[serde(default = "default_aliyun_window_size")]
#[schemars(range(min = 1, max = 2_000))]
pub window_size: u32,
/// Chars carried between windows so a span split across a boundary is still caught.
#[serde(default = "default_aliyun_window_overlap_size")]
pub window_overlap_size: u32,
/// Max bytes buffered in `buffer_full` mode before `on_buffer_exceeded` applies.
#[serde(default = "default_acs_max_buffer_bytes")]
#[schemars(range(min = 1))]
pub max_buffer_bytes: u64,
/// Buffer-overflow policy. Use `fail_open` to allow output when the buffer cap is hit.
#[serde(default = "default_acs_on_buffer_exceeded")]
Expand All @@ -316,10 +335,13 @@ fn default_aliyun_window_overlap_size() -> u32 {
#[serde(deny_unknown_fields)]
pub struct BedrockConfig {
/// Guardrail identifier issued by the AWS console.
#[schemars(length(min = 1, max = 64))]
pub guardrail_id: String,
/// Version label: `DRAFT`, `1`, `2`, ...
#[schemars(length(min = 1, max = 16))]
pub guardrail_version: String,
/// AWS region for the Bedrock endpoint, such as `us-east-1`.
#[schemars(length(min = 1))]
pub region: String,
/// IAM credentials for Bedrock requests.
pub aws_credentials: BedrockAWSCredentials,
Expand Down Expand Up @@ -392,6 +414,7 @@ pub struct AppliedGuardrail {
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, PartialEq, Eq)]
pub struct Guardrail {
/// Operator-facing name that surfaces in metric labels and error reasons.
#[schemars(length(min = 1))]
pub name: String,

/// When false, the chain skips this rule entirely. Allows operators
Expand Down Expand Up @@ -499,6 +522,7 @@ pub enum GuardrailScopeType {
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, PartialEq, Eq)]
pub struct GuardrailAttachment {
/// UUID of the guardrail definition this attachment points to.
#[schemars(length(min = 1))]
pub guardrail_id: String,

/// What dimension of the request this attachment is scoped to.
Expand Down
4 changes: 2 additions & 2 deletions crates/aisix-core/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ pub use observability_exporter::{
};
pub use provider_key::{
ParamConstraints, ProviderKey, RequestOverrides, ResponseOverrides, StreamDoneMarker,
TelemetryTags,
TelemetryKind, TelemetryTags,
};
pub use rate_limit::RateLimit;
pub use rate_limit_policy::RateLimitPolicy;
pub use rate_limit_policy::{PolicyScope, PolicyWindow, RateLimitPolicy};
pub use routing::{OnAllFilteredPolicy, Routing, RoutingStrategy, RoutingTarget};
pub use schema::{
validate_apikey, validate_cache_policy, validate_guardrail, validate_guardrail_attachment,
Expand Down
Loading
Loading