diff --git a/packages/services/agent-service/src/infrastructure/stt/openai-stt.service.ts b/packages/services/agent-service/src/infrastructure/stt/openai-stt.service.ts index fe34a06..2aa9209 100644 --- a/packages/services/agent-service/src/infrastructure/stt/openai-stt.service.ts +++ b/packages/services/agent-service/src/infrastructure/stt/openai-stt.service.ts @@ -2,8 +2,8 @@ * OpenAISttService * * Calls the OpenAI Whisper transcriptions API to convert audio buffers to text. - * Supports the self-signed proxy at 67.223.119.33:8443 by disabling TLS verification - * when OPENAI_BASE_URL points to an https host (same pattern as voice-agent). + * Supports the self-signed proxy at 67.223.119.33:8443 by disabling TLS + * verification via https.request (Node 18 native fetch ignores https.Agent). */ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @@ -16,12 +16,12 @@ export class OpenAISttService { private readonly baseUrl: string; constructor(private readonly configService: ConfigService) { - this.apiKey = this.configService.get('OPENAI_API_KEY', ''); // Strip trailing slash and /v1 suffix — we always append /v1/... - this.baseUrl = this.configService.get( - 'OPENAI_BASE_URL', - 'https://api.openai.com', - ).replace(/\/$/, '').replace(/\/v1$/, ''); + this.baseUrl = this.configService + .get('OPENAI_BASE_URL', 'https://api.openai.com') + .replace(/\/$/, '') + .replace(/\/v1$/, ''); + this.apiKey = this.configService.get('OPENAI_API_KEY', ''); } /** @@ -37,7 +37,9 @@ export class OpenAISttService { language = 'zh', ): Promise { const url = `${this.baseUrl}/v1/audio/transcriptions`; - this.logger.log(`STT: transcribing ${filename} (${audioBuffer.length} bytes) → ${url}`); + this.logger.log( + `STT: transcribing ${filename} (${audioBuffer.length} bytes) → ${url}`, + ); // Build multipart/form-data manually to avoid external dependencies const boundary = `----FormBoundary${Date.now().toString(16)}`; @@ -66,27 +68,51 @@ export class OpenAISttService { const body = Buffer.concat(parts); - // Use a custom https agent to allow self-signed certs (proxy at 67.223.119.33:8443) - const agent = new https.Agent({ rejectUnauthorized: false }); - - const response = await fetch(url, { - method: 'POST', - headers: { - Authorization: `Bearer ${this.apiKey}`, - 'Content-Type': `multipart/form-data; boundary=${boundary}`, - 'Content-Length': String(body.length), - }, - body, - // @ts-ignore — undici (Node 18+ native fetch) accepts dispatcher via agent option - agent, + // Use https.request so rejectUnauthorized: false works for the self-signed + // proxy. Node 18 native fetch (undici) does NOT honour https.Agent. + const parsedUrl = new URL(url); + const result = await new Promise<{ text: string }>((resolve, reject) => { + const req = https.request( + { + hostname: parsedUrl.hostname, + port: parsedUrl.port || 443, + path: parsedUrl.pathname + parsedUrl.search, + method: 'POST', + headers: { + Authorization: `Bearer ${this.apiKey}`, + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'Content-Length': body.length, + }, + rejectUnauthorized: false, + }, + (res) => { + const chunks: Buffer[] = []; + res.on('data', (chunk: Buffer) => chunks.push(chunk)); + res.on('end', () => { + const text = Buffer.concat(chunks).toString('utf8'); + if (res.statusCode && res.statusCode >= 400) { + reject( + new Error( + `Whisper STT failed: HTTP ${res.statusCode} — ${text}`, + ), + ); + } else { + try { + resolve(JSON.parse(text) as { text: string }); + } catch { + reject( + new Error(`Whisper STT: invalid JSON response: ${text}`), + ); + } + } + }); + }, + ); + req.on('error', reject); + req.write(body); + req.end(); }); - if (!response.ok) { - const errText = await response.text().catch(() => ''); - throw new Error(`Whisper STT failed: HTTP ${response.status} — ${errText}`); - } - - const result = (await response.json()) as { text: string }; this.logger.log(`STT result: "${result.text?.slice(0, 80)}"`); return result.text ?? ''; }