refactor(packages): deslop shared packages third pass (#159933)

* refactor(packages): deslop shared packages third pass

Consolidate shared package projection, schema traversal, transcript parsing, and request setup while preserving public export maps, API declarations, runtime behavior, and protocol schemas. Remove redundant facade narration and shrink the assertion baseline for removed casts.

* refactor(memory): keep transcript type at session accessor owner

* refactor(packages): retain transcript declaration closure

* refactor(packages): preserve declarations and sparse-array conventions
This commit is contained in:
Peter Steinberger 2026-09-27 20:53:42 -07:00 • committed by GitHub
parent 915567dc34
commit 81b73247b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
109 changed files with 421 additions and 1007 deletions

View file

@ -1206,7 +1206,7 @@ packages/acp-core/src/types.ts 2
packages/agent-core/src/agent-loop.ts 3
packages/agent-core/src/harness/compaction/branch-summarization.ts 1
packages/agent-core/src/harness/compaction/compaction.ts 1
packages/agent-core/src/harness/session/tool-result-pairing.ts 14
packages/agent-core/src/harness/session/tool-result-pairing.ts 3
packages/agent-core/src/harness/session/uuid.ts 1
packages/agent-core/src/harness/utils/truncate.ts 1
packages/agent-core/src/turn-interruption.ts 1
@ -1301,7 +1301,6 @@ packages/llm-core/src/utils/event-stream.ts 1
packages/llm-core/src/validation.ts 1
packages/markdown-core/src/assistant-transcript.ts 1
packages/markdown-core/src/format-capabilities.ts 1
packages/markdown-core/src/frontmatter.ts 1
packages/markdown-core/src/ir.ts 5
packages/markdown-core/src/reasoning-tag-parser.ts 1
packages/media-core/src/read-byte-stream-with-limit.ts 1
@ -1316,7 +1315,7 @@ packages/memory-host-sdk/src/host/memory-schema-migration.ts 1
packages/memory-host-sdk/src/host/memory-schema-recall.ts 1
packages/memory-host-sdk/src/host/memory-schema.ts 7
packages/memory-host-sdk/src/host/multimodal.ts 1
packages/memory-host-sdk/src/host/read-retry.ts 4
packages/memory-host-sdk/src/host/read-retry.ts 1
packages/memory-host-sdk/src/host/session-files.ts 8
packages/memory-host-sdk/src/host/session-reset-recall.ts 3
packages/memory-host-sdk/src/host/sqlite-vec.ts 3

View file

@ -1,5 +1,3 @@
// Public barrel for shared ACP session, metadata, and runtime helper contracts.
export * from "./error-format.js";
export * from "./meta.js";
export * from "./session-interaction-mode.js";

View file

@ -99,18 +99,13 @@ export function toAcpRuntimeError(params: {
cause: params.error,
});
}
if (params.error instanceof Error) {
return new AcpRuntimeError(
params.fallbackCode,
messageWithAcpRequestErrorDetails(params.error),
{
cause: params.error,
},
);
}
return new AcpRuntimeError(params.fallbackCode, params.fallbackMessage, {
cause: params.error,
});
return new AcpRuntimeError(
params.fallbackCode,
params.error instanceof Error
? messageWithAcpRequestErrorDetails(params.error)
: params.fallbackMessage,
{ cause: params.error },
);
}
/**

View file

@ -1,4 +1,3 @@
// ACP Core type module defines shared TypeScript contracts.
export type AcpRuntimePromptMode = "prompt" | "steer";
export type AcpRuntimeSessionMode = "persistent" | "oneshot";

View file

@ -378,24 +378,14 @@ export function findStructuredAuthParamRanges(value: string): StructuredAuthPara
}
export function redactStructuredAuthHeaders(value: string, replacement: string): string {
const ranges = findStructuredAuthParamRanges(value);
if (ranges.length === 0) {
return value;
}
const merged: StructuredAuthParamRange[] = [];
for (const range of ranges) {
const previous = merged.at(-1);
if (previous && range.start <= previous.end) {
previous.end = Math.max(previous.end, range.end);
} else {
merged.push({ ...range });
}
}
const parts: string[] = [];
let cursor = 0;
for (const range of merged) {
parts.push(value.slice(cursor, range.start), replacement);
cursor = range.end;
for (const range of findStructuredAuthParamRanges(value)) {
// Ranges follow header order; overlapping credentials share one replacement.
if (parts.length === 0 || range.start > cursor) {
parts.push(value.slice(cursor, range.start), replacement);
}
cursor = Math.max(cursor, range.end);
}
parts.push(value.slice(cursor));
return parts.join("");

View file

@ -125,65 +125,55 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
const llmMessages: Message[] = [];
// Preserve map's hole skipping and captured length without its intermediate array.
messages.forEach((message) => {
let content: (TextContent | ImageContent)[];
switch (message.role) {
case "bashExecution":
if (message.excludeFromContext) {
return;
}
llmMessages.push({
role: "user",
content: [{ type: "text", text: bashExecutionToText(message) }],
timestamp: message.timestamp,
});
content = [{ type: "text", text: bashExecutionToText(message) }];
break;
case "custom": {
case "custom":
if (message.excludeFromContext) {
return;
}
const content =
content =
typeof message.content === "string"
? [{ type: "text" as const, text: message.content }]
? [{ type: "text", text: message.content }]
: message.content;
// Preserve carrier identity so provider-owned replay and cache policy
// can distinguish transient context from append-only context.
llmMessages.push({
role: "user",
content,
timestamp: message.timestamp,
...(isRuntimeContextCarrier(message) ? { runtimeContextCarrier: true } : {}),
});
break;
}
case "branchSummary":
llmMessages.push({
role: "user",
content: [
{
type: "text" as const,
text: BRANCH_SUMMARY_PREFIX + message.summary + BRANCH_SUMMARY_SUFFIX,
},
],
timestamp: message.timestamp,
});
content = [
{ type: "text", text: BRANCH_SUMMARY_PREFIX + message.summary + BRANCH_SUMMARY_SUFFIX },
];
break;
case "compactionSummary":
llmMessages.push({
role: "user",
content: [
{
type: "text" as const,
text: COMPACTION_SUMMARY_PREFIX + message.summary + COMPACTION_SUMMARY_SUFFIX,
},
],
timestamp: normalizeCompactionSummaryTimestamp(message.timestamp),
});
content = [
{
type: "text",
text: COMPACTION_SUMMARY_PREFIX + message.summary + COMPACTION_SUMMARY_SUFFIX,
},
];
break;
case "user":
case "assistant":
case "toolResult":
llmMessages.push(message);
break;
return;
default:
return;
}
// Preserve carrier identity so provider-owned replay and cache policy can
// distinguish transient context from append-only context.
llmMessages.push({
role: "user",
content,
timestamp:
message.role === "compactionSummary"
? normalizeCompactionSummaryTimestamp(message.timestamp)
: message.timestamp,
...(isRuntimeContextCarrier(message) ? { runtimeContextCarrier: true } : {}),
});
});
return llmMessages;
}

View file

@ -1,3 +1,4 @@
import { asOptionalObjectRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { AgentMessage } from "../../types.js";
import type { SessionTreeEntry } from "../types.js";
@ -90,12 +91,9 @@ export function createToolCallOccurrenceQueue<T>(): ToolCallOccurrenceQueue<T> {
}
function readToolCall(block: unknown): ToolCallLike | undefined {
if (!block || typeof block !== "object") {
return undefined;
}
const record = block as { type?: unknown; id?: unknown; name?: unknown };
const record = asOptionalObjectRecord(block);
if (
typeof record.type !== "string" ||
typeof record?.type !== "string" ||
!TOOL_CALL_TYPES.has(record.type) ||
typeof record.id !== "string" ||
!record.id
@ -159,7 +157,7 @@ export function makeMissingToolResult(params: {
details: { [SYNTHETIC_MISSING_TOOL_RESULT_DETAIL_KEY]: true, reason: "missing_tool_result" },
isError: true,
timestamp: Date.now(),
} as ToolResultMessage;
};
}
export function isSyntheticMissingToolResult(message: {
@ -170,24 +168,18 @@ export function isSyntheticMissingToolResult(message: {
if (!message.isError) {
return false;
}
const details = message.details;
if (
details &&
typeof details === "object" &&
(details as Record<string, unknown>)[SYNTHETIC_MISSING_TOOL_RESULT_DETAIL_KEY] === true
asOptionalObjectRecord(message.details)?.[SYNTHETIC_MISSING_TOOL_RESULT_DETAIL_KEY] === true
) {
return true;
}
const content = message.content;
return (
Array.isArray(content) &&
content.some(
(block) =>
typeof block === "object" &&
block !== null &&
(block as { type?: string }).type === "text" &&
(block as { text?: string }).text === DEFAULT_MISSING_TOOL_RESULT_TEXT,
)
content.some((block) => {
const record = asOptionalObjectRecord(block);
return record?.type === "text" && record.text === DEFAULT_MISSING_TOOL_RESULT_TEXT;
})
);
}
@ -195,20 +187,12 @@ function normalizeToolResultName(
message: ToolResultMessage,
fallbackName?: string,
): ToolResultMessage {
const rawToolName = (message as { toolName?: unknown }).toolName;
const normalizedToolName = normalizeOptionalString(rawToolName);
if (normalizedToolName) {
return rawToolName === normalizedToolName
? message
: ({ ...message, toolName: normalizedToolName } as ToolResultMessage);
}
const normalizedFallback = normalizeOptionalString(fallbackName);
if (normalizedFallback) {
return { ...message, toolName: normalizedFallback } as ToolResultMessage;
}
return typeof rawToolName === "string"
? ({ ...message, toolName: "unknown" } as ToolResultMessage)
: message;
const rawToolName = message.toolName;
const toolName =
normalizeOptionalString(rawToolName) ??
normalizeOptionalString(fallbackName) ??
(typeof rawToolName === "string" ? "unknown" : undefined);
return toolName && toolName !== rawToolName ? { ...message, toolName } : message;
}
export function normalizeLegacyToolResultId(
@ -222,12 +206,12 @@ export function normalizeLegacyToolResultId(
if (!toolCall) {
return message;
}
const resultName = normalizeOptionalString((message as { toolName?: unknown }).toolName);
const resultName = normalizeOptionalString(message.toolName);
const callName = normalizeOptionalString(toolCall.name);
if (resultName && callName && resultName !== callName) {
return message;
}
return { ...message, toolCallId: toolCall.id, isError: true } as ToolResultMessage;
return { ...message, toolCallId: toolCall.id, isError: true };
}
/** Classifies call/result ownership without reordering or synthesizing transcript messages. */

View file

@ -2,6 +2,7 @@ import type { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/messages
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import type { AnthropicOptions } from "../provider-options.js";
import { sortPromptCacheToolsByName } from "../utils/prompt-cache-stability.js";
import { SCHEMA_MAP_KEYS } from "./schema-walk.js";
import { projectRuntimeToolInputSchema } from "./tool-schema-json-projection.js";
type AnthropicToolDescriptor = {
@ -101,14 +102,6 @@ const schemaValueKeywords = new Set([
"unevaluatedProperties",
]);
const schemaArrayKeywords = new Set(["allOf", "anyOf", "oneOf", "prefixItems"]);
const schemaMapKeywords = new Set([
"$defs",
"definitions",
"dependencies",
"dependentSchemas",
"patternProperties",
"properties",
]);
/** Normalize the detached JSON projection and report tuple changes to enclosing schemas. */
function normalizeAnthropicJsonSchema(schema: unknown): boolean {
@ -128,7 +121,7 @@ function normalizeAnthropicJsonSchema(schema: unknown): boolean {
}
continue;
}
if (schemaMapKeywords.has(key) && isRecord(value)) {
if (SCHEMA_MAP_KEYS.has(key) && isRecord(value)) {
for (const entry of Object.values(value)) {
changed = normalizeAnthropicJsonSchema(entry) || changed;
}

View file

@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { configureAiTransportHost, getAiTransportHost } from "../host.js";
import { buildOpenAIResponsesReplayContext } from "../transports/openai-responses-compaction-replay.js";
import { buildProviderReplayContext } from "../transports/provider-replay-context.js";
import type { Context, Model } from "../types.js";
import { isOpenAICompatibleAzureResponsesBaseUrl } from "./azure-openai-responses-client-compat.js";
import {
@ -186,7 +186,7 @@ describe("azure-openai-responses", () => {
const routeA = "https://route-a.openai.azure.com/openai/v1";
const routeB = "https://route-b.openai.azure.com/openai/v1";
const sessionId = "azure-replay-session";
const replayContext = buildOpenAIResponsesReplayContext(
const replayContext = buildProviderReplayContext(
{ ...azureResponsesModel, baseUrl: routeA },
{ sessionId },
);

View file

@ -143,19 +143,10 @@ export function buildGoogleGenerateContentParams<T extends GoogleApiType>(
): Omit<GenerateContentParameters, "contents"> & { contents: Content[] } {
const contents = convertMessages(model, context);
const generationConfig: GenerateContentConfig = {};
if (options.temperature !== undefined) {
generationConfig.temperature = options.temperature;
}
if (options.maxTokens !== undefined) {
generationConfig.maxOutputTokens = options.maxTokens;
}
if (options.stop !== undefined && options.stop.length > 0) {
generationConfig.stopSequences = options.stop;
}
const config: GenerateContentConfig = {
...(Object.keys(generationConfig).length > 0 && generationConfig),
...(options.temperature !== undefined && { temperature: options.temperature }),
...(options.maxTokens !== undefined && { maxOutputTokens: options.maxTokens }),
...(options.stop !== undefined && options.stop.length > 0 && { stopSequences: options.stop }),
...(context.systemPrompt && {
systemInstruction: sanitizeSurrogates(stripSystemPromptCacheBoundary(context.systemPrompt)),
}),

View file

@ -168,14 +168,104 @@ function projectOpenAIToolDescriptors(
};
}
function requireProjectedFunction(
name: string,
type ToolChoice = OpenAIResponsesToolChoice | OpenAICompletionsSdkToolChoice;
function reconcileToolChoice(
choice: OpenAIResponsesToolChoice,
projection: OpenAIToolProjection,
choiceLabel: string,
): void {
if (!projection.tools.some((tool) => tool.name === name)) {
throw new Error(`${choiceLabel} requested unavailable tool "${name}" after schema conversion`);
responses: true,
): OpenAIResponsesToolChoice | undefined;
function reconcileToolChoice(
choice: OpenAICompletionsSdkToolChoice,
projection: OpenAIToolProjection,
responses: false,
): OpenAICompletionsSdkToolChoice | undefined;
function reconcileToolChoice(
choice: ToolChoice,
projection: OpenAIToolProjection,
responses: boolean,
): ToolChoice | undefined {
const label = responses ? "OpenAI Responses" : "OpenAI Chat Completions";
if (choice === "auto") {
return projection.tools.length > 0 ? choice : undefined;
}
if (choice === "required") {
if (projection.tools.length === 0) {
throw new Error(
`${label} tool_choice requires a tool, but no tools survived schema conversion`,
);
}
return choice;
}
if (choice === "none" || !isRecord(choice)) {
return choice;
}
const choiceType = choice.type;
if (!responses && choiceType === "custom") {
throw new Error(
"OpenAI Chat Completions custom tool_choice is unsupported because this adapter emits function tools only",
);
}
if (choiceType === "function") {
const functionChoice = responses ? choice : choice.function;
const functionName = isRecord(functionChoice) ? functionChoice.name : undefined;
if (typeof functionName !== "string") {
return choice;
}
if (!projection.tools.some((tool) => tool.name === functionName)) {
throw new Error(
`${label} tool_choice requested unavailable tool "${functionName}" after schema conversion`,
);
}
return responses
? { type: "function", name: functionName }
: { type: "function", function: { name: functionName } };
}
if (choiceType !== "allowed_tools") {
return choice;
}
const allowedConfig = responses ? choice : choice.allowed_tools;
if (!isRecord(allowedConfig)) {
return choice;
}
const { mode, tools } = allowedConfig;
if ((mode !== "auto" && mode !== "required") || !Array.isArray(tools)) {
return choice;
}
const responseTools: OpenAIResponsesAllowedToolChoice["tools"] = [];
const completionTools: OpenAICompletionsAllowedToolChoice["allowed_tools"]["tools"] = [];
for (const tool of tools) {
if (!isRecord(tool) || tool.type !== "function") {
if (responses) {
responseTools.push(tool);
}
continue;
}
const functionChoice = responses ? tool : tool.function;
const functionName = isRecord(functionChoice) ? functionChoice.name : undefined;
if (
typeof functionName === "string" &&
projection.tools.some((projectedTool) => projectedTool.name === functionName)
) {
if (responses) {
responseTools.push({ type: "function", name: functionName });
} else {
completionTools.push({ type: "function", function: { name: functionName } });
}
}
}
if (responseTools.length === 0 && completionTools.length === 0) {
if (mode === "auto") {
return "none";
}
throw new Error(
`${label} tool_choice requires a tool, but no allowed tools survived schema conversion`,
);
}
return responses
? { type: "allowed_tools", mode, tools: responseTools }
: { type: "allowed_tools", allowed_tools: { mode, tools: completionTools } };
}
/** Keeps Responses tool choices aligned with surviving function schemas. */
@ -183,65 +273,7 @@ export function reconcileOpenAIResponsesToolChoice(
choice: OpenAIResponsesToolChoice,
projection: OpenAIToolProjection,
): OpenAIResponsesToolChoice | undefined {
if (choice === "auto") {
return projection.tools.length > 0 ? choice : undefined;
}
if (choice === "required") {
if (projection.tools.length === 0) {
throw new Error(
"OpenAI Responses tool_choice requires a tool, but no tools survived schema conversion",
);
}
return choice;
}
if (choice === "none" || !isRecord(choice)) {
return choice;
}
const choiceType = choice.type;
if (choiceType === "function") {
const functionName = choice.name;
if (typeof functionName !== "string") {
return choice;
}
requireProjectedFunction(functionName, projection, "OpenAI Responses tool_choice");
return { type: "function", name: functionName };
}
if (choiceType !== "allowed_tools") {
return choice;
}
const mode = choice.mode;
const tools = choice.tools;
if ((mode !== "auto" && mode !== "required") || !Array.isArray(tools)) {
return choice;
}
const normalizedAllowedTools: OpenAIResponsesAllowedToolChoice["tools"] = [];
for (const tool of tools) {
if (!isRecord(tool) || tool.type !== "function") {
normalizedAllowedTools.push(tool);
continue;
}
const functionName = tool.name;
if (
typeof functionName === "string" &&
projection.tools.some((projectedTool) => projectedTool.name === functionName)
) {
normalizedAllowedTools.push({ type: "function", name: functionName });
}
}
if (normalizedAllowedTools.length === 0) {
if (mode === "auto") {
return "none";
}
throw new Error(
"OpenAI Responses tool_choice requires a tool, but no allowed tools survived schema conversion",
);
}
return {
type: "allowed_tools",
mode,
tools: normalizedAllowedTools,
};
return reconcileToolChoice(choice, projection, true);
}
/** Keeps Chat Completions tool choices aligned with surviving function schemas. */
@ -249,81 +281,5 @@ export function reconcileOpenAICompletionsToolChoice(
choice: OpenAICompletionsSdkToolChoice,
projection: OpenAIToolProjection,
): OpenAICompletionsSdkToolChoice | undefined {
if (choice === "auto") {
return projection.tools.length > 0 ? choice : undefined;
}
if (choice === "required") {
if (projection.tools.length === 0) {
throw new Error(
"OpenAI Chat Completions tool_choice requires a tool, but no tools survived schema conversion",
);
}
return choice;
}
if (choice === "none" || !isRecord(choice)) {
return choice;
}
const choiceType = choice.type;
if (choiceType === "custom") {
throw new Error(
"OpenAI Chat Completions custom tool_choice is unsupported because this adapter emits function tools only",
);
}
if (choiceType === "function") {
const functionChoice = choice.function;
if (!isRecord(functionChoice)) {
return choice;
}
const functionName = functionChoice.name;
if (typeof functionName !== "string") {
return choice;
}
requireProjectedFunction(functionName, projection, "OpenAI Chat Completions tool_choice");
return { type: "function", function: { name: functionName } };
}
if (choiceType !== "allowed_tools") {
return choice;
}
const allowedConfig = choice.allowed_tools;
if (!isRecord(allowedConfig)) {
return choice;
}
const mode = allowedConfig.mode;
const tools = allowedConfig.tools;
if ((mode !== "auto" && mode !== "required") || !Array.isArray(tools)) {
return choice;
}
const normalizedAllowedTools: OpenAICompletionsAllowedToolChoice["allowed_tools"]["tools"] = [];
for (const tool of tools) {
if (!isRecord(tool) || tool.type !== "function") {
continue;
}
const functionChoice = tool.function;
const functionName = isRecord(functionChoice) ? functionChoice.name : undefined;
if (
typeof functionName === "string" &&
projection.tools.some((projectedTool) => projectedTool.name === functionName)
) {
normalizedAllowedTools.push({
type: "function",
function: { name: functionName },
});
}
}
if (normalizedAllowedTools.length === 0) {
if (mode === "auto") {
return "none";
}
throw new Error(
"OpenAI Chat Completions tool_choice requires a tool, but no allowed tools survived schema conversion",
);
}
return {
type: "allowed_tools",
allowed_tools: {
mode,
tools: normalizedAllowedTools,
},
};
return reconcileToolChoice(choice, projection, false);
}

View file

@ -1,16 +1,6 @@
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import type { TSchema } from "typebox";
const OPENAI_STRICT_COMPAT_SCHEMA_MAP_KEYS = new Set([
"$defs",
"definitions",
"dependentSchemas",
// Draft-07 dependencies mix schema values with property-name arrays. The
// recursive helpers leave scalar array entries untouched.
"dependencies",
"patternProperties",
"properties",
]);
import { SCHEMA_ARRAY_KEYS, SCHEMA_MAP_KEYS, SCHEMA_OBJECT_KEYS } from "./schema-walk.js";
// Annotation-only keywords whose null values can be dropped without changing
// what the schema accepts; null constraint keywords must stay so projection
@ -23,24 +13,9 @@ const OPENAI_NULLABLE_ANNOTATION_KEYS = new Set([
"title",
]);
const OPENAI_STRICT_COMPAT_SCHEMA_NESTED_KEYS = new Set([
"additionalItems",
"additionalProperties",
"allOf",
"anyOf",
"contains",
"contentSchema",
"else",
"if",
"items",
"not",
"oneOf",
"prefixItems",
"propertyNames",
"then",
"unevaluatedItems",
"unevaluatedProperties",
]);
const OPENAI_STRICT_COMPAT_SCHEMA_NESTED_KEYS = new Set(
[...SCHEMA_OBJECT_KEYS, ...SCHEMA_ARRAY_KEYS].toSorted(),
);
function normalizeOpenAIStrictCompatSchemaMap(schema: unknown): unknown {
if (!isRecord(schema)) {
@ -87,7 +62,7 @@ function normalizeOpenAIStrictCompatSchemaRecursive(
changed = true;
return [];
}
const next = OPENAI_STRICT_COMPAT_SCHEMA_MAP_KEYS.has(key)
const next = SCHEMA_MAP_KEYS.has(key)
? normalizeOpenAIStrictCompatSchemaMap(value)
: OPENAI_STRICT_COMPAT_SCHEMA_NESTED_KEYS.has(key)
? normalizeOpenAIStrictCompatSchemaRecursive(value)
@ -204,7 +179,7 @@ export function findOpenAIStrictSchemaViolations(
// Schema maps contain user-chosen names. Walk their values as schemas, but
// never interpret map keys such as `$defs.anyOf` as schema keywords.
for (const key of OPENAI_STRICT_COMPAT_SCHEMA_MAP_KEYS) {
for (const key of SCHEMA_MAP_KEYS) {
const schemaMap = record[key];
if (!isRecord(schemaMap)) {
continue;

View file

@ -1,43 +1,17 @@
import { evaluateSchemaWalk, type SchemaWalk } from "./schema-walk.js";
// This helper accepts draft-07 through 2020-12 schemas. Keep the union of
// schema-bearing keys aligned with the package's dialect-specific walkers.
const SCHEMA_MAP_KEYS = new Set([
"$defs",
"definitions",
"dependentSchemas",
// Draft-07 dependencies mix schemas with property-name arrays. Stripping
// leaves the string entries in those arrays unchanged.
"dependencies",
"patternProperties",
"properties",
]);
/** Containers whose value is a single nested schema. */
const SCHEMA_OBJECT_KEYS = new Set([
"additionalItems",
"additionalProperties",
"contains",
"contentSchema",
"else",
"if",
"items",
"not",
"propertyNames",
"then",
"unevaluatedItems",
"unevaluatedProperties",
]);
/** Containers whose value is a list of nested schemas. */
const SCHEMA_ARRAY_KEYS = new Set(["allOf", "anyOf", "items", "oneOf", "prefixItems"]);
import {
evaluateSchemaWalk,
SCHEMA_ARRAY_KEYS,
SCHEMA_MAP_KEYS,
SCHEMA_OBJECT_KEYS,
type SchemaWalk,
} from "./schema-walk.js";
function* stripSchemaArray(
schemas: unknown[],
unsupportedKeywords: ReadonlySet<string>,
ancestors: Set<object>,
result: unknown[],
): SchemaWalk {
const result: unknown[] = [];
result.length = schemas.length;
for (let index = 0; index < result.length; index += 1) {
if (index in schemas) {
@ -61,9 +35,7 @@ function* stripSchemaKeywords(
ancestors.add(schema);
try {
if (Array.isArray(schema)) {
const result: unknown[] = [];
yield stripSchemaArray(schema, unsupportedKeywords, ancestors, result);
return result;
return yield stripSchemaArray(schema, unsupportedKeywords, ancestors);
}
const obj = schema as Record<string, unknown>;
const cleaned: Record<string, unknown> = {};
@ -78,9 +50,7 @@ function* stripSchemaKeywords(
}
cleaned[key] = Object.fromEntries(entries);
} else if (SCHEMA_ARRAY_KEYS.has(key) && Array.isArray(value)) {
const result: unknown[] = [];
yield stripSchemaArray(value, unsupportedKeywords, ancestors, result);
cleaned[key] = result;
cleaned[key] = yield stripSchemaArray(value, unsupportedKeywords, ancestors);
} else if (SCHEMA_OBJECT_KEYS.has(key) && value && typeof value === "object") {
cleaned[key] = yield stripSchemaKeywords(value, unsupportedKeywords, ancestors);
} else {

View file

@ -1,3 +1,34 @@
// Schema-bearing containers shared by the draft-07 through 2020-12 walkers.
export const SCHEMA_MAP_KEYS = new Set([
"$defs",
"definitions",
"dependentSchemas",
// Draft-07 dependencies mix schemas with property-name arrays. Stripping
// leaves the string entries in those arrays unchanged.
"dependencies",
"patternProperties",
"properties",
]);
/** Containers whose value is a single nested schema. */
export const SCHEMA_OBJECT_KEYS = new Set([
"additionalItems",
"additionalProperties",
"contains",
"contentSchema",
"else",
"if",
"items",
"not",
"propertyNames",
"then",
"unevaluatedItems",
"unevaluatedProperties",
]);
/** Containers whose value is a list of nested schemas. */
export const SCHEMA_ARRAY_KEYS = new Set(["allOf", "anyOf", "items", "oneOf", "prefixItems"]);
export type SchemaWalk = Generator<SchemaWalk, unknown, unknown>;
/** Resume child schema walks on a heap stack, never through recursive yield delegation. */

View file

@ -1,6 +1,7 @@
import { types as utilTypes } from "node:util";
import { expectDefined } from "@openclaw/normalization-core/expect";
import { isRecord as isJsonObject } from "@openclaw/normalization-core/record-coerce";
import { SCHEMA_MAP_KEYS } from "./schema-walk.js";
import type { PreparedToolSchemaNormalization } from "./tool-schema-normalization-cache.js";
/** JSON-safe schema value used when projecting runtime tool parameters. */
@ -97,15 +98,6 @@ function serializeToolInputSchema(
};
}
const schemaMapKeywords = new Set([
"$defs",
"definitions",
"dependencies",
"dependentSchemas",
"patternProperties",
"properties",
]);
function inspectJsonSchema(
schema: RuntimeToolInputSchemaJson,
path: (string | number)[],
@ -142,7 +134,7 @@ function inspectJsonSchema(
continue;
}
path.push(".", key);
if (schemaMapKeywords.has(key) && isJsonObject(value)) {
if (SCHEMA_MAP_KEYS.has(key) && isJsonObject(value)) {
for (const schemaName of Object.keys(value)) {
const childSchema = value[schemaName];
if (childSchema === undefined) {

View file

@ -79,18 +79,6 @@ type AnthropicTransportModel = Model<"anthropic-messages"> & {
provider: string;
};
type AnthropicMessagesClient = {
messages: {
stream(
params: Record<string, unknown>,
options?: { signal?: AbortSignal; headers?: Record<string, string> },
): Promise<{
response: Response;
stream: AsyncIterable<Record<string, unknown>> | Iterable<Record<string, unknown>>;
}>;
};
};
function resolveAnthropicRequestModelId(model: AnthropicTransportModel): string {
if (isDirectAnthropicModel(model) && /^anthropic\//i.test(model.id)) {
return model.id.replace(/^anthropic\//i, "");
@ -290,46 +278,42 @@ async function* parseAnthropicSseBody(
}
}
function createAnthropicMessagesClient(params: {
function createAnthropicMessageRequest(params: {
apiKey?: string | null;
authToken?: string;
baseURL?: string;
defaultHeaders?: Record<string, string>;
fetch: typeof fetch;
}): AnthropicMessagesClient {
}) {
const url = resolveAnthropicMessagesUrl(params.baseURL);
return {
messages: {
async stream(
body: Record<string, unknown>,
options?: { signal?: AbortSignal; headers?: Record<string, string> },
) {
const headers = new Headers(
mergeTransportHeaders(
{
"content-type": "application/json",
"anthropic-version": "2023-06-01",
...(params.apiKey ? { "x-api-key": params.apiKey } : {}),
...(params.authToken ? { authorization: `Bearer ${params.authToken}` } : {}),
},
params.defaultHeaders,
),
);
for (const [name, value] of Object.entries(options?.headers ?? {})) {
headers.set(name, value);
}
const response = await params.fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
signal: options?.signal,
});
return {
response,
stream: response.body ? parseAnthropicSseBody(response.body, options?.signal) : [],
};
},
},
return async (
body: Record<string, unknown>,
options?: { signal?: AbortSignal; headers?: Record<string, string> },
) => {
const headers = new Headers(
mergeTransportHeaders(
{
"content-type": "application/json",
"anthropic-version": "2023-06-01",
...(params.apiKey ? { "x-api-key": params.apiKey } : {}),
...(params.authToken ? { authorization: `Bearer ${params.authToken}` } : {}),
},
params.defaultHeaders,
),
);
for (const [name, value] of Object.entries(options?.headers ?? {})) {
headers.set(name, value);
}
const response = await params.fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
signal: options?.signal,
});
return {
response,
stream: response.body ? parseAnthropicSseBody(response.body, options?.signal) : [],
};
};
}
@ -385,7 +369,7 @@ function createAnthropicTransportClient(params: {
if (copilot || usesFoundryBearerAuth(resolveModelHeaderSentinels(model))) {
const betaFeatures = needsInterleavedBeta ? ["interleaved-thinking-2025-05-14"] : [];
return {
client: createAnthropicMessagesClient({
request: createAnthropicMessageRequest({
apiKey: null,
authToken: apiKey,
baseURL: model.baseUrl,
@ -412,7 +396,7 @@ function createAnthropicTransportClient(params: {
const betaHeader = buildAnthropicBetaHeader(model, betaFeatures, { oauth: true });
const identity = buildAnthropicClaudeCodeIdentity(betaHeader, model.headers, optionHeaders);
return {
client: createAnthropicMessagesClient({
request: createAnthropicMessageRequest({
apiKey: null,
authToken: apiKey,
baseURL: model.baseUrl,
@ -439,7 +423,7 @@ function createAnthropicTransportClient(params: {
optionHeaders,
);
return {
client: createAnthropicMessagesClient({
request: createAnthropicMessageRequest({
apiKey,
baseURL: model.baseUrl,
defaultHeaders,
@ -569,7 +553,7 @@ export function createAnthropicMessagesTransportStreamFn(): StreamFn {
}
const transportOptions = resolveAnthropicTransportOptions(model, options, apiKey);
const requestContext = prepareClaudeNoPrefillRequestContext(model, context);
const { client, isOAuthToken, directApiKeyBetaHeader, claudeCodeVersion } =
const { request, isOAuthToken, directApiKeyBetaHeader, claudeCodeVersion } =
createAnthropicTransportClient({
model,
context: requestContext,
@ -600,7 +584,7 @@ export function createAnthropicMessagesTransportStreamFn(): StreamFn {
const bindingHeaders =
applyAnthropicThinkingBindingControls(params, betaHeader) ??
(betaHeader ? { "anthropic-beta": betaHeader } : undefined);
const { response, stream: anthropicStream } = await client.messages.stream(
const { response, stream: anthropicStream } = await request(
{ ...params, stream: true },
{ signal: transportOptions.signal, headers: bindingHeaders },
);

View file

@ -36,13 +36,6 @@ type OpenAIResponsesCompactionSuppressionState = ProviderReplayState & {
baseUrlHash: string;
};
export function buildOpenAIResponsesReplayContext(
model: Model,
options?: Pick<BaseOpenAIStreamOptions, "authProfileId" | "sessionId">,
): OpenAIResponsesReplayContext {
return buildProviderReplayContext(model, options);
}
export function isOpenAIResponsesReplayContext(
value: unknown,
): value is OpenAIResponsesReplayContext {
@ -155,7 +148,7 @@ export function suppressOpenAIResponsesCompaction(
options?: Pick<BaseOpenAIStreamOptions, "authProfileId" | "onCompactionRejected" | "sessionId">,
rejectedCheckpoint?: OpenAIResponsesCompactionRejection,
): void {
const context = buildOpenAIResponsesReplayContext(model, options);
const context = buildProviderReplayContext(model, options);
if (!context.baseUrlHash) {
return;
}
@ -239,7 +232,7 @@ export function resolveNewestOpenAIResponsesCompactionReplay(
}
| { owner: AssistantMessage; mode: "refresh-required" }
| undefined {
const context = buildOpenAIResponsesReplayContext(model, options);
const context = buildProviderReplayContext(model, options);
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (message?.role !== "assistant") {
@ -363,6 +356,6 @@ export function buildOpenAIResponsesReasoningReplayMetadata(
return {
v: 1,
source: "openai-responses",
...buildOpenAIResponsesReplayContext(model, options),
...buildProviderReplayContext(model, options),
};
}

View file

@ -16,7 +16,6 @@ import {
import { shortHash } from "../utils/hash.js";
import { stripSystemPromptCacheBoundary } from "../utils/system-prompt-cache-boundary.js";
import {
buildOpenAIResponsesReplayContext,
buildOpenAIResponsesCompactionReplayPlan,
isOpenAIResponsesReplayContext,
isSafeResponsesReplayItemId,
@ -33,7 +32,10 @@ import {
} from "./openai-responses-contracts.js";
import { createResponsesInputReplay } from "./openai-responses-input-replay.js";
import { resolveReplayableResponsesMessageId } from "./openai-responses-replay.js";
import { providerReplayContextMatches } from "./provider-replay-context.js";
import {
buildProviderReplayContext,
providerReplayContextMatches,
} from "./provider-replay-context.js";
import {
sanitizeNonEmptyTransportPayloadText,
sanitizeTransportPayloadText,
@ -256,7 +258,7 @@ function convertResponsesMessagesWithStyle(
const providerStyle = conversionStyle === "provider";
const shouldReplayReasoningItems = options?.replayReasoningItems ?? true;
const shouldReplayResponsesItemIds = options?.replayResponsesItemIds ?? true;
const replayContext = buildOpenAIResponsesReplayContext(model, {
const replayContext = buildProviderReplayContext(model, {
sessionId: options?.sessionId,
authProfileId: options?.authProfileId,
});

View file

@ -10,23 +10,6 @@ function isControlCharacter(char: string): boolean {
return codePoint !== undefined && codePoint >= 0x00 && codePoint <= 0x1f;
}
function escapeControlCharacter(char: string): string {
switch (char) {
case "\b":
return "\\b";
case "\f":
return "\\f";
case "\n":
return "\\n";
case "\r":
return "\\r";
case "\t":
return "\\t";
default:
return `\\u${char.codePointAt(0)?.toString(16).padStart(4, "0") ?? "0000"}`;
}
}
/**
* Repairs malformed JSON string literals by:
* - escaping raw control characters inside strings
@ -115,7 +98,7 @@ export function repairJson(
continue;
}
repaired += isControlCharacter(char) ? escapeControlCharacter(char) : char;
repaired += isControlCharacter(char) ? JSON.stringify(char).slice(1, -1) : char;
stringValuePrefix += char;
}

View file

@ -1,16 +1,4 @@
/**
* Bounded SSE / NDJSON stream reader guard.
*
* Wraps a `ReadableStreamDefaultReader<Uint8Array>` so the caller's existing
* chunk-by-chunk parsing logic is unchanged, but accumulated bytes are tracked
* against a hard cap. On overflow the underlying reader is cancelled and a
* canonical error is thrown. Mirrors the `readResponseWithLimit` / bounded
* JSON response pattern (see `src/agents/provider-http-errors.ts`).
*
* Internal helper for now. If extensions need it, promote to a plugin-SDK
* subpath in a separate, dedicated PR with full SDK metadata sync.
*/
/** Bounds cumulative stream bytes without waiting for reader cleanup after overflow. */
export type SseStreamOverflow = {
size: number;
maxBytes: number;

View file

@ -283,7 +283,7 @@ export class GatewayPendingRequests {
errorCode?: string,
): void {
const endedAtMs = this.opts.nowMs();
try {
this.invoke("request timing", () =>
this.opts.onTiming?.({
id,
method: pending.method,
@ -292,10 +292,8 @@ export class GatewayPendingRequests {
startedAtMs: pending.startedAtMs,
endedAtMs,
errorCode,
});
} catch (error) {
this.opts.onCallbackError?.("request timing", error);
}
}),
);
}
private invoke(label: string, callback: () => void): void {

View file

@ -14,12 +14,7 @@ function isTestRuntimeEnv(env: NodeJS.ProcessEnv): boolean {
env.VITEST_POOL_ID !== undefined ||
env.VITEST_WORKER_ID !== undefined ||
env.NODE_ENV === "test" ||
(env !== process.env &&
(process.env.VITEST === "true" ||
process.env.VITEST === "1" ||
process.env.VITEST_POOL_ID !== undefined ||
process.env.VITEST_WORKER_ID !== undefined ||
process.env.NODE_ENV === "test"))
(env !== process.env && isTestRuntimeEnv(process.env))
);
}

View file

@ -54,141 +54,83 @@ function matchRoleAt(
return null;
}
function findDelimitedEnd(params: {
text: string;
contentStart: number;
lineEnd: number;
close: "]" | ">";
minContentLength: number;
maxContentLength: number;
}): number | null {
const searchEnd = Math.min(params.lineEnd, params.contentStart + params.maxContentLength + 1);
let closeAt = -1;
for (let index = params.contentStart; index < searchEnd; index += 1) {
const char = params.text[index];
function findDelimitedEnd(
text: string,
contentStart: number,
lineEnd: number,
close: "]" | ">",
minContentLength: number,
): number | null {
const searchEnd = Math.min(lineEnd, contentStart + 160 + 1);
for (let index = contentStart; index < searchEnd; index += 1) {
const char = text[index];
// Paired backticks are parsed as code and excluded earlier. An unmatched
// delimiter leaves a header that target renderers cannot wrap consistently.
if (char === "`") {
return null;
}
if (char === params.close) {
closeAt = index;
break;
if (char === close) {
return index - contentStart >= minContentLength ? index + 1 : null;
}
}
if (closeAt === -1) {
return null;
}
const contentLength = closeAt - params.contentStart;
if (contentLength < params.minContentLength || contentLength > params.maxContentLength) {
return null;
}
return closeAt + 1;
return null;
}
function isHeaderBoundary(char: string | undefined): boolean {
return char === undefined || isLineTrailingWhitespace(char) || char === ":" || char === ":";
}
function matchRoleTimestampHeader(
function matchRoleHeader(
text: string,
start: number,
lineEnd: number,
): AssistantTranscriptRoleHeaderSpan | null {
const role = matchRoleAt(text, start, lineEnd);
if (!role) {
return null;
const opener = text[start];
let roleStart = start;
if (opener === "[") {
const bracketEnd = findDelimitedEnd(text, start + 1, lineEnd, "]", 4);
if (!bracketEnd) {
return null;
}
roleStart = skipHorizontalWhitespace(text, bracketEnd, lineEnd);
} else if (opener === "<") {
roleStart = skipHorizontalWhitespace(text, start + 1, lineEnd);
}
const bracketStart = skipHorizontalWhitespace(text, role.end, lineEnd);
if (text[bracketStart] !== "[") {
return null;
}
const headerEnd = findDelimitedEnd({
text,
contentStart: bracketStart + 1,
lineEnd,
close: "]",
minContentLength: 1,
maxContentLength: 160,
});
if (!headerEnd || !isHeaderBoundary(text[headerEnd])) {
return null;
}
return {
start,
end: headerEnd,
kind: "role_timestamp_bracket",
role: role.role,
};
}
function matchTimestampRoleHeader(
text: string,
start: number,
lineEnd: number,
): AssistantTranscriptRoleHeaderSpan | null {
if (text[start] !== "[") {
return null;
}
const bracketEnd = findDelimitedEnd({
text,
contentStart: start + 1,
lineEnd,
close: "]",
minContentLength: 4,
maxContentLength: 160,
});
if (!bracketEnd) {
return null;
}
const roleStart = skipHorizontalWhitespace(text, bracketEnd, lineEnd);
const role = matchRoleAt(text, roleStart, lineEnd);
if (!role) {
return null;
}
const colonAt = skipHorizontalWhitespace(text, role.end, lineEnd);
if (text[colonAt] !== ":" && text[colonAt] !== ":") {
return null;
}
return {
start,
end: colonAt + 1,
kind: "timestamp_role_colon",
role: role.role,
};
}
function matchAngleRoleHeader(
text: string,
start: number,
lineEnd: number,
): AssistantTranscriptRoleHeaderSpan | null {
if (text[start] !== "<") {
return null;
let end: number | null;
let kind: AssistantTranscriptRoleHeaderKind;
if (opener === "[") {
const colonAt = skipHorizontalWhitespace(text, role.end, lineEnd);
if (text[colonAt] !== ":" && text[colonAt] !== ":") {
return null;
}
end = colonAt + 1;
kind = "timestamp_role_colon";
} else {
if (opener === "<") {
const boundary = text[role.end];
if (boundary !== ">" && !isHorizontalWhitespace(boundary)) {
return null;
}
end = findDelimitedEnd(text, role.end, lineEnd, ">", 0);
kind = "angle_role_header";
} else {
const bracketStart = skipHorizontalWhitespace(text, role.end, lineEnd);
if (text[bracketStart] !== "[") {
return null;
}
end = findDelimitedEnd(text, bracketStart + 1, lineEnd, "]", 1);
kind = "role_timestamp_bracket";
}
if (!end || !isHeaderBoundary(text[end])) {
return null;
}
}
const roleStart = skipHorizontalWhitespace(text, start + 1, lineEnd);
const role = matchRoleAt(text, roleStart, lineEnd);
const roleBoundary = role ? text[role.end] : undefined;
if (!role || (roleBoundary !== ">" && !isHorizontalWhitespace(roleBoundary))) {
return null;
}
const headerEnd = findDelimitedEnd({
text,
contentStart: role.end,
lineEnd,
close: ">",
minContentLength: 0,
maxContentLength: 160,
});
if (!headerEnd || !isHeaderBoundary(text[headerEnd])) {
return null;
}
return {
start,
end: headerEnd,
kind: "angle_role_header",
role: role.role,
};
return { start, end, kind, role: role.role };
}
function rangesOverlap(left: TextRange, right: TextRange): boolean {
@ -213,10 +155,7 @@ export function findAssistantTranscriptRoleHeaderSpans(
const newlineAt = text.indexOf("\n", lineStart);
const lineEnd = newlineAt === -1 ? text.length : newlineAt;
const contentStart = skipHorizontalWhitespace(text, lineStart, lineEnd);
const span =
matchTimestampRoleHeader(text, contentStart, lineEnd) ??
matchAngleRoleHeader(text, contentStart, lineEnd) ??
matchRoleTimestampHeader(text, contentStart, lineEnd);
const span = matchRoleHeader(text, contentStart, lineEnd);
if (span) {
for (;;) {
const excludedRange = sortedExcludedRanges[excludedRangeIndex];

View file

@ -137,33 +137,26 @@ function parseYamlFrontmatterOnce(
fallback: ParsedFrontmatter,
): ParsedYamlFrontmatterAttempt {
let doc: ReturnType<typeof parseDocument> | undefined;
const failed = (issues: FrontmatterParseIssue[]): ParsedYamlFrontmatterAttempt => ({
document: doc,
result: { frontmatter: fallback, issues },
});
try {
doc = parseDocument(block, { schema: "core", prettyErrors: false });
if (doc.errors.length > 0 || !isMap(doc.contents)) {
return {
document: doc,
result: {
frontmatter: fallback,
issues:
doc.errors.length > 0
? doc.errors.map((error) => ({
code: error.code ?? error.name,
message: error.message,
}))
: [{ code: "INVALID_ROOT", message: "frontmatter must be a YAML mapping" }],
},
};
return failed(
doc.errors.length > 0
? doc.errors.map((error) => ({
code: error.code ?? error.name,
message: error.message,
}))
: [{ code: "INVALID_ROOT", message: "frontmatter must be a YAML mapping" }],
);
}
const parsed = doc.toJS() as unknown;
if (!isRecord(parsed)) {
return {
document: doc,
result: {
frontmatter: fallback,
issues: [{ code: "INVALID_ROOT", message: "frontmatter must be a YAML mapping" }],
},
};
return failed([{ code: "INVALID_ROOT", message: "frontmatter must be a YAML mapping" }]);
}
const inlineColonKeys = new Set<string>();
@ -184,7 +177,7 @@ function parseYamlFrontmatterOnce(
}
const result: ParsedFrontmatter = {};
for (const [rawKey, value] of Object.entries(parsed as Record<string, unknown>)) {
for (const [rawKey, value] of Object.entries(parsed)) {
const key = rawKey.trim();
const coerced = key ? coerceYamlFrontmatterValue(value) : undefined;
if (!coerced) {
@ -205,13 +198,7 @@ function parseYamlFrontmatterOnce(
return { document: doc, result: { frontmatter: result, issues: [] } };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
document: doc,
result: {
frontmatter: fallback,
issues: [{ code: "YAML_EXCEPTION", message }],
},
};
return failed([{ code: "YAML_EXCEPTION", message }]);
}
}

View file

@ -1,5 +1,3 @@
// Public barrel for media-generation shared model refs and catalog helpers.
export * from "./capability-model-ref.js";
export * from "./catalog.js";
export * from "./model-ref.js";

View file

@ -1,5 +1,3 @@
// Active media-understanding model selection contract.
/** Provider/model pair selected for one media-understanding request. */
export type ActiveMediaModel = {
provider: string;

View file

@ -1,7 +1,5 @@
import type { MediaUnderstandingCapability } from "./types.js";
// Shared defaults for media-understanding limits, prompts, and concurrency.
const MB = 1024 * 1024;
/** Default max response characters for bounded text outputs. */

View file

@ -1,5 +1,3 @@
// Media-understanding skip error used for non-fatal attachment omissions.
/** Reason a media-understanding attachment was skipped. */
type MediaUnderstandingSkipReason =
| "maxBytes"

View file

@ -1,5 +1,3 @@
// OpenAI-compatible video request/response helpers.
/** Minimal OpenAI-compatible video response payload shape. */
export type OpenAiCompatibleVideoPayload = {
choices?: Array<{

View file

@ -1,5 +1,3 @@
// Output extractors for media-understanding provider CLI responses.
/** Parse the last JSON object in a noisy provider output string. */
function extractLastJsonObject(raw: string): unknown {
const trimmed = raw.trim();

View file

@ -1,30 +1,17 @@
// Provider id normalization for media-understanding config and execution.
/** Normalize a provider id for comparison. */
function normalizeProviderId(provider: string): string {
return provider.trim().toLowerCase();
}
const MEDIA_PROVIDER_ALIASES = new Map([
["gemini", "google"],
["minimax-cn", "minimax"],
["minimax-portal-cn", "minimax-portal"],
]);
/** Normalize provider aliases to canonical config provider ids. */
export function normalizeMediaProviderId(id: string): string {
const normalized = normalizeProviderId(id);
if (normalized === "gemini") {
return "google";
}
if (normalized === "minimax-cn") {
return "minimax";
}
if (normalized === "minimax-portal-cn") {
return "minimax-portal";
}
return normalized;
const normalized = id.trim().toLowerCase();
return MEDIA_PROVIDER_ALIASES.get(normalized) ?? normalized;
}
/** Normalize provider ids while preserving execution-specific regional aliases. */
export function normalizeMediaExecutionProviderId(id: string): string {
const normalized = normalizeProviderId(id);
if (normalized === "minimax-cn" || normalized === "minimax-portal-cn") {
return normalized;
}
return normalizeMediaProviderId(normalized);
const normalized = id.trim().toLowerCase();
return normalized === "gemini" ? "google" : normalized;
}

View file

@ -8,8 +8,6 @@ type MediaCapabilityProvider = {
describeVideo?: unknown;
};
// Capability checks for media-understanding provider objects.
/** Image providers can use shared model dispatch; audio/video require registered methods. */
export function providerSupportsCapability(
provider: MediaCapabilityProvider | undefined,

View file

@ -1,5 +1,3 @@
// Shared media-understanding provider, attachment, output, and capability contracts.
/** Kind of media-understanding output produced for an attachment. */
export type MediaUnderstandingKind =
| "audio.transcription"

View file

@ -1,7 +1,5 @@
import { DEFAULT_VIDEO_MAX_BASE64_BYTES } from "./defaults.js";
// Video payload size helpers for base64-expanded request bodies.
/** Estimate base64 size for a byte count. */
export function estimateBase64Size(bytes: number): number {
return Math.ceil(bytes / 3) * 4;

View file

@ -105,12 +105,9 @@ export function classifyMemoryMultimodalPath(
return null;
}
const lower = normalizeLowercaseStringOrEmpty(filePath);
for (const modality of settings.modalities) {
for (const extension of getMemoryMultimodalExtensions(modality)) {
if (lower.endsWith(extension)) {
return modality;
}
}
}
return null;
return (
settings.modalities.find((modality) =>
getMemoryMultimodalExtensions(modality).some((extension) => lower.endsWith(extension)),
) ?? null
);
}

View file

@ -6,29 +6,15 @@ const TRANSIENT_MEMORY_READ_ERRNO = -11;
const TRANSIENT_MEMORY_READ_CODES = new Set(["EAGAIN", "EWOULDBLOCK", "EDEADLK"]);
const TRANSIENT_MEMORY_READ_MESSAGE = /Unknown system error -11\b/i;
/** Extract errno from Node filesystem-style errors. */
function getErrno(error: unknown): number | undefined {
return typeof (error as NodeJS.ErrnoException | undefined)?.errno === "number"
? (error as NodeJS.ErrnoException).errno
: undefined;
}
/** Extract code from Node filesystem-style errors. */
function getCode(error: unknown): string | undefined {
return typeof (error as NodeJS.ErrnoException | undefined)?.code === "string"
? (error as NodeJS.ErrnoException).code
: undefined;
}
/** Return true for transient memory read failures that should be retried. */
export function isTransientMemoryReadError(error: unknown): boolean {
const code = getCode(error);
if (code && TRANSIENT_MEMORY_READ_CODES.has(code)) {
const details = error as { code?: unknown; errno?: unknown } | null | undefined;
const code = details?.code;
if (typeof code === "string" && TRANSIENT_MEMORY_READ_CODES.has(code)) {
return true;
}
const errno = getErrno(error);
if (errno === TRANSIENT_MEMORY_READ_ERRNO) {
if (details?.errno === TRANSIENT_MEMORY_READ_ERRNO) {
return true;
}
@ -45,6 +31,6 @@ export async function retryTransientMemoryRead<T>(
minDelayMs: 25,
maxDelayMs: 50,
label,
shouldRetry: (error) => isTransientMemoryReadError(error),
shouldRetry: isTransientMemoryReadError,
});
}

View file

@ -40,21 +40,14 @@ export function createMemorySearchDeadlineControl(): MemorySearchDeadlineControl
report(action) {
if (action === "pause") {
depth += 1;
if (depth === 1) {
for (const listener of listeners) {
listener("pause");
}
if (depth !== 1) {
return;
}
} else if (depth === 0 || --depth !== 0) {
return;
}
if (depth === 0) {
return;
}
depth -= 1;
if (depth === 0) {
for (const listener of listeners) {
listener("resume");
}
for (const listener of listeners) {
listener(action === "pause" ? "pause" : "resume");
}
},
subscribe(listener) {

View file

@ -1,5 +1,3 @@
// Public barrel for model catalog normalization, ids, refs, and types.
export * from "./configured-model-refs.js";
export * from "./model-catalog-normalize.js";
export * from "./model-catalog-refs.js";

View file

@ -3,8 +3,6 @@ import { normalizeProviderId } from "./provider-id.js";
export { normalizeProviderId as normalizeModelCatalogProviderId } from "./provider-id.js";
// Stable model catalog ref and merge-key builders.
export type ModelCatalogRef = {
provider: string;
modelId: string;

View file

@ -1,4 +1,3 @@
// Shared model catalog data contracts for provider manifests and normalized rows.
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
import {
MODEL_DATA_APIS,

View file

@ -1,5 +1,16 @@
const ANTIGRAVITY_BARE_PRO_IDS = new Set(["gemini-3-pro", "gemini-3.1-pro", "gemini-3-1-pro"]);
const GOOGLE_PROVIDER_PREFIX = "google/";
const GOOGLE_MODEL_ALIASES = new Map([
["gemini-3-pro", "gemini-3.1-pro-preview"],
["gemini-3-pro-preview", "gemini-3.1-pro-preview"],
["gemini-3-flash", "gemini-3-flash-preview"],
["gemini-3.1-pro", "gemini-3.1-pro-preview"],
// The Flash Lite preview endpoint retired after the model graduated to GA.
["gemini-3.1-flash-lite-preview", "gemini-3.1-flash-lite"],
["gemini-3.1-flash", "gemini-3-flash-preview"],
["gemini-3.1-flash-preview", "gemini-3-flash-preview"],
["gemma-4-26b", "gemma-4-26b-a4b-it"],
]);
export function normalizeGooglePreviewModelId(id: string): string {
if (id.startsWith(GOOGLE_PROVIDER_PREFIX)) {
@ -7,35 +18,11 @@ export function normalizeGooglePreviewModelId(id: string): string {
const normalizedModelId = normalizeGooglePreviewModelId(modelId);
return normalizedModelId === modelId ? id : `${GOOGLE_PROVIDER_PREFIX}${normalizedModelId}`;
}
if (id === "gemini-3-pro" || id === "gemini-3-pro-preview") {
return "gemini-3.1-pro-preview";
}
if (id === "gemini-3-flash") {
return "gemini-3-flash-preview";
}
if (id === "gemini-3.1-pro") {
return "gemini-3.1-pro-preview";
}
// Gemini 3.1 Flash Lite graduated to GA on 2026-05-07; the -preview
// endpoint is deprecated (shutdown 2026-05-25). Map old preview name
// to the stable GA id.
if (id === "gemini-3.1-flash-lite-preview") {
return "gemini-3.1-flash-lite";
}
if (id === "gemini-3.1-flash" || id === "gemini-3.1-flash-preview") {
return "gemini-3-flash-preview";
}
if (id === "gemma-4-26b") {
return "gemma-4-26b-a4b-it";
}
return id;
return GOOGLE_MODEL_ALIASES.get(id) ?? id;
}
export function normalizeTogetherModelId(id: string): string {
if (id === "moonshotai/Kimi-K2.5") {
return "moonshotai/Kimi-K2.6";
}
return id;
return id === "moonshotai/Kimi-K2.5" ? "moonshotai/Kimi-K2.6" : id;
}
export function normalizeAntigravityPreviewModelId(id: string): string {

View file

@ -1,5 +1,3 @@
// Public network policy package surface for IP parsing, redaction, and URL
// userinfo stripping helpers.
export * from "./ip.js";
export * from "./ipv4.js";
export * from "./redact-sensitive-url.js";

View file

@ -63,6 +63,7 @@ const schemaMapKeywords = new Set([
"$defs",
"definitions",
"dependentSchemas",
"dependencies",
"patternProperties",
"properties",
]);
@ -96,12 +97,21 @@ type NormalizationOptions = {
format?: "annotation";
};
function normalizeSchemaMap(value: unknown, options: NormalizationOptions): unknown {
function normalizeSchemaMap(
value: unknown,
options: NormalizationOptions,
preserveStringArrays: boolean,
): unknown {
if (!isRecord(value)) {
return value;
}
return Object.fromEntries(
Object.entries(value).map(([key, entry]) => [key, normalizeJsonSchemaNode(entry, options)]),
Object.entries(value).map(([key, entry]) => [
key,
preserveStringArrays && isStringArray(entry)
? entry
: normalizeJsonSchemaNode(entry, options),
]),
);
}
@ -128,18 +138,6 @@ function repairJsonSchemaPatternForUnicodeRegExp(pattern: string): string {
return compilesUnicodePattern(repaired) ? repaired : pattern;
}
function normalizeSchemaDependencies(value: unknown, options: NormalizationOptions): unknown {
if (!isRecord(value)) {
return value;
}
return Object.fromEntries(
Object.entries(value).map(([key, entry]) => [
key,
isStringArray(entry) ? entry : normalizeJsonSchemaNode(entry, options),
]),
);
}
function normalizePatternProperties(
value: Record<string, unknown>,
options: NormalizationOptions,
@ -231,10 +229,7 @@ function normalizeJsonSchemaNode(schema: unknown, options: NormalizationOptions)
return [key, normalizePatternProperties(value, options)];
}
if (schemaMapKeywords.has(key)) {
return [key, normalizeSchemaMap(value, options)];
}
if (key === "dependencies") {
return [key, normalizeSchemaDependencies(value, options)];
return [key, normalizeSchemaMap(value, options, key === "dependencies")];
}
if (schemaValueKeywords.has(key) || schemaArrayKeywords.has(key)) {
return [key, normalizeJsonSchemaNode(value, options)];

View file

@ -1,3 +1 @@
// Public package facade for account id normalization helpers.
export * from "../../../src/plugin-sdk/account-id.js";

View file

@ -1,3 +1 @@
// Public package facade for ACP runtime backend helpers.
export * from "../../../src/plugin-sdk/acp-runtime-backend.js";

View file

@ -1,3 +1 @@
// Public package facade for ACP runtime helpers.
export * from "../../../src/plugin-sdk/acp-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for async lock runtime helpers.
export * from "../../../src/plugin-sdk/async-lock-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for browser config contracts.
export * from "../../../src/plugin-sdk/browser-config.js";

View file

@ -1,3 +1 @@
// Public package facade for channel activity runtime helpers.
export * from "../../../src/plugin-sdk/channel-activity-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for CLI runtime helpers.
export * from "../../../src/plugin-sdk/cli-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for concurrency runtime helpers.
export * from "../../../src/plugin-sdk/concurrency-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for config contracts.
export * from "../../../src/plugin-sdk/config-contracts.js";

View file

@ -1,3 +1 @@
// Public package facade for config mutation helpers.
export * from "../../../src/plugin-sdk/config-mutation.js";

View file

@ -1,3 +1 @@
// Public package facade for config runtime helpers.
export * from "../../../src/plugin-sdk/config-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for passive Control UI link-reader contracts.
export * from "../../../src/plugin-sdk/control-ui-link-reader.js";

View file

@ -1,3 +1 @@
// Public package facade for core plugin SDK contracts.
export * from "../../../src/plugin-sdk/core.js";

View file

@ -1,3 +1 @@
// Public package facade for cron store runtime helpers.
export * from "../../../src/plugin-sdk/cron-store-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for dedupe runtime helpers.
export * from "../../../src/plugin-sdk/dedupe-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for delivery queue runtime helpers.
export * from "../../../src/plugin-sdk/delivery-queue-runtime.js";

View file

@ -1,2 +1 @@
// Public package facade for provider-owned embedding batch contracts.
export * from "../../../src/plugin-sdk/embedding-provider-runtime-contract.js";

View file

@ -1,3 +1 @@
// Public package facade for error runtime helpers.
export * from "../../../src/plugin-sdk/error-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for exec approval runtime helpers.
export * from "../../../src/plugin-sdk/exec-approvals-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for file access runtime helpers.
export * from "../../../src/plugin-sdk/file-access-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for gateway method runtime helpers.
export * from "../../../src/plugin-sdk/gateway-method-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for heartbeat runtime helpers.
export * from "../../../src/plugin-sdk/heartbeat-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for infra runtime helpers.
export * from "../../../src/plugin-sdk/infra-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for model session runtime helpers.
export * from "../../../src/plugin-sdk/model-session-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for shared node-selection policy.
export * from "../../../src/plugin-sdk/node-selection-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for number runtime helpers.
export * from "../../../src/plugin-sdk/number-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for plugin config runtime helpers.
export * from "../../../src/plugin-sdk/plugin-config-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for plugin entry contracts.
export * from "../../../src/plugin-sdk/plugin-entry.js";

View file

@ -1,5 +1,3 @@
// Public package facade for plugin runtime helpers.
export {
clearPluginCommands,
clearPluginInteractiveHandlers,

View file

@ -1,3 +1 @@
// Public package facade for lazy provider auth login flow runtime helpers.
export * from "../../../src/plugin-sdk/provider-auth-login-flow-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for provider auth runtime helpers.
export * from "../../../src/plugin-sdk/provider-auth-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for provider auth contracts.
export * from "../../../src/plugin-sdk/provider-auth.js";

View file

@ -1,3 +1 @@
// Public package facade for provider entry contracts.
export * from "../../../src/plugin-sdk/provider-entry.js";

View file

@ -1,3 +1 @@
// Public package facade for provider environment variable contracts.
export * from "../../../src/plugin-sdk/provider-env-vars.js";

View file

@ -1,3 +1 @@
// Public package facade for provider HTTP helpers.
export * from "../../../src/plugin-sdk/provider-http.js";

View file

@ -1,3 +1 @@
// Public package facade for provider model shared helpers.
export * from "../../../src/plugin-sdk/provider-model-shared.js";

View file

@ -1,3 +1 @@
// Public package facade for provider model type contracts.
export * from "../../../src/plugin-sdk/provider-model-types.js";

View file

@ -1,3 +1 @@
// Public package facade for provider onboarding contracts.
export * from "../../../src/plugin-sdk/provider-onboard.js";

View file

@ -1,3 +1 @@
// Public package facade for provider stream shared helpers.
export * from "../../../src/plugin-sdk/provider-stream-shared.js";

View file

@ -1,3 +1 @@
// Public package facade for provider tool contracts.
export * from "../../../src/plugin-sdk/provider-tools.js";

View file

@ -1,3 +1 @@
// Public package facade for provider usage contracts.
export * from "../../../src/plugin-sdk/provider-usage.js";

View file

@ -1,3 +1 @@
// Public package facade for web search provider config contracts.
export * from "../../../src/plugin-sdk/provider-web-search-config-contract.js";

View file

@ -1,3 +1 @@
// Public package facade for provider web search contracts.
export * from "../../../src/plugin-sdk/provider-web-search-contract.js";

View file

@ -1,3 +1 @@
// Public package facade for web search provider contracts.
export * from "../../../src/plugin-sdk/provider-web-search.js";

View file

@ -1,5 +1,3 @@
// Public package facade for runtime environment helpers.
export {
computeBackoff,
createNonExitingRuntime,

View file

@ -1,3 +1 @@
// Public package facade for secret input contracts.
export * from "../../../src/plugin-sdk/secret-input.js";

View file

@ -1,3 +1 @@
// Public package facade for secret ref runtime helpers.
export * from "../../../src/plugin-sdk/secret-ref-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for secure random runtime helpers.
export * from "../../../src/plugin-sdk/secure-random-runtime.js";

View file

@ -1,5 +1,3 @@
// Public package facade for security runtime helpers.
export {
appendRegularFile,
assertNoSymlinkParents,

View file

@ -1,3 +1 @@
// Public package facade for SSRF runtime helpers.
export * from "../../../src/plugin-sdk/ssrf-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for system event runtime helpers.
export * from "../../../src/plugin-sdk/system-event-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for talk config runtime helpers.
export * from "../../../src/plugin-sdk/talk-config-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for text utility runtime helpers.
export * from "../../../src/plugin-sdk/text-utility-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for time runtime helpers.
export * from "../../../src/plugin-sdk/time-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for transport ready runtime helpers.
export * from "../../../src/plugin-sdk/transport-ready-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for TTS runtime helpers.
export * from "../../../src/plugin-sdk/tts-runtime.js";

View file

@ -1,3 +1 @@
// Public package facade for video generation provider contracts.
export * from "../../../src/plugin-sdk/video-generation.js";

Some files were not shown because too many files have changed in this diff Show more