Skip to content

feat(desktop): AI provider custom headers support (LLM, STT, batch)#3878

Closed
devin-ai-integration[bot] wants to merge 4 commits intomainfrom
devin/1770827400-ai-provider-custom-headers
Closed

feat(desktop): AI provider custom headers support (LLM, STT, batch)#3878
devin-ai-integration[bot] wants to merge 4 commits intomainfrom
devin/1770827400-ai-provider-custom-headers

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Feb 11, 2026

Summary

Implements optional custom HTTP headers for AI providers end-to-end:

  • Persistence
    • packages/store: zod schema adds optional custom_headers (JSON string)
    • TinyBase settings store: ai_providers table gains custom_headers; transforms updated
  • UI
    • Settings → NonHyprProviderCard: “Custom Headers” editor (key/value UI, stored as JSON string)
  • LLM
    • useLLMConnection: parses provider custom_headers JSON → Record<string,string>
    • Passes headers to all providers supported by ai-sdk (OpenAI, Anthropic, Google, OpenRouter, OpenAI-compatible) and adds headers to Ollama fetch
  • Live STT (listener plugin)
    • TS → Rust: SessionParams gains optional custom_headers (Record<string,string>)
    • Rust: ListenerArgs carries custom_headers; adapters apply headers via ListenClientBuilder.extra_header()
  • Batch STT (listener2)
    • TS → Rust: BatchParams gains optional custom_headers (Record<string,string>)
    • Streaming batch path (spawn_batch_task_with_adapter) applies headers via ListenClientBuilder.extra_header()
    • Note: Non-streaming batch path (BatchClient::transcribe_file) does not yet apply headers (follow-up item)

Generated TS bindings were updated in repo (plugins/listener/js/bindings.gen.ts, plugins/listener2/js/bindings.gen.ts) to keep TS types in sync until Specta regenerates them in CI.

Linked issue: #3875

Review & Testing Checklist for Human (5 items)

  • LLM headers end-to-end:
    • Configure a non-Hypr LLM provider with base_url and a custom header (e.g., Authorization: Bearer test). Confirm outbound requests include the custom header across:
      • openai, anthropic, google_generative_ai, openrouter, openai-compatible, ollama
    • For Anthropic, verify required default headers remain and custom headers merge correctly
  • Live STT headers end-to-end:
    • Configure a non-Hypr STT provider (Deepgram/Soniox/etc.) with base_url + custom headers
    • Start listening and confirm the WebSocket connection includes headers (server logs or a proxy)
    • Validate both MicOnly and Mic+Speaker modes work
  • Settings persistence and backwards compatibility:
    • Save providers with and without custom headers; reload app; verify values persist
    • Ensure existing users without custom_headers continue to work (TinyBase defaults/undefined path)
  • Batch STT behavior:
    • AM/Argmax (streaming) path: confirm headers are present in the streaming connection
    • Non-AM providers (Deepgram/Soniox/AssemblyAI): Note that run_batch (non-streaming) currently does not apply custom headers; confirm this limitation and whether a follow-up is acceptable
  • UI/UX:
    • Custom Headers editor: add/remove pairs; confirm serialization (JSON) and no crashes with empty inputs
    • Verify “Advanced” section layout changes are consistent with design

Recommended test plan:

  1. In Settings → AI providers, set a custom base_url to a test server that logs headers; add custom headers (e.g., X-Test: 123).
  2. LLM:
    • Trigger any chat/title/enhance operation. Verify headers on server side.
  3. Live STT:
    • Start a session using the configured STT provider; verify headers on server side during WS connect.
  4. Batch STT:
    • AM model: run batch transcription; verify headers on server side during WS connect.
    • Deepgram/Soniox/AssemblyAI: run batch transcription; confirm current limitation (no headers on non-streaming path). If necessary, we can extend owhisper-client BatchClient to support extra headers in a follow-up.

Notes

  • Generated TS bindings were manually updated in-repo to include the new optional fields; Specta should regenerate matching definitions in CI. If CI regenerates and alters these files, we should prefer the generated output.
  • Batch non-streaming path doesn’t yet support custom headers. A clean follow-up is to add extra_headers support to owhisper-client’s BatchClient and adapters, then plumb params in listener2/ext.rs::run_batch_with_adapter.
  • Link to Devin run: https://app.devin.ai/sessions/d1eabd7851614d15b37b18ca8a12d920
  • Requested by: @ComputelessComputer

Open with Devin

…atch)\n\n- Store custom_headers in TinyBase + Zod schema\n- Settings UI: add custom headers editor (JSON key/value)\n- LLM: pass headers to providers (OpenAI, Anthropic, Google, OpenRouter, Ollama, OpenAI-compatible)\n- STT live: plumb headers TS→Rust; apply via ListenClientBuilder.extra_header()\n- Batch: plumb headers; apply for streaming batch; prepare for non-streaming\n\nCloses #3875

Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
@netlify
Copy link

netlify bot commented Feb 11, 2026

Deploy Preview for hyprnote canceled.

Name Link
🔨 Latest commit 2437742
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/698cb727ab88cb0008895512

@netlify
Copy link

netlify bot commented Feb 11, 2026

Deploy Preview for hyprnote-storybook canceled.

Name Link
🔨 Latest commit 2437742
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/698cb7279cbb4e0008b69ca2

@devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

Open in Devin Review

Comment on lines +281 to +291
function serializeHeaders(
headers: Array<{ key: string; value: string }>,
): string {
const filtered = headers.filter((h) => h.key.trim());
if (filtered.length === 0) return "";
const obj: Record<string, string> = {};
for (const h of filtered) {
obj[h.key] = h.value;
}
return JSON.stringify(obj);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🔴 "Add header" button does nothing because new empty-key headers are immediately filtered out during serialization

When the user clicks "Add header", the addHeader function appends { key: "", value: "" } to the headers array and calls update(), which calls serializeHeaders(). However, serializeHeaders at line 284 filters out all headers where h.key.trim() is falsy. Since the newly added header has an empty key, it is immediately removed during serialization. The onChange callback receives the same serialized string as before, so the form value doesn't change and no new input row appears in the UI.

Root Cause and Impact

The flow is:

  1. addHeader()update([...headers, { key: "", value: "" }])
  2. updateonChange(serializeHeaders(newHeaders))
  3. serializeHeaders filters: headers.filter((h) => h.key.trim()) — the new { key: "", value: "" } entry is removed
  4. The serialized output is identical to before → form value unchanged → no new row renders

This means the Custom Headers editor is completely non-functional — users cannot add any headers at all.

Impact: The entire custom headers UI feature is broken. Users see an "Add header" button that does nothing when clicked.

Prompt for agents
The CustomHeadersField component needs to manage its own local state for the header rows, rather than deriving them purely from the serialized JSON string. The issue is that serializeHeaders filters out empty-key headers, but the UI needs to show empty rows so the user can type into them.

Option 1 (recommended): Use React useState for the headers array inside CustomHeadersField. Initialize from the parsed value, but keep the local state for intermediate editing. Only call onChange (which triggers serialization) when the user actually types a non-empty key. This means addHeader should only update local state, not call onChange.

Option 2: Change serializeHeaders to not filter out empty keys, and instead only filter on save/submit. But this would store invalid JSON with empty keys.

Option 3: Change the component to use local state via useState, sync from props on mount, and call onChange on blur or after a debounce rather than on every keystroke.

The key fix is in the CustomHeadersField component at apps/desktop/src/components/settings/ai/shared/index.tsx around lines 293-358. The addHeader, removeHeader, and updateHeader functions need to work with local state, and only propagate to the form (via onChange) when the data is meaningful.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 125 to 126
}, [
current_stt_provider,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🔴 Missing customHeadersRaw in useMemo dependency array causes stale custom headers in STT connection

In useSTTConnection.ts, the customHeadersRaw variable is used inside the useMemo callback (lines 110 and 123) but is not included in the dependency array (lines 125-135). This means when a user updates custom headers in settings, the memoized connection object will retain the old/stale customHeaders value until some other dependency (like baseUrl, apiKey, etc.) changes.

Root Cause and Impact

At line 89, customHeadersRaw is derived from providerConfig?.custom_headers?.trim(). It's referenced at lines 110 and 123 inside the useMemo, but the dependency array at lines 125-135 does not include customHeadersRaw:

}, [
    current_stt_provider,
    current_stt_model,
    isLocalModel,
    isCloudModel,
    local.data,
    baseUrl,
    apiKey,
    auth,
    billing.isPro,
  ]);

Impact: After changing custom headers in the STT provider settings, the live STT and batch STT connections will continue using the old headers until the user also changes another setting (like base URL or API key) that triggers the memo to recompute. This makes the custom headers feature appear broken for STT.

(Refers to lines 125-135)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 260 to 264
headers: {
"anthropic-version": "2023-06-01",
"anthropic-dangerous-direct-browser-access": "true",
...conn.customHeaders,
},
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🟡 Custom headers can override required Anthropic default headers via object spread

In the Anthropic provider case in createLanguageModel, custom headers are spread after the required default headers using ...conn.customHeaders. If a user sets a custom header with key anthropic-version or anthropic-dangerous-direct-browser-access, it will override the required defaults, potentially breaking Anthropic API calls.

Root Cause and Impact

At useLLMConnection.ts:260-264:

headers: {
  "anthropic-version": "2023-06-01",
  "anthropic-dangerous-direct-browser-access": "true",
  ...conn.customHeaders,  // can override the above
},

The spread operator places custom headers after the defaults, so any matching keys in conn.customHeaders will take precedence. For example, if a user accidentally or intentionally sets anthropic-version to a wrong value, the Anthropic API call would fail with an API version error.

Impact: Users who set custom headers with names matching Anthropic's required headers will break their Anthropic integration. The fix should spread custom headers first, then apply the required defaults, or filter out the protected header names from custom headers.

Suggested change
headers: {
"anthropic-version": "2023-06-01",
"anthropic-dangerous-direct-browser-access": "true",
...conn.customHeaders,
},
headers: {
...conn.customHeaders,
"anthropic-version": "2023-06-01",
"anthropic-dangerous-direct-browser-access": "true",
},
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

devin-ai-integration bot and others added 2 commits February 11, 2026 16:55
…stency

Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
… compatibility

Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
type: "llm" | "stt";
base_url: string;
api_key: string;
custom_headers: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

Type mismatch: custom_headers is defined as required (non-optional) but lines 118-119 and 161 conditionally include it. This creates a type safety issue where ProviderRow objects may be missing the required field.

Fix by making it optional:

custom_headers?: string;
Suggested change
custom_headers: string;
custom_headers?: string;

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
@yujonglee yujonglee closed this Feb 14, 2026
@yujonglee yujonglee deleted the devin/1770827400-ai-provider-custom-headers branch February 14, 2026 01:49
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.

2 participants