Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

## [Windows] Unreleased

- Raycast: automatic cookie import walks every supported browser in the shared order and uses the first accepted session, instead of reading Chrome alone (upstream #4443, 0aef66c73).
- Price Codex **codex-auto-review** requests made from a ChatGPT account at $0 from 2026-10-06 07:00 UTC onward: tokens stay in totals, the model is no longer listed as unpriced, and an explicit custom price for the model still applies (upstream #4452, #4453).
- Show a spend-only Claude account's monthly cap (for example an Enterprise seat) as one primary meter with calendar-month pace and a midnight-UTC-on-the-1st reset inferred when Claude reports none, instead of a separate Extra usage bar; quota accounts keep their session and weekly lanes and Extra usage section unchanged. Fixes #4381 (upstream #4480)
- Read the Charm Hyper balance from your signed-in browser session on the team dashboard, with plan and refresh rows when shown. The credits endpoint only accepts API keys, so session balances no longer depend on it. An expired session is reported as expired before any API-key fallback; API-key balances are unchanged (upstream #4435).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ See the full history in [CHANGELOG.md](CHANGELOG.md).
| OpenAI | Admin API / API Key | Usage, Requests, Project-scoped cost, Credit Balance |
| Grok | Cookies / auth.json | Billing |
| Helmcode (also NaN Builders) | Browser cookies / manual Cookie header | Per-model token quotas, reset windows, Helmcode prepaid balance |
| Raycast | Cookies (Chrome auto or manual) | Monthly AI credits, plan, renewal |
| Raycast | Cookies (browser auto or manual) | Monthly AI credits, plan, renewal |
| Replicate | Cookies / token accounts | Monthly spend, credit balance |
| Aixy | API Key / token accounts | Applicable budget balances, 7-day key usage |
| ClawRouter | API Key | Monthly budget, requests, tokens, routed-provider spend |
Expand Down
43 changes: 43 additions & 0 deletions rust/src/browser/detection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,49 @@ mod tests {
}
}

/// Firefox Developer Edition keeps its profiles under the shared Roaming
/// root with a `<salt>.dev-edition-default` name. It must be detected as
/// the default profile, like `.default-release`, and not be dropped.
#[test]
fn firefox_developer_edition_profiles_are_detected_as_default() {
let temp = tempfile::tempdir().unwrap();
let roaming = temp.path().join("Roaming");
let profiles = roaming.join("Mozilla/Firefox/Profiles");
let dev_edition = profiles.join("k7x2m9ab.dev-edition-default");
std::fs::create_dir_all(&dev_edition).unwrap();
std::fs::create_dir_all(profiles.join("q3w8e1rt.default-release")).unwrap();
std::fs::create_dir_all(profiles.join("z9y8x7wv.other")).unwrap();

let detected = BrowserDetector::detect_in_roots(
BrowserType::Firefox,
temp.path(),
Some(roaming.as_path()),
)
.unwrap();

assert_eq!(detected.user_data_dir, profiles);
assert_eq!(detected.profiles.len(), 3);
let dev = detected
.profiles
.iter()
.find(|profile| profile.name == "k7x2m9ab.dev-edition-default")
.expect("Developer Edition profile should be detected");
assert_eq!(dev.path, dev_edition);
assert!(dev.is_default);
assert!(
detected
.profiles
.iter()
.any(|profile| profile.name == "q3w8e1rt.default-release" && profile.is_default)
);
assert!(
detected
.profiles
.iter()
.any(|profile| profile.name == "z9y8x7wv.other" && !profile.is_default)
);
}

#[test]
fn test_browser_detection() {
let browsers = BrowserDetector::detect_all();
Expand Down
2 changes: 1 addition & 1 deletion rust/src/locale/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1135,7 +1135,7 @@ UsageSpendModelsPeriodCaption = Local model history for: { "{}" }.
UsageSpendProjectsPeriodCaption = Ranked Codex local project spend for: { "{}" }
BurndownChartTitle = Usage remaining
BurndownChartAriaLabel = Recorded remaining quota burndown
ProviderRaycastAutoImportHelp = Automatically imports the signed-in www.raycast.com session from Chrome.
ProviderRaycastAutoImportHelp = Automatically imports the signed-in www.raycast.com session from your installed browsers.
ProviderRaycastManualCookieHelp = Paste a Cookie header from www.raycast.com/settings.
ProviderRaycastCookiesDisabled = Raycast cookies are disabled.
CookieHelpCodexManual = Paste a Cookie header from a chatgpt.com request.
Expand Down
2 changes: 1 addition & 1 deletion rust/src/locale/es-MX.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1132,7 +1132,7 @@ UsageSpendModelsPeriodCaption = Historial local de modelos para: { "{}" }.
UsageSpendProjectsPeriodCaption = Gasto local de proyectos de Codex, ordenado, para: { "{}" }
BurndownChartTitle = Uso restante
BurndownChartAriaLabel = Consumo registrado de la cuota restante
ProviderRaycastAutoImportHelp = Importa automáticamente desde Chrome la sesión iniciada en www.raycast.com.
ProviderRaycastAutoImportHelp = Importa automáticamente la sesión iniciada en www.raycast.com desde los navegadores instalados.
ProviderRaycastManualCookieHelp = Pega un encabezado Cookie de www.raycast.com/settings.
ProviderRaycastCookiesDisabled = Las cookies de Raycast están desactivadas.
CookieHelpCodexManual = Pega un encabezado Cookie de una solicitud a chatgpt.com.
Expand Down
2 changes: 1 addition & 1 deletion rust/src/locale/ja-JP.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,7 @@ UsageSpendModelsPeriodCaption = ローカルのモデル履歴の期間: { "{}"
UsageSpendProjectsPeriodCaption = Codex ローカルプロジェクトのコストランキングの期間: { "{}" }
BurndownChartTitle = 残り使用量
BurndownChartAriaLabel = 記録された残りクォータの推移
ProviderRaycastAutoImportHelp = Chrome からサインイン済みの www.raycast.com セッションを自動でインポートします。
ProviderRaycastAutoImportHelp = インストール済みのブラウザーからサインイン済みの www.raycast.com セッションを自動でインポートします。
ProviderRaycastManualCookieHelp = www.raycast.com/settings の Cookie ヘッダーを貼り付けます。
ProviderRaycastCookiesDisabled = Raycast の Cookie は無効です。
CookieHelpCodexManual = chatgpt.com へのリクエストの Cookie ヘッダーを貼り付けます。
Expand Down
2 changes: 1 addition & 1 deletion rust/src/locale/ko-KR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1132,7 +1132,7 @@ UsageSpendModelsPeriodCaption = 로컬 모델 기록 기간: { "{}" }.
UsageSpendProjectsPeriodCaption = Codex 로컬 프로젝트 비용 순위 기간: { "{}" }
BurndownChartTitle = 남은 사용량
BurndownChartAriaLabel = 기록된 남은 할당량 소진 추이
ProviderRaycastAutoImportHelp = Chrome에서 로그인된 www.raycast.com 세션을 자동으로 가져옵니다.
ProviderRaycastAutoImportHelp = 설치된 브라우저에서 로그인된 www.raycast.com 세션을 자동으로 가져옵니다.
ProviderRaycastManualCookieHelp = www.raycast.com/settings의 Cookie 헤더를 붙여넣습니다.
ProviderRaycastCookiesDisabled = Raycast 쿠키가 비활성화되었습니다.
CookieHelpCodexManual = chatgpt.com 요청의 Cookie 헤더를 붙여넣습니다.
Expand Down
2 changes: 1 addition & 1 deletion rust/src/locale/pt-BR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,7 @@ ProviderLiteLLMModelActivity = Mostrar atividade por modelo
ProviderLiteLLMModelActivityHelper = Adiciona tokens e solicitações por modelo do seu usuário nos últimos 30 dias UTC (top 20). Faz até 3 solicitações extras ao seu proxy LiteLLM por atualização; se ele estiver indisponível, a seção fica oculta e os orçamentos não são afetados. Desativado por padrão.
ProviderClaudeWorkspaceSpend = Mostrar gasto por workspace
ProviderClaudeWorkspaceSpendHelper = Adiciona o gasto da Admin API por workspace nos últimos 30 dias (top 20, exibido só quando mais de um workspace tem gasto). Os totais da organização não mudam e nenhuma solicitação extra é feita. Desativado por padrão.
ProviderRaycastAutoImportHelp = Importa automaticamente a sessão conectada de www.raycast.com do Chrome.
ProviderRaycastAutoImportHelp = Importa automaticamente a sessão conectada de www.raycast.com dos navegadores instalados.
ProviderRaycastManualCookieHelp = Cole um cabeçalho Cookie de www.raycast.com/settings.
ProviderRaycastCookiesDisabled = Os cookies do Raycast estão desativados.
CookieHelpCodexManual = Cole um cabeçalho Cookie de uma solicitação para chatgpt.com.
Expand Down
2 changes: 1 addition & 1 deletion rust/src/locale/ru-RU.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,7 @@ UsageSpendModelsPeriodCaption = Локальная история моделей
UsageSpendProjectsPeriodCaption = Расходы локальных проектов Codex по убыванию за период: { "{}" }
BurndownChartTitle = Остаток лимита
BurndownChartAriaLabel = График расхода оставшейся квоты
ProviderRaycastAutoImportHelp = Автоматически импортирует сеанс www.raycast.com, открытый в Chrome.
ProviderRaycastAutoImportHelp = Автоматически импортирует сеанс www.raycast.com, открытый в установленных браузерах.
ProviderRaycastManualCookieHelp = Вставьте заголовок Cookie со страницы www.raycast.com/settings.
ProviderRaycastCookiesDisabled = Файлы cookie Raycast отключены.
CookieHelpCodexManual = Вставьте заголовок Cookie из запроса к chatgpt.com.
Expand Down
2 changes: 1 addition & 1 deletion rust/src/locale/tr-TR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,7 @@ DeepSeekPricingEffective = Geçerli olduğu yerel saat:
DeepSeekPricingAdvice = Resmî tarife: yoğun saatler UTC 01:00-04:00 ve 06:00-10:00; yoğun olmayan saatlerde yarı fiyat.
MistralMonthlySpend = Aylık API harcaması
MistralMonthlySpendHelper = Kullandıkça öde Mistral hesapları için geçerli ayın API kullanımı.
ProviderRaycastAutoImportHelp = Chrome'da açık olan www.raycast.com oturumunu otomatik olarak içe aktarır.
ProviderRaycastAutoImportHelp = Yüklü tarayıcılardaki açık www.raycast.com oturumunu otomatik olarak içe aktarır.
ProviderRaycastManualCookieHelp = www.raycast.com/settings adresinden bir Cookie başlığı yapıştırın.
ProviderRaycastCookiesDisabled = Raycast çerezleri devre dışı.
CookieHelpCodexManual = chatgpt.com isteğinden bir Cookie başlığı yapıştırın.
Expand Down
2 changes: 1 addition & 1 deletion rust/src/locale/uk-UA.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1131,7 +1131,7 @@ UsageSpendModelsPeriodCaption = Локальна історія моделей
UsageSpendProjectsPeriodCaption = Витрати локальних проєктів Codex за спаданням за період: { "{}" }
BurndownChartTitle = Залишок ліміту
BurndownChartAriaLabel = Графік витрачання залишку квоти
ProviderRaycastAutoImportHelp = Автоматично імпортує сеанс www.raycast.com, у якому виконано вхід у Chrome.
ProviderRaycastAutoImportHelp = Автоматично імпортує сеанс www.raycast.com, у якому виконано вхід, з установлених браузерів.
ProviderRaycastManualCookieHelp = Вставте заголовок Cookie зі сторінки www.raycast.com/settings.
ProviderRaycastCookiesDisabled = Файли cookie Raycast вимкнено.
CookieHelpCodexManual = Вставте заголовок Cookie із запиту до chatgpt.com.
Expand Down
2 changes: 1 addition & 1 deletion rust/src/locale/zh-CN.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,7 @@ UsageSpendModelsPeriodCaption = 本地模型历史:{ "{}" }。
UsageSpendProjectsPeriodCaption = Codex 本地项目费用排名:{ "{}" }
BurndownChartTitle = 剩余用量
BurndownChartAriaLabel = 已记录的剩余配额消耗曲线
ProviderRaycastAutoImportHelp = 自动从 Chrome 导入已登录的 www.raycast.com 会话。
ProviderRaycastAutoImportHelp = 自动从已安装的浏览器导入已登录的 www.raycast.com 会话。
ProviderRaycastManualCookieHelp = 粘贴来自 www.raycast.com/settings 的 Cookie 标头。
ProviderRaycastCookiesDisabled = Raycast Cookie 已禁用。
CookieHelpCodexManual = 粘贴来自 chatgpt.com 请求的 Cookie 标头。
Expand Down
2 changes: 1 addition & 1 deletion rust/src/locale/zh-TW.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,7 @@ UsageSpendModelsPeriodCaption = 本機模型歷史:{ "{}" }。
UsageSpendProjectsPeriodCaption = Codex 本機專案費用排名:{ "{}" }
BurndownChartTitle = 剩餘用量
BurndownChartAriaLabel = 已記錄的剩餘配額消耗趨勢
ProviderRaycastAutoImportHelp = 自動從 Chrome 匯入已登入的 www.raycast.com 工作階段。
ProviderRaycastAutoImportHelp = 自動從已安裝的瀏覽器匯入已登入的 www.raycast.com 工作階段。
ProviderRaycastManualCookieHelp = 貼上來自 www.raycast.com/settings 的 Cookie 標頭。
ProviderRaycastCookiesDisabled = Raycast Cookie 已停用。
CookieHelpCodexManual = 貼上來自 chatgpt.com 請求的 Cookie 標頭。
Expand Down
12 changes: 12 additions & 0 deletions rust/src/providers/raycast/cookies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! profile is dropped before a request is built.

use crate::browser::cookies::Cookie;
use crate::browser::detection::BrowserType;

pub(super) const SESSION_COOKIE: &str = "__raycast_session";
const CSRF_COOKIE: &str = "csrf_token";
Expand Down Expand Up @@ -66,6 +67,17 @@ pub(super) fn browser_candidates(cookies: &[Cookie]) -> Vec<String> {
headers
}

/// Candidate headers for each scanned browser, in the scan's browser order and
/// labelled with the browser's display name. Browsers without a usable session
/// keep an empty list so callers can skip them.
pub(super) fn browser_groups(
scan: &[(BrowserType, Vec<Cookie>)],
) -> Vec<(&'static str, Vec<String>)> {
scan.iter()
.map(|(browser, cookies)| (browser.display_name(), browser_candidates(cookies)))
.collect()
}

/// Reduce a pasted header to the two forwarded cookies. Returns `None` without
/// a non-empty `__raycast_session`. The first occurrence of a name wins, which
/// matches the most specific cookie a browser sends.
Expand Down
54 changes: 42 additions & 12 deletions rust/src/providers/raycast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
//!
//! Raycast has no public credits API for a website session, so this provider
//! calls the unofficial `frontend_api/current_user/ai_credits` route with the
//! `__raycast_session` and `csrf_token` cookies only. Automatic import reads
//! Chrome alone to avoid unrelated browser prompts; a Manual Cookie header is
//! pinned and never falls back to a browser; Off makes no request. Cookie
//! material stays in memory for the current fetch and is never logged.
//! `__raycast_session` and `csrf_token` cookies only. Automatic import walks
//! every detected browser in the shared browser order and stops at the first
//! accepted session; a Manual Cookie header is pinned and never falls back to a
//! browser; Off makes no request. Cookie material stays in memory for the
//! current fetch and is never logged.

mod cookies;
mod parse;
Expand All @@ -16,7 +17,6 @@ use async_trait::async_trait;
use reqwest::{Client, StatusCode};
use std::time::Duration;

use crate::browser::detection::BrowserType;
use crate::core::{
FetchContext, ManualEmptyCookiePolicy, Provider, ProviderError, ProviderFetchResult,
ProviderId, ProviderMetadata, ProviderStateKind, SourceMode,
Expand All @@ -30,7 +30,6 @@ const USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleW
const MAX_RESPONSE_BYTES: usize = 1024 * 1024;
const MIN_TIMEOUT_SECONDS: u64 = 1;
const MAX_TIMEOUT_SECONDS: u64 = 30;
const BROWSER: BrowserType = BrowserType::Chrome;

const MISSING_SESSION: &str = "No Raycast session cookies found. Sign in at www.raycast.com/settings or paste a Cookie header.";
const SESSION_EXPIRED: &str = "Raycast website session expired. Sign in at www.raycast.com/settings or paste a fresh Cookie header.";
Expand Down Expand Up @@ -141,18 +140,49 @@ impl RaycastProvider {
if ctx.manual_cookie_missing {
return Err(ProviderError::Other(MISSING_SESSION.to_string()));
}
let extracted = tokio::task::spawn_blocking(|| {
crate::providers::browser_cookies_from_browser(BROWSER, cookies::EXTRACT_DOMAIN)
let scanned = tokio::task::spawn_blocking(|| {
crate::providers::browser_cookie_candidates_for_domain(cookies::EXTRACT_DOMAIN)
})
.await
.map_err(|_| ProviderError::Other("Raycast cookie import was interrupted.".into()))?;
let headers = match extracted {
Ok(found) => cookies::browser_candidates(&found),
let groups = match scanned {
Ok(scan) => cookies::browser_groups(&scan),
Err(ProviderError::NoCookies) => Vec::new(),
Err(error) => return Err(error),
};
self.fetch_candidates(&headers, BROWSER.display_name(), timeout)
.await
self.fetch_browser_groups(&groups, timeout).await
}

/// Try each browser's candidate headers in the shared browser order. A
/// browser whose sessions are all rejected yields to the next browser; a
/// browser with no session is skipped without a request; any other outcome
/// (success or a non-authentication failure) ends the search.
async fn fetch_browser_groups(
&self,
groups: &[(&str, Vec<String>)],
timeout: Duration,
) -> Result<ProviderFetchResult, ProviderError> {
let mut rejected = false;
for (source_label, headers) in groups {
if headers.is_empty() {
continue;
}
match self.fetch_candidates(headers, source_label, timeout).await {
Ok(result) => return Ok(result),
Err(ProviderError::Other(message)) if message == SESSION_EXPIRED => {
rejected = true;
}
Err(error) => return Err(error),
}
}
Err(ProviderError::Other(
if rejected {
SESSION_EXPIRED
} else {
MISSING_SESSION
}
.to_string(),
))
}
}

Expand Down
Loading