genaiscriptでパケットキャプチャを実装する
genaiscriptには強力なログ機能が備わっていますが、API呼び出しでエラーが発生した場合、ネットワークレベルの情報を確認するためにパケットキャプチャを行う必要があります。この目的のためにソースコードをカスタマイズして、プロキシ経由の通信を可能にします。
genaiscriptの実行パスの理解
genaiscriptはnpx経由で実行されますが、モジュールの優先順位は以下の通りです:
- ローカルインストール:
npm install genaiscript→/your/local/project/node_modules/genaiscript/built/genaiscript.cjs - グローバルインストール:
npm install -g genaiscript→/Users/username/.nvm/versions/node/v20.15.1/lib/node_modules/genaiscript/built/genaiscript.cjs - npxキャッシュ:
npx --yes genaiscript@1.75.3 run demo→/Users/username/.npm/_npx/86ce7b375aee60ee/node_modules/genaiscript/built/genaiscript.cjs
バージョン番号の一致が重要です。異なるバージョンが指定された場合は、自動で対応バージョンがキャッシュディレクトリにダウンロードされます。
OpenAIリクエストにプロキシを追加
// genaiscript.cjs
// ../core/src/openai.ts
function getConfigHeaders(cfg) {
let { token, type, base } = cfg;
// ... existing code ...
const fetchHeaders = {
...getConfigHeaders(cfg),
"Content-Type": "application/json",
...headers || {}
};
traceFetchPost(trace, url, fetchHeaders, postReq);
const body = JSON.stringify(postReq);
let r2;
+ // パケットキャプチャ用プロキシ設定(OpenAI APIリクエストのみ対象)
+ const { HttpsProxyAgent } = require('https-proxy-agent');
+ const proxy = 'http://192.168.8.110:9090';
+ const agent = new HttpsProxyAgent(proxy);
try {
r2 = await fetchRetry(url, {
+ agent: agent,
headers: fetchHeaders,
body,
method: "POST",
signal: toSignal(cancellationToken),
...rest || {}
});
qwen72bのtool_callインデックス不整合対応
一部のプロバイダ(例:siliconflow, aliyun)では、qwen72bのtool_callインデックスが正しく返されないことがあります。この問題に対応するため、IDベースでユニークなインデックスを割り当てる方法を実装します。
// genaiscript.cjs
// ../core/src/openai.ts
let toolCall_ids = new Set();
const doChoices = (json, tokens) => {
const obj = JSON.parse(json);
if (!postReq.stream) trace.detailsFenced(`response`, obj, "json");
if (obj.usage) usage = obj.usage;
if (!responseModel && obj.model) responseModel = obj.model;
if (!obj.choices?.length) return;
else if (obj.choices?.length != 1)
throw new Error("too many choices in response");
const choice = obj.choices[0];
const { finish_reason } = choice;
if (finish_reason) finishReason = finish_reason;
if (choice.delta) {
const { delta, logprobs } = choice;
if (logprobs?.content) lbs.push(...logprobs.content);
if (typeof delta?.content === "string" && delta?.content !== "") {
numTokens += estimateTokens(delta.content, encoder);
chatResp += delta.content;
tokens.push(
...serializeChunkChoiceToLogProbs(choice)
);
trace.appendToken(delta.content);
} else if (Array.isArray(delta.tool_calls)) {
const { tool_calls } = delta;
for (const call of tool_calls) {
let id = call.id;
if (typeof id == 'string' && id.length > 0 && !toolCall_ids.has(id)) {
toolCall_ids.add(id);
}
let uniq_index = toolCall_ids.size - 1;
const tc = toolCalls[uniq_index] || (toolCalls[uniq_index] = {
id: call.id,
name: call.function.name,
arguments: ""
});
if (call.function.arguments)
tc.arguments += call.function.arguments;
}
}
}
SSL証明書検証の無効化
プロキシツール(例:Proxyman)による中間者証明書の検証エラーを回避するため、node-fetchでSSL証明書検証を無効化します。
// node_modules/node-fetch/lib/index.js
let agent = request.agent;
if (typeof agent === 'function') {
agent = agent(parsedURL);
}
// SSL証明書検証を無視
if (parsedURL.protocol === 'https:') {
if (agent) {
agent.options.rejectUnauthorized = false;
} else {
agent = new HttpsProxyAgent(parsedURL.href, { rejectUnauthorized: false });
}
return Object.assign({}, parsedURL, {
method: request.method,
headers: exportNodeCompatibleHeaders(headers),
agent
});
} else {
return Object.assign({}, parsedURL, {
method: request.method,
headers: exportNodeCompatibleHeaders(headers),
agent
});
}