fix(antigravity-auth): address Oracle feedback - custom credentials, logging, docs
- Fix custom credentials to actually work in OAuth/refresh flows - oauth.ts: Add clientId/clientSecret parameters to buildAuthURL(), exchangeCode() - token.ts: Add clientId/clientSecret parameters to refreshAccessToken() - fetch.ts: Pass credentials to oauth/token functions - plugin.ts: Use closure cache for credentials, pass to all flows - Unify console.* logging policy with ANTIGRAVITY_DEBUG guards - constants.ts: Document logging policy - tools.ts: Guard console.warn with ANTIGRAVITY_DEBUG - plugin.ts: Guard 4 console.error with ANTIGRAVITY_DEBUG - Add explicit init.body type handling - fetch.ts: Check body type, pass-through non-string bodies - fetch.ts: Document body type assumption - Document SSE buffering behavior - response.ts: Add warning that current implementation buffers - response.ts: Add TODO for future ReadableStream enhancement 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
## 현재 진행 중인 작업
|
## 현재 진행 중인 작업
|
||||||
|
|
||||||
**모든 작업 완료** - ✅ Phase 1-4 완료 (14 tasks)
|
**모든 작업 완료** - ✅ Phase 1-4 완료 (14 tasks) + Oracle 문서화 이슈 수정
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
/**
|
/**
|
||||||
* Antigravity OAuth configuration constants.
|
* Antigravity OAuth configuration constants.
|
||||||
* Values sourced from cliproxyapi/sdk/auth/antigravity.go
|
* Values sourced from cliproxyapi/sdk/auth/antigravity.go
|
||||||
|
*
|
||||||
|
* ## Logging Policy
|
||||||
|
*
|
||||||
|
* All console logging in antigravity modules follows a consistent policy:
|
||||||
|
*
|
||||||
|
* - **Debug logs**: Guard with `if (process.env.ANTIGRAVITY_DEBUG === "1")`
|
||||||
|
* - Includes: info messages, warnings, non-fatal errors
|
||||||
|
* - Enable debugging: `ANTIGRAVITY_DEBUG=1 opencode`
|
||||||
|
*
|
||||||
|
* - **Fatal errors**: None currently. All errors are handled by returning
|
||||||
|
* appropriate error responses to OpenCode's auth system.
|
||||||
|
*
|
||||||
|
* This policy ensures production silence while enabling verbose debugging
|
||||||
|
* when needed for troubleshooting OAuth flows.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// OAuth 2.0 Client Credentials
|
// OAuth 2.0 Client Credentials
|
||||||
|
|||||||
@@ -8,6 +8,12 @@
|
|||||||
* - Applies response transformation (including thinking extraction)
|
* - Applies response transformation (including thinking extraction)
|
||||||
* - Implements endpoint fallback (daily → autopush → prod)
|
* - Implements endpoint fallback (daily → autopush → prod)
|
||||||
*
|
*
|
||||||
|
* **Body Type Assumption:**
|
||||||
|
* This interceptor assumes `init.body` is a JSON string (OpenAI format).
|
||||||
|
* Non-string bodies (ReadableStream, Blob, FormData, URLSearchParams, etc.)
|
||||||
|
* are passed through unchanged to the original fetch to avoid breaking
|
||||||
|
* other requests that may not be OpenAI-format API calls.
|
||||||
|
*
|
||||||
* Debug logging available via ANTIGRAVITY_DEBUG=1 environment variable.
|
* Debug logging available via ANTIGRAVITY_DEBUG=1 environment variable.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -59,9 +65,6 @@ function isRetryableError(status: number): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt fetch with a single endpoint
|
|
||||||
*/
|
|
||||||
async function attemptFetch(
|
async function attemptFetch(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
url: string,
|
url: string,
|
||||||
@@ -69,43 +72,42 @@ async function attemptFetch(
|
|||||||
accessToken: string,
|
accessToken: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
modelName?: string
|
modelName?: string
|
||||||
): Promise<Response | null> {
|
): Promise<Response | null | "pass-through"> {
|
||||||
debugLog(`Trying endpoint: ${endpoint}`)
|
debugLog(`Trying endpoint: ${endpoint}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse request body if present
|
const rawBody = init.body
|
||||||
let body: Record<string, unknown> = {}
|
|
||||||
if (init.body) {
|
if (rawBody !== undefined && typeof rawBody !== "string") {
|
||||||
|
debugLog(`Non-string body detected (${typeof rawBody}), signaling pass-through`)
|
||||||
|
return "pass-through"
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedBody: Record<string, unknown> = {}
|
||||||
|
if (rawBody) {
|
||||||
try {
|
try {
|
||||||
body =
|
parsedBody = JSON.parse(rawBody) as Record<string, unknown>
|
||||||
typeof init.body === "string"
|
|
||||||
? (JSON.parse(init.body) as Record<string, unknown>)
|
|
||||||
: (init.body as unknown as Record<string, unknown>)
|
|
||||||
} catch {
|
} catch {
|
||||||
// If body parsing fails, use empty object
|
parsedBody = {}
|
||||||
body = {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply tool normalization if tools present
|
if (parsedBody.tools && Array.isArray(parsedBody.tools)) {
|
||||||
if (body.tools && Array.isArray(body.tools)) {
|
const normalizedTools = normalizeToolsForGemini(parsedBody.tools as OpenAITool[])
|
||||||
const normalizedTools = normalizeToolsForGemini(body.tools as OpenAITool[])
|
|
||||||
if (normalizedTools) {
|
if (normalizedTools) {
|
||||||
body.tools = normalizedTools
|
parsedBody.tools = normalizedTools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform request
|
|
||||||
const transformed = transformRequest(
|
const transformed = transformRequest(
|
||||||
url,
|
url,
|
||||||
body,
|
parsedBody,
|
||||||
accessToken,
|
accessToken,
|
||||||
projectId,
|
projectId,
|
||||||
modelName,
|
modelName,
|
||||||
endpoint
|
endpoint
|
||||||
)
|
)
|
||||||
|
|
||||||
// Make the request
|
|
||||||
const response = await fetch(transformed.url, {
|
const response = await fetch(transformed.url, {
|
||||||
method: init.method || "POST",
|
method: init.method || "POST",
|
||||||
headers: transformed.headers,
|
headers: transformed.headers,
|
||||||
@@ -113,7 +115,6 @@ async function attemptFetch(
|
|||||||
signal: init.signal,
|
signal: init.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check for retryable errors
|
|
||||||
if (!response.ok && isRetryableError(response.status)) {
|
if (!response.ok && isRetryableError(response.status)) {
|
||||||
debugLog(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`)
|
debugLog(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`)
|
||||||
return null
|
return null
|
||||||
@@ -121,7 +122,6 @@ async function attemptFetch(
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Network error - try next endpoint
|
|
||||||
debugLog(
|
debugLog(
|
||||||
`Endpoint failed: ${endpoint} (${error instanceof Error ? error.message : "Unknown error"}), trying next`
|
`Endpoint failed: ${endpoint} (${error instanceof Error ? error.message : "Unknown error"}), trying next`
|
||||||
)
|
)
|
||||||
@@ -179,6 +179,8 @@ async function transformResponseWithThinking(
|
|||||||
* @param getAuth - Async function to retrieve current auth state
|
* @param getAuth - Async function to retrieve current auth state
|
||||||
* @param client - Auth client for saving updated tokens
|
* @param client - Auth client for saving updated tokens
|
||||||
* @param providerId - Provider identifier (e.g., "google")
|
* @param providerId - Provider identifier (e.g., "google")
|
||||||
|
* @param clientId - Optional custom client ID for token refresh (defaults to ANTIGRAVITY_CLIENT_ID)
|
||||||
|
* @param clientSecret - Optional custom client secret for token refresh (defaults to ANTIGRAVITY_CLIENT_SECRET)
|
||||||
* @returns Custom fetch function compatible with standard fetch signature
|
* @returns Custom fetch function compatible with standard fetch signature
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@@ -186,7 +188,9 @@ async function transformResponseWithThinking(
|
|||||||
* const customFetch = createAntigravityFetch(
|
* const customFetch = createAntigravityFetch(
|
||||||
* () => auth(),
|
* () => auth(),
|
||||||
* client,
|
* client,
|
||||||
* "google"
|
* "google",
|
||||||
|
* "custom-client-id",
|
||||||
|
* "custom-client-secret"
|
||||||
* )
|
* )
|
||||||
*
|
*
|
||||||
* // Use like standard fetch
|
* // Use like standard fetch
|
||||||
@@ -199,7 +203,9 @@ async function transformResponseWithThinking(
|
|||||||
export function createAntigravityFetch(
|
export function createAntigravityFetch(
|
||||||
getAuth: () => Promise<Auth>,
|
getAuth: () => Promise<Auth>,
|
||||||
client: AuthClient,
|
client: AuthClient,
|
||||||
providerId: string
|
providerId: string,
|
||||||
|
clientId?: string,
|
||||||
|
clientSecret?: string
|
||||||
): (url: string, init?: RequestInit) => Promise<Response> {
|
): (url: string, init?: RequestInit) => Promise<Response> {
|
||||||
// Cache for current token state
|
// Cache for current token state
|
||||||
let cachedTokens: AntigravityTokens | null = null
|
let cachedTokens: AntigravityTokens | null = null
|
||||||
@@ -237,7 +243,7 @@ export function createAntigravityFetch(
|
|||||||
debugLog("Token expired, refreshing...")
|
debugLog("Token expired, refreshing...")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newTokens = await refreshAccessToken(refreshParts.refreshToken)
|
const newTokens = await refreshAccessToken(refreshParts.refreshToken, clientId, clientSecret)
|
||||||
|
|
||||||
// Update cached tokens
|
// Update cached tokens
|
||||||
cachedTokens = {
|
cachedTokens = {
|
||||||
@@ -297,7 +303,6 @@ export function createAntigravityFetch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try each endpoint in fallback order
|
|
||||||
const maxEndpoints = Math.min(ANTIGRAVITY_ENDPOINT_FALLBACKS.length, 3)
|
const maxEndpoints = Math.min(ANTIGRAVITY_ENDPOINT_FALLBACKS.length, 3)
|
||||||
|
|
||||||
for (let i = 0; i < maxEndpoints; i++) {
|
for (let i = 0; i < maxEndpoints; i++) {
|
||||||
@@ -312,10 +317,13 @@ export function createAntigravityFetch(
|
|||||||
modelName
|
modelName
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (response === "pass-through") {
|
||||||
|
debugLog("Non-string body detected, passing through to original fetch")
|
||||||
|
return fetch(url, init)
|
||||||
|
}
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
debugLog(`Success with endpoint: ${endpoint}`)
|
debugLog(`Success with endpoint: ${endpoint}`)
|
||||||
|
|
||||||
// Transform response (with thinking extraction if applicable)
|
|
||||||
return transformResponseWithThinking(response, modelName || "")
|
return transformResponseWithThinking(response, modelName || "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,10 +121,12 @@ export function decodeState(encoded: string): OAuthState {
|
|||||||
* Build the OAuth authorization URL with PKCE.
|
* Build the OAuth authorization URL with PKCE.
|
||||||
*
|
*
|
||||||
* @param projectId - Optional GCP project ID to include in state
|
* @param projectId - Optional GCP project ID to include in state
|
||||||
|
* @param clientId - Optional custom client ID (defaults to ANTIGRAVITY_CLIENT_ID)
|
||||||
* @returns Authorization result with URL and verifier
|
* @returns Authorization result with URL and verifier
|
||||||
*/
|
*/
|
||||||
export async function buildAuthURL(
|
export async function buildAuthURL(
|
||||||
projectId?: string
|
projectId?: string,
|
||||||
|
clientId: string = ANTIGRAVITY_CLIENT_ID
|
||||||
): Promise<AuthorizationResult> {
|
): Promise<AuthorizationResult> {
|
||||||
const pkce = await generatePKCEPair()
|
const pkce = await generatePKCEPair()
|
||||||
|
|
||||||
@@ -134,7 +136,7 @@ export async function buildAuthURL(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(GOOGLE_AUTH_URL)
|
const url = new URL(GOOGLE_AUTH_URL)
|
||||||
url.searchParams.set("client_id", ANTIGRAVITY_CLIENT_ID)
|
url.searchParams.set("client_id", clientId)
|
||||||
url.searchParams.set("redirect_uri", ANTIGRAVITY_REDIRECT_URI)
|
url.searchParams.set("redirect_uri", ANTIGRAVITY_REDIRECT_URI)
|
||||||
url.searchParams.set("response_type", "code")
|
url.searchParams.set("response_type", "code")
|
||||||
url.searchParams.set("scope", ANTIGRAVITY_SCOPES.join(" "))
|
url.searchParams.set("scope", ANTIGRAVITY_SCOPES.join(" "))
|
||||||
@@ -155,15 +157,19 @@ export async function buildAuthURL(
|
|||||||
*
|
*
|
||||||
* @param code - Authorization code from OAuth callback
|
* @param code - Authorization code from OAuth callback
|
||||||
* @param verifier - PKCE verifier from initial auth request
|
* @param verifier - PKCE verifier from initial auth request
|
||||||
|
* @param clientId - Optional custom client ID (defaults to ANTIGRAVITY_CLIENT_ID)
|
||||||
|
* @param clientSecret - Optional custom client secret (defaults to ANTIGRAVITY_CLIENT_SECRET)
|
||||||
* @returns Token exchange result with access and refresh tokens
|
* @returns Token exchange result with access and refresh tokens
|
||||||
*/
|
*/
|
||||||
export async function exchangeCode(
|
export async function exchangeCode(
|
||||||
code: string,
|
code: string,
|
||||||
verifier: string
|
verifier: string,
|
||||||
|
clientId: string = ANTIGRAVITY_CLIENT_ID,
|
||||||
|
clientSecret: string = ANTIGRAVITY_CLIENT_SECRET
|
||||||
): Promise<AntigravityTokenExchangeResult> {
|
): Promise<AntigravityTokenExchangeResult> {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id: ANTIGRAVITY_CLIENT_ID,
|
client_id: clientId,
|
||||||
client_secret: ANTIGRAVITY_CLIENT_SECRET,
|
client_secret: clientSecret,
|
||||||
code,
|
code,
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
redirect_uri: ANTIGRAVITY_REDIRECT_URI,
|
redirect_uri: ANTIGRAVITY_REDIRECT_URI,
|
||||||
@@ -317,18 +323,22 @@ export function startCallbackServer(
|
|||||||
*
|
*
|
||||||
* @param projectId - Optional GCP project ID
|
* @param projectId - Optional GCP project ID
|
||||||
* @param openBrowser - Function to open URL in browser
|
* @param openBrowser - Function to open URL in browser
|
||||||
|
* @param clientId - Optional custom client ID (defaults to ANTIGRAVITY_CLIENT_ID)
|
||||||
|
* @param clientSecret - Optional custom client secret (defaults to ANTIGRAVITY_CLIENT_SECRET)
|
||||||
* @returns Object with tokens and user info
|
* @returns Object with tokens and user info
|
||||||
*/
|
*/
|
||||||
export async function performOAuthFlow(
|
export async function performOAuthFlow(
|
||||||
projectId?: string,
|
projectId?: string,
|
||||||
openBrowser?: (url: string) => Promise<void>
|
openBrowser?: (url: string) => Promise<void>,
|
||||||
|
clientId: string = ANTIGRAVITY_CLIENT_ID,
|
||||||
|
clientSecret: string = ANTIGRAVITY_CLIENT_SECRET
|
||||||
): Promise<{
|
): Promise<{
|
||||||
tokens: AntigravityTokenExchangeResult
|
tokens: AntigravityTokenExchangeResult
|
||||||
userInfo: AntigravityUserInfo
|
userInfo: AntigravityUserInfo
|
||||||
verifier: string
|
verifier: string
|
||||||
}> {
|
}> {
|
||||||
// Build auth URL first to get the verifier
|
// Build auth URL first to get the verifier
|
||||||
const auth = await buildAuthURL(projectId)
|
const auth = await buildAuthURL(projectId, clientId)
|
||||||
|
|
||||||
// Start callback server
|
// Start callback server
|
||||||
const callbackPromise = startCallbackServer()
|
const callbackPromise = startCallbackServer()
|
||||||
@@ -356,7 +366,7 @@ export async function performOAuthFlow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exchange code for tokens
|
// Exchange code for tokens
|
||||||
const tokens = await exchangeCode(callback.code, auth.verifier)
|
const tokens = await exchangeCode(callback.code, auth.verifier, clientId, clientSecret)
|
||||||
|
|
||||||
// Fetch user info
|
// Fetch user info
|
||||||
const userInfo = await fetchUserInfo(tokens.access_token)
|
const userInfo = await fetchUserInfo(tokens.access_token)
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ function isOAuthAuth(
|
|||||||
export async function createGoogleAntigravityAuthPlugin({
|
export async function createGoogleAntigravityAuthPlugin({
|
||||||
client,
|
client,
|
||||||
}: PluginInput): Promise<{ auth: AuthHook }> {
|
}: PluginInput): Promise<{ auth: AuthHook }> {
|
||||||
|
// Cache for custom credentials from provider.options
|
||||||
|
// These are populated by loader() and used by authorize()
|
||||||
|
// Falls back to defaults if loader hasn't been called yet
|
||||||
|
let cachedClientId: string = ANTIGRAVITY_CLIENT_ID
|
||||||
|
let cachedClientSecret: string = ANTIGRAVITY_CLIENT_SECRET
|
||||||
|
|
||||||
const authHook: AuthHook = {
|
const authHook: AuthHook = {
|
||||||
/**
|
/**
|
||||||
* Provider identifier - must be "google" as Antigravity is
|
* Provider identifier - must be "google" as Antigravity is
|
||||||
@@ -100,18 +106,16 @@ export async function createGoogleAntigravityAuthPlugin({
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read credentials from provider.options (opencode.json)
|
cachedClientId =
|
||||||
// Fall back to default credentials if not configured
|
|
||||||
const clientId =
|
|
||||||
(provider.options?.clientId as string) || ANTIGRAVITY_CLIENT_ID
|
(provider.options?.clientId as string) || ANTIGRAVITY_CLIENT_ID
|
||||||
const clientSecret =
|
cachedClientSecret =
|
||||||
(provider.options?.clientSecret as string) || ANTIGRAVITY_CLIENT_SECRET
|
(provider.options?.clientSecret as string) || ANTIGRAVITY_CLIENT_SECRET
|
||||||
|
|
||||||
// Log if using custom credentials (for debugging)
|
// Log if using custom credentials (for debugging)
|
||||||
if (
|
if (
|
||||||
process.env.ANTIGRAVITY_DEBUG === "1" &&
|
process.env.ANTIGRAVITY_DEBUG === "1" &&
|
||||||
(clientId !== ANTIGRAVITY_CLIENT_ID ||
|
(cachedClientId !== ANTIGRAVITY_CLIENT_ID ||
|
||||||
clientSecret !== ANTIGRAVITY_CLIENT_SECRET)
|
cachedClientSecret !== ANTIGRAVITY_CLIENT_SECRET)
|
||||||
) {
|
) {
|
||||||
console.log(
|
console.log(
|
||||||
"[antigravity-plugin] Using custom credentials from provider.options"
|
"[antigravity-plugin] Using custom credentials from provider.options"
|
||||||
@@ -153,13 +157,12 @@ export async function createGoogleAntigravityAuthPlugin({
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the Antigravity fetch interceptor
|
|
||||||
// Note: The fetch interceptor uses constants for token refresh internally
|
|
||||||
// Custom credentials in provider.options are for future extensibility
|
|
||||||
const antigravityFetch = createAntigravityFetch(
|
const antigravityFetch = createAntigravityFetch(
|
||||||
getAuth,
|
getAuth,
|
||||||
authClient,
|
authClient,
|
||||||
GOOGLE_PROVIDER_ID
|
GOOGLE_PROVIDER_ID,
|
||||||
|
cachedClientId,
|
||||||
|
cachedClientSecret
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -185,8 +188,7 @@ export async function createGoogleAntigravityAuthPlugin({
|
|||||||
* @returns Authorization result with URL and callback
|
* @returns Authorization result with URL and callback
|
||||||
*/
|
*/
|
||||||
authorize: async (): Promise<AuthOuathResult> => {
|
authorize: async (): Promise<AuthOuathResult> => {
|
||||||
// Build OAuth URL with PKCE
|
const { url, verifier } = await buildAuthURL(undefined, cachedClientId)
|
||||||
const { url, verifier } = await buildAuthURL()
|
|
||||||
|
|
||||||
// Start local callback server to receive OAuth callback
|
// Start local callback server to receive OAuth callback
|
||||||
const callbackPromise = startCallbackServer()
|
const callbackPromise = startCallbackServer()
|
||||||
@@ -208,28 +210,33 @@ export async function createGoogleAntigravityAuthPlugin({
|
|||||||
|
|
||||||
// Check for errors
|
// Check for errors
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
|
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||||
console.error(
|
console.error(
|
||||||
`[antigravity-plugin] OAuth error: ${result.error}`
|
`[antigravity-plugin] OAuth error: ${result.error}`
|
||||||
)
|
)
|
||||||
|
}
|
||||||
return { type: "failed" as const }
|
return { type: "failed" as const }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.code) {
|
if (!result.code) {
|
||||||
|
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||||
console.error(
|
console.error(
|
||||||
"[antigravity-plugin] No authorization code received"
|
"[antigravity-plugin] No authorization code received"
|
||||||
)
|
)
|
||||||
|
}
|
||||||
return { type: "failed" as const }
|
return { type: "failed" as const }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify state and extract original verifier
|
// Verify state and extract original verifier
|
||||||
const state = decodeState(result.state)
|
const state = decodeState(result.state)
|
||||||
if (state.verifier !== verifier) {
|
if (state.verifier !== verifier) {
|
||||||
|
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||||
console.error("[antigravity-plugin] PKCE verifier mismatch")
|
console.error("[antigravity-plugin] PKCE verifier mismatch")
|
||||||
|
}
|
||||||
return { type: "failed" as const }
|
return { type: "failed" as const }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exchange authorization code for tokens
|
const tokens = await exchangeCode(result.code, verifier, cachedClientId, cachedClientSecret)
|
||||||
const tokens = await exchangeCode(result.code, verifier)
|
|
||||||
|
|
||||||
// Fetch user info (optional, for logging)
|
// Fetch user info (optional, for logging)
|
||||||
try {
|
try {
|
||||||
@@ -263,11 +270,13 @@ export async function createGoogleAntigravityAuthPlugin({
|
|||||||
expires: Date.now() + tokens.expires_in * 1000,
|
expires: Date.now() + tokens.expires_in * 1000,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||||
console.error(
|
console.error(
|
||||||
`[antigravity-plugin] OAuth flow failed: ${
|
`[antigravity-plugin] OAuth flow failed: ${
|
||||||
error instanceof Error ? error.message : "Unknown error"
|
error instanceof Error ? error.message : "Unknown error"
|
||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
|
}
|
||||||
return { type: "failed" as const }
|
return { type: "failed" as const }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*
|
*
|
||||||
* Key responsibilities:
|
* Key responsibilities:
|
||||||
* - Non-streaming response transformation
|
* - Non-streaming response transformation
|
||||||
* - SSE streaming response transformation (preserving stream)
|
* - SSE streaming response transformation (buffered - see transformStreamingResponse)
|
||||||
* - Error response handling with retry-after extraction
|
* - Error response handling with retry-after extraction
|
||||||
* - Usage metadata extraction from x-antigravity-* headers
|
* - Usage metadata extraction from x-antigravity-* headers
|
||||||
*/
|
*/
|
||||||
@@ -340,19 +340,29 @@ export function transformStreamingPayload(payload: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform a streaming SSE response
|
* Transforms a streaming SSE response from Antigravity to OpenAI format.
|
||||||
*
|
*
|
||||||
* For streaming responses:
|
* **⚠️ CURRENT IMPLEMENTATION: BUFFERING**
|
||||||
* - Preserves the SSE format for downstream consumers
|
* This implementation reads the entire stream into memory before transforming.
|
||||||
|
* While functional, it does not preserve true streaming characteristics:
|
||||||
|
* - Blocks until entire response is received
|
||||||
|
* - Consumes memory proportional to response size
|
||||||
|
* - Increases Time-To-First-Byte (TTFB)
|
||||||
|
*
|
||||||
|
* **TODO: Future Enhancement**
|
||||||
|
* Implement true streaming using ReadableStream transformation:
|
||||||
|
* - Parse SSE chunks incrementally
|
||||||
|
* - Transform and yield chunks as they arrive
|
||||||
|
* - Reduce memory footprint and TTFB
|
||||||
|
*
|
||||||
|
* For streaming responses (current buffered approach):
|
||||||
* - Unwraps the `response` field from each SSE event
|
* - Unwraps the `response` field from each SSE event
|
||||||
|
* - Returns transformed SSE text as new Response
|
||||||
* - Extracts usage metadata from headers
|
* - Extracts usage metadata from headers
|
||||||
*
|
*
|
||||||
* Note: This reads the entire stream and returns a new Response.
|
|
||||||
* The stream is preserved as SSE text, not blocked.
|
|
||||||
*
|
|
||||||
* Note: Does NOT handle thinking block extraction (Task 10)
|
* Note: Does NOT handle thinking block extraction (Task 10)
|
||||||
*
|
*
|
||||||
* @param response - Fetch Response object with SSE body
|
* @param response - The SSE response from Antigravity API
|
||||||
* @returns TransformResult with transformed response and metadata
|
* @returns TransformResult with transformed response and metadata
|
||||||
*/
|
*/
|
||||||
export async function transformStreamingResponse(response: Response): Promise<TransformResult> {
|
export async function transformStreamingResponse(response: Response): Promise<TransformResult> {
|
||||||
@@ -425,6 +435,7 @@ export async function transformStreamingResponse(response: Response): Promise<Tr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle SSE stream
|
// Handle SSE stream
|
||||||
|
// NOTE: Current implementation buffers entire stream - see JSDoc for details
|
||||||
try {
|
try {
|
||||||
const text = await response.text()
|
const text = await response.text()
|
||||||
const transformed = transformStreamingPayload(text)
|
const transformed = transformStreamingPayload(text)
|
||||||
|
|||||||
@@ -36,16 +36,20 @@ export function isTokenExpired(tokens: AntigravityTokens): boolean {
|
|||||||
* Exchanges the refresh token for a new access token via Google's OAuth endpoint.
|
* Exchanges the refresh token for a new access token via Google's OAuth endpoint.
|
||||||
*
|
*
|
||||||
* @param refreshToken - The refresh token to use
|
* @param refreshToken - The refresh token to use
|
||||||
|
* @param clientId - Optional custom client ID (defaults to ANTIGRAVITY_CLIENT_ID)
|
||||||
|
* @param clientSecret - Optional custom client secret (defaults to ANTIGRAVITY_CLIENT_SECRET)
|
||||||
* @returns Token exchange result with new access token, or throws on error
|
* @returns Token exchange result with new access token, or throws on error
|
||||||
*/
|
*/
|
||||||
export async function refreshAccessToken(
|
export async function refreshAccessToken(
|
||||||
refreshToken: string
|
refreshToken: string,
|
||||||
|
clientId: string = ANTIGRAVITY_CLIENT_ID,
|
||||||
|
clientSecret: string = ANTIGRAVITY_CLIENT_SECRET
|
||||||
): Promise<AntigravityTokenExchangeResult> {
|
): Promise<AntigravityTokenExchangeResult> {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
client_id: ANTIGRAVITY_CLIENT_ID,
|
client_id: clientId,
|
||||||
client_secret: ANTIGRAVITY_CLIENT_SECRET,
|
client_secret: clientSecret,
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = await fetch(GOOGLE_TOKEN_URL, {
|
const response = await fetch(GOOGLE_TOKEN_URL, {
|
||||||
|
|||||||
@@ -117,13 +117,15 @@ export function normalizeToolsForGemini(
|
|||||||
|
|
||||||
functionDeclarations.push(declaration)
|
functionDeclarations.push(declaration)
|
||||||
} else {
|
} else {
|
||||||
// Log warning for unsupported tool types
|
// Log warning for unsupported tool types (debug only)
|
||||||
|
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[antigravity-tools] Unsupported tool type: "${tool.type}". ` +
|
`[antigravity-tools] Unsupported tool type: "${tool.type}". ` +
|
||||||
`Only "function" type tools are supported for Gemini. Tool will be skipped.`
|
`Only "function" type tools are supported for Gemini. Tool will be skipped.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return undefined if no valid function declarations
|
// Return undefined if no valid function declarations
|
||||||
if (functionDeclarations.length === 0) {
|
if (functionDeclarations.length === 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user