fix(pi): mirror pinned native runtime semantics

This commit is contained in:
SaladDay
2026-08-02 13:56:18 +00:00
parent 2f856b0ffb
commit 15c735f040
4 changed files with 435 additions and 89 deletions
+159 -12
View File
@@ -71,6 +71,18 @@ pub(crate) struct PiComposerReason {
pub json_pointer: String,
}
/// One configured header together with the source pointer that Pi resolves.
///
/// `headers` remains the pinned composer's flattened observable result, while
/// these entries retain the provider-vs-model boundary needed to reproduce
/// the later `ModelRuntime` merge on the wire.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PiComposedHeader {
pub name: String,
pub value: String,
pub json_pointer: String,
}
/// The lossless native result of pinned Pi composition.
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct PiComposedNativeModel {
@@ -86,6 +98,8 @@ pub(crate) struct PiComposedNativeModel {
pub context_window: Value,
pub max_tokens: Value,
pub headers: BTreeMap<String, String>,
pub provider_headers: Vec<PiComposedHeader>,
pub model_headers: Vec<PiComposedHeader>,
pub compat: Option<Value>,
pub api_key: Option<String>,
pub oauth: Option<Value>,
@@ -176,7 +190,11 @@ pub(super) fn compose_explicit_custom_catalog(
);
}
let provider_compat = provider_object.get("compat").cloned();
let provider_headers = string_map(provider_object.get("headers"));
let provider_header_entries = header_entries(provider_object.get("headers"), "/headers");
let provider_headers = provider_header_entries
.iter()
.map(|entry| (entry.name.clone(), entry.value.clone()))
.collect::<BTreeMap<_, _>>();
let provider_extra = unknown_fields(provider_object, PROVIDER_FIELDS);
let api_key = provider_object
.get("apiKey")
@@ -282,6 +300,8 @@ pub(super) fn compose_explicit_custom_catalog(
.cloned()
.unwrap_or_else(|| json!(16384)),
headers: BTreeMap::new(),
provider_headers: provider_header_entries.clone(),
model_headers: Vec::new(),
compat: merge_compat(provider_compat.clone(), definition.get("compat").cloned()),
api_key: api_key.clone(),
oauth: oauth.clone(),
@@ -301,22 +321,50 @@ pub(super) fn compose_explicit_custom_catalog(
// 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
let (definition_index, definition) = definitions
.iter()
.filter_map(Value::as_object)
.find(|definition| {
definition.get("id").and_then(Value::as_str) == Some(model.id.as_str())
.enumerate()
.find_map(|(index, definition)| {
definition
.as_object()
.filter(|definition| {
definition.get("id").and_then(Value::as_str) == Some(model.id.as_str())
})
.map(|definition| (index, definition))
})
.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();
// rawModelHeaders constructs one case-sensitive JavaScript object from
// override headers followed by the first matching model definition.
// Exact-name replacement keeps its insertion slot; differently-cased
// names remain distinct until ModelRuntime performs its later
// case-insensitive HTTP merge.
let mut model_headers = Vec::new();
if let Some(model_override) = model_override {
headers.extend(string_map(model_override.get("headers")));
overlay_header_entries(
&mut model_headers,
header_entries(
model_override.get("headers"),
&format!("/modelOverrides/{}/headers", escape_json_pointer(&model.id)),
),
);
}
overlay_header_entries(
&mut model_headers,
header_entries(
definition.get("headers"),
&format!("/models/{definition_index}/headers"),
),
);
let mut headers = provider_headers.clone();
for entry in &model_headers {
headers.insert(entry.name.clone(), entry.value.clone());
}
headers.extend(string_map(definition.get("headers")));
model.headers = headers;
model.model_headers = model_headers;
if let Some(model_override) = model_override {
if let Some(name) = model_override.get("name").and_then(Value::as_str) {
@@ -393,9 +441,13 @@ fn default_cost() -> Value {
}
fn merge_cost(base: &Value, overlay: &Map<String, Value>) -> Value {
let mut merged = base.as_object().cloned().unwrap_or_default();
let base = base.as_object();
let mut merged = Map::new();
for key in ["input", "output", "cacheRead", "cacheWrite", "tiers"] {
if let Some(value) = overlay.get(key) {
if let Some(value) = overlay
.get(key)
.or_else(|| base.and_then(|base| base.get(key)))
{
merged.insert(key.to_string(), value.clone());
}
}
@@ -435,15 +487,38 @@ fn merge_compat(base: Option<Value>, overlay: Option<Value>) -> Option<Value> {
Some(Value::Object(merged))
}
fn string_map(value: Option<&Value>) -> BTreeMap<String, String> {
fn header_entries(value: Option<&Value>, base_pointer: &str) -> Vec<PiComposedHeader> {
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())))
.filter_map(|(name, value)| {
value.as_str().map(|value| PiComposedHeader {
name: name.clone(),
value: value.to_string(),
json_pointer: format!("{base_pointer}/{}", escape_json_pointer(name)),
})
})
.collect()
}
fn overlay_header_entries(
base: &mut Vec<PiComposedHeader>,
overlay: impl IntoIterator<Item = PiComposedHeader>,
) {
for entry in overlay {
if let Some(existing) = base.iter_mut().find(|existing| existing.name == entry.name) {
*existing = entry;
} else {
base.push(entry);
}
}
}
fn escape_json_pointer(segment: &str) -> String {
segment.replace('~', "~0").replace('/', "~1")
}
fn unknown_fields(object: &Map<String, Value>, recognized: &[&str]) -> BTreeMap<String, Value> {
object
.iter()
@@ -704,6 +779,78 @@ mod tests {
assert_eq!(composed.models[0].headers["x-tenant"], "${TENANT}");
}
#[test]
fn pinned_cost_override_reconstructs_only_known_cost_members() {
let value = json!({
"api": "anthropic-messages",
"baseUrl": "https://cost.example",
"apiKey": "literal",
"models": [{
"id": "m",
"cost": {
"input": 1,
"output": 2,
"cacheRead": 0.1,
"cacheWrite": 0.2,
"futureRate": 9
}
}],
"modelOverrides": {
"m": {"cost": {"output": 3}}
}
});
let raw = evaluate_provider_value(&value);
let composed = compose_explicit_custom_catalog(
"cost-shape",
raw.valid_provider.as_ref().expect("raw-valid"),
);
assert_eq!(
composed.models[0].cost,
json!({
"input": 1,
"output": 3,
"cacheRead": 0.1,
"cacheWrite": 0.2
}),
"pinned applyModelOverride drops unknown base cost keys when an override exists"
);
}
#[test]
fn header_layers_retain_runtime_precedence_and_source_pointers() {
let value = json!({
"api": "anthropic-messages",
"baseUrl": "https://headers.example",
"apiKey": "literal",
"headers": {"authorization": "Bearer provider"},
"models": [{
"id": "m",
"headers": {"Authorization": "Bearer model"}
}],
"modelOverrides": {
"m": {"headers": {"x-layer": "override"}}
}
});
let raw = evaluate_provider_value(&value);
let composed = compose_explicit_custom_catalog(
"header-layers",
raw.valid_provider.as_ref().expect("raw-valid"),
);
let model = &composed.models[0];
assert_eq!(
model.provider_headers[0].json_pointer,
"/headers/authorization"
);
assert_eq!(
model
.model_headers
.iter()
.map(|entry| (entry.name.as_str(), entry.value.as_str()))
.collect::<Vec<_>>(),
vec![("x-layer", "override"), ("Authorization", "Bearer model")]
);
}
#[test]
fn unknown_provider_model_and_override_fields_are_retained_losslessly() {
let value = json!({
+179 -62
View File
@@ -6,7 +6,9 @@
#![allow(dead_code)]
use super::composer::{PiComposedNativeModel, PiComposerStatus, PiNativeComposition};
use super::composer::{
PiComposedHeader, PiComposedNativeModel, PiComposerStatus, PiNativeComposition,
};
use http::{HeaderMap, HeaderName, HeaderValue};
use url::Url;
@@ -177,11 +179,19 @@ pub(super) struct CandidateHeaderPlan {
endpoint: Url,
credential: DeferredHeaderValue,
auth_header: bool,
custom_headers: Vec<(HeaderName, String, DeferredHeaderValue)>,
protocol_headers: Vec<(HeaderName, String, DeferredHeaderValue)>,
provider_headers: Vec<PlannedHeader>,
model_headers: Vec<PlannedHeader>,
protocol_identity_predictable: bool,
}
#[derive(Debug, Clone)]
struct PlannedHeader {
name: HeaderName,
json_pointer: String,
value: DeferredHeaderValue,
class: ConfiguredHeaderClass,
}
#[derive(Debug, Clone)]
pub(super) struct MaterializedCandidate {
pub endpoint: Url,
@@ -239,48 +249,17 @@ impl CandidateHeaderPlan {
});
};
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;
};
let class = configured_header_class(&parsed_name);
if class == ConfiguredHeaderClass::GatewayOwned {
reasons.push(PiGatewayReason {
code: PiGatewayReasonCode::ProtectedHeader,
json_pointer: pointer,
});
continue;
}
if !is_deferred(value) && parse_transport_header_value(value).is_none() {
reasons.push(PiGatewayReason {
code: PiGatewayReasonCode::InvalidHeaderValue,
json_pointer: pointer,
});
continue;
}
let planned = (
parsed_name,
pointer,
DeferredHeaderValue::new(value.clone()),
);
if class == ConfiguredHeaderClass::Protocol {
protocol_identity_predictable &= !value.starts_with('!');
protocol_headers.push(planned);
} else {
custom_headers.push(planned);
}
}
let provider_headers = plan_configured_headers(
&model.provider_headers,
&mut reasons,
&mut protocol_identity_predictable,
);
let model_headers = plan_configured_headers(
&model.model_headers,
&mut reasons,
&mut protocol_identity_predictable,
);
if !reasons.is_empty() {
return Err(reasons);
}
@@ -298,8 +277,8 @@ impl CandidateHeaderPlan {
endpoint,
credential: DeferredHeaderValue::new(credential),
auth_header: model.auth_header,
custom_headers,
protocol_headers,
provider_headers,
model_headers,
protocol_identity_predictable,
})
}
@@ -337,20 +316,27 @@ impl CandidateHeaderPlan {
}
};
headers.insert(auth_name, auth_value);
// Pinned Anthropic/OpenAI SDKs merge explicit configured headers after
// their synthesized family auth, so an explicit candidate-auth value
// wins when authHeader is disabled.
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)?;
// The pinned Anthropic SDK contributes this protocol default before
// configured headers. Identity must therefore compare the final value
// even when the user omitted it.
if self.family == PiGatewayApiFamily::AnthropicMessages {
let name = HeaderName::from_static("anthropic-version");
let value = HeaderValue::from_static("2023-06-01");
protocol_headers.insert(name.clone(), value.clone());
headers.insert(name.clone(), value);
headers.insert(name, value);
}
// Pi's provider composer applies authHeader after custom headers. It
// overwrites only Authorization and leaves family auth (for example
// x-api-key) at its already-materialized final value.
// Provider headers are part of provider auth resolution. Pinned SDKs
// merge them after synthesized family auth.
apply_planned_headers(
&self.provider_headers,
resolver,
&mut headers,
&mut protocol_headers,
)?;
// Provider-level authHeader is applied after provider headers.
if self.auth_header {
let credential = bearer_credential.to_str().map_err(|_| PiGatewayReason {
code: PiGatewayReasonCode::InvalidHeaderValue,
@@ -364,6 +350,17 @@ impl CandidateHeaderPlan {
})?;
headers.insert(HeaderName::from_static("authorization"), bearer);
}
// ModelRuntime then performs a case-insensitive model-header overlay.
// Keeping this as a separate phase is essential: flattening the two
// layers before authHeader can send a different credential than Pi.
apply_planned_headers(
&self.model_headers,
resolver,
&mut headers,
&mut protocol_headers,
)?;
let host = authority_header(&self.endpoint).ok_or_else(|| PiGatewayReason {
code: PiGatewayReasonCode::InvalidEndpoint,
json_pointer: "/baseUrl".to_string(),
@@ -379,6 +376,64 @@ impl CandidateHeaderPlan {
}
}
fn plan_configured_headers(
entries: &[PiComposedHeader],
reasons: &mut Vec<PiGatewayReason>,
protocol_identity_predictable: &mut bool,
) -> Vec<PlannedHeader> {
let mut planned = Vec::with_capacity(entries.len());
for entry in entries {
let Ok(name) = HeaderName::from_bytes(entry.name.as_bytes()) else {
reasons.push(PiGatewayReason {
code: PiGatewayReasonCode::InvalidHeaderName,
json_pointer: entry.json_pointer.clone(),
});
continue;
};
let class = configured_header_class(&name);
if class == ConfiguredHeaderClass::GatewayOwned {
reasons.push(PiGatewayReason {
code: PiGatewayReasonCode::ProtectedHeader,
json_pointer: entry.json_pointer.clone(),
});
continue;
}
if !is_deferred(&entry.value) && parse_transport_header_value(&entry.value).is_none() {
reasons.push(PiGatewayReason {
code: PiGatewayReasonCode::InvalidHeaderValue,
json_pointer: entry.json_pointer.clone(),
});
continue;
}
if class == ConfiguredHeaderClass::Protocol {
*protocol_identity_predictable &= !entry.value.starts_with('!');
}
planned.push(PlannedHeader {
name,
json_pointer: entry.json_pointer.clone(),
value: DeferredHeaderValue::new(entry.value.clone()),
class,
});
}
planned
}
fn apply_planned_headers(
planned: &[PlannedHeader],
resolver: &impl DeferredValueResolver,
headers: &mut HeaderMap,
protocol_headers: &mut HeaderMap,
) -> Result<(), PiGatewayReason> {
for entry in planned {
let value = entry.value.materialize(resolver, &entry.json_pointer)?;
if entry.class == ConfiguredHeaderClass::Protocol {
protocol_headers.insert(entry.name.clone(), value.clone());
}
headers.insert(entry.name.clone(), value);
}
Ok(())
}
impl MaterializedCandidate {
pub(super) fn failover_protocol_identity(&self) -> Option<(PiGatewayApiFamily, &HeaderMap)> {
// Auth, tenant and arbitrary custom headers are deliberately excluded.
@@ -438,10 +493,6 @@ fn parse_transport_header_value(value: &str) -> Option<HeaderValue> {
HeaderValue::from_str(value).ok()
}
fn escape_json_pointer(value: &str) -> String {
value.replace('~', "~0").replace('/', "~1")
}
#[cfg(test)]
mod tests {
use super::*;
@@ -766,6 +817,72 @@ mod tests {
);
}
#[test]
fn model_headers_overlay_provider_auth_case_insensitively() {
for (provider_name, model_name) in [
("authorization", "Authorization"),
("Authorization", "authorization"),
] {
let composition = composed(json!({
"api": "anthropic-messages",
"baseUrl": "https://candidate.example/v1",
"apiKey": "candidate-secret",
"authHeader": true,
"headers": {provider_name: "Bearer provider-token"},
"models": [{
"id": "m",
"headers": {model_name: "Bearer model-token"}
}]
}));
let materialized = assess_composition(&composition)
.plans
.remove(0)
.materialize(&|_expression: &str| None)
.expect("layered header materialization");
assert_eq!(
materialized.headers[&HeaderName::from_static("authorization")],
"Bearer model-token",
"pinned ModelRuntime applies model headers after provider authHeader"
);
}
}
#[test]
fn anthropic_protocol_identity_includes_the_sdk_default() {
let omitted = composed(json!({
"api": "anthropic-messages",
"baseUrl": "https://first.example/v1",
"apiKey": "first",
"models": [{"id": "m"}]
}));
let explicit = composed(json!({
"api": "anthropic-messages",
"baseUrl": "https://second.example/v1",
"apiKey": "second",
"headers": {"anthropic-version": "2023-06-01"},
"models": [{"id": "m"}]
}));
let omitted = assess_composition(&omitted)
.plans
.remove(0)
.materialize(&|_expression: &str| None)
.expect("omitted SDK default");
let explicit = assess_composition(&explicit)
.plans
.remove(0)
.materialize(&|_expression: &str| None)
.expect("explicit SDK default");
assert_eq!(
omitted.headers[&HeaderName::from_static("anthropic-version")],
"2023-06-01"
);
assert_eq!(
omitted.failover_protocol_identity(),
explicit.failover_protocol_identity(),
"wire-equivalent omitted and explicit SDK defaults must remain failover-compatible"
);
}
#[test]
fn four_families_materialize_their_own_auth_headers() {
for (family, auth_name, expected_value) in [
+54 -14
View File
@@ -7,13 +7,14 @@
#![allow(dead_code)]
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{BTreeMap, HashSet};
use thiserror::Error;
use url::Url;
pub(crate) type PiHeaderMap = BTreeMap<String, String>;
pub(crate) type PiHeaderMap = IndexMap<String, String>;
pub(crate) type PiThinkingLevelMap = BTreeMap<String, Value>;
/// Pi uses JavaScript/TypeBox `Number`, not `Integer`, for model limits.
@@ -183,7 +184,7 @@ pub(crate) struct PiManagedModelOverride {
pub context_window: Option<PiNumber>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<PiNumber>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub headers: PiHeaderMap,
#[serde(skip_serializing_if = "Option::is_none")]
pub compat: Option<Value>,
@@ -213,7 +214,7 @@ pub(crate) struct PiManagedModel {
pub context_window: Option<PiNumber>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<PiNumber>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub headers: PiHeaderMap,
#[serde(skip_serializing_if = "Option::is_none")]
pub compat: Option<Value>,
@@ -234,7 +235,7 @@ pub(crate) struct PiManagedProviderConfig {
/// executes env/command/file/network resolution.
#[serde(skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub headers: PiHeaderMap,
#[serde(skip_serializing_if = "Option::is_none")]
pub auth_header: Option<bool>,
@@ -266,6 +267,11 @@ pub(crate) struct PiEffectiveModel {
pub context_window: PiNumber,
pub max_tokens: PiNumber,
pub headers: PiHeaderMap,
/// Provider auth headers and the later model overlay are kept distinct so
/// a gateway can reproduce pinned Pi's case-insensitive runtime merge
/// without changing the serialized effective projection.
#[serde(skip)]
pub header_layers: PiEffectiveHeaderLayers,
#[serde(skip_serializing_if = "Option::is_none")]
pub compat: Option<Value>,
pub provider_extra: BTreeMap<String, Value>,
@@ -273,6 +279,12 @@ pub(crate) struct PiEffectiveModel {
pub override_extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct PiEffectiveHeaderLayers {
pub provider: PiHeaderMap,
pub model: PiHeaderMap,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub(crate) enum PiConfigError {
#[error("Pi provider must declare at least one managed model")]
@@ -453,7 +465,7 @@ pub(crate) fn validate_pi_managed_provider(
let mut model_ids = HashSet::with_capacity(provider.models.len());
for (index, model) in provider.models.iter().enumerate() {
if model.id.trim().is_empty() {
if model.id.is_empty() {
return Err(PiConfigError::EmptyModelId);
}
if !model_ids.insert(model.id.as_str()) {
@@ -519,11 +531,13 @@ fn effective_pi_model_unchecked(
model_id: model.id.clone(),
})?;
let model_override = provider.model_overrides.get(&model.id);
let mut headers = provider.headers.clone();
let mut model_headers = PiHeaderMap::new();
if let Some(model_override) = model_override {
headers.extend(model_override.headers.clone());
model_headers.extend(model_override.headers.clone());
}
headers.extend(model.headers.clone());
model_headers.extend(model.headers.clone());
let mut headers = provider.headers.clone();
headers.extend(model_headers.clone());
let thinking_level_map = merge_thinking_level_maps(
model.thinking_level_map.as_ref(),
@@ -570,6 +584,10 @@ fn effective_pi_model_unchecked(
.or(model.max_tokens)
.unwrap_or(PiNumber::DEFAULT_MAX_TOKENS),
headers,
header_layers: PiEffectiveHeaderLayers {
provider: provider.headers.clone(),
model: model_headers,
},
compat,
provider_extra: provider.extra.clone(),
model_extra: model.extra.clone(),
@@ -634,7 +652,7 @@ fn merge_compat(base: Option<Value>, overlay: Option<Value>) -> Option<Value> {
}
fn validate_optional_text(value: Option<&str>, field: &'static str) -> Result<(), PiConfigError> {
if value.is_some_and(|value| value.trim().is_empty()) {
if value.is_some_and(str::is_empty) {
return Err(PiConfigError::EmptyOptionalField { field });
}
Ok(())
@@ -744,7 +762,7 @@ mod tests {
cost: None,
context_window: None,
max_tokens: None,
headers: BTreeMap::new(),
headers: PiHeaderMap::new(),
compat: None,
extra: BTreeMap::new(),
}
@@ -756,7 +774,7 @@ mod tests {
base_url: Some("https://example.com/api".into()),
api: Some(api("anthropic-messages")),
api_key: Some("$PI_KEY".into()),
headers: BTreeMap::new(),
headers: PiHeaderMap::new(),
auth_header: None,
models,
model_overrides: BTreeMap::new(),
@@ -800,11 +818,31 @@ mod tests {
assert_eq!(effective.base_url, "https://model.example/v2");
}
#[test]
fn schema_valid_whitespace_strings_remain_manageable() {
let config: PiManagedProviderConfig = serde_json::from_value(json!({
"name": " ",
"api": "anthropic-messages",
"baseUrl": "https://example.com",
"apiKey": " ",
"models": [{"id": " ", "name": " "}]
}))
.expect("pinned schema-valid whitespace fields");
validate_pi_managed_provider(&config)
.expect("managed validation must use pinned minLength semantics without trimming");
assert_eq!(
effective_pi_model(&config, " ")
.expect("whitespace model id")
.id,
" "
);
}
#[test]
fn override_precedence_and_nested_compat_are_stable() {
let mut base_model = model("m");
base_model.reasoning = Some(false);
base_model.headers = BTreeMap::from([
base_model.headers = PiHeaderMap::from([
("layer".into(), "model".into()),
("model".into(), "yes".into()),
]);
@@ -813,7 +851,7 @@ mod tests {
"openRouterRouting": {"only": ["model"], "zdr": true}
}));
let mut config = provider(vec![base_model]);
config.headers = BTreeMap::from([
config.headers = PiHeaderMap::from([
("layer".into(), "provider".into()),
("provider".into(), "yes".into()),
]);
@@ -822,7 +860,7 @@ mod tests {
"m".into(),
PiManagedModelOverride {
reasoning: Some(true),
headers: BTreeMap::from([
headers: PiHeaderMap::from([
("layer".into(), "override".into()),
("override".into(), "yes".into()),
]),
@@ -837,6 +875,8 @@ mod tests {
let effective = effective_pi_model(&config, "m").expect("effective");
assert!(effective.reasoning);
assert_eq!(effective.headers["layer"], "model");
assert_eq!(effective.header_layers.provider["layer"], "provider");
assert_eq!(effective.header_layers.model["layer"], "model");
assert_eq!(
effective.compat,
Some(json!({
+43 -1
View File
@@ -364,7 +364,7 @@ fn collect_managed_reasons(config: &PiManagedProviderConfig) -> Vec<PiDiagnostic
}
for (index, model) in config.models.iter().enumerate() {
let pointer = format!("/models/{index}");
if model.id.trim().is_empty() {
if model.id.is_empty() {
add_reason(
&mut reasons,
diagnostic_reason(
@@ -860,6 +860,48 @@ mod tests {
);
}
#[test]
fn public_inspection_accepts_schema_valid_whitespace_strings() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("models.json");
fs::write(
&path,
r#"{"providers":{"whitespace":{
"name":" ",
"api":"anthropic-messages",
"baseUrl":"https://whitespace.example",
"apiKey":" ",
"models":[{"id":" ","name":" "}]
}}}"#,
)
.expect("write");
let inspection =
PiNativeInspectionService::inspect_entry(&path, "whitespace", &BTreeMap::new())
.expect("inspect")
.expect("entry");
assert_eq!(
inspection.diagnostic.raw_validity,
PiRawNativeValidity::Valid
);
assert_eq!(
inspection.diagnostic.managed_assessment,
PiManagedAssessment::Manageable
);
assert_eq!(
inspection.diagnostic.management_status,
PiManagementStatus::Importable
);
assert_eq!(
inspection
.managed_config
.as_ref()
.expect("managed config")
.models[0]
.id,
" "
);
}
#[test]
fn exact_entry_fingerprint_changes_only_when_that_raw_entry_changes() {
let temp = tempfile::tempdir().expect("tempdir");