Add timestamp to messages

This commit is contained in:
Mario Zechner 2025-10-26 00:43:43 +02:00
parent ef09efaac9
commit 55dc0b6e08
24 changed files with 388 additions and 220 deletions

View file

@ -159,6 +159,7 @@ export class Agent {
role: "user", role: "user",
content, content,
attachments: attachments?.length ? attachments : undefined, attachments: attachments?.length ? attachments : undefined,
timestamp: Date.now(),
}; };
this.abortController = new AbortController(); this.abortController = new AbortController();
@ -260,6 +261,7 @@ export class Agent {
}, },
stopReason: this.abortController?.signal.aborted ? "aborted" : "error", stopReason: this.abortController?.signal.aborted ? "aborted" : "error",
errorMessage: err?.message || String(err), errorMessage: err?.message || String(err),
timestamp: Date.now(),
}; };
this.appendMessage(msg as AppMessage); this.appendMessage(msg as AppMessage);
this.patch({ error: err?.message || String(err) }); this.patch({ error: err?.message || String(err) });

View file

@ -46,6 +46,7 @@ function streamSimpleProxy(
cacheWrite: 0, cacheWrite: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}, },
timestamp: Date.now(),
}; };
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined; let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;

View file

@ -86,7 +86,7 @@ describe("Agent", () => {
expect(agent.state.tools).toBe(tools); expect(agent.state.tools).toBe(tools);
// Test replaceMessages // Test replaceMessages
const messages = [{ role: "user" as const, content: "Hello" }]; const messages = [{ role: "user" as const, content: "Hello", timestamp: Date.now() }];
agent.replaceMessages(messages); agent.replaceMessages(messages);
expect(agent.state.messages).toEqual(messages); expect(agent.state.messages).toEqual(messages);
expect(agent.state.messages).not.toBe(messages); // Should be a copy expect(agent.state.messages).not.toBe(messages); // Should be a copy
@ -107,7 +107,7 @@ describe("Agent", () => {
transport: new ProviderTransport(), transport: new ProviderTransport(),
}); });
const message = { role: "user" as const, content: "Queued message" }; const message = { role: "user" as const, content: "Queued message", timestamp: Date.now() };
await agent.queueMessage(message); await agent.queueMessage(message);
// The message is queued but not yet in state.messages // The message is queued but not yet in state.messages

View file

@ -223,6 +223,7 @@ async function executeToolCalls<T>(
output: typeof resultOrError === "string" ? resultOrError : resultOrError.output, output: typeof resultOrError === "string" ? resultOrError : resultOrError.output,
details: typeof resultOrError === "string" ? ({} as T) : resultOrError.details, details: typeof resultOrError === "string" ? ({} as T) : resultOrError.details,
isError, isError,
timestamp: Date.now(),
}; };
results.push(toolResultMessage); results.push(toolResultMessage);

View file

@ -5,6 +5,23 @@ import type { Model } from "./types.js";
export const MODELS = { export const MODELS = {
anthropic: { anthropic: {
"claude-opus-4-0": {
id: "claude-opus-4-0",
name: "Claude Opus 4 (latest)",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 15,
output: 75,
cacheRead: 1.5,
cacheWrite: 18.75,
},
contextWindow: 200000,
maxTokens: 32000,
} satisfies Model<"anthropic-messages">,
"claude-3-5-sonnet-20241022": { "claude-3-5-sonnet-20241022": {
id: "claude-3-5-sonnet-20241022", id: "claude-3-5-sonnet-20241022",
name: "Claude Sonnet 3.5 v2", name: "Claude Sonnet 3.5 v2",
@ -22,6 +39,40 @@ export const MODELS = {
contextWindow: 200000, contextWindow: 200000,
maxTokens: 8192, maxTokens: 8192,
} satisfies Model<"anthropic-messages">, } satisfies Model<"anthropic-messages">,
"claude-opus-4-1": {
id: "claude-opus-4-1",
name: "Claude Opus 4.1 (latest)",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 15,
output: 75,
cacheRead: 1.5,
cacheWrite: 18.75,
},
contextWindow: 200000,
maxTokens: 32000,
} satisfies Model<"anthropic-messages">,
"claude-haiku-4-5": {
id: "claude-haiku-4-5",
name: "Claude Haiku 4.5 (latest)",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 1,
output: 5,
cacheRead: 0.1,
cacheWrite: 1.25,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-3-5-sonnet-20240620": { "claude-3-5-sonnet-20240620": {
id: "claude-3-5-sonnet-20240620", id: "claude-3-5-sonnet-20240620",
name: "Claude Sonnet 3.5", name: "Claude Sonnet 3.5",
@ -39,6 +90,23 @@ export const MODELS = {
contextWindow: 200000, contextWindow: 200000,
maxTokens: 8192, maxTokens: 8192,
} satisfies Model<"anthropic-messages">, } satisfies Model<"anthropic-messages">,
"claude-3-5-haiku-latest": {
id: "claude-3-5-haiku-latest",
name: "Claude Haiku 3.5 (latest)",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.8,
output: 4,
cacheRead: 0.08,
cacheWrite: 1,
},
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"anthropic-messages">,
"claude-3-opus-20240229": { "claude-3-opus-20240229": {
id: "claude-3-opus-20240229", id: "claude-3-opus-20240229",
name: "Claude Opus 3", name: "Claude Opus 3",
@ -56,6 +124,23 @@ export const MODELS = {
contextWindow: 200000, contextWindow: 200000,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"anthropic-messages">, } satisfies Model<"anthropic-messages">,
"claude-sonnet-4-5": {
id: "claude-sonnet-4-5",
name: "Claude Sonnet 4.5 (latest)",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-sonnet-4-5-20250929": { "claude-sonnet-4-5-20250929": {
id: "claude-sonnet-4-5-20250929", id: "claude-sonnet-4-5-20250929",
name: "Claude Sonnet 4.5", name: "Claude Sonnet 4.5",
@ -158,6 +243,40 @@ export const MODELS = {
contextWindow: 200000, contextWindow: 200000,
maxTokens: 64000, maxTokens: 64000,
} satisfies Model<"anthropic-messages">, } satisfies Model<"anthropic-messages">,
"claude-3-7-sonnet-latest": {
id: "claude-3-7-sonnet-latest",
name: "Claude Sonnet 3.7 (latest)",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-sonnet-4-0": {
id: "claude-sonnet-4-0",
name: "Claude Sonnet 4 (latest)",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-opus-4-1-20250805": { "claude-opus-4-1-20250805": {
id: "claude-opus-4-1-20250805", id: "claude-opus-4-1-20250805",
name: "Claude Opus 4.1", name: "Claude Opus 4.1",
@ -209,125 +328,6 @@ export const MODELS = {
contextWindow: 200000, contextWindow: 200000,
maxTokens: 64000, maxTokens: 64000,
} satisfies Model<"anthropic-messages">, } satisfies Model<"anthropic-messages">,
"claude-sonnet-4-0": {
id: "claude-sonnet-4-0",
name: "Claude Sonnet 4",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-3-7-sonnet-latest": {
id: "claude-3-7-sonnet-latest",
name: "Claude Sonnet 3.7",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-sonnet-4-5": {
id: "claude-sonnet-4-5",
name: "Claude Sonnet 4.5",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-3-5-haiku-latest": {
id: "claude-3-5-haiku-latest",
name: "Claude Haiku 3.5",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.8,
output: 4,
cacheRead: 0.08,
cacheWrite: 1,
},
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"anthropic-messages">,
"claude-haiku-4-5": {
id: "claude-haiku-4-5",
name: "Claude Haiku 4.5",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 1,
output: 5,
cacheRead: 0.1,
cacheWrite: 1.25,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-opus-4-1": {
id: "claude-opus-4-1",
name: "Claude Opus 4.1",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 15,
output: 75,
cacheRead: 1.5,
cacheWrite: 18.75,
},
contextWindow: 200000,
maxTokens: 32000,
} satisfies Model<"anthropic-messages">,
"claude-opus-4-0": {
id: "claude-opus-4-0",
name: "Claude Opus 4",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 15,
output: 75,
cacheRead: 1.5,
cacheWrite: 18.75,
},
contextWindow: 200000,
maxTokens: 32000,
} satisfies Model<"anthropic-messages">,
}, },
google: { google: {
"gemini-2.5-flash-preview-05-20": { "gemini-2.5-flash-preview-05-20": {
@ -1804,6 +1804,23 @@ export const MODELS = {
} satisfies Model<"anthropic-messages">, } satisfies Model<"anthropic-messages">,
}, },
openrouter: { openrouter: {
"minimax/minimax-m2:free": {
id: "minimax/minimax-m2:free",
name: "MiniMax: MiniMax M2 (free)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 204800,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"openrouter/andromeda-alpha": { "openrouter/andromeda-alpha": {
id: "openrouter/andromeda-alpha", id: "openrouter/andromeda-alpha",
name: "Andromeda Alpha", name: "Andromeda Alpha",
@ -1974,6 +1991,23 @@ export const MODELS = {
contextWindow: 202752, contextWindow: 202752,
maxTokens: 202752, maxTokens: 202752,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"z-ai/glm-4.6:exacto": {
id: "z-ai/glm-4.6:exacto",
name: "Z.AI: GLM 4.6 (exacto)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0.6,
output: 1.9,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 202752,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"deepseek/deepseek-v3.2-exp": { "deepseek/deepseek-v3.2-exp": {
id: "deepseek/deepseek-v3.2-exp", id: "deepseek/deepseek-v3.2-exp",
name: "DeepSeek: DeepSeek V3.2 Exp", name: "DeepSeek: DeepSeek V3.2 Exp",
@ -2014,7 +2048,7 @@ export const MODELS = {
api: "openai-completions", api: "openai-completions",
provider: "openrouter", provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1", baseUrl: "https://openrouter.ai/api/v1",
reasoning: true, reasoning: false,
input: ["text", "image"], input: ["text", "image"],
cost: { cost: {
input: 0.3, input: 0.3,
@ -2076,6 +2110,23 @@ export const MODELS = {
contextWindow: 163840, contextWindow: 163840,
maxTokens: 163840, maxTokens: 163840,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"deepseek/deepseek-v3.1-terminus:exacto": {
id: "deepseek/deepseek-v3.1-terminus:exacto",
name: "DeepSeek: DeepSeek V3.1 Terminus (exacto)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0.27,
output: 1,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"alibaba/tongyi-deepresearch-30b-a3b:free": { "alibaba/tongyi-deepresearch-30b-a3b:free": {
id: "alibaba/tongyi-deepresearch-30b-a3b:free", id: "alibaba/tongyi-deepresearch-30b-a3b:free",
name: "Tongyi DeepResearch 30B A3B (free)", name: "Tongyi DeepResearch 30B A3B (free)",
@ -2136,13 +2187,13 @@ export const MODELS = {
reasoning: true, reasoning: true,
input: ["text"], input: ["text"],
cost: { cost: {
input: 0.14, input: 0.15,
output: 1.2, output: 1.2,
cacheRead: 0, cacheRead: 0,
cacheWrite: 0, cacheWrite: 0,
}, },
contextWindow: 262144, contextWindow: 262144,
maxTokens: 4096, maxTokens: 262144,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"qwen/qwen3-next-80b-a3b-instruct": { "qwen/qwen3-next-80b-a3b-instruct": {
id: "qwen/qwen3-next-80b-a3b-instruct", id: "qwen/qwen3-next-80b-a3b-instruct",
@ -2263,6 +2314,23 @@ export const MODELS = {
contextWindow: 262144, contextWindow: 262144,
maxTokens: 262144, maxTokens: 262144,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"moonshotai/kimi-k2-0905:exacto": {
id: "moonshotai/kimi-k2-0905:exacto",
name: "MoonshotAI: Kimi K2 0905 (exacto)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.6,
output: 2.5,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"deepcogito/cogito-v2-preview-llama-70b": { "deepcogito/cogito-v2-preview-llama-70b": {
id: "deepcogito/cogito-v2-preview-llama-70b", id: "deepcogito/cogito-v2-preview-llama-70b",
name: "Deep Cogito: Cogito V2 Preview Llama 70B", name: "Deep Cogito: Cogito V2 Preview Llama 70B",
@ -2545,7 +2613,7 @@ export const MODELS = {
input: ["text"], input: ["text"],
cost: { cost: {
input: 0.35, input: 0.35,
output: 1.5, output: 1.55,
cacheRead: 0, cacheRead: 0,
cacheWrite: 0, cacheWrite: 0,
}, },
@ -2654,6 +2722,23 @@ export const MODELS = {
contextWindow: 262144, contextWindow: 262144,
maxTokens: 262144, maxTokens: 262144,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"qwen/qwen3-coder:exacto": {
id: "qwen/qwen3-coder:exacto",
name: "Qwen: Qwen3 Coder 480B A35B (exacto)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0.38,
output: 1.53,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"qwen/qwen3-235b-a22b-2507": { "qwen/qwen3-235b-a22b-2507": {
id: "qwen/qwen3-235b-a22b-2507", id: "qwen/qwen3-235b-a22b-2507",
name: "Qwen: Qwen3 235B A22B Instruct 2507", name: "Qwen: Qwen3 235B A22B Instruct 2507",
@ -2824,9 +2909,9 @@ export const MODELS = {
contextWindow: 40000, contextWindow: 40000,
maxTokens: 40000, maxTokens: 40000,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"mistralai/magistral-medium-2506": { "mistralai/magistral-medium-2506:thinking": {
id: "mistralai/magistral-medium-2506", id: "mistralai/magistral-medium-2506:thinking",
name: "Mistral: Magistral Medium 2506", name: "Mistral: Magistral Medium 2506 (thinking)",
api: "openai-completions", api: "openai-completions",
provider: "openrouter", provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1", baseUrl: "https://openrouter.ai/api/v1",
@ -2841,9 +2926,9 @@ export const MODELS = {
contextWindow: 40960, contextWindow: 40960,
maxTokens: 40000, maxTokens: 40000,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"mistralai/magistral-medium-2506:thinking": { "mistralai/magistral-medium-2506": {
id: "mistralai/magistral-medium-2506:thinking", id: "mistralai/magistral-medium-2506",
name: "Mistral: Magistral Medium 2506 (thinking)", name: "Mistral: Magistral Medium 2506",
api: "openai-completions", api: "openai-completions",
provider: "openrouter", provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1", baseUrl: "https://openrouter.ai/api/v1",
@ -3028,6 +3113,23 @@ export const MODELS = {
contextWindow: 40960, contextWindow: 40960,
maxTokens: 40960, maxTokens: 40960,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"qwen/qwen3-8b": {
id: "qwen/qwen3-8b",
name: "Qwen: Qwen3 8B",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0.035,
output: 0.13799999999999998,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 20000,
} satisfies Model<"openai-completions">,
"qwen/qwen3-14b": { "qwen/qwen3-14b": {
id: "qwen/qwen3-14b", id: "qwen/qwen3-14b",
name: "Qwen: Qwen3 14B", name: "Qwen: Qwen3 14B",
@ -3212,8 +3314,8 @@ export const MODELS = {
cacheRead: 0, cacheRead: 0,
cacheWrite: 0, cacheWrite: 0,
}, },
contextWindow: 128000, contextWindow: 96000,
maxTokens: 4096, maxTokens: 96000,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"mistralai/mistral-small-3.1-24b-instruct": { "mistralai/mistral-small-3.1-24b-instruct": {
id: "mistralai/mistral-small-3.1-24b-instruct", id: "mistralai/mistral-small-3.1-24b-instruct",
@ -3603,7 +3705,7 @@ export const MODELS = {
cacheRead: 0, cacheRead: 0,
cacheWrite: 0, cacheWrite: 0,
}, },
contextWindow: 128000, contextWindow: 131072,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"qwen/qwen-2.5-7b-instruct": { "qwen/qwen-2.5-7b-instruct": {
@ -3621,7 +3723,7 @@ export const MODELS = {
cacheWrite: 0, cacheWrite: 0,
}, },
contextWindow: 32768, contextWindow: 32768,
maxTokens: 16384, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"nvidia/llama-3.1-nemotron-70b-instruct": { "nvidia/llama-3.1-nemotron-70b-instruct": {
id: "nvidia/llama-3.1-nemotron-70b-instruct", id: "nvidia/llama-3.1-nemotron-70b-instruct",
@ -3708,23 +3810,6 @@ export const MODELS = {
contextWindow: 32768, contextWindow: 32768,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"cohere/command-r-plus-08-2024": {
id: "cohere/command-r-plus-08-2024",
name: "Cohere: Command R+ (08-2024)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 2.5,
output: 10,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4000,
} satisfies Model<"openai-completions">,
"cohere/command-r-08-2024": { "cohere/command-r-08-2024": {
id: "cohere/command-r-08-2024", id: "cohere/command-r-08-2024",
name: "Cohere: Command R (08-2024)", name: "Cohere: Command R (08-2024)",
@ -3742,6 +3827,23 @@ export const MODELS = {
contextWindow: 128000, contextWindow: 128000,
maxTokens: 4000, maxTokens: 4000,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"cohere/command-r-plus-08-2024": {
id: "cohere/command-r-plus-08-2024",
name: "Cohere: Command R+ (08-2024)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 2.5,
output: 10,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4000,
} satisfies Model<"openai-completions">,
"sao10k/l3.1-euryale-70b": { "sao10k/l3.1-euryale-70b": {
id: "sao10k/l3.1-euryale-70b", id: "sao10k/l3.1-euryale-70b",
name: "Sao10K: Llama 3.1 Euryale 70B v2.2", name: "Sao10K: Llama 3.1 Euryale 70B v2.2",
@ -3793,23 +3895,6 @@ export const MODELS = {
contextWindow: 65536, contextWindow: 65536,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-405b-instruct": {
id: "meta-llama/llama-3.1-405b-instruct",
name: "Meta: Llama 3.1 405B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.7999999999999999,
output: 0.7999999999999999,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 32768,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-8b-instruct": { "meta-llama/llama-3.1-8b-instruct": {
id: "meta-llama/llama-3.1-8b-instruct", id: "meta-llama/llama-3.1-8b-instruct",
name: "Meta: Llama 3.1 8B Instruct", name: "Meta: Llama 3.1 8B Instruct",
@ -3844,6 +3929,23 @@ export const MODELS = {
contextWindow: 131072, contextWindow: 131072,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-405b-instruct": {
id: "meta-llama/llama-3.1-405b-instruct",
name: "Meta: Llama 3.1 405B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.7999999999999999,
output: 0.7999999999999999,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 32768,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"mistralai/mistral-nemo": { "mistralai/mistral-nemo": {
id: "mistralai/mistral-nemo", id: "mistralai/mistral-nemo",
name: "Mistral: Mistral Nemo", name: "Mistral: Mistral Nemo",
@ -3963,23 +4065,6 @@ export const MODELS = {
contextWindow: 128000, contextWindow: 128000,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"meta-llama/llama-3-8b-instruct": {
id: "meta-llama/llama-3-8b-instruct",
name: "Meta: Llama 3 8B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.03,
output: 0.06,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 8192,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3-70b-instruct": { "meta-llama/llama-3-70b-instruct": {
id: "meta-llama/llama-3-70b-instruct", id: "meta-llama/llama-3-70b-instruct",
name: "Meta: Llama 3 70B Instruct", name: "Meta: Llama 3 70B Instruct",
@ -3997,6 +4082,23 @@ export const MODELS = {
contextWindow: 8192, contextWindow: 8192,
maxTokens: 16384, maxTokens: 16384,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"meta-llama/llama-3-8b-instruct": {
id: "meta-llama/llama-3-8b-instruct",
name: "Meta: Llama 3 8B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.03,
output: 0.06,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 8192,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"mistralai/mixtral-8x22b-instruct": { "mistralai/mixtral-8x22b-instruct": {
id: "mistralai/mixtral-8x22b-instruct", id: "mistralai/mixtral-8x22b-instruct",
name: "Mistral: Mixtral 8x22B Instruct", name: "Mistral: Mixtral 8x22B Instruct",
@ -4031,23 +4133,6 @@ export const MODELS = {
contextWindow: 128000, contextWindow: 128000,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"mistralai/mistral-small": {
id: "mistralai/mistral-small",
name: "Mistral Small",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.19999999999999998,
output: 0.6,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 32768,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/mistral-tiny": { "mistralai/mistral-tiny": {
id: "mistralai/mistral-tiny", id: "mistralai/mistral-tiny",
name: "Mistral Tiny", name: "Mistral Tiny",
@ -4065,6 +4150,23 @@ export const MODELS = {
contextWindow: 32768, contextWindow: 32768,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"mistralai/mistral-small": {
id: "mistralai/mistral-small",
name: "Mistral Small",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.19999999999999998,
output: 0.6,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 32768,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/mixtral-8x7b-instruct": { "mistralai/mixtral-8x7b-instruct": {
id: "mistralai/mixtral-8x7b-instruct", id: "mistralai/mixtral-8x7b-instruct",
name: "Mistral: Mixtral 8x7B Instruct", name: "Mistral: Mixtral 8x7B Instruct",

View file

@ -54,6 +54,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}, },
stopReason: "stop", stopReason: "stop",
timestamp: Date.now(),
}; };
try { try {

View file

@ -59,6 +59,7 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}, },
stopReason: "stop", stopReason: "stop",
timestamp: Date.now(),
}; };
try { try {

View file

@ -53,6 +53,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}, },
stopReason: "stop", stopReason: "stop",
timestamp: Date.now(),
}; };
try { try {

View file

@ -62,6 +62,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}, },
stopReason: "stop", stopReason: "stop",
timestamp: Date.now(),
}; };
try { try {

View file

@ -95,6 +95,7 @@ export type StopReason = "stop" | "length" | "toolUse" | "error" | "aborted";
export interface UserMessage { export interface UserMessage {
role: "user"; role: "user";
content: string | (TextContent | ImageContent)[]; content: string | (TextContent | ImageContent)[];
timestamp: number; // Unix timestamp in milliseconds
} }
export interface AssistantMessage { export interface AssistantMessage {
@ -106,6 +107,7 @@ export interface AssistantMessage {
usage: Usage; usage: Usage;
stopReason: StopReason; stopReason: StopReason;
errorMessage?: string; errorMessage?: string;
timestamp: number; // Unix timestamp in milliseconds
} }
export interface ToolResultMessage<TDetails = any> { export interface ToolResultMessage<TDetails = any> {
@ -115,6 +117,7 @@ export interface ToolResultMessage<TDetails = any> {
output: string; output: string;
details?: TDetails; details?: TDetails;
isError: boolean; isError: boolean;
timestamp: number; // Unix timestamp in milliseconds
} }
export type Message = UserMessage | AssistantMessage | ToolResultMessage; export type Message = UserMessage | AssistantMessage | ToolResultMessage;

View file

@ -9,6 +9,7 @@ async function testAbortSignal<TApi extends Api>(llm: Model<TApi>, options: Opti
{ {
role: "user", role: "user",
content: "What is 15 + 27? Think step by step. Then list 50 first names.", content: "What is 15 + 27? Think step by step. Then list 50 first names.",
timestamp: Date.now(),
}, },
], ],
}; };
@ -29,7 +30,11 @@ async function testAbortSignal<TApi extends Api>(llm: Model<TApi>, options: Opti
expect(msg.content.length).toBeGreaterThan(0); expect(msg.content.length).toBeGreaterThan(0);
context.messages.push(msg); context.messages.push(msg);
context.messages.push({ role: "user", content: "Please continue, but only generate 5 names." }); context.messages.push({
role: "user",
content: "Please continue, but only generate 5 names.",
timestamp: Date.now(),
});
const followUp = await complete(llm, context, options); const followUp = await complete(llm, context, options);
expect(followUp.stopReason).toBe("stop"); expect(followUp.stopReason).toBe("stop");
@ -42,7 +47,7 @@ async function testImmediateAbort<TApi extends Api>(llm: Model<TApi>, options: O
controller.abort(); controller.abort();
const context: Context = { const context: Context = {
messages: [{ role: "user", content: "Hello" }], messages: [{ role: "user", content: "Hello", timestamp: Date.now() }],
}; };
const response = await complete(llm, context, { ...options, signal: controller.signal }); const response = await complete(llm, context, { ...options, signal: controller.signal });

View file

@ -27,6 +27,7 @@ async function calculateTest<TApi extends Api>(model: Model<TApi>, options: Opti
1. Calculate 3485 * 4234 and 88823 * 3482 in parallel 1. Calculate 3485 * 4234 and 88823 * 3482 in parallel
2. Calculate the sum of the two results using the calculator tool 2. Calculate the sum of the two results using the calculator tool
3. Output ONLY the final sum as a single integer number, nothing else.`, 3. Output ONLY the final sum as a single integer number, nothing else.`,
timestamp: Date.now(),
}; };
// Calculate expected results (using integers) // Calculate expected results (using integers)
@ -176,6 +177,7 @@ async function abortTest<TApi extends Api>(model: Model<TApi>, options: OptionsF
const userPrompt: UserMessage = { const userPrompt: UserMessage = {
role: "user", role: "user",
content: "Calculate 100 * 200, then 300 * 400, then 500 * 600, then sum all three results.", content: "Calculate 100 * 200, then 300 * 400, then 500 * 600, then sum all three results.",
timestamp: Date.now(),
}; };
// Create abort controller // Create abort controller

View file

@ -8,6 +8,7 @@ async function testEmptyMessage<TApi extends Api>(llm: Model<TApi>, options: Opt
const emptyMessage: UserMessage = { const emptyMessage: UserMessage = {
role: "user", role: "user",
content: [], content: [],
timestamp: Date.now(),
}; };
const context: Context = { const context: Context = {
@ -34,6 +35,7 @@ async function testEmptyStringMessage<TApi extends Api>(llm: Model<TApi>, option
{ {
role: "user", role: "user",
content: "", content: "",
timestamp: Date.now(),
}, },
], ],
}; };
@ -58,6 +60,7 @@ async function testWhitespaceOnlyMessage<TApi extends Api>(llm: Model<TApi>, opt
{ {
role: "user", role: "user",
content: " \n\t ", content: " \n\t ",
timestamp: Date.now(),
}, },
], ],
}; };
@ -92,6 +95,7 @@ async function testEmptyAssistantMessage<TApi extends Api>(llm: Model<TApi>, opt
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}, },
stopReason: "stop", stopReason: "stop",
timestamp: Date.now(),
}; };
const context: Context = { const context: Context = {
@ -99,11 +103,13 @@ async function testEmptyAssistantMessage<TApi extends Api>(llm: Model<TApi>, opt
{ {
role: "user", role: "user",
content: "Hello, how are you?", content: "Hello, how are you?",
timestamp: Date.now(),
}, },
emptyAssistant, emptyAssistant,
{ {
role: "user", role: "user",
content: "Please respond this time.", content: "Please respond this time.",
timestamp: Date.now(),
}, },
], ],
}; };

View file

@ -49,6 +49,7 @@ const providerContexts = {
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}, },
stopReason: "toolUse", stopReason: "toolUse",
timestamp: Date.now(),
} satisfies AssistantMessage, } satisfies AssistantMessage,
toolResult: { toolResult: {
role: "toolResult" as const, role: "toolResult" as const,
@ -56,6 +57,7 @@ const providerContexts = {
toolName: "get_weather", toolName: "get_weather",
output: "Weather in Tokyo: 18°C, partly cloudy", output: "Weather in Tokyo: 18°C, partly cloudy",
isError: false, isError: false,
timestamp: Date.now(),
} satisfies ToolResultMessage, } satisfies ToolResultMessage,
facts: { facts: {
calculation: 391, calculation: 391,
@ -98,6 +100,7 @@ const providerContexts = {
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}, },
stopReason: "toolUse", stopReason: "toolUse",
timestamp: Date.now(),
} satisfies AssistantMessage, } satisfies AssistantMessage,
toolResult: { toolResult: {
role: "toolResult" as const, role: "toolResult" as const,
@ -105,6 +108,7 @@ const providerContexts = {
toolName: "get_weather", toolName: "get_weather",
output: "Weather in Berlin: 22°C, sunny", output: "Weather in Berlin: 22°C, sunny",
isError: false, isError: false,
timestamp: Date.now(),
} satisfies ToolResultMessage, } satisfies ToolResultMessage,
facts: { facts: {
calculation: 456, calculation: 456,
@ -146,6 +150,7 @@ const providerContexts = {
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}, },
stopReason: "toolUse", stopReason: "toolUse",
timestamp: Date.now(),
} satisfies AssistantMessage, } satisfies AssistantMessage,
toolResult: { toolResult: {
role: "toolResult" as const, role: "toolResult" as const,
@ -153,6 +158,7 @@ const providerContexts = {
toolName: "get_weather", toolName: "get_weather",
output: "Weather in London: 15°C, rainy", output: "Weather in London: 15°C, rainy",
isError: false, isError: false,
timestamp: Date.now(),
} satisfies ToolResultMessage, } satisfies ToolResultMessage,
facts: { facts: {
calculation: 525, calculation: 525,
@ -196,6 +202,7 @@ const providerContexts = {
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}, },
stopReason: "toolUse", stopReason: "toolUse",
timestamp: Date.now(),
} satisfies AssistantMessage, } satisfies AssistantMessage,
toolResult: { toolResult: {
role: "toolResult" as const, role: "toolResult" as const,
@ -203,6 +210,7 @@ const providerContexts = {
toolName: "get_weather", toolName: "get_weather",
output: "Weather in Sydney: 25°C, clear", output: "Weather in Sydney: 25°C, clear",
isError: false, isError: false,
timestamp: Date.now(),
} satisfies ToolResultMessage, } satisfies ToolResultMessage,
facts: { facts: {
calculation: 486, calculation: 486,
@ -239,6 +247,7 @@ const providerContexts = {
}, },
stopReason: "error", stopReason: "error",
errorMessage: "Request was aborted", errorMessage: "Request was aborted",
timestamp: Date.now(),
} satisfies AssistantMessage, } satisfies AssistantMessage,
toolResult: null, toolResult: null,
facts: { facts: {
@ -263,6 +272,7 @@ async function testProviderHandoff<TApi extends Api>(
{ {
role: "user", role: "user",
content: "Please do some calculations, tell me about capitals, and check the weather.", content: "Please do some calculations, tell me about capitals, and check the weather.",
timestamp: Date.now(),
}, },
sourceContext.message, sourceContext.message,
]; ];
@ -281,6 +291,7 @@ async function testProviderHandoff<TApi extends Api>(
3) What was the temperature? 3) What was the temperature?
4) What capital city was mentioned? 4) What capital city was mentioned?
Please include the specific numbers and names.`, Please include the specific numbers and names.`,
timestamp: Date.now(),
}); });
const context: Context = { const context: Context = {

View file

@ -32,7 +32,7 @@ const calculatorTool: Tool<typeof calculatorSchema> = {
async function basicTextGeneration<TApi extends Api>(model: Model<TApi>, options?: OptionsForApi<TApi>) { async function basicTextGeneration<TApi extends Api>(model: Model<TApi>, options?: OptionsForApi<TApi>) {
const context: Context = { const context: Context = {
systemPrompt: "You are a helpful assistant. Be concise.", systemPrompt: "You are a helpful assistant. Be concise.",
messages: [{ role: "user", content: "Reply with exactly: 'Hello test successful'" }], messages: [{ role: "user", content: "Reply with exactly: 'Hello test successful'", timestamp: Date.now() }],
}; };
const response = await complete(model, context, options); const response = await complete(model, context, options);
@ -44,7 +44,7 @@ async function basicTextGeneration<TApi extends Api>(model: Model<TApi>, options
expect(response.content.map((b) => (b.type === "text" ? b.text : "")).join("")).toContain("Hello test successful"); expect(response.content.map((b) => (b.type === "text" ? b.text : "")).join("")).toContain("Hello test successful");
context.messages.push(response); context.messages.push(response);
context.messages.push({ role: "user", content: "Now say 'Goodbye test successful'" }); context.messages.push({ role: "user", content: "Now say 'Goodbye test successful'", timestamp: Date.now() });
const secondResponse = await complete(model, context, options); const secondResponse = await complete(model, context, options);
@ -65,6 +65,7 @@ async function handleToolCall<TApi extends Api>(model: Model<TApi>, options?: Op
{ {
role: "user", role: "user",
content: "Calculate 15 + 27 using the calculator tool.", content: "Calculate 15 + 27 using the calculator tool.",
timestamp: Date.now(),
}, },
], ],
tools: [calculatorTool], tools: [calculatorTool],
@ -141,7 +142,7 @@ async function handleStreaming<TApi extends Api>(model: Model<TApi>, options?: O
let textCompleted = false; let textCompleted = false;
const context: Context = { const context: Context = {
messages: [{ role: "user", content: "Count from 1 to 3" }], messages: [{ role: "user", content: "Count from 1 to 3", timestamp: Date.now() }],
}; };
const s = stream(model, context, options); const s = stream(model, context, options);
@ -174,6 +175,7 @@ async function handleThinking<TApi extends Api>(model: Model<TApi>, options?: Op
{ {
role: "user", role: "user",
content: `Think long and hard about ${(Math.random() * 255) | 0} + 27. Think step by step. Then output the result.`, content: `Think long and hard about ${(Math.random() * 255) | 0} + 27. Think step by step. Then output the result.`,
timestamp: Date.now(),
}, },
], ],
}; };
@ -228,6 +230,7 @@ async function handleImage<TApi extends Api>(model: Model<TApi>, options?: Optio
}, },
imageContent, imageContent,
], ],
timestamp: Date.now(),
}, },
], ],
}; };
@ -251,6 +254,7 @@ async function multiTurn<TApi extends Api>(model: Model<TApi>, options?: Options
{ {
role: "user", role: "user",
content: "Think about this briefly, then calculate 42 * 17 and 453 + 434 using the calculator tool.", content: "Think about this briefly, then calculate 42 * 17 and 453 + 434 using the calculator tool.",
timestamp: Date.now(),
}, },
], ],
tools: [calculatorTool], tools: [calculatorTool],
@ -303,6 +307,7 @@ async function multiTurn<TApi extends Api>(model: Model<TApi>, options?: Options
toolName: block.name, toolName: block.name,
output: `${result}`, output: `${result}`,
isError: false, isError: false,
timestamp: Date.now(),
}); });
} }
} }

View file

@ -33,6 +33,7 @@ describe("Tool Call Without Result Tests", () => {
context.messages.push({ context.messages.push({
role: "user", role: "user",
content: "Please calculate 25 * 18 using the calculate tool.", content: "Please calculate 25 * 18 using the calculate tool.",
timestamp: Date.now(),
}); });
// Step 3: Get the assistant's response (should contain a tool call) // Step 3: Get the assistant's response (should contain a tool call)
@ -54,6 +55,7 @@ describe("Tool Call Without Result Tests", () => {
context.messages.push({ context.messages.push({
role: "user", role: "user",
content: "Never mind, just tell me what is 2+2?", content: "Never mind, just tell me what is 2+2?",
timestamp: Date.now(),
}); });
// Step 5: The fix should filter out the orphaned tool call, and the request should succeed // Step 5: The fix should filter out the orphaned tool call, and the request should succeed

View file

@ -22,6 +22,7 @@ async function testEmojiInToolResults<TApi extends Api>(llm: Model<TApi>, option
{ {
role: "user", role: "user",
content: "Use the test tool", content: "Use the test tool",
timestamp: Date.now(),
}, },
{ {
role: "assistant", role: "assistant",
@ -44,6 +45,7 @@ async function testEmojiInToolResults<TApi extends Api>(llm: Model<TApi>, option
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}, },
stopReason: "toolUse", stopReason: "toolUse",
timestamp: Date.now(),
}, },
], ],
tools: [ tools: [
@ -72,6 +74,7 @@ async function testEmojiInToolResults<TApi extends Api>(llm: Model<TApi>, option
- Mathematical symbols: - Mathematical symbols:
- Special quotes: "curly" 'quotes'`, - Special quotes: "curly" 'quotes'`,
isError: false, isError: false,
timestamp: Date.now(),
}; };
context.messages.push(toolResult); context.messages.push(toolResult);
@ -80,6 +83,7 @@ async function testEmojiInToolResults<TApi extends Api>(llm: Model<TApi>, option
context.messages.push({ context.messages.push({
role: "user", role: "user",
content: "Summarize the tool result briefly.", content: "Summarize the tool result briefly.",
timestamp: Date.now(),
}); });
// This should not throw a surrogate pair error // This should not throw a surrogate pair error
@ -97,6 +101,7 @@ async function testRealWorldLinkedInData<TApi extends Api>(llm: Model<TApi>, opt
{ {
role: "user", role: "user",
content: "Use the linkedin tool to get comments", content: "Use the linkedin tool to get comments",
timestamp: Date.now(),
}, },
{ {
role: "assistant", role: "assistant",
@ -119,6 +124,7 @@ async function testRealWorldLinkedInData<TApi extends Api>(llm: Model<TApi>, opt
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}, },
stopReason: "toolUse", stopReason: "toolUse",
timestamp: Date.now(),
}, },
], ],
tools: [ tools: [
@ -151,6 +157,7 @@ Unanswered Comments: 2
] ]
}`, }`,
isError: false, isError: false,
timestamp: Date.now(),
}; };
context.messages.push(toolResult); context.messages.push(toolResult);
@ -158,6 +165,7 @@ Unanswered Comments: 2
context.messages.push({ context.messages.push({
role: "user", role: "user",
content: "How many comments are there?", content: "How many comments are there?",
timestamp: Date.now(),
}); });
// This should not throw a surrogate pair error // This should not throw a surrogate pair error
@ -175,6 +183,7 @@ async function testUnpairedHighSurrogate<TApi extends Api>(llm: Model<TApi>, opt
{ {
role: "user", role: "user",
content: "Use the test tool", content: "Use the test tool",
timestamp: Date.now(),
}, },
{ {
role: "assistant", role: "assistant",
@ -197,6 +206,7 @@ async function testUnpairedHighSurrogate<TApi extends Api>(llm: Model<TApi>, opt
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}, },
stopReason: "toolUse", stopReason: "toolUse",
timestamp: Date.now(),
}, },
], ],
tools: [ tools: [
@ -218,6 +228,7 @@ async function testUnpairedHighSurrogate<TApi extends Api>(llm: Model<TApi>, opt
toolName: "test_tool", toolName: "test_tool",
output: `Text with unpaired surrogate: ${unpairedSurrogate} <- should be sanitized`, output: `Text with unpaired surrogate: ${unpairedSurrogate} <- should be sanitized`,
isError: false, isError: false,
timestamp: Date.now(),
}; };
context.messages.push(toolResult); context.messages.push(toolResult);
@ -225,6 +236,7 @@ async function testUnpairedHighSurrogate<TApi extends Api>(llm: Model<TApi>, opt
context.messages.push({ context.messages.push({
role: "user", role: "user",
content: "What did the tool return?", content: "What did the tool return?",
timestamp: Date.now(),
}); });
// This should not throw a surrogate pair error // This should not throw a surrogate pair error

View file

@ -162,6 +162,7 @@ export class Agent {
role: "user", role: "user",
content, content,
attachments: attachments?.length ? attachments : undefined, attachments: attachments?.length ? attachments : undefined,
timestamp: Date.now(),
}; };
this.abortController = new AbortController(); this.abortController = new AbortController();
@ -311,6 +312,7 @@ export class Agent {
}, },
stopReason: this.abortController?.signal.aborted ? "aborted" : "error", stopReason: this.abortController?.signal.aborted ? "aborted" : "error",
errorMessage: err?.message || String(err), errorMessage: err?.message || String(err),
timestamp: Date.now(),
}; };
this.appendMessage(msg as AppMessage); this.appendMessage(msg as AppMessage);
this.patch({ error: err?.message || String(err) }); this.patch({ error: err?.message || String(err) });

View file

@ -48,6 +48,7 @@ function streamSimpleProxy(
cacheWrite: 0, cacheWrite: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
}, },
timestamp: Date.now(),
}; };
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined; let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;

View file

@ -229,7 +229,14 @@ export class ToolMessage extends LitElement {
// Render tool content (renderer handles errors and styling) // Render tool content (renderer handles errors and styling)
const result: ToolResultMessageType<any> | undefined = this.aborted const result: ToolResultMessageType<any> | undefined = this.aborted
? { role: "toolResult", isError: true, output: "", toolCallId: this.toolCall.id, toolName: this.toolCall.name } ? {
role: "toolResult",
isError: true,
output: "",
toolCallId: this.toolCall.id,
toolName: this.toolCall.name,
timestamp: Date.now(),
}
: this.result; : this.result;
const renderResult = renderTool( const renderResult = renderTool(
toolName, toolName,

View file

@ -63,7 +63,7 @@ export class ProviderKeyInput extends LitElement {
} }
const context: Context = { const context: Context = {
messages: [{ role: "user", content: "Reply with: ok" }], messages: [{ role: "user", content: "Reply with: ok", timestamp: Date.now() }],
}; };
const result = await complete(model, context, { const result = await complete(model, context, {

View file

@ -208,13 +208,6 @@ export class SettingsDialog extends LitElement {
)} )}
</div> </div>
</div> </div>
<!-- Footer -->
<div class="pt-4 flex-shrink-0">
<p class="text-xs text-muted-foreground text-center">
${i18n("Settings are stored locally in your browser")}
</p>
</div>
</div> </div>
`, `,
})} })}

View file

@ -1,3 +1,4 @@
import type { AgentState } from "../../agent/agent.js";
import { Store } from "../store.js"; import { Store } from "../store.js";
import type { SessionData, SessionMetadata, StoreConfig } from "../types.js"; import type { SessionData, SessionMetadata, StoreConfig } from "../types.js";
@ -82,7 +83,12 @@ export class SessionsStore extends Store {
} }
// Alias methods for backward compatibility // Alias methods for backward compatibility
async saveSession(id: string, state: any, metadata: SessionMetadata | undefined, title?: string): Promise<void> { async saveSession(
id: string,
state: AgentState,
metadata: SessionMetadata | undefined,
title?: string,
): Promise<void> {
// If metadata is provided, use it; otherwise create it from state // If metadata is provided, use it; otherwise create it from state
const meta: SessionMetadata = metadata || { const meta: SessionMetadata = metadata || {
id, id,
@ -90,7 +96,7 @@ export class SessionsStore extends Store {
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
lastModified: new Date().toISOString(), lastModified: new Date().toISOString(),
messageCount: state.messages?.length || 0, messageCount: state.messages?.length || 0,
usage: state.usage || { usage: {
input: 0, input: 0,
output: 0, output: 0,
cacheRead: 0, cacheRead: 0,

View file

@ -100,7 +100,10 @@ export function createExtractDocumentTool(): AgentTool<typeof extractDocumentSch
// Extract filename from URL // Extract filename from URL
const urlParts = url.split("/"); const urlParts = url.split("/");
const fileName = urlParts[urlParts.length - 1]?.split("?")[0] || "document"; let fileName = urlParts[urlParts.length - 1]?.split("?")[0] || "document";
if (url.startsWith("https://arxiv.org/")) {
fileName = fileName + ".pdf";
}
// Use loadAttachment to process the document // Use loadAttachment to process the document
const attachment = await loadAttachment(arrayBuffer, fileName); const attachment = await loadAttachment(arrayBuffer, fileName);