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 { BaseExecutor } from "./base.js";
|
||||||
import { PROVIDERS } from "../config/providers.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";
|
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
||||||
|
|
||||||
// Cache project IDs resolved from raw API keys { apiKey → projectId }
|
// Cache project IDs resolved from raw API keys { apiKey → projectId }
|
||||||
const projectIdCache = new Map();
|
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.
|
* Resolve GCP project ID from a raw Vertex API key.
|
||||||
* Sends a dummy 404 request and parses "projects/{id}" from the error message.
|
* 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) {
|
buildUrl(model, stream, urlIndex = 0, credentials = null) {
|
||||||
const saJson = parseVertexSaJson(credentials?.apiKey);
|
const saJson = parseVertexSaJson(credentials?.apiKey);
|
||||||
const rawKey = !saJson ? credentials?.apiKey : null;
|
const adcJson = parseVertexAdcJson(credentials?.apiKey);
|
||||||
const projectId = saJson?.project_id || credentials?.providerSpecificData?.projectId;
|
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") {
|
if (this.provider === "vertex-partner") {
|
||||||
// Partner models require project_id in path regardless of auth method
|
// Partner models require project_id in path regardless of auth method
|
||||||
@@ -56,8 +83,14 @@ export class VertexExecutor extends BaseExecutor {
|
|||||||
// Gemini on Vertex
|
// Gemini on Vertex
|
||||||
const action = stream ? "streamGenerateContent" : "generateContent";
|
const action = stream ? "streamGenerateContent" : "generateContent";
|
||||||
|
|
||||||
if (saJson) {
|
if (usesOAuth) {
|
||||||
// SA JSON + Bearer token: must use project-scoped path to avoid RESOURCE_PROJECT_INVALID
|
// 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";
|
const location = credentials?.providerSpecificData?.location || "us-central1";
|
||||||
let url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${model}:${action}`;
|
let url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${model}:${action}`;
|
||||||
if (stream) url += "?alt=sse";
|
if (stream) url += "?alt=sse";
|
||||||
@@ -97,16 +130,29 @@ export class VertexExecutor extends BaseExecutor {
|
|||||||
|
|
||||||
async execute({ model, body, stream, credentials, signal, log, proxyOptions = null }) {
|
async execute({ model, body, stream, credentials, signal, log, proxyOptions = null }) {
|
||||||
const saJson = parseVertexSaJson(credentials?.apiKey);
|
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) {
|
if (saJson) {
|
||||||
const result = await refreshVertexToken(saJson, log);
|
const result = await refreshVertexToken(saJson, log);
|
||||||
if (!result?.accessToken) throw new Error("Vertex: failed to mint access token from Service Account JSON");
|
if (!result?.accessToken) throw new Error("Vertex: failed to mint access token from Service Account JSON");
|
||||||
credentials.accessToken = result.accessToken;
|
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
|
// 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);
|
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.");
|
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}`);
|
log?.debug?.("VERTEX", `Resolved project_id: ${projectId}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user