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
17 changes: 0 additions & 17 deletions src/api/providers/__tests__/gemini.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,23 +206,6 @@ describe("GeminiHandler", () => {
expect(handler.calculateCost({ info: mockInfo, inputTokens: 0, outputTokens })).toBeCloseTo(expectedCost)
})

it("should calculate cost with cache write tokens", () => {
const inputTokens = 10000
const outputTokens = 20000
const cacheWriteTokens = 5000
const CACHE_TTL = 5 // Match the constant in gemini.ts

// Added non-null assertions (!)
const expectedInputCost = (inputTokens / 1_000_000) * mockInfo.inputPrice!
const expectedOutputCost = (outputTokens / 1_000_000) * mockInfo.outputPrice!
const expectedCacheWriteCost =
mockInfo.cacheWritesPrice! * (cacheWriteTokens / 1_000_000) * (CACHE_TTL / 60)
const expectedCost = expectedInputCost + expectedOutputCost + expectedCacheWriteCost

const cost = handler.calculateCost({ info: mockInfo, inputTokens, outputTokens })
expect(cost).toBeCloseTo(expectedCost)
})

it("should calculate cost with cache read tokens", () => {
const inputTokens = 10000 // Total logical input
const outputTokens = 20000
Expand Down
20 changes: 12 additions & 8 deletions src/api/providers/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
: (maxTokens ?? undefined)

// Only forward encrypted reasoning continuations (thoughtSignature) when we are
// using effort-based reasoning (thinkingLevel). Budget-only configs should NOT
// send thoughtSignature parts back to Gemini.
const includeThoughtSignatures = Boolean(thinkingConfig?.thinkingLevel)
// using reasoning (thinkingConfig is present). Both effort-based (thinkingLevel)
// and budget-based (thinkingBudget) models require this for active loops.
const includeThoughtSignatures = Boolean(thinkingConfig)

// The message list can include provider-specific meta entries such as
// `{ type: "reasoning", ... }` that are intended only for providers like
Expand Down Expand Up @@ -162,10 +162,9 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
}>) {
// Capture thought signatures so they can be persisted into API history.
const thoughtSignature = part.thoughtSignature
// Only persist encrypted reasoning when an effort-based thinking level is set
// (i.e. thinkingConfig.thinkingLevel is present). Budget-based configs that only
// set thinkingBudget should NOT trigger encrypted continuation.
if (thinkingConfig?.thinkingLevel && thoughtSignature) {
// Persist encrypted reasoning when using reasoning. Both effort-based
// and budget-based models require this for active loops.
if (thinkingConfig && thoughtSignature) {
this.lastThoughtSignature = thoughtSignature
}

Expand Down Expand Up @@ -351,7 +350,12 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
const countTokensRequest = {
model,
// Token counting does not need encrypted continuation; always drop thoughtSignature.
contents: convertAnthropicContentToGemini(content, { includeThoughtSignatures: false }),
contents: [
{
role: "user",
parts: convertAnthropicContentToGemini(content, { includeThoughtSignatures: false }),
},
],
}

const response = await this.client.models.countTokens(countTokensRequest)
Expand Down
1 change: 1 addition & 0 deletions src/api/transform/__tests__/gemini-format.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ describe("convertAnthropicMessageToGemini", () => {
name: "calculator",
args: { operation: "add", numbers: [2, 3] },
},
thoughtSignature: "skip_thought_signature_validator",
},
],
})
Expand Down
34 changes: 28 additions & 6 deletions src/api/transform/gemini-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,30 @@ export function convertAnthropicContentToGemini(
): Part[] {
const includeThoughtSignatures = options?.includeThoughtSignatures ?? true

// First pass: find thoughtSignature if it exists in the content blocks
let activeThoughtSignature: string | undefined
if (Array.isArray(content)) {
const sigBlock = content.find((block) => isThoughtSignatureContentBlock(block)) as ThoughtSignatureContentBlock
if (sigBlock?.thoughtSignature) {
activeThoughtSignature = sigBlock.thoughtSignature
}
}

// Determine the signature to attach to function calls.
// If we're in a mode that expects signatures (includeThoughtSignatures is true):
// 1. Use the actual signature if we found one in the history/content.
// 2. Fallback to "skip_thought_signature_validator" if missing (e.g. cross-model history).
let functionCallSignature: string | undefined
if (includeThoughtSignatures) {
functionCallSignature = activeThoughtSignature || "skip_thought_signature_validator"
}

if (typeof content === "string") {
return [{ text: content }]
}

return content.flatMap((block): Part | Part[] => {
// Handle thoughtSignature blocks first so that the main switch can continue
// to operate on the standard Anthropic content union. This preserves strong
// typing for known block types while still allowing provider-specific
// extensions when needed.
// Handle thoughtSignature blocks first
if (isThoughtSignatureContentBlock(block)) {
if (includeThoughtSignatures && typeof block.thoughtSignature === "string") {
// The Google GenAI SDK currently exposes thoughtSignature as an
Expand All @@ -54,7 +69,10 @@ export function convertAnthropicContentToGemini(
name: block.name,
args: block.input as Record<string, unknown>,
},
}
// Inject the thoughtSignature into the functionCall part if required.
// This is necessary for Gemini 2.5/3+ thinking models to validate the tool call.
...(functionCallSignature ? { thoughtSignature: functionCallSignature } : {}),
} as Part
case "tool_result": {
if (!block.content) {
return []
Expand Down Expand Up @@ -108,6 +126,10 @@ export function convertAnthropicMessageToGemini(
): Content {
return {
role: message.role === "assistant" ? "model" : "user",
parts: convertAnthropicContentToGemini(message.content, options),
parts: convertAnthropicContentToGemini(message.content, {
...options,
includeThoughtSignatures:
message.role === "assistant" ? (options?.includeThoughtSignatures ?? true) : false,
}),
}
}
Loading