mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
[JUNE 25] Permanent failover to Flash model for OAuth users after persistent 429 errors (#1376)
Co-authored-by: Scott Densmore <scottdensmore@mac.com>
This commit is contained in:
@@ -35,7 +35,10 @@ import {
|
||||
TelemetryTarget,
|
||||
StartSessionEvent,
|
||||
} from '../telemetry/index.js';
|
||||
import { DEFAULT_GEMINI_EMBEDDING_MODEL } from './models.js';
|
||||
import {
|
||||
DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
} from './models.js';
|
||||
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
||||
|
||||
export enum ApprovalMode {
|
||||
@@ -85,6 +88,11 @@ export interface SandboxConfig {
|
||||
image: string;
|
||||
}
|
||||
|
||||
export type FlashFallbackHandler = (
|
||||
currentModel: string,
|
||||
fallbackModel: string,
|
||||
) => Promise<boolean>;
|
||||
|
||||
export interface ConfigParameters {
|
||||
sessionId: string;
|
||||
embeddingModel?: string;
|
||||
@@ -156,6 +164,8 @@ export class Config {
|
||||
private readonly bugCommand: BugCommandSettings | undefined;
|
||||
private readonly model: string;
|
||||
private readonly extensionContextFilePaths: string[];
|
||||
private modelSwitchedDuringSession: boolean = false;
|
||||
flashFallbackHandler?: FlashFallbackHandler;
|
||||
|
||||
constructor(params: ConfigParameters) {
|
||||
this.sessionId = params.sessionId;
|
||||
@@ -216,9 +226,24 @@ export class Config {
|
||||
}
|
||||
|
||||
async refreshAuth(authMethod: AuthType) {
|
||||
// Check if this is actually a switch to a different auth method
|
||||
const previousAuthType = this.contentGeneratorConfig?.authType;
|
||||
const _isAuthMethodSwitch =
|
||||
previousAuthType && previousAuthType !== authMethod;
|
||||
|
||||
// Always use the original default model when switching auth methods
|
||||
// This ensures users don't stay on Flash after switching between auth types
|
||||
// and allows API key users to get proper fallback behavior from getEffectiveModel
|
||||
const modelToUse = this.model; // Use the original default model
|
||||
|
||||
// Temporarily clear contentGeneratorConfig to prevent getModel() from returning
|
||||
// the previous session's model (which might be Flash)
|
||||
this.contentGeneratorConfig = undefined!;
|
||||
|
||||
const contentConfig = await createContentGeneratorConfig(
|
||||
this.getModel(),
|
||||
modelToUse,
|
||||
authMethod,
|
||||
this,
|
||||
);
|
||||
|
||||
const gc = new GeminiClient(this);
|
||||
@@ -226,6 +251,11 @@ export class Config {
|
||||
this.toolRegistry = await createToolRegistry(this);
|
||||
await gc.initialize(contentConfig);
|
||||
this.contentGeneratorConfig = contentConfig;
|
||||
|
||||
// Reset the session flag since we're explicitly changing auth and using default model
|
||||
this.modelSwitchedDuringSession = false;
|
||||
|
||||
// Note: In the future, we may want to reset any cached state when switching auth methods
|
||||
}
|
||||
|
||||
getSessionId(): string {
|
||||
@@ -240,6 +270,28 @@ export class Config {
|
||||
return this.contentGeneratorConfig?.model || this.model;
|
||||
}
|
||||
|
||||
setModel(newModel: string): void {
|
||||
if (this.contentGeneratorConfig) {
|
||||
this.contentGeneratorConfig.model = newModel;
|
||||
this.modelSwitchedDuringSession = true;
|
||||
}
|
||||
}
|
||||
|
||||
isModelSwitchedDuringSession(): boolean {
|
||||
return this.modelSwitchedDuringSession;
|
||||
}
|
||||
|
||||
resetModelToDefault(): void {
|
||||
if (this.contentGeneratorConfig) {
|
||||
this.contentGeneratorConfig.model = this.model; // Reset to the original default model
|
||||
this.modelSwitchedDuringSession = false;
|
||||
}
|
||||
}
|
||||
|
||||
setFlashFallbackHandler(handler: FlashFallbackHandler): void {
|
||||
this.flashFallbackHandler = handler;
|
||||
}
|
||||
|
||||
getEmbeddingModel(): string {
|
||||
return this.embeddingModel;
|
||||
}
|
||||
@@ -445,3 +497,6 @@ export function createToolRegistry(config: Config): Promise<ToolRegistry> {
|
||||
return registry;
|
||||
})();
|
||||
}
|
||||
|
||||
// Export model constants for use in CLI
|
||||
export { DEFAULT_GEMINI_FLASH_MODEL };
|
||||
|
||||
139
packages/core/src/config/flashFallback.test.ts
Normal file
139
packages/core/src/config/flashFallback.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { Config } from './config.js';
|
||||
import { DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_FLASH_MODEL } from './models.js';
|
||||
|
||||
describe('Flash Model Fallback Configuration', () => {
|
||||
let config: Config;
|
||||
|
||||
beforeEach(() => {
|
||||
config = new Config({
|
||||
sessionId: 'test-session',
|
||||
targetDir: '/test',
|
||||
debugMode: false,
|
||||
cwd: '/test',
|
||||
model: DEFAULT_GEMINI_MODEL,
|
||||
});
|
||||
|
||||
// Initialize contentGeneratorConfig for testing
|
||||
(
|
||||
config as unknown as { contentGeneratorConfig: unknown }
|
||||
).contentGeneratorConfig = {
|
||||
model: DEFAULT_GEMINI_MODEL,
|
||||
authType: 'oauth-personal',
|
||||
};
|
||||
});
|
||||
|
||||
describe('setModel', () => {
|
||||
it('should update the model and mark as switched during session', () => {
|
||||
expect(config.getModel()).toBe(DEFAULT_GEMINI_MODEL);
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(false);
|
||||
|
||||
config.setModel(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
|
||||
expect(config.getModel()).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle multiple model switches during session', () => {
|
||||
config.setModel(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(true);
|
||||
|
||||
config.setModel('gemini-1.5-pro');
|
||||
expect(config.getModel()).toBe('gemini-1.5-pro');
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(true);
|
||||
});
|
||||
|
||||
it('should only mark as switched if contentGeneratorConfig exists', () => {
|
||||
// Create config without initializing contentGeneratorConfig
|
||||
const newConfig = new Config({
|
||||
sessionId: 'test-session-2',
|
||||
targetDir: '/test',
|
||||
debugMode: false,
|
||||
cwd: '/test',
|
||||
model: DEFAULT_GEMINI_MODEL,
|
||||
});
|
||||
|
||||
// Should not crash when contentGeneratorConfig is undefined
|
||||
newConfig.setModel(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(newConfig.isModelSwitchedDuringSession()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModel', () => {
|
||||
it('should return contentGeneratorConfig model if available', () => {
|
||||
// Simulate initialized content generator config
|
||||
config.setModel(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(config.getModel()).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
|
||||
it('should fallback to initial model if contentGeneratorConfig is not available', () => {
|
||||
// Test with fresh config where contentGeneratorConfig might not be set
|
||||
const newConfig = new Config({
|
||||
sessionId: 'test-session-2',
|
||||
targetDir: '/test',
|
||||
debugMode: false,
|
||||
cwd: '/test',
|
||||
model: 'custom-model',
|
||||
});
|
||||
|
||||
expect(newConfig.getModel()).toBe('custom-model');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isModelSwitchedDuringSession', () => {
|
||||
it('should start as false for new session', () => {
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(false);
|
||||
});
|
||||
|
||||
it('should remain false if no model switch occurs', () => {
|
||||
// Perform other operations that don't involve model switching
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(false);
|
||||
});
|
||||
|
||||
it('should persist switched state throughout session', () => {
|
||||
config.setModel(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(true);
|
||||
|
||||
// Should remain true even after getting model
|
||||
config.getModel();
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetModelToDefault', () => {
|
||||
it('should reset model to default and clear session switch flag', () => {
|
||||
// Switch to Flash first
|
||||
config.setModel(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(config.getModel()).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(true);
|
||||
|
||||
// Reset to default
|
||||
config.resetModelToDefault();
|
||||
|
||||
// Should be back to default with flag cleared
|
||||
expect(config.getModel()).toBe(DEFAULT_GEMINI_MODEL);
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle case where contentGeneratorConfig is not initialized', () => {
|
||||
// Create config without initializing contentGeneratorConfig
|
||||
const newConfig = new Config({
|
||||
sessionId: 'test-session-2',
|
||||
targetDir: '/test',
|
||||
debugMode: false,
|
||||
cwd: '/test',
|
||||
model: DEFAULT_GEMINI_MODEL,
|
||||
});
|
||||
|
||||
// Should not crash when contentGeneratorConfig is undefined
|
||||
expect(() => newConfig.resetModelToDefault()).not.toThrow();
|
||||
expect(newConfig.isModelSwitchedDuringSession()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user