blob: 1c51db8cb3e4a6a5fa17f892b856be47dcf2573e [file]
import type { ModelMessage } from './model-protocol.js';
import { lowerModelTools, normalizeAiSdkUsage, type AiSdkUsageLike } from './model-adapter.js';
import { rawFinishReasonString, type NormalizedUsage } from './model-protocol.js';
import type { ModelToolSet } from './model-protocol.js';
export type ToolFreeModelCallContent =
| { readonly prompt: string; readonly messages?: never }
| { readonly prompt?: never; readonly messages: readonly ModelMessage[] };
export type ToolFreeModelCallInput = ToolFreeModelCallContent & {
readonly model: unknown;
/** Optional original Agent system prefix for cache-compatible auxiliary calls. */
readonly system?: string;
readonly providerOptions?: unknown;
readonly abortSignal?: AbortSignal;
readonly maxOutputTokens: number;
readonly maxRetries?: number;
};
export interface ToolFreeModelCallResult {
readonly text: string;
readonly usage?: NormalizedUsage;
readonly finishReason?: string;
}
export interface ProviderPrefixModelCallInput {
readonly model: unknown;
readonly system?: string;
readonly messages: readonly ModelMessage[];
readonly tools: ModelToolSet;
readonly activeTools: readonly string[];
readonly providerOptions?: unknown;
readonly abortSignal?: AbortSignal;
readonly maxOutputTokens?: number;
/** Anthropic omits Tool schemas when AI SDK receives `none`; omit there and fail closed below. */
readonly toolChoicePolicy: 'none' | 'omit';
}
export type ProviderPrefixModelCallResult = ToolFreeModelCallResult;
export class ProviderPrefixModelCallUnavailableError extends Error {
readonly name = 'ProviderPrefixModelCallUnavailableError';
}
/** One non-continuing call that preserves the source Agent's provider-visible prefix. */
export async function generateProviderPrefixModelCall(
input: ProviderPrefixModelCallInput,
): Promise<ProviderPrefixModelCallResult> {
if (input.activeTools.some((name) => input.tools[name]?.kind === 'provider')) {
// Provider-native Tools may execute remotely before a response exists, and
// compatible endpoints are not guaranteed to honor `toolChoice: 'none'`.
// Never dispatch an auxiliary request with one active, regardless of the
// transport-specific Tool-choice policy.
throw new ProviderPrefixModelCallUnavailableError(
'Memory extraction is unavailable with active provider-native Tools',
);
}
const ai = (await import('ai')) as unknown as {
generateText(options: Record<string, unknown>): Promise<{
text: string;
toolCalls?: readonly unknown[];
usage?: AiSdkUsageLike;
finishReason?: unknown;
}>;
};
const result = await ai.generateText({
model: input.model,
...(input.system === undefined ? {} : { system: input.system }),
messages: input.messages,
tools: lowerModelTools(input.tools),
activeTools: input.activeTools,
// Preserve the source request's Tool schema for Provider cache reuse. Most
// adapters can disable calls while retaining schemas; Anthropic cannot, so
// its request keeps the source default and the result is rejected below if
// the model nevertheless chooses a Tool.
...(input.toolChoicePolicy === 'none' ? { toolChoice: 'none' } : {}),
...(input.abortSignal === undefined ? {} : { abortSignal: input.abortSignal }),
...(input.providerOptions === undefined ? {} : { providerOptions: input.providerOptions }),
...(input.maxOutputTokens === undefined ? {} : { maxOutputTokens: input.maxOutputTokens }),
maxRetries: 0,
});
if ((result.toolCalls?.length ?? 0) > 0) {
throw new Error('Provider returned a disabled Tool Call');
}
const usage = normalizeAiSdkUsage(result.usage, { rawFinishReason: result.finishReason });
const finishReason = rawFinishReasonString(result.finishReason);
return {
text: result.text,
...(usage ? { usage } : {}),
...(finishReason ? { finishReason } : {}),
};
}
/** Runs one model call without exposing tools and returns its accounting facts. */
export async function generateToolFreeModelCall(
input: ToolFreeModelCallInput,
): Promise<ToolFreeModelCallResult> {
const ai = (await import('ai')) as unknown as {
generateText(options: Record<string, unknown>): Promise<{
text: string;
usage?: AiSdkUsageLike;
finishReason?: unknown;
}>;
};
const result = await ai.generateText({
model: input.model,
...(input.system === undefined ? {} : { system: input.system }),
...(input.prompt === undefined ? { messages: input.messages } : { prompt: input.prompt }),
...(input.abortSignal === undefined ? {} : { abortSignal: input.abortSignal }),
...(input.providerOptions === undefined ? {} : { providerOptions: input.providerOptions }),
maxOutputTokens: input.maxOutputTokens,
...(input.maxRetries === undefined ? {} : { maxRetries: input.maxRetries }),
});
const usage = normalizeAiSdkUsage(result.usage, { rawFinishReason: result.finishReason });
const finishReason = rawFinishReasonString(result.finishReason);
return {
text: result.text,
...(usage ? { usage } : {}),
...(finishReason ? { finishReason } : {}),
};
}