feat(ai,agent,coding-agent): move client identity and provider attribution into pi-ai

pi-ai gets a ClientIdentity request option and createModels() default that choose the User-Agent (detailed with version, short, or custom) and provider attribution headers for OpenRouter, NVIDIA NIM, and OpenCode. Attribution is off by default in pi-ai and on by default in pi-agent-core and pi-durable (HarnessOptions.identity). pi-coding-agent sets its identity from the telemetry setting instead of adding headers itself.
This commit is contained in:
Armin Ronacher 2026-10-21 12:17:35 +02:00 • committed by Mario Zechner
parent adfc14461a
commit adc5fa9f80
55 changed files with 958 additions and 465 deletions

View file

@ -37,7 +37,8 @@ const BUDGETS = {
"packages/ai": {
"./models": {
// Request telemetry reaches pi-ai's telemetry.ts and pi-telemetry's root (index, noop, metrics, memory).
maxFiles: 18,
// Request identity reaches utils/identity.ts and its version constant.
maxFiles: 20,
forbid: ["providers/", "models.generated.ts", "index.ts", "utils/validation.ts", "utils/typebox-helpers.ts"],
allow: ["packages/telemetry/src/index.ts"],
},
@ -63,7 +64,8 @@ const BUDGETS = {
// memory and SQLite storages share scan order and cursor handling. Values from Chord's root (copyJson,
// replicatedState) reach its host runtime through the root barrel. Chord's context reaches pi-telemetry's root
// (index, noop, metrics, memory) for its NOOP telemetry default; harness/telemetry.ts is Durable's schema.
maxFiles: 71,
// The Harness's request identity reaches pi-ai's utils/identity.ts and its version constant.
maxFiles: 73,
forbid: [
"packages/ai/src/index.ts",
"packages/ai/src/utils/typebox-helpers.ts",

View file

@ -5,8 +5,8 @@
* exact internal dependency versions in all workspace packages, including private ones.
*/
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { findPackageDirectories } from "./package-workspaces.mjs";
const GENERATED_PACKAGE_SUFFIXES = [join("coding-agent", "install-lock")];
@ -75,3 +75,14 @@ if (totalUpdates === 0) {
} else {
console.log(`\nUpdated ${totalUpdates} dependency version(s).`);
}
// pi-ai reports its version in the User-Agent without reading package.json, which browsers cannot do.
const aiPackage = workspacePackages.find((pkg) => pkg.data.name === "@earendil-works/pi-ai");
const aiVersionPath = aiPackage && join(dirname(aiPackage.path), "src", "utils", "version.ts");
if (aiVersionPath && existsSync(aiVersionPath)) {
const content = `// The @earendil-works/pi-ai package version. scripts/sync-versions.js keeps it in sync with package.json.\nexport const PI_AI_VERSION = ${JSON.stringify(aiPackage.data.version)};\n`;
if (readFileSync(aiVersionPath, "utf8") !== content) {
writeFileSync(aiVersionPath, content);
console.log(`\nUpdated ${aiVersionPath} to ${aiPackage.data.version}.`);
}
}

View file

@ -91,3 +91,22 @@ test("pins published and private dependencies without touching registry aliases,
await rm(root, { recursive: true, force: true });
}
});
test("writes the pi-ai version constant", async () => {
const root = await mkdtemp(join(tmpdir(), "pi-sync-versions-"));
try {
await writeManifest(root, "packages/ai", {
name: "@earendil-works/pi-ai",
version: "2.0.0",
});
const versionPath = join(root, "packages/ai/src/utils/version.ts");
await mkdir(join(root, "packages/ai/src/utils"), { recursive: true });
await writeFile(versionPath, 'export const PI_AI_VERSION = "1.0.0";\n');
const result = runSyncVersions(root);
assert.equal(result.status, 0, result.stderr);
assert.match(await readFile(versionPath, "utf8"), /export const PI_AI_VERSION = "2\.0\.0";\n$/);
} finally {
await rm(root, { recursive: true, force: true });
}
});