Files
gods-eye-view/server/providers/common/textTools.js
T
manjunath22466andDarren Dignam af461febae feat(voice): add typed AI chat beside voice
Chat sits next to the mic in the command dock. Replies are silent and never
request the microphone; the first message connects with the selected provider
and runs the same map actions as voice through each provider's text API
(OpenAI OPENAI_CHAT_MODEL, Gemini GEMINI_CHAT_MODEL via generateContent, which
never starts Live). Only one mode runs at a time.

- Chat panel rendered by the voice control presenter: conversation, recent and
  quick-command suggestions, Up/Down history, attachments (map-layer
  references or small UTF-8 text files, sent only with the message)
- Chat and voice panels sit above the dock or dock into the right rail's
  column; minimized bars sit centred above the dock, and height animates on
  swaps
- A centred panel steps down to its bar for the Location and Visual Presets
  trays without moving focus, rests above an open tray, and docks right while
  expanded over one; it returns to the centre once the trays close
- Minimized voice previews the last sentence from each side; the empty Voice
  Preview suggests things to say
- Settings opened from chat keep chat's frame and show the shared AI provider
  and quick commands; voice Settings show Back to voice
- Closing chat ends a typed session; the voice panel's close turns voice off
- The dock widens its voice control for the chat button and restyles its
  wings, which show icons only at 900px and below; assistant panels share the
  left panels' glass, and Cyber keeps its own surfaces
- Server text-chat handlers with body, time and per-IP limits
  (GEV_RATELIMIT_*_CHAT_PER_MIN), documented in SECURITY.md; tests and all
  eleven locales

Co-authored-by: Darren Dignam <[email protected]>
2026-11-02 20:56:39 +05:30

55 lines
1.8 KiB
JavaScript

import { validateValue } from '../../../src/tools/schema.js';
export function invalidCalls(calls, tools) {
const schemas = new Map(tools.map((tool) => [tool.name, tool.parameters]));
return calls.flatMap((call, index) => {
const schema = schemas.get(call.name);
const errors = !schema
? ['Unknown tool']
: call.parseError
? ['Arguments must be valid JSON']
: validateValue(schema, call.args === undefined ? {} : call.args);
return errors.length
? [{ index, error: errors.slice(0, 4).join('; ').slice(0, 1000) }]
: [];
});
}
// Missing usage stays unknown, never an invented zero-cost request.
export function parseUsage(provider, data) {
const raw = provider === 'gemini' ? data?.usageMetadata : data?.usage;
const number = (value) =>
Number.isSafeInteger(value) && value >= 0 ? value : null;
const inputTokens = number(
provider === 'gemini' ? raw?.promptTokenCount : raw?.prompt_tokens,
);
const candidateTokens = number(
provider === 'gemini' ? raw?.candidatesTokenCount : raw?.completion_tokens,
);
const totalTokens = number(
provider === 'gemini' ? raw?.totalTokenCount : raw?.total_tokens,
);
if (inputTokens === null || candidateTokens === null || totalTokens === null)
return null;
const reasoningTokens =
number(
provider === 'gemini'
? raw?.thoughtsTokenCount
: raw?.completion_tokens_details?.reasoning_tokens,
) ?? 0;
const outputTokens =
candidateTokens + (provider === 'gemini' ? reasoningTokens : 0);
return {
inputTokens,
outputTokens,
totalTokens,
reasoningTokens,
cachedInputTokens:
number(
provider === 'gemini'
? raw?.cachedContentTokenCount
: raw?.prompt_tokens_details?.cached_tokens,
) ?? 0,
};
}