mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 20:21:39 +03:00
fix(provider): support CLOUDSDK_CONFIG for Google Vertex ADC (#54164)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
9f282f8c2c
commit
c9c5985e9d
@@ -1,4 +1,4 @@
|
||||
import type { AnyAuthClient } from "google-auth-library"
|
||||
import type { AnyAuthClient, GoogleAuthOptions } from "google-auth-library"
|
||||
import { Effect, Redacted } from "effect"
|
||||
import { Auth, MissingCredentialError } from "../route/auth.js"
|
||||
import { ProviderConfigurationError, ProviderID } from "../schema/index.js"
|
||||
@@ -6,6 +6,31 @@ import { ProviderConfigurationError, ProviderID } from "../schema/index.js"
|
||||
const SCOPE = "https://www.googleapis.com/auth/cloud-platform"
|
||||
const id = ProviderID.make("google-vertex")
|
||||
|
||||
export const loadADCClient = async (project?: string) => {
|
||||
const { GoogleAuth } = await import("google-auth-library")
|
||||
// google-auth-library 10.5.0 ignores CLOUDSDK_CONFIG during ADC file discovery.
|
||||
// Fix this in shipped AI code: a repository-level Bun dependency patch would not
|
||||
// reach npm or Bun consumers of the published @opencode/ai package.
|
||||
// Override only the well-known-file lookup: keyFilename bypasses ADC's quota-project
|
||||
// preparation. Keep explicit credentials and metadata fallback in Google's ADC chain.
|
||||
class CloudSDKAuth extends GoogleAuth {
|
||||
override async _tryGetApplicationCredentialsFromWellKnownFile(options?: GoogleAuthOptions["clientOptions"]) {
|
||||
const config = process.env.CLOUDSDK_CONFIG
|
||||
if (!config) return super._tryGetApplicationCredentialsFromWellKnownFile(options)
|
||||
// Keep local filesystem helpers lazy for explicitly authenticated Worker callers;
|
||||
// this ADC-file lookup itself requires a Node-compatible filesystem.
|
||||
const { existsSync } = await import("node:fs")
|
||||
const { homedir } = await import("node:os")
|
||||
const { join } = await import("node:path")
|
||||
const directory = config === "~" ? homedir() : config.startsWith("~/") ? join(homedir(), config.slice(2)) : config
|
||||
const file = join(directory, "application_default_credentials.json")
|
||||
if (!existsSync(file)) return null
|
||||
return this._getApplicationCredentialsFromFilePath(file, options)
|
||||
}
|
||||
}
|
||||
return new CloudSDKAuth({ projectId: project, scopes: [SCOPE] }).getClient()
|
||||
}
|
||||
|
||||
export type OAuthOptions =
|
||||
| { readonly accessToken?: string; readonly auth?: never }
|
||||
| { readonly accessToken?: never; readonly auth?: Auth.Definition }
|
||||
@@ -61,9 +86,7 @@ const adc = (project?: string) => {
|
||||
let client: Promise<AnyAuthClient> | undefined
|
||||
const loadClient = () => {
|
||||
if (client) return client
|
||||
client = import("google-auth-library").then(({ GoogleAuth }) =>
|
||||
new GoogleAuth({ projectId: project, scopes: [SCOPE] }).getClient(),
|
||||
)
|
||||
client = loadADCClient(project)
|
||||
return client
|
||||
}
|
||||
return Auth.effect(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { mkdtemp, rm } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { LanguageModel, LLM, Message, ToolCallPart } from "../../src/index.js"
|
||||
import { GoogleVertex, GoogleVertexChat, GoogleVertexMessages, GoogleVertexResponses } from "../../src/providers.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
@@ -11,6 +14,57 @@ import { deltaChunk, finishChunk } from "../lib/openai-chunks.js"
|
||||
import { sseEvents } from "../lib/sse.js"
|
||||
|
||||
describe("Google Vertex providers", () => {
|
||||
test("loads CLOUDSDK_CONFIG credentials and preserves quota-project overrides", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "vertex-adc-"))
|
||||
try {
|
||||
await Bun.write(
|
||||
join(directory, "application_default_credentials.json"),
|
||||
JSON.stringify({
|
||||
type: "authorized_user",
|
||||
client_id: "fixture-client",
|
||||
client_secret: "fixture-secret",
|
||||
refresh_token: "fixture-refresh-token",
|
||||
quota_project_id: "file-quota",
|
||||
}),
|
||||
)
|
||||
// Isolate Google's environment and credential caches; no token refresh or model call.
|
||||
const child = Bun.spawn(
|
||||
[
|
||||
process.execPath,
|
||||
"-e",
|
||||
`import { GoogleVertexShared } from ${JSON.stringify(join(import.meta.dir, "../../src/providers/google-vertex-shared.ts"))};
|
||||
const client = await GoogleVertexShared.loadADCClient("fixture-project");
|
||||
console.log(JSON.stringify({
|
||||
custom: client.credentials.refresh_token === "fixture-refresh-token",
|
||||
quota: client.quotaProjectId,
|
||||
}));`,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: directory,
|
||||
APPDATA: directory,
|
||||
CLOUDSDK_CONFIG: directory,
|
||||
GOOGLE_APPLICATION_CREDENTIALS: undefined,
|
||||
google_application_credentials: undefined,
|
||||
GOOGLE_CLOUD_QUOTA_PROJECT: "environment-quota",
|
||||
METADATA_SERVER_DETECTION: "none",
|
||||
},
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
},
|
||||
)
|
||||
const result = JSON.parse(await new Response(child.stdout).text())
|
||||
expect({ exitCode: await child.exited, stderr: await new Response(child.stderr).text() }).toEqual({
|
||||
exitCode: 0,
|
||||
stderr: "",
|
||||
})
|
||||
expect(result).toEqual({ custom: true, quota: "environment-quota" })
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it.effect("sends Gemini requests to the global Vertex endpoint", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(
|
||||
|
||||
@@ -58,7 +58,13 @@ export const GoogleVertexPlugin = define({
|
||||
const read = (file: string) => fs.readFileStringSafe(file).pipe(Effect.orElseSucceed(() => undefined))
|
||||
// Same lookup as gcloud itself. Only project IDs are read; nothing here contacts Google.
|
||||
const gcloud =
|
||||
process.env.CLOUDSDK_CONFIG ??
|
||||
(process.env.CLOUDSDK_CONFIG
|
||||
? process.env.CLOUDSDK_CONFIG === "~"
|
||||
? Global.Path.home
|
||||
: process.env.CLOUDSDK_CONFIG.startsWith("~/")
|
||||
? path.join(Global.Path.home, process.env.CLOUDSDK_CONFIG.slice(2))
|
||||
: process.env.CLOUDSDK_CONFIG
|
||||
: undefined) ??
|
||||
(process.platform === "win32" && process.env.APPDATA
|
||||
? path.join(process.env.APPDATA, "gcloud")
|
||||
: path.join(Global.Path.home, ".config", "gcloud"))
|
||||
|
||||
Reference in New Issue
Block a user