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.
- package/CHANGELOG.md +49 -0
- package/README.es.md +22 -11
- package/README.ja.md +14 -5
- package/README.ko.md +19 -8
- package/README.md +22 -10
- package/README.pt-BR.md +22 -11
- package/README.zh-CN.md +14 -5
- package/dist/app/package.json +1 -1
- package/dist/cli/src/index.js +2 -1
- package/dist/mod/hooks/i18n.ts +36 -12
- package/dist/mod/hooks/judge.ts +17 -1
- package/dist/mod/hooks/register.tsx +39 -9
- package/dist/mod/hooks/session.ts +11 -3
- package/dist/mod/hooks/ui/alerts.tsx +21 -5
- package/dist/mod/hooks/ui/band.tsx +1 -1
- package/dist/mod/hooks/ui/view.ts +4 -1
- package/dist/mod/tests/register.test.tsx +53 -0
- package/dist/mod/tests/ui.test.tsx +10 -6
- package/dist/mod/types/index.d.ts +4 -1
- package/dist/router/src/admin.js +72 -2
- package/dist/router/src/agents.js +4 -0
- package/dist/router/src/config.js +24 -0
- package/dist/router/src/ingress/server.js +2 -2
- package/dist/router/src/mod-settings.js +2 -1
- package/dist/router/src/providers/chatgpt/index.js +19 -6
- package/dist/router/src/providers/chatgpt/translate.js +3 -0
- package/dist/router/src/providers/google/index.js +4 -3
- package/dist/router/src/providers/grok/auth.js +289 -0
- package/dist/router/src/providers/grok/catalog.js +52 -0
- package/dist/router/src/providers/grok/index.js +94 -0
- package/dist/router/src/providers/openai/index.js +71 -17
- package/dist/router/src/providers/openai/translate.js +11 -1
- package/dist/router/src/proxy.js +91 -21
- package/dist/router/src/redact.js +7 -2
- package/dist/router/src/requestlog.js +78 -20
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +105 -0
- package/dist/ui/i18n.js +6 -6
- package/docs/ARCHITECTURE.md +91 -0
- 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.",
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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.
|
|
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": [
|