@moxxy/plugin-provider-google 0.21.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/LICENSE +21 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +25 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +33 -0
- package/dist/models.js.map +1 -0
- package/package.json +64 -0
- package/src/index.test.ts +134 -0
- package/src/index.ts +51 -0
- package/src/models.ts +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moxxy (moxxy.ai)
|
|
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/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { geminiModels } from './models.js';
|
|
2
|
+
export { geminiModels };
|
|
3
|
+
/**
|
|
4
|
+
* Model used when a request didn't pin one. MUST exist in {@link geminiModels}:
|
|
5
|
+
* the descriptor for the default model drives the default request's
|
|
6
|
+
* context-window/capability budget, so a default id absent from the catalog
|
|
7
|
+
* would silently fall back to the host's generic miss-path budget (exactly the
|
|
8
|
+
* unlisted-id trap the catalog docstring warns about) for every default call.
|
|
9
|
+
* The exported constant lets the package's own tests pin that invariant.
|
|
10
|
+
*/
|
|
11
|
+
export declare const GEMINI_DEFAULT_MODEL = "gemini-2.5-flash";
|
|
12
|
+
/**
|
|
13
|
+
* Google Gemini, served via Gemini's OpenAI-compatibility endpoint so it
|
|
14
|
+
* reuses the shared {@link defineOpenAICompatProvider} with the `google` slug +
|
|
15
|
+
* base URL + Gemini catalog forced on (so usage stats, provider events and
|
|
16
|
+
* error context attribute to `google`, not `openai`). The canonical key is
|
|
17
|
+
* `GOOGLE_API_KEY`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const googleProviderDef: import("@moxxy/sdk").ProviderDef;
|
|
20
|
+
export declare const googlePlugin: import("@moxxy/sdk").Plugin;
|
|
21
|
+
export default googlePlugin;
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,CAAC;AAKxB;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,qBAAqB,CAAC;AAEvD;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,kCAc5B,CAAC;AAKH,eAAO,MAAM,YAAY,6BAGvB,CAAC;AAEH,eAAe,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { definePlugin } from '@moxxy/sdk';
|
|
2
|
+
import { defineOpenAICompatProvider } from '@moxxy/plugin-provider-openai';
|
|
3
|
+
import { geminiModels } from './models.js';
|
|
4
|
+
export { geminiModels };
|
|
5
|
+
/** Gemini's OpenAI-compatibility endpoint (trailing slash matters for the SDK). */
|
|
6
|
+
const GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai/';
|
|
7
|
+
/**
|
|
8
|
+
* Model used when a request didn't pin one. MUST exist in {@link geminiModels}:
|
|
9
|
+
* the descriptor for the default model drives the default request's
|
|
10
|
+
* context-window/capability budget, so a default id absent from the catalog
|
|
11
|
+
* would silently fall back to the host's generic miss-path budget (exactly the
|
|
12
|
+
* unlisted-id trap the catalog docstring warns about) for every default call.
|
|
13
|
+
* The exported constant lets the package's own tests pin that invariant.
|
|
14
|
+
*/
|
|
15
|
+
export const GEMINI_DEFAULT_MODEL = 'gemini-2.5-flash';
|
|
16
|
+
/**
|
|
17
|
+
* Google Gemini, served via Gemini's OpenAI-compatibility endpoint so it
|
|
18
|
+
* reuses the shared {@link defineOpenAICompatProvider} with the `google` slug +
|
|
19
|
+
* base URL + Gemini catalog forced on (so usage stats, provider events and
|
|
20
|
+
* error context attribute to `google`, not `openai`). The canonical key is
|
|
21
|
+
* `GOOGLE_API_KEY`.
|
|
22
|
+
*/
|
|
23
|
+
export const googleProviderDef = defineOpenAICompatProvider({
|
|
24
|
+
name: 'google',
|
|
25
|
+
baseURL: GEMINI_BASE_URL,
|
|
26
|
+
defaultModel: GEMINI_DEFAULT_MODEL,
|
|
27
|
+
models: geminiModels,
|
|
28
|
+
auth: {
|
|
29
|
+
kind: 'apiKey',
|
|
30
|
+
// Pin the env var explicitly: Google's own docs hand users GEMINI_API_KEY,
|
|
31
|
+
// but moxxy resolves credentials under the canonical GOOGLE_API_KEY. Naming
|
|
32
|
+
// it here (instead of relying on name-inference) keeps the setup hint and
|
|
33
|
+
// the resolver in agreement.
|
|
34
|
+
envVar: 'GOOGLE_API_KEY',
|
|
35
|
+
hint: 'Google AI Studio (Gemini) API key from https://aistudio.google.com/apikey (set GOOGLE_API_KEY)',
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
// `version` is intentionally omitted — definePlugin defaults it; a hardcoded
|
|
39
|
+
// literal here would permanently diverge from package.json and masquerade as a
|
|
40
|
+
// real version in diagnostics.
|
|
41
|
+
export const googlePlugin = definePlugin({
|
|
42
|
+
name: '@moxxy/plugin-provider-google',
|
|
43
|
+
providers: [googleProviderDef],
|
|
44
|
+
});
|
|
45
|
+
export default googlePlugin;
|
|
46
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB,mFAAmF;AACnF,MAAM,eAAe,GAAG,0DAA0D,CAAC;AAEnF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,kBAAkB,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,0BAA0B,CAAC;IAC1D,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE,eAAe;IACxB,YAAY,EAAE,oBAAoB;IAClC,MAAM,EAAE,YAAY;IACpB,IAAI,EAAE;QACJ,IAAI,EAAE,QAAQ;QACd,2EAA2E;QAC3E,4EAA4E;QAC5E,0EAA0E;QAC1E,6BAA6B;QAC7B,MAAM,EAAE,gBAAgB;QACxB,IAAI,EAAE,gGAAgG;KACvG;CACF,CAAC,CAAC;AAEH,6EAA6E;AAC7E,+EAA+E;AAC/E,+BAA+B;AAC/B,MAAM,CAAC,MAAM,YAAY,GAAG,YAAY,CAAC;IACvC,IAAI,EAAE,+BAA+B;IACrC,SAAS,EAAE,CAAC,iBAAiB,CAAC;CAC/B,CAAC,CAAC;AAEH,eAAe,YAAY,CAAC"}
|
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ModelDescriptor } from '@moxxy/sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Google Gemini model catalog, as of 2026-06 (verify against
|
|
4
|
+
* https://ai.google.dev/gemini-api/docs/models). Served here via Gemini's
|
|
5
|
+
* OpenAI-compatibility endpoint, so these stream through the shared
|
|
6
|
+
* {@link import('@moxxy/plugin-provider-openai').OpenAIProvider}. The Gemini 3
|
|
7
|
+
* and 2.5 families all carry a 1M-token context window and are natively
|
|
8
|
+
* multimodal (image input).
|
|
9
|
+
*
|
|
10
|
+
* An unlisted model id still works for raw inference — it's passed straight
|
|
11
|
+
* through to the endpoint — but it loses Gemini-correct budgeting: the
|
|
12
|
+
* descriptor lookup misses, so context-window/capability gating falls back to
|
|
13
|
+
* the host's miss-path default instead of the 1M window asserted here.
|
|
14
|
+
*
|
|
15
|
+
* supportsDocuments is intentionally NOT set: although Gemini natively accepts
|
|
16
|
+
* PDFs on its own API (via `inline_data`), the OpenAI-compatibility surface
|
|
17
|
+
* does not honor the OpenAI `file`/`file_data` content part the shared
|
|
18
|
+
* translate layer emits for `document` blocks. Asserting it would make the
|
|
19
|
+
* desktop ship raw PDF bytes the endpoint rejects/ignores — losing the
|
|
20
|
+
* document entirely with no fallback. Leaving it unset keeps the safe
|
|
21
|
+
* extracted-text path. Images ride `image_url` data URLs, which the compat
|
|
22
|
+
* endpoint does accept, so supportsImages stays true.
|
|
23
|
+
*/
|
|
24
|
+
export declare const geminiModels: ReadonlyArray<ModelDescriptor>;
|
|
25
|
+
//# sourceMappingURL=models.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,YAAY,EAAE,aAAa,CAAC,eAAe,CAUvD,CAAC"}
|
package/dist/models.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gemini model catalog, as of 2026-06 (verify against
|
|
3
|
+
* https://ai.google.dev/gemini-api/docs/models). Served here via Gemini's
|
|
4
|
+
* OpenAI-compatibility endpoint, so these stream through the shared
|
|
5
|
+
* {@link import('@moxxy/plugin-provider-openai').OpenAIProvider}. The Gemini 3
|
|
6
|
+
* and 2.5 families all carry a 1M-token context window and are natively
|
|
7
|
+
* multimodal (image input).
|
|
8
|
+
*
|
|
9
|
+
* An unlisted model id still works for raw inference — it's passed straight
|
|
10
|
+
* through to the endpoint — but it loses Gemini-correct budgeting: the
|
|
11
|
+
* descriptor lookup misses, so context-window/capability gating falls back to
|
|
12
|
+
* the host's miss-path default instead of the 1M window asserted here.
|
|
13
|
+
*
|
|
14
|
+
* supportsDocuments is intentionally NOT set: although Gemini natively accepts
|
|
15
|
+
* PDFs on its own API (via `inline_data`), the OpenAI-compatibility surface
|
|
16
|
+
* does not honor the OpenAI `file`/`file_data` content part the shared
|
|
17
|
+
* translate layer emits for `document` blocks. Asserting it would make the
|
|
18
|
+
* desktop ship raw PDF bytes the endpoint rejects/ignores — losing the
|
|
19
|
+
* document entirely with no fallback. Leaving it unset keeps the safe
|
|
20
|
+
* extracted-text path. Images ride `image_url` data URLs, which the compat
|
|
21
|
+
* endpoint does accept, so supportsImages stays true.
|
|
22
|
+
*/
|
|
23
|
+
export const geminiModels = [
|
|
24
|
+
// Gemini 3 family: current frontier. Reasoning models.
|
|
25
|
+
{ id: 'gemini-3-pro', contextWindow: 1_000_000, maxOutputTokens: 65_536, supportsTools: true, supportsStreaming: true, supportsImages: true, supportsReasoning: true },
|
|
26
|
+
{ id: 'gemini-3-flash', contextWindow: 1_000_000, maxOutputTokens: 65_536, supportsTools: true, supportsStreaming: true, supportsImages: true, supportsReasoning: true },
|
|
27
|
+
// Gemini 2.5 family: widely available, strong price/performance. pro/flash
|
|
28
|
+
// are reasoning models.
|
|
29
|
+
{ id: 'gemini-2.5-pro', contextWindow: 1_000_000, maxOutputTokens: 65_536, supportsTools: true, supportsStreaming: true, supportsImages: true, supportsReasoning: true },
|
|
30
|
+
{ id: 'gemini-2.5-flash', contextWindow: 1_000_000, maxOutputTokens: 65_536, supportsTools: true, supportsStreaming: true, supportsImages: true, supportsReasoning: true },
|
|
31
|
+
{ id: 'gemini-2.5-flash-lite', contextWindow: 1_000_000, maxOutputTokens: 65_536, supportsTools: true, supportsStreaming: true, supportsImages: true },
|
|
32
|
+
];
|
|
33
|
+
//# sourceMappingURL=models.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,YAAY,GAAmC;IAC1D,uDAAuD;IACvD,EAAE,EAAE,EAAE,cAAc,EAAE,aAAa,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE;IACtK,EAAE,EAAE,EAAE,gBAAgB,EAAE,aAAa,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE;IAExK,2EAA2E;IAC3E,wBAAwB;IACxB,EAAE,EAAE,EAAE,gBAAgB,EAAE,aAAa,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE;IACxK,EAAE,EAAE,EAAE,kBAAkB,EAAE,aAAa,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE;IAC1K,EAAE,EAAE,EAAE,uBAAuB,EAAE,aAAa,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE;CACvJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@moxxy/plugin-provider-google",
|
|
3
|
+
"version": "0.21.1",
|
|
4
|
+
"description": "Google Gemini LLMProvider plugin for moxxy. Streams the Gemini OpenAI-compatible endpoint (generativelanguage.googleapis.com/v1beta/openai).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"moxxy",
|
|
7
|
+
"agent",
|
|
8
|
+
"provider",
|
|
9
|
+
"google",
|
|
10
|
+
"gemini",
|
|
11
|
+
"llm"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://moxxy.ai",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/moxxy-ai/moxxy/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/moxxy-ai/moxxy.git",
|
|
20
|
+
"directory": "packages/plugin-provider-google"
|
|
21
|
+
},
|
|
22
|
+
"author": "Michal Makowski <michal.makowski97@gmail.com>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src"
|
|
39
|
+
],
|
|
40
|
+
"moxxy": {
|
|
41
|
+
"plugin": {
|
|
42
|
+
"entry": "./dist/index.js",
|
|
43
|
+
"kind": "provider"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@moxxy/plugin-provider-openai": "0.21.1",
|
|
48
|
+
"@moxxy/sdk": "0.21.1"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^22.10.0",
|
|
52
|
+
"typescript": "^5.7.3",
|
|
53
|
+
"vitest": "^2.1.8",
|
|
54
|
+
"zod": "^3.24.0",
|
|
55
|
+
"@moxxy/tsconfig": "0.0.0",
|
|
56
|
+
"@moxxy/vitest-preset": "0.0.0"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsc -p tsconfig.json",
|
|
60
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"clean": "rm -rf dist .turbo"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { googlePlugin, googleProviderDef, geminiModels, GEMINI_DEFAULT_MODEL } from './index.js';
|
|
3
|
+
|
|
4
|
+
describe('@moxxy/plugin-provider-google', () => {
|
|
5
|
+
it('registers the google provider', () => {
|
|
6
|
+
expect(googlePlugin.providers?.map((p) => p.name)).toEqual(['google']);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('advertises the Gemini catalog (1M-context, vision-capable)', () => {
|
|
10
|
+
expect(googleProviderDef.models).toEqual(geminiModels);
|
|
11
|
+
expect(geminiModels.find((m) => m.id === 'gemini-3-pro')).toMatchObject({
|
|
12
|
+
contextWindow: 1_000_000,
|
|
13
|
+
supportsImages: true,
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('createClient stamps the google slug so usage/errors attribute to Gemini, not openai', () => {
|
|
18
|
+
const client = googleProviderDef.createClient({ apiKey: 'test-key' });
|
|
19
|
+
expect(client.name).toBe('google');
|
|
20
|
+
expect(client.models).toEqual(geminiModels);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('exposes an apiKey auth descriptor pinned to the canonical GOOGLE_API_KEY env var', () => {
|
|
24
|
+
expect(googleProviderDef.auth).toMatchObject({ kind: 'apiKey', envVar: 'GOOGLE_API_KEY' });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('every Gemini model advertises supportsImages (image_url data URLs are honored by the compat endpoint)', () => {
|
|
28
|
+
for (const m of geminiModels) {
|
|
29
|
+
expect(m.supportsImages, `${m.id} supportsImages`).toBe(true);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('no Gemini model over-claims supportsDocuments (the OpenAI-compat endpoint rejects the file part)', () => {
|
|
34
|
+
// Worst case if this regresses: the desktop ships raw PDF bytes as an
|
|
35
|
+
// OpenAI `file`/`file_data` part the Gemini compat endpoint does not honor,
|
|
36
|
+
// so the document is dropped/400s with no fallback. Keeping the flag unset
|
|
37
|
+
// preserves the safe extracted-text path.
|
|
38
|
+
for (const m of geminiModels) {
|
|
39
|
+
expect(m.supportsDocuments, `${m.id} supportsDocuments`).toBeFalsy();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('the reasoning-tier Gemini models advertise supportsReasoning', () => {
|
|
44
|
+
// Reasoning gating (reasoning_effort + reasoning-stream surfacing) keys off
|
|
45
|
+
// this flag; the pro/flash tiers are reasoning models.
|
|
46
|
+
const reasoningIds = ['gemini-3-pro', 'gemini-3-flash', 'gemini-2.5-pro', 'gemini-2.5-flash'];
|
|
47
|
+
for (const id of reasoningIds) {
|
|
48
|
+
expect(geminiModels.find((m) => m.id === id)?.supportsReasoning, id).toBe(true);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('the default model exists in the catalog (else every default request gets a wrong budget)', () => {
|
|
53
|
+
// Worst case if this regresses: a request that pins no model falls through
|
|
54
|
+
// to GEMINI_DEFAULT_MODEL, whose descriptor lookup then MISSES, so the
|
|
55
|
+
// default call silently inherits the host's generic miss-path
|
|
56
|
+
// context-window/capability budget instead of Gemini's 1M window — the exact
|
|
57
|
+
// unlisted-id trap the catalog docstring warns about, now hitting the
|
|
58
|
+
// package's OWN configured default.
|
|
59
|
+
expect(geminiModels.find((m) => m.id === GEMINI_DEFAULT_MODEL), GEMINI_DEFAULT_MODEL).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('the catalog is non-empty and free of duplicate ids', () => {
|
|
63
|
+
// An empty catalog would defeat all context/capability gating; a duplicate
|
|
64
|
+
// id makes descriptor lookup order-dependent (a stale/wrong descriptor can
|
|
65
|
+
// shadow the intended one). Both are silent-correctness hazards a careless
|
|
66
|
+
// future edit could introduce.
|
|
67
|
+
expect(geminiModels.length).toBeGreaterThan(0);
|
|
68
|
+
const ids = geminiModels.map((m) => m.id);
|
|
69
|
+
expect(new Set(ids).size, `duplicate model id in catalog: ${ids.join(', ')}`).toBe(ids.length);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('every catalog entry carries a sane, positive context/output budget', () => {
|
|
73
|
+
// A zero/negative/NaN window or output cap would corrupt compaction/elision
|
|
74
|
+
// budgeting downstream (e.g. premature compaction or unbounded asks). Assert
|
|
75
|
+
// the worst case never ships from this catalog.
|
|
76
|
+
for (const m of geminiModels) {
|
|
77
|
+
expect(Number.isFinite(m.contextWindow) && m.contextWindow > 0, `${m.id} contextWindow`).toBe(true);
|
|
78
|
+
if (m.maxOutputTokens !== undefined) {
|
|
79
|
+
expect(
|
|
80
|
+
Number.isFinite(m.maxOutputTokens) && m.maxOutputTokens > 0,
|
|
81
|
+
`${m.id} maxOutputTokens`,
|
|
82
|
+
).toBe(true);
|
|
83
|
+
// The output cap can never exceed the total context window.
|
|
84
|
+
expect(m.maxOutputTokens, `${m.id} maxOutputTokens <= contextWindow`).toBeLessThanOrEqual(
|
|
85
|
+
m.contextWindow,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('wires validateKey so a bad Gemini key is caught at setup, not first inference', () => {
|
|
92
|
+
// defineOpenAICompatProvider defaults validation ON (probes the base URL's
|
|
93
|
+
// /models). Dropping it would let an invalid key sail through setup and only
|
|
94
|
+
// fail — opaquely — on the first real turn. Pin that it stays wired.
|
|
95
|
+
expect(typeof googleProviderDef.validateKey).toBe('function');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('createClient narrows away hostile/wrong-typed config fields without crashing', () => {
|
|
99
|
+
// The registry hands createClient an untyped Record<string, unknown>. A
|
|
100
|
+
// wrong-typed baseURL/defaultModel (number/object/null) plus unknown junk
|
|
101
|
+
// must be narrowed away and fall back to vendor defaults — they must NOT
|
|
102
|
+
// smuggle a bad value into the client or crash construction. A valid apiKey
|
|
103
|
+
// is always present in practice (the registry resolves it before calling),
|
|
104
|
+
// so we supply one and stress only the other fields.
|
|
105
|
+
const hostile: Record<string, unknown> = {
|
|
106
|
+
apiKey: 'test-key',
|
|
107
|
+
baseURL: { evil: true } as unknown as string,
|
|
108
|
+
defaultModel: 12345 as unknown as string,
|
|
109
|
+
extraJunk: 'ignored',
|
|
110
|
+
__proto__pollution: 'ignored',
|
|
111
|
+
};
|
|
112
|
+
expect(() => googleProviderDef.createClient(hostile)).not.toThrow();
|
|
113
|
+
const client = googleProviderDef.createClient(hostile);
|
|
114
|
+
// Slug attribution and catalog must survive the hostile config.
|
|
115
|
+
expect(client.name).toBe('google');
|
|
116
|
+
expect(client.models).toEqual(geminiModels);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('createClient surfaces a clear error (never a silently-broken client) when no key resolves', () => {
|
|
120
|
+
// In practice the registry only calls createClient with a resolved key. If
|
|
121
|
+
// it ever didn't, the underlying OpenAI SDK constructor must FAIL LOUDLY
|
|
122
|
+
// (no key) rather than hand back a half-constructed client that 401s opaquely
|
|
123
|
+
// on the first turn. Pin that the keyless path throws synchronously here.
|
|
124
|
+
// The OpenAI SDK falls back to process.env.OPENAI_API_KEY, so clear it for
|
|
125
|
+
// the assertion to keep this deterministic regardless of the runner's env.
|
|
126
|
+
const saved = process.env.OPENAI_API_KEY;
|
|
127
|
+
delete process.env.OPENAI_API_KEY;
|
|
128
|
+
try {
|
|
129
|
+
expect(() => googleProviderDef.createClient({})).toThrow();
|
|
130
|
+
} finally {
|
|
131
|
+
if (saved !== undefined) process.env.OPENAI_API_KEY = saved;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { definePlugin } from '@moxxy/sdk';
|
|
2
|
+
import { defineOpenAICompatProvider } from '@moxxy/plugin-provider-openai';
|
|
3
|
+
import { geminiModels } from './models.js';
|
|
4
|
+
|
|
5
|
+
export { geminiModels };
|
|
6
|
+
|
|
7
|
+
/** Gemini's OpenAI-compatibility endpoint (trailing slash matters for the SDK). */
|
|
8
|
+
const GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai/';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Model used when a request didn't pin one. MUST exist in {@link geminiModels}:
|
|
12
|
+
* the descriptor for the default model drives the default request's
|
|
13
|
+
* context-window/capability budget, so a default id absent from the catalog
|
|
14
|
+
* would silently fall back to the host's generic miss-path budget (exactly the
|
|
15
|
+
* unlisted-id trap the catalog docstring warns about) for every default call.
|
|
16
|
+
* The exported constant lets the package's own tests pin that invariant.
|
|
17
|
+
*/
|
|
18
|
+
export const GEMINI_DEFAULT_MODEL = 'gemini-2.5-flash';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Google Gemini, served via Gemini's OpenAI-compatibility endpoint so it
|
|
22
|
+
* reuses the shared {@link defineOpenAICompatProvider} with the `google` slug +
|
|
23
|
+
* base URL + Gemini catalog forced on (so usage stats, provider events and
|
|
24
|
+
* error context attribute to `google`, not `openai`). The canonical key is
|
|
25
|
+
* `GOOGLE_API_KEY`.
|
|
26
|
+
*/
|
|
27
|
+
export const googleProviderDef = defineOpenAICompatProvider({
|
|
28
|
+
name: 'google',
|
|
29
|
+
baseURL: GEMINI_BASE_URL,
|
|
30
|
+
defaultModel: GEMINI_DEFAULT_MODEL,
|
|
31
|
+
models: geminiModels,
|
|
32
|
+
auth: {
|
|
33
|
+
kind: 'apiKey',
|
|
34
|
+
// Pin the env var explicitly: Google's own docs hand users GEMINI_API_KEY,
|
|
35
|
+
// but moxxy resolves credentials under the canonical GOOGLE_API_KEY. Naming
|
|
36
|
+
// it here (instead of relying on name-inference) keeps the setup hint and
|
|
37
|
+
// the resolver in agreement.
|
|
38
|
+
envVar: 'GOOGLE_API_KEY',
|
|
39
|
+
hint: 'Google AI Studio (Gemini) API key from https://aistudio.google.com/apikey (set GOOGLE_API_KEY)',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// `version` is intentionally omitted — definePlugin defaults it; a hardcoded
|
|
44
|
+
// literal here would permanently diverge from package.json and masquerade as a
|
|
45
|
+
// real version in diagnostics.
|
|
46
|
+
export const googlePlugin = definePlugin({
|
|
47
|
+
name: '@moxxy/plugin-provider-google',
|
|
48
|
+
providers: [googleProviderDef],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export default googlePlugin;
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ModelDescriptor } from '@moxxy/sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Google Gemini model catalog, as of 2026-06 (verify against
|
|
5
|
+
* https://ai.google.dev/gemini-api/docs/models). Served here via Gemini's
|
|
6
|
+
* OpenAI-compatibility endpoint, so these stream through the shared
|
|
7
|
+
* {@link import('@moxxy/plugin-provider-openai').OpenAIProvider}. The Gemini 3
|
|
8
|
+
* and 2.5 families all carry a 1M-token context window and are natively
|
|
9
|
+
* multimodal (image input).
|
|
10
|
+
*
|
|
11
|
+
* An unlisted model id still works for raw inference — it's passed straight
|
|
12
|
+
* through to the endpoint — but it loses Gemini-correct budgeting: the
|
|
13
|
+
* descriptor lookup misses, so context-window/capability gating falls back to
|
|
14
|
+
* the host's miss-path default instead of the 1M window asserted here.
|
|
15
|
+
*
|
|
16
|
+
* supportsDocuments is intentionally NOT set: although Gemini natively accepts
|
|
17
|
+
* PDFs on its own API (via `inline_data`), the OpenAI-compatibility surface
|
|
18
|
+
* does not honor the OpenAI `file`/`file_data` content part the shared
|
|
19
|
+
* translate layer emits for `document` blocks. Asserting it would make the
|
|
20
|
+
* desktop ship raw PDF bytes the endpoint rejects/ignores — losing the
|
|
21
|
+
* document entirely with no fallback. Leaving it unset keeps the safe
|
|
22
|
+
* extracted-text path. Images ride `image_url` data URLs, which the compat
|
|
23
|
+
* endpoint does accept, so supportsImages stays true.
|
|
24
|
+
*/
|
|
25
|
+
export const geminiModels: ReadonlyArray<ModelDescriptor> = [
|
|
26
|
+
// Gemini 3 family: current frontier. Reasoning models.
|
|
27
|
+
{ id: 'gemini-3-pro', contextWindow: 1_000_000, maxOutputTokens: 65_536, supportsTools: true, supportsStreaming: true, supportsImages: true, supportsReasoning: true },
|
|
28
|
+
{ id: 'gemini-3-flash', contextWindow: 1_000_000, maxOutputTokens: 65_536, supportsTools: true, supportsStreaming: true, supportsImages: true, supportsReasoning: true },
|
|
29
|
+
|
|
30
|
+
// Gemini 2.5 family: widely available, strong price/performance. pro/flash
|
|
31
|
+
// are reasoning models.
|
|
32
|
+
{ id: 'gemini-2.5-pro', contextWindow: 1_000_000, maxOutputTokens: 65_536, supportsTools: true, supportsStreaming: true, supportsImages: true, supportsReasoning: true },
|
|
33
|
+
{ id: 'gemini-2.5-flash', contextWindow: 1_000_000, maxOutputTokens: 65_536, supportsTools: true, supportsStreaming: true, supportsImages: true, supportsReasoning: true },
|
|
34
|
+
{ id: 'gemini-2.5-flash-lite', contextWindow: 1_000_000, maxOutputTokens: 65_536, supportsTools: true, supportsStreaming: true, supportsImages: true },
|
|
35
|
+
];
|