diff --git a/apps/cli/package-lock.json b/apps/cli/package-lock.json index 5a7c8a43..a5dcf009 100644 --- a/apps/cli/package-lock.json +++ b/apps/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rowboatlabs/rowboatx", - "version": "0.10.0", + "version": "0.15.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rowboatlabs/rowboatx", - "version": "0.10.0", + "version": "0.15.0", "license": "Apache-2.0", "dependencies": { "@ai-sdk/anthropic": "^2.0.44", @@ -15,8 +15,8 @@ "@ai-sdk/openai-compatible": "^1.0.27", "@ai-sdk/provider": "^2.0.0", "@modelcontextprotocol/sdk": "^1.20.2", - "@openrouter/ai-sdk-provider": "^1.2.3", - "ai": "^5.0.78", + "@openrouter/ai-sdk-provider": "^1.2.6", + "ai": "^5.0.102", "json-schema-to-zod": "^2.6.1", "nanoid": "^5.1.6", "ollama-ai-provider-v2": "^1.5.4", @@ -66,14 +66,31 @@ } }, "node_modules/@ai-sdk/gateway": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.1.tgz", - "integrity": "sha512-vPVIbnP35ZnayS937XLo85vynR85fpBQWHCdUweq7apzqFOTU2YkUd4V3msebEHbQ2Zro60ZShDDy9SMiyWTqA==", + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.15.tgz", + "integrity": "sha512-i1YVKzC1dg9LGvt+GthhD7NlRhz9J4+ZRj3KELU14IZ/MHPsOBiFeEoCCIDLR+3tqT8/+5nIsK3eZ7DFRfMfdw==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.12", - "@vercel/oidc": "3.0.3" + "@ai-sdk/provider-utils": "3.0.17", + "@vercel/oidc": "3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider-utils": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz", + "integrity": "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.6" }, "engines": { "node": ">=18" @@ -276,9 +293,9 @@ } }, "node_modules/@openrouter/ai-sdk-provider": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-1.2.3.tgz", - "integrity": "sha512-a6Nc8dPRHakRH9966YJ/HZJhLOds7DuPTscNZDoAr+Aw+tEFUlacSJMvb/b3gukn74mgbuaJRji9YOn62ipfVg==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-1.2.6.tgz", + "integrity": "sha512-DExO4FXod5vEdLFpQGsyNva8u3FWHj2IPaP8to+zEGsBEUY7lu5t24uIMxmmLKZ0sYYWAtmTLSV4Y9uOVqQoAg==", "license": "Apache-2.0", "dependencies": { "@openrouter/sdk": "^0.1.8" @@ -370,9 +387,9 @@ } }, "node_modules/@vercel/oidc": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.3.tgz", - "integrity": "sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz", + "integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==", "license": "Apache-2.0", "engines": { "node": ">= 20" @@ -418,14 +435,14 @@ } }, "node_modules/ai": { - "version": "5.0.78", - "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.78.tgz", - "integrity": "sha512-ec77fmQwJGLduswMrW4AAUGSOiu8dZaIwMmWHHGKsrMUFFS6ugfkTyx0srtuKYHNRRLRC2dT7cPirnUl98VnxA==", + "version": "5.0.102", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.102.tgz", + "integrity": "sha512-snRK3nS5DESOjjpq7S74g8YszWVMzjagfHqlJWZsbtl9PyOS+2XUd8dt2wWg/jdaq/jh0aU66W1mx5qFjUQyEg==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "2.0.1", + "@ai-sdk/gateway": "2.0.15", "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.12", + "@ai-sdk/provider-utils": "3.0.17", "@opentelemetry/api": "1.9.0" }, "engines": { @@ -435,6 +452,23 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/ai/node_modules/@ai-sdk/provider-utils": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz", + "integrity": "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -483,23 +517,27 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/bytes": { @@ -1006,15 +1044,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/inherits": { @@ -1329,22 +1371,6 @@ "node": ">= 0.10" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", diff --git a/apps/cli/package.json b/apps/cli/package.json index f68aff28..f313c56b 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -31,8 +31,8 @@ "@ai-sdk/openai-compatible": "^1.0.27", "@ai-sdk/provider": "^2.0.0", "@modelcontextprotocol/sdk": "^1.20.2", - "@openrouter/ai-sdk-provider": "^1.2.3", - "ai": "^5.0.78", + "@openrouter/ai-sdk-provider": "^1.2.6", + "ai": "^5.0.102", "json-schema-to-zod": "^2.6.1", "nanoid": "^5.1.6", "ollama-ai-provider-v2": "^1.5.4", diff --git a/apps/cli/src/app.ts b/apps/cli/src/app.ts index 766f422a..9140a2fd 100644 --- a/apps/cli/src/app.ts +++ b/apps/cli/src/app.ts @@ -14,6 +14,7 @@ import { Example } from "./application/entities/example.js"; import { z } from "zod"; import { Flavor } from "./application/entities/models.js"; import { examples } from "./examples/index.js"; +import { modelMessageSchema } from "ai"; export async function updateState(agent: string, runId: string) { const state = new AgentState(agent, runId); @@ -225,6 +226,7 @@ export async function modelConfig() { const defaultApiKeyEnvVars: Record, string> = { "rowboat [free]": "", openai: "OPENAI_API_KEY", + aigateway: "AI_GATEWAY_API_KEY", anthropic: "ANTHROPIC_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY", ollama: "", @@ -234,6 +236,7 @@ export async function modelConfig() { const defaultBaseUrls: Record, string> = { "rowboat [free]": "", openai: "https://api.openai.com/v1", + aigateway: "https://ai-gateway.vercel.sh/v1/ai", anthropic: "https://api.anthropic.com/v1", google: "https://generativelanguage.googleapis.com/v1beta", ollama: "http://localhost:11434", @@ -243,6 +246,7 @@ export async function modelConfig() { const defaultModels: Record, string> = { "rowboat [free]": "google/gemini-3-pro-preview", openai: "gpt-5.1", + aigateway: "gpt-5.1", anthropic: "claude-sonnet-4-5", google: "gemini-2.5-pro", ollama: "llama3.1", diff --git a/apps/cli/src/application/entities/llm-step-events.ts b/apps/cli/src/application/entities/llm-step-events.ts index 65e7d8a5..b9c428cf 100644 --- a/apps/cli/src/application/entities/llm-step-events.ts +++ b/apps/cli/src/application/entities/llm-step-events.ts @@ -1,40 +1,46 @@ import { z } from "zod"; +import { ProviderOptions } from "./message.js"; -export const LlmStepStreamReasoningStartEvent = z.object({ +const BaseEvent = z.object({ + providerOptions: ProviderOptions.optional(), +}) + +export const LlmStepStreamReasoningStartEvent = BaseEvent.extend({ type: z.literal("reasoning-start"), }); -export const LlmStepStreamReasoningDeltaEvent = z.object({ +export const LlmStepStreamReasoningDeltaEvent = BaseEvent.extend({ type: z.literal("reasoning-delta"), delta: z.string(), }); -export const LlmStepStreamReasoningEndEvent = z.object({ +export const LlmStepStreamReasoningEndEvent = BaseEvent.extend({ type: z.literal("reasoning-end"), }); -export const LlmStepStreamTextStartEvent = z.object({ +export const LlmStepStreamTextStartEvent = BaseEvent.extend({ type: z.literal("text-start"), }); -export const LlmStepStreamTextDeltaEvent = z.object({ +export const LlmStepStreamTextDeltaEvent = BaseEvent.extend({ type: z.literal("text-delta"), delta: z.string(), }); -export const LlmStepStreamTextEndEvent = z.object({ +export const LlmStepStreamTextEndEvent = BaseEvent.extend({ type: z.literal("text-end"), }); -export const LlmStepStreamToolCallEvent = z.object({ +export const LlmStepStreamToolCallEvent = BaseEvent.extend({ type: z.literal("tool-call"), toolCallId: z.string(), toolName: z.string(), input: z.any(), }); -export const LlmStepStreamUsageEvent = z.object({ - type: z.literal("usage"), +export const LlmStepStreamFinishStepEvent = z.object({ + type: z.literal("finish-step"), + finishReason: z.enum(["stop", "tool-calls", "length", "content-filter", "error", "other", "unknown"]), usage: z.object({ inputTokens: z.number().optional(), outputTokens: z.number().optional(), @@ -42,6 +48,7 @@ export const LlmStepStreamUsageEvent = z.object({ reasoningTokens: z.number().optional(), cachedInputTokens: z.number().optional(), }), + providerOptions: ProviderOptions.optional(), }); export const LlmStepStreamEvent = z.union([ @@ -52,5 +59,5 @@ export const LlmStepStreamEvent = z.union([ LlmStepStreamTextDeltaEvent, LlmStepStreamTextEndEvent, LlmStepStreamToolCallEvent, - LlmStepStreamUsageEvent, + LlmStepStreamFinishStepEvent, ]); \ No newline at end of file diff --git a/apps/cli/src/application/entities/message.ts b/apps/cli/src/application/entities/message.ts index ce5d4b67..702b103a 100644 --- a/apps/cli/src/application/entities/message.ts +++ b/apps/cli/src/application/entities/message.ts @@ -1,13 +1,17 @@ import { z } from "zod"; +export const ProviderOptions = z.record(z.string(), z.record(z.string(), z.json())); + export const TextPart = z.object({ type: z.literal("text"), text: z.string(), + providerOptions: ProviderOptions.optional(), }); export const ReasoningPart = z.object({ type: z.literal("reasoning"), text: z.string(), + providerOptions: ProviderOptions.optional(), }); export const ToolCallPart = z.object({ @@ -15,6 +19,7 @@ export const ToolCallPart = z.object({ toolCallId: z.string(), toolName: z.string(), arguments: z.any(), + providerOptions: ProviderOptions.optional(), }); export const AssistantContentPart = z.union([ @@ -26,6 +31,7 @@ export const AssistantContentPart = z.union([ export const UserMessage = z.object({ role: z.literal("user"), content: z.string(), + providerOptions: ProviderOptions.optional(), }); export const AssistantMessage = z.object({ @@ -34,11 +40,13 @@ export const AssistantMessage = z.object({ z.string(), z.array(AssistantContentPart), ]), + providerOptions: ProviderOptions.optional(), }); export const SystemMessage = z.object({ role: z.literal("system"), content: z.string(), + providerOptions: ProviderOptions.optional(), }); export const ToolMessage = z.object({ @@ -46,6 +54,7 @@ export const ToolMessage = z.object({ content: z.string(), toolCallId: z.string(), toolName: z.string(), + providerOptions: ProviderOptions.optional(), }); export const Message = z.discriminatedUnion("role", [ diff --git a/apps/cli/src/application/entities/models.ts b/apps/cli/src/application/entities/models.ts index 9c698cc4..0dabad3f 100644 --- a/apps/cli/src/application/entities/models.ts +++ b/apps/cli/src/application/entities/models.ts @@ -2,6 +2,7 @@ import z from "zod"; export const Flavor = z.enum([ "rowboat [free]", + "aigateway", "anthropic", "google", "ollama", diff --git a/apps/cli/src/application/lib/agent.ts b/apps/cli/src/application/lib/agent.ts index 09528f85..5ee019ac 100644 --- a/apps/cli/src/application/lib/agent.ts +++ b/apps/cli/src/application/lib/agent.ts @@ -1,9 +1,9 @@ -import { jsonSchema, ModelMessage } from "ai"; +import { jsonSchema, ModelMessage, modelMessageSchema } from "ai"; import fs from "fs"; import path from "path"; import { getModelConfig, WorkDir } from "../config/config.js"; import { Agent, ToolAttachment } from "../entities/agent.js"; -import { AssistantContentPart, AssistantMessage, Message, MessageList, ToolCallPart, ToolMessage, UserMessage } from "../entities/message.js"; +import { AssistantContentPart, AssistantMessage, Message, MessageList, ProviderOptions, ToolCallPart, ToolMessage, UserMessage } from "../entities/message.js"; import { runIdGenerator } from "./run-id-gen.js"; import { LanguageModel, stepCountIs, streamText, tool, Tool, ToolSet } from "ai"; import { z } from "zod"; @@ -90,6 +90,7 @@ export class StreamStepMessageBuilder { private parts: z.infer[] = []; private textBuffer: string = ""; private reasoningBuffer: string = ""; + private providerOptions: z.infer | undefined = undefined; flushBuffers() { // skip reasoning @@ -123,8 +124,12 @@ export class StreamStepMessageBuilder { toolCallId: event.toolCallId, toolName: event.toolName, arguments: event.input, + providerOptions: event.providerOptions, }); break; + case "finish-step": + this.providerOptions = event.providerOptions; + break; } } @@ -133,6 +138,7 @@ export class StreamStepMessageBuilder { return { role: "assistant", content: this.parts, + providerOptions: this.providerOptions, }; } } @@ -173,12 +179,14 @@ export async function loadAgent(id: string): Promise> { export function convertFromMessages(messages: z.infer[]): ModelMessage[] { const result: ModelMessage[] = []; for (const msg of messages) { + const { providerOptions } = msg; switch (msg.role) { case "assistant": if (typeof msg.content === 'string') { result.push({ role: "assistant", content: msg.content, + providerOptions, }); } else { result.push({ @@ -195,9 +203,11 @@ export function convertFromMessages(messages: z.infer[]): ModelM toolCallId: part.toolCallId, toolName: part.toolName, input: part.arguments, + providerOptions: part.providerOptions, }; } }), + providerOptions, }); } break; @@ -205,12 +215,14 @@ export function convertFromMessages(messages: z.infer[]): ModelM result.push({ role: "system", content: msg.content, + providerOptions, }); break; case "user": result.push({ role: "user", content: msg.content, + providerOptions, }); break; case "tool": @@ -227,11 +239,13 @@ export function convertFromMessages(messages: z.infer[]): ModelM }, }, ], + providerOptions, }); break; } } - return result; + // doing this because: https://github.com/OpenRouterTeam/ai-sdk-provider/issues/262 + return JSON.parse(JSON.stringify(result)); } async function buildTools(agent: z.infer): Promise { @@ -446,7 +460,7 @@ export async function* streamAgent(state: AgentState): AsyncGenerator { const { apiKey, baseURL, headers } = providerConfig; switch (providerConfig.flavor) { case "rowboat [free]": - providerMap[name] = createOpenAICompatible({ - name: "rowboat [free]", - baseURL: "https://ai-gateway.rowboatlabs.com/v1", + providerMap[name] = createGateway({ + apiKey: "rowboatx", + baseURL: "https://ai-gateway.rowboatlabs.com/v1/ai", }); break; case "openai": @@ -40,6 +41,13 @@ export async function getProvider(name: string = ""): Promise { headers, }); break; + case "aigateway": + providerMap[name] = createGateway({ + apiKey, + baseURL, + headers + }); + break; case "anthropic": providerMap[name] = createAnthropic({ apiKey, @@ -65,7 +73,7 @@ export async function getProvider(name: string = ""): Promise { name, apiKey, baseURL : baseURL || "", - headers + headers, }); break; case "openrouter": diff --git a/apps/cli/src/application/lib/stream-renderer.ts b/apps/cli/src/application/lib/stream-renderer.ts index 30c4dcf5..5fc83bab 100644 --- a/apps/cli/src/application/lib/stream-renderer.ts +++ b/apps/cli/src/application/lib/stream-renderer.ts @@ -77,8 +77,8 @@ export class StreamRenderer { case "tool-call": this.onToolCall(event.toolCallId, event.toolName, event.input); break; - case "usage": - this.onUsage(event.usage); + case "finish-step": + this.onFinishStep(event.finishReason, event.usage); break; } } @@ -219,13 +219,15 @@ export class StreamRenderer { this.write("\n"); } - private onUsage(usage: { - inputTokens?: number; - outputTokens?: number; - totalTokens?: number; - reasoningTokens?: number; - cachedInputTokens?: number; - }) { + private onFinishStep( + finishReason: "stop" | "tool-calls" | "length" | "content-filter" | "error" | "other" | "unknown", + usage: { + inputTokens?: number; + outputTokens?: number; + totalTokens?: number; + reasoningTokens?: number; + cachedInputTokens?: number; + }) { const parts: string[] = []; if (usage.inputTokens !== undefined) parts.push(`${this.dim("in:")} ${usage.inputTokens}`); if (usage.outputTokens !== undefined) parts.push(`${this.dim("out:")} ${usage.outputTokens}`); @@ -234,8 +236,13 @@ export class StreamRenderer { if (usage.totalTokens !== undefined) parts.push(`${this.dim("total:")} ${this.bold(usage.totalTokens.toString())}`); const line = parts.join(this.dim(" | ")); this.write("\n"); - this.write(this.dim("╭─ Usage\n")); - this.write(this.dim("│ ") + line); + this.write(this.bold("╭─ ") + this.bold("Finish")); + this.write("\n"); + this.write(this.dim("│ ") + this.dim("reason: ") + finishReason); + if (line.length) { + this.write("\n"); + this.write(this.dim("│ ") + line); + } this.write("\n"); this.write(this.dim("╰─────────────\n")); }