refactor: add non-interactive provider plugin setup
This commit is contained in:
@@ -4,8 +4,10 @@ import {
|
||||
ensureOllamaModelPulled,
|
||||
OLLAMA_DEFAULT_BASE_URL,
|
||||
promptAndConfigureOllama,
|
||||
configureOllamaNonInteractive,
|
||||
type OpenClawPluginApi,
|
||||
type ProviderAuthContext,
|
||||
type ProviderAuthMethodNonInteractiveContext,
|
||||
type ProviderAuthResult,
|
||||
type ProviderDiscoveryContext,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
@@ -50,6 +52,12 @@ const ollamaPlugin = {
|
||||
defaultModel: `ollama/${result.defaultModelId}`,
|
||||
};
|
||||
},
|
||||
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) =>
|
||||
configureOllamaNonInteractive({
|
||||
nextConfig: ctx.config,
|
||||
opts: ctx.opts,
|
||||
runtime: ctx.runtime,
|
||||
}),
|
||||
},
|
||||
],
|
||||
discovery: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@openclaw/ollama-provider",
|
||||
"name": "@openclaw/ollama",
|
||||
"version": "2026.3.11",
|
||||
"private": true,
|
||||
"description": "OpenClaw Ollama provider plugin",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
buildSglangProvider,
|
||||
configureOpenAICompatibleSelfHostedProviderNonInteractive,
|
||||
emptyPluginConfigSchema,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
||||
type OpenClawPluginApi,
|
||||
type ProviderAuthContext,
|
||||
type ProviderAuthMethodNonInteractiveContext,
|
||||
type ProviderAuthResult,
|
||||
type ProviderDiscoveryContext,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
@@ -49,6 +51,15 @@ const sglangPlugin = {
|
||||
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: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@openclaw/sglang-provider",
|
||||
"name": "@openclaw/sglang",
|
||||
"version": "2026.3.11",
|
||||
"private": true,
|
||||
"description": "OpenClaw SGLang provider plugin",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
buildVllmProvider,
|
||||
configureOpenAICompatibleSelfHostedProviderNonInteractive,
|
||||
emptyPluginConfigSchema,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
||||
type OpenClawPluginApi,
|
||||
type ProviderAuthContext,
|
||||
type ProviderAuthMethodNonInteractiveContext,
|
||||
type ProviderAuthResult,
|
||||
type ProviderDiscoveryContext,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
@@ -49,6 +51,15 @@ const vllmPlugin = {
|
||||
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: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@openclaw/vllm-provider",
|
||||
"name": "@openclaw/vllm",
|
||||
"version": "2026.3.11",
|
||||
"private": true,
|
||||
"description": "OpenClaw vLLM provider plugin",
|
||||
|
||||
@@ -17,7 +17,7 @@ type OnboardEnv = {
|
||||
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) => {
|
||||
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 () => {
|
||||
await withOnboardEnv("openclaw-onboard-vllm-non-interactive-", async ({ runtime }) => {
|
||||
await expect(
|
||||
runNonInteractiveOnboardingWithDefaults(runtime, {
|
||||
it("configures vLLM via the provider plugin in non-interactive mode", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-vllm-non-interactive-", async (env) => {
|
||||
const cfg = await runOnboardingAndReadConfig(env, {
|
||||
authChoice: "vllm",
|
||||
skipSkills: true,
|
||||
customBaseUrl: "http://127.0.0.1:8100/v1",
|
||||
customApiKey: "vllm-test-key", // pragma: allowlist secret
|
||||
customModelId: "Qwen/Qwen3-8B",
|
||||
});
|
||||
|
||||
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",
|
||||
}),
|
||||
).rejects.toThrow('Auth choice "vllm" requires interactive mode.');
|
||||
],
|
||||
});
|
||||
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 type { ApiKeyCredential } from "../../../agents/auth-profiles/types.js";
|
||||
import { normalizeProviderId } from "../../../agents/model-selection.js";
|
||||
import { parseDurationMs } from "../../../cli/parse-duration.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 { normalizeSecretInputModeInput } from "../../auth-choice.apply-helpers.js";
|
||||
import { buildTokenProfileId, validateAnthropicSetupToken } from "../../auth-token.js";
|
||||
import { configureOllamaNonInteractive } from "../../ollama-setup.js";
|
||||
import {
|
||||
applyAuthProfileConfig,
|
||||
applyCloudflareAiGatewayConfig,
|
||||
@@ -29,6 +29,7 @@ import type { AuthChoice, OnboardOptions } from "../../onboard-types.js";
|
||||
import { detectZaiEndpoint } from "../../zai-endpoint-detect.js";
|
||||
import { resolveNonInteractiveApiKey } from "../api-keys.js";
|
||||
import { applySimpleNonInteractiveApiKeyChoice } from "./auth-choice.api-key-providers.js";
|
||||
import { applyNonInteractivePluginProviderChoice } from "./auth-choice.plugin-providers.js";
|
||||
|
||||
type ResolvedNonInteractiveApiKey = NonNullable<
|
||||
Awaited<ReturnType<typeof resolveNonInteractiveApiKey>>
|
||||
@@ -83,6 +84,46 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
...input,
|
||||
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 (
|
||||
resolved: ResolvedNonInteractiveApiKey,
|
||||
setter: (value: SecretInput) => Promise<void> | void,
|
||||
@@ -120,19 +161,22 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (authChoice === "vllm") {
|
||||
runtime.error(
|
||||
[
|
||||
'Auth choice "vllm" requires interactive mode.',
|
||||
"Use interactive onboard/configure to enter base URL, API key, and model ID.",
|
||||
].join("\n"),
|
||||
);
|
||||
runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (authChoice === "ollama") {
|
||||
return configureOllamaNonInteractive({ nextConfig, opts, runtime });
|
||||
const pluginProviderChoice = await applyNonInteractivePluginProviderChoice({
|
||||
nextConfig,
|
||||
authChoice,
|
||||
opts,
|
||||
runtime,
|
||||
baseConfig,
|
||||
resolveApiKey: (input) =>
|
||||
resolveApiKey({
|
||||
...input,
|
||||
cfg: baseConfig,
|
||||
runtime,
|
||||
}),
|
||||
toApiKeyCredential,
|
||||
});
|
||||
if (pluginProviderChoice !== undefined) {
|
||||
return pluginProviderChoice;
|
||||
}
|
||||
|
||||
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 {
|
||||
ProviderAuthMethodNonInteractiveContext,
|
||||
ProviderNonInteractiveApiKeyResult,
|
||||
} from "../plugins/types.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_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: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
@@ -74,46 +126,125 @@ export async function promptAndConfigureOpenAICompatibleSelfHostedProvider(param
|
||||
.replace(/\/+$/, "");
|
||||
const apiKey = String(apiKeyRaw ?? "").trim();
|
||||
const modelId = String(modelIdRaw ?? "").trim();
|
||||
const modelRef = `${params.providerId}/${modelId}`;
|
||||
const profileId = `${params.providerId}:default`;
|
||||
const credential: AuthProfileCredential = {
|
||||
type: "api_key",
|
||||
provider: params.providerId,
|
||||
key: apiKey,
|
||||
};
|
||||
|
||||
const nextConfig: OpenClawConfig = {
|
||||
...params.cfg,
|
||||
models: {
|
||||
...params.cfg.models,
|
||||
mode: params.cfg.models?.mode ?? "merge",
|
||||
providers: {
|
||||
...params.cfg.models?.providers,
|
||||
[params.providerId]: {
|
||||
const configured = buildOpenAICompatibleSelfHostedProviderConfig({
|
||||
cfg: params.cfg,
|
||||
providerId: params.providerId,
|
||||
baseUrl,
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
providerApiKey: params.defaultApiKeyEnvVar,
|
||||
modelId,
|
||||
input: params.input,
|
||||
reasoning: params.reasoning,
|
||||
contextWindow: params.contextWindow,
|
||||
maxTokens: params.maxTokens,
|
||||
});
|
||||
|
||||
return {
|
||||
config: nextConfig,
|
||||
config: configured.config,
|
||||
credential,
|
||||
modelId,
|
||||
modelRef,
|
||||
profileId,
|
||||
modelId: configured.modelId,
|
||||
modelRef: configured.modelRef,
|
||||
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,
|
||||
OpenClawPluginService,
|
||||
ProviderAuthContext,
|
||||
ProviderAuthMethodNonInteractiveContext,
|
||||
ProviderAuthResult,
|
||||
} from "../plugins/types.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 {
|
||||
applyProviderDefaultModel,
|
||||
configureOpenAICompatibleSelfHostedProviderNonInteractive,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
||||
SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
|
||||
SELF_HOSTED_DEFAULT_COST,
|
||||
|
||||
@@ -18,5 +18,8 @@ export function resolvePluginProviders(params: {
|
||||
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 { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
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 { ReplyPayload } from "../auto-reply/types.js";
|
||||
import type { ChannelDock } from "../channels/dock.js";
|
||||
import type { ChannelId, ChannelPlugin } from "../channels/plugins/types.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 { ModelProviderConfig } from "../config/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 = {
|
||||
id: string;
|
||||
label: string;
|
||||
hint?: string;
|
||||
kind: ProviderAuthKind;
|
||||
run: (ctx: ProviderAuthContext) => Promise<ProviderAuthResult>;
|
||||
runNonInteractive?: (
|
||||
ctx: ProviderAuthMethodNonInteractiveContext,
|
||||
) => Promise<OpenClawConfig | null>;
|
||||
};
|
||||
|
||||
export type ProviderDiscoveryOrder = "simple" | "profile" | "paired" | "late";
|
||||
@@ -174,11 +221,11 @@ export type ProviderModelSelectedContext = {
|
||||
|
||||
export type ProviderPlugin = {
|
||||
id: string;
|
||||
pluginId?: string;
|
||||
label: string;
|
||||
docsPath?: string;
|
||||
aliases?: string[];
|
||||
envVars?: string[];
|
||||
models?: ModelProviderConfig;
|
||||
auth: ProviderAuthMethod[];
|
||||
discovery?: ProviderPluginDiscovery;
|
||||
wizard?: ProviderPluginWizard;
|
||||
|
||||
Reference in New Issue
Block a user