mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:10:49 +00:00
refactor: share provider selection runtime helper
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
64c5f94fe0234da8ae2312ab30694ebc5675091fadebac92c106210f45a66e91 plugin-sdk-api-baseline.json
|
||||
fd00bb4cd8f1e32503f94e8542db95235ec641eb62ae45d6d4b653d9ff60cb09 plugin-sdk-api-baseline.jsonl
|
||||
1d2767b688414ac41305e88c830858c00947e2d7c713f1a25d86f38cd577620e plugin-sdk-api-baseline.json
|
||||
e5167477ab6aa2e67bd4361048cf5f6f8fd1cb7ee570544c634d14417f890674 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -273,6 +273,7 @@ Current bundled provider examples:
|
||||
| `plugin-sdk/provider-auth-api-key` | Provider API-key setup helpers | API-key onboarding/profile-write helpers |
|
||||
| `plugin-sdk/provider-auth-result` | Provider auth-result helpers | Standard OAuth auth-result builder |
|
||||
| `plugin-sdk/provider-auth-login` | Provider interactive login helpers | Shared interactive login helpers |
|
||||
| `plugin-sdk/provider-selection-runtime` | Provider selection helpers | Configured-or-auto provider selection and raw provider config merging |
|
||||
| `plugin-sdk/provider-env-vars` | Provider env-var helpers | Provider auth env-var lookup helpers |
|
||||
| `plugin-sdk/provider-model-shared` | Shared provider model/replay helpers | `ProviderReplayFamily`, `buildProviderReplayFamilyHooks`, `normalizeModelCompat`, shared replay-policy builders, provider-endpoint helpers, and model-id normalization helpers |
|
||||
| `plugin-sdk/provider-catalog-shared` | Shared provider catalog helpers | `findCatalogTemplate`, `buildSingleProviderApiKeyCatalog`, `supportsNativeStreamingUsageCompat`, `applyProviderNativeStreamingUsageCompat` |
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { resolveProviderRawConfig, selectConfiguredOrAutoProvider } from "./provider-selection.js";
|
||||
|
||||
type AutoSelectableProvider = {
|
||||
id: string;
|
||||
autoSelectOrder?: number;
|
||||
};
|
||||
|
||||
export type ResolvedConfiguredProvider<TProvider, TConfig> =
|
||||
| {
|
||||
ok: true;
|
||||
configuredProviderId?: string;
|
||||
provider: TProvider;
|
||||
providerConfig: TConfig;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
code: "missing-configured-provider" | "no-registered-provider" | "provider-not-configured";
|
||||
configuredProviderId?: string;
|
||||
provider?: TProvider;
|
||||
};
|
||||
|
||||
export function resolveConfiguredCapabilityProvider<
|
||||
TConfig,
|
||||
TFullConfig,
|
||||
TProvider extends AutoSelectableProvider,
|
||||
>(params: {
|
||||
configuredProviderId?: string;
|
||||
providerConfigs?: Record<string, Record<string, unknown> | undefined>;
|
||||
cfg: TFullConfig | undefined;
|
||||
cfgForResolve: TFullConfig;
|
||||
getConfiguredProvider: (providerId: string | undefined) => TProvider | undefined;
|
||||
listProviders: () => Iterable<TProvider>;
|
||||
resolveProviderConfig: (params: {
|
||||
provider: TProvider;
|
||||
cfg: TFullConfig;
|
||||
rawConfig: Record<string, unknown>;
|
||||
}) => TConfig;
|
||||
isProviderConfigured: (params: {
|
||||
provider: TProvider;
|
||||
cfg: TFullConfig | undefined;
|
||||
providerConfig: TConfig;
|
||||
}) => boolean;
|
||||
}): ResolvedConfiguredProvider<TProvider, TConfig> {
|
||||
const selection = selectConfiguredOrAutoProvider({
|
||||
configuredProviderId: params.configuredProviderId,
|
||||
getConfiguredProvider: params.getConfiguredProvider,
|
||||
listProviders: params.listProviders,
|
||||
});
|
||||
if (selection.missingConfiguredProvider) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "missing-configured-provider",
|
||||
configuredProviderId: selection.configuredProviderId,
|
||||
};
|
||||
}
|
||||
|
||||
const provider = selection.provider;
|
||||
if (!provider) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "no-registered-provider",
|
||||
configuredProviderId: selection.configuredProviderId,
|
||||
};
|
||||
}
|
||||
|
||||
const rawProviderConfig = resolveProviderRawConfig({
|
||||
providerId: provider.id,
|
||||
configuredProviderId: selection.configuredProviderId,
|
||||
providerConfigs: params.providerConfigs,
|
||||
});
|
||||
const providerConfig = params.resolveProviderConfig({
|
||||
provider,
|
||||
cfg: params.cfgForResolve,
|
||||
rawConfig: rawProviderConfig,
|
||||
});
|
||||
|
||||
if (!params.isProviderConfigured({ provider, cfg: params.cfg, providerConfig })) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "provider-not-configured",
|
||||
configuredProviderId: selection.configuredProviderId,
|
||||
provider,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
configuredProviderId: selection.configuredProviderId,
|
||||
provider,
|
||||
providerConfig,
|
||||
};
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
type AutoSelectableProvider = {
|
||||
autoSelectOrder?: number;
|
||||
};
|
||||
|
||||
export function selectConfiguredOrAutoProvider<TProvider extends AutoSelectableProvider>(params: {
|
||||
configuredProviderId?: string;
|
||||
getConfiguredProvider: (providerId: string | undefined) => TProvider | undefined;
|
||||
listProviders: () => Iterable<TProvider>;
|
||||
}): {
|
||||
configuredProviderId?: string;
|
||||
missingConfiguredProvider: boolean;
|
||||
provider: TProvider | undefined;
|
||||
} {
|
||||
const configuredProviderId = normalizeOptionalString(params.configuredProviderId);
|
||||
const configuredProvider = params.getConfiguredProvider(configuredProviderId);
|
||||
|
||||
if (configuredProviderId && !configuredProvider) {
|
||||
return {
|
||||
configuredProviderId,
|
||||
missingConfiguredProvider: true,
|
||||
provider: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
configuredProviderId,
|
||||
missingConfiguredProvider: false,
|
||||
provider:
|
||||
configuredProvider ??
|
||||
[...params.listProviders()].toSorted(
|
||||
(left, right) =>
|
||||
(left.autoSelectOrder ?? Number.MAX_SAFE_INTEGER) -
|
||||
(right.autoSelectOrder ?? Number.MAX_SAFE_INTEGER),
|
||||
)[0],
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveProviderRawConfig(params: {
|
||||
providerId: string;
|
||||
configuredProviderId?: string;
|
||||
providerConfigs?: Record<string, Record<string, unknown> | undefined>;
|
||||
}): Record<string, unknown> {
|
||||
const canonicalProviderConfig =
|
||||
params.providerConfigs?.[params.providerId] &&
|
||||
typeof params.providerConfigs[params.providerId] === "object"
|
||||
? (params.providerConfigs[params.providerId] as Record<string, unknown>)
|
||||
: undefined;
|
||||
const selectedProviderConfig =
|
||||
params.configuredProviderId &&
|
||||
params.providerConfigs?.[params.configuredProviderId] &&
|
||||
typeof params.providerConfigs[params.configuredProviderId] === "object"
|
||||
? (params.providerConfigs[params.configuredProviderId] as Record<string, unknown>)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...canonicalProviderConfig,
|
||||
...selectedProviderConfig,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { resolveConfiguredCapabilityProvider } from "openclaw/plugin-sdk/provider-selection-runtime";
|
||||
import type {
|
||||
RealtimeVoiceProviderConfig,
|
||||
RealtimeVoiceProviderPlugin,
|
||||
@@ -8,7 +9,6 @@ import type { VoiceCallConfig } from "./config.js";
|
||||
import { resolveVoiceCallConfig, validateProviderConfig } from "./config.js";
|
||||
import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js";
|
||||
import { CallManager } from "./manager.js";
|
||||
import { resolveConfiguredCapabilityProvider } from "./provider-runtime-resolution.js";
|
||||
import type { VoiceCallProvider } from "./providers/base.js";
|
||||
import type { TwilioProvider } from "./providers/twilio.js";
|
||||
import type { TelephonyTtsRuntime } from "./telephony-tts.js";
|
||||
|
||||
@@ -191,7 +191,7 @@ describe("VoiceCallWebhookServer realtime transcription provider selection", ()
|
||||
const server = new VoiceCallWebhookServer(config, manager, provider);
|
||||
try {
|
||||
await server.start();
|
||||
expect(mocks.getRealtimeTranscriptionProvider).toHaveBeenCalledWith(undefined, null);
|
||||
expect(mocks.getRealtimeTranscriptionProvider).not.toHaveBeenCalled();
|
||||
expect(mocks.listRealtimeTranscriptionProviders).toHaveBeenCalledWith(null);
|
||||
expect(server.getMediaStreamHandler()).toBeTruthy();
|
||||
} finally {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import http from "node:http";
|
||||
import { URL } from "node:url";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { resolveConfiguredCapabilityProvider } from "openclaw/plugin-sdk/provider-selection-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
createWebhookInFlightLimiter,
|
||||
@@ -18,7 +19,6 @@ import { getHeader } from "./http-headers.js";
|
||||
import type { CallManager } from "./manager.js";
|
||||
import type { MediaStreamConfig } from "./media-stream.js";
|
||||
import { MediaStreamHandler } from "./media-stream.js";
|
||||
import { resolveConfiguredCapabilityProvider } from "./provider-runtime-resolution.js";
|
||||
import type { VoiceCallProvider } from "./providers/base.js";
|
||||
import { isProviderStatusTerminal } from "./providers/shared/call-status.js";
|
||||
import type { TwilioProvider } from "./providers/twilio.js";
|
||||
|
||||
@@ -1025,6 +1025,10 @@
|
||||
"types": "./dist/plugin-sdk/provider-auth-login.d.ts",
|
||||
"default": "./dist/plugin-sdk/provider-auth-login.js"
|
||||
},
|
||||
"./plugin-sdk/provider-selection-runtime": {
|
||||
"types": "./dist/plugin-sdk/provider-selection-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/provider-selection-runtime.js"
|
||||
},
|
||||
"./plugin-sdk/plugin-entry": {
|
||||
"types": "./dist/plugin-sdk/plugin-entry.d.ts",
|
||||
"default": "./dist/plugin-sdk/plugin-entry.js"
|
||||
|
||||
@@ -77,6 +77,9 @@ export const pluginSdkDocMetadata = {
|
||||
"provider-onboard": {
|
||||
category: "provider",
|
||||
},
|
||||
"provider-selection-runtime": {
|
||||
category: "provider",
|
||||
},
|
||||
opencode: {
|
||||
category: "provider",
|
||||
},
|
||||
|
||||
@@ -242,6 +242,7 @@
|
||||
"provider-auth-api-key",
|
||||
"provider-auth-result",
|
||||
"provider-auth-login",
|
||||
"provider-selection-runtime",
|
||||
"plugin-entry",
|
||||
"provider-catalog-shared",
|
||||
"provider-entry",
|
||||
|
||||
86
src/plugin-sdk/provider-selection-runtime.test.ts
Normal file
86
src/plugin-sdk/provider-selection-runtime.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
resolveConfiguredCapabilityProvider,
|
||||
resolveProviderRawConfig,
|
||||
selectConfiguredOrAutoProvider,
|
||||
type AutoSelectableProvider,
|
||||
} from "./provider-selection-runtime.js";
|
||||
|
||||
type TestProvider = AutoSelectableProvider & {
|
||||
configured?: boolean;
|
||||
};
|
||||
|
||||
describe("plugin-sdk provider-selection-runtime", () => {
|
||||
const providers: TestProvider[] = [
|
||||
{ id: "first", autoSelectOrder: 1 },
|
||||
{ id: "second", autoSelectOrder: 2, configured: true },
|
||||
];
|
||||
|
||||
it("selects an explicit provider when it exists", () => {
|
||||
const selection = selectConfiguredOrAutoProvider({
|
||||
configuredProviderId: " second ",
|
||||
getConfiguredProvider: (providerId) => providers.find((entry) => entry.id === providerId),
|
||||
listProviders: () => providers,
|
||||
});
|
||||
|
||||
expect(selection).toEqual({
|
||||
configuredProviderId: "second",
|
||||
missingConfiguredProvider: false,
|
||||
provider: providers[1],
|
||||
});
|
||||
});
|
||||
|
||||
it("reports a missing explicit provider", () => {
|
||||
const resolution = resolveConfiguredCapabilityProvider({
|
||||
configuredProviderId: "missing",
|
||||
cfg: {},
|
||||
cfgForResolve: {},
|
||||
getConfiguredProvider: (providerId) => providers.find((entry) => entry.id === providerId),
|
||||
listProviders: () => providers,
|
||||
resolveProviderConfig: ({ rawConfig }) => rawConfig,
|
||||
isProviderConfigured: ({ provider }) => provider.configured === true,
|
||||
});
|
||||
|
||||
expect(resolution).toEqual({
|
||||
ok: false,
|
||||
code: "missing-configured-provider",
|
||||
configuredProviderId: "missing",
|
||||
});
|
||||
});
|
||||
|
||||
it("auto-selects the first configured provider by order", () => {
|
||||
const resolution = resolveConfiguredCapabilityProvider({
|
||||
cfg: {},
|
||||
cfgForResolve: {},
|
||||
getConfiguredProvider: (providerId) => providers.find((entry) => entry.id === providerId),
|
||||
listProviders: () => providers,
|
||||
resolveProviderConfig: ({ provider, rawConfig }) => ({
|
||||
...rawConfig,
|
||||
providerId: provider.id,
|
||||
}),
|
||||
isProviderConfigured: ({ providerConfig }) => providerConfig.providerId === "second",
|
||||
});
|
||||
|
||||
expect(resolution).toMatchObject({
|
||||
ok: true,
|
||||
provider: providers[1],
|
||||
providerConfig: { providerId: "second" },
|
||||
});
|
||||
});
|
||||
|
||||
it("merges canonical and selected provider config", () => {
|
||||
expect(
|
||||
resolveProviderRawConfig({
|
||||
providerId: "canonical",
|
||||
configuredProviderId: "alias",
|
||||
providerConfigs: {
|
||||
canonical: { apiKey: "default", model: "base" },
|
||||
alias: { model: "alias-model" },
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
apiKey: "default",
|
||||
model: "alias-model",
|
||||
});
|
||||
});
|
||||
});
|
||||
208
src/plugin-sdk/provider-selection-runtime.ts
Normal file
208
src/plugin-sdk/provider-selection-runtime.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
|
||||
export type AutoSelectableProvider = {
|
||||
id: string;
|
||||
autoSelectOrder?: number;
|
||||
};
|
||||
|
||||
export type ProviderSelection<TProvider> = {
|
||||
configuredProviderId?: string;
|
||||
missingConfiguredProvider: boolean;
|
||||
provider: TProvider | undefined;
|
||||
};
|
||||
|
||||
export type ResolvedConfiguredProvider<TProvider, TConfig> =
|
||||
| {
|
||||
ok: true;
|
||||
configuredProviderId?: string;
|
||||
provider: TProvider;
|
||||
providerConfig: TConfig;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
code: "missing-configured-provider" | "no-registered-provider" | "provider-not-configured";
|
||||
configuredProviderId?: string;
|
||||
provider?: TProvider;
|
||||
};
|
||||
|
||||
export function selectConfiguredOrAutoProvider<TProvider extends AutoSelectableProvider>(params: {
|
||||
configuredProviderId?: string;
|
||||
getConfiguredProvider: (providerId: string | undefined) => TProvider | undefined;
|
||||
listProviders: () => Iterable<TProvider>;
|
||||
}): ProviderSelection<TProvider> {
|
||||
const configuredProviderId = normalizeOptionalString(params.configuredProviderId);
|
||||
const configuredProvider = configuredProviderId
|
||||
? params.getConfiguredProvider(configuredProviderId)
|
||||
: undefined;
|
||||
|
||||
if (configuredProviderId && !configuredProvider) {
|
||||
return {
|
||||
configuredProviderId,
|
||||
missingConfiguredProvider: true,
|
||||
provider: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
configuredProviderId,
|
||||
missingConfiguredProvider: false,
|
||||
provider:
|
||||
configuredProvider ?? [...params.listProviders()].toSorted(compareProviderAutoSelectOrder)[0],
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveProviderRawConfig(params: {
|
||||
providerId: string;
|
||||
configuredProviderId?: string;
|
||||
providerConfigs?: Record<string, Record<string, unknown> | undefined>;
|
||||
}): Record<string, unknown> {
|
||||
const canonicalProviderConfig = readProviderConfig(params.providerConfigs, params.providerId);
|
||||
const selectedProviderConfig = readProviderConfig(
|
||||
params.providerConfigs,
|
||||
params.configuredProviderId,
|
||||
);
|
||||
|
||||
return {
|
||||
...canonicalProviderConfig,
|
||||
...selectedProviderConfig,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveConfiguredCapabilityProvider<
|
||||
TConfig,
|
||||
TFullConfig,
|
||||
TProvider extends AutoSelectableProvider,
|
||||
>(params: {
|
||||
configuredProviderId?: string;
|
||||
providerConfigs?: Record<string, Record<string, unknown> | undefined>;
|
||||
cfg: TFullConfig | undefined;
|
||||
cfgForResolve: TFullConfig;
|
||||
getConfiguredProvider: (providerId: string | undefined) => TProvider | undefined;
|
||||
listProviders: () => Iterable<TProvider>;
|
||||
resolveProviderConfig: (params: {
|
||||
provider: TProvider;
|
||||
cfg: TFullConfig;
|
||||
rawConfig: Record<string, unknown>;
|
||||
}) => TConfig;
|
||||
isProviderConfigured: (params: {
|
||||
provider: TProvider;
|
||||
cfg: TFullConfig | undefined;
|
||||
providerConfig: TConfig;
|
||||
}) => boolean;
|
||||
}): ResolvedConfiguredProvider<TProvider, TConfig> {
|
||||
const configuredProviderId = normalizeOptionalString(params.configuredProviderId);
|
||||
if (configuredProviderId) {
|
||||
const provider = params.getConfiguredProvider(configuredProviderId);
|
||||
if (!provider) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "missing-configured-provider",
|
||||
configuredProviderId,
|
||||
};
|
||||
}
|
||||
|
||||
return resolveProviderCandidate({
|
||||
...params,
|
||||
configuredProviderId,
|
||||
provider,
|
||||
});
|
||||
}
|
||||
|
||||
const providers = [...params.listProviders()].toSorted(compareProviderAutoSelectOrder);
|
||||
if (providers.length === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "no-registered-provider",
|
||||
};
|
||||
}
|
||||
|
||||
let firstUnconfigured: TProvider | undefined;
|
||||
for (const provider of providers) {
|
||||
const resolution = resolveProviderCandidate({
|
||||
...params,
|
||||
provider,
|
||||
});
|
||||
if (resolution.ok) {
|
||||
return resolution;
|
||||
}
|
||||
firstUnconfigured ??= provider;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
code: "provider-not-configured",
|
||||
provider: firstUnconfigured,
|
||||
};
|
||||
}
|
||||
|
||||
function compareProviderAutoSelectOrder<TProvider extends AutoSelectableProvider>(
|
||||
left: TProvider,
|
||||
right: TProvider,
|
||||
): number {
|
||||
return (
|
||||
(left.autoSelectOrder ?? Number.MAX_SAFE_INTEGER) -
|
||||
(right.autoSelectOrder ?? Number.MAX_SAFE_INTEGER)
|
||||
);
|
||||
}
|
||||
|
||||
function readProviderConfig(
|
||||
providerConfigs: Record<string, Record<string, unknown> | undefined> | undefined,
|
||||
providerId: string | undefined,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
const providerConfig = providerConfigs?.[providerId];
|
||||
return providerConfig && typeof providerConfig === "object" ? providerConfig : undefined;
|
||||
}
|
||||
|
||||
function resolveProviderCandidate<
|
||||
TConfig,
|
||||
TFullConfig,
|
||||
TProvider extends AutoSelectableProvider,
|
||||
>(params: {
|
||||
configuredProviderId?: string;
|
||||
providerConfigs?: Record<string, Record<string, unknown> | undefined>;
|
||||
cfg: TFullConfig | undefined;
|
||||
cfgForResolve: TFullConfig;
|
||||
provider: TProvider;
|
||||
resolveProviderConfig: (params: {
|
||||
provider: TProvider;
|
||||
cfg: TFullConfig;
|
||||
rawConfig: Record<string, unknown>;
|
||||
}) => TConfig;
|
||||
isProviderConfigured: (params: {
|
||||
provider: TProvider;
|
||||
cfg: TFullConfig | undefined;
|
||||
providerConfig: TConfig;
|
||||
}) => boolean;
|
||||
}): ResolvedConfiguredProvider<TProvider, TConfig> {
|
||||
const rawProviderConfig = resolveProviderRawConfig({
|
||||
providerId: params.provider.id,
|
||||
configuredProviderId: params.configuredProviderId,
|
||||
providerConfigs: params.providerConfigs,
|
||||
});
|
||||
const providerConfig = params.resolveProviderConfig({
|
||||
provider: params.provider,
|
||||
cfg: params.cfgForResolve,
|
||||
rawConfig: rawProviderConfig,
|
||||
});
|
||||
|
||||
if (
|
||||
!params.isProviderConfigured({ provider: params.provider, cfg: params.cfg, providerConfig })
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "provider-not-configured",
|
||||
configuredProviderId: params.configuredProviderId,
|
||||
provider: params.provider,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
configuredProviderId: params.configuredProviderId,
|
||||
provider: params.provider,
|
||||
providerConfig,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user