Skip to content

Instantly share code, notes, and snippets.

@calloc134
Created July 5, 2025 13:34
Show Gist options
  • Select an option

  • Save calloc134/1ea422b23c1bf934f2f1ae1616d66eba to your computer and use it in GitHub Desktop.

Select an option

Save calloc134/1ea422b23c1bf934f2f1ae1616d66eba to your computer and use it in GitHub Desktop.
OpenAI Responses APIを使って検索とFunction Callingを同時に呼び出すメモ
// 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