Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
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.
| 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" }); | ||
| } |
| 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); | ||
| } |
| 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 }); | ||
| } |
| 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 }); | ||
| } |
There was a problem hiding this comment.
💡 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".
| 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 }); |
There was a problem hiding this comment.
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 👍 / 👎.
| 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, |
There was a problem hiding this comment.
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 👍 / 👎.
| // 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")), | ||
| }); | ||
| } |
| setPreflightText(finalText); | ||
| setPreflightMeta(metas.at(-1) ?? null); | ||
| setPreflightRan(toolRuns.some((r) => r.name === "simulate_deposit")); |
| 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(",")}` |
| import { useCallback, useState } from "react"; | ||
| import { buildBtlChatRequest, extractOpenAiText, type BtlRuntimeMeta } from "./client"; | ||
| import { postLlmRecommend } from "@/components/integrations/lifi-earn/earnApi"; | ||
|
|
| const request = buildGeminiIntentRequest(intent, candidates, args.walletAssets, args.sourceTokenSymbol, args.sourceChainId, args.model); | ||
| let lastError: string | null = null; |
| const serialized = JSON.stringify(body); | ||
| if (serialized.length > MAX_BODY_BYTES) { | ||
| return res.status(413).json({ error: "Request body too large" }); | ||
| } |
| 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) }); | ||
| } |
| | `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`) | |
| 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", |
| function buildParseRequest( | ||
| userText: string, | ||
| rawUserText: string, | ||
| chains: EarnChainInfo[], | ||
| protocols: EarnProtocolInfo[] | ||
| ) { |
| 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); | |||
…lls 502/empty-response flakiness
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
src/lib/btl) + a non-streaming tool-calling loop (runBtlAgent) that runs the real REVM simulator on a deposit and narrates it.AiCostChip,BtlRuntimePanel, a foldableBtlExplanationpanel, and a "Powered by BTL" badge across surfaces.The rules-based fallback and the
LLM_MODE=offkill switch keep the app crash-proof. Build is green and the BTL client is unit-tested.To deploy: set
BTL_API_KEY(Preview scope); leavePROXY_SECRETunset. Existing LI.FI / Etherscan / EDB env covers the surfaces the AI runs on.