diff --git a/open-sse/executors/vertex.js b/open-sse/executors/vertex.js index 47d7e893..4cea2ad8 100644 --- a/open-sse/executors/vertex.js +++ b/open-sse/executors/vertex.js @@ -1,11 +1,33 @@ import { BaseExecutor } from "./base.js"; import { PROVIDERS } from "../config/providers.js"; -import { parseVertexSaJson, refreshVertexToken } from "../services/tokenRefresh.js"; +import { parseVertexSaJson, refreshVertexToken, refreshGoogleToken } from "../services/tokenRefresh.js"; import { proxyAwareFetch } from "../utils/proxyFetch.js"; // Cache project IDs resolved from raw API keys { apiKey → projectId } const projectIdCache = new Map(); +/** + * Parse Google ADC user credential JSON from apiKey string. + * This is the format produced by `gcloud auth application-default login`. + */ +function parseVertexAdcJson(apiKey) { + if (typeof apiKey !== "string") return null; + try { + const parsed = JSON.parse(apiKey); + if ( + parsed.type === "authorized_user" && + parsed.client_id && + parsed.client_secret && + parsed.refresh_token + ) { + return parsed; + } + return null; + } catch { + return null; + } +} + /** * Resolve GCP project ID from a raw Vertex API key. * Sends a dummy 404 request and parses "projects/{id}" from the error message. @@ -43,8 +65,13 @@ export class VertexExecutor extends BaseExecutor { buildUrl(model, stream, urlIndex = 0, credentials = null) { const saJson = parseVertexSaJson(credentials?.apiKey); - const rawKey = !saJson ? credentials?.apiKey : null; - const projectId = saJson?.project_id || credentials?.providerSpecificData?.projectId; + const adcJson = parseVertexAdcJson(credentials?.apiKey); + const usesOAuth = !!saJson || !!adcJson || !!credentials?.accessToken; + const rawKey = !usesOAuth ? credentials?.apiKey : null; + const projectId = + saJson?.project_id || + adcJson?.quota_project_id || + credentials?.providerSpecificData?.projectId; if (this.provider === "vertex-partner") { // Partner models require project_id in path regardless of auth method @@ -56,8 +83,14 @@ export class VertexExecutor extends BaseExecutor { // Gemini on Vertex const action = stream ? "streamGenerateContent" : "generateContent"; - if (saJson) { - // SA JSON + Bearer token: must use project-scoped path to avoid RESOURCE_PROJECT_INVALID + if (usesOAuth) { + // SA JSON / ADC / pre-set accessToken: must use project-scoped path to avoid RESOURCE_PROJECT_INVALID + if (!projectId) { + throw new Error( + "Vertex OAuth/ADC requires a project_id. " + + "Add quota_project_id to your ADC JSON or set providerSpecificData.projectId." + ); + } const location = credentials?.providerSpecificData?.location || "us-central1"; let url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${model}:${action}`; if (stream) url += "?alt=sse"; @@ -97,16 +130,29 @@ export class VertexExecutor extends BaseExecutor { async execute({ model, body, stream, credentials, signal, log, proxyOptions = null }) { const saJson = parseVertexSaJson(credentials?.apiKey); + const adcJson = parseVertexAdcJson(credentials?.apiKey); - // SA JSON flow: mint Bearer token (cached) + // SA JSON flow: mint Bearer token via JWT assertion (cached) if (saJson) { const result = await refreshVertexToken(saJson, log); if (!result?.accessToken) throw new Error("Vertex: failed to mint access token from Service Account JSON"); credentials.accessToken = result.accessToken; } + // ADC user credential flow: refresh Bearer token via Google OAuth2 token endpoint + if (adcJson) { + const result = await refreshGoogleToken( + adcJson.refresh_token, + adcJson.client_id, + adcJson.client_secret, + log + ); + if (!result?.accessToken) throw new Error("Vertex: failed to refresh access token from ADC JSON (authorized_user)"); + credentials.accessToken = result.accessToken; + } + // vertex-partner with raw key: auto-resolve project_id if not provided - if (this.provider === "vertex-partner" && !saJson && !credentials?.providerSpecificData?.projectId) { + if (this.provider === "vertex-partner" && !saJson && !adcJson && !credentials?.providerSpecificData?.projectId) { const projectId = await resolveProjectId(credentials.apiKey); if (!projectId) throw new Error("Vertex: could not resolve project_id from API key. Please add it manually in provider settings."); log?.debug?.("VERTEX", `Resolved project_id: ${projectId}`);