diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts index f08ce7e576..28cba9ec38 100644 --- a/open-sse/executors/antigravity.ts +++ b/open-sse/executors/antigravity.ts @@ -583,6 +583,9 @@ export class AntigravityExecutor extends BaseExecutor { : credentials.refreshToken, expiresIn: typeof tokens.expires_in === "number" ? tokens.expires_in : undefined, projectId: credentials.projectId, + // Preserve providerSpecificData so a projectId stored there survives the refresh + // (the onCredentialsRefreshed DB write) instead of being dropped → 422 (#2480). + providerSpecificData: credentials.providerSpecificData, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index 2140278d3f..cc8c744a11 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -508,7 +508,15 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra // Both Antigravity and Gemini CLI need the project field for the Cloud Code API. // For Gemini CLI, the stored projectId may be stale; the executor's transformRequest // refreshes it via loadCodeAssist before the request is sent to the API. - let projectId = credentials?.projectId; + // Fall back to providerSpecificData.projectId — some connections (and post-refresh + // credentials) store it there rather than at the top level, which otherwise produced a + // spurious 422 "Missing Google projectId" on the Antigravity /v1beta path (#2480). + const providerSpecificProjectId = ( + credentials?.providerSpecificData as { projectId?: unknown } | undefined + )?.projectId; + let projectId = + credentials?.projectId || + (typeof providerSpecificProjectId === "string" ? providerSpecificProjectId : ""); if (!projectId) { console.warn( diff --git a/tests/unit/translator-openai-to-gemini.test.ts b/tests/unit/translator-openai-to-gemini.test.ts index daf2b12e61..993be797c1 100644 --- a/tests/unit/translator-openai-to-gemini.test.ts +++ b/tests/unit/translator-openai-to-gemini.test.ts @@ -891,3 +891,26 @@ test("OpenAI -> Antigravity Gemini path preserves thinkingConfig (only Claude is assert.equal((result as any).request?.generationConfig.thinkingConfig.thinkingBudget > 0, true); assert.equal((result as any).request?.generationConfig.thinkingConfig.includeThoughts, true); }); + +// Regression for #2480: when projectId is stored in providerSpecificData rather than at +// the top level of the credential record, the Antigravity Cloud Code envelope must still +// pick it up — otherwise the /v1beta path 422s with "Missing Google projectId". +test("openaiToAntigravityRequest falls back to providerSpecificData.projectId (#2480)", () => { + const result = openaiToAntigravityRequest( + "gemini-3.1-flash-lite", + { messages: [{ role: "user", content: "Hello" }] }, + false, + { providerSpecificData: { projectId: "proj-from-psd" } } as any + ); + assert.equal(result.project, "proj-from-psd"); +}); + +test("openaiToAntigravityRequest prefers top-level projectId over providerSpecificData (#2480)", () => { + const result = openaiToAntigravityRequest( + "gemini-3.1-flash-lite", + { messages: [{ role: "user", content: "Hello" }] }, + false, + { projectId: "proj-top", providerSpecificData: { projectId: "proj-psd" } } as any + ); + assert.equal(result.project, "proj-top"); +});