Add Google Vertex AI provider (#1163)

# Summary

* Adds first-class **Google Vertex AI provider** using
`@ai-sdk/google-vertex`.
* Supports **Gemini 2.5** models and partner **MaaS (Model Garden)**
models via full publisher IDs.
* New **Vertex-specific settings UI** for Project, Location, and Service
Account JSON.
* Implements a **“thinking” toggle** for Gemini 2.5 Flash

  * Pro: always on
  * Flash: toggleable
  * Flash Lite: none
* Fixes *“AI not found”* for Vertex built-ins by mapping to
`publishers/google` paths.
* Hardens **cross-platform file ops** and ensures all tests pass.

---

# What’s New

### Vertex AI Provider

* Uses `@ai-sdk/google-vertex` with `googleAuthOptions.credentials` from
pasted Service Account JSON.
* Configurable **project** and **location**.
* Base URL → `/projects/{project}/locations/{location}`

  * Built-ins: `publishers/google/models/<id>`
  * Partner MaaS: `publishers/<partner>/models/...`

### Built-in Vertex Models

* `gemini-2.5-pro`
* `gemini-2.5-flash`
* `gemini-2.5-flash-lite`

### Thinking Behavior

* Vertex + Google marked as thinking-capable.
* Pro: always thinking
* Flash: toggle in UI
* Flash Lite: none

### Vertex Settings UI

* New **Google Vertex AI panel** for Project ID, Location, Service
Account JSON.
* Keys encrypted like other secrets.

---

# Fixes

* **Model resolution:** built-ins auto-map to
`publishers/google/models/<id>`.
* **Partner MaaS support:** full publisher IDs work directly (e.g.
DeepSeek).
* **Cross-platform paths:** normalize file ops with `toPosixPath`,
preserve `safeJoin` semantics.

---

# Why This Is Better

* Users can select **Vertex alongside other providers**.
* **More models** available through Model Garden.
* **Dedicated setup UI** reduces misconfig.
* **Thinking toggle** gives control over cost vs. reasoning depth.

---

# Files Changed

* **Provider & Models**: `language_model_helpers.ts`,
`get_model_client.ts`
* **Streaming**: `chat_stream_handlers.ts`
* **Schemas & Encryption**: `schemas.ts`, `settings.ts`
* **Settings UI**: `VertexConfiguration.tsx`, `ApiKeyConfiguration.tsx`
* **Models UI**: `ModelsSection.tsx` (Flash toggle)
* **Setup Detection**: `useLanguageModelProviders.ts`
* **Path Utils**: `path_utils.ts`, `response_processor.ts`
* **Deps**: `package.json` → `@ai-sdk/google-vertex@3.0.16`

---

# Tests & Validation

* **TypeScript**: `npm run ts` → 
* **Lint**: `npm run lint` → 
* **Unit tests**: `npm test` →  231 passed, 0 failed

---

# Migration / Notes

* No breaking changes.
* For Vertex usage:

  * Ensure Vertex AI API is enabled.
  * Service Account needs `roles/aiplatform.user`.
  * Region must support model (e.g. `us-central1`).
* Thinking toggle currently affects **only** Gemini 2.5 Flash.

---

# Manual QA

1. Configure Vertex with Project/Location/Service Account JSON.
2. Test built-ins:

   * `gemini-2.5-pro`
   * `gemini-2.5-flash` (toggle on/off)
   * `gemini-2.5-flash-lite`
3. Test MaaS partner model (e.g., DeepSeek) via full publisher ID.
4. Verify other providers remain unaffected.
    
<!-- This is an auto-generated description by cubic. -->
---

## Summary by cubic
Adds a first-class Google Vertex AI provider with Gemini 2.5 models, a
Vertex settings panel, and a “thinking” toggle for Gemini 2.5 Flash.
Also fixes model resolution for Vertex and hardens cross-platform file
operations.

- **New Features**
- Vertex AI provider via @ai-sdk/google-vertex with project, location,
and service account JSON.
- Built-in models: gemini-2.5-pro, gemini-2.5-flash,
gemini-2.5-flash-lite.
- “Thinking” support: Pro always on; Flash toggle in Models UI; Flash
Lite none.
- MaaS partners supported via full publisher paths (e.g.,
publishers/<partner>/models/...).
  - Vertex settings UI with encrypted service account key storage.

- **Bug Fixes**
  - Built-in Vertex models auto-map to publishers/google/models/<id>.
  - Consistent file ops across platforms using toPosixPath.
- Vertex readiness detection requires project/location/service account
JSON.
- Streaming “thinking” behavior respects Vertex Flash toggle and Pro
always-on.

<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Md Rakibul Islam Rocky <mdrirocky08@gmail.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Co-authored-by: Will Chen <willchen90@gmail.com>
This commit is contained in:
Md Rakibul Islam Rocky
2025-09-09 08:41:12 +03:00
committed by GitHub
parent e962964afe
commit 4db6d63b72
11 changed files with 588 additions and 36 deletions

View File

@@ -7,6 +7,7 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { AzureConfiguration } from "./AzureConfiguration";
import { VertexConfiguration } from "./VertexConfiguration";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { UserSettings } from "@/lib/schemas";
@@ -51,6 +52,10 @@ export function ApiKeyConfiguration({
if (provider === "azure") {
return <AzureConfiguration envVars={envVars} />;
}
// Special handling for Google Vertex AI which uses service account credentials
if (provider === "vertex") {
return <VertexConfiguration />;
}
const envApiKey = envVarName ? envVars[envVarName] : undefined;
const userApiKey = settings?.providerSettings?.[provider]?.apiKey?.value;

View File

@@ -75,8 +75,20 @@ export function ProviderSettingsPage({ provider }: ProviderSettingsPageProps) {
? !!(envVars["AZURE_API_KEY"] && envVars["AZURE_RESOURCE_NAME"])
: false;
// Special handling for Vertex configuration status
const vertexSettings = settings?.providerSettings?.vertex as any;
const isVertexConfigured = Boolean(
vertexSettings?.projectId &&
vertexSettings?.location &&
vertexSettings?.serviceAccountKey?.value,
);
const isConfigured =
provider === "azure" ? isAzureConfigured : isValidUserKey || hasEnvKey; // Configured if either is set
provider === "azure"
? isAzureConfigured
: provider === "vertex"
? isVertexConfigured
: isValidUserKey || hasEnvKey; // Configured if either is set
// --- Save Handler ---
const handleSaveKey = async () => {

View File

@@ -0,0 +1,141 @@
import { useState, useEffect } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Info, CheckCircle2 } from "lucide-react";
import { useSettings } from "@/hooks/useSettings";
import type { UserSettings, VertexProviderSetting } from "@/lib/schemas";
export function VertexConfiguration() {
const { settings, updateSettings } = useSettings();
const existing =
(settings?.providerSettings?.vertex as VertexProviderSetting) ?? {};
const [projectId, setProjectId] = useState(existing.projectId || "");
const [location, setLocation] = useState(existing.location || "");
const [serviceAccountKey, setServiceAccountKey] = useState(
existing.serviceAccountKey?.value || "",
);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [saved, setSaved] = useState(false);
useEffect(() => {
setProjectId(existing.projectId || "");
setLocation(existing.location || "");
setServiceAccountKey(existing.serviceAccountKey?.value || "");
}, [settings?.providerSettings?.vertex]);
const onSave = async () => {
setError(null);
setSaved(false);
try {
// If provided, ensure the service account JSON parses
if (serviceAccountKey) {
JSON.parse(serviceAccountKey);
}
} catch (e: any) {
setError("Service account JSON is invalid: " + e.message);
return;
}
setSaving(true);
try {
const settingsUpdate: Partial<UserSettings> = {
providerSettings: {
...settings?.providerSettings,
vertex: {
...existing,
projectId: projectId.trim() || undefined,
location: location || undefined,
serviceAccountKey: serviceAccountKey
? { value: serviceAccountKey }
: undefined,
},
},
};
await updateSettings(settingsUpdate);
setSaved(true);
} catch (e: any) {
setError(e?.message || "Failed to save Vertex settings");
} finally {
setSaving(false);
}
};
const isConfigured = Boolean(
(projectId.trim() && location && serviceAccountKey) ||
(existing.projectId &&
existing.location &&
existing.serviceAccountKey?.value),
);
return (
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Project ID</label>
<Input
value={projectId}
onChange={(e) => setProjectId(e.target.value)}
placeholder="your-gcp-project-id"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Location</label>
<Input
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="us-central1"
/>
<p className="mt-1 text-xs text-muted-foreground">
If you see a "model not found" error, try a different region. Some
partner models (MaaS) are only available in specific locations
(e.g., us-central1, us-west2).
</p>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Service Account JSON Key
</label>
<Textarea
value={serviceAccountKey}
onChange={(e) => setServiceAccountKey(e.target.value)}
placeholder="Paste the full JSON contents of your service account key here"
className="min-h-40"
/>
</div>
</div>
<div className="flex items-center gap-2">
<Button onClick={onSave} disabled={saving}>
{saving ? "Saving..." : "Save Settings"}
</Button>
{saved && !error && (
<span className="flex items-center text-green-600 text-sm">
<CheckCircle2 className="h-4 w-4 mr-1" /> Saved
</span>
)}
</div>
{!isConfigured && (
<Alert variant="default">
<Info className="h-4 w-4" />
<AlertTitle>Configuration Required</AlertTitle>
<AlertDescription>
Provide Project, Location, and a service account JSON key with
Vertex AI access.
</AlertDescription>
</Alert>
)}
{error && (
<Alert variant="destructive">
<AlertTitle>Save Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
</div>
);
}