Created
July 5, 2025 13:34
-
-
Save calloc134/1ea422b23c1bf934f2f1ae1616d66eba to your computer and use it in GitHub Desktop.
OpenAI Responses APIを使って検索とFunction Callingを同時に呼び出すメモ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // extended_index.ts | |
| import OpenAI from "openai"; | |
| import { encoding_for_model } from "@dqbd/tiktoken"; | |
| import readline from "readline"; | |
| import type { | |
| FunctionTool as FunctionTool_nonBeta, | |
| WebSearchTool, | |
| } from "openai/resources/responses/responses.mjs"; | |
| const webSearchPreviewTool: WebSearchTool = { | |
| type: "web_search_preview", | |
| search_context_size: "medium", | |
| }; | |
| const calculateDiscountTool_nonBeta: FunctionTool_nonBeta = { | |
| type: "function", | |
| name: "calculate_discount", | |
| description: | |
| "Calculate the discounted price from the original price and discount rate.", | |
| strict: true, | |
| parameters: { | |
| type: "object", | |
| properties: { | |
| originalPrice: { | |
| type: "number", | |
| description: "The original price before discount (in yen).", | |
| }, | |
| discountRate: { | |
| type: "number", | |
| description: "The discount rate (between 0.0 and 1.0).", | |
| }, | |
| }, | |
| required: ["originalPrice", "discountRate"], // どの引数が必須か | |
| additionalProperties: false, | |
| }, | |
| }; | |
| // --- カスタムツール定義 --- | |
| const tools_nonBeta = [webSearchPreviewTool, calculateDiscountTool_nonBeta]; | |
| const tools_beta = [webSearchPreviewTool, calculateDiscountTool_nonBeta]; | |
| const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); | |
| // o4-mini のトークンエンコーダ | |
| const encoder = encoding_for_model("o4-mini"); | |
| const PRICE_PER_TOKEN = 1.1 / 1_000_000; | |
| // カスタムツール本体の実装 | |
| function calculateDiscount(originalPrice: number, discountRate: number) { | |
| const discountedPrice = Math.round(originalPrice * (1 - discountRate)); | |
| console.log( | |
| `[デバッグ]: 関数 'calculate_discount' が呼び出されました。` + | |
| `元の価格: ¥${originalPrice}, 割引率: ${discountRate}, 割引後の価格: ¥${discountedPrice}` | |
| ); | |
| return { discountedPrice }; | |
| } | |
| async function processPrompt(prompt: string) { | |
| console.log(`\n> ${prompt}\n`); | |
| // 1. ストリーミングで初回呼び出し | |
| const stream = await client.responses.create({ | |
| model: "o4-mini", | |
| input: prompt, | |
| tools: tools_nonBeta, | |
| tool_choice: "auto", | |
| stream: true, | |
| }); | |
| let fullText = ""; | |
| // 関数コールを蓄積するためのマップ | |
| const toolCalls: Record<number, any> = {}; | |
| for await (const event of stream) { | |
| switch (event.type) { | |
| case "response.output_text.delta": | |
| process.stdout.write(event.delta); | |
| fullText += event.delta; | |
| break; | |
| case "response.output_text.done": | |
| process.stdout.write("\n"); | |
| break; | |
| case "response.output_item.added": | |
| if (event.item.type === "function_call") { | |
| // 新しい関数呼び出しが開始 | |
| toolCalls[event.output_index] = { ...event.item, arguments: "" }; | |
| } | |
| break; | |
| case "response.function_call_arguments.delta": | |
| // 引数文字列をチャンク単位で追加 | |
| toolCalls[event.output_index].arguments += event.delta; | |
| break; | |
| case "response.function_call_arguments.done": | |
| // 関数呼び出し完了時点でループを抜け、実行フェーズへ | |
| toolCalls[event.output_index].arguments = event.arguments; | |
| break; | |
| } | |
| // 関数呼び出し完了が検知できればループを抜ける | |
| if (event.type === "response.function_call_arguments.done") { | |
| break; | |
| } | |
| } | |
| // 2. ツール呼び出しがあった場合、実行して再度ストリーミングで応答取得 | |
| const callIndex = Object.keys(toolCalls)[0]; | |
| if (callIndex !== undefined) { | |
| const call = toolCalls[Number(callIndex)]; | |
| const args = JSON.parse(call.arguments); | |
| const result = calculateDiscount(args.originalPrice, args.discountRate); | |
| // ツール実行結果を含む新たなストリーミング呼び出し | |
| const finalStream = await client.responses.create({ | |
| model: "o4-mini", | |
| input: [ | |
| // ユーザーメッセージ | |
| { role: "user", content: prompt }, | |
| // モデルが呼び出した function_call の再利用(call_id を追加) | |
| { | |
| type: "function_call", | |
| name: call.name, | |
| arguments: call.arguments, | |
| call_id: call.call_id, | |
| }, | |
| // 実際に実行した結果を function_call_output として渡す | |
| { | |
| type: "function_call_output", | |
| call_id: call.call_id, | |
| output: JSON.stringify(result), | |
| }, | |
| ], | |
| tools: tools_beta, | |
| stream: true, | |
| }); | |
| for await (const event of finalStream) { | |
| if (event.type === "response.output_text.delta") { | |
| process.stdout.write(event.delta); | |
| fullText += event.delta; | |
| } else if (event.type === "response.output_text.done") { | |
| process.stdout.write("\n"); | |
| } | |
| } | |
| } | |
| // トークンと課金額の計算・表示 | |
| const promptTokens = encoder.encode(prompt).length; | |
| const completionTokens = encoder.encode(fullText).length; | |
| const totalTokens = promptTokens + completionTokens; | |
| const costUSD = totalTokens * PRICE_PER_TOKEN; | |
| console.log("\n=== トークン使用量 ==="); | |
| console.log(`プロンプトトークン: ${promptTokens}`); | |
| console.log(`生成トークン: ${completionTokens}`); | |
| console.log(`合計トークン: ${totalTokens}`); | |
| console.log("\n=== 課金額(推定) ==="); | |
| console.log(`モデル: o4-mini`); | |
| console.log(`単価: 1M トークンあたり $1.10`); | |
| console.log(`今回の料金: $${costUSD.toFixed(6)} USD\n`); | |
| } | |
| function main() { | |
| const rl = readline.createInterface({ | |
| input: process.stdin, | |
| output: process.stdout, | |
| prompt: "> ", | |
| }); | |
| console.log( | |
| "ChatGPT CLI(o4-mini)へようこそ。プロンプトを入力してください。(終了するには Ctrl+C)" | |
| ); | |
| rl.prompt(); | |
| rl.on("line", async (line) => { | |
| const prompt = line.trim(); | |
| if (prompt) { | |
| await processPrompt(prompt); | |
| } | |
| rl.prompt(); | |
| }).on("close", () => { | |
| console.log("\n終了します。お疲れさまでした!"); | |
| process.exit(0); | |
| }); | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment