Skip to content

Commit 2d59395

Browse files
committed
refactor: move provider endpoint metadata into manifests
1 parent 67ebc43 commit 2d59395

13 files changed

Lines changed: 247 additions & 86 deletions

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,12 @@
128128
- Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch.
129129
- Type-check/build: `pnpm build`
130130
- TypeScript checks are split by architecture boundary:
131-
- `pnpm tsgo` / `pnpm tsgo:core`: core production graph (`src/`, `ui/`, `packages/`; no `extensions/`).
131+
- `pnpm tsgo` / `pnpm tsgo:core`: core production roots (`src/`, `ui/`, `packages/`; no `extensions/` include roots).
132132
- `pnpm tsgo:core:test`: core colocated tests.
133133
- `pnpm tsgo:extensions`: bundled extension production graph.
134134
- `pnpm tsgo:extensions:test`: bundled extension colocated tests.
135135
- `pnpm tsgo:all`: every TypeScript graph above; this is what `pnpm check` runs.
136-
- `pnpm tsgo:profile [core-test|extensions-test|--all]`: profile fresh graph cost into `.artifacts/tsgo-profile/`.
136+
- `pnpm tsgo:profile [core-test|extensions-test|--all]`: profile fresh graph cost into `.artifacts/tsgo-profile/`; if a core graph lists `extensions/<id>/`, treat that as boundary/perf debt from imports (usually plugin-sdk facades or shared helpers pulling extension sources).
137137
- Narrow aliases remain for local loops: `pnpm tsgo:test:src`, `pnpm tsgo:test:ui`, `pnpm tsgo:test:packages`.
138138
- Do not add `tsc --noEmit`, `typecheck`, or `check:types` lanes for repo type checking. Use `tsgo` graphs. `tsc` is allowed only when emitting declaration/package-boundary compatibility artifacts that `tsgo` does not replace.
139139
- Boundary rule: core must not know extension implementation details. Extensions hook into core through manifests, registries, capabilities, and public `openclaw/plugin-sdk/*` contracts. If you find core production code naming a specific extension, or a core test that is really testing extension-owned behavior, call it out and prefer moving coverage/logic to the owning extension or a generic contract test.

docs/plugins/manifest.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ Those belong in your plugin code and `package.json`.
9595
"modelSupport": {
9696
"modelPrefixes": ["router-"]
9797
},
98+
"providerEndpoints": [
99+
{
100+
"endpointClass": "xai-native",
101+
"hosts": ["api.x.ai"]
102+
}
103+
],
98104
"cliBackends": ["openrouter-cli"],
99105
"syntheticAuthRefs": ["openrouter-cli"],
100106
"providerAuthEnvVars": {
@@ -153,6 +159,7 @@ Those belong in your plugin code and `package.json`.
153159
| `channels` | No | `string[]` | Channel ids owned by this plugin. Used for discovery and config validation. |
154160
| `providers` | No | `string[]` | Provider ids owned by this plugin. |
155161
| `modelSupport` | No | `object` | Manifest-owned shorthand model-family metadata used to auto-load the plugin before runtime. |
162+
| `providerEndpoints` | No | `object[]` | Manifest-owned endpoint host/baseUrl metadata for provider routes that core must classify before provider runtime loads. |
156163
| `cliBackends` | No | `string[]` | CLI inference backend ids owned by this plugin. Used for startup auto-activation from explicit config refs. |
157164
| `syntheticAuthRefs` | No | `string[]` | Provider or CLI backend refs whose plugin-owned synthetic auth hook should be probed during cold model discovery before runtime loads. |
158165
| `nonSecretAuthMarkers` | No | `string[]` | Bundled-plugin-owned placeholder API key values that represent non-secret local, OAuth, or ambient credential state. |
@@ -602,6 +609,9 @@ See [Configuration reference](/gateway/configuration) for the full `plugins.*` s
602609
- `providerAuthAliases` lets provider variants reuse another provider's auth
603610
env vars, auth profiles, config-backed auth, and API-key onboarding choice
604611
without hardcoding that relationship in core.
612+
- `providerEndpoints` lets provider plugins own simple endpoint host/baseUrl
613+
matching metadata. Use it only for endpoint classes core already supports;
614+
the plugin still owns runtime behavior.
605615
- `syntheticAuthRefs` is the cheap metadata path for provider-owned synthetic
606616
auth hooks that must be visible to cold model discovery before the runtime
607617
registry exists. Only list refs whose runtime provider or CLI backend actually

extensions/xai/api.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
getModelProviderHint,
33
normalizeNativeXaiModelId,
44
normalizeProviderId,
5-
resolveProviderEndpoint,
65
} from "openclaw/plugin-sdk/provider-model-shared";
76
import {
87
applyXaiModelCompat,
@@ -30,9 +29,19 @@ export {
3029
resolveXaiModelCompatPatch,
3130
} from "openclaw/plugin-sdk/provider-tools";
3231

32+
const XAI_NATIVE_ENDPOINT_HOSTS = new Set(["api.x.ai", "api.grok.x.ai"]);
33+
34+
function resolveHostname(value: string): string | undefined {
35+
try {
36+
return new URL(value).hostname.toLowerCase();
37+
} catch {
38+
return undefined;
39+
}
40+
}
41+
3342
function isXaiNativeEndpoint(baseUrl: unknown): boolean {
3443
return (
35-
typeof baseUrl === "string" && resolveProviderEndpoint(baseUrl).endpointClass === "xai-native"
44+
typeof baseUrl === "string" && XAI_NATIVE_ENDPOINT_HOSTS.has(resolveHostname(baseUrl) ?? "")
3645
);
3746
}
3847

extensions/xai/openclaw.plugin.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
"id": "xai",
33
"enabledByDefault": true,
44
"providers": ["xai"],
5+
"providerEndpoints": [
6+
{
7+
"endpointClass": "xai-native",
8+
"hosts": ["api.x.ai", "api.grok.x.ai"]
9+
}
10+
],
511
"syntheticAuthRefs": ["xai"],
612
"providerAuthEnvVars": {
713
"xai": ["XAI_API_KEY"]

src/agents/provider-attribution.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
12
import {
23
normalizeOptionalLowercaseString,
34
normalizeOptionalString,
@@ -131,6 +132,7 @@ const OPENAI_RESPONSES_APIS = new Set([
131132
]);
132133
const OPENAI_RESPONSES_PROVIDERS = new Set(["openai", "azure-openai", "azure-openai-responses"]);
133134
const MOONSHOT_COMPAT_PROVIDERS = new Set(["moonshot", "kimi"]);
135+
const MANIFEST_PROVIDER_ENDPOINT_CLASSES = new Set<ProviderEndpointClass>(["xai-native"]);
134136

135137
function formatOpenClawUserAgent(version: string): string {
136138
return `${OPENCLAW_ATTRIBUTION_ORIGINATOR}/${version}`;
@@ -186,6 +188,36 @@ function normalizeComparableBaseUrl(value: string): string | undefined {
186188
}
187189
}
188190

191+
function isManifestProviderEndpointClass(value: string): value is ProviderEndpointClass {
192+
return MANIFEST_PROVIDER_ENDPOINT_CLASSES.has(value as ProviderEndpointClass);
193+
}
194+
195+
function resolveManifestProviderEndpoint(params: {
196+
host: string;
197+
normalizedBaseUrl?: string;
198+
}): ProviderEndpointResolution | undefined {
199+
const registry = loadPluginManifestRegistry({ cache: true });
200+
for (const plugin of registry.plugins) {
201+
for (const endpoint of plugin.providerEndpoints ?? []) {
202+
if (!isManifestProviderEndpointClass(endpoint.endpointClass)) {
203+
continue;
204+
}
205+
if (endpoint.hosts?.some((host) => host.toLowerCase() === params.host)) {
206+
return { endpointClass: endpoint.endpointClass, hostname: params.host };
207+
}
208+
if (
209+
params.normalizedBaseUrl &&
210+
endpoint.baseUrls?.some(
211+
(baseUrl) => normalizeComparableBaseUrl(baseUrl) === params.normalizedBaseUrl,
212+
)
213+
) {
214+
return { endpointClass: endpoint.endpointClass, hostname: params.host };
215+
}
216+
}
217+
}
218+
return undefined;
219+
}
220+
189221
function isLocalEndpointHost(host: string): boolean {
190222
return (
191223
LOCAL_ENDPOINT_HOSTS.has(host) ||
@@ -246,9 +278,6 @@ export function resolveProviderEndpoint(
246278
if (host === "openrouter.ai" || host.endsWith(".openrouter.ai")) {
247279
return { endpointClass: "openrouter", hostname: host };
248280
}
249-
if (host === "api.x.ai" || host === "api.grok.x.ai") {
250-
return { endpointClass: "xai-native", hostname: host };
251-
}
252281
if (host === "api.z.ai") {
253282
return { endpointClass: "zai-native", hostname: host };
254283
}
@@ -273,6 +302,10 @@ export function resolveProviderEndpoint(
273302
googleVertexRegion: googleVertexHost[1],
274303
};
275304
}
305+
const manifestEndpoint = resolveManifestProviderEndpoint({ host, normalizedBaseUrl });
306+
if (manifestEndpoint) {
307+
return manifestEndpoint;
308+
}
276309
if (isLocalEndpointHost(host)) {
277310
return { endpointClass: "local", hostname: host };
278311
}

0 commit comments

Comments
 (0)