Skip to content

feat: support transcription for openai-compatible providers - #995

Closed
Yiipu wants to merge 9 commits into
team-reflect:masterfrom
Yiipu:draft/openai-compatible-transcription
Closed

feat: support transcription for openai-compatible providers#995
Yiipu wants to merge 9 commits into
team-reflect:masterfrom
Yiipu:draft/openai-compatible-transcription

Conversation

@Yiipu

@Yiipu Yiipu commented Jul 30, 2026

Copy link
Copy Markdown

Problem

#996 intentionally scoped openai-compatible to chat, editor AI transforms, capture enrichment, asset descriptions, and audio memo title generation — "where the configured chat model can be used directly." Transcription was excluded because the /v1/audio/transcriptions endpoint is a different API surface from /v1/chat/completions, and the model types differ (speech-to-text vs chat).

However, for users with OpenAI-compatible transcription endpoints (local Whisper servers, Groq, or API proxies that expose /v1/audio/transcriptions), being able to configure transcription through the same openai-compatible provider entry is natural — the base URL is the same, only the model differs.

Changes

  1. Schema: openai-compatible entries gain an optional transcriptionModel field (empty = transcription not supported). A new defaultTranscriptionProviderId setting is introduced, independent of the chat default.

  2. Catalog: AiProviderInfo gains supportsTranscription — a compile-time constant for hosted providers (OpenAI/Google: true, Anthropic/OpenRouter: false), and false for openai-compatible (the user opts in per-entry via transcriptionModel). aiProviderSupportsTranscription() merges both sources.

  3. Provider-config: The hardcoded TRANSCRIPTION_PROVIDERS constant is replaced by transcriptionProviders() — a dynamic function that derives eligible providers from the configured state. resolveTranscriptionTarget and pickTranscriptionConfig now work with any provider that has transcription capability.

  4. Transcribe client: transcribeAudio accepts optional baseUrl and model params. When the provider is openai-compatible, the client posts to {baseUrl}/audio/transcriptions with the user-supplied model.

  5. Audio memo pipeline: memoNoteBody forwards baseUrl + transcriptionModel to the transcription client when the provider is openai-compatible.

Still deferred

  • Settings UI for the new transcriptionModel field and defaultTranscriptionProviderId

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for OpenAI-compatible AI providers with custom endpoints and models.
    • Added a separate default provider setting for audio transcription.
    • Improved provider selection for transcription, including automatic fallback options.
    • Added clearer handling for providers that require API keys and those that support transcription.
  • Bug Fixes

    • Improved transcription error handling and API key resolution.
    • Normalized and validated custom provider URLs for safer configuration.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d51ae27-b88a-4811-813e-b7186066e5cd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The PR adds OpenAI-compatible provider settings, separate transcription-provider defaults, capability-based transcription selection, configurable transcription endpoints and models, shared API-key resolution, and desktop wiring for provider state synchronization.

Changes

AI provider and transcription support

Layer / File(s) Summary
Provider contracts and core adapters
packages/core/src/settings/..., packages/core/src/ai/..., packages/core/src/exports/ai-actions.ts
Provider schemas, catalog capabilities, OpenAI-compatible model/transcription requests, API-key handling, validation tests, and public exports are updated.
Independent transcription provider routing
packages/core/src/ai/provider-config.ts, packages/core/src/actions/audio-memo.ts, apps/desktop/src/hooks/use-audio-memo-pipeline.ts, apps/desktop/src/providers/..., apps/desktop/src/lib/...
Provider state gains an independent transcription default; candidate ordering, key resolution, audio memo handling, and desktop pipeline wiring use it.
Desktop provider configuration UI
apps/desktop/src/hooks/use-ai-providers.ts, apps/desktop/src/providers/chat-provider.tsx
Provider creation and removal maintain transcription defaults, OpenAI-compatible settings are normalized, and chat uses the shared API-key resolver.

Permission schema formatting

Layer / File(s) Summary
Reformatted plugin permission schemas
plugins/tauri-plugin-keyboard/permissions/schemas/schema.json, plugins/tauri-plugin-recording/permissions/schemas/schema.json
JSON schema arrays, unions, enums, and file-ending formatting are expanded without changing constraints.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Settings
  participant ProviderConfig
  participant KeyResolver
  participant AudioMemo
  participant Transcription
  Settings->>ProviderConfig: provide provider list and transcription default
  ProviderConfig->>KeyResolver: resolve candidate API keys
  KeyResolver-->>ProviderConfig: return usable key
  ProviderConfig->>AudioMemo: resolve transcription target
  AudioMemo->>Transcription: submit audio with endpoint and model
  Transcription-->>AudioMemo: return transcript or error
Loading

Possibly related PRs

Suggested labels: codex

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding transcription support for openai-compatible providers.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@Yiipu
Yiipu force-pushed the draft/openai-compatible-transcription branch from 9c4953c to ae18310 Compare July 30, 2026 07:46
@Yiipu
Yiipu marked this pull request as ready for review July 30, 2026 07:46
Yiipu added 5 commits July 30, 2026 15:51
- Add optional transcriptionModel field to openai-compatible provider config
- Add defaultTranscriptionProviderId to settings schema (independent of chat default)
- Add supportsTranscription flag to AiProviderInfo catalog
- Add aiProviderSupportsTranscription() helper that merges catalog constants
  with per-entry user configuration for openai-compatible providers

Transcription eligibility is now derived: hosted providers are known at
compile time (OpenAI/Google: true, Anthropic/OpenRouter: false), while
openai-compatible entries become eligible when the user supplies a
non-empty transcriptionModel.
Replace the compile-time TRANSCRIPTION_PROVIDERS constant with a dynamic
transcriptionProviders() function that derives eligible providers from
the configured state. Changes:

- transcriptionProviders(): returns all providers where
  aiProviderSupportsTranscription() is true, sorted OpenAI > Google >
  openai-compatible, with the transcription default first within each group
- pickTranscriptionConfig(): returns the first candidate (no longer
  hardcoded to only OpenAI/Google)
- resolveTranscriptionTarget(): iterates candidates, skips keyless entries
- defaultTranscriptionProvider(): resolves the transcription default,
  falling back to the first transcription-capable entry
- AiProvidersState gains defaultTranscriptionProviderId
- withAiProviderAdded/Removed maintain the transcription default
  alongside the chat default

The TranscriptionProvider type is widened to include 'openai-compatible'
alongside 'openai' and 'google'.
Extend transcribeAudio and transcribeWithOpenAi to accept optional
baseUrl and model parameters. When provided (openai-compatible
providers), the client posts to {baseUrl}/audio/transcriptions instead
of the hardcoded api.openai.com endpoint, and uses the user-supplied
model instead of the default with fallback retry.

The provider routing is simplified: anything that isn't 'google' now
flows through transcribeWithOpenAi, since openai-compatible providers
use the same /v1/audio/transcriptions API shape.
…ipeline

memoNoteBody now accepts AiProviderConfig (instead of the removed
TranscriptionConfig) and forwards baseUrl + transcriptionModel to
transcribeAudio when the provider is openai-compatible.

The error message for missing transcription config is updated from
'No OpenAI or Gemini model is configured' to the generic
'No transcription provider is configured'.

Exports are updated to include the new public API surface:
aiProviderSupportsTranscription, defaultTranscriptionProvider,
resolveTranscriptionTarget, transcriptionProviders.
…scriptionProviderId

Every AiProvidersState construction site now includes the new
defaultTranscriptionProviderId field.  useAiProviders gains a
makeTranscriptionDefault callback that writes the transcription
default independently of the chat default.

New openai-compatible entries are initialized with an empty
transcriptionModel (transcription off by default).
@Yiipu
Yiipu force-pushed the draft/openai-compatible-transcription branch from ae18310 to aabace9 Compare July 30, 2026 07:52

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/desktop/src/components/settings/add-ai-provider-dialog.tsx (1)

126-135: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Stale validation errors can survive a provider switch.

These setValue calls reset model/baseUrl to valid defaults but don't clear or re-trigger validation, so a previously shown "Enter a model id."/"Enter an http(s) endpoint URL." error can linger on screen even though the field now holds a valid value (until the next submit attempt re-validates).

🐛 Suggested fix
                 const next = aiProvider(aiProviderIdSchema.parse(value))
                 setValue('provider', next.id)
-                setValue('model', next.models[0].id)
+                setValue('model', next.models[0].id, { shouldValidate: true })
                 setValue(
                   'baseUrl',
                   next.id === 'openai-compatible' ? DEFAULT_OPENAI_COMPATIBLE_BASE_URL : '',
+                  { shouldValidate: true },
                 )
🤖 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 `@apps/desktop/src/components/settings/add-ai-provider-dialog.tsx` around lines
126 - 135, Update the provider-switch handler in onValueChange to clear or
re-trigger validation for the model and baseUrl fields after setting their new
defaults, so stale errors are removed immediately while preserving validation
for future edits and submissions.
packages/core/src/actions/audio-memo.ts (1)

543-552: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Transcription key lookup bypasses aiApiKeyForConfig, breaking keyless openai-compatible transcription.

getKey here resolves keys via raw getSecret(aiKeySecretName(id)), unlike the enrichment-key path a few lines below (line 580) which correctly uses aiApiKeyForConfig(enrichmentConfig). For an openai-compatible provider configured without an API key (keyHint === '', a valid "no-key compatible endpoint" per audio-memo-title.ts's doc comment), no secret was ever stored, so getSecret returns null here — resolveTranscriptionTarget then treats this provider as keyless and skips it, ultimately reporting 'no-key' even though the provider is fully usable. The downstream message ("The API key for the configured openai-compatible model is missing from the keychain") is also misleading for this case.

This file already imports aiApiKeyForConfig and applies it correctly elsewhere (line 580) — getKey for the transcription target just needs the same treatment.

🐛 Proposed fix
   const keys = new Map<string, Promise<string | null>>()
+  const providersById = new Map(input.providers.providers.map((provider) => [provider.id, provider]))
   const getKey = (id: string): Promise<string | null> => {
     let key = keys.get(id)
     if (key === undefined) {
-      key = getSecret(aiKeySecretName(id)).catch(() => null)
+      const providerConfig = providersById.get(id)
+      key = (
+        providerConfig === undefined
+          ? getSecret(aiKeySecretName(id))
+          : aiApiKeyForConfig(providerConfig)
+      ).catch(() => null)
       keys.set(id, key)
     }
     return key
   }
🤖 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 `@packages/core/src/actions/audio-memo.ts` around lines 543 - 552, Update the
transcription target key resolver getKey to obtain each provider key through the
existing aiApiKeyForConfig flow, matching the enrichment-key lookup below, while
preserving the existing per-ID promise caching and null-on-error behavior. Do
not use raw getSecret(aiKeySecretName(id)) for this lookup so keyless
openai-compatible providers remain eligible.
🧹 Nitpick comments (5)
packages/core/src/ai/language-model.ts (1)

41-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the expanded public API contract.

languageModel is exported and now exposes OpenAI-compatible endpoint, model, and empty-key behavior without API documentation. Add a concise JSDoc contract above the function.

As per coding guidelines, “Always document public APIs.”

🤖 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 `@packages/core/src/ai/language-model.ts` around lines 41 - 48, Document the
exported languageModel function with concise JSDoc describing its
OpenAI-compatible endpoint support, configured model selection, and behavior
when the API key is empty. Place the documentation directly above the function
declaration and preserve the existing implementation.

Source: Coding guidelines

packages/core/src/ai/validate-key.ts (1)

28-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing doc on new public interface.

ApiKeyValidationInput is a new exported type with no doc comment describing its fields (particularly the optional baseUrl, which is only meaningful for openai-compatible).

As per coding guidelines, **/*.{ts,tsx}: "Always document public APIs."

🤖 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 `@packages/core/src/ai/validate-key.ts` around lines 28 - 32, Document the
exported ApiKeyValidationInput interface and its fields, including that baseUrl
is optional and applies only to the openai-compatible provider. Follow the
repository’s existing public API documentation style.

Source: Coding guidelines

packages/core/src/ai/openai-compatible.ts (1)

7-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing docs on exported helpers.

normalizeOpenAICompatibleBaseUrl and isHttpBaseUrl are exported without doc comments (unlike isPlainHttpRemoteBaseUrl below, which explains its rationale well).

As per coding guidelines, **/*.{ts,tsx}: "Always document public APIs."

📝 Suggested docs
+/** Strips whitespace and a trailing slash so base URLs compare/concatenate cleanly. */
 export function normalizeOpenAICompatibleBaseUrl(value: string): string {
   return value.trim().replace(/\/+$/u, '')
 }

+/** Whether `value` is a well-formed http(s) URL with no query string or fragment. */
 export function isHttpBaseUrl(value: string): boolean {
🤖 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 `@packages/core/src/ai/openai-compatible.ts` around lines 7 - 22, Add doc
comments to the exported helpers normalizeOpenAICompatibleBaseUrl and
isHttpBaseUrl, documenting their purpose and behavior consistently with the
existing isPlainHttpRemoteBaseUrl documentation. Keep the implementations
unchanged.

Source: Coding guidelines

packages/core/src/settings/schema.test.ts (1)

294-344: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a dedicated defaultTranscriptionProviderId describe block.

New aiProviders tests cover the schema's baseUrl validation well, but there's no test mirroring the existing describe('defaultAiProviderId', ...) block (string passthrough + invalid-value degrade-to-null) for the newly added defaultTranscriptionProviderId field — only its empty-document default is checked.

✅ Suggested addition
+  describe('defaultTranscriptionProviderId', () => {
+    it('passes a string id through and defaults invalid values to null', () => {
+      expect(
+        settingsSchema.parse({ defaultTranscriptionProviderId: 'abc' })
+          .defaultTranscriptionProviderId,
+      ).toBe('abc')
+      expect(
+        settingsSchema.parse({ defaultTranscriptionProviderId: null })
+          .defaultTranscriptionProviderId,
+      ).toBeNull()
+      expect(
+        settingsSchema.parse({ defaultTranscriptionProviderId: 42 })
+          .defaultTranscriptionProviderId,
+      ).toBeNull()
+    })
+  })
🤖 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 `@packages/core/src/settings/schema.test.ts` around lines 294 - 344, Add a
dedicated describe block for defaultTranscriptionProviderId, mirroring the
existing defaultAiProviderId tests: verify valid string values are preserved and
invalid values degrade to null, while retaining the existing empty-document
default coverage.
packages/core/src/actions/audio-memo.ts (1)

388-400: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a type-guard predicate instead of as TranscriptionProvider.

input.config.provider as TranscriptionProvider casts past the type system rather than narrowing it; the safety here relies on callers only ever passing transcription-capable configs (enforced by aiProviderSupportsTranscription elsewhere), which the type system can't see. A small exported predicate (e.g. isTranscriptionProvider(provider): provider is TranscriptionProvider in provider-config.ts) would make this a real narrowing check instead of an assertion.

As per coding guidelines, "Avoid unnecessary type assertions" and "Use discriminated unions and type guards for variant data; export helper predicates when they clarify a public contract."

🤖 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 `@packages/core/src/actions/audio-memo.ts` around lines 388 - 400, Replace the
`as TranscriptionProvider` assertion in the `transcribeAudio` call with a
type-guard-based narrowing check. Add and export an `isTranscriptionProvider`
predicate in `provider-config.ts`, use it to validate or narrow
`input.config.provider` before constructing the transcription request, and
preserve the existing provider-specific configuration behavior.

Source: Coding guidelines

🤖 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 `@apps/desktop/src-tauri/capabilities/default.json`:
- Line 21: Document the renderer network trust boundary for the http:default
capability near its broad http and https URL allowlist, explicitly noting that
it applies to main and note windows and may expose custom AI endpoints to
webview-rendered note content. Preserve the intended localhost/LAN/clear-text
support, and identify either validated Tauri-command routing or a tighter
host/port scope as the required mitigation for untrusted renderer content.

In `@apps/desktop/src/capability-http-scope.test.ts`:
- Around line 24-26: Replace the type assertion applied to the JSON loaded in
the capability test with Zod runtime validation. Define or reuse a schema for
the capability object whose permissions accept both string identifiers and
objects containing identifier and allow fields, then parse the result before
use; do not restrict validation to a url field.

In `@packages/core/src/ai/provider-config.ts`:
- Around line 37-51: Update withAiProviderAdded so
defaultTranscriptionProviderId is set to entry.id only when makeDefault or
isFirst is true and aiProviderSupportsTranscription(entry) is true; otherwise
preserve state.defaultTranscriptionProviderId. Keep the defaultProviderId logic
unchanged.

In `@packages/core/src/ai/transcribe.test.ts`:
- Around line 301-321: Update transcribeWithOpenAi and the openai-compatible
transcribeAudio flow so the Authorization header is omitted when request.apiKey
is empty, while preserving the Bearer header for non-empty keys. Extend the
existing transcribeAudio (openai-compatible) tests with an apiKey: '' case and
assert Authorization is absent.

---

Outside diff comments:
In `@apps/desktop/src/components/settings/add-ai-provider-dialog.tsx`:
- Around line 126-135: Update the provider-switch handler in onValueChange to
clear or re-trigger validation for the model and baseUrl fields after setting
their new defaults, so stale errors are removed immediately while preserving
validation for future edits and submissions.

In `@packages/core/src/actions/audio-memo.ts`:
- Around line 543-552: Update the transcription target key resolver getKey to
obtain each provider key through the existing aiApiKeyForConfig flow, matching
the enrichment-key lookup below, while preserving the existing per-ID promise
caching and null-on-error behavior. Do not use raw
getSecret(aiKeySecretName(id)) for this lookup so keyless openai-compatible
providers remain eligible.

---

Nitpick comments:
In `@packages/core/src/actions/audio-memo.ts`:
- Around line 388-400: Replace the `as TranscriptionProvider` assertion in the
`transcribeAudio` call with a type-guard-based narrowing check. Add and export
an `isTranscriptionProvider` predicate in `provider-config.ts`, use it to
validate or narrow `input.config.provider` before constructing the transcription
request, and preserve the existing provider-specific configuration behavior.

In `@packages/core/src/ai/language-model.ts`:
- Around line 41-48: Document the exported languageModel function with concise
JSDoc describing its OpenAI-compatible endpoint support, configured model
selection, and behavior when the API key is empty. Place the documentation
directly above the function declaration and preserve the existing
implementation.

In `@packages/core/src/ai/openai-compatible.ts`:
- Around line 7-22: Add doc comments to the exported helpers
normalizeOpenAICompatibleBaseUrl and isHttpBaseUrl, documenting their purpose
and behavior consistently with the existing isPlainHttpRemoteBaseUrl
documentation. Keep the implementations unchanged.

In `@packages/core/src/ai/validate-key.ts`:
- Around line 28-32: Document the exported ApiKeyValidationInput interface and
its fields, including that baseUrl is optional and applies only to the
openai-compatible provider. Follow the repository’s existing public API
documentation style.

In `@packages/core/src/settings/schema.test.ts`:
- Around line 294-344: Add a dedicated describe block for
defaultTranscriptionProviderId, mirroring the existing defaultAiProviderId
tests: verify valid string values are preserved and invalid values degrade to
null, while retaining the existing empty-document default coverage.
🪄 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: 2b27082c-ac35-4b68-8327-b45b44c5f255

📥 Commits

Reviewing files that changed from the base of the PR and between 2ff6ce3 and ae18310.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (58)
  • apps/desktop/package.json
  • apps/desktop/src-tauri/capabilities/default.json
  • apps/desktop/src/capability-http-scope.test.ts
  • apps/desktop/src/components/chat/chat-screen.test.tsx
  • apps/desktop/src/components/settings/add-ai-provider-dialog.tsx
  • apps/desktop/src/components/settings/ai-provider-row.tsx
  • apps/desktop/src/components/settings/ai-providers-section.test.tsx
  • apps/desktop/src/components/settings/describe-assets-field.tsx
  • apps/desktop/src/editor/ai-menu/use-editor-ai-menu.tsx
  • apps/desktop/src/hooks/use-add-ai-provider-submit.ts
  • apps/desktop/src/hooks/use-ai-providers.ts
  • apps/desktop/src/hooks/use-audio-memo-pipeline.ts
  • apps/desktop/src/lib/asset-describe-controller.test.tsx
  • apps/desktop/src/lib/capture-controller.test.tsx
  • apps/desktop/src/lib/chat-model-groups.ts
  • apps/desktop/src/lib/transcription-reconciler.test.tsx
  • apps/desktop/src/mobile/add-ai-provider-drawer.test.tsx
  • apps/desktop/src/mobile/add-ai-provider-drawer.tsx
  • apps/desktop/src/mobile/ai-provider-actions-drawer.tsx
  • apps/desktop/src/mobile/screens/chat.test.tsx
  • apps/desktop/src/mobile/screens/settings.tsx
  • apps/desktop/src/providers/asset-describe-provider.tsx
  • apps/desktop/src/providers/capture-provider.tsx
  • apps/desktop/src/providers/chat-provider.test.tsx
  • apps/desktop/src/providers/chat-provider.tsx
  • packages/core/package.json
  • packages/core/src/actions/asset-description.test.ts
  • packages/core/src/actions/asset-description.ts
  • packages/core/src/actions/audio-memo.test.ts
  • packages/core/src/actions/audio-memo.ts
  • packages/core/src/actions/capture-enrichment.ts
  • packages/core/src/actions/capture-harness.ts
  • packages/core/src/ai/audio-memo-title.test.ts
  • packages/core/src/ai/audio-memo-title.ts
  • packages/core/src/ai/chat/model-options.test.ts
  • packages/core/src/ai/chat/stream-chat.ts
  • packages/core/src/ai/describe-asset.ts
  • packages/core/src/ai/describe-page.ts
  • packages/core/src/ai/language-model.test.ts
  • packages/core/src/ai/language-model.ts
  • packages/core/src/ai/openai-compatible.ts
  • packages/core/src/ai/provider-catalog.test.ts
  • packages/core/src/ai/provider-catalog.ts
  • packages/core/src/ai/provider-config.test.ts
  • packages/core/src/ai/provider-config.ts
  • packages/core/src/ai/secrets.test.ts
  • packages/core/src/ai/secrets.ts
  • packages/core/src/ai/transcribe.test.ts
  • packages/core/src/ai/transcribe.ts
  • packages/core/src/ai/transform-selection.ts
  • packages/core/src/ai/validate-key.test.ts
  • packages/core/src/ai/validate-key.ts
  • packages/core/src/exports/ai-actions.ts
  • packages/core/src/exports/platform.ts
  • packages/core/src/settings/schema.test.ts
  • packages/core/src/settings/schema.ts
  • plugins/tauri-plugin-keyboard/permissions/schemas/schema.json
  • plugins/tauri-plugin-recording/permissions/schemas/schema.json

Comment on lines 37 to 51
export function withAiProviderAdded(
state: AiProvidersState,
entry: AiProviderConfig,
makeDefault: boolean,
): AiProvidersState {
const isFirst = state.providers.length === 0
return {
providers: [...state.providers, entry],
defaultProviderId:
makeDefault || state.providers.length === 0 ? entry.id : state.defaultProviderId,
defaultProviderId: makeDefault || isFirst ? entry.id : state.defaultProviderId,
defaultTranscriptionProviderId:
makeDefault || (isFirst && aiProviderSupportsTranscription(entry))
? entry.id
: state.defaultTranscriptionProviderId,
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

makeDefault branch doesn't gate on transcription capability, contradicting its own doc comment.

The doc above this function says the transcription default "follows the same rule for transcription-capable entries," but the makeDefault branch sets defaultTranscriptionProviderId = entry.id unconditionally — unlike the isFirst branch, which correctly checks aiProviderSupportsTranscription(entry). Adding a non-transcription-capable provider (e.g. Anthropic) as the default provider would also incorrectly mark it as the default transcription provider.

Every current reader (defaultTranscriptionProvider, transcriptionProviders) re-validates capability before trusting this id, so there's no visible symptom today, but the stored state is inconsistent with its own contract.

🐛 Proposed fix
     defaultTranscriptionProviderId:
-      makeDefault || (isFirst && aiProviderSupportsTranscription(entry))
+      (makeDefault || isFirst) && aiProviderSupportsTranscription(entry)
         ? entry.id
         : state.defaultTranscriptionProviderId,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function withAiProviderAdded(
state: AiProvidersState,
entry: AiProviderConfig,
makeDefault: boolean,
): AiProvidersState {
const isFirst = state.providers.length === 0
return {
providers: [...state.providers, entry],
defaultProviderId:
makeDefault || state.providers.length === 0 ? entry.id : state.defaultProviderId,
defaultProviderId: makeDefault || isFirst ? entry.id : state.defaultProviderId,
defaultTranscriptionProviderId:
makeDefault || (isFirst && aiProviderSupportsTranscription(entry))
? entry.id
: state.defaultTranscriptionProviderId,
}
}
export function withAiProviderAdded(
state: AiProvidersState,
entry: AiProviderConfig,
makeDefault: boolean,
): AiProvidersState {
const isFirst = state.providers.length === 0
return {
providers: [...state.providers, entry],
defaultProviderId: makeDefault || isFirst ? entry.id : state.defaultProviderId,
defaultTranscriptionProviderId:
(makeDefault || isFirst) && aiProviderSupportsTranscription(entry)
? entry.id
: state.defaultTranscriptionProviderId,
}
}
🤖 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 `@packages/core/src/ai/provider-config.ts` around lines 37 - 51, Update
withAiProviderAdded so defaultTranscriptionProviderId is set to entry.id only
when makeDefault or isFirst is true and aiProviderSupportsTranscription(entry)
is true; otherwise preserve state.defaultTranscriptionProviderId. Keep the
defaultProviderId logic unchanged.

Comment on lines +301 to +321
describe('transcribeAudio (openai-compatible)', () => {
it('posts multipart with a custom model to the configured base URL', async () => {
const calls: RecordedCall[] = []
const fetchFn = recordingFetch(calls, () => jsonResponse(200, { text: ' hello ' }))

const text = await transcribeAudio(
request({
provider: 'openai-compatible',
baseUrl: 'https://transcribe.example.com/v1',
model: 'whisper-large-v3',
fetchFn,
}),
)

expect(text).toBe('hello')
expect(calls).toHaveLength(1)
expect(calls[0]!.url).toBe('https://transcribe.example.com/v1/audio/transcriptions')
expect(calls[0]!.headers['Authorization']).toBe('Bearer sk-test')
const form = calls[0]!.body as FormData
expect(form.get('model')).toBe('whisper-large-v3')
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Omit authorization for no-key transcription endpoints.

This new flow only tests sk-test, but transcribeWithOpenAi always sends Authorization: Bearer ${request.apiKey}. An empty optional key therefore produces Bearer instead of no header, unlike languageModel; no-auth compatible transcription servers can reject that request.

Proposed fix
-      headers: { Authorization: `Bearer ${request.apiKey}` },
+      headers:
+        request.apiKey.trim() === ''
+          ? undefined
+          : { Authorization: `Bearer ${request.apiKey}` },

Add a compatible-provider test with apiKey: '' asserting Authorization is absent.

🤖 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 `@packages/core/src/ai/transcribe.test.ts` around lines 301 - 321, Update
transcribeWithOpenAi and the openai-compatible transcribeAudio flow so the
Authorization header is omitted when request.apiKey is empty, while preserving
the Bearer header for non-empty keys. Extend the existing transcribeAudio
(openai-compatible) tests with an apiKey: '' case and assert Authorization is
absent.

@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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/desktop/src/components/settings/add-ai-provider-dialog.tsx (1)

126-135: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Stale validation errors can survive a provider switch.

These setValue calls reset model/baseUrl to valid defaults but don't clear or re-trigger validation, so a previously shown "Enter a model id."/"Enter an http(s) endpoint URL." error can linger on screen even though the field now holds a valid value (until the next submit attempt re-validates).

🐛 Suggested fix
                 const next = aiProvider(aiProviderIdSchema.parse(value))
                 setValue('provider', next.id)
-                setValue('model', next.models[0].id)
+                setValue('model', next.models[0].id, { shouldValidate: true })
                 setValue(
                   'baseUrl',
                   next.id === 'openai-compatible' ? DEFAULT_OPENAI_COMPATIBLE_BASE_URL : '',
+                  { shouldValidate: true },
                 )
🤖 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 `@apps/desktop/src/components/settings/add-ai-provider-dialog.tsx` around lines
126 - 135, Update the provider-switch handler in onValueChange to clear or
re-trigger validation for the model and baseUrl fields after setting their new
defaults, so stale errors are removed immediately while preserving validation
for future edits and submissions.
packages/core/src/actions/audio-memo.ts (1)

543-552: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Transcription key lookup bypasses aiApiKeyForConfig, breaking keyless openai-compatible transcription.

getKey here resolves keys via raw getSecret(aiKeySecretName(id)), unlike the enrichment-key path a few lines below (line 580) which correctly uses aiApiKeyForConfig(enrichmentConfig). For an openai-compatible provider configured without an API key (keyHint === '', a valid "no-key compatible endpoint" per audio-memo-title.ts's doc comment), no secret was ever stored, so getSecret returns null here — resolveTranscriptionTarget then treats this provider as keyless and skips it, ultimately reporting 'no-key' even though the provider is fully usable. The downstream message ("The API key for the configured openai-compatible model is missing from the keychain") is also misleading for this case.

This file already imports aiApiKeyForConfig and applies it correctly elsewhere (line 580) — getKey for the transcription target just needs the same treatment.

🐛 Proposed fix
   const keys = new Map<string, Promise<string | null>>()
+  const providersById = new Map(input.providers.providers.map((provider) => [provider.id, provider]))
   const getKey = (id: string): Promise<string | null> => {
     let key = keys.get(id)
     if (key === undefined) {
-      key = getSecret(aiKeySecretName(id)).catch(() => null)
+      const providerConfig = providersById.get(id)
+      key = (
+        providerConfig === undefined
+          ? getSecret(aiKeySecretName(id))
+          : aiApiKeyForConfig(providerConfig)
+      ).catch(() => null)
       keys.set(id, key)
     }
     return key
   }
🤖 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 `@packages/core/src/actions/audio-memo.ts` around lines 543 - 552, Update the
transcription target key resolver getKey to obtain each provider key through the
existing aiApiKeyForConfig flow, matching the enrichment-key lookup below, while
preserving the existing per-ID promise caching and null-on-error behavior. Do
not use raw getSecret(aiKeySecretName(id)) for this lookup so keyless
openai-compatible providers remain eligible.
🧹 Nitpick comments (5)
packages/core/src/ai/language-model.ts (1)

41-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the expanded public API contract.

languageModel is exported and now exposes OpenAI-compatible endpoint, model, and empty-key behavior without API documentation. Add a concise JSDoc contract above the function.

As per coding guidelines, “Always document public APIs.”

🤖 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 `@packages/core/src/ai/language-model.ts` around lines 41 - 48, Document the
exported languageModel function with concise JSDoc describing its
OpenAI-compatible endpoint support, configured model selection, and behavior
when the API key is empty. Place the documentation directly above the function
declaration and preserve the existing implementation.

Source: Coding guidelines

packages/core/src/ai/validate-key.ts (1)

28-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing doc on new public interface.

ApiKeyValidationInput is a new exported type with no doc comment describing its fields (particularly the optional baseUrl, which is only meaningful for openai-compatible).

As per coding guidelines, **/*.{ts,tsx}: "Always document public APIs."

🤖 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 `@packages/core/src/ai/validate-key.ts` around lines 28 - 32, Document the
exported ApiKeyValidationInput interface and its fields, including that baseUrl
is optional and applies only to the openai-compatible provider. Follow the
repository’s existing public API documentation style.

Source: Coding guidelines

packages/core/src/ai/openai-compatible.ts (1)

7-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing docs on exported helpers.

normalizeOpenAICompatibleBaseUrl and isHttpBaseUrl are exported without doc comments (unlike isPlainHttpRemoteBaseUrl below, which explains its rationale well).

As per coding guidelines, **/*.{ts,tsx}: "Always document public APIs."

📝 Suggested docs
+/** Strips whitespace and a trailing slash so base URLs compare/concatenate cleanly. */
 export function normalizeOpenAICompatibleBaseUrl(value: string): string {
   return value.trim().replace(/\/+$/u, '')
 }

+/** Whether `value` is a well-formed http(s) URL with no query string or fragment. */
 export function isHttpBaseUrl(value: string): boolean {
🤖 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 `@packages/core/src/ai/openai-compatible.ts` around lines 7 - 22, Add doc
comments to the exported helpers normalizeOpenAICompatibleBaseUrl and
isHttpBaseUrl, documenting their purpose and behavior consistently with the
existing isPlainHttpRemoteBaseUrl documentation. Keep the implementations
unchanged.

Source: Coding guidelines

packages/core/src/settings/schema.test.ts (1)

294-344: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a dedicated defaultTranscriptionProviderId describe block.

New aiProviders tests cover the schema's baseUrl validation well, but there's no test mirroring the existing describe('defaultAiProviderId', ...) block (string passthrough + invalid-value degrade-to-null) for the newly added defaultTranscriptionProviderId field — only its empty-document default is checked.

✅ Suggested addition
+  describe('defaultTranscriptionProviderId', () => {
+    it('passes a string id through and defaults invalid values to null', () => {
+      expect(
+        settingsSchema.parse({ defaultTranscriptionProviderId: 'abc' })
+          .defaultTranscriptionProviderId,
+      ).toBe('abc')
+      expect(
+        settingsSchema.parse({ defaultTranscriptionProviderId: null })
+          .defaultTranscriptionProviderId,
+      ).toBeNull()
+      expect(
+        settingsSchema.parse({ defaultTranscriptionProviderId: 42 })
+          .defaultTranscriptionProviderId,
+      ).toBeNull()
+    })
+  })
🤖 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 `@packages/core/src/settings/schema.test.ts` around lines 294 - 344, Add a
dedicated describe block for defaultTranscriptionProviderId, mirroring the
existing defaultAiProviderId tests: verify valid string values are preserved and
invalid values degrade to null, while retaining the existing empty-document
default coverage.
packages/core/src/actions/audio-memo.ts (1)

388-400: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a type-guard predicate instead of as TranscriptionProvider.

input.config.provider as TranscriptionProvider casts past the type system rather than narrowing it; the safety here relies on callers only ever passing transcription-capable configs (enforced by aiProviderSupportsTranscription elsewhere), which the type system can't see. A small exported predicate (e.g. isTranscriptionProvider(provider): provider is TranscriptionProvider in provider-config.ts) would make this a real narrowing check instead of an assertion.

As per coding guidelines, "Avoid unnecessary type assertions" and "Use discriminated unions and type guards for variant data; export helper predicates when they clarify a public contract."

🤖 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 `@packages/core/src/actions/audio-memo.ts` around lines 388 - 400, Replace the
`as TranscriptionProvider` assertion in the `transcribeAudio` call with a
type-guard-based narrowing check. Add and export an `isTranscriptionProvider`
predicate in `provider-config.ts`, use it to validate or narrow
`input.config.provider` before constructing the transcription request, and
preserve the existing provider-specific configuration behavior.

Source: Coding guidelines

🤖 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 `@apps/desktop/src-tauri/capabilities/default.json`:
- Line 21: Document the renderer network trust boundary for the http:default
capability near its broad http and https URL allowlist, explicitly noting that
it applies to main and note windows and may expose custom AI endpoints to
webview-rendered note content. Preserve the intended localhost/LAN/clear-text
support, and identify either validated Tauri-command routing or a tighter
host/port scope as the required mitigation for untrusted renderer content.

In `@apps/desktop/src/capability-http-scope.test.ts`:
- Around line 24-26: Replace the type assertion applied to the JSON loaded in
the capability test with Zod runtime validation. Define or reuse a schema for
the capability object whose permissions accept both string identifiers and
objects containing identifier and allow fields, then parse the result before
use; do not restrict validation to a url field.

In `@packages/core/src/ai/provider-config.ts`:
- Around line 37-51: Update withAiProviderAdded so
defaultTranscriptionProviderId is set to entry.id only when makeDefault or
isFirst is true and aiProviderSupportsTranscription(entry) is true; otherwise
preserve state.defaultTranscriptionProviderId. Keep the defaultProviderId logic
unchanged.

In `@packages/core/src/ai/transcribe.test.ts`:
- Around line 301-321: Update transcribeWithOpenAi and the openai-compatible
transcribeAudio flow so the Authorization header is omitted when request.apiKey
is empty, while preserving the Bearer header for non-empty keys. Extend the
existing transcribeAudio (openai-compatible) tests with an apiKey: '' case and
assert Authorization is absent.

---

Outside diff comments:
In `@apps/desktop/src/components/settings/add-ai-provider-dialog.tsx`:
- Around line 126-135: Update the provider-switch handler in onValueChange to
clear or re-trigger validation for the model and baseUrl fields after setting
their new defaults, so stale errors are removed immediately while preserving
validation for future edits and submissions.

In `@packages/core/src/actions/audio-memo.ts`:
- Around line 543-552: Update the transcription target key resolver getKey to
obtain each provider key through the existing aiApiKeyForConfig flow, matching
the enrichment-key lookup below, while preserving the existing per-ID promise
caching and null-on-error behavior. Do not use raw
getSecret(aiKeySecretName(id)) for this lookup so keyless openai-compatible
providers remain eligible.

---

Nitpick comments:
In `@packages/core/src/actions/audio-memo.ts`:
- Around line 388-400: Replace the `as TranscriptionProvider` assertion in the
`transcribeAudio` call with a type-guard-based narrowing check. Add and export
an `isTranscriptionProvider` predicate in `provider-config.ts`, use it to
validate or narrow `input.config.provider` before constructing the transcription
request, and preserve the existing provider-specific configuration behavior.

In `@packages/core/src/ai/language-model.ts`:
- Around line 41-48: Document the exported languageModel function with concise
JSDoc describing its OpenAI-compatible endpoint support, configured model
selection, and behavior when the API key is empty. Place the documentation
directly above the function declaration and preserve the existing
implementation.

In `@packages/core/src/ai/openai-compatible.ts`:
- Around line 7-22: Add doc comments to the exported helpers
normalizeOpenAICompatibleBaseUrl and isHttpBaseUrl, documenting their purpose
and behavior consistently with the existing isPlainHttpRemoteBaseUrl
documentation. Keep the implementations unchanged.

In `@packages/core/src/ai/validate-key.ts`:
- Around line 28-32: Document the exported ApiKeyValidationInput interface and
its fields, including that baseUrl is optional and applies only to the
openai-compatible provider. Follow the repository’s existing public API
documentation style.

In `@packages/core/src/settings/schema.test.ts`:
- Around line 294-344: Add a dedicated describe block for
defaultTranscriptionProviderId, mirroring the existing defaultAiProviderId
tests: verify valid string values are preserved and invalid values degrade to
null, while retaining the existing empty-document default coverage.
🪄 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: 2b27082c-ac35-4b68-8327-b45b44c5f255

📥 Commits

Reviewing files that changed from the base of the PR and between 2ff6ce3 and ae18310.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (58)
  • apps/desktop/package.json
  • apps/desktop/src-tauri/capabilities/default.json
  • apps/desktop/src/capability-http-scope.test.ts
  • apps/desktop/src/components/chat/chat-screen.test.tsx
  • apps/desktop/src/components/settings/add-ai-provider-dialog.tsx
  • apps/desktop/src/components/settings/ai-provider-row.tsx
  • apps/desktop/src/components/settings/ai-providers-section.test.tsx
  • apps/desktop/src/components/settings/describe-assets-field.tsx
  • apps/desktop/src/editor/ai-menu/use-editor-ai-menu.tsx
  • apps/desktop/src/hooks/use-add-ai-provider-submit.ts
  • apps/desktop/src/hooks/use-ai-providers.ts
  • apps/desktop/src/hooks/use-audio-memo-pipeline.ts
  • apps/desktop/src/lib/asset-describe-controller.test.tsx
  • apps/desktop/src/lib/capture-controller.test.tsx
  • apps/desktop/src/lib/chat-model-groups.ts
  • apps/desktop/src/lib/transcription-reconciler.test.tsx
  • apps/desktop/src/mobile/add-ai-provider-drawer.test.tsx
  • apps/desktop/src/mobile/add-ai-provider-drawer.tsx
  • apps/desktop/src/mobile/ai-provider-actions-drawer.tsx
  • apps/desktop/src/mobile/screens/chat.test.tsx
  • apps/desktop/src/mobile/screens/settings.tsx
  • apps/desktop/src/providers/asset-describe-provider.tsx
  • apps/desktop/src/providers/capture-provider.tsx
  • apps/desktop/src/providers/chat-provider.test.tsx
  • apps/desktop/src/providers/chat-provider.tsx
  • packages/core/package.json
  • packages/core/src/actions/asset-description.test.ts
  • packages/core/src/actions/asset-description.ts
  • packages/core/src/actions/audio-memo.test.ts
  • packages/core/src/actions/audio-memo.ts
  • packages/core/src/actions/capture-enrichment.ts
  • packages/core/src/actions/capture-harness.ts
  • packages/core/src/ai/audio-memo-title.test.ts
  • packages/core/src/ai/audio-memo-title.ts
  • packages/core/src/ai/chat/model-options.test.ts
  • packages/core/src/ai/chat/stream-chat.ts
  • packages/core/src/ai/describe-asset.ts
  • packages/core/src/ai/describe-page.ts
  • packages/core/src/ai/language-model.test.ts
  • packages/core/src/ai/language-model.ts
  • packages/core/src/ai/openai-compatible.ts
  • packages/core/src/ai/provider-catalog.test.ts
  • packages/core/src/ai/provider-catalog.ts
  • packages/core/src/ai/provider-config.test.ts
  • packages/core/src/ai/provider-config.ts
  • packages/core/src/ai/secrets.test.ts
  • packages/core/src/ai/secrets.ts
  • packages/core/src/ai/transcribe.test.ts
  • packages/core/src/ai/transcribe.ts
  • packages/core/src/ai/transform-selection.ts
  • packages/core/src/ai/validate-key.test.ts
  • packages/core/src/ai/validate-key.ts
  • packages/core/src/exports/ai-actions.ts
  • packages/core/src/exports/platform.ts
  • packages/core/src/settings/schema.test.ts
  • packages/core/src/settings/schema.ts
  • plugins/tauri-plugin-keyboard/permissions/schemas/schema.json
  • plugins/tauri-plugin-recording/permissions/schemas/schema.json
🛑 Comments failed to post (2)
apps/desktop/src-tauri/capabilities/default.json (1)

21-21: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repository files matching default.json =="
git ls-files | rg 'apps/desktop/src-tauri/capabilities/.*\.json$|apps/desktop/src-tauri/src/.*\.rs$|capabilities|permissions'

echo
echo "== default.json =="
cat -n apps/desktop/src-tauri/capabilities/default.json

echo
echo "== Tauri capability/network permission references =="
rg -n '"http":|"http:default"|default://localhost|http://localhost|https://localhost|https://.*\*|http://.*\*|permissions|allow\[' apps/desktop/src-tauri apps/desktop -g '!node_modules' || true

echo
echo "== command definitions invocations =="
fd -e rs . apps/desktop/src-tauri/src -x sh -c 'echo "--- $1"; ast-grep outline "$1" || true' sh {}

Repository: team-reflect/reflect-open

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== capability-http-scope.test.ts =="
cat -n apps/desktop/src/capability-http-scope.test.ts

echo
echo "== AI provider related files =="
git ls-files apps/desktop/src | rg -i 'provider|ai|openai|anthropic|settings' || true

echo
echo "== http:default / custom provider endpoint references =="
rg -n 'http:default|baseUrl|base.*url|baseUrl:|openai|anthropic|custom provider|Custom|provider.*endpoint|endpoint.*provider|http\.fetch|fetch\(' apps/desktop/src apps/desktop/src-tauri/src -g '!node_modules' || true

echo
echo "== tauri plugin-http permission schema/deny/allow usage =="
rg -n '"http"|"http:default"|deny\[\]|allow\[\]|permission' plugins tauri\.conf\.json apps/desktop/src-tauri/src-tauri/Cargo.toml 2>/dev/null || true

echo
echo "== invoke_handler and tauri command registrations =="
cat -n apps/desktop/src-tauri/src/lib.rs | sed -n '1,180p'

Repository: team-reflect/reflect-open

Length of output: 49354


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== provider-fetch.ts =="
cat -n apps/desktop/src/lib/provider-fetch.ts

echo
echo "== AI provider submit hook =="
cat -n apps/desktop/src/hooks/use-add-ai-provider-submit.ts | sed -n '1,140p'

echo
echo "== AI provider section relevant validation =="
cat -n apps/desktop/src/components/settings/ai-providers-section.tsx | sed -n '130,220p'

echo
echo "== provider url validation helpers =="
rg -n 'function isPlainHttpRemoteBaseUrl|isPlainHttpRemoteBaseUrl|isHttpBaseUrl|normalizeOpenAICompatibleBaseUrl|normalizeBaseUrl|providerFetch' apps/desktop/src/lib apps/desktop/src/hooks apps/desktop/src/components/settings -g '*.ts' -g '*.tsx' || true

echo
echo "== settings provider provider config interfaces =="
cat -n apps/desktop/src/hooks/use-ai-providers.ts | sed -n '1,140p'

Repository: team-reflect/reflect-open

Length of output: 19035


Document the renderer network trust boundary for http:default.

The scope intentionally covers custom AI endpoints on localhost/LAN/clear-text hosts, but default.json applies to the main and note windows. If webview-rendered note content can execute fetches directly, this grants that content the same network reach; consider routing provider endpoints through a Tauri command with validated endpoints or tightening the host/port scope if the renderer is untrusted.

🤖 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 `@apps/desktop/src-tauri/capabilities/default.json` at line 21, Document the
renderer network trust boundary for the http:default capability near its broad
http and https URL allowlist, explicitly noting that it applies to main and note
windows and may expose custom AI endpoints to webview-rendered note content.
Preserve the intended localhost/LAN/clear-text support, and identify either
validated Tauri-command routing or a tighter host/port scope as the required
mitigation for untrusted renderer content.
apps/desktop/src/capability-http-scope.test.ts (1)

24-26: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate target file and related capability JSON"
fd -a 'capability-http-scope\.test\.ts$|default\.json$(|/tauri/capabilities/)' . | sed 's#^\./##' | head -50

echo
echo "Inspect target file around imports and target lines"
file="$(fd 'capability-http-scope\.test\.ts$' . | head -1)"
if [ -n "${file:=}" ]; then
  wc -l "$file"
  sed -n '1,80p' "$file" | cat -n
fi

echo
echo "Search for existing capability/tauri JSON Zod parsing patterns"
rg -n "CapabilitySchema|permissions.*zod|default\.json|JSON\.parse.*as|readFileSync.*json" -S --glob '!node_modules' --glob '!dist' . | head -120

Repository: team-reflect/reflect-open

Length of output: 6576


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect default capability JSON shape"
cat -n apps/desktop/src-tauri/capabilities/default.json | sed -n '1,220p'

echo
echo "Check Tauri Rust capability parsing code"
sed -n '70,110p' apps/desktop/src-tauri/src/lib.rs | cat -n

Repository: team-reflect/reflect-open

Length of output: 2792


Validate the capability JSON with Zod instead of asserting its parsed shape.

JSON.parse(...) as ... provides no runtime validation for the file-derived capability file. The default.json permissions also include string identifiers plus objects with identifier/allow, so the Zod schema should narrow that union rather than just checking url.

Proposed fix
+import { z } from 'zod'

+const ScopeEntrySchema = z.object({
+  url: z.string(),
+})
+const ObjectPermissionSchema = z.object({
+  identifier: z.string(),
+  allow: z.array(ScopeEntrySchema),
+})
+const CapabilitySchema = z.object({
+  permissions: z.array(z.union([z.string(), ObjectPermissionSchema])),
+})

-const capability = JSON.parse(
+const capability = CapabilitySchema.parse(JSON.parse(
   readFileSync(new URL('../src-tauri/capabilities/default.json', import.meta.url), 'utf8'),
-) as { permissions: (string | Permission)[] }
+))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

import { z } from 'zod'

const ScopeEntrySchema = z.object({
  url: z.string(),
})
const ObjectPermissionSchema = z.object({
  identifier: z.string(),
  allow: z.array(ScopeEntrySchema),
})
const CapabilitySchema = z.object({
  permissions: z.array(z.union([z.string(), ObjectPermissionSchema])),
})

const capability = CapabilitySchema.parse(JSON.parse(
  readFileSync(new URL('../src-tauri/capabilities/default.json', import.meta.url), 'utf8'),
))
🤖 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 `@apps/desktop/src/capability-http-scope.test.ts` around lines 24 - 26, Replace
the type assertion applied to the JSON loaded in the capability test with Zod
runtime validation. Define or reuse a schema for the capability object whose
permissions accept both string identifiers and objects containing identifier and
allow fields, then parse the result before use; do not restrict validation to a
url field.

Source: Coding guidelines

@Yiipu
Yiipu marked this pull request as draft July 30, 2026 08:16

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/hooks/use-ai-providers.ts (1)

71-117: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Validate baseUrl before storing the keychain secret for OpenAI-compatible providers.

draft.baseUrl is normalized but not checked before setSecret, so an invalid OpenAI-compatible URL can be written to the keychain even though the settings entry later parses out with .safeParse()/no .catch() before the schema transform. Add the isHttpBaseUrl(draft.baseUrl ?? '') guard before persisting the secret, as the add-provider forms already require before submission.

🤖 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 `@apps/desktop/src/hooks/use-ai-providers.ts` around lines 71 - 117, Validate
the OpenAI-compatible provider’s baseUrl before calling setSecret in
addProvider. Use isHttpBaseUrl(draft.baseUrl ?? '') as a guard, rejecting
invalid URLs before any keychain write while preserving the existing behavior
for other provider types.

Source: Coding guidelines

♻️ Duplicate comments (1)
packages/core/src/ai/transcribe.test.ts (1)

301-321: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Still no coverage for the no-key OpenAI-compatible case — underlying header bug appears unresolved.

Per the prior review, transcribeWithOpenAi unconditionally sends Authorization: Bearer ${request.apiKey}; the transcribe.ts snippet in this diff's context still shows that unconditional header. None of the new transcribeAudio (openai-compatible) tests use apiKey: '', so the no-auth compatible-server scenario (which secrets.test.ts and language-model.test.ts now explicitly support) remains untested and, per the source snippet, still broken — a compatible server without auth would receive a malformed Bearer header.

🐛 Suggested test addition (mirrors the fix requested previously)
+  it('omits Authorization when the endpoint requires no key', async () => {
+    const calls: RecordedCall[] = []
+    const fetchFn = recordingFetch(calls, () => jsonResponse(200, { text: 'hello' }))
+
+    await transcribeAudio(
+      request({
+        provider: 'openai-compatible',
+        apiKey: '',
+        baseUrl: 'https://transcribe.example.com/v1',
+        model: 'whisper-large-v3',
+        fetchFn,
+      }),
+    )
+
+    expect(calls[0]!.headers['Authorization']).toBeUndefined()
+  })
🤖 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 `@packages/core/src/ai/transcribe.test.ts` around lines 301 - 321, Add coverage
in the transcribeAudio (openai-compatible) tests for an empty apiKey, asserting
the request omits the Authorization header while still using the configured base
URL and model. Update transcribeWithOpenAi to add the Bearer Authorization
header only when request.apiKey is non-empty, preserving authenticated requests
unchanged.
🧹 Nitpick comments (1)
packages/core/src/actions/audio-memo.ts (1)

379-400: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer a type guard over the as TranscriptionProvider assertion.

input.config.provider as TranscriptionProvider silently widens/narrows past the fact that AiProviderConfig.provider also includes anthropic/openrouter, which TranscriptionProvider's own doc comment says "should never reach transcribeAudio". It's safe today only because callers (resolveTranscriptionTarget/transcriptionProviders) pre-filter via aiProviderSupportsTranscription, but the assertion itself doesn't encode that invariant.

♻️ Suggested approach: export and use a type-guard predicate
// provider-config.ts
+export function isTranscriptionProvider(
+  provider: AiProviderConfig['provider'],
+): provider is TranscriptionProvider {
+  return provider === 'openai' || provider === 'google' || provider === 'openai-compatible'
+}
// audio-memo.ts
-    const text = await transcribeAudio({
-      provider: input.config.provider as TranscriptionProvider,
+    if (!isTranscriptionProvider(input.config.provider)) {
+      throw new Error(`unsupported transcription provider: ${input.config.provider}`)
+    }
+    const text = await transcribeAudio({
+      provider: input.config.provider,

As per coding guidelines, **/*.{ts,tsx}: "Never use any or as any.", "Avoid unnecessary type assertions and do not use assertions to parse JSON.", and "Use discriminated unions and type guards for variant data; export helper predicates when they clarify a public contract."

🤖 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 `@packages/core/src/actions/audio-memo.ts` around lines 379 - 400, Replace the
`as TranscriptionProvider` assertion in `memoNoteBody` with an exported
type-guard predicate that validates whether `input.config.provider` supports
transcription, then call `transcribeAudio` only through the narrowed provider
type. Reuse the existing provider-support definitions such as
`aiProviderSupportsTranscription` or `transcriptionProviders` so unsupported
`anthropic` and `openrouter` values cannot reach `transcribeAudio`, preserving
the current behavior for valid transcription providers.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@apps/desktop/src/hooks/use-ai-providers.ts`:
- Around line 71-117: Validate the OpenAI-compatible provider’s baseUrl before
calling setSecret in addProvider. Use isHttpBaseUrl(draft.baseUrl ?? '') as a
guard, rejecting invalid URLs before any keychain write while preserving the
existing behavior for other provider types.

---

Duplicate comments:
In `@packages/core/src/ai/transcribe.test.ts`:
- Around line 301-321: Add coverage in the transcribeAudio (openai-compatible)
tests for an empty apiKey, asserting the request omits the Authorization header
while still using the configured base URL and model. Update transcribeWithOpenAi
to add the Bearer Authorization header only when request.apiKey is non-empty,
preserving authenticated requests unchanged.

---

Nitpick comments:
In `@packages/core/src/actions/audio-memo.ts`:
- Around line 379-400: Replace the `as TranscriptionProvider` assertion in
`memoNoteBody` with an exported type-guard predicate that validates whether
`input.config.provider` supports transcription, then call `transcribeAudio` only
through the narrowed provider type. Reuse the existing provider-support
definitions such as `aiProviderSupportsTranscription` or
`transcriptionProviders` so unsupported `anthropic` and `openrouter` values
cannot reach `transcribeAudio`, preserving the current behavior for valid
transcription providers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 69127053-5c81-4493-a783-581448599669

📥 Commits

Reviewing files that changed from the base of the PR and between ae18310 and aabace9.

📒 Files selected for processing (27)
  • apps/desktop/src/components/settings/describe-assets-field.tsx
  • apps/desktop/src/hooks/use-ai-providers.ts
  • apps/desktop/src/hooks/use-audio-memo-pipeline.ts
  • apps/desktop/src/lib/asset-describe-controller.test.tsx
  • apps/desktop/src/lib/capture-controller.test.tsx
  • apps/desktop/src/lib/transcription-reconciler.test.tsx
  • apps/desktop/src/providers/asset-describe-provider.tsx
  • apps/desktop/src/providers/capture-provider.tsx
  • apps/desktop/src/providers/chat-provider.tsx
  • packages/core/src/actions/asset-description.test.ts
  • packages/core/src/actions/audio-memo.test.ts
  • packages/core/src/actions/audio-memo.ts
  • packages/core/src/actions/capture-harness.ts
  • packages/core/src/ai/audio-memo-title.test.ts
  • packages/core/src/ai/chat/model-options.test.ts
  • packages/core/src/ai/language-model.test.ts
  • packages/core/src/ai/provider-catalog.ts
  • packages/core/src/ai/provider-config.test.ts
  • packages/core/src/ai/provider-config.ts
  • packages/core/src/ai/secrets.test.ts
  • packages/core/src/ai/transcribe.test.ts
  • packages/core/src/ai/transcribe.ts
  • packages/core/src/exports/ai-actions.ts
  • packages/core/src/settings/schema.test.ts
  • packages/core/src/settings/schema.ts
  • plugins/tauri-plugin-keyboard/permissions/schemas/schema.json
  • plugins/tauri-plugin-recording/permissions/schemas/schema.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/desktop/src/lib/asset-describe-controller.test.tsx
  • packages/core/src/ai/chat/model-options.test.ts
  • plugins/tauri-plugin-keyboard/permissions/schemas/schema.json
  • packages/core/src/ai/audio-memo-title.test.ts
  • plugins/tauri-plugin-recording/permissions/schemas/schema.json

Yiipu and others added 4 commits July 30, 2026 16:40
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove duplicate TranscriptionTarget, TranscriptionMiss, and
resolveTranscriptionTarget definitions left over from a merge
between the master-base version and the current-branch version.

The retained version uses the dynamic transcriptionProviders()
function with the correct defaultTranscriptionProviderId.

Also:
- Add TRANSCRIPTION_PROVIDERS constant for desktop consent UI
- Remove unused memoNoteBody function (dead code)
- Fix type narrowing with as TranscriptionProvider assertion
- Fix test snapshots missing defaultTranscriptionProviderId

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

- Split default checkbox into chat and transcription options in add forms

- Add transcriptionModel field to add-provider forms (openai-compatible)

- Extend AiProviderRow with second row for transcription (model + default)

- Add setTranscriptionModel hook and transcriptionProvider to useAiProviders

- Update AiProviderActionsDrawer with transcription model editing

- Update NO_PROVIDER_REASON to include OpenAI-compatible providers

- Export transcription model constants from @reflect/core

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The openai-compatible transcription provider was effectively
non-functional: two wiring gaps stopped it from ever reaching the
user's local endpoint.

1. transcribeSessionParts dropped baseUrl and transcriptionModel when
   calling transcribeAudio, so audio was sent to OpenAI's hosted
   endpoint with gpt-4o-mini-transcribe regardless of the configured
   local server.

2. resolveTranscriptionTarget resolved keys via a raw getSecret, which
   bypassed aiApiKeyForConfig's no-key handling - a keyless
   openai-compatible entry (a local server with no API key) always
   read null from the keychain and was skipped as 'no-key', silently
   stopping every pass before any transcription call.

The reconciler now forwards config.baseUrl and config.transcriptionModel
for openai-compatible providers, and resolveTranscriptionTarget receives
the full AiProviderConfig so callers can use aiApiKeyForConfig for key
resolution.
@Yiipu

Yiipu commented Jul 31, 2026

Copy link
Copy Markdown
Author

Closing this PR in favor of #1007.

The scope grew beyond what the original PR body described. #995 started as transcription-only, but the actual diff now includes independent chat and transcription defaults, a disabled sentinel for openai-compatible entries, and the full desktop and mobile settings UI. The original body listed five change areas and explicitly deferred the settings UI. The bot review also landed on that stale description.

#1007 starts from the same commits with a clean review surface and an accurate PR body. It stays draft until I manually open it.

@Yiipu Yiipu closed this Jul 31, 2026
@Yiipu
Yiipu deleted the draft/openai-compatible-transcription branch August 4, 2026 21:39
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