Skip to content

Add multi-provider search support (Gemini, OpenAI, Claude)#5

Merged
arberx merged 8 commits intomainfrom
arberx/multi-provider-search
Mar 10, 2026
Merged

Add multi-provider search support (Gemini, OpenAI, Claude)#5
arberx merged 8 commits intomainfrom
arberx/multi-provider-search

Conversation

@arberx
Copy link
Copy Markdown
Member

@arberx arberx commented Mar 10, 2026

Summary

  • Adds full multi-provider visibility run support across Gemini, OpenAI, and Claude/Anthropic in parallel
  • Introduces ProviderAdapter interface in packages/contracts and provider packages for OpenAI and Claude
  • Refactors JobRunner to fan out across all configured providers per keyword, with per-provider rate limiting and partial-failure tracking
  • Updates CLI, API, and web dashboard to expose and configure all three providers (surface parity)
  • Fixes two issues identified in code review:
    • Bug fix: allFailed run status was incorrectly based on providerErrors.size which accumulated entries from transient keyword failures even when the provider later succeeded on other keywords. Now correctly uses totalSnapshotsInserted === 0 as the guard.
    • Surface parity fix: PUT /settings/providers/:name API and ProviderConfigForm UI shipped without a corresponding CLI command. Adds canonry settings provider <name> --api-key <key> [--model <model>].

Test plan

  • pnpm run typecheck passes across all packages
  • pnpm run test passes
  • canonry init prompts for Gemini, OpenAI, and Anthropic API keys
  • canonry run <project> fans out to all configured providers
  • canonry settings shows all provider statuses
  • canonry settings provider gemini --api-key <key> updates a provider key
  • Run with all providers failing on one keyword but succeeding on others is marked partial, not failed
  • Settings page in web dashboard shows per-provider config forms

🤖 Generated with Claude Code

arberx and others added 8 commits March 9, 2026 23:44
… update

- Fix allFailed logic in job-runner: was based on providerErrors.size which
  accumulated entries from any keyword failure even when the provider later
  succeeded. Now uses totalSnapshotsInserted === 0 as the guard so a run is
  only marked failed when no data was written at all.

- Add canonry settings provider <name> --api-key <key> CLI command to satisfy
  CLAUDE.md surface parity: the PUT /settings/providers/:name API endpoint and
  its ProviderConfigForm UI counterpart shipped without a corresponding CLI
  command. Adds setProvider() in commands/settings.ts, updateProvider() on
  ApiClient, and the settings provider <name> subcommand in cli.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements full multi-provider visibility runs in parallel across Gemini,
OpenAI, and Claude/Anthropic. Each provider has its own adapter, rate
limiting, and per-minute quota window. Run status reflects partial
failures across providers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. --provider flag was a no-op: POST /runs now accepts a providers[]
   override that is threaded through onRunCreated -> executeRun ->
   registry.getForProject, so single-provider runs work correctly.

2. Daily quota over-counted by multiplying keywords * providers, then
   comparing against a single-provider limit. Changed to compare
   keywords-per-provider (projectKeywords.length) against the minimum
   per-provider daily quota.

3. Timeline produced fake emerging/lost transitions within one run when
   multiple providers emitted snapshots for the same keyword. Added a
   deduplication step (prefer 'cited', one entry per runId+keywordId)
   before computing transitions.

4. Updating a provider key without --model dropped the live model
   override until restart: server.ts re-registered the adapter with
   model: model || undefined instead of model: model || existing?.model.

5. apps/api never passed providerSummary to apiRoutes so
   GET /api/v1/settings always returned an empty providers list even
   when env-backed providers were configured.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Usage counter incremented by totalSnapshotsInserted (keywords x providers)
   but quota check used queriesPerProvider (keywords only), causing valid later
   runs to be blocked too early. Changed to increment by queriesPerProvider.

2. POST /runs accepted invalid provider names and silently queued a run that
   would fail inside the job runner. Added a 400 validation guard at the API
   boundary before the run is inserted.

3. buildEvidenceFromTimeline used the aggregated timeline transition for each
   per-provider evidence row, producing contradictory labels (e.g. 'emerging'
   for a provider that returned 'not-cited'). For multi-provider runs the
   label now derives from the provider's own citation state.

4. snapshots/diff built the per-run keyword maps with a plain Map constructor
   that let the last provider snapshot win, collapsing multi-provider results
   arbitrarily. Now uses prefer-cited deduplication matching the timeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Polish homepage layout: projects first, tighter typography

- Move project list to top of portfolio page as primary content
- Relocate "What changed" and "Activity" into a compact secondary
  grid below projects (only shows when there are attention items)
- Reduce heading sizes and font weights for a more refined feel
- Tighten padding, spacing, and font sizes across cards and rows
- Use negative letter-spacing on headings for premium typographic tone
- Soften borders and hover states for subtler interactivity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add scrollable containers for projects, attention, and activity lists

Cap project list at 32rem, attention and activity lists at 18rem.
Overflow scrolls with a bottom fade mask and thin scrollbar styling
so the page stays compact even with many items.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Ensure database directory exists before opening SQLite file

createClient now calls mkdirSync with recursive: true on the parent
directory so `canonry serve` works without a prior `canonry init`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Switch to keyword-level visibility scoring and add info tooltips

Visibility score now measures "% of keywords visible in at least one
AI engine" instead of raw snapshot counts. This makes the metric
resilient to partial runs and provider count changes — adding a
provider can only help your score, never hurt it.

- computeKeywordVisibility: groups snapshots by keyword, marks visible
  if any provider cites it
- Project page gauges and portfolio cards updated to new metric
- InfoTooltip component: CSS-only hover tooltip on gauge labels
- Tooltips added to all four gauges (visibility, readiness, pressure,
  run status) and the provider breakdown section header
- Provider breakdown still uses per-snapshot scoring as a diagnostic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1. Quota check used project-scoped query counts (aggregated across all
   providers) against per-provider limits. A provider that had never been
   used could be falsely blocked by another provider's accumulated usage.
   Now tracks and checks usage with scope "${projectId}:${providerName}"
   per provider, and increments the same scoped counter after each run.

2. getForProject iterated the provider list without deduplicating, so a
   project or run-override like ["gemini","gemini"] would dispatch the
   same provider twice per keyword. Added a seen-set guard to skip
   duplicate entries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 'Portfolio ranking' text was removed from the overview page; the
current subtitle is 'Visibility and execution state across all projects'.
Update the test to match what is actually rendered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@arberx arberx merged commit d7c96d6 into main Mar 10, 2026
1 check passed
@arberx arberx deleted the arberx/multi-provider-search branch March 10, 2026 04:17
arberx added a commit that referenced this pull request Mar 16, 2026
- Remove redundant if/else in llm.ts that set the same Authorization header
- Remove unused ToolDefinition interface (actual interface is AgentTool)

Addresses review comments #5 and #6 (Style)
arberx added a commit that referenced this pull request Mar 16, 2026
- Remove redundant if/else in llm.ts that set the same Authorization header
- Remove unused ToolDefinition interface (actual interface is AgentTool)

Addresses review comments #5 and #6 (Style)
arberx added a commit that referenced this pull request Mar 16, 2026
- Remove redundant if/else in llm.ts that set the same Authorization header
- Remove unused ToolDefinition interface (actual interface is AgentTool)

Addresses review comments #5 and #6 (Style)
arberx added a commit that referenced this pull request Mar 17, 2026
- Remove redundant if/else in llm.ts that set the same Authorization header
- Remove unused ToolDefinition interface (actual interface is AgentTool)

Addresses review comments #5 and #6 (Style)
arberx added a commit that referenced this pull request Mar 17, 2026
- Remove redundant if/else in llm.ts that set the same Authorization header
- Remove unused ToolDefinition interface (actual interface is AgentTool)

Addresses review comments #5 and #6 (Style)
arberx added a commit that referenced this pull request Mar 17, 2026
- Remove redundant if/else in llm.ts that set the same Authorization header
- Remove unused ToolDefinition interface (actual interface is AgentTool)

Addresses review comments #5 and #6 (Style)
arberx added a commit that referenced this pull request Mar 18, 2026
Issue #1 — trailing-slash mismatch on root route:
  Register both '/canonry/' and '/canonry' (bare) variants so
  either URL shape returns the SPA without a 404.

Issue #2 — SPA fallback over-broad scope:
  When basePath is set, only serve index.html for paths that
  start with basePath; return JSON 404 for everything else so
  co-hosted apps on the same origin are not hijacked.

Issue #3 — bare '/' base path causing duplicate-route error:
  Normalised base path equal to '/' is now treated as undefined
  (no base path), keeping the fastify-static prefix at '/' and
  avoiding a duplicate route registration error.

Issue #4 — routePrefix without leading slash silently mis-routes:
  Validate opts.routePrefix at startup and throw a descriptive
  error if it does not start with '/'.

Issue #5 — copy-pasted health handler body:
  Extract to a shared healthHandler const; both /health and
  ${basePath}health now reference the same function.
arberx added a commit that referenced this pull request Mar 18, 2026
* fix: base-path-aware API route prefix and SPA fallback

When `--base-path /canonry/` is set, three things were broken:

1. API routes were registered at /api/v1 regardless of base path,
   so requests to /canonry/api/v1/* had no matching route.
2. setNotFoundHandler checked request.url.startsWith('/api/') —
   correct without base path, but missed /canonry/api/v1/* URLs,
   causing the SPA catch-all to return 200 HTML for all API routes.
3. @fastify/static and root route handler were prefix-unaware,
   so static assets and the root index.html were not served under
   the configured base path.

Fix:
- Add `routePrefix` option to ApiRoutesOptions (named to avoid
  collision with Fastify's reserved `prefix` register option).
- Compute basePath once before apiRoutes registration and use it
  to set routePrefix = `${basePath}api/v1` or '/api/v1' default.
- Register @fastify/static and root handler at `basePath ?? '/'`.
- Update setNotFoundHandler guard to check both '/api/' and
  '${basePath}api/' so it works with and without --base-path.
- Register /health at both '/health' and '${basePath}health'.

Closes #113

* fix: address review feedback on base-path routing

Issue #1 — trailing-slash mismatch on root route:
  Register both '/canonry/' and '/canonry' (bare) variants so
  either URL shape returns the SPA without a 404.

Issue #2 — SPA fallback over-broad scope:
  When basePath is set, only serve index.html for paths that
  start with basePath; return JSON 404 for everything else so
  co-hosted apps on the same origin are not hijacked.

Issue #3 — bare '/' base path causing duplicate-route error:
  Normalised base path equal to '/' is now treated as undefined
  (no base path), keeping the fastify-static prefix at '/' and
  avoiding a duplicate route registration error.

Issue #4 — routePrefix without leading slash silently mis-routes:
  Validate opts.routePrefix at startup and throw a descriptive
  error if it does not start with '/'.

Issue #5 — copy-pasted health handler body:
  Extract to a shared healthHandler const; both /health and
  ${basePath}health now reference the same function.

---------

Co-authored-by: Claw (AINYC Agent) <agent@ainyc.ai>
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