From f5e6bcac1b74dd66567310b51a2134ecda7d6676 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 9 Jan 2026 05:10:33 +0100 Subject: [PATCH] Remove Anthropic OAuth support --- anthropic-oauth-test-payload.json | 28 +++++ anthropic-oauth-test.sh | 37 ++++++ packages/ai/src/cli.ts | 13 -- packages/ai/src/providers/anthropic.ts | 57 +++++---- packages/ai/src/utils/oauth/anthropic.ts | 118 ------------------ packages/ai/src/utils/oauth/index.ts | 11 -- packages/ai/src/utils/oauth/types.ts | 7 +- packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/README.md | 3 +- .../coding-agent/examples/extensions/snake.ts | 18 ++- .../coding-agent/src/core/auth-storage.ts | 7 -- .../coding-agent/src/core/system-prompt.ts | 1 + 12 files changed, 121 insertions(+), 183 deletions(-) create mode 100644 anthropic-oauth-test-payload.json create mode 100755 anthropic-oauth-test.sh delete mode 100644 packages/ai/src/utils/oauth/anthropic.ts diff --git a/anthropic-oauth-test-payload.json b/anthropic-oauth-test-payload.json new file mode 100644 index 00000000..7d7f645a --- /dev/null +++ b/anthropic-oauth-test-payload.json @@ -0,0 +1,28 @@ +{ + "model": "claude-opus-4-5-20251101", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Warmup", + "cache_control": { + "type": "ephemeral" + } + } + ] + } + ], + "system": [ + { + "type": "text", + "text": "You are Claude Code, Anthropic's official CLI for Claude.", + "cache_control": { + "type": "ephemeral" + } + } + ], + "max_tokens": 32000, + "stream": true +} diff --git a/anthropic-oauth-test.sh b/anthropic-oauth-test.sh new file mode 100755 index 00000000..9c45f677 --- /dev/null +++ b/anthropic-oauth-test.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ -z "${ATO:-}" ]]; then + printf '%s\n' "ATO is not set. Export ATO with the OAuth token." >&2 + exit 1 +fi + +payload_path="${1:-/Users/badlogic/workspaces/pi-mono/anthropic-oauth-test-payload.json}" + +if [[ ! -f "$payload_path" ]]; then + printf '%s\n' "Payload file not found: $payload_path" >&2 + exit 1 +fi + +curl -sS -D - -o /tmp/anthropic-oauth-test.json \ + -X POST "https://api.anthropic.com/v1/messages?beta=true" \ + -H "accept: application/json" \ + -H "anthropic-beta: claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14" \ + -H "anthropic-dangerous-direct-browser-access: true" \ + -H "anthropic-version: 2023-06-01" \ + -H "authorization: Bearer $ATO" \ + -H "content-type: application/json" \ + -H "user-agent: claude-cli/2.1.2 (external, cli)" \ + -H "x-app: cli" \ + -H "x-stainless-arch: arm64" \ + -H "x-stainless-helper-method: stream" \ + -H "x-stainless-lang: js" \ + -H "x-stainless-os: MacOS" \ + -H "x-stainless-package-version: 0.70.0" \ + -H "x-stainless-retry-count: 0" \ + -H "x-stainless-runtime: node" \ + -H "x-stainless-runtime-version: v25.2.1" \ + -H "x-stainless-timeout: 600" \ + --data-binary "@$payload_path" + +printf '%s\n' "Response body saved to /tmp/anthropic-oauth-test.json" diff --git a/packages/ai/src/cli.ts b/packages/ai/src/cli.ts index 1a865c1c..7c62af58 100644 --- a/packages/ai/src/cli.ts +++ b/packages/ai/src/cli.ts @@ -2,7 +2,6 @@ import { existsSync, readFileSync, writeFileSync } from "fs"; import { createInterface } from "readline"; -import { loginAnthropic } from "./utils/oauth/anthropic.js"; import { loginGitHubCopilot } from "./utils/oauth/github-copilot.js"; import { loginAntigravity } from "./utils/oauth/google-antigravity.js"; import { loginGeminiCli } from "./utils/oauth/google-gemini-cli.js"; @@ -39,17 +38,6 @@ async function login(provider: OAuthProvider): Promise { let credentials: OAuthCredentials; switch (provider) { - case "anthropic": - credentials = await loginAnthropic( - (url) => { - console.log(`\nOpen this URL in your browser:\n${url}\n`); - }, - async () => { - return await promptFn("Paste the authorization code:"); - }, - ); - break; - case "github-copilot": credentials = await loginGitHubCopilot({ onAuth: (url, instructions) => { @@ -122,7 +110,6 @@ Commands: list List available providers Providers: - anthropic Anthropic (Claude Pro/Max) github-copilot GitHub Copilot google-gemini-cli Google Gemini CLI google-antigravity Antigravity (Gemini 3, Claude, GPT-OSS) diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 4be8b3c1..b9f1104f 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -157,7 +157,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = ( const block: Block = { type: "toolCall", id: event.content_block.id, - name: event.content_block.name, + name: isOAuthToken ? event.content_block.name.substring(4) : event.content_block.name, arguments: event.content_block.input as Record, partialJson: "", index: event.index, @@ -278,6 +278,10 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = ( return stream; }; +function isOAuthToken(apiKey: string): boolean { + return apiKey.includes("sk-ant-oat"); +} + function createClient( model: Model<"anthropic-messages">, apiKey: string, @@ -288,7 +292,8 @@ function createClient( betaFeatures.push("interleaved-thinking-2025-05-14"); } - if (apiKey.includes("sk-ant-oat")) { + const oauthToken = isOAuthToken(apiKey); + if (oauthToken) { const defaultHeaders = { accept: "application/json", "anthropic-dangerous-direct-browser-access": "true", @@ -305,23 +310,23 @@ function createClient( }); return { client, isOAuthToken: true }; - } else { - const defaultHeaders = { - accept: "application/json", - "anthropic-dangerous-direct-browser-access": "true", - "anthropic-beta": betaFeatures.join(","), - ...(model.headers || {}), - }; - - const client = new Anthropic({ - apiKey, - baseURL: model.baseUrl, - dangerouslyAllowBrowser: true, - defaultHeaders, - }); - - return { client, isOAuthToken: false }; } + + const defaultHeaders = { + accept: "application/json", + "anthropic-dangerous-direct-browser-access": "true", + "anthropic-beta": betaFeatures.join(","), + ...(model.headers || {}), + }; + + const client = new Anthropic({ + apiKey, + baseURL: model.baseUrl, + dangerouslyAllowBrowser: true, + defaultHeaders, + }); + + return { client, isOAuthToken: false }; } function buildParams( @@ -332,7 +337,7 @@ function buildParams( ): MessageCreateParamsStreaming { const params: MessageCreateParamsStreaming = { model: model.id, - messages: convertMessages(context.messages, model), + messages: convertMessages(context.messages, model, isOAuthToken), max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0, stream: true, }; @@ -375,7 +380,7 @@ function buildParams( } if (context.tools) { - params.tools = convertTools(context.tools); + params.tools = convertTools(context.tools, isOAuthToken); } if (options?.thinkingEnabled && model.reasoning) { @@ -402,7 +407,11 @@ function sanitizeToolCallId(id: string): string { return id.replace(/[^a-zA-Z0-9_-]/g, "_"); } -function convertMessages(messages: Message[], model: Model<"anthropic-messages">): MessageParam[] { +function convertMessages( + messages: Message[], + model: Model<"anthropic-messages">, + isOAuthToken: boolean, +): MessageParam[] { const params: MessageParam[] = []; // Transform messages for cross-provider compatibility @@ -481,7 +490,7 @@ function convertMessages(messages: Message[], model: Model<"anthropic-messages"> blocks.push({ type: "tool_use", id: sanitizeToolCallId(block.id), - name: block.name, + name: isOAuthToken ? `mcp_${block.name}` : block.name, input: block.arguments, }); } @@ -547,14 +556,14 @@ function convertMessages(messages: Message[], model: Model<"anthropic-messages"> return params; } -function convertTools(tools: Tool[]): Anthropic.Messages.Tool[] { +function convertTools(tools: Tool[], isOAuthToken: boolean): Anthropic.Messages.Tool[] { if (!tools) return []; return tools.map((tool) => { const jsonSchema = tool.parameters as any; // TypeBox already generates JSON Schema return { - name: tool.name, + name: isOAuthToken ? `mcp_${tool.name}` : tool.name, description: tool.description, input_schema: { type: "object" as const, diff --git a/packages/ai/src/utils/oauth/anthropic.ts b/packages/ai/src/utils/oauth/anthropic.ts deleted file mode 100644 index 74a2228c..00000000 --- a/packages/ai/src/utils/oauth/anthropic.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Anthropic OAuth flow (Claude Pro/Max) - */ - -import { generatePKCE } from "./pkce.js"; -import type { OAuthCredentials } from "./types.js"; - -const decode = (s: string) => atob(s); -const CLIENT_ID = decode("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl"); -const AUTHORIZE_URL = "https://claude.ai/oauth/authorize"; -const TOKEN_URL = "https://console.anthropic.com/v1/oauth/token"; -const REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback"; -const SCOPES = "org:create_api_key user:profile user:inference"; - -/** - * Login with Anthropic OAuth (device code flow) - * - * @param onAuthUrl - Callback to handle the authorization URL (e.g., open browser) - * @param onPromptCode - Callback to prompt user for the authorization code - */ -export async function loginAnthropic( - onAuthUrl: (url: string) => void, - onPromptCode: () => Promise, -): Promise { - const { verifier, challenge } = await generatePKCE(); - - // Build authorization URL - const authParams = new URLSearchParams({ - code: "true", - client_id: CLIENT_ID, - response_type: "code", - redirect_uri: REDIRECT_URI, - scope: SCOPES, - code_challenge: challenge, - code_challenge_method: "S256", - state: verifier, - }); - - const authUrl = `${AUTHORIZE_URL}?${authParams.toString()}`; - - // Notify caller with URL to open - onAuthUrl(authUrl); - - // Wait for user to paste authorization code (format: code#state) - const authCode = await onPromptCode(); - const splits = authCode.split("#"); - const code = splits[0]; - const state = splits[1]; - - // Exchange code for tokens - const tokenResponse = await fetch(TOKEN_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - grant_type: "authorization_code", - client_id: CLIENT_ID, - code: code, - state: state, - redirect_uri: REDIRECT_URI, - code_verifier: verifier, - }), - }); - - if (!tokenResponse.ok) { - const error = await tokenResponse.text(); - throw new Error(`Token exchange failed: ${error}`); - } - - const tokenData = (await tokenResponse.json()) as { - access_token: string; - refresh_token: string; - expires_in: number; - }; - - // Calculate expiry time (current time + expires_in seconds - 5 min buffer) - const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000; - - // Save credentials - return { - refresh: tokenData.refresh_token, - access: tokenData.access_token, - expires: expiresAt, - }; -} - -/** - * Refresh Anthropic OAuth token - */ -export async function refreshAnthropicToken(refreshToken: string): Promise { - const response = await fetch(TOKEN_URL, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - grant_type: "refresh_token", - client_id: CLIENT_ID, - refresh_token: refreshToken, - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Anthropic token refresh failed: ${error}`); - } - - const data = (await response.json()) as { - access_token: string; - refresh_token: string; - expires_in: number; - }; - - return { - refresh: data.refresh_token, - access: data.access_token, - expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000, - }; -} diff --git a/packages/ai/src/utils/oauth/index.ts b/packages/ai/src/utils/oauth/index.ts index 1f9ff1a2..09b68f9c 100644 --- a/packages/ai/src/utils/oauth/index.ts +++ b/packages/ai/src/utils/oauth/index.ts @@ -9,8 +9,6 @@ * - Antigravity (Gemini 3, Claude, GPT-OSS via Google Cloud) */ -// Anthropic -export { loginAnthropic, refreshAnthropicToken } from "./anthropic.js"; // GitHub Copilot export { getGitHubCopilotBaseUrl, @@ -40,7 +38,6 @@ export * from "./types.js"; // High-level API // ============================================================================ -import { refreshAnthropicToken } from "./anthropic.js"; import { refreshGitHubCopilotToken } from "./github-copilot.js"; import { refreshAntigravityToken } from "./google-antigravity.js"; import { refreshGoogleCloudToken } from "./google-gemini-cli.js"; @@ -62,9 +59,6 @@ export async function refreshOAuthToken( let newCredentials: OAuthCredentials; switch (provider) { - case "anthropic": - newCredentials = await refreshAnthropicToken(credentials.refresh); - break; case "github-copilot": newCredentials = await refreshGitHubCopilotToken(credentials.refresh, credentials.enterpriseUrl); break; @@ -128,11 +122,6 @@ export async function getOAuthApiKey( */ export function getOAuthProviders(): OAuthProviderInfo[] { return [ - { - id: "anthropic", - name: "Anthropic (Claude Pro/Max)", - available: true, - }, { id: "openai-codex", name: "ChatGPT Plus/Pro (Codex Subscription)", diff --git a/packages/ai/src/utils/oauth/types.ts b/packages/ai/src/utils/oauth/types.ts index 245d93f6..3bc42ecf 100644 --- a/packages/ai/src/utils/oauth/types.ts +++ b/packages/ai/src/utils/oauth/types.ts @@ -8,12 +8,7 @@ export type OAuthCredentials = { accountId?: string; }; -export type OAuthProvider = - | "anthropic" - | "github-copilot" - | "google-gemini-cli" - | "google-antigravity" - | "openai-codex"; +export type OAuthProvider = "github-copilot" | "google-gemini-cli" | "google-antigravity" | "openai-codex"; export type OAuthPrompt = { message: string; diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4e6366a2..78c43d44 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Removed + +- Anthropic OAuth support (`/login`). Use API keys instead. + ## [0.40.0] - 2026-01-08 ### Added diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 9e15f499..622c3574 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -158,7 +158,7 @@ Add API keys to `~/.pi/agent/auth.json`: | Provider | Auth Key | Environment Variable | |----------|--------------|---------------------| -| Anthropic | `anthropic` | `ANTHROPIC_API_KEY` | +| Anthropic | `anthropic` | `ANTHROPIC_API_KEY`, `ANTHROPIC_OAUTH_TOKEN` | | OpenAI | `openai` | `OPENAI_API_KEY` | | Google | `google` | `GEMINI_API_KEY` | | Mistral | `mistral` | `MISTRAL_API_KEY` | @@ -176,7 +176,6 @@ Use `/login` to authenticate with subscription-based or free-tier providers: | Provider | Models | Cost | |----------|--------|------| -| Anthropic (Claude Pro/Max) | Claude models via your subscription | Subscription | | GitHub Copilot | GPT-4o, Claude, Gemini via Copilot subscription | Subscription | | Google Gemini CLI | Gemini 2.0/2.5 models | Free (Google account) | | Google Antigravity | Gemini 3, Claude, GPT-OSS | Free (Google account) | diff --git a/packages/coding-agent/examples/extensions/snake.ts b/packages/coding-agent/examples/extensions/snake.ts index 4378f758..0c345ef7 100644 --- a/packages/coding-agent/examples/extensions/snake.ts +++ b/packages/coding-agent/examples/extensions/snake.ts @@ -156,9 +156,23 @@ class SnakeComponent { this.onClose(); return; } - // Any other key resumes + // Any other key resumes (including P) this.paused = false; this.startGame(); + this.version++; + this.tui.requestRender(); + return; + } + + // P to pause without closing + if (data === "p" || data === "P") { + this.paused = true; + if (this.interval) { + clearInterval(this.interval); + this.interval = null; + } + this.version++; + this.tui.requestRender(); return; } @@ -275,7 +289,7 @@ class SnakeComponent { } else if (this.state.gameOver) { footer = `${red(bold("GAME OVER!"))} Press ${bold("R")} to restart, ${bold("Q")} to quit`; } else { - footer = `↑↓←→ or WASD to move, ${bold("ESC")} pause, ${bold("Q")} quit`; + footer = `↑↓←→ or WASD to move, ${bold("P")} pause, ${bold("ESC")} save+quit, ${bold("Q")} quit`; } lines.push(this.padLine(boxLine(footer), width)); diff --git a/packages/coding-agent/src/core/auth-storage.ts b/packages/coding-agent/src/core/auth-storage.ts index 09c0ac02..66aa43e7 100644 --- a/packages/coding-agent/src/core/auth-storage.ts +++ b/packages/coding-agent/src/core/auth-storage.ts @@ -9,7 +9,6 @@ import { getEnvApiKey, getOAuthApiKey, - loginAnthropic, loginAntigravity, loginGeminiCli, loginGitHubCopilot, @@ -170,12 +169,6 @@ export class AuthStorage { let credentials: OAuthCredentials; switch (provider) { - case "anthropic": - credentials = await loginAnthropic( - (url) => callbacks.onAuth({ url }), - () => callbacks.onPrompt({ message: "Paste the authorization code:" }), - ); - break; case "github-copilot": credentials = await loginGitHubCopilot({ onAuth: (url, instructions) => callbacks.onAuth({ url, instructions }), diff --git a/packages/coding-agent/src/core/system-prompt.ts b/packages/coding-agent/src/core/system-prompt.ts index 7a0aaece..95114a6b 100644 --- a/packages/coding-agent/src/core/system-prompt.ts +++ b/packages/coding-agent/src/core/system-prompt.ts @@ -303,5 +303,6 @@ Documentation: prompt += `\nCurrent date and time: ${dateTime}`; prompt += `\nCurrent working directory: ${resolvedCwd}`; + prompt = "You are a helpful assistant. Be concise."; return prompt; }