Skip to content

Commit c2d425e

Browse files
authored
feat: integrate with Clawdbot for auto-routing and model provider (#221)
* feat: integrate with Clawdbot for auto-routing and model provider - Enhanced get-ai-gateway.sh to detect Clawdbot installation - Added interactive prompt for enabling auto-routing feature - Added configuration for default model in auto-routing mode - Direct modification of model-router config inside container (no restart) - Created Higress AI Gateway plugin for Clawdbot (API Key auth) - Created Higress Auto Router skill for natural language routing config Fixed issues from review: - Avoid double prefix for model refs (higress/higress/auto) - Separate spinner instances for different progress steps - Add trim() to user inputs - Use awk for cross-platform compatibility (macOS/Linux) - Load saved config on container restart for welcome message - Trigger hot-reload via docker exec touch * fix: simplify config reload to touch method only
1 parent 004f4d3 commit c2d425e

5 files changed

Lines changed: 811 additions & 1 deletion

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"id": "higress-ai-gateway",
3+
"name": "Higress AI Gateway",
4+
"description": "Model provider plugin for Higress AI Gateway with auto-routing support",
5+
"providers": ["higress"],
6+
"skills": ["higress-auto-router"]
7+
}
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
2+
3+
const DEFAULT_GATEWAY_URL = "http://localhost:8080";
4+
const DEFAULT_CONSOLE_URL = "http://localhost:8001";
5+
const DEFAULT_CONTEXT_WINDOW = 128_000;
6+
const DEFAULT_MAX_TOKENS = 8192;
7+
8+
// Common models that Higress AI Gateway typically supports
9+
const DEFAULT_MODEL_IDS = [
10+
// Auto-routing special model
11+
"higress/auto",
12+
// OpenAI models
13+
"gpt-4o",
14+
"gpt-4o-mini",
15+
"gpt-4-turbo",
16+
// Anthropic models
17+
"claude-opus-4.5",
18+
"claude-sonnet-4.5",
19+
"claude-haiku-4.5",
20+
// Qwen models
21+
"qwen-turbo",
22+
"qwen-plus",
23+
"qwen-max",
24+
"qwen-coder",
25+
// DeepSeek models
26+
"deepseek-chat",
27+
"deepseek-coder",
28+
// Other common models
29+
"moonshot-v1-8k",
30+
"glm-4",
31+
] as const;
32+
33+
function normalizeBaseUrl(value: string): string {
34+
const trimmed = value.trim();
35+
if (!trimmed) return DEFAULT_GATEWAY_URL;
36+
let normalized = trimmed;
37+
while (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
38+
if (!normalized.endsWith("/v1")) normalized = `${normalized}/v1`;
39+
return normalized;
40+
}
41+
42+
function validateUrl(value: string): string | undefined {
43+
const normalized = normalizeBaseUrl(value);
44+
try {
45+
new URL(normalized);
46+
} catch {
47+
return "Enter a valid URL";
48+
}
49+
return undefined;
50+
}
51+
52+
function parseModelIds(input: string): string[] {
53+
const parsed = input
54+
.split(/[\n,]/)
55+
.map((model) => model.trim())
56+
.filter(Boolean);
57+
return Array.from(new Set(parsed));
58+
}
59+
60+
function buildModelDefinition(modelId: string) {
61+
const isAutoModel = modelId === "higress/auto";
62+
return {
63+
id: modelId,
64+
name: isAutoModel ? "Higress Auto Router" : modelId,
65+
api: "openai-completions",
66+
reasoning: false,
67+
input: ["text", "image"],
68+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
69+
contextWindow: DEFAULT_CONTEXT_WINDOW,
70+
maxTokens: DEFAULT_MAX_TOKENS,
71+
};
72+
}
73+
74+
async function testGatewayConnection(gatewayUrl: string): Promise<boolean> {
75+
try {
76+
const response = await fetch(`${gatewayUrl}/v1/models`, {
77+
method: "GET",
78+
headers: { "Content-Type": "application/json" },
79+
signal: AbortSignal.timeout(5000),
80+
});
81+
return response.ok || response.status === 401; // 401 means gateway is up but needs auth
82+
} catch {
83+
return false;
84+
}
85+
}
86+
87+
async function fetchAvailableModels(consoleUrl: string): Promise<string[]> {
88+
try {
89+
// Try to get models from Higress Console API
90+
const response = await fetch(`${consoleUrl}/v1/ai/routes`, {
91+
method: "GET",
92+
headers: { "Content-Type": "application/json" },
93+
signal: AbortSignal.timeout(5000),
94+
});
95+
if (response.ok) {
96+
const data = (await response.json()) as { data?: { model?: string }[] };
97+
if (data.data && Array.isArray(data.data)) {
98+
return data.data
99+
.map((route: { model?: string }) => route.model)
100+
.filter((m): m is string => typeof m === "string");
101+
}
102+
}
103+
} catch {
104+
// Ignore errors, use defaults
105+
}
106+
return [];
107+
}
108+
109+
const higressPlugin = {
110+
id: "higress-ai-gateway",
111+
name: "Higress AI Gateway",
112+
description: "Model provider plugin for Higress AI Gateway with auto-routing support",
113+
configSchema: emptyPluginConfigSchema(),
114+
register(api) {
115+
api.registerProvider({
116+
id: "higress",
117+
label: "Higress AI Gateway",
118+
docsPath: "/providers/models",
119+
aliases: ["higress-gateway", "higress-ai"],
120+
auth: [
121+
{
122+
id: "api-key",
123+
label: "API Key",
124+
hint: "Configure Higress AI Gateway endpoint with optional API key",
125+
kind: "custom",
126+
run: async (ctx) => {
127+
// Step 1: Get Gateway URL
128+
const gatewayUrlInput = await ctx.prompter.text({
129+
message: "Higress AI Gateway URL",
130+
initialValue: DEFAULT_GATEWAY_URL,
131+
validate: validateUrl,
132+
});
133+
const gatewayUrl = normalizeBaseUrl(gatewayUrlInput);
134+
135+
// Step 2: Get Console URL (for auto-router configuration)
136+
const consoleUrlInput = await ctx.prompter.text({
137+
message: "Higress Console URL (for auto-router config)",
138+
initialValue: DEFAULT_CONSOLE_URL,
139+
validate: validateUrl,
140+
});
141+
const consoleUrl = normalizeBaseUrl(consoleUrlInput);
142+
143+
// Step 3: Test connection (create a new spinner)
144+
const spin = ctx.prompter.progress("Testing gateway connection…");
145+
const isConnected = await testGatewayConnection(gatewayUrl);
146+
if (!isConnected) {
147+
spin.stop("Gateway connection failed");
148+
await ctx.prompter.note(
149+
[
150+
"Could not connect to Higress AI Gateway.",
151+
"Make sure the gateway is running and the URL is correct.",
152+
"",
153+
`Tried: ${gatewayUrl}/v1/models`,
154+
].join("\n"),
155+
"Connection Warning",
156+
);
157+
} else {
158+
spin.stop("Gateway connected");
159+
}
160+
161+
// Step 4: Get API Key (optional for local gateway)
162+
const apiKeyInput = await ctx.prompter.text({
163+
message: "API Key (leave empty if not required)",
164+
initialValue: "",
165+
});
166+
const apiKey = apiKeyInput.trim() || "higress-local";
167+
168+
// Step 5: Fetch available models (create a new spinner)
169+
const spin2 = ctx.prompter.progress("Fetching available models…");
170+
const fetchedModels = await fetchAvailableModels(consoleUrl);
171+
const defaultModels = fetchedModels.length > 0
172+
? ["higress/auto", ...fetchedModels]
173+
: DEFAULT_MODEL_IDS;
174+
spin2.stop();
175+
176+
// Step 6: Let user customize model list
177+
const modelInput = await ctx.prompter.text({
178+
message: "Model IDs (comma-separated, higress/auto enables auto-routing)",
179+
initialValue: defaultModels.slice(0, 10).join(", "),
180+
validate: (value) =>
181+
parseModelIds(value).length > 0 ? undefined : "Enter at least one model id",
182+
});
183+
184+
const modelIds = parseModelIds(modelInput);
185+
const hasAutoModel = modelIds.includes("higress/auto");
186+
187+
// FIX: Avoid double prefix - if modelId already starts with provider, don't add prefix again
188+
const defaultModelId = hasAutoModel
189+
? "higress/auto"
190+
: (modelIds[0] ?? "qwen-turbo");
191+
const defaultModelRef = defaultModelId.startsWith("higress/")
192+
? defaultModelId
193+
: `higress/${defaultModelId}`;
194+
195+
// Step 7: Configure default model for auto-routing
196+
let autoRoutingDefaultModel = "qwen-turbo";
197+
if (hasAutoModel) {
198+
const autoRoutingModelInput = await ctx.prompter.text({
199+
message: "Default model for auto-routing (when no rule matches)",
200+
initialValue: "qwen-turbo",
201+
});
202+
autoRoutingDefaultModel = autoRoutingModelInput.trim(); // FIX: Add trim() here
203+
}
204+
205+
return {
206+
profiles: [
207+
{
208+
profileId: `higress:${apiKey === "higress-local" ? "local" : "default"}`,
209+
credential: {
210+
type: "token",
211+
provider: "higress",
212+
token: apiKey,
213+
},
214+
},
215+
],
216+
configPatch: {
217+
models: {
218+
providers: {
219+
higress: {
220+
baseUrl: `${gatewayUrl}/v1`,
221+
apiKey: apiKey,
222+
api: "openai-completions",
223+
authHeader: apiKey !== "higress-local",
224+
models: modelIds.map((modelId) => buildModelDefinition(modelId)),
225+
},
226+
},
227+
},
228+
agents: {
229+
defaults: {
230+
models: Object.fromEntries(
231+
modelIds.map((modelId) => {
232+
// FIX: Avoid double prefix - only add provider prefix if not already present
233+
const modelRef = modelId.startsWith("higress/")
234+
? modelId
235+
: `higress/${modelId}`;
236+
return [modelRef, {}];
237+
}),
238+
),
239+
},
240+
},
241+
plugins: {
242+
entries: {
243+
"higress-ai-gateway": {
244+
enabled: true,
245+
config: {
246+
gatewayUrl,
247+
consoleUrl,
248+
autoRoutingDefaultModel,
249+
},
250+
},
251+
},
252+
},
253+
},
254+
defaultModel: defaultModelRef,
255+
notes: [
256+
"Higress AI Gateway is now configured as a model provider.",
257+
hasAutoModel
258+
? `Auto-routing enabled: use model "higress/auto" to route based on message content.`
259+
: "Add 'higress/auto' to models to enable auto-routing.",
260+
`Gateway endpoint: ${gatewayUrl}/v1/chat/completions`,
261+
`Console: ${consoleUrl}`,
262+
"",
263+
"To configure auto-routing rules, use the higress-auto-router skill:",
264+
' Say: "route to claude-opus-4.5 when solving difficult problems"',
265+
],
266+
};
267+
},
268+
},
269+
],
270+
});
271+
},
272+
};
273+
274+
export default higressPlugin;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "@higress/clawdbot-ai-gateway",
3+
"version": "1.0.0",
4+
"description": "Higress AI Gateway model provider plugin for Clawdbot with auto-routing support",
5+
"main": "index.ts",
6+
"clawdbot": {
7+
"extensions": ["./index.ts"]
8+
},
9+
"keywords": [
10+
"clawdbot",
11+
"higress",
12+
"ai-gateway",
13+
"model-router",
14+
"auto-routing"
15+
],
16+
"author": "Higress Team",
17+
"license": "Apache-2.0",
18+
"repository": {
19+
"type": "git",
20+
"url": "https://github.com/higress-group/higress-standalone"
21+
}
22+
}

0 commit comments

Comments
 (0)