provider detection

This commit is contained in:
Michael Neale
2025-11-11 16:04:15 +11:00
parent 6ce41cb21c
commit f21c71f72a
6 changed files with 228 additions and 9 deletions
+2
View File
@@ -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)
}
+40
View File
@@ -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": [
+8 -1
View File
@@ -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',
+21
View File
@@ -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;
+124 -8
View File
@@ -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 */}