-
Notifications
You must be signed in to change notification settings - Fork 31
feat: integrate with Clawdbot for auto-routing and model provider #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
a22b869
feat: integrate with Clawdbot for auto-routing and model provider
johnlanni 10647bf
fix: update auto-routing to modify config file directly instead of Co…
johnlanni 26bd67b
fix: update skill to modify config inside container with hot-reload
johnlanni 9649654
fix: address cursor bot review issues
johnlanni File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "id": "higress-ai-gateway", | ||
| "name": "Higress AI Gateway", | ||
| "description": "Model provider plugin for Higress AI Gateway with auto-routing support", | ||
| "providers": ["higress"], | ||
| "skills": ["higress-auto-router"] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,274 @@ | ||
| import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk"; | ||
|
|
||
| const DEFAULT_GATEWAY_URL = "http://localhost:8080"; | ||
| const DEFAULT_CONSOLE_URL = "http://localhost:8001"; | ||
| const DEFAULT_CONTEXT_WINDOW = 128_000; | ||
| const DEFAULT_MAX_TOKENS = 8192; | ||
|
|
||
| // Common models that Higress AI Gateway typically supports | ||
| const DEFAULT_MODEL_IDS = [ | ||
| // Auto-routing special model | ||
| "higress/auto", | ||
| // OpenAI models | ||
| "gpt-4o", | ||
| "gpt-4o-mini", | ||
| "gpt-4-turbo", | ||
| // Anthropic models | ||
| "claude-opus-4.5", | ||
| "claude-sonnet-4.5", | ||
| "claude-haiku-4.5", | ||
| // Qwen models | ||
| "qwen-turbo", | ||
| "qwen-plus", | ||
| "qwen-max", | ||
| "qwen-coder", | ||
| // DeepSeek models | ||
| "deepseek-chat", | ||
| "deepseek-coder", | ||
| // Other common models | ||
| "moonshot-v1-8k", | ||
| "glm-4", | ||
| ] as const; | ||
|
|
||
| function normalizeBaseUrl(value: string): string { | ||
| const trimmed = value.trim(); | ||
| if (!trimmed) return DEFAULT_GATEWAY_URL; | ||
| let normalized = trimmed; | ||
| while (normalized.endsWith("/")) normalized = normalized.slice(0, -1); | ||
| if (!normalized.endsWith("/v1")) normalized = `${normalized}/v1`; | ||
| return normalized; | ||
| } | ||
|
|
||
| function validateUrl(value: string): string | undefined { | ||
| const normalized = normalizeBaseUrl(value); | ||
| try { | ||
| new URL(normalized); | ||
| } catch { | ||
| return "Enter a valid URL"; | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| function parseModelIds(input: string): string[] { | ||
| const parsed = input | ||
| .split(/[\n,]/) | ||
| .map((model) => model.trim()) | ||
| .filter(Boolean); | ||
| return Array.from(new Set(parsed)); | ||
| } | ||
|
|
||
| function buildModelDefinition(modelId: string) { | ||
| const isAutoModel = modelId === "higress/auto"; | ||
| return { | ||
| id: modelId, | ||
| name: isAutoModel ? "Higress Auto Router" : modelId, | ||
| api: "openai-completions", | ||
| reasoning: false, | ||
| input: ["text", "image"], | ||
| cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, | ||
| contextWindow: DEFAULT_CONTEXT_WINDOW, | ||
| maxTokens: DEFAULT_MAX_TOKENS, | ||
| }; | ||
| } | ||
|
|
||
| async function testGatewayConnection(gatewayUrl: string): Promise<boolean> { | ||
| try { | ||
| const response = await fetch(`${gatewayUrl}/v1/models`, { | ||
| method: "GET", | ||
| headers: { "Content-Type": "application/json" }, | ||
| signal: AbortSignal.timeout(5000), | ||
| }); | ||
| return response.ok || response.status === 401; // 401 means gateway is up but needs auth | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| async function fetchAvailableModels(consoleUrl: string): Promise<string[]> { | ||
| try { | ||
| // Try to get models from Higress Console API | ||
| const response = await fetch(`${consoleUrl}/v1/ai/routes`, { | ||
| method: "GET", | ||
| headers: { "Content-Type": "application/json" }, | ||
| signal: AbortSignal.timeout(5000), | ||
| }); | ||
| if (response.ok) { | ||
| const data = (await response.json()) as { data?: { model?: string }[] }; | ||
| if (data.data && Array.isArray(data.data)) { | ||
| return data.data | ||
| .map((route: { model?: string }) => route.model) | ||
| .filter((m): m is string => typeof m === "string"); | ||
| } | ||
| } | ||
| } catch { | ||
| // Ignore errors, use defaults | ||
| } | ||
| return []; | ||
| } | ||
|
|
||
| const higressPlugin = { | ||
| id: "higress-ai-gateway", | ||
| name: "Higress AI Gateway", | ||
| description: "Model provider plugin for Higress AI Gateway with auto-routing support", | ||
| configSchema: emptyPluginConfigSchema(), | ||
| register(api) { | ||
| api.registerProvider({ | ||
| id: "higress", | ||
| label: "Higress AI Gateway", | ||
| docsPath: "/providers/models", | ||
| aliases: ["higress-gateway", "higress-ai"], | ||
| auth: [ | ||
| { | ||
| id: "api-key", | ||
| label: "API Key", | ||
| hint: "Configure Higress AI Gateway endpoint with optional API key", | ||
| kind: "custom", | ||
| run: async (ctx) => { | ||
| // Step 1: Get Gateway URL | ||
| const gatewayUrlInput = await ctx.prompter.text({ | ||
| message: "Higress AI Gateway URL", | ||
| initialValue: DEFAULT_GATEWAY_URL, | ||
| validate: validateUrl, | ||
| }); | ||
| const gatewayUrl = normalizeBaseUrl(gatewayUrlInput); | ||
|
|
||
| // Step 2: Get Console URL (for auto-router configuration) | ||
| const consoleUrlInput = await ctx.prompter.text({ | ||
| message: "Higress Console URL (for auto-router config)", | ||
| initialValue: DEFAULT_CONSOLE_URL, | ||
| validate: validateUrl, | ||
| }); | ||
| const consoleUrl = normalizeBaseUrl(consoleUrlInput); | ||
|
|
||
| // Step 3: Test connection (create a new spinner) | ||
| const spin = ctx.prompter.progress("Testing gateway connection…"); | ||
| const isConnected = await testGatewayConnection(gatewayUrl); | ||
| if (!isConnected) { | ||
| spin.stop("Gateway connection failed"); | ||
| await ctx.prompter.note( | ||
| [ | ||
| "Could not connect to Higress AI Gateway.", | ||
| "Make sure the gateway is running and the URL is correct.", | ||
| "", | ||
| `Tried: ${gatewayUrl}/v1/models`, | ||
| ].join("\n"), | ||
| "Connection Warning", | ||
| ); | ||
| } else { | ||
| spin.stop("Gateway connected"); | ||
| } | ||
|
|
||
| // Step 4: Get API Key (optional for local gateway) | ||
| const apiKeyInput = await ctx.prompter.text({ | ||
| message: "API Key (leave empty if not required)", | ||
| initialValue: "", | ||
| }); | ||
| const apiKey = apiKeyInput.trim() || "higress-local"; | ||
|
|
||
| // Step 5: Fetch available models (create a new spinner) | ||
| const spin2 = ctx.prompter.progress("Fetching available models…"); | ||
| const fetchedModels = await fetchAvailableModels(consoleUrl); | ||
| const defaultModels = fetchedModels.length > 0 | ||
| ? ["higress/auto", ...fetchedModels] | ||
| : DEFAULT_MODEL_IDS; | ||
| spin2.stop(); | ||
|
|
||
| // Step 6: Let user customize model list | ||
| const modelInput = await ctx.prompter.text({ | ||
| message: "Model IDs (comma-separated, higress/auto enables auto-routing)", | ||
| initialValue: defaultModels.slice(0, 10).join(", "), | ||
| validate: (value) => | ||
| parseModelIds(value).length > 0 ? undefined : "Enter at least one model id", | ||
| }); | ||
|
|
||
| const modelIds = parseModelIds(modelInput); | ||
| const hasAutoModel = modelIds.includes("higress/auto"); | ||
|
|
||
| // FIX: Avoid double prefix - if modelId already starts with provider, don't add prefix again | ||
| const defaultModelId = hasAutoModel | ||
| ? "higress/auto" | ||
| : (modelIds[0] ?? "qwen-turbo"); | ||
| const defaultModelRef = defaultModelId.startsWith("higress/") | ||
| ? defaultModelId | ||
| : `higress/${defaultModelId}`; | ||
|
|
||
| // Step 7: Configure default model for auto-routing | ||
| let autoRoutingDefaultModel = "qwen-turbo"; | ||
| if (hasAutoModel) { | ||
| const autoRoutingModelInput = await ctx.prompter.text({ | ||
| message: "Default model for auto-routing (when no rule matches)", | ||
| initialValue: "qwen-turbo", | ||
| }); | ||
| autoRoutingDefaultModel = autoRoutingModelInput.trim(); // FIX: Add trim() here | ||
| } | ||
|
|
||
| return { | ||
| profiles: [ | ||
| { | ||
| profileId: `higress:${apiKey === "higress-local" ? "local" : "default"}`, | ||
| credential: { | ||
| type: "token", | ||
| provider: "higress", | ||
| token: apiKey, | ||
| }, | ||
| }, | ||
| ], | ||
| configPatch: { | ||
| models: { | ||
| providers: { | ||
| higress: { | ||
| baseUrl: `${gatewayUrl}/v1`, | ||
| apiKey: apiKey, | ||
| api: "openai-completions", | ||
| authHeader: apiKey !== "higress-local", | ||
| models: modelIds.map((modelId) => buildModelDefinition(modelId)), | ||
| }, | ||
| }, | ||
| }, | ||
| agents: { | ||
| defaults: { | ||
| models: Object.fromEntries( | ||
| modelIds.map((modelId) => { | ||
| // FIX: Avoid double prefix - only add provider prefix if not already present | ||
| const modelRef = modelId.startsWith("higress/") | ||
| ? modelId | ||
| : `higress/${modelId}`; | ||
| return [modelRef, {}]; | ||
| }), | ||
| ), | ||
| }, | ||
| }, | ||
| plugins: { | ||
| entries: { | ||
| "higress-ai-gateway": { | ||
| enabled: true, | ||
| config: { | ||
| gatewayUrl, | ||
| consoleUrl, | ||
| autoRoutingDefaultModel, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| defaultModel: defaultModelRef, | ||
| notes: [ | ||
| "Higress AI Gateway is now configured as a model provider.", | ||
| hasAutoModel | ||
| ? `Auto-routing enabled: use model "higress/auto" to route based on message content.` | ||
| : "Add 'higress/auto' to models to enable auto-routing.", | ||
| `Gateway endpoint: ${gatewayUrl}/v1/chat/completions`, | ||
| `Console: ${consoleUrl}`, | ||
| "", | ||
| "To configure auto-routing rules, use the higress-auto-router skill:", | ||
| ' Say: "route to claude-opus-4.5 when solving difficult problems"', | ||
| ], | ||
| }; | ||
| }, | ||
| }, | ||
| ], | ||
| }); | ||
| }, | ||
| }; | ||
|
|
||
| export default higressPlugin; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| { | ||
| "name": "@higress/clawdbot-ai-gateway", | ||
| "version": "1.0.0", | ||
| "description": "Higress AI Gateway model provider plugin for Clawdbot with auto-routing support", | ||
| "main": "index.ts", | ||
| "clawdbot": { | ||
| "extensions": ["./index.ts"] | ||
| }, | ||
| "keywords": [ | ||
| "clawdbot", | ||
| "higress", | ||
| "ai-gateway", | ||
| "model-router", | ||
| "auto-routing" | ||
| ], | ||
| "author": "Higress Team", | ||
| "license": "Apache-2.0", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/higress-group/higress-standalone" | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Double
/v1suffix in URL paths breaks APIHigh Severity
The
normalizeBaseUrl()function adds a/v1suffix to URLs, buttestGatewayConnection(),fetchAvailableModels(), and the provider config all append/v1again. This creates malformed URLs likehttp://localhost:8080/v1/v1/models. The gateway connection test and model fetching will fail, and thebaseUrlin the provider configuration will be incorrect.Additional Locations (2)
all-in-one/clawdbot-integration/plugin/index.ts#L89-L90all-in-one/clawdbot-integration/plugin/index.ts#L219-L220