feat(quotio): add auto-detection of base URL and API key

- Check QUOTIO_BASE_URL/QUOTIO_API_KEY environment variables
- Probe default endpoint (127.0.0.1:18317) automatically
- Skip prompts if Quotio is detected and user confirms
- Fall back to manual configuration if auto-detection fails
- Add helpful tip about environment variables for future runs
This commit is contained in:
newideas99 2026-01-26 23:23:19 -05:00
parent 92c771e107
commit 12525eb4b8

View File

@ -7,6 +7,7 @@ import { applyAuthProfileConfig } from "./onboard-auth.js";
const QUOTIO_DEFAULT_BASE_URL = "http://127.0.0.1:18317/v1";
const QUOTIO_DEFAULT_API_KEY = "quotio-local";
const QUOTIO_PROBE_TIMEOUT_MS = 3000;
type QuotioModel = {
id: string;
@ -20,6 +21,86 @@ type QuotioModelsResponse = {
data: QuotioModel[];
};
type QuotioDetectionResult = {
baseUrl: string;
apiKey: string;
models: QuotioModel[];
autoDetected: boolean;
};
function getEnvQuotioConfig(): { baseUrl?: string; apiKey?: string } {
return {
baseUrl: process.env.QUOTIO_BASE_URL || process.env.QUOTIO_URL,
apiKey: process.env.QUOTIO_API_KEY || process.env.QUOTIO_KEY,
};
}
async function probeQuotioEndpoint(
baseUrl: string,
apiKey: string,
timeoutMs: number = QUOTIO_PROBE_TIMEOUT_MS,
): Promise<{ ok: boolean; models: QuotioModel[] }> {
try {
const modelsUrl = baseUrl.endsWith("/") ? `${baseUrl}models` : `${baseUrl}/models`;
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (apiKey) {
headers["Authorization"] = `Bearer ${apiKey}`;
}
const response = await fetch(modelsUrl, {
headers,
signal: AbortSignal.timeout(timeoutMs),
});
if (!response.ok) {
return { ok: false, models: [] };
}
const data = (await response.json()) as QuotioModelsResponse;
if (!data.data || !Array.isArray(data.data) || data.data.length === 0) {
return { ok: false, models: [] };
}
return { ok: true, models: data.data };
} catch {
return { ok: false, models: [] };
}
}
async function autoDetectQuotio(): Promise<QuotioDetectionResult | null> {
const env = getEnvQuotioConfig();
const baseUrl = env.baseUrl || QUOTIO_DEFAULT_BASE_URL;
const apiKey = env.apiKey || QUOTIO_DEFAULT_API_KEY;
const result = await probeQuotioEndpoint(baseUrl, apiKey);
if (result.ok) {
return {
baseUrl,
apiKey,
models: result.models,
autoDetected: true,
};
}
if (env.baseUrl || env.apiKey) {
const defaultResult = await probeQuotioEndpoint(
QUOTIO_DEFAULT_BASE_URL,
QUOTIO_DEFAULT_API_KEY,
);
if (defaultResult.ok) {
return {
baseUrl: QUOTIO_DEFAULT_BASE_URL,
apiKey: QUOTIO_DEFAULT_API_KEY,
models: defaultResult.models,
autoDetected: true,
};
}
}
return null;
}
async function discoverQuotioModels(
baseUrl: string,
apiKey: string,
@ -112,15 +193,111 @@ export async function applyAuthChoiceQuotio(
let nextConfig = params.config;
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
await params.prompter.note(
[
"Quotio is a local OpenAI-compatible proxy that routes to various AI models.",
"Make sure Quotio is running before continuing.",
"Default endpoint: http://127.0.0.1:18317/v1",
].join("\n"),
"Quotio",
);
await params.prompter.note("Detecting Quotio...", "Auto-detection");
const detected = await autoDetectQuotio();
let finalBaseUrl: string;
let finalApiKey: string;
let discoveredModels: QuotioModel[];
if (detected) {
await params.prompter.note(
`Found Quotio at ${detected.baseUrl} with ${detected.models.length} model(s).`,
"Auto-detected",
);
const useDetected = await params.prompter.confirm({
message: `Use detected configuration? (${detected.baseUrl})`,
initialValue: true,
});
if (useDetected) {
finalBaseUrl = detected.baseUrl;
finalApiKey = detected.apiKey;
discoveredModels = detected.models;
} else {
const manualConfig = await promptManualConfig(params);
if (!manualConfig) return { config: params.config };
finalBaseUrl = manualConfig.baseUrl;
finalApiKey = manualConfig.apiKey;
discoveredModels = manualConfig.models;
}
} else {
await params.prompter.note(
[
"Could not auto-detect Quotio.",
"Make sure Quotio is running, or configure manually.",
"Tip: Set QUOTIO_BASE_URL and QUOTIO_API_KEY environment variables for auto-detection.",
].join("\n"),
"Not detected",
);
const manualConfig = await promptManualConfig(params);
if (!manualConfig) return { config: params.config };
finalBaseUrl = manualConfig.baseUrl;
finalApiKey = manualConfig.apiKey;
discoveredModels = manualConfig.models;
}
if (discoveredModels.length === 0) {
await params.prompter.note(
"No models found. Please check your Quotio configuration.",
"Setup Failed",
);
return { config: params.config };
}
const modelOptions = discoveredModels.map((m) => ({
value: m.id,
label: m.id,
hint: m.owned_by ? `by ${m.owned_by}` : undefined,
}));
const selectedModelId = await params.prompter.select({
message: "Select default model",
options: modelOptions,
});
const modelDefinitions = discoveredModels.map(buildModelDefinition);
const defaultModelRef = `quotio/${String(selectedModelId)}`;
upsertAuthProfile({
profileId: "quotio:default",
credential: {
type: "api_key",
provider: "quotio",
key: finalApiKey,
},
agentDir,
});
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "quotio:default",
provider: "quotio",
mode: "api_key",
});
nextConfig = applyQuotioProviderConfig(nextConfig, finalBaseUrl, finalApiKey, modelDefinitions);
let agentModelOverride: string | undefined;
if (params.setDefaultModel) {
nextConfig = applyQuotioDefaultModel(nextConfig, defaultModelRef);
await params.prompter.note(`Default model set to ${defaultModelRef}`, "Model configured");
} else if (params.agentId) {
agentModelOverride = defaultModelRef;
await params.prompter.note(
`Default model set to ${defaultModelRef} for agent "${params.agentId}".`,
"Model configured",
);
}
return { config: nextConfig, agentModelOverride };
}
async function promptManualConfig(
params: ApplyAuthChoiceParams,
): Promise<{ baseUrl: string; apiKey: string; models: QuotioModel[] } | null> {
const baseUrl = await params.prompter.text({
message: "Enter Quotio base URL",
initialValue: QUOTIO_DEFAULT_BASE_URL,
@ -145,74 +322,15 @@ export async function applyAuthChoiceQuotio(
await params.prompter.note("Discovering available models from Quotio...", "Connecting");
const { models: discoveredModels, error } = await discoverQuotioModels(
normalizedBaseUrl,
normalizedApiKey,
);
const { models, error } = await discoverQuotioModels(normalizedBaseUrl, normalizedApiKey);
if (error || discoveredModels.length === 0) {
if (error) {
await params.prompter.note(
error
? `Could not fetch models: ${error}\nPlease ensure Quotio is running and try again.`
: "No models found. Please check your Quotio configuration.",
`Could not fetch models: ${error}\nPlease ensure Quotio is running and try again.`,
"Discovery Failed",
);
return { config: params.config };
return null;
}
await params.prompter.note(
`Found ${discoveredModels.length} model(s) available.`,
"Discovery Complete",
);
const modelOptions = discoveredModels.map((m) => ({
value: m.id,
label: m.id,
hint: m.owned_by ? `by ${m.owned_by}` : undefined,
}));
const selectedModelId = await params.prompter.select({
message: "Select default model",
options: modelOptions,
});
const modelDefinitions = discoveredModels.map(buildModelDefinition);
const defaultModelRef = `quotio/${String(selectedModelId)}`;
upsertAuthProfile({
profileId: "quotio:default",
credential: {
type: "api_key",
provider: "quotio",
key: normalizedApiKey,
},
agentDir,
});
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "quotio:default",
provider: "quotio",
mode: "api_key",
});
nextConfig = applyQuotioProviderConfig(
nextConfig,
normalizedBaseUrl,
normalizedApiKey,
modelDefinitions,
);
let agentModelOverride: string | undefined;
if (params.setDefaultModel) {
nextConfig = applyQuotioDefaultModel(nextConfig, defaultModelRef);
await params.prompter.note(`Default model set to ${defaultModelRef}`, "Model configured");
} else if (params.agentId) {
agentModelOverride = defaultModelRef;
await params.prompter.note(
`Default model set to ${defaultModelRef} for agent "${params.agentId}".`,
"Model configured",
);
}
return { config: nextConfig, agentModelOverride };
return { baseUrl: normalizedBaseUrl, apiKey: normalizedApiKey, models };
}