anthropics / anthropic-sdk-typescript

Access to Anthropic's safety-first language model APIs
https://www.npmjs.com/package/@anthropic-ai/sdk
MIT License
732 stars 79 forks source link

vertex-sdk: Cloudflare workers edge runtime not supported #508

Open rxliuli opened 3 months ago

rxliuli commented 3 months ago

At first I tried to use google-auth-libray for verification and found that it contained nodejs dependencies, so I implemented the logic of obtaining the access token myself, but errors still occurred, as follows

image
// src/index.ts
import { Hono } from 'hono'
import { AnthropicVertex } from '@anthropic-ai/vertex-sdk'
import { authenticate } from './authenticate'

const app = new Hono<{
  Bindings: {
    VERTEX_ANTROPIC_GOOGLE_SA_CLIENT_EMAIL: string
    VERTEX_ANTROPIC_GOOGLE_SA_PRIVATE_KEY: string
    VERTEX_ANTROPIC_REGION: string
    VERTEX_ANTROPIC_PROJECTID: string
    VERTEX_ANTROPIC_MODEL: string
  }
}>().get('/', async (c) => {
  const token = await authenticate({
    clientEmail: c.env.VERTEX_ANTROPIC_GOOGLE_SA_CLIENT_EMAIL!,
    privateKey: c.env.VERTEX_ANTROPIC_GOOGLE_SA_PRIVATE_KEY!,
  })
  const client = new AnthropicVertex({
    region: c.env.VERTEX_ANTROPIC_REGION,
    projectId: c.env.VERTEX_ANTROPIC_PROJECTID,
    accessToken: token.access_token,
  })
  const resp = await client.messages.create({
    model: c.env.VERTEX_ANTROPIC_MODEL,
    messages: [{ role: 'user', content: 'Hello!' }],
    max_tokens: 100,
  })
  return c.json(resp)
})

export default app
// src/authenticate.ts
import { SignJWT, importPKCS8 } from 'jose'

type Token = {
  access_token: string
  expires_in: number
  token_type: string
}

type TokenWithExpiration = Token & {
  expires_at: number
}

let token: TokenWithExpiration | null = null

async function createToken(options: {
  clientEmail: string
  privateKey: string
}) {
  const rawPrivateKey = options.privateKey.replace(/\\n/g, '\n')
  const privateKey = await importPKCS8(rawPrivateKey, 'RS256')

  const payload = {
    iss: options.clientEmail,
    scope: 'https://www.googleapis.com/auth/cloud-platform',
    aud: 'https://www.googleapis.com/oauth2/v4/token',
    exp: Math.floor(Date.now() / 1000) + 60 * 60,
    iat: Math.floor(Date.now() / 1000),
  }
  const token = await new SignJWT(payload)
    .setProtectedHeader({ alg: 'RS256' })
    .setIssuedAt()
    .setIssuer(options.clientEmail)
    .setAudience('https://www.googleapis.com/oauth2/v4/token')
    .setExpirationTime('1h')
    .sign(privateKey)

  // Form data for the token request
  const form = {
    grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
    assertion: token,
  }

  // Make the token request
  const tokenResponse = await fetch(
    'https://www.googleapis.com/oauth2/v4/token',
    {
      method: 'POST',
      body: JSON.stringify(form),
      headers: { 'Content-Type': 'application/json' },
    },
  )

  const json = (await tokenResponse.json()) as Token

  return {
    ...json,
    expires_at: Math.floor(Date.now() / 1000) + json.expires_in,
  }
}

export async function authenticate(options: {
  clientEmail: string
  privateKey: string
}): Promise<Token> {
  if (token === null) {
    token = await createToken(options)
  } else if (token.expires_at < Math.floor(Date.now() / 1000)) {
    token = await createToken(options)
  }
  return token
}
RobertCraigie commented 2 months ago

Thanks for the report. Have you opened an issue / is there an existing issue with google-auth-library for supporting cloudflare workers? Ideally this would be fixed upstream.