@chrysb/alphaclaw 0.6.0-beta.1 → 0.6.0-beta.3

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 (28) hide show
  1. package/lib/public/js/components/agents-tab/agent-bindings-section/channel-item-trailing.js +203 -0
  2. package/lib/public/js/components/agents-tab/agent-bindings-section/helpers.js +10 -12
  3. package/lib/public/js/components/agents-tab/agent-bindings-section/index.js +19 -287
  4. package/lib/public/js/components/agents-tab/agent-bindings-section/use-agent-bindings.js +1 -1
  5. package/lib/public/js/components/agents-tab/agent-bindings-section/use-channel-items.js +211 -0
  6. package/lib/public/js/components/agents-tab/agent-pairing-section.js +17 -4
  7. package/lib/public/js/components/agents-tab/create-channel-modal.js +29 -6
  8. package/lib/public/js/components/channels.js +19 -14
  9. package/lib/public/js/components/models-tab/provider-auth-card.js +18 -1
  10. package/lib/public/js/components/models-tab/use-models.js +15 -8
  11. package/lib/public/js/lib/channel-accounts.js +20 -0
  12. package/lib/public/js/lib/model-config.js +8 -4
  13. package/lib/server/agents/agents.js +207 -0
  14. package/lib/server/agents/bindings.js +74 -0
  15. package/lib/server/agents/channels.js +674 -0
  16. package/lib/server/agents/service.js +28 -1458
  17. package/lib/server/agents/shared.js +631 -0
  18. package/lib/server/constants.js +6 -0
  19. package/lib/server/db/usage/pricing.js +1 -0
  20. package/lib/server/openclaw-config.js +13 -0
  21. package/lib/server/routes/models.js +12 -1
  22. package/lib/server/routes/pairings.js +29 -3
  23. package/lib/server/routes/system.js +1 -6
  24. package/lib/server/routes/telegram.js +34 -16
  25. package/lib/server/telegram-workspace.js +22 -7
  26. package/lib/server/topic-registry.js +1 -4
  27. package/lib/server/utils/channels.js +13 -0
  28. package/package.json +1 -1
@@ -0,0 +1,211 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import {
5
+ canAgentBindAccount,
6
+ getChannelItemSortRank,
7
+ getResolvedAccountStatusInfo,
8
+ isImplicitDefaultAccount,
9
+ resolveChannelAccountLabel,
10
+ } from "./helpers.js";
11
+
12
+ const html = htm.bind(h);
13
+
14
+ export const useChannelItems = ({
15
+ agentId = "",
16
+ agentNameMap = new Map(),
17
+ channelStatus = {},
18
+ configuredChannelMap = new Map(),
19
+ configuredChannels = [],
20
+ defaultAgentId = "",
21
+ isDefaultAgent = false,
22
+ }) => {
23
+ const hasDiscordAccount = useMemo(() => {
24
+ const discordChannel = configuredChannelMap.get("discord");
25
+ return Array.isArray(discordChannel?.accounts) && discordChannel.accounts.length > 0;
26
+ }, [configuredChannelMap]);
27
+
28
+ const [showAssignedElsewhere, setShowAssignedElsewhere] = useState(false);
29
+
30
+ const channelItemData = useMemo(() => {
31
+ const channelOrderMap = new Map(
32
+ configuredChannels.map((entry, index) => [
33
+ String(entry?.channel || "").trim(),
34
+ index,
35
+ ]),
36
+ );
37
+ const accountOrderMap = new Map(
38
+ configuredChannels.flatMap((entry) =>
39
+ (Array.isArray(entry?.accounts) ? entry.accounts : []).map(
40
+ (account, accountIndex) => [
41
+ `${String(entry?.channel || "").trim()}:${String(account?.id || "").trim() || "default"}`,
42
+ accountIndex,
43
+ ],
44
+ ),
45
+ ),
46
+ );
47
+ const channelIds = Array.from(
48
+ new Set([
49
+ ...configuredChannels.map((entry) => String(entry.channel || "").trim()),
50
+ ]),
51
+ ).filter(Boolean);
52
+
53
+ return channelIds
54
+ .flatMap((channelId) => {
55
+ const configuredChannel = configuredChannelMap.get(channelId);
56
+ const statusInfo = channelStatus?.[channelId] || null;
57
+ const accounts = Array.isArray(configuredChannel?.accounts)
58
+ ? configuredChannel.accounts
59
+ : [];
60
+
61
+ if (!configuredChannel && !statusInfo) return [];
62
+
63
+ return accounts.map((account) => {
64
+ const accountId = String(account?.id || "").trim() || "default";
65
+ const boundAgentId = String(account?.boundAgentId || "").trim();
66
+ const accountStatusInfo = getResolvedAccountStatusInfo({
67
+ account,
68
+ statusInfo,
69
+ accountId,
70
+ });
71
+ const isImplicitDefaultOwned =
72
+ isDefaultAgent &&
73
+ isImplicitDefaultAccount({ accountId, boundAgentId });
74
+ const isOwned = boundAgentId === agentId || isImplicitDefaultOwned;
75
+ const isImplicitDefaultElsewhere =
76
+ !isDefaultAgent &&
77
+ isImplicitDefaultAccount({ accountId, boundAgentId });
78
+ const isAvailable = canAgentBindAccount({
79
+ accountId,
80
+ boundAgentId,
81
+ agentId,
82
+ isDefaultAgent,
83
+ });
84
+ const ownerAgentId =
85
+ boundAgentId ||
86
+ (isImplicitDefaultAccount({ accountId, boundAgentId })
87
+ ? defaultAgentId
88
+ : "");
89
+ const ownerAgentName = String(
90
+ agentNameMap.get(ownerAgentId) || ownerAgentId || "",
91
+ ).trim();
92
+ const canNavigateToOwnerAgent =
93
+ !!ownerAgentId && ownerAgentId !== agentId && !!ownerAgentName;
94
+ const canOpenWorkspace =
95
+ channelId === "telegram" &&
96
+ isOwned &&
97
+ accountStatusInfo?.status === "paired";
98
+
99
+ const accountData = {
100
+ id: accountId,
101
+ provider: channelId,
102
+ name: resolveChannelAccountLabel({ channelId, account }),
103
+ rawName: String(account?.name || "").trim(),
104
+ ownerAgentId,
105
+ ownerAgentName,
106
+ boundAgentId,
107
+ isOwned,
108
+ envKey: String(account?.envKey || "").trim(),
109
+ token: String(account?.token || "").trim(),
110
+ isAvailable,
111
+ isBoundElsewhere:
112
+ !isOwned &&
113
+ (!isAvailable || isImplicitDefaultElsewhere || !!ownerAgentId),
114
+ };
115
+
116
+ return {
117
+ id: `${channelId}:${accountId}`,
118
+ channel: channelId,
119
+ accountId,
120
+ channelOrder: Number(channelOrderMap.get(channelId) ?? 9999),
121
+ accountOrder: Number(
122
+ accountOrderMap.get(`${channelId}:${accountId}`) ?? 9999,
123
+ ),
124
+ label: resolveChannelAccountLabel({ channelId, account }),
125
+ isAwaitingPairing: accountStatusInfo?.status !== "paired",
126
+ canOpenWorkspace,
127
+ canNavigateToOwnerAgent,
128
+ ownerAgentId,
129
+ ownerAgentName,
130
+ accountStatusInfo,
131
+ accountData,
132
+ isOwned,
133
+ isAvailable,
134
+ dimmedLabel: accountData.isBoundElsewhere,
135
+ isBoundElsewhere: accountData.isBoundElsewhere,
136
+ };
137
+ });
138
+ })
139
+ .filter(Boolean)
140
+ .sort((a, b) => {
141
+ const rankDiff = getChannelItemSortRank(a) - getChannelItemSortRank(b);
142
+ if (rankDiff !== 0) return rankDiff;
143
+ const channelOrderDiff =
144
+ Number(a?.channelOrder ?? 9999) - Number(b?.channelOrder ?? 9999);
145
+ if (channelOrderDiff !== 0) return channelOrderDiff;
146
+ const accountOrderDiff =
147
+ Number(a?.accountOrder ?? 9999) - Number(b?.accountOrder ?? 9999);
148
+ if (accountOrderDiff !== 0) return accountOrderDiff;
149
+ return String(a?.label || "").localeCompare(String(b?.label || ""));
150
+ });
151
+ }, [
152
+ agentId,
153
+ agentNameMap,
154
+ channelStatus,
155
+ configuredChannelMap,
156
+ configuredChannels,
157
+ defaultAgentId,
158
+ isDefaultAgent,
159
+ ]);
160
+
161
+ const visibleChannelItems = channelItemData.filter(
162
+ (item) => !item?.isBoundElsewhere,
163
+ );
164
+ const assignedElsewhereItems = channelItemData.filter(
165
+ (item) => !!item?.isBoundElsewhere,
166
+ );
167
+
168
+ useEffect(() => {
169
+ if (assignedElsewhereItems.length === 0) {
170
+ setShowAssignedElsewhere(false);
171
+ return;
172
+ }
173
+ if (visibleChannelItems.length === 0) {
174
+ setShowAssignedElsewhere(true);
175
+ }
176
+ }, [agentId, assignedElsewhereItems.length, visibleChannelItems.length]);
177
+
178
+ const mergedChannelItems = useMemo(() => {
179
+ const baseItems = [...visibleChannelItems];
180
+ if (assignedElsewhereItems.length === 0) return baseItems;
181
+ baseItems.push({
182
+ id: "__assigned_elsewhere_toggle",
183
+ label: html`
184
+ <span class="inline-flex items-center gap-1.5">
185
+ <span class=${`arrow inline-block ${showAssignedElsewhere ? "" : "-rotate-90"}`}>▼</span>
186
+ <span>Assigned elsewhere</span>
187
+ </span>
188
+ `,
189
+ labelClassName: "text-xs",
190
+ clickable: true,
191
+ onClick: () => setShowAssignedElsewhere((current) => !current),
192
+ dimmedLabel: true,
193
+ trailing: html`
194
+ <span class="inline-flex items-center gap-1.5 text-gray-500">
195
+ <span class="text-[11px] px-2 py-0.5 rounded-full border border-border">
196
+ ${assignedElsewhereItems.length}
197
+ </span>
198
+ </span>
199
+ `,
200
+ });
201
+ if (showAssignedElsewhere) {
202
+ baseItems.push(...assignedElsewhereItems);
203
+ }
204
+ return baseItems;
205
+ }, [assignedElsewhereItems, showAssignedElsewhere, visibleChannelItems]);
206
+
207
+ return {
208
+ hasDiscordAccount,
209
+ mergedChannelItems,
210
+ };
211
+ };
@@ -40,6 +40,7 @@ export const AgentPairingSection = ({ agent = {} }) => {
40
40
  const [loadingBindings, setLoadingBindings] = useState(true);
41
41
  const [pairingStatusRefreshing, setPairingStatusRefreshing] = useState(false);
42
42
  const pairingRefreshTimerRef = useRef(null);
43
+ const pairingDelayedRefreshTimerRefs = useRef([]);
43
44
  const agentId = String(agent?.id || "").trim();
44
45
  const isDefaultAgent = !!agent?.default;
45
46
 
@@ -82,6 +83,10 @@ export const AgentPairingSection = ({ agent = {} }) => {
82
83
  if (pairingRefreshTimerRef.current) {
83
84
  clearTimeout(pairingRefreshTimerRef.current);
84
85
  }
86
+ for (const timerId of pairingDelayedRefreshTimerRefs.current) {
87
+ clearTimeout(timerId);
88
+ }
89
+ pairingDelayedRefreshTimerRefs.current = [];
85
90
  },
86
91
  [],
87
92
  );
@@ -217,19 +222,27 @@ export const AgentPairingSection = ({ agent = {} }) => {
217
222
  setPairingStatusRefreshing(false);
218
223
  pairingRefreshTimerRef.current = null;
219
224
  }, 2800);
225
+ for (const timerId of pairingDelayedRefreshTimerRefs.current) {
226
+ clearTimeout(timerId);
227
+ }
228
+ pairingDelayedRefreshTimerRefs.current = [];
220
229
  const refresh = () => {
221
230
  pairingsPoll.refresh();
222
231
  loadBindings();
223
232
  announcePairingsChanged(agentId);
224
233
  };
225
234
  refresh();
226
- setTimeout(refresh, 500);
227
- setTimeout(refresh, 2000);
235
+ pairingDelayedRefreshTimerRefs.current.push(setTimeout(refresh, 500));
236
+ pairingDelayedRefreshTimerRefs.current.push(setTimeout(refresh, 2000));
228
237
  }, [agentId, loadBindings, pairingsPoll]);
229
238
 
230
239
  const handleApprove = async (id, channel, accountId = "") => {
231
- await approvePairing(id, channel, accountId);
232
- refreshAfterPairingAction();
240
+ try {
241
+ await approvePairing(id, channel, accountId);
242
+ refreshAfterPairingAction();
243
+ } catch (err) {
244
+ showToast(err.message || "Could not approve pairing", "error");
245
+ }
233
246
  };
234
247
 
235
248
  const handleReject = async (id, channel, accountId = "") => {
@@ -63,9 +63,19 @@ export const CreateChannelModal = ({
63
63
  ? initialProvider
64
64
  : ALL_CHANNELS[0] || "telegram";
65
65
  const providerLabel = getChannelMeta(nextProvider).label || "Channel";
66
+ const nextSelectedChannel =
67
+ existingChannels.find(
68
+ (entry) =>
69
+ String(entry?.channel || "").trim() === String(nextProvider || "").trim(),
70
+ ) || null;
71
+ const nextProviderHasAccounts =
72
+ Array.isArray(nextSelectedChannel?.accounts)
73
+ && nextSelectedChannel.accounts.length > 0;
66
74
  const nextName = isEditMode
67
75
  ? String(account?.name || "").trim() || providerLabel
68
- : providerLabel;
76
+ : nextProviderHasAccounts
77
+ ? ""
78
+ : providerLabel;
69
79
  const nextAgentId = isEditMode
70
80
  ? String(account?.ownerAgentId || "").trim()
71
81
  || String(initialAgentId || "").trim()
@@ -85,12 +95,16 @@ export const CreateChannelModal = ({
85
95
  setAgentId(nextAgentId);
86
96
  setError("");
87
97
  setNameEditedManually(isEditMode);
88
- }, [visible, initialAgentId, initialProvider, agents, isEditMode, account]);
98
+ }, [
99
+ visible,
100
+ initialAgentId,
101
+ initialProvider,
102
+ agents,
103
+ existingChannels,
104
+ isEditMode,
105
+ account,
106
+ ]);
89
107
 
90
- useEffect(() => {
91
- if (nameEditedManually) return;
92
- setName(getChannelMeta(provider).label || "Channel");
93
- }, [provider, nameEditedManually]);
94
108
  const selectedChannel = useMemo(
95
109
  () =>
96
110
  existingChannels.find(
@@ -103,6 +117,15 @@ export const CreateChannelModal = ({
103
117
  () => Array.isArray(selectedChannel?.accounts) && selectedChannel.accounts.length > 0,
104
118
  [selectedChannel],
105
119
  );
120
+ useEffect(() => {
121
+ if (nameEditedManually) return;
122
+ const providerLabel = getChannelMeta(provider).label || "Channel";
123
+ if (!isEditMode && providerHasAccounts) {
124
+ setName("");
125
+ return;
126
+ }
127
+ setName(providerLabel);
128
+ }, [provider, providerHasAccounts, nameEditedManually, isEditMode]);
106
129
  const isSingleAccountProvider = String(provider || "").trim() === "discord";
107
130
 
108
131
  const accountId = useMemo(() => {
@@ -16,6 +16,10 @@ import {
16
16
  fetchChannelAccounts,
17
17
  updateChannelAccount,
18
18
  } from "../lib/api.js";
19
+ import {
20
+ isImplicitDefaultAccount,
21
+ resolveChannelAccountLabel,
22
+ } from "../lib/channel-accounts.js";
19
23
  import { createChannelAccountWithProgress } from "../lib/channel-create-operation.js";
20
24
  import { CreateChannelModal } from "./agents-tab/create-channel-modal.js";
21
25
  import { showToast } from "./toast.js";
@@ -40,18 +44,6 @@ const getChannelMeta = (channelId = "") => {
40
44
  );
41
45
  };
42
46
 
43
- const resolveChannelAccountLabel = ({ channelId, account = {} }) => {
44
- const providerLabel = getChannelMeta(channelId).label || "Channel";
45
- const configuredName = String(account?.name || "").trim();
46
- if (configuredName) return configuredName;
47
- const accountId = String(account?.id || "").trim();
48
- if (!accountId || accountId === "default") return providerLabel;
49
- return `${providerLabel} ${accountId}`;
50
- };
51
-
52
- const isImplicitDefaultAccount = ({ accountId, boundAgentId }) =>
53
- String(accountId || "").trim() === "default" &&
54
- !String(boundAgentId || "").trim();
55
47
  const announceRestartRequired = () =>
56
48
  window.dispatchEvent(new CustomEvent("alphaclaw:restart-required"));
57
49
 
@@ -60,6 +52,7 @@ export const ChannelsCard = ({
60
52
  items = [],
61
53
  loadingLabel = "Loading...",
62
54
  actions = null,
55
+ renderItem = null,
63
56
  }) => html`
64
57
  <div class="bg-surface border border-border rounded-xl p-4">
65
58
  <div class="flex items-center justify-between gap-3 mb-3">
@@ -71,6 +64,10 @@ export const ChannelsCard = ({
71
64
  ? items.map((item) => {
72
65
  const channelMeta = getChannelMeta(item.channel || item.id);
73
66
  const clickable = !!item.clickable;
67
+ const customItem = renderItem
68
+ ? renderItem({ item, channelMeta, clickable })
69
+ : null;
70
+ if (customItem) return customItem;
74
71
  return html`
75
72
  <div
76
73
  key=${item.id || item.channel}
@@ -342,7 +339,11 @@ export const Channels = ({
342
339
  const accountData = {
343
340
  id: accountId,
344
341
  provider: channelId,
345
- name: resolveChannelAccountLabel({ channelId, account }),
342
+ name: resolveChannelAccountLabel({
343
+ channelId,
344
+ account,
345
+ providerLabel: getChannelMeta(channelId).label || "Channel",
346
+ }),
346
347
  ownerAgentId,
347
348
  envKey: String(account?.envKey || "").trim(),
348
349
  token: String(account?.token || "").trim(),
@@ -422,7 +423,11 @@ export const Channels = ({
422
423
  accountOrder: Number(
423
424
  accountOrderMap.get(`${channelId}:${accountId}`) ?? 9999,
424
425
  ),
425
- label: resolveChannelAccountLabel({ channelId, account }),
426
+ label: resolveChannelAccountLabel({
427
+ channelId,
428
+ account,
429
+ providerLabel: getChannelMeta(channelId).label || "Channel",
430
+ }),
426
431
  isAwaitingPairing: accountStatus !== "paired",
427
432
  detailText: isClickable ? "Workspace" : "",
428
433
  detailChevron: isClickable,
@@ -100,6 +100,9 @@ const resolveProfileId = (mode, provider) => {
100
100
  return `${p}:${mode.profileSuffix || "default"}`;
101
101
  };
102
102
 
103
+ const getCredentialValue = (value) =>
104
+ String(value?.key || value?.token || value?.access || "").trim();
105
+
103
106
  const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
104
107
  const [authStarted, setAuthStarted] = useState(false);
105
108
  const [authWaiting, setAuthWaiting] = useState(false);
@@ -285,6 +288,18 @@ export const ProviderAuthCard = ({
285
288
 
286
289
  const effectiveOrder = getEffectiveOrder(provider);
287
290
  const activeProfileId = effectiveOrder?.[0] || null;
291
+ const savedOrder = authOrder[provider] || null;
292
+
293
+ const hasUnsavedProfileChanges = credentialModes.some((mode) => {
294
+ const profileId = resolveProfileId(mode, provider);
295
+ const savedValue = authProfiles.find((p) => p.id === profileId) || null;
296
+ const draftValue = getProfileValue(profileId);
297
+ return getCredentialValue(draftValue) !== getCredentialValue(savedValue);
298
+ });
299
+
300
+ const hasUnsavedOrderChanges =
301
+ JSON.stringify(effectiveOrder || null) !== JSON.stringify(savedOrder);
302
+ const hasUnsavedChanges = hasUnsavedProfileChanges || hasUnsavedOrderChanges;
288
303
 
289
304
  const isConnected =
290
305
  credentialModes.some((mode) => {
@@ -306,7 +321,9 @@ export const ProviderAuthCard = ({
306
321
  <h3 class="card-label">${meta.label}</h3>
307
322
  ${showsInlineOauthStatus && credentialModes.length === 0
308
323
  ? null
309
- : isConnected
324
+ : hasUnsavedChanges
325
+ ? html`<${Badge} tone="warning">Unsaved</${Badge}>`
326
+ : isConnected
310
327
  ? html`<${Badge} tone="success">Connected</${Badge}>`
311
328
  : html`<${Badge} tone="warning">Not configured</${Badge}>`}
312
329
  </div>
@@ -9,6 +9,8 @@ import {
9
9
  import { showToast } from "../toast.js";
10
10
 
11
11
  let kModelsTabCache = null;
12
+ const getCredentialValue = (value) =>
13
+ String(value?.key || value?.token || value?.access || "").trim();
12
14
 
13
15
  export const useModels = (agentId) => {
14
16
  const isScoped = !!agentId;
@@ -99,10 +101,7 @@ export const useModels = (agentId) => {
99
101
  const hasProfileChanges = Object.entries(profileEdits).some(
100
102
  ([id, cred]) => {
101
103
  const existing = authProfiles.find((p) => p.id === id);
102
- const newVal = cred?.key || cred?.token || cred?.access || "";
103
- const oldVal =
104
- existing?.key || existing?.token || existing?.access || "";
105
- return newVal !== oldVal && newVal !== "";
104
+ return getCredentialValue(cred) !== getCredentialValue(existing);
106
105
  },
107
106
  );
108
107
  const hasOrderChanges = Object.entries(orderEdits).some(
@@ -199,9 +198,9 @@ export const useModels = (agentId) => {
199
198
  setSaving(true);
200
199
  try {
201
200
  const changedProfiles = Object.entries(profileEdits)
202
- .filter(([, cred]) => {
203
- const val = cred?.key || cred?.token || cred?.access || "";
204
- return val !== "";
201
+ .filter(([id, cred]) => {
202
+ const existing = authProfiles.find((p) => p.id === id);
203
+ return getCredentialValue(cred) !== getCredentialValue(existing);
205
204
  })
206
205
  .map(([id, cred]) => ({ id, ...cred }));
207
206
 
@@ -225,7 +224,15 @@ export const useModels = (agentId) => {
225
224
  } finally {
226
225
  setSaving(false);
227
226
  }
228
- }, [saving, primary, configuredModels, profileEdits, orderEdits, refresh]);
227
+ }, [
228
+ saving,
229
+ primary,
230
+ configuredModels,
231
+ profileEdits,
232
+ orderEdits,
233
+ authProfiles,
234
+ refresh,
235
+ ]);
229
236
 
230
237
  const refreshCodexStatus = useCallback(async () => {
231
238
  try {
@@ -0,0 +1,20 @@
1
+ export const resolveChannelAccountLabel = ({
2
+ channelId,
3
+ account = {},
4
+ providerLabel = "",
5
+ }) => {
6
+ const fallbackProviderLabel = channelId
7
+ ? channelId.charAt(0).toUpperCase() + channelId.slice(1)
8
+ : "Channel";
9
+ const resolvedProviderLabel = String(providerLabel || "").trim()
10
+ || fallbackProviderLabel;
11
+ const configuredName = String(account?.name || "").trim();
12
+ if (configuredName) return configuredName;
13
+ const accountId = String(account?.id || "").trim();
14
+ if (!accountId || accountId === "default") return resolvedProviderLabel;
15
+ return `${resolvedProviderLabel} ${accountId}`;
16
+ };
17
+
18
+ export const isImplicitDefaultAccount = ({ accountId, boundAgentId }) =>
19
+ String(accountId || "").trim() === "default" &&
20
+ !String(boundAgentId || "").trim();
@@ -11,19 +11,23 @@ export const getAuthProviderFromModelProvider = (provider) => {
11
11
  export const kFeaturedModelDefs = [
12
12
  {
13
13
  label: "Opus 4.6",
14
- preferredKeys: ["anthropic/claude-opus-4-6", "anthropic/claude-opus-4-5"],
14
+ preferredKeys: ["anthropic/claude-opus-4-6"],
15
15
  },
16
16
  {
17
17
  label: "Sonnet 4.6",
18
- preferredKeys: ["anthropic/claude-sonnet-4-6", "anthropic/claude-sonnet-4-5"],
18
+ preferredKeys: ["anthropic/claude-sonnet-4-6"],
19
19
  },
20
20
  {
21
21
  label: "Codex 5.3",
22
- preferredKeys: ["openai-codex/gpt-5.3-codex", "openai-codex/gpt-5.2-codex"],
22
+ preferredKeys: ["openai-codex/gpt-5.3-codex"],
23
+ },
24
+ {
25
+ label: "GPT-5.4",
26
+ preferredKeys: ["openai-codex/gpt-5.4"],
23
27
  },
24
28
  {
25
29
  label: "Gemini 3.1 Pro",
26
- preferredKeys: ["google/gemini-3.1-pro-preview", "google/gemini-3-flash-preview"],
30
+ preferredKeys: ["google/gemini-3.1-pro-preview"],
27
31
  },
28
32
  ];
29
33