@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 +21 -0
- package/README.md +193 -0
- package/SETUP.md +164 -0
- package/index.ts +153 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +56 -0
- package/proxy.ts +213 -0
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;
|
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
|
+
}
|