mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-29 06:25:16 -07:00
chore: add removal of function call if response causes 404
This commit is contained in:
@@ -1344,6 +1344,60 @@ describe('GeminiChat', () => {
|
|||||||
).toHaveBeenCalledTimes(1);
|
).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should remove function response AND preceding function call on 400 error', async () => {
|
||||||
|
// Set up history with a user message and model function call
|
||||||
|
const initialUserMessage: Content = {
|
||||||
|
role: 'user',
|
||||||
|
parts: [{ text: 'Call a tool for me' }],
|
||||||
|
};
|
||||||
|
const modelFunctionCall: Content = {
|
||||||
|
role: 'model',
|
||||||
|
parts: [{ functionCall: { name: 'test_tool', args: {} } }],
|
||||||
|
};
|
||||||
|
chat.addHistory(initialUserMessage);
|
||||||
|
chat.addHistory(modelFunctionCall);
|
||||||
|
|
||||||
|
// Verify initial history state
|
||||||
|
expect(chat.getHistory().length).toBe(2);
|
||||||
|
|
||||||
|
const error400 = new ApiError({ message: 'Bad Request', status: 400 });
|
||||||
|
vi.mocked(mockContentGenerator.generateContentStream).mockRejectedValue(
|
||||||
|
error400,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send a function response that will fail with 400
|
||||||
|
const functionResponse = [
|
||||||
|
{
|
||||||
|
functionResponse: {
|
||||||
|
name: 'test_tool',
|
||||||
|
response: { invalid: 'data' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const stream = await chat.sendMessageStream(
|
||||||
|
{ model: 'gemini-2.5-flash' },
|
||||||
|
functionResponse,
|
||||||
|
'prompt-id-400-fn',
|
||||||
|
new AbortController().signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(async () => {
|
||||||
|
for await (const _ of stream) {
|
||||||
|
/* consume stream */
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
).rejects.toThrow(error400);
|
||||||
|
|
||||||
|
// History should only contain the initial user message.
|
||||||
|
// Both the function response AND the model's function call should be removed
|
||||||
|
// to avoid a dangling function call state.
|
||||||
|
const history = chat.getHistory();
|
||||||
|
expect(history.length).toBe(1);
|
||||||
|
expect(history[0]).toEqual(initialUserMessage);
|
||||||
|
});
|
||||||
|
|
||||||
it('should retry on 429 Rate Limit errors', async () => {
|
it('should retry on 429 Rate Limit errors', async () => {
|
||||||
const error429 = new ApiError({ message: 'Rate Limited', status: 429 });
|
const error429 = new ApiError({ message: 'Rate Limited', status: 429 });
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ import {
|
|||||||
ContentRetryFailureEvent,
|
ContentRetryFailureEvent,
|
||||||
} from '../telemetry/types.js';
|
} from '../telemetry/types.js';
|
||||||
import { handleFallback } from '../fallback/handler.js';
|
import { handleFallback } from '../fallback/handler.js';
|
||||||
import { isFunctionResponse } from '../utils/messageInspectors.js';
|
import {
|
||||||
|
isFunctionResponse,
|
||||||
|
isFunctionCall,
|
||||||
|
} from '../utils/messageInspectors.js';
|
||||||
import { partListUnionToString } from './geminiRequest.js';
|
import { partListUnionToString } from './geminiRequest.js';
|
||||||
import type { ModelConfigKey } from '../services/modelConfigService.js';
|
import type { ModelConfigKey } from '../services/modelConfigService.js';
|
||||||
import { estimateTokenCountSync } from '../utils/tokenCalculation.js';
|
import { estimateTokenCountSync } from '../utils/tokenCalculation.js';
|
||||||
@@ -387,8 +390,7 @@ export class GeminiChat {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isConnectionPhase && !isRetryable) {
|
if (isConnectionPhase && !isRetryable) {
|
||||||
// Remove failed user content to not break subsequent requests
|
this.popFailedUserContent();
|
||||||
this.history.pop();
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,8 +434,7 @@ export class GeminiChat {
|
|||||||
new ContentRetryFailureEvent(maxAttempts, lastError.type, model),
|
new ContentRetryFailureEvent(maxAttempts, lastError.type, model),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Remove failed user content so it doesn't break subsequent requests
|
this.popFailedUserContent();
|
||||||
this.history.pop();
|
|
||||||
throw lastError;
|
throw lastError;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -678,6 +679,21 @@ export class GeminiChat {
|
|||||||
this.history = [];
|
this.history = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes failed user content from history. If the content was a function
|
||||||
|
* response, also removes the preceding model function call to keep
|
||||||
|
* history consistent (avoids dangling function call state).
|
||||||
|
*/
|
||||||
|
private popFailedUserContent(): void {
|
||||||
|
const popped = this.history.pop();
|
||||||
|
if (popped && isFunctionResponse(popped)) {
|
||||||
|
const prev = this.history[this.history.length - 1];
|
||||||
|
if (prev && isFunctionCall(prev)) {
|
||||||
|
this.history.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new entry to the chat history.
|
* Adds a new entry to the chat history.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user