@qualve/googleai 0.0.1 → 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/package.json +12 -3
- package/src/file.js +81 -0
- package/src/index.js +5 -82
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qualve/googleai",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Google Gemini provider for Qualve LLM tasks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -12,18 +12,27 @@
|
|
|
12
12
|
},
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
|
-
"url": "https://github.com/qualve/ai.git",
|
|
15
|
+
"url": "git+https://github.com/qualve/ai.git",
|
|
16
16
|
"directory": "providers/googleai"
|
|
17
17
|
},
|
|
18
18
|
"contributors": [
|
|
19
19
|
"Dmitry Sharabin",
|
|
20
20
|
"Lea Verou"
|
|
21
21
|
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"release": "npm login && release-it"
|
|
24
|
+
},
|
|
25
|
+
"release-it": {
|
|
26
|
+
"git": {
|
|
27
|
+
"tagName": "${npm.name}@${version}",
|
|
28
|
+
"commitMessage": "Release ${npm.name}@${version}"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
22
31
|
"peerDependencies": {
|
|
23
32
|
"qualve": "*"
|
|
24
33
|
},
|
|
25
34
|
"dependencies": {
|
|
26
35
|
"@qualve/llm": "*",
|
|
27
|
-
"@google/genai": "^
|
|
36
|
+
"@google/genai": "^2.4.0"
|
|
28
37
|
}
|
|
29
38
|
}
|
package/src/file.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import LLMFile from "@qualve/llm/file";
|
|
3
|
+
|
|
4
|
+
export default class GeminiFile extends LLMFile {
|
|
5
|
+
get remoteFilename () {
|
|
6
|
+
let name = super.remoteFilename;
|
|
7
|
+
// Gemini file ID: lowercase alphanumeric or dashes, no leading/trailing dashes.
|
|
8
|
+
name = name.replace(/[_.]/g, "-").replace(/^-|-$/g, "");
|
|
9
|
+
|
|
10
|
+
// Gemini file IDs are limited to 40 chars.
|
|
11
|
+
// Batch slice inputs can exceed this (e.g. "ba-answers-normalized-unique-500-999-json").
|
|
12
|
+
// Truncate with a hash suffix to preserve uniqueness.
|
|
13
|
+
let maxLength = 40;
|
|
14
|
+
if (name.length > maxLength) {
|
|
15
|
+
let hash = createHash("sha256").update(name).digest("hex").slice(0, 6);
|
|
16
|
+
name = name.slice(0, maxLength - 7) + "-" + hash;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return name;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Display name for the Gemini API (preserves original filename format). */
|
|
23
|
+
get displayName () {
|
|
24
|
+
return super.remoteFilename;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async doUpload () {
|
|
28
|
+
let { client } = this.context;
|
|
29
|
+
return client.files.upload({
|
|
30
|
+
file: this.toBlob(),
|
|
31
|
+
config: { name: this.remoteFilename, displayName: this.displayName, mimeType: this.mimeType },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Execute a file operation with shared error handling for not-found cases.
|
|
37
|
+
* Gemini returns 403 (not 404) when a file doesn't exist, so we disambiguate
|
|
38
|
+
* by listing files to check whether it's a real permission error.
|
|
39
|
+
* @param {"get" | "delete"} method
|
|
40
|
+
* @returns {Promise<object|null>}
|
|
41
|
+
*/
|
|
42
|
+
async #safeFileOp (method) {
|
|
43
|
+
let name = "files/" + this.remoteFilename;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// If we don't await here, the error is unhandled
|
|
47
|
+
return await this.context.client.files[method]({ name });
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
if (e.status === 403 || e.status === 404) {
|
|
51
|
+
// 403 can mean "not found" on Gemini — verify by listing files.
|
|
52
|
+
// 404 is a straightforward not-found.
|
|
53
|
+
if (e.status === 403) {
|
|
54
|
+
let files = await this.context.client.files.list();
|
|
55
|
+
for await (let file of files) {
|
|
56
|
+
if (file.name === name) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`You don't have permission to access file ${this.path}`,
|
|
59
|
+
{ cause: e },
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
throw new Error(`Failed to ${method} file ${this.path}`, { cause: e });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Not found
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async getRemote () {
|
|
75
|
+
return this.#safeFileOp("get");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async deleteRemote () {
|
|
79
|
+
return this.#safeFileOp("delete");
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { LLMTask } from "@qualve/llm";
|
|
1
|
+
import LLMTask from "@qualve/llm";
|
|
3
2
|
import { createUserContent, createPartFromUri, GoogleGenAI } from "@google/genai";
|
|
3
|
+
import GeminiFile from "./file.js";
|
|
4
4
|
|
|
5
5
|
export default class Gemini extends LLMTask {
|
|
6
6
|
static id = "gemini";
|
|
7
7
|
static name = "Gemini";
|
|
8
|
+
static File = GeminiFile;
|
|
8
9
|
static models = [
|
|
9
10
|
"gemini-3.1-pro-preview",
|
|
10
11
|
"gemini-3.1-flash-preview",
|
|
@@ -28,84 +29,6 @@ export default class Gemini extends LLMTask {
|
|
|
28
29
|
httpOptions: { timeout: 30 * 60_000 }, // 30 minutes — LLM tasks with thinking can be very slow
|
|
29
30
|
});
|
|
30
31
|
|
|
31
|
-
getFileInfo (filepath) {
|
|
32
|
-
let { name, dirName } = super.getFileInfo(filepath);
|
|
33
|
-
let displayName = name;
|
|
34
|
-
|
|
35
|
-
// Gemini file ID: lowercase alphanumeric or dashes, no leading/trailing dashes.
|
|
36
|
-
name = name.replace(/[_.]/g, "-").replace(/^-|-$/g, "");
|
|
37
|
-
|
|
38
|
-
// Gemini file IDs are limited to 40 chars.
|
|
39
|
-
// Batch slice inputs can exceed this (e.g. "ba-answers-normalized-unique-500-999-json").
|
|
40
|
-
// Truncate with a hash suffix to preserve uniqueness.
|
|
41
|
-
let maxLength = 40;
|
|
42
|
-
if (name.length > maxLength) {
|
|
43
|
-
let hash = createHash("sha256").update(name).digest("hex").slice(0, 6);
|
|
44
|
-
name = name.slice(0, maxLength - 7) + "-" + hash;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return { name, dirName, displayName };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async uploadFile (filepath, { mimeType, contents }) {
|
|
51
|
-
let { name, displayName } = this.getFileInfo(filepath);
|
|
52
|
-
return this.client.files.upload({
|
|
53
|
-
file: new Blob([contents], { type: mimeType }),
|
|
54
|
-
config: { name, displayName, mimeType },
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Execute a file operation with shared error handling for not-found cases.
|
|
60
|
-
* Gemini returns 403 (not 404) when a file doesn't exist, so we disambiguate
|
|
61
|
-
* by listing files to check whether it's a real permission error.
|
|
62
|
-
* @param {string} filepath - The local file path (used for name resolution and error messages).
|
|
63
|
-
* @param {"get" | "delete"} method - The method name on `this.client.files` to call.
|
|
64
|
-
* @returns {Promise<object|null>} The operation result, or null if the file was not found.
|
|
65
|
-
*/
|
|
66
|
-
async #safeFileOp (filepath, method) {
|
|
67
|
-
let { name } = this.getFileInfo(filepath);
|
|
68
|
-
name = "files/" + name;
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
// If we don't await here, the error is unhandled
|
|
72
|
-
return await this.client.files[method]({ name });
|
|
73
|
-
}
|
|
74
|
-
catch (e) {
|
|
75
|
-
if (e.status === 403 || e.status === 404) {
|
|
76
|
-
// 403 can mean "not found" on Gemini — verify by listing files.
|
|
77
|
-
// 404 is a straightforward not-found.
|
|
78
|
-
if (e.status === 403) {
|
|
79
|
-
let files = await this.client.files.list();
|
|
80
|
-
for await (let file of files) {
|
|
81
|
-
if (file.name === name) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
`You don't have permission to access file ${filepath}`,
|
|
84
|
-
{
|
|
85
|
-
cause: e,
|
|
86
|
-
},
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
throw new Error(`Failed to ${method} file ${filepath}`, { cause: e });
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Not found
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async getFile (filepath) {
|
|
102
|
-
return this.#safeFileOp(filepath, "get");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async deleteFile (filepath) {
|
|
106
|
-
return this.#safeFileOp(filepath, "delete");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
32
|
async listFiles () {
|
|
110
33
|
return [...(await this.client.files.list())];
|
|
111
34
|
}
|
|
@@ -119,7 +42,7 @@ export default class Gemini extends LLMTask {
|
|
|
119
42
|
...system,
|
|
120
43
|
...prompt,
|
|
121
44
|
...input
|
|
122
|
-
.map(f =>
|
|
45
|
+
.map(f => f.toString())
|
|
123
46
|
.filter(Boolean),
|
|
124
47
|
]),
|
|
125
48
|
});
|
|
@@ -184,7 +107,7 @@ export default class Gemini extends LLMTask {
|
|
|
184
107
|
// LANGUAGE: Unsupported language detected.
|
|
185
108
|
// BLOCKLIST: Content contains forbidden terms.
|
|
186
109
|
// PROHIBITED_CONTENT: Content potentially contains prohibited material.
|
|
187
|
-
// SPII:
|
|
110
|
+
// SPII: Content potentially contains Sensitive Personally Identifiable Information.
|
|
188
111
|
let reasons = {
|
|
189
112
|
STOP: LLMTask.stopReasons.COMPLETE,
|
|
190
113
|
MAX_TOKENS: LLMTask.stopReasons.MAX_TOKENS,
|