@context-chef/ai-sdk-middleware 0.1.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,17 @@
1
1
  # @context-chef/ai-sdk-middleware
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@context-chef/ai-sdk-middleware.svg)](https://www.npmjs.com/package/@context-chef/ai-sdk-middleware)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@context-chef/ai-sdk-middleware.svg)](https://www.npmjs.com/package/@context-chef/ai-sdk-middleware)
5
+ [![License](https://img.shields.io/npm/l/@context-chef/ai-sdk-middleware.svg)](https://github.com/MyPrototypeWhat/context-chef/blob/main/LICENSE)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
7
+ [![AI SDK](https://img.shields.io/badge/AI%20SDK-v6-black.svg)](https://ai-sdk.dev)
4
8
 
5
- [Vercel AI SDK](https://sdk.vercel.ai) middleware powered by [context-chef](https://github.com/MyPrototypeWhat/context-chef). Transparent history compression, tool result truncation, and token budget management — zero code changes required.
9
+ [Vercel AI SDK](https://ai-sdk.dev) middleware powered by [context-chef](https://github.com/MyPrototypeWhat/context-chef). Transparent history compression, tool result truncation, and token budget management — zero code changes required.
6
10
 
7
11
  ## Installation
8
12
 
9
13
  ```bash
10
- npm install @context-chef/ai-sdk-middleware @context-chef/core ai
14
+ npm install @context-chef/ai-sdk-middleware ai
11
15
  ```
12
16
 
13
17
  ## Quick Start
@@ -19,9 +23,11 @@ import { generateText } from 'ai';
19
23
 
20
24
  const model = withContextChef(openai('gpt-4o'), {
21
25
  contextWindow: 128_000,
26
+ compress: { model: openai('gpt-4o-mini') },
27
+ truncate: { threshold: 5000, headChars: 500, tailChars: 1000 },
22
28
  });
23
29
 
24
- // Use exactly like normaleverything below is unchanged
30
+ // Everything below stays exactly the sameworks with generateText and streamText
25
31
  const result = await generateText({
26
32
  model,
27
33
  messages: conversationHistory,
@@ -29,7 +35,7 @@ const result = await generateText({
29
35
  });
30
36
  ```
31
37
 
32
- That's it. History compression and token budget tracking happen automatically behind the scenes.
38
+ That's it. History compression, tool result truncation, and token budget tracking happen automatically behind the scenes.
33
39
 
34
40
  ## Features
35
41
 
@@ -72,6 +78,22 @@ const model = withContextChef(openai('gpt-4o'), {
72
78
  });
73
79
  ```
74
80
 
81
+ Optionally persist the original content via a storage adapter so the LLM can retrieve it later via a `context://vfs/` URI:
82
+
83
+ ```typescript
84
+ import { FileSystemAdapter } from '@context-chef/core';
85
+
86
+ const model = withContextChef(openai('gpt-4o'), {
87
+ contextWindow: 128_000,
88
+ truncate: {
89
+ threshold: 5000,
90
+ headChars: 500,
91
+ tailChars: 1000,
92
+ storage: new FileSystemAdapter('.context_vfs'), // or your own DB adapter
93
+ },
94
+ });
95
+ ```
96
+
75
97
  ### Token Budget Tracking
76
98
 
77
99
  The middleware automatically extracts token usage from `generateText` and `streamText` responses and feeds it back to the compression engine. No manual `reportTokenUsage()` calls needed.
@@ -100,6 +122,7 @@ const wrappedModel = withContextChef(model, options);
100
122
  | `truncate.threshold` | `number` | Yes (if truncate) | Character count to trigger truncation |
101
123
  | `truncate.headChars` | `number` | No | Characters to preserve from start (default: `0`) |
102
124
  | `truncate.tailChars` | `number` | No | Characters to preserve from end (default: `1000`) |
125
+ | `truncate.storage` | `VFSStorageAdapter` | No | Storage adapter to persist original content before truncation |
103
126
  | `tokenizer` | `(msgs) => number` | No | Custom tokenizer for precise counting |
104
127
  | `onCompress` | `(summary, count) => void` | No | Hook called after compression |
105
128
 
@@ -132,11 +155,12 @@ const aiSdkPrompt = toAISDK(irMessages);
132
155
  ## How It Works
133
156
 
134
157
  ```
135
- generateText({ model: wrappedModel, messages })
158
+ generateText / streamText ({ model: wrappedModel, messages })
136
159
  |
137
160
  v
138
161
  transformParams (before LLM call)
139
162
  1. Truncate large tool results (if configured)
163
+ - Optionally persist originals to storage adapter
140
164
  2. Convert AI SDK messages -> context-chef IR
141
165
  3. Run Janitor compression (if over token budget)
142
166
  4. Convert back to AI SDK messages
package/dist/index.cjs CHANGED
@@ -6,7 +6,7 @@ let _context_chef_core = require("@context-chef/core");
6
6
  /**
7
7
  * Converts an AI SDK V3 prompt to context-chef IR messages.
8
8
  *
9
- * Original AI SDK content is stored in `_originalContent` for lossless round-trip.
9
+ * Original AI SDK content is stored in per-role fields for lossless round-trip.
10
10
  * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.
11
11
  * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).
12
12
  */
@@ -26,7 +26,7 @@ function fromAISDK(prompt) {
26
26
  messages.push({
27
27
  role: "user",
28
28
  content: text,
29
- _originalContent: msg.content,
29
+ _userContent: msg.content,
30
30
  _originalText: text,
31
31
  ...msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}
32
32
  });
@@ -50,7 +50,7 @@ function fromAISDK(prompt) {
50
50
  const m = {
51
51
  role: "assistant",
52
52
  content: joinedText,
53
- _originalContent: msg.content,
53
+ _assistantContent: msg.content,
54
54
  _originalText: joinedText,
55
55
  ...msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}
56
56
  };
@@ -66,7 +66,7 @@ function fromAISDK(prompt) {
66
66
  role: "tool",
67
67
  content: text,
68
68
  tool_call_id: part.toolCallId,
69
- _originalContent: [part],
69
+ _toolContent: [part],
70
70
  _originalText: text,
71
71
  _toolName: part.toolName
72
72
  });
@@ -76,9 +76,15 @@ function fromAISDK(prompt) {
76
76
  return messages;
77
77
  }
78
78
  /**
79
+ * Narrows a generic Message to AISDKMessage for typed access to pass-through fields.
80
+ */
81
+ function asAISDK(msg) {
82
+ return msg;
83
+ }
84
+ /**
79
85
  * Converts context-chef IR messages back to AI SDK V3 prompt format.
80
86
  *
81
- * Uses `_originalContent` when content is unmodified (detected via `_originalText`).
87
+ * Uses per-role original content when unmodified (detected via `_originalText`).
82
88
  * Falls back to constructing from IR fields when content was modified by Janitor
83
89
  * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).
84
90
  */
@@ -86,40 +92,37 @@ function toAISDK(messages) {
86
92
  const prompt = [];
87
93
  let i = 0;
88
94
  while (i < messages.length) {
89
- const msg = messages[i];
90
- const providerOptions = msg._providerOptions;
95
+ const msg = asAISDK(messages[i]);
91
96
  const contentModified = msg._originalText !== void 0 && msg._originalText !== msg.content;
92
97
  if (msg.role === "system") {
93
98
  prompt.push({
94
99
  role: "system",
95
100
  content: msg.content,
96
- ...providerOptions ? { providerOptions } : {}
101
+ ...msg._providerOptions ? { providerOptions: msg._providerOptions } : {}
97
102
  });
98
103
  i++;
99
104
  continue;
100
105
  }
101
106
  if (msg.role === "user") {
102
- const content = !contentModified && Array.isArray(msg._originalContent) ? msg._originalContent : [{
103
- type: "text",
104
- text: msg.content
105
- }];
106
107
  prompt.push({
107
108
  role: "user",
108
- content,
109
- ...providerOptions ? { providerOptions } : {}
109
+ content: !contentModified && msg._userContent ? msg._userContent : [{
110
+ type: "text",
111
+ text: msg.content
112
+ }],
113
+ ...msg._providerOptions ? { providerOptions: msg._providerOptions } : {}
110
114
  });
111
115
  i++;
112
116
  continue;
113
117
  }
114
118
  if (msg.role === "assistant") {
115
- const content = !contentModified && Array.isArray(msg._originalContent) ? msg._originalContent : [{
116
- type: "text",
117
- text: msg.content
118
- }];
119
119
  prompt.push({
120
120
  role: "assistant",
121
- content,
122
- ...providerOptions ? { providerOptions } : {}
121
+ content: !contentModified && msg._assistantContent ? msg._assistantContent : [{
122
+ type: "text",
123
+ text: msg.content
124
+ }],
125
+ ...msg._providerOptions ? { providerOptions: msg._providerOptions } : {}
123
126
  });
124
127
  i++;
125
128
  continue;
@@ -127,9 +130,10 @@ function toAISDK(messages) {
127
130
  if (msg.role === "tool") {
128
131
  const toolResults = [];
129
132
  while (i < messages.length && messages[i].role === "tool") {
130
- const toolMsg = messages[i];
131
- if (!(toolMsg._originalText !== void 0 && toolMsg._originalText !== toolMsg.content) && toolMsg._originalContent) toolResults.push(...toolMsg._originalContent);
132
- else toolResults.push({
133
+ const toolMsg = asAISDK(messages[i]);
134
+ if (!(toolMsg._originalText !== void 0 && toolMsg._originalText !== toolMsg.content) && toolMsg._toolContent) {
135
+ for (const part of toolMsg._toolContent) if (part.type === "tool-result") toolResults.push(part);
136
+ } else toolResults.push({
133
137
  type: "tool-result",
134
138
  toolCallId: toolMsg.tool_call_id ?? "",
135
139
  toolName: toolMsg._toolName ?? "unknown",
@@ -156,7 +160,7 @@ function stringifyToolOutput(output) {
156
160
  case "error-text": return output.value;
157
161
  case "json":
158
162
  case "error-json": return JSON.stringify(output.value);
159
- case "content": return output.value.map((v) => v.type === "text" ? v.text ?? "" : "").filter(Boolean).join("\n");
163
+ case "content": return output.value.map((v) => v.type === "text" ? v.text : "").filter(Boolean).join("\n");
160
164
  default: return JSON.stringify(output);
161
165
  }
162
166
  }
@@ -301,6 +305,14 @@ function createMiddleware(options) {
301
305
  };
302
306
  }
303
307
  /**
308
+ * Maps an IR role to a role accepted by generateText.
309
+ * Tool messages are handled separately before this is called.
310
+ */
311
+ function toCompressRole(role) {
312
+ if (role === "system" || role === "user" || role === "assistant") return role;
313
+ return "user";
314
+ }
315
+ /**
304
316
  * Adapts an AI SDK LanguageModelV3 into the compressionModel callback
305
317
  * that Janitor expects: (messages: Message[]) => Promise<string>
306
318
  *
@@ -324,7 +336,7 @@ function createCompressionAdapter(model) {
324
336
  };
325
337
  }
326
338
  return {
327
- role: m.role,
339
+ role: toCompressRole(m.role),
328
340
  content: m.content
329
341
  };
330
342
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["Offloader","Janitor"],"sources":["../src/adapter.ts","../src/truncator.ts","../src/middleware.ts","../src/index.ts"],"sourcesContent":["import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n SharedV3ProviderOptions,\n} from '@ai-sdk/provider';\nimport type { Message, ToolCall } from '@context-chef/core';\n\n/**\n * Converts an AI SDK V3 prompt to context-chef IR messages.\n *\n * Original AI SDK content is stored in `_originalContent` for lossless round-trip.\n * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.\n * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).\n */\nexport function fromAISDK(prompt: LanguageModelV3Prompt): Message[] {\n const messages: Message[] = [];\n\n for (const msg of prompt) {\n if (msg.role === 'system') {\n messages.push({\n role: 'system',\n content: msg.content,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'user') {\n const text = msg.content\n .filter((p) => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n messages.push({\n role: 'user',\n content: text,\n _originalContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\n let thinking: { thinking: string } | undefined;\n\n for (const part of msg.content) {\n if (part.type === 'text') text.push(part.text);\n else if (part.type === 'tool-call') {\n toolCalls.push({\n id: part.toolCallId,\n type: 'function',\n function: {\n name: part.toolName,\n arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),\n },\n });\n } else if (part.type === 'reasoning') {\n thinking = { thinking: part.text };\n }\n }\n\n const joinedText = text.join('\\n');\n const m: Message = {\n role: 'assistant',\n content: joinedText,\n _originalContent: msg.content,\n _originalText: joinedText,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (toolCalls.length > 0) m.tool_calls = toolCalls;\n if (thinking) m.thinking = thinking;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'tool') {\n for (const part of msg.content) {\n if (part.type === 'tool-result') {\n const text = stringifyToolOutput(part.output);\n messages.push({\n role: 'tool',\n content: text,\n tool_call_id: part.toolCallId,\n _originalContent: [part],\n _originalText: text,\n _toolName: part.toolName,\n });\n }\n }\n }\n }\n\n return messages;\n}\n\n/**\n * Converts context-chef IR messages back to AI SDK V3 prompt format.\n *\n * Uses `_originalContent` when content is unmodified (detected via `_originalText`).\n * Falls back to constructing from IR fields when content was modified by Janitor\n * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).\n */\nexport function toAISDK(messages: Message[]): LanguageModelV3Prompt {\n const prompt: LanguageModelV3Prompt = [];\n\n let i = 0;\n while (i < messages.length) {\n const msg = messages[i];\n const providerOptions = msg._providerOptions as SharedV3ProviderOptions | undefined;\n const contentModified = msg._originalText !== undefined && msg._originalText !== msg.content;\n\n if (msg.role === 'system') {\n prompt.push({\n role: 'system',\n content: msg.content,\n ...(providerOptions ? { providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'user') {\n const content =\n !contentModified && Array.isArray(msg._originalContent)\n ? (msg._originalContent as any)\n : [{ type: 'text' as const, text: msg.content }];\n prompt.push({\n role: 'user',\n content,\n ...(providerOptions ? { providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'assistant') {\n const content =\n !contentModified && Array.isArray(msg._originalContent)\n ? (msg._originalContent as any)\n : [{ type: 'text' as const, text: msg.content }];\n prompt.push({\n role: 'assistant',\n content,\n ...(providerOptions ? { providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'tool') {\n const toolResults: LanguageModelV3ToolResultPart[] = [];\n while (i < messages.length && messages[i].role === 'tool') {\n const toolMsg = messages[i];\n const toolModified =\n toolMsg._originalText !== undefined && toolMsg._originalText !== toolMsg.content;\n\n if (!toolModified && toolMsg._originalContent) {\n toolResults.push(...(toolMsg._originalContent as LanguageModelV3ToolResultPart[]));\n } else {\n toolResults.push({\n type: 'tool-result',\n toolCallId: toolMsg.tool_call_id ?? '',\n toolName: (toolMsg._toolName as string) ?? 'unknown',\n output: { type: 'text', value: toolMsg.content },\n });\n }\n i++;\n }\n prompt.push({ role: 'tool', content: toolResults });\n continue;\n }\n\n i++;\n }\n\n return prompt;\n}\n\nfunction stringifyToolOutput(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v: { type: string; text?: string }) => (v.type === 'text' ? (v.text ?? '') : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return JSON.stringify(output);\n }\n}\n","import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n} from '@ai-sdk/provider';\nimport { Offloader } from '@context-chef/core';\nimport type { TruncateOptions } from './types';\n\n/**\n * Truncates tool-result content within an AI SDK prompt when it exceeds the configured threshold.\n * When a storage adapter is provided, original content is persisted and a URI is included in the output.\n */\nexport async function truncateToolResults(\n prompt: LanguageModelV3Prompt,\n options: TruncateOptions,\n): Promise<LanguageModelV3Prompt> {\n const { threshold, headChars = 0, tailChars = 1000, storage } = options;\n\n const offloader = storage\n ? new Offloader({ threshold, adapter: storage, storageDir: '' })\n : null;\n\n const result: LanguageModelV3Prompt = [];\n\n for (const msg of prompt) {\n if (msg.role !== 'tool') {\n result.push(msg);\n continue;\n }\n\n const newContent: typeof msg.content = [];\n\n for (const part of msg.content) {\n if (part.type !== 'tool-result') {\n newContent.push(part);\n continue;\n }\n\n const text = extractText(part.output);\n if (text.length <= threshold || headChars + tailChars >= text.length) {\n newContent.push(part);\n continue;\n }\n\n // With storage: use Offloader to persist original and get a URI-annotated truncation\n if (offloader) {\n try {\n const vfsResult = await offloader.offloadAsync(text, { threshold, headChars, tailChars });\n newContent.push({\n ...part,\n output: { type: 'text', value: vfsResult.content } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n continue;\n } catch (error) {\n console.warn(\n `[context-chef] Storage adapter write failed for tool result (${part.toolCallId}). ` +\n `Falling back to simple truncation. Error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Fall through to simple truncation below\n }\n }\n\n // Without storage: simple truncation, original is discarded\n const head = text.slice(0, headChars);\n const tail = text.slice(text.length - tailChars);\n const totalLines = text.split('\\n').length;\n\n const truncated = [\n head,\n `\\n--- truncated (${totalLines} lines, ${text.length} chars total) ---\\n`,\n tail,\n ]\n .filter(Boolean)\n .join('')\n .trim();\n\n newContent.push({\n ...part,\n output: { type: 'text', value: truncated } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n }\n\n result.push({ ...msg, content: newContent });\n }\n\n return result;\n}\n\nfunction extractText(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v: { type: string; text?: string }) => (v.type === 'text' ? (v.text ?? '') : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return '';\n }\n}\n","import type { LanguageModelV3, LanguageModelV3StreamPart } from '@ai-sdk/provider';\nimport { generateText, type LanguageModelMiddleware } from 'ai';\nimport { Janitor, type Message } from '@context-chef/core';\n\nimport { fromAISDK, toAISDK } from './adapter';\nimport { truncateToolResults } from './truncator';\nimport type { ContextChefOptions } from './types';\n\n/**\n * Creates a LanguageModelMiddleware that transparently applies\n * context-chef compression and truncation to AI SDK model calls.\n *\n * The middleware holds a stateful Janitor instance that tracks\n * token usage across calls for compression decisions.\n */\nexport function createMiddleware(options: ContextChefOptions): LanguageModelMiddleware {\n let usageWarned = false;\n\n const janitor = new Janitor({\n contextWindow: options.contextWindow,\n tokenizer: options.tokenizer ? (msgs: Message[]) => options.tokenizer?.(msgs) ?? 0 : undefined,\n preserveRatio: options.compress?.preserveRatio ?? 0.8,\n compressionModel: options.compress?.model\n ? createCompressionAdapter(options.compress.model)\n : undefined,\n onCompress: options.onCompress\n ? (summary, count) => options.onCompress?.(summary.content, count)\n : undefined,\n });\n\n return {\n specificationVersion: 'v3',\n\n transformParams: async ({ params }) => {\n let { prompt } = params;\n\n // 1. Truncate large tool results\n if (options.truncate) {\n prompt = await truncateToolResults(prompt, options.truncate);\n }\n\n // 2. Compress history if over token budget\n const irMessages = fromAISDK(prompt);\n const compressed = await janitor.compress(irMessages);\n\n // Only convert back if compression actually changed something\n if (compressed !== irMessages) {\n prompt = toAISDK(compressed);\n }\n\n return { ...params, prompt };\n },\n\n wrapGenerate: async ({ doGenerate }) => {\n const result = await doGenerate();\n\n if (result.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(result.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Model response did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n\n return result;\n },\n\n wrapStream: async ({ doStream }) => {\n const { stream, ...rest } = await doStream();\n\n const transform = new TransformStream<LanguageModelV3StreamPart, LanguageModelV3StreamPart>({\n transform(chunk, controller) {\n if (chunk.type === 'finish') {\n if (chunk.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(chunk.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Stream finish did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n }\n controller.enqueue(chunk);\n },\n });\n\n return { ...rest, stream: stream.pipeThrough(transform) };\n },\n };\n}\n\n/**\n * Adapts an AI SDK LanguageModelV3 into the compressionModel callback\n * that Janitor expects: (messages: Message[]) => Promise<string>\n *\n * Tool messages are converted to user messages describing the tool interaction,\n * since generateText only accepts system/user/assistant roles.\n */\nfunction createCompressionAdapter(model: LanguageModelV3): (messages: Message[]) => Promise<string> {\n return async (messages: Message[]): Promise<string> => {\n const formatted = messages.map((m) => {\n if (m.role === 'tool') {\n return {\n role: 'user' as const,\n content: `[Tool result${m.tool_call_id ? ` (${m.tool_call_id})` : ''}: ${m.content}]`,\n };\n }\n // assistant messages with tool_calls: include tool call info in content\n if (m.role === 'assistant' && m.tool_calls?.length) {\n const toolCallsDesc = m.tool_calls\n .map((tc) => `[Called tool: ${tc.function.name}(${tc.function.arguments})]`)\n .join('\\n');\n return {\n role: 'assistant' as const,\n content: m.content ? `${m.content}\\n${toolCallsDesc}` : toolCallsDesc,\n };\n }\n return {\n role: m.role as 'system' | 'user' | 'assistant',\n content: m.content,\n };\n });\n\n const { text } = await generateText({\n model,\n messages: formatted,\n maxOutputTokens: 2048,\n });\n\n return text || '[Compression produced no output]';\n };\n}\n","import type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { wrapLanguageModel } from 'ai';\n\nimport { createMiddleware } from './middleware';\nimport type { ContextChefOptions } from './types';\n\nexport { fromAISDK, toAISDK } from './adapter';\nexport { createMiddleware } from './middleware';\nexport type { CompressOptions, ContextChefOptions, TruncateOptions } from './types';\n\n/**\n * Wraps an AI SDK language model with context-chef middleware for\n * transparent history compression, tool result truncation, and token budget management.\n *\n * @example\n * ```typescript\n * import { withContextChef } from '@context-chef/ai-sdk-middleware';\n * import { openai } from '@ai-sdk/openai';\n * import { generateText } from 'ai';\n *\n * const model = withContextChef(openai('gpt-4o'), {\n * contextWindow: 128_000,\n * compress: { model: openai('gpt-4o-mini') },\n * truncate: { threshold: 5000, headChars: 500, tailChars: 1000 },\n * });\n *\n * // Use exactly like normal — zero other code changes\n * const result = await generateText({ model, messages, tools });\n * ```\n */\nexport function withContextChef(model: LanguageModelV3, options: ContextChefOptions): LanguageModelV3 {\n const middleware = createMiddleware(options);\n return wrapLanguageModel({ model, middleware });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["Offloader","Janitor"],"sources":["../src/adapter.ts","../src/truncator.ts","../src/middleware.ts","../src/index.ts"],"sourcesContent":["import type {\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n SharedV3ProviderOptions,\n} from '@ai-sdk/provider';\nimport type { Message, ToolCall } from '@context-chef/core';\n\n/** Content types for each AI SDK message role */\ntype UserContent = Extract<LanguageModelV3Message, { role: 'user' }>['content'];\ntype AssistantContent = Extract<LanguageModelV3Message, { role: 'assistant' }>['content'];\ntype ToolContent = Extract<LanguageModelV3Message, { role: 'tool' }>['content'];\n\n/**\n * Extended IR message with typed pass-through fields for lossless AI SDK round-trip.\n * Per-role content fields avoid union types, so no `as` casts are needed in `toAISDK`.\n */\nexport interface AISDKMessage extends Message {\n _userContent?: UserContent;\n _assistantContent?: AssistantContent;\n _toolContent?: ToolContent;\n _originalText?: string;\n _providerOptions?: SharedV3ProviderOptions;\n _toolName?: string;\n}\n\n/**\n * Converts an AI SDK V3 prompt to context-chef IR messages.\n *\n * Original AI SDK content is stored in per-role fields for lossless round-trip.\n * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.\n * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).\n */\nexport function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[] {\n const messages: AISDKMessage[] = [];\n\n for (const msg of prompt) {\n if (msg.role === 'system') {\n messages.push({\n role: 'system',\n content: msg.content,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'user') {\n const text = msg.content\n .filter((p) => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n messages.push({\n role: 'user',\n content: text,\n _userContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\n let thinking: { thinking: string } | undefined;\n\n for (const part of msg.content) {\n if (part.type === 'text') text.push(part.text);\n else if (part.type === 'tool-call') {\n toolCalls.push({\n id: part.toolCallId,\n type: 'function',\n function: {\n name: part.toolName,\n arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),\n },\n });\n } else if (part.type === 'reasoning') {\n thinking = { thinking: part.text };\n }\n }\n\n const joinedText = text.join('\\n');\n const m: AISDKMessage = {\n role: 'assistant',\n content: joinedText,\n _assistantContent: msg.content,\n _originalText: joinedText,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (toolCalls.length > 0) m.tool_calls = toolCalls;\n if (thinking) m.thinking = thinking;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'tool') {\n for (const part of msg.content) {\n if (part.type === 'tool-result') {\n const text = stringifyToolOutput(part.output);\n messages.push({\n role: 'tool',\n content: text,\n tool_call_id: part.toolCallId,\n _toolContent: [part],\n _originalText: text,\n _toolName: part.toolName,\n });\n }\n }\n }\n }\n\n return messages;\n}\n\n/**\n * Narrows a generic Message to AISDKMessage for typed access to pass-through fields.\n */\nfunction asAISDK(msg: Message): AISDKMessage {\n return msg;\n}\n\n/**\n * Converts context-chef IR messages back to AI SDK V3 prompt format.\n *\n * Uses per-role original content when unmodified (detected via `_originalText`).\n * Falls back to constructing from IR fields when content was modified by Janitor\n * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).\n */\nexport function toAISDK(messages: Message[]): LanguageModelV3Prompt {\n const prompt: LanguageModelV3Prompt = [];\n\n let i = 0;\n while (i < messages.length) {\n const msg = asAISDK(messages[i]);\n const contentModified = msg._originalText !== undefined && msg._originalText !== msg.content;\n\n if (msg.role === 'system') {\n prompt.push({\n role: 'system',\n content: msg.content,\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'user') {\n prompt.push({\n role: 'user',\n content:\n !contentModified && msg._userContent\n ? msg._userContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'assistant') {\n prompt.push({\n role: 'assistant',\n content:\n !contentModified && msg._assistantContent\n ? msg._assistantContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'tool') {\n const toolResults: LanguageModelV3ToolResultPart[] = [];\n while (i < messages.length && messages[i].role === 'tool') {\n const toolMsg = asAISDK(messages[i]);\n const toolModified =\n toolMsg._originalText !== undefined && toolMsg._originalText !== toolMsg.content;\n\n if (!toolModified && toolMsg._toolContent) {\n for (const part of toolMsg._toolContent) {\n if (part.type === 'tool-result') {\n toolResults.push(part);\n }\n }\n } else {\n toolResults.push({\n type: 'tool-result',\n toolCallId: toolMsg.tool_call_id ?? '',\n toolName: toolMsg._toolName ?? 'unknown',\n output: { type: 'text', value: toolMsg.content },\n });\n }\n i++;\n }\n prompt.push({ role: 'tool', content: toolResults });\n continue;\n }\n\n i++;\n }\n\n return prompt;\n}\n\nfunction stringifyToolOutput(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v) => (v.type === 'text' ? v.text : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return JSON.stringify(output);\n }\n}\n","import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n} from '@ai-sdk/provider';\nimport { Offloader } from '@context-chef/core';\nimport type { TruncateOptions } from './types';\n\n/**\n * Truncates tool-result content within an AI SDK prompt when it exceeds the configured threshold.\n * When a storage adapter is provided, original content is persisted and a URI is included in the output.\n */\nexport async function truncateToolResults(\n prompt: LanguageModelV3Prompt,\n options: TruncateOptions,\n): Promise<LanguageModelV3Prompt> {\n const { threshold, headChars = 0, tailChars = 1000, storage } = options;\n\n const offloader = storage ? new Offloader({ threshold, adapter: storage, storageDir: '' }) : null;\n\n const result: LanguageModelV3Prompt = [];\n\n for (const msg of prompt) {\n if (msg.role !== 'tool') {\n result.push(msg);\n continue;\n }\n\n const newContent: typeof msg.content = [];\n\n for (const part of msg.content) {\n if (part.type !== 'tool-result') {\n newContent.push(part);\n continue;\n }\n\n const text = extractText(part.output);\n if (text.length <= threshold || headChars + tailChars >= text.length) {\n newContent.push(part);\n continue;\n }\n\n // With storage: use Offloader to persist original and get a URI-annotated truncation\n if (offloader) {\n try {\n const vfsResult = await offloader.offloadAsync(text, { threshold, headChars, tailChars });\n newContent.push({\n ...part,\n output: {\n type: 'text',\n value: vfsResult.content,\n } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n continue;\n } catch (error) {\n console.warn(\n `[context-chef] Storage adapter write failed for tool result (${part.toolCallId}). ` +\n `Falling back to simple truncation. Error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Fall through to simple truncation below\n }\n }\n\n // Without storage: simple truncation, original is discarded\n const head = text.slice(0, headChars);\n const tail = text.slice(text.length - tailChars);\n const totalLines = text.split('\\n').length;\n\n const truncated = [\n head,\n `\\n--- truncated (${totalLines} lines, ${text.length} chars total) ---\\n`,\n tail,\n ]\n .filter(Boolean)\n .join('')\n .trim();\n\n newContent.push({\n ...part,\n output: { type: 'text', value: truncated } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n }\n\n result.push({ ...msg, content: newContent });\n }\n\n return result;\n}\n\nfunction extractText(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v: { type: string; text?: string }) => (v.type === 'text' ? (v.text ?? '') : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return '';\n }\n}\n","import type { LanguageModelV3, LanguageModelV3StreamPart } from '@ai-sdk/provider';\nimport { Janitor, type Message } from '@context-chef/core';\nimport { generateText, type LanguageModelMiddleware } from 'ai';\n\nimport { fromAISDK, toAISDK } from './adapter';\nimport { truncateToolResults } from './truncator';\nimport type { ContextChefOptions } from './types';\n\ntype CompressRole = 'system' | 'user' | 'assistant';\n\n/**\n * Creates a LanguageModelMiddleware that transparently applies\n * context-chef compression and truncation to AI SDK model calls.\n *\n * The middleware holds a stateful Janitor instance that tracks\n * token usage across calls for compression decisions.\n */\nexport function createMiddleware(options: ContextChefOptions): LanguageModelMiddleware {\n let usageWarned = false;\n\n const janitor = new Janitor({\n contextWindow: options.contextWindow,\n tokenizer: options.tokenizer ? (msgs: Message[]) => options.tokenizer?.(msgs) ?? 0 : undefined,\n preserveRatio: options.compress?.preserveRatio ?? 0.8,\n compressionModel: options.compress?.model\n ? createCompressionAdapter(options.compress.model)\n : undefined,\n onCompress: options.onCompress\n ? (summary, count) => options.onCompress?.(summary.content, count)\n : undefined,\n });\n\n return {\n specificationVersion: 'v3',\n\n transformParams: async ({ params }) => {\n let { prompt } = params;\n\n // 1. Truncate large tool results\n if (options.truncate) {\n prompt = await truncateToolResults(prompt, options.truncate);\n }\n\n // 2. Compress history if over token budget\n const irMessages = fromAISDK(prompt);\n const compressed = await janitor.compress(irMessages);\n\n // Only convert back if compression actually changed something\n if (compressed !== irMessages) {\n prompt = toAISDK(compressed);\n }\n\n return { ...params, prompt };\n },\n\n wrapGenerate: async ({ doGenerate }) => {\n const result = await doGenerate();\n\n if (result.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(result.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Model response did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n\n return result;\n },\n\n wrapStream: async ({ doStream }) => {\n const { stream, ...rest } = await doStream();\n\n const transform = new TransformStream<LanguageModelV3StreamPart, LanguageModelV3StreamPart>({\n transform(chunk, controller) {\n if (chunk.type === 'finish') {\n if (chunk.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(chunk.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Stream finish did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n }\n controller.enqueue(chunk);\n },\n });\n\n return { ...rest, stream: stream.pipeThrough(transform) };\n },\n };\n}\n\n/**\n * Maps an IR role to a role accepted by generateText.\n * Tool messages are handled separately before this is called.\n */\nfunction toCompressRole(role: string): CompressRole {\n if (role === 'system' || role === 'user' || role === 'assistant') return role;\n return 'user';\n}\n\n/**\n * Adapts an AI SDK LanguageModelV3 into the compressionModel callback\n * that Janitor expects: (messages: Message[]) => Promise<string>\n *\n * Tool messages are converted to user messages describing the tool interaction,\n * since generateText only accepts system/user/assistant roles.\n */\nfunction createCompressionAdapter(\n model: LanguageModelV3,\n): (messages: Message[]) => Promise<string> {\n return async (messages: Message[]): Promise<string> => {\n const formatted = messages.map((m): { role: CompressRole; content: string } => {\n if (m.role === 'tool') {\n return {\n role: 'user' satisfies CompressRole,\n content: `[Tool result${m.tool_call_id ? ` (${m.tool_call_id})` : ''}: ${m.content}]`,\n };\n }\n if (m.role === 'assistant' && m.tool_calls?.length) {\n const toolCallsDesc = m.tool_calls\n .map((tc) => `[Called tool: ${tc.function.name}(${tc.function.arguments})]`)\n .join('\\n');\n return {\n role: 'assistant' satisfies CompressRole,\n content: m.content ? `${m.content}\\n${toolCallsDesc}` : toolCallsDesc,\n };\n }\n return {\n role: toCompressRole(m.role),\n content: m.content,\n };\n });\n\n const { text } = await generateText({\n model,\n messages: formatted,\n maxOutputTokens: 2048,\n });\n\n return text || '[Compression produced no output]';\n };\n}\n","import type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { wrapLanguageModel } from 'ai';\n\nimport { createMiddleware } from './middleware';\nimport type { ContextChefOptions } from './types';\n\nexport { type AISDKMessage, fromAISDK, toAISDK } from './adapter';\nexport { createMiddleware } from './middleware';\nexport type { CompressOptions, ContextChefOptions, TruncateOptions } from './types';\n\n/**\n * Wraps an AI SDK language model with context-chef middleware for\n * transparent history compression, tool result truncation, and token budget management.\n *\n * @example\n * ```typescript\n * import { withContextChef } from '@context-chef/ai-sdk-middleware';\n * import { openai } from '@ai-sdk/openai';\n * import { generateText } from 'ai';\n *\n * const model = withContextChef(openai('gpt-4o'), {\n * contextWindow: 128_000,\n * compress: { model: openai('gpt-4o-mini') },\n * truncate: { threshold: 5000, headChars: 500, tailChars: 1000 },\n * });\n *\n * // Use exactly like normal — zero other code changes\n * const result = await generateText({ model, messages, tools });\n * ```\n */\nexport function withContextChef(\n model: LanguageModelV3,\n options: ContextChefOptions,\n): LanguageModelV3 {\n const middleware = createMiddleware(options);\n return wrapLanguageModel({ model, middleware });\n}\n"],"mappings":";;;;;;;;;;;;AAkCA,SAAgB,UAAU,QAA+C;CACvE,MAAM,WAA2B,EAAE;AAEnC,MAAK,MAAM,OAAO,QAAQ;AACxB,MAAI,IAAI,SAAS,UAAU;AACzB,YAAS,KAAK;IACZ,MAAM;IACN,SAAS,IAAI;IACb,GAAI,IAAI,kBAAkB,EAAE,kBAAkB,IAAI,iBAAiB,GAAG,EAAE;IACzE,CAAC;AACF;;AAGF,MAAI,IAAI,SAAS,QAAQ;GACvB,MAAM,OAAO,IAAI,QACd,QAAQ,MAAM,EAAE,SAAS,OAAO,CAChC,KAAK,MAAM,EAAE,KAAK,CAClB,KAAK,KAAK;AACb,YAAS,KAAK;IACZ,MAAM;IACN,SAAS;IACT,cAAc,IAAI;IAClB,eAAe;IACf,GAAI,IAAI,kBAAkB,EAAE,kBAAkB,IAAI,iBAAiB,GAAG,EAAE;IACzE,CAAC;AACF;;AAGF,MAAI,IAAI,SAAS,aAAa;GAC5B,MAAM,OAAiB,EAAE;GACzB,MAAM,YAAwB,EAAE;GAChC,IAAI;AAEJ,QAAK,MAAM,QAAQ,IAAI,QACrB,KAAI,KAAK,SAAS,OAAQ,MAAK,KAAK,KAAK,KAAK;YACrC,KAAK,SAAS,YACrB,WAAU,KAAK;IACb,IAAI,KAAK;IACT,MAAM;IACN,UAAU;KACR,MAAM,KAAK;KACX,WAAW,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAK,UAAU,KAAK,MAAM;KACpF;IACF,CAAC;YACO,KAAK,SAAS,YACvB,YAAW,EAAE,UAAU,KAAK,MAAM;GAItC,MAAM,aAAa,KAAK,KAAK,KAAK;GAClC,MAAM,IAAkB;IACtB,MAAM;IACN,SAAS;IACT,mBAAmB,IAAI;IACvB,eAAe;IACf,GAAI,IAAI,kBAAkB,EAAE,kBAAkB,IAAI,iBAAiB,GAAG,EAAE;IACzE;AACD,OAAI,UAAU,SAAS,EAAG,GAAE,aAAa;AACzC,OAAI,SAAU,GAAE,WAAW;AAC3B,YAAS,KAAK,EAAE;AAChB;;AAGF,MAAI,IAAI,SAAS,QACf;QAAK,MAAM,QAAQ,IAAI,QACrB,KAAI,KAAK,SAAS,eAAe;IAC/B,MAAM,OAAO,oBAAoB,KAAK,OAAO;AAC7C,aAAS,KAAK;KACZ,MAAM;KACN,SAAS;KACT,cAAc,KAAK;KACnB,cAAc,CAAC,KAAK;KACpB,eAAe;KACf,WAAW,KAAK;KACjB,CAAC;;;;AAMV,QAAO;;;;;AAMT,SAAS,QAAQ,KAA4B;AAC3C,QAAO;;;;;;;;;AAUT,SAAgB,QAAQ,UAA4C;CAClE,MAAM,SAAgC,EAAE;CAExC,IAAI,IAAI;AACR,QAAO,IAAI,SAAS,QAAQ;EAC1B,MAAM,MAAM,QAAQ,SAAS,GAAG;EAChC,MAAM,kBAAkB,IAAI,kBAAkB,UAAa,IAAI,kBAAkB,IAAI;AAErF,MAAI,IAAI,SAAS,UAAU;AACzB,UAAO,KAAK;IACV,MAAM;IACN,SAAS,IAAI;IACb,GAAI,IAAI,mBAAmB,EAAE,iBAAiB,IAAI,kBAAkB,GAAG,EAAE;IAC1E,CAAC;AACF;AACA;;AAGF,MAAI,IAAI,SAAS,QAAQ;AACvB,UAAO,KAAK;IACV,MAAM;IACN,SACE,CAAC,mBAAmB,IAAI,eACpB,IAAI,eACJ,CAAC;KAAE,MAAM;KAAQ,MAAM,IAAI;KAAS,CAAC;IAC3C,GAAI,IAAI,mBAAmB,EAAE,iBAAiB,IAAI,kBAAkB,GAAG,EAAE;IAC1E,CAAC;AACF;AACA;;AAGF,MAAI,IAAI,SAAS,aAAa;AAC5B,UAAO,KAAK;IACV,MAAM;IACN,SACE,CAAC,mBAAmB,IAAI,oBACpB,IAAI,oBACJ,CAAC;KAAE,MAAM;KAAQ,MAAM,IAAI;KAAS,CAAC;IAC3C,GAAI,IAAI,mBAAmB,EAAE,iBAAiB,IAAI,kBAAkB,GAAG,EAAE;IAC1E,CAAC;AACF;AACA;;AAGF,MAAI,IAAI,SAAS,QAAQ;GACvB,MAAM,cAA+C,EAAE;AACvD,UAAO,IAAI,SAAS,UAAU,SAAS,GAAG,SAAS,QAAQ;IACzD,MAAM,UAAU,QAAQ,SAAS,GAAG;AAIpC,QAAI,EAFF,QAAQ,kBAAkB,UAAa,QAAQ,kBAAkB,QAAQ,YAEtD,QAAQ,cAC3B;UAAK,MAAM,QAAQ,QAAQ,aACzB,KAAI,KAAK,SAAS,cAChB,aAAY,KAAK,KAAK;UAI1B,aAAY,KAAK;KACf,MAAM;KACN,YAAY,QAAQ,gBAAgB;KACpC,UAAU,QAAQ,aAAa;KAC/B,QAAQ;MAAE,MAAM;MAAQ,OAAO,QAAQ;MAAS;KACjD,CAAC;AAEJ;;AAEF,UAAO,KAAK;IAAE,MAAM;IAAQ,SAAS;IAAa,CAAC;AACnD;;AAGF;;AAGF,QAAO;;AAGT,SAAS,oBAAoB,QAAiD;AAC5E,SAAQ,OAAO,MAAf;EACE,KAAK;EACL,KAAK,aACH,QAAO,OAAO;EAChB,KAAK;EACL,KAAK,aACH,QAAO,KAAK,UAAU,OAAO,MAAM;EACrC,KAAK,UACH,QAAO,OAAO,MACX,KAAK,MAAO,EAAE,SAAS,SAAS,EAAE,OAAO,GAAI,CAC7C,OAAO,QAAQ,CACf,KAAK,KAAK;EACf,QACE,QAAO,KAAK,UAAU,OAAO;;;;;;;;;;AClNnC,eAAsB,oBACpB,QACA,SACgC;CAChC,MAAM,EAAE,WAAW,YAAY,GAAG,YAAY,KAAM,YAAY;CAEhE,MAAM,YAAY,UAAU,IAAIA,6BAAU;EAAE;EAAW,SAAS;EAAS,YAAY;EAAI,CAAC,GAAG;CAE7F,MAAM,SAAgC,EAAE;AAExC,MAAK,MAAM,OAAO,QAAQ;AACxB,MAAI,IAAI,SAAS,QAAQ;AACvB,UAAO,KAAK,IAAI;AAChB;;EAGF,MAAM,aAAiC,EAAE;AAEzC,OAAK,MAAM,QAAQ,IAAI,SAAS;AAC9B,OAAI,KAAK,SAAS,eAAe;AAC/B,eAAW,KAAK,KAAK;AACrB;;GAGF,MAAM,OAAO,YAAY,KAAK,OAAO;AACrC,OAAI,KAAK,UAAU,aAAa,YAAY,aAAa,KAAK,QAAQ;AACpE,eAAW,KAAK,KAAK;AACrB;;AAIF,OAAI,UACF,KAAI;IACF,MAAM,YAAY,MAAM,UAAU,aAAa,MAAM;KAAE;KAAW;KAAW;KAAW,CAAC;AACzF,eAAW,KAAK;KACd,GAAG;KACH,QAAQ;MACN,MAAM;MACN,OAAO,UAAU;MAClB;KACF,CAAyC;AAC1C;YACO,OAAO;AACd,YAAQ,KACN,gEAAgE,KAAK,WAAW,+CACjC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,GACtG;;GAML,MAAM,OAAO,KAAK,MAAM,GAAG,UAAU;GACrC,MAAM,OAAO,KAAK,MAAM,KAAK,SAAS,UAAU;GAGhD,MAAM,YAAY;IAChB;IACA,oBAJiB,KAAK,MAAM,KAAK,CAAC,OAIH,UAAU,KAAK,OAAO;IACrD;IACD,CACE,OAAO,QAAQ,CACf,KAAK,GAAG,CACR,MAAM;AAET,cAAW,KAAK;IACd,GAAG;IACH,QAAQ;KAAE,MAAM;KAAQ,OAAO;KAAW;IAC3C,CAAyC;;AAG5C,SAAO,KAAK;GAAE,GAAG;GAAK,SAAS;GAAY,CAAC;;AAG9C,QAAO;;AAGT,SAAS,YAAY,QAAiD;AACpE,SAAQ,OAAO,MAAf;EACE,KAAK;EACL,KAAK,aACH,QAAO,OAAO;EAChB,KAAK;EACL,KAAK,aACH,QAAO,KAAK,UAAU,OAAO,MAAM;EACrC,KAAK,UACH,QAAO,OAAO,MACX,KAAK,MAAwC,EAAE,SAAS,SAAU,EAAE,QAAQ,KAAM,GAAI,CACtF,OAAO,QAAQ,CACf,KAAK,KAAK;EACf,QACE,QAAO;;;;;;;;;;;;;ACtFb,SAAgB,iBAAiB,SAAsD;CACrF,IAAI,cAAc;CAElB,MAAM,UAAU,IAAIC,2BAAQ;EAC1B,eAAe,QAAQ;EACvB,WAAW,QAAQ,aAAa,SAAoB,QAAQ,YAAY,KAAK,IAAI,IAAI;EACrF,eAAe,QAAQ,UAAU,iBAAiB;EAClD,kBAAkB,QAAQ,UAAU,QAChC,yBAAyB,QAAQ,SAAS,MAAM,GAChD;EACJ,YAAY,QAAQ,cACf,SAAS,UAAU,QAAQ,aAAa,QAAQ,SAAS,MAAM,GAChE;EACL,CAAC;AAEF,QAAO;EACL,sBAAsB;EAEtB,iBAAiB,OAAO,EAAE,aAAa;GACrC,IAAI,EAAE,WAAW;AAGjB,OAAI,QAAQ,SACV,UAAS,MAAM,oBAAoB,QAAQ,QAAQ,SAAS;GAI9D,MAAM,aAAa,UAAU,OAAO;GACpC,MAAM,aAAa,MAAM,QAAQ,SAAS,WAAW;AAGrD,OAAI,eAAe,WACjB,UAAS,QAAQ,WAAW;AAG9B,UAAO;IAAE,GAAG;IAAQ;IAAQ;;EAG9B,cAAc,OAAO,EAAE,iBAAiB;GACtC,MAAM,SAAS,MAAM,YAAY;AAEjC,OAAI,OAAO,OAAO,aAAa,SAAS,KACtC,SAAQ,eAAe,OAAO,MAAM,YAAY,MAAM;YAC7C,CAAC,eAAe,CAAC,QAAQ,WAAW;AAC7C,kBAAc;AACd,YAAQ,KACN,wLAGD;;AAGH,UAAO;;EAGT,YAAY,OAAO,EAAE,eAAe;GAClC,MAAM,EAAE,QAAQ,GAAG,SAAS,MAAM,UAAU;GAE5C,MAAM,YAAY,IAAI,gBAAsE,EAC1F,UAAU,OAAO,YAAY;AAC3B,QAAI,MAAM,SAAS,UACjB;SAAI,MAAM,OAAO,aAAa,SAAS,KACrC,SAAQ,eAAe,MAAM,MAAM,YAAY,MAAM;cAC5C,CAAC,eAAe,CAAC,QAAQ,WAAW;AAC7C,oBAAc;AACd,cAAQ,KACN,uLAGD;;;AAGL,eAAW,QAAQ,MAAM;MAE5B,CAAC;AAEF,UAAO;IAAE,GAAG;IAAM,QAAQ,OAAO,YAAY,UAAU;IAAE;;EAE5D;;;;;;AAOH,SAAS,eAAe,MAA4B;AAClD,KAAI,SAAS,YAAY,SAAS,UAAU,SAAS,YAAa,QAAO;AACzE,QAAO;;;;;;;;;AAUT,SAAS,yBACP,OAC0C;AAC1C,QAAO,OAAO,aAAyC;EAuBrD,MAAM,EAAE,SAAS,2BAAmB;GAClC;GACA,UAxBgB,SAAS,KAAK,MAA+C;AAC7E,QAAI,EAAE,SAAS,OACb,QAAO;KACL,MAAM;KACN,SAAS,eAAe,EAAE,eAAe,KAAK,EAAE,aAAa,KAAK,GAAG,IAAI,EAAE,QAAQ;KACpF;AAEH,QAAI,EAAE,SAAS,eAAe,EAAE,YAAY,QAAQ;KAClD,MAAM,gBAAgB,EAAE,WACrB,KAAK,OAAO,iBAAiB,GAAG,SAAS,KAAK,GAAG,GAAG,SAAS,UAAU,IAAI,CAC3E,KAAK,KAAK;AACb,YAAO;MACL,MAAM;MACN,SAAS,EAAE,UAAU,GAAG,EAAE,QAAQ,IAAI,kBAAkB;MACzD;;AAEH,WAAO;KACL,MAAM,eAAe,EAAE,KAAK;KAC5B,SAAS,EAAE;KACZ;KACD;GAKA,iBAAiB;GAClB,CAAC;AAEF,SAAO,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;ACpHnB,SAAgB,gBACd,OACA,SACiB;AAEjB,kCAAyB;EAAE;EAAO,YADf,iBAAiB,QAAQ;EACE,CAAC"}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { LanguageModelV3, LanguageModelV3Prompt } from "@ai-sdk/provider";
1
+ import { LanguageModelV3, LanguageModelV3Message, LanguageModelV3Prompt, SharedV3ProviderOptions } from "@ai-sdk/provider";
2
2
  import { Message, VFSStorageAdapter } from "@context-chef/core";
3
3
  import { LanguageModelMiddleware } from "ai";
4
4
 
@@ -38,18 +38,40 @@ interface ContextChefOptions {
38
38
  }
39
39
  //#endregion
40
40
  //#region src/adapter.d.ts
41
+ /** Content types for each AI SDK message role */
42
+ type UserContent = Extract<LanguageModelV3Message, {
43
+ role: 'user';
44
+ }>['content'];
45
+ type AssistantContent = Extract<LanguageModelV3Message, {
46
+ role: 'assistant';
47
+ }>['content'];
48
+ type ToolContent = Extract<LanguageModelV3Message, {
49
+ role: 'tool';
50
+ }>['content'];
51
+ /**
52
+ * Extended IR message with typed pass-through fields for lossless AI SDK round-trip.
53
+ * Per-role content fields avoid union types, so no `as` casts are needed in `toAISDK`.
54
+ */
55
+ interface AISDKMessage extends Message {
56
+ _userContent?: UserContent;
57
+ _assistantContent?: AssistantContent;
58
+ _toolContent?: ToolContent;
59
+ _originalText?: string;
60
+ _providerOptions?: SharedV3ProviderOptions;
61
+ _toolName?: string;
62
+ }
41
63
  /**
42
64
  * Converts an AI SDK V3 prompt to context-chef IR messages.
43
65
  *
44
- * Original AI SDK content is stored in `_originalContent` for lossless round-trip.
66
+ * Original AI SDK content is stored in per-role fields for lossless round-trip.
45
67
  * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.
46
68
  * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).
47
69
  */
48
- declare function fromAISDK(prompt: LanguageModelV3Prompt): Message[];
70
+ declare function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[];
49
71
  /**
50
72
  * Converts context-chef IR messages back to AI SDK V3 prompt format.
51
73
  *
52
- * Uses `_originalContent` when content is unmodified (detected via `_originalText`).
74
+ * Uses per-role original content when unmodified (detected via `_originalText`).
53
75
  * Falls back to constructing from IR fields when content was modified by Janitor
54
76
  * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).
55
77
  */
@@ -88,5 +110,5 @@ declare function createMiddleware(options: ContextChefOptions): LanguageModelMid
88
110
  */
89
111
  declare function withContextChef(model: LanguageModelV3, options: ContextChefOptions): LanguageModelV3;
90
112
  //#endregion
91
- export { type CompressOptions, type ContextChefOptions, type TruncateOptions, createMiddleware, fromAISDK, toAISDK, withContextChef };
113
+ export { type AISDKMessage, type CompressOptions, type ContextChefOptions, type TruncateOptions, createMiddleware, fromAISDK, toAISDK, withContextChef };
92
114
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { LanguageModelMiddleware } from "ai";
2
2
  import { Message, VFSStorageAdapter } from "@context-chef/core";
3
- import { LanguageModelV3, LanguageModelV3Prompt } from "@ai-sdk/provider";
3
+ import { LanguageModelV3, LanguageModelV3Message, LanguageModelV3Prompt, SharedV3ProviderOptions } from "@ai-sdk/provider";
4
4
 
5
5
  //#region src/types.d.ts
6
6
  interface TruncateOptions {
@@ -38,18 +38,40 @@ interface ContextChefOptions {
38
38
  }
39
39
  //#endregion
40
40
  //#region src/adapter.d.ts
41
+ /** Content types for each AI SDK message role */
42
+ type UserContent = Extract<LanguageModelV3Message, {
43
+ role: 'user';
44
+ }>['content'];
45
+ type AssistantContent = Extract<LanguageModelV3Message, {
46
+ role: 'assistant';
47
+ }>['content'];
48
+ type ToolContent = Extract<LanguageModelV3Message, {
49
+ role: 'tool';
50
+ }>['content'];
51
+ /**
52
+ * Extended IR message with typed pass-through fields for lossless AI SDK round-trip.
53
+ * Per-role content fields avoid union types, so no `as` casts are needed in `toAISDK`.
54
+ */
55
+ interface AISDKMessage extends Message {
56
+ _userContent?: UserContent;
57
+ _assistantContent?: AssistantContent;
58
+ _toolContent?: ToolContent;
59
+ _originalText?: string;
60
+ _providerOptions?: SharedV3ProviderOptions;
61
+ _toolName?: string;
62
+ }
41
63
  /**
42
64
  * Converts an AI SDK V3 prompt to context-chef IR messages.
43
65
  *
44
- * Original AI SDK content is stored in `_originalContent` for lossless round-trip.
66
+ * Original AI SDK content is stored in per-role fields for lossless round-trip.
45
67
  * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.
46
68
  * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).
47
69
  */
48
- declare function fromAISDK(prompt: LanguageModelV3Prompt): Message[];
70
+ declare function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[];
49
71
  /**
50
72
  * Converts context-chef IR messages back to AI SDK V3 prompt format.
51
73
  *
52
- * Uses `_originalContent` when content is unmodified (detected via `_originalText`).
74
+ * Uses per-role original content when unmodified (detected via `_originalText`).
53
75
  * Falls back to constructing from IR fields when content was modified by Janitor
54
76
  * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).
55
77
  */
@@ -88,5 +110,5 @@ declare function createMiddleware(options: ContextChefOptions): LanguageModelMid
88
110
  */
89
111
  declare function withContextChef(model: LanguageModelV3, options: ContextChefOptions): LanguageModelV3;
90
112
  //#endregion
91
- export { type CompressOptions, type ContextChefOptions, type TruncateOptions, createMiddleware, fromAISDK, toAISDK, withContextChef };
113
+ export { type AISDKMessage, type CompressOptions, type ContextChefOptions, type TruncateOptions, createMiddleware, fromAISDK, toAISDK, withContextChef };
92
114
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -5,7 +5,7 @@ import { Janitor, Offloader } from "@context-chef/core";
5
5
  /**
6
6
  * Converts an AI SDK V3 prompt to context-chef IR messages.
7
7
  *
8
- * Original AI SDK content is stored in `_originalContent` for lossless round-trip.
8
+ * Original AI SDK content is stored in per-role fields for lossless round-trip.
9
9
  * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.
10
10
  * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).
11
11
  */
@@ -25,7 +25,7 @@ function fromAISDK(prompt) {
25
25
  messages.push({
26
26
  role: "user",
27
27
  content: text,
28
- _originalContent: msg.content,
28
+ _userContent: msg.content,
29
29
  _originalText: text,
30
30
  ...msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}
31
31
  });
@@ -49,7 +49,7 @@ function fromAISDK(prompt) {
49
49
  const m = {
50
50
  role: "assistant",
51
51
  content: joinedText,
52
- _originalContent: msg.content,
52
+ _assistantContent: msg.content,
53
53
  _originalText: joinedText,
54
54
  ...msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}
55
55
  };
@@ -65,7 +65,7 @@ function fromAISDK(prompt) {
65
65
  role: "tool",
66
66
  content: text,
67
67
  tool_call_id: part.toolCallId,
68
- _originalContent: [part],
68
+ _toolContent: [part],
69
69
  _originalText: text,
70
70
  _toolName: part.toolName
71
71
  });
@@ -75,9 +75,15 @@ function fromAISDK(prompt) {
75
75
  return messages;
76
76
  }
77
77
  /**
78
+ * Narrows a generic Message to AISDKMessage for typed access to pass-through fields.
79
+ */
80
+ function asAISDK(msg) {
81
+ return msg;
82
+ }
83
+ /**
78
84
  * Converts context-chef IR messages back to AI SDK V3 prompt format.
79
85
  *
80
- * Uses `_originalContent` when content is unmodified (detected via `_originalText`).
86
+ * Uses per-role original content when unmodified (detected via `_originalText`).
81
87
  * Falls back to constructing from IR fields when content was modified by Janitor
82
88
  * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).
83
89
  */
@@ -85,40 +91,37 @@ function toAISDK(messages) {
85
91
  const prompt = [];
86
92
  let i = 0;
87
93
  while (i < messages.length) {
88
- const msg = messages[i];
89
- const providerOptions = msg._providerOptions;
94
+ const msg = asAISDK(messages[i]);
90
95
  const contentModified = msg._originalText !== void 0 && msg._originalText !== msg.content;
91
96
  if (msg.role === "system") {
92
97
  prompt.push({
93
98
  role: "system",
94
99
  content: msg.content,
95
- ...providerOptions ? { providerOptions } : {}
100
+ ...msg._providerOptions ? { providerOptions: msg._providerOptions } : {}
96
101
  });
97
102
  i++;
98
103
  continue;
99
104
  }
100
105
  if (msg.role === "user") {
101
- const content = !contentModified && Array.isArray(msg._originalContent) ? msg._originalContent : [{
102
- type: "text",
103
- text: msg.content
104
- }];
105
106
  prompt.push({
106
107
  role: "user",
107
- content,
108
- ...providerOptions ? { providerOptions } : {}
108
+ content: !contentModified && msg._userContent ? msg._userContent : [{
109
+ type: "text",
110
+ text: msg.content
111
+ }],
112
+ ...msg._providerOptions ? { providerOptions: msg._providerOptions } : {}
109
113
  });
110
114
  i++;
111
115
  continue;
112
116
  }
113
117
  if (msg.role === "assistant") {
114
- const content = !contentModified && Array.isArray(msg._originalContent) ? msg._originalContent : [{
115
- type: "text",
116
- text: msg.content
117
- }];
118
118
  prompt.push({
119
119
  role: "assistant",
120
- content,
121
- ...providerOptions ? { providerOptions } : {}
120
+ content: !contentModified && msg._assistantContent ? msg._assistantContent : [{
121
+ type: "text",
122
+ text: msg.content
123
+ }],
124
+ ...msg._providerOptions ? { providerOptions: msg._providerOptions } : {}
122
125
  });
123
126
  i++;
124
127
  continue;
@@ -126,9 +129,10 @@ function toAISDK(messages) {
126
129
  if (msg.role === "tool") {
127
130
  const toolResults = [];
128
131
  while (i < messages.length && messages[i].role === "tool") {
129
- const toolMsg = messages[i];
130
- if (!(toolMsg._originalText !== void 0 && toolMsg._originalText !== toolMsg.content) && toolMsg._originalContent) toolResults.push(...toolMsg._originalContent);
131
- else toolResults.push({
132
+ const toolMsg = asAISDK(messages[i]);
133
+ if (!(toolMsg._originalText !== void 0 && toolMsg._originalText !== toolMsg.content) && toolMsg._toolContent) {
134
+ for (const part of toolMsg._toolContent) if (part.type === "tool-result") toolResults.push(part);
135
+ } else toolResults.push({
132
136
  type: "tool-result",
133
137
  toolCallId: toolMsg.tool_call_id ?? "",
134
138
  toolName: toolMsg._toolName ?? "unknown",
@@ -155,7 +159,7 @@ function stringifyToolOutput(output) {
155
159
  case "error-text": return output.value;
156
160
  case "json":
157
161
  case "error-json": return JSON.stringify(output.value);
158
- case "content": return output.value.map((v) => v.type === "text" ? v.text ?? "" : "").filter(Boolean).join("\n");
162
+ case "content": return output.value.map((v) => v.type === "text" ? v.text : "").filter(Boolean).join("\n");
159
163
  default: return JSON.stringify(output);
160
164
  }
161
165
  }
@@ -300,6 +304,14 @@ function createMiddleware(options) {
300
304
  };
301
305
  }
302
306
  /**
307
+ * Maps an IR role to a role accepted by generateText.
308
+ * Tool messages are handled separately before this is called.
309
+ */
310
+ function toCompressRole(role) {
311
+ if (role === "system" || role === "user" || role === "assistant") return role;
312
+ return "user";
313
+ }
314
+ /**
303
315
  * Adapts an AI SDK LanguageModelV3 into the compressionModel callback
304
316
  * that Janitor expects: (messages: Message[]) => Promise<string>
305
317
  *
@@ -323,7 +335,7 @@ function createCompressionAdapter(model) {
323
335
  };
324
336
  }
325
337
  return {
326
- role: m.role,
338
+ role: toCompressRole(m.role),
327
339
  content: m.content
328
340
  };
329
341
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/adapter.ts","../src/truncator.ts","../src/middleware.ts","../src/index.ts"],"sourcesContent":["import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n SharedV3ProviderOptions,\n} from '@ai-sdk/provider';\nimport type { Message, ToolCall } from '@context-chef/core';\n\n/**\n * Converts an AI SDK V3 prompt to context-chef IR messages.\n *\n * Original AI SDK content is stored in `_originalContent` for lossless round-trip.\n * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.\n * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).\n */\nexport function fromAISDK(prompt: LanguageModelV3Prompt): Message[] {\n const messages: Message[] = [];\n\n for (const msg of prompt) {\n if (msg.role === 'system') {\n messages.push({\n role: 'system',\n content: msg.content,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'user') {\n const text = msg.content\n .filter((p) => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n messages.push({\n role: 'user',\n content: text,\n _originalContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\n let thinking: { thinking: string } | undefined;\n\n for (const part of msg.content) {\n if (part.type === 'text') text.push(part.text);\n else if (part.type === 'tool-call') {\n toolCalls.push({\n id: part.toolCallId,\n type: 'function',\n function: {\n name: part.toolName,\n arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),\n },\n });\n } else if (part.type === 'reasoning') {\n thinking = { thinking: part.text };\n }\n }\n\n const joinedText = text.join('\\n');\n const m: Message = {\n role: 'assistant',\n content: joinedText,\n _originalContent: msg.content,\n _originalText: joinedText,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (toolCalls.length > 0) m.tool_calls = toolCalls;\n if (thinking) m.thinking = thinking;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'tool') {\n for (const part of msg.content) {\n if (part.type === 'tool-result') {\n const text = stringifyToolOutput(part.output);\n messages.push({\n role: 'tool',\n content: text,\n tool_call_id: part.toolCallId,\n _originalContent: [part],\n _originalText: text,\n _toolName: part.toolName,\n });\n }\n }\n }\n }\n\n return messages;\n}\n\n/**\n * Converts context-chef IR messages back to AI SDK V3 prompt format.\n *\n * Uses `_originalContent` when content is unmodified (detected via `_originalText`).\n * Falls back to constructing from IR fields when content was modified by Janitor\n * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).\n */\nexport function toAISDK(messages: Message[]): LanguageModelV3Prompt {\n const prompt: LanguageModelV3Prompt = [];\n\n let i = 0;\n while (i < messages.length) {\n const msg = messages[i];\n const providerOptions = msg._providerOptions as SharedV3ProviderOptions | undefined;\n const contentModified = msg._originalText !== undefined && msg._originalText !== msg.content;\n\n if (msg.role === 'system') {\n prompt.push({\n role: 'system',\n content: msg.content,\n ...(providerOptions ? { providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'user') {\n const content =\n !contentModified && Array.isArray(msg._originalContent)\n ? (msg._originalContent as any)\n : [{ type: 'text' as const, text: msg.content }];\n prompt.push({\n role: 'user',\n content,\n ...(providerOptions ? { providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'assistant') {\n const content =\n !contentModified && Array.isArray(msg._originalContent)\n ? (msg._originalContent as any)\n : [{ type: 'text' as const, text: msg.content }];\n prompt.push({\n role: 'assistant',\n content,\n ...(providerOptions ? { providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'tool') {\n const toolResults: LanguageModelV3ToolResultPart[] = [];\n while (i < messages.length && messages[i].role === 'tool') {\n const toolMsg = messages[i];\n const toolModified =\n toolMsg._originalText !== undefined && toolMsg._originalText !== toolMsg.content;\n\n if (!toolModified && toolMsg._originalContent) {\n toolResults.push(...(toolMsg._originalContent as LanguageModelV3ToolResultPart[]));\n } else {\n toolResults.push({\n type: 'tool-result',\n toolCallId: toolMsg.tool_call_id ?? '',\n toolName: (toolMsg._toolName as string) ?? 'unknown',\n output: { type: 'text', value: toolMsg.content },\n });\n }\n i++;\n }\n prompt.push({ role: 'tool', content: toolResults });\n continue;\n }\n\n i++;\n }\n\n return prompt;\n}\n\nfunction stringifyToolOutput(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v: { type: string; text?: string }) => (v.type === 'text' ? (v.text ?? '') : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return JSON.stringify(output);\n }\n}\n","import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n} from '@ai-sdk/provider';\nimport { Offloader } from '@context-chef/core';\nimport type { TruncateOptions } from './types';\n\n/**\n * Truncates tool-result content within an AI SDK prompt when it exceeds the configured threshold.\n * When a storage adapter is provided, original content is persisted and a URI is included in the output.\n */\nexport async function truncateToolResults(\n prompt: LanguageModelV3Prompt,\n options: TruncateOptions,\n): Promise<LanguageModelV3Prompt> {\n const { threshold, headChars = 0, tailChars = 1000, storage } = options;\n\n const offloader = storage\n ? new Offloader({ threshold, adapter: storage, storageDir: '' })\n : null;\n\n const result: LanguageModelV3Prompt = [];\n\n for (const msg of prompt) {\n if (msg.role !== 'tool') {\n result.push(msg);\n continue;\n }\n\n const newContent: typeof msg.content = [];\n\n for (const part of msg.content) {\n if (part.type !== 'tool-result') {\n newContent.push(part);\n continue;\n }\n\n const text = extractText(part.output);\n if (text.length <= threshold || headChars + tailChars >= text.length) {\n newContent.push(part);\n continue;\n }\n\n // With storage: use Offloader to persist original and get a URI-annotated truncation\n if (offloader) {\n try {\n const vfsResult = await offloader.offloadAsync(text, { threshold, headChars, tailChars });\n newContent.push({\n ...part,\n output: { type: 'text', value: vfsResult.content } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n continue;\n } catch (error) {\n console.warn(\n `[context-chef] Storage adapter write failed for tool result (${part.toolCallId}). ` +\n `Falling back to simple truncation. Error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Fall through to simple truncation below\n }\n }\n\n // Without storage: simple truncation, original is discarded\n const head = text.slice(0, headChars);\n const tail = text.slice(text.length - tailChars);\n const totalLines = text.split('\\n').length;\n\n const truncated = [\n head,\n `\\n--- truncated (${totalLines} lines, ${text.length} chars total) ---\\n`,\n tail,\n ]\n .filter(Boolean)\n .join('')\n .trim();\n\n newContent.push({\n ...part,\n output: { type: 'text', value: truncated } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n }\n\n result.push({ ...msg, content: newContent });\n }\n\n return result;\n}\n\nfunction extractText(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v: { type: string; text?: string }) => (v.type === 'text' ? (v.text ?? '') : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return '';\n }\n}\n","import type { LanguageModelV3, LanguageModelV3StreamPart } from '@ai-sdk/provider';\nimport { generateText, type LanguageModelMiddleware } from 'ai';\nimport { Janitor, type Message } from '@context-chef/core';\n\nimport { fromAISDK, toAISDK } from './adapter';\nimport { truncateToolResults } from './truncator';\nimport type { ContextChefOptions } from './types';\n\n/**\n * Creates a LanguageModelMiddleware that transparently applies\n * context-chef compression and truncation to AI SDK model calls.\n *\n * The middleware holds a stateful Janitor instance that tracks\n * token usage across calls for compression decisions.\n */\nexport function createMiddleware(options: ContextChefOptions): LanguageModelMiddleware {\n let usageWarned = false;\n\n const janitor = new Janitor({\n contextWindow: options.contextWindow,\n tokenizer: options.tokenizer ? (msgs: Message[]) => options.tokenizer?.(msgs) ?? 0 : undefined,\n preserveRatio: options.compress?.preserveRatio ?? 0.8,\n compressionModel: options.compress?.model\n ? createCompressionAdapter(options.compress.model)\n : undefined,\n onCompress: options.onCompress\n ? (summary, count) => options.onCompress?.(summary.content, count)\n : undefined,\n });\n\n return {\n specificationVersion: 'v3',\n\n transformParams: async ({ params }) => {\n let { prompt } = params;\n\n // 1. Truncate large tool results\n if (options.truncate) {\n prompt = await truncateToolResults(prompt, options.truncate);\n }\n\n // 2. Compress history if over token budget\n const irMessages = fromAISDK(prompt);\n const compressed = await janitor.compress(irMessages);\n\n // Only convert back if compression actually changed something\n if (compressed !== irMessages) {\n prompt = toAISDK(compressed);\n }\n\n return { ...params, prompt };\n },\n\n wrapGenerate: async ({ doGenerate }) => {\n const result = await doGenerate();\n\n if (result.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(result.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Model response did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n\n return result;\n },\n\n wrapStream: async ({ doStream }) => {\n const { stream, ...rest } = await doStream();\n\n const transform = new TransformStream<LanguageModelV3StreamPart, LanguageModelV3StreamPart>({\n transform(chunk, controller) {\n if (chunk.type === 'finish') {\n if (chunk.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(chunk.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Stream finish did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n }\n controller.enqueue(chunk);\n },\n });\n\n return { ...rest, stream: stream.pipeThrough(transform) };\n },\n };\n}\n\n/**\n * Adapts an AI SDK LanguageModelV3 into the compressionModel callback\n * that Janitor expects: (messages: Message[]) => Promise<string>\n *\n * Tool messages are converted to user messages describing the tool interaction,\n * since generateText only accepts system/user/assistant roles.\n */\nfunction createCompressionAdapter(model: LanguageModelV3): (messages: Message[]) => Promise<string> {\n return async (messages: Message[]): Promise<string> => {\n const formatted = messages.map((m) => {\n if (m.role === 'tool') {\n return {\n role: 'user' as const,\n content: `[Tool result${m.tool_call_id ? ` (${m.tool_call_id})` : ''}: ${m.content}]`,\n };\n }\n // assistant messages with tool_calls: include tool call info in content\n if (m.role === 'assistant' && m.tool_calls?.length) {\n const toolCallsDesc = m.tool_calls\n .map((tc) => `[Called tool: ${tc.function.name}(${tc.function.arguments})]`)\n .join('\\n');\n return {\n role: 'assistant' as const,\n content: m.content ? `${m.content}\\n${toolCallsDesc}` : toolCallsDesc,\n };\n }\n return {\n role: m.role as 'system' | 'user' | 'assistant',\n content: m.content,\n };\n });\n\n const { text } = await generateText({\n model,\n messages: formatted,\n maxOutputTokens: 2048,\n });\n\n return text || '[Compression produced no output]';\n };\n}\n","import type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { wrapLanguageModel } from 'ai';\n\nimport { createMiddleware } from './middleware';\nimport type { ContextChefOptions } from './types';\n\nexport { fromAISDK, toAISDK } from './adapter';\nexport { createMiddleware } from './middleware';\nexport type { CompressOptions, ContextChefOptions, TruncateOptions } from './types';\n\n/**\n * Wraps an AI SDK language model with context-chef middleware for\n * transparent history compression, tool result truncation, and token budget management.\n *\n * @example\n * ```typescript\n * import { withContextChef } from '@context-chef/ai-sdk-middleware';\n * import { openai } from '@ai-sdk/openai';\n * import { generateText } from 'ai';\n *\n * const model = withContextChef(openai('gpt-4o'), {\n * contextWindow: 128_000,\n * compress: { model: openai('gpt-4o-mini') },\n * truncate: { threshold: 5000, headChars: 500, tailChars: 1000 },\n * });\n *\n * // Use exactly like normal — zero other code changes\n * const result = await generateText({ model, messages, tools });\n * ```\n */\nexport function withContextChef(model: LanguageModelV3, options: ContextChefOptions): LanguageModelV3 {\n const middleware = createMiddleware(options);\n return wrapLanguageModel({ model, middleware });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/adapter.ts","../src/truncator.ts","../src/middleware.ts","../src/index.ts"],"sourcesContent":["import type {\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n SharedV3ProviderOptions,\n} from '@ai-sdk/provider';\nimport type { Message, ToolCall } from '@context-chef/core';\n\n/** Content types for each AI SDK message role */\ntype UserContent = Extract<LanguageModelV3Message, { role: 'user' }>['content'];\ntype AssistantContent = Extract<LanguageModelV3Message, { role: 'assistant' }>['content'];\ntype ToolContent = Extract<LanguageModelV3Message, { role: 'tool' }>['content'];\n\n/**\n * Extended IR message with typed pass-through fields for lossless AI SDK round-trip.\n * Per-role content fields avoid union types, so no `as` casts are needed in `toAISDK`.\n */\nexport interface AISDKMessage extends Message {\n _userContent?: UserContent;\n _assistantContent?: AssistantContent;\n _toolContent?: ToolContent;\n _originalText?: string;\n _providerOptions?: SharedV3ProviderOptions;\n _toolName?: string;\n}\n\n/**\n * Converts an AI SDK V3 prompt to context-chef IR messages.\n *\n * Original AI SDK content is stored in per-role fields for lossless round-trip.\n * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.\n * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).\n */\nexport function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[] {\n const messages: AISDKMessage[] = [];\n\n for (const msg of prompt) {\n if (msg.role === 'system') {\n messages.push({\n role: 'system',\n content: msg.content,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'user') {\n const text = msg.content\n .filter((p) => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n messages.push({\n role: 'user',\n content: text,\n _userContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\n let thinking: { thinking: string } | undefined;\n\n for (const part of msg.content) {\n if (part.type === 'text') text.push(part.text);\n else if (part.type === 'tool-call') {\n toolCalls.push({\n id: part.toolCallId,\n type: 'function',\n function: {\n name: part.toolName,\n arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),\n },\n });\n } else if (part.type === 'reasoning') {\n thinking = { thinking: part.text };\n }\n }\n\n const joinedText = text.join('\\n');\n const m: AISDKMessage = {\n role: 'assistant',\n content: joinedText,\n _assistantContent: msg.content,\n _originalText: joinedText,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (toolCalls.length > 0) m.tool_calls = toolCalls;\n if (thinking) m.thinking = thinking;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'tool') {\n for (const part of msg.content) {\n if (part.type === 'tool-result') {\n const text = stringifyToolOutput(part.output);\n messages.push({\n role: 'tool',\n content: text,\n tool_call_id: part.toolCallId,\n _toolContent: [part],\n _originalText: text,\n _toolName: part.toolName,\n });\n }\n }\n }\n }\n\n return messages;\n}\n\n/**\n * Narrows a generic Message to AISDKMessage for typed access to pass-through fields.\n */\nfunction asAISDK(msg: Message): AISDKMessage {\n return msg;\n}\n\n/**\n * Converts context-chef IR messages back to AI SDK V3 prompt format.\n *\n * Uses per-role original content when unmodified (detected via `_originalText`).\n * Falls back to constructing from IR fields when content was modified by Janitor\n * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).\n */\nexport function toAISDK(messages: Message[]): LanguageModelV3Prompt {\n const prompt: LanguageModelV3Prompt = [];\n\n let i = 0;\n while (i < messages.length) {\n const msg = asAISDK(messages[i]);\n const contentModified = msg._originalText !== undefined && msg._originalText !== msg.content;\n\n if (msg.role === 'system') {\n prompt.push({\n role: 'system',\n content: msg.content,\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'user') {\n prompt.push({\n role: 'user',\n content:\n !contentModified && msg._userContent\n ? msg._userContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'assistant') {\n prompt.push({\n role: 'assistant',\n content:\n !contentModified && msg._assistantContent\n ? msg._assistantContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'tool') {\n const toolResults: LanguageModelV3ToolResultPart[] = [];\n while (i < messages.length && messages[i].role === 'tool') {\n const toolMsg = asAISDK(messages[i]);\n const toolModified =\n toolMsg._originalText !== undefined && toolMsg._originalText !== toolMsg.content;\n\n if (!toolModified && toolMsg._toolContent) {\n for (const part of toolMsg._toolContent) {\n if (part.type === 'tool-result') {\n toolResults.push(part);\n }\n }\n } else {\n toolResults.push({\n type: 'tool-result',\n toolCallId: toolMsg.tool_call_id ?? '',\n toolName: toolMsg._toolName ?? 'unknown',\n output: { type: 'text', value: toolMsg.content },\n });\n }\n i++;\n }\n prompt.push({ role: 'tool', content: toolResults });\n continue;\n }\n\n i++;\n }\n\n return prompt;\n}\n\nfunction stringifyToolOutput(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v) => (v.type === 'text' ? v.text : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return JSON.stringify(output);\n }\n}\n","import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n} from '@ai-sdk/provider';\nimport { Offloader } from '@context-chef/core';\nimport type { TruncateOptions } from './types';\n\n/**\n * Truncates tool-result content within an AI SDK prompt when it exceeds the configured threshold.\n * When a storage adapter is provided, original content is persisted and a URI is included in the output.\n */\nexport async function truncateToolResults(\n prompt: LanguageModelV3Prompt,\n options: TruncateOptions,\n): Promise<LanguageModelV3Prompt> {\n const { threshold, headChars = 0, tailChars = 1000, storage } = options;\n\n const offloader = storage ? new Offloader({ threshold, adapter: storage, storageDir: '' }) : null;\n\n const result: LanguageModelV3Prompt = [];\n\n for (const msg of prompt) {\n if (msg.role !== 'tool') {\n result.push(msg);\n continue;\n }\n\n const newContent: typeof msg.content = [];\n\n for (const part of msg.content) {\n if (part.type !== 'tool-result') {\n newContent.push(part);\n continue;\n }\n\n const text = extractText(part.output);\n if (text.length <= threshold || headChars + tailChars >= text.length) {\n newContent.push(part);\n continue;\n }\n\n // With storage: use Offloader to persist original and get a URI-annotated truncation\n if (offloader) {\n try {\n const vfsResult = await offloader.offloadAsync(text, { threshold, headChars, tailChars });\n newContent.push({\n ...part,\n output: {\n type: 'text',\n value: vfsResult.content,\n } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n continue;\n } catch (error) {\n console.warn(\n `[context-chef] Storage adapter write failed for tool result (${part.toolCallId}). ` +\n `Falling back to simple truncation. Error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Fall through to simple truncation below\n }\n }\n\n // Without storage: simple truncation, original is discarded\n const head = text.slice(0, headChars);\n const tail = text.slice(text.length - tailChars);\n const totalLines = text.split('\\n').length;\n\n const truncated = [\n head,\n `\\n--- truncated (${totalLines} lines, ${text.length} chars total) ---\\n`,\n tail,\n ]\n .filter(Boolean)\n .join('')\n .trim();\n\n newContent.push({\n ...part,\n output: { type: 'text', value: truncated } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n }\n\n result.push({ ...msg, content: newContent });\n }\n\n return result;\n}\n\nfunction extractText(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v: { type: string; text?: string }) => (v.type === 'text' ? (v.text ?? '') : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return '';\n }\n}\n","import type { LanguageModelV3, LanguageModelV3StreamPart } from '@ai-sdk/provider';\nimport { Janitor, type Message } from '@context-chef/core';\nimport { generateText, type LanguageModelMiddleware } from 'ai';\n\nimport { fromAISDK, toAISDK } from './adapter';\nimport { truncateToolResults } from './truncator';\nimport type { ContextChefOptions } from './types';\n\ntype CompressRole = 'system' | 'user' | 'assistant';\n\n/**\n * Creates a LanguageModelMiddleware that transparently applies\n * context-chef compression and truncation to AI SDK model calls.\n *\n * The middleware holds a stateful Janitor instance that tracks\n * token usage across calls for compression decisions.\n */\nexport function createMiddleware(options: ContextChefOptions): LanguageModelMiddleware {\n let usageWarned = false;\n\n const janitor = new Janitor({\n contextWindow: options.contextWindow,\n tokenizer: options.tokenizer ? (msgs: Message[]) => options.tokenizer?.(msgs) ?? 0 : undefined,\n preserveRatio: options.compress?.preserveRatio ?? 0.8,\n compressionModel: options.compress?.model\n ? createCompressionAdapter(options.compress.model)\n : undefined,\n onCompress: options.onCompress\n ? (summary, count) => options.onCompress?.(summary.content, count)\n : undefined,\n });\n\n return {\n specificationVersion: 'v3',\n\n transformParams: async ({ params }) => {\n let { prompt } = params;\n\n // 1. Truncate large tool results\n if (options.truncate) {\n prompt = await truncateToolResults(prompt, options.truncate);\n }\n\n // 2. Compress history if over token budget\n const irMessages = fromAISDK(prompt);\n const compressed = await janitor.compress(irMessages);\n\n // Only convert back if compression actually changed something\n if (compressed !== irMessages) {\n prompt = toAISDK(compressed);\n }\n\n return { ...params, prompt };\n },\n\n wrapGenerate: async ({ doGenerate }) => {\n const result = await doGenerate();\n\n if (result.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(result.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Model response did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n\n return result;\n },\n\n wrapStream: async ({ doStream }) => {\n const { stream, ...rest } = await doStream();\n\n const transform = new TransformStream<LanguageModelV3StreamPart, LanguageModelV3StreamPart>({\n transform(chunk, controller) {\n if (chunk.type === 'finish') {\n if (chunk.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(chunk.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Stream finish did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n }\n controller.enqueue(chunk);\n },\n });\n\n return { ...rest, stream: stream.pipeThrough(transform) };\n },\n };\n}\n\n/**\n * Maps an IR role to a role accepted by generateText.\n * Tool messages are handled separately before this is called.\n */\nfunction toCompressRole(role: string): CompressRole {\n if (role === 'system' || role === 'user' || role === 'assistant') return role;\n return 'user';\n}\n\n/**\n * Adapts an AI SDK LanguageModelV3 into the compressionModel callback\n * that Janitor expects: (messages: Message[]) => Promise<string>\n *\n * Tool messages are converted to user messages describing the tool interaction,\n * since generateText only accepts system/user/assistant roles.\n */\nfunction createCompressionAdapter(\n model: LanguageModelV3,\n): (messages: Message[]) => Promise<string> {\n return async (messages: Message[]): Promise<string> => {\n const formatted = messages.map((m): { role: CompressRole; content: string } => {\n if (m.role === 'tool') {\n return {\n role: 'user' satisfies CompressRole,\n content: `[Tool result${m.tool_call_id ? ` (${m.tool_call_id})` : ''}: ${m.content}]`,\n };\n }\n if (m.role === 'assistant' && m.tool_calls?.length) {\n const toolCallsDesc = m.tool_calls\n .map((tc) => `[Called tool: ${tc.function.name}(${tc.function.arguments})]`)\n .join('\\n');\n return {\n role: 'assistant' satisfies CompressRole,\n content: m.content ? `${m.content}\\n${toolCallsDesc}` : toolCallsDesc,\n };\n }\n return {\n role: toCompressRole(m.role),\n content: m.content,\n };\n });\n\n const { text } = await generateText({\n model,\n messages: formatted,\n maxOutputTokens: 2048,\n });\n\n return text || '[Compression produced no output]';\n };\n}\n","import type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { wrapLanguageModel } from 'ai';\n\nimport { createMiddleware } from './middleware';\nimport type { ContextChefOptions } from './types';\n\nexport { type AISDKMessage, fromAISDK, toAISDK } from './adapter';\nexport { createMiddleware } from './middleware';\nexport type { CompressOptions, ContextChefOptions, TruncateOptions } from './types';\n\n/**\n * Wraps an AI SDK language model with context-chef middleware for\n * transparent history compression, tool result truncation, and token budget management.\n *\n * @example\n * ```typescript\n * import { withContextChef } from '@context-chef/ai-sdk-middleware';\n * import { openai } from '@ai-sdk/openai';\n * import { generateText } from 'ai';\n *\n * const model = withContextChef(openai('gpt-4o'), {\n * contextWindow: 128_000,\n * compress: { model: openai('gpt-4o-mini') },\n * truncate: { threshold: 5000, headChars: 500, tailChars: 1000 },\n * });\n *\n * // Use exactly like normal — zero other code changes\n * const result = await generateText({ model, messages, tools });\n * ```\n */\nexport function withContextChef(\n model: LanguageModelV3,\n options: ContextChefOptions,\n): LanguageModelV3 {\n const middleware = createMiddleware(options);\n return wrapLanguageModel({ model, middleware });\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-chef/ai-sdk-middleware",
3
- "version": "0.1.1",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -15,11 +15,6 @@
15
15
  "files": [
16
16
  "dist"
17
17
  ],
18
- "scripts": {
19
- "build": "tsdown",
20
- "test": "vitest run",
21
- "typecheck": "tsc -p tsconfig.build.json --noEmit"
22
- },
23
18
  "keywords": [
24
19
  "ai-sdk",
25
20
  "vercel-ai",
@@ -43,7 +38,7 @@
43
38
  "url": "https://github.com/MyPrototypeWhat/context-chef/issues"
44
39
  },
45
40
  "dependencies": {
46
- "@context-chef/core": "workspace:*"
41
+ "@context-chef/core": "2.1.2"
47
42
  },
48
43
  "peerDependencies": {
49
44
  "@ai-sdk/provider": ">=3",
@@ -56,5 +51,10 @@
56
51
  "tsdown": "^0.20.3",
57
52
  "typescript": "^5.9.3",
58
53
  "vitest": "^4.0.18"
54
+ },
55
+ "scripts": {
56
+ "build": "tsdown",
57
+ "test": "vitest run",
58
+ "typecheck": "tsc --noEmit"
59
59
  }
60
- }
60
+ }