Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion app/src/components/settings/panels/AIPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
type ProviderRef as ApiProviderRef,
clearCloudProviderKey,
type CloudProviderView,
describeProviderVerificationFailure,
flushCloudProviders,
importOpenAiCodexCliAuth,
listProviderModels,
Expand Down Expand Up @@ -2152,7 +2153,17 @@ const CustomRoutingDialog = ({
setTestReply(result.reply);
} catch (err) {
if (testRequestIdRef.current !== requestId) return;
setTestError(err instanceof Error ? err.message : String(err));
// #5146 §2.4: a raw upstream string ("401", "model_not_found", a bare
// 404) tells the user nothing about what to change. Map the common
// shapes onto a concrete next step; unrecognised errors pass through.
const raw = err instanceof Error ? err.message : String(err);
// The banner copy is deliberately generic (a provider error can echo
// request material), so keep the raw text on the console where it is
// still reachable for diagnosis.
console.error(`[ai-settings][test] provider test failed workload=${workload.id}`, raw);
// The bare slug, not `currentProviderString` — that is the composite
// `provider:model[@temp]` and would read as "'openai:gpt-4o' rejected it".
setTestError(describeProviderVerificationFailure(registrySlug ?? '', raw, t));
} finally {
if (testRequestIdRef.current === requestId) {
setTestBusy(false);
Expand Down
13 changes: 12 additions & 1 deletion app/src/components/settings/panels/__tests__/AIPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ vi.mock('../../../../services/api/aiSettingsApi', () => ({
loadLocalProviderSnapshot: vi.fn(),
loadProviderAuthErrors: vi.fn().mockResolvedValue([]),
testProviderModel: vi.fn(),
// #5146 §2.4: AIPanel no longer renders the raw provider string — it maps
// the failure onto actionable copy first. Mirror that shape here so the
// banner asserted below is what a user actually sees; the mapping itself is
// covered in aiSettingsApi.test.ts.
describeProviderVerificationFailure: (slug: string, _raw: string) =>
`The key was saved, but '${slug}' rejected it. Check that you pasted the whole key.`,
modelRegistryVision: vi.fn(() => false),
upsertModelRegistryVision: vi.fn((registry: unknown[]) => registry),
setCloudProviderKey: vi.fn().mockResolvedValue(undefined),
Expand Down Expand Up @@ -1440,7 +1446,12 @@ describe('AIPanel', () => {
const dialog = await screen.findByRole('dialog', { name: /Custom routing/i });
fireEvent.click(within(dialog).getByRole('button', { name: /^Test$/i }));

expect(await within(dialog).findByRole('alert')).toHaveTextContent('401 invalid api key');
// The banner carries the actionable message keyed to the provider slug,
// not the raw upstream string (which can echo request material).
const alert = await within(dialog).findByRole('alert');
expect(alert).toHaveTextContent('rejected it');
expect(alert).toHaveTextContent('openai');
expect(alert).not.toHaveTextContent('401 invalid api key');
});

it('renders background loop diagnostics with newest spend row and budget math', async () => {
Expand Down
14 changes: 14 additions & 0 deletions app/src/lib/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4600,6 +4600,20 @@ const messages: TranslationMap = {
'settings.ai.modelVisionDesc':
'فعّل هذا إذا كان النموذج يقبل الصور. يتيح لمربع الدردشة إرفاق الصور عند اختيار هذا النموذج.',
'settings.ai.testFailed': 'فشل الاختبار',
'settings.ai.providerTest.authRejected':
"تم حفظ المفتاح، لكن '{slug}' رفضه. تأكد من لصق المفتاح كاملاً ومن أنه ما زال نشطاً في لوحة تحكم المزوّد.",
'settings.ai.providerTest.modelNotRecognized':
"تم حفظ المفتاح وقبوله، لكن '{slug}' لا يتعرف على النموذج المحدد. اختر معرّف نموذج يقدمه هذا المزوّد فعلياً (يُضبط نموذجه الافتراضي في إدخال المزوّد).",
'settings.ai.providerTest.quotaOrBilling':
"تم حفظ المفتاح وقبوله، لكن '{slug}' رفض الطلب لأسباب تتعلق بالحصة أو الفوترة. تحقق من رصيد حسابك وحدود الاستخدام لدى المزوّد.",
'settings.ai.providerTest.endpointNotFound':
"تم حفظ المفتاح، لكن نقطة النهاية المهيأة لـ '{slug}' أعادت 404. تحقق من عنوان URL الأساسي: عادةً ما يحتاج المزوّد المتوافق مع OpenAI إلى اللاحقة '/v1' (مثل https://api.openai.com/v1).",
'settings.ai.providerTest.timeout':
"تم حفظ المفتاح، لكن '{slug}' لم يستجب في الوقت المحدد. تحقق من عنوان نقطة النهاية ومن شبكتك ثم أعد الاختبار.",
'settings.ai.providerTest.emptyReply':
"تم حفظ المفتاح، لكن '{slug}' أعاد رداً فارغاً على رسالة اختبار. تحقق من معرّف النموذج المهيأ لهذا المزوّد.",
'settings.ai.providerTest.unknown':
"تم حفظ المفتاح، لكن فشل استدعاء اختباري إلى '{slug}'. تحقق من صفحة حالة المزوّد ومن عنوان نقطة النهاية ثم أعد الاختبار.",
'settings.ai.testingModel': 'نموذج الاختبار...',
'settings.ai.modelResponse': 'استجابة النموذج',
'settings.ai.providerWithValue': 'الموفر: {value}',
Expand Down
14 changes: 14 additions & 0 deletions app/src/lib/i18n/bn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4714,6 +4714,20 @@ const messages: TranslationMap = {
'settings.ai.modelVisionDesc':
'এই মডেল ছবি গ্রহণ করলে সক্ষম করুন। এই মডেল নির্বাচিত থাকলে চ্যাটে ছবি সংযুক্ত করতে দেয়।',
'settings.ai.testFailed': 'পরীক্ষা ব্যর্থ হয়েছে',
'settings.ai.providerTest.authRejected':
"কী সংরক্ষণ করা হয়েছে, কিন্তু '{slug}' সেটি প্রত্যাখ্যান করেছে। দেখুন আপনি পুরো কী পেস্ট করেছেন কি না এবং সেটি প্রদানকারীর ড্যাশবোর্ডে এখনও সক্রিয় কি না।",
'settings.ai.providerTest.modelNotRecognized':
"কী সংরক্ষণ ও গৃহীত হয়েছে, কিন্তু '{slug}' নির্বাচিত মডেলটি চিনতে পারছে না। এই প্রদানকারী সত্যিই দেয় এমন একটি মডেল আইডি বেছে নিন (এর ডিফল্ট মডেল প্রদানকারীর এন্ট্রিতে সেট করা হয়)।",
'settings.ai.providerTest.quotaOrBilling':
"কী সংরক্ষণ ও গৃহীত হয়েছে, কিন্তু '{slug}' কোটা বা বিলিংয়ের কারণে অনুরোধটি প্রত্যাখ্যান করেছে। প্রদানকারীর কাছে আপনার অ্যাকাউন্ট ব্যালেন্স ও ব্যবহারের সীমা দেখুন।",
'settings.ai.providerTest.endpointNotFound':
"কী সংরক্ষণ করা হয়েছে, কিন্তু '{slug}' এর জন্য কনফিগার করা এন্ডপয়েন্ট 404 ফেরত দিয়েছে। বেস URL দেখুন: OpenAI-সঙ্গতিপূর্ণ প্রদানকারীর সাধারণত '/v1' প্রত্যয় প্রয়োজন (যেমন https://api.openai.com/v1)।",
'settings.ai.providerTest.timeout':
"কী সংরক্ষণ করা হয়েছে, কিন্তু '{slug}' সময়মতো সাড়া দেয়নি। এন্ডপয়েন্ট URL ও আপনার নেটওয়ার্ক দেখে আবার পরীক্ষা করুন।",
'settings.ai.providerTest.emptyReply':
"কী সংরক্ষণ করা হয়েছে, কিন্তু '{slug}' পরীক্ষামূলক প্রম্পটে খালি উত্তর ফেরত দিয়েছে। এই প্রদানকারীর জন্য কনফিগার করা মডেল আইডি দেখুন।",
'settings.ai.providerTest.unknown':
"কী সংরক্ষণ করা হয়েছে, কিন্তু '{slug}' এ পরীক্ষামূলক কল ব্যর্থ হয়েছে। প্রদানকারীর স্ট্যাটাস পৃষ্ঠা ও এন্ডপয়েন্ট URL দেখে আবার পরীক্ষা করুন।",
'settings.ai.testingModel': 'পরীক্ষার মডেল...',
'settings.ai.modelResponse': 'মডেল প্রতিক্রিয়া',
'settings.ai.providerWithValue': 'প্রদানকারী: {value}',
Expand Down
14 changes: 14 additions & 0 deletions app/src/lib/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4847,6 +4847,20 @@ const messages: TranslationMap = {
'settings.ai.modelVisionDesc':
'Aktivieren, wenn dieses Modell Bilder akzeptiert. Ermöglicht das Anhängen von Bildern im Chat, wenn dieses Modell ausgewählt ist.',
'settings.ai.testFailed': 'Test fehlgeschlagen.',
'settings.ai.providerTest.authRejected':
"Der Schlüssel wurde gespeichert, aber '{slug}' hat ihn abgelehnt. Prüfen Sie, ob Sie den vollständigen Schlüssel eingefügt haben und ob er im Dashboard des Anbieters noch aktiv ist.",
'settings.ai.providerTest.modelNotRecognized':
"Der Schlüssel wurde gespeichert und akzeptiert, aber '{slug}' kennt das ausgewählte Modell nicht. Wählen Sie eine Modell-ID, die dieser Anbieter tatsächlich bereitstellt (das Standardmodell wird im Anbietereintrag festgelegt).",
'settings.ai.providerTest.quotaOrBilling':
"Der Schlüssel wurde gespeichert und akzeptiert, aber '{slug}' hat die Anfrage aus Kontingent- oder Abrechnungsgründen abgelehnt. Prüfen Sie Ihr Guthaben und Ihre Limits beim Anbieter.",
'settings.ai.providerTest.endpointNotFound':
"Der Schlüssel wurde gespeichert, aber der konfigurierte Endpunkt für '{slug}' lieferte 404. Prüfen Sie die Basis-URL: ein OpenAI-kompatibler Anbieter benötigt meist den Zusatz '/v1' (z. B. https://api.openai.com/v1).",
'settings.ai.providerTest.timeout':
"Der Schlüssel wurde gespeichert, aber '{slug}' hat nicht rechtzeitig geantwortet. Prüfen Sie die Endpunkt-URL und Ihr Netzwerk und testen Sie erneut.",
'settings.ai.providerTest.emptyReply':
"Der Schlüssel wurde gespeichert, aber '{slug}' lieferte auf eine Testanfrage eine leere Antwort. Prüfen Sie die für diesen Anbieter konfigurierte Modell-ID.",
'settings.ai.providerTest.unknown':
"Der Schlüssel wurde gespeichert, aber ein Testaufruf an '{slug}' ist fehlgeschlagen. Prüfen Sie die Statusseite des Anbieters und die Endpunkt-URL und testen Sie erneut.",
'settings.ai.testingModel': 'Modell wird getestet...',
'settings.ai.modelResponse': 'Modellantwort',
'settings.ai.providerWithValue': 'Anbieter: {value}',
Expand Down
14 changes: 14 additions & 0 deletions app/src/lib/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5302,6 +5302,20 @@ const en: TranslationMap = {
'settings.ai.modelVisionDesc':
'Enable if this model accepts images. Lets the chat composer attach images when this model is selected.',
'settings.ai.testFailed': 'Test failed',
'settings.ai.providerTest.authRejected':
"The key was saved, but '{slug}' rejected it. Check that you pasted the whole key and that it is still active in the provider's dashboard.",
'settings.ai.providerTest.modelNotRecognized':
"The key was saved and accepted, but '{slug}' does not recognise the selected model. Pick a model id this provider actually serves (its default model is set on the provider entry).",
'settings.ai.providerTest.quotaOrBilling':
"The key was saved and accepted, but '{slug}' refused the request for quota or billing reasons. Check your account balance and rate limits with the provider.",
'settings.ai.providerTest.endpointNotFound':
"The key was saved, but the configured endpoint for '{slug}' returned 404. Check the base URL: an OpenAI-compatible provider usually needs the '/v1' suffix (e.g. https://api.openai.com/v1).",
'settings.ai.providerTest.timeout':
"The key was saved, but '{slug}' did not respond in time. Check the endpoint URL and your network, then test again.",
'settings.ai.providerTest.emptyReply':
"The key was saved, but '{slug}' returned an empty response to a test prompt. Check the model id configured for this provider.",
'settings.ai.providerTest.unknown':
"The key was saved, but a test call to '{slug}' failed. Check the provider's status page and the endpoint URL, then test again.",
'settings.ai.testingModel': 'Testing model...',
'settings.ai.modelResponse': 'Model response',
'settings.ai.providerWithValue': 'Provider: {value}',
Expand Down
14 changes: 14 additions & 0 deletions app/src/lib/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4794,6 +4794,20 @@ const messages: TranslationMap = {
'settings.ai.modelVisionDesc':
'Actívalo si este modelo acepta imágenes. Permite adjuntar imágenes en el chat cuando este modelo está seleccionado.',
'settings.ai.testFailed': 'Prueba fallida',
'settings.ai.providerTest.authRejected':
"La clave se guardó, pero '{slug}' la rechazó. Comprueba que pegaste la clave completa y que sigue activa en el panel del proveedor.",
'settings.ai.providerTest.modelNotRecognized':
"La clave se guardó y se aceptó, pero '{slug}' no reconoce el modelo seleccionado. Elige un identificador de modelo que este proveedor sirva realmente (su modelo predeterminado se define en la entrada del proveedor).",
'settings.ai.providerTest.quotaOrBilling':
"La clave se guardó y se aceptó, pero '{slug}' rechazó la solicitud por motivos de cuota o facturación. Revisa el saldo de tu cuenta y los límites de uso con el proveedor.",
'settings.ai.providerTest.endpointNotFound':
"La clave se guardó, pero el endpoint configurado para '{slug}' devolvió 404. Revisa la URL base: un proveedor compatible con OpenAI suele necesitar el sufijo '/v1' (por ejemplo, https://api.openai.com/v1).",
'settings.ai.providerTest.timeout':
"La clave se guardó, pero '{slug}' no respondió a tiempo. Revisa la URL del endpoint y tu red, y vuelve a probar.",
'settings.ai.providerTest.emptyReply':
"La clave se guardó, pero '{slug}' devolvió una respuesta vacía a un mensaje de prueba. Revisa el identificador de modelo configurado para este proveedor.",
'settings.ai.providerTest.unknown':
"La clave se guardó, pero una llamada de prueba a '{slug}' falló. Revisa la página de estado del proveedor y la URL del endpoint, y vuelve a probar.",
'settings.ai.testingModel': 'Modelo de prueba...',
'settings.ai.modelResponse': 'Respuesta modelo',
'settings.ai.providerWithValue': 'Proveedor: {value}',
Expand Down
14 changes: 14 additions & 0 deletions app/src/lib/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4824,6 +4824,20 @@ const messages: TranslationMap = {
'settings.ai.modelVisionDesc':
'Activez si ce modèle accepte les images. Permet de joindre des images dans le chat lorsque ce modèle est sélectionné.',
'settings.ai.testFailed': 'Test échoué',
'settings.ai.providerTest.authRejected':
"La clé a été enregistrée, mais '{slug}' l'a refusée. Vérifiez que vous avez collé la clé entière et qu'elle est toujours active dans le tableau de bord du fournisseur.",
'settings.ai.providerTest.modelNotRecognized':
"La clé a été enregistrée et acceptée, mais '{slug}' ne reconnaît pas le modèle sélectionné. Choisissez un identifiant de modèle réellement proposé par ce fournisseur (son modèle par défaut est défini sur la fiche du fournisseur).",
'settings.ai.providerTest.quotaOrBilling':
"La clé a été enregistrée et acceptée, mais '{slug}' a refusé la requête pour des raisons de quota ou de facturation. Vérifiez le solde de votre compte et vos limites auprès du fournisseur.",
'settings.ai.providerTest.endpointNotFound':
"La clé a été enregistrée, mais le point de terminaison configuré pour '{slug}' a renvoyé une erreur 404. Vérifiez l'URL de base : un fournisseur compatible OpenAI nécessite généralement le suffixe '/v1' (par exemple https://api.openai.com/v1).",
'settings.ai.providerTest.timeout':
"La clé a été enregistrée, mais '{slug}' n'a pas répondu à temps. Vérifiez l'URL du point de terminaison et votre réseau, puis réessayez.",
'settings.ai.providerTest.emptyReply':
"La clé a été enregistrée, mais '{slug}' a renvoyé une réponse vide à une invite de test. Vérifiez l'identifiant de modèle configuré pour ce fournisseur.",
'settings.ai.providerTest.unknown':
"La clé a été enregistrée, mais un appel de test vers '{slug}' a échoué. Vérifiez la page d'état du fournisseur et l'URL du point de terminaison, puis réessayez.",
'settings.ai.testingModel': 'Modèle de test...',
'settings.ai.modelResponse': 'Réponse du modèle',
'settings.ai.providerWithValue': 'Fournisseur : {value}',
Expand Down
Loading
Loading