Merge branch 'vk/53e9-pesquise-os-melh' into main

This commit is contained in:
Gemini Nexus Bot committed 2026-04-22 03:26:52 -03:00
commit f4b713c3f4
1 file changed
+13 -86
+13 -86
View File
@@ -751,7 +751,7 @@ export class ProxyService {
token.token.upstream_proxy_url, token.token.upstream_proxy_url,
extraHeaders, extraHeaders,
); );
return this.processStreamResponse(stream, request.model, request.stream_options); return this.processStreamResponse(stream, request.model);
} catch (streamError) { } catch (streamError) {
this.logger.warn( this.logger.warn(
`Stream path failed for model=${request.model}; falling back to non-stream generation: ${ `Stream path failed for model=${request.model}; falling back to non-stream generation: ${
@@ -810,7 +810,7 @@ export class ProxyService {
token.token.upstream_proxy_url, token.token.upstream_proxy_url,
extraHeaders, extraHeaders,
); );
return this.processStreamResponse(stream, request.model, request.stream_options); return this.processStreamResponse(stream, request.model);
} }
const response = await this.generateInternalWithStreamFallback( const response = await this.generateInternalWithStreamFallback(
@@ -957,38 +957,19 @@ export class ProxyService {
private processStreamResponse( private processStreamResponse(
upstreamStream: NodeJS.ReadableStream, upstreamStream: NodeJS.ReadableStream,
model: string, model: string,
streamOptions?: { include_usage?: boolean },
): Observable<string> { ): Observable<string> {
return new Observable<string>((subscriber) => { return new Observable<string>((subscriber) => {
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let buffer = ''; let buffer = '';
let hasEmittedChunk = false; let hasEmittedChunk = false;
let hasSentDone = false; let hasSentDone = false;
let isThinking = false;
let toolCallIndex = 0; let toolCallIndex = 0;
let lastUsageMetadata: GeminiResponse['usageMetadata'];
const streamId = `chatcmpl-${uuidv4()}`; const streamId = `chatcmpl-${uuidv4()}`;
const created = Math.floor(Date.now() / 1000); const created = Math.floor(Date.now() / 1000);
// Emit initial role chunk immediately to prevent strict clients (like Hermes) from timing out during long TTFT
const initialChunk = {
id: streamId,
object: 'chat.completion.chunk',
created: created,
model: model,
choices: [
{
index: 0,
delta: { role: 'assistant', content: '' },
finish_reason: null,
},
],
};
subscriber.next(`data: ${JSON.stringify(initialChunk)}\n\n`);
hasEmittedChunk = true;
const pushChunk = (payload: Record<string, unknown>): void => { const pushChunk = (payload: Record<string, unknown>): void => {
hasEmittedChunk = true;
subscriber.next(`data: ${JSON.stringify(payload)}\n\n`); subscriber.next(`data: ${JSON.stringify(payload)}\n\n`);
}; };
@@ -1009,17 +990,10 @@ export class ProxyService {
const candidate = json.candidates?.[0]; const candidate = json.candidates?.[0];
const parts = candidate?.content?.parts || []; const parts = candidate?.content?.parts || [];
if (json.usageMetadata) {
lastUsageMetadata = json.usageMetadata;
}
for (const part of parts) { for (const part of parts) {
if (part.thought && part.text) { if (part.thought && part.text) {
let textToEmit = part.text; // Reasoning/thinking parts: emit via reasoning_content delta field
if (!isThinking) { // so clients that support it can display thinking separately.
isThinking = true;
textToEmit = `<think>\n${textToEmit}`;
}
const reasoningChunk = { const reasoningChunk = {
id: streamId, id: streamId,
object: 'chat.completion.chunk', object: 'chat.completion.chunk',
@@ -1028,7 +1002,7 @@ export class ProxyService {
choices: [ choices: [
{ {
index: 0, index: 0,
delta: { content: textToEmit }, delta: { reasoning_content: part.text },
finish_reason: null, finish_reason: null,
}, },
], ],
@@ -1090,11 +1064,6 @@ export class ProxyService {
} }
if (part.text) { if (part.text) {
let textToEmit = part.text;
if (isThinking && !part.thought) {
isThinking = false;
textToEmit = `\n</think>\n\n${textToEmit}`;
}
const contentChunk = { const contentChunk = {
id: streamId, id: streamId,
object: 'chat.completion.chunk', object: 'chat.completion.chunk',
@@ -1103,7 +1072,7 @@ export class ProxyService {
choices: [ choices: [
{ {
index: 0, index: 0,
delta: { content: textToEmit }, delta: { content: part.text },
finish_reason: null, finish_reason: null,
}, },
], ],
@@ -1113,26 +1082,9 @@ export class ProxyService {
} }
if (candidate?.finishReason) { if (candidate?.finishReason) {
if (isThinking) {
isThinking = false;
const closingChunk = {
id: streamId,
object: 'chat.completion.chunk',
created: created,
model: model,
choices: [
{
index: 0,
delta: { content: '\n</think>\n\n' },
finish_reason: null,
},
],
};
pushChunk(closingChunk);
}
// Per the OpenAI spec, finish_reason must be "tool_calls" when the // Per the OpenAI spec, finish_reason must be "tool_calls" when the
// response contains function calls, even though Gemini always sends "STOP". // response contains function calls. Gemini always sends "STOP" even
// for tool call responses, so we infer from toolCallIndex.
const mappedFinishReason = const mappedFinishReason =
toolCallIndex > 0 toolCallIndex > 0
? 'tool_calls' ? 'tool_calls'
@@ -1157,7 +1109,7 @@ export class ProxyService {
subscriber.complete(); subscriber.complete();
} }
} catch { } catch {
// ignore parse errors // ignore parse errors on individual SSE lines
} }
} }
}); });
@@ -1178,26 +1130,6 @@ export class ProxyService {
], ],
}); });
} }
if (streamOptions?.include_usage && lastUsageMetadata) {
const usageChunk = {
id: streamId,
object: 'chat.completion.chunk',
created,
model,
choices: [],
usage: {
prompt_tokens: lastUsageMetadata.promptTokenCount || 0,
completion_tokens:
(lastUsageMetadata.candidatesTokenCount || 0) +
(lastUsageMetadata.thoughtsTokenCount || 0),
total_tokens:
(lastUsageMetadata.promptTokenCount || 0) +
(lastUsageMetadata.candidatesTokenCount || 0) +
(lastUsageMetadata.thoughtsTokenCount || 0),
},
};
pushChunk(usageChunk);
}
if (!hasSentDone) { if (!hasSentDone) {
subscriber.next('data: [DONE]\n\n'); subscriber.next('data: [DONE]\n\n');
hasSentDone = true; hasSentDone = true;
@@ -1205,17 +1137,12 @@ export class ProxyService {
subscriber.complete(); subscriber.complete();
}); });
upstreamStream.on('error', (err) => { upstreamStream.on('error', (err: unknown) => {
this.logger.error('OpenAI-compatible stream error:', err); // Convert to clean Error to avoid circular reference issues (socket objects)
const cleanError = err instanceof Error ? new Error(err.message) : new Error(String(err)); const cleanError = err instanceof Error ? new Error(err.message) : new Error(String(err));
this.logger.error(`OpenAI-compatible stream error: ${cleanError.message}`);
subscriber.error(cleanError); subscriber.error(cleanError);
}); });
return () => {
if (typeof (upstreamStream as any).destroy === 'function') {
(upstreamStream as any).destroy();
}
};
}); });
} }