From a8b8c9edc8fe93894c354e4a72c8a18cf982ebdf Mon Sep 17 00:00:00 2001 From: gabemontero Date: Fri, 31 Jul 2026 15:44:47 -0400 Subject: [PATCH 1/4] docs(ai-integrations): add openspec for RHIDP-15205 TechDocs/model card integration openspec covering connector self-discovery, auto TechDocsKey annotation, wildcard model card route, mkdocs.yml generation, and url-reader config alignment with cluster-nested structure. Signed-off-by: gabemontero Assisted-by: Claude Opus 4.6 Co-Authored-By: Claude Opus 4.6 --- .../techdocs-modelcard-finalization/design.md | 166 ++++++++++++++ .../proposal.md | 68 ++++++ .../specs/techdocs-integration/spec.md | 213 ++++++++++++++++++ .../techdocs-modelcard-finalization/tasks.md | 80 +++++++ 4 files changed, 527 insertions(+) create mode 100644 workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/design.md create mode 100644 workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/proposal.md create mode 100644 workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/specs/techdocs-integration/spec.md create mode 100644 workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/tasks.md diff --git a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/design.md b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/design.md new file mode 100644 index 00000000000..849dc6e8f91 --- /dev/null +++ b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/design.md @@ -0,0 +1,166 @@ +# Design: TechDocs / Model Card Integration Finalization + +## Canonical Touchpoints + +- Parent design: `openspec/changes/transition-oai-connector-to-kserve-plugin/design.md` — Decision 6 +- Jira: [RHIDP-15205](https://redhat.atlassian.net/browse/RHIDP-15205) + +## Context + +The TechDocs / model card pipeline spans three plugins: + +1. **`kserve-kubeflow-connector-backend`** — K8s Informer watches InferenceService CRs, `KServe.ts` converts them to `ModelCatalog` JSON with annotations including `TechDocsKey`, router serves model card markdown at `/modelcard/:sourceId/*` +2. **`catalog-backend-module-model-catalog`** — Entity provider polls the connector's REST API. `ModelCatalogGenerator.ts` (lines 149-159) transforms the `TechDocsKey` annotation: prepends `svcUrl` (the connector's base URL) and wraps in `url:` prefix to produce the `backstage.io/techdocs-ref` annotation +3. **`catalog-techdoc-url-reader-backend`** — Custom URL reader that matches `backstage.io/techdocs-ref` URLs containing `modelcard` and a known connector ID. Fetches the markdown, writes it to disk with a `mkdocs.yml`, and returns a tree response for the TechDocs builder + +The `url:` prefix on `backstage.io/techdocs-ref` is a Backstage convention that triggers URL reader dispatch. The entity provider already handles adding this prefix — KServe.ts must set TechDocsKey as a **path only** (e.g., `/modelcard/sourceId/modelName`), not a full URL. + +### Data flow + +``` +InferenceService CR (K8s) + ↓ [Informer watch] +KServe.ts: generateModelCatalog() + → sets TechDocsKey = "/modelcard//" (path only) + ↓ [REST API: /models/:model/:version] +ModelCatalogGenerator.ts (entity provider) + → reads TechDocsKey annotation + → prepends svcUrl: "http://localhost:7007/api/kserve-kubeflow-connector" + → wraps in url: prefix + → sets backstage.io/techdocs-ref = "url:http://localhost:7007/api/kserve-kubeflow-connector/modelcard//" + ↓ [Backstage TechDocs builder] +ModeCatalogBridgeTechdocUrlReader (url-reader) + → bridgePredicate matches URL (contains "modelcard" and connector ID) + → readUrl fetches model card markdown from connector + → readTree wraps in ModelCatalogBridgeUrlReaderServiceReadTreeResponse + → dir() writes mkdocs.yml + docs/index.md + ↓ [mkdocs build] +TechDocs rendered in RHDH UI +``` + +### Config structure + +```yaml +catalog: + providers: + modelCatalog: + kserve-kubeflow-connector: # connector key + cluster-1: # cluster sub-key + name: my-k8s-cluster + kubeflow-model-catalog-url: 'https://...' + default-owner: team-alpha + default-lifecycle: production + cluster-2: # future: multiple clusters + name: another-cluster + ... +``` + +Both the connector (`plugin.ts`) and url-reader (`readBridgeConfigs`) navigate this two-level structure. The entity provider's existing config reading at `catalog.providers.modelCatalog` also uses this pattern. The `config.d.ts` schema must declare these fields or Backstage's config validation silently strips them. + +## Goals / Non-Goals + +**Goals:** + +- Connector resolves its own base URL via `coreServices.discovery` and threads it through the config chain +- Auto-set TechDocsKey path when catalog annotations are present on InferenceService CRs +- Wildcard route handles model names containing slashes +- URL reader generates `mkdocs.yml` for TechDocs builder compatibility +- URL reader config aligned with cluster-nested config structure +- Config schema declares all new fields in `config.d.ts` +- `yarn build:all`, `yarn tsc`, unit tests, prettier, and lint pass +- Integration tested against upstream KServe/Kubeflow and RHOAI on OCP + +**Non-Goals:** + +- Merging url-reader into the connector (deferred per parent Decision 6) +- Resolving plugin-to-plugin auth (RHDH_TOKEN workaround stays) +- Multi-cluster support (TODO placed, deferred) +- TechDocs search index population (separate Backstage concern) + +## Decisions + +### D1 — Use `coreServices.discovery` for connector self-URL resolution + +**Choice:** Add `discovery: coreServices.discovery` to the connector plugin's deps. Call `discovery.getBaseUrl('kserve-kubeflow-connector')` at init time and store the result as `connectorBaseUrl` on `ConnectorConfig`. Thread it through `ReconcilerConfig` to `KServe.ts`. + +**Rationale:** The connector needs to construct TechDocsKey URLs pointing back at its own `/modelcard` endpoint. Hardcoding `localhost:7007` would break in any non-dev deployment. `DiscoveryService` is the standard Backstage mechanism for plugin URL resolution, already used by other plugins in the workspace. + +**Alternative considered:** Read the URL from `app-config.yaml`. Rejected because `DiscoveryService` already derives this from the backend config and handles proxy/ingress scenarios correctly. + +### D2 — TechDocsKey is a path, not a full URL + +**Choice:** In `KServe.ts`, set `techdocsUrl = `/modelcard/${sourceId}/${modelName}`\` — a path relative to the connector's base URL. Do not include `connectorBaseUrl` or the `url:` prefix in the annotation value. + +**Rationale:** `ModelCatalogGenerator.ts` (lines 149-159) already handles the full URL construction: + +1. Reads the `TechDocs` annotation key from the model +2. Prepends `svcUrl` (the connector's base URL, passed by the entity provider) +3. Wraps the result in `url:` prefix + +If KServe.ts included the full URL or the `url:` prefix, the result would be double-concatenated (e.g., `url:http://...url:http://...`). The path-only approach keeps KServe.ts decoupled from the entity provider's URL construction logic. + +### D3 — Wildcard route for multi-segment model names + +**Choice:** Change the Express route from `/modelcard/:sourceId/:modelName` to `/modelcard/:sourceId/*`. Access the model name via `(req.params as Record)[0]`. + +**Rationale:** KubeFlow Model Catalog model names frequently contain slashes (e.g., `RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16`). Express named parameters (`:modelName`) only capture a single path segment. The wildcard `*` captures everything after `sourceId/` into `req.params[0]`, matching the key format used by `InformerService.ts` (`${catalogSource}/${catalogModel}`). + +**Alternative considered:** URL-encode slashes in the model name when constructing the path in KServe.ts. Rejected because the stored model card key in the `modelCards` Map uses the raw unencoded form from the catalog annotations — encoding in one place and not the other would cause lookup mismatches. + +**Alternative considered:** Switch to query parameters (`/modelcard?sourceId=...&modelName=...`) like the old golang sidecar. Rejected because it would require changing the router, all callers, and the url-reader's predicate matching — more disruption for no benefit. + +### D4 — Generate minimal `mkdocs.yml` in url-reader `dir()` method + +**Choice:** Write a minimal `mkdocs.yml` (`site_name: Model Card\nnav:\n - Home: index.md\n`) alongside the `docs/index.md` in the url-reader's `dir()` method. + +**Rationale:** Backstage's TechDocs generator runs `mkdocs build` which requires `mkdocs.yml` at the project root. The generator's `patchMkdocsYmlWithPlugins` function reads and patches this file to inject the `techdocs-core` plugin. Without it, the build fails with "Could not read MkDocs YAML config file". The old golang sidecar may have worked with a Backstage version (1.42) that didn't require this, but the current version (1.52) does. + +### D5 — Two-level config iteration in url-reader + +**Choice:** Update `readBridgeConfigs` to iterate connector keys then cluster sub-keys: + +```typescript +for (const connectorId of configs.keys()) { + const connectorConfig = configs.getConfig(connectorId); + for (const clusterKey of connectorConfig.keys()) { + const clusterConfig = connectorConfig.getConfig(clusterKey); + result.push(readBridgeConfig(connectorId, clusterConfig)); + } +} +``` + +**Rationale:** The config structure nests cluster keys under connector keys (e.g., `kserve-kubeflow-connector.cluster-1`). The old flat iteration (`configs.keys().map(id => readBridgeConfig(id, ...))`) treated the connector key as the leaf, missing the cluster level entirely. The entity provider already uses this two-level pattern. + +### D6 — Config schema in config.d.ts uses index signature for cluster sub-keys + +**Choice:** Add cluster sub-key fields to the existing `config.d.ts` using a `[clusterKey: string]` index signature within the connector key object: + +```typescript +[clusterKey: string]: + | { name?: string; 'kubeflow-model-catalog-url'?: string; 'default-owner'?: string; 'default-lifecycle'?: string; } + | string | SchedulerServiceTaskScheduleDefinitionConfig | undefined; +``` + +**Rationale:** Backstage's config validation silently strips fields not declared in any plugin's `config.d.ts`. Without this declaration, `providerConfigs.keys()` returns an empty array because the cluster sub-keys and their fields don't pass validation. The union type accommodates both the new cluster sub-key objects and the existing scalar/schedule fields at the connector level. + +## Risks / Trade-offs + +| Risk | Mitigation | +| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `config.d.ts` union type is permissive — allows invalid field combinations at the connector level | Acceptable for prototyping; a stricter schema can be added when the config shape stabilizes | +| `connectorBaseUrl` is resolved once at init — won't track dynamic URL changes | Standard Backstage pattern; `DiscoveryService` is designed for init-time resolution | +| Wildcard route matches any number of path segments — could match unintended URLs | The model card key lookup in `getModelCard()` will return `undefined` for unknown keys, resulting in a 404 | +| `mkdocs.yml` is minimal — may not work with all mkdocs themes or plugins | TechDocs patcher auto-injects `techdocs-core`; the minimal config is sufficient | +| Multi-cluster TODO is unimplemented — only the first cluster key is used | Documented with TODO comment; sufficient for single-cluster deployments | + +## Verification + +After all changes: + +1. `yarn tsc` passes with no errors +2. `yarn build:all` succeeds +3. Unit tests pass (`yarn test -- --watchAll=false` in url-reader plugin) +4. Prettier and lint checks pass +5. `curl` to `/modelcard/redhat_ai_validated_models/RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16` returns 200 with model card markdown +6. TechDocs page renders in browser for entities with auto-set `backstage.io/techdocs-ref` +7. Integration tested against upstream KServe/Kubeflow and RHOAI on OCP diff --git a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/proposal.md b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/proposal.md new file mode 100644 index 00000000000..dd5dd0bf295 --- /dev/null +++ b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/proposal.md @@ -0,0 +1,68 @@ +# Proposal: TechDocs / Model Card Integration Finalization + +## Why + +The `kserve-kubeflow-connector-backend` plugin can fetch model card markdown from the KubeFlow Model Catalog API and serve it at a REST endpoint. The `catalog-techdoc-url-reader-backend` plugin can consume that endpoint to render model cards as TechDocs in the RHDH UI. However, three gaps prevent this integration from working end-to-end after the rebase: + +1. **No self-discovery**: The connector doesn't know its own base URL. Without it, entities can't carry a `backstage.io/techdocs-ref` annotation pointing back to the connector's `/modelcard` endpoint — the URL must be constructed at runtime via `coreServices.discovery`. + +2. **No automatic TechDocsKey annotation**: When an InferenceService has catalog annotations (`rhdh.io/catalog-source`, `rhdh.io/catalog-model`) indicating a model card is available, no code sets the `TechDocsKey` property automatically. Platform engineers must manually annotate every InferenceService, which doesn't scale. + +3. **Config misalignment**: The url-reader plugin's `BridgeConfig` still uses the old `baseUrl` field and flat config structure, while the connector and entity provider now use cluster-nested config with fields like `kubeflow-model-catalog-url`, `default-owner`, and `default-lifecycle`. + +Additionally, the Express route `/modelcard/:sourceId/:modelName` only matches model names without slashes, but KubeFlow Model Catalog model names frequently contain slashes (e.g., `RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16`). And the url-reader's `dir()` method doesn't generate the `mkdocs.yml` file that Backstage's TechDocs builder (mkdocs) requires. + +## Starting Point + +The connector was merged in PR #3705 and rebased in RHIDP-15199. The url-reader plugin exists but its config reading is stale. The entity provider (`catalog-backend-module-model-catalog`) already handles the `backstage.io/techdocs-ref` annotation in `ModelCatalogGenerator.ts` — it prepends `svcUrl` and wraps in the `url:` prefix that triggers Backstage URL reader dispatch. + +## What Changes + +- **Connector self-discovery**: Add `coreServices.discovery` to the connector plugin, call `discovery.getBaseUrl('kserve-kubeflow-connector')` to obtain the connector's own URL, and thread it through `ConnectorConfig` → `ReconcilerConfig` → KServe.ts +- **Auto TechDocsKey annotation**: In `KServe.ts`, when catalog annotations are present but no explicit TechDocsKey annotation exists, auto-set TechDocsKey as a path (`/modelcard//`) — `ModelCatalogGenerator.ts` already prepends `svcUrl` and adds the `url:` prefix +- **Wildcard model card route**: Change Express route from `/modelcard/:sourceId/:modelName` to `/modelcard/:sourceId/*` to handle model names containing slashes +- **mkdocs.yml generation**: Add `mkdocs.yml` creation to the url-reader's `dir()` method so the TechDocs builder can process model card markdown +- **Config alignment**: Update url-reader's `BridgeConfig` type and `readBridgeConfigs` to match the cluster-nested config structure (`catalog.providers.modelCatalog...{fields}`) +- **Config schema declaration**: Add new config fields to `catalog-backend-module-model-catalog/config.d.ts` so Backstage's config validation doesn't silently strip them + +## Capabilities + +### New Capabilities + +- `connector-self-discovery`: Connector resolves its own base URL via `coreServices.discovery` at startup +- `auto-techdocs-annotation`: TechDocsKey automatically set on entities when catalog annotations indicate model card availability +- `mkdocs-generation`: URL reader generates `mkdocs.yml` alongside fetched markdown for TechDocs builder compatibility + +### Modified Capabilities + +- `modelcard-endpoint`: Route changed from `/modelcard/:sourceId/:modelName` to `/modelcard/:sourceId/*` to handle multi-segment model names +- `url-reader-config`: `BridgeConfig` updated from `{id, baseUrl}` to `{id, name, kubeflowModelCatalogUrl, defaultOwner, defaultLifecycle}` with two-level config iteration (connector → cluster) +- `config-schema`: `config.d.ts` extended with cluster sub-key fields (`name`, `kubeflow-model-catalog-url`, `default-owner`, `default-lifecycle`) + +## Non-goals + +- Merging `catalog-techdoc-url-reader-backend` into the connector plugin (deferred per Design Decision 6 in parent openspec) +- Resolving the plugin-to-plugin auth workaround (the `RHDH_TOKEN` env var for static admin token) +- Adding TechDocs search indexing support +- Multi-cluster connector instances (TODO added, deferred) + +## Canonical Touchpoints + +- **Parent openspec**: `openspec/changes/transition-oai-connector-to-kserve-plugin/` — Design Decision 6 +- **Jira**: [RHIDP-15205](https://redhat.atlassian.net/browse/RHIDP-15205) +- **Plan**: RHDHPLAN-404 + +**Change type**: feature + +## Impact + +- `plugins/kserve-kubeflow-connector-backend/src/plugin.ts` — `coreServices.discovery` added, cluster-key config navigation +- `plugins/kserve-kubeflow-connector-backend/src/services/InformerService.ts` — `connectorBaseUrl` on `ConnectorConfig`, threaded to reconciler +- `plugins/kserve-kubeflow-connector-backend/src/services/types.ts` — `connectorBaseUrl` on `ReconcilerConfig` +- `plugins/kserve-kubeflow-connector-backend/src/services/KServe.ts` — auto TechDocsKey path construction (~15 lines added) +- `plugins/kserve-kubeflow-connector-backend/src/router.ts` — wildcard route change (1 line) +- `plugins/catalog-techdoc-url-reader-backend/src/plugin.ts` — `BridgeConfig` type update, `readBridgeConfigs` two-level iteration, `mkdocs.yml` generation in `dir()` +- `plugins/catalog-techdoc-url-reader-backend/src/plugin.test.ts` — test fixtures updated for new config shape +- `plugins/catalog-backend-module-model-catalog/config.d.ts` — schema extended with cluster sub-key fields +- `app-config.yaml` — `cluster-1:` nesting level added with new fields +- Net change: +120 lines, -38 lines across 9 files diff --git a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/specs/techdocs-integration/spec.md b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/specs/techdocs-integration/spec.md new file mode 100644 index 00000000000..a00ba8c3a82 --- /dev/null +++ b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/specs/techdocs-integration/spec.md @@ -0,0 +1,213 @@ +## Connector Self-Discovery + +### Requirement: Connector resolves its own base URL at startup + +The connector plugin SHALL use `coreServices.discovery` to determine its own HTTP base URL, so that TechDocsKey paths can be resolved into full URLs by downstream consumers. + +#### Scenario: connectorBaseUrl is populated at init + +- **WHEN** the connector plugin initializes +- **THEN** `discovery.getBaseUrl('kserve-kubeflow-connector')` is called +- **AND** the result is stored as `connectorBaseUrl` on `ConnectorConfig` +- **AND** `connectorBaseUrl` is threaded through to `ReconcilerConfig` + +#### Scenario: connectorBaseUrl reaches KServe.ts + +- **WHEN** `callKServeBackstagePrinters` is called during InferenceService reconciliation +- **THEN** `connectorBaseUrl` is passed as a parameter +- **AND** it is available inside `generateModelCatalog` for TechDocsKey construction + +--- + +## Auto TechDocsKey Annotation + +### Requirement: TechDocsKey is auto-set when catalog annotations are present + +When an InferenceService CR has `rhdh.io/catalog-source` and `rhdh.io/catalog-model` annotations but no explicit `model-catalog-bridge.ai.redhat.com/techdocs` annotation, the connector SHALL automatically set TechDocsKey as a path to the model card endpoint. + +#### Scenario: Auto-set TechDocsKey for annotated InferenceService + +- **WHEN** an InferenceService has `rhdh.io/catalog-source` = `` and `rhdh.io/catalog-model` = `` +- **AND** no `model-catalog-bridge.ai.redhat.com/techdocs` annotation is set +- **AND** `connectorBaseUrl` is defined +- **THEN** `techdocsUrl` is set to `/modelcard//` +- **AND** the value is a path only — no `url:` prefix, no `connectorBaseUrl` prefix +- **AND** the `TechDocs` key in the model annotations contains this path + +#### Scenario: Explicit TechDocsKey takes precedence + +- **WHEN** an InferenceService has a `model-catalog-bridge.ai.redhat.com/techdocs` annotation set +- **THEN** the explicit value is used as `techdocsUrl` +- **AND** the auto-set logic is skipped + +#### Scenario: No catalog annotations skips auto-set + +- **WHEN** an InferenceService does NOT have both `rhdh.io/catalog-source` and `rhdh.io/catalog-model` annotations +- **THEN** no TechDocsKey is auto-set +- **AND** `techdocsUrl` remains undefined (unless explicitly annotated) + +--- + +## TechDocsKey → backstage.io/techdocs-ref Pipeline + +### Requirement: TechDocsKey path is correctly transformed into a full techdocs-ref annotation + +The entity provider's `ModelCatalogGenerator.ts` SHALL prepend `svcUrl` and add the `url:` prefix to produce the final `backstage.io/techdocs-ref` annotation. + +#### Scenario: Full URL construction + +- **GIVEN** TechDocsKey is `/modelcard/redhat_ai_validated_models/RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16` +- **AND** svcUrl is `http://localhost:7007/api/kserve-kubeflow-connector` +- **WHEN** `ModelCatalogGenerator.ts` processes the TechDocs annotation +- **THEN** `backstage.io/techdocs-ref` is set to `url:http://localhost:7007/api/kserve-kubeflow-connector/modelcard/redhat_ai_validated_models/RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16` + +--- + +## Model Card Endpoint + +### Requirement: Model card endpoint handles multi-segment model names + +The connector's `/modelcard` route SHALL match URLs where the model name contains slashes. + +#### Scenario: Two-segment model name + +- **WHEN** a GET request is made to `/modelcard/sourceA/modelB` +- **THEN** the route matches with `sourceId` = `sourceA` and model name = `modelB` +- **AND** `getModelCard("sourceA/modelB")` is called + +#### Scenario: Three-segment model name (slash in model name) + +- **WHEN** a GET request is made to `/modelcard/redhat_ai_validated_models/RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16` +- **THEN** the route matches with `sourceId` = `redhat_ai_validated_models` and model name = `RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16` +- **AND** `getModelCard("redhat_ai_validated_models/RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16")` is called + +#### Scenario: Model card found + +- **WHEN** the model card key matches an entry in the model cards map +- **THEN** the response is 200 with `Content-Type: text/markdown` and the model card content + +#### Scenario: Model card not found + +- **WHEN** the model card key does NOT match any entry +- **THEN** the response is 404 + +--- + +## mkdocs.yml Generation + +### Requirement: URL reader generates mkdocs.yml for TechDocs builder + +The url-reader's `dir()` method SHALL write a `mkdocs.yml` file alongside the `docs/` directory so that the TechDocs builder (`mkdocs build`) can process the model card markdown. + +#### Scenario: mkdocs.yml is created + +- **WHEN** `dir()` is called on a `ModelCatalogBridgeUrlReaderServiceReadTreeResponse` +- **THEN** a file `mkdocs.yml` is written at the root of the output directory +- **AND** the file contains at minimum `site_name` and `nav` fields +- **AND** the `docs/index.md` file is written alongside it + +#### Scenario: TechDocs builder succeeds + +- **WHEN** the TechDocs builder runs `mkdocs build` on the output directory +- **THEN** the build succeeds without "Could not read MkDocs YAML config file" errors +- **AND** the `techdocs-core` plugin is auto-injected by Backstage's patcher + +--- + +## URL Reader Config Alignment + +### Requirement: BridgeConfig matches cluster-nested config structure + +The url-reader's `BridgeConfig` type and config reading SHALL match the cluster-nested config structure used by the connector and entity provider. + +#### Scenario: Two-level config iteration + +- **GIVEN** config has `catalog.providers.modelCatalog.kserve-kubeflow-connector.cluster-1` and `.cluster-2` +- **WHEN** `readBridgeConfigs` is called +- **THEN** it returns two `BridgeConfig` entries, both with `id` = `kserve-kubeflow-connector` +- **AND** each entry has `name`, `kubeflowModelCatalogUrl`, `defaultOwner`, `defaultLifecycle` from its respective cluster sub-config + +#### Scenario: Missing optional fields default to empty string + +- **GIVEN** a cluster sub-config with no optional fields set +- **WHEN** `readBridgeConfig` is called +- **THEN** `name`, `kubeflowModelCatalogUrl`, `defaultOwner`, `defaultLifecycle` all default to `''` + +#### Scenario: No provider config returns empty array + +- **GIVEN** config has no `catalog.providers.modelCatalog` section +- **WHEN** `readBridgeConfigs` is called +- **THEN** it returns `[]` + +--- + +## Config Schema Declaration + +### Requirement: config.d.ts declares cluster sub-key fields + +The config schema in `catalog-backend-module-model-catalog/config.d.ts` SHALL declare the new fields so Backstage config validation does not strip them. + +#### Scenario: Config fields pass validation + +- **WHEN** `app-config.yaml` contains `name`, `kubeflow-model-catalog-url`, `default-owner`, `default-lifecycle` under a cluster sub-key +- **THEN** Backstage config validation preserves these fields +- **AND** `providerConfigs.keys()` returns a non-empty array + +#### Scenario: Existing fields still work + +- **WHEN** `app-config.yaml` contains `baseUrl`, `system`, `owner`, `schedule` at the connector level +- **THEN** these fields continue to pass validation and are accessible via the config API + +--- + +## Build and Quality + +### Requirement: All quality gates pass + +#### Scenario: TypeScript compilation + +- **WHEN** `yarn tsc` is run from the workspace root +- **THEN** it completes with zero errors + +#### Scenario: Full build + +- **WHEN** `yarn build:all` is run +- **THEN** it completes with zero errors + +#### Scenario: Unit tests + +- **WHEN** `yarn test -- --watchAll=false` is run in the url-reader plugin +- **THEN** all tests pass + +#### Scenario: Code formatting + +- **WHEN** `yarn prettier` is run +- **THEN** no formatting changes are needed + +#### Scenario: Lint + +- **WHEN** `yarn lint:all` is run +- **THEN** no lint errors are reported + +--- + +## Integration + +### Requirement: End-to-end model card rendering + +#### Scenario: Model card renders as TechDocs + +- **WHEN** an InferenceService with catalog annotations is discovered by the connector +- **AND** the entity provider creates a Backstage entity with `backstage.io/techdocs-ref` +- **AND** a user navigates to the entity's TechDocs page in the RHDH UI +- **THEN** the model card markdown is rendered as a documentation page + +#### Scenario: Works on upstream KServe/Kubeflow + +- **WHEN** the connector is pointed at an upstream KServe/Kubeflow installation +- **THEN** model cards render as TechDocs for annotated InferenceServices + +#### Scenario: Works on RHOAI on OCP + +- **WHEN** the connector is pointed at an RHOAI installation on OpenShift +- **THEN** model cards render as TechDocs for annotated InferenceServices diff --git a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/tasks.md b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/tasks.md new file mode 100644 index 00000000000..803206008e7 --- /dev/null +++ b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/tasks.md @@ -0,0 +1,80 @@ +# Tasks: TechDocs / Model Card Integration Finalization + +## 1. Add Connector Self-Discovery via coreServices.discovery + +- [ ] 1.1 Add `discovery: coreServices.discovery` to the `deps` object in `plugins/kserve-kubeflow-connector-backend/src/plugin.ts` +- [ ] 1.2 Add `discovery` to the destructured `init` params +- [ ] 1.3 Call `const connectorBaseUrl = await discovery.getBaseUrl('kserve-kubeflow-connector')` before creating `connectorConfig` +- [ ] 1.4 Add `connectorBaseUrl?: string` to the `ConnectorConfig` interface in `InformerService.ts` +- [ ] 1.5 Add `connectorBaseUrl?: string` to the `ReconcilerConfig` interface in `types.ts` +- [ ] 1.6 Include `connectorBaseUrl` in the `connectorConfig` object created in `plugin.ts` +- [ ] 1.7 Thread `connectorConfig.connectorBaseUrl` into `ReconcilerConfig` in `setupInformer` +- [ ] 1.8 Verify `yarn tsc` passes + +## 2. Navigate Cluster-Nested Config in Connector plugin.ts + +- [ ] 2.1 Update config reading in `plugin.ts` to navigate `catalog.providers.modelCatalog.kserve-kubeflow-connector` then iterate cluster sub-keys +- [ ] 2.2 Read `kubeflow-model-catalog-url`, `default-owner`, `default-lifecycle` from the cluster sub-config (not the connector-level config) +- [ ] 2.3 Add TODO comment at the cluster key iteration point noting where multi-cluster looping would occur +- [ ] 2.4 Verify `yarn tsc` passes + +## 3. Auto-Set TechDocsKey in KServe.ts + +- [ ] 3.1 Import `CATALOG_SOURCE_ANNOTATION` and `CATALOG_MODEL_ANNOTATION` from `./Catalog` in `KServe.ts` +- [ ] 3.2 Add `connectorBaseUrl?: string` parameter to `callBackstagePrinters` function signature +- [ ] 3.3 Add `connectorBaseUrl?: string` parameter to `generateModelCatalog` function signature +- [ ] 3.4 Pass `connectorBaseUrl` from `callBackstagePrinters` through to `generateModelCatalog` +- [ ] 3.5 In `generateModelCatalog`, after reading `techdocsUrl` from annotations: when `techdocsUrl` is undefined AND `connectorBaseUrl` is defined AND catalog annotations (`CATALOG_SOURCE_ANNOTATION`, `CATALOG_MODEL_ANNOTATION`) are both present on the InferenceService, set `techdocsUrl = `/modelcard/${sourceId}/${modelName}`\` — path only, no `url:` prefix and no `connectorBaseUrl` prefix (ModelCatalogGenerator.ts handles both) +- [ ] 3.6 In `InformerService.ts`, pass `config.connectorBaseUrl` to `callKServeBackstagePrinters` as the new parameter +- [ ] 3.7 Verify `yarn tsc` passes + +## 4. Wildcard Model Card Route + +- [ ] 4.1 Change Express route in `router.ts` from `/modelcard/:sourceId/:modelName` to `/modelcard/:sourceId/*` +- [ ] 4.2 Change `req.params.modelName` to `(req.params as Record)[0]` — TypeScript's Express types don't allow numeric indexing when named params are also present +- [ ] 4.3 Verify `yarn tsc` passes +- [ ] 4.4 Verify via `curl` that URLs with multi-segment model names (e.g., `/modelcard/redhat_ai_validated_models/RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16`) return 200 + +## 5. Add mkdocs.yml Generation to URL Reader + +- [ ] 5.1 In `catalog-techdoc-url-reader-backend/src/plugin.ts`, update the `dir()` method of `ModelCatalogBridgeUrlReaderServiceReadTreeResponse` +- [ ] 5.2 Before creating the `docs/` subdirectory, write a minimal `mkdocs.yml` at the root of `dir`: `site_name: Model Card\nnav:\n - Home: index.md\n` +- [ ] 5.3 Verify `yarn tsc` passes +- [ ] 5.4 Verify TechDocs page renders in browser (requires `mkdocs` and `mkdocs-techdocs-core` pip packages installed) + +## 6. Align URL Reader BridgeConfig with Cluster-Nested Config + +- [ ] 6.1 Update `BridgeConfig` type: replace `baseUrl: string` with `name: string`, `kubeflowModelCatalogUrl: string`, `defaultOwner: string`, `defaultLifecycle: string` +- [ ] 6.2 Update `readBridgeConfigs` to iterate two levels: connector keys then cluster sub-keys +- [ ] 6.3 Update `readBridgeConfig` to read `name`, `kubeflow-model-catalog-url`, `default-owner`, `default-lifecycle` from the cluster sub-config +- [ ] 6.4 Update test fixtures in `plugin.test.ts` to use cluster-nested config structure and new `BridgeConfig` fields +- [ ] 6.5 Verify url-reader tests pass (`yarn test -- --watchAll=false`) +- [ ] 6.6 Verify `yarn tsc` passes + +## 7. Extend config.d.ts Schema + +- [ ] 7.1 Add `[clusterKey: string]` index signature to the connector key object in `catalog-backend-module-model-catalog/config.d.ts` +- [ ] 7.2 Declare `name?`, `kubeflow-model-catalog-url?`, `default-owner?`, `default-lifecycle?` fields in the cluster sub-key type +- [ ] 7.3 Use union type to accommodate both cluster sub-key objects and existing scalar/schedule fields +- [ ] 7.4 Verify `yarn tsc` passes +- [ ] 7.5 Verify Backstage config validation no longer strips the new fields (connector's `providerConfigs.keys()` returns non-empty array) + +## 8. Update app-config.yaml + +- [ ] 8.1 Add `cluster-1:` nesting level under `kserve-kubeflow-connector:` in `app-config.yaml` +- [ ] 8.2 Indent `name`, `kubeflow-model-catalog-url`, `default-owner`, `default-lifecycle` under `cluster-1:` +- [ ] 8.3 Add `name: my-k8s-cluster` field + +## 9. Verification + +- [ ] 9.1 `yarn tsc` passes with no errors +- [ ] 9.2 `yarn build:all` succeeds +- [ ] 9.3 Unit tests pass (`yarn test -- --watchAll=false` in url-reader plugin) +- [ ] 9.4 Prettier checks pass (`yarn prettier`) +- [ ] 9.5 Lint checks pass (`yarn lint:all`) +- [ ] 9.6 `curl` to `/modelcard//` returns 200 with model card markdown +- [ ] 9.7 TechDocs page renders in RHDH UI for entities with auto-set `backstage.io/techdocs-ref` +- [ ] 9.8 Integration tested against upstream KServe/Kubeflow +- [ ] 9.9 Integration tested against RHOAI on OCP +- [ ] 9.10 `connectorBaseUrl` is populated at connector startup (verify in logs or debugger) +- [ ] 9.11 TechDocsKey annotation is auto-set as path only (no `url:` prefix, no `connectorBaseUrl` prefix) — verify the resulting `backstage.io/techdocs-ref` is `url:/modelcard//` From 689e3a7db15cbb4677d892039f0e920fe9a130b0 Mon Sep 17 00:00:00 2001 From: gabemontero Date: Fri, 31 Jul 2026 16:04:25 -0400 Subject: [PATCH 2/4] chore(openspec): add missing .openspec.yaml manifest Adds the required .openspec.yaml manifest file to the techdocs-modelcard-finalization openspec change directory. Signed-off-by: gabemontero Assisted-by: Claude Opus 4.6 Co-Authored-By: Claude Opus 4.6 --- .../changes/techdocs-modelcard-finalization/.openspec.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/.openspec.yaml diff --git a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/.openspec.yaml b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/.openspec.yaml new file mode 100644 index 00000000000..d8c366f43bc --- /dev/null +++ b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +created: 2026-07-31 +status: draft From 898d608587e00071efc2a38e3b792c925a3e4929 Mon Sep 17 00:00:00 2001 From: gabemontero Date: Fri, 31 Jul 2026 16:13:36 -0400 Subject: [PATCH 3/4] fix(openspec): address review findings on techdocs-modelcard-finalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Correct D5 rationale: entity provider uses flat single-level config iteration, not two-level — it reads connector-level fields and is unaffected by this change - Update D5 pseudocode to filter non-cluster keys (e.g. schedule) via getOptionalConfig and field presence checks - Remove stale baseUrl references (replaced by cluster-level fields) - Add full connector base URL to curl verification examples - Update spec scenario for existing config fields (baseUrl removed) Signed-off-by: gabemontero Assisted-by: Claude Opus 4.6 Co-Authored-By: Claude Opus 4.6 --- .../techdocs-modelcard-finalization/design.md | 13 ++++++++----- .../specs/techdocs-integration/spec.md | 4 ++-- .../techdocs-modelcard-finalization/tasks.md | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/design.md b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/design.md index 849dc6e8f91..e16f0f06ca8 100644 --- a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/design.md +++ b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/design.md @@ -55,7 +55,7 @@ catalog: ... ``` -Both the connector (`plugin.ts`) and url-reader (`readBridgeConfigs`) navigate this two-level structure. The entity provider's existing config reading at `catalog.providers.modelCatalog` also uses this pattern. The `config.d.ts` schema must declare these fields or Backstage's config validation silently strips them. +Both the connector (`plugin.ts`) and url-reader (`readBridgeConfigs`) navigate this two-level structure. The entity provider's config reading (`readModelCatalogApiEntityConfigs` in `config.ts`) uses flat single-level iteration and reads connector-level fields (e.g., `schedule`) — it does not need the cluster sub-keys and is unaffected by this change. The `config.d.ts` schema must declare these fields or Backstage's config validation silently strips them. ## Goals / Non-Goals @@ -117,19 +117,22 @@ If KServe.ts included the full URL or the `url:` prefix, the result would be dou ### D5 — Two-level config iteration in url-reader -**Choice:** Update `readBridgeConfigs` to iterate connector keys then cluster sub-keys: +**Choice:** Update `readBridgeConfigs` to iterate connector keys then cluster sub-keys, filtering out non-cluster keys (e.g., `schedule`) that may coexist at the connector level: ```typescript for (const connectorId of configs.keys()) { const connectorConfig = configs.getConfig(connectorId); for (const clusterKey of connectorConfig.keys()) { - const clusterConfig = connectorConfig.getConfig(clusterKey); + const clusterConfig = connectorConfig.getOptionalConfig(clusterKey); + if (!clusterConfig || !clusterConfig.has('kubeflow-model-catalog-url')) { + continue; // skip connector-level non-cluster objects (e.g., schedule) + } result.push(readBridgeConfig(connectorId, clusterConfig)); } } ``` -**Rationale:** The config structure nests cluster keys under connector keys (e.g., `kserve-kubeflow-connector.cluster-1`). The old flat iteration (`configs.keys().map(id => readBridgeConfig(id, ...))`) treated the connector key as the leaf, missing the cluster level entirely. The entity provider already uses this two-level pattern. +**Rationale:** The config structure nests cluster keys under connector keys (e.g., `kserve-kubeflow-connector.cluster-1`). The old flat iteration (`configs.keys().map(id => readBridgeConfig(id, ...))`) treated the connector key as the leaf, missing the cluster level entirely. The entity provider's config reading (`readModelCatalogApiEntityConfigs`) uses flat single-level iteration for its own connector-level fields and is unaffected by this change. Connector-level keys like `schedule` may coexist alongside cluster sub-keys, so the iteration must filter them via `getOptionalConfig` and field presence checks. ### D6 — Config schema in config.d.ts uses index signature for cluster sub-keys @@ -161,6 +164,6 @@ After all changes: 2. `yarn build:all` succeeds 3. Unit tests pass (`yarn test -- --watchAll=false` in url-reader plugin) 4. Prettier and lint checks pass -5. `curl` to `/modelcard/redhat_ai_validated_models/RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16` returns 200 with model card markdown +5. `curl http://localhost:7007/api/kserve-kubeflow-connector/modelcard/redhat_ai_validated_models/RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16` returns 200 with model card markdown 6. TechDocs page renders in browser for entities with auto-set `backstage.io/techdocs-ref` 7. Integration tested against upstream KServe/Kubeflow and RHOAI on OCP diff --git a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/specs/techdocs-integration/spec.md b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/specs/techdocs-integration/spec.md index a00ba8c3a82..a367e65fe2c 100644 --- a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/specs/techdocs-integration/spec.md +++ b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/specs/techdocs-integration/spec.md @@ -153,9 +153,9 @@ The config schema in `catalog-backend-module-model-catalog/config.d.ts` SHALL de - **THEN** Backstage config validation preserves these fields - **AND** `providerConfigs.keys()` returns a non-empty array -#### Scenario: Existing fields still work +#### Scenario: Existing connector-level fields still work -- **WHEN** `app-config.yaml` contains `baseUrl`, `system`, `owner`, `schedule` at the connector level +- **WHEN** `app-config.yaml` contains `system`, `owner`, `schedule` at the connector level - **THEN** these fields continue to pass validation and are accessible via the config API --- diff --git a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/tasks.md b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/tasks.md index 803206008e7..1cd55a0876c 100644 --- a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/tasks.md +++ b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/tasks.md @@ -45,7 +45,7 @@ ## 6. Align URL Reader BridgeConfig with Cluster-Nested Config - [ ] 6.1 Update `BridgeConfig` type: replace `baseUrl: string` with `name: string`, `kubeflowModelCatalogUrl: string`, `defaultOwner: string`, `defaultLifecycle: string` -- [ ] 6.2 Update `readBridgeConfigs` to iterate two levels: connector keys then cluster sub-keys +- [ ] 6.2 Update `readBridgeConfigs` to iterate two levels: connector keys then cluster sub-keys, filtering out connector-level non-cluster objects (e.g., `schedule`) via `getOptionalConfig` and field presence checks (e.g., `has('kubeflow-model-catalog-url')`) - [ ] 6.3 Update `readBridgeConfig` to read `name`, `kubeflow-model-catalog-url`, `default-owner`, `default-lifecycle` from the cluster sub-config - [ ] 6.4 Update test fixtures in `plugin.test.ts` to use cluster-nested config structure and new `BridgeConfig` fields - [ ] 6.5 Verify url-reader tests pass (`yarn test -- --watchAll=false`) @@ -72,7 +72,7 @@ - [ ] 9.3 Unit tests pass (`yarn test -- --watchAll=false` in url-reader plugin) - [ ] 9.4 Prettier checks pass (`yarn prettier`) - [ ] 9.5 Lint checks pass (`yarn lint:all`) -- [ ] 9.6 `curl` to `/modelcard//` returns 200 with model card markdown +- [ ] 9.6 `curl http://localhost:7007/api/kserve-kubeflow-connector/modelcard//` returns 200 with model card markdown - [ ] 9.7 TechDocs page renders in RHDH UI for entities with auto-set `backstage.io/techdocs-ref` - [ ] 9.8 Integration tested against upstream KServe/Kubeflow - [ ] 9.9 Integration tested against RHOAI on OCP From 6ad98cff79a64bbdd4bd90431e942bcc47ea1edd Mon Sep 17 00:00:00 2001 From: gabemontero Date: Fri, 31 Jul 2026 16:15:01 -0400 Subject: [PATCH 4/4] fix(openspec): clarify localhost:7007 URLs assume yarn dev Add parenthetical noting that http://localhost:7007 URLs assume RHDH running locally via yarn dev from the ai-integrations workspace. Signed-off-by: gabemontero Assisted-by: Claude Opus 4.6 Co-Authored-By: Claude Opus 4.6 --- .../changes/techdocs-modelcard-finalization/design.md | 4 ++-- .../specs/techdocs-integration/spec.md | 2 +- .../openspec/changes/techdocs-modelcard-finalization/tasks.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/design.md b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/design.md index e16f0f06ca8..086b54b8fe8 100644 --- a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/design.md +++ b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/design.md @@ -25,7 +25,7 @@ KServe.ts: generateModelCatalog() ↓ [REST API: /models/:model/:version] ModelCatalogGenerator.ts (entity provider) → reads TechDocsKey annotation - → prepends svcUrl: "http://localhost:7007/api/kserve-kubeflow-connector" + → prepends svcUrl: "http://localhost:7007/api/kserve-kubeflow-connector" (when running via `yarn dev`) → wraps in url: prefix → sets backstage.io/techdocs-ref = "url:http://localhost:7007/api/kserve-kubeflow-connector/modelcard//" ↓ [Backstage TechDocs builder] @@ -164,6 +164,6 @@ After all changes: 2. `yarn build:all` succeeds 3. Unit tests pass (`yarn test -- --watchAll=false` in url-reader plugin) 4. Prettier and lint checks pass -5. `curl http://localhost:7007/api/kserve-kubeflow-connector/modelcard/redhat_ai_validated_models/RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16` returns 200 with model card markdown +5. `curl http://localhost:7007/api/kserve-kubeflow-connector/modelcard/redhat_ai_validated_models/RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16` returns 200 with model card markdown (assumes RHDH running locally via `yarn dev` from the `ai-integrations` workspace) 6. TechDocs page renders in browser for entities with auto-set `backstage.io/techdocs-ref` 7. Integration tested against upstream KServe/Kubeflow and RHOAI on OCP diff --git a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/specs/techdocs-integration/spec.md b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/specs/techdocs-integration/spec.md index a367e65fe2c..dc98eca795a 100644 --- a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/specs/techdocs-integration/spec.md +++ b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/specs/techdocs-integration/spec.md @@ -57,7 +57,7 @@ The entity provider's `ModelCatalogGenerator.ts` SHALL prepend `svcUrl` and add #### Scenario: Full URL construction - **GIVEN** TechDocsKey is `/modelcard/redhat_ai_validated_models/RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16` -- **AND** svcUrl is `http://localhost:7007/api/kserve-kubeflow-connector` +- **AND** svcUrl is `http://localhost:7007/api/kserve-kubeflow-connector` (when running via `yarn dev` from the `ai-integrations` workspace) - **WHEN** `ModelCatalogGenerator.ts` processes the TechDocs annotation - **THEN** `backstage.io/techdocs-ref` is set to `url:http://localhost:7007/api/kserve-kubeflow-connector/modelcard/redhat_ai_validated_models/RedHatAI/Meta-Llama-3.1-8B-Instruct-quantized.w4a16` diff --git a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/tasks.md b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/tasks.md index 1cd55a0876c..0776edc6c36 100644 --- a/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/tasks.md +++ b/workspaces/ai-integrations/openspec/changes/techdocs-modelcard-finalization/tasks.md @@ -72,7 +72,7 @@ - [ ] 9.3 Unit tests pass (`yarn test -- --watchAll=false` in url-reader plugin) - [ ] 9.4 Prettier checks pass (`yarn prettier`) - [ ] 9.5 Lint checks pass (`yarn lint:all`) -- [ ] 9.6 `curl http://localhost:7007/api/kserve-kubeflow-connector/modelcard//` returns 200 with model card markdown +- [ ] 9.6 `curl http://localhost:7007/api/kserve-kubeflow-connector/modelcard//` returns 200 with model card markdown (assumes RHDH running locally via `yarn dev` from the `ai-integrations` workspace) - [ ] 9.7 TechDocs page renders in RHDH UI for entities with auto-set `backstage.io/techdocs-ref` - [ ] 9.8 Integration tested against upstream KServe/Kubeflow - [ ] 9.9 Integration tested against RHOAI on OCP