Skip to content

Commit f5bc836

Browse files
committed
feat: expose verified image and GPT-OSS models
1 parent 7d635d7 commit f5bc836

26 files changed

Lines changed: 587 additions & 123 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Antigravity Auth
22

33
Google Antigravity OAuth for coding agents. Authenticate with your Google
4-
account and access **Gemini 3** and **Claude 4.6** models through Antigravity's
5-
endpoints.
4+
account and access **Gemini 3**, **Claude 4.6**, and **GPT-OSS 120B** models through
5+
Antigravity's endpoints.
66

77
This is a monorepo publishing three packages:
88

@@ -33,7 +33,7 @@ configuration, login, and troubleshooting.
3333
**pi** — install the extension package:
3434

3535
```bash
36-
pi package add @cortexkit/pi-antigravity-auth
36+
pi install npm:@cortexkit/pi-antigravity-auth
3737
```
3838

3939
Then `/login google-antigravity`. See the [pi package README](packages/pi/README.md).

packages/core/src/agy-request-metadata.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import {
1515
type ModelMetadataFixture = {
1616
models: Record<string, {
1717
modelEnum: string
18-
thinkingBudget: number
19-
maxOutputTokens: number
18+
thinkingBudget: number | null
19+
maxOutputTokens: number | null
2020
}>
2121
}
2222

packages/core/src/agy-request-metadata.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const AGY_MODEL_ENUM_BY_WIRE_MODEL: Readonly<Record<string, string>> = {
2424
"gemini-pro-agent": "MODEL_PLACEHOLDER_M16",
2525
"claude-sonnet-4-6": "MODEL_PLACEHOLDER_M35",
2626
"claude-opus-4-6-thinking": "MODEL_PLACEHOLDER_M26",
27+
"gemini-3.1-flash-image": "MODEL_PLACEHOLDER_M21",
2728
"gpt-oss-120b-medium": "MODEL_OPENAI_GPT_OSS_120B_MEDIUM",
2829
}
2930

packages/core/src/model-registry.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ describe("model registry", () => {
2828
expect(modelNames).toEqual([
2929
"antigravity-claude-opus-4-6-thinking",
3030
"antigravity-claude-sonnet-4-6-thinking",
31+
"antigravity-gemini-3.1-flash-image",
3132
"antigravity-gemini-3.1-pro",
3233
"antigravity-gemini-3.5-flash",
34+
"antigravity-gpt-oss-120b-medium",
3335
])
3436

3537
for (const definition of Object.values(definitions)) {
@@ -53,7 +55,23 @@ describe("model registry", () => {
5355
expect(aliases["gemini-3.5-flash-medium"]).toBe("gemini-3.5-flash")
5456
expect(aliases["gemini-claude-opus-4-6-thinking-medium"]).toBe("claude-opus-4-6-thinking")
5557
expect(aliases["gemini-claude-sonnet-4-6-thinking-high"]).toBe("claude-sonnet-4-6")
58+
expect(aliases["gpt-oss-120b"]).toBe("gpt-oss-120b-medium")
59+
})
60+
61+
it("matches the live GPT-OSS capability metadata", () => {
62+
expect(getPublicModelDefinitions()["antigravity-gpt-oss-120b-medium"]).toMatchObject({
63+
reasoning: true,
64+
limit: { context: 131072, output: 32768 },
65+
})
66+
})
5667

57-
expect(getPublicModelDefinitions()["antigravity-gpt-oss-120b"]).toBeUndefined()
68+
it("advertises image output only on the image route", () => {
69+
expect(getPublicModelDefinitions()["antigravity-gemini-3.1-flash-image"]).toMatchObject({
70+
reasoning: false,
71+
modalities: {
72+
input: ["text", "image"],
73+
output: ["text", "image"],
74+
},
75+
})
5876
})
5977
})

packages/core/src/model-registry.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,14 +137,11 @@ const ALL_MODEL_DEFINITIONS: OpencodeModelDefinitions = {
137137
output: ["text", "image"],
138138
},
139139
}),
140-
"antigravity-gpt-oss-120b": defineModel("antigravity-gpt-oss-120b", {
141-
name: "GPT-OSS 120B (Antigravity)",
142-
reasoning: false,
143-
limit: { context: 128000, output: 16384 },
140+
"antigravity-gpt-oss-120b-medium": defineModel("antigravity-gpt-oss-120b-medium", {
141+
name: "GPT-OSS 120B Medium (Antigravity)",
142+
reasoning: true,
143+
limit: { context: 131072, output: 32768 },
144144
modalities: DEFAULT_MODALITIES,
145-
variants: {
146-
medium: {},
147-
},
148145
}),
149146
"gemini-2.5-flash": defineModel("gemini-2.5-flash", {
150147
name: "Gemini 2.5 Flash (Gemini CLI)",
@@ -222,6 +219,7 @@ const RESOLVER_ALIASES: Record<string, string> = {
222219
"claude-sonnet-4-6-thinking-low": "claude-sonnet-4-6",
223220
"claude-sonnet-4-6-thinking-medium": "claude-sonnet-4-6",
224221
"claude-sonnet-4-6-thinking-high": "claude-sonnet-4-6",
222+
"gpt-oss-120b": "gpt-oss-120b-medium",
225223
}
226224

227225
const GEMINI_35_FLASH_ROUTES: Gemini35FlashRouteMetadata = {
@@ -259,6 +257,8 @@ const ANTIGRAVITY_OPENCODE_MODEL_IDS = [
259257
"antigravity-gemini-3.1-pro",
260258
"antigravity-claude-sonnet-4-6-thinking",
261259
"antigravity-claude-opus-4-6-thinking",
260+
"antigravity-gemini-3.1-flash-image",
261+
"antigravity-gpt-oss-120b-medium",
262262
] as const
263263

264264
function pickModelDefinitions(ids: readonly string[]): OpencodeModelDefinitions {

packages/core/src/transform/gemini.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,34 @@ describe("transform/gemini", () => {
14581458
expect(props["x"]!.type).toBe("STRING");
14591459
});
14601460

1461+
it("normalizes existing GPT-OSS function declarations and numeric constraints", () => {
1462+
const payload: RequestPayload = {
1463+
contents: [],
1464+
tools: [{
1465+
functionDeclarations: [{
1466+
name: "prompt_tool",
1467+
parametersJsonSchema: {
1468+
type: "object",
1469+
properties: {
1470+
prompt: { type: "string", minLength: "1" },
1471+
},
1472+
required: ["prompt"],
1473+
},
1474+
}],
1475+
}],
1476+
};
1477+
1478+
applyGeminiTransforms(payload, { model: "gpt-oss-120b-medium" });
1479+
1480+
const tools = payload.tools as Array<Record<string, unknown>>;
1481+
const declarations = tools[0]!.functionDeclarations as Array<Record<string, unknown>>;
1482+
const parameters = declarations[0]!.parameters as Record<string, unknown>;
1483+
const properties = parameters.properties as Record<string, Record<string, unknown>>;
1484+
expect(parameters.type).toBe("OBJECT");
1485+
expect(properties.prompt).toEqual({ type: "STRING", description: "minLength: 1" });
1486+
expect(declarations[0]).not.toHaveProperty("parametersJsonSchema");
1487+
});
1488+
14611489
it("handles mixed tools and googleSearch", () => {
14621490
const payload: RequestPayload = {
14631491
contents: [],

packages/core/src/transform/gemini.ts

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,37 @@ const UNSUPPORTED_SCHEMA_FIELDS = new Set([
4949
"maxContains",
5050
]);
5151

52-
export function toGeminiSchema(schema: unknown): unknown {
52+
const NUMERIC_SCHEMA_CONSTRAINTS = new Set([
53+
"minLength",
54+
"maxLength",
55+
"minItems",
56+
"maxItems",
57+
"minProperties",
58+
"maxProperties",
59+
"minimum",
60+
"maximum",
61+
"exclusiveMinimum",
62+
"exclusiveMaximum",
63+
"multipleOf",
64+
])
65+
66+
export interface GeminiSchemaOptions {
67+
/**
68+
* AGY's GPT bridge re-encodes protobuf numeric constraints as strings before
69+
* OpenAI JSON Schema validation. Move them to descriptions instead.
70+
*/
71+
moveNumericConstraintsToDescription?: boolean
72+
}
73+
74+
export function toGeminiSchema(schema: unknown, options: GeminiSchemaOptions = {}): unknown {
5375
// Return primitives and arrays as-is
5476
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
5577
return schema;
5678
}
5779

5880
const inputSchema = schema as Record<string, unknown>;
5981
const result: Record<string, unknown> = {};
82+
const numericConstraintHints: string[] = [];
6083

6184
// First pass: collect all property names for required validation
6285
const propertyNames = new Set<string>();
@@ -79,18 +102,25 @@ export function toGeminiSchema(schema: unknown): unknown {
79102
// Recursively transform nested property schemas
80103
const props: Record<string, unknown> = {};
81104
for (const [propName, propSchema] of Object.entries(value as Record<string, unknown>)) {
82-
props[propName] = toGeminiSchema(propSchema);
105+
props[propName] = toGeminiSchema(propSchema, options);
83106
}
84107
result[key] = props;
85108
} else if (key === "items" && typeof value === "object") {
86109
// Transform array items schema
87-
result[key] = toGeminiSchema(value);
110+
result[key] = toGeminiSchema(value, options);
88111
} else if ((key === "anyOf" || key === "oneOf" || key === "allOf") && Array.isArray(value)) {
89112
// Transform union type schemas
90-
result[key] = value.map((item) => toGeminiSchema(item));
113+
result[key] = value.map((item) => toGeminiSchema(item, options));
91114
} else if (key === "enum" && Array.isArray(value)) {
92115
// Keep enum values as-is
93116
result[key] = value;
117+
} else if (
118+
options.moveNumericConstraintsToDescription &&
119+
NUMERIC_SCHEMA_CONSTRAINTS.has(key)
120+
) {
121+
if (typeof value === "string" || typeof value === "number") {
122+
numericConstraintHints.push(`${key}: ${value}`)
123+
}
94124
} else if (key === "default" || key === "examples") {
95125
// Keep default and examples as-is
96126
result[key] = value;
@@ -114,6 +144,13 @@ export function toGeminiSchema(schema: unknown): unknown {
114144
}
115145
}
116146

147+
if (numericConstraintHints.length > 0) {
148+
const hint = numericConstraintHints.join(", ")
149+
result.description = typeof result.description === "string" && result.description
150+
? `${result.description} (${hint})`
151+
: hint
152+
}
153+
117154
// Issue #80: Ensure array schemas have an 'items' field
118155
// Gemini API requires: "parameters.properties[X].items: missing field"
119156
if (result.type === "ARRAY" && !result.items) {
@@ -229,6 +266,7 @@ export function buildImageGenerationConfig(): ImageConfig {
229266
*/
230267
export function normalizeGeminiTools(
231268
payload: RequestPayload,
269+
schemaOptions: GeminiSchemaOptions = {},
232270
): { toolDebugMissing: number; toolDebugSummaries: string[] } {
233271
let toolDebugMissing = 0;
234272
const toolDebugSummaries: string[] = [];
@@ -245,6 +283,22 @@ export function normalizeGeminiTools(
245283
return t;
246284
}
247285

286+
if (Array.isArray(t.functionDeclarations)) {
287+
return {
288+
...t,
289+
functionDeclarations: t.functionDeclarations.map((declaration) => {
290+
const normalized = { ...(declaration as Record<string, unknown>) }
291+
const rawSchema = normalized.parameters ?? normalized.parametersJsonSchema ?? {
292+
type: "OBJECT",
293+
properties: {},
294+
}
295+
normalized.parameters = toGeminiSchema(rawSchema, schemaOptions)
296+
delete normalized.parametersJsonSchema
297+
return normalized
298+
}),
299+
}
300+
}
301+
248302
const newTool = { ...t };
249303

250304
const schemaCandidates = [
@@ -276,7 +330,7 @@ export function normalizeGeminiTools(
276330
toolDebugMissing += 1;
277331
} else {
278332
// Transform existing schema to Gemini-compatible format
279-
schema = toGeminiSchema(schema) as Record<string, unknown>;
333+
schema = toGeminiSchema(schema, schemaOptions) as Record<string, unknown>;
280334
}
281335

282336
const nameCandidate =
@@ -417,7 +471,9 @@ export function applyGeminiTransforms(
417471
}
418472

419473
// 3. Normalize tools
420-
const result = normalizeGeminiTools(payload);
474+
const result = normalizeGeminiTools(payload, {
475+
moveNumericConstraintsToDescription: model.toLowerCase().startsWith("gpt-oss-"),
476+
});
421477

422478
// 4. Wrap tools in functionDeclarations format (fixes #203, #206)
423479
// Antigravity strict protobuf validation rejects wrapper-level 'parameters' field

packages/core/src/transform/model-resolver.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ describe("resolveModelWithTier", () => {
177177
expect(result.isThinkingModel).toBe(true);
178178
expect(result.thinkingBudget).toBe(1024);
179179
expect(result.quotaPreference).toBe("antigravity");
180-
}); });
180+
});
181+
});
181182

182183
describe("Image models", () => {
183184
it("marks antigravity-gemini-3-pro-image as explicit quota", () => {
@@ -195,6 +196,32 @@ describe("resolveModelWithTier", () => {
195196
expect(result.explicitQuota).toBe(true);
196197
expect(result.quotaPreference).toBe("antigravity");
197198
});
199+
200+
it("resolves the live Gemini 3.1 Flash Image route without thinking", () => {
201+
expect(resolveModelWithTier("antigravity-gemini-3.1-flash-image")).toMatchObject({
202+
actualModel: "gemini-3.1-flash-image",
203+
isThinkingModel: false,
204+
isImageModel: true,
205+
quotaPreference: "antigravity",
206+
explicitQuota: true,
207+
});
208+
});
209+
});
210+
211+
describe("GPT-OSS", () => {
212+
it("resolves the only live GPT-OSS route with its captured thinking budget", () => {
213+
expect(resolveModelWithTier("antigravity-gpt-oss-120b-medium")).toMatchObject({
214+
actualModel: "gpt-oss-120b-medium",
215+
thinkingBudget: 8192,
216+
isThinkingModel: true,
217+
quotaPreference: "antigravity",
218+
explicitQuota: true,
219+
});
220+
});
221+
222+
it("maps the short GPT-OSS alias to the medium route", () => {
223+
expect(resolveModelWithTier("antigravity-gpt-oss-120b").actualModel).toBe("gpt-oss-120b-medium");
224+
});
198225
});
199226
});
200227

packages/core/src/transform/model-resolver.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export function resolveModelWithTier(requestedModel: string, options: ModelResol
194194
const isGemini3Flash = isGemini3FlashModel(modelWithoutQuota);
195195
const isGemini31Pro = /^gemini-3\.1-pro/i.test(baseName);
196196
const isGemini35Flash = /^gemini-3\.5-flash/i.test(baseName);
197+
const isGptOss120b = /^gpt-oss-120b(?:-medium)?$/i.test(baseName);
197198

198199
if (isGemini31Pro && quotaPreference === "antigravity") {
199200
return {
@@ -216,6 +217,16 @@ export function resolveModelWithTier(requestedModel: string, options: ModelResol
216217
explicitQuota,
217218
};
218219
}
220+
221+
if (isGptOss120b && quotaPreference === "antigravity") {
222+
return {
223+
actualModel: "gpt-oss-120b-medium",
224+
thinkingBudget: 8192,
225+
isThinkingModel: true,
226+
quotaPreference,
227+
explicitQuota,
228+
};
229+
}
219230

220231
let antigravityModel = modelWithoutQuota;
221232
if (skipAlias) {

packages/opencode/README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Enable Opencode to authenticate against **Antigravity** (Google's IDE) via OAuth
88

99
## What You Get
1010

11-
- **Claude Opus 4.6, Sonnet 4.6** and **Gemini 3.1 Pro/Flash** via Google OAuth
11+
- **Claude Opus 4.6, Sonnet 4.6, Gemini 3.5 Flash, Gemini 3.1 Pro/Image, and GPT-OSS 120B** via Google OAuth
1212
- **Multi-account support** — add multiple Google accounts, auto-rotates when rate-limited
1313
- **Dual quota system** — access both Antigravity and Gemini CLI quotas from one plugin
1414
- **Thinking models** — extended thinking for Claude and Gemini 3 with configurable budgets
@@ -104,15 +104,16 @@ opencode run "Hello" --model=google/antigravity-claude-opus-4-6-thinking --varia
104104

105105
### Model Reference
106106

107-
**Antigravity quota** (default routing for Claude and Gemini):
107+
**Antigravity quota:**
108108

109109
| Model | Variants | Notes |
110110
|-------|----------|-------|
111-
| `antigravity-gemini-3-pro` | low, high | Gemini 3 Pro with thinking |
112-
| `antigravity-gemini-3.1-pro` | low, high | Gemini 3.1 Pro with thinking (rollout-dependent) |
113-
| `antigravity-gemini-3-flash` | minimal, low, medium, high | Gemini 3 Flash with thinking |
114-
| `antigravity-claude-sonnet-4-6` || Claude Sonnet 4.6 |
115-
| `antigravity-claude-opus-4-6-thinking` | low, max | Claude Opus 4.6 with extended thinking |
111+
| `antigravity-gemini-3.5-flash` | low, high | Gemini 3.5 Flash with medium as the default |
112+
| `antigravity-gemini-3.1-pro` | low, high | Gemini 3.1 Pro with thinking |
113+
| `antigravity-gemini-3.1-flash-image` || Gemini 3.1 image generation |
114+
| `antigravity-claude-sonnet-4-6-thinking` || Claude Sonnet 4.6 with thinking |
115+
| `antigravity-claude-opus-4-6-thinking` || Claude Opus 4.6 with thinking |
116+
| `antigravity-gpt-oss-120b-medium` || GPT-OSS 120B medium reasoning |
116117

117118
**Gemini CLI quota** (separate from Antigravity; used when `cli_first` is true or as fallback):
118119

0 commit comments

Comments
 (0)