free-antigravity-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +142 -0
- package/dist/chat.d.ts +2 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +212 -0
- package/dist/chat.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +216 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +125 -0
- package/dist/config.js.map +1 -0
- package/dist/crypto.d.ts +5 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +93 -0
- package/dist/crypto.js.map +1 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +10 -0
- package/dist/logger.js.map +1 -0
- package/dist/proxy/modelUtils.d.ts +31 -0
- package/dist/proxy/modelUtils.d.ts.map +1 -0
- package/dist/proxy/modelUtils.js +55 -0
- package/dist/proxy/modelUtils.js.map +1 -0
- package/dist/proxy/registry.d.ts +40 -0
- package/dist/proxy/registry.d.ts.map +1 -0
- package/dist/proxy/registry.js +176 -0
- package/dist/proxy/registry.js.map +1 -0
- package/dist/proxy/shared.d.ts +39 -0
- package/dist/proxy/shared.d.ts.map +1 -0
- package/dist/proxy/shared.js +74 -0
- package/dist/proxy/shared.js.map +1 -0
- package/dist/proxy/translators/anthropic.d.ts +119 -0
- package/dist/proxy/translators/anthropic.d.ts.map +1 -0
- package/dist/proxy/translators/anthropic.js +273 -0
- package/dist/proxy/translators/anthropic.js.map +1 -0
- package/dist/proxy/translators/google.d.ts +86 -0
- package/dist/proxy/translators/google.d.ts.map +1 -0
- package/dist/proxy/translators/google.js +111 -0
- package/dist/proxy/translators/google.js.map +1 -0
- package/dist/proxy/translators/ollama.d.ts +27 -0
- package/dist/proxy/translators/ollama.d.ts.map +1 -0
- package/dist/proxy/translators/ollama.js +82 -0
- package/dist/proxy/translators/ollama.js.map +1 -0
- package/dist/proxy/translators/openai.d.ts +132 -0
- package/dist/proxy/translators/openai.d.ts.map +1 -0
- package/dist/proxy/translators/openai.js +396 -0
- package/dist/proxy/translators/openai.js.map +1 -0
- package/dist/proxy/translators/utils.d.ts +60 -0
- package/dist/proxy/translators/utils.d.ts.map +1 -0
- package/dist/proxy/translators/utils.js +504 -0
- package/dist/proxy/translators/utils.js.map +1 -0
- package/dist/proxy.d.ts +22 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +576 -0
- package/dist/proxy.js.map +1 -0
- package/dist/schemaValidator.d.ts +50 -0
- package/dist/schemaValidator.d.ts.map +1 -0
- package/dist/schemaValidator.js +208 -0
- package/dist/schemaValidator.js.map +1 -0
- package/install.cmd +33 -0
- package/package.json +46 -0
- package/src/chat.ts +184 -0
- package/src/cli.ts +184 -0
- package/src/config.ts +99 -0
- package/src/crypto.ts +49 -0
- package/src/logger.ts +8 -0
- package/src/proxy/modelUtils.ts +86 -0
- package/src/proxy/registry.ts +196 -0
- package/src/proxy/shared.ts +102 -0
- package/src/proxy/translators/anthropic.ts +420 -0
- package/src/proxy/translators/google.ts +162 -0
- package/src/proxy/translators/ollama.ts +88 -0
- package/src/proxy/translators/openai.ts +556 -0
- package/src/proxy/translators/utils.ts +552 -0
- package/src/proxy.ts +573 -0
- package/src/schemaValidator.ts +215 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama Translator.
|
|
3
|
+
*
|
|
4
|
+
* Ollama is fully OpenAI-compatible (channels/v1/chat/completions).
|
|
5
|
+
* This module re-exports the OpenAI translator functions and adds
|
|
6
|
+
* Ollama-specific helpers:
|
|
7
|
+
* - Default URL normalization (localhost:11434 fallback)
|
|
8
|
+
* - User-friendly error message translation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { log } from '../../logger';
|
|
12
|
+
|
|
13
|
+
// ─── Re-export all OpenAI translator functions ────────────────────────────
|
|
14
|
+
// The registry auto-discovers these by naming convention:
|
|
15
|
+
// mapGeminiToOpenAI, mapOpenAIToGemini, mapOpenAIChunkToGemini
|
|
16
|
+
// Ollama uses the exact same format, so we re-export verbatim.
|
|
17
|
+
|
|
18
|
+
export { mapGeminiToOpenAI, mapOpenAIToGemini, mapOpenAIChunkToGemini } from './openai';
|
|
19
|
+
|
|
20
|
+
// ─── Ollama-Specific Helpers ──────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Normalizes an Ollama API URL to the standard chat completions endpoint.
|
|
24
|
+
*
|
|
25
|
+
* Handles these common patterns:
|
|
26
|
+
* http://localhost:11434 → http://localhost:11434/v1/chat/completions
|
|
27
|
+
* http://localhost:11434/v1 → http://localhost:11434/v1/chat/completions
|
|
28
|
+
* http://localhost → http://localhost:11434/v1/chat/completions
|
|
29
|
+
* http://10.0.0.5:11434/api/generate → kept as-is (non-chat endpoint)
|
|
30
|
+
*
|
|
31
|
+
* If localhost has no port, defaults to Ollama's standard port 11434.
|
|
32
|
+
*/
|
|
33
|
+
export function getOllamaApiUrl(baseUrl: string): string {
|
|
34
|
+
let url = baseUrl;
|
|
35
|
+
|
|
36
|
+
// If it already has a specific API path, don't touch it
|
|
37
|
+
if (url.includes('/api/')) {
|
|
38
|
+
return url;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Clean trailing slash
|
|
42
|
+
url = url.replace(/\/$/, '');
|
|
43
|
+
|
|
44
|
+
// If no port on localhost, use default Ollama port
|
|
45
|
+
if (url.match(/^https?:\/\/localhost$/)) {
|
|
46
|
+
url = 'http://localhost:11434';
|
|
47
|
+
log.info('[OllamaTranslator] Added default Ollama port 11434');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If URL ends with /v1, append /chat/completions
|
|
51
|
+
if (url.endsWith('/v1')) {
|
|
52
|
+
url += '/chat/completions';
|
|
53
|
+
return url;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// If URL doesn't have a chat completions path, add full /v1/chat/completions
|
|
57
|
+
if (!url.includes('/chat/completions') && !url.includes('/completions')) {
|
|
58
|
+
url += '/v1/chat/completions';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return url;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Translate raw Ollama errors into user-friendly messages.
|
|
66
|
+
*/
|
|
67
|
+
export function translateOllamaError(statusCode: number, body: string): string {
|
|
68
|
+
// Connection refused — Ollama service not running
|
|
69
|
+
if (body.includes('ECONNREFUSED') || body.includes('connect ECONNREFUSED')) {
|
|
70
|
+
return 'Ollama is not running. Start it with `ollama serve` or launch the Ollama desktop app.';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Model not pulled yet
|
|
74
|
+
if (statusCode === 404 || (body.includes('model') && body.includes('not found'))) {
|
|
75
|
+
const modelMatch = body.match(/model ['"]([^'"]+)['"]/);
|
|
76
|
+
const modelName = modelMatch ? modelMatch[1] : 'unknown';
|
|
77
|
+
return `Ollama model "${modelName}" not found. Pull it: ollama pull ${modelName}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Server-side errors (OOM, crash, etc.)
|
|
81
|
+
if (statusCode >= 500) {
|
|
82
|
+
return `Ollama server error (${statusCode}). Check if Ollama has enough resources (RAM/VRAM).`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Generic fallback with truncated body
|
|
86
|
+
const snippet = body.substring(0, 200);
|
|
87
|
+
return `Ollama error (${statusCode}): ${snippet}`;
|
|
88
|
+
}
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI/Ollama provider translator.
|
|
3
|
+
* Handles Gemini ↔ OpenAI/Ollama request/response mapping and streaming chunks.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { log } from '../../logger';
|
|
7
|
+
import {
|
|
8
|
+
fixParamTypes,
|
|
9
|
+
translateToolCallToNative,
|
|
10
|
+
formatTranslatedResponse,
|
|
11
|
+
normalizeToolArgs,
|
|
12
|
+
ToolCallArgs,
|
|
13
|
+
TranslatedCallInfo,
|
|
14
|
+
} from './utils';
|
|
15
|
+
import {
|
|
16
|
+
modelToolCallIds,
|
|
17
|
+
modelReasoningContent,
|
|
18
|
+
activeStreamContexts,
|
|
19
|
+
translatedToolCalls,
|
|
20
|
+
stateTimestamps,
|
|
21
|
+
touchStateTimestamp,
|
|
22
|
+
StreamContext,
|
|
23
|
+
} from '../shared';
|
|
24
|
+
|
|
25
|
+
// ─── Types ────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
interface GeminiTool {
|
|
28
|
+
functionDeclarations?: GeminiFunctionDeclaration[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface GeminiFunctionDeclaration {
|
|
32
|
+
name: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
parameters?: GeminiParameters;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface GeminiParameters {
|
|
38
|
+
type: string;
|
|
39
|
+
properties?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface OpenAITool {
|
|
43
|
+
type: 'function';
|
|
44
|
+
function: {
|
|
45
|
+
name: string;
|
|
46
|
+
description: string;
|
|
47
|
+
parameters: Record<string, unknown>;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface GeminiContent {
|
|
52
|
+
role?: string;
|
|
53
|
+
parts?: GeminiPart[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface GeminiPart {
|
|
57
|
+
text?: string;
|
|
58
|
+
thought?: boolean;
|
|
59
|
+
functionCall?: GeminiFunctionCall;
|
|
60
|
+
functionResponse?: GeminiFunctionResponse;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface GeminiFunctionCall {
|
|
64
|
+
name: string;
|
|
65
|
+
args: Record<string, unknown>;
|
|
66
|
+
id?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface GeminiFunctionResponse {
|
|
70
|
+
name: string;
|
|
71
|
+
response: unknown;
|
|
72
|
+
id?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface GeminiRequestBody {
|
|
76
|
+
systemInstruction?: { parts: GeminiPart[] };
|
|
77
|
+
contents?: GeminiContent[];
|
|
78
|
+
tools?: GeminiTool[];
|
|
79
|
+
generationConfig?: {
|
|
80
|
+
temperature?: number;
|
|
81
|
+
maxOutputTokens?: number;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface OpenAIMessage {
|
|
86
|
+
role: string;
|
|
87
|
+
content: string | null;
|
|
88
|
+
tool_calls?: OpenAIToolCall[];
|
|
89
|
+
tool_call_id?: string;
|
|
90
|
+
reasoning_content?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface OpenAIToolCall {
|
|
94
|
+
id: string;
|
|
95
|
+
type: 'function';
|
|
96
|
+
function: {
|
|
97
|
+
name: string;
|
|
98
|
+
arguments: string;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface OpenAIRequestBody {
|
|
103
|
+
model: string;
|
|
104
|
+
messages: OpenAIMessage[];
|
|
105
|
+
temperature?: number;
|
|
106
|
+
max_tokens?: number;
|
|
107
|
+
tools?: OpenAITool[];
|
|
108
|
+
stream?: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface OpenAIResponse {
|
|
112
|
+
choices?: OpenAIChoice[];
|
|
113
|
+
usage?: {
|
|
114
|
+
prompt_tokens: number;
|
|
115
|
+
completion_tokens: number;
|
|
116
|
+
total_tokens: number;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface OpenAIChoice {
|
|
121
|
+
message?: {
|
|
122
|
+
content: string;
|
|
123
|
+
reasoning_content?: string;
|
|
124
|
+
reasoning?: string;
|
|
125
|
+
tool_calls?: OpenAIToolCall[];
|
|
126
|
+
};
|
|
127
|
+
finish_reason?: string;
|
|
128
|
+
delta?: {
|
|
129
|
+
content?: string;
|
|
130
|
+
reasoning_content?: string;
|
|
131
|
+
reasoning?: string;
|
|
132
|
+
tool_calls?: OpenAIToolCallDelta[];
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface OpenAIToolCallDelta {
|
|
137
|
+
index?: number;
|
|
138
|
+
id?: string;
|
|
139
|
+
function?: {
|
|
140
|
+
name?: string;
|
|
141
|
+
arguments?: string;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface GeminiGenerateContentResponse {
|
|
146
|
+
candidates: GeminiCandidate[];
|
|
147
|
+
usageMetadata?: GeminiUsageMetadata;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface GeminiCandidate {
|
|
151
|
+
content: {
|
|
152
|
+
parts: GeminiPart[];
|
|
153
|
+
role: string;
|
|
154
|
+
};
|
|
155
|
+
finishReason: string;
|
|
156
|
+
index: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface GeminiUsageMetadata {
|
|
160
|
+
promptTokenCount: number;
|
|
161
|
+
candidatesTokenCount: number;
|
|
162
|
+
totalTokenCount: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface DSMLParsedResult {
|
|
166
|
+
functionCalls: { name: string; args: Record<string, unknown> }[];
|
|
167
|
+
cleanText: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── REQUEST: Gemini → OpenAI ──────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
function mapGeminiToolsToOpenAI(geminiTools: GeminiTool[]): OpenAITool[] {
|
|
173
|
+
if (!geminiTools || !Array.isArray(geminiTools)) return [];
|
|
174
|
+
const openaiTools: OpenAITool[] = [];
|
|
175
|
+
for (const toolGroup of geminiTools) {
|
|
176
|
+
if (toolGroup.functionDeclarations && Array.isArray(toolGroup.functionDeclarations)) {
|
|
177
|
+
for (const func of toolGroup.functionDeclarations) {
|
|
178
|
+
const params = func.parameters
|
|
179
|
+
? (JSON.parse(JSON.stringify(func.parameters)) as Record<string, unknown>)
|
|
180
|
+
: { type: 'object', properties: {} };
|
|
181
|
+
if (params.type && typeof params.type === 'string') {
|
|
182
|
+
(params as Record<string, string>).type = (params.type as string).toLowerCase();
|
|
183
|
+
}
|
|
184
|
+
if (params.properties) {
|
|
185
|
+
fixParamTypes(params.properties as Record<string, unknown>);
|
|
186
|
+
}
|
|
187
|
+
openaiTools.push({
|
|
188
|
+
type: 'function',
|
|
189
|
+
function: {
|
|
190
|
+
name: func.name,
|
|
191
|
+
description: func.description || '',
|
|
192
|
+
parameters: params,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return openaiTools;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function mapGeminiToOpenAI(geminiBody: GeminiRequestBody, modelName: string): OpenAIRequestBody {
|
|
202
|
+
const messages: OpenAIMessage[] = [];
|
|
203
|
+
|
|
204
|
+
if (geminiBody.systemInstruction && geminiBody.systemInstruction.parts) {
|
|
205
|
+
const systemText = geminiBody.systemInstruction.parts.map((p) => p.text || '').join('');
|
|
206
|
+
if (systemText) {
|
|
207
|
+
messages.push({ role: 'system', content: systemText });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (geminiBody.contents) {
|
|
212
|
+
for (const item of geminiBody.contents) {
|
|
213
|
+
if (item.parts) {
|
|
214
|
+
const hasFunctionCall = item.parts.some((p) => p.functionCall);
|
|
215
|
+
const hasFunctionResponse = item.parts.some((p) => p.functionResponse);
|
|
216
|
+
|
|
217
|
+
if (hasFunctionCall && item.role === 'model') {
|
|
218
|
+
const toolCalls: OpenAIToolCall[] = [];
|
|
219
|
+
for (const p of item.parts) {
|
|
220
|
+
if (p.functionCall) {
|
|
221
|
+
const callId = p.functionCall.id || 'call_' + Math.random().toString(36).slice(2, 10);
|
|
222
|
+
let originalName = p.functionCall.name;
|
|
223
|
+
let originalArgs = p.functionCall.args;
|
|
224
|
+
const translatedInfo = translatedToolCalls.get(callId);
|
|
225
|
+
if (translatedInfo) {
|
|
226
|
+
originalName = translatedInfo.originalName;
|
|
227
|
+
originalArgs = { CommandLine: translatedInfo.cmd, Cwd: translatedInfo.cwd };
|
|
228
|
+
}
|
|
229
|
+
toolCalls.push({
|
|
230
|
+
id: callId,
|
|
231
|
+
type: 'function',
|
|
232
|
+
function: {
|
|
233
|
+
name: originalName,
|
|
234
|
+
arguments: typeof originalArgs === 'string' ? originalArgs : JSON.stringify(originalArgs || {}),
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
messages.push({ role: 'assistant', content: null, tool_calls: toolCalls });
|
|
240
|
+
} else if (hasFunctionResponse) {
|
|
241
|
+
for (const p of item.parts) {
|
|
242
|
+
if (p.functionResponse) {
|
|
243
|
+
const funcName = p.functionResponse.name || '';
|
|
244
|
+
const modelTCIds = modelToolCallIds.get(modelName) || {};
|
|
245
|
+
const toolCallId = p.functionResponse.id || modelTCIds[funcName] || 'call_' + funcName;
|
|
246
|
+
const responseData = p.functionResponse.response;
|
|
247
|
+
let contentStr = '';
|
|
248
|
+
const translatedInfo = translatedToolCalls.get(toolCallId);
|
|
249
|
+
if (translatedInfo) {
|
|
250
|
+
contentStr = formatTranslatedResponse(translatedInfo, responseData);
|
|
251
|
+
} else {
|
|
252
|
+
contentStr = typeof responseData === 'string' ? responseData : JSON.stringify(responseData || {});
|
|
253
|
+
}
|
|
254
|
+
messages.push({ role: 'tool', content: contentStr, tool_call_id: toolCallId });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
const role = item.role === 'model' ? 'assistant' : item.role || 'user';
|
|
259
|
+
let content = '';
|
|
260
|
+
let reasoning_content = '';
|
|
261
|
+
if (role === 'assistant') {
|
|
262
|
+
const regularParts = (item.parts || []).filter((p) => !p.thought);
|
|
263
|
+
const thoughtParts = (item.parts || []).filter((p) => p.thought);
|
|
264
|
+
content = regularParts.map((p) => p.text || '').join('');
|
|
265
|
+
reasoning_content = thoughtParts.map((p) => p.text || '').join('');
|
|
266
|
+
} else {
|
|
267
|
+
content = (item.parts || []).map((p) => p.text || '').join('');
|
|
268
|
+
}
|
|
269
|
+
const msg: OpenAIMessage = { role, content };
|
|
270
|
+
if (reasoning_content) msg.reasoning_content = reasoning_content;
|
|
271
|
+
messages.push(msg);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Inject reasoning_content into assistant messages missing it
|
|
278
|
+
let lastAssistantIdx = -1;
|
|
279
|
+
for (let i = 0; i < messages.length; i++) {
|
|
280
|
+
if (messages[i].role === 'assistant') lastAssistantIdx = i;
|
|
281
|
+
}
|
|
282
|
+
for (let i = 0; i < messages.length; i++) {
|
|
283
|
+
if (messages[i].role === 'assistant' && !(messages[i] as OpenAIMessage).reasoning_content) {
|
|
284
|
+
const preservedReasoning = modelReasoningContent.get(modelName) || '';
|
|
285
|
+
messages[i].reasoning_content = i === lastAssistantIdx && preservedReasoning ? preservedReasoning : '';
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const payload: OpenAIRequestBody = {
|
|
290
|
+
model: modelName,
|
|
291
|
+
messages,
|
|
292
|
+
temperature: geminiBody.generationConfig?.temperature ?? 0.7,
|
|
293
|
+
max_tokens: geminiBody.generationConfig?.maxOutputTokens ?? 4000,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
if (geminiBody.tools && Array.isArray(geminiBody.tools)) {
|
|
297
|
+
const openaiTools = mapGeminiToolsToOpenAI(geminiBody.tools);
|
|
298
|
+
if (openaiTools.length > 0) payload.tools = openaiTools;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return payload;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ─── RESPONSE: OpenAI → Gemini ─────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
function parseDSMLToolCalls(text: string): DSMLParsedResult | null {
|
|
307
|
+
try {
|
|
308
|
+
const invokeRegex = /<DSML\|invoke name="([^"]+)">([\s\S]*?)<\/DSML\|invoke>/g;
|
|
309
|
+
const functionCalls: { name: string; args: Record<string, unknown> }[] = [];
|
|
310
|
+
let invokeMatch: RegExpExecArray | null;
|
|
311
|
+
while ((invokeMatch = invokeRegex.exec(text)) !== null) {
|
|
312
|
+
const funcName = invokeMatch[1];
|
|
313
|
+
const paramsBlock = invokeMatch[2];
|
|
314
|
+
const args: Record<string, unknown> = {};
|
|
315
|
+
const paramRegex = /<DSML\|parameter name="([^"]+)"(?: string="([^"]+)")?>([\s\S]*?)<\/DSML\|parameter>/g;
|
|
316
|
+
let paramMatch: RegExpExecArray | null;
|
|
317
|
+
while ((paramMatch = paramRegex.exec(paramsBlock)) !== null) {
|
|
318
|
+
const paramName = paramMatch[1];
|
|
319
|
+
let paramValue: unknown = paramMatch[3].trim();
|
|
320
|
+
const isString = paramMatch[2] === 'true';
|
|
321
|
+
if (!isString) {
|
|
322
|
+
try {
|
|
323
|
+
paramValue = JSON.parse(paramValue as string);
|
|
324
|
+
} catch (e) {
|
|
325
|
+
log.debug('[OpenAI] DSML param parse fallback:', (e as Error).message); /* keep as string */
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
args[paramName] = paramValue;
|
|
329
|
+
}
|
|
330
|
+
functionCalls.push({ name: funcName, args });
|
|
331
|
+
}
|
|
332
|
+
if (functionCalls.length === 0) return null;
|
|
333
|
+
log.info(
|
|
334
|
+
`[Proxy] Detected ${functionCalls.length} DSML tool call(s): ${functionCalls.map((f) => f.name).join(', ')}`,
|
|
335
|
+
);
|
|
336
|
+
let cleanText = text;
|
|
337
|
+
cleanText = cleanText.replace(/<DSML\|tool_calls>[\s\S]*?<\/DSML\|tool_calls>/g, '');
|
|
338
|
+
cleanText = cleanText.replace(/<DSML\|invoke name="[^"]+">[\s\S]*?<\/DSML\|invoke>/g, '');
|
|
339
|
+
cleanText = cleanText.trim();
|
|
340
|
+
return { functionCalls, cleanText };
|
|
341
|
+
} catch (e) {
|
|
342
|
+
log.error('[Proxy] Failed to parse DSML tool calls:', e);
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function mapOpenAIToGemini(openAiRes: OpenAIResponse, modelName: string): GeminiGenerateContentResponse {
|
|
348
|
+
const choice = openAiRes.choices?.[0];
|
|
349
|
+
|
|
350
|
+
if (choice?.message?.tool_calls && choice.message.tool_calls.length > 0) {
|
|
351
|
+
const parts: GeminiPart[] = choice.message.tool_calls.map((tc) => {
|
|
352
|
+
let args: ToolCallArgs;
|
|
353
|
+
try {
|
|
354
|
+
args =
|
|
355
|
+
typeof tc.function.arguments === 'string'
|
|
356
|
+
? JSON.parse(tc.function.arguments)
|
|
357
|
+
: (tc.function.arguments as unknown as ToolCallArgs);
|
|
358
|
+
} catch (e) {
|
|
359
|
+
log.debug('[OpenAI] Tool call args parse fallback:', (e as Error).message);
|
|
360
|
+
args = {};
|
|
361
|
+
}
|
|
362
|
+
args = normalizeToolArgs(tc.function.name, args) as ToolCallArgs;
|
|
363
|
+
const modelTCIds = modelToolCallIds.get(modelName) || {};
|
|
364
|
+
modelTCIds[tc.function.name] = tc.id;
|
|
365
|
+
modelToolCallIds.set(modelName, modelTCIds);
|
|
366
|
+
touchStateTimestamp(stateTimestamps.toolCallIds, modelName);
|
|
367
|
+
const translated = translateToolCallToNative(tc.function.name, args);
|
|
368
|
+
if (translated.name !== tc.function.name) {
|
|
369
|
+
translated.args = normalizeToolArgs(translated.name, translated.args) as Record<string, unknown>;
|
|
370
|
+
translatedToolCalls.set(tc.id, {
|
|
371
|
+
originalName: tc.function.name,
|
|
372
|
+
translatedName: translated.name,
|
|
373
|
+
cmd: args.CommandLine || '',
|
|
374
|
+
cwd: args.Cwd || '',
|
|
375
|
+
});
|
|
376
|
+
touchStateTimestamp(stateTimestamps.translatedCalls, tc.id);
|
|
377
|
+
}
|
|
378
|
+
return { functionCall: { name: translated.name, args: translated.args as Record<string, unknown>, id: tc.id } };
|
|
379
|
+
});
|
|
380
|
+
return {
|
|
381
|
+
candidates: [{ content: { parts, role: 'model' }, finishReason: 'TOOL_CALL', index: 0 }],
|
|
382
|
+
usageMetadata: {
|
|
383
|
+
promptTokenCount: openAiRes.usage?.prompt_tokens || 0,
|
|
384
|
+
candidatesTokenCount: openAiRes.usage?.completion_tokens || 0,
|
|
385
|
+
totalTokenCount: openAiRes.usage?.total_tokens || 0,
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const text = choice?.message?.content || '';
|
|
391
|
+
const dsml = parseDSMLToolCalls(text);
|
|
392
|
+
if (dsml && dsml.functionCalls.length > 0) {
|
|
393
|
+
const parts: GeminiPart[] = dsml.functionCalls.map((fc) => {
|
|
394
|
+
const na = normalizeToolArgs(fc.name, fc.args);
|
|
395
|
+
const tr = translateToolCallToNative(fc.name, na);
|
|
396
|
+
return { functionCall: { name: tr.name, args: tr.args as Record<string, unknown> } };
|
|
397
|
+
});
|
|
398
|
+
if (dsml.cleanText) parts.unshift({ text: dsml.cleanText });
|
|
399
|
+
return {
|
|
400
|
+
candidates: [{ content: { parts, role: 'model' }, finishReason: 'TOOL_CALL', index: 0 }],
|
|
401
|
+
usageMetadata: {
|
|
402
|
+
promptTokenCount: openAiRes.usage?.prompt_tokens || 0,
|
|
403
|
+
candidatesTokenCount: openAiRes.usage?.completion_tokens || 0,
|
|
404
|
+
totalTokenCount: openAiRes.usage?.total_tokens || 0,
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const reasoning = choice?.message?.reasoning_content || choice?.message?.reasoning || '';
|
|
410
|
+
const parts: GeminiPart[] = [];
|
|
411
|
+
if (reasoning) parts.push({ text: reasoning, thought: true });
|
|
412
|
+
if (text) parts.push({ text });
|
|
413
|
+
const finishReason = choice?.finish_reason === 'stop' ? 'STOP' : 'OTHER';
|
|
414
|
+
return {
|
|
415
|
+
candidates: [{ content: { parts, role: 'model' }, finishReason, index: 0 }],
|
|
416
|
+
usageMetadata: {
|
|
417
|
+
promptTokenCount: openAiRes.usage?.prompt_tokens || 0,
|
|
418
|
+
candidatesTokenCount: openAiRes.usage?.completion_tokens || 0,
|
|
419
|
+
totalTokenCount: openAiRes.usage?.total_tokens || 0,
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ─── STREAM CHUNK: OpenAI → Gemini ────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
export function mapOpenAIChunkToGemini(chunk: OpenAIResponse, modelName: string): GeminiCandidate | null {
|
|
427
|
+
const choice = chunk.choices?.[0];
|
|
428
|
+
if (!choice) return null;
|
|
429
|
+
const delta = choice.delta;
|
|
430
|
+
const streamId = ((chunk as Record<string, unknown>).id as string) || 'default_stream';
|
|
431
|
+
|
|
432
|
+
if (!activeStreamContexts.has(streamId)) {
|
|
433
|
+
activeStreamContexts.set(streamId, { accumulatedText: '', accumulatedReasoning: '', toolCalls: {} });
|
|
434
|
+
touchStateTimestamp(stateTimestamps.streamCtx, streamId);
|
|
435
|
+
}
|
|
436
|
+
const context = activeStreamContexts.get(streamId)!;
|
|
437
|
+
|
|
438
|
+
if (delta?.tool_calls && Array.isArray(delta.tool_calls)) {
|
|
439
|
+
for (const tc of delta.tool_calls) {
|
|
440
|
+
const idx = tc.index ?? 0;
|
|
441
|
+
if (!context.toolCalls[idx]) context.toolCalls[idx] = { id: '', name: '', arguments: '' };
|
|
442
|
+
if (tc.id) context.toolCalls[idx].id = tc.id;
|
|
443
|
+
if (tc.function?.name) context.toolCalls[idx].name += tc.function.name;
|
|
444
|
+
if (tc.function?.arguments) context.toolCalls[idx].arguments += tc.function.arguments;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
let text = delta?.content || '';
|
|
449
|
+
const reasoning = delta?.reasoning_content || delta?.reasoning || '';
|
|
450
|
+
if (reasoning) {
|
|
451
|
+
context.accumulatedReasoning += reasoning;
|
|
452
|
+
return { content: { parts: [{ text: reasoning, thought: true }], role: 'model' }, finishReason: 'OTHER', index: 0 };
|
|
453
|
+
}
|
|
454
|
+
if (text) context.accumulatedText += text;
|
|
455
|
+
|
|
456
|
+
const dsml = parseDSMLToolCalls(context.accumulatedText);
|
|
457
|
+
if (dsml && dsml.functionCalls.length > 0) {
|
|
458
|
+
const parts: GeminiPart[] = dsml.functionCalls.map((fc) => {
|
|
459
|
+
const na = normalizeToolArgs(fc.name, fc.args);
|
|
460
|
+
const tr = translateToolCallToNative(fc.name, na);
|
|
461
|
+
return { functionCall: { name: tr.name, args: tr.args as Record<string, unknown> } };
|
|
462
|
+
});
|
|
463
|
+
context.accumulatedText = '';
|
|
464
|
+
return { content: { parts, role: 'model' }, finishReason: 'TOOL_CALL', index: 0 };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const finishReason = choice.finish_reason;
|
|
468
|
+
if (finishReason === 'stop' || finishReason === 'length') {
|
|
469
|
+
// Check for pending native tool_calls before closing stream
|
|
470
|
+
const pendingToolCalls = Object.values(context.toolCalls).filter((tc) => tc.name && tc.arguments);
|
|
471
|
+
if (pendingToolCalls.length > 0) {
|
|
472
|
+
const parts: GeminiPart[] = pendingToolCalls.map((tc) => {
|
|
473
|
+
let args: ToolCallArgs = {};
|
|
474
|
+
try {
|
|
475
|
+
args = JSON.parse(tc.arguments);
|
|
476
|
+
} catch (_e) {
|
|
477
|
+
args = {};
|
|
478
|
+
}
|
|
479
|
+
args = normalizeToolArgs(tc.name, args) as ToolCallArgs;
|
|
480
|
+
const modelTCIds = modelToolCallIds.get(modelName) || {};
|
|
481
|
+
modelTCIds[tc.name] = tc.id;
|
|
482
|
+
modelToolCallIds.set(modelName, modelTCIds);
|
|
483
|
+
touchStateTimestamp(stateTimestamps.toolCallIds, modelName);
|
|
484
|
+
const translated = translateToolCallToNative(tc.name, args);
|
|
485
|
+
if (translated.name !== tc.name) {
|
|
486
|
+
translatedToolCalls.set(tc.id, {
|
|
487
|
+
originalName: tc.name,
|
|
488
|
+
translatedName: translated.name,
|
|
489
|
+
cmd: args.CommandLine || '',
|
|
490
|
+
cwd: args.Cwd || '',
|
|
491
|
+
});
|
|
492
|
+
touchStateTimestamp(stateTimestamps.translatedCalls, tc.id);
|
|
493
|
+
}
|
|
494
|
+
return { functionCall: { name: translated.name, args: translated.args as Record<string, unknown>, id: tc.id } };
|
|
495
|
+
});
|
|
496
|
+
activeStreamContexts.delete(streamId);
|
|
497
|
+
return { content: { parts, role: 'model' }, finishReason: 'TOOL_CALL', index: 0 };
|
|
498
|
+
}
|
|
499
|
+
// Check for accumulated DSML tool calls
|
|
500
|
+
if (context.accumulatedText) {
|
|
501
|
+
const dsml2 = parseDSMLToolCalls(context.accumulatedText);
|
|
502
|
+
if (dsml2 && dsml2.functionCalls.length > 0) {
|
|
503
|
+
const parts: GeminiPart[] = dsml2.functionCalls.map((fc) => {
|
|
504
|
+
const na = normalizeToolArgs(fc.name, fc.args);
|
|
505
|
+
const tr = translateToolCallToNative(fc.name, na);
|
|
506
|
+
return { functionCall: { name: tr.name, args: tr.args as Record<string, unknown> } };
|
|
507
|
+
});
|
|
508
|
+
if (dsml2.cleanText) parts.unshift({ text: dsml2.cleanText });
|
|
509
|
+
activeStreamContexts.delete(streamId);
|
|
510
|
+
return { content: { parts, role: 'model' }, finishReason: 'TOOL_CALL', index: 0 };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
activeStreamContexts.delete(streamId);
|
|
514
|
+
return { content: { parts: text ? [{ text }] : [], role: 'model' }, finishReason: 'STOP', index: 0 };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Only emit tool calls when finishReason signals completion (args are fully accumulated)
|
|
518
|
+
if (finishReason === 'tool_calls') {
|
|
519
|
+
const parts: GeminiPart[] = Object.values(context.toolCalls).map((tc) => {
|
|
520
|
+
let args: ToolCallArgs = {};
|
|
521
|
+
try {
|
|
522
|
+
args = JSON.parse(tc.arguments);
|
|
523
|
+
} catch (e) {
|
|
524
|
+
log.debug('[OpenAI] Stream tool args parse fallback:', (e as Error).message);
|
|
525
|
+
args = {};
|
|
526
|
+
}
|
|
527
|
+
args = normalizeToolArgs(tc.name, args) as ToolCallArgs;
|
|
528
|
+
const modelTCIds = modelToolCallIds.get(modelName) || {};
|
|
529
|
+
modelTCIds[tc.name] = tc.id;
|
|
530
|
+
modelToolCallIds.set(modelName, modelTCIds);
|
|
531
|
+
touchStateTimestamp(stateTimestamps.toolCallIds, modelName);
|
|
532
|
+
const translated = translateToolCallToNative(tc.name, args);
|
|
533
|
+
if (translated.name !== tc.name) {
|
|
534
|
+
translated.args = normalizeToolArgs(translated.name, translated.args) as Record<string, unknown>;
|
|
535
|
+
translatedToolCalls.set(tc.id, {
|
|
536
|
+
originalName: tc.name,
|
|
537
|
+
translatedName: translated.name,
|
|
538
|
+
cmd: args.CommandLine || '',
|
|
539
|
+
cwd: args.Cwd || '',
|
|
540
|
+
});
|
|
541
|
+
touchStateTimestamp(stateTimestamps.translatedCalls, tc.id);
|
|
542
|
+
}
|
|
543
|
+
return { functionCall: { name: translated.name, args: translated.args as Record<string, unknown>, id: tc.id } };
|
|
544
|
+
});
|
|
545
|
+
activeStreamContexts.delete(streamId);
|
|
546
|
+
return { content: { parts, role: 'model' }, finishReason: 'TOOL_CALL', index: 0 };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (text) {
|
|
550
|
+
return { content: { parts: [{ text }], role: 'model' }, finishReason: 'OTHER', index: 0 };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export { mapGeminiToolsToOpenAI };
|