wizards 0.2.7 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/runtime/dist/env.js +4 -0
- package/apps/runtime/dist/media/generate.js +2 -2
- package/apps/runtime/dist/media/transcribe.js +18 -6
- package/apps/runtime/dist/models.js +566 -218
- package/apps/runtime/dist/routes/studio.js +152 -10
- package/apps/web/dist/assets/Account-YJEQEo5k.js +1 -0
- package/apps/web/dist/assets/{ChangelogDialog-DEfZRDK3.js → ChangelogDialog-BNQ9yup8.js} +1 -1
- package/apps/web/dist/assets/{ChatPanel-CNiFyu_w.js → ChatPanel-CtE1wcUD.js} +1 -1
- package/apps/web/dist/assets/{CreditsDialog-CUvc2iPM.js → CreditsDialog-DSjsgLoo.js} +1 -1
- package/apps/web/dist/assets/EditorPage-CAxamSak.js +2 -0
- package/apps/web/dist/assets/{FlowPreview-BC25OUH0.js → FlowPreview-B3IxmikV.js} +1 -1
- package/apps/web/dist/assets/Harness-Bo-kwm1H.js +16 -0
- package/apps/web/dist/assets/HomePage-BdIa2zcg.js +1 -0
- package/apps/web/dist/assets/ImportWizard-DMea5ZH1.js +1 -0
- package/apps/web/dist/assets/LocalRuntime-DFswQjxc.js +1 -0
- package/apps/web/dist/assets/NewWizardPage-Cnb6UYme.js +2 -0
- package/apps/web/dist/assets/ProjectPage-CR_FIUya.js +2 -0
- package/apps/web/dist/assets/{PublicRunner-BCrmXPkr.js → PublicRunner-CIHEMLlN.js} +1 -1
- package/apps/web/dist/assets/{ReadOnly-CvmN9XW4.js → ReadOnly-B0-ReU_0.js} +1 -1
- package/apps/web/dist/assets/RunnerView-COKGppwq.js +2 -0
- package/apps/web/dist/assets/SettingsPage-BqAogRSI.js +1 -0
- package/apps/web/dist/assets/SetupPage-Bk4qXYZV.js +1 -0
- package/apps/web/dist/assets/{SharePage-BqwqI3Ga.js → SharePage-ChEtCkhn.js} +1 -1
- package/apps/web/dist/assets/{api-Bf-l9Sd8.js → api-u3nydnnp.js} +1 -1
- package/apps/web/dist/assets/changelog-C03GWYid.js +1 -0
- package/apps/web/dist/assets/circle-alert-B76z9TWZ.js +1 -0
- package/apps/web/dist/assets/definition-Co2Mg1qo.js +55 -0
- package/apps/web/dist/assets/esm-Rtjq9ReB.js +7 -0
- package/apps/web/dist/assets/image-BShbErBm.js +1 -0
- package/apps/web/dist/assets/index-BAQ2e4is.js +10 -0
- package/apps/web/dist/assets/index-CQQD8QNR.css +1 -0
- package/apps/web/dist/assets/meta-MVWkLr9H.js +1 -0
- package/apps/web/dist/assets/oss-credits-CIxnNZor.js +1 -0
- package/apps/web/dist/assets/{outputs-CsESg-Eg.js → outputs-s8BpXH6V.js} +1 -1
- package/apps/web/dist/assets/{video-CjgmvRDk.js → pen-line-NK46C_nG.js} +1 -1
- package/apps/web/dist/assets/refresh-cw-Cmk7VCe0.js +1 -0
- package/apps/web/dist/assets/ui-gGAdXx-P.js +1 -0
- package/apps/web/dist/assets/video-DBmwF1Jm.js +1 -0
- package/apps/web/dist/assets/volume-2-Bp2tyaKE.js +1 -0
- package/apps/web/dist/index.html +4 -7
- package/apps/web/dist/mcp/flow.html +2 -2
- package/node_modules/@engenty-wizards/shared/dist/providers.d.ts +75 -0
- package/node_modules/@engenty-wizards/shared/dist/providers.js +339 -0
- package/package.json +5 -1
- package/apps/web/dist/assets/Account-DLLeEwJL.js +0 -1
- package/apps/web/dist/assets/EditorPage-CZuCz7lB.js +0 -2
- package/apps/web/dist/assets/HomePage-DQKuhMFZ.js +0 -1
- package/apps/web/dist/assets/ImportWizard-BockXJXm.js +0 -1
- package/apps/web/dist/assets/LocalRuntime-C199bg_B.js +0 -16
- package/apps/web/dist/assets/NewWizardPage-C4cde8FO.js +0 -2
- package/apps/web/dist/assets/ProjectPage-DJC1K4w_.js +0 -2
- package/apps/web/dist/assets/RunnerView-CCe8-k9Q.js +0 -2
- package/apps/web/dist/assets/SettingsPage-BuocMkLt.js +0 -1
- package/apps/web/dist/assets/SetupPage-o6Jim2Pu.js +0 -1
- package/apps/web/dist/assets/changelog-CFeEzKqb.js +0 -1
- package/apps/web/dist/assets/cloud-off-D--dZLDw.js +0 -1
- package/apps/web/dist/assets/definition-DndaH5Ow.js +0 -55
- package/apps/web/dist/assets/esm-rJY59Ixx.js +0 -7
- package/apps/web/dist/assets/globe-ffVj8XwO.js +0 -1
- package/apps/web/dist/assets/index-B8n2AYO0.css +0 -1
- package/apps/web/dist/assets/index-BR5ELbeZ.js +0 -10
- package/apps/web/dist/assets/meta-CKE9QceT.js +0 -1
- package/apps/web/dist/assets/oss-credits-C-U2Tlpn.js +0 -1
- package/apps/web/dist/assets/react-dom-DenslgJm.js +0 -1
- package/apps/web/dist/assets/session-C7LDDcj7.js +0 -1
- package/apps/web/dist/assets/ui-BT0Y_Hci.js +0 -1
- /package/apps/web/dist/assets/{LocalRuntime-BrP-ENHg.css → Harness-BrP-ENHg.css} +0 -0
|
@@ -1,34 +1,50 @@
|
|
|
1
1
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
2
|
+
import { createElevenLabs } from "@ai-sdk/elevenlabs";
|
|
3
|
+
import { createFal } from "@ai-sdk/fal";
|
|
2
4
|
import { createGateway } from "@ai-sdk/gateway";
|
|
5
|
+
import { createGoogle } from "@ai-sdk/google";
|
|
3
6
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
7
|
+
import { createReplicate } from "@ai-sdk/replicate";
|
|
4
8
|
import { MODEL_CLASSES, TEXT_CLASSES, } from "@engenty-wizards/shared/definition";
|
|
5
|
-
import {
|
|
9
|
+
import { creditsModelOf, isCreditsRef, PROVIDER_IDS, providerInfo, providerOfRef, } from "@engenty-wizards/shared/providers";
|
|
10
|
+
import { accountToken, linkedAccount } from "./auth/account.js";
|
|
6
11
|
import { env } from "./env.js";
|
|
7
12
|
import { detectHarness, HarnessModel, harness, isHarnessVendor, notInstalled, signedOut, } from "./harness/index.js";
|
|
8
13
|
import { managed } from "./manage.js";
|
|
9
14
|
import { ModelUnavailableError } from "./model-errors.js";
|
|
15
|
+
import { seal, unseal } from "./secrets/crypto.js";
|
|
10
16
|
import { vaultDelete, vaultGet, vaultSet } from "./secrets/vault.js";
|
|
11
17
|
import { readSetting, writeSetting } from "./settings.js";
|
|
12
|
-
import { currentTenantOrNull } from "./tenants/tenant.js";
|
|
18
|
+
import { currentTenant, currentTenantOrNull } from "./tenants/tenant.js";
|
|
13
19
|
export { isHarnessVendor } from "./harness/index.js";
|
|
14
20
|
/**
|
|
15
|
-
* A step names a model class, never a model. Where the class runs is decided here:
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* A
|
|
23
|
-
* `openai:gpt-5.4-mini`, `ollama:qwen3`, `claude/sonnet
|
|
21
|
+
* A step names a model class, never a model. Where the class runs is decided here, in this order:
|
|
22
|
+
* 1. its own way: an AI client installed on the machine (Claude Code, Codex, Gemini CLI, Cursor
|
|
23
|
+
* Agent — on its subscription), an API key the person brought (OpenAI, Anthropic, Google,
|
|
24
|
+
* fal.ai, ElevenLabs, Replicate, AI Gateway) or a local model;
|
|
25
|
+
* 2. credits: the model-gateway binds `wizards/<class>`, meters the call and books it — on the
|
|
26
|
+
* tenant of a Manage-App's runtime, or on the account a runtime that runs alone is linked to.
|
|
27
|
+
* A runtime of a Manage-App keeps a tenant's own keys (sealed) and runs on its credits where it
|
|
28
|
+
* has none. A binding is `[provider:]vendor/model`, e.g. `anthropic/claude-haiku-4.5` (AI
|
|
29
|
+
* Gateway), `openai:gpt-5.4-mini`, `fal:fal-ai/flux/schnell`, `ollama:qwen3`, `claude/sonnet`
|
|
30
|
+
* (an installed client), or `credits`.
|
|
24
31
|
*/
|
|
25
32
|
export { ModelUnavailableError } from "./model-errors.js";
|
|
26
|
-
// ---
|
|
27
|
-
export const LOCAL_KEYS =
|
|
28
|
-
const local = {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
// --- configuration -----------------------------------------------------------------
|
|
34
|
+
export const LOCAL_KEYS = PROVIDER_IDS;
|
|
35
|
+
const local = { settings: { source: "own", bindings: {} }, keys: {} };
|
|
36
|
+
function envKey(name) {
|
|
37
|
+
const value = {
|
|
38
|
+
gateway: env.aiGatewayKey,
|
|
39
|
+
openai: env.openaiKey,
|
|
40
|
+
anthropic: env.anthropicKey,
|
|
41
|
+
google: env.googleKey,
|
|
42
|
+
fal: env.falKey,
|
|
43
|
+
elevenlabs: env.elevenlabsKey,
|
|
44
|
+
replicate: env.replicateKey,
|
|
45
|
+
}[name];
|
|
46
|
+
return value || undefined;
|
|
47
|
+
}
|
|
32
48
|
/** Reads the model settings and keys of a runtime that runs alone; called at start and after a change. */
|
|
33
49
|
export async function loadLocalModels() {
|
|
34
50
|
if (managed) {
|
|
@@ -38,23 +54,41 @@ export async function loadLocalModels() {
|
|
|
38
54
|
source: "own",
|
|
39
55
|
bindings: {},
|
|
40
56
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
const keys = {};
|
|
58
|
+
for (const name of LOCAL_KEYS) {
|
|
59
|
+
keys[name] = (await vaultGet(`key.${name}`)) ?? envKey(name);
|
|
60
|
+
}
|
|
61
|
+
local.keys = keys;
|
|
46
62
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
const tenantConfigs = new Map();
|
|
64
|
+
const tenantSetting = (tenant) => `models:${tenant}`;
|
|
65
|
+
/** A team thinks on its credits unless it brought a key: `own`, with the credits behind it. */
|
|
66
|
+
function teamSettings(bindings) {
|
|
67
|
+
return { source: "own", bindings, creditFallback: true };
|
|
68
|
+
}
|
|
69
|
+
async function tenantConfig(tenant) {
|
|
70
|
+
const hit = tenantConfigs.get(tenant);
|
|
71
|
+
if (hit) {
|
|
72
|
+
return hit;
|
|
73
|
+
}
|
|
74
|
+
const stored = await readSetting(tenantSetting(tenant));
|
|
75
|
+
const value = {
|
|
76
|
+
settings: teamSettings(stored?.bindings ?? {}),
|
|
77
|
+
keys: (stored?.keys ? unseal(stored.keys) : null) ?? {},
|
|
53
78
|
};
|
|
79
|
+
tenantConfigs.set(tenant, value);
|
|
80
|
+
return value;
|
|
54
81
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
82
|
+
async function config() {
|
|
83
|
+
if (!managed) {
|
|
84
|
+
return local;
|
|
85
|
+
}
|
|
86
|
+
const tenant = currentTenantOrNull();
|
|
87
|
+
return tenant ? tenantConfig(tenant) : { settings: teamSettings({}), keys: {} };
|
|
88
|
+
}
|
|
89
|
+
function mergeBindings(current, changes = {}) {
|
|
90
|
+
const bindings = { ...current };
|
|
91
|
+
for (const [cls, ref] of Object.entries(changes)) {
|
|
58
92
|
if (ref?.trim()) {
|
|
59
93
|
bindings[cls] = ref.trim();
|
|
60
94
|
}
|
|
@@ -62,10 +96,23 @@ export async function saveLocalModels(input) {
|
|
|
62
96
|
delete bindings[cls];
|
|
63
97
|
}
|
|
64
98
|
}
|
|
99
|
+
return bindings;
|
|
100
|
+
}
|
|
101
|
+
export function localModelSettings() {
|
|
102
|
+
return {
|
|
103
|
+
...local.settings,
|
|
104
|
+
creditFallback: local.settings.creditFallback !== false,
|
|
105
|
+
bindings: Object.fromEntries(MODEL_CLASSES.map((c) => [c, ownRef(local, c) || local.settings.bindings[c] || ""])),
|
|
106
|
+
ollamaUrl: local.settings.ollamaUrl ?? env.ollamaUrl,
|
|
107
|
+
keys: Object.fromEntries(LOCAL_KEYS.map((k) => [k, Boolean(local.keys[k])])),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export async function saveLocalModels(input) {
|
|
65
111
|
await writeSetting("models", {
|
|
66
112
|
source: input.source ?? local.settings.source,
|
|
67
|
-
bindings,
|
|
113
|
+
bindings: mergeBindings(local.settings.bindings, input.bindings),
|
|
68
114
|
ollamaUrl: input.ollamaUrl?.trim() || local.settings.ollamaUrl,
|
|
115
|
+
creditFallback: input.creditFallback ?? local.settings.creditFallback,
|
|
69
116
|
});
|
|
70
117
|
for (const [name, value] of Object.entries(input.keys ?? {})) {
|
|
71
118
|
if (value?.trim()) {
|
|
@@ -77,7 +124,128 @@ export async function saveLocalModels(input) {
|
|
|
77
124
|
}
|
|
78
125
|
await loadLocalModels();
|
|
79
126
|
}
|
|
80
|
-
|
|
127
|
+
/** A team's keys and bindings on a runtime of a Manage-App. No client and no local model there. */
|
|
128
|
+
async function saveTenantModels(tenant, input) {
|
|
129
|
+
const current = await tenantConfig(tenant);
|
|
130
|
+
const keys = { ...current.keys };
|
|
131
|
+
for (const [name, value] of Object.entries(input.keys ?? {})) {
|
|
132
|
+
if (value?.trim()) {
|
|
133
|
+
keys[name] = value.trim();
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
delete keys[name];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
await writeSetting(tenantSetting(tenant), {
|
|
140
|
+
bindings: mergeBindings(current.settings.bindings, input.bindings),
|
|
141
|
+
keys: Object.keys(keys).length ? seal(keys) : null,
|
|
142
|
+
});
|
|
143
|
+
tenantConfigs.delete(tenant);
|
|
144
|
+
}
|
|
145
|
+
/** Saves for this machine, or for the current tenant of a Manage-App's runtime. */
|
|
146
|
+
export async function saveModels(input) {
|
|
147
|
+
if (managed) {
|
|
148
|
+
await saveTenantModels(currentTenant(), input);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
await saveLocalModels(input);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** A stored key, for a check of the key itself; never sent to the studio. */
|
|
155
|
+
export async function storedKey(name) {
|
|
156
|
+
return (await config()).keys[name];
|
|
157
|
+
}
|
|
158
|
+
// --- where a class runs -------------------------------------------------------------
|
|
159
|
+
const isText = (cls) => TEXT_CLASSES.includes(cls);
|
|
160
|
+
/**
|
|
161
|
+
* `fal:fal-ai/flux/schnell` → provider fal, id `fal-ai/flux/schnell`. Without a provider the
|
|
162
|
+
* binding is an AI Gateway id (`google/veo-…`) or an installed client (`codex/default`).
|
|
163
|
+
*/
|
|
164
|
+
function parse(ref) {
|
|
165
|
+
const match = /^([a-z]+):(.*)$/s.exec(ref);
|
|
166
|
+
if (match) {
|
|
167
|
+
const [, provider, id] = match;
|
|
168
|
+
// Ollama and the gateway name a model as the gateway does; the others are their own vendor.
|
|
169
|
+
const vendor = provider === "ollama" || provider === "gateway" ? id.split("/")[0] : provider;
|
|
170
|
+
return { provider, id, vendor };
|
|
171
|
+
}
|
|
172
|
+
return { provider: null, id: ref, vendor: ref.split("/")[0] };
|
|
173
|
+
}
|
|
174
|
+
const vendorOf = (ref) => parse(ref).vendor;
|
|
175
|
+
/** The part of `codex/default` after the client. */
|
|
176
|
+
const aliasOf = (id) => id.split("/").slice(1).join("/");
|
|
177
|
+
/** Whether this configuration can reach a binding without credits. */
|
|
178
|
+
function reachable(cfg, ref) {
|
|
179
|
+
if (!ref || isCreditsRef(ref)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const { provider, vendor } = parse(ref);
|
|
183
|
+
if ((provider === null && isHarnessVendor(vendor)) || provider === "ollama") {
|
|
184
|
+
// An installed client and a local model are of this machine.
|
|
185
|
+
return !managed;
|
|
186
|
+
}
|
|
187
|
+
if (provider === null || provider === "gateway") {
|
|
188
|
+
return (Boolean(cfg.keys.gateway) ||
|
|
189
|
+
(provider === null &&
|
|
190
|
+
(vendor === "openai" || vendor === "anthropic") &&
|
|
191
|
+
Boolean(cfg.keys[vendor])));
|
|
192
|
+
}
|
|
193
|
+
return Boolean(cfg.keys[provider]);
|
|
194
|
+
}
|
|
195
|
+
/** What a class runs on without credits: a client, an own key or a local model. "" = no way of its own. */
|
|
196
|
+
function ownRef(cfg, cls) {
|
|
197
|
+
const bound = cfg.settings.bindings[cls];
|
|
198
|
+
const client = managed ? null : harness(cfg.settings.source);
|
|
199
|
+
if (isText(cls) && client) {
|
|
200
|
+
// Text thinks on the installed client; a binding counts only where it names that client.
|
|
201
|
+
return bound?.startsWith(`${client.id}/`)
|
|
202
|
+
? bound
|
|
203
|
+
: `${client.id}/${client.classes[cls]}`;
|
|
204
|
+
}
|
|
205
|
+
// Text on the account's credits: every text class goes there.
|
|
206
|
+
if (isText(cls) && cfg.settings.source === "account") {
|
|
207
|
+
return "";
|
|
208
|
+
}
|
|
209
|
+
if (isCreditsRef(bound)) {
|
|
210
|
+
return "";
|
|
211
|
+
}
|
|
212
|
+
if (bound) {
|
|
213
|
+
return reachable(cfg, bound) ? bound : "";
|
|
214
|
+
}
|
|
215
|
+
// A client that makes images on the sign-in makes them too, before any key is paid for.
|
|
216
|
+
if (cls === "image" && client?.image) {
|
|
217
|
+
return `${client.id}/${client.image.alias}`;
|
|
218
|
+
}
|
|
219
|
+
// The defaults name gateway models; without a gateway key nothing reaches them.
|
|
220
|
+
return reachable(cfg, env.models[cls]) ? env.models[cls] : "";
|
|
221
|
+
}
|
|
222
|
+
const NOTHING = {
|
|
223
|
+
classifier: "Für Text ist kein Modell eingerichtet.",
|
|
224
|
+
standard: "Für Text ist kein Modell eingerichtet.",
|
|
225
|
+
high: "Für Text ist kein Modell eingerichtet.",
|
|
226
|
+
highest: "Für Text ist kein Modell eingerichtet.",
|
|
227
|
+
image: "Für Bilder ist kein Modell eingerichtet.",
|
|
228
|
+
video: "Für Videos ist kein Modell eingerichtet.",
|
|
229
|
+
speech: "Für Sprachausgabe ist kein Modell eingerichtet.",
|
|
230
|
+
audio: "Für Sprachnotizen ist kein Modell eingerichtet.",
|
|
231
|
+
};
|
|
232
|
+
/** Why a class has no way: the provider picked for it lacks its key, or nothing is set up. */
|
|
233
|
+
function missingText(cfg, cls) {
|
|
234
|
+
const bound = cfg.settings.bindings[cls];
|
|
235
|
+
if (isCreditsRef(bound)) {
|
|
236
|
+
return "Dafür ist das Guthaben gewählt, aber kein Konto angemeldet.";
|
|
237
|
+
}
|
|
238
|
+
const provider = bound ? providerInfo(providerOfRef(bound) ?? "") : null;
|
|
239
|
+
if (provider) {
|
|
240
|
+
return `${NOTHING[cls].replace(/ ist kein Modell eingerichtet\.$/, "")} ist ${provider.name} gewählt, aber kein API Key hinterlegt.`;
|
|
241
|
+
}
|
|
242
|
+
return NOTHING[cls];
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The way to the credits. On a Manage-App's runtime always; alone, through the linked account —
|
|
246
|
+
* where text runs on it (`chosen`), or as the fallback for what has no way of its own.
|
|
247
|
+
*/
|
|
248
|
+
async function creditAccess(cfg, chosen) {
|
|
81
249
|
if (managed) {
|
|
82
250
|
return {
|
|
83
251
|
baseUrl: env.manage.gatewayUrl,
|
|
@@ -85,15 +253,43 @@ async function gatewayAccess() {
|
|
|
85
253
|
tenant: currentTenantOrNull(),
|
|
86
254
|
};
|
|
87
255
|
}
|
|
88
|
-
if (
|
|
256
|
+
if (!chosen && (cfg.settings.creditFallback === false || !(await linkedAccount()))) {
|
|
89
257
|
return null;
|
|
90
258
|
}
|
|
91
259
|
const token = await accountToken();
|
|
92
260
|
if (!token) {
|
|
93
|
-
|
|
261
|
+
if (chosen) {
|
|
262
|
+
throw new ModelUnavailableError("Das Konto ist nicht mehr angemeldet. Bitte in den Einstellungen neu anmelden.");
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
94
265
|
}
|
|
95
266
|
return { baseUrl: env.local.gatewayUrl, token, tenant: null };
|
|
96
267
|
}
|
|
268
|
+
/** A class goes to the credits on purpose: text on the account, or a binding that says so. */
|
|
269
|
+
function creditsChosen(cfg, cls) {
|
|
270
|
+
return (isCreditsRef(cfg.settings.bindings[cls]) ||
|
|
271
|
+
(isText(cls) && cfg.settings.source === "account" && !managed));
|
|
272
|
+
}
|
|
273
|
+
async function routeOf(cls) {
|
|
274
|
+
const cfg = await config();
|
|
275
|
+
const ref = ownRef(cfg, cls);
|
|
276
|
+
if (ref) {
|
|
277
|
+
return { kind: "own", cfg, ref };
|
|
278
|
+
}
|
|
279
|
+
const access = await creditAccess(cfg, creditsChosen(cfg, cls));
|
|
280
|
+
if (access) {
|
|
281
|
+
return { kind: "credits", access, model: creditsModelOf(cfg.settings.bindings[cls]) };
|
|
282
|
+
}
|
|
283
|
+
throw new ModelUnavailableError(missingText(cfg, cls));
|
|
284
|
+
}
|
|
285
|
+
/** The key of a provider, or why the call cannot be made. */
|
|
286
|
+
function need(cfg, name) {
|
|
287
|
+
const key = cfg.keys[name];
|
|
288
|
+
if (!key) {
|
|
289
|
+
throw new ModelUnavailableError(`Für ${providerInfo(name)?.name ?? name} ist kein API Key hinterlegt.`);
|
|
290
|
+
}
|
|
291
|
+
return key;
|
|
292
|
+
}
|
|
97
293
|
function gatewayClient(access, meta) {
|
|
98
294
|
return createGateway({
|
|
99
295
|
baseURL: `${access.baseUrl}/v4/ai`,
|
|
@@ -109,7 +305,8 @@ function gatewayClient(access, meta) {
|
|
|
109
305
|
let gatewayCatalog = null;
|
|
110
306
|
/** What the gateway binds each class to, with prices in credits. Cached for five minutes. */
|
|
111
307
|
export async function classCatalog() {
|
|
112
|
-
const
|
|
308
|
+
const cfg = await config();
|
|
309
|
+
const access = await creditAccess(cfg, cfg.settings.source === "account").catch(() => null);
|
|
113
310
|
if (!access) {
|
|
114
311
|
return null;
|
|
115
312
|
}
|
|
@@ -137,99 +334,72 @@ export async function classCatalog() {
|
|
|
137
334
|
return gatewayCatalog?.value ?? null;
|
|
138
335
|
}
|
|
139
336
|
}
|
|
140
|
-
|
|
141
|
-
function
|
|
142
|
-
const
|
|
143
|
-
const [vendor, ...model] = (rest ?? ref).split("/");
|
|
144
|
-
return { provider: maybeProvider, vendor, model: model.join("/") };
|
|
145
|
-
}
|
|
146
|
-
const vendorOf = (ref) => split(ref).vendor;
|
|
147
|
-
function localRef(cls) {
|
|
148
|
-
const bound = local.settings.bindings[cls];
|
|
149
|
-
const client = harness(local.settings.source);
|
|
150
|
-
if (client && TEXT_CLASSES.includes(cls)) {
|
|
151
|
-
// Text thinks on the installed client; a binding counts only where it names that client.
|
|
152
|
-
return bound?.startsWith(`${client.id}/`)
|
|
153
|
-
? bound
|
|
154
|
-
: `${client.id}/${client.classes[cls]}`;
|
|
155
|
-
}
|
|
156
|
-
if (bound) {
|
|
157
|
-
return bound;
|
|
158
|
-
}
|
|
159
|
-
// A client that makes images on the sign-in makes them too, before any key is paid for.
|
|
160
|
-
if (cls === "image" && client?.image) {
|
|
161
|
-
return `${client.id}/${client.image.alias}`;
|
|
162
|
-
}
|
|
163
|
-
// The defaults for images, video and sound name gateway models; without a gateway key nothing
|
|
164
|
-
// reaches them, so the class stays empty instead of showing a model that cannot run.
|
|
165
|
-
if (!TEXT_CLASSES.includes(cls) && !local.keys.gateway) {
|
|
166
|
-
return "";
|
|
167
|
-
}
|
|
168
|
-
return env.models[cls];
|
|
169
|
-
}
|
|
170
|
-
function ownGateway() {
|
|
171
|
-
return local.keys.gateway ? createGateway({ apiKey: local.keys.gateway }) : null;
|
|
172
|
-
}
|
|
173
|
-
async function localLanguageModel(ref, meta) {
|
|
174
|
-
if (!ref) {
|
|
175
|
-
throw new ModelUnavailableError("Für diese Klasse ist kein Modell eingerichtet.");
|
|
176
|
-
}
|
|
177
|
-
const { provider, vendor, model } = split(ref);
|
|
178
|
-
const client = harness(vendor);
|
|
179
|
-
if (client) {
|
|
180
|
-
if (!(await detectHarness(client.id))?.version) {
|
|
181
|
-
throw new ModelUnavailableError(notInstalled(client));
|
|
182
|
-
}
|
|
183
|
-
return { model: client.model(model, meta.effort), gateway: false };
|
|
184
|
-
}
|
|
185
|
-
if (provider === "ollama") {
|
|
186
|
-
const ollama = createOpenAI({
|
|
187
|
-
baseURL: local.settings.ollamaUrl ?? env.ollamaUrl,
|
|
188
|
-
apiKey: "ollama",
|
|
189
|
-
});
|
|
190
|
-
return { model: ollama.chat(model ? `${vendor}/${model}` : vendor), gateway: false };
|
|
191
|
-
}
|
|
192
|
-
const gateway = ownGateway();
|
|
193
|
-
if ((provider === null || provider === "gateway") && gateway) {
|
|
194
|
-
return { model: gateway(`${vendor}/${model}`), gateway: true };
|
|
195
|
-
}
|
|
196
|
-
if ((provider === "openai" || vendor === "openai") && local.keys.openai) {
|
|
197
|
-
return {
|
|
198
|
-
model: createOpenAI({ apiKey: local.keys.openai })(model || vendor),
|
|
199
|
-
gateway: false,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
if ((provider === "anthropic" || vendor === "anthropic") && local.keys.anthropic) {
|
|
203
|
-
return {
|
|
204
|
-
model: createAnthropic({ apiKey: local.keys.anthropic })(model || vendor),
|
|
205
|
-
gateway: false,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
throw new ModelUnavailableError(`Für das Modell "${ref}" ist kein Zugang eingerichtet. Bitte in den Einstellungen einen Schlüssel hinterlegen oder ein Konto anmelden.`);
|
|
209
|
-
}
|
|
210
|
-
async function viaGateway(cls, meta, pick) {
|
|
211
|
-
const access = await gatewayAccess();
|
|
212
|
-
if (!access) {
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
const bound = (await classCatalog())?.classes[cls]?.model ?? "";
|
|
337
|
+
/** A call on the credits: the model a binding names, or the gateway's model of the class. */
|
|
338
|
+
async function viaCredits(route, cls, meta, pick) {
|
|
339
|
+
const bound = route.model ?? (await classCatalog())?.classes[cls]?.model ?? "";
|
|
216
340
|
return {
|
|
217
|
-
model: pick(gatewayClient(access, meta), `wizards/${cls}`),
|
|
341
|
+
model: pick(gatewayClient(route.access, meta), route.model ?? `wizards/${cls}`),
|
|
218
342
|
ref: bound,
|
|
219
343
|
vendor: vendorOf(bound.replace(/^[a-z]+:/, "")),
|
|
220
344
|
gateway: true,
|
|
221
345
|
metered: true,
|
|
222
346
|
};
|
|
223
347
|
}
|
|
348
|
+
/** The installed client a binding names, checked to be there. */
|
|
349
|
+
async function installedClient(ref) {
|
|
350
|
+
const { provider, vendor } = parse(ref);
|
|
351
|
+
const client = provider === null ? harness(vendor) : null;
|
|
352
|
+
if (client && !(await detectHarness(client.id))?.version) {
|
|
353
|
+
throw new ModelUnavailableError(notInstalled(client));
|
|
354
|
+
}
|
|
355
|
+
return client;
|
|
356
|
+
}
|
|
357
|
+
async function ownLanguageModel(cfg, ref, meta) {
|
|
358
|
+
const { provider, id, vendor } = parse(ref);
|
|
359
|
+
const client = await installedClient(ref);
|
|
360
|
+
if (client) {
|
|
361
|
+
return { model: client.model(aliasOf(id), meta.effort), gateway: false };
|
|
362
|
+
}
|
|
363
|
+
switch (provider) {
|
|
364
|
+
case "ollama":
|
|
365
|
+
return {
|
|
366
|
+
model: createOpenAI({
|
|
367
|
+
baseURL: cfg.settings.ollamaUrl ?? env.ollamaUrl,
|
|
368
|
+
apiKey: "ollama",
|
|
369
|
+
}).chat(id),
|
|
370
|
+
gateway: false,
|
|
371
|
+
};
|
|
372
|
+
case "openai":
|
|
373
|
+
return { model: createOpenAI({ apiKey: need(cfg, "openai") })(id), gateway: false };
|
|
374
|
+
case "anthropic":
|
|
375
|
+
return { model: createAnthropic({ apiKey: need(cfg, "anthropic") })(id), gateway: false };
|
|
376
|
+
case "google":
|
|
377
|
+
return { model: createGoogle({ apiKey: need(cfg, "google") })(id), gateway: false };
|
|
378
|
+
case null:
|
|
379
|
+
case "gateway":
|
|
380
|
+
if (cfg.keys.gateway) {
|
|
381
|
+
return { model: createGateway({ apiKey: cfg.keys.gateway })(id), gateway: true };
|
|
382
|
+
}
|
|
383
|
+
if (provider === null && vendor === "openai" && cfg.keys.openai) {
|
|
384
|
+
return { model: createOpenAI({ apiKey: cfg.keys.openai })(aliasOf(id)), gateway: false };
|
|
385
|
+
}
|
|
386
|
+
if (provider === null && vendor === "anthropic" && cfg.keys.anthropic) {
|
|
387
|
+
return {
|
|
388
|
+
model: createAnthropic({ apiKey: cfg.keys.anthropic })(aliasOf(id)),
|
|
389
|
+
gateway: false,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
throw new ModelUnavailableError(`Für das Modell "${ref}" ist kein Zugang eingerichtet. Bitte in den Einstellungen einen API Key hinterlegen oder ein Konto anmelden.`);
|
|
394
|
+
}
|
|
224
395
|
/** The language model a text class runs on. */
|
|
225
396
|
export async function textModel(cls, meta = {}) {
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
228
|
-
return
|
|
397
|
+
const route = await routeOf(cls);
|
|
398
|
+
if (route.kind === "credits") {
|
|
399
|
+
return viaCredits(route, cls, meta, (client, id) => client(id));
|
|
229
400
|
}
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
return { model, ref, vendor: vendorOf(ref), gateway, metered: false };
|
|
401
|
+
const { model, gateway } = await ownLanguageModel(route.cfg, route.ref, meta);
|
|
402
|
+
return { model, ref: route.ref, vendor: vendorOf(route.ref), gateway, metered: false };
|
|
233
403
|
}
|
|
234
404
|
/** Hands the tools an agent will call to a model that runs them itself (an installed AI client). */
|
|
235
405
|
export function attachTools(resolved, tools) {
|
|
@@ -237,98 +407,159 @@ export function attachTools(resolved, tools) {
|
|
|
237
407
|
resolved.model.attach(tools);
|
|
238
408
|
}
|
|
239
409
|
}
|
|
410
|
+
/** Gemini image models of the gateway are chat models answering with image files. */
|
|
411
|
+
export function isChatImageModel(ref) {
|
|
412
|
+
return ref.includes("gemini") && ref.includes("image");
|
|
413
|
+
}
|
|
414
|
+
/** `chat`: the model answers through chat with an image file (`textModel("image")`). */
|
|
240
415
|
export async function imageModel(meta = {}) {
|
|
241
|
-
const
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const {
|
|
247
|
-
const
|
|
416
|
+
const route = await routeOf("image");
|
|
417
|
+
if (route.kind === "credits") {
|
|
418
|
+
const resolved = await viaCredits(route, "image", meta, (c, id) => c.imageModel(id));
|
|
419
|
+
return { ...resolved, chat: isChatImageModel(resolved.ref) };
|
|
420
|
+
}
|
|
421
|
+
const { cfg, ref } = route;
|
|
422
|
+
const { provider, id, vendor } = parse(ref);
|
|
423
|
+
const own = (model, gateway = false) => ({
|
|
424
|
+
model,
|
|
425
|
+
ref,
|
|
426
|
+
vendor,
|
|
427
|
+
gateway,
|
|
428
|
+
metered: false,
|
|
429
|
+
chat: false,
|
|
430
|
+
});
|
|
431
|
+
const client = await installedClient(ref);
|
|
248
432
|
if (client?.image) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
vendor
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
};
|
|
433
|
+
return own(client.image.model(aliasOf(id)));
|
|
434
|
+
}
|
|
435
|
+
switch (provider) {
|
|
436
|
+
case "openai":
|
|
437
|
+
return own(createOpenAI({ apiKey: need(cfg, "openai") }).image(id));
|
|
438
|
+
case "google":
|
|
439
|
+
return own(createGoogle({ apiKey: need(cfg, "google") }).image(id));
|
|
440
|
+
case "fal":
|
|
441
|
+
return own(createFal({ apiKey: need(cfg, "fal") }).image(id));
|
|
442
|
+
case "replicate":
|
|
443
|
+
return own(createReplicate({ apiToken: need(cfg, "replicate") }).image(id));
|
|
444
|
+
case null:
|
|
445
|
+
case "gateway":
|
|
446
|
+
if (cfg.keys.gateway) {
|
|
447
|
+
return {
|
|
448
|
+
...own(createGateway({ apiKey: cfg.keys.gateway }).imageModel(id), true),
|
|
449
|
+
chat: isChatImageModel(id),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
if (provider === null && vendor === "openai" && cfg.keys.openai) {
|
|
453
|
+
return own(createOpenAI({ apiKey: cfg.keys.openai }).image(aliasOf(id)));
|
|
454
|
+
}
|
|
272
455
|
}
|
|
273
456
|
throw new ModelUnavailableError("Für Bilder ist kein Modell eingerichtet.");
|
|
274
457
|
}
|
|
275
458
|
export async function videoModel(meta = {}) {
|
|
276
|
-
const
|
|
277
|
-
if (
|
|
278
|
-
return
|
|
279
|
-
}
|
|
280
|
-
const ref =
|
|
281
|
-
const {
|
|
282
|
-
const gateway =
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
459
|
+
const route = await routeOf("video");
|
|
460
|
+
if (route.kind === "credits") {
|
|
461
|
+
return viaCredits(route, "video", meta, (c, id) => c.videoModel(id));
|
|
462
|
+
}
|
|
463
|
+
const { cfg, ref } = route;
|
|
464
|
+
const { provider, id, vendor } = parse(ref);
|
|
465
|
+
const own = (model, gateway = false) => ({
|
|
466
|
+
model,
|
|
467
|
+
ref,
|
|
468
|
+
vendor,
|
|
469
|
+
gateway,
|
|
470
|
+
metered: false,
|
|
471
|
+
});
|
|
472
|
+
switch (provider) {
|
|
473
|
+
case "google":
|
|
474
|
+
return own(createGoogle({ apiKey: need(cfg, "google") }).video(id));
|
|
475
|
+
case "fal":
|
|
476
|
+
return own(createFal({ apiKey: need(cfg, "fal") }).video(id));
|
|
477
|
+
case "replicate":
|
|
478
|
+
return own(createReplicate({ apiToken: need(cfg, "replicate") }).video(id));
|
|
479
|
+
case null:
|
|
480
|
+
case "gateway":
|
|
481
|
+
if (cfg.keys.gateway) {
|
|
482
|
+
return own(createGateway({ apiKey: cfg.keys.gateway }).videoModel(id), true);
|
|
483
|
+
}
|
|
291
484
|
}
|
|
292
485
|
throw new ModelUnavailableError("Für Videos ist kein Modell eingerichtet.");
|
|
293
486
|
}
|
|
294
487
|
/** The model that reads a text aloud. */
|
|
295
488
|
export async function speechModel(meta = {}) {
|
|
296
|
-
const
|
|
297
|
-
if (
|
|
298
|
-
const
|
|
299
|
-
if (!
|
|
489
|
+
const route = await routeOf("speech");
|
|
490
|
+
if (route.kind === "credits") {
|
|
491
|
+
const resolved = await viaCredits(route, "speech", meta, (c, id) => c.speechModel(id));
|
|
492
|
+
if (!resolved.ref) {
|
|
300
493
|
throw new ModelUnavailableError("Für Sprachausgabe ist kein Modell eingerichtet.");
|
|
301
494
|
}
|
|
302
|
-
return
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
495
|
+
return resolved;
|
|
496
|
+
}
|
|
497
|
+
const { cfg, ref } = route;
|
|
498
|
+
const { provider, id, vendor } = parse(ref);
|
|
499
|
+
const own = (model, gateway = false) => ({
|
|
500
|
+
model,
|
|
501
|
+
ref,
|
|
502
|
+
vendor,
|
|
503
|
+
gateway,
|
|
504
|
+
metered: false,
|
|
505
|
+
});
|
|
506
|
+
switch (provider) {
|
|
507
|
+
case "openai":
|
|
508
|
+
return own(createOpenAI({ apiKey: need(cfg, "openai") }).speech(id));
|
|
509
|
+
case "google":
|
|
510
|
+
return own(createGoogle({ apiKey: need(cfg, "google") }).speech(id));
|
|
511
|
+
case "fal":
|
|
512
|
+
return own(createFal({ apiKey: need(cfg, "fal") }).speech(id));
|
|
513
|
+
case "elevenlabs":
|
|
514
|
+
return own(createElevenLabs({ apiKey: need(cfg, "elevenlabs") }).speech(id));
|
|
515
|
+
case null:
|
|
516
|
+
case "gateway":
|
|
517
|
+
if (cfg.keys.gateway) {
|
|
518
|
+
return own(createGateway({ apiKey: cfg.keys.gateway }).speechModel(id), true);
|
|
519
|
+
}
|
|
520
|
+
if (provider === null && vendor === "openai" && cfg.keys.openai) {
|
|
521
|
+
return own(createOpenAI({ apiKey: cfg.keys.openai }).speech(aliasOf(id)));
|
|
522
|
+
}
|
|
309
523
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
524
|
+
throw new ModelUnavailableError("Für Sprachausgabe ist kein Modell eingerichtet.");
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* What turns a voice note into text: a transcription model (ElevenLabs Scribe, Whisper), or a
|
|
528
|
+
* chat model that takes audio files (Gemini) — that one also on the credits.
|
|
529
|
+
*/
|
|
530
|
+
export async function listenerModel(meta = {}) {
|
|
531
|
+
const route = await routeOf("audio");
|
|
532
|
+
if (route.kind === "credits") {
|
|
533
|
+
// On the credits a transcription model is called as one: named so, or bound to the class.
|
|
534
|
+
const catalog = await classCatalog();
|
|
535
|
+
const transcribes = route.model
|
|
536
|
+
? catalog?.models?.find((m) => m.id === route.model)?.kind === "transcription"
|
|
537
|
+
: Boolean(catalog?.classes.audio?.transcribes);
|
|
538
|
+
if (transcribes) {
|
|
539
|
+
return {
|
|
540
|
+
kind: "transcription",
|
|
541
|
+
resolved: await viaCredits(route, "audio", meta, (c, id) => c.transcriptionModel(id)),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
321
544
|
}
|
|
322
|
-
if (
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
545
|
+
if (route.kind === "own") {
|
|
546
|
+
const { cfg, ref } = route;
|
|
547
|
+
const { provider, id, vendor } = parse(ref);
|
|
548
|
+
const transcriber = provider === "elevenlabs"
|
|
549
|
+
? createElevenLabs({ apiKey: need(cfg, "elevenlabs") }).transcription(id)
|
|
550
|
+
: provider === "fal"
|
|
551
|
+
? createFal({ apiKey: need(cfg, "fal") }).transcription(id)
|
|
552
|
+
: provider === "openai" && /transcribe|whisper/.test(id)
|
|
553
|
+
? createOpenAI({ apiKey: need(cfg, "openai") }).transcription(id)
|
|
554
|
+
: null;
|
|
555
|
+
if (transcriber) {
|
|
556
|
+
return {
|
|
557
|
+
kind: "transcription",
|
|
558
|
+
resolved: { model: transcriber, ref, vendor, gateway: false, metered: false },
|
|
559
|
+
};
|
|
560
|
+
}
|
|
330
561
|
}
|
|
331
|
-
|
|
562
|
+
return { kind: "chat", resolved: await textModel("audio", meta) };
|
|
332
563
|
}
|
|
333
564
|
/**
|
|
334
565
|
* The model that turns passages and questions into vectors for a project's document index.
|
|
@@ -336,38 +567,41 @@ export async function speechModel(meta = {}) {
|
|
|
336
567
|
* works on keywords alone.
|
|
337
568
|
*/
|
|
338
569
|
export async function embeddingModel() {
|
|
339
|
-
const
|
|
340
|
-
if (
|
|
341
|
-
const
|
|
342
|
-
|
|
570
|
+
const cfg = await config();
|
|
571
|
+
if (managed) {
|
|
572
|
+
const access = await creditAccess(cfg, true).catch(() => null);
|
|
573
|
+
const bound = access ? (await classCatalog())?.classes.embedding : null;
|
|
574
|
+
return access && bound
|
|
343
575
|
? { model: gatewayClient(access, {}).embeddingModel("wizards/embedding"), ref: bound.model }
|
|
344
576
|
: null;
|
|
345
577
|
}
|
|
346
578
|
const ref = env.models.embedding;
|
|
347
|
-
const { provider,
|
|
579
|
+
const { provider, id, vendor } = parse(ref);
|
|
348
580
|
if (provider === "ollama") {
|
|
349
581
|
const ollama = createOpenAI({
|
|
350
|
-
baseURL:
|
|
582
|
+
baseURL: cfg.settings.ollamaUrl ?? env.ollamaUrl,
|
|
351
583
|
apiKey: "ollama",
|
|
352
584
|
});
|
|
353
|
-
return { model: ollama.embedding(
|
|
585
|
+
return { model: ollama.embedding(id), ref };
|
|
354
586
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return { model: gateway.embeddingModel(`${vendor}/${model}`), ref };
|
|
587
|
+
if ((provider === null || provider === "gateway") && cfg.keys.gateway) {
|
|
588
|
+
return { model: createGateway({ apiKey: cfg.keys.gateway }).embeddingModel(id), ref };
|
|
358
589
|
}
|
|
359
|
-
if ((provider === "openai" || vendor === "openai") &&
|
|
360
|
-
return {
|
|
590
|
+
if ((provider === "openai" || vendor === "openai") && cfg.keys.openai) {
|
|
591
|
+
return {
|
|
592
|
+
model: createOpenAI({ apiKey: cfg.keys.openai }).embedding(provider ? id : aliasOf(id)),
|
|
593
|
+
ref,
|
|
594
|
+
};
|
|
361
595
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
596
|
+
// No key of its own: the linked account's gateway, where its catalog has the class.
|
|
597
|
+
const access = await creditAccess(cfg, false).catch(() => null);
|
|
598
|
+
const bound = access ? (await classCatalog())?.classes.embedding : null;
|
|
599
|
+
return access && bound
|
|
600
|
+
? { model: gatewayClient(access, {}).embeddingModel("wizards/embedding"), ref: bound.model }
|
|
601
|
+
: null;
|
|
367
602
|
}
|
|
368
603
|
/** Search tools an AI Gateway runs itself; they travel in the request, so any gateway client names them. */
|
|
369
604
|
export const gatewayTools = createGateway({ apiKey: "unused" }).tools;
|
|
370
|
-
/** Whether any text model can answer at all: the studio says so before the first chat turn. */
|
|
371
605
|
/**
|
|
372
606
|
* Whether a class can run here, found out without calling it: null, or what the person reads.
|
|
373
607
|
* A class the gateway serves counts as there unless its catalog leaves it unbound.
|
|
@@ -380,10 +614,20 @@ export async function classProblem(cls) {
|
|
|
380
614
|
? await videoModel()
|
|
381
615
|
: cls === "speech"
|
|
382
616
|
? await speechModel()
|
|
383
|
-
:
|
|
617
|
+
: cls === "audio"
|
|
618
|
+
? (await listenerModel()).resolved
|
|
619
|
+
: await textModel(cls);
|
|
384
620
|
if (resolved.metered) {
|
|
385
|
-
const
|
|
386
|
-
|
|
621
|
+
const catalog = await classCatalog();
|
|
622
|
+
const named = creditsModelOf((await config()).settings.bindings[cls]);
|
|
623
|
+
if (named) {
|
|
624
|
+
return catalog?.models && !catalog.models.some((m) => m.id === named)
|
|
625
|
+
? "Dieses Modell gibt es beim Konto nicht."
|
|
626
|
+
: null;
|
|
627
|
+
}
|
|
628
|
+
return catalog?.classes && !catalog.classes[cls]
|
|
629
|
+
? "Dafür ist beim Konto kein Modell eingerichtet."
|
|
630
|
+
: null;
|
|
387
631
|
}
|
|
388
632
|
const client = harness(resolved.vendor);
|
|
389
633
|
if (client && (await detectHarness(client.id))?.auth === "none") {
|
|
@@ -398,6 +642,7 @@ export async function classProblem(cls) {
|
|
|
398
642
|
throw err;
|
|
399
643
|
}
|
|
400
644
|
}
|
|
645
|
+
/** Whether any text model can answer at all: the studio says so before the first chat turn. */
|
|
401
646
|
export async function hasTextModel() {
|
|
402
647
|
try {
|
|
403
648
|
await textModel("standard");
|
|
@@ -407,6 +652,103 @@ export async function hasTextModel() {
|
|
|
407
652
|
return false;
|
|
408
653
|
}
|
|
409
654
|
}
|
|
655
|
+
export async function classWays() {
|
|
656
|
+
const cfg = await config();
|
|
657
|
+
const ways = {};
|
|
658
|
+
for (const cls of MODEL_CLASSES) {
|
|
659
|
+
const ref = ownRef(cfg, cls);
|
|
660
|
+
if (ref) {
|
|
661
|
+
const { provider, vendor } = parse(ref);
|
|
662
|
+
const client = provider === null && isHarnessVendor(vendor);
|
|
663
|
+
ways[cls] = {
|
|
664
|
+
kind: client ? "client" : provider === "ollama" ? "local" : "key",
|
|
665
|
+
by: client
|
|
666
|
+
? vendor
|
|
667
|
+
: providerOfRef(ref) === "gateway" && !cfg.keys.gateway
|
|
668
|
+
? vendor
|
|
669
|
+
: providerOfRef(ref),
|
|
670
|
+
ref,
|
|
671
|
+
problem: null,
|
|
672
|
+
};
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
let problem = null;
|
|
676
|
+
const access = await creditAccess(cfg, creditsChosen(cfg, cls)).catch((err) => {
|
|
677
|
+
problem = err.message;
|
|
678
|
+
return null;
|
|
679
|
+
});
|
|
680
|
+
ways[cls] = access
|
|
681
|
+
? {
|
|
682
|
+
kind: "credits",
|
|
683
|
+
by: null,
|
|
684
|
+
ref: creditsModelOf(cfg.settings.bindings[cls]) ??
|
|
685
|
+
(await classCatalog())?.classes[cls]?.model ??
|
|
686
|
+
"",
|
|
687
|
+
problem: null,
|
|
688
|
+
}
|
|
689
|
+
: { kind: "none", by: null, ref: "", problem: problem ?? missingText(cfg, cls) };
|
|
690
|
+
}
|
|
691
|
+
return ways;
|
|
692
|
+
}
|
|
693
|
+
/** What the settings page shows: the choices, which keys are there (never the keys), and the ways. */
|
|
694
|
+
export async function modelSettings() {
|
|
695
|
+
const cfg = await config();
|
|
696
|
+
return {
|
|
697
|
+
source: cfg.settings.source,
|
|
698
|
+
bindings: cfg.settings.bindings,
|
|
699
|
+
ollamaUrl: cfg.settings.ollamaUrl ?? env.ollamaUrl,
|
|
700
|
+
creditFallback: cfg.settings.creditFallback !== false,
|
|
701
|
+
keys: Object.fromEntries(LOCAL_KEYS.map((k) => [k, Boolean(cfg.keys[k])])),
|
|
702
|
+
ways: await classWays(),
|
|
703
|
+
/** What a class costs on the credits, where they are at hand. */
|
|
704
|
+
prices: (await classCatalog())?.classes ?? null,
|
|
705
|
+
/** The media models the credits pay for, which a binding may name. */
|
|
706
|
+
creditModels: (await classCatalog())?.models ?? [],
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
// --- Checking a key ------------------------------------------------------------------
|
|
710
|
+
/** A cheap authenticated call: whether the provider takes the key. Nothing is generated. */
|
|
711
|
+
export async function checkKey(name, key) {
|
|
712
|
+
const requests = {
|
|
713
|
+
openai: ["https://api.openai.com/v1/models", { authorization: `Bearer ${key}` }],
|
|
714
|
+
anthropic: [
|
|
715
|
+
"https://api.anthropic.com/v1/models?limit=1",
|
|
716
|
+
{ "x-api-key": key, "anthropic-version": "2023-06-01" },
|
|
717
|
+
],
|
|
718
|
+
google: [
|
|
719
|
+
"https://generativelanguage.googleapis.com/v1beta/models?pageSize=1",
|
|
720
|
+
{ "x-goog-api-key": key },
|
|
721
|
+
],
|
|
722
|
+
fal: [
|
|
723
|
+
"https://api.fal.ai/v1/models?endpoint_id=fal-ai/flux/schnell",
|
|
724
|
+
{ authorization: `Key ${key}` },
|
|
725
|
+
],
|
|
726
|
+
elevenlabs: ["https://api.elevenlabs.io/v1/models", { "xi-api-key": key }],
|
|
727
|
+
replicate: ["https://api.replicate.com/v1/account", { authorization: `Bearer ${key}` }],
|
|
728
|
+
gateway: ["https://ai-gateway.vercel.sh/v1/credits", { authorization: `Bearer ${key}` }],
|
|
729
|
+
};
|
|
730
|
+
const [url, headers] = requests[name];
|
|
731
|
+
try {
|
|
732
|
+
const res = await fetch(url, { headers, signal: AbortSignal.timeout(10_000) });
|
|
733
|
+
if (res.ok) {
|
|
734
|
+
return { ok: true, message: null };
|
|
735
|
+
}
|
|
736
|
+
const body = await res.text().catch(() => "");
|
|
737
|
+
// A key restricted to some permissions is still a key the provider knows.
|
|
738
|
+
if (res.status === 403 || /missing_permissions/.test(body)) {
|
|
739
|
+
return { ok: true, message: null };
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
ok: false,
|
|
743
|
+
message: res.status === 401
|
|
744
|
+
? "Der Anbieter kennt diesen Key nicht."
|
|
745
|
+
: `Der Anbieter antwortet mit ${res.status}.`,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
return { ok: false, message: "Der Anbieter ist gerade nicht erreichbar." };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
410
752
|
let catalog = new Map();
|
|
411
753
|
let catalogLoadedAt = 0;
|
|
412
754
|
/** Public prices of the AI Gateway: what a runtime that runs alone shows as the cost of a run. */
|
|
@@ -426,9 +768,11 @@ export async function loadCatalog(force = false) {
|
|
|
426
768
|
// Prices fall back to conservative defaults below.
|
|
427
769
|
}
|
|
428
770
|
}
|
|
771
|
+
/** The AI Gateway's prices, by its id of the model: what an own key of the same model costs too. */
|
|
429
772
|
function priceOf(ref) {
|
|
430
|
-
const {
|
|
431
|
-
return catalog.get(`${
|
|
773
|
+
const { provider, id } = parse(ref);
|
|
774
|
+
return (catalog.get(provider === null || provider === "gateway" ? id : `${provider}/${id}`)?.pricing ??
|
|
775
|
+
{});
|
|
432
776
|
}
|
|
433
777
|
const n = (v, fallback) => {
|
|
434
778
|
const x = Number(v);
|
|
@@ -487,3 +831,7 @@ export function speechCostUsd(ref, characters) {
|
|
|
487
831
|
return n(p.audio_output_token_cost, 0.00001) * Math.ceil((characters / 15) * 25);
|
|
488
832
|
}
|
|
489
833
|
export const WEB_SEARCH_COST_USD = 0.01;
|
|
834
|
+
/** A transcript, priced by the minute; the gateway's catalog has no transcription prices. */
|
|
835
|
+
export function transcriptionCostUsd(seconds) {
|
|
836
|
+
return (seconds / 60) * 0.006;
|
|
837
|
+
}
|