mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
provider detection
This commit is contained in:
@@ -346,6 +346,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::config_management::remove_custom_provider,
|
||||
super::routes::config_management::check_provider,
|
||||
super::routes::config_management::set_config_provider,
|
||||
super::routes::config_management::detect_api_keys,
|
||||
super::routes::agent::start_agent,
|
||||
super::routes::agent::resume_agent,
|
||||
super::routes::agent::get_tools,
|
||||
@@ -398,6 +399,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::config_management::UpdateCustomProviderRequest,
|
||||
super::routes::config_management::CheckProviderRequest,
|
||||
super::routes::config_management::SetProviderRequest,
|
||||
super::routes::config_management::DetectedApiKey,
|
||||
super::routes::reply::PermissionConfirmationRequest,
|
||||
super::routes::reply::ChatRequest,
|
||||
super::routes::session::ImportSessionRequest,
|
||||
|
||||
@@ -781,6 +781,38 @@ pub async fn set_config_provider(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct DetectedApiKey {
|
||||
pub provider: String,
|
||||
pub env_var: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/config/detect_api_keys",
|
||||
responses(
|
||||
(status = 200, description = "API key detection result", body = Option<DetectedApiKey>),
|
||||
)
|
||||
)]
|
||||
pub async fn detect_api_keys() -> Result<Json<Option<DetectedApiKey>>, StatusCode> {
|
||||
let keys_to_check = [
|
||||
("anthropic", "ANTHROPIC_API_KEY"),
|
||||
("openai", "OPENAI_API_KEY"),
|
||||
("openrouter", "OPENROUTER_API_KEY"),
|
||||
];
|
||||
|
||||
for (provider, env_var) in keys_to_check.iter() {
|
||||
if std::env::var(env_var).is_ok() {
|
||||
return Ok(Json(Some(DetectedApiKey {
|
||||
provider: provider.to_string(),
|
||||
env_var: env_var.to_string(),
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(None))
|
||||
}
|
||||
|
||||
pub fn routes(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/config", get(read_all_config))
|
||||
@@ -807,6 +839,7 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
.route("/config/custom-providers/{id}", get(get_custom_provider))
|
||||
.route("/config/check_provider", post(check_provider))
|
||||
.route("/config/set_provider", post(set_config_provider))
|
||||
.route("/config/detect_api_keys", get(detect_api_keys))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
|
||||
@@ -551,6 +551,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/detect_api_keys": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"super::routes::config_management"
|
||||
],
|
||||
"operationId": "detect_api_keys",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "API key detection result",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/DetectedApiKey"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/extensions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -2448,6 +2473,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"DetectedApiKey": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"provider",
|
||||
"env_var"
|
||||
],
|
||||
"properties": {
|
||||
"env_var": {
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmbeddedResource": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { Client, Options as Options2, TDataShape } from './client';
|
||||
import { client } from './client.gen';
|
||||
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CheckProviderData, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
|
||||
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CheckProviderData, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectApiKeysData, DetectApiKeysResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||
/**
|
||||
@@ -163,6 +163,13 @@ export const updateCustomProvider = <ThrowOnError extends boolean = false>(optio
|
||||
});
|
||||
};
|
||||
|
||||
export const detectApiKeys = <ThrowOnError extends boolean = false>(options?: Options<DetectApiKeysData, ThrowOnError>) => {
|
||||
return (options?.client ?? client).get<DetectApiKeysResponses, unknown, ThrowOnError>({
|
||||
url: '/config/detect_api_keys',
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const getExtensions = <ThrowOnError extends boolean = false>(options?: Options<GetExtensionsData, ThrowOnError>) => {
|
||||
return (options?.client ?? client).get<GetExtensionsResponses, GetExtensionsErrors, ThrowOnError>({
|
||||
url: '/config/extensions',
|
||||
|
||||
@@ -121,6 +121,11 @@ export type DeleteRecipeRequest = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type DetectedApiKey = {
|
||||
env_var: string;
|
||||
provider: string;
|
||||
};
|
||||
|
||||
export type EmbeddedResource = {
|
||||
_meta?: {
|
||||
[key: string]: unknown;
|
||||
@@ -1341,6 +1346,22 @@ export type UpdateCustomProviderResponses = {
|
||||
|
||||
export type UpdateCustomProviderResponse = UpdateCustomProviderResponses[keyof UpdateCustomProviderResponses];
|
||||
|
||||
export type DetectApiKeysData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/config/detect_api_keys';
|
||||
};
|
||||
|
||||
export type DetectApiKeysResponses = {
|
||||
/**
|
||||
* API key detection result
|
||||
*/
|
||||
200: DetectedApiKey | null;
|
||||
};
|
||||
|
||||
export type DetectApiKeysResponse = DetectApiKeysResponses[keyof DetectApiKeysResponses];
|
||||
|
||||
export type GetExtensionsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { OllamaSetup } from './OllamaSetup';
|
||||
|
||||
import { Goose } from './icons/Goose';
|
||||
import { OpenRouter } from './icons';
|
||||
import { detectApiKeys, providers, setConfigProvider } from '../api/sdk.gen';
|
||||
|
||||
interface ProviderGuardProps {
|
||||
didSelectProvider: boolean;
|
||||
@@ -23,6 +24,10 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
|
||||
const [hasProvider, setHasProvider] = useState(false);
|
||||
const [showFirstTimeSetup, setShowFirstTimeSetup] = useState(false);
|
||||
const [showOllamaSetup, setShowOllamaSetup] = useState(false);
|
||||
const [detectedApiKey, setDetectedApiKey] = useState<{
|
||||
provider: string;
|
||||
env_var: string;
|
||||
} | null>(null);
|
||||
|
||||
const [openRouterSetupState, setOpenRouterSetupState] = useState<{
|
||||
show: boolean;
|
||||
@@ -101,6 +106,50 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
|
||||
}
|
||||
};
|
||||
|
||||
const handleDetectedKeySetup = async () => {
|
||||
if (!detectedApiKey) return;
|
||||
|
||||
try {
|
||||
console.log(`Setting up with detected ${detectedApiKey.provider} API key`);
|
||||
|
||||
// Get provider metadata to find default model
|
||||
const providersResult = await providers();
|
||||
const providerDetails = providersResult.data?.find((p) => p.name === detectedApiKey.provider);
|
||||
|
||||
if (!providerDetails) {
|
||||
throw new Error(`Provider ${detectedApiKey.provider} not found`);
|
||||
}
|
||||
|
||||
const defaultModel = providerDetails.metadata.default_model;
|
||||
|
||||
// Set the provider and model
|
||||
await setConfigProvider({
|
||||
body: {
|
||||
provider: detectedApiKey.provider,
|
||||
model: defaultModel,
|
||||
},
|
||||
});
|
||||
|
||||
toastService.configure({ silent: false });
|
||||
toastService.success({
|
||||
title: 'Success!',
|
||||
msg: `Started goose with ${defaultModel} using your ${detectedApiKey.env_var}. You can change the model via the dropdown.`,
|
||||
});
|
||||
|
||||
setShowFirstTimeSetup(false);
|
||||
setHasProvider(true);
|
||||
navigate('/', { replace: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to setup with detected API key:', error);
|
||||
toastService.configure({ silent: false });
|
||||
toastService.error({
|
||||
title: 'Setup Failed',
|
||||
msg: `Failed to setup with detected API key: ${error instanceof Error ? error.message : String(error)}`,
|
||||
traceback: error instanceof Error ? error.stack || '' : '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenRouterSetup = async () => {
|
||||
setOpenRouterSetupState({
|
||||
show: true,
|
||||
@@ -173,17 +222,26 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
|
||||
const provider = (await read('GOOSE_PROVIDER', false)) ?? config.GOOSE_DEFAULT_PROVIDER;
|
||||
const model = (await read('GOOSE_MODEL', false)) ?? config.GOOSE_DEFAULT_MODEL;
|
||||
|
||||
// Always check for Ollama regardless of provider status
|
||||
|
||||
if (provider && model) {
|
||||
console.log('ProviderGuard - Provider and model found, continuing normally');
|
||||
setHasProvider(true);
|
||||
} else {
|
||||
console.log('ProviderGuard - No provider/model configured');
|
||||
|
||||
// Detect API keys in environment
|
||||
try {
|
||||
const result = await detectApiKeys();
|
||||
if (result.data) {
|
||||
console.log('ProviderGuard - Detected API key:', result.data);
|
||||
setDetectedApiKey(result.data);
|
||||
}
|
||||
} catch {
|
||||
console.log('ProviderGuard - No API keys detected in environment');
|
||||
}
|
||||
|
||||
setShowFirstTimeSetup(true);
|
||||
}
|
||||
} catch (error) {
|
||||
// On error, assume no provider and redirect to welcome
|
||||
console.error('Error checking provider configuration:', error);
|
||||
navigate('/welcome', { replace: true });
|
||||
} finally {
|
||||
@@ -192,11 +250,7 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
|
||||
};
|
||||
|
||||
checkProvider();
|
||||
}, [
|
||||
navigate,
|
||||
read,
|
||||
didSelectProvider, // When the user makes a selection, re-trigger this check
|
||||
]);
|
||||
}, [navigate, read, didSelectProvider]);
|
||||
|
||||
if (
|
||||
isChecking &&
|
||||
@@ -287,6 +341,68 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
|
||||
{/* Setup options - same width container */}
|
||||
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{/* Quick Setup Card - shown when API key is detected */}
|
||||
{detectedApiKey && (
|
||||
<div className="relative">
|
||||
<div className="absolute -top-2 -right-2 sm:-top-3 sm:-right-3 z-20">
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium bg-green-600 text-white rounded-full">
|
||||
Ready to Go!
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={handleDetectedKeySetup}
|
||||
className="relative w-full p-4 sm:p-6 bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 border-2 border-green-500 dark:border-green-600 rounded-xl hover:border-green-600 dark:hover:border-green-500 transition-all duration-200 cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<svg
|
||||
className="w-5 h-5 text-green-600 dark:text-green-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 className="font-semibold text-text-standard text-sm sm:text-base">
|
||||
Quick Setup with{' '}
|
||||
{detectedApiKey.provider.charAt(0).toUpperCase() +
|
||||
detectedApiKey.provider.slice(1)}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-green-600 dark:text-green-400 group-hover:text-green-700 dark:group-hover:text-green-300 transition-colors">
|
||||
<svg
|
||||
className="w-4 h-4 sm:w-5 sm:h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-text-standard text-sm sm:text-base font-medium mb-1">
|
||||
We detected your {detectedApiKey.env_var} environment variable!
|
||||
</p>
|
||||
<p className="text-text-muted text-sm sm:text-base">
|
||||
Click here to start using Goose immediately with your existing API key.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
{/* Tetrate Card */}
|
||||
{/* Recommended badge - positioned relative to wrapper */}
|
||||
|
||||
Reference in New Issue
Block a user