request-header.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. /**
  2. * Request-header reconstruction utilities over full `request/header` session
  3. * events. Anyone holding a session log reconstructs the {@link EpochHeader}
  4. * any request was built under by taking the latest canonical snapshot; the
  5. * loop uses the same equality helper to avoid logging unchanged headers.
  6. *
  7. * @module dsh-session/request-header
  8. */
  9. import { callConfigEquals } from '@deepseek-ai/dsh-llm'
  10. import type { ToolSchema } from '@deepseek-ai/dsh-llm'
  11. import type { EpochHeader, SessionEvent } from './types.ts'
  12. /**
  13. * Normalize a header to canonical form: an empty tool list becomes an absent
  14. * field, matching how requests are built. Logging, folding, and comparison use
  15. * this one representation.
  16. * @param header - the header to normalize (not mutated).
  17. * @returns the canonical header.
  18. */
  19. export function canonicalHeader(header: EpochHeader): EpochHeader {
  20. const adapterDefaults = header.adapterDefaults
  21. return {
  22. config: header.config,
  23. ...adapterDefaults?.reasoningEffort === true || adapterDefaults?.maxTokens === true
  24. ? { adapterDefaults }
  25. : {},
  26. ...header.tools !== undefined && header.tools.length > 0 ? { tools: header.tools } : {},
  27. }
  28. }
  29. /** Canonical JSON equality for tool schemas assembled through the same path. */
  30. function sameSchema(a: ToolSchema, b: ToolSchema): boolean {
  31. return JSON.stringify(a) === JSON.stringify(b)
  32. }
  33. /**
  34. * Field-wise equality over canonical headers. Tool schemas compare in order.
  35. * @param a - one canonical header.
  36. * @param b - the other.
  37. * @returns whether config, adapter defaults, and tools all match.
  38. */
  39. export function headerEquals(a: EpochHeader, b: EpochHeader): boolean {
  40. if (
  41. !callConfigEquals(a.config, b.config)
  42. || a.adapterDefaults?.reasoningEffort !== b.adapterDefaults?.reasoningEffort
  43. || a.adapterDefaults?.maxTokens !== b.adapterDefaults?.maxTokens
  44. ) return false
  45. const at = a.tools ?? []
  46. const bt = b.tools ?? []
  47. return at.length === bt.length && at.every((tool, i) => sameSchema(tool, bt[i] as ToolSchema))
  48. }
  49. /**
  50. * Fold the header events of a log (or any prefix) into the
  51. * {@link EpochHeader} in force after the last snapshot. Non-header events are
  52. * skipped. This is the pure offline reconstruction path; the live session
  53. * tracks the same fold incrementally.
  54. * @param events - session events in log order.
  55. * @param from - a previously folded state to continue from.
  56. * @returns the latest canonical header, or undefined when none exists yet.
  57. */
  58. export function foldRequestHeader(events: readonly SessionEvent[], from?: EpochHeader): EpochHeader | undefined {
  59. let state = from
  60. for (const event of events) {
  61. if (event.type === 'request/header') state = canonicalHeader(event.data.header)
  62. }
  63. return state
  64. }