clauderipple 0.8.9 → 0.8.10
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 +26 -0
- package/dist/app/dist/main.js +23 -3
- package/dist/app/package.json +1 -1
- package/dist/cli/src/index.js +1 -1
- package/dist/cli/src/tray.js +41 -5
- package/dist/router/src/admin.js +25 -1
- package/dist/router/src/compat.js +10 -2
- package/dist/router/src/providers/anthropic-account-pool.js +10 -4
- package/dist/router/src/providers/anthropic-accounts.js +26 -1
- package/dist/router/src/providers/chatgpt/catalog.js +45 -14
- package/dist/router/src/providers/chatgpt/index.js +3 -2
- package/dist/router/src/providers/chatgpt/translate.js +8 -4
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +30 -6
- package/dist/ui/i18n.js +6 -6
- package/dist/ui/style.css +2 -0
- package/docs/ARCHITECTURE.md +6 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.10 — 2026-10-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Choose which Claude account is used first.** With several Claude accounts, the current Claude
|
|
8
|
+
login was always tried first. Each account card now has a "Use first" button (Providers →
|
|
9
|
+
Claude → Accounts). New conversations start on that account, move to the next one when it hits
|
|
10
|
+
its limit, and return to it once the limit resets. A conversation already running stays on its
|
|
11
|
+
account to keep its cache.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **DeepSeek subagents no longer fail with 400 on the Artifact tool.** DeepSeek's Anthropic
|
|
16
|
+
endpoint rejected Claude Desktop's Artifact tool over a regular expression it cannot compile
|
|
17
|
+
(`"^[^\0]*$" is not a "regex"`). That pattern is now removed on the way out, as it already was for
|
|
18
|
+
OpenAI-format providers (#51).
|
|
19
|
+
- **The tray starts again after a Windows restart.** Installed with the install script, the tray
|
|
20
|
+
was set to open at logon without its own script, so Windows opened a bare Electron instead
|
|
21
|
+
(#50). Restarting the tray once after updating fixes the entry. `clauderipple tray` now also
|
|
22
|
+
reports a tray that quits right after starting, with its output in `logs/tray.log`. Running it
|
|
23
|
+
while a tray is already up no longer adds a second icon.
|
|
24
|
+
- **New ChatGPT models show up without the Codex CLI installed.** OpenAI's model list hides
|
|
25
|
+
models from older Codex versions, and without a Codex CLI the router asked as 0.155.0, so GPT-6.1
|
|
26
|
+
Sol never appeared (#50). It now asks as the newest published Codex. New models appear unticked in
|
|
27
|
+
the ChatGPT provider form.
|
|
28
|
+
|
|
3
29
|
## 0.8.9 — 2026-10-08
|
|
4
30
|
|
|
5
31
|
### Fixed
|
package/dist/app/dist/main.js
CHANGED
|
@@ -512,9 +512,19 @@ function writeAppState(patch) {
|
|
|
512
512
|
/* a read-only home only costs us the remembered default */
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
|
+
/**
|
|
516
|
+
* How Windows starts this tray at login. A packaged app is its own binary. Installed from npm, the
|
|
517
|
+
* binary is a bare Electron that needs our script after it: registered without one, every logon
|
|
518
|
+
* opened Electron's default app and no tray (issue #50). macOS ignores `path`/`args`.
|
|
519
|
+
*/
|
|
520
|
+
function loginItemLaunch() {
|
|
521
|
+
if (electron_1.app.isPackaged || process.platform !== "win32" || !process.argv[1])
|
|
522
|
+
return undefined;
|
|
523
|
+
return { path: process.execPath, args: [node_path_1.default.resolve(process.argv[1])] };
|
|
524
|
+
}
|
|
515
525
|
function loginItemOn() {
|
|
516
526
|
try {
|
|
517
|
-
return electron_1.app.getLoginItemSettings().openAtLogin;
|
|
527
|
+
return electron_1.app.getLoginItemSettings(loginItemLaunch()).openAtLogin;
|
|
518
528
|
}
|
|
519
529
|
catch {
|
|
520
530
|
return false;
|
|
@@ -522,7 +532,7 @@ function loginItemOn() {
|
|
|
522
532
|
}
|
|
523
533
|
function setLoginItem(on) {
|
|
524
534
|
try {
|
|
525
|
-
electron_1.app.setLoginItemSettings({ openAtLogin: on });
|
|
535
|
+
electron_1.app.setLoginItemSettings({ openAtLogin: on, ...loginItemLaunch() });
|
|
526
536
|
}
|
|
527
537
|
catch {
|
|
528
538
|
/* macOS may refuse in an unsigned development build */
|
|
@@ -798,7 +808,9 @@ async function offerUpdate(check) {
|
|
|
798
808
|
availableUpdate = null;
|
|
799
809
|
if (electron_1.Notification.isSupported())
|
|
800
810
|
new electron_1.Notification({ title: "ClaudeRipple", body: L.updateDone(version) }).show();
|
|
801
|
-
// A new tray from the new files, then this one goes.
|
|
811
|
+
// A new tray from the new files, then this one goes. The lock is let go first, or the new tray
|
|
812
|
+
// would see this one and leave.
|
|
813
|
+
electron_1.app.releaseSingleInstanceLock();
|
|
802
814
|
await runCliResult(["tray"]);
|
|
803
815
|
electron_1.app.quit();
|
|
804
816
|
}
|
|
@@ -914,6 +926,10 @@ function render() {
|
|
|
914
926
|
tray.setContextMenu(electron_1.Menu.buildFromTemplate(template));
|
|
915
927
|
tray.setToolTip(s ? L.tooltip(state) : L.tooltipDown);
|
|
916
928
|
}
|
|
929
|
+
// One tray. `clauderipple tray` run while one is up started a second icon (Windows VM,
|
|
930
|
+
// 2026-10-10); the newcomer now leaves at once with code 0, which the CLI reads as "already running".
|
|
931
|
+
if (!electron_1.app.requestSingleInstanceLock())
|
|
932
|
+
electron_1.app.exit(0);
|
|
917
933
|
electron_1.app.whenReady().then(() => {
|
|
918
934
|
L = STRINGS[uiLang()];
|
|
919
935
|
// Without this, Windows attributes our notifications to "Electron" instead of ClaudeRipple.
|
|
@@ -927,6 +943,10 @@ electron_1.app.whenReady().then(() => {
|
|
|
927
943
|
// Default to starting at login on first run; after that the user's choice stands.
|
|
928
944
|
if (!appState().loginItemInitialized)
|
|
929
945
|
setLoginItem(true);
|
|
946
|
+
// An entry an older version wrote names bare Electron (the query without args finds it); the
|
|
947
|
+
// user had start-at-login on, so it is rewritten with the script rather than left broken.
|
|
948
|
+
else if (loginItemLaunch() && electron_1.app.getLoginItemSettings().openAtLogin && !loginItemOn())
|
|
949
|
+
setLoginItem(true);
|
|
930
950
|
tray = new electron_1.Tray(icon("down"));
|
|
931
951
|
tray.on("click", () => tray?.popUpContextMenu());
|
|
932
952
|
render();
|
package/dist/app/package.json
CHANGED
package/dist/cli/src/index.js
CHANGED
|
@@ -647,7 +647,7 @@ try {
|
|
|
647
647
|
await update();
|
|
648
648
|
break;
|
|
649
649
|
case "tray": {
|
|
650
|
-
const result = args.includes("--install") ? installTrayRuntime() : startTray();
|
|
650
|
+
const result = args.includes("--install") ? installTrayRuntime() : await startTray();
|
|
651
651
|
console.log(result.message);
|
|
652
652
|
if (!result.ok)
|
|
653
653
|
process.exit(1);
|
package/dist/cli/src/tray.js
CHANGED
|
@@ -52,10 +52,11 @@ export function trayEnv(env = process.env) {
|
|
|
52
52
|
return out;
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
* Starts the tray and returns
|
|
56
|
-
* command typed in a terminal wants; the supervisor uses the foreground
|
|
55
|
+
* Starts the tray and returns once it has stayed up for a moment. `detached` outlives this
|
|
56
|
+
* process, which is what a command typed in a terminal wants; the supervisor uses the foreground
|
|
57
|
+
* form instead.
|
|
57
58
|
*/
|
|
58
|
-
export function startTray(options = {}) {
|
|
59
|
+
export async function startTray(options = {}) {
|
|
59
60
|
const current = runtime();
|
|
60
61
|
if (current.packaged) {
|
|
61
62
|
// The packaged app IS the tray: relaunching its own binary with no script is what opens it.
|
|
@@ -75,15 +76,50 @@ export function startTray(options = {}) {
|
|
|
75
76
|
if (!fs.existsSync(main)) {
|
|
76
77
|
return { ok: false, message: `the tray is not built: ${main} is missing (run: npm run build)` };
|
|
77
78
|
}
|
|
79
|
+
// Electron's own output goes to a file. With it discarded, a tray that died at launch still
|
|
80
|
+
// printed "✓ tray started" and left nothing to go on (issue #50).
|
|
81
|
+
const log = trayLogPath();
|
|
82
|
+
fs.mkdirSync(path.dirname(log), { recursive: true });
|
|
83
|
+
const out = fs.openSync(log, "a");
|
|
78
84
|
const child = spawn(electron, [main], {
|
|
79
85
|
detached: options.detached ?? true,
|
|
80
|
-
stdio: "ignore",
|
|
86
|
+
stdio: ["ignore", out, out],
|
|
81
87
|
// Electron would otherwise re-enter as a plain Node process, since that is how the CLI runs.
|
|
82
88
|
env: trayEnv(),
|
|
83
89
|
});
|
|
90
|
+
fs.closeSync(out);
|
|
84
91
|
if (options.detached ?? true)
|
|
85
92
|
child.unref();
|
|
86
|
-
return
|
|
93
|
+
return earlyExit(child, log);
|
|
94
|
+
}
|
|
95
|
+
export function trayLogPath() {
|
|
96
|
+
return path.join(homeDir(), "logs", "tray.log");
|
|
97
|
+
}
|
|
98
|
+
/** A tray that is still running after a moment has started; one that exits before then has not. */
|
|
99
|
+
export function earlyExit(child, log, waitMs = 2_000) {
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
// Not unref'd: the command has to stay up long enough to see the exit it is waiting for.
|
|
102
|
+
const timer = setTimeout(() => resolve({ ok: true, message: "✓ tray started" }), waitMs);
|
|
103
|
+
const failed = (detail) => {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
let tail = "";
|
|
106
|
+
try {
|
|
107
|
+
tail = fs.readFileSync(log, "utf8").trim().split(/\r?\n/).slice(-8).join("\n");
|
|
108
|
+
}
|
|
109
|
+
catch { /* nothing logged */ }
|
|
110
|
+
resolve({ ok: false, message: `the tray exited at once (${detail}). Its output is in ${log}${tail ? `:\n${tail}` : ""}` });
|
|
111
|
+
};
|
|
112
|
+
child.once("error", (error) => failed(error.message));
|
|
113
|
+
child.once("exit", (code, signal) => {
|
|
114
|
+
// A clean exit at once is a tray that found another one running (main.ts's single-instance lock).
|
|
115
|
+
if (code === 0 && !signal) {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
resolve({ ok: true, message: "✓ tray is already running" });
|
|
118
|
+
}
|
|
119
|
+
else
|
|
120
|
+
failed(signal ? `signal ${signal}` : `exit code ${code}`);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
87
123
|
}
|
|
88
124
|
/** The npm that came with the Node running us, as a script we can hand to that same Node. */
|
|
89
125
|
function npmCli() {
|
package/dist/router/src/admin.js
CHANGED
|
@@ -39,7 +39,7 @@ import { desktopClaudeCodeDirs } from "../../cli/src/claude-auth.js";
|
|
|
39
39
|
import { statusModEnabled, syncModelSlots } from "../../cli/src/settings.js";
|
|
40
40
|
import { ClaudeOAuthSession } from "./providers/claude-oauth.js";
|
|
41
41
|
import { readClaudeAuthFile } from "./providers/anthropic-token-file.js";
|
|
42
|
-
import { listClaudeAccounts, removeClaudeAccount, renameClaudeAccount } from "./providers/anthropic-accounts.js";
|
|
42
|
+
import { listClaudeAccounts, removeClaudeAccount, renameClaudeAccount, setPreferredClaudeAccount } from "./providers/anthropic-accounts.js";
|
|
43
43
|
import { readChatGptAccounts, removeChatGptAccount, summarize as summarizeChatGptAccount, updateChatGptAccount } from "./providers/chatgpt/accounts.js";
|
|
44
44
|
import { IMAGE_ASPECTS, IMAGE_BACKGROUNDS, IMAGE_FORMATS } from "./providers/chatgpt/index.js";
|
|
45
45
|
import { ANTIGRAVITY_WARNING, staticAntigravityModels } from "./providers/google/antigravity.js";
|
|
@@ -1643,6 +1643,30 @@ export function startAdmin(deps) {
|
|
|
1643
1643
|
});
|
|
1644
1644
|
return;
|
|
1645
1645
|
}
|
|
1646
|
+
// Which account a new conversation tries first: a stored account id, or null for the current login.
|
|
1647
|
+
if (pathname === "/api/claude-accounts" && method === "PUT") {
|
|
1648
|
+
let preferred;
|
|
1649
|
+
try {
|
|
1650
|
+
preferred = JSON.parse((await readBody(req)).toString("utf8")).preferred;
|
|
1651
|
+
}
|
|
1652
|
+
catch {
|
|
1653
|
+
sendJson(res, 400, { error: "invalid JSON" });
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
if (preferred === "current")
|
|
1657
|
+
preferred = null;
|
|
1658
|
+
if (preferred !== null && (typeof preferred !== "string" || !preferred)) {
|
|
1659
|
+
sendJson(res, 400, { error: "expected {preferred: account id or null}" });
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
if (!setPreferredClaudeAccount(homeDir(), preferred)) {
|
|
1663
|
+
sendJson(res, 404, { error: "Claude account not found" });
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
deps.log.info(`admin: Claude accounts now try ${preferred === null ? "the current login" : `account ${preferred.slice(0, 8)}`} first`);
|
|
1667
|
+
sendJson(res, 200, { ok: true });
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1646
1670
|
if (pathname.startsWith("/api/claude-accounts/") && (method === "PATCH" || method === "DELETE")) {
|
|
1647
1671
|
let id;
|
|
1648
1672
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Sanitise Claude Code's Anthropic-only request extensions for providers that expose
|
|
2
2
|
// an Anthropic-shaped Messages endpoint without implementing Anthropic's full feature set.
|
|
3
3
|
// Pure: callers receive a new object and an audit list; the input is never mutated.
|
|
4
|
-
import { droppedServerToolNote } from "./providers/chatgpt/translate.js";
|
|
4
|
+
import { droppedServerToolNote, scrubSchema } from "./providers/chatgpt/translate.js";
|
|
5
5
|
export const EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max", "ultra"];
|
|
6
6
|
export const STRICT_COMPAT_CAPS = {
|
|
7
7
|
effortLevels: [],
|
|
@@ -169,6 +169,7 @@ export function sanitizeForCompatible(json, caps) {
|
|
|
169
169
|
const droppedNames = new Set();
|
|
170
170
|
let deferred = 0;
|
|
171
171
|
let dropped = 0;
|
|
172
|
+
const patterns = { count: 0 };
|
|
172
173
|
for (const tool of out.tools) {
|
|
173
174
|
const source = record(tool);
|
|
174
175
|
if (!source) {
|
|
@@ -188,13 +189,20 @@ export function sanitizeForCompatible(json, caps) {
|
|
|
188
189
|
delete next.defer_loading;
|
|
189
190
|
deferred++;
|
|
190
191
|
}
|
|
192
|
+
// A strict regex engine refuses the whole request over one pattern it cannot compile:
|
|
193
|
+
// DeepSeek's Anthropic endpoint answers 400 `"^[^\\0]*$" is not a "regex"` for the Artifact
|
|
194
|
+
// tool (issue #51). The client validates its own tool input, so the pattern adds nothing here.
|
|
195
|
+
if (next.input_schema !== undefined)
|
|
196
|
+
next.input_schema = scrubSchema(next.input_schema, patterns);
|
|
191
197
|
kept.push(next);
|
|
192
198
|
}
|
|
193
199
|
if (deferred > 0)
|
|
194
200
|
changes.push(`defer_loading×${deferred}`);
|
|
195
201
|
if (dropped > 0)
|
|
196
202
|
changes.push(`server_tools×${dropped}`);
|
|
197
|
-
if (
|
|
203
|
+
if (patterns.count > 0)
|
|
204
|
+
changes.push(`pattern×${patterns.count}`);
|
|
205
|
+
if (deferred > 0 || dropped > 0 || patterns.count > 0)
|
|
198
206
|
out.tools = kept;
|
|
199
207
|
const toolChoice = record(out.tool_choice);
|
|
200
208
|
if (toolChoice && typeof toolChoice.name === "string" && droppedNames.has(toolChoice.name)) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Runtime view of Claude subscription accounts: current Claude Code/Desktop login first
|
|
2
|
-
// ClaudeRipple-owned OAuth accounts. Stored grants are refreshed just before expiry and projected as
|
|
1
|
+
// Runtime view of Claude subscription accounts: current Claude Code/Desktop login first (unless the
|
|
2
|
+
// user marked a stored account preferred, which then leads), followed by ClaudeRipple-owned OAuth accounts. Stored grants are refreshed just before expiry and projected as
|
|
3
3
|
// ordinary proxy credentials, so the shared CredentialPool supplies affinity, cooldown and retry.
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import { redactErrorText } from "../redact.js";
|
|
@@ -115,12 +115,18 @@ export class ClaudeAccountAuthPool {
|
|
|
115
115
|
// first migration write, omit that duplicate instead of trying the same token twice.
|
|
116
116
|
if (currentToken && account.token === currentToken)
|
|
117
117
|
continue;
|
|
118
|
-
|
|
118
|
+
const credential = {
|
|
119
119
|
id: credentialId(account.id, account.token, account.refreshToken),
|
|
120
120
|
ownerId: account.id,
|
|
121
121
|
label: account.label,
|
|
122
122
|
headers: nativeAnthropicHeaders({ type: "anthropic", auth: "claude-code" }, { accessToken: account.token, expiresAt: account.expiresAt }),
|
|
123
|
-
}
|
|
123
|
+
};
|
|
124
|
+
// The pool tries credentials in this order, so the account the user chose goes ahead of the
|
|
125
|
+
// current login. A conversation already held by another account stays there (its cache).
|
|
126
|
+
if (account.preferred)
|
|
127
|
+
out.unshift(credential);
|
|
128
|
+
else
|
|
129
|
+
out.push(credential);
|
|
124
130
|
}
|
|
125
131
|
return out;
|
|
126
132
|
}
|
|
@@ -27,7 +27,8 @@ function validAccount(value) {
|
|
|
27
27
|
&& typeof a.label === "string" && a.label.length > 0
|
|
28
28
|
&& (a.subjectHash === undefined || (typeof a.subjectHash === "string" && /^[0-9a-f]{64}$/.test(a.subjectHash)))
|
|
29
29
|
&& (a.email === undefined || typeof a.email === "string")
|
|
30
|
-
&& (a.needsReauth === undefined || typeof a.needsReauth === "boolean")
|
|
30
|
+
&& (a.needsReauth === undefined || typeof a.needsReauth === "boolean")
|
|
31
|
+
&& (a.preferred === undefined || typeof a.preferred === "boolean");
|
|
31
32
|
}
|
|
32
33
|
function parseClaudeAccountsFile(home) {
|
|
33
34
|
try {
|
|
@@ -178,6 +179,29 @@ export function renameClaudeAccount(home, id, label) {
|
|
|
178
179
|
return true;
|
|
179
180
|
});
|
|
180
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Make one stored account the first one tried, or with `null` hand first place back to the current
|
|
184
|
+
* Claude login. False when the id names no stored account.
|
|
185
|
+
*/
|
|
186
|
+
export function setPreferredClaudeAccount(home, id) {
|
|
187
|
+
return withAccountLock(home, () => {
|
|
188
|
+
const accounts = readClaudeOAuthAccountsForMutation(home);
|
|
189
|
+
if (id !== null && !accounts.some((account) => account.id === id))
|
|
190
|
+
return false;
|
|
191
|
+
let changed = false;
|
|
192
|
+
const next = accounts.map((account) => {
|
|
193
|
+
const want = account.id === id;
|
|
194
|
+
if ((account.preferred === true) === want)
|
|
195
|
+
return account;
|
|
196
|
+
changed = true;
|
|
197
|
+
const { preferred: _, ...rest } = account;
|
|
198
|
+
return want ? { ...rest, preferred: true } : rest;
|
|
199
|
+
});
|
|
200
|
+
if (changed)
|
|
201
|
+
writeClaudeAccountsFile(home, next);
|
|
202
|
+
return true;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
181
205
|
export function removeClaudeAccount(home, id) {
|
|
182
206
|
return withAccountLock(home, () => {
|
|
183
207
|
const accounts = readClaudeOAuthAccountsForMutation(home);
|
|
@@ -209,6 +233,7 @@ export function summarize(account) {
|
|
|
209
233
|
...(account.email ? { email: account.email } : {}),
|
|
210
234
|
expiresAt: account.expiresAt,
|
|
211
235
|
needsReauth: account.needsReauth === true,
|
|
236
|
+
preferred: account.preferred === true,
|
|
212
237
|
};
|
|
213
238
|
}
|
|
214
239
|
export function listClaudeAccounts(home) {
|
|
@@ -11,8 +11,13 @@
|
|
|
11
11
|
import fs from "node:fs";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { codexHome } from "../../../../cli/src/codex.js";
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The client_version floor: below this the backend hides models ClaudeRipple already supports.
|
|
16
|
+
* 0.158.0 omits gpt-6.1-sol and 0.159.0 lists it (measured 2026-10-10). A floor always trails the
|
|
17
|
+
* next release, so the newest published Codex version is asked for as well (`latestCodexVersion`):
|
|
18
|
+
* a Windows user with no Codex CLI was asking with this floor and never saw 6.1 Sol (issue #50).
|
|
19
|
+
*/
|
|
20
|
+
export const CODEX_CLIENT_VERSION_FLOOR = "0.159.0";
|
|
16
21
|
/**
|
|
17
22
|
* Copied from the Codex catalog 2026-09-23, used only when the live catalog cannot be read. The
|
|
18
23
|
* catalog lists `ultra` on astra, sol and 5.6 terra/sol and not on either Luna — the reverse of a
|
|
@@ -78,20 +83,46 @@ function compareVersions(a, b) {
|
|
|
78
83
|
/**
|
|
79
84
|
* The version to ask the catalogue with: the installed Codex CLI's, from the cache it writes
|
|
80
85
|
* (`<codexHome>/models_cache.json`, honouring `CODEX_HOME`), but never below the floor — an older
|
|
81
|
-
* CLI must not cost us the models the newer backend would list.
|
|
82
|
-
*
|
|
86
|
+
* CLI must not cost us the models the newer backend would list. `latest` (the newest published
|
|
87
|
+
* Codex) wins when it is higher still. Missing or unreadable file, or a version that is not
|
|
88
|
+
* numeric, answers the floor.
|
|
83
89
|
*/
|
|
84
|
-
export function codexClientVersion(home = codexHome()) {
|
|
85
|
-
let
|
|
90
|
+
export function codexClientVersion(home = codexHome(), latest) {
|
|
91
|
+
let best = CODEX_CLIENT_VERSION_FLOOR;
|
|
92
|
+
const consider = (value) => {
|
|
93
|
+
if (typeof value !== "string" || !/^\d+(\.\d+)*/.test(value))
|
|
94
|
+
return;
|
|
95
|
+
// "0.155.1-nightly.3" is 0.155.1 as far as the backend's filter is concerned.
|
|
96
|
+
const core = value.split("-")[0];
|
|
97
|
+
if (compareVersions(core, best) > 0)
|
|
98
|
+
best = core;
|
|
99
|
+
};
|
|
86
100
|
try {
|
|
87
|
-
|
|
101
|
+
consider(JSON.parse(fs.readFileSync(path.join(home, "models_cache.json"), "utf8")).client_version);
|
|
88
102
|
}
|
|
89
|
-
catch {
|
|
90
|
-
|
|
103
|
+
catch { /* no Codex CLI here */ }
|
|
104
|
+
consider(latest);
|
|
105
|
+
return best;
|
|
106
|
+
}
|
|
107
|
+
const NPM_LATEST = "https://registry.npmjs.org/@openai/codex/latest";
|
|
108
|
+
const LATEST_TTL_MS = 24 * 60 * 60_000;
|
|
109
|
+
const LATEST_RETRY_MS = 60 * 60_000;
|
|
110
|
+
let latestCache = null;
|
|
111
|
+
/**
|
|
112
|
+
* The newest published Codex CLI version, from the npm registry, kept a day; null when the
|
|
113
|
+
* registry cannot be read, which is then not asked again for an hour. Never throws.
|
|
114
|
+
*/
|
|
115
|
+
export async function latestCodexVersion(fetchImpl = fetch, now = Date.now()) {
|
|
116
|
+
if (latestCache && latestCache.until > now)
|
|
117
|
+
return latestCache.version;
|
|
118
|
+
let version = null;
|
|
119
|
+
try {
|
|
120
|
+
const res = await fetchImpl(NPM_LATEST, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(3_000) });
|
|
121
|
+
const value = res.ok ? (await res.json()).version : undefined;
|
|
122
|
+
if (typeof value === "string" && /^\d+\.\d+\.\d+/.test(value))
|
|
123
|
+
version = value;
|
|
91
124
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const core = cached.split("-")[0];
|
|
96
|
-
return compareVersions(core, CODEX_CLIENT_VERSION_FLOOR) > 0 ? core : CODEX_CLIENT_VERSION_FLOOR;
|
|
125
|
+
catch { /* offline or blocked: the floor and the local CLI still answer */ }
|
|
126
|
+
latestCache = { version, until: now + (version ? LATEST_TTL_MS : LATEST_RETRY_MS) };
|
|
127
|
+
return version;
|
|
97
128
|
}
|
|
@@ -9,7 +9,7 @@ import { fetchWithRetry } from "../retry.js";
|
|
|
9
9
|
import { looksLikeAuth } from "../openai/index.js";
|
|
10
10
|
import { StreamMapper, conversationKey, estimateTokens, formatSse, serverToolNames, toResponsesRequest, toolNameRestoreMap } from "./translate.js";
|
|
11
11
|
import { credentialHeaderValues, redactErrorText } from "../../redact.js";
|
|
12
|
-
import { codexClientVersion, parseCodexCatalog } from "./catalog.js";
|
|
12
|
+
import { codexClientVersion, latestCodexVersion, parseCodexCatalog } from "./catalog.js";
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { homeDir } from "../../config.js";
|
|
@@ -635,11 +635,12 @@ export class ChatGptAdapter {
|
|
|
635
635
|
this.log.warn(`chatgpt ${this.name}: model-catalog fetch skipped: ${tokens.message}`);
|
|
636
636
|
return null;
|
|
637
637
|
}
|
|
638
|
+
const version = codexClientVersion(undefined, await latestCodexVersion());
|
|
638
639
|
const ac = new AbortController();
|
|
639
640
|
const timer = setTimeout(() => ac.abort(), MODELS_TIMEOUT_MS);
|
|
640
641
|
let res;
|
|
641
642
|
try {
|
|
642
|
-
res = await fetch(`${(this.cfg.url ?? DEFAULT_BASE).replace(/\/$/, "")}${MODELS_PATH}?client_version=${encodeURIComponent(
|
|
643
|
+
res = await fetch(`${(this.cfg.url ?? DEFAULT_BASE).replace(/\/$/, "")}${MODELS_PATH}?client_version=${encodeURIComponent(version)}`, {
|
|
643
644
|
method: "GET",
|
|
644
645
|
headers: {
|
|
645
646
|
authorization: `Bearer ${tokens.accessToken}`,
|
|
@@ -114,16 +114,20 @@ const UNSUPPORTED_REGEX = /\(\?[=!<]|\\[0-9]/;
|
|
|
114
114
|
export function unsupportedPattern(p) {
|
|
115
115
|
return UNSUPPORTED_REGEX.test(p);
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
/** Drops the patterns above anywhere in a schema; `removed.count` says how many went. */
|
|
118
|
+
export function scrubSchema(node, removed) {
|
|
118
119
|
if (Array.isArray(node))
|
|
119
|
-
return node.map(scrubSchema);
|
|
120
|
+
return node.map((item) => scrubSchema(item, removed));
|
|
120
121
|
if (typeof node !== "object" || node === null)
|
|
121
122
|
return node;
|
|
122
123
|
const out = {};
|
|
123
124
|
for (const [k, v] of Object.entries(node)) {
|
|
124
|
-
if (k === "pattern" && typeof v === "string" && unsupportedPattern(v))
|
|
125
|
+
if (k === "pattern" && typeof v === "string" && unsupportedPattern(v)) {
|
|
126
|
+
if (removed)
|
|
127
|
+
removed.count++;
|
|
125
128
|
continue;
|
|
126
|
-
|
|
129
|
+
}
|
|
130
|
+
out[k] = scrubSchema(v, removed);
|
|
127
131
|
}
|
|
128
132
|
return out;
|
|
129
133
|
}
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
// from the manifests. Until 0.1.1 the router and the CLI each carried their own literal, and both
|
|
6
6
|
// still said "0.1.0" in the 0.1.1 release: after an update nobody could tell which router was
|
|
7
7
|
// running, and the tray app had nothing to compare (2026-09-15, reported from a Windows install).
|
|
8
|
-
export const VERSION = "0.8.
|
|
8
|
+
export const VERSION = "0.8.10";
|
package/dist/ui/app.js
CHANGED
|
@@ -1141,14 +1141,35 @@ function claudeQuotaNode(account) {
|
|
|
1141
1141
|
return text ? el("p", { class: "small account-quota", text }) : null;
|
|
1142
1142
|
}
|
|
1143
1143
|
|
|
1144
|
+
/** "Use first" for an account the pool does not lead with; `null` hands first place back to the current login. */
|
|
1145
|
+
function claudePreferButton(name, id, disabled) {
|
|
1146
|
+
const button = el("button", { class: "btn secondary compact", type: "button", text: t("providers.preferAction") });
|
|
1147
|
+
button.disabled = disabled;
|
|
1148
|
+
button.addEventListener("click", async () => {
|
|
1149
|
+
button.disabled = true;
|
|
1150
|
+
try { await api("/api/claude-accounts", { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ preferred: id }) }); await refreshClaudeAccountPanel(name); }
|
|
1151
|
+
catch (error) { button.disabled = false; toast(t("common.actionFailed"), true, error.message); }
|
|
1152
|
+
});
|
|
1153
|
+
return button;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1144
1156
|
function renderClaudeAccountRows(target, data, name, generation) {
|
|
1145
1157
|
if (generation !== providerDetailGeneration) return;
|
|
1158
|
+
// Rows follow the order the router tries them in: the preferred account, else the current login, first.
|
|
1159
|
+
const stored = Array.isArray(data.accounts) ? [...data.accounts] : [];
|
|
1160
|
+
const preferredIndex = stored.findIndex((account) => account.preferred);
|
|
1161
|
+
if (preferredIndex > 0) stored.unshift(...stored.splice(preferredIndex, 1));
|
|
1162
|
+
const leader = preferredIndex >= 0 ? stored[0].id : data.current ? "current" : stored[0] && stored[0].id;
|
|
1163
|
+
const choosable = (data.current ? 1 : 0) + stored.length > 1;
|
|
1164
|
+
const firstBadge = (id) => (choosable && id === leader ? el("span", { class: "badge first", text: t("providers.preferBadge") }) : null);
|
|
1146
1165
|
const rows = [];
|
|
1147
|
-
|
|
1166
|
+
const currentRow = data.current ? el("article", { class: "account-card current" }, [
|
|
1148
1167
|
el("div", { class: "account-card-copy" }, [el("strong", { text: data.current.label }), el("span", { class: "small", text: anthropicSourceText(data.current.source) }), claudeQuotaNode(data.current), hint(t("providers.currentAccountHelp"))].filter(Boolean)),
|
|
1149
|
-
el("span", { class: "badge ok", text: t("providers.anthropicCurrent") }),
|
|
1150
|
-
|
|
1151
|
-
|
|
1168
|
+
el("div", { class: "account-card-badges" }, [firstBadge("current"), el("span", { class: "badge ok", text: t("providers.anthropicCurrent") })].filter(Boolean)),
|
|
1169
|
+
choosable && leader !== "current" ? el("div", { class: "account-card-actions" }, [claudePreferButton(name, null, false)]) : null,
|
|
1170
|
+
].filter(Boolean)) : null;
|
|
1171
|
+
if (currentRow && preferredIndex < 0) rows.push(currentRow);
|
|
1172
|
+
for (const account of stored) {
|
|
1152
1173
|
const unavailable = account.needsReauth || account.expiresAt <= Date.now();
|
|
1153
1174
|
const rename = el("button", { class: "btn secondary compact", type: "button", text: t("common.edit") });
|
|
1154
1175
|
rename.addEventListener("click", async () => {
|
|
@@ -1164,6 +1185,7 @@ function renderClaudeAccountRows(target, data, name, generation) {
|
|
|
1164
1185
|
catch (error) { toast(t("common.actionFailed"), true, error.message); }
|
|
1165
1186
|
});
|
|
1166
1187
|
const actions = [rename, remove];
|
|
1188
|
+
if (choosable && account.id !== leader) actions.unshift(claudePreferButton(name, account.id, unavailable));
|
|
1167
1189
|
if (unavailable) {
|
|
1168
1190
|
const reauth = el("button", { class: "btn secondary compact", type: "button", text: t("providers.reauthAction") });
|
|
1169
1191
|
reauth.addEventListener("click", () => openClaudeAccountConsent(name));
|
|
@@ -1171,12 +1193,14 @@ function renderClaudeAccountRows(target, data, name, generation) {
|
|
|
1171
1193
|
}
|
|
1172
1194
|
rows.push(el("article", { class: "account-card" }, [
|
|
1173
1195
|
el("div", { class: "account-card-copy" }, [el("strong", { text: account.label }), account.email && account.email !== account.label ? el("span", { class: "small", text: account.email }) : null, claudeQuotaNode(account), hint(t("providers.addedAccountHelp"))].filter(Boolean)),
|
|
1174
|
-
el("span", { class: `badge ${unavailable ? "bad" : "ok"}`, text: unavailable ? t("providers.anthropicReauth") : t("pool.ready") }),
|
|
1196
|
+
el("div", { class: "account-card-badges" }, [firstBadge(account.id), el("span", { class: `badge ${unavailable ? "bad" : "ok"}`, text: unavailable ? t("providers.anthropicReauth") : t("pool.ready") })].filter(Boolean)),
|
|
1175
1197
|
el("div", { class: "account-card-actions" }, actions),
|
|
1176
1198
|
]));
|
|
1199
|
+
if (currentRow && rows.length === 1 && preferredIndex >= 0) rows.push(currentRow);
|
|
1177
1200
|
}
|
|
1178
1201
|
target.replaceChildren(...(rows.length ? rows : [el("div", { class: "empty-card", text: t("providers.anthropicNoAccounts") })]));
|
|
1179
|
-
|
|
1202
|
+
if (choosable) target.appendChild(hint(t("providers.preferHelp")));
|
|
1203
|
+
const count = (data.current ? 1 : 0) + stored.length;
|
|
1180
1204
|
const countNode = $("#claude-account-count");
|
|
1181
1205
|
if (countNode) countNode.textContent = t("providers.accountCount", { count });
|
|
1182
1206
|
}
|
package/dist/ui/i18n.js
CHANGED
|
@@ -10,7 +10,7 @@ const I18N = {
|
|
|
10
10
|
"update.check": "업데이트 확인", "update.checking": "확인 중…", "update.latest": "최신 버전입니다.", "update.install": "{latest}(으)로 업데이트", "update.download": "{latest} 받기", "update.checkout": "{latest}이(가) 나왔습니다. 소스 체크아웃이라 git pull로 업데이트하세요.", "update.running": "업데이트 중…", "update.checkFailed": "업데이트를 확인하지 못했습니다.", "update.confirm": "ClaudeRipple {current} → {latest}로 업데이트할까요? 진행 중인 요청이 끝나면 라우터가 새 버전으로 다시 시작하고, 이 창은 저절로 새로고침됩니다.", "update.started": "업데이트를 시작했습니다. 라우터가 새 버전으로 다시 시작하면 이 창이 새로고침됩니다. 진행 기록은 ~/.clauderipple/logs/update.log에 있습니다.", "update.failed": "업데이트를 시작하지 못했습니다.",
|
|
11
11
|
"picker.title": "모델 피커","picker.statusHelp": "켜면 Claude Desktop의 모델 피커에 프로바이더 모델이 실제 이름으로 나타납니다. 끄면 모델 매핑대로 Claude 이름 뒤에서 답합니다.", "picker.state": "상태", "picker.on": "켜짐", "picker.off": "꺼짐", "picker.turnOn": "피커 켜기", "picker.turnOff": "피커 끄기", "picker.last": "최근에 모델 {count}개를 표시했습니다.", "picker.restartHelp": "켠 뒤 Claude Desktop을 완전히 껐다 다시 여세요.", "picker.confirmOn": "인증서 신뢰를 위해 운영체제가 확인 창을 띄웁니다(macOS는 로그인 암호, Windows는 인증서 확인). ClaudeRipple은 암호를 보지 않습니다. 계속할까요?", "picker.working": "처리 중입니다.", "picker.doneOn": "켜졌습니다. Claude Desktop을 완전히 껐다 다시 여세요.", "picker.doneOff": "꺼졌습니다. Claude Desktop을 완전히 껐다 다시 여세요.",
|
|
12
12
|
"slots.title": "모델 매핑", "slots.subtitle": "Claude 앱에서 고른 모델의 요청을 실제로 어떤 모델이 답할지 정합니다. 바꾸면 자동으로 저장됩니다.", "slots.pickerHelp": "켜면 Claude Desktop의 모델 피커에 프로바이더 모델이 실제 이름으로 나타납니다. 끄면 위의 매핑대로 Claude 이름 뒤에서 답합니다.", "slots.pickerModels": "피커에 넣을 모델", "slots.pickerModelsHelp": "체크한 모델이 Claude Desktop 피커에 나타나고, 고르면 그 모델이 바로 답합니다. 새로 넣은 모델은 새 세션부터 고를 수 있습니다(진행 중인 세션은 시작할 때의 목록을 씁니다).", "slots.add": "+ 매핑 추가", "slots.th.claude": "Claude 앱에서 고른 모델", "slots.th.target": "실제로 답하는 모델", "slots.th.effort": "추론 강도 (effort)", "slots.passthrough": "그대로 Claude가 답함", "slots.noProviderModels": "먼저 프로바이더를 연결하고 쓸 모델을 고르세요.", "slots.noChanges": "바꾸는 매핑이 없습니다. 모든 모델이 Claude 그대로 답합니다.", "slots.duplicate": "같은 Claude 모델을 두 번 매핑할 수 없습니다.", "slots.effortHint": "앱에서 고른 강도가 모델이 지원하지 않으면 가장 가까운 단계로 바꿔 보냅니다. (예: xhigh → high)", "slots.noEffort": "이 모델은 추론 강도를 받지 않습니다", "pool.ready": "사용 중", "pool.cooling": "{seconds}초 뒤 복귀", "pool.quarantined": "거부됨 — 키 확인 필요", "slots.modelSlots": "Claude Code 자체 모델 슬롯", "slots.modelSlotsHelp": "Claude Code가 요청을 만들기 전에 스스로 정하는 자리라, 매핑으로는 닿지 않습니다. 비워 두면 Claude가 답하고 Claude 한도를 씁니다. 특히 웹검색이 '작고 빠른 모델'에서 돌기 때문에, 그걸 옮기지 않으면 라우팅한 세션도 검색만은 Claude 한도를 씁니다. 바꾸면 설치 시 settings.json에 반영됩니다.", "slots.noWebSearch": "이 슬롯에선 웹검색 불가", "slots.noWebSearchWarn": "저장했습니다. 다만 여기서는 웹서치가 실패합니다 — ClaudeRipple이 이 모델로 검색을 돌릴 수 없습니다. (모델 자체가 검색을 못 하는 게 아닐 수도 있습니다. 기본값으로 두면 Claude가 검색합니다.)", "slots.slotDefault": "기본값 (Claude)", "slots.slot.smallFast": "작고 빠른 모델", "slots.slot.subagent": "서브에이전트", "slots.slot.main": "세션 기본 모델", "slots.slotHelp.smallFast": "웹검색·제목·분류", "slots.slotHelp.subagent": "모델을 따로 지정하지 않은 서브에이전트", "slots.slotHelp.main": "세션이 시작할 때의 모델", "slots.slotSaved": "모델 슬롯을 저장했습니다. 새 세션부터 적용됩니다.", "slots.windowGlobal": "기본값", "slots.windowHelp": "이 모델의 컨텍스트 창(토큰). 모델마다 다르므로 각각 적습니다. 비워 두면 프로바이더가 알려준 값을, 그것도 없으면 전체 기본값을 씁니다. 압축 시점이 아니라 모델의 실제 용량을 적으세요.",
|
|
13
|
-
"providers.title": "프로바이더", "providers.subtitle": "연결된 서비스와 계정을 한곳에서 관리합니다. 프로바이더를 고르면 설정과 계정이 오른쪽에 열립니다.", "providers.search": "프로바이더 검색…", "providers.overview": "개요", "providers.accounts": "계정", "providers.settings": "설정", "providers.modelsTab": "모델", "providers.selectedModels": "선택한 모델", "providers.modelsCountShort": "모델 {count}개", "providers.noSelection": "왼쪽에서 프로바이더를 선택하세요.", "providers.rotationOn": "자동 전환 켜짐", "providers.rotationOff": "자동 전환 꺼짐", "providers.rotationSaving": "자동 전환 저장 중…", "providers.rotationSaved": "자동 전환 설정을 저장했습니다.", "providers.accountCount": "연결된 계정 {count}개", "providers.accountCountHelp": "현재 Claude 로그인과 ClaudeRipple에 추가한 계정의 합계입니다.", "providers.accountPoolTitle": "Claude 계정", "providers.accountPoolSubtitle": "대화는 한 계정에 고정됩니다. 응답 시작 전 한도·인증 문제가 생길 때만 다음 계정으로 전환합니다.", "providers.addClaudeAccount": "+ Claude 계정 추가", "providers.currentAccountHelp": "Claude Code 또는 Claude Desktop이 현재 쓰는 외부 로그인입니다. ClaudeRipple에서는 삭제할 수 없습니다.", "providers.addedAccountHelp": "ClaudeRipple이 이 컴퓨터의 사용자 전용 파일에 OAuth 로그인을 저장하고 자동 갱신합니다.", "providers.reauthAction": "다시 로그인", "providers.dangerZone": "추가 계정 관리", "providers.saveSettings": "설정 저장", "providers.connection": "연결", "providers.authentication": "인증 방식", "providers.rotationRequired": "계정을 여러 개 추가해도 자동 전환을 켜야 Claude 앱·Claude Code 요청에 사용됩니다.", "providers.anthropic": "Claude (Anthropic)", "providers.anthropicHelp": "Claude 구독 계정을 자동 전환하거나, Claude 모델을 Codex 등 OpenAI 방식 도구에 제공합니다.", "providers.anthropicIngressOnly": "자동 전환을 끄면 여기서 고른 모델은 Codex 같은 OpenAI 방식 도구에만 제공되고 Claude 앱의 모델 매핑에는 나타나지 않습니다.", "providers.anthropicPoolRouting": "자동 전환을 켜면 Claude 앱·Claude Code 요청을 대화별로 한 계정에 고정하고, 한도 응답 전까지 프롬프트 캐시를 유지합니다. 429 또는 인증 거부가 응답 전에 오면 다음 계정으로 전환합니다. 여러 구독 계정 사용은 Anthropic 약관과 계정 제한 대상이 될 수 있으므로 본인 계정과 허용된 용도로만 쓰세요.", "providers.anthropicLoginReuse": "로그인 재사용", "providers.anthropicPoolOn": "계정 자동 전환", "providers.anthropicPool": "Claude 계정 자동 전환", "providers.anthropicPoolHelp": "현재 Claude 로그인과 아래 추가 계정을 함께 씁니다. 대화 중에는 같은 계정을 유지하고, 한도 또는 인증 문제일 때만 바꿉니다.", "providers.anthropicCurrent": "현재 로그인", "providers.anthropicReauth": "재로그인 필요", "providers.anthropicNoAccounts": "ClaudeRipple에 추가한 계정이 없습니다.", "providers.anthropicRenamePrompt": "이 계정의 표시 이름", "providers.anthropicRemoveConfirm": "Claude 계정 {name}을(를) ClaudeRipple에서 지울까요?", "providers.anthropicLogoutAll": "추가 계정 모두 삭제", "providers.anthropicLogoutAllConfirm": "ClaudeRipple에 추가한 Claude 계정을 모두 지울까요? Claude Code와 Claude Desktop의 현재 로그인은 그대로입니다.", "providers.anthropicAuthClaudeCode": "Claude Code 로그인 재사용 (자동)", "providers.anthropicAuthApiKey": "API 키", "providers.anthropicCredentialsHelp": "Claude Code 로그인 또는 Anthropic API 키 중 사용할 방식을 고르세요.", "providers.anthropicSourceObserved": "Claude Desktop 세션에서 감지됨", "providers.anthropicSourceClaudeCode": "터미널 claude 로그인", "providers.anthropicSourceTokenFile": "ClaudeRipple 토큰", "providers.anthropicSourceMissing": "없음 — 아래 버튼으로 연결", "providers.anthropicLogin": "+ Claude 계정 추가", "providers.anthropicOAuthTitle": "Claude 계정 추가", "providers.anthropicOAuthWarning": "Claude 구독 OAuth를 타사 프록시에서 사용하는 방식은 Anthropic이 지원하는 통합이 아니며, 약관 또는 계정 제한 대상이 될 수 있습니다. 본인 계정과 허용된 용도로만 사용하세요.", "providers.anthropicOAuthAccept": "위험을 이해했으며 OAuth로 계속 진행합니다.", "providers.anthropicOAuthContinue": "OAuth로 계속", "providers.anthropicLogout": "연결 해제", "providers.anthropicLoginDone": "Claude 구독을 연결했습니다.", "providers.anthropicSignInBrowser": "브라우저에서 Claude에 로그인하세요. 창이 열리지 않았으면 아래 링크를 여세요.", "providers.anthropicSignInLink": "로그인 페이지 열기", "providers.anthropicSignInPaste": "브라우저에서 로그인한 뒤 표시되는 코드를 여기에 붙여넣으세요.", "providers.anthropicSignInSubmit": "코드 제출", "providers.anthropicSignInManual": "코드 붙여넣기로 다시 시도", "providers.anthropicSignInWaiting": "브라우저 승인을 기다리는 중… (최대 5분)", "providers.anthropicSignInFailed": "로그인에 실패했습니다", "providers.anthropicSignedInActive": "ClaudeRipple 로그인을 씁니다.", "providers.anthropicSignedInStandby": "ClaudeRipple 로그인도 저장돼 있습니다. 위 출처가 사라지면 이것을 씁니다.", "providers.add": "+ 프로바이더 추가", "providers.refresh": "모두 연결 확인", "providers.check": "연결 확인", "providers.checking": "확인 중", "providers.empty": "아직 연결한 프로바이더가 없습니다. 위의 버튼으로 추가하세요.", "providers.modelsCount": "쓰는 모델 {count}개: {names}", "providers.noModels": "고른 모델이 없습니다. 수정에서 골라 주세요.", "providers.removeConfirm": "프로바이더 {name}을(를) 지울까요? 이 프로바이더를 쓰는 모델 매핑도 함께 지워집니다.", "providers.choose": "프로바이더 추가", "providers.chooseHelp": "연결할 서비스를 하나 고르세요.", "providers.chatgpt": "ChatGPT 구독", "providers.chatgptHelp": "Codex가 포함된 ChatGPT 요금제(Plus·Pro·Business 등)로 GPT 모델을 씁니다. API 키 없이 로그인만 하면 됩니다.", "providers.presetHelp": "API 키만 붙여 넣으면 됩니다.", "providers.openaiGroup": "OpenAI 호환", "providers.openaiGroupHelp": "요청은 ClaudeRipple이 OpenAI API 형식으로 번역해 보냅니다.", "providers.verified": "공식 문서로 확인됨", "providers.custom": "직접 입력", "providers.customName": "새 프로바이더", "providers.customHelp": "목록에 없는 Anthropic 호환·OpenAI 호환 API를 주소와 키로 직접 연결합니다.", "providers.formatHelp": "서비스 문서에 적힌 형식을 고르세요. 주소가 /chat/completions로 끝나는 서비스는 OpenAI Chat Completions입니다.", "providers.addTitle": "프로바이더 추가", "providers.edit": "프로바이더 수정", "providers.name": "이름", "providers.nameHelp": "목록과 매핑에서 이 프로바이더를 부르는 이름입니다.", "providers.nameRequired": "이름을 입력하세요.", "providers.apiKey": "API 키", "providers.keyHelp": "프로바이더 사이트에서 발급한 API 키입니다. 이 컴퓨터의 설정 파일에만 저장됩니다.", "providers.keyPlaceholder": "API 키를 붙여 넣으세요", "providers.keySaved": "저장된 키를 그대로 씁니다. 바꾸려면 새 키를 붙여 넣으세요.", "providers.url": "API 주소 (URL)", "providers.urlHelp": "Anthropic 호환 엔드포인트의 기본 주소입니다. 서비스 문서와 다를 때만 바꾸세요.", "providers.openaiUrlHelp": "OpenAI 호환 API의 기본 주소입니다. 요청은 이 주소의 Chat Completions 또는 Responses endpoint로 번역됩니다.", "providers.wire": "API 형식 (wire)", "providers.wireHelp": "대부분 Chat Completions를 씁니다. 공급자가 stateless Responses API를 지원할 때만 Responses를 고르세요.", "providers.urlRequired": "올바른 URL을 입력하세요.", "providers.keyType": "인증 헤더", "providers.keyTypeHelp": "대부분 x-api-key입니다. 서비스 문서가 Authorization: Bearer를 쓰라고 하면 그것을 고르세요.", "providers.extraHeaders": "추가 HTTP 헤더", "providers.extraHeadersHelp": "서비스가 따로 요구하는 헤더만 '이름: 값' 형식으로 한 줄씩.", "providers.models": "쓸 모델", "providers.modelsHelp": "체크한 모델이 모델 매핑의 선택 목록에 나타납니다.", "providers.modelsFound": "프로바이더가 알려 준 모델 목록입니다.", "providers.modelsFallback": "목록을 받아오지 못해 알려진 기본 모델을 보여 줍니다.", "measure.running": "모델 능력 검사 중… ({done}/{total})", "measure.help": "각 모델이 어느 방식으로 답하는지, 어떤 추론 강도를 받는지 확인합니다. 공개 모델 카탈로그(models.dev)에 있으면 그 값을 쓰고, 없으면 모델에 직접 물어봅니다.", "measure.done": "모델 능력을 확인했습니다: {summary}", "measure.nothing": "확인된 것이 없습니다. 모델이 응답하지 않았습니다.", "measure.notEntitled": "{models}: 이 프로바이더의 자체 앱 안에서만 쓸 수 있는 모델입니다. 여기서는 고르셔도 쓰이지 않습니다.", "measure.failed": "모델 능력을 확인하지 못했습니다.", "measure.authFailed": "API 키가 거부돼 모델 능력을 확인하지 못했습니다.", "providers.probeNotEntitled": "키는 통과했지만 요금제가 거부했습니다.", "providerStatus.notEntitled": "요금제 제한", "providers.probeOk": "연결됐습니다.", "providers.probeFailed": "연결하지 못했습니다.", "providers.apiSoon": "잠시 후 다시 확인하세요.", "providers.showInPicker": "Claude 앱 피커에도 실제 이름으로 표시", "providers.pickerOffHint": "지금은 피커 모드가 꺼져 있어 체크해도 피커에 나타나지 않습니다. 저장할 때 켤지 물어봅니다.", "providers.pickerOffPrompt": "피커 모드가 꺼져 있어 이 모델들은 아직 Claude Desktop 피커에 나타나지 않습니다. 지금 켤까요? 인증서 신뢰를 위해 운영체제가 확인 창을 띄웁니다.", "providers.credentials": "로그인 방식", "providers.credentialsHelp": "ChatGPT 계정 토큰을 어디서 가져올지 정합니다.", "providers.authAuto": "자동 (직접 로그인이 있으면 그것, 없으면 Codex CLI 것)", "providers.authOwn": "ClaudeRipple에서 직접 로그인", "providers.authBorrow": "Codex CLI 로그인 재사용 (~/.codex/auth.json)", "providers.login": "ChatGPT 로그인 방법", "providers.loginHelp": "메뉴 막대 앱의 'ChatGPT 로그인…' 또는 터미널의 clauderipple login이 브라우저 로그인 창을 엽니다. 토큰은 이 컴퓨터에만 저장됩니다.", "providers.loginHint": "메뉴 막대 앱에서 'ChatGPT 로그인…'을 누르거나 터미널에서 clauderipple login을 실행하세요.", "providers.chatgptLogin": "ChatGPT 로그인", "providers.chatgptLoginWaiting": "브라우저에서 로그인을 마치세요. 완료되면 여기가 갱신됩니다.", "providers.chatgptLoginDone": "ChatGPT 로그인을 완료했습니다.", "providers.chatgptAccountsTitle": "ChatGPT 계정", "providers.chatgptAccountsSubtitle": "계정을 여러 개 추가하면 한 계정이 한도에 걸릴 때 다음 계정이 같은 요청을 이어받습니다. 대화는 답한 계정에 고정되어 캐시를 지킵니다. 순서는 목록 순서입니다.", "providers.addChatgptAccount": "+ ChatGPT 계정 추가", "providers.chatgptAddWaiting": "브라우저에서 추가할 계정으로 로그인하세요. 로그인 화면에 이미 로그인된 계정이 보이면 다른 계정을 고르세요.", "providers.chatgptAdded": "ChatGPT 계정을 추가했습니다.", "providers.chatgptNoAccounts": "로그인한 ChatGPT 계정이 없습니다.", "providers.chatgptAccountCount": "계정 {count}개", "providers.chatgptCodexHelp": "Codex CLI의 로그인입니다. ClaudeRipple은 읽기만 하고 갱신하지 않으며, 순서는 맨 뒤입니다.", "providers.chatgptOwnHelp": "ClaudeRipple이 이 컴퓨터의 사용자 전용 파일에 저장하고 자동 갱신합니다.", "providers.chatgptInUse": "지금 사용 중", "providers.chatgptStandby": "대기", "providers.chatgptPause": "일시정지", "providers.chatgptResume": "다시 사용", "providers.chatgptPaused": "일시정지됨", "providers.chatgptClearCooldown": "지금 복귀", "providers.chatgptRemoveConfirm": "ChatGPT 계정 {name}을(를) ClaudeRipple에서 지울까요?", "providers.chatgptReauthHelp": "로그인이 만료되었거나 거부되었습니다. 같은 계정으로 다시 로그인하면 이 자리에 복구됩니다.", "pool.coolingFor": "{duration} 뒤 복귀", "time.hoursMinutes": "{h}시간 {m}분", "time.minutes": "{m}분", "time.seconds": "{s}초", "quota.window5h": "5시간", "quota.windowWeek": "주간", "quota.windowHours": "{hours}시간", "quota.line": "{window} {percent}%", "quota.resetAt": "{time} 초기화", "providers.effortLevels": "추론 강도: {levels}", "providers.effortNone": "받지 않음", "providers.modelEffort": "강도 조절", "providers.modelNoEffort": "강도 없음", "providers.defaultEffort": "기본 추론 강도 (effort)", "providers.defaultEffortHelp": "매핑이나 앱에서 강도를 따로 정하지 않은 요청에 쓰는 값입니다.", "providers.identity": "모델에게 자기 정체 알리기 (identity)", "providers.append": "시스템 프롬프트 추가 문구", "providers.appendHelp": "모든 요청의 시스템 프롬프트 끝에 붙는 고정 문구입니다. 자주 바꾸면 프롬프트 캐시가 깨져 비용이 늘어납니다.",
|
|
13
|
+
"providers.title": "프로바이더", "providers.subtitle": "연결된 서비스와 계정을 한곳에서 관리합니다. 프로바이더를 고르면 설정과 계정이 오른쪽에 열립니다.", "providers.search": "프로바이더 검색…", "providers.overview": "개요", "providers.accounts": "계정", "providers.settings": "설정", "providers.modelsTab": "모델", "providers.selectedModels": "선택한 모델", "providers.modelsCountShort": "모델 {count}개", "providers.noSelection": "왼쪽에서 프로바이더를 선택하세요.", "providers.rotationOn": "자동 전환 켜짐", "providers.rotationOff": "자동 전환 꺼짐", "providers.rotationSaving": "자동 전환 저장 중…", "providers.rotationSaved": "자동 전환 설정을 저장했습니다.", "providers.accountCount": "연결된 계정 {count}개", "providers.accountCountHelp": "현재 Claude 로그인과 ClaudeRipple에 추가한 계정의 합계입니다.", "providers.accountPoolTitle": "Claude 계정", "providers.accountPoolSubtitle": "대화는 한 계정에 고정됩니다. 응답 시작 전 한도·인증 문제가 생길 때만 다음 계정으로 전환합니다.", "providers.addClaudeAccount": "+ Claude 계정 추가", "providers.currentAccountHelp": "Claude Code 또는 Claude Desktop이 현재 쓰는 외부 로그인입니다. ClaudeRipple에서는 삭제할 수 없습니다.", "providers.addedAccountHelp": "ClaudeRipple이 이 컴퓨터의 사용자 전용 파일에 OAuth 로그인을 저장하고 자동 갱신합니다.", "providers.reauthAction": "다시 로그인", "providers.preferBadge": "1순위", "providers.preferAction": "1순위로", "providers.preferHelp": "새 대화는 맨 위 1순위 계정부터 씁니다. 한도에 걸리면 다음 계정으로 넘어가고, 한도가 풀리면 새 대화부터 다시 1순위 계정을 씁니다. 진행 중인 대화는 캐시를 지키려고 쓰던 계정에 남습니다.","providers.dangerZone": "추가 계정 관리", "providers.saveSettings": "설정 저장", "providers.connection": "연결", "providers.authentication": "인증 방식", "providers.rotationRequired": "계정을 여러 개 추가해도 자동 전환을 켜야 Claude 앱·Claude Code 요청에 사용됩니다.", "providers.anthropic": "Claude (Anthropic)", "providers.anthropicHelp": "Claude 구독 계정을 자동 전환하거나, Claude 모델을 Codex 등 OpenAI 방식 도구에 제공합니다.", "providers.anthropicIngressOnly": "자동 전환을 끄면 여기서 고른 모델은 Codex 같은 OpenAI 방식 도구에만 제공되고 Claude 앱의 모델 매핑에는 나타나지 않습니다.", "providers.anthropicPoolRouting": "자동 전환을 켜면 Claude 앱·Claude Code 요청을 대화별로 한 계정에 고정하고, 한도 응답 전까지 프롬프트 캐시를 유지합니다. 429 또는 인증 거부가 응답 전에 오면 다음 계정으로 전환합니다. 여러 구독 계정 사용은 Anthropic 약관과 계정 제한 대상이 될 수 있으므로 본인 계정과 허용된 용도로만 쓰세요.", "providers.anthropicLoginReuse": "로그인 재사용", "providers.anthropicPoolOn": "계정 자동 전환", "providers.anthropicPool": "Claude 계정 자동 전환", "providers.anthropicPoolHelp": "현재 Claude 로그인과 아래 추가 계정을 함께 씁니다. 대화 중에는 같은 계정을 유지하고, 한도 또는 인증 문제일 때만 바꿉니다.", "providers.anthropicCurrent": "현재 로그인", "providers.anthropicReauth": "재로그인 필요", "providers.anthropicNoAccounts": "ClaudeRipple에 추가한 계정이 없습니다.", "providers.anthropicRenamePrompt": "이 계정의 표시 이름", "providers.anthropicRemoveConfirm": "Claude 계정 {name}을(를) ClaudeRipple에서 지울까요?", "providers.anthropicLogoutAll": "추가 계정 모두 삭제", "providers.anthropicLogoutAllConfirm": "ClaudeRipple에 추가한 Claude 계정을 모두 지울까요? Claude Code와 Claude Desktop의 현재 로그인은 그대로입니다.", "providers.anthropicAuthClaudeCode": "Claude Code 로그인 재사용 (자동)", "providers.anthropicAuthApiKey": "API 키", "providers.anthropicCredentialsHelp": "Claude Code 로그인 또는 Anthropic API 키 중 사용할 방식을 고르세요.", "providers.anthropicSourceObserved": "Claude Desktop 세션에서 감지됨", "providers.anthropicSourceClaudeCode": "터미널 claude 로그인", "providers.anthropicSourceTokenFile": "ClaudeRipple 토큰", "providers.anthropicSourceMissing": "없음 — 아래 버튼으로 연결", "providers.anthropicLogin": "+ Claude 계정 추가", "providers.anthropicOAuthTitle": "Claude 계정 추가", "providers.anthropicOAuthWarning": "Claude 구독 OAuth를 타사 프록시에서 사용하는 방식은 Anthropic이 지원하는 통합이 아니며, 약관 또는 계정 제한 대상이 될 수 있습니다. 본인 계정과 허용된 용도로만 사용하세요.", "providers.anthropicOAuthAccept": "위험을 이해했으며 OAuth로 계속 진행합니다.", "providers.anthropicOAuthContinue": "OAuth로 계속", "providers.anthropicLogout": "연결 해제", "providers.anthropicLoginDone": "Claude 구독을 연결했습니다.", "providers.anthropicSignInBrowser": "브라우저에서 Claude에 로그인하세요. 창이 열리지 않았으면 아래 링크를 여세요.", "providers.anthropicSignInLink": "로그인 페이지 열기", "providers.anthropicSignInPaste": "브라우저에서 로그인한 뒤 표시되는 코드를 여기에 붙여넣으세요.", "providers.anthropicSignInSubmit": "코드 제출", "providers.anthropicSignInManual": "코드 붙여넣기로 다시 시도", "providers.anthropicSignInWaiting": "브라우저 승인을 기다리는 중… (최대 5분)", "providers.anthropicSignInFailed": "로그인에 실패했습니다", "providers.anthropicSignedInActive": "ClaudeRipple 로그인을 씁니다.", "providers.anthropicSignedInStandby": "ClaudeRipple 로그인도 저장돼 있습니다. 위 출처가 사라지면 이것을 씁니다.", "providers.add": "+ 프로바이더 추가", "providers.refresh": "모두 연결 확인", "providers.check": "연결 확인", "providers.checking": "확인 중", "providers.empty": "아직 연결한 프로바이더가 없습니다. 위의 버튼으로 추가하세요.", "providers.modelsCount": "쓰는 모델 {count}개: {names}", "providers.noModels": "고른 모델이 없습니다. 수정에서 골라 주세요.", "providers.removeConfirm": "프로바이더 {name}을(를) 지울까요? 이 프로바이더를 쓰는 모델 매핑도 함께 지워집니다.", "providers.choose": "프로바이더 추가", "providers.chooseHelp": "연결할 서비스를 하나 고르세요.", "providers.chatgpt": "ChatGPT 구독", "providers.chatgptHelp": "Codex가 포함된 ChatGPT 요금제(Plus·Pro·Business 등)로 GPT 모델을 씁니다. API 키 없이 로그인만 하면 됩니다.", "providers.presetHelp": "API 키만 붙여 넣으면 됩니다.", "providers.openaiGroup": "OpenAI 호환", "providers.openaiGroupHelp": "요청은 ClaudeRipple이 OpenAI API 형식으로 번역해 보냅니다.", "providers.verified": "공식 문서로 확인됨", "providers.custom": "직접 입력", "providers.customName": "새 프로바이더", "providers.customHelp": "목록에 없는 Anthropic 호환·OpenAI 호환 API를 주소와 키로 직접 연결합니다.", "providers.formatHelp": "서비스 문서에 적힌 형식을 고르세요. 주소가 /chat/completions로 끝나는 서비스는 OpenAI Chat Completions입니다.", "providers.addTitle": "프로바이더 추가", "providers.edit": "프로바이더 수정", "providers.name": "이름", "providers.nameHelp": "목록과 매핑에서 이 프로바이더를 부르는 이름입니다.", "providers.nameRequired": "이름을 입력하세요.", "providers.apiKey": "API 키", "providers.keyHelp": "프로바이더 사이트에서 발급한 API 키입니다. 이 컴퓨터의 설정 파일에만 저장됩니다.", "providers.keyPlaceholder": "API 키를 붙여 넣으세요", "providers.keySaved": "저장된 키를 그대로 씁니다. 바꾸려면 새 키를 붙여 넣으세요.", "providers.url": "API 주소 (URL)", "providers.urlHelp": "Anthropic 호환 엔드포인트의 기본 주소입니다. 서비스 문서와 다를 때만 바꾸세요.", "providers.openaiUrlHelp": "OpenAI 호환 API의 기본 주소입니다. 요청은 이 주소의 Chat Completions 또는 Responses endpoint로 번역됩니다.", "providers.wire": "API 형식 (wire)", "providers.wireHelp": "대부분 Chat Completions를 씁니다. 공급자가 stateless Responses API를 지원할 때만 Responses를 고르세요.", "providers.urlRequired": "올바른 URL을 입력하세요.", "providers.keyType": "인증 헤더", "providers.keyTypeHelp": "대부분 x-api-key입니다. 서비스 문서가 Authorization: Bearer를 쓰라고 하면 그것을 고르세요.", "providers.extraHeaders": "추가 HTTP 헤더", "providers.extraHeadersHelp": "서비스가 따로 요구하는 헤더만 '이름: 값' 형식으로 한 줄씩.", "providers.models": "쓸 모델", "providers.modelsHelp": "체크한 모델이 모델 매핑의 선택 목록에 나타납니다.", "providers.modelsFound": "프로바이더가 알려 준 모델 목록입니다.", "providers.modelsFallback": "목록을 받아오지 못해 알려진 기본 모델을 보여 줍니다.", "measure.running": "모델 능력 검사 중… ({done}/{total})", "measure.help": "각 모델이 어느 방식으로 답하는지, 어떤 추론 강도를 받는지 확인합니다. 공개 모델 카탈로그(models.dev)에 있으면 그 값을 쓰고, 없으면 모델에 직접 물어봅니다.", "measure.done": "모델 능력을 확인했습니다: {summary}", "measure.nothing": "확인된 것이 없습니다. 모델이 응답하지 않았습니다.", "measure.notEntitled": "{models}: 이 프로바이더의 자체 앱 안에서만 쓸 수 있는 모델입니다. 여기서는 고르셔도 쓰이지 않습니다.", "measure.failed": "모델 능력을 확인하지 못했습니다.", "measure.authFailed": "API 키가 거부돼 모델 능력을 확인하지 못했습니다.", "providers.probeNotEntitled": "키는 통과했지만 요금제가 거부했습니다.", "providerStatus.notEntitled": "요금제 제한", "providers.probeOk": "연결됐습니다.", "providers.probeFailed": "연결하지 못했습니다.", "providers.apiSoon": "잠시 후 다시 확인하세요.", "providers.showInPicker": "Claude 앱 피커에도 실제 이름으로 표시", "providers.pickerOffHint": "지금은 피커 모드가 꺼져 있어 체크해도 피커에 나타나지 않습니다. 저장할 때 켤지 물어봅니다.", "providers.pickerOffPrompt": "피커 모드가 꺼져 있어 이 모델들은 아직 Claude Desktop 피커에 나타나지 않습니다. 지금 켤까요? 인증서 신뢰를 위해 운영체제가 확인 창을 띄웁니다.", "providers.credentials": "로그인 방식", "providers.credentialsHelp": "ChatGPT 계정 토큰을 어디서 가져올지 정합니다.", "providers.authAuto": "자동 (직접 로그인이 있으면 그것, 없으면 Codex CLI 것)", "providers.authOwn": "ClaudeRipple에서 직접 로그인", "providers.authBorrow": "Codex CLI 로그인 재사용 (~/.codex/auth.json)", "providers.login": "ChatGPT 로그인 방법", "providers.loginHelp": "메뉴 막대 앱의 'ChatGPT 로그인…' 또는 터미널의 clauderipple login이 브라우저 로그인 창을 엽니다. 토큰은 이 컴퓨터에만 저장됩니다.", "providers.loginHint": "메뉴 막대 앱에서 'ChatGPT 로그인…'을 누르거나 터미널에서 clauderipple login을 실행하세요.", "providers.chatgptLogin": "ChatGPT 로그인", "providers.chatgptLoginWaiting": "브라우저에서 로그인을 마치세요. 완료되면 여기가 갱신됩니다.", "providers.chatgptLoginDone": "ChatGPT 로그인을 완료했습니다.", "providers.chatgptAccountsTitle": "ChatGPT 계정", "providers.chatgptAccountsSubtitle": "계정을 여러 개 추가하면 한 계정이 한도에 걸릴 때 다음 계정이 같은 요청을 이어받습니다. 대화는 답한 계정에 고정되어 캐시를 지킵니다. 순서는 목록 순서입니다.", "providers.addChatgptAccount": "+ ChatGPT 계정 추가", "providers.chatgptAddWaiting": "브라우저에서 추가할 계정으로 로그인하세요. 로그인 화면에 이미 로그인된 계정이 보이면 다른 계정을 고르세요.", "providers.chatgptAdded": "ChatGPT 계정을 추가했습니다.", "providers.chatgptNoAccounts": "로그인한 ChatGPT 계정이 없습니다.", "providers.chatgptAccountCount": "계정 {count}개", "providers.chatgptCodexHelp": "Codex CLI의 로그인입니다. ClaudeRipple은 읽기만 하고 갱신하지 않으며, 순서는 맨 뒤입니다.", "providers.chatgptOwnHelp": "ClaudeRipple이 이 컴퓨터의 사용자 전용 파일에 저장하고 자동 갱신합니다.", "providers.chatgptInUse": "지금 사용 중", "providers.chatgptStandby": "대기", "providers.chatgptPause": "일시정지", "providers.chatgptResume": "다시 사용", "providers.chatgptPaused": "일시정지됨", "providers.chatgptClearCooldown": "지금 복귀", "providers.chatgptRemoveConfirm": "ChatGPT 계정 {name}을(를) ClaudeRipple에서 지울까요?", "providers.chatgptReauthHelp": "로그인이 만료되었거나 거부되었습니다. 같은 계정으로 다시 로그인하면 이 자리에 복구됩니다.", "pool.coolingFor": "{duration} 뒤 복귀", "time.hoursMinutes": "{h}시간 {m}분", "time.minutes": "{m}분", "time.seconds": "{s}초", "quota.window5h": "5시간", "quota.windowWeek": "주간", "quota.windowHours": "{hours}시간", "quota.line": "{window} {percent}%", "quota.resetAt": "{time} 초기화", "providers.effortLevels": "추론 강도: {levels}", "providers.effortNone": "받지 않음", "providers.modelEffort": "강도 조절", "providers.modelNoEffort": "강도 없음", "providers.defaultEffort": "기본 추론 강도 (effort)", "providers.defaultEffortHelp": "매핑이나 앱에서 강도를 따로 정하지 않은 요청에 쓰는 값입니다.", "providers.identity": "모델에게 자기 정체 알리기 (identity)", "providers.append": "시스템 프롬프트 추가 문구", "providers.appendHelp": "모든 요청의 시스템 프롬프트 끝에 붙는 고정 문구입니다. 자주 바꾸면 프롬프트 캐시가 깨져 비용이 늘어납니다.",
|
|
14
14
|
"providerStatus.connected": "연결됨", "providerStatus.keyNeeded": "키 확인 필요", "providerStatus.loginNeeded": "로그인 필요", "providerStatus.disconnected": "연결 안 됨", "providerStatus.checking": "확인 중",
|
|
15
15
|
"clients.title": "클라이언트", "clients.subtitle": "ClaudeRipple을 쓰는 앱을 설정합니다.", "clients.desktop.title": "Claude Desktop", "clients.desktop.help": "모델 피커에 프로바이더 모델을 실제 이름으로 표시할지 정합니다.", "clients.codex.title": "Codex (앱·CLI)", "clients.codex.help": "Codex 설정(~/.codex/config.toml)에 ClaudeRipple 프로바이더를 추가합니다. 터미널은 `codex --profile clauderipple -m <모델>`, Codex 앱은 설정에서 프로바이더 clauderipple을 기본으로 고르면 됩니다.", "clients.codex.note": "켜면 Codex의 GPT 요청도 ClaudeRipple을 거칩니다. Codex는 로그인을 그대로 유지하고, ChatGPT 계정을 여러 개 추가해 두면 한 계정이 한도에 걸릴 때 로그아웃 없이 다음 계정이 이어받습니다. 아래 목록은 Codex에 추가로 보이는 Claude·Anthropic 호환 모델입니다.", "clients.codex.turnOn": "Codex에서 ClaudeRipple 사용", "clients.codex.turnOff": "Codex 연결 해제", "clients.claudeCode.title": "터미널 Claude Code", "clients.claudeCode.help": "터미널 `claude`도 같은 라우터를 씁니다. `/model <이름>`으로 고르세요.",
|
|
16
16
|
"logs.title": "로그", "logs.requests": "요청", "logs.raw": "원본 로그", "logs.lastHour": "최근 1시간", "logs.autoscroll": "자동 스크롤", "logs.polling": "3초마다 갱신", "logs.rawPolling": "3초마다 갱신 · 최근 200줄", "logs.provider": "프로바이더", "logs.all": "전체", "logs.showCountTokens": "토큰 수 계산 표시", "logs.empty": "아직 요청이 없습니다.", "logs.summary.requests": "요청 수", "logs.summary.success": "성공 / 실패", "logs.summary.input": "입력 토큰", "logs.summary.output": "출력 토큰", "logs.summary.duration": "평균 응답 시간", "logs.cacheHit": "캐시 {percent}%", "logs.th.time": "시간", "logs.th.models": "모델", "logs.th.effort": "effort", "logs.th.input": "입력", "logs.th.output": "출력", "logs.th.duration": "소요", "logs.th.status": "상태", "logs.status.ok": "성공", "logs.status.error": "오류", "logs.status.resent": "다시 보냄", "logs.status.resentHelp": "실패가 아닙니다. 클라이언트가 이 요청을 스스로 다시 보냈습니다.", "logs.detail.id": "요청 ID", "logs.detail.kind": "종류", "logs.detail.stop": "종료 사유", "logs.detail.uncached": "새로 계산한 입력", "logs.detail.cacheRead": "캐시 읽기", "logs.detail.cacheWrite": "캐시 기록", "logs.detail.note": "메모", "logs.none": "없음", "logs.seconds": "{value}초",
|
|
@@ -85,7 +85,7 @@ const I18N = {
|
|
|
85
85
|
"update.check": "Check for updates", "update.checking": "Checking…", "update.latest": "This is the latest version.", "update.install": "Update to {latest}", "update.download": "Get {latest}", "update.checkout": "{latest} is out. This is a source checkout; update it with git pull.", "update.running": "Updating…", "update.checkFailed": "Could not check for updates.", "update.confirm": "Update ClaudeRipple {current} → {latest}? The router restarts on the new version once requests in flight have finished, and this window reloads by itself.", "update.started": "The update has started. This window reloads when the router is back on the new version. Its output is in ~/.clauderipple/logs/update.log.", "update.failed": "Could not start the update.",
|
|
86
86
|
"picker.title": "Model picker","picker.statusHelp": "On: provider models appear under their real names in the Claude Desktop picker. Off: they answer behind Claude names per the mapping.", "picker.state": "State", "picker.on": "On", "picker.off": "Off", "picker.turnOn": "Turn picker on", "picker.turnOff": "Turn picker off", "picker.last": "Most recently showed {count} models.", "picker.restartHelp": "After turning it on, quit Claude Desktop completely and open it again.", "picker.confirmOn": "Your operating system will ask you to confirm trusting the certificate (macOS asks for your login password, Windows shows a certificate dialog). ClaudeRipple never sees your password. Continue?", "picker.working": "Working.", "picker.doneOn": "Turned on. Quit Claude Desktop completely and open it again.", "picker.doneOff": "Turned off. Quit Claude Desktop completely and open it again.",
|
|
87
87
|
"slots.title": "Model mapping", "slots.subtitle": "Decide which model actually answers when you pick a Claude model in the app. Changes save automatically.", "slots.pickerHelp": "On: provider models appear under their real names in the Claude Desktop picker. Off: they answer behind Claude names per the mapping above.", "slots.pickerModels": "Models to put in the picker", "slots.pickerModelsHelp": "Checked models appear in the Claude Desktop picker; picking one makes it answer directly. Newly added models are selectable from the next session (a running session keeps the list it started with).", "slots.add": "+ Add mapping", "slots.th.claude": "Picked in the Claude app", "slots.th.target": "Actually answered by", "slots.th.effort": "Reasoning effort", "slots.passthrough": "Claude answers as usual", "slots.noProviderModels": "Connect a provider and choose its models first.", "slots.noChanges": "No mappings. Every model answers as Claude.", "slots.duplicate": "A Claude model can be mapped only once.", "slots.effortHint": "If the effort chosen in the app is unsupported by the model, ClaudeRipple sends the nearest level instead. (For example: xhigh → high)", "slots.noEffort": "This model does not take a reasoning effort", "pool.ready": "in use", "pool.cooling": "back in {seconds}s", "pool.quarantined": "rejected — check the key", "slots.modelSlots": "Claude Code's own model slots", "slots.modelSlotsHelp": "Claude Code chooses these before a request exists, so a mapping cannot reach them. Left empty, Claude answers them and they cost Claude quota. Web search in particular runs on the small, fast model, so a routed session still searches on Claude quota until that one is moved. Changes are written to settings.json on install.", "slots.noWebSearch": "no web search here", "slots.noWebSearchWarn": "Saved, but web search will fail on this model — ClaudeRipple cannot run a search through it. The model itself may be able to; leaving this on the default lets Claude search.", "slots.slotDefault": "default (Claude)", "slots.slot.smallFast": "Small, fast model", "slots.slot.subagent": "Subagents", "slots.slot.main": "Session default", "slots.slotHelp.smallFast": "web search, titles, classifiers", "slots.slotHelp.subagent": "a subagent that names no model of its own", "slots.slotHelp.main": "what a session starts on", "slots.slotSaved": "Model slots saved. They take effect in a new session.", "slots.windowGlobal": "default", "slots.windowHelp": "This model's context window, in tokens. Routed models do not share one, so each carries its own. Left blank it takes what the provider reported, and otherwise the global default. Enter the model's real window, not a compaction point.",
|
|
88
|
-
"providers.title": "Providers", "providers.subtitle": "Manage connected services and accounts in one place. Pick a provider to open its settings and accounts.", "providers.search": "Search providers…", "providers.overview": "Overview", "providers.accounts": "Accounts", "providers.settings": "Settings", "providers.modelsTab": "Models", "providers.selectedModels": "Selected models", "providers.modelsCountShort": "{count} models", "providers.noSelection": "Select a provider on the left.", "providers.rotationOn": "Rotation on", "providers.rotationOff": "Rotation off", "providers.rotationSaving": "Saving rotation…", "providers.rotationSaved": "Account rotation saved.", "providers.accountCount": "{count} connected accounts", "providers.accountCountHelp": "Includes the current Claude login and accounts added to ClaudeRipple.", "providers.accountPoolTitle": "Claude accounts", "providers.accountPoolSubtitle": "A conversation stays on one account. ClaudeRipple switches only when quota or authentication fails before output starts.", "providers.addClaudeAccount": "+ Add Claude account", "providers.currentAccountHelp": "The external sign-in currently used by Claude Code or Claude Desktop. ClaudeRipple cannot remove it.", "providers.addedAccountHelp": "ClaudeRipple stores this OAuth grant on this computer and refreshes it automatically.", "providers.reauthAction": "Sign in again", "providers.dangerZone": "Added account management", "providers.saveSettings": "Save settings", "providers.connection": "Connection", "providers.authentication": "Authentication", "providers.rotationRequired": "Added accounts are used for Claude app and Claude Code requests only when account rotation is on.", "providers.anthropic": "Claude (Anthropic)", "providers.anthropicHelp": "Rotate Claude subscription accounts, or serve Claude models to OpenAI-style tools such as Codex.", "providers.anthropicIngressOnly": "With account rotation off, the models chosen here are served only to OpenAI-style tools such as Codex and do not appear in Claude app mappings.", "providers.anthropicPoolRouting": "With account rotation on, Claude app and Claude Code requests stay on one account per conversation to preserve the prompt cache. A 429 or authentication refusal before the response starts moves the request to the next account. Using multiple subscriptions may be subject to Anthropic's terms and account restrictions; use only your own authorized accounts.", "providers.anthropicLoginReuse": "Reuse sign-in", "providers.anthropicPoolOn": "Account rotation", "providers.anthropicPool": "Rotate Claude accounts automatically", "providers.anthropicPoolHelp": "Uses the current Claude login plus the additional accounts below. A conversation stays on its account unless quota or authentication fails.", "providers.anthropicCurrent": "Current login", "providers.anthropicReauth": "Sign in again", "providers.anthropicNoAccounts": "No accounts have been added to ClaudeRipple.", "providers.anthropicRenamePrompt": "Display name for this account", "providers.anthropicRemoveConfirm": "Remove Claude account {name} from ClaudeRipple?", "providers.anthropicLogoutAll": "Remove all added accounts", "providers.anthropicLogoutAllConfirm": "Remove every Claude account added to ClaudeRipple? The current Claude Code and Claude Desktop login is not changed.", "providers.anthropicAuthClaudeCode": "Reuse Claude Code sign-in (automatic)", "providers.anthropicAuthApiKey": "API key", "providers.anthropicCredentialsHelp": "Choose your Claude Code sign-in or an Anthropic API key.", "providers.anthropicSourceObserved": "Detected from a Claude Desktop session", "providers.anthropicSourceClaudeCode": "Terminal Claude sign-in", "providers.anthropicSourceTokenFile": "ClaudeRipple token", "providers.anthropicSourceMissing": "None — connect below", "providers.anthropicLogin": "+ Add Claude account", "providers.anthropicOAuthTitle": "Add Claude account", "providers.anthropicOAuthWarning": "Using Claude subscription OAuth through a third-party proxy is not an Anthropic-supported integration and may be subject to terms or account restrictions. Use only your own accounts for authorized purposes.", "providers.anthropicOAuthAccept": "I understand the risk and want to continue with OAuth.", "providers.anthropicOAuthContinue": "Continue with OAuth", "providers.anthropicLogout": "Disconnect", "providers.anthropicLoginDone": "Claude subscription connected.", "providers.anthropicSignInBrowser": "Sign in to Claude in your browser. If no window opened, use the link below.", "providers.anthropicSignInLink": "Open the sign-in page", "providers.anthropicSignInPaste": "After signing in, paste the code the page shows here.", "providers.anthropicSignInSubmit": "Submit code", "providers.anthropicSignInManual": "Try again by pasting a code", "providers.anthropicSignInWaiting": "Waiting for the browser… (up to 5 minutes)", "providers.anthropicSignInFailed": "Sign-in failed", "providers.anthropicSignedInActive": "Using the ClaudeRipple sign-in.", "providers.anthropicSignedInStandby": "A ClaudeRipple sign-in is stored too, and takes over if the source above goes away.", "providers.add": "+ Add provider", "providers.refresh": "Test all", "providers.check": "Test connection", "providers.checking": "Checking", "providers.empty": "No providers connected yet. Add one with the button above.", "providers.modelsCount": "{count} models in use: {names}", "providers.noModels": "No models chosen. Pick some under Edit.", "providers.removeConfirm": "Remove provider {name}? Model mappings that use it are removed too.", "providers.choose": "Add provider", "providers.chooseHelp": "Pick the service to connect.", "providers.chatgpt": "ChatGPT subscription", "providers.chatgptHelp": "Use GPT models through any ChatGPT plan that includes Codex (Plus, Pro, Business…). Sign-in only, no API key.", "providers.presetHelp": "Paste an API key and you are done.", "providers.openaiGroup": "OpenAI-compatible", "providers.openaiGroupHelp": "ClaudeRipple translates requests into the OpenAI API format.", "providers.verified": "Verified against official docs", "providers.custom": "Enter manually", "providers.customName": "New provider", "providers.customHelp": "Connect any Anthropic- or OpenAI-compatible API by URL and key.", "providers.formatHelp": "Pick the format the service docs name. A service whose endpoint ends in /chat/completions is OpenAI Chat Completions.", "providers.addTitle": "Add provider", "providers.edit": "Edit provider", "providers.name": "Name", "providers.nameHelp": "How this provider is called in lists and mappings.", "providers.nameRequired": "Enter a name.", "providers.apiKey": "API key", "providers.keyHelp": "The API key issued on the provider's site. Stored only in this computer's config file.", "providers.keyPlaceholder": "Paste your API key", "providers.keySaved": "The saved key stays in use. Paste a new key to replace it.", "providers.url": "API base URL", "providers.urlHelp": "Base URL of the Anthropic-compatible endpoint. Change only if the service docs differ.", "providers.openaiUrlHelp": "Base URL of the OpenAI-compatible API. Requests are translated to its Chat Completions or Responses endpoint.", "providers.wire": "API wire format", "providers.wireHelp": "Use Chat Completions for most providers. Select Responses only when the provider supports the stateless Responses API.", "providers.urlRequired": "Enter a valid URL.", "providers.keyType": "Auth header", "providers.keyTypeHelp": "Usually x-api-key. Pick Authorization: Bearer only if the service docs say so.", "providers.extraHeaders": "Extra HTTP headers", "providers.extraHeadersHelp": "Only headers the service requires, one 'name: value' per line.", "providers.models": "Models to use", "providers.modelsHelp": "Checked models appear in the Model mapping list.", "providers.modelsFound": "Model list reported by the provider.", "providers.modelsFallback": "Could not fetch the list; showing known default models.", "measure.running": "Measuring model capabilities… ({done}/{total})", "measure.help": "Checks which wire each model answers on and which reasoning efforts it takes — from the public model catalogue (models.dev) when it lists the model, otherwise by asking the model.", "measure.done": "Measured: {summary}", "measure.nothing": "Nothing was measured: no model answered.", "measure.notEntitled": "{models}: usable only from the provider\u2019s own app. Ticking it here will not make it work.", "measure.failed": "Could not measure model capabilities.", "measure.authFailed": "The API key was refused, so nothing could be measured.", "providers.probeNotEntitled": "The key was accepted; the plan refused.", "providerStatus.notEntitled": "Plan refused", "providers.probeOk": "Connected.", "providers.probeFailed": "Could not connect.", "providers.apiSoon": "Please try again shortly.", "providers.showInPicker": "Also show by real name in the Claude app picker", "providers.pickerOffHint": "Picker mode is off right now, so ticking this does not show them yet. You will be asked to turn it on when you save.", "providers.pickerOffPrompt": "Picker mode is off, so these models will not appear in the Claude Desktop picker yet. Turn it on now? Your operating system will ask you to confirm trusting the certificate.", "providers.credentials": "Sign-in source", "providers.credentialsHelp": "Where the ChatGPT account token comes from.", "providers.authAuto": "Auto (own sign-in if present, else Codex CLI's)", "providers.authOwn": "Sign in with ClaudeRipple", "providers.authBorrow": "Reuse Codex CLI sign-in (~/.codex/auth.json)", "providers.login": "How to sign in", "providers.loginHelp": "'Sign in to ChatGPT…' in the menu-bar app or clauderipple login in a terminal opens the browser sign-in. Tokens stay on this computer.", "providers.loginHint": "Use 'Sign in to ChatGPT…' in the menu-bar app or run clauderipple login in a terminal.", "providers.chatgptLogin": "Sign in to ChatGPT", "providers.chatgptLoginWaiting": "Finish signing in in your browser. This will update when it is done.", "providers.chatgptLoginDone": "ChatGPT sign-in complete.", "providers.chatgptAccountsTitle": "ChatGPT accounts", "providers.chatgptAccountsSubtitle": "With several accounts, when one hits its limit the next one takes over the same request. A conversation stays on the account that answered, to keep its cache. Accounts are used in list order.", "providers.addChatgptAccount": "+ Add ChatGPT account", "providers.chatgptAddWaiting": "Sign in with the account to add in your browser. If it shows an account you already added, pick another.", "providers.chatgptAdded": "ChatGPT account added.", "providers.chatgptNoAccounts": "No ChatGPT account is signed in.", "providers.chatgptAccountCount": "{count} accounts", "providers.chatgptCodexHelp": "The Codex CLI's own login. ClaudeRipple only reads it, never refreshes it, and uses it last.", "providers.chatgptOwnHelp": "ClaudeRipple stores this login in a file only your user can read and refreshes it automatically.", "providers.chatgptInUse": "in use now", "providers.chatgptStandby": "standby", "providers.chatgptPause": "Pause", "providers.chatgptResume": "Resume", "providers.chatgptPaused": "paused", "providers.chatgptClearCooldown": "Use now", "providers.chatgptRemoveConfirm": "Remove ChatGPT account {name} from ClaudeRipple?", "providers.chatgptReauthHelp": "This login expired or was refused. Signing in again with the same account restores it in place.", "pool.coolingFor": "back in {duration}", "time.hoursMinutes": "{h}h {m}m", "time.minutes": "{m}m", "time.seconds": "{s}s", "quota.window5h": "5h", "quota.windowWeek": "weekly", "quota.windowHours": "{hours}h", "quota.line": "{window} {percent}%", "quota.resetAt": "resets {time}", "providers.effortLevels": "Reasoning effort: {levels}", "providers.effortNone": "Does not take reasoning effort", "providers.modelEffort": "effort", "providers.modelNoEffort": "no effort", "providers.defaultEffort": "Default reasoning effort", "providers.defaultEffortHelp": "Used when neither the mapping nor the app sets an effort.", "providers.identity": "Tell the model what it is (identity)", "providers.append": "System prompt addendum", "providers.appendHelp": "Fixed text appended to every request's system prompt. Changing it often breaks the prompt cache and raises cost.",
|
|
88
|
+
"providers.title": "Providers", "providers.subtitle": "Manage connected services and accounts in one place. Pick a provider to open its settings and accounts.", "providers.search": "Search providers…", "providers.overview": "Overview", "providers.accounts": "Accounts", "providers.settings": "Settings", "providers.modelsTab": "Models", "providers.selectedModels": "Selected models", "providers.modelsCountShort": "{count} models", "providers.noSelection": "Select a provider on the left.", "providers.rotationOn": "Rotation on", "providers.rotationOff": "Rotation off", "providers.rotationSaving": "Saving rotation…", "providers.rotationSaved": "Account rotation saved.", "providers.accountCount": "{count} connected accounts", "providers.accountCountHelp": "Includes the current Claude login and accounts added to ClaudeRipple.", "providers.accountPoolTitle": "Claude accounts", "providers.accountPoolSubtitle": "A conversation stays on one account. ClaudeRipple switches only when quota or authentication fails before output starts.", "providers.addClaudeAccount": "+ Add Claude account", "providers.currentAccountHelp": "The external sign-in currently used by Claude Code or Claude Desktop. ClaudeRipple cannot remove it.", "providers.addedAccountHelp": "ClaudeRipple stores this OAuth grant on this computer and refreshes it automatically.", "providers.reauthAction": "Sign in again", "providers.preferBadge": "First", "providers.preferAction": "Use first", "providers.preferHelp": "New conversations start on the account at the top. When it hits its limit they move to the next one, and once it resets new conversations go back to it. A conversation already running stays on its account to keep its cache.","providers.dangerZone": "Added account management", "providers.saveSettings": "Save settings", "providers.connection": "Connection", "providers.authentication": "Authentication", "providers.rotationRequired": "Added accounts are used for Claude app and Claude Code requests only when account rotation is on.", "providers.anthropic": "Claude (Anthropic)", "providers.anthropicHelp": "Rotate Claude subscription accounts, or serve Claude models to OpenAI-style tools such as Codex.", "providers.anthropicIngressOnly": "With account rotation off, the models chosen here are served only to OpenAI-style tools such as Codex and do not appear in Claude app mappings.", "providers.anthropicPoolRouting": "With account rotation on, Claude app and Claude Code requests stay on one account per conversation to preserve the prompt cache. A 429 or authentication refusal before the response starts moves the request to the next account. Using multiple subscriptions may be subject to Anthropic's terms and account restrictions; use only your own authorized accounts.", "providers.anthropicLoginReuse": "Reuse sign-in", "providers.anthropicPoolOn": "Account rotation", "providers.anthropicPool": "Rotate Claude accounts automatically", "providers.anthropicPoolHelp": "Uses the current Claude login plus the additional accounts below. A conversation stays on its account unless quota or authentication fails.", "providers.anthropicCurrent": "Current login", "providers.anthropicReauth": "Sign in again", "providers.anthropicNoAccounts": "No accounts have been added to ClaudeRipple.", "providers.anthropicRenamePrompt": "Display name for this account", "providers.anthropicRemoveConfirm": "Remove Claude account {name} from ClaudeRipple?", "providers.anthropicLogoutAll": "Remove all added accounts", "providers.anthropicLogoutAllConfirm": "Remove every Claude account added to ClaudeRipple? The current Claude Code and Claude Desktop login is not changed.", "providers.anthropicAuthClaudeCode": "Reuse Claude Code sign-in (automatic)", "providers.anthropicAuthApiKey": "API key", "providers.anthropicCredentialsHelp": "Choose your Claude Code sign-in or an Anthropic API key.", "providers.anthropicSourceObserved": "Detected from a Claude Desktop session", "providers.anthropicSourceClaudeCode": "Terminal Claude sign-in", "providers.anthropicSourceTokenFile": "ClaudeRipple token", "providers.anthropicSourceMissing": "None — connect below", "providers.anthropicLogin": "+ Add Claude account", "providers.anthropicOAuthTitle": "Add Claude account", "providers.anthropicOAuthWarning": "Using Claude subscription OAuth through a third-party proxy is not an Anthropic-supported integration and may be subject to terms or account restrictions. Use only your own accounts for authorized purposes.", "providers.anthropicOAuthAccept": "I understand the risk and want to continue with OAuth.", "providers.anthropicOAuthContinue": "Continue with OAuth", "providers.anthropicLogout": "Disconnect", "providers.anthropicLoginDone": "Claude subscription connected.", "providers.anthropicSignInBrowser": "Sign in to Claude in your browser. If no window opened, use the link below.", "providers.anthropicSignInLink": "Open the sign-in page", "providers.anthropicSignInPaste": "After signing in, paste the code the page shows here.", "providers.anthropicSignInSubmit": "Submit code", "providers.anthropicSignInManual": "Try again by pasting a code", "providers.anthropicSignInWaiting": "Waiting for the browser… (up to 5 minutes)", "providers.anthropicSignInFailed": "Sign-in failed", "providers.anthropicSignedInActive": "Using the ClaudeRipple sign-in.", "providers.anthropicSignedInStandby": "A ClaudeRipple sign-in is stored too, and takes over if the source above goes away.", "providers.add": "+ Add provider", "providers.refresh": "Test all", "providers.check": "Test connection", "providers.checking": "Checking", "providers.empty": "No providers connected yet. Add one with the button above.", "providers.modelsCount": "{count} models in use: {names}", "providers.noModels": "No models chosen. Pick some under Edit.", "providers.removeConfirm": "Remove provider {name}? Model mappings that use it are removed too.", "providers.choose": "Add provider", "providers.chooseHelp": "Pick the service to connect.", "providers.chatgpt": "ChatGPT subscription", "providers.chatgptHelp": "Use GPT models through any ChatGPT plan that includes Codex (Plus, Pro, Business…). Sign-in only, no API key.", "providers.presetHelp": "Paste an API key and you are done.", "providers.openaiGroup": "OpenAI-compatible", "providers.openaiGroupHelp": "ClaudeRipple translates requests into the OpenAI API format.", "providers.verified": "Verified against official docs", "providers.custom": "Enter manually", "providers.customName": "New provider", "providers.customHelp": "Connect any Anthropic- or OpenAI-compatible API by URL and key.", "providers.formatHelp": "Pick the format the service docs name. A service whose endpoint ends in /chat/completions is OpenAI Chat Completions.", "providers.addTitle": "Add provider", "providers.edit": "Edit provider", "providers.name": "Name", "providers.nameHelp": "How this provider is called in lists and mappings.", "providers.nameRequired": "Enter a name.", "providers.apiKey": "API key", "providers.keyHelp": "The API key issued on the provider's site. Stored only in this computer's config file.", "providers.keyPlaceholder": "Paste your API key", "providers.keySaved": "The saved key stays in use. Paste a new key to replace it.", "providers.url": "API base URL", "providers.urlHelp": "Base URL of the Anthropic-compatible endpoint. Change only if the service docs differ.", "providers.openaiUrlHelp": "Base URL of the OpenAI-compatible API. Requests are translated to its Chat Completions or Responses endpoint.", "providers.wire": "API wire format", "providers.wireHelp": "Use Chat Completions for most providers. Select Responses only when the provider supports the stateless Responses API.", "providers.urlRequired": "Enter a valid URL.", "providers.keyType": "Auth header", "providers.keyTypeHelp": "Usually x-api-key. Pick Authorization: Bearer only if the service docs say so.", "providers.extraHeaders": "Extra HTTP headers", "providers.extraHeadersHelp": "Only headers the service requires, one 'name: value' per line.", "providers.models": "Models to use", "providers.modelsHelp": "Checked models appear in the Model mapping list.", "providers.modelsFound": "Model list reported by the provider.", "providers.modelsFallback": "Could not fetch the list; showing known default models.", "measure.running": "Measuring model capabilities… ({done}/{total})", "measure.help": "Checks which wire each model answers on and which reasoning efforts it takes — from the public model catalogue (models.dev) when it lists the model, otherwise by asking the model.", "measure.done": "Measured: {summary}", "measure.nothing": "Nothing was measured: no model answered.", "measure.notEntitled": "{models}: usable only from the provider\u2019s own app. Ticking it here will not make it work.", "measure.failed": "Could not measure model capabilities.", "measure.authFailed": "The API key was refused, so nothing could be measured.", "providers.probeNotEntitled": "The key was accepted; the plan refused.", "providerStatus.notEntitled": "Plan refused", "providers.probeOk": "Connected.", "providers.probeFailed": "Could not connect.", "providers.apiSoon": "Please try again shortly.", "providers.showInPicker": "Also show by real name in the Claude app picker", "providers.pickerOffHint": "Picker mode is off right now, so ticking this does not show them yet. You will be asked to turn it on when you save.", "providers.pickerOffPrompt": "Picker mode is off, so these models will not appear in the Claude Desktop picker yet. Turn it on now? Your operating system will ask you to confirm trusting the certificate.", "providers.credentials": "Sign-in source", "providers.credentialsHelp": "Where the ChatGPT account token comes from.", "providers.authAuto": "Auto (own sign-in if present, else Codex CLI's)", "providers.authOwn": "Sign in with ClaudeRipple", "providers.authBorrow": "Reuse Codex CLI sign-in (~/.codex/auth.json)", "providers.login": "How to sign in", "providers.loginHelp": "'Sign in to ChatGPT…' in the menu-bar app or clauderipple login in a terminal opens the browser sign-in. Tokens stay on this computer.", "providers.loginHint": "Use 'Sign in to ChatGPT…' in the menu-bar app or run clauderipple login in a terminal.", "providers.chatgptLogin": "Sign in to ChatGPT", "providers.chatgptLoginWaiting": "Finish signing in in your browser. This will update when it is done.", "providers.chatgptLoginDone": "ChatGPT sign-in complete.", "providers.chatgptAccountsTitle": "ChatGPT accounts", "providers.chatgptAccountsSubtitle": "With several accounts, when one hits its limit the next one takes over the same request. A conversation stays on the account that answered, to keep its cache. Accounts are used in list order.", "providers.addChatgptAccount": "+ Add ChatGPT account", "providers.chatgptAddWaiting": "Sign in with the account to add in your browser. If it shows an account you already added, pick another.", "providers.chatgptAdded": "ChatGPT account added.", "providers.chatgptNoAccounts": "No ChatGPT account is signed in.", "providers.chatgptAccountCount": "{count} accounts", "providers.chatgptCodexHelp": "The Codex CLI's own login. ClaudeRipple only reads it, never refreshes it, and uses it last.", "providers.chatgptOwnHelp": "ClaudeRipple stores this login in a file only your user can read and refreshes it automatically.", "providers.chatgptInUse": "in use now", "providers.chatgptStandby": "standby", "providers.chatgptPause": "Pause", "providers.chatgptResume": "Resume", "providers.chatgptPaused": "paused", "providers.chatgptClearCooldown": "Use now", "providers.chatgptRemoveConfirm": "Remove ChatGPT account {name} from ClaudeRipple?", "providers.chatgptReauthHelp": "This login expired or was refused. Signing in again with the same account restores it in place.", "pool.coolingFor": "back in {duration}", "time.hoursMinutes": "{h}h {m}m", "time.minutes": "{m}m", "time.seconds": "{s}s", "quota.window5h": "5h", "quota.windowWeek": "weekly", "quota.windowHours": "{hours}h", "quota.line": "{window} {percent}%", "quota.resetAt": "resets {time}", "providers.effortLevels": "Reasoning effort: {levels}", "providers.effortNone": "Does not take reasoning effort", "providers.modelEffort": "effort", "providers.modelNoEffort": "no effort", "providers.defaultEffort": "Default reasoning effort", "providers.defaultEffortHelp": "Used when neither the mapping nor the app sets an effort.", "providers.identity": "Tell the model what it is (identity)", "providers.append": "System prompt addendum", "providers.appendHelp": "Fixed text appended to every request's system prompt. Changing it often breaks the prompt cache and raises cost.",
|
|
89
89
|
"providerStatus.connected": "Connected", "providerStatus.keyNeeded": "Key needed", "providerStatus.loginNeeded": "Sign-in needed", "providerStatus.disconnected": "Not connected", "providerStatus.checking": "Checking",
|
|
90
90
|
"clients.title": "Clients", "clients.subtitle": "Set up the apps that use ClaudeRipple.", "clients.desktop.title": "Claude Desktop", "clients.desktop.help": "Choose whether provider models appear by real name in the model picker.", "clients.codex.title": "Codex (app and CLI)", "clients.codex.help": "Adds ClaudeRipple as a provider to Codex's config (~/.codex/config.toml). Terminal: `codex --profile clauderipple -m <model>`; Codex app: make clauderipple the default provider in that config.", "clients.codex.note": "When on, Codex's GPT requests go through ClaudeRipple too. Codex keeps its own sign-in, and with several ChatGPT accounts added, the next one takes over when one hits its limit — no sign-out. The list below is the Claude and Anthropic-compatible models Codex gains.", "clients.codex.turnOn": "Use ClaudeRipple in Codex", "clients.codex.turnOff": "Disconnect Codex", "clients.claudeCode.title": "Terminal Claude Code", "clients.claudeCode.help": "Terminal `claude` uses the same router. Choose with `/model <name>`.",
|
|
91
91
|
"logs.title": "Logs", "logs.requests": "Requests", "logs.raw": "Raw logs", "logs.lastHour": "Last hour", "logs.autoscroll": "Auto-scroll", "logs.polling": "Updates every 3s", "logs.rawPolling": "Updates every 3s · last 200 lines", "logs.provider": "Provider", "logs.all": "All", "logs.showCountTokens": "Show token counts", "logs.empty": "No requests yet.", "logs.summary.requests": "Requests", "logs.summary.success": "Succeeded / failed", "logs.summary.input": "Input tokens", "logs.summary.output": "Output tokens", "logs.summary.duration": "Average response", "logs.cacheHit": "Cache {percent}%", "logs.th.time": "Time", "logs.th.models": "Model", "logs.th.effort": "effort", "logs.th.input": "Input", "logs.th.output": "Output", "logs.th.duration": "Duration", "logs.th.status": "Status", "logs.status.ok": "ok", "logs.status.error": "error", "logs.status.resent": "resent", "logs.status.resentHelp": "Not a failure: the client sent this request again on its own.", "logs.detail.id": "Request ID", "logs.detail.kind": "Kind", "logs.detail.stop": "Stop reason", "logs.detail.uncached": "Uncached input", "logs.detail.cacheRead": "Cache read", "logs.detail.cacheWrite": "Cache write", "logs.detail.note": "Note", "logs.none": "None", "logs.seconds": "{value}s",
|
|
@@ -270,7 +270,7 @@ const I18N = {
|
|
|
270
270
|
"providers.addClaudeAccount": "+ 添加 Claude 账号",
|
|
271
271
|
"providers.currentAccountHelp": "Claude Code 或 Claude Desktop 当前使用的外部登录。ClaudeRipple 无法移除它。",
|
|
272
272
|
"providers.addedAccountHelp": "ClaudeRipple 将此 OAuth 授权保存在本机,并自动刷新。",
|
|
273
|
-
"providers.reauthAction": "重新登录",
|
|
273
|
+
"providers.reauthAction": "重新登录", "providers.preferBadge": "优先", "providers.preferAction": "设为优先", "providers.preferHelp": "新对话从最上面的优先账号开始。它达到限额时切换到下一个账号,限额重置后新对话会回到它。进行中的对话为保留缓存,继续使用原来的账号。",
|
|
274
274
|
"providers.dangerZone": "管理已添加的账号",
|
|
275
275
|
"providers.saveSettings": "保存设置",
|
|
276
276
|
"providers.connection": "连接",
|
|
@@ -654,7 +654,7 @@ const I18N = {
|
|
|
654
654
|
"providers.addClaudeAccount": "+ Claudeアカウントを追加",
|
|
655
655
|
"providers.currentAccountHelp": "Claude CodeまたはClaude Desktopが現在使っている外部のログインです。ClaudeRippleからは削除できません。",
|
|
656
656
|
"providers.addedAccountHelp": "ClaudeRippleがこのOAuthグラントをこのコンピューターに保存し、自動で更新します。",
|
|
657
|
-
"providers.reauthAction": "再ログイン",
|
|
657
|
+
"providers.reauthAction": "再ログイン", "providers.preferBadge": "優先", "providers.preferAction": "優先にする", "providers.preferHelp": "新しい会話はいちばん上の優先アカウントから使います。上限に達すると次のアカウントに切り替わり、上限がリセットされると新しい会話から再び優先アカウントに戻ります。進行中の会話はキャッシュを保つため、使っていたアカウントのままです。",
|
|
658
658
|
"providers.dangerZone": "追加したアカウントの管理",
|
|
659
659
|
"providers.saveSettings": "設定を保存",
|
|
660
660
|
"providers.connection": "接続",
|
|
@@ -1038,7 +1038,7 @@ const I18N = {
|
|
|
1038
1038
|
"providers.addClaudeAccount": "+ Añadir cuenta de Claude",
|
|
1039
1039
|
"providers.currentAccountHelp": "El inicio de sesión externo que usan ahora Claude Code o Claude Desktop. ClaudeRipple no puede quitarlo.",
|
|
1040
1040
|
"providers.addedAccountHelp": "ClaudeRipple guarda esta autorización OAuth en este equipo y la renueva automáticamente.",
|
|
1041
|
-
"providers.reauthAction": "Volver a iniciar sesión",
|
|
1041
|
+
"providers.reauthAction": "Volver a iniciar sesión", "providers.preferBadge": "Primera", "providers.preferAction": "Usar primero", "providers.preferHelp": "Las conversaciones nuevas empiezan en la cuenta de arriba. Cuando llega a su límite pasan a la siguiente y, cuando el límite se restablece, las nuevas vuelven a ella. Una conversación en curso se queda en su cuenta para conservar la caché.",
|
|
1042
1042
|
"providers.dangerZone": "Gestión de cuentas añadidas",
|
|
1043
1043
|
"providers.saveSettings": "Guardar configuración",
|
|
1044
1044
|
"providers.connection": "Conexión",
|
|
@@ -1422,7 +1422,7 @@ const I18N = {
|
|
|
1422
1422
|
"providers.addClaudeAccount": "+ Adicionar conta do Claude",
|
|
1423
1423
|
"providers.currentAccountHelp": "O login externo usado no momento pelo Claude Code ou pelo Claude Desktop. O ClaudeRipple não pode removê-lo.",
|
|
1424
1424
|
"providers.addedAccountHelp": "O ClaudeRipple guarda esta autorização OAuth neste computador e a renova automaticamente.",
|
|
1425
|
-
"providers.reauthAction": "Entrar novamente",
|
|
1425
|
+
"providers.reauthAction": "Entrar novamente", "providers.preferBadge": "Primeira", "providers.preferAction": "Usar primeiro", "providers.preferHelp": "Conversas novas começam na conta do topo. Quando ela atinge o limite, passam para a próxima e, quando o limite é redefinido, as novas voltam para ela. Uma conversa em andamento fica na conta em que está para manter o cache.",
|
|
1426
1426
|
"providers.dangerZone": "Gerenciar contas adicionadas",
|
|
1427
1427
|
"providers.saveSettings": "Salvar configurações",
|
|
1428
1428
|
"providers.connection": "Conexão",
|
package/dist/ui/style.css
CHANGED
|
@@ -342,6 +342,8 @@ label.check input { width: auto; }
|
|
|
342
342
|
.account-card-copy strong, .account-card-copy > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
343
343
|
.account-card-copy .hint { margin: 1px 0 0; }
|
|
344
344
|
.account-card-actions { display: flex; gap: 6px; }
|
|
345
|
+
.account-card-badges { display: flex; gap: 6px; align-items: center; justify-content: flex-end; }
|
|
346
|
+
.badge.first { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
|
|
345
347
|
/* Cards with more actions than fit beside the text keep them on their own row at every width. */
|
|
346
348
|
.account-card.stacked { grid-template-columns: minmax(0, 1fr) auto; }
|
|
347
349
|
.account-card.stacked .account-card-actions { grid-column: 1 / -1; flex-wrap: wrap; }
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -335,8 +335,12 @@ request went, and a `Pane` holds the request log.
|
|
|
335
335
|
visibility: "list"|"hide", context_window, supported_reasoning_levels:
|
|
336
336
|
[{effort}] }] }` — what the Codex CLI's picker shows. **The server filters by
|
|
337
337
|
`client_version`:** `0.146.0` omitted gpt-6-astra/sol/luna, `0.155.0`
|
|
338
|
-
listed them
|
|
339
|
-
|
|
338
|
+
listed them; `0.158.0` omitted gpt-6.1-sol and `0.159.0` listed it
|
|
339
|
+
(2026-10-10, also listed at `0.162.1`, `1.0.0`, `99.0.0`). A floor alone
|
|
340
|
+
trails every release — a Windows user with no Codex CLI never saw 6.1 Sol
|
|
341
|
+
(#50) — so the adapter (`catalog.ts`) asks with the highest of the floor,
|
|
342
|
+
`~/.codex/models_cache.json`'s `client_version` and the newest `@openai/codex`
|
|
343
|
+
on the npm registry (kept a day), drops `hide` entries, keeps
|
|
340
344
|
the list an hour, and falls back to a measured list in the same file. The
|
|
341
345
|
GUI's ChatGPT form fetches it on open; new models arrive unticked. Model ids
|
|
342
346
|
unknown to the backend answer `400 "… model is not supported when using
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clauderipple",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.10",
|
|
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": [
|