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

@Timidan Timidan commented Jul 6, 2026

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.
Copilot AI review requested due to automatic review settings July 6, 2026 05:02
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
web3-toolkit Ready Ready Preview, Comment Jul 6, 2026 2:13pm

Copilot AI 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
File Description
vite.config.ts Dev-server proxy now forwards BTL chat completions + cost headers; expands watch ignores.
src/lib/btl/useBtlExplain.ts Generic “explain” hook wrapping BTL calls + meta.
src/lib/btl/models.ts BTL model registry + defaults and AB toggle list.
src/lib/btl/client.ts Shared request builder, text extraction, meta parsing, JSON parsing helpers.
src/lib/btl/agent.ts Tool-calling loop (runBtlAgent) for non-streaming chat/tool execution.
src/components/smart-decoder/SmartDecoder.tsx Adds “What does this call do?” AI explanation panel for decoded calldata.
src/components/simulation-results/SummaryTab.tsx Adds “Explain this revert” AI explanation panel for reverted simulations.
src/components/integrations/lifi-earn/earnApi.ts postLlmRecommend now returns { data, meta } and parses BTL headers.
src/components/integrations/lifi-earn/DepositFlow.tsx Adds AI preflight that calls REVM simulation via tool-calling agent; extracts tx builder.
src/components/integrations/lifi-earn/concierge/VaultRecommendations.tsx Shows per-recommendation cost chip for AI-sourced recommendations.
src/components/integrations/lifi-earn/concierge/types.ts Adds optional meta to recommendations for cost/routing.
src/components/integrations/lifi-earn/concierge/LlmErrorAlert.tsx Updates error copy from Gemini to BTL.
src/components/integrations/lifi-earn/concierge/intent/IntentPanel.tsx Adds per-parse cost chip + model A/B toggle + cache key fingerprinting.
src/components/integrations/lifi-earn/concierge/intent/hooks/useIntentRecommendation.ts Migrates intent recommendation request/parse to OpenAI-style responses + stores meta.
src/components/integrations/lifi-earn/concierge/intent/hooks/useIntentParser.ts Migrates NL intent parsing to OpenAI-style responses + returns meta.
src/components/integrations/lifi-earn/concierge/IdleSweepPanel.tsx Adds session-level BTL receipt panel and “Powered by BTL” badge gating.
src/components/integrations/lifi-earn/concierge/hooks/useVaultRecommendations.ts Migrates idle-sweep recommendations to OpenAI-style responses + stores meta.
src/components/integrations/lifi-earn/concierge/BtlRuntimePanel.tsx New aggregated “receipt” UI for total BTL cost across calls.
src/components/integrations/lifi-earn/buildDepositTx.ts Extracts quote→tx mapping used by both manual sim and AI tool.
src/components/explorer/StorageLayoutViewer.tsx Adds AI storage-slot annotation flow returning structured JSON rendered as chips.
src/components/explorer/ContractDiff.tsx Adds AI “upgrade audit” panel; includes verified-source snippets when available.
src/components/BtlBadge.tsx New “Powered by BTL” badge component + external link.
src/components/BtlBadge.css Styling for the badge.
src/components/btl/ThinkingIndicator.tsx New animated “thinking” indicator shared by explanation panels.
src/components/btl/SlotAnnotationChips.tsx New tooltip chip UI for per-slot annotations + anomaly highlighting.
src/components/btl/BtlExplanation.tsx New shared collapsible explanation panel + lightweight markdown renderer + footer badge.
src/components/btl/AiCostChip.tsx New per-call cost chip UI rendered from forwarded BTL headers.
README.md Documentation updated from Gemini → BTL Runtime env/config.
public/logos/btl-runtime.svg Adds BTL Runtime logo asset used by the badge.
api/llm-recommend.ts Serverless 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 thread api/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-connector chatgpt-codex-connector Bot 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 thread src/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 thread src/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 👍 / 👎.

Copilot AI 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 thread api/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;

Copilot AI 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 thread api/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 thread src/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 thread README.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);
Copilot AI review requested due to automatic review settings July 6, 2026 14:12

Copilot AI 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 free to 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