Skip to content

Slice/m2.1#348

Open
failerko wants to merge 33 commits into
mainfrom
slice/M2.1
Open

Slice/m2.1#348
failerko wants to merge 33 commits into
mainfrom
slice/M2.1

Conversation

@failerko

@failerko failerko commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • AI provider setup for OpenAI-compatible endpoints and API keys
    • Notification banner prompting setup when no AI providers are configured
    • One-click "quick wire" to rapidly wire a selected model
    • Deep-linkable settings tabs (e.g., /settings?tab=providers)
    • Automatic fetching of provider model catalogs
  • Tests

    • Expanded test coverage for provider management, model resolution, and transport/retry behaviors
  • Documentation

    • Added guidance on test setup and module-graph pitfalls

failerko and others added 30 commits June 12, 2026 12:17
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real providers from appSettingsStore are now the primary lookup source;
the temporary stub registry stays as fallback until Slice 2.7 removes it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
429 is special-cased before the generic 4xx rule so it stays retryable; parseRetryAfter handles seconds and HTTP-date values. retryAfterMs lives on the classifier return object only — CallRetryError is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Provider retry attempts now wait classified.retryAfterMs (from a Retry-After header) or exponential backoff (500ms base, 30s cap) between attempts. The delay is abortable — aborting mid-backoff resolves the promise with {status:'aborted'} immediately. Parse retries are unaffected. Tests for both new timing paths added; existing provider-retry tests converted to fake timers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wraps generateText/streamText so every provider call carries maxRetries:0 (callWithRetry is the sole retry authority), SDK-native timeout config, and the caller's abort signal. mapCallError distinguishes APICallError (rethrow for classifyProviderError), internal SDK timeout (→ ProviderTimeoutError), and genuine user cancel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Promotes the CallRetryError→PipelineError mapper from a test-local function into lib/pipeline/call-error.ts, re-exported from the pipeline barrel. lib/ai stays free of the upward PipelineError dependency; fault-scenarios now imports via public barrels only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Once an action imported AGENT_IDS from @/lib/ai, vitest.setup.ts's registerAllDomains pulled the whole @/lib/ai barrel — the 'ai' SDK plus transport/fetch and model — into every unit test's setup graph, loading those modules before the test files' vi.mocks registered and silently breaking them. AGENT_IDS is config data that indexes app_settings.assignments, so lib/db is its proper home; lib/ai re-exports it so its public surface is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real provider keys were never fed to the redaction comparator (only the stub registry was), so a configured OAI-compat call leaked its raw Authorization key into the diagnostics buffer; app_settings hydrate now syncs the comparator from providers[] on every (re)hydrate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setHttpCallKnownSecretValues replaces the whole comparator and app_settings hydrate is the other writer, so registerStubProvider() overwrote it with only the stub key and dropped a configured provider's real key; the stub sync now merges the configured keys so a real API key can never be evicted from redaction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An unbounded Retry-After header (e.g. 3600s) made callWithRetry sleep for the full duration, bypassing BACKOFF_CAP_MS and stalling the phase; the honored delay is now capped at the same ceiling as the exponential fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
opts is now the SDK's own generateText/streamText options type (Parameters<typeof ...>), so timeout (TimeoutConfiguration) and the output schema flow through with no hand-rolled TimeoutConfig or narrow opts object; the proxies just force maxRetries:0 and pass through, returning the full SDK result. Error classification is consolidated in classifyProviderError (dropped the proxy-level mapCallError + .text extraction), which also resolves the prior streamProviderCall asymmetry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dev smoke phase now resolves the quick-wired narrative model and makes a real streamed call against the user's endpoint (the only real-generation path until 2.7), surfacing the reply as the entry content and landing a redacted streamed call in httpCallSink; it falls back to the stub when no provider is configured so the smoke and its tests still run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 764b7336-a72f-4ea4-b94f-9f7f63a5fa59

📥 Commits

Reviewing files that changed from the base of the PR and between 64dd4a4 and d3675ca.

📒 Files selected for processing (13)
  • .nvmrc
  • components/compounds/provider-setup-form.tsx
  • lib/actions/settings/providers.ts
  • lib/ai/agents.ts
  • lib/ai/catalog.ts
  • lib/ai/providers.ts
  • lib/ai/resolve-model.test.ts
  • lib/ai/resolve-model.ts
  • lib/db/app-settings/agents.ts
  • lib/db/index.ts
  • lib/db/stories/story-config-schema.ts
  • lib/pipeline/call-error.test.ts
  • lib/pipeline/call-error.ts
✅ Files skipped from review due to trivial changes (1)
  • .nvmrc
🚧 Files skipped from review as they are similar to previous changes (7)
  • lib/pipeline/call-error.test.ts
  • lib/pipeline/call-error.ts
  • lib/ai/providers.ts
  • lib/ai/resolve-model.ts
  • lib/ai/catalog.ts
  • components/compounds/provider-setup-form.tsx
  • lib/actions/settings/providers.ts

📝 Walkthrough

Walkthrough

This PR implements a complete provider abstraction system enabling users to configure and use OpenAI-compatible AI providers. It introduces agent ID registry, model resolution logic, provider mutations, catalog fetching, retry/backoff handling with Retry-After support, secret redaction, UI components for provider setup, and deep-linked settings routing. All layers are comprehensively tested and documented.

Changes

Provider Configuration System

Layer / File(s) Summary
Agent ID registry and type exports
lib/db/app-settings/agents.ts, lib/ai/agents.ts, lib/db/index.ts
Introduces AGENT_IDS as single-source-of-truth agent identifier list and derives AgentId type; re-exports through lib/db and lib/ai public surfaces.
Model resolution configuration and logic
lib/ai/resolve-model.ts, lib/ai/resolve-model.test.ts
Defines config/result/param types and implements core resolution that maps narrative/agent requests to provider/model pairs via assignments and profiles; handles story overrides and failure cases.
Catalog fetching and provider model creation
lib/ai/catalog.ts, lib/ai/catalog.test.ts, lib/ai/providers.ts, lib/ai/providers.test.ts
Fetches remote /models endpoints with optional Bearer auth; creates OpenAI-compatible model clients with request capture wiring using @ai-sdk/openai-compatible.
Model lookup with configured provider preference
lib/ai/model.ts, lib/ai/model.test.ts
Updates getModel to consult configured providers from appSettingsStore before falling back to temporary stub registry.
Provider mutations and persistence
lib/actions/settings/providers.ts, lib/actions/settings/providers.test.ts, lib/actions/settings/index.ts, lib/actions/index.ts
Implements actions to add/update/remove providers, set default provider, upsert profiles, assign models to agents, and quick-wire complete setups; persists to database and rehydrates store.
Provider secret redaction and hydration
lib/stores/app-settings/app-settings.ts, lib/ai/stub/temporary-registry.ts, lib/ai/stub/temporary-registry.test.ts
Registers configured provider API keys into HTTP header redaction during hydration; syncs secrets from both configured and temporary providers.
Retry backoff, Retry-After parsing, and call wrappers
lib/ai/transport/call-with-retry.ts, lib/ai/transport/call-with-retry.test.ts, lib/ai/transport/classify-provider-error.ts, lib/ai/transport/classify-provider-error.test.ts, lib/ai/transport/provider-call.ts, lib/ai/transport/provider-call.test.ts
Adds exponential backoff with abort support; extracts and respects HTTP Retry-After headers on 429 responses; wraps AI SDK calls to enforce maxRetries: 0 while preserving options.
Pipeline error conversion
lib/pipeline/call-error.ts, lib/pipeline/call-error.test.ts, lib/pipeline/index.ts, lib/pipeline/__tests__/fault-scenarios.test.ts
Maps CallRetryError tiers into PipelineError kind discriminant; centralizes error classification for pipeline phases.
AI module public surface
lib/ai/index.ts
Re-exports agents, model resolution, catalog, provider creation, retry/transport, and error classification for unified public API.
Provider setup UI component
components/compounds/provider-setup-form.tsx, components/compounds/provider-setup-form.stories.tsx
React Native form for adding/editing OpenAI-compatible providers; fetches and caches model catalogs; selects models via picker; quick-wires profile assignments; comprehensive Storybook coverage with fixtures.
AI configuration banner
components/compounds/ai-config-banner.tsx, components/compounds/ai-config-banner.stories.tsx
Warning banner displayed when no providers configured; tappable CTA navigates to settings providers tab.
Settings routing and landing page integration
app/settings/index.tsx, app/index.tsx
Settings route supports deep-linking to specific tabs via ?tab=providers query parameter; landing page displays AiConfigBanner when providers list is empty.
Smoke pipeline provider integration
components/reader/smoke/smoke-pipeline.ts
Smoke pipeline attempts real streamed narrative generation using resolved provider models; falls back to stub on resolution failure; populates story entry with generated content.
Documentation, lessons learned, and localization
docs/implementation/lessons-learned/*, docs/implementation/milestones/02-first-user-loop/slices/01-provider.md, locales/en/common.json, locales/en/settings.json, package.json
Documents vitest.setup import graph mocking pitfalls with mitigation steps; finalizes provider slice implementation notes; adds English translations for AI banner and provider settings; adds @ai-sdk/openai-compatible dependency.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #335: This PR implements the provider abstraction and OpenAI-compatible provider support, directly addressing the Slice 2.1 objectives described in the issue.

Suggested reviewers

  • a-frazier

🐰 A warren of providers now configured,
With agents wired and secrets redacted,
Quick-wire the narrative flame—
Smoke pipeline breathes real, at last.
No more stubs; the user's choice reigns. 🔑✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Slice/m2.1' is vague and generic, using abbreviated terminology without conveying meaningful information about the actual changes. Replace with a descriptive title that captures the main change, such as 'Add provider configuration and AI model setup form' or 'Implement openai-compatible provider support with provider settings UI'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces AI provider configuration and model routing. It adds an AI configuration banner on the landing page when no providers are configured, a provider setup form in settings to configure an OpenAI-compatible provider, and updates the settings route to support deep linking to specific tabs. It also implements model resolution, exponential backoff with Retry-After support, and API key redaction in diagnostics. The review feedback highlights several important improvements: using React effects to properly sync asynchronous store values and route parameters in the UI, using unsaved input values when fetching the model catalog, conditionally omitting the Authorization header for local providers that do not require an API key, and performing case-insensitive header lookups for retry-after.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread components/compounds/provider-setup-form.tsx
Comment thread components/compounds/provider-setup-form.tsx
Comment thread components/compounds/provider-setup-form.tsx Outdated
Comment thread app/settings/index.tsx
Comment thread app/settings/index.tsx
Comment thread lib/ai/catalog.ts Outdated
Comment thread lib/ai/transport/classify-provider-error.ts
failerko and others added 2 commits June 13, 2026 00:34
The /models catalog fetch hand-rolled an unconditional Authorization header, sending a bare `Bearer ` for keyless local endpoints (LM Studio, llama.cpp) where createOpenAICompatible already omits it — and that fetch is the first call such a setup makes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (6)
components/compounds/provider-setup-form.tsx (1)

169-175: 🗄️ Data Integrity & Integration | ⚡ Quick win

Make custom model insertion idempotent to avoid duplicate persisted IDs.

At Line 174, appending directly can store repeated customModelIds when the same model is added multiple times.

Suggested refactor
               await updateProvider(
                 provider.id,
-                { customModelIds: [...(provider.customModelIds ?? []), ref.modelId] },
+                {
+                  customModelIds: Array.from(
+                    new Set([...(provider.customModelIds ?? []), ref.modelId]),
+                  ),
+                },
                 { db },
               )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/compounds/provider-setup-form.tsx` around lines 169 - 175, The
onAddCustom handler currently appends ref.modelId into provider.customModelIds
and can create duplicates; update the logic in onAddCustom to first read
provider.customModelIds (via provider.customModelIds) and only add ref.modelId
if it isn't already present (e.g., check includes or use a Set) before calling
updateProvider(provider.id, { customModelIds: ... }, { db }) so persisted
customModelIds remain idempotent.
app/settings/index.tsx (1)

51-53: 🎯 Functional Correctness | ⚡ Quick win

Sync selected tab with query param updates, not just initial mount.

At Line 53, the useState initializer runs once, so later tab query changes won’t update selectedTab.

Suggested refactor
-import { useState } from 'react'
+import { useEffect, useState } from 'react'
@@
-  const { tab } = useLocalSearchParams<{ tab?: string }>()
-  const initialTab = SETTINGS_TAB_IDS.includes(tab as SettingsTabId) ? (tab as SettingsTabId) : null
-  const [selectedTab, setSelectedTab] = useState<SettingsTabId | null>(initialTab)
+  const { tab } = useLocalSearchParams<{ tab?: string | string[] }>()
+  const tabParam = Array.isArray(tab) ? tab[0] : tab
+  const resolvedTab = SETTINGS_TAB_IDS.find((id) => id === tabParam) ?? null
+  const [selectedTab, setSelectedTab] = useState<SettingsTabId | null>(resolvedTab)
+
+  useEffect(() => {
+    setSelectedTab(resolvedTab)
+  }, [resolvedTab])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/settings/index.tsx` around lines 51 - 53, The selectedTab state only uses
the initialTab computed once, so subsequent changes to the URL query param `tab`
(from useLocalSearchParams) won’t update it; add a useEffect that watches `tab`
(or `initialTab`) and calls setSelectedTab with the computed SettingsTabId (or
null) whenever `tab` changes to keep selectedTab in sync with the query param;
reference the existing identifiers useLocalSearchParams, initialTab,
selectedTab, and setSelectedTab when implementing this effect.
lib/ai/resolve-model.test.ts (1)

81-102: 📐 Maintainability & Code Quality | ⚡ Quick win

Add a regression test for wizard-assist ignoring storyModels overrides.

Current tests validate override short-circuit generally, but don’t pin the documented exception for wizard-assist. A focused case here would prevent contract drift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/ai/resolve-model.test.ts` around lines 81 - 102, Add a regression test in
resolve-model.test.ts that specifically asserts that
resolveModel('wizard-assist', cfg) ignores any entry in cfg.storyModels (e.g.,
storyModels: { 'wizard-assist': 'override-model' }) — create a
ResolveModelConfig mirroring the other tests (use base/defaultProviderId as
needed) and assert the result does NOT short-circuit to the override and instead
follows the normal wizard-assist resolution path (i.e., the returned
providerId/modelId should match the existing base behavior for wizard-assist
rather than 'override-model'); place this alongside the other tests using the
same expect/shape as resolveModel(...) assertions.
lib/ai/providers.test.ts (1)

15-25: 📐 Maintainability & Code Quality | ⚡ Quick win

Add regression coverage for the required endpoint guard.

The suite currently doesn’t assert the new endpoint requirement from createProviderModel (Line 45 in lib/ai/providers.ts). Adding this test prevents silent regressions.

Suggested test addition
 describe('createProviderModel · openai-compatible', () => {
+  it('throws when endpoint is blank/whitespace', () => {
+    expect(() =>
+      createProviderModel({ ...oai, endpoint: '   ' }, 'my-model'),
+    ).toThrow(/requires an endpoint/)
+  })
+
   it('builds a language model for an openai-compatible provider', () => {
     const model = createProviderModel(oai, 'my-model', 'action-1')
     expect(model).toBeDefined()
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/ai/providers.test.ts` around lines 15 - 25, The test suite is missing
coverage for the new required endpoint guard in createProviderModel; add a test
that copies the existing oai test fixture but removes or clears its endpoint
(e.g., const broken = { ...oai, endpoint: undefined } or delete broken.endpoint)
and assert that calling createProviderModel(broken, 'm', 'action-1') throws;
place this alongside the existing openai-compatible tests so the provider
endpoint requirement is enforced and prevents regressions.
lib/ai/model.test.ts (1)

89-102: 📐 Maintainability & Code Quality | ⚡ Quick win

Add an explicit precedence test for ID collisions (configured vs temporary).

This file validates configured resolution and fallback separately, but not the key new rule: configured provider should win when both sources contain the same providerId.

Suggested test addition
   it('resolves a real openai-compatible provider from app settings', () => {
@@
     const model = getModel('prov-real', 'my-model')
     expect(model).toBeDefined()
   })
+
+  it('prefers configured provider over temporary provider when ids collide', () => {
+    appSettings.providers = [
+      {
+        id: 'shared-id',
+        type: 'openai-compatible',
+        displayName: 'Configured',
+        apiKey: 'sk-real',
+        endpoint: 'http://localhost:1234/v1',
+        favoriteModelIds: [],
+      },
+    ]
+    setTemporaryProvidersForTests([
+      {
+        id: 'shared-id',
+        type: 'anthropic',
+        displayName: 'Temporary',
+        apiKey: 'sk-temp',
+        favoriteModelIds: ['claude-3-haiku-20240307'],
+      },
+    ])
+
+    expect(() => getModel('shared-id', 'my-model')).not.toThrow()
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/ai/model.test.ts` around lines 89 - 102, Add a unit test to assert
configured providers take precedence over temporary providers when IDs collide:
create appSettings.providers with an entry id 'prov-collide' (type
'openai-compatible', apiKey 'sk-config', endpoint etc.), also ensure the
temporary/ephemeral provider store (the same source getModel consults for temp
providers) contains a provider with the same id 'prov-collide' but different
metadata (e.g. apiKey 'sk-temp'); call getModel('prov-collide', 'any-model') and
assert the returned model corresponds to the configured provider (inspect
provider id/type or apiKey) rather than the temporary one; reference getModel
and appSettings.providers to locate where to add the test.
lib/ai/catalog.test.ts (1)

54-58: 📐 Maintainability & Code Quality | ⚡ Quick win

Extend endpoint-validation tests to include whitespace-only values.

The current test catches undefined endpoints, but not ' '. Add a whitespace case to protect the normalization contract.

Suggested test addition
   it('throws a clear error when the endpoint is missing', async () => {
     await expect(fetchModelCatalog({ ...provider, endpoint: undefined })).rejects.toThrow(
       /endpoint/,
     )
   })
+
+  it('throws a clear error when the endpoint is whitespace', async () => {
+    await expect(fetchModelCatalog({ ...provider, endpoint: '   ' })).rejects.toThrow(
+      /endpoint/,
+    )
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/ai/catalog.test.ts` around lines 54 - 58, Add a test case in
lib/ai/catalog.test.ts that calls fetchModelCatalog with provider where endpoint
is a whitespace-only string (e.g., '   ') and assert it rejects with an error
matching /endpoint/; specifically, mirror the existing test that uses
fetchModelCatalog({ ...provider, endpoint: undefined }) but pass endpoint: '   '
to ensure endpoint normalization rejects whitespace-only values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/compounds/provider-setup-form.tsx`:
- Around line 86-95: The wired success flag is not cleared when the selection
changes or when a quick-wire attempt fails, leaving stale success UI; update the
onQuickWire error path to call setWired(false) and ensure the selection-change
flow/reset for picked also clears wired (e.g., in the handler that sets picked
or a useEffect watching picked call setWired(false)). Reference functions/vars:
onQuickWire, quickWireModel, picked, wired, setWired so the wired state is reset
on failure and whenever picked changes.

In `@lib/actions/settings/providers.ts`:
- Around line 44-49: The update currently silently no-ops if no provider with
the given id exists; before calling providerInstanceSchema.parse and mapping,
check for existence by using the current providers array returned by
appSettingsStore.getAppSettings().providers (e.g., const current =
appSettingsStore.getAppSettings().providers; const existing = current.find(p =>
p.id === id)); if existing is undefined throw an error (or return a rejected
Promise) indicating the provider id was not found, otherwise proceed to build
next by mapping and call providerInstanceSchema.parse and persistConfig(ctx, {
providers: next }).
- Around line 26-28: After updating appSettings in save flow, the call to
rehydrateAppSettings(ctx.db) (invoked after await
ctx.db.update(...).where(eq(appSettings.id, APP_SETTINGS_SINGLETON_ID))) can
return a failure status that is currently ignored; change the code to capture
the result of rehydrateAppSettings and if it indicates failure (e.g., status ===
'config-corrupt' or similar error result) propagate that by throwing or
returning an error so the save action does not resolve successfully when
hydration failed—locate the save function around the ctx.db.update call and add
handling for rehydrateAppSettings' return value to surface the failure.

In `@lib/ai/providers.ts`:
- Around line 45-52: The provider endpoint isn’t normalized; update
createProviderModel and fetchModelCatalog to trim provider.endpoint once,
validate the trimmed value (e.g., ensure trimmed.length > 0) and use the trimmed
value for subsequent logic (instead of the raw provider.endpoint) — for example,
compute const endpoint = (provider.endpoint || '').trim() at the top of
createProviderModel and fetchModelCatalog, throw the existing error if endpoint
is empty, and pass endpoint into createOpenAICompatible / any URL construction
or fetch logic.

In `@lib/ai/resolve-model.ts`:
- Around line 60-66: The current override branch (`const override =
config.storyModels?.[target]`) applies storyModels for any ResolveTarget, which
lets disallowed targets like "wizard-assist" bypass normal resolution; restrict
this by only honoring overrides for the documented story-model targets: add an
allowlist (e.g., ALLOWED_STORY_MODEL_TARGETS) and check `if
(!ALLOWED_STORY_MODEL_TARGETS.includes(target))` before using `override`; keep
the rest of the logic (finding provider via `config.providers.find(p => p.id ===
config.defaultProviderId)` and returning the provider/model) unchanged so
disallowed targets ignore `storyModels` entries.

In `@lib/pipeline/call-error.ts`:
- Line 10: The provider error mapping in lib/pipeline/call-error.ts currently
drops empty-string details because it uses a truthy check; update the mapping
that builds the provider object (the line producing ? { kind: 'provider',
reason: e.reason, ...(e.detail ? { detail: e.detail } : {}) }) to use an
explicit undefined check (e.detail !== undefined) so that detail: '' is
preserved and only undefined is omitted.

---

Nitpick comments:
In `@app/settings/index.tsx`:
- Around line 51-53: The selectedTab state only uses the initialTab computed
once, so subsequent changes to the URL query param `tab` (from
useLocalSearchParams) won’t update it; add a useEffect that watches `tab` (or
`initialTab`) and calls setSelectedTab with the computed SettingsTabId (or null)
whenever `tab` changes to keep selectedTab in sync with the query param;
reference the existing identifiers useLocalSearchParams, initialTab,
selectedTab, and setSelectedTab when implementing this effect.

In `@components/compounds/provider-setup-form.tsx`:
- Around line 169-175: The onAddCustom handler currently appends ref.modelId
into provider.customModelIds and can create duplicates; update the logic in
onAddCustom to first read provider.customModelIds (via provider.customModelIds)
and only add ref.modelId if it isn't already present (e.g., check includes or
use a Set) before calling updateProvider(provider.id, { customModelIds: ... }, {
db }) so persisted customModelIds remain idempotent.

In `@lib/ai/catalog.test.ts`:
- Around line 54-58: Add a test case in lib/ai/catalog.test.ts that calls
fetchModelCatalog with provider where endpoint is a whitespace-only string
(e.g., '   ') and assert it rejects with an error matching /endpoint/;
specifically, mirror the existing test that uses fetchModelCatalog({
...provider, endpoint: undefined }) but pass endpoint: '   ' to ensure endpoint
normalization rejects whitespace-only values.

In `@lib/ai/model.test.ts`:
- Around line 89-102: Add a unit test to assert configured providers take
precedence over temporary providers when IDs collide: create
appSettings.providers with an entry id 'prov-collide' (type 'openai-compatible',
apiKey 'sk-config', endpoint etc.), also ensure the temporary/ephemeral provider
store (the same source getModel consults for temp providers) contains a provider
with the same id 'prov-collide' but different metadata (e.g. apiKey 'sk-temp');
call getModel('prov-collide', 'any-model') and assert the returned model
corresponds to the configured provider (inspect provider id/type or apiKey)
rather than the temporary one; reference getModel and appSettings.providers to
locate where to add the test.

In `@lib/ai/providers.test.ts`:
- Around line 15-25: The test suite is missing coverage for the new required
endpoint guard in createProviderModel; add a test that copies the existing oai
test fixture but removes or clears its endpoint (e.g., const broken = { ...oai,
endpoint: undefined } or delete broken.endpoint) and assert that calling
createProviderModel(broken, 'm', 'action-1') throws; place this alongside the
existing openai-compatible tests so the provider endpoint requirement is
enforced and prevents regressions.

In `@lib/ai/resolve-model.test.ts`:
- Around line 81-102: Add a regression test in resolve-model.test.ts that
specifically asserts that resolveModel('wizard-assist', cfg) ignores any entry
in cfg.storyModels (e.g., storyModels: { 'wizard-assist': 'override-model' }) —
create a ResolveModelConfig mirroring the other tests (use
base/defaultProviderId as needed) and assert the result does NOT short-circuit
to the override and instead follows the normal wizard-assist resolution path
(i.e., the returned providerId/modelId should match the existing base behavior
for wizard-assist rather than 'override-model'); place this alongside the other
tests using the same expect/shape as resolveModel(...) assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f766c7fe-0635-428b-b3bb-ad16873e648d

📥 Commits

Reviewing files that changed from the base of the PR and between d99b9fc and 64dd4a4.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (44)
  • app/index.tsx
  • app/settings/index.tsx
  • components/compounds/ai-config-banner.stories.tsx
  • components/compounds/ai-config-banner.tsx
  • components/compounds/provider-setup-form.stories.tsx
  • components/compounds/provider-setup-form.tsx
  • components/reader/smoke/smoke-pipeline.ts
  • docs/implementation/lessons-learned/README.md
  • docs/implementation/lessons-learned/test-setup-import-graph-breaks-mocks.md
  • docs/implementation/milestones/02-first-user-loop/slices/01-provider.md
  • lib/actions/index.ts
  • lib/actions/settings/index.ts
  • lib/actions/settings/providers.test.ts
  • lib/actions/settings/providers.ts
  • lib/ai/agents.test.ts
  • lib/ai/agents.ts
  • lib/ai/catalog.test.ts
  • lib/ai/catalog.ts
  • lib/ai/index.ts
  • lib/ai/model.test.ts
  • lib/ai/model.ts
  • lib/ai/providers.test.ts
  • lib/ai/providers.ts
  • lib/ai/resolve-model.test.ts
  • lib/ai/resolve-model.ts
  • lib/ai/stub/temporary-registry.test.ts
  • lib/ai/stub/temporary-registry.ts
  • lib/ai/transport/call-with-retry.test.ts
  • lib/ai/transport/call-with-retry.ts
  • lib/ai/transport/classify-provider-error.test.ts
  • lib/ai/transport/classify-provider-error.ts
  • lib/ai/transport/provider-call.test.ts
  • lib/ai/transport/provider-call.ts
  • lib/db/app-settings/agents.ts
  • lib/db/index.ts
  • lib/diagnostics/sinks/http-redaction.test.ts
  • lib/pipeline/__tests__/fault-scenarios.test.ts
  • lib/pipeline/call-error.test.ts
  • lib/pipeline/call-error.ts
  • lib/pipeline/index.ts
  • lib/stores/app-settings/app-settings.ts
  • locales/en/common.json
  • locales/en/settings.json
  • package.json

Comment thread components/compounds/provider-setup-form.tsx
Comment thread lib/actions/settings/providers.ts
Comment thread lib/actions/settings/providers.ts
Comment thread lib/ai/providers.ts Outdated
Comment thread lib/ai/resolve-model.ts Outdated
Comment thread lib/pipeline/call-error.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant