Naposledy aktivní 1791843245

plugin to make pi.dev / pi.gui send the session as user.id to avoid sharing KV_cache

Revize 53cbf2cb4849f4e867b8d3c8e8ee652ddf60b728

deepseek_user_id_with_session_name Raw
1/**
2 * DeepSeek `user_id` — ONE ID PER SESSION, never one shared value.
3 *
4 * DeepSeek's Chat Completions API accepts a custom `user_id` body parameter
5 * (https://api-docs.deepseek.com/quick_start/rate_limit) used for:
6 * - content-safety review isolation
7 * - KV-cache isolation
8 * - scheduling/concurrency isolation
9 *
10 * WHY PER SESSION. Those are ISOLATION keys: a single fixed value puts every session that uses it
11 * into the SAME namespace, which is the opposite of what the parameter is for — three concurrent
12 * sessions then share whatever is keyed by it. The id is `<prefix>-<session name>` (e.g.
13 * `santiago-BUILDING-ATOMIQUE-101`), so the value both ISOLATES the session and NAMES it: the
14 * provider's own usage and rate-limit views show which session spent what, which a constant can
15 * never do.
16 *
17 * THE NAME COMES FROM pi.getSessionName() — the same accessor pi-intercom resolves a session's
18 * alias through — and the runtime session id is used when a session has no name yet, so the id is
19 * unique from the first request rather than only after someone renames the session. A rename
20 * changes the id and therefore starts a fresh cache namespace: correct, and the price of the id
21 * following the name.
22 *
23 * Pi's models.json config surface only supports HTTP `headers`, not body parameters, so this hooks
24 * the `before_provider_request` event, which can replace the outgoing payload. Scoped to
25 * `deepseek*` model ids so other providers are never touched.
26 */
27
28import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
29
30const USER_PREFIX = "santiago";
31
32/** The id is a provider-facing token: DeepSeek's Chat Completions docs allow exactly
33 * [a-zA-Z0-9\-_], up to 512 characters (stated in the `user_id` row of the API reference), and ask
34 * that no user privacy information ride it. A session label is a workspace name, not personal data;
35 * the cap is far below their limit so a long label cannot be rejected on length. */
36const MAX_ID = 96;
37
38/**
39 * Does a session name look like an ALIAS (the thing this id wants) or like a PROMPT?
40 *
41 * An unaliased session's name is its opening prompt — this session's own first `session_info` record
42 * was the whole handoff text — and DeepSeek's API reference says plainly: "Do not include user
43 * privacy information in the user_id." So a prompt-shaped name is refused deliberately and the
44 * runtime session id is used instead; only a short, single-token label becomes the readable half.
45 */
46function looksLikeAlias(name: string): boolean {
47 return name !== "" && name.length <= 48 && !/\s/.test(name);
48}
49
50/**
51 * Build the per-session `user_id`. Exported so it can be asserted directly.
52 *
53 * NO FALLBACK TO A SHARED VALUE: if neither an alias nor a session id can be read, this returns
54 * undefined and the caller leaves the payload alone, because the one thing this id must never be
55 * is the SAME for two sessions.
56 */
57export function deepseekUserId(pi: Pick<ExtensionAPI, "getSessionName">): string | undefined {
58 const name = pi.getSessionName()?.trim();
59 const sessionId = process.env.PI_SESSION_ID?.trim();
60 const alias = name !== undefined && looksLikeAlias(name) ? name : undefined;
61 const basis = alias ?? (sessionId !== undefined && sessionId !== "" ? sessionId : undefined);
62 if (basis === undefined) {
63 return undefined;
64 }
65 const clean = basis.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, MAX_ID);
66 return clean === "" ? undefined : `${USER_PREFIX}-${clean}`;
67}
68
69export default function (pi: ExtensionAPI) {
70 pi.on("before_provider_request", (event) => {
71 const payload = event.payload as Record<string, unknown> | null | undefined;
72 if (!payload || typeof payload !== "object") {
73 return;
74 }
75
76 const model = typeof payload.model === "string" ? payload.model : "";
77 if (!model.startsWith("deepseek")) {
78 return;
79 }
80
81 const userId = deepseekUserId(pi);
82 if (userId === undefined) {
83 return;
84 }
85
86 return { ...payload, user_id: userId };
87 });
88}