Skip to content

feat: BTL Runtime integration — AI backbone + tool-calling agent - #27

Open
Timidan wants to merge 7 commits into
masterfrom
feat/btl
Open

feat: BTL Runtime integration — AI backbone + tool-calling agent#27
Timidan wants to merge 7 commits into
masterfrom
feat/btl

Conversation

@Timidan

Copy link
Copy Markdown
Owner

Integrates the BTL Runtime (an OpenAI-compatible gateway) as HexKit's AI backbone, and adds a tool-calling agent that simulates real transactions.

What's in it

  • Both LLM proxies repointed Gemini → BTL — Bearer key injected server-side, per-request cost headers forwarded to the browser.
  • Shared BTL client (src/lib/btl) + a non-streaming tool-calling loop (runBtlAgent) that runs the real REVM simulator on a deposit and narrates it.
  • Concierge on BTL: NL intent parsing + vault ranking, a live per-call cost chip, and a one-click OpenAI/DeepSeek provider toggle.
  • Four explainer weaves: revert explainer, decoded-calldata explainer, contract-upgrade auditor (verified-source diff → risk table), and a storage-slot annotator (structured JSON → hoverable chips).
  • Cost-transparency UI: AiCostChip, BtlRuntimePanel, a foldable BtlExplanation panel, and a "Powered by BTL" badge across surfaces.

The rules-based fallback and the LLM_MODE=off kill switch keep the app crash-proof. Build is green and the BTL client is unit-tested.

To deploy: set BTL_API_KEY (Preview scope); leave PROXY_SECRET unset. Existing LI.FI / Etherscan / EDB env covers the surfaces the AI runs on.

Route every AI feature through the BTL Runtime (an OpenAI-compatible
gateway) and add a tool-calling agent that simulates real transactions.
- Repoint both LLM proxies (api/llm-recommend.ts, vite dev plugin) from
Gemini to BTL: inject the Bearer key server-side and forward the
per-request cost headers to the browser.
- Shared BTL client (src/lib/btl): OpenAI-shape request/response +
cost-meta parsing, a non-streaming tool-calling loop (runBtlAgent),
and a freeform "explain" hook.
- Concierge (LI.FI Earn): intent parsing + vault ranking on BTL, a live
per-call cost chip, and a one-click OpenAI/DeepSeek provider toggle.
- Deposit preflight: a tool-calling agent runs the real REVM simulator
and narrates the deposit.
- Four explainer weaves: revert explainer, decoded-calldata explainer,
contract-upgrade auditor (verified-source diff to a risk table), and a
storage-slot annotator (structured JSON to hoverable chips).
- Cost-transparency UI: AiCostChip, BtlRuntimePanel, a foldable
BtlExplanation panel, and a "Powered by BTL" badge across surfaces.
The rules-based fallback and the LLM_MODE=off kill switch keep the app
crash-proof.
CopilotAI review requested due to automatic review settings July 6, 2026 05:02
@vercel

vercelBot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
web3-toolkitReadyReadyPreview, CommentJul 6, 2026 2:13pm

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR switches HexKit’s AI backbone from Gemini to the BTL Runtime (OpenAI-compatible gateway), adds a shared BTL client + tool-calling agent loop, and surfaces cost/route transparency + “AI explain” panels across several UX surfaces (concierge, simulation revert, call decode, storage slots, and contract upgrade diff).

Changes:

  • Repoint /api/llm-recommend (Vercel + Vite dev proxy) to BTL chat completions, forwarding per-call cost/routing headers to the browser.
  • Introduce shared BTL utilities (src/lib/btl/*) including a generic “explain” hook and a tool-calling agent loop used to preflight deposits via real REVM simulation.
  • Add UI components for explanations and cost transparency (BtlExplanation, AiCostChip, BtlRuntimePanel, BtlBadge) and integrate them into multiple features.

Reviewed changes

Copilot reviewed 29 out of 30 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
vite.config.tsDev-server proxy now forwards BTL chat completions + cost headers; expands watch ignores.
src/lib/btl/useBtlExplain.tsGeneric “explain” hook wrapping BTL calls + meta.
src/lib/btl/models.tsBTL model registry + defaults and AB toggle list.
src/lib/btl/client.tsShared request builder, text extraction, meta parsing, JSON parsing helpers.
src/lib/btl/agent.tsTool-calling loop (runBtlAgent) for non-streaming chat/tool execution.
src/components/smart-decoder/SmartDecoder.tsxAdds “What does this call do?” AI explanation panel for decoded calldata.
src/components/simulation-results/SummaryTab.tsxAdds “Explain this revert” AI explanation panel for reverted simulations.
src/components/integrations/lifi-earn/earnApi.tspostLlmRecommend now returns { data, meta } and parses BTL headers.
src/components/integrations/lifi-earn/DepositFlow.tsxAdds AI preflight that calls REVM simulation via tool-calling agent; extracts tx builder.
src/components/integrations/lifi-earn/concierge/VaultRecommendations.tsxShows per-recommendation cost chip for AI-sourced recommendations.
src/components/integrations/lifi-earn/concierge/types.tsAdds optional meta to recommendations for cost/routing.
src/components/integrations/lifi-earn/concierge/LlmErrorAlert.tsxUpdates error copy from Gemini to BTL.
src/components/integrations/lifi-earn/concierge/intent/IntentPanel.tsxAdds per-parse cost chip + model A/B toggle + cache key fingerprinting.
src/components/integrations/lifi-earn/concierge/intent/hooks/useIntentRecommendation.tsMigrates intent recommendation request/parse to OpenAI-style responses + stores meta.
src/components/integrations/lifi-earn/concierge/intent/hooks/useIntentParser.tsMigrates NL intent parsing to OpenAI-style responses + returns meta.
src/components/integrations/lifi-earn/concierge/IdleSweepPanel.tsxAdds session-level BTL receipt panel and “Powered by BTL” badge gating.
src/components/integrations/lifi-earn/concierge/hooks/useVaultRecommendations.tsMigrates idle-sweep recommendations to OpenAI-style responses + stores meta.
src/components/integrations/lifi-earn/concierge/BtlRuntimePanel.tsxNew aggregated “receipt” UI for total BTL cost across calls.
src/components/integrations/lifi-earn/buildDepositTx.tsExtracts quote→tx mapping used by both manual sim and AI tool.
src/components/explorer/StorageLayoutViewer.tsxAdds AI storage-slot annotation flow returning structured JSON rendered as chips.
src/components/explorer/ContractDiff.tsxAdds AI “upgrade audit” panel; includes verified-source snippets when available.
src/components/BtlBadge.tsxNew “Powered by BTL” badge component + external link.
src/components/BtlBadge.cssStyling for the badge.
src/components/btl/ThinkingIndicator.tsxNew animated “thinking” indicator shared by explanation panels.
src/components/btl/SlotAnnotationChips.tsxNew tooltip chip UI for per-slot annotations + anomaly highlighting.
src/components/btl/BtlExplanation.tsxNew shared collapsible explanation panel + lightweight markdown renderer + footer badge.
src/components/btl/AiCostChip.tsxNew per-call cost chip UI rendered from forwarded BTL headers.
README.mdDocumentation updated from Gemini → BTL Runtime env/config.
public/logos/btl-runtime.svgAdds BTL Runtime logo asset used by the badge.
api/llm-recommend.tsServerless proxy now targets BTL chat completions and forwards cost/routing headers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadapi/llm-recommend.ts
Comment on lines +72 to 79
if (!Array.isArray((body as any).messages)) {
return res.status(400).json({ error: "Body must include `messages` array" });
}

const serialized = JSON.stringify(body);
if (serialized.length > MAX_BODY_BYTES) {
return res.status(413).json({ error: "Request body too large" });
}
Comment on lines +26 to +32
function buildSourceSnippet(ctx: ContractContext): string {
if (!ctx.metadata?.sources) return '';
return Object.entries(ctx.metadata.sources)
.map(([path, content]) => `// ${path}\n${content}`)
.join('\n\n')
.slice(0, SOURCE_CHAR_CAP);
}
Comment on lines +340 to 346
const userText =
"INPUT:\n```json\n" +
JSON.stringify(userPayload, null, 2) +
"\n```\n\nReturn ONLY the JSON object matching required_output_shape. No prose, no code fences.";

function extractGeminiText(raw: unknown): string | null {
// Gemini 3 Pro can return multi-part content with `thought: true` parts
// before the answer — concatenate every non-thought text part.
try {
const r = raw as {
candidates?: Array<{
content?: {
parts?: Array<{ text?: string; thought?: boolean }>;
};
}>;
};
const parts = r.candidates?.[0]?.content?.parts ?? [];
const joined = parts
.filter((p) => !p.thought && typeof p.text === "string")
.map((p) => p.text ?? "")
.join("")
.trim();
return joined.length > 0 ? joined : null;
} catch {
return null;
}
}

function safeParseJson(text: string): unknown {
try {
return JSON.parse(text);
} catch {
// Strip common noise: code fences, leading commentary
const stripped = text
.replace(/^```(?:json)?/i, "")
.replace(/```$/i, "")
.trim();
try {
return JSON.parse(stripped);
} catch {
// Thinking models sometimes prepend prose before the JSON object.
// Find the first `{` and last `}` and try parsing that substring.
const first = stripped.indexOf("{");
const last = stripped.lastIndexOf("}");
if (first >= 0 && last > first) {
try {
return JSON.parse(stripped.slice(first, last + 1));
} catch {
/* fall through */
}
}
return null;
}
}
return buildBtlChatRequest(system, userText, { temperature: 0.2 });
}
Comment on lines +154 to 160
const userText =
"INPUT:\n```json\n" +
JSON.stringify(payload, null, 2) +
"\n```\n\nReturn ONLY the JSON object.";

function safeParseJson(text: string): unknown {
try {
return JSON.parse(text);
} catch {
const stripped = text
.replace(/^```(?:json)?/i, "")
.replace(/```$/i, "")
.trim();
try {
return JSON.parse(stripped);
} catch {
const first = stripped.indexOf("{");
const last = stripped.lastIndexOf("}");
if (first >= 0 && last > first) {
try {
return JSON.parse(stripped.slice(first, last + 1));
} catch {
/* fall through */
}
}
return null;
}
}
return buildBtlChatRequest(system, userText, { temperature: 0.1 });
}

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:b4a05a8069

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadsrc/lib/btl/agent.ts Outdated
const toolRuns: Array<{ name: string; args: unknown; result: unknown }> = [];

for (let i = 0; i < maxIters; i++) {
const { data, meta } = await callBtl({ model, messages, tools: toolSpecs, tool_choice: "auto", temperature: 0.2 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require the preflight tool call

When the model decides to answer without tools, which is allowed by tool_choice: "auto", runBtlAgent returns that text immediately and DepositFlow.handlePreflight displays it even if toolRuns is empty. In that scenario the “Preflight with AI” path can present a deposit assessment that never ran simulate_deposit/REVM, despite the feature relying on simulation-backed numbers; force the required tool for this preflight or reject final text until the tool has run.

Useful? React with 👍 / 👎.

Comment threadsrc/lib/btl/client.ts
opts: { model?: string; temperature?: number; jsonMode?: boolean; tools?: unknown[]; toolChoice?: unknown; maxTokens?: number } = {},
): Record<string, unknown> {
const body: Record<string, unknown> = {
model: opts.model ?? BTL_DEFAULT_MODEL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor BTL_MODEL for default requests

For normal app calls opts.model is absent, so this hard-codes deepseek-v4-flash into the browser request body; the server proxy then forwards that body unchanged, meaning the documented BTL_MODEL environment variable cannot change the deployed default model. Deployments that set BTL_MODEL to an approved or fallback model will still send the bundled default unless the client omits model or the proxy overwrites/allowlists it server-side.

Useful? React with 👍 / 👎.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 29 out of 30 changed files in this pull request and generated 6 comments.

Comment threadapi/llm-recommend.ts Outdated
Comment on lines +42 to +55
// TEMP debug probe — reports env visibility without leaking the key. Remove after diagnosis.
if (req.query?.debug === "1") {
const k = process.env.BTL_API_KEY || "";
return res.status(200).json({
vercelEnv: process.env.VERCEL_ENV || null,
gitBranch: process.env.VERCEL_GIT_COMMIT_REF || null,
commit: process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) || null,
hasKey: k.length > 0,
keyLen: k.length,
keyPrefix: k ? k.slice(0, 3) : null,
baseUrl: process.env.BTL_BASE_URL || "(default)",
btlEnvNames: Object.keys(process.env).filter((n) => n.toUpperCase().includes("BTL")),
});
}
Comment on lines +677 to +679
setPreflightText(finalText);
setPreflightMeta(metas.at(-1) ?? null);
setPreflightRan(toolRuns.some((r) => r.name === "simulate_deposit"));
Comment on lines +51 to +58
const slots = json.slots
.filter((s): s is Record<string, unknown> => !!s && typeof s === 'object' && typeof (s as { note?: unknown }).note === 'string')
.map((s) => ({
slot: (s.slot as string) ?? null,
label: (s.label as string) ?? null,
note: s.note as string,
unusual: !!s.unusual,
}));
argsList.map((a) =>
a
? `${a.synthChainId}:${a.synthTokenAddress}:${a.sourceTokenSymbol ?? ""}:${a.rankedVaults.slice(0, 8).map((v) => v.slug).join(",")}`
? `${a.synthChainId}:${a.synthTokenAddress}:${a.sourceTokenSymbol ?? ""}:${a.model ?? ""}:${intentFingerprint(a.intent)}:${a.rankedVaults.map((v) => v.slug).join(",")}`
Comment on lines +1 to +4
import { useCallback, useState } from "react";
import { buildBtlChatRequest, extractOpenAiText, type BtlRuntimeMeta } from "./client";
import { postLlmRecommend } from "@/components/integrations/lifi-earn/earnApi";

Comment on lines +150 to 151
const request = buildGeminiIntentRequest(intent, candidates, args.walletAssets, args.sourceTokenSymbol, args.sourceChainId, args.model);
let lastError: string | null = null;

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 29 out of 30 changed files in this pull request and generated 8 comments.

Comment threadapi/llm-recommend.ts
Comment on lines 76 to 79
const serialized = JSON.stringify(body);
if (serialized.length > MAX_BODY_BYTES) {
return res.status(413).json({ error: "Request body too large" });
}
Comment threadsrc/lib/btl/agent.ts
Comment on lines +30 to +34
try { result = tool ? await tool.run(JSON.parse(c.function.arguments || "{}")) : { error: "unknown tool" }; }
catch (e: any) { result = { error: e?.message ?? "tool failed" }; }
toolRuns.push({ name: c.function.name, args: c.function.arguments, result });
messages.push({ role: "tool", tool_call_id: c.id, content: JSON.stringify(result) });
}
Comment threadREADME.md
Comment on lines +173 to +175
| `BTL_API_KEY` | BTL Runtime API key for the yield concierge LLM |
| `BTL_MODEL` | BTL model (default: `deepseek-v4-flash`) |
| `BTL_BASE_URL` | BTL Runtime base URL (default: `https://api.badtheorylabs.com`) |
Comment on lines 262 to 265
argsList.map((a) =>
a
? `${a.synthChainId}:${a.synthTokenAddress}:${a.sourceTokenSymbol ?? ""}:${a.rankedVaults.slice(0, 8).map((v) => v.slug).join(",")}`
? `${a.synthChainId}:${a.synthTokenAddress}:${a.sourceTokenSymbol ?? ""}:${a.model ?? ""}:${intentFingerprint(a.intent)}:${a.rankedVaults.map((v) => v.slug).join(",")}`
: "null",
Comment on lines 67 to 71
function buildParseRequest(
userText: string,
rawUserText: string,
chains: EarnChainInfo[],
protocols: EarnProtocolInfo[]
) {
Comment on lines +184 to 187
export async function postLlmRecommend(body: unknown): Promise<{ data: unknown; meta: BtlRuntimeMeta }> {
const res = await fetch(LLM_PROXY, {
method: "POST",
headers: { "Content-Type": "application/json", ...proxyHeaders() },
}

const request = buildGeminiIntentRequest(intent, candidates, args.walletAssets, args.sourceTokenSymbol, args.sourceChainId);
const request = buildGeminiIntentRequest(intent, candidates, args.walletAssets, args.sourceTokenSymbol, args.sourceChainId, args.model);
@@ -204,11 +206,12 @@ async function fetchRecommendationForAsset(
const request = buildGeminiRequest([source], candidateMap);
CopilotAI review requested due to automatic review settings July 6, 2026 14:12
CopilotAI reviewed Jul 6, 2026

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Timidan