zardbot-telegram 1.0.0 → 1.0.1
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/dist/bot/commands/voice.js +4 -9
- package/dist/bot/handlers/model.js +208 -38
- package/dist/i18n/de.js +9 -2
- package/dist/i18n/en.js +9 -2
- package/dist/i18n/es.js +9 -2
- package/dist/i18n/fr.js +9 -2
- package/dist/i18n/ru.js +9 -2
- package/dist/i18n/zh.js +9 -2
- package/dist/model/manager.js +174 -0
- package/package.json +1 -1
|
@@ -108,15 +108,10 @@ export async function handleVoiceCallback(ctx) {
|
|
|
108
108
|
if (callbackData.action === "select" && callbackData.voice) {
|
|
109
109
|
setCurrentTtsVoice(callbackData.voice);
|
|
110
110
|
setTtsEnabled(true);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
await ctx.editMessageText(t("tts.menu_current", { voice: currentVoice || "default" }), {
|
|
116
|
-
reply_markup: {
|
|
117
|
-
inline_keyboard: buildVoiceKeyboard(voices, 0, currentVoice),
|
|
118
|
-
},
|
|
119
|
-
});
|
|
111
|
+
const msg = ctx.callbackQuery?.message;
|
|
112
|
+
if (msg && "message_id" in msg) {
|
|
113
|
+
await ctx.api.editMessageText(ctx.chat.id, msg.message_id, t("tts.voice_changed", { voice: callbackData.voice }), { reply_markup: undefined });
|
|
114
|
+
}
|
|
120
115
|
return true;
|
|
121
116
|
}
|
|
122
117
|
if (callbackData.action === "page" && typeof callbackData.page === "number") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { InlineKeyboard } from "grammy";
|
|
2
|
-
import { selectModel, fetchCurrentModel, getModelSelectionLists } from "../../model/manager.js";
|
|
2
|
+
import { selectModel, fetchCurrentModel, getModelSelectionLists, getAllProviderIDs, getModelsForProvider } from "../../model/manager.js";
|
|
3
3
|
import { formatModelForDisplay } from "../../model/types.js";
|
|
4
4
|
import { formatVariantForButton } from "../../variant/manager.js";
|
|
5
5
|
import { logger } from "../../utils/logger.js";
|
|
@@ -20,11 +20,68 @@ function buildModelSelectionMenuText(modelLists) {
|
|
|
20
20
|
}
|
|
21
21
|
return lines.join("\n");
|
|
22
22
|
}
|
|
23
|
+
function buildProviderSelectionMenuText() {
|
|
24
|
+
return t("model.menu.select_provider");
|
|
25
|
+
}
|
|
26
|
+
function buildProviderModelSelectionMenuText(providerID) {
|
|
27
|
+
return t("model.menu.select_model_for_provider", { provider: providerID });
|
|
28
|
+
}
|
|
23
29
|
/**
|
|
24
30
|
* Handle model selection callback
|
|
25
31
|
* @param ctx grammY context
|
|
26
32
|
* @returns true if handled, false otherwise
|
|
27
33
|
*/
|
|
34
|
+
/**
|
|
35
|
+
* Build inline keyboard with all available providers
|
|
36
|
+
* @returns InlineKeyboard with provider selection buttons
|
|
37
|
+
*/
|
|
38
|
+
export async function buildProviderSelectionMenu() {
|
|
39
|
+
const keyboard = new InlineKeyboard();
|
|
40
|
+
try {
|
|
41
|
+
const providerIDs = await getAllProviderIDs();
|
|
42
|
+
if (!providerIDs || providerIDs.length === 0) {
|
|
43
|
+
logger.warn("[ModelHandler] No providers found from any source");
|
|
44
|
+
return keyboard;
|
|
45
|
+
}
|
|
46
|
+
// Sort providers alphabetically
|
|
47
|
+
providerIDs.sort();
|
|
48
|
+
// Add provider buttons
|
|
49
|
+
for (const providerID of providerIDs) {
|
|
50
|
+
keyboard.text(providerID, `model:provider:${providerID}`).row();
|
|
51
|
+
}
|
|
52
|
+
return keyboard;
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
logger.error("[ModelHandler] Error building provider selection menu:", err);
|
|
56
|
+
return keyboard;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build inline keyboard with models for a specific provider
|
|
61
|
+
* @param providerID Provider ID to show models for
|
|
62
|
+
* @returns InlineKeyboard with model selection buttons
|
|
63
|
+
*/
|
|
64
|
+
export async function buildProviderModelSelectionMenu(providerID) {
|
|
65
|
+
const keyboard = new InlineKeyboard();
|
|
66
|
+
try {
|
|
67
|
+
const models = await getModelsForProvider(providerID);
|
|
68
|
+
if (!models || models.length === 0) {
|
|
69
|
+
logger.warn(`[ModelHandler] No models found for provider ${providerID}`);
|
|
70
|
+
return keyboard;
|
|
71
|
+
}
|
|
72
|
+
// Sort models alphabetically
|
|
73
|
+
models.sort();
|
|
74
|
+
// Add model buttons
|
|
75
|
+
for (const modelID of models) {
|
|
76
|
+
keyboard.text(modelID, `model:model:${providerID}:${modelID}`).row();
|
|
77
|
+
}
|
|
78
|
+
return keyboard;
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
logger.error("[ModelHandler] Error building model selection menu for provider " + providerID + ":", err);
|
|
82
|
+
return keyboard;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
28
85
|
export async function handleModelSelect(ctx) {
|
|
29
86
|
const callbackQuery = ctx.callbackQuery;
|
|
30
87
|
if (!callbackQuery?.data || !callbackQuery.data.startsWith("model:")) {
|
|
@@ -39,47 +96,111 @@ export async function handleModelSelect(ctx) {
|
|
|
39
96
|
if (ctx.chat) {
|
|
40
97
|
keyboardManager.initialize(ctx.api, ctx.chat.id);
|
|
41
98
|
}
|
|
42
|
-
// Parse callback data: "model:providerID:modelID"
|
|
43
99
|
const parts = callbackQuery.data.split(":");
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
100
|
+
const secondPart = parts[1];
|
|
101
|
+
// Handle "model:provider:list" - show all providers
|
|
102
|
+
if (secondPart === "provider" && parts[2] === "list") {
|
|
103
|
+
const keyboard = await buildProviderSelectionMenu();
|
|
104
|
+
if (keyboard.inline_keyboard.length === 0) {
|
|
105
|
+
await ctx.answerCallbackQuery({ text: t("model.menu.empty") }).catch(() => { });
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
await ctx.answerCallbackQuery().catch(() => { });
|
|
109
|
+
await ctx.editMessageReplyMarkup({ reply_markup: keyboard }).catch(() => { });
|
|
48
110
|
return true;
|
|
49
111
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
providerID
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const currentAgent = getStoredAgent();
|
|
65
|
-
const contextInfo = pinnedMessageManager.getContextInfo() ??
|
|
66
|
-
(pinnedMessageManager.getContextLimit() > 0
|
|
67
|
-
? { tokensUsed: 0, tokensLimit: pinnedMessageManager.getContextLimit() }
|
|
68
|
-
: null);
|
|
69
|
-
if (contextInfo) {
|
|
70
|
-
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
|
|
112
|
+
// Handle "model:provider:providerID" - show models for specific provider
|
|
113
|
+
if (secondPart === "provider" && parts.length >= 3) {
|
|
114
|
+
const providerID = parts.slice(2).join(":");
|
|
115
|
+
const keyboard = await buildProviderModelSelectionMenu(providerID);
|
|
116
|
+
if (keyboard.inline_keyboard.length === 0) {
|
|
117
|
+
await ctx.answerCallbackQuery({ text: t("model.menu.empty") }).catch(() => { });
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
// Add back button
|
|
121
|
+
keyboard.row();
|
|
122
|
+
keyboard.text(t("model.menu.back_to_providers"), "model:provider:list");
|
|
123
|
+
await ctx.answerCallbackQuery().catch(() => { });
|
|
124
|
+
await ctx.editMessageReplyMarkup({ reply_markup: keyboard }).catch(() => { });
|
|
125
|
+
return true;
|
|
71
126
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
127
|
+
// Handle "model:model:providerID:modelID" - select a specific model
|
|
128
|
+
if (secondPart === "model" && parts.length >= 4) {
|
|
129
|
+
const providerID = parts[2];
|
|
130
|
+
const modelID = parts.slice(3).join(":"); // Handle model IDs that may contain ":"
|
|
131
|
+
const modelInfo = {
|
|
132
|
+
providerID,
|
|
133
|
+
modelID,
|
|
134
|
+
variant: "default", // Reset to default when switching models
|
|
135
|
+
};
|
|
136
|
+
// Select model and persist
|
|
137
|
+
selectModel(modelInfo);
|
|
138
|
+
// Update keyboard manager state (may not be initialized if no session selected)
|
|
139
|
+
keyboardManager.updateModel(modelInfo);
|
|
140
|
+
// Refresh context limit for new model
|
|
141
|
+
await pinnedMessageManager.refreshContextLimit();
|
|
142
|
+
// Update Reply Keyboard with new model and context
|
|
143
|
+
const currentAgent = getStoredAgent();
|
|
144
|
+
const contextInfo = pinnedMessageManager.getContextInfo() ??
|
|
145
|
+
(pinnedMessageManager.getContextLimit() > 0
|
|
146
|
+
? { tokensUsed: 0, tokensLimit: pinnedMessageManager.getContextLimit() }
|
|
147
|
+
: null);
|
|
148
|
+
if (contextInfo) {
|
|
149
|
+
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
|
|
150
|
+
}
|
|
151
|
+
const variantName = formatVariantForButton(modelInfo.variant || "default");
|
|
152
|
+
const keyboard = createMainKeyboard(currentAgent, modelInfo, contextInfo ?? undefined, variantName);
|
|
153
|
+
const displayName = formatModelForDisplay(modelInfo.providerID, modelInfo.modelID);
|
|
154
|
+
clearActiveInlineMenu("model_selected");
|
|
155
|
+
// Send confirmation message with updated keyboard
|
|
156
|
+
await ctx.answerCallbackQuery({ text: t("model.changed_callback", { name: displayName }) });
|
|
157
|
+
await ctx.reply(t("model.changed_message", { name: displayName }), {
|
|
158
|
+
reply_markup: keyboard,
|
|
159
|
+
});
|
|
160
|
+
// Delete the inline menu message
|
|
161
|
+
await ctx.deleteMessage().catch(() => { });
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
// Handle legacy format "model:providerID:modelID"
|
|
165
|
+
if (parts.length >= 3) {
|
|
166
|
+
const providerID = parts[1];
|
|
167
|
+
const modelID = parts.slice(2).join(":"); // Handle model IDs that may contain ":"
|
|
168
|
+
const modelInfo = {
|
|
169
|
+
providerID,
|
|
170
|
+
modelID,
|
|
171
|
+
variant: "default", // Reset to default when switching models
|
|
172
|
+
};
|
|
173
|
+
// Select model and persist
|
|
174
|
+
selectModel(modelInfo);
|
|
175
|
+
// Update keyboard manager state (may not be initialized if no session selected)
|
|
176
|
+
keyboardManager.updateModel(modelInfo);
|
|
177
|
+
// Refresh context limit for new model
|
|
178
|
+
await pinnedMessageManager.refreshContextLimit();
|
|
179
|
+
// Update Reply Keyboard with new model and context
|
|
180
|
+
const currentAgent = getStoredAgent();
|
|
181
|
+
const contextInfo = pinnedMessageManager.getContextInfo() ??
|
|
182
|
+
(pinnedMessageManager.getContextLimit() > 0
|
|
183
|
+
? { tokensUsed: 0, tokensLimit: pinnedMessageManager.getContextLimit() }
|
|
184
|
+
: null);
|
|
185
|
+
if (contextInfo) {
|
|
186
|
+
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
|
|
187
|
+
}
|
|
188
|
+
const variantName = formatVariantForButton(modelInfo.variant || "default");
|
|
189
|
+
const keyboard = createMainKeyboard(currentAgent, modelInfo, contextInfo ?? undefined, variantName);
|
|
190
|
+
const displayName = formatModelForDisplay(modelInfo.providerID, modelInfo.modelID);
|
|
191
|
+
clearActiveInlineMenu("model_selected");
|
|
192
|
+
// Send confirmation message with updated keyboard
|
|
193
|
+
await ctx.answerCallbackQuery({ text: t("model.changed_callback", { name: displayName }) });
|
|
194
|
+
await ctx.reply(t("model.changed_message", { name: displayName }), {
|
|
195
|
+
reply_markup: keyboard,
|
|
196
|
+
});
|
|
197
|
+
// Delete the inline menu message
|
|
198
|
+
await ctx.deleteMessage().catch(() => { });
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
logger.error(`[ModelHandler] Invalid callback data format: ${callbackQuery.data}`);
|
|
202
|
+
clearActiveInlineMenu("model_select_invalid_callback");
|
|
203
|
+
await ctx.answerCallbackQuery({ text: t("model.change_error_callback") }).catch(() => { });
|
|
83
204
|
return true;
|
|
84
205
|
}
|
|
85
206
|
catch (err) {
|
|
@@ -99,6 +220,8 @@ export async function buildModelSelectionMenu(currentModel, modelLists) {
|
|
|
99
220
|
const lists = modelLists ?? (await getModelSelectionLists());
|
|
100
221
|
const favorites = lists.favorites;
|
|
101
222
|
const recent = lists.recent;
|
|
223
|
+
// Add "All Providers" button at the top
|
|
224
|
+
keyboard.text(t("model.menu.all_providers"), "model:provider:list").row();
|
|
102
225
|
if (favorites.length === 0 && recent.length === 0) {
|
|
103
226
|
logger.warn("[ModelHandler] No model choices found in favorites/recent");
|
|
104
227
|
return keyboard;
|
|
@@ -141,3 +264,50 @@ export async function showModelSelectionMenu(ctx) {
|
|
|
141
264
|
await ctx.reply(t("model.menu.error"));
|
|
142
265
|
}
|
|
143
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Show provider selection menu
|
|
269
|
+
* @param ctx grammY context
|
|
270
|
+
*/
|
|
271
|
+
export async function showProviderSelectionMenu(ctx) {
|
|
272
|
+
try {
|
|
273
|
+
const keyboard = await buildProviderSelectionMenu();
|
|
274
|
+
if (keyboard.inline_keyboard.length === 0) {
|
|
275
|
+
await ctx.reply(t("model.menu.empty"));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const text = buildProviderSelectionMenuText();
|
|
279
|
+
await replyWithInlineMenu(ctx, {
|
|
280
|
+
menuKind: "model",
|
|
281
|
+
text,
|
|
282
|
+
keyboard,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
logger.error("[ModelHandler] Error showing provider selection menu:", err);
|
|
287
|
+
await ctx.reply(t("model.menu.error"));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Show model selection menu for a specific provider
|
|
292
|
+
* @param ctx grammY context
|
|
293
|
+
* @param providerID Provider ID to show models for
|
|
294
|
+
*/
|
|
295
|
+
export async function showProviderModelSelectionMenu(ctx, providerID) {
|
|
296
|
+
try {
|
|
297
|
+
const keyboard = await buildProviderModelSelectionMenu(providerID);
|
|
298
|
+
if (keyboard.inline_keyboard.length === 0) {
|
|
299
|
+
await ctx.reply(t("model.menu.empty"));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const text = buildProviderModelSelectionMenuText(providerID);
|
|
303
|
+
await replyWithInlineMenu(ctx, {
|
|
304
|
+
menuKind: "model",
|
|
305
|
+
text,
|
|
306
|
+
keyboard,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
logger.error(`[ModelHandler] Error showing model selection menu for provider ${providerID}:`, err);
|
|
311
|
+
await ctx.reply(t("model.menu.error"));
|
|
312
|
+
}
|
|
313
|
+
}
|
package/dist/i18n/de.js
CHANGED
|
@@ -146,6 +146,13 @@ export const de = {
|
|
|
146
146
|
"model.menu.recent_title": "🕘 Zuletzt verwendet",
|
|
147
147
|
"model.menu.recent_empty": "— Leer.",
|
|
148
148
|
"model.menu.favorites_hint": "ℹ️ Füge Modelle in OpenCode CLI zu den Favoriten hinzu, damit sie oben angezeigt werden.",
|
|
149
|
+
"model.menu.select_provider": "🌐 Wählen Sie einen Anbieter:",
|
|
150
|
+
"model.menu.select_model_for_provider": "Modell für {provider} auswählen:",
|
|
151
|
+
"model.menu.all_providers": "🌐 Alle Anbieter",
|
|
152
|
+
"model.menu.back_to_providers": "⬅️ Zurück zu Anbietern",
|
|
153
|
+
"model.menu.back_to_main": "⬅️ Zurück zum Hauptmenü",
|
|
154
|
+
"model.menu.provider_source_api": "📡 API",
|
|
155
|
+
"model.menu.provider_source_config": "📝 Konfig",
|
|
149
156
|
"model.menu.error": "🔴 Modellliste konnte nicht geladen werden",
|
|
150
157
|
"variant.model_not_selected_callback": "Fehler: Modell ist nicht ausgewählt",
|
|
151
158
|
"variant.changed_callback": "Variante geändert: {name}",
|
|
@@ -251,7 +258,7 @@ export const de = {
|
|
|
251
258
|
"runtime.wizard.start": "OpenCode Telegram Bot Einrichtung.\n",
|
|
252
259
|
"runtime.wizard.saved": "Konfiguration gespeichert:\n- {envPath}\n- {settingsPath}\n",
|
|
253
260
|
"runtime.wizard.not_configured_starting": "Anwendung ist noch nicht konfiguriert. Starte Assistent...\n",
|
|
254
|
-
"runtime.wizard.tty_required": "Der interaktive Assistent erfordert ein TTY-Terminal. Führe `
|
|
261
|
+
"runtime.wizard.tty_required": "Der interaktive Assistent erfordert ein TTY-Terminal. Führe `zardbot-telegram config` in einer interaktiven Shell aus.",
|
|
255
262
|
"rename.no_session": "⚠️ Keine aktive Sitzung. Erstelle oder wähle zuerst eine Sitzung.",
|
|
256
263
|
"rename.prompt": "📝 Neuen Titel für die Sitzung eingeben:\n\nAktuell: {title}",
|
|
257
264
|
"rename.empty_title": "⚠️ Titel darf nicht leer sein.",
|
|
@@ -317,7 +324,7 @@ export const de = {
|
|
|
317
324
|
"commands.page_empty_callback": "Keine Befehle auf dieser Seite",
|
|
318
325
|
"commands.page_load_error_callback": "Diese Seite konnte nicht geladen werden. Bitte versuche es erneut.",
|
|
319
326
|
"cmd.description.rename": "Aktuelle Sitzung umbenennen",
|
|
320
|
-
"cli.usage": "Verwendung:\n
|
|
327
|
+
"cli.usage": "Verwendung:\n zardbot-telegram [start] [--mode sources|installed]\n zardbot-telegram status\n zardbot-telegram stop\n zardbot-telegram config\n\nHinweise:\n - Ohne Befehl wird standardmäßig `start` verwendet\n - `--mode` wird derzeit nur für `start` unterstützt",
|
|
321
328
|
"cli.placeholder.status": "Befehl `status` ist derzeit ein Platzhalter. Echte Statusprüfungen werden in der Service-Schicht hinzugefügt (Phase 5).",
|
|
322
329
|
"cli.placeholder.stop": "Befehl `stop` ist derzeit ein Platzhalter. Ein echter Stop des Hintergrundprozesses wird in der Service-Schicht hinzugefügt (Phase 5).",
|
|
323
330
|
"cli.placeholder.unavailable": "Befehl ist nicht verfügbar.",
|
package/dist/i18n/en.js
CHANGED
|
@@ -146,6 +146,13 @@ export const en = {
|
|
|
146
146
|
"model.menu.recent_title": "🕘 Recent",
|
|
147
147
|
"model.menu.recent_empty": "— Empty.",
|
|
148
148
|
"model.menu.favorites_hint": "ℹ️ Add models to favorites in OpenCode CLI to keep them at the top.",
|
|
149
|
+
"model.menu.select_provider": "🌐 Select a provider:",
|
|
150
|
+
"model.menu.select_model_for_provider": "Select model for {provider}:",
|
|
151
|
+
"model.menu.all_providers": "🌐 All Providers",
|
|
152
|
+
"model.menu.back_to_providers": "⬅️ Back to Providers",
|
|
153
|
+
"model.menu.back_to_main": "⬅️ Back to Main Menu",
|
|
154
|
+
"model.menu.provider_source_api": "📡 API",
|
|
155
|
+
"model.menu.provider_source_config": "📝 Config",
|
|
149
156
|
"model.menu.error": "🔴 Failed to get models list",
|
|
150
157
|
"variant.model_not_selected_callback": "Error: model is not selected",
|
|
151
158
|
"variant.changed_callback": "Variant changed: {name}",
|
|
@@ -251,7 +258,7 @@ export const en = {
|
|
|
251
258
|
"runtime.wizard.start": "OpenCode Telegram Bot setup.\n",
|
|
252
259
|
"runtime.wizard.saved": "Configuration saved:\n- {envPath}\n- {settingsPath}\n",
|
|
253
260
|
"runtime.wizard.not_configured_starting": "Application is not configured yet. Starting wizard...\n",
|
|
254
|
-
"runtime.wizard.tty_required": "Interactive wizard requires a TTY terminal. Run `
|
|
261
|
+
"runtime.wizard.tty_required": "Interactive wizard requires a TTY terminal. Run `zardbot-telegram config` in an interactive shell.",
|
|
255
262
|
"rename.no_session": "⚠️ No active session. Create or select a session first.",
|
|
256
263
|
"rename.prompt": "📝 Enter new title for session:\n\nCurrent: {title}",
|
|
257
264
|
"rename.empty_title": "⚠️ Title cannot be empty.",
|
|
@@ -317,7 +324,7 @@ export const en = {
|
|
|
317
324
|
"commands.page_empty_callback": "No commands on this page",
|
|
318
325
|
"commands.page_load_error_callback": "Cannot load this page. Please try again.",
|
|
319
326
|
"cmd.description.rename": "Rename current session",
|
|
320
|
-
"cli.usage": "Usage:\n
|
|
327
|
+
"cli.usage": "Usage:\n zardbot-telegram [start] [--mode sources|installed]\n zardbot-telegram status\n zardbot-telegram stop\n zardbot-telegram config\n\nNotes:\n - No command defaults to `start`\n - `--mode` is currently supported for `start` only",
|
|
321
328
|
"cli.placeholder.status": "Command `status` is currently a placeholder. Real status checks will be added in service layer (Phase 5).",
|
|
322
329
|
"cli.placeholder.stop": "Command `stop` is currently a placeholder. Real background process stop will be added in service layer (Phase 5).",
|
|
323
330
|
"cli.placeholder.unavailable": "Command is unavailable.",
|
package/dist/i18n/es.js
CHANGED
|
@@ -146,6 +146,13 @@ export const es = {
|
|
|
146
146
|
"model.menu.recent_title": "🕘 Recientes",
|
|
147
147
|
"model.menu.recent_empty": "— Vacío.",
|
|
148
148
|
"model.menu.favorites_hint": "ℹ️ Agrega modelos a favoritos en OpenCode CLI para mantenerlos arriba de la lista.",
|
|
149
|
+
"model.menu.select_provider": "🌐 Selecciona un proveedor:",
|
|
150
|
+
"model.menu.select_model_for_provider": "Selecciona el modelo para {provider}:",
|
|
151
|
+
"model.menu.all_providers": "🌐 Todos los proveedores",
|
|
152
|
+
"model.menu.back_to_providers": "⬅️ Volver a proveedores",
|
|
153
|
+
"model.menu.back_to_main": "⬅️ Volver al menú principal",
|
|
154
|
+
"model.menu.provider_source_api": "📡 API",
|
|
155
|
+
"model.menu.provider_source_config": "📝 Config",
|
|
149
156
|
"model.menu.error": "🔴 No se pudo obtener la lista de modelos",
|
|
150
157
|
"variant.model_not_selected_callback": "Error: no hay un modelo seleccionado",
|
|
151
158
|
"variant.changed_callback": "Variante cambiada: {name}",
|
|
@@ -251,7 +258,7 @@ export const es = {
|
|
|
251
258
|
"runtime.wizard.start": "Configuración de OpenCode Telegram Bot.\n",
|
|
252
259
|
"runtime.wizard.saved": "Configuración guardada:\n- {envPath}\n- {settingsPath}\n",
|
|
253
260
|
"runtime.wizard.not_configured_starting": "La aplicación aún no está configurada. Iniciando el asistente...\n",
|
|
254
|
-
"runtime.wizard.tty_required": "El asistente interactivo requiere un terminal TTY. Ejecuta `
|
|
261
|
+
"runtime.wizard.tty_required": "El asistente interactivo requiere un terminal TTY. Ejecuta `zardbot-telegram config` en una shell interactiva.",
|
|
255
262
|
"rename.no_session": "⚠️ No hay una sesión activa. Crea o selecciona una sesión primero.",
|
|
256
263
|
"rename.prompt": "📝 Introduce un nuevo título para la sesión:\n\nActual: {title}",
|
|
257
264
|
"rename.empty_title": "⚠️ El título no puede estar vacío.",
|
|
@@ -317,7 +324,7 @@ export const es = {
|
|
|
317
324
|
"commands.page_empty_callback": "No hay comandos en esta página",
|
|
318
325
|
"commands.page_load_error_callback": "No se pudo cargar esta página. Por favor, inténtalo de nuevo.",
|
|
319
326
|
"cmd.description.rename": "Renombrar la sesión actual",
|
|
320
|
-
"cli.usage": "Uso:\n
|
|
327
|
+
"cli.usage": "Uso:\n zardbot-telegram [start] [--mode sources|installed]\n zardbot-telegram status\n zardbot-telegram stop\n zardbot-telegram config\n\nNotas:\n - Sin comando, el valor por defecto es `start`\n - `--mode` actualmente solo se admite para `start`",
|
|
321
328
|
"cli.placeholder.status": "El comando `status` es actualmente un marcador de posición. Las comprobaciones reales de estado se agregarán en la capa de servicio (Fase 5).",
|
|
322
329
|
"cli.placeholder.stop": "El comando `stop` es actualmente un marcador de posición. La detención real del proceso en segundo plano se agregará en la capa de servicio (Fase 5).",
|
|
323
330
|
"cli.placeholder.unavailable": "El comando no esta disponible.",
|
package/dist/i18n/fr.js
CHANGED
|
@@ -146,6 +146,13 @@ export const fr = {
|
|
|
146
146
|
"model.menu.recent_title": "🕘 Récents",
|
|
147
147
|
"model.menu.recent_empty": "— Vide.",
|
|
148
148
|
"model.menu.favorites_hint": "ℹ️ Ajoutez des modèles aux favoris dans l'interface OpenCode pour les garder en tête de liste.",
|
|
149
|
+
"model.menu.select_provider": "🌐 Sélectionnez un fournisseur :",
|
|
150
|
+
"model.menu.select_model_for_provider": "Sélectionnez un modèle pour {provider} :",
|
|
151
|
+
"model.menu.all_providers": "🌐 Tous les fournisseurs",
|
|
152
|
+
"model.menu.back_to_providers": "⬅️ Retour aux fournisseurs",
|
|
153
|
+
"model.menu.back_to_main": "⬅️ Retour au menu principal",
|
|
154
|
+
"model.menu.provider_source_api": "📡 API",
|
|
155
|
+
"model.menu.provider_source_config": "📝 Config",
|
|
149
156
|
"model.menu.error": "🔴 Impossible de récupérer la liste des modèles",
|
|
150
157
|
"variant.model_not_selected_callback": "Erreur : aucun modèle sélectionné",
|
|
151
158
|
"variant.changed_callback": "Variante modifiée : {name}",
|
|
@@ -251,7 +258,7 @@ export const fr = {
|
|
|
251
258
|
"runtime.wizard.start": "Configuration d'OpenCode Telegram Bot.\n",
|
|
252
259
|
"runtime.wizard.saved": "Configuration enregistrée :\n- {envPath}\n- {settingsPath}\n",
|
|
253
260
|
"runtime.wizard.not_configured_starting": "L'application n'est pas encore configurée. Lancement de l'assistant...\n",
|
|
254
|
-
"runtime.wizard.tty_required": "L'assistant interactif nécessite un terminal TTY. Exécutez `
|
|
261
|
+
"runtime.wizard.tty_required": "L'assistant interactif nécessite un terminal TTY. Exécutez `zardbot-telegram config` dans un shell interactif.",
|
|
255
262
|
"rename.no_session": "⚠️ Aucune session active. Créez ou sélectionnez d'abord une session.",
|
|
256
263
|
"rename.prompt": "📝 Entrez le nouveau titre de la session :\n\nActuel : {title}",
|
|
257
264
|
"rename.empty_title": "⚠️ Le titre ne peut pas être vide.",
|
|
@@ -317,7 +324,7 @@ export const fr = {
|
|
|
317
324
|
"commands.page_empty_callback": "Aucune commande sur cette page",
|
|
318
325
|
"commands.page_load_error_callback": "Impossible de charger cette page. Veuillez réessayer.",
|
|
319
326
|
"cmd.description.rename": "Renommer la session actuelle",
|
|
320
|
-
"cli.usage": "Utilisation :\n
|
|
327
|
+
"cli.usage": "Utilisation :\n zardbot-telegram [start] [--mode sources|installed]\n zardbot-telegram status\n zardbot-telegram stop\n zardbot-telegram config\n\nNotes :\n - Sans commande, `start` est utilisé par défaut\n - `--mode` n'est actuellement pris en charge que pour `start`",
|
|
321
328
|
"cli.placeholder.status": "La commande `status` est actuellement un placeholder. Les vraies vérifications d'état seront ajoutées dans la couche service (Phase 5).",
|
|
322
329
|
"cli.placeholder.stop": "La commande `stop` est actuellement un placeholder. Le véritable arrêt du processus en arrière-plan sera ajouté dans la couche service (Phase 5).",
|
|
323
330
|
"cli.placeholder.unavailable": "Commande indisponible.",
|
package/dist/i18n/ru.js
CHANGED
|
@@ -146,6 +146,13 @@ export const ru = {
|
|
|
146
146
|
"model.menu.recent_title": "🕘 Недавние",
|
|
147
147
|
"model.menu.recent_empty": "— Список пуст.",
|
|
148
148
|
"model.menu.favorites_hint": "ℹ️ Добавляйте модели в избранное через OpenCode CLI, чтобы они были вверху списка.",
|
|
149
|
+
"model.menu.select_provider": "🌐 Выберите провайдера:",
|
|
150
|
+
"model.menu.select_model_for_provider": "Выберите модель для {provider}:",
|
|
151
|
+
"model.menu.all_providers": "🌐 Все провайдеры",
|
|
152
|
+
"model.menu.back_to_providers": "⬅️ Назад к провайдерам",
|
|
153
|
+
"model.menu.back_to_main": "⬅️ Назад в главное меню",
|
|
154
|
+
"model.menu.provider_source_api": "📡 API",
|
|
155
|
+
"model.menu.provider_source_config": "📝 Конфиг",
|
|
149
156
|
"model.menu.error": "🔴 Не удалось получить список моделей",
|
|
150
157
|
"variant.model_not_selected_callback": "Ошибка: модель не выбрана",
|
|
151
158
|
"variant.changed_callback": "Вариант изменен: {name}",
|
|
@@ -251,7 +258,7 @@ export const ru = {
|
|
|
251
258
|
"runtime.wizard.start": "Настройка OpenCode Telegram Bot.\n",
|
|
252
259
|
"runtime.wizard.saved": "Конфигурация сохранена:\n- {envPath}\n- {settingsPath}\n",
|
|
253
260
|
"runtime.wizard.not_configured_starting": "Приложение еще не сконфигурировано. Запускаю wizard...\n",
|
|
254
|
-
"runtime.wizard.tty_required": "Интерактивный wizard требует TTY-терминал. Запустите `
|
|
261
|
+
"runtime.wizard.tty_required": "Интерактивный wizard требует TTY-терминал. Запустите `zardbot-telegram config` в интерактивной оболочке.",
|
|
255
262
|
"rename.no_session": "⚠️ Нет активной сессии. Сначала создайте или выберите сессию.",
|
|
256
263
|
"rename.prompt": "📝 Введите новое название сессии:\n\nТекущее: {title}",
|
|
257
264
|
"rename.empty_title": "⚠️ Название не может быть пустым.",
|
|
@@ -317,7 +324,7 @@ export const ru = {
|
|
|
317
324
|
"commands.page_empty_callback": "На этой странице нет команд",
|
|
318
325
|
"commands.page_load_error_callback": "Не удалось загрузить эту страницу. Пожалуйста, попробуйте снова.",
|
|
319
326
|
"cmd.description.rename": "Переименовать текущую сессию",
|
|
320
|
-
"cli.usage": "Использование:\n
|
|
327
|
+
"cli.usage": "Использование:\n zardbot-telegram [start] [--mode sources|installed]\n zardbot-telegram status\n zardbot-telegram stop\n zardbot-telegram config\n\nЗаметки:\n - Без команды по умолчанию используется `start`\n - `--mode` сейчас поддерживается только для `start`",
|
|
321
328
|
"cli.placeholder.status": "Команда `status` пока работает как заглушка. Реальная проверка статуса появится на этапе service-слоя (Этап 5).",
|
|
322
329
|
"cli.placeholder.stop": "Команда `stop` пока работает как заглушка. Реальная остановка фонового процесса появится на этапе service-слоя (Этап 5).",
|
|
323
330
|
"cli.placeholder.unavailable": "Команда недоступна.",
|
package/dist/i18n/zh.js
CHANGED
|
@@ -146,6 +146,13 @@ export const zh = {
|
|
|
146
146
|
"model.menu.recent_title": "🕘 最近使用",
|
|
147
147
|
"model.menu.recent_empty": "— 列表为空。",
|
|
148
148
|
"model.menu.favorites_hint": "ℹ️ 可在 OpenCode CLI 中将模型加入收藏,使其显示在列表顶部。",
|
|
149
|
+
"model.menu.select_provider": "🌐 请选择提供商:",
|
|
150
|
+
"model.menu.select_model_for_provider": "为 {provider} 选择模型:",
|
|
151
|
+
"model.menu.all_providers": "🌐 所有提供商",
|
|
152
|
+
"model.menu.back_to_providers": "⬅️ 返回提供商列表",
|
|
153
|
+
"model.menu.back_to_main": "⬅️ 返回主菜单",
|
|
154
|
+
"model.menu.provider_source_api": "📡 API",
|
|
155
|
+
"model.menu.provider_source_config": "📝 配置",
|
|
149
156
|
"model.menu.error": "🔴 获取模型列表失败",
|
|
150
157
|
"variant.model_not_selected_callback": "错误:未选择模型",
|
|
151
158
|
"variant.changed_callback": "变体已更改:{name}",
|
|
@@ -251,7 +258,7 @@ export const zh = {
|
|
|
251
258
|
"runtime.wizard.start": "OpenCode Telegram Bot 设置。\n",
|
|
252
259
|
"runtime.wizard.saved": "配置已保存:\n- {envPath}\n- {settingsPath}\n",
|
|
253
260
|
"runtime.wizard.not_configured_starting": "应用尚未配置。正在启动向导...\n",
|
|
254
|
-
"runtime.wizard.tty_required": "交互式向导需要 TTY 终端。请在交互式 shell 中运行 `
|
|
261
|
+
"runtime.wizard.tty_required": "交互式向导需要 TTY 终端。请在交互式 shell 中运行 `zardbot-telegram config`。",
|
|
255
262
|
"rename.no_session": "⚠️ 没有活动会话。请先创建或选择一个会话。",
|
|
256
263
|
"rename.prompt": "📝 请输入会话的新标题:\n\n当前:{title}",
|
|
257
264
|
"rename.empty_title": "⚠️ 标题不能为空。",
|
|
@@ -317,7 +324,7 @@ export const zh = {
|
|
|
317
324
|
"commands.page_empty_callback": "这一页没有命令",
|
|
318
325
|
"commands.page_load_error_callback": "无法加载此页面。请重试。",
|
|
319
326
|
"cmd.description.rename": "重命名当前会话",
|
|
320
|
-
"cli.usage": "用法:\n
|
|
327
|
+
"cli.usage": "用法:\n zardbot-telegram [start] [--mode sources|installed]\n zardbot-telegram status\n zardbot-telegram stop\n zardbot-telegram config\n\n注意:\n - 无命令时默认为 `start`\n - `--mode` 当前仅支持 `start`",
|
|
321
328
|
"cli.placeholder.status": "`status` 命令当前为占位符。实际状态检查将在服务层中添加(第5阶段)。",
|
|
322
329
|
"cli.placeholder.stop": "`stop` 命令当前为占位符。实际后台进程停止功能将在服务层中添加(第5阶段)。",
|
|
323
330
|
"cli.placeholder.unavailable": "命令不可用。",
|
package/dist/model/manager.js
CHANGED
|
@@ -3,13 +3,82 @@ import { config } from "../config.js";
|
|
|
3
3
|
import { opencodeClient } from "../opencode/client.js";
|
|
4
4
|
import { logger } from "../utils/logger.js";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import os from "node:os";
|
|
6
7
|
const MODEL_CATALOG_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
7
8
|
let cachedValidModelKeys = null;
|
|
8
9
|
let modelCatalogCacheExpiresAt = 0;
|
|
9
10
|
let modelCatalogFetchInFlight = null;
|
|
11
|
+
// Cache for full provider/model data (API + JSONC config)
|
|
12
|
+
let cachedProviderModelData = null;
|
|
13
|
+
let providerModelDataCacheExpiresAt = 0;
|
|
14
|
+
let providerModelDataFetchInFlight = null;
|
|
10
15
|
function getModelKey(providerID, modelID) {
|
|
11
16
|
return `${providerID}/${modelID}`;
|
|
12
17
|
}
|
|
18
|
+
function getOpenCodeConfigFilePaths() {
|
|
19
|
+
const paths = [];
|
|
20
|
+
// Standard XDG config locations
|
|
21
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
22
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
23
|
+
if (xdgConfigHome) {
|
|
24
|
+
paths.push(path.join(xdgConfigHome, "opencode", "opencode.jsonc"));
|
|
25
|
+
}
|
|
26
|
+
if (homeDir) {
|
|
27
|
+
paths.push(path.join(homeDir, ".config", "opencode", "opencode.jsonc"));
|
|
28
|
+
}
|
|
29
|
+
// Add common alternative locations
|
|
30
|
+
paths.push("/root/.config/opencode/opencode.jsonc");
|
|
31
|
+
return paths;
|
|
32
|
+
}
|
|
33
|
+
async function readJsoncConfigFiles() {
|
|
34
|
+
try {
|
|
35
|
+
const fs = await import("fs/promises");
|
|
36
|
+
const configPaths = getOpenCodeConfigFilePaths();
|
|
37
|
+
const providers = new Map();
|
|
38
|
+
for (const configPath of configPaths) {
|
|
39
|
+
try {
|
|
40
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
41
|
+
// Remove JSON comments (simple implementation for JSONC)
|
|
42
|
+
const cleanContent = content
|
|
43
|
+
.replace(/\/\/.*$/gm, "") // Remove single-line comments
|
|
44
|
+
.replace(/\/\*[\s\S]*?\*\//g, ""); // Remove multi-line comments
|
|
45
|
+
const configData = JSON.parse(cleanContent);
|
|
46
|
+
if (configData?.provider && typeof configData.provider === "object") {
|
|
47
|
+
for (const [providerID, providerConfig] of Object.entries(configData.provider)) {
|
|
48
|
+
const typedProviderConfig = providerConfig;
|
|
49
|
+
if (typedProviderConfig?.models && typeof typedProviderConfig.models === "object") {
|
|
50
|
+
const models = Object.keys(typedProviderConfig.models);
|
|
51
|
+
if (!providers.has(providerID)) {
|
|
52
|
+
providers.set(providerID, new Set());
|
|
53
|
+
}
|
|
54
|
+
const providerModels = providers.get(providerID);
|
|
55
|
+
models.forEach(modelID => providerModels.add(modelID));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
// Ignore individual file errors and continue with others
|
|
62
|
+
logger.debug(`[ModelManager] Ignoring config file ${configPath}:`, err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (providers.size === 0) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const result = [];
|
|
69
|
+
providers.forEach((models, providerID) => {
|
|
70
|
+
result.push({
|
|
71
|
+
providerID,
|
|
72
|
+
models: Array.from(models)
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
logger.warn("[ModelManager] Error reading JSONC config files:", err);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
13
82
|
function getEnvDefaultModel() {
|
|
14
83
|
const providerID = config.opencode.model.provider;
|
|
15
84
|
const modelID = config.opencode.model.modelId;
|
|
@@ -34,6 +103,77 @@ function filterModelsByCatalog(models, validModelKeys) {
|
|
|
34
103
|
}
|
|
35
104
|
return models.filter((model) => validModelKeys.has(getModelKey(model.providerID, model.modelID)));
|
|
36
105
|
}
|
|
106
|
+
async function getAllProvidersAndModels() {
|
|
107
|
+
// Check cache first
|
|
108
|
+
if (cachedProviderModelData && Date.now() < providerModelDataCacheExpiresAt) {
|
|
109
|
+
logger.debug(`[ModelManager] Provider/model data cache hit: providers=${cachedProviderModelData.length}, ttlMs=${providerModelDataCacheExpiresAt - Date.now()}`);
|
|
110
|
+
return cachedProviderModelData;
|
|
111
|
+
}
|
|
112
|
+
// If there's already a fetch in flight, wait for it
|
|
113
|
+
if (providerModelDataFetchInFlight) {
|
|
114
|
+
logger.debug("[ModelManager] Awaiting in-flight provider/model data refresh");
|
|
115
|
+
return providerModelDataFetchInFlight;
|
|
116
|
+
}
|
|
117
|
+
providerModelDataFetchInFlight = (async () => {
|
|
118
|
+
try {
|
|
119
|
+
logger.debug("[ModelManager] Refreshing provider/model data from all sources");
|
|
120
|
+
// Fetch from API
|
|
121
|
+
const apiResponse = await opencodeClient.config.providers();
|
|
122
|
+
let apiProviders = [];
|
|
123
|
+
if (!apiResponse.error && apiResponse.data) {
|
|
124
|
+
apiProviders = apiResponse.data.providers.map(provider => ({
|
|
125
|
+
providerID: provider.id,
|
|
126
|
+
models: Object.keys(provider.models),
|
|
127
|
+
source: "api"
|
|
128
|
+
}));
|
|
129
|
+
logger.debug(`[ModelManager] Loaded ${apiProviders.length} providers from API`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
logger.warn("[ModelManager] Failed to load providers from API:", apiResponse.error);
|
|
133
|
+
}
|
|
134
|
+
// Fetch from JSONC config files
|
|
135
|
+
const configProviders = await readJsoncConfigFiles();
|
|
136
|
+
const configProvidersWithSource = configProviders?.map(config => ({
|
|
137
|
+
...config,
|
|
138
|
+
source: "config"
|
|
139
|
+
})) || [];
|
|
140
|
+
if (configProvidersWithSource.length > 0) {
|
|
141
|
+
logger.debug(`[ModelManager] Loaded ${configProvidersWithSource.length} providers from config files`);
|
|
142
|
+
}
|
|
143
|
+
// Merge providers from both sources, API takes precedence for duplicates
|
|
144
|
+
const mergedProviders = new Map();
|
|
145
|
+
// Add API providers first (they take precedence)
|
|
146
|
+
apiProviders.forEach(provider => {
|
|
147
|
+
mergedProviders.set(provider.providerID, provider);
|
|
148
|
+
});
|
|
149
|
+
// Add config providers (only if not already present from API)
|
|
150
|
+
configProvidersWithSource.forEach(provider => {
|
|
151
|
+
if (!mergedProviders.has(provider.providerID)) {
|
|
152
|
+
mergedProviders.set(provider.providerID, provider);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const result = Array.from(mergedProviders.values());
|
|
156
|
+
// Cache the result
|
|
157
|
+
cachedProviderModelData = result;
|
|
158
|
+
providerModelDataCacheExpiresAt = Date.now() + MODEL_CATALOG_CACHE_TTL_MS;
|
|
159
|
+
logger.debug(`[ModelManager] Provider/model data refreshed: totalProviders=${result.length}, apiProviders=${apiProviders.length}, configProviders=${configProvidersWithSource.length}`);
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
logger.warn("[ModelManager] Error refreshing provider/model data:", err);
|
|
164
|
+
// Return cached data if available
|
|
165
|
+
if (cachedProviderModelData) {
|
|
166
|
+
logger.warn("[ModelManager] Using stale provider/model data cache after refresh exception");
|
|
167
|
+
return cachedProviderModelData;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
providerModelDataFetchInFlight = null;
|
|
173
|
+
}
|
|
174
|
+
})();
|
|
175
|
+
return providerModelDataFetchInFlight;
|
|
176
|
+
}
|
|
37
177
|
async function getValidModelKeys() {
|
|
38
178
|
if (cachedValidModelKeys && Date.now() < modelCatalogCacheExpiresAt) {
|
|
39
179
|
logger.debug(`[ModelManager] Model catalog cache hit: models=${cachedValidModelKeys.size}, ttlMs=${modelCatalogCacheExpiresAt - Date.now()}`);
|
|
@@ -202,6 +342,9 @@ export function __resetModelCatalogCacheForTests() {
|
|
|
202
342
|
cachedValidModelKeys = null;
|
|
203
343
|
modelCatalogCacheExpiresAt = 0;
|
|
204
344
|
modelCatalogFetchInFlight = null;
|
|
345
|
+
cachedProviderModelData = null;
|
|
346
|
+
providerModelDataCacheExpiresAt = 0;
|
|
347
|
+
providerModelDataFetchInFlight = null;
|
|
205
348
|
}
|
|
206
349
|
/**
|
|
207
350
|
* Get list of favorite models from OpenCode local state file
|
|
@@ -211,6 +354,37 @@ export async function getFavoriteModels() {
|
|
|
211
354
|
const { favorites } = await getModelSelectionLists();
|
|
212
355
|
return favorites;
|
|
213
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Get all available providers and their models from both API and config files
|
|
359
|
+
* @returns Array of providers with their models, or null if unavailable
|
|
360
|
+
*/
|
|
361
|
+
export async function getAllProvidersWithModels() {
|
|
362
|
+
return getAllProvidersAndModels();
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Get a simplified list of all providers (just provider IDs)
|
|
366
|
+
* @returns Array of provider IDs, or null if unavailable
|
|
367
|
+
*/
|
|
368
|
+
export async function getAllProviderIDs() {
|
|
369
|
+
const providersData = await getAllProvidersAndModels();
|
|
370
|
+
if (!providersData) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
return providersData.map(provider => provider.providerID);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Get all models for a specific provider
|
|
377
|
+
* @param providerID Provider ID to get models for
|
|
378
|
+
* @returns Array of model IDs, or null if provider not found or unavailable
|
|
379
|
+
*/
|
|
380
|
+
export async function getModelsForProvider(providerID) {
|
|
381
|
+
const providersData = await getAllProvidersAndModels();
|
|
382
|
+
if (!providersData) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
const provider = providersData.find(p => p.providerID === providerID);
|
|
386
|
+
return provider?.models ?? null;
|
|
387
|
+
}
|
|
214
388
|
/**
|
|
215
389
|
* Get current model from settings or fallback to config
|
|
216
390
|
* @returns Current model info
|