bosun 0.36.4 → 0.36.5

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/monitor.mjs CHANGED
@@ -991,7 +991,6 @@ let {
991
991
  fleet: fleetConfig,
992
992
  internalExecutor: internalExecutorConfig,
993
993
  executorMode: configExecutorMode,
994
- githubReconcile: githubReconcileConfig,
995
994
  } = config;
996
995
 
997
996
  const telegramWeeklyReportEnabled = parseEnvBoolean(
@@ -1023,12 +1022,6 @@ let triggerSystemConfig =
1023
1022
  : { enabled: false, templates: [], defaults: { executor: "auto", model: "auto" } };
1024
1023
  let kanbanBackend = String(kanbanConfig?.backend || "internal").toLowerCase();
1025
1024
  let executorMode = configExecutorMode || getExecutorMode();
1026
- let githubReconcile = githubReconcileConfig || {
1027
- enabled: false,
1028
- intervalMs: 5 * 60 * 1000,
1029
- mergedLookbackHours: 72,
1030
- trackingLabels: ["tracking"],
1031
- };
1032
1025
  let chdirUnsupportedInRuntime = false;
1033
1026
 
1034
1027
  function isChdirUnsupportedError(err) {
@@ -10158,6 +10151,10 @@ function startTaskPlannerStatusLoop() {
10158
10151
  }, 25_000);
10159
10152
  }
10160
10153
 
10154
+ // GitHub reconciler hooks are currently optional; keep shutdown/reload calls safe.
10155
+ function restartGitHubReconciler() {}
10156
+ function stopGitHubReconciler() {}
10157
+
10161
10158
  async function maybeTriggerTaskPlanner(reason, details, options = {}) {
10162
10159
  if (internalTaskExecutor?.isPaused?.()) {
10163
10160
  console.log("[monitor] task planner skipped: executor paused");
@@ -13056,6 +13053,61 @@ function formatOrchestratorTailForMonitorPrompt({
13056
13053
  }
13057
13054
  }
13058
13055
 
13056
+ const MONITOR_MONITOR_ACTIONABLE_DIGEST_MAX_AGE_MS = (() => {
13057
+ const raw = Number(
13058
+ process.env.DEVMODE_MONITOR_MONITOR_ACTIONABLE_DIGEST_MAX_AGE_MS ||
13059
+ 15 * 60 * 1000,
13060
+ );
13061
+ if (!Number.isFinite(raw) || raw <= 0) return 15 * 60 * 1000;
13062
+ return Math.max(60 * 1000, Math.min(24 * 60 * 60 * 1000, Math.trunc(raw)));
13063
+ })();
13064
+
13065
+ function parseDigestEntryTimestampMs(entry, { nowMs = Date.now(), digestStartedAt = 0 } = {}) {
13066
+ const explicitTs = Number(entry?.timestamp ?? entry?.timeMs ?? entry?.ts);
13067
+ if (Number.isFinite(explicitTs) && explicitTs > 0) return explicitTs;
13068
+
13069
+ const timeText = String(entry?.time || "").trim();
13070
+ const match = timeText.match(/^(\d{2}):(\d{2}):(\d{2})$/);
13071
+ if (!match) return null;
13072
+
13073
+ const hour = Number(match[1]);
13074
+ const minute = Number(match[2]);
13075
+ const second = Number(match[3]);
13076
+ if (![hour, minute, second].every(Number.isFinite)) return null;
13077
+
13078
+ const nowDate = new Date(nowMs);
13079
+ let candidate = Date.UTC(
13080
+ nowDate.getUTCFullYear(),
13081
+ nowDate.getUTCMonth(),
13082
+ nowDate.getUTCDate(),
13083
+ hour,
13084
+ minute,
13085
+ second,
13086
+ 0,
13087
+ );
13088
+
13089
+ // Digest times are rendered as UTC HH:MM:SS.
13090
+ if (candidate > nowMs + 60_000) {
13091
+ candidate -= 24 * 60 * 60 * 1000;
13092
+ }
13093
+ // If digest started before midnight and entry time is after midnight.
13094
+ if (
13095
+ digestStartedAt > 0 &&
13096
+ candidate < digestStartedAt - 60_000 &&
13097
+ candidate + 24 * 60 * 60 * 1000 <= nowMs + 60_000
13098
+ ) {
13099
+ candidate += 24 * 60 * 60 * 1000;
13100
+ }
13101
+ return candidate;
13102
+ }
13103
+
13104
+ function isDigestEntryActionable(entry, { nowMs = Date.now(), digestStartedAt = 0 } = {}) {
13105
+ if (Number(entry?.priority || 99) > 3) return false;
13106
+ const timestampMs = parseDigestEntryTimestampMs(entry, { nowMs, digestStartedAt });
13107
+ if (!Number.isFinite(timestampMs)) return true;
13108
+ return nowMs - timestampMs <= MONITOR_MONITOR_ACTIONABLE_DIGEST_MAX_AGE_MS;
13109
+ }
13110
+
13059
13111
  async function buildMonitorMonitorPrompt({ trigger, entries, text }) {
13060
13112
  const digestSnapshot = getDigestSnapshot();
13061
13113
  const digestEntries =
@@ -13063,8 +13115,10 @@ async function buildMonitorMonitorPrompt({ trigger, entries, text }) {
13063
13115
  ? entries
13064
13116
  : digestSnapshot?.entries || [];
13065
13117
  const latestDigestText = String(text || monitorMonitor.lastDigestText || "");
13066
- const actionableEntries = digestEntries.filter(
13067
- (entry) => Number(entry?.priority || 99) <= 3,
13118
+ const nowMs = Date.now();
13119
+ const digestStartedAt = Number(digestSnapshot?.startedAt || 0);
13120
+ const actionableEntries = digestEntries.filter((entry) =>
13121
+ isDigestEntryActionable(entry, { nowMs, digestStartedAt }),
13068
13122
  );
13069
13123
  const modeHint =
13070
13124
  actionableEntries.length > 0 ? "reliability-fix" : "code-analysis";
@@ -14578,7 +14632,6 @@ function applyConfig(nextConfig, options = {}) {
14578
14632
  if (workflowAutomationEnabled) {
14579
14633
  ensureWorkflowAutomationEngine().catch(() => {});
14580
14634
  }
14581
- githubReconcile = nextConfig.githubReconcile || githubReconcile;
14582
14635
  agentPrompts = nextConfig.agentPrompts;
14583
14636
  configExecutorConfig = nextConfig.executorConfig;
14584
14637
  executorScheduler = nextConfig.scheduler;
@@ -14698,7 +14751,6 @@ function applyConfig(nextConfig, options = {}) {
14698
14751
  } else {
14699
14752
  stopMonitorMonitorSupervisor();
14700
14753
  }
14701
- restartGitHubReconciler();
14702
14754
 
14703
14755
  const nextArgs = scriptArgs?.join(" ") || "";
14704
14756
  const scriptChanged = prevScriptPath !== scriptPath || prevArgs !== nextArgs;
@@ -14732,7 +14784,6 @@ process.on("SIGINT", async () => {
14732
14784
  shuttingDown = true;
14733
14785
  stopWorkspaceSyncTimers();
14734
14786
  stopTaskPlannerStatusLoop();
14735
- stopGitHubReconciler();
14736
14787
  // Stop monitor-monitor immediately (it's safely restartable)
14737
14788
  stopMonitorMonitorSupervisor();
14738
14789
  if (vkLogStream) {
@@ -14786,7 +14837,6 @@ process.on("exit", () => {
14786
14837
  shuttingDown = true;
14787
14838
  stopWorkspaceSyncTimers();
14788
14839
  stopTaskPlannerStatusLoop();
14789
- stopGitHubReconciler();
14790
14840
  stopMonitorMonitorSupervisor();
14791
14841
  stopAgentAlertTailer();
14792
14842
  stopAgentWorkAnalyzer();
@@ -14802,7 +14852,6 @@ process.on("SIGTERM", async () => {
14802
14852
  shuttingDown = true;
14803
14853
  stopWorkspaceSyncTimers();
14804
14854
  stopTaskPlannerStatusLoop();
14805
- stopGitHubReconciler();
14806
14855
  // Stop monitor-monitor immediately (it's safely restartable)
14807
14856
  stopMonitorMonitorSupervisor();
14808
14857
  if (vkLogStream) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.36.4",
3
+ "version": "0.36.5",
4
4
  "description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
5
5
  "type": "module",
6
6
  "license": "Apache 2.0",
@@ -436,8 +436,8 @@ function buildStableSetupDefaults({
436
436
  workflowMaxConcurrentBranches: 8,
437
437
  voiceEnabled: true,
438
438
  voiceProvider: "auto",
439
- voiceModel: "gpt-realtime-1.5",
440
- voiceVisionModel: "gpt-4.1-mini",
439
+ voiceModel: "gpt-audio-1.5",
440
+ voiceVisionModel: "gpt-4.1-nano",
441
441
  voiceId: "alloy",
442
442
  voiceTurnDetection: "server_vad",
443
443
  voiceFallbackMode: "browser",
@@ -445,7 +445,7 @@ function buildStableSetupDefaults({
445
445
  openaiRealtimeApiKey: "",
446
446
  azureOpenaiRealtimeEndpoint: "",
447
447
  azureOpenaiRealtimeApiKey: "",
448
- azureOpenaiRealtimeDeployment: "gpt-realtime-1.5",
448
+ azureOpenaiRealtimeDeployment: "gpt-audio-1.5",
449
449
  copilotEnableAllMcpTools: false,
450
450
  // Backward-compatible fields consumed by older setup UI revisions.
451
451
  distribution: "primary-only",
@@ -864,16 +864,16 @@ function applyNonBlockingSetupEnvDefaults(envMap, env = {}, sourceEnv = process.
864
864
  env.VOICE_MODEL,
865
865
  envMap.VOICE_MODEL,
866
866
  sourceEnv.VOICE_MODEL,
867
- ) || "gpt-realtime-1.5",
868
- ).trim() || "gpt-realtime-1.5";
867
+ ) || "gpt-audio-1.5",
868
+ ).trim() || "gpt-audio-1.5";
869
869
  envMap.VOICE_VISION_MODEL = String(
870
870
  pickNonEmptyValue(
871
871
  env.voiceVisionModel,
872
872
  env.VOICE_VISION_MODEL,
873
873
  envMap.VOICE_VISION_MODEL,
874
874
  sourceEnv.VOICE_VISION_MODEL,
875
- ) || "gpt-4.1-mini",
876
- ).trim() || "gpt-4.1-mini";
875
+ ) || "gpt-4.1-nano",
876
+ ).trim() || "gpt-4.1-nano";
877
877
  envMap.VOICE_ID = normalizeEnumValue(
878
878
  pickNonEmptyValue(
879
879
  env.voiceId,
@@ -953,8 +953,8 @@ function applyNonBlockingSetupEnvDefaults(envMap, env = {}, sourceEnv = process.
953
953
  env.AZURE_OPENAI_REALTIME_DEPLOYMENT,
954
954
  envMap.AZURE_OPENAI_REALTIME_DEPLOYMENT,
955
955
  sourceEnv.AZURE_OPENAI_REALTIME_DEPLOYMENT,
956
- ) || "gpt-realtime-1.5",
957
- ).trim() || "gpt-realtime-1.5";
956
+ ) || "gpt-audio-1.5",
957
+ ).trim() || "gpt-audio-1.5";
958
958
 
959
959
  envMap.CONTAINER_ENABLED = toBooleanEnvString(
960
960
  pickNonEmptyValue(env.containerEnabled, envMap.CONTAINER_ENABLED, sourceEnv.CONTAINER_ENABLED),
@@ -125,12 +125,12 @@ export const SETTINGS_SCHEMA = [
125
125
  // ── Voice Assistant ──────────────────────────────────────────
126
126
  { key: "VOICE_ENABLED", label: "Enable Voice Mode", category: "voice", type: "boolean", defaultVal: true, description: "Enable the real-time voice assistant in the chat UI." },
127
127
  { key: "VOICE_PROVIDER", label: "Voice Provider", category: "voice", type: "select", defaultVal: "auto", options: ["auto", "openai", "azure", "claude", "gemini", "fallback"], description: "Voice API provider. 'auto' selects based on available keys. 'fallback' uses browser speech APIs." },
128
- { key: "VOICE_MODEL", label: "Voice Model", category: "voice", type: "string", defaultVal: "gpt-4o-realtime-preview-2024-12-17", description: "OpenAI Realtime model to use for voice sessions." },
129
- { key: "VOICE_VISION_MODEL", label: "Vision Model", category: "voice", type: "string", defaultVal: "gpt-4.1-mini", description: "Vision model used for live screen/camera understanding in voice mode." },
128
+ { key: "VOICE_MODEL", label: "Voice Model", category: "voice", type: "select", defaultVal: "gpt-audio-1.5", options: ["gpt-audio-1.5", "gpt-realtime-1.5", "gpt-4o-realtime-preview-2024-12-17", "gemini-2.5-pro", "gemini-2.5-flash", "claude-3-7-sonnet-latest", "custom"], description: "Audio model for voice sessions. Select 'custom' to enter a model slug manually." },
129
+ { key: "VOICE_VISION_MODEL", label: "Vision Model", category: "voice", type: "select", defaultVal: "gpt-4.1-nano", options: ["gpt-4.1-nano", "gpt-4.1-mini", "gpt-4.1", "gemini-2.5-flash", "gemini-2.5-pro", "claude-3-7-sonnet-latest", "custom"], description: "Vision model for live screen/camera understanding. Select 'custom' to enter a model slug manually." },
130
130
  { key: "OPENAI_REALTIME_API_KEY", label: "OpenAI Realtime Key", category: "voice", type: "secret", sensitive: true, description: "Dedicated API key for voice. Falls back to OPENAI_API_KEY if not set." },
131
131
  { key: "AZURE_OPENAI_REALTIME_ENDPOINT", label: "Azure Realtime Endpoint", category: "voice", type: "string", description: "Azure OpenAI endpoint for Realtime API (e.g., https://myresource.openai.azure.com).", validate: "^$|^https?://" },
132
132
  { key: "AZURE_OPENAI_REALTIME_API_KEY", label: "Azure Realtime Key", category: "voice", type: "secret", sensitive: true, description: "Azure OpenAI API key for Realtime API. Falls back to AZURE_OPENAI_API_KEY if not set." },
133
- { key: "AZURE_OPENAI_REALTIME_DEPLOYMENT", label: "Azure Deployment", category: "voice", type: "string", defaultVal: "gpt-4o-realtime-preview", description: "Azure deployment name for the Realtime model." },
133
+ { key: "AZURE_OPENAI_REALTIME_DEPLOYMENT", label: "Azure Deployment", category: "voice", type: "select", defaultVal: "gpt-audio-1.5", options: ["gpt-audio-1.5", "gpt-realtime-1.5", "gpt-4o-realtime-preview", "custom"], description: "Azure deployment name for the Realtime model. Select 'custom' to enter manually." },
134
134
  { key: "VOICE_ID", label: "Voice", category: "voice", type: "select", defaultVal: "alloy", options: ["alloy", "ash", "ballad", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer", "verse"], description: "Voice personality for text-to-speech output." },
135
135
  { key: "VOICE_TURN_DETECTION", label: "Turn Detection", category: "voice", type: "select", defaultVal: "server_vad", options: ["server_vad", "semantic_vad", "none"], description: "How the model detects when you stop speaking. 'semantic_vad' is more intelligent but higher latency." },
136
136
  { key: "VOICE_DELEGATE_EXECUTOR", label: "Delegate Executor", category: "voice", type: "select", defaultVal: "codex-sdk", options: ["codex-sdk", "copilot-sdk", "claude-sdk", "gemini-sdk", "opencode-sdk"], description: "Which agent executor voice tool calls delegate to for complex tasks." },
@@ -307,8 +307,11 @@ export function validateSetting(def, value) {
307
307
  return { valid: false, error: "Must be true or false" };
308
308
  return { valid: true };
309
309
  case "select":
310
- if (def.options && !def.options.includes(String(value)))
310
+ if (def.options && !def.options.includes(String(value))) {
311
+ // Allow arbitrary values when the schema includes "custom" as an option
312
+ if (def.options.includes("custom")) return { valid: true };
311
313
  return { valid: false, error: `Must be one of: ${def.options.join(", ")}` };
314
+ }
312
315
  return { valid: true };
313
316
  default:
314
317
  if (def.validate) {
package/ui/setup.html CHANGED
@@ -705,8 +705,8 @@ function App() {
705
705
  // Voice assistant
706
706
  const [voiceEnabled, setVoiceEnabled] = useState(true);
707
707
  const [voiceProvider, setVoiceProvider] = useState("auto");
708
- const [voiceModel, setVoiceModel] = useState("gpt-4o-realtime-preview-2024-12-17");
709
- const [voiceVisionModel, setVoiceVisionModel] = useState("gpt-4.1-mini");
708
+ const [voiceModel, setVoiceModel] = useState("gpt-audio-1.5");
709
+ const [voiceVisionModel, setVoiceVisionModel] = useState("gpt-4.1-nano");
710
710
  const [voiceId, setVoiceId] = useState("alloy");
711
711
  const [voiceTurnDetection, setVoiceTurnDetection] = useState("server_vad");
712
712
  const [voiceFallbackMode, setVoiceFallbackMode] = useState("browser");
@@ -714,7 +714,7 @@ function App() {
714
714
  const [openaiRealtimeApiKey, setOpenaiRealtimeApiKey] = useState("");
715
715
  const [azureOpenaiRealtimeEndpoint, setAzureOpenaiRealtimeEndpoint] = useState("");
716
716
  const [azureOpenaiRealtimeApiKey, setAzureOpenaiRealtimeApiKey] = useState("");
717
- const [azureOpenaiRealtimeDeployment, setAzureOpenaiRealtimeDeployment] = useState("gpt-4o-realtime-preview");
717
+ const [azureOpenaiRealtimeDeployment, setAzureOpenaiRealtimeDeployment] = useState("gpt-audio-1.5");
718
718
  const [voiceProviders, setVoiceProviders] = useState([
719
719
  {
720
720
  id: Date.now(),
@@ -727,11 +727,11 @@ function App() {
727
727
  ]);
728
728
 
729
729
  const VOICE_PROVIDER_MODEL_DEFAULTS = {
730
- openai: { model: "gpt-4o-realtime-preview-2024-12-17", visionModel: "gpt-4.1-mini" },
731
- azure: { model: "gpt-4o-realtime-preview-2024-12-17", visionModel: "gpt-4.1-mini" },
732
- claude: { model: "claude-3-7-sonnet-latest", visionModel: "claude-3-7-sonnet-latest" },
733
- gemini: { model: "gemini-2.5-pro", visionModel: "gemini-2.5-flash" },
734
- fallback: { model: "", visionModel: "" },
730
+ openai: { model: "gpt-audio-1.5", visionModel: "gpt-4.1-nano", models: ["gpt-audio-1.5", "gpt-realtime-1.5", "gpt-4o-realtime-preview-2024-12-17"], visionModels: ["gpt-4.1-nano", "gpt-4.1-mini", "gpt-4.1"] },
731
+ azure: { model: "gpt-audio-1.5", visionModel: "gpt-4.1-nano", models: ["gpt-audio-1.5", "gpt-realtime-1.5", "gpt-4o-realtime-preview"], visionModels: ["gpt-4.1-nano", "gpt-4.1-mini", "gpt-4.1"] },
732
+ claude: { model: "claude-3-7-sonnet-latest", visionModel: "claude-3-7-sonnet-latest", models: ["claude-3-7-sonnet-latest", "claude-sonnet-4-20250514"], visionModels: ["claude-3-7-sonnet-latest", "claude-sonnet-4-20250514"] },
733
+ gemini: { model: "gemini-2.5-pro", visionModel: "gemini-2.5-flash", models: ["gemini-2.5-pro", "gemini-2.5-flash"], visionModels: ["gemini-2.5-flash", "gemini-2.5-pro"] },
734
+ fallback: { model: "", visionModel: "", models: [], visionModels: [] },
735
735
  };
736
736
 
737
737
  const getVoiceProviderModelDefaults = (provider) =>
@@ -748,7 +748,7 @@ function App() {
748
748
  const normalizedAzureDeployment = String(
749
749
  entry.azureDeployment ??
750
750
  fallback.azureDeployment ??
751
- (normalizedProvider === "azure" ? "gpt-4o-realtime-preview" : ""),
751
+ (normalizedProvider === "azure" ? "gpt-audio-1.5" : ""),
752
752
  ).trim();
753
753
  return {
754
754
  id: entry.id || Date.now() + Math.random(),
@@ -783,7 +783,7 @@ function App() {
783
783
  visionModel: defaults_.visionModel,
784
784
  azureDeployment:
785
785
  nextProvider === "azure"
786
- ? String(target.azureDeployment || "gpt-4o-realtime-preview")
786
+ ? String(target.azureDeployment || "gpt-audio-1.5")
787
787
  : "",
788
788
  });
789
789
  return list;
@@ -1157,10 +1157,10 @@ function App() {
1157
1157
  if (env.AZURE_OPENAI_REALTIME_API_KEY) { setAzureOpenaiRealtimeApiKey(env.AZURE_OPENAI_REALTIME_API_KEY); envLoaded = true; }
1158
1158
  if (env.AZURE_OPENAI_REALTIME_DEPLOYMENT) { setAzureOpenaiRealtimeDeployment(env.AZURE_OPENAI_REALTIME_DEPLOYMENT); envLoaded = true; }
1159
1159
  const resolvedVoiceProvider = String(env.VOICE_PROVIDER || existingVoice.provider || d.voiceProvider || voiceProvider || "openai").trim().toLowerCase();
1160
- const resolvedVoiceModel = String(env.VOICE_MODEL || existingVoice.model || d.voiceModel || voiceModel || "gpt-4o-realtime-preview-2024-12-17").trim();
1161
- const resolvedVoiceVisionModel = String(env.VOICE_VISION_MODEL || existingVoice.visionModel || d.voiceVisionModel || voiceVisionModel || "gpt-4.1-mini").trim();
1160
+ const resolvedVoiceModel = String(env.VOICE_MODEL || existingVoice.model || d.voiceModel || voiceModel || "gpt-audio-1.5").trim();
1161
+ const resolvedVoiceVisionModel = String(env.VOICE_VISION_MODEL || existingVoice.visionModel || d.voiceVisionModel || voiceVisionModel || "gpt-4.1-nano").trim();
1162
1162
  const resolvedVoiceId = String(env.VOICE_ID || existingVoice.voiceId || d.voiceId || voiceId || "alloy").trim();
1163
- const resolvedAzureDeployment = String(env.AZURE_OPENAI_REALTIME_DEPLOYMENT || existingVoice.azureDeployment || d.azureOpenaiRealtimeDeployment || azureOpenaiRealtimeDeployment || "gpt-4o-realtime-preview").trim();
1163
+ const resolvedAzureDeployment = String(env.AZURE_OPENAI_REALTIME_DEPLOYMENT || existingVoice.azureDeployment || d.azureOpenaiRealtimeDeployment || azureOpenaiRealtimeDeployment || "gpt-audio-1.5").trim();
1164
1164
  if (Array.isArray(existingVoice.providers) && existingVoice.providers.length > 0) {
1165
1165
  setVoiceProviders(
1166
1166
  normalizeVoiceProviders(existingVoice.providers, {
@@ -2521,23 +2521,25 @@ function App() {
2521
2521
 
2522
2522
  // ── Step 7: Advanced Settings ──────────────────────────────────────────────
2523
2523
 
2524
- const StepAdvanced = () => {
2525
- // Section is rendered as a real Preact component (`<${Section}/>`) so its
2526
- // useState call is properly isolated to its own fiber — safe to use here.
2527
- function Section({ id, title, defaultOpen, children }) {
2528
- const [open, setOpen] = useState(!!defaultOpen);
2529
- return html`
2530
- <div style="border:1px solid var(--border-primary);border-radius:var(--radius-sm);margin-bottom:10px;overflow:hidden">
2531
- <div style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;cursor:pointer;background:var(--bg-input);user-select:none"
2532
- onclick=${() => setOpen((v) => !v)}>
2533
- <span style="font-weight:600;font-size:0.88rem">${iconText(title)}</span>
2534
- <span style="color:var(--text-dim);font-size:0.8rem">${open ? "▲" : "▼"}</span>
2535
- </div>
2536
- ${open && html`<div style="padding:16px">${children}</div>`}
2524
+ // Section is defined at module scope so its identity is stable across
2525
+ // re-renders. Defining it inside StepAdvanced would create a new function
2526
+ // reference on every state update, causing Preact to unmount/remount the
2527
+ // component (losing open state + input focus).
2528
+ function Section({ id, title, defaultOpen, children }) {
2529
+ const [open, setOpen] = useState(!!defaultOpen);
2530
+ return html`
2531
+ <div style="border:1px solid var(--border-primary);border-radius:var(--radius-sm);margin-bottom:10px;overflow:hidden">
2532
+ <div style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;cursor:pointer;background:var(--bg-input);user-select:none"
2533
+ onclick=${() => setOpen((v) => !v)}>
2534
+ <span style="font-weight:600;font-size:0.88rem">${iconText(title)}</span>
2535
+ <span style="color:var(--text-dim);font-size:0.8rem">${open ? "▲" : "▼"}</span>
2537
2536
  </div>
2538
- `;
2539
- }
2537
+ ${open && html`<div style="padding:16px">${children}</div>`}
2538
+ </div>
2539
+ `;
2540
+ }
2540
2541
 
2542
+ const StepAdvanced = () => {
2541
2543
  return html`
2542
2544
  <h2>Advanced Settings</h2>
2543
2545
  <p class="step-desc">Fine-tune execution, model profiles, and infrastructure. Defaults are sensible — only change what you need.</p>
@@ -2876,21 +2878,73 @@ function App() {
2876
2878
  </div>
2877
2879
  <div class="form-group">
2878
2880
  <label>Model</label>
2879
- <input
2880
- type="text"
2881
- value=${providerRow.model}
2882
- oninput=${(e) => updateVoiceProviderRow(idx, "model", e.target.value)}
2883
- placeholder="Provider model"
2884
- />
2881
+ ${(() => {
2882
+ const defaults_ = getVoiceProviderModelDefaults(providerRow.provider);
2883
+ const knownModels = defaults_.models || [];
2884
+ const isCustom = knownModels.length > 0 && !knownModels.includes(providerRow.model) && providerRow.model !== "";
2885
+ return html`
2886
+ <select value=${isCustom ? "custom" : providerRow.model}
2887
+ onchange=${(e) => {
2888
+ if (e.target.value === "custom") {
2889
+ updateVoiceProviderRow(idx, "model", "");
2890
+ } else {
2891
+ updateVoiceProviderRow(idx, "model", e.target.value);
2892
+ }
2893
+ }}>
2894
+ ${knownModels.map((m) => html`<option value=${m}>${m}</option>`)}
2895
+ <option value="custom">custom...</option>
2896
+ </select>
2897
+ ${(isCustom || (knownModels.length > 0 && !knownModels.includes(providerRow.model))) && html`
2898
+ <input type="text" value=${providerRow.model}
2899
+ oninput=${(e) => updateVoiceProviderRow(idx, "model", e.target.value)}
2900
+ placeholder="Enter custom model slug..."
2901
+ style="margin-top:4px"
2902
+ />
2903
+ `}
2904
+ ${knownModels.length === 0 && html`
2905
+ <input type="text" value=${providerRow.model}
2906
+ oninput=${(e) => updateVoiceProviderRow(idx, "model", e.target.value)}
2907
+ placeholder="Provider model"
2908
+ style="margin-top:4px"
2909
+ />
2910
+ `}
2911
+ `;
2912
+ })()}
2885
2913
  </div>
2886
2914
  <div class="form-group">
2887
2915
  <label>Vision Model</label>
2888
- <input
2889
- type="text"
2890
- value=${providerRow.visionModel}
2891
- oninput=${(e) => updateVoiceProviderRow(idx, "visionModel", e.target.value)}
2892
- placeholder="Provider vision model"
2893
- />
2916
+ ${(() => {
2917
+ const defaults_ = getVoiceProviderModelDefaults(providerRow.provider);
2918
+ const knownVisionModels = defaults_.visionModels || [];
2919
+ const isCustomVision = knownVisionModels.length > 0 && !knownVisionModels.includes(providerRow.visionModel) && providerRow.visionModel !== "";
2920
+ return html`
2921
+ <select value=${isCustomVision ? "custom" : providerRow.visionModel}
2922
+ onchange=${(e) => {
2923
+ if (e.target.value === "custom") {
2924
+ updateVoiceProviderRow(idx, "visionModel", "");
2925
+ } else {
2926
+ updateVoiceProviderRow(idx, "visionModel", e.target.value);
2927
+ }
2928
+ }}>
2929
+ ${knownVisionModels.map((m) => html`<option value=${m}>${m}</option>`)}
2930
+ <option value="custom">custom...</option>
2931
+ </select>
2932
+ ${(isCustomVision || (knownVisionModels.length > 0 && !knownVisionModels.includes(providerRow.visionModel))) && html`
2933
+ <input type="text" value=${providerRow.visionModel}
2934
+ oninput=${(e) => updateVoiceProviderRow(idx, "visionModel", e.target.value)}
2935
+ placeholder="Enter custom vision model slug..."
2936
+ style="margin-top:4px"
2937
+ />
2938
+ `}
2939
+ ${knownVisionModels.length === 0 && html`
2940
+ <input type="text" value=${providerRow.visionModel}
2941
+ oninput=${(e) => updateVoiceProviderRow(idx, "visionModel", e.target.value)}
2942
+ placeholder="Provider vision model"
2943
+ style="margin-top:4px"
2944
+ />
2945
+ `}
2946
+ `;
2947
+ })()}
2894
2948
  </div>
2895
2949
  <div class="form-group">
2896
2950
  <label>Voice Persona</label>
@@ -2915,7 +2969,7 @@ function App() {
2915
2969
  type="text"
2916
2970
  value=${providerRow.azureDeployment || ""}
2917
2971
  oninput=${(e) => updateVoiceProviderRow(idx, "azureDeployment", e.target.value)}
2918
- placeholder="gpt-4o-realtime-preview"
2972
+ placeholder="gpt-audio-1.5"
2919
2973
  />
2920
2974
  </div>
2921
2975
  `}
@@ -50,7 +50,7 @@ async function getGoogleGenAI() {
50
50
 
51
51
  // ── Constants ───────────────────────────────────────────────────────────────
52
52
 
53
- const OPENAI_REALTIME_MODEL = "gpt-realtime-1.5";
53
+ const OPENAI_REALTIME_MODEL = "gpt-audio-1.5";
54
54
  const GEMINI_LIVE_MODEL = "gemini-2.5-flash-native-audio-preview-12-2025";
55
55
 
56
56
  const SDK_PROVIDERS = Object.freeze({
@@ -316,7 +316,7 @@ export async function connectRealtimeSession(sessionHandle, config = {}) {
316
316
  }
317
317
  const endpoint = String(config.azureEndpoint || "").trim().replace(/\/+$/, "");
318
318
  const deployment = String(
319
- config.azureDeployment || "gpt-realtime-1.5",
319
+ config.azureDeployment || "gpt-audio-1.5",
320
320
  ).trim();
321
321
  connectOpts.apiKey = credential;
322
322
  connectOpts.url = `${endpoint}/openai/realtime?api-version=2025-04-01-preview&deployment=${deployment}`;
package/voice-relay.mjs CHANGED
@@ -22,9 +22,9 @@ let _configLoadedAt = 0; // timestamp of last config load
22
22
  const CONFIG_TTL_MS = 30_000; // re-read config every 30s
23
23
 
24
24
  const OPENAI_REALTIME_URL = "https://api.openai.com/v1/realtime";
25
- const OPENAI_REALTIME_MODEL = "gpt-realtime-1.5"; // Released 2026-02-23; replaces gpt-4o-realtime-preview
25
+ const OPENAI_REALTIME_MODEL = "gpt-audio-1.5";
26
26
  const OPENAI_RESPONSES_URL = "https://api.openai.com/v1/responses";
27
- const OPENAI_DEFAULT_VISION_MODEL = "gpt-4.1-mini";
27
+ const OPENAI_DEFAULT_VISION_MODEL = "gpt-4.1-nano";
28
28
 
29
29
  const AZURE_API_VERSION = "2025-04-01-preview";
30
30
  const ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages";