fix(ai): replace curl with fetch in Anthropic OAuth token exchange

This commit is contained in:
Mario Zechner
2026-03-14 03:21:08 +01:00
parent 9b794558c6
commit c7309aedac
2 changed files with 29 additions and 71 deletions

View File

@@ -257,7 +257,7 @@ export const MODELS = {
cacheRead: 0.5, cacheRead: 0.5,
cacheWrite: 6.25, cacheWrite: 6.25,
}, },
contextWindow: 200000, contextWindow: 1000000,
maxTokens: 128000, maxTokens: 128000,
} satisfies Model<"bedrock-converse-stream">, } satisfies Model<"bedrock-converse-stream">,
"anthropic.claude-sonnet-4-20250514-v1:0": { "anthropic.claude-sonnet-4-20250514-v1:0": {
@@ -308,7 +308,7 @@ export const MODELS = {
cacheRead: 0.3, cacheRead: 0.3,
cacheWrite: 3.75, cacheWrite: 3.75,
}, },
contextWindow: 200000, contextWindow: 1000000,
maxTokens: 64000, maxTokens: 64000,
} satisfies Model<"bedrock-converse-stream">, } satisfies Model<"bedrock-converse-stream">,
"deepseek.r1-v1:0": { "deepseek.r1-v1:0": {
@@ -410,7 +410,7 @@ export const MODELS = {
cacheRead: 0.5, cacheRead: 0.5,
cacheWrite: 6.25, cacheWrite: 6.25,
}, },
contextWindow: 200000, contextWindow: 1000000,
maxTokens: 128000, maxTokens: 128000,
} satisfies Model<"bedrock-converse-stream">, } satisfies Model<"bedrock-converse-stream">,
"eu.anthropic.claude-sonnet-4-20250514-v1:0": { "eu.anthropic.claude-sonnet-4-20250514-v1:0": {
@@ -461,7 +461,7 @@ export const MODELS = {
cacheRead: 0.3, cacheRead: 0.3,
cacheWrite: 3.75, cacheWrite: 3.75,
}, },
contextWindow: 200000, contextWindow: 1000000,
maxTokens: 64000, maxTokens: 64000,
} satisfies Model<"bedrock-converse-stream">, } satisfies Model<"bedrock-converse-stream">,
"global.anthropic.claude-haiku-4-5-20251001-v1:0": { "global.anthropic.claude-haiku-4-5-20251001-v1:0": {
@@ -512,7 +512,7 @@ export const MODELS = {
cacheRead: 0.5, cacheRead: 0.5,
cacheWrite: 6.25, cacheWrite: 6.25,
}, },
contextWindow: 200000, contextWindow: 1000000,
maxTokens: 128000, maxTokens: 128000,
} satisfies Model<"bedrock-converse-stream">, } satisfies Model<"bedrock-converse-stream">,
"global.anthropic.claude-sonnet-4-20250514-v1:0": { "global.anthropic.claude-sonnet-4-20250514-v1:0": {
@@ -563,7 +563,7 @@ export const MODELS = {
cacheRead: 0.3, cacheRead: 0.3,
cacheWrite: 3.75, cacheWrite: 3.75,
}, },
contextWindow: 200000, contextWindow: 1000000,
maxTokens: 64000, maxTokens: 64000,
} satisfies Model<"bedrock-converse-stream">, } satisfies Model<"bedrock-converse-stream">,
"google.gemma-3-27b-it": { "google.gemma-3-27b-it": {
@@ -1294,7 +1294,7 @@ export const MODELS = {
cacheRead: 0.5, cacheRead: 0.5,
cacheWrite: 6.25, cacheWrite: 6.25,
}, },
contextWindow: 200000, contextWindow: 1000000,
maxTokens: 128000, maxTokens: 128000,
} satisfies Model<"bedrock-converse-stream">, } satisfies Model<"bedrock-converse-stream">,
"us.anthropic.claude-sonnet-4-20250514-v1:0": { "us.anthropic.claude-sonnet-4-20250514-v1:0": {
@@ -1345,7 +1345,7 @@ export const MODELS = {
cacheRead: 0.3, cacheRead: 0.3,
cacheWrite: 3.75, cacheWrite: 3.75,
}, },
contextWindow: 200000, contextWindow: 1000000,
maxTokens: 64000, maxTokens: 64000,
} satisfies Model<"bedrock-converse-stream">, } satisfies Model<"bedrock-converse-stream">,
"writer.palmyra-x4-v1:0": { "writer.palmyra-x4-v1:0": {
@@ -1721,7 +1721,7 @@ export const MODELS = {
cacheRead: 0.5, cacheRead: 0.5,
cacheWrite: 6.25, cacheWrite: 6.25,
}, },
contextWindow: 200000, contextWindow: 1000000,
maxTokens: 128000, maxTokens: 128000,
} satisfies Model<"anthropic-messages">, } satisfies Model<"anthropic-messages">,
"claude-sonnet-4-0": { "claude-sonnet-4-0": {
@@ -5898,7 +5898,7 @@ export const MODELS = {
cacheRead: 0.5, cacheRead: 0.5,
cacheWrite: 6.25, cacheWrite: 6.25,
}, },
contextWindow: 200000, contextWindow: 1000000,
maxTokens: 128000, maxTokens: 128000,
} satisfies Model<"anthropic-messages">, } satisfies Model<"anthropic-messages">,
"claude-sonnet-4": { "claude-sonnet-4": {

View File

@@ -1,8 +1,8 @@
/** /**
* Anthropic OAuth flow (Claude Pro/Max) * Anthropic OAuth flow (Claude Pro/Max)
* *
* NOTE: This module uses Node.js http.createServer and child_process for the OAuth callback * NOTE: This module uses Node.js http.createServer for the OAuth callback server.
* and token exchange. It is only intended for CLI use, not browser environments. * It is only intended for CLI use, not browser environments.
*/ */
import type { Server } from "node:http"; import type { Server } from "node:http";
@@ -18,7 +18,6 @@ type CallbackServerInfo = {
type NodeApis = { type NodeApis = {
createServer: typeof import("node:http").createServer; createServer: typeof import("node:http").createServer;
execFile: typeof import("node:child_process").execFile;
}; };
let nodeApis: NodeApis | null = null; let nodeApis: NodeApis | null = null;
@@ -53,12 +52,9 @@ async function getNodeApis(): Promise<NodeApis> {
if (typeof process === "undefined" || (!process.versions?.node && !process.versions?.bun)) { if (typeof process === "undefined" || (!process.versions?.node && !process.versions?.bun)) {
throw new Error("Anthropic OAuth is only available in Node.js environments"); throw new Error("Anthropic OAuth is only available in Node.js environments");
} }
nodeApisPromise = Promise.all([import("node:http"), import("node:child_process")]).then( nodeApisPromise = import("node:http").then((httpModule) => ({
([httpModule, childProcessModule]) => ({ createServer: httpModule.createServer,
createServer: httpModule.createServer, }));
execFile: childProcessModule.execFile,
}),
);
} }
nodeApis = await nodeApisPromise; nodeApis = await nodeApisPromise;
return nodeApis; return nodeApis;
@@ -183,59 +179,21 @@ async function startCallbackServer(expectedState: string): Promise<CallbackServe
}); });
} }
async function postJsonWithCurl(url: string, body: Record<string, string | number>): Promise<string> { async function postJson(url: string, body: Record<string, string | number>): Promise<string> {
const { execFile } = await getNodeApis(); const response = await fetch(url, {
const payload = JSON.stringify(body); method: "POST",
const marker = "__PI_CURL_HTTP_STATUS__:"; headers: {
"Content-Type": "application/json",
const result = await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { Accept: "application/json",
execFile( },
"curl", body: JSON.stringify(body),
[ signal: AbortSignal.timeout(30_000),
"-sS",
"--connect-timeout",
"15",
"--max-time",
"30",
"-X",
"POST",
url,
"-H",
"Content-Type: application/json",
"-H",
"Accept: application/json",
"--data-binary",
payload,
"-w",
`\n${marker}%{http_code}`,
],
(error, stdout, stderr) => {
if (error) {
reject(
new Error(`curl request failed. url=${url}; details=${formatErrorDetails(error)}; stderr=${stderr}`),
);
return;
}
resolve({ stdout, stderr });
},
);
}); });
const markerIndex = result.stdout.lastIndexOf(`\n${marker}`); const responseBody = await response.text();
if (markerIndex === -1) {
throw new Error(
`curl response missing status marker. url=${url}; stdout=${result.stdout}; stderr=${result.stderr}`,
);
}
const responseBody = result.stdout.slice(0, markerIndex); if (!response.ok) {
const statusText = result.stdout.slice(markerIndex + `\n${marker}`.length).trim(); throw new Error(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`);
const status = Number.parseInt(statusText, 10);
if (!Number.isFinite(status)) {
throw new Error(`curl returned invalid status. url=${url}; status=${statusText}; body=${responseBody}`);
}
if (status < 200 || status >= 300) {
throw new Error(`HTTP request failed. status=${status}; url=${url}; body=${responseBody}`);
} }
return responseBody; return responseBody;
@@ -249,7 +207,7 @@ async function exchangeAuthorizationCode(
): Promise<OAuthCredentials> { ): Promise<OAuthCredentials> {
let responseBody: string; let responseBody: string;
try { try {
responseBody = await postJsonWithCurl(TOKEN_URL, { responseBody = await postJson(TOKEN_URL, {
grant_type: "authorization_code", grant_type: "authorization_code",
client_id: CLIENT_ID, client_id: CLIENT_ID,
code, code,
@@ -406,7 +364,7 @@ export async function loginAnthropic(options: {
export async function refreshAnthropicToken(refreshToken: string): Promise<OAuthCredentials> { export async function refreshAnthropicToken(refreshToken: string): Promise<OAuthCredentials> {
let responseBody: string; let responseBody: string;
try { try {
responseBody = await postJsonWithCurl(TOKEN_URL, { responseBody = await postJson(TOKEN_URL, {
grant_type: "refresh_token", grant_type: "refresh_token",
client_id: CLIENT_ID, client_id: CLIENT_ID,
refresh_token: refreshToken, refresh_token: refreshToken,