{"version":3,"file":"hash-request.mjs","names":[],"sources":["../../../../../../../ai/src/vcr/hash-request.ts"],"sourcesContent":["import type { Message } from \"../contracts/conversation-message.type\";\nimport type { ModelCallOptions } from \"../contracts/model.contract\";\nimport type { ToolConfig } from \"../contracts/tool.contract\";\n\n/**\n * Default `ModelCallOptions` fields folded into the request hash. These are\n * the inputs that materially change the model's output; everything else\n * (notably `signal` and unknown provider keys) is excluded so an otherwise\n * identical logical call still matches its recording.\n */\nexport const DEFAULT_HASH_OPTIONS: readonly string[] = [\n  \"temperature\",\n  \"maxTokens\",\n  \"responseSchema\",\n  \"tools\",\n  \"reasoning\",\n];\n\n/**\n * Reduce a tool to the parts the model actually conditions on: its name,\n * description, and the *shape* of its input schema. Two tools that differ\n * only by object identity (a fresh schema instance per import) hash\n * identically; a real contract change (renamed field, new description)\n * invalidates the recording.\n *\n * The input schema is fingerprinted structurally — a Standard Schema is an\n * opaque object, so we serialize its enumerable own keys rather than\n * attempting to read its internals.\n */\nfunction fingerprintTool(tool: ToolConfig<unknown, unknown>): unknown {\n  return {\n    name: tool.name,\n    description: tool.description,\n    input: tool.input ? schemaShape(tool.input) : undefined,\n  };\n}\n\n/**\n * Produce a stable, JSON-safe fingerprint of an arbitrary schema object.\n * Records only the structural skeleton (own enumerable keys, recursively)\n * so harmless instance differences don't perturb the hash while a genuine\n * structural change does.\n */\nfunction schemaShape(value: unknown, depth = 0): unknown {\n  if (depth > 6 || value === null || typeof value !== \"object\") {\n    return typeof value;\n  }\n\n  if (Array.isArray(value)) {\n    return value.map((item) => schemaShape(item, depth + 1));\n  }\n\n  const out: Record<string, unknown> = {};\n\n  for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n    out[key] = schemaShape((value as Record<string, unknown>)[key], depth + 1);\n  }\n\n  return out;\n}\n\n/**\n * Pick the hashable subset of `options`, normalizing `tools` into their\n * name+description+schema-shape fingerprint. `signal` and any field not in\n * `hashOptions` are dropped.\n */\nfunction pickOptions(\n  options: ModelCallOptions | undefined,\n  hashOptions: readonly string[],\n): Record<string, unknown> {\n  if (!options) {\n    return {};\n  }\n\n  const picked: Record<string, unknown> = {};\n\n  for (const key of hashOptions) {\n    const value = options[key];\n\n    if (value === undefined) {\n      continue;\n    }\n\n    if (key === \"tools\" && Array.isArray(value)) {\n      picked[key] = (value as ToolConfig<unknown, unknown>[]).map(fingerprintTool);\n      continue;\n    }\n\n    picked[key] = value;\n  }\n\n  return picked;\n}\n\n/**\n * Recursively sort object keys so two logically-equal payloads serialize to\n * byte-identical JSON regardless of property insertion order.\n */\nfunction canonicalize(value: unknown): unknown {\n  if (value === null || typeof value !== \"object\") {\n    return value;\n  }\n\n  if (Array.isArray(value)) {\n    return value.map(canonicalize);\n  }\n\n  const out: Record<string, unknown> = {};\n\n  for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n    out[key] = canonicalize((value as Record<string, unknown>)[key]);\n  }\n\n  return out;\n}\n\n/**\n * Non-cryptographic 53-bit string hash (FNV-style, cyrb53). Deterministic\n * across runs and platforms; collision-resistant enough for a per-cassette\n * keyspace. Returned as a base-36 string.\n */\nfunction hashString(input: string): string {\n  let h1 = 0xdeadbeef;\n  let h2 = 0x41c6ce57;\n\n  for (let i = 0; i < input.length; i++) {\n    const ch = input.charCodeAt(i);\n\n    h1 = Math.imul(h1 ^ ch, 2654435761);\n    h2 = Math.imul(h2 ^ ch, 1597334677);\n  }\n\n  h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);\n  h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n  h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);\n  h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n\n  const combined = 4294967296 * (2097151 & h2) + (h1 >>> 0);\n\n  return combined.toString(36);\n}\n\n/**\n * Compute the stable VCR request hash for a model call.\n *\n * The hash covers the full `messages` array plus the picked, normalized\n * `options` subset (see {@link DEFAULT_HASH_OPTIONS}). Inputs are\n * canonicalized (recursive key sort) before serialization so property order\n * never affects the result. `signal` and unknown provider keys are excluded.\n *\n * @example\n * const a = hashRequest(messages, { temperature: 0.2 });\n * const b = hashRequest(messages, { temperature: 0.2, signal });\n * // a === b — signal is excluded.\n */\nexport function hashRequest(\n  messages: Message[],\n  options?: ModelCallOptions,\n  hashOptions: readonly string[] = DEFAULT_HASH_OPTIONS,\n): string {\n  const payload = canonicalize({\n    messages,\n    options: pickOptions(options, hashOptions),\n  });\n\n  return hashString(JSON.stringify(payload));\n}\n"],"mappings":";;;;;;;AAUA,MAAa,uBAA0C;CACrD;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;AAaA,SAAS,gBAAgB,MAA6C;CACpE,OAAO;EACL,MAAM,KAAK;EACX,aAAa,KAAK;EAClB,OAAO,KAAK,QAAQ,YAAY,KAAK,KAAK,IAAI;CAChD;AACF;;;;;;;AAQA,SAAS,YAAY,OAAgB,QAAQ,GAAY;CACvD,IAAI,QAAQ,KAAK,UAAU,QAAQ,OAAO,UAAU,UAClD,OAAO,OAAO;CAGhB,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,SAAS,YAAY,MAAM,QAAQ,CAAC,CAAC;CAGzD,MAAM,MAA+B,CAAC;CAEtC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAgC,CAAC,CAAC,KAAK,GACnE,IAAI,OAAO,YAAa,MAAkC,MAAM,QAAQ,CAAC;CAG3E,OAAO;AACT;;;;;;AAOA,SAAS,YACP,SACA,aACyB;CACzB,IAAI,CAAC,SACH,OAAO,CAAC;CAGV,MAAM,SAAkC,CAAC;CAEzC,KAAK,MAAM,OAAO,aAAa;EAC7B,MAAM,QAAQ,QAAQ;EAEtB,IAAI,UAAU,QACZ;EAGF,IAAI,QAAQ,WAAW,MAAM,QAAQ,KAAK,GAAG;GAC3C,OAAO,OAAQ,MAAyC,IAAI,eAAe;GAC3E;EACF;EAEA,OAAO,OAAO;CAChB;CAEA,OAAO;AACT;;;;;AAMA,SAAS,aAAa,OAAyB;CAC7C,IAAI,UAAU,QAAQ,OAAO,UAAU,UACrC,OAAO;CAGT,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,IAAI,YAAY;CAG/B,MAAM,MAA+B,CAAC;CAEtC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAgC,CAAC,CAAC,KAAK,GACnE,IAAI,OAAO,aAAc,MAAkC,IAAI;CAGjE,OAAO;AACT;;;;;;AAOA,SAAS,WAAW,OAAuB;CACzC,IAAI,KAAK;CACT,IAAI,KAAK;CAET,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,MAAM,KAAK,MAAM,WAAW,CAAC;EAE7B,KAAK,KAAK,KAAK,KAAK,IAAI,UAAU;EAClC,KAAK,KAAK,KAAK,KAAK,IAAI,UAAU;CACpC;CAEA,KAAK,KAAK,KAAK,KAAM,OAAO,IAAK,UAAU;CAC3C,MAAM,KAAK,KAAK,KAAM,OAAO,IAAK,UAAU;CAC5C,KAAK,KAAK,KAAK,KAAM,OAAO,IAAK,UAAU;CAC3C,MAAM,KAAK,KAAK,KAAM,OAAO,IAAK,UAAU;CAI5C,QAFiB,cAAc,UAAU,OAAO,OAAO,GAExC,CAAC,SAAS,EAAE;AAC7B;;;;;;;;;;;;;;AAeA,SAAgB,YACd,UACA,SACA,cAAiC,sBACzB;CACR,MAAM,UAAU,aAAa;EAC3B;EACA,SAAS,YAAY,SAAS,WAAW;CAC3C,CAAC;CAED,OAAO,WAAW,KAAK,UAAU,OAAO,CAAC;AAC3C"}