mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-24 21:41:08 +02:00
chore(x): migrate to AI SDK 7
Bump apps/x from AI SDK v5 to v7 (single hop through the 6-0 and 7-0
migration guides). All deps upgraded via pnpm --filter:
ai@7, @ai-sdk/{anthropic@4,google@4,openai@4,openai-compatible@3,provider@4},
@openrouter/ai-sdk-provider@3, ollama-ai-provider-v2@4.
Changes (per the official migration guides):
- Provider spec bumped V2->V4: ProviderV2 -> ProviderV4 (models.ts, gateway.ts)
- streamText: result.fullStream -> result.stream (+ StreamTextInvoker seam,
test fakes); stepCountIs -> isStepCount
- system -> instructions on SDK generateText/streamText/generateObject calls
(left our own generateObjectSafe.system param untouched)
- allowSystemInMessages: true on calls passing stored messages (v7 rejects
system-role messages in the array by default; convertFromMessages emits them)
- image content part {type:'image'} -> {type:'file'} (message-encoding.ts)
- tool() no longer accepts a `name` key (removed; tools keyed by ToolSet key)
- usage token relocation: cachedInputTokens -> inputTokenDetails.cacheReadTokens,
reasoningTokens -> outputTokenDetails.reasoningTokens (mapUsage rewrite);
renderer decoupled from LanguageModelUsage to a local flat UsageSummary
- providerMetadata relay cast to shared ProviderOptions (spec loosened)
No persisted-data changes: @x/shared is AI-SDK-free, so old chats
list/view/continue unchanged. Full typecheck clean; 637 tests pass
(shared 107, core 447, renderer 83); main.cjs bundles ESM-only ai into CJS.
Pending live check (needs creds): OpenAI strictJsonSchema now defaults true
in v7 and may reject zod schemas for openai-family flavors at runtime.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
10525c58b8
commit
cc2daf1874
16 changed files with 210 additions and 207 deletions
|
|
@ -17,19 +17,19 @@
|
|||
"@agentclientprotocol/claude-agent-acp": "^0.55.0",
|
||||
"@agentclientprotocol/codex-acp": "^1.1.0",
|
||||
"@agentclientprotocol/sdk": "^1.1.0",
|
||||
"@ai-sdk/anthropic": "^2.0.63",
|
||||
"@ai-sdk/google": "^2.0.53",
|
||||
"@ai-sdk/openai": "^2.0.91",
|
||||
"@ai-sdk/openai-compatible": "^1.0.33",
|
||||
"@ai-sdk/provider": "^2.0.1",
|
||||
"@ai-sdk/anthropic": "^4.0.12",
|
||||
"@ai-sdk/google": "^4.0.12",
|
||||
"@ai-sdk/openai": "^4.0.11",
|
||||
"@ai-sdk/openai-compatible": "^3.0.7",
|
||||
"@ai-sdk/provider": "^4.0.3",
|
||||
"@composio/core": "^0.6.0",
|
||||
"@google-cloud/local-auth": "^3.0.1",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@openrouter/ai-sdk-provider": "^1.2.6",
|
||||
"@openrouter/ai-sdk-provider": "^3.0.0",
|
||||
"@react-pdf/renderer": "^4.3.2",
|
||||
"@types/react": "^19.2.7",
|
||||
"@x/shared": "workspace:*",
|
||||
"ai": "^5.0.133",
|
||||
"ai": "^7.0.22",
|
||||
"awilix": "^12.0.5",
|
||||
"baileys": "7.0.0-rc13",
|
||||
"chokidar": "^4.0.3",
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
"isomorphic-git": "^1.29.0",
|
||||
"mammoth": "^1.11.0",
|
||||
"node-html-markdown": "^2.0.0",
|
||||
"ollama-ai-provider-v2": "^1.5.4",
|
||||
"ollama-ai-provider-v2": "^4.0.1",
|
||||
"openid-client": "^6.8.1",
|
||||
"papaparse": "^5.5.3",
|
||||
"pdf-parse": "^2.4.5",
|
||||
|
|
|
|||
|
|
@ -304,8 +304,8 @@ async function handleLlmGenerate(
|
|||
const model = createProvider(providerConfig).languageModel(resolved.model);
|
||||
const result = await withUseCase({ useCase: 'app_llm_generate', subUseCase: slug }, () => generateText({
|
||||
model,
|
||||
...(system ? { system } : {}),
|
||||
...(rawMessages ? { messages: rawMessages as ModelMessage[] } : { prompt: prompt as string }),
|
||||
...(system ? { instructions: system } : {}),
|
||||
...(rawMessages ? { messages: rawMessages as ModelMessage[], allowSystemInMessages: true } : { prompt: prompt as string }),
|
||||
...(temperature !== undefined ? { temperature } : {}),
|
||||
maxOutputTokens,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -653,7 +653,7 @@ Respond with ONLY valid JSON: either a schedule object or null. No other text.`;
|
|||
try {
|
||||
const result = await withUseCase({ useCase: 'knowledge_sync', subUseCase: 'inline_task_classify' }, () => generateText({
|
||||
model,
|
||||
system: systemPrompt,
|
||||
instructions: systemPrompt,
|
||||
prompt: instruction,
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ async function generateBrief(event: CalendarEvent, ctx: Awaited<ReturnType<typeo
|
|||
|
||||
const result = await withUseCase({ useCase: 'meeting_prep' }, () => generateText({
|
||||
model,
|
||||
system: BRIEF_SYSTEM,
|
||||
instructions: BRIEF_SYSTEM,
|
||||
prompt: parts.join('\n\n'),
|
||||
}));
|
||||
captureLlmUsage({ useCase: 'meeting_prep', model: modelId, provider: providerName, usage: result.usage });
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ export async function summarizeMeeting(transcript: string, meetingStartTime?: st
|
|||
|
||||
const result = await withUseCase({ useCase: 'meeting_note' }, () => generateText({
|
||||
model,
|
||||
system: SYSTEM_PROMPT,
|
||||
instructions: SYSTEM_PROMPT,
|
||||
prompt,
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ProviderV2 } from '@ai-sdk/provider';
|
||||
import { ProviderV4 } from '@ai-sdk/provider';
|
||||
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
||||
import { getAccessToken } from '../auth/tokens.js';
|
||||
import { getCurrentUseCase } from '../analytics/use_case.js';
|
||||
|
|
@ -16,7 +16,7 @@ const authedFetch: typeof fetch = async (input, init) => {
|
|||
return fetch(input, { ...init, headers });
|
||||
};
|
||||
|
||||
export function getGatewayProvider(): ProviderV2 {
|
||||
export function getGatewayProvider(): ProviderV4 {
|
||||
return createOpenRouter({
|
||||
baseURL: `${API_URL}/v1/llm`,
|
||||
apiKey: 'managed-by-rowboat',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ProviderV2 } from "@ai-sdk/provider";
|
||||
import { ProviderV4 } from "@ai-sdk/provider";
|
||||
import { createGateway, generateText, type LanguageModel } from "ai";
|
||||
import { createOpenAI } from "@ai-sdk/openai";
|
||||
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
||||
|
|
@ -22,7 +22,7 @@ import {
|
|||
export const Provider = LlmProvider;
|
||||
export const ModelConfig = LlmModelConfig;
|
||||
|
||||
export function createProvider(config: z.infer<typeof Provider>): ProviderV2 {
|
||||
export function createProvider(config: z.infer<typeof Provider>): ProviderV4 {
|
||||
const { apiKey, baseURL, headers } = config;
|
||||
switch (config.flavor) {
|
||||
case "openai":
|
||||
|
|
@ -78,7 +78,7 @@ export function createProvider(config: z.infer<typeof Provider>): ProviderV2 {
|
|||
apiKey,
|
||||
baseURL,
|
||||
headers,
|
||||
}) as unknown as ProviderV2;
|
||||
}) as unknown as ProviderV4;
|
||||
case "rowboat":
|
||||
return getGatewayProvider();
|
||||
default:
|
||||
|
|
@ -342,7 +342,7 @@ export async function generateOneShot(opts: GenerateTextOptions): Promise<Genera
|
|||
{ useCase: "copilot_chat", subUseCase: "email_compose" },
|
||||
() => generateText({
|
||||
model: languageModel,
|
||||
...(opts.system ? { system: opts.system } : {}),
|
||||
...(opts.system ? { instructions: opts.system } : {}),
|
||||
prompt: opts.prompt,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export async function generateObjectSafe<T>(
|
|||
try {
|
||||
const result = await generateObject({
|
||||
model: options.model,
|
||||
...(options.system ? { system: options.system } : {}),
|
||||
...(options.system ? { instructions: options.system } : {}),
|
||||
prompt: options.prompt,
|
||||
schema: options.schema,
|
||||
});
|
||||
|
|
@ -68,7 +68,7 @@ export async function generateObjectSafe<T>(
|
|||
].join("\n\n").trim();
|
||||
const result = await generateObject({
|
||||
model: options.model,
|
||||
system,
|
||||
instructions: system,
|
||||
prompt: options.prompt,
|
||||
schema: options.schema,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -101,7 +101,9 @@ export function convertFromMessages(messages: z.infer<typeof Message>[]): ModelM
|
|||
// model knows which images show the user vs their screen.
|
||||
const textSegments: string[] = userMessageContextPrefix ? [userMessageContextPrefix] : [];
|
||||
const attachmentLines: string[] = [];
|
||||
type EncodedImagePart = { type: "image"; image: string; mediaType: string };
|
||||
// AI SDK 7 collapsed the `image` content part into `file`
|
||||
// (data + mediaType); image parts are deprecated.
|
||||
type EncodedImagePart = { type: "file"; data: string; mediaType: string };
|
||||
const cameraParts: EncodedImagePart[] = [];
|
||||
const screenParts: EncodedImagePart[] = [];
|
||||
const frameTimes: string[] = [];
|
||||
|
|
@ -113,7 +115,7 @@ export function convertFromMessages(messages: z.infer<typeof Message>[]): ModelM
|
|||
attachmentLines.push(`- ${part.filename} (${part.mimeType}${sizeStr}) at ${part.path}${lineStr}`);
|
||||
} else if (part.type === "image") {
|
||||
const target = part.source === "screen" ? screenParts : cameraParts;
|
||||
target.push({ type: "image", image: part.data, mediaType: part.mediaType });
|
||||
target.push({ type: "file", data: part.data, mediaType: part.mediaType });
|
||||
if (part.capturedAt) frameTimes.push(part.capturedAt);
|
||||
} else {
|
||||
textSegments.push(part.text);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { jsonSchema } from "ai";
|
||||
import { Agent, ToolAttachment } from "@x/shared/dist/agent.js";
|
||||
import { AssistantContentPart, AssistantMessage, MessageList, ProviderOptions, ToolCallPart, ToolMessage, UserMessageContext } from "@x/shared/dist/message.js";
|
||||
import { LanguageModel, stepCountIs, streamText, tool, Tool, ToolSet } from "ai";
|
||||
import { LanguageModel, isStepCount, streamText, tool, Tool, ToolSet } from "ai";
|
||||
import { z } from "zod";
|
||||
import { LlmStepStreamEvent } from "@x/shared/dist/llm-step-events.js";
|
||||
import { execTool } from "../tools/exec-tool.js";
|
||||
|
|
@ -255,7 +255,6 @@ async function mapAgentTool(t: z.infer<typeof ToolAttachment>): Promise<Tool> {
|
|||
switch (t.type) {
|
||||
case "mcp":
|
||||
return tool({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
inputSchema: jsonSchema(t.inputSchema),
|
||||
});
|
||||
|
|
@ -265,7 +264,6 @@ async function mapAgentTool(t: z.infer<typeof ToolAttachment>): Promise<Tool> {
|
|||
throw new Error(`Agent ${t.name} not found`);
|
||||
}
|
||||
return tool({
|
||||
name: t.name,
|
||||
description: agent.description,
|
||||
inputSchema: z.object({
|
||||
message: z.string().describe("The message to send to the workflow"),
|
||||
|
|
@ -1151,21 +1149,23 @@ async function* streamLlm(
|
|||
}, () => streamText({
|
||||
model,
|
||||
messages: converted,
|
||||
system: instructions,
|
||||
instructions,
|
||||
allowSystemInMessages: true,
|
||||
tools,
|
||||
stopWhen: stepCountIs(1),
|
||||
stopWhen: isStepCount(1),
|
||||
abortSignal: signal,
|
||||
}))
|
||||
: streamText({
|
||||
model,
|
||||
messages: converted,
|
||||
system: instructions,
|
||||
instructions,
|
||||
allowSystemInMessages: true,
|
||||
tools,
|
||||
stopWhen: stepCountIs(1),
|
||||
stopWhen: isStepCount(1),
|
||||
abortSignal: signal,
|
||||
});
|
||||
const { fullStream } = streamResult;
|
||||
for await (const event of fullStream) {
|
||||
const { stream } = streamResult;
|
||||
for await (const event of stream) {
|
||||
// Check abort on every chunk for responsiveness
|
||||
signal?.throwIfAborted();
|
||||
console.log("-> \t\tstream event", JSON.stringify(event));
|
||||
|
|
@ -1179,39 +1179,39 @@ async function* streamLlm(
|
|||
case "reasoning-start":
|
||||
yield {
|
||||
type: "reasoning-start",
|
||||
providerOptions: event.providerMetadata,
|
||||
providerOptions: event.providerMetadata as z.infer<typeof ProviderOptions> | undefined,
|
||||
};
|
||||
break;
|
||||
case "reasoning-delta":
|
||||
yield {
|
||||
type: "reasoning-delta",
|
||||
delta: event.text,
|
||||
providerOptions: event.providerMetadata,
|
||||
providerOptions: event.providerMetadata as z.infer<typeof ProviderOptions> | undefined,
|
||||
};
|
||||
break;
|
||||
case "reasoning-end":
|
||||
yield {
|
||||
type: "reasoning-end",
|
||||
providerOptions: event.providerMetadata,
|
||||
providerOptions: event.providerMetadata as z.infer<typeof ProviderOptions> | undefined,
|
||||
};
|
||||
break;
|
||||
case "text-start":
|
||||
yield {
|
||||
type: "text-start",
|
||||
providerOptions: event.providerMetadata,
|
||||
providerOptions: event.providerMetadata as z.infer<typeof ProviderOptions> | undefined,
|
||||
};
|
||||
break;
|
||||
case "text-end":
|
||||
yield {
|
||||
type: "text-end",
|
||||
providerOptions: event.providerMetadata,
|
||||
providerOptions: event.providerMetadata as z.infer<typeof ProviderOptions> | undefined,
|
||||
};
|
||||
break;
|
||||
case "text-delta":
|
||||
yield {
|
||||
type: "text-delta",
|
||||
delta: event.text,
|
||||
providerOptions: event.providerMetadata,
|
||||
providerOptions: event.providerMetadata as z.infer<typeof ProviderOptions> | undefined,
|
||||
};
|
||||
break;
|
||||
case "tool-call":
|
||||
|
|
@ -1220,7 +1220,7 @@ async function* streamLlm(
|
|||
toolCallId: event.toolCallId,
|
||||
toolName: event.toolName,
|
||||
input: event.input,
|
||||
providerOptions: event.providerMetadata,
|
||||
providerOptions: event.providerMetadata as z.infer<typeof ProviderOptions> | undefined,
|
||||
};
|
||||
break;
|
||||
case "finish-step":
|
||||
|
|
@ -1238,7 +1238,7 @@ async function* streamLlm(
|
|||
type: "finish-step",
|
||||
usage: event.usage,
|
||||
finishReason: event.finishReason,
|
||||
providerOptions: event.providerMetadata,
|
||||
providerOptions: event.providerMetadata as z.infer<typeof ProviderOptions> | undefined,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ function makeRegistry(parts: Array<Record<string, unknown>>, capture: InvokerOpt
|
|||
invoke: (options) => {
|
||||
capture.push(options);
|
||||
return {
|
||||
fullStream: (async function* () {
|
||||
stream: (async function* () {
|
||||
yield* parts;
|
||||
})(),
|
||||
};
|
||||
|
|
@ -378,7 +378,7 @@ describe("RealModelRegistry", () => {
|
|||
invoke: (options) => {
|
||||
capture.push(options);
|
||||
return {
|
||||
fullStream: (async function* () {
|
||||
stream: (async function* () {
|
||||
yield { type: "finish-step", finishReason: "stop", usage: {} };
|
||||
})(),
|
||||
};
|
||||
|
|
@ -528,7 +528,7 @@ describe("RealModelRegistry", () => {
|
|||
invoke: (options) => {
|
||||
capture.push(options);
|
||||
return {
|
||||
fullStream: (async function* () {
|
||||
stream: (async function* () {
|
||||
yield { type: "finish-step", finishReason: "stop", usage: {} };
|
||||
})(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
jsonSchema,
|
||||
stepCountIs,
|
||||
isStepCount,
|
||||
streamText,
|
||||
tool,
|
||||
type LanguageModel,
|
||||
|
|
@ -39,10 +39,18 @@ export type StreamTextInvoker = (options: {
|
|||
topP?: number;
|
||||
maxOutputTokens?: number;
|
||||
providerOptions?: Record<string, Record<string, JsonValue>>;
|
||||
}) => { fullStream: AsyncIterable<unknown> };
|
||||
}) => { stream: AsyncIterable<unknown> };
|
||||
|
||||
const defaultInvoker: StreamTextInvoker = (options) =>
|
||||
streamText({ ...options, stopWhen: stepCountIs(1) });
|
||||
const defaultInvoker: StreamTextInvoker = ({ system, ...options }) =>
|
||||
// Our seam keeps a `system` field; the SDK's top-level option is now
|
||||
// `instructions`. `allowSystemInMessages` opts into system-role messages
|
||||
// in the array (v7 rejects them by default) — persisted chats rely on it.
|
||||
streamText({
|
||||
...options,
|
||||
instructions: system,
|
||||
allowSystemInMessages: true,
|
||||
stopWhen: isStepCount(1),
|
||||
});
|
||||
|
||||
export interface RealModelRegistryDeps {
|
||||
resolveProvider?: (name: string) => Promise<z.infer<typeof LlmProvider>>;
|
||||
|
|
@ -217,7 +225,7 @@ export class RealModelRegistry implements IModelRegistry {
|
|||
...generationParams,
|
||||
});
|
||||
|
||||
for await (const raw of result.fullStream) {
|
||||
for await (const raw of result.stream) {
|
||||
request.signal.throwIfAborted();
|
||||
const event = raw as {
|
||||
type: string;
|
||||
|
|
@ -226,7 +234,13 @@ export class RealModelRegistry implements IModelRegistry {
|
|||
toolName?: string;
|
||||
input?: unknown;
|
||||
finishReason?: string;
|
||||
usage?: Record<string, number | undefined>;
|
||||
usage?: {
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalTokens?: number;
|
||||
inputTokenDetails?: { cacheReadTokens?: number | undefined };
|
||||
outputTokenDetails?: { reasoningTokens?: number | undefined };
|
||||
};
|
||||
providerMetadata?: unknown;
|
||||
error?: unknown;
|
||||
};
|
||||
|
|
@ -390,24 +404,33 @@ function mergeProviderOptions(
|
|||
}
|
||||
|
||||
function mapUsage(
|
||||
usage: Record<string, number | undefined> | undefined,
|
||||
usage:
|
||||
| {
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalTokens?: number;
|
||||
inputTokenDetails?: { cacheReadTokens?: number | undefined };
|
||||
outputTokenDetails?: { reasoningTokens?: number | undefined };
|
||||
}
|
||||
| undefined,
|
||||
): z.infer<typeof TurnUsage> {
|
||||
const mapped: z.infer<typeof TurnUsage> = {};
|
||||
if (!usage) {
|
||||
return mapped;
|
||||
}
|
||||
for (const key of [
|
||||
"inputTokens",
|
||||
"outputTokens",
|
||||
"totalTokens",
|
||||
"reasoningTokens",
|
||||
"cachedInputTokens",
|
||||
] as const) {
|
||||
const value = usage[key];
|
||||
// AI SDK 7 relocated cached/reasoning tokens into nested detail objects
|
||||
// (usage.inputTokenDetails.cacheReadTokens, usage.outputTokenDetails.reasoningTokens);
|
||||
// our persisted TurnUsage keeps the flat names.
|
||||
const put = (key: keyof z.infer<typeof TurnUsage>, value: number | undefined) => {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
mapped[key] = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
put("inputTokens", usage.inputTokens);
|
||||
put("outputTokens", usage.outputTokens);
|
||||
put("totalTokens", usage.totalTokens);
|
||||
put("reasoningTokens", usage.outputTokenDetails?.reasoningTokens);
|
||||
put("cachedInputTokens", usage.inputTokenDetails?.cacheReadTokens);
|
||||
return mapped;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue