fix(pi): certify pinned native inspection semantics

This commit is contained in:
SaladDay
2026-08-02 15:24:48 +00:00
parent 15c735f040
commit 2a0a8125eb
12 changed files with 942 additions and 120 deletions
+24
View File
@@ -50,6 +50,30 @@
> 文档考据说明:修正案 1/2(`pi-support-contracts-amendment-*.md`)的条款已按其自身要求**合并**进 `pi-support-contracts-zh.md` 与 `pi-support-review-contract-zh.md`,独立文件已随合并删除,这是预期状态而非丢失;本文引用的"修正案 2 §F/E1"以合并后的规范文档对应章节为准。 > 文档考据说明:修正案 1/2(`pi-support-contracts-amendment-*.md`)的条款已按其自身要求**合并**进 `pi-support-contracts-zh.md` 与 `pi-support-review-contract-zh.md`,独立文件已随合并删除,这是预期状态而非丢失;本文引用的"修正案 2 §F/E1"以合并后的规范文档对应章节为准。
## 4.7 协议放宽:契约纲领化(用户裁决,2026-08-02,优先于 §2、§4)
自本条起,**所有后续契约以纲领为主**:只给目标与验收标准,实现方(编码 Codex)
获得相当大的自由度。§2"测试先行 + 冻结认证套件"的重仪式**不再默认适用**。
**保留的最小规则**(全部有实证代价支撑,不是仪式):
1. **既有测试不得弱化**:测试总数只增不减,盲审核对;
2. **冻结物不动**:pinned 夹具、已重冻的 infra 三文件、restore 面;
3. **行为主张须有证据**:凡断言"pinned Pi 如此行为",须有 oracle 或
request-capture 实证(`scripts/pi-transport-capture.mjs`)。此条是四轮
返工换来的——读源码推断先后写反过 Google header-only 契约与 authHeader 次序;
4. **不 push、不建 PR、不动 `stash@{0}`、不碰 PR #5598**
**取消的**:精确红绿账本与"偏离即硬停止";每工程 2 轮 fresh 双审的固定配额;
实现路径、DTO 形状、测试组织方式的规定;裁决方逐条批准。实现方自行判断何时
完成,自行组织验证与审查,完成后报终态 SHA。
**为什么放宽**:前置 A/B/C 的证据表明,重仪式的边际收益在下降而成本在上升——
后期盲审 finding 多为"防作弊机器"的自我军备竞赛,且契约越长裁决方自身出错越多
(表名、文件映射、虚假覆盖声称、与上游相反的契约,均为契约复杂化后的产物)。
实现方在四个工程中零作弊记录(精确硬停、拒改认证文件、顶住 reviewer 的错误建议),
信任已由行为挣得。
## 4.6 前置 B 终止关账(用户裁决,2026-08-02,优先于 §4.5) ## 4.6 前置 B 终止关账(用户裁决,2026-08-02,优先于 §4.5)
§4.5 的缩围执行后,重认证 R1 仍在**存量产品面**出现 3 项 High(配置目录可变 §4.5 的缩围执行后,重认证 R1 仍在**存量产品面**出现 3 项 High(配置目录可变
+259
View File
@@ -0,0 +1,259 @@
#!/usr/bin/env node
/**
* Pi transport request-capture harness.
*
* 现有的 native-oracle 只执行 pinned Pi 的 schema evaluator / composer /
* config-value resolver,**不执行** adapter 与厂商 SDK 的头合并,因此
* "Pi 实际发出什么认证头" 一直只能靠读源码推断。本脚本补上这一层:
* 起一个本地 HTTP 抓包端点当 baseUrl,用 pinned Pi 的 adapter 真发一次
* 请求,记录实际发出的 header。
*
* 用法:
* PI_CHECKOUT=/path/to/pinned/pi node scripts/pi-transport-capture.mjs
*
* 不含任何密钥:测试用的 apiKey 是本地抓包用的假值;若要打真实端点,
* 通过环境变量传入(PI_CAPTURE_BASE_URL / PI_CAPTURE_API_KEY),不要写进文件。
*
* 输出为 JSON,可作为 transport 断言的出处依据。若要升级为受冻结的
* oracle 夹具,请比照 scripts/generate-pi-native-oracle.mjs 补 provenance
* (pinned commit、源码哈希、bundler 版本)。
*/
import { createServer } from "node:http";
import { execFileSync } from "node:child_process";
import { createRequire } from "node:module";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
const PI = process.env.PI_CHECKOUT;
const EXPECTED_PI_COMMIT = "ab366ebe94cacd419d986be454f12b1b9913aaca";
if (!PI) {
console.error(
"PI_CHECKOUT must point at a pinned Pi checkout (with node_modules).",
);
process.exit(2);
}
const piCommit = execFileSync("git", ["-C", PI, "rev-parse", "HEAD"], {
encoding: "utf8",
}).trim();
if (piCommit !== EXPECTED_PI_COMMIT) {
throw new Error(
`Pi checkout pin mismatch: expected ${EXPECTED_PI_COMMIT}, got ${piCommit}`,
);
}
const requireFromPi = createRequire(join(PI, "package.json"));
const { buildSync, version: esbuildVersion } = requireFromPi("esbuild");
const ANTHROPIC_SSE =
'event: message_start\ndata: {"type":"message_start","message":{"id":"m","type":"message","role":"assistant","model":"m","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"output_tokens":1}}}\n\n' +
'event: message_delta\ndata: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":1}}\n\n' +
'event: message_stop\ndata: {"type":"message_stop"}\n\n';
const OPENAI_SSE =
'data: {"type":"response.completed","response":{"id":"r","status":"completed","output":[],"usage":{"input_tokens":1,"output_tokens":1}}}\n\n' +
"data: [DONE]\n\n";
const captured = [];
const server = createServer((request, response) => {
const chunks = [];
request.on("data", (chunk) => chunks.push(chunk));
request.on("end", () => {
captured.push({ url: request.url, headers: { ...request.headers } });
response.writeHead(200, { "content-type": "text/event-stream" });
response.end(request.url.includes("messages") ? ANTHROPIC_SSE : OPENAI_SSE);
});
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const baseUrl =
process.env.PI_CAPTURE_BASE_URL ??
`http://127.0.0.1:${server.address().port}`;
const harnessDirectory = mkdtempSync(join(PI, ".cc-switch-transport-capture-"));
process.on("exit", () =>
rmSync(harnessDirectory, { recursive: true, force: true }),
);
const aiShimPath = join(harnessDirectory, "pi-ai-shim.mjs");
const compatShimPath = join(harnessDirectory, "pi-ai-compat-shim.mjs");
writeFileSync(
aiShimPath,
'export function lazyStream() { throw new Error("transport shim must not execute during compat capture"); }\n',
);
writeFileSync(
compatShimPath,
'export function getApiProvider() { throw new Error("transport shim must not execute during compat capture"); }\n',
);
const entryPoint = join(harnessDirectory, "entry.mjs");
writeFileSync(
entryPoint,
[
`export { streamSimple as anthropicMessages } from "${PI}/packages/ai/src/api/anthropic-messages.ts";`,
`export { streamSimple as openaiResponses } from "${PI}/packages/ai/src/api/openai-responses.ts";`,
`export { streamSimple as openaiCompletions } from "${PI}/packages/ai/src/api/openai-completions.ts";`,
`export { composeModelProvider } from "${PI}/packages/coding-agent/src/core/provider-composer.ts";`,
].join("\n"),
);
const bundlePath = join(harnessDirectory, "bundle.mjs");
buildSync({
entryPoints: [entryPoint],
bundle: true,
platform: "node",
format: "esm",
outfile: bundlePath,
external: ["node:*"],
packages: "external",
alias: {
"@earendil-works/pi-ai": aiShimPath,
"@earendil-works/pi-ai/compat": compatShimPath,
},
logLevel: "silent",
});
const adapters = await import(pathToFileURL(bundlePath).href);
const API_BY_ADAPTER = {
anthropicMessages: "anthropic-messages",
openaiResponses: "openai-responses",
openaiCompletions: "openai-completions",
};
/** 每个用例只改变凭证与显式 header,其余保持最小合法模型。 */
const CASES = [
["anthropicMessages", "plain-key", "sk-ant-api03-plain", {}],
["anthropicMessages", "oauth-token", "sk-ant-oat01-token", {}],
[
"anthropicMessages",
"explicit-x-api-key",
"synthesized-secret",
{ "x-api-key": "explicit-secret" },
],
[
"anthropicMessages",
"explicit-authorization",
"synthesized-secret",
{ authorization: "Bearer configured" },
],
["openaiResponses", "plain-key", "sk-plain", {}],
["openaiResponses", "oauth-shaped-token", "sk-ant-oat01-not-anthropic", {}],
[
"openaiResponses",
"explicit-authorization",
"synthesized-secret",
{ authorization: "Bearer configured" },
],
[
"openaiCompletions",
"explicit-authorization",
"synthesized-secret",
{ authorization: "Bearer configured" },
],
[
"openaiCompletions",
"explicit-x-api-key",
"synthesized-secret",
{ "x-api-key": "explicit-secret" },
],
];
const results = [];
for (const [adapter, label, apiKey, headers] of CASES) {
const model = {
id: "m",
name: "m",
api: API_BY_ADAPTER[adapter],
provider: "candidate",
baseUrl,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1000,
maxTokens: 100,
};
const before = captured.length;
let error;
try {
const stream = adapters[adapter](
model,
{ messages: [{ role: "user", content: "hi" }] },
{
apiKey: process.env.PI_CAPTURE_API_KEY ?? apiKey,
headers,
maxTokens: 16,
},
);
for await (const _event of stream) {
// drain
}
} catch (caught) {
error = String(caught);
}
const request =
captured.length > before ? captured[captured.length - 1] : undefined;
results.push({
adapter: API_BY_ADAPTER[adapter],
case: label,
requestSent: Boolean(request),
error: request ? undefined : error,
authHeaders: request
? Object.fromEntries(
Object.entries(request.headers).filter(([name]) =>
[
"authorization",
"x-api-key",
"x-goog-api-key",
"anthropic-beta",
"anthropic-version",
"openai-beta",
].includes(name),
),
)
: undefined,
});
}
const compatInput = {
api: "openai-responses",
baseUrl: "https://compat.example/v1",
apiKey: "literal",
compat: {
openRouterRouting: ["first", "second"],
chatTemplateKwargs: "ab",
baseOnly: true,
},
models: [{ id: "m", compat: { supportsStore: true } }],
modelOverrides: {
m: {
compat: {
openRouterRouting: null,
chatTemplateKwargs: { named: true },
overlayOnly: true,
},
},
},
};
const compatProvider = adapters.composeModelProvider(
"compat-spread",
undefined,
{
getProvider(providerId) {
return providerId === "compat-spread" ? compatInput : undefined;
},
},
undefined,
);
const compatSpread = compatProvider.getModels()[0].compat;
server.close();
console.log(
JSON.stringify(
{
bundler: `esbuild@${esbuildVersion}`,
piCheckout: PI,
piCommit,
baseUrl,
results,
compatSpread,
},
null,
2,
),
);
+1 -1
View File
@@ -23,7 +23,7 @@ test-hooks = []
tauri-build = { version = "2.4.0", features = [] } tauri-build = { version = "2.4.0", features = [] }
[dependencies] [dependencies]
serde_json = { version = "1.0", features = ["preserve_order"] } serde_json = { version = "1.0", features = ["arbitrary_precision", "preserve_order"] }
jsonc-parser = { version = "0.33", features = ["cst", "serde_json"] } jsonc-parser = { version = "0.33", features = ["cst", "serde_json"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
log = "0.4" log = "0.4"
+56 -34
View File
@@ -148,22 +148,18 @@ struct ArchitectureVisitor<'a> {
internal_edges: BTreeSet<String>, internal_edges: BTreeSet<String>,
} }
fn pi_config_source_module(path: &str) -> Option<&'static str> {
["raw_schema", "composer", "gateway", "native", "model"]
.into_iter()
.find(|module| {
let root = format!("pi_config/{module}");
path.ends_with(&format!("{root}.rs")) || path.contains(&format!("{root}/"))
})
}
impl ArchitectureVisitor<'_> { impl ArchitectureVisitor<'_> {
fn record_dependency(&mut self, segments: &[String]) { fn record_dependency(&mut self, segments: &[String]) {
let source_module = if self.path.ends_with("pi_config/raw_schema.rs") { let Some(source_module) = pi_config_source_module(self.path) else {
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; return;
}; };
let qualified_internal_path = segments.len() > 1 let qualified_internal_path = segments.len() > 1
@@ -398,6 +394,27 @@ fn rust_sources(root: &Path) -> Vec<PathBuf> {
output output
} }
fn scan_production_tree(
manifest_dir: &Path,
source_root: &Path,
) -> (Vec<Violation>, BTreeSet<String>) {
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('\\', "/");
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);
}
(violations, edges)
}
fn type_leaf(ty: &Type) -> String { fn type_leaf(ty: &Type) -> String {
match ty { match ty {
Type::Path(path) => path Type::Path(path) => path
@@ -480,26 +497,7 @@ fn provider_write_api_snapshot(source: &str) -> serde_json::Value {
fn architecture_scanner_accepts_production_tree_and_matches_machine_snapshots() { fn architecture_scanner_accepts_production_tree_and_matches_machine_snapshots() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let source_root = manifest_dir.join("src"); let source_root = manifest_dir.join("src");
let mut violations = Vec::new(); let (violations, edges) = scan_production_tree(manifest_dir, &source_root);
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!( assert!(
violations.is_empty(), violations.is_empty(),
"architecture violations:\n{}", "architecture violations:\n{}",
@@ -648,4 +646,28 @@ fn architecture_scanner_negative_fixtures_prove_each_guard_fires() {
violations.is_empty(), violations.is_empty(),
"a predicate requiring test must stay excluded: {violations:?}" "a predicate requiring test must stay excluded: {violations:?}"
); );
let temp = tempfile::tempdir().expect("temp architecture tree");
let nested = temp.path().join("src/pi_config/composer/tests/mod.rs");
fs::create_dir_all(nested.parent().expect("nested parent")).expect("create nested tree");
fs::write(
&nested,
r#"
use super::super::gateway::PiGatewayApiFamily;
fn production_escape_attempt() {
save_provider();
}
"#,
)
.expect("write production nested-module fixture");
let (violations, _) = scan_production_tree(temp.path(), &temp.path().join("src"));
for expected_kind in ["cross_layer_import", "forbidden_provider_symbol"] {
assert!(
violations
.iter()
.any(|violation| violation.kind == expected_kind),
"production code under tests/ must inherit composer ownership and trigger \
{expected_kind}: {violations:?}"
);
}
} }
+50 -36
View File
@@ -6,7 +6,10 @@
#![allow(dead_code)] #![allow(dead_code)]
use super::raw_schema::{PiRawApiId, PiRawValidProvider}; use super::{
merge_pi_compat,
raw_schema::{PiRawApiId, PiRawValidProvider},
};
use serde_json::{json, Map, Value}; use serde_json::{json, Map, Value};
use std::collections::{BTreeMap, HashSet}; use std::collections::{BTreeMap, HashSet};
@@ -302,7 +305,7 @@ pub(super) fn compose_explicit_custom_catalog(
headers: BTreeMap::new(), headers: BTreeMap::new(),
provider_headers: provider_header_entries.clone(), provider_headers: provider_header_entries.clone(),
model_headers: Vec::new(), model_headers: Vec::new(),
compat: merge_compat(provider_compat.clone(), definition.get("compat").cloned()), compat: merge_pi_compat(provider_compat.clone(), definition.get("compat").cloned()),
api_key: api_key.clone(), api_key: api_key.clone(),
oauth: oauth.clone(), oauth: oauth.clone(),
auth_header, auth_header,
@@ -398,7 +401,7 @@ pub(super) fn compose_explicit_custom_catalog(
model.max_tokens = max_tokens.clone(); model.max_tokens = max_tokens.clone();
} }
model.compat = model.compat =
merge_compat(model.compat.clone(), model_override.get("compat").cloned()); merge_pi_compat(model.compat.clone(), model_override.get("compat").cloned());
model.override_extra = unknown_fields(model_override, OVERRIDE_FIELDS); model.override_extra = unknown_fields(model_override, OVERRIDE_FIELDS);
} }
} }
@@ -454,39 +457,6 @@ fn merge_cost(base: &Value, overlay: &Map<String, Value>) -> Value {
Value::Object(merged) 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 header_entries(value: Option<&Value>, base_pointer: &str) -> Vec<PiComposedHeader> { fn header_entries(value: Option<&Value>, base_pointer: &str) -> Vec<PiComposedHeader> {
value value
.and_then(Value::as_object) .and_then(Value::as_object)
@@ -816,6 +786,50 @@ mod tests {
); );
} }
#[test]
fn compat_spread_matches_pinned_composer_request_capture() {
let value = json!({
"api": "openai-responses",
"baseUrl": "https://compat.example/v1",
"apiKey": "literal",
"compat": {
"openRouterRouting": ["first", "second"],
"chatTemplateKwargs": "ab",
"baseOnly": true
},
"models": [{
"id": "m",
"compat": {"supportsStore": true}
}],
"modelOverrides": {
"m": {
"compat": {
"openRouterRouting": null,
"chatTemplateKwargs": {"named": true},
"overlayOnly": true
}
}
}
});
let raw = evaluate_provider_value(&value);
let composed = compose_explicit_custom_catalog(
"compat-spread",
raw.valid_provider.as_ref().expect("raw-valid"),
);
assert_eq!(
composed.models[0].compat,
Some(json!({
"openRouterRouting": {"0": "first", "1": "second"},
"chatTemplateKwargs": {"0": "a", "1": "b", "named": true},
"baseOnly": true,
"supportsStore": true,
"overlayOnly": true
})),
"captured by scripts/pi-transport-capture.mjs at the pinned Pi commit"
);
}
#[test] #[test]
fn header_layers_retain_runtime_precedence_and_source_pointers() { fn header_layers_retain_runtime_precedence_and_source_pointers() {
let value = json!({ let value = json!({
+23
View File
@@ -288,6 +288,29 @@ mod tests {
assert!(entry.raw_source.contains("\"model//literal\"")); assert!(entry.raw_source.contains("\"model//literal\""));
} }
#[test]
fn parses_javascript_overflow_without_hiding_sibling_entries() {
let document = parse_models_source(
Path::new("models.json"),
r#"{
"providers": {
"healthy": {"models": [{"id": "healthy"}]},
"overflow": {"models": [{"id": "m", "contextWindow": 1e400}]}
}
}"#,
)
.expect("JSON.parse accepts the numeric token before TypeBox validation");
assert!(document.providers().contains_key("healthy"));
let overflow = &document.providers()["overflow"].value["models"][0]["contextWindow"];
assert!(
overflow
.as_number()
.is_some_and(|number| number.as_f64().is_none()),
"the raw evaluator must still be able to distinguish non-finite JavaScript Number"
);
}
#[test] #[test]
fn rejects_json_extensions_that_pinned_pi_rejects() { fn rejects_json_extensions_that_pinned_pi_rejects() {
let temp = tempfile::tempdir().expect("tempdir"); let temp = tempfile::tempdir().expect("tempdir");
+32 -2
View File
@@ -122,6 +122,7 @@ pub(super) enum PiGatewayCapability {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum PiGatewayReasonCode { pub(super) enum PiGatewayReasonCode {
UnsupportedFamily, UnsupportedFamily,
UnsupportedCredentialKind,
InvalidEndpoint, InvalidEndpoint,
MissingCredential, MissingCredential,
InvalidHeaderName, InvalidHeaderName,
@@ -242,11 +243,27 @@ impl CandidateHeaderPlan {
} }
}; };
let credential = model.api_key.as_ref(); let credential = model.api_key.as_ref();
if credential.is_none() { match credential {
reasons.push(PiGatewayReason { None => reasons.push(PiGatewayReason {
code: PiGatewayReasonCode::MissingCredential, code: PiGatewayReasonCode::MissingCredential,
json_pointer: "/apiKey".to_string(), json_pointer: "/apiKey".to_string(),
}),
Some(credential) if !is_deferred(credential) => {
if parse_transport_header_value(credential).is_none() {
reasons.push(PiGatewayReason {
code: PiGatewayReasonCode::InvalidHeaderValue,
json_pointer: "/apiKey".to_string(),
}); });
} else if family == PiGatewayApiFamily::AnthropicMessages
&& is_anthropic_oauth_credential(credential)
{
reasons.push(PiGatewayReason {
code: PiGatewayReasonCode::UnsupportedCredentialKind,
json_pointer: "/apiKey".to_string(),
});
}
}
Some(_) => {}
}; };
let mut protocol_identity_predictable = true; let mut protocol_identity_predictable = true;
@@ -292,6 +309,14 @@ impl CandidateHeaderPlan {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
let mut protocol_headers = HeaderMap::new(); let mut protocol_headers = HeaderMap::new();
let credential = self.credential.materialize(resolver, "/apiKey")?; let credential = self.credential.materialize(resolver, "/apiKey")?;
if self.family == PiGatewayApiFamily::AnthropicMessages
&& credential.to_str().is_ok_and(is_anthropic_oauth_credential)
{
return Err(PiGatewayReason {
code: PiGatewayReasonCode::UnsupportedCredentialKind,
json_pointer: "/apiKey".to_string(),
});
}
let bearer_credential = credential.clone(); let bearer_credential = credential.clone();
let (auth_name, auth_value) = match self.family { let (auth_name, auth_value) = match self.family {
PiGatewayApiFamily::AnthropicMessages => { PiGatewayApiFamily::AnthropicMessages => {
@@ -486,6 +511,11 @@ fn is_deferred(value: &str) -> bool {
value.starts_with('!') || value.contains('$') value.starts_with('!') || value.contains('$')
} }
fn is_anthropic_oauth_credential(value: &str) -> bool {
// Pinned Pi's Anthropic adapter uses `includes`, not a prefix test.
value.contains("sk-ant-oat")
}
fn parse_transport_header_value(value: &str) -> Option<HeaderValue> { fn parse_transport_header_value(value: &str) -> Option<HeaderValue> {
if !value.bytes().all(|byte| matches!(byte, 0x20..=0x7e)) { if !value.bytes().all(|byte| matches!(byte, 0x20..=0x7e)) {
return None; return None;
+112
View File
@@ -4,6 +4,8 @@
//! Pi's shared files and from the proxy data plane. Callers must use the //! Pi's shared files and from the proxy data plane. Callers must use the
//! typed model resolver rather than reimplementing provider/model inheritance. //! typed model resolver rather than reimplementing provider/model inheritance.
use serde_json::{Map, Value};
mod composer; mod composer;
mod document; mod document;
mod gateway; mod gateway;
@@ -12,3 +14,113 @@ pub(crate) mod native;
#[cfg(test)] #[cfg(test)]
mod native_inspection_certification; mod native_inspection_certification;
mod raw_schema; mod raw_schema;
const PI_COMPAT_NESTED_SPREAD_KEYS: [&str; 3] = [
"openRouterRouting",
"vercelGatewayRouting",
"chatTemplateKwargs",
];
/// Mirror pinned Pi's `mergeCompat` JavaScript object-spread semantics.
///
/// Arrays expose numeric enumerable properties, strings expose character
/// properties, objects expose their own fields, and the remaining JSON
/// primitives expose none. Existing key positions are retained when an
/// overlay replaces their values, matching object spread.
fn merge_pi_compat(base: Option<Value>, overlay: Option<Value>) -> Option<Value> {
let Some(overlay) = overlay else {
return base;
};
if !javascript_truthy(&overlay) {
return base;
}
let mut merged = javascript_object_spread(base.as_ref());
merged.extend(javascript_object_spread(Some(&overlay)));
for key in PI_COMPAT_NESTED_SPREAD_KEYS {
let base_value = javascript_property(base.as_ref(), key);
let overlay_value = javascript_property(Some(&overlay), key);
if base_value.is_some_and(javascript_is_object)
|| overlay_value.is_some_and(javascript_is_object)
{
let mut nested = javascript_object_spread(base_value);
nested.extend(javascript_object_spread(overlay_value));
merged.insert(key.to_string(), Value::Object(nested));
}
}
Some(Value::Object(merged))
}
fn javascript_truthy(value: &Value) -> bool {
match value {
Value::Null | Value::Bool(false) => false,
Value::Number(value) => value.as_f64().is_none_or(|value| value != 0.0),
Value::String(value) => !value.is_empty(),
Value::Bool(true) | Value::Array(_) | Value::Object(_) => true,
}
}
fn javascript_is_object(value: &Value) -> bool {
matches!(value, Value::Array(_) | Value::Object(_))
}
fn javascript_property<'a>(value: Option<&'a Value>, key: &str) -> Option<&'a Value> {
value.and_then(Value::as_object)?.get(key)
}
fn javascript_object_spread(value: Option<&Value>) -> Map<String, Value> {
match value {
Some(Value::Object(object)) => object.clone(),
Some(Value::Array(values)) => values
.iter()
.enumerate()
.map(|(index, value)| (index.to_string(), value.clone()))
.collect(),
Some(Value::String(value)) => value
.chars()
.enumerate()
.map(|(index, value)| (index.to_string(), Value::String(value.to_string())))
.collect(),
Some(Value::Null | Value::Bool(_) | Value::Number(_)) | None => Map::new(),
}
}
#[cfg(test)]
mod compat_spread_tests {
use super::*;
use serde_json::json;
#[test]
fn compat_nested_values_follow_javascript_object_spread() {
let merged = merge_pi_compat(
Some(json!({
"openRouterRouting": ["first", "second"],
"chatTemplateKwargs": "ab",
"baseOnly": true
})),
Some(json!({
"openRouterRouting": null,
"chatTemplateKwargs": {"named": true},
"overlayOnly": true
})),
)
.expect("truthy overlay produces an object");
assert_eq!(
merged,
json!({
"openRouterRouting": {"0": "first", "1": "second"},
"chatTemplateKwargs": {"0": "a", "1": "b", "named": true},
"baseOnly": true,
"overlayOnly": true
})
);
}
#[test]
fn compat_falsy_overlay_returns_base_without_spreading() {
let base = Some(json!({"openRouterRouting": ["kept"]}));
assert_eq!(merge_pi_compat(base.clone(), Some(Value::Null)), base);
}
}
+106 -30
View File
@@ -7,6 +7,7 @@
#![allow(dead_code)] #![allow(dead_code)]
use super::merge_pi_compat;
use indexmap::IndexMap; use indexmap::IndexMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
@@ -144,8 +145,8 @@ pub(crate) struct PiModelCostTier {
pub(crate) struct PiModelCost { pub(crate) struct PiModelCost {
#[serde(flatten)] #[serde(flatten)]
pub rates: PiModelCostRates, pub rates: PiModelCostRates,
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(skip_serializing_if = "Option::is_none")]
pub tiers: Vec<PiModelCostTier>, pub tiers: Option<Vec<PiModelCostTier>>,
#[serde(flatten)] #[serde(flatten)]
pub extra: BTreeMap<String, Value>, pub extra: BTreeMap<String, Value>,
} }
@@ -404,6 +405,7 @@ pub(crate) enum PiReasonCode {
InvalidThinkingLevel, InvalidThinkingLevel,
CompositionFailed, CompositionFailed,
GatewayCredentialUnavailable, GatewayCredentialUnavailable,
UnsupportedCredentialKind,
UnsupportedGatewayFamily, UnsupportedGatewayFamily,
InvalidHeaderName, InvalidHeaderName,
InvalidHeaderValue, InvalidHeaderValue,
@@ -550,8 +552,8 @@ fn effective_pi_model_unchecked(
.map(|entry| apply_cost_override(base_cost.clone(), entry)) .map(|entry| apply_cost_override(base_cost.clone(), entry))
.unwrap_or(base_cost); .unwrap_or(base_cost);
let compat = merge_compat( let compat = merge_pi_compat(
merge_compat(provider.compat.clone(), model.compat.clone()), merge_pi_compat(provider.compat.clone(), model.compat.clone()),
model_override.and_then(|entry| entry.compat.clone()), model_override.and_then(|entry| entry.compat.clone()),
); );
@@ -620,37 +622,12 @@ fn apply_cost_override(mut base: PiModelCost, model_override: &PiModelCostOverri
cache_write: model_override.cache_write.unwrap_or(base.rates.cache_write), cache_write: model_override.cache_write.unwrap_or(base.rates.cache_write),
}; };
if let Some(tiers) = &model_override.tiers { if let Some(tiers) = &model_override.tiers {
base.tiers = tiers.clone(); base.tiers = Some(tiers.clone());
} }
base.extra.extend(model_override.extra.clone()); base.extra.extend(model_override.extra.clone());
base base
} }
fn merge_compat(base: Option<Value>, overlay: Option<Value>) -> Option<Value> {
match (base, overlay) {
(None, None) => None,
(value, None) | (None, value) => value,
(Some(Value::Object(mut base)), Some(Value::Object(overlay))) => {
for (key, value) in overlay {
if matches!(
key.as_str(),
"openRouterRouting" | "vercelGatewayRouting" | "chatTemplateKwargs"
) {
if let Some(Value::Object(base_nested)) = base.get_mut(&key) {
if let Value::Object(overlay_nested) = &value {
base_nested.extend(overlay_nested.clone());
continue;
}
}
}
base.insert(key, value);
}
Some(Value::Object(base))
}
(Some(_), Some(overlay)) => Some(overlay),
}
}
fn validate_optional_text(value: Option<&str>, field: &'static str) -> Result<(), PiConfigError> { fn validate_optional_text(value: Option<&str>, field: &'static str) -> Result<(), PiConfigError> {
if value.is_some_and(str::is_empty) { if value.is_some_and(str::is_empty) {
return Err(PiConfigError::EmptyOptionalField { field }); return Err(PiConfigError::EmptyOptionalField { field });
@@ -891,6 +868,46 @@ mod tests {
); );
} }
#[test]
fn effective_compat_uses_the_pinned_composer_spread_result() {
let config: PiManagedProviderConfig = serde_json::from_value(json!({
"api": "openai-responses",
"baseUrl": "https://compat.example/v1",
"compat": {
"openRouterRouting": ["first", "second"],
"chatTemplateKwargs": "ab",
"baseOnly": true
},
"models": [{
"id": "m",
"compat": {"supportsStore": true}
}],
"modelOverrides": {
"m": {
"compat": {
"openRouterRouting": null,
"chatTemplateKwargs": {"named": true},
"overlayOnly": true
}
}
}
}))
.expect("deserialize pinned compat vector");
assert_eq!(
effective_pi_model(&config, "m")
.expect("effective compat")
.compat,
Some(json!({
"openRouterRouting": {"0": "first", "1": "second"},
"chatTemplateKwargs": {"0": "a", "1": "b", "named": true},
"baseOnly": true,
"supportsStore": true,
"overlayOnly": true
}))
);
}
#[test] #[test]
fn fractional_pi_numbers_survive_managed_round_trip() { fn fractional_pi_numbers_survive_managed_round_trip() {
let mut fractional = model("fractional"); let mut fractional = model("fractional");
@@ -1001,6 +1018,51 @@ mod tests {
); );
} }
#[test]
fn empty_cost_tiers_remain_distinct_from_absent_at_managed_and_effective_boundaries() {
let config: PiManagedProviderConfig = serde_json::from_value(json!({
"api": "anthropic-messages",
"baseUrl": "https://cost.example",
"models": [
{
"id": "empty",
"cost": {
"input": 1.0,
"output": 2.0,
"cacheRead": 0.5,
"cacheWrite": 0.25,
"tiers": []
}
},
{
"id": "absent",
"cost": {
"input": 1.0,
"output": 2.0,
"cacheRead": 0.5,
"cacheWrite": 0.25
}
}
]
}))
.expect("deserialize explicit and absent tiers");
let managed = serde_json::to_value(&config).expect("serialize managed config");
assert_eq!(managed.pointer("/models/0/cost/tiers"), Some(&json!([])));
assert_eq!(managed.pointer("/models/1/cost/tiers"), None);
let empty_effective = serde_json::to_value(
effective_pi_model(&config, "empty").expect("effective empty tiers"),
)
.expect("serialize effective empty tiers");
let absent_effective = serde_json::to_value(
effective_pi_model(&config, "absent").expect("effective absent tiers"),
)
.expect("serialize effective absent tiers");
assert_eq!(empty_effective.pointer("/cost/tiers"), Some(&json!([])));
assert_eq!(absent_effective.pointer("/cost/tiers"), None);
}
#[test] #[test]
fn diagnostic_reason_serialization_is_structured() { fn diagnostic_reason_serialization_is_structured() {
let reason = PiDiagnosticReason::new( let reason = PiDiagnosticReason::new(
@@ -1016,5 +1078,19 @@ mod tests {
"jsonPointer": "/models/0/api" "jsonPointer": "/models/0/api"
}) })
); );
let credential_reason = PiDiagnosticReason::new(
PiDiagnosticLayer::Gateway,
PiReasonCode::UnsupportedCredentialKind,
Some("/apiKey".into()),
);
assert_eq!(
serde_json::to_value(credential_reason).expect("serialize"),
json!({
"layer": "gateway",
"code": "unsupported_credential_kind",
"jsonPointer": "/apiKey"
})
);
} }
} }
+3
View File
@@ -604,6 +604,9 @@ fn map_gateway_reasons(gateway: &PiGatewayAssessment) -> Vec<PiDiagnosticReason>
PiGatewayReasonCode::UnsupportedFamily => { PiGatewayReasonCode::UnsupportedFamily => {
PiReasonCode::UnsupportedGatewayFamily PiReasonCode::UnsupportedGatewayFamily
} }
PiGatewayReasonCode::UnsupportedCredentialKind => {
PiReasonCode::UnsupportedCredentialKind
}
PiGatewayReasonCode::InvalidEndpoint => PiReasonCode::InvalidEndpoint, PiGatewayReasonCode::InvalidEndpoint => PiReasonCode::InvalidEndpoint,
PiGatewayReasonCode::MissingCredential => { PiGatewayReasonCode::MissingCredential => {
PiReasonCode::GatewayCredentialUnavailable PiReasonCode::GatewayCredentialUnavailable
@@ -11,12 +11,13 @@
//! 盲审前置条件而非充分条件。既有测试不得弱化——以测试总数只增不减 + 盲审 //! 盲审前置条件而非充分条件。既有测试不得弱化——以测试总数只增不减 + 盲审
//! 核对为准,不设扫描器。 //! 核对为准,不设扫描器。
//! //!
//! ## 条裁决及其上游证据 //! ## 条裁决及其上游证据
//! C1【无损性】pinned schema 对 `thinkingLevelMap` 只约束 7 个标准键 //! C1【无损性】pinned schema 对 `thinkingLevelMap` 只约束 7 个标准键
//! (string|null;oracle 实证 `low: 2` 非法),额外键无约束(oracle 实证 //! (string|null;oracle 实证 `low: 2` 非法),额外键无约束(oracle 实证
//! `future: {nested:true}` 合法);`cost`/tier 同样接受未来键。managed 与 //! `future: {nested:true}` 合法);`cost`/tier 同样接受未来键。managed 与
//! **effective 边界**(`effective_pi_model` 是 projection/routing/failover //! **effective 边界**(`effective_pi_model` 是 projection/routing/failover
//! 的共同入口)都必须无损,`{}` 与缺席必须保持可区分 //! 的共同入口)都必须无损,**空容器与缺席必须保持可区分**(`{}` 之于
//! thinkingLevelMap、`[]` 之于 cost.tiers 同理)。
//! 据此取代两个既有测试中把收窄固化为断言的部分: //! 据此取代两个既有测试中把收窄固化为断言的部分:
//! `managed_narrowing_rejects_duplicates_or_unknown_thinking_keys` 与 //! `managed_narrowing_rejects_duplicates_or_unknown_thinking_keys` 与
//! `unknown_thinking_shape_is_lossless_for_composer_and_narrowed_separately` //! `unknown_thinking_shape_is_lossless_for_composer_and_narrowed_separately`
@@ -41,22 +42,64 @@
//! `!command` / 展开 `${ENV}`,再使用结果;**从不按 HTTP 头规则校验原始 //! `!command` / 展开 `${ENV}`,再使用结果;**从不按 HTTP 头规则校验原始
//! 表达式**(命令输出 trim,环境模板不 trim,解析结果亦不做头合法性校验)。 //! 表达式**(命令输出 trim,环境模板不 trim,解析结果亦不做头合法性校验)。
//! 因此原始表达式含头非法字符、而解析结果合法的配置必须被接受;头合法性 //! 因此原始表达式含头非法字符、而解析结果合法的配置必须被接受;头合法性
//! 校验只能发生在物化之后(这是网关自身的传输约束,保留)。字面量值仍 //! 校验只能发生在物化之后(这是网关自身的传输约束,保留)。**字面量值仍
//! 原样校验。 //! 判定期校验,且该规则对 credential 与 header 一视同仁**——判定期说
//! "可代理"而每次物化必然失败,是判定层与执行层自相矛盾。
//!
//! C5【凭证种类,2026-08-02 新增,**已 request-capture 实证**】pinned
//! Anthropic 传输层以 `apiKey.includes("sk-ant-oat")`(子串,非前缀)判定
//! OAuth,命中则以 `Authorization: Bearer` 发送、**不发 x-api-key**,并附
//! `anthropic-beta: claude-code-20250219,oauth-2025-04-20,...`;**models.json
//! 里的字面量 apiKey 同样会走该分支**;该判定**只在 Anthropic 族**,同形
//! token 在 OpenAI 族仍按普通 Bearer 发送。因此网关不得把这类凭证当普通
//! x-api-key 代理:字面量命中即判定期 DirectOnly 并给结构化理由(不得是
//! MissingCredential);deferred 凭证判定期不可知,则**物化期解析出命中值
//! 时必须失败**,绝不发出错误的认证形态。
//! **完整 OAuth 传输(Bearer + oauth beta 值)不在前置 C 范围**——按
//! docs/pi-support-restructure-zh.md §1,gateway 数据面属主工程,且需要
//! 先补 request-capture oracle。本工程只保证判定诚实、不发错凭证。
//! C6【entry 隔离,2026-08-02 新增】pinned Pi 逐 entry 做 TypeBox 判定,
//! 单个 entry 的取值错误(如 `contextWindow: 1e400`)只令该 entry 非法;
//! 整文件解析失败会让合法的兄弟 entry 被连坐隐藏,违反四层判定"每个
//! entry 独立"的核心设计。
//!
//! ## 实现方义务(不在本文件断言,交盲审核查)
//! O1 `compat` 需复现 JavaScript object-spread 对嵌套值(尤其数组)的语义;
//! O2 架构扫描器:cfg 布尔语义(`cfg(not(test))` 的生产代码必须被扫描)、
//! 不得按 `tests/` 路径整体跳过文件、嵌套模块须继承父层归属。
//! //!
//! ## 预期红绿(2026-08-02 复审修订基线) //! ## 预期红绿(2026-08-02 复审修订基线)
//! 轮实现已使 C1/C2/C3 四项转绿。本次修订新增两项: //! 前两轮实现已使 C1–C4 全部转绿(9 项)。本轮据盲审 finding 新增四项,
//! `certify_header_only_credentials_stay_direct_only` **应绿**——它把"无 //! 全部**应红**:`certify_oauth_credentials_are_never_proxied_as_api_key`(C5)、
//! apiKey 即降级"钉为契约(上游证据见 C2,首轮盲审曾按缺陷提报,现裁定 //! `certify_literal_credentials_are_validated_at_plan_time`(C4 扩展:
//! 实现正确、契约缺失);`certify_deferred_header_values_are_validated_after_resolution` //! 判定期说可代理、物化必失败的自相矛盾)、
//! **应红**(C4:现实现对原始表达式做头校验)。 //! `certify_empty_containers_stay_distinct_from_absent`(C1 扩展:cost.tiers
//! 即:应红 1、应绿 8。偏离(非清单红、应红变绿、编译失败)即上报 //! `[]` 被抹成缺席)、`certify_one_bad_entry_does_not_hide_its_siblings`(C6)
//! 即:应红 4、应绿 9。偏离(非清单红、应红变绿、编译失败)即上报。
//!
//! ## 本轮裁决的两点澄清
//! 1. 盲审以"C2 同族复发"停止,**裁定不成立**:C2 管的是认证头的分类与取值
//! 次序,C5 是凭证种类识别,且完整 OAuth 传输按范围表属主工程。停止解除。
//! 2. 盲审对"网关强制 apiKey"的上一轮 High 已于前轮裁定为实现正确(见 C2)。
//!
//! ## 上游实证(request-capture,2026-08-02)
//! `scripts/pi-transport-capture.mjs` 以本地抓包端点作 baseUrl,用 pinned Pi
//! 的 adapter 真发请求,实测矩阵(据此 C2/C5 不再是"读源码推断"):
//! - anthropic 普通 key → `x-api-key: <key>`;
//! - anthropic `sk-ant-oat...` → `authorization: Bearer <token>` +
//! `anthropic-beta: claude-code-20250219,oauth-2025-04-20,...`,**无 x-api-key**;
//! - anthropic apiKey + 显式 `x-api-key` → 发**显式值**(显式覆盖合成);
//! - anthropic apiKey + 显式 `authorization` → 两者**并存**
//! (`authorization` 取显式值,`x-api-key` 取合成值);
//! - openai responses/completions + 显式 `authorization` → 发**显式值**;
//! - openai + `sk-ant-oat` 形状 token → 仍是普通 `Bearer`,无 OAuth 特殊处理;
//! - openai completions + 显式 `x-api-key` → 与合成 `authorization` **并存**。
//! //!
//! ## 残余 //! ## 残余
//! 显式优先只对 Anthropic/OpenAI 两族有 SDK 证据(Google 两值并存的优先级、 //! Google 两值并存的优先级、头名大小写变体未实测;命令输出 trim 与环境模板
//! 大小写变体未断言);transport oracle 只执行 resolver,不执行 adapter/SDK //! 不 trim 的差异属数据面语义,本只读面不断言;完整 OAuth 传输实现按范围表
//! 头合并,主工程触碰数据面须先补 request-capture oracle;命令输出 trim 与 //! 归主工程(harness 已就位,可直接扩为受冻结的 transport oracle);
//! 环境模板不 trim 的差异属数据面语义,本只读面不断言;其余按盲审 finding 处理。 //! 其余按盲审 finding 处理。
use super::composer::compose_explicit_custom_catalog; use super::composer::compose_explicit_custom_catalog;
use super::gateway::{assess_composition, PiGatewayCapability, PiGatewayReasonCode}; use super::gateway::{assess_composition, PiGatewayCapability, PiGatewayReasonCode};
@@ -64,7 +107,7 @@ use super::model::{
effective_pi_model, validate_pi_managed_provider, PiConfigError, PiManagedAssessment, effective_pi_model, validate_pi_managed_provider, PiConfigError, PiManagedAssessment,
PiManagedProviderConfig, PiManagementStatus, PiRawNativeValidity, PiManagedProviderConfig, PiManagementStatus, PiRawNativeValidity,
}; };
use super::native::inspect_pi_native_entry; use super::native::{inspect_pi_native_catalog, inspect_pi_native_entry};
use super::raw_schema::evaluate_provider_value; use super::raw_schema::evaluate_provider_value;
use serde_json::{json, Value}; use serde_json::{json, Value};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@@ -598,3 +641,213 @@ fn certify_deferred_header_values_are_validated_after_resolution() {
"a literal header value outside visible ASCII must still be rejected" "a literal header value outside visible ASCII must still be rejected"
); );
} }
// ---------------------------------------------------------------------------
// 应红(C5):OAuth 凭证绝不能按 x-api-key 代理
// ---------------------------------------------------------------------------
#[test]
fn certify_oauth_credentials_are_never_proxied_as_api_key() {
// 字面量命中:判定期即可知,必须 DirectOnly 并给出结构化理由——
// 而不是宣称可代理再发出错误的认证形态。
let literal = composed_catalog(json!({
"api": "anthropic-messages",
"baseUrl": "https://anthropic.example",
"apiKey": "sk-ant-oat01-example-token",
"models": [{"id": "m"}]
}));
let gateway = assess_composition(&literal);
assert_eq!(
gateway.capability,
PiGatewayCapability::DirectOnly,
"pinned Pi sends an sk-ant-oat credential as an OAuth Bearer with oauth beta \
headers; proxying it as x-api-key would send the wrong auth form"
);
assert!(
!gateway.reasons.is_empty(),
"the downgrade must carry a structured reason"
);
assert!(
!has_gateway_reason(&gateway, PiGatewayReasonCode::MissingCredential),
"the credential is present; MissingCredential would misreport the cause"
);
// deferred 凭证:判定期不可知,允许 Proxyable;但物化解析出命中值时必须
// 失败,绝不发出错误的认证形态。
let deferred = composed_catalog(json!({
"api": "anthropic-messages",
"baseUrl": "https://anthropic.example",
"apiKey": "!load-token",
"models": [{"id": "m"}]
}));
let gateway = assess_composition(&deferred);
assert_eq!(
gateway.capability,
PiGatewayCapability::Proxyable,
"a deferred credential's kind is unknowable at plan time"
);
assert!(
gateway.plans[0]
.materialize(&|_: &str| Some("sk-ant-oat01-resolved".to_string()))
.is_err(),
"materialising a resolved OAuth credential must fail rather than send it as \
a plain api key"
);
// 防过度收窄:普通 Anthropic key 不受影响;非 Anthropic 族不适用该判定
// (pinned 的 includes 检查只在 Anthropic 传输层)。
for (api, key) in [
("anthropic-messages", "sk-ant-api03-plain"),
("openai-responses", "sk-ant-oat01-not-anthropic"),
] {
let plain = composed_catalog(json!({
"api": api,
"baseUrl": "https://plain.example/v1",
"apiKey": key,
"models": [{"id": "m"}]
}));
assert_eq!(
assess_composition(&plain).capability,
PiGatewayCapability::Proxyable,
"{api}: the OAuth rule must not over-reach"
);
}
}
// ---------------------------------------------------------------------------
// 应红(C4 扩展):字面量凭证必须在判定期校验
// ---------------------------------------------------------------------------
#[test]
fn certify_literal_credentials_are_validated_at_plan_time() {
// 判定期宣称"可代理"、而每次物化必然失败,是判定层与执行层自相矛盾。
let illegal = composed_catalog(json!({
"api": "openai-responses",
"baseUrl": "https://openai.example/v1",
"apiKey": "café",
"models": [{"id": "m"}]
}));
let gateway = assess_composition(&illegal);
assert!(
gateway.plans.is_empty() || gateway.plans[0].materialize(&|_: &str| None).is_err(),
"sanity: this literal credential can never materialise"
);
assert_eq!(
gateway.capability,
PiGatewayCapability::DirectOnly,
"a literal credential that can never materialise must not be judged proxyable"
);
assert!(
!gateway.reasons.is_empty(),
"the downgrade must carry a structured reason"
);
// 对称约束:deferred 凭证仍不得因原始表达式在判定期被拒(C4)。
let deferred = composed_catalog(json!({
"api": "openai-responses",
"baseUrl": "https://openai.example/v1",
"apiKey": "!echo café",
"models": [{"id": "m"}]
}));
assert_eq!(
assess_composition(&deferred).capability,
PiGatewayCapability::Proxyable,
"a deferred credential must not be validated as a header value before it is \
resolved"
);
}
// ---------------------------------------------------------------------------
// 应红(C1 扩展):空容器与缺席必须可区分
// ---------------------------------------------------------------------------
#[test]
fn certify_empty_containers_stay_distinct_from_absent() {
let base_rates = json!({
"input": 1.0, "output": 2.0, "cacheRead": 0.5, "cacheWrite": 0.25
});
let mut with_empty = base_rates.as_object().expect("rates").clone();
with_empty.insert("tiers".into(), json!([]));
let catalog = json!({
"providers": {
"tiers": {
"api": "anthropic-messages",
"baseUrl": "https://tiers.example",
"apiKey": "literal",
"models": [
{"id": "empty-tiers", "cost": Value::Object(with_empty)},
{"id": "absent-tiers", "cost": base_rates.clone()}
]
}
}
});
let (_temp, path) = write_catalog(&catalog);
let managed = inspect_pi_native_entry(&path, "tiers", &BTreeMap::new())
.expect("inspect")
.expect("entry present")
.managed_config
.expect("managed config");
let round_trip = serde_json::to_value(&managed).expect("serialize managed config");
assert_eq!(
round_trip.pointer("/models/0/cost/tiers"),
Some(&json!([])),
"an explicitly empty tiers list must survive as an empty list"
);
assert_eq!(
round_trip.pointer("/models/1/cost/tiers"),
None,
"an absent tiers list must stay absent"
);
}
// ---------------------------------------------------------------------------
// 应红(C6):单个 entry 的错误不得连坐兄弟 entry
// ---------------------------------------------------------------------------
#[test]
fn certify_one_bad_entry_does_not_hide_its_siblings() {
// pinned Pi 逐 entry 判定:`contextWindow: 1e400` 只令该 entry 非法。
// 整文件解析失败会让合法条目一并消失,破坏"每个 entry 独立"的判定设计。
let source = r#"{
"providers": {
"healthy": {
"api": "anthropic-messages",
"baseUrl": "https://healthy.example",
"apiKey": "literal",
"models": [{"id": "m"}]
},
"overflow": {
"api": "anthropic-messages",
"baseUrl": "https://overflow.example",
"apiKey": "literal",
"models": [{"id": "m", "contextWindow": 1e400}]
}
}
}"#;
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("models.json");
fs::write(&path, source).expect("write");
let diagnostics = inspect_pi_native_catalog(&path, &BTreeMap::new())
.expect("one malformed entry must not fail the whole catalog");
assert_eq!(diagnostics.len(), 2, "both entries must still be reported");
let healthy = diagnostics
.iter()
.find(|diagnostic| diagnostic.provider_key == "healthy")
.expect("healthy entry present");
assert_eq!(
healthy.raw_validity,
PiRawNativeValidity::Valid,
"a legal sibling must not be hidden by a malformed entry"
);
assert_eq!(healthy.management_status, PiManagementStatus::Importable);
let overflow = diagnostics
.iter()
.find(|diagnostic| diagnostic.provider_key == "overflow")
.expect("overflow entry present");
assert_ne!(
overflow.raw_validity,
PiRawNativeValidity::Valid,
"the out-of-range contextWindow entry itself must not be judged valid"
);
}
+7 -1
View File
@@ -662,7 +662,13 @@ fn evaluate_schema(schema: &Value, instance: &Value, pointer: &str) -> SchemaOut
"object" => instance.is_object(), "object" => instance.is_object(),
"array" => instance.is_array(), "array" => instance.is_array(),
"string" => instance.is_string(), "string" => instance.is_string(),
"number" => instance.is_number(), // TypeBox Number follows JavaScript Number semantics and rejects
// NaN/Infinity. With serde_json arbitrary precision, an overflow
// token remains a Number but has no finite f64 representation.
"number" => instance
.as_number()
.and_then(serde_json::Number::as_f64)
.is_some(),
"boolean" => instance.is_boolean(), "boolean" => instance.is_boolean(),
"null" => instance.is_null(), "null" => instance.is_null(),
_ => return unsupported(pointer), _ => return unsupported(pointer),