fix(ai,agent,coding-agent,durable): client identity follow-ups

- ClientAttribution is { name, url, categories? } with required name and url, so
  OpenRouter always gets an HTTP-Referer. identity.ts maps the fields to each
  provider's headers; NVIDIA gets the name with its first letter upper-cased.
  attribution: null turns it off. PI_ATTRIBUTION is pi's, including the
  cli-agent category; agent-core, durable, and the coding agent use it.
- Categories are a string array, joined for OpenRouter.
- withClientIdentity() is a typed wrapper instead of a Proxy and keeps a
  MutableModels mutable. Durable no longer caches wrappers in a WeakMap.
- The Codex originator stays pi; login agentName no longer follows attribution.
- Models and the coding agent's ModelRuntime add x-opencode-session for custom
  providers pointing at OpenCode, replacing the coding agent's transformHeaders.
This commit is contained in:
Mario Zechner 2026-10-21 11:18:16 +00:00
parent adc5fa9f80
commit 04a93bad8f
20 changed files with 236 additions and 194 deletions

View file

@ -4,7 +4,7 @@
### Added
- Added `AgentOptions.identity` and `Agent.identity` (a pi-ai `ClientIdentity`). Agent loop requests use `DEFAULT_AGENT_IDENTITY` (`{ attribution: true }`) under it, so OpenRouter, NVIDIA NIM, and OpenCode requests are attributed to pi by default; pass `{ attribution: false }` to send no attribution headers.
- Added `AgentOptions.identity` and `Agent.identity` (a pi-ai `ClientIdentity`) to set the `User-Agent` and provider attribution of agent loop requests.
## [1.3.0] - 2026-10-15

View file

@ -233,9 +233,9 @@ const agent = new Agent({
sessionId: "session-123",
// How requests identify the app to providers (pi-ai ClientIdentity), merged over
// DEFAULT_AGENT_IDENTITY ({ attribution: true }: provider attribution headers name pi).
// Pass a name to attribute requests to your app, or false to send none.
identity: { attribution: "my-app" },
// DEFAULT_AGENT_IDENTITY ({ attribution: PI_ATTRIBUTION }: provider attribution headers name pi).
// Pass your app's attribution, or null to send none.
identity: { attribution: { name: "my-app", url: "https://my-app.example" } },
// Dynamic API key resolution (for expiring OAuth tokens)
getApiKey: async (provider) => refreshToken(),

View file

@ -11,6 +11,7 @@ import {
getToolStateChanges,
mergeClientIdentity,
normalizeContext,
PI_ATTRIBUTION,
type SystemMessage,
type ToolResultMessage,
type ToolStateChanges,
@ -37,7 +38,7 @@ export type AgentEventSink = (event: AgentEvent) => Promise<void> | void;
* Request identity of agent loops: pi-ai's defaults with provider attribution to pi. `AgentLoopConfig.identity`
* overrides it field by field.
*/
export const DEFAULT_AGENT_IDENTITY: ClientIdentity = { attribution: true };
export const DEFAULT_AGENT_IDENTITY: ClientIdentity = { attribution: PI_ATTRIBUTION };
/**
* Start an agent loop with a new prompt message.

View file

@ -141,7 +141,7 @@ export interface AgentOptions {
toolExecution?: ToolExecutionMode;
/**
* How requests identify the app to providers, merged field by field over `DEFAULT_AGENT_IDENTITY`
* (`{ attribution: true }`). Pass `{ attribution: false }` to send no attribution headers.
* (`{ attribution: PI_ATTRIBUTION }`). Pass `{ attribution: null }` to send no attribution headers.
*/
identity?: ClientIdentity;
}

View file

@ -1,4 +1,4 @@
import type { ClientIdentity } from "@earendil-works/pi-ai";
import { type ClientIdentity, PI_ATTRIBUTION } from "@earendil-works/pi-ai";
import {
type AssistantMessage,
createAssistantMessageEventStream,
@ -1214,15 +1214,16 @@ describe("Agent", () => {
});
await agent.prompt("hello");
agent.identity = { userAgent: "short", attribution: "acme" };
const acme = { name: "acme", url: "https://acme.example" };
agent.identity = { userAgent: "short", attribution: acme };
await agent.prompt("hello again");
agent.identity = { userAgent: "short" };
await agent.prompt("and again");
expect(received).toEqual([
{ attribution: true },
{ attribution: "acme", userAgent: "short" },
{ attribution: true, userAgent: "short" },
{ attribution: PI_ATTRIBUTION },
{ attribution: acme, userAgent: "short" },
{ attribution: PI_ATTRIBUTION, userAgent: "short" },
]);
});
});