hydramcp 1.0.3 → 1.0.5
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.
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
* Subscription Provider — use your monthly subscriptions as an API.
|
|
3
3
|
*
|
|
4
4
|
* Reads OAuth tokens stored by CLI tools (Claude Code, Gemini CLI, Codex CLI)
|
|
5
|
-
* and makes direct HTTP requests to provider APIs.
|
|
5
|
+
* and makes direct HTTP requests to provider-internal APIs.
|
|
6
6
|
*
|
|
7
7
|
* Token locations:
|
|
8
8
|
* Claude → ~/.claude/.credentials.json
|
|
9
9
|
* Gemini → ~/.gemini/oauth_creds.json
|
|
10
10
|
* Codex → ~/.codex/auth.json
|
|
11
11
|
*
|
|
12
|
+
* Endpoints (from CLIProxyAPI & Gemini CLI source):
|
|
13
|
+
* Gemini → https://cloudcode-pa.googleapis.com/v1internal:generateContent
|
|
14
|
+
* Codex → https://chatgpt.com/backend-api/codex/responses (SSE)
|
|
15
|
+
* Claude → CLI subprocess (api.anthropic.com rejects OAuth without TLS fingerprint)
|
|
16
|
+
*
|
|
12
17
|
* Approach learned from CLIProxyAPI (github.com/router-for-me/CLIProxyAPI).
|
|
13
|
-
* 100% our code. Zero external dependencies.
|
|
14
18
|
*/
|
|
15
19
|
import { Provider, ModelInfo, QueryOptions, QueryResponse } from "./provider.js";
|
|
16
20
|
export declare class SubscriptionProvider implements Provider {
|
|
@@ -18,10 +22,6 @@ export declare class SubscriptionProvider implements Provider {
|
|
|
18
22
|
private backends;
|
|
19
23
|
private modelToBackend;
|
|
20
24
|
private tokenCache;
|
|
21
|
-
/**
|
|
22
|
-
* Detect which subscription tokens exist on disk.
|
|
23
|
-
* Returns the number of backends with valid tokens.
|
|
24
|
-
*/
|
|
25
25
|
detect(): Promise<number>;
|
|
26
26
|
healthCheck(): Promise<boolean>;
|
|
27
27
|
listModels(): Promise<ModelInfo[]>;
|
|
@@ -2,20 +2,28 @@
|
|
|
2
2
|
* Subscription Provider — use your monthly subscriptions as an API.
|
|
3
3
|
*
|
|
4
4
|
* Reads OAuth tokens stored by CLI tools (Claude Code, Gemini CLI, Codex CLI)
|
|
5
|
-
* and makes direct HTTP requests to provider APIs.
|
|
5
|
+
* and makes direct HTTP requests to provider-internal APIs.
|
|
6
6
|
*
|
|
7
7
|
* Token locations:
|
|
8
8
|
* Claude → ~/.claude/.credentials.json
|
|
9
9
|
* Gemini → ~/.gemini/oauth_creds.json
|
|
10
10
|
* Codex → ~/.codex/auth.json
|
|
11
11
|
*
|
|
12
|
+
* Endpoints (from CLIProxyAPI & Gemini CLI source):
|
|
13
|
+
* Gemini → https://cloudcode-pa.googleapis.com/v1internal:generateContent
|
|
14
|
+
* Codex → https://chatgpt.com/backend-api/codex/responses (SSE)
|
|
15
|
+
* Claude → CLI subprocess (api.anthropic.com rejects OAuth without TLS fingerprint)
|
|
16
|
+
*
|
|
12
17
|
* Approach learned from CLIProxyAPI (github.com/router-for-me/CLIProxyAPI).
|
|
13
|
-
* 100% our code. Zero external dependencies.
|
|
14
18
|
*/
|
|
15
19
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
16
20
|
import { join } from "node:path";
|
|
17
21
|
import { homedir } from "node:os";
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
18
23
|
import { logger } from "../utils/logger.js";
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Token file readers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
19
27
|
function readClaudeTokens() {
|
|
20
28
|
try {
|
|
21
29
|
const raw = readFileSync(join(homedir(), ".claude", ".credentials.json"), "utf-8");
|
|
@@ -56,18 +64,18 @@ function readCodexTokens() {
|
|
|
56
64
|
const tokens = data.tokens;
|
|
57
65
|
if (!tokens?.access_token || !tokens?.refresh_token)
|
|
58
66
|
return null;
|
|
59
|
-
// Codex access_token is a JWT — extract exp from payload
|
|
60
67
|
let expiresAt = 0;
|
|
61
68
|
try {
|
|
62
69
|
const payload = JSON.parse(Buffer.from(tokens.access_token.split(".")[1], "base64").toString());
|
|
63
70
|
if (payload.exp)
|
|
64
|
-
expiresAt = payload.exp * 1000;
|
|
71
|
+
expiresAt = payload.exp * 1000;
|
|
65
72
|
}
|
|
66
|
-
catch { /* non-JWT or malformed
|
|
73
|
+
catch { /* non-JWT or malformed */ }
|
|
67
74
|
return {
|
|
68
75
|
accessToken: tokens.access_token,
|
|
69
76
|
refreshToken: tokens.refresh_token,
|
|
70
77
|
expiresAt,
|
|
78
|
+
accountId: tokens.account_id ?? undefined,
|
|
71
79
|
};
|
|
72
80
|
}
|
|
73
81
|
catch {
|
|
@@ -96,7 +104,6 @@ async function refreshClaudeToken(refreshToken) {
|
|
|
96
104
|
const expiresIn = data.expires_in ?? 86400;
|
|
97
105
|
if (!accessToken)
|
|
98
106
|
return null;
|
|
99
|
-
// Write back to credentials file
|
|
100
107
|
try {
|
|
101
108
|
const credPath = join(homedir(), ".claude", ".credentials.json");
|
|
102
109
|
const existing = JSON.parse(readFileSync(credPath, "utf-8"));
|
|
@@ -105,7 +112,7 @@ async function refreshClaudeToken(refreshToken) {
|
|
|
105
112
|
existing.claudeAiOauth.expiresAt = Date.now() + expiresIn * 1000;
|
|
106
113
|
writeFileSync(credPath, JSON.stringify(existing), "utf-8");
|
|
107
114
|
}
|
|
108
|
-
catch { /* non-fatal
|
|
115
|
+
catch { /* non-fatal */ }
|
|
109
116
|
return {
|
|
110
117
|
accessToken,
|
|
111
118
|
refreshToken: newRefresh || refreshToken,
|
|
@@ -136,7 +143,6 @@ async function refreshGeminiToken(refreshToken) {
|
|
|
136
143
|
const expiresIn = data.expires_in ?? 3600;
|
|
137
144
|
if (!accessToken)
|
|
138
145
|
return null;
|
|
139
|
-
// Write back
|
|
140
146
|
try {
|
|
141
147
|
const credPath = join(homedir(), ".gemini", "oauth_creds.json");
|
|
142
148
|
const existing = JSON.parse(readFileSync(credPath, "utf-8"));
|
|
@@ -149,7 +155,7 @@ async function refreshGeminiToken(refreshToken) {
|
|
|
149
155
|
catch { /* non-fatal */ }
|
|
150
156
|
return {
|
|
151
157
|
accessToken,
|
|
152
|
-
refreshToken,
|
|
158
|
+
refreshToken,
|
|
153
159
|
expiresAt: Date.now() + expiresIn * 1000,
|
|
154
160
|
};
|
|
155
161
|
}
|
|
@@ -178,10 +184,11 @@ async function refreshCodexToken(refreshToken) {
|
|
|
178
184
|
const expiresIn = data.expires_in ?? 864000;
|
|
179
185
|
if (!accessToken)
|
|
180
186
|
return null;
|
|
181
|
-
|
|
187
|
+
let accountId;
|
|
182
188
|
try {
|
|
183
189
|
const credPath = join(homedir(), ".codex", "auth.json");
|
|
184
190
|
const existing = JSON.parse(readFileSync(credPath, "utf-8"));
|
|
191
|
+
accountId = existing.tokens?.account_id;
|
|
185
192
|
existing.tokens.access_token = accessToken;
|
|
186
193
|
existing.tokens.refresh_token = newRefresh || refreshToken;
|
|
187
194
|
if (data.id_token)
|
|
@@ -194,58 +201,159 @@ async function refreshCodexToken(refreshToken) {
|
|
|
194
201
|
accessToken,
|
|
195
202
|
refreshToken: newRefresh || refreshToken,
|
|
196
203
|
expiresAt: Date.now() + expiresIn * 1000,
|
|
204
|
+
accountId,
|
|
197
205
|
};
|
|
198
206
|
}
|
|
199
207
|
catch {
|
|
200
208
|
return null;
|
|
201
209
|
}
|
|
202
210
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
stream: false,
|
|
212
|
-
};
|
|
213
|
-
if (options?.temperature !== undefined)
|
|
214
|
-
body.temperature = options.temperature;
|
|
215
|
-
if (options?.max_tokens !== undefined)
|
|
216
|
-
body.max_tokens = options.max_tokens;
|
|
217
|
-
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Gemini project ID — resolved via Cloud Code Assist loadCodeAssist API
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
let cachedGeminiProjectId = null;
|
|
215
|
+
async function getGeminiProjectId(token) {
|
|
216
|
+
if (cachedGeminiProjectId)
|
|
217
|
+
return cachedGeminiProjectId;
|
|
218
|
+
const res = await fetch("https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", {
|
|
218
219
|
method: "POST",
|
|
219
220
|
headers: {
|
|
220
221
|
"Content-Type": "application/json",
|
|
221
222
|
"Authorization": `Bearer ${token}`,
|
|
223
|
+
"User-Agent": "google-api-nodejs-client/9.15.1",
|
|
224
|
+
"X-Goog-Api-Client": "gl-node/22.17.0",
|
|
222
225
|
},
|
|
223
|
-
body: JSON.stringify(
|
|
226
|
+
body: JSON.stringify({
|
|
227
|
+
metadata: {
|
|
228
|
+
ideType: "IDE_UNSPECIFIED",
|
|
229
|
+
platform: "PLATFORM_UNSPECIFIED",
|
|
230
|
+
pluginType: "GEMINI",
|
|
231
|
+
},
|
|
232
|
+
}),
|
|
224
233
|
});
|
|
225
234
|
if (!res.ok) {
|
|
226
235
|
const err = await res.text();
|
|
227
|
-
throw new Error(`
|
|
236
|
+
throw new Error(`Gemini loadCodeAssist failed (${res.status}): ${err}`);
|
|
228
237
|
}
|
|
229
238
|
const data = (await res.json());
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
239
|
+
let projectId = null;
|
|
240
|
+
if (typeof data.cloudaicompanionProject === "string") {
|
|
241
|
+
projectId = data.cloudaicompanionProject;
|
|
242
|
+
}
|
|
243
|
+
else if (data.cloudaicompanionProject &&
|
|
244
|
+
typeof data.cloudaicompanionProject === "object" &&
|
|
245
|
+
typeof data.cloudaicompanionProject.id === "string") {
|
|
246
|
+
projectId = data.cloudaicompanionProject.id;
|
|
247
|
+
}
|
|
248
|
+
if (!projectId && Array.isArray(data.allowedTiers)) {
|
|
249
|
+
const defaultTier = data.allowedTiers.find((t) => t.isDefault === true);
|
|
250
|
+
if (typeof defaultTier?.id === "string") {
|
|
251
|
+
projectId = defaultTier.id;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!projectId) {
|
|
255
|
+
throw new Error("Gemini: no project ID from loadCodeAssist. Run `gemini` CLI once to set up your account.");
|
|
256
|
+
}
|
|
257
|
+
cachedGeminiProjectId = projectId;
|
|
258
|
+
logger.info(`Gemini project ID resolved: ${projectId}`);
|
|
259
|
+
return projectId;
|
|
260
|
+
}
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Query: Codex — chatgpt.com/backend-api/codex SSE streaming
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
async function queryCodex(tokens, model, prompt, options) {
|
|
265
|
+
const startTime = Date.now();
|
|
266
|
+
const input = [];
|
|
267
|
+
if (options?.system_prompt) {
|
|
268
|
+
input.push({ role: "developer", content: options.system_prompt });
|
|
269
|
+
}
|
|
270
|
+
input.push({ role: "user", content: prompt });
|
|
271
|
+
const body = {
|
|
272
|
+
model,
|
|
273
|
+
instructions: "",
|
|
274
|
+
input,
|
|
275
|
+
stream: true,
|
|
276
|
+
store: false,
|
|
277
|
+
};
|
|
278
|
+
// Note: chatgpt.com/backend-api/codex does NOT support temperature or
|
|
279
|
+
// max_output_tokens for Codex reasoning models. Omitting these params.
|
|
280
|
+
const headers = {
|
|
281
|
+
"Content-Type": "application/json",
|
|
282
|
+
"Authorization": `Bearer ${tokens.accessToken}`,
|
|
283
|
+
"Accept": "text/event-stream",
|
|
284
|
+
"Version": "0.98.0",
|
|
285
|
+
"Openai-Beta": "responses=experimental",
|
|
286
|
+
"User-Agent": "codex_cli_rs/0.98.0",
|
|
287
|
+
"Originator": "codex_cli_rs",
|
|
288
|
+
"Connection": "Keep-Alive",
|
|
289
|
+
};
|
|
290
|
+
if (tokens.accountId) {
|
|
291
|
+
headers["Chatgpt-Account-Id"] = tokens.accountId;
|
|
292
|
+
}
|
|
293
|
+
const res = await fetch("https://chatgpt.com/backend-api/codex/responses", { method: "POST", headers, body: JSON.stringify(body) });
|
|
294
|
+
if (!res.ok) {
|
|
295
|
+
const err = await res.text();
|
|
296
|
+
throw new Error(`Codex subscription query failed (${res.status}): ${err}`);
|
|
297
|
+
}
|
|
298
|
+
// Parse SSE stream — look for response.completed event
|
|
299
|
+
const text = await res.text();
|
|
300
|
+
const lines = text.split("\n");
|
|
301
|
+
let content = "";
|
|
302
|
+
let usage;
|
|
303
|
+
let finishReason;
|
|
304
|
+
for (const line of lines) {
|
|
305
|
+
if (!line.startsWith("data: "))
|
|
306
|
+
continue;
|
|
307
|
+
try {
|
|
308
|
+
const event = JSON.parse(line.slice(6));
|
|
309
|
+
if (event.type === "response.output_text.done") {
|
|
310
|
+
content += event.text ?? "";
|
|
311
|
+
}
|
|
312
|
+
else if (event.type === "response.completed") {
|
|
313
|
+
const resp = event.response;
|
|
314
|
+
if (resp?.usage)
|
|
315
|
+
usage = resp.usage;
|
|
316
|
+
finishReason = resp?.status;
|
|
317
|
+
// Also extract content from completed response if not yet captured
|
|
318
|
+
if (!content && resp?.output) {
|
|
319
|
+
for (const item of resp.output) {
|
|
320
|
+
if (item.type === "message" && item.content) {
|
|
321
|
+
for (const block of item.content) {
|
|
322
|
+
if (block.type === "output_text")
|
|
323
|
+
content += block.text ?? "";
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch { /* skip non-JSON lines */ }
|
|
331
|
+
}
|
|
234
332
|
return {
|
|
235
333
|
model,
|
|
236
|
-
content
|
|
237
|
-
usage
|
|
334
|
+
content,
|
|
335
|
+
usage: usage
|
|
336
|
+
? {
|
|
337
|
+
prompt_tokens: usage.input_tokens ?? 0,
|
|
338
|
+
completion_tokens: usage.output_tokens ?? 0,
|
|
339
|
+
total_tokens: usage.total_tokens ?? 0,
|
|
340
|
+
}
|
|
341
|
+
: undefined,
|
|
238
342
|
latency_ms: Date.now() - startTime,
|
|
239
|
-
finish_reason:
|
|
343
|
+
finish_reason: finishReason,
|
|
240
344
|
};
|
|
241
345
|
}
|
|
242
|
-
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// Query: Gemini — Cloud Code Assist direct HTTP
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
async function queryGemini(tokens, model, prompt, options) {
|
|
243
350
|
const startTime = Date.now();
|
|
244
|
-
const
|
|
351
|
+
const projectId = await getGeminiProjectId(tokens.accessToken);
|
|
352
|
+
const request = {
|
|
245
353
|
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
246
354
|
};
|
|
247
355
|
if (options?.system_prompt) {
|
|
248
|
-
|
|
356
|
+
request.systemInstruction = { parts: [{ text: options.system_prompt }] };
|
|
249
357
|
}
|
|
250
358
|
const genConfig = {};
|
|
251
359
|
if (options?.temperature !== undefined)
|
|
@@ -253,12 +361,17 @@ async function queryGemini(token, model, prompt, options) {
|
|
|
253
361
|
if (options?.max_tokens !== undefined)
|
|
254
362
|
genConfig.maxOutputTokens = options.max_tokens;
|
|
255
363
|
if (Object.keys(genConfig).length > 0)
|
|
256
|
-
|
|
257
|
-
const
|
|
364
|
+
request.generationConfig = genConfig;
|
|
365
|
+
const body = { model, project: projectId, request };
|
|
366
|
+
const res = await fetch("https://cloudcode-pa.googleapis.com/v1internal:generateContent", {
|
|
258
367
|
method: "POST",
|
|
259
368
|
headers: {
|
|
260
369
|
"Content-Type": "application/json",
|
|
261
|
-
"
|
|
370
|
+
"Accept": "application/json",
|
|
371
|
+
"Authorization": `Bearer ${tokens.accessToken}`,
|
|
372
|
+
"User-Agent": "google-api-nodejs-client/9.15.1",
|
|
373
|
+
"X-Goog-Api-Client": "gl-node/22.17.0",
|
|
374
|
+
"Client-Metadata": "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI",
|
|
262
375
|
},
|
|
263
376
|
body: JSON.stringify(body),
|
|
264
377
|
});
|
|
@@ -266,74 +379,111 @@ async function queryGemini(token, model, prompt, options) {
|
|
|
266
379
|
const err = await res.text();
|
|
267
380
|
throw new Error(`Gemini subscription query failed (${res.status}): ${err}`);
|
|
268
381
|
}
|
|
382
|
+
// Cloud Code Assist wraps the standard Gemini response in a "response" field
|
|
269
383
|
const data = (await res.json());
|
|
270
|
-
const
|
|
384
|
+
const inner = (data.response ?? data);
|
|
385
|
+
const candidates = inner.candidates;
|
|
271
386
|
const parts = candidates?.[0]?.content?.parts;
|
|
272
387
|
const content = parts?.map((p) => p.text ?? "").join("") ?? "";
|
|
273
|
-
const meta =
|
|
388
|
+
const meta = inner.usageMetadata;
|
|
274
389
|
return {
|
|
275
390
|
model,
|
|
276
391
|
content,
|
|
277
|
-
usage: meta
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
392
|
+
usage: meta
|
|
393
|
+
? {
|
|
394
|
+
prompt_tokens: meta.promptTokenCount ?? 0,
|
|
395
|
+
completion_tokens: meta.candidatesTokenCount ?? 0,
|
|
396
|
+
total_tokens: meta.totalTokenCount ?? 0,
|
|
397
|
+
}
|
|
398
|
+
: undefined,
|
|
282
399
|
latency_ms: Date.now() - startTime,
|
|
283
400
|
finish_reason: candidates?.[0]?.finishReason ?? undefined,
|
|
284
401
|
};
|
|
285
402
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// Query: Claude — CLI subprocess (api.anthropic.com requires TLS fingerprint
|
|
405
|
+
// bypass for OAuth tokens, which needs Go's utls library. Node.js can't do it
|
|
406
|
+
// natively, so we use the Claude CLI as a subprocess.)
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
function execCLI(command, args, stdinData, timeoutMs = 120_000) {
|
|
409
|
+
return new Promise((resolve) => {
|
|
410
|
+
const isWin = process.platform === "win32";
|
|
411
|
+
const proc = spawn(command, args, {
|
|
412
|
+
shell: isWin,
|
|
413
|
+
env: { ...process.env },
|
|
414
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
415
|
+
});
|
|
416
|
+
let stdout = "";
|
|
417
|
+
let stderr = "";
|
|
418
|
+
const timer = setTimeout(() => {
|
|
419
|
+
proc.kill();
|
|
420
|
+
resolve({ stdout, stderr: stderr + "\n[TIMEOUT]", code: 124 });
|
|
421
|
+
}, timeoutMs);
|
|
422
|
+
proc.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
423
|
+
proc.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
424
|
+
proc.on("close", (code) => {
|
|
425
|
+
clearTimeout(timer);
|
|
426
|
+
resolve({ stdout, stderr, code: code ?? 1 });
|
|
427
|
+
});
|
|
428
|
+
proc.on("error", (err) => {
|
|
429
|
+
clearTimeout(timer);
|
|
430
|
+
resolve({ stdout, stderr: err.message, code: 1 });
|
|
431
|
+
});
|
|
432
|
+
if (stdinData && proc.stdin) {
|
|
433
|
+
proc.stdin.write(stdinData);
|
|
434
|
+
proc.stdin.end();
|
|
435
|
+
}
|
|
305
436
|
});
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
437
|
+
}
|
|
438
|
+
async function queryClaude(_tokens, model, prompt, options) {
|
|
439
|
+
const startTime = Date.now();
|
|
440
|
+
const args = [
|
|
441
|
+
"--output-format", "json",
|
|
442
|
+
"-p", "-", // Read prompt from stdin
|
|
443
|
+
"--model", model,
|
|
444
|
+
];
|
|
445
|
+
if (options?.max_tokens)
|
|
446
|
+
args.push("--max-tokens", String(options.max_tokens));
|
|
447
|
+
const result = await execCLI("claude", args, prompt, 120_000);
|
|
448
|
+
if (result.code !== 0) {
|
|
449
|
+
throw new Error(`Claude CLI failed (code ${result.code}): ${result.stderr.substring(0, 200)}`);
|
|
450
|
+
}
|
|
451
|
+
// Parse JSON output from claude --output-format json
|
|
452
|
+
let content = "";
|
|
453
|
+
try {
|
|
454
|
+
const data = JSON.parse(result.stdout);
|
|
455
|
+
if (typeof data.result === "string") {
|
|
456
|
+
content = data.result;
|
|
457
|
+
}
|
|
458
|
+
else if (typeof data.content === "string") {
|
|
459
|
+
content = data.content;
|
|
460
|
+
}
|
|
461
|
+
else if (Array.isArray(data.content)) {
|
|
462
|
+
content = data.content
|
|
463
|
+
.filter((b) => b.type === "text")
|
|
464
|
+
.map((b) => b.text ?? "")
|
|
465
|
+
.join("");
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
content = result.stdout;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
content = result.stdout;
|
|
309
473
|
}
|
|
310
|
-
const data = (await res.json());
|
|
311
|
-
const contentBlocks = data.content;
|
|
312
|
-
const text = contentBlocks?.filter((b) => b.type === "text").map((b) => b.text ?? "").join("") ?? "";
|
|
313
|
-
const usage = data.usage;
|
|
314
474
|
return {
|
|
315
475
|
model,
|
|
316
|
-
content
|
|
317
|
-
usage: usage ? {
|
|
318
|
-
prompt_tokens: usage.input_tokens,
|
|
319
|
-
completion_tokens: usage.output_tokens,
|
|
320
|
-
total_tokens: usage.input_tokens + usage.output_tokens,
|
|
321
|
-
} : undefined,
|
|
476
|
+
content,
|
|
322
477
|
latency_ms: Date.now() - startTime,
|
|
323
|
-
finish_reason: data.stop_reason ?? undefined,
|
|
324
478
|
};
|
|
325
479
|
}
|
|
326
|
-
// ---------------------------------------------------------------------------
|
|
327
|
-
// Backend configs
|
|
328
|
-
// ---------------------------------------------------------------------------
|
|
329
480
|
const CLAUDE_BACKEND = {
|
|
330
481
|
id: "claude-sub",
|
|
331
482
|
displayName: "Claude (subscription)",
|
|
332
483
|
readTokens: readClaudeTokens,
|
|
333
484
|
refreshTokens: refreshClaudeToken,
|
|
334
|
-
query:
|
|
485
|
+
query: queryClaude,
|
|
335
486
|
models: [
|
|
336
|
-
{ id: "claude-opus-4-6", name: "Claude Opus 4.6" },
|
|
337
487
|
{ id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5" },
|
|
338
488
|
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
|
|
339
489
|
],
|
|
@@ -355,11 +505,11 @@ const CODEX_BACKEND = {
|
|
|
355
505
|
displayName: "Codex (subscription)",
|
|
356
506
|
readTokens: readCodexTokens,
|
|
357
507
|
refreshTokens: refreshCodexToken,
|
|
358
|
-
query:
|
|
508
|
+
query: queryCodex,
|
|
359
509
|
models: [
|
|
360
|
-
{ id: "gpt-
|
|
361
|
-
{ id: "
|
|
362
|
-
{ id: "
|
|
510
|
+
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
|
511
|
+
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
|
|
512
|
+
{ id: "gpt-5.1-codex-mini", name: "GPT-5.1 Codex Mini" },
|
|
363
513
|
],
|
|
364
514
|
};
|
|
365
515
|
const ALL_BACKENDS = [CLAUDE_BACKEND, GEMINI_BACKEND, CODEX_BACKEND];
|
|
@@ -371,10 +521,6 @@ export class SubscriptionProvider {
|
|
|
371
521
|
backends = [];
|
|
372
522
|
modelToBackend = new Map();
|
|
373
523
|
tokenCache = new Map();
|
|
374
|
-
/**
|
|
375
|
-
* Detect which subscription tokens exist on disk.
|
|
376
|
-
* Returns the number of backends with valid tokens.
|
|
377
|
-
*/
|
|
378
524
|
async detect() {
|
|
379
525
|
for (const backend of ALL_BACKENDS) {
|
|
380
526
|
const tokens = backend.readTokens();
|
|
@@ -402,7 +548,6 @@ export class SubscriptionProvider {
|
|
|
402
548
|
async query(model, prompt, options) {
|
|
403
549
|
const backend = this.modelToBackend.get(model);
|
|
404
550
|
if (!backend) {
|
|
405
|
-
// Partial match fallback
|
|
406
551
|
const match = this.backends.find((b) => b.models.some((m) => model.includes(m.id) || m.id.includes(model)));
|
|
407
552
|
if (!match) {
|
|
408
553
|
throw new Error(`No subscription handles model "${model}". ` +
|
|
@@ -433,6 +578,6 @@ export class SubscriptionProvider {
|
|
|
433
578
|
logger.warn(`Subscription: ${backend.displayName} token refresh failed, trying existing token`);
|
|
434
579
|
}
|
|
435
580
|
}
|
|
436
|
-
return backend.query(tokens
|
|
581
|
+
return backend.query(tokens, model, prompt, options);
|
|
437
582
|
}
|
|
438
583
|
}
|
package/package.json
CHANGED