@openclaw/cloudflare-ai-gateway-provider 0.0.0 → 2026.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/api.js +4 -0
- package/dist/catalog-provider.js +45 -0
- package/dist/index.js +169 -0
- package/dist/models.js +42 -0
- package/dist/onboard.js +60 -0
- package/dist/stream-wrappers.js +30 -0
- package/npm-shrinkwrap.json +12 -0
- package/openclaw.plugin.json +49 -0
- package/package.json +44 -8
package/README.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# OpenClaw Cloudflare AI Gateway Provider
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official OpenClaw provider plugin for Cloudflare AI Gateway.
|
|
4
|
+
|
|
5
|
+
Install from OpenClaw:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install @openclaw/cloudflare-ai-gateway-provider
|
|
9
|
+
openclaw gateway restart
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
See <https://docs.openclaw.ai/providers/cloudflare-ai-gateway> for setup and configuration.
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID, CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF, CLOUDFLARE_AI_GATEWAY_PROVIDER_ID, buildCloudflareAiGatewayModelDefinition, resolveCloudflareAiGatewayBaseUrl } from "./models.js";
|
|
2
|
+
import { buildCloudflareAiGatewayCatalogProvider } from "./catalog-provider.js";
|
|
3
|
+
import { applyCloudflareAiGatewayConfig, applyCloudflareAiGatewayProviderConfig, buildCloudflareAiGatewayConfigPatch } from "./onboard.js";
|
|
4
|
+
export { CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID, CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF, CLOUDFLARE_AI_GATEWAY_PROVIDER_ID, applyCloudflareAiGatewayConfig, applyCloudflareAiGatewayProviderConfig, buildCloudflareAiGatewayCatalogProvider, buildCloudflareAiGatewayConfigPatch, buildCloudflareAiGatewayModelDefinition, resolveCloudflareAiGatewayBaseUrl };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { buildCloudflareAiGatewayModelDefinition, resolveCloudflareAiGatewayBaseUrl } from "./models.js";
|
|
2
|
+
import { coerceSecretRef, resolveNonEnvSecretRefApiKeyMarker } from "openclaw/plugin-sdk/provider-auth";
|
|
3
|
+
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
|
+
//#region extensions/cloudflare-ai-gateway/catalog-provider.ts
|
|
5
|
+
/**
|
|
6
|
+
* Builds runtime model catalog entries from stored Cloudflare AI Gateway auth
|
|
7
|
+
* profiles.
|
|
8
|
+
*/
|
|
9
|
+
function resolveCloudflareAiGatewayApiKey(cred) {
|
|
10
|
+
if (!cred || cred.type !== "api_key") return;
|
|
11
|
+
const keyRef = coerceSecretRef(cred.keyRef);
|
|
12
|
+
const keyRefId = normalizeOptionalString(keyRef?.id);
|
|
13
|
+
if (keyRef && keyRefId) return keyRef.source === "env" ? keyRefId : resolveNonEnvSecretRefApiKeyMarker(keyRef.source);
|
|
14
|
+
return normalizeOptionalString(cred.key);
|
|
15
|
+
}
|
|
16
|
+
function resolveCloudflareAiGatewayMetadata(cred) {
|
|
17
|
+
if (!cred || cred.type !== "api_key") return {};
|
|
18
|
+
return {
|
|
19
|
+
accountId: normalizeOptionalString(cred.metadata?.accountId),
|
|
20
|
+
gatewayId: normalizeOptionalString(cred.metadata?.gatewayId)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Returns a provider catalog entry when credentials and Gateway metadata are
|
|
25
|
+
* complete enough to construct an Anthropic-compatible base URL.
|
|
26
|
+
*/
|
|
27
|
+
function buildCloudflareAiGatewayCatalogProvider(params) {
|
|
28
|
+
const apiKey = normalizeOptionalString(params.envApiKey) ?? resolveCloudflareAiGatewayApiKey(params.credential);
|
|
29
|
+
if (!apiKey) return null;
|
|
30
|
+
const { accountId, gatewayId } = resolveCloudflareAiGatewayMetadata(params.credential);
|
|
31
|
+
if (!accountId || !gatewayId) return null;
|
|
32
|
+
const baseUrl = resolveCloudflareAiGatewayBaseUrl({
|
|
33
|
+
accountId,
|
|
34
|
+
gatewayId
|
|
35
|
+
});
|
|
36
|
+
if (!baseUrl) return null;
|
|
37
|
+
return {
|
|
38
|
+
baseUrl,
|
|
39
|
+
api: "anthropic-messages",
|
|
40
|
+
apiKey,
|
|
41
|
+
models: [buildCloudflareAiGatewayModelDefinition()]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { buildCloudflareAiGatewayCatalogProvider };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF } from "./models.js";
|
|
2
|
+
import { buildCloudflareAiGatewayCatalogProvider } from "./catalog-provider.js";
|
|
3
|
+
import { applyCloudflareAiGatewayConfig, buildCloudflareAiGatewayConfigPatch } from "./onboard.js";
|
|
4
|
+
import { wrapCloudflareAiGatewayProviderStream } from "./stream-wrappers.js";
|
|
5
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
6
|
+
import { applyAuthProfileConfig, buildApiKeyCredential, ensureApiKeyFromOptionEnvOrPrompt, ensureAuthProfileStore, listProfilesForProvider, normalizeApiKeyInput, normalizeOptionalSecretInput, upsertAuthProfileWithLock, validateApiKeyInput } from "openclaw/plugin-sdk/provider-auth";
|
|
7
|
+
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
8
|
+
//#region extensions/cloudflare-ai-gateway/index.ts
|
|
9
|
+
/**
|
|
10
|
+
* Bundled provider plugin entry for Cloudflare AI Gateway setup, catalog
|
|
11
|
+
* discovery, failover classification, and stream wrapping.
|
|
12
|
+
*/
|
|
13
|
+
const PROVIDER_ID = "cloudflare-ai-gateway";
|
|
14
|
+
const PROVIDER_ENV_VAR = "CLOUDFLARE_AI_GATEWAY_API_KEY";
|
|
15
|
+
const PROFILE_ID = "cloudflare-ai-gateway:default";
|
|
16
|
+
async function upsertAuthProfileWithLockOrThrow(params) {
|
|
17
|
+
if (!await upsertAuthProfileWithLock(params)) throw new Error("Failed to update auth profile store; the auth store lock may be busy. Wait a moment and retry.");
|
|
18
|
+
}
|
|
19
|
+
function readRequiredTextInput(value) {
|
|
20
|
+
return typeof value === "string" ? value.trim() : "";
|
|
21
|
+
}
|
|
22
|
+
async function resolveCloudflareGatewayMetadataInteractive(ctx) {
|
|
23
|
+
let accountId = normalizeOptionalString(ctx.accountId) ?? "";
|
|
24
|
+
let gatewayId = normalizeOptionalString(ctx.gatewayId) ?? "";
|
|
25
|
+
if (!accountId) accountId = readRequiredTextInput(await ctx.prompter.text({
|
|
26
|
+
message: "Enter Cloudflare Account ID",
|
|
27
|
+
validate: (val) => readRequiredTextInput(val) ? void 0 : "Account ID is required"
|
|
28
|
+
}));
|
|
29
|
+
if (!gatewayId) gatewayId = readRequiredTextInput(await ctx.prompter.text({
|
|
30
|
+
message: "Enter Cloudflare AI Gateway ID",
|
|
31
|
+
validate: (val) => readRequiredTextInput(val) ? void 0 : "Gateway ID is required"
|
|
32
|
+
}));
|
|
33
|
+
return {
|
|
34
|
+
accountId,
|
|
35
|
+
gatewayId
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
var cloudflare_ai_gateway_default = definePluginEntry({
|
|
39
|
+
id: PROVIDER_ID,
|
|
40
|
+
name: "Cloudflare AI Gateway Provider",
|
|
41
|
+
description: "Bundled Cloudflare AI Gateway provider plugin",
|
|
42
|
+
register(api) {
|
|
43
|
+
api.registerProvider({
|
|
44
|
+
id: PROVIDER_ID,
|
|
45
|
+
label: "Cloudflare AI Gateway",
|
|
46
|
+
docsPath: "/providers/cloudflare-ai-gateway",
|
|
47
|
+
envVars: ["CLOUDFLARE_AI_GATEWAY_API_KEY"],
|
|
48
|
+
auth: [{
|
|
49
|
+
id: "api-key",
|
|
50
|
+
label: "Cloudflare AI Gateway",
|
|
51
|
+
hint: "Account ID + Gateway ID + API key",
|
|
52
|
+
kind: "api_key",
|
|
53
|
+
wizard: {
|
|
54
|
+
choiceId: "cloudflare-ai-gateway-api-key",
|
|
55
|
+
choiceLabel: "Cloudflare AI Gateway",
|
|
56
|
+
choiceHint: "Account ID + Gateway ID + API key",
|
|
57
|
+
groupId: "cloudflare-ai-gateway",
|
|
58
|
+
groupLabel: "Cloudflare AI Gateway",
|
|
59
|
+
groupHint: "Account ID + Gateway ID + API key"
|
|
60
|
+
},
|
|
61
|
+
run: async (ctx) => {
|
|
62
|
+
const metadata = await resolveCloudflareGatewayMetadataInteractive({
|
|
63
|
+
accountId: normalizeOptionalSecretInput(ctx.opts?.cloudflareAiGatewayAccountId),
|
|
64
|
+
gatewayId: normalizeOptionalSecretInput(ctx.opts?.cloudflareAiGatewayGatewayId),
|
|
65
|
+
prompter: ctx.prompter
|
|
66
|
+
});
|
|
67
|
+
let capturedSecretInput = "";
|
|
68
|
+
let capturedCredential = false;
|
|
69
|
+
let capturedMode;
|
|
70
|
+
await ensureApiKeyFromOptionEnvOrPrompt({
|
|
71
|
+
token: normalizeOptionalSecretInput(ctx.opts?.cloudflareAiGatewayApiKey),
|
|
72
|
+
tokenProvider: "cloudflare-ai-gateway",
|
|
73
|
+
secretInputMode: ctx.allowSecretRefPrompt === false ? ctx.secretInputMode ?? "plaintext" : ctx.secretInputMode,
|
|
74
|
+
config: ctx.config,
|
|
75
|
+
expectedProviders: [PROVIDER_ID],
|
|
76
|
+
provider: PROVIDER_ID,
|
|
77
|
+
envLabel: PROVIDER_ENV_VAR,
|
|
78
|
+
promptMessage: "Enter Cloudflare AI Gateway API key",
|
|
79
|
+
normalize: normalizeApiKeyInput,
|
|
80
|
+
validate: validateApiKeyInput,
|
|
81
|
+
prompter: ctx.prompter,
|
|
82
|
+
setCredential: async (apiKey, mode) => {
|
|
83
|
+
capturedSecretInput = apiKey;
|
|
84
|
+
capturedCredential = true;
|
|
85
|
+
capturedMode = mode;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
if (!capturedCredential) throw new Error("Missing Cloudflare AI Gateway API key.");
|
|
89
|
+
return {
|
|
90
|
+
profiles: [{
|
|
91
|
+
profileId: PROFILE_ID,
|
|
92
|
+
credential: buildApiKeyCredential(PROVIDER_ID, capturedSecretInput ?? "", {
|
|
93
|
+
accountId: metadata.accountId,
|
|
94
|
+
gatewayId: metadata.gatewayId
|
|
95
|
+
}, capturedMode ? { secretInputMode: capturedMode } : void 0)
|
|
96
|
+
}],
|
|
97
|
+
configPatch: buildCloudflareAiGatewayConfigPatch(metadata),
|
|
98
|
+
defaultModel: CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
runNonInteractive: async (ctx) => {
|
|
102
|
+
const authStore = ensureAuthProfileStore(ctx.agentDir, { allowKeychainPrompt: false });
|
|
103
|
+
const storedMetadata = authStore.profiles[PROFILE_ID]?.type === "api_key" ? {
|
|
104
|
+
accountId: normalizeOptionalString(authStore.profiles[PROFILE_ID]?.metadata?.accountId),
|
|
105
|
+
gatewayId: normalizeOptionalString(authStore.profiles[PROFILE_ID]?.metadata?.gatewayId)
|
|
106
|
+
} : {};
|
|
107
|
+
const accountId = normalizeOptionalSecretInput(ctx.opts.cloudflareAiGatewayAccountId) ?? storedMetadata.accountId;
|
|
108
|
+
const gatewayId = normalizeOptionalSecretInput(ctx.opts.cloudflareAiGatewayGatewayId) ?? storedMetadata.gatewayId;
|
|
109
|
+
if (!accountId || !gatewayId) {
|
|
110
|
+
ctx.runtime.error("Cloudflare AI Gateway setup requires --cloudflare-ai-gateway-account-id and --cloudflare-ai-gateway-gateway-id.");
|
|
111
|
+
ctx.runtime.exit(1);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const resolved = await ctx.resolveApiKey({
|
|
115
|
+
provider: PROVIDER_ID,
|
|
116
|
+
flagValue: normalizeOptionalSecretInput(ctx.opts.cloudflareAiGatewayApiKey),
|
|
117
|
+
flagName: "--cloudflare-ai-gateway-api-key",
|
|
118
|
+
envVar: PROVIDER_ENV_VAR
|
|
119
|
+
});
|
|
120
|
+
if (!resolved) return null;
|
|
121
|
+
if (resolved.source !== "profile") {
|
|
122
|
+
const credential = ctx.toApiKeyCredential({
|
|
123
|
+
provider: PROVIDER_ID,
|
|
124
|
+
resolved,
|
|
125
|
+
metadata: {
|
|
126
|
+
accountId,
|
|
127
|
+
gatewayId
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
if (!credential) return null;
|
|
131
|
+
await upsertAuthProfileWithLockOrThrow({
|
|
132
|
+
profileId: PROFILE_ID,
|
|
133
|
+
credential,
|
|
134
|
+
agentDir: ctx.agentDir
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return applyCloudflareAiGatewayConfig(applyAuthProfileConfig(ctx.config, {
|
|
138
|
+
profileId: PROFILE_ID,
|
|
139
|
+
provider: PROVIDER_ID,
|
|
140
|
+
mode: "api_key"
|
|
141
|
+
}), {
|
|
142
|
+
accountId,
|
|
143
|
+
gatewayId
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}],
|
|
147
|
+
catalog: {
|
|
148
|
+
order: "late",
|
|
149
|
+
run: async (ctx) => {
|
|
150
|
+
const authStore = ensureAuthProfileStore(ctx.agentDir, { allowKeychainPrompt: false });
|
|
151
|
+
const envManagedApiKey = normalizeOptionalString(ctx.env[PROVIDER_ENV_VAR]) ? PROVIDER_ENV_VAR : void 0;
|
|
152
|
+
for (const profileId of listProfilesForProvider(authStore, PROVIDER_ID)) {
|
|
153
|
+
const provider = buildCloudflareAiGatewayCatalogProvider({
|
|
154
|
+
credential: authStore.profiles[profileId],
|
|
155
|
+
envApiKey: envManagedApiKey
|
|
156
|
+
});
|
|
157
|
+
if (!provider) continue;
|
|
158
|
+
return { provider };
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
classifyFailoverReason: ({ errorMessage }) => /\bworkers?_ai\b.*\b(?:rate|limit|quota)\b/i.test(errorMessage) ? "rate_limit" : void 0,
|
|
164
|
+
wrapStreamFn: wrapCloudflareAiGatewayProviderStream
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
//#endregion
|
|
169
|
+
export { cloudflare_ai_gateway_default as default };
|
package/dist/models.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//#region extensions/cloudflare-ai-gateway/models.ts
|
|
2
|
+
/** Provider id used in model refs and auth profiles. */
|
|
3
|
+
const CLOUDFLARE_AI_GATEWAY_PROVIDER_ID = "cloudflare-ai-gateway";
|
|
4
|
+
/** Default Cloudflare AI Gateway model id exposed by the bundled provider. */
|
|
5
|
+
const CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID = "claude-sonnet-4-6";
|
|
6
|
+
/** Fully-qualified default model ref used by onboarding. */
|
|
7
|
+
const CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF = `${CLOUDFLARE_AI_GATEWAY_PROVIDER_ID}/${CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID}`;
|
|
8
|
+
const CLOUDFLARE_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW = 2e5;
|
|
9
|
+
const CLOUDFLARE_AI_GATEWAY_DEFAULT_MAX_TOKENS = 64e3;
|
|
10
|
+
const CLOUDFLARE_AI_GATEWAY_DEFAULT_COST = {
|
|
11
|
+
input: 3,
|
|
12
|
+
output: 15,
|
|
13
|
+
cacheRead: .3,
|
|
14
|
+
cacheWrite: 3.75
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Builds a provider model definition, allowing tests/catalog code to override
|
|
18
|
+
* the model id while preserving Cloudflare defaults.
|
|
19
|
+
*/
|
|
20
|
+
function buildCloudflareAiGatewayModelDefinition(params) {
|
|
21
|
+
return {
|
|
22
|
+
id: params?.id?.trim() || "claude-sonnet-4-6",
|
|
23
|
+
name: params?.name ?? "Claude Sonnet 4.6",
|
|
24
|
+
reasoning: params?.reasoning ?? true,
|
|
25
|
+
input: params?.input ?? ["text", "image"],
|
|
26
|
+
cost: CLOUDFLARE_AI_GATEWAY_DEFAULT_COST,
|
|
27
|
+
contextWindow: CLOUDFLARE_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW,
|
|
28
|
+
maxTokens: CLOUDFLARE_AI_GATEWAY_DEFAULT_MAX_TOKENS
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Constructs the Anthropic Messages base URL for a Cloudflare account/gateway
|
|
33
|
+
* pair, returning an empty string for incomplete metadata.
|
|
34
|
+
*/
|
|
35
|
+
function resolveCloudflareAiGatewayBaseUrl(params) {
|
|
36
|
+
const accountId = params.accountId.trim();
|
|
37
|
+
const gatewayId = params.gatewayId.trim();
|
|
38
|
+
if (!accountId || !gatewayId) return "";
|
|
39
|
+
return `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/anthropic`;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID, CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF, CLOUDFLARE_AI_GATEWAY_PROVIDER_ID, buildCloudflareAiGatewayModelDefinition, resolveCloudflareAiGatewayBaseUrl };
|
package/dist/onboard.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF, buildCloudflareAiGatewayModelDefinition, resolveCloudflareAiGatewayBaseUrl } from "./models.js";
|
|
2
|
+
import { applyAgentDefaultModelPrimary, applyProviderConfigWithDefaultModel } from "openclaw/plugin-sdk/provider-onboard";
|
|
3
|
+
//#region extensions/cloudflare-ai-gateway/onboard.ts
|
|
4
|
+
/**
|
|
5
|
+
* Config patch helpers used by Cloudflare AI Gateway interactive and
|
|
6
|
+
* non-interactive onboarding flows.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Builds the minimal config patch for provider setup and default model aliasing.
|
|
10
|
+
*/
|
|
11
|
+
function buildCloudflareAiGatewayConfigPatch(params) {
|
|
12
|
+
return {
|
|
13
|
+
models: { providers: { "cloudflare-ai-gateway": {
|
|
14
|
+
baseUrl: resolveCloudflareAiGatewayBaseUrl(params),
|
|
15
|
+
api: "anthropic-messages",
|
|
16
|
+
models: [buildCloudflareAiGatewayModelDefinition()]
|
|
17
|
+
} } },
|
|
18
|
+
agents: { defaults: { models: { [CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF]: { alias: "Cloudflare AI Gateway" } } } }
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Applies provider model config while preserving existing agent model aliases.
|
|
23
|
+
*/
|
|
24
|
+
function applyCloudflareAiGatewayProviderConfig(cfg, params) {
|
|
25
|
+
const models = { ...cfg.agents?.defaults?.models };
|
|
26
|
+
models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF] = {
|
|
27
|
+
...models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF],
|
|
28
|
+
alias: models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF]?.alias ?? "Cloudflare AI Gateway"
|
|
29
|
+
};
|
|
30
|
+
const existingProvider = cfg.models?.providers?.["cloudflare-ai-gateway"];
|
|
31
|
+
const baseUrl = params?.accountId && params?.gatewayId ? resolveCloudflareAiGatewayBaseUrl({
|
|
32
|
+
accountId: params.accountId,
|
|
33
|
+
gatewayId: params.gatewayId
|
|
34
|
+
}) : typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl : void 0;
|
|
35
|
+
if (!baseUrl) return {
|
|
36
|
+
...cfg,
|
|
37
|
+
agents: {
|
|
38
|
+
...cfg.agents,
|
|
39
|
+
defaults: {
|
|
40
|
+
...cfg.agents?.defaults,
|
|
41
|
+
models
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return applyProviderConfigWithDefaultModel(cfg, {
|
|
46
|
+
agentModels: models,
|
|
47
|
+
providerId: "cloudflare-ai-gateway",
|
|
48
|
+
api: "anthropic-messages",
|
|
49
|
+
baseUrl,
|
|
50
|
+
defaultModel: buildCloudflareAiGatewayModelDefinition()
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Applies Cloudflare AI Gateway config and makes its default model primary.
|
|
55
|
+
*/
|
|
56
|
+
function applyCloudflareAiGatewayConfig(cfg, params) {
|
|
57
|
+
return applyAgentDefaultModelPrimary(applyCloudflareAiGatewayProviderConfig(cfg, params), CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF);
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { applyCloudflareAiGatewayConfig, applyCloudflareAiGatewayProviderConfig, buildCloudflareAiGatewayConfigPatch };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createAnthropicThinkingPrefillPayloadWrapper } from "openclaw/plugin-sdk/provider-stream-shared";
|
|
2
|
+
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
|
3
|
+
//#region extensions/cloudflare-ai-gateway/stream-wrappers.ts
|
|
4
|
+
const log = createSubsystemLogger("cloudflare-ai-gateway-stream");
|
|
5
|
+
function shouldPatchAnthropicMessagesPayload(model) {
|
|
6
|
+
return model?.api === void 0 || model.api === "anthropic-messages";
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Creates a wrapper that removes trailing assistant prefill messages before
|
|
10
|
+
* extended-thinking Anthropic requests are sent through Cloudflare.
|
|
11
|
+
*/
|
|
12
|
+
function createCloudflareAiGatewayAnthropicThinkingPrefillWrapper(baseStreamFn) {
|
|
13
|
+
return createAnthropicThinkingPrefillPayloadWrapper(baseStreamFn, (stripped) => {
|
|
14
|
+
log.warn(`removed ${stripped} trailing assistant prefill message${stripped === 1 ? "" : "s"} because Anthropic extended thinking requires conversations to end with a user turn`);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Applies the Anthropic payload wrapper only for Anthropic-compatible models.
|
|
19
|
+
*/
|
|
20
|
+
function wrapCloudflareAiGatewayProviderStream(ctx) {
|
|
21
|
+
if (!shouldPatchAnthropicMessagesPayload(ctx.model)) return ctx.streamFn;
|
|
22
|
+
return createCloudflareAiGatewayAnthropicThinkingPrefillWrapper(ctx.streamFn);
|
|
23
|
+
}
|
|
24
|
+
/** Test-only access to wrapper decisions and logger injection points. */
|
|
25
|
+
const testing = {
|
|
26
|
+
log,
|
|
27
|
+
shouldPatchAnthropicMessagesPayload
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
export { testing as __testing, testing, createCloudflareAiGatewayAnthropicThinkingPrefillWrapper, wrapCloudflareAiGatewayProviderStream };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "cloudflare-ai-gateway",
|
|
3
|
+
"activation": {
|
|
4
|
+
"onStartup": false
|
|
5
|
+
},
|
|
6
|
+
"enabledByDefault": true,
|
|
7
|
+
"providers": ["cloudflare-ai-gateway"],
|
|
8
|
+
"modelPricing": {
|
|
9
|
+
"providers": {
|
|
10
|
+
"cloudflare-ai-gateway": {
|
|
11
|
+
"openRouter": {
|
|
12
|
+
"passthroughProviderModel": true
|
|
13
|
+
},
|
|
14
|
+
"liteLLM": {
|
|
15
|
+
"passthroughProviderModel": true
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"setup": {
|
|
21
|
+
"providers": [
|
|
22
|
+
{
|
|
23
|
+
"id": "cloudflare-ai-gateway",
|
|
24
|
+
"envVars": ["CLOUDFLARE_AI_GATEWAY_API_KEY"]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"providerAuthChoices": [
|
|
29
|
+
{
|
|
30
|
+
"provider": "cloudflare-ai-gateway",
|
|
31
|
+
"method": "api-key",
|
|
32
|
+
"choiceId": "cloudflare-ai-gateway-api-key",
|
|
33
|
+
"choiceLabel": "Cloudflare AI Gateway",
|
|
34
|
+
"choiceHint": "Account ID + Gateway ID + API key",
|
|
35
|
+
"groupId": "cloudflare-ai-gateway",
|
|
36
|
+
"groupLabel": "Cloudflare AI Gateway",
|
|
37
|
+
"groupHint": "Account ID + Gateway ID + API key",
|
|
38
|
+
"optionKey": "cloudflareAiGatewayApiKey",
|
|
39
|
+
"cliFlag": "--cloudflare-ai-gateway-api-key",
|
|
40
|
+
"cliOption": "--cloudflare-ai-gateway-api-key <key>",
|
|
41
|
+
"cliDescription": "Cloudflare AI Gateway API key"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"configSchema": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"additionalProperties": false,
|
|
47
|
+
"properties": {}
|
|
48
|
+
}
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/cloudflare-ai-gateway-provider",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"license": "MIT",
|
|
3
|
+
"version": "2026.6.9",
|
|
4
|
+
"description": "OpenClaw Cloudflare AI Gateway provider plugin.",
|
|
6
5
|
"repository": {
|
|
7
6
|
"type": "git",
|
|
8
|
-
"url": "
|
|
7
|
+
"url": "https://github.com/openclaw/openclaw"
|
|
9
8
|
},
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
9
|
+
"type": "module",
|
|
10
|
+
"openclaw": {
|
|
11
|
+
"extensions": [
|
|
12
|
+
"./index.ts"
|
|
13
|
+
],
|
|
14
|
+
"install": {
|
|
15
|
+
"clawhubSpec": "clawhub:@openclaw/cloudflare-ai-gateway-provider",
|
|
16
|
+
"npmSpec": "@openclaw/cloudflare-ai-gateway-provider",
|
|
17
|
+
"defaultChoice": "npm",
|
|
18
|
+
"minHostVersion": ">=2026.6.8"
|
|
19
|
+
},
|
|
20
|
+
"compat": {
|
|
21
|
+
"pluginApi": ">=2026.6.9"
|
|
22
|
+
},
|
|
23
|
+
"build": {
|
|
24
|
+
"openclawVersion": "2026.6.9",
|
|
25
|
+
"bundledDist": false
|
|
26
|
+
},
|
|
27
|
+
"release": {
|
|
28
|
+
"publishToClawHub": true,
|
|
29
|
+
"publishToNpm": true
|
|
30
|
+
},
|
|
31
|
+
"runtimeExtensions": [
|
|
32
|
+
"./dist/index.js"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist/**",
|
|
37
|
+
"openclaw.plugin.json",
|
|
38
|
+
"npm-shrinkwrap.json",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"openclaw": ">=2026.6.9"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"openclaw": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"bundledDependencies": []
|
|
14
50
|
}
|