authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-03 16:35:19-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 16:35:19-07:00
logbaf67b6e3accd48d510fd0d39a4e6b978a1eea80
tree16f54313f76035e495878ff6ca735fc9ad2dd307
parentf25e4428961982e3935300cfd3c78f7d0fac4ee9
signature Signed by PGP key B5690EEEBB952194

fix(antigravity): keep model choices up to date (#9511)


10 files changed, 113 insertions(+), 10 deletions(-)

apps/server/src/provider/Drivers/AntigravityDriver.ts+4
......@@ -233,6 +233,9 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
233233 if (event._tag === "EventStreamBarrier") {
234234 return Deferred.succeed(event.acknowledge, undefined).pipe(Effect.asVoid);
235235 }
236 if (event._tag === "ConfigOptionsUpdated") {
237 return provider.onConfigOptionsUpdated(event.configOptions);
238 }
236239 return event._tag === "AvailableCommandsUpdated"
237240 ? provider.onAvailableCommands(event.availableCommands)
238241 : Effect.void;
......@@ -299,6 +302,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
299302 withProcess: authFlow.withProcess,
300303 defaultModel,
301304 onSessionStarted: provider.onSessionStarted,
305 onConfigOptionsUpdated: provider.onConfigOptionsUpdated,
302306 onAvailableCommands: provider.onAvailableCommands,
303307 onAuthRequired: provider.onAuthRequired,
304308 ...(loggers.native ? { nativeEventLogger: loggers.native } : {}),
apps/server/src/provider/Layers/AntigravityAdapter.test.ts+8
......@@ -297,6 +297,7 @@ it.layer(layer)("AntigravityAdapter", (it) => {
297297 );
298298 const requestLog = path.join(cwd, "requests.ndjson");
299299 const commands: string[] = [];
300 const modelSelections: string[] = [];
300301 const observed: ProviderRuntimeEvent[] = [];
301302 const completed = yield* Deferred.make<void>();
302303 const adapter = yield* makeAntigravityAdapter(decodeSettings({ enabled: true }), {
......@@ -322,6 +323,11 @@ it.layer(layer)("AntigravityAdapter", (it) => {
322323 Effect.sync(() => {
323324 commands.push(...available.map((command) => command.name));
324325 }),
326 onConfigOptionsUpdated: (configOptions) =>
327 Effect.sync(() => {
328 const model = configOptions.find((option) => option.category === "model");
329 if (model?.type === "select") modelSelections.push(model.currentValue);
330 }),
325331 });
326332 yield* adapter.streamEvents.pipe(
327333 Stream.runForEach((event) =>
......@@ -350,6 +356,8 @@ it.layer(layer)("AntigravityAdapter", (it) => {
350356 yield* adapter.sendTurn({ threadId, input: "Reply with one short line." });
351357 yield* Deferred.await(completed);
352358 expect(commands).toEqual(["plan", "logout", "plan", "logout"]);
359 expect(modelSelections.length).toBeGreaterThan(0);
360 expect(modelSelections.every((model) => model === nativeAlternative)).toBe(true);
353361 expect(
354362 observed
355363 .filter((event) => event.type === "content.delta")
apps/server/src/provider/Layers/AntigravityAdapter.ts+6
......@@ -133,6 +133,9 @@ export interface AntigravityAdapterOptions {
133133 commands: ReadonlyArray<EffectAcpSchema.AvailableCommand>,
134134 cwd: string,
135135 ) => Effect.Effect<void>;
136 readonly onConfigOptionsUpdated?: (
137 configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>,
138 ) => Effect.Effect<void>;
136139 readonly onAuthRequired?: Effect.Effect<void>;
137140 /** Model the provider default alias selects, when the account offers it. */
138141 readonly defaultModel?: Effect.Effect<string | undefined>;
......@@ -503,6 +506,9 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
503506 case "AvailableCommandsUpdated":
504507 yield* options.onAvailableCommands?.(event.availableCommands, context.cwd) ?? Effect.void;
505508 return;
509 case "ConfigOptionsUpdated":
510 yield* options.onConfigOptionsUpdated?.(event.configOptions) ?? Effect.void;
511 return;
506512 case "ConnectionTerminated":
507513 context.stopped = true;
508514 context.disconnected = true;
apps/server/src/provider/Layers/AntigravityProvider.test.ts+35-2
......@@ -175,7 +175,6 @@ describe("Antigravity model catalog", () => {
175175
176176 it("uses legacy session models only when model config is absent", () => {
177177 const fromLegacy = buildAntigravityModelsFromSession({
178 sessionId: "legacy-session",
179178 models: sessionSetupResult.models,
180179 });
181180 expect(fromLegacy).toEqual(buildAntigravityModelsFromSession(sessionSetupResult));
......@@ -189,7 +188,6 @@ describe("Antigravity model catalog", () => {
189188
190189 it("flattens native option groups without combining distinct model IDs", () => {
191190 const models = buildAntigravityModelsFromSession({
192 sessionId: "grouped-session",
193191 configOptions: [
194192 {
195193 ...modelConfig,
......@@ -356,7 +354,9 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => {
356354 supportsTextGeneration: false,
357355 });
358356 yield* harness.provider.onAvailableCommands(commands, "/workspace");
357 yield* harness.provider.onConfigOptionsUpdated([modelConfig]);
359358 expect((yield* harness.provider.snapshotForCwd("/workspace")).slashCommands).toEqual([]);
359 expect((yield* harness.provider.snapshot.getSnapshot).models).toEqual([]);
360360 }
361361 const refreshed = yield* harness.provider.snapshot.refresh;
362362 expect(refreshed.auth.status).toBe("unauthenticated");
......@@ -365,6 +365,39 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => {
365365 ),
366366 );
367367
368 it.effect("replaces live model choices and accepts an empty catalog", () =>
369 Effect.scoped(
370 Effect.gen(function* () {
371 const harness = yield* makeHarness();
372 yield* harness.initialize;
373 yield* harness.provider.onSessionStarted(started, "/workspace");
374 yield* harness.provider.onAvailableCommands(commands, "/workspace");
375 const before = yield* harness.provider.snapshot.getSnapshot;
376 const configOptions = [
377 {
378 ...modelConfig,
379 currentValue: "gemini-3.8-flash-high",
380 options: modelOptions.slice(0, 3),
381 },
382 ];
383 const nextSnapshot = yield* Stream.toPull(
384 harness.provider.snapshot.streamChanges.pipe(
385 Stream.filter((snapshot) => snapshot.models.length === 3),
386 ),
387 );
388 yield* harness.provider.onConfigOptionsUpdated(configOptions);
389 expect((yield* nextSnapshot)[0]).toMatchObject({
390 models: buildAntigravityModelsFromSession({ configOptions }),
391 auth: before.auth,
392 workspaceSnapshots: before.workspaceSnapshots,
393 slashCommands: commands,
394 });
395 yield* harness.provider.onConfigOptionsUpdated([]);
396 expect((yield* harness.provider.snapshot.getSnapshot).models).toEqual([]);
397 }),
398 ),
399 );
400
368401 it.effect("replaces one account's catalog instead of combining accounts", () =>
369402 Effect.scoped(
370403 Effect.gen(function* () {
apps/server/src/provider/Layers/AntigravityProvider.ts+15-1
......@@ -37,7 +37,10 @@ const SIGN_IN_MESSAGE = "Sign in with Google to use Antigravity.";
3737const AUTH_UNCHECKED_MESSAGE =
3838 "Antigravity is installed. Google account access is not checked yet.";
3939
40type SessionSetupResult = AcpSessionRuntimeStartResult["sessionSetupResult"];
40type SessionSetupResult = Pick<
41 AcpSessionRuntimeStartResult["sessionSetupResult"],
42 "configOptions" | "models"
43>;
4144
4245/** Keep the native model IDs, including model-specific thinking levels. */
4346export function buildAntigravityModelsFromSession(
......@@ -299,6 +302,16 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
299302 });
300303 });
301304
305 const onConfigOptionsUpdated = Effect.fn("AntigravityProvider.onConfigOptionsUpdated")(function* (
306 configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>,
307 ) {
308 const models = buildAntigravityModelsFromSession({ configOptions });
309 yield* SubscriptionRef.update(metadata, (state) => {
310 if (state.draft.auth.status !== "authenticated") return state;
311 return { ...state, draft: { ...state.draft, models } };
312 });
313 });
314
302315 const onAvailableCommands = Effect.fn("AntigravityProvider.onAvailableCommands")(function* (
303316 commands: ReadonlyArray<EffectAcpSchema.AvailableCommand>,
304317 cwd?: string,
......@@ -373,6 +386,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
373386 return {
374387 snapshot: { ...managed, getSnapshot },
375388 onSessionStarted,
389 onConfigOptionsUpdated,
376390 onAvailableCommands,
377391 onSignedOut: clearAccountMetadata(),
378392 onAuthRequired: clearAccountMetadata(),
apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts+18
......@@ -69,11 +69,15 @@ describe("AcpSessionRuntime", () => {
6969 expect(events.map((event) => event._tag)).toEqual([
7070 "AvailableCommandsUpdated",
7171 "ModeChanged",
72 "ConfigOptionsUpdated",
7273 ]);
7374 expect(events[0]).toMatchObject({
7475 availableCommands: [{ name: "plan", description: "Native command" }],
7576 });
7677 expect(yield* runtime.getModeState).toMatchObject({ currentModeId: "code" });
78 expect(events[2]).toMatchObject({
79 configOptions: yield* runtime.getConfigOptions,
80 });
7781 expect(
7882 (yield* runtime.getConfigOptions).find((option) => option.category === "model"),
7983 ).toMatchObject({ currentValue: "gpt-5.4" });
......@@ -81,6 +85,20 @@ describe("AcpSessionRuntime", () => {
8185 );
8286 }
8387
88 it.effect("publishes model changes returned by a config request and live notifications", () =>
89 Effect.gen(function* () {
90 const runtime = yield* AcpSessionRuntime.make(mockRuntimeOptions);
91 yield* runtime.start();
92 const updates = yield* Stream.toPull(
93 runtime.getEvents().pipe(Stream.filter((event) => event._tag === "ConfigOptionsUpdated")),
94 );
95 const selected = yield* runtime.setConfigOption("model", "composer-2");
96 expect((yield* updates)[0]?.configOptions).toEqual(selected.configOptions);
97 yield* runtime.request("_test/startup-metadata", {});
98 expect((yield* updates)[0]?.configOptions).toEqual(yield* runtime.getConfigOptions);
99 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
100 );
101
84102 it.effect("awaits native resume instead of using the load replay idle fallback", () =>
85103 Effect.gen(function* () {
86104 const resumeStarted = yield* Deferred.make<void>();
apps/server/src/provider/acp/AcpRuntimeModel.ts+13
......@@ -90,6 +90,11 @@ export type AcpParsedSessionEvent =
9090 readonly availableCommands: ReadonlyArray<EffectAcpSchema.AvailableCommand>;
9191 readonly rawPayload: unknown;
9292 }
93 | {
94 readonly _tag: "ConfigOptionsUpdated";
95 readonly configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>;
96 readonly rawPayload: unknown;
97 }
9398 | {
9499 readonly _tag: "AssistantItemStarted";
95100 readonly itemId: string;
......@@ -788,6 +793,14 @@ export function parseSessionUpdateEvent(params: EffectAcpSchema.SessionNotificat
788793 let modeId: string | undefined;
789794
790795 switch (upd.sessionUpdate) {
796 case "config_option_update": {
797 events.push({
798 _tag: "ConfigOptionsUpdated",
799 configOptions: upd.configOptions,
800 rawPayload: params,
801 });
802 break;
803 }
791804 case "available_commands_update": {
792805 events.push({
793806 _tag: "AvailableCommandsUpdated",
apps/server/src/provider/acp/AcpSessionRuntime.ts+11-7
......@@ -615,13 +615,17 @@ export const make = (
615615 });
616616 });
617617
618 const updateConfigOptions = (
619 response:
620 | EffectAcpSchema.SetSessionConfigOptionResponse
621 | EffectAcpSchema.LoadSessionResponse
622 | EffectAcpSchema.NewSessionResponse
623 | EffectAcpSchema.ResumeSessionResponse,
624 ): Effect.Effect<void> => Ref.set(configOptionsRef, sessionConfigOptionsFromSetup(response));
618 const updateConfigOptions = Effect.fn("AcpSessionRuntime.updateConfigOptions")(function* (
619 response: EffectAcpSchema.SetSessionConfigOptionResponse,
620 ) {
621 const configOptions = sessionConfigOptionsFromSetup(response);
622 yield* Ref.set(configOptionsRef, configOptions);
623 yield* Queue.offer(eventQueue, {
624 _tag: "ConfigOptionsUpdated",
625 configOptions,
626 rawPayload: response,
627 });
628 });
625629
626630 const updateCurrentModeId = (modeId: string): Effect.Effect<void> =>
627631 Ref.update(modeStateRef, (current) =>
docs/internals/providers.md+2
......@@ -168,6 +168,8 @@ Account access starts unknown and becomes authenticated after successful session
168168including an explicit model refresh.
169169The [provider snapshot][antigravity-provider] takes models and commands from setup and native
170170updates. It preserves returned Gemini model IDs, labels, order, and thinking-level choices.
171ACP `config_option_update` notifications and `session/set_config_option` responses replace
172the instance's model catalog. Child session notifications do not change the root catalog.
171173The registry treats a successful empty catalog as authoritative and clears cached metadata
172174after sign-out. It must not retain a previous account's models. Cached models do not prove
173175current access. The auth response does not supply an email, plan tier, or reliable quota.
docs/user/providers-antigravity.md+1
......@@ -107,6 +107,7 @@ installations. Clear **Binary path** to use managed installation controls.
107107The current official ACP exposes Gemini models only. T3 Code uses the model IDs and names
108108returned for your account, including any model choices with different thinking levels. Models
109109available in other Antigravity apps might not be available through this agent.
110The model picker updates when a running session reports new model choices.
110111
111112New threads use Gemini 3.8 Flash (High) when your account offers it. Older Gemini generations
112113stay available under **Legacy models** in the picker.