clauderipple 0.6.0 → 0.8.0
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 +66 -0
- package/README.ko.md +43 -2
- package/README.md +60 -4
- package/dist/app/package.json +1 -1
- package/dist/cli/src/claude-auth.js +35 -1
- package/dist/cli/src/codex.js +23 -0
- package/dist/cli/src/hooks/agent-title.js +58 -29
- package/dist/cli/src/index.js +89 -1
- package/dist/cli/src/runtime.js +3 -0
- package/dist/cli/src/settings.js +49 -0
- package/dist/mod/.claude-plugin/plugin.json +1 -0
- package/dist/mod/hooks/hooks.json +1 -0
- package/dist/mod/hooks/register.tsx +309 -0
- package/dist/mod/hooks/worker.ts +49 -0
- package/dist/mod/tsconfig.json +3 -0
- package/dist/mod/types/index.d.ts +30 -0
- package/dist/router/src/admin.js +497 -34
- package/dist/router/src/agents.js +50 -9
- package/dist/router/src/catalog.js +80 -0
- package/dist/router/src/compat.js +27 -1
- package/dist/router/src/config.js +33 -0
- package/dist/router/src/index.js +13 -1
- package/dist/router/src/ingress/server.js +3 -2
- package/dist/router/src/picker.js +33 -2
- package/dist/router/src/presets.js +2 -1
- package/dist/router/src/providers/chatgpt/index.js +154 -15
- package/dist/router/src/providers/chatgpt/translate.js +34 -6
- package/dist/router/src/providers/google/accounts.js +345 -0
- package/dist/router/src/providers/google/antigravity.js +392 -0
- package/dist/router/src/providers/google/index.js +617 -0
- package/dist/router/src/providers/google/login.js +249 -0
- package/dist/router/src/providers/google/thoughtSignatures.js +47 -0
- package/dist/router/src/providers/google/translate.js +539 -0
- package/dist/router/src/providers/google/transport.js +118 -0
- package/dist/router/src/providers/openai/translate.js +3 -3
- package/dist/router/src/proxy.js +99 -1
- package/dist/router/src/requestlog.js +2 -0
- package/dist/router/src/routing.js +22 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +436 -44
- package/dist/ui/i18n.js +88 -4
- package/dist/ui/index.html +10 -0
- package/dist/ui/style.css +4 -0
- package/docs/ARCHITECTURE.md +300 -3
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// stable prompt prefix can retain their native prompt-cache behavior.
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import { clampEffort } from "../../compat.js";
|
|
6
|
-
import { conversationKey, estimateTokens, normalizeSchema, serverToolNames,
|
|
6
|
+
import { conversationKey, estimateTokens, normalizeSchema, serverToolNames, systemWithDroppedToolNote, toolNameForResponses } from "../chatgpt/translate.js";
|
|
7
7
|
import { identityPrefix, instructionsSuffix } from "../../identity.js";
|
|
8
8
|
function textOf(content) {
|
|
9
9
|
if (typeof content === "string")
|
|
@@ -87,7 +87,7 @@ function mappedEffort(opts) {
|
|
|
87
87
|
/** Convert every Anthropic message into OpenAI Chat Completion messages without inventing unstable text. */
|
|
88
88
|
export function toChatMessages(req, opts) {
|
|
89
89
|
const messages = [];
|
|
90
|
-
const sys =
|
|
90
|
+
const sys = systemWithDroppedToolNote(req);
|
|
91
91
|
const content = opts ? systemWithIdentity(sys, opts) : sys;
|
|
92
92
|
if (content)
|
|
93
93
|
messages.push({ role: "system", content });
|
|
@@ -248,7 +248,7 @@ export function toOpenAiRequest(req, opts) {
|
|
|
248
248
|
input: toResponsesInput(req),
|
|
249
249
|
stream: true,
|
|
250
250
|
};
|
|
251
|
-
const instructions = systemWithIdentity(
|
|
251
|
+
const instructions = systemWithIdentity(systemWithDroppedToolNote(req), opts);
|
|
252
252
|
if (instructions)
|
|
253
253
|
out.instructions = instructions;
|
|
254
254
|
if (responseTools.length) {
|
package/dist/router/src/proxy.js
CHANGED
|
@@ -29,9 +29,11 @@ import { classify, CredentialPool, retryAfterMs } from "./pool.js";
|
|
|
29
29
|
import { PRESETS } from "./presets.js";
|
|
30
30
|
import { ChatGptAdapter } from "./providers/chatgpt/index.js";
|
|
31
31
|
import { OpenAiCompatibleAdapter } from "./providers/openai/index.js";
|
|
32
|
+
import { GoogleAdapter } from "./providers/google/index.js";
|
|
32
33
|
import { conversationKey } from "./providers/chatgpt/translate.js";
|
|
33
34
|
import { providerFor, terminateHosts } from "./config.js";
|
|
34
35
|
import { injectPickerModels, isBootstrapPath } from "./picker.js";
|
|
36
|
+
import { effortLevelsForModel } from "./admin.js";
|
|
35
37
|
import { ResponseUsageTap } from "./requestlog.js";
|
|
36
38
|
import { credentialHeaderValues, redactErrorText, redactHeaders } from "./redact.js";
|
|
37
39
|
import { ClaudeAccountAuthPool } from "./providers/anthropic-account-pool.js";
|
|
@@ -177,6 +179,7 @@ export class Proxy {
|
|
|
177
179
|
providerAgents = new Map();
|
|
178
180
|
chatgptAdapters = new Map();
|
|
179
181
|
openaiAdapters = new Map();
|
|
182
|
+
googleAdapters = new Map();
|
|
180
183
|
claudeAccounts;
|
|
181
184
|
/** Each Claude subscription account's usage, for the admin status (never a token). */
|
|
182
185
|
claudeUsage;
|
|
@@ -253,6 +256,20 @@ export class Proxy {
|
|
|
253
256
|
return this.chatgpt(first[0], first[1]);
|
|
254
257
|
return this.chatgpt("codex-login", { type: "chatgpt", auth: "borrow-codex" });
|
|
255
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* One image for `/api/image`, on the first chatgpt provider and the first model it declares. The
|
|
261
|
+
* model only runs the tool call — the image comes from the backend's own image model — so which
|
|
262
|
+
* one it is barely matters.
|
|
263
|
+
*/
|
|
264
|
+
generateImage(req, signal) {
|
|
265
|
+
const first = Object.entries(this.deps.config().providers).find(([, p]) => p.type === "chatgpt");
|
|
266
|
+
if (!first || first[1].type !== "chatgpt")
|
|
267
|
+
return Promise.reject(new Error("image generation needs a ChatGPT provider; add one in the dashboard"));
|
|
268
|
+
const model = first[1].models?.[0]?.id;
|
|
269
|
+
if (!model)
|
|
270
|
+
return Promise.reject(new Error(`provider ${first[0]} declares no models`));
|
|
271
|
+
return this.chatgpt(first[0], first[1]).generateImage(model, req, signal);
|
|
272
|
+
}
|
|
256
273
|
/** Dashboard action: put one resting ChatGPT account back into rotation now. */
|
|
257
274
|
chatgptClearCooldown(ownerId) {
|
|
258
275
|
for (const [name, p] of Object.entries(this.deps.config().providers)) {
|
|
@@ -278,6 +295,52 @@ export class Proxy {
|
|
|
278
295
|
this.openaiAdapters.set(name, { key, adapter });
|
|
279
296
|
return adapter;
|
|
280
297
|
}
|
|
298
|
+
google(name, cfg) {
|
|
299
|
+
const key = JSON.stringify(cfg);
|
|
300
|
+
const cur = this.googleAdapters.get(name);
|
|
301
|
+
if (cur && cur.key === key)
|
|
302
|
+
return cur.adapter;
|
|
303
|
+
const adapter = new GoogleAdapter(name, cfg, this.deps.home, this.deps.log, this.pool);
|
|
304
|
+
this.googleAdapters.set(name, { key, adapter });
|
|
305
|
+
return adapter;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Every configured google provider's Antigravity accounts: rotation state only, never a token.
|
|
309
|
+
* Empty for api-key providers and for an adapter that has not been created yet.
|
|
310
|
+
*/
|
|
311
|
+
googleAccounts() {
|
|
312
|
+
const out = {};
|
|
313
|
+
for (const [name, p] of Object.entries(this.deps.config().providers)) {
|
|
314
|
+
if (p.type === "google" && p.auth === "antigravity")
|
|
315
|
+
out[name] = this.google(name, p).accountStatus();
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
/** The models a google provider can reach now, from Cloud Code Assist's own catalogue. */
|
|
320
|
+
googleFetchModels(name) {
|
|
321
|
+
const cfg = this.deps.config().providers[name];
|
|
322
|
+
if (!cfg || cfg.type !== "google" || cfg.auth !== "antigravity")
|
|
323
|
+
return Promise.resolve(null);
|
|
324
|
+
return this.google(name, cfg).fetchModels();
|
|
325
|
+
}
|
|
326
|
+
/** Dashboard action: put one resting Google account back into rotation now. */
|
|
327
|
+
googleClearCooldown(ownerId) {
|
|
328
|
+
for (const [name, p] of Object.entries(this.deps.config().providers)) {
|
|
329
|
+
if (p.type === "google" && p.auth === "antigravity")
|
|
330
|
+
this.google(name, p).clearCooldown(ownerId);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** Ask again whether an account still needs Google's verification page; the first provider that knows answers. */
|
|
334
|
+
async googleRecheckVerification(ownerId) {
|
|
335
|
+
for (const [name, p] of Object.entries(this.deps.config().providers)) {
|
|
336
|
+
if (p.type !== "google" || p.auth !== "antigravity")
|
|
337
|
+
continue;
|
|
338
|
+
const result = await this.google(name, p).recheckVerification(ownerId);
|
|
339
|
+
if (result !== "unknown")
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
return "unknown";
|
|
343
|
+
}
|
|
281
344
|
constructor(deps) {
|
|
282
345
|
this.deps = deps;
|
|
283
346
|
this.claudeAccounts = new ClaudeAccountAuthPool({
|
|
@@ -511,6 +574,9 @@ export class Proxy {
|
|
|
511
574
|
provider: "anthropic",
|
|
512
575
|
stream: false,
|
|
513
576
|
};
|
|
577
|
+
const session = req.headers["x-claude-code-session-id"];
|
|
578
|
+
if (typeof session === "string" && /^[\w-]{1,128}$/.test(session))
|
|
579
|
+
record.session = session;
|
|
514
580
|
let observedUsage;
|
|
515
581
|
let observedStopReason;
|
|
516
582
|
// `failed` counts requests that did not get a proper response (vanished, upstream error,
|
|
@@ -733,6 +799,38 @@ export class Proxy {
|
|
|
733
799
|
}
|
|
734
800
|
return;
|
|
735
801
|
}
|
|
802
|
+
if (provider.type === "google") {
|
|
803
|
+
// Google holds no server-side thread either, so a `continue` is refused as it is for the
|
|
804
|
+
// other translated providers; the CLI resends the turn with the full history.
|
|
805
|
+
const td = threadDecision(json);
|
|
806
|
+
if (td === "refuse") {
|
|
807
|
+
const out = JSON.stringify(THREAD_UNSUPPORTED);
|
|
808
|
+
res.writeHead(400, { "content-type": "application/json", "content-length": String(Buffer.byteLength(out)) }).end(out);
|
|
809
|
+
finish("400", out.length, "thread continue refused → CLI resends stateless", false);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (td === "strip")
|
|
813
|
+
stripThreadFields(json);
|
|
814
|
+
record = { ...record, target: route.model, provider: route.provider };
|
|
815
|
+
const routeEffort = effortOf(json);
|
|
816
|
+
if (routeEffort)
|
|
817
|
+
record.effort = routeEffort;
|
|
818
|
+
tag = `GOOGLE ${route.tag} effort=${routeEffort ?? "-"}`;
|
|
819
|
+
try {
|
|
820
|
+
const o = await this.google(route.provider, provider).handle(req, res, path, json, route.model, routeEffort);
|
|
821
|
+
this.recordOutcome(route.provider, o.status);
|
|
822
|
+
finish(String(o.status), o.bytes, o.note, o.status >= 400, { ...(o.usage ? { usage: o.usage } : {}), ...(o.stopReason ? { stopReason: o.stopReason } : {}) });
|
|
823
|
+
}
|
|
824
|
+
catch (e) {
|
|
825
|
+
this.recordOutcome(route.provider, 0);
|
|
826
|
+
finish("-", 0, `google error ${e.code ?? ""} ${e.message}`);
|
|
827
|
+
if (!res.headersSent)
|
|
828
|
+
res.writeHead(502, { "content-type": "application/json" }).end(JSON.stringify({ type: "error", error: { type: "api_error", message: e.message } }));
|
|
829
|
+
else
|
|
830
|
+
res.destroy();
|
|
831
|
+
}
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
736
834
|
if (provider.type === "anthropic") {
|
|
737
835
|
if (!provider.accountPool || provider.auth !== "claude-code") {
|
|
738
836
|
finish("400", 0, "native Anthropic provider is available through OpenAI ingress only", false);
|
|
@@ -1134,7 +1232,7 @@ export class Proxy {
|
|
|
1134
1232
|
else if (status === 200 && isPickerBootstrap) {
|
|
1135
1233
|
try {
|
|
1136
1234
|
const j = JSON.parse(out.toString("utf8"));
|
|
1137
|
-
const r = injectPickerModels(j, cfg.cli.extraModels, cfg.cli.autoCompactWindow);
|
|
1235
|
+
const r = injectPickerModels(j, cfg.cli.extraModels, cfg.cli.autoCompactWindow, (model) => effortLevelsForModel(cfg, model));
|
|
1138
1236
|
if (r.surfaces.length > 0) {
|
|
1139
1237
|
this.lastPickerInjection = { at: new Date().toISOString(), ...r };
|
|
1140
1238
|
out = Buffer.from(JSON.stringify(j));
|
|
@@ -50,6 +50,18 @@ export function markerOverride(body, aliases) {
|
|
|
50
50
|
export function resolve(model, body, cfg) {
|
|
51
51
|
if (typeof model !== "string")
|
|
52
52
|
return null;
|
|
53
|
+
// Measured on Desktop's engine 2.1.284: the registered AUTO_MODE_MODEL env var is
|
|
54
|
+
// unused. Match the classifier's system prompt, never quoted conversation text.
|
|
55
|
+
// Its decision and wire format remain the engine's; only the inference target changes.
|
|
56
|
+
if (cfg.autoModeClassifier && isAutoModeClassifier(body)) {
|
|
57
|
+
const target = cfg.autoModeClassifier;
|
|
58
|
+
return {
|
|
59
|
+
provider: target.provider,
|
|
60
|
+
model: target.model,
|
|
61
|
+
effort: target.effort,
|
|
62
|
+
tag: `${model}->${target.model} (auto-mode classifier)`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
53
65
|
let base = model;
|
|
54
66
|
let effort;
|
|
55
67
|
const at = model.indexOf("@");
|
|
@@ -128,6 +140,16 @@ export function resolve(model, body, cfg) {
|
|
|
128
140
|
return null;
|
|
129
141
|
return { provider: owner, model: finalModel, effort: ov?.effort ?? effort, tag: `${model}->${finalModel}${ov ? " (marker)" : ""}` };
|
|
130
142
|
}
|
|
143
|
+
export function isAutoModeClassifier(body) {
|
|
144
|
+
const request = body;
|
|
145
|
+
if (!request || (Array.isArray(request.tools) && request.tools.length > 0))
|
|
146
|
+
return false;
|
|
147
|
+
const system = request.system;
|
|
148
|
+
const texts = typeof system === "string" ? [system] : Array.isArray(system)
|
|
149
|
+
? system.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text)
|
|
150
|
+
: [];
|
|
151
|
+
return texts.some((text) => text.trimStart().startsWith("You are a security monitor for autonomous AI coding agents."));
|
|
152
|
+
}
|
|
131
153
|
/**
|
|
132
154
|
* Providers whose own `models` list carries this id. Dated and undated forms match each other, the
|
|
133
155
|
* same way a slot lookup does. Ingress-only providers are included on purpose — see the call site.
|
|
@@ -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
|
+
export const VERSION = "0.8.0";
|