From dd49d122fe5892ecf571e7ef266e17170f95c85b Mon Sep 17 00:00:00 2001 From: newideas99 Date: Mon, 26 Jan 2026 23:09:12 -0500 Subject: [PATCH] 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";