{"version":3,"file":"transcription.cjs","names":["getContext","getTestId","matchFixtureDiagnostic","applyChaos","flattenHeaders","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","resolveResponse","isErrorResponse","serializeErrorResponse","isTranscriptionResponse"],"sources":["../src/transcription.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n  isTranscriptionResponse,\n  isErrorResponse,\n  serializeErrorResponse,\n  flattenHeaders,\n  getTestId,\n  resolveResponse,\n  resolveStrictMode,\n  strictOverrideField,\n  getContext,\n  strictNoMatchMessage,\n  strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n/**\n * Extract the multipart boundary string from a Content-Type header.\n */\nexport function extractBoundary(contentType: string | undefined): string | undefined {\n  if (!contentType) return undefined;\n  const match = contentType.match(/boundary=([^\\s;]+)/i);\n  return match?.[1];\n}\n\n/**\n * Extract a text field from multipart form data using boundary-based parsing.\n * Splits the body by the multipart boundary so each part is isolated, then\n * checks each part's Content-Disposition header for the target field name.\n * This avoids false matches from binary audio data that might contain\n * header-like byte sequences.\n */\nexport function extractFormField(\n  raw: string,\n  fieldName: string,\n  boundary: string | undefined,\n): string | undefined {\n  if (!boundary) {\n    // Fallback: no boundary available, use simple regex (best-effort)\n    console.warn(\"extractFormField: no multipart boundary found, using best-effort regex fallback\");\n    const escaped = fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n    const pattern = new RegExp(\n      `Content-Disposition:\\\\s*form-data;[^\\\\r\\\\n]*name=\"${escaped}\"[^\\\\r\\\\n]*\\\\r\\\\n\\\\r\\\\n([^\\\\r\\\\n]*)`,\n      \"i\",\n    );\n    const match = raw.match(pattern);\n    return match?.[1];\n  }\n\n  // Split by boundary delimiter — each chunk is one part\n  const delimiter = `--${boundary}`;\n  const parts = raw.split(delimiter);\n\n  for (const part of parts) {\n    // Skip the preamble (before first boundary) and epilogue (after closing boundary)\n    if (!part || part.trimStart().startsWith(\"--\")) continue;\n\n    // Split part into headers and body at the first blank line (\\r\\n\\r\\n)\n    const headerEnd = part.indexOf(\"\\r\\n\\r\\n\");\n    if (headerEnd === -1) continue;\n\n    const headers = part.slice(0, headerEnd);\n    const body = part.slice(headerEnd + 4);\n\n    // Check if this part's Content-Disposition names the target field\n    const cdMatch = headers.match(/Content-Disposition:\\s*form-data;[^\\r\\n]*name=\"([^\"]+)\"/i);\n    if (cdMatch && cdMatch[1] === fieldName) {\n      // Return the body value, trimming trailing \\r\\n from the part boundary\n      return body.replace(/\\r\\n$/, \"\");\n    }\n  }\n  return undefined;\n}\n\nexport async function handleTranscription(\n  req: http.IncomingMessage,\n  res: http.ServerResponse,\n  raw: string,\n  fixtures: Fixture[],\n  journal: Journal,\n  defaults: HandlerDefaults,\n  setCorsHeaders: (res: http.ServerResponse) => void,\n  endpointType: \"transcription\" | \"translation\" = \"transcription\",\n): Promise<void> {\n  setCorsHeaders(res);\n  const defaultPath =\n    endpointType === \"translation\" ? \"/v1/audio/translations\" : \"/v1/audio/transcriptions\";\n  const path = req.url ?? defaultPath;\n  const method = req.method ?? \"POST\";\n\n  const contentType = Array.isArray(req.headers[\"content-type\"])\n    ? req.headers[\"content-type\"][0]\n    : req.headers[\"content-type\"];\n  const boundary = extractBoundary(contentType);\n\n  const model = extractFormField(raw, \"model\", boundary) ?? \"whisper-1\";\n  const responseFormat = extractFormField(raw, \"response_format\", boundary) ?? \"json\";\n\n  const syntheticReq: ChatCompletionRequest = {\n    model,\n    messages: [],\n    _endpointType: endpointType,\n    _context: getContext(req),\n  };\n\n  const testId = getTestId(req);\n  const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n    fixtures,\n    syntheticReq,\n    journal.getFixtureMatchCountsForTest(testId),\n    defaults.requestTransform,\n  );\n\n  if (fixture) {\n    journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n    defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n  } else {\n    defaults.logger.debug(`No fixture matched for request`);\n  }\n\n  if (\n    applyChaos(\n      res,\n      fixture,\n      defaults.chaos,\n      req.headers,\n      journal,\n      { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n      fixture ? \"fixture\" : \"proxy\",\n      defaults.registry,\n      defaults.logger,\n    )\n  )\n    return;\n\n  if (!fixture) {\n    const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n    if (effectiveStrict) {\n      const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n      defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n      journal.add({\n        method,\n        path,\n        headers: flattenHeaders(req.headers),\n        body: syntheticReq,\n        response: {\n          status: 503,\n          fixture: null,\n          ...strictOverrideField(defaults.strict, req.headers),\n        },\n      });\n      writeErrorResponse(\n        res,\n        503,\n        JSON.stringify({\n          error: {\n            message: strictMessage,\n            type: \"invalid_request_error\",\n            code: \"no_fixture_match\",\n          },\n        }),\n      );\n      return;\n    }\n    if (defaults.record) {\n      const outcome = await proxyAndRecord(\n        req,\n        res,\n        syntheticReq,\n        \"openai\",\n        req.url ?? defaultPath,\n        fixtures,\n        defaults,\n        raw,\n      );\n      if (outcome === \"handled_by_hook\") return;\n      if (outcome !== \"not_configured\") {\n        journal.add({\n          method,\n          path,\n          headers: flattenHeaders(req.headers),\n          body: syntheticReq,\n          response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n        });\n        return;\n      }\n    }\n\n    journal.add({\n      method,\n      path,\n      headers: flattenHeaders(req.headers),\n      body: syntheticReq,\n      response: {\n        status: 404,\n        fixture: null,\n        ...strictOverrideField(defaults.strict, req.headers),\n      },\n    });\n    writeErrorResponse(\n      res,\n      404,\n      JSON.stringify({\n        error: {\n          message: \"No fixture matched\",\n          type: \"invalid_request_error\",\n          code: \"no_fixture_match\",\n        },\n      }),\n    );\n    return;\n  }\n\n  const response = await resolveResponse(fixture, syntheticReq);\n\n  if (isErrorResponse(response)) {\n    const status = response.status ?? 500;\n    journal.add({\n      method,\n      path,\n      headers: flattenHeaders(req.headers),\n      body: syntheticReq,\n      response: { status, fixture },\n    });\n    writeErrorResponse(res, status, serializeErrorResponse(response), {\n      retryAfter: response.retryAfter,\n    });\n    return;\n  }\n\n  if (!isTranscriptionResponse(response)) {\n    journal.add({\n      method,\n      path,\n      headers: flattenHeaders(req.headers),\n      body: syntheticReq,\n      response: { status: 500, fixture },\n    });\n    writeErrorResponse(\n      res,\n      500,\n      JSON.stringify({\n        error: {\n          message: \"Fixture response is not a transcription type\",\n          type: \"server_error\",\n        },\n      }),\n    );\n    return;\n  }\n\n  journal.add({\n    method,\n    path,\n    headers: flattenHeaders(req.headers),\n    body: syntheticReq,\n    response: { status: 200, fixture },\n  });\n\n  const t = response.transcription;\n  const useVerbose = responseFormat === \"verbose_json\" || t.words != null || t.segments != null;\n\n  if (useVerbose) {\n    const verboseBody: Record<string, unknown> = {\n      task: endpointType === \"translation\" ? \"translate\" : \"transcribe\",\n      language: t.language ?? \"english\",\n      duration: t.duration ?? 0,\n      text: t.text,\n    };\n    if (t.words && t.words.length > 0) {\n      verboseBody.words = t.words;\n    }\n    if (t.segments && t.segments.length > 0) {\n      verboseBody.segments = t.segments;\n    }\n    res.writeHead(200, { \"Content-Type\": \"application/json\" });\n    res.end(JSON.stringify(verboseBody));\n  } else {\n    res.writeHead(200, { \"Content-Type\": \"application/json\" });\n    res.end(JSON.stringify({ text: t.text }));\n  }\n}\n"],"mappings":"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"}