GeminiのJS SDKを使ってJSON outputでスキーマを指定させるときに型がおかしく特定のスキーマを書くことができない事象の調査
どんな事象が起きる?
GeminiにはJSONで出力させるときにスキーマを指定する機能がある。このスキーマはOpenAPI3.0互換のものが書けるはずだが、TypeScript用の型定義に従うと以下のような型定義がエラーになる。
const generationConfig: GenerationConfig = {
temperature: 1,
topP: 0.95,
topK: 64,
maxOutputTokens: 16384,
responseMimeType: "application/json",
responseSchema: {
type: FunctionDeclarationSchemaType.OBJECT,
properties: {
chapters: {
type: FunctionDeclarationSchemaType.ARRAY,
items: {
// オブジェクト リテラルは既知のプロパティのみ指定できます。'items' は
// 型 'FunctionDeclarationSchema' に存在しません。ts(2353)
type: FunctionDeclarationSchemaType.OBJECT,
properties: {
title: {
type: FunctionDeclarationSchemaType.STRING,
},
},
},
},
},
required: ["chapters"],
},
};
これは以下のスキーマ定義であり
{
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}
}
},
"required": ["list"]
}
以下のような応答を期待している。
{
"list": [
{ "name": "Tokyo" },
{ "name": "New York" },
{ "name": "London" },
{ "name": "Paris" },
{ "name": "Sydney" }
]
}
よくあるレスポンスだと思うが、OBJECT
の下になぜかARRAY
を書くことができない。
型定義はどうなっているのか?
まずresponseSchemaの型定義は以下のようになっている。
export interface ResponseSchema extends Schema {}
export interface Schema {
type?: FunctionDeclarationSchemaType;
format?: string;
description?: string;
nullable?: boolean;
items?: FunctionDeclarationSchema;
enum?: string[];
properties?: { [k: string]: FunctionDeclarationSchema };
required?: string[];
example?: unknown;
}
properties
にはFunctionDeclarationSchema
を書くことになっており
export interface FunctionDeclarationSchema {
type: FunctionDeclarationSchemaType;
properties: { [k: string]: FunctionDeclarationSchemaProperty };
description?: string;
required?: string[];
}
ここにはitems
がないためARRAY
を書くことができない定義となっている。
スキーマが設定できる物であるのかを確かめる
スキーマはOpenAPI互換と書かれているが、実際には設定できない何かしらの制約が存在し、型定義で絞っている可能性がある。直接APIのリクエストを投げてみて通るかどうか見てみる。
curlしてもいいが、認証通すのが面倒なのでGoogle AI Studioを使って投げてみたが、ちゃんと投げることができて想定通りの応答が返ってくることが確認できた。
他の言語ではどう書く?
試しにgoで書いてみた。
model.GenerationConfig = genai.GenerationConfig{
ResponseMIMEType: "application/json",
ResponseSchema: &genai.Schema{
Type: genai.TypeObject,
Properties: map[string]*genai.Schema{
"list": {
Type: genai.TypeArray,
Items: &genai.Schema{
Type: genai.TypeObject,
Properties: map[string]*genai.Schema{
"name": {
Type: genai.TypeString,
},
},
},
},
},
},
}
書けた。ならTypeScriptでも書けていいだろう。
コードの由来を確かめる
SDKによくあることとして、多数の言語用クライアントを生成しなければならないため、一つのAPI定義からクライアントを自動生成していることがる。なのでSDKのコードを書き換えるPRを出してみ意味がなかったりするのでどこで作られた物か調べる必要がある。
機能が追加されたのはAdd responseSchema
to generationConfig
by lahirumaramba · Pull Request #158 · google-gemini/generative-ai-js。特に自動生成された形跡はなさそう?
元々Function Calling用のスキーマ定義を流用して書き換えた際にミスったような気配がする。
上記PRの前の実装を見るとFunction CallingのときにRootにArrayを指定できないように見える。
なんか実装が適当じゃないか?
直す場所は分かったが、どう直すかが問題である。コードを読むと他にも書けないようなパターンが結構ある。なんでこうなるかというと何が書けるかという仕様が明言されていないためだと思われる。
公式ドキュメントにはJSON schemaとだけ書かれており、SDKのドキュメントには
a select subset of an OpenAPI 3.0 schema object.
とだけある。
Go SDKのSchema定義を見るとJSにはないプロパティがたくさん定義されており中の人も適当に書いてるのではないか疑惑がある。
内部の仕組みを考えるとふわっとなんでも受付そうな感じではある。
Python での型定義を見て仕様の参考にしてみる
Protocol Buffersで定義されている
ここを見ていて思ったが、API定義はどこかにあるんじゃないかと思ったので探してみた。
あった。
この定義ならObjectの下にArrayも書ける。
gRPCの定義はこっち