Add the harness, envs, per-bot models and chat attachments
- Harness: providers, model catalog synced hourly, automatic free picks, cooldowns, tool loop with context trimming, MCP computer tools - Bots queue runs replies and tasks through the harness; jobs wait (attempt refunded) while every model is resting - Envs: Settings > Environment, scoped general/workspace/computer, written to computers and redacted from tool output - Models: Settings > Models, per-harness provider select defaulting to OpenRouter, and a models modal on each bot's chat (the coordinator's edits the defaults) - Chat: thinking row while a reply runs, file attachments (upload, paste, drop) that replies read as images, audio and document text - Fix media uploads failing after a user's first file, and private files being served from the public media path Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_HARNESS_MODELS } from "@/db/types/db";
|
||||
import type { HarnessKind } from "@/src/dict/harness-dict";
|
||||
import modelServesHarness from "@/src/harness/catalog/model-serves-harness";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import harnessModelRowToCatalog from "./harness-model-row-to-catalog";
|
||||
|
||||
type Params = {
|
||||
harness: HarnessKind;
|
||||
model_id: number;
|
||||
/**
|
||||
* The provider the setting is limited to, if any.
|
||||
*/
|
||||
provider_id?: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Throws unless the model is available, can do the harness's job and,
|
||||
* when the setting is limited to a provider, comes from it.
|
||||
*/
|
||||
export default async function assertHarnessModel({ harness, model_id, provider_id }: Params) {
|
||||
const row = (
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_HARNESS_MODELS>({
|
||||
sql: `SELECT * FROM harness_models WHERE id = ? AND available = 1 LIMIT 1`,
|
||||
values: [model_id],
|
||||
})
|
||||
).payload?.[0];
|
||||
|
||||
if (!row) {
|
||||
throw new AppError(`That model isn't available anymore.`);
|
||||
}
|
||||
|
||||
if (provider_id && row.provider_id != provider_id) {
|
||||
throw new AppError(`That model isn't from the picked provider.`);
|
||||
}
|
||||
|
||||
if (!modelServesHarness({ model: harnessModelRowToCatalog({ row }), harness })) {
|
||||
throw new AppError(`That model can't do this job.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabHarnessProviders from "./grab-harness-providers";
|
||||
|
||||
type Params = {
|
||||
provider_id: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Throws unless the provider exists and is turned on.
|
||||
*/
|
||||
export default async function assertHarnessProvider({ provider_id }: Params) {
|
||||
const provider = (await grabHarnessProviders({ provider_id }))[0];
|
||||
|
||||
if (!provider?.id || !provider.enabled) {
|
||||
throw new AppError(`That provider isn't available. Turn it on in Settings, under Models.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabHarnessProviders from "./grab-harness-providers";
|
||||
|
||||
type Params = {
|
||||
provider_id: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a provider and its models. Harnesses (default and per bot) that
|
||||
* picked one of its models, or were limited to it, go back to automatic
|
||||
* on any provider.
|
||||
*/
|
||||
export default async function deleteHarnessProvider({ provider_id }: Params) {
|
||||
const provider = (await grabHarnessProviders({ provider_id }))[0];
|
||||
|
||||
if (!provider?.id) {
|
||||
throw new AppError(`Provider not found.`);
|
||||
}
|
||||
|
||||
for (const table of ["harness_settings", "bot_harness_settings"]) {
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE ${table} SET model_id = NULL, provider_id = NULL, updated_at = ?
|
||||
WHERE provider_id = ? OR model_id IN (SELECT id FROM harness_models WHERE provider_id = ?)`,
|
||||
values: [Date.now(), provider.id, provider.id],
|
||||
});
|
||||
}
|
||||
|
||||
const res = await BunSQLite.sql({
|
||||
sql: `DELETE FROM harness_providers WHERE id = ?`,
|
||||
values: [provider.id],
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(`Couldn't remove the provider: ${res.error}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type {
|
||||
BUN_SQLITE_COBBLE_HARNESS_PROVIDERS,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
import { DefaultHarnessProviderKind } from "@/src/dict/harness-provider-dict";
|
||||
import grabProviderPreset from "@/src/harness/providers/grab-provider-preset";
|
||||
|
||||
/**
|
||||
* Adds the default provider (OpenRouter) when no provider exists, so the
|
||||
* free models work as soon as a key is set. Idempotent; a super admin who
|
||||
* removed it and added others doesn't get it back.
|
||||
*/
|
||||
export default async function ensureDefaultHarnessProvider() {
|
||||
const existing = await BunSQLite.sql<BUN_SQLITE_COBBLE_HARNESS_PROVIDERS>({
|
||||
sql: `SELECT id FROM harness_providers LIMIT 1`,
|
||||
});
|
||||
|
||||
if (existing.payload?.[0]) return;
|
||||
|
||||
const preset = grabProviderPreset({ kind: DefaultHarnessProviderKind });
|
||||
|
||||
const res = await BunSQLite.insert<
|
||||
BUN_SQLITE_COBBLE_HARNESS_PROVIDERS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "harness_providers",
|
||||
data: [
|
||||
{
|
||||
kind: preset.value,
|
||||
name: preset.title,
|
||||
base_url: preset.base_url,
|
||||
enabled: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(`Couldn't add the default model provider: ${res.msg}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Harnesses } from "@/src/dict/harness-dict";
|
||||
import type { BotHarnessOverviewPayload } from "@/src/types";
|
||||
import grabHarnessModelEntries from "./grab-harness-model-entries";
|
||||
import grabHarnessOverviewItem from "./grab-harness-overview-item";
|
||||
import grabHarnessSetting from "./grab-harness-setting";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* A worker bot the caller has checked belongs to the session user.
|
||||
*/
|
||||
bot_id: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A worker bot's model settings page: per harness, the bot's own setting
|
||||
* (or the default it inherits), the models it can pick, what it runs on
|
||||
* now, and what the default runs on.
|
||||
*/
|
||||
export default async function grabBotHarnessOverview({
|
||||
bot_id,
|
||||
}: Params): Promise<BotHarnessOverviewPayload> {
|
||||
const [own, defaults] = await Promise.all([
|
||||
Promise.all(
|
||||
Harnesses.map((harness) => grabHarnessSetting({ harness: harness.value, bot_id })),
|
||||
),
|
||||
Promise.all(
|
||||
Harnesses.map((harness) => grabHarnessSetting({ harness: harness.value })),
|
||||
),
|
||||
]);
|
||||
|
||||
const entries = await grabHarnessModelEntries({
|
||||
pinned_ids: [...own, ...defaults].flatMap((setting) =>
|
||||
setting.model_id ? [setting.model_id] : [],
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
harnesses: await Promise.all(
|
||||
own.map(async (setting, index) => {
|
||||
const [item, default_item] = await Promise.all([
|
||||
grabHarnessOverviewItem({ harness: setting.harness, setting, entries, bot_id }),
|
||||
grabHarnessOverviewItem({
|
||||
harness: setting.harness,
|
||||
setting: defaults[index]!,
|
||||
entries,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
...item,
|
||||
inherit: setting.source == "default",
|
||||
default: {
|
||||
enabled: default_item.enabled,
|
||||
current: default_item.current,
|
||||
candidate_count: default_item.candidate_count,
|
||||
},
|
||||
};
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_HARNESS_PROVIDERS } from "@/db/types/db";
|
||||
import { DefaultHarnessProviderKind } from "@/src/dict/harness-provider-dict";
|
||||
|
||||
/**
|
||||
* The default provider (`DefaultHarnessProviderKind`, OpenRouter): the
|
||||
* oldest enabled one of that kind. Undefined once it's removed or turned
|
||||
* off, which leaves harnesses on any provider.
|
||||
*/
|
||||
export default async function grabDefaultHarnessProviderId() {
|
||||
const row = (
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_HARNESS_PROVIDERS>({
|
||||
sql: `SELECT id FROM harness_providers WHERE kind = ? AND enabled = 1 ORDER BY id ASC LIMIT 1`,
|
||||
values: [DefaultHarnessProviderKind],
|
||||
})
|
||||
).payload?.[0];
|
||||
|
||||
return row?.id || undefined;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_HARNESS_MODELS } from "@/db/types/db";
|
||||
import type { HarnessCatalogModel } from "@/src/harness/types";
|
||||
import type { HarnessModelOption } from "@/src/types";
|
||||
import harnessModelRowToCatalog from "./harness-model-row-to-catalog";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Picked models, listed even when no longer available.
|
||||
*/
|
||||
pinned_ids?: number[];
|
||||
};
|
||||
|
||||
type ModelRow = BUN_SQLITE_COBBLE_HARNESS_MODELS & { provider_name?: string };
|
||||
|
||||
export type HarnessModelEntry = {
|
||||
row: ModelRow;
|
||||
model: HarnessCatalogModel;
|
||||
option: HarnessModelOption;
|
||||
};
|
||||
|
||||
/**
|
||||
* Every model a setting can pick (available, on an enabled provider),
|
||||
* free first, plus the given picked ones, each with its picker option.
|
||||
*/
|
||||
export default async function grabHarnessModelEntries({
|
||||
pinned_ids = [],
|
||||
}: Params = {}): Promise<HarnessModelEntry[]> {
|
||||
const ids = [...new Set(pinned_ids)];
|
||||
|
||||
const rows =
|
||||
(
|
||||
await BunSQLite.sql<ModelRow>({
|
||||
sql: `SELECT m.*, p.name AS provider_name FROM harness_models m
|
||||
JOIN harness_providers p ON p.id = m.provider_id
|
||||
WHERE (m.available = 1 AND p.enabled = 1)
|
||||
${ids.length ? `OR m.id IN (${ids.map(() => "?").join(", ")})` : ""}
|
||||
ORDER BY m.free DESC, p.id ASC, m.name ASC`,
|
||||
values: ids,
|
||||
})
|
||||
).payload || [];
|
||||
|
||||
return rows.map((row) => {
|
||||
const model = harnessModelRowToCatalog({ row });
|
||||
|
||||
return {
|
||||
row,
|
||||
model,
|
||||
option: {
|
||||
id: row.id || 0,
|
||||
provider_id: row.provider_id || 0,
|
||||
provider_name: row.provider_name || "",
|
||||
slug: model.slug,
|
||||
name: `${model.name || model.slug}${row.available == 0 ? " (no longer listed)" : ""}`,
|
||||
free: model.free,
|
||||
voices: model.voices,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { HarnessKind } from "@/src/dict/harness-dict";
|
||||
import { Harnesses } from "@/src/dict/harness-dict";
|
||||
import modelServesHarness from "@/src/harness/catalog/model-serves-harness";
|
||||
import resolveHarnessCandidates from "@/src/harness/resolve/resolve-harness-candidates";
|
||||
import type { HarnessOverview } from "@/src/types";
|
||||
import type { HarnessModelEntry } from "./grab-harness-model-entries";
|
||||
import type { HarnessSetting } from "./grab-harness-setting";
|
||||
|
||||
type Params = {
|
||||
harness: HarnessKind;
|
||||
setting: HarnessSetting;
|
||||
entries: HarnessModelEntry[];
|
||||
/**
|
||||
* Resolve what runs now for this bot instead of the default.
|
||||
*/
|
||||
bot_id?: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* One harness as the model settings pages show it: its setting, the
|
||||
* models it can be set to, and the model it would use right now.
|
||||
*/
|
||||
export default async function grabHarnessOverviewItem({
|
||||
harness,
|
||||
setting,
|
||||
entries,
|
||||
bot_id,
|
||||
}: Params): Promise<HarnessOverview> {
|
||||
const meta = Harnesses.find((h) => h.value == harness)!;
|
||||
const candidates = setting.enabled
|
||||
? await resolveHarnessCandidates({ harness, bot_id }).catch(() => [])
|
||||
: [];
|
||||
const first = candidates[0];
|
||||
const serving = entries.filter(({ model }) => modelServesHarness({ model, harness }));
|
||||
const providers = [
|
||||
...new Map(
|
||||
serving.map(({ option }) => [
|
||||
option.provider_id,
|
||||
{ id: option.provider_id, name: option.provider_name },
|
||||
]),
|
||||
).values(),
|
||||
];
|
||||
|
||||
if (setting.provider_id && !providers.some((p) => p.id == setting.provider_id)) {
|
||||
providers.push({ id: setting.provider_id, name: "Unavailable provider" });
|
||||
}
|
||||
const current = first
|
||||
? entries.find(({ row, model }) => `${row.provider_id}:${model.slug}` == first.key)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
harness,
|
||||
title: meta.title,
|
||||
description: meta.description,
|
||||
required: meta.required,
|
||||
model_id: setting.model_id,
|
||||
provider_id: setting.provider_id,
|
||||
providers,
|
||||
fallback: setting.fallback,
|
||||
enabled: setting.enabled,
|
||||
options: setting.options,
|
||||
models: serving.map(({ option }) => option),
|
||||
current: current && first ? { ...current.option, source: first.source } : undefined,
|
||||
candidate_count: candidates.length,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Harnesses } from "@/src/dict/harness-dict";
|
||||
import type { HarnessOverviewPayload } from "@/src/types";
|
||||
import grabHarnessModelEntries from "./grab-harness-model-entries";
|
||||
import grabHarnessOverviewItem from "./grab-harness-overview-item";
|
||||
import grabHarnessProviders from "./grab-harness-providers";
|
||||
import grabHarnessSetting from "./grab-harness-setting";
|
||||
import toPublicHarnessProvider from "./to-public-harness-provider";
|
||||
|
||||
/**
|
||||
* Everything the Models settings page shows: the providers (without keys)
|
||||
* and, per harness, its default setting, the models it can be set to
|
||||
* (free first) and what it runs on right now.
|
||||
*/
|
||||
export default async function grabHarnessOverview(): Promise<HarnessOverviewPayload> {
|
||||
const providers = await grabHarnessProviders();
|
||||
const settings = await Promise.all(
|
||||
Harnesses.map((harness) => grabHarnessSetting({ harness: harness.value })),
|
||||
);
|
||||
const entries = await grabHarnessModelEntries({
|
||||
pinned_ids: settings.flatMap((setting) =>
|
||||
setting.model_id ? [setting.model_id] : [],
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
providers: providers.map((provider) => toPublicHarnessProvider({ provider })),
|
||||
harnesses: await Promise.all(
|
||||
settings.map((setting) =>
|
||||
grabHarnessOverviewItem({ harness: setting.harness, setting, entries }),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_HARNESS_PROVIDERS } from "@/db/types/db";
|
||||
|
||||
type Params = {
|
||||
provider_id?: number | string;
|
||||
enabled_only?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider rows, oldest first. They hold the encrypted key: server only,
|
||||
* never hand them to the client as they are (`toPublicHarnessProvider`).
|
||||
*/
|
||||
export default async function grabHarnessProviders({
|
||||
provider_id,
|
||||
enabled_only,
|
||||
}: Params = {}) {
|
||||
const where: string[] = [];
|
||||
const values: (string | number)[] = [];
|
||||
|
||||
if (provider_id) {
|
||||
where.push(`id = ?`);
|
||||
values.push(provider_id);
|
||||
}
|
||||
|
||||
if (enabled_only) {
|
||||
where.push(`enabled = 1`);
|
||||
}
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_HARNESS_PROVIDERS>({
|
||||
sql: `SELECT * FROM harness_providers ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY id ASC`,
|
||||
values,
|
||||
});
|
||||
|
||||
return res.payload || [];
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type {
|
||||
BUN_SQLITE_COBBLE_BOT_HARNESS_SETTINGS,
|
||||
BUN_SQLITE_COBBLE_HARNESS_SETTINGS,
|
||||
} from "@/db/types/db";
|
||||
import type { HarnessKind } from "@/src/dict/harness-dict";
|
||||
import parseJobJson from "@/src/bots-queue/parse-job-json";
|
||||
import grabDefaultHarnessProviderId from "./grab-default-harness-provider-id";
|
||||
|
||||
export type HarnessSetting = {
|
||||
harness: HarnessKind;
|
||||
/**
|
||||
* The picked `harness_models` row; undefined picks automatically.
|
||||
*/
|
||||
model_id?: number;
|
||||
/**
|
||||
* The `harness_providers` row the harness is limited to (its pick
|
||||
* and its automatic picks); undefined: any provider.
|
||||
*/
|
||||
provider_id?: number;
|
||||
fallback: boolean;
|
||||
enabled: boolean;
|
||||
options: Record<string, unknown>;
|
||||
/**
|
||||
* `bot` when a worker bot overrides the default for this harness.
|
||||
*/
|
||||
source: "default" | "bot";
|
||||
};
|
||||
|
||||
type Params = {
|
||||
harness: HarnessKind;
|
||||
/**
|
||||
* The bot the call is for. A worker bot's own setting wins; the
|
||||
* coordinator always uses the default.
|
||||
*/
|
||||
bot_id?: number | string;
|
||||
};
|
||||
|
||||
type SettingRow = Pick<
|
||||
BUN_SQLITE_COBBLE_HARNESS_SETTINGS,
|
||||
"model_id" | "provider_id" | "fallback" | "enabled" | "options"
|
||||
>;
|
||||
|
||||
/**
|
||||
* A harness's setting for a call. With no row it is automatic on the
|
||||
* default provider (OpenRouter), on, with fallback. Text is always on.
|
||||
*/
|
||||
export default async function grabHarnessSetting({
|
||||
harness,
|
||||
bot_id,
|
||||
}: Params): Promise<HarnessSetting> {
|
||||
const bot_row = bot_id
|
||||
? await grabBotOverride({ harness, bot_id })
|
||||
: undefined;
|
||||
|
||||
if (bot_row) {
|
||||
return toSetting({ harness, row: bot_row, source: "bot" });
|
||||
}
|
||||
|
||||
const row = (
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_HARNESS_SETTINGS>({
|
||||
sql: `SELECT * FROM harness_settings WHERE harness = ? LIMIT 1`,
|
||||
values: [harness],
|
||||
})
|
||||
).payload?.[0];
|
||||
|
||||
if (!row) {
|
||||
return {
|
||||
...toSetting({ harness, source: "default" }),
|
||||
provider_id: await grabDefaultHarnessProviderId(),
|
||||
};
|
||||
}
|
||||
|
||||
return toSetting({ harness, row, source: "default" });
|
||||
}
|
||||
|
||||
async function grabBotOverride({
|
||||
harness,
|
||||
bot_id,
|
||||
}: {
|
||||
harness: HarnessKind;
|
||||
bot_id: number | string;
|
||||
}) {
|
||||
return (
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_HARNESS_SETTINGS>({
|
||||
sql: `SELECT s.* FROM bot_harness_settings s
|
||||
JOIN bots b ON b.id = s.bot_id
|
||||
WHERE s.bot_id = ? AND s.harness = ? AND b.kind != 'coordinator'
|
||||
LIMIT 1`,
|
||||
values: [bot_id, harness],
|
||||
})
|
||||
).payload?.[0];
|
||||
}
|
||||
|
||||
function toSetting({
|
||||
harness,
|
||||
row,
|
||||
source,
|
||||
}: {
|
||||
harness: HarnessKind;
|
||||
row?: SettingRow;
|
||||
source: HarnessSetting["source"];
|
||||
}): HarnessSetting {
|
||||
return {
|
||||
harness,
|
||||
model_id: typeof row?.model_id == "number" ? row.model_id : undefined,
|
||||
provider_id: typeof row?.provider_id == "number" ? row.provider_id : undefined,
|
||||
fallback: row ? row.fallback != 0 : true,
|
||||
enabled: harness == "text" || !row || row.enabled != 0,
|
||||
options: parseJobJson({ text: row?.options }) || {},
|
||||
source,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { BUN_SQLITE_COBBLE_HARNESS_MODELS } from "@/db/types/db";
|
||||
import type { HarnessCatalogModel } from "@/src/harness/types";
|
||||
|
||||
type Params = {
|
||||
row: BUN_SQLITE_COBBLE_HARNESS_MODELS;
|
||||
};
|
||||
|
||||
/**
|
||||
* A `harness_models` row as a catalog entry.
|
||||
*/
|
||||
export default function harnessModelRowToCatalog({
|
||||
row,
|
||||
}: Params): HarnessCatalogModel {
|
||||
return {
|
||||
slug: row.slug || "",
|
||||
name: row.name || row.slug || "",
|
||||
free: row.free == 1,
|
||||
input_modalities: parseList({ text: row.input_modalities }) || ["text"],
|
||||
output_modalities: parseList({ text: row.output_modalities }) || [
|
||||
"text",
|
||||
],
|
||||
supports_tools: row.supports_tools == 1,
|
||||
context_length: numberOrUndefined(row.context_length),
|
||||
max_output_tokens: numberOrUndefined(row.max_output_tokens),
|
||||
voices: parseList({ text: row.voices }),
|
||||
intelligence_score: numberOrUndefined(row.intelligence_score),
|
||||
agentic_score: numberOrUndefined(row.agentic_score),
|
||||
released_at: numberOrUndefined(row.released_at),
|
||||
expires_at: numberOrUndefined(row.expires_at),
|
||||
};
|
||||
}
|
||||
|
||||
function parseList({ text }: { text?: string | null }) {
|
||||
if (!text) return undefined;
|
||||
|
||||
try {
|
||||
const value = JSON.parse(text);
|
||||
return Array.isArray(value) ? value.map(String) : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function numberOrUndefined(value: unknown) {
|
||||
return typeof value == "number" ? value : undefined;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { BUN_SQLITE_COBBLE_HARNESS_PROVIDERS } from "@/db/types/db";
|
||||
import type { HarnessProviderKind } from "@/src/dict/harness-provider-dict";
|
||||
import grabProviderPreset from "@/src/harness/providers/grab-provider-preset";
|
||||
import type { HarnessProviderConfig } from "@/src/harness/types";
|
||||
import { decrypt } from "@/src/utils/crypt";
|
||||
import grabEnv from "../envs/grab-env";
|
||||
|
||||
type Params = {
|
||||
provider: BUN_SQLITE_COBBLE_HARNESS_PROVIDERS;
|
||||
};
|
||||
|
||||
/**
|
||||
* A provider row as the adapters need it, with its key decrypted (or, if
|
||||
* none is saved, the preset's env: set in Settings, under Environment, or
|
||||
* the process environment, `grabEnv`). `usable` is
|
||||
* false when the provider needs a key and has none.
|
||||
*/
|
||||
export default async function resolveProviderConfig({
|
||||
provider,
|
||||
}: Params): Promise<{ config: HarnessProviderConfig; usable: boolean }> {
|
||||
const preset = grabProviderPreset({ kind: provider.kind });
|
||||
let api_key: string | undefined;
|
||||
|
||||
if (provider.api_key) {
|
||||
api_key = await decrypt(provider.api_key).catch(() => undefined);
|
||||
}
|
||||
|
||||
if (!api_key && preset.key_env) {
|
||||
api_key = await grabEnv({ key: preset.key_env });
|
||||
}
|
||||
|
||||
const config: HarnessProviderConfig = {
|
||||
id: provider.id || undefined,
|
||||
kind: preset.value as HarnessProviderKind,
|
||||
name: provider.name || preset.title,
|
||||
base_url: provider.base_url || preset.base_url,
|
||||
api_key,
|
||||
};
|
||||
|
||||
return {
|
||||
config,
|
||||
usable: Boolean(config.base_url) && (!preset.needs_key || !!api_key),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { HarnessKind } from "@/src/dict/harness-dict";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import assertHarnessModel from "./assert-harness-model";
|
||||
import assertHarnessProvider from "./assert-harness-provider";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* A worker bot the caller has checked belongs to the session user.
|
||||
*/
|
||||
bot_id: number;
|
||||
harness: HarnessKind;
|
||||
/**
|
||||
* Drop the bot's own setting and use the default again.
|
||||
*/
|
||||
inherit: boolean;
|
||||
model_id: number | null;
|
||||
/**
|
||||
* Limits the harness to one provider; null: any.
|
||||
*/
|
||||
provider_id?: number | null;
|
||||
fallback: boolean;
|
||||
enabled: boolean;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves (or, with `inherit`, removes) a worker bot's own setting for one
|
||||
* harness. Same rules as the default: the model must be available and fit
|
||||
* the job, and text can't be turned off.
|
||||
*/
|
||||
export default async function saveBotHarnessSetting({
|
||||
bot_id,
|
||||
harness,
|
||||
inherit,
|
||||
model_id,
|
||||
provider_id = null,
|
||||
fallback,
|
||||
enabled,
|
||||
options,
|
||||
}: Params) {
|
||||
if (inherit) {
|
||||
await BunSQLite.sql({
|
||||
sql: `DELETE FROM bot_harness_settings WHERE bot_id = ? AND harness = ?`,
|
||||
values: [bot_id, harness],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (harness == "text" && !enabled) {
|
||||
throw new AppError(`The text model can't be turned off.`);
|
||||
}
|
||||
|
||||
if (provider_id) {
|
||||
await assertHarnessProvider({ provider_id });
|
||||
}
|
||||
|
||||
if (model_id) {
|
||||
await assertHarnessModel({ harness, model_id, provider_id });
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const res = await BunSQLite.sql({
|
||||
sql: `INSERT INTO bot_harness_settings (created_at, updated_at, bot_id, harness, model_id, provider_id, fallback, enabled, options)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(bot_id, harness) DO UPDATE SET updated_at = excluded.updated_at,
|
||||
model_id = excluded.model_id, provider_id = excluded.provider_id, fallback = excluded.fallback,
|
||||
enabled = excluded.enabled, options = excluded.options`,
|
||||
values: [
|
||||
now,
|
||||
now,
|
||||
bot_id,
|
||||
harness,
|
||||
model_id,
|
||||
provider_id,
|
||||
fallback ? 1 : 0,
|
||||
enabled ? 1 : 0,
|
||||
JSON.stringify(options || {}),
|
||||
],
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(`Couldn't save the bot's model setting: ${res.error}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type {
|
||||
BUN_SQLITE_COBBLE_HARNESS_PROVIDERS,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
import type { HarnessProviderKind } from "@/src/dict/harness-provider-dict";
|
||||
import grabProviderPreset from "@/src/harness/providers/grab-provider-preset";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import { encrypt } from "@/src/utils/crypt";
|
||||
import grabHarnessProviders from "./grab-harness-providers";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Updates this provider; adds one when not given.
|
||||
*/
|
||||
provider_id?: number;
|
||||
kind: HarnessProviderKind;
|
||||
name?: string;
|
||||
base_url?: string;
|
||||
/**
|
||||
* A new key. Left out keeps the saved one.
|
||||
*/
|
||||
api_key?: string;
|
||||
clear_key?: boolean;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds or updates a model provider. The key is encrypted before it's
|
||||
* stored and never read back to the client. Returns the provider row.
|
||||
*/
|
||||
export default async function saveHarnessProvider({
|
||||
provider_id,
|
||||
kind,
|
||||
name,
|
||||
base_url,
|
||||
api_key,
|
||||
clear_key,
|
||||
enabled,
|
||||
}: Params) {
|
||||
const preset = grabProviderPreset({ kind });
|
||||
const final_base_url = (base_url || preset.base_url).trim().replace(/\/+$/, "");
|
||||
|
||||
if (!/^https?:\/\/[^\s/]+/i.test(final_base_url)) {
|
||||
throw new AppError(`Enter the provider's API address, starting with http:// or https://.`);
|
||||
}
|
||||
|
||||
const existing = provider_id
|
||||
? (await grabHarnessProviders({ provider_id }))[0]
|
||||
: undefined;
|
||||
|
||||
if (provider_id && !existing) {
|
||||
throw new AppError(`Provider not found.`);
|
||||
}
|
||||
|
||||
const key_update = api_key?.trim()
|
||||
? { api_key: await encrypt(api_key.trim()) }
|
||||
: clear_key
|
||||
? { api_key: "" }
|
||||
: {};
|
||||
|
||||
const data: BUN_SQLITE_COBBLE_HARNESS_PROVIDERS = {
|
||||
kind: preset.value,
|
||||
name: name?.trim() || preset.title,
|
||||
base_url: final_base_url,
|
||||
enabled: enabled ? 1 : 0,
|
||||
...key_update,
|
||||
};
|
||||
|
||||
if (existing?.id) {
|
||||
const res = await BunSQLite.update<
|
||||
BUN_SQLITE_COBBLE_HARNESS_PROVIDERS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "harness_providers",
|
||||
data,
|
||||
targetId: existing.id,
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(`Couldn't save the provider: ${res.msg}`);
|
||||
}
|
||||
|
||||
return (await grabHarnessProviders({ provider_id: existing.id }))[0]!;
|
||||
}
|
||||
|
||||
const res = await BunSQLite.insert<
|
||||
BUN_SQLITE_COBBLE_HARNESS_PROVIDERS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "harness_providers",
|
||||
data: [data],
|
||||
});
|
||||
|
||||
const new_id = res.singleRes?.id || res.postInsertReturn?.insertId;
|
||||
|
||||
if (!res.success || !new_id) {
|
||||
throw new Error(`Couldn't add the provider: ${res.msg}`);
|
||||
}
|
||||
|
||||
return (await grabHarnessProviders({ provider_id: new_id }))[0]!;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { HarnessKind } from "@/src/dict/harness-dict";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import assertHarnessModel from "./assert-harness-model";
|
||||
import assertHarnessProvider from "./assert-harness-provider";
|
||||
|
||||
type Params = {
|
||||
harness: HarnessKind;
|
||||
/**
|
||||
* A `harness_models` row, or null to pick automatically.
|
||||
*/
|
||||
model_id: number | null;
|
||||
/**
|
||||
* Limits the harness to one provider; null: any.
|
||||
*/
|
||||
provider_id?: number | null;
|
||||
fallback: boolean;
|
||||
enabled: boolean;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves a harness's model setting. The picked model must be available and
|
||||
* able to do the harness's job. Text can't be turned off.
|
||||
*/
|
||||
export default async function saveHarnessSetting({
|
||||
harness,
|
||||
model_id,
|
||||
provider_id = null,
|
||||
fallback,
|
||||
enabled,
|
||||
options,
|
||||
}: Params) {
|
||||
if (harness == "text" && !enabled) {
|
||||
throw new AppError(`The text model can't be turned off.`);
|
||||
}
|
||||
|
||||
if (provider_id) {
|
||||
await assertHarnessProvider({ provider_id });
|
||||
}
|
||||
|
||||
if (model_id) {
|
||||
await assertHarnessModel({ harness, model_id, provider_id });
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const res = await BunSQLite.sql({
|
||||
sql: `INSERT INTO harness_settings (created_at, updated_at, harness, model_id, provider_id, fallback, enabled, options)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(harness) DO UPDATE SET updated_at = excluded.updated_at,
|
||||
model_id = excluded.model_id, provider_id = excluded.provider_id, fallback = excluded.fallback,
|
||||
enabled = excluded.enabled, options = excluded.options`,
|
||||
values: [
|
||||
now,
|
||||
now,
|
||||
harness,
|
||||
model_id,
|
||||
provider_id,
|
||||
fallback ? 1 : 0,
|
||||
enabled ? 1 : 0,
|
||||
JSON.stringify(options || {}),
|
||||
],
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(`Couldn't save the model setting: ${res.error}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import ensureDefaultHarnessProvider from "./ensure-default-harness-provider";
|
||||
import grabHarnessProviders from "./grab-harness-providers";
|
||||
import syncHarnessProviderModels from "./sync-harness-provider-models";
|
||||
|
||||
/**
|
||||
* Refreshes every enabled provider's model list, so automatic picks follow
|
||||
* free models as they come and go. One provider failing doesn't stop the
|
||||
* rest; its error is saved on its row.
|
||||
*/
|
||||
export default async function syncAllHarnessModels() {
|
||||
await ensureDefaultHarnessProvider();
|
||||
|
||||
const providers = await grabHarnessProviders({ enabled_only: true });
|
||||
const results: { provider_id?: number; count?: number; error?: string }[] =
|
||||
[];
|
||||
|
||||
for (const provider of providers) {
|
||||
try {
|
||||
const count = await syncHarnessProviderModels({ provider });
|
||||
results.push({ provider_id: provider.id || undefined, count });
|
||||
} catch (error: unknown) {
|
||||
results.push({
|
||||
provider_id: provider.id || undefined,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_HARNESS_PROVIDERS } from "@/db/types/db";
|
||||
import grabHarnessAdapter from "@/src/harness/providers/grab-harness-adapter";
|
||||
import resolveProviderConfig from "./resolve-provider-config";
|
||||
import upsertHarnessModels from "./upsert-harness-models";
|
||||
|
||||
type Params = {
|
||||
provider: BUN_SQLITE_COBBLE_HARNESS_PROVIDERS;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches one provider's model list into `harness_models`, and records
|
||||
* when it ran and why it failed, if it did. Returns how many models the
|
||||
* provider listed. Throws when the list couldn't be fetched.
|
||||
*/
|
||||
export default async function syncHarnessProviderModels({ provider }: Params) {
|
||||
if (!provider.id) {
|
||||
throw new Error(`Provider has no id`);
|
||||
}
|
||||
|
||||
const { config } = await resolveProviderConfig({ provider });
|
||||
|
||||
try {
|
||||
const models = await grabHarnessAdapter({
|
||||
kind: config.kind,
|
||||
}).listModels({ provider: config });
|
||||
|
||||
const count = await upsertHarnessModels({
|
||||
provider_id: provider.id,
|
||||
models,
|
||||
});
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE harness_providers SET last_synced_at = ?, sync_error = NULL, updated_at = ? WHERE id = ?`,
|
||||
values: [Date.now(), Date.now(), provider.id],
|
||||
});
|
||||
|
||||
return count;
|
||||
} catch (error: unknown) {
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE harness_providers SET sync_error = ?, updated_at = ? WHERE id = ?`,
|
||||
values: [
|
||||
(error instanceof Error ? error.message : String(error)).slice(
|
||||
0,
|
||||
500,
|
||||
),
|
||||
Date.now(),
|
||||
provider.id,
|
||||
],
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { BUN_SQLITE_COBBLE_HARNESS_PROVIDERS } from "@/db/types/db";
|
||||
import grabProviderPreset from "@/src/harness/providers/grab-provider-preset";
|
||||
import type { PublicHarnessProvider } from "@/src/types";
|
||||
|
||||
type Params = {
|
||||
provider: BUN_SQLITE_COBBLE_HARNESS_PROVIDERS;
|
||||
};
|
||||
|
||||
/**
|
||||
* A provider row safe for the client: the key is dropped, only whether
|
||||
* one is saved (or set in the environment) is kept.
|
||||
*/
|
||||
export default function toPublicHarnessProvider({
|
||||
provider,
|
||||
}: Params): PublicHarnessProvider {
|
||||
const preset = grabProviderPreset({ kind: provider.kind });
|
||||
|
||||
return {
|
||||
id: provider.id || 0,
|
||||
kind: preset.value,
|
||||
name: provider.name || preset.title,
|
||||
base_url: provider.base_url || preset.base_url,
|
||||
enabled: provider.enabled != 0,
|
||||
has_key: Boolean(provider.api_key),
|
||||
has_env_key: Boolean(preset.key_env && process.env[preset.key_env]),
|
||||
needs_key: preset.needs_key,
|
||||
key_env: preset.key_env || undefined,
|
||||
last_synced_at: provider.last_synced_at || undefined,
|
||||
sync_error: provider.sync_error || undefined,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { HarnessCatalogModel } from "@/src/harness/types";
|
||||
|
||||
type Params = {
|
||||
provider_id: number;
|
||||
models: HarnessCatalogModel[];
|
||||
now?: number;
|
||||
};
|
||||
|
||||
const Columns = [
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"provider_id",
|
||||
"slug",
|
||||
"name",
|
||||
"free",
|
||||
"available",
|
||||
"input_modalities",
|
||||
"output_modalities",
|
||||
"supports_tools",
|
||||
"context_length",
|
||||
"max_output_tokens",
|
||||
"voices",
|
||||
"intelligence_score",
|
||||
"agentic_score",
|
||||
"released_at",
|
||||
"expires_at",
|
||||
"last_seen_at",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Rows per statement, well under SQLite's bound-variable limit.
|
||||
*/
|
||||
const ChunkSize = 50;
|
||||
|
||||
/**
|
||||
* Writes a provider's model list: new models are added, listed ones are
|
||||
* refreshed and marked available, and models it no longer lists are kept
|
||||
* but marked unavailable (a super admin's pick may point at them).
|
||||
*/
|
||||
export default async function upsertHarnessModels({
|
||||
provider_id,
|
||||
models,
|
||||
now = Date.now(),
|
||||
}: Params) {
|
||||
const unique = [...new Map(models.map((m) => [m.slug, m])).values()];
|
||||
|
||||
for (let i = 0; i < unique.length; i += ChunkSize) {
|
||||
const chunk = unique.slice(i, i + ChunkSize);
|
||||
|
||||
const res = await BunSQLite.sql({
|
||||
sql: `INSERT INTO harness_models (${Columns.join(", ")})
|
||||
VALUES ${chunk.map(() => `(${Columns.map(() => "?").join(", ")})`).join(", ")}
|
||||
ON CONFLICT(provider_id, slug) DO UPDATE SET
|
||||
${Columns.filter((c) => c != "created_at" && c != "provider_id" && c != "slug")
|
||||
.map((c) => `${c} = excluded.${c}`)
|
||||
.join(", ")}`,
|
||||
values: chunk.flatMap((model) => [
|
||||
now,
|
||||
now,
|
||||
provider_id,
|
||||
model.slug,
|
||||
model.name || model.slug,
|
||||
model.free ? 1 : 0,
|
||||
1,
|
||||
JSON.stringify(model.input_modalities),
|
||||
JSON.stringify(model.output_modalities),
|
||||
model.supports_tools ? 1 : 0,
|
||||
model.context_length ?? null,
|
||||
model.max_output_tokens ?? null,
|
||||
model.voices ? JSON.stringify(model.voices) : null,
|
||||
model.intelligence_score ?? null,
|
||||
model.agentic_score ?? null,
|
||||
model.released_at ?? null,
|
||||
model.expires_at ?? null,
|
||||
now,
|
||||
]),
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(`Couldn't save the model list: ${res.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE harness_models SET available = 0, updated_at = ?
|
||||
WHERE provider_id = ? AND available = 1 AND (last_seen_at IS NULL OR last_seen_at < ?)`,
|
||||
values: [now, provider_id, now],
|
||||
});
|
||||
|
||||
return unique.length;
|
||||
}
|
||||
Reference in New Issue
Block a user