codex-proxy / src /translation /openai-to-codex.ts
icebear
fix: remove unsupported previous_response_id parameter (#30)
3c0eaf7 unverified
raw
history blame
5.73 kB
/**
* Translate OpenAI Chat Completions request β†’ Codex Responses API request.
*/
import type { ChatCompletionRequest, ChatMessage } from "../types/openai.js";
import type {
CodexResponsesRequest,
CodexInputItem,
CodexContentPart,
} from "../proxy/codex-api.js";
import { resolveModelId, getModelInfo } from "../models/model-store.js";
import { getConfig } from "../config.js";
import { buildInstructions } from "./shared-utils.js";
import {
openAIToolsToCodex,
openAIToolChoiceToCodex,
openAIFunctionsToCodex,
} from "./tool-format.js";
/** Extract plain text from content (string, array, null, or undefined). */
function extractText(content: ChatMessage["content"]): string {
if (content == null) return "";
if (typeof content === "string") return content;
return content
.filter((p) => p.type === "text" && p.text)
.map((p) => p.text!)
.join("\n");
}
/**
* Extract content from a message, preserving images as structured content parts.
* Returns a plain string if text-only, or CodexContentPart[] if images are present.
*/
function extractContent(
content: ChatMessage["content"],
): string | CodexContentPart[] {
if (content == null) return "";
if (typeof content === "string") return content;
const hasImage = content.some((p) => p.type === "image_url");
if (!hasImage) {
// Text-only: return plain string (preserves existing behavior)
return content
.filter((p) => p.type === "text" && p.text)
.map((p) => p.text!)
.join("\n");
}
// Multimodal: convert to Codex content parts
const parts: CodexContentPart[] = [];
for (const p of content) {
if (p.type === "text" && p.text) {
parts.push({ type: "input_text", text: p.text });
} else if (p.type === "image_url") {
// OpenAI format: image_url: { url: "data:..." } or image_url: "string"
const imageUrl = p.image_url as
| string
| { url: string; detail?: string }
| undefined;
if (!imageUrl) continue;
const url = typeof imageUrl === "string" ? imageUrl : imageUrl.url;
if (url) {
parts.push({ type: "input_image", image_url: url });
}
}
}
return parts.length > 0 ? parts : "";
}
/**
* Convert a ChatCompletionRequest to a CodexResponsesRequest.
*
* Mapping:
* - system/developer messages β†’ instructions field
* - user/assistant messages β†’ input array
* - model β†’ resolved model ID
* - reasoning_effort β†’ reasoning.effort
*/
export function translateToCodexRequest(
req: ChatCompletionRequest,
): CodexResponsesRequest {
// Collect system/developer messages as instructions
const systemMessages = req.messages.filter(
(m) => m.role === "system" || m.role === "developer",
);
const userInstructions =
systemMessages.map((m) => extractText(m.content)).join("\n\n") ||
"You are a helpful assistant.";
const instructions = buildInstructions(userInstructions);
// Build input items from non-system messages
// Handles new format (tool/tool_calls) and legacy format (function/function_call)
const input: CodexInputItem[] = [];
for (const msg of req.messages) {
if (msg.role === "system" || msg.role === "developer") continue;
if (msg.role === "assistant") {
// First push the text content
const text = extractText(msg.content);
if (text || (!msg.tool_calls?.length && !msg.function_call)) {
input.push({ role: "assistant", content: text });
}
// Then push tool calls as native function_call items
if (msg.tool_calls?.length) {
for (const tc of msg.tool_calls) {
input.push({
type: "function_call",
call_id: tc.id,
name: tc.function.name,
arguments: tc.function.arguments,
});
}
}
if (msg.function_call) {
input.push({
type: "function_call",
call_id: `fc_${msg.function_call.name}`,
name: msg.function_call.name,
arguments: msg.function_call.arguments,
});
}
} else if (msg.role === "tool") {
// Native tool result
input.push({
type: "function_call_output",
call_id: msg.tool_call_id ?? "unknown",
output: extractText(msg.content),
});
} else if (msg.role === "function") {
// Legacy function result β†’ native format
input.push({
type: "function_call_output",
call_id: `fc_${msg.name ?? "unknown"}`,
output: extractText(msg.content),
});
} else {
input.push({ role: "user", content: extractContent(msg.content) });
}
}
// Ensure at least one input message
if (input.length === 0) {
input.push({ role: "user", content: "" });
}
// Resolve model
const modelId = resolveModelId(req.model);
const modelInfo = getModelInfo(modelId);
const config = getConfig();
// Convert tools to Codex format
const codexTools = req.tools?.length
? openAIToolsToCodex(req.tools)
: req.functions?.length
? openAIFunctionsToCodex(req.functions)
: [];
const codexToolChoice = openAIToolChoiceToCodex(req.tool_choice);
// 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;
}
// Always request reasoning summary (translation layer filters output on demand)
const effort =
req.reasoning_effort ??
modelInfo?.defaultReasoningEffort ??
config.model.default_reasoning_effort;
request.reasoning = { summary: "auto", ...(effort ? { effort } : {}) };
return request;
}