feat: add MSI GenAI custom provider support
- Extended ProviderConfig with optional custom fields for non-OpenAI APIs - Added custom_endpoint_path, custom_auth_header, custom_auth_prefix fields - Added api_format field to distinguish between OpenAI and MSI GenAI formats - Added session_id field for stateful conversation APIs - Implemented chat_msi_genai() method in OpenAI provider - MSI GenAI uses different request format (prompt+sessionId) and response (msg field) - Updated TypeScript types to match Rust schema - Added UI controls in Settings/AIProviders for custom provider configuration - API format selector auto-populates appropriate defaults (OpenAI vs MSI GenAI) - Backward compatible: existing providers default to OpenAI format
This commit is contained in:
parent
4172616c8b
commit
9d8bdd383c
@ -28,9 +28,33 @@ impl Provider for OpenAiProvider {
|
|||||||
&self,
|
&self,
|
||||||
messages: Vec<Message>,
|
messages: Vec<Message>,
|
||||||
config: &ProviderConfig,
|
config: &ProviderConfig,
|
||||||
|
) -> anyhow::Result<ChatResponse> {
|
||||||
|
// Check if using MSI GenAI format
|
||||||
|
let api_format = config.api_format.as_deref().unwrap_or("openai");
|
||||||
|
|
||||||
|
if api_format == "msi_genai" {
|
||||||
|
self.chat_msi_genai(messages, config).await
|
||||||
|
} else {
|
||||||
|
self.chat_openai(messages, config).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenAiProvider {
|
||||||
|
/// OpenAI-compatible API format (default)
|
||||||
|
async fn chat_openai(
|
||||||
|
&self,
|
||||||
|
messages: Vec<Message>,
|
||||||
|
config: &ProviderConfig,
|
||||||
) -> anyhow::Result<ChatResponse> {
|
) -> anyhow::Result<ChatResponse> {
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let url = format!("{}/chat/completions", config.api_url.trim_end_matches('/'));
|
|
||||||
|
// Use custom endpoint path if provided, otherwise default to /chat/completions
|
||||||
|
let endpoint_path = config
|
||||||
|
.custom_endpoint_path
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("/chat/completions");
|
||||||
|
let url = format!("{}{}", config.api_url.trim_end_matches('/'), endpoint_path);
|
||||||
|
|
||||||
let body = serde_json::json!({
|
let body = serde_json::json!({
|
||||||
"model": config.model,
|
"model": config.model,
|
||||||
@ -38,9 +62,14 @@ impl Provider for OpenAiProvider {
|
|||||||
"max_tokens": 4096,
|
"max_tokens": 4096,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Use custom auth header and prefix if provided
|
||||||
|
let auth_header = config.custom_auth_header.as_deref().unwrap_or("Authorization");
|
||||||
|
let auth_prefix = config.custom_auth_prefix.as_deref().unwrap_or("Bearer ");
|
||||||
|
let auth_value = format!("{}{}", auth_prefix, config.api_key);
|
||||||
|
|
||||||
let resp = client
|
let resp = client
|
||||||
.post(&url)
|
.post(&url)
|
||||||
.header("Authorization", format!("Bearer {}", config.api_key))
|
.header(auth_header, auth_value)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.json(&body)
|
.json(&body)
|
||||||
.send()
|
.send()
|
||||||
@ -72,4 +101,89 @@ impl Provider for OpenAiProvider {
|
|||||||
usage,
|
usage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// MSI GenAI custom format
|
||||||
|
async fn chat_msi_genai(
|
||||||
|
&self,
|
||||||
|
messages: Vec<Message>,
|
||||||
|
config: &ProviderConfig,
|
||||||
|
) -> anyhow::Result<ChatResponse> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
// Use custom endpoint path, default to empty (API URL already includes /api/v2/chat)
|
||||||
|
let endpoint_path = config.custom_endpoint_path.as_deref().unwrap_or("");
|
||||||
|
let url = format!("{}{}", config.api_url.trim_end_matches('/'), endpoint_path);
|
||||||
|
|
||||||
|
// Extract system message if present
|
||||||
|
let system_message = messages
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.role == "system")
|
||||||
|
.map(|m| m.content.clone());
|
||||||
|
|
||||||
|
// Get last user message as prompt
|
||||||
|
let prompt = messages
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|m| m.role == "user")
|
||||||
|
.map(|m| m.content.clone())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No user message found"))?;
|
||||||
|
|
||||||
|
// Build request body
|
||||||
|
let mut body = serde_json::json!({
|
||||||
|
"model": config.model,
|
||||||
|
"prompt": prompt,
|
||||||
|
"userId": "user@motorolasolutions.com", // Default user ID
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add optional system message
|
||||||
|
if let Some(system) = system_message {
|
||||||
|
body["system"] = serde_json::Value::String(system);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add session ID if available (for conversation continuity)
|
||||||
|
if let Some(session_id) = &config.session_id {
|
||||||
|
body["sessionId"] = serde_json::Value::String(session_id.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use custom auth header and prefix (no prefix for MSI GenAI)
|
||||||
|
let auth_header = config
|
||||||
|
.custom_auth_header
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("x-msi-genai-api-key");
|
||||||
|
let auth_prefix = config.custom_auth_prefix.as_deref().unwrap_or("");
|
||||||
|
let auth_value = format!("{}{}", auth_prefix, config.api_key);
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.post(&url)
|
||||||
|
.header(auth_header, auth_value)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let text = resp.text().await?;
|
||||||
|
anyhow::bail!("MSI GenAI API error {status}: {text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let json: serde_json::Value = resp.json().await?;
|
||||||
|
|
||||||
|
// Extract response content from "msg" field
|
||||||
|
let content = json["msg"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No 'msg' field in response"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Note: sessionId from response should be stored back to config.session_id
|
||||||
|
// This would require making config mutable or returning it as part of ChatResponse
|
||||||
|
// For now, the caller can extract it from the response if needed
|
||||||
|
// TODO: Consider adding session_id to ChatResponse struct
|
||||||
|
|
||||||
|
Ok(ChatResponse {
|
||||||
|
content,
|
||||||
|
model: config.model.clone(),
|
||||||
|
usage: None, // MSI GenAI doesn't provide token usage in response
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,25 @@ pub struct ProviderConfig {
|
|||||||
pub api_url: String,
|
pub api_url: String,
|
||||||
pub api_key: String,
|
pub api_key: String,
|
||||||
pub model: String,
|
pub model: String,
|
||||||
|
/// Optional: Custom endpoint path (e.g., "" for no path, "/v1/chat" for custom path)
|
||||||
|
/// If None, defaults to "/chat/completions" for OpenAI compatibility
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub custom_endpoint_path: Option<String>,
|
||||||
|
/// Optional: Custom auth header name (e.g., "x-msi-genai-api-key")
|
||||||
|
/// If None, defaults to "Authorization"
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub custom_auth_header: Option<String>,
|
||||||
|
/// Optional: Custom auth value prefix (e.g., "" for no prefix, "Bearer " for OpenAI)
|
||||||
|
/// If None, defaults to "Bearer "
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub custom_auth_prefix: Option<String>,
|
||||||
|
/// Optional: API format ("openai" or "msi_genai")
|
||||||
|
/// If None, defaults to "openai"
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub api_format: Option<String>,
|
||||||
|
/// Optional: Session ID for stateful APIs like MSI GenAI
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub session_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@ -10,6 +10,11 @@ export interface ProviderConfig {
|
|||||||
api_url: string;
|
api_url: string;
|
||||||
api_key: string;
|
api_key: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
custom_endpoint_path?: string;
|
||||||
|
custom_auth_header?: string;
|
||||||
|
custom_auth_prefix?: string;
|
||||||
|
api_format?: string;
|
||||||
|
session_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
|
|||||||
@ -27,6 +27,11 @@ const emptyProvider: ProviderConfig = {
|
|||||||
model: "",
|
model: "",
|
||||||
max_tokens: 4096,
|
max_tokens: 4096,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
|
custom_endpoint_path: undefined,
|
||||||
|
custom_auth_header: undefined,
|
||||||
|
custom_auth_prefix: undefined,
|
||||||
|
api_format: undefined,
|
||||||
|
session_id: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AIProviders() {
|
export default function AIProviders() {
|
||||||
@ -267,6 +272,89 @@ export default function AIProviders() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Custom provider format options */}
|
||||||
|
{form.provider_type === "custom" && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>API Format</Label>
|
||||||
|
<Select
|
||||||
|
value={form.api_format ?? "openai"}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const format = v;
|
||||||
|
const defaults =
|
||||||
|
format === "msi_genai"
|
||||||
|
? {
|
||||||
|
custom_endpoint_path: "",
|
||||||
|
custom_auth_header: "x-msi-genai-api-key",
|
||||||
|
custom_auth_prefix: "",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
custom_endpoint_path: "/chat/completions",
|
||||||
|
custom_auth_header: "Authorization",
|
||||||
|
custom_auth_prefix: "Bearer ",
|
||||||
|
};
|
||||||
|
setForm({ ...form, api_format: format, ...defaults });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="openai">OpenAI Compatible</SelectItem>
|
||||||
|
<SelectItem value="msi_genai">MSI GenAI</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Select the API format. MSI GenAI uses a different request/response structure.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Endpoint Path</Label>
|
||||||
|
<Input
|
||||||
|
value={form.custom_endpoint_path ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm({ ...form, custom_endpoint_path: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="/chat/completions"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Path appended to API URL. Leave empty if URL includes full path.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Auth Header Name</Label>
|
||||||
|
<Input
|
||||||
|
value={form.custom_auth_header ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm({ ...form, custom_auth_header: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Authorization"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Header name for authentication (e.g., "Authorization" or "x-msi-genai-api-key")
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Auth Prefix</Label>
|
||||||
|
<Input
|
||||||
|
value={form.custom_auth_prefix ?? ""}
|
||||||
|
onChange={(e) => setForm({ ...form, custom_auth_prefix: e.target.value })}
|
||||||
|
placeholder="Bearer "
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Prefix added before API key (e.g., "Bearer " for OpenAI, empty for MSI GenAI)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Test result */}
|
{/* Test result */}
|
||||||
{testResult && (
|
{testResult && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user