Linting fixes. Fixed crash when hitting rate limit
This commit is contained in:
@@ -9,7 +9,7 @@ export default tseslint.config(
|
|||||||
...tseslint.configs.strictTypeChecked,
|
...tseslint.configs.strictTypeChecked,
|
||||||
...tseslint.configs.stylisticTypeChecked,
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
{
|
{
|
||||||
ignores: ['**/node_modules/*', '**/*.mjs', '**/*.js', 'src/mapper.ts'],
|
ignores: ['**/node_modules/*', '**/*.mjs', '**/*.js'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '@google/gemini-cli-core/dist/src/config/models.js';
|
} from '@google/gemini-cli-core/dist/src/config/models.js';
|
||||||
|
|
||||||
import { Content, GeminiResponse, Model } from './types.js';
|
import { Content, GeminiResponse, Model } from './types.js';
|
||||||
|
import consola from 'consola';
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* 1. Build the ContentGenerator exactly like the CLI does */
|
/* 1. Build the ContentGenerator exactly like the CLI does */
|
||||||
@@ -32,6 +33,33 @@ const generatorPromise: Promise<ContentGenerator> = (async () => {
|
|||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
type GenConfig = Record<string, unknown>;
|
type GenConfig = Record<string, unknown>;
|
||||||
|
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
const INITIAL_RETRY_DELAY = 1000; // 1 second
|
||||||
|
|
||||||
|
async function withRetry<T>(operation: () => Promise<T>): Promise<T> {
|
||||||
|
let retries = 0;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error) {
|
||||||
|
// Check if it's an Error object with a message property
|
||||||
|
if (!(error instanceof Error) ||
|
||||||
|
!error.message.includes('RESOURCE_EXHAUSTED') ||
|
||||||
|
retries >= MAX_RETRIES) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
retries++;
|
||||||
|
const delay = INITIAL_RETRY_DELAY * Math.pow(2, retries - 1);
|
||||||
|
|
||||||
|
consola.error(
|
||||||
|
`Rate limit hit, retrying in ${delay}ms ` +
|
||||||
|
`(attempt ${retries}/${MAX_RETRIES})`,
|
||||||
|
);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function sendChat({
|
export async function sendChat({
|
||||||
contents,
|
contents,
|
||||||
generationConfig = {},
|
generationConfig = {},
|
||||||
@@ -41,11 +69,11 @@ export async function sendChat({
|
|||||||
tools?: unknown, // accepted but ignored for now
|
tools?: unknown, // accepted but ignored for now
|
||||||
}): Promise<GeminiResponse> {
|
}): Promise<GeminiResponse> {
|
||||||
const generator = await generatorPromise;
|
const generator = await generatorPromise;
|
||||||
const gResp = await generator.generateContent({
|
const gResp = await withRetry(() => generator.generateContent({
|
||||||
model: modelName,
|
model: modelName,
|
||||||
contents,
|
contents,
|
||||||
config: generationConfig,
|
config: generationConfig,
|
||||||
});
|
}));
|
||||||
return {
|
return {
|
||||||
text: gResp.text ?? '',
|
text: gResp.text ?? '',
|
||||||
usageMetadata: {
|
usageMetadata: {
|
||||||
@@ -65,11 +93,11 @@ export async function* sendChatStream({
|
|||||||
tools?: unknown,
|
tools?: unknown,
|
||||||
}) {
|
}) {
|
||||||
const generator = await generatorPromise;
|
const generator = await generatorPromise;
|
||||||
const stream = await generator.generateContentStream({
|
const stream = await withRetry(() => generator.generateContentStream({
|
||||||
model: modelName,
|
model: modelName,
|
||||||
contents,
|
contents,
|
||||||
config: generationConfig,
|
config: generationConfig,
|
||||||
});
|
}));
|
||||||
for await (const chunk of stream) yield chunk;
|
for await (const chunk of stream) yield chunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,3 +122,4 @@ export function listModels(): Model[] {
|
|||||||
// export async function embed(_input: unknown) {
|
// export async function embed(_input: unknown) {
|
||||||
// throw new Error('Embeddings endpoint not implemented yet.');
|
// throw new Error('Embeddings endpoint not implemented yet.');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,25 @@
|
|||||||
/* mapper.ts – OpenAI ⇆ Gemini (with reasoning/1 M context) */
|
/* mapper.ts – OpenAI ⇆ Gemini (with reasoning/1 M context) */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
import { fetchAndEncode } from './remoteimage';
|
import { fetchAndEncode } from './remoteimage';
|
||||||
import { z } from 'zod';
|
import { z, ZodRawShape } from 'zod';
|
||||||
import { ToolRegistry } from '@google/gemini-cli-core/dist/src/tools/tool-registry.js';
|
import { ToolRegistry }
|
||||||
import { RequestBody } from './types';
|
from '@google/gemini-cli-core/dist/src/tools/tool-registry.js';
|
||||||
|
import { Config } from '@google/gemini-cli-core/dist/src/config/config.js';
|
||||||
|
import { Tool } from '@google/gemini-cli-core/dist/src/tools/tools.js';
|
||||||
|
import {
|
||||||
|
Part,
|
||||||
|
RequestBody,
|
||||||
|
GeminiResponse,
|
||||||
|
GeminiStreamChunk,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ----------------------------------------------------------------- */
|
||||||
interface Part { text?: string; inlineData?: { mimeType: string, data: string } }
|
async function callLocalFunction(/*_name: string, _args: unknown*/) {
|
||||||
|
return Promise.resolve({
|
||||||
/* ------------------------------------------------------------------ */
|
ok: true,
|
||||||
function callLocalFunction(_name: string, _args: unknown) {
|
llmContent: [],
|
||||||
return { ok: true };
|
returnDisplay: 'Function executed successfully',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
@@ -61,21 +70,29 @@ export async function mapRequest(body: RequestBody) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* ---- Tool / function mapping ----------------------------------- */
|
/* ---- Tool / function mapping ----------------------------------- */
|
||||||
const tools = new ToolRegistry({} as any);
|
// Note: ToolRegistry expects a complex Config object that we don't have
|
||||||
|
// access to. Casting to `Config` is a workaround.
|
||||||
|
const tools = new ToolRegistry({} as Config);
|
||||||
|
|
||||||
if (body.functions?.length) {
|
if (body.functions?.length) {
|
||||||
const reg = tools as any;
|
for (const fn of body.functions) {
|
||||||
body.functions.forEach((fn: any) =>
|
tools.registerTool({
|
||||||
reg.registerTool(
|
name: fn.name,
|
||||||
fn.name,
|
displayName: fn.name,
|
||||||
{
|
description: fn.description ?? '',
|
||||||
title: fn.name,
|
schema: z.object(
|
||||||
description: fn.description ?? '',
|
(fn.parameters?.properties as ZodRawShape) ?? {},
|
||||||
inputSchema: z.object(fn.parameters?.properties ?? {}),
|
),
|
||||||
},
|
isOutputMarkdown: false,
|
||||||
(args: unknown) => callLocalFunction(fn.name, args),
|
canUpdateOutput: false,
|
||||||
),
|
validateToolParams: () => null,
|
||||||
);
|
getDescription: (params: unknown) =>
|
||||||
|
`Executing ${fn.name} with parameters: ` +
|
||||||
|
JSON.stringify(params),
|
||||||
|
shouldConfirmExecute: () => Promise.resolve(false),
|
||||||
|
execute: () => callLocalFunction(),
|
||||||
|
} as Tool);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { geminiReq, tools };
|
return { geminiReq, tools };
|
||||||
@@ -84,8 +101,12 @@ export async function mapRequest(body: RequestBody) {
|
|||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
/* Non-stream response: Gemini ➞ OpenAI */
|
/* Non-stream response: Gemini ➞ OpenAI */
|
||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
export function mapResponse(gResp: any) {
|
export function mapResponse(gResp: GeminiResponse) {
|
||||||
const usage = gResp.usageMetadata ?? {};
|
const usage = gResp.usageMetadata ?? {
|
||||||
|
promptTokens: 0,
|
||||||
|
candidatesTokens: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
id: `chatcmpl-${Date.now()}`,
|
id: `chatcmpl-${Date.now()}`,
|
||||||
object: 'chat.completion',
|
object: 'chat.completion',
|
||||||
@@ -99,9 +120,9 @@ export function mapResponse(gResp: any) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
usage: {
|
usage: {
|
||||||
prompt_tokens: usage.promptTokens ?? 0,
|
prompt_tokens: usage.promptTokens,
|
||||||
completion_tokens: usage.candidatesTokens ?? 0,
|
completion_tokens: usage.candidatesTokens,
|
||||||
total_tokens: usage.totalTokens ?? 0,
|
total_tokens: usage.totalTokens,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -110,9 +131,9 @@ export function mapResponse(gResp: any) {
|
|||||||
/* Stream chunk mapper: Gemini ➞ OpenAI */
|
/* Stream chunk mapper: Gemini ➞ OpenAI */
|
||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
|
|
||||||
export function mapStreamChunk(chunk: any) {
|
export function mapStreamChunk(chunk: GeminiStreamChunk) {
|
||||||
const part = chunk?.candidates?.[0]?.content?.parts?.[0] ?? {};
|
const part = chunk?.candidates?.[0]?.content?.parts?.[0] ?? {};
|
||||||
const delta: any = { role: 'assistant' };
|
const delta: { role: 'assistant', content?: string } = { role: 'assistant' };
|
||||||
|
|
||||||
if (part.thought === true) {
|
if (part.thought === true) {
|
||||||
delta.content = `<think>${part.text ?? ''}`; // ST renders grey bubble
|
delta.content = `<think>${part.text ?? ''}`; // ST renders grey bubble
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import consola from 'consola';
|
|||||||
import http from 'http';
|
import http from 'http';
|
||||||
import { listModels, sendChat, sendChatStream } from './chatwrapper';
|
import { listModels, sendChat, sendChatStream } from './chatwrapper';
|
||||||
import { mapRequest, mapResponse, mapStreamChunk } from './mapper.js';
|
import { mapRequest, mapResponse, mapStreamChunk } from './mapper.js';
|
||||||
import { RequestBody, GeminiResponse } from './types';
|
import { RequestBody, GeminiResponse, GeminiStreamChunk, Part } from './types';
|
||||||
import { config } from './config';
|
import { config } from './config';
|
||||||
|
|
||||||
/* ── basic config ─────────────────────────────────────────────────── */
|
/* ── basic config ─────────────────────────────────────────────────── */
|
||||||
@@ -41,8 +41,11 @@ function readJSON(
|
|||||||
error: { message: 'Request body is missing for POST request' },
|
error: { message: 'Request body is missing for POST request' },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return resolve(null);
|
resolve(null);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resolve(JSON.parse(data) as RequestBody);
|
resolve(JSON.parse(data) as RequestBody);
|
||||||
@@ -51,6 +54,7 @@ function readJSON(
|
|||||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ error: { message: 'Malformed JSON' } }));
|
res.end(JSON.stringify({ error: { message: 'Malformed JSON' } }));
|
||||||
resolve(null);
|
resolve(null);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -101,7 +105,35 @@ http
|
|||||||
});
|
});
|
||||||
|
|
||||||
for await (const chunk of sendChatStream({ ...geminiReq, tools })) {
|
for await (const chunk of sendChatStream({ ...geminiReq, tools })) {
|
||||||
res.write(`data: ${JSON.stringify(mapStreamChunk(chunk))}\n\n`);
|
// Transform the chunk to match our expected type
|
||||||
|
const transformedParts =
|
||||||
|
chunk.candidates?.[0]?.content?.parts?.map(part => {
|
||||||
|
const transformedPart: Part = {
|
||||||
|
text: part.text,
|
||||||
|
thought: part.text?.startsWith?.('<think>') ?? false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (part.inlineData?.data) {
|
||||||
|
transformedPart.inlineData = {
|
||||||
|
mimeType: part.inlineData.mimeType ?? 'text/plain',
|
||||||
|
data: part.inlineData.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformedPart;
|
||||||
|
}) ?? [];
|
||||||
|
|
||||||
|
const streamChunk: GeminiStreamChunk = {
|
||||||
|
candidates: [{
|
||||||
|
content: {
|
||||||
|
parts: transformedParts,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
res.write(
|
||||||
|
`data: ${JSON.stringify(mapStreamChunk(streamChunk))}\n\n`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
res.end('data: [DONE]\n\n');
|
res.end('data: [DONE]\n\n');
|
||||||
} else {
|
} else {
|
||||||
@@ -112,10 +144,23 @@ http
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as Error;
|
const error = err as Error;
|
||||||
consola.error('Proxy error ➜', error);
|
consola.error('Proxy error ➜', error);
|
||||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ error: { message: error.message } }));
|
// For streaming responses, send error in stream format
|
||||||
|
if (body.stream && res.headersSent) {
|
||||||
|
res.write(`data: ${JSON.stringify({
|
||||||
|
error: {
|
||||||
|
message: error.message,
|
||||||
|
type: 'error',
|
||||||
|
},
|
||||||
|
})}\n\n`);
|
||||||
|
res.end('data: [DONE]\n\n');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// For non-streaming responses or if headers haven't been sent yet
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: { message: error.message } }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- anything else ---------- */
|
/* ---- anything else ---------- */
|
||||||
|
|||||||
37
src/types.ts
37
src/types.ts
@@ -6,9 +6,16 @@ export interface Model {
|
|||||||
object: 'model';
|
object: 'model';
|
||||||
owned_by: 'google';
|
owned_by: 'google';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InlineData {
|
||||||
|
mimeType: string;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Part {
|
export interface Part {
|
||||||
text?: string;
|
text?: string;
|
||||||
inlineData?: { mimeType: string, data: string };
|
inlineData?: InlineData;
|
||||||
|
thought?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Content {
|
export interface Content {
|
||||||
@@ -16,6 +23,14 @@ export interface Content {
|
|||||||
parts: Part[];
|
parts: Part[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FunctionDef {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
parameters?: {
|
||||||
|
properties?: Record<string, unknown>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface RequestBody {
|
export interface RequestBody {
|
||||||
messages: {
|
messages: {
|
||||||
content:
|
content:
|
||||||
@@ -28,13 +43,7 @@ export interface RequestBody {
|
|||||||
generationConfig?: Record<string, unknown>;
|
generationConfig?: Record<string, unknown>;
|
||||||
include_reasoning?: boolean;
|
include_reasoning?: boolean;
|
||||||
stream?: boolean;
|
stream?: boolean;
|
||||||
functions?: {
|
functions?: FunctionDef[];
|
||||||
name: string,
|
|
||||||
description?: string,
|
|
||||||
parameters?: {
|
|
||||||
properties?: Record<string, unknown>,
|
|
||||||
},
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GeminiResponse {
|
export interface GeminiResponse {
|
||||||
@@ -44,4 +53,12 @@ export interface GeminiResponse {
|
|||||||
candidatesTokens: number,
|
candidatesTokens: number,
|
||||||
totalTokens: number,
|
totalTokens: number,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GeminiStreamChunk {
|
||||||
|
candidates?: {
|
||||||
|
content?: {
|
||||||
|
parts?: Part[],
|
||||||
|
},
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user