bareloop 0.39.0 → 0.41.0
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/CHANGELOG.md +60 -0
- package/bareloop.context.md +137 -112
- package/package.json +2 -2
- package/src/authorflow.js +333 -58
- package/src/authoring.js +65 -2
- package/src/authorjob.js +57 -9
- package/src/authorreadout.js +50 -2
- package/src/authorrun.js +30 -27
- package/src/authorscout.js +9 -6
- package/src/bridges.js +1 -1
- package/src/bundle.js +7 -4
- package/src/bundlerun.js +4 -1
- package/src/calibrate.js +1 -1
- package/src/cardauthor.js +4 -1
- package/src/cli.js +1 -1
- package/src/config.js +129 -29
- package/src/declaredclose.js +4 -3
- package/src/draftspend.js +6 -1
- package/src/index.js +2 -4
- package/src/interviewrun.js +73 -52
- package/src/job.js +50 -11
- package/src/jobblock.js +140 -0
- package/src/judged.js +12 -44
- package/src/keysfile.js +15 -7
- package/src/kinds.js +2 -2
- package/src/ledger.js +1 -1
- package/src/modellist.js +84 -0
- package/src/monthly.js +23 -43
- package/src/panel/authorroutes.js +34 -19
- package/src/panel/authorsession.js +187 -72
- package/src/panel/index.html +699 -215
- package/src/panel/jobplan.js +107 -0
- package/src/panel/runroutes.js +3 -0
- package/src/panel/server.js +102 -28
- package/src/panel/settingsroutes.js +232 -173
- package/src/planrun.js +6 -6
- package/src/promptregisters.js +1 -1
- package/src/providerrows.js +226 -141
- package/src/providers.js +58 -250
- package/src/providershapes.js +18 -0
- package/src/replay.js +1 -1
- package/src/reuse.js +1 -1
- package/src/root.js +1 -1
- package/src/run.js +3 -3
- package/src/selection.js +1 -1
- package/src/source.js +64 -1
- package/src/text.js +13 -0
- package/src/thinking.js +130 -0
- package/src/transport.js +1 -1
- package/src/userrun.js +53 -76
- package/types/authorflow.d.ts +258 -39
- package/types/authoring.d.ts +7 -1
- package/types/authorjob.d.ts +35 -3
- package/types/authorreadout.d.ts +17 -1
- package/types/authorscout.d.ts +5 -3
- package/types/config.d.ts +23 -6
- package/types/declaredclose.d.ts +4 -1
- package/types/draftspend.d.ts +8 -2
- package/types/index.d.ts +1 -1
- package/types/job.d.ts +6 -6
- package/types/jobblock.d.ts +102 -0
- package/types/judged.d.ts +5 -36
- package/types/keysfile.d.ts +15 -4
- package/types/modellist.d.ts +50 -0
- package/types/monthly.d.ts +4 -3
- package/types/panel/authorsession.d.ts +44 -15
- package/types/panel/jobplan.d.ts +58 -0
- package/types/panel/server.d.ts +34 -4
- package/types/planrun.d.ts +1 -1
- package/types/providerrows.d.ts +192 -99
- package/types/providers.d.ts +35 -116
- package/types/providershapes.d.ts +10 -0
- package/types/run.d.ts +1 -1
- package/types/source.d.ts +26 -0
- package/types/thinking.d.ts +90 -0
- package/types/userrun.d.ts +6 -1
package/src/providerrows.js
CHANGED
|
@@ -1,37 +1,24 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// (
|
|
1
|
+
// rewritten-from: fwdloop src/provider.js@f9449e8 (`effectiveModels`, `tickedLines`)
|
|
2
|
+
// PANEL-BUILD.md P4b / P8 — the Providers rows, driven by the keys file. NOTHING here is a
|
|
3
|
+
// hardcoded provider list, model, address or price: one row exists per key in
|
|
4
|
+
// `~/.config/bareloop/.env` that has a value; each row's API shape / Base URL / old-max_tokens
|
|
5
|
+
// tick come from config.json `keys.<ENV NAME>` and its model lines from `models.<ENV NAME>`
|
|
6
|
+
// (src/config.js), else the plain defaults below (shape `openai-api`, no address, no lines).
|
|
7
|
+
// A row's API shape names a `src/providers.js` table entry (`PROVIDER_TABLE`); its Base URL is
|
|
8
|
+
// that provider's `baseUrl` option.
|
|
7
9
|
//
|
|
8
10
|
// This module is also THE one owner of "model -> key + shape + URL": Chat's Model menu,
|
|
9
11
|
// the author doors and the run doors all resolve through `findRow` / `keyNameFor` /
|
|
10
12
|
// `applyConfiguredKey` / `modelChoiceFor` below — no second lookup exists. Key VALUES
|
|
11
13
|
// never pass through here — only names.
|
|
12
|
-
import {
|
|
13
|
-
import { readConfig, ConfigError } from './config.js';
|
|
14
|
+
import { readConfig } from './config.js';
|
|
14
15
|
import { filledKeyNames } from './keysfile.js';
|
|
16
|
+
import { SHAPES } from './providershapes.js';
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
export const DEEPSEEK_BASE_URL = 'https://api.deepseek.com/v1';
|
|
18
|
+
export { SHAPES };
|
|
18
19
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
* `defaultUrl` is the host a blank Base URL means ('' = the vendor's own, unshown).
|
|
22
|
-
* @type {readonly {id: string, label: string, defaultUrl: string}[]}
|
|
23
|
-
*/
|
|
24
|
-
export const SHAPES = Object.freeze([
|
|
25
|
-
Object.freeze({ id: 'anthropic-api', label: 'Anthropic', defaultUrl: 'https://api.anthropic.com/v1' }),
|
|
26
|
-
Object.freeze({ id: 'openai-api', label: 'OpenAI-compatible', defaultUrl: 'https://api.openai.com/v1' }),
|
|
27
|
-
Object.freeze({ id: 'gemini-api', label: 'Gemini', defaultUrl: '' }),
|
|
28
|
-
]);
|
|
29
|
-
|
|
30
|
-
/** The five empty lines a fresh keys file is created with, in this order. */
|
|
31
|
-
export const PRESET_KEY_NAMES = Object.freeze(['ANTHROPIC_API_KEY', 'DEEPSEEK_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY', 'LOCAL_API_KEY']);
|
|
32
|
-
|
|
33
|
-
/** A local server's default endpoint: Ollama's OpenAI-compatible host (LM Studio / llama.cpp: change the URL). */
|
|
34
|
-
export const LOCAL_BASE_URL = 'http://127.0.0.1:11434/v1';
|
|
20
|
+
/** What a row with nothing saved reads as: no vendor is assumed. */
|
|
21
|
+
const DEFAULT_SHAPE = 'openai-api';
|
|
35
22
|
|
|
36
23
|
/**
|
|
37
24
|
* A key line whose value is the word `null` is SET and means "no real key": the row exists and
|
|
@@ -52,32 +39,61 @@ export function usableKey(raw) {
|
|
|
52
39
|
}
|
|
53
40
|
|
|
54
41
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* @
|
|
58
|
-
* @
|
|
42
|
+
* @typedef {object} ModelLine one model line of a row (`models.<ENV>` in config.json)
|
|
43
|
+
* @property {string} model the model id
|
|
44
|
+
* @property {boolean} use ticked = offered in Chat (needs both prices)
|
|
45
|
+
* @property {number} [inPerM] the customer's own price, USD per 1M input tokens
|
|
46
|
+
* @property {number} [outPerM] the same, for output tokens
|
|
59
47
|
*/
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
case 'ANTHROPIC_API_KEY': return { name: 'claude-sonnet-5', shape: 'anthropic-api', baseUrl: '' };
|
|
63
|
-
case 'DEEPSEEK_API_KEY': return { name: 'deepseek-flash', shape: 'openai-api', baseUrl: DEEPSEEK_BASE_URL };
|
|
64
|
-
case 'GEMINI_API_KEY': return { name: '', shape: 'gemini-api', baseUrl: '' };
|
|
65
|
-
case 'LOCAL_API_KEY': return { name: '', shape: 'openai-api', baseUrl: LOCAL_BASE_URL };
|
|
66
|
-
default: return { name: '', shape: 'openai-api', baseUrl: '' };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
48
|
+
|
|
49
|
+
/** @typedef {{ rates: { in: number, out: number }, inPerM: number, outPerM: number, envName: string }|null} PriceResult */
|
|
69
50
|
|
|
70
51
|
/**
|
|
71
52
|
* @typedef {object} KeyRow
|
|
72
53
|
* @property {string} envName the key variable NAME (read-only label; never a value)
|
|
73
|
-
* @property {string} name the model id Chat uses ('' = not offered in Chat)
|
|
74
54
|
* @property {string} provider the `src/providers.js` table entry (the API shape's id)
|
|
75
|
-
* @property {string} baseUrl the saved Base URL ('' =
|
|
76
|
-
* @property {
|
|
77
|
-
*
|
|
78
|
-
* @property {unknown} [priceOutPerM] the same, for output tokens
|
|
55
|
+
* @property {string} baseUrl the saved Base URL ('' = none typed yet)
|
|
56
|
+
* @property {boolean} legacyMaxTokens the "old max_tokens" tick (only meaningful on `openai-api`)
|
|
57
|
+
* @property {ModelLine[]} models the row's model lines
|
|
79
58
|
*/
|
|
80
59
|
|
|
60
|
+
const isPrice = (/** @type {unknown} */ v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The model lines of a row, pure (no I/O, no rewrite of the file). A row WITH `models.<ENV>` uses
|
|
64
|
+
* that list and ignores `keys.<ENV>.{name,priceInPerM,priceOutPerM}`. A row WITHOUT it whose
|
|
65
|
+
* `keys.<ENV>.name` is a non-empty saved string reads as ONE line: ticked when BOTH older prices
|
|
66
|
+
* are finite numbers >= 0 (the line carries them), else UNTICKED with no price (a ticked line
|
|
67
|
+
* needs both prices). A row with no saved name reads as no lines — nothing is carried from a
|
|
68
|
+
* built-in table.
|
|
69
|
+
* @param {string} envName
|
|
70
|
+
* @param {Record<string, any>} config
|
|
71
|
+
* @returns {ModelLine[]}
|
|
72
|
+
*/
|
|
73
|
+
export function effectiveModels(envName, config) {
|
|
74
|
+
const models = config?.models;
|
|
75
|
+
if (models && typeof models === 'object' && Object.hasOwn(models, envName) && Array.isArray(models[envName])) {
|
|
76
|
+
return models[envName].map((/** @type {ModelLine} */ l) => ({ ...l }));
|
|
77
|
+
}
|
|
78
|
+
const s = config?.keys?.[envName];
|
|
79
|
+
if (!s || typeof s !== 'object' || typeof s.name !== 'string' || s.name === '') return [];
|
|
80
|
+
return isPrice(s.priceInPerM) && isPrice(s.priceOutPerM)
|
|
81
|
+
? [{ model: s.name, use: true, inPerM: s.priceInPerM, outPerM: s.priceOutPerM }]
|
|
82
|
+
: [{ model: s.name, use: false }];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @param {ModelLine} l @returns {boolean} a line Chat may offer: ticked and priced */
|
|
86
|
+
export const isTickedLine = (l) => l.use === true && isPrice(l.inPerM) && isPrice(l.outPerM);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Every ticked-and-priced line of the rows, in keys-file order.
|
|
90
|
+
* @param {readonly KeyRow[]} rows
|
|
91
|
+
* @returns {{ row: KeyRow, line: ModelLine }[]}
|
|
92
|
+
*/
|
|
93
|
+
export function tickedLines(rows) {
|
|
94
|
+
return rows.flatMap((row) => row.models.filter(isTickedLine).map((line) => ({ row, line })));
|
|
95
|
+
}
|
|
96
|
+
|
|
81
97
|
/**
|
|
82
98
|
* The rows: one per name in `filled`, saved entry over defaults.
|
|
83
99
|
* @param {{ filled: readonly string[], config: Record<string, any> }} src
|
|
@@ -86,47 +102,71 @@ export function defaultsFor(envName) {
|
|
|
86
102
|
export function keyRows({ filled, config }) {
|
|
87
103
|
const saved = config?.keys && typeof config.keys === 'object' && !Array.isArray(config.keys) ? config.keys : {};
|
|
88
104
|
return filled.map((envName) => {
|
|
89
|
-
const d = defaultsFor(envName);
|
|
90
105
|
const s = saved[envName] && typeof saved[envName] === 'object' ? saved[envName] : {};
|
|
106
|
+
const provider = SHAPES.some((x) => x.id === s.shape) ? s.shape : DEFAULT_SHAPE;
|
|
91
107
|
return {
|
|
92
108
|
envName,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
...(Object.hasOwn(s, 'priceOutPerM') ? { priceOutPerM: s.priceOutPerM } : {}),
|
|
109
|
+
provider,
|
|
110
|
+
baseUrl: typeof s.baseUrl === 'string' ? s.baseUrl : '',
|
|
111
|
+
legacyMaxTokens: s.legacyMaxTokens === true && provider === 'openai-api',
|
|
112
|
+
models: effectiveModels(envName, config),
|
|
98
113
|
};
|
|
99
114
|
});
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
/**
|
|
103
|
-
* The rows
|
|
104
|
-
*
|
|
118
|
+
* The key names that make rows: every key in the keys file with a value (file order), plus — when `env` is given — a key only the
|
|
119
|
+
* SHELL holds that config already names (`keys.<ENV>` or `models.<ENV>`), sorted after them. The page shows such a row as
|
|
120
|
+
* "found in shell"; the run path must resolve it the same way.
|
|
121
|
+
* @param {readonly string[]} filled the keys file's filled names
|
|
122
|
+
* @param {Record<string, any>} config
|
|
123
|
+
* @param {Record<string, string|undefined>} [env] the shell side
|
|
124
|
+
* @returns {string[]}
|
|
125
|
+
*/
|
|
126
|
+
export function rowNamesOf(filled, config, env) {
|
|
127
|
+
if (!env) return [...filled];
|
|
128
|
+
const named = new Set([...Object.keys(config?.keys ?? {}), ...Object.keys(config?.models ?? {})]);
|
|
129
|
+
const shellOnly = [...named].filter((n) => !filled.includes(n) && typeof env[n] === 'string' && env[n] !== '').sort();
|
|
130
|
+
return [...filled, ...shellOnly];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The rows for a keys home. `undefined` home (a door that skipped the keys file) = no rows. With `env`, a shell-only key that
|
|
135
|
+
* config names is a row too ({@link rowNamesOf}).
|
|
105
136
|
* @param {string|undefined} home
|
|
137
|
+
* @param {Record<string, string|undefined>} [env]
|
|
106
138
|
* @returns {KeyRow[]}
|
|
107
139
|
*/
|
|
108
|
-
export function rowsForHome(home) {
|
|
140
|
+
export function rowsForHome(home, env) {
|
|
109
141
|
if (home === undefined) return [];
|
|
110
|
-
|
|
142
|
+
const config = readConfig({ home }).config;
|
|
143
|
+
return keyRows({ filled: rowNamesOf(filledKeyNames(home), config, env), config });
|
|
111
144
|
}
|
|
112
145
|
|
|
113
146
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
147
|
+
* EVERY row a config home names, keyed or not: the keys file's filled names plus every name `keys.<ENV>` / `models.<ENV>` carries. For
|
|
148
|
+
* a readout that must say which variable a model line WOULD read ("KEY is not set in this shell") before any key exists. Never for a run.
|
|
149
|
+
* @param {string|undefined} home
|
|
150
|
+
* @returns {KeyRow[]}
|
|
116
151
|
*/
|
|
117
|
-
export function
|
|
118
|
-
|
|
152
|
+
export function namedRowsForHome(home) {
|
|
153
|
+
if (home === undefined) return [];
|
|
154
|
+
const config = readConfig({ home }).config;
|
|
155
|
+
const named = [...new Set([...Object.keys(config?.keys ?? {}), ...Object.keys(config?.models ?? {})])];
|
|
156
|
+
const filled = filledKeyNames(home);
|
|
157
|
+
return keyRows({ filled: [...filled, ...named.filter((n) => !filled.includes(n))], config });
|
|
119
158
|
}
|
|
120
159
|
|
|
121
160
|
/**
|
|
122
|
-
* An endpoint in comparable form:
|
|
123
|
-
*
|
|
161
|
+
* An endpoint in comparable form: scheme + host lower-cased, trailing slashes dropped. A blank
|
|
162
|
+
* Base URL is blank — there is no default host.
|
|
124
163
|
* @param {string} provider
|
|
125
164
|
* @param {string|null|undefined} baseUrl
|
|
126
165
|
* @returns {string}
|
|
127
166
|
*/
|
|
128
167
|
export function endpointOf(provider, baseUrl) {
|
|
129
|
-
|
|
168
|
+
void provider;
|
|
169
|
+
const raw = (typeof baseUrl === 'string' ? baseUrl : '').trim();
|
|
130
170
|
if (raw === '') return '';
|
|
131
171
|
try {
|
|
132
172
|
const u = new URL(raw);
|
|
@@ -136,8 +176,7 @@ export function endpointOf(provider, baseUrl) {
|
|
|
136
176
|
|
|
137
177
|
/**
|
|
138
178
|
* A job's provider in plain words for a refusal line: the host its Base URL points at (else the
|
|
139
|
-
* shape's
|
|
140
|
-
* Never a key, a path or a status code.
|
|
179
|
+
* shape's label), plus the model when the spec names one. Never a key, a path or a status code.
|
|
141
180
|
* @param {{provider?: string, baseUrl?: string|null, model?: string|null}} spec
|
|
142
181
|
* @returns {string}
|
|
143
182
|
*/
|
|
@@ -151,18 +190,10 @@ export function providerInWords(spec) {
|
|
|
151
190
|
return model !== '' ? `${host}, ${model}` : host;
|
|
152
191
|
}
|
|
153
192
|
|
|
154
|
-
/**
|
|
155
|
-
* The URL a row talks to, for display ('' = the vendor's own host, which has no URL to show).
|
|
156
|
-
* @param {KeyRow} row
|
|
157
|
-
* @returns {string}
|
|
158
|
-
*/
|
|
159
|
-
export function shownUrl(row) {
|
|
160
|
-
return row.baseUrl.trim() !== '' ? row.baseUrl : defaultUrlOf(row.provider);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
193
|
/**
|
|
164
194
|
* Which row a `(provider, baseUrl[, model])` identity belongs to: the rows on that shape and
|
|
165
|
-
* endpoint; when several share it, the one
|
|
195
|
+
* endpoint; when several share it, the one that HAS a ticked-and-priced line for `model`, else
|
|
196
|
+
* the first.
|
|
166
197
|
* @param {readonly KeyRow[]} rows
|
|
167
198
|
* @param {{ provider?: string|null, baseUrl?: string|null, model?: string|null }} id
|
|
168
199
|
* @returns {KeyRow|null}
|
|
@@ -172,70 +203,110 @@ export function findRow(rows, { provider, baseUrl, model }) {
|
|
|
172
203
|
const want = endpointOf(provider, baseUrl);
|
|
173
204
|
const same = rows.filter((r) => r.provider === provider && endpointOf(r.provider, r.baseUrl) === want);
|
|
174
205
|
if (same.length === 0) return null;
|
|
175
|
-
return (model ? same.find((r) => r.
|
|
206
|
+
return (model ? same.find((r) => r.models.some((l) => l.model === model && isTickedLine(l))) : undefined) ?? same[0];
|
|
176
207
|
}
|
|
177
208
|
|
|
178
209
|
/**
|
|
179
|
-
*
|
|
180
|
-
* (
|
|
210
|
+
* How a Model-menu pick resolves: the row named by `provider` (its key's env name) with a ticked-and-priced line for `model`.
|
|
211
|
+
* With no `provider` (an older caller) the first row ticking the model. `baseUrl` is absent when blank, exactly as a spec omits it.
|
|
212
|
+
* A model no row ticks, or a row that does not tick it, is `null` — never offered, never run.
|
|
181
213
|
* @param {readonly KeyRow[]} rows
|
|
182
|
-
* @param {string}
|
|
183
|
-
* @returns {{ provider: string, baseUrl?: string, envName: string, name: string }|null}
|
|
214
|
+
* @param {{ provider?: string|null, model?: string|null }} pick `provider` is the KEY's env name
|
|
215
|
+
* @returns {{ provider: string, baseUrl?: string, envName: string, name: string, legacyMaxTokens: boolean }|null}
|
|
184
216
|
*/
|
|
185
|
-
export function modelChoiceFor(rows, model) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
217
|
+
export function modelChoiceFor(rows, { provider, model }) {
|
|
218
|
+
if (typeof model !== 'string' || model === '') return null;
|
|
219
|
+
const hit = tickedLines(rows).find((x) => x.line.model === model && (typeof provider === 'string' && provider !== '' ? x.row.envName === provider : true));
|
|
220
|
+
if (!hit) return null;
|
|
221
|
+
const r = hit.row;
|
|
222
|
+
return { provider: r.provider, ...(r.baseUrl.trim() !== '' ? { baseUrl: r.baseUrl.trim() } : {}), envName: r.envName, name: hit.line.model, legacyMaxTokens: r.legacyMaxTokens };
|
|
189
223
|
}
|
|
190
224
|
|
|
191
225
|
/**
|
|
192
|
-
* The
|
|
226
|
+
* The lines Chat's Model menu offers: every ticked-and-priced line of the rows, in keys-file order, labelled `<KEY> · <model>`.
|
|
193
227
|
* @param {readonly KeyRow[]} rows
|
|
194
|
-
* @returns {{
|
|
228
|
+
* @returns {{ slot: string, model: string, label: string }[]}
|
|
195
229
|
*/
|
|
196
|
-
export function
|
|
197
|
-
return rows.
|
|
198
|
-
id: r.name, envName: r.envName, shape: SHAPES.find((s) => s.id === r.provider)?.label ?? r.provider,
|
|
199
|
-
}));
|
|
230
|
+
export function chatLines(rows) {
|
|
231
|
+
return tickedLines(rows).map(({ row, line }) => ({ slot: row.envName, model: line.model, label: `${row.envName} · ${line.model}` }));
|
|
200
232
|
}
|
|
201
233
|
|
|
234
|
+
/** The plain sentence when a stale Chat pick names a key and a model no row ticks (fwd's wording, bareloop's seam). */
|
|
235
|
+
export const REFUSE_NO_TICK = 'Tick a model in Settings > Providers. Nothing was spent.';
|
|
236
|
+
export const REFUSE_NO_PICK = 'Pick a model. Nothing was spent.';
|
|
237
|
+
/** @param {string} key @returns {string} */
|
|
238
|
+
export const refuseNotSetUp = (key) => `${key} is not set up: put its key in your keys file and type its Base URL in Settings > Providers. Nothing was spent.`;
|
|
239
|
+
|
|
202
240
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
241
|
+
* Judge a Model-menu pick `{provider, model}` against the rows, for the doors that start a session. `ok:true` carries the choice;
|
|
242
|
+
* otherwise the plain refusal (one of the three sentences).
|
|
243
|
+
* @param {readonly KeyRow[]} rows
|
|
244
|
+
* @param {{ provider?: string|null, model?: string|null }} pick
|
|
245
|
+
* @returns {{ ok: true, choice: NonNullable<ReturnType<typeof modelChoiceFor>> } | { ok: false, say: string }}
|
|
246
|
+
*/
|
|
247
|
+
export function judgePick(rows, pick) {
|
|
248
|
+
if (typeof pick.model !== 'string' || pick.model === '') return { ok: false, say: tickedLines(rows).length === 0 ? REFUSE_NO_TICK : REFUSE_NO_PICK };
|
|
249
|
+
const choice = modelChoiceFor(rows, pick);
|
|
250
|
+
if (choice) return { ok: true, choice };
|
|
251
|
+
const key = typeof pick.provider === 'string' ? pick.provider : '';
|
|
252
|
+
if (key !== '' && !rows.some((r) => r.envName === key)) return { ok: false, say: refuseNotSetUp(key) };
|
|
253
|
+
if (key !== '') return { ok: false, say: rows.find((r) => r.envName === key)?.models.some(isTickedLine) ? REFUSE_NO_PICK : REFUSE_NO_TICK };
|
|
254
|
+
return { ok: false, say: tickedLines(rows).length === 0 ? REFUSE_NO_TICK : REFUSE_NO_PICK };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The CLI sentence when a run names no model and the spec carries none (fwd's `pickTicked` sentence).
|
|
259
|
+
*/
|
|
260
|
+
export const REFUSE_NAME_MODEL = 'Name the model with --model <id>: one ticked in Settings > Providers with both prices.';
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Which Settings line a RUN works on. A spec's own `model` wins; a spec that names none (signed before models were required)
|
|
264
|
+
* takes `--model <id>`; a spec with no `baseUrl` (the same era) matches a row on any endpoint of its shape. `--provider <KEY>`
|
|
265
|
+
* narrows to one key when two keys tick the same id. Pure; the signed spec is never changed, so its hash is untouched.
|
|
266
|
+
* @param {readonly KeyRow[]} rows
|
|
267
|
+
* @param {{ provider?: string|null, baseUrl?: string|null, model?: string|null }} spec the signed spec's worker fields
|
|
268
|
+
* @param {{ model?: string|null, provider?: string|null }} [flags] the operator's `--model` / `--provider`
|
|
269
|
+
* @returns {{ ok: true, row: KeyRow, line: ModelLine, model: string, baseUrl: string|undefined } | { ok: false, say: string }}
|
|
270
|
+
*/
|
|
271
|
+
export function resolveWorker(rows, spec, flags = {}) {
|
|
272
|
+
const model = typeof spec.model === 'string' && spec.model !== '' ? spec.model : (typeof flags.model === 'string' && flags.model !== '' ? flags.model : '');
|
|
273
|
+
if (model === '') return { ok: false, say: REFUSE_NAME_MODEL };
|
|
274
|
+
const want = typeof spec.baseUrl === 'string' && spec.baseUrl.trim() !== '' ? endpointOf(String(spec.provider), spec.baseUrl) : null;
|
|
275
|
+
const key = typeof flags.provider === 'string' && flags.provider !== '' ? flags.provider : null;
|
|
276
|
+
const hits = tickedLines(rows).filter(({ row, line }) => row.provider === spec.provider && line.model === model
|
|
277
|
+
&& (want === null || endpointOf(row.provider, row.baseUrl) === want) && (key === null || row.envName === key));
|
|
278
|
+
if (hits.length === 0) return { ok: false, say: refuseNotSetUp(model) };
|
|
279
|
+
const names = [...new Set(hits.map((h) => h.row.envName))];
|
|
280
|
+
if (names.length > 1) return { ok: false, say: `${model} is ticked on more than one key (${names.join(', ')}): name one with --provider <KEY>. Nothing was spent.` };
|
|
281
|
+
const { row, line } = hits[0];
|
|
282
|
+
return { ok: true, row, line, model, baseUrl: row.baseUrl.trim() !== '' ? row.baseUrl.trim() : undefined };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The key variable NAME a job on `(provider, baseUrl[, model])` reads: the matching row's own env name. There is NO built-in
|
|
287
|
+
* name: no matching row = no key (`name: null`).
|
|
206
288
|
* @param {string|null|undefined} provider
|
|
207
289
|
* @param {string|null|undefined} baseUrl
|
|
208
290
|
* @param {readonly KeyRow[]} rows
|
|
209
291
|
* @param {string|null|undefined} [model]
|
|
210
|
-
* @returns {{ name: string,
|
|
292
|
+
* @returns {{ name: string|null, chosen: boolean }}
|
|
211
293
|
*/
|
|
212
294
|
export function keyNameFor(provider, baseUrl, rows, model) {
|
|
213
|
-
const builtIn = resolveProvider(/** @type {string} */ (provider)).envKey;
|
|
214
295
|
const row = findRow(rows, { provider, baseUrl, model });
|
|
215
|
-
return row
|
|
216
|
-
? { name: row.envName, builtIn, chosen: true }
|
|
217
|
-
: { name: builtIn, builtIn, chosen: false };
|
|
296
|
+
return row ? { name: row.envName, chosen: true } : { name: null, chosen: false };
|
|
218
297
|
}
|
|
219
298
|
|
|
220
299
|
/**
|
|
221
|
-
* The
|
|
222
|
-
*
|
|
223
|
-
* no-key marker becomes {@link NO_KEY_PLACEHOLDER}). A copy — never mutates the input.
|
|
224
|
-
* No matching row and no marker = the env unchanged.
|
|
300
|
+
* The key VALUE for a row's env name out of an env (the `null` no-key marker becomes {@link NO_KEY_PLACEHOLDER}); `undefined`
|
|
301
|
+
* when the row's variable is unset or empty.
|
|
225
302
|
* @param {Record<string,string|undefined>} env
|
|
226
|
-
* @param {string|null|undefined}
|
|
227
|
-
* @
|
|
228
|
-
* @param {readonly KeyRow[]} rows
|
|
229
|
-
* @param {string|null|undefined} [model]
|
|
230
|
-
* @returns {Record<string,string|undefined>}
|
|
303
|
+
* @param {string|null|undefined} envName
|
|
304
|
+
* @returns {string|undefined}
|
|
231
305
|
*/
|
|
232
|
-
export function
|
|
233
|
-
if (typeof
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const value = env[k.name];
|
|
237
|
-
if (!k.chosen && value !== 'null') return env;
|
|
238
|
-
return { ...env, [k.builtIn]: usableKey(value) };
|
|
306
|
+
export function keyValueOf(env, envName) {
|
|
307
|
+
if (typeof envName !== 'string') return undefined;
|
|
308
|
+
const v = env[envName];
|
|
309
|
+
return v === undefined || v === '' ? undefined : usableKey(v);
|
|
239
310
|
}
|
|
240
311
|
|
|
241
312
|
/**
|
|
@@ -264,12 +335,12 @@ export function priceReadout(price) {
|
|
|
264
335
|
}
|
|
265
336
|
|
|
266
337
|
/**
|
|
267
|
-
* The customer's own price for the row a `(provider, baseUrl[, model])` identity
|
|
268
|
-
* the SAME row match {@link keyNameFor} uses (`findRow`), so the price and the key
|
|
269
|
-
* from one row.
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
338
|
+
* The customer's own price for the model LINE of the row a `(provider, baseUrl[, model])` identity
|
|
339
|
+
* belongs to — the SAME row match {@link keyNameFor} uses (`findRow`), so the price and the key
|
|
340
|
+
* always come from one row. With a `model`, the row's line of that name; without one, the row's
|
|
341
|
+
* first ticked line. `null` = no price set (the run keeps bare-agent's built-in guess). A line's
|
|
342
|
+
* price is a pair or nothing ({@link modelsProblem} guards the file), so a half price never
|
|
343
|
+
* reaches here. `rates` is bare-agent's `Loop({ rates })` shape (USD per 1K); `inPerM`/`outPerM`
|
|
273
344
|
* are what a readout prints.
|
|
274
345
|
* @param {string|null|undefined} provider
|
|
275
346
|
* @param {string|null|undefined} baseUrl
|
|
@@ -280,31 +351,45 @@ export function priceReadout(price) {
|
|
|
280
351
|
export function ratesFor(provider, baseUrl, rows, model) {
|
|
281
352
|
const row = findRow(rows, { provider, baseUrl, model });
|
|
282
353
|
if (!row) return null;
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const inPerM = /** @type {number} */ (
|
|
296
|
-
const outPerM = /** @type {number} */ (
|
|
297
|
-
return { rates: { in: inPerM / 1000, out: outPerM / 1000 }, inPerM, outPerM
|
|
354
|
+
const line = model ? row.models.find((l) => l.model === model) : row.models.find(isTickedLine);
|
|
355
|
+
const r = line ? ratesOfLine(line) : null;
|
|
356
|
+
return r ? { ...r, envName: row.envName } : null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* A model line's own price in bare-agent's shape, or `null` when the line carries none (a price is a pair or nothing).
|
|
361
|
+
* @param {ModelLine} line
|
|
362
|
+
* @returns {{ rates: { in: number, out: number }, inPerM: number, outPerM: number }|null}
|
|
363
|
+
*/
|
|
364
|
+
export function ratesOfLine(line) {
|
|
365
|
+
if (!isPrice(line.inPerM) || !isPrice(line.outPerM)) return null;
|
|
366
|
+
const inPerM = /** @type {number} */ (line.inPerM);
|
|
367
|
+
const outPerM = /** @type {number} */ (line.outPerM);
|
|
368
|
+
return { rates: { in: inPerM / 1000, out: outPerM / 1000 }, inPerM, outPerM };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* The row a judge's key and endpoint come from: the worker's own row when the judge shares the worker's API shape, else the judge
|
|
373
|
+
* shape's row that ticks the judge's model (else the first row of that shape). `null` when no row is on that shape.
|
|
374
|
+
* @param {readonly KeyRow[]} rows
|
|
375
|
+
* @param {{ provider: string, model?: string|null }} judge
|
|
376
|
+
* @param {string|null|undefined} workerProvider
|
|
377
|
+
* @param {KeyRow|null} workerRow
|
|
378
|
+
* @returns {KeyRow|null}
|
|
379
|
+
*/
|
|
380
|
+
export function judgeRowFor(rows, judge, workerProvider, workerRow) {
|
|
381
|
+
if (judge.provider === workerProvider && workerRow) return workerRow;
|
|
382
|
+
const onShape = rows.filter((r) => r.provider === judge.provider);
|
|
383
|
+
return onShape.find((r) => r.models.some((l) => l.model === judge.model && isTickedLine(l))) ?? onShape[0] ?? null;
|
|
298
384
|
}
|
|
299
385
|
|
|
300
386
|
/**
|
|
301
|
-
* The price a judge's rounds are booked at. A price belongs to the MODEL's
|
|
302
|
-
* the worker's own provider it calls the worker's endpoint, so a row on that endpoint
|
|
303
|
-
* judge's model prices it ({@link ratesFor}); a judge model that is absent or names
|
|
304
|
-
* keeps the worker's price (the worker's row, or none = the guess). A different
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* can come from different rows — same provider + endpoint, judge model naming another row.
|
|
387
|
+
* The price a judge's rounds are booked at. A price belongs to the MODEL's line: when the judge is
|
|
388
|
+
* the worker's own provider it calls the worker's endpoint, so a row on that endpoint with a line
|
|
389
|
+
* NAMED for the judge's model prices it ({@link ratesFor}); a judge model that is absent or names
|
|
390
|
+
* no line there keeps the worker's price (the worker's row, or none = the guess). A different
|
|
391
|
+
* provider resolves with no endpoint. The judge's KEY is chosen elsewhere (`applyConfiguredKey`,
|
|
392
|
+
* by the worker's model), so the two can come from different rows.
|
|
308
393
|
* @param {{ provider: string, model?: string|null }} judge
|
|
309
394
|
* @param {string|null|undefined} workerProvider
|
|
310
395
|
* @param {string|null|undefined} workerBaseUrl
|
|
@@ -315,6 +400,6 @@ export function ratesFor(provider, baseUrl, rows, model) {
|
|
|
315
400
|
export function judgeRatesFor(judge, workerProvider, workerBaseUrl, workerPrice, rows) {
|
|
316
401
|
if (judge.provider !== workerProvider) return ratesFor(judge.provider, undefined, rows, judge.model);
|
|
317
402
|
const want = endpointOf(judge.provider, workerBaseUrl);
|
|
318
|
-
const named = judge.model ? rows.some((r) => r.provider === judge.provider && endpointOf(r.provider, r.baseUrl) === want && r.
|
|
403
|
+
const named = judge.model ? rows.some((r) => r.provider === judge.provider && endpointOf(r.provider, r.baseUrl) === want && r.models.some((l) => l.model === judge.model)) : false;
|
|
319
404
|
return named ? ratesFor(judge.provider, workerBaseUrl, rows, judge.model) : workerPrice;
|
|
320
405
|
}
|