Compare commits

..
Author SHA1 Message Date
Dayuan Jiang 623eb91e57 ci(docker): allow a manual run on main to rebuild the image for App Runner (#992) 2026-10-12 23:28:58 +09:00
Dayuan Jiang 2f92936f03 fix(chat): closing the start screen's MCP video closes the whole card (#991) 2026-10-12 21:47:17 +09:00
Dayuan Jiang 458d8050cc refactor(providers): one record per provider, one place for the key-and-URL decision (#988)
Every fact about a provider lives in one record in lib/providers.ts (SDK kind,
credential kind, env var prefix, default URL, picker group, four-language
notes, and a short model list for the six providers that cannot list models).
The thirteen tables keyed by provider name are gone. resolveEndpoint decides
whose key a call uses and where it goes; usesServerCredentials reads its
answer instead of mirroring getAIModel's branches; serverCredentialError holds
the server credential check once. The UI, the connection test and the routes
read the record's credential and sdk fields instead of comparing provider
names (44 such checks before, 6 after, in the admin panel's fixed-variable
branches).

Intended behavior changes: providers that can list their models have no
hand-written list; the settings dialog fetches the live list in the background
once credentials are entered for a provider without models and offers it
behind "Browse models". Azure has no placeholder default URL (old entries that
hold it still count as unset); the base URL field of Azure and of the new
"custom" OpenAI-compatible preset (CUSTOM_API_KEY, CUSTOM_BASE_URL) is
labelled required. The connection test answers 400 for an unknown provider
name; prototype names are refused everywhere. Provider lists in error messages
and the admin "add provider" dropdown follow the picker's order.

Also fixed, from before the refactor: the redirect guard drops x-access-code
on cross-origin redirects, and edgeOneEndpoint accepts the Origin header only
when it matches Host or X-Forwarded-Host.

Verification: a behavior snapshot of getAIModel's SDK factory calls and the
quota functions over 25 providers, 22 client override shapes and 9 server
environments (5843 rows), recorded on the old code and unchanged apart from
the intended changes; 752 unit tests, 65 provider e2e tests, tsc, biome,
next build. Reviewed by five models from one brief; their findings and the
resulting fixes are recorded in docs/plans/2026-10-provider-refactor.md.
2026-10-12 21:31:03 +09:00
4 changed files with 13 additions and 24 deletions
+3
View File
@@ -9,6 +9,9 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
# Run it by hand on main when a push did not trigger it (GitHub drops an
# event now and then); it then pushes to ECR like a push to main
workflow_dispatch:
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
+4 -17
View File
@@ -11,7 +11,7 @@ const MCP_URL =
"https://github.com/DayuanJiang/next-ai-draw-io/tree/main/packages/mcp-server" "https://github.com/DayuanJiang/next-ai-draw-io/tree/main/packages/mcp-server"
/** /**
* Whether the user closed the MCP video once. Starts as closed so the server * Whether the user closed the MCP notice once. Starts as closed so the server
* render and the first client render agree; the stored choice is read after * render and the first client render agree; the stored choice is read after
* mount. * mount.
*/ */
@@ -90,25 +90,12 @@ const cardClass =
"w-full rounded-xl border border-purple-500/20 bg-gradient-to-r from-purple-500/10 to-blue-500/10" "w-full rounded-xl border border-purple-500/20 bg-gradient-to-r from-purple-500/10 to-blue-500/10"
/** /**
* MCP server notice on the start screen. Until the user closes it, the card * MCP server notice on the start screen: the card with the promo video. Once
* also plays the promo video; after that it is the plain card for good. * the user closes it, it does not come back.
*/ */
export function McpNotice({ className }: { className?: string }) { export function McpNotice({ className }: { className?: string }) {
const [closed, close] = useVideoClosed() const [closed, close] = useVideoClosed()
if (closed) { if (closed) return null
return (
<div
className={cn(
cardClass,
"px-4 py-3 transition-colors hover:border-purple-500/40",
className,
)}
data-testid="mcp-notice"
>
<NoticeLink />
</div>
)
}
return ( return (
<div <div
className={cn(cardClass, "p-3", className)} className={cn(cardClass, "p-3", className)}
+1 -1
View File
@@ -30,6 +30,6 @@ export const STORAGE_KEYS = {
// Langfuse session id // Langfuse session id
sessionId: "next-ai-draw-io-session-id", sessionId: "next-ai-draw-io-session-id",
// The start screen's MCP video was closed (it does not come back) // The start screen's MCP card with its video was closed (it does not come back)
mcpVideoClosed: "next-ai-draw-io-mcp-video-closed", mcpVideoClosed: "next-ai-draw-io-mcp-video-closed",
} as const } as const
+5 -6
View File
@@ -170,18 +170,17 @@ test.describe("Workspace", () => {
await expect(getChatInput(page)).toHaveValue("Draw a cat for me") await expect(getChatInput(page)).toHaveValue("Draw a cat for me")
}) })
test("the MCP card's video stays away once closed", async ({ page }) => { test("the MCP card and its video stay away once closed", async ({
page,
}) => {
await openApp(page) await openApp(page)
const card = page.locator('[data-testid="mcp-notice"]') const card = page.locator('[data-testid="mcp-notice"]')
const video = page.locator('[data-testid="mcp-video"]')
await expect(card.locator('[data-testid="mcp-video"]')).toBeVisible() await expect(card.locator('[data-testid="mcp-video"]')).toBeVisible()
await page.locator('[data-testid="mcp-video-close"]').click() await page.locator('[data-testid="mcp-video-close"]').click()
await expect(video).toHaveCount(0) await expect(card).toHaveCount(0)
await expect(card).toBeVisible()
await page.reload({ waitUntil: "networkidle" }) await page.reload({ waitUntil: "networkidle" })
await expect(page.locator('[data-testid="lobby-hero"]')).toBeVisible() await expect(page.locator('[data-testid="lobby-hero"]')).toBeVisible()
await expect(card).toBeVisible() await expect(card).toHaveCount(0)
await expect(video).toHaveCount(0)
}) })
test("each AI change becomes a version card; the latest undoes and redoes it", async ({ test("each AI change becomes a version card; the latest undoes and redoes it", async ({