From 4dd9d974525eb2823b5f9756d65fc40d14e3fb67 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 14:48:05 +0000 Subject: [PATCH] Let the model card workflow delete a card The delete-model-card command only ran from the CLI, which needs the storage keys. The card endpoint now takes `delete: true` to run it on the server, and the Generate Model Card workflow gets a matching checkbox, so a card can be taken down without local credentials. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015roAwvpcszBvjX1Ce5NFuj --- .github/workflows/generate-model-card.yml | 9 +++++++-- .../generate-model-card/__tests__/route.test.ts | 13 +++++++++++-- src/app/api/internal/generate-model-card/route.ts | 13 ++++++++++--- src/cli/commands/generate-model-card.ts | 2 +- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/.github/workflows/generate-model-card.yml b/.github/workflows/generate-model-card.yml index 08b92a6..878093f 100644 --- a/.github/workflows/generate-model-card.yml +++ b/.github/workflows/generate-model-card.yml @@ -5,6 +5,10 @@ on: pattern: description: 'Model ID pattern, as for `pnpm cli generate-model-card` (e.g. apertus-v1.5-70b)' required: true + delete: + description: 'Delete the card instead of generating it' + type: boolean + default: false jobs: generate: @@ -13,9 +17,10 @@ jobs: - name: Generate the model card on the server env: PATTERN: ${{ github.event.inputs.pattern }} + DELETE: ${{ github.event.inputs.delete }} run: | - BODY=$(python3 -c 'import json, os; print(json.dumps({"pattern": os.environ["PATTERN"]}))') - echo "Generating model card for: $PATTERN" + BODY=$(python3 -c 'import json, os; print(json.dumps({"pattern": os.environ["PATTERN"], "delete": os.environ.get("DELETE") == "true"}))') + echo "Model card for: $PATTERN (delete: ${DELETE:-false})" # Card generation reads every published run, so it can outlast the # request; the server keeps going if curl gives up (exit 28). # RAILWAY_APP_URL may redirect (http -> https, www -> apex); follow it as a POST. diff --git a/src/app/api/internal/generate-model-card/__tests__/route.test.ts b/src/app/api/internal/generate-model-card/__tests__/route.test.ts index bcdd2be..4925332 100644 --- a/src/app/api/internal/generate-model-card/__tests__/route.test.ts +++ b/src/app/api/internal/generate-model-card/__tests__/route.test.ts @@ -4,14 +4,14 @@ import { vi } from 'vitest'; import { NextRequest, NextResponse } from 'next/server'; -vi.mock('@/cli/commands/generate-model-card', () => ({ actionGenerateModelCard: vi.fn() })); +vi.mock('@/cli/commands/generate-model-card', () => ({ actionGenerateModelCard: vi.fn(), actionDeleteModelCard: vi.fn() })); vi.mock('@/lib/background-function-auth', () => ({ checkBackgroundAuth: vi.fn(() => null) })); vi.mock('@/utils/logger', () => ({ getLogger: vi.fn(async () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn() })), })); import { POST } from '../route'; -import { actionGenerateModelCard } from '@/cli/commands/generate-model-card'; +import { actionDeleteModelCard, actionGenerateModelCard } from '@/cli/commands/generate-model-card'; import { checkBackgroundAuth } from '@/lib/background-function-auth'; import { getConfig } from '@/cli/config'; @@ -34,6 +34,15 @@ describe('POST /api/internal/generate-model-card', () => { expect(res.status).toBe(200); expect(actionGenerateModelCard).toHaveBeenCalledWith('apertus-v1.5-70b', {}); + expect(actionDeleteModelCard).not.toHaveBeenCalled(); + }); + + it('deletes the card instead when asked', async () => { + const res = await POST(request({ pattern: 'apertus-v1.5-70b', delete: true })); + + expect(res.status).toBe(200); + expect(actionDeleteModelCard).toHaveBeenCalledWith('apertus-v1.5-70b', { force: true }); + expect(actionGenerateModelCard).not.toHaveBeenCalled(); }); it('rejects unauthenticated requests', async () => { diff --git a/src/app/api/internal/generate-model-card/route.ts b/src/app/api/internal/generate-model-card/route.ts index 50028b4..592614d 100644 --- a/src/app/api/internal/generate-model-card/route.ts +++ b/src/app/api/internal/generate-model-card/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { checkBackgroundAuth } from '@/lib/background-function-auth'; import { configure } from '@/cli/config'; import { getLogger } from '@/utils/logger'; -import { actionGenerateModelCard } from '@/cli/commands/generate-model-card'; +import { actionDeleteModelCard, actionGenerateModelCard } from '@/cli/commands/generate-model-card'; // A substring of base model IDs, as the generate-model-card CLI takes it // (e.g. "apertus-v1.5-70b", "claude-3-5-sonnet", "openai:gpt-4o"). @@ -11,15 +11,17 @@ const PATTERN_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]{1,99}$/; /** * Runs the generate-model-card CLI command on the server, where the storage * and OpenRouter credentials live. Triggered by the "Generate Model Card" - * GitHub workflow. + * GitHub workflow. With `delete: true` it removes the card instead (the + * delete-model-card command), so its page stops being served. */ export async function POST(req: NextRequest) { const authError = checkBackgroundAuth(req); if (authError) return authError; let pattern: unknown; + let deleteCard: unknown; try { - ({ pattern } = await req.json()); + ({ pattern, delete: deleteCard } = await req.json()); } catch { return NextResponse.json({ error: 'Invalid JSON in request body.' }, { status: 400 }); } @@ -38,6 +40,11 @@ export async function POST(req: NextRequest) { }, }); + if (deleteCard === true) { + await actionDeleteModelCard(pattern, { force: true }); + return NextResponse.json({ message: 'Model card deleted.', pattern }); + } + await actionGenerateModelCard(pattern, {}); return NextResponse.json({ message: 'Model card generation finished.', pattern }); } diff --git a/src/cli/commands/generate-model-card.ts b/src/cli/commands/generate-model-card.ts index 4d1d324..5b76ff0 100644 --- a/src/cli/commands/generate-model-card.ts +++ b/src/cli/commands/generate-model-card.ts @@ -465,7 +465,7 @@ export async function actionGenerateModelCard(modelIdPattern: string, options: { logger.info(`==========================\n`); } -async function actionDeleteModelCard(modelIdPattern: string, options: { force?: boolean }) { +export async function actionDeleteModelCard(modelIdPattern: string, options: { force?: boolean }) { const { logger } = getConfig(); if (!options.force) {