Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ version-compare = "0.1"
vte = { git = "https://github.com/warpdotdev/vte.git", rev = "4b399c87b63ba88f45709edaa6383fc519f6c900", default-features = false }
walkdir = "2"
warp-workflows = { git = "https://github.com/warpdotdev/workflows", rev = "793a98ddda6ef19682aed66364faebd2829f0e01" }
warp_multi_agent_api = { git = "https://github.com/warpdotdev/warp-proto-apis.git", rev = "3d26f166b74469d5c12080ef6a3423fadee4d0e3" }
warp_multi_agent_api = { git = "https://github.com/warpdotdev/warp-proto-apis.git", rev = "d0e6727" }
wasm-bindgen = "0.2.89"
wasm-bindgen-futures = "0.4.42"
web-sys = { version = "0.3.69", features = [
Expand Down
5 changes: 4 additions & 1 deletion app/src/ai/agent/conversation_tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::collections::HashMap;

use ai::api_keys::ApiKeyManager;
use ai::api_keys::{ApiKeyManager, CustomEndpointSchema};
use warp_core::features::FeatureFlag;
use warp_multi_agent_api as api;
use warpui::{App, SingletonEntity};
Expand Down Expand Up @@ -118,6 +118,7 @@ fn tool_call_result_message(

fn start_recording_tool_call() -> api::message::tool_call::Tool {
api::message::tool_call::Tool::StartRecording(api::message::tool_call::StartRecording {
description: String::new(),
frame_rate: 15,
limits: None,
summary: String::new(),
Expand Down Expand Up @@ -604,6 +605,7 @@ fn update_cost_and_usage_resolves_custom_endpoint_alias_for_footer_usage() {
Some("Friendly alias".to_string()),
Some("config-key".to_string()),
)],
CustomEndpointSchema::default(),
ctx,
);
});
Expand Down Expand Up @@ -766,6 +768,7 @@ fn footer_model_token_usage_keeps_custom_endpoint_usage_distinct_from_same_label
Some("Resolved custom".to_string()),
Some("config-key".to_string()),
)],
CustomEndpointSchema::default(),
ctx,
);
});
Expand Down
3 changes: 3 additions & 0 deletions app/src/ai/llms_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ fn endpoint(
name: name.into(),
url: url.into(),
api_key: api_key.into(),
schema: Default::default(),
models,
}
}
Expand Down Expand Up @@ -532,6 +533,7 @@ fn active_models_fall_back_to_usable_choice_or_custom_endpoint_when_default_disa
None,
Some(custom_model_id.to_string()),
)],
ai::api_keys::CustomEndpointSchema::default(),
ctx,
);
});
Expand Down Expand Up @@ -700,6 +702,7 @@ fn reconcile_preserves_custom_models_saved_on_execution_profile() {
Some("Custom Model".to_string()),
Some(custom_model_id.to_string()),
)],
ai::api_keys::CustomEndpointSchema::default(),
ctx,
);
});
Expand Down
6 changes: 5 additions & 1 deletion app/src/settings_view/ai_page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2366,6 +2366,7 @@ impl AISettingsPageView {
name,
url,
api_key,
schema,
models,
} => {
if !Self::can_use_custom_inference_controls(ctx) {
Expand All @@ -2378,6 +2379,7 @@ impl AISettingsPageView {
url.clone(),
api_key.clone(),
models.clone(),
*schema,
ctx,
);
});
Expand Down Expand Up @@ -2405,6 +2407,7 @@ impl AISettingsPageView {
name,
url,
api_key,
schema,
models,
} => {
if !Self::can_use_custom_inference_controls(ctx) {
Expand All @@ -2418,6 +2421,7 @@ impl AISettingsPageView {
url.clone(),
api_key.clone(),
models.clone(),
*schema,
ctx,
);
});
Expand Down Expand Up @@ -8531,7 +8535,7 @@ impl ApiKeysWidget {

if show_custom_endpoints {
add_paragraph(vec![FormattedTextFragment::plain_text(
"Add custom endpoints to use third-party models. Custom endpoints must support the OpenAI-compatible Chat Completions API.",
"Add custom endpoints to use third-party models. Custom endpoints must support OpenAI Chat Completions, OpenAI Responses, or Anthropic Messages.",
)]);
}

Expand Down
61 changes: 60 additions & 1 deletion app/src/settings_view/custom_inference_modal.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

use ::ai::api_keys::CustomEndpoint;
use ::ai::api_keys::{CustomEndpoint, CustomEndpointSchema};
use url::Url;
use warp_editor::editor::NavigationKey;
use warpui::elements::{
Expand All @@ -25,6 +25,7 @@ use crate::editor::{
use crate::modal::{Modal, ModalViewState};
use crate::ui_components::icons::Icon;
use crate::view_components::action_button::{ActionButton, DangerSecondaryTheme};
use crate::view_components::{Dropdown, DropdownItem};

const LABEL_FONT_SIZE: f32 = 12.;
const INPUT_WIDTH: f32 = 480.;
Expand Down Expand Up @@ -54,13 +55,15 @@ pub enum CustomEndpointModalEvent {
name: String,
url: String,
api_key: String,
schema: CustomEndpointSchema,
models: Vec<(String, Option<String>, Option<String>)>,
},
SaveEndpoint {
index: usize,
name: String,
url: String,
api_key: String,
schema: CustomEndpointSchema,
models: Vec<(String, Option<String>, Option<String>)>,
},
RemoveEndpoint {
Expand All @@ -75,6 +78,7 @@ pub enum CustomEndpointModalAction {
AddModel,
RemoveModel(usize),
RemoveEndpoint,
SetSchema(CustomEndpointSchema),
}

struct ModelRow {
Expand All @@ -88,6 +92,8 @@ pub struct CustomEndpointModal {
endpoint_name_editor: ViewHandle<EditorView>,
endpoint_url_editor: ViewHandle<EditorView>,
api_key_editor: ViewHandle<EditorView>,
schema_dropdown: ViewHandle<Dropdown<CustomEndpointModalAction>>,
schema: CustomEndpointSchema,
model_rows: Vec<ModelRow>,
cancel_button_mouse_state: MouseStateHandle,
save_button_mouse_state: MouseStateHandle,
Expand Down Expand Up @@ -175,6 +181,30 @@ impl CustomEndpointModal {
editor
});

let schema = endpoint.map(|endpoint| endpoint.schema).unwrap_or_default();
let schema_dropdown = ctx.add_typed_action_view(|ctx| {
let mut dropdown = Dropdown::new(ctx);
dropdown.set_items(
[
CustomEndpointSchema::OpenaiChatCompletions,
CustomEndpointSchema::OpenaiResponses,
CustomEndpointSchema::AnthropicMessages,
]
.into_iter()
.map(|schema| {
DropdownItem::new(
schema.display_name(),
CustomEndpointModalAction::SetSchema(schema),
)
})
.collect(),
ctx,
);
dropdown
});
schema_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_selected_by_name(schema.display_name(), ctx);
});
let mut model_rows = Vec::new();
if let Some(ep) = endpoint {
for model in &ep.models {
Expand Down Expand Up @@ -233,6 +263,8 @@ impl CustomEndpointModal {
endpoint_name_editor,
endpoint_url_editor,
api_key_editor,
schema_dropdown,
schema,
model_rows,
cancel_button_mouse_state: Default::default(),
save_button_mouse_state: Default::default(),
Expand Down Expand Up @@ -319,6 +351,16 @@ impl CustomEndpointModal {
self.api_key_editor.update(ctx, |editor, ctx| {
editor.set_buffer_text(endpoint.map(|e| e.api_key.as_str()).unwrap_or(""), ctx);
});
self.schema_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_selected_by_name(
endpoint
.map(|endpoint| endpoint.schema)
.unwrap_or_default()
.display_name(),
ctx,
);
});
self.schema = endpoint.map(|endpoint| endpoint.schema).unwrap_or_default();
// Rebuild model rows
// Old model row editors will be dropped with the modal body
self.model_rows.clear();
Expand Down Expand Up @@ -434,13 +476,15 @@ impl CustomEndpointModal {
name,
url,
api_key,
schema: self.schema,
models,
});
} else {
ctx.emit(CustomEndpointModalEvent::AddEndpoint {
name,
url,
api_key,
schema: self.schema,
models,
});
}
Expand Down Expand Up @@ -704,6 +748,17 @@ impl View for CustomEndpointModal {
.with_margin_bottom(16.)
.finish(),
);
// Request/response protocol
column.add_child(
Container::new(label("API schema"))
.with_margin_bottom(4.)
.finish(),
);
column.add_child(
Container::new(ChildView::new(&self.schema_dropdown).finish())
.with_margin_bottom(16.)
.finish(),
);

// Endpoint name
column.add_child(
Expand Down Expand Up @@ -1067,6 +1122,10 @@ impl TypedActionView for CustomEndpointModal {
ctx.emit(CustomEndpointModalEvent::RemoveEndpoint { index });
}
}
CustomEndpointModalAction::SetSchema(schema) => {
self.schema = *schema;
ctx.notify();
}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion app/src/settings_view/custom_inference_modal_tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use ai::api_keys::CustomEndpointModel;
use ai::api_keys::{CustomEndpointModel, CustomEndpointSchema};
use pathfinder_geometry::vector::vec2f;
use warpui::platform::WindowStyle;
use warpui::scene::Scene;
Expand All @@ -13,6 +13,7 @@ fn endpoint_with_models(model_count: usize) -> CustomEndpoint {
name: "Test endpoint".to_string(),
url: "https://api.example.com/v1".to_string(),
api_key: "key".to_string(),
schema: CustomEndpointSchema::default(),
models: (0..model_count)
.map(|index| CustomEndpointModel {
name: format!("model-{index}"),
Expand Down
51 changes: 51 additions & 0 deletions crates/ai/src/api_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,52 @@ pub struct CustomEndpoint {
pub url: String,
pub api_key: String,
pub models: Vec<CustomEndpointModel>,
pub schema: CustomEndpointSchema,
}

/// The request/response protocol used by a custom inference endpoint.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CustomEndpointSchema {
/// OpenAI Chat Completions, retained as the legacy/default protocol.
#[default]
OpenaiChatCompletions,
/// OpenAI Responses.
OpenaiResponses,
/// Anthropic Messages.
AnthropicMessages,
}

impl CustomEndpointSchema {
pub fn display_name(self) -> &'static str {
match self {
Self::OpenaiChatCompletions => "OpenAI Chat Completions",
Self::OpenaiResponses => "OpenAI Responses",
Self::AnthropicMessages => "Anthropic Messages",
}
}

pub fn from_display_name(name: &str) -> Option<Self> {
match name {
"OpenAI Chat Completions" => Some(Self::OpenaiChatCompletions),
"OpenAI Responses" => Some(Self::OpenaiResponses),
"Anthropic Messages" => Some(Self::AnthropicMessages),
_ => None,
}
}
fn to_proto(self) -> api::request::settings::custom_model_providers::CustomEndpointSchema {
match self {
Self::OpenaiChatCompletions => {
api::request::settings::custom_model_providers::CustomEndpointSchema::OpenaiChatCompletions
}
Self::OpenaiResponses => {
api::request::settings::custom_model_providers::CustomEndpointSchema::OpenaiResponses
}
Self::AnthropicMessages => {
api::request::settings::custom_model_providers::CustomEndpointSchema::AnthropicMessages
}
}
}
}

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
Expand Down Expand Up @@ -295,12 +341,14 @@ impl ApiKeyManager {
url: String,
api_key: String,
models: Vec<(String, Option<String>, Option<String>)>,
schema: CustomEndpointSchema,
ctx: &mut ModelContext<Self>,
) {
self.keys.custom_endpoints.push(CustomEndpoint {
name,
url,
api_key,
schema,
models: models
.into_iter()
.map(|(name, alias, config_key)| CustomEndpointModel {
Expand All @@ -323,6 +371,7 @@ impl ApiKeyManager {
url: String,
api_key: String,
models: Vec<(String, Option<String>, Option<String>)>,
schema: CustomEndpointSchema,
ctx: &mut ModelContext<Self>,
) {
if index >= self.keys.custom_endpoints.len() {
Expand All @@ -332,6 +381,7 @@ impl ApiKeyManager {
name,
url,
api_key,
schema,
models: models
.into_iter()
.map(|(name, alias, config_key)| CustomEndpointModel {
Expand Down Expand Up @@ -431,6 +481,7 @@ impl ApiKeyManager {
|endpoint| api::request::settings::custom_model_providers::CustomModelProvider {
base_url: endpoint.url.clone(),
api_key: endpoint.api_key.clone(),
schema: endpoint.schema.to_proto() as i32,
models: endpoint
.models
.iter()
Expand Down
Loading