Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/api/providers/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,17 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
let pendingGroundingMetadata: GroundingMetadata | undefined
let finalResponse: { responseId?: string } | undefined
let finishReason: string | undefined

let toolCallCounter = 0
let hasContent = false
let hasReasoning = false

for await (const chunk of result) {
// Track the final structured response (per SDK pattern: candidate.finishReason)
if (chunk.candidates && chunk.candidates[0]?.finishReason) {
finalResponse = chunk as { responseId?: string }
finishReason = chunk.candidates[0].finishReason
}
// Process candidates and their parts to separate thoughts from content
if (chunk.candidates && chunk.candidates.length > 0) {
Expand Down Expand Up @@ -233,9 +237,11 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
if (part.thought) {
// This is a thinking/reasoning part
if (part.text) {
hasReasoning = true
yield { type: "reasoning", text: part.text }
}
} else if (part.functionCall) {
hasContent = true
// Gemini sends complete function calls in a single chunk
// Emit as partial chunks for consistent handling with NativeToolCallParser
const callId = `${part.functionCall.name}-${toolCallCounter}`
Expand Down Expand Up @@ -263,6 +269,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
} else {
// This is regular content
if (part.text) {
hasContent = true
yield { type: "text", text: part.text }
}
}
Expand All @@ -272,6 +279,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl

// Fallback to the original text property if no candidates structure
else if (chunk.text) {
hasContent = true
yield { type: "text", text: chunk.text }
}

Expand All @@ -280,6 +288,21 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
}
}

// If we had reasoning but no content, emit a placeholder text to prevent "Empty assistant response" errors.
// This typically happens when the model hits max output tokens while reasoning.
if (hasReasoning && !hasContent) {
let message = t("common:errors.gemini.thinking_complete_no_output")
if (finishReason === "MAX_TOKENS") {
message = t("common:errors.gemini.thinking_complete_truncated")
} else if (finishReason === "SAFETY") {
message = t("common:errors.gemini.thinking_complete_safety")
} else if (finishReason === "RECITATION") {
message = t("common:errors.gemini.thinking_complete_recitation")
}

yield { type: "text", text: message }
}

if (finalResponse?.responseId) {
// Capture responseId so Task.addToApiConversationHistory can store it
// alongside the assistant message in api_history.json.
Expand Down
35 changes: 27 additions & 8 deletions src/api/transform/gemini-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ type ReasoningContentBlock = {
type ExtendedContentBlockParam = Anthropic.ContentBlockParam | ThoughtSignatureContentBlock | ReasoningContentBlock
type ExtendedAnthropicContent = string | ExtendedContentBlockParam[]

// Extension type to safely add thoughtSignature to Part
type PartWithThoughtSignature = Part & {
thoughtSignature?: string
}

function isThoughtSignatureContentBlock(block: ExtendedContentBlockParam): block is ThoughtSignatureContentBlock {
return block.type === "thoughtSignature"
}
Expand Down Expand Up @@ -47,16 +52,11 @@ export function convertAnthropicContentToGemini(
return [{ text: content }]
}

return content.flatMap((block): Part | Part[] => {
const parts = content.flatMap((block): Part | Part[] => {
// Handle thoughtSignature blocks first
if (isThoughtSignatureContentBlock(block)) {
if (includeThoughtSignatures && typeof block.thoughtSignature === "string") {
// The Google GenAI SDK currently exposes thoughtSignature as an
// extension field on Part; model it structurally without widening
// the upstream type.
return { thoughtSignature: block.thoughtSignature } as Part
}
// Explicitly omit thoughtSignature when not including it.
// We process thought signatures globally and attach them to the relevant parts
// or create a placeholder part if no other content exists.
return []
}

Expand Down Expand Up @@ -135,6 +135,25 @@ export function convertAnthropicContentToGemini(
return []
}
})

// Post-processing: Ensure thought signature is attached if required
if (includeThoughtSignatures && activeThoughtSignature) {
const hasSignature = parts.some((p) => "thoughtSignature" in p)

if (!hasSignature) {
if (parts.length > 0) {
// Attach to the first part (usually text)
// We use the intersection type to allow adding the property safely
;(parts[0] as PartWithThoughtSignature).thoughtSignature = activeThoughtSignature
} else {
// Create a placeholder part if no other content exists
const placeholder: PartWithThoughtSignature = { text: "", thoughtSignature: activeThoughtSignature }
parts.push(placeholder)
}
}
}

return parts
}

export function convertAnthropicMessageToGemini(
Expand Down
6 changes: 5 additions & 1 deletion src/i18n/locales/ca/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/i18n/locales/de/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@
"gemini": {
"generate_stream": "Gemini generate context stream error: {{error}}",
"generate_complete_prompt": "Gemini completion error: {{error}}",
"sources": "Sources:"
"sources": "Sources:",
"thinking_complete_no_output": "(Thinking complete, but no output was generated.)",
"thinking_complete_truncated": "(Thinking complete, but output was truncated due to token limit.)",
"thinking_complete_safety": "(Thinking complete, but output was blocked due to safety settings.)",
"thinking_complete_recitation": "(Thinking complete, but output was blocked due to recitation check.)"
},
"cerebras": {
"authenticationFailed": "Cerebras API authentication failed. Please check your API key is valid and not expired.",
Expand Down
6 changes: 5 additions & 1 deletion src/i18n/locales/es/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/i18n/locales/fr/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/i18n/locales/hi/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/i18n/locales/id/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/i18n/locales/it/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/i18n/locales/ja/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/i18n/locales/ko/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/i18n/locales/nl/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/i18n/locales/pl/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/i18n/locales/pt-BR/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/i18n/locales/ru/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/i18n/locales/tr/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/i18n/locales/vi/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading