mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 11:43:57 +08:00
config(pi): execute pinned native schema and composer oracles
Evaluate raw Pi documents with the vendored upstream TypeBox schema and replay composer/transport expectations captured by actually executing Pi ab366ebe94cacd419d986be454f12b1b9913aaca. Bind all 252 canonical fields to successful raw and own-layer composer evidence (70 provider, 92 model, 90 override), fail closed where pinned runtime context is unavailable, expose structured inspection, and enforce module/write/restore boundaries with negative fixtures.
This commit is contained in:
@@ -0,0 +1,968 @@
|
||||
//! Public, side-effect-free inspection service for Pi's native catalog.
|
||||
//!
|
||||
//! This module orchestrates independent raw, managed, composer and gateway
|
||||
//! assessments. No assessment is allowed to gate execution of a sibling layer.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use super::composer::{
|
||||
compose_explicit_custom_catalog, PiComposerReasonCode, PiComposerStatus, PiNativeComposition,
|
||||
};
|
||||
use super::document::{read_pi_models_document, PiRawProviderEntry};
|
||||
use super::gateway::{
|
||||
assess_composition, PiGatewayAssessment, PiGatewayCapability, PiGatewayReasonCode,
|
||||
};
|
||||
use super::model::{
|
||||
validate_pi_managed_provider, PiCompositionStatus, PiConfigError, PiDiagnosticLayer,
|
||||
PiDiagnosticReason, PiGatewayStatus, PiManagedAssessment, PiManagedProviderConfig,
|
||||
PiManagementStatus, PiNativeDiagnostic, PiNativeEntryKind, PiRawNativeValidity, PiReasonCode,
|
||||
};
|
||||
use super::raw_schema::{
|
||||
evaluate_provider_value, PiRawReasonCode, PiRawSchemaEvaluation, PiRawValidity,
|
||||
};
|
||||
use crate::config::get_home_dir;
|
||||
use crate::error::AppError;
|
||||
use serde_json::Value;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use url::Url;
|
||||
|
||||
/// Built-in provider IDs at Pi commit
|
||||
/// `ab366ebe94cacd419d986be454f12b1b9913aaca`.
|
||||
const PI_BUILTIN_PROVIDER_KEYS: &[&str] = &[
|
||||
"amazon-bedrock",
|
||||
"ant-ling",
|
||||
"anthropic",
|
||||
"azure-openai-responses",
|
||||
"cerebras",
|
||||
"cloudflare-ai-gateway",
|
||||
"cloudflare-workers-ai",
|
||||
"deepseek",
|
||||
"fireworks",
|
||||
"github-copilot",
|
||||
"google",
|
||||
"google-vertex",
|
||||
"groq",
|
||||
"huggingface",
|
||||
"kimi-coding",
|
||||
"minimax",
|
||||
"minimax-cn",
|
||||
"mistral",
|
||||
"moonshotai",
|
||||
"moonshotai-cn",
|
||||
"nvidia",
|
||||
"openai",
|
||||
"openai-codex",
|
||||
"opencode",
|
||||
"opencode-go",
|
||||
"openrouter",
|
||||
"qwen-token-plan",
|
||||
"qwen-token-plan-cn",
|
||||
"radius",
|
||||
"together",
|
||||
"vercel-ai-gateway",
|
||||
"xai",
|
||||
"xiaomi",
|
||||
"xiaomi-token-plan-ams",
|
||||
"xiaomi-token-plan-cn",
|
||||
"xiaomi-token-plan-sgp",
|
||||
"zai",
|
||||
"zai-coding-cn",
|
||||
];
|
||||
|
||||
const RECOGNIZED_PROVIDER_FIELDS: &[&str] = &[
|
||||
"name",
|
||||
"baseUrl",
|
||||
"apiKey",
|
||||
"api",
|
||||
"oauth",
|
||||
"headers",
|
||||
"compat",
|
||||
"authHeader",
|
||||
"models",
|
||||
"modelOverrides",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PiNativeEntryInspection {
|
||||
pub diagnostic: PiNativeDiagnostic,
|
||||
pub managed_config: Option<PiManagedProviderConfig>,
|
||||
pub composition: PiNativeComposition,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ManagedResult {
|
||||
assessment: PiManagedAssessment,
|
||||
config: Option<PiManagedProviderConfig>,
|
||||
reasons: Vec<PiDiagnosticReason>,
|
||||
}
|
||||
|
||||
/// The public read-only service entry used by commands and certification tests.
|
||||
pub(crate) struct PiNativeInspectionService;
|
||||
|
||||
impl PiNativeInspectionService {
|
||||
pub(crate) fn inspect_current(
|
||||
managed_claims: &BTreeMap<String, String>,
|
||||
) -> Result<Vec<PiNativeDiagnostic>, AppError> {
|
||||
Self::inspect_catalog(&get_pi_models_path()?, managed_claims)
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_catalog(
|
||||
path: &Path,
|
||||
managed_claims: &BTreeMap<String, String>,
|
||||
) -> Result<Vec<PiNativeDiagnostic>, AppError> {
|
||||
let document = read_pi_models_document(path)?;
|
||||
Ok(document
|
||||
.providers()
|
||||
.iter()
|
||||
.map(|(provider_key, entry)| {
|
||||
analyze_native_entry(provider_key, entry, managed_claims).diagnostic
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_entry(
|
||||
path: &Path,
|
||||
provider_key: &str,
|
||||
managed_claims: &BTreeMap<String, String>,
|
||||
) -> Result<Option<PiNativeEntryInspection>, AppError> {
|
||||
let document = read_pi_models_document(path)?;
|
||||
Ok(document
|
||||
.providers()
|
||||
.get(provider_key)
|
||||
.map(|entry| analyze_native_entry(provider_key, entry, managed_claims)))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_current_pi_native_catalog(
|
||||
managed_claims: &BTreeMap<String, String>,
|
||||
) -> Result<Vec<PiNativeDiagnostic>, AppError> {
|
||||
PiNativeInspectionService::inspect_current(managed_claims)
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_pi_native_catalog(
|
||||
path: &Path,
|
||||
managed_claims: &BTreeMap<String, String>,
|
||||
) -> Result<Vec<PiNativeDiagnostic>, AppError> {
|
||||
PiNativeInspectionService::inspect_catalog(path, managed_claims)
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_pi_native_entry(
|
||||
path: &Path,
|
||||
provider_key: &str,
|
||||
managed_claims: &BTreeMap<String, String>,
|
||||
) -> Result<Option<PiNativeEntryInspection>, AppError> {
|
||||
PiNativeInspectionService::inspect_entry(path, provider_key, managed_claims)
|
||||
}
|
||||
|
||||
fn normalize_pi_agent_dir(value: &str, home: &Path) -> Result<PathBuf, AppError> {
|
||||
if value == "~" {
|
||||
return Ok(home.to_path_buf());
|
||||
}
|
||||
if let Some(suffix) = value.strip_prefix("~/") {
|
||||
return Ok(home.join(suffix));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
if let Some(suffix) = value.strip_prefix("~\\") {
|
||||
return Ok(home.join(suffix));
|
||||
}
|
||||
if value.starts_with("file://") {
|
||||
let url = Url::parse(value).map_err(|error| {
|
||||
AppError::Config(format!("invalid Pi agent directory URL: {error}"))
|
||||
})?;
|
||||
return url.to_file_path().map_err(|_| {
|
||||
AppError::Config(format!(
|
||||
"Pi agent directory URL is not a local file path: {value}"
|
||||
))
|
||||
});
|
||||
}
|
||||
Ok(PathBuf::from(value))
|
||||
}
|
||||
|
||||
pub(crate) fn get_pi_agent_dir() -> Result<PathBuf, AppError> {
|
||||
let Some(raw) = std::env::var_os("PI_CODING_AGENT_DIR") else {
|
||||
return Ok(get_home_dir().join(".pi").join("agent"));
|
||||
};
|
||||
if raw.is_empty() {
|
||||
return Ok(get_home_dir().join(".pi").join("agent"));
|
||||
}
|
||||
normalize_pi_agent_dir(&raw.to_string_lossy(), &get_home_dir())
|
||||
}
|
||||
|
||||
pub(crate) fn get_pi_models_path() -> Result<PathBuf, AppError> {
|
||||
Ok(get_pi_agent_dir()?.join("models.json"))
|
||||
}
|
||||
|
||||
fn analyze_native_entry(
|
||||
provider_key: &str,
|
||||
entry: &PiRawProviderEntry,
|
||||
managed_claims: &BTreeMap<String, String>,
|
||||
) -> PiNativeEntryInspection {
|
||||
// Raw, composition and managed conversion are deliberately invoked from
|
||||
// the same immutable JSON value. Neither result controls whether a sibling
|
||||
// assessment is attempted.
|
||||
let raw = evaluate_provider_value(&entry.value);
|
||||
let raw_validity = map_raw_validity(raw.validity);
|
||||
let kind = classify_kind(provider_key, &entry.value, raw.validity);
|
||||
let composition = match (raw.valid_provider.as_ref(), kind) {
|
||||
(Some(provider), PiNativeEntryKind::CustomCatalog) => {
|
||||
compose_explicit_custom_catalog(provider_key, provider)
|
||||
}
|
||||
(Some(_), _) => PiNativeComposition::catalog_required("/models"),
|
||||
(None, _) => PiNativeComposition::unavailable_without_valid_raw(),
|
||||
};
|
||||
let managed = assess_managed(raw.validity, kind, &entry.value);
|
||||
let gateway = if raw.validity == PiRawValidity::Valid {
|
||||
assess_composition(&composition)
|
||||
} else {
|
||||
PiGatewayAssessment {
|
||||
capability: PiGatewayCapability::Unknown,
|
||||
reasons: Vec::new(),
|
||||
plans: Vec::new(),
|
||||
}
|
||||
};
|
||||
|
||||
let management_status = managed_claims
|
||||
.get(provider_key)
|
||||
.map(|provider_id| PiManagementStatus::Managed {
|
||||
provider_id: provider_id.clone(),
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
if raw.validity == PiRawValidity::Valid
|
||||
&& managed.assessment == PiManagedAssessment::Manageable
|
||||
{
|
||||
PiManagementStatus::Importable
|
||||
} else {
|
||||
PiManagementStatus::Unsupported
|
||||
}
|
||||
});
|
||||
|
||||
let mut reasons = map_raw_reasons(&raw);
|
||||
extend_reasons(&mut reasons, managed.reasons.clone());
|
||||
extend_reasons(&mut reasons, map_composer_reasons(&composition));
|
||||
extend_reasons(&mut reasons, map_gateway_reasons(&gateway));
|
||||
|
||||
PiNativeEntryInspection {
|
||||
diagnostic: PiNativeDiagnostic {
|
||||
provider_key: provider_key.to_string(),
|
||||
display_name: entry
|
||||
.value
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned),
|
||||
fingerprint: fingerprint(&entry.raw_source),
|
||||
kind,
|
||||
raw_validity,
|
||||
managed_assessment: managed.assessment,
|
||||
composition_status: map_composition_status(composition.status),
|
||||
management_status,
|
||||
gateway_status: map_gateway_status(gateway.capability),
|
||||
reasons,
|
||||
},
|
||||
managed_config: managed.config,
|
||||
composition,
|
||||
}
|
||||
}
|
||||
|
||||
fn assess_managed(
|
||||
raw_validity: PiRawValidity,
|
||||
kind: PiNativeEntryKind,
|
||||
value: &Value,
|
||||
) -> ManagedResult {
|
||||
if raw_validity != PiRawValidity::Valid {
|
||||
return ManagedResult {
|
||||
assessment: PiManagedAssessment::Unsupported,
|
||||
config: None,
|
||||
reasons: Vec::new(),
|
||||
};
|
||||
}
|
||||
if kind != PiNativeEntryKind::CustomCatalog {
|
||||
let mut reasons = vec![diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::CatalogRequired,
|
||||
"/models",
|
||||
)];
|
||||
if value
|
||||
.get("modelOverrides")
|
||||
.and_then(Value::as_object)
|
||||
.is_some_and(|overrides| !overrides.is_empty())
|
||||
{
|
||||
reasons.push(diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::ModelOverridesOnly,
|
||||
"/modelOverrides",
|
||||
));
|
||||
}
|
||||
return ManagedResult {
|
||||
assessment: PiManagedAssessment::Unsupported,
|
||||
config: None,
|
||||
reasons,
|
||||
};
|
||||
}
|
||||
|
||||
let config = match serde_json::from_value::<PiManagedProviderConfig>(value.clone()) {
|
||||
Ok(config) => config,
|
||||
Err(_) => {
|
||||
return ManagedResult {
|
||||
assessment: PiManagedAssessment::Unsupported,
|
||||
config: None,
|
||||
reasons: vec![diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::ManagedTypeConversionFailed,
|
||||
&managed_conversion_pointer(value),
|
||||
)],
|
||||
};
|
||||
}
|
||||
};
|
||||
let mut reasons = collect_managed_reasons(&config);
|
||||
if reasons.is_empty() {
|
||||
if let Err(error) = validate_pi_managed_provider(&config) {
|
||||
reasons.push(managed_validation_reason(error));
|
||||
}
|
||||
}
|
||||
if !reasons.is_empty() {
|
||||
return ManagedResult {
|
||||
assessment: PiManagedAssessment::Unsupported,
|
||||
config: None,
|
||||
reasons,
|
||||
};
|
||||
}
|
||||
ManagedResult {
|
||||
assessment: PiManagedAssessment::Manageable,
|
||||
config: Some(config),
|
||||
reasons,
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_managed_reasons(config: &PiManagedProviderConfig) -> Vec<PiDiagnosticReason> {
|
||||
let mut reasons = Vec::new();
|
||||
let mut ids = HashSet::with_capacity(config.models.len());
|
||||
if config.models.is_empty() {
|
||||
add_reason(
|
||||
&mut reasons,
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::MissingExplicitModels,
|
||||
"/models",
|
||||
),
|
||||
);
|
||||
}
|
||||
if config
|
||||
.base_url
|
||||
.as_deref()
|
||||
.is_some_and(|value| !valid_http_endpoint(value))
|
||||
{
|
||||
add_reason(
|
||||
&mut reasons,
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::InvalidEndpoint,
|
||||
"/baseUrl",
|
||||
),
|
||||
);
|
||||
}
|
||||
for (index, model) in config.models.iter().enumerate() {
|
||||
let pointer = format!("/models/{index}");
|
||||
if model.id.trim().is_empty() {
|
||||
add_reason(
|
||||
&mut reasons,
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::EmptyModelId,
|
||||
&format!("{pointer}/id"),
|
||||
),
|
||||
);
|
||||
} else if !ids.insert(model.id.as_str()) {
|
||||
add_reason(
|
||||
&mut reasons,
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::DuplicateModelId,
|
||||
&format!("{pointer}/id"),
|
||||
),
|
||||
);
|
||||
}
|
||||
if model
|
||||
.base_url
|
||||
.as_deref()
|
||||
.is_some_and(|value| !valid_http_endpoint(value))
|
||||
{
|
||||
add_reason(
|
||||
&mut reasons,
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::InvalidEndpoint,
|
||||
&format!("{pointer}/baseUrl"),
|
||||
),
|
||||
);
|
||||
}
|
||||
if model.api.is_none() && config.api.is_none() {
|
||||
add_reason(
|
||||
&mut reasons,
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::MissingEffectiveApi,
|
||||
&format!("{pointer}/api"),
|
||||
),
|
||||
);
|
||||
}
|
||||
if model.base_url.is_none() && config.base_url.is_none() {
|
||||
add_reason(
|
||||
&mut reasons,
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::MissingEffectiveEndpoint,
|
||||
&format!("{pointer}/baseUrl"),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (field, value) in [
|
||||
("contextWindow", model.context_window),
|
||||
("maxTokens", model.max_tokens),
|
||||
] {
|
||||
if value.is_some_and(|value| value.get() <= 0.0) {
|
||||
add_reason(
|
||||
&mut reasons,
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::NonPositiveModelLimit,
|
||||
&format!("{pointer}/{field}"),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
for level in model.thinking_level_map.keys() {
|
||||
if !is_managed_thinking_level(level) {
|
||||
add_reason(
|
||||
&mut reasons,
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::InvalidThinkingLevel,
|
||||
&format!("{pointer}/thinkingLevelMap/{}", escape_json_pointer(level)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (model_id, model_override) in &config.model_overrides {
|
||||
let pointer = format!("/modelOverrides/{}", escape_json_pointer(model_id));
|
||||
if !ids.contains(model_id.as_str()) {
|
||||
add_reason(
|
||||
&mut reasons,
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::UnknownModelOverride,
|
||||
&pointer,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (field, value) in [
|
||||
("contextWindow", model_override.context_window),
|
||||
("maxTokens", model_override.max_tokens),
|
||||
] {
|
||||
if value.is_some_and(|value| value.get() <= 0.0) {
|
||||
add_reason(
|
||||
&mut reasons,
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::NonPositiveModelLimit,
|
||||
&format!("{pointer}/{field}"),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
for level in model_override.thinking_level_map.keys() {
|
||||
if !is_managed_thinking_level(level) {
|
||||
add_reason(
|
||||
&mut reasons,
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::InvalidThinkingLevel,
|
||||
&format!("{pointer}/thinkingLevelMap/{}", escape_json_pointer(level)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
reasons
|
||||
}
|
||||
|
||||
fn managed_conversion_pointer(value: &Value) -> String {
|
||||
if let Some(models) = value.get("models").and_then(Value::as_array) {
|
||||
for (index, model) in models.iter().enumerate() {
|
||||
if let Some(map) = model.get("thinkingLevelMap").and_then(Value::as_object) {
|
||||
for (key, value) in map {
|
||||
if !(value.is_string() || value.is_null()) {
|
||||
return format!(
|
||||
"/models/{index}/thinkingLevelMap/{}",
|
||||
escape_json_pointer(key)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(overrides) = value.get("modelOverrides").and_then(Value::as_object) {
|
||||
for (model_id, model_override) in overrides {
|
||||
if let Some(map) = model_override
|
||||
.get("thinkingLevelMap")
|
||||
.and_then(Value::as_object)
|
||||
{
|
||||
for (key, value) in map {
|
||||
if !(value.is_string() || value.is_null()) {
|
||||
return format!(
|
||||
"/modelOverrides/{}/thinkingLevelMap/{}",
|
||||
escape_json_pointer(model_id),
|
||||
escape_json_pointer(key)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn managed_validation_reason(error: PiConfigError) -> PiDiagnosticReason {
|
||||
let (code, pointer) = match error {
|
||||
PiConfigError::ProviderHasNoModels => (PiReasonCode::MissingExplicitModels, "/models"),
|
||||
PiConfigError::EmptyApiId => (PiReasonCode::ManagedTypeConversionFailed, "/api"),
|
||||
PiConfigError::EmptyModelId => (PiReasonCode::EmptyModelId, "/models"),
|
||||
PiConfigError::DuplicateModelId(_) => (PiReasonCode::DuplicateModelId, "/models"),
|
||||
PiConfigError::ModelNotFound(_) => (PiReasonCode::ManagedTypeConversionFailed, "/models"),
|
||||
PiConfigError::MissingEffectiveApi { .. } => (PiReasonCode::MissingEffectiveApi, "/models"),
|
||||
PiConfigError::MissingEffectiveEndpoint { .. } => {
|
||||
(PiReasonCode::MissingEffectiveEndpoint, "/models")
|
||||
}
|
||||
PiConfigError::InvalidEndpoint { .. } => (PiReasonCode::InvalidEndpoint, "/baseUrl"),
|
||||
PiConfigError::UnknownModelOverride(_) => {
|
||||
(PiReasonCode::UnknownModelOverride, "/modelOverrides")
|
||||
}
|
||||
PiConfigError::InvalidCompat { .. } => (PiReasonCode::InvalidCompat, "/compat"),
|
||||
PiConfigError::EmptyOptionalField { .. } => (PiReasonCode::EmptyOptionalField, ""),
|
||||
PiConfigError::NonPositiveModelLimit { .. } => {
|
||||
(PiReasonCode::NonPositiveModelLimit, "/models")
|
||||
}
|
||||
PiConfigError::InvalidThinkingLevel { .. } => {
|
||||
(PiReasonCode::InvalidThinkingLevel, "/models")
|
||||
}
|
||||
};
|
||||
diagnostic_reason(PiDiagnosticLayer::Managed, code, pointer)
|
||||
}
|
||||
|
||||
fn classify_kind(
|
||||
provider_key: &str,
|
||||
value: &Value,
|
||||
raw_validity: PiRawValidity,
|
||||
) -> PiNativeEntryKind {
|
||||
if PI_BUILTIN_PROVIDER_KEYS.contains(&provider_key) {
|
||||
return PiNativeEntryKind::BuiltInOverlay;
|
||||
}
|
||||
if raw_validity != PiRawValidity::Valid {
|
||||
return PiNativeEntryKind::UnknownShape;
|
||||
}
|
||||
let Some(object) = value.as_object() else {
|
||||
return PiNativeEntryKind::UnknownShape;
|
||||
};
|
||||
if object
|
||||
.get("models")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|models| !models.is_empty())
|
||||
{
|
||||
PiNativeEntryKind::CustomCatalog
|
||||
} else if object
|
||||
.keys()
|
||||
.any(|key| RECOGNIZED_PROVIDER_FIELDS.contains(&key.as_str()))
|
||||
{
|
||||
PiNativeEntryKind::ExtensionOverlay
|
||||
} else {
|
||||
PiNativeEntryKind::UnknownShape
|
||||
}
|
||||
}
|
||||
|
||||
fn map_raw_validity(validity: PiRawValidity) -> PiRawNativeValidity {
|
||||
match validity {
|
||||
PiRawValidity::Valid => PiRawNativeValidity::Valid,
|
||||
PiRawValidity::Invalid => PiRawNativeValidity::Invalid,
|
||||
PiRawValidity::Unknown => PiRawNativeValidity::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_composition_status(status: PiComposerStatus) -> PiCompositionStatus {
|
||||
match status {
|
||||
PiComposerStatus::Composed => PiCompositionStatus::Composed,
|
||||
PiComposerStatus::Failed => PiCompositionStatus::Failed,
|
||||
PiComposerStatus::Unknown => PiCompositionStatus::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_gateway_status(capability: PiGatewayCapability) -> PiGatewayStatus {
|
||||
match capability {
|
||||
PiGatewayCapability::Proxyable => PiGatewayStatus::Proxyable,
|
||||
PiGatewayCapability::DirectOnly => PiGatewayStatus::DirectOnly,
|
||||
PiGatewayCapability::Unknown => PiGatewayStatus::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_raw_reasons(raw: &PiRawSchemaEvaluation) -> Vec<PiDiagnosticReason> {
|
||||
raw.reasons
|
||||
.iter()
|
||||
.map(|reason| {
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::RawSchema,
|
||||
match reason.code {
|
||||
PiRawReasonCode::SchemaMismatch => PiReasonCode::RawSchemaMismatch,
|
||||
PiRawReasonCode::UnsupportedOperator => {
|
||||
PiReasonCode::RawSchemaUnsupportedOperator
|
||||
}
|
||||
PiRawReasonCode::PinDrift => PiReasonCode::RawSchemaPinDrift,
|
||||
PiRawReasonCode::AmbiguousSchema => PiReasonCode::RawSchemaAmbiguous,
|
||||
},
|
||||
&reason.json_pointer,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn map_composer_reasons(composition: &PiNativeComposition) -> Vec<PiDiagnosticReason> {
|
||||
composition
|
||||
.reasons
|
||||
.iter()
|
||||
.map(|reason| {
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::Composition,
|
||||
match reason.code {
|
||||
PiComposerReasonCode::CatalogRequired => PiReasonCode::CatalogRequired,
|
||||
PiComposerReasonCode::MissingExplicitModels => {
|
||||
PiReasonCode::MissingExplicitModels
|
||||
}
|
||||
PiComposerReasonCode::MissingEffectiveApi => PiReasonCode::MissingEffectiveApi,
|
||||
PiComposerReasonCode::MissingEffectiveEndpoint => {
|
||||
PiReasonCode::MissingEffectiveEndpoint
|
||||
}
|
||||
PiComposerReasonCode::NonPositiveModelLimit => {
|
||||
PiReasonCode::NonPositiveModelLimit
|
||||
}
|
||||
PiComposerReasonCode::CompositionFailed => PiReasonCode::CompositionFailed,
|
||||
},
|
||||
&reason.json_pointer,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn map_gateway_reasons(gateway: &PiGatewayAssessment) -> Vec<PiDiagnosticReason> {
|
||||
gateway
|
||||
.reasons
|
||||
.iter()
|
||||
.map(|reason| {
|
||||
diagnostic_reason(
|
||||
PiDiagnosticLayer::Gateway,
|
||||
match reason.code {
|
||||
PiGatewayReasonCode::UnsupportedFamily => {
|
||||
PiReasonCode::UnsupportedGatewayFamily
|
||||
}
|
||||
PiGatewayReasonCode::InvalidEndpoint => PiReasonCode::InvalidEndpoint,
|
||||
PiGatewayReasonCode::MissingCredential => {
|
||||
PiReasonCode::GatewayCredentialUnavailable
|
||||
}
|
||||
PiGatewayReasonCode::InvalidHeaderName => PiReasonCode::InvalidHeaderName,
|
||||
PiGatewayReasonCode::InvalidHeaderValue => PiReasonCode::InvalidHeaderValue,
|
||||
PiGatewayReasonCode::ProtectedHeader => PiReasonCode::ProtectedHeader,
|
||||
PiGatewayReasonCode::DeferredValueUnavailable => {
|
||||
PiReasonCode::DeferredValueUnavailable
|
||||
}
|
||||
},
|
||||
&reason.json_pointer,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn diagnostic_reason(
|
||||
layer: PiDiagnosticLayer,
|
||||
code: PiReasonCode,
|
||||
pointer: &str,
|
||||
) -> PiDiagnosticReason {
|
||||
PiDiagnosticReason::new(layer, code, Some(pointer.to_string()))
|
||||
}
|
||||
|
||||
fn add_reason(reasons: &mut Vec<PiDiagnosticReason>, reason: PiDiagnosticReason) {
|
||||
if !reasons.contains(&reason) {
|
||||
reasons.push(reason);
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_reasons(
|
||||
reasons: &mut Vec<PiDiagnosticReason>,
|
||||
candidates: impl IntoIterator<Item = PiDiagnosticReason>,
|
||||
) {
|
||||
for reason in candidates {
|
||||
add_reason(reasons, reason);
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_http_endpoint(value: &str) -> bool {
|
||||
Url::parse(value)
|
||||
.ok()
|
||||
.is_some_and(|url| matches!(url.scheme(), "http" | "https") && url.host().is_some())
|
||||
}
|
||||
|
||||
fn is_managed_thinking_level(value: &str) -> bool {
|
||||
matches!(
|
||||
value,
|
||||
"off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
|
||||
)
|
||||
}
|
||||
|
||||
fn escape_json_pointer(value: &str) -> String {
|
||||
value.replace('~', "~0").replace('/', "~1")
|
||||
}
|
||||
|
||||
fn fingerprint(raw_source: &str) -> String {
|
||||
format!("sha256:{:x}", Sha256::digest(raw_source.as_bytes()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
use std::fs;
|
||||
|
||||
fn by_key<'a>(diagnostics: &'a [PiNativeDiagnostic], key: &str) -> &'a PiNativeDiagnostic {
|
||||
diagnostics
|
||||
.iter()
|
||||
.find(|diagnostic| diagnostic.provider_key == key)
|
||||
.expect("diagnostic")
|
||||
}
|
||||
|
||||
fn has_reason(
|
||||
diagnostic: &PiNativeDiagnostic,
|
||||
layer: PiDiagnosticLayer,
|
||||
code: PiReasonCode,
|
||||
pointer: &str,
|
||||
) -> bool {
|
||||
diagnostic.reasons.iter().any(|reason| {
|
||||
reason.layer == layer
|
||||
&& reason.code == code
|
||||
&& reason.json_pointer.as_deref() == Some(pointer)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_inspection_service_certifies_the_native_state_matrix() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let path = temp.path().join("models.json");
|
||||
fs::write(
|
||||
&path,
|
||||
r#"{
|
||||
"providers": {
|
||||
"anthropic": {"baseUrl": "https://builtin.example"},
|
||||
"extension": {"api": "openai-responses"},
|
||||
"future-custom": {
|
||||
"api": "future-wire-v9",
|
||||
"baseUrl": "https://future.example/v9",
|
||||
"apiKey": "$FUTURE_KEY",
|
||||
"models": [{"id": "future"}]
|
||||
},
|
||||
"known-custom": {
|
||||
"api": "openai-responses",
|
||||
"baseUrl": "https://known.example/v1",
|
||||
"apiKey": "!read-secret",
|
||||
"headers": {"x-tenant": "${TENANT}"},
|
||||
"models": [{"id": "known"}]
|
||||
},
|
||||
"malformed": {"models": "not-an-array"}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("write fixture");
|
||||
let bytes_before = fs::read(&path).expect("before");
|
||||
let claims = BTreeMap::new();
|
||||
let diagnostics =
|
||||
PiNativeInspectionService::inspect_catalog(&path, &claims).expect("inspect service");
|
||||
assert_eq!(fs::read(&path).expect("after"), bytes_before);
|
||||
assert_eq!(diagnostics.len(), 5);
|
||||
|
||||
for key in ["anthropic", "extension"] {
|
||||
let diagnostic = by_key(&diagnostics, key);
|
||||
assert_eq!(diagnostic.raw_validity, PiRawNativeValidity::Valid);
|
||||
assert_eq!(diagnostic.composition_status, PiCompositionStatus::Unknown);
|
||||
assert_eq!(diagnostic.gateway_status, PiGatewayStatus::Unknown);
|
||||
assert!(has_reason(
|
||||
diagnostic,
|
||||
PiDiagnosticLayer::Composition,
|
||||
PiReasonCode::CatalogRequired,
|
||||
"/models"
|
||||
));
|
||||
}
|
||||
|
||||
let future = by_key(&diagnostics, "future-custom");
|
||||
assert_eq!(future.raw_validity, PiRawNativeValidity::Valid);
|
||||
assert_eq!(future.composition_status, PiCompositionStatus::Composed);
|
||||
assert_eq!(future.managed_assessment, PiManagedAssessment::Manageable);
|
||||
assert_eq!(future.management_status, PiManagementStatus::Importable);
|
||||
assert_eq!(future.gateway_status, PiGatewayStatus::DirectOnly);
|
||||
assert!(has_reason(
|
||||
future,
|
||||
PiDiagnosticLayer::Gateway,
|
||||
PiReasonCode::UnsupportedGatewayFamily,
|
||||
"/models/0/api"
|
||||
));
|
||||
|
||||
let known = by_key(&diagnostics, "known-custom");
|
||||
assert_eq!(known.composition_status, PiCompositionStatus::Composed);
|
||||
assert_eq!(known.management_status, PiManagementStatus::Importable);
|
||||
assert_eq!(known.gateway_status, PiGatewayStatus::Proxyable);
|
||||
|
||||
let malformed = by_key(&diagnostics, "malformed");
|
||||
assert_eq!(malformed.raw_validity, PiRawNativeValidity::Invalid);
|
||||
assert_eq!(malformed.composition_status, PiCompositionStatus::Unknown);
|
||||
assert_eq!(malformed.gateway_status, PiGatewayStatus::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_rejection_does_not_control_raw_composition() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let path = temp.path().join("models.json");
|
||||
fs::write(
|
||||
&path,
|
||||
r#"{"providers":{"duplicate":{
|
||||
"apiKey":"literal",
|
||||
"models":[
|
||||
{"id":"same","api":"openai-responses","baseUrl":"https://one.example"},
|
||||
{"id":"same","api":"future-wire","baseUrl":"https://two.example"}
|
||||
],
|
||||
"modelOverrides":{"missing":{"maxTokens":7.5}}
|
||||
}}}"#,
|
||||
)
|
||||
.expect("write");
|
||||
let diagnostic =
|
||||
&PiNativeInspectionService::inspect_catalog(&path, &BTreeMap::new()).unwrap()[0];
|
||||
assert_eq!(diagnostic.raw_validity, PiRawNativeValidity::Valid);
|
||||
assert_eq!(
|
||||
diagnostic.managed_assessment,
|
||||
PiManagedAssessment::Unsupported
|
||||
);
|
||||
assert_eq!(diagnostic.composition_status, PiCompositionStatus::Composed);
|
||||
assert!(has_reason(
|
||||
diagnostic,
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::DuplicateModelId,
|
||||
"/models/1/id"
|
||||
));
|
||||
assert!(has_reason(
|
||||
diagnostic,
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::UnknownModelOverride,
|
||||
"/modelOverrides/missing"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_thinking_shape_is_lossless_for_composer_and_narrowed_separately() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let path = temp.path().join("models.json");
|
||||
fs::write(
|
||||
&path,
|
||||
r#"{"providers":{"thinking":{
|
||||
"api":"anthropic-messages",
|
||||
"baseUrl":"https://thinking.example",
|
||||
"apiKey":"literal",
|
||||
"models":[{
|
||||
"id":"m",
|
||||
"thinkingLevelMap":{"high":"future-high","future":{"opaque":true}}
|
||||
}]
|
||||
}}}"#,
|
||||
)
|
||||
.expect("write");
|
||||
let inspection =
|
||||
PiNativeInspectionService::inspect_entry(&path, "thinking", &BTreeMap::new())
|
||||
.expect("inspect")
|
||||
.expect("entry");
|
||||
assert_eq!(
|
||||
inspection.diagnostic.raw_validity,
|
||||
PiRawNativeValidity::Valid
|
||||
);
|
||||
assert_eq!(
|
||||
inspection.diagnostic.composition_status,
|
||||
PiCompositionStatus::Composed
|
||||
);
|
||||
assert_eq!(
|
||||
inspection.composition.models[0]
|
||||
.thinking_level_map
|
||||
.as_ref()
|
||||
.expect("opaque thinking")["future"],
|
||||
json!({"opaque": true})
|
||||
);
|
||||
assert_eq!(
|
||||
inspection.diagnostic.managed_assessment,
|
||||
PiManagedAssessment::Unsupported
|
||||
);
|
||||
assert!(has_reason(
|
||||
&inspection.diagnostic,
|
||||
PiDiagnosticLayer::Managed,
|
||||
PiReasonCode::ManagedTypeConversionFailed,
|
||||
"/models/0/thinkingLevelMap/future"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_entry_fingerprint_changes_only_when_that_raw_entry_changes() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let path = temp.path().join("models.json");
|
||||
let claims = BTreeMap::new();
|
||||
fs::write(
|
||||
&path,
|
||||
r#"{"providers":{
|
||||
"target":{"api":"openai-responses","baseUrl":"https://target","apiKey":"x","models":[{"id":"m"}]},
|
||||
"sibling":{"api":"openai-responses","baseUrl":"https://sibling","apiKey":"x","models":[{"id":"m"}]}
|
||||
}}"#,
|
||||
)
|
||||
.expect("write");
|
||||
let first = PiNativeInspectionService::inspect_entry(&path, "target", &claims)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.diagnostic
|
||||
.fingerprint;
|
||||
fs::write(
|
||||
&path,
|
||||
r#"{"providers":{
|
||||
"target":{"api":"openai-responses","baseUrl":"https://target","apiKey":"x","models":[{"id":"m"}]},
|
||||
"sibling":{"api":"openai-responses","baseUrl":"https://changed","apiKey":"x","models":[{"id":"m"}]}
|
||||
}}"#,
|
||||
)
|
||||
.expect("write sibling");
|
||||
let after_sibling = PiNativeInspectionService::inspect_entry(&path, "target", &claims)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.diagnostic
|
||||
.fingerprint;
|
||||
assert_eq!(first, after_sibling);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_dir_normalization_matches_pi_path_semantics() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let file_url = Url::from_file_path(temp.path())
|
||||
.expect("absolute temp path")
|
||||
.to_string();
|
||||
assert_eq!(
|
||||
normalize_pi_agent_dir(&file_url, Path::new("/unused")).expect("file URL"),
|
||||
temp.path()
|
||||
);
|
||||
let spaced = " relative agent dir ";
|
||||
assert_eq!(
|
||||
normalize_pi_agent_dir(spaced, Path::new("/unused")).expect("spaced path"),
|
||||
PathBuf::from(spaced)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pi_config_error_stays_managed_only() {
|
||||
let error = PiConfigError::EmptyApiId;
|
||||
assert_eq!(error.to_string(), "Pi API id cannot be empty");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user