mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
chore: Enable "curly" rule to avoid single-statement if confusion/errors.
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
"suspicious": "error"
|
"suspicious": "error"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"curly": "error",
|
||||||
"eslint-plugin-unicorn/prefer-array-find": "off",
|
"eslint-plugin-unicorn/prefer-array-find": "off",
|
||||||
"eslint/no-await-in-loop": "off",
|
"eslint/no-await-in-loop": "off",
|
||||||
"eslint/no-new": "off",
|
"eslint/no-new": "off",
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export type AcpClientHandle = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function toArgs(value: string[] | string | undefined): string[] {
|
function toArgs(value: string[] | string | undefined): string[] {
|
||||||
if (!value) return [];
|
if (!value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return Array.isArray(value) ? value : [value];
|
return Array.isArray(value) ? value : [value];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +43,9 @@ function buildServerArgs(opts: AcpClientOptions): string[] {
|
|||||||
|
|
||||||
function printSessionUpdate(notification: SessionNotification): void {
|
function printSessionUpdate(notification: SessionNotification): void {
|
||||||
const update = notification.update;
|
const update = notification.update;
|
||||||
if (!("sessionUpdate" in update)) return;
|
if (!("sessionUpdate" in update)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (update.sessionUpdate) {
|
switch (update.sessionUpdate) {
|
||||||
case "agent_message_chunk": {
|
case "agent_message_chunk": {
|
||||||
@@ -62,7 +66,9 @@ function printSessionUpdate(notification: SessionNotification): void {
|
|||||||
}
|
}
|
||||||
case "available_commands_update": {
|
case "available_commands_update": {
|
||||||
const names = update.availableCommands?.map((cmd) => `/${cmd.name}`).join(" ");
|
const names = update.availableCommands?.map((cmd) => `/${cmd.name}`).join(" ");
|
||||||
if (names) console.log(`\n[commands] ${names}`);
|
if (names) {
|
||||||
|
console.log(`\n[commands] ${names}`);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export function extractTextFromPrompt(prompt: ContentBlock[]): string {
|
|||||||
}
|
}
|
||||||
if (block.type === "resource") {
|
if (block.type === "resource") {
|
||||||
const resource = block.resource as { text?: string } | undefined;
|
const resource = block.resource as { text?: string } | undefined;
|
||||||
if (resource?.text) parts.push(resource.text);
|
if (resource?.text) {
|
||||||
|
parts.push(resource.text);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (block.type === "resource_link") {
|
if (block.type === "resource_link") {
|
||||||
@@ -31,9 +33,13 @@ export function extractTextFromPrompt(prompt: ContentBlock[]): string {
|
|||||||
export function extractAttachmentsFromPrompt(prompt: ContentBlock[]): GatewayAttachment[] {
|
export function extractAttachmentsFromPrompt(prompt: ContentBlock[]): GatewayAttachment[] {
|
||||||
const attachments: GatewayAttachment[] = [];
|
const attachments: GatewayAttachment[] = [];
|
||||||
for (const block of prompt) {
|
for (const block of prompt) {
|
||||||
if (block.type !== "image") continue;
|
if (block.type !== "image") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const image = block as ImageContent;
|
const image = block as ImageContent;
|
||||||
if (!image.data || !image.mimeType) continue;
|
if (!image.data || !image.mimeType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
attachments.push({
|
attachments.push({
|
||||||
type: "image",
|
type: "image",
|
||||||
mimeType: image.mimeType,
|
mimeType: image.mimeType,
|
||||||
@@ -48,7 +54,9 @@ export function formatToolTitle(
|
|||||||
args: Record<string, unknown> | undefined,
|
args: Record<string, unknown> | undefined,
|
||||||
): string {
|
): string {
|
||||||
const base = name ?? "tool";
|
const base = name ?? "tool";
|
||||||
if (!args || Object.keys(args).length === 0) return base;
|
if (!args || Object.keys(args).length === 0) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
const parts = Object.entries(args).map(([key, value]) => {
|
const parts = Object.entries(args).map(([key, value]) => {
|
||||||
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
||||||
const safe = raw.length > 100 ? `${raw.slice(0, 100)}...` : raw;
|
const safe = raw.length > 100 ? `${raw.slice(0, 100)}...` : raw;
|
||||||
@@ -58,16 +66,30 @@ export function formatToolTitle(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function inferToolKind(name?: string): ToolKind {
|
export function inferToolKind(name?: string): ToolKind {
|
||||||
if (!name) return "other";
|
if (!name) {
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
const normalized = name.toLowerCase();
|
const normalized = name.toLowerCase();
|
||||||
if (normalized.includes("read")) return "read";
|
if (normalized.includes("read")) {
|
||||||
if (normalized.includes("write") || normalized.includes("edit")) return "edit";
|
return "read";
|
||||||
if (normalized.includes("delete") || normalized.includes("remove")) return "delete";
|
}
|
||||||
if (normalized.includes("move") || normalized.includes("rename")) return "move";
|
if (normalized.includes("write") || normalized.includes("edit")) {
|
||||||
if (normalized.includes("search") || normalized.includes("find")) return "search";
|
return "edit";
|
||||||
|
}
|
||||||
|
if (normalized.includes("delete") || normalized.includes("remove")) {
|
||||||
|
return "delete";
|
||||||
|
}
|
||||||
|
if (normalized.includes("move") || normalized.includes("rename")) {
|
||||||
|
return "move";
|
||||||
|
}
|
||||||
|
if (normalized.includes("search") || normalized.includes("find")) {
|
||||||
|
return "search";
|
||||||
|
}
|
||||||
if (normalized.includes("exec") || normalized.includes("run") || normalized.includes("bash")) {
|
if (normalized.includes("exec") || normalized.includes("run") || normalized.includes("bash")) {
|
||||||
return "execute";
|
return "execute";
|
||||||
}
|
}
|
||||||
if (normalized.includes("fetch") || normalized.includes("http")) return "fetch";
|
if (normalized.includes("fetch") || normalized.includes("http")) {
|
||||||
|
return "fetch";
|
||||||
|
}
|
||||||
return "other";
|
return "other";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ export function readString(
|
|||||||
meta: Record<string, unknown> | null | undefined,
|
meta: Record<string, unknown> | null | undefined,
|
||||||
keys: string[],
|
keys: string[],
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
if (!meta) return undefined;
|
if (!meta) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const value = meta[key];
|
const value = meta[key];
|
||||||
if (typeof value === "string" && value.trim()) return value.trim();
|
if (typeof value === "string" && value.trim()) {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -14,10 +18,14 @@ export function readBool(
|
|||||||
meta: Record<string, unknown> | null | undefined,
|
meta: Record<string, unknown> | null | undefined,
|
||||||
keys: string[],
|
keys: string[],
|
||||||
): boolean | undefined {
|
): boolean | undefined {
|
||||||
if (!meta) return undefined;
|
if (!meta) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const value = meta[key];
|
const value = meta[key];
|
||||||
if (typeof value === "boolean") return value;
|
if (typeof value === "boolean") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -26,10 +34,14 @@ export function readNumber(
|
|||||||
meta: Record<string, unknown> | null | undefined,
|
meta: Record<string, unknown> | null | undefined,
|
||||||
keys: string[],
|
keys: string[],
|
||||||
): number | undefined {
|
): number | undefined {
|
||||||
if (!meta) return undefined;
|
if (!meta) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const value = meta[key];
|
const value = meta[key];
|
||||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ export type AcpSessionMeta = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function parseSessionMeta(meta: unknown): AcpSessionMeta {
|
export function parseSessionMeta(meta: unknown): AcpSessionMeta {
|
||||||
if (!meta || typeof meta !== "object") return {};
|
if (!meta || typeof meta !== "object") {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
const record = meta as Record<string, unknown>;
|
const record = meta as Record<string, unknown>;
|
||||||
return {
|
return {
|
||||||
sessionKey: readString(record, ["sessionKey", "session", "key"]),
|
sessionKey: readString(record, ["sessionKey", "session", "key"]),
|
||||||
@@ -45,7 +47,9 @@ export async function resolveSessionKey(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (params.meta.sessionKey) {
|
if (params.meta.sessionKey) {
|
||||||
if (!requireExisting) return params.meta.sessionKey;
|
if (!requireExisting) {
|
||||||
|
return params.meta.sessionKey;
|
||||||
|
}
|
||||||
const resolved = await params.gateway.request<{ ok: true; key: string }>("sessions.resolve", {
|
const resolved = await params.gateway.request<{ ok: true; key: string }>("sessions.resolve", {
|
||||||
key: params.meta.sessionKey,
|
key: params.meta.sessionKey,
|
||||||
});
|
});
|
||||||
@@ -66,7 +70,9 @@ export async function resolveSessionKey(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (requestedKey) {
|
if (requestedKey) {
|
||||||
if (!requireExisting) return requestedKey;
|
if (!requireExisting) {
|
||||||
|
return requestedKey;
|
||||||
|
}
|
||||||
const resolved = await params.gateway.request<{ ok: true; key: string }>("sessions.resolve", {
|
const resolved = await params.gateway.request<{ ok: true; key: string }>("sessions.resolve", {
|
||||||
key: requestedKey,
|
key: requestedKey,
|
||||||
});
|
});
|
||||||
@@ -86,6 +92,8 @@ export async function resetSessionIfNeeded(params: {
|
|||||||
opts: AcpServerOptions;
|
opts: AcpServerOptions;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const resetSession = params.meta.resetSession ?? params.opts.resetSession ?? false;
|
const resetSession = params.meta.resetSession ?? params.opts.resetSession ?? false;
|
||||||
if (!resetSession) return;
|
if (!resetSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await params.gateway.request("sessions.reset", { key: params.sessionKey });
|
await params.gateway.request("sessions.reset", { key: params.sessionKey });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ export function createInMemorySessionStore(): AcpSessionStore {
|
|||||||
|
|
||||||
const setActiveRun: AcpSessionStore["setActiveRun"] = (sessionId, runId, abortController) => {
|
const setActiveRun: AcpSessionStore["setActiveRun"] = (sessionId, runId, abortController) => {
|
||||||
const session = sessions.get(sessionId);
|
const session = sessions.get(sessionId);
|
||||||
if (!session) return;
|
if (!session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
session.activeRunId = runId;
|
session.activeRunId = runId;
|
||||||
session.abortController = abortController;
|
session.abortController = abortController;
|
||||||
runIdToSessionId.set(runId, sessionId);
|
runIdToSessionId.set(runId, sessionId);
|
||||||
@@ -47,17 +49,25 @@ export function createInMemorySessionStore(): AcpSessionStore {
|
|||||||
|
|
||||||
const clearActiveRun: AcpSessionStore["clearActiveRun"] = (sessionId) => {
|
const clearActiveRun: AcpSessionStore["clearActiveRun"] = (sessionId) => {
|
||||||
const session = sessions.get(sessionId);
|
const session = sessions.get(sessionId);
|
||||||
if (!session) return;
|
if (!session) {
|
||||||
if (session.activeRunId) runIdToSessionId.delete(session.activeRunId);
|
return;
|
||||||
|
}
|
||||||
|
if (session.activeRunId) {
|
||||||
|
runIdToSessionId.delete(session.activeRunId);
|
||||||
|
}
|
||||||
session.activeRunId = null;
|
session.activeRunId = null;
|
||||||
session.abortController = null;
|
session.abortController = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelActiveRun: AcpSessionStore["cancelActiveRun"] = (sessionId) => {
|
const cancelActiveRun: AcpSessionStore["cancelActiveRun"] = (sessionId) => {
|
||||||
const session = sessions.get(sessionId);
|
const session = sessions.get(sessionId);
|
||||||
if (!session?.abortController) return false;
|
if (!session?.abortController) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
session.abortController.abort();
|
session.abortController.abort();
|
||||||
if (session.activeRunId) runIdToSessionId.delete(session.activeRunId);
|
if (session.activeRunId) {
|
||||||
|
runIdToSessionId.delete(session.activeRunId);
|
||||||
|
}
|
||||||
session.abortController = null;
|
session.abortController = null;
|
||||||
session.activeRunId = null;
|
session.activeRunId = null;
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -210,7 +210,9 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error(`Session ${params.sessionId} not found`);
|
throw new Error(`Session ${params.sessionId} not found`);
|
||||||
}
|
}
|
||||||
if (!params.modeId) return {};
|
if (!params.modeId) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await this.gateway.request("sessions.patch", {
|
await this.gateway.request("sessions.patch", {
|
||||||
key: session.sessionKey,
|
key: session.sessionKey,
|
||||||
@@ -276,7 +278,9 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
|
|
||||||
async cancel(params: CancelNotification): Promise<void> {
|
async cancel(params: CancelNotification): Promise<void> {
|
||||||
const session = this.sessionStore.getSession(params.sessionId);
|
const session = this.sessionStore.getSession(params.sessionId);
|
||||||
if (!session) return;
|
if (!session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.sessionStore.cancelActiveRun(params.sessionId);
|
this.sessionStore.cancelActiveRun(params.sessionId);
|
||||||
try {
|
try {
|
||||||
@@ -294,24 +298,38 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
|
|
||||||
private async handleAgentEvent(evt: EventFrame): Promise<void> {
|
private async handleAgentEvent(evt: EventFrame): Promise<void> {
|
||||||
const payload = evt.payload as Record<string, unknown> | undefined;
|
const payload = evt.payload as Record<string, unknown> | undefined;
|
||||||
if (!payload) return;
|
if (!payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const stream = payload.stream as string | undefined;
|
const stream = payload.stream as string | undefined;
|
||||||
const data = payload.data as Record<string, unknown> | undefined;
|
const data = payload.data as Record<string, unknown> | undefined;
|
||||||
const sessionKey = payload.sessionKey as string | undefined;
|
const sessionKey = payload.sessionKey as string | undefined;
|
||||||
if (!stream || !data || !sessionKey) return;
|
if (!stream || !data || !sessionKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (stream !== "tool") return;
|
if (stream !== "tool") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const phase = data.phase as string | undefined;
|
const phase = data.phase as string | undefined;
|
||||||
const name = data.name as string | undefined;
|
const name = data.name as string | undefined;
|
||||||
const toolCallId = data.toolCallId as string | undefined;
|
const toolCallId = data.toolCallId as string | undefined;
|
||||||
if (!toolCallId) return;
|
if (!toolCallId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const pending = this.findPendingBySessionKey(sessionKey);
|
const pending = this.findPendingBySessionKey(sessionKey);
|
||||||
if (!pending) return;
|
if (!pending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (phase === "start") {
|
if (phase === "start") {
|
||||||
if (!pending.toolCalls) pending.toolCalls = new Set();
|
if (!pending.toolCalls) {
|
||||||
if (pending.toolCalls.has(toolCallId)) return;
|
pending.toolCalls = new Set();
|
||||||
|
}
|
||||||
|
if (pending.toolCalls.has(toolCallId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
pending.toolCalls.add(toolCallId);
|
pending.toolCalls.add(toolCallId);
|
||||||
const args = data.args as Record<string, unknown> | undefined;
|
const args = data.args as Record<string, unknown> | undefined;
|
||||||
await this.connection.sessionUpdate({
|
await this.connection.sessionUpdate({
|
||||||
@@ -344,17 +362,25 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
|
|
||||||
private async handleChatEvent(evt: EventFrame): Promise<void> {
|
private async handleChatEvent(evt: EventFrame): Promise<void> {
|
||||||
const payload = evt.payload as Record<string, unknown> | undefined;
|
const payload = evt.payload as Record<string, unknown> | undefined;
|
||||||
if (!payload) return;
|
if (!payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const sessionKey = payload.sessionKey as string | undefined;
|
const sessionKey = payload.sessionKey as string | undefined;
|
||||||
const state = payload.state as string | undefined;
|
const state = payload.state as string | undefined;
|
||||||
const runId = payload.runId as string | undefined;
|
const runId = payload.runId as string | undefined;
|
||||||
const messageData = payload.message as Record<string, unknown> | undefined;
|
const messageData = payload.message as Record<string, unknown> | undefined;
|
||||||
if (!sessionKey || !state) return;
|
if (!sessionKey || !state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const pending = this.findPendingBySessionKey(sessionKey);
|
const pending = this.findPendingBySessionKey(sessionKey);
|
||||||
if (!pending) return;
|
if (!pending) {
|
||||||
if (runId && pending.idempotencyKey !== runId) return;
|
return;
|
||||||
|
}
|
||||||
|
if (runId && pending.idempotencyKey !== runId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (state === "delta" && messageData) {
|
if (state === "delta" && messageData) {
|
||||||
await this.handleDeltaEvent(pending.sessionId, messageData);
|
await this.handleDeltaEvent(pending.sessionId, messageData);
|
||||||
@@ -381,10 +407,14 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
const content = messageData.content as Array<{ type: string; text?: string }> | undefined;
|
const content = messageData.content as Array<{ type: string; text?: string }> | undefined;
|
||||||
const fullText = content?.find((c) => c.type === "text")?.text ?? "";
|
const fullText = content?.find((c) => c.type === "text")?.text ?? "";
|
||||||
const pending = this.pendingPrompts.get(sessionId);
|
const pending = this.pendingPrompts.get(sessionId);
|
||||||
if (!pending) return;
|
if (!pending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const sentSoFar = pending.sentTextLength ?? 0;
|
const sentSoFar = pending.sentTextLength ?? 0;
|
||||||
if (fullText.length <= sentSoFar) return;
|
if (fullText.length <= sentSoFar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newText = fullText.slice(sentSoFar);
|
const newText = fullText.slice(sentSoFar);
|
||||||
pending.sentTextLength = fullText.length;
|
pending.sentTextLength = fullText.length;
|
||||||
@@ -407,7 +437,9 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
|
|
||||||
private findPendingBySessionKey(sessionKey: string): PendingPrompt | undefined {
|
private findPendingBySessionKey(sessionKey: string): PendingPrompt | undefined {
|
||||||
for (const pending of this.pendingPrompts.values()) {
|
for (const pending of this.pendingPrompts.values()) {
|
||||||
if (pending.sessionKey === sessionKey) return pending;
|
if (pending.sessionKey === sessionKey) {
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,20 @@ import { resolveUserPath } from "../utils.js";
|
|||||||
export function resolveOpenClawAgentDir(): string {
|
export function resolveOpenClawAgentDir(): string {
|
||||||
const override =
|
const override =
|
||||||
process.env.OPENCLAW_AGENT_DIR?.trim() || process.env.PI_CODING_AGENT_DIR?.trim();
|
process.env.OPENCLAW_AGENT_DIR?.trim() || process.env.PI_CODING_AGENT_DIR?.trim();
|
||||||
if (override) return resolveUserPath(override);
|
if (override) {
|
||||||
|
return resolveUserPath(override);
|
||||||
|
}
|
||||||
const defaultAgentDir = path.join(resolveStateDir(), "agents", DEFAULT_AGENT_ID, "agent");
|
const defaultAgentDir = path.join(resolveStateDir(), "agents", DEFAULT_AGENT_ID, "agent");
|
||||||
return resolveUserPath(defaultAgentDir);
|
return resolveUserPath(defaultAgentDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureOpenClawAgentEnv(): string {
|
export function ensureOpenClawAgentEnv(): string {
|
||||||
const dir = resolveOpenClawAgentDir();
|
const dir = resolveOpenClawAgentDir();
|
||||||
if (!process.env.OPENCLAW_AGENT_DIR) process.env.OPENCLAW_AGENT_DIR = dir;
|
if (!process.env.OPENCLAW_AGENT_DIR) {
|
||||||
if (!process.env.PI_CODING_AGENT_DIR) process.env.PI_CODING_AGENT_DIR = dir;
|
process.env.OPENCLAW_AGENT_DIR = dir;
|
||||||
|
}
|
||||||
|
if (!process.env.PI_CODING_AGENT_DIR) {
|
||||||
|
process.env.PI_CODING_AGENT_DIR = dir;
|
||||||
|
}
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,18 +34,24 @@ let defaultAgentWarned = false;
|
|||||||
|
|
||||||
function listAgents(cfg: OpenClawConfig): AgentEntry[] {
|
function listAgents(cfg: OpenClawConfig): AgentEntry[] {
|
||||||
const list = cfg.agents?.list;
|
const list = cfg.agents?.list;
|
||||||
if (!Array.isArray(list)) return [];
|
if (!Array.isArray(list)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return list.filter((entry): entry is AgentEntry => Boolean(entry && typeof entry === "object"));
|
return list.filter((entry): entry is AgentEntry => Boolean(entry && typeof entry === "object"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listAgentIds(cfg: OpenClawConfig): string[] {
|
export function listAgentIds(cfg: OpenClawConfig): string[] {
|
||||||
const agents = listAgents(cfg);
|
const agents = listAgents(cfg);
|
||||||
if (agents.length === 0) return [DEFAULT_AGENT_ID];
|
if (agents.length === 0) {
|
||||||
|
return [DEFAULT_AGENT_ID];
|
||||||
|
}
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const ids: string[] = [];
|
const ids: string[] = [];
|
||||||
for (const entry of agents) {
|
for (const entry of agents) {
|
||||||
const id = normalizeAgentId(entry?.id);
|
const id = normalizeAgentId(entry?.id);
|
||||||
if (seen.has(id)) continue;
|
if (seen.has(id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
seen.add(id);
|
seen.add(id);
|
||||||
ids.push(id);
|
ids.push(id);
|
||||||
}
|
}
|
||||||
@@ -54,7 +60,9 @@ export function listAgentIds(cfg: OpenClawConfig): string[] {
|
|||||||
|
|
||||||
export function resolveDefaultAgentId(cfg: OpenClawConfig): string {
|
export function resolveDefaultAgentId(cfg: OpenClawConfig): string {
|
||||||
const agents = listAgents(cfg);
|
const agents = listAgents(cfg);
|
||||||
if (agents.length === 0) return DEFAULT_AGENT_ID;
|
if (agents.length === 0) {
|
||||||
|
return DEFAULT_AGENT_ID;
|
||||||
|
}
|
||||||
const defaults = agents.filter((agent) => agent?.default);
|
const defaults = agents.filter((agent) => agent?.default);
|
||||||
if (defaults.length > 1 && !defaultAgentWarned) {
|
if (defaults.length > 1 && !defaultAgentWarned) {
|
||||||
defaultAgentWarned = true;
|
defaultAgentWarned = true;
|
||||||
@@ -94,7 +102,9 @@ export function resolveAgentConfig(
|
|||||||
): ResolvedAgentConfig | undefined {
|
): ResolvedAgentConfig | undefined {
|
||||||
const id = normalizeAgentId(agentId);
|
const id = normalizeAgentId(agentId);
|
||||||
const entry = resolveAgentEntry(cfg, id);
|
const entry = resolveAgentEntry(cfg, id);
|
||||||
if (!entry) return undefined;
|
if (!entry) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
name: typeof entry.name === "string" ? entry.name : undefined,
|
name: typeof entry.name === "string" ? entry.name : undefined,
|
||||||
workspace: typeof entry.workspace === "string" ? entry.workspace : undefined,
|
workspace: typeof entry.workspace === "string" ? entry.workspace : undefined,
|
||||||
@@ -116,8 +126,12 @@ export function resolveAgentConfig(
|
|||||||
|
|
||||||
export function resolveAgentModelPrimary(cfg: OpenClawConfig, agentId: string): string | undefined {
|
export function resolveAgentModelPrimary(cfg: OpenClawConfig, agentId: string): string | undefined {
|
||||||
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
||||||
if (!raw) return undefined;
|
if (!raw) {
|
||||||
if (typeof raw === "string") return raw.trim() || undefined;
|
return undefined;
|
||||||
|
}
|
||||||
|
if (typeof raw === "string") {
|
||||||
|
return raw.trim() || undefined;
|
||||||
|
}
|
||||||
const primary = raw.primary?.trim();
|
const primary = raw.primary?.trim();
|
||||||
return primary || undefined;
|
return primary || undefined;
|
||||||
}
|
}
|
||||||
@@ -127,20 +141,28 @@ export function resolveAgentModelFallbacksOverride(
|
|||||||
agentId: string,
|
agentId: string,
|
||||||
): string[] | undefined {
|
): string[] | undefined {
|
||||||
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
||||||
if (!raw || typeof raw === "string") return undefined;
|
if (!raw || typeof raw === "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
// Important: treat an explicitly provided empty array as an override to disable global fallbacks.
|
// Important: treat an explicitly provided empty array as an override to disable global fallbacks.
|
||||||
if (!Object.hasOwn(raw, "fallbacks")) return undefined;
|
if (!Object.hasOwn(raw, "fallbacks")) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return Array.isArray(raw.fallbacks) ? raw.fallbacks : undefined;
|
return Array.isArray(raw.fallbacks) ? raw.fallbacks : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveAgentWorkspaceDir(cfg: OpenClawConfig, agentId: string) {
|
export function resolveAgentWorkspaceDir(cfg: OpenClawConfig, agentId: string) {
|
||||||
const id = normalizeAgentId(agentId);
|
const id = normalizeAgentId(agentId);
|
||||||
const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
|
const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
|
||||||
if (configured) return resolveUserPath(configured);
|
if (configured) {
|
||||||
|
return resolveUserPath(configured);
|
||||||
|
}
|
||||||
const defaultAgentId = resolveDefaultAgentId(cfg);
|
const defaultAgentId = resolveDefaultAgentId(cfg);
|
||||||
if (id === defaultAgentId) {
|
if (id === defaultAgentId) {
|
||||||
const fallback = cfg.agents?.defaults?.workspace?.trim();
|
const fallback = cfg.agents?.defaults?.workspace?.trim();
|
||||||
if (fallback) return resolveUserPath(fallback);
|
if (fallback) {
|
||||||
|
return resolveUserPath(fallback);
|
||||||
|
}
|
||||||
return DEFAULT_AGENT_WORKSPACE_DIR;
|
return DEFAULT_AGENT_WORKSPACE_DIR;
|
||||||
}
|
}
|
||||||
return path.join(os.homedir(), ".openclaw", `workspace-${id}`);
|
return path.join(os.homedir(), ".openclaw", `workspace-${id}`);
|
||||||
@@ -149,7 +171,9 @@ export function resolveAgentWorkspaceDir(cfg: OpenClawConfig, agentId: string) {
|
|||||||
export function resolveAgentDir(cfg: OpenClawConfig, agentId: string) {
|
export function resolveAgentDir(cfg: OpenClawConfig, agentId: string) {
|
||||||
const id = normalizeAgentId(agentId);
|
const id = normalizeAgentId(agentId);
|
||||||
const configured = resolveAgentConfig(cfg, id)?.agentDir?.trim();
|
const configured = resolveAgentConfig(cfg, id)?.agentDir?.trim();
|
||||||
if (configured) return resolveUserPath(configured);
|
if (configured) {
|
||||||
|
return resolveUserPath(configured);
|
||||||
|
}
|
||||||
const root = resolveStateDir(process.env, os.homedir);
|
const root = resolveStateDir(process.env, os.homedir);
|
||||||
return path.join(root, "agents", id, "agent");
|
return path.join(root, "agents", id, "agent");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ function resolvePayloadLogConfig(env: NodeJS.ProcessEnv): PayloadLogConfig {
|
|||||||
|
|
||||||
function getWriter(filePath: string): PayloadLogWriter {
|
function getWriter(filePath: string): PayloadLogWriter {
|
||||||
const existing = writers.get(filePath);
|
const existing = writers.get(filePath);
|
||||||
if (existing) return existing;
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
const dir = path.dirname(filePath);
|
const dir = path.dirname(filePath);
|
||||||
const ready = fs.mkdir(dir, { recursive: true }).catch(() => undefined);
|
const ready = fs.mkdir(dir, { recursive: true }).catch(() => undefined);
|
||||||
@@ -75,8 +77,12 @@ function getWriter(filePath: string): PayloadLogWriter {
|
|||||||
function safeJsonStringify(value: unknown): string | null {
|
function safeJsonStringify(value: unknown): string | null {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(value, (_key, val) => {
|
return JSON.stringify(value, (_key, val) => {
|
||||||
if (typeof val === "bigint") return val.toString();
|
if (typeof val === "bigint") {
|
||||||
if (typeof val === "function") return "[Function]";
|
return val.toString();
|
||||||
|
}
|
||||||
|
if (typeof val === "function") {
|
||||||
|
return "[Function]";
|
||||||
|
}
|
||||||
if (val instanceof Error) {
|
if (val instanceof Error) {
|
||||||
return { name: val.name, message: val.message, stack: val.stack };
|
return { name: val.name, message: val.message, stack: val.stack };
|
||||||
}
|
}
|
||||||
@@ -91,8 +97,12 @@ function safeJsonStringify(value: unknown): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatError(error: unknown): string | undefined {
|
function formatError(error: unknown): string | undefined {
|
||||||
if (error instanceof Error) return error.message;
|
if (error instanceof Error) {
|
||||||
if (typeof error === "string") return error;
|
return error.message;
|
||||||
|
}
|
||||||
|
if (typeof error === "string") {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") {
|
if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") {
|
||||||
return String(error);
|
return String(error);
|
||||||
}
|
}
|
||||||
@@ -104,7 +114,9 @@ function formatError(error: unknown): string | undefined {
|
|||||||
|
|
||||||
function digest(value: unknown): string | undefined {
|
function digest(value: unknown): string | undefined {
|
||||||
const serialized = safeJsonStringify(value);
|
const serialized = safeJsonStringify(value);
|
||||||
if (!serialized) return undefined;
|
if (!serialized) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return crypto.createHash("sha256").update(serialized).digest("hex");
|
return crypto.createHash("sha256").update(serialized).digest("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +152,9 @@ export function createAnthropicPayloadLogger(params: {
|
|||||||
}): AnthropicPayloadLogger | null {
|
}): AnthropicPayloadLogger | null {
|
||||||
const env = params.env ?? process.env;
|
const env = params.env ?? process.env;
|
||||||
const cfg = resolvePayloadLogConfig(env);
|
const cfg = resolvePayloadLogConfig(env);
|
||||||
if (!cfg.enabled) return null;
|
if (!cfg.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const writer = getWriter(cfg.filePath);
|
const writer = getWriter(cfg.filePath);
|
||||||
const base: Omit<PayloadLogEvent, "ts" | "stage"> = {
|
const base: Omit<PayloadLogEvent, "ts" | "stage"> = {
|
||||||
@@ -155,7 +169,9 @@ export function createAnthropicPayloadLogger(params: {
|
|||||||
|
|
||||||
const record = (event: PayloadLogEvent) => {
|
const record = (event: PayloadLogEvent) => {
|
||||||
const line = safeJsonStringify(event);
|
const line = safeJsonStringify(event);
|
||||||
if (!line) return;
|
if (!line) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
writer.write(`${line}\n`);
|
writer.write(`${line}\n`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,12 @@ function listSetupTokenProfiles(store: {
|
|||||||
}): string[] {
|
}): string[] {
|
||||||
return Object.entries(store.profiles)
|
return Object.entries(store.profiles)
|
||||||
.filter(([, cred]) => {
|
.filter(([, cred]) => {
|
||||||
if (cred.type !== "token") return false;
|
if (cred.type !== "token") {
|
||||||
if (normalizeProviderId(cred.provider) !== "anthropic") return false;
|
return false;
|
||||||
|
}
|
||||||
|
if (normalizeProviderId(cred.provider) !== "anthropic") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return isSetupToken(cred.token);
|
return isSetupToken(cred.token);
|
||||||
})
|
})
|
||||||
.map(([id]) => id);
|
.map(([id]) => id);
|
||||||
@@ -56,7 +60,9 @@ function listSetupTokenProfiles(store: {
|
|||||||
function pickSetupTokenProfile(candidates: string[]): string {
|
function pickSetupTokenProfile(candidates: string[]): string {
|
||||||
const preferred = ["anthropic:setup-token-test", "anthropic:setup-token", "anthropic:default"];
|
const preferred = ["anthropic:setup-token-test", "anthropic:setup-token", "anthropic:default"];
|
||||||
for (const id of preferred) {
|
for (const id of preferred) {
|
||||||
if (candidates.includes(id)) return id;
|
if (candidates.includes(id)) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return candidates[0] ?? "";
|
return candidates[0] ?? "";
|
||||||
}
|
}
|
||||||
@@ -124,7 +130,9 @@ function pickModel(models: Array<Model<Api>>, raw?: string): Model<Api> | null {
|
|||||||
const normalized = raw?.trim() ?? "";
|
const normalized = raw?.trim() ?? "";
|
||||||
if (normalized) {
|
if (normalized) {
|
||||||
const parsed = parseModelRef(normalized, "anthropic");
|
const parsed = parseModelRef(normalized, "anthropic");
|
||||||
if (!parsed) return null;
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
models.find(
|
models.find(
|
||||||
(model) =>
|
(model) =>
|
||||||
@@ -141,7 +149,9 @@ function pickModel(models: Array<Model<Api>>, raw?: string): Model<Api> | null {
|
|||||||
];
|
];
|
||||||
for (const id of preferred) {
|
for (const id of preferred) {
|
||||||
const match = models.find((model) => model.id === id);
|
const match = models.find((model) => model.id === id);
|
||||||
if (match) return match;
|
if (match) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return models[0] ?? null;
|
return models[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,21 +104,33 @@ function seekSequence(
|
|||||||
start: number,
|
start: number,
|
||||||
eof: boolean,
|
eof: boolean,
|
||||||
): number | null {
|
): number | null {
|
||||||
if (pattern.length === 0) return start;
|
if (pattern.length === 0) {
|
||||||
if (pattern.length > lines.length) return null;
|
return start;
|
||||||
|
}
|
||||||
|
if (pattern.length > lines.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const maxStart = lines.length - pattern.length;
|
const maxStart = lines.length - pattern.length;
|
||||||
const searchStart = eof && lines.length >= pattern.length ? maxStart : start;
|
const searchStart = eof && lines.length >= pattern.length ? maxStart : start;
|
||||||
if (searchStart > maxStart) return null;
|
if (searchStart > maxStart) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = searchStart; i <= maxStart; i += 1) {
|
for (let i = searchStart; i <= maxStart; i += 1) {
|
||||||
if (linesMatch(lines, pattern, i, (value) => value)) return i;
|
if (linesMatch(lines, pattern, i, (value) => value)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (let i = searchStart; i <= maxStart; i += 1) {
|
for (let i = searchStart; i <= maxStart; i += 1) {
|
||||||
if (linesMatch(lines, pattern, i, (value) => value.trimEnd())) return i;
|
if (linesMatch(lines, pattern, i, (value) => value.trimEnd())) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (let i = searchStart; i <= maxStart; i += 1) {
|
for (let i = searchStart; i <= maxStart; i += 1) {
|
||||||
if (linesMatch(lines, pattern, i, (value) => value.trim())) return i;
|
if (linesMatch(lines, pattern, i, (value) => value.trim())) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (let i = searchStart; i <= maxStart; i += 1) {
|
for (let i = searchStart; i <= maxStart; i += 1) {
|
||||||
if (linesMatch(lines, pattern, i, (value) => normalizePunctuation(value.trim()))) {
|
if (linesMatch(lines, pattern, i, (value) => normalizePunctuation(value.trim()))) {
|
||||||
|
|||||||
@@ -183,22 +183,32 @@ function recordSummary(
|
|||||||
bucket: keyof ApplyPatchSummary,
|
bucket: keyof ApplyPatchSummary,
|
||||||
value: string,
|
value: string,
|
||||||
) {
|
) {
|
||||||
if (seen[bucket].has(value)) return;
|
if (seen[bucket].has(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
seen[bucket].add(value);
|
seen[bucket].add(value);
|
||||||
summary[bucket].push(value);
|
summary[bucket].push(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSummary(summary: ApplyPatchSummary): string {
|
function formatSummary(summary: ApplyPatchSummary): string {
|
||||||
const lines = ["Success. Updated the following files:"];
|
const lines = ["Success. Updated the following files:"];
|
||||||
for (const file of summary.added) lines.push(`A ${file}`);
|
for (const file of summary.added) {
|
||||||
for (const file of summary.modified) lines.push(`M ${file}`);
|
lines.push(`A ${file}`);
|
||||||
for (const file of summary.deleted) lines.push(`D ${file}`);
|
}
|
||||||
|
for (const file of summary.modified) {
|
||||||
|
lines.push(`M ${file}`);
|
||||||
|
}
|
||||||
|
for (const file of summary.deleted) {
|
||||||
|
lines.push(`D ${file}`);
|
||||||
|
}
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureDir(filePath: string) {
|
async function ensureDir(filePath: string) {
|
||||||
const parent = path.dirname(filePath);
|
const parent = path.dirname(filePath);
|
||||||
if (!parent || parent === ".") return;
|
if (!parent || parent === ".") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await fs.mkdir(parent, { recursive: true });
|
await fs.mkdir(parent, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,21 +241,31 @@ function normalizeUnicodeSpaces(value: string): string {
|
|||||||
|
|
||||||
function expandPath(filePath: string): string {
|
function expandPath(filePath: string): string {
|
||||||
const normalized = normalizeUnicodeSpaces(filePath);
|
const normalized = normalizeUnicodeSpaces(filePath);
|
||||||
if (normalized === "~") return os.homedir();
|
if (normalized === "~") {
|
||||||
if (normalized.startsWith("~/")) return os.homedir() + normalized.slice(1);
|
return os.homedir();
|
||||||
|
}
|
||||||
|
if (normalized.startsWith("~/")) {
|
||||||
|
return os.homedir() + normalized.slice(1);
|
||||||
|
}
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePathFromCwd(filePath: string, cwd: string): string {
|
function resolvePathFromCwd(filePath: string, cwd: string): string {
|
||||||
const expanded = expandPath(filePath);
|
const expanded = expandPath(filePath);
|
||||||
if (path.isAbsolute(expanded)) return path.normalize(expanded);
|
if (path.isAbsolute(expanded)) {
|
||||||
|
return path.normalize(expanded);
|
||||||
|
}
|
||||||
return path.resolve(cwd, expanded);
|
return path.resolve(cwd, expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDisplayPath(resolved: string, cwd: string): string {
|
function toDisplayPath(resolved: string, cwd: string): string {
|
||||||
const relative = path.relative(cwd, resolved);
|
const relative = path.relative(cwd, resolved);
|
||||||
if (!relative || relative === "") return path.basename(resolved);
|
if (!relative || relative === "") {
|
||||||
if (relative.startsWith("..") || path.isAbsolute(relative)) return resolved;
|
return path.basename(resolved);
|
||||||
|
}
|
||||||
|
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
return relative;
|
return relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +295,9 @@ function parsePatchText(input: string): { hunks: Hunk[]; patch: string } {
|
|||||||
|
|
||||||
function checkPatchBoundariesLenient(lines: string[]): string[] {
|
function checkPatchBoundariesLenient(lines: string[]): string[] {
|
||||||
const strictError = checkPatchBoundariesStrict(lines);
|
const strictError = checkPatchBoundariesStrict(lines);
|
||||||
if (!strictError) return lines;
|
if (!strictError) {
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
if (lines.length < 4) {
|
if (lines.length < 4) {
|
||||||
throw new Error(strictError);
|
throw new Error(strictError);
|
||||||
@@ -285,7 +307,9 @@ function checkPatchBoundariesLenient(lines: string[]): string[] {
|
|||||||
if ((first === "<<EOF" || first === "<<'EOF'" || first === '<<"EOF"') && last.endsWith("EOF")) {
|
if ((first === "<<EOF" || first === "<<'EOF'" || first === '<<"EOF"') && last.endsWith("EOF")) {
|
||||||
const inner = lines.slice(1, lines.length - 1);
|
const inner = lines.slice(1, lines.length - 1);
|
||||||
const innerError = checkPatchBoundariesStrict(inner);
|
const innerError = checkPatchBoundariesStrict(inner);
|
||||||
if (!innerError) return inner;
|
if (!innerError) {
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
throw new Error(innerError);
|
throw new Error(innerError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,12 +44,20 @@ export function resolveAuthProfileSource(_profileId: string): AuthProfileSource
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function formatRemainingShort(remainingMs?: number): string {
|
export function formatRemainingShort(remainingMs?: number): string {
|
||||||
if (remainingMs === undefined || Number.isNaN(remainingMs)) return "unknown";
|
if (remainingMs === undefined || Number.isNaN(remainingMs)) {
|
||||||
if (remainingMs <= 0) return "0m";
|
return "unknown";
|
||||||
|
}
|
||||||
|
if (remainingMs <= 0) {
|
||||||
|
return "0m";
|
||||||
|
}
|
||||||
const minutes = Math.max(1, Math.round(remainingMs / 60_000));
|
const minutes = Math.max(1, Math.round(remainingMs / 60_000));
|
||||||
if (minutes < 60) return `${minutes}m`;
|
if (minutes < 60) {
|
||||||
|
return `${minutes}m`;
|
||||||
|
}
|
||||||
const hours = Math.round(minutes / 60);
|
const hours = Math.round(minutes / 60);
|
||||||
if (hours < 48) return `${hours}h`;
|
if (hours < 48) {
|
||||||
|
return `${hours}h`;
|
||||||
|
}
|
||||||
const days = Math.round(hours / 24);
|
const days = Math.round(hours / 24);
|
||||||
return `${days}d`;
|
return `${days}d`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,26 @@ describe("auth-profiles (chutes)", () => {
|
|||||||
await fs.rm(tempDir, { recursive: true, force: true });
|
await fs.rm(tempDir, { recursive: true, force: true });
|
||||||
tempDir = null;
|
tempDir = null;
|
||||||
}
|
}
|
||||||
if (previousStateDir === undefined) delete process.env.OPENCLAW_STATE_DIR;
|
if (previousStateDir === undefined) {
|
||||||
else process.env.OPENCLAW_STATE_DIR = previousStateDir;
|
delete process.env.OPENCLAW_STATE_DIR;
|
||||||
if (previousAgentDir === undefined) delete process.env.OPENCLAW_AGENT_DIR;
|
} else {
|
||||||
else process.env.OPENCLAW_AGENT_DIR = previousAgentDir;
|
process.env.OPENCLAW_STATE_DIR = previousStateDir;
|
||||||
if (previousPiAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
}
|
||||||
else process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
if (previousAgentDir === undefined) {
|
||||||
if (previousChutesClientId === undefined) delete process.env.CHUTES_CLIENT_ID;
|
delete process.env.OPENCLAW_AGENT_DIR;
|
||||||
else process.env.CHUTES_CLIENT_ID = previousChutesClientId;
|
} else {
|
||||||
|
process.env.OPENCLAW_AGENT_DIR = previousAgentDir;
|
||||||
|
}
|
||||||
|
if (previousPiAgentDir === undefined) {
|
||||||
|
delete process.env.PI_CODING_AGENT_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
||||||
|
}
|
||||||
|
if (previousChutesClientId === undefined) {
|
||||||
|
delete process.env.CHUTES_CLIENT_ID;
|
||||||
|
} else {
|
||||||
|
process.env.CHUTES_CLIENT_ID = previousChutesClientId;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("refreshes expired Chutes OAuth credentials", async () => {
|
it("refreshes expired Chutes OAuth credentials", async () => {
|
||||||
@@ -59,7 +71,9 @@ describe("auth-profiles (chutes)", () => {
|
|||||||
|
|
||||||
const fetchSpy = vi.fn(async (input: string | URL) => {
|
const fetchSpy = vi.fn(async (input: string | URL) => {
|
||||||
const url = typeof input === "string" ? input : input.toString();
|
const url = typeof input === "string" ? input : input.toString();
|
||||||
if (url !== CHUTES_TOKEN_ENDPOINT) return new Response("not found", { status: 404 });
|
if (url !== CHUTES_TOKEN_ENDPOINT) {
|
||||||
|
return new Response("not found", { status: 404 });
|
||||||
|
}
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
access_token: "at_new",
|
access_token: "at_new",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export function resolveAuthProfileDisplayLabel(params: {
|
|||||||
const profile = store.profiles[profileId];
|
const profile = store.profiles[profileId];
|
||||||
const configEmail = cfg?.auth?.profiles?.[profileId]?.email?.trim();
|
const configEmail = cfg?.auth?.profiles?.[profileId]?.email?.trim();
|
||||||
const email = configEmail || (profile && "email" in profile ? profile.email?.trim() : undefined);
|
const email = configEmail || (profile && "email" in profile ? profile.email?.trim() : undefined);
|
||||||
if (email) return `${profileId} (${email})`;
|
if (email) {
|
||||||
|
return `${profileId} (${email})`;
|
||||||
|
}
|
||||||
return profileId;
|
return profileId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ export function formatAuthDoctorHint(params: {
|
|||||||
profileId?: string;
|
profileId?: string;
|
||||||
}): string {
|
}): string {
|
||||||
const providerKey = normalizeProviderId(params.provider);
|
const providerKey = normalizeProviderId(params.provider);
|
||||||
if (providerKey !== "anthropic") return "";
|
if (providerKey !== "anthropic") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
const legacyProfileId = params.profileId ?? "anthropic:default";
|
const legacyProfileId = params.profileId ?? "anthropic:default";
|
||||||
const suggested = suggestOAuthProfileIdForLegacyDefault({
|
const suggested = suggestOAuthProfileIdForLegacyDefault({
|
||||||
@@ -21,7 +23,9 @@ export function formatAuthDoctorHint(params: {
|
|||||||
provider: providerKey,
|
provider: providerKey,
|
||||||
legacyProfileId,
|
legacyProfileId,
|
||||||
});
|
});
|
||||||
if (!suggested || suggested === legacyProfileId) return "";
|
if (!suggested || suggested === legacyProfileId) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
const storeOauthProfiles = listProfilesForProvider(params.store, providerKey)
|
const storeOauthProfiles = listProfilesForProvider(params.store, providerKey)
|
||||||
.filter((id) => params.store.profiles[id]?.type === "oauth")
|
.filter((id) => params.store.profiles[id]?.type === "oauth")
|
||||||
|
|||||||
@@ -8,8 +8,12 @@ import {
|
|||||||
import type { AuthProfileCredential, AuthProfileStore, OAuthCredential } from "./types.js";
|
import type { AuthProfileCredential, AuthProfileStore, OAuthCredential } from "./types.js";
|
||||||
|
|
||||||
function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCredential): boolean {
|
function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCredential): boolean {
|
||||||
if (!a) return false;
|
if (!a) {
|
||||||
if (a.type !== "oauth") return false;
|
return false;
|
||||||
|
}
|
||||||
|
if (a.type !== "oauth") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
a.provider === b.provider &&
|
a.provider === b.provider &&
|
||||||
a.access === b.access &&
|
a.access === b.access &&
|
||||||
@@ -23,12 +27,18 @@ function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCr
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: number): boolean {
|
function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: number): boolean {
|
||||||
if (!cred) return false;
|
if (!cred) {
|
||||||
if (cred.type !== "oauth" && cred.type !== "token") return false;
|
return false;
|
||||||
|
}
|
||||||
|
if (cred.type !== "oauth" && cred.type !== "token") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (cred.provider !== "qwen-portal") {
|
if (cred.provider !== "qwen-portal") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (typeof cred.expires !== "number") return true;
|
if (typeof cred.expires !== "number") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return cred.expires > now + EXTERNAL_CLI_NEAR_EXPIRY_MS;
|
return cred.expires > now + EXTERNAL_CLI_NEAR_EXPIRY_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,12 +31,21 @@ describe("resolveApiKeyForProfile fallback to main agent", () => {
|
|||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
|
|
||||||
// Restore original environment
|
// Restore original environment
|
||||||
if (previousStateDir === undefined) delete process.env.OPENCLAW_STATE_DIR;
|
if (previousStateDir === undefined) {
|
||||||
else process.env.OPENCLAW_STATE_DIR = previousStateDir;
|
delete process.env.OPENCLAW_STATE_DIR;
|
||||||
if (previousAgentDir === undefined) delete process.env.OPENCLAW_AGENT_DIR;
|
} else {
|
||||||
else process.env.OPENCLAW_AGENT_DIR = previousAgentDir;
|
process.env.OPENCLAW_STATE_DIR = previousStateDir;
|
||||||
if (previousPiAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
}
|
||||||
else process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
if (previousAgentDir === undefined) {
|
||||||
|
delete process.env.OPENCLAW_AGENT_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.OPENCLAW_AGENT_DIR = previousAgentDir;
|
||||||
|
}
|
||||||
|
if (previousPiAgentDir === undefined) {
|
||||||
|
delete process.env.PI_CODING_AGENT_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
||||||
|
}
|
||||||
|
|
||||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ async function refreshOAuthTokenWithLock(params: {
|
|||||||
|
|
||||||
const store = ensureAuthProfileStore(params.agentDir);
|
const store = ensureAuthProfileStore(params.agentDir);
|
||||||
const cred = store.profiles[params.profileId];
|
const cred = store.profiles[params.profileId];
|
||||||
if (!cred || cred.type !== "oauth") return null;
|
if (!cred || cred.type !== "oauth") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (Date.now() < cred.expires) {
|
if (Date.now() < cred.expires) {
|
||||||
return {
|
return {
|
||||||
@@ -63,7 +65,9 @@ async function refreshOAuthTokenWithLock(params: {
|
|||||||
return { apiKey: newCredentials.access, newCredentials };
|
return { apiKey: newCredentials.access, newCredentials };
|
||||||
})()
|
})()
|
||||||
: await getOAuthApiKey(cred.provider, oauthCreds);
|
: await getOAuthApiKey(cred.provider, oauthCreds);
|
||||||
if (!result) return null;
|
if (!result) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
store.profiles[params.profileId] = {
|
store.profiles[params.profileId] = {
|
||||||
...cred,
|
...cred,
|
||||||
...result.newCredentials,
|
...result.newCredentials,
|
||||||
@@ -91,10 +95,16 @@ async function tryResolveOAuthProfile(params: {
|
|||||||
}): Promise<{ apiKey: string; provider: string; email?: string } | null> {
|
}): Promise<{ apiKey: string; provider: string; email?: string } | null> {
|
||||||
const { cfg, store, profileId } = params;
|
const { cfg, store, profileId } = params;
|
||||||
const cred = store.profiles[profileId];
|
const cred = store.profiles[profileId];
|
||||||
if (!cred || cred.type !== "oauth") return null;
|
if (!cred || cred.type !== "oauth") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const profileConfig = cfg?.auth?.profiles?.[profileId];
|
const profileConfig = cfg?.auth?.profiles?.[profileId];
|
||||||
if (profileConfig && profileConfig.provider !== cred.provider) return null;
|
if (profileConfig && profileConfig.provider !== cred.provider) {
|
||||||
if (profileConfig && profileConfig.mode !== cred.type) return null;
|
return null;
|
||||||
|
}
|
||||||
|
if (profileConfig && profileConfig.mode !== cred.type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (Date.now() < cred.expires) {
|
if (Date.now() < cred.expires) {
|
||||||
return {
|
return {
|
||||||
@@ -108,7 +118,9 @@ async function tryResolveOAuthProfile(params: {
|
|||||||
profileId,
|
profileId,
|
||||||
agentDir: params.agentDir,
|
agentDir: params.agentDir,
|
||||||
});
|
});
|
||||||
if (!refreshed) return null;
|
if (!refreshed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
apiKey: refreshed.apiKey,
|
apiKey: refreshed.apiKey,
|
||||||
provider: cred.provider,
|
provider: cred.provider,
|
||||||
@@ -124,12 +136,18 @@ export async function resolveApiKeyForProfile(params: {
|
|||||||
}): Promise<{ apiKey: string; provider: string; email?: string } | null> {
|
}): Promise<{ apiKey: string; provider: string; email?: string } | null> {
|
||||||
const { cfg, store, profileId } = params;
|
const { cfg, store, profileId } = params;
|
||||||
const cred = store.profiles[profileId];
|
const cred = store.profiles[profileId];
|
||||||
if (!cred) return null;
|
if (!cred) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const profileConfig = cfg?.auth?.profiles?.[profileId];
|
const profileConfig = cfg?.auth?.profiles?.[profileId];
|
||||||
if (profileConfig && profileConfig.provider !== cred.provider) return null;
|
if (profileConfig && profileConfig.provider !== cred.provider) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (profileConfig && profileConfig.mode !== cred.type) {
|
if (profileConfig && profileConfig.mode !== cred.type) {
|
||||||
// Compatibility: treat "oauth" config as compatible with stored token profiles.
|
// Compatibility: treat "oauth" config as compatible with stored token profiles.
|
||||||
if (!(profileConfig.mode === "oauth" && cred.type === "token")) return null;
|
if (!(profileConfig.mode === "oauth" && cred.type === "token")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cred.type === "api_key") {
|
if (cred.type === "api_key") {
|
||||||
@@ -137,7 +155,9 @@ export async function resolveApiKeyForProfile(params: {
|
|||||||
}
|
}
|
||||||
if (cred.type === "token") {
|
if (cred.type === "token") {
|
||||||
const token = cred.token?.trim();
|
const token = cred.token?.trim();
|
||||||
if (!token) return null;
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
typeof cred.expires === "number" &&
|
typeof cred.expires === "number" &&
|
||||||
Number.isFinite(cred.expires) &&
|
Number.isFinite(cred.expires) &&
|
||||||
@@ -161,7 +181,9 @@ export async function resolveApiKeyForProfile(params: {
|
|||||||
profileId,
|
profileId,
|
||||||
agentDir: params.agentDir,
|
agentDir: params.agentDir,
|
||||||
});
|
});
|
||||||
if (!result) return null;
|
if (!result) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
apiKey: result.apiKey,
|
apiKey: result.apiKey,
|
||||||
provider: cred.provider,
|
provider: cred.provider,
|
||||||
@@ -191,7 +213,9 @@ export async function resolveApiKeyForProfile(params: {
|
|||||||
profileId: fallbackProfileId,
|
profileId: fallbackProfileId,
|
||||||
agentDir: params.agentDir,
|
agentDir: params.agentDir,
|
||||||
});
|
});
|
||||||
if (fallbackResolved) return fallbackResolved;
|
if (fallbackResolved) {
|
||||||
|
return fallbackResolved;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// keep original error
|
// keep original error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ function resolveProfileUnusableUntil(stats: {
|
|||||||
const values = [stats.cooldownUntil, stats.disabledUntil]
|
const values = [stats.cooldownUntil, stats.disabledUntil]
|
||||||
.filter((value): value is number => typeof value === "number")
|
.filter((value): value is number => typeof value === "number")
|
||||||
.filter((value) => Number.isFinite(value) && value > 0);
|
.filter((value) => Number.isFinite(value) && value > 0);
|
||||||
if (values.length === 0) return null;
|
if (values.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return Math.max(...values);
|
return Math.max(...values);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,17 +28,25 @@ export function resolveAuthProfileOrder(params: {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const storedOrder = (() => {
|
const storedOrder = (() => {
|
||||||
const order = store.order;
|
const order = store.order;
|
||||||
if (!order) return undefined;
|
if (!order) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
for (const [key, value] of Object.entries(order)) {
|
for (const [key, value] of Object.entries(order)) {
|
||||||
if (normalizeProviderId(key) === providerKey) return value;
|
if (normalizeProviderId(key) === providerKey) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
})();
|
})();
|
||||||
const configuredOrder = (() => {
|
const configuredOrder = (() => {
|
||||||
const order = cfg?.auth?.order;
|
const order = cfg?.auth?.order;
|
||||||
if (!order) return undefined;
|
if (!order) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
for (const [key, value] of Object.entries(order)) {
|
for (const [key, value] of Object.entries(order)) {
|
||||||
if (normalizeProviderId(key) === providerKey) return value;
|
if (normalizeProviderId(key) === providerKey) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
})();
|
})();
|
||||||
@@ -49,12 +59,18 @@ export function resolveAuthProfileOrder(params: {
|
|||||||
const baseOrder =
|
const baseOrder =
|
||||||
explicitOrder ??
|
explicitOrder ??
|
||||||
(explicitProfiles.length > 0 ? explicitProfiles : listProfilesForProvider(store, providerKey));
|
(explicitProfiles.length > 0 ? explicitProfiles : listProfilesForProvider(store, providerKey));
|
||||||
if (baseOrder.length === 0) return [];
|
if (baseOrder.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const filtered = baseOrder.filter((profileId) => {
|
const filtered = baseOrder.filter((profileId) => {
|
||||||
const cred = store.profiles[profileId];
|
const cred = store.profiles[profileId];
|
||||||
if (!cred) return false;
|
if (!cred) {
|
||||||
if (normalizeProviderId(cred.provider) !== providerKey) return false;
|
return false;
|
||||||
|
}
|
||||||
|
if (normalizeProviderId(cred.provider) !== providerKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const profileConfig = cfg?.auth?.profiles?.[profileId];
|
const profileConfig = cfg?.auth?.profiles?.[profileId];
|
||||||
if (profileConfig) {
|
if (profileConfig) {
|
||||||
if (normalizeProviderId(profileConfig.provider) !== providerKey) {
|
if (normalizeProviderId(profileConfig.provider) !== providerKey) {
|
||||||
@@ -62,12 +78,18 @@ export function resolveAuthProfileOrder(params: {
|
|||||||
}
|
}
|
||||||
if (profileConfig.mode !== cred.type) {
|
if (profileConfig.mode !== cred.type) {
|
||||||
const oauthCompatible = profileConfig.mode === "oauth" && cred.type === "token";
|
const oauthCompatible = profileConfig.mode === "oauth" && cred.type === "token";
|
||||||
if (!oauthCompatible) return false;
|
if (!oauthCompatible) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cred.type === "api_key") return Boolean(cred.key?.trim());
|
if (cred.type === "api_key") {
|
||||||
|
return Boolean(cred.key?.trim());
|
||||||
|
}
|
||||||
if (cred.type === "token") {
|
if (cred.type === "token") {
|
||||||
if (!cred.token?.trim()) return false;
|
if (!cred.token?.trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
typeof cred.expires === "number" &&
|
typeof cred.expires === "number" &&
|
||||||
Number.isFinite(cred.expires) &&
|
Number.isFinite(cred.expires) &&
|
||||||
@@ -85,7 +107,9 @@ export function resolveAuthProfileOrder(params: {
|
|||||||
});
|
});
|
||||||
const deduped: string[] = [];
|
const deduped: string[] = [];
|
||||||
for (const entry of filtered) {
|
for (const entry of filtered) {
|
||||||
if (!deduped.includes(entry)) deduped.push(entry);
|
if (!deduped.includes(entry)) {
|
||||||
|
deduped.push(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user specified explicit order (store override or config), respect it
|
// If user specified explicit order (store override or config), respect it
|
||||||
@@ -165,7 +189,9 @@ function orderProfilesByMode(order: string[], store: AuthProfileStore): string[]
|
|||||||
const sorted = scored
|
const sorted = scored
|
||||||
.toSorted((a, b) => {
|
.toSorted((a, b) => {
|
||||||
// First by type (oauth > token > api_key)
|
// First by type (oauth > token > api_key)
|
||||||
if (a.typeScore !== b.typeScore) return a.typeScore - b.typeScore;
|
if (a.typeScore !== b.typeScore) {
|
||||||
|
return a.typeScore - b.typeScore;
|
||||||
|
}
|
||||||
// Then by lastUsed (oldest first)
|
// Then by lastUsed (oldest first)
|
||||||
return a.lastUsed - b.lastUsed;
|
return a.lastUsed - b.lastUsed;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export function resolveAuthStorePathForDisplay(agentDir?: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ensureAuthStoreFile(pathname: string) {
|
export function ensureAuthStoreFile(pathname: string) {
|
||||||
if (fs.existsSync(pathname)) return;
|
if (fs.existsSync(pathname)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const payload: AuthProfileStore = {
|
const payload: AuthProfileStore = {
|
||||||
version: AUTH_STORE_VERSION,
|
version: AUTH_STORE_VERSION,
|
||||||
profiles: {},
|
profiles: {},
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ export async function setAuthProfileOrder(params: {
|
|||||||
|
|
||||||
const deduped: string[] = [];
|
const deduped: string[] = [];
|
||||||
for (const entry of sanitized) {
|
for (const entry of sanitized) {
|
||||||
if (!deduped.includes(entry)) deduped.push(entry);
|
if (!deduped.includes(entry)) {
|
||||||
|
deduped.push(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await updateAuthProfileStoreWithLock({
|
return await updateAuthProfileStoreWithLock({
|
||||||
@@ -27,7 +29,9 @@ export async function setAuthProfileOrder(params: {
|
|||||||
updater: (store) => {
|
updater: (store) => {
|
||||||
store.order = store.order ?? {};
|
store.order = store.order ?? {};
|
||||||
if (deduped.length === 0) {
|
if (deduped.length === 0) {
|
||||||
if (!store.order[providerKey]) return false;
|
if (!store.order[providerKey]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
delete store.order[providerKey];
|
delete store.order[providerKey];
|
||||||
if (Object.keys(store.order).length === 0) {
|
if (Object.keys(store.order).length === 0) {
|
||||||
store.order = undefined;
|
store.order = undefined;
|
||||||
@@ -68,7 +72,9 @@ export async function markAuthProfileGood(params: {
|
|||||||
agentDir,
|
agentDir,
|
||||||
updater: (freshStore) => {
|
updater: (freshStore) => {
|
||||||
const profile = freshStore.profiles[profileId];
|
const profile = freshStore.profiles[profileId];
|
||||||
if (!profile || profile.provider !== provider) return false;
|
if (!profile || profile.provider !== provider) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
freshStore.lastGood = { ...freshStore.lastGood, [provider]: profileId };
|
freshStore.lastGood = { ...freshStore.lastGood, [provider]: profileId };
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -78,7 +84,9 @@ export async function markAuthProfileGood(params: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const profile = store.profiles[profileId];
|
const profile = store.profiles[profileId];
|
||||||
if (!profile || profile.provider !== provider) return;
|
if (!profile || profile.provider !== provider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
store.lastGood = { ...store.lastGood, [provider]: profileId };
|
store.lastGood = { ...store.lastGood, [provider]: profileId };
|
||||||
saveAuthProfileStore(store, agentDir);
|
saveAuthProfileStore(store, agentDir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ import type { AuthProfileIdRepairResult, AuthProfileStore } from "./types.js";
|
|||||||
|
|
||||||
function getProfileSuffix(profileId: string): string {
|
function getProfileSuffix(profileId: string): string {
|
||||||
const idx = profileId.indexOf(":");
|
const idx = profileId.indexOf(":");
|
||||||
if (idx < 0) return "";
|
if (idx < 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
return profileId.slice(idx + 1);
|
return profileId.slice(idx + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEmailLike(value: string): boolean {
|
function isEmailLike(value: string): boolean {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (!trimmed) return false;
|
if (!trimmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return trimmed.includes("@") && trimmed.includes(".");
|
return trimmed.includes("@") && trimmed.includes(".");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +28,9 @@ export function suggestOAuthProfileIdForLegacyDefault(params: {
|
|||||||
}): string | null {
|
}): string | null {
|
||||||
const providerKey = normalizeProviderId(params.provider);
|
const providerKey = normalizeProviderId(params.provider);
|
||||||
const legacySuffix = getProfileSuffix(params.legacyProfileId);
|
const legacySuffix = getProfileSuffix(params.legacyProfileId);
|
||||||
if (legacySuffix !== "default") return null;
|
if (legacySuffix !== "default") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const legacyCfg = params.cfg?.auth?.profiles?.[params.legacyProfileId];
|
const legacyCfg = params.cfg?.auth?.profiles?.[params.legacyProfileId];
|
||||||
if (
|
if (
|
||||||
@@ -38,27 +44,39 @@ export function suggestOAuthProfileIdForLegacyDefault(params: {
|
|||||||
const oauthProfiles = listProfilesForProvider(params.store, providerKey).filter(
|
const oauthProfiles = listProfilesForProvider(params.store, providerKey).filter(
|
||||||
(id) => params.store.profiles[id]?.type === "oauth",
|
(id) => params.store.profiles[id]?.type === "oauth",
|
||||||
);
|
);
|
||||||
if (oauthProfiles.length === 0) return null;
|
if (oauthProfiles.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const configuredEmail = legacyCfg?.email?.trim();
|
const configuredEmail = legacyCfg?.email?.trim();
|
||||||
if (configuredEmail) {
|
if (configuredEmail) {
|
||||||
const byEmail = oauthProfiles.find((id) => {
|
const byEmail = oauthProfiles.find((id) => {
|
||||||
const cred = params.store.profiles[id];
|
const cred = params.store.profiles[id];
|
||||||
if (!cred || cred.type !== "oauth") return false;
|
if (!cred || cred.type !== "oauth") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const email = cred.email?.trim();
|
const email = cred.email?.trim();
|
||||||
return email === configuredEmail || id === `${providerKey}:${configuredEmail}`;
|
return email === configuredEmail || id === `${providerKey}:${configuredEmail}`;
|
||||||
});
|
});
|
||||||
if (byEmail) return byEmail;
|
if (byEmail) {
|
||||||
|
return byEmail;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastGood = params.store.lastGood?.[providerKey] ?? params.store.lastGood?.[params.provider];
|
const lastGood = params.store.lastGood?.[providerKey] ?? params.store.lastGood?.[params.provider];
|
||||||
if (lastGood && oauthProfiles.includes(lastGood)) return lastGood;
|
if (lastGood && oauthProfiles.includes(lastGood)) {
|
||||||
|
return lastGood;
|
||||||
|
}
|
||||||
|
|
||||||
const nonLegacy = oauthProfiles.filter((id) => id !== params.legacyProfileId);
|
const nonLegacy = oauthProfiles.filter((id) => id !== params.legacyProfileId);
|
||||||
if (nonLegacy.length === 1) return nonLegacy[0] ?? null;
|
if (nonLegacy.length === 1) {
|
||||||
|
return nonLegacy[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
const emailLike = nonLegacy.filter((id) => isEmailLike(getProfileSuffix(id)));
|
const emailLike = nonLegacy.filter((id) => isEmailLike(getProfileSuffix(id)));
|
||||||
if (emailLike.length === 1) return emailLike[0] ?? null;
|
if (emailLike.length === 1) {
|
||||||
|
return emailLike[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -107,17 +125,25 @@ export function repairOAuthProfileIdMismatch(params: {
|
|||||||
const providerKey = normalizeProviderId(params.provider);
|
const providerKey = normalizeProviderId(params.provider);
|
||||||
const nextOrder = (() => {
|
const nextOrder = (() => {
|
||||||
const order = params.cfg.auth?.order;
|
const order = params.cfg.auth?.order;
|
||||||
if (!order) return undefined;
|
if (!order) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const resolvedKey = Object.keys(order).find((key) => normalizeProviderId(key) === providerKey);
|
const resolvedKey = Object.keys(order).find((key) => normalizeProviderId(key) === providerKey);
|
||||||
if (!resolvedKey) return order;
|
if (!resolvedKey) {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
const existing = order[resolvedKey];
|
const existing = order[resolvedKey];
|
||||||
if (!Array.isArray(existing)) return order;
|
if (!Array.isArray(existing)) {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
const replaced = existing
|
const replaced = existing
|
||||||
.map((id) => (id === legacyProfileId ? toProfileId : id))
|
.map((id) => (id === legacyProfileId ? toProfileId : id))
|
||||||
.filter((id): id is string => typeof id === "string" && id.trim().length > 0);
|
.filter((id): id is string => typeof id === "string" && id.trim().length > 0);
|
||||||
const deduped: string[] = [];
|
const deduped: string[] = [];
|
||||||
for (const entry of replaced) {
|
for (const entry of replaced) {
|
||||||
if (!deduped.includes(entry)) deduped.push(entry);
|
if (!deduped.includes(entry)) {
|
||||||
|
deduped.push(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { ...order, [resolvedKey]: deduped };
|
return { ...order, [resolvedKey]: deduped };
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -53,8 +53,11 @@ describe("resolveSessionAuthProfileOverride", () => {
|
|||||||
expect(resolved).toBe("zai:work");
|
expect(resolved).toBe("zai:work");
|
||||||
expect(sessionEntry.authProfileOverride).toBe("zai:work");
|
expect(sessionEntry.authProfileOverride).toBe("zai:work");
|
||||||
} finally {
|
} finally {
|
||||||
if (prevStateDir === undefined) delete process.env.OPENCLAW_STATE_DIR;
|
if (prevStateDir === undefined) {
|
||||||
else process.env.OPENCLAW_STATE_DIR = prevStateDir;
|
delete process.env.OPENCLAW_STATE_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.OPENCLAW_STATE_DIR = prevStateDir;
|
||||||
|
}
|
||||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ function isProfileForProvider(params: {
|
|||||||
store: ReturnType<typeof ensureAuthProfileStore>;
|
store: ReturnType<typeof ensureAuthProfileStore>;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
const entry = params.store.profiles[params.profileId];
|
const entry = params.store.profiles[params.profileId];
|
||||||
if (!entry?.provider) return false;
|
if (!entry?.provider) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return normalizeProviderId(entry.provider) === normalizeProviderId(params.provider);
|
return normalizeProviderId(entry.provider) === normalizeProviderId(params.provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +58,9 @@ export async function resolveSessionAuthProfileOverride(params: {
|
|||||||
storePath,
|
storePath,
|
||||||
isNewSession,
|
isNewSession,
|
||||||
} = params;
|
} = params;
|
||||||
if (!sessionEntry || !sessionStore || !sessionKey) return sessionEntry?.authProfileOverride;
|
if (!sessionEntry || !sessionStore || !sessionKey) {
|
||||||
|
return sessionEntry?.authProfileOverride;
|
||||||
|
}
|
||||||
|
|
||||||
const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
|
const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
|
||||||
const order = resolveAuthProfileOrder({ cfg, store, provider });
|
const order = resolveAuthProfileOrder({ cfg, store, provider });
|
||||||
@@ -77,16 +81,22 @@ export async function resolveSessionAuthProfileOverride(params: {
|
|||||||
current = undefined;
|
current = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order.length === 0) return undefined;
|
if (order.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const pickFirstAvailable = () =>
|
const pickFirstAvailable = () =>
|
||||||
order.find((profileId) => !isProfileInCooldown(store, profileId)) ?? order[0];
|
order.find((profileId) => !isProfileInCooldown(store, profileId)) ?? order[0];
|
||||||
const pickNextAvailable = (active: string) => {
|
const pickNextAvailable = (active: string) => {
|
||||||
const startIndex = order.indexOf(active);
|
const startIndex = order.indexOf(active);
|
||||||
if (startIndex < 0) return pickFirstAvailable();
|
if (startIndex < 0) {
|
||||||
|
return pickFirstAvailable();
|
||||||
|
}
|
||||||
for (let offset = 1; offset <= order.length; offset += 1) {
|
for (let offset = 1; offset <= order.length; offset += 1) {
|
||||||
const candidate = order[(startIndex + offset) % order.length];
|
const candidate = order[(startIndex + offset) % order.length];
|
||||||
if (!isProfileInCooldown(store, candidate)) return candidate;
|
if (!isProfileInCooldown(store, candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return order[startIndex] ?? order[0];
|
return order[startIndex] ?? order[0];
|
||||||
};
|
};
|
||||||
@@ -117,7 +127,9 @@ export async function resolveSessionAuthProfileOverride(params: {
|
|||||||
next = pickFirstAvailable();
|
next = pickFirstAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!next) return current;
|
if (!next) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
const shouldPersist =
|
const shouldPersist =
|
||||||
next !== sessionEntry.authProfileOverride ||
|
next !== sessionEntry.authProfileOverride ||
|
||||||
sessionEntry.authProfileOverrideSource !== "auto" ||
|
sessionEntry.authProfileOverrideSource !== "auto" ||
|
||||||
|
|||||||
@@ -48,12 +48,18 @@ export async function updateAuthProfileStoreWithLock(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function coerceLegacyStore(raw: unknown): LegacyAuthStore | null {
|
function coerceLegacyStore(raw: unknown): LegacyAuthStore | null {
|
||||||
if (!raw || typeof raw !== "object") return null;
|
if (!raw || typeof raw !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const record = raw as Record<string, unknown>;
|
const record = raw as Record<string, unknown>;
|
||||||
if ("profiles" in record) return null;
|
if ("profiles" in record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const entries: LegacyAuthStore = {};
|
const entries: LegacyAuthStore = {};
|
||||||
for (const [key, value] of Object.entries(record)) {
|
for (const [key, value] of Object.entries(record)) {
|
||||||
if (!value || typeof value !== "object") continue;
|
if (!value || typeof value !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const typed = value as Partial<AuthProfileCredential>;
|
const typed = value as Partial<AuthProfileCredential>;
|
||||||
if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") {
|
if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") {
|
||||||
continue;
|
continue;
|
||||||
@@ -67,29 +73,41 @@ function coerceLegacyStore(raw: unknown): LegacyAuthStore | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function coerceAuthStore(raw: unknown): AuthProfileStore | null {
|
function coerceAuthStore(raw: unknown): AuthProfileStore | null {
|
||||||
if (!raw || typeof raw !== "object") return null;
|
if (!raw || typeof raw !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const record = raw as Record<string, unknown>;
|
const record = raw as Record<string, unknown>;
|
||||||
if (!record.profiles || typeof record.profiles !== "object") return null;
|
if (!record.profiles || typeof record.profiles !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const profiles = record.profiles as Record<string, unknown>;
|
const profiles = record.profiles as Record<string, unknown>;
|
||||||
const normalized: Record<string, AuthProfileCredential> = {};
|
const normalized: Record<string, AuthProfileCredential> = {};
|
||||||
for (const [key, value] of Object.entries(profiles)) {
|
for (const [key, value] of Object.entries(profiles)) {
|
||||||
if (!value || typeof value !== "object") continue;
|
if (!value || typeof value !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const typed = value as Partial<AuthProfileCredential>;
|
const typed = value as Partial<AuthProfileCredential>;
|
||||||
if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") {
|
if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!typed.provider) continue;
|
if (!typed.provider) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
normalized[key] = typed as AuthProfileCredential;
|
normalized[key] = typed as AuthProfileCredential;
|
||||||
}
|
}
|
||||||
const order =
|
const order =
|
||||||
record.order && typeof record.order === "object"
|
record.order && typeof record.order === "object"
|
||||||
? Object.entries(record.order as Record<string, unknown>).reduce(
|
? Object.entries(record.order as Record<string, unknown>).reduce(
|
||||||
(acc, [provider, value]) => {
|
(acc, [provider, value]) => {
|
||||||
if (!Array.isArray(value)) return acc;
|
if (!Array.isArray(value)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
const list = value
|
const list = value
|
||||||
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
if (list.length === 0) return acc;
|
if (list.length === 0) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
acc[provider] = list;
|
acc[provider] = list;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
@@ -115,9 +133,15 @@ function mergeRecord<T>(
|
|||||||
base?: Record<string, T>,
|
base?: Record<string, T>,
|
||||||
override?: Record<string, T>,
|
override?: Record<string, T>,
|
||||||
): Record<string, T> | undefined {
|
): Record<string, T> | undefined {
|
||||||
if (!base && !override) return undefined;
|
if (!base && !override) {
|
||||||
if (!base) return { ...override };
|
return undefined;
|
||||||
if (!override) return { ...base };
|
}
|
||||||
|
if (!base) {
|
||||||
|
return { ...override };
|
||||||
|
}
|
||||||
|
if (!override) {
|
||||||
|
return { ...base };
|
||||||
|
}
|
||||||
return { ...base, ...override };
|
return { ...base, ...override };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,13 +169,19 @@ function mergeAuthProfileStores(
|
|||||||
function mergeOAuthFileIntoStore(store: AuthProfileStore): boolean {
|
function mergeOAuthFileIntoStore(store: AuthProfileStore): boolean {
|
||||||
const oauthPath = resolveOAuthPath();
|
const oauthPath = resolveOAuthPath();
|
||||||
const oauthRaw = loadJsonFile(oauthPath);
|
const oauthRaw = loadJsonFile(oauthPath);
|
||||||
if (!oauthRaw || typeof oauthRaw !== "object") return false;
|
if (!oauthRaw || typeof oauthRaw !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const oauthEntries = oauthRaw as Record<string, OAuthCredentials>;
|
const oauthEntries = oauthRaw as Record<string, OAuthCredentials>;
|
||||||
let mutated = false;
|
let mutated = false;
|
||||||
for (const [provider, creds] of Object.entries(oauthEntries)) {
|
for (const [provider, creds] of Object.entries(oauthEntries)) {
|
||||||
if (!creds || typeof creds !== "object") continue;
|
if (!creds || typeof creds !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const profileId = `${provider}:default`;
|
const profileId = `${provider}:default`;
|
||||||
if (store.profiles[profileId]) continue;
|
if (store.profiles[profileId]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
store.profiles[profileId] = {
|
store.profiles[profileId] = {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
provider,
|
provider,
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ function resolveProfileUnusableUntil(stats: ProfileUsageStats): number | null {
|
|||||||
const values = [stats.cooldownUntil, stats.disabledUntil]
|
const values = [stats.cooldownUntil, stats.disabledUntil]
|
||||||
.filter((value): value is number => typeof value === "number")
|
.filter((value): value is number => typeof value === "number")
|
||||||
.filter((value) => Number.isFinite(value) && value > 0);
|
.filter((value) => Number.isFinite(value) && value > 0);
|
||||||
if (values.length === 0) return null;
|
if (values.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return Math.max(...values);
|
return Math.max(...values);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +18,9 @@ function resolveProfileUnusableUntil(stats: ProfileUsageStats): number | null {
|
|||||||
*/
|
*/
|
||||||
export function isProfileInCooldown(store: AuthProfileStore, profileId: string): boolean {
|
export function isProfileInCooldown(store: AuthProfileStore, profileId: string): boolean {
|
||||||
const stats = store.usageStats?.[profileId];
|
const stats = store.usageStats?.[profileId];
|
||||||
if (!stats) return false;
|
if (!stats) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const unusableUntil = resolveProfileUnusableUntil(stats);
|
const unusableUntil = resolveProfileUnusableUntil(stats);
|
||||||
return unusableUntil ? Date.now() < unusableUntil : false;
|
return unusableUntil ? Date.now() < unusableUntil : false;
|
||||||
}
|
}
|
||||||
@@ -34,7 +38,9 @@ export async function markAuthProfileUsed(params: {
|
|||||||
const updated = await updateAuthProfileStoreWithLock({
|
const updated = await updateAuthProfileStoreWithLock({
|
||||||
agentDir,
|
agentDir,
|
||||||
updater: (freshStore) => {
|
updater: (freshStore) => {
|
||||||
if (!freshStore.profiles[profileId]) return false;
|
if (!freshStore.profiles[profileId]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
freshStore.usageStats = freshStore.usageStats ?? {};
|
freshStore.usageStats = freshStore.usageStats ?? {};
|
||||||
freshStore.usageStats[profileId] = {
|
freshStore.usageStats[profileId] = {
|
||||||
...freshStore.usageStats[profileId],
|
...freshStore.usageStats[profileId],
|
||||||
@@ -52,7 +58,9 @@ export async function markAuthProfileUsed(params: {
|
|||||||
store.usageStats = updated.usageStats;
|
store.usageStats = updated.usageStats;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!store.profiles[profileId]) return;
|
if (!store.profiles[profileId]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
store.usageStats = store.usageStats ?? {};
|
store.usageStats = store.usageStats ?? {};
|
||||||
store.usageStats[profileId] = {
|
store.usageStats[profileId] = {
|
||||||
@@ -97,9 +105,13 @@ function resolveAuthCooldownConfig(params: {
|
|||||||
const cooldowns = params.cfg?.auth?.cooldowns;
|
const cooldowns = params.cfg?.auth?.cooldowns;
|
||||||
const billingOverride = (() => {
|
const billingOverride = (() => {
|
||||||
const map = cooldowns?.billingBackoffHoursByProvider;
|
const map = cooldowns?.billingBackoffHoursByProvider;
|
||||||
if (!map) return undefined;
|
if (!map) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
for (const [key, value] of Object.entries(map)) {
|
for (const [key, value] of Object.entries(map)) {
|
||||||
if (normalizeProviderId(key) === params.providerId) return value;
|
if (normalizeProviderId(key) === params.providerId) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
})();
|
})();
|
||||||
@@ -139,7 +151,9 @@ export function resolveProfileUnusableUntilForDisplay(
|
|||||||
profileId: string,
|
profileId: string,
|
||||||
): number | null {
|
): number | null {
|
||||||
const stats = store.usageStats?.[profileId];
|
const stats = store.usageStats?.[profileId];
|
||||||
if (!stats) return null;
|
if (!stats) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return resolveProfileUnusableUntil(stats);
|
return resolveProfileUnusableUntil(stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +214,9 @@ export async function markAuthProfileFailure(params: {
|
|||||||
agentDir,
|
agentDir,
|
||||||
updater: (freshStore) => {
|
updater: (freshStore) => {
|
||||||
const profile = freshStore.profiles[profileId];
|
const profile = freshStore.profiles[profileId];
|
||||||
if (!profile) return false;
|
if (!profile) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
freshStore.usageStats = freshStore.usageStats ?? {};
|
freshStore.usageStats = freshStore.usageStats ?? {};
|
||||||
const existing = freshStore.usageStats[profileId] ?? {};
|
const existing = freshStore.usageStats[profileId] ?? {};
|
||||||
|
|
||||||
@@ -224,7 +240,9 @@ export async function markAuthProfileFailure(params: {
|
|||||||
store.usageStats = updated.usageStats;
|
store.usageStats = updated.usageStats;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!store.profiles[profileId]) return;
|
if (!store.profiles[profileId]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
store.usageStats = store.usageStats ?? {};
|
store.usageStats = store.usageStats ?? {};
|
||||||
const existing = store.usageStats[profileId] ?? {};
|
const existing = store.usageStats[profileId] ?? {};
|
||||||
@@ -275,7 +293,9 @@ export async function clearAuthProfileCooldown(params: {
|
|||||||
const updated = await updateAuthProfileStoreWithLock({
|
const updated = await updateAuthProfileStoreWithLock({
|
||||||
agentDir,
|
agentDir,
|
||||||
updater: (freshStore) => {
|
updater: (freshStore) => {
|
||||||
if (!freshStore.usageStats?.[profileId]) return false;
|
if (!freshStore.usageStats?.[profileId]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
freshStore.usageStats[profileId] = {
|
freshStore.usageStats[profileId] = {
|
||||||
...freshStore.usageStats[profileId],
|
...freshStore.usageStats[profileId],
|
||||||
@@ -289,7 +309,9 @@ export async function clearAuthProfileCooldown(params: {
|
|||||||
store.usageStats = updated.usageStats;
|
store.usageStats = updated.usageStats;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!store.usageStats?.[profileId]) return;
|
if (!store.usageStats?.[profileId]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
store.usageStats[profileId] = {
|
store.usageStats[profileId] = {
|
||||||
...store.usageStats[profileId],
|
...store.usageStats[profileId],
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ const MAX_JOB_TTL_MS = 3 * 60 * 60 * 1000; // 3 hours
|
|||||||
const DEFAULT_PENDING_OUTPUT_CHARS = 30_000;
|
const DEFAULT_PENDING_OUTPUT_CHARS = 30_000;
|
||||||
|
|
||||||
function clampTtl(value: number | undefined) {
|
function clampTtl(value: number | undefined) {
|
||||||
if (!value || Number.isNaN(value)) return DEFAULT_JOB_TTL_MS;
|
if (!value || Number.isNaN(value)) {
|
||||||
|
return DEFAULT_JOB_TTL_MS;
|
||||||
|
}
|
||||||
return Math.min(Math.max(value, MIN_JOB_TTL_MS), MAX_JOB_TTL_MS);
|
return Math.min(Math.max(value, MIN_JOB_TTL_MS), MAX_JOB_TTL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +157,9 @@ export function markBackgrounded(session: ProcessSession) {
|
|||||||
|
|
||||||
function moveToFinished(session: ProcessSession, status: ProcessStatus) {
|
function moveToFinished(session: ProcessSession, status: ProcessStatus) {
|
||||||
runningSessions.delete(session.id);
|
runningSessions.delete(session.id);
|
||||||
if (!session.backgrounded) return;
|
if (!session.backgrounded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
finishedSessions.set(session.id, {
|
finishedSessions.set(session.id, {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
command: session.command,
|
command: session.command,
|
||||||
@@ -174,18 +178,24 @@ function moveToFinished(session: ProcessSession, status: ProcessStatus) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function tail(text: string, max = 2000) {
|
export function tail(text: string, max = 2000) {
|
||||||
if (text.length <= max) return text;
|
if (text.length <= max) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
return text.slice(text.length - max);
|
return text.slice(text.length - max);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sumPendingChars(buffer: string[]) {
|
function sumPendingChars(buffer: string[]) {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
for (const chunk of buffer) total += chunk.length;
|
for (const chunk of buffer) {
|
||||||
|
total += chunk.length;
|
||||||
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
function capPendingBuffer(buffer: string[], pendingChars: number, cap: number) {
|
function capPendingBuffer(buffer: string[], pendingChars: number, cap: number) {
|
||||||
if (pendingChars <= cap) return pendingChars;
|
if (pendingChars <= cap) {
|
||||||
|
return pendingChars;
|
||||||
|
}
|
||||||
const last = buffer.at(-1);
|
const last = buffer.at(-1);
|
||||||
if (last && last.length >= cap) {
|
if (last && last.length >= cap) {
|
||||||
buffer.length = 0;
|
buffer.length = 0;
|
||||||
@@ -205,7 +215,9 @@ function capPendingBuffer(buffer: string[], pendingChars: number, cap: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function trimWithCap(text: string, max: number) {
|
export function trimWithCap(text: string, max: number) {
|
||||||
if (text.length <= max) return text;
|
if (text.length <= max) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
return text.slice(text.length - max);
|
return text.slice(text.length - max);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +240,9 @@ export function resetProcessRegistryForTests() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setJobTtlMs(value?: number) {
|
export function setJobTtlMs(value?: number) {
|
||||||
if (value === undefined || Number.isNaN(value)) return;
|
if (value === undefined || Number.isNaN(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
jobTtlMs = clampTtl(value);
|
jobTtlMs = clampTtl(value);
|
||||||
stopSweeper();
|
stopSweeper();
|
||||||
startSweeper();
|
startSweeper();
|
||||||
@@ -244,13 +258,17 @@ function pruneFinishedSessions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startSweeper() {
|
function startSweeper() {
|
||||||
if (sweeper) return;
|
if (sweeper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
sweeper = setInterval(pruneFinishedSessions, Math.max(30_000, jobTtlMs / 6));
|
sweeper = setInterval(pruneFinishedSessions, Math.max(30_000, jobTtlMs / 6));
|
||||||
sweeper.unref?.();
|
sweeper.unref?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopSweeper() {
|
function stopSweeper() {
|
||||||
if (!sweeper) return;
|
if (!sweeper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
clearInterval(sweeper);
|
clearInterval(sweeper);
|
||||||
sweeper = null;
|
sweeper = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ test("background exec is not killed when tool signal aborts", async () => {
|
|||||||
expect(running?.exited).toBe(false);
|
expect(running?.exited).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
const pid = running?.pid;
|
const pid = running?.pid;
|
||||||
if (pid) killProcessTree(pid);
|
if (pid) {
|
||||||
|
killProcessTree(pid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,7 +78,9 @@ test("background exec still times out after tool signal abort", async () => {
|
|||||||
expect(finished?.status).toBe("failed");
|
expect(finished?.status).toBe("failed");
|
||||||
} finally {
|
} finally {
|
||||||
const pid = running?.pid;
|
const pid = running?.pid;
|
||||||
if (pid) killProcessTree(pid);
|
if (pid) {
|
||||||
|
killProcessTree(pid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,7 +109,9 @@ test("yielded background exec is not killed when tool signal aborts", async () =
|
|||||||
expect(running?.exited).toBe(false);
|
expect(running?.exited).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
const pid = running?.pid;
|
const pid = running?.pid;
|
||||||
if (pid) killProcessTree(pid);
|
if (pid) {
|
||||||
|
killProcessTree(pid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,6 +141,8 @@ test("yielded background exec still times out", async () => {
|
|||||||
expect(finished?.status).toBe("failed");
|
expect(finished?.status).toBe("failed");
|
||||||
} finally {
|
} finally {
|
||||||
const pid = running?.pid;
|
const pid = running?.pid;
|
||||||
if (pid) killProcessTree(pid);
|
if (pid) {
|
||||||
|
killProcessTree(pid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ describe("exec PATH login shell merge", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("merges login-shell PATH for host=gateway", async () => {
|
it("merges login-shell PATH for host=gateway", async () => {
|
||||||
if (isWin) return;
|
if (isWin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
process.env.PATH = "/usr/bin";
|
process.env.PATH = "/usr/bin";
|
||||||
|
|
||||||
const { createExecTool } = await import("./bash-tools.exec.js");
|
const { createExecTool } = await import("./bash-tools.exec.js");
|
||||||
@@ -79,7 +81,9 @@ describe("exec PATH login shell merge", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("skips login-shell PATH when env.PATH is provided", async () => {
|
it("skips login-shell PATH when env.PATH is provided", async () => {
|
||||||
if (isWin) return;
|
if (isWin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
process.env.PATH = "/usr/bin";
|
process.env.PATH = "/usr/bin";
|
||||||
|
|
||||||
const { createExecTool } = await import("./bash-tools.exec.js");
|
const { createExecTool } = await import("./bash-tools.exec.js");
|
||||||
|
|||||||
@@ -252,13 +252,19 @@ function normalizeNotifyOutput(value: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizePathPrepend(entries?: string[]) {
|
function normalizePathPrepend(entries?: string[]) {
|
||||||
if (!Array.isArray(entries)) return [];
|
if (!Array.isArray(entries)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const normalized: string[] = [];
|
const normalized: string[] = [];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (typeof entry !== "string") continue;
|
if (typeof entry !== "string") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const trimmed = entry.trim();
|
const trimmed = entry.trim();
|
||||||
if (!trimmed || seen.has(trimmed)) continue;
|
if (!trimmed || seen.has(trimmed)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
seen.add(trimmed);
|
seen.add(trimmed);
|
||||||
normalized.push(trimmed);
|
normalized.push(trimmed);
|
||||||
}
|
}
|
||||||
@@ -266,7 +272,9 @@ function normalizePathPrepend(entries?: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mergePathPrepend(existing: string | undefined, prepend: string[]) {
|
function mergePathPrepend(existing: string | undefined, prepend: string[]) {
|
||||||
if (prepend.length === 0) return existing;
|
if (prepend.length === 0) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
const partsExisting = (existing ?? "")
|
const partsExisting = (existing ?? "")
|
||||||
.split(path.delimiter)
|
.split(path.delimiter)
|
||||||
.map((part) => part.trim())
|
.map((part) => part.trim())
|
||||||
@@ -274,7 +282,9 @@ function mergePathPrepend(existing: string | undefined, prepend: string[]) {
|
|||||||
const merged: string[] = [];
|
const merged: string[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const part of [...prepend, ...partsExisting]) {
|
for (const part of [...prepend, ...partsExisting]) {
|
||||||
if (seen.has(part)) continue;
|
if (seen.has(part)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
seen.add(part);
|
seen.add(part);
|
||||||
merged.push(part);
|
merged.push(part);
|
||||||
}
|
}
|
||||||
@@ -286,27 +296,43 @@ function applyPathPrepend(
|
|||||||
prepend: string[],
|
prepend: string[],
|
||||||
options?: { requireExisting?: boolean },
|
options?: { requireExisting?: boolean },
|
||||||
) {
|
) {
|
||||||
if (prepend.length === 0) return;
|
if (prepend.length === 0) {
|
||||||
if (options?.requireExisting && !env.PATH) return;
|
return;
|
||||||
|
}
|
||||||
|
if (options?.requireExisting && !env.PATH) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const merged = mergePathPrepend(env.PATH, prepend);
|
const merged = mergePathPrepend(env.PATH, prepend);
|
||||||
if (merged) env.PATH = merged;
|
if (merged) {
|
||||||
|
env.PATH = merged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyShellPath(env: Record<string, string>, shellPath?: string | null) {
|
function applyShellPath(env: Record<string, string>, shellPath?: string | null) {
|
||||||
if (!shellPath) return;
|
if (!shellPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const entries = shellPath
|
const entries = shellPath
|
||||||
.split(path.delimiter)
|
.split(path.delimiter)
|
||||||
.map((part) => part.trim())
|
.map((part) => part.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
if (entries.length === 0) return;
|
if (entries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const merged = mergePathPrepend(env.PATH, entries);
|
const merged = mergePathPrepend(env.PATH, entries);
|
||||||
if (merged) env.PATH = merged;
|
if (merged) {
|
||||||
|
env.PATH = merged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeNotifyOnExit(session: ProcessSession, status: "completed" | "failed") {
|
function maybeNotifyOnExit(session: ProcessSession, status: "completed" | "failed") {
|
||||||
if (!session.backgrounded || !session.notifyOnExit || session.exitNotified) return;
|
if (!session.backgrounded || !session.notifyOnExit || session.exitNotified) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sessionKey = session.sessionKey?.trim();
|
const sessionKey = session.sessionKey?.trim();
|
||||||
if (!sessionKey) return;
|
if (!sessionKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
session.exitNotified = true;
|
session.exitNotified = true;
|
||||||
const exitLabel = session.exitSignal
|
const exitLabel = session.exitSignal
|
||||||
? `signal ${session.exitSignal}`
|
? `signal ${session.exitSignal}`
|
||||||
@@ -329,13 +355,17 @@ function resolveApprovalRunningNoticeMs(value?: number) {
|
|||||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||||
return DEFAULT_APPROVAL_RUNNING_NOTICE_MS;
|
return DEFAULT_APPROVAL_RUNNING_NOTICE_MS;
|
||||||
}
|
}
|
||||||
if (value <= 0) return 0;
|
if (value <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
return Math.floor(value);
|
return Math.floor(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitExecSystemEvent(text: string, opts: { sessionKey?: string; contextKey?: string }) {
|
function emitExecSystemEvent(text: string, opts: { sessionKey?: string; contextKey?: string }) {
|
||||||
const sessionKey = opts.sessionKey?.trim();
|
const sessionKey = opts.sessionKey?.trim();
|
||||||
if (!sessionKey) return;
|
if (!sessionKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
enqueueSystemEvent(text, { sessionKey, contextKey: opts.contextKey });
|
enqueueSystemEvent(text, { sessionKey, contextKey: opts.contextKey });
|
||||||
requestHeartbeatNow({ reason: "exec-event" });
|
requestHeartbeatNow({ reason: "exec-event" });
|
||||||
}
|
}
|
||||||
@@ -528,13 +558,17 @@ async function runExecProcess(opts: {
|
|||||||
let resolveFn: ((outcome: ExecProcessOutcome) => void) | null = null;
|
let resolveFn: ((outcome: ExecProcessOutcome) => void) | null = null;
|
||||||
|
|
||||||
const settle = (outcome: ExecProcessOutcome) => {
|
const settle = (outcome: ExecProcessOutcome) => {
|
||||||
if (settled) return;
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
settled = true;
|
settled = true;
|
||||||
resolveFn?.(outcome);
|
resolveFn?.(outcome);
|
||||||
};
|
};
|
||||||
|
|
||||||
const finalizeTimeout = () => {
|
const finalizeTimeout = () => {
|
||||||
if (session.exited) return;
|
if (session.exited) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
markExited(session, null, "SIGKILL", "failed");
|
markExited(session, null, "SIGKILL", "failed");
|
||||||
maybeNotifyOnExit(session, "failed");
|
maybeNotifyOnExit(session, "failed");
|
||||||
const aggregated = session.aggregated.trim();
|
const aggregated = session.aggregated.trim();
|
||||||
@@ -567,7 +601,9 @@ async function runExecProcess(opts: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const emitUpdate = () => {
|
const emitUpdate = () => {
|
||||||
if (!opts.onUpdate) return;
|
if (!opts.onUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const tailText = session.tail || session.aggregated;
|
const tailText = session.tail || session.aggregated;
|
||||||
const warningText = opts.warnings.length ? `${opts.warnings.join("\n")}\n\n` : "";
|
const warningText = opts.warnings.length ? `${opts.warnings.join("\n")}\n\n` : "";
|
||||||
opts.onUpdate({
|
opts.onUpdate({
|
||||||
@@ -619,8 +655,12 @@ async function runExecProcess(opts: {
|
|||||||
const promise = new Promise<ExecProcessOutcome>((resolve) => {
|
const promise = new Promise<ExecProcessOutcome>((resolve) => {
|
||||||
resolveFn = resolve;
|
resolveFn = resolve;
|
||||||
const handleExit = (code: number | null, exitSignal: NodeJS.Signals | number | null) => {
|
const handleExit = (code: number | null, exitSignal: NodeJS.Signals | number | null) => {
|
||||||
if (timeoutTimer) clearTimeout(timeoutTimer);
|
if (timeoutTimer) {
|
||||||
if (timeoutFinalizeTimer) clearTimeout(timeoutFinalizeTimer);
|
clearTimeout(timeoutTimer);
|
||||||
|
}
|
||||||
|
if (timeoutFinalizeTimer) {
|
||||||
|
clearTimeout(timeoutFinalizeTimer);
|
||||||
|
}
|
||||||
const durationMs = Date.now() - startedAt;
|
const durationMs = Date.now() - startedAt;
|
||||||
const wasSignal = exitSignal != null;
|
const wasSignal = exitSignal != null;
|
||||||
const isSuccess = code === 0 && !wasSignal && !timedOut;
|
const isSuccess = code === 0 && !wasSignal && !timedOut;
|
||||||
@@ -631,7 +671,9 @@ async function runExecProcess(opts: {
|
|||||||
session.stdin.destroyed = true;
|
session.stdin.destroyed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settled) return;
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const aggregated = session.aggregated.trim();
|
const aggregated = session.aggregated.trim();
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
const reason = timedOut
|
const reason = timedOut
|
||||||
@@ -675,8 +717,12 @@ async function runExecProcess(opts: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
child.once("error", (err) => {
|
child.once("error", (err) => {
|
||||||
if (timeoutTimer) clearTimeout(timeoutTimer);
|
if (timeoutTimer) {
|
||||||
if (timeoutFinalizeTimer) clearTimeout(timeoutFinalizeTimer);
|
clearTimeout(timeoutTimer);
|
||||||
|
}
|
||||||
|
if (timeoutFinalizeTimer) {
|
||||||
|
clearTimeout(timeoutFinalizeTimer);
|
||||||
|
}
|
||||||
markExited(session, null, null, "failed");
|
markExited(session, null, null, "failed");
|
||||||
maybeNotifyOnExit(session, "failed");
|
maybeNotifyOnExit(session, "failed");
|
||||||
const aggregated = session.aggregated.trim();
|
const aggregated = session.aggregated.trim();
|
||||||
@@ -795,8 +841,12 @@ export function createExecTool(
|
|||||||
const contextParts: string[] = [];
|
const contextParts: string[] = [];
|
||||||
const provider = defaults?.messageProvider?.trim();
|
const provider = defaults?.messageProvider?.trim();
|
||||||
const sessionKey = defaults?.sessionKey?.trim();
|
const sessionKey = defaults?.sessionKey?.trim();
|
||||||
if (provider) contextParts.push(`provider=${provider}`);
|
if (provider) {
|
||||||
if (sessionKey) contextParts.push(`session=${sessionKey}`);
|
contextParts.push(`provider=${provider}`);
|
||||||
|
}
|
||||||
|
if (sessionKey) {
|
||||||
|
contextParts.push(`session=${sessionKey}`);
|
||||||
|
}
|
||||||
if (!elevatedDefaults?.enabled) {
|
if (!elevatedDefaults?.enabled) {
|
||||||
gates.push("enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled)");
|
gates.push("enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled)");
|
||||||
} else {
|
} else {
|
||||||
@@ -1098,7 +1148,9 @@ export function createExecTool(
|
|||||||
{ sessionKey: notifySessionKey, contextKey },
|
{ sessionKey: notifySessionKey, contextKey },
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
if (runningTimer) clearTimeout(runningTimer);
|
if (runningTimer) {
|
||||||
|
clearTimeout(runningTimer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -1267,7 +1319,9 @@ export function createExecTool(
|
|||||||
if (allowlistMatches.length > 0) {
|
if (allowlistMatches.length > 0) {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const match of allowlistMatches) {
|
for (const match of allowlistMatches) {
|
||||||
if (seen.has(match.pattern)) continue;
|
if (seen.has(match.pattern)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
seen.add(match.pattern);
|
seen.add(match.pattern);
|
||||||
recordAllowlistUse(
|
recordAllowlistUse(
|
||||||
approvals.file,
|
approvals.file,
|
||||||
@@ -1317,7 +1371,9 @@ export function createExecTool(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const outcome = await run.promise;
|
const outcome = await run.promise;
|
||||||
if (runningTimer) clearTimeout(runningTimer);
|
if (runningTimer) {
|
||||||
|
clearTimeout(runningTimer);
|
||||||
|
}
|
||||||
const output = normalizeNotifyOutput(
|
const output = normalizeNotifyOutput(
|
||||||
tail(outcome.aggregated || "", DEFAULT_NOTIFY_TAIL_CHARS),
|
tail(outcome.aggregated || "", DEFAULT_NOTIFY_TAIL_CHARS),
|
||||||
);
|
);
|
||||||
@@ -1357,7 +1413,9 @@ export function createExecTool(
|
|||||||
if (allowlistMatches.length > 0) {
|
if (allowlistMatches.length > 0) {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const match of allowlistMatches) {
|
for (const match of allowlistMatches) {
|
||||||
if (seen.has(match.pattern)) continue;
|
if (seen.has(match.pattern)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
seen.add(match.pattern);
|
seen.add(match.pattern);
|
||||||
recordAllowlistUse(
|
recordAllowlistUse(
|
||||||
approvals.file,
|
approvals.file,
|
||||||
@@ -1396,12 +1454,15 @@ export function createExecTool(
|
|||||||
|
|
||||||
// Tool-call abort should not kill backgrounded sessions; timeouts still must.
|
// Tool-call abort should not kill backgrounded sessions; timeouts still must.
|
||||||
const onAbortSignal = () => {
|
const onAbortSignal = () => {
|
||||||
if (yielded || run.session.backgrounded) return;
|
if (yielded || run.session.backgrounded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
run.kill();
|
run.kill();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (signal?.aborted) onAbortSignal();
|
if (signal?.aborted) {
|
||||||
else if (signal) {
|
onAbortSignal();
|
||||||
|
} else if (signal) {
|
||||||
signal.addEventListener("abort", onAbortSignal, { once: true });
|
signal.addEventListener("abort", onAbortSignal, { once: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1430,8 +1491,12 @@ export function createExecTool(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onYieldNow = () => {
|
const onYieldNow = () => {
|
||||||
if (yieldTimer) clearTimeout(yieldTimer);
|
if (yieldTimer) {
|
||||||
if (yielded) return;
|
clearTimeout(yieldTimer);
|
||||||
|
}
|
||||||
|
if (yielded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
yielded = true;
|
yielded = true;
|
||||||
markBackgrounded(run.session);
|
markBackgrounded(run.session);
|
||||||
resolveRunning();
|
resolveRunning();
|
||||||
@@ -1442,7 +1507,9 @@ export function createExecTool(
|
|||||||
onYieldNow();
|
onYieldNow();
|
||||||
} else {
|
} else {
|
||||||
yieldTimer = setTimeout(() => {
|
yieldTimer = setTimeout(() => {
|
||||||
if (yielded) return;
|
if (yielded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
yielded = true;
|
yielded = true;
|
||||||
markBackgrounded(run.session);
|
markBackgrounded(run.session);
|
||||||
resolveRunning();
|
resolveRunning();
|
||||||
@@ -1452,8 +1519,12 @@ export function createExecTool(
|
|||||||
|
|
||||||
run.promise
|
run.promise
|
||||||
.then((outcome) => {
|
.then((outcome) => {
|
||||||
if (yieldTimer) clearTimeout(yieldTimer);
|
if (yieldTimer) {
|
||||||
if (yielded || run.session.backgrounded) return;
|
clearTimeout(yieldTimer);
|
||||||
|
}
|
||||||
|
if (yielded || run.session.backgrounded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (outcome.status === "failed") {
|
if (outcome.status === "failed") {
|
||||||
reject(new Error(outcome.reason ?? "Command failed."));
|
reject(new Error(outcome.reason ?? "Command failed."));
|
||||||
return;
|
return;
|
||||||
@@ -1475,8 +1546,12 @@ export function createExecTool(
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (yieldTimer) clearTimeout(yieldTimer);
|
if (yieldTimer) {
|
||||||
if (yielded || run.session.backgrounded) return;
|
clearTimeout(yieldTimer);
|
||||||
|
}
|
||||||
|
if (yielded || run.session.backgrounded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
reject(err as Error);
|
reject(err as Error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -337,8 +337,11 @@ export function createProcessTool(
|
|||||||
}
|
}
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
stdin.write(params.data ?? "", (err) => {
|
stdin.write(params.data ?? "", (err) => {
|
||||||
if (err) reject(err);
|
if (err) {
|
||||||
else resolve();
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (params.eof) {
|
if (params.eof) {
|
||||||
@@ -414,8 +417,11 @@ export function createProcessTool(
|
|||||||
}
|
}
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
stdin.write(data, (err) => {
|
stdin.write(data, (err) => {
|
||||||
if (err) reject(err);
|
if (err) {
|
||||||
else resolve();
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -472,8 +478,11 @@ export function createProcessTool(
|
|||||||
}
|
}
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
stdin.write("\r", (err) => {
|
stdin.write("\r", (err) => {
|
||||||
if (err) reject(err);
|
if (err) {
|
||||||
else resolve();
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -540,8 +549,11 @@ export function createProcessTool(
|
|||||||
}
|
}
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
stdin.write(payload, (err) => {
|
stdin.write(payload, (err) => {
|
||||||
if (err) reject(err);
|
if (err) {
|
||||||
else resolve();
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -38,9 +38,13 @@ export function buildSandboxEnv(params: {
|
|||||||
|
|
||||||
export function coerceEnv(env?: NodeJS.ProcessEnv | Record<string, string>) {
|
export function coerceEnv(env?: NodeJS.ProcessEnv | Record<string, string>) {
|
||||||
const record: Record<string, string> = {};
|
const record: Record<string, string> = {};
|
||||||
if (!env) return record;
|
if (!env) {
|
||||||
|
return record;
|
||||||
|
}
|
||||||
for (const [key, value] of Object.entries(env)) {
|
for (const [key, value] of Object.entries(env)) {
|
||||||
if (typeof value === "string") record[key] = value;
|
if (typeof value === "string") {
|
||||||
|
record[key] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
@@ -53,7 +57,9 @@ export function buildDockerExecArgs(params: {
|
|||||||
tty: boolean;
|
tty: boolean;
|
||||||
}) {
|
}) {
|
||||||
const args = ["exec", "-i"];
|
const args = ["exec", "-i"];
|
||||||
if (params.tty) args.push("-t");
|
if (params.tty) {
|
||||||
|
args.push("-t");
|
||||||
|
}
|
||||||
if (params.workdir) {
|
if (params.workdir) {
|
||||||
args.push("-w", params.workdir);
|
args.push("-w", params.workdir);
|
||||||
}
|
}
|
||||||
@@ -122,7 +128,9 @@ export function resolveWorkdir(workdir: string, warnings: string[]) {
|
|||||||
const fallback = current ?? homedir();
|
const fallback = current ?? homedir();
|
||||||
try {
|
try {
|
||||||
const stats = statSync(workdir);
|
const stats = statSync(workdir);
|
||||||
if (stats.isDirectory()) return workdir;
|
if (stats.isDirectory()) {
|
||||||
|
return workdir;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore, fallback below
|
// ignore, fallback below
|
||||||
}
|
}
|
||||||
@@ -145,13 +153,17 @@ export function clampNumber(
|
|||||||
min: number,
|
min: number,
|
||||||
max: number,
|
max: number,
|
||||||
) {
|
) {
|
||||||
if (value === undefined || Number.isNaN(value)) return defaultValue;
|
if (value === undefined || Number.isNaN(value)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
return Math.min(Math.max(value, min), max);
|
return Math.min(Math.max(value, min), max);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readEnvInt(key: string) {
|
export function readEnvInt(key: string) {
|
||||||
const raw = process.env[key];
|
const raw = process.env[key];
|
||||||
if (!raw) return undefined;
|
if (!raw) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const parsed = Number.parseInt(raw, 10);
|
const parsed = Number.parseInt(raw, 10);
|
||||||
return Number.isFinite(parsed) ? parsed : undefined;
|
return Number.isFinite(parsed) ? parsed : undefined;
|
||||||
}
|
}
|
||||||
@@ -165,7 +177,9 @@ export function chunkString(input: string, limit = CHUNK_LIMIT) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function truncateMiddle(str: string, max: number) {
|
export function truncateMiddle(str: string, max: number) {
|
||||||
if (str.length <= max) return str;
|
if (str.length <= max) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
const half = Math.floor((max - 3) / 2);
|
const half = Math.floor((max - 3) / 2);
|
||||||
return `${sliceUtf16Safe(str, 0, half)}...${sliceUtf16Safe(str, -half)}`;
|
return `${sliceUtf16Safe(str, 0, half)}...${sliceUtf16Safe(str, -half)}`;
|
||||||
}
|
}
|
||||||
@@ -175,7 +189,9 @@ export function sliceLogLines(
|
|||||||
offset?: number,
|
offset?: number,
|
||||||
limit?: number,
|
limit?: number,
|
||||||
): { slice: string; totalLines: number; totalChars: number } {
|
): { slice: string; totalLines: number; totalChars: number } {
|
||||||
if (!text) return { slice: "", totalLines: 0, totalChars: 0 };
|
if (!text) {
|
||||||
|
return { slice: "", totalLines: 0, totalChars: 0 };
|
||||||
|
}
|
||||||
const normalized = text.replace(/\r\n/g, "\n");
|
const normalized = text.replace(/\r\n/g, "\n");
|
||||||
const lines = normalized.split("\n");
|
const lines = normalized.split("\n");
|
||||||
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
||||||
@@ -198,11 +214,17 @@ export function sliceLogLines(
|
|||||||
|
|
||||||
export function deriveSessionName(command: string): string | undefined {
|
export function deriveSessionName(command: string): string | undefined {
|
||||||
const tokens = tokenizeCommand(command);
|
const tokens = tokenizeCommand(command);
|
||||||
if (tokens.length === 0) return undefined;
|
if (tokens.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const verb = tokens[0];
|
const verb = tokens[0];
|
||||||
let target = tokens.slice(1).find((t) => !t.startsWith("-"));
|
let target = tokens.slice(1).find((t) => !t.startsWith("-"));
|
||||||
if (!target) target = tokens[1];
|
if (!target) {
|
||||||
if (!target) return verb;
|
target = tokens[1];
|
||||||
|
}
|
||||||
|
if (!target) {
|
||||||
|
return verb;
|
||||||
|
}
|
||||||
const cleaned = truncateMiddle(stripQuotes(target), 48);
|
const cleaned = truncateMiddle(stripQuotes(target), 48);
|
||||||
return `${stripQuotes(verb)} ${cleaned}`;
|
return `${stripQuotes(verb)} ${cleaned}`;
|
||||||
}
|
}
|
||||||
@@ -224,15 +246,21 @@ function stripQuotes(value: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function formatDuration(ms: number) {
|
export function formatDuration(ms: number) {
|
||||||
if (ms < 1000) return `${ms}ms`;
|
if (ms < 1000) {
|
||||||
|
return `${ms}ms`;
|
||||||
|
}
|
||||||
const seconds = Math.floor(ms / 1000);
|
const seconds = Math.floor(ms / 1000);
|
||||||
if (seconds < 60) return `${seconds}s`;
|
if (seconds < 60) {
|
||||||
|
return `${seconds}s`;
|
||||||
|
}
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
const rem = seconds % 60;
|
const rem = seconds % 60;
|
||||||
return `${minutes}m${rem.toString().padStart(2, "0")}s`;
|
return `${minutes}m${rem.toString().padStart(2, "0")}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pad(str: string, width: number) {
|
export function pad(str: string, width: number) {
|
||||||
if (str.length >= width) return str;
|
if (str.length >= width) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
return str + " ".repeat(width - str.length);
|
return str + " ".repeat(width - str.length);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import { sanitizeBinaryOutput } from "./shell-utils.js";
|
|||||||
const isWin = process.platform === "win32";
|
const isWin = process.platform === "win32";
|
||||||
const resolveShellFromPath = (name: string) => {
|
const resolveShellFromPath = (name: string) => {
|
||||||
const envPath = process.env.PATH ?? "";
|
const envPath = process.env.PATH ?? "";
|
||||||
if (!envPath) return undefined;
|
if (!envPath) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const entries = envPath.split(path.delimiter).filter(Boolean);
|
const entries = envPath.split(path.delimiter).filter(Boolean);
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const candidate = path.join(entry, name);
|
const candidate = path.join(entry, name);
|
||||||
@@ -71,11 +73,15 @@ describe("exec tool backgrounding", () => {
|
|||||||
const originalShell = process.env.SHELL;
|
const originalShell = process.env.SHELL;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
if (!isWin && defaultShell) process.env.SHELL = defaultShell;
|
if (!isWin && defaultShell) {
|
||||||
|
process.env.SHELL = defaultShell;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
if (!isWin) process.env.SHELL = originalShell;
|
if (!isWin) {
|
||||||
|
process.env.SHELL = originalShell;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it(
|
it(
|
||||||
@@ -301,12 +307,16 @@ describe("exec PATH handling", () => {
|
|||||||
const originalShell = process.env.SHELL;
|
const originalShell = process.env.SHELL;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
if (!isWin && defaultShell) process.env.SHELL = defaultShell;
|
if (!isWin && defaultShell) {
|
||||||
|
process.env.SHELL = defaultShell;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
process.env.PATH = originalPath;
|
process.env.PATH = originalPath;
|
||||||
if (!isWin) process.env.SHELL = originalShell;
|
if (!isWin) {
|
||||||
|
process.env.SHELL = originalShell;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prepends configured path entries", async () => {
|
it("prepends configured path entries", async () => {
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ const discoveryCache = new Map<string, BedrockDiscoveryCacheEntry>();
|
|||||||
let hasLoggedBedrockError = false;
|
let hasLoggedBedrockError = false;
|
||||||
|
|
||||||
function normalizeProviderFilter(filter?: string[]): string[] {
|
function normalizeProviderFilter(filter?: string[]): string[] {
|
||||||
if (!filter || filter.length === 0) return [];
|
if (!filter || filter.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const normalized = new Set(
|
const normalized = new Set(
|
||||||
filter.map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0),
|
filter.map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0),
|
||||||
);
|
);
|
||||||
@@ -59,10 +61,16 @@ function mapInputModalities(summary: BedrockModelSummary): Array<"text" | "image
|
|||||||
const mapped = new Set<"text" | "image">();
|
const mapped = new Set<"text" | "image">();
|
||||||
for (const modality of inputs) {
|
for (const modality of inputs) {
|
||||||
const lower = modality.toLowerCase();
|
const lower = modality.toLowerCase();
|
||||||
if (lower === "text") mapped.add("text");
|
if (lower === "text") {
|
||||||
if (lower === "image") mapped.add("image");
|
mapped.add("text");
|
||||||
|
}
|
||||||
|
if (lower === "image") {
|
||||||
|
mapped.add("image");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mapped.size === 0) {
|
||||||
|
mapped.add("text");
|
||||||
}
|
}
|
||||||
if (mapped.size === 0) mapped.add("text");
|
|
||||||
return Array.from(mapped);
|
return Array.from(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,21 +90,35 @@ function resolveDefaultMaxTokens(config?: BedrockDiscoveryConfig): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function matchesProviderFilter(summary: BedrockModelSummary, filter: string[]): boolean {
|
function matchesProviderFilter(summary: BedrockModelSummary, filter: string[]): boolean {
|
||||||
if (filter.length === 0) return true;
|
if (filter.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const providerName =
|
const providerName =
|
||||||
summary.providerName ??
|
summary.providerName ??
|
||||||
(typeof summary.modelId === "string" ? summary.modelId.split(".")[0] : undefined);
|
(typeof summary.modelId === "string" ? summary.modelId.split(".")[0] : undefined);
|
||||||
const normalized = providerName?.trim().toLowerCase();
|
const normalized = providerName?.trim().toLowerCase();
|
||||||
if (!normalized) return false;
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return filter.includes(normalized);
|
return filter.includes(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldIncludeSummary(summary: BedrockModelSummary, filter: string[]): boolean {
|
function shouldIncludeSummary(summary: BedrockModelSummary, filter: string[]): boolean {
|
||||||
if (!summary.modelId?.trim()) return false;
|
if (!summary.modelId?.trim()) {
|
||||||
if (!matchesProviderFilter(summary, filter)) return false;
|
return false;
|
||||||
if (summary.responseStreamingSupported !== true) return false;
|
}
|
||||||
if (!includesTextModalities(summary.outputModalities)) return false;
|
if (!matchesProviderFilter(summary, filter)) {
|
||||||
if (!isActive(summary)) return false;
|
return false;
|
||||||
|
}
|
||||||
|
if (summary.responseStreamingSupported !== true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!includesTextModalities(summary.outputModalities)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isActive(summary)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +182,9 @@ export async function discoverBedrockModels(params: {
|
|||||||
const response = await client.send(new ListFoundationModelsCommand({}));
|
const response = await client.send(new ListFoundationModelsCommand({}));
|
||||||
const discovered: ModelDefinitionConfig[] = [];
|
const discovered: ModelDefinitionConfig[] = [];
|
||||||
for (const summary of response.modelSummaries ?? []) {
|
for (const summary of response.modelSummaries ?? []) {
|
||||||
if (!shouldIncludeSummary(summary, providerFilter)) continue;
|
if (!shouldIncludeSummary(summary, providerFilter)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
discovered.push(
|
discovered.push(
|
||||||
toModelDefinition(summary, {
|
toModelDefinition(summary, {
|
||||||
contextWindow: defaultContextWindow,
|
contextWindow: defaultContextWindow,
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ export function makeBootstrapWarn(params: {
|
|||||||
sessionLabel: string;
|
sessionLabel: string;
|
||||||
warn?: (message: string) => void;
|
warn?: (message: string) => void;
|
||||||
}): ((message: string) => void) | undefined {
|
}): ((message: string) => void) | undefined {
|
||||||
if (!params.warn) return undefined;
|
if (!params.warn) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return (message: string) => params.warn?.(`${message} (sessionKey=${params.sessionLabel})`);
|
return (message: string) => params.warn?.(`${message} (sessionKey=${params.sessionLabel})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,9 @@ function resolveCacheTraceConfig(params: CacheTraceInit): CacheTraceConfig {
|
|||||||
|
|
||||||
function getWriter(filePath: string): CacheTraceWriter {
|
function getWriter(filePath: string): CacheTraceWriter {
|
||||||
const existing = writers.get(filePath);
|
const existing = writers.get(filePath);
|
||||||
if (existing) return existing;
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
const dir = path.dirname(filePath);
|
const dir = path.dirname(filePath);
|
||||||
const ready = fs.mkdir(dir, { recursive: true }).catch(() => undefined);
|
const ready = fs.mkdir(dir, { recursive: true }).catch(() => undefined);
|
||||||
@@ -125,10 +127,18 @@ function getWriter(filePath: string): CacheTraceWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stableStringify(value: unknown): string {
|
function stableStringify(value: unknown): string {
|
||||||
if (value === null || value === undefined) return String(value);
|
if (value === null || value === undefined) {
|
||||||
if (typeof value === "number" && !Number.isFinite(value)) return JSON.stringify(String(value));
|
return String(value);
|
||||||
if (typeof value === "bigint") return JSON.stringify(value.toString());
|
}
|
||||||
if (typeof value !== "object") return JSON.stringify(value) ?? "null";
|
if (typeof value === "number" && !Number.isFinite(value)) {
|
||||||
|
return JSON.stringify(String(value));
|
||||||
|
}
|
||||||
|
if (typeof value === "bigint") {
|
||||||
|
return JSON.stringify(value.toString());
|
||||||
|
}
|
||||||
|
if (typeof value !== "object") {
|
||||||
|
return JSON.stringify(value) ?? "null";
|
||||||
|
}
|
||||||
if (value instanceof Error) {
|
if (value instanceof Error) {
|
||||||
return stableStringify({
|
return stableStringify({
|
||||||
name: value.name,
|
name: value.name,
|
||||||
@@ -174,8 +184,12 @@ function summarizeMessages(messages: AgentMessage[]): {
|
|||||||
function safeJsonStringify(value: unknown): string | null {
|
function safeJsonStringify(value: unknown): string | null {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(value, (_key, val) => {
|
return JSON.stringify(value, (_key, val) => {
|
||||||
if (typeof val === "bigint") return val.toString();
|
if (typeof val === "bigint") {
|
||||||
if (typeof val === "function") return "[Function]";
|
return val.toString();
|
||||||
|
}
|
||||||
|
if (typeof val === "function") {
|
||||||
|
return "[Function]";
|
||||||
|
}
|
||||||
if (val instanceof Error) {
|
if (val instanceof Error) {
|
||||||
return { name: val.name, message: val.message, stack: val.stack };
|
return { name: val.name, message: val.message, stack: val.stack };
|
||||||
}
|
}
|
||||||
@@ -191,7 +205,9 @@ function safeJsonStringify(value: unknown): string | null {
|
|||||||
|
|
||||||
export function createCacheTrace(params: CacheTraceInit): CacheTrace | null {
|
export function createCacheTrace(params: CacheTraceInit): CacheTrace | null {
|
||||||
const cfg = resolveCacheTraceConfig(params);
|
const cfg = resolveCacheTraceConfig(params);
|
||||||
if (!cfg.enabled) return null;
|
if (!cfg.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const writer = params.writer ?? getWriter(cfg.filePath);
|
const writer = params.writer ?? getWriter(cfg.filePath);
|
||||||
let seq = 0;
|
let seq = 0;
|
||||||
@@ -221,8 +237,12 @@ export function createCacheTrace(params: CacheTraceInit): CacheTrace | null {
|
|||||||
event.system = payload.system;
|
event.system = payload.system;
|
||||||
event.systemDigest = digest(payload.system);
|
event.systemDigest = digest(payload.system);
|
||||||
}
|
}
|
||||||
if (payload.options) event.options = payload.options;
|
if (payload.options) {
|
||||||
if (payload.model) event.model = payload.model;
|
event.options = payload.options;
|
||||||
|
}
|
||||||
|
if (payload.model) {
|
||||||
|
event.model = payload.model;
|
||||||
|
}
|
||||||
|
|
||||||
const messages = payload.messages;
|
const messages = payload.messages;
|
||||||
if (Array.isArray(messages)) {
|
if (Array.isArray(messages)) {
|
||||||
@@ -236,11 +256,17 @@ export function createCacheTrace(params: CacheTraceInit): CacheTrace | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.note) event.note = payload.note;
|
if (payload.note) {
|
||||||
if (payload.error) event.error = payload.error;
|
event.note = payload.note;
|
||||||
|
}
|
||||||
|
if (payload.error) {
|
||||||
|
event.error = payload.error;
|
||||||
|
}
|
||||||
|
|
||||||
const line = safeJsonStringify(event);
|
const line = safeJsonStringify(event);
|
||||||
if (!line) return;
|
if (!line) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
writer.write(`${line}\n`);
|
writer.write(`${line}\n`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,13 @@ export function listChannelSupportedActions(params: {
|
|||||||
cfg?: OpenClawConfig;
|
cfg?: OpenClawConfig;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
}): ChannelMessageActionName[] {
|
}): ChannelMessageActionName[] {
|
||||||
if (!params.channel) return [];
|
if (!params.channel) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const plugin = getChannelPlugin(params.channel as Parameters<typeof getChannelPlugin>[0]);
|
const plugin = getChannelPlugin(params.channel as Parameters<typeof getChannelPlugin>[0]);
|
||||||
if (!plugin?.actions?.listActions) return [];
|
if (!plugin?.actions?.listActions) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const cfg = params.cfg ?? ({} as OpenClawConfig);
|
const cfg = params.cfg ?? ({} as OpenClawConfig);
|
||||||
return runPluginListActions(plugin, cfg);
|
return runPluginListActions(plugin, cfg);
|
||||||
}
|
}
|
||||||
@@ -32,7 +36,9 @@ export function listAllChannelSupportedActions(params: {
|
|||||||
}): ChannelMessageActionName[] {
|
}): ChannelMessageActionName[] {
|
||||||
const actions = new Set<ChannelMessageActionName>();
|
const actions = new Set<ChannelMessageActionName>();
|
||||||
for (const plugin of listChannelPlugins()) {
|
for (const plugin of listChannelPlugins()) {
|
||||||
if (!plugin.actions?.listActions) continue;
|
if (!plugin.actions?.listActions) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const cfg = params.cfg ?? ({} as OpenClawConfig);
|
const cfg = params.cfg ?? ({} as OpenClawConfig);
|
||||||
const channelActions = runPluginListActions(plugin, cfg);
|
const channelActions = runPluginListActions(plugin, cfg);
|
||||||
for (const action of channelActions) {
|
for (const action of channelActions) {
|
||||||
@@ -47,9 +53,13 @@ export function listChannelAgentTools(params: { cfg?: OpenClawConfig }): Channel
|
|||||||
const tools: ChannelAgentTool[] = [];
|
const tools: ChannelAgentTool[] = [];
|
||||||
for (const plugin of listChannelPlugins()) {
|
for (const plugin of listChannelPlugins()) {
|
||||||
const entry = plugin.agentTools;
|
const entry = plugin.agentTools;
|
||||||
if (!entry) continue;
|
if (!entry) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const resolved = typeof entry === "function" ? entry(params) : entry;
|
const resolved = typeof entry === "function" ? entry(params) : entry;
|
||||||
if (Array.isArray(resolved)) tools.push(...resolved);
|
if (Array.isArray(resolved)) {
|
||||||
|
tools.push(...resolved);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return tools;
|
return tools;
|
||||||
}
|
}
|
||||||
@@ -60,10 +70,14 @@ export function resolveChannelMessageToolHints(params: {
|
|||||||
accountId?: string | null;
|
accountId?: string | null;
|
||||||
}): string[] {
|
}): string[] {
|
||||||
const channelId = normalizeAnyChannelId(params.channel);
|
const channelId = normalizeAnyChannelId(params.channel);
|
||||||
if (!channelId) return [];
|
if (!channelId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const dock = getChannelDock(channelId);
|
const dock = getChannelDock(channelId);
|
||||||
const resolve = dock?.agentPrompt?.messageToolHints;
|
const resolve = dock?.agentPrompt?.messageToolHints;
|
||||||
if (!resolve) return [];
|
if (!resolve) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const cfg = params.cfg ?? ({} as OpenClawConfig);
|
const cfg = params.cfg ?? ({} as OpenClawConfig);
|
||||||
return (resolve({ cfg, accountId: params.accountId }) ?? [])
|
return (resolve({ cfg, accountId: params.accountId }) ?? [])
|
||||||
.map((entry) => entry.trim())
|
.map((entry) => entry.trim())
|
||||||
@@ -76,7 +90,9 @@ function runPluginListActions(
|
|||||||
plugin: ChannelPlugin,
|
plugin: ChannelPlugin,
|
||||||
cfg: OpenClawConfig,
|
cfg: OpenClawConfig,
|
||||||
): ChannelMessageActionName[] {
|
): ChannelMessageActionName[] {
|
||||||
if (!plugin.actions?.listActions) return [];
|
if (!plugin.actions?.listActions) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const listed = plugin.actions.listActions({ cfg });
|
const listed = plugin.actions.listActions({ cfg });
|
||||||
return Array.isArray(listed) ? listed : [];
|
return Array.isArray(listed) ? listed : [];
|
||||||
@@ -89,7 +105,9 @@ function runPluginListActions(
|
|||||||
function logListActionsError(pluginId: string, err: unknown) {
|
function logListActionsError(pluginId: string, err: unknown) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
const key = `${pluginId}:${message}`;
|
const key = `${pluginId}:${message}`;
|
||||||
if (loggedListActionErrors.has(key)) return;
|
if (loggedListActionErrors.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
loggedListActionErrors.add(key);
|
loggedListActionErrors.add(key);
|
||||||
const stack = err instanceof Error && err.stack ? err.stack : null;
|
const stack = err instanceof Error && err.stack ? err.stack : null;
|
||||||
const details = stack ?? message;
|
const details = stack ?? message;
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ describe("chutes-oauth", () => {
|
|||||||
it("refreshes tokens using stored client id and falls back to old refresh token", async () => {
|
it("refreshes tokens using stored client id and falls back to old refresh token", async () => {
|
||||||
const fetchFn: typeof fetch = async (input, init) => {
|
const fetchFn: typeof fetch = async (input, init) => {
|
||||||
const url = String(input);
|
const url = String(input);
|
||||||
if (url !== CHUTES_TOKEN_ENDPOINT) return new Response("not found", { status: 404 });
|
if (url !== CHUTES_TOKEN_ENDPOINT) {
|
||||||
|
return new Response("not found", { status: 404 });
|
||||||
|
}
|
||||||
expect(init?.method).toBe("POST");
|
expect(init?.method).toBe("POST");
|
||||||
const body = init?.body as URLSearchParams;
|
const body = init?.body as URLSearchParams;
|
||||||
expect(String(body.get("grant_type"))).toBe("refresh_token");
|
expect(String(body.get("grant_type"))).toBe("refresh_token");
|
||||||
|
|||||||
@@ -39,13 +39,17 @@ export function parseOAuthCallbackInput(
|
|||||||
expectedState: string,
|
expectedState: string,
|
||||||
): { code: string; state: string } | { error: string } {
|
): { code: string; state: string } | { error: string } {
|
||||||
const trimmed = input.trim();
|
const trimmed = input.trim();
|
||||||
if (!trimmed) return { error: "No input provided" };
|
if (!trimmed) {
|
||||||
|
return { error: "No input provided" };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL(trimmed);
|
const url = new URL(trimmed);
|
||||||
const code = url.searchParams.get("code");
|
const code = url.searchParams.get("code");
|
||||||
const state = url.searchParams.get("state");
|
const state = url.searchParams.get("state");
|
||||||
if (!code) return { error: "Missing 'code' parameter in URL" };
|
if (!code) {
|
||||||
|
return { error: "Missing 'code' parameter in URL" };
|
||||||
|
}
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return { error: "Missing 'state' parameter. Paste the full URL." };
|
return { error: "Missing 'state' parameter. Paste the full URL." };
|
||||||
}
|
}
|
||||||
@@ -71,9 +75,13 @@ export async function fetchChutesUserInfo(params: {
|
|||||||
const response = await fetchFn(CHUTES_USERINFO_ENDPOINT, {
|
const response = await fetchFn(CHUTES_USERINFO_ENDPOINT, {
|
||||||
headers: { Authorization: `Bearer ${params.accessToken}` },
|
headers: { Authorization: `Bearer ${params.accessToken}` },
|
||||||
});
|
});
|
||||||
if (!response.ok) return null;
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const data = (await response.json()) as unknown;
|
const data = (await response.json()) as unknown;
|
||||||
if (!data || typeof data !== "object") return null;
|
if (!data || typeof data !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const typed = data as ChutesUserInfo;
|
const typed = data as ChutesUserInfo;
|
||||||
return typed;
|
return typed;
|
||||||
}
|
}
|
||||||
@@ -119,7 +127,9 @@ export async function exchangeChutesCodeForTokens(params: {
|
|||||||
const refresh = data.refresh_token?.trim();
|
const refresh = data.refresh_token?.trim();
|
||||||
const expiresIn = data.expires_in ?? 0;
|
const expiresIn = data.expires_in ?? 0;
|
||||||
|
|
||||||
if (!access) throw new Error("Chutes token exchange returned no access_token");
|
if (!access) {
|
||||||
|
throw new Error("Chutes token exchange returned no access_token");
|
||||||
|
}
|
||||||
if (!refresh) {
|
if (!refresh) {
|
||||||
throw new Error("Chutes token exchange returned no refresh_token");
|
throw new Error("Chutes token exchange returned no refresh_token");
|
||||||
}
|
}
|
||||||
@@ -160,7 +170,9 @@ export async function refreshChutesTokens(params: {
|
|||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
});
|
});
|
||||||
if (clientSecret) body.set("client_secret", clientSecret);
|
if (clientSecret) {
|
||||||
|
body.set("client_secret", clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetchFn(CHUTES_TOKEN_ENDPOINT, {
|
const response = await fetchFn(CHUTES_TOKEN_ENDPOINT, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -181,7 +193,9 @@ export async function refreshChutesTokens(params: {
|
|||||||
const newRefresh = data.refresh_token?.trim();
|
const newRefresh = data.refresh_token?.trim();
|
||||||
const expiresIn = data.expires_in ?? 0;
|
const expiresIn = data.expires_in ?? 0;
|
||||||
|
|
||||||
if (!access) throw new Error("Chutes token refresh returned no access_token");
|
if (!access) {
|
||||||
|
throw new Error("Chutes token refresh returned no access_token");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...params.credential,
|
...params.credential,
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ function createDeferred<T>() {
|
|||||||
|
|
||||||
async function waitForCalls(mockFn: { mock: { calls: unknown[][] } }, count: number) {
|
async function waitForCalls(mockFn: { mock: { calls: unknown[][] } }, count: number) {
|
||||||
for (let i = 0; i < 50; i += 1) {
|
for (let i = 0; i < 50; i += 1) {
|
||||||
if (mockFn.mock.calls.length >= count) return;
|
if (mockFn.mock.calls.length >= count) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
}
|
}
|
||||||
throw new Error(`Expected ${count} calls, got ${mockFn.mock.calls.length}`);
|
throw new Error(`Expected ${count} calls, got ${mockFn.mock.calls.length}`);
|
||||||
|
|||||||
@@ -83,13 +83,17 @@ function pickBackendConfig(
|
|||||||
normalizedId: string,
|
normalizedId: string,
|
||||||
): CliBackendConfig | undefined {
|
): CliBackendConfig | undefined {
|
||||||
for (const [key, entry] of Object.entries(config)) {
|
for (const [key, entry] of Object.entries(config)) {
|
||||||
if (normalizeBackendKey(key) === normalizedId) return entry;
|
if (normalizeBackendKey(key) === normalizedId) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig): CliBackendConfig {
|
function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig): CliBackendConfig {
|
||||||
if (!override) return { ...base };
|
if (!override) {
|
||||||
|
return { ...base };
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
...override,
|
...override,
|
||||||
@@ -126,18 +130,26 @@ export function resolveCliBackendConfig(
|
|||||||
if (normalized === "claude-cli") {
|
if (normalized === "claude-cli") {
|
||||||
const merged = mergeBackendConfig(DEFAULT_CLAUDE_BACKEND, override);
|
const merged = mergeBackendConfig(DEFAULT_CLAUDE_BACKEND, override);
|
||||||
const command = merged.command?.trim();
|
const command = merged.command?.trim();
|
||||||
if (!command) return null;
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return { id: normalized, config: { ...merged, command } };
|
return { id: normalized, config: { ...merged, command } };
|
||||||
}
|
}
|
||||||
if (normalized === "codex-cli") {
|
if (normalized === "codex-cli") {
|
||||||
const merged = mergeBackendConfig(DEFAULT_CODEX_BACKEND, override);
|
const merged = mergeBackendConfig(DEFAULT_CODEX_BACKEND, override);
|
||||||
const command = merged.command?.trim();
|
const command = merged.command?.trim();
|
||||||
if (!command) return null;
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return { id: normalized, config: { ...merged, command } };
|
return { id: normalized, config: { ...merged, command } };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!override) return null;
|
if (!override) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const command = override.command?.trim();
|
const command = override.command?.trim();
|
||||||
if (!command) return null;
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return { id: normalized, config: { ...override, command } };
|
return { id: normalized, config: { ...override, command } };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ function readCodexKeychainCredentials(options?: {
|
|||||||
execSync?: ExecSyncFn;
|
execSync?: ExecSyncFn;
|
||||||
}): CodexCliCredential | null {
|
}): CodexCliCredential | null {
|
||||||
const platform = options?.platform ?? process.platform;
|
const platform = options?.platform ?? process.platform;
|
||||||
if (platform !== "darwin") return null;
|
if (platform !== "darwin") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const execSyncImpl = options?.execSync ?? execSync;
|
const execSyncImpl = options?.execSync ?? execSync;
|
||||||
|
|
||||||
const codexHome = resolveCodexHomePath();
|
const codexHome = resolveCodexHomePath();
|
||||||
@@ -132,8 +134,12 @@ function readCodexKeychainCredentials(options?: {
|
|||||||
const tokens = parsed.tokens as Record<string, unknown> | undefined;
|
const tokens = parsed.tokens as Record<string, unknown> | undefined;
|
||||||
const accessToken = tokens?.access_token;
|
const accessToken = tokens?.access_token;
|
||||||
const refreshToken = tokens?.refresh_token;
|
const refreshToken = tokens?.refresh_token;
|
||||||
if (typeof accessToken !== "string" || !accessToken) return null;
|
if (typeof accessToken !== "string" || !accessToken) {
|
||||||
if (typeof refreshToken !== "string" || !refreshToken) return null;
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof refreshToken !== "string" || !refreshToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// No explicit expiry stored; treat as fresh for an hour from last_refresh or now.
|
// No explicit expiry stored; treat as fresh for an hour from last_refresh or now.
|
||||||
const lastRefreshRaw = parsed.last_refresh;
|
const lastRefreshRaw = parsed.last_refresh;
|
||||||
@@ -167,15 +173,23 @@ function readCodexKeychainCredentials(options?: {
|
|||||||
function readQwenCliCredentials(options?: { homeDir?: string }): QwenCliCredential | null {
|
function readQwenCliCredentials(options?: { homeDir?: string }): QwenCliCredential | null {
|
||||||
const credPath = resolveQwenCliCredentialsPath(options?.homeDir);
|
const credPath = resolveQwenCliCredentialsPath(options?.homeDir);
|
||||||
const raw = loadJsonFile(credPath);
|
const raw = loadJsonFile(credPath);
|
||||||
if (!raw || typeof raw !== "object") return null;
|
if (!raw || typeof raw !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const data = raw as Record<string, unknown>;
|
const data = raw as Record<string, unknown>;
|
||||||
const accessToken = data.access_token;
|
const accessToken = data.access_token;
|
||||||
const refreshToken = data.refresh_token;
|
const refreshToken = data.refresh_token;
|
||||||
const expiresAt = data.expiry_date;
|
const expiresAt = data.expiry_date;
|
||||||
|
|
||||||
if (typeof accessToken !== "string" || !accessToken) return null;
|
if (typeof accessToken !== "string" || !accessToken) {
|
||||||
if (typeof refreshToken !== "string" || !refreshToken) return null;
|
return null;
|
||||||
if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return null;
|
}
|
||||||
|
if (typeof refreshToken !== "string" || !refreshToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
@@ -197,14 +211,20 @@ function readClaudeCliKeychainCredentials(
|
|||||||
|
|
||||||
const data = JSON.parse(result.trim());
|
const data = JSON.parse(result.trim());
|
||||||
const claudeOauth = data?.claudeAiOauth;
|
const claudeOauth = data?.claudeAiOauth;
|
||||||
if (!claudeOauth || typeof claudeOauth !== "object") return null;
|
if (!claudeOauth || typeof claudeOauth !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken = claudeOauth.accessToken;
|
const accessToken = claudeOauth.accessToken;
|
||||||
const refreshToken = claudeOauth.refreshToken;
|
const refreshToken = claudeOauth.refreshToken;
|
||||||
const expiresAt = claudeOauth.expiresAt;
|
const expiresAt = claudeOauth.expiresAt;
|
||||||
|
|
||||||
if (typeof accessToken !== "string" || !accessToken) return null;
|
if (typeof accessToken !== "string" || !accessToken) {
|
||||||
if (typeof expiresAt !== "number" || expiresAt <= 0) return null;
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof expiresAt !== "number" || expiresAt <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof refreshToken === "string" && refreshToken) {
|
if (typeof refreshToken === "string" && refreshToken) {
|
||||||
return {
|
return {
|
||||||
@@ -246,18 +266,26 @@ export function readClaudeCliCredentials(options?: {
|
|||||||
|
|
||||||
const credPath = resolveClaudeCliCredentialsPath(options?.homeDir);
|
const credPath = resolveClaudeCliCredentialsPath(options?.homeDir);
|
||||||
const raw = loadJsonFile(credPath);
|
const raw = loadJsonFile(credPath);
|
||||||
if (!raw || typeof raw !== "object") return null;
|
if (!raw || typeof raw !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const data = raw as Record<string, unknown>;
|
const data = raw as Record<string, unknown>;
|
||||||
const claudeOauth = data.claudeAiOauth as Record<string, unknown> | undefined;
|
const claudeOauth = data.claudeAiOauth as Record<string, unknown> | undefined;
|
||||||
if (!claudeOauth || typeof claudeOauth !== "object") return null;
|
if (!claudeOauth || typeof claudeOauth !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken = claudeOauth.accessToken;
|
const accessToken = claudeOauth.accessToken;
|
||||||
const refreshToken = claudeOauth.refreshToken;
|
const refreshToken = claudeOauth.refreshToken;
|
||||||
const expiresAt = claudeOauth.expiresAt;
|
const expiresAt = claudeOauth.expiresAt;
|
||||||
|
|
||||||
if (typeof accessToken !== "string" || !accessToken) return null;
|
if (typeof accessToken !== "string" || !accessToken) {
|
||||||
if (typeof expiresAt !== "number" || expiresAt <= 0) return null;
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof expiresAt !== "number" || expiresAt <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof refreshToken === "string" && refreshToken) {
|
if (typeof refreshToken === "string" && refreshToken) {
|
||||||
return {
|
return {
|
||||||
@@ -362,11 +390,15 @@ export function writeClaudeCliFileCredentials(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const raw = loadJsonFile(credPath);
|
const raw = loadJsonFile(credPath);
|
||||||
if (!raw || typeof raw !== "object") return false;
|
if (!raw || typeof raw !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const data = raw as Record<string, unknown>;
|
const data = raw as Record<string, unknown>;
|
||||||
const existingOauth = data.claudeAiOauth as Record<string, unknown> | undefined;
|
const existingOauth = data.claudeAiOauth as Record<string, unknown> | undefined;
|
||||||
if (!existingOauth || typeof existingOauth !== "object") return false;
|
if (!existingOauth || typeof existingOauth !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
data.claudeAiOauth = {
|
data.claudeAiOauth = {
|
||||||
...existingOauth,
|
...existingOauth,
|
||||||
@@ -416,21 +448,31 @@ export function readCodexCliCredentials(options?: {
|
|||||||
platform: options?.platform,
|
platform: options?.platform,
|
||||||
execSync: options?.execSync,
|
execSync: options?.execSync,
|
||||||
});
|
});
|
||||||
if (keychain) return keychain;
|
if (keychain) {
|
||||||
|
return keychain;
|
||||||
|
}
|
||||||
|
|
||||||
const authPath = resolveCodexCliAuthPath();
|
const authPath = resolveCodexCliAuthPath();
|
||||||
const raw = loadJsonFile(authPath);
|
const raw = loadJsonFile(authPath);
|
||||||
if (!raw || typeof raw !== "object") return null;
|
if (!raw || typeof raw !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const data = raw as Record<string, unknown>;
|
const data = raw as Record<string, unknown>;
|
||||||
const tokens = data.tokens as Record<string, unknown> | undefined;
|
const tokens = data.tokens as Record<string, unknown> | undefined;
|
||||||
if (!tokens || typeof tokens !== "object") return null;
|
if (!tokens || typeof tokens !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken = tokens.access_token;
|
const accessToken = tokens.access_token;
|
||||||
const refreshToken = tokens.refresh_token;
|
const refreshToken = tokens.refresh_token;
|
||||||
|
|
||||||
if (typeof accessToken !== "string" || !accessToken) return null;
|
if (typeof accessToken !== "string" || !accessToken) {
|
||||||
if (typeof refreshToken !== "string" || !refreshToken) return null;
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof refreshToken !== "string" || !refreshToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let expires: number;
|
let expires: number;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -228,12 +228,20 @@ export async function runCliAgent(params: {
|
|||||||
const stdout = result.stdout.trim();
|
const stdout = result.stdout.trim();
|
||||||
const stderr = result.stderr.trim();
|
const stderr = result.stderr.trim();
|
||||||
if (logOutputText) {
|
if (logOutputText) {
|
||||||
if (stdout) log.info(`cli stdout:\n${stdout}`);
|
if (stdout) {
|
||||||
if (stderr) log.info(`cli stderr:\n${stderr}`);
|
log.info(`cli stdout:\n${stdout}`);
|
||||||
|
}
|
||||||
|
if (stderr) {
|
||||||
|
log.info(`cli stderr:\n${stderr}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (shouldLogVerbose()) {
|
if (shouldLogVerbose()) {
|
||||||
if (stdout) log.debug(`cli stdout:\n${stdout}`);
|
if (stdout) {
|
||||||
if (stderr) log.debug(`cli stderr:\n${stderr}`);
|
log.debug(`cli stdout:\n${stdout}`);
|
||||||
|
}
|
||||||
|
if (stderr) {
|
||||||
|
log.debug(`cli stderr:\n${stderr}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.code !== 0) {
|
if (result.code !== 0) {
|
||||||
@@ -278,7 +286,9 @@ export async function runCliAgent(params: {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof FailoverError) throw err;
|
if (err instanceof FailoverError) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
if (isFailoverErrorMessage(message)) {
|
if (isFailoverErrorMessage(message)) {
|
||||||
const reason = classifyFailoverReason(message) ?? "unknown";
|
const reason = classifyFailoverReason(message) ?? "unknown";
|
||||||
|
|||||||
@@ -25,19 +25,29 @@ export async function cleanupResumeProcesses(
|
|||||||
backend: CliBackendConfig,
|
backend: CliBackendConfig,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (process.platform === "win32") return;
|
if (process.platform === "win32") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const resumeArgs = backend.resumeArgs ?? [];
|
const resumeArgs = backend.resumeArgs ?? [];
|
||||||
if (resumeArgs.length === 0) return;
|
if (resumeArgs.length === 0) {
|
||||||
if (!resumeArgs.some((arg) => arg.includes("{sessionId}"))) return;
|
return;
|
||||||
|
}
|
||||||
|
if (!resumeArgs.some((arg) => arg.includes("{sessionId}"))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const commandToken = path.basename(backend.command ?? "").trim();
|
const commandToken = path.basename(backend.command ?? "").trim();
|
||||||
if (!commandToken) return;
|
if (!commandToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const resumeTokens = resumeArgs.map((arg) => arg.replaceAll("{sessionId}", sessionId));
|
const resumeTokens = resumeArgs.map((arg) => arg.replaceAll("{sessionId}", sessionId));
|
||||||
const pattern = [commandToken, ...resumeTokens]
|
const pattern = [commandToken, ...resumeTokens]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((token) => escapeRegex(token))
|
.map((token) => escapeRegex(token))
|
||||||
.join(".*");
|
.join(".*");
|
||||||
if (!pattern) return;
|
if (!pattern) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await runExec("pkill", ["-f", pattern]);
|
await runExec("pkill", ["-f", pattern]);
|
||||||
@@ -48,14 +58,18 @@ export async function cleanupResumeProcesses(
|
|||||||
|
|
||||||
function buildSessionMatchers(backend: CliBackendConfig): RegExp[] {
|
function buildSessionMatchers(backend: CliBackendConfig): RegExp[] {
|
||||||
const commandToken = path.basename(backend.command ?? "").trim();
|
const commandToken = path.basename(backend.command ?? "").trim();
|
||||||
if (!commandToken) return [];
|
if (!commandToken) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const matchers: RegExp[] = [];
|
const matchers: RegExp[] = [];
|
||||||
const sessionArg = backend.sessionArg?.trim();
|
const sessionArg = backend.sessionArg?.trim();
|
||||||
const sessionArgs = backend.sessionArgs ?? [];
|
const sessionArgs = backend.sessionArgs ?? [];
|
||||||
const resumeArgs = backend.resumeArgs ?? [];
|
const resumeArgs = backend.resumeArgs ?? [];
|
||||||
|
|
||||||
const addMatcher = (args: string[]) => {
|
const addMatcher = (args: string[]) => {
|
||||||
if (args.length === 0) return;
|
if (args.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const tokens = [commandToken, ...args];
|
const tokens = [commandToken, ...args];
|
||||||
const pattern = tokens
|
const pattern = tokens
|
||||||
.map((token, index) => {
|
.map((token, index) => {
|
||||||
@@ -80,7 +94,9 @@ function buildSessionMatchers(backend: CliBackendConfig): RegExp[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tokenToRegex(token: string): string {
|
function tokenToRegex(token: string): string {
|
||||||
if (!token.includes("{sessionId}")) return escapeRegex(token);
|
if (!token.includes("{sessionId}")) {
|
||||||
|
return escapeRegex(token);
|
||||||
|
}
|
||||||
const parts = token.split("{sessionId}").map((part) => escapeRegex(part));
|
const parts = token.split("{sessionId}").map((part) => escapeRegex(part));
|
||||||
return parts.join("\\S+");
|
return parts.join("\\S+");
|
||||||
}
|
}
|
||||||
@@ -93,24 +109,38 @@ export async function cleanupSuspendedCliProcesses(
|
|||||||
backend: CliBackendConfig,
|
backend: CliBackendConfig,
|
||||||
threshold = 10,
|
threshold = 10,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (process.platform === "win32") return;
|
if (process.platform === "win32") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const matchers = buildSessionMatchers(backend);
|
const matchers = buildSessionMatchers(backend);
|
||||||
if (matchers.length === 0) return;
|
if (matchers.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout } = await runExec("ps", ["-ax", "-o", "pid=,stat=,command="]);
|
const { stdout } = await runExec("ps", ["-ax", "-o", "pid=,stat=,command="]);
|
||||||
const suspended: number[] = [];
|
const suspended: number[] = [];
|
||||||
for (const line of stdout.split("\n")) {
|
for (const line of stdout.split("\n")) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const match = /^(\d+)\s+(\S+)\s+(.*)$/.exec(trimmed);
|
const match = /^(\d+)\s+(\S+)\s+(.*)$/.exec(trimmed);
|
||||||
if (!match) continue;
|
if (!match) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const pid = Number(match[1]);
|
const pid = Number(match[1]);
|
||||||
const stat = match[2] ?? "";
|
const stat = match[2] ?? "";
|
||||||
const command = match[3] ?? "";
|
const command = match[3] ?? "";
|
||||||
if (!Number.isFinite(pid)) continue;
|
if (!Number.isFinite(pid)) {
|
||||||
if (!stat.includes("T")) continue;
|
continue;
|
||||||
if (!matchers.some((matcher) => matcher.test(command))) continue;
|
}
|
||||||
|
if (!stat.includes("T")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!matchers.some((matcher) => matcher.test(command))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
suspended.push(pid);
|
suspended.push(pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,9 +183,13 @@ function buildModelAliasLines(cfg?: OpenClawConfig) {
|
|||||||
const entries: Array<{ alias: string; model: string }> = [];
|
const entries: Array<{ alias: string; model: string }> = [];
|
||||||
for (const [keyRaw, entryRaw] of Object.entries(models)) {
|
for (const [keyRaw, entryRaw] of Object.entries(models)) {
|
||||||
const model = String(keyRaw ?? "").trim();
|
const model = String(keyRaw ?? "").trim();
|
||||||
if (!model) continue;
|
if (!model) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const alias = String((entryRaw as { alias?: string } | undefined)?.alias ?? "").trim();
|
const alias = String((entryRaw as { alias?: string } | undefined)?.alias ?? "").trim();
|
||||||
if (!alias) continue;
|
if (!alias) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
entries.push({ alias, model });
|
entries.push({ alias, model });
|
||||||
}
|
}
|
||||||
return entries
|
return entries
|
||||||
@@ -217,12 +251,18 @@ export function buildSystemPrompt(params: {
|
|||||||
|
|
||||||
export function normalizeCliModel(modelId: string, backend: CliBackendConfig): string {
|
export function normalizeCliModel(modelId: string, backend: CliBackendConfig): string {
|
||||||
const trimmed = modelId.trim();
|
const trimmed = modelId.trim();
|
||||||
if (!trimmed) return trimmed;
|
if (!trimmed) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
const direct = backend.modelAliases?.[trimmed];
|
const direct = backend.modelAliases?.[trimmed];
|
||||||
if (direct) return direct;
|
if (direct) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
const lower = trimmed.toLowerCase();
|
const lower = trimmed.toLowerCase();
|
||||||
const mapped = backend.modelAliases?.[lower];
|
const mapped = backend.modelAliases?.[lower];
|
||||||
if (mapped) return mapped;
|
if (mapped) {
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +275,9 @@ function toUsage(raw: Record<string, unknown>): CliUsage | undefined {
|
|||||||
pick("cache_read_input_tokens") ?? pick("cached_input_tokens") ?? pick("cacheRead");
|
pick("cache_read_input_tokens") ?? pick("cached_input_tokens") ?? pick("cacheRead");
|
||||||
const cacheWrite = pick("cache_write_input_tokens") ?? pick("cacheWrite");
|
const cacheWrite = pick("cache_write_input_tokens") ?? pick("cacheWrite");
|
||||||
const total = pick("total_tokens") ?? pick("total");
|
const total = pick("total_tokens") ?? pick("total");
|
||||||
if (!input && !output && !cacheRead && !cacheWrite && !total) return undefined;
|
if (!input && !output && !cacheRead && !cacheWrite && !total) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return { input, output, cacheRead, cacheWrite, total };
|
return { input, output, cacheRead, cacheWrite, total };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,15 +286,30 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function collectText(value: unknown): string {
|
function collectText(value: unknown): string {
|
||||||
if (!value) return "";
|
if (!value) {
|
||||||
if (typeof value === "string") return value;
|
return "";
|
||||||
if (Array.isArray(value)) return value.map((entry) => collectText(entry)).join("");
|
}
|
||||||
if (!isRecord(value)) return "";
|
if (typeof value === "string") {
|
||||||
if (typeof value.text === "string") return value.text;
|
return value;
|
||||||
if (typeof value.content === "string") return value.content;
|
}
|
||||||
if (Array.isArray(value.content))
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((entry) => collectText(entry)).join("");
|
||||||
|
}
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof value.text === "string") {
|
||||||
|
return value.text;
|
||||||
|
}
|
||||||
|
if (typeof value.content === "string") {
|
||||||
|
return value.content;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value.content)) {
|
||||||
return value.content.map((entry) => collectText(entry)).join("");
|
return value.content.map((entry) => collectText(entry)).join("");
|
||||||
if (isRecord(value.message)) return collectText(value.message);
|
}
|
||||||
|
if (isRecord(value.message)) {
|
||||||
|
return collectText(value.message);
|
||||||
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,21 +325,27 @@ function pickSessionId(
|
|||||||
];
|
];
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
const value = parsed[field];
|
const value = parsed[field];
|
||||||
if (typeof value === "string" && value.trim()) return value.trim();
|
if (typeof value === "string" && value.trim()) {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCliJson(raw: string, backend: CliBackendConfig): CliOutput | null {
|
export function parseCliJson(raw: string, backend: CliBackendConfig): CliOutput | null {
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
let parsed: unknown;
|
let parsed: unknown;
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(trimmed);
|
parsed = JSON.parse(trimmed);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!isRecord(parsed)) return null;
|
if (!isRecord(parsed)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const sessionId = pickSessionId(parsed, backend);
|
const sessionId = pickSessionId(parsed, backend);
|
||||||
const usage = isRecord(parsed.usage) ? toUsage(parsed.usage) : undefined;
|
const usage = isRecord(parsed.usage) ? toUsage(parsed.usage) : undefined;
|
||||||
const text =
|
const text =
|
||||||
@@ -298,7 +361,9 @@ export function parseCliJsonl(raw: string, backend: CliBackendConfig): CliOutput
|
|||||||
.split(/\r?\n/g)
|
.split(/\r?\n/g)
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
if (lines.length === 0) return null;
|
if (lines.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
let sessionId: string | undefined;
|
let sessionId: string | undefined;
|
||||||
let usage: CliUsage | undefined;
|
let usage: CliUsage | undefined;
|
||||||
const texts: string[] = [];
|
const texts: string[] = [];
|
||||||
@@ -309,8 +374,12 @@ export function parseCliJsonl(raw: string, backend: CliBackendConfig): CliOutput
|
|||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!isRecord(parsed)) continue;
|
if (!isRecord(parsed)) {
|
||||||
if (!sessionId) sessionId = pickSessionId(parsed, backend);
|
continue;
|
||||||
|
}
|
||||||
|
if (!sessionId) {
|
||||||
|
sessionId = pickSessionId(parsed, backend);
|
||||||
|
}
|
||||||
if (!sessionId && typeof parsed.thread_id === "string") {
|
if (!sessionId && typeof parsed.thread_id === "string") {
|
||||||
sessionId = parsed.thread_id.trim();
|
sessionId = parsed.thread_id.trim();
|
||||||
}
|
}
|
||||||
@@ -326,7 +395,9 @@ export function parseCliJsonl(raw: string, backend: CliBackendConfig): CliOutput
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const text = texts.join("\n").trim();
|
const text = texts.join("\n").trim();
|
||||||
if (!text) return null;
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return { text, sessionId, usage };
|
return { text, sessionId, usage };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,11 +407,19 @@ export function resolveSystemPromptUsage(params: {
|
|||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
}): string | null {
|
}): string | null {
|
||||||
const systemPrompt = params.systemPrompt?.trim();
|
const systemPrompt = params.systemPrompt?.trim();
|
||||||
if (!systemPrompt) return null;
|
if (!systemPrompt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const when = params.backend.systemPromptWhen ?? "first";
|
const when = params.backend.systemPromptWhen ?? "first";
|
||||||
if (when === "never") return null;
|
if (when === "never") {
|
||||||
if (when === "first" && !params.isNewSession) return null;
|
return null;
|
||||||
if (!params.backend.systemPromptArg?.trim()) return null;
|
}
|
||||||
|
if (when === "first" && !params.isNewSession) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!params.backend.systemPromptArg?.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return systemPrompt;
|
return systemPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,9 +429,15 @@ export function resolveSessionIdToSend(params: {
|
|||||||
}): { sessionId?: string; isNew: boolean } {
|
}): { sessionId?: string; isNew: boolean } {
|
||||||
const mode = params.backend.sessionMode ?? "always";
|
const mode = params.backend.sessionMode ?? "always";
|
||||||
const existing = params.cliSessionId?.trim();
|
const existing = params.cliSessionId?.trim();
|
||||||
if (mode === "none") return { sessionId: undefined, isNew: !existing };
|
if (mode === "none") {
|
||||||
if (mode === "existing") return { sessionId: existing, isNew: !existing };
|
return { sessionId: undefined, isNew: !existing };
|
||||||
if (existing) return { sessionId: existing, isNew: false };
|
}
|
||||||
|
if (mode === "existing") {
|
||||||
|
return { sessionId: existing, isNew: !existing };
|
||||||
|
}
|
||||||
|
if (existing) {
|
||||||
|
return { sessionId: existing, isNew: false };
|
||||||
|
}
|
||||||
return { sessionId: crypto.randomUUID(), isNew: true };
|
return { sessionId: crypto.randomUUID(), isNew: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,15 +457,25 @@ export function resolvePromptInput(params: { backend: CliBackendConfig; prompt:
|
|||||||
|
|
||||||
function resolveImageExtension(mimeType: string): string {
|
function resolveImageExtension(mimeType: string): string {
|
||||||
const normalized = mimeType.toLowerCase();
|
const normalized = mimeType.toLowerCase();
|
||||||
if (normalized.includes("png")) return "png";
|
if (normalized.includes("png")) {
|
||||||
if (normalized.includes("jpeg") || normalized.includes("jpg")) return "jpg";
|
return "png";
|
||||||
if (normalized.includes("gif")) return "gif";
|
}
|
||||||
if (normalized.includes("webp")) return "webp";
|
if (normalized.includes("jpeg") || normalized.includes("jpg")) {
|
||||||
|
return "jpg";
|
||||||
|
}
|
||||||
|
if (normalized.includes("gif")) {
|
||||||
|
return "gif";
|
||||||
|
}
|
||||||
|
if (normalized.includes("webp")) {
|
||||||
|
return "webp";
|
||||||
|
}
|
||||||
return "bin";
|
return "bin";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appendImagePathsToPrompt(prompt: string, paths: string[]): string {
|
export function appendImagePathsToPrompt(prompt: string, paths: string[]): string {
|
||||||
if (!paths.length) return prompt;
|
if (!paths.length) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
const trimmed = prompt.trimEnd();
|
const trimmed = prompt.trimEnd();
|
||||||
const separator = trimmed ? "\n\n" : "";
|
const separator = trimmed ? "\n\n" : "";
|
||||||
return `${trimmed}${separator}${paths.join("\n")}`;
|
return `${trimmed}${separator}${paths.join("\n")}`;
|
||||||
|
|||||||
@@ -5,13 +5,19 @@ export function getCliSessionId(
|
|||||||
entry: SessionEntry | undefined,
|
entry: SessionEntry | undefined,
|
||||||
provider: string,
|
provider: string,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
if (!entry) return undefined;
|
if (!entry) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const normalized = normalizeProviderId(provider);
|
const normalized = normalizeProviderId(provider);
|
||||||
const fromMap = entry.cliSessionIds?.[normalized];
|
const fromMap = entry.cliSessionIds?.[normalized];
|
||||||
if (fromMap?.trim()) return fromMap.trim();
|
if (fromMap?.trim()) {
|
||||||
|
return fromMap.trim();
|
||||||
|
}
|
||||||
if (normalized === "claude-cli") {
|
if (normalized === "claude-cli") {
|
||||||
const legacy = entry.claudeCliSessionId?.trim();
|
const legacy = entry.claudeCliSessionId?.trim();
|
||||||
if (legacy) return legacy;
|
if (legacy) {
|
||||||
|
return legacy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -19,7 +25,9 @@ export function getCliSessionId(
|
|||||||
export function setCliSessionId(entry: SessionEntry, provider: string, sessionId: string): void {
|
export function setCliSessionId(entry: SessionEntry, provider: string, sessionId: string): void {
|
||||||
const normalized = normalizeProviderId(provider);
|
const normalized = normalizeProviderId(provider);
|
||||||
const trimmed = sessionId.trim();
|
const trimmed = sessionId.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const existing = entry.cliSessionIds ?? {};
|
const existing = entry.cliSessionIds ?? {};
|
||||||
entry.cliSessionIds = { ...existing };
|
entry.cliSessionIds = { ...existing };
|
||||||
entry.cliSessionIds[normalized] = trimmed;
|
entry.cliSessionIds[normalized] = trimmed;
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ export function estimateMessagesTokens(messages: AgentMessage[]): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeParts(parts: number, messageCount: number): number {
|
function normalizeParts(parts: number, messageCount: number): number {
|
||||||
if (!Number.isFinite(parts) || parts <= 1) return 1;
|
if (!Number.isFinite(parts) || parts <= 1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
return Math.min(Math.max(1, Math.floor(parts)), Math.max(1, messageCount));
|
return Math.min(Math.max(1, Math.floor(parts)), Math.max(1, messageCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,9 +28,13 @@ export function splitMessagesByTokenShare(
|
|||||||
messages: AgentMessage[],
|
messages: AgentMessage[],
|
||||||
parts = DEFAULT_PARTS,
|
parts = DEFAULT_PARTS,
|
||||||
): AgentMessage[][] {
|
): AgentMessage[][] {
|
||||||
if (messages.length === 0) return [];
|
if (messages.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const normalizedParts = normalizeParts(parts, messages.length);
|
const normalizedParts = normalizeParts(parts, messages.length);
|
||||||
if (normalizedParts <= 1) return [messages];
|
if (normalizedParts <= 1) {
|
||||||
|
return [messages];
|
||||||
|
}
|
||||||
|
|
||||||
const totalTokens = estimateMessagesTokens(messages);
|
const totalTokens = estimateMessagesTokens(messages);
|
||||||
const targetTokens = totalTokens / normalizedParts;
|
const targetTokens = totalTokens / normalizedParts;
|
||||||
@@ -63,7 +69,9 @@ export function chunkMessagesByMaxTokens(
|
|||||||
messages: AgentMessage[],
|
messages: AgentMessage[],
|
||||||
maxTokens: number,
|
maxTokens: number,
|
||||||
): AgentMessage[][] {
|
): AgentMessage[][] {
|
||||||
if (messages.length === 0) return [];
|
if (messages.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const chunks: AgentMessage[][] = [];
|
const chunks: AgentMessage[][] = [];
|
||||||
let currentChunk: AgentMessage[] = [];
|
let currentChunk: AgentMessage[] = [];
|
||||||
@@ -100,7 +108,9 @@ export function chunkMessagesByMaxTokens(
|
|||||||
* When messages are large, we use smaller chunks to avoid exceeding model limits.
|
* When messages are large, we use smaller chunks to avoid exceeding model limits.
|
||||||
*/
|
*/
|
||||||
export function computeAdaptiveChunkRatio(messages: AgentMessage[], contextWindow: number): number {
|
export function computeAdaptiveChunkRatio(messages: AgentMessage[], contextWindow: number): number {
|
||||||
if (messages.length === 0) return BASE_CHUNK_RATIO;
|
if (messages.length === 0) {
|
||||||
|
return BASE_CHUNK_RATIO;
|
||||||
|
}
|
||||||
|
|
||||||
const totalTokens = estimateMessagesTokens(messages);
|
const totalTokens = estimateMessagesTokens(messages);
|
||||||
const avgTokens = totalTokens / messages.length;
|
const avgTokens = totalTokens / messages.length;
|
||||||
@@ -320,7 +330,9 @@ export function pruneHistoryForContextShare(params: {
|
|||||||
|
|
||||||
while (keptMessages.length > 0 && estimateMessagesTokens(keptMessages) > budgetTokens) {
|
while (keptMessages.length > 0 && estimateMessagesTokens(keptMessages) > budgetTokens) {
|
||||||
const chunks = splitMessagesByTokenShare(keptMessages, parts);
|
const chunks = splitMessagesByTokenShare(keptMessages, parts);
|
||||||
if (chunks.length <= 1) break;
|
if (chunks.length <= 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
const [dropped, ...rest] = chunks;
|
const [dropped, ...rest] = chunks;
|
||||||
droppedChunks += 1;
|
droppedChunks += 1;
|
||||||
droppedMessages += dropped.length;
|
droppedMessages += dropped.length;
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ export type ContextWindowInfo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function normalizePositiveInt(value: unknown): number | null {
|
function normalizePositiveInt(value: unknown): number | null {
|
||||||
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const int = Math.floor(value);
|
const int = Math.floor(value);
|
||||||
return int > 0 ? int : null;
|
return int > 0 ? int : null;
|
||||||
}
|
}
|
||||||
@@ -24,7 +26,9 @@ export function resolveContextWindowInfo(params: {
|
|||||||
defaultTokens: number;
|
defaultTokens: number;
|
||||||
}): ContextWindowInfo {
|
}): ContextWindowInfo {
|
||||||
const fromModel = normalizePositiveInt(params.modelContextWindow);
|
const fromModel = normalizePositiveInt(params.modelContextWindow);
|
||||||
if (fromModel) return { tokens: fromModel, source: "model" };
|
if (fromModel) {
|
||||||
|
return { tokens: fromModel, source: "model" };
|
||||||
|
}
|
||||||
|
|
||||||
const fromModelsConfig = (() => {
|
const fromModelsConfig = (() => {
|
||||||
const providers = params.cfg?.models?.providers as
|
const providers = params.cfg?.models?.providers as
|
||||||
@@ -35,10 +39,14 @@ export function resolveContextWindowInfo(params: {
|
|||||||
const match = models.find((m) => m?.id === params.modelId);
|
const match = models.find((m) => m?.id === params.modelId);
|
||||||
return normalizePositiveInt(match?.contextWindow);
|
return normalizePositiveInt(match?.contextWindow);
|
||||||
})();
|
})();
|
||||||
if (fromModelsConfig) return { tokens: fromModelsConfig, source: "modelsConfig" };
|
if (fromModelsConfig) {
|
||||||
|
return { tokens: fromModelsConfig, source: "modelsConfig" };
|
||||||
|
}
|
||||||
|
|
||||||
const fromAgentConfig = normalizePositiveInt(params.cfg?.agents?.defaults?.contextTokens);
|
const fromAgentConfig = normalizePositiveInt(params.cfg?.agents?.defaults?.contextTokens);
|
||||||
if (fromAgentConfig) return { tokens: fromAgentConfig, source: "agentContextTokens" };
|
if (fromAgentConfig) {
|
||||||
|
return { tokens: fromAgentConfig, source: "agentContextTokens" };
|
||||||
|
}
|
||||||
|
|
||||||
return { tokens: Math.floor(params.defaultTokens), source: "default" };
|
return { tokens: Math.floor(params.defaultTokens), source: "default" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ const loadPromise = (async () => {
|
|||||||
const modelRegistry = discoverModels(authStorage, agentDir);
|
const modelRegistry = discoverModels(authStorage, agentDir);
|
||||||
const models = modelRegistry.getAll() as ModelEntry[];
|
const models = modelRegistry.getAll() as ModelEntry[];
|
||||||
for (const m of models) {
|
for (const m of models) {
|
||||||
if (!m?.id) continue;
|
if (!m?.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (typeof m.contextWindow === "number" && m.contextWindow > 0) {
|
if (typeof m.contextWindow === "number" && m.contextWindow > 0) {
|
||||||
MODEL_CACHE.set(m.id, m.contextWindow);
|
MODEL_CACHE.set(m.id, m.contextWindow);
|
||||||
}
|
}
|
||||||
@@ -29,7 +31,9 @@ const loadPromise = (async () => {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
export function lookupContextTokens(modelId?: string): number | undefined {
|
export function lookupContextTokens(modelId?: string): number | undefined {
|
||||||
if (!modelId) return undefined;
|
if (!modelId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
// Best-effort: kick off loading, but don't block.
|
// Best-effort: kick off loading, but don't block.
|
||||||
void loadPromise;
|
void loadPromise;
|
||||||
return MODEL_CACHE.get(modelId);
|
return MODEL_CACHE.get(modelId);
|
||||||
|
|||||||
@@ -20,8 +20,12 @@ export function resolveUserTimezone(configured?: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveUserTimeFormat(preference?: TimeFormatPreference): ResolvedTimeFormat {
|
export function resolveUserTimeFormat(preference?: TimeFormatPreference): ResolvedTimeFormat {
|
||||||
if (preference === "12" || preference === "24") return preference;
|
if (preference === "12" || preference === "24") {
|
||||||
if (cachedTimeFormat) return cachedTimeFormat;
|
return preference;
|
||||||
|
}
|
||||||
|
if (cachedTimeFormat) {
|
||||||
|
return cachedTimeFormat;
|
||||||
|
}
|
||||||
cachedTimeFormat = detectSystemTimeFormat() ? "24" : "12";
|
cachedTimeFormat = detectSystemTimeFormat() ? "24" : "12";
|
||||||
return cachedTimeFormat;
|
return cachedTimeFormat;
|
||||||
}
|
}
|
||||||
@@ -29,7 +33,9 @@ export function resolveUserTimeFormat(preference?: TimeFormatPreference): Resolv
|
|||||||
export function normalizeTimestamp(
|
export function normalizeTimestamp(
|
||||||
raw: unknown,
|
raw: unknown,
|
||||||
): { timestampMs: number; timestampUtc: string } | undefined {
|
): { timestampMs: number; timestampUtc: string } | undefined {
|
||||||
if (raw == null) return undefined;
|
if (raw == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
let timestampMs: number | undefined;
|
let timestampMs: number | undefined;
|
||||||
|
|
||||||
if (raw instanceof Date) {
|
if (raw instanceof Date) {
|
||||||
@@ -38,7 +44,9 @@ export function normalizeTimestamp(
|
|||||||
timestampMs = raw < 1_000_000_000_000 ? Math.round(raw * 1000) : Math.round(raw);
|
timestampMs = raw < 1_000_000_000_000 ? Math.round(raw * 1000) : Math.round(raw);
|
||||||
} else if (typeof raw === "string") {
|
} else if (typeof raw === "string") {
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return undefined;
|
if (!trimmed) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
||||||
const num = Number(trimmed);
|
const num = Number(trimmed);
|
||||||
if (Number.isFinite(num)) {
|
if (Number.isFinite(num)) {
|
||||||
@@ -52,11 +60,15 @@ export function normalizeTimestamp(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const parsed = Date.parse(trimmed);
|
const parsed = Date.parse(trimmed);
|
||||||
if (!Number.isNaN(parsed)) timestampMs = parsed;
|
if (!Number.isNaN(parsed)) {
|
||||||
|
timestampMs = parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timestampMs === undefined || !Number.isFinite(timestampMs)) return undefined;
|
if (timestampMs === undefined || !Number.isFinite(timestampMs)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return { timestampMs, timestampUtc: new Date(timestampMs).toISOString() };
|
return { timestampMs, timestampUtc: new Date(timestampMs).toISOString() };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +77,9 @@ export function withNormalizedTimestamp<T extends Record<string, unknown>>(
|
|||||||
rawTimestamp: unknown,
|
rawTimestamp: unknown,
|
||||||
): T & { timestampMs?: number; timestampUtc?: string } {
|
): T & { timestampMs?: number; timestampUtc?: string } {
|
||||||
const normalized = normalizeTimestamp(rawTimestamp);
|
const normalized = normalizeTimestamp(rawTimestamp);
|
||||||
if (!normalized) return value;
|
if (!normalized) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...value,
|
...value,
|
||||||
timestampMs:
|
timestampMs:
|
||||||
@@ -86,8 +100,12 @@ function detectSystemTimeFormat(): boolean {
|
|||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
timeout: 500,
|
timeout: 500,
|
||||||
}).trim();
|
}).trim();
|
||||||
if (result === "1") return true;
|
if (result === "1") {
|
||||||
if (result === "0") return false;
|
return true;
|
||||||
|
}
|
||||||
|
if (result === "0") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Not set, fall through
|
// Not set, fall through
|
||||||
}
|
}
|
||||||
@@ -99,8 +117,12 @@ function detectSystemTimeFormat(): boolean {
|
|||||||
'powershell -Command "(Get-Culture).DateTimeFormat.ShortTimePattern"',
|
'powershell -Command "(Get-Culture).DateTimeFormat.ShortTimePattern"',
|
||||||
{ encoding: "utf8", timeout: 1000 },
|
{ encoding: "utf8", timeout: 1000 },
|
||||||
).trim();
|
).trim();
|
||||||
if (result.startsWith("H")) return true;
|
if (result.startsWith("H")) {
|
||||||
if (result.startsWith("h")) return false;
|
return true;
|
||||||
|
}
|
||||||
|
if (result.startsWith("h")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fall through
|
// Fall through
|
||||||
}
|
}
|
||||||
@@ -116,7 +138,9 @@ function detectSystemTimeFormat(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ordinalSuffix(day: number): string {
|
function ordinalSuffix(day: number): string {
|
||||||
if (day >= 11 && day <= 13) return "th";
|
if (day >= 11 && day <= 13) {
|
||||||
|
return "th";
|
||||||
|
}
|
||||||
switch (day % 10) {
|
switch (day % 10) {
|
||||||
case 1:
|
case 1:
|
||||||
return "st";
|
return "st";
|
||||||
@@ -148,10 +172,13 @@ export function formatUserTime(
|
|||||||
}).formatToParts(date);
|
}).formatToParts(date);
|
||||||
const map: Record<string, string> = {};
|
const map: Record<string, string> = {};
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
if (part.type !== "literal") map[part.type] = part.value;
|
if (part.type !== "literal") {
|
||||||
|
map[part.type] = part.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!map.weekday || !map.year || !map.month || !map.day || !map.hour || !map.minute)
|
if (!map.weekday || !map.year || !map.month || !map.day || !map.hour || !map.minute) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
}
|
||||||
const dayNum = parseInt(map.day, 10);
|
const dayNum = parseInt(map.day, 10);
|
||||||
const suffix = ordinalSuffix(dayNum);
|
const suffix = ordinalSuffix(dayNum);
|
||||||
const timePart = use24Hour
|
const timePart = use24Hour
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ export async function resolveOpenClawDocsPath(params: {
|
|||||||
const workspaceDir = params.workspaceDir?.trim();
|
const workspaceDir = params.workspaceDir?.trim();
|
||||||
if (workspaceDir) {
|
if (workspaceDir) {
|
||||||
const workspaceDocs = path.join(workspaceDir, "docs");
|
const workspaceDocs = path.join(workspaceDir, "docs");
|
||||||
if (fs.existsSync(workspaceDocs)) return workspaceDocs;
|
if (fs.existsSync(workspaceDocs)) {
|
||||||
|
return workspaceDocs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageRoot = await resolveOpenClawPackageRoot({
|
const packageRoot = await resolveOpenClawPackageRoot({
|
||||||
@@ -20,7 +22,9 @@ export async function resolveOpenClawDocsPath(params: {
|
|||||||
argv1: params.argv1,
|
argv1: params.argv1,
|
||||||
moduleUrl: params.moduleUrl,
|
moduleUrl: params.moduleUrl,
|
||||||
});
|
});
|
||||||
if (!packageRoot) return null;
|
if (!packageRoot) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const packageDocs = path.join(packageRoot, "docs");
|
const packageDocs = path.join(packageRoot, "docs");
|
||||||
return fs.existsSync(packageDocs) ? packageDocs : null;
|
return fs.existsSync(packageDocs) ? packageDocs : null;
|
||||||
|
|||||||
@@ -56,11 +56,15 @@ export function resolveFailoverStatus(reason: FailoverReason): number | undefine
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getStatusCode(err: unknown): number | undefined {
|
function getStatusCode(err: unknown): number | undefined {
|
||||||
if (!err || typeof err !== "object") return undefined;
|
if (!err || typeof err !== "object") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const candidate =
|
const candidate =
|
||||||
(err as { status?: unknown; statusCode?: unknown }).status ??
|
(err as { status?: unknown; statusCode?: unknown }).status ??
|
||||||
(err as { statusCode?: unknown }).statusCode;
|
(err as { statusCode?: unknown }).statusCode;
|
||||||
if (typeof candidate === "number") return candidate;
|
if (typeof candidate === "number") {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
if (typeof candidate === "string" && /^\d+$/.test(candidate)) {
|
if (typeof candidate === "string" && /^\d+$/.test(candidate)) {
|
||||||
return Number(candidate);
|
return Number(candidate);
|
||||||
}
|
}
|
||||||
@@ -68,67 +72,107 @@ function getStatusCode(err: unknown): number | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getErrorName(err: unknown): string {
|
function getErrorName(err: unknown): string {
|
||||||
if (!err || typeof err !== "object") return "";
|
if (!err || typeof err !== "object") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
return "name" in err ? String(err.name) : "";
|
return "name" in err ? String(err.name) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getErrorCode(err: unknown): string | undefined {
|
function getErrorCode(err: unknown): string | undefined {
|
||||||
if (!err || typeof err !== "object") return undefined;
|
if (!err || typeof err !== "object") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const candidate = (err as { code?: unknown }).code;
|
const candidate = (err as { code?: unknown }).code;
|
||||||
if (typeof candidate !== "string") return undefined;
|
if (typeof candidate !== "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const trimmed = candidate.trim();
|
const trimmed = candidate.trim();
|
||||||
return trimmed ? trimmed : undefined;
|
return trimmed ? trimmed : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getErrorMessage(err: unknown): string {
|
function getErrorMessage(err: unknown): string {
|
||||||
if (err instanceof Error) return err.message;
|
if (err instanceof Error) {
|
||||||
if (typeof err === "string") return err;
|
return err.message;
|
||||||
|
}
|
||||||
|
if (typeof err === "string") {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") {
|
if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") {
|
||||||
return String(err);
|
return String(err);
|
||||||
}
|
}
|
||||||
if (typeof err === "symbol") return err.description ?? "";
|
if (typeof err === "symbol") {
|
||||||
|
return err.description ?? "";
|
||||||
|
}
|
||||||
if (err && typeof err === "object") {
|
if (err && typeof err === "object") {
|
||||||
const message = (err as { message?: unknown }).message;
|
const message = (err as { message?: unknown }).message;
|
||||||
if (typeof message === "string") return message;
|
if (typeof message === "string") {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasTimeoutHint(err: unknown): boolean {
|
function hasTimeoutHint(err: unknown): boolean {
|
||||||
if (!err) return false;
|
if (!err) {
|
||||||
if (getErrorName(err) === "TimeoutError") return true;
|
return false;
|
||||||
|
}
|
||||||
|
if (getErrorName(err) === "TimeoutError") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const message = getErrorMessage(err);
|
const message = getErrorMessage(err);
|
||||||
return Boolean(message && TIMEOUT_HINT_RE.test(message));
|
return Boolean(message && TIMEOUT_HINT_RE.test(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTimeoutError(err: unknown): boolean {
|
export function isTimeoutError(err: unknown): boolean {
|
||||||
if (hasTimeoutHint(err)) return true;
|
if (hasTimeoutHint(err)) {
|
||||||
if (!err || typeof err !== "object") return false;
|
return true;
|
||||||
if (getErrorName(err) !== "AbortError") return false;
|
}
|
||||||
|
if (!err || typeof err !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getErrorName(err) !== "AbortError") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const message = getErrorMessage(err);
|
const message = getErrorMessage(err);
|
||||||
if (message && ABORT_TIMEOUT_RE.test(message)) return true;
|
if (message && ABORT_TIMEOUT_RE.test(message)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const cause = "cause" in err ? (err as { cause?: unknown }).cause : undefined;
|
const cause = "cause" in err ? (err as { cause?: unknown }).cause : undefined;
|
||||||
const reason = "reason" in err ? (err as { reason?: unknown }).reason : undefined;
|
const reason = "reason" in err ? (err as { reason?: unknown }).reason : undefined;
|
||||||
return hasTimeoutHint(cause) || hasTimeoutHint(reason);
|
return hasTimeoutHint(cause) || hasTimeoutHint(reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveFailoverReasonFromError(err: unknown): FailoverReason | null {
|
export function resolveFailoverReasonFromError(err: unknown): FailoverReason | null {
|
||||||
if (isFailoverError(err)) return err.reason;
|
if (isFailoverError(err)) {
|
||||||
|
return err.reason;
|
||||||
|
}
|
||||||
|
|
||||||
const status = getStatusCode(err);
|
const status = getStatusCode(err);
|
||||||
if (status === 402) return "billing";
|
if (status === 402) {
|
||||||
if (status === 429) return "rate_limit";
|
return "billing";
|
||||||
if (status === 401 || status === 403) return "auth";
|
}
|
||||||
if (status === 408) return "timeout";
|
if (status === 429) {
|
||||||
|
return "rate_limit";
|
||||||
|
}
|
||||||
|
if (status === 401 || status === 403) {
|
||||||
|
return "auth";
|
||||||
|
}
|
||||||
|
if (status === 408) {
|
||||||
|
return "timeout";
|
||||||
|
}
|
||||||
|
|
||||||
const code = (getErrorCode(err) ?? "").toUpperCase();
|
const code = (getErrorCode(err) ?? "").toUpperCase();
|
||||||
if (["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "ECONNABORTED"].includes(code)) {
|
if (["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "ECONNABORTED"].includes(code)) {
|
||||||
return "timeout";
|
return "timeout";
|
||||||
}
|
}
|
||||||
if (isTimeoutError(err)) return "timeout";
|
if (isTimeoutError(err)) {
|
||||||
|
return "timeout";
|
||||||
|
}
|
||||||
|
|
||||||
const message = getErrorMessage(err);
|
const message = getErrorMessage(err);
|
||||||
if (!message) return null;
|
if (!message) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return classifyFailoverReason(message);
|
return classifyFailoverReason(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,9 +207,13 @@ export function coerceToFailoverError(
|
|||||||
profileId?: string;
|
profileId?: string;
|
||||||
},
|
},
|
||||||
): FailoverError | null {
|
): FailoverError | null {
|
||||||
if (isFailoverError(err)) return err;
|
if (isFailoverError(err)) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
const reason = resolveFailoverReasonFromError(err);
|
const reason = resolveFailoverReasonFromError(err);
|
||||||
if (!reason) return null;
|
if (!reason) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const message = getErrorMessage(err) || String(err);
|
const message = getErrorMessage(err) || String(err);
|
||||||
const status = getStatusCode(err) ?? resolveFailoverStatus(reason);
|
const status = getStatusCode(err) ?? resolveFailoverStatus(reason);
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ function normalizeAvatarValue(value: string | undefined | null): string | null {
|
|||||||
|
|
||||||
function resolveAvatarSource(cfg: OpenClawConfig, agentId: string): string | null {
|
function resolveAvatarSource(cfg: OpenClawConfig, agentId: string): string | null {
|
||||||
const fromConfig = normalizeAvatarValue(resolveAgentIdentity(cfg, agentId)?.avatar);
|
const fromConfig = normalizeAvatarValue(resolveAgentIdentity(cfg, agentId)?.avatar);
|
||||||
if (fromConfig) return fromConfig;
|
if (fromConfig) {
|
||||||
|
return fromConfig;
|
||||||
|
}
|
||||||
const workspace = resolveAgentWorkspaceDir(cfg, agentId);
|
const workspace = resolveAgentWorkspaceDir(cfg, agentId);
|
||||||
const fromIdentity = normalizeAvatarValue(loadAgentIdentityFromWorkspace(workspace)?.avatar);
|
const fromIdentity = normalizeAvatarValue(loadAgentIdentityFromWorkspace(workspace)?.avatar);
|
||||||
return fromIdentity;
|
return fromIdentity;
|
||||||
@@ -47,7 +49,9 @@ function resolveExistingPath(value: string): string {
|
|||||||
|
|
||||||
function isPathWithin(root: string, target: string): boolean {
|
function isPathWithin(root: string, target: string): boolean {
|
||||||
const relative = path.relative(root, target);
|
const relative = path.relative(root, target);
|
||||||
if (!relative) return true;
|
if (!relative) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,20 +42,38 @@ export function parseIdentityMarkdown(content: string): AgentIdentityFile {
|
|||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const cleaned = line.trim().replace(/^\s*-\s*/, "");
|
const cleaned = line.trim().replace(/^\s*-\s*/, "");
|
||||||
const colonIndex = cleaned.indexOf(":");
|
const colonIndex = cleaned.indexOf(":");
|
||||||
if (colonIndex === -1) continue;
|
if (colonIndex === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const label = cleaned.slice(0, colonIndex).replace(/[*_]/g, "").trim().toLowerCase();
|
const label = cleaned.slice(0, colonIndex).replace(/[*_]/g, "").trim().toLowerCase();
|
||||||
const value = cleaned
|
const value = cleaned
|
||||||
.slice(colonIndex + 1)
|
.slice(colonIndex + 1)
|
||||||
.replace(/^[*_]+|[*_]+$/g, "")
|
.replace(/^[*_]+|[*_]+$/g, "")
|
||||||
.trim();
|
.trim();
|
||||||
if (!value) continue;
|
if (!value) {
|
||||||
if (isIdentityPlaceholder(value)) continue;
|
continue;
|
||||||
if (label === "name") identity.name = value;
|
}
|
||||||
if (label === "emoji") identity.emoji = value;
|
if (isIdentityPlaceholder(value)) {
|
||||||
if (label === "creature") identity.creature = value;
|
continue;
|
||||||
if (label === "vibe") identity.vibe = value;
|
}
|
||||||
if (label === "theme") identity.theme = value;
|
if (label === "name") {
|
||||||
if (label === "avatar") identity.avatar = value;
|
identity.name = value;
|
||||||
|
}
|
||||||
|
if (label === "emoji") {
|
||||||
|
identity.emoji = value;
|
||||||
|
}
|
||||||
|
if (label === "creature") {
|
||||||
|
identity.creature = value;
|
||||||
|
}
|
||||||
|
if (label === "vibe") {
|
||||||
|
identity.vibe = value;
|
||||||
|
}
|
||||||
|
if (label === "theme") {
|
||||||
|
identity.theme = value;
|
||||||
|
}
|
||||||
|
if (label === "avatar") {
|
||||||
|
identity.avatar = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
@@ -75,7 +93,9 @@ export function loadIdentityFromFile(identityPath: string): AgentIdentityFile |
|
|||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(identityPath, "utf-8");
|
const content = fs.readFileSync(identityPath, "utf-8");
|
||||||
const parsed = parseIdentityMarkdown(content);
|
const parsed = parseIdentityMarkdown(content);
|
||||||
if (!identityHasValues(parsed)) return null;
|
if (!identityHasValues(parsed)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ export function resolveAgentIdentity(
|
|||||||
|
|
||||||
export function resolveAckReaction(cfg: OpenClawConfig, agentId: string): string {
|
export function resolveAckReaction(cfg: OpenClawConfig, agentId: string): string {
|
||||||
const configured = cfg.messages?.ackReaction;
|
const configured = cfg.messages?.ackReaction;
|
||||||
if (configured !== undefined) return configured.trim();
|
if (configured !== undefined) {
|
||||||
|
return configured.trim();
|
||||||
|
}
|
||||||
const emoji = resolveAgentIdentity(cfg, agentId)?.emoji?.trim();
|
const emoji = resolveAgentIdentity(cfg, agentId)?.emoji?.trim();
|
||||||
return emoji || DEFAULT_ACK_REACTION;
|
return emoji || DEFAULT_ACK_REACTION;
|
||||||
}
|
}
|
||||||
@@ -22,7 +24,9 @@ export function resolveIdentityNamePrefix(
|
|||||||
agentId: string,
|
agentId: string,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const name = resolveAgentIdentity(cfg, agentId)?.name?.trim();
|
const name = resolveAgentIdentity(cfg, agentId)?.name?.trim();
|
||||||
if (!name) return undefined;
|
if (!name) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return `[${name}]`;
|
return `[${name}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,10 +41,14 @@ export function resolveMessagePrefix(
|
|||||||
opts?: { configured?: string; hasAllowFrom?: boolean; fallback?: string },
|
opts?: { configured?: string; hasAllowFrom?: boolean; fallback?: string },
|
||||||
): string {
|
): string {
|
||||||
const configured = opts?.configured ?? cfg.messages?.messagePrefix;
|
const configured = opts?.configured ?? cfg.messages?.messagePrefix;
|
||||||
if (configured !== undefined) return configured;
|
if (configured !== undefined) {
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
|
||||||
const hasAllowFrom = opts?.hasAllowFrom === true;
|
const hasAllowFrom = opts?.hasAllowFrom === true;
|
||||||
if (hasAllowFrom) return "";
|
if (hasAllowFrom) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
return resolveIdentityNamePrefix(cfg, agentId) ?? opts?.fallback ?? "[openclaw]";
|
return resolveIdentityNamePrefix(cfg, agentId) ?? opts?.fallback ?? "[openclaw]";
|
||||||
}
|
}
|
||||||
@@ -76,7 +84,9 @@ export function resolveHumanDelayConfig(
|
|||||||
): HumanDelayConfig | undefined {
|
): HumanDelayConfig | undefined {
|
||||||
const defaults = cfg.agents?.defaults?.humanDelay;
|
const defaults = cfg.agents?.defaults?.humanDelay;
|
||||||
const overrides = resolveAgentConfig(cfg, agentId)?.humanDelay;
|
const overrides = resolveAgentConfig(cfg, agentId)?.humanDelay;
|
||||||
if (!defaults && !overrides) return undefined;
|
if (!defaults && !overrides) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
mode: overrides?.mode ?? defaults?.mode,
|
mode: overrides?.mode ?? defaults?.mode,
|
||||||
minMs: overrides?.minMs ?? defaults?.minMs,
|
minMs: overrides?.minMs ?? defaults?.minMs,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
const KEY_SPLIT_RE = /[\s,;]+/g;
|
const KEY_SPLIT_RE = /[\s,;]+/g;
|
||||||
|
|
||||||
function parseKeyList(raw?: string | null): string[] {
|
function parseKeyList(raw?: string | null): string[] {
|
||||||
if (!raw) return [];
|
if (!raw) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return raw
|
return raw
|
||||||
.split(KEY_SPLIT_RE)
|
.split(KEY_SPLIT_RE)
|
||||||
.map((value) => value.trim())
|
.map((value) => value.trim())
|
||||||
@@ -11,9 +13,13 @@ function parseKeyList(raw?: string | null): string[] {
|
|||||||
function collectEnvPrefixedKeys(prefix: string): string[] {
|
function collectEnvPrefixedKeys(prefix: string): string[] {
|
||||||
const keys: string[] = [];
|
const keys: string[] = [];
|
||||||
for (const [name, value] of Object.entries(process.env)) {
|
for (const [name, value] of Object.entries(process.env)) {
|
||||||
if (!name.startsWith(prefix)) continue;
|
if (!name.startsWith(prefix)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const trimmed = value?.trim();
|
const trimmed = value?.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
keys.push(trimmed);
|
keys.push(trimmed);
|
||||||
}
|
}
|
||||||
return keys;
|
return keys;
|
||||||
@@ -21,7 +27,9 @@ function collectEnvPrefixedKeys(prefix: string): string[] {
|
|||||||
|
|
||||||
export function collectAnthropicApiKeys(): string[] {
|
export function collectAnthropicApiKeys(): string[] {
|
||||||
const forcedSingle = process.env.OPENCLAW_LIVE_ANTHROPIC_KEY?.trim();
|
const forcedSingle = process.env.OPENCLAW_LIVE_ANTHROPIC_KEY?.trim();
|
||||||
if (forcedSingle) return [forcedSingle];
|
if (forcedSingle) {
|
||||||
|
return [forcedSingle];
|
||||||
|
}
|
||||||
|
|
||||||
const fromList = parseKeyList(process.env.OPENCLAW_LIVE_ANTHROPIC_KEYS);
|
const fromList = parseKeyList(process.env.OPENCLAW_LIVE_ANTHROPIC_KEYS);
|
||||||
const fromEnv = collectEnvPrefixedKeys("ANTHROPIC_API_KEY");
|
const fromEnv = collectEnvPrefixedKeys("ANTHROPIC_API_KEY");
|
||||||
@@ -29,33 +37,61 @@ export function collectAnthropicApiKeys(): string[] {
|
|||||||
|
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const add = (value?: string) => {
|
const add = (value?: string) => {
|
||||||
if (!value) return;
|
if (!value) {
|
||||||
if (seen.has(value)) return;
|
return;
|
||||||
|
}
|
||||||
|
if (seen.has(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
seen.add(value);
|
seen.add(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const value of fromList) add(value);
|
for (const value of fromList) {
|
||||||
if (primary) add(primary);
|
add(value);
|
||||||
for (const value of fromEnv) add(value);
|
}
|
||||||
|
if (primary) {
|
||||||
|
add(primary);
|
||||||
|
}
|
||||||
|
for (const value of fromEnv) {
|
||||||
|
add(value);
|
||||||
|
}
|
||||||
|
|
||||||
return Array.from(seen);
|
return Array.from(seen);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAnthropicRateLimitError(message: string): boolean {
|
export function isAnthropicRateLimitError(message: string): boolean {
|
||||||
const lower = message.toLowerCase();
|
const lower = message.toLowerCase();
|
||||||
if (lower.includes("rate_limit")) return true;
|
if (lower.includes("rate_limit")) {
|
||||||
if (lower.includes("rate limit")) return true;
|
return true;
|
||||||
if (lower.includes("429")) return true;
|
}
|
||||||
|
if (lower.includes("rate limit")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (lower.includes("429")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAnthropicBillingError(message: string): boolean {
|
export function isAnthropicBillingError(message: string): boolean {
|
||||||
const lower = message.toLowerCase();
|
const lower = message.toLowerCase();
|
||||||
if (lower.includes("credit balance")) return true;
|
if (lower.includes("credit balance")) {
|
||||||
if (lower.includes("insufficient credit")) return true;
|
return true;
|
||||||
if (lower.includes("insufficient credits")) return true;
|
}
|
||||||
if (lower.includes("payment required")) return true;
|
if (lower.includes("insufficient credit")) {
|
||||||
if (lower.includes("billing") && lower.includes("disabled")) return true;
|
return true;
|
||||||
if (lower.includes("402")) return true;
|
}
|
||||||
|
if (lower.includes("insufficient credits")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (lower.includes("payment required")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (lower.includes("billing") && lower.includes("disabled")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (lower.includes("402")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ function matchesAny(id: string, values: string[]): boolean {
|
|||||||
export function isModernModelRef(ref: ModelRef): boolean {
|
export function isModernModelRef(ref: ModelRef): boolean {
|
||||||
const provider = ref.provider?.trim().toLowerCase() ?? "";
|
const provider = ref.provider?.trim().toLowerCase() ?? "";
|
||||||
const id = ref.id?.trim().toLowerCase() ?? "";
|
const id = ref.id?.trim().toLowerCase() ?? "";
|
||||||
if (!provider || !id) return false;
|
if (!provider || !id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (provider === "anthropic") {
|
if (provider === "anthropic") {
|
||||||
return matchesPrefix(id, ANTHROPIC_PREFIXES);
|
return matchesPrefix(id, ANTHROPIC_PREFIXES);
|
||||||
|
|||||||
@@ -94,17 +94,25 @@ function normalizeSources(
|
|||||||
const normalized = new Set<"memory" | "sessions">();
|
const normalized = new Set<"memory" | "sessions">();
|
||||||
const input = sources?.length ? sources : DEFAULT_SOURCES;
|
const input = sources?.length ? sources : DEFAULT_SOURCES;
|
||||||
for (const source of input) {
|
for (const source of input) {
|
||||||
if (source === "memory") normalized.add("memory");
|
if (source === "memory") {
|
||||||
if (source === "sessions" && sessionMemoryEnabled) normalized.add("sessions");
|
normalized.add("memory");
|
||||||
|
}
|
||||||
|
if (source === "sessions" && sessionMemoryEnabled) {
|
||||||
|
normalized.add("sessions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (normalized.size === 0) {
|
||||||
|
normalized.add("memory");
|
||||||
}
|
}
|
||||||
if (normalized.size === 0) normalized.add("memory");
|
|
||||||
return Array.from(normalized);
|
return Array.from(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveStorePath(agentId: string, raw?: string): string {
|
function resolveStorePath(agentId: string, raw?: string): string {
|
||||||
const stateDir = resolveStateDir(process.env, os.homedir);
|
const stateDir = resolveStateDir(process.env, os.homedir);
|
||||||
const fallback = path.join(stateDir, "memory", `${agentId}.sqlite`);
|
const fallback = path.join(stateDir, "memory", `${agentId}.sqlite`);
|
||||||
if (!raw) return fallback;
|
if (!raw) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
const withToken = raw.includes("{agentId}") ? raw.replaceAll("{agentId}", agentId) : raw;
|
const withToken = raw.includes("{agentId}") ? raw.replaceAll("{agentId}", agentId) : raw;
|
||||||
return resolveUserPath(withToken);
|
return resolveUserPath(withToken);
|
||||||
}
|
}
|
||||||
@@ -286,6 +294,8 @@ export function resolveMemorySearchConfig(
|
|||||||
const defaults = cfg.agents?.defaults?.memorySearch;
|
const defaults = cfg.agents?.defaults?.memorySearch;
|
||||||
const overrides = resolveAgentConfig(cfg, agentId)?.memorySearch;
|
const overrides = resolveAgentConfig(cfg, agentId)?.memorySearch;
|
||||||
const resolved = mergeConfig(defaults, overrides, agentId);
|
const resolved = mergeConfig(defaults, overrides, agentId);
|
||||||
if (!resolved.enabled) return null;
|
if (!resolved.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,11 +45,17 @@ export async function minimaxUnderstandImage(params: {
|
|||||||
modelBaseUrl?: string;
|
modelBaseUrl?: string;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const apiKey = params.apiKey.trim();
|
const apiKey = params.apiKey.trim();
|
||||||
if (!apiKey) throw new Error("MiniMax VLM: apiKey required");
|
if (!apiKey) {
|
||||||
|
throw new Error("MiniMax VLM: apiKey required");
|
||||||
|
}
|
||||||
const prompt = params.prompt.trim();
|
const prompt = params.prompt.trim();
|
||||||
if (!prompt) throw new Error("MiniMax VLM: prompt required");
|
if (!prompt) {
|
||||||
|
throw new Error("MiniMax VLM: prompt required");
|
||||||
|
}
|
||||||
const imageDataUrl = params.imageDataUrl.trim();
|
const imageDataUrl = params.imageDataUrl.trim();
|
||||||
if (!imageDataUrl) throw new Error("MiniMax VLM: imageDataUrl required");
|
if (!imageDataUrl) {
|
||||||
|
throw new Error("MiniMax VLM: imageDataUrl required");
|
||||||
|
}
|
||||||
if (!/^data:image\/(png|jpeg|webp);base64,/i.test(imageDataUrl)) {
|
if (!/^data:image\/(png|jpeg|webp);base64,/i.test(imageDataUrl)) {
|
||||||
throw new Error("MiniMax VLM: imageDataUrl must be a base64 data:image/(png|jpeg|webp) URL");
|
throw new Error("MiniMax VLM: imageDataUrl must be a base64 data:image/(png|jpeg|webp) URL");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ function resolveProviderConfig(
|
|||||||
): ModelProviderConfig | undefined {
|
): ModelProviderConfig | undefined {
|
||||||
const providers = cfg?.models?.providers ?? {};
|
const providers = cfg?.models?.providers ?? {};
|
||||||
const direct = providers[provider] as ModelProviderConfig | undefined;
|
const direct = providers[provider] as ModelProviderConfig | undefined;
|
||||||
if (direct) return direct;
|
if (direct) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
const normalized = normalizeProviderId(provider);
|
const normalized = normalizeProviderId(provider);
|
||||||
if (normalized === provider) {
|
if (normalized === provider) {
|
||||||
const matched = Object.entries(providers).find(
|
const matched = Object.entries(providers).find(
|
||||||
@@ -74,11 +76,15 @@ function resolveEnvSourceLabel(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveAwsSdkEnvVarName(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
export function resolveAwsSdkEnvVarName(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
||||||
if (env[AWS_BEARER_ENV]?.trim()) return AWS_BEARER_ENV;
|
if (env[AWS_BEARER_ENV]?.trim()) {
|
||||||
|
return AWS_BEARER_ENV;
|
||||||
|
}
|
||||||
if (env[AWS_ACCESS_KEY_ENV]?.trim() && env[AWS_SECRET_KEY_ENV]?.trim()) {
|
if (env[AWS_ACCESS_KEY_ENV]?.trim() && env[AWS_SECRET_KEY_ENV]?.trim()) {
|
||||||
return AWS_ACCESS_KEY_ENV;
|
return AWS_ACCESS_KEY_ENV;
|
||||||
}
|
}
|
||||||
if (env[AWS_PROFILE_ENV]?.trim()) return AWS_PROFILE_ENV;
|
if (env[AWS_PROFILE_ENV]?.trim()) {
|
||||||
|
return AWS_PROFILE_ENV;
|
||||||
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +238,9 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
|||||||
const applied = new Set(getShellEnvAppliedKeys());
|
const applied = new Set(getShellEnvAppliedKeys());
|
||||||
const pick = (envVar: string): EnvApiKeyResult | null => {
|
const pick = (envVar: string): EnvApiKeyResult | null => {
|
||||||
const value = process.env[envVar]?.trim();
|
const value = process.env[envVar]?.trim();
|
||||||
if (!value) return null;
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const source = applied.has(envVar) ? `shell env: ${envVar}` : `env: ${envVar}`;
|
const source = applied.has(envVar) ? `shell env: ${envVar}` : `env: ${envVar}`;
|
||||||
return { apiKey: value, source };
|
return { apiKey: value, source };
|
||||||
};
|
};
|
||||||
@@ -255,7 +263,9 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
|||||||
|
|
||||||
if (normalized === "google-vertex") {
|
if (normalized === "google-vertex") {
|
||||||
const envKey = getEnvApiKey(normalized);
|
const envKey = getEnvApiKey(normalized);
|
||||||
if (!envKey) return null;
|
if (!envKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return { apiKey: envKey, source: "gcloud adc" };
|
return { apiKey: envKey, source: "gcloud adc" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +299,9 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
|||||||
opencode: "OPENCODE_API_KEY",
|
opencode: "OPENCODE_API_KEY",
|
||||||
};
|
};
|
||||||
const envVar = envMap[normalized];
|
const envVar = envMap[normalized];
|
||||||
if (!envVar) return null;
|
if (!envVar) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return pick(envVar);
|
return pick(envVar);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,10 +311,14 @@ export function resolveModelAuthMode(
|
|||||||
store?: AuthProfileStore,
|
store?: AuthProfileStore,
|
||||||
): ModelAuthMode | undefined {
|
): ModelAuthMode | undefined {
|
||||||
const resolved = provider?.trim();
|
const resolved = provider?.trim();
|
||||||
if (!resolved) return undefined;
|
if (!resolved) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const authOverride = resolveProviderAuthOverride(cfg, resolved);
|
const authOverride = resolveProviderAuthOverride(cfg, resolved);
|
||||||
if (authOverride === "aws-sdk") return "aws-sdk";
|
if (authOverride === "aws-sdk") {
|
||||||
|
return "aws-sdk";
|
||||||
|
}
|
||||||
|
|
||||||
const authStore = store ?? ensureAuthProfileStore();
|
const authStore = store ?? ensureAuthProfileStore();
|
||||||
const profiles = listProfilesForProvider(authStore, resolved);
|
const profiles = listProfilesForProvider(authStore, resolved);
|
||||||
@@ -315,10 +331,18 @@ export function resolveModelAuthMode(
|
|||||||
const distinct = ["oauth", "token", "api_key"].filter((k) =>
|
const distinct = ["oauth", "token", "api_key"].filter((k) =>
|
||||||
modes.has(k as "oauth" | "token" | "api_key"),
|
modes.has(k as "oauth" | "token" | "api_key"),
|
||||||
);
|
);
|
||||||
if (distinct.length >= 2) return "mixed";
|
if (distinct.length >= 2) {
|
||||||
if (modes.has("oauth")) return "oauth";
|
return "mixed";
|
||||||
if (modes.has("token")) return "token";
|
}
|
||||||
if (modes.has("api_key")) return "api-key";
|
if (modes.has("oauth")) {
|
||||||
|
return "oauth";
|
||||||
|
}
|
||||||
|
if (modes.has("token")) {
|
||||||
|
return "token";
|
||||||
|
}
|
||||||
|
if (modes.has("api_key")) {
|
||||||
|
return "api-key";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authOverride === undefined && normalizeProviderId(resolved) === "amazon-bedrock") {
|
if (authOverride === undefined && normalizeProviderId(resolved) === "amazon-bedrock") {
|
||||||
@@ -330,7 +354,9 @@ export function resolveModelAuthMode(
|
|||||||
return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key";
|
return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getCustomProviderApiKey(cfg, resolved)) return "api-key";
|
if (getCustomProviderApiKey(cfg, resolved)) {
|
||||||
|
return "api-key";
|
||||||
|
}
|
||||||
|
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
@@ -355,6 +381,8 @@ export async function getApiKeyForModel(params: {
|
|||||||
|
|
||||||
export function requireApiKey(auth: ResolvedProviderAuth, provider: string): string {
|
export function requireApiKey(auth: ResolvedProviderAuth, provider: string): string {
|
||||||
const key = auth.apiKey?.trim();
|
const key = auth.apiKey?.trim();
|
||||||
if (key) return key;
|
if (key) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
throw new Error(`No API key resolved for provider "${provider}" (auth mode: ${auth.mode}).`);
|
throw new Error(`No API key resolved for provider "${provider}" (auth mode: ${auth.mode}).`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,14 +45,18 @@ export async function loadModelCatalog(params?: {
|
|||||||
if (params?.useCache === false) {
|
if (params?.useCache === false) {
|
||||||
modelCatalogPromise = null;
|
modelCatalogPromise = null;
|
||||||
}
|
}
|
||||||
if (modelCatalogPromise) return modelCatalogPromise;
|
if (modelCatalogPromise) {
|
||||||
|
return modelCatalogPromise;
|
||||||
|
}
|
||||||
|
|
||||||
modelCatalogPromise = (async () => {
|
modelCatalogPromise = (async () => {
|
||||||
const models: ModelCatalogEntry[] = [];
|
const models: ModelCatalogEntry[] = [];
|
||||||
const sortModels = (entries: ModelCatalogEntry[]) =>
|
const sortModels = (entries: ModelCatalogEntry[]) =>
|
||||||
entries.sort((a, b) => {
|
entries.sort((a, b) => {
|
||||||
const p = a.provider.localeCompare(b.provider);
|
const p = a.provider.localeCompare(b.provider);
|
||||||
if (p !== 0) return p;
|
if (p !== 0) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
@@ -74,9 +78,13 @@ export async function loadModelCatalog(params?: {
|
|||||||
const entries = Array.isArray(registry) ? registry : registry.getAll();
|
const entries = Array.isArray(registry) ? registry : registry.getAll();
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const id = String(entry?.id ?? "").trim();
|
const id = String(entry?.id ?? "").trim();
|
||||||
if (!id) continue;
|
if (!id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const provider = String(entry?.provider ?? "").trim();
|
const provider = String(entry?.provider ?? "").trim();
|
||||||
if (!provider) continue;
|
if (!provider) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const name = String(entry?.name ?? id).trim() || id;
|
const name = String(entry?.name ?? id).trim() || id;
|
||||||
const contextWindow =
|
const contextWindow =
|
||||||
typeof entry?.contextWindow === "number" && entry.contextWindow > 0
|
typeof entry?.contextWindow === "number" && entry.contextWindow > 0
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ function isOpenAiCompletionsModel(model: Model<Api>): model is Model<"openai-com
|
|||||||
export function normalizeModelCompat(model: Model<Api>): Model<Api> {
|
export function normalizeModelCompat(model: Model<Api>): Model<Api> {
|
||||||
const baseUrl = model.baseUrl ?? "";
|
const baseUrl = model.baseUrl ?? "";
|
||||||
const isZai = model.provider === "zai" || baseUrl.includes("api.z.ai");
|
const isZai = model.provider === "zai" || baseUrl.includes("api.z.ai");
|
||||||
if (!isZai || !isOpenAiCompletionsModel(model)) return model;
|
if (!isZai || !isOpenAiCompletionsModel(model)) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
const openaiModel = model;
|
const openaiModel = model;
|
||||||
const compat = openaiModel.compat ?? undefined;
|
const compat = openaiModel.compat ?? undefined;
|
||||||
if (compat?.supportsDeveloperRole === false) return model;
|
if (compat?.supportsDeveloperRole === false) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
openaiModel.compat = compat
|
openaiModel.compat = compat
|
||||||
? { ...compat, supportsDeveloperRole: false }
|
? { ...compat, supportsDeveloperRole: false }
|
||||||
|
|||||||
@@ -158,7 +158,9 @@ describe("runWithModelFallback", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const run = vi.fn().mockImplementation(async (providerId, modelId) => {
|
const run = vi.fn().mockImplementation(async (providerId, modelId) => {
|
||||||
if (providerId === "fallback") return "ok";
|
if (providerId === "fallback") {
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
throw new Error(`unexpected provider: ${providerId}/${modelId}`);
|
throw new Error(`unexpected provider: ${providerId}/${modelId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -219,7 +221,9 @@ describe("runWithModelFallback", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const run = vi.fn().mockImplementation(async (providerId) => {
|
const run = vi.fn().mockImplementation(async (providerId) => {
|
||||||
if (providerId === provider) return "ok";
|
if (providerId === provider) {
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
return "unexpected";
|
return "unexpected";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,12 @@ type FallbackAttempt = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function isAbortError(err: unknown): boolean {
|
function isAbortError(err: unknown): boolean {
|
||||||
if (!err || typeof err !== "object") return false;
|
if (!err || typeof err !== "object") {
|
||||||
if (isFailoverError(err)) return false;
|
return false;
|
||||||
|
}
|
||||||
|
if (isFailoverError(err)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const name = "name" in err ? String(err.name) : "";
|
const name = "name" in err ? String(err.name) : "";
|
||||||
// Only treat explicit AbortError names as user aborts.
|
// Only treat explicit AbortError names as user aborts.
|
||||||
// Message-based checks (e.g., "aborted") can mask timeouts and skip fallback.
|
// Message-based checks (e.g., "aborted") can mask timeouts and skip fallback.
|
||||||
@@ -55,11 +59,15 @@ function buildAllowedModelKeys(
|
|||||||
const modelMap = cfg?.agents?.defaults?.models ?? {};
|
const modelMap = cfg?.agents?.defaults?.models ?? {};
|
||||||
return Object.keys(modelMap);
|
return Object.keys(modelMap);
|
||||||
})();
|
})();
|
||||||
if (rawAllowlist.length === 0) return null;
|
if (rawAllowlist.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const keys = new Set<string>();
|
const keys = new Set<string>();
|
||||||
for (const raw of rawAllowlist) {
|
for (const raw of rawAllowlist) {
|
||||||
const parsed = parseModelRef(String(raw ?? ""), defaultProvider);
|
const parsed = parseModelRef(String(raw ?? ""), defaultProvider);
|
||||||
if (!parsed) continue;
|
if (!parsed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
keys.add(modelKey(parsed.provider, parsed.model));
|
keys.add(modelKey(parsed.provider, parsed.model));
|
||||||
}
|
}
|
||||||
return keys.size > 0 ? keys : null;
|
return keys.size > 0 ? keys : null;
|
||||||
@@ -79,10 +87,16 @@ function resolveImageFallbackCandidates(params: {
|
|||||||
const candidates: ModelCandidate[] = [];
|
const candidates: ModelCandidate[] = [];
|
||||||
|
|
||||||
const addCandidate = (candidate: ModelCandidate, enforceAllowlist: boolean) => {
|
const addCandidate = (candidate: ModelCandidate, enforceAllowlist: boolean) => {
|
||||||
if (!candidate.provider || !candidate.model) return;
|
if (!candidate.provider || !candidate.model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const key = modelKey(candidate.provider, candidate.model);
|
const key = modelKey(candidate.provider, candidate.model);
|
||||||
if (seen.has(key)) return;
|
if (seen.has(key)) {
|
||||||
if (enforceAllowlist && allowlist && !allowlist.has(key)) return;
|
return;
|
||||||
|
}
|
||||||
|
if (enforceAllowlist && allowlist && !allowlist.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
candidates.push(candidate);
|
candidates.push(candidate);
|
||||||
};
|
};
|
||||||
@@ -93,7 +107,9 @@ function resolveImageFallbackCandidates(params: {
|
|||||||
defaultProvider: params.defaultProvider,
|
defaultProvider: params.defaultProvider,
|
||||||
aliasIndex,
|
aliasIndex,
|
||||||
});
|
});
|
||||||
if (!resolved) return;
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
addCandidate(resolved.ref, enforceAllowlist);
|
addCandidate(resolved.ref, enforceAllowlist);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,7 +121,9 @@ function resolveImageFallbackCandidates(params: {
|
|||||||
| string
|
| string
|
||||||
| undefined;
|
| undefined;
|
||||||
const primary = typeof imageModel === "string" ? imageModel.trim() : imageModel?.primary;
|
const primary = typeof imageModel === "string" ? imageModel.trim() : imageModel?.primary;
|
||||||
if (primary?.trim()) addRaw(primary, false);
|
if (primary?.trim()) {
|
||||||
|
addRaw(primary, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageFallbacks = (() => {
|
const imageFallbacks = (() => {
|
||||||
@@ -153,10 +171,16 @@ function resolveFallbackCandidates(params: {
|
|||||||
const candidates: ModelCandidate[] = [];
|
const candidates: ModelCandidate[] = [];
|
||||||
|
|
||||||
const addCandidate = (candidate: ModelCandidate, enforceAllowlist: boolean) => {
|
const addCandidate = (candidate: ModelCandidate, enforceAllowlist: boolean) => {
|
||||||
if (!candidate.provider || !candidate.model) return;
|
if (!candidate.provider || !candidate.model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const key = modelKey(candidate.provider, candidate.model);
|
const key = modelKey(candidate.provider, candidate.model);
|
||||||
if (seen.has(key)) return;
|
if (seen.has(key)) {
|
||||||
if (enforceAllowlist && allowlist && !allowlist.has(key)) return;
|
return;
|
||||||
|
}
|
||||||
|
if (enforceAllowlist && allowlist && !allowlist.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
candidates.push(candidate);
|
candidates.push(candidate);
|
||||||
};
|
};
|
||||||
@@ -164,12 +188,16 @@ function resolveFallbackCandidates(params: {
|
|||||||
addCandidate({ provider, model }, false);
|
addCandidate({ provider, model }, false);
|
||||||
|
|
||||||
const modelFallbacks = (() => {
|
const modelFallbacks = (() => {
|
||||||
if (params.fallbacksOverride !== undefined) return params.fallbacksOverride;
|
if (params.fallbacksOverride !== undefined) {
|
||||||
|
return params.fallbacksOverride;
|
||||||
|
}
|
||||||
const model = params.cfg?.agents?.defaults?.model as
|
const model = params.cfg?.agents?.defaults?.model as
|
||||||
| { fallbacks?: string[] }
|
| { fallbacks?: string[] }
|
||||||
| string
|
| string
|
||||||
| undefined;
|
| undefined;
|
||||||
if (model && typeof model === "object") return model.fallbacks ?? [];
|
if (model && typeof model === "object") {
|
||||||
|
return model.fallbacks ?? [];
|
||||||
|
}
|
||||||
return [];
|
return [];
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -179,7 +207,9 @@ function resolveFallbackCandidates(params: {
|
|||||||
defaultProvider,
|
defaultProvider,
|
||||||
aliasIndex,
|
aliasIndex,
|
||||||
});
|
});
|
||||||
if (!resolved) continue;
|
if (!resolved) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
addCandidate(resolved.ref, true);
|
addCandidate(resolved.ref, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,13 +283,17 @@ export async function runWithModelFallback<T>(params: {
|
|||||||
attempts,
|
attempts,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (shouldRethrowAbort(err)) throw err;
|
if (shouldRethrowAbort(err)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
const normalized =
|
const normalized =
|
||||||
coerceToFailoverError(err, {
|
coerceToFailoverError(err, {
|
||||||
provider: candidate.provider,
|
provider: candidate.provider,
|
||||||
model: candidate.model,
|
model: candidate.model,
|
||||||
}) ?? err;
|
}) ?? err;
|
||||||
if (!isFailoverError(normalized)) throw err;
|
if (!isFailoverError(normalized)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
lastError = normalized;
|
lastError = normalized;
|
||||||
const described = describeFailoverError(normalized);
|
const described = describeFailoverError(normalized);
|
||||||
@@ -281,7 +315,9 @@ export async function runWithModelFallback<T>(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempts.length <= 1 && lastError) throw lastError;
|
if (attempts.length <= 1 && lastError) {
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
const summary =
|
const summary =
|
||||||
attempts.length > 0
|
attempts.length > 0
|
||||||
? attempts
|
? attempts
|
||||||
@@ -340,7 +376,9 @@ export async function runWithImageModelFallback<T>(params: {
|
|||||||
attempts,
|
attempts,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (shouldRethrowAbort(err)) throw err;
|
if (shouldRethrowAbort(err)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
lastError = err;
|
lastError = err;
|
||||||
attempts.push({
|
attempts.push({
|
||||||
provider: candidate.provider,
|
provider: candidate.provider,
|
||||||
@@ -357,7 +395,9 @@ export async function runWithImageModelFallback<T>(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempts.length <= 1 && lastError) throw lastError;
|
if (attempts.length <= 1 && lastError) {
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
const summary =
|
const summary =
|
||||||
attempts.length > 0
|
attempts.length > 0
|
||||||
? attempts
|
? attempts
|
||||||
|
|||||||
@@ -85,9 +85,15 @@ export type OpenRouterScanOptions = {
|
|||||||
type OpenAIModel = Model<"openai-completions">;
|
type OpenAIModel = Model<"openai-completions">;
|
||||||
|
|
||||||
function normalizeCreatedAtMs(value: unknown): number | null {
|
function normalizeCreatedAtMs(value: unknown): number | null {
|
||||||
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||||
if (value <= 0) return null;
|
return null;
|
||||||
if (value > 1e12) return Math.round(value);
|
}
|
||||||
|
if (value <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value > 1e12) {
|
||||||
|
return Math.round(value);
|
||||||
|
}
|
||||||
return Math.round(value * 1000);
|
return Math.round(value * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,16 +103,24 @@ function inferParamBFromIdOrName(text: string): number | null {
|
|||||||
let best: number | null = null;
|
let best: number | null = null;
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const numRaw = match[1];
|
const numRaw = match[1];
|
||||||
if (!numRaw) continue;
|
if (!numRaw) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const value = Number(numRaw);
|
const value = Number(numRaw);
|
||||||
if (!Number.isFinite(value) || value <= 0) continue;
|
if (!Number.isFinite(value) || value <= 0) {
|
||||||
if (best === null || value > best) best = value;
|
continue;
|
||||||
|
}
|
||||||
|
if (best === null || value > best) {
|
||||||
|
best = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseModality(modality: string | null): Array<"text" | "image"> {
|
function parseModality(modality: string | null): Array<"text" | "image"> {
|
||||||
if (!modality) return ["text"];
|
if (!modality) {
|
||||||
|
return ["text"];
|
||||||
|
}
|
||||||
const normalized = modality.toLowerCase();
|
const normalized = modality.toLowerCase();
|
||||||
const parts = normalized.split(/[^a-z]+/).filter(Boolean);
|
const parts = normalized.split(/[^a-z]+/).filter(Boolean);
|
||||||
const hasImage = parts.includes("image");
|
const hasImage = parts.includes("image");
|
||||||
@@ -114,17 +128,27 @@ function parseModality(modality: string | null): Array<"text" | "image"> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseNumberString(value: unknown): number | null {
|
function parseNumberString(value: unknown): number | null {
|
||||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
if (typeof value !== "string") return null;
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const num = Number(trimmed);
|
const num = Number(trimmed);
|
||||||
if (!Number.isFinite(num)) return null;
|
if (!Number.isFinite(num)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return num;
|
return num;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseOpenRouterPricing(value: unknown): OpenRouterModelPricing | null {
|
function parseOpenRouterPricing(value: unknown): OpenRouterModelPricing | null {
|
||||||
if (!value || typeof value !== "object") return null;
|
if (!value || typeof value !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const obj = value as Record<string, unknown>;
|
const obj = value as Record<string, unknown>;
|
||||||
const prompt = parseNumberString(obj.prompt);
|
const prompt = parseNumberString(obj.prompt);
|
||||||
const completion = parseNumberString(obj.completion);
|
const completion = parseNumberString(obj.completion);
|
||||||
@@ -133,7 +157,9 @@ function parseOpenRouterPricing(value: unknown): OpenRouterModelPricing | null {
|
|||||||
const webSearch = parseNumberString(obj.web_search) ?? 0;
|
const webSearch = parseNumberString(obj.web_search) ?? 0;
|
||||||
const internalReasoning = parseNumberString(obj.internal_reasoning) ?? 0;
|
const internalReasoning = parseNumberString(obj.internal_reasoning) ?? 0;
|
||||||
|
|
||||||
if (prompt === null || completion === null) return null;
|
if (prompt === null || completion === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
prompt,
|
prompt,
|
||||||
completion,
|
completion,
|
||||||
@@ -145,8 +171,12 @@ function parseOpenRouterPricing(value: unknown): OpenRouterModelPricing | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isFreeOpenRouterModel(entry: OpenRouterModelMeta): boolean {
|
function isFreeOpenRouterModel(entry: OpenRouterModelMeta): boolean {
|
||||||
if (entry.id.endsWith(":free")) return true;
|
if (entry.id.endsWith(":free")) {
|
||||||
if (!entry.pricing) return false;
|
return true;
|
||||||
|
}
|
||||||
|
if (!entry.pricing) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return entry.pricing.prompt === 0 && entry.pricing.completion === 0;
|
return entry.pricing.prompt === 0 && entry.pricing.completion === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,10 +205,14 @@ async function fetchOpenRouterModels(fetchImpl: typeof fetch): Promise<OpenRoute
|
|||||||
|
|
||||||
return entries
|
return entries
|
||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
if (!entry || typeof entry !== "object") return null;
|
if (!entry || typeof entry !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const obj = entry as Record<string, unknown>;
|
const obj = entry as Record<string, unknown>;
|
||||||
const id = typeof obj.id === "string" ? obj.id.trim() : "";
|
const id = typeof obj.id === "string" ? obj.id.trim() : "";
|
||||||
if (!id) return null;
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const name = typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : id;
|
const name = typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : id;
|
||||||
|
|
||||||
const contextLength =
|
const contextLength =
|
||||||
@@ -311,7 +345,9 @@ async function probeImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureImageInput(model: OpenAIModel): OpenAIModel {
|
function ensureImageInput(model: OpenAIModel): OpenAIModel {
|
||||||
if (model.input.includes("image")) return model;
|
if (model.input.includes("image")) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...model,
|
...model,
|
||||||
input: Array.from(new Set([...model.input, "image"])),
|
input: Array.from(new Set([...model.input, "image"])),
|
||||||
@@ -333,7 +369,9 @@ async function mapWithConcurrency<T, R>(
|
|||||||
while (true) {
|
while (true) {
|
||||||
const current = nextIndex;
|
const current = nextIndex;
|
||||||
nextIndex += 1;
|
nextIndex += 1;
|
||||||
if (current >= items.length) return;
|
if (current >= items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
results[current] = await fn(items[current], current);
|
results[current] = await fn(items[current], current);
|
||||||
completed += 1;
|
completed += 1;
|
||||||
opts?.onProgress?.(completed, items.length);
|
opts?.onProgress?.(completed, items.length);
|
||||||
@@ -369,19 +407,27 @@ export async function scanOpenRouterModels(
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
const filtered = catalog.filter((entry) => {
|
const filtered = catalog.filter((entry) => {
|
||||||
if (!isFreeOpenRouterModel(entry)) return false;
|
if (!isFreeOpenRouterModel(entry)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (providerFilter) {
|
if (providerFilter) {
|
||||||
const prefix = entry.id.split("/")[0]?.toLowerCase() ?? "";
|
const prefix = entry.id.split("/")[0]?.toLowerCase() ?? "";
|
||||||
if (prefix !== providerFilter) return false;
|
if (prefix !== providerFilter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (minParamB > 0) {
|
if (minParamB > 0) {
|
||||||
const params = entry.inferredParamB ?? 0;
|
const params = entry.inferredParamB ?? 0;
|
||||||
if (params < minParamB) return false;
|
if (params < minParamB) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (maxAgeDays > 0 && entry.createdAtMs) {
|
if (maxAgeDays > 0 && entry.createdAtMs) {
|
||||||
const ageMs = now - entry.createdAtMs;
|
const ageMs = now - entry.createdAtMs;
|
||||||
const ageDays = ageMs / (24 * 60 * 60 * 1000);
|
const ageDays = ageMs / (24 * 60 * 60 * 1000);
|
||||||
if (ageDays > maxAgeDays) return false;
|
if (ageDays > maxAgeDays) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,39 +26,63 @@ export function modelKey(provider: string, model: string) {
|
|||||||
|
|
||||||
export function normalizeProviderId(provider: string): string {
|
export function normalizeProviderId(provider: string): string {
|
||||||
const normalized = provider.trim().toLowerCase();
|
const normalized = provider.trim().toLowerCase();
|
||||||
if (normalized === "z.ai" || normalized === "z-ai") return "zai";
|
if (normalized === "z.ai" || normalized === "z-ai") {
|
||||||
if (normalized === "opencode-zen") return "opencode";
|
return "zai";
|
||||||
if (normalized === "qwen") return "qwen-portal";
|
}
|
||||||
if (normalized === "kimi-code") return "kimi-coding";
|
if (normalized === "opencode-zen") {
|
||||||
|
return "opencode";
|
||||||
|
}
|
||||||
|
if (normalized === "qwen") {
|
||||||
|
return "qwen-portal";
|
||||||
|
}
|
||||||
|
if (normalized === "kimi-code") {
|
||||||
|
return "kimi-coding";
|
||||||
|
}
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCliProvider(provider: string, cfg?: OpenClawConfig): boolean {
|
export function isCliProvider(provider: string, cfg?: OpenClawConfig): boolean {
|
||||||
const normalized = normalizeProviderId(provider);
|
const normalized = normalizeProviderId(provider);
|
||||||
if (normalized === "claude-cli") return true;
|
if (normalized === "claude-cli") {
|
||||||
if (normalized === "codex-cli") return true;
|
return true;
|
||||||
|
}
|
||||||
|
if (normalized === "codex-cli") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const backends = cfg?.agents?.defaults?.cliBackends ?? {};
|
const backends = cfg?.agents?.defaults?.cliBackends ?? {};
|
||||||
return Object.keys(backends).some((key) => normalizeProviderId(key) === normalized);
|
return Object.keys(backends).some((key) => normalizeProviderId(key) === normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeAnthropicModelId(model: string): string {
|
function normalizeAnthropicModelId(model: string): string {
|
||||||
const trimmed = model.trim();
|
const trimmed = model.trim();
|
||||||
if (!trimmed) return trimmed;
|
if (!trimmed) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
const lower = trimmed.toLowerCase();
|
const lower = trimmed.toLowerCase();
|
||||||
if (lower === "opus-4.5") return "claude-opus-4-5";
|
if (lower === "opus-4.5") {
|
||||||
if (lower === "sonnet-4.5") return "claude-sonnet-4-5";
|
return "claude-opus-4-5";
|
||||||
|
}
|
||||||
|
if (lower === "sonnet-4.5") {
|
||||||
|
return "claude-sonnet-4-5";
|
||||||
|
}
|
||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeProviderModelId(provider: string, model: string): string {
|
function normalizeProviderModelId(provider: string, model: string): string {
|
||||||
if (provider === "anthropic") return normalizeAnthropicModelId(model);
|
if (provider === "anthropic") {
|
||||||
if (provider === "google") return normalizeGoogleModelId(model);
|
return normalizeAnthropicModelId(model);
|
||||||
|
}
|
||||||
|
if (provider === "google") {
|
||||||
|
return normalizeGoogleModelId(model);
|
||||||
|
}
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseModelRef(raw: string, defaultProvider: string): ModelRef | null {
|
export function parseModelRef(raw: string, defaultProvider: string): ModelRef | null {
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const slash = trimmed.indexOf("/");
|
const slash = trimmed.indexOf("/");
|
||||||
if (slash === -1) {
|
if (slash === -1) {
|
||||||
const provider = normalizeProviderId(defaultProvider);
|
const provider = normalizeProviderId(defaultProvider);
|
||||||
@@ -68,7 +92,9 @@ export function parseModelRef(raw: string, defaultProvider: string): ModelRef |
|
|||||||
const providerRaw = trimmed.slice(0, slash).trim();
|
const providerRaw = trimmed.slice(0, slash).trim();
|
||||||
const provider = normalizeProviderId(providerRaw);
|
const provider = normalizeProviderId(providerRaw);
|
||||||
const model = trimmed.slice(slash + 1).trim();
|
const model = trimmed.slice(slash + 1).trim();
|
||||||
if (!provider || !model) return null;
|
if (!provider || !model) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const normalizedModel = normalizeProviderModelId(provider, model);
|
const normalizedModel = normalizeProviderModelId(provider, model);
|
||||||
return { provider, model: normalizedModel };
|
return { provider, model: normalizedModel };
|
||||||
}
|
}
|
||||||
@@ -83,9 +109,13 @@ export function buildModelAliasIndex(params: {
|
|||||||
const rawModels = params.cfg.agents?.defaults?.models ?? {};
|
const rawModels = params.cfg.agents?.defaults?.models ?? {};
|
||||||
for (const [keyRaw, entryRaw] of Object.entries(rawModels)) {
|
for (const [keyRaw, entryRaw] of Object.entries(rawModels)) {
|
||||||
const parsed = parseModelRef(String(keyRaw ?? ""), params.defaultProvider);
|
const parsed = parseModelRef(String(keyRaw ?? ""), params.defaultProvider);
|
||||||
if (!parsed) continue;
|
if (!parsed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const alias = String((entryRaw as { alias?: string } | undefined)?.alias ?? "").trim();
|
const alias = String((entryRaw as { alias?: string } | undefined)?.alias ?? "").trim();
|
||||||
if (!alias) continue;
|
if (!alias) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const aliasKey = normalizeAliasKey(alias);
|
const aliasKey = normalizeAliasKey(alias);
|
||||||
byAlias.set(aliasKey, { alias, ref: parsed });
|
byAlias.set(aliasKey, { alias, ref: parsed });
|
||||||
const key = modelKey(parsed.provider, parsed.model);
|
const key = modelKey(parsed.provider, parsed.model);
|
||||||
@@ -103,7 +133,9 @@ export function resolveModelRefFromString(params: {
|
|||||||
aliasIndex?: ModelAliasIndex;
|
aliasIndex?: ModelAliasIndex;
|
||||||
}): { ref: ModelRef; alias?: string } | null {
|
}): { ref: ModelRef; alias?: string } | null {
|
||||||
const trimmed = params.raw.trim();
|
const trimmed = params.raw.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (!trimmed.includes("/")) {
|
if (!trimmed.includes("/")) {
|
||||||
const aliasKey = normalizeAliasKey(trimmed);
|
const aliasKey = normalizeAliasKey(trimmed);
|
||||||
const aliasMatch = params.aliasIndex?.byAlias.get(aliasKey);
|
const aliasMatch = params.aliasIndex?.byAlias.get(aliasKey);
|
||||||
@@ -112,7 +144,9 @@ export function resolveModelRefFromString(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const parsed = parseModelRef(trimmed, params.defaultProvider);
|
const parsed = parseModelRef(trimmed, params.defaultProvider);
|
||||||
if (!parsed) return null;
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return { ref: parsed };
|
return { ref: parsed };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +157,9 @@ export function resolveConfiguredModelRef(params: {
|
|||||||
}): ModelRef {
|
}): ModelRef {
|
||||||
const rawModel = (() => {
|
const rawModel = (() => {
|
||||||
const raw = params.cfg.agents?.defaults?.model as { primary?: string } | string | undefined;
|
const raw = params.cfg.agents?.defaults?.model as { primary?: string } | string | undefined;
|
||||||
if (typeof raw === "string") return raw.trim();
|
if (typeof raw === "string") {
|
||||||
|
return raw.trim();
|
||||||
|
}
|
||||||
return raw?.primary?.trim() ?? "";
|
return raw?.primary?.trim() ?? "";
|
||||||
})();
|
})();
|
||||||
if (rawModel) {
|
if (rawModel) {
|
||||||
@@ -135,7 +171,9 @@ export function resolveConfiguredModelRef(params: {
|
|||||||
if (!trimmed.includes("/")) {
|
if (!trimmed.includes("/")) {
|
||||||
const aliasKey = normalizeAliasKey(trimmed);
|
const aliasKey = normalizeAliasKey(trimmed);
|
||||||
const aliasMatch = aliasIndex.byAlias.get(aliasKey);
|
const aliasMatch = aliasIndex.byAlias.get(aliasKey);
|
||||||
if (aliasMatch) return aliasMatch.ref;
|
if (aliasMatch) {
|
||||||
|
return aliasMatch.ref;
|
||||||
|
}
|
||||||
|
|
||||||
// Default to anthropic if no provider is specified, but warn as this is deprecated.
|
// Default to anthropic if no provider is specified, but warn as this is deprecated.
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -149,7 +187,9 @@ export function resolveConfiguredModelRef(params: {
|
|||||||
defaultProvider: params.defaultProvider,
|
defaultProvider: params.defaultProvider,
|
||||||
aliasIndex,
|
aliasIndex,
|
||||||
});
|
});
|
||||||
if (resolved) return resolved.ref;
|
if (resolved) {
|
||||||
|
return resolved.ref;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { provider: params.defaultProvider, model: params.defaultModel };
|
return { provider: params.defaultProvider, model: params.defaultModel };
|
||||||
}
|
}
|
||||||
@@ -209,7 +249,9 @@ export function buildAllowedModelSet(params: {
|
|||||||
const catalogKeys = new Set(params.catalog.map((entry) => modelKey(entry.provider, entry.id)));
|
const catalogKeys = new Set(params.catalog.map((entry) => modelKey(entry.provider, entry.id)));
|
||||||
|
|
||||||
if (allowAny) {
|
if (allowAny) {
|
||||||
if (defaultKey) catalogKeys.add(defaultKey);
|
if (defaultKey) {
|
||||||
|
catalogKeys.add(defaultKey);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
allowAny: true,
|
allowAny: true,
|
||||||
allowedCatalog: params.catalog,
|
allowedCatalog: params.catalog,
|
||||||
@@ -221,7 +263,9 @@ export function buildAllowedModelSet(params: {
|
|||||||
const configuredProviders = (params.cfg.models?.providers ?? {}) as Record<string, unknown>;
|
const configuredProviders = (params.cfg.models?.providers ?? {}) as Record<string, unknown>;
|
||||||
for (const raw of rawAllowlist) {
|
for (const raw of rawAllowlist) {
|
||||||
const parsed = parseModelRef(String(raw), params.defaultProvider);
|
const parsed = parseModelRef(String(raw), params.defaultProvider);
|
||||||
if (!parsed) continue;
|
if (!parsed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const key = modelKey(parsed.provider, parsed.model);
|
const key = modelKey(parsed.provider, parsed.model);
|
||||||
const providerKey = normalizeProviderId(parsed.provider);
|
const providerKey = normalizeProviderId(parsed.provider);
|
||||||
if (isCliProvider(parsed.provider, params.cfg)) {
|
if (isCliProvider(parsed.provider, params.cfg)) {
|
||||||
@@ -244,7 +288,9 @@ export function buildAllowedModelSet(params: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (allowedCatalog.length === 0 && allowedKeys.size === 0) {
|
if (allowedCatalog.length === 0 && allowedKeys.size === 0) {
|
||||||
if (defaultKey) catalogKeys.add(defaultKey);
|
if (defaultKey) {
|
||||||
|
catalogKeys.add(defaultKey);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
allowAny: true,
|
allowAny: true,
|
||||||
allowedCatalog: params.catalog,
|
allowedCatalog: params.catalog,
|
||||||
@@ -296,7 +342,9 @@ export function resolveAllowedModelRef(params: {
|
|||||||
error: string;
|
error: string;
|
||||||
} {
|
} {
|
||||||
const trimmed = params.raw.trim();
|
const trimmed = params.raw.trim();
|
||||||
if (!trimmed) return { error: "invalid model: empty" };
|
if (!trimmed) {
|
||||||
|
return { error: "invalid model: empty" };
|
||||||
|
}
|
||||||
|
|
||||||
const aliasIndex = buildModelAliasIndex({
|
const aliasIndex = buildModelAliasIndex({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
@@ -307,7 +355,9 @@ export function resolveAllowedModelRef(params: {
|
|||||||
defaultProvider: params.defaultProvider,
|
defaultProvider: params.defaultProvider,
|
||||||
aliasIndex,
|
aliasIndex,
|
||||||
});
|
});
|
||||||
if (!resolved) return { error: `invalid model: ${trimmed}` };
|
if (!resolved) {
|
||||||
|
return { error: `invalid model: ${trimmed}` };
|
||||||
|
}
|
||||||
|
|
||||||
const status = getModelRefStatus({
|
const status = getModelRefStatus({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
@@ -330,11 +380,15 @@ export function resolveThinkingDefault(params: {
|
|||||||
catalog?: ModelCatalogEntry[];
|
catalog?: ModelCatalogEntry[];
|
||||||
}): ThinkLevel {
|
}): ThinkLevel {
|
||||||
const configured = params.cfg.agents?.defaults?.thinkingDefault;
|
const configured = params.cfg.agents?.defaults?.thinkingDefault;
|
||||||
if (configured) return configured;
|
if (configured) {
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
const candidate = params.catalog?.find(
|
const candidate = params.catalog?.find(
|
||||||
(entry) => entry.provider === params.provider && entry.id === params.model,
|
(entry) => entry.provider === params.provider && entry.id === params.model,
|
||||||
);
|
);
|
||||||
if (candidate?.reasoning) return "low";
|
if (candidate?.reasoning) {
|
||||||
|
return "low";
|
||||||
|
}
|
||||||
return "off";
|
return "off";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +401,9 @@ export function resolveHooksGmailModel(params: {
|
|||||||
defaultProvider: string;
|
defaultProvider: string;
|
||||||
}): ModelRef | null {
|
}): ModelRef | null {
|
||||||
const hooksModel = params.cfg.hooks?.gmail?.model;
|
const hooksModel = params.cfg.hooks?.gmail?.model;
|
||||||
if (!hooksModel?.trim()) return null;
|
if (!hooksModel?.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const aliasIndex = buildModelAliasIndex({
|
const aliasIndex = buildModelAliasIndex({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
|
|||||||
@@ -126,12 +126,21 @@ describe("models-config", () => {
|
|||||||
|
|
||||||
expect(parsed.providers["github-copilot"]?.baseUrl).toBe("https://api.copilot.example");
|
expect(parsed.providers["github-copilot"]?.baseUrl).toBe("https://api.copilot.example");
|
||||||
} finally {
|
} finally {
|
||||||
if (previous === undefined) delete process.env.COPILOT_GITHUB_TOKEN;
|
if (previous === undefined) {
|
||||||
else process.env.COPILOT_GITHUB_TOKEN = previous;
|
delete process.env.COPILOT_GITHUB_TOKEN;
|
||||||
if (previousGh === undefined) delete process.env.GH_TOKEN;
|
} else {
|
||||||
else process.env.GH_TOKEN = previousGh;
|
process.env.COPILOT_GITHUB_TOKEN = previous;
|
||||||
if (previousGithub === undefined) delete process.env.GITHUB_TOKEN;
|
}
|
||||||
else process.env.GITHUB_TOKEN = previousGithub;
|
if (previousGh === undefined) {
|
||||||
|
delete process.env.GH_TOKEN;
|
||||||
|
} else {
|
||||||
|
process.env.GH_TOKEN = previousGh;
|
||||||
|
}
|
||||||
|
if (previousGithub === undefined) {
|
||||||
|
delete process.env.GITHUB_TOKEN;
|
||||||
|
} else {
|
||||||
|
process.env.GITHUB_TOKEN = previousGithub;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,8 +85,11 @@ describe("models-config", () => {
|
|||||||
const ids = parsed.providers.minimax?.models?.map((model) => model.id);
|
const ids = parsed.providers.minimax?.models?.map((model) => model.id);
|
||||||
expect(ids).toContain("MiniMax-VL-01");
|
expect(ids).toContain("MiniMax-VL-01");
|
||||||
} finally {
|
} finally {
|
||||||
if (prevKey === undefined) delete process.env.MINIMAX_API_KEY;
|
if (prevKey === undefined) {
|
||||||
else process.env.MINIMAX_API_KEY = prevKey;
|
delete process.env.MINIMAX_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.MINIMAX_API_KEY = prevKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -135,7 +135,9 @@ function normalizeApiKeyConfig(value: string): string {
|
|||||||
|
|
||||||
function resolveEnvApiKeyVarName(provider: string): string | undefined {
|
function resolveEnvApiKeyVarName(provider: string): string | undefined {
|
||||||
const resolved = resolveEnvApiKey(provider);
|
const resolved = resolveEnvApiKey(provider);
|
||||||
if (!resolved) return undefined;
|
if (!resolved) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const match = /^(?:env: |shell env: )([A-Z0-9_]+)$/.exec(resolved.source);
|
const match = /^(?:env: |shell env: )([A-Z0-9_]+)$/.exec(resolved.source);
|
||||||
return match ? match[1] : undefined;
|
return match ? match[1] : undefined;
|
||||||
}
|
}
|
||||||
@@ -151,16 +153,26 @@ function resolveApiKeyFromProfiles(params: {
|
|||||||
const ids = listProfilesForProvider(params.store, params.provider);
|
const ids = listProfilesForProvider(params.store, params.provider);
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const cred = params.store.profiles[id];
|
const cred = params.store.profiles[id];
|
||||||
if (!cred) continue;
|
if (!cred) {
|
||||||
if (cred.type === "api_key") return cred.key;
|
continue;
|
||||||
if (cred.type === "token") return cred.token;
|
}
|
||||||
|
if (cred.type === "api_key") {
|
||||||
|
return cred.key;
|
||||||
|
}
|
||||||
|
if (cred.type === "token") {
|
||||||
|
return cred.token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeGoogleModelId(id: string): string {
|
export function normalizeGoogleModelId(id: string): string {
|
||||||
if (id === "gemini-3-pro") return "gemini-3-pro-preview";
|
if (id === "gemini-3-pro") {
|
||||||
if (id === "gemini-3-flash") return "gemini-3-flash-preview";
|
return "gemini-3-pro-preview";
|
||||||
|
}
|
||||||
|
if (id === "gemini-3-flash") {
|
||||||
|
return "gemini-3-flash-preview";
|
||||||
|
}
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +180,9 @@ function normalizeGoogleProvider(provider: ProviderConfig): ProviderConfig {
|
|||||||
let mutated = false;
|
let mutated = false;
|
||||||
const models = provider.models.map((model) => {
|
const models = provider.models.map((model) => {
|
||||||
const nextId = normalizeGoogleModelId(model.id);
|
const nextId = normalizeGoogleModelId(model.id);
|
||||||
if (nextId === model.id) return model;
|
if (nextId === model.id) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
mutated = true;
|
mutated = true;
|
||||||
return { ...model, id: nextId };
|
return { ...model, id: nextId };
|
||||||
});
|
});
|
||||||
@@ -180,7 +194,9 @@ export function normalizeProviders(params: {
|
|||||||
agentDir: string;
|
agentDir: string;
|
||||||
}): ModelsConfig["providers"] {
|
}): ModelsConfig["providers"] {
|
||||||
const { providers } = params;
|
const { providers } = params;
|
||||||
if (!providers) return providers;
|
if (!providers) {
|
||||||
|
return providers;
|
||||||
|
}
|
||||||
const authStore = ensureAuthProfileStore(params.agentDir, {
|
const authStore = ensureAuthProfileStore(params.agentDir, {
|
||||||
allowKeychainPrompt: false,
|
allowKeychainPrompt: false,
|
||||||
});
|
});
|
||||||
@@ -230,7 +246,9 @@ export function normalizeProviders(params: {
|
|||||||
|
|
||||||
if (normalizedKey === "google") {
|
if (normalizedKey === "google") {
|
||||||
const googleNormalized = normalizeGoogleProvider(normalizedProvider);
|
const googleNormalized = normalizeGoogleProvider(normalizedProvider);
|
||||||
if (googleNormalized !== normalizedProvider) mutated = true;
|
if (googleNormalized !== normalizedProvider) {
|
||||||
|
mutated = true;
|
||||||
|
}
|
||||||
normalizedProvider = googleNormalized;
|
normalizedProvider = googleNormalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,7 +446,9 @@ export async function resolveImplicitCopilotProvider(params: {
|
|||||||
const envToken = env.COPILOT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN;
|
const envToken = env.COPILOT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN;
|
||||||
const githubToken = (envToken ?? "").trim();
|
const githubToken = (envToken ?? "").trim();
|
||||||
|
|
||||||
if (!hasProfile && !githubToken) return null;
|
if (!hasProfile && !githubToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let selectedGithubToken = githubToken;
|
let selectedGithubToken = githubToken;
|
||||||
if (!selectedGithubToken && hasProfile) {
|
if (!selectedGithubToken && hasProfile) {
|
||||||
@@ -484,12 +504,18 @@ export async function resolveImplicitBedrockProvider(params: {
|
|||||||
const discoveryConfig = params.config?.models?.bedrockDiscovery;
|
const discoveryConfig = params.config?.models?.bedrockDiscovery;
|
||||||
const enabled = discoveryConfig?.enabled;
|
const enabled = discoveryConfig?.enabled;
|
||||||
const hasAwsCreds = resolveAwsSdkEnvVarName(env) !== undefined;
|
const hasAwsCreds = resolveAwsSdkEnvVarName(env) !== undefined;
|
||||||
if (enabled === false) return null;
|
if (enabled === false) {
|
||||||
if (enabled !== true && !hasAwsCreds) return null;
|
return null;
|
||||||
|
}
|
||||||
|
if (enabled !== true && !hasAwsCreds) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const region = discoveryConfig?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION ?? "us-east-1";
|
const region = discoveryConfig?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION ?? "us-east-1";
|
||||||
const models = await discoverBedrockModels({ region, config: discoveryConfig });
|
const models = await discoverBedrockModels({ region, config: discoveryConfig });
|
||||||
if (models.length === 0) return null;
|
if (models.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
baseUrl: `https://bedrock-runtime.${region}.amazonaws.com`,
|
baseUrl: `https://bedrock-runtime.${region}.amazonaws.com`,
|
||||||
|
|||||||
@@ -79,24 +79,51 @@ describe("models-config", () => {
|
|||||||
await expect(fs.stat(path.join(agentDir, "models.json"))).rejects.toThrow();
|
await expect(fs.stat(path.join(agentDir, "models.json"))).rejects.toThrow();
|
||||||
expect(result.wrote).toBe(false);
|
expect(result.wrote).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
if (previous === undefined) delete process.env.COPILOT_GITHUB_TOKEN;
|
if (previous === undefined) {
|
||||||
else process.env.COPILOT_GITHUB_TOKEN = previous;
|
delete process.env.COPILOT_GITHUB_TOKEN;
|
||||||
if (previousGh === undefined) delete process.env.GH_TOKEN;
|
} else {
|
||||||
else process.env.GH_TOKEN = previousGh;
|
process.env.COPILOT_GITHUB_TOKEN = previous;
|
||||||
if (previousGithub === undefined) delete process.env.GITHUB_TOKEN;
|
}
|
||||||
else process.env.GITHUB_TOKEN = previousGithub;
|
if (previousGh === undefined) {
|
||||||
if (previousKimiCode === undefined) delete process.env.KIMI_API_KEY;
|
delete process.env.GH_TOKEN;
|
||||||
else process.env.KIMI_API_KEY = previousKimiCode;
|
} else {
|
||||||
if (previousMinimax === undefined) delete process.env.MINIMAX_API_KEY;
|
process.env.GH_TOKEN = previousGh;
|
||||||
else process.env.MINIMAX_API_KEY = previousMinimax;
|
}
|
||||||
if (previousMoonshot === undefined) delete process.env.MOONSHOT_API_KEY;
|
if (previousGithub === undefined) {
|
||||||
else process.env.MOONSHOT_API_KEY = previousMoonshot;
|
delete process.env.GITHUB_TOKEN;
|
||||||
if (previousSynthetic === undefined) delete process.env.SYNTHETIC_API_KEY;
|
} else {
|
||||||
else process.env.SYNTHETIC_API_KEY = previousSynthetic;
|
process.env.GITHUB_TOKEN = previousGithub;
|
||||||
if (previousVenice === undefined) delete process.env.VENICE_API_KEY;
|
}
|
||||||
else process.env.VENICE_API_KEY = previousVenice;
|
if (previousKimiCode === undefined) {
|
||||||
if (previousXiaomi === undefined) delete process.env.XIAOMI_API_KEY;
|
delete process.env.KIMI_API_KEY;
|
||||||
else process.env.XIAOMI_API_KEY = previousXiaomi;
|
} else {
|
||||||
|
process.env.KIMI_API_KEY = previousKimiCode;
|
||||||
|
}
|
||||||
|
if (previousMinimax === undefined) {
|
||||||
|
delete process.env.MINIMAX_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.MINIMAX_API_KEY = previousMinimax;
|
||||||
|
}
|
||||||
|
if (previousMoonshot === undefined) {
|
||||||
|
delete process.env.MOONSHOT_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.MOONSHOT_API_KEY = previousMoonshot;
|
||||||
|
}
|
||||||
|
if (previousSynthetic === undefined) {
|
||||||
|
delete process.env.SYNTHETIC_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.SYNTHETIC_API_KEY = previousSynthetic;
|
||||||
|
}
|
||||||
|
if (previousVenice === undefined) {
|
||||||
|
delete process.env.VENICE_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.VENICE_API_KEY = previousVenice;
|
||||||
|
}
|
||||||
|
if (previousXiaomi === undefined) {
|
||||||
|
delete process.env.XIAOMI_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.XIAOMI_API_KEY = previousXiaomi;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -146,8 +173,11 @@ describe("models-config", () => {
|
|||||||
expect(ids).toContain("MiniMax-M2.1");
|
expect(ids).toContain("MiniMax-M2.1");
|
||||||
expect(ids).toContain("MiniMax-VL-01");
|
expect(ids).toContain("MiniMax-VL-01");
|
||||||
} finally {
|
} finally {
|
||||||
if (prevKey === undefined) delete process.env.MINIMAX_API_KEY;
|
if (prevKey === undefined) {
|
||||||
else process.env.MINIMAX_API_KEY = prevKey;
|
delete process.env.MINIMAX_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.MINIMAX_API_KEY = prevKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -179,8 +209,11 @@ describe("models-config", () => {
|
|||||||
const ids = parsed.providers.synthetic?.models?.map((model) => model.id);
|
const ids = parsed.providers.synthetic?.models?.map((model) => model.id);
|
||||||
expect(ids).toContain("hf:MiniMaxAI/MiniMax-M2.1");
|
expect(ids).toContain("hf:MiniMaxAI/MiniMax-M2.1");
|
||||||
} finally {
|
} finally {
|
||||||
if (prevKey === undefined) delete process.env.SYNTHETIC_API_KEY;
|
if (prevKey === undefined) {
|
||||||
else process.env.SYNTHETIC_API_KEY = prevKey;
|
delete process.env.SYNTHETIC_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.SYNTHETIC_API_KEY = prevKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,10 +22,14 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
function mergeProviderModels(implicit: ProviderConfig, explicit: ProviderConfig): ProviderConfig {
|
function mergeProviderModels(implicit: ProviderConfig, explicit: ProviderConfig): ProviderConfig {
|
||||||
const implicitModels = Array.isArray(implicit.models) ? implicit.models : [];
|
const implicitModels = Array.isArray(implicit.models) ? implicit.models : [];
|
||||||
const explicitModels = Array.isArray(explicit.models) ? explicit.models : [];
|
const explicitModels = Array.isArray(explicit.models) ? explicit.models : [];
|
||||||
if (implicitModels.length === 0) return { ...implicit, ...explicit };
|
if (implicitModels.length === 0) {
|
||||||
|
return { ...implicit, ...explicit };
|
||||||
|
}
|
||||||
|
|
||||||
const getId = (model: unknown): string => {
|
const getId = (model: unknown): string => {
|
||||||
if (!model || typeof model !== "object") return "";
|
if (!model || typeof model !== "object") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
const id = (model as { id?: unknown }).id;
|
const id = (model as { id?: unknown }).id;
|
||||||
return typeof id === "string" ? id.trim() : "";
|
return typeof id === "string" ? id.trim() : "";
|
||||||
};
|
};
|
||||||
@@ -35,8 +39,12 @@ function mergeProviderModels(implicit: ProviderConfig, explicit: ProviderConfig)
|
|||||||
...explicitModels,
|
...explicitModels,
|
||||||
...implicitModels.filter((model) => {
|
...implicitModels.filter((model) => {
|
||||||
const id = getId(model);
|
const id = getId(model);
|
||||||
if (!id) return false;
|
if (!id) {
|
||||||
if (seen.has(id)) return false;
|
return false;
|
||||||
|
}
|
||||||
|
if (seen.has(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
seen.add(id);
|
seen.add(id);
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
@@ -56,7 +64,9 @@ function mergeProviders(params: {
|
|||||||
const out: Record<string, ProviderConfig> = params.implicit ? { ...params.implicit } : {};
|
const out: Record<string, ProviderConfig> = params.implicit ? { ...params.implicit } : {};
|
||||||
for (const [key, explicit] of Object.entries(params.explicit ?? {})) {
|
for (const [key, explicit] of Object.entries(params.explicit ?? {})) {
|
||||||
const providerKey = key.trim();
|
const providerKey = key.trim();
|
||||||
if (!providerKey) continue;
|
if (!providerKey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const implicit = out[providerKey];
|
const implicit = out[providerKey];
|
||||||
out[providerKey] = implicit ? mergeProviderModels(implicit, explicit) : explicit;
|
out[providerKey] = implicit ? mergeProviderModels(implicit, explicit) : explicit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,12 +100,21 @@ describe("models-config", () => {
|
|||||||
expect.objectContaining({ githubToken: "alpha-token" }),
|
expect.objectContaining({ githubToken: "alpha-token" }),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
if (previous === undefined) delete process.env.COPILOT_GITHUB_TOKEN;
|
if (previous === undefined) {
|
||||||
else process.env.COPILOT_GITHUB_TOKEN = previous;
|
delete process.env.COPILOT_GITHUB_TOKEN;
|
||||||
if (previousGh === undefined) delete process.env.GH_TOKEN;
|
} else {
|
||||||
else process.env.GH_TOKEN = previousGh;
|
process.env.COPILOT_GITHUB_TOKEN = previous;
|
||||||
if (previousGithub === undefined) delete process.env.GITHUB_TOKEN;
|
}
|
||||||
else process.env.GITHUB_TOKEN = previousGithub;
|
if (previousGh === undefined) {
|
||||||
|
delete process.env.GH_TOKEN;
|
||||||
|
} else {
|
||||||
|
process.env.GH_TOKEN = previousGh;
|
||||||
|
}
|
||||||
|
if (previousGithub === undefined) {
|
||||||
|
delete process.env.GITHUB_TOKEN;
|
||||||
|
} else {
|
||||||
|
process.env.GITHUB_TOKEN = previousGithub;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ const describeLive = LIVE ? describe : describe.skip;
|
|||||||
|
|
||||||
function parseProviderFilter(raw?: string): Set<string> | null {
|
function parseProviderFilter(raw?: string): Set<string> | null {
|
||||||
const trimmed = raw?.trim();
|
const trimmed = raw?.trim();
|
||||||
if (!trimmed || trimmed === "all") return null;
|
if (!trimmed || trimmed === "all") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const ids = trimmed
|
const ids = trimmed
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
@@ -33,7 +35,9 @@ function parseProviderFilter(raw?: string): Set<string> | null {
|
|||||||
|
|
||||||
function parseModelFilter(raw?: string): Set<string> | null {
|
function parseModelFilter(raw?: string): Set<string> | null {
|
||||||
const trimmed = raw?.trim();
|
const trimmed = raw?.trim();
|
||||||
if (!trimmed || trimmed === "all") return null;
|
if (!trimmed || trimmed === "all") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const ids = trimmed
|
const ids = trimmed
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
@@ -47,19 +51,35 @@ function logProgress(message: string): void {
|
|||||||
|
|
||||||
function isGoogleModelNotFoundError(err: unknown): boolean {
|
function isGoogleModelNotFoundError(err: unknown): boolean {
|
||||||
const msg = String(err);
|
const msg = String(err);
|
||||||
if (!/not found/i.test(msg)) return false;
|
if (!/not found/i.test(msg)) {
|
||||||
if (/models\/.+ is not found for api version/i.test(msg)) return true;
|
return false;
|
||||||
if (/"status"\\s*:\\s*"NOT_FOUND"/.test(msg)) return true;
|
}
|
||||||
if (/"code"\\s*:\\s*404/.test(msg)) return true;
|
if (/models\/.+ is not found for api version/i.test(msg)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (/"status"\\s*:\\s*"NOT_FOUND"/.test(msg)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (/"code"\\s*:\\s*404/.test(msg)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isModelNotFoundErrorMessage(raw: string): boolean {
|
function isModelNotFoundErrorMessage(raw: string): boolean {
|
||||||
const msg = raw.trim();
|
const msg = raw.trim();
|
||||||
if (!msg) return false;
|
if (!msg) {
|
||||||
if (/\b404\b/.test(msg) && /not[_-]?found/i.test(msg)) return true;
|
return false;
|
||||||
if (/not_found_error/i.test(msg)) return true;
|
}
|
||||||
if (/model:\s*[a-z0-9._-]+/i.test(msg) && /not[_-]?found/i.test(msg)) return true;
|
if (/\b404\b/.test(msg) && /not[_-]?found/i.test(msg)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (/not_found_error/i.test(msg)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (/model:\s*[a-z0-9._-]+/i.test(msg) && /not[_-]?found/i.test(msg)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +94,9 @@ function isInstructionsRequiredError(raw: string): boolean {
|
|||||||
|
|
||||||
function toInt(value: string | undefined, fallback: number): number {
|
function toInt(value: string | undefined, fallback: number): number {
|
||||||
const trimmed = value?.trim();
|
const trimmed = value?.trim();
|
||||||
if (!trimmed) return fallback;
|
if (!trimmed) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
const parsed = Number.parseInt(trimmed, 10);
|
const parsed = Number.parseInt(trimmed, 10);
|
||||||
return Number.isFinite(parsed) ? parsed : fallback;
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
}
|
}
|
||||||
@@ -82,10 +104,14 @@ function toInt(value: string | undefined, fallback: number): number {
|
|||||||
function resolveTestReasoning(
|
function resolveTestReasoning(
|
||||||
model: Model<Api>,
|
model: Model<Api>,
|
||||||
): "minimal" | "low" | "medium" | "high" | "xhigh" | undefined {
|
): "minimal" | "low" | "medium" | "high" | "xhigh" | undefined {
|
||||||
if (!model.reasoning) return undefined;
|
if (!model.reasoning) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const id = model.id.toLowerCase();
|
const id = model.id.toLowerCase();
|
||||||
if (model.provider === "openai" || model.provider === "openai-codex") {
|
if (model.provider === "openai" || model.provider === "openai-codex") {
|
||||||
if (id.includes("pro")) return "high";
|
if (id.includes("pro")) {
|
||||||
|
return "high";
|
||||||
|
}
|
||||||
return "medium";
|
return "medium";
|
||||||
}
|
}
|
||||||
return "low";
|
return "low";
|
||||||
@@ -142,7 +168,9 @@ async function completeOkWithRetry(params: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const first = await runOnce();
|
const first = await runOnce();
|
||||||
if (first.text.length > 0) return first;
|
if (first.text.length > 0) {
|
||||||
|
return first;
|
||||||
|
}
|
||||||
return await runOnce();
|
return await runOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,9 +213,13 @@ describeLive("live models (profile keys)", () => {
|
|||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
for (const model of models) {
|
for (const model of models) {
|
||||||
if (providers && !providers.has(model.provider)) continue;
|
if (providers && !providers.has(model.provider)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const id = `${model.provider}/${model.id}`;
|
const id = `${model.provider}/${model.id}`;
|
||||||
if (filter && !filter.has(id)) continue;
|
if (filter && !filter.has(id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!filter && useModern) {
|
if (!filter && useModern) {
|
||||||
if (!isModernModelRef({ provider: model.provider, id: model.id })) {
|
if (!isModernModelRef({ provider: model.provider, id: model.id })) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -25,11 +25,18 @@ function installFailingFetchCapture() {
|
|||||||
const fetchImpl: typeof fetch = async (_input, init) => {
|
const fetchImpl: typeof fetch = async (_input, init) => {
|
||||||
const rawBody = init?.body;
|
const rawBody = init?.body;
|
||||||
const bodyText = (() => {
|
const bodyText = (() => {
|
||||||
if (!rawBody) return "";
|
if (!rawBody) {
|
||||||
if (typeof rawBody === "string") return rawBody;
|
return "";
|
||||||
if (rawBody instanceof Uint8Array) return Buffer.from(rawBody).toString("utf8");
|
}
|
||||||
if (rawBody instanceof ArrayBuffer)
|
if (typeof rawBody === "string") {
|
||||||
|
return rawBody;
|
||||||
|
}
|
||||||
|
if (rawBody instanceof Uint8Array) {
|
||||||
|
return Buffer.from(rawBody).toString("utf8");
|
||||||
|
}
|
||||||
|
if (rawBody instanceof ArrayBuffer) {
|
||||||
return Buffer.from(new Uint8Array(rawBody)).toString("utf8");
|
return Buffer.from(new Uint8Array(rawBody)).toString("utf8");
|
||||||
|
}
|
||||||
return String(rawBody);
|
return String(rawBody);
|
||||||
})();
|
})();
|
||||||
lastBody = bodyText ? (JSON.parse(bodyText) as unknown) : undefined;
|
lastBody = bodyText ? (JSON.parse(bodyText) as unknown) : undefined;
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ describe("gateway tool", () => {
|
|||||||
config: { commands: { restart: true } },
|
config: { commands: { restart: true } },
|
||||||
}).find((candidate) => candidate.name === "gateway");
|
}).find((candidate) => candidate.name === "gateway");
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing gateway tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing gateway tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call1", {
|
const result = await tool.execute("call1", {
|
||||||
action: "restart",
|
action: "restart",
|
||||||
@@ -78,7 +80,9 @@ describe("gateway tool", () => {
|
|||||||
agentSessionKey: "agent:main:whatsapp:dm:+15555550123",
|
agentSessionKey: "agent:main:whatsapp:dm:+15555550123",
|
||||||
}).find((candidate) => candidate.name === "gateway");
|
}).find((candidate) => candidate.name === "gateway");
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing gateway tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing gateway tool");
|
||||||
|
}
|
||||||
|
|
||||||
const raw = '{\n agents: { defaults: { workspace: "~/openclaw" } }\n}\n';
|
const raw = '{\n agents: { defaults: { workspace: "~/openclaw" } }\n}\n';
|
||||||
await tool.execute("call2", {
|
await tool.execute("call2", {
|
||||||
@@ -104,7 +108,9 @@ describe("gateway tool", () => {
|
|||||||
agentSessionKey: "agent:main:whatsapp:dm:+15555550123",
|
agentSessionKey: "agent:main:whatsapp:dm:+15555550123",
|
||||||
}).find((candidate) => candidate.name === "gateway");
|
}).find((candidate) => candidate.name === "gateway");
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing gateway tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing gateway tool");
|
||||||
|
}
|
||||||
|
|
||||||
const raw = '{\n channels: { telegram: { groups: { "*": { requireMention: false } } } }\n}\n';
|
const raw = '{\n channels: { telegram: { groups: { "*": { requireMention: false } } } }\n}\n';
|
||||||
await tool.execute("call4", {
|
await tool.execute("call4", {
|
||||||
@@ -130,7 +136,9 @@ describe("gateway tool", () => {
|
|||||||
agentSessionKey: "agent:main:whatsapp:dm:+15555550123",
|
agentSessionKey: "agent:main:whatsapp:dm:+15555550123",
|
||||||
}).find((candidate) => candidate.name === "gateway");
|
}).find((candidate) => candidate.name === "gateway");
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing gateway tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing gateway tool");
|
||||||
|
}
|
||||||
|
|
||||||
await tool.execute("call3", {
|
await tool.execute("call3", {
|
||||||
action: "update.run",
|
action: "update.run",
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ describe("agents_list", () => {
|
|||||||
const tool = createOpenClawTools({
|
const tool = createOpenClawTools({
|
||||||
agentSessionKey: "main",
|
agentSessionKey: "main",
|
||||||
}).find((candidate) => candidate.name === "agents_list");
|
}).find((candidate) => candidate.name === "agents_list");
|
||||||
if (!tool) throw new Error("missing agents_list tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing agents_list tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call1", {});
|
const result = await tool.execute("call1", {});
|
||||||
expect(result.details).toMatchObject({
|
expect(result.details).toMatchObject({
|
||||||
@@ -70,7 +72,9 @@ describe("agents_list", () => {
|
|||||||
const tool = createOpenClawTools({
|
const tool = createOpenClawTools({
|
||||||
agentSessionKey: "main",
|
agentSessionKey: "main",
|
||||||
}).find((candidate) => candidate.name === "agents_list");
|
}).find((candidate) => candidate.name === "agents_list");
|
||||||
if (!tool) throw new Error("missing agents_list tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing agents_list tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call2", {});
|
const result = await tool.execute("call2", {});
|
||||||
const agents = (
|
const agents = (
|
||||||
@@ -110,7 +114,9 @@ describe("agents_list", () => {
|
|||||||
const tool = createOpenClawTools({
|
const tool = createOpenClawTools({
|
||||||
agentSessionKey: "main",
|
agentSessionKey: "main",
|
||||||
}).find((candidate) => candidate.name === "agents_list");
|
}).find((candidate) => candidate.name === "agents_list");
|
||||||
if (!tool) throw new Error("missing agents_list tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing agents_list tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call3", {});
|
const result = await tool.execute("call3", {});
|
||||||
expect(result.details).toMatchObject({
|
expect(result.details).toMatchObject({
|
||||||
@@ -145,7 +151,9 @@ describe("agents_list", () => {
|
|||||||
const tool = createOpenClawTools({
|
const tool = createOpenClawTools({
|
||||||
agentSessionKey: "main",
|
agentSessionKey: "main",
|
||||||
}).find((candidate) => candidate.name === "agents_list");
|
}).find((candidate) => candidate.name === "agents_list");
|
||||||
if (!tool) throw new Error("missing agents_list tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing agents_list tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call4", {});
|
const result = await tool.execute("call4", {});
|
||||||
const agents = (
|
const agents = (
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ describe("nodes camera_snap", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes");
|
const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes");
|
||||||
if (!tool) throw new Error("missing nodes tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing nodes tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call1", {
|
const result = await tool.execute("call1", {
|
||||||
action: "camera_snap",
|
action: "camera_snap",
|
||||||
@@ -73,7 +75,9 @@ describe("nodes camera_snap", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes");
|
const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes");
|
||||||
if (!tool) throw new Error("missing nodes tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing nodes tool");
|
||||||
|
}
|
||||||
|
|
||||||
await tool.execute("call1", {
|
await tool.execute("call1", {
|
||||||
action: "camera_snap",
|
action: "camera_snap",
|
||||||
@@ -114,7 +118,9 @@ describe("nodes run", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes");
|
const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes");
|
||||||
if (!tool) throw new Error("missing nodes tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing nodes tool");
|
||||||
|
}
|
||||||
|
|
||||||
await tool.execute("call1", {
|
await tool.execute("call1", {
|
||||||
action: "run",
|
action: "run",
|
||||||
|
|||||||
@@ -94,7 +94,9 @@ describe("session_status tool", () => {
|
|||||||
(candidate) => candidate.name === "session_status",
|
(candidate) => candidate.name === "session_status",
|
||||||
);
|
);
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing session_status tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing session_status tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call1", {});
|
const result = await tool.execute("call1", {});
|
||||||
const details = result.details as { ok?: boolean; statusText?: string };
|
const details = result.details as { ok?: boolean; statusText?: string };
|
||||||
@@ -115,7 +117,9 @@ describe("session_status tool", () => {
|
|||||||
(candidate) => candidate.name === "session_status",
|
(candidate) => candidate.name === "session_status",
|
||||||
);
|
);
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing session_status tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing session_status tool");
|
||||||
|
}
|
||||||
|
|
||||||
await expect(tool.execute("call2", { sessionKey: "nope" })).rejects.toThrow(
|
await expect(tool.execute("call2", { sessionKey: "nope" })).rejects.toThrow(
|
||||||
"Unknown sessionId",
|
"Unknown sessionId",
|
||||||
@@ -138,7 +142,9 @@ describe("session_status tool", () => {
|
|||||||
(candidate) => candidate.name === "session_status",
|
(candidate) => candidate.name === "session_status",
|
||||||
);
|
);
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing session_status tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing session_status tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call3", { sessionKey: sessionId });
|
const result = await tool.execute("call3", { sessionKey: sessionId });
|
||||||
const details = result.details as { ok?: boolean; sessionKey?: string };
|
const details = result.details as { ok?: boolean; sessionKey?: string };
|
||||||
@@ -160,7 +166,9 @@ describe("session_status tool", () => {
|
|||||||
(candidate) => candidate.name === "session_status",
|
(candidate) => candidate.name === "session_status",
|
||||||
);
|
);
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing session_status tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing session_status tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call4", { sessionKey: "temp:slug-generator" });
|
const result = await tool.execute("call4", { sessionKey: "temp:slug-generator" });
|
||||||
const details = result.details as { ok?: boolean; sessionKey?: string };
|
const details = result.details as { ok?: boolean; sessionKey?: string };
|
||||||
@@ -182,7 +190,9 @@ describe("session_status tool", () => {
|
|||||||
(candidate) => candidate.name === "session_status",
|
(candidate) => candidate.name === "session_status",
|
||||||
);
|
);
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing session_status tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing session_status tool");
|
||||||
|
}
|
||||||
|
|
||||||
await expect(tool.execute("call5", { sessionKey: "agent:other:main" })).rejects.toThrow(
|
await expect(tool.execute("call5", { sessionKey: "agent:other:main" })).rejects.toThrow(
|
||||||
"Agent-to-agent status is disabled",
|
"Agent-to-agent status is disabled",
|
||||||
@@ -222,7 +232,9 @@ describe("session_status tool", () => {
|
|||||||
(candidate) => candidate.name === "session_status",
|
(candidate) => candidate.name === "session_status",
|
||||||
);
|
);
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing session_status tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing session_status tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call6", { sessionKey: "main" });
|
const result = await tool.execute("call6", { sessionKey: "main" });
|
||||||
const details = result.details as { ok?: boolean; sessionKey?: string };
|
const details = result.details as { ok?: boolean; sessionKey?: string };
|
||||||
@@ -247,7 +259,9 @@ describe("session_status tool", () => {
|
|||||||
(candidate) => candidate.name === "session_status",
|
(candidate) => candidate.name === "session_status",
|
||||||
);
|
);
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing session_status tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing session_status tool");
|
||||||
|
}
|
||||||
|
|
||||||
await tool.execute("call3", { model: "default" });
|
await tool.execute("call3", { model: "default" });
|
||||||
expect(updateSessionStoreMock).toHaveBeenCalled();
|
expect(updateSessionStoreMock).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ describe("sessions tools", () => {
|
|||||||
const byName = (name: string) => {
|
const byName = (name: string) => {
|
||||||
const tool = tools.find((candidate) => candidate.name === name);
|
const tool = tools.find((candidate) => candidate.name === name);
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error(`missing ${name} tool`);
|
if (!tool) {
|
||||||
|
throw new Error(`missing ${name} tool`);
|
||||||
|
}
|
||||||
return tool;
|
return tool;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,7 +58,9 @@ describe("sessions tools", () => {
|
|||||||
const properties = schema.properties ?? {};
|
const properties = schema.properties ?? {};
|
||||||
const value = properties[prop] as { type?: unknown } | undefined;
|
const value = properties[prop] as { type?: unknown } | undefined;
|
||||||
expect(value).toBeDefined();
|
expect(value).toBeDefined();
|
||||||
if (!value) throw new Error(`missing ${toolName} schema prop: ${prop}`);
|
if (!value) {
|
||||||
|
throw new Error(`missing ${toolName} schema prop: ${prop}`);
|
||||||
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,7 +124,9 @@ describe("sessions tools", () => {
|
|||||||
|
|
||||||
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_list");
|
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_list");
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing sessions_list tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_list tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call1", { messageLimit: 1 });
|
const result = await tool.execute("call1", { messageLimit: 1 });
|
||||||
const details = result.details as {
|
const details = result.details as {
|
||||||
@@ -157,7 +163,9 @@ describe("sessions tools", () => {
|
|||||||
|
|
||||||
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_history");
|
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_history");
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing sessions_history tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_history tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call3", { sessionKey: "main" });
|
const result = await tool.execute("call3", { sessionKey: "main" });
|
||||||
const details = result.details as { messages?: unknown[] };
|
const details = result.details as { messages?: unknown[] };
|
||||||
@@ -193,7 +201,9 @@ describe("sessions tools", () => {
|
|||||||
|
|
||||||
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_history");
|
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_history");
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing sessions_history tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_history tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call5", { sessionKey: sessionId });
|
const result = await tool.execute("call5", { sessionKey: sessionId });
|
||||||
const details = result.details as { messages?: unknown[] };
|
const details = result.details as { messages?: unknown[] };
|
||||||
@@ -220,7 +230,9 @@ describe("sessions tools", () => {
|
|||||||
|
|
||||||
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_history");
|
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_history");
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing sessions_history tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_history tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call6", { sessionKey: sessionId });
|
const result = await tool.execute("call6", { sessionKey: sessionId });
|
||||||
const details = result.details as { status?: string; error?: string };
|
const details = result.details as { status?: string; error?: string };
|
||||||
@@ -295,7 +307,9 @@ describe("sessions tools", () => {
|
|||||||
agentChannel: "discord",
|
agentChannel: "discord",
|
||||||
}).find((candidate) => candidate.name === "sessions_send");
|
}).find((candidate) => candidate.name === "sessions_send");
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing sessions_send tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_send tool");
|
||||||
|
}
|
||||||
|
|
||||||
const fire = await tool.execute("call5", {
|
const fire = await tool.execute("call5", {
|
||||||
sessionKey: "main",
|
sessionKey: "main",
|
||||||
@@ -395,7 +409,9 @@ describe("sessions tools", () => {
|
|||||||
agentChannel: "discord",
|
agentChannel: "discord",
|
||||||
}).find((candidate) => candidate.name === "sessions_send");
|
}).find((candidate) => candidate.name === "sessions_send");
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing sessions_send tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_send tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call7", {
|
const result = await tool.execute("call7", {
|
||||||
sessionKey: sessionId,
|
sessionKey: sessionId,
|
||||||
@@ -485,7 +501,9 @@ describe("sessions tools", () => {
|
|||||||
agentChannel: "discord",
|
agentChannel: "discord",
|
||||||
}).find((candidate) => candidate.name === "sessions_send");
|
}).find((candidate) => candidate.name === "sessions_send");
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing sessions_send tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_send tool");
|
||||||
|
}
|
||||||
|
|
||||||
const waited = await tool.execute("call7", {
|
const waited = await tool.execute("call7", {
|
||||||
sessionKey: targetKey,
|
sessionKey: targetKey,
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "main",
|
agentSessionKey: "main",
|
||||||
agentChannel: "whatsapp",
|
agentChannel: "whatsapp",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call7", {
|
const result = await tool.execute("call7", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
@@ -124,7 +126,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "main",
|
agentSessionKey: "main",
|
||||||
agentChannel: "whatsapp",
|
agentChannel: "whatsapp",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call8", {
|
const result = await tool.execute("call8", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
|
|||||||
@@ -93,7 +93,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "discord:group:req",
|
agentSessionKey: "discord:group:req",
|
||||||
agentChannel: "discord",
|
agentChannel: "discord",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call1b", {
|
const result = await tool.execute("call1b", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "discord:group:req",
|
agentSessionKey: "discord:group:req",
|
||||||
agentSurface: "discord",
|
agentSurface: "discord",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call3", {
|
const result = await tool.execute("call3", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
@@ -112,7 +114,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "discord:group:req",
|
agentSessionKey: "discord:group:req",
|
||||||
agentChannel: "discord",
|
agentChannel: "discord",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call-thinking", {
|
const result = await tool.execute("call-thinking", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
@@ -143,7 +147,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "discord:group:req",
|
agentSessionKey: "discord:group:req",
|
||||||
agentChannel: "discord",
|
agentChannel: "discord",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call-thinking-invalid", {
|
const result = await tool.execute("call-thinking-invalid", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
@@ -180,7 +186,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "agent:main:main",
|
agentSessionKey: "agent:main:main",
|
||||||
agentChannel: "discord",
|
agentChannel: "discord",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call-default-model", {
|
const result = await tool.execute("call-default-model", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "main",
|
agentSessionKey: "main",
|
||||||
agentChannel: "whatsapp",
|
agentChannel: "whatsapp",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call10", {
|
const result = await tool.execute("call10", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
@@ -111,7 +113,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "main",
|
agentSessionKey: "main",
|
||||||
agentChannel: "whatsapp",
|
agentChannel: "whatsapp",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call9", {
|
const result = await tool.execute("call9", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
@@ -175,7 +179,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "discord:group:req",
|
agentSessionKey: "discord:group:req",
|
||||||
agentChannel: "discord",
|
agentChannel: "discord",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call1", {
|
const result = await tool.execute("call1", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
@@ -187,7 +193,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
runId: "run-1",
|
runId: "run-1",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!childRunId) throw new Error("missing child runId");
|
if (!childRunId) {
|
||||||
|
throw new Error("missing child runId");
|
||||||
|
}
|
||||||
emitAgentEvent({
|
emitAgentEvent({
|
||||||
runId: childRunId,
|
runId: childRunId,
|
||||||
stream: "lifecycle",
|
stream: "lifecycle",
|
||||||
@@ -277,7 +285,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentChannel: "whatsapp",
|
agentChannel: "whatsapp",
|
||||||
agentAccountId: "kev",
|
agentAccountId: "kev",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call2", {
|
const result = await tool.execute("call2", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
@@ -289,7 +299,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
runId: "run-1",
|
runId: "run-1",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!childRunId) throw new Error("missing child runId");
|
if (!childRunId) {
|
||||||
|
throw new Error("missing child runId");
|
||||||
|
}
|
||||||
emitAgentEvent({
|
emitAgentEvent({
|
||||||
runId: childRunId,
|
runId: childRunId,
|
||||||
stream: "lifecycle",
|
stream: "lifecycle",
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "agent:research:main",
|
agentSessionKey: "agent:research:main",
|
||||||
agentChannel: "discord",
|
agentChannel: "discord",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call-agent-model", {
|
const result = await tool.execute("call-agent-model", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
@@ -112,7 +114,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "main",
|
agentSessionKey: "main",
|
||||||
agentChannel: "whatsapp",
|
agentChannel: "whatsapp",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call4", {
|
const result = await tool.execute("call4", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
@@ -147,7 +151,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "main",
|
agentSessionKey: "main",
|
||||||
agentChannel: "whatsapp",
|
agentChannel: "whatsapp",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call5", {
|
const result = await tool.execute("call5", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "main",
|
agentSessionKey: "main",
|
||||||
agentChannel: "whatsapp",
|
agentChannel: "whatsapp",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call2", {
|
const result = await tool.execute("call2", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
@@ -111,7 +113,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
runId: "run-1",
|
runId: "run-1",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!childRunId) throw new Error("missing child runId");
|
if (!childRunId) {
|
||||||
|
throw new Error("missing child runId");
|
||||||
|
}
|
||||||
emitAgentEvent({
|
emitAgentEvent({
|
||||||
runId: childRunId,
|
runId: childRunId,
|
||||||
stream: "lifecycle",
|
stream: "lifecycle",
|
||||||
@@ -159,7 +163,9 @@ describe("openclaw-tools: subagents", () => {
|
|||||||
agentSessionKey: "main",
|
agentSessionKey: "main",
|
||||||
agentChannel: "whatsapp",
|
agentChannel: "whatsapp",
|
||||||
}).find((candidate) => candidate.name === "sessions_spawn");
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
if (!tool) throw new Error("missing sessions_spawn tool");
|
if (!tool) {
|
||||||
|
throw new Error("missing sessions_spawn tool");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tool.execute("call6", {
|
const result = await tool.execute("call6", {
|
||||||
task: "do thing",
|
task: "do thing",
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ export class EmbeddedBlockChunker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
append(text: string) {
|
append(text: string) {
|
||||||
if (!text) return;
|
if (!text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.#buffer += text;
|
this.#buffer += text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +49,9 @@ export class EmbeddedBlockChunker {
|
|||||||
const { force, emit } = params;
|
const { force, emit } = params;
|
||||||
const minChars = Math.max(1, Math.floor(this.#chunking.minChars));
|
const minChars = Math.max(1, Math.floor(this.#chunking.minChars));
|
||||||
const maxChars = Math.max(minChars, Math.floor(this.#chunking.maxChars));
|
const maxChars = Math.max(minChars, Math.floor(this.#chunking.maxChars));
|
||||||
if (this.#buffer.length < minChars && !force) return;
|
if (this.#buffer.length < minChars && !force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (force && this.#buffer.length <= maxChars) {
|
if (force && this.#buffer.length <= maxChars) {
|
||||||
if (this.#buffer.trim().length > 0) {
|
if (this.#buffer.trim().length > 0) {
|
||||||
@@ -103,14 +107,20 @@ export class EmbeddedBlockChunker {
|
|||||||
this.#buffer = stripLeadingNewlines(this.#buffer.slice(nextStart));
|
this.#buffer = stripLeadingNewlines(this.#buffer.slice(nextStart));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#buffer.length < minChars && !force) return;
|
if (this.#buffer.length < minChars && !force) {
|
||||||
if (this.#buffer.length < maxChars && !force) return;
|
return;
|
||||||
|
}
|
||||||
|
if (this.#buffer.length < maxChars && !force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#pickSoftBreakIndex(buffer: string, minCharsOverride?: number): BreakResult {
|
#pickSoftBreakIndex(buffer: string, minCharsOverride?: number): BreakResult {
|
||||||
const minChars = Math.max(1, Math.floor(minCharsOverride ?? this.#chunking.minChars));
|
const minChars = Math.max(1, Math.floor(minCharsOverride ?? this.#chunking.minChars));
|
||||||
if (buffer.length < minChars) return { index: -1 };
|
if (buffer.length < minChars) {
|
||||||
|
return { index: -1 };
|
||||||
|
}
|
||||||
const fenceSpans = parseFenceSpans(buffer);
|
const fenceSpans = parseFenceSpans(buffer);
|
||||||
const preference = this.#chunking.breakPreference ?? "paragraph";
|
const preference = this.#chunking.breakPreference ?? "paragraph";
|
||||||
|
|
||||||
@@ -119,8 +129,12 @@ export class EmbeddedBlockChunker {
|
|||||||
while (paragraphIdx !== -1) {
|
while (paragraphIdx !== -1) {
|
||||||
const candidates = [paragraphIdx, paragraphIdx + 1];
|
const candidates = [paragraphIdx, paragraphIdx + 1];
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (candidate < minChars) continue;
|
if (candidate < minChars) {
|
||||||
if (candidate < 0 || candidate >= buffer.length) continue;
|
continue;
|
||||||
|
}
|
||||||
|
if (candidate < 0 || candidate >= buffer.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (isSafeFenceBreak(fenceSpans, candidate)) {
|
if (isSafeFenceBreak(fenceSpans, candidate)) {
|
||||||
return { index: candidate };
|
return { index: candidate };
|
||||||
}
|
}
|
||||||
@@ -144,13 +158,17 @@ export class EmbeddedBlockChunker {
|
|||||||
let sentenceIdx = -1;
|
let sentenceIdx = -1;
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const at = match.index ?? -1;
|
const at = match.index ?? -1;
|
||||||
if (at < minChars) continue;
|
if (at < minChars) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const candidate = at + 1;
|
const candidate = at + 1;
|
||||||
if (isSafeFenceBreak(fenceSpans, candidate)) {
|
if (isSafeFenceBreak(fenceSpans, candidate)) {
|
||||||
sentenceIdx = candidate;
|
sentenceIdx = candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sentenceIdx >= minChars) return { index: sentenceIdx };
|
if (sentenceIdx >= minChars) {
|
||||||
|
return { index: sentenceIdx };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { index: -1 };
|
return { index: -1 };
|
||||||
@@ -159,7 +177,9 @@ export class EmbeddedBlockChunker {
|
|||||||
#pickBreakIndex(buffer: string, minCharsOverride?: number): BreakResult {
|
#pickBreakIndex(buffer: string, minCharsOverride?: number): BreakResult {
|
||||||
const minChars = Math.max(1, Math.floor(minCharsOverride ?? this.#chunking.minChars));
|
const minChars = Math.max(1, Math.floor(minCharsOverride ?? this.#chunking.minChars));
|
||||||
const maxChars = Math.max(minChars, Math.floor(this.#chunking.maxChars));
|
const maxChars = Math.max(minChars, Math.floor(this.#chunking.maxChars));
|
||||||
if (buffer.length < minChars) return { index: -1 };
|
if (buffer.length < minChars) {
|
||||||
|
return { index: -1 };
|
||||||
|
}
|
||||||
const window = buffer.slice(0, Math.min(maxChars, buffer.length));
|
const window = buffer.slice(0, Math.min(maxChars, buffer.length));
|
||||||
const fenceSpans = parseFenceSpans(buffer);
|
const fenceSpans = parseFenceSpans(buffer);
|
||||||
|
|
||||||
@@ -169,8 +189,12 @@ export class EmbeddedBlockChunker {
|
|||||||
while (paragraphIdx >= minChars) {
|
while (paragraphIdx >= minChars) {
|
||||||
const candidates = [paragraphIdx, paragraphIdx + 1];
|
const candidates = [paragraphIdx, paragraphIdx + 1];
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (candidate < minChars) continue;
|
if (candidate < minChars) {
|
||||||
if (candidate < 0 || candidate >= buffer.length) continue;
|
continue;
|
||||||
|
}
|
||||||
|
if (candidate < 0 || candidate >= buffer.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (isSafeFenceBreak(fenceSpans, candidate)) {
|
if (isSafeFenceBreak(fenceSpans, candidate)) {
|
||||||
return { index: candidate };
|
return { index: candidate };
|
||||||
}
|
}
|
||||||
@@ -194,13 +218,17 @@ export class EmbeddedBlockChunker {
|
|||||||
let sentenceIdx = -1;
|
let sentenceIdx = -1;
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const at = match.index ?? -1;
|
const at = match.index ?? -1;
|
||||||
if (at < minChars) continue;
|
if (at < minChars) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const candidate = at + 1;
|
const candidate = at + 1;
|
||||||
if (isSafeFenceBreak(fenceSpans, candidate)) {
|
if (isSafeFenceBreak(fenceSpans, candidate)) {
|
||||||
sentenceIdx = candidate;
|
sentenceIdx = candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sentenceIdx >= minChars) return { index: sentenceIdx };
|
if (sentenceIdx >= minChars) {
|
||||||
|
return { index: sentenceIdx };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preference === "newline" && buffer.length < maxChars) {
|
if (preference === "newline" && buffer.length < maxChars) {
|
||||||
@@ -214,7 +242,9 @@ export class EmbeddedBlockChunker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.length >= maxChars) {
|
if (buffer.length >= maxChars) {
|
||||||
if (isSafeFenceBreak(fenceSpans, maxChars)) return { index: maxChars };
|
if (isSafeFenceBreak(fenceSpans, maxChars)) {
|
||||||
|
return { index: maxChars };
|
||||||
|
}
|
||||||
const fence = findFenceSpanAt(fenceSpans, maxChars);
|
const fence = findFenceSpanAt(fenceSpans, maxChars);
|
||||||
if (fence) {
|
if (fence) {
|
||||||
return {
|
return {
|
||||||
@@ -234,6 +264,8 @@ export class EmbeddedBlockChunker {
|
|||||||
|
|
||||||
function stripLeadingNewlines(value: string): string {
|
function stripLeadingNewlines(value: string): string {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
while (i < value.length && value[i] === "\n") i++;
|
while (i < value.length && value[i] === "\n") {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
return i > 0 ? value.slice(i) : value;
|
return i > 0 ? value.slice(i) : value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,19 @@ type ThoughtSignatureSanitizeOptions = {
|
|||||||
|
|
||||||
function isBase64Signature(value: string): boolean {
|
function isBase64Signature(value: string): boolean {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (!trimmed) return false;
|
if (!trimmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const compact = trimmed.replace(/\s+/g, "");
|
const compact = trimmed.replace(/\s+/g, "");
|
||||||
if (!/^[A-Za-z0-9+/=_-]+$/.test(compact)) return false;
|
if (!/^[A-Za-z0-9+/=_-]+$/.test(compact)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const isUrl = compact.includes("-") || compact.includes("_");
|
const isUrl = compact.includes("-") || compact.includes("_");
|
||||||
try {
|
try {
|
||||||
const buf = Buffer.from(compact, isUrl ? "base64url" : "base64");
|
const buf = Buffer.from(compact, isUrl ? "base64url" : "base64");
|
||||||
if (buf.length === 0) return false;
|
if (buf.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const encoded = buf.toString(isUrl ? "base64url" : "base64");
|
const encoded = buf.toString(isUrl ? "base64url" : "base64");
|
||||||
const normalize = (input: string) => input.replace(/=+$/g, "");
|
const normalize = (input: string) => input.replace(/=+$/g, "");
|
||||||
return normalize(encoded) === normalize(compact);
|
return normalize(encoded) === normalize(compact);
|
||||||
@@ -45,7 +51,9 @@ export function stripThoughtSignatures<T>(
|
|||||||
content: T,
|
content: T,
|
||||||
options?: ThoughtSignatureSanitizeOptions,
|
options?: ThoughtSignatureSanitizeOptions,
|
||||||
): T {
|
): T {
|
||||||
if (!Array.isArray(content)) return content;
|
if (!Array.isArray(content)) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
const allowBase64Only = options?.allowBase64Only ?? false;
|
const allowBase64Only = options?.allowBase64Only ?? false;
|
||||||
const includeCamelCase = options?.includeCamelCase ?? false;
|
const includeCamelCase = options?.includeCamelCase ?? false;
|
||||||
const shouldStripSignature = (value: unknown): boolean => {
|
const shouldStripSignature = (value: unknown): boolean => {
|
||||||
@@ -55,7 +63,9 @@ export function stripThoughtSignatures<T>(
|
|||||||
return typeof value !== "string" || !isBase64Signature(value);
|
return typeof value !== "string" || !isBase64Signature(value);
|
||||||
};
|
};
|
||||||
return content.map((block) => {
|
return content.map((block) => {
|
||||||
if (!block || typeof block !== "object") return block;
|
if (!block || typeof block !== "object") {
|
||||||
|
return block;
|
||||||
|
}
|
||||||
const rec = block as ContentBlockWithSignature;
|
const rec = block as ContentBlockWithSignature;
|
||||||
const stripSnake = shouldStripSignature(rec.thought_signature);
|
const stripSnake = shouldStripSignature(rec.thought_signature);
|
||||||
const stripCamel = includeCamelCase ? shouldStripSignature(rec.thoughtSignature) : false;
|
const stripCamel = includeCamelCase ? shouldStripSignature(rec.thoughtSignature) : false;
|
||||||
@@ -63,8 +73,12 @@ export function stripThoughtSignatures<T>(
|
|||||||
return block;
|
return block;
|
||||||
}
|
}
|
||||||
const next = { ...rec };
|
const next = { ...rec };
|
||||||
if (stripSnake) delete next.thought_signature;
|
if (stripSnake) {
|
||||||
if (stripCamel) delete next.thoughtSignature;
|
delete next.thought_signature;
|
||||||
|
}
|
||||||
|
if (stripCamel) {
|
||||||
|
delete next.thoughtSignature;
|
||||||
|
}
|
||||||
return next;
|
return next;
|
||||||
}) as T;
|
}) as T;
|
||||||
}
|
}
|
||||||
@@ -162,7 +176,9 @@ export function buildBootstrapContextFiles(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const trimmed = trimBootstrapContent(file.content ?? "", file.name, maxChars);
|
const trimmed = trimBootstrapContent(file.content ?? "", file.name, maxChars);
|
||||||
if (!trimmed.content) continue;
|
if (!trimmed.content) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (trimmed.truncated) {
|
if (trimmed.truncated) {
|
||||||
opts?.warn?.(
|
opts?.warn?.(
|
||||||
`workspace bootstrap file ${file.name} is ${trimmed.originalLength} chars (limit ${trimmed.maxChars}); truncating in injected context`,
|
`workspace bootstrap file ${file.name} is ${trimmed.originalLength} chars (limit ${trimmed.maxChars}); truncating in injected context`,
|
||||||
@@ -188,7 +204,9 @@ export function sanitizeGoogleTurnOrdering(messages: AgentMessage[]): AgentMessa
|
|||||||
) {
|
) {
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
if (role !== "assistant") return messages;
|
if (role !== "assistant") {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
// Cloud Code Assist rejects histories that begin with a model turn (tool call or text).
|
// Cloud Code Assist rejects histories that begin with a model turn (tool call or text).
|
||||||
// Prepend a tiny synthetic user turn so the rest of the transcript can be used.
|
// Prepend a tiny synthetic user turn so the rest of the transcript can be used.
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { formatSandboxToolPolicyBlockedMessage } from "../sandbox.js";
|
|||||||
import type { FailoverReason } from "./types.js";
|
import type { FailoverReason } from "./types.js";
|
||||||
|
|
||||||
export function isContextOverflowError(errorMessage?: string): boolean {
|
export function isContextOverflowError(errorMessage?: string): boolean {
|
||||||
if (!errorMessage) return false;
|
if (!errorMessage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const lower = errorMessage.toLowerCase();
|
const lower = errorMessage.toLowerCase();
|
||||||
const hasRequestSizeExceeds = lower.includes("request size exceeds");
|
const hasRequestSizeExceeds = lower.includes("request size exceeds");
|
||||||
const hasContextWindow =
|
const hasContextWindow =
|
||||||
@@ -30,15 +32,25 @@ const CONTEXT_OVERFLOW_HINT_RE =
|
|||||||
/context.*overflow|context window.*(too (?:large|long)|exceed|over|limit|max(?:imum)?|requested|sent|tokens)|(?:prompt|request|input).*(too (?:large|long)|exceed|over|limit|max(?:imum)?)/i;
|
/context.*overflow|context window.*(too (?:large|long)|exceed|over|limit|max(?:imum)?|requested|sent|tokens)|(?:prompt|request|input).*(too (?:large|long)|exceed|over|limit|max(?:imum)?)/i;
|
||||||
|
|
||||||
export function isLikelyContextOverflowError(errorMessage?: string): boolean {
|
export function isLikelyContextOverflowError(errorMessage?: string): boolean {
|
||||||
if (!errorMessage) return false;
|
if (!errorMessage) {
|
||||||
if (CONTEXT_WINDOW_TOO_SMALL_RE.test(errorMessage)) return false;
|
return false;
|
||||||
if (isContextOverflowError(errorMessage)) return true;
|
}
|
||||||
|
if (CONTEXT_WINDOW_TOO_SMALL_RE.test(errorMessage)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isContextOverflowError(errorMessage)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return CONTEXT_OVERFLOW_HINT_RE.test(errorMessage);
|
return CONTEXT_OVERFLOW_HINT_RE.test(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCompactionFailureError(errorMessage?: string): boolean {
|
export function isCompactionFailureError(errorMessage?: string): boolean {
|
||||||
if (!errorMessage) return false;
|
if (!errorMessage) {
|
||||||
if (!isContextOverflowError(errorMessage)) return false;
|
return false;
|
||||||
|
}
|
||||||
|
if (!isContextOverflowError(errorMessage)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const lower = errorMessage.toLowerCase();
|
const lower = errorMessage.toLowerCase();
|
||||||
return (
|
return (
|
||||||
lower.includes("summarization failed") ||
|
lower.includes("summarization failed") ||
|
||||||
@@ -73,15 +85,21 @@ const HTTP_ERROR_HINTS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function stripFinalTagsFromText(text: string): string {
|
function stripFinalTagsFromText(text: string): string {
|
||||||
if (!text) return text;
|
if (!text) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
return text.replace(FINAL_TAG_RE, "");
|
return text.replace(FINAL_TAG_RE, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function collapseConsecutiveDuplicateBlocks(text: string): string {
|
function collapseConsecutiveDuplicateBlocks(text: string): string {
|
||||||
const trimmed = text.trim();
|
const trimmed = text.trim();
|
||||||
if (!trimmed) return text;
|
if (!trimmed) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
const blocks = trimmed.split(/\n{2,}/);
|
const blocks = trimmed.split(/\n{2,}/);
|
||||||
if (blocks.length < 2) return text;
|
if (blocks.length < 2) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeBlock = (value: string) => value.trim().replace(/\s+/g, " ");
|
const normalizeBlock = (value: string) => value.trim().replace(/\s+/g, " ");
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
@@ -96,15 +114,21 @@ function collapseConsecutiveDuplicateBlocks(text: string): string {
|
|||||||
lastNormalized = normalized;
|
lastNormalized = normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.length === blocks.length) return text;
|
if (result.length === blocks.length) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
return result.join("\n\n");
|
return result.join("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLikelyHttpErrorText(raw: string): boolean {
|
function isLikelyHttpErrorText(raw: string): boolean {
|
||||||
const match = raw.match(HTTP_STATUS_PREFIX_RE);
|
const match = raw.match(HTTP_STATUS_PREFIX_RE);
|
||||||
if (!match) return false;
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const code = Number(match[1]);
|
const code = Number(match[1]);
|
||||||
if (!Number.isFinite(code) || code < 400) return false;
|
if (!Number.isFinite(code) || code < 400) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const message = match[2].toLowerCase();
|
const message = match[2].toLowerCase();
|
||||||
return HTTP_ERROR_HINTS.some((hint) => message.includes(hint));
|
return HTTP_ERROR_HINTS.some((hint) => message.includes(hint));
|
||||||
}
|
}
|
||||||
@@ -112,10 +136,16 @@ function isLikelyHttpErrorText(raw: string): boolean {
|
|||||||
type ErrorPayload = Record<string, unknown>;
|
type ErrorPayload = Record<string, unknown>;
|
||||||
|
|
||||||
function isErrorPayloadObject(payload: unknown): payload is ErrorPayload {
|
function isErrorPayloadObject(payload: unknown): payload is ErrorPayload {
|
||||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const record = payload as ErrorPayload;
|
const record = payload as ErrorPayload;
|
||||||
if (record.type === "error") return true;
|
if (record.type === "error") {
|
||||||
if (typeof record.request_id === "string" || typeof record.requestId === "string") return true;
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof record.request_id === "string" || typeof record.requestId === "string") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if ("error" in record) {
|
if ("error" in record) {
|
||||||
const err = record.error;
|
const err = record.error;
|
||||||
if (err && typeof err === "object" && !Array.isArray(err)) {
|
if (err && typeof err === "object" && !Array.isArray(err)) {
|
||||||
@@ -133,18 +163,26 @@ function isErrorPayloadObject(payload: unknown): payload is ErrorPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseApiErrorPayload(raw: string): ErrorPayload | null {
|
function parseApiErrorPayload(raw: string): ErrorPayload | null {
|
||||||
if (!raw) return null;
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const candidates = [trimmed];
|
const candidates = [trimmed];
|
||||||
if (ERROR_PAYLOAD_PREFIX_RE.test(trimmed)) {
|
if (ERROR_PAYLOAD_PREFIX_RE.test(trimmed)) {
|
||||||
candidates.push(trimmed.replace(ERROR_PAYLOAD_PREFIX_RE, "").trim());
|
candidates.push(trimmed.replace(ERROR_PAYLOAD_PREFIX_RE, "").trim());
|
||||||
}
|
}
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (!candidate.startsWith("{") || !candidate.endsWith("}")) continue;
|
if (!candidate.startsWith("{") || !candidate.endsWith("}")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(candidate) as unknown;
|
const parsed = JSON.parse(candidate) as unknown;
|
||||||
if (isErrorPayloadObject(parsed)) return parsed;
|
if (isErrorPayloadObject(parsed)) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore parse errors
|
// ignore parse errors
|
||||||
}
|
}
|
||||||
@@ -166,9 +204,13 @@ function stableStringify(value: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getApiErrorPayloadFingerprint(raw?: string): string | null {
|
export function getApiErrorPayloadFingerprint(raw?: string): string | null {
|
||||||
if (!raw) return null;
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const payload = parseApiErrorPayload(raw);
|
const payload = parseApiErrorPayload(raw);
|
||||||
if (!payload) return null;
|
if (!payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return stableStringify(payload);
|
return stableStringify(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,9 +226,13 @@ export type ApiErrorInfo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function parseApiErrorInfo(raw?: string): ApiErrorInfo | null {
|
export function parseApiErrorInfo(raw?: string): ApiErrorInfo | null {
|
||||||
if (!raw) return null;
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let httpCode: string | undefined;
|
let httpCode: string | undefined;
|
||||||
let candidate = trimmed;
|
let candidate = trimmed;
|
||||||
@@ -198,7 +244,9 @@ export function parseApiErrorInfo(raw?: string): ApiErrorInfo | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload = parseApiErrorPayload(candidate);
|
const payload = parseApiErrorPayload(candidate);
|
||||||
if (!payload) return null;
|
if (!payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const requestId =
|
const requestId =
|
||||||
typeof payload.request_id === "string"
|
typeof payload.request_id === "string"
|
||||||
@@ -214,9 +262,15 @@ export function parseApiErrorInfo(raw?: string): ApiErrorInfo | null {
|
|||||||
let errMessage: string | undefined;
|
let errMessage: string | undefined;
|
||||||
if (payload.error && typeof payload.error === "object" && !Array.isArray(payload.error)) {
|
if (payload.error && typeof payload.error === "object" && !Array.isArray(payload.error)) {
|
||||||
const err = payload.error as Record<string, unknown>;
|
const err = payload.error as Record<string, unknown>;
|
||||||
if (typeof err.type === "string") errType = err.type;
|
if (typeof err.type === "string") {
|
||||||
if (typeof err.code === "string" && !errType) errType = err.code;
|
errType = err.type;
|
||||||
if (typeof err.message === "string") errMessage = err.message;
|
}
|
||||||
|
if (typeof err.code === "string" && !errType) {
|
||||||
|
errType = err.code;
|
||||||
|
}
|
||||||
|
if (typeof err.message === "string") {
|
||||||
|
errMessage = err.message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -229,7 +283,9 @@ export function parseApiErrorInfo(raw?: string): ApiErrorInfo | null {
|
|||||||
|
|
||||||
export function formatRawAssistantErrorForUi(raw?: string): string {
|
export function formatRawAssistantErrorForUi(raw?: string): string {
|
||||||
const trimmed = (raw ?? "").trim();
|
const trimmed = (raw ?? "").trim();
|
||||||
if (!trimmed) return "LLM request failed with an unknown error.";
|
if (!trimmed) {
|
||||||
|
return "LLM request failed with an unknown error.";
|
||||||
|
}
|
||||||
|
|
||||||
const httpMatch = trimmed.match(HTTP_STATUS_PREFIX_RE);
|
const httpMatch = trimmed.match(HTTP_STATUS_PREFIX_RE);
|
||||||
if (httpMatch) {
|
if (httpMatch) {
|
||||||
@@ -256,8 +312,12 @@ export function formatAssistantErrorText(
|
|||||||
): string | undefined {
|
): string | undefined {
|
||||||
// Also format errors if errorMessage is present, even if stopReason isn't "error"
|
// Also format errors if errorMessage is present, even if stopReason isn't "error"
|
||||||
const raw = (msg.errorMessage ?? "").trim();
|
const raw = (msg.errorMessage ?? "").trim();
|
||||||
if (msg.stopReason !== "error" && !raw) return undefined;
|
if (msg.stopReason !== "error" && !raw) {
|
||||||
if (!raw) return "LLM request failed with an unknown error.";
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!raw) {
|
||||||
|
return "LLM request failed with an unknown error.";
|
||||||
|
}
|
||||||
|
|
||||||
const unknownTool =
|
const unknownTool =
|
||||||
raw.match(/unknown tool[:\s]+["']?([a-z0-9_-]+)["']?/i) ??
|
raw.match(/unknown tool[:\s]+["']?([a-z0-9_-]+)["']?/i) ??
|
||||||
@@ -268,7 +328,9 @@ export function formatAssistantErrorText(
|
|||||||
sessionKey: opts?.sessionKey,
|
sessionKey: opts?.sessionKey,
|
||||||
toolName: unknownTool[1],
|
toolName: unknownTool[1],
|
||||||
});
|
});
|
||||||
if (rewritten) return rewritten;
|
if (rewritten) {
|
||||||
|
return rewritten;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isContextOverflowError(raw)) {
|
if (isContextOverflowError(raw)) {
|
||||||
@@ -311,10 +373,14 @@ export function formatAssistantErrorText(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeUserFacingText(text: string): string {
|
export function sanitizeUserFacingText(text: string): string {
|
||||||
if (!text) return text;
|
if (!text) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
const stripped = stripFinalTagsFromText(text);
|
const stripped = stripFinalTagsFromText(text);
|
||||||
const trimmed = stripped.trim();
|
const trimmed = stripped.trim();
|
||||||
if (!trimmed) return stripped;
|
if (!trimmed) {
|
||||||
|
return stripped;
|
||||||
|
}
|
||||||
|
|
||||||
if (/incorrect role information|roles must alternate/i.test(trimmed)) {
|
if (/incorrect role information|roles must alternate/i.test(trimmed)) {
|
||||||
return (
|
return (
|
||||||
@@ -348,7 +414,9 @@ export function sanitizeUserFacingText(text: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isRateLimitAssistantError(msg: AssistantMessage | undefined): boolean {
|
export function isRateLimitAssistantError(msg: AssistantMessage | undefined): boolean {
|
||||||
if (!msg || msg.stopReason !== "error") return false;
|
if (!msg || msg.stopReason !== "error") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return isRateLimitErrorMessage(msg.errorMessage ?? "");
|
return isRateLimitErrorMessage(msg.errorMessage ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +472,9 @@ const IMAGE_DIMENSION_PATH_RE = /messages\.(\d+)\.content\.(\d+)\.image/i;
|
|||||||
const IMAGE_SIZE_ERROR_RE = /image exceeds\s*(\d+(?:\.\d+)?)\s*mb/i;
|
const IMAGE_SIZE_ERROR_RE = /image exceeds\s*(\d+(?:\.\d+)?)\s*mb/i;
|
||||||
|
|
||||||
function matchesErrorPatterns(raw: string, patterns: readonly ErrorPattern[]): boolean {
|
function matchesErrorPatterns(raw: string, patterns: readonly ErrorPattern[]): boolean {
|
||||||
if (!raw) return false;
|
if (!raw) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const value = raw.toLowerCase();
|
const value = raw.toLowerCase();
|
||||||
return patterns.some((pattern) =>
|
return patterns.some((pattern) =>
|
||||||
pattern instanceof RegExp ? pattern.test(value) : value.includes(pattern),
|
pattern instanceof RegExp ? pattern.test(value) : value.includes(pattern),
|
||||||
@@ -421,8 +491,12 @@ export function isTimeoutErrorMessage(raw: string): boolean {
|
|||||||
|
|
||||||
export function isBillingErrorMessage(raw: string): boolean {
|
export function isBillingErrorMessage(raw: string): boolean {
|
||||||
const value = raw.toLowerCase();
|
const value = raw.toLowerCase();
|
||||||
if (!value) return false;
|
if (!value) {
|
||||||
if (matchesErrorPatterns(value, ERROR_PATTERNS.billing)) return true;
|
return false;
|
||||||
|
}
|
||||||
|
if (matchesErrorPatterns(value, ERROR_PATTERNS.billing)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
value.includes("billing") &&
|
value.includes("billing") &&
|
||||||
(value.includes("upgrade") ||
|
(value.includes("upgrade") ||
|
||||||
@@ -433,7 +507,9 @@ export function isBillingErrorMessage(raw: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isBillingAssistantError(msg: AssistantMessage | undefined): boolean {
|
export function isBillingAssistantError(msg: AssistantMessage | undefined): boolean {
|
||||||
if (!msg || msg.stopReason !== "error") return false;
|
if (!msg || msg.stopReason !== "error") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return isBillingErrorMessage(msg.errorMessage ?? "");
|
return isBillingErrorMessage(msg.errorMessage ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,9 +527,13 @@ export function parseImageDimensionError(raw: string): {
|
|||||||
contentIndex?: number;
|
contentIndex?: number;
|
||||||
raw: string;
|
raw: string;
|
||||||
} | null {
|
} | null {
|
||||||
if (!raw) return null;
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const lower = raw.toLowerCase();
|
const lower = raw.toLowerCase();
|
||||||
if (!lower.includes("image dimensions exceed max allowed size")) return null;
|
if (!lower.includes("image dimensions exceed max allowed size")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const limitMatch = raw.match(IMAGE_DIMENSION_ERROR_RE);
|
const limitMatch = raw.match(IMAGE_DIMENSION_ERROR_RE);
|
||||||
const pathMatch = raw.match(IMAGE_DIMENSION_PATH_RE);
|
const pathMatch = raw.match(IMAGE_DIMENSION_PATH_RE);
|
||||||
return {
|
return {
|
||||||
@@ -472,9 +552,13 @@ export function parseImageSizeError(raw: string): {
|
|||||||
maxMb?: number;
|
maxMb?: number;
|
||||||
raw: string;
|
raw: string;
|
||||||
} | null {
|
} | null {
|
||||||
if (!raw) return null;
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const lower = raw.toLowerCase();
|
const lower = raw.toLowerCase();
|
||||||
if (!lower.includes("image exceeds") || !lower.includes("mb")) return null;
|
if (!lower.includes("image exceeds") || !lower.includes("mb")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const match = raw.match(IMAGE_SIZE_ERROR_RE);
|
const match = raw.match(IMAGE_SIZE_ERROR_RE);
|
||||||
return {
|
return {
|
||||||
maxMb: match?.[1] ? Number.parseFloat(match[1]) : undefined,
|
maxMb: match?.[1] ? Number.parseFloat(match[1]) : undefined,
|
||||||
@@ -483,7 +567,9 @@ export function parseImageSizeError(raw: string): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isImageSizeError(errorMessage?: string): boolean {
|
export function isImageSizeError(errorMessage?: string): boolean {
|
||||||
if (!errorMessage) return false;
|
if (!errorMessage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return Boolean(parseImageSizeError(errorMessage));
|
return Boolean(parseImageSizeError(errorMessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,19 +578,37 @@ export function isCloudCodeAssistFormatError(raw: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isAuthAssistantError(msg: AssistantMessage | undefined): boolean {
|
export function isAuthAssistantError(msg: AssistantMessage | undefined): boolean {
|
||||||
if (!msg || msg.stopReason !== "error") return false;
|
if (!msg || msg.stopReason !== "error") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return isAuthErrorMessage(msg.errorMessage ?? "");
|
return isAuthErrorMessage(msg.errorMessage ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function classifyFailoverReason(raw: string): FailoverReason | null {
|
export function classifyFailoverReason(raw: string): FailoverReason | null {
|
||||||
if (isImageDimensionErrorMessage(raw)) return null;
|
if (isImageDimensionErrorMessage(raw)) {
|
||||||
if (isImageSizeError(raw)) return null;
|
return null;
|
||||||
if (isRateLimitErrorMessage(raw)) return "rate_limit";
|
}
|
||||||
if (isOverloadedErrorMessage(raw)) return "rate_limit";
|
if (isImageSizeError(raw)) {
|
||||||
if (isCloudCodeAssistFormatError(raw)) return "format";
|
return null;
|
||||||
if (isBillingErrorMessage(raw)) return "billing";
|
}
|
||||||
if (isTimeoutErrorMessage(raw)) return "timeout";
|
if (isRateLimitErrorMessage(raw)) {
|
||||||
if (isAuthErrorMessage(raw)) return "auth";
|
return "rate_limit";
|
||||||
|
}
|
||||||
|
if (isOverloadedErrorMessage(raw)) {
|
||||||
|
return "rate_limit";
|
||||||
|
}
|
||||||
|
if (isCloudCodeAssistFormatError(raw)) {
|
||||||
|
return "format";
|
||||||
|
}
|
||||||
|
if (isBillingErrorMessage(raw)) {
|
||||||
|
return "billing";
|
||||||
|
}
|
||||||
|
if (isTimeoutErrorMessage(raw)) {
|
||||||
|
return "timeout";
|
||||||
|
}
|
||||||
|
if (isAuthErrorMessage(raw)) {
|
||||||
|
return "auth";
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,6 +617,8 @@ export function isFailoverErrorMessage(raw: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isFailoverAssistantError(msg: AssistantMessage | undefined): boolean {
|
export function isFailoverAssistantError(msg: AssistantMessage | undefined): boolean {
|
||||||
if (!msg || msg.stopReason !== "error") return false;
|
if (!msg || msg.stopReason !== "error") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return isFailoverErrorMessage(msg.errorMessage ?? "");
|
return isFailoverErrorMessage(msg.errorMessage ?? "");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ export function isAntigravityClaude(params: {
|
|||||||
}): boolean {
|
}): boolean {
|
||||||
const provider = params.provider?.toLowerCase();
|
const provider = params.provider?.toLowerCase();
|
||||||
const api = params.api?.toLowerCase();
|
const api = params.api?.toLowerCase();
|
||||||
if (provider !== "google-antigravity" && api !== "google-antigravity") return false;
|
if (provider !== "google-antigravity" && api !== "google-antigravity") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return params.modelId?.toLowerCase().includes("claude") ?? false;
|
return params.modelId?.toLowerCase().includes("claude") ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,20 @@ export function isEmptyAssistantMessageContent(
|
|||||||
message: Extract<AgentMessage, { role: "assistant" }>,
|
message: Extract<AgentMessage, { role: "assistant" }>,
|
||||||
): boolean {
|
): boolean {
|
||||||
const content = message.content;
|
const content = message.content;
|
||||||
if (content == null) return true;
|
if (content == null) {
|
||||||
if (!Array.isArray(content)) return false;
|
return true;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(content)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return content.every((block) => {
|
return content.every((block) => {
|
||||||
if (!block || typeof block !== "object") return true;
|
if (!block || typeof block !== "object") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const rec = block as { type?: unknown; text?: unknown };
|
const rec = block as { type?: unknown; text?: unknown };
|
||||||
if (rec.type !== "text") return false;
|
if (rec.type !== "text") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return typeof rec.text !== "string" || rec.text.trim().length === 0;
|
return typeof rec.text !== "string" || rec.text.trim().length === 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -110,9 +118,13 @@ export async function sanitizeSessionMessagesImages(
|
|||||||
: stripThoughtSignatures(content, options?.sanitizeThoughtSignatures); // Strip for Gemini
|
: stripThoughtSignatures(content, options?.sanitizeThoughtSignatures); // Strip for Gemini
|
||||||
|
|
||||||
const filteredContent = strippedContent.filter((block) => {
|
const filteredContent = strippedContent.filter((block) => {
|
||||||
if (!block || typeof block !== "object") return true;
|
if (!block || typeof block !== "object") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const rec = block as { type?: unknown; text?: unknown };
|
const rec = block as { type?: unknown; text?: unknown };
|
||||||
if (rec.type !== "text" || typeof rec.text !== "string") return true;
|
if (rec.type !== "text" || typeof rec.text !== "string") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return rec.text.trim().length > 0;
|
return rec.text.trim().length > 0;
|
||||||
});
|
});
|
||||||
const finalContent = (await sanitizeContentBlocksImages(
|
const finalContent = (await sanitizeContentBlocksImages(
|
||||||
|
|||||||
@@ -20,17 +20,27 @@ export function isMessagingToolDuplicateNormalized(
|
|||||||
normalized: string,
|
normalized: string,
|
||||||
normalizedSentTexts: string[],
|
normalizedSentTexts: string[],
|
||||||
): boolean {
|
): boolean {
|
||||||
if (normalizedSentTexts.length === 0) return false;
|
if (normalizedSentTexts.length === 0) {
|
||||||
if (!normalized || normalized.length < MIN_DUPLICATE_TEXT_LENGTH) return false;
|
return false;
|
||||||
|
}
|
||||||
|
if (!normalized || normalized.length < MIN_DUPLICATE_TEXT_LENGTH) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return normalizedSentTexts.some((normalizedSent) => {
|
return normalizedSentTexts.some((normalizedSent) => {
|
||||||
if (!normalizedSent || normalizedSent.length < MIN_DUPLICATE_TEXT_LENGTH) return false;
|
if (!normalizedSent || normalizedSent.length < MIN_DUPLICATE_TEXT_LENGTH) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return normalized.includes(normalizedSent) || normalizedSent.includes(normalized);
|
return normalized.includes(normalizedSent) || normalizedSent.includes(normalized);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMessagingToolDuplicate(text: string, sentTexts: string[]): boolean {
|
export function isMessagingToolDuplicate(text: string, sentTexts: string[]): boolean {
|
||||||
if (sentTexts.length === 0) return false;
|
if (sentTexts.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const normalized = normalizeTextForComparison(text);
|
const normalized = normalizeTextForComparison(text);
|
||||||
if (!normalized || normalized.length < MIN_DUPLICATE_TEXT_LENGTH) return false;
|
if (!normalized || normalized.length < MIN_DUPLICATE_TEXT_LENGTH) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return isMessagingToolDuplicateNormalized(normalized, sentTexts.map(normalizeTextForComparison));
|
return isMessagingToolDuplicateNormalized(normalized, sentTexts.map(normalizeTextForComparison));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,15 @@ type OpenAIReasoningSignature = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function parseOpenAIReasoningSignature(value: unknown): OpenAIReasoningSignature | null {
|
function parseOpenAIReasoningSignature(value: unknown): OpenAIReasoningSignature | null {
|
||||||
if (!value) return null;
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
let candidate: { id?: unknown; type?: unknown } | null = null;
|
let candidate: { id?: unknown; type?: unknown } | null = null;
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
candidate = JSON.parse(trimmed) as { id?: unknown; type?: unknown };
|
candidate = JSON.parse(trimmed) as { id?: unknown; type?: unknown };
|
||||||
} catch {
|
} catch {
|
||||||
@@ -25,10 +29,14 @@ function parseOpenAIReasoningSignature(value: unknown): OpenAIReasoningSignature
|
|||||||
} else if (typeof value === "object") {
|
} else if (typeof value === "object") {
|
||||||
candidate = value as { id?: unknown; type?: unknown };
|
candidate = value as { id?: unknown; type?: unknown };
|
||||||
}
|
}
|
||||||
if (!candidate) return null;
|
if (!candidate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const id = typeof candidate.id === "string" ? candidate.id : "";
|
const id = typeof candidate.id === "string" ? candidate.id : "";
|
||||||
const type = typeof candidate.type === "string" ? candidate.type : "";
|
const type = typeof candidate.type === "string" ? candidate.type : "";
|
||||||
if (!id.startsWith("rs_")) return null;
|
if (!id.startsWith("rs_")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (type === "reasoning" || type.startsWith("reasoning.")) {
|
if (type === "reasoning" || type.startsWith("reasoning.")) {
|
||||||
return { id, type };
|
return { id, type };
|
||||||
}
|
}
|
||||||
@@ -41,8 +49,12 @@ function hasFollowingNonThinkingBlock(
|
|||||||
): boolean {
|
): boolean {
|
||||||
for (let i = index + 1; i < content.length; i++) {
|
for (let i = index + 1; i < content.length; i++) {
|
||||||
const block = content[i];
|
const block = content[i];
|
||||||
if (!block || typeof block !== "object") return true;
|
if (!block || typeof block !== "object") {
|
||||||
if ((block as { type?: unknown }).type !== "thinking") return true;
|
return true;
|
||||||
|
}
|
||||||
|
if ((block as { type?: unknown }).type !== "thinking") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { normalizeThinkLevel, type ThinkLevel } from "../../auto-reply/thinking.
|
|||||||
function extractSupportedValues(raw: string): string[] {
|
function extractSupportedValues(raw: string): string[] {
|
||||||
const match =
|
const match =
|
||||||
raw.match(/supported values are:\s*([^\n.]+)/i) ?? raw.match(/supported values:\s*([^\n.]+)/i);
|
raw.match(/supported values are:\s*([^\n.]+)/i) ?? raw.match(/supported values:\s*([^\n.]+)/i);
|
||||||
if (!match?.[1]) return [];
|
if (!match?.[1]) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const fragment = match[1];
|
const fragment = match[1];
|
||||||
const quoted = Array.from(fragment.matchAll(/['"]([^'"]+)['"]/g)).map((entry) =>
|
const quoted = Array.from(fragment.matchAll(/['"]([^'"]+)['"]/g)).map((entry) =>
|
||||||
entry[1]?.trim(),
|
entry[1]?.trim(),
|
||||||
@@ -22,13 +24,21 @@ export function pickFallbackThinkingLevel(params: {
|
|||||||
attempted: Set<ThinkLevel>;
|
attempted: Set<ThinkLevel>;
|
||||||
}): ThinkLevel | undefined {
|
}): ThinkLevel | undefined {
|
||||||
const raw = params.message?.trim();
|
const raw = params.message?.trim();
|
||||||
if (!raw) return undefined;
|
if (!raw) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const supported = extractSupportedValues(raw);
|
const supported = extractSupportedValues(raw);
|
||||||
if (supported.length === 0) return undefined;
|
if (supported.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
for (const entry of supported) {
|
for (const entry of supported) {
|
||||||
const normalized = normalizeThinkLevel(entry);
|
const normalized = normalizeThinkLevel(entry);
|
||||||
if (!normalized) continue;
|
if (!normalized) {
|
||||||
if (params.attempted.has(normalized)) continue;
|
continue;
|
||||||
|
}
|
||||||
|
if (params.attempted.has(normalized)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ const CORE_MESSAGING_TOOLS = new Set(["sessions_send", "message"]);
|
|||||||
|
|
||||||
// Provider docking: any plugin with `actions` opts into messaging tool handling.
|
// Provider docking: any plugin with `actions` opts into messaging tool handling.
|
||||||
export function isMessagingTool(toolName: string): boolean {
|
export function isMessagingTool(toolName: string): boolean {
|
||||||
if (CORE_MESSAGING_TOOLS.has(toolName)) return true;
|
if (CORE_MESSAGING_TOOLS.has(toolName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const providerId = normalizeChannelId(toolName);
|
const providerId = normalizeChannelId(toolName);
|
||||||
return Boolean(providerId && getChannelPlugin(providerId)?.actions);
|
return Boolean(providerId && getChannelPlugin(providerId)?.actions);
|
||||||
}
|
}
|
||||||
@@ -21,13 +23,19 @@ export function isMessagingToolSendAction(
|
|||||||
args: Record<string, unknown>,
|
args: Record<string, unknown>,
|
||||||
): boolean {
|
): boolean {
|
||||||
const action = typeof args.action === "string" ? args.action.trim() : "";
|
const action = typeof args.action === "string" ? args.action.trim() : "";
|
||||||
if (toolName === "sessions_send") return true;
|
if (toolName === "sessions_send") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (toolName === "message") {
|
if (toolName === "message") {
|
||||||
return action === "send" || action === "thread-reply";
|
return action === "send" || action === "thread-reply";
|
||||||
}
|
}
|
||||||
const providerId = normalizeChannelId(toolName);
|
const providerId = normalizeChannelId(toolName);
|
||||||
if (!providerId) return false;
|
if (!providerId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const plugin = getChannelPlugin(providerId);
|
const plugin = getChannelPlugin(providerId);
|
||||||
if (!plugin?.actions?.extractToolSend) return false;
|
if (!plugin?.actions?.extractToolSend) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return Boolean(plugin.actions.extractToolSend({ args })?.to);
|
return Boolean(plugin.actions.extractToolSend({ args })?.to);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ const _ensureModels = (cfg: OpenClawConfig, agentDir: string) =>
|
|||||||
ensureOpenClawModelsJson(cfg, agentDir) as unknown;
|
ensureOpenClawModelsJson(cfg, agentDir) as unknown;
|
||||||
|
|
||||||
const _textFromContent = (content: unknown) => {
|
const _textFromContent = (content: unknown) => {
|
||||||
if (typeof content === "string") return content;
|
if (typeof content === "string") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
if (Array.isArray(content) && content[0]?.type === "text") {
|
if (Array.isArray(content) && content[0]?.type === "text") {
|
||||||
return (content[0] as { text?: string }).text;
|
return (content[0] as { text?: string }).text;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ const _ensureModels = (cfg: OpenClawConfig, agentDir: string) =>
|
|||||||
ensureOpenClawModelsJson(cfg, agentDir) as unknown;
|
ensureOpenClawModelsJson(cfg, agentDir) as unknown;
|
||||||
|
|
||||||
const _textFromContent = (content: unknown) => {
|
const _textFromContent = (content: unknown) => {
|
||||||
if (typeof content === "string") return content;
|
if (typeof content === "string") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
if (Array.isArray(content) && content[0]?.type === "text") {
|
if (Array.isArray(content) && content[0]?.type === "text") {
|
||||||
return (content[0] as { text?: string }).text;
|
return (content[0] as { text?: string }).text;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ const _ensureModels = (cfg: OpenClawConfig, agentDir: string) =>
|
|||||||
ensureOpenClawModelsJson(cfg, agentDir) as unknown;
|
ensureOpenClawModelsJson(cfg, agentDir) as unknown;
|
||||||
|
|
||||||
const _textFromContent = (content: unknown) => {
|
const _textFromContent = (content: unknown) => {
|
||||||
if (typeof content === "string") return content;
|
if (typeof content === "string") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
if (Array.isArray(content) && content[0]?.type === "text") {
|
if (Array.isArray(content) && content[0]?.type === "text") {
|
||||||
return (content[0] as { text?: string }).text;
|
return (content[0] as { text?: string }).text;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ const _ensureModels = (cfg: OpenClawConfig, agentDir: string) =>
|
|||||||
ensureOpenClawModelsJson(cfg, agentDir);
|
ensureOpenClawModelsJson(cfg, agentDir);
|
||||||
|
|
||||||
const _textFromContent = (content: unknown) => {
|
const _textFromContent = (content: unknown) => {
|
||||||
if (typeof content === "string") return content;
|
if (typeof content === "string") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
if (Array.isArray(content) && content[0]?.type === "text") {
|
if (Array.isArray(content) && content[0]?.type === "text") {
|
||||||
return (content[0] as { text?: string }).text;
|
return (content[0] as { text?: string }).text;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ const _ensureModels = (cfg: OpenClawConfig, agentDir: string) =>
|
|||||||
ensureOpenClawModelsJson(cfg, agentDir) as unknown;
|
ensureOpenClawModelsJson(cfg, agentDir) as unknown;
|
||||||
|
|
||||||
const _textFromContent = (content: unknown) => {
|
const _textFromContent = (content: unknown) => {
|
||||||
if (typeof content === "string") return content;
|
if (typeof content === "string") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
if (Array.isArray(content) && content[0]?.type === "text") {
|
if (Array.isArray(content) && content[0]?.type === "text") {
|
||||||
return (content[0] as { text?: string }).text;
|
return (content[0] as { text?: string }).text;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user