File size: 4,323 Bytes
6aaf221
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/* ------------------------------------------------------------------ */
/*  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<string, unknown> = {
    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 = `<think>${part.text ?? ''}`;  // ST renders grey bubble
  } else if (typeof part.text === 'string') {
    delta.content = part.text;
  }
  return { choices: [ { delta, index: 0 } ] };
}