add customised github copilot configuration (#8251)
This commit is contained in:
@@ -54,10 +54,43 @@ pub const GITHUB_COPILOT_STREAM_MODELS: &[&str] = &[
|
|||||||
|
|
||||||
const GITHUB_COPILOT_DOC_URL: &str =
|
const GITHUB_COPILOT_DOC_URL: &str =
|
||||||
"https://docs.github.com/en/copilot/using-github-copilot/ai-models";
|
"https://docs.github.com/en/copilot/using-github-copilot/ai-models";
|
||||||
const GITHUB_COPILOT_CLIENT_ID: &str = "Iv1.b507a08c87ecfe98";
|
const DEFAULT_GITHUB_HOST: &str = "github.com";
|
||||||
const GITHUB_COPILOT_DEVICE_CODE_URL: &str = "https://github.com/login/device/code";
|
const DEFAULT_GITHUB_COPILOT_CLIENT_ID: &str = "Iv1.b507a08c87ecfe98";
|
||||||
const GITHUB_COPILOT_ACCESS_TOKEN_URL: &str = "https://github.com/login/oauth/access_token";
|
|
||||||
const GITHUB_COPILOT_API_KEY_URL: &str = "https://api.github.com/copilot_internal/v2/token";
|
fn normalize_host(host: &str) -> String {
|
||||||
|
let host = host.trim_end_matches('/');
|
||||||
|
let host = host.strip_prefix("https://").unwrap_or(host);
|
||||||
|
host.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct GithubCopilotUrls {
|
||||||
|
device_code_url: String,
|
||||||
|
access_token_url: String,
|
||||||
|
copilot_token_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GithubCopilotUrls {
|
||||||
|
fn new(host: &str, copilot_token_url: Option<&str>) -> Self {
|
||||||
|
if host == "github.com" {
|
||||||
|
Self {
|
||||||
|
device_code_url: "https://github.com/login/device/code".to_string(),
|
||||||
|
access_token_url: "https://github.com/login/oauth/access_token".to_string(),
|
||||||
|
copilot_token_url: "https://api.github.com/copilot_internal/v2/token".to_string(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let base = format!("https://{}", host);
|
||||||
|
let copilot_token_url = copilot_token_url
|
||||||
|
.map(|u| u.trim_end_matches('/').to_string())
|
||||||
|
.unwrap_or_else(|| format!("{}/api/copilot_internal/v2/token", base));
|
||||||
|
Self {
|
||||||
|
device_code_url: format!("{}/login/device/code", base),
|
||||||
|
access_token_url: format!("{}/login/oauth/access_token", base),
|
||||||
|
copilot_token_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct DeviceCodeInfo {
|
struct DeviceCodeInfo {
|
||||||
@@ -96,8 +129,13 @@ struct DiskCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DiskCache {
|
impl DiskCache {
|
||||||
fn new() -> Self {
|
fn new(host: &str) -> Self {
|
||||||
let cache_path = Paths::in_config_dir("githubcopilot/info.json");
|
let cache_path = if host == DEFAULT_GITHUB_HOST {
|
||||||
|
Paths::in_config_dir("githubcopilot/info.json")
|
||||||
|
} else {
|
||||||
|
let safe_host = host.replace(['/', ':', '.'], "_");
|
||||||
|
Paths::in_config_dir(&format!("githubcopilot/{}/info.json", safe_host))
|
||||||
|
};
|
||||||
Self { cache_path }
|
Self { cache_path }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,12 +176,22 @@ pub struct GithubCopilotProvider {
|
|||||||
mu: tokio::sync::Mutex<RefCell<Option<CopilotState>>>,
|
mu: tokio::sync::Mutex<RefCell<Option<CopilotState>>>,
|
||||||
model: ModelConfig,
|
model: ModelConfig,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
|
urls: GithubCopilotUrls,
|
||||||
|
#[serde(skip)]
|
||||||
|
client_id: String,
|
||||||
|
#[serde(skip)]
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GithubCopilotProvider {
|
impl GithubCopilotProvider {
|
||||||
pub async fn cleanup() -> Result<()> {
|
pub async fn cleanup() -> Result<()> {
|
||||||
DiskCache::new().clear().await
|
let config = Config::global();
|
||||||
|
let host = normalize_host(
|
||||||
|
&config
|
||||||
|
.get_param::<String>("GITHUB_COPILOT_HOST")
|
||||||
|
.unwrap_or_else(|_| DEFAULT_GITHUB_HOST.to_string()),
|
||||||
|
);
|
||||||
|
DiskCache::new(&host).clear().await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn payload_contains_image(payload: &Value) -> bool {
|
fn payload_contains_image(payload: &Value) -> bool {
|
||||||
@@ -170,16 +218,29 @@ impl GithubCopilotProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn from_env(model: ModelConfig) -> Result<Self> {
|
pub async fn from_env(model: ModelConfig) -> Result<Self> {
|
||||||
|
let config = Config::global();
|
||||||
|
let host = normalize_host(
|
||||||
|
&config
|
||||||
|
.get_param::<String>("GITHUB_COPILOT_HOST")
|
||||||
|
.unwrap_or_else(|_| DEFAULT_GITHUB_HOST.to_string()),
|
||||||
|
);
|
||||||
|
let client_id: String = config
|
||||||
|
.get_param("GITHUB_COPILOT_CLIENT_ID")
|
||||||
|
.unwrap_or_else(|_| DEFAULT_GITHUB_COPILOT_CLIENT_ID.to_string());
|
||||||
|
let copilot_token_url: Option<String> = config.get_param("GITHUB_COPILOT_TOKEN_URL").ok();
|
||||||
|
let urls = GithubCopilotUrls::new(&host, copilot_token_url.as_deref());
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
.timeout(Duration::from_secs(600))
|
.timeout(Duration::from_secs(600))
|
||||||
.build()?;
|
.build()?;
|
||||||
let cache = DiskCache::new();
|
let cache = DiskCache::new(&host);
|
||||||
let mu = tokio::sync::Mutex::new(RefCell::new(None));
|
let mu = tokio::sync::Mutex::new(RefCell::new(None));
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
client,
|
client,
|
||||||
cache,
|
cache,
|
||||||
mu,
|
mu,
|
||||||
model,
|
model,
|
||||||
|
urls,
|
||||||
|
client_id,
|
||||||
name: GITHUB_COPILOT_PROVIDER_NAME.to_string(),
|
name: GITHUB_COPILOT_PROVIDER_NAME.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -258,7 +319,7 @@ impl GithubCopilotProvider {
|
|||||||
};
|
};
|
||||||
let resp = self
|
let resp = self
|
||||||
.client
|
.client
|
||||||
.get(GITHUB_COPILOT_API_KEY_URL)
|
.get(&self.urls.copilot_token_url)
|
||||||
.headers(self.get_github_headers())
|
.headers(self.get_github_headers())
|
||||||
.header(http::header::AUTHORIZATION, format!("bearer {}", &token))
|
.header(http::header::AUTHORIZATION, format!("bearer {}", &token))
|
||||||
.send()
|
.send()
|
||||||
@@ -311,10 +372,10 @@ impl GithubCopilotProvider {
|
|||||||
scope: String,
|
scope: String,
|
||||||
}
|
}
|
||||||
self.client
|
self.client
|
||||||
.post(GITHUB_COPILOT_DEVICE_CODE_URL)
|
.post(&self.urls.device_code_url)
|
||||||
.headers(self.get_github_headers())
|
.headers(self.get_github_headers())
|
||||||
.json(&DeviceCodeRequest {
|
.json(&DeviceCodeRequest {
|
||||||
client_id: GITHUB_COPILOT_CLIENT_ID.to_string(),
|
client_id: self.client_id.clone(),
|
||||||
scope: "read:user".to_string(),
|
scope: "read:user".to_string(),
|
||||||
})
|
})
|
||||||
.send()
|
.send()
|
||||||
@@ -346,10 +407,10 @@ impl GithubCopilotProvider {
|
|||||||
for attempt in 0..MAX_ATTEMPTS {
|
for attempt in 0..MAX_ATTEMPTS {
|
||||||
let resp = self
|
let resp = self
|
||||||
.client
|
.client
|
||||||
.post(GITHUB_COPILOT_ACCESS_TOKEN_URL)
|
.post(&self.urls.access_token_url)
|
||||||
.headers(self.get_github_headers())
|
.headers(self.get_github_headers())
|
||||||
.json(&AccessTokenRequest {
|
.json(&AccessTokenRequest {
|
||||||
client_id: GITHUB_COPILOT_CLIENT_ID.to_string(),
|
client_id: self.client_id.clone(),
|
||||||
device_code: device_code.to_string(),
|
device_code: device_code.to_string(),
|
||||||
grant_type: "urn:ietf:params:oauth:grant-type:device_code".to_string(),
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code".to_string(),
|
||||||
})
|
})
|
||||||
@@ -412,13 +473,12 @@ impl ProviderDef for GithubCopilotProvider {
|
|||||||
GITHUB_COPILOT_DEFAULT_MODEL,
|
GITHUB_COPILOT_DEFAULT_MODEL,
|
||||||
GITHUB_COPILOT_KNOWN_MODELS.to_vec(),
|
GITHUB_COPILOT_KNOWN_MODELS.to_vec(),
|
||||||
GITHUB_COPILOT_DOC_URL,
|
GITHUB_COPILOT_DOC_URL,
|
||||||
vec![ConfigKey::new_oauth_device_code(
|
vec![
|
||||||
"GITHUB_COPILOT_TOKEN",
|
ConfigKey::new_oauth_device_code("GITHUB_COPILOT_TOKEN", true, true, None, false),
|
||||||
true,
|
ConfigKey::new("GITHUB_COPILOT_HOST", false, false, None, false),
|
||||||
true,
|
ConfigKey::new("GITHUB_COPILOT_CLIENT_ID", false, false, None, false),
|
||||||
None,
|
ConfigKey::new("GITHUB_COPILOT_TOKEN_URL", false, false, None, false),
|
||||||
false,
|
],
|
||||||
)],
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,7 +693,7 @@ fn promote_tool_choice(response: Value) -> Value {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::promote_tool_choice;
|
use super::{normalize_host, promote_tool_choice, GithubCopilotUrls};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -677,4 +737,59 @@ mod tests {
|
|||||||
let promoted = promote_tool_choice(response.clone());
|
let promoted = promote_tool_choice(response.clone());
|
||||||
assert_eq!(promoted, response);
|
assert_eq!(promoted, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_host_strips_prefix_and_slash() {
|
||||||
|
assert_eq!(normalize_host("github.com"), "github.com");
|
||||||
|
assert_eq!(normalize_host("https://github.com"), "github.com");
|
||||||
|
assert_eq!(normalize_host("github.com/"), "github.com");
|
||||||
|
assert_eq!(normalize_host("https://github.com/"), "github.com");
|
||||||
|
assert_eq!(
|
||||||
|
normalize_host("https://my-enterprise.ghe.com/"),
|
||||||
|
"my-enterprise.ghe.com"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn urls_default_github_com() {
|
||||||
|
let urls = GithubCopilotUrls::new("github.com", None);
|
||||||
|
assert_eq!(urls.device_code_url, "https://github.com/login/device/code");
|
||||||
|
assert_eq!(
|
||||||
|
urls.access_token_url,
|
||||||
|
"https://github.com/login/oauth/access_token"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
urls.copilot_token_url,
|
||||||
|
"https://api.github.com/copilot_internal/v2/token"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn urls_enterprise_host() {
|
||||||
|
let urls = GithubCopilotUrls::new("my-enterprise.ghe.com", None);
|
||||||
|
assert_eq!(
|
||||||
|
urls.device_code_url,
|
||||||
|
"https://my-enterprise.ghe.com/login/device/code"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
urls.access_token_url,
|
||||||
|
"https://my-enterprise.ghe.com/login/oauth/access_token"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
urls.copilot_token_url,
|
||||||
|
"https://my-enterprise.ghe.com/api/copilot_internal/v2/token"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn urls_enterprise_with_token_url_override() {
|
||||||
|
let urls = GithubCopilotUrls::new(
|
||||||
|
"my-enterprise.ghe.com",
|
||||||
|
Some("https://my-enterprise.ghe.com/api/v3/copilot_internal/v2/token"),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
urls.copilot_token_url,
|
||||||
|
"https://my-enterprise.ghe.com/api/v3/copilot_internal/v2/token"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,8 +172,10 @@ export default function ProviderConfigurationModal({
|
|||||||
primaryParameters = provider.metadata.config_keys;
|
primaryParameters = provider.metadata.config_keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this provider uses OAuth for configuration
|
const configKeys = provider.metadata.config_keys.filter((key) => !key.oauth_flow);
|
||||||
const isOAuthProvider = provider.metadata.config_keys.some((key) => key.oauth_flow);
|
const hasOAuth = provider.metadata.config_keys.some((key) => key.oauth_flow);
|
||||||
|
const hasConfig = configKeys.length > 0;
|
||||||
|
const hasDeviceCodeFlow = provider.metadata.config_keys.some((key) => key.device_code_flow);
|
||||||
|
|
||||||
const isConfigured = provider.is_configured;
|
const isConfigured = provider.is_configured;
|
||||||
const headerText = showDeleteConfirmation
|
const headerText = showDeleteConfirmation
|
||||||
@@ -189,8 +191,10 @@ export default function ProviderConfigurationModal({
|
|||||||
? isActiveProvider
|
? isActiveProvider
|
||||||
? intl.formatMessage(i18n.cannotDeleteActive)
|
? intl.formatMessage(i18n.cannotDeleteActive)
|
||||||
: intl.formatMessage(i18n.deleteConfirmation)
|
: intl.formatMessage(i18n.deleteConfirmation)
|
||||||
: isOAuthProvider
|
: hasOAuth
|
||||||
? intl.formatMessage(i18n.oauthSignInDescription, { providerName: provider.metadata.display_name })
|
? intl.formatMessage(i18n.oauthSignInDescription, {
|
||||||
|
providerName: provider.metadata.display_name,
|
||||||
|
})
|
||||||
: isExternalSetup
|
: isExternalSetup
|
||||||
? provider.metadata.description
|
? provider.metadata.description
|
||||||
: intl.formatMessage(i18n.addApiKeyDescription);
|
: intl.formatMessage(i18n.addApiKeyDescription);
|
||||||
@@ -199,6 +203,16 @@ export default function ProviderConfigurationModal({
|
|||||||
setIsOAuthLoading(true);
|
setIsOAuthLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
|
if (hasConfig) {
|
||||||
|
for (const key of configKeys) {
|
||||||
|
const entry = configValues[key.name];
|
||||||
|
const value =
|
||||||
|
entry?.value ?? (typeof entry?.serverValue === 'string' ? entry.serverValue : null);
|
||||||
|
if (value) {
|
||||||
|
await upsert(key.name, value, key.secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
await configureProviderOauth({
|
await configureProviderOauth({
|
||||||
path: { name: provider.name },
|
path: { name: provider.name },
|
||||||
});
|
});
|
||||||
@@ -228,7 +242,9 @@ export default function ProviderConfigurationModal({
|
|||||||
!configValues[parameter.name]?.value &&
|
!configValues[parameter.name]?.value &&
|
||||||
!configValues[parameter.name]?.serverValue
|
!configValues[parameter.name]?.serverValue
|
||||||
) {
|
) {
|
||||||
errors[parameter.name] = intl.formatMessage(i18n.parameterRequired, { paramName: parameter.name });
|
errors[parameter.name] = intl.formatMessage(i18n.parameterRequired, {
|
||||||
|
paramName: parameter.name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -331,7 +347,9 @@ export default function ProviderConfigurationModal({
|
|||||||
<>
|
<>
|
||||||
<Dialog open={!!error} onOpenChange={(open) => !open && setError(null)}>
|
<Dialog open={!!error} onOpenChange={(open) => !open && setError(null)}>
|
||||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
|
||||||
<DialogTitle className="flex items-center gap-2">{intl.formatMessage(i18n.errorTitle)}</DialogTitle>
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
{intl.formatMessage(i18n.errorTitle)}
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription className="text-inherit text-base">
|
<DialogDescription className="text-inherit text-base">
|
||||||
{intl.formatMessage(i18n.errorCheckingConfig)}
|
{intl.formatMessage(i18n.errorCheckingConfig)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
@@ -357,78 +375,97 @@ export default function ProviderConfigurationModal({
|
|||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
{/* Contains information used to set up each provider */}
|
{/* Contains information used to set up each provider */}
|
||||||
{/* Only show the form when NOT in delete confirmation mode */}
|
{/* Only show the form when NOT in delete confirmation mode */}
|
||||||
{!showDeleteConfirmation ? (
|
{!showDeleteConfirmation && (
|
||||||
isOAuthProvider ? (
|
<>
|
||||||
<div className="flex flex-col items-center gap-4 py-6">
|
{hasOAuth && (
|
||||||
<Button
|
<div className="flex flex-col items-center gap-4 py-6">
|
||||||
onClick={handleOAuthLogin}
|
<Button
|
||||||
disabled={isOAuthLoading}
|
onClick={handleOAuthLogin}
|
||||||
className="flex items-center gap-2 px-6 py-3"
|
disabled={isOAuthLoading}
|
||||||
size="lg"
|
className="flex items-center gap-2 px-6 py-3"
|
||||||
>
|
size="lg"
|
||||||
<LogIn size={20} />
|
>
|
||||||
{isOAuthLoading
|
<LogIn size={20} />
|
||||||
? intl.formatMessage(i18n.signingIn)
|
{isOAuthLoading
|
||||||
: intl.formatMessage(i18n.signInWith, { providerName: provider.metadata.display_name })}
|
? intl.formatMessage(i18n.signingIn)
|
||||||
</Button>
|
: intl.formatMessage(i18n.signInWith, {
|
||||||
<p className="text-sm text-text-secondary text-center">
|
providerName: provider.metadata.display_name,
|
||||||
{provider.metadata.config_keys.some((key) => key.device_code_flow)
|
})}
|
||||||
? intl.formatMessage(i18n.deviceCodeFlowHint)
|
</Button>
|
||||||
: intl.formatMessage(i18n.browserWindowHint)}
|
<p className="text-sm text-text-secondary text-center">
|
||||||
</p>
|
{hasDeviceCodeFlow
|
||||||
</div>
|
? intl.formatMessage(i18n.deviceCodeFlowHint)
|
||||||
) : provider.metadata.config_keys.length === 0 &&
|
: intl.formatMessage(i18n.browserWindowHint)}
|
||||||
provider.metadata.setup_steps &&
|
|
||||||
provider.metadata.setup_steps.length > 0 ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className="text-sm text-text-secondary">
|
|
||||||
{intl.formatMessage(i18n.externalSetupIntro)}
|
|
||||||
</p>
|
|
||||||
<ol className="ml-5 list-decimal text-sm text-text-primary space-y-2">
|
|
||||||
{provider.metadata.setup_steps.map((step, i) => (
|
|
||||||
<li key={i}>{renderSetupStep(step)}</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
{provider.metadata.model_doc_link && (
|
|
||||||
<p className="text-sm text-text-secondary mt-4">
|
|
||||||
{intl.formatMessage(i18n.seeDocumentation, {
|
|
||||||
link: (chunks: React.ReactNode) => (
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
window.electron.openExternal(provider.metadata.model_doc_link);
|
|
||||||
}}
|
|
||||||
className="underline hover:text-text-primary"
|
|
||||||
>
|
|
||||||
{chunks}
|
|
||||||
</a>
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
) : (
|
|
||||||
<>
|
{hasOAuth && hasConfig && (
|
||||||
{/* Contains information used to set up each provider */}
|
|
||||||
<DefaultProviderSetupForm
|
<DefaultProviderSetupForm
|
||||||
configValues={configValues}
|
configValues={configValues}
|
||||||
setConfigValues={setConfigValues}
|
setConfigValues={setConfigValues}
|
||||||
provider={provider}
|
provider={{
|
||||||
|
...provider,
|
||||||
|
metadata: {
|
||||||
|
...provider.metadata,
|
||||||
|
config_keys: configKeys,
|
||||||
|
},
|
||||||
|
}}
|
||||||
validationErrors={validationErrors}
|
validationErrors={validationErrors}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{primaryParameters.length > 0 &&
|
{isExternalSetup && (
|
||||||
provider.metadata.config_keys &&
|
<div className="space-y-3">
|
||||||
provider.metadata.config_keys.length > 0 && <SecureStorageNotice />}
|
<p className="text-sm text-text-secondary">
|
||||||
</>
|
{intl.formatMessage(i18n.externalSetupIntro)}
|
||||||
)
|
</p>
|
||||||
) : null}
|
<ol className="ml-5 list-decimal text-sm text-text-primary space-y-2">
|
||||||
|
{provider.metadata.setup_steps?.map((step, i) => (
|
||||||
|
<li key={i}>{renderSetupStep(step)}</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
{provider.metadata.model_doc_link && (
|
||||||
|
<p className="text-sm text-text-secondary mt-4">
|
||||||
|
{intl.formatMessage(i18n.seeDocumentation, {
|
||||||
|
link: (chunks: React.ReactNode) => (
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
window.electron.openExternal(provider.metadata.model_doc_link);
|
||||||
|
}}
|
||||||
|
className="underline hover:text-text-primary"
|
||||||
|
>
|
||||||
|
{chunks}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!hasOAuth && !isExternalSetup && (
|
||||||
|
<>
|
||||||
|
<DefaultProviderSetupForm
|
||||||
|
configValues={configValues}
|
||||||
|
setConfigValues={setConfigValues}
|
||||||
|
provider={provider}
|
||||||
|
validationErrors={validationErrors}
|
||||||
|
/>
|
||||||
|
{primaryParameters.length > 0 && provider.metadata.config_keys.length > 0 && (
|
||||||
|
<SecureStorageNotice />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
{isOAuthProvider && !showDeleteConfirmation ? (
|
{hasOAuth && !showDeleteConfirmation ? (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 justify-end">
|
||||||
<Button variant="outline" onClick={handleCancel}>
|
<Button variant="outline" onClick={handleCancel}>
|
||||||
{intl.formatMessage(i18n.cancel)}
|
{intl.formatMessage(i18n.cancel)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -438,10 +475,7 @@ export default function ProviderConfigurationModal({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : provider.metadata.config_keys.length === 0 &&
|
) : isExternalSetup && !showDeleteConfirmation ? (
|
||||||
provider.metadata.setup_steps &&
|
|
||||||
provider.metadata.setup_steps.length > 0 &&
|
|
||||||
!showDeleteConfirmation ? (
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
+7
-2
@@ -3,6 +3,7 @@ import { Input } from '../../../../../ui/input';
|
|||||||
import { useConfig } from '../../../../../ConfigContext';
|
import { useConfig } from '../../../../../ConfigContext';
|
||||||
import { ProviderDetails, ConfigKey } from '../../../../../../api';
|
import { ProviderDetails, ConfigKey } from '../../../../../../api';
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../../../../ui/collapsible';
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../../../../ui/collapsible';
|
||||||
|
import { configLabels, configPlaceholders } from '../../../../../../utils/configUtils';
|
||||||
import { defineMessages, useIntl } from '../../../../../../i18n';
|
import { defineMessages, useIntl } from '../../../../../../i18n';
|
||||||
|
|
||||||
const i18n = defineMessages({
|
const i18n = defineMessages({
|
||||||
@@ -138,9 +139,11 @@ export default function DefaultProviderSetupForm({
|
|||||||
return parameter.default;
|
return parameter.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (configPlaceholders[parameter.name]) return configPlaceholders[parameter.name];
|
||||||
const name = parameter.name.toLowerCase();
|
const name = parameter.name.toLowerCase();
|
||||||
if (name.includes('api_key')) return intl.formatMessage(i18n.apiKeyPlaceholder);
|
if (name.includes('api_key')) return intl.formatMessage(i18n.apiKeyPlaceholder);
|
||||||
if (name.includes('api_url') || name.includes('host')) return intl.formatMessage(i18n.apiHostPlaceholder);
|
if (name.includes('api_url') || name.includes('host'))
|
||||||
|
return intl.formatMessage(i18n.apiHostPlaceholder);
|
||||||
if (name.includes('models')) return intl.formatMessage(i18n.modelsPlaceholder);
|
if (name.includes('models')) return intl.formatMessage(i18n.modelsPlaceholder);
|
||||||
|
|
||||||
return parameter.name
|
return parameter.name
|
||||||
@@ -150,9 +153,11 @@ export default function DefaultProviderSetupForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getFieldLabel = (parameter: ConfigKey) => {
|
const getFieldLabel = (parameter: ConfigKey) => {
|
||||||
|
if (configLabels[parameter.name]) return configLabels[parameter.name];
|
||||||
const name = parameter.name.toLowerCase();
|
const name = parameter.name.toLowerCase();
|
||||||
if (name.includes('api_key')) return intl.formatMessage(i18n.apiKeyLabel);
|
if (name.includes('api_key')) return intl.formatMessage(i18n.apiKeyLabel);
|
||||||
if (name.includes('api_url') || name.includes('host')) return intl.formatMessage(i18n.apiHostLabel);
|
if (name.includes('api_url') || name.includes('host'))
|
||||||
|
return intl.formatMessage(i18n.apiHostLabel);
|
||||||
if (name.includes('models')) return intl.formatMessage(i18n.modelsLabel);
|
if (name.includes('models')) return intl.formatMessage(i18n.modelsLabel);
|
||||||
|
|
||||||
let parameter_name = parameter.name.toUpperCase();
|
let parameter_name = parameter.name.toUpperCase();
|
||||||
|
|||||||
@@ -57,6 +57,17 @@ export const configLabels: Record<string, string> = {
|
|||||||
// snowflake
|
// snowflake
|
||||||
SNOWFLAKE_HOST: 'Snowflake Host',
|
SNOWFLAKE_HOST: 'Snowflake Host',
|
||||||
SNOWFLAKE_TOKEN: 'Snowflake Token',
|
SNOWFLAKE_TOKEN: 'Snowflake Token',
|
||||||
|
|
||||||
|
// github copilot
|
||||||
|
GITHUB_COPILOT_HOST: 'Custom GitHub Host',
|
||||||
|
GITHUB_COPILOT_CLIENT_ID: 'Custom GitHub OAuth Client ID',
|
||||||
|
GITHUB_COPILOT_TOKEN_URL: 'Custom GitHub Copilot Token URL',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const configPlaceholders: Record<string, string> = {
|
||||||
|
GITHUB_COPILOT_HOST: 'my-enterprise.ghe.com',
|
||||||
|
GITHUB_COPILOT_CLIENT_ID: 'Iv1.xxxxxxxxxxxxxxxx',
|
||||||
|
GITHUB_COPILOT_TOKEN_URL: 'https://my-enterprise.ghe.com/api/copilot_internal/v2/token',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const providerPrefixes: Record<string, string[]> = {
|
export const providerPrefixes: Record<string, string[]> = {
|
||||||
@@ -70,6 +81,7 @@ export const providerPrefixes: Record<string, string[]> = {
|
|||||||
azure_openai: ['AZURE_'],
|
azure_openai: ['AZURE_'],
|
||||||
gcp_vertex_ai: ['GCP_'],
|
gcp_vertex_ai: ['GCP_'],
|
||||||
snowflake: ['SNOWFLAKE_'],
|
snowflake: ['SNOWFLAKE_'],
|
||||||
|
github_copilot: ['GITHUB_COPILOT_'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUiNames = (key: string): string => {
|
export const getUiNames = (key: string): string => {
|
||||||
|
|||||||
Reference in New Issue
Block a user