/* ------------------------------------------------------------------ */ /* mapper.ts – OpenAI ⇆ Gemini (with reasoning/1 M context) */ /* ------------------------------------------------------------------ */ import { fetchAndEncode } from './remoteimage'; import { z } from 'zod'; import { ToolRegistry } from '@google/gemini-cli-core/dist/src/tools/tool-registry.js'; /* ------------------------------------------------------------------ */ type Part = { text?: string; inlineData?: { mimeType: string; data: string } }; /* ------------------------------------------------------------------ */ async function callLocalFunction(_name: string, _args: unknown) { return { ok: true }; } /* ================================================================== */ /* Request mapper: OpenAI ➞ Gemini */ /* ================================================================== */ export async function mapRequest(body: any) { const parts: Part[] = []; /* ---- convert messages & vision --------------------------------- */ for (const m of body.messages) { if (Array.isArray(m.content)) { for (const item of m.content) { if (item.type === 'image_url') { parts.push({ inlineData: await fetchAndEncode(item.image_url.url) }); } else if (item.type === 'text') { parts.push({ text: item.text }); } } } else { parts.push({ text: m.content }); } } /* ---- base generationConfig ------------------------------------- */ const generationConfig: Record = { temperature: body.temperature, maxOutputTokens: body.max_tokens, topP: body.top_p, ...(body.generationConfig ?? {}), // copy anything ST already merged }; if (body.include_reasoning === true) { generationConfig.enable_thoughts = true; // ← current flag generationConfig.thinking_budget ??= 2048; // optional limit } /* ---- auto-enable reasoning & 1 M context ----------------------- */ if (body.include_reasoning === true && generationConfig.thinking !== true) { generationConfig.thinking = true; generationConfig.thinking_budget ??= 2048; } generationConfig.maxInputTokens ??= 1_000_000; // lift context cap const geminiReq = { contents: [{ role: 'user', parts }], generationConfig, stream: body.stream, }; /* ---- Tool / function mapping ----------------------------------- */ const tools = new ToolRegistry({} as any); if (body.functions?.length) { const reg = tools as any; body.functions.forEach((fn: any) => reg.registerTool( fn.name, { title: fn.name, description: fn.description ?? '', inputSchema: z.object(fn.parameters?.properties ?? {}), }, async (args: unknown) => callLocalFunction(fn.name, args), ), ); } return { geminiReq, tools }; } /* ================================================================== */ /* Non-stream response: Gemini ➞ OpenAI */ /* ================================================================== */ export function mapResponse(gResp: any) { const usage = gResp.usageMetadata ?? {}; return { id: `chatcmpl-${Date.now()}`, object: 'chat.completion', created: Math.floor(Date.now() / 1000), model: 'gemini-2.5-pro-latest', choices: [ { index: 0, message: { role: 'assistant', content: gResp.text }, finish_reason: 'stop', }, ], usage: { prompt_tokens: usage.promptTokens ?? 0, completion_tokens: usage.candidatesTokens ?? 0, total_tokens: usage.totalTokens ?? 0, }, }; } /* ================================================================== */ /* Stream chunk mapper: Gemini ➞ OpenAI */ /* ================================================================== */ export function mapStreamChunk(chunk: any) { const part = chunk?.candidates?.[0]?.content?.parts?.[0] ?? {}; const delta: any = { role: 'assistant' }; if (part.thought === true) { delta.content = `${part.text ?? ''}`; // ST renders grey bubble } else if (typeof part.text === 'string') { delta.content = part.text; } return { choices: [ { delta, index: 0 } ] }; }