santiago revised this gist . Go to revision
No changes
santiago revised this gist . Go to revision
1 file changed, 88 insertions
deepseek_user_id_with_session_name(file created)
| @@ -0,0 +1,88 @@ | |||
| 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 | + | ||
| 28 | + | import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; | |
| 29 | + | ||
| 30 | + | const 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. */ | |
| 36 | + | const 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 | + | */ | |
| 46 | + | function 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 | + | */ | |
| 57 | + | export 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 | + | ||
| 69 | + | export 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 | + | } | |
Newer
Older