Compare commits

...
9 changed files with 48 additions and 7 deletions

No files matched your search

@@ -0,0 +1,4 @@
- Without `OPENAI_API_KEY`, the mic now says the key is missing and to add it in
Provider Settings, instead of showing the raw server error with a hint to
check microphone permissions. `/api/realtime/token` returns
`code: "API_KEY_MISSING"` with its 503 (asturksever, #1599).
+6 -1
View File
@@ -224,7 +224,12 @@ function createRealtimeTokenHandler({
if (!bearer) { if (!bearer) {
res.statusCode = 503; res.statusCode = 503;
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'OPENAI_API_KEY is not set' })); res.end(
JSON.stringify({
error: 'OPENAI_API_KEY is not set',
code: 'API_KEY_MISSING',
}),
);
return; return;
} }
} }
+4 -1
View File
@@ -150,7 +150,10 @@ test('the installed keyless HUD route stays successful after the voice quota is
const firstToken = await invokeRoute(token); const firstToken = await invokeRoute(token);
const secondToken = await invokeRoute(token); const secondToken = await invokeRoute(token);
assert.equal(firstToken.statusCode, 503); assert.equal(firstToken.statusCode, 503);
assert.deepEqual(firstToken.body, { error: 'OPENAI_API_KEY is not set' }); assert.deepEqual(firstToken.body, {
error: 'OPENAI_API_KEY is not set',
code: 'API_KEY_MISSING',
});
assert.equal(secondToken.statusCode, 429); assert.equal(secondToken.statusCode, 429);
for (let attempt = 0; attempt < 2; attempt += 1) { for (let attempt = 0; attempt < 2; attempt += 1) {
+1 -1
View File
@@ -166,7 +166,7 @@ const frameStyle = (panel) => ({
<button class="gev-voice-error-dismiss" type="button" @click="controls?.dismissError()">{{ t('DISMISS') }}</button> <button class="gev-voice-error-dismiss" type="button" @click="controls?.dismissError()">{{ t('DISMISS') }}</button>
</div> </div>
<div id="gev-voice-error-detail">{{ view.error.text }}</div> <div id="gev-voice-error-detail">{{ view.error.text }}</div>
<div class="gev-voice-error-hint" :hidden="view.error.typed">{{ t('Check microphone permission and network access, then try again.') }}</div> <div class="gev-voice-error-hint" :hidden="view.error.typed || view.error.hint === false">{{ t('Check microphone permission and network access, then try again.') }}</div>
</div> </div>
</div> </div>
</template> </template>
+1 -1
View File
@@ -107,7 +107,7 @@ function initialState(panel) {
t('Hold Space to speak · tap Space to activate focused controls'), t('Hold Space to speak · tap Space to activate focused controls'),
}, },
// A typed chat error reads as chat's, without the microphone hint. // A typed chat error reads as chat's, without the microphone hint.
error: { text: '', typed: false }, error: { text: '', typed: false, hint: true },
// Connection and turn state beside the selected mode's indicator. // Connection and turn state beside the selected mode's indicator.
phase: { text: '' }, phase: { text: '' },
voicePanel: { voicePanel: {
+3 -2
View File
@@ -39,8 +39,9 @@ export function createRealtimeBackend({
if (!response.ok) { if (!response.ok) {
const reason = const reason =
typeof data?.error === 'string' ? data.error : data?.error?.message; typeof data?.error === 'string' ? data.error : data?.error?.message;
throw new Error( throw Object.assign(
reason || `Realtime token failed: HTTP ${response.status}`, new Error(reason || `Realtime token failed: HTTP ${response.status}`),
typeof data?.code === 'string' ? { code: data.code } : {},
); );
} }
const token = const token =
+14
View File
@@ -143,3 +143,17 @@ test('controller lifetime stops pending transport and releases resources through
assert.equal(controller.stream, null); assert.equal(controller.stream, null);
assert.equal(controller.isActive(), false); assert.equal(controller.isActive(), false);
}); });
test('a missing server key keeps its code, so the UI can point to Provider Settings', async () => {
const backend = createRealtimeBackend({
tokenTransport: async () =>
Response.json(
{ error: 'OPENAI_API_KEY is not set', code: 'API_KEY_MISSING' },
{ status: 503 },
),
});
await assert.rejects(backend.requestToken(), {
message: 'OPENAI_API_KEY is not set',
code: 'API_KEY_MISSING',
});
});
+4 -1
View File
@@ -18,7 +18,7 @@ import {
} from './pointerContext.js'; } from './pointerContext.js';
import { shouldPauseRadioForVoice } from './realtimeProtocol.js'; import { shouldPauseRadioForVoice } from './realtimeProtocol.js';
import { postDebugLog } from './realtimeDiagnostics.js'; import { missingVoiceKeyText, postDebugLog } from './realtimeDiagnostics.js';
export { export {
readStoredVoiceTier, readStoredVoiceTier,
@@ -436,6 +436,9 @@ export class GevRealtimeController extends RealtimeFacade {
status === 'error' status === 'error'
? resolvedDetail() || t('Voice session could not be started.') ? resolvedDetail() || t('Voice session could not be started.')
: '', : '',
// "Check microphone permission" is wrong advice for a missing key.
hint: () =>
!(status === 'error' && resolvedDetail() === missingVoiceKeyText()),
}); });
if (status === 'idle' || status === 'connecting' || status === 'error') { if (status === 'idle' || status === 'connecting' || status === 'error') {
this.setVoiceSpeaker('idle'); this.setVoiceSpeaker('idle');
+11
View File
@@ -1,3 +1,11 @@
import { t } from '../i18n/index.js';
/** The voice error when the server has no OpenAI key: where to add it. */
export const missingVoiceKeyText = () =>
t('Needs {envVars} — add it in Provider Settings', {
envVars: 'OPENAI_API_KEY',
});
const ERROR_LOG_LIMIT = 30; const ERROR_LOG_LIMIT = 30;
const ERROR_STORAGE_KEY = 'gev-realtime-errors'; const ERROR_STORAGE_KEY = 'gev-realtime-errors';
@@ -83,6 +91,7 @@ function createErrorRecord(source, error, extra = {}) {
String(error?.message || '').trim() || String(error?.message || '').trim() ||
'No browser error message supplied', 'No browser error message supplied',
errorDetail: rtcError?.errorDetail || null, errorDetail: rtcError?.errorDetail || null,
code: typeof rtcError?.code === 'string' ? rtcError.code : null,
sctpCauseCode: rtcError?.sctpCauseCode ?? null, sctpCauseCode: rtcError?.sctpCauseCode ?? null,
receivedAlert: rtcError?.receivedAlert ?? null, receivedAlert: rtcError?.receivedAlert ?? null,
sentAlert: rtcError?.sentAlert ?? null, sentAlert: rtcError?.sentAlert ?? null,
@@ -91,6 +100,8 @@ function createErrorRecord(source, error, extra = {}) {
} }
function formatErrorForDisplay(record) { function formatErrorForDisplay(record) {
// A missing key is a setup step, not a connection fault: say where to add it.
if (record.code === 'API_KEY_MISSING') return missingVoiceKeyText();
const primary = [record.source, record.message].filter(Boolean).join(': '); const primary = [record.source, record.message].filter(Boolean).join(': ');
const state = [ const state = [
record.errorDetail && `detail=${record.errorDetail}`, record.errorDetail && `detail=${record.errorDetail}`,