UNPKG

kestrel.markets

Version:

A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.

201 lines (183 loc) 11.2 kB
/** * # session/harness/vertex-maas — Vertex Model Garden MaaS plumbing (ADC bearer + the region trap) (kestrel-k8he) * * Vertex AI Model Garden serves the open frontier weights (DeepSeek V3.2, Qwen3-Next-80B, Kimi K2 * Thinking) **as-a-service on GCP CREDITS**, over an OpenAI-compatible chat-completions endpoint. * Live probe (wf_e58255fd-e01, 2026-07-18) verified all three MaaS slugs serve 200 on * * `https://aiplatform.googleapis.com/v1beta1/projects/<project>/locations/global/endpoints/openapi/chat/completions` * * with an **ADC bearer** (`gcloud auth application-default print-access-token`) — NOT an API key. * This module owns everything about that path that does not touch a provider package (so it stays * importable by cost-planning / test code): the base-URL builder, the fail-closed project + location * resolution, and the injectable gcloud-ADC token source. The wire-touching client construction * (`createOpenAICompatible` + the bearer-attaching fetch) lives in `./ai-sdk-client.ts`, the ONE * provider-bearing module. * * ## THE REGION TRAP (pin location=global, fail LOUDLY on anything else) * The same live probe showed `us-central1` rejects EVERY MaaS slug ("Model … is not available in * region"); only `location=global` serves them. A harness configured with a region — an * `opts.region`, a `GOOGLE_VERTEX_LOCATION`/`GOOGLE_CLOUD_LOCATION` in the shell (both common gcloud * conventions) — must therefore FAIL with the clear region error at construction, never silently * "helpfully" rewrite the location to global: a silent rewrite would hide that the caller's config * disagrees with the wire, and the next person to copy that config onto a raw curl would get the * opaque vendor 404 with no breadcrumb. {@link resolveVertexMaasLocation} is that guard. * * ## DEAD ADC: surface the login, never silently skip (the gcloud auto-reauth doctrine) * An ADC bearer expires (~1h) and dies entirely when the grant is revoked. When the refresh * subprocess fails, {@link gcloudAdcTokenSource} throws {@link AdcAuthError} carrying the EXACT * reauth command (`gcloud auth application-default login`) — the documented doctrine is that an * agent RUNS that login (it opens the owner's browser) and waits, and NEVER quietly skips the * Vertex lane. What this code must never do is swallow the failure and let a runner tally the seat * as "no result" as if the lane did not exist. */ import { execFile } from "node:child_process"; /** The ONLY location Vertex Model Garden MaaS serves from (probe wf_e58255fd-e01: us-central1 * rejects all MaaS slugs). Pinned; a configured non-global location throws — see the module doc. */ export const VERTEX_MAAS_LOCATION = "global"; /** The provider `name` given to `createOpenAICompatible` — also the `providerOptions` key an * openai-compatible model reads passthrough options from (same convention as `fireworks`). */ export const VERTEX_MAAS_PROVIDER_NAME = "vertex-maas"; /** The env names a GCP project id may arrive under, in precedence order. `GOOGLE_VERTEX_PROJECT` is * the `@ai-sdk/google-vertex` convention; the other two are gcloud's own. */ export const VERTEX_MAAS_PROJECT_ENV_VARS = ["GOOGLE_VERTEX_PROJECT", "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT"] as const; /** The env names a location/region may leak in from (both gcloud conventions). Read ONLY to fail * loudly on the region trap — never to actually route anywhere but {@link VERTEX_MAAS_LOCATION}. */ export const VERTEX_MAAS_LOCATION_ENV_VARS = ["GOOGLE_VERTEX_LOCATION", "GOOGLE_CLOUD_LOCATION"] as const; /** The GCP project id from the environment (first present, non-trivial value wins), or `undefined`. * A project id is an ADDRESS, not a secret — but it is account-specific, so it is never hardcoded. */ export function vertexMaasProjectFromEnv(env: NodeJS.ProcessEnv = process.env): string | undefined { for (const name of VERTEX_MAAS_PROJECT_ENV_VARS) { const v = env[name]; if (typeof v === "string" && v.trim() !== "") return v.trim(); } return undefined; } /** Fail-closed project resolution: the env project, or a THROW naming the env vars to set. A blank * project would 404 opaquely on the wire (the URL embeds it); this error says what to fix instead. */ export function requireVertexMaasProject(env: NodeJS.ProcessEnv = process.env): string { const project = vertexMaasProjectFromEnv(env); if (project === undefined) { throw new Error( `vertex-maas: no GCP project id — set one of ${VERTEX_MAAS_PROJECT_ENV_VARS.join(" / ")} ` + `(the MaaS endpoint URL embeds the project; refusing to construct a client that can only 404).`, ); } return project; } /** Raised when a vertex-maas client is configured with a non-global location — the REGION TRAP made * loud. Fail-closed: the caller's config disagrees with the only location the wire serves, so the * client refuses to construct rather than silently rewriting the location (see the module doc). */ export class VertexMaasRegionError extends Error { constructor(configured: string, sourceOfTruth: string) { super( `vertex-maas: location "${configured}" (from ${sourceOfTruth}) is not available for Vertex Model Garden MaaS — ` + `regional endpoints (us-central1 included) reject ALL MaaS model slugs with "not available in region" ` + `(live probe wf_e58255fd-e01, 2026-07-18). The ONLY serving location is "${VERTEX_MAAS_LOCATION}". ` + `Drop the region override (opts.region / ${VERTEX_MAAS_LOCATION_ENV_VARS.join(" / ")}) or set it to "${VERTEX_MAAS_LOCATION}" — ` + `refusing to silently rewrite your configured location.`, ); this.name = "VertexMaasRegionError"; } } /** * Resolve the MaaS serving location: always {@link VERTEX_MAAS_LOCATION}, and a THROW * ({@link VertexMaasRegionError}) when the caller CONFIGURED anything else — an explicit * `configured` (the client's `region` option) first, else a location leaked in from the gcloud env * conventions. A `configured`/env value of exactly `"global"` is accepted (it agrees with the pin). */ export function resolveVertexMaasLocation(configured?: string, env: NodeJS.ProcessEnv = process.env): string { if (configured !== undefined && configured.trim() !== "" && configured.trim() !== VERTEX_MAAS_LOCATION) { throw new VertexMaasRegionError(configured.trim(), "the client's region option"); } for (const name of VERTEX_MAAS_LOCATION_ENV_VARS) { const v = env[name]; if (typeof v === "string" && v.trim() !== "" && v.trim() !== VERTEX_MAAS_LOCATION) { throw new VertexMaasRegionError(v.trim(), name); } } return VERTEX_MAAS_LOCATION; } /** The OpenAI-compatible base URL for a project's MaaS endpoint. `createOpenAICompatible` appends * `/chat/completions` — this is everything before it (verified live, probe wf_e58255fd-e01). */ export function vertexMaasBaseUrl(project: string, location: string = VERTEX_MAAS_LOCATION): string { return `https://aiplatform.googleapis.com/v1beta1/projects/${project}/locations/${location}/endpoints/openapi`; } /** One ADC-refresh subprocess result — the injectable seam, same shape discipline as the codex-cli * lane's `CodexExecFn` (tests inject a stand-in; the real one spawns gcloud). */ export interface AdcExecResult { readonly stdout: string; readonly stderr: string; readonly exitCode: number; } /** The injectable subprocess seam: run `cmd args…` and hand back its streams + exit code. */ export type AdcExecFn = (cmd: string, args: readonly string[]) => Promise<AdcExecResult>; /** Raised when the ADC bearer cannot be refreshed — dead/expired Application Default Credentials. * The message carries the exact reauth command; per the gcloud auto-reauth doctrine the agent runs * it (browser login) and waits — the Vertex lane is NEVER silently skipped. */ export class AdcAuthError extends Error { constructor(detail: string) { super( `vertex-maas: gcloud ADC token refresh failed — ${detail}. ` + `Application Default Credentials are dead or absent. Run \`gcloud auth application-default login\` ` + `(opens the browser login; wait for it to complete), then retry. Per the gcloud auto-reauth doctrine ` + `this lane must SURFACE the login and wait — never silently skip the Vertex seat.`, ); this.name = "AdcAuthError"; } } /** How long a fetched ADC bearer is reused before re-running gcloud (tokens live ~60 min; 45 keeps a * comfortable margin so a long fan-out never sends a just-expired bearer). */ export const ADC_TOKEN_TTL_MS = 45 * 60 * 1000; /** A refreshable ADC bearer-token source. `token()` returns a live bearer (cached within * {@link ADC_TOKEN_TTL_MS}); `issued()` lists every token value this source has ever handed out — * the belt-and-suspenders feed into `scrubSecrets`, so an ADC bearer (which lives in NO env var and * would escape the `CREDENTIAL_ENV_VARS` scrub) can still never reach a wire-evidence artifact. */ export interface AdcTokenSource { token(): Promise<string>; issued(): readonly string[]; } /** The REAL subprocess seam: spawn `gcloud auth application-default print-access-token`. */ const realAdcExec: AdcExecFn = (cmd, args) => new Promise((resolve) => { execFile(cmd, [...args], { timeout: 30_000 }, (error, stdout, stderr) => { const code = (error as NodeJS.ErrnoException | null)?.code; const exitCode = error === null ? 0 : typeof code === "number" ? code : 1; resolve({ stdout: String(stdout), stderr: String(stderr), exitCode }); }); }); /** * The gcloud-ADC {@link AdcTokenSource}: refresh via `gcloud auth application-default * print-access-token`, cache for {@link ADC_TOKEN_TTL_MS}, and FAIL LOUDLY ({@link AdcAuthError}, * naming the reauth command) on a dead ADC — never a silent skip. `exec` is the test seam (same * pattern as the codex-cli lane); NEVER set on a production client. */ export function gcloudAdcTokenSource(opts: { readonly exec?: AdcExecFn; readonly now?: () => number } = {}): AdcTokenSource { const exec = opts.exec ?? realAdcExec; const now = opts.now ?? Date.now; const handedOut: string[] = []; let cached: { readonly token: string; readonly at: number } | undefined; return { async token(): Promise<string> { if (cached !== undefined && now() - cached.at < ADC_TOKEN_TTL_MS) return cached.token; let r: AdcExecResult; try { r = await exec("gcloud", ["auth", "application-default", "print-access-token"]); } catch (e) { throw new AdcAuthError(`could not spawn gcloud (${e instanceof Error ? e.message : String(e)})`); } const token = r.stdout.trim(); if (r.exitCode !== 0 || token === "") { throw new AdcAuthError( `exit ${r.exitCode}${r.stderr.trim() !== "" ? `: ${r.stderr.trim()}` : token === "" ? ": empty token" : ""}`, ); } cached = { token, at: now() }; if (!handedOut.includes(token)) handedOut.push(token); return token; }, issued(): readonly string[] { return handedOut; }, }; }