watsonxのAPIをNode.jsから呼び出してみる

# はじめに
前回の記事「[watsonxのAPIを呼び出せるようになるまで](https://qiita.com/katahiro/items/3258cd42226ed82268ac)」では、 `curl` コマンドを利用してwatsonxのAPIを呼び出すことを紹介しましたが、今回の記事では `Node.js` から呼び出す方法を紹介します。
本当はNode.js用のSDKが公開されていれば良いのですが、2023/8/3時点ではPythonしか無いので、Node使いとしてはSDK無しでの実装が必要です。

### 実行環境
- node: v18.17.0
- npm: 9.6.7
- os: windows 11

# コード紹介
とりあえずコード全文。
Node.jsのv18から `fetch API` が素で使えるようになったので、特にpackageにモジュールを追加していません。
ただ、v17以前の環境で動かす場合、 `node-fetch` のモジュールで置き換えても動作することは確認済です。
```index.js
(async() => {
  const WML_APIKEY = `前回の記事で作成したAPIキー`;
  const PROJECT_ID = 'watsonxのProject ID';
  const iamEndpointUrl = 'https://iam.cloud.ibm.com/identity/token';
  const iamRequestUrl = `${iamEndpointUrl}?grant_type=urn:ibm:params:oauth:grant-type:apikey&apikey=${WML_APIKEY}`
  const options = {
    method: 'POST',
    headers: { 
      "Content-Type": "application/x-www-form-urlencoded",
      "Accept": "application/json",
    },
  }
  try {
    const iamResponse = await fetch(iamRequestUrl, options);
    const iamJson = await iamResponse.json();
    if(iamJson.access_token){
      
      const accessToken = iamJson.access_token;
      const watsonxEndpointUrl = 'https://us-south.ml.cloud.ibm.com/ml/v1-beta/generation/text?version=2023-05-29';
      const watsonxBody = {
        model_id: "ibm/mpt-7b-instruct2",
        input: "入力:\\n日本の首都は？",
        parameters: {
          decoding_method: "greedy",
          max_new_tokens: 20,
          min_new_tokens: 0,
          stop_sequences: [],
          repetition_penalty: 1
        },
        project_id: PROJECT_ID
      }
      const watsonxOptions = {
        method: 'POST',
        headers: { 
          'Content-Type': 'application/json',
          'Accept': 'application/json',
          'Authorization': `Bearer ${accessToken}`
        },
        body: JSON.stringify(watsonxBody),
      }
      const watsonxResponse = await fetch(watsonxEndpointUrl, watsonxOptions);
      const watsonxJson = await watsonxResponse.json();
      console.log(watsonxJson);
    }
  } catch (error) {
    console.log(error);
  }
})();
```
実行結果は、以下のような内容です。
```console
C:\node-apps\watsonx-test> node index.js
{
  model_id: 'ibm/mpt-7b-instruct2',
  created_at: '2023-08-03T01:28:10.364Z',
  results: [
    {
      generated_text: '\\n東京です。\\n東京は日本の首都',
      generated_token_count: 20,
      input_token_count: 11,
      stop_reason: 'MAX_TOKENS'
    }
  ]
}
```
何か、、、くどい回答ですね。

# 詰まったところ
当初は何度やっても以下のようなエラーが返ってきて、APIに渡してるJSONの先頭文字に `o` なんて無いのにと悩んでいました。
```json
{
  errors: [
    {
      code: 'json_unmarshal_failed',
      message: "Failed to deserialize json: invalid character 'o' looking for beginning of value"
    }
  ],
  trace: '90726e661564ae478594ef92f64b5d88',
  status_code: 400
}
```
普段はSDKを用いてサービスのAPIを利用することに慣れているので、下記のようにAPIに渡したい内容を記述しがちです。
```javascript
const watsonxBody = {
  model_id: "ibm/mpt-7b-instruct2",
  input: "入力:\\n日本の首都は？",
  parameters: {
    decoding_method: "greedy",
    max_new_tokens: 20,
    min_new_tokens: 0,
    stop_sequences: [],
    repetition_penalty: 1
  },
  project_id: PROJECT_ID
}
```
でも、これってよく考えるとJSONじゃなくてObjectですよね。
なのでfetchに何も考えずに渡すと `[object object]` になってしまってるので、先頭文字のoは認識できないよってエラーになってたようです。気づくのに半日以上かけてしまいました。。。orz
なので、fetchにbodyを渡す際には `JSON.stringify()` を利用して文字列にすることで、エラーが解消しました。
```javascript
body: JSON.stringify(watsonxBody),
```

# 注意点
今回紹介したコードでは下記のように、都度IAMからアクセストークンを取得しています。
```javascript
const iamResponse = await fetch(iamRequestUrl, options);
const iamJson = await iamResponse.json();
```
しかし、アクセストークンはデフォルトでは1時間再利用可能となっているので、都度発行するのは本来正しくありません。

常駐アプリケーションに組み込む際は、アクセストークンを発行してから1時間以内に再利用する場合は、既存のアクセストークンを流用するようにコードを書くのが良いです。
やり方はいくつかあると思いますが、例えば、アクセストークンを取得する関数を別で定義して、アクセストークンを取得した時に、そのアクセストークンとそのプログラムが稼働するプラットフォームの時刻情報を記録して、再度その関数が呼び出された時にプラットフォームの時刻を取得して、記録された時刻と1時間以内の差異なら記録されたアクセストークンを取得する、1時間以上あるならば再発行といったコードを書くのも1つの手かと思います。

# さいごに
いかがでしたでしょうか？
Python使いの人も多いと思いますが、私のようにNodeばかり使ってる人でも、watsonxをプログラムから呼び出せることが証明できました。
ちょっとアプリから呼び出せるようにしてみようかな、とか、少しでもwatonsxの興味が広がれば幸いです。