diff --git a/extensions/ollama/index.ts b/extensions/ollama/index.ts index 04278077c..6ba28a3af 100644 --- a/extensions/ollama/index.ts +++ b/extensions/ollama/index.ts @@ -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: { diff --git a/extensions/ollama/package.json b/extensions/ollama/package.json index c7650bfd7..eaeb45141 100644 --- a/extensions/ollama/package.json +++ b/extensions/ollama/package.json @@ -1,5 +1,5 @@ { - "name": "@openclaw/ollama-provider", + "name": "@openclaw/ollama", "version": "2026.3.11", "private": true, "description": "OpenClaw Ollama provider plugin", diff --git a/extensions/sglang/index.ts b/extensions/sglang/index.ts index 3dfc53ec9..4c9102cae 100644 --- a/extensions/sglang/index.ts +++ b/extensions/sglang/index.ts @@ -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: { diff --git a/extensions/sglang/package.json b/extensions/sglang/package.json index d02a814ba..2550df9f3 100644 --- a/extensions/sglang/package.json +++ b/extensions/sglang/package.json @@ -1,5 +1,5 @@ { - "name": "@openclaw/sglang-provider", + "name": "@openclaw/sglang", "version": "2026.3.11", "private": true, "description": "OpenClaw SGLang provider plugin", diff --git a/extensions/vllm/index.ts b/extensions/vllm/index.ts index 4e1920d1b..fd0a5e189 100644 --- a/extensions/vllm/index.ts +++ b/extensions/vllm/index.ts @@ -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: { diff --git a/extensions/vllm/package.json b/extensions/vllm/package.json index bc3b543ed..2fff5f9f3 100644 --- a/extensions/vllm/package.json +++ b/extensions/vllm/package.json @@ -1,5 +1,5 @@ { - "name": "@openclaw/vllm-provider", + "name": "@openclaw/vllm", "version": "2026.3.11", "private": true, "description": "OpenClaw vLLM provider plugin", diff --git a/src/commands/onboard-non-interactive.provider-auth.test.ts b/src/commands/onboard-non-interactive.provider-auth.test.ts index 0c0e2f38f..d1eb0a774 100644 --- a/src/commands/onboard-non-interactive.provider-auth.test.ts +++ b/src/commands/onboard-non-interactive.provider-auth.test.ts @@ -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(); @@ -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, { - authChoice: "vllm", - skipSkills: true, - }), - ).rejects.toThrow('Auth choice "vllm" requires interactive mode.'); + 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", + 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", + }), + ], + }); + 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", + }); }); }); diff --git a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts new file mode 100644 index 000000000..01007aa7a --- /dev/null +++ b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts @@ -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 { + 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, + }); +} diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index b0fb88115..d435771d7 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -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> @@ -83,6 +84,46 @@ export async function applyNonInteractiveAuthChoice(params: { ...input, secretInputMode: requestedSecretInputMode, }); + const toApiKeyCredential = (params: { + provider: string; + resolved: ResolvedNonInteractiveApiKey; + email?: string; + metadata?: Record; + }): 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, @@ -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") { diff --git a/src/commands/self-hosted-provider-setup.ts b/src/commands/self-hosted-provider-setup.ts index 8d2f6526f..6a50820ce 100644 --- a/src/commands/self-hosted-provider-setup.ts +++ b/src/commands/self-hosted-provider-setup.ts @@ -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]: { - 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, - }, - ], - }, - }, - }, - }; + const configured = buildOpenAICompatibleSelfHostedProviderConfig({ + cfg: params.cfg, + providerId: params.providerId, + baseUrl, + 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 { + 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); +} diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index 5fc93a0e3..2a14be3b3 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -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, diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index 788a28ca8..4847a6193 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -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, + })); } diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 237d887d3..40e3de135 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -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; +}; + +export type ProviderAuthMethodNonInteractiveContext = { + authChoice: string; + config: OpenClawConfig; + baseConfig: OpenClawConfig; + opts: OnboardOptions; + runtime: RuntimeEnv; + agentDir?: string; + workspaceDir?: string; + resolveApiKey: ( + params: ProviderResolveNonInteractiveApiKeyParams, + ) => Promise; + toApiKeyCredential: ( + params: ProviderNonInteractiveApiKeyCredentialParams, + ) => ApiKeyCredential | null; +}; + export type ProviderAuthMethod = { id: string; label: string; hint?: string; kind: ProviderAuthKind; run: (ctx: ProviderAuthContext) => Promise; + runNonInteractive?: ( + ctx: ProviderAuthMethodNonInteractiveContext, + ) => Promise; }; 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;