clauderipple 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.es.md +22 -11
  3. package/README.ja.md +14 -5
  4. package/README.ko.md +19 -8
  5. package/README.md +22 -10
  6. package/README.pt-BR.md +22 -11
  7. package/README.zh-CN.md +14 -5
  8. package/dist/app/package.json +1 -1
  9. package/dist/cli/src/index.js +2 -1
  10. package/dist/mod/hooks/i18n.ts +36 -12
  11. package/dist/mod/hooks/judge.ts +17 -1
  12. package/dist/mod/hooks/register.tsx +39 -9
  13. package/dist/mod/hooks/session.ts +11 -3
  14. package/dist/mod/hooks/ui/alerts.tsx +21 -5
  15. package/dist/mod/hooks/ui/band.tsx +1 -1
  16. package/dist/mod/hooks/ui/view.ts +4 -1
  17. package/dist/mod/tests/register.test.tsx +53 -0
  18. package/dist/mod/tests/ui.test.tsx +10 -6
  19. package/dist/mod/types/index.d.ts +4 -1
  20. package/dist/router/src/admin.js +72 -2
  21. package/dist/router/src/agents.js +4 -0
  22. package/dist/router/src/config.js +24 -0
  23. package/dist/router/src/ingress/server.js +2 -2
  24. package/dist/router/src/mod-settings.js +2 -1
  25. package/dist/router/src/providers/chatgpt/index.js +19 -6
  26. package/dist/router/src/providers/chatgpt/translate.js +3 -0
  27. package/dist/router/src/providers/google/index.js +4 -3
  28. package/dist/router/src/providers/grok/auth.js +289 -0
  29. package/dist/router/src/providers/grok/catalog.js +52 -0
  30. package/dist/router/src/providers/grok/index.js +94 -0
  31. package/dist/router/src/providers/openai/index.js +71 -17
  32. package/dist/router/src/providers/openai/translate.js +11 -1
  33. package/dist/router/src/proxy.js +91 -21
  34. package/dist/router/src/redact.js +7 -2
  35. package/dist/router/src/requestlog.js +78 -20
  36. package/dist/router/src/version.js +1 -1
  37. package/dist/ui/app.js +105 -0
  38. package/dist/ui/i18n.js +6 -6
  39. package/docs/ARCHITECTURE.md +91 -0
  40. package/package.json +1 -1
package/dist/ui/app.js CHANGED
@@ -105,6 +105,8 @@ function fallbackEffortLevels(provider, model) {
105
105
  const explicit = modelEffortLevels(provider, model);
106
106
  if (explicit !== undefined) return explicit;
107
107
  if (provider.type === "chatgpt") return CHATGPT_ULTRA_MODELS.has(model) ? [...CHATGPT_EFFORT_LEVELS, "ultra"] : [...CHATGPT_EFFORT_LEVELS];
108
+ // What the router's grok adapter sends a model the proxy's listing has not described.
109
+ if (provider.type === "grok") return ["low", "medium", "high", "xhigh"];
108
110
  const preset = provider.preset && presetById(provider.preset);
109
111
  if (provider.type === "openai-compatible") return provider.caps && provider.caps.reasoning === "effort" && Array.isArray(provider.caps.effortLevels) ? provider.caps.effortLevels : [];
110
112
  return provider.caps && Array.isArray(provider.caps.effortLevels) ? provider.caps.effortLevels : (preset && preset.effortLevels) || [];
@@ -946,6 +948,8 @@ async function probeProvider(name, provider, onComplete) {
946
948
  ? { type: "anthropic", auth: provider.auth, ...(provider.auth === "api-key" && provider.apiKey ? { apiKey: provider.apiKey } : {}) }
947
949
  : provider.type === "google"
948
950
  ? { type: "google", auth: provider.auth, ...(provider.apiKey ? { apiKey: provider.apiKey } : {}), ...(provider.url ? { url: provider.url } : {}) }
951
+ : provider.type === "grok"
952
+ ? { type: "grok", ...Object.fromEntries(["home", "cli", "url", "clientVersion"].filter((key) => provider[key]).map((key) => [key, provider[key]])) }
949
953
  : {
950
954
  type: provider.type,
951
955
  url: provider.url,
@@ -1057,6 +1061,7 @@ function providerKind(name, provider) {
1057
1061
  const preset = provider.preset && presetById(provider.preset);
1058
1062
  if (provider.type === "chatgpt") return t("providers.chatgpt");
1059
1063
  if (provider.type === "google") return `${t("providers.google")} · ${provider.auth === "antigravity" ? t("providers.googleAntigravity") : t("providers.apiKey")}`;
1064
+ if (provider.type === "grok") return t("providers.grok");
1060
1065
  if (provider.type === "anthropic") return provider.auth === "claude-code"
1061
1066
  ? `${t("providers.anthropic")} · ${provider.accountPool ? t("providers.rotationOn") : t("providers.rotationOff")}`
1062
1067
  : `${t("providers.anthropic")} · ${t("providers.apiKey")}`;
@@ -1763,6 +1768,7 @@ function openProviderChooser() {
1763
1768
  tile(t("providers.anthropic"), t("providers.anthropicHelp"), { kind: "anthropic" }),
1764
1769
  tile(t("providers.chatgpt"), t("providers.chatgptHelp"), { kind: "chatgpt" }),
1765
1770
  tile(t("providers.google"), t("providers.googleHelp"), { kind: "google" }),
1771
+ tile(t("providers.grok"), t("providers.grokHelp"), { kind: "grok" }),
1766
1772
  ], null, true);
1767
1773
  const native = presets.filter((preset) => (preset.kind || "anthropic-compatible") === "anthropic-compatible");
1768
1774
  group(t("providers.anthropicGroup"), native.map((preset) => tile(preset.name, null, { preset })), t("providers.presetHelp"));
@@ -1947,6 +1953,104 @@ function openAnthropicProviderForm(options) {
1947
1953
  showModal(form, () => { formActive = false; });
1948
1954
  if (auth.value === "claude-code") void runProbe();
1949
1955
  }
1956
+ /**
1957
+ * What a Grok form's check leaves ticked. A handful of models, so a new provider's first listing
1958
+ * starts with all of them ticked. Every later check — and every check of a saved provider — keeps
1959
+ * what is ticked at that moment, so a model the user unticked stays unticked.
1960
+ */
1961
+ function grokTickedAfterCheck(firstListing, tickedIds, listedIds) {
1962
+ return new Set(firstListing ? listedIds : tickedIds);
1963
+ }
1964
+
1965
+ /**
1966
+ * A Grok subscription: nothing to type. The router reads the Grok CLI's own session, so the form
1967
+ * checks that it is there, lists what the proxy offers and saves the ticked models. The check runs
1968
+ * as soon as the form opens, since there is no key to wait for.
1969
+ */
1970
+ function openGrokProviderForm(options) {
1971
+ let formActive = true;
1972
+ const existing = options.provider;
1973
+ const nameInput = el("input", { value: options.name || t("providers.grok"), maxlength: "60" });
1974
+ const result = el("div", { class: "probe-result" });
1975
+ const probeButton = el("button", { class: "btn secondary", type: "button", text: t("providers.check") });
1976
+ let foundModels = modelsOf(existing);
1977
+ const currentChecked = new Set(foundModels.map((model) => model.id));
1978
+ let listed = false;
1979
+ const modelArea = el("div", { class: "form-field" });
1980
+ function renderModels(checked, help) {
1981
+ modelArea.replaceChildren(el("span", { text: t("providers.models") }), hint(help || t("providers.modelsHelp")), modelChecklist(foundModels, checked, { allowCustom: true }));
1982
+ }
1983
+ renderModels(currentChecked);
1984
+ // Fields this form has no control for (a home, a binary, a pinned version) go to the probe as saved.
1985
+ const probeBody = () => ({ type: "grok", ...Object.fromEntries(["home", "cli", "url", "clientVersion"].filter((key) => existing && existing[key]).map((key) => [key, existing[key]])) });
1986
+ async function runProbe() {
1987
+ probeButton.disabled = true;
1988
+ result.textContent = t("providers.checking");
1989
+ try {
1990
+ const response = await api("/api/providers/probe", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(probeBody()) });
1991
+ if (!formActive) return;
1992
+ const failed = response.auth === "missing" || response.auth === "bad-key" ? t("providers.grokSignInFirst") : t("providers.probeFailed");
1993
+ result.replaceChildren(...[
1994
+ el("span", { class: response.ok ? "ok-text" : "bad-text", text: response.ok ? t("providers.probeOk") : failed }),
1995
+ response.error ? el("div", { class: "small", text: response.error }) : null,
1996
+ ].filter(Boolean));
1997
+ if (Array.isArray(response.models) && response.models.length) {
1998
+ const ticked = modelArea.querySelector(".model-picker").selected();
1999
+ const keep = grokTickedAfterCheck(!existing && !listed, ticked.map((model) => model.id), response.models.map((model) => model.id));
2000
+ listed = true;
2001
+ foundModels = [...ticked.filter((model) => !response.models.some((m) => m.id === model.id)), ...response.models];
2002
+ renderModels(keep);
2003
+ }
2004
+ } catch (error) { if (formActive) result.replaceChildren(el("span", { class: "bad-text", text: t("providers.probeFailed") }), el("div", { class: "small", text: error.message })); }
2005
+ finally { if (formActive) probeButton.disabled = false; }
2006
+ }
2007
+ probeButton.addEventListener("click", () => void runProbe());
2008
+ const advanced = el("details", { class: "details" }, [el("summary", { text: t("common.advanced") })]);
2009
+ const identity = el("input", { type: "checkbox", checked: !(existing && existing.identity === false) });
2010
+ const append = el("textarea", { rows: "2", value: (existing && existing.instructionsAppend) || "" });
2011
+ advanced.appendChild(el("div", { class: "advanced-content" }, [
2012
+ el("div", { class: "form-field" }, [el("label", { class: "check" }, [identity, el("span", { text: t("providers.identity") })]), el("small", { text: t("providers.identityHelp") })]),
2013
+ inputRow(t("providers.append"), append, t("providers.appendHelp")),
2014
+ ]));
2015
+ const pickerInput = el("input", { type: "checkbox", checked: existing ? modelsOf(existing).some((model) => ((currentConfig.cli && currentConfig.cli.extraModels) || []).some((extra) => extra.model === model.id)) : true });
2016
+ const form = el("div", { class: "provider-form" }, [
2017
+ el("h1", { id: "modal-title", text: existing ? t("providers.edit") : t("providers.addTitle") }),
2018
+ inputRow(t("providers.name"), nameInput, t("providers.nameHelp")),
2019
+ el("div", { class: "form-field" }, [el("span", { text: t("providers.credentials") }), hint(t("providers.grokFormHelp")), el("small", { class: "warn-text", text: t("providers.grokWarningShort") })]),
2020
+ probeButton, result, modelArea,
2021
+ el("label", { class: "check picker-check" }, [pickerInput, el("span", { text: t("providers.showInPicker") })]),
2022
+ pickerModeOn() ? null : hint(t("providers.pickerOffHint")),
2023
+ advanced,
2024
+ ]);
2025
+ const saveButton = el("button", { class: "btn", type: "button", "data-default-action": "", text: existing ? t("common.save") : t("providers.add") });
2026
+ saveButton.addEventListener("click", async () => {
2027
+ const typedName = nameInput.value.trim();
2028
+ if (!typedName) { toast(t("providers.nameRequired"), true); return; }
2029
+ const next = clone(await latestConfig());
2030
+ const providerName = existing ? options.name : uniqueName(typedName, next.providers);
2031
+ const saved = existing && (next.providers[options.name] || existing);
2032
+ const checkedModels = withSavedModelFields(form.querySelector(".model-picker").selected(), saved);
2033
+ const formKeys = new Set(["type", "identity", "instructionsAppend", "models"]);
2034
+ const kept = saved ? Object.fromEntries(Object.entries(saved).filter(([key]) => !formKeys.has(key))) : {};
2035
+ next.providers[providerName] = {
2036
+ ...kept,
2037
+ type: "grok",
2038
+ identity: identity.checked,
2039
+ ...(append.value.trim() ? { instructionsAppend: append.value.trim() } : {}),
2040
+ models: checkedModels,
2041
+ };
2042
+ const existingSelections = pickerSelectionsExcept(next, providerName);
2043
+ applyPickerSelections(next, pickerInput.checked ? [...existingSelections, ...checkedModels.map((model) => ({ ...model, provider: providerName }))] : existingSelections);
2044
+ saveButton.disabled = true;
2045
+ try { await configRequest(next); currentConfig = next; selectedProviderName = providerName; providerDetailTab = "overview"; slotsLoaded = false; clientsLoaded = false; providersLoaded = false; closeModal(); await loadProviders(); toast(t("common.saved")); await offerPickerOn(pickerInput.checked && checkedModels.length > 0); }
2046
+ catch (error) { toast(t("common.saveFailed"), true, error.message); }
2047
+ finally { saveButton.disabled = false; }
2048
+ });
2049
+ form.appendChild(el("div", { class: "actions end" }, [el("button", { class: "btn secondary", type: "button", text: t("common.cancel"), onclick: closeModal }), saveButton]));
2050
+ showModal(form, () => { formActive = false; });
2051
+ void runProbe();
2052
+ }
2053
+
1950
2054
  function openGoogleProviderForm(options) {
1951
2055
  let formActive = true;
1952
2056
  const existing = options.provider;
@@ -2066,6 +2170,7 @@ function openProviderForm(options) {
2066
2170
  const displayName = options.name || (preset && preset.name) || (isChatgpt ? t("providers.chatgpt") : isAnthropic ? t("providers.anthropic") : t("providers.customName"));
2067
2171
  if (isAnthropic) { openAnthropicProviderForm(options); return; }
2068
2172
  if (options.kind === "google" || (existing && existing.type === "google")) { openGoogleProviderForm(options); return; }
2173
+ if (options.kind === "grok" || (existing && existing.type === "grok")) { openGrokProviderForm(options); return; }
2069
2174
  const nameInput = el("input", { value: displayName, maxlength: "60" });
2070
2175
  const keyInput = el("input", { type: "password", autocomplete: "off", placeholder: t("providers.keyPlaceholder") });
2071
2176
  const showKey = el("button", { class: "eye-button", type: "button", text: t("common.show") });
package/dist/ui/i18n.js CHANGED
@@ -41,7 +41,7 @@ const I18N = {
41
41
  "providers.googleWarningShort": "Google 이용약관에서 해당 사용을 금지하고 있으며 계정 정지 사례가 있습니다. 주 계정 대신 보조 계정 사용을 권장합니다.",
42
42
  "providers.googleOpenManually": "브라우저가 열리지 않았습니다. 아래 주소를 직접 여세요.",
43
43
  "providers.googleAdded": "Google 계정을 추가했습니다.",
44
- "providers.googleSignInFirst": "로그인된 Google 계정이 없습니다. 먼저 로그인해 주세요.",
44
+ "providers.googleSignInFirst": "로그인된 Google 계정이 없습니다. 먼저 로그인해 주세요.", "providers.grok": "Grok 구독", "providers.grokHelp": "Grok CLI가 포함된 Grok 요금제로 Grok 모델을 씁니다. `grok login`으로 로그인만 하면 되고 API 키는 필요 없습니다.", "providers.grokFormHelp": "ClaudeRipple은 Grok CLI의 로그인 세션(~/.grok/auth.json)을 읽기만 하고, 만료가 가까우면 CLI에 갱신을 맡깁니다. 터미널에서 `grok login`을 한 번 실행해 두세요.", "providers.grokWarningShort": "Grok CLI 전용 채널을 씁니다. xAI가 API로 공개한 경로가 아니라서 CLI 업데이트나 xAI 쪽 변경으로 예고 없이 막힐 수 있습니다.", "providers.grokSignInFirst": "Grok CLI 로그인 세션이 없습니다. 먼저 터미널에서 `grok login`을 실행하세요.",
45
45
  "providers.googleRemoveConfirm": "Google 계정 {name}을(를) ClaudeRipple에서 삭제하시겠습니까? 실제 Google 계정은 삭제되지 않습니다.",
46
46
  "providers.googleProject": "프로젝트 {id}",
47
47
  "providers.googleOwnHelp": "ClaudeRipple이 로컬 사용자 전용 파일에 로그인 정보를 안전하게 저장하고 자동으로 갱신합니다.",
@@ -247,7 +247,7 @@ const I18N = {
247
247
  "providers.googleWarningShort": "Google's terms prohibit this use and accounts have been suspended for it. A secondary account is recommended.",
248
248
  "providers.googleOpenManually": "The browser did not open. Open this address yourself.",
249
249
  "providers.googleAdded": "Google account added.",
250
- "providers.googleSignInFirst": "No Google account is signed in. Sign in first.",
250
+ "providers.googleSignInFirst": "No Google account is signed in. Sign in first.", "providers.grok": "Grok subscription", "providers.grokHelp": "Use Grok models through a Grok plan that includes the Grok CLI. Sign in with `grok login`, no API key.", "providers.grokFormHelp": "ClaudeRipple only reads the Grok CLI's session (~/.grok/auth.json) and asks the CLI to refresh it when it runs low. Run `grok login` once in a terminal.", "providers.grokWarningShort": "This uses the Grok CLI's own channel, which xAI does not publish as an API; a CLI update or a change on xAI's side can break it without warning.", "providers.grokSignInFirst": "No Grok CLI session. Run `grok login` in a terminal first.",
251
251
  "providers.googleRemoveConfirm": "Remove {name} from ClaudeRipple? The Google account itself is untouched.",
252
252
  "providers.googleProject": "project {id}",
253
253
  "providers.googleOwnHelp": "ClaudeRipple keeps this sign-in in a file only your user can read and refreshes it automatically.",
@@ -737,7 +737,7 @@ const I18N = {
737
737
  "providers.googleWarningShort": "Google 的条款禁止这种用法,已有账号因此被停用。建议使用备用账号。",
738
738
  "providers.googleOpenManually": "浏览器未能打开,请手动打开此地址。",
739
739
  "providers.googleAdded": "已添加 Google 账号。",
740
- "providers.googleSignInFirst": "尚未登录任何 Google 账号,请先登录。",
740
+ "providers.googleSignInFirst": "尚未登录任何 Google 账号,请先登录。", "providers.grok": "Grok 订阅", "providers.grokHelp": "通过包含 Grok CLI 的 Grok 套餐使用 Grok 模型。用 `grok login` 登录即可,无需 API 密钥。", "providers.grokFormHelp": "ClaudeRipple 只读取 Grok CLI 的登录会话(~/.grok/auth.json),快过期时请 CLI 刷新。请先在终端运行一次 `grok login`。", "providers.grokWarningShort": "这里使用的是 Grok CLI 自己的通道,xAI 并未将其作为 API 公开,CLI 更新或 xAI 一侧的变更都可能使其在没有预告的情况下失效。", "providers.grokSignInFirst": "没有 Grok CLI 登录会话。请先在终端运行 `grok login`。",
741
741
  "providers.googleRemoveConfirm": "从 ClaudeRipple 中移除 {name}?Google 账号本身不受影响。",
742
742
  "providers.googleProject": "项目 {id}",
743
743
  "providers.googleOwnHelp": "ClaudeRipple 将此登录保存在只有你的系统用户账号能读取的文件中,并自动刷新。",
@@ -1227,7 +1227,7 @@ const I18N = {
1227
1227
  "providers.googleWarningShort": "Googleの規約はこの使い方を禁止しており、実際にアカウントが停止された例があります。サブアカウントの使用をおすすめします。",
1228
1228
  "providers.googleOpenManually": "ブラウザが開きませんでした。このアドレスを手動で開いてください。",
1229
1229
  "providers.googleAdded": "Googleアカウントを追加しました",
1230
- "providers.googleSignInFirst": "ログイン済みのGoogleアカウントがありません。先にログインしてください。",
1230
+ "providers.googleSignInFirst": "ログイン済みのGoogleアカウントがありません。先にログインしてください。", "providers.grok": "Grokサブスクリプション", "providers.grokHelp": "Grok CLIを含むGrokプランでGrokモデルを使います。`grok login`でログインするだけで、APIキーは不要です。", "providers.grokFormHelp": "ClaudeRippleはGrok CLIのログインセッション(~/.grok/auth.json)を読むだけで、期限が近づくと更新をCLIに任せます。ターミナルで一度`grok login`を実行してください。", "providers.grokWarningShort": "Grok CLI専用の経路を使います。xAIがAPIとして公開しているものではないため、CLIの更新やxAI側の変更で予告なく使えなくなる可能性があります。", "providers.grokSignInFirst": "Grok CLIのログインセッションがありません。先にターミナルで`grok login`を実行してください。",
1231
1231
  "providers.googleRemoveConfirm": "{name} をClaudeRippleから削除しますか?Googleアカウント自体には影響しません。",
1232
1232
  "providers.googleProject": "プロジェクト {id}",
1233
1233
  "providers.googleOwnHelp": "ClaudeRippleがこのログインを自分のユーザーだけが読めるファイルに保存し、自動で更新します。",
@@ -1717,7 +1717,7 @@ const I18N = {
1717
1717
  "providers.googleWarningShort": "Los términos de Google prohíben este uso y se han suspendido cuentas por ello. Se recomienda una cuenta secundaria.",
1718
1718
  "providers.googleOpenManually": "El navegador no se abrió. Abre esta dirección manualmente.",
1719
1719
  "providers.googleAdded": "Cuenta de Google añadida.",
1720
- "providers.googleSignInFirst": "No hay ninguna cuenta de Google con sesión iniciada. Inicia sesión primero.",
1720
+ "providers.googleSignInFirst": "No hay ninguna cuenta de Google con sesión iniciada. Inicia sesión primero.", "providers.grok": "Suscripción de Grok", "providers.grokHelp": "Usa modelos Grok con un plan de Grok que incluya la CLI de Grok. Solo inicia sesión con `grok login`, sin clave de API.", "providers.grokFormHelp": "ClaudeRipple solo lee la sesión de la CLI de Grok (~/.grok/auth.json) y le pide a la CLI que la renueve cuando está por vencer. Ejecuta `grok login` una vez en una terminal.", "providers.grokWarningShort": "Usa el canal propio de la CLI de Grok, que xAI no publica como API; una actualización de la CLI o un cambio por parte de xAI puede romperlo sin aviso.", "providers.grokSignInFirst": "No hay sesión de la CLI de Grok. Ejecuta `grok login` en una terminal primero.",
1721
1721
  "providers.googleRemoveConfirm": "¿Quitar {name} de ClaudeRipple? La cuenta de Google en sí no se modifica.",
1722
1722
  "providers.googleProject": "proyecto {id}",
1723
1723
  "providers.googleOwnHelp": "ClaudeRipple guarda este inicio de sesión en un archivo que solo tu usuario puede leer y lo renueva automáticamente.",
@@ -2207,7 +2207,7 @@ const I18N = {
2207
2207
  "providers.googleWarningShort": "Os termos do Google proíbem este uso, e contas já foram suspensas por isso. Recomenda-se usar uma conta secundária.",
2208
2208
  "providers.googleOpenManually": "O navegador não abriu. Abra este endereço manualmente.",
2209
2209
  "providers.googleAdded": "Conta do Google adicionada.",
2210
- "providers.googleSignInFirst": "Nenhuma conta do Google conectada. Entre primeiro.",
2210
+ "providers.googleSignInFirst": "Nenhuma conta do Google conectada. Entre primeiro.", "providers.grok": "Assinatura do Grok", "providers.grokHelp": "Use modelos Grok com um plano do Grok que inclua a CLI do Grok. Só entre com `grok login`, sem chave de API.", "providers.grokFormHelp": "O ClaudeRipple só lê a sessão da CLI do Grok (~/.grok/auth.json) e pede à CLI que a renove quando estiver perto de expirar. Rode `grok login` uma vez em um terminal.", "providers.grokWarningShort": "Usa o canal próprio da CLI do Grok, que a xAI não publica como API; uma atualização da CLI ou uma mudança do lado da xAI pode quebrá-lo sem aviso.", "providers.grokSignInFirst": "Nenhuma sessão da CLI do Grok. Rode `grok login` em um terminal primeiro.",
2211
2211
  "providers.googleRemoveConfirm": "Remover {name} do ClaudeRipple? A conta do Google em si não é afetada.",
2212
2212
  "providers.googleProject": "projeto {id}",
2213
2213
  "providers.googleOwnHelp": "O ClaudeRipple guarda este login em um arquivo que só o seu usuário pode ler e o renova automaticamente.",
@@ -759,6 +759,19 @@ must preserve **Data: Artificial Analysis (artificialanalysis.ai)** attribution.
759
759
 
760
760
  ### 4a. Claude Code request shapes a translator must handle (measured 2026-09-13, CLI 2.1.266)
761
761
 
762
+ **Request-log metadata (`requests.jsonl`, 2026-10-14).** These fields are optional;
763
+ old records and `RequestLog.summary()` remain compatible. No prompt, auth or reasoning
764
+ text is added. Source: `requestlog.ts`, `proxy.ts`, provider adapter outcomes.
765
+
766
+ - `usage.cacheWrite`: native/Anthropic numeric cache creation tokens, including explicit 0; absent/non-number is omitted, as is a translated adapter's synthetic 0.
767
+ - `upstreamRequestId`: the final forwarded native Anthropic response's `request-id` header (passthrough or account pool).
768
+ - `messageId`: the Anthropic message id delivered by a translated adapter or forwarded compatible response; not a generated id from a non-streaming error.
769
+ - `upstreamCalled`: `false` only for a known pre-model-API exit; omitted when a search backend was attempted or the call state is unknown.
770
+ - `refusal`: `thread-continue`, `drain`, `unroutable`, `no-account`, `unknown-provider` or `web-search-unsupported`, from the actual exit branch.
771
+ - `purpose`: `auto-mode-classifier` from the security-monitor request shape regardless of routing settings, or `web-search` for router-served searches.
772
+ - `requestedTier`: the service tier actually sent upstream, including ChatGPT's `speed: "fast"` → `priority`; omitted when no tier was sent.
773
+ - `servedTier`: the response-reported tier (Responses/Chat `service_tier` or Anthropic `usage.service_tier`); never inferred from the request.
774
+
762
775
  - **Server-side threads ("tether").** The first request of a session carries
763
776
  `thread: {type:"create"}` and the full history; later turns carry
764
777
  `thread: {type:"continue", previous_message_id: <our last assistant msg id>}` and
@@ -1243,6 +1256,84 @@ subscription exists in this environment. Files are under `src/`:
1243
1256
  `PATCH/DELETE /api/google-accounts/:id`, and `POST /api/providers/probe` with
1244
1257
  `{type:"google", auth:"antigravity"}`. CLI: `clauderipple google-login` / `google-logout`.
1245
1258
 
1259
+ ### 4e. `grok` providers (implemented 2026-10-08)
1260
+
1261
+ Grok on a Grok subscription, through the chat proxy the Grok CLI itself talks to, signed in
1262
+ with the CLI's own session. A translated provider like §4c: the adapter
1263
+ (`packages/router/src/providers/grok/`) reuses `OpenAiCompatibleAdapter` on the Chat wire
1264
+ and adds only what an API key would not need — a credential that rotates under the router,
1265
+ and the headers the proxy routes on. Measured 2026-10-08 against CLI 1.0.46 on a SuperGrok
1266
+ plan unless marked otherwise.
1267
+
1268
+ - **Endpoint and headers.** `POST https://cli-chat-proxy.grok.com/v1/chat/completions`,
1269
+ `GET …/v1/models`. The CLI's own README ("Using auth.json for API Access") documents calling
1270
+ it directly with `Authorization: Bearer <auth.json key>`, `X-XAI-Token-Auth: xai-grok-cli`
1271
+ and `x-grok-model-override: <model>` (the proxy routes on the header, not the body). It does
1272
+ not mention that the proxy also refuses a request without `x-grok-client-version`:
1273
+ `426 {"error":"Your Grok CLI version (none) is outdated. Please update to version 1.0.13 or
1274
+ later …"}`. The router sends `clientVersion` from the config when set, else the installed
1275
+ CLI's version (`grok --version`, read again when the binary changes), else 1.0.46. The header names were
1276
+ read from the CLI binary's strings; nothing else from it is used. This channel is the CLI's,
1277
+ not a published API, so the probe returns a facts-only `warning` (like Antigravity's) and the
1278
+ form says so.
1279
+ - **Session.** `<grok home>/auth.json` (`$GROK_HOME`, else `~/.grok`), written by `grok login`:
1280
+ one entry per issuer keyed `https://auth.x.ai::<id>`, each with `key`, `expires_at`,
1281
+ `refresh_token`. The README's own `jq` example reads a different key
1282
+ (`https://accounts.x.ai/sign-in`), so the entry that expires last is taken and no key name
1283
+ is assumed. A session token lives **six hours** (`expires_at` − `create_time`; the README
1284
+ says 7 days); a running CLI refreshes it about five minutes before expiry under its own lock
1285
+ (`auth lock: attempting acquire (timeout=25000ms)`, `oidc try_refresh_pure`). Whether the
1286
+ refresh token rotates was not measured. So, as with `borrow-codex`, the router never
1287
+ refreshes the grant itself — if it does rotate, racing the CLI could sign the CLI out.
1288
+ - **Refresh by asking the CLI.** With no CLI running, the file just goes stale. When the token
1289
+ has under 10 minutes left the router runs `grok models` with
1290
+ `GROK_AUTH_EARLY_INVALIDATION_SECS=600`: the CLI then sees the token as expiring, refreshes
1291
+ it under its lock and rewrites the file (measured: expiry moved six hours, file rewritten).
1292
+ One run serves every turn waiting on it; 30s bound, since the CLI's lock wait is 25s. A token
1293
+ still alive is used even when that run fails, and a run that left it short is not repeated
1294
+ for a minute, so a broken IdP does not add 30s to every turn. Under launchd's environment
1295
+ (`env -i PATH=/usr/bin:/bin:/usr/sbin:/sbin HOME=…`) `grok models` exits 0 and leaves no
1296
+ process behind. The router runs under launchd with
1297
+ `PATH=/usr/bin:/bin:/usr/sbin:/sbin`, so the binary is looked up in `<home>/bin/grok` first,
1298
+ then PATH, then `~/.local/bin/grok`; `HOME` is set there, which the CLI needs to find its
1299
+ session.
1300
+ - **A 401 is asked once more.** The token read a moment ago can be the one the CLI just
1301
+ replaced. On a 401 before any byte is written the adapter re-reads the file; if it still
1302
+ holds the refused token the CLI refresh is forced (`GROK_AUTH_EARLY_INVALIDATION_SECS=86400`,
1303
+ the value measured to make the CLI refresh, at most once a minute — a proxy that refuses a fresh token is refusing the
1304
+ account), and the turn is asked again only when a different token exists. Otherwise the
1305
+ user reads `authentication_error "Grok: … — sign in with grok login, …"`. An unknown token is
1306
+ `401 {"error":"Invalid or expired credentials (auth_kind=bearer, x_xai_token_auth=xai-grok-cli,
1307
+ upstream=PermissionDenied, reason=no auth context)"}` — a bare string under `error`, which
1308
+ `vendorMessage` now reads. `fetchWithRetry` still never retries a 401 on its own.
1309
+ - **Models and effort.** `/v1/models` lists grok-4.7, grok-4.7-build-fast, grok-4.6 and
1310
+ grok-4.5, each with `context_window` 256000, `api_backend: "responses"` and
1311
+ `reasoning_efforts` listed high to low: low..xhigh, except grok-4.5 low..high. The probe
1312
+ saves that ladder per model. On Chat, every model accepted low/medium/high/xhigh (grok-4.5's
1313
+ `xhigh` too — a 200 is only "not refused", so the listing wins) and refused the string
1314
+ `"none"` and `max` with `400 {"code":"invalid-argument"}`; omitting the field is accepted and
1315
+ the model's default (`high`) applies. A model the listing did not describe gets low..xhigh.
1316
+ - **Chat, not Responses.** Both wires answer. On Chat the reasoning arrives as text the mapper
1317
+ turns into thinking blocks; the Responses mapper has no reasoning handling (§4c), so a Grok
1318
+ turn there showed none. Tools stream as ordinary `tool_calls` deltas and `tool` messages are
1319
+ accepted back; the usage chunk carries `prompt_tokens_details.cached_tokens`.
1320
+ - **Cache.** No conversation header: a repeated 25.5k-token prefix was read from cache on the
1321
+ second call without one (1,152 cached on the first, 25,472 of 25,573 on the second), while
1322
+ the same pair with `x-grok-conv-id` on Chat missed both times. One
1323
+ Claude Code session through a dev router (CLI 2.1.291, grok-4.7, four turns with Read, Write
1324
+ and Bash) read 21,888 / 21,995, 21,888 / 22,210 and 22,144 / 22,257 from cache after its
1325
+ cold first turn; a second session's second turn 21,760 / 22,023. A session's first turn is
1326
+ cold. Watch the daily figure in `requests.jsonl` as for the other translated providers.
1327
+ - **Probe.** `POST /api/providers/probe` with `{type:"grok", home?, cli?, url?,
1328
+ clientVersion?}` checks the session through the same `GrokAuth` the adapter uses (so a near-
1329
+ expired one is refreshed here too) and asks `/models` with the headers live traffic sends.
1330
+ `auth: "missing"` is no session or an expired one the CLI could not refresh; `"bad-key"` is
1331
+ the proxy refusing it. The measured list above is offered when the listing cannot be read.
1332
+ - Routing, agent files, picker entries and the refusal of `web_search` side requests are the
1333
+ same as for any translated provider. A grok model may not carry `wire`, `url` or
1334
+ `authHeader` (validation refuses it): `providerFor` would turn it into an openai-compatible
1335
+ provider without the session headers. Not available through the OpenAI ingress.
1336
+
1246
1337
  ## 5. Failure modes that must not exist in the product (all observed)
1247
1338
 
1248
1339
  | Observed | Product requirement |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clauderipple",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Use GPT (any ChatGPT plan with Codex), DeepSeek and 400+ other models in the Claude Desktop Code tab and Claude Code while staying signed in to your Claude subscription — no third-party gateway mode. Several ChatGPT accounts that take over when one runs out, in Codex too.",
5
5
  "license": "GPL-3.0-only",
6
6
  "workspaces": [