fix(agent-service): use https.request for Whisper STT to bypass self-signed cert

Node 18 native fetch (undici) ignores https.Agent, causing fetch failed
on the self-signed proxy at 67.223.119.33:8443. Switch to https.request
with rejectUnauthorized: false which works reliably.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-06 07:51:37 -08:00
parent 72584182df
commit 947a47869e
1 changed files with 53 additions and 27 deletions

View File

@ -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<string>('OPENAI_API_KEY', '');
// Strip trailing slash and /v1 suffix — we always append /v1/...
this.baseUrl = this.configService.get<string>(
'OPENAI_BASE_URL',
'https://api.openai.com',
).replace(/\/$/, '').replace(/\/v1$/, '');
this.baseUrl = this.configService
.get<string>('OPENAI_BASE_URL', 'https://api.openai.com')
.replace(/\/$/, '')
.replace(/\/v1$/, '');
this.apiKey = this.configService.get<string>('OPENAI_API_KEY', '');
}
/**
@ -37,7 +37,9 @@ export class OpenAISttService {
language = 'zh',
): Promise<string> {
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 ?? '';
}