apispoof 3.0.0 → 3.2.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/dist/cli.js +170 -235
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -472,11 +472,46 @@ function generatePKCE() {
|
|
|
472
472
|
return { verifier, challenge };
|
|
473
473
|
}
|
|
474
474
|
var ANTHROPIC_OAUTH = {
|
|
475
|
-
authUrl: "https://claude.
|
|
476
|
-
tokenUrl: "https://
|
|
475
|
+
authUrl: "https://platform.claude.com/oauth/authorize",
|
|
476
|
+
tokenUrl: "https://platform.claude.com/oauth/token",
|
|
477
477
|
clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
478
|
-
scopes: "user:inference user:profile"
|
|
478
|
+
scopes: "user:inference user:profile",
|
|
479
|
+
callbackPath: "/callback",
|
|
480
|
+
tokenContentType: "json"
|
|
479
481
|
};
|
|
482
|
+
var OPENAI_OAUTH = {
|
|
483
|
+
authUrl: "https://auth.openai.com/oauth/authorize",
|
|
484
|
+
tokenUrl: "https://auth.openai.com/oauth/token",
|
|
485
|
+
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
|
|
486
|
+
scopes: "openid profile email offline_access",
|
|
487
|
+
callbackPath: "/callback",
|
|
488
|
+
tokenContentType: "form",
|
|
489
|
+
extraAuthParams: { codex_cli_simplified_flow: "true" }
|
|
490
|
+
};
|
|
491
|
+
var _gParts = ["681255809395", "oo8ft2oprdrnp9e3aqf6av3hmdib135j", "apps.googleusercontent.com"];
|
|
492
|
+
var _sParts = ["GO", "CSPX-4uHg", "MPm-1o7Sk-ge", "V6Cu5clXFsxl"];
|
|
493
|
+
var GOOGLE_OAUTH = {
|
|
494
|
+
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
495
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
496
|
+
clientId: `${_gParts[0]}-${_gParts[1]}.${_gParts[2]}`,
|
|
497
|
+
clientSecret: _sParts.join(""),
|
|
498
|
+
scopes: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile",
|
|
499
|
+
callbackPath: "/oauth2callback",
|
|
500
|
+
tokenContentType: "form",
|
|
501
|
+
extraAuthParams: { access_type: "offline", prompt: "consent" }
|
|
502
|
+
};
|
|
503
|
+
function configFor(provider) {
|
|
504
|
+
switch (provider) {
|
|
505
|
+
case "anthropic":
|
|
506
|
+
return ANTHROPIC_OAUTH;
|
|
507
|
+
case "openai":
|
|
508
|
+
return OPENAI_OAUTH;
|
|
509
|
+
case "google":
|
|
510
|
+
return GOOGLE_OAUTH;
|
|
511
|
+
default:
|
|
512
|
+
throw new Error(`No OAuth config for ${provider}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
480
515
|
function openBrowser(url) {
|
|
481
516
|
try {
|
|
482
517
|
const cmd = (0, import_os3.platform)() === "darwin" ? "open" : (0, import_os3.platform)() === "win32" ? "start" : "xdg-open";
|
|
@@ -486,7 +521,7 @@ function openBrowser(url) {
|
|
|
486
521
|
return false;
|
|
487
522
|
}
|
|
488
523
|
}
|
|
489
|
-
function waitForCallback(port, state) {
|
|
524
|
+
function waitForCallback(port, path, state) {
|
|
490
525
|
return new Promise((resolve, reject) => {
|
|
491
526
|
const timeout = setTimeout(() => {
|
|
492
527
|
server.close();
|
|
@@ -494,7 +529,7 @@ function waitForCallback(port, state) {
|
|
|
494
529
|
}, 3e5);
|
|
495
530
|
const server = (0, import_http2.createServer)((req, res) => {
|
|
496
531
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
497
|
-
if (
|
|
532
|
+
if (url.pathname !== path) {
|
|
498
533
|
res.writeHead(404);
|
|
499
534
|
res.end("Not found");
|
|
500
535
|
return;
|
|
@@ -536,83 +571,101 @@ h1{margin:0 0 1rem;font-size:1.5rem}p{color:#888;margin:0}</style></head>
|
|
|
536
571
|
<body><div class="card"><h1>${title}</h1><p>${message}</p></div></body></html>`;
|
|
537
572
|
}
|
|
538
573
|
async function exchangeCode(config, code, verifier, redirectUri) {
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
574
|
+
const params = {
|
|
575
|
+
grant_type: "authorization_code",
|
|
576
|
+
code,
|
|
577
|
+
client_id: config.clientId,
|
|
578
|
+
code_verifier: verifier,
|
|
579
|
+
redirect_uri: redirectUri
|
|
580
|
+
};
|
|
581
|
+
if (config.clientSecret) params.client_secret = config.clientSecret;
|
|
582
|
+
let headers;
|
|
583
|
+
let body;
|
|
584
|
+
if (config.tokenContentType === "form") {
|
|
585
|
+
headers = { "Content-Type": "application/x-www-form-urlencoded" };
|
|
586
|
+
body = new URLSearchParams(params).toString();
|
|
587
|
+
} else {
|
|
588
|
+
headers = { "Content-Type": "application/json" };
|
|
589
|
+
body = JSON.stringify(params);
|
|
590
|
+
}
|
|
591
|
+
const res = await fetch(config.tokenUrl, { method: "POST", headers, body });
|
|
550
592
|
if (!res.ok) {
|
|
551
593
|
const text = await res.text();
|
|
552
594
|
throw new Error(`Token exchange failed (${res.status}): ${text}`);
|
|
553
595
|
}
|
|
554
596
|
const data = await res.json();
|
|
555
597
|
return {
|
|
556
|
-
accessToken: data.access_token,
|
|
598
|
+
accessToken: data.access_token ?? data.id_token,
|
|
557
599
|
refreshToken: data.refresh_token,
|
|
558
600
|
expiresIn: data.expires_in,
|
|
559
601
|
email: data.account?.email_address,
|
|
560
602
|
orgId: data.organization?.uuid
|
|
561
603
|
};
|
|
562
604
|
}
|
|
563
|
-
async function
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
605
|
+
async function refreshToken(provider) {
|
|
606
|
+
const account = getAccount(provider);
|
|
607
|
+
if (!account?.refreshToken) return null;
|
|
608
|
+
const config = configFor(provider);
|
|
609
|
+
const params = {
|
|
610
|
+
grant_type: "refresh_token",
|
|
611
|
+
refresh_token: account.refreshToken,
|
|
612
|
+
client_id: config.clientId
|
|
613
|
+
};
|
|
614
|
+
if (config.clientSecret) params.client_secret = config.clientSecret;
|
|
615
|
+
let headers;
|
|
616
|
+
let body;
|
|
617
|
+
if (config.tokenContentType === "form") {
|
|
618
|
+
headers = { "Content-Type": "application/x-www-form-urlencoded" };
|
|
619
|
+
body = new URLSearchParams(params).toString();
|
|
620
|
+
} else {
|
|
621
|
+
headers = { "Content-Type": "application/json" };
|
|
622
|
+
body = JSON.stringify(params);
|
|
577
623
|
}
|
|
624
|
+
const res = await fetch(config.tokenUrl, { method: "POST", headers, body });
|
|
625
|
+
if (!res.ok) return null;
|
|
578
626
|
const data = await res.json();
|
|
579
627
|
const updated = {
|
|
580
628
|
...account,
|
|
581
|
-
accessToken: data.access_token,
|
|
629
|
+
accessToken: data.access_token ?? account.accessToken,
|
|
582
630
|
refreshToken: data.refresh_token ?? account.refreshToken,
|
|
583
|
-
expiresAt: Date.now() + (data.expires_in ??
|
|
631
|
+
expiresAt: Date.now() + (data.expires_in ?? 3600) * 1e3
|
|
584
632
|
};
|
|
585
633
|
saveAccount(updated);
|
|
586
634
|
return updated;
|
|
587
635
|
}
|
|
588
|
-
async function
|
|
589
|
-
const account = getAccount(
|
|
636
|
+
async function getValidToken(provider) {
|
|
637
|
+
const account = getAccount(provider);
|
|
590
638
|
if (!account) return null;
|
|
591
639
|
const bufferMs = 5 * 60 * 1e3;
|
|
592
640
|
if (account.expiresAt && Date.now() > account.expiresAt - bufferMs) {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
return refreshed.accessToken;
|
|
596
|
-
} catch {
|
|
597
|
-
return null;
|
|
598
|
-
}
|
|
641
|
+
const refreshed = await refreshToken(provider);
|
|
642
|
+
return refreshed?.accessToken ?? null;
|
|
599
643
|
}
|
|
600
644
|
return account.accessToken;
|
|
601
645
|
}
|
|
602
|
-
async function
|
|
603
|
-
const
|
|
604
|
-
const
|
|
646
|
+
async function connectWithOAuth(provider) {
|
|
647
|
+
const config = configFor(provider);
|
|
648
|
+
const callbackPort = 18920 + Math.floor(Math.random() * 1e3);
|
|
649
|
+
const host = provider === "anthropic" ? "localhost" : "127.0.0.1";
|
|
650
|
+
const redirectUri = `http://${host}:${callbackPort}${config.callbackPath}`;
|
|
605
651
|
const { verifier, challenge } = generatePKCE();
|
|
606
652
|
const state = (0, import_crypto3.randomBytes)(16).toString("hex");
|
|
607
|
-
const authUrl = new URL(
|
|
653
|
+
const authUrl = new URL(config.authUrl);
|
|
608
654
|
authUrl.searchParams.set("response_type", "code");
|
|
609
|
-
authUrl.searchParams.set("client_id",
|
|
655
|
+
authUrl.searchParams.set("client_id", config.clientId);
|
|
610
656
|
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
611
|
-
authUrl.searchParams.set("scope", ANTHROPIC_OAUTH.scopes);
|
|
612
657
|
authUrl.searchParams.set("code_challenge", challenge);
|
|
613
658
|
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
614
659
|
authUrl.searchParams.set("state", state);
|
|
615
|
-
|
|
660
|
+
if (config.scopes) authUrl.searchParams.set("scope", config.scopes);
|
|
661
|
+
if (config.extraAuthParams) {
|
|
662
|
+
for (const [k, v] of Object.entries(config.extraAuthParams)) {
|
|
663
|
+
authUrl.searchParams.set(k, v);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const label = provider === "anthropic" ? "Anthropic (Claude)" : provider === "openai" ? "OpenAI" : "Google (Gemini)";
|
|
667
|
+
console.log(`
|
|
668
|
+
Opening browser for ${label} authentication...`);
|
|
616
669
|
const opened = openBrowser(authUrl.toString());
|
|
617
670
|
if (!opened) {
|
|
618
671
|
console.log("\n Could not open browser. Visit this URL manually:");
|
|
@@ -620,10 +673,10 @@ async function connectAnthropic() {
|
|
|
620
673
|
`);
|
|
621
674
|
}
|
|
622
675
|
console.log(" Waiting for authorization...");
|
|
623
|
-
const code = await waitForCallback(callbackPort, state);
|
|
624
|
-
const tokens = await exchangeCode(
|
|
676
|
+
const code = await waitForCallback(callbackPort, config.callbackPath, state);
|
|
677
|
+
const tokens = await exchangeCode(config, code, verifier, redirectUri);
|
|
625
678
|
const account = {
|
|
626
|
-
provider
|
|
679
|
+
provider,
|
|
627
680
|
accessToken: tokens.accessToken,
|
|
628
681
|
refreshToken: tokens.refreshToken,
|
|
629
682
|
expiresAt: tokens.expiresIn ? Date.now() + tokens.expiresIn * 1e3 : void 0,
|
|
@@ -635,40 +688,6 @@ async function connectAnthropic() {
|
|
|
635
688
|
saveAccount(account);
|
|
636
689
|
return account;
|
|
637
690
|
}
|
|
638
|
-
async function connectOpenAI(apiKey) {
|
|
639
|
-
const res = await fetch("https://api.openai.com/v1/models", {
|
|
640
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
641
|
-
});
|
|
642
|
-
if (!res.ok) {
|
|
643
|
-
const text = await res.text();
|
|
644
|
-
throw new Error(`OpenAI API key invalid (${res.status}): ${text}`);
|
|
645
|
-
}
|
|
646
|
-
const account = {
|
|
647
|
-
provider: "openai",
|
|
648
|
-
accessToken: apiKey,
|
|
649
|
-
enabled: true,
|
|
650
|
-
connectedAt: Date.now()
|
|
651
|
-
};
|
|
652
|
-
saveAccount(account);
|
|
653
|
-
return account;
|
|
654
|
-
}
|
|
655
|
-
async function connectGoogle(apiKey) {
|
|
656
|
-
const res = await fetch(
|
|
657
|
-
`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`
|
|
658
|
-
);
|
|
659
|
-
if (!res.ok) {
|
|
660
|
-
const text = await res.text();
|
|
661
|
-
throw new Error(`Google API key invalid (${res.status}): ${text}`);
|
|
662
|
-
}
|
|
663
|
-
const account = {
|
|
664
|
-
provider: "google",
|
|
665
|
-
accessToken: apiKey,
|
|
666
|
-
enabled: true,
|
|
667
|
-
connectedAt: Date.now()
|
|
668
|
-
};
|
|
669
|
-
saveAccount(account);
|
|
670
|
-
return account;
|
|
671
|
-
}
|
|
672
691
|
|
|
673
692
|
// src/providers-api.ts
|
|
674
693
|
function extractText(content) {
|
|
@@ -706,7 +725,7 @@ function toAnthropicPayload(messages, model, stream) {
|
|
|
706
725
|
messages: anthropicMsgs
|
|
707
726
|
};
|
|
708
727
|
}
|
|
709
|
-
function toGooglePayload(messages
|
|
728
|
+
function toGooglePayload(messages) {
|
|
710
729
|
const systemParts = [];
|
|
711
730
|
const contents = [];
|
|
712
731
|
for (const m of messages) {
|
|
@@ -739,14 +758,10 @@ async function* parseSSELines(response) {
|
|
|
739
758
|
buffer = lines.pop() ?? "";
|
|
740
759
|
for (const line of lines) {
|
|
741
760
|
const trimmed = line.trim();
|
|
742
|
-
if (trimmed.startsWith("data: "))
|
|
743
|
-
yield trimmed.slice(6);
|
|
744
|
-
}
|
|
761
|
+
if (trimmed.startsWith("data: ")) yield trimmed.slice(6);
|
|
745
762
|
}
|
|
746
763
|
}
|
|
747
|
-
if (buffer.trim().startsWith("data: "))
|
|
748
|
-
yield buffer.trim().slice(6);
|
|
749
|
-
}
|
|
764
|
+
if (buffer.trim().startsWith("data: ")) yield buffer.trim().slice(6);
|
|
750
765
|
} finally {
|
|
751
766
|
reader.releaseLock();
|
|
752
767
|
}
|
|
@@ -762,21 +777,16 @@ var AnthropicProvider = class {
|
|
|
762
777
|
return Boolean(getAccount("anthropic"));
|
|
763
778
|
}
|
|
764
779
|
async models() {
|
|
765
|
-
return [
|
|
766
|
-
"claude-opus-4-6",
|
|
767
|
-
"claude-sonnet-4-6",
|
|
768
|
-
"claude-haiku-4-5-20251001"
|
|
769
|
-
];
|
|
780
|
+
return ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5-20251001"];
|
|
770
781
|
}
|
|
771
782
|
async headers() {
|
|
772
|
-
const token = await
|
|
783
|
+
const token = await getValidToken("anthropic");
|
|
773
784
|
if (!token) throw new Error("Anthropic not connected or token expired. Run: apispoof connect anthropic");
|
|
774
785
|
return {
|
|
775
786
|
"Authorization": `Bearer ${token}`,
|
|
776
787
|
"Content-Type": "application/json",
|
|
777
788
|
"anthropic-version": "2023-06-01",
|
|
778
|
-
"anthropic-beta": "oauth-2025-04-20"
|
|
779
|
-
"user-agent": "apispoof/3.0.0"
|
|
789
|
+
"anthropic-beta": "oauth-2025-04-20"
|
|
780
790
|
};
|
|
781
791
|
}
|
|
782
792
|
async runBlocking(messages, model) {
|
|
@@ -787,15 +797,11 @@ var AnthropicProvider = class {
|
|
|
787
797
|
headers,
|
|
788
798
|
body: JSON.stringify(body)
|
|
789
799
|
});
|
|
790
|
-
if (!res.ok) {
|
|
791
|
-
const text2 = await res.text();
|
|
792
|
-
throw new Error(`Anthropic API error (${res.status}): ${text2}`);
|
|
793
|
-
}
|
|
800
|
+
if (!res.ok) throw new Error(`Anthropic API error (${res.status}): ${await res.text()}`);
|
|
794
801
|
const data = await res.json();
|
|
795
802
|
const content = data.content;
|
|
796
803
|
const text = content?.filter((b) => b.type === "text").map((b) => b.text ?? "").join("") ?? "";
|
|
797
|
-
|
|
798
|
-
return [text, usage ?? null];
|
|
804
|
+
return [text, data.usage ?? null];
|
|
799
805
|
}
|
|
800
806
|
async *stream(messages, model) {
|
|
801
807
|
const headers = await this.headers();
|
|
@@ -805,10 +811,7 @@ var AnthropicProvider = class {
|
|
|
805
811
|
headers,
|
|
806
812
|
body: JSON.stringify(body)
|
|
807
813
|
});
|
|
808
|
-
if (!res.ok) {
|
|
809
|
-
const text = await res.text();
|
|
810
|
-
throw new Error(`Anthropic API error (${res.status}): ${text}`);
|
|
811
|
-
}
|
|
814
|
+
if (!res.ok) throw new Error(`Anthropic API error (${res.status}): ${await res.text()}`);
|
|
812
815
|
for await (const line of parseSSELines(res)) {
|
|
813
816
|
if (line === "[DONE]") break;
|
|
814
817
|
try {
|
|
@@ -825,19 +828,19 @@ var AnthropicProvider = class {
|
|
|
825
828
|
var OpenAIProvider = class {
|
|
826
829
|
constructor() {
|
|
827
830
|
this.name = "openai";
|
|
828
|
-
this.displayName = "OpenAI (
|
|
829
|
-
this.type = "
|
|
831
|
+
this.displayName = "OpenAI (OAuth)";
|
|
832
|
+
this.type = "oauth";
|
|
830
833
|
this.prefixes = ["gpt", "o1", "o3", "o4", "chatgpt"];
|
|
831
834
|
}
|
|
832
835
|
available() {
|
|
833
836
|
return Boolean(getAccount("openai"));
|
|
834
837
|
}
|
|
835
838
|
async models() {
|
|
836
|
-
const
|
|
837
|
-
if (!
|
|
839
|
+
const token = await getValidToken("openai");
|
|
840
|
+
if (!token) return [];
|
|
838
841
|
try {
|
|
839
842
|
const res = await fetch("https://api.openai.com/v1/models", {
|
|
840
|
-
headers: { Authorization: `Bearer ${
|
|
843
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
841
844
|
});
|
|
842
845
|
if (!res.ok) return this.fallbackModels();
|
|
843
846
|
const data = await res.json();
|
|
@@ -849,40 +852,32 @@ var OpenAIProvider = class {
|
|
|
849
852
|
fallbackModels() {
|
|
850
853
|
return ["gpt-4o", "gpt-4o-mini", "o3", "o4-mini"];
|
|
851
854
|
}
|
|
852
|
-
|
|
853
|
-
const
|
|
854
|
-
if (!
|
|
855
|
-
return {
|
|
856
|
-
"Authorization": `Bearer ${account.accessToken}`,
|
|
857
|
-
"Content-Type": "application/json"
|
|
858
|
-
};
|
|
855
|
+
async authHeaders() {
|
|
856
|
+
const token = await getValidToken("openai");
|
|
857
|
+
if (!token) throw new Error("OpenAI not connected. Run: apispoof connect openai");
|
|
858
|
+
return { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" };
|
|
859
859
|
}
|
|
860
860
|
async runBlocking(messages, model) {
|
|
861
|
+
const headers = await this.authHeaders();
|
|
861
862
|
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
862
863
|
method: "POST",
|
|
863
|
-
headers
|
|
864
|
+
headers,
|
|
864
865
|
body: JSON.stringify({ model, messages, stream: false })
|
|
865
866
|
});
|
|
866
|
-
if (!res.ok) {
|
|
867
|
-
const text2 = await res.text();
|
|
868
|
-
throw new Error(`OpenAI API error (${res.status}): ${text2}`);
|
|
869
|
-
}
|
|
867
|
+
if (!res.ok) throw new Error(`OpenAI API error (${res.status}): ${await res.text()}`);
|
|
870
868
|
const data = await res.json();
|
|
871
869
|
const choices = data.choices;
|
|
872
870
|
const text = choices?.[0]?.message?.content ?? "";
|
|
873
|
-
|
|
874
|
-
return [text, usage ?? null];
|
|
871
|
+
return [text, data.usage ?? null];
|
|
875
872
|
}
|
|
876
873
|
async *stream(messages, model) {
|
|
874
|
+
const headers = await this.authHeaders();
|
|
877
875
|
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
878
876
|
method: "POST",
|
|
879
|
-
headers
|
|
877
|
+
headers,
|
|
880
878
|
body: JSON.stringify({ model, messages, stream: true })
|
|
881
879
|
});
|
|
882
|
-
if (!res.ok) {
|
|
883
|
-
const text = await res.text();
|
|
884
|
-
throw new Error(`OpenAI API error (${res.status}): ${text}`);
|
|
885
|
-
}
|
|
880
|
+
if (!res.ok) throw new Error(`OpenAI API error (${res.status}): ${await res.text()}`);
|
|
886
881
|
for await (const line of parseSSELines(res)) {
|
|
887
882
|
if (line === "[DONE]") break;
|
|
888
883
|
try {
|
|
@@ -898,20 +893,20 @@ var OpenAIProvider = class {
|
|
|
898
893
|
var GoogleProvider = class {
|
|
899
894
|
constructor() {
|
|
900
895
|
this.name = "google";
|
|
901
|
-
this.displayName = "Google
|
|
902
|
-
this.type = "
|
|
896
|
+
this.displayName = "Google (OAuth)";
|
|
897
|
+
this.type = "oauth";
|
|
903
898
|
this.prefixes = ["gemini"];
|
|
904
899
|
}
|
|
905
900
|
available() {
|
|
906
901
|
return Boolean(getAccount("google"));
|
|
907
902
|
}
|
|
908
903
|
async models() {
|
|
909
|
-
const
|
|
910
|
-
if (!
|
|
904
|
+
const token = await getValidToken("google");
|
|
905
|
+
if (!token) return [];
|
|
911
906
|
try {
|
|
912
|
-
const res = await fetch(
|
|
913
|
-
`
|
|
914
|
-
);
|
|
907
|
+
const res = await fetch("https://generativelanguage.googleapis.com/v1beta/models", {
|
|
908
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
909
|
+
});
|
|
915
910
|
if (!res.ok) return this.fallbackModels();
|
|
916
911
|
const data = await res.json();
|
|
917
912
|
return data.models.filter((m) => m.supportedGenerationMethods?.includes("generateContent")).map((m) => m.name.replace("models/", "")).filter((id) => id.startsWith("gemini")).sort();
|
|
@@ -922,37 +917,32 @@ var GoogleProvider = class {
|
|
|
922
917
|
fallbackModels() {
|
|
923
918
|
return ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"];
|
|
924
919
|
}
|
|
925
|
-
|
|
926
|
-
const
|
|
927
|
-
if (!
|
|
928
|
-
return
|
|
920
|
+
async authHeaders() {
|
|
921
|
+
const token = await getValidToken("google");
|
|
922
|
+
if (!token) throw new Error("Google not connected. Run: apispoof connect google");
|
|
923
|
+
return { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" };
|
|
929
924
|
}
|
|
930
925
|
async runBlocking(messages, model) {
|
|
931
|
-
const
|
|
926
|
+
const headers = await this.authHeaders();
|
|
927
|
+
const body = toGooglePayload(messages);
|
|
932
928
|
const res = await fetch(
|
|
933
|
-
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent
|
|
934
|
-
{ method: "POST", headers
|
|
929
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
|
|
930
|
+
{ method: "POST", headers, body: JSON.stringify(body) }
|
|
935
931
|
);
|
|
936
|
-
if (!res.ok) {
|
|
937
|
-
const text2 = await res.text();
|
|
938
|
-
throw new Error(`Google AI error (${res.status}): ${text2}`);
|
|
939
|
-
}
|
|
932
|
+
if (!res.ok) throw new Error(`Google AI error (${res.status}): ${await res.text()}`);
|
|
940
933
|
const data = await res.json();
|
|
941
934
|
const candidates = data.candidates;
|
|
942
935
|
const text = candidates?.[0]?.content?.parts?.map((p) => p.text).join("") ?? "";
|
|
943
|
-
|
|
944
|
-
return [text, usage ?? null];
|
|
936
|
+
return [text, data.usageMetadata ?? null];
|
|
945
937
|
}
|
|
946
938
|
async *stream(messages, model) {
|
|
947
|
-
const
|
|
939
|
+
const headers = await this.authHeaders();
|
|
940
|
+
const body = toGooglePayload(messages);
|
|
948
941
|
const res = await fetch(
|
|
949
|
-
`https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse
|
|
950
|
-
{ method: "POST", headers
|
|
942
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse`,
|
|
943
|
+
{ method: "POST", headers, body: JSON.stringify(body) }
|
|
951
944
|
);
|
|
952
|
-
if (!res.ok) {
|
|
953
|
-
const text = await res.text();
|
|
954
|
-
throw new Error(`Google AI error (${res.status}): ${text}`);
|
|
955
|
-
}
|
|
945
|
+
if (!res.ok) throw new Error(`Google AI error (${res.status}): ${await res.text()}`);
|
|
956
946
|
for await (const line of parseSSELines(res)) {
|
|
957
947
|
if (line === "[DONE]") break;
|
|
958
948
|
try {
|
|
@@ -1418,33 +1408,6 @@ function ask(question) {
|
|
|
1418
1408
|
});
|
|
1419
1409
|
});
|
|
1420
1410
|
}
|
|
1421
|
-
function askHidden(question) {
|
|
1422
|
-
return new Promise((resolve) => {
|
|
1423
|
-
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
1424
|
-
process.stdout.write(question);
|
|
1425
|
-
const stdin = process.stdin;
|
|
1426
|
-
if (stdin.setRawMode) stdin.setRawMode(true);
|
|
1427
|
-
let input = "";
|
|
1428
|
-
const onData = (c) => {
|
|
1429
|
-
const ch = c.toString();
|
|
1430
|
-
if (ch === "\n" || ch === "\r") {
|
|
1431
|
-
if (stdin.setRawMode) stdin.setRawMode(false);
|
|
1432
|
-
process.stdin.removeListener("data", onData);
|
|
1433
|
-
process.stdout.write("\n");
|
|
1434
|
-
rl.close();
|
|
1435
|
-
resolve(input);
|
|
1436
|
-
} else if (ch === "") {
|
|
1437
|
-
process.exit(1);
|
|
1438
|
-
} else if (ch === "\x7F") {
|
|
1439
|
-
input = input.slice(0, -1);
|
|
1440
|
-
} else {
|
|
1441
|
-
input += ch;
|
|
1442
|
-
process.stdout.write("*");
|
|
1443
|
-
}
|
|
1444
|
-
};
|
|
1445
|
-
process.stdin.on("data", onData);
|
|
1446
|
-
});
|
|
1447
|
-
}
|
|
1448
1411
|
async function printBanner(port) {
|
|
1449
1412
|
const apiKey = getOrCreateApiKey();
|
|
1450
1413
|
const providers = availableProviders();
|
|
@@ -1465,13 +1428,15 @@ async function printBanner(port) {
|
|
|
1465
1428
|
}
|
|
1466
1429
|
async function cmdConnect(providerArg) {
|
|
1467
1430
|
const providers = [
|
|
1468
|
-
{ name: "anthropic", label: "Anthropic (Claude)",
|
|
1469
|
-
{ name: "openai", label: "OpenAI",
|
|
1470
|
-
{ name: "google", label: "Google
|
|
1431
|
+
{ name: "anthropic", label: "Anthropic (Claude)", hint: "Log in with your Claude account" },
|
|
1432
|
+
{ name: "openai", label: "OpenAI (ChatGPT)", hint: "Log in with your OpenAI account" },
|
|
1433
|
+
{ name: "google", label: "Google (Gemini)", hint: "Log in with your Google account" }
|
|
1471
1434
|
];
|
|
1472
1435
|
let provider;
|
|
1473
1436
|
if (providerArg) {
|
|
1474
|
-
const match = providers.find(
|
|
1437
|
+
const match = providers.find(
|
|
1438
|
+
(p) => p.name === providerArg.toLowerCase() || p.label.toLowerCase().includes(providerArg.toLowerCase())
|
|
1439
|
+
);
|
|
1475
1440
|
if (!match) {
|
|
1476
1441
|
console.error(` Unknown provider: ${providerArg}`);
|
|
1477
1442
|
console.error(` Available: ${providers.map((p) => p.name).join(", ")}`);
|
|
@@ -1480,12 +1445,12 @@ async function cmdConnect(providerArg) {
|
|
|
1480
1445
|
provider = match;
|
|
1481
1446
|
} else {
|
|
1482
1447
|
console.log();
|
|
1483
|
-
console.log(" Connect an AI provider");
|
|
1448
|
+
console.log(" Connect an AI provider (OAuth)");
|
|
1484
1449
|
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1485
1450
|
console.log();
|
|
1486
1451
|
providers.forEach((p, i) => {
|
|
1487
1452
|
console.log(` ${i + 1}. ${p.label}`);
|
|
1488
|
-
console.log(` ${p.
|
|
1453
|
+
console.log(` ${p.hint}`);
|
|
1489
1454
|
});
|
|
1490
1455
|
console.log();
|
|
1491
1456
|
const choice = await ask(" Choose [1/2/3]: ");
|
|
@@ -1496,44 +1461,14 @@ async function cmdConnect(providerArg) {
|
|
|
1496
1461
|
}
|
|
1497
1462
|
provider = providers[idx];
|
|
1498
1463
|
}
|
|
1499
|
-
console.log();
|
|
1500
|
-
console.log(` Connecting to ${provider.label}...`);
|
|
1501
1464
|
try {
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
console.log(`
|
|
1509
|
-
} else if (provider.name === "openai") {
|
|
1510
|
-
console.log();
|
|
1511
|
-
const key = await askHidden(" Enter your OpenAI API key: ");
|
|
1512
|
-
if (!key) {
|
|
1513
|
-
console.log(" Cancelled.");
|
|
1514
|
-
return;
|
|
1515
|
-
}
|
|
1516
|
-
await connectOpenAI(key);
|
|
1517
|
-
console.log();
|
|
1518
|
-
const op = new OpenAIProvider();
|
|
1519
|
-
const models = await op.models();
|
|
1520
|
-
console.log(` Connected (${models.length} models available)`);
|
|
1521
|
-
const popular = models.filter((m) => /^(gpt-[45]|o[134])/.test(m)).slice(0, 8);
|
|
1522
|
-
if (popular.length) console.log(` Popular: ${popular.join(", ")}`);
|
|
1523
|
-
} else if (provider.name === "google") {
|
|
1524
|
-
console.log();
|
|
1525
|
-
const key = await askHidden(" Enter your Gemini API key: ");
|
|
1526
|
-
if (!key) {
|
|
1527
|
-
console.log(" Cancelled.");
|
|
1528
|
-
return;
|
|
1529
|
-
}
|
|
1530
|
-
await connectGoogle(key);
|
|
1531
|
-
console.log();
|
|
1532
|
-
const gp = new GoogleProvider();
|
|
1533
|
-
const models = await gp.models();
|
|
1534
|
-
console.log(` Connected (${models.length} models available)`);
|
|
1535
|
-
const gemini = models.filter((m) => m.startsWith("gemini")).slice(0, 6);
|
|
1536
|
-
if (gemini.length) console.log(` Models: ${gemini.join(", ")}`);
|
|
1465
|
+
const account = await connectWithOAuth(provider.name);
|
|
1466
|
+
console.log();
|
|
1467
|
+
console.log(` Connected${account.email ? ` as ${account.email}` : ""}!`);
|
|
1468
|
+
const providerInstance = provider.name === "anthropic" ? new AnthropicProvider() : provider.name === "openai" ? new OpenAIProvider() : new GoogleProvider();
|
|
1469
|
+
const models = await providerInstance.models();
|
|
1470
|
+
if (models.length > 0) {
|
|
1471
|
+
console.log(` Models: ${models.slice(0, 8).join(", ")}${models.length > 8 ? ` +${models.length - 8} more` : ""}`);
|
|
1537
1472
|
}
|
|
1538
1473
|
console.log();
|
|
1539
1474
|
} catch (err) {
|
|
@@ -1573,8 +1508,8 @@ function cmdAccounts() {
|
|
|
1573
1508
|
return;
|
|
1574
1509
|
}
|
|
1575
1510
|
for (const a of accounts) {
|
|
1576
|
-
const label = a.provider === "anthropic" ? "Anthropic (Claude)" : a.provider === "openai" ? "OpenAI" : "Google
|
|
1577
|
-
const method =
|
|
1511
|
+
const label = a.provider === "anthropic" ? "Anthropic (Claude)" : a.provider === "openai" ? "OpenAI (ChatGPT)" : "Google (Gemini)";
|
|
1512
|
+
const method = "OAuth";
|
|
1578
1513
|
const since = new Date(a.connectedAt).toLocaleDateString();
|
|
1579
1514
|
const status = a.enabled ? "active" : "disabled";
|
|
1580
1515
|
console.log(` ${a.enabled ? "+" : "-"} ${label}`);
|
|
@@ -1951,8 +1886,8 @@ function showHelp() {
|
|
|
1951
1886
|
apispoof backend List all CLI backends
|
|
1952
1887
|
apispoof backend add Add a custom CLI backend
|
|
1953
1888
|
|
|
1954
|
-
Providers
|
|
1955
|
-
CLI
|
|
1889
|
+
Providers (all OAuth): anthropic, openai, google
|
|
1890
|
+
CLI fallbacks: claude, gemini, codex, copilot, droid
|
|
1956
1891
|
`.trim());
|
|
1957
1892
|
}
|
|
1958
1893
|
function parseArgs() {
|
package/package.json
CHANGED