mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-03 19:12:04 +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:
File diff suppressed because it is too large
Load Diff
Generated
+12
-1
@@ -783,6 +783,7 @@ dependencies = [
|
||||
"indexmap 2.13.0",
|
||||
"json-five",
|
||||
"json5",
|
||||
"jsonc-parser",
|
||||
"libc",
|
||||
"log",
|
||||
"objc2 0.5.2",
|
||||
@@ -800,6 +801,7 @@ dependencies = [
|
||||
"serde_yaml",
|
||||
"serial_test",
|
||||
"sha2",
|
||||
"syn 2.0.117",
|
||||
"sys-locale",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
@@ -2799,6 +2801,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonc-parser"
|
||||
version = "0.33.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0560e3f9a9a03ea6b6e90b41138c5db9e21526c99eb192c1a26c68176593285"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonptr"
|
||||
version = "0.6.3"
|
||||
@@ -4687,7 +4698,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -24,6 +24,7 @@ tauri-build = { version = "2.4.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
jsonc-parser = { version = "0.33", features = ["cst", "serde_json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
@@ -117,3 +118,4 @@ strip = "symbols"
|
||||
[dev-dependencies]
|
||||
serial_test = "3"
|
||||
tempfile = "3"
|
||||
syn = { version = "2", features = ["full", "visit"] }
|
||||
|
||||
@@ -0,0 +1,554 @@
|
||||
use regex::Regex;
|
||||
use serde_json::json;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
use syn::visit::{self, Visit};
|
||||
use syn::{
|
||||
Attribute, ExprLit, ImplItem, Item, ItemImpl, ItemStruct, ItemUse, Lit, Meta, Path as SynPath,
|
||||
Type, UseTree,
|
||||
};
|
||||
|
||||
static PROVIDER_DML: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r#"(?is)\b(?:INSERT(?:\s+OR\s+(?:ABORT|FAIL|IGNORE|REPLACE|ROLLBACK))?\s+INTO|UPDATE(?:\s+OR\s+(?:ABORT|FAIL|IGNORE|REPLACE|ROLLBACK))?|DELETE\s+FROM)\s+["`\[]?(providers|provider_endpoints)\b"#,
|
||||
)
|
||||
.expect("compile provider DML classifier")
|
||||
});
|
||||
static RESTORE_CREATE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"(?is)\bCREATE\s+(?:TEMP(?:ORARY)?\s+)?(?:TABLE|INDEX|TRIGGER|VIEW)\b")
|
||||
.expect("compile restore DDL classifier")
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct Violation {
|
||||
kind: &'static str,
|
||||
path: String,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
fn is_cfg_test(attrs: &[Attribute]) -> bool {
|
||||
attrs.iter().any(|attribute| {
|
||||
if !attribute.path().is_ident("cfg") {
|
||||
return false;
|
||||
}
|
||||
match &attribute.meta {
|
||||
Meta::List(list) => list
|
||||
.tokens
|
||||
.to_string()
|
||||
.split(|character: char| !character.is_ascii_alphanumeric() && character != '_')
|
||||
.any(|token| token == "test"),
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn flatten_use(tree: &UseTree, prefix: &mut Vec<String>, output: &mut Vec<Vec<String>>) {
|
||||
match tree {
|
||||
UseTree::Path(path) => {
|
||||
prefix.push(path.ident.to_string());
|
||||
flatten_use(&path.tree, prefix, output);
|
||||
prefix.pop();
|
||||
}
|
||||
UseTree::Name(name) => {
|
||||
let mut path = prefix.clone();
|
||||
path.push(name.ident.to_string());
|
||||
output.push(path);
|
||||
}
|
||||
UseTree::Rename(rename) => {
|
||||
let mut path = prefix.clone();
|
||||
path.push(rename.ident.to_string());
|
||||
output.push(path);
|
||||
}
|
||||
UseTree::Group(group) => {
|
||||
for item in &group.items {
|
||||
flatten_use(item, prefix, output);
|
||||
}
|
||||
}
|
||||
UseTree::Glob(_) => output.push(prefix.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn path_segments(path: &SynPath) -> Vec<String> {
|
||||
path.segments
|
||||
.iter()
|
||||
.map(|segment| segment.ident.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct ArchitectureVisitor<'a> {
|
||||
path: &'a str,
|
||||
violations: Vec<Violation>,
|
||||
internal_edges: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl ArchitectureVisitor<'_> {
|
||||
fn record_dependency(&mut self, segments: &[String]) {
|
||||
let source_module = if self.path.ends_with("pi_config/raw_schema.rs") {
|
||||
Some("raw_schema")
|
||||
} else if self.path.ends_with("pi_config/composer.rs") {
|
||||
Some("composer")
|
||||
} else if self.path.ends_with("pi_config/gateway.rs") {
|
||||
Some("gateway")
|
||||
} else if self.path.ends_with("pi_config/native.rs") {
|
||||
Some("native")
|
||||
} else if self.path.ends_with("pi_config/model.rs") {
|
||||
Some("model")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let Some(source_module) = source_module else {
|
||||
return;
|
||||
};
|
||||
let qualified_internal_path = segments.len() > 1
|
||||
&& segments.iter().any(|segment| {
|
||||
matches!(segment.as_str(), "super" | "self" | "crate" | "pi_config")
|
||||
});
|
||||
if qualified_internal_path {
|
||||
for module in [
|
||||
"raw_schema",
|
||||
"composer",
|
||||
"gateway",
|
||||
"native",
|
||||
"model",
|
||||
"document",
|
||||
] {
|
||||
if module != source_module && segments.iter().any(|segment| segment == module) {
|
||||
self.internal_edges
|
||||
.insert(format!("{source_module}->{module}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let imports_gateway = segments
|
||||
.iter()
|
||||
.any(|segment| segment == "gateway" || segment.starts_with("PiGateway"));
|
||||
let imports_model_module = segments.len() > 1
|
||||
&& segments.iter().any(|segment| segment == "model")
|
||||
&& segments.iter().any(|segment| {
|
||||
matches!(segment.as_str(), "super" | "self" | "crate" | "pi_config")
|
||||
});
|
||||
let imports_managed = imports_model_module
|
||||
|| segments.iter().any(|segment| {
|
||||
segment == "PiApiFamily"
|
||||
|| segment.starts_with("PiManaged")
|
||||
|| segment == "PiEffectiveModel"
|
||||
});
|
||||
if matches!(source_module, "raw_schema" | "composer")
|
||||
&& (imports_gateway || imports_managed)
|
||||
{
|
||||
self.violations.push(Violation {
|
||||
kind: "cross_layer_import",
|
||||
path: self.path.to_string(),
|
||||
detail: format!(
|
||||
"{source_module} imports managed/gateway path {}",
|
||||
segments.join("::")
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let imports_raw_valid = segments
|
||||
.iter()
|
||||
.any(|segment| segment == "raw_schema" || segment == "PiRawValidProvider");
|
||||
if source_module == "gateway" && imports_raw_valid {
|
||||
self.violations.push(Violation {
|
||||
kind: "cross_layer_import",
|
||||
path: self.path.to_string(),
|
||||
detail: format!(
|
||||
"gateway constructs or imports raw-valid path {}",
|
||||
segments.join("::")
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn inspect_string(&mut self, literal: &str) {
|
||||
if PROVIDER_DML.is_match(literal) && !provider_dml_path_allowed(self.path) {
|
||||
self.violations.push(Violation {
|
||||
kind: "provider_dml",
|
||||
path: self.path.to_string(),
|
||||
detail: "provider table DML exists outside typed row/state, endpoint, migration, or canonical-copy authority".to_string(),
|
||||
});
|
||||
}
|
||||
if self.path.ends_with("database/backup.rs") && RESTORE_CREATE.is_match(literal) {
|
||||
self.violations.push(Violation {
|
||||
kind: "restore_create",
|
||||
path: self.path.to_string(),
|
||||
detail: "restore production code contains CREATE TABLE/INDEX/TRIGGER/VIEW"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ast> Visit<'ast> for ArchitectureVisitor<'_> {
|
||||
fn visit_item(&mut self, item: &'ast Item) {
|
||||
let attrs = match item {
|
||||
Item::Const(item) => &item.attrs,
|
||||
Item::Enum(item) => &item.attrs,
|
||||
Item::ExternCrate(item) => &item.attrs,
|
||||
Item::Fn(item) => &item.attrs,
|
||||
Item::ForeignMod(item) => &item.attrs,
|
||||
Item::Impl(item) => &item.attrs,
|
||||
Item::Macro(item) => &item.attrs,
|
||||
Item::Mod(item) => &item.attrs,
|
||||
Item::Static(item) => &item.attrs,
|
||||
Item::Struct(item) => &item.attrs,
|
||||
Item::Trait(item) => &item.attrs,
|
||||
Item::TraitAlias(item) => &item.attrs,
|
||||
Item::Type(item) => &item.attrs,
|
||||
Item::Union(item) => &item.attrs,
|
||||
Item::Use(item) => &item.attrs,
|
||||
Item::Verbatim(_) => {
|
||||
visit::visit_item(self, item);
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
visit::visit_item(self, item);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if !is_cfg_test(attrs) {
|
||||
visit::visit_item(self, item);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_impl_item(&mut self, item: &'ast ImplItem) {
|
||||
let attrs = match item {
|
||||
ImplItem::Const(item) => &item.attrs,
|
||||
ImplItem::Fn(item) => &item.attrs,
|
||||
ImplItem::Type(item) => &item.attrs,
|
||||
ImplItem::Macro(item) => &item.attrs,
|
||||
ImplItem::Verbatim(_) => {
|
||||
visit::visit_impl_item(self, item);
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
visit::visit_impl_item(self, item);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if !is_cfg_test(attrs) {
|
||||
visit::visit_impl_item(self, item);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_ident(&mut self, identifier: &'ast syn::Ident) {
|
||||
let identifier = identifier.to_string();
|
||||
if matches!(
|
||||
identifier.as_str(),
|
||||
"save_provider" | "save_provider_row_on_tx" | "save_provider_aggregate"
|
||||
) || identifier.starts_with("upsert_provider")
|
||||
{
|
||||
self.violations.push(Violation {
|
||||
kind: "forbidden_provider_symbol",
|
||||
path: self.path.to_string(),
|
||||
detail: format!("forbidden provider write symbol '{identifier}'"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_expr_lit(&mut self, expression: &'ast ExprLit) {
|
||||
if let Lit::Str(literal) = &expression.lit {
|
||||
self.inspect_string(&literal.value());
|
||||
}
|
||||
visit::visit_expr_lit(self, expression);
|
||||
}
|
||||
|
||||
fn visit_item_use(&mut self, item: &'ast ItemUse) {
|
||||
let mut paths = Vec::new();
|
||||
flatten_use(&item.tree, &mut Vec::new(), &mut paths);
|
||||
for path in paths {
|
||||
self.record_dependency(&path);
|
||||
}
|
||||
visit::visit_item_use(self, item);
|
||||
}
|
||||
|
||||
fn visit_path(&mut self, path: &'ast SynPath) {
|
||||
self.record_dependency(&path_segments(path));
|
||||
visit::visit_path(self, path);
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_dml_path_allowed(path: &str) -> bool {
|
||||
[
|
||||
"database/dao/provider_write.rs",
|
||||
"database/dao/providers.rs",
|
||||
"database/dao/failover.rs",
|
||||
"database/schema.rs",
|
||||
"database/migration.rs",
|
||||
"database/backup.rs",
|
||||
]
|
||||
.iter()
|
||||
.any(|allowed| path.ends_with(allowed))
|
||||
}
|
||||
|
||||
fn scan_source(path: &str, source: &str) -> (Vec<Violation>, BTreeSet<String>) {
|
||||
let syntax = match syn::parse_file(source) {
|
||||
Ok(syntax) => syntax,
|
||||
Err(error) => {
|
||||
return (
|
||||
vec![Violation {
|
||||
kind: "parse_error",
|
||||
path: path.to_string(),
|
||||
detail: error.to_string(),
|
||||
}],
|
||||
BTreeSet::new(),
|
||||
)
|
||||
}
|
||||
};
|
||||
let mut visitor = ArchitectureVisitor {
|
||||
path,
|
||||
violations: Vec::new(),
|
||||
internal_edges: BTreeSet::new(),
|
||||
};
|
||||
visitor.visit_file(&syntax);
|
||||
(visitor.violations, visitor.internal_edges)
|
||||
}
|
||||
|
||||
fn rust_sources(root: &Path) -> Vec<PathBuf> {
|
||||
fn walk(directory: &Path, output: &mut Vec<PathBuf>) {
|
||||
let mut entries = fs::read_dir(directory)
|
||||
.unwrap_or_else(|error| panic!("read {}: {error}", directory.display()))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap_or_else(|error| panic!("read entry in {}: {error}", directory.display()));
|
||||
entries.sort_by_key(|entry| entry.path());
|
||||
for entry in entries {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
walk(&path, output);
|
||||
} else if path.extension().is_some_and(|extension| extension == "rs") {
|
||||
output.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut output = Vec::new();
|
||||
walk(root, &mut output);
|
||||
output
|
||||
}
|
||||
|
||||
fn type_leaf(ty: &Type) -> String {
|
||||
match ty {
|
||||
Type::Path(path) => path
|
||||
.path
|
||||
.segments
|
||||
.last()
|
||||
.map(|segment| segment.ident.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
Type::Reference(reference) => type_leaf(&reference.elem),
|
||||
_ => "other".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_write_api_snapshot(source: &str) -> serde_json::Value {
|
||||
let syntax = syn::parse_file(source).expect("parse provider write authority");
|
||||
let type_names = [
|
||||
"ProviderKey",
|
||||
"ProviderRowUpdate",
|
||||
"NewEndpoint",
|
||||
"NewProviderAggregate",
|
||||
"RenameProvider",
|
||||
];
|
||||
let method_names = [
|
||||
"create_provider",
|
||||
"update_provider",
|
||||
"rename_db_only_additive_provider",
|
||||
"add_provider_endpoint",
|
||||
"remove_provider_endpoint",
|
||||
"touch_provider_endpoint",
|
||||
];
|
||||
let mut types = BTreeMap::<String, Vec<String>>::new();
|
||||
let mut methods = BTreeMap::<String, Vec<String>>::new();
|
||||
for item in syntax.items {
|
||||
if let Item::Struct(ItemStruct { ident, fields, .. }) = &item {
|
||||
if type_names.contains(&ident.to_string().as_str()) {
|
||||
types.insert(
|
||||
ident.to_string(),
|
||||
fields
|
||||
.iter()
|
||||
.filter_map(|field| field.ident.as_ref().map(ToString::to_string))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Item::Impl(ItemImpl { self_ty, items, .. }) = item {
|
||||
if type_leaf(&self_ty) != "Database" {
|
||||
continue;
|
||||
}
|
||||
for item in items {
|
||||
let ImplItem::Fn(function) = item else {
|
||||
continue;
|
||||
};
|
||||
let name = function.sig.ident.to_string();
|
||||
if !method_names.contains(&name.as_str()) {
|
||||
continue;
|
||||
}
|
||||
let inputs = function
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|argument| match argument {
|
||||
syn::FnArg::Receiver(_) => None,
|
||||
syn::FnArg::Typed(argument) => Some(type_leaf(&argument.ty)),
|
||||
})
|
||||
.collect();
|
||||
methods.insert(name, inputs);
|
||||
}
|
||||
}
|
||||
}
|
||||
json!({
|
||||
"manifestVersion": 1,
|
||||
"codeAuthority": "src-tauri/src/database/dao/provider_write.rs",
|
||||
"types": types,
|
||||
"databaseMethods": methods,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_scanner_accepts_production_tree_and_matches_machine_snapshots() {
|
||||
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let source_root = manifest_dir.join("src");
|
||||
let mut violations = Vec::new();
|
||||
let mut edges = BTreeSet::new();
|
||||
for path in rust_sources(&source_root) {
|
||||
let relative = path
|
||||
.strip_prefix(manifest_dir)
|
||||
.expect("source is under manifest")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
if relative.ends_with("architecture_tests.rs")
|
||||
|| relative.ends_with("/tests.rs")
|
||||
|| relative.contains("/tests/")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let source = fs::read_to_string(&path)
|
||||
.unwrap_or_else(|error| panic!("read {}: {error}", path.display()));
|
||||
let (mut source_violations, source_edges) = scan_source(&relative, &source);
|
||||
violations.append(&mut source_violations);
|
||||
edges.extend(source_edges);
|
||||
}
|
||||
assert!(
|
||||
violations.is_empty(),
|
||||
"architecture violations:\n{}",
|
||||
violations
|
||||
.iter()
|
||||
.map(|violation| format!(
|
||||
"{} {}: {}",
|
||||
violation.kind, violation.path, violation.detail
|
||||
))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
|
||||
// Integration tests compile as external consumers and used to retain
|
||||
// calls to the deleted generic writer even after `cargo test --lib`
|
||||
// passed. They may contain deliberate SQL fixtures, so this pass checks
|
||||
// only that the forbidden API surface is absent from actual Rust syntax.
|
||||
let mut forbidden_test_symbols = Vec::new();
|
||||
for path in rust_sources(&manifest_dir.join("tests")) {
|
||||
let relative = path
|
||||
.strip_prefix(manifest_dir)
|
||||
.expect("integration test is under manifest")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
let source = fs::read_to_string(&path)
|
||||
.unwrap_or_else(|error| panic!("read {}: {error}", path.display()));
|
||||
let (source_violations, _) = scan_source(&relative, &source);
|
||||
forbidden_test_symbols.extend(
|
||||
source_violations
|
||||
.into_iter()
|
||||
.filter(|violation| violation.kind == "forbidden_provider_symbol"),
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
forbidden_test_symbols.is_empty(),
|
||||
"forbidden provider write symbols remain in integration tests:\n{}",
|
||||
forbidden_test_symbols
|
||||
.iter()
|
||||
.map(|violation| format!("{}: {}", violation.path, violation.detail))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
|
||||
let module_snapshot: serde_json::Value = serde_json::from_str(include_str!(
|
||||
"../../tests/fixtures/pi/module-boundaries-v1.json"
|
||||
))
|
||||
.expect("parse module boundary snapshot");
|
||||
assert_eq!(
|
||||
json!({
|
||||
"manifestVersion": 1,
|
||||
"codeAuthority": "src-tauri/src/architecture_tests.rs",
|
||||
"edges": edges,
|
||||
}),
|
||||
module_snapshot
|
||||
);
|
||||
|
||||
let provider_source = fs::read_to_string(source_root.join("database/dao/provider_write.rs"))
|
||||
.expect("read provider write authority");
|
||||
let provider_snapshot: serde_json::Value = serde_json::from_str(include_str!(
|
||||
"../../tests/fixtures/pi/provider-write-api-v1.json"
|
||||
))
|
||||
.expect("parse provider write API snapshot");
|
||||
assert_eq!(
|
||||
provider_write_api_snapshot(&provider_source),
|
||||
provider_snapshot
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_scanner_negative_fixtures_prove_each_guard_fires() {
|
||||
let cases = [
|
||||
(
|
||||
"src/services/forbidden.rs",
|
||||
"fn call() { save_provider(); }",
|
||||
"forbidden_provider_symbol",
|
||||
),
|
||||
(
|
||||
"src/services/rogue.rs",
|
||||
r#"fn write(conn: &Db) { conn.execute("UPDATE OR REPLACE providers SET name = 'x'", []); }"#,
|
||||
"provider_dml",
|
||||
),
|
||||
(
|
||||
"src/database/backup.rs",
|
||||
r#"fn stage(conn: &Db) { conn.execute("CREATE TABLE leaked (id INTEGER)", []); }"#,
|
||||
"restore_create",
|
||||
),
|
||||
(
|
||||
"src/pi_config/composer.rs",
|
||||
"use super::gateway::PiGatewayApiFamily;",
|
||||
"cross_layer_import",
|
||||
),
|
||||
(
|
||||
"src/pi_config/gateway.rs",
|
||||
"use super::raw_schema::PiRawValidProvider;",
|
||||
"cross_layer_import",
|
||||
),
|
||||
];
|
||||
for (path, source, expected_kind) in cases {
|
||||
let (violations, _) = scan_source(path, source);
|
||||
assert!(
|
||||
violations
|
||||
.iter()
|
||||
.any(|violation| violation.kind == expected_kind),
|
||||
"negative fixture {path} did not trigger {expected_kind}: {violations:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let ignored_test_fixture = r#"
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn legacy() {
|
||||
save_provider();
|
||||
let _ = "CREATE TABLE ignored (id INTEGER)";
|
||||
let _ = "DELETE FROM providers";
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let (violations, _) = scan_source("src/services/ignored.rs", ignored_test_fixture);
|
||||
assert!(
|
||||
violations.is_empty(),
|
||||
"#[cfg(test)] content must be excluded: {violations:?}"
|
||||
);
|
||||
}
|
||||
@@ -39,6 +39,9 @@ mod tray;
|
||||
mod usage_events;
|
||||
mod usage_script;
|
||||
|
||||
#[cfg(test)]
|
||||
mod architecture_tests;
|
||||
|
||||
pub use app_config::{AppType, InstalledSkill, McpApps, McpServer, MultiAppConfig, SkillApps};
|
||||
pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic};
|
||||
pub use commands::open_provider_terminal;
|
||||
|
||||
@@ -0,0 +1,752 @@
|
||||
//! Credential-blind Pi native model composition.
|
||||
//!
|
||||
//! The only Pi-layer input is [`PiRawValidProvider`]. This module does not
|
||||
//! import managed DTOs or gateway families, and it never resolves credentials,
|
||||
//! environment variables, commands, files, or network resources.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use super::raw_schema::{PiRawApiId, PiRawValidProvider};
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
const PROVIDER_FIELDS: &[&str] = &[
|
||||
"name",
|
||||
"baseUrl",
|
||||
"apiKey",
|
||||
"api",
|
||||
"oauth",
|
||||
"headers",
|
||||
"compat",
|
||||
"authHeader",
|
||||
"models",
|
||||
"modelOverrides",
|
||||
];
|
||||
const MODEL_FIELDS: &[&str] = &[
|
||||
"id",
|
||||
"name",
|
||||
"baseUrl",
|
||||
"api",
|
||||
"reasoning",
|
||||
"thinkingLevelMap",
|
||||
"input",
|
||||
"cost",
|
||||
"contextWindow",
|
||||
"maxTokens",
|
||||
"headers",
|
||||
"compat",
|
||||
];
|
||||
const OVERRIDE_FIELDS: &[&str] = &[
|
||||
"name",
|
||||
"reasoning",
|
||||
"thinkingLevelMap",
|
||||
"input",
|
||||
"cost",
|
||||
"contextWindow",
|
||||
"maxTokens",
|
||||
"headers",
|
||||
"compat",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum PiComposerStatus {
|
||||
Composed,
|
||||
Failed,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum PiComposerReasonCode {
|
||||
CatalogRequired,
|
||||
MissingExplicitModels,
|
||||
MissingEffectiveApi,
|
||||
MissingEffectiveEndpoint,
|
||||
NonPositiveModelLimit,
|
||||
CompositionFailed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct PiComposerReason {
|
||||
pub code: PiComposerReasonCode,
|
||||
pub json_pointer: String,
|
||||
}
|
||||
|
||||
/// The lossless native result of pinned Pi composition.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct PiComposedNativeModel {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub api: PiRawApiId,
|
||||
pub provider: String,
|
||||
pub base_url: String,
|
||||
pub reasoning: bool,
|
||||
pub thinking_level_map: Option<Value>,
|
||||
pub input: Value,
|
||||
pub cost: Value,
|
||||
pub context_window: Value,
|
||||
pub max_tokens: Value,
|
||||
pub headers: BTreeMap<String, String>,
|
||||
pub compat: Option<Value>,
|
||||
pub api_key: Option<String>,
|
||||
pub oauth: Option<Value>,
|
||||
pub auth_header: bool,
|
||||
pub provider_extra: BTreeMap<String, Value>,
|
||||
pub model_extra: BTreeMap<String, Value>,
|
||||
pub override_extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct PiNativeComposition {
|
||||
pub status: PiComposerStatus,
|
||||
pub provider_id: Option<String>,
|
||||
pub provider_name: Option<String>,
|
||||
pub provider_base_url: Option<String>,
|
||||
pub models: Vec<PiComposedNativeModel>,
|
||||
pub ignored_override_keys: Vec<String>,
|
||||
pub reasons: Vec<PiComposerReason>,
|
||||
}
|
||||
|
||||
impl PiNativeComposition {
|
||||
pub(super) fn unavailable_without_valid_raw() -> Self {
|
||||
Self {
|
||||
status: PiComposerStatus::Unknown,
|
||||
provider_id: None,
|
||||
provider_name: None,
|
||||
provider_base_url: None,
|
||||
models: Vec::new(),
|
||||
ignored_override_keys: Vec::new(),
|
||||
reasons: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn catalog_required(pointer: &str) -> Self {
|
||||
Self {
|
||||
status: PiComposerStatus::Unknown,
|
||||
provider_id: None,
|
||||
provider_name: None,
|
||||
provider_base_url: None,
|
||||
models: Vec::new(),
|
||||
ignored_override_keys: Vec::new(),
|
||||
reasons: vec![PiComposerReason {
|
||||
code: PiComposerReasonCode::CatalogRequired,
|
||||
json_pointer: pointer.to_string(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn failed(code: PiComposerReasonCode, pointer: impl Into<String>) -> Self {
|
||||
Self {
|
||||
status: PiComposerStatus::Failed,
|
||||
provider_id: None,
|
||||
provider_name: None,
|
||||
provider_base_url: None,
|
||||
models: Vec::new(),
|
||||
ignored_override_keys: Vec::new(),
|
||||
reasons: vec![PiComposerReason {
|
||||
code,
|
||||
json_pointer: pointer.into(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn compose_explicit_custom_catalog(
|
||||
provider_id: &str,
|
||||
provider: &PiRawValidProvider,
|
||||
) -> PiNativeComposition {
|
||||
let Some(provider_object) = provider.raw().as_object() else {
|
||||
return PiNativeComposition::failed(PiComposerReasonCode::CompositionFailed, "");
|
||||
};
|
||||
let Some(definitions) = provider_object
|
||||
.get("models")
|
||||
.and_then(Value::as_array)
|
||||
.filter(|models| !models.is_empty())
|
||||
else {
|
||||
return PiNativeComposition::failed(PiComposerReasonCode::MissingExplicitModels, "/models");
|
||||
};
|
||||
|
||||
let provider_api = provider_object.get("api").and_then(Value::as_str);
|
||||
let provider_base_url = provider_object.get("baseUrl").and_then(Value::as_str);
|
||||
if provider_object.get("oauth").and_then(Value::as_str) == Some("radius")
|
||||
&& provider_base_url.is_none()
|
||||
{
|
||||
return PiNativeComposition::failed(
|
||||
PiComposerReasonCode::MissingEffectiveEndpoint,
|
||||
"/baseUrl",
|
||||
);
|
||||
}
|
||||
let provider_compat = provider_object.get("compat").cloned();
|
||||
let provider_headers = string_map(provider_object.get("headers"));
|
||||
let provider_extra = unknown_fields(provider_object, PROVIDER_FIELDS);
|
||||
let api_key = provider_object
|
||||
.get("apiKey")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned);
|
||||
let oauth = provider_object.get("oauth").cloned();
|
||||
let auth_header = provider_object
|
||||
.get("authHeader")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let overrides = provider_object
|
||||
.get("modelOverrides")
|
||||
.and_then(Value::as_object);
|
||||
|
||||
let mut models: Vec<PiComposedNativeModel> = Vec::with_capacity(definitions.len());
|
||||
for (index, definition_value) in definitions.iter().enumerate() {
|
||||
let Some(definition) = definition_value.as_object() else {
|
||||
return PiNativeComposition::failed(
|
||||
PiComposerReasonCode::CompositionFailed,
|
||||
format!("/models/{index}"),
|
||||
);
|
||||
};
|
||||
let Some(id) = definition.get("id").and_then(Value::as_str) else {
|
||||
return PiNativeComposition::failed(
|
||||
PiComposerReasonCode::CompositionFailed,
|
||||
format!("/models/{index}/id"),
|
||||
);
|
||||
};
|
||||
let existing_index = models.iter().position(|model| model.id == id);
|
||||
let defaults = existing_index
|
||||
.and_then(|position| models.get(position))
|
||||
.or_else(|| models.first());
|
||||
|
||||
let api_value = definition
|
||||
.get("api")
|
||||
.and_then(Value::as_str)
|
||||
.or(provider_api)
|
||||
.or_else(|| defaults.map(|model| model.api.as_str()));
|
||||
let Some(api_value) = api_value else {
|
||||
return PiNativeComposition::failed(
|
||||
PiComposerReasonCode::MissingEffectiveApi,
|
||||
format!("/models/{index}/api"),
|
||||
);
|
||||
};
|
||||
let Some(api) = PiRawApiId::new(api_value) else {
|
||||
return PiNativeComposition::failed(
|
||||
PiComposerReasonCode::MissingEffectiveApi,
|
||||
format!("/models/{index}/api"),
|
||||
);
|
||||
};
|
||||
|
||||
let base_url = definition
|
||||
.get("baseUrl")
|
||||
.and_then(Value::as_str)
|
||||
.or(provider_base_url)
|
||||
.or_else(|| defaults.map(|model| model.base_url.as_str()));
|
||||
let Some(base_url) = base_url.filter(|value| !value.is_empty()) else {
|
||||
return PiNativeComposition::failed(
|
||||
PiComposerReasonCode::MissingEffectiveEndpoint,
|
||||
format!("/models/{index}/baseUrl"),
|
||||
);
|
||||
};
|
||||
|
||||
for (field, code) in [
|
||||
("contextWindow", PiComposerReasonCode::NonPositiveModelLimit),
|
||||
("maxTokens", PiComposerReasonCode::NonPositiveModelLimit),
|
||||
] {
|
||||
if definition
|
||||
.get(field)
|
||||
.and_then(Value::as_f64)
|
||||
.is_some_and(|value| value <= 0.0)
|
||||
{
|
||||
return PiNativeComposition::failed(code, format!("/models/{index}/{field}"));
|
||||
}
|
||||
}
|
||||
|
||||
let model = PiComposedNativeModel {
|
||||
id: id.to_string(),
|
||||
name: definition
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or(id)
|
||||
.to_string(),
|
||||
api,
|
||||
provider: provider_id.to_string(),
|
||||
base_url: base_url.to_string(),
|
||||
reasoning: definition
|
||||
.get("reasoning")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
thinking_level_map: definition.get("thinkingLevelMap").cloned(),
|
||||
input: definition
|
||||
.get("input")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!(["text"])),
|
||||
cost: definition.get("cost").cloned().unwrap_or_else(default_cost),
|
||||
context_window: definition
|
||||
.get("contextWindow")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!(128000)),
|
||||
max_tokens: definition
|
||||
.get("maxTokens")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!(16384)),
|
||||
headers: BTreeMap::new(),
|
||||
compat: merge_compat(provider_compat.clone(), definition.get("compat").cloned()),
|
||||
api_key: api_key.clone(),
|
||||
oauth: oauth.clone(),
|
||||
auth_header,
|
||||
provider_extra: provider_extra.clone(),
|
||||
model_extra: unknown_fields(definition, MODEL_FIELDS),
|
||||
override_extra: BTreeMap::new(),
|
||||
};
|
||||
if let Some(existing_index) = existing_index {
|
||||
models[existing_index] = model;
|
||||
} else {
|
||||
models.push(model);
|
||||
}
|
||||
}
|
||||
|
||||
for model in &mut models {
|
||||
// Pinned Pi's rawModelHeaders uses Array.find, so duplicate model
|
||||
// definitions obtain request headers from the first definition even
|
||||
// though the later definition replaces the composed model slot.
|
||||
let definition = definitions
|
||||
.iter()
|
||||
.filter_map(Value::as_object)
|
||||
.find(|definition| {
|
||||
definition.get("id").and_then(Value::as_str) == Some(model.id.as_str())
|
||||
})
|
||||
.expect("raw-valid composed model has a source definition");
|
||||
let model_override =
|
||||
overrides.and_then(|overrides| overrides.get(&model.id).and_then(Value::as_object));
|
||||
|
||||
let mut headers = provider_headers.clone();
|
||||
if let Some(model_override) = model_override {
|
||||
headers.extend(string_map(model_override.get("headers")));
|
||||
}
|
||||
headers.extend(string_map(definition.get("headers")));
|
||||
model.headers = headers;
|
||||
|
||||
if let Some(model_override) = model_override {
|
||||
if let Some(name) = model_override.get("name").and_then(Value::as_str) {
|
||||
model.name = name.to_string();
|
||||
}
|
||||
if let Some(reasoning) = model_override.get("reasoning").and_then(Value::as_bool) {
|
||||
model.reasoning = reasoning;
|
||||
}
|
||||
if let Some(override_map) = model_override
|
||||
.get("thinkingLevelMap")
|
||||
.and_then(Value::as_object)
|
||||
{
|
||||
let mut merged = model
|
||||
.thinking_level_map
|
||||
.take()
|
||||
.and_then(|value| value.as_object().cloned())
|
||||
.unwrap_or_default();
|
||||
merged.extend(override_map.clone());
|
||||
model.thinking_level_map = Some(Value::Object(merged));
|
||||
}
|
||||
if let Some(input) = model_override.get("input") {
|
||||
model.input = input.clone();
|
||||
}
|
||||
if let Some(cost) = model_override.get("cost").and_then(Value::as_object) {
|
||||
model.cost = merge_cost(&model.cost, cost);
|
||||
}
|
||||
if let Some(context_window) = model_override.get("contextWindow") {
|
||||
model.context_window = context_window.clone();
|
||||
}
|
||||
if let Some(max_tokens) = model_override.get("maxTokens") {
|
||||
model.max_tokens = max_tokens.clone();
|
||||
}
|
||||
model.compat =
|
||||
merge_compat(model.compat.clone(), model_override.get("compat").cloned());
|
||||
model.override_extra = unknown_fields(model_override, OVERRIDE_FIELDS);
|
||||
}
|
||||
}
|
||||
|
||||
let model_ids = models
|
||||
.iter()
|
||||
.map(|model| model.id.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
let ignored_override_keys = overrides
|
||||
.into_iter()
|
||||
.flat_map(|overrides| overrides.keys())
|
||||
.filter(|model_id| !model_ids.contains(model_id.as_str()))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
PiNativeComposition {
|
||||
status: PiComposerStatus::Composed,
|
||||
provider_id: Some(provider_id.to_string()),
|
||||
provider_name: Some(
|
||||
provider_object
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or(provider_id)
|
||||
.to_string(),
|
||||
),
|
||||
provider_base_url: provider_base_url.map(ToOwned::to_owned),
|
||||
models,
|
||||
ignored_override_keys,
|
||||
reasons: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_cost() -> Value {
|
||||
json!({
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
})
|
||||
}
|
||||
|
||||
fn merge_cost(base: &Value, overlay: &Map<String, Value>) -> Value {
|
||||
let mut merged = base.as_object().cloned().unwrap_or_default();
|
||||
for key in ["input", "output", "cacheRead", "cacheWrite", "tiers"] {
|
||||
if let Some(value) = overlay.get(key) {
|
||||
merged.insert(key.to_string(), value.clone());
|
||||
}
|
||||
}
|
||||
Value::Object(merged)
|
||||
}
|
||||
|
||||
fn merge_compat(base: Option<Value>, overlay: Option<Value>) -> Option<Value> {
|
||||
let Some(overlay) = overlay else {
|
||||
return base;
|
||||
};
|
||||
let mut merged = base
|
||||
.as_ref()
|
||||
.and_then(Value::as_object)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let Some(overlay_object) = overlay.as_object() else {
|
||||
return Some(overlay);
|
||||
};
|
||||
for (key, value) in overlay_object {
|
||||
if matches!(
|
||||
key.as_str(),
|
||||
"openRouterRouting" | "vercelGatewayRouting" | "chatTemplateKwargs"
|
||||
) {
|
||||
let base_nested = merged.get(key).and_then(Value::as_object);
|
||||
let override_nested = value.as_object();
|
||||
if base_nested.is_some() || override_nested.is_some() {
|
||||
let mut nested = base_nested.cloned().unwrap_or_default();
|
||||
if let Some(override_nested) = override_nested {
|
||||
nested.extend(override_nested.clone());
|
||||
}
|
||||
merged.insert(key.clone(), Value::Object(nested));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
merged.insert(key.clone(), value.clone());
|
||||
}
|
||||
Some(Value::Object(merged))
|
||||
}
|
||||
|
||||
fn string_map(value: Option<&Value>) -> BTreeMap<String, String> {
|
||||
value
|
||||
.and_then(Value::as_object)
|
||||
.into_iter()
|
||||
.flat_map(|object| object.iter())
|
||||
.filter_map(|(key, value)| value.as_str().map(|value| (key.clone(), value.to_string())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn unknown_fields(object: &Map<String, Value>, recognized: &[&str]) -> BTreeMap<String, Value> {
|
||||
object
|
||||
.iter()
|
||||
.filter(|(key, _)| !recognized.contains(&key.as_str()))
|
||||
.map(|(key, value)| (key.clone(), value.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::pi_config::raw_schema::{evaluate_provider_value, PiRawValidity};
|
||||
use serde::Deserialize;
|
||||
|
||||
const COMPOSER_ORACLE_SOURCE: &str =
|
||||
include_str!("../../../tests/fixtures/pi/native-oracle/composer-oracle-v1.json");
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ComposerOracle {
|
||||
cases: Vec<ComposerOracleCase>,
|
||||
fail_closed_cases: Vec<FailClosedCase>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ComposerOracleCase {
|
||||
id: String,
|
||||
provider_id: String,
|
||||
input: Value,
|
||||
execution: Execution,
|
||||
#[serde(default)]
|
||||
auth_execution: Option<Value>,
|
||||
#[serde(default)]
|
||||
expected: Option<Value>,
|
||||
#[serde(default)]
|
||||
expected_error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Execution {
|
||||
status: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct FailClosedCase {
|
||||
id: String,
|
||||
rust_expected_status: String,
|
||||
reason_code: String,
|
||||
}
|
||||
|
||||
fn model_as_oracle_value(model: &PiComposedNativeModel) -> Value {
|
||||
let mut object = Map::new();
|
||||
object.insert("id".into(), json!(model.id));
|
||||
object.insert("name".into(), json!(model.name));
|
||||
object.insert("api".into(), json!(model.api.as_str()));
|
||||
object.insert("provider".into(), json!(model.provider));
|
||||
object.insert("baseUrl".into(), json!(model.base_url));
|
||||
object.insert("reasoning".into(), json!(model.reasoning));
|
||||
if let Some(thinking) = &model.thinking_level_map {
|
||||
object.insert("thinkingLevelMap".into(), thinking.clone());
|
||||
}
|
||||
object.insert("input".into(), model.input.clone());
|
||||
object.insert("cost".into(), model.cost.clone());
|
||||
object.insert("contextWindow".into(), model.context_window.clone());
|
||||
object.insert("maxTokens".into(), model.max_tokens.clone());
|
||||
object.insert("authHeader".into(), json!(model.auth_header));
|
||||
if let Some(compat) = &model.compat {
|
||||
object.insert("compat".into(), compat.clone());
|
||||
}
|
||||
if !model.headers.is_empty() {
|
||||
object.insert("headers".into(), json!(model.headers));
|
||||
}
|
||||
Value::Object(object)
|
||||
}
|
||||
|
||||
fn provider_as_oracle_value(composition: &PiNativeComposition) -> Value {
|
||||
let mut object = Map::new();
|
||||
object.insert(
|
||||
"id".into(),
|
||||
json!(composition
|
||||
.provider_id
|
||||
.as_ref()
|
||||
.expect("composed provider id")),
|
||||
);
|
||||
object.insert(
|
||||
"name".into(),
|
||||
json!(composition
|
||||
.provider_name
|
||||
.as_ref()
|
||||
.expect("composed provider name")),
|
||||
);
|
||||
if let Some(base_url) = &composition.provider_base_url {
|
||||
object.insert("baseUrl".into(), json!(base_url));
|
||||
}
|
||||
Value::Object(object)
|
||||
}
|
||||
|
||||
fn json_numbers_equal(left: &Value, right: &Value) -> bool {
|
||||
match (left, right) {
|
||||
(Value::Number(left), Value::Number(right)) => left.as_f64() == right.as_f64(),
|
||||
(Value::Array(left), Value::Array(right)) => {
|
||||
left.len() == right.len()
|
||||
&& left
|
||||
.iter()
|
||||
.zip(right)
|
||||
.all(|(left, right)| json_numbers_equal(left, right))
|
||||
}
|
||||
(Value::Object(left), Value::Object(right)) => {
|
||||
left.len() == right.len()
|
||||
&& left.iter().all(|(key, left)| {
|
||||
right
|
||||
.get(key)
|
||||
.is_some_and(|right| json_numbers_equal(left, right))
|
||||
})
|
||||
}
|
||||
_ => left == right,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_composer_matches_actual_pinned_upstream_execution() {
|
||||
let oracle: ComposerOracle =
|
||||
serde_json::from_str(COMPOSER_ORACLE_SOURCE).expect("parse composer oracle");
|
||||
for case in oracle.cases {
|
||||
let raw = evaluate_provider_value(&case.input);
|
||||
if case.execution.status == "error" {
|
||||
assert!(
|
||||
case.expected_error.is_some(),
|
||||
"upstream error vector '{}' records its actual error",
|
||||
case.id
|
||||
);
|
||||
match raw.validity {
|
||||
PiRawValidity::Invalid => {}
|
||||
PiRawValidity::Valid => {
|
||||
let result = compose_explicit_custom_catalog(
|
||||
&case.provider_id,
|
||||
raw.valid_provider.as_ref().expect("raw-valid provider"),
|
||||
);
|
||||
assert_eq!(
|
||||
result.status,
|
||||
PiComposerStatus::Failed,
|
||||
"raw-valid upstream error case '{}'",
|
||||
case.id
|
||||
);
|
||||
}
|
||||
PiRawValidity::Unknown => {
|
||||
panic!("oracle case '{}' unexpectedly became Unknown", case.id)
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
assert_eq!(raw.validity, PiRawValidity::Valid, "case '{}'", case.id);
|
||||
let result = compose_explicit_custom_catalog(
|
||||
&case.provider_id,
|
||||
raw.valid_provider.as_ref().expect("raw-valid provider"),
|
||||
);
|
||||
assert_eq!(
|
||||
result.status,
|
||||
PiComposerStatus::Composed,
|
||||
"case '{}'",
|
||||
case.id
|
||||
);
|
||||
let auth_execution = case
|
||||
.auth_execution
|
||||
.as_ref()
|
||||
.expect("successful composer case records actual auth execution");
|
||||
assert_eq!(
|
||||
auth_execution.pointer("/status").and_then(Value::as_str),
|
||||
Some("success"),
|
||||
"case '{}'",
|
||||
case.id
|
||||
);
|
||||
let actual_resolved_key = auth_execution
|
||||
.pointer("/result/auth/apiKey")
|
||||
.and_then(Value::as_str)
|
||||
.expect("successful literal composer vector resolves an API key");
|
||||
assert!(
|
||||
result
|
||||
.models
|
||||
.iter()
|
||||
.all(|model| { model.api_key.as_deref() == Some(actual_resolved_key) }),
|
||||
"case '{}' preserves the same literal key that pinned Pi resolved",
|
||||
case.id
|
||||
);
|
||||
if case
|
||||
.input
|
||||
.get("authHeader")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let expected_bearer = format!("Bearer {actual_resolved_key}");
|
||||
assert_eq!(
|
||||
auth_execution
|
||||
.pointer("/result/auth/headers/Authorization")
|
||||
.and_then(Value::as_str),
|
||||
Some(expected_bearer.as_str()),
|
||||
"case '{}' uses pinned Pi authHeader behavior",
|
||||
case.id
|
||||
);
|
||||
}
|
||||
let actual = json!({
|
||||
"provider": provider_as_oracle_value(&result),
|
||||
"models": result
|
||||
.models
|
||||
.iter()
|
||||
.map(model_as_oracle_value)
|
||||
.collect::<Vec<_>>(),
|
||||
"ignoredOverrideKeys": result.ignored_override_keys,
|
||||
});
|
||||
let expected = case.expected.expect("successful upstream expected output");
|
||||
assert!(
|
||||
json_numbers_equal(&actual, &expected),
|
||||
"oracle case '{}'\nactual: {actual:#}\nexpected: {expected:#}",
|
||||
case.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_upstream_catalog_semantics_are_explicitly_unknown() {
|
||||
let oracle: ComposerOracle =
|
||||
serde_json::from_str(COMPOSER_ORACLE_SOURCE).expect("parse composer oracle");
|
||||
assert_eq!(oracle.fail_closed_cases.len(), 2);
|
||||
for case in oracle.fail_closed_cases {
|
||||
assert_eq!(case.rust_expected_status, "unknown", "case '{}'", case.id);
|
||||
assert_eq!(case.reason_code, "catalog_required", "case '{}'", case.id);
|
||||
let result = PiNativeComposition::catalog_required("/models");
|
||||
assert_eq!(result.status, PiComposerStatus::Unknown);
|
||||
assert_eq!(
|
||||
result.reasons[0].code,
|
||||
PiComposerReasonCode::CatalogRequired
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credential_expressions_are_preserved_without_execution() {
|
||||
let value = json!({
|
||||
"api": "openai-responses",
|
||||
"baseUrl": "https://example.test/v1",
|
||||
"apiKey": "!read-secret",
|
||||
"oauth": "radius",
|
||||
"authHeader": true,
|
||||
"headers": {"x-tenant": "${TENANT}"},
|
||||
"models": [{"id": "m"}]
|
||||
});
|
||||
let raw = evaluate_provider_value(&value);
|
||||
let composed = compose_explicit_custom_catalog(
|
||||
"deferred",
|
||||
raw.valid_provider.as_ref().expect("raw-valid"),
|
||||
);
|
||||
assert_eq!(composed.status, PiComposerStatus::Composed);
|
||||
assert_eq!(composed.models[0].api_key.as_deref(), Some("!read-secret"));
|
||||
assert_eq!(composed.models[0].oauth, Some(json!("radius")));
|
||||
assert!(composed.models[0].auth_header);
|
||||
assert_eq!(composed.models[0].headers["x-tenant"], "${TENANT}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_provider_model_and_override_fields_are_retained_losslessly() {
|
||||
let value = json!({
|
||||
"api": "future-wire-v9",
|
||||
"baseUrl": "https://example.test/v9",
|
||||
"apiKey": "literal",
|
||||
"futureProviderShape": {
|
||||
"nested": [1, {"flag": true}]
|
||||
},
|
||||
"models": [{
|
||||
"id": "m",
|
||||
"futureModelShape": {
|
||||
"mode": "novel",
|
||||
"threshold": 0.125
|
||||
}
|
||||
}],
|
||||
"modelOverrides": {
|
||||
"m": {
|
||||
"futureOverrideShape": [
|
||||
null,
|
||||
{"preserve": "exactly"}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
let raw = evaluate_provider_value(&value);
|
||||
let composed = compose_explicit_custom_catalog(
|
||||
"lossless",
|
||||
raw.valid_provider.as_ref().expect("raw-valid"),
|
||||
);
|
||||
assert_eq!(composed.status, PiComposerStatus::Composed);
|
||||
let model = &composed.models[0];
|
||||
assert_eq!(
|
||||
model.provider_extra["futureProviderShape"],
|
||||
json!({"nested": [1, {"flag": true}]})
|
||||
);
|
||||
assert_eq!(
|
||||
model.model_extra["futureModelShape"],
|
||||
json!({"mode": "novel", "threshold": 0.125})
|
||||
);
|
||||
assert_eq!(
|
||||
model.override_extra["futureOverrideShape"],
|
||||
json!([null, {"preserve": "exactly"}])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
//! Read-only access to Pi's shared `models.json` document.
|
||||
//!
|
||||
//! The semantic parse mirrors Pi's pinned `stripJsonComments()` behavior.
|
||||
//! A CST parse is additionally required so callers can fingerprint one exact
|
||||
//! provider value without making unrelated entries part of the revision.
|
||||
|
||||
use crate::error::AppError;
|
||||
use indexmap::IndexMap;
|
||||
use jsonc_parser::cst::{CstContainerNode, CstNode, CstObject, CstObjectProp, CstRootNode};
|
||||
use jsonc_parser::ParseOptions;
|
||||
use regex::Regex;
|
||||
use serde_json::Value;
|
||||
use std::fs::{self, File, Metadata, OpenOptions};
|
||||
use std::io::{Read, Take};
|
||||
use std::path::Path;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
const MAX_PI_MODELS_BYTES: u64 = 8 * 1024 * 1024;
|
||||
const EMPTY_MODELS_DOCUMENT: &str = "{\"providers\":{}}";
|
||||
|
||||
static PI_JSON_LINE_COMMENTS: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r#""(?:\\.|[^"\\])*"|//[^\n]*"#).expect("Pi JSON line-comment regex must compile")
|
||||
});
|
||||
static PI_JSON_TRAILING_COMMAS: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r#""(?:\\.|[^"\\])*"|,(\s*[}\]])"#)
|
||||
.expect("Pi JSON trailing-comma regex must compile")
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct PiRawProviderEntry {
|
||||
pub value: Value,
|
||||
pub raw_source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct PiModelsDocument {
|
||||
providers: IndexMap<String, PiRawProviderEntry>,
|
||||
}
|
||||
|
||||
impl PiModelsDocument {
|
||||
pub fn providers(&self) -> &IndexMap<String, PiRawProviderEntry> {
|
||||
&self.providers
|
||||
}
|
||||
}
|
||||
|
||||
fn pi_models_parse_options() -> ParseOptions {
|
||||
// Pinned Pi accepts standard double-quoted JSON with `//` comments and
|
||||
// trailing commas. jsonc-parser is broader by default, so all other
|
||||
// extensions stay disabled.
|
||||
ParseOptions {
|
||||
allow_comments: true,
|
||||
allow_loose_object_property_names: false,
|
||||
allow_trailing_commas: true,
|
||||
allow_missing_commas: false,
|
||||
allow_single_quoted_strings: false,
|
||||
allow_hexadecimal_numbers: false,
|
||||
allow_unary_plus_numbers: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn jsonc_error(path: &Path, message: impl std::fmt::Display) -> AppError {
|
||||
AppError::Config(format!(
|
||||
"JSON parse error in Pi models file {}: {message}",
|
||||
path.display()
|
||||
))
|
||||
}
|
||||
|
||||
/// Mirrors Pi commit `ab366ebe94cacd419d986be454f12b1b9913aaca`
|
||||
/// (`packages/coding-agent/src/utils/json.ts`).
|
||||
fn strip_pi_json_comments(input: &str) -> String {
|
||||
let without_comments =
|
||||
PI_JSON_LINE_COMMENTS.replace_all(input, |captures: ®ex::Captures<'_>| {
|
||||
let matched = captures
|
||||
.get(0)
|
||||
.expect("the full regex match is always present")
|
||||
.as_str();
|
||||
if matched.starts_with('"') {
|
||||
matched.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
});
|
||||
PI_JSON_TRAILING_COMMAS
|
||||
.replace_all(&without_comments, |captures: ®ex::Captures<'_>| {
|
||||
captures
|
||||
.get(1)
|
||||
.or_else(|| captures.get(0))
|
||||
.expect("the full regex match is always present")
|
||||
.as_str()
|
||||
.to_string()
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn cst_property_name(property: &CstObjectProp) -> Option<String> {
|
||||
property.name()?.decoded_value().ok()
|
||||
}
|
||||
|
||||
fn last_cst_property(object: &CstObject, name: &str) -> Option<CstObjectProp> {
|
||||
object
|
||||
.properties()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.find(|property| cst_property_name(property).as_deref() == Some(name))
|
||||
}
|
||||
|
||||
fn cst_object(node: CstNode, path: &Path, label: &str) -> Result<CstObject, AppError> {
|
||||
match node {
|
||||
CstNode::Container(CstContainerNode::Object(object)) => Ok(object),
|
||||
_ => Err(jsonc_error(path, format!("{label} must be an object"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_models_source(path: &Path, source: &str) -> Result<PiModelsDocument, AppError> {
|
||||
let document: Value = serde_json::from_str(&strip_pi_json_comments(source))
|
||||
.map_err(|error| AppError::json(path, error))?;
|
||||
let root = CstRootNode::parse(source, &pi_models_parse_options())
|
||||
.map_err(|error| jsonc_error(path, error))?;
|
||||
if root.to_serde_value().as_ref() != Some(&document) {
|
||||
return Err(jsonc_error(path, "CST does not match Pi's parsed document"));
|
||||
}
|
||||
|
||||
let semantic_providers = document
|
||||
.as_object()
|
||||
.and_then(|root| root.get("providers"))
|
||||
.and_then(Value::as_object)
|
||||
.ok_or_else(|| jsonc_error(path, "root must contain a providers object"))?;
|
||||
|
||||
let root_object = cst_object(
|
||||
root.value()
|
||||
.ok_or_else(|| jsonc_error(path, "document must contain a JSON value"))?,
|
||||
path,
|
||||
"root",
|
||||
)?;
|
||||
let providers_node = last_cst_property(&root_object, "providers")
|
||||
.and_then(|property| property.value())
|
||||
.ok_or_else(|| jsonc_error(path, "CST is missing the providers value"))?;
|
||||
let providers_object = cst_object(providers_node, path, "providers")?;
|
||||
|
||||
let mut providers = IndexMap::with_capacity(semantic_providers.len());
|
||||
for (provider_key, value) in semantic_providers {
|
||||
let raw_source = last_cst_property(&providers_object, provider_key)
|
||||
.and_then(|property| property.value())
|
||||
.ok_or_else(|| {
|
||||
jsonc_error(
|
||||
path,
|
||||
format!("CST is missing provider entry '{provider_key}'"),
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
providers.insert(
|
||||
provider_key.clone(),
|
||||
PiRawProviderEntry {
|
||||
value: value.clone(),
|
||||
raw_source,
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(PiModelsDocument { providers })
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn open_read_only(path: &Path) -> std::io::Result<File> {
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(libc::O_NOFOLLOW)
|
||||
.open(path)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn open_read_only(path: &Path) -> std::io::Result<File> {
|
||||
OpenOptions::new().read(true).open(path)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn same_file(opened: &Metadata, current: &Metadata) -> bool {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
opened.dev() == current.dev() && opened.ino() == current.ino()
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn same_file(opened: &Metadata, current: &Metadata) -> bool {
|
||||
opened.len() == current.len() && opened.modified().ok() == current.modified().ok()
|
||||
}
|
||||
|
||||
fn read_limited(mut reader: Take<&mut File>, path: &Path) -> Result<Vec<u8>, AppError> {
|
||||
let mut bytes = Vec::new();
|
||||
reader
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(|error| AppError::io(path, error))?;
|
||||
if bytes.len() as u64 > MAX_PI_MODELS_BYTES {
|
||||
return Err(AppError::Config(format!(
|
||||
"Pi models file exceeds {} bytes: {}",
|
||||
MAX_PI_MODELS_BYTES,
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn read_models_bytes(path: &Path) -> Result<Option<Vec<u8>>, AppError> {
|
||||
let initial = match fs::symlink_metadata(path) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(error) => return Err(AppError::io(path, error)),
|
||||
};
|
||||
if !initial.file_type().is_file() {
|
||||
return Err(AppError::Config(format!(
|
||||
"Pi models path must be a regular file, not a symlink or special file: {}",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
if initial.len() > MAX_PI_MODELS_BYTES {
|
||||
return Err(AppError::Config(format!(
|
||||
"Pi models file exceeds {} bytes: {}",
|
||||
MAX_PI_MODELS_BYTES,
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut file = open_read_only(path).map_err(|error| AppError::io(path, error))?;
|
||||
let opened = file.metadata().map_err(|error| AppError::io(path, error))?;
|
||||
if !opened.file_type().is_file() || opened.len() > MAX_PI_MODELS_BYTES {
|
||||
return Err(AppError::Config(format!(
|
||||
"Pi models path changed or exceeded its size limit: {}",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
let bytes = read_limited(file.by_ref().take(MAX_PI_MODELS_BYTES + 1), path)?;
|
||||
let completed = file.metadata().map_err(|error| AppError::io(path, error))?;
|
||||
|
||||
let current = fs::symlink_metadata(path).map_err(|error| AppError::io(path, error))?;
|
||||
if !current.file_type().is_file()
|
||||
|| !same_file(&completed, ¤t)
|
||||
|| opened.len() != bytes.len() as u64
|
||||
|| completed.len() != bytes.len() as u64
|
||||
|| current.len() != bytes.len() as u64
|
||||
|| opened.modified().ok() != completed.modified().ok()
|
||||
{
|
||||
return Err(AppError::Config(format!(
|
||||
"Pi models file changed during inspection: {}",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
Ok(Some(bytes))
|
||||
}
|
||||
|
||||
pub(super) fn read_pi_models_document(path: &Path) -> Result<PiModelsDocument, AppError> {
|
||||
let bytes =
|
||||
read_models_bytes(path)?.unwrap_or_else(|| EMPTY_MODELS_DOCUMENT.as_bytes().to_vec());
|
||||
let source = std::str::from_utf8(&bytes)
|
||||
.map_err(|error| jsonc_error(path, format!("file is not UTF-8: {error}")))?;
|
||||
parse_models_source(path, source)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn reads_exact_pi_jsonc_dialect_and_retains_entry_cst() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let path = temp.path().join("models.json");
|
||||
fs::write(
|
||||
&path,
|
||||
r#"{
|
||||
// root
|
||||
"providers": {
|
||||
"custom": {
|
||||
// nested comment
|
||||
"baseUrl": "https://example.test/v1",
|
||||
"models": [{"id": "model//literal",},],
|
||||
},
|
||||
},
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.expect("write");
|
||||
|
||||
let document = read_pi_models_document(&path).expect("read");
|
||||
let entry = &document.providers()["custom"];
|
||||
assert_eq!(entry.value["models"][0]["id"], "model//literal");
|
||||
assert!(entry.raw_source.contains("// nested comment"));
|
||||
assert!(entry.raw_source.contains("\"model//literal\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_json_extensions_that_pinned_pi_rejects() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
for (case, source) in [
|
||||
("single-quoted", "{'providers': {}}"),
|
||||
("bare-key", "{providers: {}}"),
|
||||
("block-comment", "{\"providers\": {/* nope */}}"),
|
||||
("missing-comma", "{\"providers\": {} \"other\": 1}"),
|
||||
] {
|
||||
let path = temp.path().join(format!("{case}.json"));
|
||||
fs::write(&path, source).expect("write");
|
||||
assert!(read_pi_models_document(&path).is_err(), "{case} must fail");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_file_is_an_empty_catalog_and_oversized_file_fails() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let missing = temp.path().join("missing-models.json");
|
||||
assert!(read_pi_models_document(&missing)
|
||||
.expect("missing")
|
||||
.providers()
|
||||
.is_empty());
|
||||
|
||||
let oversized = temp.path().join("oversized-models.json");
|
||||
let mut file = File::create(&oversized).expect("create");
|
||||
file.write_all(b"{").expect("seed");
|
||||
file.set_len(MAX_PI_MODELS_BYTES + 1).expect("extend");
|
||||
assert!(read_pi_models_document(&oversized).is_err());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn symlink_is_never_followed() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let target = temp.path().join("target.json");
|
||||
let link = temp.path().join("models.json");
|
||||
fs::write(&target, EMPTY_MODELS_DOCUMENT).expect("target");
|
||||
symlink(&target, &link).expect("symlink");
|
||||
assert!(read_pi_models_document(&link).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,767 @@
|
||||
//! Gateway-only Pi API families and candidate header planning.
|
||||
//!
|
||||
//! The four-family enum is intentionally confined to this module. Raw and
|
||||
//! managed layers retain opaque API identifiers and cannot accidentally reject
|
||||
//! a future Pi family merely because the gateway has not implemented it.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use super::composer::{PiComposedNativeModel, PiComposerStatus, PiNativeComposition};
|
||||
use http::{HeaderMap, HeaderName, HeaderValue};
|
||||
use url::Url;
|
||||
|
||||
const PROTECTED_HEADERS: &[&str] = &[
|
||||
"authorization",
|
||||
"connection",
|
||||
"content-length",
|
||||
"host",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailer",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
"x-api-key",
|
||||
"x-goog-api-key",
|
||||
];
|
||||
const PROTOCOL_HEADERS: &[&str] = &[
|
||||
"anthropic-version",
|
||||
"anthropic-beta",
|
||||
"openai-beta",
|
||||
"openai-version",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub(super) enum PiGatewayApiFamily {
|
||||
AnthropicMessages,
|
||||
OpenAiCompletions,
|
||||
OpenAiResponses,
|
||||
GoogleGenerativeAi,
|
||||
}
|
||||
|
||||
impl PiGatewayApiFamily {
|
||||
pub(super) const ALL: [Self; 4] = [
|
||||
Self::AnthropicMessages,
|
||||
Self::OpenAiCompletions,
|
||||
Self::OpenAiResponses,
|
||||
Self::GoogleGenerativeAi,
|
||||
];
|
||||
|
||||
pub(super) const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::AnthropicMessages => "anthropic-messages",
|
||||
Self::OpenAiCompletions => "openai-completions",
|
||||
Self::OpenAiResponses => "openai-responses",
|
||||
Self::GoogleGenerativeAi => "google-generative-ai",
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
Self::ALL
|
||||
.into_iter()
|
||||
.find(|family| family.as_str() == value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum PiGatewayCapability {
|
||||
Proxyable,
|
||||
DirectOnly,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum PiGatewayReasonCode {
|
||||
UnsupportedFamily,
|
||||
InvalidEndpoint,
|
||||
MissingCredential,
|
||||
InvalidHeaderName,
|
||||
InvalidHeaderValue,
|
||||
ProtectedHeader,
|
||||
DeferredValueUnavailable,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct PiGatewayReason {
|
||||
pub code: PiGatewayReasonCode,
|
||||
pub json_pointer: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct PiGatewayAssessment {
|
||||
pub capability: PiGatewayCapability,
|
||||
pub reasons: Vec<PiGatewayReason>,
|
||||
pub plans: Vec<CandidateHeaderPlan>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct DeferredHeaderValue {
|
||||
raw: String,
|
||||
}
|
||||
|
||||
impl DeferredHeaderValue {
|
||||
fn new(raw: impl Into<String>) -> Self {
|
||||
Self { raw: raw.into() }
|
||||
}
|
||||
|
||||
fn materialize(
|
||||
&self,
|
||||
resolver: &impl DeferredValueResolver,
|
||||
pointer: &str,
|
||||
) -> Result<HeaderValue, PiGatewayReason> {
|
||||
let materialized = if is_deferred(&self.raw) {
|
||||
resolver.resolve(&self.raw).ok_or_else(|| PiGatewayReason {
|
||||
code: PiGatewayReasonCode::DeferredValueUnavailable,
|
||||
json_pointer: pointer.to_string(),
|
||||
})?
|
||||
} else {
|
||||
self.raw.clone()
|
||||
};
|
||||
HeaderValue::from_str(&materialized).map_err(|_| PiGatewayReason {
|
||||
code: PiGatewayReasonCode::InvalidHeaderValue,
|
||||
json_pointer: pointer.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct CandidateHeaderPlan {
|
||||
family: PiGatewayApiFamily,
|
||||
endpoint: Url,
|
||||
credential: DeferredHeaderValue,
|
||||
auth_header: bool,
|
||||
custom_headers: Vec<(HeaderName, String, DeferredHeaderValue)>,
|
||||
protocol_headers: Vec<(HeaderName, String, DeferredHeaderValue)>,
|
||||
protocol_identity_predictable: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct MaterializedCandidate {
|
||||
pub endpoint: Url,
|
||||
pub headers: HeaderMap,
|
||||
family: PiGatewayApiFamily,
|
||||
protocol_headers: HeaderMap,
|
||||
protocol_identity_predictable: bool,
|
||||
}
|
||||
|
||||
pub(super) trait DeferredValueResolver {
|
||||
fn resolve(&self, expression: &str) -> Option<String>;
|
||||
}
|
||||
|
||||
impl<F> DeferredValueResolver for F
|
||||
where
|
||||
F: Fn(&str) -> Option<String>,
|
||||
{
|
||||
fn resolve(&self, expression: &str) -> Option<String> {
|
||||
self(expression)
|
||||
}
|
||||
}
|
||||
|
||||
impl CandidateHeaderPlan {
|
||||
fn build(
|
||||
model: &PiComposedNativeModel,
|
||||
model_index: usize,
|
||||
) -> Result<Self, Vec<PiGatewayReason>> {
|
||||
let mut reasons = Vec::new();
|
||||
let Some(family) = PiGatewayApiFamily::parse(model.api.as_str()) else {
|
||||
reasons.push(PiGatewayReason {
|
||||
code: PiGatewayReasonCode::UnsupportedFamily,
|
||||
json_pointer: format!("/models/{model_index}/api"),
|
||||
});
|
||||
return Err(reasons);
|
||||
};
|
||||
let endpoint = match Url::parse(&model.base_url) {
|
||||
Ok(endpoint)
|
||||
if matches!(endpoint.scheme(), "http" | "https") && endpoint.host().is_some() =>
|
||||
{
|
||||
endpoint
|
||||
}
|
||||
_ => {
|
||||
reasons.push(PiGatewayReason {
|
||||
code: PiGatewayReasonCode::InvalidEndpoint,
|
||||
json_pointer: format!("/models/{model_index}/baseUrl"),
|
||||
});
|
||||
return Err(reasons);
|
||||
}
|
||||
};
|
||||
let Some(credential) = model.api_key.as_ref() else {
|
||||
reasons.push(PiGatewayReason {
|
||||
code: PiGatewayReasonCode::MissingCredential,
|
||||
json_pointer: "/apiKey".to_string(),
|
||||
});
|
||||
return Err(reasons);
|
||||
};
|
||||
|
||||
let mut custom_headers = Vec::new();
|
||||
let mut protocol_headers = Vec::new();
|
||||
let mut protocol_identity_predictable = true;
|
||||
for (name, value) in &model.headers {
|
||||
let pointer = format!(
|
||||
"/models/{model_index}/headers/{}",
|
||||
escape_json_pointer(name)
|
||||
);
|
||||
let Ok(parsed_name) = HeaderName::from_bytes(name.as_bytes()) else {
|
||||
reasons.push(PiGatewayReason {
|
||||
code: PiGatewayReasonCode::InvalidHeaderName,
|
||||
json_pointer: pointer,
|
||||
});
|
||||
continue;
|
||||
};
|
||||
if PROTECTED_HEADERS.contains(&parsed_name.as_str()) {
|
||||
reasons.push(PiGatewayReason {
|
||||
code: PiGatewayReasonCode::ProtectedHeader,
|
||||
json_pointer: pointer,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if HeaderValue::from_str(value).is_err() {
|
||||
reasons.push(PiGatewayReason {
|
||||
code: PiGatewayReasonCode::InvalidHeaderValue,
|
||||
json_pointer: pointer,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let planned = (
|
||||
parsed_name,
|
||||
pointer,
|
||||
DeferredHeaderValue::new(value.clone()),
|
||||
);
|
||||
if PROTOCOL_HEADERS.contains(&planned.0.as_str()) {
|
||||
protocol_identity_predictable &= !value.starts_with('!');
|
||||
protocol_headers.push(planned);
|
||||
} else {
|
||||
custom_headers.push(planned);
|
||||
}
|
||||
}
|
||||
if !reasons.is_empty() {
|
||||
return Err(reasons);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
family,
|
||||
endpoint,
|
||||
credential: DeferredHeaderValue::new(credential.clone()),
|
||||
auth_header: model.auth_header,
|
||||
custom_headers,
|
||||
protocol_headers,
|
||||
protocol_identity_predictable,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn materialize(
|
||||
&self,
|
||||
resolver: &impl DeferredValueResolver,
|
||||
) -> Result<MaterializedCandidate, PiGatewayReason> {
|
||||
// A new map is allocated for every candidate. No value from a prior
|
||||
// candidate can survive failover.
|
||||
let mut headers = HeaderMap::new();
|
||||
let mut protocol_headers = HeaderMap::new();
|
||||
let credential = self.credential.materialize(resolver, "/apiKey")?;
|
||||
let bearer_credential = credential.clone();
|
||||
let (auth_name, auth_value) = match self.family {
|
||||
PiGatewayApiFamily::AnthropicMessages => {
|
||||
(HeaderName::from_static("x-api-key"), credential)
|
||||
}
|
||||
PiGatewayApiFamily::GoogleGenerativeAi => {
|
||||
(HeaderName::from_static("x-goog-api-key"), credential)
|
||||
}
|
||||
PiGatewayApiFamily::OpenAiCompletions | PiGatewayApiFamily::OpenAiResponses => {
|
||||
let credential = credential.to_str().map_err(|_| PiGatewayReason {
|
||||
code: PiGatewayReasonCode::InvalidHeaderValue,
|
||||
json_pointer: "/apiKey".to_string(),
|
||||
})?;
|
||||
let bearer =
|
||||
HeaderValue::from_str(&format!("Bearer {credential}")).map_err(|_| {
|
||||
PiGatewayReason {
|
||||
code: PiGatewayReasonCode::InvalidHeaderValue,
|
||||
json_pointer: "/apiKey".to_string(),
|
||||
}
|
||||
})?;
|
||||
(HeaderName::from_static("authorization"), bearer)
|
||||
}
|
||||
};
|
||||
headers.insert(auth_name, auth_value);
|
||||
if self.auth_header {
|
||||
let credential = bearer_credential.to_str().map_err(|_| PiGatewayReason {
|
||||
code: PiGatewayReasonCode::InvalidHeaderValue,
|
||||
json_pointer: "/apiKey".to_string(),
|
||||
})?;
|
||||
let bearer = HeaderValue::from_str(&format!("Bearer {credential}")).map_err(|_| {
|
||||
PiGatewayReason {
|
||||
code: PiGatewayReasonCode::InvalidHeaderValue,
|
||||
json_pointer: "/apiKey".to_string(),
|
||||
}
|
||||
})?;
|
||||
headers.insert(HeaderName::from_static("authorization"), bearer);
|
||||
}
|
||||
|
||||
for (name, pointer, value) in &self.custom_headers {
|
||||
headers.insert(name.clone(), value.materialize(resolver, pointer)?);
|
||||
}
|
||||
for (name, pointer, value) in &self.protocol_headers {
|
||||
let value = value.materialize(resolver, pointer)?;
|
||||
protocol_headers.insert(name.clone(), value.clone());
|
||||
headers.insert(name.clone(), value);
|
||||
}
|
||||
let host = authority_header(&self.endpoint).ok_or_else(|| PiGatewayReason {
|
||||
code: PiGatewayReasonCode::InvalidEndpoint,
|
||||
json_pointer: "/baseUrl".to_string(),
|
||||
})?;
|
||||
headers.insert(HeaderName::from_static("host"), host);
|
||||
Ok(MaterializedCandidate {
|
||||
endpoint: self.endpoint.clone(),
|
||||
headers,
|
||||
family: self.family,
|
||||
protocol_headers,
|
||||
protocol_identity_predictable: self.protocol_identity_predictable,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MaterializedCandidate {
|
||||
pub(super) fn failover_protocol_identity(&self) -> Option<(PiGatewayApiFamily, &HeaderMap)> {
|
||||
// Auth, tenant and arbitrary custom headers are deliberately excluded.
|
||||
self.protocol_identity_predictable
|
||||
.then_some((self.family, &self.protocol_headers))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn assess_composition(composition: &PiNativeComposition) -> PiGatewayAssessment {
|
||||
if composition.status != PiComposerStatus::Composed {
|
||||
return PiGatewayAssessment {
|
||||
capability: PiGatewayCapability::Unknown,
|
||||
reasons: Vec::new(),
|
||||
plans: Vec::new(),
|
||||
};
|
||||
}
|
||||
let mut plans = Vec::with_capacity(composition.models.len());
|
||||
let mut reasons = Vec::new();
|
||||
for (index, model) in composition.models.iter().enumerate() {
|
||||
match CandidateHeaderPlan::build(model, index) {
|
||||
Ok(plan) => plans.push(plan),
|
||||
Err(mut model_reasons) => reasons.append(&mut model_reasons),
|
||||
}
|
||||
}
|
||||
if reasons.is_empty() && plans.len() == composition.models.len() {
|
||||
PiGatewayAssessment {
|
||||
capability: PiGatewayCapability::Proxyable,
|
||||
reasons,
|
||||
plans,
|
||||
}
|
||||
} else {
|
||||
PiGatewayAssessment {
|
||||
capability: PiGatewayCapability::DirectOnly,
|
||||
reasons,
|
||||
plans: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn authority_header(url: &Url) -> Option<HeaderValue> {
|
||||
let host = url.host_str()?;
|
||||
let authority = match url.port() {
|
||||
Some(port) => format!("{host}:{port}"),
|
||||
None => host.to_string(),
|
||||
};
|
||||
HeaderValue::from_str(&authority).ok()
|
||||
}
|
||||
|
||||
fn is_deferred(value: &str) -> bool {
|
||||
value.starts_with('!') || value.contains('$')
|
||||
}
|
||||
|
||||
fn escape_json_pointer(value: &str) -> String {
|
||||
value.replace('~', "~0").replace('/', "~1")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::pi_config::composer::compose_explicit_custom_catalog;
|
||||
use crate::pi_config::raw_schema::evaluate_provider_value;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const TRANSPORT_ORACLE_SOURCE: &str =
|
||||
include_str!("../../../tests/fixtures/pi/native-oracle/transport-oracle-v1.json");
|
||||
|
||||
fn composed(input: serde_json::Value) -> PiNativeComposition {
|
||||
let raw = evaluate_provider_value(&input);
|
||||
compose_explicit_custom_catalog(
|
||||
"candidate",
|
||||
raw.valid_provider.as_ref().expect("raw-valid input"),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_gateway_module_closes_the_four_family_set() {
|
||||
assert_eq!(
|
||||
PiGatewayApiFamily::ALL.map(PiGatewayApiFamily::as_str),
|
||||
[
|
||||
"anthropic-messages",
|
||||
"openai-completions",
|
||||
"openai-responses",
|
||||
"google-generative-ai",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_api_is_composed_but_direct_only() {
|
||||
let composition = composed(json!({
|
||||
"api": "future-wire-v9",
|
||||
"baseUrl": "https://future.example/v9",
|
||||
"apiKey": "literal",
|
||||
"models": [{"id": "future"}]
|
||||
}));
|
||||
assert_eq!(composition.status, PiComposerStatus::Composed);
|
||||
let gateway = assess_composition(&composition);
|
||||
assert_eq!(gateway.capability, PiGatewayCapability::DirectOnly);
|
||||
assert_eq!(
|
||||
gateway.reasons[0].code,
|
||||
PiGatewayReasonCode::UnsupportedFamily
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_plan_rejects_invalid_protected_and_hop_headers() {
|
||||
for (name, expected) in [
|
||||
("bad header", PiGatewayReasonCode::InvalidHeaderName),
|
||||
("Host", PiGatewayReasonCode::ProtectedHeader),
|
||||
("Content-Length", PiGatewayReasonCode::ProtectedHeader),
|
||||
("Connection", PiGatewayReasonCode::ProtectedHeader),
|
||||
] {
|
||||
let composition = composed(json!({
|
||||
"api": "openai-responses",
|
||||
"baseUrl": "https://example.test/v1",
|
||||
"apiKey": "literal",
|
||||
"headers": {name: "value"},
|
||||
"models": [{"id": "m"}]
|
||||
}));
|
||||
let gateway = assess_composition(&composition);
|
||||
assert_eq!(
|
||||
gateway.capability,
|
||||
PiGatewayCapability::DirectOnly,
|
||||
"{name}"
|
||||
);
|
||||
assert_eq!(gateway.reasons[0].code, expected, "{name}");
|
||||
}
|
||||
|
||||
let composition = composed(json!({
|
||||
"api": "openai-responses",
|
||||
"baseUrl": "https://example.test/v1",
|
||||
"apiKey": "literal",
|
||||
"headers": {"x-inject": "ok\r\nbad: value"},
|
||||
"models": [{"id": "m"}]
|
||||
}));
|
||||
// TypeBox rejects non-header string syntax only at the gateway layer;
|
||||
// the raw schema intentionally accepts arbitrary strings.
|
||||
let gateway = assess_composition(&composition);
|
||||
assert_eq!(
|
||||
gateway.reasons[0].code,
|
||||
PiGatewayReasonCode::InvalidHeaderValue
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_materialization_is_per_candidate_and_precedes_network_io() {
|
||||
let first = composed(json!({
|
||||
"api": "openai-responses",
|
||||
"baseUrl": "https://first.example:8443/v1",
|
||||
"apiKey": "${FIRST_KEY}",
|
||||
"headers": {"x-tenant": "${FIRST_TENANT}"},
|
||||
"models": [{"id": "m"}]
|
||||
}));
|
||||
let second = composed(json!({
|
||||
"api": "openai-responses",
|
||||
"baseUrl": "https://second.example/v1",
|
||||
"apiKey": "${SECOND_KEY}",
|
||||
"headers": {"x-tenant": "${SECOND_TENANT}"},
|
||||
"models": [{"id": "m"}]
|
||||
}));
|
||||
let first_plan = assess_composition(&first).plans.remove(0);
|
||||
let second_plan = assess_composition(&second).plans.remove(0);
|
||||
|
||||
let first_values = BTreeMap::from([
|
||||
("${FIRST_KEY}".to_string(), "first-secret".to_string()),
|
||||
("${FIRST_TENANT}".to_string(), "tenant-a".to_string()),
|
||||
]);
|
||||
let first_materialized = first_plan
|
||||
.materialize(&|expression: &str| first_values.get(expression).cloned())
|
||||
.expect("first materialization");
|
||||
assert_eq!(
|
||||
first_materialized.headers[&HeaderName::from_static("host")],
|
||||
"first.example:8443"
|
||||
);
|
||||
assert_eq!(
|
||||
first_materialized.headers[&HeaderName::from_static("authorization")],
|
||||
"Bearer first-secret"
|
||||
);
|
||||
|
||||
let missing = second_plan.materialize(&|_expression: &str| None);
|
||||
assert_eq!(
|
||||
missing.expect_err("missing deferred value").code,
|
||||
PiGatewayReasonCode::DeferredValueUnavailable
|
||||
);
|
||||
|
||||
let second_values = BTreeMap::from([
|
||||
("${SECOND_KEY}".to_string(), "second-secret".to_string()),
|
||||
("${SECOND_TENANT}".to_string(), "tenant-b".to_string()),
|
||||
]);
|
||||
let second_materialized = second_plan
|
||||
.materialize(&|expression: &str| second_values.get(expression).cloned())
|
||||
.expect("second materialization");
|
||||
assert_eq!(
|
||||
second_materialized.headers[&HeaderName::from_static("host")],
|
||||
"second.example"
|
||||
);
|
||||
assert_eq!(
|
||||
second_materialized.headers[&HeaderName::from_static("authorization")],
|
||||
"Bearer second-secret"
|
||||
);
|
||||
assert_eq!(
|
||||
second_materialized.headers[&HeaderName::from_static("x-tenant")],
|
||||
"tenant-b"
|
||||
);
|
||||
assert_ne!(
|
||||
first_materialized.headers[&HeaderName::from_static("authorization")],
|
||||
second_materialized.headers[&HeaderName::from_static("authorization")]
|
||||
);
|
||||
assert_eq!(
|
||||
first_materialized.failover_protocol_identity(),
|
||||
second_materialized.failover_protocol_identity()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protocol_identity_uses_final_values_but_excludes_auth_tenant_and_custom_headers() {
|
||||
let first = composed(json!({
|
||||
"api": "anthropic-messages",
|
||||
"baseUrl": "https://first.example/v1",
|
||||
"apiKey": "first-secret",
|
||||
"headers": {
|
||||
"anthropic-version": "${VERSION_A}",
|
||||
"x-tenant": "tenant-a",
|
||||
"x-private": "private-a"
|
||||
},
|
||||
"models": [{"id": "m"}]
|
||||
}));
|
||||
let second = composed(json!({
|
||||
"api": "anthropic-messages",
|
||||
"baseUrl": "https://second.example/v1",
|
||||
"apiKey": "second-secret",
|
||||
"headers": {
|
||||
"anthropic-version": "${VERSION_B}",
|
||||
"x-tenant": "tenant-b",
|
||||
"x-private": "private-b"
|
||||
},
|
||||
"models": [{"id": "m"}]
|
||||
}));
|
||||
let first = assess_composition(&first)
|
||||
.plans
|
||||
.remove(0)
|
||||
.materialize(&|expression: &str| {
|
||||
(expression == "${VERSION_A}").then(|| "2023-06-01".to_string())
|
||||
})
|
||||
.expect("first protocol materialization");
|
||||
let same_protocol = assess_composition(&second)
|
||||
.plans
|
||||
.remove(0)
|
||||
.materialize(&|expression: &str| {
|
||||
(expression == "${VERSION_B}").then(|| "2023-06-01".to_string())
|
||||
})
|
||||
.expect("second protocol materialization");
|
||||
assert_eq!(
|
||||
first.failover_protocol_identity(),
|
||||
same_protocol.failover_protocol_identity(),
|
||||
"auth, origin, tenant and arbitrary custom headers do not affect wire identity"
|
||||
);
|
||||
|
||||
let changed = composed(json!({
|
||||
"api": "anthropic-messages",
|
||||
"baseUrl": "https://third.example/v1",
|
||||
"apiKey": "third-secret",
|
||||
"headers": {"anthropic-version": "2024-01-01"},
|
||||
"models": [{"id": "m"}]
|
||||
}));
|
||||
let changed = assess_composition(&changed)
|
||||
.plans
|
||||
.remove(0)
|
||||
.materialize(&|_expression: &str| None)
|
||||
.expect("changed protocol materialization");
|
||||
assert_ne!(
|
||||
first.failover_protocol_identity(),
|
||||
changed.failover_protocol_identity()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_protocol_headers_fail_before_candidate_use_and_commands_are_ineligible() {
|
||||
let composition = composed(json!({
|
||||
"api": "openai-responses",
|
||||
"baseUrl": "https://candidate.example/v1",
|
||||
"apiKey": "literal",
|
||||
"headers": {"openai-version": "${OPENAI_VERSION}"},
|
||||
"models": [{"id": "m"}]
|
||||
}));
|
||||
let plan = assess_composition(&composition).plans.remove(0);
|
||||
assert_eq!(
|
||||
plan.materialize(&|_expression: &str| None)
|
||||
.expect_err("unresolved protocol material")
|
||||
.code,
|
||||
PiGatewayReasonCode::DeferredValueUnavailable
|
||||
);
|
||||
|
||||
let command = composed(json!({
|
||||
"api": "openai-responses",
|
||||
"baseUrl": "https://candidate.example/v1",
|
||||
"apiKey": "literal",
|
||||
"headers": {"openai-version": "!resolve-version"},
|
||||
"models": [{"id": "m"}]
|
||||
}));
|
||||
let command = assess_composition(&command)
|
||||
.plans
|
||||
.remove(0)
|
||||
.materialize(&|expression: &str| {
|
||||
(expression == "!resolve-version").then(|| "2024-01-01".to_string())
|
||||
})
|
||||
.expect("command materializes for direct candidate use");
|
||||
assert!(
|
||||
command.failover_protocol_identity().is_none(),
|
||||
"unpredictable protocol commands are failover-ineligible"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_header_adds_candidate_local_bearer_without_reusing_another_candidate() {
|
||||
let composition = composed(json!({
|
||||
"api": "anthropic-messages",
|
||||
"baseUrl": "https://candidate.example/v1",
|
||||
"apiKey": "${KEY}",
|
||||
"authHeader": true,
|
||||
"models": [{"id": "m"}]
|
||||
}));
|
||||
let materialized = assess_composition(&composition)
|
||||
.plans
|
||||
.remove(0)
|
||||
.materialize(&|expression: &str| {
|
||||
(expression == "${KEY}").then(|| "candidate-secret".to_string())
|
||||
})
|
||||
.expect("authHeader materialization");
|
||||
assert_eq!(
|
||||
materialized.headers[&HeaderName::from_static("x-api-key")],
|
||||
"candidate-secret"
|
||||
);
|
||||
assert_eq!(
|
||||
materialized.headers[&HeaderName::from_static("authorization")],
|
||||
"Bearer candidate-secret"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_families_materialize_their_own_auth_headers() {
|
||||
for (family, auth_name, expected_value) in [
|
||||
("anthropic-messages", "x-api-key", "secret"),
|
||||
("openai-completions", "authorization", "Bearer secret"),
|
||||
("openai-responses", "authorization", "Bearer secret"),
|
||||
("google-generative-ai", "x-goog-api-key", "secret"),
|
||||
] {
|
||||
let composition = composed(json!({
|
||||
"api": family,
|
||||
"baseUrl": "https://candidate.example/v1",
|
||||
"apiKey": "secret",
|
||||
"models": [{"id": "m"}]
|
||||
}));
|
||||
let mut assessment = assess_composition(&composition);
|
||||
assert_eq!(assessment.capability, PiGatewayCapability::Proxyable);
|
||||
let materialized = assessment
|
||||
.plans
|
||||
.remove(0)
|
||||
.materialize(&|_expression: &str| None)
|
||||
.expect("literal materialization");
|
||||
assert_eq!(
|
||||
materialized.headers[&HeaderName::from_bytes(auth_name.as_bytes()).unwrap()],
|
||||
expected_value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_values_replay_actual_pinned_pi_transport_results() {
|
||||
let oracle: Value =
|
||||
serde_json::from_str(TRANSPORT_ORACLE_SOURCE).expect("parse transport oracle");
|
||||
for case in oracle["cases"].as_array().expect("transport cases") {
|
||||
let input = case["input"].as_str().expect("transport input");
|
||||
let composition = composed(json!({
|
||||
"api": "openai-responses",
|
||||
"baseUrl": "https://candidate.example/v1",
|
||||
"apiKey": input,
|
||||
"models": [{"id": "m"}]
|
||||
}));
|
||||
let plan = assess_composition(&composition).plans.remove(0);
|
||||
match case.pointer("/execution/status").and_then(Value::as_str) {
|
||||
Some("success") => {
|
||||
let expected = case["expected"].as_str().expect("actual Pi result");
|
||||
let materialized = plan
|
||||
.materialize(&|expression: &str| {
|
||||
(expression == input).then(|| expected.to_string())
|
||||
})
|
||||
.expect("replay actual Pi resolver result");
|
||||
let expected_bearer = format!("Bearer {expected}");
|
||||
assert_eq!(
|
||||
materialized.headers[&HeaderName::from_static("authorization")],
|
||||
expected_bearer,
|
||||
"transport case '{}'",
|
||||
case["id"]
|
||||
);
|
||||
}
|
||||
Some("error") => {
|
||||
assert!(case["expectedError"].is_string());
|
||||
assert_eq!(
|
||||
plan.materialize(&|_expression: &str| None)
|
||||
.expect_err("actual Pi resolver failure must discard candidate")
|
||||
.code,
|
||||
PiGatewayReasonCode::DeferredValueUnavailable,
|
||||
"transport case '{}'",
|
||||
case["id"]
|
||||
);
|
||||
}
|
||||
status => panic!("unexpected transport status {status:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
let header_case = &oracle["headerCase"];
|
||||
let input_headers = header_case["input"]
|
||||
.as_object()
|
||||
.expect("transport header input");
|
||||
let expected_headers = header_case["expected"]
|
||||
.as_object()
|
||||
.expect("actual Pi header output");
|
||||
let composition = composed(json!({
|
||||
"api": "openai-responses",
|
||||
"baseUrl": "https://candidate.example/v1",
|
||||
"apiKey": "literal-key",
|
||||
"headers": input_headers,
|
||||
"models": [{"id": "m"}]
|
||||
}));
|
||||
let materialized = assess_composition(&composition)
|
||||
.plans
|
||||
.remove(0)
|
||||
.materialize(&|expression: &str| {
|
||||
input_headers.iter().find_map(|(name, configured)| {
|
||||
(configured.as_str() == Some(expression))
|
||||
.then(|| expected_headers[name].as_str().map(ToOwned::to_owned))
|
||||
.flatten()
|
||||
})
|
||||
})
|
||||
.expect("replay actual Pi header resolver results");
|
||||
for (name, expected) in expected_headers {
|
||||
let name = HeaderName::from_bytes(name.as_bytes()).expect("oracle header name");
|
||||
assert_eq!(
|
||||
materialized.headers[&name],
|
||||
expected.as_str().expect("oracle header value")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,9 @@
|
||||
//! Pi's shared files and from the proxy data plane. Callers must use the
|
||||
//! typed model resolver rather than reimplementing provider/model inheritance.
|
||||
|
||||
mod composer;
|
||||
mod document;
|
||||
mod gateway;
|
||||
pub(crate) mod model;
|
||||
pub(crate) mod native;
|
||||
mod raw_schema;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"manifestVersion": 1,
|
||||
"codeAuthority": "src-tauri/src/architecture_tests.rs",
|
||||
"edges": [
|
||||
"composer->raw_schema",
|
||||
"gateway->composer",
|
||||
"native->composer",
|
||||
"native->document",
|
||||
"native->gateway",
|
||||
"native->model",
|
||||
"native->raw_schema"
|
||||
]
|
||||
}
|
||||
+2186
File diff suppressed because it is too large
Load Diff
+3000
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"version": 1,
|
||||
"pi": {
|
||||
"repository": "https://github.com/earendil-works/pi.git",
|
||||
"commit": "ab366ebe94cacd419d986be454f12b1b9913aaca"
|
||||
},
|
||||
"typeboxVersion": "1.3.7",
|
||||
"sources": {
|
||||
"modelConfig": {
|
||||
"path": "packages/coding-agent/src/core/model-config.ts",
|
||||
"sha256": "62141770d675ad6357a72e07354355f0eda29281c0e5be1b48d2360f341c7360"
|
||||
},
|
||||
"providerComposer": {
|
||||
"path": "packages/coding-agent/src/core/provider-composer.ts",
|
||||
"sha256": "17308a4179b330526eabf6c917fa13e9dbd9ece90d1555b870e87d39b5b60d9d"
|
||||
},
|
||||
"resolveConfigValue": {
|
||||
"path": "packages/coding-agent/src/core/resolve-config-value.ts",
|
||||
"sha256": "0f53dad47fe5d5d8837c022b7951ccd3bd5a9b577bd662f0986272110e83bcc7"
|
||||
}
|
||||
},
|
||||
"artifacts": {
|
||||
"provider-schema.snapshot.json": "e498c9f1b344eee1bd3c3ba74d1b648dcb835378cfad92800ec80078b825745c",
|
||||
"raw-oracle-v1.json": "5aaa37160f96a0fe50867d900ca38c73f13aba769e156a883324368d9dbeeb9a",
|
||||
"composer-oracle-v1.json": "f7e54bb84e5fd6d50e5762dc304834410fa73ef608c2f9c42475c5983f8e0cf5",
|
||||
"transport-oracle-v1.json": "b2c816e53b60da5cd6352d2c23934939e9f6dd0077971488fe9dd36fa723e855",
|
||||
"field-coverage-v1.json": "b8b85e611cf1dbef86c611df185ba8ac2d64160087d0c6e47747f838a0fafe42"
|
||||
},
|
||||
"harness": {
|
||||
"path": "scripts/generate-pi-native-oracle.mjs",
|
||||
"sha256": "f7a138831284b48ef655ef500a63313f0fc89cf08d319094895027ba4777cc20",
|
||||
"upstreamEntry": "packages/coding-agent/src/core/provider-composer.ts",
|
||||
"entryFunctions": [
|
||||
"composeModelProvider",
|
||||
"Provider.getModels",
|
||||
"resolveCompatibilityRequestConfig",
|
||||
"Provider.auth.apiKey.resolve"
|
||||
],
|
||||
"bundler": "esbuild@0.28.1",
|
||||
"transportShims": {
|
||||
"pi-ai": "debdf80a40ad2086467c7c3ed6fe43d6167caa85e5bff86e10f28cc36d8fcc53",
|
||||
"pi-ai/compat": "533cd8f8a242bb25b42db8249abcb539daf30889ea748b5114c177e44c3849d2"
|
||||
},
|
||||
"assertion": "expected composer outputs were captured by executing the pinned upstream entry functions; transport shims are unreachable during credential-blind model composition",
|
||||
"transportResolver": {
|
||||
"upstreamEntry": "packages/coding-agent/src/core/resolve-config-value.ts",
|
||||
"entryFunctions": [
|
||||
"resolveConfigValueOrThrow",
|
||||
"resolveHeadersOrThrow"
|
||||
],
|
||||
"bundler": "esbuild@0.28.1",
|
||||
"platform": "linux"
|
||||
}
|
||||
},
|
||||
"uncoveredSemantics": [
|
||||
{
|
||||
"id": "builtin-overlay-without-pinned-base-catalog",
|
||||
"unavailableContext": "pinned built-in Provider instance and model catalog",
|
||||
"rustExpectedStatus": "unknown",
|
||||
"reasonCode": "catalog_required"
|
||||
},
|
||||
{
|
||||
"id": "extension-overlay-without-extension-registration",
|
||||
"unavailableContext": "extension ProviderConfigInput and registered model catalog",
|
||||
"rustExpectedStatus": "unknown",
|
||||
"reasonCode": "catalog_required"
|
||||
},
|
||||
{
|
||||
"id": "radius-oauth-credential-lifecycle",
|
||||
"unavailableContext": "interactive Radius login, refresh credentials, and network exchange",
|
||||
"rustExpectedStatus": "direct_only",
|
||||
"reasonCode": "missing_gateway_credential"
|
||||
}
|
||||
],
|
||||
"schemaOperatorInventory": [
|
||||
"anyOf",
|
||||
"const",
|
||||
"items",
|
||||
"minLength",
|
||||
"patternProperties",
|
||||
"properties",
|
||||
"required",
|
||||
"type"
|
||||
],
|
||||
"evaluatorOperatorAllowlist": [
|
||||
"additionalProperties",
|
||||
"anyOf",
|
||||
"const",
|
||||
"items",
|
||||
"minLength",
|
||||
"patternProperties",
|
||||
"properties",
|
||||
"required",
|
||||
"type"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+723
@@ -0,0 +1,723 @@
|
||||
{
|
||||
"version": 1,
|
||||
"execution": {
|
||||
"engine": "typebox Value.Check",
|
||||
"typeboxVersion": "1.3.7",
|
||||
"schemaTarget": "ModelsConfigSchema.properties.providers.patternProperties['^.*$']"
|
||||
},
|
||||
"cases": [
|
||||
{
|
||||
"id": "all-schema-fields-valid",
|
||||
"input": {
|
||||
"name": "All Fields Provider",
|
||||
"baseUrl": "https://all-fields.example/v1",
|
||||
"apiKey": "literal-all-fields-key",
|
||||
"api": "openai-responses",
|
||||
"oauth": "radius",
|
||||
"headers": {
|
||||
"x-provider-field": "provider-value"
|
||||
},
|
||||
"compat": {
|
||||
"supportsStore": true,
|
||||
"supportsDeveloperRole": true,
|
||||
"supportsReasoningEffort": true,
|
||||
"supportsUsageInStreaming": true,
|
||||
"maxTokensField": "max_completion_tokens",
|
||||
"requiresToolResultName": true,
|
||||
"requiresAssistantAfterToolResult": true,
|
||||
"requiresThinkingAsText": true,
|
||||
"requiresReasoningContentOnAssistantMessages": true,
|
||||
"thinkingFormat": "chat-template",
|
||||
"chatTemplateKwargs": {
|
||||
"stringValue": "literal",
|
||||
"numberValue": 1.25,
|
||||
"booleanValue": true,
|
||||
"nullValue": null,
|
||||
"variableValue": {
|
||||
"$var": "thinking.effort",
|
||||
"omitWhenOff": true
|
||||
}
|
||||
},
|
||||
"cacheControlFormat": "anthropic",
|
||||
"openRouterRouting": {
|
||||
"allow_fallbacks": true,
|
||||
"require_parameters": true,
|
||||
"data_collection": "deny",
|
||||
"zdr": true,
|
||||
"enforce_distillable_text": true,
|
||||
"order": [
|
||||
"provider-a",
|
||||
"provider-b"
|
||||
],
|
||||
"only": [
|
||||
"provider-a"
|
||||
],
|
||||
"ignore": [
|
||||
"provider-z"
|
||||
],
|
||||
"quantizations": [
|
||||
"fp8"
|
||||
],
|
||||
"sort": {
|
||||
"by": "price",
|
||||
"partition": null
|
||||
},
|
||||
"max_price": {
|
||||
"prompt": 1.1,
|
||||
"completion": "2.2",
|
||||
"image": 3.3,
|
||||
"audio": "4.4",
|
||||
"request": 5.5
|
||||
},
|
||||
"preferred_min_throughput": {
|
||||
"p50": 10.5,
|
||||
"p75": 9.5,
|
||||
"p90": 8.5,
|
||||
"p99": 7.5
|
||||
},
|
||||
"preferred_max_latency": {
|
||||
"p50": 100.5,
|
||||
"p75": 200.5,
|
||||
"p90": 300.5,
|
||||
"p99": 400.5
|
||||
}
|
||||
},
|
||||
"vercelGatewayRouting": {
|
||||
"only": [
|
||||
"provider-a"
|
||||
],
|
||||
"order": [
|
||||
"provider-a",
|
||||
"provider-b"
|
||||
]
|
||||
},
|
||||
"supportsOpenAIGrammarTools": true,
|
||||
"supportsStrictMode": true,
|
||||
"sendSessionAffinityHeaders": true,
|
||||
"deferredToolsMode": "kimi",
|
||||
"sessionAffinityFormat": "openrouter",
|
||||
"supportsLongCacheRetention": true,
|
||||
"supportsToolSearch": true,
|
||||
"supportsEagerToolInputStreaming": true,
|
||||
"supportsCacheControlOnTools": true,
|
||||
"supportsTemperature": true,
|
||||
"forceAdaptiveThinking": true,
|
||||
"allowEmptySignature": true,
|
||||
"supportsStrictTools": true,
|
||||
"supportsToolReferences": true
|
||||
},
|
||||
"authHeader": true,
|
||||
"models": [
|
||||
{
|
||||
"id": "all-fields-model",
|
||||
"name": "All Fields Model",
|
||||
"api": "anthropic-messages",
|
||||
"baseUrl": "https://all-fields-model.example/v1",
|
||||
"reasoning": true,
|
||||
"thinkingLevelMap": {
|
||||
"off": null,
|
||||
"minimal": "minimal-effort",
|
||||
"low": "low-effort",
|
||||
"medium": "medium-effort",
|
||||
"high": "high-effort",
|
||||
"xhigh": "xhigh-effort",
|
||||
"max": "max-effort"
|
||||
},
|
||||
"input": [
|
||||
"text",
|
||||
"image"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0.11,
|
||||
"output": 0.22,
|
||||
"cacheRead": 0.033,
|
||||
"cacheWrite": 0.044,
|
||||
"tiers": [
|
||||
{
|
||||
"inputTokensAbove": 1000.5,
|
||||
"input": 0.55,
|
||||
"output": 0.66,
|
||||
"cacheRead": 0.077,
|
||||
"cacheWrite": 0.088
|
||||
}
|
||||
]
|
||||
},
|
||||
"contextWindow": 128000.5,
|
||||
"maxTokens": 16384.25,
|
||||
"headers": {
|
||||
"x-model-field": "model-value"
|
||||
},
|
||||
"compat": {
|
||||
"supportsStore": true,
|
||||
"supportsDeveloperRole": true,
|
||||
"supportsReasoningEffort": true,
|
||||
"supportsUsageInStreaming": true,
|
||||
"maxTokensField": "max_completion_tokens",
|
||||
"requiresToolResultName": true,
|
||||
"requiresAssistantAfterToolResult": true,
|
||||
"requiresThinkingAsText": true,
|
||||
"requiresReasoningContentOnAssistantMessages": true,
|
||||
"thinkingFormat": "chat-template",
|
||||
"chatTemplateKwargs": {
|
||||
"stringValue": "literal",
|
||||
"numberValue": 1.25,
|
||||
"booleanValue": true,
|
||||
"nullValue": null,
|
||||
"variableValue": {
|
||||
"$var": "thinking.effort",
|
||||
"omitWhenOff": true
|
||||
}
|
||||
},
|
||||
"cacheControlFormat": "anthropic",
|
||||
"openRouterRouting": {
|
||||
"allow_fallbacks": true,
|
||||
"require_parameters": true,
|
||||
"data_collection": "deny",
|
||||
"zdr": true,
|
||||
"enforce_distillable_text": true,
|
||||
"order": [
|
||||
"provider-a",
|
||||
"provider-b"
|
||||
],
|
||||
"only": [
|
||||
"provider-a"
|
||||
],
|
||||
"ignore": [
|
||||
"provider-z"
|
||||
],
|
||||
"quantizations": [
|
||||
"fp8"
|
||||
],
|
||||
"sort": {
|
||||
"by": "price",
|
||||
"partition": null
|
||||
},
|
||||
"max_price": {
|
||||
"prompt": 1.1,
|
||||
"completion": "2.2",
|
||||
"image": 3.3,
|
||||
"audio": "4.4",
|
||||
"request": 5.5
|
||||
},
|
||||
"preferred_min_throughput": {
|
||||
"p50": 10.5,
|
||||
"p75": 9.5,
|
||||
"p90": 8.5,
|
||||
"p99": 7.5
|
||||
},
|
||||
"preferred_max_latency": {
|
||||
"p50": 100.5,
|
||||
"p75": 200.5,
|
||||
"p90": 300.5,
|
||||
"p99": 400.5
|
||||
}
|
||||
},
|
||||
"vercelGatewayRouting": {
|
||||
"only": [
|
||||
"provider-a"
|
||||
],
|
||||
"order": [
|
||||
"provider-a",
|
||||
"provider-b"
|
||||
]
|
||||
},
|
||||
"supportsOpenAIGrammarTools": true,
|
||||
"supportsStrictMode": true,
|
||||
"sendSessionAffinityHeaders": true,
|
||||
"deferredToolsMode": "kimi",
|
||||
"sessionAffinityFormat": "openrouter",
|
||||
"supportsLongCacheRetention": true,
|
||||
"supportsToolSearch": true,
|
||||
"supportsEagerToolInputStreaming": true,
|
||||
"supportsCacheControlOnTools": true,
|
||||
"supportsTemperature": true,
|
||||
"forceAdaptiveThinking": true,
|
||||
"allowEmptySignature": true,
|
||||
"supportsStrictTools": true,
|
||||
"supportsToolReferences": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"modelOverrides": {
|
||||
"all-fields-model": {
|
||||
"name": "All Fields Override",
|
||||
"reasoning": false,
|
||||
"thinkingLevelMap": {
|
||||
"off": null,
|
||||
"minimal": "minimal-effort",
|
||||
"low": "low-effort",
|
||||
"medium": "medium-effort",
|
||||
"high": "high-effort",
|
||||
"xhigh": "xhigh-effort",
|
||||
"max": "max-effort"
|
||||
},
|
||||
"input": [
|
||||
"image",
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0.11,
|
||||
"output": 0.22,
|
||||
"cacheRead": 0.033,
|
||||
"cacheWrite": 0.044,
|
||||
"tiers": [
|
||||
{
|
||||
"inputTokensAbove": 1000.5,
|
||||
"input": 0.55,
|
||||
"output": 0.66,
|
||||
"cacheRead": 0.077,
|
||||
"cacheWrite": 0.088
|
||||
}
|
||||
]
|
||||
},
|
||||
"contextWindow": 256000.75,
|
||||
"maxTokens": 32768.5,
|
||||
"headers": {
|
||||
"x-override-field": "override-value"
|
||||
},
|
||||
"compat": {
|
||||
"supportsStore": true,
|
||||
"supportsDeveloperRole": true,
|
||||
"supportsReasoningEffort": true,
|
||||
"supportsUsageInStreaming": true,
|
||||
"maxTokensField": "max_completion_tokens",
|
||||
"requiresToolResultName": true,
|
||||
"requiresAssistantAfterToolResult": true,
|
||||
"requiresThinkingAsText": true,
|
||||
"requiresReasoningContentOnAssistantMessages": true,
|
||||
"thinkingFormat": "chat-template",
|
||||
"chatTemplateKwargs": {
|
||||
"stringValue": "literal",
|
||||
"numberValue": 1.25,
|
||||
"booleanValue": true,
|
||||
"nullValue": null,
|
||||
"variableValue": {
|
||||
"$var": "thinking.effort",
|
||||
"omitWhenOff": true
|
||||
}
|
||||
},
|
||||
"cacheControlFormat": "anthropic",
|
||||
"openRouterRouting": {
|
||||
"allow_fallbacks": true,
|
||||
"require_parameters": true,
|
||||
"data_collection": "deny",
|
||||
"zdr": true,
|
||||
"enforce_distillable_text": true,
|
||||
"order": [
|
||||
"provider-a",
|
||||
"provider-b"
|
||||
],
|
||||
"only": [
|
||||
"provider-a"
|
||||
],
|
||||
"ignore": [
|
||||
"provider-z"
|
||||
],
|
||||
"quantizations": [
|
||||
"fp8"
|
||||
],
|
||||
"sort": {
|
||||
"by": "price",
|
||||
"partition": null
|
||||
},
|
||||
"max_price": {
|
||||
"prompt": 1.1,
|
||||
"completion": "2.2",
|
||||
"image": 3.3,
|
||||
"audio": "4.4",
|
||||
"request": 5.5
|
||||
},
|
||||
"preferred_min_throughput": {
|
||||
"p50": 10.5,
|
||||
"p75": 9.5,
|
||||
"p90": 8.5,
|
||||
"p99": 7.5
|
||||
},
|
||||
"preferred_max_latency": {
|
||||
"p50": 100.5,
|
||||
"p75": 200.5,
|
||||
"p90": 300.5,
|
||||
"p99": 400.5
|
||||
}
|
||||
},
|
||||
"vercelGatewayRouting": {
|
||||
"only": [
|
||||
"provider-a"
|
||||
],
|
||||
"order": [
|
||||
"provider-a",
|
||||
"provider-b"
|
||||
]
|
||||
},
|
||||
"supportsOpenAIGrammarTools": true,
|
||||
"supportsStrictMode": true,
|
||||
"sendSessionAffinityHeaders": true,
|
||||
"deferredToolsMode": "kimi",
|
||||
"sessionAffinityFormat": "openrouter",
|
||||
"supportsLongCacheRetention": true,
|
||||
"supportsToolSearch": true,
|
||||
"supportsEagerToolInputStreaming": true,
|
||||
"supportsCacheControlOnTools": true,
|
||||
"supportsTemperature": true,
|
||||
"forceAdaptiveThinking": true,
|
||||
"allowEmptySignature": true,
|
||||
"supportsStrictTools": true,
|
||||
"supportsToolReferences": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "empty-provider-object",
|
||||
"input": {},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "null-provider",
|
||||
"input": null,
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "additional-provider-property",
|
||||
"input": {
|
||||
"futureProviderField": {
|
||||
"nested": true
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "empty-present-base-url",
|
||||
"input": {
|
||||
"baseUrl": ""
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "non-url-base-url-is-raw-string",
|
||||
"input": {
|
||||
"baseUrl": "not a URL"
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "radius-oauth",
|
||||
"input": {
|
||||
"oauth": "radius"
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "unknown-oauth-literal",
|
||||
"input": {
|
||||
"oauth": "other"
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "models-null",
|
||||
"input": {
|
||||
"models": null
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "model-missing-id",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"api": "openai-responses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "model-empty-id",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "integer-model-numbers",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "integer",
|
||||
"contextWindow": 128000,
|
||||
"maxTokens": 16384
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "fractional-model-numbers",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "fractional",
|
||||
"contextWindow": 128000.5,
|
||||
"maxTokens": 16384.25
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "negative-number-is-still-typebox-number",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "negative",
|
||||
"maxTokens": -1.5
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "string-is-not-number",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "string-limit",
|
||||
"maxTokens": "16384"
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "complete-cost-with-fractions",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "priced",
|
||||
"cost": {
|
||||
"input": 0.1,
|
||||
"output": 0.2,
|
||||
"cacheRead": 0.03,
|
||||
"cacheWrite": 0.04,
|
||||
"tiers": [
|
||||
{
|
||||
"inputTokensAbove": 1000.5,
|
||||
"input": 0.5,
|
||||
"output": 0.6,
|
||||
"cacheRead": 0.07,
|
||||
"cacheWrite": 0.08
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "incomplete-model-cost",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "priced",
|
||||
"cost": {
|
||||
"input": 0.1,
|
||||
"output": 0.2,
|
||||
"cacheRead": 0.03
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "compat-openrouter-recursive-valid",
|
||||
"input": {
|
||||
"compat": {
|
||||
"thinkingFormat": "chat-template",
|
||||
"chatTemplateKwargs": {
|
||||
"temperature": 0.25,
|
||||
"thinking": {
|
||||
"$var": "thinking.effort",
|
||||
"omitWhenOff": true
|
||||
},
|
||||
"nullable": null
|
||||
},
|
||||
"openRouterRouting": {
|
||||
"data_collection": "deny",
|
||||
"sort": {
|
||||
"by": "price",
|
||||
"partition": null
|
||||
},
|
||||
"max_price": {
|
||||
"prompt": 1.5,
|
||||
"completion": "2.0"
|
||||
},
|
||||
"preferred_min_throughput": {
|
||||
"p50": 10.5,
|
||||
"p99": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "compat-nested-union-invalid-in-every-branch",
|
||||
"input": {
|
||||
"compat": {
|
||||
"openRouterRouting": {
|
||||
"preferred_min_throughput": {
|
||||
"p50": "fast"
|
||||
}
|
||||
},
|
||||
"supportsToolSearch": "yes",
|
||||
"supportsTemperature": 1
|
||||
}
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "compat-union-additional-field",
|
||||
"input": {
|
||||
"compat": {
|
||||
"futureCompatField": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "compat-null",
|
||||
"input": {
|
||||
"compat": null
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "thinking-map-unknown-key-and-string-value",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "thinking",
|
||||
"thinkingLevelMap": {
|
||||
"low": null,
|
||||
"max": "future-effort",
|
||||
"future": {
|
||||
"nested": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "thinking-map-known-key-invalid-value",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "thinking",
|
||||
"thinkingLevelMap": {
|
||||
"low": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "compat-sort-string-union-branch",
|
||||
"input": {
|
||||
"compat": {
|
||||
"openRouterRouting": {
|
||||
"sort": "price"
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "compat-sort-object-union-branch",
|
||||
"input": {
|
||||
"compat": {
|
||||
"openRouterRouting": {
|
||||
"sort": {
|
||||
"by": "price",
|
||||
"partition": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "compat-sort-invalid-union-boundary",
|
||||
"input": {
|
||||
"compat": {
|
||||
"openRouterRouting": {
|
||||
"sort": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "headers-record-valid",
|
||||
"input": {
|
||||
"headers": {
|
||||
"Authorization": "Bearer literal",
|
||||
"x-count": "2"
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "headers-record-invalid-value",
|
||||
"input": {
|
||||
"headers": {
|
||||
"x-count": 2
|
||||
}
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "model-override-fractional-and-extra",
|
||||
"input": {
|
||||
"modelOverrides": {
|
||||
"model/a": {
|
||||
"contextWindow": 10.5,
|
||||
"maxTokens": 2.25,
|
||||
"futureOverrideField": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "input-union-invalid",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "input",
|
||||
"input": [
|
||||
"text",
|
||||
"audio"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"version": 1,
|
||||
"piCommit": "ab366ebe94cacd419d986be454f12b1b9913aaca",
|
||||
"engine": "pinned upstream TypeScript",
|
||||
"bundler": "esbuild@0.28.1",
|
||||
"upstreamEntry": "packages/coding-agent/src/core/resolve-config-value.ts",
|
||||
"platform": "linux",
|
||||
"cases": [
|
||||
{
|
||||
"id": "literal-value",
|
||||
"input": "literal-secret",
|
||||
"environment": {},
|
||||
"execution": {
|
||||
"status": "success",
|
||||
"entryFunction": "resolveConfigValueOrThrow"
|
||||
},
|
||||
"expected": "literal-secret"
|
||||
},
|
||||
{
|
||||
"id": "environment-template",
|
||||
"input": "prefix-${PI_ORACLE_VALUE}-suffix",
|
||||
"environment": {
|
||||
"PI_ORACLE_VALUE": "environment-secret"
|
||||
},
|
||||
"execution": {
|
||||
"status": "success",
|
||||
"entryFunction": "resolveConfigValueOrThrow"
|
||||
},
|
||||
"expected": "prefix-environment-secret-suffix"
|
||||
},
|
||||
{
|
||||
"id": "escaped-dollar-and-bang",
|
||||
"input": "$$literal-$!bang",
|
||||
"environment": {},
|
||||
"execution": {
|
||||
"status": "success",
|
||||
"entryFunction": "resolveConfigValueOrThrow"
|
||||
},
|
||||
"expected": "$literal-!bang"
|
||||
},
|
||||
{
|
||||
"id": "shell-command",
|
||||
"input": "!printf pi-command-value",
|
||||
"environment": {},
|
||||
"execution": {
|
||||
"status": "success",
|
||||
"entryFunction": "resolveConfigValueOrThrow"
|
||||
},
|
||||
"expected": "pi-command-value"
|
||||
},
|
||||
{
|
||||
"id": "missing-environment",
|
||||
"input": "${PI_ORACLE_MISSING}",
|
||||
"environment": {},
|
||||
"execution": {
|
||||
"status": "error",
|
||||
"entryFunction": "resolveConfigValueOrThrow"
|
||||
},
|
||||
"expectedError": "Failed to resolve oracle missing-environment from environment variable: PI_ORACLE_MISSING"
|
||||
}
|
||||
],
|
||||
"headerCase": {
|
||||
"id": "provider-header-materialization",
|
||||
"input": {
|
||||
"x-literal": "literal-header",
|
||||
"x-environment": "${PI_ORACLE_HEADER}",
|
||||
"x-command": "!printf pi-command-header"
|
||||
},
|
||||
"environment": {
|
||||
"PI_ORACLE_HEADER": "environment-header"
|
||||
},
|
||||
"execution": {
|
||||
"status": "success",
|
||||
"entryFunction": "resolveHeadersOrThrow"
|
||||
},
|
||||
"expected": {
|
||||
"x-literal": "literal-header",
|
||||
"x-environment": "environment-header",
|
||||
"x-command": "pi-command-header"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user