tinker-agent 1.11.0 → 2.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.
@@ -8,7 +8,6 @@ import {
8
8
  IMAGE_INPUT_POLICY,
9
9
  IMAGE_INPUT_POLICY_VERSION,
10
10
  } from "../image/image-input-policy";
11
- import type { InputTokenEstimator } from "./input-token-estimator";
12
11
  import type { ModelContextBudget } from "./model-context-profile";
13
12
  import { ProviderResponseError } from "./model-client";
14
13
  import type {
@@ -22,7 +21,6 @@ import type {
22
21
  PreparedModelRequest,
23
22
  PreparedPromptSegment,
24
23
  } from "./model-client";
25
- import { MoonshotInputTokenEstimator } from "./moonshot-input-token-estimator";
26
24
  import {
27
25
  deepFreeze,
28
26
  imageUserSegment,
@@ -38,7 +36,6 @@ import {
38
36
  toOpenAIResponsesTools,
39
37
  } from "./openai-responses-mapping";
40
38
  import { OpenAIResponsesStreamAccumulator } from "./openai-responses-stream";
41
- import { responsesPayloadForChatTokenEstimator } from "./openai-responses-token-estimator";
42
39
  import type { ReasoningEffortController } from "./reasoning-effort";
43
40
  import { sha256, stableJsonStringify } from "./model-request-preflight";
44
41
 
@@ -50,7 +47,6 @@ export class OpenAIResponsesModelClient implements ModelClient {
50
47
  adapter: "openai-responses",
51
48
  serializationVersion: OPENAI_RESPONSES_SERIALIZATION_VERSION,
52
49
  });
53
- readonly inputTokenEstimator?: InputTokenEstimator;
54
50
  readonly inputModalities: readonly ("text" | "image")[];
55
51
  readonly reasoningEffort?: ReasoningEffortController;
56
52
  private readonly client: OpenAI;
@@ -65,14 +61,6 @@ export class OpenAIResponsesModelClient implements ModelClient {
65
61
  contextBudget: ModelContextBudget;
66
62
  baseURL?: string;
67
63
  inputModalities?: readonly ("text" | "image")[];
68
- tokenEstimator?: {
69
- kind: "moonshot-estimate-token-count-v1";
70
- model: string;
71
- apiBase: string;
72
- apiKey: string;
73
- timeoutMs: number;
74
- maxRetries: 0;
75
- };
76
64
  model: string;
77
65
  providerName?: string;
78
66
  reasoningEffort?: ReasoningEffortController;
@@ -85,13 +73,9 @@ export class OpenAIResponsesModelClient implements ModelClient {
85
73
  this.stream = options.stream ?? true;
86
74
  this.reasoningEffort = options.reasoningEffort;
87
75
  this.inputModalities = Object.freeze([...(options.inputModalities ?? ["text"])]);
88
- const supportsImages = this.inputModalities.includes("image");
89
76
  if (!this.inputModalities.includes("text")) {
90
77
  throw new Error('OpenAI Responses input modalities must include "text".');
91
78
  }
92
- if (supportsImages && options.tokenEstimator === undefined) {
93
- throw new Error("Image-capable OpenAI Responses requires a token estimator.");
94
- }
95
79
  this.client = new OpenAI({
96
80
  apiKey: options.apiKey,
97
81
  baseURL: options.baseURL,
@@ -99,16 +83,6 @@ export class OpenAIResponsesModelClient implements ModelClient {
99
83
  maxRetries: 0,
100
84
  fetch: options.fetch,
101
85
  });
102
- if (options.tokenEstimator !== undefined) {
103
- this.inputTokenEstimator = new MoonshotInputTokenEstimator({
104
- apiKey: options.tokenEstimator.apiKey,
105
- baseURL: options.tokenEstimator.apiBase,
106
- model: options.tokenEstimator.model,
107
- timeoutMs: options.tokenEstimator.timeoutMs,
108
- fetch: options.fetch,
109
- payloadMapper: responsesPayloadForChatTokenEstimator,
110
- });
111
- }
112
86
  }
113
87
 
114
88
  prepare(input: ModelRequestInput): PreparedModelRequest {
@@ -163,9 +137,6 @@ export class OpenAIResponsesModelClient implements ModelClient {
163
137
  version: IMAGE_INPUT_POLICY_VERSION,
164
138
  ...IMAGE_INPUT_POLICY,
165
139
  },
166
- ...(this.inputTokenEstimator === undefined
167
- ? {}
168
- : { tokenEstimator: this.inputTokenEstimator.compatibility }),
169
140
  }),
170
141
  );
171
142
  const prepared: PreparedModelRequest = {
@@ -12,10 +12,15 @@ export type RawContextBreakdown = {
12
12
  toolTokens: number;
13
13
  toolSchemaTokens: number;
14
14
  protocolTokens: number;
15
+ textAndProtocolTokens: number;
16
+ imageTokens: number;
15
17
  totalTokens: number;
16
18
  };
17
19
 
18
- type BreakdownKey = Exclude<keyof RawContextBreakdown, "totalTokens">;
20
+ type BreakdownKey = Exclude<
21
+ keyof RawContextBreakdown,
22
+ "totalTokens" | "textAndProtocolTokens" | "imageTokens"
23
+ >;
19
24
 
20
25
  const breakdownKeys: BreakdownKey[] = [
21
26
  "kernelTokens",
@@ -37,13 +42,16 @@ export function estimatePromptSegments(
37
42
  toolSchemaTokens: 0,
38
43
  protocolTokens: segments.length * 8,
39
44
  };
45
+ let imageTokens = 0;
40
46
 
41
47
  for (const segment of segments) {
42
48
  exact[breakdownKey(segment.kind)] += estimateText(segment.normalizedText);
43
- exact[breakdownKey(segment.kind)] += (segment.media ?? []).reduce(
49
+ const segmentImageTokens = (segment.media ?? []).reduce(
44
50
  (total, media) => total + media.planningTokens,
45
51
  0,
46
52
  );
53
+ exact[breakdownKey(segment.kind)] += segmentImageTokens;
54
+ imageTokens += segmentImageTokens;
47
55
  }
48
56
 
49
57
  const totalTokens = Math.ceil(
@@ -69,7 +77,12 @@ export function estimatePromptSegments(
69
77
  remaining -= 1;
70
78
  }
71
79
 
72
- return { ...rounded, totalTokens };
80
+ return {
81
+ ...rounded,
82
+ textAndProtocolTokens: totalTokens - imageTokens,
83
+ imageTokens,
84
+ totalTokens,
85
+ };
73
86
  }
74
87
 
75
88
  export class RollingTokenCalibration {
@@ -32,7 +32,6 @@ import {
32
32
  MODEL_MESSAGE_PROTOCOL_ADAPTERS,
33
33
  type ModelMessageProtocol,
34
34
  } from "../model/model-client";
35
- import type { InputTokenEstimatorCompatibility } from "../model/input-token-estimator";
36
35
  import type { ToolDefinition, ToolRawResult } from "../tools/types";
37
36
  import { sha256, stableJsonStringify } from "../model/model-request-preflight";
38
37
  import {
@@ -149,7 +148,16 @@ export type SessionImageInputCompatibility = {
149
148
  readonly policyVersion: string;
150
149
  readonly policySha256: string;
151
150
  readonly inputModalities: readonly ("text" | "image")[];
152
- readonly tokenEstimator?: InputTokenEstimatorCompatibility;
151
+ readonly tokenEstimator?: LegacyInputTokenEstimatorCompatibility;
152
+ };
153
+
154
+ type LegacyInputTokenEstimatorCompatibility = {
155
+ readonly kind: "moonshot-estimate-token-count-v1";
156
+ readonly coverageVersion: "full-request-v1";
157
+ readonly model: string;
158
+ readonly endpoint: string;
159
+ readonly timeoutMs: number;
160
+ readonly maxRetries: 0;
153
161
  };
154
162
 
155
163
  export type CompletedTurnMessageSnapshot =
@@ -3704,7 +3712,6 @@ export function createSessionCompatibilityContract(input: {
3704
3712
  contextProfile: ModelContextProfile;
3705
3713
  messageProtocol: ModelMessageProtocol;
3706
3714
  inputModalities?: readonly ("text" | "image")[];
3707
- tokenEstimator?: InputTokenEstimatorCompatibility;
3708
3715
  }): SessionCompatibilityContract {
3709
3716
  if (input.modelName.trim() === "") {
3710
3717
  throw new Error("Session compatibility model name must not be empty.");
@@ -3722,14 +3729,6 @@ export function createSessionCompatibilityContract(input: {
3722
3729
  throw new Error("Session compatibility message protocol is invalid.");
3723
3730
  }
3724
3731
  const inputModalities = normalizeInputModalities(input.inputModalities ?? ["text"]);
3725
- if (inputModalities.includes("image") && input.tokenEstimator === undefined) {
3726
- throw new Error(
3727
- "Session compatibility image input requires a token estimator identity.",
3728
- );
3729
- }
3730
- if (input.tokenEstimator !== undefined) {
3731
- validateTokenEstimatorCompatibility(input.tokenEstimator);
3732
- }
3733
3732
  return Object.freeze({
3734
3733
  modelName: input.modelName,
3735
3734
  ...(input.profileName === undefined ? {} : { profileName: input.profileName }),
@@ -3745,9 +3744,6 @@ export function createSessionCompatibilityContract(input: {
3745
3744
  }),
3746
3745
  ),
3747
3746
  inputModalities,
3748
- ...(input.tokenEstimator === undefined
3749
- ? {}
3750
- : { tokenEstimator: immutableCanonicalClone(input.tokenEstimator) }),
3751
3747
  }),
3752
3748
  });
3753
3749
  }
@@ -3764,9 +3760,6 @@ function normalizeSessionCompatibilityContract(
3764
3760
  contextProfile: contract.contextProfile,
3765
3761
  messageProtocol: contract.messageProtocol,
3766
3762
  inputModalities: contract.imageInput.inputModalities,
3767
- ...(contract.imageInput.tokenEstimator === undefined
3768
- ? {}
3769
- : { tokenEstimator: contract.imageInput.tokenEstimator }),
3770
3763
  });
3771
3764
  }
3772
3765
 
@@ -3787,7 +3780,7 @@ function normalizeInputModalities(
3787
3780
  }
3788
3781
 
3789
3782
  function validateTokenEstimatorCompatibility(
3790
- estimator: InputTokenEstimatorCompatibility,
3783
+ estimator: LegacyInputTokenEstimatorCompatibility,
3791
3784
  ): void {
3792
3785
  if (
3793
3786
  estimator.kind !== "moonshot-estimate-token-count-v1" ||
@@ -5594,9 +5587,6 @@ function decodeSessionCompatibilityContract(
5594
5587
  enumFromSql(value, ["text", "image"] as const, "compatibility input modality"),
5595
5588
  ),
5596
5589
  );
5597
- if (modalities.includes("image") && tokenEstimator === undefined) {
5598
- throw new Error("Stored image input compatibility has no token estimator.");
5599
- }
5600
5590
  const policyVersion = stringFromSql(
5601
5591
  imageInput.policyVersion,
5602
5592
  "compatibility image policyVersion",
@@ -5625,7 +5615,7 @@ function decodeSessionCompatibilityContract(
5625
5615
 
5626
5616
  function decodeTokenEstimatorCompatibility(
5627
5617
  value: unknown,
5628
- ): InputTokenEstimatorCompatibility {
5618
+ ): LegacyInputTokenEstimatorCompatibility {
5629
5619
  const record = recordFromSql(value, "session compatibility token estimator");
5630
5620
  assertObjectKeys(
5631
5621
  record,
@@ -5633,7 +5623,7 @@ function decodeTokenEstimatorCompatibility(
5633
5623
  ["kind", "coverageVersion", "model", "endpoint", "timeoutMs", "maxRetries"],
5634
5624
  "session compatibility token estimator",
5635
5625
  );
5636
- const estimator: InputTokenEstimatorCompatibility = {
5626
+ const estimator: LegacyInputTokenEstimatorCompatibility = {
5637
5627
  kind: enumFromSql(
5638
5628
  record.kind,
5639
5629
  ["moonshot-estimate-token-count-v1"] as const,
@@ -1,25 +0,0 @@
1
- import type { MaterializedModelRequest } from "./model-client";
2
-
3
- export type InputTokenEstimate = {
4
- readonly inputTokens: number;
5
- readonly source: "provider_estimated";
6
- readonly coverage: "messages" | "full_request";
7
- };
8
-
9
- export type InputTokenEstimatorCompatibility = {
10
- readonly kind: "moonshot-estimate-token-count-v1";
11
- readonly coverageVersion: "full-request-v1";
12
- readonly model: string;
13
- readonly endpoint: string;
14
- readonly timeoutMs: number;
15
- readonly maxRetries: 0;
16
- };
17
-
18
- export interface InputTokenEstimator {
19
- readonly kind: string;
20
- readonly compatibility: InputTokenEstimatorCompatibility;
21
- estimate(
22
- request: MaterializedModelRequest,
23
- options: { signal: AbortSignal },
24
- ): Promise<InputTokenEstimate>;
25
- }
@@ -1,111 +0,0 @@
1
- import type { InputTokenEstimate, InputTokenEstimator } from "./input-token-estimator";
2
- import type { MaterializedModelRequest } from "./model-client";
3
- import { IMAGE_INPUT_POLICY } from "../image/image-input-policy";
4
-
5
- export class MoonshotInputTokenEstimator implements InputTokenEstimator {
6
- readonly kind = "moonshot-estimate-token-count-v1";
7
- readonly compatibility: InputTokenEstimator["compatibility"];
8
- private readonly endpoint: string;
9
-
10
- constructor(
11
- private readonly options: {
12
- apiKey: string;
13
- baseURL: string;
14
- model: string;
15
- timeoutMs: number;
16
- fetch?: typeof fetch;
17
- payloadMapper?: (payload: unknown) => unknown;
18
- },
19
- ) {
20
- const base = new URL(
21
- options.baseURL.endsWith("/") ? options.baseURL : `${options.baseURL}/`,
22
- );
23
- base.username = "";
24
- base.password = "";
25
- base.search = "";
26
- base.hash = "";
27
- const endpoint = new URL("tokenizers/estimate-token-count", base);
28
- this.endpoint = endpoint.toString();
29
- this.compatibility = Object.freeze({
30
- kind: "moonshot-estimate-token-count-v1",
31
- coverageVersion: "full-request-v1",
32
- model: options.model,
33
- endpoint: this.endpoint,
34
- timeoutMs: options.timeoutMs,
35
- maxRetries: 0,
36
- });
37
- }
38
-
39
- async estimate(
40
- request: MaterializedModelRequest,
41
- options: { signal: AbortSignal },
42
- ): Promise<InputTokenEstimate> {
43
- const chatPayload = requireRecord(
44
- this.options.payloadMapper?.(request.payload) ?? request.payload,
45
- "materialized chat payload",
46
- );
47
- if (!Array.isArray(chatPayload.messages)) {
48
- throw new Error("Materialized request has no token estimator messages.");
49
- }
50
- const payload = {
51
- model: this.options.model,
52
- messages: chatPayload.messages,
53
- ...(Array.isArray(chatPayload.tools) ? { tools: chatPayload.tools } : {}),
54
- };
55
- const body = JSON.stringify(payload);
56
- const bodyBytes = Buffer.byteLength(body, "utf8");
57
- if (bodyBytes > IMAGE_INPUT_POLICY.maxRequestBodyBytes) {
58
- throw new Error(
59
- `Token estimate request is ${bodyBytes} bytes; maximum is ${IMAGE_INPUT_POLICY.maxRequestBodyBytes}.`,
60
- );
61
- }
62
-
63
- const controller = new AbortController();
64
- const timeout = setTimeout(
65
- () => controller.abort(new Error("Token estimate timed out.")),
66
- this.options.timeoutMs,
67
- );
68
- const onAbort = () => controller.abort(options.signal.reason);
69
- options.signal.addEventListener("abort", onAbort, { once: true });
70
- try {
71
- options.signal.throwIfAborted();
72
- const response = await (this.options.fetch ?? fetch)(this.endpoint, {
73
- method: "POST",
74
- headers: {
75
- authorization: `Bearer ${this.options.apiKey}`,
76
- "content-type": "application/json",
77
- },
78
- body,
79
- signal: controller.signal,
80
- });
81
- if (!response.ok) {
82
- throw new Error(`Token estimate endpoint returned HTTP ${response.status}.`);
83
- }
84
- const decoded: unknown = await response.json();
85
- const root = requireRecord(decoded, "token estimate response");
86
- if (root.error !== undefined && root.error !== null) {
87
- throw new Error("Token estimate endpoint returned an error response.");
88
- }
89
- const data = requireRecord(root.data, "token estimate response data");
90
- const inputTokens = data.total_tokens;
91
- if (!Number.isSafeInteger(inputTokens) || (inputTokens as number) < 0) {
92
- throw new Error("Token estimate response total_tokens is invalid.");
93
- }
94
- return Object.freeze({
95
- inputTokens: inputTokens as number,
96
- source: "provider_estimated",
97
- coverage: "full_request",
98
- });
99
- } finally {
100
- clearTimeout(timeout);
101
- options.signal.removeEventListener("abort", onAbort);
102
- }
103
- }
104
- }
105
-
106
- function requireRecord(value: unknown, name: string): Record<string, unknown> {
107
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
108
- throw new Error(`${name} must be an object.`);
109
- }
110
- return value as Record<string, unknown>;
111
- }
@@ -1,155 +0,0 @@
1
- type ChatEstimatorToolCall = {
2
- id: string;
3
- type: "function";
4
- function: {
5
- name: string;
6
- arguments: string;
7
- };
8
- };
9
-
10
- type ChatEstimatorMessage = {
11
- role: "system" | "user" | "assistant" | "tool";
12
- content: unknown;
13
- tool_calls?: ChatEstimatorToolCall[];
14
- tool_call_id?: string;
15
- };
16
-
17
- export function responsesPayloadForChatTokenEstimator(
18
- payload: unknown,
19
- ): Record<string, unknown> {
20
- const root = requireRecord(payload, "Responses token estimator payload");
21
- if (!Array.isArray(root.input)) {
22
- throw new Error("Responses token estimator payload input must be an array.");
23
- }
24
-
25
- const messages: ChatEstimatorMessage[] = [];
26
- for (const [index, rawItem] of root.input.entries()) {
27
- const path = `Responses token estimator input[${index}]`;
28
- const item = requireRecord(rawItem, path);
29
- const type = requireString(item.type, `${path}.type`);
30
- if (type === "message") {
31
- messages.push(toChatMessage(item, path));
32
- continue;
33
- }
34
- if (type === "function_call") {
35
- appendFunctionCall(messages, item, path);
36
- continue;
37
- }
38
- if (type === "function_call_output") {
39
- messages.push({
40
- role: "tool",
41
- tool_call_id: requireString(item.call_id, `${path}.call_id`),
42
- content: requireString(item.output, `${path}.output`),
43
- });
44
- continue;
45
- }
46
- throw new Error(`${path}.type is unsupported: ${JSON.stringify(type)}.`);
47
- }
48
-
49
- return {
50
- messages,
51
- ...(root.tools === undefined ? {} : { tools: toChatTools(root.tools) }),
52
- };
53
- }
54
-
55
- function toChatMessage(
56
- item: Record<string, unknown>,
57
- path: string,
58
- ): ChatEstimatorMessage {
59
- const role = requireString(item.role, `${path}.role`);
60
- if (role !== "system" && role !== "user" && role !== "assistant") {
61
- throw new Error(`${path}.role is unsupported: ${JSON.stringify(role)}.`);
62
- }
63
- return {
64
- role,
65
- content: toChatContent(item.content, `${path}.content`),
66
- };
67
- }
68
-
69
- function toChatContent(value: unknown, path: string): unknown {
70
- if (typeof value === "string") {
71
- return value;
72
- }
73
- if (!Array.isArray(value)) {
74
- throw new Error(`${path} must be a string or an array.`);
75
- }
76
- return value.map((rawPart, index) => {
77
- const partPath = `${path}[${index}]`;
78
- const part = requireRecord(rawPart, partPath);
79
- const type = requireString(part.type, `${partPath}.type`);
80
- if (type === "input_text") {
81
- return {
82
- type: "text",
83
- text: requireString(part.text, `${partPath}.text`),
84
- };
85
- }
86
- if (type === "input_image") {
87
- return {
88
- type: "image_url",
89
- image_url: {
90
- url: requireString(part.image_url, `${partPath}.image_url`),
91
- ...(part.detail === undefined
92
- ? {}
93
- : { detail: requireString(part.detail, `${partPath}.detail`) }),
94
- },
95
- };
96
- }
97
- throw new Error(`${partPath}.type is unsupported: ${JSON.stringify(type)}.`);
98
- });
99
- }
100
-
101
- function appendFunctionCall(
102
- messages: ChatEstimatorMessage[],
103
- item: Record<string, unknown>,
104
- path: string,
105
- ): void {
106
- const call: ChatEstimatorToolCall = {
107
- id: requireString(item.call_id, `${path}.call_id`),
108
- type: "function",
109
- function: {
110
- name: requireString(item.name, `${path}.name`),
111
- arguments: requireString(item.arguments, `${path}.arguments`),
112
- },
113
- };
114
- const previous = messages[messages.length - 1];
115
- if (previous?.role === "assistant") {
116
- (previous.tool_calls ??= []).push(call);
117
- return;
118
- }
119
- messages.push({ role: "assistant", content: null, tool_calls: [call] });
120
- }
121
-
122
- function toChatTools(value: unknown): Record<string, unknown>[] {
123
- if (!Array.isArray(value)) {
124
- throw new Error("Responses token estimator payload tools must be an array.");
125
- }
126
- return value.map((rawTool, index) => {
127
- const path = `Responses token estimator tools[${index}]`;
128
- const tool = requireRecord(rawTool, path);
129
- if (tool.type !== "function") {
130
- throw new Error(`${path}.type must be "function".`);
131
- }
132
- return {
133
- type: "function",
134
- function: {
135
- name: requireString(tool.name, `${path}.name`),
136
- description: requireString(tool.description, `${path}.description`),
137
- parameters: requireRecord(tool.parameters, `${path}.parameters`),
138
- },
139
- };
140
- });
141
- }
142
-
143
- function requireRecord(value: unknown, path: string): Record<string, unknown> {
144
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
145
- throw new Error(`${path} must be an object.`);
146
- }
147
- return value as Record<string, unknown>;
148
- }
149
-
150
- function requireString(value: unknown, path: string): string {
151
- if (typeof value !== "string") {
152
- throw new Error(`${path} must be a string.`);
153
- }
154
- return value;
155
- }