Skip to content

fix(providers): use native ElevenLabs API on desktop to avoid unspeech proxy 401#1107

Open
Hanfeng-Lin wants to merge 4 commits intomoeru-ai:mainfrom
Hanfeng-Lin:elevenlabs_debug
Open

fix(providers): use native ElevenLabs API on desktop to avoid unspeech proxy 401#1107
Hanfeng-Lin wants to merge 4 commits intomoeru-ai:mainfrom
Hanfeng-Lin:elevenlabs_debug

Conversation

@Hanfeng-Lin
Copy link
Copy Markdown

This is a follow-up to the previous PR #1100 , addressing the rejection reason.

Problem

The previous PR called ElevenLabs API directly from all platforms, which broke the web version due to CORS — browsers block cross-origin requests to api.elevenlabs.io since it doesn't return Access-Control-Allow-Origin headers.

The underlying issue remains: the public unspeech.hyp3r.link proxy causes HTTP 401 errors for Free Tier users because many users share the same proxy IP, triggering ElevenLabs' abuse detection.

Solution

Use isStageTamagotchi() to branch by platform:

  • Desktop: Electron's renderer is not subject to browser CORS rules, so direct cross-origin fetch to api.elevenlabs.io works, avoiding the shared-IP 401 issue.
  • Web: Keeps routing through the configured unspeech proxy — required for CORS compliance.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves persistent HTTP 401 errors for ElevenLabs Free Tier users by intelligently routing API requests based on the application platform. Desktop clients now directly access the native ElevenLabs API, circumventing issues with shared proxy IPs and CORS limitations. Web clients continue to utilize the unspeech proxy to ensure CORS compliance. This change significantly improves the reliability of ElevenLabs integration for desktop users while maintaining functionality for web users.

Highlights

  • ElevenLabs API Routing: Implemented platform-specific routing for ElevenLabs API calls, using the native API for desktop (Electron) and the unspeech proxy for web.
  • 401 Error Resolution: Addressed the HTTP 401 errors experienced by ElevenLabs Free Tier users on desktop by bypassing the shared-IP unspeech proxy.
  • Custom Fetch Logic: Introduced custom fetch logic for desktop to transform OpenAI-format requests into ElevenLabs' native API format.
  • Voice Listing Enhancement: Updated the voice listing mechanism to fetch voices directly from ElevenLabs for desktop and via the unspeech proxy for web, while maintaining consistent voice reordering.
Changelog
  • packages/stage-ui/src/stores/providers.ts
    • Modified the baseUrl for ElevenLabs to dynamically switch between the native ElevenLabs API and the unspeech proxy based on whether the application is running on desktop (isStageTamagotchi()).
    • Refactored the createProvider function for ElevenLabs to include a custom fetch implementation for desktop, translating OpenAI-format requests to ElevenLabs' native text-to-speech endpoint.
    • Updated the listVoices capability to perform platform-specific voice fetching: direct API calls for desktop and proxy-based calls for web, followed by a unified voice reordering logic.
Activity
  • No human activity has been recorded on this pull request since its creation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Code Review

This pull request introduces platform-specific logic for the ElevenLabs provider to use the native API on desktop, avoiding CORS issues and potential 401 errors from the public proxy. The changes are well-structured and address the problem described. I've identified a few areas for improvement regarding error handling, cleaner JSON payload construction, and a potential bug in array manipulation. My suggestions aim to enhance the robustness and maintainability of the new implementation.

Note: Security Review did not run due to the size of the PR.

voice?: string
model?: string
}
const voiceId = encodeURIComponent(body.voice ?? '')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If body.voice is undefined or an empty string, voiceId will be empty. This will likely cause the fetch call on line 899 to fail with a 404 error, as the URL will be .../text-to-speech/. It would be more robust to add a check for an empty voiceId and return an appropriate error response before making the API call.

Comment on lines +976 to +978
const lo = Math.min(...['Aria', 'Bill'].map((n) => { const i = voices.findIndex(v => v.name.includes(n)); return i !== -1 ? i : voices.length - 1 }))
const hi = Math.max(...['Aria', 'Bill'].map((n) => { const i = voices.findIndex(v => v.name.includes(n)); return i !== -1 ? i : 0 }))
return [...voices.slice(0, lo), ...voices.slice(hi + 1), ...voices.slice(lo, hi + 1)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

This logic for rearranging voices has a bug when neither 'Aria' nor 'Bill' are found in the voices array. In that case, lo becomes voices.length - 1 and hi becomes 0, causing lo > hi. The spread syntax on the next line will then duplicate parts of the array. A more robust implementation would handle this edge case explicitly.

Suggested change
const lo = Math.min(...['Aria', 'Bill'].map((n) => { const i = voices.findIndex(v => v.name.includes(n)); return i !== -1 ? i : voices.length - 1 }))
const hi = Math.max(...['Aria', 'Bill'].map((n) => { const i = voices.findIndex(v => v.name.includes(n)); return i !== -1 ? i : 0 }))
return [...voices.slice(0, lo), ...voices.slice(hi + 1), ...voices.slice(lo, hi + 1)]
const ariaIdx = voices.findIndex(v => v.name.includes('Aria'));
const billIdx = voices.findIndex(v => v.name.includes('Bill'));
if (ariaIdx === -1 && billIdx === -1) {
return voices;
}
const lo = Math.min(ariaIdx > -1 ? ariaIdx : Infinity, billIdx > -1 ? billIdx : Infinity);
const hi = Math.max(ariaIdx, billIdx);
return [...voices.slice(0, lo), ...voices.slice(hi + 1), ...voices.slice(lo, hi + 1)];

Comment on lines +909 to +914
voice_settings: {
stability: voiceSettings.stability ?? 0.5,
similarity_boost: voiceSettings.similarityBoost ?? 0.75,
style: voiceSettings.style,
use_speaker_boost: voiceSettings.useSpeakerBoost,
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

If voiceSettings.style or voiceSettings.useSpeakerBoost are undefined, they will be stringified as null in the JSON payload. While many APIs handle null gracefully, it's safer and cleaner to omit these properties from the payload if they are not defined. You can use conditional spreading to achieve this.

Suggested change
voice_settings: {
stability: voiceSettings.stability ?? 0.5,
similarity_boost: voiceSettings.similarityBoost ?? 0.75,
style: voiceSettings.style,
use_speaker_boost: voiceSettings.useSpeakerBoost,
},
voice_settings: {
stability: voiceSettings.stability ?? 0.5,
similarity_boost: voiceSettings.similarityBoost ?? 0.75,
...(voiceSettings.style !== undefined && { style: voiceSettings.style }),
...(voiceSettings.useSpeakerBoost !== undefined && { use_speaker_boost: voiceSettings.useSpeakerBoost })
}

@nekomeowww
Copy link
Copy Markdown
Member

But the request still happens right inside of the browser - provided by Electrom, which is based on Chromium, the CORS issue still exists...

@Hanfeng-Lin
Copy link
Copy Markdown
Author

The desktop version has already been tested and confirmed working without CORS issue. The network requests reach api.elevenlabs.io and receive valid responses, which would be impossible if browser-level CORS were enforced. The web end was also tested and the proxy is working as normal.
I think Electron has more relaxed security policies which could bypass the CORS restrictions, either by webPreferences: { webSecurity: false } in BrowserWindow or session.webRequest.onHeadersReceived to inject CORS headers into responses

@shinohara-rin
Copy link
Copy Markdown
Contributor

$ curl -sI -X OPTIONS https://api.elevenlabs.io/v1/text-to-speech/test
-H "Origin: http://localhost"
-H "Access-Control-Request-Method: POST"

access-control-allow-origin: *
access-control-allow-headers: *

strange things, looks like elevenlabs return CORS headers as of now(or is it just me???)

@Hanfeng-Lin
Copy link
Copy Markdown
Author

Thanks @shinohara-rin for pointing out! I've updated the branch to bypass the unspeech proxy and call ElevenLabs' native API directly across all platforms. Previously this was Desktop-only due to CORS issues, but ElevenLabs now returns 'Access-Control-Allow-Origin: *'.

Both platforms were tested successfully

Copy link
Copy Markdown
Contributor

@shinohara-rin shinohara-rin left a comment

Choose a reason for hiding this comment

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

It looks a bit dirty to me to handcraft an http client here, this might only work as a temporary fix.

shall we consider supporting the elevenlabs native api? @nekomeowww
with CORS out of the way, we still have the anti-abuse problem to deal with

@Hanfeng-Lin
Copy link
Copy Markdown
Author

If we're sticking with the native ElevenLabs API (because this solves the 401 redirect issue caused by shared IPs once and for all), I can extract the dirty work related to fetching and create a dedicated adapter file in src/stores/providers/elevenlabs/native.ts, exposing a clean createNativeElevenLabsProvider(). This way, providers.ts can now function like before, simply calling a single line of code.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 7, 2026

⏳ Approval required for deploying to Cloudflare Workers (Preview) for stage-web.

Name Link
🔭 Waiting for approval For maintainers, approve here

Hey, @nekomeowww, @sumimakito, @luoling8192, @LemonNekoGH, kindly take some time to review and approve this deployment when you are available. Thank you! 🙏

@nekomeowww nekomeowww added scope/providers Scope related to providers we support bug/providers Some providers aren't working pr-review/hold/unsure Pull Request that unsure about purpose, not sure if needed labels Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug/providers Some providers aren't working pr-review/hold/unsure Pull Request that unsure about purpose, not sure if needed scope/providers Scope related to providers we support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants