From aa300d2a713bf7a7bea2da1b347c936d4d53f4a0 Mon Sep 17 00:00:00 2001 From: moose-lab Date: Mon, 8 Jun 2026 13:23:53 +0800 Subject: [PATCH 1/2] feat: add OpenAI generation provider --- .env.example | 4 + README.md | 25 +-- api/generations.test.ts | 30 ++-- api/generations.ts | 4 +- src/App.test.tsx | 20 +-- src/components/landing/HeroSection.tsx | 4 +- src/server/generation/generation.test.ts | 187 ++++++++++++++++++++++- src/server/generation/image-generator.ts | 118 +++++++++++++- src/server/generation/llm.ts | 158 ++++++++++++++++++- src/server/generation/orchestrator.ts | 99 +++++++++--- src/server/generation/prompt.ts | 38 ++++- src/server/generation/types.ts | 2 +- 12 files changed, 623 insertions(+), 66 deletions(-) diff --git a/.env.example b/.env.example index 6bf7704..9203a2d 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,9 @@ # Server-side only. Never expose real API keys in browser code. OPENAI_API_KEY= +# Optional override for OpenAI-compatible API gateways. Leave blank for https://api.openai.com/v1. +OPENAI_BASE_URL= +# Optional fallback provider for /api/generations when provider is set to "wavespeed". +WAVESPEED_API_KEY= DATABASE_URL= BETTER_AUTH_SECRET= BETTER_AUTH_URL=http://localhost:5173 diff --git a/README.md b/README.md index a1b268f..a655778 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,12 @@ src/ server/ auth.ts Better Auth configuration db/ Neon Drizzle client and auth schema + generation/ GitHub source lookup, launch brief orchestration, and image provider adapters styles/ app.css Responsive product UI api/ auth/[...all].ts Vercel Function mounted at /api/auth/* + generations.ts Vercel Function mounted at /api/generations drizzle/ *.sql Database migrations public/ @@ -114,6 +116,10 @@ Secrets belong on the server, not in browser code: ```env OPENAI_API_KEY= +# Optional OpenAI-compatible gateway override. Leave blank for the official OpenAI API. +OPENAI_BASE_URL= +# Optional fallback provider key when /api/generations uses provider="wavespeed". +WAVESPEED_API_KEY= DATABASE_URL= BETTER_AUTH_SECRET= BETTER_AUTH_URL= @@ -147,14 +153,18 @@ http://localhost:5173/api/auth/callback/google https://your-domain.example/api/auth/callback/google ``` -For live generation, add a Node API layer or Vercel serverless route that: +## Live Generation API -1. fetches GitHub README / metadata / optional PDF context -2. calls the copy model -3. calls the image model -4. stores a manifest with copy, prompt, image path, model settings, and output preset +`POST /api/generations` runs the server-side launch-card workflow: -The current app keeps the core workflow deterministic and client-safe. +1. fetches GitHub repository metadata and README content, with deterministic fallback data for failed source lookups +2. calls OpenAI Responses for repository analysis and launch planning when `provider` is `openai` +3. calls OpenAI Images for the marketing card render and returns `outputs[locale].imageUrl` for browser preview +4. writes prompt, image, quality report, and manifest artifacts under the server output root + +The Hero generator sends `provider: "openai"` by default and requires `OPENAI_API_KEY` in server runtime configuration. The API still accepts `provider: "mock"` for tests and `provider: "wavespeed"` for the existing Wavespeed-compatible path. + +ChatGPT OAuth is not used for this backend path. ChatGPT Actions OAuth authenticates ChatGPT to a third-party action API; QuickFork's own web backend calls OpenAI directly with a server-side API key. ## Local Development @@ -193,10 +203,7 @@ VERCEL_PROJECT_ID= ## Roadmap -- Fetch GitHub README and repository metadata automatically. - Add PDF upload and extraction for paper-style repositories. -- Add server-side OpenAI generation using `OPENAI_API_KEY`. -- Save generation manifests for repeatable publishing. - Export README banners, slide images, and social cards as separate assets. - Add project pages for public showcases. diff --git a/api/generations.test.ts b/api/generations.test.ts index 41fa58a..d41db17 100644 --- a/api/generations.test.ts +++ b/api/generations.test.ts @@ -9,22 +9,22 @@ describe("/api/generations input contract", () => { repoUrl: " https://github.com/QwenLM/FlashQLA ", locales: ["en", "zh"], preset: "4:3", - provider: "mock", + provider: "openai", imageQuality: "low", models: { - llm: "openai/gpt-5.5", - image: "openai/gpt-image-2/text-to-image", + llm: "gpt-5.5", + image: "gpt-image-2", }, }), ).toMatchObject({ repoUrl: "https://github.com/QwenLM/FlashQLA", locales: ["en", "zh"], preset: "4:3", - provider: "mock", + provider: "openai", imageQuality: "low", models: { - llm: "openai/gpt-5.5", - image: "openai/gpt-image-2/text-to-image", + llm: "gpt-5.5", + image: "gpt-image-2", }, }); }); @@ -60,12 +60,24 @@ describe("/api/generations input contract", () => { ).toThrow(/locales/i); }); - it("uses the production provider requirement in validation errors", () => { - expect(() => + it("accepts direct OpenAI and Wavespeed providers while rejecting unknown providers", () => { + expect( normalizeCreateGenerationInput({ repoUrl: "https://github.com/QwenLM/FlashQLA", provider: "openai", + }).provider, + ).toBe("openai"); + expect( + normalizeCreateGenerationInput({ + repoUrl: "https://github.com/QwenLM/FlashQLA", + provider: "wavespeed", + }).provider, + ).toBe("wavespeed"); + expect(() => + normalizeCreateGenerationInput({ + repoUrl: "https://github.com/QwenLM/FlashQLA", + provider: "chatgpt-oauth", }), - ).toThrow("provider must be wavespeed."); + ).toThrow("provider must be openai, wavespeed, or mock."); }); }); diff --git a/api/generations.ts b/api/generations.ts index 792ac43..ac4a5b1 100644 --- a/api/generations.ts +++ b/api/generations.ts @@ -18,7 +18,7 @@ const presetValues = [ "16:9", "21:9", ] as const; -const providerValues = ["mock", "wavespeed"] as const; +const providerValues = ["mock", "openai", "wavespeed"] as const; const qualityValues = ["low"] as const; function sendJson(res: ServerResponse, statusCode: number, body: unknown) { @@ -80,7 +80,7 @@ export function normalizeCreateGenerationInput(body: unknown): CreateGenerationI throw new GenerationError("VALIDATION_ERROR", "preset is not supported."); } if (value.provider !== undefined && !isOneOf(value.provider, providerValues)) { - throw new GenerationError("VALIDATION_ERROR", "provider must be wavespeed."); + throw new GenerationError("VALIDATION_ERROR", "provider must be openai, wavespeed, or mock."); } if (value.imageQuality !== undefined && !isOneOf(value.imageQuality, qualityValues)) { throw new GenerationError("VALIDATION_ERROR", "imageQuality must be low."); diff --git a/src/App.test.tsx b/src/App.test.tsx index fd77503..5ac3847 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1357,18 +1357,18 @@ describe("App", () => { en: { promptPath: "output/project-launch/qwenlm-flashqla/en/marketing_card_prompt.txt", imagePath: "output/project-launch/qwenlm-flashqla/en/marketing-card.png", - imageUrl: "https://wavespeed.ai/generated/qwenlm-flashqla.png", + imageUrl: "data:image/png;base64,cXdlbmxtLWZsYXNocWxh", qualityReportPath: "output/project-launch/qwenlm-flashqla/en/quality-report.json", }, }, stages: [ { id: "repo", label: "Repository source", status: "completed" }, - { id: "readme", label: "GPT5.5 README analysis", status: "completed", model: "openai/gpt-5.5" }, - { id: "image", label: "gpt-image-2 render", status: "completed", model: "openai/gpt-image-2/text-to-image" }, + { id: "readme", label: "OpenAI README analysis", status: "completed", model: "gpt-5.5" }, + { id: "image", label: "gpt-image-2 render", status: "completed", model: "gpt-image-2" }, ], modelCalls: [ - { provider: "wavespeed", model: "openai/gpt-5.5", purpose: "readme_analysis", status: "completed" }, - { provider: "wavespeed", model: "openai/gpt-image-2/text-to-image", purpose: "image_generation", status: "completed" }, + { provider: "openai", model: "gpt-5.5", purpose: "readme_analysis", status: "completed" }, + { provider: "openai", model: "gpt-image-2", purpose: "image_generation", status: "completed" }, ], launchBrief: { summary: "CUDA kernels for faster attention inference.", @@ -1562,19 +1562,19 @@ describe("App", () => { repoUrl: "https://github.com/QwenLM/FlashQLA", locales: ["en"], preset: "4:3", - provider: "wavespeed", + provider: "openai", imageQuality: "low", }); expect(await screen.findByText(/generated gen_qwenlm_flashqla_test/i)).toBeInTheDocument(); const previewImage = await within(form).findByRole("img", { name: /qwenlm\/flashqla launch card/i }); const controls = form.querySelector(".referenceControls"); - expect(previewImage).toHaveAttribute("src", "https://wavespeed.ai/generated/qwenlm-flashqla.png"); - expect(previewImage.closest(".generationPreview")).toHaveAccessibleName("Generated Wavespeed image result"); + expect(previewImage).toHaveAttribute("src", "data:image/png;base64,cXdlbmxtLWZsYXNocWxh"); + expect(previewImage.closest(".generationPreview")).toHaveAccessibleName("Generated OpenAI image result"); expect(controls).not.toBeNull(); expect((controls?.compareDocumentPosition(previewImage) ?? 0) & Node.DOCUMENT_POSITION_FOLLOWING).toBe(Node.DOCUMENT_POSITION_FOLLOWING); expect(within(form).getByRole("link", { name: /download generated image/i })).toHaveAttribute( "href", - "https://wavespeed.ai/generated/qwenlm-flashqla.png", + "data:image/png;base64,cXdlbmxtLWZsYXNocWxh", ); const briefRegion = await screen.findByRole("region", { name: /free repo launch brief/i }); expect(within(briefRegion).getByText(/AI project builders, open-source maintainers/i)).toBeInTheDocument(); @@ -1733,7 +1733,7 @@ describe("App", () => { const previewDialog = await screen.findByRole("dialog", { name: /generated image preview/i }); expect(within(previewDialog).getByRole("img", { name: /qwenlm\/flashqla launch card/i })).toHaveAttribute( "src", - "https://wavespeed.ai/generated/qwenlm-flashqla.png", + "data:image/png;base64,cXdlbmxtLWZsYXNocWxh", ); expect(within(previewDialog).getByRole("link", { name: /download generated image/i })).toHaveAttribute( "download", diff --git a/src/components/landing/HeroSection.tsx b/src/components/landing/HeroSection.tsx index a04f7c1..da54c95 100644 --- a/src/components/landing/HeroSection.tsx +++ b/src/components/landing/HeroSection.tsx @@ -208,7 +208,7 @@ async function createGeneration(input: { repoUrl: input.repoUrl, locales: input.locales, preset: input.preset, - provider: "wavespeed", + provider: "openai", imageQuality: input.imageQuality, }), }); @@ -399,7 +399,7 @@ function ProjectLaunchInputPanel() { {generatedImageUrl ? ( -
+