feat(quotio): add model discovery and selection
- Fetch available models from Quotio /models endpoint - Let user select their preferred default model from discovered list - Remove hardcoded model definitions in favor of dynamic discovery - Handle connection errors gracefully with user feedback
This commit is contained in:
parent
dd49d122fe
commit
92c771e107
@ -1,47 +1,72 @@
|
|||||||
import { upsertAuthProfile } from "../agents/auth-profiles.js";
|
import { upsertAuthProfile } from "../agents/auth-profiles.js";
|
||||||
import { resolveClawdbotAgentDir } from "../agents/agent-paths.js";
|
import { resolveClawdbotAgentDir } from "../agents/agent-paths.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
import type { ModelDefinitionConfig } from "../config/types.models.js";
|
||||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||||
import { applyAuthProfileConfig } from "./onboard-auth.js";
|
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_DEFAULT_MODEL = "quotio/gemini-claude-sonnet-4-thinking";
|
|
||||||
|
|
||||||
const QUOTIO_MODELS = [
|
type QuotioModel = {
|
||||||
{
|
id: string;
|
||||||
id: "gemini-claude-opus-4-5-thinking",
|
object?: string;
|
||||||
name: "Claude Opus 4.5 (Quotio)",
|
created?: number;
|
||||||
|
owned_by?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type QuotioModelsResponse = {
|
||||||
|
object: string;
|
||||||
|
data: QuotioModel[];
|
||||||
|
};
|
||||||
|
|
||||||
|
async function discoverQuotioModels(
|
||||||
|
baseUrl: string,
|
||||||
|
apiKey: string,
|
||||||
|
): Promise<{ models: QuotioModel[]; error?: string }> {
|
||||||
|
try {
|
||||||
|
const modelsUrl = baseUrl.endsWith("/") ? `${baseUrl}models` : `${baseUrl}/models`;
|
||||||
|
const response = await fetch(modelsUrl, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return { models: [], error: `HTTP ${response.status}: ${response.statusText}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as QuotioModelsResponse;
|
||||||
|
if (!data.data || !Array.isArray(data.data)) {
|
||||||
|
return { models: [], error: "Invalid response format from /models endpoint" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { models: data.data };
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
return { models: [], error: message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildModelDefinition(model: QuotioModel): ModelDefinitionConfig {
|
||||||
|
return {
|
||||||
|
id: model.id,
|
||||||
|
name: model.id,
|
||||||
reasoning: false,
|
reasoning: false,
|
||||||
input: ["text", "image"] as Array<"text" | "image">,
|
input: ["text", "image"] as Array<"text" | "image">,
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
maxTokens: 32000,
|
maxTokens: 32000,
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
},
|
};
|
||||||
{
|
}
|
||||||
id: "gemini-claude-sonnet-4-thinking",
|
|
||||||
name: "Claude Sonnet 4 (Quotio)",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text", "image"] as Array<"text" | "image">,
|
|
||||||
contextWindow: 200000,
|
|
||||||
maxTokens: 32000,
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "gemini-3-flash",
|
|
||||||
name: "Gemini 3 Flash (Quotio)",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text", "image"] as Array<"text" | "image">,
|
|
||||||
contextWindow: 1000000,
|
|
||||||
maxTokens: 65536,
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function applyQuotioProviderConfig(
|
function applyQuotioProviderConfig(
|
||||||
config: ClawdbotConfig,
|
config: ClawdbotConfig,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
|
models: ModelDefinitionConfig[],
|
||||||
): ClawdbotConfig {
|
): ClawdbotConfig {
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
@ -53,16 +78,16 @@ function applyQuotioProviderConfig(
|
|||||||
baseUrl,
|
baseUrl,
|
||||||
apiKey,
|
apiKey,
|
||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: QUOTIO_MODELS,
|
models,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyQuotioDefaultModel(config: ClawdbotConfig): ClawdbotConfig {
|
function applyQuotioDefaultModel(config: ClawdbotConfig, modelRef: string): ClawdbotConfig {
|
||||||
const models = { ...config.agents?.defaults?.models };
|
const models = { ...config.agents?.defaults?.models };
|
||||||
models[QUOTIO_DEFAULT_MODEL] = models[QUOTIO_DEFAULT_MODEL] ?? {};
|
models[modelRef] = models[modelRef] ?? {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
@ -72,7 +97,7 @@ function applyQuotioDefaultModel(config: ClawdbotConfig): ClawdbotConfig {
|
|||||||
...config.agents?.defaults,
|
...config.agents?.defaults,
|
||||||
models,
|
models,
|
||||||
model: {
|
model: {
|
||||||
primary: QUOTIO_DEFAULT_MODEL,
|
primary: modelRef,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -90,7 +115,7 @@ export async function applyAuthChoiceQuotio(
|
|||||||
await params.prompter.note(
|
await params.prompter.note(
|
||||||
[
|
[
|
||||||
"Quotio is a local OpenAI-compatible proxy that routes to various AI models.",
|
"Quotio is a local OpenAI-compatible proxy that routes to various AI models.",
|
||||||
"Make sure Quotio is running before using clawdbot.",
|
"Make sure Quotio is running before continuing.",
|
||||||
"Default endpoint: http://127.0.0.1:18317/v1",
|
"Default endpoint: http://127.0.0.1:18317/v1",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
"Quotio",
|
"Quotio",
|
||||||
@ -115,12 +140,51 @@ export async function applyAuthChoiceQuotio(
|
|||||||
initialValue: QUOTIO_DEFAULT_API_KEY,
|
initialValue: QUOTIO_DEFAULT_API_KEY,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const normalizedBaseUrl = String(baseUrl).trim() || QUOTIO_DEFAULT_BASE_URL;
|
||||||
|
const normalizedApiKey = String(apiKey).trim() || QUOTIO_DEFAULT_API_KEY;
|
||||||
|
|
||||||
|
await params.prompter.note("Discovering available models from Quotio...", "Connecting");
|
||||||
|
|
||||||
|
const { models: discoveredModels, error } = await discoverQuotioModels(
|
||||||
|
normalizedBaseUrl,
|
||||||
|
normalizedApiKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error || discoveredModels.length === 0) {
|
||||||
|
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.",
|
||||||
|
"Discovery Failed",
|
||||||
|
);
|
||||||
|
return { config: params.config };
|
||||||
|
}
|
||||||
|
|
||||||
|
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({
|
upsertAuthProfile({
|
||||||
profileId: "quotio:default",
|
profileId: "quotio:default",
|
||||||
credential: {
|
credential: {
|
||||||
type: "api_key",
|
type: "api_key",
|
||||||
provider: "quotio",
|
provider: "quotio",
|
||||||
key: String(apiKey).trim() || QUOTIO_DEFAULT_API_KEY,
|
key: normalizedApiKey,
|
||||||
},
|
},
|
||||||
agentDir,
|
agentDir,
|
||||||
});
|
});
|
||||||
@ -133,18 +197,19 @@ export async function applyAuthChoiceQuotio(
|
|||||||
|
|
||||||
nextConfig = applyQuotioProviderConfig(
|
nextConfig = applyQuotioProviderConfig(
|
||||||
nextConfig,
|
nextConfig,
|
||||||
String(baseUrl).trim() || QUOTIO_DEFAULT_BASE_URL,
|
normalizedBaseUrl,
|
||||||
String(apiKey).trim() || QUOTIO_DEFAULT_API_KEY,
|
normalizedApiKey,
|
||||||
|
modelDefinitions,
|
||||||
);
|
);
|
||||||
|
|
||||||
let agentModelOverride: string | undefined;
|
let agentModelOverride: string | undefined;
|
||||||
if (params.setDefaultModel) {
|
if (params.setDefaultModel) {
|
||||||
nextConfig = applyQuotioDefaultModel(nextConfig);
|
nextConfig = applyQuotioDefaultModel(nextConfig, defaultModelRef);
|
||||||
await params.prompter.note(`Default model set to ${QUOTIO_DEFAULT_MODEL}`, "Model configured");
|
await params.prompter.note(`Default model set to ${defaultModelRef}`, "Model configured");
|
||||||
} else if (params.agentId) {
|
} else if (params.agentId) {
|
||||||
agentModelOverride = QUOTIO_DEFAULT_MODEL;
|
agentModelOverride = defaultModelRef;
|
||||||
await params.prompter.note(
|
await params.prompter.note(
|
||||||
`Default model set to ${QUOTIO_DEFAULT_MODEL} for agent "${params.agentId}".`,
|
`Default model set to ${defaultModelRef} for agent "${params.agentId}".`,
|
||||||
"Model configured",
|
"Model configured",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user