model-selection-projection.ts 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. /** Durable model-selection intent and request-use projection. */
  2. import type { Context } from '@deepseek-ai/cordis'
  3. import type { SessionEvent } from '@deepseek-ai/dsh-session'
  4. import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
  5. import { z } from 'zod'
  6. import type {
  7. ModelSelection,
  8. ModelSelectionProjection,
  9. ModelSelectionProjectionState,
  10. } from './types.ts'
  11. const modelSelectionSchema = z.object({
  12. provider: z.string().min(1),
  13. model: z.string().min(1),
  14. reasoningEffort: z.string().min(1).optional(),
  15. }) as unknown as z.ZodType<ModelSelection>
  16. const modelSelectionProjectionStateSchema = z.object({
  17. lastUsed: modelSelectionSchema.nullable(),
  18. pending: modelSelectionSchema.nullable(),
  19. }) as unknown as z.ZodType<ModelSelectionProjectionState>
  20. const modelSelectionProjectionSchema = z.object({
  21. lastUsed: modelSelectionSchema.nullable(),
  22. next: modelSelectionSchema.nullable(),
  23. }) as unknown as z.ZodType<ModelSelectionProjection>
  24. /**
  25. * Advance durable model-selection state by one Session event.
  26. * @param state - selection state before the event.
  27. * @param event - next committed Session event.
  28. * @returns the original or advanced selection state.
  29. */
  30. function applyModelSelectionProjection(
  31. state: ModelSelectionProjectionState,
  32. event: SessionEvent,
  33. ): ModelSelectionProjectionState {
  34. if (event.type === 'model/selection') {
  35. return sameSelection(state.pending, event.data)
  36. ? state
  37. : { lastUsed: state.lastUsed, pending: event.data }
  38. }
  39. if (event.type !== 'request/header') return state
  40. const lastUsed: ModelSelection = {
  41. provider: event.data.header.config.provider,
  42. model: event.data.header.config.model,
  43. ...(event.data.header.config.reasoningEffort === undefined
  44. ? {}
  45. : { reasoningEffort: String(event.data.header.config.reasoningEffort) }),
  46. }
  47. const pending = sameSelection(state.pending, lastUsed) ? null : state.pending
  48. return sameSelection(state.lastUsed, lastUsed) && pending === state.pending
  49. ? state
  50. : { lastUsed, pending }
  51. }
  52. const modelSelectionProjection = {
  53. key: 'modelSelection',
  54. stateSchema: modelSelectionProjectionStateSchema,
  55. init: () => ({ lastUsed: null, pending: null }),
  56. apply: applyModelSelectionProjection,
  57. wire: {
  58. viewSchema: modelSelectionProjectionSchema,
  59. view: state => ({ lastUsed: state.lastUsed, next: state.pending ?? state.lastUsed }),
  60. },
  61. stateVersion: 2,
  62. } satisfies ProjectionDefinition<'modelSelection', ModelSelectionProjectionState>
  63. function sameSelection(left: ModelSelection | null, right: ModelSelection | null): boolean {
  64. return left === right || (left !== null && right !== null
  65. && left.provider === right.provider
  66. && left.model === right.model
  67. && left.reasoningEffort === right.reasoningEffort)
  68. }
  69. /**
  70. * Register the durable model-selection projection when the registry is present.
  71. * @param ctx - Session Controller context.
  72. */
  73. export function installModelSelectionProjection(ctx: Context): void {
  74. ctx.sessionProjections.register(modelSelectionProjection)
  75. }