refactor: add non-interactive provider plugin setup
This commit is contained in:
@@ -4,8 +4,10 @@ import {
|
|||||||
ensureOllamaModelPulled,
|
ensureOllamaModelPulled,
|
||||||
OLLAMA_DEFAULT_BASE_URL,
|
OLLAMA_DEFAULT_BASE_URL,
|
||||||
promptAndConfigureOllama,
|
promptAndConfigureOllama,
|
||||||
|
configureOllamaNonInteractive,
|
||||||
type OpenClawPluginApi,
|
type OpenClawPluginApi,
|
||||||
type ProviderAuthContext,
|
type ProviderAuthContext,
|
||||||
|
type ProviderAuthMethodNonInteractiveContext,
|
||||||
type ProviderAuthResult,
|
type ProviderAuthResult,
|
||||||
type ProviderDiscoveryContext,
|
type ProviderDiscoveryContext,
|
||||||
} from "openclaw/plugin-sdk/core";
|
} from "openclaw/plugin-sdk/core";
|
||||||
@@ -50,6 +52,12 @@ const ollamaPlugin = {
|
|||||||
defaultModel: `ollama/${result.defaultModelId}`,
|
defaultModel: `ollama/${result.defaultModelId}`,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) =>
|
||||||
|
configureOllamaNonInteractive({
|
||||||
|
nextConfig: ctx.config,
|
||||||
|
opts: ctx.opts,
|
||||||
|
runtime: ctx.runtime,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
discovery: {
|
discovery: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@openclaw/ollama-provider",
|
"name": "@openclaw/ollama",
|
||||||
"version": "2026.3.11",
|
"version": "2026.3.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "OpenClaw Ollama provider plugin",
|
"description": "OpenClaw Ollama provider plugin",
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
buildSglangProvider,
|
buildSglangProvider,
|
||||||
|
configureOpenAICompatibleSelfHostedProviderNonInteractive,
|
||||||
emptyPluginConfigSchema,
|
emptyPluginConfigSchema,
|
||||||
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
||||||
type OpenClawPluginApi,
|
type OpenClawPluginApi,
|
||||||
type ProviderAuthContext,
|
type ProviderAuthContext,
|
||||||
|
type ProviderAuthMethodNonInteractiveContext,
|
||||||
type ProviderAuthResult,
|
type ProviderAuthResult,
|
||||||
type ProviderDiscoveryContext,
|
type ProviderDiscoveryContext,
|
||||||
} from "openclaw/plugin-sdk/core";
|
} from "openclaw/plugin-sdk/core";
|
||||||
@@ -49,6 +51,15 @@ const sglangPlugin = {
|
|||||||
defaultModel: result.modelRef,
|
defaultModel: result.modelRef,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) =>
|
||||||
|
configureOpenAICompatibleSelfHostedProviderNonInteractive({
|
||||||
|
ctx,
|
||||||
|
providerId: PROVIDER_ID,
|
||||||
|
providerLabel: "SGLang",
|
||||||
|
defaultBaseUrl: DEFAULT_BASE_URL,
|
||||||
|
defaultApiKeyEnvVar: "SGLANG_API_KEY",
|
||||||
|
modelPlaceholder: "Qwen/Qwen3-8B",
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
discovery: {
|
discovery: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@openclaw/sglang-provider",
|
"name": "@openclaw/sglang",
|
||||||
"version": "2026.3.11",
|
"version": "2026.3.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "OpenClaw SGLang provider plugin",
|
"description": "OpenClaw SGLang provider plugin",
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
buildVllmProvider,
|
buildVllmProvider,
|
||||||
|
configureOpenAICompatibleSelfHostedProviderNonInteractive,
|
||||||
emptyPluginConfigSchema,
|
emptyPluginConfigSchema,
|
||||||
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
||||||
type OpenClawPluginApi,
|
type OpenClawPluginApi,
|
||||||
type ProviderAuthContext,
|
type ProviderAuthContext,
|
||||||
|
type ProviderAuthMethodNonInteractiveContext,
|
||||||
type ProviderAuthResult,
|
type ProviderAuthResult,
|
||||||
type ProviderDiscoveryContext,
|
type ProviderDiscoveryContext,
|
||||||
} from "openclaw/plugin-sdk/core";
|
} from "openclaw/plugin-sdk/core";
|
||||||
@@ -49,6 +51,15 @@ const vllmPlugin = {
|
|||||||
defaultModel: result.modelRef,
|
defaultModel: result.modelRef,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) =>
|
||||||
|
configureOpenAICompatibleSelfHostedProviderNonInteractive({
|
||||||
|
ctx,
|
||||||
|
providerId: PROVIDER_ID,
|
||||||
|
providerLabel: "vLLM",
|
||||||
|
defaultBaseUrl: DEFAULT_BASE_URL,
|
||||||
|
defaultApiKeyEnvVar: "VLLM_API_KEY",
|
||||||
|
modelPlaceholder: "meta-llama/Meta-Llama-3-8B-Instruct",
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
discovery: {
|
discovery: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@openclaw/vllm-provider",
|
"name": "@openclaw/vllm",
|
||||||
"version": "2026.3.11",
|
"version": "2026.3.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "OpenClaw vLLM provider plugin",
|
"description": "OpenClaw vLLM provider plugin",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type OnboardEnv = {
|
|||||||
runtime: NonInteractiveRuntime;
|
runtime: NonInteractiveRuntime;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensureWorkspaceAndSessionsMock = vi.fn(async (..._args: unknown[]) => {});
|
const ensureWorkspaceAndSessionsMock = vi.hoisted(() => vi.fn(async (..._args: unknown[]) => {}));
|
||||||
|
|
||||||
vi.mock("./onboard-helpers.js", async (importOriginal) => {
|
vi.mock("./onboard-helpers.js", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("./onboard-helpers.js")>();
|
const actual = await importOriginal<typeof import("./onboard-helpers.js")>();
|
||||||
@@ -474,14 +474,63 @@ describe("onboard (non-interactive): provider auth", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects vLLM auth choice in non-interactive mode", async () => {
|
it("configures vLLM via the provider plugin in non-interactive mode", async () => {
|
||||||
await withOnboardEnv("openclaw-onboard-vllm-non-interactive-", async ({ runtime }) => {
|
await withOnboardEnv("openclaw-onboard-vllm-non-interactive-", async (env) => {
|
||||||
await expect(
|
const cfg = await runOnboardingAndReadConfig(env, {
|
||||||
runNonInteractiveOnboardingWithDefaults(runtime, {
|
authChoice: "vllm",
|
||||||
authChoice: "vllm",
|
customBaseUrl: "http://127.0.0.1:8100/v1",
|
||||||
skipSkills: true,
|
customApiKey: "vllm-test-key", // pragma: allowlist secret
|
||||||
}),
|
customModelId: "Qwen/Qwen3-8B",
|
||||||
).rejects.toThrow('Auth choice "vllm" requires interactive mode.');
|
});
|
||||||
|
|
||||||
|
expect(cfg.auth?.profiles?.["vllm:default"]?.provider).toBe("vllm");
|
||||||
|
expect(cfg.auth?.profiles?.["vllm:default"]?.mode).toBe("api_key");
|
||||||
|
expect(cfg.models?.providers?.vllm).toEqual({
|
||||||
|
baseUrl: "http://127.0.0.1:8100/v1",
|
||||||
|
api: "openai-completions",
|
||||||
|
apiKey: "VLLM_API_KEY",
|
||||||
|
models: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "Qwen/Qwen3-8B",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(cfg.agents?.defaults?.model?.primary).toBe("vllm/Qwen/Qwen3-8B");
|
||||||
|
await expectApiKeyProfile({
|
||||||
|
profileId: "vllm:default",
|
||||||
|
provider: "vllm",
|
||||||
|
key: "vllm-test-key",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("configures SGLang via the provider plugin in non-interactive mode", async () => {
|
||||||
|
await withOnboardEnv("openclaw-onboard-sglang-non-interactive-", async (env) => {
|
||||||
|
const cfg = await runOnboardingAndReadConfig(env, {
|
||||||
|
authChoice: "sglang",
|
||||||
|
customBaseUrl: "http://127.0.0.1:31000/v1",
|
||||||
|
customApiKey: "sglang-test-key", // pragma: allowlist secret
|
||||||
|
customModelId: "Qwen/Qwen3-32B",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(cfg.auth?.profiles?.["sglang:default"]?.provider).toBe("sglang");
|
||||||
|
expect(cfg.auth?.profiles?.["sglang:default"]?.mode).toBe("api_key");
|
||||||
|
expect(cfg.models?.providers?.sglang).toEqual({
|
||||||
|
baseUrl: "http://127.0.0.1:31000/v1",
|
||||||
|
api: "openai-completions",
|
||||||
|
apiKey: "SGLANG_API_KEY",
|
||||||
|
models: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "Qwen/Qwen3-32B",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(cfg.agents?.defaults?.model?.primary).toBe("sglang/Qwen/Qwen3-32B");
|
||||||
|
await expectApiKeyProfile({
|
||||||
|
profileId: "sglang:default",
|
||||||
|
provider: "sglang",
|
||||||
|
key: "sglang-test-key",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { resolveDefaultAgentId, resolveAgentWorkspaceDir } from "../../../agents/agent-scope.js";
|
||||||
|
import type { ApiKeyCredential } from "../../../agents/auth-profiles/types.js";
|
||||||
|
import { resolveDefaultAgentWorkspaceDir } from "../../../agents/workspace.js";
|
||||||
|
import type { OpenClawConfig } from "../../../config/config.js";
|
||||||
|
import { enablePluginInConfig } from "../../../plugins/enable.js";
|
||||||
|
import {
|
||||||
|
PROVIDER_PLUGIN_CHOICE_PREFIX,
|
||||||
|
resolveProviderPluginChoice,
|
||||||
|
} from "../../../plugins/provider-wizard.js";
|
||||||
|
import { resolvePluginProviders } from "../../../plugins/providers.js";
|
||||||
|
import type {
|
||||||
|
ProviderNonInteractiveApiKeyCredentialParams,
|
||||||
|
ProviderResolveNonInteractiveApiKeyParams,
|
||||||
|
} from "../../../plugins/types.js";
|
||||||
|
import type { RuntimeEnv } from "../../../runtime.js";
|
||||||
|
import { resolvePreferredProviderForAuthChoice } from "../../auth-choice.preferred-provider.js";
|
||||||
|
import type { OnboardOptions } from "../../onboard-types.js";
|
||||||
|
|
||||||
|
function buildIsolatedProviderResolutionConfig(
|
||||||
|
cfg: OpenClawConfig,
|
||||||
|
providerId: string | undefined,
|
||||||
|
): OpenClawConfig {
|
||||||
|
if (!providerId) {
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
const allow = new Set(cfg.plugins?.allow ?? []);
|
||||||
|
allow.add(providerId);
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
plugins: {
|
||||||
|
...cfg.plugins,
|
||||||
|
allow: Array.from(allow),
|
||||||
|
entries: {
|
||||||
|
...cfg.plugins?.entries,
|
||||||
|
[providerId]: {
|
||||||
|
...cfg.plugins?.entries?.[providerId],
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyNonInteractivePluginProviderChoice(params: {
|
||||||
|
nextConfig: OpenClawConfig;
|
||||||
|
authChoice: string;
|
||||||
|
opts: OnboardOptions;
|
||||||
|
runtime: RuntimeEnv;
|
||||||
|
baseConfig: OpenClawConfig;
|
||||||
|
resolveApiKey: (input: ProviderResolveNonInteractiveApiKeyParams) => Promise<{
|
||||||
|
key: string;
|
||||||
|
source: "profile" | "env" | "flag";
|
||||||
|
envVarName?: string;
|
||||||
|
} | null>;
|
||||||
|
toApiKeyCredential: (
|
||||||
|
input: ProviderNonInteractiveApiKeyCredentialParams,
|
||||||
|
) => ApiKeyCredential | null;
|
||||||
|
}): Promise<OpenClawConfig | null | undefined> {
|
||||||
|
const agentId = resolveDefaultAgentId(params.nextConfig);
|
||||||
|
const workspaceDir =
|
||||||
|
resolveAgentWorkspaceDir(params.nextConfig, agentId) ?? resolveDefaultAgentWorkspaceDir();
|
||||||
|
const prefixedProviderId = params.authChoice.startsWith(PROVIDER_PLUGIN_CHOICE_PREFIX)
|
||||||
|
? params.authChoice.slice(PROVIDER_PLUGIN_CHOICE_PREFIX.length).split(":", 1)[0]?.trim()
|
||||||
|
: undefined;
|
||||||
|
const preferredProviderId =
|
||||||
|
prefixedProviderId ||
|
||||||
|
resolvePreferredProviderForAuthChoice({
|
||||||
|
choice: params.authChoice,
|
||||||
|
config: params.nextConfig,
|
||||||
|
workspaceDir,
|
||||||
|
});
|
||||||
|
const resolutionConfig = buildIsolatedProviderResolutionConfig(
|
||||||
|
params.nextConfig,
|
||||||
|
preferredProviderId,
|
||||||
|
);
|
||||||
|
const providerChoice = resolveProviderPluginChoice({
|
||||||
|
providers: resolvePluginProviders({
|
||||||
|
config: resolutionConfig,
|
||||||
|
workspaceDir,
|
||||||
|
}),
|
||||||
|
choice: params.authChoice,
|
||||||
|
});
|
||||||
|
if (!providerChoice) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enableResult = enablePluginInConfig(
|
||||||
|
params.nextConfig,
|
||||||
|
providerChoice.provider.pluginId ?? providerChoice.provider.id,
|
||||||
|
);
|
||||||
|
if (!enableResult.enabled) {
|
||||||
|
params.runtime.error(
|
||||||
|
`${providerChoice.provider.label} plugin is disabled (${enableResult.reason ?? "blocked"}).`,
|
||||||
|
);
|
||||||
|
params.runtime.exit(1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = providerChoice.method;
|
||||||
|
if (!method.runNonInteractive) {
|
||||||
|
params.runtime.error(
|
||||||
|
[
|
||||||
|
`Auth choice "${params.authChoice}" requires interactive mode.`,
|
||||||
|
`The ${providerChoice.provider.label} provider plugin does not implement non-interactive setup.`,
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
params.runtime.exit(1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return method.runNonInteractive({
|
||||||
|
authChoice: params.authChoice,
|
||||||
|
config: enableResult.config,
|
||||||
|
baseConfig: params.baseConfig,
|
||||||
|
opts: params.opts,
|
||||||
|
runtime: params.runtime,
|
||||||
|
workspaceDir,
|
||||||
|
resolveApiKey: params.resolveApiKey,
|
||||||
|
toApiKeyCredential: params.toApiKeyCredential,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { upsertAuthProfile } from "../../../agents/auth-profiles.js";
|
import { upsertAuthProfile } from "../../../agents/auth-profiles.js";
|
||||||
|
import type { ApiKeyCredential } from "../../../agents/auth-profiles/types.js";
|
||||||
import { normalizeProviderId } from "../../../agents/model-selection.js";
|
import { normalizeProviderId } from "../../../agents/model-selection.js";
|
||||||
import { parseDurationMs } from "../../../cli/parse-duration.js";
|
import { parseDurationMs } from "../../../cli/parse-duration.js";
|
||||||
import type { OpenClawConfig } from "../../../config/config.js";
|
import type { OpenClawConfig } from "../../../config/config.js";
|
||||||
@@ -8,7 +9,6 @@ import { resolveDefaultSecretProviderAlias } from "../../../secrets/ref-contract
|
|||||||
import { normalizeSecretInput } from "../../../utils/normalize-secret-input.js";
|
import { normalizeSecretInput } from "../../../utils/normalize-secret-input.js";
|
||||||
import { normalizeSecretInputModeInput } from "../../auth-choice.apply-helpers.js";
|
import { normalizeSecretInputModeInput } from "../../auth-choice.apply-helpers.js";
|
||||||
import { buildTokenProfileId, validateAnthropicSetupToken } from "../../auth-token.js";
|
import { buildTokenProfileId, validateAnthropicSetupToken } from "../../auth-token.js";
|
||||||
import { configureOllamaNonInteractive } from "../../ollama-setup.js";
|
|
||||||
import {
|
import {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
applyCloudflareAiGatewayConfig,
|
applyCloudflareAiGatewayConfig,
|
||||||
@@ -29,6 +29,7 @@ import type { AuthChoice, OnboardOptions } from "../../onboard-types.js";
|
|||||||
import { detectZaiEndpoint } from "../../zai-endpoint-detect.js";
|
import { detectZaiEndpoint } from "../../zai-endpoint-detect.js";
|
||||||
import { resolveNonInteractiveApiKey } from "../api-keys.js";
|
import { resolveNonInteractiveApiKey } from "../api-keys.js";
|
||||||
import { applySimpleNonInteractiveApiKeyChoice } from "./auth-choice.api-key-providers.js";
|
import { applySimpleNonInteractiveApiKeyChoice } from "./auth-choice.api-key-providers.js";
|
||||||
|
import { applyNonInteractivePluginProviderChoice } from "./auth-choice.plugin-providers.js";
|
||||||
|
|
||||||
type ResolvedNonInteractiveApiKey = NonNullable<
|
type ResolvedNonInteractiveApiKey = NonNullable<
|
||||||
Awaited<ReturnType<typeof resolveNonInteractiveApiKey>>
|
Awaited<ReturnType<typeof resolveNonInteractiveApiKey>>
|
||||||
@@ -83,6 +84,46 @@ export async function applyNonInteractiveAuthChoice(params: {
|
|||||||
...input,
|
...input,
|
||||||
secretInputMode: requestedSecretInputMode,
|
secretInputMode: requestedSecretInputMode,
|
||||||
});
|
});
|
||||||
|
const toApiKeyCredential = (params: {
|
||||||
|
provider: string;
|
||||||
|
resolved: ResolvedNonInteractiveApiKey;
|
||||||
|
email?: string;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
}): ApiKeyCredential | null => {
|
||||||
|
const storeSecretRef = requestedSecretInputMode === "ref" && params.resolved.source === "env"; // pragma: allowlist secret
|
||||||
|
if (storeSecretRef) {
|
||||||
|
if (!params.resolved.envVarName) {
|
||||||
|
runtime.error(
|
||||||
|
[
|
||||||
|
`--secret-input-mode ref requires an explicit environment variable for provider "${params.provider}".`,
|
||||||
|
"Set the provider API key env var and retry, or use --secret-input-mode plaintext.",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
runtime.exit(1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "api_key",
|
||||||
|
provider: params.provider,
|
||||||
|
keyRef: {
|
||||||
|
source: "env",
|
||||||
|
provider: resolveDefaultSecretProviderAlias(baseConfig, "env", {
|
||||||
|
preferFirstProviderForSource: true,
|
||||||
|
}),
|
||||||
|
id: params.resolved.envVarName,
|
||||||
|
},
|
||||||
|
...(params.email ? { email: params.email } : {}),
|
||||||
|
...(params.metadata ? { metadata: params.metadata } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "api_key",
|
||||||
|
provider: params.provider,
|
||||||
|
key: params.resolved.key,
|
||||||
|
...(params.email ? { email: params.email } : {}),
|
||||||
|
...(params.metadata ? { metadata: params.metadata } : {}),
|
||||||
|
};
|
||||||
|
};
|
||||||
const maybeSetResolvedApiKey = async (
|
const maybeSetResolvedApiKey = async (
|
||||||
resolved: ResolvedNonInteractiveApiKey,
|
resolved: ResolvedNonInteractiveApiKey,
|
||||||
setter: (value: SecretInput) => Promise<void> | void,
|
setter: (value: SecretInput) => Promise<void> | void,
|
||||||
@@ -120,19 +161,22 @@ export async function applyNonInteractiveAuthChoice(params: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authChoice === "vllm") {
|
const pluginProviderChoice = await applyNonInteractivePluginProviderChoice({
|
||||||
runtime.error(
|
nextConfig,
|
||||||
[
|
authChoice,
|
||||||
'Auth choice "vllm" requires interactive mode.',
|
opts,
|
||||||
"Use interactive onboard/configure to enter base URL, API key, and model ID.",
|
runtime,
|
||||||
].join("\n"),
|
baseConfig,
|
||||||
);
|
resolveApiKey: (input) =>
|
||||||
runtime.exit(1);
|
resolveApiKey({
|
||||||
return null;
|
...input,
|
||||||
}
|
cfg: baseConfig,
|
||||||
|
runtime,
|
||||||
if (authChoice === "ollama") {
|
}),
|
||||||
return configureOllamaNonInteractive({ nextConfig, opts, runtime });
|
toApiKeyCredential,
|
||||||
|
});
|
||||||
|
if (pluginProviderChoice !== undefined) {
|
||||||
|
return pluginProviderChoice;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authChoice === "token") {
|
if (authChoice === "token") {
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import type { AuthProfileCredential } from "../agents/auth-profiles/types.js";
|
import { upsertAuthProfileWithLock } from "../agents/auth-profiles.js";
|
||||||
|
import type { ApiKeyCredential, AuthProfileCredential } from "../agents/auth-profiles/types.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import type {
|
||||||
|
ProviderAuthMethodNonInteractiveContext,
|
||||||
|
ProviderNonInteractiveApiKeyResult,
|
||||||
|
} from "../plugins/types.js";
|
||||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
|
import { applyAuthProfileConfig } from "./onboard-auth.js";
|
||||||
|
|
||||||
export const SELF_HOSTED_DEFAULT_CONTEXT_WINDOW = 128000;
|
export const SELF_HOSTED_DEFAULT_CONTEXT_WINDOW = 128000;
|
||||||
export const SELF_HOSTED_DEFAULT_MAX_TOKENS = 8192;
|
export const SELF_HOSTED_DEFAULT_MAX_TOKENS = 8192;
|
||||||
@@ -33,6 +39,52 @@ export function applyProviderDefaultModel(cfg: OpenClawConfig, modelRef: string)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildOpenAICompatibleSelfHostedProviderConfig(params: {
|
||||||
|
cfg: OpenClawConfig;
|
||||||
|
providerId: string;
|
||||||
|
baseUrl: string;
|
||||||
|
providerApiKey: string;
|
||||||
|
modelId: string;
|
||||||
|
input?: Array<"text" | "image">;
|
||||||
|
reasoning?: boolean;
|
||||||
|
contextWindow?: number;
|
||||||
|
maxTokens?: number;
|
||||||
|
}): { config: OpenClawConfig; modelId: string; modelRef: string; profileId: string } {
|
||||||
|
const modelRef = `${params.providerId}/${params.modelId}`;
|
||||||
|
const profileId = `${params.providerId}:default`;
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
...params.cfg,
|
||||||
|
models: {
|
||||||
|
...params.cfg.models,
|
||||||
|
mode: params.cfg.models?.mode ?? "merge",
|
||||||
|
providers: {
|
||||||
|
...params.cfg.models?.providers,
|
||||||
|
[params.providerId]: {
|
||||||
|
baseUrl: params.baseUrl,
|
||||||
|
api: "openai-completions",
|
||||||
|
apiKey: params.providerApiKey,
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: params.modelId,
|
||||||
|
name: params.modelId,
|
||||||
|
reasoning: params.reasoning ?? false,
|
||||||
|
input: params.input ?? ["text"],
|
||||||
|
cost: SELF_HOSTED_DEFAULT_COST,
|
||||||
|
contextWindow: params.contextWindow ?? SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
|
||||||
|
maxTokens: params.maxTokens ?? SELF_HOSTED_DEFAULT_MAX_TOKENS,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modelId: params.modelId,
|
||||||
|
modelRef,
|
||||||
|
profileId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function promptAndConfigureOpenAICompatibleSelfHostedProvider(params: {
|
export async function promptAndConfigureOpenAICompatibleSelfHostedProvider(params: {
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
prompter: WizardPrompter;
|
prompter: WizardPrompter;
|
||||||
@@ -74,46 +126,125 @@ export async function promptAndConfigureOpenAICompatibleSelfHostedProvider(param
|
|||||||
.replace(/\/+$/, "");
|
.replace(/\/+$/, "");
|
||||||
const apiKey = String(apiKeyRaw ?? "").trim();
|
const apiKey = String(apiKeyRaw ?? "").trim();
|
||||||
const modelId = String(modelIdRaw ?? "").trim();
|
const modelId = String(modelIdRaw ?? "").trim();
|
||||||
const modelRef = `${params.providerId}/${modelId}`;
|
|
||||||
const profileId = `${params.providerId}:default`;
|
|
||||||
const credential: AuthProfileCredential = {
|
const credential: AuthProfileCredential = {
|
||||||
type: "api_key",
|
type: "api_key",
|
||||||
provider: params.providerId,
|
provider: params.providerId,
|
||||||
key: apiKey,
|
key: apiKey,
|
||||||
};
|
};
|
||||||
|
const configured = buildOpenAICompatibleSelfHostedProviderConfig({
|
||||||
const nextConfig: OpenClawConfig = {
|
cfg: params.cfg,
|
||||||
...params.cfg,
|
providerId: params.providerId,
|
||||||
models: {
|
baseUrl,
|
||||||
...params.cfg.models,
|
providerApiKey: params.defaultApiKeyEnvVar,
|
||||||
mode: params.cfg.models?.mode ?? "merge",
|
modelId,
|
||||||
providers: {
|
input: params.input,
|
||||||
...params.cfg.models?.providers,
|
reasoning: params.reasoning,
|
||||||
[params.providerId]: {
|
contextWindow: params.contextWindow,
|
||||||
baseUrl,
|
maxTokens: params.maxTokens,
|
||||||
api: "openai-completions",
|
});
|
||||||
apiKey: params.defaultApiKeyEnvVar,
|
|
||||||
models: [
|
|
||||||
{
|
|
||||||
id: modelId,
|
|
||||||
name: modelId,
|
|
||||||
reasoning: params.reasoning ?? false,
|
|
||||||
input: params.input ?? ["text"],
|
|
||||||
cost: SELF_HOSTED_DEFAULT_COST,
|
|
||||||
contextWindow: params.contextWindow ?? SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
|
|
||||||
maxTokens: params.maxTokens ?? SELF_HOSTED_DEFAULT_MAX_TOKENS,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config: nextConfig,
|
config: configured.config,
|
||||||
credential,
|
credential,
|
||||||
modelId,
|
modelId: configured.modelId,
|
||||||
modelRef,
|
modelRef: configured.modelRef,
|
||||||
profileId,
|
profileId: configured.profileId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildMissingNonInteractiveModelIdMessage(params: {
|
||||||
|
authChoice: string;
|
||||||
|
providerLabel: string;
|
||||||
|
modelPlaceholder: string;
|
||||||
|
}): string {
|
||||||
|
return [
|
||||||
|
`Missing --custom-model-id for --auth-choice ${params.authChoice}.`,
|
||||||
|
`Pass the ${params.providerLabel} model id to use, for example ${params.modelPlaceholder}.`,
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSelfHostedProviderCredential(params: {
|
||||||
|
ctx: ProviderAuthMethodNonInteractiveContext;
|
||||||
|
providerId: string;
|
||||||
|
resolved: ProviderNonInteractiveApiKeyResult;
|
||||||
|
}): ApiKeyCredential | null {
|
||||||
|
return params.ctx.toApiKeyCredential({
|
||||||
|
provider: params.providerId,
|
||||||
|
resolved: params.resolved,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function configureOpenAICompatibleSelfHostedProviderNonInteractive(params: {
|
||||||
|
ctx: ProviderAuthMethodNonInteractiveContext;
|
||||||
|
providerId: string;
|
||||||
|
providerLabel: string;
|
||||||
|
defaultBaseUrl: string;
|
||||||
|
defaultApiKeyEnvVar: string;
|
||||||
|
modelPlaceholder: string;
|
||||||
|
input?: Array<"text" | "image">;
|
||||||
|
reasoning?: boolean;
|
||||||
|
contextWindow?: number;
|
||||||
|
maxTokens?: number;
|
||||||
|
}): Promise<OpenClawConfig | null> {
|
||||||
|
const baseUrl = (params.ctx.opts.customBaseUrl?.trim() || params.defaultBaseUrl).replace(
|
||||||
|
/\/+$/,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
const modelId = params.ctx.opts.customModelId?.trim();
|
||||||
|
if (!modelId) {
|
||||||
|
params.ctx.runtime.error(
|
||||||
|
buildMissingNonInteractiveModelIdMessage({
|
||||||
|
authChoice: params.ctx.authChoice,
|
||||||
|
providerLabel: params.providerLabel,
|
||||||
|
modelPlaceholder: params.modelPlaceholder,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
params.ctx.runtime.exit(1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = await params.ctx.resolveApiKey({
|
||||||
|
provider: params.providerId,
|
||||||
|
flagValue: params.ctx.opts.customApiKey,
|
||||||
|
flagName: "--custom-api-key",
|
||||||
|
envVar: params.defaultApiKeyEnvVar,
|
||||||
|
envVarName: params.defaultApiKeyEnvVar,
|
||||||
|
});
|
||||||
|
if (!resolved) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const credential = buildSelfHostedProviderCredential({
|
||||||
|
ctx: params.ctx,
|
||||||
|
providerId: params.providerId,
|
||||||
|
resolved,
|
||||||
|
});
|
||||||
|
if (!credential) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configured = buildOpenAICompatibleSelfHostedProviderConfig({
|
||||||
|
cfg: params.ctx.config,
|
||||||
|
providerId: params.providerId,
|
||||||
|
baseUrl,
|
||||||
|
providerApiKey: params.defaultApiKeyEnvVar,
|
||||||
|
modelId,
|
||||||
|
input: params.input,
|
||||||
|
reasoning: params.reasoning,
|
||||||
|
contextWindow: params.contextWindow,
|
||||||
|
maxTokens: params.maxTokens,
|
||||||
|
});
|
||||||
|
await upsertAuthProfileWithLock({
|
||||||
|
profileId: configured.profileId,
|
||||||
|
credential,
|
||||||
|
agentDir: params.ctx.agentDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
const withProfile = applyAuthProfileConfig(configured.config, {
|
||||||
|
profileId: configured.profileId,
|
||||||
|
provider: params.providerId,
|
||||||
|
mode: "api_key",
|
||||||
|
});
|
||||||
|
params.ctx.runtime.log(`Default ${params.providerLabel} model: ${modelId}`);
|
||||||
|
return applyProviderDefaultModel(withProfile, configured.modelRef);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type {
|
|||||||
ProviderDiscoveryContext,
|
ProviderDiscoveryContext,
|
||||||
OpenClawPluginService,
|
OpenClawPluginService,
|
||||||
ProviderAuthContext,
|
ProviderAuthContext,
|
||||||
|
ProviderAuthMethodNonInteractiveContext,
|
||||||
ProviderAuthResult,
|
ProviderAuthResult,
|
||||||
} from "../plugins/types.js";
|
} from "../plugins/types.js";
|
||||||
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
||||||
@@ -15,6 +16,7 @@ export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
|||||||
export { buildOauthProviderAuthResult } from "./provider-auth-result.js";
|
export { buildOauthProviderAuthResult } from "./provider-auth-result.js";
|
||||||
export {
|
export {
|
||||||
applyProviderDefaultModel,
|
applyProviderDefaultModel,
|
||||||
|
configureOpenAICompatibleSelfHostedProviderNonInteractive,
|
||||||
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
||||||
SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
|
SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
|
||||||
SELF_HOSTED_DEFAULT_COST,
|
SELF_HOSTED_DEFAULT_COST,
|
||||||
|
|||||||
@@ -18,5 +18,8 @@ export function resolvePluginProviders(params: {
|
|||||||
logger: createPluginLoaderLogger(log),
|
logger: createPluginLoaderLogger(log),
|
||||||
});
|
});
|
||||||
|
|
||||||
return registry.providers.map((entry) => entry.provider);
|
return registry.providers.map((entry) => ({
|
||||||
|
...entry.provider,
|
||||||
|
pluginId: entry.pluginId,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import type { AuthProfileCredential, OAuthCredential } from "../agents/auth-profiles/types.js";
|
import type {
|
||||||
|
ApiKeyCredential,
|
||||||
|
AuthProfileCredential,
|
||||||
|
OAuthCredential,
|
||||||
|
} from "../agents/auth-profiles/types.js";
|
||||||
import type { AnyAgentTool } from "../agents/tools/common.js";
|
import type { AnyAgentTool } from "../agents/tools/common.js";
|
||||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
import type { ChannelDock } from "../channels/dock.js";
|
import type { ChannelDock } from "../channels/dock.js";
|
||||||
import type { ChannelId, ChannelPlugin } from "../channels/plugins/types.js";
|
import type { ChannelId, ChannelPlugin } from "../channels/plugins/types.js";
|
||||||
import type { createVpsAwareOAuthHandlers } from "../commands/oauth-flow.js";
|
import type { createVpsAwareOAuthHandlers } from "../commands/oauth-flow.js";
|
||||||
|
import type { OnboardOptions } from "../commands/onboard-types.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import type { ModelProviderConfig } from "../config/types.js";
|
import type { ModelProviderConfig } from "../config/types.js";
|
||||||
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
||||||
@@ -111,12 +116,54 @@ export type ProviderAuthContext = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ProviderNonInteractiveApiKeyResult = {
|
||||||
|
key: string;
|
||||||
|
source: "profile" | "env" | "flag";
|
||||||
|
envVarName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProviderResolveNonInteractiveApiKeyParams = {
|
||||||
|
provider: string;
|
||||||
|
flagValue?: string;
|
||||||
|
flagName: `--${string}`;
|
||||||
|
envVar: string;
|
||||||
|
envVarName?: string;
|
||||||
|
allowProfile?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProviderNonInteractiveApiKeyCredentialParams = {
|
||||||
|
provider: string;
|
||||||
|
resolved: ProviderNonInteractiveApiKeyResult;
|
||||||
|
email?: string;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProviderAuthMethodNonInteractiveContext = {
|
||||||
|
authChoice: string;
|
||||||
|
config: OpenClawConfig;
|
||||||
|
baseConfig: OpenClawConfig;
|
||||||
|
opts: OnboardOptions;
|
||||||
|
runtime: RuntimeEnv;
|
||||||
|
agentDir?: string;
|
||||||
|
workspaceDir?: string;
|
||||||
|
resolveApiKey: (
|
||||||
|
params: ProviderResolveNonInteractiveApiKeyParams,
|
||||||
|
) => Promise<ProviderNonInteractiveApiKeyResult | null>;
|
||||||
|
toApiKeyCredential: (
|
||||||
|
params: ProviderNonInteractiveApiKeyCredentialParams,
|
||||||
|
) => ApiKeyCredential | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type ProviderAuthMethod = {
|
export type ProviderAuthMethod = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
kind: ProviderAuthKind;
|
kind: ProviderAuthKind;
|
||||||
run: (ctx: ProviderAuthContext) => Promise<ProviderAuthResult>;
|
run: (ctx: ProviderAuthContext) => Promise<ProviderAuthResult>;
|
||||||
|
runNonInteractive?: (
|
||||||
|
ctx: ProviderAuthMethodNonInteractiveContext,
|
||||||
|
) => Promise<OpenClawConfig | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProviderDiscoveryOrder = "simple" | "profile" | "paired" | "late";
|
export type ProviderDiscoveryOrder = "simple" | "profile" | "paired" | "late";
|
||||||
@@ -174,11 +221,11 @@ export type ProviderModelSelectedContext = {
|
|||||||
|
|
||||||
export type ProviderPlugin = {
|
export type ProviderPlugin = {
|
||||||
id: string;
|
id: string;
|
||||||
|
pluginId?: string;
|
||||||
label: string;
|
label: string;
|
||||||
docsPath?: string;
|
docsPath?: string;
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
envVars?: string[];
|
envVars?: string[];
|
||||||
models?: ModelProviderConfig;
|
|
||||||
auth: ProviderAuthMethod[];
|
auth: ProviderAuthMethod[];
|
||||||
discovery?: ProviderPluginDiscovery;
|
discovery?: ProviderPluginDiscovery;
|
||||||
wizard?: ProviderPluginWizard;
|
wizard?: ProviderPluginWizard;
|
||||||
|
|||||||
Reference in New Issue
Block a user