diff --git a/package-lock.json b/package-lock.json index 10bb48f..37eb668 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,15 +51,13 @@ "version": "2.11.0", "resolved": "https://registry.npmmirror.com/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", - "license": "(Apache-2.0 AND BSD-3-Clause)", - "peer": true + "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@connectrpc/connect": { "version": "2.0.0-rc.3", "resolved": "https://registry.npmmirror.com/@connectrpc/connect/-/connect-2.0.0-rc.3.tgz", "integrity": "sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "@bufbuild/protobuf": "^2.2.0" } @@ -268,7 +266,6 @@ "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -922,7 +919,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -1675,7 +1671,6 @@ "resolved": "https://registry.npmmirror.com/pg/-/pg-8.17.2.tgz", "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", @@ -2542,7 +2537,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2755,7 +2749,6 @@ "resolved": "https://registry.npmmirror.com/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/infra/providers/gemini.ts b/src/infra/providers/gemini.ts index 3506611..1172d09 100644 --- a/src/infra/providers/gemini.ts +++ b/src/infra/providers/gemini.ts @@ -231,8 +231,10 @@ export class GeminiProvider implements ModelProvider { const decoder = new TextDecoder(); let buffer = ''; let textStarted = false; - const textIndex = 0; - let toolIndex = 1; + let thinkStarted = false; + const thinkIndex = 0; + const textIndex = 1; + let toolIndex = 2; const toolCalls: Array<{ name: string; args: any; thoughtSignature?: string }> = []; let lastUsage: { input: number; output: number } | undefined; let collectAll = false; @@ -274,11 +276,27 @@ export class GeminiProvider implements ModelProvider { break; } - const { textChunks, functionCalls, usage } = this.parseGeminiChunk(event); + const { textChunks, thoughtChunks, functionCalls, usage } = this.parseGeminiChunk(event); if (usage) { lastUsage = usage; } + for (const text of thoughtChunks) { + if (!thinkStarted) { + thinkStarted = true; + yield { + type: 'content_block_start', + index: thinkIndex, + content_block: { type: 'reasoning', reasoning: '' }, + }; + } + yield { + type: 'content_block_delta', + index: thinkIndex, + delta: { type: 'reasoning_delta', text }, + }; + } + for (const text of textChunks) { if (!textStarted) { textStarted = true; @@ -306,10 +324,25 @@ export class GeminiProvider implements ModelProvider { const parsed = JSON.parse(buffer.trim()); const events = Array.isArray(parsed) ? parsed : [parsed]; for (const event of events) { - const { textChunks, functionCalls, usage } = this.parseGeminiChunk(event); + const { textChunks, thoughtChunks, functionCalls, usage } = this.parseGeminiChunk(event); if (usage) { lastUsage = usage; } + for (const text of thoughtChunks) { + if (!thinkStarted) { + thinkStarted = true; + yield { + type: 'content_block_start', + index: thinkIndex, + content_block: { type: 'reasoning', reasoning: '' }, + }; + } + yield { + type: 'content_block_delta', + index: thinkIndex, + delta: { type: 'reasoning_delta', text }, + }; + } for (const text of textChunks) { if (!textStarted) { textStarted = true; @@ -412,9 +445,15 @@ export class GeminiProvider implements ModelProvider { if (opts.maxTokens !== undefined) generationConfig.maxOutputTokens = opts.maxTokens; if (opts.thinking?.budgetTokens) { - generationConfig.thinkingConfig = { thinkingBudget: opts.thinking.budgetTokens }; + generationConfig.thinkingConfig = { + thinkingBudget: opts.thinking.budgetTokens, + includeThoughts: true, + }; } else if (opts.thinking?.level) { - generationConfig.thinkingConfig = { thinkingLevel: opts.thinking.level.toUpperCase() }; + generationConfig.thinkingConfig = { + thinkingLevel: opts.thinking.level.toUpperCase(), + includeThoughts: true, + }; } const body: any = { @@ -574,7 +613,11 @@ export class GeminiProvider implements ModelProvider { const parts = content?.parts ?? []; for (const part of parts) { if (typeof part?.text === 'string') { - blocks.push({ type: 'text', text: part.text }); + if (part.thought === true && this.reasoningTransport === 'provider') { + blocks.push({ type: 'reasoning', reasoning: part.text }); + } else { + blocks.push({ type: 'text', text: part.text }); + } } else if (part?.functionCall) { const call = part.functionCall; const thoughtSignature = part?.thoughtSignature ?? call?.thoughtSignature; @@ -592,18 +635,24 @@ export class GeminiProvider implements ModelProvider { private parseGeminiChunk(event: any): { textChunks: string[]; + thoughtChunks: string[]; functionCalls: Array<{ name: string; args: any; thoughtSignature?: string }>; usage?: { input: number; output: number }; } { const textChunks: string[] = []; + const thoughtChunks: string[] = []; const functionCalls: Array<{ name: string; args: any; thoughtSignature?: string }> = []; const candidates = Array.isArray(event?.candidates) ? event.candidates : []; for (const candidate of candidates) { const parts = candidate?.content?.parts ?? []; for (const part of parts) { - if (typeof part?.text === 'string') { - textChunks.push(part.text); + if (typeof part?.text === 'string' && part.text.length > 0) { + if (part.thought === true && this.reasoningTransport === 'provider') { + thoughtChunks.push(part.text); + } else { + textChunks.push(part.text); + } } else if (part?.functionCall) { const thoughtSignature = part?.thoughtSignature ?? part?.functionCall?.thoughtSignature; functionCalls.push({ @@ -623,6 +672,6 @@ export class GeminiProvider implements ModelProvider { } : undefined; - return { textChunks, functionCalls, usage }; + return { textChunks, thoughtChunks, functionCalls, usage }; } }