the-token-company 0.2.5 → 0.3.1

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/dist/anthropic.js CHANGED
@@ -1,6 +1,67 @@
1
1
  import { TheTokenCompany } from "./client.js";
2
2
  import { StatsTTC, compressAnthropicMessages, resolveAggressiveness } from "./compress.js";
3
3
  import { CompressionStats } from "./types.js";
4
+ const TTC_SEARCH_TOOL = {
5
+ name: "ttc_web_search",
6
+ description: "Search the web for current information. Use this when you need up-to-date facts, prices, news, or any information that may have changed after your training cutoff.",
7
+ input_schema: {
8
+ type: "object",
9
+ properties: {
10
+ query: {
11
+ type: "string",
12
+ description: "The search query"
13
+ }
14
+ },
15
+ required: ["query"]
16
+ }
17
+ };
18
+ function injectSearchTool(params) {
19
+ let tools = params.tools ? [...params.tools] : [];
20
+ tools = tools.filter((t) => !String(t.type ?? "").startsWith("web_search_"));
21
+ if (!tools.some((t) => t.name === "ttc_web_search")) {
22
+ tools.push(TTC_SEARCH_TOOL);
23
+ }
24
+ return { ...params, tools };
25
+ }
26
+ function hasSearchToolUse(response) {
27
+ if (response.stop_reason !== "tool_use")
28
+ return false;
29
+ return response.content?.some((b) => b.type === "tool_use" && b.name === "ttc_web_search");
30
+ }
31
+ function formatSearchResults(results) {
32
+ return results.map(r => `Source: ${r.title}\nURL: ${r.url}\n${r.content}`).join("\n\n");
33
+ }
34
+ async function handleSearchLoop(response, params, originalCreate, ttcClient, rest) {
35
+ while (hasSearchToolUse(response)) {
36
+ const messages = [...(params.messages || [])];
37
+ // Add assistant response
38
+ const assistantContent = response.content.map((b) => {
39
+ if (typeof b.toJSON === 'function')
40
+ return b.toJSON();
41
+ return b;
42
+ });
43
+ messages.push({ role: "assistant", content: assistantContent });
44
+ // Build tool results
45
+ const toolResults = [];
46
+ for (const block of response.content) {
47
+ if (block.type === "tool_use" && block.name === "ttc_web_search") {
48
+ const query = block.input?.query || "";
49
+ const searchResult = await ttcClient.search(query);
50
+ toolResults.push({
51
+ type: "tool_result",
52
+ tool_use_id: block.id,
53
+ content: formatSearchResults(searchResult.results),
54
+ });
55
+ }
56
+ }
57
+ if (toolResults.length > 0) {
58
+ messages.push({ role: "user", content: toolResults });
59
+ }
60
+ const newParams = { ...params, messages };
61
+ response = await originalCreate(newParams, ...rest);
62
+ }
63
+ return response;
64
+ }
4
65
  /**
5
66
  * Wrap an Anthropic client to auto-compress non-assistant messages.
6
67
  *
@@ -24,25 +85,43 @@ import { CompressionStats } from "./types.js";
24
85
  */
25
86
  export function withCompression(client, options) {
26
87
  const stats = new CompressionStats();
27
- const compressor = new StatsTTC(new TheTokenCompany({ apiKey: options.compressionApiKey, appId: options.appId, fetch: options.fetch }), stats);
88
+ const ttcClient = new TheTokenCompany({ apiKey: options.compressionApiKey, baseUrl: options.baseUrl, appId: options.appId, fetch: options.fetch });
89
+ const compressor = new StatsTTC(ttcClient, stats);
28
90
  const model = options.model ?? "bear-2";
29
91
  const roleAggr = resolveAggressiveness(options.aggressiveness ?? 0.2);
92
+ if (options.compressAssistant && !("assistant" in roleAggr)) {
93
+ roleAggr["assistant"] = roleAggr["user"] ?? 0.2;
94
+ }
30
95
  const systemAggr = roleAggr["system"];
96
+ const stripServerToolResults = options.stripServerToolResults ?? false;
97
+ const webSearch = options.webSearch ?? false;
31
98
  const originalCreate = client.messages.create.bind(client.messages);
32
99
  client.messages.create = async function (params, ...rest) {
33
100
  stats._startTurn();
34
101
  if (params?.messages) {
35
102
  params = {
36
103
  ...params,
37
- messages: await compressAnthropicMessages(compressor, params.messages, model, roleAggr),
104
+ messages: await compressAnthropicMessages(compressor, params.messages, model, roleAggr, {
105
+ stripServerToolResults,
106
+ skipToolName: webSearch ? "ttc_web_search" : undefined,
107
+ }),
38
108
  };
39
109
  }
40
110
  if (systemAggr != null && typeof params?.system === "string" && params.system.trim()) {
41
111
  const result = await compressor.compress(params.system, { model, aggressiveness: systemAggr });
42
112
  params = { ...params, system: result.output };
43
113
  }
114
+ // Inject search tool
115
+ if (webSearch) {
116
+ params = injectSearchTool(params);
117
+ }
44
118
  stats._endTurn();
45
- return originalCreate(params, ...rest);
119
+ let response = await originalCreate(params, ...rest);
120
+ // Handle search tool loop
121
+ if (webSearch) {
122
+ response = await handleSearchLoop(response, params, originalCreate, ttcClient, rest);
123
+ }
124
+ return response;
46
125
  };
47
126
  client.compression = stats;
48
127
  return client;
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CompressResult, TheTokenCompanyOptions } from "./types.js";
1
+ import type { CompressResult, SearchRequestOptions, SearchResult, TheTokenCompanyOptions } from "./types.js";
2
2
  export declare const BEAR_1 = "bear-1";
3
3
  export declare const BEAR_1_1 = "bear-1.1";
4
4
  export declare const BEAR_1_2 = "bear-1.2";
@@ -16,5 +16,6 @@ export declare class TheTokenCompany {
16
16
  aggressiveness?: number;
17
17
  appId?: string;
18
18
  }): Promise<CompressResult>;
19
+ search(query: string, options?: SearchRequestOptions): Promise<SearchResult>;
19
20
  private parseError;
20
21
  }
package/dist/client.js CHANGED
@@ -69,6 +69,50 @@ export class TheTokenCompany {
69
69
  compressionRatio: data.output_tokens === 0 ? 0 : data.original_input_tokens / data.output_tokens,
70
70
  };
71
71
  }
72
+ async search(query, options = {}) {
73
+ const { maxResults = 5, searchDepth = "basic", includeRawContent = false, model = "bear-2", aggressiveness = 0.3, } = options;
74
+ const resolvedAppId = options.appId ?? this.appId;
75
+ const payload = {
76
+ query,
77
+ max_results: maxResults,
78
+ search_depth: searchDepth,
79
+ include_raw_content: includeRawContent,
80
+ model,
81
+ compression_settings: { aggressiveness },
82
+ ...(resolvedAppId !== undefined && { app_id: resolvedAppId }),
83
+ };
84
+ const jsonBody = JSON.stringify(payload);
85
+ const headers = {
86
+ Authorization: `Bearer ${this.apiKey}`,
87
+ "Content-Type": "application/json",
88
+ };
89
+ let body;
90
+ if (this.gzip) {
91
+ headers["Content-Encoding"] = "gzip";
92
+ body = new Uint8Array(await gzipAsync(jsonBody));
93
+ }
94
+ else {
95
+ body = jsonBody;
96
+ }
97
+ const response = await this.fetchFn(`${this.baseUrl}/v1/search`, {
98
+ method: "POST",
99
+ headers,
100
+ body: body,
101
+ signal: AbortSignal.timeout(this.timeout),
102
+ });
103
+ if (!response.ok) {
104
+ throw await this.parseError(response);
105
+ }
106
+ const data = (await response.json());
107
+ return {
108
+ results: data.results,
109
+ query: data.query,
110
+ searchTime: data.search_time,
111
+ originalInputTokens: data.original_input_tokens,
112
+ outputTokens: data.output_tokens,
113
+ tokensSaved: data.original_input_tokens - data.output_tokens,
114
+ };
115
+ }
72
116
  async parseError(response) {
73
117
  let msg;
74
118
  try {
@@ -30,6 +30,7 @@ export declare function compressOpenAIMessages(ttc: Compressor, messages: OpenAI
30
30
  interface AnthropicBlock {
31
31
  type: string;
32
32
  text?: string;
33
+ tool_use_id?: string;
33
34
  content?: string | Array<{
34
35
  type: string;
35
36
  text?: string;
@@ -42,7 +43,10 @@ interface AnthropicMessage {
42
43
  content?: string | AnthropicBlock[] | null;
43
44
  [key: string]: unknown;
44
45
  }
45
- export declare function compressAnthropicMessages(ttc: Compressor, messages: AnthropicMessage[], model: string, roleAggr: Record<string, number>): Promise<AnthropicMessage[]>;
46
+ export declare function compressAnthropicMessages(ttc: Compressor, messages: AnthropicMessage[], model: string, roleAggr: Record<string, number>, options?: {
47
+ stripServerToolResults?: boolean;
48
+ skipToolName?: string;
49
+ }): Promise<AnthropicMessage[]>;
46
50
  interface AISDKTextPart {
47
51
  type: "text";
48
52
  text: string;
package/dist/compress.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const DEFAULT_ROLES = ["user", "system", "tool"];
2
+ const SERVER_TOOL_BLOCK_TYPES = new Set(["web_search_tool_result", "server_tool_use"]);
2
3
  export class StatsTTC {
3
4
  inner;
4
5
  stats;
@@ -46,8 +47,37 @@ export async function compressOpenAIMessages(ttc, messages, model, roleAggr) {
46
47
  return msg;
47
48
  }));
48
49
  }
49
- export async function compressAnthropicMessages(ttc, messages, model, roleAggr) {
50
+ function collectToolUseIds(messages, toolName) {
51
+ const ids = new Set();
52
+ for (const msg of messages) {
53
+ if (msg.role !== "assistant" || !Array.isArray(msg.content))
54
+ continue;
55
+ for (const block of msg.content) {
56
+ if (block.type === "tool_use" && block.name === toolName && block.id) {
57
+ ids.add(block.id);
58
+ }
59
+ }
60
+ }
61
+ return ids;
62
+ }
63
+ export async function compressAnthropicMessages(ttc, messages, model, roleAggr, options) {
64
+ const stripServerToolResults = options?.stripServerToolResults ?? false;
65
+ const skipIds = options?.skipToolName ? collectToolUseIds(messages, options.skipToolName) : undefined;
50
66
  return Promise.all(messages.map(async (msg) => {
67
+ if (msg.role === "assistant") {
68
+ const assistantAggr = roleAggr["assistant"];
69
+ if (assistantAggr == null && !stripServerToolResults)
70
+ return msg;
71
+ if (typeof msg.content === "string" && assistantAggr != null && msg.content.trim()) {
72
+ const result = await ttc.compress(msg.content, { model, aggressiveness: assistantAggr });
73
+ return { ...msg, content: result.output };
74
+ }
75
+ if (Array.isArray(msg.content)) {
76
+ const blocks = await compressAssistantBlocks(ttc, msg.content, model, assistantAggr, stripServerToolResults);
77
+ return { ...msg, content: blocks };
78
+ }
79
+ return msg;
80
+ }
51
81
  if (msg.role !== "user")
52
82
  return msg;
53
83
  const userAggr = roleAggr["user"];
@@ -59,19 +89,35 @@ export async function compressAnthropicMessages(ttc, messages, model, roleAggr)
59
89
  return { ...msg, content: result.output };
60
90
  }
61
91
  if (Array.isArray(msg.content)) {
62
- const blocks = await compressAnthropicBlocks(ttc, msg.content, model, userAggr, toolAggr);
92
+ const blocks = await compressAnthropicBlocks(ttc, msg.content, model, userAggr, toolAggr, skipIds);
63
93
  return { ...msg, content: blocks };
64
94
  }
65
95
  return msg;
66
96
  }));
67
97
  }
68
- async function compressAnthropicBlocks(ttc, blocks, model, userAggr, toolAggr) {
98
+ async function compressAssistantBlocks(ttc, blocks, model, assistantAggr, stripServerToolResults) {
99
+ const results = await Promise.all(blocks.map(async (block) => {
100
+ if (stripServerToolResults && SERVER_TOOL_BLOCK_TYPES.has(block.type)) {
101
+ return null;
102
+ }
103
+ if (block.type === "text" && assistantAggr != null && typeof block.text === "string" && block.text.trim()) {
104
+ const result = await ttc.compress(block.text, { model, aggressiveness: assistantAggr });
105
+ return { ...block, text: result.output };
106
+ }
107
+ return block;
108
+ }));
109
+ const filtered = results.filter((b) => b != null);
110
+ return filtered.length > 0 ? filtered : blocks;
111
+ }
112
+ async function compressAnthropicBlocks(ttc, blocks, model, userAggr, toolAggr, skipIds) {
69
113
  return Promise.all(blocks.map(async (block) => {
70
114
  if (block.type === "text" && userAggr != null && typeof block.text === "string" && block.text.trim()) {
71
115
  const result = await ttc.compress(block.text, { model, aggressiveness: userAggr });
72
116
  return { ...block, text: result.output };
73
117
  }
74
118
  if (block.type === "tool_result" && toolAggr != null) {
119
+ if (block.tool_use_id && skipIds?.has(block.tool_use_id))
120
+ return block;
75
121
  return compressToolResult(ttc, block, model, toolAggr);
76
122
  }
77
123
  return block;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { TheTokenCompany, BEAR_1, BEAR_1_1, BEAR_1_2, BEAR_2 } from "./client.js";
2
2
  export { TheTokenCompanyError, AuthenticationError, InvalidRequestError, PaymentRequiredError, RequestTooLargeError, RateLimitError, APIError, } from "./errors.js";
3
3
  export { CompressionStats } from "./types.js";
4
- export type { CompressResult, TheTokenCompanyOptions, Aggressiveness, WithCompressionOptions, TurnStats, } from "./types.js";
4
+ export type { CompressResult, TheTokenCompanyOptions, Aggressiveness, WithCompressionOptions, TurnStats, SearchRequestOptions, SearchResultItem, SearchResult, } from "./types.js";
5
5
  /**
6
6
  * Wrap text in `<ttc_safe>` tags to protect it from compression.
7
7
  */
package/dist/types.d.ts CHANGED
@@ -31,9 +31,38 @@ export interface WithCompressionOptions {
31
31
  compressionApiKey: string;
32
32
  model?: string;
33
33
  aggressiveness?: Aggressiveness;
34
+ /** Compress text blocks in assistant messages (multi-turn web search optimization). */
35
+ compressAssistant?: boolean;
36
+ /** Strip server-side tool result blocks (e.g. web_search_tool_result) from assistant messages. Disables citations in subsequent turns. */
37
+ stripServerToolResults?: boolean;
38
+ /** Replace Anthropic's server-side web search with TTC's compressible search. */
39
+ webSearch?: boolean;
40
+ baseUrl?: string;
34
41
  appId?: string;
35
42
  fetch?: typeof globalThis.fetch;
36
43
  }
44
+ export interface SearchRequestOptions {
45
+ maxResults?: number;
46
+ searchDepth?: "basic" | "advanced";
47
+ includeRawContent?: boolean;
48
+ model?: string;
49
+ aggressiveness?: number;
50
+ appId?: string;
51
+ }
52
+ export interface SearchResultItem {
53
+ url: string;
54
+ title: string;
55
+ content: string;
56
+ score?: number;
57
+ }
58
+ export interface SearchResult {
59
+ results: SearchResultItem[];
60
+ query: string;
61
+ searchTime: number;
62
+ originalInputTokens: number;
63
+ outputTokens: number;
64
+ tokensSaved: number;
65
+ }
37
66
  export interface TurnStats {
38
67
  inputTokens: number;
39
68
  outputTokens: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "the-token-company",
3
- "version": "0.2.5",
3
+ "version": "0.3.1",
4
4
  "description": "Node.js SDK for The Token Company — compress LLM prompts to reduce costs and latency",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",