Fix bug Tunnel

This commit is contained in:
decolua
2026-02-22 21:44:11 +07:00
parent a5eb5a864e
commit d21f7aaadc
9 changed files with 734 additions and 237 deletions

View File

@@ -367,7 +367,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
const targetFormat = modelTargetFormat || getTargetFormat(provider);
// Track if client actually wants streaming (before we force it for providers)
const clientRequestedStreaming = body.stream === true;
const clientRequestedStreaming = body.stream === true || sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI;
const providerRequiresStreaming = provider === 'openai' || provider === 'codex';
// Force streaming for OpenAI/Codex models (they don't support non-streaming mode properly)
@@ -411,6 +411,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
appendRequestLog({ model, provider, connectionId, status: "PENDING" }).catch(() => { });
const msgCount = translatedBody.messages?.length
|| translatedBody.input?.length
|| translatedBody.contents?.length
|| translatedBody.request?.contents?.length
|| 0;
@@ -579,13 +580,13 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
// (Codex API doesn't always set Content-Type on streaming responses)
const isSSEResponse = contentType.includes("text/event-stream") || (contentType === "" && provider === "codex");
if (isSSEResponse) {
const isResponsesApi = sourceFormat === 'openai-responses';
// Codex always returns Responses API SSE format regardless of client source format
const isCodexResponsesApi = provider === "codex" || sourceFormat === "openai-responses";
if (isResponsesApi) {
// Responses API SSE → Responses API JSON (for pydantic_ai, OpenAI SDK, etc.)
if (isCodexResponsesApi) {
// Responses API SSE → parse → translate to client format
try {
const jsonResponse = await convertResponsesStreamToJson(providerResponse.body);
log?.info?.("STREAM", `Converted Responses API SSE → JSON for non-streaming client`);
if (onRequestSuccess) await onRequestSuccess();
@@ -606,6 +607,9 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
}).catch(() => { });
}
const msgItem = jsonResponse.output?.find(item => item.type === "message");
const textContent = msgItem?.content?.find(c => c.type === "output_text")?.text || msgItem?.content?.[0]?.text || null;
console.log(`[DBG] codex status=${jsonResponse.status} output.len=${jsonResponse.output?.length} msgItem.type=${msgItem?.type} textLen=${textContent?.length||0}`);
const totalLatency = Date.now() - requestStartTime;
saveRequestDetail({
provider: provider || "unknown",
@@ -617,22 +621,67 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
request: extractRequestConfig(body, stream),
providerRequest: finalBody || translatedBody || null,
providerResponse: null,
response: {
content: jsonResponse.output?.[0]?.content?.[0]?.text || null,
thinking: null,
finish_reason: jsonResponse.status || "unknown"
},
response: { content: textContent, thinking: null, finish_reason: jsonResponse.status || "unknown" },
status: "success",
endpoint: clientRawRequest?.endpoint || null
}).catch(() => { });
// If client is openai-responses → return as-is
if (sourceFormat === "openai-responses") {
return {
success: true,
response: new Response(JSON.stringify(jsonResponse), {
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
})
};
}
// Translate Responses API JSON → OpenAI chat completion JSON
const openaiMsg = {
id: jsonResponse.id || `chatcmpl-${Date.now()}`,
object: "chat.completion",
created: jsonResponse.created_at || Math.floor(Date.now() / 1000),
model: jsonResponse.model || model,
choices: [{
index: 0,
message: { role: "assistant", content: textContent || "" },
finish_reason: jsonResponse.status === "completed" ? "stop" : (jsonResponse.status || "stop")
}],
usage: {
prompt_tokens: usage.input_tokens || 0,
completion_tokens: usage.output_tokens || 0,
total_tokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
}
};
// Build client-format response based on sourceFormat
let finalResp;
if (sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI) {
// Antigravity/Gemini non-streaming format
finalResp = {
response: {
candidates: [{
content: { role: "model", parts: [{ text: textContent || "" }] },
finishReason: "STOP",
index: 0
}],
usageMetadata: {
promptTokenCount: usage.input_tokens || 0,
candidatesTokenCount: usage.output_tokens || 0,
totalTokenCount: (usage.input_tokens || 0) + (usage.output_tokens || 0)
},
modelVersion: model,
responseId: jsonResponse.id || `resp_${Date.now()}`
}
};
} else {
finalResp = openaiMsg;
}
return {
success: true,
response: new Response(JSON.stringify(jsonResponse), {
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
}
response: new Response(JSON.stringify(finalResp), {
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
})
};
} catch (error) {
@@ -906,9 +955,12 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
&& !isDroidCLI;
if (needsCodexTranslation) {
// For claude clients, translate directly to claude format
// For openai/openai-responses clients, translate to openai (responsesHandler will re-add event: lines)
const codexTarget = sourceFormat === FORMATS.CLAUDE ? FORMATS.CLAUDE : FORMATS.OPENAI;
// Translate Codex (openai-responses) SSE → client's source format
// Claude → claude, Antigravity/Gemini → antigravity, others → openai
let codexTarget;
if (sourceFormat === FORMATS.CLAUDE) codexTarget = FORMATS.CLAUDE;
else if (sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI) codexTarget = FORMATS.ANTIGRAVITY;
else codexTarget = FORMATS.OPENAI;
log?.debug?.("STREAM", `Codex translation mode: openai-responses → ${codexTarget}`);
transformStream = createSSETransformStreamWithLogger('openai-responses', codexTarget, provider, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey);
} else if (needsTranslation(targetFormat, sourceFormat)) {