Add AWS Bedrock Converse Stream API to the list of supported model APIs,
enabling custom provider configurations for Amazon Bedrock endpoints.
This allows users to configure Bedrock models in their clawdbot.json:
"models": {
"providers": {
"amazon-bedrock": {
"baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com",
"api": "bedrock-converse-stream",
"models": [...]
}
}
}
The underlying adapter already exists; this change exposes it as a valid
configuration option.
389 lines
12 KiB
TypeScript
389 lines
12 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { isSafeExecutableValue } from "../infra/exec-safety.js";
|
|
|
|
export const ModelApiSchema = z.union([
|
|
z.literal("openai-completions"),
|
|
z.literal("openai-responses"),
|
|
z.literal("anthropic-messages"),
|
|
z.literal("google-generative-ai"),
|
|
z.literal("github-copilot"),
|
|
z.literal("bedrock-converse-stream"),
|
|
]);
|
|
|
|
export const ModelCompatSchema = z
|
|
.object({
|
|
supportsStore: z.boolean().optional(),
|
|
supportsDeveloperRole: z.boolean().optional(),
|
|
supportsReasoningEffort: z.boolean().optional(),
|
|
maxTokensField: z
|
|
.union([z.literal("max_completion_tokens"), z.literal("max_tokens")])
|
|
.optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
export const ModelDefinitionSchema = z
|
|
.object({
|
|
id: z.string().min(1),
|
|
name: z.string().min(1),
|
|
api: ModelApiSchema.optional(),
|
|
reasoning: z.boolean(),
|
|
input: z.array(z.union([z.literal("text"), z.literal("image")])),
|
|
cost: z
|
|
.object({
|
|
input: z.number(),
|
|
output: z.number(),
|
|
cacheRead: z.number(),
|
|
cacheWrite: z.number(),
|
|
})
|
|
.strict(),
|
|
contextWindow: z.number().positive(),
|
|
maxTokens: z.number().positive(),
|
|
headers: z.record(z.string(), z.string()).optional(),
|
|
compat: ModelCompatSchema,
|
|
})
|
|
.strict();
|
|
|
|
export const ModelProviderSchema = z
|
|
.object({
|
|
baseUrl: z.string().min(1),
|
|
apiKey: z.string().optional(),
|
|
api: ModelApiSchema.optional(),
|
|
headers: z.record(z.string(), z.string()).optional(),
|
|
authHeader: z.boolean().optional(),
|
|
models: z.array(ModelDefinitionSchema),
|
|
})
|
|
.strict();
|
|
|
|
export const ModelsConfigSchema = z
|
|
.object({
|
|
mode: z.union([z.literal("merge"), z.literal("replace")]).optional(),
|
|
providers: z.record(z.string(), ModelProviderSchema).optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
export const GroupChatSchema = z
|
|
.object({
|
|
mentionPatterns: z.array(z.string()).optional(),
|
|
historyLimit: z.number().int().positive().optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
export const DmConfigSchema = z
|
|
.object({
|
|
historyLimit: z.number().int().min(0).optional(),
|
|
})
|
|
.strict();
|
|
|
|
export const IdentitySchema = z
|
|
.object({
|
|
name: z.string().optional(),
|
|
theme: z.string().optional(),
|
|
emoji: z.string().optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
export const QueueModeSchema = z.union([
|
|
z.literal("steer"),
|
|
z.literal("followup"),
|
|
z.literal("collect"),
|
|
z.literal("steer-backlog"),
|
|
z.literal("steer+backlog"),
|
|
z.literal("queue"),
|
|
z.literal("interrupt"),
|
|
]);
|
|
export const QueueDropSchema = z.union([
|
|
z.literal("old"),
|
|
z.literal("new"),
|
|
z.literal("summarize"),
|
|
]);
|
|
export const ReplyToModeSchema = z.union([z.literal("off"), z.literal("first"), z.literal("all")]);
|
|
|
|
// GroupPolicySchema: controls how group messages are handled
|
|
// Used with .default("allowlist").optional() pattern:
|
|
// - .optional() allows field omission in input config
|
|
// - .default("allowlist") ensures runtime always resolves to "allowlist" if not provided
|
|
export const GroupPolicySchema = z.enum(["open", "disabled", "allowlist"]);
|
|
|
|
export const DmPolicySchema = z.enum(["pairing", "allowlist", "open", "disabled"]);
|
|
|
|
export const BlockStreamingCoalesceSchema = z
|
|
.object({
|
|
minChars: z.number().int().positive().optional(),
|
|
maxChars: z.number().int().positive().optional(),
|
|
idleMs: z.number().int().nonnegative().optional(),
|
|
})
|
|
.strict();
|
|
|
|
export const BlockStreamingChunkSchema = z
|
|
.object({
|
|
minChars: z.number().int().positive().optional(),
|
|
maxChars: z.number().int().positive().optional(),
|
|
breakPreference: z
|
|
.union([z.literal("paragraph"), z.literal("newline"), z.literal("sentence")])
|
|
.optional(),
|
|
})
|
|
.strict();
|
|
|
|
export const HumanDelaySchema = z
|
|
.object({
|
|
mode: z.union([z.literal("off"), z.literal("natural"), z.literal("custom")]).optional(),
|
|
minMs: z.number().int().nonnegative().optional(),
|
|
maxMs: z.number().int().nonnegative().optional(),
|
|
})
|
|
.strict();
|
|
|
|
export const CliBackendSchema = z
|
|
.object({
|
|
command: z.string(),
|
|
args: z.array(z.string()).optional(),
|
|
output: z.union([z.literal("json"), z.literal("text"), z.literal("jsonl")]).optional(),
|
|
resumeOutput: z.union([z.literal("json"), z.literal("text"), z.literal("jsonl")]).optional(),
|
|
input: z.union([z.literal("arg"), z.literal("stdin")]).optional(),
|
|
maxPromptArgChars: z.number().int().positive().optional(),
|
|
env: z.record(z.string(), z.string()).optional(),
|
|
clearEnv: z.array(z.string()).optional(),
|
|
modelArg: z.string().optional(),
|
|
modelAliases: z.record(z.string(), z.string()).optional(),
|
|
sessionArg: z.string().optional(),
|
|
sessionArgs: z.array(z.string()).optional(),
|
|
resumeArgs: z.array(z.string()).optional(),
|
|
sessionMode: z
|
|
.union([z.literal("always"), z.literal("existing"), z.literal("none")])
|
|
.optional(),
|
|
sessionIdFields: z.array(z.string()).optional(),
|
|
systemPromptArg: z.string().optional(),
|
|
systemPromptMode: z.union([z.literal("append"), z.literal("replace")]).optional(),
|
|
systemPromptWhen: z
|
|
.union([z.literal("first"), z.literal("always"), z.literal("never")])
|
|
.optional(),
|
|
imageArg: z.string().optional(),
|
|
imageMode: z.union([z.literal("repeat"), z.literal("list")]).optional(),
|
|
serialize: z.boolean().optional(),
|
|
})
|
|
.strict();
|
|
|
|
export const normalizeAllowFrom = (values?: Array<string | number>): string[] =>
|
|
(values ?? []).map((v) => String(v).trim()).filter(Boolean);
|
|
|
|
export const requireOpenAllowFrom = (params: {
|
|
policy?: string;
|
|
allowFrom?: Array<string | number>;
|
|
ctx: z.RefinementCtx;
|
|
path: Array<string | number>;
|
|
message: string;
|
|
}) => {
|
|
if (params.policy !== "open") return;
|
|
const allow = normalizeAllowFrom(params.allowFrom);
|
|
if (allow.includes("*")) return;
|
|
params.ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: params.path,
|
|
message: params.message,
|
|
});
|
|
};
|
|
|
|
export const MSTeamsReplyStyleSchema = z.enum(["thread", "top-level"]);
|
|
|
|
export const RetryConfigSchema = z
|
|
.object({
|
|
attempts: z.number().int().min(1).optional(),
|
|
minDelayMs: z.number().int().min(0).optional(),
|
|
maxDelayMs: z.number().int().min(0).optional(),
|
|
jitter: z.number().min(0).max(1).optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
export const QueueModeBySurfaceSchema = z
|
|
.object({
|
|
whatsapp: QueueModeSchema.optional(),
|
|
telegram: QueueModeSchema.optional(),
|
|
discord: QueueModeSchema.optional(),
|
|
slack: QueueModeSchema.optional(),
|
|
signal: QueueModeSchema.optional(),
|
|
imessage: QueueModeSchema.optional(),
|
|
msteams: QueueModeSchema.optional(),
|
|
webchat: QueueModeSchema.optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
export const DebounceMsBySurfaceSchema = z
|
|
.object({
|
|
whatsapp: z.number().int().nonnegative().optional(),
|
|
telegram: z.number().int().nonnegative().optional(),
|
|
discord: z.number().int().nonnegative().optional(),
|
|
slack: z.number().int().nonnegative().optional(),
|
|
signal: z.number().int().nonnegative().optional(),
|
|
imessage: z.number().int().nonnegative().optional(),
|
|
msteams: z.number().int().nonnegative().optional(),
|
|
webchat: z.number().int().nonnegative().optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
export const QueueSchema = z
|
|
.object({
|
|
mode: QueueModeSchema.optional(),
|
|
byChannel: QueueModeBySurfaceSchema,
|
|
debounceMs: z.number().int().nonnegative().optional(),
|
|
cap: z.number().int().positive().optional(),
|
|
drop: QueueDropSchema.optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
export const InboundDebounceSchema = z
|
|
.object({
|
|
debounceMs: z.number().int().nonnegative().optional(),
|
|
byChannel: DebounceMsBySurfaceSchema,
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
export const TranscribeAudioSchema = z
|
|
.object({
|
|
command: z.array(z.string()).superRefine((value, ctx) => {
|
|
const executable = value[0];
|
|
if (!isSafeExecutableValue(executable)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: [0],
|
|
message: "expected safe executable name or path",
|
|
});
|
|
}
|
|
}),
|
|
timeoutSeconds: z.number().int().positive().optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
export const HexColorSchema = z.string().regex(/^#?[0-9a-fA-F]{6}$/, "expected hex color (RRGGBB)");
|
|
|
|
export const ExecutableTokenSchema = z
|
|
.string()
|
|
.refine(isSafeExecutableValue, "expected safe executable name or path");
|
|
|
|
export const MediaUnderstandingScopeSchema = z
|
|
.object({
|
|
default: z.union([z.literal("allow"), z.literal("deny")]).optional(),
|
|
rules: z
|
|
.array(
|
|
z
|
|
.object({
|
|
action: z.union([z.literal("allow"), z.literal("deny")]),
|
|
match: z
|
|
.object({
|
|
channel: z.string().optional(),
|
|
chatType: z
|
|
.union([z.literal("direct"), z.literal("group"), z.literal("channel")])
|
|
.optional(),
|
|
keyPrefix: z.string().optional(),
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
})
|
|
.strict(),
|
|
)
|
|
.optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
export const MediaUnderstandingCapabilitiesSchema = z
|
|
.array(z.union([z.literal("image"), z.literal("audio"), z.literal("video")]))
|
|
.optional();
|
|
|
|
export const MediaUnderstandingAttachmentsSchema = z
|
|
.object({
|
|
mode: z.union([z.literal("first"), z.literal("all")]).optional(),
|
|
maxAttachments: z.number().int().positive().optional(),
|
|
prefer: z
|
|
.union([z.literal("first"), z.literal("last"), z.literal("path"), z.literal("url")])
|
|
.optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
const DeepgramAudioSchema = z
|
|
.object({
|
|
detectLanguage: z.boolean().optional(),
|
|
punctuate: z.boolean().optional(),
|
|
smartFormat: z.boolean().optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
const ProviderOptionValueSchema = z.union([z.string(), z.number(), z.boolean()]);
|
|
const ProviderOptionsSchema = z
|
|
.record(z.string(), z.record(z.string(), ProviderOptionValueSchema))
|
|
.optional();
|
|
|
|
export const MediaUnderstandingModelSchema = z
|
|
.object({
|
|
provider: z.string().optional(),
|
|
model: z.string().optional(),
|
|
capabilities: MediaUnderstandingCapabilitiesSchema,
|
|
type: z.union([z.literal("provider"), z.literal("cli")]).optional(),
|
|
command: z.string().optional(),
|
|
args: z.array(z.string()).optional(),
|
|
prompt: z.string().optional(),
|
|
maxChars: z.number().int().positive().optional(),
|
|
maxBytes: z.number().int().positive().optional(),
|
|
timeoutSeconds: z.number().int().positive().optional(),
|
|
language: z.string().optional(),
|
|
providerOptions: ProviderOptionsSchema,
|
|
deepgram: DeepgramAudioSchema,
|
|
baseUrl: z.string().optional(),
|
|
headers: z.record(z.string(), z.string()).optional(),
|
|
profile: z.string().optional(),
|
|
preferredProfile: z.string().optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
export const ToolsMediaUnderstandingSchema = z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
scope: MediaUnderstandingScopeSchema,
|
|
maxBytes: z.number().int().positive().optional(),
|
|
maxChars: z.number().int().positive().optional(),
|
|
prompt: z.string().optional(),
|
|
timeoutSeconds: z.number().int().positive().optional(),
|
|
language: z.string().optional(),
|
|
providerOptions: ProviderOptionsSchema,
|
|
deepgram: DeepgramAudioSchema,
|
|
baseUrl: z.string().optional(),
|
|
headers: z.record(z.string(), z.string()).optional(),
|
|
attachments: MediaUnderstandingAttachmentsSchema,
|
|
models: z.array(MediaUnderstandingModelSchema).optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
export const ToolsMediaSchema = z
|
|
.object({
|
|
models: z.array(MediaUnderstandingModelSchema).optional(),
|
|
concurrency: z.number().int().positive().optional(),
|
|
image: ToolsMediaUnderstandingSchema.optional(),
|
|
audio: ToolsMediaUnderstandingSchema.optional(),
|
|
video: ToolsMediaUnderstandingSchema.optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
export const NativeCommandsSettingSchema = z.union([z.boolean(), z.literal("auto")]);
|
|
|
|
export const ProviderCommandsSchema = z
|
|
.object({
|
|
native: NativeCommandsSettingSchema.optional(),
|
|
nativeSkills: NativeCommandsSettingSchema.optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|