bridge: add gateway provider routing model
This commit is contained in:
parent
17bbe1f71a
commit
26db84561a
@ -11,7 +11,14 @@
|
||||
|
||||
## ACP Forwarding Topology
|
||||
|
||||
This repository exposes one bridge entrypoint and forwards to four verified public targets. The full Mermaid diagram lives in [docs/architecture/acp-forwarding-topology.md](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-bridge/docs/architecture/acp-forwarding-topology.md).
|
||||
This repository exposes one APP-facing bridge entrypoint and proxies traffic
|
||||
to four independent upstream production services. The APP-facing canonical ACP
|
||||
paths remain `/acp/rpc` and `/acp` under
|
||||
`https://xworkmate-bridge.svc.plus`.
|
||||
|
||||
Architecture topology: [docs/architecture/acp-forwarding-topology.md](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-bridge/docs/architecture/acp-forwarding-topology.md)
|
||||
|
||||
ADR for the unified APP-facing bridge contract: [docs/architecture/adr-unified-bridge-entrypoints.md](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-bridge/docs/architecture/adr-unified-bridge-entrypoints.md)
|
||||
|
||||
Example provider sync config: [example/config.yaml](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-bridge/example/config.yaml)
|
||||
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
# ACP Public Validation - 2026-04-09
|
||||
|
||||
This document records the post-deployment public validation for `xworkmate-bridge.svc.plus` and the unified ACP ingress at `acp-server.svc.plus`.
|
||||
This document records the post-deployment validation of the bridge public
|
||||
origin at `xworkmate-bridge.svc.plus` and the independent upstream ACP ingress
|
||||
at `acp-server.svc.plus`.
|
||||
|
||||
It is intended as an app-integration reference so future clients use the verified public endpoints and expected JSON-RPC methods.
|
||||
For APP integration, the canonical public contract remains the bridge origin
|
||||
and the `.../acp/rpc` path on that origin. The direct `acp-server.svc.plus`
|
||||
URLs in this document are upstream validation targets, not the preferred APP
|
||||
entry points.
|
||||
|
||||
## Verified Public Endpoints
|
||||
|
||||
@ -18,6 +23,10 @@ The public ACP JSON-RPC endpoint is the `.../acp/rpc` path.
|
||||
|
||||
Do not send JSON-RPC requests to `.../acp` for HTTP clients.
|
||||
|
||||
Recommended APP-facing endpoint:
|
||||
|
||||
- `https://xworkmate-bridge.svc.plus/acp/rpc`
|
||||
|
||||
Verified public HTTP JSON-RPC endpoints:
|
||||
|
||||
- Codex: `https://acp-server.svc.plus/codex/acp/rpc`
|
||||
@ -146,13 +155,15 @@ Verified result summary:
|
||||
- `opencode` long conversation passed
|
||||
- `gemini` long conversation passed
|
||||
|
||||
This is the current app-integration baseline for `acp-server.svc.plus`.
|
||||
This confirms the upstream ACP baseline. The APP-facing baseline remains
|
||||
`https://xworkmate-bridge.svc.plus/acp/rpc`.
|
||||
|
||||
## App Integration Notes
|
||||
|
||||
### Recommended request shape
|
||||
|
||||
Use JSON-RPC `POST` requests against `.../acp/rpc`.
|
||||
For APP integration, use JSON-RPC `POST` requests against
|
||||
`https://xworkmate-bridge.svc.plus/acp/rpc`.
|
||||
|
||||
For capability discovery:
|
||||
|
||||
|
||||
@ -26,6 +26,18 @@ Relevant source:
|
||||
|
||||
## 2. ACP Bridge HTTP / WebSocket API
|
||||
|
||||
Canonical APP-facing public origin:
|
||||
|
||||
- `https://xworkmate-bridge.svc.plus`
|
||||
|
||||
Canonical APP-facing ACP paths:
|
||||
|
||||
- `POST /acp/rpc`
|
||||
- `GET /acp` for WebSocket ACP
|
||||
|
||||
Independent upstream ACP and gateway services may exist behind the bridge, but
|
||||
they are not the primary APP contract.
|
||||
|
||||
### 2.1 Default listen address
|
||||
|
||||
`serve` mode reads:
|
||||
@ -121,6 +133,10 @@ Response shape:
|
||||
{ "providerId": "opencode", "label": "OpenCode" },
|
||||
{ "providerId": "gemini", "label": "Gemini" }
|
||||
],
|
||||
"gatewayProviders": [
|
||||
{ "providerId": "local", "label": "Local" },
|
||||
{ "providerId": "openclaw", "label": "OpenClaw" }
|
||||
],
|
||||
"capabilities": {
|
||||
"single_agent": true,
|
||||
"multi_agent": true,
|
||||
@ -128,6 +144,10 @@ Response shape:
|
||||
{ "providerId": "codex", "label": "Codex" },
|
||||
{ "providerId": "opencode", "label": "OpenCode" },
|
||||
{ "providerId": "gemini", "label": "Gemini" }
|
||||
],
|
||||
"gatewayProviders": [
|
||||
{ "providerId": "local", "label": "Local" },
|
||||
{ "providerId": "openclaw", "label": "OpenClaw" }
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -137,10 +157,13 @@ Response shape:
|
||||
Notes:
|
||||
|
||||
- `providerCatalog` is bridge-owned and built in at startup
|
||||
- production provider map is fixed to:
|
||||
- `gatewayProviders` is the APP-facing gateway provider catalog
|
||||
- production upstream routing map is fixed to:
|
||||
- `codex` -> `https://acp-server.svc.plus/codex/acp/rpc`
|
||||
- `opencode` -> `https://acp-server.svc.plus/opencode/acp/rpc`
|
||||
- `gemini` -> `https://acp-server.svc.plus/gemini/acp/rpc`
|
||||
- APP traffic reaches those upstreams through the bridge's canonical public
|
||||
ACP path, not by depending on upstream URLs directly
|
||||
- upstream ACP auth uses `Authorization: Bearer $INTERNAL_SERVICE_TOKEN`
|
||||
- `multiAgent` is controlled by `ACP_MULTI_AGENT_ENABLED`, default `true`
|
||||
|
||||
@ -298,8 +321,8 @@ Purpose:
|
||||
|
||||
- resolve routing metadata into:
|
||||
- execution target
|
||||
- endpoint target
|
||||
- provider
|
||||
- gateway provider
|
||||
- model
|
||||
- selected skills
|
||||
- install suggestion / unavailable state
|
||||
@ -318,6 +341,7 @@ Key input fields:
|
||||
- `taskPrompt`
|
||||
- `workingDirectory`
|
||||
- `routing.routingMode`
|
||||
- `routing.preferredGatewayProviderId`
|
||||
- `routing.preferredGatewayTarget`
|
||||
- `routing.explicitExecutionTarget`
|
||||
- `routing.explicitProviderId`
|
||||
@ -332,8 +356,9 @@ Key input fields:
|
||||
Representative response fields:
|
||||
|
||||
- `resolvedExecutionTarget`
|
||||
- `resolvedEndpointTarget`
|
||||
- `resolvedProviderId`
|
||||
- `resolvedGatewayProviderId`
|
||||
- `resolvedEndpointTarget`
|
||||
- `resolvedModel`
|
||||
- `resolvedSkills`
|
||||
- `skillResolutionSource`
|
||||
@ -345,6 +370,76 @@ Representative response fields:
|
||||
- `skillCandidates`
|
||||
- `memorySources`
|
||||
|
||||
APP-facing interpretation:
|
||||
|
||||
- if `resolvedExecutionTarget = single-agent`, use `resolvedProviderId`
|
||||
- if `resolvedExecutionTarget = gateway`, use `resolvedGatewayProviderId`
|
||||
- `resolvedEndpointTarget` is retained as a compatibility field for bridge
|
||||
internals; APP code should prefer `resolvedGatewayProviderId`
|
||||
|
||||
### 3.7.1 UI / APP consumption model
|
||||
|
||||
Recommended APP-side flow:
|
||||
|
||||
1. Call `acp.capabilities` at startup or refresh time.
|
||||
2. Read:
|
||||
- `providerCatalog` as `singleAgentProviders`
|
||||
- `gatewayProviders` as `gatewayProviders`
|
||||
3. Before execution, call `xworkmate.routing.resolve`.
|
||||
4. Use the resolved fields, not local heuristics, to decide which UI state and
|
||||
execution branch to use.
|
||||
5. Send `session.start` or `session.message` through the bridge canonical path.
|
||||
|
||||
Suggested APP-side view model:
|
||||
|
||||
```json
|
||||
{
|
||||
"executionTargets": ["single-agent", "multi-agent", "gateway"],
|
||||
"singleAgentProviders": ["codex", "opencode", "gemini"],
|
||||
"gatewayProviders": ["local", "openclaw"]
|
||||
}
|
||||
```
|
||||
|
||||
Recommended interpretation rules:
|
||||
|
||||
- if `resolvedExecutionTarget = single-agent`
|
||||
- read `resolvedProviderId`
|
||||
- ignore `resolvedGatewayProviderId`
|
||||
- if `resolvedExecutionTarget = multi-agent`
|
||||
- treat the run as bridge-orchestrated multi-agent execution
|
||||
- do not force a single provider picker as the primary routing control
|
||||
- if `resolvedExecutionTarget = gateway`
|
||||
- read `resolvedGatewayProviderId`
|
||||
- ignore `resolvedProviderId`
|
||||
|
||||
Compatibility rule:
|
||||
|
||||
- `resolvedEndpointTarget` is a compatibility field
|
||||
- APP code should not use `local` / `remote` as its primary business model
|
||||
- current bridge mapping is:
|
||||
- `resolvedGatewayProviderId = local` -> `resolvedEndpointTarget = local`
|
||||
- `resolvedGatewayProviderId = openclaw` -> `resolvedEndpointTarget = remote`
|
||||
|
||||
UI binding guidance:
|
||||
|
||||
- provider picker for single-agent mode should be populated from
|
||||
`providerCatalog`
|
||||
- gateway picker should be populated from `gatewayProviders`
|
||||
- gateway UI should display `local` and `openclaw` as selectable providers
|
||||
rather than exposing transport-level `local` / `remote` terminology
|
||||
- disabled or unavailable states should come from `xworkmate.routing.resolve`
|
||||
response fields such as:
|
||||
- `unavailable`
|
||||
- `unavailableCode`
|
||||
- `unavailableMessage`
|
||||
|
||||
Do not:
|
||||
|
||||
- infer provider availability from hardcoded lists
|
||||
- treat `openclaw` as a single-agent provider
|
||||
- re-derive auto-routing from prompt text on the APP side
|
||||
- use upstream URLs as the APP-facing contract
|
||||
|
||||
### 3.8 `xworkmate.mounts.reconcile`
|
||||
|
||||
Purpose:
|
||||
@ -381,6 +476,8 @@ Managed MCP server item shape:
|
||||
Purpose:
|
||||
|
||||
- connect a bridge runtime session to the bridge-owned production gateway route
|
||||
- from the APP perspective this still enters through the canonical bridge ACP
|
||||
path and bridge JSON-RPC methods, not a direct `openclaw` URL contract
|
||||
|
||||
Key params:
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
This document describes the bridge-only production forwarding model for `xworkmate-bridge.svc.plus`.
|
||||
|
||||
See also: [adr-unified-bridge-entrypoints.md](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-bridge/docs/architecture/adr-unified-bridge-entrypoints.md)
|
||||
|
||||
## Topology
|
||||
|
||||
```mermaid
|
||||
@ -30,27 +32,29 @@ flowchart TD
|
||||
## Three-Layer View
|
||||
|
||||
This view separates what the app sees, what the bridge owns, and what the
|
||||
real upstream production targets are.
|
||||
real upstream production targets are. The upstream ACP and gateway services
|
||||
exist independently, but for the app they are all accessed through the single
|
||||
public bridge origin: `https://xworkmate-bridge.svc.plus`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph L1["APP 视角"]
|
||||
APP["xworkmate-app"]
|
||||
APPACP["ACP 能力发现<br/>acp.capabilities"]
|
||||
APPGW["Gateway 连接<br/>xworkmate.gateway.connect"]
|
||||
APP --> APPACP
|
||||
APP --> APPGW
|
||||
APPENTRY["https://xworkmate-bridge.svc.plus<br/>统一代理入口"]
|
||||
APPMETHODS["bridge methods<br/>acp.capabilities / session.* / xworkmate.gateway.*"]
|
||||
APP --> APPENTRY
|
||||
APPENTRY --> APPMETHODS
|
||||
end
|
||||
|
||||
subgraph L2["Bridge 视角"]
|
||||
BRIDGE["xworkmate-bridge<br/>唯一上游发现真源"]
|
||||
|
||||
CAP["Bridge-owned ACP server list"]
|
||||
CAP["Bridge-owned ACP routing catalog"]
|
||||
CAP1["codex"]
|
||||
CAP2["opencode"]
|
||||
CAP3["gemini"]
|
||||
|
||||
GW["Bridge-owned gateway upstream"]
|
||||
GW["Bridge-owned gateway routing"]
|
||||
GW1["remote mode -> openclaw"]
|
||||
|
||||
BRIDGE --> CAP
|
||||
@ -69,8 +73,7 @@ flowchart LR
|
||||
U4["wss://openclaw.svc.plus<br/>reported as openclaw.svc.plus:443"]
|
||||
end
|
||||
|
||||
APPACP --> BRIDGE
|
||||
APPGW --> BRIDGE
|
||||
APPMETHODS --> BRIDGE
|
||||
|
||||
CAP1 --> U1
|
||||
CAP2 --> U2
|
||||
@ -80,6 +83,12 @@ flowchart LR
|
||||
|
||||
Important distinction:
|
||||
|
||||
- the upstream services are independent production services, not embedded
|
||||
inside the bridge
|
||||
- for the app, ACP discovery, session execution, and gateway runtime traffic
|
||||
are all proxied through `https://xworkmate-bridge.svc.plus`
|
||||
- upstream authentication is unified through
|
||||
`Authorization: Bearer $INTERNAL_SERVICE_TOKEN`
|
||||
- `acp.capabilities.providerCatalog` currently advertises only the ACP
|
||||
single-agent providers: `codex`, `opencode`, and `gemini`
|
||||
- `gateway` is not part of that provider catalog; it is exposed through the
|
||||
@ -89,20 +98,30 @@ Important distinction:
|
||||
|
||||
## Production Truth
|
||||
|
||||
Bridge owns the production map:
|
||||
The production upstream services exist independently. The bridge owns the
|
||||
routing map used to proxy app traffic to them:
|
||||
|
||||
- `codex` -> `https://acp-server.svc.plus/codex/acp/rpc`
|
||||
- `opencode` -> `https://acp-server.svc.plus/opencode/acp/rpc`
|
||||
- `gemini` -> `https://acp-server.svc.plus/gemini/acp/rpc`
|
||||
- gateway -> `wss://openclaw.svc.plus`
|
||||
|
||||
Upstream auth is bridge-internal:
|
||||
Upstream auth is unified and bridge-internal:
|
||||
|
||||
- `Authorization: Bearer $INTERNAL_SERVICE_TOKEN`
|
||||
|
||||
Canonical APP-facing paths stay on the bridge origin:
|
||||
|
||||
- `POST https://xworkmate-bridge.svc.plus/acp/rpc`
|
||||
- `GET https://xworkmate-bridge.svc.plus/acp`
|
||||
|
||||
## Invariants
|
||||
|
||||
- app-facing cloud entry is only `https://xworkmate-bridge.svc.plus`
|
||||
- app traffic reaches upstream ACP and gateway services only through the
|
||||
bridge proxy
|
||||
- upstream ACP and gateway routes use the same bearer token contract:
|
||||
`Authorization: Bearer $INTERNAL_SERVICE_TOKEN`
|
||||
- `acp.capabilities` returns the built-in production catalog
|
||||
- no production `xworkmate.providers.sync`
|
||||
- no app direct call to `acp-server.svc.plus/*`
|
||||
|
||||
126
docs/architecture/adr-unified-bridge-entrypoints.md
Normal file
126
docs/architecture/adr-unified-bridge-entrypoints.md
Normal file
@ -0,0 +1,126 @@
|
||||
# ADR: Unified Bridge Entry Points for APP Traffic
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-04-11
|
||||
|
||||
## Context
|
||||
|
||||
`xworkmate-bridge` currently proxies app traffic to four independent upstream
|
||||
production services:
|
||||
|
||||
- `codex` -> `https://acp-server.svc.plus/codex/acp/rpc`
|
||||
- `opencode` -> `https://acp-server.svc.plus/opencode/acp/rpc`
|
||||
- `gemini` -> `https://acp-server.svc.plus/gemini/acp/rpc`
|
||||
- `gateway` -> `wss://openclaw.svc.plus`
|
||||
|
||||
These upstream services exist independently, but exposing them directly as
|
||||
APP-facing endpoints creates several problems:
|
||||
|
||||
- the APP would need to know provider-specific or gateway-specific hostnames
|
||||
- routing truth would be split between URL shape and bridge-side routing logic
|
||||
- auth handling would be harder to keep consistent
|
||||
- upstream implementation details would leak into client contracts
|
||||
|
||||
The bridge already acts as the single public integration surface for ACP
|
||||
discovery, task execution, and gateway runtime operations.
|
||||
|
||||
## Decision
|
||||
|
||||
For APP traffic, the canonical public entry point is the bridge origin:
|
||||
|
||||
- `https://xworkmate-bridge.svc.plus`
|
||||
|
||||
The canonical APP-facing ACP paths are:
|
||||
|
||||
- `POST /acp/rpc`
|
||||
- `GET /acp` for WebSocket ACP
|
||||
|
||||
The APP should not depend on provider-specific public URLs such as:
|
||||
|
||||
- `/codex/acp/rpc`
|
||||
- `/opencode/acp/rpc`
|
||||
- `/gemini/acp/rpc`
|
||||
- `/openclaw/`
|
||||
|
||||
Provider choice remains bridge-owned routing, not URL-owned routing.
|
||||
|
||||
APP-facing routing should be modeled in three layers:
|
||||
|
||||
- `executionTarget`
|
||||
- `single-agent`
|
||||
- `multi-agent`
|
||||
- `gateway`
|
||||
- `singleAgentProviders`
|
||||
- `codex`
|
||||
- `opencode`
|
||||
- `gemini`
|
||||
- `gatewayProviders`
|
||||
- `local`
|
||||
- `openclaw`
|
||||
|
||||
For APP integration, `gatewayProviders` is the stable gateway-facing concept.
|
||||
The older `local` / `remote` endpoint-target split is retained only as a
|
||||
bridge-internal compatibility layer.
|
||||
|
||||
APP and UI code should consume bridge state in two phases:
|
||||
|
||||
1. `acp.capabilities`
|
||||
- discover `singleAgentProviders`
|
||||
- discover `gatewayProviders`
|
||||
2. `xworkmate.routing.resolve`
|
||||
- determine `resolvedExecutionTarget`
|
||||
- determine `resolvedProviderId` or `resolvedGatewayProviderId`
|
||||
- determine unavailable state
|
||||
|
||||
The APP should treat `resolvedProviderId` and `resolvedGatewayProviderId` as
|
||||
mutually exclusive routing outputs depending on `resolvedExecutionTarget`.
|
||||
|
||||
Gateway access remains bridge-owned via JSON-RPC methods:
|
||||
|
||||
- `xworkmate.gateway.connect`
|
||||
- `xworkmate.gateway.request`
|
||||
- `xworkmate.gateway.disconnect`
|
||||
|
||||
Upstream authentication is unified for both ACP and gateway routes:
|
||||
|
||||
- `Authorization: Bearer $INTERNAL_SERVICE_TOKEN`
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- APP integration stays stable behind one public origin
|
||||
- provider and gateway topology remain internal bridge concerns
|
||||
- auth contract is consistent across all upstream forwarding
|
||||
- bridge can change upstream mappings without changing APP contracts
|
||||
|
||||
### Trade-offs
|
||||
|
||||
- direct provider-specific bridge URLs, if exposed at all, must be treated as
|
||||
aliases or operator/debug paths, not primary client contracts
|
||||
- documentation must clearly distinguish canonical APP paths from independent
|
||||
upstream targets
|
||||
|
||||
## Path Naming Guidance
|
||||
|
||||
Use these terms consistently in docs:
|
||||
|
||||
- `canonical APP-facing path`: `/acp/rpc` and `/acp`
|
||||
- `independent upstream service`: `acp-server.svc.plus/*` and
|
||||
`wss://openclaw.svc.plus`
|
||||
- `bridge-owned routing`: bridge logic that selects and proxies to upstreams
|
||||
- `gatewayProvider`: the APP-facing identifier for a gateway backend such as
|
||||
`local` or `openclaw`
|
||||
- `endpoint target`: an internal compatibility field, not the preferred APP
|
||||
concept
|
||||
|
||||
Avoid describing upstream URLs as if the APP should call them directly.
|
||||
|
||||
If provider-specific public bridge paths are ever introduced, they should be
|
||||
documented as optional aliases only. They should not replace `/acp/rpc` as the
|
||||
canonical APP-facing contract.
|
||||
@ -14,12 +14,17 @@
|
||||
### 1. 路由发现层
|
||||
|
||||
- `acp.capabilities` 返回动态 provider 列表。
|
||||
- 至少覆盖 `opencode / codex / openclaw / gateway` 中当前环境真实可用项。
|
||||
- 至少覆盖:
|
||||
- `singleAgentProviders`: `opencode / codex / gemini`
|
||||
- `gatewayProviders`: `local / openclaw`
|
||||
- `xworkmate.routing.resolve` 根据 `taskPrompt`、`executionTarget`、`selectedSkills` 返回正确的:
|
||||
- `resolvedExecutionTarget`
|
||||
- `resolvedProviderId`
|
||||
- `resolvedGatewayProviderId`
|
||||
- `resolvedEndpointTarget`
|
||||
- `acp.capabilities` 暴露 bridge 内建的生产 provider catalog,并参与后续路由选择。
|
||||
- APP 对 gateway 的分区应以 `gatewayProviders` / `resolvedGatewayProviderId`
|
||||
为主,不以 `local / remote` 传输语义为主。
|
||||
|
||||
### 2. 典型 Case 层
|
||||
|
||||
@ -97,7 +102,11 @@ flutter test test/runtime/app_controller_single_agent_workspace_binding_regressi
|
||||
|
||||
- `acp.capabilities` 的 provider 列表来自 bridge 当前环境,而不是本地写死。
|
||||
- bridge 内建生产 catalog 包含 `codex / opencode / gemini`,且不依赖 app 侧预同步。
|
||||
- `xworkmate.routing.resolve` 在 skill / prompt / target 组合下,返回合理的 provider 与 endpoint target。
|
||||
- bridge 还会暴露 `gatewayProviders = local / openclaw`。
|
||||
- `xworkmate.routing.resolve` 在 skill / prompt / target 组合下,返回合理的
|
||||
`resolvedProviderId` 或 `resolvedGatewayProviderId`。
|
||||
- `resolvedEndpointTarget` 仅作为兼容字段保留,APP 侧 gateway 分流以
|
||||
`resolvedGatewayProviderId` 为主。
|
||||
|
||||
### 执行层断言
|
||||
|
||||
@ -145,7 +154,7 @@ flutter test test/runtime/app_controller_single_agent_workspace_binding_regressi
|
||||
|
||||
额外约定:
|
||||
|
||||
- `openclaw` 作为扩展路由的一部分,先按 bridge 发现结果驱动。
|
||||
- `openclaw` 作为 `gatewayProviders` 之一,按 bridge 发现结果驱动。
|
||||
- 如果当前环境没有暴露某个 provider,测试允许 `skip`,但要保留断言入口和记录。
|
||||
- UI 本轮不改结构,只验证 provider 列表来源与展示结果是否随 bridge 动态变化。
|
||||
|
||||
|
||||
@ -44,6 +44,9 @@ func buildResolvedExecutionParams(
|
||||
if strings.TrimSpace(resolved.ResolvedProviderID) != "" {
|
||||
next["provider"] = strings.TrimSpace(resolved.ResolvedProviderID)
|
||||
}
|
||||
if strings.TrimSpace(resolved.ResolvedGatewayProviderID) != "" {
|
||||
next["gatewayProvider"] = strings.TrimSpace(resolved.ResolvedGatewayProviderID)
|
||||
}
|
||||
if strings.TrimSpace(resolved.ResolvedModel) != "" {
|
||||
next["model"] = strings.TrimSpace(resolved.ResolvedModel)
|
||||
}
|
||||
@ -53,6 +56,7 @@ func buildResolvedExecutionParams(
|
||||
next["resolvedExecutionTarget"] = resolved.ResolvedExecutionTarget
|
||||
next["resolvedEndpointTarget"] = resolved.ResolvedEndpointTarget
|
||||
next["resolvedProviderId"] = resolved.ResolvedProviderID
|
||||
next["resolvedGatewayProviderId"] = resolved.ResolvedGatewayProviderID
|
||||
next["resolvedModel"] = resolved.ResolvedModel
|
||||
next["resolvedSkills"] = append([]string(nil), resolved.ResolvedSkills...)
|
||||
return next
|
||||
@ -178,6 +182,7 @@ func sanitizeExternalACPParams(method string, params map[string]any) map[string]
|
||||
delete(next, "resolvedExecutionTarget")
|
||||
delete(next, "resolvedEndpointTarget")
|
||||
delete(next, "resolvedProviderId")
|
||||
delete(next, "resolvedGatewayProviderId")
|
||||
delete(next, "resolvedModel")
|
||||
delete(next, "resolvedSkills")
|
||||
delete(next, inboundAuthorizationHeaderKey)
|
||||
|
||||
@ -3,6 +3,7 @@ package acp
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"xworkmate-bridge/internal/router"
|
||||
"xworkmate-bridge/internal/shared"
|
||||
)
|
||||
|
||||
@ -112,3 +113,16 @@ func providerLabel(provider syncedProvider) string {
|
||||
}
|
||||
return provider.ProviderID
|
||||
}
|
||||
|
||||
func availableGatewayProviderCatalog() []map[string]any {
|
||||
return []map[string]any{
|
||||
{
|
||||
"providerId": router.GatewayProviderLocal,
|
||||
"label": "Local",
|
||||
},
|
||||
{
|
||||
"providerId": router.GatewayProviderOpenClaw,
|
||||
"label": "OpenClaw",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,9 +38,16 @@ func TestCapabilitiesExposeBuiltInProductionProviderCatalog(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("expected providerCatalog array, got %#v", result)
|
||||
}
|
||||
gatewayProviders, ok := result["gatewayProviders"].([]map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected gatewayProviders array, got %#v", result)
|
||||
}
|
||||
if len(providerCatalog) != 3 {
|
||||
t.Fatalf("expected 3 built-in providers, got %#v", providerCatalog)
|
||||
}
|
||||
if len(gatewayProviders) != 2 {
|
||||
t.Fatalf("expected 2 built-in gateway providers, got %#v", gatewayProviders)
|
||||
}
|
||||
wantOrder := []string{"codex", "opencode", "gemini"}
|
||||
wantLabels := []string{"Codex", "OpenCode", "Gemini"}
|
||||
for index, want := range wantOrder {
|
||||
@ -51,6 +58,16 @@ func TestCapabilitiesExposeBuiltInProductionProviderCatalog(t *testing.T) {
|
||||
t.Fatalf("expected label %q at index %d, got %#v", wantLabels[index], index, providerCatalog)
|
||||
}
|
||||
}
|
||||
wantGatewayOrder := []string{"local", "openclaw"}
|
||||
wantGatewayLabels := []string{"Local", "OpenClaw"}
|
||||
for index, want := range wantGatewayOrder {
|
||||
if got := gatewayProviders[index]["providerId"]; got != want {
|
||||
t.Fatalf("expected gateway provider %q at index %d, got %#v", want, index, gatewayProviders)
|
||||
}
|
||||
if got := gatewayProviders[index]["label"]; got != wantGatewayLabels[index] {
|
||||
t.Fatalf("expected gateway label %q at index %d, got %#v", wantGatewayLabels[index], index, gatewayProviders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersSyncMethodIsRemovedFromProductionFlow(t *testing.T) {
|
||||
|
||||
@ -27,23 +27,24 @@ func resolveRoutingMetadataWithProviders(
|
||||
|
||||
resolver := router.NewResolver()
|
||||
result := resolver.Resolve(router.Request{
|
||||
Prompt: strings.TrimSpace(sharedString(params, "taskPrompt")),
|
||||
WorkingDirectory: strings.TrimSpace(sharedString(params, "workingDirectory")),
|
||||
RoutingMode: strings.TrimSpace(sharedString(routingParams, "routingMode")),
|
||||
PreferredGatewayTarget: strings.TrimSpace(sharedString(routingParams, "preferredGatewayTarget")),
|
||||
ExplicitExecutionTarget: strings.TrimSpace(sharedString(routingParams, "explicitExecutionTarget")),
|
||||
ExplicitProviderID: strings.TrimSpace(sharedString(routingParams, "explicitProviderId")),
|
||||
ExplicitModel: strings.TrimSpace(sharedString(routingParams, "explicitModel")),
|
||||
ExplicitSkills: parseRoutingStringSlice(routingParams["explicitSkills"]),
|
||||
AllowSkillInstall: parseBool(routingParams["allowSkillInstall"]),
|
||||
Prompt: strings.TrimSpace(sharedString(params, "taskPrompt")),
|
||||
WorkingDirectory: strings.TrimSpace(sharedString(params, "workingDirectory")),
|
||||
RoutingMode: strings.TrimSpace(sharedString(routingParams, "routingMode")),
|
||||
PreferredGatewayTarget: strings.TrimSpace(sharedString(routingParams, "preferredGatewayTarget")),
|
||||
PreferredGatewayProviderID: strings.TrimSpace(sharedString(routingParams, "preferredGatewayProviderId")),
|
||||
ExplicitExecutionTarget: strings.TrimSpace(sharedString(routingParams, "explicitExecutionTarget")),
|
||||
ExplicitProviderID: strings.TrimSpace(sharedString(routingParams, "explicitProviderId")),
|
||||
ExplicitModel: strings.TrimSpace(sharedString(routingParams, "explicitModel")),
|
||||
ExplicitSkills: parseRoutingStringSlice(routingParams["explicitSkills"]),
|
||||
AllowSkillInstall: parseBool(routingParams["allowSkillInstall"]),
|
||||
InstallApproval: skills.InstallApproval{
|
||||
RequestID: strings.TrimSpace(sharedString(installApproval, "requestId")),
|
||||
ApprovedSkillKeys: parseRoutingStringSlice(installApproval["approvedSkillKeys"]),
|
||||
},
|
||||
AvailableSkills: parseRoutingSkillCandidates(routingParams["availableSkills"]),
|
||||
AvailableSkills: parseRoutingSkillCandidates(routingParams["availableSkills"]),
|
||||
AvailableProviders: append([]string(nil), availableProviders...),
|
||||
AIGatewayBaseURL: strings.TrimSpace(sharedString(params, "aiGatewayBaseUrl")),
|
||||
AIGatewayAPIKey: strings.TrimSpace(sharedString(params, "aiGatewayApiKey")),
|
||||
AIGatewayBaseURL: strings.TrimSpace(sharedString(params, "aiGatewayBaseUrl")),
|
||||
AIGatewayAPIKey: strings.TrimSpace(sharedString(params, "aiGatewayApiKey")),
|
||||
})
|
||||
return result, true
|
||||
}
|
||||
@ -55,6 +56,7 @@ func mergeRoutingResponse(response map[string]any, result router.Result) map[str
|
||||
response["resolvedExecutionTarget"] = result.ResolvedExecutionTarget
|
||||
response["resolvedEndpointTarget"] = result.ResolvedEndpointTarget
|
||||
response["resolvedProviderId"] = result.ResolvedProviderID
|
||||
response["resolvedGatewayProviderId"] = result.ResolvedGatewayProviderID
|
||||
response["resolvedModel"] = result.ResolvedModel
|
||||
response["resolvedSkills"] = append([]string(nil), result.ResolvedSkills...)
|
||||
response["skillResolutionSource"] = result.SkillResolutionSource
|
||||
|
||||
@ -58,6 +58,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
|
||||
name string
|
||||
prompt string
|
||||
expectedExecutionTarget string
|
||||
expectedGatewayProviderID string
|
||||
expectedSkillSource string
|
||||
expectedResolvedSkill string
|
||||
expectedNeedsSkillInstall bool
|
||||
@ -101,6 +102,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
|
||||
name: "image-cog",
|
||||
prompt: "use image-cog to generate consistent characters",
|
||||
expectedExecutionTarget: "gateway",
|
||||
expectedGatewayProviderID: "local",
|
||||
expectedSkillSource: "find_skills",
|
||||
expectedNeedsSkillInstall: true,
|
||||
},
|
||||
@ -108,6 +110,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
|
||||
name: "image-video-generation-editting",
|
||||
prompt: "wan 图生视频并做视频编辑",
|
||||
expectedExecutionTarget: "gateway",
|
||||
expectedGatewayProviderID: "local",
|
||||
expectedSkillSource: "find_skills",
|
||||
expectedNeedsSkillInstall: true,
|
||||
},
|
||||
@ -115,15 +118,17 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
|
||||
name: "video-translator",
|
||||
prompt: "translate video subtitles and dub the clip",
|
||||
expectedExecutionTarget: "gateway",
|
||||
expectedGatewayProviderID: "local",
|
||||
expectedSkillSource: "find_skills",
|
||||
expectedNeedsSkillInstall: true,
|
||||
},
|
||||
{
|
||||
name: "browser-search-news",
|
||||
prompt: "跨浏览器执行并搜索最新资讯采集结果",
|
||||
expectedExecutionTarget: "gateway",
|
||||
expectedSkillSource: "local_match",
|
||||
expectedResolvedSkill: "Browser Automation",
|
||||
name: "browser-search-news",
|
||||
prompt: "跨浏览器执行并搜索最新资讯采集结果",
|
||||
expectedExecutionTarget: "gateway",
|
||||
expectedGatewayProviderID: "local",
|
||||
expectedSkillSource: "local_match",
|
||||
expectedResolvedSkill: "Browser Automation",
|
||||
},
|
||||
}
|
||||
|
||||
@ -149,6 +154,11 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
|
||||
if got := result["resolvedExecutionTarget"]; got != tc.expectedExecutionTarget {
|
||||
t.Fatalf("expected execution target %q, got %#v", tc.expectedExecutionTarget, got)
|
||||
}
|
||||
if tc.expectedGatewayProviderID != "" {
|
||||
if got := result["resolvedGatewayProviderId"]; got != tc.expectedGatewayProviderID {
|
||||
t.Fatalf("expected gateway provider %q, got %#v", tc.expectedGatewayProviderID, got)
|
||||
}
|
||||
}
|
||||
if got := result["skillResolutionSource"]; got != tc.expectedSkillSource {
|
||||
t.Fatalf("expected skill source %q, got %#v", tc.expectedSkillSource, got)
|
||||
}
|
||||
|
||||
@ -300,19 +300,22 @@ func (s *Server) handleRequest(
|
||||
switch method {
|
||||
case "acp.capabilities":
|
||||
providerCatalog := s.availableProviderCatalog()
|
||||
gatewayProviders := availableGatewayProviderCatalog()
|
||||
singleAgent := len(providerCatalog) > 0
|
||||
multiAgent := shared.BoolArg(
|
||||
shared.EnvOrDefault("ACP_MULTI_AGENT_ENABLED", "true"),
|
||||
true,
|
||||
)
|
||||
result := map[string]any{
|
||||
"singleAgent": singleAgent,
|
||||
"multiAgent": multiAgent,
|
||||
"providerCatalog": providerCatalog,
|
||||
"singleAgent": singleAgent,
|
||||
"multiAgent": multiAgent,
|
||||
"providerCatalog": providerCatalog,
|
||||
"gatewayProviders": gatewayProviders,
|
||||
"capabilities": map[string]any{
|
||||
"single_agent": singleAgent,
|
||||
"multi_agent": multiAgent,
|
||||
"providerCatalog": providerCatalog,
|
||||
"single_agent": singleAgent,
|
||||
"multi_agent": multiAgent,
|
||||
"providerCatalog": providerCatalog,
|
||||
"gatewayProviders": gatewayProviders,
|
||||
},
|
||||
}
|
||||
return result, nil
|
||||
|
||||
@ -20,39 +20,44 @@ const (
|
||||
EndpointTargetSingleAgent = "singleAgent"
|
||||
EndpointTargetLocal = "local"
|
||||
EndpointTargetRemote = "remote"
|
||||
|
||||
GatewayProviderLocal = "local"
|
||||
GatewayProviderOpenClaw = "openclaw"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
Prompt string
|
||||
WorkingDirectory string
|
||||
RoutingMode string
|
||||
PreferredGatewayTarget string
|
||||
ExplicitExecutionTarget string
|
||||
ExplicitProviderID string
|
||||
ExplicitModel string
|
||||
ExplicitSkills []string
|
||||
AllowSkillInstall bool
|
||||
InstallApproval skills.InstallApproval
|
||||
AvailableSkills []skills.Candidate
|
||||
AvailableProviders []string
|
||||
AIGatewayBaseURL string
|
||||
AIGatewayAPIKey string
|
||||
Prompt string
|
||||
WorkingDirectory string
|
||||
RoutingMode string
|
||||
PreferredGatewayTarget string
|
||||
PreferredGatewayProviderID string
|
||||
ExplicitExecutionTarget string
|
||||
ExplicitProviderID string
|
||||
ExplicitModel string
|
||||
ExplicitSkills []string
|
||||
AllowSkillInstall bool
|
||||
InstallApproval skills.InstallApproval
|
||||
AvailableSkills []skills.Candidate
|
||||
AvailableProviders []string
|
||||
AIGatewayBaseURL string
|
||||
AIGatewayAPIKey string
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
ResolvedExecutionTarget string
|
||||
ResolvedEndpointTarget string
|
||||
ResolvedProviderID string
|
||||
ResolvedModel string
|
||||
ResolvedSkills []string
|
||||
SkillResolutionSource string
|
||||
SkillCandidates []skills.Candidate
|
||||
NeedsSkillInstall bool
|
||||
SkillInstallRequestID string
|
||||
MemorySources []memory.Source
|
||||
Unavailable bool
|
||||
UnavailableCode string
|
||||
UnavailableMessage string
|
||||
ResolvedExecutionTarget string
|
||||
ResolvedEndpointTarget string
|
||||
ResolvedProviderID string
|
||||
ResolvedGatewayProviderID string
|
||||
ResolvedModel string
|
||||
ResolvedSkills []string
|
||||
SkillResolutionSource string
|
||||
SkillCandidates []skills.Candidate
|
||||
NeedsSkillInstall bool
|
||||
SkillInstallRequestID string
|
||||
MemorySources []memory.Source
|
||||
Unavailable bool
|
||||
UnavailableCode string
|
||||
UnavailableMessage string
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
@ -81,7 +86,7 @@ func (r Resolver) Resolve(req Request) Result {
|
||||
MemorySources: mem.Sources,
|
||||
}
|
||||
|
||||
result.ResolvedExecutionTarget, result.ResolvedEndpointTarget = r.resolveExecution(req, mem.Preferences)
|
||||
result.ResolvedExecutionTarget, result.ResolvedEndpointTarget, result.ResolvedGatewayProviderID = r.resolveExecution(req, mem.Preferences)
|
||||
result.ResolvedProviderID, result.Unavailable, result.UnavailableCode, result.UnavailableMessage = resolveProvider(
|
||||
req,
|
||||
mem.Preferences,
|
||||
@ -124,7 +129,10 @@ func (r Resolver) Resolve(req Request) Result {
|
||||
}
|
||||
if result.ResolvedEndpointTarget == "" {
|
||||
if result.ResolvedExecutionTarget == ExecutionTargetGateway {
|
||||
result.ResolvedEndpointTarget = normalizeGatewayTarget(req.PreferredGatewayTarget)
|
||||
result.ResolvedGatewayProviderID, result.ResolvedEndpointTarget = resolveGatewayRouting(
|
||||
req.PreferredGatewayProviderID,
|
||||
req.PreferredGatewayTarget,
|
||||
)
|
||||
} else {
|
||||
result.ResolvedEndpointTarget = EndpointTargetSingleAgent
|
||||
}
|
||||
@ -132,10 +140,14 @@ func (r Resolver) Resolve(req Request) Result {
|
||||
return result
|
||||
}
|
||||
|
||||
func (r Resolver) resolveExecution(req Request, prefs memory.Preferences) (string, string) {
|
||||
func (r Resolver) resolveExecution(req Request, prefs memory.Preferences) (string, string, string) {
|
||||
explicit := strings.TrimSpace(req.ExplicitExecutionTarget)
|
||||
if strings.EqualFold(strings.TrimSpace(req.RoutingMode), RoutingModeExplicit) && explicit != "" {
|
||||
return mapExplicitTarget(explicit)
|
||||
return mapExplicitTarget(
|
||||
explicit,
|
||||
req.PreferredGatewayProviderID,
|
||||
req.PreferredGatewayTarget,
|
||||
)
|
||||
}
|
||||
|
||||
prompt := normalize(req.Prompt)
|
||||
@ -146,40 +158,56 @@ func (r Resolver) resolveExecution(req Request, prefs memory.Preferences) (strin
|
||||
|
||||
switch {
|
||||
case localTask && complexTask:
|
||||
return ExecutionTargetMultiAgent, EndpointTargetSingleAgent
|
||||
return ExecutionTargetMultiAgent, EndpointTargetSingleAgent, ""
|
||||
case onlineTask && complexTask:
|
||||
return ExecutionTargetMultiAgent, EndpointTargetSingleAgent
|
||||
return ExecutionTargetMultiAgent, EndpointTargetSingleAgent, ""
|
||||
case localTask:
|
||||
return ExecutionTargetSingleAgent, EndpointTargetSingleAgent
|
||||
return ExecutionTargetSingleAgent, EndpointTargetSingleAgent, ""
|
||||
case onlineTask:
|
||||
return ExecutionTargetGateway, normalizeGatewayTarget(req.PreferredGatewayTarget)
|
||||
providerID, endpointTarget := resolveGatewayRouting(
|
||||
req.PreferredGatewayProviderID,
|
||||
req.PreferredGatewayTarget,
|
||||
)
|
||||
return ExecutionTargetGateway, endpointTarget, providerID
|
||||
case complexTask:
|
||||
return ExecutionTargetMultiAgent, EndpointTargetSingleAgent
|
||||
return ExecutionTargetMultiAgent, EndpointTargetSingleAgent, ""
|
||||
}
|
||||
|
||||
switch normalizeExecutionTarget(r.classify(req)) {
|
||||
case ExecutionTargetGateway:
|
||||
return ExecutionTargetGateway, normalizeGatewayTarget(req.PreferredGatewayTarget)
|
||||
providerID, endpointTarget := resolveGatewayRouting(
|
||||
req.PreferredGatewayProviderID,
|
||||
req.PreferredGatewayTarget,
|
||||
)
|
||||
return ExecutionTargetGateway, endpointTarget, providerID
|
||||
case ExecutionTargetMultiAgent:
|
||||
return ExecutionTargetMultiAgent, EndpointTargetSingleAgent
|
||||
return ExecutionTargetMultiAgent, EndpointTargetSingleAgent, ""
|
||||
case ExecutionTargetSingleAgent:
|
||||
return ExecutionTargetSingleAgent, EndpointTargetSingleAgent
|
||||
return ExecutionTargetSingleAgent, EndpointTargetSingleAgent, ""
|
||||
}
|
||||
|
||||
switch normalizeExecutionTarget(strings.TrimSpace(prefs.PreferredRoute)) {
|
||||
case ExecutionTargetGateway:
|
||||
return ExecutionTargetGateway, normalizeGatewayTarget(req.PreferredGatewayTarget)
|
||||
providerID, endpointTarget := resolveGatewayRouting(
|
||||
req.PreferredGatewayProviderID,
|
||||
req.PreferredGatewayTarget,
|
||||
)
|
||||
return ExecutionTargetGateway, endpointTarget, providerID
|
||||
case ExecutionTargetMultiAgent:
|
||||
return ExecutionTargetMultiAgent, EndpointTargetSingleAgent
|
||||
return ExecutionTargetMultiAgent, EndpointTargetSingleAgent, ""
|
||||
case ExecutionTargetSingleAgent:
|
||||
if len(normalizeProviders(req.AvailableProviders)) > 0 {
|
||||
return ExecutionTargetSingleAgent, EndpointTargetSingleAgent
|
||||
return ExecutionTargetSingleAgent, EndpointTargetSingleAgent, ""
|
||||
}
|
||||
}
|
||||
if len(normalizeProviders(req.AvailableProviders)) > 0 {
|
||||
return ExecutionTargetSingleAgent, EndpointTargetSingleAgent
|
||||
return ExecutionTargetSingleAgent, EndpointTargetSingleAgent, ""
|
||||
}
|
||||
return ExecutionTargetGateway, normalizeGatewayTarget(req.PreferredGatewayTarget)
|
||||
providerID, endpointTarget := resolveGatewayRouting(
|
||||
req.PreferredGatewayProviderID,
|
||||
req.PreferredGatewayTarget,
|
||||
)
|
||||
return ExecutionTargetGateway, endpointTarget, providerID
|
||||
}
|
||||
|
||||
func (r Resolver) classify(req Request) string {
|
||||
@ -193,27 +221,75 @@ func (r Resolver) classify(req Request) string {
|
||||
}))
|
||||
}
|
||||
|
||||
func mapExplicitTarget(value string) (string, string) {
|
||||
func mapExplicitTarget(
|
||||
value string,
|
||||
preferredGatewayProviderID string,
|
||||
preferredGatewayTarget string,
|
||||
) (string, string, string) {
|
||||
switch strings.TrimSpace(value) {
|
||||
case EndpointTargetLocal:
|
||||
return ExecutionTargetGateway, EndpointTargetLocal
|
||||
return ExecutionTargetGateway, EndpointTargetLocal, GatewayProviderLocal
|
||||
case EndpointTargetRemote:
|
||||
return ExecutionTargetGateway, EndpointTargetRemote
|
||||
return ExecutionTargetGateway, EndpointTargetRemote, GatewayProviderOpenClaw
|
||||
case "multiAgent", ExecutionTargetMultiAgent:
|
||||
return ExecutionTargetMultiAgent, EndpointTargetSingleAgent
|
||||
return ExecutionTargetMultiAgent, EndpointTargetSingleAgent, ""
|
||||
case EndpointTargetSingleAgent, ExecutionTargetSingleAgent:
|
||||
return ExecutionTargetSingleAgent, EndpointTargetSingleAgent
|
||||
return ExecutionTargetSingleAgent, EndpointTargetSingleAgent, ""
|
||||
case ExecutionTargetGateway:
|
||||
providerID, endpointTarget := resolveGatewayRouting(
|
||||
preferredGatewayProviderID,
|
||||
preferredGatewayTarget,
|
||||
)
|
||||
return ExecutionTargetGateway, endpointTarget, providerID
|
||||
default:
|
||||
return ExecutionTargetSingleAgent, EndpointTargetSingleAgent
|
||||
return ExecutionTargetSingleAgent, EndpointTargetSingleAgent, ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeGatewayTarget(value string) string {
|
||||
switch strings.TrimSpace(value) {
|
||||
case EndpointTargetLocal, "":
|
||||
return EndpointTargetLocal
|
||||
_, endpointTarget := resolveGatewayRouting("", value)
|
||||
return endpointTarget
|
||||
}
|
||||
|
||||
func resolveGatewayRouting(preferredGatewayProviderID, preferredGatewayTarget string) (string, string) {
|
||||
providerID := normalizeGatewayProvider(preferredGatewayProviderID)
|
||||
if providerID == "" {
|
||||
providerID = gatewayProviderFromEndpointTarget(preferredGatewayTarget)
|
||||
}
|
||||
if providerID == "" {
|
||||
providerID = GatewayProviderLocal
|
||||
}
|
||||
return providerID, endpointTargetForGatewayProvider(providerID)
|
||||
}
|
||||
|
||||
func normalizeGatewayProvider(value string) string {
|
||||
switch normalize(value) {
|
||||
case GatewayProviderLocal:
|
||||
return GatewayProviderLocal
|
||||
case GatewayProviderOpenClaw:
|
||||
return GatewayProviderOpenClaw
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func gatewayProviderFromEndpointTarget(value string) string {
|
||||
switch strings.TrimSpace(value) {
|
||||
case EndpointTargetRemote:
|
||||
return GatewayProviderOpenClaw
|
||||
case EndpointTargetLocal, "":
|
||||
return GatewayProviderLocal
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func endpointTargetForGatewayProvider(providerID string) string {
|
||||
switch normalizeGatewayProvider(providerID) {
|
||||
case GatewayProviderOpenClaw:
|
||||
return EndpointTargetRemote
|
||||
default:
|
||||
return EndpointTargetLocal
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -96,6 +96,9 @@ func TestResolveAutoOnlineTaskToGateway(t *testing.T) {
|
||||
if result.ResolvedEndpointTarget != EndpointTargetLocal {
|
||||
t.Fatalf("expected local gateway target, got %#v", result)
|
||||
}
|
||||
if result.ResolvedGatewayProviderID != GatewayProviderLocal {
|
||||
t.Fatalf("expected local gateway provider, got %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveComplexTaskUpgradesToMultiAgent(t *testing.T) {
|
||||
@ -133,4 +136,30 @@ func TestResolveUsesClassifierForBoundarySamples(t *testing.T) {
|
||||
if result.ResolvedEndpointTarget != EndpointTargetLocal {
|
||||
t.Fatalf("expected local endpoint target, got %#v", result)
|
||||
}
|
||||
if result.ResolvedGatewayProviderID != GatewayProviderLocal {
|
||||
t.Fatalf("expected local gateway provider, got %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveGatewayProviderMapsOpenClawToRemoteEndpoint(t *testing.T) {
|
||||
resolver := Resolver{
|
||||
SkillFinder: skills.StaticFinder{},
|
||||
SkillInstaller: nil,
|
||||
MemoryService: memory.Service{},
|
||||
}
|
||||
|
||||
result := resolver.Resolve(Request{
|
||||
Prompt: "search the web for latest news",
|
||||
PreferredGatewayProviderID: GatewayProviderOpenClaw,
|
||||
})
|
||||
|
||||
if result.ResolvedExecutionTarget != ExecutionTargetGateway {
|
||||
t.Fatalf("expected gateway route, got %#v", result)
|
||||
}
|
||||
if result.ResolvedGatewayProviderID != GatewayProviderOpenClaw {
|
||||
t.Fatalf("expected openclaw gateway provider, got %#v", result)
|
||||
}
|
||||
if result.ResolvedEndpointTarget != EndpointTargetRemote {
|
||||
t.Fatalf("expected remote endpoint target for openclaw, got %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user