Plugins: extract provider auth and wizard flows
This commit is contained in:
parent
aa47414c95
commit
c8d30dc144
11
src/agents/google-model-id.test.ts
Normal file
11
src/agents/google-model-id.test.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { normalizeGoogleModelId } from "./google-model-id.js";
|
||||||
|
|
||||||
|
describe("normalizeGoogleModelId", () => {
|
||||||
|
it("preserves compatibility with legacy Gemini aliases", () => {
|
||||||
|
expect(normalizeGoogleModelId("gemini-3.1-flash")).toBe("gemini-3-flash-preview");
|
||||||
|
expect(normalizeGoogleModelId("gemini-3.1-flash-preview")).toBe("gemini-3-flash-preview");
|
||||||
|
expect(normalizeGoogleModelId("gemini-3.1-flash-lite")).toBe("gemini-3.1-flash-lite-preview");
|
||||||
|
expect(normalizeGoogleModelId("gemini-3-pro")).toBe("gemini-3-pro-preview");
|
||||||
|
});
|
||||||
|
});
|
||||||
21
src/agents/google-model-id.ts
Normal file
21
src/agents/google-model-id.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export function normalizeGoogleModelId(id: string): string {
|
||||||
|
if (id === "gemini-3-pro") {
|
||||||
|
return "gemini-3-pro-preview";
|
||||||
|
}
|
||||||
|
if (id === "gemini-3-flash") {
|
||||||
|
return "gemini-3-flash-preview";
|
||||||
|
}
|
||||||
|
if (id === "gemini-3.1-pro") {
|
||||||
|
return "gemini-3.1-pro-preview";
|
||||||
|
}
|
||||||
|
if (id === "gemini-3.1-flash-lite") {
|
||||||
|
return "gemini-3.1-flash-lite-preview";
|
||||||
|
}
|
||||||
|
// Preserve compatibility with earlier OpenClaw docs/config that pointed at a
|
||||||
|
// non-existent Gemini Flash preview ID. Google's current Flash text model is
|
||||||
|
// `gemini-3-flash-preview`.
|
||||||
|
if (id === "gemini-3.1-flash" || id === "gemini-3.1-flash-preview") {
|
||||||
|
return "gemini-3-flash-preview";
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
38
src/agents/model-ref.test.ts
Normal file
38
src/agents/model-ref.test.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { modelKey, parseModelRef } from "./model-ref.js";
|
||||||
|
|
||||||
|
describe("modelKey", () => {
|
||||||
|
it("keeps canonical OpenRouter native ids without duplicating the provider", () => {
|
||||||
|
expect(modelKey("openrouter", "openrouter/hunter-alpha")).toBe("openrouter/hunter-alpha");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseModelRef", () => {
|
||||||
|
it("uses the default provider when omitted", () => {
|
||||||
|
expect(parseModelRef("claude-3-5-sonnet", "anthropic")).toEqual({
|
||||||
|
provider: "anthropic",
|
||||||
|
model: "claude-3-5-sonnet",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes anthropic shorthand aliases", () => {
|
||||||
|
expect(parseModelRef("anthropic/opus-4.6", "openai")).toEqual({
|
||||||
|
provider: "anthropic",
|
||||||
|
model: "claude-opus-4-6",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves nested model ids after the provider prefix", () => {
|
||||||
|
expect(parseModelRef("nvidia/moonshotai/kimi-k2.5", "anthropic")).toEqual({
|
||||||
|
provider: "nvidia",
|
||||||
|
model: "moonshotai/kimi-k2.5",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes OpenRouter-native model refs without duplicating the provider", () => {
|
||||||
|
expect(parseModelRef("openrouter/hunter-alpha", "anthropic")).toEqual({
|
||||||
|
provider: "openrouter",
|
||||||
|
model: "openrouter/hunter-alpha",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
94
src/agents/model-ref.ts
Normal file
94
src/agents/model-ref.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { normalizeGoogleModelId } from "./google-model-id.js";
|
||||||
|
import { normalizeProviderId } from "./provider-id.js";
|
||||||
|
|
||||||
|
export type ModelRef = {
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function modelKey(provider: string, model: string) {
|
||||||
|
const providerId = provider.trim();
|
||||||
|
const modelId = model.trim();
|
||||||
|
if (!providerId) {
|
||||||
|
return modelId;
|
||||||
|
}
|
||||||
|
if (!modelId) {
|
||||||
|
return providerId;
|
||||||
|
}
|
||||||
|
return modelId.toLowerCase().startsWith(`${providerId.toLowerCase()}/`)
|
||||||
|
? modelId
|
||||||
|
: `${providerId}/${modelId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function legacyModelKey(provider: string, model: string): string | null {
|
||||||
|
const providerId = provider.trim();
|
||||||
|
const modelId = model.trim();
|
||||||
|
if (!providerId || !modelId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const rawKey = `${providerId}/${modelId}`;
|
||||||
|
const canonicalKey = modelKey(providerId, modelId);
|
||||||
|
return rawKey === canonicalKey ? null : rawKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAnthropicModelId(model: string): string {
|
||||||
|
const trimmed = model.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
const lower = trimmed.toLowerCase();
|
||||||
|
switch (lower) {
|
||||||
|
case "opus-4.6":
|
||||||
|
return "claude-opus-4-6";
|
||||||
|
case "opus-4.5":
|
||||||
|
return "claude-opus-4-5";
|
||||||
|
case "sonnet-4.6":
|
||||||
|
return "claude-sonnet-4-6";
|
||||||
|
case "sonnet-4.5":
|
||||||
|
return "claude-sonnet-4-5";
|
||||||
|
default:
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProviderModelId(provider: string, model: string): string {
|
||||||
|
if (provider === "anthropic") {
|
||||||
|
return normalizeAnthropicModelId(model);
|
||||||
|
}
|
||||||
|
if (provider === "vercel-ai-gateway" && !model.includes("/")) {
|
||||||
|
const normalizedAnthropicModel = normalizeAnthropicModelId(model);
|
||||||
|
if (normalizedAnthropicModel.startsWith("claude-")) {
|
||||||
|
return `anthropic/${normalizedAnthropicModel}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (provider === "google" || provider === "google-vertex") {
|
||||||
|
return normalizeGoogleModelId(model);
|
||||||
|
}
|
||||||
|
if (provider === "openrouter" && !model.includes("/")) {
|
||||||
|
return `openrouter/${model}`;
|
||||||
|
}
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeModelRef(provider: string, model: string): ModelRef {
|
||||||
|
const normalizedProvider = normalizeProviderId(provider);
|
||||||
|
const normalizedModel = normalizeProviderModelId(normalizedProvider, model.trim());
|
||||||
|
return { provider: normalizedProvider, model: normalizedModel };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseModelRef(raw: string, defaultProvider: string): ModelRef | null {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const slash = trimmed.indexOf("/");
|
||||||
|
if (slash === -1) {
|
||||||
|
return normalizeModelRef(defaultProvider, trimmed);
|
||||||
|
}
|
||||||
|
const providerRaw = trimmed.slice(0, slash).trim();
|
||||||
|
const model = trimmed.slice(slash + 1).trim();
|
||||||
|
if (!providerRaw || !model) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizeModelRef(providerRaw, model);
|
||||||
|
}
|
||||||
@ -19,11 +19,6 @@ import { splitTrailingAuthProfile } from "./model-ref-profile.js";
|
|||||||
|
|
||||||
const log = createSubsystemLogger("model-selection");
|
const log = createSubsystemLogger("model-selection");
|
||||||
|
|
||||||
export type ModelRef = {
|
|
||||||
provider: string;
|
|
||||||
model: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive";
|
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive";
|
||||||
|
|
||||||
export type ModelAliasIndex = {
|
export type ModelAliasIndex = {
|
||||||
@ -35,31 +30,6 @@ function normalizeAliasKey(value: string): string {
|
|||||||
return value.trim().toLowerCase();
|
return value.trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function modelKey(provider: string, model: string) {
|
|
||||||
const providerId = provider.trim();
|
|
||||||
const modelId = model.trim();
|
|
||||||
if (!providerId) {
|
|
||||||
return modelId;
|
|
||||||
}
|
|
||||||
if (!modelId) {
|
|
||||||
return providerId;
|
|
||||||
}
|
|
||||||
return modelId.toLowerCase().startsWith(`${providerId.toLowerCase()}/`)
|
|
||||||
? modelId
|
|
||||||
: `${providerId}/${modelId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function legacyModelKey(provider: string, model: string): string | null {
|
|
||||||
const providerId = provider.trim();
|
|
||||||
const modelId = model.trim();
|
|
||||||
if (!providerId || !modelId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const rawKey = `${providerId}/${modelId}`;
|
|
||||||
const canonicalKey = modelKey(providerId, modelId);
|
|
||||||
return rawKey === canonicalKey ? null : rawKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findNormalizedProviderValue<T>(
|
export function findNormalizedProviderValue<T>(
|
||||||
entries: Record<string, T> | undefined,
|
entries: Record<string, T> | undefined,
|
||||||
provider: string,
|
provider: string,
|
||||||
@ -99,75 +69,6 @@ export function isCliProvider(provider: string, cfg?: OpenClawConfig): boolean {
|
|||||||
return Object.keys(backends).some((key) => normalizeProviderId(key) === normalized);
|
return Object.keys(backends).some((key) => normalizeProviderId(key) === normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeAnthropicModelId(model: string): string {
|
|
||||||
const trimmed = model.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
const lower = trimmed.toLowerCase();
|
|
||||||
// Keep alias resolution local so bundled startup paths cannot trip a TDZ on
|
|
||||||
// a module-level alias table while config parsing is still initializing.
|
|
||||||
switch (lower) {
|
|
||||||
case "opus-4.6":
|
|
||||||
return "claude-opus-4-6";
|
|
||||||
case "opus-4.5":
|
|
||||||
return "claude-opus-4-5";
|
|
||||||
case "sonnet-4.6":
|
|
||||||
return "claude-sonnet-4-6";
|
|
||||||
case "sonnet-4.5":
|
|
||||||
return "claude-sonnet-4-5";
|
|
||||||
default:
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeProviderModelId(provider: string, model: string): string {
|
|
||||||
if (provider === "anthropic") {
|
|
||||||
return normalizeAnthropicModelId(model);
|
|
||||||
}
|
|
||||||
if (provider === "vercel-ai-gateway" && !model.includes("/")) {
|
|
||||||
// Allow Vercel-specific Claude refs without an upstream prefix.
|
|
||||||
const normalizedAnthropicModel = normalizeAnthropicModelId(model);
|
|
||||||
if (normalizedAnthropicModel.startsWith("claude-")) {
|
|
||||||
return `anthropic/${normalizedAnthropicModel}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (provider === "google" || provider === "google-vertex") {
|
|
||||||
return normalizeGoogleModelId(model);
|
|
||||||
}
|
|
||||||
// OpenRouter-native models (e.g. "openrouter/aurora-alpha") need the full
|
|
||||||
// "openrouter/<name>" as the model ID sent to the API. Models from external
|
|
||||||
// providers already contain a slash (e.g. "anthropic/claude-sonnet-4-5") and
|
|
||||||
// are passed through as-is (#12924).
|
|
||||||
if (provider === "openrouter" && !model.includes("/")) {
|
|
||||||
return `openrouter/${model}`;
|
|
||||||
}
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeModelRef(provider: string, model: string): ModelRef {
|
|
||||||
const normalizedProvider = normalizeProviderId(provider);
|
|
||||||
const normalizedModel = normalizeProviderModelId(normalizedProvider, model.trim());
|
|
||||||
return { provider: normalizedProvider, model: normalizedModel };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseModelRef(raw: string, defaultProvider: string): ModelRef | null {
|
|
||||||
const trimmed = raw.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const slash = trimmed.indexOf("/");
|
|
||||||
if (slash === -1) {
|
|
||||||
return normalizeModelRef(defaultProvider, trimmed);
|
|
||||||
}
|
|
||||||
const providerRaw = trimmed.slice(0, slash).trim();
|
|
||||||
const model = trimmed.slice(slash + 1).trim();
|
|
||||||
if (!providerRaw || !model) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return normalizeModelRef(providerRaw, model);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function inferUniqueProviderFromConfiguredModels(params: {
|
export function inferUniqueProviderFromConfiguredModels(params: {
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
model: string;
|
model: string;
|
||||||
|
|||||||
@ -1,82 +1,30 @@
|
|||||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import {
|
||||||
|
applyExtensionHostDefaultModel,
|
||||||
|
mergeExtensionHostConfigPatch,
|
||||||
|
pickExtensionHostAuthMethod,
|
||||||
|
resolveExtensionHostProviderMatch,
|
||||||
|
} from "../extension-host/provider-auth.js";
|
||||||
import type { ProviderAuthMethod, ProviderPlugin } from "../plugins/types.js";
|
import type { ProviderAuthMethod, ProviderPlugin } from "../plugins/types.js";
|
||||||
|
|
||||||
export function resolveProviderMatch(
|
export function resolveProviderMatch(
|
||||||
providers: ProviderPlugin[],
|
providers: ProviderPlugin[],
|
||||||
rawProvider?: string,
|
rawProvider?: string,
|
||||||
): ProviderPlugin | null {
|
): ProviderPlugin | null {
|
||||||
const raw = rawProvider?.trim();
|
return resolveExtensionHostProviderMatch(providers, rawProvider);
|
||||||
if (!raw) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const normalized = normalizeProviderId(raw);
|
|
||||||
return (
|
|
||||||
providers.find((provider) => normalizeProviderId(provider.id) === normalized) ??
|
|
||||||
providers.find(
|
|
||||||
(provider) =>
|
|
||||||
provider.aliases?.some((alias) => normalizeProviderId(alias) === normalized) ?? false,
|
|
||||||
) ??
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pickAuthMethod(
|
export function pickAuthMethod(
|
||||||
provider: ProviderPlugin,
|
provider: ProviderPlugin,
|
||||||
rawMethod?: string,
|
rawMethod?: string,
|
||||||
): ProviderAuthMethod | null {
|
): ProviderAuthMethod | null {
|
||||||
const raw = rawMethod?.trim();
|
return pickExtensionHostAuthMethod(provider, rawMethod);
|
||||||
if (!raw) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const normalized = raw.toLowerCase();
|
|
||||||
return (
|
|
||||||
provider.auth.find((method) => method.id.toLowerCase() === normalized) ??
|
|
||||||
provider.auth.find((method) => method.label.toLowerCase() === normalized) ??
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
||||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeConfigPatch<T>(base: T, patch: unknown): T {
|
export function mergeConfigPatch<T>(base: T, patch: unknown): T {
|
||||||
if (!isPlainRecord(base) || !isPlainRecord(patch)) {
|
return mergeExtensionHostConfigPatch(base, patch);
|
||||||
return patch as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
const next: Record<string, unknown> = { ...base };
|
|
||||||
for (const [key, value] of Object.entries(patch)) {
|
|
||||||
const existing = next[key];
|
|
||||||
if (isPlainRecord(existing) && isPlainRecord(value)) {
|
|
||||||
next[key] = mergeConfigPatch(existing, value);
|
|
||||||
} else {
|
|
||||||
next[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return next as T;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyDefaultModel(cfg: OpenClawConfig, model: string): OpenClawConfig {
|
export function applyDefaultModel(cfg: OpenClawConfig, model: string): OpenClawConfig {
|
||||||
const models = { ...cfg.agents?.defaults?.models };
|
return applyExtensionHostDefaultModel(cfg, model);
|
||||||
models[model] = models[model] ?? {};
|
|
||||||
|
|
||||||
const existingModel = cfg.agents?.defaults?.model;
|
|
||||||
return {
|
|
||||||
...cfg,
|
|
||||||
agents: {
|
|
||||||
...cfg.agents,
|
|
||||||
defaults: {
|
|
||||||
...cfg.agents?.defaults,
|
|
||||||
models,
|
|
||||||
model: {
|
|
||||||
...(existingModel && typeof existingModel === "object" && "fallbacks" in existingModel
|
|
||||||
? { fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks }
|
|
||||||
: undefined),
|
|
||||||
primary: model,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
106
src/extension-host/provider-auth.test.ts
Normal file
106
src/extension-host/provider-auth.test.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import type { ProviderPlugin } from "../plugins/types.js";
|
||||||
|
import {
|
||||||
|
applyExtensionHostDefaultModel,
|
||||||
|
mergeExtensionHostConfigPatch,
|
||||||
|
pickExtensionHostAuthMethod,
|
||||||
|
resolveExtensionHostProviderMatch,
|
||||||
|
} from "./provider-auth.js";
|
||||||
|
|
||||||
|
function makeProvider(overrides: Partial<ProviderPlugin> & Pick<ProviderPlugin, "id" | "label">) {
|
||||||
|
return {
|
||||||
|
auth: [],
|
||||||
|
...overrides,
|
||||||
|
} satisfies ProviderPlugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("resolveExtensionHostProviderMatch", () => {
|
||||||
|
it("matches providers by normalized id and aliases", () => {
|
||||||
|
const providers = [
|
||||||
|
makeProvider({
|
||||||
|
id: "openrouter",
|
||||||
|
label: "OpenRouter",
|
||||||
|
aliases: ["Open Router"],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(resolveExtensionHostProviderMatch(providers, "openrouter")?.id).toBe("openrouter");
|
||||||
|
expect(resolveExtensionHostProviderMatch(providers, " Open Router ")?.id).toBe("openrouter");
|
||||||
|
expect(resolveExtensionHostProviderMatch(providers, "missing")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("pickExtensionHostAuthMethod", () => {
|
||||||
|
it("matches auth methods by id or label", () => {
|
||||||
|
const provider = makeProvider({
|
||||||
|
id: "ollama",
|
||||||
|
label: "Ollama",
|
||||||
|
auth: [
|
||||||
|
{ id: "local", label: "Local", kind: "custom", run: vi.fn() },
|
||||||
|
{ id: "cloud", label: "Cloud", kind: "custom", run: vi.fn() },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(pickExtensionHostAuthMethod(provider, "local")?.id).toBe("local");
|
||||||
|
expect(pickExtensionHostAuthMethod(provider, "cloud")?.id).toBe("cloud");
|
||||||
|
expect(pickExtensionHostAuthMethod(provider, "Cloud")?.id).toBe("cloud");
|
||||||
|
expect(pickExtensionHostAuthMethod(provider, "missing")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mergeExtensionHostConfigPatch", () => {
|
||||||
|
it("deep-merges plain record config patches", () => {
|
||||||
|
expect(
|
||||||
|
mergeExtensionHostConfigPatch(
|
||||||
|
{
|
||||||
|
models: { providers: { ollama: { baseUrl: "http://127.0.0.1:11434" } } },
|
||||||
|
auth: { profiles: { existing: { provider: "anthropic" } } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
models: { providers: { ollama: { api: "ollama" } } },
|
||||||
|
auth: { profiles: { fresh: { provider: "ollama" } } },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
models: { providers: { ollama: { baseUrl: "http://127.0.0.1:11434", api: "ollama" } } },
|
||||||
|
auth: {
|
||||||
|
profiles: {
|
||||||
|
existing: { provider: "anthropic" },
|
||||||
|
fresh: { provider: "ollama" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applyExtensionHostDefaultModel", () => {
|
||||||
|
it("sets the primary model while preserving fallback config", () => {
|
||||||
|
expect(
|
||||||
|
applyExtensionHostDefaultModel(
|
||||||
|
{
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: {
|
||||||
|
primary: "anthropic/claude-sonnet-4-5",
|
||||||
|
fallbacks: ["openai/gpt-5"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ollama/qwen3:4b",
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
models: {
|
||||||
|
"ollama/qwen3:4b": {},
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
primary: "ollama/qwen3:4b",
|
||||||
|
fallbacks: ["openai/gpt-5"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
82
src/extension-host/provider-auth.ts
Normal file
82
src/extension-host/provider-auth.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||||
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import type { ProviderAuthMethod, ProviderPlugin } from "../plugins/types.js";
|
||||||
|
|
||||||
|
export function resolveExtensionHostProviderMatch(
|
||||||
|
providers: ProviderPlugin[],
|
||||||
|
rawProvider?: string,
|
||||||
|
): ProviderPlugin | null {
|
||||||
|
const raw = rawProvider?.trim();
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normalized = normalizeProviderId(raw);
|
||||||
|
return (
|
||||||
|
providers.find((provider) => normalizeProviderId(provider.id) === normalized) ??
|
||||||
|
providers.find(
|
||||||
|
(provider) =>
|
||||||
|
provider.aliases?.some((alias) => normalizeProviderId(alias) === normalized) ?? false,
|
||||||
|
) ??
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickExtensionHostAuthMethod(
|
||||||
|
provider: ProviderPlugin,
|
||||||
|
rawMethod?: string,
|
||||||
|
): ProviderAuthMethod | null {
|
||||||
|
const raw = rawMethod?.trim();
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normalized = raw.toLowerCase();
|
||||||
|
return (
|
||||||
|
provider.auth.find((method) => method.id.toLowerCase() === normalized) ??
|
||||||
|
provider.auth.find((method) => method.label.toLowerCase() === normalized) ??
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeExtensionHostConfigPatch<T>(base: T, patch: unknown): T {
|
||||||
|
if (!isPlainRecord(base) || !isPlainRecord(patch)) {
|
||||||
|
return patch as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next: Record<string, unknown> = { ...base };
|
||||||
|
for (const [key, value] of Object.entries(patch)) {
|
||||||
|
const existing = next[key];
|
||||||
|
if (isPlainRecord(existing) && isPlainRecord(value)) {
|
||||||
|
next[key] = mergeExtensionHostConfigPatch(existing, value);
|
||||||
|
} else {
|
||||||
|
next[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyExtensionHostDefaultModel(cfg: OpenClawConfig, model: string): OpenClawConfig {
|
||||||
|
const models = { ...cfg.agents?.defaults?.models };
|
||||||
|
models[model] = models[model] ?? {};
|
||||||
|
|
||||||
|
const existingModel = cfg.agents?.defaults?.model;
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
agents: {
|
||||||
|
...cfg.agents,
|
||||||
|
defaults: {
|
||||||
|
...cfg.agents?.defaults,
|
||||||
|
models,
|
||||||
|
model: {
|
||||||
|
...(existingModel && typeof existingModel === "object" && "fallbacks" in existingModel
|
||||||
|
? { fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks }
|
||||||
|
: undefined),
|
||||||
|
primary: model,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
83
src/extension-host/provider-wizard.test.ts
Normal file
83
src/extension-host/provider-wizard.test.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import type { ProviderPlugin } from "../plugins/types.js";
|
||||||
|
import {
|
||||||
|
buildExtensionHostProviderMethodChoice,
|
||||||
|
resolveExtensionHostProviderChoice,
|
||||||
|
resolveExtensionHostProviderModelPickerEntries,
|
||||||
|
resolveExtensionHostProviderWizardOptions,
|
||||||
|
} from "./provider-wizard.js";
|
||||||
|
|
||||||
|
function makeProvider(overrides: Partial<ProviderPlugin> & Pick<ProviderPlugin, "id" | "label">) {
|
||||||
|
return {
|
||||||
|
auth: [],
|
||||||
|
...overrides,
|
||||||
|
} satisfies ProviderPlugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("resolveExtensionHostProviderWizardOptions", () => {
|
||||||
|
it("uses explicit onboarding choice ids and bound method ids", () => {
|
||||||
|
const provider = makeProvider({
|
||||||
|
id: "vllm",
|
||||||
|
label: "vLLM",
|
||||||
|
auth: [
|
||||||
|
{ id: "local", label: "Local", kind: "custom", run: vi.fn() },
|
||||||
|
{ id: "cloud", label: "Cloud", kind: "custom", run: vi.fn() },
|
||||||
|
],
|
||||||
|
wizard: {
|
||||||
|
onboarding: {
|
||||||
|
choiceId: "self-hosted-vllm",
|
||||||
|
methodId: "local",
|
||||||
|
choiceLabel: "vLLM local",
|
||||||
|
groupId: "local-runtimes",
|
||||||
|
groupLabel: "Local runtimes",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolveExtensionHostProviderWizardOptions([provider])).toEqual([
|
||||||
|
{
|
||||||
|
value: "self-hosted-vllm",
|
||||||
|
label: "vLLM local",
|
||||||
|
groupId: "local-runtimes",
|
||||||
|
groupLabel: "Local runtimes",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
resolveExtensionHostProviderChoice({
|
||||||
|
providers: [provider],
|
||||||
|
choice: "self-hosted-vllm",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
provider,
|
||||||
|
method: provider.auth[0],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveExtensionHostProviderModelPickerEntries", () => {
|
||||||
|
it("builds model-picker entries from provider metadata", () => {
|
||||||
|
const provider = makeProvider({
|
||||||
|
id: "sglang",
|
||||||
|
label: "SGLang",
|
||||||
|
auth: [
|
||||||
|
{ id: "server", label: "Server", kind: "custom", run: vi.fn() },
|
||||||
|
{ id: "cloud", label: "Cloud", kind: "custom", run: vi.fn() },
|
||||||
|
],
|
||||||
|
wizard: {
|
||||||
|
modelPicker: {
|
||||||
|
label: "SGLang server",
|
||||||
|
hint: "OpenAI-compatible local runtime",
|
||||||
|
methodId: "server",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolveExtensionHostProviderModelPickerEntries([provider])).toEqual([
|
||||||
|
{
|
||||||
|
value: buildExtensionHostProviderMethodChoice("sglang", "server"),
|
||||||
|
label: "SGLang server",
|
||||||
|
hint: "OpenAI-compatible local runtime",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
201
src/extension-host/provider-wizard.ts
Normal file
201
src/extension-host/provider-wizard.ts
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||||
|
import type {
|
||||||
|
ProviderAuthMethod,
|
||||||
|
ProviderPlugin,
|
||||||
|
ProviderPluginWizardModelPicker,
|
||||||
|
ProviderPluginWizardOnboarding,
|
||||||
|
} from "../plugins/types.js";
|
||||||
|
|
||||||
|
export const EXTENSION_HOST_PROVIDER_CHOICE_PREFIX = "provider-plugin:";
|
||||||
|
|
||||||
|
export type ExtensionHostProviderWizardOption = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
hint?: string;
|
||||||
|
groupId: string;
|
||||||
|
groupLabel: string;
|
||||||
|
groupHint?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExtensionHostProviderModelPickerEntry = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
hint?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeChoiceId(choiceId: string): string {
|
||||||
|
return choiceId.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveWizardOnboardingChoiceId(
|
||||||
|
provider: ProviderPlugin,
|
||||||
|
wizard: ProviderPluginWizardOnboarding,
|
||||||
|
): string {
|
||||||
|
const explicit = wizard.choiceId?.trim();
|
||||||
|
if (explicit) {
|
||||||
|
return explicit;
|
||||||
|
}
|
||||||
|
const explicitMethodId = wizard.methodId?.trim();
|
||||||
|
if (explicitMethodId) {
|
||||||
|
return buildExtensionHostProviderMethodChoice(provider.id, explicitMethodId);
|
||||||
|
}
|
||||||
|
if (provider.auth.length === 1) {
|
||||||
|
return provider.id;
|
||||||
|
}
|
||||||
|
return buildExtensionHostProviderMethodChoice(provider.id, provider.auth[0]?.id ?? "default");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMethodById(
|
||||||
|
provider: ProviderPlugin,
|
||||||
|
methodId?: string,
|
||||||
|
): ProviderAuthMethod | undefined {
|
||||||
|
const normalizedMethodId = methodId?.trim().toLowerCase();
|
||||||
|
if (!normalizedMethodId) {
|
||||||
|
return provider.auth[0];
|
||||||
|
}
|
||||||
|
return provider.auth.find((method) => method.id.trim().toLowerCase() === normalizedMethodId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOnboardingOptionForMethod(params: {
|
||||||
|
provider: ProviderPlugin;
|
||||||
|
wizard: ProviderPluginWizardOnboarding;
|
||||||
|
method: ProviderAuthMethod;
|
||||||
|
value: string;
|
||||||
|
}): ExtensionHostProviderWizardOption {
|
||||||
|
const normalizedGroupId = params.wizard.groupId?.trim() || params.provider.id;
|
||||||
|
return {
|
||||||
|
value: normalizeChoiceId(params.value),
|
||||||
|
label:
|
||||||
|
params.wizard.choiceLabel?.trim() ||
|
||||||
|
(params.provider.auth.length === 1 ? params.provider.label : params.method.label),
|
||||||
|
hint: params.wizard.choiceHint?.trim() || params.method.hint,
|
||||||
|
groupId: normalizedGroupId,
|
||||||
|
groupLabel: params.wizard.groupLabel?.trim() || params.provider.label,
|
||||||
|
groupHint: params.wizard.groupHint?.trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveModelPickerChoiceValue(
|
||||||
|
provider: ProviderPlugin,
|
||||||
|
modelPicker: ProviderPluginWizardModelPicker,
|
||||||
|
): string {
|
||||||
|
const explicitMethodId = modelPicker.methodId?.trim();
|
||||||
|
if (explicitMethodId) {
|
||||||
|
return buildExtensionHostProviderMethodChoice(provider.id, explicitMethodId);
|
||||||
|
}
|
||||||
|
if (provider.auth.length === 1) {
|
||||||
|
return provider.id;
|
||||||
|
}
|
||||||
|
return buildExtensionHostProviderMethodChoice(provider.id, provider.auth[0]?.id ?? "default");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildExtensionHostProviderMethodChoice(
|
||||||
|
providerId: string,
|
||||||
|
methodId: string,
|
||||||
|
): string {
|
||||||
|
return `${EXTENSION_HOST_PROVIDER_CHOICE_PREFIX}${providerId.trim()}:${methodId.trim()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveExtensionHostProviderWizardOptions(
|
||||||
|
providers: ProviderPlugin[],
|
||||||
|
): ExtensionHostProviderWizardOption[] {
|
||||||
|
const options: ExtensionHostProviderWizardOption[] = [];
|
||||||
|
|
||||||
|
for (const provider of providers) {
|
||||||
|
const wizard = provider.wizard?.onboarding;
|
||||||
|
if (!wizard) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const explicitMethod = resolveMethodById(provider, wizard.methodId);
|
||||||
|
if (explicitMethod) {
|
||||||
|
options.push(
|
||||||
|
buildOnboardingOptionForMethod({
|
||||||
|
provider,
|
||||||
|
wizard,
|
||||||
|
method: explicitMethod,
|
||||||
|
value: resolveWizardOnboardingChoiceId(provider, wizard),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const method of provider.auth) {
|
||||||
|
options.push(
|
||||||
|
buildOnboardingOptionForMethod({
|
||||||
|
provider,
|
||||||
|
wizard,
|
||||||
|
method,
|
||||||
|
value: buildExtensionHostProviderMethodChoice(provider.id, method.id),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveExtensionHostProviderModelPickerEntries(
|
||||||
|
providers: ProviderPlugin[],
|
||||||
|
): ExtensionHostProviderModelPickerEntry[] {
|
||||||
|
const entries: ExtensionHostProviderModelPickerEntry[] = [];
|
||||||
|
|
||||||
|
for (const provider of providers) {
|
||||||
|
const modelPicker = provider.wizard?.modelPicker;
|
||||||
|
if (!modelPicker) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entries.push({
|
||||||
|
value: resolveModelPickerChoiceValue(provider, modelPicker),
|
||||||
|
label: modelPicker.label?.trim() || `${provider.label} (custom)`,
|
||||||
|
hint: modelPicker.hint?.trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveExtensionHostProviderChoice(params: {
|
||||||
|
providers: ProviderPlugin[];
|
||||||
|
choice: string;
|
||||||
|
}): { provider: ProviderPlugin; method: ProviderAuthMethod } | null {
|
||||||
|
const choice = params.choice.trim();
|
||||||
|
if (!choice) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (choice.startsWith(EXTENSION_HOST_PROVIDER_CHOICE_PREFIX)) {
|
||||||
|
const payload = choice.slice(EXTENSION_HOST_PROVIDER_CHOICE_PREFIX.length);
|
||||||
|
const separator = payload.indexOf(":");
|
||||||
|
const providerId = separator >= 0 ? payload.slice(0, separator) : payload;
|
||||||
|
const methodId = separator >= 0 ? payload.slice(separator + 1) : undefined;
|
||||||
|
const provider = params.providers.find(
|
||||||
|
(entry) => normalizeProviderId(entry.id) === normalizeProviderId(providerId),
|
||||||
|
);
|
||||||
|
if (!provider) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const method = resolveMethodById(provider, methodId);
|
||||||
|
return method ? { provider, method } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const provider of params.providers) {
|
||||||
|
const onboarding = provider.wizard?.onboarding;
|
||||||
|
if (onboarding) {
|
||||||
|
const onboardingChoiceId = resolveWizardOnboardingChoiceId(provider, onboarding);
|
||||||
|
if (normalizeChoiceId(onboardingChoiceId) === choice) {
|
||||||
|
const method = resolveMethodById(provider, onboarding.methodId);
|
||||||
|
if (method) {
|
||||||
|
return { provider, method };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
normalizeProviderId(provider.id) === normalizeProviderId(choice) &&
|
||||||
|
provider.auth.length > 0
|
||||||
|
) {
|
||||||
|
return { provider, method: provider.auth[0] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@ -1,15 +1,16 @@
|
|||||||
import { DEFAULT_PROVIDER } from "../agents/defaults.js";
|
import { DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||||
import { parseModelRef } from "../agents/model-selection.js";
|
import { parseModelRef } from "../agents/model-ref.js";
|
||||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import {
|
||||||
|
buildExtensionHostProviderMethodChoice,
|
||||||
|
resolveExtensionHostProviderChoice,
|
||||||
|
resolveExtensionHostProviderModelPickerEntries,
|
||||||
|
resolveExtensionHostProviderWizardOptions,
|
||||||
|
} from "../extension-host/provider-wizard.js";
|
||||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
import { resolvePluginProviders } from "./providers.js";
|
import { resolvePluginProviders } from "./providers.js";
|
||||||
import type {
|
import type { ProviderAuthMethod, ProviderPlugin } from "./types.js";
|
||||||
ProviderAuthMethod,
|
|
||||||
ProviderPlugin,
|
|
||||||
ProviderPluginWizardModelPicker,
|
|
||||||
ProviderPluginWizardOnboarding,
|
|
||||||
} from "./types.js";
|
|
||||||
|
|
||||||
export const PROVIDER_PLUGIN_CHOICE_PREFIX = "provider-plugin:";
|
export const PROVIDER_PLUGIN_CHOICE_PREFIX = "provider-plugin:";
|
||||||
|
|
||||||
@ -28,60 +29,8 @@ export type ProviderModelPickerEntry = {
|
|||||||
hint?: string;
|
hint?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeChoiceId(choiceId: string): string {
|
|
||||||
return choiceId.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveWizardOnboardingChoiceId(
|
|
||||||
provider: ProviderPlugin,
|
|
||||||
wizard: ProviderPluginWizardOnboarding,
|
|
||||||
): string {
|
|
||||||
const explicit = wizard.choiceId?.trim();
|
|
||||||
if (explicit) {
|
|
||||||
return explicit;
|
|
||||||
}
|
|
||||||
const explicitMethodId = wizard.methodId?.trim();
|
|
||||||
if (explicitMethodId) {
|
|
||||||
return buildProviderPluginMethodChoice(provider.id, explicitMethodId);
|
|
||||||
}
|
|
||||||
if (provider.auth.length === 1) {
|
|
||||||
return provider.id;
|
|
||||||
}
|
|
||||||
return buildProviderPluginMethodChoice(provider.id, provider.auth[0]?.id ?? "default");
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveMethodById(
|
|
||||||
provider: ProviderPlugin,
|
|
||||||
methodId?: string,
|
|
||||||
): ProviderAuthMethod | undefined {
|
|
||||||
const normalizedMethodId = methodId?.trim().toLowerCase();
|
|
||||||
if (!normalizedMethodId) {
|
|
||||||
return provider.auth[0];
|
|
||||||
}
|
|
||||||
return provider.auth.find((method) => method.id.trim().toLowerCase() === normalizedMethodId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildOnboardingOptionForMethod(params: {
|
|
||||||
provider: ProviderPlugin;
|
|
||||||
wizard: ProviderPluginWizardOnboarding;
|
|
||||||
method: ProviderAuthMethod;
|
|
||||||
value: string;
|
|
||||||
}): ProviderWizardOption {
|
|
||||||
const normalizedGroupId = params.wizard.groupId?.trim() || params.provider.id;
|
|
||||||
return {
|
|
||||||
value: normalizeChoiceId(params.value),
|
|
||||||
label:
|
|
||||||
params.wizard.choiceLabel?.trim() ||
|
|
||||||
(params.provider.auth.length === 1 ? params.provider.label : params.method.label),
|
|
||||||
hint: params.wizard.choiceHint?.trim() || params.method.hint,
|
|
||||||
groupId: normalizedGroupId,
|
|
||||||
groupLabel: params.wizard.groupLabel?.trim() || params.provider.label,
|
|
||||||
groupHint: params.wizard.groupHint?.trim(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildProviderPluginMethodChoice(providerId: string, methodId: string): string {
|
export function buildProviderPluginMethodChoice(providerId: string, methodId: string): string {
|
||||||
return `${PROVIDER_PLUGIN_CHOICE_PREFIX}${providerId.trim()}:${methodId.trim()}`;
|
return buildExtensionHostProviderMethodChoice(providerId, methodId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveProviderWizardOptions(params: {
|
export function resolveProviderWizardOptions(params: {
|
||||||
@ -89,54 +38,7 @@ export function resolveProviderWizardOptions(params: {
|
|||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
}): ProviderWizardOption[] {
|
}): ProviderWizardOption[] {
|
||||||
const providers = resolvePluginProviders(params);
|
return resolveExtensionHostProviderWizardOptions(resolvePluginProviders(params));
|
||||||
const options: ProviderWizardOption[] = [];
|
|
||||||
|
|
||||||
for (const provider of providers) {
|
|
||||||
const wizard = provider.wizard?.onboarding;
|
|
||||||
if (!wizard) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const explicitMethod = resolveMethodById(provider, wizard.methodId);
|
|
||||||
if (explicitMethod) {
|
|
||||||
options.push(
|
|
||||||
buildOnboardingOptionForMethod({
|
|
||||||
provider,
|
|
||||||
wizard,
|
|
||||||
method: explicitMethod,
|
|
||||||
value: resolveWizardOnboardingChoiceId(provider, wizard),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const method of provider.auth) {
|
|
||||||
options.push(
|
|
||||||
buildOnboardingOptionForMethod({
|
|
||||||
provider,
|
|
||||||
wizard,
|
|
||||||
method,
|
|
||||||
value: buildProviderPluginMethodChoice(provider.id, method.id),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveModelPickerChoiceValue(
|
|
||||||
provider: ProviderPlugin,
|
|
||||||
modelPicker: ProviderPluginWizardModelPicker,
|
|
||||||
): string {
|
|
||||||
const explicitMethodId = modelPicker.methodId?.trim();
|
|
||||||
if (explicitMethodId) {
|
|
||||||
return buildProviderPluginMethodChoice(provider.id, explicitMethodId);
|
|
||||||
}
|
|
||||||
if (provider.auth.length === 1) {
|
|
||||||
return provider.id;
|
|
||||||
}
|
|
||||||
return buildProviderPluginMethodChoice(provider.id, provider.auth[0]?.id ?? "default");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveProviderModelPickerEntries(params: {
|
export function resolveProviderModelPickerEntries(params: {
|
||||||
@ -144,68 +46,14 @@ export function resolveProviderModelPickerEntries(params: {
|
|||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
}): ProviderModelPickerEntry[] {
|
}): ProviderModelPickerEntry[] {
|
||||||
const providers = resolvePluginProviders(params);
|
return resolveExtensionHostProviderModelPickerEntries(resolvePluginProviders(params));
|
||||||
const entries: ProviderModelPickerEntry[] = [];
|
|
||||||
|
|
||||||
for (const provider of providers) {
|
|
||||||
const modelPicker = provider.wizard?.modelPicker;
|
|
||||||
if (!modelPicker) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
entries.push({
|
|
||||||
value: resolveModelPickerChoiceValue(provider, modelPicker),
|
|
||||||
label: modelPicker.label?.trim() || `${provider.label} (custom)`,
|
|
||||||
hint: modelPicker.hint?.trim(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveProviderPluginChoice(params: {
|
export function resolveProviderPluginChoice(params: {
|
||||||
providers: ProviderPlugin[];
|
providers: ProviderPlugin[];
|
||||||
choice: string;
|
choice: string;
|
||||||
}): { provider: ProviderPlugin; method: ProviderAuthMethod } | null {
|
}): { provider: ProviderPlugin; method: ProviderAuthMethod } | null {
|
||||||
const choice = params.choice.trim();
|
return resolveExtensionHostProviderChoice(params);
|
||||||
if (!choice) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (choice.startsWith(PROVIDER_PLUGIN_CHOICE_PREFIX)) {
|
|
||||||
const payload = choice.slice(PROVIDER_PLUGIN_CHOICE_PREFIX.length);
|
|
||||||
const separator = payload.indexOf(":");
|
|
||||||
const providerId = separator >= 0 ? payload.slice(0, separator) : payload;
|
|
||||||
const methodId = separator >= 0 ? payload.slice(separator + 1) : undefined;
|
|
||||||
const provider = params.providers.find(
|
|
||||||
(entry) => normalizeProviderId(entry.id) === normalizeProviderId(providerId),
|
|
||||||
);
|
|
||||||
if (!provider) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const method = resolveMethodById(provider, methodId);
|
|
||||||
return method ? { provider, method } : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const provider of params.providers) {
|
|
||||||
const onboarding = provider.wizard?.onboarding;
|
|
||||||
if (onboarding) {
|
|
||||||
const onboardingChoiceId = resolveWizardOnboardingChoiceId(provider, onboarding);
|
|
||||||
if (normalizeChoiceId(onboardingChoiceId) === choice) {
|
|
||||||
const method = resolveMethodById(provider, onboarding.methodId);
|
|
||||||
if (method) {
|
|
||||||
return { provider, method };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
normalizeProviderId(provider.id) === normalizeProviderId(choice) &&
|
|
||||||
provider.auth.length > 0
|
|
||||||
) {
|
|
||||||
return { provider, method: provider.auth[0] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runProviderModelSelectedHook(params: {
|
export async function runProviderModelSelectedHook(params: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user