mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-24 15:41:40 +00:00
refactor(plugins): move auth profile hooks into providers
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
export {
|
||||
augmentModelCatalogWithProviderPlugins,
|
||||
buildProviderAuthDoctorHintWithPlugin,
|
||||
buildProviderMissingAuthMessageWithPlugin,
|
||||
formatProviderAuthProfileApiKeyWithPlugin,
|
||||
refreshProviderOAuthCredentialWithPlugin,
|
||||
} from "./provider-runtime.js";
|
||||
|
||||
@@ -14,7 +14,9 @@ vi.mock("./providers.js", () => ({
|
||||
|
||||
import {
|
||||
augmentModelCatalogWithProviderPlugins,
|
||||
buildProviderAuthDoctorHintWithPlugin,
|
||||
buildProviderMissingAuthMessageWithPlugin,
|
||||
formatProviderAuthProfileApiKeyWithPlugin,
|
||||
prepareProviderExtraParams,
|
||||
resolveProviderCacheTtlEligibility,
|
||||
resolveProviderBinaryThinking,
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
normalizeProviderResolvedModelWithPlugin,
|
||||
prepareProviderDynamicModel,
|
||||
prepareProviderRuntimeAuth,
|
||||
refreshProviderOAuthCredentialWithPlugin,
|
||||
resolveProviderRuntimePlugin,
|
||||
runProviderDynamicModel,
|
||||
wrapProviderStreamFn,
|
||||
@@ -87,6 +90,10 @@ describe("provider-runtime", () => {
|
||||
baseUrl: "https://runtime.example.com/v1",
|
||||
expiresAt: 123,
|
||||
}));
|
||||
const refreshOAuth = vi.fn(async (cred) => ({
|
||||
...cred,
|
||||
access: "refreshed-access-token",
|
||||
}));
|
||||
const resolveUsageAuth = vi.fn(async () => ({
|
||||
token: "usage-token",
|
||||
accountId: "usage-account",
|
||||
@@ -96,34 +103,7 @@ describe("provider-runtime", () => {
|
||||
displayName: "Demo",
|
||||
windows: [{ label: "Day", usedPercent: 25 }],
|
||||
}));
|
||||
resolvePluginProvidersMock.mockImplementation((params: unknown) => {
|
||||
const scopedParams = params as { onlyPluginIds?: string[] } | undefined;
|
||||
if (scopedParams?.onlyPluginIds?.includes("openai")) {
|
||||
return [
|
||||
{
|
||||
id: "openai",
|
||||
label: "OpenAI",
|
||||
auth: [],
|
||||
buildMissingAuthMessage: () =>
|
||||
'No API key found for provider "openai". Use openai-codex/gpt-5.4.',
|
||||
suppressBuiltInModel: ({ provider, modelId }) =>
|
||||
provider === "azure-openai-responses" && modelId === "gpt-5.3-codex-spark"
|
||||
? { suppress: true, errorMessage: "openai-codex/gpt-5.3-codex-spark" }
|
||||
: undefined,
|
||||
augmentModelCatalog: () => [
|
||||
{ provider: "openai", id: "gpt-5.4", name: "gpt-5.4" },
|
||||
{ provider: "openai", id: "gpt-5.4-pro", name: "gpt-5.4-pro" },
|
||||
{ provider: "openai-codex", id: "gpt-5.4", name: "gpt-5.4" },
|
||||
{
|
||||
provider: "openai-codex",
|
||||
id: "gpt-5.3-codex-spark",
|
||||
name: "gpt-5.3-codex-spark",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
resolvePluginProvidersMock.mockImplementation((_params: unknown) => {
|
||||
return [
|
||||
{
|
||||
id: "demo",
|
||||
@@ -143,6 +123,11 @@ describe("provider-runtime", () => {
|
||||
...model,
|
||||
api: "openai-codex-responses",
|
||||
}),
|
||||
formatApiKey: (cred) =>
|
||||
cred.type === "oauth" ? JSON.stringify({ token: cred.access }) : "",
|
||||
refreshOAuth,
|
||||
buildAuthDoctorHint: ({ provider, profileId }) =>
|
||||
provider === "demo" ? `Repair ${profileId}` : undefined,
|
||||
prepareRuntimeAuth,
|
||||
resolveUsageAuth,
|
||||
fetchUsageSnapshot,
|
||||
@@ -152,6 +137,27 @@ describe("provider-runtime", () => {
|
||||
resolveDefaultThinkingLevel: ({ reasoning }) => (reasoning ? "low" : "off"),
|
||||
isModernModelRef: ({ modelId }) => modelId.startsWith("gpt-5"),
|
||||
},
|
||||
{
|
||||
id: "openai",
|
||||
label: "OpenAI",
|
||||
auth: [],
|
||||
buildMissingAuthMessage: () =>
|
||||
'No API key found for provider "openai". Use openai-codex/gpt-5.4.',
|
||||
suppressBuiltInModel: ({ provider, modelId }) =>
|
||||
provider === "azure-openai-responses" && modelId === "gpt-5.3-codex-spark"
|
||||
? { suppress: true, errorMessage: "openai-codex/gpt-5.3-codex-spark" }
|
||||
: undefined,
|
||||
augmentModelCatalog: () => [
|
||||
{ provider: "openai", id: "gpt-5.4", name: "gpt-5.4" },
|
||||
{ provider: "openai", id: "gpt-5.4-pro", name: "gpt-5.4-pro" },
|
||||
{ provider: "openai-codex", id: "gpt-5.4", name: "gpt-5.4" },
|
||||
{
|
||||
provider: "openai-codex",
|
||||
id: "gpt-5.3-codex-spark",
|
||||
name: "gpt-5.3-codex-spark",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@@ -241,6 +247,45 @@ describe("provider-runtime", () => {
|
||||
expiresAt: 123,
|
||||
});
|
||||
|
||||
expect(
|
||||
formatProviderAuthProfileApiKeyWithPlugin({
|
||||
provider: "demo",
|
||||
context: {
|
||||
type: "oauth",
|
||||
provider: "demo",
|
||||
access: "oauth-access",
|
||||
refresh: "oauth-refresh",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
}),
|
||||
).toBe('{"token":"oauth-access"}');
|
||||
|
||||
await expect(
|
||||
refreshProviderOAuthCredentialWithPlugin({
|
||||
provider: "demo",
|
||||
context: {
|
||||
type: "oauth",
|
||||
provider: "demo",
|
||||
access: "oauth-access",
|
||||
refresh: "oauth-refresh",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
access: "refreshed-access-token",
|
||||
});
|
||||
|
||||
await expect(
|
||||
buildProviderAuthDoctorHintWithPlugin({
|
||||
provider: "demo",
|
||||
context: {
|
||||
provider: "demo",
|
||||
profileId: "demo:default",
|
||||
store: { version: 1, profiles: {} },
|
||||
},
|
||||
}),
|
||||
).resolves.toBe("Repair demo:default");
|
||||
|
||||
await expect(
|
||||
resolveProviderUsageAuthWithPlugin({
|
||||
provider: "demo",
|
||||
@@ -376,12 +421,8 @@ describe("provider-runtime", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["openai"],
|
||||
}),
|
||||
);
|
||||
expect(prepareDynamicModel).toHaveBeenCalledTimes(1);
|
||||
expect(refreshOAuth).toHaveBeenCalledTimes(1);
|
||||
expect(prepareRuntimeAuth).toHaveBeenCalledTimes(1);
|
||||
expect(resolveUsageAuth).toHaveBeenCalledTimes(1);
|
||||
expect(fetchUsageSnapshot).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { AuthProfileCredential, OAuthCredential } from "../agents/auth-profiles/types.js";
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveOwningPluginIdsForProvider, resolvePluginProviders } from "./providers.js";
|
||||
import type {
|
||||
ProviderAuthDoctorHintContext,
|
||||
ProviderAugmentModelCatalogContext,
|
||||
ProviderBuildMissingAuthMessageContext,
|
||||
ProviderBuiltInModelSuppressionContext,
|
||||
@@ -46,19 +48,6 @@ function resolveProviderPluginsForHooks(params: {
|
||||
});
|
||||
}
|
||||
|
||||
const GLOBAL_PROVIDER_HOOK_PLUGIN_IDS = ["openai"] as const;
|
||||
|
||||
function resolveGlobalProviderHookPlugins(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ProviderPlugin[] {
|
||||
return resolveProviderPluginsForHooks({
|
||||
...params,
|
||||
onlyPluginIds: [...GLOBAL_PROVIDER_HOOK_PLUGIN_IDS],
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveProviderRuntimePlugin(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
@@ -174,6 +163,36 @@ export async function resolveProviderUsageSnapshotWithPlugin(params: {
|
||||
return await resolveProviderRuntimePlugin(params)?.fetchUsageSnapshot?.(params.context);
|
||||
}
|
||||
|
||||
export function formatProviderAuthProfileApiKeyWithPlugin(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: AuthProfileCredential;
|
||||
}) {
|
||||
return resolveProviderRuntimePlugin(params)?.formatApiKey?.(params.context);
|
||||
}
|
||||
|
||||
export async function refreshProviderOAuthCredentialWithPlugin(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: OAuthCredential;
|
||||
}) {
|
||||
return await resolveProviderRuntimePlugin(params)?.refreshOAuth?.(params.context);
|
||||
}
|
||||
|
||||
export async function buildProviderAuthDoctorHintWithPlugin(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderAuthDoctorHintContext;
|
||||
}) {
|
||||
return await resolveProviderRuntimePlugin(params)?.buildAuthDoctorHint?.(params.context);
|
||||
}
|
||||
|
||||
export function resolveProviderCacheTtlEligibility(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
@@ -231,10 +250,9 @@ export function buildProviderMissingAuthMessageWithPlugin(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderBuildMissingAuthMessageContext;
|
||||
}) {
|
||||
const plugin = resolveGlobalProviderHookPlugins(params).find((providerPlugin) =>
|
||||
matchesProviderId(providerPlugin, params.provider),
|
||||
return (
|
||||
resolveProviderRuntimePlugin(params)?.buildMissingAuthMessage?.(params.context) ?? undefined
|
||||
);
|
||||
return plugin?.buildMissingAuthMessage?.(params.context) ?? undefined;
|
||||
}
|
||||
|
||||
export function resolveProviderBuiltInModelSuppression(params: {
|
||||
@@ -243,7 +261,7 @@ export function resolveProviderBuiltInModelSuppression(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderBuiltInModelSuppressionContext;
|
||||
}) {
|
||||
for (const plugin of resolveGlobalProviderHookPlugins(params)) {
|
||||
for (const plugin of resolveProviderPluginsForHooks(params)) {
|
||||
const result = plugin.suppressBuiltInModel?.(params.context);
|
||||
if (result?.suppress) {
|
||||
return result;
|
||||
@@ -259,7 +277,7 @@ export async function augmentModelCatalogWithProviderPlugins(params: {
|
||||
context: ProviderAugmentModelCatalogContext;
|
||||
}) {
|
||||
const supplemental = [] as ProviderAugmentModelCatalogContext["entries"];
|
||||
for (const plugin of resolveGlobalProviderHookPlugins(params)) {
|
||||
for (const plugin of resolveProviderPluginsForHooks(params)) {
|
||||
const next = await plugin.augmentModelCatalog?.(params.context);
|
||||
if (!next || next.length === 0) {
|
||||
continue;
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
ApiKeyCredential,
|
||||
AuthProfileCredential,
|
||||
OAuthCredential,
|
||||
AuthProfileStore,
|
||||
} from "../agents/auth-profiles/types.js";
|
||||
import type { ModelCatalogEntry } from "../agents/model-catalog.js";
|
||||
import type { ProviderCapabilities } from "../agents/provider-capabilities.js";
|
||||
@@ -367,6 +368,20 @@ export type ProviderFetchUsageSnapshotContext = {
|
||||
fetchFn: typeof fetch;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider-owned auth-doctor hint input.
|
||||
*
|
||||
* Called when OAuth refresh fails and OpenClaw wants a provider-specific repair
|
||||
* hint to append to the generic re-auth message. Use this for legacy profile-id
|
||||
* migrations or other provider-owned auth-store cleanup guidance.
|
||||
*/
|
||||
export type ProviderAuthDoctorHintContext = {
|
||||
config?: OpenClawConfig;
|
||||
store: AuthProfileStore;
|
||||
provider: string;
|
||||
profileId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider-owned extra-param normalization before OpenClaw builds its generic
|
||||
* stream option wrapper.
|
||||
@@ -732,8 +747,34 @@ export type ProviderPlugin = {
|
||||
*/
|
||||
isModernModelRef?: (ctx: ProviderModernModelPolicyContext) => boolean | undefined;
|
||||
wizard?: ProviderPluginWizard;
|
||||
/**
|
||||
* Provider-owned auth-profile API-key formatter.
|
||||
*
|
||||
* OpenClaw uses this when a stored auth profile is already valid and needs to
|
||||
* be converted into the runtime `apiKey` string expected by the provider. Use
|
||||
* this for providers whose auth profile stores extra metadata alongside the
|
||||
* bearer token (for example Gemini CLI's `{ token, projectId }` payload).
|
||||
*/
|
||||
formatApiKey?: (cred: AuthProfileCredential) => string;
|
||||
/**
|
||||
* Provider-owned OAuth refresh.
|
||||
*
|
||||
* OpenClaw calls this before falling back to the shared `pi-ai` OAuth
|
||||
* refreshers. Use it when the provider has a custom refresh endpoint, or when
|
||||
* the provider needs custom refresh-failure behavior that should stay out of
|
||||
* core auth-profile code.
|
||||
*/
|
||||
refreshOAuth?: (cred: OAuthCredential) => Promise<OAuthCredential>;
|
||||
/**
|
||||
* Provider-owned auth-doctor hint.
|
||||
*
|
||||
* Return a multiline repair hint when OAuth refresh fails and the provider
|
||||
* wants to steer users toward a specific auth-profile migration or recovery
|
||||
* path. Return nothing to keep OpenClaw's generic error text.
|
||||
*/
|
||||
buildAuthDoctorHint?: (
|
||||
ctx: ProviderAuthDoctorHintContext,
|
||||
) => string | Promise<string | null | undefined> | null | undefined;
|
||||
onModelSelected?: (ctx: ProviderModelSelectedContext) => Promise<void>;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user