Compare commits

...

12 Commits

Author SHA1 Message Date
Sehoon Shon 2df15df871 fix(core): strip thinkingConfig for models without thinking feature 2026-04-01 01:49:39 -04:00
Sehoon Shon de128bc172 fix(core): revert defaultModelConfigs.ts and fix formatting 2026-04-01 01:26:38 -04:00
Sehoon Shon a10425c947 fix(core): resolve build failures and lint errors 2026-04-01 01:21:09 -04:00
Sehoon Shon bd12535d0b fix(core): stabilize gemini-3-flash-preview by disabling thinkingConfig and lowering temperature 2026-04-01 01:11:18 -04:00
Sehoon Shon d35b390b33 fix(core): revert models.ts and refine history coalescing to resolve 500 errors 2026-04-01 01:06:50 -04:00
Sehoon Shon 3a8b101063 fix(core): revert PREVIEW_GEMINI_FLASH_MODEL back to 3.0 preview 2026-04-01 00:52:02 -04:00
Sehoon Shon d18586fec2 fix(core): map PREVIEW_GEMINI_FLASH_MODEL to stable 3.1 lite and fix history coalescing 2026-04-01 00:26:27 -04:00
Sehoon Shon 428a4808db fix(core): improve chat history merging and switch to stable preview flash model
- Refactor GeminiChat history merging to handle consecutive turns of any role.
- Implement part-level filtering to remove empty text segments while preserving tool calls.
- Switch integration tests to 'gemini-3.1-flash-lite-preview' for improved stability.
- Ensure crash report context follows alternating role rules.
2026-03-31 21:35:13 -04:00
Sehoon Shon 4bb389efb7 fix(core): improve history merging logic and switch to stable flash for e2e tests 2026-03-31 15:54:42 -04:00
Sehoon Shon 4b72b7f846 test(core): update turn test expectation for error report context 2026-03-31 13:55:11 -04:00
Sehoon Shon 3658d0ebc7 fix(core): merge consecutive user roles in chat history and update e2e tests 2026-03-31 13:34:56 -04:00
Sehoon Shon ee092dc172 test(utils): use flash model for integration tests in test-rig 2026-03-31 10:57:41 -04:00
6 changed files with 108 additions and 34 deletions
+1 -1
View File
@@ -133,7 +133,7 @@ describe('file-system', () => {
).toBeTruthy();
const newFileContent = rig.readFile(fileName);
expect(newFileContent).toBe('hello');
expect(newFileContent.trim()).toBe('hello');
});
it('should perform a read-then-write sequence', async () => {
+92 -25
View File
@@ -163,34 +163,90 @@ function extractCuratedHistory(comprehensiveHistory: Content[]): Content[] {
return [];
}
const curatedHistory: Content[] = [];
const length = comprehensiveHistory.length;
let i = 0;
while (i < length) {
if (comprehensiveHistory[i].role === 'user') {
curatedHistory.push(comprehensiveHistory[i]);
i++;
} else {
const modelOutput: Content[] = [];
let isValid = true;
while (i < length && comprehensiveHistory[i].role === 'model') {
modelOutput.push(comprehensiveHistory[i]);
if (isValid && !isValidContent(comprehensiveHistory[i])) {
isValid = false;
}
i++;
for (const entry of comprehensiveHistory) {
const role = entry.role;
const rawParts = entry.parts || [];
// Filter out invalid/empty parts
const validParts = rawParts.filter((part) => {
if (part === undefined || Object.keys(part).length === 0) {
return false;
}
if (isValid) {
curatedHistory.push(...modelOutput);
// Thought parts must be non-empty strings
if (part.thought !== undefined) {
return (
typeof part.thought === 'string' &&
(part.thought as string).trim() !== ''
);
}
// Text parts must be non-empty strings
if (part.text !== undefined) {
return typeof part.text === 'string' && part.text.trim() !== '';
}
// Keep other parts (functionCall, functionResponse, etc.)
return true;
});
if (validParts.length === 0) {
continue;
}
let lastEntry = curatedHistory[curatedHistory.length - 1];
if (!lastEntry || lastEntry.role !== role) {
// Create new entry
lastEntry = {
role,
parts: [],
};
curatedHistory.push(lastEntry);
}
const lastEntryParts = lastEntry.parts || [];
for (const part of validParts) {
if (part.text !== undefined) {
// Coalesce text into the last text part of this turn
const existingTextPart = lastEntryParts.find(
(p) => p.text !== undefined,
);
if (existingTextPart) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const lastText = existingTextPart.text as string;
const separator = lastText.endsWith('\n') ? '' : '\n';
existingTextPart.text = lastText + separator + part.text;
} else {
lastEntryParts.push({ ...part });
}
} else if (part.thought !== undefined) {
// Coalesce thought into the last thought part of this turn
const existingThoughtPart = lastEntryParts.find(
(p) => p.thought !== undefined,
);
if (existingThoughtPart) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const lastThought = existingThoughtPart.thought as unknown as string;
const separator = lastThought.endsWith('\n') ? '' : '\n';
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
existingThoughtPart.thought = (lastThought +
separator +
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(part.thought as unknown as string)) as unknown as boolean;
} else {
// Insert thought at the beginning for model consistency
lastEntryParts.unshift({ ...part });
}
} else {
// Add as is (functionCall, functionResponse)
lastEntryParts.push({ ...part });
}
}
lastEntry.parts = lastEntryParts;
}
return curatedHistory;
}
/**
* Custom error to signal that a stream completed with invalid content,
* which should trigger a retry.
*/
export class InvalidStreamError extends Error {
readonly type:
| 'NO_FINISH_REASON'
@@ -342,7 +398,17 @@ export class GeminiChat {
}
// Add user content to history ONCE before any attempts.
this.history.push(userContent);
if (
this.history.length > 0 &&
this.history[this.history.length - 1].role === 'user'
) {
const lastUser = this.history[this.history.length - 1];
const lastUserParts = lastUser.parts || [];
const currentUserParts = userContent.parts || [];
lastUser.parts = [...lastUserParts, ...currentUserParts];
} else {
this.history.push(userContent);
}
const requestContents = this.getHistory(true);
const streamWithRetries = async function* (
@@ -935,7 +1001,8 @@ export class GeminiChat {
isValidNonThoughtTextPart(lastPart) &&
isValidNonThoughtTextPart(part)
) {
lastPart.text += part.text;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(lastPart as { text: string }).text += part.text as string;
} else {
consolidatedParts.push(part);
}
@@ -943,7 +1010,8 @@ export class GeminiChat {
const responseText = consolidatedParts
.filter((part) => part.text)
.map((part) => part.text)
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
.map((part) => part.text as string)
.join('')
.trim();
@@ -1049,7 +1117,6 @@ export class GeminiChat {
const thoughtPart = content.parts[0];
if (thoughtPart.text) {
// Extract subject and description using the same logic as turn.ts
const rawText = thoughtPart.text;
const subjectStringMatches = rawText.match(/\*\*(.*?)\*\*/s);
const subject = subjectStringMatches
+1 -1
View File
@@ -274,7 +274,7 @@ describe('Turn', () => {
expect(reportError).toHaveBeenCalledWith(
error,
'Error when talking to Gemini API',
[...historyContent, { role: 'user', parts: reqParts }],
[...historyContent],
'Turn.run-sendMessageStream',
);
});
+1 -5
View File
@@ -5,7 +5,6 @@
*/
import {
createUserContent,
type PartListUnion,
type GenerateContentResponse,
type FunctionCall,
@@ -375,10 +374,7 @@ export class Turn {
throw error;
}
const contextForReport = [
...this.chat.getHistory(/*curated*/ true),
createUserContent(req),
];
const contextForReport = [...this.chat.getHistory(/*curated*/ true)];
await reportError(
error,
'Error when talking to Gemini API',
@@ -488,6 +488,14 @@ export class ModelConfigService {
);
}
// Automatically strip thinkingConfig if the model does not support thinking.
const modelDefinition = this.getModelDefinition(resolved.model);
if (modelDefinition && modelDefinition.features?.thinking === false) {
if (resolved.generateContentConfig.thinkingConfig) {
delete resolved.generateContentConfig.thinkingConfig;
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return {
model: resolved.model,
+5 -2
View File
@@ -11,7 +11,10 @@ import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { env } from 'node:process';
import { setTimeout as sleep } from 'node:timers/promises';
import { PREVIEW_GEMINI_MODEL, GEMINI_DIR } from '@google/gemini-cli-core';
import {
PREVIEW_GEMINI_FLASH_MODEL,
GEMINI_DIR,
} from '@google/gemini-cli-core';
export { GEMINI_DIR };
import * as pty from '@lydell/node-pty';
import stripAnsi from 'strip-ansi';
@@ -457,7 +460,7 @@ export class TestRig {
...(env['GEMINI_TEST_TYPE'] === 'integration'
? {
model: {
name: PREVIEW_GEMINI_MODEL,
name: PREVIEW_GEMINI_FLASH_MODEL,
},
}
: {}),