copilot-hub 0.1.19 → 0.1.21

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.
Files changed (53) hide show
  1. package/README.md +3 -2
  2. package/apps/agent-engine/dist/config.js +58 -0
  3. package/apps/agent-engine/dist/index.js +90 -16
  4. package/apps/control-plane/dist/channels/codex-quota-cache.js +16 -0
  5. package/apps/control-plane/dist/channels/hub-model-utils.js +244 -24
  6. package/apps/control-plane/dist/channels/hub-ops-commands.js +631 -279
  7. package/apps/control-plane/dist/channels/telegram-channel.js +5 -7
  8. package/apps/control-plane/dist/config.js +58 -0
  9. package/apps/control-plane/dist/index.js +16 -0
  10. package/apps/control-plane/dist/test/hub-model-utils.test.js +110 -13
  11. package/package.json +3 -2
  12. package/packages/core/dist/agent-supervisor.d.ts +5 -0
  13. package/packages/core/dist/agent-supervisor.js +11 -0
  14. package/packages/core/dist/agent-supervisor.js.map +1 -1
  15. package/packages/core/dist/bot-manager.js +17 -1
  16. package/packages/core/dist/bot-manager.js.map +1 -1
  17. package/packages/core/dist/bot-runtime.d.ts +4 -0
  18. package/packages/core/dist/bot-runtime.js +5 -1
  19. package/packages/core/dist/bot-runtime.js.map +1 -1
  20. package/packages/core/dist/codex-app-client.d.ts +13 -2
  21. package/packages/core/dist/codex-app-client.js +51 -13
  22. package/packages/core/dist/codex-app-client.js.map +1 -1
  23. package/packages/core/dist/codex-app-utils.d.ts +6 -0
  24. package/packages/core/dist/codex-app-utils.js +49 -0
  25. package/packages/core/dist/codex-app-utils.js.map +1 -1
  26. package/packages/core/dist/codex-provider.d.ts +3 -1
  27. package/packages/core/dist/codex-provider.js +3 -1
  28. package/packages/core/dist/codex-provider.js.map +1 -1
  29. package/packages/core/dist/kernel-control-plane.d.ts +1 -0
  30. package/packages/core/dist/kernel-control-plane.js +132 -13
  31. package/packages/core/dist/kernel-control-plane.js.map +1 -1
  32. package/packages/core/dist/provider-factory.d.ts +2 -0
  33. package/packages/core/dist/provider-factory.js +3 -0
  34. package/packages/core/dist/provider-factory.js.map +1 -1
  35. package/packages/core/dist/provider-options.js +24 -17
  36. package/packages/core/dist/provider-options.js.map +1 -1
  37. package/packages/core/dist/state-store.d.ts +1 -0
  38. package/packages/core/dist/state-store.js +28 -2
  39. package/packages/core/dist/state-store.js.map +1 -1
  40. package/packages/core/dist/telegram-channel.d.ts +1 -0
  41. package/packages/core/dist/telegram-channel.js +3 -0
  42. package/packages/core/dist/telegram-channel.js.map +1 -1
  43. package/scripts/dist/cli.mjs +132 -203
  44. package/scripts/dist/codex-runtime.mjs +352 -0
  45. package/scripts/dist/codex-version.mjs +91 -0
  46. package/scripts/dist/configure.mjs +26 -49
  47. package/scripts/dist/daemon.mjs +58 -0
  48. package/scripts/src/cli.mts +166 -233
  49. package/scripts/src/codex-runtime.mts +499 -0
  50. package/scripts/src/codex-version.mts +114 -0
  51. package/scripts/src/configure.mts +30 -65
  52. package/scripts/src/daemon.mts +69 -0
  53. package/scripts/test/codex-version.test.mjs +21 -0
package/README.md CHANGED
@@ -82,6 +82,7 @@ copilot-hub start
82
82
 
83
83
  `start` runs guided setup automatically if required values are missing.
84
84
  On interactive terminals, `start` can also offer OS-native service installation when it is not yet configured.
85
+ `start` and `update` also verify the supported Codex CLI range and can install the validated version automatically when needed.
85
86
 
86
87
  ## Quick start from source
87
88
 
@@ -165,9 +166,9 @@ Default values are already applied, and actions start from that agent workspace
165
166
  ## Startup troubleshooting
166
167
 
167
168
  - If `npm run start` fails, first read the error and follow the suggested action.
168
- - `npm run start` now auto-detects Codex from VS Code (Windows) and can install Codex CLI automatically if missing.
169
+ - `npm run start` now checks that Codex CLI is inside the supported range and can install the validated version automatically if missing or outside that range.
169
170
  - For Codex login issues, run `codex login` (or the configured `CODEX_BIN`) and retry `npm run start`.
170
- - If auto-install is skipped or unavailable, install Codex CLI with `npm install -g @openai/codex` or set `CODEX_BIN` in `.env`.
171
+ - If auto-install is skipped or unavailable, install Codex CLI with `npm install -g @openai/codex@0.113.0` or set `CODEX_BIN` in `.env` to a binary in the supported `0.113.x` range.
171
172
  - If you are still stuck, ask your favorite LLM with the exact error output.
172
173
 
173
174
  ## Commands
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
3
4
  import dotenv from "dotenv";
4
5
  import { createWorkspaceBoundaryPolicy, assertWorkspaceAllowed, parseWorkspaceAllowedRoots, } from "@copilot-hub/core/workspace-policy";
5
6
  import { parseTurnActivityTimeoutSetting } from "@copilot-hub/core/codex-app-utils";
@@ -124,6 +125,10 @@ function resolveCodexBin(rawValue) {
124
125
  return value;
125
126
  }
126
127
  if (process.platform === "win32") {
128
+ const npmGlobalCodex = findWindowsNpmGlobalCodexBin();
129
+ if (npmGlobalCodex) {
130
+ return npmGlobalCodex;
131
+ }
127
132
  const vscodeCodex = findVscodeCodexExe();
128
133
  if (vscodeCodex) {
129
134
  return vscodeCodex;
@@ -155,6 +160,59 @@ function findVscodeCodexExe() {
155
160
  }
156
161
  return null;
157
162
  }
163
+ function findWindowsNpmGlobalCodexBin() {
164
+ if (process.platform !== "win32") {
165
+ return null;
166
+ }
167
+ const candidates = [];
168
+ const appData = String(process.env.APPDATA ?? "").trim();
169
+ if (appData) {
170
+ candidates.push(path.join(appData, "npm", "codex.cmd"));
171
+ candidates.push(path.join(appData, "npm", "codex.exe"));
172
+ candidates.push(path.join(appData, "npm", "codex"));
173
+ }
174
+ const npmPrefix = readNpmPrefix();
175
+ if (npmPrefix) {
176
+ candidates.push(path.join(npmPrefix, "codex.cmd"));
177
+ candidates.push(path.join(npmPrefix, "codex.exe"));
178
+ candidates.push(path.join(npmPrefix, "codex"));
179
+ }
180
+ for (const candidate of candidates) {
181
+ if (fs.existsSync(candidate)) {
182
+ return candidate;
183
+ }
184
+ }
185
+ return null;
186
+ }
187
+ function readNpmPrefix() {
188
+ const result = spawnNpm(["config", "get", "prefix"]);
189
+ if (result.error || result.status !== 0) {
190
+ return "";
191
+ }
192
+ const value = String(result.stdout ?? "").trim();
193
+ if (!value || value.toLowerCase() === "undefined") {
194
+ return "";
195
+ }
196
+ return value;
197
+ }
198
+ function spawnNpm(args) {
199
+ if (process.platform === "win32") {
200
+ const comspec = process.env.ComSpec || "cmd.exe";
201
+ const commandLine = ["npm", ...args].join(" ");
202
+ return spawnSync(comspec, ["/d", "/s", "/c", commandLine], {
203
+ cwd: process.cwd(),
204
+ stdio: ["ignore", "pipe", "pipe"],
205
+ shell: false,
206
+ encoding: "utf8",
207
+ });
208
+ }
209
+ return spawnSync("npm", args, {
210
+ cwd: process.cwd(),
211
+ stdio: ["ignore", "pipe", "pipe"],
212
+ shell: false,
213
+ encoding: "utf8",
214
+ });
215
+ }
158
216
  function normalizeThreadMode(value) {
159
217
  const mode = String(value ?? "single")
160
218
  .trim()
@@ -5,7 +5,9 @@ import { fileURLToPath } from "node:url";
5
5
  import express from "express";
6
6
  import { BotManager } from "@copilot-hub/core/bot-manager";
7
7
  import { CodexAppClient } from "@copilot-hub/core/codex-app-client";
8
+ import { buildShellWrappedCommandLine, requiresShellWrappedSpawn, } from "@copilot-hub/core/codex-app-utils";
8
9
  import { loadBotRegistry } from "@copilot-hub/core/bot-registry";
10
+ import { invalidateCodexQuotaUsageCache } from "@copilot-hub/core/telegram-channel";
9
11
  import { config } from "./config.js";
10
12
  import { InstanceLock } from "@copilot-hub/core/instance-lock";
11
13
  import { KernelControlPlane } from "@copilot-hub/core/kernel-control-plane";
@@ -215,6 +217,7 @@ function buildApiApp({ botManager, controlPlane, registryFilePath, }) {
215
217
  const refreshed = await refreshRunningBotProviders({
216
218
  botManager: requireBotManager(),
217
219
  });
220
+ invalidateCodexQuotaUsageCache();
218
221
  res.json({
219
222
  ok: true,
220
223
  switched: true,
@@ -273,8 +276,18 @@ function buildApiApp({ botManager, controlPlane, registryFilePath, }) {
273
276
  .trim()
274
277
  .toLowerCase();
275
278
  const hasModel = Object.prototype.hasOwnProperty.call(req.body ?? {}, "model");
279
+ const hasReasoningEffort = Object.prototype.hasOwnProperty.call(req.body ?? {}, "reasoningEffort");
280
+ const hasServiceTier = Object.prototype.hasOwnProperty.call(req.body ?? {}, "serviceTier");
276
281
  const rawModel = req.body?.model;
282
+ const rawReasoningEffort = req.body?.reasoningEffort;
283
+ const rawServiceTier = req.body?.serviceTier;
277
284
  const model = rawModel === null || rawModel === undefined ? null : String(rawModel).trim();
285
+ const reasoningEffort = rawReasoningEffort === null || rawReasoningEffort === undefined
286
+ ? null
287
+ : String(rawReasoningEffort).trim().toLowerCase();
288
+ const serviceTier = rawServiceTier === null || rawServiceTier === undefined
289
+ ? null
290
+ : String(rawServiceTier).trim().toLowerCase();
278
291
  if (!sandboxMode) {
279
292
  res.status(400).json({ error: "Field 'sandboxMode' is required." });
280
293
  return;
@@ -291,6 +304,12 @@ function buildApiApp({ botManager, controlPlane, registryFilePath, }) {
291
304
  if (hasModel) {
292
305
  payload.model = model;
293
306
  }
307
+ if (hasReasoningEffort) {
308
+ payload.reasoningEffort = reasoningEffort;
309
+ }
310
+ if (hasServiceTier) {
311
+ payload.serviceTier = serviceTier;
312
+ }
294
313
  const result = await controlPlane.runSystemAction(CONTROL_ACTIONS.BOTS_SET_POLICY, payload);
295
314
  res.json(result);
296
315
  }));
@@ -476,13 +495,21 @@ function startCodexDeviceAuthSession() {
476
495
  refreshedBots: [],
477
496
  refreshFailures: [],
478
497
  };
479
- const child = spawn(codexBin, ["login", "--device-auth"], {
480
- cwd: config.kernelRootPath,
481
- shell: false,
482
- stdio: ["ignore", "pipe", "pipe"],
483
- windowsHide: true,
484
- env: process.env,
485
- });
498
+ const child = requiresShellWrappedSpawn(codexBin)
499
+ ? spawn(buildShellWrappedCommandLine(codexBin, ["login", "--device-auth"]), {
500
+ cwd: config.kernelRootPath,
501
+ shell: true,
502
+ stdio: ["ignore", "pipe", "pipe"],
503
+ windowsHide: true,
504
+ env: process.env,
505
+ })
506
+ : spawn(codexBin, ["login", "--device-auth"], {
507
+ cwd: config.kernelRootPath,
508
+ shell: false,
509
+ stdio: ["ignore", "pipe", "pipe"],
510
+ windowsHide: true,
511
+ env: process.env,
512
+ });
486
513
  session.child = child;
487
514
  codexDeviceAuthSession = session;
488
515
  child.stdout.on("data", (chunk) => {
@@ -507,6 +534,7 @@ function startCodexDeviceAuthSession() {
507
534
  botManager: requireBotManager(),
508
535
  })
509
536
  .then((refreshed) => {
537
+ invalidateCodexQuotaUsageCache();
510
538
  session.refreshedBots = refreshed.refreshedBotIds;
511
539
  session.refreshFailures = refreshed.failures;
512
540
  session.status = "succeeded";
@@ -706,6 +734,8 @@ function normalizeModelCatalog(rawModels) {
706
734
  displayName: String(entry?.displayName ?? model).trim() || model,
707
735
  description: String(entry?.description ?? "").trim(),
708
736
  isDefault: entry?.isDefault === true,
737
+ supportedReasoningEfforts: normalizeReasoningCatalog(entry?.supportedReasoningEfforts),
738
+ defaultReasoningEffort: normalizeReasoningEffortValue(entry?.defaultReasoningEffort),
709
739
  });
710
740
  }
711
741
  return normalized.sort((a, b) => {
@@ -725,17 +755,58 @@ function looksLikeCodexApiKey(value) {
725
755
  }
726
756
  return key.startsWith("sk-");
727
757
  }
758
+ function normalizeReasoningCatalog(value) {
759
+ const options = [];
760
+ const seen = new Set();
761
+ for (const entry of Array.isArray(value) ? value : []) {
762
+ const option = isRecord(entry) ? entry : {};
763
+ const reasoningEffort = normalizeReasoningEffortValue(option.reasoningEffort ?? option.effort ?? option.id);
764
+ if (!reasoningEffort || seen.has(reasoningEffort)) {
765
+ continue;
766
+ }
767
+ seen.add(reasoningEffort);
768
+ options.push({
769
+ reasoningEffort,
770
+ description: String(option.description ?? "").trim(),
771
+ });
772
+ }
773
+ return options;
774
+ }
775
+ function normalizeReasoningEffortValue(value) {
776
+ const normalized = String(value ?? "")
777
+ .trim()
778
+ .toLowerCase();
779
+ if (normalized === "none" ||
780
+ normalized === "minimal" ||
781
+ normalized === "low" ||
782
+ normalized === "medium" ||
783
+ normalized === "high" ||
784
+ normalized === "xhigh") {
785
+ return normalized;
786
+ }
787
+ return null;
788
+ }
728
789
  function runCodexCommand(args, { inputText = "" } = {}) {
729
790
  const codexBin = String(config.codexBin ?? "codex").trim() || "codex";
730
- const result = spawnSync(codexBin, args, {
731
- cwd: config.kernelRootPath,
732
- shell: false,
733
- stdio: ["pipe", "pipe", "pipe"],
734
- windowsHide: true,
735
- encoding: "utf8",
736
- input: inputText,
737
- env: process.env,
738
- });
791
+ const result = requiresShellWrappedSpawn(codexBin)
792
+ ? spawnSync(buildShellWrappedCommandLine(codexBin, args), {
793
+ cwd: config.kernelRootPath,
794
+ shell: true,
795
+ stdio: ["pipe", "pipe", "pipe"],
796
+ windowsHide: true,
797
+ encoding: "utf8",
798
+ input: inputText,
799
+ env: process.env,
800
+ })
801
+ : spawnSync(codexBin, args, {
802
+ cwd: config.kernelRootPath,
803
+ shell: false,
804
+ stdio: ["pipe", "pipe", "pipe"],
805
+ windowsHide: true,
806
+ encoding: "utf8",
807
+ input: inputText,
808
+ env: process.env,
809
+ });
739
810
  if (result.error) {
740
811
  return {
741
812
  ok: false,
@@ -764,6 +835,9 @@ function formatCodexSpawnError(codexBin, error) {
764
835
  if (code === "EPERM") {
765
836
  return `Codex binary '${codexBin}' cannot be executed (EPERM).`;
766
837
  }
838
+ if (code === "EINVAL" && requiresShellWrappedSpawn(codexBin)) {
839
+ return `Codex binary '${codexBin}' must be launched through the Windows shell.`;
840
+ }
767
841
  const message = error instanceof Error ? error.message : String(error);
768
842
  return `Failed to execute '${codexBin}': ${firstNonEmptyLine(message)}`;
769
843
  }
@@ -0,0 +1,16 @@
1
+ let cachedCodexUsage = null;
2
+ export function readCachedCodexQuotaSnapshot(now) {
3
+ if (cachedCodexUsage && now < cachedCodexUsage.expiresAt) {
4
+ return cachedCodexUsage.snapshot;
5
+ }
6
+ return null;
7
+ }
8
+ export function writeCachedCodexQuotaSnapshot(snapshot, expiresAt) {
9
+ cachedCodexUsage = {
10
+ expiresAt,
11
+ snapshot,
12
+ };
13
+ }
14
+ export function invalidateCodexQuotaUsageCache() {
15
+ cachedCodexUsage = null;
16
+ }
@@ -1,4 +1,4 @@
1
- const MODEL_INLINE_MAX = 6;
1
+ const MODEL_INLINE_MAX = 24;
2
2
  const MODEL_PATTERN = /^[A-Za-z0-9._:-]+$/;
3
3
  export function parseSetModelCommand(text, botIdPattern) {
4
4
  const tokens = String(text ?? "")
@@ -88,6 +88,30 @@ export function formatModelLabel(value) {
88
88
  }
89
89
  return model;
90
90
  }
91
+ export function formatReasoningLabel(value) {
92
+ const reasoningEffort = normalizeReasoningEffortValue(value);
93
+ if (!reasoningEffort) {
94
+ return "Default";
95
+ }
96
+ switch (reasoningEffort) {
97
+ case "xhigh":
98
+ return "Extra High";
99
+ case "none":
100
+ return "None";
101
+ default:
102
+ return reasoningEffort.charAt(0).toUpperCase() + reasoningEffort.slice(1);
103
+ }
104
+ }
105
+ export function formatFastModeLabel(value) {
106
+ const serviceTier = normalizeServiceTierValue(value);
107
+ if (serviceTier === "fast") {
108
+ return "Fast";
109
+ }
110
+ if (serviceTier === "flex") {
111
+ return "Flex (manual)";
112
+ }
113
+ return "Standard";
114
+ }
91
115
  export function formatModelButtonText(label, selected) {
92
116
  const text = String(label ?? "").trim() || "Model";
93
117
  return selected ? `* ${text}` : text;
@@ -117,6 +141,8 @@ export function buildSessionModelOptions({ catalog, currentModel, inlineMax = MO
117
141
  label: String(entry.displayName ?? model).trim() || model,
118
142
  isDefault: entry.isDefault === true,
119
143
  selected: normalizedCurrent === key,
144
+ supportedReasoningEfforts: normalizeReasoningCatalog(entry.supportedReasoningEfforts),
145
+ defaultReasoningEffort: normalizeReasoningEffortValue(entry.defaultReasoningEffort),
120
146
  });
121
147
  }
122
148
  options.sort((a, b) => {
@@ -136,6 +162,47 @@ export function buildSessionModelOptions({ catalog, currentModel, inlineMax = MO
136
162
  key: `k${index}`,
137
163
  }));
138
164
  }
165
+ export function buildReasoningOptionsForModel({ modelSelection, currentModel, currentReasoningEffort, }) {
166
+ if (!modelSelection.ok || !modelSelection.model) {
167
+ return [
168
+ {
169
+ key: "default",
170
+ reasoningEffort: null,
171
+ label: "Default",
172
+ description: "Use the default reasoning level for the resolved model.",
173
+ selected: true,
174
+ },
175
+ ];
176
+ }
177
+ const selectedModel = String(modelSelection.model).trim().toLowerCase();
178
+ const normalizedCurrentModel = String(currentModel ?? "")
179
+ .trim()
180
+ .toLowerCase();
181
+ const normalizedCurrentReasoning = normalizeReasoningEffortValue(currentReasoningEffort);
182
+ const selectedReasoning = selectedModel === normalizedCurrentModel ? normalizedCurrentReasoning : null;
183
+ const options = [
184
+ {
185
+ key: "default",
186
+ reasoningEffort: null,
187
+ label: "Default",
188
+ description: modelSelection.defaultReasoningEffort !== null
189
+ ? `Use ${formatReasoningLabel(modelSelection.defaultReasoningEffort)} for this model.`
190
+ : "Use the model default reasoning level.",
191
+ selected: selectedReasoning === null,
192
+ },
193
+ ];
194
+ for (const entry of modelSelection.supportedReasoningEfforts) {
195
+ options.push({
196
+ key: `r${options.length - 1}`,
197
+ reasoningEffort: entry.reasoningEffort,
198
+ label: formatReasoningLabel(entry.reasoningEffort),
199
+ description: String(entry.description ?? "").trim() ||
200
+ `${formatReasoningLabel(entry.reasoningEffort)} reasoning effort.`,
201
+ selected: selectedReasoning === entry.reasoningEffort,
202
+ });
203
+ }
204
+ return options;
205
+ }
139
206
  export function resolveModelSelectionFromAction({ session, profileId, }) {
140
207
  const target = String(profileId ?? "")
141
208
  .trim()
@@ -143,15 +210,21 @@ export function resolveModelSelectionFromAction({ session, profileId, }) {
143
210
  if (!target) {
144
211
  return {
145
212
  ok: false,
213
+ key: "",
146
214
  model: null,
147
215
  label: "",
216
+ supportedReasoningEfforts: [],
217
+ defaultReasoningEffort: null,
148
218
  };
149
219
  }
150
220
  if (target === "auto") {
151
221
  return {
152
222
  ok: true,
223
+ key: "auto",
153
224
  model: null,
154
225
  label: "Auto (workspace default)",
226
+ supportedReasoningEfforts: [],
227
+ defaultReasoningEffort: null,
155
228
  };
156
229
  }
157
230
  const options = Array.isArray(session?.modelOptions) ? session.modelOptions : [];
@@ -161,14 +234,47 @@ export function resolveModelSelectionFromAction({ session, profileId, }) {
161
234
  if (!matched) {
162
235
  return {
163
236
  ok: false,
237
+ key: "",
164
238
  model: null,
165
239
  label: "",
240
+ supportedReasoningEfforts: [],
241
+ defaultReasoningEffort: null,
166
242
  };
167
243
  }
168
244
  return {
169
245
  ok: true,
246
+ key: matched.key,
170
247
  model: String(matched.model ?? "").trim() || null,
171
248
  label: String(matched.label ?? matched.model ?? "custom").trim(),
249
+ supportedReasoningEfforts: matched.supportedReasoningEfforts,
250
+ defaultReasoningEffort: matched.defaultReasoningEffort,
251
+ };
252
+ }
253
+ export function resolveReasoningSelectionFromAction({ options, profileId, }) {
254
+ const target = String(profileId ?? "")
255
+ .trim()
256
+ .toLowerCase();
257
+ if (!target) {
258
+ return {
259
+ ok: false,
260
+ reasoningEffort: null,
261
+ label: "",
262
+ };
263
+ }
264
+ const matched = options.find((entry) => String(entry?.key ?? "")
265
+ .trim()
266
+ .toLowerCase() === target);
267
+ if (!matched) {
268
+ return {
269
+ ok: false,
270
+ reasoningEffort: null,
271
+ label: "",
272
+ };
273
+ }
274
+ return {
275
+ ok: true,
276
+ reasoningEffort: matched.reasoningEffort,
277
+ label: matched.label,
172
278
  };
173
279
  }
174
280
  export async function fetchCodexModelOptions(apiGet) {
@@ -193,7 +299,10 @@ export async function fetchCodexModelOptions(apiGet) {
193
299
  models.push({
194
300
  model,
195
301
  displayName: String(entry.displayName ?? model).trim() || model,
302
+ description: String(entry.description ?? "").trim(),
196
303
  isDefault: entry.isDefault === true,
304
+ supportedReasoningEfforts: normalizeReasoningCatalog(entry.supportedReasoningEfforts),
305
+ defaultReasoningEffort: normalizeReasoningEffortValue(entry.defaultReasoningEffort),
197
306
  });
198
307
  }
199
308
  return {
@@ -226,9 +335,6 @@ export function resolveApprovalPolicy(value) {
226
335
  }
227
336
  return "never";
228
337
  }
229
- function isObject(value) {
230
- return value !== null && typeof value === "object";
231
- }
232
338
  export function getBotPolicyState(botState) {
233
339
  const provider = isObject(botState) && isObject(botState.provider) ? botState.provider : null;
234
340
  const options = provider && isObject(provider.options) ? provider.options : {};
@@ -237,35 +343,68 @@ export function getBotPolicyState(botState) {
237
343
  approvalPolicy: resolveApprovalPolicy(options.approvalPolicy),
238
344
  };
239
345
  }
240
- export async function applyBotModelPolicy({ apiPost, botId, botState, model, }) {
241
- const policyState = getBotPolicyState(botState);
242
- return apiPost(`/api/bots/${encodeURIComponent(botId)}/policy`, {
243
- sandboxMode: policyState.sandboxMode,
244
- approvalPolicy: policyState.approvalPolicy,
245
- model,
246
- });
346
+ export function getBotProviderSelection(botState) {
347
+ const provider = isObject(botState) && isObject(botState.provider) ? botState.provider : null;
348
+ const options = provider && isObject(provider.options) ? provider.options : {};
349
+ return {
350
+ model: normalizeModelValue(options.model),
351
+ reasoningEffort: normalizeReasoningEffortValue(options.reasoningEffort),
352
+ serviceTier: normalizeServiceTierValue(options.serviceTier),
353
+ };
247
354
  }
248
- export function getRuntimeModel(runtime) {
355
+ export function getRuntimeProviderSelection(runtime) {
249
356
  if (!runtime || typeof runtime.getProviderOptions !== "function") {
250
- return null;
357
+ return {
358
+ model: null,
359
+ reasoningEffort: null,
360
+ serviceTier: null,
361
+ };
251
362
  }
252
363
  const options = runtime.getProviderOptions();
253
- if (!isObject(options)) {
254
- return null;
364
+ const record = isObject(options) ? options : {};
365
+ return {
366
+ model: normalizeModelValue(record.model),
367
+ reasoningEffort: normalizeReasoningEffortValue(record.reasoningEffort),
368
+ serviceTier: normalizeServiceTierValue(record.serviceTier),
369
+ };
370
+ }
371
+ export async function applyBotProviderPolicy({ apiPost, botId, botState, patch, }) {
372
+ const policyState = getBotPolicyState(botState);
373
+ const payload = {
374
+ sandboxMode: policyState.sandboxMode,
375
+ approvalPolicy: policyState.approvalPolicy,
376
+ };
377
+ if (Object.prototype.hasOwnProperty.call(patch, "model")) {
378
+ payload.model = patch.model ?? null;
379
+ }
380
+ if (Object.prototype.hasOwnProperty.call(patch, "reasoningEffort")) {
381
+ payload.reasoningEffort = patch.reasoningEffort ?? null;
255
382
  }
256
- const model = String(options.model ?? "").trim();
257
- return model || null;
383
+ if (Object.prototype.hasOwnProperty.call(patch, "serviceTier")) {
384
+ payload.serviceTier = patch.serviceTier ?? null;
385
+ }
386
+ return apiPost(`/api/bots/${encodeURIComponent(botId)}/policy`, payload);
258
387
  }
259
- export async function applyRuntimeModelPolicy({ runtime, model, }) {
388
+ export async function applyRuntimeProviderPolicy({ runtime, patch, }) {
260
389
  if (!runtime || typeof runtime.setProviderOptions !== "function") {
261
- throw new Error("Hub model update is not available on this runtime.");
390
+ throw new Error("Hub provider update is not available on this runtime.");
391
+ }
392
+ const payload = {};
393
+ if (Object.prototype.hasOwnProperty.call(patch, "model")) {
394
+ payload.model = patch.model ?? null;
395
+ }
396
+ if (Object.prototype.hasOwnProperty.call(patch, "reasoningEffort")) {
397
+ payload.reasoningEffort = patch.reasoningEffort ?? null;
262
398
  }
263
- await runtime.setProviderOptions({ model });
399
+ if (Object.prototype.hasOwnProperty.call(patch, "serviceTier")) {
400
+ payload.serviceTier = patch.serviceTier ?? null;
401
+ }
402
+ await runtime.setProviderOptions(payload);
264
403
  }
265
404
  export function resolveSharedModel(models) {
266
405
  let normalizedModel;
267
406
  for (const entry of Array.isArray(models) ? models : []) {
268
- const nextModel = String(entry ?? "").trim() || null;
407
+ const nextModel = normalizeModelValue(entry);
269
408
  if (normalizedModel === undefined) {
270
409
  normalizedModel = nextModel;
271
410
  continue;
@@ -279,7 +418,41 @@ export function resolveSharedModel(models) {
279
418
  model: normalizedModel ?? null,
280
419
  };
281
420
  }
282
- export async function applyModelPolicyToBots({ apiPost, bots, model, }) {
421
+ export function resolveSharedReasoningEffort(values) {
422
+ let normalizedValue;
423
+ for (const entry of Array.isArray(values) ? values : []) {
424
+ const nextValue = normalizeReasoningEffortValue(entry);
425
+ if (normalizedValue === undefined) {
426
+ normalizedValue = nextValue;
427
+ continue;
428
+ }
429
+ if (normalizedValue !== nextValue) {
430
+ return { mode: "mixed" };
431
+ }
432
+ }
433
+ return {
434
+ mode: "uniform",
435
+ reasoningEffort: normalizedValue ?? null,
436
+ };
437
+ }
438
+ export function resolveSharedServiceTier(values) {
439
+ let normalizedValue;
440
+ for (const entry of Array.isArray(values) ? values : []) {
441
+ const nextValue = normalizeServiceTierValue(entry);
442
+ if (normalizedValue === undefined) {
443
+ normalizedValue = nextValue;
444
+ continue;
445
+ }
446
+ if (normalizedValue !== nextValue) {
447
+ return { mode: "mixed" };
448
+ }
449
+ }
450
+ return {
451
+ mode: "uniform",
452
+ serviceTier: normalizedValue ?? null,
453
+ };
454
+ }
455
+ export async function applyProviderPolicyToBots({ apiPost, bots, patch, }) {
283
456
  const updatedBotIds = [];
284
457
  const failures = [];
285
458
  for (const botState of Array.isArray(bots) ? bots : []) {
@@ -288,11 +461,11 @@ export async function applyModelPolicyToBots({ apiPost, bots, model, }) {
288
461
  continue;
289
462
  }
290
463
  try {
291
- await applyBotModelPolicy({
464
+ await applyBotProviderPolicy({
292
465
  apiPost,
293
466
  botId,
294
467
  botState,
295
- model,
468
+ patch,
296
469
  });
297
470
  updatedBotIds.push(botId);
298
471
  }
@@ -308,6 +481,53 @@ export async function applyModelPolicyToBots({ apiPost, bots, model, }) {
308
481
  failures,
309
482
  };
310
483
  }
484
+ function isObject(value) {
485
+ return value !== null && typeof value === "object";
486
+ }
487
+ function normalizeModelValue(value) {
488
+ const normalized = String(value ?? "").trim();
489
+ return normalized || null;
490
+ }
491
+ function normalizeReasoningCatalog(value) {
492
+ const options = [];
493
+ const seen = new Set();
494
+ for (const entry of Array.isArray(value) ? value : []) {
495
+ const option = isObject(entry) ? entry : {};
496
+ const reasoningEffort = normalizeReasoningEffortValue(option.reasoningEffort ?? option.effort ?? option.id);
497
+ if (!reasoningEffort || seen.has(reasoningEffort)) {
498
+ continue;
499
+ }
500
+ seen.add(reasoningEffort);
501
+ options.push({
502
+ reasoningEffort,
503
+ description: String(option.description ?? "").trim(),
504
+ });
505
+ }
506
+ return options;
507
+ }
508
+ function normalizeReasoningEffortValue(value) {
509
+ const normalized = String(value ?? "")
510
+ .trim()
511
+ .toLowerCase();
512
+ if (normalized === "none" ||
513
+ normalized === "minimal" ||
514
+ normalized === "low" ||
515
+ normalized === "medium" ||
516
+ normalized === "high" ||
517
+ normalized === "xhigh") {
518
+ return normalized;
519
+ }
520
+ return null;
521
+ }
522
+ function normalizeServiceTierValue(value) {
523
+ const normalized = String(value ?? "")
524
+ .trim()
525
+ .toLowerCase();
526
+ if (normalized === "fast" || normalized === "flex") {
527
+ return normalized;
528
+ }
529
+ return null;
530
+ }
311
531
  function sanitizeError(error) {
312
532
  const raw = error instanceof Error ? error.message : String(error);
313
533
  return raw.split(/\r?\n/).slice(0, 6).join("\n");