mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-26 07:57:40 +00:00
refactor(plugins): move auth and model policy to providers
This commit is contained in:
@@ -7,8 +7,16 @@ import {
|
||||
} from "../../src/test-utils/provider-usage-fetch.js";
|
||||
import googlePlugin from "./index.js";
|
||||
|
||||
function findProvider(providers: ProviderPlugin[], id: string): ProviderPlugin {
|
||||
const provider = providers.find((candidate) => candidate.id === id);
|
||||
if (!provider) {
|
||||
throw new Error(`provider ${id} missing`);
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
function registerGooglePlugin(): {
|
||||
provider: ProviderPlugin;
|
||||
providers: ProviderPlugin[];
|
||||
webSearchProvider: {
|
||||
id: string;
|
||||
envVars: string[];
|
||||
@@ -18,13 +26,12 @@ function registerGooglePlugin(): {
|
||||
} {
|
||||
const captured = createCapturedPluginRegistration();
|
||||
googlePlugin.register(captured.api);
|
||||
const provider = captured.providers[0];
|
||||
if (!provider) {
|
||||
if (captured.providers.length === 0) {
|
||||
throw new Error("provider registration missing");
|
||||
}
|
||||
const webSearchProvider = captured.webSearchProviders[0] ?? null;
|
||||
return {
|
||||
provider,
|
||||
providers: captured.providers,
|
||||
webSearchProviderRegistered: webSearchProvider !== null,
|
||||
webSearchProvider:
|
||||
webSearchProvider === null
|
||||
@@ -38,10 +45,13 @@ function registerGooglePlugin(): {
|
||||
}
|
||||
|
||||
describe("google plugin", () => {
|
||||
it("registers both Gemini CLI auth and Gemini web search", () => {
|
||||
it("registers Google direct, Gemini CLI auth, and Gemini web search", () => {
|
||||
const result = registerGooglePlugin();
|
||||
|
||||
expect(result.provider.id).toBe("google-gemini-cli");
|
||||
expect(result.providers.map((provider) => provider.id)).toEqual([
|
||||
"google",
|
||||
"google-gemini-cli",
|
||||
]);
|
||||
expect(result.webSearchProviderRegistered).toBe(true);
|
||||
expect(result.webSearchProvider).toMatchObject({
|
||||
id: "gemini",
|
||||
@@ -50,8 +60,43 @@ describe("google plugin", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("owns gemini 3.1 forward-compat resolution", () => {
|
||||
const { provider } = registerGooglePlugin();
|
||||
it("owns google direct gemini 3.1 forward-compat resolution", () => {
|
||||
const { providers } = registerGooglePlugin();
|
||||
const provider = findProvider(providers, "google");
|
||||
const model = provider.resolveDynamicModel?.({
|
||||
provider: "google",
|
||||
modelId: "gemini-3.1-pro-preview",
|
||||
modelRegistry: {
|
||||
find: (_provider: string, id: string) =>
|
||||
id === "gemini-3-pro-preview"
|
||||
? {
|
||||
id,
|
||||
name: id,
|
||||
api: "google-generative-ai",
|
||||
provider: "google",
|
||||
baseUrl: "https://generativelanguage.googleapis.com",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1_048_576,
|
||||
maxTokens: 65_536,
|
||||
}
|
||||
: null,
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(model).toMatchObject({
|
||||
id: "gemini-3.1-pro-preview",
|
||||
provider: "google",
|
||||
api: "google-generative-ai",
|
||||
baseUrl: "https://generativelanguage.googleapis.com",
|
||||
reasoning: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("owns gemini cli 3.1 forward-compat resolution", () => {
|
||||
const { providers } = registerGooglePlugin();
|
||||
const provider = findProvider(providers, "google-gemini-cli");
|
||||
const model = provider.resolveDynamicModel?.({
|
||||
provider: "google-gemini-cli",
|
||||
modelId: "gemini-3.1-pro-preview",
|
||||
@@ -82,7 +127,8 @@ describe("google plugin", () => {
|
||||
});
|
||||
|
||||
it("owns usage-token parsing", async () => {
|
||||
const { provider } = registerGooglePlugin();
|
||||
const { providers } = registerGooglePlugin();
|
||||
const provider = findProvider(providers, "google-gemini-cli");
|
||||
await expect(
|
||||
provider.resolveUsageAuth?.({
|
||||
config: {} as never,
|
||||
@@ -101,7 +147,8 @@ describe("google plugin", () => {
|
||||
});
|
||||
|
||||
it("owns usage snapshot fetching", async () => {
|
||||
const { provider } = registerGooglePlugin();
|
||||
const { providers } = registerGooglePlugin();
|
||||
const provider = findProvider(providers, "google-gemini-cli");
|
||||
const mockFetch = createProviderUsageFetch(async (url) => {
|
||||
if (url.includes("cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota")) {
|
||||
return makeResponse(200, {
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { normalizeModelCompat } from "../../src/agents/model-compat.js";
|
||||
import { fetchGeminiUsage } from "../../src/infra/provider-usage.fetch.js";
|
||||
import { buildOauthProviderAuthResult } from "../../src/plugin-sdk/provider-auth-result.js";
|
||||
import type {
|
||||
OpenClawPluginApi,
|
||||
ProviderAuthContext,
|
||||
ProviderFetchUsageSnapshotContext,
|
||||
ProviderResolveDynamicModelContext,
|
||||
ProviderRuntimeModel,
|
||||
} from "../../src/plugins/types.js";
|
||||
import { loginGeminiCliOAuth } from "./oauth.js";
|
||||
import { isModernGoogleModel, resolveGoogle31ForwardCompatModel } from "./provider-models.js";
|
||||
|
||||
const PROVIDER_ID = "google-gemini-cli";
|
||||
const PROVIDER_LABEL = "Gemini CLI OAuth";
|
||||
const DEFAULT_MODEL = "google-gemini-cli/gemini-3.1-pro-preview";
|
||||
const GEMINI_3_1_PRO_PREFIX = "gemini-3.1-pro";
|
||||
const GEMINI_3_1_FLASH_PREFIX = "gemini-3.1-flash";
|
||||
const GEMINI_3_1_PRO_TEMPLATE_IDS = ["gemini-3-pro-preview"] as const;
|
||||
const GEMINI_3_1_FLASH_TEMPLATE_IDS = ["gemini-3-flash-preview"] as const;
|
||||
const ENV_VARS = [
|
||||
"OPENCLAW_GEMINI_OAUTH_CLIENT_ID",
|
||||
"OPENCLAW_GEMINI_OAUTH_CLIENT_SECRET",
|
||||
@@ -24,30 +18,6 @@ const ENV_VARS = [
|
||||
"GEMINI_CLI_OAUTH_CLIENT_SECRET",
|
||||
];
|
||||
|
||||
function cloneFirstTemplateModel(params: {
|
||||
modelId: string;
|
||||
templateIds: readonly string[];
|
||||
ctx: ProviderResolveDynamicModelContext;
|
||||
}): ProviderRuntimeModel | undefined {
|
||||
const trimmedModelId = params.modelId.trim();
|
||||
for (const templateId of [...new Set(params.templateIds)].filter(Boolean)) {
|
||||
const template = params.ctx.modelRegistry.find(
|
||||
PROVIDER_ID,
|
||||
templateId,
|
||||
) as ProviderRuntimeModel | null;
|
||||
if (!template) {
|
||||
continue;
|
||||
}
|
||||
return normalizeModelCompat({
|
||||
...template,
|
||||
id: trimmedModelId,
|
||||
name: trimmedModelId,
|
||||
reasoning: true,
|
||||
} as ProviderRuntimeModel);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseGoogleUsageToken(apiKey: string): string {
|
||||
try {
|
||||
const parsed = JSON.parse(apiKey) as { token?: unknown };
|
||||
@@ -64,28 +34,6 @@ async function fetchGeminiCliUsage(ctx: ProviderFetchUsageSnapshotContext) {
|
||||
return await fetchGeminiUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn, PROVIDER_ID);
|
||||
}
|
||||
|
||||
function resolveGeminiCliForwardCompatModel(
|
||||
ctx: ProviderResolveDynamicModelContext,
|
||||
): ProviderRuntimeModel | undefined {
|
||||
const trimmed = ctx.modelId.trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
|
||||
let templateIds: readonly string[];
|
||||
if (lower.startsWith(GEMINI_3_1_PRO_PREFIX)) {
|
||||
templateIds = GEMINI_3_1_PRO_TEMPLATE_IDS;
|
||||
} else if (lower.startsWith(GEMINI_3_1_FLASH_PREFIX)) {
|
||||
templateIds = GEMINI_3_1_FLASH_TEMPLATE_IDS;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cloneFirstTemplateModel({
|
||||
modelId: trimmed,
|
||||
templateIds,
|
||||
ctx,
|
||||
});
|
||||
}
|
||||
|
||||
export function registerGoogleGeminiCliProvider(api: OpenClawPluginApi) {
|
||||
api.registerProvider({
|
||||
id: PROVIDER_ID,
|
||||
@@ -133,7 +81,9 @@ export function registerGoogleGeminiCliProvider(api: OpenClawPluginApi) {
|
||||
},
|
||||
},
|
||||
],
|
||||
resolveDynamicModel: (ctx) => resolveGeminiCliForwardCompatModel(ctx),
|
||||
resolveDynamicModel: (ctx) =>
|
||||
resolveGoogle31ForwardCompatModel({ providerId: PROVIDER_ID, ctx }),
|
||||
isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId),
|
||||
resolveUsageAuth: async (ctx) => {
|
||||
const auth = await ctx.resolveOAuthToken();
|
||||
if (!auth) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import { emptyPluginConfigSchema } from "../../src/plugins/config-schema.js";
|
||||
import type { OpenClawPluginApi } from "../../src/plugins/types.js";
|
||||
import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js";
|
||||
import { isModernGoogleModel, resolveGoogle31ForwardCompatModel } from "./provider-models.js";
|
||||
|
||||
const googlePlugin = {
|
||||
id: "google",
|
||||
@@ -13,6 +14,16 @@ const googlePlugin = {
|
||||
description: "Bundled Google plugin",
|
||||
configSchema: emptyPluginConfigSchema(),
|
||||
register(api: OpenClawPluginApi) {
|
||||
api.registerProvider({
|
||||
id: "google",
|
||||
label: "Google AI Studio",
|
||||
docsPath: "/providers/models",
|
||||
envVars: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
||||
auth: [],
|
||||
resolveDynamicModel: (ctx) =>
|
||||
resolveGoogle31ForwardCompatModel({ providerId: "google", ctx }),
|
||||
isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId),
|
||||
});
|
||||
registerGoogleGeminiCliProvider(api);
|
||||
api.registerWebSearchProvider(
|
||||
createPluginBackedWebSearchProvider({
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "google",
|
||||
"providers": ["google-gemini-cli"],
|
||||
"providers": ["google", "google-gemini-cli"],
|
||||
"providerAuthEnvVars": {
|
||||
"google": ["GEMINI_API_KEY", "GOOGLE_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
63
extensions/google/provider-models.ts
Normal file
63
extensions/google/provider-models.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { normalizeModelCompat } from "../../src/agents/model-compat.js";
|
||||
import type {
|
||||
ProviderResolveDynamicModelContext,
|
||||
ProviderRuntimeModel,
|
||||
} from "../../src/plugins/types.js";
|
||||
|
||||
const GEMINI_3_1_PRO_PREFIX = "gemini-3.1-pro";
|
||||
const GEMINI_3_1_FLASH_PREFIX = "gemini-3.1-flash";
|
||||
const GEMINI_3_1_PRO_TEMPLATE_IDS = ["gemini-3-pro-preview"] as const;
|
||||
const GEMINI_3_1_FLASH_TEMPLATE_IDS = ["gemini-3-flash-preview"] as const;
|
||||
|
||||
function cloneFirstTemplateModel(params: {
|
||||
providerId: string;
|
||||
modelId: string;
|
||||
templateIds: readonly string[];
|
||||
ctx: ProviderResolveDynamicModelContext;
|
||||
}): ProviderRuntimeModel | undefined {
|
||||
const trimmedModelId = params.modelId.trim();
|
||||
for (const templateId of [...new Set(params.templateIds)].filter(Boolean)) {
|
||||
const template = params.ctx.modelRegistry.find(
|
||||
params.providerId,
|
||||
templateId,
|
||||
) as ProviderRuntimeModel | null;
|
||||
if (!template) {
|
||||
continue;
|
||||
}
|
||||
return normalizeModelCompat({
|
||||
...template,
|
||||
id: trimmedModelId,
|
||||
name: trimmedModelId,
|
||||
reasoning: true,
|
||||
} as ProviderRuntimeModel);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveGoogle31ForwardCompatModel(params: {
|
||||
providerId: string;
|
||||
ctx: ProviderResolveDynamicModelContext;
|
||||
}): ProviderRuntimeModel | undefined {
|
||||
const trimmed = params.ctx.modelId.trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
|
||||
let templateIds: readonly string[];
|
||||
if (lower.startsWith(GEMINI_3_1_PRO_PREFIX)) {
|
||||
templateIds = GEMINI_3_1_PRO_TEMPLATE_IDS;
|
||||
} else if (lower.startsWith(GEMINI_3_1_FLASH_PREFIX)) {
|
||||
templateIds = GEMINI_3_1_FLASH_TEMPLATE_IDS;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cloneFirstTemplateModel({
|
||||
providerId: params.providerId,
|
||||
modelId: trimmed,
|
||||
templateIds,
|
||||
ctx: params.ctx,
|
||||
});
|
||||
}
|
||||
|
||||
export function isModernGoogleModel(modelId: string): boolean {
|
||||
return modelId.trim().toLowerCase().startsWith("gemini-3");
|
||||
}
|
||||
Reference in New Issue
Block a user