pi-antigravity-rotator 2.2.1 → 2.2.2
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/CHANGELOG.md +12 -4
- package/README.md +5 -4
- package/package.json +3 -1
- package/src/compat/cache.ts +42 -0
- package/src/compat/model-specs.ts +78 -0
- package/src/compat/schema-sanitizer.ts +277 -0
- package/src/compat/translators.ts +1538 -0
- package/src/compat.ts +1535 -2444
- package/src/notification-poller.ts +1 -1
- package/src/telemetry.ts +1 -1
|
@@ -0,0 +1,1538 @@
|
|
|
1
|
+
import { logger, redactSensitive } from "../logger.js";
|
|
2
|
+
import { applyModelAlias } from "../types.js";
|
|
3
|
+
import type { RequestBody } from "../proxy.js";
|
|
4
|
+
import {
|
|
5
|
+
isRecord,
|
|
6
|
+
sanitizeGeminiSchema,
|
|
7
|
+
sanitizeClaudeViaGeminiSchema,
|
|
8
|
+
} from "./schema-sanitizer.js";
|
|
9
|
+
import {
|
|
10
|
+
getModelFamily,
|
|
11
|
+
getModelSpec,
|
|
12
|
+
isThinkingModel,
|
|
13
|
+
} from "./model-specs.js";
|
|
14
|
+
import {
|
|
15
|
+
thoughtSignatureCache,
|
|
16
|
+
getStoredResponse,
|
|
17
|
+
setStoredResponse,
|
|
18
|
+
makeCompatId,
|
|
19
|
+
} from "./cache.js";
|
|
20
|
+
|
|
21
|
+
const compatLogger = logger.child("compat");
|
|
22
|
+
|
|
23
|
+
const VALIDATION_LOG_MAX_CHARS = 200;
|
|
24
|
+
|
|
25
|
+
export function logValidationFailure(scope: string, payload: unknown): void {
|
|
26
|
+
const truncated = redactSensitive(JSON.stringify(payload));
|
|
27
|
+
const clipped =
|
|
28
|
+
truncated.length > VALIDATION_LOG_MAX_CHARS
|
|
29
|
+
? `${truncated.slice(0, VALIDATION_LOG_MAX_CHARS)}…[+${truncated.length - VALIDATION_LOG_MAX_CHARS} chars]`
|
|
30
|
+
: truncated;
|
|
31
|
+
compatLogger.warn(`${scope}: ${clipped}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ResponseOutputText {
|
|
35
|
+
type: "output_text";
|
|
36
|
+
text: string;
|
|
37
|
+
annotations: unknown[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ResponseMessageOutputItem {
|
|
41
|
+
id: string;
|
|
42
|
+
type: "message";
|
|
43
|
+
status: "completed";
|
|
44
|
+
role: "assistant";
|
|
45
|
+
content: ResponseOutputText[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ResponseFunctionCallOutputItem {
|
|
49
|
+
id: string;
|
|
50
|
+
type: "function_call";
|
|
51
|
+
call_id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
arguments: string;
|
|
54
|
+
status: "completed";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type ResponseOutputItem =
|
|
58
|
+
| ResponseMessageOutputItem
|
|
59
|
+
| ResponseFunctionCallOutputItem;
|
|
60
|
+
|
|
61
|
+
export interface ChatMessage {
|
|
62
|
+
role: "system" | "developer" | "user" | "assistant" | "model" | "tool";
|
|
63
|
+
content:
|
|
64
|
+
| string
|
|
65
|
+
| Array<{ type: string; text?: string; [key: string]: unknown }>
|
|
66
|
+
| null;
|
|
67
|
+
tool_calls?: OpenAIToolCall[];
|
|
68
|
+
tool_call_id?: string;
|
|
69
|
+
name?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface OpenAITool {
|
|
73
|
+
type: "function";
|
|
74
|
+
function: {
|
|
75
|
+
name: string;
|
|
76
|
+
description?: string;
|
|
77
|
+
parameters?: Record<string, unknown>;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface OpenAIToolCall {
|
|
82
|
+
id: string;
|
|
83
|
+
type: "function";
|
|
84
|
+
function: {
|
|
85
|
+
name: string;
|
|
86
|
+
arguments: string;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface OpenAIToolChoice {
|
|
91
|
+
type: "function";
|
|
92
|
+
function: { name: string };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface OpenAIChatCompletionRequest {
|
|
96
|
+
model: string;
|
|
97
|
+
messages: ChatMessage[];
|
|
98
|
+
stream?: boolean;
|
|
99
|
+
temperature?: number;
|
|
100
|
+
max_tokens?: number;
|
|
101
|
+
prompt?: string | string[];
|
|
102
|
+
input?: unknown;
|
|
103
|
+
tools?: OpenAITool[];
|
|
104
|
+
tool_choice?: unknown;
|
|
105
|
+
/** OpenAI-style reasoning effort. Mapped to Gemini thinkingLevel. */
|
|
106
|
+
reasoning_effort?: string;
|
|
107
|
+
[key: string]: unknown;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface OpenAIResponsesRequest {
|
|
111
|
+
model: string;
|
|
112
|
+
input?: unknown;
|
|
113
|
+
instructions?:
|
|
114
|
+
| string
|
|
115
|
+
| Array<{ type: string; text?: string; [key: string]: unknown }>
|
|
116
|
+
| null;
|
|
117
|
+
stream?: boolean;
|
|
118
|
+
temperature?: number;
|
|
119
|
+
max_output_tokens?: number;
|
|
120
|
+
tools?: Array<Record<string, unknown>>;
|
|
121
|
+
tool_choice?: unknown;
|
|
122
|
+
reasoning?: { effort?: string | null; [key: string]: unknown } | null;
|
|
123
|
+
metadata?: Record<string, string>;
|
|
124
|
+
store?: boolean;
|
|
125
|
+
previous_response_id?: string | null;
|
|
126
|
+
conversation?: unknown;
|
|
127
|
+
parallel_tool_calls?: boolean;
|
|
128
|
+
[key: string]: unknown;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface AnthropicMessagesRequest {
|
|
132
|
+
model: string;
|
|
133
|
+
messages: ChatMessage[];
|
|
134
|
+
system?:
|
|
135
|
+
| string
|
|
136
|
+
| Array<{ type: string; text?: string; [key: string]: unknown }>;
|
|
137
|
+
stream?: boolean;
|
|
138
|
+
max_tokens?: number;
|
|
139
|
+
temperature?: number;
|
|
140
|
+
[key: string]: unknown;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface CompatCompletion {
|
|
144
|
+
text: string;
|
|
145
|
+
thinkingText?: string; // Gemini thought blocks (thought: true), emitted as reasoning_content
|
|
146
|
+
inputTokens: number;
|
|
147
|
+
outputTokens: number;
|
|
148
|
+
responseId?: string;
|
|
149
|
+
toolCalls?: OpenAIToolCall[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type AntigravityPart =
|
|
153
|
+
| { text: string }
|
|
154
|
+
| { inlineData: { mimeType: string; data: string } };
|
|
155
|
+
export type GeminiContent = { role: "user" | "model"; parts: unknown[] };
|
|
156
|
+
|
|
157
|
+
interface GeminiFunctionDeclaration {
|
|
158
|
+
name: string;
|
|
159
|
+
description?: string;
|
|
160
|
+
parameters?: Record<string, unknown>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface GeminiToolConfig {
|
|
164
|
+
functionCallingConfig: {
|
|
165
|
+
mode: "AUTO" | "NONE" | "ANY";
|
|
166
|
+
allowedFunctionNames?: string[];
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export type ResponsesConversionResult = {
|
|
171
|
+
chatRequest: OpenAIChatCompletionRequest;
|
|
172
|
+
inputItems: Array<Record<string, unknown>>;
|
|
173
|
+
conversationMessages: ChatMessage[];
|
|
174
|
+
previousResponseId: string | null;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export function isNonEmptyString(value: unknown): value is string {
|
|
178
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function cleanCacheControl<T>(content: T): T {
|
|
182
|
+
if (!Array.isArray(content)) return content;
|
|
183
|
+
return content.map((block: Record<string, unknown>) => {
|
|
184
|
+
if (!block || typeof block !== "object") return block;
|
|
185
|
+
if ("cache_control" in block) {
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
187
|
+
const { cache_control: _cc, ...rest } = block;
|
|
188
|
+
return rest;
|
|
189
|
+
}
|
|
190
|
+
return block;
|
|
191
|
+
}) as T;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function extractText(content: ChatMessage["content"]): string {
|
|
195
|
+
if (typeof content === "string") return content;
|
|
196
|
+
if (!Array.isArray(content)) return "";
|
|
197
|
+
return cleanCacheControl(content)
|
|
198
|
+
.filter(
|
|
199
|
+
(p: { type?: string; text?: string; thinking?: string }) =>
|
|
200
|
+
(p.type === "text" && typeof p.text === "string") ||
|
|
201
|
+
(p.type === "thinking" && typeof p.thinking === "string"),
|
|
202
|
+
)
|
|
203
|
+
.map((p: { type?: string; text?: string; thinking?: string }) =>
|
|
204
|
+
p.type === "thinking"
|
|
205
|
+
? `[Thinking]\n${p.thinking}\n[/Thinking]`
|
|
206
|
+
: (p.text as string),
|
|
207
|
+
)
|
|
208
|
+
.join("\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function dataUrlToInlineData(url: string): AntigravityPart | null {
|
|
212
|
+
const match = url.match(/^data:([^;,]+);base64,(.+)$/s);
|
|
213
|
+
if (!match) return null;
|
|
214
|
+
return { inlineData: { mimeType: match[1], data: match[2] } };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function extractParts(
|
|
218
|
+
content: ChatMessage["content"],
|
|
219
|
+
): AntigravityPart[] {
|
|
220
|
+
if (content === null) return [];
|
|
221
|
+
if (typeof content === "string") return content ? [{ text: content }] : [];
|
|
222
|
+
if (!Array.isArray(content)) return [];
|
|
223
|
+
const parts: AntigravityPart[] = [];
|
|
224
|
+
for (const part of content) {
|
|
225
|
+
if (part.type === "text" && typeof part.text === "string" && part.text) {
|
|
226
|
+
parts.push({ text: part.text });
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (
|
|
230
|
+
part.type === "thinking" &&
|
|
231
|
+
typeof part.thinking === "string" &&
|
|
232
|
+
part.thinking
|
|
233
|
+
) {
|
|
234
|
+
parts.push({ text: `[Thinking]\n${part.thinking}\n[/Thinking]` });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (
|
|
238
|
+
part.type === "image_url" &&
|
|
239
|
+
isRecord(part.image_url) &&
|
|
240
|
+
typeof part.image_url.url === "string"
|
|
241
|
+
) {
|
|
242
|
+
const inline = dataUrlToInlineData(part.image_url.url);
|
|
243
|
+
if (inline) parts.push(inline);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (
|
|
247
|
+
part.type === "image" &&
|
|
248
|
+
isRecord(part.source) &&
|
|
249
|
+
part.source.type === "base64" &&
|
|
250
|
+
typeof part.source.media_type === "string" &&
|
|
251
|
+
typeof part.source.data === "string"
|
|
252
|
+
) {
|
|
253
|
+
parts.push({
|
|
254
|
+
inlineData: {
|
|
255
|
+
mimeType: part.source.media_type,
|
|
256
|
+
data: part.source.data,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return parts;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function convertOpenAIToolsToGemini(
|
|
265
|
+
tools: OpenAITool[],
|
|
266
|
+
isClaude: boolean = false,
|
|
267
|
+
): { functionDeclarations: GeminiFunctionDeclaration[] }[] {
|
|
268
|
+
const decls: GeminiFunctionDeclaration[] = tools
|
|
269
|
+
.filter((t) => t.type === "function" && isNonEmptyString(t.function?.name))
|
|
270
|
+
.map((t) => {
|
|
271
|
+
const sanitized = t.function.parameters
|
|
272
|
+
? ((isClaude
|
|
273
|
+
? sanitizeClaudeViaGeminiSchema(t.function.parameters)
|
|
274
|
+
: sanitizeGeminiSchema(t.function.parameters)) as Record<
|
|
275
|
+
string,
|
|
276
|
+
unknown
|
|
277
|
+
>)
|
|
278
|
+
: undefined;
|
|
279
|
+
return {
|
|
280
|
+
name: t.function.name,
|
|
281
|
+
...(t.function.description
|
|
282
|
+
? { description: t.function.description }
|
|
283
|
+
: {}),
|
|
284
|
+
...(sanitized ? { parameters: sanitized } : {}),
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
return decls.length > 0 ? [{ functionDeclarations: decls }] : [];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function convertToolChoiceToGemini(
|
|
291
|
+
toolChoice: unknown,
|
|
292
|
+
): GeminiToolConfig | undefined {
|
|
293
|
+
if (!toolChoice || toolChoice === "none")
|
|
294
|
+
return { functionCallingConfig: { mode: "NONE" } };
|
|
295
|
+
if (toolChoice === "auto" || toolChoice === "required")
|
|
296
|
+
return { functionCallingConfig: { mode: "AUTO" } };
|
|
297
|
+
if (
|
|
298
|
+
isRecord(toolChoice) &&
|
|
299
|
+
toolChoice.type === "function" &&
|
|
300
|
+
isRecord(toolChoice.function) &&
|
|
301
|
+
isNonEmptyString(toolChoice.function.name)
|
|
302
|
+
) {
|
|
303
|
+
return {
|
|
304
|
+
functionCallingConfig: {
|
|
305
|
+
mode: "ANY",
|
|
306
|
+
allowedFunctionNames: [toolChoice.function.name],
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return { functionCallingConfig: { mode: "AUTO" } };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function validateMessages(value: unknown): value is ChatMessage[] {
|
|
314
|
+
return (
|
|
315
|
+
Array.isArray(value) &&
|
|
316
|
+
value.every((msg) => {
|
|
317
|
+
if (!isRecord(msg)) return false;
|
|
318
|
+
if (
|
|
319
|
+
!["system", "developer", "user", "assistant", "model", "tool"].includes(
|
|
320
|
+
String(msg.role),
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
return false;
|
|
324
|
+
return (
|
|
325
|
+
typeof msg.content === "string" ||
|
|
326
|
+
msg.content === null ||
|
|
327
|
+
Array.isArray(msg.content)
|
|
328
|
+
);
|
|
329
|
+
})
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function extractTextFromUnknownContent(content: unknown): string {
|
|
334
|
+
if (typeof content === "string") return content;
|
|
335
|
+
if (!Array.isArray(content)) return "";
|
|
336
|
+
return content
|
|
337
|
+
.map((part) => {
|
|
338
|
+
if (typeof part === "string") return part;
|
|
339
|
+
if (!isRecord(part)) return "";
|
|
340
|
+
if (typeof part.text === "string") return part.text;
|
|
341
|
+
if (typeof part.input_text === "string") return part.input_text;
|
|
342
|
+
if (typeof part.output_text === "string") return part.output_text;
|
|
343
|
+
return "";
|
|
344
|
+
})
|
|
345
|
+
.filter(Boolean)
|
|
346
|
+
.join("\n");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function normalizeContentBlocks(
|
|
350
|
+
content: unknown,
|
|
351
|
+
): ChatMessage["content"] {
|
|
352
|
+
if (typeof content === "string" || content === null) return content;
|
|
353
|
+
if (!Array.isArray(content)) return extractTextFromUnknownContent(content);
|
|
354
|
+
const blocks = content.flatMap((part) => {
|
|
355
|
+
if (typeof part === "string") return [{ type: "text", text: part }];
|
|
356
|
+
if (!isRecord(part)) return [];
|
|
357
|
+
if (part.type === "input_text" && typeof part.text === "string")
|
|
358
|
+
return [{ type: "text", text: part.text }];
|
|
359
|
+
if (part.type === "output_text" && typeof part.text === "string")
|
|
360
|
+
return [{ type: "text", text: part.text }];
|
|
361
|
+
if (typeof part.text === "string")
|
|
362
|
+
return [
|
|
363
|
+
{
|
|
364
|
+
...part,
|
|
365
|
+
type: typeof part.type === "string" ? part.type : "text",
|
|
366
|
+
text: part.text,
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
if (typeof part.input_text === "string")
|
|
370
|
+
return [{ type: "text", text: part.input_text }];
|
|
371
|
+
if (typeof part.output_text === "string")
|
|
372
|
+
return [{ type: "text", text: part.output_text }];
|
|
373
|
+
if (part.type === "input_image" && typeof part.image_url === "string") {
|
|
374
|
+
return [{ type: "image_url", image_url: { url: part.image_url } }];
|
|
375
|
+
}
|
|
376
|
+
return [part as { type: string; text?: string; [key: string]: unknown }];
|
|
377
|
+
});
|
|
378
|
+
return blocks.length > 0 ? blocks : "";
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function normalizeInstructionsContent(
|
|
382
|
+
content: OpenAIResponsesRequest["instructions"],
|
|
383
|
+
): ChatMessage["content"] {
|
|
384
|
+
if (typeof content === "string" || content === null || content === undefined)
|
|
385
|
+
return content ?? "";
|
|
386
|
+
return normalizeContentBlocks(content);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function contentToResponseInputBlocks(
|
|
390
|
+
content: ChatMessage["content"],
|
|
391
|
+
role: string,
|
|
392
|
+
): Array<Record<string, unknown>> {
|
|
393
|
+
if (typeof content === "string") {
|
|
394
|
+
if (!content) return [];
|
|
395
|
+
return [
|
|
396
|
+
{
|
|
397
|
+
type:
|
|
398
|
+
role === "assistant" || role === "model"
|
|
399
|
+
? "output_text"
|
|
400
|
+
: "input_text",
|
|
401
|
+
text: content,
|
|
402
|
+
},
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
if (!Array.isArray(content)) return [];
|
|
406
|
+
return cleanCacheControl(content).flatMap((part) => {
|
|
407
|
+
if (!isRecord(part)) return [];
|
|
408
|
+
if (typeof part.text === "string") {
|
|
409
|
+
return [
|
|
410
|
+
{
|
|
411
|
+
type:
|
|
412
|
+
role === "assistant" || role === "model"
|
|
413
|
+
? "output_text"
|
|
414
|
+
: "input_text",
|
|
415
|
+
text: part.text,
|
|
416
|
+
},
|
|
417
|
+
];
|
|
418
|
+
}
|
|
419
|
+
if (
|
|
420
|
+
part.type === "image_url" &&
|
|
421
|
+
isRecord(part.image_url) &&
|
|
422
|
+
typeof part.image_url.url === "string"
|
|
423
|
+
) {
|
|
424
|
+
return [{ type: "input_image", image_url: part.image_url.url }];
|
|
425
|
+
}
|
|
426
|
+
return [part];
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function parseResponsesInput(
|
|
431
|
+
input: unknown,
|
|
432
|
+
callIdToName: Map<string, string> = new Map(),
|
|
433
|
+
): ParsedResponsesInput {
|
|
434
|
+
if (typeof input === "string") {
|
|
435
|
+
return {
|
|
436
|
+
inputItems: [
|
|
437
|
+
{
|
|
438
|
+
id: makeCompatId("in"),
|
|
439
|
+
type: "message",
|
|
440
|
+
role: "user",
|
|
441
|
+
content: [{ type: "input_text", text: input }],
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
messages: [{ role: "user", content: input }],
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (!Array.isArray(input)) return { inputItems: [], messages: [] };
|
|
448
|
+
|
|
449
|
+
const inputItems: Array<Record<string, unknown>> = [];
|
|
450
|
+
const messages: ChatMessage[] = [];
|
|
451
|
+
|
|
452
|
+
for (const rawItem of input) {
|
|
453
|
+
if (typeof rawItem === "string") {
|
|
454
|
+
inputItems.push({
|
|
455
|
+
id: makeCompatId("in"),
|
|
456
|
+
type: "message",
|
|
457
|
+
role: "user",
|
|
458
|
+
content: [{ type: "input_text", text: rawItem }],
|
|
459
|
+
});
|
|
460
|
+
messages.push({ role: "user", content: rawItem });
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (!isRecord(rawItem)) continue;
|
|
464
|
+
|
|
465
|
+
if (
|
|
466
|
+
rawItem.type === "function_call_output" &&
|
|
467
|
+
typeof rawItem.call_id === "string"
|
|
468
|
+
) {
|
|
469
|
+
const outputText =
|
|
470
|
+
typeof rawItem.output === "string"
|
|
471
|
+
? rawItem.output
|
|
472
|
+
: JSON.stringify(rawItem.output ?? "");
|
|
473
|
+
const toolName =
|
|
474
|
+
typeof rawItem.name === "string"
|
|
475
|
+
? rawItem.name
|
|
476
|
+
: callIdToName.get(rawItem.call_id) || "unknown";
|
|
477
|
+
inputItems.push({
|
|
478
|
+
id: makeCompatId("in"),
|
|
479
|
+
type: "function_call_output",
|
|
480
|
+
call_id: rawItem.call_id,
|
|
481
|
+
output: outputText,
|
|
482
|
+
});
|
|
483
|
+
messages.push({
|
|
484
|
+
role: "tool",
|
|
485
|
+
content: outputText,
|
|
486
|
+
name: toolName,
|
|
487
|
+
tool_call_id: rawItem.call_id,
|
|
488
|
+
});
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (rawItem.type === "function_call" && typeof rawItem.name === "string") {
|
|
493
|
+
const callId =
|
|
494
|
+
typeof rawItem.call_id === "string"
|
|
495
|
+
? rawItem.call_id
|
|
496
|
+
: makeCompatId("call");
|
|
497
|
+
const args =
|
|
498
|
+
typeof rawItem.arguments === "string"
|
|
499
|
+
? rawItem.arguments
|
|
500
|
+
: JSON.stringify(rawItem.arguments ?? {});
|
|
501
|
+
inputItems.push({
|
|
502
|
+
id: makeCompatId("in"),
|
|
503
|
+
type: "function_call",
|
|
504
|
+
call_id: callId,
|
|
505
|
+
name: rawItem.name,
|
|
506
|
+
arguments: args,
|
|
507
|
+
});
|
|
508
|
+
const lastMsg = messages[messages.length - 1];
|
|
509
|
+
if (
|
|
510
|
+
lastMsg &&
|
|
511
|
+
lastMsg.role === "assistant" &&
|
|
512
|
+
Array.isArray(lastMsg.tool_calls)
|
|
513
|
+
) {
|
|
514
|
+
lastMsg.tool_calls.push({
|
|
515
|
+
id: callId,
|
|
516
|
+
type: "function",
|
|
517
|
+
function: { name: rawItem.name, arguments: args },
|
|
518
|
+
});
|
|
519
|
+
} else {
|
|
520
|
+
messages.push({
|
|
521
|
+
role: "assistant",
|
|
522
|
+
content: null,
|
|
523
|
+
tool_calls: [
|
|
524
|
+
{
|
|
525
|
+
id: callId,
|
|
526
|
+
type: "function",
|
|
527
|
+
function: { name: rawItem.name, arguments: args },
|
|
528
|
+
},
|
|
529
|
+
],
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const isMessage =
|
|
536
|
+
rawItem.type === "message" ||
|
|
537
|
+
typeof rawItem.role === "string" ||
|
|
538
|
+
"content" in rawItem;
|
|
539
|
+
if (!isMessage) continue;
|
|
540
|
+
const rawRole = typeof rawItem.role === "string" ? rawItem.role : "user";
|
|
541
|
+
const role = rawRole === "developer" ? "system" : rawRole;
|
|
542
|
+
if (!["system", "user", "assistant", "model", "tool"].includes(role))
|
|
543
|
+
continue;
|
|
544
|
+
const content =
|
|
545
|
+
"content" in rawItem
|
|
546
|
+
? normalizeContentBlocks(rawItem.content)
|
|
547
|
+
: extractTextFromUnknownContent(rawItem);
|
|
548
|
+
inputItems.push({
|
|
549
|
+
id: makeCompatId("in"),
|
|
550
|
+
type: "message",
|
|
551
|
+
role: rawRole,
|
|
552
|
+
content: contentToResponseInputBlocks(content, role),
|
|
553
|
+
});
|
|
554
|
+
messages.push({ role: role as ChatMessage["role"], content });
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return { inputItems, messages };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
type ParsedResponsesInput = {
|
|
561
|
+
inputItems: Array<Record<string, unknown>>;
|
|
562
|
+
messages: ChatMessage[];
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
export function messagesFromResponsesInput(
|
|
566
|
+
input: unknown,
|
|
567
|
+
): ChatMessage[] | null {
|
|
568
|
+
if (typeof input === "string") return [{ role: "user", content: input }];
|
|
569
|
+
if (!Array.isArray(input)) return null;
|
|
570
|
+
|
|
571
|
+
const messages: ChatMessage[] = [];
|
|
572
|
+
for (const item of input) {
|
|
573
|
+
if (typeof item === "string") {
|
|
574
|
+
messages.push({ role: "user", content: item });
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (!isRecord(item)) continue;
|
|
578
|
+
const role = typeof item.role === "string" ? item.role : "user";
|
|
579
|
+
if (!["system", "user", "assistant", "model", "tool"].includes(role))
|
|
580
|
+
continue;
|
|
581
|
+
const content =
|
|
582
|
+
"content" in item
|
|
583
|
+
? normalizeContentBlocks(item.content)
|
|
584
|
+
: extractTextFromUnknownContent(item);
|
|
585
|
+
messages.push({ role: role as ChatMessage["role"], content });
|
|
586
|
+
}
|
|
587
|
+
return messages.length > 0 ? messages : null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export function messagesFromLooseMessages(
|
|
591
|
+
value: unknown,
|
|
592
|
+
): ChatMessage[] | null {
|
|
593
|
+
if (typeof value === "string") return [{ role: "user", content: value }];
|
|
594
|
+
if (isRecord(value)) return messagesFromResponsesInput([value]);
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function messagesFromAntigravityRequest(
|
|
599
|
+
value: Record<string, unknown>,
|
|
600
|
+
): ChatMessage[] | null {
|
|
601
|
+
const request = isRecord(value.request) ? value.request : null;
|
|
602
|
+
if (!request || !Array.isArray(request.contents)) return null;
|
|
603
|
+
const messages: ChatMessage[] = [];
|
|
604
|
+
if (
|
|
605
|
+
isRecord(request.systemInstruction) &&
|
|
606
|
+
Array.isArray(request.systemInstruction.parts)
|
|
607
|
+
) {
|
|
608
|
+
const systemText = request.systemInstruction.parts
|
|
609
|
+
.map((part) =>
|
|
610
|
+
isRecord(part) && typeof part.text === "string" ? part.text : "",
|
|
611
|
+
)
|
|
612
|
+
.filter(Boolean)
|
|
613
|
+
.join("\n");
|
|
614
|
+
if (systemText) messages.push({ role: "system", content: systemText });
|
|
615
|
+
}
|
|
616
|
+
for (const turn of request.contents) {
|
|
617
|
+
if (!isRecord(turn) || !Array.isArray(turn.parts)) continue;
|
|
618
|
+
const role =
|
|
619
|
+
turn.role === "model" || turn.role === "assistant" ? "assistant" : "user";
|
|
620
|
+
const content = turn.parts
|
|
621
|
+
.map((part) =>
|
|
622
|
+
isRecord(part) && typeof part.text === "string" ? part.text : "",
|
|
623
|
+
)
|
|
624
|
+
.filter(Boolean)
|
|
625
|
+
.join("\n");
|
|
626
|
+
messages.push({ role, content });
|
|
627
|
+
}
|
|
628
|
+
return messages.length > 0 ? messages : null;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export function normalizeOpenAIChatCompletionRequest(value: unknown): unknown {
|
|
632
|
+
if (!isRecord(value) || Array.isArray(value.messages)) return value;
|
|
633
|
+
const messages = (() => {
|
|
634
|
+
if ("messages" in value) return messagesFromLooseMessages(value.messages);
|
|
635
|
+
if (typeof value.prompt === "string")
|
|
636
|
+
return [{ role: "user", content: value.prompt }];
|
|
637
|
+
if (Array.isArray(value.prompt))
|
|
638
|
+
return (
|
|
639
|
+
messagesFromResponsesInput(value.prompt) ??
|
|
640
|
+
value.prompt.map((prompt) => ({
|
|
641
|
+
role: "user",
|
|
642
|
+
content: String(prompt),
|
|
643
|
+
}))
|
|
644
|
+
);
|
|
645
|
+
if ("input" in value) return messagesFromResponsesInput(value.input);
|
|
646
|
+
return messagesFromAntigravityRequest(value);
|
|
647
|
+
})();
|
|
648
|
+
return messages ? { ...value, messages } : value;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export function normalizeOpenAIResponsesRequest(value: unknown): unknown {
|
|
652
|
+
if (!isRecord(value)) return value;
|
|
653
|
+
|
|
654
|
+
let normalized: Record<string, unknown> = { ...value };
|
|
655
|
+
if (Array.isArray(value.tools)) {
|
|
656
|
+
const before = value.tools.length;
|
|
657
|
+
const filtered: unknown[] = [];
|
|
658
|
+
for (const t of value.tools) {
|
|
659
|
+
if (!isRecord(t) || typeof t.type !== "string") continue;
|
|
660
|
+
if (t.type !== "function") continue;
|
|
661
|
+
|
|
662
|
+
if (isNonEmptyString(t.name) && !isRecord(t.function)) {
|
|
663
|
+
filtered.push({
|
|
664
|
+
type: "function",
|
|
665
|
+
function: {
|
|
666
|
+
name: t.name,
|
|
667
|
+
...(typeof t.description === "string"
|
|
668
|
+
? { description: t.description }
|
|
669
|
+
: {}),
|
|
670
|
+
...(isRecord(t.parameters) ? { parameters: t.parameters } : {}),
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (isRecord(t.function) && isNonEmptyString(t.function.name)) {
|
|
677
|
+
filtered.push(t);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const dropped = before - filtered.length;
|
|
681
|
+
if (dropped > 0) {
|
|
682
|
+
compatLogger.warn(
|
|
683
|
+
`Filtered ${dropped} unsupported/unnamed tool(s) from Responses request (kept ${filtered.length} function tools)`,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
normalized = {
|
|
687
|
+
...normalized,
|
|
688
|
+
tools: filtered.length > 0 ? filtered : undefined,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if ("input" in normalized) return normalized;
|
|
693
|
+
if ("messages" in normalized)
|
|
694
|
+
return { ...normalized, input: normalized.messages };
|
|
695
|
+
if ("prompt" in normalized)
|
|
696
|
+
return { ...normalized, input: normalized.prompt };
|
|
697
|
+
return normalized;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function normalizeAnthropicMessagesRequest(value: unknown): unknown {
|
|
701
|
+
if (!isRecord(value) || Array.isArray(value.messages)) return value;
|
|
702
|
+
const messages =
|
|
703
|
+
"messages" in value
|
|
704
|
+
? messagesFromLooseMessages(value.messages)
|
|
705
|
+
: "input" in value
|
|
706
|
+
? messagesFromResponsesInput(value.input)
|
|
707
|
+
: messagesFromAntigravityRequest(value);
|
|
708
|
+
return messages ? { ...value, messages } : value;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export function convertResponsesToChatRequest(
|
|
712
|
+
input: OpenAIResponsesRequest,
|
|
713
|
+
): ResponsesConversionResult {
|
|
714
|
+
const previousResponseId = input.previous_response_id ?? null;
|
|
715
|
+
const previous = previousResponseId
|
|
716
|
+
? getStoredResponse(previousResponseId)
|
|
717
|
+
: null;
|
|
718
|
+
if (previousResponseId && !previous) {
|
|
719
|
+
throw new Error(`previous_response_id not found: ${previousResponseId}`);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const previousCallIdToName = previous
|
|
723
|
+
? new Map(Object.entries(previous.callIdToName))
|
|
724
|
+
: new Map<string, string>();
|
|
725
|
+
const previousConversationMessages: ChatMessage[] = previous
|
|
726
|
+
? (previous.conversationMessages as unknown as ChatMessage[])
|
|
727
|
+
: [];
|
|
728
|
+
|
|
729
|
+
const parsed = parseResponsesInput(input.input, previousCallIdToName);
|
|
730
|
+
const conversationMessages = [
|
|
731
|
+
...previousConversationMessages,
|
|
732
|
+
...parsed.messages,
|
|
733
|
+
];
|
|
734
|
+
const chatMessages = [
|
|
735
|
+
...(input.instructions
|
|
736
|
+
? [
|
|
737
|
+
{
|
|
738
|
+
role: "system" as const,
|
|
739
|
+
content: normalizeInstructionsContent(input.instructions),
|
|
740
|
+
},
|
|
741
|
+
]
|
|
742
|
+
: []),
|
|
743
|
+
...conversationMessages,
|
|
744
|
+
];
|
|
745
|
+
|
|
746
|
+
return {
|
|
747
|
+
chatRequest: {
|
|
748
|
+
model: input.model,
|
|
749
|
+
messages: chatMessages,
|
|
750
|
+
stream: input.stream,
|
|
751
|
+
temperature: input.temperature,
|
|
752
|
+
max_tokens: input.max_output_tokens,
|
|
753
|
+
tools: input.tools as OpenAITool[] | undefined,
|
|
754
|
+
tool_choice: input.tool_choice,
|
|
755
|
+
reasoning_effort:
|
|
756
|
+
typeof input.reasoning?.effort === "string"
|
|
757
|
+
? input.reasoning.effort
|
|
758
|
+
: undefined,
|
|
759
|
+
parallel_tool_calls: input.parallel_tool_calls,
|
|
760
|
+
},
|
|
761
|
+
inputItems: parsed.inputItems,
|
|
762
|
+
conversationMessages,
|
|
763
|
+
previousResponseId,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export function openAIToAntigravityBody(
|
|
768
|
+
input: OpenAIChatCompletionRequest,
|
|
769
|
+
): RequestBody {
|
|
770
|
+
const systemParts: string[] = [];
|
|
771
|
+
const conversationMessages = input.messages.filter((msg) => {
|
|
772
|
+
if (msg.role === "system" || msg.role === "developer") {
|
|
773
|
+
const text =
|
|
774
|
+
typeof msg.content === "string"
|
|
775
|
+
? msg.content
|
|
776
|
+
: extractText(msg.content);
|
|
777
|
+
if (text) systemParts.push(text);
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
return true;
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const isClaude = /^claude-/i.test(input.model);
|
|
784
|
+
const isThinking = isThinkingModel(input.model);
|
|
785
|
+
const isGeminiThinking = !isClaude && isThinking;
|
|
786
|
+
|
|
787
|
+
const contents: GeminiContent[] = [];
|
|
788
|
+
for (let i = 0; i < conversationMessages.length; i++) {
|
|
789
|
+
const msg = conversationMessages[i];
|
|
790
|
+
if (msg.role === "assistant" || msg.role === "model") {
|
|
791
|
+
let msgToolCalls = msg.tool_calls;
|
|
792
|
+
if (Array.isArray(msgToolCalls) && msgToolCalls.length > 0) {
|
|
793
|
+
const completedToolCallIds = new Set<string>();
|
|
794
|
+
let j = i + 1;
|
|
795
|
+
while (
|
|
796
|
+
j < conversationMessages.length &&
|
|
797
|
+
conversationMessages[j].role === "tool"
|
|
798
|
+
) {
|
|
799
|
+
const toolCallId = conversationMessages[j].tool_call_id;
|
|
800
|
+
if (typeof toolCallId === "string") {
|
|
801
|
+
completedToolCallIds.add(toolCallId);
|
|
802
|
+
}
|
|
803
|
+
j++;
|
|
804
|
+
}
|
|
805
|
+
msgToolCalls = msgToolCalls.filter(
|
|
806
|
+
(tc) => tc.id && completedToolCallIds.has(tc.id),
|
|
807
|
+
);
|
|
808
|
+
if (msgToolCalls.length === 0) {
|
|
809
|
+
msgToolCalls = undefined;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const hasMissingSig =
|
|
814
|
+
isGeminiThinking &&
|
|
815
|
+
Array.isArray(msgToolCalls) &&
|
|
816
|
+
msgToolCalls.length > 0 &&
|
|
817
|
+
!thoughtSignatureCache.has(msgToolCalls[0].id);
|
|
818
|
+
|
|
819
|
+
if (hasMissingSig) {
|
|
820
|
+
const toolNames = msgToolCalls!
|
|
821
|
+
.map((tc) => tc.function.name)
|
|
822
|
+
.join(", ");
|
|
823
|
+
const resultParts: string[] = [];
|
|
824
|
+
while (
|
|
825
|
+
i + 1 < conversationMessages.length &&
|
|
826
|
+
conversationMessages[i + 1].role === "tool"
|
|
827
|
+
) {
|
|
828
|
+
i++;
|
|
829
|
+
const toolMsg = conversationMessages[i];
|
|
830
|
+
const toolText =
|
|
831
|
+
typeof toolMsg.content === "string"
|
|
832
|
+
? toolMsg.content
|
|
833
|
+
: extractText(toolMsg.content);
|
|
834
|
+
resultParts.push(
|
|
835
|
+
`${toolMsg.name || "tool"}: ${toolText.slice(0, 500)}`,
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
const summaryText = `[Context: The assistant used tools (${toolNames}) and received results:\n${resultParts.join("\n")}]`;
|
|
839
|
+
contents.push({ role: "user", parts: [{ text: summaryText }] });
|
|
840
|
+
contents.push({
|
|
841
|
+
role: "model",
|
|
842
|
+
parts: [{ text: "Understood, I have the tool results." }],
|
|
843
|
+
});
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const parts: unknown[] = [];
|
|
848
|
+
if (msg.content) {
|
|
849
|
+
const textContent =
|
|
850
|
+
typeof msg.content === "string"
|
|
851
|
+
? msg.content
|
|
852
|
+
: extractText(msg.content);
|
|
853
|
+
if (textContent) parts.push({ text: textContent });
|
|
854
|
+
}
|
|
855
|
+
if (Array.isArray(msgToolCalls) && msgToolCalls.length > 0) {
|
|
856
|
+
let isFirstInMessage = true;
|
|
857
|
+
for (const tc of msgToolCalls) {
|
|
858
|
+
let args: unknown;
|
|
859
|
+
try {
|
|
860
|
+
args =
|
|
861
|
+
typeof tc.function.arguments === "string"
|
|
862
|
+
? JSON.parse(tc.function.arguments)
|
|
863
|
+
: tc.function.arguments;
|
|
864
|
+
} catch {
|
|
865
|
+
args = {};
|
|
866
|
+
}
|
|
867
|
+
const cachedSig = isFirstInMessage
|
|
868
|
+
? thoughtSignatureCache.get(tc.id)
|
|
869
|
+
: undefined;
|
|
870
|
+
parts.push({
|
|
871
|
+
...(cachedSig ? { thoughtSignature: cachedSig } : {}),
|
|
872
|
+
functionCall: {
|
|
873
|
+
...(isClaude ? { id: tc.id } : {}),
|
|
874
|
+
name: tc.function.name,
|
|
875
|
+
args,
|
|
876
|
+
},
|
|
877
|
+
});
|
|
878
|
+
isFirstInMessage = false;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (parts.length === 0) {
|
|
882
|
+
parts.push({ text: "..." });
|
|
883
|
+
}
|
|
884
|
+
if (parts.length > 0) {
|
|
885
|
+
if (isClaude) {
|
|
886
|
+
const lastContent = contents[contents.length - 1];
|
|
887
|
+
const prevHasFunctionCall =
|
|
888
|
+
lastContent &&
|
|
889
|
+
lastContent.role === "model" &&
|
|
890
|
+
lastContent.parts.some((p: any) => p.functionCall);
|
|
891
|
+
const hasFunctionCall = parts.some((p: any) => p.functionCall);
|
|
892
|
+
if (prevHasFunctionCall && !hasFunctionCall) {
|
|
893
|
+
// Skip
|
|
894
|
+
} else if (hasFunctionCall) {
|
|
895
|
+
const fcOnly = parts.filter((p: any) => p.functionCall);
|
|
896
|
+
if (prevHasFunctionCall) {
|
|
897
|
+
lastContent.parts.push(...fcOnly);
|
|
898
|
+
} else {
|
|
899
|
+
contents.push({ role: "model", parts: fcOnly });
|
|
900
|
+
}
|
|
901
|
+
} else {
|
|
902
|
+
contents.push({ role: "model", parts });
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
contents.push({ role: "model", parts });
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
} else if (msg.role === "tool") {
|
|
909
|
+
const responseText =
|
|
910
|
+
typeof msg.content === "string"
|
|
911
|
+
? msg.content
|
|
912
|
+
: extractText(msg.content);
|
|
913
|
+
const fnName = msg.name || "unknown";
|
|
914
|
+
const toolCallId = msg.tool_call_id;
|
|
915
|
+
let responseData: unknown;
|
|
916
|
+
try {
|
|
917
|
+
const parsed = JSON.parse(responseText);
|
|
918
|
+
responseData =
|
|
919
|
+
parsed !== null &&
|
|
920
|
+
typeof parsed === "object" &&
|
|
921
|
+
!Array.isArray(parsed)
|
|
922
|
+
? parsed
|
|
923
|
+
: { output: parsed };
|
|
924
|
+
} catch {
|
|
925
|
+
responseData = { output: responseText };
|
|
926
|
+
}
|
|
927
|
+
const toolImages: AntigravityPart[] = [];
|
|
928
|
+
if (msg.content && Array.isArray(msg.content)) {
|
|
929
|
+
for (const part of msg.content) {
|
|
930
|
+
if (
|
|
931
|
+
part.type === "image_url" &&
|
|
932
|
+
isRecord(part.image_url) &&
|
|
933
|
+
typeof part.image_url.url === "string"
|
|
934
|
+
) {
|
|
935
|
+
const inlineData = dataUrlToInlineData(part.image_url.url);
|
|
936
|
+
if (inlineData) toolImages.push(inlineData);
|
|
937
|
+
} else if (
|
|
938
|
+
part.type === "image" &&
|
|
939
|
+
isRecord(part.source) &&
|
|
940
|
+
typeof part.source.data === "string"
|
|
941
|
+
) {
|
|
942
|
+
const mediaType =
|
|
943
|
+
typeof part.source.media_type === "string"
|
|
944
|
+
? part.source.media_type
|
|
945
|
+
: "image/png";
|
|
946
|
+
toolImages.push({
|
|
947
|
+
inlineData: { mimeType: mediaType, data: part.source.data },
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const fnResponsePart = {
|
|
954
|
+
functionResponse: {
|
|
955
|
+
...(isClaude && toolCallId ? { id: toolCallId } : {}),
|
|
956
|
+
name: fnName,
|
|
957
|
+
response: responseData,
|
|
958
|
+
},
|
|
959
|
+
};
|
|
960
|
+
const lastContent = contents[contents.length - 1];
|
|
961
|
+
if (
|
|
962
|
+
lastContent &&
|
|
963
|
+
lastContent.role === "user" &&
|
|
964
|
+
Array.isArray(lastContent.parts) &&
|
|
965
|
+
lastContent.parts.length > 0 &&
|
|
966
|
+
isRecord(lastContent.parts[0] as any) &&
|
|
967
|
+
(lastContent.parts[0] as any).functionResponse !== undefined
|
|
968
|
+
) {
|
|
969
|
+
const firstNonFnIdx = lastContent.parts.findIndex(
|
|
970
|
+
(p: any) => !isRecord(p) || p.functionResponse === undefined,
|
|
971
|
+
);
|
|
972
|
+
if (firstNonFnIdx === -1) {
|
|
973
|
+
lastContent.parts.push(fnResponsePart);
|
|
974
|
+
} else {
|
|
975
|
+
lastContent.parts.splice(firstNonFnIdx, 0, fnResponsePart);
|
|
976
|
+
}
|
|
977
|
+
if (toolImages.length > 0) {
|
|
978
|
+
lastContent.parts.push(...toolImages);
|
|
979
|
+
}
|
|
980
|
+
} else {
|
|
981
|
+
contents.push({ role: "user", parts: [fnResponsePart, ...toolImages] });
|
|
982
|
+
}
|
|
983
|
+
} else {
|
|
984
|
+
const msgParts = extractParts(msg.content);
|
|
985
|
+
if (msgParts.length > 0) contents.push({ role: "user", parts: msgParts });
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (contents.length === 0)
|
|
990
|
+
contents.push({ role: "user", parts: [{ text: "Hello" }] });
|
|
991
|
+
|
|
992
|
+
const inputTools = Array.isArray(input.tools)
|
|
993
|
+
? (input.tools as OpenAITool[])
|
|
994
|
+
: [];
|
|
995
|
+
const geminiTools = convertOpenAIToolsToGemini(inputTools, isClaude);
|
|
996
|
+
const geminiToolConfig =
|
|
997
|
+
input.tool_choice !== undefined
|
|
998
|
+
? convertToolChoiceToGemini(input.tool_choice)
|
|
999
|
+
: undefined;
|
|
1000
|
+
|
|
1001
|
+
const modelSpec = getModelSpec(input.model);
|
|
1002
|
+
const modelFamily = getModelFamily(input.model);
|
|
1003
|
+
let maxOutputTokens =
|
|
1004
|
+
typeof input.max_tokens === "number" ? input.max_tokens : undefined;
|
|
1005
|
+
if (maxOutputTokens && maxOutputTokens > modelSpec.maxOutputTokens) {
|
|
1006
|
+
compatLogger.debug(
|
|
1007
|
+
`Capping ${input.model} maxOutputTokens ${maxOutputTokens} → ${modelSpec.maxOutputTokens}`,
|
|
1008
|
+
);
|
|
1009
|
+
maxOutputTokens = modelSpec.maxOutputTokens;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
let thinkingConfigObj: Record<string, unknown> | undefined;
|
|
1013
|
+
if (modelFamily === "claude" && isThinking) {
|
|
1014
|
+
const tb = modelSpec.thinkingBudget;
|
|
1015
|
+
thinkingConfigObj = { include_thoughts: true, thinking_budget: tb };
|
|
1016
|
+
if (!maxOutputTokens || maxOutputTokens <= tb) {
|
|
1017
|
+
maxOutputTokens = Math.min(tb + 8192, modelSpec.maxOutputTokens);
|
|
1018
|
+
compatLogger.debug(
|
|
1019
|
+
`Adjusted Claude maxOutputTokens → ${maxOutputTokens}`,
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
} else if (isThinking) {
|
|
1023
|
+
const tb = modelSpec.thinkingBudget;
|
|
1024
|
+
thinkingConfigObj =
|
|
1025
|
+
tb === -1
|
|
1026
|
+
? { includeThoughts: true }
|
|
1027
|
+
: { includeThoughts: true, thinkingBudget: tb };
|
|
1028
|
+
if (tb !== -1 && (!maxOutputTokens || maxOutputTokens <= tb)) {
|
|
1029
|
+
maxOutputTokens = Math.min(tb + 8192, modelSpec.maxOutputTokens);
|
|
1030
|
+
compatLogger.debug(
|
|
1031
|
+
`Adjusted Gemini maxOutputTokens → ${maxOutputTokens}`,
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
} else if (input.reasoning_effort) {
|
|
1035
|
+
const budgets: Record<string, number> = {
|
|
1036
|
+
low: Math.round(modelSpec.thinkingBudget / 4),
|
|
1037
|
+
medium: Math.round(modelSpec.thinkingBudget / 2),
|
|
1038
|
+
high: modelSpec.thinkingBudget,
|
|
1039
|
+
};
|
|
1040
|
+
const b = budgets[input.reasoning_effort.toLowerCase()];
|
|
1041
|
+
if (b) thinkingConfigObj = { includeThoughts: true, thinkingBudget: b };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const generationConfig: Record<string, unknown> = {
|
|
1045
|
+
...(typeof input.temperature === "number"
|
|
1046
|
+
? { temperature: input.temperature }
|
|
1047
|
+
: {}),
|
|
1048
|
+
...(maxOutputTokens ? { maxOutputTokens } : {}),
|
|
1049
|
+
...(thinkingConfigObj ? { thinkingConfig: thinkingConfigObj } : {}),
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
const request: Record<string, unknown> = {
|
|
1053
|
+
contents,
|
|
1054
|
+
generationConfig,
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
if (systemParts.length > 0) {
|
|
1058
|
+
if (!isClaude && isThinking) {
|
|
1059
|
+
const firstTurn = contents[0];
|
|
1060
|
+
if (
|
|
1061
|
+
firstTurn &&
|
|
1062
|
+
firstTurn.role === "user" &&
|
|
1063
|
+
(firstTurn.parts[0] as any)?.text !== undefined
|
|
1064
|
+
) {
|
|
1065
|
+
(firstTurn.parts[0] as any).text =
|
|
1066
|
+
systemParts.join("\n\n") + "\n\n" + (firstTurn.parts[0] as any).text;
|
|
1067
|
+
} else if (firstTurn && firstTurn.role === "user") {
|
|
1068
|
+
firstTurn.parts.unshift({ text: systemParts.join("\n\n") + "\n\n" });
|
|
1069
|
+
} else {
|
|
1070
|
+
contents.unshift({
|
|
1071
|
+
role: "user",
|
|
1072
|
+
parts: [{ text: systemParts.join("\n\n") }],
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
} else {
|
|
1076
|
+
request.systemInstruction = {
|
|
1077
|
+
role: "system",
|
|
1078
|
+
parts: [{ text: systemParts.join("\n\n") }],
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (geminiTools.length > 0) request.tools = geminiTools;
|
|
1084
|
+
if (geminiToolConfig) request.toolConfig = geminiToolConfig;
|
|
1085
|
+
|
|
1086
|
+
const mappedModel = applyModelAlias(input.model);
|
|
1087
|
+
|
|
1088
|
+
return {
|
|
1089
|
+
project: "compat-placeholder",
|
|
1090
|
+
model: mappedModel,
|
|
1091
|
+
displayModel: input.model,
|
|
1092
|
+
userAgent: "antigravity",
|
|
1093
|
+
requestType: "agent",
|
|
1094
|
+
request,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
export function convertAnthropicToolsToOpenAI(
|
|
1099
|
+
tools: unknown,
|
|
1100
|
+
): OpenAITool[] | undefined {
|
|
1101
|
+
if (!Array.isArray(tools) || tools.length === 0) return undefined;
|
|
1102
|
+
const result: OpenAITool[] = [];
|
|
1103
|
+
for (const t of tools) {
|
|
1104
|
+
if (!isRecord(t) || !isNonEmptyString(t.name)) continue;
|
|
1105
|
+
result.push({
|
|
1106
|
+
type: "function",
|
|
1107
|
+
function: {
|
|
1108
|
+
name: t.name as string,
|
|
1109
|
+
...(typeof t.description === "string"
|
|
1110
|
+
? { description: t.description }
|
|
1111
|
+
: {}),
|
|
1112
|
+
...(isRecord(t.input_schema)
|
|
1113
|
+
? { parameters: t.input_schema as Record<string, unknown> }
|
|
1114
|
+
: {}),
|
|
1115
|
+
},
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
return result.length > 0 ? result : undefined;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
export function convertAnthropicToolChoice(toolChoice: unknown): unknown {
|
|
1122
|
+
if (!isRecord(toolChoice)) return toolChoice;
|
|
1123
|
+
if (toolChoice.type === "auto") return "auto";
|
|
1124
|
+
if (toolChoice.type === "any") return "required";
|
|
1125
|
+
if (toolChoice.type === "tool" && isNonEmptyString(toolChoice.name)) {
|
|
1126
|
+
return { type: "function", function: { name: toolChoice.name } };
|
|
1127
|
+
}
|
|
1128
|
+
return "auto";
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
export function convertAnthropicMessagesToOpenAI(
|
|
1132
|
+
messages: ChatMessage[],
|
|
1133
|
+
): ChatMessage[] {
|
|
1134
|
+
const result: ChatMessage[] = [];
|
|
1135
|
+
for (const msg of messages) {
|
|
1136
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
1137
|
+
const blocks = msg.content as Array<Record<string, unknown>>;
|
|
1138
|
+
const toolUseBlocks = blocks.filter(
|
|
1139
|
+
(b) => isRecord(b) && b.type === "tool_use" && isNonEmptyString(b.name),
|
|
1140
|
+
);
|
|
1141
|
+
if (toolUseBlocks.length > 0) {
|
|
1142
|
+
const textParts = blocks
|
|
1143
|
+
.filter(
|
|
1144
|
+
(b) =>
|
|
1145
|
+
isRecord(b) && b.type === "text" && typeof b.text === "string",
|
|
1146
|
+
)
|
|
1147
|
+
.map((b) => b.text as string)
|
|
1148
|
+
.join("");
|
|
1149
|
+
const toolCalls: OpenAIToolCall[] = toolUseBlocks.map((b) => ({
|
|
1150
|
+
id: (b.id as string) || `call_${Date.now().toString(36)}`,
|
|
1151
|
+
type: "function" as const,
|
|
1152
|
+
function: {
|
|
1153
|
+
name: b.name as string,
|
|
1154
|
+
arguments:
|
|
1155
|
+
typeof b.input === "string"
|
|
1156
|
+
? b.input
|
|
1157
|
+
: JSON.stringify(b.input ?? {}),
|
|
1158
|
+
},
|
|
1159
|
+
}));
|
|
1160
|
+
result.push({
|
|
1161
|
+
role: "assistant",
|
|
1162
|
+
content: textParts || null,
|
|
1163
|
+
tool_calls: toolCalls,
|
|
1164
|
+
});
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
1169
|
+
const blocks = msg.content as Array<Record<string, unknown>>;
|
|
1170
|
+
const toolResults = blocks.filter(
|
|
1171
|
+
(b) => isRecord(b) && b.type === "tool_result",
|
|
1172
|
+
);
|
|
1173
|
+
if (toolResults.length > 0) {
|
|
1174
|
+
const otherBlocks = blocks.filter(
|
|
1175
|
+
(b) => !isRecord(b) || b.type !== "tool_result",
|
|
1176
|
+
);
|
|
1177
|
+
if (otherBlocks.length > 0) {
|
|
1178
|
+
result.push({
|
|
1179
|
+
role: "user",
|
|
1180
|
+
content: otherBlocks as ChatMessage["content"],
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
for (const tr of toolResults) {
|
|
1184
|
+
const content =
|
|
1185
|
+
typeof tr.content === "string"
|
|
1186
|
+
? tr.content
|
|
1187
|
+
: Array.isArray(tr.content)
|
|
1188
|
+
? extractTextFromUnknownContent(tr.content)
|
|
1189
|
+
: JSON.stringify(tr.content ?? "");
|
|
1190
|
+
result.push({
|
|
1191
|
+
role: "tool",
|
|
1192
|
+
content,
|
|
1193
|
+
tool_call_id: tr.tool_use_id as string,
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
result.push(msg);
|
|
1200
|
+
}
|
|
1201
|
+
return result;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
export function anthropicToAntigravityBody(
|
|
1205
|
+
input: AnthropicMessagesRequest,
|
|
1206
|
+
): RequestBody {
|
|
1207
|
+
const systemText =
|
|
1208
|
+
typeof input.system === "string"
|
|
1209
|
+
? input.system
|
|
1210
|
+
: Array.isArray(input.system)
|
|
1211
|
+
? extractText(input.system as ChatMessage["content"])
|
|
1212
|
+
: "";
|
|
1213
|
+
const tools = convertAnthropicToolsToOpenAI(input.tools);
|
|
1214
|
+
const toolChoice = convertAnthropicToolChoice(input.tool_choice);
|
|
1215
|
+
const convertedMessages = convertAnthropicMessagesToOpenAI(input.messages);
|
|
1216
|
+
return openAIToAntigravityBody({
|
|
1217
|
+
model: input.model,
|
|
1218
|
+
stream: input.stream,
|
|
1219
|
+
temperature: input.temperature,
|
|
1220
|
+
max_tokens: input.max_tokens,
|
|
1221
|
+
tools,
|
|
1222
|
+
tool_choice: toolChoice,
|
|
1223
|
+
messages: [
|
|
1224
|
+
...(systemText ? [{ role: "system" as const, content: systemText }] : []),
|
|
1225
|
+
...convertedMessages,
|
|
1226
|
+
],
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
export function mapReasoningEffortToThinkingLevel(
|
|
1231
|
+
effort: string | undefined,
|
|
1232
|
+
modelId: string,
|
|
1233
|
+
): number | undefined {
|
|
1234
|
+
const lowerModel = modelId.toLowerCase();
|
|
1235
|
+
const isGemini31Pro = /gemini-3\.1-pro/i.test(modelId);
|
|
1236
|
+
const isGemini3Flash =
|
|
1237
|
+
lowerModel.includes("gemini-3-flash") ||
|
|
1238
|
+
lowerModel.includes("gemini-3.5-flash");
|
|
1239
|
+
|
|
1240
|
+
let effectiveEffort = effort;
|
|
1241
|
+
if (!effectiveEffort) {
|
|
1242
|
+
if (lowerModel.endsWith("-high") || lowerModel.includes("gemini-pro-agent"))
|
|
1243
|
+
effectiveEffort = "high";
|
|
1244
|
+
else if (lowerModel.endsWith("-low")) effectiveEffort = "low";
|
|
1245
|
+
else if (isGemini3Flash) effectiveEffort = "high";
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (!effectiveEffort) return undefined;
|
|
1249
|
+
|
|
1250
|
+
if (isGemini31Pro) {
|
|
1251
|
+
switch (effectiveEffort.toLowerCase()) {
|
|
1252
|
+
case "high":
|
|
1253
|
+
return 10001;
|
|
1254
|
+
case "medium":
|
|
1255
|
+
return 5000;
|
|
1256
|
+
case "low":
|
|
1257
|
+
return 1001;
|
|
1258
|
+
default:
|
|
1259
|
+
return undefined;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
if (isGemini3Flash) {
|
|
1264
|
+
switch (effectiveEffort.toLowerCase()) {
|
|
1265
|
+
case "high":
|
|
1266
|
+
return -1;
|
|
1267
|
+
case "medium":
|
|
1268
|
+
return 4096;
|
|
1269
|
+
case "low":
|
|
1270
|
+
return 1024;
|
|
1271
|
+
default:
|
|
1272
|
+
return undefined;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
return undefined;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
export function validateOpenAIChatCompletionRequest(
|
|
1280
|
+
value: unknown,
|
|
1281
|
+
):
|
|
1282
|
+
| { ok: true; value: OpenAIChatCompletionRequest }
|
|
1283
|
+
| { ok: false; errors: string[] } {
|
|
1284
|
+
if (!isRecord(value))
|
|
1285
|
+
return { ok: false, errors: ["body must be a JSON object"] };
|
|
1286
|
+
const errors: string[] = [];
|
|
1287
|
+
if (!isNonEmptyString(value.model))
|
|
1288
|
+
errors.push("body.model must be a non-empty string");
|
|
1289
|
+
if (!validateMessages(value.messages)) {
|
|
1290
|
+
logValidationFailure("OpenAI messages validation failed", value.messages);
|
|
1291
|
+
errors.push("body.messages must be an array of chat messages");
|
|
1292
|
+
}
|
|
1293
|
+
if (value.stream !== undefined && typeof value.stream !== "boolean")
|
|
1294
|
+
errors.push("body.stream must be boolean when provided");
|
|
1295
|
+
if (value.temperature !== undefined && typeof value.temperature !== "number")
|
|
1296
|
+
errors.push("body.temperature must be number when provided");
|
|
1297
|
+
if (value.max_tokens !== undefined && typeof value.max_tokens !== "number")
|
|
1298
|
+
errors.push("body.max_tokens must be number when provided");
|
|
1299
|
+
return errors.length > 0
|
|
1300
|
+
? { ok: false, errors }
|
|
1301
|
+
: { ok: true, value: value as unknown as OpenAIChatCompletionRequest };
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function validateResponsesTools(value: unknown): string[] {
|
|
1305
|
+
if (value === undefined) return [];
|
|
1306
|
+
if (!Array.isArray(value))
|
|
1307
|
+
return ["body.tools must be an array when provided"];
|
|
1308
|
+
const errors: string[] = [];
|
|
1309
|
+
for (const tool of value) {
|
|
1310
|
+
if (!isRecord(tool)) {
|
|
1311
|
+
errors.push("each tool must be an object");
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
if (tool.type !== "function") {
|
|
1315
|
+
errors.push(`only function tools are supported (got: ${tool.type})`);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return errors;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
export function validateOpenAIResponsesRequest(
|
|
1322
|
+
value: unknown,
|
|
1323
|
+
):
|
|
1324
|
+
| { ok: true; value: OpenAIResponsesRequest }
|
|
1325
|
+
| { ok: false; errors: string[] } {
|
|
1326
|
+
if (!isRecord(value))
|
|
1327
|
+
return { ok: false, errors: ["body must be a JSON object"] };
|
|
1328
|
+
const errors: string[] = [];
|
|
1329
|
+
if (!isNonEmptyString(value.model))
|
|
1330
|
+
errors.push("body.model must be a non-empty string");
|
|
1331
|
+
if (value.stream !== undefined && typeof value.stream !== "boolean")
|
|
1332
|
+
errors.push("body.stream must be boolean when provided");
|
|
1333
|
+
if (value.temperature !== undefined && typeof value.temperature !== "number")
|
|
1334
|
+
errors.push("body.temperature must be number when provided");
|
|
1335
|
+
if (
|
|
1336
|
+
value.max_output_tokens !== undefined &&
|
|
1337
|
+
typeof value.max_output_tokens !== "number"
|
|
1338
|
+
)
|
|
1339
|
+
errors.push("body.max_output_tokens must be number when provided");
|
|
1340
|
+
if (value.store !== undefined && typeof value.store !== "boolean")
|
|
1341
|
+
errors.push("body.store must be boolean when provided");
|
|
1342
|
+
if (
|
|
1343
|
+
value.previous_response_id !== undefined &&
|
|
1344
|
+
value.previous_response_id !== null &&
|
|
1345
|
+
!isNonEmptyString(value.previous_response_id)
|
|
1346
|
+
) {
|
|
1347
|
+
errors.push("body.previous_response_id must be a non-empty string or null");
|
|
1348
|
+
}
|
|
1349
|
+
if (value.conversation !== undefined && value.conversation !== null) {
|
|
1350
|
+
errors.push(
|
|
1351
|
+
"body.conversation is not supported; use previous_response_id instead",
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
if (value.metadata !== undefined && !isRecord(value.metadata))
|
|
1355
|
+
errors.push("body.metadata must be an object when provided");
|
|
1356
|
+
if (
|
|
1357
|
+
value.reasoning !== undefined &&
|
|
1358
|
+
value.reasoning !== null &&
|
|
1359
|
+
!isRecord(value.reasoning)
|
|
1360
|
+
) {
|
|
1361
|
+
errors.push("body.reasoning must be an object when provided");
|
|
1362
|
+
} else if (
|
|
1363
|
+
isRecord(value.reasoning) &&
|
|
1364
|
+
value.reasoning.effort !== undefined &&
|
|
1365
|
+
value.reasoning.effort !== null &&
|
|
1366
|
+
typeof value.reasoning.effort !== "string"
|
|
1367
|
+
) {
|
|
1368
|
+
errors.push("body.reasoning.effort must be a string when provided");
|
|
1369
|
+
}
|
|
1370
|
+
if (
|
|
1371
|
+
value.instructions !== undefined &&
|
|
1372
|
+
value.instructions !== null &&
|
|
1373
|
+
typeof value.instructions !== "string" &&
|
|
1374
|
+
!Array.isArray(value.instructions)
|
|
1375
|
+
) {
|
|
1376
|
+
errors.push(
|
|
1377
|
+
"body.instructions must be a string or content array when provided",
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
errors.push(...validateResponsesTools(value.tools));
|
|
1381
|
+
return errors.length > 0
|
|
1382
|
+
? { ok: false, errors }
|
|
1383
|
+
: { ok: true, value: value as unknown as OpenAIResponsesRequest };
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
export function validateAnthropicMessagesRequest(
|
|
1387
|
+
value: unknown,
|
|
1388
|
+
):
|
|
1389
|
+
| { ok: true; value: AnthropicMessagesRequest }
|
|
1390
|
+
| { ok: false; errors: string[] } {
|
|
1391
|
+
if (!isRecord(value))
|
|
1392
|
+
return { ok: false, errors: ["body must be a JSON object"] };
|
|
1393
|
+
const errors: string[] = [];
|
|
1394
|
+
if (!isNonEmptyString(value.model))
|
|
1395
|
+
errors.push("body.model must be a non-empty string");
|
|
1396
|
+
if (!validateMessages(value.messages))
|
|
1397
|
+
errors.push("body.messages must be an array of chat messages");
|
|
1398
|
+
if (
|
|
1399
|
+
value.system !== undefined &&
|
|
1400
|
+
typeof value.system !== "string" &&
|
|
1401
|
+
!Array.isArray(value.system)
|
|
1402
|
+
)
|
|
1403
|
+
errors.push("body.system must be string or content array when provided");
|
|
1404
|
+
if (value.stream !== undefined && typeof value.stream !== "boolean")
|
|
1405
|
+
errors.push("body.stream must be boolean when provided");
|
|
1406
|
+
if (value.temperature !== undefined && typeof value.temperature !== "number")
|
|
1407
|
+
errors.push("body.temperature must be number when provided");
|
|
1408
|
+
if (value.max_tokens !== undefined && typeof value.max_tokens !== "number")
|
|
1409
|
+
errors.push("body.max_tokens must be number when provided");
|
|
1410
|
+
return errors.length > 0
|
|
1411
|
+
? { ok: false, errors }
|
|
1412
|
+
: { ok: true, value: value as unknown as AnthropicMessagesRequest };
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
export function responseUsageFromCompletion(
|
|
1416
|
+
completion: CompatCompletion,
|
|
1417
|
+
): Record<string, unknown> {
|
|
1418
|
+
return {
|
|
1419
|
+
input_tokens: completion.inputTokens,
|
|
1420
|
+
input_tokens_details: { cached_tokens: 0 },
|
|
1421
|
+
output_tokens: completion.outputTokens,
|
|
1422
|
+
output_tokens_details: { reasoning_tokens: 0 },
|
|
1423
|
+
total_tokens: completion.inputTokens + completion.outputTokens,
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
export function buildResponsesOutput(completion: CompatCompletion): {
|
|
1428
|
+
output: ResponseOutputItem[];
|
|
1429
|
+
outputText: string;
|
|
1430
|
+
callIdToName: Map<string, string>;
|
|
1431
|
+
} {
|
|
1432
|
+
const output: ResponseOutputItem[] = [];
|
|
1433
|
+
const callIdToName = new Map<string, string>();
|
|
1434
|
+
if (completion.thinkingText) {
|
|
1435
|
+
output.push({
|
|
1436
|
+
id: makeCompatId("rs"),
|
|
1437
|
+
type: "reasoning",
|
|
1438
|
+
status: "completed",
|
|
1439
|
+
summary: [{ type: "summary_text", text: completion.thinkingText }],
|
|
1440
|
+
} as unknown as ResponseOutputItem);
|
|
1441
|
+
}
|
|
1442
|
+
if (completion.text) {
|
|
1443
|
+
output.push({
|
|
1444
|
+
id: makeCompatId("msg"),
|
|
1445
|
+
type: "message",
|
|
1446
|
+
status: "completed",
|
|
1447
|
+
role: "assistant",
|
|
1448
|
+
content: [
|
|
1449
|
+
{ type: "output_text", text: completion.text, annotations: [] },
|
|
1450
|
+
],
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
for (const toolCall of completion.toolCalls ?? []) {
|
|
1454
|
+
callIdToName.set(toolCall.id, toolCall.function.name);
|
|
1455
|
+
output.push({
|
|
1456
|
+
id: makeCompatId("fc"),
|
|
1457
|
+
type: "function_call",
|
|
1458
|
+
call_id: toolCall.id,
|
|
1459
|
+
name: toolCall.function.name,
|
|
1460
|
+
arguments: toolCall.function.arguments,
|
|
1461
|
+
status: "completed",
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
return { output, outputText: completion.text, callIdToName };
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
export function buildAssistantMessageFromCompletion(
|
|
1468
|
+
completion: CompatCompletion,
|
|
1469
|
+
): ChatMessage {
|
|
1470
|
+
return completion.toolCalls && completion.toolCalls.length > 0
|
|
1471
|
+
? {
|
|
1472
|
+
role: "assistant",
|
|
1473
|
+
content: completion.text || null,
|
|
1474
|
+
tool_calls: completion.toolCalls,
|
|
1475
|
+
}
|
|
1476
|
+
: { role: "assistant", content: completion.text };
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
export function buildResponsesResponse(
|
|
1480
|
+
request: OpenAIResponsesRequest,
|
|
1481
|
+
responseId: string,
|
|
1482
|
+
createdAt: number,
|
|
1483
|
+
completion: CompatCompletion,
|
|
1484
|
+
status: "in_progress" | "completed" | "cancelled",
|
|
1485
|
+
previousResponseId: string | null,
|
|
1486
|
+
): Record<string, unknown> {
|
|
1487
|
+
const { output, outputText } = buildResponsesOutput(completion);
|
|
1488
|
+
return {
|
|
1489
|
+
id: responseId,
|
|
1490
|
+
object: "response",
|
|
1491
|
+
created_at: createdAt,
|
|
1492
|
+
status,
|
|
1493
|
+
error: null,
|
|
1494
|
+
incomplete_details: null,
|
|
1495
|
+
instructions: request.instructions ?? null,
|
|
1496
|
+
max_output_tokens: request.max_output_tokens ?? null,
|
|
1497
|
+
model: request.model,
|
|
1498
|
+
output,
|
|
1499
|
+
output_text: outputText,
|
|
1500
|
+
parallel_tool_calls: request.parallel_tool_calls ?? true,
|
|
1501
|
+
previous_response_id: previousResponseId,
|
|
1502
|
+
reasoning: { effort: request.reasoning?.effort ?? null },
|
|
1503
|
+
store: request.store !== false,
|
|
1504
|
+
temperature: request.temperature ?? null,
|
|
1505
|
+
text: { format: { type: "text" } },
|
|
1506
|
+
tool_choice: request.tool_choice ?? "auto",
|
|
1507
|
+
tools: Array.isArray(request.tools) ? request.tools : [],
|
|
1508
|
+
top_p: null,
|
|
1509
|
+
truncation: "disabled",
|
|
1510
|
+
usage: responseUsageFromCompletion(completion),
|
|
1511
|
+
metadata: isRecord(request.metadata) ? request.metadata : {},
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
export function saveResponsesEntry(
|
|
1516
|
+
response: Record<string, unknown>,
|
|
1517
|
+
inputItems: Array<Record<string, unknown>>,
|
|
1518
|
+
conversationMessages: ChatMessage[],
|
|
1519
|
+
completion: CompatCompletion,
|
|
1520
|
+
): void {
|
|
1521
|
+
const responseId = typeof response.id === "string" ? response.id : null;
|
|
1522
|
+
if (!responseId) return;
|
|
1523
|
+
const { callIdToName } = buildResponsesOutput(completion);
|
|
1524
|
+
const mergedConversation = [
|
|
1525
|
+
...conversationMessages,
|
|
1526
|
+
buildAssistantMessageFromCompletion(completion),
|
|
1527
|
+
];
|
|
1528
|
+
const expiresAt = Date.now() + 6 * 60 * 60 * 1000;
|
|
1529
|
+
setStoredResponse(responseId, {
|
|
1530
|
+
response,
|
|
1531
|
+
inputItems,
|
|
1532
|
+
conversationMessages: mergedConversation as unknown as Array<
|
|
1533
|
+
Record<string, unknown>
|
|
1534
|
+
>,
|
|
1535
|
+
callIdToName: Object.fromEntries(callIdToName),
|
|
1536
|
+
expiresAt,
|
|
1537
|
+
});
|
|
1538
|
+
}
|