inboundMessages.ts 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. import type {
  2. Base64ImageSource,
  3. ContentBlockParam,
  4. ImageBlockParam,
  5. } from '@anthropic-ai/sdk/resources/messages.mjs'
  6. import type { UUID } from 'crypto'
  7. import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
  8. import { detectImageFormatFromBase64 } from '../utils/imageResizer.js'
  9. /**
  10. * Process an inbound user message from the bridge, extracting content
  11. * and UUID for enqueueing. Supports both string content and
  12. * ContentBlockParam[] (e.g. messages containing images).
  13. *
  14. * Normalizes image blocks from bridge clients that may use camelCase
  15. * `mediaType` instead of snake_case `media_type` (mobile-apps#5825).
  16. *
  17. * Returns the extracted fields, or undefined if the message should be
  18. * skipped (non-user type, missing/empty content).
  19. */
  20. export function extractInboundMessageFields(
  21. msg: SDKMessage,
  22. ):
  23. | { content: string | Array<ContentBlockParam>; uuid: UUID | undefined }
  24. | undefined {
  25. if (msg.type !== 'user') return undefined
  26. const content = msg.message?.content
  27. if (!content) return undefined
  28. if (Array.isArray(content) && content.length === 0) return undefined
  29. const uuid =
  30. 'uuid' in msg && typeof msg.uuid === 'string'
  31. ? (msg.uuid as UUID)
  32. : undefined
  33. return {
  34. content: Array.isArray(content) ? normalizeImageBlocks(content) : content,
  35. uuid,
  36. }
  37. }
  38. /**
  39. * Normalize image content blocks from bridge clients. iOS/web clients may
  40. * send `mediaType` (camelCase) instead of `media_type` (snake_case), or
  41. * omit the field entirely. Without normalization, the bad block poisons
  42. * the session — every subsequent API call fails with
  43. * "media_type: Field required".
  44. *
  45. * Fast-path scan returns the original array reference when no
  46. * normalization is needed (zero allocation on the happy path).
  47. */
  48. export function normalizeImageBlocks(
  49. blocks: Array<ContentBlockParam>,
  50. ): Array<ContentBlockParam> {
  51. if (!blocks.some(isMalformedBase64Image)) return blocks
  52. return blocks.map(block => {
  53. if (!isMalformedBase64Image(block)) return block
  54. const src = block.source as unknown as Record<string, unknown>
  55. const mediaType =
  56. typeof src.mediaType === 'string' && src.mediaType
  57. ? src.mediaType
  58. : detectImageFormatFromBase64(block.source.data)
  59. return {
  60. ...block,
  61. source: {
  62. type: 'base64' as const,
  63. media_type: mediaType as Base64ImageSource['media_type'],
  64. data: block.source.data,
  65. },
  66. }
  67. })
  68. }
  69. function isMalformedBase64Image(
  70. block: ContentBlockParam,
  71. ): block is ImageBlockParam & { source: Base64ImageSource } {
  72. if (block.type !== 'image' || block.source?.type !== 'base64') return false
  73. return !(block.source as unknown as Record<string, unknown>).media_type
  74. }