@serii84/vertex-partner-provider 1.0.32 → 1.1.1
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/index.js +79 -82
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Vertex Partner Provider
|
|
3
|
-
*
|
|
2
|
+
* Vertex Partner Provider
|
|
3
|
+
* Enables Vertex AI partner models (GLM, Kimi, DeepSeek, MiniMax, Qwen) with AI SDK
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
|
-
const
|
|
7
|
+
const { createOpenAICompatible } = require('@ai-sdk/openai-compatible');
|
|
8
|
+
const { GoogleAuth } = require('google-auth-library');
|
|
8
9
|
|
|
10
|
+
// Optional debug logging
|
|
9
11
|
const DEBUG = process.env.VERTEX_DEBUG === 'true';
|
|
10
12
|
const DEBUG_FILE = process.env.VERTEX_DEBUG_FILE || '/tmp/vertex-debug.log';
|
|
11
13
|
|
|
@@ -16,9 +18,7 @@ function debugLog(...args) {
|
|
|
16
18
|
fs.appendFileSync(DEBUG_FILE, `[${timestamp}] ${message}\n`);
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
const { GoogleAuth } = require('google-auth-library');
|
|
21
|
-
|
|
21
|
+
// Auth client singleton
|
|
22
22
|
let authClient = null;
|
|
23
23
|
|
|
24
24
|
async function getAuthToken(googleAuthOptions) {
|
|
@@ -33,8 +33,11 @@ async function getAuthToken(googleAuthOptions) {
|
|
|
33
33
|
return token.token;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Clean response to standard OpenAI format
|
|
38
|
+
* Removes vendor-specific fields and normalizes structure
|
|
39
|
+
*/
|
|
36
40
|
function cleanResponse(parsed) {
|
|
37
|
-
// Build a clean response with only standard OpenAI fields
|
|
38
41
|
const cleaned = {
|
|
39
42
|
id: parsed.id,
|
|
40
43
|
object: parsed.object,
|
|
@@ -44,11 +47,9 @@ function cleanResponse(parsed) {
|
|
|
44
47
|
|
|
45
48
|
if (parsed.choices) {
|
|
46
49
|
cleaned.choices = parsed.choices.map(choice => {
|
|
47
|
-
const cleanChoice = {
|
|
48
|
-
index: choice.index,
|
|
49
|
-
};
|
|
50
|
+
const cleanChoice = { index: choice.index };
|
|
50
51
|
|
|
51
|
-
//
|
|
52
|
+
// Handle finish_reason (normalize if object, ensure string)
|
|
52
53
|
if (choice.finish_reason != null) {
|
|
53
54
|
if (typeof choice.finish_reason === 'object') {
|
|
54
55
|
cleanChoice.finish_reason = choice.finish_reason.type
|
|
@@ -60,23 +61,26 @@ function cleanResponse(parsed) {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
//
|
|
64
|
+
// Keep logprobs if present (standard OpenAI field)
|
|
65
|
+
if (choice.logprobs != null) cleanChoice.logprobs = choice.logprobs;
|
|
66
|
+
|
|
67
|
+
// Clean delta for streaming responses
|
|
64
68
|
if (choice.delta) {
|
|
65
69
|
const cleanDelta = {};
|
|
66
70
|
if (choice.delta.role) cleanDelta.role = choice.delta.role;
|
|
67
71
|
if (choice.delta.content) cleanDelta.content = choice.delta.content;
|
|
68
|
-
|
|
72
|
+
// Preserve reasoning_content for AI SDK's native reasoning support
|
|
73
|
+
if (choice.delta.reasoning_content) cleanDelta.reasoning_content = choice.delta.reasoning_content;
|
|
69
74
|
if (choice.delta.tool_calls) cleanDelta.tool_calls = choice.delta.tool_calls;
|
|
70
75
|
cleanChoice.delta = cleanDelta;
|
|
71
|
-
} else {
|
|
72
|
-
// Always include delta, even if empty (required by OpenAI format)
|
|
73
|
-
cleanChoice.delta = {};
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
// Clean message for non-streaming responses
|
|
76
79
|
if (choice.message) {
|
|
77
80
|
const cleanMessage = { role: choice.message.role };
|
|
78
81
|
if (choice.message.content) cleanMessage.content = choice.message.content;
|
|
79
|
-
|
|
82
|
+
// Preserve reasoning_content for AI SDK's native reasoning support
|
|
83
|
+
if (choice.message.reasoning_content) cleanMessage.reasoning_content = choice.message.reasoning_content;
|
|
80
84
|
if (choice.message.tool_calls) cleanMessage.tool_calls = choice.message.tool_calls;
|
|
81
85
|
cleanChoice.message = cleanMessage;
|
|
82
86
|
}
|
|
@@ -85,7 +89,7 @@ function cleanResponse(parsed) {
|
|
|
85
89
|
});
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
// Clean usage - only
|
|
92
|
+
// Clean usage - only standard fields
|
|
89
93
|
if (parsed.usage) {
|
|
90
94
|
const { prompt_tokens, completion_tokens, total_tokens } = parsed.usage;
|
|
91
95
|
if (prompt_tokens != null || completion_tokens != null || total_tokens != null) {
|
|
@@ -96,84 +100,78 @@ function cleanResponse(parsed) {
|
|
|
96
100
|
return cleaned;
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Transform SSE stream to clean format
|
|
105
|
+
*/
|
|
99
106
|
function transformStream(response) {
|
|
100
|
-
// Log response headers for debugging
|
|
101
107
|
debugLog('Response headers:', Object.fromEntries(response.headers.entries()));
|
|
102
108
|
|
|
103
109
|
const reader = response.body.getReader();
|
|
104
110
|
const decoder = new TextDecoder();
|
|
105
111
|
const encoder = new TextEncoder();
|
|
106
|
-
|
|
107
112
|
let buffer = '';
|
|
108
|
-
|
|
113
|
+
|
|
109
114
|
const transformedStream = new ReadableStream({
|
|
110
115
|
async pull(controller) {
|
|
111
116
|
try {
|
|
112
117
|
const { done, value } = await reader.read();
|
|
113
|
-
|
|
118
|
+
|
|
114
119
|
if (done) {
|
|
115
|
-
// Process
|
|
116
|
-
if (buffer.trim()) {
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (cleaned.choices && cleaned.choices.length > 0) {
|
|
127
|
-
controller.enqueue(encoder.encode('data: ' + JSON.stringify(cleaned) + '\n\n'));
|
|
128
|
-
}
|
|
129
|
-
} catch (e) {
|
|
130
|
-
debugLog('Final buffer parse error:', e.message);
|
|
120
|
+
// Process remaining buffer
|
|
121
|
+
if (buffer.trim() && buffer.startsWith('data: ')) {
|
|
122
|
+
const data = buffer.slice(6).trim();
|
|
123
|
+
if (data === '[DONE]') {
|
|
124
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
125
|
+
} else if (data) {
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(data);
|
|
128
|
+
const cleaned = cleanResponse(parsed);
|
|
129
|
+
if (cleaned.choices?.length > 0) {
|
|
130
|
+
controller.enqueue(encoder.encode('data: ' + JSON.stringify(cleaned) + '\n\n'));
|
|
131
131
|
}
|
|
132
|
+
} catch (e) {
|
|
133
|
+
debugLog('Final buffer parse error:', e.message);
|
|
132
134
|
}
|
|
133
135
|
}
|
|
134
136
|
}
|
|
135
137
|
controller.close();
|
|
136
138
|
return;
|
|
137
139
|
}
|
|
138
|
-
|
|
140
|
+
|
|
139
141
|
buffer += decoder.decode(value, { stream: true });
|
|
140
|
-
|
|
142
|
+
|
|
141
143
|
let boundary;
|
|
142
144
|
while ((boundary = buffer.indexOf('\n\n')) !== -1) {
|
|
143
145
|
const message = buffer.slice(0, boundary);
|
|
144
146
|
buffer = buffer.slice(boundary + 2);
|
|
145
|
-
|
|
147
|
+
|
|
146
148
|
if (!message.trim()) continue;
|
|
147
|
-
|
|
149
|
+
|
|
148
150
|
if (message.startsWith('data: ')) {
|
|
149
151
|
const data = message.slice(6);
|
|
150
|
-
|
|
152
|
+
|
|
151
153
|
if (data === '[DONE]') {
|
|
152
154
|
debugLog('Forwarding [DONE]');
|
|
153
155
|
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
154
156
|
continue;
|
|
155
157
|
}
|
|
156
|
-
|
|
158
|
+
|
|
157
159
|
try {
|
|
158
160
|
const parsed = JSON.parse(data);
|
|
159
|
-
|
|
160
161
|
debugLog('Raw chunk:', parsed);
|
|
161
162
|
|
|
162
163
|
const cleaned = cleanResponse(parsed);
|
|
163
164
|
|
|
164
|
-
// Skip chunks with
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
debugLog('Skipping chunk with empty choices');
|
|
165
|
+
// Skip chunks with no useful data (no choices AND no usage)
|
|
166
|
+
if (!cleaned.choices?.length && !cleaned.usage) {
|
|
167
|
+
debugLog('Skipping empty chunk');
|
|
168
168
|
continue;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
debugLog('Cleaned chunk:', cleaned);
|
|
172
|
-
|
|
173
172
|
controller.enqueue(encoder.encode('data: ' + JSON.stringify(cleaned) + '\n\n'));
|
|
174
173
|
} catch (e) {
|
|
175
|
-
debugLog('JSON parse error:', e.message
|
|
176
|
-
// Skip invalid JSON
|
|
174
|
+
debugLog('JSON parse error:', e.message);
|
|
177
175
|
}
|
|
178
176
|
}
|
|
179
177
|
}
|
|
@@ -181,12 +179,12 @@ function transformStream(response) {
|
|
|
181
179
|
controller.error(err);
|
|
182
180
|
}
|
|
183
181
|
},
|
|
184
|
-
|
|
182
|
+
|
|
185
183
|
cancel() {
|
|
186
184
|
reader.cancel();
|
|
187
185
|
}
|
|
188
186
|
});
|
|
189
|
-
|
|
187
|
+
|
|
190
188
|
return new Response(transformedStream, {
|
|
191
189
|
headers: response.headers,
|
|
192
190
|
status: response.status,
|
|
@@ -194,19 +192,17 @@ function transformStream(response) {
|
|
|
194
192
|
});
|
|
195
193
|
}
|
|
196
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Transform non-streaming response to clean format
|
|
197
|
+
*/
|
|
197
198
|
async function transformNonStreamingResponse(response) {
|
|
198
199
|
const text = await response.text();
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (!text || text.trim() === '') {
|
|
200
|
+
|
|
201
|
+
if (!text?.trim()) {
|
|
202
202
|
return new Response(JSON.stringify({
|
|
203
203
|
id: 'empty',
|
|
204
204
|
object: 'chat.completion',
|
|
205
|
-
choices: [{
|
|
206
|
-
index: 0,
|
|
207
|
-
message: { role: 'assistant', content: '' },
|
|
208
|
-
finish_reason: 'stop'
|
|
209
|
-
}],
|
|
205
|
+
choices: [{ index: 0, message: { role: 'assistant', content: '' }, finish_reason: 'stop' }],
|
|
210
206
|
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
211
207
|
}), {
|
|
212
208
|
headers: { 'content-type': 'application/json' },
|
|
@@ -214,18 +210,16 @@ async function transformNonStreamingResponse(response) {
|
|
|
214
210
|
statusText: response.statusText,
|
|
215
211
|
});
|
|
216
212
|
}
|
|
217
|
-
|
|
213
|
+
|
|
218
214
|
try {
|
|
219
215
|
const data = JSON.parse(text);
|
|
220
216
|
const cleaned = cleanResponse(data);
|
|
221
|
-
|
|
222
217
|
return new Response(JSON.stringify(cleaned), {
|
|
223
218
|
headers: { 'content-type': 'application/json' },
|
|
224
219
|
status: response.status,
|
|
225
220
|
statusText: response.statusText,
|
|
226
221
|
});
|
|
227
222
|
} catch (e) {
|
|
228
|
-
// If JSON parse fails, return original text as-is
|
|
229
223
|
return new Response(text, {
|
|
230
224
|
headers: response.headers,
|
|
231
225
|
status: response.status,
|
|
@@ -234,6 +228,14 @@ async function transformNonStreamingResponse(response) {
|
|
|
234
228
|
}
|
|
235
229
|
}
|
|
236
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Create a Vertex AI partner model provider
|
|
233
|
+
* @param {Object} options
|
|
234
|
+
* @param {string} options.project - GCP project ID (or GOOGLE_VERTEX_PROJECT env var)
|
|
235
|
+
* @param {string} options.location - GCP region (default: 'global')
|
|
236
|
+
* @param {string} options.publisher - Model publisher (e.g., 'zai-org', 'moonshotai', 'deepseek-ai')
|
|
237
|
+
* @param {Object} options.googleAuthOptions - Additional GoogleAuth options
|
|
238
|
+
*/
|
|
237
239
|
function createVertexPartner(options = {}) {
|
|
238
240
|
const {
|
|
239
241
|
project = process.env.GOOGLE_VERTEX_PROJECT,
|
|
@@ -245,17 +247,17 @@ function createVertexPartner(options = {}) {
|
|
|
245
247
|
if (!project) throw new Error('project is required');
|
|
246
248
|
if (!publisher) throw new Error('publisher is required');
|
|
247
249
|
|
|
248
|
-
const baseHost = location === 'global'
|
|
249
|
-
? 'aiplatform.googleapis.com'
|
|
250
|
+
const baseHost = location === 'global'
|
|
251
|
+
? 'aiplatform.googleapis.com'
|
|
250
252
|
: `${location}-aiplatform.googleapis.com`;
|
|
251
|
-
|
|
253
|
+
|
|
252
254
|
const baseURL = `https://${baseHost}/v1/projects/${project}/locations/${location}/endpoints/openapi`;
|
|
253
255
|
|
|
254
256
|
const authFetch = async (url, init) => {
|
|
255
257
|
const token = await getAuthToken(googleAuthOptions);
|
|
256
258
|
const headers = new Headers(init?.headers);
|
|
257
259
|
headers.set('Authorization', `Bearer ${token}`);
|
|
258
|
-
|
|
260
|
+
|
|
259
261
|
let isStreaming = false;
|
|
260
262
|
if (init?.body) {
|
|
261
263
|
try {
|
|
@@ -263,27 +265,22 @@ function createVertexPartner(options = {}) {
|
|
|
263
265
|
isStreaming = body.stream === true;
|
|
264
266
|
} catch (e) {}
|
|
265
267
|
}
|
|
266
|
-
|
|
268
|
+
|
|
267
269
|
const response = await fetch(url, { ...init, headers });
|
|
268
|
-
|
|
269
|
-
if (!response.ok)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (isStreaming) {
|
|
275
|
-
return transformStream(response);
|
|
276
|
-
} else {
|
|
277
|
-
return transformNonStreamingResponse(response);
|
|
278
|
-
}
|
|
270
|
+
|
|
271
|
+
if (!response.ok) return response;
|
|
272
|
+
|
|
273
|
+
return isStreaming ? transformStream(response) : transformNonStreamingResponse(response);
|
|
279
274
|
};
|
|
280
275
|
|
|
281
276
|
const provider = createOpenAICompatible({
|
|
282
277
|
name: `vertex-${publisher}`,
|
|
283
278
|
baseURL,
|
|
284
279
|
fetch: authFetch,
|
|
280
|
+
includeUsage: true,
|
|
285
281
|
});
|
|
286
282
|
|
|
283
|
+
// Wrap provider to auto-prefix model IDs with publisher
|
|
287
284
|
const wrappedProvider = (modelId) => {
|
|
288
285
|
const fullModelId = modelId.includes('/') ? modelId : `${publisher}/${modelId}`;
|
|
289
286
|
return provider(fullModelId);
|
|
@@ -300,4 +297,4 @@ function createVertexPartner(options = {}) {
|
|
|
300
297
|
return wrappedProvider;
|
|
301
298
|
}
|
|
302
299
|
|
|
303
|
-
module.exports = { createVertexPartner };
|
|
300
|
+
module.exports = { createVertexPartner };
|