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:
parent
92c771e107
commit
12525eb4b8
@ -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_BASE_URL = "http://127.0.0.1:18317/v1";
|
||||||
const QUOTIO_DEFAULT_API_KEY = "quotio-local";
|
const QUOTIO_DEFAULT_API_KEY = "quotio-local";
|
||||||
|
const QUOTIO_PROBE_TIMEOUT_MS = 3000;
|
||||||
|
|
||||||
type QuotioModel = {
|
type QuotioModel = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -20,6 +21,86 @@ type QuotioModelsResponse = {
|
|||||||
data: QuotioModel[];
|
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(
|
async function discoverQuotioModels(
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
@ -112,15 +193,111 @@ export async function applyAuthChoiceQuotio(
|
|||||||
let nextConfig = params.config;
|
let nextConfig = params.config;
|
||||||
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
|
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
|
||||||
|
|
||||||
await params.prompter.note(
|
await params.prompter.note("Detecting Quotio...", "Auto-detection");
|
||||||
[
|
|
||||||
"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",
|
|
||||||
);
|
|
||||||
|
|
||||||
|
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({
|
const baseUrl = await params.prompter.text({
|
||||||
message: "Enter Quotio base URL",
|
message: "Enter Quotio base URL",
|
||||||
initialValue: QUOTIO_DEFAULT_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");
|
await params.prompter.note("Discovering available models from Quotio...", "Connecting");
|
||||||
|
|
||||||
const { models: discoveredModels, error } = await discoverQuotioModels(
|
const { models, error } = await discoverQuotioModels(normalizedBaseUrl, normalizedApiKey);
|
||||||
normalizedBaseUrl,
|
|
||||||
normalizedApiKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error || discoveredModels.length === 0) {
|
if (error) {
|
||||||
await params.prompter.note(
|
await params.prompter.note(
|
||||||
error
|
`Could not fetch models: ${error}\nPlease ensure Quotio is running and try again.`,
|
||||||
? `Could not fetch models: ${error}\nPlease ensure Quotio is running and try again.`
|
|
||||||
: "No models found. Please check your Quotio configuration.",
|
|
||||||
"Discovery Failed",
|
"Discovery Failed",
|
||||||
);
|
);
|
||||||
return { config: params.config };
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await params.prompter.note(
|
return { baseUrl: normalizedBaseUrl, apiKey: normalizedApiKey, models };
|
||||||
`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 };
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user