mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-08 04:14:02 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caa0cf6d49 | ||
|
|
d7fac874c1 | ||
|
|
9efe169a6e |
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).
|
||||
@@ -1,3 +0,0 @@
|
||||
- MCP tools resolve place names through the keyless Photon geocoder when the
|
||||
server has no geocoder configured, instead of answering "No place matched"
|
||||
for every name (asturksever, #1603).
|
||||
@@ -5081,8 +5081,7 @@
|
||||
"src/layers/alpr/records.js",
|
||||
"src/layers/alpr/tileRecords.js",
|
||||
"src/sources/infrastructureData.js",
|
||||
"src/sources/eventPacks.js",
|
||||
"src/keylessGeocoder.js"
|
||||
"src/sources/eventPacks.js"
|
||||
],
|
||||
"external": ["@mapbox/vector-tile", "pbf"]
|
||||
},
|
||||
|
||||
@@ -224,7 +224,12 @@ function createRealtimeTokenHandler({
|
||||
if (!bearer) {
|
||||
res.statusCode = 503;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,10 @@ test('the installed keyless HUD route stays successful after the voice quota is
|
||||
const firstToken = await invokeRoute(token);
|
||||
const secondToken = await invokeRoute(token);
|
||||
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);
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
|
||||
+1
-19
@@ -24,7 +24,6 @@ export function createGeocodePlaceService({
|
||||
cacheMs = 10 * 60_000,
|
||||
timeoutMs = 15_000,
|
||||
now = () => Date.now(),
|
||||
keylessGeocoder = null,
|
||||
} = {}) {
|
||||
// Recent answers by name, so tools that combine others resolve a place to
|
||||
// the same point once instead of once per section. A pending lookup is
|
||||
@@ -34,17 +33,8 @@ export function createGeocodePlaceService({
|
||||
async function lookup(name, signal) {
|
||||
const query = new URLSearchParams({ q: name });
|
||||
const response = await fetchImpl(`/api/geocode?${query}`, { signal });
|
||||
const payload = await response.json().catch(() => null);
|
||||
// No server geocoder (no Google key, no Nominatim): ask the keyless one
|
||||
// the search box uses, rather than reporting every name as unmatched.
|
||||
if (
|
||||
keylessGeocoder &&
|
||||
(payload?.configured === false || payload?.code === 'NOMINATIM_DISABLED')
|
||||
) {
|
||||
const { place } = await keylessGeocoder.geocode(name, { signal });
|
||||
return place ? placeFromKeylessResult(place) : null;
|
||||
}
|
||||
if (!response.ok) throw new Error(`Geocode HTTP ${response.status}`);
|
||||
const payload = await response.json();
|
||||
return placeFromGeocodeResult(payload?.results?.[0]);
|
||||
}
|
||||
function start(key, name) {
|
||||
@@ -110,14 +100,6 @@ export function createGeocodePlaceService({
|
||||
}
|
||||
|
||||
/** Convert one geocode result to a place with bounds, or null. */
|
||||
/** A keyless (Photon) answer in the same shape as a geocode result's. */
|
||||
function placeFromKeylessResult({ lat, lng, label, name, viewport }) {
|
||||
return placeFromGeocodeResult({
|
||||
formatted_address: label || name || null,
|
||||
geometry: { location: { lat, lng }, ...(viewport ? { viewport } : {}) },
|
||||
});
|
||||
}
|
||||
|
||||
export function placeFromGeocodeResult(result) {
|
||||
const location = result?.geometry?.location;
|
||||
if (!Number.isFinite(location?.lat) || !Number.isFinite(location?.lng))
|
||||
|
||||
@@ -93,49 +93,3 @@ test('a shared lookup outlives one cancelled caller, and times out on its own',
|
||||
await places.resolve('Slow').catch(() => {});
|
||||
assert.equal(signals.length, 3, 'a timed-out lookup is not cached');
|
||||
});
|
||||
|
||||
test('without a server geocoder, names resolve through the keyless geocoder', async () => {
|
||||
const asked = [];
|
||||
const places = createGeocodePlaceService({
|
||||
fetchImpl: async () =>
|
||||
Response.json(
|
||||
{
|
||||
error: 'Place search is not configured',
|
||||
code: 'NOMINATIM_DISABLED',
|
||||
retryable: false,
|
||||
},
|
||||
{ status: 503 },
|
||||
),
|
||||
keylessGeocoder: {
|
||||
geocode: async (name) => {
|
||||
asked.push(name);
|
||||
return name === 'Paris'
|
||||
? {
|
||||
place: {
|
||||
lat: 48.8566,
|
||||
lng: 2.3522,
|
||||
name: 'Paris',
|
||||
label: 'Paris, France',
|
||||
viewport: {
|
||||
southwest: { lat: 48.81, lng: 2.22 },
|
||||
northeast: { lat: 48.9, lng: 2.47 },
|
||||
},
|
||||
},
|
||||
answered: true,
|
||||
}
|
||||
: { place: null, answered: true };
|
||||
},
|
||||
},
|
||||
});
|
||||
const paris = await places.resolve('Paris');
|
||||
assert.equal(paris.name, 'Paris, France');
|
||||
assert.deepEqual(paris.point, { lat: 48.8566, lon: 2.3522 });
|
||||
assert.deepEqual(paris.bounds, {
|
||||
west: 2.22,
|
||||
south: 48.81,
|
||||
east: 2.47,
|
||||
north: 48.9,
|
||||
});
|
||||
assert.equal(await places.resolve('Nowhere-at-all'), null);
|
||||
assert.deepEqual(asked, ['Paris', 'Nowhere-at-all']);
|
||||
});
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
createVesselSource,
|
||||
createFlightSource,
|
||||
} from '../sources/live/standalone.js';
|
||||
import { createPhotonGeocoder } from '../keylessGeocoder.js';
|
||||
import {
|
||||
createGeocodePlaceService,
|
||||
createPlaceSearchService,
|
||||
@@ -87,10 +86,7 @@ export function createToolServices({ fetchImpl, appUrl, panelKey }) {
|
||||
fetchImpl,
|
||||
tileFetchImpl: fetchImpl,
|
||||
}),
|
||||
places: createGeocodePlaceService({
|
||||
fetchImpl,
|
||||
keylessGeocoder: createPhotonGeocoder({ fetchImpl }),
|
||||
}),
|
||||
places: createGeocodePlaceService({ fetchImpl }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ const frameStyle = (panel) => ({
|
||||
<button class="gev-voice-error-dismiss" type="button" @click="controls?.dismissError()">{{ t('DISMISS') }}</button>
|
||||
</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>
|
||||
</template>
|
||||
@@ -107,7 +107,7 @@ function initialState(panel) {
|
||||
t('Hold Space to speak · tap Space to activate focused controls'),
|
||||
},
|
||||
// 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.
|
||||
phase: { text: '' },
|
||||
voicePanel: {
|
||||
|
||||
@@ -39,8 +39,9 @@ export function createRealtimeBackend({
|
||||
if (!response.ok) {
|
||||
const reason =
|
||||
typeof data?.error === 'string' ? data.error : data?.error?.message;
|
||||
throw new Error(
|
||||
reason || `Realtime token failed: HTTP ${response.status}`,
|
||||
throw Object.assign(
|
||||
new Error(reason || `Realtime token failed: HTTP ${response.status}`),
|
||||
typeof data?.code === 'string' ? { code: data.code } : {},
|
||||
);
|
||||
}
|
||||
const token =
|
||||
|
||||
@@ -143,3 +143,17 @@ test('controller lifetime stops pending transport and releases resources through
|
||||
assert.equal(controller.stream, null);
|
||||
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',
|
||||
});
|
||||
});
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from './pointerContext.js';
|
||||
|
||||
import { shouldPauseRadioForVoice } from './realtimeProtocol.js';
|
||||
import { postDebugLog } from './realtimeDiagnostics.js';
|
||||
import { missingVoiceKeyText, postDebugLog } from './realtimeDiagnostics.js';
|
||||
|
||||
export {
|
||||
readStoredVoiceTier,
|
||||
@@ -436,6 +436,9 @@ export class GevRealtimeController extends RealtimeFacade {
|
||||
status === 'error'
|
||||
? 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') {
|
||||
this.setVoiceSpeaker('idle');
|
||||
|
||||
@@ -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_STORAGE_KEY = 'gev-realtime-errors';
|
||||
@@ -83,6 +91,7 @@ function createErrorRecord(source, error, extra = {}) {
|
||||
String(error?.message || '').trim() ||
|
||||
'No browser error message supplied',
|
||||
errorDetail: rtcError?.errorDetail || null,
|
||||
code: typeof rtcError?.code === 'string' ? rtcError.code : null,
|
||||
sctpCauseCode: rtcError?.sctpCauseCode ?? null,
|
||||
receivedAlert: rtcError?.receivedAlert ?? null,
|
||||
sentAlert: rtcError?.sentAlert ?? null,
|
||||
@@ -91,6 +100,8 @@ function createErrorRecord(source, error, extra = {}) {
|
||||
}
|
||||
|
||||
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 state = [
|
||||
record.errorDetail && `detail=${record.errorDetail}`,
|
||||
|
||||
Reference in new issue
Block a user