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:
committed by
decolua
parent
b977bf7414
commit
9406bd1806
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user