Spaces:
Paused
Paused
File size: 7,774 Bytes
d0eb8b9 ab2754a d0eb8b9 142c9c4 d0eb8b9 5416ffb d0eb8b9 e25a730 ab2754a d0eb8b9 ab2754a d0eb8b9 ab2754a 142c9c4 ab2754a 142c9c4 ab2754a 759fe9e ab2754a 759fe9e ab2754a 759fe9e ab2754a 759fe9e ab2754a 759fe9e ab2754a 759fe9e ab2754a 759fe9e ab2754a 759fe9e ab2754a 759fe9e ab2754a d0eb8b9 e25a730 d0eb8b9 e25a730 d0eb8b9 d6c3bb0 d0eb8b9 ab2754a d0eb8b9 5416ffb d0eb8b9 ab2754a d0eb8b9 ab2754a d0eb8b9 ab2754a 5416ffb d0eb8b9 5416ffb d0eb8b9 7366e72 d0eb8b9 5416ffb 53d3b3b e25a730 53d3b3b e25a730 53d3b3b e25a730 53d3b3b e25a730 d0eb8b9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | /**
* Translate Google Gemini generateContent request β Codex Responses API request.
*/
import type {
GeminiGenerateContentRequest,
GeminiContent,
GeminiPart,
} from "../types/gemini.js";
import type {
CodexResponsesRequest,
CodexInputItem,
CodexContentPart,
} from "../proxy/codex-api.js";
import { parseModelName, getModelInfo } from "../models/model-store.js";
import { getConfig } from "../config.js";
import { buildInstructions, budgetToEffort, prepareSchema } from "./shared-utils.js";
import { geminiToolsToCodex, geminiToolConfigToCodex } from "./tool-format.js";
/**
* Extract text-only content from Gemini parts.
*/
function extractTextFromParts(parts: GeminiPart[]): string {
return parts
.filter((p) => !p.thought && p.text)
.map((p) => p.text!)
.join("\n");
}
/**
* Build multimodal content (text + images) from Gemini parts.
* Returns plain string if text-only, or CodexContentPart[] if images present.
*/
function extractMultimodalFromParts(
parts: GeminiPart[],
): string | CodexContentPart[] {
const hasImage = parts.some((p) => p.inlineData);
if (!hasImage) return extractTextFromParts(parts);
const codexParts: CodexContentPart[] = [];
for (const p of parts) {
if (!p.thought && p.text) {
codexParts.push({ type: "input_text", text: p.text });
} else if (p.inlineData) {
codexParts.push({
type: "input_image",
image_url: `data:${p.inlineData.mimeType};base64,${p.inlineData.data}`,
});
}
}
return codexParts.length > 0 ? codexParts : "";
}
/**
* Convert Gemini content parts into native Codex input items.
*/
function partsToInputItems(
role: "user" | "assistant",
parts: GeminiPart[],
): CodexInputItem[] {
const items: CodexInputItem[] = [];
const hasFunctionParts = parts.some((p) => p.functionCall || p.functionResponse);
// Build content β multimodal for user, text-only for assistant
if (role === "user") {
const content = extractMultimodalFromParts(parts);
if (content || !hasFunctionParts) {
items.push({ role: "user", content: content || "" });
}
} else {
const text = extractTextFromParts(parts);
if (text || !hasFunctionParts) {
items.push({ role: "assistant", content: text });
}
}
// Track call_ids by function name to correlate functionCall β functionResponse
let callCounter = 0;
const nameToCallIds = new Map<string, string[]>();
for (const p of parts) {
if (p.functionCall) {
const callId = `fc_${callCounter++}`;
let args: string;
try {
args = JSON.stringify(p.functionCall.args ?? {});
} catch {
args = "{}";
}
items.push({
type: "function_call",
call_id: callId,
name: p.functionCall.name,
arguments: args,
});
// Record call_id for this function name (for matching responses)
const ids = nameToCallIds.get(p.functionCall.name) ?? [];
ids.push(callId);
nameToCallIds.set(p.functionCall.name, ids);
} else if (p.functionResponse) {
let output: string;
try {
output = JSON.stringify(p.functionResponse.response ?? {});
} catch {
output = String(p.functionResponse.response);
}
// Match response to the earliest unmatched call with the same name
const ids = nameToCallIds.get(p.functionResponse.name);
const callId = ids?.shift() ?? `fc_${callCounter++}`;
items.push({
type: "function_call_output",
call_id: callId,
output,
});
}
}
return items;
}
/**
* Extract text from Gemini content parts (for session hashing).
*/
function flattenParts(parts: GeminiPart[]): string {
return extractTextFromParts(parts);
}
/**
* Convert Gemini contents to SessionManager-compatible message format.
*/
export function geminiContentsToMessages(
contents: GeminiContent[],
systemInstruction?: GeminiContent,
): Array<{ role: string; content: string }> {
const messages: Array<{ role: string; content: string }> = [];
if (systemInstruction) {
messages.push({
role: "system",
content: flattenParts(systemInstruction.parts),
});
}
for (const c of contents) {
const role = c.role === "model" ? "assistant" : c.role ?? "user";
messages.push({ role, content: flattenParts(c.parts) });
}
return messages;
}
/**
* Convert a GeminiGenerateContentRequest to a CodexResponsesRequest.
*
* Mapping:
* - systemInstruction β instructions field
* - contents β input array (role: "model" β "assistant")
* - model (from URL) β resolved model ID
* - thinkingConfig β reasoning.effort
*/
export interface GeminiTranslationResult {
codexRequest: CodexResponsesRequest;
tupleSchema: Record<string, unknown> | null;
}
export function translateGeminiToCodexRequest(
req: GeminiGenerateContentRequest,
geminiModel: string,
): GeminiTranslationResult {
// Extract system instructions
let userInstructions: string;
if (req.systemInstruction) {
userInstructions = flattenParts(req.systemInstruction.parts);
} else {
userInstructions = "You are a helpful assistant.";
}
const instructions = buildInstructions(userInstructions);
// Build input items from contents
const input: CodexInputItem[] = [];
for (const content of req.contents) {
const role = content.role === "model" ? "assistant" : "user";
const items = partsToInputItems(
role as "user" | "assistant",
content.parts as GeminiPart[],
);
input.push(...items);
}
// Ensure at least one input message
if (input.length === 0) {
input.push({ role: "user", content: "" });
}
// Resolve model (suffix parsing extracts service_tier and reasoning_effort)
const parsed = parseModelName(geminiModel);
const modelId = parsed.modelId;
const modelInfo = getModelInfo(modelId);
const config = getConfig();
// Convert tools to Codex format
const codexTools = req.tools?.length ? geminiToolsToCodex(req.tools) : [];
const codexToolChoice = geminiToolConfigToCodex(req.toolConfig);
// Build request
const request: CodexResponsesRequest = {
model: modelId,
instructions,
input,
stream: true,
store: false,
tools: codexTools,
};
// Add tool_choice if specified
if (codexToolChoice) {
request.tool_choice = codexToolChoice;
}
// Reasoning effort: thinking config > suffix > model default > config default
const thinkingEffort = budgetToEffort(
req.generationConfig?.thinkingConfig?.thinkingBudget,
);
const effort =
thinkingEffort ??
parsed.reasoningEffort ??
modelInfo?.defaultReasoningEffort ??
config.model.default_reasoning_effort;
request.reasoning = { summary: "auto", ...(effort ? { effort } : {}) };
// Service tier: suffix > config default
const serviceTier =
parsed.serviceTier ??
config.model.default_service_tier ??
null;
if (serviceTier) {
request.service_tier = serviceTier;
}
// Response format: translate responseMimeType + responseSchema β text.format
let tupleSchema: Record<string, unknown> | null = null;
const mimeType = req.generationConfig?.responseMimeType;
if (mimeType === "application/json") {
const schema = req.generationConfig?.responseSchema;
if (schema && Object.keys(schema).length > 0) {
const prepared = prepareSchema(schema as Record<string, unknown>);
tupleSchema = prepared.originalSchema;
request.text = {
format: {
type: "json_schema",
name: "gemini_schema",
schema: prepared.schema,
strict: true,
},
};
} else {
request.text = { format: { type: "json_object" } };
}
}
return { codexRequest: request, tupleSchema };
}
|