Compare commits

...

15 Commits

Author SHA1 Message Date
Innei 20c93e3ea6 🐛 fix CI type-check regressions 2026-03-18 23:28:26 +08:00
Innei 4fef538d67 Merge branch 'canary' into rfc12946-migration 2026-03-18 20:30:18 +08:00
Innei 5e86797e48 🔧 fix(types): resolve CI type-check regressions 2026-03-18 19:34:23 +08:00
Innei c92996a440 🐛 fix: resolve tsgo CI type errors from RFC12946 strict types
- Relax cleanObject/sanitizeForLogging generic constraint to T extends object
- Export UniformTool from context-engine tools index
- Add type casts at TRPC boundaries where strict interfaces cross zod schemas
- Fix test mock with unknown field in DynamicInterventionMetadata
2026-03-18 18:51:25 +08:00
Innei 57fb9b49f9 🐛 fix: resolve type errors after RFC12946 migration and canary rebase
- Add missing fields to OperationMetadata (taskDescription, groupId, cancelled, etc.)
- Fix GeneralChatAgent test type errors with TestOverrides helper
- Fix streamingExecutor sessionId→agentId, null→undefined conversion
- Fix pluginTypes/lobe-web-browsing unknown type casts
- Fix createAgentExecutors import type duplication
2026-03-18 18:37:30 +08:00
Innei bf82c03f21 🐛 fix: add humanIntervention to LobeToolManifest and restore manifest-level fallback
LobeToolManifest was previously typed as `any` via toolManifestMap, masking
that manifest-level humanIntervention was used at runtime. Now that the type
is explicit, add the field to the interface and restore the fallback logic.
2026-03-18 18:15:08 +08:00
Innei 69f1c913f1 🐛 fix(agent-runtime): remove invalid manifest.humanIntervention access
LobeToolManifest has no humanIntervention field - it only exists on
LobeChatPluginApi (api level). The manifest-level fallback was dead code.
2026-03-18 18:15:08 +08:00
Innei 51e66e54df 🐛 fix(agent-runtime): use AgentStateMetadata instead of Record<string,unknown> for resolveDynamicPolicy
Fixes CI type error where AgentStateMetadata (no index signature) was not
assignable to Record<string, unknown>.
2026-03-18 18:15:08 +08:00
Innei 5fbedceb50 ♻️ refactor: migrate remaining model-runtime files + fix pre-existing lint errors
- Record<string, any> → Record<string, unknown> in 6 remaining files
- Fix no-console, preserve-caught-error, no-useless-assignment, regexp lint errors
2026-03-18 18:15:08 +08:00
Innei b4319667dc ♻️ refactor: Phase 3+4 - migrate Record<string,any> across packages
Phase 3 (database boundaries):
- JSONB schema types → Record<string, unknown>
- Model layer params merging, metadata updates
- Topic importer, knowledge repository types

Phase 4 (runtime factories + remaining packages):
- model-runtime: factory generic constraints, provider implementations, type definitions
- builtin-tools: interventions, placeholders, cloud-sandbox, local-system, web-browsing
- conversation-flow, file-loaders, eval-dataset-parser, openapi, utils, electron-client-ipc
- agent-tracing, web-crawler, memory-user-memory
2026-03-18 18:15:07 +08:00
Innei 3a58ac91e6 ♻️ refactor(types): Phase 2 - replace metadata Record<string,unknown> with MessageMetadata
- AssistantContentBlock.metadata → MessageMetadata
- SendMessageParams/SendGroupMessageParams.metadata → MessageMetadata
- aiChat newAssistantMessage.metadata → MessageMetadata
- createAgentExecutors metadata construction → Partial<MessageMetadata>
- UpdateToolMessageParams.metadata → MessageMetadata
- messageService.updateToolMessage.metadata → MessageMetadata
2026-03-18 18:15:07 +08:00
Innei 4a58984e44 ♻️ refactor(types): Phase 1 - type-safe shared exported types
- packages/types: Remove index signature from GroupAgentConfig, use Record<string, unknown> for JSON Schema properties
- packages/agent-runtime: Use ToolArguments instead of Record<string, any> in GeneralChatAgent, remove index signature from agentConfig
- packages/context-engine: Remove index signatures from MinimalMessage metadata/tools, use Record<string, unknown> in AgentBuilderContext
2026-03-18 18:15:07 +08:00
Innei 61e419225d ♻️ refactor(server): type-safe ComfyUI workflow params and remaining Record<string,any> cleanup
- Define ComfyUIWorkflowParams interface with explicit fields (prompt, width, height, steps, seed, cfg, etc.)
- Replace Record<string, unknown> with ComfyUIWorkflowParams across all workflow builders
- SimpleSDParams now extends ComfyUIWorkflowParams instead of Record<string, unknown>
- Migrate remaining Record<string, any> to Record<string, unknown> in klavis, mcp, message services
2026-03-18 18:15:07 +08:00
Innei 72047413d8 🔧 chore: migration updates across types, services, and features
Made-with: Cursor
2026-03-18 18:15:06 +08:00
Innei 48e6672f7c ♻️ refactor(types): RFC12946 type system migration - improve type safety with unknown vs any
- Replace generic 'any' types with proper 'unknown' type for better type safety
- Extract inline type definitions into named interfaces (RichTextEditorState, LobeDocumentMetadata, LobeDocumentPageMetadata)
- Standardize ChatMessagePluginError and error object structures across message types
- Improve type definitions in document, export, tool, and agent group modules
- Update state and payload types for better type coverage
2026-03-18 18:15:06 +08:00
239 changed files with 1532 additions and 951 deletions
@@ -1,7 +1,12 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AgentManagerRuntime } from '../AgentManagerRuntime';
import type { IAgentService, IDiscoverService } from '../types';
import type {
GetAvailableModelsState,
IAgentService,
IDiscoverService,
SearchAgentState,
} from '../types';
// Create mock services
const mockAgentService: IAgentService = {
@@ -303,10 +308,11 @@ describe('AgentManagerRuntime', () => {
} as any);
const result = await runtime.searchAgents({ keyword: 'test' });
const state = result.state as SearchAgentState | undefined;
expect(result.success).toBe(true);
expect(result.state?.source).toBe('all');
expect(result.state?.agents).toHaveLength(2);
expect(state?.source).toBe('all');
expect(state?.agents).toHaveLength(2);
});
it('should return no agents found message', async () => {
@@ -336,10 +342,11 @@ describe('AgentManagerRuntime', () => {
it('should filter by providerId', async () => {
const result = await runtime.getAvailableModels({ providerId: 'openai' });
const state = result.state as GetAvailableModelsState | undefined;
expect(result.success).toBe(true);
expect(result.state?.providers).toHaveLength(1);
expect(result.state?.providers[0].id).toBe('openai');
expect(state?.providers).toHaveLength(1);
expect(state?.providers[0].id).toBe('openai');
});
});
@@ -3,6 +3,7 @@ import {
type ExtendedHumanInterventionConfig,
type HumanInterventionConfig,
type HumanInterventionPolicy,
type ToolArguments,
} from '@lobechat/types';
import { createDefaultGlobalAudits, DEFAULT_SECURITY_BLACKLIST } from '../audit';
@@ -13,6 +14,7 @@ import {
type AgentInstructionCompressContext,
type AgentRuntimeContext,
type AgentState,
type AgentStateMetadata,
type GeneralAgentCallingToolInstructionPayload,
type GeneralAgentCallLLMInstructionPayload,
type GeneralAgentCallLLMResultPayload,
@@ -61,7 +63,7 @@ export class GeneralChatAgent implements Agent {
// Find the specific API in the manifest
const api = manifest.api?.find((a: any) => a.name === apiName);
// API-level config takes precedence over tool-level config
// API-level config takes precedence over manifest-level config
return api?.humanIntervention ?? manifest.humanIntervention;
}
@@ -75,7 +77,7 @@ export class GeneralChatAgent implements Agent {
private matchesAlwaysPolicy(
config: HumanInterventionConfig | undefined,
toolArgs: Record<string, any>,
toolArgs: ToolArguments,
): boolean {
if (!config) return false;
if (config === 'always') return true;
@@ -100,8 +102,8 @@ export class GeneralChatAgent implements Agent {
private resolveDynamicPolicy(
config: ExtendedHumanInterventionConfig | undefined,
toolArgs: Record<string, any>,
metadata?: Record<string, any>,
toolArgs: ToolArguments,
metadata?: AgentStateMetadata,
): HumanInterventionPolicy | undefined {
if (!this.isDynamicInterventionConfig(config)) {
return undefined;
@@ -146,7 +148,7 @@ export class GeneralChatAgent implements Agent {
const toolKey = `${identifier}/${apiName}`;
// Parse arguments for intervention checking
let toolArgs: Record<string, any> = {};
let toolArgs: ToolArguments = {};
try {
toolArgs = JSON.parse(toolCalling.arguments || '{}');
} catch {
@@ -1,16 +1,28 @@
import { type ChatToolPayload, type GlobalInterventionAuditConfig } from '@lobechat/types';
import type { LobeToolManifest } from '@lobechat/context-engine';
import {
type ChatToolPayload,
type GlobalInterventionAuditConfig,
type LobeChatPluginApi,
} from '@lobechat/types';
import { describe, expect, it } from 'vitest';
import { type AgentRuntimeContext, type AgentState } from '../../types';
import { GeneralChatAgent } from '../GeneralChatAgent';
/**
* Test helper: partial manifests are fine for unit tests.
* We cast at the boundary so every test doesn't need explicit casts.
*/
type TestOverrides = Record<string, any>;
describe('GeneralChatAgent', () => {
const mockModelRuntimeConfig = {
model: 'gpt-4o-mini',
provider: 'openai',
};
const createMockState = (overrides?: Partial<AgentState>): AgentState => ({
const createMockState = (overrides?: TestOverrides): AgentState => ({
operationId: 'test-session',
status: 'running',
messages: [],
@@ -168,7 +180,7 @@ describe('GeneralChatAgent', () => {
'test-plugin': {
identifier: 'test-plugin',
// No humanIntervention config = no intervention needed
},
} as LobeToolManifest,
},
});
@@ -217,8 +229,8 @@ describe('GeneralChatAgent', () => {
const state = createMockState({
toolManifestMap: {
'plugin-1': { identifier: 'plugin-1' },
'plugin-2': { identifier: 'plugin-2' },
'plugin-1': { identifier: 'plugin-1' } as LobeToolManifest,
'plugin-2': { identifier: 'plugin-2' } as LobeToolManifest,
},
});
@@ -261,7 +273,7 @@ describe('GeneralChatAgent', () => {
'test-plugin': {
identifier: 'test-plugin',
// No humanIntervention config
},
} as LobeToolManifest,
},
});
@@ -304,8 +316,8 @@ describe('GeneralChatAgent', () => {
toolManifestMap: {
'dangerous-plugin': {
identifier: 'dangerous-plugin',
humanIntervention: 'require', // Always require approval
},
humanIntervention: 'required', // Always require approval
} as LobeToolManifest,
},
});
@@ -354,11 +366,11 @@ describe('GeneralChatAgent', () => {
'safe-plugin': {
identifier: 'safe-plugin',
// No intervention
},
} as LobeToolManifest,
'dangerous-plugin': {
identifier: 'dangerous-plugin',
humanIntervention: 'require',
},
humanIntervention: 'required',
} as LobeToolManifest,
},
});
@@ -1385,7 +1397,7 @@ describe('GeneralChatAgent', () => {
content:
'All tasks above have been completed. Please summarize the results or continue with your response following user query language.',
role: 'user',
},
} as any,
]),
);
});
@@ -1457,7 +1469,9 @@ describe('GeneralChatAgent', () => {
agentConfig: { maxSteps: 100 },
dynamicInterventionAudits: {
pathScopeAudit: (toolArgs, metadata) => {
const workingDirectory = metadata?.workingDirectory as string | undefined;
const workingDirectory = (metadata as Record<string, unknown>)?.workingDirectory as
| string
| undefined;
if (!workingDirectory) return false;
const path = toolArgs.path as string;
return !path.startsWith(workingDirectory);
@@ -1490,9 +1504,9 @@ describe('GeneralChatAgent', () => {
type: 'pathScopeAudit',
},
},
},
} as LobeChatPluginApi,
],
},
} as LobeToolManifest,
},
});
@@ -1518,7 +1532,9 @@ describe('GeneralChatAgent', () => {
agentConfig: { maxSteps: 100 },
dynamicInterventionAudits: {
pathScopeAudit: (toolArgs, metadata) => {
const workingDirectory = metadata?.workingDirectory as string | undefined;
const workingDirectory = (metadata as Record<string, unknown>)?.workingDirectory as
| string
| undefined;
if (!workingDirectory) return false;
const path = toolArgs.path as string;
return !path.startsWith(workingDirectory);
@@ -1551,9 +1567,9 @@ describe('GeneralChatAgent', () => {
type: 'pathScopeAudit',
},
},
},
} as LobeChatPluginApi,
],
},
} as LobeToolManifest,
},
});
@@ -1601,14 +1617,14 @@ describe('GeneralChatAgent', () => {
{
name: 'safe-api',
// Safe API
},
} as LobeChatPluginApi,
{
name: 'dangerous-api',
// API-level config overrides tool-level
humanIntervention: 'require',
},
humanIntervention: 'required',
} as LobeChatPluginApi,
],
},
} as LobeToolManifest,
},
});
@@ -1650,7 +1666,7 @@ describe('GeneralChatAgent', () => {
'dangerous-tool': {
identifier: 'dangerous-tool',
humanIntervention: 'required', // Tool requires approval
},
} as LobeToolManifest,
},
userInterventionConfig: {
approvalMode: 'auto-run', // But user config overrides
@@ -1706,7 +1722,7 @@ describe('GeneralChatAgent', () => {
bash: {
identifier: 'bash',
humanIntervention: 'never', // Tool doesn't require approval by default
},
} as LobeToolManifest,
},
userInterventionConfig: {
approvalMode: 'allow-list',
@@ -1767,11 +1783,11 @@ describe('GeneralChatAgent', () => {
'web-search': {
identifier: 'web-search',
humanIntervention: 'never', // Safe tool
},
} as LobeToolManifest,
'bash': {
identifier: 'bash',
humanIntervention: 'required', // Dangerous tool
},
} as LobeToolManifest,
},
userInterventionConfig: {
approvalMode: 'manual', // Use tool's own config
@@ -1826,9 +1842,9 @@ describe('GeneralChatAgent', () => {
{
name: 'installPlugin',
humanIntervention: 'always', // Always requires intervention
},
} as LobeChatPluginApi,
],
},
} as LobeToolManifest,
},
userInterventionConfig: {
approvalMode: 'auto-run', // User has auto-run enabled
@@ -1874,7 +1890,7 @@ describe('GeneralChatAgent', () => {
'sensitive-plugin': {
identifier: 'sensitive-plugin',
humanIntervention: 'always', // Tool-level always policy
},
} as LobeToolManifest,
},
userInterventionConfig: {
approvalMode: 'auto-run',
@@ -1928,16 +1944,16 @@ describe('GeneralChatAgent', () => {
'web-search': {
identifier: 'web-search',
humanIntervention: 'required', // Would be bypassed by auto-run
},
} as LobeToolManifest,
'agent-builder': {
identifier: 'agent-builder',
api: [
{
name: 'installPlugin',
humanIntervention: 'always', // Cannot be bypassed
},
} as LobeChatPluginApi,
],
},
} as LobeToolManifest,
},
userInterventionConfig: {
approvalMode: 'auto-run',
@@ -1994,9 +2010,9 @@ describe('GeneralChatAgent', () => {
name: 'writeFile',
// Rule-based config with 'always' policy
humanIntervention: [{ policy: 'always' }],
},
} as LobeChatPluginApi,
],
},
} as LobeToolManifest,
},
userInterventionConfig: {
approvalMode: 'auto-run',
@@ -2048,7 +2064,7 @@ describe('GeneralChatAgent', () => {
const state = createMockState({
toolManifestMap: {
'my-tool': { identifier: 'my-tool', humanIntervention: 'never' },
'my-tool': { identifier: 'my-tool', humanIntervention: 'never' } as LobeToolManifest,
},
});
@@ -2093,7 +2109,7 @@ describe('GeneralChatAgent', () => {
const state = createMockState({
toolManifestMap: {
'my-tool': { identifier: 'my-tool', humanIntervention: 'never' },
'my-tool': { identifier: 'my-tool', humanIntervention: 'never' } as LobeToolManifest,
},
userInterventionConfig: { approvalMode: 'auto-run' },
});
@@ -2138,7 +2154,7 @@ describe('GeneralChatAgent', () => {
const state = createMockState({
toolManifestMap: {
'my-tool': { identifier: 'my-tool' },
'my-tool': { identifier: 'my-tool' } as LobeToolManifest,
},
userInterventionConfig: { approvalMode: 'headless' },
});
@@ -2179,7 +2195,7 @@ describe('GeneralChatAgent', () => {
const state = createMockState({
toolManifestMap: {
'my-tool': { identifier: 'my-tool' },
'my-tool': { identifier: 'my-tool' } as LobeToolManifest,
},
userInterventionConfig: { approvalMode: 'headless' },
});
@@ -2225,7 +2241,7 @@ describe('GeneralChatAgent', () => {
const state = createMockState({
toolManifestMap: {
'my-tool': { identifier: 'my-tool', humanIntervention: 'never' },
'my-tool': { identifier: 'my-tool', humanIntervention: 'never' } as LobeToolManifest,
},
});
@@ -2276,7 +2292,7 @@ describe('GeneralChatAgent', () => {
const state = createMockState({
metadata: { workingDirectory: '/workspace' },
toolManifestMap: {
'my-tool': { identifier: 'my-tool', humanIntervention: 'never' },
'my-tool': { identifier: 'my-tool', humanIntervention: 'never' } as LobeToolManifest,
},
userInterventionConfig: { approvalMode: 'auto-run' },
});
@@ -2333,7 +2349,7 @@ describe('GeneralChatAgent', () => {
};
const state = createMockState({
toolManifestMap: { 'my-tool': { identifier: 'my-tool' } },
toolManifestMap: { 'my-tool': { identifier: 'my-tool' } as LobeToolManifest },
});
const context = createMockContext('llm_result', {
@@ -2366,7 +2382,7 @@ describe('GeneralChatAgent', () => {
const state = createMockState({
toolManifestMap: {
bash: { identifier: 'bash', humanIntervention: 'never' },
bash: { identifier: 'bash', humanIntervention: 'never' } as LobeToolManifest,
},
});
@@ -2410,7 +2426,7 @@ describe('GeneralChatAgent', () => {
'dangerous-tool': {
identifier: 'dangerous-tool',
humanIntervention: 'required', // Tool requires approval
},
} as LobeToolManifest,
},
userInterventionConfig: {
approvalMode: 'headless', // Headless mode for async tasks
@@ -2460,9 +2476,9 @@ describe('GeneralChatAgent', () => {
{
name: 'installPlugin',
humanIntervention: 'always', // Always requires intervention normally
},
} as LobeChatPluginApi,
],
},
} as LobeToolManifest,
},
userInterventionConfig: {
approvalMode: 'headless', // Headless mode bypasses even 'always'
@@ -2509,7 +2525,7 @@ describe('GeneralChatAgent', () => {
bash: {
identifier: 'bash',
humanIntervention: 'never',
},
} as LobeToolManifest,
},
userInterventionConfig: {
approvalMode: 'headless',
@@ -2565,19 +2581,19 @@ describe('GeneralChatAgent', () => {
'web-search': {
identifier: 'web-search',
humanIntervention: 'required',
},
} as LobeToolManifest,
'bash': {
identifier: 'bash',
},
} as LobeToolManifest,
'agent-builder': {
identifier: 'agent-builder',
api: [
{
name: 'installPlugin',
humanIntervention: 'always',
},
} as LobeChatPluginApi,
],
},
} as LobeToolManifest,
},
userInterventionConfig: {
approvalMode: 'headless',
@@ -2629,8 +2645,8 @@ describe('GeneralChatAgent', () => {
const state = createMockState({
toolManifestMap: {
search: { identifier: 'search', humanIntervention: 'required' },
crawl: { identifier: 'crawl', humanIntervention: 'always' },
search: { identifier: 'search', humanIntervention: 'required' } as LobeToolManifest,
crawl: { identifier: 'crawl', humanIntervention: 'always' } as LobeToolManifest,
},
userInterventionConfig: {
approvalMode: 'headless',
@@ -39,7 +39,7 @@ describe('createSecurityBlacklistAudit', () => {
const audit = createSecurityBlacklistAudit();
// No securityBlacklist in metadata → uses default
expect(audit({ command: 'rm -rf /' }, {})).toBe(true);
expect(audit({ command: 'rm -rf /' }, { otherField: 'value' })).toBe(true);
expect(audit({ command: 'rm -rf /' }, { otherField: 'value' } as any)).toBe(true);
});
it('should fall back to DEFAULT_SECURITY_BLACKLIST when metadata is undefined', () => {
@@ -1,6 +1,8 @@
import {
type DynamicInterventionMetadata,
type DynamicInterventionResolver,
type GlobalInterventionAuditConfig,
type SecurityBlacklistConfig,
} from '@lobechat/types';
import { InterventionChecker } from '../core/InterventionChecker';
@@ -8,12 +10,18 @@ import { DEFAULT_SECURITY_BLACKLIST } from './defaultSecurityBlacklist';
export const SECURITY_BLACKLIST_AUDIT_TYPE = 'securityBlacklist';
declare module '@lobechat/types' {
interface DynamicInterventionMetadataOverrides {
securityBlacklist?: SecurityBlacklistConfig;
}
}
/**
* Create a DynamicInterventionResolver that checks security blacklist rules.
* Reads blacklist from `metadata.securityBlacklist`, falls back to DEFAULT_SECURITY_BLACKLIST.
*/
export const createSecurityBlacklistAudit = (): DynamicInterventionResolver => {
return (toolArgs: Record<string, any>, metadata?: Record<string, any>): boolean => {
return (toolArgs, metadata?: DynamicInterventionMetadata): boolean => {
const securityBlacklist = metadata?.securityBlacklist ?? DEFAULT_SECURITY_BLACKLIST;
const result = InterventionChecker.checkSecurityBlacklist(securityBlacklist, toolArgs);
return result.blocked;
@@ -1,9 +1,10 @@
import {
import {
type ArgumentMatcher,
type HumanInterventionPolicy,
type HumanInterventionRule,
type SecurityBlacklistRule,
type ShouldInterveneParams,
type ToolArguments,
} from '@lobechat/types';
import { DEFAULT_SECURITY_BLACKLIST } from '../audit/defaultSecurityBlacklist';
@@ -38,7 +39,7 @@ export class InterventionChecker {
*/
static checkSecurityBlacklist(
securityBlacklist: SecurityBlacklistRule[] = [],
toolArgs: Record<string, any> = {},
toolArgs: ToolArguments = {},
): SecurityCheckResult {
for (const rule of securityBlacklist) {
if (this.matchesSecurityRule(rule, toolArgs)) {
@@ -102,7 +103,7 @@ export class InterventionChecker {
*/
private static matchesSecurityRule(
rule: SecurityBlacklistRule,
toolArgs: Record<string, any>,
toolArgs: ToolArguments,
): boolean {
// Security rules must have match criteria
if (!rule.match) return false;
@@ -130,7 +131,7 @@ export class InterventionChecker {
* @param toolArgs - Tool call arguments
* @returns true if matches
*/
private static matchesRule(rule: HumanInterventionRule, toolArgs: Record<string, any>): boolean {
private static matchesRule(rule: HumanInterventionRule, toolArgs: ToolArguments): boolean {
// No match criteria means it's a default rule
if (!rule.match) return true;
@@ -230,7 +231,7 @@ export class InterventionChecker {
* @param args - Tool call arguments
* @returns Hash string
*/
static hashArguments(args: Record<string, any>): string {
static hashArguments(args: ToolArguments): string {
const sortedKeys = Object.keys(args).sort();
const str = sortedKeys.map((key) => `${key}=${JSON.stringify(args[key])}`).join('&');
+23 -4
View File
@@ -1,4 +1,4 @@
import type { ChatToolPayload } from '@lobechat/types';
import type { ChatToolPayload, UIChatMessage } from '@lobechat/types';
import pMap from 'p-map';
import type {
@@ -389,22 +389,37 @@ export class AgentRuntime {
* @returns Complete AgentState with defaults filled in
*/
static createInitialState(
partialState?: Partial<AgentState> & { operationId: string },
partialState?: Omit<Partial<AgentState>, 'messages'> & {
messages?: Array<Partial<UIChatMessage> & Pick<UIChatMessage, 'content' | 'role'>>;
operationId: string;
},
): AgentState {
const { messages: initialMessages, ...restState } = partialState || { operationId: '' };
const now = new Date().toISOString();
const nowMs = Date.now();
const normalizedMessages =
initialMessages?.map(
(message, index) =>
({
createdAt: nowMs,
id: `message-${index + 1}`,
updatedAt: nowMs,
...message,
}) as UIChatMessage,
) ?? [];
return {
cost: AgentRuntime.createDefaultCost(),
// Default values
createdAt: now,
lastModified: now,
messages: [],
messages: normalizedMessages,
status: 'idle',
stepCount: 0,
toolManifestMap: {},
usage: AgentRuntime.createDefaultUsage(),
// User provided values override defaults
...(partialState || { operationId: '' }),
...restState,
};
}
@@ -518,11 +533,15 @@ export class AgentRuntime {
const args = JSON.parse(toolArgs);
const result = await handler(args);
const messageTimestamp = Date.now();
newState.messages.push({
content: JSON.stringify(result),
createdAt: messageTimestamp,
id: `tool-${toolId}`,
role: 'tool',
tool_call_id: toolId,
updatedAt: messageTimestamp,
});
events.push({ id: toolId, result, type: 'tool_result' });
@@ -69,7 +69,6 @@ export interface GeneralAgentHumanAbortPayload {
export interface GeneralAgentConfig {
agentConfig?: {
[key: string]: any;
maxSteps?: number;
};
/**
+1 -1
View File
@@ -23,7 +23,7 @@ export interface RuntimeConfig {
/** Function to get operation context and abort controller */
getOperation?: (operationId: string) => {
abortController: AbortController;
context: Record<string, any>;
context: Record<string, unknown>;
};
/** Operation ID for tracking this runtime instance */
operationId?: string;
+27 -8
View File
@@ -1,7 +1,15 @@
import type { ActivatedStepTool, OperationToolSet, ToolSource } from '@lobechat/context-engine';
import type {
ActivatedStepTool,
LobeToolManifest,
OperationToolSet,
ToolSource,
UniformTool,
} from '@lobechat/context-engine';
import type {
ChatToolPayload,
SecurityBlacklistConfig,
ToolArguments,
UIChatMessage,
UserInterventionConfig,
} from '@lobechat/types';
@@ -27,7 +35,7 @@ export interface AgentState {
costLimit?: CostLimit;
// --- Metadata ---
createdAt: string;
error?: any;
error?: unknown;
/**
* When true, the agent is in force-finish mode (maxSteps exceeded).
* Tools are allowed to complete, but the next LLM call will have tools stripped
@@ -45,7 +53,7 @@ export interface AgentState {
/** Timestamp when interruption occurred */
interruptedAt: string;
/** The instruction that was being executed when interrupted */
interruptedInstruction?: any;
interruptedInstruction?: unknown;
/** Whether the interruption can be resumed */
canResume: boolean;
};
@@ -58,10 +66,10 @@ export interface AgentState {
maxSteps?: number;
// --- Core Context ---
messages: any[];
messages: UIChatMessage[];
// --- Extensible metadata ---
metadata?: Record<string, any>;
metadata?: AgentStateMetadata;
/**
* Model runtime configuration
@@ -115,9 +123,9 @@ export interface AgentState {
stepCount: number;
systemRole?: string;
toolManifestMap: Record<string, any>;
toolManifestMap: Record<string, LobeToolManifest>;
tools?: any[];
tools?: UniformTool[];
/** Tool source map for routing tool execution to correct handler */
toolSourceMap?: Record<string, ToolSource>;
@@ -147,7 +155,18 @@ export interface ToolsCalling {
type: 'function';
}
export interface AgentStateMetadataOverrides {}
export interface AgentStateMetadata extends AgentStateMetadataOverrides {
activeDeviceId?: string;
agentId?: string;
securityBlacklist?: SecurityBlacklistConfig;
threadId?: string;
topicId?: string;
workingDirectory?: string;
}
/**
* A registry for tools, mapping tool names to their implementation.
*/
export type ToolRegistry = Record<string, (args: any) => Promise<any>>;
export type ToolRegistry = Record<string, (args: ToolArguments) => Promise<unknown>>;
+1 -1
View File
@@ -672,7 +672,7 @@ export function renderMemory(step: StepSnapshot): string {
];
for (const category of sortedKeys) {
const items = (memories as Record<string, any>)[category];
const items = (memories as Record<string, unknown>)[category];
if (category === 'persona') {
const persona = items as any;
@@ -34,11 +34,12 @@ export class CalculatorExecutionRuntime {
async calculate(args: CalculateParams): Promise<BuiltinServerRuntimeOutput> {
try {
const result = await calculatorExecutor.calculate(args);
const resultState = result.state as CalculateState | undefined;
const state: CalculateState = {
expression: result.state?.expression,
precision: result.state?.precision,
result: result.state?.result as number | string | undefined,
expression: resultState?.expression,
precision: resultState?.precision,
result: resultState?.result,
};
return {
@@ -59,12 +60,13 @@ export class CalculatorExecutionRuntime {
async evaluate(args: EvaluateParams): Promise<BuiltinServerRuntimeOutput> {
try {
const result = await calculatorExecutor.evaluate(args);
const resultState = result.state as EvaluateState | undefined;
const state: EvaluateState = {
expression: result.state?.expression,
precision: result.state?.precision,
result: result.state?.result as number | string | undefined,
variables: result.state?.variables,
expression: resultState?.expression,
precision: resultState?.precision,
result: resultState?.result,
variables: resultState?.variables,
};
return {
@@ -85,16 +87,17 @@ export class CalculatorExecutionRuntime {
async sort(args: SortParams): Promise<BuiltinServerRuntimeOutput> {
try {
const result = await calculatorExecutor.sort(args);
const resultState = result.state as SortState | undefined;
const state: SortState = {
largest: result.state?.largest as number | string | undefined,
mode: result.state?.mode,
originalNumbers: result.state?.originalNumbers,
precision: result.state?.precision,
result: result.state?.result,
reverse: result.state?.reverse,
smallest: result.state?.smallest as number | string | undefined,
sorted: result.state?.sorted as (string | number)[] | undefined,
largest: resultState?.largest,
mode: resultState?.mode,
originalNumbers: resultState?.originalNumbers,
precision: resultState?.precision,
result: resultState?.result,
reverse: resultState?.reverse,
smallest: resultState?.smallest,
sorted: resultState?.sorted,
};
return {
@@ -115,13 +118,14 @@ export class CalculatorExecutionRuntime {
async base(args: BaseParams): Promise<BuiltinServerRuntimeOutput> {
try {
const result = await calculatorExecutor.base(args);
const resultState = result.state as BaseState | undefined;
const state: BaseState = {
convertedNumber: result.state?.convertedNumber,
decimalValue: result.state?.decimalValue,
originalBase: result.state?.originalBase as string | undefined,
originalNumber: result.state?.originalNumber as string | undefined,
targetBase: result.state?.targetBase as string | undefined,
convertedNumber: resultState?.convertedNumber,
decimalValue: resultState?.decimalValue,
originalBase: resultState?.originalBase,
originalNumber: resultState?.originalNumber,
targetBase: resultState?.targetBase,
};
return {
@@ -142,11 +146,12 @@ export class CalculatorExecutionRuntime {
async solve(args: SolveParams): Promise<BuiltinServerRuntimeOutput> {
try {
const result = await calculatorExecutor.solve(args);
const resultState = result.state as SolveState | undefined;
const state: SolveState = {
equation: result.state?.equation,
result: result.state?.result as string | string[] | undefined,
variable: result.state?.variable,
equation: resultState?.equation,
result: resultState?.result,
variable: resultState?.variable,
};
return {
@@ -167,11 +172,12 @@ export class CalculatorExecutionRuntime {
async differentiate(args: DifferentiateParams): Promise<BuiltinServerRuntimeOutput> {
try {
const result = await calculatorExecutor.differentiate(args);
const resultState = result.state as DifferentiateState | undefined;
const state: DifferentiateState = {
expression: result.state?.expression,
result: result.state?.result as string | undefined,
variable: result.state?.variable,
expression: resultState?.expression,
result: resultState?.result,
variable: resultState?.variable,
};
return {
@@ -192,10 +198,11 @@ export class CalculatorExecutionRuntime {
async execute(args: ExecuteParams): Promise<BuiltinServerRuntimeOutput> {
try {
const result = await calculatorExecutor.execute(args);
const resultState = result.state as ExecuteState | undefined;
const state: ExecuteState = {
expression: result.state?.expression,
result: result.state?.result as string | undefined,
expression: resultState?.expression,
result: resultState?.result,
};
return {
@@ -216,13 +223,14 @@ export class CalculatorExecutionRuntime {
async defintegrate(args: DefintegrateParams): Promise<BuiltinServerRuntimeOutput> {
try {
const result = await calculatorExecutor.defintegrate(args);
const resultState = result.state as DefintegrateState | undefined;
const state: DefintegrateState = {
expression: result.state?.expression,
lowerBound: result.state?.lowerBound,
result: result.state?.result as string | undefined,
upperBound: result.state?.upperBound,
variable: result.state?.variable,
expression: resultState?.expression,
lowerBound: resultState?.lowerBound,
result: resultState?.result,
upperBound: resultState?.upperBound,
variable: resultState?.variable,
};
return {
@@ -243,11 +251,12 @@ export class CalculatorExecutionRuntime {
async integrate(args: IntegrateParams): Promise<BuiltinServerRuntimeOutput> {
try {
const result = await calculatorExecutor.integrate(args);
const resultState = result.state as IntegrateState | undefined;
const state: IntegrateState = {
expression: result.state?.expression,
result: result.state?.result as string | undefined,
variable: result.state?.variable,
expression: resultState?.expression,
result: resultState?.result,
variable: resultState?.variable,
};
return {
@@ -268,12 +277,13 @@ export class CalculatorExecutionRuntime {
async limit(args: LimitParams): Promise<BuiltinServerRuntimeOutput> {
try {
const result = await calculatorExecutor.limit(args);
const resultState = result.state as LimitState | undefined;
const state: LimitState = {
expression: result.state?.expression,
point: result.state?.point,
result: result.state?.result as string | undefined,
variable: result.state?.variable,
expression: resultState?.expression,
point: resultState?.point,
result: resultState?.result,
variable: resultState?.variable,
};
return {
@@ -360,7 +360,7 @@ export class CloudSandboxExecutionRuntime {
success: true,
};
} catch (error) {
console.log('executeCode error', error);
console.error('executeCode error', error);
return this.handleError(error);
}
}
@@ -46,7 +46,10 @@ class ClientSandboxService implements ISandboxService {
this.userId = userId;
}
async callTool(toolName: string, params: Record<string, any>): Promise<SandboxCallToolResult> {
async callTool(
toolName: string,
params: Record<string, unknown>,
): Promise<SandboxCallToolResult> {
return cloudSandboxService.callTool(toolName, params, {
topicId: this.topicId,
userId: this.userId,
@@ -37,7 +37,7 @@ export interface ISandboxService {
* @param toolName - The name of the tool to call (e.g., 'runCommand', 'writeLocalFile')
* @param params - The parameters for the tool
*/
callTool: (toolName: string, params: Record<string, any>) => Promise<SandboxCallToolResult>;
callTool: (toolName: string, params: Record<string, unknown>) => Promise<SandboxCallToolResult>;
/**
* Export a file from sandbox and upload to cloud storage
@@ -11,8 +11,10 @@ import ExecuteTasksIntervention from './ExecuteTasks';
* before the tool is executed.
*/
export const GroupManagementInterventions: Record<string, BuiltinIntervention> = {
[GroupManagementApiName.executeAgentTask]: ExecuteTaskIntervention as BuiltinIntervention,
[GroupManagementApiName.executeAgentTasks]: ExecuteTasksIntervention as BuiltinIntervention,
[GroupManagementApiName.executeAgentTask]:
ExecuteTaskIntervention as unknown as BuiltinIntervention,
[GroupManagementApiName.executeAgentTasks]:
ExecuteTasksIntervention as unknown as BuiltinIntervention,
};
export { default as ExecuteTaskIntervention } from './ExecuteTask';
@@ -12,9 +12,9 @@ import CreatePlanIntervention from './CreatePlan';
* before the tool is executed.
*/
export const GTDInterventions: Record<string, BuiltinIntervention> = {
[GTDApiName.clearTodos]: ClearTodosIntervention as BuiltinIntervention,
[GTDApiName.createPlan]: CreatePlanIntervention as BuiltinIntervention,
[GTDApiName.createTodos]: AddTodoIntervention as BuiltinIntervention,
[GTDApiName.clearTodos]: ClearTodosIntervention as unknown as BuiltinIntervention,
[GTDApiName.createPlan]: CreatePlanIntervention as unknown as BuiltinIntervention,
[GTDApiName.createTodos]: AddTodoIntervention as unknown as BuiltinIntervention,
};
export { default as AddTodoIntervention } from './AddTodo';
@@ -114,12 +114,14 @@ TodoListUI.displayName = 'TodoListUI';
* TodoList Render component for GTD tool
* Read-only display of todo items matching the style of AddTodoIntervention
*/
const TodoListRender = memo<BuiltinRenderProps<unknown, TodoListRenderState>>(({ pluginState }) => {
const todos = pluginState?.todos;
const items: TodoItem[] = todos?.items || [];
const TodoListRender = memo<BuiltinRenderProps<Record<string, never>, TodoListRenderState>>(
({ pluginState }) => {
const todos = pluginState?.todos;
const items: TodoItem[] = todos?.items || [];
return <TodoListUI items={items} />;
});
return <TodoListUI items={items} />;
},
);
TodoListRender.displayName = 'TodoListRender';
@@ -11,7 +11,7 @@ import FileItem from '../../components/FileItem';
interface SearchFilesProps {
listResults?: LocalFileItem[];
messageId: string;
pluginError: ChatMessagePluginError;
pluginError?: ChatMessagePluginError;
}
const SearchFiles = memo<SearchFilesProps>(({ listResults = [], messageId }) => {
@@ -11,7 +11,7 @@ const ListFiles = memo<BuiltinRenderProps<ListLocalFileParams, LocalFileListStat
<SearchResult
listResults={pluginState?.listResults}
messageId={messageId}
pluginError={pluginError}
pluginError={pluginError || undefined}
/>
);
},
@@ -10,7 +10,7 @@ import FileItem from '../../components/FileItem';
interface SearchFilesProps {
messageId: string;
pluginError: ChatMessagePluginError;
pluginError?: ChatMessagePluginError;
searchResults?: FileResult[];
}
@@ -14,7 +14,7 @@ const SearchFiles = memo<BuiltinRenderProps<LocalSearchFilesParams, LocalFileSea
<SearchQuery args={args} messageId={messageId} pluginState={pluginState} />
<SearchResult
messageId={messageId}
pluginError={pluginError}
pluginError={pluginError || undefined}
searchResults={pluginState?.searchResults}
/>
</Flexbox>
@@ -1,7 +1,13 @@
import { type DynamicInterventionResolver } from '@lobechat/types';
import type { DynamicInterventionMetadata, DynamicInterventionResolver } from '@lobechat/types';
import { normalizePathForScope, resolvePathWithScope } from './utils/path';
declare module '@lobechat/types' {
interface DynamicInterventionMetadataOverrides {
workingDirectory?: string;
}
}
/**
* Check if a path is within the working directory
*/
@@ -24,7 +30,7 @@ const isPathWithinWorkingDirectory = (
* Extract all path values from tool arguments
* Looks for common path parameter names used in local-system tools
*/
const extractPaths = (toolArgs: Record<string, any>): string[] => {
const extractPaths = (toolArgs: Record<string, unknown>): string[] => {
const paths: string[] = [];
const pathParamNames = ['path', 'file_path', 'directory', 'oldPath', 'newPath'];
@@ -44,9 +50,10 @@ const extractPaths = (toolArgs: Record<string, any>): string[] => {
// Handle 'items' array for moveLocalFiles (contains oldPath/newPath objects)
if (Array.isArray(toolArgs.items)) {
for (const item of toolArgs.items) {
if (typeof item === 'object') {
if (item.oldPath) paths.push(item.oldPath);
if (item.newPath) paths.push(item.newPath);
if (typeof item === 'object' && item !== null) {
const moveItem = item as { newPath?: string; oldPath?: string };
if (moveItem.oldPath) paths.push(moveItem.oldPath);
if (moveItem.newPath) paths.push(moveItem.newPath);
}
}
}
@@ -59,11 +66,11 @@ const extractPaths = (toolArgs: Record<string, any>): string[] => {
* Returns true if any path is outside the working directory (requires intervention)
*/
export const pathScopeAudit: DynamicInterventionResolver = (
toolArgs: Record<string, any>,
metadata?: Record<string, any>,
toolArgs,
metadata?: DynamicInterventionMetadata,
): boolean => {
const workingDirectory = metadata?.workingDirectory as string | undefined;
const toolScope = toolArgs.scope as string | undefined;
const workingDirectory = metadata?.workingDirectory;
const toolScope = typeof toolArgs.scope === 'string' ? toolArgs.scope : undefined;
// If no working directory is set, no intervention needed
if (!workingDirectory) {
@@ -1,4 +1,4 @@
import {
import {
type GetCommandOutputResult,
type GlobFilesResult,
type GrepContentResult,
@@ -32,7 +32,7 @@ export interface FileResult {
isDirectory: boolean;
lastAccessTime: Date;
metadata?: {
[key: string]: any;
[key: string]: unknown;
};
modifiedTime: Date;
name: string;
@@ -50,7 +50,7 @@ export const resolveArgsWithScope = <T extends { scope?: string }>(
fallbackScope?: string,
): T => {
const scope = args.scope || fallbackScope;
const currentPath = (args as Record<string, any>)[pathField] as string | undefined;
const currentPath = (args as Record<string, unknown>)[pathField] as string | undefined;
const resolved = resolvePathWithScope(currentPath, scope);
if (resolved === currentPath) return args;
return { ...args, [pathField]: resolved };
@@ -9,5 +9,6 @@ import AddExperienceMemoryIntervention from './AddExperienceMemory';
* Intervention components display when human approval is required before tool execution.
*/
export const MemoryInterventions: Record<string, BuiltinIntervention> = {
[MemoryApiName.addExperienceMemory]: AddExperienceMemoryIntervention as BuiltinIntervention,
[MemoryApiName.addExperienceMemory]:
AddExperienceMemoryIntervention as unknown as BuiltinIntervention,
};
@@ -6,5 +6,5 @@ import { CreateDocumentPlaceholder } from './CreateDocument';
export { CreateDocumentPlaceholder } from './CreateDocument';
export const NotebookPlaceholders: Record<string, BuiltinPlaceholder> = {
[NotebookApiName.createDocument]: CreateDocumentPlaceholder as BuiltinPlaceholder,
[NotebookApiName.createDocument]: CreateDocumentPlaceholder as unknown as BuiltinPlaceholder,
};
@@ -10,12 +10,18 @@ const Inspector = memo<BuiltinPortalProps>(({ arguments: args, messageId, state,
switch (apiName) {
// 兼容旧版数据
case WebBrowsingApiName.search: {
return <Search messageId={messageId} query={args as SearchQuery} response={state} />;
return (
<Search
messageId={messageId}
query={args as unknown as SearchQuery}
response={state as unknown as any}
/>
);
}
case WebBrowsingApiName.crawlSinglePage: {
const url = args.url;
const result = (state as CrawlPluginState).results.find(
const result = (state as unknown as CrawlPluginState).results.find(
(result) => result.originalUrl === url,
);
@@ -26,8 +32,8 @@ const Inspector = memo<BuiltinPortalProps>(({ arguments: args, messageId, state,
return (
<PageContents
messageId={messageId}
results={(state as CrawlPluginState).results}
urls={args.urls}
results={(state as unknown as CrawlPluginState).results}
urls={args.urls as string[]}
/>
);
}
@@ -3,6 +3,7 @@ import {
FileIcon,
FlaskConicalIcon,
ImageIcon,
type LucideIcon,
MapIcon,
MusicIcon,
NewspaperIcon,
@@ -11,7 +12,7 @@ import {
VideoIcon,
} from 'lucide-react';
export const CATEGORY_ICON_MAP: Record<string, any> = {
export const CATEGORY_ICON_MAP: Record<string, LucideIcon> = {
files: FileIcon,
general: SearchIcon,
images: ImageIcon,
+2 -2
View File
@@ -23,7 +23,7 @@ import { type BuiltinIntervention } from '@lobechat/types';
* Organized by toolset (identifier) -> API name
* Only register APIs that have custom intervention UI
*/
export const BuiltinToolInterventions: Record<string, Record<string, any>> = {
export const BuiltinToolInterventions: Record<string, Record<string, unknown>> = {
[AgentBuilderManifest.identifier]: AgentBuilderInterventions,
[CloudSandboxManifest.identifier]: CloudSandboxInterventions,
[GroupManagementManifest.identifier]: GroupManagementInterventions,
@@ -47,5 +47,5 @@ export const getBuiltinIntervention = (
const toolset = BuiltinToolInterventions[identifier];
if (!toolset) return undefined;
return toolset[apiName];
return toolset[apiName] as BuiltinIntervention | undefined;
};
+4 -4
View File
@@ -15,13 +15,13 @@ import { type BuiltinPlaceholder } from '@lobechat/types';
* Builtin tools placeholders registry
* Organized by toolset (identifier) -> API name
*/
export const BuiltinToolPlaceholders: Record<string, Record<string, any>> = {
export const BuiltinToolPlaceholders: Record<string, Record<string, unknown>> = {
[LocalSystemIdentifier]: {
[LocalSystemApiName.searchLocalFiles]: LocalSystemSearchFilesPlaceholder,
[LocalSystemApiName.listLocalFiles]: LocalSystemListFilesPlaceholder,
},
[NotebookIdentifier]: NotebookPlaceholders as Record<string, any>,
[WebBrowsingManifest.identifier]: WebBrowsingPlaceholders as Record<string, any>,
[NotebookIdentifier]: NotebookPlaceholders as Record<string, unknown>,
[WebBrowsingManifest.identifier]: WebBrowsingPlaceholders as Record<string, unknown>,
};
/**
@@ -38,5 +38,5 @@ export const getBuiltinPlaceholder = (
const toolset = BuiltinToolPlaceholders[identifier];
if (!toolset) return undefined;
return toolset[apiName];
return toolset[apiName] as BuiltinPlaceholder | undefined;
};
+1 -4
View File
@@ -11,10 +11,7 @@ export const merge: typeof _merge = <T = object>(target: T, source: T) =>
if (Array.isArray(obj)) return src;
});
type MergeableItem = {
[key: string]: any;
id: string;
};
type MergeableItem = object & { id: string };
/**
* Merge two arrays based on id, preserving metadata from default items
@@ -1,3 +1,4 @@
import type { ToolSchema } from '@lobechat/types';
import { describe, expect, it } from 'vitest';
import { ToolNameResolver } from '../ToolNameResolver';
@@ -635,11 +636,11 @@ describe('ToolNameResolver', () => {
name: apiName,
parameters: {
properties: {
prompt: { type: 'string' },
size: { type: 'string' },
prompt: { type: 'string' as const },
size: { type: 'string' as const },
},
type: 'object',
},
type: 'object' as const,
} as ToolSchema,
},
],
identifier,
@@ -33,6 +33,7 @@ export type {
ToolsGenerationContext,
ToolsGenerationResult,
ToolSource,
UniformTool,
} from './types';
// Utility functions
@@ -1,4 +1,12 @@
import type { ExtendedHumanInterventionConfig } from '@/types/index';
import type { ExtendedHumanInterventionConfig, ToolSchema } from '@/types/index';
interface LobeToolManifestMeta {
avatar?: string;
description?: string;
readme?: string;
tags?: string[];
title?: string;
}
export interface LobeChatPluginApi {
description: string;
@@ -16,14 +24,15 @@ export interface LobeChatPluginApi {
*/
humanIntervention?: ExtendedHumanInterventionConfig;
name: string;
parameters: Record<string, any>;
parameters: ToolSchema;
url?: string;
}
export interface LobeToolManifest {
api: LobeChatPluginApi[];
humanIntervention?: ExtendedHumanInterventionConfig;
identifier: string;
meta: any;
meta: LobeToolManifestMeta;
systemRole?: string;
type?: 'default' | 'standalone' | 'markdown' | 'mcp' | 'builtin';
}
@@ -31,13 +40,15 @@ export interface LobeToolManifest {
/**
* Tools generation context
*/
export interface ToolsGenerationContext {
/** Additional extension context */
[key: string]: any;
export interface ToolsGenerationContextOverrides {}
export interface ToolsGenerationContext extends ToolsGenerationContextOverrides {
/** Whether image generation is allowed */
allowImageGeneration?: boolean;
/** Environment information */
environment?: 'desktop' | 'web';
/** Whether the tool was explicitly activated and should bypass normal filters */
isExplicitActivation?: boolean;
/** Whether search is enabled */
isSearchEnabled?: boolean;
/** Model name for context-aware plugin filtering */
@@ -135,12 +146,10 @@ export interface UniformFunctions {
name: string;
/**
* The parameters the functions accepts, described as a JSON Schema object. See the [guide](/docs/guides/gpt/function-calling) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format.
* @type {{ [key: string]: any }}
* @type {Record<string, unknown>}
* @memberof UniformFunctions
*/
parameters?: {
[key: string]: any;
};
parameters?: ToolSchema;
}
export interface UniformTool {
@@ -144,6 +144,8 @@ export class GroupRoleTransformProcessor extends BaseProcessor {
* Transform an assistant message from another agent to a user message
*/
private transformAssistantMessage(msg: Message): Message {
if (!msg.agentId) return msg;
const agentInfo = this.config.agentMap[msg.agentId];
if (!agentInfo) {
// No agent info found, keep original
@@ -177,6 +179,8 @@ export class GroupRoleTransformProcessor extends BaseProcessor {
* Transform a tool result message from another agent to a user message
*/
private transformToolMessage(msg: Message): Message {
if (!msg.agentId) return msg;
const agentInfo = this.config.agentMap[msg.agentId];
if (!agentInfo) {
// No agent info found, keep original
@@ -28,12 +28,11 @@ interface MinimalMessage {
metadata?: {
agentCouncil?: boolean;
compare?: boolean;
[key: string]: any;
} | null;
parentId?: string;
role: string;
tool_call_id?: string;
tools?: Array<{ id: string; [key: string]: any }>;
tools?: Array<{ id: string }>;
}
/**
@@ -72,7 +72,7 @@ export class ToolMessageReorder extends BaseProcessor {
});
// 2. Collect all valid tool messages
const toolMessages: Record<string, any> = {};
const toolMessages: Record<string, unknown> = {};
messages.forEach((message) => {
if (
message.role === 'tool' &&
@@ -67,17 +67,17 @@ describe('AgentCouncilFlattenProcessor', () => {
expect(result.messages).toHaveLength(2);
// Check first assistant message
const assistantMsg1 = result.messages[0];
const assistantMsg1 = result.messages[0]!;
expect(assistantMsg1.role).toBe('assistant');
expect(assistantMsg1.id).toBe('msg-agent-backend-1');
expect(assistantMsg1.content).toBe('Backend perspective content');
expect(assistantMsg1.agentId).toBe('agent-backend');
expect(assistantMsg1.model).toBe('gpt-4');
expect(assistantMsg1.provider).toBe('openai');
expect(assistantMsg1.meta.title).toBe('Backend Developer');
expect(assistantMsg1.meta!.title).toBe('Backend Developer');
// Check second assistant message
const assistantMsg2 = result.messages[1];
const assistantMsg2 = result.messages[1]!;
expect(assistantMsg2.role).toBe('assistant');
expect(assistantMsg2.id).toBe('msg-agent-devops-1');
expect(assistantMsg2.content).toBe('DevOps perspective content');
@@ -140,13 +140,13 @@ describe('AgentCouncilFlattenProcessor', () => {
expect(result.messages).toHaveLength(2);
// Check assistant message
const assistantMsg = result.messages[0];
const assistantMsg = result.messages[0]!;
expect(assistantMsg.role).toBe('assistant');
expect(assistantMsg.tools).toHaveLength(1);
expect(assistantMsg.tools[0].id).toBe('tool-1');
expect(assistantMsg.tools![0].id).toBe('tool-1');
// Check tool message
const toolMsg = result.messages[1];
const toolMsg = result.messages[1]!;
expect(toolMsg.role).toBe('tool');
expect(toolMsg.id).toBe('msg-tool-1');
expect(toolMsg.content).toBe('Search result');
@@ -585,7 +585,7 @@ describe('AgentCouncilFlattenProcessor', () => {
expect(result.messages[0].agentId).toBe('agent-backend');
expect(result.messages[0].model).toBe('gpt-4');
expect(result.messages[0].provider).toBe('openai');
expect(result.messages[0].meta.title).toBe('Backend Developer');
expect(result.messages[0]!.meta!.title).toBe('Backend Developer');
expect(result.messages[0].content).toContain('Backend Developer');
// Check second agent (DevOps Engineer)
@@ -593,12 +593,12 @@ describe('AgentCouncilFlattenProcessor', () => {
expect(result.messages[1].agentId).toBe('agent-devops');
expect(result.messages[1].model).toBe('claude-3-5-sonnet-20241022');
expect(result.messages[1].provider).toBe('anthropic');
expect(result.messages[1].meta.title).toBe('DevOps Engineer');
expect(result.messages[1]!.meta!.title).toBe('DevOps Engineer');
// Check third agent (Software Architect)
expect(result.messages[2].id).toBe('msg-agent-architect-1');
expect(result.messages[2].agentId).toBe('agent-architect');
expect(result.messages[2].meta.title).toBe('Software Architect');
expect(result.messages[2]!.meta!.title).toBe('Software Architect');
// Check metadata
expect(result.metadata.agentCouncilMessagesFlattened).toBe(1);
@@ -56,12 +56,12 @@ describe('GroupMessageFlattenProcessor', () => {
expect(result.messages).toHaveLength(2);
// Check assistant message
const assistantMsg = result.messages[0];
const assistantMsg = result.messages[0]!;
expect(assistantMsg.role).toBe('assistant');
expect(assistantMsg.id).toBe('msg-1');
expect(assistantMsg.content).toBe('Checking weather');
expect(assistantMsg.tools).toHaveLength(1);
expect(assistantMsg.tools[0]).toEqual({
expect(assistantMsg.tools![0]).toEqual({
id: 'tool-1',
type: 'builtin',
apiName: 'search',
@@ -475,7 +475,7 @@ describe('GroupMessageFlattenProcessor', () => {
const context = createContext(input);
const result = await processor.process(context);
const assistantMsg = result.messages[0];
const assistantMsg = result.messages[0]!;
expect(assistantMsg.parentId).toBe('parent-1');
expect(assistantMsg.threadId).toBe('thread-1');
expect(assistantMsg.groupId).toBe('group-1');
@@ -669,8 +669,8 @@ describe('GroupMessageFlattenProcessor', () => {
expect(assistantMsg.role).toBe('assistant');
expect(assistantMsg.id).toBe('msg_LnIlOyMUnX1ylf');
expect(assistantMsg.tools).toHaveLength(1);
expect(assistantMsg.tools[0].identifier).toBe('lobe-web-browsing');
expect(assistantMsg.tools[0].apiName).toBe('search');
expect(assistantMsg.tools![0].identifier).toBe('lobe-web-browsing');
expect(assistantMsg.tools![0].apiName).toBe('search');
expect(assistantMsg.reasoning).toBeDefined();
expect(assistantMsg.topicId).toBe('tpc_WQ1wRvxdDpLw');
expect(assistantMsg.parentId).toBe('msg_ekwWzxAKueHkd6');
@@ -17,7 +17,7 @@ vi.mock('@lobechat/utils/imageToBase64', async (importOriginal) => {
const createContext = (messages: UIChatMessage[]): PipelineContext => ({
initialState: { messages: [] } as any,
messages,
messages: messages as unknown as PipelineContext['messages'],
metadata: { model: 'gpt-4', provider: 'openai', maxTokens: 100000 },
isAborted: false,
});
@@ -241,7 +241,7 @@ Result content here`,
expect(result.messages[0].id).toBe('msg-1');
expect(result.messages[0].createdAt).toBe(1234567890);
expect(result.messages[0].extra).toEqual({ foo: 'bar' });
expect(result.messages[0].metadata.customField).toBe('value');
expect(result.messages[0]!.metadata!.customField).toBe('value');
});
});
});
@@ -128,7 +128,7 @@ describe('ToolCallProcessor', () => {
const result = await processor.process(context);
expect(result.messages[0].tool_calls[0].thoughtSignature).toBeUndefined();
expect(result.messages[0]!.tool_calls![0].thoughtSignature).toBeUndefined();
});
it('should use custom genToolCallingName function', async () => {
@@ -160,7 +160,7 @@ describe('ToolCallProcessor', () => {
const result = await processor.process(context);
expect(genToolCallingName).toHaveBeenCalledWith('web', 'search', 'builtin');
expect(result.messages[0].tool_calls[0].function.name).toBe('custom_web_search_builtin');
expect(result.messages[0]!.tool_calls![0].function.name).toBe('custom_web_search_builtin');
});
it('should handle multiple tool calls', async () => {
@@ -188,9 +188,9 @@ describe('ToolCallProcessor', () => {
const result = await processor.process(context);
expect(result.messages[0].tool_calls).toHaveLength(2);
expect(result.messages[0].tool_calls[0].function.name).toBe('web.search');
expect(result.messages[0].tool_calls[1].function.name).toBe('utils.translate');
expect(result.messages[0]!.tool_calls).toHaveLength(2);
expect(result.messages[0]!.tool_calls![0].function.name).toBe('web.search');
expect(result.messages[0]!.tool_calls![1].function.name).toBe('utils.translate');
});
it('should remove tool_calls and tools when not supported', async () => {
@@ -36,11 +36,11 @@ export interface OfficialToolItem {
export interface AgentBuilderContext {
/** Agent configuration */
config?: {
chatConfig?: Record<string, any>;
chatConfig?: Record<string, unknown>;
model?: string;
openingMessage?: string;
openingQuestions?: string[];
params?: Record<string, any>;
params?: Record<string, unknown>;
plugins?: string[];
provider?: string;
systemRole?: string;
@@ -85,13 +85,13 @@ describe('AgentManagementContextInjector', () => {
expect(result.messages).toHaveLength(3);
expect(result.messages[1].content).toBe('Let @Designer Agent help me');
const delegationMsg = result.messages[2];
const delegationMsg = result.messages[2]!;
expect(delegationMsg.role).toBe('user');
expect(delegationMsg.content).toContain('<mentioned_agents>');
expect(delegationMsg.content).toContain('agt_designer');
expect(delegationMsg.content).toContain('Designer Agent');
expect(delegationMsg.content).toContain('MUST use the callAgent tool');
expect(delegationMsg.meta.injectType).toBe('agent-mention-delegation');
expect(delegationMsg.meta!.injectType).toBe('agent-mention-delegation');
});
it('should inject after the LAST user message, not the first', async () => {
+37 -6
View File
@@ -12,11 +12,14 @@ import type { UIChatMessage } from '@lobechat/types';
*/
export interface PipelineContextMetadataOverrides {}
export interface ContextEngineAgentStateOverrides {}
export interface PipelineMessageOverrides {}
/**
* Agent state - inferred from original project types
*/
export interface AgentState {
[key: string]: any;
export interface AgentState extends ContextEngineAgentStateOverrides {
messages: UIChatMessage[];
model?: string;
provider?: string;
@@ -45,10 +48,38 @@ export interface MessageToolCall {
thoughtSignature?: string;
type: 'function';
}
export interface Message {
[key: string]: any;
export interface Message extends PipelineMessageOverrides {
agentId?: string | 'supervisor';
agentName?: string;
children?: any[];
content: string | any[];
createdAt?: number | string;
error?: unknown;
extra?: any;
groupId?: string;
id?: string;
imageList?: unknown[];
members?: any[];
meta?: Record<string, unknown>;
metadata?: Record<string, unknown> | null;
model?: string | null;
name?: string;
parentId?: string;
plugin?: any;
pluginError?: unknown;
pluginState?: any;
provider?: string | null;
reasoning?: unknown;
role: string;
targetId?: string | null;
tasks?: any[];
threadId?: string | null;
tool_call_id?: string;
tool_calls?: MessageToolCall[];
tools?: any[];
topicId?: string;
updatedAt?: number | string;
}
/**
@@ -112,7 +143,7 @@ export interface PipelineResult {
/** Whether aborted */
isAborted: boolean;
/** Final processed messages */
messages: any[];
messages: Message[];
/** Metadata from processing */
metadata: PipelineContextMetadata;
/** Execution statistics */
@@ -164,7 +195,7 @@ export interface FileContext {
export interface RetrievalChunk {
content: string;
id: string;
metadata?: Record<string, any>;
metadata?: Record<string, unknown>;
similarity: number;
}
+1 -1
View File
@@ -98,7 +98,7 @@ export function parse(messages: Message[], messageGroups?: MessageGroupMetadata[
processedMessage.tools.length > 0 &&
processedMessage.metadata
) {
const cleanedMetadata: Record<string, any> = {};
const cleanedMetadata: Record<string, unknown> = {};
Object.entries(processedMessage.metadata).forEach(([key, value]) => {
if (usagePerformanceFields.has(key)) {
cleanedMetadata[key] = value;
@@ -21,7 +21,7 @@ export class FlatListBuilder {
private branchResolver: BranchResolver,
private messageCollector: MessageCollector,
private messageTransformer: MessageTransformer,
) { }
) {}
/**
* Generate flatList from messages array
@@ -793,7 +793,7 @@ export class FlatListBuilder {
const msgPerformance = assistant.performance || metaPerformance;
// Extract non-usage/performance metadata fields
const otherMetadata: Record<string, any> = {};
const otherMetadata: Record<string, unknown> = {};
if (assistant.metadata) {
const usagePerformanceFields = new Set([
'acceptedPredictionTokens',
@@ -851,7 +851,7 @@ export class FlatListBuilder {
const aggregated = this.messageTransformer.aggregateMetadata(children);
// Collect all non-usage/performance metadata from all children
const groupMetadata: Record<string, any> = {};
const groupMetadata: Record<string, unknown> = {};
children.forEach((child) => {
if ((child as any).metadata) {
Object.assign(groupMetadata, (child as any).metadata);
@@ -936,7 +936,7 @@ export class FlatListBuilder {
const msgPerformance = message.performance || metaPerformance;
// Extract non-usage/performance metadata fields
const otherMetadata: Record<string, any> = {};
const otherMetadata: Record<string, unknown> = {};
if (message.metadata) {
const usagePerformanceFields = new Set([
'acceptedPredictionTokens',
+3 -3
View File
@@ -407,10 +407,10 @@ export class AgentModel {
// First process the params field: undefined means delete, null means disable flag
const existingParams = agent.params ?? {};
const updatedParams: Record<string, any> = { ...existingParams };
const updatedParams: Record<string, unknown> = { ...existingParams };
if (data.params) {
const incomingParams = data.params as Record<string, any>;
const incomingParams = data.params as Record<string, unknown>;
Object.keys(incomingParams).forEach((key) => {
const incomingValue = incomingParams[key];
@@ -435,7 +435,7 @@ export class AgentModel {
// Final cleanup: ensure no undefined or null values enter the database
if (mergedValue.params) {
const params = mergedValue.params as Record<string, any>;
const params = mergedValue.params as Record<string, unknown>;
Object.keys(params).forEach((key) => {
if (params[key] === undefined) {
delete params[key];
+8 -8
View File
@@ -1361,7 +1361,7 @@ export class MessageModel {
}
// 2. Handle metadata merge if provided
let mergedMetadata: Record<string, any> | undefined;
let mergedMetadata: Record<string, unknown> | undefined;
if (metadata) {
const [existingMessage] = await trx
.select({ metadata: messages.metadata })
@@ -1389,7 +1389,7 @@ export class MessageModel {
}
};
updateMetadata = async (id: string, metadata: Record<string, any>) => {
updateMetadata = async (id: string, metadata: Record<string, unknown>) => {
const item = await this.db.query.messages.findFirst({
where: and(eq(messages.id, id), eq(messages.userId, this.userId)),
});
@@ -1402,7 +1402,7 @@ export class MessageModel {
.where(and(eq(messages.userId, this.userId), eq(messages.id, id)));
};
updatePluginState = async (id: string, state: Record<string, any>): Promise<void> => {
updatePluginState = async (id: string, state: Record<string, unknown>): Promise<void> => {
const item = await this.db.query.messagePlugins.findFirst({
where: eq(messagePlugins.id, id),
});
@@ -1431,9 +1431,9 @@ export class MessageModel {
id: string,
params: {
content?: string;
metadata?: Record<string, any>;
pluginError?: any;
pluginState?: Record<string, any>;
metadata?: Record<string, unknown>;
pluginError?: unknown;
pluginState?: Record<string, unknown>;
},
): Promise<{ success: boolean }> => {
const { content, metadata, pluginState, pluginError } = params;
@@ -1442,7 +1442,7 @@ export class MessageModel {
await this.db.transaction(async (trx) => {
// Update messages table (content, metadata)
if (content !== undefined || metadata !== undefined) {
const messageUpdateData: Record<string, any> = {};
const messageUpdateData: Record<string, unknown> = {};
if (content !== undefined) {
messageUpdateData.content = content;
@@ -1471,7 +1471,7 @@ export class MessageModel {
});
if (pluginItem) {
const pluginUpdateData: Record<string, any> = {};
const pluginUpdateData: Record<string, unknown> = {};
if (pluginState !== undefined) {
pluginUpdateData.state = merge(pluginItem.state || {}, pluginState);
+3 -3
View File
@@ -507,10 +507,10 @@ export class SessionModel {
// First process the params field: undefined means delete, null means disable flag
const existingParams = session.agent.params ?? {};
const updatedParams: Record<string, any> = { ...existingParams };
const updatedParams: Record<string, unknown> = { ...existingParams };
if (data.params) {
const incomingParams = data.params as Record<string, any>;
const incomingParams = data.params as Record<string, unknown>;
Object.keys(incomingParams).forEach((key) => {
const incomingValue = incomingParams[key];
@@ -535,7 +535,7 @@ export class SessionModel {
// Final cleanup: ensure no undefined or null values enter the database
if (mergedValue.params) {
const params = mergedValue.params as Record<string, any>;
const params = mergedValue.params as Record<string, unknown>;
Object.keys(params).forEach((key) => {
if (params[key] === undefined) {
delete params[key];
@@ -11,11 +11,11 @@ export interface KnowledgeItem {
chunkTaskId?: string | null;
content?: string | null;
createdAt: Date;
editorData?: Record<string, any> | null;
editorData?: Record<string, unknown> | null;
embeddingTaskId?: string | null;
fileType: string;
id: string;
metadata?: Record<string, any> | null;
metadata?: Record<string, unknown> | null;
name: string;
size: number;
slug?: string | null;
@@ -20,16 +20,16 @@ interface PreparedMessage {
agentId: string;
content: string;
createdAt: Date;
error?: Record<string, any> | null;
error?: unknown | null;
id: string;
metadata?: Record<string, any> | null;
metadata?: unknown | null;
model?: string | null;
parentId: string | null;
provider?: string | null;
reasoning?: Record<string, any> | null;
reasoning?: unknown | null;
role: string;
search?: Record<string, any> | null;
tools?: Record<string, any>[] | null;
search?: unknown | null;
tools?: unknown[] | null;
topicId: string;
traceId?: string | null;
updatedAt: Date;
@@ -39,10 +39,10 @@ interface PreparedMessage {
interface PreparedMessagePlugin {
apiName?: string | null;
arguments?: string | null;
error?: Record<string, any> | null;
error?: unknown | null;
id: string;
identifier?: string | null;
state?: Record<string, any> | null;
state?: unknown | null;
toolCallId?: string | null;
type?: string | null;
userId: string;
@@ -208,7 +208,14 @@ export class TopicImporterRepo {
// If message has plugin data (tool messages), prepare plugin record
if (msg.plugin || msg.pluginState || msg.pluginError || msg.tool_call_id) {
const plugin = msg.plugin as Record<string, any> | undefined;
const plugin = msg.plugin as
| {
apiName?: string;
arguments?: string;
identifier?: string;
type?: string;
}
| undefined;
preparedPlugins.push({
apiName: plugin?.apiName || null,
arguments: plugin?.arguments || null,
@@ -111,7 +111,7 @@ export const agentDocuments = pgTable(
* Canonical behavior config for context/retrieval policy.
* Keep extensible fields here.
*/
policy: jsonb('policy').$type<Record<string, any>>(),
policy: jsonb('policy').$type<Record<string, unknown>>(),
/**
* Indexed projection of policy.context.position for fast filtering/sorting.
*/
+1 -1
View File
@@ -30,7 +30,7 @@ export const agentSkills = pgTable(
// Content and editor state
content: text('content'),
editorData: jsonb('editor_data').$type<Record<string, any>>(),
editorData: jsonb('editor_data').$type<Record<string, unknown>>(),
// Resource mapping: Record<VirtualPath, SkillResourceMeta>
resources: jsonb('resources').$type<Record<string, SkillResourceMeta>>().default({}),
+1 -1
View File
@@ -33,7 +33,7 @@ export const chatGroups = pgTable(
backgroundColor: text('background_color'),
marketIdentifier: text('market_identifier'),
content: text('content'),
editorData: jsonb('editor_data').$type<Record<string, any>>(),
editorData: jsonb('editor_data').$type<Record<string, unknown>>(),
config: jsonb('config').$type<ChatGroupConfig>(),
+2 -2
View File
@@ -66,7 +66,7 @@ export const documents = pgTable(
totalLineCount: integer('total_line_count').notNull(),
// Metadata
metadata: jsonb('metadata').$type<Record<string, any>>(),
metadata: jsonb('metadata').$type<Record<string, unknown>>(),
// Page/chunk data
pages: jsonb('pages').$type<LobeDocumentPage[]>(),
@@ -95,7 +95,7 @@ export const documents = pgTable(
.notNull(),
clientId: text('client_id'),
editorData: jsonb('editor_data').$type<Record<string, any>>(),
editorData: jsonb('editor_data').$type<Record<string, unknown>>(),
slug: varchar('slug', { length: 255 }).$defaultFn(() => randomSlug(3)),
@@ -8,7 +8,7 @@ export interface LocalFileItem {
lastAccessTime: Date;
// Spotlight specific metadata
metadata?: {
[key: string]: any;
[key: string]: unknown;
};
modifiedTime: Date;
name: string;
@@ -8,7 +8,7 @@ export interface ShortcutConfig {
*/
id: string;
}
export type ShortcutActionType = Record<string, any>;
export type ShortcutActionType = Record<string, unknown>;
export interface ShortcutUpdateResult {
errorType?:
@@ -3,7 +3,7 @@ import * as Papa from 'papaparse';
import type { ParseOptions, ParseResult } from '../types';
export function parseCSV(content: string, options?: ParseOptions): ParseResult {
const result = Papa.parse<Record<string, any>>(content, {
const result = Papa.parse<Record<string, unknown>>(content, {
delimiter: options?.csvDelimiter,
dynamicTyping: true,
header: true,
@@ -2,10 +2,7 @@ import * as XLSX from 'xlsx';
import type { ParseOptions, ParseResult } from '../types';
export function parseXLSX(
data: Buffer | Uint8Array,
options?: ParseOptions,
): ParseResult {
export function parseXLSX(data: Buffer | Uint8Array, options?: ParseOptions): ParseResult {
const workbook = XLSX.read(data, { type: 'array' });
// Select sheet
@@ -23,7 +20,7 @@ export function parseXLSX(
return { format: 'xlsx', headers: [], metadata: { sheetName }, rows: [], totalCount: 0 };
}
const allRows = XLSX.utils.sheet_to_json<Record<string, any>>(worksheet, {
const allRows = XLSX.utils.sheet_to_json<Record<string, unknown>>(worksheet, {
defval: '',
raw: false,
});
+1 -1
View File
@@ -14,6 +14,6 @@ export interface ParseResult {
metadata?: {
sheetName?: string;
};
rows: Record<string, any>[];
rows: Record<string, unknown>[];
totalCount: number;
}
@@ -12,7 +12,7 @@ const log = debug('file-loaders:excel');
* Converts sheet data (array of objects) to a Markdown table string.
* Handles empty sheets and escapes pipe characters.
*/
function sheetToMarkdownTable(jsonData: Record<string, any>[]): string {
function sheetToMarkdownTable(jsonData: Record<string, unknown>[]): string {
log('Converting sheet data to Markdown table, rows:', jsonData?.length || 0);
if (!jsonData || jsonData.length === 0) {
log('Sheet is empty, returning placeholder message');
@@ -71,7 +71,7 @@ export class ExcelLoader implements FileLoaderInterface {
log(`Processing sheet: ${sheetName}`);
const worksheet = workbook.Sheets[sheetName];
// Use sheet_to_json to get array of objects for our custom markdown function
const jsonData = xlsx.utils.sheet_to_json<Record<string, any>>(worksheet, {
const jsonData = xlsx.utils.sheet_to_json<Record<string, unknown>>(worksheet, {
// Get formatted strings, not raw values
defval: '',
raw: false, // Use empty string for blank cells
+3 -3
View File
@@ -33,7 +33,7 @@ export interface FileDocument {
/**
* Allows adding other file-level metadata.
*/
[key: string]: any;
[key: string]: unknown;
/**
* Document author (if available).
*/
@@ -98,7 +98,7 @@ export interface DocumentPage {
/**
* Allows adding other page/chunk-specific metadata.
*/
[key: string]: any;
[key: string]: unknown;
/**
* If the original file unit is further divided into chunks, this is the index of the current chunk.
@@ -189,7 +189,7 @@ export interface FileLoaderInterface {
*/
aggregateContent: (pages: DocumentPage[]) => Promise<string>;
attachDocumentMetadata?: (filePath: string) => Promise<Record<string, any>>;
attachDocumentMetadata?: (filePath: string) => Promise<Record<string, unknown>>;
/**
* Loads file content based on the file path and splits it into logical pages/chunks.
@@ -4,7 +4,14 @@ import { join } from 'node:path';
import { renderPlaceholderTemplate } from '@lobechat/context-engine';
interface TracePayload {
agentCalls?: Record<string, any>;
agentCalls?: Record<
string,
{
request?: {
messages?: Array<{ content?: string }>;
};
}
>;
contexts?: {
trimmed?: {
retrievedContexts?: string[];
+1 -1
View File
@@ -60,7 +60,7 @@ export abstract class LobeOpenAICompatibleRuntime {
abstract generateObject(
payload: GenerateObjectPayload,
options?: GenerateObjectOptions,
): Promise<Record<string, any>>;
): Promise<Record<string, unknown>>;
abstract models(): Promise<AIBaseModelCard[]>;
@@ -40,7 +40,7 @@ import type { ApiType, RuntimeClass } from './apiTypes';
const log = debug('lobe-model-runtime:router-runtime');
interface ProviderIniOptions extends Record<string, any> {
interface ProviderIniOptions extends Record<string, unknown> {
accessKeyId?: string;
accessKeySecret?: string;
apiKey?: string;
@@ -74,12 +74,13 @@ interface RouterInstance {
runtime?: RuntimeClass;
}
type ConstructorOptions<T extends Record<string, any> = any> = ClientOptions & T;
type ConstructorOptions<T extends Record<string, unknown> = Record<string, unknown>> =
ClientOptions & T;
type Routers =
| RouterInstance[]
| ((
options: ClientOptions & Record<string, any>,
options: ClientOptions & Record<string, unknown>,
runtimeContext: {
model?: string;
},
@@ -100,7 +101,9 @@ export interface RouteAttemptResult {
userId?: string;
}
export interface CreateRouterRuntimeOptions<T extends Record<string, any> = any> {
export interface CreateRouterRuntimeOptions<
T extends Record<string, unknown> = Record<string, unknown>,
> {
apiKey?: string;
chatCompletion?: {
excludeUsage?: boolean;
@@ -139,7 +142,7 @@ export interface CreateRouterRuntimeOptions<T extends Record<string, any> = any>
chatCompletion: () => boolean;
responses?: () => boolean;
};
defaultHeaders?: Record<string, any>;
defaultHeaders?: Record<string, unknown>;
errorType?: {
bizError: ILobeAgentRuntimeErrorType;
invalidAPIKey: ILobeAgentRuntimeErrorType;
@@ -172,12 +175,12 @@ export const createRouterRuntime = ({
...params
}: CreateRouterRuntimeOptions) => {
return class UniformRuntime implements LobeRuntimeAI {
public _options: ClientOptions & Record<string, any>;
public _options: ClientOptions & Record<string, unknown>;
private _routers: Routers;
private _params: any;
private _id: string;
constructor(options: ClientOptions & Record<string, any> = {}) {
constructor(options: ClientOptions & Record<string, unknown> = {}) {
this._options = {
...options,
apiKey: options.apiKey?.trim() || DEFAULT_API_KEY,
@@ -262,7 +265,7 @@ export const createRouterRuntime = ({
*/
if (resolvedApiType === 'vertexai') {
const { apiKey, googleAuthOptions, project, location, ...restOptions } = finalOptions;
const credentials = safeParseJSON<Record<string, any>>(apiKey);
const credentials = safeParseJSON<Record<string, unknown>>(apiKey);
const vertexOptions: GoogleGenAIOptions = {
...(restOptions as GoogleGenAIOptions),
vertexai: true,
@@ -328,6 +331,8 @@ export const createRouterRuntime = ({
runtime,
} = await this.createRuntimeFromOption(matchedRouter, optionItem);
const userId = typeof this._options.userId === 'string' ? this._options.userId : undefined;
try {
const result = await requestHandler(runtime);
@@ -363,7 +368,7 @@ export const createRouterRuntime = ({
remark,
routerId: matchedRouter.id,
success: true,
userId: this._options.userId,
userId,
})
.catch((e) => {
log('onRouteAttempt callback error: %O', e);
@@ -386,7 +391,7 @@ export const createRouterRuntime = ({
remark,
routerId: matchedRouter.id,
success: false,
userId: this._options.userId,
userId,
})
.catch((e) => {
log('onRouteAttempt callback error: %O', e);
@@ -37,17 +37,20 @@ import { handleAnthropicError } from './handleAnthropicError';
import { resolveCacheTTL } from './resolveCacheTTL';
import { resolveMaxTokens } from './resolveMaxTokens';
type ConstructorOptions<T extends Record<string, any> = any> = ClientOptions & T;
type ConstructorOptions<T extends Record<string, unknown> = Record<string, unknown>> =
ClientOptions & T;
type AnthropicTools = Anthropic.Tool | Anthropic.WebSearchTool20250305;
export const DEFAULT_ANTHROPIC_BASE_URL = 'https://api.anthropic.com';
export interface CustomClientOptions<T extends Record<string, any> = any> {
export interface CustomClientOptions<T extends Record<string, unknown> = Record<string, unknown>> {
createClient?: (options: ConstructorOptions<T>) => Anthropic;
}
export interface AnthropicCompatibleFactoryOptions<T extends Record<string, any> = any> {
export interface AnthropicCompatibleFactoryOptions<
T extends Record<string, unknown> = Record<string, unknown>,
> {
apiKey?: string;
baseURL?: string;
chatCompletion?: {
@@ -100,7 +103,9 @@ export interface AnthropicCompatibleFactoryOptions<T extends Record<string, any>
provider: string;
}
export interface AnthropicCompatibleParamsInput<T extends Record<string, any> = any> extends Omit<
export interface AnthropicCompatibleParamsInput<
T extends Record<string, any> = Record<string, never>,
> extends Omit<
AnthropicCompatibleFactoryOptions<T>,
'chatCompletion' | 'customClient' | 'generateObject' | 'models'
> {
@@ -242,7 +247,7 @@ export const resolveDefaultAnthropicPricingOptions = (
/**
* Create Anthropic SDK client with optional beta headers.
*/
export const createDefaultAnthropicClient = <T extends Record<string, any> = any>(
export const createDefaultAnthropicClient = <T extends Record<string, any> = Record<string, never>>(
options: ConstructorOptions<T>,
) => {
const betaHeaders = process.env.ANTHROPIC_BETA_HEADERS;
@@ -258,7 +263,7 @@ export const createDefaultAnthropicClient = <T extends Record<string, any> = any
/**
* Default Anthropic error handler with desensitized endpoint.
*/
export const handleDefaultAnthropicError = <T extends Record<string, any> = any>(
export const handleDefaultAnthropicError = <T extends Record<string, any> = Record<string, never>>(
error: any,
options: ConstructorOptions<T>,
): Omit<ChatCompletionErrorPayload, 'provider'> => {
@@ -363,7 +368,9 @@ export const createDefaultAnthropicModels = async ({
/**
* Build provider params by merging overrides with Anthropic defaults.
*/
export const createAnthropicCompatibleParams = <T extends Record<string, any> = any>(
export const createAnthropicCompatibleParams = <
T extends Record<string, any> = Record<string, never>,
>(
options: AnthropicCompatibleParamsInput<T>,
): AnthropicCompatibleFactoryOptions<T> => {
const {
@@ -390,7 +397,9 @@ export const createAnthropicCompatibleParams = <T extends Record<string, any> =
} as AnthropicCompatibleFactoryOptions<T>;
};
export const createAnthropicCompatibleRuntime = <T extends Record<string, any> = any>({
export const createAnthropicCompatibleRuntime = <
T extends Record<string, any> = Record<string, never>,
>({
provider,
baseURL: DEFAULT_BASE_URL = DEFAULT_ANTHROPIC_BASE_URL,
apiKey: DEFAULT_API_KEY,
@@ -416,7 +425,7 @@ export const createAnthropicCompatibleRuntime = <T extends Record<string, any> =
baseURL!: string;
protected _options: ConstructorOptions<T>;
constructor(options: ClientOptions & Record<string, any> = {}) {
constructor(options: ClientOptions & Record<string, unknown> = {}) {
const apiKey = typeof options.apiKey === 'string' ? options.apiKey.trim() : options.apiKey;
const baseURL =
typeof options.baseURL === 'string' ? options.baseURL.trim() : options.baseURL;
@@ -449,7 +458,7 @@ export const createAnthropicCompatibleRuntime = <T extends Record<string, any> =
}
this.baseURL = baseURL || this.client.baseURL;
this.id = options.id || provider;
this.id = typeof options.id === 'string' ? options.id : provider;
this.logPrefix = `lobe-model-runtime:${this.id}`;
}
@@ -1,8 +1,7 @@
import type Anthropic from '@anthropic-ai/sdk';
import { imageUrlToBase64 } from '@lobechat/utils';
import type OpenAI from 'openai';
import type { OpenAIChatMessage, UserMessageContentPart } from '../../types';
import type { ChatCompletionTool, OpenAIChatMessage, UserMessageContentPart } from '../../types';
import { parseDataUri } from '../../utils/uriParser';
const ANTHROPIC_SUPPORTED_IMAGE_TYPES = new Set([
@@ -341,7 +340,7 @@ export const buildAnthropicMessages = async (
};
export const buildAnthropicTools = (
tools?: OpenAI.ChatCompletionTool[],
tools?: ChatCompletionTool[],
options: { enabledContextCaching?: boolean } = {},
) => {
if (!tools) return;
@@ -259,7 +259,7 @@ const UNSUPPORTED_SCHEMA_KEYS = new Set(['examples', 'default']);
* Google's API doesn't support certain JSON Schema keywords like 'const'
* This function recursively processes the schema and converts unsupported keywords
*/
const sanitizeSchemaForGoogle = (schema: Record<string, any>): Record<string, any> => {
const sanitizeSchemaForGoogle = (schema: unknown): unknown => {
if (!schema || typeof schema !== 'object') return schema;
// Handle arrays
@@ -267,7 +267,7 @@ const sanitizeSchemaForGoogle = (schema: Record<string, any>): Record<string, an
return schema.map((item) => sanitizeSchemaForGoogle(item));
}
const result: Record<string, any> = {};
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(schema)) {
// Strip unsupported JSON Schema keywords (e.g. examples, default, $schema)
@@ -305,23 +305,34 @@ const sanitizeSchemaForGoogle = (schema: Record<string, any>): Record<string, an
*/
export const buildGoogleTool = (tool: ChatCompletionTool): FunctionDeclaration => {
const functionDeclaration = tool.function;
const parameters = functionDeclaration.parameters;
const parameters =
functionDeclaration.parameters && typeof functionDeclaration.parameters === 'object'
? (functionDeclaration.parameters as Record<string, unknown>)
: undefined;
// refs: https://github.com/lobehub/lobe-chat/pull/5002
const rawProperties =
parameters?.properties && Object.keys(parameters.properties).length > 0
parameters?.properties &&
typeof parameters.properties === 'object' &&
!Array.isArray(parameters.properties) &&
Object.keys(parameters.properties).length > 0
? parameters.properties
: { dummy: { type: 'string' } }; // dummy property to avoid empty object
// Sanitize properties to remove unsupported JSON Schema keywords for Google
const properties = sanitizeSchemaForGoogle(rawProperties);
const properties = sanitizeSchemaForGoogle(rawProperties) as Record<string, unknown>;
const description =
typeof parameters?.description === 'string' ? parameters.description : undefined;
const required = Array.isArray(parameters?.required)
? parameters.required.filter((item): item is string => typeof item === 'string')
: undefined;
return {
description: functionDeclaration.description,
name: functionDeclaration.name,
parameters: {
description: parameters?.description,
properties,
required: parameters?.required,
description,
properties: properties as any,
required,
type: SchemaType.OBJECT,
},
};
@@ -29,7 +29,7 @@ async function generateByImageMode(
['imageUrls', 'image'],
['imageUrl', 'image'],
]);
const userInput: Record<string, any> = Object.fromEntries(
const userInput: Record<string, unknown> = Object.fromEntries(
Object.entries(params).map(([key, value]) => [
paramsMap.get(key as RuntimeImageGenParamsValue) ?? key,
value,
@@ -46,15 +46,20 @@ async function generateByImageMode(
// If there are imageUrls parameters, convert them to File objects
if (isImageEdit) {
try {
const imageInput = userInput.image;
const imageUrls = Array.isArray(imageInput)
? imageInput.filter((url): url is string => typeof url === 'string')
: [];
// Convert all image URLs to File objects
const imageFiles = await Promise.all(
userInput.image.map((url: string) => convertImageUrlToFile(url)),
imageUrls.map((url: string) => convertImageUrlToFile(url)),
);
// According to official docs, if there are multiple images, pass an array; if only one, pass a single File
userInput.image = imageFiles.length === 1 ? imageFiles[0] : imageFiles;
} catch (error) {
throw new Error(`Failed to convert image URLs to File objects: ${error}`);
throw new Error(`Failed to convert image URLs to File objects: ${error}`, { cause: error });
}
} else {
delete userInput.image;
@@ -180,7 +185,7 @@ async function generateByChatModel(
});
log('Successfully processed image URL for chat input');
} catch (error) {
throw new Error(`Failed to process image URL: ${error}`);
throw new Error(`Failed to process image URL: ${error}`, { cause: error });
}
}
@@ -38,6 +38,7 @@ import { desensitizeUrl } from '../../utils/desensitizeUrl';
import { getModelPropertyWithFallback } from '../../utils/getFallbackModelProperty';
import { getModelPricing } from '../../utils/getModelPricing';
import { handleOpenAIError } from '../../utils/handleOpenAIError';
import { mergeHeaders, toHeadersInit } from '../../utils/headers';
import { isExceededContextWindowError } from '../../utils/isExceededContextWindowError';
import { isQuotaLimitError } from '../../utils/isQuotaLimitError';
import { postProcessModelList } from '../../utils/postProcessModelList';
@@ -67,7 +68,8 @@ export const CHAT_MODELS_BLOCK_LIST = [
'dall-e',
];
type ConstructorOptions<T extends Record<string, any> = any> = ClientOptions & T;
type ConstructorOptions<T extends Record<string, unknown> = Record<string, unknown>> =
ClientOptions & T;
export type CreateImageOptions = Omit<ClientOptions, 'apiKey'> & {
apiKey: string;
provider: string;
@@ -78,7 +80,7 @@ export type CreateVideoOptions = Omit<ClientOptions, 'apiKey'> & {
provider: string;
};
export interface CustomClientOptions<T extends Record<string, any> = any> {
export interface CustomClientOptions<T extends Record<string, unknown> = Record<string, unknown>> {
createChatCompletionStream?: (
client: any,
payload: ChatStreamPayload,
@@ -87,7 +89,9 @@ export interface CustomClientOptions<T extends Record<string, any> = any> {
createClient?: (options: ConstructorOptions<T>) => any;
}
export interface OpenAICompatibleFactoryOptions<T extends Record<string, any> = any> {
export interface OpenAICompatibleFactoryOptions<
T extends Record<string, unknown> = Record<string, unknown>,
> {
apiKey?: string;
baseURL?: string;
chatCompletion?: {
@@ -179,7 +183,9 @@ export interface OpenAICompatibleFactoryOptions<T extends Record<string, any> =
};
}
export const createOpenAICompatibleRuntime = <T extends Record<string, any> = any>({
export const createOpenAICompatibleRuntime = <
T extends Record<string, unknown> = Record<string, unknown>,
>({
provider,
baseURL: DEFAULT_BASE_URL,
apiKey: DEFAULT_API_KEY,
@@ -209,7 +215,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
baseURL!: string;
protected _options: ConstructorOptions<T>;
constructor(options: ClientOptions & Record<string, any> = {}) {
constructor(options: ClientOptions & Record<string, unknown> = {}) {
const _options = {
...options,
apiKey: options.apiKey?.trim() || DEFAULT_API_KEY,
@@ -231,7 +237,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
this.baseURL = baseURL || this.client.baseURL;
this.id = options.id || provider;
this.id = typeof options.id === 'string' ? options.id : provider;
this.logPrefix = `lobe-model-runtime:${this.id}`;
}
@@ -396,8 +402,8 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
if (targetBaseURL !== this.baseURL) {
const restOptions = {
...(this._options as ConstructorOptions<T> & Record<string, any>),
} as Record<string, any>;
...(this._options as ConstructorOptions<T> & Record<string, unknown>),
} as Record<string, unknown>;
const optionApiKey = restOptions.apiKey;
delete restOptions.apiKey;
delete restOptions.baseURL;
@@ -415,7 +421,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
baseURL: targetBaseURL,
...constructorOptions,
...restOptions,
} as ConstructorOptions<T> & Record<string, any>;
} as ConstructorOptions<T> & Record<string, unknown>;
this._options = nextOptions;
@@ -486,9 +492,9 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
response = (await this.client.chat.completions.create(finalPayload, {
// https://github.com/lobehub/lobe-chat/pull/318
headers: { Accept: '*/*', ...options?.requestHeaders },
headers: mergeHeaders({ Accept: '*/*' }, options?.requestHeaders),
signal: options?.signal,
})) as unknown as Stream<OpenAI.Chat.Completions.ChatCompletionChunk>;
} as any)) as unknown as Stream<OpenAI.Chat.Completions.ChatCompletionChunk>;
}
if (postPayload.stream) {
@@ -699,10 +705,10 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
messages,
model,
tool_choice: { function: { name: tool.function.name }, type: 'function' },
tools: [tool],
tools: [tool as unknown as OpenAI.ChatCompletionTool],
user: options?.user,
},
{ headers: options?.headers, signal: options?.signal },
{ headers: toHeadersInit(options?.headers), signal: options?.signal } as any,
);
if (res.usage) {
@@ -755,7 +761,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
text: { format: { strict: true, type: 'json_schema', ...processedSchema } },
user: options?.user,
},
{ headers: options?.headers, signal: options?.signal },
{ headers: toHeadersInit(options?.headers), signal: options?.signal } as any,
);
if (res.usage) {
@@ -780,10 +786,13 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
{
messages,
model,
response_format: { json_schema: processedSchema, type: 'json_schema' },
response_format: {
json_schema: processedSchema as any,
type: 'json_schema',
} as any,
user: options?.user,
},
{ headers: options?.headers, signal: options?.signal },
{ headers: toHeadersInit(options?.headers), signal: options?.signal } as any,
);
if (res.usage) {
await options?.onUsage?.(convertOpenAIUsage(res.usage, usagePayload));
@@ -830,7 +839,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
try {
const res = await this.client.embeddings.create(
{ ...payload, encoding_format: 'float', user: options?.user },
{ headers: options?.headers, signal: options?.signal },
{ headers: toHeadersInit(options?.headers), signal: options?.signal } as any,
);
if (res.usage && options?.onUsage) {
@@ -860,10 +869,13 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
);
try {
const mp3 = await this.client.audio.speech.create(payload as any, {
headers: options?.headers,
signal: options?.signal,
});
const mp3 = await this.client.audio.speech.create(
payload as any,
{
headers: toHeadersInit(options?.headers),
signal: options?.signal,
} as any,
);
const buffer = await mp3.arrayBuffer();
log('generated audio with size: %d bytes', buffer.byteLength);
return buffer;
@@ -1061,9 +1073,9 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
log('sending responses.create request');
const response = await this.client.responses.create(postPayload, {
headers: options?.requestHeaders,
headers: toHeadersInit(options?.requestHeaders),
signal: options?.signal,
});
} as any);
const streamOptions: OpenAIStreamOptions = {
bizErrorTypeTransformer: chatCompletion?.handleStreamBizErrorType,
@@ -1170,7 +1182,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
tools: tools!.map((tool) => this.convertChatCompletionToolToResponseTool(tool)),
user: options?.user,
},
{ headers: options?.headers, signal: options?.signal },
{ headers: toHeadersInit(options?.headers), signal: options?.signal } as any,
);
if (res.usage) {
@@ -1207,10 +1219,10 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
messages: msgs,
model,
tool_choice: 'required',
tools,
tools: tools as unknown as OpenAI.ChatCompletionTool[] | undefined,
user: options?.user,
},
{ headers: options?.headers, signal: options?.signal },
{ headers: toHeadersInit(options?.headers), signal: options?.signal } as any,
);
if (res.usage) {
@@ -6,7 +6,7 @@ const log = debug('lobe-cost:computeImagePricing');
export interface ImageGenerationParams {
// Other possible parameters for future extensions
[key: string]: any;
[key: string]: unknown;
quality?: 'standard' | 'hd';
size?: string;
}
@@ -40,7 +40,7 @@ export const computeImageCost = (
return undefined;
}
let pricePerImageInUSD = 0;
let pricePerImageInUSD: number;
let lookupKey: string | undefined;
switch (imageGenUnit.strategy) {
@@ -5,7 +5,7 @@ import type { FixedPricingUnit, LookupPricingUnit, Pricing } from 'model-bank';
const log = debug('lobe-cost:computeVideoCost');
export interface VideoGenerationParams {
[key: string]: any;
[key: string]: unknown;
generateAudio?: boolean;
}
@@ -36,7 +36,7 @@ export const computeVideoCost = (
}
const currency = pricing.currency || 'USD';
let pricePerMillionTokens = 0;
let pricePerMillionTokens: number;
let lookupKey: string | undefined;
switch (videoGenUnit.strategy) {
@@ -76,7 +76,9 @@ export const computeVideoCost = (
}
pricePerMillionTokens = lookupPrice;
log(`Lookup pricing for key "${lookupKey}": ${pricePerMillionTokens} per million tokens (${currency})`);
log(
`Lookup pricing for key "${lookupKey}": ${pricePerMillionTokens} per million tokens (${currency})`,
);
break;
}
default: {
@@ -1,6 +1,7 @@
import debug from 'debug';
import type { ChatMethodOptions } from '../types/chat';
import { mergeHeaders } from '../utils/headers';
const log = debug('model-runtime:helpers:mergeChatMethodOptions');
@@ -105,21 +106,11 @@ export const mergeMultipleChatMethodOptions = (options: ChatMethodOptions[]): Ch
},
},
};
completionOptions.headers = options.reduce((acc, option) => {
if (option)
return {
...acc,
...option.headers,
};
return acc;
}, {});
completionOptions.requestHeaders = options.reduce((acc, option) => {
if (option)
return {
...acc,
...option.requestHeaders,
};
return acc;
}, {});
const headers = mergeHeaders(...options.map((option) => option?.headers));
const requestHeaders = mergeHeaders(...options.map((option) => option?.requestHeaders));
completionOptions.headers = headers;
completionOptions.requestHeaders = requestHeaders;
return completionOptions;
};
@@ -20,6 +20,7 @@ import { AgentRuntimeErrorType } from '../../types/error';
import type { CreateImagePayload, CreateImageResponse } from '../../types/image';
import { AgentRuntimeError } from '../../utils/createError';
import { debugStream } from '../../utils/debugStream';
import { toHeadersInit } from '../../utils/headers';
import { StreamingResponse } from '../../utils/response';
import { sanitizeError } from '../../utils/sanitizeError';
@@ -88,10 +89,16 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
: baseParams;
const response = enableStreaming
? await this.client.chat.completions.create({ ...openaiParams, stream: true })
: await this.client.chat.completions.create({ ...openaiParams, stream: false });
? await this.client.chat.completions.create({
...(openaiParams as any),
stream: true,
} as any)
: await this.client.chat.completions.create({
...(openaiParams as any),
stream: false,
} as any);
if (enableStreaming) {
const stream = response as Stream<OpenAI.ChatCompletionChunk>;
const stream = response as unknown as Stream<OpenAI.ChatCompletionChunk>;
const [prod, debug] = stream.tee();
if (process.env.DEBUG_AZURE_CHAT_COMPLETION === '1') {
debugStream(debug.toReadableStream()).catch(console.error);
@@ -117,7 +124,7 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
try {
const res = await this.client.embeddings.create(
{ ...payload, encoding_format: 'float', user: options?.user },
{ headers: options?.headers, signal: options?.signal },
{ headers: toHeadersInit(options?.headers), signal: options?.signal } as any,
);
if (res.usage && options?.onUsage) {
@@ -146,7 +153,7 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
try {
// Clone params and remap imageUrls/imageUrl -> image
const userInput: Record<string, any> = { ...params };
const userInput: Record<string, unknown> = { ...params };
// Convert imageUrls to 'image' for edit API
if (Array.isArray(userInput.imageUrls) && userInput.imageUrls.length > 0) {
@@ -157,7 +164,7 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
}
// Backward compatibility: single imageUrl -> image
if (userInput.imageUrl && !userInput.image) {
if (typeof userInput.imageUrl === 'string' && !userInput.image) {
userInput.image = await convertImageUrlToFile(userInput.imageUrl);
}
@@ -251,7 +258,7 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
}
protected handleError(e: any, model?: string): never {
let error = e as { [key: string]: any; code: string; message: string };
let error = e as { [key: string]: unknown; code: string; message: string };
if (error.code) {
switch (error.code) {
@@ -261,10 +268,10 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
}
} else {
error = {
cause: error.cause,
message: error.message,
name: error.name,
} as any;
cause: (e as any).cause,
message: (e as any).message,
name: (e as any).name,
} as unknown as { [key: string]: unknown; code: string; message: string };
}
const errorType = error.code
@@ -129,7 +129,7 @@ export class LobeAzureAI implements LobeRuntimeAI {
);
}
} catch (e) {
let error = e as { [key: string]: any; code: string; message: string };
let error = e as { [key: string]: unknown; code: string; message: string };
if (error.code) {
switch (error.code) {
@@ -139,10 +139,10 @@ export class LobeAzureAI implements LobeRuntimeAI {
}
} else {
error = {
cause: error.cause,
message: error.message,
name: error.name,
} as any;
cause: (e as any).cause,
message: (e as any).message,
name: (e as any).name,
} as unknown as { [key: string]: unknown; code: string; message: string };
}
const errorType = error.code
@@ -21,9 +21,9 @@ export interface BflAsyncWebhookResponse {
}
export interface BflResultResponse {
details?: Record<string, any> | null;
details?: Record<string, unknown> | null;
id: string;
preview?: Record<string, any> | null;
preview?: Record<string, unknown> | null;
progress?: number | null;
result?: any;
status: BflStatusResponse;
@@ -13,6 +13,7 @@ import type { ChatMethodOptions, ChatStreamPayload } from '../../types';
import { AgentRuntimeErrorType } from '../../types/error';
import { AgentRuntimeError } from '../../utils/createError';
import { debugStream } from '../../utils/debugStream';
import { toHeadersInit } from '../../utils/headers';
import { StreamingResponse } from '../../utils/response';
export interface CloudflareModelCard {
@@ -61,14 +62,17 @@ export class LobeCloudflareAI implements LobeRuntimeAI {
const { model, tools, apiMode: _, ...restPayload } = payload;
const functions = tools?.map((tool) => tool.function);
const headers = options?.headers || {};
const headers = new Headers(toHeadersInit(options?.headers));
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
headers.set('Authorization', `Bearer ${this.apiKey}`);
}
const url = new URL(model, this.baseURL);
const response = await fetch(url, {
body: JSON.stringify({ tools: functions, ...restPayload }),
headers: { 'Content-Type': 'application/json', ...headers },
headers: new Headers({
...Object.fromEntries(headers.entries()),
'Content-Type': 'application/json',
}),
method: 'POST',
signal: options?.signal,
});
@@ -95,7 +95,7 @@ interface LobeGoogleAIParams {
apiKey?: string;
baseURL?: string;
client?: GoogleGenAI;
defaultHeaders?: Record<string, any>;
defaultHeaders?: Record<string, unknown>;
id?: string;
isVertexAi?: boolean;
}
@@ -1,5 +1,6 @@
import type { ChatModelCard } from '@lobechat/types';
import { ModelProvider } from 'model-bank';
import type OpenAI from 'openai';
import type { OpenAICompatibleFactoryOptions } from '../../core/openaiCompatibleFactory';
import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
@@ -33,7 +34,7 @@ export const params = {
messages: payload.messages as any,
model: payload.model,
stream: true,
...(payload.tools && { tools: payload.tools }),
...(payload.tools && { tools: payload.tools as unknown as OpenAI.ChatCompletionTool[] }),
};
},
noUserId: true,
@@ -44,7 +44,7 @@ export const params = {
// Check if model uses enable_thinking parameter (without clear_thinking)
const useEnableThinking = model && enableThinkingModels.has(model);
const chatTemplateKwargs: Record<string, any> = {};
const chatTemplateKwargs: Record<string, unknown> = {};
if (thinkingFlag !== undefined) {
if (usePreservedThinking) {
@@ -68,11 +68,11 @@ async function createQwenImageTask(
const url = `${baseUrl}/api/v1/services/aigc/${endpoint}/image-synthesis`;
log('Creating %s task with model: %s, endpoint: %s', endpoint, model, url);
const input: Record<string, any> = {
const input: Record<string, unknown> = {
prompt: params.prompt,
};
const parameters: Record<string, any> = {
const parameters: Record<string, unknown> = {
n: 1,
...(typeof params.seed === 'number' ? { seed: params.seed } : {}),
...(params.width && params.height
@@ -95,7 +95,7 @@ export class LobeReplicateAI implements LobeRuntimeAI {
this.debugLog('[Replicate createImage] Model:', model);
this.debugLog('[Replicate createImage] Params received:', JSON.stringify(params, null, 2));
const input: Record<string, any> = {};
const input: Record<string, unknown> = {};
// Redux models don't use prompt - they only use the input image
if (!model.includes('redux')) {
@@ -139,7 +139,7 @@ export class LobeReplicateAI implements LobeRuntimeAI {
const isPrivate192Range = hostname.startsWith('192.168.');
// 172.16.0.0 172.31.255.255
const isPrivate172Range = /^172\.(1[6-9]|2\d|3[01])\./.test(hostname);
const isPrivate172Range = /^172\.(?:1[6-9]|2\d|3[01])\./.test(hostname);
const isLocalTld = hostname.endsWith('.local');
@@ -186,7 +186,9 @@ export class LobeReplicateAI implements LobeRuntimeAI {
this.debugLog('[Replicate createImage] Mapped to', imageParamName, 'as Buffer');
} catch (fetchError: any) {
this.debugLog('[Replicate createImage] Error fetching local image:', fetchError);
throw new Error(`Failed to fetch local image: ${fetchError.message}`);
throw new Error(`Failed to fetch local image: ${fetchError.message}`, {
cause: fetchError,
});
}
} else {
// Public URL - use directly
@@ -416,7 +418,7 @@ export class LobeReplicateAI implements LobeRuntimeAI {
if (!isReplicateDebug) return;
console.log(...args);
console.info(...args);
}
}
@@ -52,7 +52,7 @@ export async function createSiliconCloudImage(
['steps', 'num_inference_steps'],
]);
const body: Record<string, any> = {
const body: Record<string, unknown> = {
model,
prompt: params.prompt,
};
@@ -32,7 +32,7 @@ export async function createVolcengineImage(
['cfg', 'guidance_scale'],
]);
const userInput: Record<string, any> = Object.fromEntries(
const userInput: Record<string, unknown> = Object.fromEntries(
Object.entries(params).map(([key, value]) => [
paramsMap.get(key as RuntimeImageGenParamsValue) ?? key,
value,
@@ -44,7 +44,7 @@ export async function createWenxinImage(
}
}
const requestBody: Record<string, any> = {
const requestBody: Record<string, unknown> = {
model,
prompt: params.prompt,
...(images !== undefined && { image: images }),
+7 -7
View File
@@ -1,7 +1,9 @@
import type { ModelPerformance, ModelTokensUsage, ModelUsage } from '@lobechat/types';
import type { ModelPerformance, ModelTokensUsage, ModelUsage, ToolSchema } from '@lobechat/types';
import type { MessageToolCall, MessageToolCallChunk } from './toolsCalling';
export type RuntimeHeaders = HeadersInit | Record<string, string | undefined>;
export type LLMRoleType = 'user' | 'system' | 'assistant' | 'function' | 'tool';
export type ChatResponseFormat =
@@ -9,7 +11,7 @@ export type ChatResponseFormat =
| {
json_schema: {
name: string;
schema: Record<string, any>;
schema: ToolSchema;
strict?: boolean;
};
type: 'json_schema';
@@ -173,13 +175,13 @@ export interface ChatMethodOptions {
/**
* response headers
*/
headers?: Record<string, any>;
headers?: RuntimeHeaders;
/** Metadata passed to hooks (billing, tracing, etc.) */
metadata?: Record<string, unknown>;
/**
* send the request to the ai api endpoint
*/
requestHeaders?: Record<string, any>;
requestHeaders?: RuntimeHeaders;
signal?: AbortSignal;
/**
* userId for the chat completion
@@ -205,9 +207,7 @@ export interface ChatCompletionFunctions {
* @type {{ [key: string]: any }}
* @memberof ChatCompletionFunctions
*/
parameters?: {
[key: string]: any;
};
parameters?: ToolSchema;
}
export interface ChatCompletionTool {
@@ -1,5 +1,7 @@
import type { ModelUsage } from '@lobechat/types';
import type { RuntimeHeaders } from './chat';
export interface EmbeddingsPayload {
/**
* The number of dimensions the resulting output embeddings should have. Only
@@ -19,7 +21,7 @@ export interface EmbeddingsPayload {
}
export interface EmbeddingsOptions {
headers?: Record<string, any>;
headers?: RuntimeHeaders;
/** Metadata passed to hooks (billing, tracing, etc.) */
metadata?: Record<string, unknown>;
onUsage?: (usage: ModelUsage) => void | Promise<void>;
@@ -1,6 +1,6 @@
import type { ModelUsage } from '@lobechat/types';
import type { ChatCompletionTool } from './chat';
import type { ChatCompletionTool, RuntimeHeaders } from './chat';
interface GenerateObjectMessage {
content: string;
@@ -13,7 +13,7 @@ export interface GenerateObjectSchema {
name: string;
schema: {
additionalProperties?: boolean;
properties: Record<string, any>;
properties: Record<string, unknown>;
required?: string[];
type: 'object';
};
@@ -32,7 +32,7 @@ export interface GenerateObjectOptions {
/**
* response headers
*/
headers?: Record<string, any>;
headers?: RuntimeHeaders;
/** Metadata passed to hooks (billing, tracing, etc.) */
metadata?: Record<string, unknown>;
+3 -1
View File
@@ -1,3 +1,5 @@
import type { RuntimeHeaders } from './chat';
export interface TextToSpeechPayload {
input: string;
model: string;
@@ -5,7 +7,7 @@ export interface TextToSpeechPayload {
}
export interface TextToSpeechOptions {
headers?: Record<string, any>;
headers?: RuntimeHeaders;
signal?: AbortSignal;
/**
* userId for the embeddings
+1 -1
View File
@@ -10,7 +10,7 @@ export interface AgentInitErrorPayload {
}
export interface ChatCompletionErrorPayload {
[key: string]: any;
[key: string]: unknown;
endpoint?: string;
error: object;
errorType: ILobeAgentRuntimeErrorType;
@@ -0,0 +1,40 @@
import type { RuntimeHeaders } from '../types/chat';
export const toHeadersInit = (headers?: RuntimeHeaders): Record<string, string> | undefined => {
if (!headers) return undefined;
const normalized = new Headers();
if (headers instanceof Headers || Array.isArray(headers)) {
new Headers(headers).forEach((value, key) => {
normalized.set(key, value);
});
return Object.fromEntries(normalized.entries());
}
for (const [key, value] of Object.entries(headers)) {
if (typeof value === 'string') {
normalized.set(key, value);
}
}
return Object.fromEntries(normalized.entries());
};
export const mergeHeaders = (
...headersList: Array<RuntimeHeaders | undefined>
): Record<string, string> => {
const merged = new Headers();
for (const header of headersList) {
const normalized = toHeadersInit(header);
if (!normalized) continue;
new Headers(normalized).forEach((value, key) => {
merged.set(key, value);
});
}
return Object.fromEntries(merged.entries());
};
+21 -4
View File
@@ -459,7 +459,7 @@ const getModelLocalEnableConfig = (
* Common logic for processing model cards
*/
const processModelCard = (
model: { [key: string]: any; id: string },
model: { [key: string]: unknown; id: string },
config: ModelProcessorConfig,
knownModel?: any,
options?: { includeKnownExtendParams?: boolean; includeSearchSettings?: boolean },
@@ -495,7 +495,13 @@ const processModelCard = (
return undefined;
}
const mergedSettings = mergeSettings(model.settings, knownModel?.settings, options);
const mergedSettings = mergeSettings(
model.settings && typeof model.settings === 'object'
? (model.settings as AiModelSettings)
: undefined,
knownModel?.settings,
options,
);
const formatPricing = (pricing?: {
cachedInput?: number;
@@ -557,7 +563,7 @@ const processModelCard = (
contextWindowTokens: model.contextWindowTokens ?? knownModel?.contextWindowTokens ?? undefined,
description: model.description ?? knownModel?.description ?? '',
displayName: processDisplayName(model.displayName ?? knownModel?.displayName ?? model.id),
enabled: model?.enabled || false,
enabled: typeof model.enabled === 'boolean' ? model.enabled : false,
functionCall:
model.functionCall ??
knownModel?.abilities?.functionCall ??
@@ -570,7 +576,18 @@ const processModelCard = (
((isKeywordListMatch(model.id.toLowerCase(), imageOutputKeywords) && !isExcludedModel) ||
false),
maxOutput: model.maxOutput ?? knownModel?.maxOutput ?? undefined,
pricing: formatPricing(model?.pricing) ?? undefined,
pricing:
formatPricing(
model.pricing && typeof model.pricing === 'object'
? (model.pricing as {
cachedInput?: number;
input?: number;
output?: number;
units?: any[];
writeCacheInput?: number;
})
: undefined,
) ?? undefined,
reasoning:
model.reasoning ??
knownModel?.abilities?.reasoning ??
+19 -8
View File
@@ -1,14 +1,25 @@
import type { RuntimeHeaders } from '../types/chat';
import { toHeadersInit } from './headers';
export const StreamingResponse = (
stream: ReadableStream,
options?: { headers?: Record<string, string> },
options?: { headers?: RuntimeHeaders },
) => {
const headers = new Headers({
'Cache-Control': 'no-cache',
'Content-Type': 'text/event-stream',
// for Nginx: disable chunk buffering
'X-Accel-Buffering': 'no',
});
if (options?.headers) {
const extraHeaders = new Headers(toHeadersInit(options.headers));
extraHeaders.forEach((value, key) => {
headers.set(key, value);
});
}
return new Response(stream, {
headers: {
'Cache-Control': 'no-cache',
'Content-Type': 'text/event-stream',
// for Nginx: disable chunk buffering
'X-Accel-Buffering': 'no',
...options?.headers,
},
headers,
});
};
@@ -1,4 +1,4 @@
export const safeParseJSON = <T = Record<string, any>>(text?: string) => {
export const safeParseJSON = <T = Record<string, unknown>>(text?: string) => {
if (typeof text !== 'string') return undefined;
let json: T;
+1 -1
View File
@@ -85,7 +85,7 @@ export async function parseFormData(c: Context): Promise<FormData> {
multiples: true,
});
const { fields, files } = await new Promise<{ fields: Record<string, any>; files: any }>(
const { fields, files } = await new Promise<{ fields: Record<string, unknown>; files: any }>(
(resolve, reject) => {
form.parse(fakeReq, (err: any, fds: any, fls: any) => {
if (err) return reject(err);
+3 -1
View File
@@ -7,7 +7,9 @@ const DEFAULT_PAGE_SIZE = 20;
* @param request Query parameter object
* @returns { limit, offset } if pagination parameters are provided; otherwise an empty object
*/
export function processPaginationConditions(request: Record<string, any> & IPaginationQuery): {
export function processPaginationConditions<T extends IPaginationQuery>(
request: T,
): {
limit?: number;
offset?: number;
} {
@@ -129,7 +129,7 @@ export class ResponsesService extends BaseService {
// Handle tool_calls from assistant
if (hasToolCalls) {
for (const toolCall of msg.tool_calls) {
for (const toolCall of msg.tool_calls ?? []) {
output.push({
arguments: toolCall.function?.arguments ?? '{}',
call_id: toolCall.id ?? `call_${itemCounter}`,
@@ -575,7 +575,7 @@ export class ResponsesService extends BaseService {
status: ResponseObject['status'];
usage?: ResponseUsage;
}): ResponseObject {
const p = opts.params as Record<string, any>;
const p = opts.params as Record<string, unknown>;
return {
background: p.background ?? false,
completed_at: opts.completedAt ?? null,
+3 -3
View File
@@ -1,7 +1,7 @@
import { z } from 'zod';
import type { AgentItem } from '../agent';
import type { TaskDetail, UIChatMessage } from '../message';
import type { RichTextEditorState, TaskDetail, UIChatMessage } from '../message';
import type { ChatTopic } from '../topic';
export interface LobeChatGroupMetaConfig {
@@ -42,7 +42,7 @@ export const InsertChatGroupSchema = z.object({
config: ChatGroupConfigSchema.optional().nullable(),
content: z.string().optional().nullable(),
description: z.string().optional().nullable(),
editorData: z.record(z.string(), z.any()).optional().nullable(),
editorData: z.record(z.string(), z.unknown()).optional().nullable(),
groupId: z.string().optional().nullable(),
id: z.string().optional(),
marketIdentifier: z.string().optional().nullable(),
@@ -104,7 +104,7 @@ export interface ChatGroupItem {
content?: string | null;
createdAt: Date;
description?: string | null;
editorData?: Record<string, any> | null;
editorData?: RichTextEditorState | null;
groupId?: string | null;
id: string;
marketIdentifier?: string | null;

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