feat(cli): add dench-cloud model catalog and config helpers
Shared module for Dench Cloud catalog fetching, API key validation, and OpenClaw config patching.
This commit is contained in:
parent
e490380d01
commit
1af869f64b
@ -47,7 +47,7 @@
|
||||
"prepack": "pnpm build:plugin-env && pnpm build && pnpm web:build && pnpm web:prepack",
|
||||
"start": "node denchclaw.mjs",
|
||||
"test": "pnpm test:cli && pnpm --dir apps/web test",
|
||||
"test:cli": "vitest run --config vitest.unit.config.ts src/cli/run-main.test.ts src/cli/bootstrap-external.test.ts src/cli/bootstrap-external.bootstrap-command.test.ts src/cli/workspace-seed.test.ts src/cli/web-runtime.test.ts src/cli/web-runtime-command.test.ts src/cli/flatten-standalone-deps.test.ts",
|
||||
"test:cli": "vitest run --config vitest.unit.config.ts src/cli/run-main.test.ts src/cli/bootstrap-external.test.ts src/cli/bootstrap-external.bootstrap-command.test.ts src/cli/dench-cloud.test.ts src/cli/workspace-seed.test.ts src/cli/web-runtime.test.ts src/cli/web-runtime-command.test.ts src/cli/flatten-standalone-deps.test.ts",
|
||||
"test:web": "pnpm --dir apps/web test",
|
||||
"web:build": "pnpm --dir apps/web build",
|
||||
"web:dev": "pnpm --dir apps/web dev",
|
||||
|
||||
172
src/cli/dench-cloud.test.ts
Normal file
172
src/cli/dench-cloud.test.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
buildDenchCloudConfigPatch,
|
||||
fetchDenchCloudCatalog,
|
||||
normalizeDenchCloudCatalogResponse,
|
||||
readConfiguredDenchCloudSettings,
|
||||
validateDenchCloudApiKey,
|
||||
} from "./dench-cloud.js";
|
||||
|
||||
function createJsonResponse(params?: {
|
||||
status?: number;
|
||||
payload?: unknown;
|
||||
}): Response {
|
||||
const status = params?.status ?? 200;
|
||||
return {
|
||||
status,
|
||||
ok: status >= 200 && status < 300,
|
||||
json: async () => params?.payload ?? {},
|
||||
} as unknown as Response;
|
||||
}
|
||||
|
||||
describe("dench-cloud helpers", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("normalizes the public gateway catalog into stable model records", () => {
|
||||
const models = normalizeDenchCloudCatalogResponse({
|
||||
object: "list",
|
||||
data: [
|
||||
{
|
||||
id: "gpt-5.4",
|
||||
stableId: "gpt-5.4",
|
||||
name: "GPT-5.4",
|
||||
provider: "openai",
|
||||
transportProvider: "openai",
|
||||
input: ["text", "image"],
|
||||
contextWindow: 128000,
|
||||
maxTokens: 128000,
|
||||
supportsStreaming: true,
|
||||
supportsImages: true,
|
||||
supportsResponses: true,
|
||||
supportsReasoning: false,
|
||||
cost: {
|
||||
input: 3.375,
|
||||
output: 20.25,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
marginPercent: 0.35,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(models).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "gpt-5.4",
|
||||
stableId: "gpt-5.4",
|
||||
displayName: "GPT-5.4",
|
||||
contextWindow: 128000,
|
||||
maxTokens: 128000,
|
||||
cost: expect.objectContaining({
|
||||
input: 3.375,
|
||||
output: 20.25,
|
||||
marginPercent: 0.35,
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("falls back to the bundled model list when the public catalog is unavailable", async () => {
|
||||
const fetchMock = vi.fn(async () => createJsonResponse({ status: 503, payload: {} }));
|
||||
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||
|
||||
const result = await fetchDenchCloudCatalog("https://gateway.merseoriginals.com");
|
||||
expect(fetchMock).toHaveBeenCalledWith("https://gateway.merseoriginals.com/v1/public/models");
|
||||
expect(result.source).toBe("fallback");
|
||||
expect(result.models.map((model) => model.stableId)).toEqual([
|
||||
"anthropic.claude-opus-4-6-v1",
|
||||
"gpt-5.4",
|
||||
"anthropic.claude-sonnet-4-6-v1",
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects invalid Dench Cloud API keys with an actionable message", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => createJsonResponse({ status: 401, payload: {} })) as unknown as typeof fetch,
|
||||
);
|
||||
|
||||
await expect(
|
||||
validateDenchCloudApiKey("https://gateway.merseoriginals.com", "bad-key"),
|
||||
).rejects.toThrow("Check your key at dench.com/settings");
|
||||
});
|
||||
|
||||
it("builds the Dench Cloud config patch with provider models and agent aliases", () => {
|
||||
const patch = buildDenchCloudConfigPatch({
|
||||
gatewayUrl: "https://gateway.merseoriginals.com",
|
||||
apiKey: "dench_live_key",
|
||||
models: [
|
||||
{
|
||||
id: "claude-opus-4.6",
|
||||
stableId: "anthropic.claude-opus-4-6-v1",
|
||||
displayName: "Claude Opus 4.6",
|
||||
provider: "anthropic",
|
||||
transportProvider: "bedrock",
|
||||
api: "openai-completions",
|
||||
input: ["text", "image"],
|
||||
reasoning: false,
|
||||
contextWindow: 200000,
|
||||
maxTokens: 64000,
|
||||
supportsStreaming: true,
|
||||
supportsImages: true,
|
||||
supportsResponses: true,
|
||||
supportsReasoning: false,
|
||||
cost: {
|
||||
input: 6.75,
|
||||
output: 33.75,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
marginPercent: 0.35,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(patch.models.providers["dench-cloud"]).toEqual(
|
||||
expect.objectContaining({
|
||||
baseUrl: "https://gateway.merseoriginals.com/v1",
|
||||
apiKey: "dench_live_key",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
expect.objectContaining({
|
||||
id: "anthropic.claude-opus-4-6-v1",
|
||||
name: "Claude Opus 4.6 (Dench Cloud)",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(patch.agents.defaults.models["dench-cloud/anthropic.claude-opus-4-6-v1"]).toEqual(
|
||||
expect.objectContaining({
|
||||
alias: "Claude Opus 4.6 (Dench Cloud)",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("reads existing Dench Cloud gateway config from openclaw.json", () => {
|
||||
const result = readConfiguredDenchCloudSettings({
|
||||
models: {
|
||||
providers: {
|
||||
"dench-cloud": {
|
||||
baseUrl: "https://gateway.merseoriginals.com/v1",
|
||||
apiKey: "dench_cfg_key",
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "dench-cloud/anthropic.claude-opus-4-6-v1",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
gatewayUrl: "https://gateway.merseoriginals.com",
|
||||
apiKey: "dench_cfg_key",
|
||||
selectedModel: "anthropic.claude-opus-4-6-v1",
|
||||
});
|
||||
});
|
||||
});
|
||||
453
src/cli/dench-cloud.ts
Normal file
453
src/cli/dench-cloud.ts
Normal file
@ -0,0 +1,453 @@
|
||||
export const DEFAULT_DENCH_CLOUD_GATEWAY_URL = "https://gateway.merseoriginals.com";
|
||||
export const DEFAULT_DENCH_CLOUD_MARGIN_PERCENT = 0.35;
|
||||
|
||||
export type DenchCloudCatalogCost = {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
marginPercent?: number;
|
||||
};
|
||||
|
||||
export type DenchCloudCatalogModel = {
|
||||
id: string;
|
||||
stableId: string;
|
||||
displayName: string;
|
||||
provider: "openai" | "anthropic";
|
||||
transportProvider: "openai" | "bedrock";
|
||||
api: "openai-completions";
|
||||
input: Array<"text" | "image">;
|
||||
reasoning: boolean;
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
supportsStreaming: boolean;
|
||||
supportsImages: boolean;
|
||||
supportsResponses: boolean;
|
||||
supportsReasoning: boolean;
|
||||
cost: DenchCloudCatalogCost;
|
||||
};
|
||||
|
||||
export type DenchCloudCatalogSource = "live" | "fallback";
|
||||
|
||||
export type DenchCloudCatalogLoadResult = {
|
||||
models: DenchCloudCatalogModel[];
|
||||
source: DenchCloudCatalogSource;
|
||||
detail?: string;
|
||||
};
|
||||
|
||||
type UnknownRecord = Record<string, unknown>;
|
||||
|
||||
function roundUsd(value: number): number {
|
||||
return Number(value.toFixed(8));
|
||||
}
|
||||
|
||||
function markupCost(value: number): number {
|
||||
return roundUsd(value * (1 + DEFAULT_DENCH_CLOUD_MARGIN_PERCENT));
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): UnknownRecord | undefined {
|
||||
return value && typeof value === "object" ? (value as UnknownRecord) : undefined;
|
||||
}
|
||||
|
||||
function readString(input: UnknownRecord, ...keys: string[]): string | undefined {
|
||||
for (const key of keys) {
|
||||
const value = input[key];
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readNumber(input: UnknownRecord, ...keys: string[]): number | undefined {
|
||||
for (const key of keys) {
|
||||
const value = input[key];
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readBoolean(input: UnknownRecord, ...keys: string[]): boolean | undefined {
|
||||
for (const key of keys) {
|
||||
const value = input[key];
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isProvider(value: string | undefined): value is "openai" | "anthropic" {
|
||||
return value === "openai" || value === "anthropic";
|
||||
}
|
||||
|
||||
function isTransportProvider(value: string | undefined): value is "openai" | "bedrock" {
|
||||
return value === "openai" || value === "bedrock";
|
||||
}
|
||||
|
||||
function normalizeInputKinds(input: unknown, supportsImages: boolean): Array<"text" | "image"> {
|
||||
if (!Array.isArray(input)) {
|
||||
return supportsImages ? ["text", "image"] : ["text"];
|
||||
}
|
||||
|
||||
const kinds = new Set<"text" | "image">();
|
||||
for (const value of input) {
|
||||
if (value === "text" || value === "image") {
|
||||
kinds.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!kinds.has("text")) {
|
||||
kinds.add("text");
|
||||
}
|
||||
if (supportsImages) {
|
||||
kinds.add("image");
|
||||
}
|
||||
return [...kinds];
|
||||
}
|
||||
|
||||
export function normalizeDenchGatewayUrl(value: string | undefined): string {
|
||||
const raw = (value || DEFAULT_DENCH_CLOUD_GATEWAY_URL).trim();
|
||||
const withProtocol = raw.startsWith("http://") || raw.startsWith("https://")
|
||||
? raw
|
||||
: `https://${raw}`;
|
||||
return withProtocol.replace(/\/+$/, "").replace(/\/v1$/u, "");
|
||||
}
|
||||
|
||||
export function buildDenchGatewayApiBaseUrl(gatewayUrl: string | undefined): string {
|
||||
return `${normalizeDenchGatewayUrl(gatewayUrl)}/v1`;
|
||||
}
|
||||
|
||||
export function buildDenchGatewayCatalogUrl(gatewayUrl: string | undefined): string {
|
||||
return `${normalizeDenchGatewayUrl(gatewayUrl)}/v1/public/models`;
|
||||
}
|
||||
|
||||
export const RECOMMENDED_DENCH_CLOUD_MODEL_ID = "claude-opus-4.6";
|
||||
|
||||
export const FALLBACK_DENCH_CLOUD_MODELS: DenchCloudCatalogModel[] = [
|
||||
{
|
||||
id: "claude-opus-4.6",
|
||||
stableId: "anthropic.claude-opus-4-6-v1",
|
||||
displayName: "Claude Opus 4.6",
|
||||
provider: "anthropic",
|
||||
transportProvider: "bedrock",
|
||||
api: "openai-completions",
|
||||
input: ["text", "image"],
|
||||
reasoning: false,
|
||||
contextWindow: 200000,
|
||||
maxTokens: 64000,
|
||||
supportsStreaming: true,
|
||||
supportsImages: true,
|
||||
supportsResponses: true,
|
||||
supportsReasoning: false,
|
||||
cost: {
|
||||
input: markupCost(5),
|
||||
output: markupCost(25),
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
marginPercent: DEFAULT_DENCH_CLOUD_MARGIN_PERCENT,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "gpt-5.4",
|
||||
stableId: "gpt-5.4",
|
||||
displayName: "GPT-5.4",
|
||||
provider: "openai",
|
||||
transportProvider: "openai",
|
||||
api: "openai-completions",
|
||||
input: ["text", "image"],
|
||||
reasoning: false,
|
||||
contextWindow: 128000,
|
||||
maxTokens: 128000,
|
||||
supportsStreaming: true,
|
||||
supportsImages: true,
|
||||
supportsResponses: true,
|
||||
supportsReasoning: false,
|
||||
cost: {
|
||||
input: markupCost(2.5),
|
||||
output: markupCost(15),
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
marginPercent: DEFAULT_DENCH_CLOUD_MARGIN_PERCENT,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-4.6",
|
||||
stableId: "anthropic.claude-sonnet-4-6-v1",
|
||||
displayName: "Claude Sonnet 4.6",
|
||||
provider: "anthropic",
|
||||
transportProvider: "bedrock",
|
||||
api: "openai-completions",
|
||||
input: ["text", "image"],
|
||||
reasoning: false,
|
||||
contextWindow: 200000,
|
||||
maxTokens: 64000,
|
||||
supportsStreaming: true,
|
||||
supportsImages: true,
|
||||
supportsResponses: true,
|
||||
supportsReasoning: false,
|
||||
cost: {
|
||||
input: markupCost(3),
|
||||
output: markupCost(15),
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
marginPercent: DEFAULT_DENCH_CLOUD_MARGIN_PERCENT,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function cloneFallbackDenchCloudModels(): DenchCloudCatalogModel[] {
|
||||
return FALLBACK_DENCH_CLOUD_MODELS.map((model) => ({
|
||||
...model,
|
||||
input: [...model.input],
|
||||
cost: { ...model.cost },
|
||||
}));
|
||||
}
|
||||
|
||||
export function normalizeDenchCloudCatalogModel(input: unknown): DenchCloudCatalogModel | null {
|
||||
const record = asRecord(input);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const publicId = readString(record, "id", "publicId", "public_id");
|
||||
const stableId = readString(record, "stableId", "stable_id") || publicId;
|
||||
const displayName = readString(record, "name", "displayName", "display_name");
|
||||
const provider = readString(record, "provider");
|
||||
const transportProvider = readString(record, "transportProvider", "transport_provider");
|
||||
if (!publicId || !stableId || !displayName || !isProvider(provider) || !isTransportProvider(transportProvider)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const supportsImages = readBoolean(record, "supportsImages", "supports_images") ?? false;
|
||||
const supportsStreaming = readBoolean(record, "supportsStreaming", "supports_streaming") ?? true;
|
||||
const supportsResponses = readBoolean(record, "supportsResponses", "supports_responses") ?? true;
|
||||
const supportsReasoning = readBoolean(record, "supportsReasoning", "supports_reasoning")
|
||||
?? readBoolean(record, "reasoning")
|
||||
?? false;
|
||||
const contextWindow = readNumber(record, "contextWindow", "context_window") ?? 128000;
|
||||
const maxTokens = readNumber(record, "maxTokens", "max_tokens", "maxOutputTokens", "max_output_tokens") ?? 128000;
|
||||
|
||||
const costRecord = asRecord(record.cost) ?? {};
|
||||
const inputCost = readNumber(costRecord, "input") ?? 0;
|
||||
const outputCost = readNumber(costRecord, "output") ?? 0;
|
||||
const cacheRead = readNumber(costRecord, "cacheRead", "cache_read") ?? 0;
|
||||
const cacheWrite = readNumber(costRecord, "cacheWrite", "cache_write") ?? 0;
|
||||
const marginPercent = readNumber(costRecord, "marginPercent", "margin_percent");
|
||||
|
||||
return {
|
||||
id: publicId,
|
||||
stableId,
|
||||
displayName,
|
||||
provider,
|
||||
transportProvider,
|
||||
api: "openai-completions",
|
||||
input: normalizeInputKinds(record.input, supportsImages),
|
||||
reasoning: supportsReasoning,
|
||||
contextWindow,
|
||||
maxTokens,
|
||||
supportsStreaming,
|
||||
supportsImages,
|
||||
supportsResponses,
|
||||
supportsReasoning,
|
||||
cost: {
|
||||
input: inputCost,
|
||||
output: outputCost,
|
||||
cacheRead,
|
||||
cacheWrite,
|
||||
...(marginPercent !== undefined ? { marginPercent } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeDenchCloudCatalogResponse(payload: unknown): DenchCloudCatalogModel[] {
|
||||
const root = asRecord(payload);
|
||||
const data = root?.data;
|
||||
if (!Array.isArray(data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const models: DenchCloudCatalogModel[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const entry of data) {
|
||||
const normalized = normalizeDenchCloudCatalogModel(entry);
|
||||
if (!normalized || seen.has(normalized.stableId)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(normalized.stableId);
|
||||
models.push(normalized);
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
export async function fetchDenchCloudCatalog(
|
||||
gatewayUrl: string,
|
||||
): Promise<DenchCloudCatalogLoadResult> {
|
||||
try {
|
||||
const response = await fetch(buildDenchGatewayCatalogUrl(gatewayUrl));
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
const models = normalizeDenchCloudCatalogResponse(payload);
|
||||
if (!models.length) {
|
||||
throw new Error("response did not contain any usable models");
|
||||
}
|
||||
|
||||
return {
|
||||
models,
|
||||
source: "live",
|
||||
};
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
models: cloneFallbackDenchCloudModels(),
|
||||
source: "fallback",
|
||||
detail,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateDenchCloudApiKey(
|
||||
gatewayUrl: string,
|
||||
apiKey: string,
|
||||
): Promise<void> {
|
||||
const response = await fetch(`${buildDenchGatewayApiBaseUrl(gatewayUrl)}/models`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message =
|
||||
response.status === 401 || response.status === 403
|
||||
? "Invalid Dench Cloud API key."
|
||||
: `Dench Cloud validation failed with HTTP ${response.status}.`;
|
||||
throw new Error(`${message} Check your key at dench.com/settings.`);
|
||||
}
|
||||
|
||||
export function buildDenchCloudProviderModels(models: DenchCloudCatalogModel[]) {
|
||||
return models.map((model) => ({
|
||||
id: model.stableId,
|
||||
name: `${model.displayName} (Dench Cloud)`,
|
||||
reasoning: model.reasoning,
|
||||
input: [...model.input],
|
||||
cost: {
|
||||
input: model.cost.input,
|
||||
output: model.cost.output,
|
||||
cacheRead: model.cost.cacheRead,
|
||||
cacheWrite: model.cost.cacheWrite,
|
||||
},
|
||||
contextWindow: model.contextWindow,
|
||||
maxTokens: model.maxTokens,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildDenchCloudAgentModelEntries(models: DenchCloudCatalogModel[]) {
|
||||
return Object.fromEntries(
|
||||
models.map((model) => [
|
||||
`dench-cloud/${model.stableId}`,
|
||||
{ alias: `${model.displayName} (Dench Cloud)` },
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export function buildDenchCloudProviderConfig(params: {
|
||||
gatewayUrl: string;
|
||||
apiKey: string;
|
||||
models: DenchCloudCatalogModel[];
|
||||
}) {
|
||||
return {
|
||||
baseUrl: buildDenchGatewayApiBaseUrl(params.gatewayUrl),
|
||||
apiKey: params.apiKey,
|
||||
api: "openai-completions",
|
||||
models: buildDenchCloudProviderModels(params.models),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDenchCloudConfigPatch(params: {
|
||||
gatewayUrl: string;
|
||||
apiKey: string;
|
||||
models: DenchCloudCatalogModel[];
|
||||
}) {
|
||||
return {
|
||||
models: {
|
||||
mode: "merge",
|
||||
providers: {
|
||||
"dench-cloud": buildDenchCloudProviderConfig(params),
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
models: buildDenchCloudAgentModelEntries(params.models),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveDenchCloudModel(
|
||||
models: DenchCloudCatalogModel[],
|
||||
requestedId: string | undefined,
|
||||
): DenchCloudCatalogModel | undefined {
|
||||
const normalized = requestedId?.trim();
|
||||
if (!normalized) {
|
||||
return (
|
||||
models.find((model) => model.id === RECOMMENDED_DENCH_CLOUD_MODEL_ID) ||
|
||||
models[0]
|
||||
);
|
||||
}
|
||||
|
||||
return models.find((model) => model.id === normalized || model.stableId === normalized);
|
||||
}
|
||||
|
||||
export function formatDenchCloudModelHint(model: DenchCloudCatalogModel): string {
|
||||
const parts: string[] = [model.provider];
|
||||
if (model.reasoning) parts.push("reasoning");
|
||||
if (model.id === RECOMMENDED_DENCH_CLOUD_MODEL_ID) parts.push("recommended");
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
export function readConfiguredDenchCloudSettings(
|
||||
rawConfig: Record<string, unknown> | undefined,
|
||||
): {
|
||||
gatewayUrl?: string;
|
||||
apiKey?: string;
|
||||
selectedModel?: string;
|
||||
} {
|
||||
const provider = asRecord(
|
||||
asRecord(asRecord(rawConfig?.models)?.providers)?.["dench-cloud"],
|
||||
);
|
||||
const defaults = asRecord(asRecord(rawConfig?.agents)?.defaults);
|
||||
const modelValue = defaults?.model;
|
||||
const modelSetting = asRecord(modelValue);
|
||||
const modelPrimary =
|
||||
typeof modelValue === "string"
|
||||
? modelValue
|
||||
: typeof modelSetting?.primary === "string"
|
||||
? modelSetting.primary
|
||||
: undefined;
|
||||
|
||||
const selectedModel =
|
||||
typeof modelPrimary === "string" && modelPrimary.startsWith("dench-cloud/")
|
||||
? modelPrimary.slice("dench-cloud/".length)
|
||||
: undefined;
|
||||
|
||||
const baseUrl = readString(provider ?? {}, "baseUrl", "base_url");
|
||||
return {
|
||||
gatewayUrl: baseUrl ? normalizeDenchGatewayUrl(baseUrl) : undefined,
|
||||
apiKey: readString(provider ?? {}, "apiKey", "api_key"),
|
||||
selectedModel,
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user