diff --git a/changelog.d/features/12071-csv-import-errors.md b/changelog.d/features/12071-csv-import-errors.md new file mode 100644 index 0000000000..096eed4ef6 --- /dev/null +++ b/changelog.d/features/12071-csv-import-errors.md @@ -0,0 +1 @@ +- **feat(providers):** import-from-file modal shows per-row API errors and ships a downloadable CSV template ([#12071](https://github.com/diegosouzapw/OmniRoute/issues/12071)) diff --git a/docs/guides/USER_GUIDE.md b/docs/guides/USER_GUIDE.md index 72acc05bfc..ceed44c426 100644 --- a/docs/guides/USER_GUIDE.md +++ b/docs/guides/USER_GUIDE.md @@ -122,6 +122,8 @@ Access via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal... ## 📖 Provider Setup +To bulk-add API-key connections from a CSV or JSON file, use **Dashboard → Providers → Import from file**. Columns are positional (`provider,name,apiKey,baseUrl,priority`); `provider` must already exist as a managed provider or a compatible node. See [Import providers from a CSV or JSON file](../providers/CSV-IMPORT.md). + ### 🔐 Subscription Providers #### Claude Code (Pro/Max) diff --git a/docs/providers/CSV-IMPORT.md b/docs/providers/CSV-IMPORT.md new file mode 100644 index 0000000000..4f701a16a2 --- /dev/null +++ b/docs/providers/CSV-IMPORT.md @@ -0,0 +1,43 @@ +--- +title: "Import providers from a CSV or JSON file" +--- + +# Import providers from a CSV or JSON file + +Dashboard → Providers → **Import from file** creates API-key connections from a CSV or JSON list. Each row can target a different provider. Partial failure is the contract: valid rows still import when others fail, and the modal lists why the failed rows were rejected. + +This import does **not** create new OpenAI/Anthropic-compatible endpoint nodes. Create those first (Dashboard → Providers → Add OpenAI-Compatible, or `omniroute nodes add`), then import rows whose `provider` column is that node's id. A per-row `baseUrl` can still override the node's URL. + +## CSV (positional) + +Column names are cosmetic. The parser splits each row and destructures by index: + +| Index | Field | Required | Notes | +| ----- | ----- | -------- | ----- | +| 0 | `provider` | yes | Existing managed provider id (`openai`, `anthropic`, …) **or** an already-registered OpenAI/Anthropic-compatible **node** id | +| 1 | `name` | yes | Connection display name | +| 2 | `apiKey` | yes | API key | +| 3 | `baseUrl` | no | Per-row URL override | +| 4 | `priority` | no | Integer 1–100 | + +A first line whose first column is the literal word `provider` (any case) is skipped as a header. Blank lines and `#` comments are skipped. + +Download a starter file from the import modal (**Download CSV template**). Example: + +```csv +# OmniRoute provider import (positional columns) +provider,name,apiKey,baseUrl,priority +openai,Prod OpenAI,sk-your-openai-key,,1 +``` + +A made-up id such as `openai-compatible-chat-001` is not a node. The API returns `Unknown or unsupported provider` for that row; the modal shows it next to the row name. + +## JSON + +A JSON array of objects with the same fields (`provider`, `name`, `apiKey`, `baseUrl?`, `priority?`). Unlike CSV, JSON keys are named. + +```json +[ + { "provider": "openai", "name": "Prod OpenAI", "apiKey": "sk-your-openai-key", "priority": 1 } +] +``` diff --git a/docs/providers/meta.json b/docs/providers/meta.json index fa6485dd57..b5eca33685 100644 --- a/docs/providers/meta.json +++ b/docs/providers/meta.json @@ -8,6 +8,7 @@ "AGENTROUTER", "ZED-DOCKER", "CURSOR-DOCKER", - "CURSOR-API-KEY-AND-CLI" + "CURSOR-API-KEY-AND-CLI", + "CSV-IMPORT" ] } diff --git a/src/app/(dashboard)/dashboard/providers/components/ImportProvidersFromFileModal.tsx b/src/app/(dashboard)/dashboard/providers/components/ImportProvidersFromFileModal.tsx index 0363f205d8..b9f9417b50 100644 --- a/src/app/(dashboard)/dashboard/providers/components/ImportProvidersFromFileModal.tsx +++ b/src/app/(dashboard)/dashboard/providers/components/ImportProvidersFromFileModal.tsx @@ -4,6 +4,12 @@ import { useTranslations } from "next-intl"; import { Button, Modal } from "@/shared/components"; import type { ParsedProviderImportEntry, ProviderImportParseError } from "./parseProviderImportFile"; import { useImportProvidersFromFile } from "./useImportProvidersFromFile"; +import { + downloadProviderImportCsvTemplate, + formatImportErrorLine, + visibleImportErrors, + type ImportResult, +} from "./providerImportFeedback"; interface ImportProvidersFromFileModalProps { isOpen: boolean; @@ -122,6 +128,30 @@ function FilePickerRow({ fileInputRef, fileName, onFile, t }: FilePickerRowProps ); } +function ImportResultPanel({ result, t }: { result: ImportResult; t: Translator }) { + const { shown, extra } = visibleImportErrors(result.errors); + const failed = result.failed > 0 || shown.length > 0; + return ( +
{t("importFromFileDescription")}
+{t("importFromFileSchemaHint")}