feat(vertex): support ADC authorized_user credential

Accept Google Application Default Credentials (authorized_user) in the
Vertex apiKey field as an alternative to Service Account JSON, for orgs
that block SA key creation. Refreshes a Bearer token via the existing
refreshGoogleToken flow and requires a project_id (quota_project_id or
providerSpecificData.projectId). SA JSON and raw key flows unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Duong Thai Hoa Tong
2026-06-13 09:55:29 +07:00
committed by decolua
parent b977bf7414
commit 9406bd1806

View File

@@ -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}`);