diff --git a/packages/core/src/model-registry.test.ts b/packages/core/src/model-registry.test.ts index aa0482e..e01b72f 100644 --- a/packages/core/src/model-registry.test.ts +++ b/packages/core/src/model-registry.test.ts @@ -30,6 +30,7 @@ describe('model registry', () => { 'antigravity-claude-opus-4-6-thinking', 'antigravity-claude-sonnet-4-6-thinking', 'antigravity-gemini-3.1-flash-image', + 'antigravity-gemini-3.1-flash-lite', 'antigravity-gemini-3.1-pro', 'antigravity-gemini-3.5-flash', 'antigravity-gemini-3.6-flash', @@ -104,4 +105,14 @@ describe('model registry', () => { }, }) }) + + it('registers flash-lite as a non-thinking text model', () => { + expect( + getPublicModelDefinitions()['antigravity-gemini-3.1-flash-lite'], + ).toMatchObject({ + reasoning: false, + limit: { context: 1048576, output: 65535 }, + modalities: { input: ['text'], output: ['text'] }, + }) + }) }) diff --git a/packages/core/src/model-registry.ts b/packages/core/src/model-registry.ts index b712fbb..13486ae 100644 --- a/packages/core/src/model-registry.ts +++ b/packages/core/src/model-registry.ts @@ -104,6 +104,15 @@ const ALL_MODEL_DEFINITIONS: OpencodeModelDefinitions = { high: { thinkingLevel: 'high' }, }, }), + 'antigravity-gemini-3.1-flash-lite': defineModel( + 'antigravity-gemini-3.1-flash-lite', + { + name: 'Gemini 3.1 Flash Lite (Antigravity)', + reasoning: false, + limit: { context: 1048576, output: 65535 }, + modalities: { input: ['text'], output: ['text'] }, + }, + ), 'antigravity-gemini-3.6-flash': defineModel('antigravity-gemini-3.6-flash', { name: 'Gemini 3.6 Flash (Antigravity)', reasoning: true, @@ -291,6 +300,7 @@ const QUOTA_GROUP_BY_MODEL_ID: Record = { 'gemini-3.1-pro-high': 'gemini-pro', 'gemini-3-flash': 'gemini-flash', 'gemini-3-flash-agent': 'gemini-flash', + 'gemini-3.1-flash-lite': 'gemini-flash', 'gemini-3.5-flash-low': 'gemini-flash', 'gemini-3.5-flash-extra-low': 'gemini-flash', 'gemini-3.6-flash-low': 'gemini-flash', @@ -304,6 +314,7 @@ const QUOTA_GROUP_BY_MODEL_ID: Record = { const ANTIGRAVITY_OPENCODE_MODEL_IDS = [ 'antigravity-gemini-3.6-flash', + 'antigravity-gemini-3.1-flash-lite', 'antigravity-gemini-3.5-flash', 'antigravity-gemini-3.1-pro', 'antigravity-claude-sonnet-4-6-thinking', diff --git a/packages/core/src/transform/model-resolver.test.ts b/packages/core/src/transform/model-resolver.test.ts index 2db7135..728fc3e 100644 --- a/packages/core/src/transform/model-resolver.test.ts +++ b/packages/core/src/transform/model-resolver.test.ts @@ -74,6 +74,63 @@ describe('resolveModelWithTier', () => { }) }) + describe('Gemini 3.1 Flash Lite Antigravity route', () => { + it('resolves as a non-thinking text model', () => { + const result = resolveModelWithTier('antigravity-gemini-3.1-flash-lite') + expect(result.actualModel).toBe('gemini-3.1-flash-lite') + expect(result.isThinkingModel).toBe(false) + expect(result.thinkingBudget).toBeUndefined() + expect(result.thinkingLevel).toBeUndefined() + expect(result.tier).toBeUndefined() + expect(result.quotaPreference).toBe('antigravity') + }) + + it('maps gemini-3.1-flash-lite directly for the header-style path', () => { + const result = resolveModelForHeaderStyle( + 'gemini-3.1-flash-lite', + 'antigravity', + ) + expect(result.actualModel).toBe('gemini-3.1-flash-lite') + expect(result.isThinkingModel).toBe(false) + expect(result.quotaPreference).toBe('antigravity') + }) + + // Tier-suffixed flash-lite must NOT carry thinkingLevel / tier on + // any quota path — flash-lite does not support thinking. + it('drops tier suffix on antigravity header-style path', () => { + const result = resolveModelForHeaderStyle( + 'gemini-3.1-flash-lite-high', + 'antigravity', + ) + expect(result.actualModel).toBe('gemini-3.1-flash-lite') + expect(result.isThinkingModel).toBe(false) + expect(result.thinkingLevel).toBeUndefined() + expect(result.tier).toBeUndefined() + expect(result.quotaPreference).toBe('antigravity') + }) + + it('drops tier suffix on gemini-cli header-style path', () => { + const result = resolveModelForHeaderStyle( + 'gemini-3.1-flash-lite-high', + 'gemini-cli', + ) + expect(result.actualModel).toBe('gemini-3.1-flash-lite') + expect(result.isThinkingModel).toBe(false) + expect(result.thinkingLevel).toBeUndefined() + expect(result.tier).toBeUndefined() + expect(result.quotaPreference).toBe('gemini-cli') + }) + + it('drops tier suffix on direct resolveModelWithTier call', () => { + const result = resolveModelWithTier('gemini-3.1-flash-lite-high') + expect(result.actualModel).toBe('gemini-3.1-flash-lite') + expect(result.isThinkingModel).toBe(false) + expect(result.thinkingLevel).toBeUndefined() + expect(result.tier).toBeUndefined() + expect(result.quotaPreference).toBe('antigravity') + }) + }) + describe('Gemini 3.6 Flash Antigravity routes', () => { it.each([ [ @@ -475,6 +532,18 @@ describe('Issue #103: resolveModelForHeaderStyle', () => { expect(result.thinkingLevel).toBe('medium') expect(result.quotaPreference).toBe('gemini-cli') }) + + it('resolves gemini-3.1-flash-lite as non-thinking on gemini-cli path', () => { + const result = resolveModelForHeaderStyle( + 'gemini-3.1-flash-lite', + 'gemini-cli', + ) + expect(result.actualModel).toBe('gemini-3.1-flash-lite') + expect(result.isThinkingModel).toBe(false) + expect(result.thinkingLevel).toBeUndefined() + expect(result.thinkingBudget).toBeUndefined() + expect(result.quotaPreference).toBe('gemini-cli') + }) }) describe('no transformation needed', () => { diff --git a/packages/core/src/transform/model-resolver.ts b/packages/core/src/transform/model-resolver.ts index ad649c8..8164473 100644 --- a/packages/core/src/transform/model-resolver.ts +++ b/packages/core/src/transform/model-resolver.ts @@ -214,6 +214,7 @@ export function resolveModelWithTier( const isGemini31Pro = /^gemini-3\.1-pro/i.test(baseName) const isGemini35Flash = /^gemini-3\.5-flash/i.test(baseName) const isGemini36Flash = /^gemini-3\.6-flash/i.test(baseName) + const isGemini31FlashLite = /^gemini-3\.1-flash-lite/i.test(baseName) const isGptOss120b = /^gpt-oss-120b(?:-medium)?$/i.test(baseName) if (isGemini31Pro && quotaPreference === 'antigravity') { @@ -249,6 +250,16 @@ export function resolveModelWithTier( } } + // Flash-lite is text-only and non-thinking on every quota path. + if (isGemini31FlashLite) { + return { + actualModel: 'gemini-3.1-flash-lite', + isThinkingModel: false, + quotaPreference, + explicitQuota, + } + } + if (isGptOss120b && quotaPreference === 'antigravity') { return { actualModel: 'gpt-oss-120b-medium', @@ -428,11 +439,15 @@ export function resolveModelForHeaderStyle( const isGemini35Flash = isGemini35FlashModel( transformedModel.replace(TIER_REGEX, ''), ) + const isGemini31FlashLite = /^gemini-3\.1-flash-lite/i.test( + transformedModel, + ) - // Don't add tier suffix to image models - they don't support thinking + // Don't add tier suffix to image or flash-lite models - they don't support thinking if ( (isGemini3Pro || isGemini3Flash) && !isGemini35Flash && + !isGemini31FlashLite && !hasTierSuffix && !isImageModel ) { @@ -456,13 +471,30 @@ export function resolveModelForHeaderStyle( // gemini-3-flash-preview bucket; retrieveUserQuota does not list a // gemini-3.5-flash bucket for the gemini-cli header path. const isGemini35Flash = isGemini35FlashModel(transformedModel) + // Flash-lite has no -preview CLI variant — keep the plain wire id. + const isGemini31FlashLite = /^gemini-3\.1-flash-lite$/i.test( + transformedModel, + ) if (isGemini35Flash) { transformedModel = getGemini35FlashGeminiCliFallbackModel() + } else if (isGemini31FlashLite) { + // No transformation needed — flash-lite resolves as non-thinking + // and has no distinct CLI preview model. } else if (!hasPreviewSuffix) { transformedModel = `${transformedModel}-preview` } const resolved = resolveModelWithTier(transformedModel, { cli_first: true }) + + // Flash-lite is non-thinking — don't pass through requestedTier even if a + // tier suffix was on the original request. + if (isGemini31FlashLite) { + return { + ...resolved, + quotaPreference: 'gemini-cli', + } + } + return { ...resolved, thinkingLevel: requestedTier ?? resolved.thinkingLevel, diff --git a/packages/opencode/script/test-models.ts b/packages/opencode/script/test-models.ts index c746158..5459368 100644 --- a/packages/opencode/script/test-models.ts +++ b/packages/opencode/script/test-models.ts @@ -14,6 +14,10 @@ const MODELS: ModelTest[] = [ { model: 'google/gemini-3.5-flash-preview', category: 'gemini-cli' }, // Antigravity Gemini + { + model: 'google/antigravity-gemini-3.1-flash-lite', + category: 'antigravity-gemini', + }, { model: 'google/antigravity-gemini-3.6-flash-low', category: 'antigravity-gemini', diff --git a/packages/opencode/src/plugin/config/models.test.ts b/packages/opencode/src/plugin/config/models.test.ts index 88097e4..43da54b 100644 --- a/packages/opencode/src/plugin/config/models.test.ts +++ b/packages/opencode/src/plugin/config/models.test.ts @@ -18,6 +18,7 @@ describe('OPENCODE_MODEL_DEFINITIONS', () => { 'antigravity-claude-opus-4-6-thinking', 'antigravity-claude-sonnet-4-6-thinking', 'antigravity-gemini-3.1-flash-image', + 'antigravity-gemini-3.1-flash-lite', 'antigravity-gemini-3.1-pro', 'antigravity-gemini-3.5-flash', 'antigravity-gemini-3.6-flash', @@ -52,6 +53,17 @@ describe('OPENCODE_MODEL_DEFINITIONS', () => { }) }) + it('registers flash-lite without thinking variants', () => { + expect(getModel('antigravity-gemini-3.1-flash-lite')).toMatchObject({ + reasoning: false, + limit: { context: 1048576, output: 65535 }, + modalities: { input: ['text'], output: ['text'] }, + }) + expect( + getModel('antigravity-gemini-3.1-flash-lite').variants, + ).toBeUndefined() + }) + it('disables unsupported automatic Claude budget variants', () => { expect(getModel('antigravity-claude-opus-4-6-thinking').variants).toEqual({ low: { disabled: true }, diff --git a/packages/opencode/src/plugin/quota.test.ts b/packages/opencode/src/plugin/quota.test.ts index 07a449c..c1507e4 100644 --- a/packages/opencode/src/plugin/quota.test.ts +++ b/packages/opencode/src/plugin/quota.test.ts @@ -53,6 +53,9 @@ describe('classifyQuotaGroup', () => { 'Gemini 3.6 Flash (Medium)', ), ).toBe('gemini-flash') + expect( + classifyQuotaGroup('gemini-3.1-flash-lite', 'Gemini 3.1 Flash Lite'), + ).toBe('gemini-flash') expect(classifyQuotaGroup('gemini-pro-agent', 'Gemini 3.1 Pro')).toBe( 'gemini-pro', ) diff --git a/packages/opencode/src/plugin/request.test.ts b/packages/opencode/src/plugin/request.test.ts index 2161798..a47cf41 100644 --- a/packages/opencode/src/plugin/request.test.ts +++ b/packages/opencode/src/plugin/request.test.ts @@ -1611,6 +1611,33 @@ describe('request.ts', () => { }) }) + it('builds the AGY 1.1.6 request for gemini-3.1-flash-lite as a non-thinking model', () => { + const result = prepareAntigravityRequest( + 'https://generativelanguage.googleapis.com/v1beta/models/antigravity-gemini-3.1-flash-lite:generateContent', + { + method: 'POST', + body: JSON.stringify({ + contents: [ + { role: 'user', parts: [{ text: 'Reply with FLASHLITE_OK' }] }, + ], + generationConfig: { maxOutputTokens: 1024 }, + }), + }, + mockAccessToken, + mockProjectId, + undefined, + 'antigravity', + ) + + const wrapped = JSON.parse(result.init.body as string) + expect(result.effectiveModel).toBe('gemini-3.1-flash-lite') + expect(wrapped.model).toBe('gemini-3.1-flash-lite') + expect(wrapped.request.generationConfig).toMatchObject({ + maxOutputTokens: 65535, + }) + expect(wrapped.request.generationConfig.thinkingConfig).toBeUndefined() + }) + it('builds the live Gemini 3.1 Flash Image request shape', () => { const result = prepareAntigravityRequest( 'https://generativelanguage.googleapis.com/v1beta/models/antigravity-gemini-3.1-flash-image:generateContent', diff --git a/packages/opencode/src/plugin/request.ts b/packages/opencode/src/plugin/request.ts index 77ab5bb..9b7cf2b 100644 --- a/packages/opencode/src/plugin/request.ts +++ b/packages/opencode/src/plugin/request.ts @@ -202,6 +202,9 @@ function getAgyMaxOutputTokens(model: string): number | undefined { ) { return 65536 } + if (lower === 'gemini-3.1-flash-lite') { + return 65535 + } if (lower === 'gemini-3.1-pro-low' || lower === 'gemini-pro-agent') { return 65535 } diff --git a/packages/pi/src/index.test.ts b/packages/pi/src/index.test.ts index 9cd0a17..ac5d7fe 100644 --- a/packages/pi/src/index.test.ts +++ b/packages/pi/src/index.test.ts @@ -14,6 +14,7 @@ describe('Pi Antigravity model catalog', () => { models: Array<{ id: string reasoning: boolean + input: Array contextWindow: number maxTokens: number }> @@ -21,9 +22,19 @@ describe('Pi Antigravity model catalog', () => { ] const modelIds = config.models.map((model) => model.id) + expect(modelIds).toContain('antigravity-gemini-3.1-flash-lite') expect(modelIds).toContain('antigravity-gemini-3.6-flash') expect(modelIds).toContain('antigravity-gpt-oss-120b-medium') expect(modelIds).not.toContain('antigravity-gemini-3.1-flash-image') + const flashLite = config.models.find( + (model) => model.id === 'antigravity-gemini-3.1-flash-lite', + ) + expect(flashLite).toMatchObject({ + reasoning: false, + contextWindow: 1048576, + maxTokens: 65535, + }) + expect(flashLite!.input).toEqual(['text']) expect( config.models.find( (model) => model.id === 'antigravity-gemini-3.6-flash', diff --git a/packages/pi/src/index.ts b/packages/pi/src/index.ts index f142a16..3007cd6 100644 --- a/packages/pi/src/index.ts +++ b/packages/pi/src/index.ts @@ -15,8 +15,15 @@ import { streamCortexKitAntigravity } from './stream.ts' const ANTIGRAVITY_PROVIDER_ID = 'google-antigravity' -function textImageInput(): Array<'text' | 'image'> { - return ['text', 'image'] +/** Derive Pi-compatible input modalities from the model registry, keeping + * selectable capabilities consistent with the source of truth. + * Pi's protocol only supports 'text' and 'image' input types. */ +function modelInput(modalities: { + input: Array +}): Array<'text' | 'image'> { + return modalities.input.filter( + (m): m is 'text' | 'image' => m === 'text' || m === 'image', + ) } async function loginAntigravity( @@ -84,7 +91,7 @@ export default function cortexKitPiAntigravityAuth(pi: ExtensionAPI): void { id: model.id, name: model.name, reasoning: model.reasoning, - input: textImageInput(), + input: modelInput(model.modalities), cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: model.limit.context, maxTokens: model.limit.output,