Support Gemini 1.5 Pro from Vertex AI (#1041)
Browse files* fix support for gemini on vertex ai
- use native messages api
- provide full content
- hide "Continue" button
- support "safety settings"
- fix confusion in readme
* respect new lines in model description
* ignore google service accounts matching `gcp-*.json`
* type checks
* copy service account, if exists, to container
* narrow response type
* fix streaming generation
---------
Co-authored-by: Nathan Sarrazin <sarrazin.nathan@gmail.com>
- .gitignore +2 -1
- Dockerfile +1 -0
- README.md +12 -6
- package-lock.json +5 -4
- package.json +1 -1
- src/lib/server/endpoints/google/endpointVertex.ts +108 -43
- src/routes/models/+page.svelte +3 -1
.gitignore
CHANGED
@@ -11,4 +11,5 @@ SECRET_CONFIG
|
|
11 |
.idea
|
12 |
!.env.ci
|
13 |
!.env
|
14 |
-
!.env.template
|
|
|
|
11 |
.idea
|
12 |
!.env.ci
|
13 |
!.env
|
14 |
+
!.env.template
|
15 |
+
gcp-*.json
|
Dockerfile
CHANGED
@@ -37,5 +37,6 @@ ENV HOME=/home/user \
|
|
37 |
COPY --from=builder-production --chown=1000 /app/node_modules /app/node_modules
|
38 |
COPY --link --chown=1000 package.json /app/package.json
|
39 |
COPY --from=builder --chown=1000 /app/build /app/build
|
|
|
40 |
|
41 |
CMD pm2 start /app/build/index.js -i $CPU_CORES --no-daemon
|
|
|
37 |
COPY --from=builder-production --chown=1000 /app/node_modules /app/node_modules
|
38 |
COPY --link --chown=1000 package.json /app/package.json
|
39 |
COPY --from=builder --chown=1000 /app/build /app/build
|
40 |
+
COPY --chown=1000 gcp-*.json /app/
|
41 |
|
42 |
CMD pm2 start /app/build/index.js -i $CPU_CORES --no-daemon
|
README.md
CHANGED
@@ -601,18 +601,24 @@ The service account credentials file can be imported as an environmental variabl
|
|
601 |
GOOGLE_APPLICATION_CREDENTIALS = clientid.json
|
602 |
```
|
603 |
|
604 |
-
Make sure docker has access to the file
|
|
|
605 |
|
606 |
```
|
607 |
MODELS=`[
|
608 |
//...
|
609 |
{
|
610 |
-
"name": "gemini-1.
|
611 |
-
"displayName": "Vertex Gemini Pro 1.
|
612 |
-
"location": "europe-west3",
|
613 |
-
"apiEndpoint": "", //alternative api endpoint url
|
614 |
"endpoints" : [{
|
615 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
616 |
}]
|
617 |
},
|
618 |
]`
|
|
|
601 |
GOOGLE_APPLICATION_CREDENTIALS = clientid.json
|
602 |
```
|
603 |
|
604 |
+
Make sure your docker container has access to the file and the variable is correctly set.
|
605 |
+
Afterwards Google Vertex endpoints can be configured as following:
|
606 |
|
607 |
```
|
608 |
MODELS=`[
|
609 |
//...
|
610 |
{
|
611 |
+
"name": "gemini-1.5-pro",
|
612 |
+
"displayName": "Vertex Gemini Pro 1.5",
|
|
|
|
|
613 |
"endpoints" : [{
|
614 |
+
"type": "vertex",
|
615 |
+
"project": "abc-xyz",
|
616 |
+
"location": "europe-west3",
|
617 |
+
"model": "gemini-1.5-pro-preview-0409", // model-name
|
618 |
+
|
619 |
+
// Optional
|
620 |
+
"safetyThreshold": "BLOCK_MEDIUM_AND_ABOVE",
|
621 |
+
"apiEndpoint": "", // alternative api endpoint url
|
622 |
}]
|
623 |
},
|
624 |
]`
|
package-lock.json
CHANGED
@@ -8,6 +8,7 @@
|
|
8 |
"name": "chat-ui",
|
9 |
"version": "0.8.2",
|
10 |
"dependencies": {
|
|
|
11 |
"@huggingface/hub": "^0.5.1",
|
12 |
"@huggingface/inference": "^2.6.3",
|
13 |
"@iconify-json/bi": "^1.1.21",
|
@@ -72,7 +73,7 @@
|
|
72 |
},
|
73 |
"optionalDependencies": {
|
74 |
"@anthropic-ai/sdk": "^0.17.1",
|
75 |
-
"@google-cloud/vertexai": "^
|
76 |
"aws4fetch": "^1.0.17",
|
77 |
"cohere-ai": "^7.9.0",
|
78 |
"openai": "^4.14.2"
|
@@ -630,9 +631,9 @@
|
|
630 |
}
|
631 |
},
|
632 |
"node_modules/@google-cloud/vertexai": {
|
633 |
-
"version": "
|
634 |
-
"resolved": "https://registry.npmjs.org/@google-cloud/vertexai/-/vertexai-
|
635 |
-
"integrity": "sha512-
|
636 |
"optional": true,
|
637 |
"dependencies": {
|
638 |
"google-auth-library": "^9.1.0"
|
|
|
8 |
"name": "chat-ui",
|
9 |
"version": "0.8.2",
|
10 |
"dependencies": {
|
11 |
+
"@google-cloud/vertexai": "^1.1.0",
|
12 |
"@huggingface/hub": "^0.5.1",
|
13 |
"@huggingface/inference": "^2.6.3",
|
14 |
"@iconify-json/bi": "^1.1.21",
|
|
|
73 |
},
|
74 |
"optionalDependencies": {
|
75 |
"@anthropic-ai/sdk": "^0.17.1",
|
76 |
+
"@google-cloud/vertexai": "^1.1.0",
|
77 |
"aws4fetch": "^1.0.17",
|
78 |
"cohere-ai": "^7.9.0",
|
79 |
"openai": "^4.14.2"
|
|
|
631 |
}
|
632 |
},
|
633 |
"node_modules/@google-cloud/vertexai": {
|
634 |
+
"version": "1.1.0",
|
635 |
+
"resolved": "https://registry.npmjs.org/@google-cloud/vertexai/-/vertexai-1.1.0.tgz",
|
636 |
+
"integrity": "sha512-hfwfdlVpJ+kM6o2b5UFfPnweBcz8tgHAFRswnqUKYqLJsvKU0DDD0Z2/YKoHyAUoPJAv20qg6KlC3msNeUKUiw==",
|
637 |
"optional": true,
|
638 |
"dependencies": {
|
639 |
"google-auth-library": "^9.1.0"
|
package.json
CHANGED
@@ -82,7 +82,7 @@
|
|
82 |
},
|
83 |
"optionalDependencies": {
|
84 |
"@anthropic-ai/sdk": "^0.17.1",
|
85 |
-
"@google-cloud/vertexai": "^
|
86 |
"aws4fetch": "^1.0.17",
|
87 |
"cohere-ai": "^7.9.0",
|
88 |
"openai": "^4.14.2"
|
|
|
82 |
},
|
83 |
"optionalDependencies": {
|
84 |
"@anthropic-ai/sdk": "^0.17.1",
|
85 |
+
"@google-cloud/vertexai": "^1.1.0",
|
86 |
"aws4fetch": "^1.0.17",
|
87 |
"cohere-ai": "^7.9.0",
|
88 |
"openai": "^4.14.2"
|
src/lib/server/endpoints/google/endpointVertex.ts
CHANGED
@@ -1,8 +1,14 @@
|
|
1 |
-
import {
|
2 |
-
|
3 |
-
|
|
|
|
|
|
|
|
|
4 |
import type { Endpoint } from "../endpoints";
|
5 |
import { z } from "zod";
|
|
|
|
|
6 |
|
7 |
export const endpointVertexParametersSchema = z.object({
|
8 |
weight: z.number().int().positive().default(1),
|
@@ -11,10 +17,20 @@ export const endpointVertexParametersSchema = z.object({
|
|
11 |
location: z.string().default("europe-west1"),
|
12 |
project: z.string(),
|
13 |
apiEndpoint: z.string().optional(),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
});
|
15 |
|
16 |
export function endpointVertex(input: z.input<typeof endpointVertexParametersSchema>): Endpoint {
|
17 |
-
const { project, location, model, apiEndpoint } =
|
|
|
18 |
|
19 |
const vertex_ai = new VertexAI({
|
20 |
project,
|
@@ -22,55 +38,104 @@ export function endpointVertex(input: z.input<typeof endpointVertexParametersSch
|
|
22 |
apiEndpoint,
|
23 |
});
|
24 |
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
},
|
32 |
-
|
33 |
-
generation_config: {},
|
34 |
-
});
|
35 |
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
});
|
43 |
|
44 |
-
const
|
45 |
-
|
46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
|
|
|
48 |
return (async function* () {
|
49 |
let generatedText = "";
|
50 |
|
51 |
for await (const data of result.stream) {
|
52 |
-
if (
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
|
70 |
-
|
71 |
-
} else {
|
72 |
-
break;
|
73 |
-
}
|
74 |
}
|
75 |
})();
|
76 |
};
|
|
|
1 |
+
import {
|
2 |
+
VertexAI,
|
3 |
+
HarmCategory,
|
4 |
+
HarmBlockThreshold,
|
5 |
+
type Content,
|
6 |
+
type TextPart,
|
7 |
+
} from "@google-cloud/vertexai";
|
8 |
import type { Endpoint } from "../endpoints";
|
9 |
import { z } from "zod";
|
10 |
+
import type { Message } from "$lib/types/Message";
|
11 |
+
import type { TextGenerationStreamOutput } from "@huggingface/inference";
|
12 |
|
13 |
export const endpointVertexParametersSchema = z.object({
|
14 |
weight: z.number().int().positive().default(1),
|
|
|
17 |
location: z.string().default("europe-west1"),
|
18 |
project: z.string(),
|
19 |
apiEndpoint: z.string().optional(),
|
20 |
+
safetyThreshold: z
|
21 |
+
.enum([
|
22 |
+
HarmBlockThreshold.HARM_BLOCK_THRESHOLD_UNSPECIFIED,
|
23 |
+
HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
|
24 |
+
HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
|
25 |
+
HarmBlockThreshold.BLOCK_NONE,
|
26 |
+
HarmBlockThreshold.BLOCK_ONLY_HIGH,
|
27 |
+
])
|
28 |
+
.optional(),
|
29 |
});
|
30 |
|
31 |
export function endpointVertex(input: z.input<typeof endpointVertexParametersSchema>): Endpoint {
|
32 |
+
const { project, location, model, apiEndpoint, safetyThreshold } =
|
33 |
+
endpointVertexParametersSchema.parse(input);
|
34 |
|
35 |
const vertex_ai = new VertexAI({
|
36 |
project,
|
|
|
38 |
apiEndpoint,
|
39 |
});
|
40 |
|
41 |
+
return async ({ messages, preprompt, generateSettings }) => {
|
42 |
+
const generativeModel = vertex_ai.getGenerativeModel({
|
43 |
+
model: model.id ?? model.name,
|
44 |
+
safetySettings: safetyThreshold
|
45 |
+
? [
|
46 |
+
{
|
47 |
+
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
48 |
+
threshold: safetyThreshold,
|
49 |
+
},
|
50 |
+
{
|
51 |
+
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
|
52 |
+
threshold: safetyThreshold,
|
53 |
+
},
|
54 |
+
{
|
55 |
+
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
56 |
+
threshold: safetyThreshold,
|
57 |
+
},
|
58 |
+
{
|
59 |
+
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
60 |
+
threshold: safetyThreshold,
|
61 |
+
},
|
62 |
+
{
|
63 |
+
category: HarmCategory.HARM_CATEGORY_UNSPECIFIED,
|
64 |
+
threshold: safetyThreshold,
|
65 |
+
},
|
66 |
+
]
|
67 |
+
: undefined,
|
68 |
+
generationConfig: {
|
69 |
+
maxOutputTokens: generateSettings?.max_new_tokens ?? 4096,
|
70 |
+
stopSequences: generateSettings?.stop,
|
71 |
+
temperature: generateSettings?.temperature ?? 1,
|
72 |
},
|
73 |
+
});
|
|
|
|
|
74 |
|
75 |
+
// Preprompt is the same as the first system message.
|
76 |
+
let systemMessage = preprompt;
|
77 |
+
if (messages[0].from === "system") {
|
78 |
+
systemMessage = messages[0].content;
|
79 |
+
messages.shift();
|
80 |
+
}
|
81 |
+
|
82 |
+
const vertexMessages = messages.map(({ from, content }: Omit<Message, "id">): Content => {
|
83 |
+
return {
|
84 |
+
role: from === "user" ? "user" : "model",
|
85 |
+
parts: [
|
86 |
+
{
|
87 |
+
text: content,
|
88 |
+
},
|
89 |
+
],
|
90 |
+
};
|
91 |
});
|
92 |
|
93 |
+
const result = await generativeModel.generateContentStream({
|
94 |
+
contents: vertexMessages,
|
95 |
+
systemInstruction: systemMessage
|
96 |
+
? {
|
97 |
+
role: "system",
|
98 |
+
parts: [
|
99 |
+
{
|
100 |
+
text: systemMessage,
|
101 |
+
},
|
102 |
+
],
|
103 |
+
}
|
104 |
+
: undefined,
|
105 |
+
});
|
106 |
|
107 |
+
let tokenId = 0;
|
108 |
return (async function* () {
|
109 |
let generatedText = "";
|
110 |
|
111 |
for await (const data of result.stream) {
|
112 |
+
if (!data?.candidates?.length) break; // Handle case where no candidates are present
|
113 |
+
|
114 |
+
const candidate = data.candidates[0];
|
115 |
+
if (!candidate.content?.parts?.length) continue; // Skip if no parts are present
|
116 |
+
|
117 |
+
const firstPart = candidate.content.parts.find((part) => "text" in part) as
|
118 |
+
| TextPart
|
119 |
+
| undefined;
|
120 |
+
if (!firstPart) continue; // Skip if no text part is found
|
121 |
+
|
122 |
+
const isLastChunk = !!candidate.finishReason;
|
123 |
+
|
124 |
+
const content = firstPart.text;
|
125 |
+
generatedText += content;
|
126 |
+
const output: TextGenerationStreamOutput = {
|
127 |
+
token: {
|
128 |
+
id: tokenId++,
|
129 |
+
text: content,
|
130 |
+
logprob: 0,
|
131 |
+
special: isLastChunk,
|
132 |
+
},
|
133 |
+
generated_text: isLastChunk ? generatedText : null,
|
134 |
+
details: null,
|
135 |
+
};
|
136 |
+
yield output;
|
137 |
|
138 |
+
if (isLastChunk) break;
|
|
|
|
|
|
|
139 |
}
|
140 |
})();
|
141 |
};
|
src/routes/models/+page.svelte
CHANGED
@@ -64,7 +64,9 @@
|
|
64 |
<dt class="flex items-center gap-2 font-semibold">
|
65 |
{model.displayName}
|
66 |
</dt>
|
67 |
-
<dd class="text-sm text-gray-500 dark:text-gray-400">
|
|
|
|
|
68 |
</a>
|
69 |
{/each}
|
70 |
</dl>
|
|
|
64 |
<dt class="flex items-center gap-2 font-semibold">
|
65 |
{model.displayName}
|
66 |
</dt>
|
67 |
+
<dd class="whitespace-pre-wrap text-sm text-gray-500 dark:text-gray-400">
|
68 |
+
{model.description || "-"}
|
69 |
+
</dd>
|
70 |
</a>
|
71 |
{/each}
|
72 |
</dl>
|