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:
parent
72584182df
commit
947a47869e
|
|
@ -2,8 +2,8 @@
|
||||||
* OpenAISttService
|
* OpenAISttService
|
||||||
*
|
*
|
||||||
* Calls the OpenAI Whisper transcriptions API to convert audio buffers to text.
|
* 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
|
* Supports the self-signed proxy at 67.223.119.33:8443 by disabling TLS
|
||||||
* when OPENAI_BASE_URL points to an https host (same pattern as voice-agent).
|
* verification via https.request (Node 18 native fetch ignores https.Agent).
|
||||||
*/
|
*/
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
@ -16,12 +16,12 @@ export class OpenAISttService {
|
||||||
private readonly baseUrl: string;
|
private readonly baseUrl: string;
|
||||||
|
|
||||||
constructor(private readonly configService: ConfigService) {
|
constructor(private readonly configService: ConfigService) {
|
||||||
this.apiKey = this.configService.get<string>('OPENAI_API_KEY', '');
|
|
||||||
// Strip trailing slash and /v1 suffix — we always append /v1/...
|
// Strip trailing slash and /v1 suffix — we always append /v1/...
|
||||||
this.baseUrl = this.configService.get<string>(
|
this.baseUrl = this.configService
|
||||||
'OPENAI_BASE_URL',
|
.get<string>('OPENAI_BASE_URL', 'https://api.openai.com')
|
||||||
'https://api.openai.com',
|
.replace(/\/$/, '')
|
||||||
).replace(/\/$/, '').replace(/\/v1$/, '');
|
.replace(/\/v1$/, '');
|
||||||
|
this.apiKey = this.configService.get<string>('OPENAI_API_KEY', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -37,7 +37,9 @@ export class OpenAISttService {
|
||||||
language = 'zh',
|
language = 'zh',
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const url = `${this.baseUrl}/v1/audio/transcriptions`;
|
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
|
// Build multipart/form-data manually to avoid external dependencies
|
||||||
const boundary = `----FormBoundary${Date.now().toString(16)}`;
|
const boundary = `----FormBoundary${Date.now().toString(16)}`;
|
||||||
|
|
@ -66,27 +68,51 @@ export class OpenAISttService {
|
||||||
|
|
||||||
const body = Buffer.concat(parts);
|
const body = Buffer.concat(parts);
|
||||||
|
|
||||||
// Use a custom https agent to allow self-signed certs (proxy at 67.223.119.33:8443)
|
// Use https.request so rejectUnauthorized: false works for the self-signed
|
||||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
// proxy. Node 18 native fetch (undici) does NOT honour https.Agent.
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
const response = await fetch(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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.apiKey}`,
|
Authorization: `Bearer ${this.apiKey}`,
|
||||||
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
||||||
'Content-Length': String(body.length),
|
'Content-Length': body.length,
|
||||||
},
|
},
|
||||||
body,
|
rejectUnauthorized: false,
|
||||||
// @ts-ignore — undici (Node 18+ native fetch) accepts dispatcher via agent option
|
},
|
||||||
agent,
|
(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)}"`);
|
this.logger.log(`STT result: "${result.text?.slice(0, 80)}"`);
|
||||||
return result.text ?? '';
|
return result.text ?? '';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue