Skip to content

Commit a22b869

Browse files
committed
feat: integrate with Clawdbot for auto-routing and model provider
1 parent a3e4073 commit a22b869

5 files changed

Lines changed: 701 additions & 1 deletion

File tree

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