From dd49d122fe5892ecf571e7ef266e17170f95c85b Mon Sep 17 00:00:00 2001 From: newideas99 Date: Mon, 26 Jan 2026 23:09:12 -0500 Subject: [PATCH 1/5] feat: add Quotio as provider option in onboarding wizard Quotio is a local OpenAI-compatible proxy that routes to various AI models (Claude via Gemini credits, etc.). This adds it as a first-class provider option in the onboarding wizard, eliminating the need for manual config edits. - Add 'quotio' to AuthChoice type - Add Quotio group to provider options - Create dedicated handler with URL/API key prompts - Configure provider with Claude Opus 4.5, Sonnet 4, Gemini 3 Flash models - Use openai-completions API for compatibility --- src/commands/auth-choice-options.ts | 14 +- src/commands/auth-choice.apply.quotio.ts | 153 ++++++++++++++++++ src/commands/auth-choice.apply.ts | 2 + .../auth-choice.preferred-provider.ts | 1 + src/commands/onboard-types.ts | 1 + 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/commands/auth-choice.apply.quotio.ts diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 6b49ff17b..b44b8a3d8 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -20,7 +20,8 @@ export type AuthChoiceGroupId = | "minimax" | "synthetic" | "venice" - | "qwen"; + | "qwen" + | "quotio"; export type AuthChoiceGroup = { value: AuthChoiceGroupId; @@ -113,6 +114,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "API key", choices: ["opencode-zen"], }, + { + value: "quotio", + label: "Quotio", + hint: "Local OpenAI-compatible proxy", + choices: ["quotio"], + }, ]; export function buildAuthChoiceOptions(params: { @@ -183,6 +190,11 @@ export function buildAuthChoiceOptions(params: { label: "MiniMax M2.1 Lightning", hint: "Faster, higher output cost", }); + options.push({ + value: "quotio", + label: "Quotio (local proxy)", + hint: "OpenAI-compatible proxy for Claude, Gemini, etc.", + }); if (params.includeSkip) { options.push({ value: "skip", label: "Skip for now" }); } diff --git a/src/commands/auth-choice.apply.quotio.ts b/src/commands/auth-choice.apply.quotio.ts new file mode 100644 index 000000000..83f156daf --- /dev/null +++ b/src/commands/auth-choice.apply.quotio.ts @@ -0,0 +1,153 @@ +import { upsertAuthProfile } from "../agents/auth-profiles.js"; +import { resolveClawdbotAgentDir } from "../agents/agent-paths.js"; +import type { ClawdbotConfig } from "../config/config.js"; +import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; +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_DEFAULT_MODEL = "quotio/gemini-claude-sonnet-4-thinking"; + +const QUOTIO_MODELS = [ + { + id: "gemini-claude-opus-4-5-thinking", + name: "Claude Opus 4.5 (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-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( + config: ClawdbotConfig, + baseUrl: string, + apiKey: string, +): ClawdbotConfig { + return { + ...config, + models: { + ...config.models, + providers: { + ...config.models?.providers, + quotio: { + baseUrl, + apiKey, + api: "openai-completions", + models: QUOTIO_MODELS, + }, + }, + }, + }; +} + +function applyQuotioDefaultModel(config: ClawdbotConfig): ClawdbotConfig { + const models = { ...config.agents?.defaults?.models }; + models[QUOTIO_DEFAULT_MODEL] = models[QUOTIO_DEFAULT_MODEL] ?? {}; + + return { + ...config, + agents: { + ...config.agents, + defaults: { + ...config.agents?.defaults, + models, + model: { + primary: QUOTIO_DEFAULT_MODEL, + }, + }, + }, + }; +} + +export async function applyAuthChoiceQuotio( + params: ApplyAuthChoiceParams, +): Promise { + if (params.authChoice !== "quotio") return null; + + 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 using clawdbot.", + "Default endpoint: http://127.0.0.1:18317/v1", + ].join("\n"), + "Quotio", + ); + + const baseUrl = await params.prompter.text({ + message: "Enter Quotio base URL", + initialValue: QUOTIO_DEFAULT_BASE_URL, + validate: (value) => { + if (!value?.trim()) return "Base URL is required"; + try { + new URL(value); + return undefined; + } catch { + return "Invalid URL format"; + } + }, + }); + + const apiKey = await params.prompter.text({ + message: "Enter Quotio API key (or leave default for local)", + initialValue: QUOTIO_DEFAULT_API_KEY, + }); + + upsertAuthProfile({ + profileId: "quotio:default", + credential: { + type: "api_key", + provider: "quotio", + key: String(apiKey).trim() || QUOTIO_DEFAULT_API_KEY, + }, + agentDir, + }); + + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "quotio:default", + provider: "quotio", + mode: "api_key", + }); + + nextConfig = applyQuotioProviderConfig( + nextConfig, + String(baseUrl).trim() || QUOTIO_DEFAULT_BASE_URL, + String(apiKey).trim() || QUOTIO_DEFAULT_API_KEY, + ); + + let agentModelOverride: string | undefined; + if (params.setDefaultModel) { + nextConfig = applyQuotioDefaultModel(nextConfig); + await params.prompter.note(`Default model set to ${QUOTIO_DEFAULT_MODEL}`, "Model configured"); + } else if (params.agentId) { + agentModelOverride = QUOTIO_DEFAULT_MODEL; + await params.prompter.note( + `Default model set to ${QUOTIO_DEFAULT_MODEL} for agent "${params.agentId}".`, + "Model configured", + ); + } + + return { config: nextConfig, agentModelOverride }; +} diff --git a/src/commands/auth-choice.apply.ts b/src/commands/auth-choice.apply.ts index f139b509f..33379658a 100644 --- a/src/commands/auth-choice.apply.ts +++ b/src/commands/auth-choice.apply.ts @@ -11,6 +11,7 @@ import { applyAuthChoiceMiniMax } from "./auth-choice.apply.minimax.js"; import { applyAuthChoiceOAuth } from "./auth-choice.apply.oauth.js"; import { applyAuthChoiceOpenAI } from "./auth-choice.apply.openai.js"; import { applyAuthChoiceQwenPortal } from "./auth-choice.apply.qwen-portal.js"; +import { applyAuthChoiceQuotio } from "./auth-choice.apply.quotio.js"; import type { AuthChoice } from "./onboard-types.js"; export type ApplyAuthChoiceParams = { @@ -46,6 +47,7 @@ export async function applyAuthChoice( applyAuthChoiceGoogleGeminiCli, applyAuthChoiceCopilotProxy, applyAuthChoiceQwenPortal, + applyAuthChoiceQuotio, ]; for (const handler of handlers) { diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index 6fe26b59a..6b639e0d7 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -28,6 +28,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { minimax: "lmstudio", "opencode-zen": "opencode", "qwen-portal": "qwen-portal", + quotio: "quotio", }; export function resolvePreferredProviderForAuthChoice(choice: AuthChoice): string | undefined { diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index aa1d9afe0..6ab27ea13 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -31,6 +31,7 @@ export type AuthChoice = | "github-copilot" | "copilot-proxy" | "qwen-portal" + | "quotio" | "skip"; export type GatewayAuthChoice = "token" | "password"; export type ResetScope = "config" | "config+creds+sessions" | "full"; From 92c771e107185f08a7679efd8aaacc41d90e6d0e Mon Sep 17 00:00:00 2001 From: newideas99 Date: Mon, 26 Jan 2026 23:19:13 -0500 Subject: [PATCH 2/5] 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 --- src/commands/auth-choice.apply.quotio.ts | 139 +++++++++++++++++------ 1 file changed, 102 insertions(+), 37 deletions(-) diff --git a/src/commands/auth-choice.apply.quotio.ts b/src/commands/auth-choice.apply.quotio.ts index 83f156daf..64b1fbbb7 100644 --- a/src/commands/auth-choice.apply.quotio.ts +++ b/src/commands/auth-choice.apply.quotio.ts @@ -1,47 +1,72 @@ import { upsertAuthProfile } from "../agents/auth-profiles.js"; import { resolveClawdbotAgentDir } from "../agents/agent-paths.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 { 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_DEFAULT_MODEL = "quotio/gemini-claude-sonnet-4-thinking"; -const QUOTIO_MODELS = [ - { - id: "gemini-claude-opus-4-5-thinking", - name: "Claude Opus 4.5 (Quotio)", +type QuotioModel = { + id: string; + object?: string; + 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, input: ["text", "image"] as Array<"text" | "image">, contextWindow: 200000, maxTokens: 32000, 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( config: ClawdbotConfig, baseUrl: string, apiKey: string, + models: ModelDefinitionConfig[], ): ClawdbotConfig { return { ...config, @@ -53,16 +78,16 @@ function applyQuotioProviderConfig( baseUrl, apiKey, 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 }; - models[QUOTIO_DEFAULT_MODEL] = models[QUOTIO_DEFAULT_MODEL] ?? {}; + models[modelRef] = models[modelRef] ?? {}; return { ...config, @@ -72,7 +97,7 @@ function applyQuotioDefaultModel(config: ClawdbotConfig): ClawdbotConfig { ...config.agents?.defaults, models, model: { - primary: QUOTIO_DEFAULT_MODEL, + primary: modelRef, }, }, }, @@ -90,7 +115,7 @@ export async function applyAuthChoiceQuotio( await params.prompter.note( [ "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", ].join("\n"), "Quotio", @@ -115,12 +140,51 @@ export async function applyAuthChoiceQuotio( 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({ profileId: "quotio:default", credential: { type: "api_key", provider: "quotio", - key: String(apiKey).trim() || QUOTIO_DEFAULT_API_KEY, + key: normalizedApiKey, }, agentDir, }); @@ -133,18 +197,19 @@ export async function applyAuthChoiceQuotio( nextConfig = applyQuotioProviderConfig( nextConfig, - String(baseUrl).trim() || QUOTIO_DEFAULT_BASE_URL, - String(apiKey).trim() || QUOTIO_DEFAULT_API_KEY, + normalizedBaseUrl, + normalizedApiKey, + modelDefinitions, ); let agentModelOverride: string | undefined; if (params.setDefaultModel) { - nextConfig = applyQuotioDefaultModel(nextConfig); - await params.prompter.note(`Default model set to ${QUOTIO_DEFAULT_MODEL}`, "Model configured"); + nextConfig = applyQuotioDefaultModel(nextConfig, defaultModelRef); + await params.prompter.note(`Default model set to ${defaultModelRef}`, "Model configured"); } else if (params.agentId) { - agentModelOverride = QUOTIO_DEFAULT_MODEL; + agentModelOverride = defaultModelRef; 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", ); } From 12525eb4b895186f91840cb9252643edaed9ccf7 Mon Sep 17 00:00:00 2001 From: newideas99 Date: Mon, 26 Jan 2026 23:23:19 -0500 Subject: [PATCH 3/5] 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 --- src/commands/auth-choice.apply.quotio.ts | 262 ++++++++++++++++------- 1 file changed, 190 insertions(+), 72 deletions(-) diff --git a/src/commands/auth-choice.apply.quotio.ts b/src/commands/auth-choice.apply.quotio.ts index 64b1fbbb7..601da3506 100644 --- a/src/commands/auth-choice.apply.quotio.ts +++ b/src/commands/auth-choice.apply.quotio.ts @@ -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 = { "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 { + 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 }; } From 892203bdb393debd2962f12a8c58151fd330b697 Mon Sep 17 00:00:00 2001 From: newideas99 Date: Mon, 26 Jan 2026 23:25:43 -0500 Subject: [PATCH 4/5] feat(quotio): read config from Quotio's config.yaml file Auto-detect base URL and API key from Quotio's config file: - macOS: ~/Library/Application Support/Quotio/config.yaml - Linux: ~/.config/quotio/config.yaml - Fallback: ~/.quotio/config.yaml Priority: env vars > config file > defaults This enables zero-config setup when Quotio is installed. --- src/commands/auth-choice.apply.quotio.ts | 49 ++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/commands/auth-choice.apply.quotio.ts b/src/commands/auth-choice.apply.quotio.ts index 601da3506..157d9d3a0 100644 --- a/src/commands/auth-choice.apply.quotio.ts +++ b/src/commands/auth-choice.apply.quotio.ts @@ -1,3 +1,7 @@ +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { parse as parseYaml } from "yaml"; import { upsertAuthProfile } from "../agents/auth-profiles.js"; import { resolveClawdbotAgentDir } from "../agents/agent-paths.js"; import type { ClawdbotConfig } from "../config/config.js"; @@ -9,6 +13,12 @@ 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 QuotioConfigFile = { + host?: string; + port?: number; + "api-keys"?: string[]; +}; + type QuotioModel = { id: string; object?: string; @@ -35,6 +45,38 @@ function getEnvQuotioConfig(): { baseUrl?: string; apiKey?: string } { }; } +function getQuotioConfigPaths(): string[] { + const home = homedir(); + return [ + join(home, "Library", "Application Support", "Quotio", "config.yaml"), + join(home, ".config", "quotio", "config.yaml"), + join(home, ".quotio", "config.yaml"), + ]; +} + +function readQuotioConfigFile(): { baseUrl?: string; apiKey?: string } { + for (const configPath of getQuotioConfigPaths()) { + try { + const content = readFileSync(configPath, "utf-8"); + const config = parseYaml(content) as QuotioConfigFile; + + if (!config) continue; + + const host = config.host || "127.0.0.1"; + const port = config.port || 18317; + const apiKeys = config["api-keys"]; + + return { + baseUrl: `http://${host}:${port}/v1`, + apiKey: apiKeys?.[0], + }; + } catch { + continue; + } + } + return {}; +} + async function probeQuotioEndpoint( baseUrl: string, apiKey: string, @@ -69,9 +111,10 @@ async function probeQuotioEndpoint( async function autoDetectQuotio(): Promise { const env = getEnvQuotioConfig(); + const fileConfig = readQuotioConfigFile(); - const baseUrl = env.baseUrl || QUOTIO_DEFAULT_BASE_URL; - const apiKey = env.apiKey || QUOTIO_DEFAULT_API_KEY; + const baseUrl = env.baseUrl || fileConfig.baseUrl || QUOTIO_DEFAULT_BASE_URL; + const apiKey = env.apiKey || fileConfig.apiKey || QUOTIO_DEFAULT_API_KEY; const result = await probeQuotioEndpoint(baseUrl, apiKey); if (result.ok) { @@ -83,7 +126,7 @@ async function autoDetectQuotio(): Promise { }; } - if (env.baseUrl || env.apiKey) { + if (baseUrl !== QUOTIO_DEFAULT_BASE_URL || apiKey !== QUOTIO_DEFAULT_API_KEY) { const defaultResult = await probeQuotioEndpoint( QUOTIO_DEFAULT_BASE_URL, QUOTIO_DEFAULT_API_KEY, From fe59a725e20262f739459230b70cd8aa650bd00e Mon Sep 17 00:00:00 2001 From: newideas99 Date: Mon, 26 Jan 2026 23:31:43 -0500 Subject: [PATCH 5/5] docs(quotio): improve descriptions based on official repo Updated hints and notes to accurately describe Quotio: - Native macOS menu bar app for unified AI accounts - Supports Claude, Gemini, OpenAI, Qwen, Antigravity - Real-time quota tracking with smart auto-failover - Link to https://www.quotio.dev for download Also fixed: Clawdbot -> Moltbot renames --- src/commands/auth-choice-options.ts | 6 +++--- src/commands/auth-choice.apply.quotio.ts | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index b44b8a3d8..1ea42bbae 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -117,7 +117,7 @@ const AUTH_CHOICE_GROUP_DEFS: { { value: "quotio", label: "Quotio", - hint: "Local OpenAI-compatible proxy", + hint: "Unified AI accounts with quota tracking", choices: ["quotio"], }, ]; @@ -192,8 +192,8 @@ export function buildAuthChoiceOptions(params: { }); options.push({ value: "quotio", - label: "Quotio (local proxy)", - hint: "OpenAI-compatible proxy for Claude, Gemini, etc.", + label: "Quotio", + hint: "Unified AI accounts with smart failover (macOS)", }); if (params.includeSkip) { options.push({ value: "skip", label: "Skip for now" }); diff --git a/src/commands/auth-choice.apply.quotio.ts b/src/commands/auth-choice.apply.quotio.ts index 157d9d3a0..90e3d997d 100644 --- a/src/commands/auth-choice.apply.quotio.ts +++ b/src/commands/auth-choice.apply.quotio.ts @@ -3,8 +3,8 @@ import { homedir } from "node:os"; import { join } from "node:path"; import { parse as parseYaml } from "yaml"; import { upsertAuthProfile } from "../agents/auth-profiles.js"; -import { resolveClawdbotAgentDir } from "../agents/agent-paths.js"; -import type { ClawdbotConfig } from "../config/config.js"; +import { resolveMoltbotAgentDir } from "../agents/agent-paths.js"; +import type { MoltbotConfig } from "../config/config.js"; import type { ModelDefinitionConfig } from "../config/types.models.js"; import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; import { applyAuthProfileConfig } from "./onboard-auth.js"; @@ -187,11 +187,11 @@ function buildModelDefinition(model: QuotioModel): ModelDefinitionConfig { } function applyQuotioProviderConfig( - config: ClawdbotConfig, + config: MoltbotConfig, baseUrl: string, apiKey: string, models: ModelDefinitionConfig[], -): ClawdbotConfig { +): MoltbotConfig { return { ...config, models: { @@ -209,7 +209,7 @@ function applyQuotioProviderConfig( }; } -function applyQuotioDefaultModel(config: ClawdbotConfig, modelRef: string): ClawdbotConfig { +function applyQuotioDefaultModel(config: MoltbotConfig, modelRef: string): MoltbotConfig { const models = { ...config.agents?.defaults?.models }; models[modelRef] = models[modelRef] ?? {}; @@ -234,7 +234,7 @@ export async function applyAuthChoiceQuotio( if (params.authChoice !== "quotio") return null; let nextConfig = params.config; - const agentDir = params.agentDir ?? resolveClawdbotAgentDir(); + const agentDir = params.agentDir ?? resolveMoltbotAgentDir(); await params.prompter.note("Detecting Quotio...", "Auto-detection"); @@ -270,8 +270,10 @@ export async function applyAuthChoiceQuotio( 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.", + "Quotio is a macOS menu bar app that unifies your AI subscriptions with quota tracking.", + "Download from: https://www.quotio.dev", + "", + "If already installed, make sure Quotio is running.", ].join("\n"), "Not detected", );