Skip to content

feat: Custom inference endpoints for third-party API models#10781

Open
dagmfactory wants to merge 7 commits into
masterfrom
dagm/custom-api-endpoint
Open

feat: Custom inference endpoints for third-party API models#10781
dagmfactory wants to merge 7 commits into
masterfrom
dagm/custom-api-endpoint

Conversation

@dagmfactory
Copy link
Copy Markdown

@dagmfactory dagmfactory commented May 12, 2026

Description

Adds support for custom inference endpoints in AI settings, allowing users to add, edit, and remove third-party OpenAI-compatible API endpoints. Custom endpoint models surface in all model selectors when the feature flag is enabled.

Changes

  1. CustomInferenceEndpoints data model (crates/ai/src/api_keys.rs, app/src/ai/llms.rs) — stores endpoint name, URL, API key, and model list with aliases.
  2. Feature flag (crates/warp_features/src/lib.rs) — gates the feature behind CustomInferenceEndpoints.
  3. Agent request wiring (app/src/ai/agent/api.rs, app/src/ai/agent/api/impl_tests.rs) — passes custom_model_providers to the agent API.
  4. AI settings UI (app/src/settings_view/ai_page.rs) — new "Custom inference" section with endpoint cards, add/edit/remove flow.
  5. Custom endpoint modal (app/src/settings_view/custom_inference_modal.rs) — form for entering endpoint details and model names/aliases.
  6. Remove confirmation dialog (app/src/settings_view/remove_custom_endpoint_confirmation_dialog.rs) — confirms destructive removal with model chips preview.
  7. Model selector integration — custom endpoint models appear in agent mode, toolbar, and ambient agent model selectors.
  8. Infrastructure — modal dismiss-on-escape keystroke, menu right-side icon support, settings info-tooltip max-width.

Screenshots / Videos

https://www.loom.com/share/0a8b6ee1ce0d43e09b698e621be8416d

Linked Issue

https://linear.app/warpdotdev/issue/QUALITY-605/bring-your-own-endpoint

Remaining work:

Disable bring your own feature for enterprise accounts
Order custom models right after auto models in the model picker
Warp TOS hyperlink in custom inference info icon

Testing

  • I have manually tested my changes locally with ./script/run
  • Added unit tests for CustomInferenceEndpoints serialization/deserialization in crates/ai/src/api_keys_tests.rs and app/src/ai/llms_tests.rs.
  • Added impl_tests.rs coverage for agent API wiring.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

CHANGELOG-NEW-FEATURE: Add custom inference endpoints to AI settings, allowing users to bring their own OpenAI-compatible third-party API endpoints and use them across all model selectors.

@cla-bot cla-bot Bot added the cla-signed label May 12, 2026
@dagmfactory dagmfactory marked this pull request as ready for review May 13, 2026 00:03
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 13, 2026

@dagmfactory

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@dagmfactory dagmfactory requested a review from danielpeng2 May 13, 2026 00:03
Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

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

Overview

This PR adds local storage, settings UI, model-selector integration, and request wiring for custom OpenAI-compatible inference endpoints.

Concerns

  • Custom endpoint secrets can be sent even when the workspace BYOK entitlement is disabled.
  • The settings UI now enables BYOK fields/custom endpoint setup based only on AI availability and the feature flag, not BYOK eligibility.
  • Custom endpoint URLs are accepted with only a non-empty check before being persisted and sent to the agent backend.
  • Custom model IDs still resolve when the feature flag is disabled, while request wiring omits the provider registry.
  • Secure-storage writes for API keys were changed to fire-and-forget spawned callbacks, which can persist stale endpoint/API-key state out of order.

Security

  • The request path bypasses the BYOK entitlement by calling custom_model_providers_for_request(true).
  • Arbitrary custom endpoint URLs need HTTPS/public-host validation before they can become cloud inference targets.

Verdict

Found: 0 critical, 5 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread app/src/ai/agent/api.rs Outdated
);
let custom_model_providers = FeatureFlag::CustomInferenceEndpoints
.is_enabled()
.then(|| api_key_manager.custom_model_providers_for_request(true))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] [SECURITY] Passing true bypasses the workspace BYOK entitlement computed above, so custom endpoint API keys are sent even when BYOK is disabled. Use is_byo_enabled for this request path.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we mean to hardcode true as the argument here? or should this be disabled when the user is an enterprise user?

Comment thread app/src/settings_view/ai_page.rs Outdated
let ai_settings = AISettings::as_ref(app);
let is_any_ai_enabled = ai_settings.is_any_ai_enabled(app);
let is_byo_enabled = UserWorkspaces::as_ref(app).is_byo_api_key_enabled(app);
let is_enabled = is_any_ai_enabled;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] This drops the previous is_byo_enabled check, so users without BYOK can edit provider keys and configure custom endpoints despite the upgrade CTA. Keep the input/add-button enablement tied to both is_any_ai_enabled and BYOK eligibility.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems valid? We want to make sure we don't regress any disabled state.

.is_empty()
});
!name.trim().is_empty()
&& !url.trim().is_empty()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] [SECURITY] The URL validation only checks for non-empty text, so arbitrary schemes or private hosts can be persisted and sent as cloud inference targets. Require HTTPS and reject local/private hosts before saving or sending an endpoint.

Comment thread app/src/ai/llms.rs
/// Resolves an `LLMId` against the user's custom-endpoint LLMs.
/// Returns `None` if the id isn't a known custom model `config_key`.
pub fn custom_llm_info_for_id(&self, id: &LLMId) -> Option<&LLMInfo> {
self.custom_llms.iter().find(|info| info.id == *id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] This lookup resolves custom models even when CustomInferenceEndpoints is disabled, while request wiring omits custom_model_providers in that state. Gate this helper too or reconcile stored custom selections back to a built-in model when the flag is off.

Comment thread crates/ai/src/api_keys.rs Outdated
// already emitted, so the UI updates immediately while the
// potentially slow platform secure-storage call runs in a
// subsequent main-thread callback.
ctx.spawn(async move { json }, |_, json, ctx| {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] These spawned secure-storage writes can complete out of order when a user saves/removes endpoints quickly, allowing an older payload to overwrite a newer delete/edit. Serialize writes or version-check the callback before writing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Did this change get made back when we were investigating lag? If there wasn't an issue with the previous writing mechanism I'm hesitant to make changes.

Copy link
Copy Markdown
Member

@danielpeng2 danielpeng2 left a comment

Choose a reason for hiding this comment

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

Doing some testing, going to leave comments about behaviour as they come up:

Comment thread app/src/ai/agent/api.rs Outdated
);
let custom_model_providers = FeatureFlag::CustomInferenceEndpoints
.is_enabled()
.then(|| api_key_manager.custom_model_providers_for_request(true))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we mean to hardcode true as the argument here? or should this be disabled when the user is an enterprise user?

Comment thread crates/ai/src/api_keys.rs Outdated
/// Returns `None` when BYOK is disabled or no endpoint has both a non-empty URL and API key.
pub fn custom_model_providers_for_request(
&self,
include_byo_keys: bool,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is include_byo_keys the right name for this condition, naming it something like include_custom_model_providers seems more clear? alternatively, we could lift this check outside of the method and check it before calling this method, which might be a simpler API.

Comment thread app/src/ai/llms.rs
|me, _event: &ApiKeyManagerEvent, ctx| {
me.rebuild_custom_llms(ctx);
me.reconcile_disabled_model_preferences(ctx);
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we make it so that we emit the LLMPreferencesEvent::UpdatedAvailableLLMs event directly at the site where the custom endpoints are modified? I'm a little worried about this indirection.

Comment thread app/src/ai/llms.rs Outdated
self.get_computer_use_available()
.choices
.iter()
.chain(self.custom_llm_choices())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we not allow custom models in computer use for now? this requires some special model setup that we aren't doing

Comment thread app/src/ai/llms.rs Outdated
},
description: Some(format!("Custom · {}", endpoint.name)),
disable_reason: None,
vision_supported: false,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe this field controls whether the user can input images when the model is selected. I think it'd be good to leave this on by default and let it error if the model doesn't actually support vision.

Comment thread app/src/settings_view/ai_page.rs Outdated
let ai_settings = AISettings::as_ref(app);
let is_any_ai_enabled = ai_settings.is_any_ai_enabled(app);
let is_byo_enabled = UserWorkspaces::as_ref(app).is_byo_api_key_enabled(app);
let is_enabled = is_any_ai_enabled;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems valid? We want to make sure we don't regress any disabled state.

Comment thread app/src/menu.rs
}
}

/// Creates a new menu item with vertically stacked primary and secondary text.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: this comment got truncated

Comment thread crates/ai/src/api_keys.rs Outdated
Comment on lines +364 to +381
// Lazy migration: stamp a UUID `config_key` on any pre-existing custom model that
// was persisted before this field landed. If any were filled, schedule a re-save so
// we don't repeat the work on the next launch.
let migrated = Self::backfill_missing_config_keys(&mut keys);
if migrated {
let json = match serde_json::to_string(&keys) {
Ok(json) => json,
Err(e) => {
log::error!("Failed to re-serialize API keys after migration: {e:#}");
return keys;
}
};
ctx.spawn(async move { json }, |_, json, ctx| {
if let Err(e) = ctx.secure_storage().write_value(SECURE_STORAGE_KEY, &json) {
log::error!("Failed to write migrated API keys to secure storage: {e:#}");
}
});
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need this migration logic? Users can't add custom models right now, so I don't see how they would create one without a UUID.

Comment thread crates/ai/src/api_keys.rs Outdated
// already emitted, so the UI updates immediately while the
// potentially slow platform secure-storage call runs in a
// subsequent main-thread callback.
ctx.spawn(async move { json }, |_, json, ctx| {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Did this change get made back when we were investigating lag? If there wasn't an issue with the previous writing mechanism I'm hesitant to make changes.

@danielpeng2
Copy link
Copy Markdown
Member

nit: the info icon isn't vertically centered with the text

image

@danielpeng2
Copy link
Copy Markdown
Member

nit: the default width of the modal feels wider than what we have in the mocks

image

@danielpeng2
Copy link
Copy Markdown
Member

when there's only 1 model, we create a gap for the "x" icon even though there's nothing there. I think it should fill to the width if there's only 1 model

image

@danielpeng2
Copy link
Copy Markdown
Member

the custom models don't have the borders around the text, and the edit button doesn't appear in the top right like the mocks

image

@danielpeng2
Copy link
Copy Markdown
Member

the remove button in the edit endpoints modal doesn't have the same styling as in the mocks

image

Copy link
Copy Markdown
Member

@danielpeng2 danielpeng2 left a comment

Choose a reason for hiding this comment

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

Nice job on this! Approving to unblock.

My most important comments are the logic/behavoural ones. We don't have to fix all the UI nits in this PR. Important thing is that we get this in before the bug bash tomorrow.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants