@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
99 lines (98 loc) • 4.66 kB
TypeScript
/**
* Google `generateContent` wire format.
*
* The Gemini CLI honours `GOOGLE_GEMINI_BASE_URL`, so it can be pointed at this
* proxy with no vendor cooperation — verified live: with the variable set the
* CLI reached the proxy and failed with `ModelNotFoundError: 404`, which is the
* right answer from a proxy that had no route to answer it. This module is the
* missing half.
*
* Three differences from the Claude and OpenAI shapes drive everything here:
*
* - Roles are `user` / `model`, not `user` / `assistant`.
* - The system prompt is a sibling `systemInstruction`, not a turn.
* - Generation settings nest under `generationConfig`.
*
* Token counts also have their own names — `promptTokenCount`,
* `candidatesTokenCount`, `totalTokenCount` — and the CLI reads them to render
* its own usage line, so getting them wrong is visible to the user rather than
* merely wrong in a log.
*/
import type { ParsedGeminiRequest, StreamSerializerAdapter } from "../types/index.js";
/**
* Parse a `generateContent` body into the shape the translation engine takes.
*
* The final user turn becomes `prompt`, with Google's `model` role mapped to
* `assistant` so downstream providers see a role they understand.
*
* `conversationMessages` carries EVERY turn, the final one included. That
* looks redundant next to `prompt`, and it is the contract the shared engine
* expects: `buildTranslationOptions` does `conversationMessages.slice(0, -1)`
* to derive history, because the final turn is already being sent as `prompt`.
* `claudeFormat` and `openaiFormat` both push unconditionally for that reason.
* Excluding the last turn here — the intuitive reading of "history" — made the
* engine's slice eat one real turn instead, so every multi-turn Gemini request
* silently lost its most recent message.
*/
export declare function parseGeminiRequest(model: string, body: Record<string, unknown>, stream: boolean): ParsedGeminiRequest;
/** Build a complete `generateContent` response body. */
/**
* Render a tool call as text.
*
* The proxy does not forward tool calls in Google's `functionCall` part shape:
* the CLI drives tools locally, so a `functionCall` it never asked for would be
* an unresolvable pending call. Text is what it can act on. Both the streaming
* serializer and the non-streaming builder go through here so the two paths
* cannot drift.
*/
export declare function renderGeminiToolUse(name: string, input: unknown): string;
export declare function buildGeminiResponse(text: string, finishReason: string, usage: {
input: number;
output: number;
total: number;
}, modelVersion: string, toolCalls?: ReadonlyArray<{
toolName: string;
args: Record<string, unknown>;
}>): Record<string, unknown>;
/** Google's error envelope, which the CLI parses to classify failures. */
export declare function buildGeminiErrorResponse(status: number, message: string, statusText?: string): Response;
/**
* SSE serializer for `streamGenerateContent?alt=sse`.
*
* Google streams whole `GenerateContentResponse` objects, one per `data:`
* frame, rather than the deltas Claude and OpenAI send. Each frame therefore
* carries a complete `candidates[0].content.parts[0].text` holding only that
* chunk's text — the CLI concatenates them — and the final frame is the one
* that carries `finishReason` and `usageMetadata`.
*/
export declare class GeminiStreamSerializer {
private readonly model;
constructor(model: string);
private frame;
/** Google sends no preamble frame; the first delta is the first frame. */
start(): string[];
pushDelta(text: string): string[];
/**
* Tool calls are surfaced as text.
*
* The translation engine can emit a tool call, but the Gemini CLI drives its
* own tools locally and does not expect `functionCall` parts from a plain
* `generateContent`. Emitting one would make the CLI wait for a tool result
* that is never coming; rendering it as text keeps the turn terminating.
*/
pushToolUse(_id: string, name: string, input: unknown): string[];
finish(finishReason: string, usage: {
input: number;
output: number;
total: number;
}): string[];
/**
* Errors ride the stream as a Google error object.
*
* Once headers are sent the status code is spent, so the CLI can only learn
* about a mid-stream failure from the body.
*/
emitError(message: string): string[];
}
/** Adapter so the translation engine can drive this like the other two. */
export declare function createGeminiSerializerAdapter(model: string): StreamSerializerAdapter;