mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 11:43:57 +08:00
fix(pi): certify pinned native inspection semantics
This commit is contained in:
@@ -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,
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user