Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/core/src/model-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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'] },
})
})
})
11 changes: 11 additions & 0 deletions packages/core/src/model-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Comment thread
iceteaSA marked this conversation as resolved.
{
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,
Expand Down Expand Up @@ -291,6 +300,7 @@ const QUOTA_GROUP_BY_MODEL_ID: Record<string, ModelQuotaGroup> = {
'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',
Expand All @@ -304,6 +314,7 @@ const QUOTA_GROUP_BY_MODEL_ID: Record<string, ModelQuotaGroup> = {

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',
Expand Down
69 changes: 69 additions & 0 deletions packages/core/src/transform/model-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
[
Expand Down Expand Up @@ -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', () => {
Expand Down
34 changes: 33 additions & 1 deletion packages/core/src/transform/model-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -428,11 +439,15 @@ export function resolveModelForHeaderStyle(
const isGemini35Flash = isGemini35FlashModel(
transformedModel.replace(TIER_REGEX, ''),
)
const isGemini31FlashLite = /^gemini-3\.1-flash-lite/i.test(
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
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
) {
Expand All @@ -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) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
// 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,
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/script/test-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/src/plugin/config/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 },
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/plugin/quota.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)
Expand Down
27 changes: 27 additions & 0 deletions packages/opencode/src/plugin/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/plugin/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
11 changes: 11 additions & 0 deletions packages/pi/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,27 @@ describe('Pi Antigravity model catalog', () => {
models: Array<{
id: string
reasoning: boolean
input: Array<string>
contextWindow: number
maxTokens: number
}>
},
]
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',
Expand Down
13 changes: 10 additions & 3 deletions packages/pi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>
}): Array<'text' | 'image'> {
return modalities.input.filter(
(m): m is 'text' | 'image' => m === 'text' || m === 'image',
)
}

async function loginAntigravity(
Expand Down Expand Up @@ -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,
Expand Down