mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 15:22:21 +00:00
fix(voice): keyless Gemini voice drops the microphone hint
The Gemini token route now answers a missing key with code API_KEY_MISSING, the session carries it to the error state as missingKey, and the voice control hides "Check microphone permission" for that error, as it already does for OpenAI. The OpenAI controller sends the same flag, so the shared control and the controller agree whichever updates last.
This commit is contained in:
1 parent
9560a6dcd1
commit
b89f81b729
8 files changed
+74
-12
No files matched your search
@@ -251,7 +251,12 @@ test('mint locks canonical action schemas and server instructions with no perman
|
||||
|
||||
test('missing key and malformed model fail without an upstream request', async (t) => {
|
||||
const missing = await fixture(t, { resolveApiKey: () => '' });
|
||||
assert.equal((await missing.send()).status, 503);
|
||||
const missingResponse = await missing.send();
|
||||
assert.equal(missingResponse.status, 503);
|
||||
assert.deepEqual(await missingResponse.json(), {
|
||||
error: 'Add a Gemini API key in Provider Settings',
|
||||
code: 'API_KEY_MISSING',
|
||||
});
|
||||
assert.equal(missing.calls.length, 0);
|
||||
for (const model of [
|
||||
'',
|
||||
|
||||
@@ -359,7 +359,11 @@ export class GeminiSessionController {
|
||||
);
|
||||
}
|
||||
|
||||
setStatus(status, detail, recovery) {
|
||||
/**
|
||||
* @param {{missingKey?: boolean}} [options] True when the server has no
|
||||
* Gemini key: the control then drops its microphone hint.
|
||||
*/
|
||||
setStatus(status, detail, recovery, { missingKey = false } = {}) {
|
||||
this.status = status;
|
||||
if (status === 'listening' && this.input.pushToTalkMode)
|
||||
detail = this.input.pushToTalkKeyHeld
|
||||
@@ -371,6 +375,7 @@ export class GeminiSessionController {
|
||||
detail: translateKnown(detail),
|
||||
};
|
||||
if (recovery) event.recovery = translateKnown(recovery);
|
||||
if (missingKey) event.missingKey = true;
|
||||
this.emit(event);
|
||||
this.input.updateVoiceButtonLabel();
|
||||
if (['idle', 'connecting', 'error'].includes(status))
|
||||
@@ -584,12 +589,15 @@ export class GeminiSessionController {
|
||||
const data = await readResponseJsonCapped(response, 256 * 1024, signal);
|
||||
signal.throwIfAborted();
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
typeof data?.error === 'string'
|
||||
? tokenErrorMessage(data.error)
|
||||
: t('Gemini token request failed (HTTP {status})', {
|
||||
status: response.status,
|
||||
}),
|
||||
throw Object.assign(
|
||||
new Error(
|
||||
typeof data?.error === 'string'
|
||||
? tokenErrorMessage(data.error)
|
||||
: t('Gemini token request failed (HTTP {status})', {
|
||||
status: response.status,
|
||||
}),
|
||||
),
|
||||
typeof data?.code === 'string' ? { code: data.code } : {},
|
||||
);
|
||||
if (
|
||||
typeof data?.model !== 'string' ||
|
||||
@@ -966,7 +974,9 @@ export class GeminiSessionController {
|
||||
: error?.message || msg('Gemini voice could not start');
|
||||
this.lastError = detail;
|
||||
this.stop({ preserveStatus: true });
|
||||
this.setStatus('error', detail, recovery);
|
||||
this.setStatus('error', detail, recovery, {
|
||||
missingKey: error?.code === 'API_KEY_MISSING',
|
||||
});
|
||||
}
|
||||
|
||||
abortTools() {
|
||||
|
||||
@@ -3638,3 +3638,21 @@ test('cancelling a queued globe reset also skips its dependent rotation', async
|
||||
h.c.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test('a missing server key reaches the error state flagged, other token failures do not', async () => {
|
||||
for (const [body, missingKey] of [
|
||||
[{ error: 'Add a Gemini API key in Provider Settings', code: 'API_KEY_MISSING' }, true],
|
||||
[{ error: 'Gemini quota is unavailable. Try again later.' }, undefined],
|
||||
]) {
|
||||
const h = harness({
|
||||
fetchImpl: async () => Response.json(body, { status: 503 }),
|
||||
});
|
||||
await h.c.start();
|
||||
const error = h.events.filter(
|
||||
(event) => event.type === 'state' && event.state === 'error',
|
||||
).at(-1);
|
||||
assert.equal(error?.detail, body.error);
|
||||
assert.equal(error?.missingKey, missingKey);
|
||||
assert.equal(h.sockets.length, 0);
|
||||
}
|
||||
});
|
||||
@@ -871,3 +871,22 @@ test('a provider switch hands chat Settings to the new mount', async (t) => {
|
||||
assert.deepEqual(f.mounts[1].options.presentation, shown);
|
||||
assert.equal(f.facade.provider, next);
|
||||
});
|
||||
|
||||
test('a missing-key error hides the microphone hint; other errors keep it', async (t) => {
|
||||
const f = compositionFixture(t, {});
|
||||
f.select('gemini');
|
||||
await settle();
|
||||
const instance = f.instances.at(-1);
|
||||
const ui = f.views.at(-1);
|
||||
instance.hooks.emit({
|
||||
type: 'state',
|
||||
state: 'error',
|
||||
detail: 'Add a Gemini API key in Provider Settings',
|
||||
missingKey: true,
|
||||
});
|
||||
assert.equal(ui.state.error.text, 'Add a Gemini API key in Provider Settings');
|
||||
assert.equal(ui.state.error.hint, false);
|
||||
instance.hooks.emit({ type: 'state', state: 'error', detail: 'Socket closed' });
|
||||
assert.equal(ui.state.error.text, 'Socket closed');
|
||||
assert.equal(ui.state.error.hint, true);
|
||||
});
|
||||
@@ -416,7 +416,12 @@ export class GevRealtimeController extends RealtimeFacade {
|
||||
*/
|
||||
setStatus(status, detail, { missingKey = false } = {}) {
|
||||
this.status = status;
|
||||
this.emitSessionEvent({ type: 'state', state: status, detail });
|
||||
this.emitSessionEvent({
|
||||
type: 'state',
|
||||
state: status,
|
||||
detail,
|
||||
...(missingKey && { missingKey: true }),
|
||||
});
|
||||
this.ui.update('root', {
|
||||
status,
|
||||
...(status === 'error' && { errorDismissed: false }),
|
||||
|
||||
@@ -129,6 +129,8 @@ export function createVoiceCommands({
|
||||
event.state === 'error'
|
||||
? event.detail || t('Voice could not be started.')
|
||||
: '',
|
||||
// "Check microphone permission" is wrong advice for a missing key.
|
||||
hint: () => !(event.state === 'error' && event.missingKey === true),
|
||||
});
|
||||
});
|
||||
// Captions, plan and result card nested in the control; it only reads
|
||||
|
||||
Reference in new issue
Block a user