지피지기 백전백퇴

Opencode는 어떻게 claude 구독 API를 쓸 수 있나?


Just show me the code

Claude 구독자는 그냥 표준 OAuth 인증을 사용한다. 다만 @ai-sdk/anthropic이 OAuth 인증을 잘 지원하지 않아서, 강제로 인증 정보를 덮어씌울 필요가 있다.

import { createAnthropic as createAnthropicSDK } from "@ai-sdk/anthropic";
import { type ProviderV2 } from "@ai-sdk/provider";
import { generatePKCE } from "@openauthjs/openauth/pkce";

// Some CLIENT_ID copied from opencode. There might be a reason this is hardcoded, not random generated...?
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";

async function authorize(mode: "max" | "console") {
        const pkce = await generatePKCE();

        const url = new URL(
                `https://${mode === "console" ? "console.anthropic.com" : "claude.ai"}/oauth/authorize`,
                import.meta.url,
        );
        url.searchParams.set("code", "true");
        url.searchParams.set("client_id", CLIENT_ID);
        url.searchParams.set("response_type", "code");
        url.searchParams.set(
                "redirect_uri",
                "https://console.anthropic.com/oauth/code/callback",
        );
        url.searchParams.set(
                "scope",
                "org:create_api_key user:profile user:inference",
        );
        url.searchParams.set("code_challenge", pkce.challenge);
        url.searchParams.set("code_challenge_method", "S256");
        url.searchParams.set("state", pkce.verifier);
        return {
                url: url.toString(),
                verifier: pkce.verifier,
        };
}

async function exchange(code: string, verifier: string): Promise<any> {
        const splits = code.split("#");
        const result = await fetch("https://console.anthropic.com/v1/oauth/token", {
                method: "POST",
                headers: {
                        "Content-Type": "application/json",
                },
                body: JSON.stringify({
                        code: splits[0],
                        state: splits[1],
                        grant_type: "authorization_code",
                        client_id: CLIENT_ID,
                        redirect_uri: "https://console.anthropic.com/oauth/code/callback",
                        code_verifier: verifier,
                }),
        });
        if (!result.ok) {
                throw new Error("failed to auth");
        }
        const json: any = await result.json();
        return {
                type: "oauth",
                refresh: json.refresh_token,
                access: json.access_token,
                expires: Date.now() + json.expires_in * 1000,
        };
}


export async function checkAuth(): Promise<void> {
        const { url, verifier } = await authorize("max");
        console.log(url);
        const code = await Bun.stdin.text();
        const creds = await exchange(code, verifier);

        // TODO: Persist creds somewhere safe

        console.log("Rerun app");
        process.exit(0);
}

/**
 * Note that this assumes auth to be pre-existed. Or it will throw Error.
 */
export async function createAnthropic(): Promise<ProviderV2> {
        const getAuth = async () => {
                // TODO: Return proper auth persisted before
                return {
                        type: "oauth",
                        refresh: "sk-ant-merongmerong",
                        access: "sk-ant-goodgood",
                        expires: 172612636123,
                }
        };

        const setAuth = async (v: any) => {
                // TODO: Update persisted auth with updated access key
        };

        const myFetch = async (input: string | URL | Request, init: RequestInit) => {
                const auth = await getAuth();
                if (auth.type !== "oauth") return fetch(input, init);
                if (!auth.access || auth.expires < Date.now()) {
                        const response = await fetch(
                                "https://console.anthropic.com/v1/oauth/token",
                                {
                                        method: "POST",
                                        headers: {
                                                "Content-Type": "application/json",
                                        },
                                        body: JSON.stringify({
                                                grant_type: "refresh_token",
                                                refresh_token: auth.refresh,
                                                client_id: CLIENT_ID,
                                        }),
                                },
                        );
                        if (!response.ok) return;
                        const json: any = await response.json();
                        setAuth({
                                type: "oauth",
                                refresh: json.refresh_token,
                                access: json.access_token,
                                expires: Date.now() + json.expires_in * 1000,
                        });
                        auth.access = json.access_token;
                }
                const headers = {
                        ...init.headers,
                        authorization: `Bearer ${auth.access}`,
                        "anthropic-beta":
                                "oauth-2025-04-20,claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
                };
                delete (headers as any)["x-api-key"];
                return fetch(input, {
                        ...init,
                        headers,
                });
        };

        return createAnthropicSDK({ apiKey: "", fetch: myFetch as any });
}

여기서 끝이 아니다

정확히 다음 문구를 system prompt로 집어넣어야만 Claude code로 인정받을 수 있다. 한글자라도 더하거나 빼는 경우 “너는 Claude code가 아니니 이 API를 사용할 수 없다…”는 차가운 거절 문구를 보게 될 것이다.

You are Claude Code, Anthropic's official CLI for Claude.

Opencode의 관련 설정 보기

결론

되는걸 확인한 후 내가 만들어 쓰는 나노 코딩 에이전트에 붙여서 쓰는데, 의외로 토큰 사용량이 적지 않은듯 몇시간 쓰지 않았는데 429 에러가 터지고 말았다…