clay-server 5.2.0-beta.9 → 5.2.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/README.md +0 -4
- package/lib/clay-product-control-tools.js +1 -0
- package/lib/clay-product-control.js +14 -11
- package/lib/clay-vendor-usage.js +44 -0
- package/lib/github-star-count.js +147 -0
- package/lib/notes-checklist.js +43 -1
- package/lib/notes.js +2 -1
- package/lib/project-session-notes.js +33 -8
- package/lib/project-sessions.js +63 -24
- package/lib/project-start-protocol.js +29 -0
- package/lib/project-vendor-login.js +2 -0
- package/lib/project.js +5 -1
- package/lib/public/app.js +27 -18
- package/lib/public/css/contextual-clay.css +1 -1
- package/lib/public/css/filebrowser.css +1 -1
- package/lib/public/css/mate-loading-scene.css +21 -0
- package/lib/public/css/overlays.css +64 -256
- package/lib/public/css/project-mate-creation.css +5 -6
- package/lib/public/css/project-start.css +108 -0
- package/lib/public/css/projects-hub.css +53 -14
- package/lib/public/css/settings-shell.css +103 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/css/title-bar.css +83 -0
- package/lib/public/index.html +50 -63
- package/lib/public/modules/account-transport-settings.js +8 -2
- package/lib/public/modules/admin-settings-email.js +7 -2
- package/lib/public/modules/admin-settings-invitations.js +2 -2
- package/lib/public/modules/admin-settings-projects.js +2 -2
- package/lib/public/modules/admin-settings-ui.js +20 -4
- package/lib/public/modules/admin-settings-users.js +4 -4
- package/lib/public/modules/app-home-hub.js +2 -1
- package/lib/public/modules/app-messages.js +10 -2
- package/lib/public/modules/app-projects.js +1 -407
- package/lib/public/modules/app-rate-limit.js +12 -1
- package/lib/public/modules/clay-tool-presentation.js +1 -0
- package/lib/public/modules/contextual-clay.js +3 -1
- package/lib/public/modules/github-star.js +66 -0
- package/lib/public/modules/home-surface-boot.js +0 -1
- package/lib/public/modules/mate-loading-scene.js +17 -0
- package/lib/public/modules/pane-appearance-bridge.js +59 -0
- package/lib/public/modules/profile.js +11 -3
- package/lib/public/modules/project-mate-creation-workspace.js +13 -4
- package/lib/public/modules/project-session-visibility-settings.js +19 -16
- package/lib/public/modules/project-settings-environment.js +12 -1
- package/lib/public/modules/project-settings.js +119 -129
- package/lib/public/modules/project-start-dialog.js +130 -0
- package/lib/public/modules/project-start-folder-state.js +78 -0
- package/lib/public/modules/project-start-folder-view.js +190 -0
- package/lib/public/modules/project-start-forms.js +66 -0
- package/lib/public/modules/project-start-path.js +60 -0
- package/lib/public/modules/project-start-state.js +54 -0
- package/lib/public/modules/project-start-submit.js +67 -0
- package/lib/public/modules/projects-hub-view.js +32 -44
- package/lib/public/modules/projects-hub.js +39 -28
- package/lib/public/modules/server-settings.js +36 -91
- package/lib/public/modules/settings-project-target.js +56 -0
- package/lib/public/modules/settings-select.js +1 -1
- package/lib/public/modules/settings-shell-registry.js +88 -0
- package/lib/public/modules/settings-shell-view.js +104 -0
- package/lib/public/modules/settings-shell.js +342 -0
- package/lib/public/modules/sidebar-mobile.js +8 -30
- package/lib/public/modules/sidebar-projects.js +2 -1
- package/lib/public/modules/theme.js +31 -3
- package/lib/public/modules/user-settings-integrations.js +2 -1
- package/lib/public/modules/user-settings-layout.js +12 -34
- package/lib/public/modules/user-settings-profile.js +1 -1
- package/lib/public/modules/user-settings.js +18 -85
- package/lib/public/modules/vendor-usage-format.js +93 -0
- package/lib/public/modules/vendor-usage-list.js +45 -0
- package/lib/public/modules/vendor-usage-model.js +215 -0
- package/lib/public/modules/vendor-usage-overview.js +323 -0
- package/lib/public/style.css +10 -7
- package/lib/public/welcome-art/conversation-desk.png +0 -0
- package/lib/public/welcome-art/mate-clay.png +0 -0
- package/lib/public/welcome-art/project-clay.png +0 -0
- package/lib/sdk-bridge.js +6 -1
- package/lib/sdk-message-processor.js +9 -1
- package/lib/sdk-quota-snapshot.js +20 -2
- package/lib/server-github-stars.js +39 -0
- package/lib/server-global-ws.js +21 -17
- package/lib/server.js +11 -0
- package/lib/session-notes-mcp-server.js +26 -4
- package/lib/session-notes-writes.js +88 -0
- package/lib/session-visibility.js +20 -8
- package/lib/sessions.js +12 -2
- package/lib/vendor-usage-acp-readers.js +97 -0
- package/lib/vendor-usage-capabilities.js +88 -0
- package/lib/vendor-usage-http-readers.js +214 -0
- package/lib/vendor-usage-metrics.js +47 -0
- package/lib/vendor-usage-native-readers.js +106 -0
- package/lib/vendor-usage-overview.js +406 -0
- package/lib/vendor-usage-service.js +281 -0
- package/lib/ws-schema.js +4 -2
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex-query-contract.js +1 -0
- package/lib/yoke/adapters/codex.js +45 -12
- package/lib/yoke/claude-hook-installer.js +1 -0
- package/lib/yoke/codex-dynamic-tool-names.js +80 -0
- package/lib/yoke/codex-model-catalog.js +37 -3
- package/lib/yoke/muse-services.js +16 -3
- package/lib/yoke/providers/claude-model-descriptions.js +22 -2
- package/lib/yoke/vendor-registry.js +6 -2
- package/package.json +2 -1
- package/lib/public/modules/server-settings-navigation.js +0 -39
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// Shared acquisition service for vendor account usage.
|
|
2
|
+
//
|
|
3
|
+
// One place decides whether a direct read is allowed, runs it with a bounded
|
|
4
|
+
// timeout (cancelling the underlying work), shares an in-flight read between
|
|
5
|
+
// callers, caches results and backs off after failures. Provider-specific
|
|
6
|
+
// parsing lives in the reader modules; the capability table says which vendors
|
|
7
|
+
// have a reader at all.
|
|
8
|
+
//
|
|
9
|
+
// Identity: a direct read is allowed only when the credential provably belongs
|
|
10
|
+
// to the requester. In account mode, vendors that run as the server's own user
|
|
11
|
+
// (Muse, Kimi, Grok, Kiro) and Codex without a mapped OS user are refused with a
|
|
12
|
+
// clear status instead of showing the server user's account to every owner.
|
|
13
|
+
// Cache entries are keyed by owner + OS user + vendor. Where a cheap credential
|
|
14
|
+
// fingerprint exists (Kimi, Grok) it is re-read before cached figures are
|
|
15
|
+
// returned (including while a read is in flight) and again when a read
|
|
16
|
+
// completes; rotation or deletion discards the figures. Figures from a source
|
|
17
|
+
// whose account cannot be verified (Muse, Kiro, Claude, Codex) are shown for at
|
|
18
|
+
// most one TTL (60 s) from the reading: after that, in peek and read alike,
|
|
19
|
+
// they are replaced by an explicit "expired" state with no numbers, and a failed
|
|
20
|
+
// refresh never extends them. Verified-identity figures may be kept, marked
|
|
21
|
+
// stale, across a transient failure for at most `staleMaxMs`. A Claude reading
|
|
22
|
+
// is bound to the exact live session and query that produced it and is dropped
|
|
23
|
+
// when that source is replaced. Sign-in failures and account changes always drop
|
|
24
|
+
// figures. A result is used only if the caller is still current when the work
|
|
25
|
+
// starts, while it waits, and when it settles.
|
|
26
|
+
|
|
27
|
+
var capabilities = require("./vendor-usage-capabilities");
|
|
28
|
+
var metrics = require("./vendor-usage-metrics");
|
|
29
|
+
var nativeReaders = require("./vendor-usage-native-readers");
|
|
30
|
+
var httpReaders = require("./vendor-usage-http-readers");
|
|
31
|
+
var acpReaders = require("./vendor-usage-acp-readers");
|
|
32
|
+
|
|
33
|
+
var DEFAULTS = { ttlMs: 60 * 1000, minForceMs: 10 * 1000, timeoutMs: 15 * 1000, backoffBaseMs: 30 * 1000, backoffMaxMs: 10 * 60 * 1000, noAuthRetryMs: 5 * 60 * 1000, staleMaxMs: 10 * 60 * 1000 };
|
|
34
|
+
|
|
35
|
+
// Failures that mean the credential or account is not (or no longer) the one
|
|
36
|
+
// the cached figures described, so those figures must not be shown again.
|
|
37
|
+
var DROPS_FIGURES = ["no_auth", "expired_auth", "unauthorized", "untrusted_origin", "account_changed", "source_changed"];
|
|
38
|
+
|
|
39
|
+
var MESSAGES = {
|
|
40
|
+
identity_unprovable: "Clay cannot prove which account this vendor uses for you, so it will not read one.",
|
|
41
|
+
account_changed: "The saved sign-in changed while usage was being read; figures from the previous account were discarded.",
|
|
42
|
+
source_changed: "The session that reported this usage was replaced; its figures were discarded.",
|
|
43
|
+
expired: "The last reading is too old and its account cannot be verified, so it is not shown. Read again.",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function defaultFetchJson(timeoutMs) {
|
|
47
|
+
return async function (url, headers) {
|
|
48
|
+
var response = await fetch(url, { headers: headers, signal: AbortSignal.timeout(timeoutMs), redirect: "error" });
|
|
49
|
+
var body = null;
|
|
50
|
+
try { body = await response.json(); } catch (e) { body = null; }
|
|
51
|
+
return { status: response.status, body: body };
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createUsageService(options) {
|
|
56
|
+
options = options || {};
|
|
57
|
+
var now = typeof options.now === "function" ? options.now : Date.now;
|
|
58
|
+
var timing = Object.assign({}, DEFAULTS, options.timing || {});
|
|
59
|
+
var fs = require("fs");
|
|
60
|
+
var os = require("os");
|
|
61
|
+
var httpDeps = Object.assign({
|
|
62
|
+
readText: function (file) { return fs.promises.readFile(file, "utf8"); },
|
|
63
|
+
readTextSync: function (file) { return fs.readFileSync(file, "utf8"); },
|
|
64
|
+
fetchJson: defaultFetchJson(timing.timeoutMs),
|
|
65
|
+
env: process.env, homedir: os.homedir,
|
|
66
|
+
}, options.http || {});
|
|
67
|
+
httpDeps.now = now;
|
|
68
|
+
var acpDeps = Object.assign({
|
|
69
|
+
binary: function () { try { return require("./yoke/kiro-acp-server").findKiroPath(); } catch (e) { return null; } },
|
|
70
|
+
spawn: function (command, args, spawnOptions) { return require("child_process").spawn(command, args, spawnOptions); },
|
|
71
|
+
tmpdir: os.tmpdir, env: process.env,
|
|
72
|
+
}, options.acp || {});
|
|
73
|
+
var readers = Object.assign({
|
|
74
|
+
claude: nativeReaders.claude,
|
|
75
|
+
codex: nativeReaders.codex,
|
|
76
|
+
muse: nativeReaders.muse,
|
|
77
|
+
kimi: function () { return httpReaders.kimi(httpDeps); },
|
|
78
|
+
grok: function () { return httpReaders.grok(httpDeps); },
|
|
79
|
+
kiro: function (context) { return acpReaders.kiro(context, acpDeps); },
|
|
80
|
+
}, options.readers || {});
|
|
81
|
+
var identities = Object.assign({
|
|
82
|
+
kimi: function () { return httpReaders.identities.kimi(httpDeps); },
|
|
83
|
+
grok: function () { return httpReaders.identities.grok(httpDeps); },
|
|
84
|
+
}, options.identities || {});
|
|
85
|
+
var multiUser = typeof options.multiUser === "function" ? options.multiUser : function () { return false; };
|
|
86
|
+
var osUsersEnabled = typeof options.osUsersEnabled === "function" ? options.osUsersEnabled : function () { return false; };
|
|
87
|
+
var sourceLive = typeof options.sourceLive === "function" ? options.sourceLive : function () { return true; };
|
|
88
|
+
var getAdapter = typeof options.getAdapter === "function" ? options.getAdapter : function () { return null; };
|
|
89
|
+
var entries = Object.create(null);
|
|
90
|
+
var generations = Object.create(null);
|
|
91
|
+
|
|
92
|
+
function keyOf(owner, linuxUser, vendor) { return owner + "\u0000" + (linuxUser || "") + "\u0000" + vendor; }
|
|
93
|
+
|
|
94
|
+
function identityOf(vendor) {
|
|
95
|
+
try { return identities[vendor] ? identities[vendor]() : null; } catch (e) { return null; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function refusal(vendor, linuxUser, hasSession) {
|
|
99
|
+
var cap = capabilities.capabilityFor(vendor);
|
|
100
|
+
if (cap.mode !== "query") return { status: "unintegrated", failureKind: "unintegrated", message: cap.reason || "No usage source is integrated." };
|
|
101
|
+
if (cap.identity === "owner-session") return hasSession === false ? { status: "idle", failureKind: null, message: null } : null;
|
|
102
|
+
if (!multiUser()) return null;
|
|
103
|
+
if (cap.identity === "owner-os-user" && osUsersEnabled() && linuxUser) return null;
|
|
104
|
+
return { status: "refused", failureKind: "identity_unprovable", message: MESSAGES.identity_unprovable };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function blank(status, failureKind, message) {
|
|
108
|
+
return { status: status, metrics: [], fetchedAtMs: null, stale: false, failureKind: failureKind || null, message: message || null, retryAtMs: null, planType: null, accountId: null, fetching: false };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function bindingOf(session) {
|
|
112
|
+
return session ? { session: session, query: session.queryInstance } : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function bindingLive(binding) {
|
|
116
|
+
return !binding || (binding.session.queryInstance === binding.query && sourceLive(binding.session) !== false);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sameBinding(a, b) {
|
|
120
|
+
return (!a && !b) || (!!a && !!b && a.session === b.session && a.query === b.query);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Figures whose account cannot be verified live only for one TTL; verified
|
|
124
|
+
// ones may stay (stale) a bounded while. Past that the numbers are removed.
|
|
125
|
+
function expire(entry) {
|
|
126
|
+
var a = entry.account;
|
|
127
|
+
if (!a.metrics.length) return;
|
|
128
|
+
var age = a.fetchedAtMs === null ? Infinity : now() - a.fetchedAtMs;
|
|
129
|
+
var limit = entry.identity ? timing.staleMaxMs : timing.ttlMs;
|
|
130
|
+
if (age < limit) return;
|
|
131
|
+
entry.account = { status: "idle", metrics: [], fetchedAtMs: null, stale: false, failureKind: "expired", message: MESSAGES.expired, planType: null, accountId: null };
|
|
132
|
+
entry.identity = null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function snapshotOf(entry) {
|
|
136
|
+
expire(entry);
|
|
137
|
+
var a = entry.account;
|
|
138
|
+
return {
|
|
139
|
+
status: a.status, metrics: a.metrics.slice(), fetchedAtMs: a.fetchedAtMs, stale: a.stale === true,
|
|
140
|
+
failureKind: a.failureKind || null, message: a.message || null, retryAtMs: entry.retryAtMs || null,
|
|
141
|
+
planType: a.planType || null, accountId: a.accountId || null, sourceSessionId: a.sourceSessionId === undefined ? null : a.sourceSessionId,
|
|
142
|
+
fetching: !!entry.inflight,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function emptyEntry() {
|
|
147
|
+
return { account: { status: "idle", metrics: [], fetchedAtMs: null }, identity: null, binding: null, failures: 0, retryAtMs: 0, inflight: null, token: 0 };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function drop(key) {
|
|
151
|
+
var entry = entries[key];
|
|
152
|
+
if (!entry) return;
|
|
153
|
+
entry.token++;
|
|
154
|
+
delete entries[key];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// True when the cached projection no longer describes the live source: the
|
|
158
|
+
// credential fingerprint differs (rotation or deletion, even mid-read) or the
|
|
159
|
+
// session/query that produced it was replaced.
|
|
160
|
+
function sourceChanged(entry, vendor) {
|
|
161
|
+
if (entry.identity && identityOf(vendor) !== entry.identity) return true;
|
|
162
|
+
return !bindingLive(entry.binding);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// The current known state, without starting a read.
|
|
166
|
+
function peek(owner, linuxUser, vendor) {
|
|
167
|
+
var refused = refusal(vendor, linuxUser, null);
|
|
168
|
+
if (refused && refused.status !== "idle") return blank(refused.status, refused.failureKind, refused.message);
|
|
169
|
+
var key = keyOf(owner, linuxUser, vendor);
|
|
170
|
+
var entry = entries[key];
|
|
171
|
+
if (!entry) return blank("idle");
|
|
172
|
+
if (sourceChanged(entry, vendor)) { drop(key); return blank("idle"); }
|
|
173
|
+
return snapshotOf(entry);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function failureDelay(entry, kind) {
|
|
177
|
+
if (kind === "no_auth") return timing.noAuthRetryMs;
|
|
178
|
+
return Math.min(timing.backoffMaxMs, timing.backoffBaseMs * Math.pow(2, Math.max(0, entry.failures - 1)));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function settleFailure(entry, error) {
|
|
182
|
+
var kind = error && error.failureKind ? error.failureKind : "error";
|
|
183
|
+
entry.failures += 1;
|
|
184
|
+
entry.retryAtMs = now() + failureDelay(entry, kind);
|
|
185
|
+
var prior = entry.account;
|
|
186
|
+
// Only a transient failure keeps the last good figures (marked stale);
|
|
187
|
+
// a sign-in problem or account change must not leave a former account's numbers.
|
|
188
|
+
// Figures survive only when their account is verifiable (a fingerprint) and
|
|
189
|
+
// they are still inside the bounded stale window.
|
|
190
|
+
var keep = DROPS_FIGURES.indexOf(kind) === -1 && !!entry.identity && prior.metrics.length > 0 && (prior.status === "ok" || prior.stale === true) && prior.fetchedAtMs !== null && now() - prior.fetchedAtMs < timing.staleMaxMs;
|
|
191
|
+
entry.account = {
|
|
192
|
+
status: kind === "no_auth" ? "no_auth" : kind === "expired_auth" ? "expired_auth" : "error",
|
|
193
|
+
metrics: keep ? prior.metrics : [], fetchedAtMs: keep ? prior.fetchedAtMs : null, stale: keep,
|
|
194
|
+
failureKind: kind, message: error && error.failureKind ? error.message : "Usage could not be read.",
|
|
195
|
+
planType: keep ? prior.planType || null : null, accountId: keep ? prior.accountId || null : null,
|
|
196
|
+
sourceSessionId: keep ? prior.sourceSessionId : null,
|
|
197
|
+
};
|
|
198
|
+
if (!keep) { entry.identity = null; entry.binding = null; }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function withTimeout(promise, controller) {
|
|
202
|
+
var timer;
|
|
203
|
+
var limit = new Promise(function (resolve, reject) {
|
|
204
|
+
timer = setTimeout(function () {
|
|
205
|
+
controller.abort();
|
|
206
|
+
reject(nativeReaders.failure("timeout", "The usage read timed out."));
|
|
207
|
+
}, timing.timeoutMs);
|
|
208
|
+
});
|
|
209
|
+
return Promise.race([promise, limit]).then(function (v) { clearTimeout(timer); return v; }, function (e) { clearTimeout(timer); throw e; });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Resolves to a snapshot, or null when the result must not be used because
|
|
213
|
+
// the caller or the account changed. Nothing is started for a caller that is
|
|
214
|
+
// already stale.
|
|
215
|
+
async function read(request) {
|
|
216
|
+
var owner = request.owner, linuxUser = request.linuxUser || null, vendor = request.vendor;
|
|
217
|
+
var isCurrent = typeof request.isCurrent === "function" ? request.isCurrent : function () { return true; };
|
|
218
|
+
if (!isCurrent()) return null;
|
|
219
|
+
var refused = refusal(vendor, linuxUser, !!request.session);
|
|
220
|
+
if (refused) return refused.status === "idle" ? peek(owner, linuxUser, vendor) : blank(refused.status, refused.failureKind, refused.message);
|
|
221
|
+
var key = keyOf(owner, linuxUser, vendor);
|
|
222
|
+
var entry = entries[key] || (entries[key] = emptyEntry());
|
|
223
|
+
var startedGeneration = generations[owner] || 0;
|
|
224
|
+
var before = identityOf(vendor);
|
|
225
|
+
var binding = bindingOf(request.session);
|
|
226
|
+
if ((entry.identity && before !== entry.identity) || !bindingLive(entry.binding) || (entry.binding && !sameBinding(entry.binding, binding))) {
|
|
227
|
+
drop(key);
|
|
228
|
+
entry = entries[key] = emptyEntry();
|
|
229
|
+
}
|
|
230
|
+
expire(entry);
|
|
231
|
+
var current = now();
|
|
232
|
+
var age = entry.account.fetchedAtMs === null ? Infinity : current - entry.account.fetchedAtMs;
|
|
233
|
+
var coolingDown = entry.retryAtMs > current;
|
|
234
|
+
var fresh = entry.account.status === "ok" && age < (request.force ? timing.minForceMs : timing.ttlMs);
|
|
235
|
+
if (!entry.inflight && (fresh || coolingDown)) return isCurrent() ? snapshotOf(entry) : null;
|
|
236
|
+
if (!entry.inflight) {
|
|
237
|
+
var token = ++entry.token;
|
|
238
|
+
var controller = new AbortController();
|
|
239
|
+
var context = { adapter: getAdapter(vendor), linuxUser: linuxUser, session: request.session || null, signal: controller.signal };
|
|
240
|
+
entry.binding = binding;
|
|
241
|
+
var run = Promise.resolve().then(function () { return withTimeout(readers[vendor](context), controller); });
|
|
242
|
+
entry.inflight = run.then(function (result) {
|
|
243
|
+
if (entry.token !== token || (generations[owner] || 0) !== startedGeneration) return;
|
|
244
|
+
var list = (result && Array.isArray(result.metrics) ? result.metrics : []).filter(metrics.valid);
|
|
245
|
+
if (list.length === 0) throw nativeReaders.failure("unavailable", "The vendor reported no usage figures for this account.");
|
|
246
|
+
if (!bindingLive(binding)) throw nativeReaders.failure("source_changed", MESSAGES.source_changed);
|
|
247
|
+
// The credential is re-read AFTER the response. For a fingerprinted
|
|
248
|
+
// source it must equal the one the reader used (or, failing that, the
|
|
249
|
+
// one seen before the call); rotation, deletion or first appearance
|
|
250
|
+
// mid-read means neither account's figures can be attributed.
|
|
251
|
+
var expected = result.identity || before;
|
|
252
|
+
if (identities[vendor]) {
|
|
253
|
+
var fresh = identityOf(vendor);
|
|
254
|
+
if (fresh !== expected) throw nativeReaders.failure("account_changed", MESSAGES.account_changed);
|
|
255
|
+
}
|
|
256
|
+
entry.failures = 0; entry.retryAtMs = 0;
|
|
257
|
+
entry.identity = identities[vendor] ? expected || null : null;
|
|
258
|
+
entry.account = { status: "ok", metrics: list, fetchedAtMs: Number.isFinite(result.observedAtMs) ? result.observedAtMs : now(), stale: false, planType: result.planType || null, accountId: result.accountId || null, sourceSessionId: result.sourceSessionId === undefined ? null : result.sourceSessionId };
|
|
259
|
+
}).catch(function (error) {
|
|
260
|
+
if (entry.token !== token || (generations[owner] || 0) !== startedGeneration) return;
|
|
261
|
+
settleFailure(entry, error);
|
|
262
|
+
}).then(function () { if (entry.token === token) entry.inflight = null; });
|
|
263
|
+
}
|
|
264
|
+
await entry.inflight;
|
|
265
|
+
if (!isCurrent() || (generations[owner] || 0) !== startedGeneration || entries[key] !== entry) return null;
|
|
266
|
+
return snapshotOf(entry);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Forget everything for an owner (account switch, logout, access change);
|
|
270
|
+
// reads still in flight can no longer store or return a result.
|
|
271
|
+
function invalidate(owner) {
|
|
272
|
+
generations[owner] = (generations[owner] || 0) + 1;
|
|
273
|
+
Object.keys(entries).forEach(function (key) {
|
|
274
|
+
if (key.indexOf(owner + "\u0000") === 0) drop(key);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return { read: read, peek: peek, invalidate: invalidate, refusal: refusal };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = { createUsageService: createUsageService, DEFAULTS: DEFAULTS };
|
package/lib/ws-schema.js
CHANGED
|
@@ -37,6 +37,8 @@ var schema = {
|
|
|
37
37
|
"session_folders_state": { direction: "s2c", handler: "lib/public/modules/session-folders.js", description: "Authoritative per-user, per-project session folder state; sent on connect and after every change to all of that user's sockets" },
|
|
38
38
|
"new_session_options_get": { direction: "c2s", handler: "lib/project-session-creation.js", description: "Ask for the installed providers, the project's default provider and whether the caller may set it" },
|
|
39
39
|
"new_session_default_set": { direction: "c2s", handler: "lib/project-session-creation.js", description: "Save the project-scoped default provider the inline New session row preselects" },
|
|
40
|
+
"vendor_usage_get": { direction: "c2s", handler: "lib/vendor-usage-overview.js", description: "Ask for the caller's observed per-vendor usage overview for this project" },
|
|
41
|
+
"vendor_usage_overview": { direction: "s2c", handler: "lib/public/modules/vendor-usage-overview.js", description: "Owner-scoped observed vendor usage windows, reset times and observation times; unknown vendors carry no windows" },
|
|
40
42
|
"new_session_options": { direction: "s2c", handler: "lib/public/modules/session-create.js", description: "Installed providers, project default provider and default-setting permission for the inline New session row" },
|
|
41
43
|
"new_session_default_result": { direction: "s2c", handler: "lib/public/modules/session-create.js", description: "Outcome of saving the project New session default" },
|
|
42
44
|
"new_session_project_default": { direction: "s2c", handler: "lib/public/modules/session-create.js", description: "The project New session default changed" },
|
|
@@ -404,9 +406,9 @@ var schema = {
|
|
|
404
406
|
// -----------------------------------------------------------------------
|
|
405
407
|
// Projects
|
|
406
408
|
// -----------------------------------------------------------------------
|
|
407
|
-
"browse_dir": { direction: "c2s", handler: "lib/project-sessions.js", description: "Browse directory entries for project creation" },
|
|
409
|
+
"browse_dir": { direction: "c2s", handler: "lib/project-sessions.js", description: "Browse directory entries for project creation (requestId echoed; reply adds canonical dir, prefix, permitted parent)" },
|
|
408
410
|
"browse_dir_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Directory listing result" },
|
|
409
|
-
"add_project": { direction: "c2s", handler: "lib/project-sessions.js", description: "Add an existing directory as a project" },
|
|
411
|
+
"add_project": { direction: "c2s", handler: "lib/project-sessions.js", description: "Add an existing directory as a project (optional requestId echoed on add_project_result, also for create/clone)" },
|
|
410
412
|
"add_project_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Add project result" },
|
|
411
413
|
"create_project": { direction: "c2s", handler: "lib/project-sessions.js", description: "Create a new empty project directory" },
|
|
412
414
|
"clone_project": { direction: "c2s", handler: "lib/project-sessions.js", description: "Clone a git repository as a new project" },
|
|
@@ -704,6 +704,15 @@ function createQueryHandle(rawQuery, messageQueue, abortController) {
|
|
|
704
704
|
return Promise.resolve(null);
|
|
705
705
|
},
|
|
706
706
|
|
|
707
|
+
// Account plan usage through the SDK's usage call, which the SDK marks
|
|
708
|
+
// experimental: guarded by capability, resolves null when absent, and the
|
|
709
|
+
// caller validates the shape. Transcript scanning is skipped.
|
|
710
|
+
getAccountUsage: function() {
|
|
711
|
+
var method = rawQuery && rawQuery.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET;
|
|
712
|
+
if (typeof method === "function") return Promise.resolve(method.call(rawQuery, { skipBehaviors: true }));
|
|
713
|
+
return Promise.resolve(null);
|
|
714
|
+
},
|
|
715
|
+
|
|
707
716
|
supportedModels: function() {
|
|
708
717
|
if (rawQuery && typeof rawQuery.supportedModels === "function") {
|
|
709
718
|
return rawQuery.supportedModels();
|
|
@@ -1482,6 +1491,9 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
|
|
|
1482
1491
|
return iterEnded ? Promise.resolve(null) : contextRequests.request(options);
|
|
1483
1492
|
},
|
|
1484
1493
|
|
|
1494
|
+
// The OS-isolated worker protocol has no account usage request.
|
|
1495
|
+
getAccountUsage: function() { return Promise.resolve(null); },
|
|
1496
|
+
|
|
1485
1497
|
supportedModels: function() {
|
|
1486
1498
|
return Promise.resolve([]);
|
|
1487
1499
|
},
|
|
@@ -17,6 +17,7 @@ var createTurnStop = require("../codex-turn-stop").createTurnStop;
|
|
|
17
17
|
var idleRecovery = require("../codex-idle-recovery");
|
|
18
18
|
var modelCatalog = require("../codex-model-catalog");
|
|
19
19
|
var normalizeQuotaSnapshot = require("../codex-rate-limits").normalizeSnapshot;
|
|
20
|
+
var dynamicToolNames = require("../codex-dynamic-tool-names");
|
|
20
21
|
|
|
21
22
|
// --- Reasoning defaults ---
|
|
22
23
|
// Reasoning summaries are the only readable thinking channel Codex offers:
|
|
@@ -33,7 +34,7 @@ var normalizeQuotaSnapshot = require("../codex-rate-limits").normalizeSnapshot;
|
|
|
33
34
|
var CODEX_REASONING_DEFAULTS = {
|
|
34
35
|
model_reasoning_summary: "detailed",
|
|
35
36
|
};
|
|
36
|
-
var DEFAULT_CODEX_MODEL =
|
|
37
|
+
var DEFAULT_CODEX_MODEL = modelCatalog.DEFAULT_MODEL;
|
|
37
38
|
var CODEX_QUERY_BOUND_TOOL_CAPABILITY = { mode: "dynamic", resume: "session-mcp-recovery" };
|
|
38
39
|
var CODEX_NOTE_TOOLS = {
|
|
39
40
|
list_notes: true,
|
|
@@ -622,6 +623,8 @@ function flattenEvent(notification, state) {
|
|
|
622
623
|
// Dynamic tools are supplied by Clay for a specific thread, such as the
|
|
623
624
|
// Driver's pair-session controls.
|
|
624
625
|
if (item.type === "dynamicToolCall" || item.type === "dynamic_tool_call") {
|
|
626
|
+
// Activity shows the original Clay name, not the Codex wire alias.
|
|
627
|
+
var dynamicDisplayName = state.dynamicToolCatalog ? state.dynamicToolCatalog.displayName(item.tool) : item.tool;
|
|
625
628
|
if (!state.toolBlocks[item.id]) {
|
|
626
629
|
state.blockCounter++;
|
|
627
630
|
state.toolBlocks[item.id] = "blk_" + state.blockCounter;
|
|
@@ -630,13 +633,13 @@ function flattenEvent(notification, state) {
|
|
|
630
633
|
yokeType: "tool_start",
|
|
631
634
|
blockId: dynamicBlockId,
|
|
632
635
|
toolId: item.id,
|
|
633
|
-
toolName:
|
|
636
|
+
toolName: dynamicDisplayName || "session_tool",
|
|
634
637
|
});
|
|
635
638
|
events.push({
|
|
636
639
|
yokeType: "tool_executing",
|
|
637
640
|
blockId: dynamicBlockId,
|
|
638
641
|
toolId: item.id,
|
|
639
|
-
toolName:
|
|
642
|
+
toolName: dynamicDisplayName || "session_tool",
|
|
640
643
|
input: item.arguments || {},
|
|
641
644
|
});
|
|
642
645
|
}
|
|
@@ -968,6 +971,15 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
968
971
|
// server request and waits for the client to execute and answer it.
|
|
969
972
|
if (method === "item/tool/call") {
|
|
970
973
|
var dynamicParams = msg.params || {};
|
|
974
|
+
var resolvedDynamicTool = (state.dynamicToolCatalog || dynamicToolNames.createCatalog([])).resolve(dynamicParams.tool, dynamicParams.namespace);
|
|
975
|
+
if (resolvedDynamicTool.error) {
|
|
976
|
+
appServer.respond(msg.id, {
|
|
977
|
+
contentItems: [{ type: "inputText", text: "Error: " + resolvedDynamicTool.error }],
|
|
978
|
+
success: false,
|
|
979
|
+
});
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
var dynamicToolName = resolvedDynamicTool.name;
|
|
971
983
|
if (!callDynamicTool) {
|
|
972
984
|
appServer.respond(msg.id, {
|
|
973
985
|
contentItems: [{ type: "inputText", text: "Session tool handler is unavailable" }],
|
|
@@ -976,7 +988,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
976
988
|
return;
|
|
977
989
|
}
|
|
978
990
|
function executeDynamicTool() {
|
|
979
|
-
return Promise.resolve(callDynamicTool(
|
|
991
|
+
return Promise.resolve(callDynamicTool(dynamicToolName, dynamicParams.arguments || {})).then(function (result) {
|
|
980
992
|
var content = result && Array.isArray(result.content) ? result.content : [];
|
|
981
993
|
var contentItems = content.map(function (item) {
|
|
982
994
|
return { type: "inputText", text: item && item.text ? item.text : JSON.stringify(item) };
|
|
@@ -987,7 +999,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
987
999
|
appServer.respond(msg.id, { contentItems: contentItems, success: !(result && result.isError) });
|
|
988
1000
|
});
|
|
989
1001
|
}
|
|
990
|
-
var permissionToolName = canonicalDynamicPermissionToolName(
|
|
1002
|
+
var permissionToolName = canonicalDynamicPermissionToolName(dynamicToolName);
|
|
991
1003
|
var permission = canUseTool
|
|
992
1004
|
? Promise.resolve(canUseTool(permissionToolName, dynamicParams.arguments || {}, {
|
|
993
1005
|
toolUseID: dynamicParams.callId || String(msg.id),
|
|
@@ -1158,13 +1170,15 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1158
1170
|
if (queryOpts.webSearchMode) {
|
|
1159
1171
|
threadParams.webSearchMode = queryOpts.webSearchMode;
|
|
1160
1172
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1173
|
+
// Built on resume too: the persisted catalog uses the same aliases.
|
|
1174
|
+
state.dynamicToolCatalog = dynamicToolNames.createCatalog(Array.isArray(queryOpts.dynamicTools) ? queryOpts.dynamicTools : []);
|
|
1175
|
+
if (state.dynamicToolCatalog.advertised.length > 0) {
|
|
1176
|
+
threadParams.dynamicTools = state.dynamicToolCatalog.advertised.map(function (entry) {
|
|
1163
1177
|
return {
|
|
1164
1178
|
type: "function",
|
|
1165
|
-
name:
|
|
1166
|
-
description:
|
|
1167
|
-
inputSchema: dynamicInputSchema(
|
|
1179
|
+
name: entry.name,
|
|
1180
|
+
description: entry.def.description || entry.def.name,
|
|
1181
|
+
inputSchema: dynamicInputSchema(entry.def.inputSchema),
|
|
1168
1182
|
};
|
|
1169
1183
|
});
|
|
1170
1184
|
}
|
|
@@ -1558,7 +1572,7 @@ function createCodexAdapter(opts) {
|
|
|
1558
1572
|
function buildReadyResponse(skillNames) {
|
|
1559
1573
|
return {
|
|
1560
1574
|
models: _cachedModels,
|
|
1561
|
-
defaultModel:
|
|
1575
|
+
defaultModel: modelCatalog.defaultModel(_cachedModels),
|
|
1562
1576
|
skills: skillNames || [],
|
|
1563
1577
|
slashCommands: skillNames || [],
|
|
1564
1578
|
fastModeState: null,
|
|
@@ -1943,7 +1957,7 @@ function createCodexAdapter(opts) {
|
|
|
1943
1957
|
throw new Error("[yoke/codex] Adapter not initialized. Call init() first.");
|
|
1944
1958
|
}
|
|
1945
1959
|
|
|
1946
|
-
var model = queryOpts.model ||
|
|
1960
|
+
var model = queryOpts.model || modelCatalog.defaultModel(_cachedModels);
|
|
1947
1961
|
var ac = queryOpts.abortController || new AbortController();
|
|
1948
1962
|
var activeEntry = {
|
|
1949
1963
|
abort: function() {
|
|
@@ -2036,6 +2050,25 @@ function createCodexAdapter(opts) {
|
|
|
2036
2050
|
return { handle: handle, backendPersistence: "ephemeral" };
|
|
2037
2051
|
},
|
|
2038
2052
|
|
|
2053
|
+
// Reads the account's own quota through the already-running app-server;
|
|
2054
|
+
// it never starts a runtime and never starts a turn. Routed to the mapped
|
|
2055
|
+
// OS user's runtime exactly like the other per-owner operations.
|
|
2056
|
+
readAccountRateLimits: async function(opts) {
|
|
2057
|
+
var requestedLinuxUser = opts && opts.linuxUser;
|
|
2058
|
+
if (requestedLinuxUser) {
|
|
2059
|
+
// Throws on a runtime bound to a different OS user; never answers from another identity.
|
|
2060
|
+
var userRuntime = getUserRuntime(requestedLinuxUser);
|
|
2061
|
+
if (userRuntime !== adapter) return userRuntime.readAccountRateLimits(withoutLinuxUser(opts));
|
|
2062
|
+
}
|
|
2063
|
+
if (_requiresLinuxUser) throw new Error("Codex requires a mapped Linux user while OS-user isolation is enabled");
|
|
2064
|
+
if (!_appServer || !_appServer.started) {
|
|
2065
|
+
var idle = new Error("Codex has not started in this project yet; open a Codex session to read account usage.");
|
|
2066
|
+
idle.failureKind = "runtime_idle";
|
|
2067
|
+
throw idle;
|
|
2068
|
+
}
|
|
2069
|
+
return _appServer.send("account/rateLimits/read", { excludeResetCreditDetails: true }, 15000);
|
|
2070
|
+
},
|
|
2071
|
+
|
|
2039
2072
|
// --- Title generation ---
|
|
2040
2073
|
generateTitle: async function(messages, opts) {
|
|
2041
2074
|
var requestedLinuxUser = opts && opts.linuxUser;
|
|
@@ -67,6 +67,7 @@ var CLAY_MANAGED_ALLOW = [
|
|
|
67
67
|
"mcp__clay-workspace__read_workspace_receipt",
|
|
68
68
|
"mcp__clay-workspace__inspect_workspace_change",
|
|
69
69
|
"mcp__clay-workspace__control_project_workspace",
|
|
70
|
+
"mcp__clay-workspace__read_vendor_usage",
|
|
70
71
|
"mcp__clay-workspace__read_project_records",
|
|
71
72
|
"mcp__clay-workspace__inspect_project_record_change",
|
|
72
73
|
"mcp__clay-workspace__control_project_records",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Reversible Codex transport names for Clay dynamic tools.
|
|
2
|
+
//
|
|
3
|
+
// Codex app-server (rust-v0.162.1 validate_dynamic_tools) rejects a
|
|
4
|
+
// thread/start whose dynamic tool names do not match ^[a-zA-Z0-9_-]+$, exceed
|
|
5
|
+
// 128 characters, repeat, or are "mcp" / start with "mcp__". Clay keeps its
|
|
6
|
+
// original names (for example mcp__server__tool) for dispatch, permissions and
|
|
7
|
+
// activity, and only the Codex wire uses a deterministic alias. Determinism
|
|
8
|
+
// lets a resumed thread's persisted catalog resolve against a rebuilt catalog.
|
|
9
|
+
|
|
10
|
+
var crypto = require("crypto");
|
|
11
|
+
|
|
12
|
+
var MAX_NAME_LENGTH = 128;
|
|
13
|
+
var ALIAS_PREFIX = "clay__";
|
|
14
|
+
var HASH_LENGTH = 16;
|
|
15
|
+
var IDENTIFIER = /^[A-Za-z0-9_-]+$/;
|
|
16
|
+
|
|
17
|
+
function isReserved(name) {
|
|
18
|
+
return name === "mcp" || name.indexOf("mcp__") === 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// True when Codex accepts the name as-is and it cannot be mistaken for an alias.
|
|
22
|
+
function isPlainName(name) {
|
|
23
|
+
return typeof name === "string" && name.length > 0 && name.length <= MAX_NAME_LENGTH
|
|
24
|
+
&& IDENTIFIER.test(name) && !isReserved(name) && name.indexOf(ALIAS_PREFIX) !== 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function transportName(name) {
|
|
28
|
+
if (typeof name !== "string" || !name) throw new Error("Dynamic tool names must be non-empty strings.");
|
|
29
|
+
if (isPlainName(name)) return name;
|
|
30
|
+
var hash = crypto.createHash("sha256").update(name, "utf8").digest("hex").slice(0, HASH_LENGTH);
|
|
31
|
+
var readable = name.replace(/^mcp__/, "").replace(/[^A-Za-z0-9_-]/g, "_");
|
|
32
|
+
var room = MAX_NAME_LENGTH - ALIAS_PREFIX.length - 2 - HASH_LENGTH;
|
|
33
|
+
return ALIAS_PREFIX + readable.slice(0, room) + "__" + hash;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Build the exact alias map for one query's catalog. Repeated identical
|
|
37
|
+
// originals resolve to the same handler, so they are advertised once. Two
|
|
38
|
+
// different originals sharing an alias fail closed instead of guessing.
|
|
39
|
+
function createCatalog(defs) {
|
|
40
|
+
var byTransport = new Map();
|
|
41
|
+
var byOriginal = new Map();
|
|
42
|
+
var advertised = [];
|
|
43
|
+
(defs || []).forEach(function (def) {
|
|
44
|
+
var original = def && def.name;
|
|
45
|
+
var alias = transportName(original);
|
|
46
|
+
if (byOriginal.has(original)) return;
|
|
47
|
+
if (byTransport.has(alias)) {
|
|
48
|
+
throw new Error("Codex dynamic tool names collide: " + byTransport.get(alias) + " and " + original);
|
|
49
|
+
}
|
|
50
|
+
byTransport.set(alias, original);
|
|
51
|
+
byOriginal.set(original, alias);
|
|
52
|
+
advertised.push({ def: def, name: alias });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Map a Codex call back to the original Clay name. Plain names keep the
|
|
56
|
+
// existing live-catalog lookup; aliases must belong to this catalog.
|
|
57
|
+
function resolve(tool, namespace) {
|
|
58
|
+
if (namespace !== undefined && namespace !== null && namespace !== "") {
|
|
59
|
+
return { error: "Clay does not register namespaced dynamic tools: " + namespace };
|
|
60
|
+
}
|
|
61
|
+
if (typeof tool !== "string" || !tool) return { error: "Dynamic tool call has no tool name." };
|
|
62
|
+
if (byTransport.has(tool)) return { name: byTransport.get(tool) };
|
|
63
|
+
if (isPlainName(tool)) return { name: tool };
|
|
64
|
+
return { error: "Session tool is not in this conversation's current catalog: " + tool };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function displayName(tool) {
|
|
68
|
+
return byTransport.has(tool) ? byTransport.get(tool) : tool;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { advertised: advertised, resolve: resolve, displayName: displayName };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
MAX_NAME_LENGTH: MAX_NAME_LENGTH,
|
|
76
|
+
ALIAS_PREFIX: ALIAS_PREFIX,
|
|
77
|
+
isPlainName: isPlainName,
|
|
78
|
+
transportName: transportName,
|
|
79
|
+
createCatalog: createCatalog,
|
|
80
|
+
};
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
// Runtime catalogs are scoped to the adapter's OS user and provider configuration.
|
|
2
|
-
|
|
2
|
+
// The runtime model/list is authoritative. This offline fallback mirrors the
|
|
3
|
+
// picker-visible presets bundled with @openai/codex 0.162.1, in their priority
|
|
4
|
+
// order, minus models the official Codex models page lists as retired for
|
|
5
|
+
// ChatGPT sign-in or deprecated (gpt-5.5, gpt-5.2). Verified 2026-10-17:
|
|
6
|
+
// https://learn.chatgpt.com/docs/models
|
|
7
|
+
var FALLBACK = ["gpt-6.1-sol", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"];
|
|
8
|
+
// Codex marks its highest-priority picker-visible preset as default; for the
|
|
9
|
+
// bundled 0.162.1 presets that is gpt-6.1-sol. Used only without a runtime flag.
|
|
10
|
+
var DEFAULT_MODEL = "gpt-6.1-sol";
|
|
11
|
+
// Fast, low-cost models for title generation, newest first (gpt-5.6-luna's
|
|
12
|
+
// bundled upgrade target is gpt-6-luna).
|
|
13
|
+
var TITLE_MODELS = ["gpt-6-luna", "gpt-5.6-luna"];
|
|
3
14
|
|
|
4
15
|
function fallback() {
|
|
5
16
|
return FALLBACK.slice();
|
|
@@ -21,6 +32,8 @@ async function load(server) {
|
|
|
21
32
|
if (!entry || entry.hidden || typeof entry.model !== "string" || !entry.model || seen.has(entry.model)) return;
|
|
22
33
|
seen.add(entry.model);
|
|
23
34
|
var model = { value: entry.model, displayName: entry.displayName || entry.model, description: entry.description || "" };
|
|
35
|
+
if (typeof entry.defaultReasoningEffort === "string" && entry.defaultReasoningEffort) model.defaultReasoningEffort = entry.defaultReasoningEffort;
|
|
36
|
+
if (entry.isDefault === true) model.isDefault = true;
|
|
24
37
|
if (Array.isArray(entry.supportedReasoningEfforts)) {
|
|
25
38
|
model.supportedEffortLevels = entry.supportedReasoningEfforts.map(function (option) { return option.reasoningEffort; }).filter(function (effort) { return typeof effort === "string" && effort.length > 0; });
|
|
26
39
|
}
|
|
@@ -37,11 +50,32 @@ async function load(server) {
|
|
|
37
50
|
return fallback();
|
|
38
51
|
}
|
|
39
52
|
|
|
53
|
+
// The runtime's own default wins. Without a flag, never name a model the
|
|
54
|
+
// catalog does not offer: use the static default only when it is listed,
|
|
55
|
+
// otherwise the first valid row. An empty catalog keeps the static default.
|
|
56
|
+
function defaultModel(models) {
|
|
57
|
+
var values = [];
|
|
58
|
+
var flagged = null;
|
|
59
|
+
(models || []).forEach(function (entry) {
|
|
60
|
+
var value = typeof entry === "string" ? entry : entry && typeof entry === "object" ? entry.value : null;
|
|
61
|
+
if (typeof value !== "string" || !value.trim()) return;
|
|
62
|
+
values.push(value);
|
|
63
|
+
if (!flagged && typeof entry === "object" && entry.isDefault === true) flagged = value;
|
|
64
|
+
});
|
|
65
|
+
if (flagged) return flagged;
|
|
66
|
+
if (values.indexOf(DEFAULT_MODEL) !== -1 || values.length === 0) return DEFAULT_MODEL;
|
|
67
|
+
return values[0];
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
function selectTitleModel(models, sessionModel) {
|
|
41
71
|
var entries = models.map(function (entry) {
|
|
42
72
|
return typeof entry === "string" ? { value: entry } : entry;
|
|
43
73
|
}).filter(function (entry) { return entry && typeof entry.value === "string" && entry.value.trim(); });
|
|
44
|
-
var selected =
|
|
74
|
+
var selected = null;
|
|
75
|
+
for (var ti = 0; ti < TITLE_MODELS.length && !selected; ti++) {
|
|
76
|
+
selected = entries.find(function (entry) { return entry.value === TITLE_MODELS[ti]; }) || null;
|
|
77
|
+
}
|
|
78
|
+
selected = selected
|
|
45
79
|
|| entries.find(function (entry) { return entry.value === sessionModel; }) || entries[0];
|
|
46
80
|
if (!selected) throw new Error("No supported Codex title model is available");
|
|
47
81
|
var efforts = selected.supportedEffortLevels;
|
|
@@ -49,4 +83,4 @@ function selectTitleModel(models, sessionModel) {
|
|
|
49
83
|
return { model: selected.value, effort: effort };
|
|
50
84
|
}
|
|
51
85
|
|
|
52
|
-
module.exports = { fallback: fallback, load: load, selectTitleModel: selectTitleModel };
|
|
86
|
+
module.exports = { DEFAULT_MODEL: DEFAULT_MODEL, fallback: fallback, load: load, defaultModel: defaultModel, selectTitleModel: selectTitleModel };
|