@fl-penly/vertex-claude 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FL-Penly
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # openclaw-vertex-claude
2
+
3
+ [OpenClaw](https://github.com/openclaw/openclaw) plugin that adds Claude model support via [Google Cloud Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude).
4
+
5
+ Use Claude Sonnet, Opus, and other Anthropic models through your own GCP project — no direct Anthropic API key required.
6
+
7
+ ## How it works
8
+
9
+ OpenClaw's built-in `google-vertex` provider only supports Gemini models. This plugin bridges the gap by:
10
+
11
+ 1. Registering a `vertex-claude` provider with `api: "anthropic-messages"`
12
+ 2. Running a lightweight local proxy (`127.0.0.1:18832`) that translates Anthropic Messages API requests into Vertex AI `rawPredict`/`streamRawPredict` calls
13
+ 3. Handling GCP authentication via Application Default Credentials (ADC)
14
+
15
+ ```
16
+ OpenClaw → anthropic-messages runtime → local proxy → Vertex AI → Claude
17
+ ```
18
+
19
+ ## Prerequisites
20
+
21
+ - [OpenClaw](https://github.com/openclaw/openclaw) `>= 2026.2.2`
22
+ - A GCP project with the Vertex AI API enabled
23
+ - Claude model access enabled in your GCP project ([request access](https://console.cloud.google.com/vertex-ai/publishers/anthropic))
24
+ - `gcloud` CLI installed and authenticated
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ openclaw plugins install @fl-penly/vertex-claude
30
+ ```
31
+
32
+ ## Setup
33
+
34
+ ### 1. Authenticate with GCP
35
+
36
+ ```bash
37
+ gcloud auth application-default login
38
+ ```
39
+
40
+ ### 2. Set environment variables
41
+
42
+ Add these to your shell profile (`.zshrc`, `.bashrc`, etc.):
43
+
44
+ ```bash
45
+ export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
46
+ export VERTEX_LOCATION="us-east5" # optional, defaults to us-east5
47
+ ```
48
+
49
+ ### 3. Restart OpenClaw gateway
50
+
51
+ ```bash
52
+ openclaw gateway restart
53
+ ```
54
+
55
+ ### 4. Authenticate the plugin
56
+
57
+ ```bash
58
+ openclaw auth login vertex-claude
59
+ ```
60
+
61
+ ### 5. Verify
62
+
63
+ ```bash
64
+ openclaw models list | grep vertex-claude
65
+ ```
66
+
67
+ You should see:
68
+
69
+ ```
70
+ vertex-claude/claude-sonnet-4-5 text+image 195k yes yes configured
71
+ vertex-claude/claude-opus-4-5 text+image 195k yes yes configured
72
+ vertex-claude/claude-opus-4-6 text+image 195k yes yes configured
73
+ ...
74
+ ```
75
+
76
+ ## Usage
77
+
78
+ Switch to a Vertex Claude model:
79
+
80
+ ```
81
+ /model vertex-claude/claude-sonnet-4-5
82
+ ```
83
+
84
+ Or set up aliases in `openclaw.json`:
85
+
86
+ ```json
87
+ {
88
+ "agents": {
89
+ "defaults": {
90
+ "models": {
91
+ "vertex-claude/claude-sonnet-4-5": { "alias": "vc-sonnet" },
92
+ "vertex-claude/claude-opus-4-5": { "alias": "vc-opus" },
93
+ "vertex-claude/claude-opus-4-6": { "alias": "vc-opus46" }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ Then switch with `/model vc-sonnet`.
101
+
102
+ ## Available models
103
+
104
+ | Model ID | Description | Reasoning |
105
+ |----------|-------------|-----------|
106
+ | `claude-sonnet-4-5` | Claude Sonnet 4.5 | No |
107
+ | `claude-sonnet-4-5-thinking` | Claude Sonnet 4.5 with extended thinking | Yes |
108
+ | `claude-opus-4-5` | Claude Opus 4.5 | No |
109
+ | `claude-opus-4-5-thinking` | Claude Opus 4.5 with extended thinking | Yes |
110
+ | `claude-opus-4-6` | Claude Opus 4.6 | Yes |
111
+
112
+ ## Configuration
113
+
114
+ ### Environment variables
115
+
116
+ | Variable | Required | Default | Description |
117
+ |----------|----------|---------|-------------|
118
+ | `GOOGLE_CLOUD_PROJECT` | Yes | — | GCP project ID |
119
+ | `VERTEX_LOCATION` | No | `us-east5` | Vertex AI region |
120
+ | `VERTEX_CLAUDE_PORT` | No | `18832` | Local proxy port |
121
+
122
+ ### Vertex AI regions with Claude support
123
+
124
+ Not all GCP regions support Claude models. Check the [Anthropic on Vertex AI docs](https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions) for the latest availability. Common regions:
125
+
126
+ - `us-east5`
127
+ - `us-central1`
128
+ - `europe-west1`
129
+
130
+ ## Troubleshooting
131
+
132
+ ### "GOOGLE_CLOUD_PROJECT is not set"
133
+
134
+ Export the environment variable before starting OpenClaw:
135
+
136
+ ```bash
137
+ export GOOGLE_CLOUD_PROJECT="your-project-id"
138
+ openclaw gateway restart
139
+ ```
140
+
141
+ ### "Failed to get GCP access token"
142
+
143
+ Re-authenticate:
144
+
145
+ ```bash
146
+ gcloud auth application-default login
147
+ ```
148
+
149
+ ### Models show `auth: missing`
150
+
151
+ Run `openclaw auth login vertex-claude` to set up credentials.
152
+
153
+ ### Proxy not starting (port conflict)
154
+
155
+ Change the proxy port:
156
+
157
+ ```bash
158
+ export VERTEX_CLAUDE_PORT=18833
159
+ openclaw gateway restart
160
+ ```
161
+
162
+ ### 403 from Vertex AI
163
+
164
+ Ensure Claude models are enabled in your GCP project. Visit [Vertex AI Model Garden](https://console.cloud.google.com/vertex-ai/publishers/anthropic) and enable the models you want to use.
165
+
166
+ ## AI-assisted setup
167
+
168
+ Have an AI assistant set this up for you — just share the setup guide:
169
+
170
+ ```
171
+ https://github.com/FL-Penly/vertex-claude/blob/main/SETUP.md
172
+ ```
173
+
174
+ Paste the URL into Claude Code, OpenClaw, or any AI coding assistant. It will follow the steps automatically.
175
+
176
+ ## Development
177
+
178
+ ```bash
179
+ git clone https://github.com/FL-Penly/vertex-claude.git
180
+ cd vertex-claude
181
+ npm install
182
+
183
+ # Link for local development
184
+ ln -s "$(pwd)" ~/.openclaw/extensions/vertex-claude
185
+
186
+ # Enable the plugin
187
+ openclaw plugins enable vertex-claude
188
+ openclaw gateway restart
189
+ ```
190
+
191
+ ## License
192
+
193
+ MIT
package/SETUP.md ADDED
@@ -0,0 +1,164 @@
1
+ # AI Setup Guide — openclaw-vertex-claude
2
+
3
+ > This document is designed for AI coding assistants (Claude Code, OpenClaw, etc.) to follow automatically. If you are an AI assistant, execute the steps below in order. If any step fails, stop and report the error.
4
+
5
+ ## What this plugin does
6
+
7
+ Adds Claude model support (Sonnet, Opus) to OpenClaw via Google Cloud Vertex AI. It runs a local proxy that translates Anthropic Messages API calls into Vertex AI rawPredict requests.
8
+
9
+ ## Step 1: Check prerequisites
10
+
11
+ Run these checks. ALL must pass before proceeding:
12
+
13
+ ```bash
14
+ # OpenClaw must be installed (>= 2026.2.2)
15
+ openclaw --version
16
+
17
+ # gcloud CLI must be installed
18
+ gcloud --version
19
+
20
+ # Node.js >= 20
21
+ node --version
22
+
23
+ # GCP project must be set
24
+ echo $GOOGLE_CLOUD_PROJECT
25
+ ```
26
+
27
+ If `GOOGLE_CLOUD_PROJECT` is empty, ask the user for their GCP project ID, then:
28
+
29
+ ```bash
30
+ # Add to shell profile (~/.zshrc or ~/.bashrc)
31
+ echo 'export GOOGLE_CLOUD_PROJECT="<project-id>"' >> ~/.zshrc
32
+ source ~/.zshrc
33
+ ```
34
+
35
+ Optionally set `VERTEX_LOCATION` (defaults to `us-east5`):
36
+
37
+ ```bash
38
+ echo 'export VERTEX_LOCATION="us-east5"' >> ~/.zshrc
39
+ source ~/.zshrc
40
+ ```
41
+
42
+ ## Step 2: Ensure GCP authentication
43
+
44
+ ```bash
45
+ gcloud auth application-default login
46
+ ```
47
+
48
+ If already authenticated, this can be skipped. Verify with:
49
+
50
+ ```bash
51
+ gcloud auth application-default print-access-token > /dev/null 2>&1 && echo "OK" || echo "NEED LOGIN"
52
+ ```
53
+
54
+ ## Step 3: Install the plugin
55
+
56
+ ```bash
57
+ openclaw plugins install @fl-penly/vertex-claude
58
+ ```
59
+
60
+ If install fails (e.g. not yet published to npm), install from source:
61
+
62
+ ```bash
63
+ git clone https://github.com/FL-Penly/vertex-claude.git ~/.openclaw/extensions/vertex-claude
64
+ cd ~/.openclaw/extensions/vertex-claude
65
+ npm install --production
66
+ ```
67
+
68
+ Then enable manually by adding to `~/.openclaw/openclaw.json` under `plugins.entries`:
69
+
70
+ ```json
71
+ {
72
+ "plugins": {
73
+ "entries": {
74
+ "vertex-claude": {
75
+ "enabled": true
76
+ }
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ ## Step 4: Configure model aliases (optional but recommended)
83
+
84
+ Add model aliases to `~/.openclaw/openclaw.json` under `agents.defaults.models`:
85
+
86
+ ```json
87
+ {
88
+ "agents": {
89
+ "defaults": {
90
+ "models": {
91
+ "vertex-claude/claude-sonnet-4-5": { "alias": "vc-sonnet" },
92
+ "vertex-claude/claude-opus-4-5": { "alias": "vc-opus" },
93
+ "vertex-claude/claude-opus-4-6": { "alias": "vc-opus46" }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ IMPORTANT: Merge these into the existing JSON structure. Do NOT overwrite the file. Use `jq` or read-modify-write.
101
+
102
+ ## Step 5: Restart the gateway
103
+
104
+ ```bash
105
+ openclaw gateway restart
106
+ ```
107
+
108
+ ## Step 6: Verify installation
109
+
110
+ ```bash
111
+ # Check models are registered
112
+ openclaw models list | grep vertex-claude
113
+
114
+ # Expected output (3+ lines with "yes yes configured"):
115
+ # vertex-claude/claude-sonnet-4-5 text+image 195k yes yes configured
116
+ # vertex-claude/claude-opus-4-5 text+image 195k yes yes configured
117
+ # vertex-claude/claude-opus-4-6 text+image 195k yes yes configured
118
+
119
+ # Check proxy is running
120
+ curl -s http://127.0.0.1:18832/health
121
+ # Expected: OK
122
+ ```
123
+
124
+ If models show `auth: missing`, run:
125
+
126
+ ```bash
127
+ openclaw auth login vertex-claude
128
+ ```
129
+
130
+ ## Step 7: Test end-to-end (optional)
131
+
132
+ ```bash
133
+ curl -s http://127.0.0.1:18832/v1/messages \
134
+ -H "Content-Type: application/json" \
135
+ -H "X-Api-Key: vertex-proxy-local" \
136
+ -d '{
137
+ "model": "claude-sonnet-4-5",
138
+ "max_tokens": 64,
139
+ "messages": [{"role": "user", "content": "Say hi in one word"}]
140
+ }' | head -c 200
141
+ ```
142
+
143
+ Expected: A JSON response with `"type": "message"` and a content block.
144
+
145
+ ## Troubleshooting
146
+
147
+ | Symptom | Fix |
148
+ |---------|-----|
149
+ | `GOOGLE_CLOUD_PROJECT is not set` | `export GOOGLE_CLOUD_PROJECT=your-project-id` then restart gateway |
150
+ | `Failed to get GCP access token` | `gcloud auth application-default login` |
151
+ | Models show `auth: missing` | `openclaw auth login vertex-claude` |
152
+ | Port 18832 in use | `export VERTEX_CLAUDE_PORT=18833` then restart gateway |
153
+ | 403 from Vertex AI | Enable Claude models at https://console.cloud.google.com/vertex-ai/publishers/anthropic |
154
+ | Plugin not loading | Check `~/.openclaw/logs/gateway.err.log` for errors |
155
+
156
+ ## Available models after setup
157
+
158
+ | Alias | Full ID | Use for |
159
+ |-------|---------|---------|
160
+ | `vc-sonnet` | `vertex-claude/claude-sonnet-4-5` | General coding, fast |
161
+ | `vc-opus` | `vertex-claude/claude-opus-4-5` | Complex reasoning |
162
+ | `vc-opus46` | `vertex-claude/claude-opus-4-6` | Latest, extended thinking |
163
+
164
+ Switch model: `/model vc-sonnet`
package/index.ts ADDED
@@ -0,0 +1,153 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
+ import type { ProxyHandle } from "./proxy.js";
4
+
5
+ const DEFAULT_PORT = 18832;
6
+
7
+ function resolvePort(): number {
8
+ return Number(process.env.VERTEX_CLAUDE_PORT) || DEFAULT_PORT;
9
+ }
10
+
11
+ const CLAUDE_MODELS = [
12
+ {
13
+ id: "claude-sonnet-4-5",
14
+ name: "Claude Sonnet 4.5 (Vertex)",
15
+ reasoning: false,
16
+ input: ["text", "image"] as string[],
17
+ cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
18
+ contextWindow: 200000,
19
+ maxTokens: 8192,
20
+ },
21
+ {
22
+ id: "claude-sonnet-4-5-thinking",
23
+ name: "Claude Sonnet 4.5 Thinking (Vertex)",
24
+ reasoning: true,
25
+ input: ["text", "image"] as string[],
26
+ cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
27
+ contextWindow: 200000,
28
+ maxTokens: 16000,
29
+ },
30
+ {
31
+ id: "claude-opus-4-5",
32
+ name: "Claude Opus 4.5 (Vertex)",
33
+ reasoning: false,
34
+ input: ["text", "image"] as string[],
35
+ cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
36
+ contextWindow: 200000,
37
+ maxTokens: 8192,
38
+ },
39
+ {
40
+ id: "claude-opus-4-5-thinking",
41
+ name: "Claude Opus 4.5 Thinking (Vertex)",
42
+ reasoning: true,
43
+ input: ["text", "image"] as string[],
44
+ cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
45
+ contextWindow: 200000,
46
+ maxTokens: 32000,
47
+ },
48
+ {
49
+ id: "claude-opus-4-6",
50
+ name: "Claude Opus 4.6 (Vertex)",
51
+ reasoning: true,
52
+ input: ["text", "image"] as string[],
53
+ cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
54
+ contextWindow: 200000,
55
+ maxTokens: 32000,
56
+ },
57
+ ];
58
+
59
+ let proxyHandle: ProxyHandle | null = null;
60
+
61
+ const plugin = {
62
+ id: "vertex-claude",
63
+ name: "Vertex Claude",
64
+ description: "Claude models via Google Vertex AI",
65
+ configSchema: emptyPluginConfigSchema(),
66
+
67
+ register(api: OpenClawPluginApi) {
68
+ const port = resolvePort();
69
+ const baseUrl = "http://127.0.0.1:" + port;
70
+
71
+ api.registerProvider({
72
+ id: "vertex-claude",
73
+ label: "Vertex AI Claude",
74
+ aliases: ["vc"],
75
+ envVars: ["GOOGLE_CLOUD_PROJECT", "VERTEX_LOCATION"],
76
+ models: {
77
+ baseUrl,
78
+ apiKey: "vertex-proxy-local",
79
+ api: "anthropic-messages",
80
+ models: CLAUDE_MODELS,
81
+ },
82
+ auth: [
83
+ {
84
+ id: "gcp-adc",
85
+ label: "GCP Application Default Credentials",
86
+ hint: "Uses gcloud ADC — run: gcloud auth application-default login",
87
+ kind: "custom" as const,
88
+ run: async () => {
89
+ const project = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
90
+ if (!project) {
91
+ throw new Error(
92
+ "GOOGLE_CLOUD_PROJECT is not set. Export it before starting OpenClaw:\n" +
93
+ " export GOOGLE_CLOUD_PROJECT=your-project-id",
94
+ );
95
+ }
96
+
97
+ const { GoogleAuth } = await import("google-auth-library");
98
+ const gauth = new GoogleAuth({ scopes: ["https://www.googleapis.com/auth/cloud-platform"] });
99
+ const token = await gauth.getAccessToken();
100
+ if (!token) {
101
+ throw new Error(
102
+ "Failed to get GCP access token. Run:\n gcloud auth application-default login",
103
+ );
104
+ }
105
+
106
+ const location = process.env.VERTEX_LOCATION || process.env.GOOGLE_CLOUD_LOCATION || "us-east5";
107
+
108
+ return {
109
+ profiles: [
110
+ {
111
+ profileId: "vertex-claude:gcp-adc",
112
+ credential: {
113
+ type: "api_key" as const,
114
+ provider: "vertex-claude",
115
+ key: "vertex-proxy-local",
116
+ },
117
+ },
118
+ ],
119
+ defaultModel: "vertex-claude/claude-sonnet-4-5",
120
+ notes: [
121
+ "Using GCP project: " + project,
122
+ "Vertex location: " + location,
123
+ "Auth: Application Default Credentials",
124
+ ],
125
+ };
126
+ },
127
+ },
128
+ ],
129
+ });
130
+
131
+ api.registerService({
132
+ id: "vertex-claude-proxy",
133
+ start: async (ctx) => {
134
+ const { startProxy } = await import("./proxy.js");
135
+ proxyHandle = await startProxy({
136
+ port,
137
+ logger: {
138
+ info: (msg: string) => ctx.logger.info(msg),
139
+ error: (msg: string) => ctx.logger.error(msg),
140
+ },
141
+ });
142
+ },
143
+ stop: async () => {
144
+ if (proxyHandle) {
145
+ await proxyHandle.stop();
146
+ proxyHandle = null;
147
+ }
148
+ },
149
+ });
150
+ },
151
+ };
152
+
153
+ export default plugin;
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "vertex-claude",
3
+ "providers": ["vertex-claude"],
4
+ "configSchema": {
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {}
8
+ }
9
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@fl-penly/vertex-claude",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "OpenClaw plugin: use Claude models via Google Cloud Vertex AI",
6
+ "license": "MIT",
7
+ "author": "FL-Penly",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/FL-Penly/vertex-claude.git"
11
+ },
12
+ "homepage": "https://github.com/FL-Penly/vertex-claude#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/FL-Penly/vertex-claude/issues"
15
+ },
16
+ "keywords": [
17
+ "openclaw",
18
+ "claude",
19
+ "vertex-ai",
20
+ "google-cloud",
21
+ "anthropic",
22
+ "ai",
23
+ "llm",
24
+ "plugin"
25
+ ],
26
+ "files": [
27
+ "index.ts",
28
+ "proxy.ts",
29
+ "openclaw.plugin.json",
30
+ "LICENSE",
31
+ "README.md",
32
+ "SETUP.md"
33
+ ],
34
+ "scripts": {
35
+ "test": "vitest run"
36
+ },
37
+ "openclaw": {
38
+ "extensions": [
39
+ "./index.ts"
40
+ ]
41
+ },
42
+ "dependencies": {
43
+ "google-auth-library": "^9.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.0.0",
47
+ "openclaw": ">=2026.2.2",
48
+ "vitest": "^3.0.0"
49
+ },
50
+ "peerDependencies": {
51
+ "openclaw": ">=2026.2.2"
52
+ },
53
+ "engines": {
54
+ "node": ">=20.0.0"
55
+ }
56
+ }
package/proxy.ts ADDED
@@ -0,0 +1,213 @@
1
+ import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
2
+ import { GoogleAuth } from "google-auth-library";
3
+
4
+ export interface ProxyLogger {
5
+ info(msg: string): void;
6
+ error(msg: string): void;
7
+ }
8
+
9
+ export interface ProxyHandle {
10
+ server: Server;
11
+ port: number;
12
+ stop: () => Promise<void>;
13
+ }
14
+
15
+ const DEFAULT_PORT = 18832;
16
+ const DEFAULT_LOCATION = "us-east5";
17
+ const REQUEST_TIMEOUT_MS = 300_000; // 5 minutes — long for streaming responses
18
+ const MAX_REQUEST_BODY_BYTES = 10 * 1024 * 1024; // 10 MB
19
+
20
+ function resolveEnv() {
21
+ const project = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || "";
22
+ const location = process.env.VERTEX_LOCATION || process.env.GOOGLE_CLOUD_LOCATION || DEFAULT_LOCATION;
23
+ return { project, location };
24
+ }
25
+
26
+ function jsonError(res: ServerResponse, status: number, message: string) {
27
+ const body = JSON.stringify({ type: "error", error: { type: "proxy_error", message } });
28
+ res.writeHead(status, { "Content-Type": "application/json" });
29
+ res.end(body);
30
+ }
31
+
32
+ async function readBody(req: IncomingMessage): Promise<Buffer> {
33
+ const chunks: Buffer[] = [];
34
+ let totalBytes = 0;
35
+ for await (const chunk of req) {
36
+ totalBytes += (chunk as Buffer).length;
37
+ if (totalBytes > MAX_REQUEST_BODY_BYTES) {
38
+ throw new Error(`Request body exceeds maximum size of ${MAX_REQUEST_BODY_BYTES} bytes`);
39
+ }
40
+ chunks.push(chunk as Buffer);
41
+ }
42
+ return Buffer.concat(chunks);
43
+ }
44
+
45
+ export async function startProxy(opts: {
46
+ port?: number;
47
+ logger?: ProxyLogger;
48
+ }): Promise<ProxyHandle> {
49
+ const port = opts.port || (Number(process.env.VERTEX_CLAUDE_PORT) || DEFAULT_PORT);
50
+ const log = opts.logger || { info: console.log, error: console.error };
51
+
52
+ const auth = new GoogleAuth({ scopes: ["https://www.googleapis.com/auth/cloud-platform"] });
53
+
54
+ async function getAccessToken(retries: number = 1): Promise<string> {
55
+ let lastErr: Error | undefined;
56
+ for (let i = 0; i <= retries; i++) {
57
+ try {
58
+ const token = await auth.getAccessToken();
59
+ if (token) return token;
60
+ } catch (err) {
61
+ lastErr = err instanceof Error ? err : new Error(String(err));
62
+ if (i < retries) {
63
+ log.info(`[vertex-claude] Token fetch failed, retrying (${i + 1}/${retries})...`);
64
+ }
65
+ }
66
+ }
67
+ throw lastErr || new Error("Failed to get GCP access token. Run: gcloud auth application-default login");
68
+ }
69
+
70
+ const server = createServer(async (req, res) => {
71
+ if (req.method === "GET" && req.url === "/health") {
72
+ res.writeHead(200, { "Content-Type": "text/plain" });
73
+ res.end("OK");
74
+ return;
75
+ }
76
+
77
+ if (req.method !== "POST" || req.url !== "/v1/messages") {
78
+ jsonError(res, 404, "Not found: " + req.method + " " + req.url);
79
+ return;
80
+ }
81
+
82
+ try {
83
+ const { project, location } = resolveEnv();
84
+ if (!project) {
85
+ jsonError(res, 500, "GOOGLE_CLOUD_PROJECT env var is required. Export it before starting OpenClaw.");
86
+ return;
87
+ }
88
+
89
+ const rawBody = await readBody(req);
90
+ let body: Record<string, unknown>;
91
+ try {
92
+ body = JSON.parse(rawBody.toString("utf-8"));
93
+ } catch (_parseErr) {
94
+ jsonError(res, 400, "Invalid JSON body");
95
+ return;
96
+ }
97
+
98
+ const modelId = body.model as string;
99
+ if (!modelId) {
100
+ jsonError(res, 400, "Missing 'model' field in request body");
101
+ return;
102
+ }
103
+
104
+ const isStream = body.stream === true;
105
+
106
+ // Vertex AI requires: model in URL path, anthropic_version in body (not header)
107
+ const endpoint = isStream ? "streamRawPredict" : "rawPredict";
108
+ const vertexUrl =
109
+ "https://" + location + "-aiplatform.googleapis.com/v1/projects/" + project +
110
+ "/locations/" + location + "/publishers/anthropic/models/" + modelId + ":" + endpoint;
111
+
112
+ const vertexBody: Record<string, unknown> = {
113
+ ...body,
114
+ anthropic_version: "vertex-2023-10-16",
115
+ };
116
+ delete vertexBody.model;
117
+ const vertexPayload = JSON.stringify(vertexBody);
118
+
119
+ const accessToken = await getAccessToken(1);
120
+
121
+ const controller = new AbortController();
122
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
123
+
124
+ let vertexRes: Response;
125
+ try {
126
+ vertexRes = await fetch(vertexUrl, {
127
+ method: "POST",
128
+ headers: {
129
+ "Authorization": "Bearer " + accessToken,
130
+ "Content-Type": "application/json",
131
+ },
132
+ body: vertexPayload,
133
+ signal: controller.signal,
134
+ });
135
+ } catch (fetchErr) {
136
+ clearTimeout(timeout);
137
+ const msg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
138
+ if (msg.includes("aborted")) {
139
+ jsonError(res, 504, "Upstream request to Vertex AI timed out after " + (REQUEST_TIMEOUT_MS / 1000) + "s");
140
+ } else {
141
+ jsonError(res, 502, "Failed to connect to Vertex AI: " + msg);
142
+ }
143
+ return;
144
+ }
145
+
146
+ if (!vertexRes.ok) {
147
+ clearTimeout(timeout);
148
+ const errBody = await vertexRes.text();
149
+ log.error("[vertex-claude] Vertex returned " + vertexRes.status + ": " + errBody.slice(0, 500));
150
+ const contentType = vertexRes.headers.get("content-type") || "application/json";
151
+ res.writeHead(vertexRes.status, { "Content-Type": contentType });
152
+ res.end(errBody);
153
+ return;
154
+ }
155
+
156
+ const contentType = vertexRes.headers.get("content-type") || "application/json";
157
+ res.writeHead(vertexRes.status, { "Content-Type": contentType });
158
+
159
+ if (!vertexRes.body) {
160
+ clearTimeout(timeout);
161
+ const text = await vertexRes.text();
162
+ res.end(text);
163
+ return;
164
+ }
165
+
166
+ const reader = (vertexRes.body as ReadableStream<Uint8Array>).getReader();
167
+ try {
168
+ while (true) {
169
+ const { done, value } = await reader.read();
170
+ if (done) break;
171
+ res.write(value);
172
+ }
173
+ } catch (streamErr) {
174
+ const msg = streamErr instanceof Error ? streamErr.message : String(streamErr);
175
+ log.error("[vertex-claude] Stream error: " + msg);
176
+ } finally {
177
+ clearTimeout(timeout);
178
+ res.end();
179
+ }
180
+ } catch (err) {
181
+ const msg = err instanceof Error ? err.message : String(err);
182
+ log.error("[vertex-claude] Proxy error: " + msg);
183
+ if (!res.headersSent) {
184
+ jsonError(res, 500, msg);
185
+ } else {
186
+ res.end();
187
+ }
188
+ }
189
+ });
190
+
191
+ await new Promise<void>((resolve, reject) => {
192
+ const onError = (err: Error) => {
193
+ server.off("error", onError);
194
+ reject(err);
195
+ };
196
+ server.once("error", onError);
197
+ server.listen(port, "127.0.0.1", () => {
198
+ server.off("error", onError);
199
+ log.info("[vertex-claude] Proxy listening on http://127.0.0.1:" + port);
200
+ resolve();
201
+ });
202
+ });
203
+
204
+ const stop = () =>
205
+ new Promise<void>((resolve) => {
206
+ server.close(() => {
207
+ log.info("[vertex-claude] Proxy stopped");
208
+ resolve();
209
+ });
210
+ });
211
+
212
+ return { server, port, stop };
213
+ }