Chatモデル作成時にModalitiesとPricingをハッシュへシリアライズ
find_or_create_model! が生成するチャットモデルの modalities と pricing を、永続化直前に ハッシュ に変換して保存するよう変更されました。これにより JSONB カラムの形がレジストリ同期と同一になり、価格計算等の downstream ロジックが期待通りに動作します。
背景
find_or_create_model! は LLM から取得した Model::Info をそのまま ActiveRecord の JSONB カラムに書き込んでいました。Model::Pricing オブジェクトは as_json がデフォルトでインスタンス変数(@data, @values)を出力するため、DB には "data" や "values" といった余計なキーが混入していました。この不整合により、例えば model.pricing.dig("text_tokens", "standard", "input_per_million") が nil になるなど、コスト計算が正しく行えないケースが報告されました(Issue #784)。
技術的な変更
コード変更概要
resolve_model_from_strings 内で属性を設定する箇所が、to_h 呼び出しを追加するだけにシンプルに変更されました。具体的な差分は以下の通りです。
変更前:
m.modalities = model_info.modalities || {}
m.pricing = model_info.pricing || {}
変更後:
m.modalities = model_info.modalities.to_h
m.pricing = model_info.pricing.to_h
この変更により、Modalities と Pricing のオブジェクトはそれぞれ内部実装に依存しない 純粋なハッシュ へ変換され、ActiveRecord がそれをそのまま JSON にシリアライズします。結果として DB に保存される JSON の構造は acts_as_model のレジストリ同期が生成する形と一致します。
テストでの検証
新たに追加されたテストケースは、ハッシュ変換後の永続化が正しく行われることを検証しています。model_info に意図的な modalities と pricing を設定し、chat_class.create! を実行した後に chat.model.reload で取得したレコードの modalities と pricing が期待通りのハッシュ構造になっていることを expect(...).to eq で確認します。このテストは変更前に失敗していたシナリオをカバーし、回帰防止にも寄与します。
設計判断
ハッシュ化のタイミング をモデル作成直前に持ってくる選択は、既存の公開 API を一切変更せずに問題を解決できる点で妥当性があります。Model::Info の modalities と pricing は本来 値オブジェクト として扱われ、外部からは .to_h が提供されているため、呼び出し側で変換するだけで十分です。代替案として新しい属性キー(例: modalities_hash)を導入する案も検討されましたが、キーの増加は既存コードの更新を必要とし、後方互換性リスクが高まります。今回の実装は最小限の差分で一貫性を確保し、他コンポーネント(from_llm_attributes など)との挙動差異を排除しています。
まとめ
find_or_create_model! が modalities と pricing をハッシュにシリアライズして永続化するよう改修したことで、JSONB カラムの構造がレジストリ同期と揃い、価格情報の取得や計算が期待通りに機能するようになりました。変更は公開 API に影響せず、テストによって正確性が保証されているため、既存ユーザーへのリスクは最小限です。