mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
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:
parent
adc5fa9f80
commit
04a93bad8f
20 changed files with 236 additions and 194 deletions
|
|
@ -1,10 +1,8 @@
|
|||
import type { Api, ClientIdentity, Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
||||
import { type ClientIdentity, PI_ATTRIBUTION } from "@earendil-works/pi-ai";
|
||||
import { VERSION } from "../config.ts";
|
||||
import type { SettingsManager } from "./settings-manager.ts";
|
||||
import { isInstallTelemetryEnabled } from "./telemetry.ts";
|
||||
|
||||
const OPENCODE_HOST = "opencode.ai";
|
||||
|
||||
/**
|
||||
* How pi's requests identify it to providers: a detailed `User-Agent` with pi's version, and attribution to pi as a
|
||||
* CLI agent while install telemetry is enabled.
|
||||
|
|
@ -13,28 +11,6 @@ export function getClientIdentity(settingsManager: SettingsManager): ClientIdent
|
|||
return {
|
||||
userAgent: "detailed",
|
||||
version: VERSION,
|
||||
attribution: isInstallTelemetryEnabled(settingsManager) ? { categories: "cli-agent" } : false,
|
||||
attribution: isInstallTelemetryEnabled(settingsManager) ? PI_ATTRIBUTION : null,
|
||||
};
|
||||
}
|
||||
|
||||
function isOpenCodeModel(model: Model<Api>): boolean {
|
||||
if (model.provider === "opencode" || model.provider === "opencode-go") return true;
|
||||
try {
|
||||
return new URL(model.baseUrl).hostname === OPENCODE_HOST;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenCode's per-conversation routing header. pi-ai's OpenCode providers add it themselves; this also covers custom
|
||||
* providers pointing at OpenCode. Configured and request headers override it.
|
||||
*/
|
||||
export function withOpenCodeSessionHeader(
|
||||
model: Model<Api>,
|
||||
sessionId: string | undefined,
|
||||
headers: ProviderHeaders,
|
||||
): ProviderHeaders {
|
||||
if (!sessionId || !isOpenCodeModel(model)) return headers;
|
||||
return { "x-opencode-session": sessionId, ...headers };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
type DeferredCancelOptions,
|
||||
type DeferredFetchOptions,
|
||||
type DeferredHandle,
|
||||
getOpenCodeSessionHeaders,
|
||||
type ImageApi,
|
||||
type ImageModel,
|
||||
type ImagesContext,
|
||||
|
|
@ -677,13 +678,20 @@ export class ModelRuntime implements Models {
|
|||
|
||||
const { transformHeaders, ...rawProviderOptions } = options ?? {};
|
||||
const providerOptions = rawProviderOptions as Omit<TOptions, "transformHeaders"> & ProviderRequestOptions<TModel>;
|
||||
let headers = mergeHeaders(resolution.auth.headers, providerOptions.headers);
|
||||
const requestModel: TModel = resolution.auth.baseUrl ? { ...model, baseUrl: resolution.auth.baseUrl } : model;
|
||||
const sessionId =
|
||||
"sessionId" in providerOptions && typeof providerOptions.sessionId === "string"
|
||||
? providerOptions.sessionId
|
||||
: undefined;
|
||||
let headers = mergeHeaders(
|
||||
mergeHeaders(getOpenCodeSessionHeaders(requestModel, sessionId), resolution.auth.headers),
|
||||
providerOptions.headers,
|
||||
);
|
||||
if (transformHeaders) headers = await transformHeaders(headers ?? {});
|
||||
const env =
|
||||
resolution.env || providerOptions.env
|
||||
? { ...(resolution.env ?? {}), ...(providerOptions.env ?? {}) }
|
||||
: undefined;
|
||||
const requestModel: TModel = resolution.auth.baseUrl ? { ...model, baseUrl: resolution.auth.baseUrl } : model;
|
||||
return {
|
||||
provider,
|
||||
model: requestModel,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { resolvePath } from "../utils/paths.ts";
|
|||
import { AgentSession } from "./agent-session.ts";
|
||||
import { formatNoModelsAvailableMessage } from "./auth-guidance.ts";
|
||||
import { CacheWarmer } from "./cache-warmer.ts";
|
||||
import { getClientIdentity, withOpenCodeSessionHeader } from "./client-identity.ts";
|
||||
import { getClientIdentity } from "./client-identity.ts";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.ts";
|
||||
import type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from "./extensions/index.ts";
|
||||
import { createToolNameMatcher } from "./mcp-servers.ts";
|
||||
|
|
@ -337,10 +337,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|||
() => settingsManager.getCacheWarmingMode(),
|
||||
async (event) => extensionRunnerRef.current?.emitCacheWarmingDecision(event) ?? event.action,
|
||||
);
|
||||
const buildRequestOptions = (
|
||||
requestModel: Model<any>,
|
||||
options: ModelsSimpleStreamOptions = {},
|
||||
): ModelsSimpleStreamOptions => {
|
||||
const buildRequestOptions = (options: ModelsSimpleStreamOptions = {}): ModelsSimpleStreamOptions => {
|
||||
const providerRetrySettings = settingsManager.getProviderRetrySettings();
|
||||
const httpIdleTimeoutMs = settingsManager.getHttpIdleTimeoutMs();
|
||||
const effectiveTimeoutMs = httpIdleTimeoutMs === 0 ? 2147483647 : httpIdleTimeoutMs;
|
||||
|
|
@ -353,12 +350,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|||
maxRetryDelayMs: options.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,
|
||||
// Replaces the agent loop's default identity; read per request so telemetry changes apply at once.
|
||||
identity: getClientIdentity(settingsManager),
|
||||
transformHeaders: async (requestHeaders) => {
|
||||
const headers = withOpenCodeSessionHeader(requestModel, options.sessionId, requestHeaders);
|
||||
return headerRunner?.hasHandlers("before_provider_headers")
|
||||
transformHeaders: async (headers) =>
|
||||
headerRunner?.hasHandlers("before_provider_headers")
|
||||
? headerRunner.emitBeforeProviderHeaders(headers)
|
||||
: headers;
|
||||
},
|
||||
: headers,
|
||||
};
|
||||
};
|
||||
// Warm only requests for the selected model. Requests a virtual selection routed, or that an
|
||||
|
|
@ -415,7 +410,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|||
},
|
||||
convertToLlm: convertToLlmWithBlockImages,
|
||||
streamFn: async (model, context, options) => {
|
||||
const requestOptions = buildRequestOptions(model, options);
|
||||
const requestOptions = buildRequestOptions(options);
|
||||
// Compaction and summaries use their own routing ids; only session requests
|
||||
// replace the cache entry, so warming restarts from them. Keep warming while
|
||||
// the current transcript still extends the request's prefix. Agent state may
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
getAttributionHeaders,
|
||||
type Model,
|
||||
normalizeContext,
|
||||
PI_ATTRIBUTION,
|
||||
type SimpleStreamOptions,
|
||||
} from "@earendil-works/pi-ai";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
|
@ -145,7 +146,7 @@ describe("createAgentSession client identity", () => {
|
|||
expect(options?.identity).toEqual({
|
||||
userAgent: "detailed",
|
||||
version: VERSION,
|
||||
attribution: { categories: "cli-agent" },
|
||||
attribution: PI_ATTRIBUTION,
|
||||
});
|
||||
expect(getAttributionHeaders(model, options?.identity)).toEqual({
|
||||
"HTTP-Referer": "https://pi.dev",
|
||||
|
|
@ -158,7 +159,7 @@ describe("createAgentSession client identity", () => {
|
|||
const model = createModel("nvidia", "https://integrate.api.nvidia.com/v1");
|
||||
const options = await captureOptions(model, { telemetryEnabled: false });
|
||||
|
||||
expect(options?.identity).toEqual({ userAgent: "detailed", version: VERSION, attribution: false });
|
||||
expect(options?.identity).toEqual({ userAgent: "detailed", version: VERSION, attribution: null });
|
||||
expect(getAttributionHeaders(model, options?.identity)).toEqual({});
|
||||
});
|
||||
|
||||
|
|
@ -166,7 +167,7 @@ describe("createAgentSession client identity", () => {
|
|||
process.env.PI_TELEMETRY = "0";
|
||||
const options = await captureOptions(createModel("openrouter", "https://openrouter.ai/api/v1"));
|
||||
|
||||
expect(options?.identity?.attribution).toBe(false);
|
||||
expect(options?.identity?.attribution).toBeNull();
|
||||
});
|
||||
|
||||
it("adds OpenCode session headers for custom OpenCode providers", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue