Skip to content

feat(exa-hosted-key): Restore exa hosted key#3499

Merged
TheodoreSpeaks merged 1 commit intofeat/mothership-copilotfrom
feat/revert-exa
Mar 10, 2026
Merged

feat(exa-hosted-key): Restore exa hosted key#3499
TheodoreSpeaks merged 1 commit intofeat/mothership-copilotfrom
feat/revert-exa

Conversation

@TheodoreSpeaks
Copy link
Collaborator

Summary

Restore exa hosted key. See: #3221
Fixes #(issue)

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Other: ___________

Testing

Tested previously with exact same code. See previous pr for testing.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Screenshots/Videos

@cursor
Copy link

cursor bot commented Mar 10, 2026

PR Summary

High Risk
Updates core tool execution and billing paths by injecting hosted API keys, adding rate limiting/retry behavior, and changing how tool costs are computed and logged; regressions could impact throttling behavior and usage accounting on hosted deployments.

Overview
Restores Exa hosted-key support end-to-end: Exa can now be configured as a workspace BYOK provider, and Exa tools declare hosting metadata (env key prefix, pricing, and rate limits) so hosted Sim can run them without user-supplied keys.

Introduces a new HostedKeyRateLimiter (round-robin across {PREFIX}_1..N, per-workspace request limits, optional custom dimensions) and updates tool execution to prefer BYOK keys, otherwise inject a hosted key, retry upstream 429/503s with backoff, emit new throttling telemetry, and log fixed tool usage costs (with optional metadata) while stripping internal __* fields from outputs.

Adds hideWhenHosted subblock support to hide API key inputs in the editor/serializer/param generation when hosted, and propagates tool cost accounting (toolCost) through provider responses, execution cost aggregation, and billing usage logs (including batch logging).

Written by Cursor Bugbot for commit a56b7ce. This will update automatically on new commits. Configure here.

@vercel
Copy link

vercel bot commented Mar 10, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 10, 2026 0:33am

Request Review

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@TheodoreSpeaks TheodoreSpeaks merged commit 898f8ce into feat/mothership-copilot Mar 10, 2026
4 checks passed
@TheodoreSpeaks TheodoreSpeaks deleted the feat/revert-exa branch March 10, 2026 00:41
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR restores Exa AI hosted-key support, allowing Sim to inject its own Exa API keys when users haven't provided one (or have BYOK configured). The change spans the full billing/rate-limiting stack: a new HostedKeyRateLimiter with per-workspace enforcement, round-robin key selection, a ToolHostingConfig type that tools opt into, cost tracking via costDollars returned by the Exa API, sumToolCosts aggregation across providers, and UI changes to hide the API key field on hosted deployments.

Key concern:

Fragile cost tracking for Exa hosted keys — All four Exa tools throw inside getCost when costDollars?.total == null. If the Exa API omits this field (free tier, query type, or API version change), the exception propagates through applyHostedKeyCostToResult and causes the entire tool execution to fail after the Exa API call already succeeded. A graceful fallback (log a warning, return { cost: 0 }) would prevent silent data loss.

Confidence Score: 4/5

  • The PR is largely safe to merge, with one concrete reliability concern in Exa tool cost handling that should be addressed.
  • The hosted-key infrastructure is well-designed: rate limiting correctly enforces per-workspace budgets, cost tracking aggregates properly, and BYOK integration is solid. However, all four Exa tools unconditionally throw an exception in getCost when the API omits costDollars—a realistic scenario given free tiers, API version changes, or edge cases. This causes silent data loss: the API call succeeds, but the tool execution fails after the fact. This is a real failure mode that should be fixed with a graceful fallback (return { cost: 0 } with a warning log) instead of throwing. Outside this pattern, the integration of hosted keys, rate limiting, cost aggregation, and billing logging is correct.
  • The four Exa tool files (apps/sim/tools/exa/search.ts, answer.ts, find_similar_links.ts, get_contents.ts) all use the same throw-on-null pattern in getCost and should be updated to return a safe default instead of throwing.

Last reviewed commit: a56b7ce

Comment on lines +95 to +101
getCost: (_params, output) => {
const costDollars = output.__costDollars as { total?: number } | undefined
if (costDollars?.total == null) {
throw new Error('Exa search response missing costDollars field')
}
return { cost: costDollars.total, metadata: { costDollars } }
},
Copy link
Contributor

Choose a reason for hiding this comment

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

getCost throws unconditionally when costDollars?.total == null, but __costDollars is typed as optional (the type { total?: number } | undefined acknowledges total may be absent). If the Exa API ever responds without a costDollars field—due to free tier, certain query types, or API version changes—the exception will propagate through applyHostedKeyCostToResult after the Exa API call already succeeded. This causes the entire tool execution to fail, and the successful API result is lost.

Consider returning { cost: 0 } (with a warning log) instead of throwing when cost data is unavailable:

getCost: (_params, output) => {
  const costDollars = output.__costDollars as { total?: number } | undefined
  if (costDollars?.total == null) {
    logger.warn('Exa search response missing costDollars field — skipping cost tracking')
    return { cost: 0 }
  }
  return { cost: costDollars.total, metadata: { costDollars } }
}

The same pattern appears in answer.ts, find_similar_links.ts, and get_contents.ts.

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.

1 participant