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.
- package/CHANGELOG.md +17 -1
- package/README.md +7 -23
- package/package.json +1 -1
- package/src/agent/context-meter.ts +14 -85
- package/src/agent/loop.ts +1 -14
- package/src/agent/runtime-session.ts +0 -23
- package/src/cli/config.ts +0 -5
- package/src/cli/model-profiles.ts +0 -98
- package/src/cli/public-config-contract.ts +1 -88
- package/src/cli/runner-dependencies.ts +0 -7
- package/src/cli/tui-memory.ts +0 -3
- package/src/image/image-input-policy.ts +53 -2
- package/src/image/image-probe.ts +8 -2
- package/src/image/provider-image.ts +99 -0
- package/src/model/fake-model-client.ts +69 -69
- package/src/model/model-client.ts +2 -2
- package/src/model/model-request-preflight.ts +0 -1
- package/src/model/openai-chat-model-client.ts +4 -28
- package/src/model/openai-model-utils.ts +69 -31
- package/src/model/openai-responses-model-client.ts +0 -29
- package/src/model/token-estimator.ts +16 -3
- package/src/session/session-store.ts +13 -23
- package/src/model/input-token-estimator.ts +0 -25
- package/src/model/moonshot-input-token-estimator.ts +0 -111
- package/src/model/openai-responses-token-estimator.ts +0 -155
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [2.0.0] - 2026-08-20
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Estimate image input locally with deterministic size buckets instead of a
|
|
13
|
+
provider-side token-estimation request. Images are normalized for orientation
|
|
14
|
+
and provider limits before estimation and upload, keeping preflight accounting
|
|
15
|
+
aligned with the payload sent to the model.
|
|
16
|
+
|
|
17
|
+
### Removed
|
|
18
|
+
|
|
19
|
+
- Remove the `tokenEstimator` model-profile setting. Existing image profiles must
|
|
20
|
+
delete that field; sessions created under the previous image policy remain
|
|
21
|
+
inspectable but cannot be resumed for execution.
|
|
22
|
+
|
|
8
23
|
## [1.11.0] - 2026-08-15
|
|
9
24
|
|
|
10
25
|
### Added
|
|
@@ -208,7 +223,8 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
208
223
|
- First formal npm release under the `tinker-agent` package name with the `tinker`
|
|
209
224
|
executable.
|
|
210
225
|
|
|
211
|
-
[Unreleased]: https://github.com/ishowshao/tinker/compare/
|
|
226
|
+
[Unreleased]: https://github.com/ishowshao/tinker/compare/v2.0.0...HEAD
|
|
227
|
+
[2.0.0]: https://github.com/ishowshao/tinker/releases/tag/v2.0.0
|
|
212
228
|
[1.11.0]: https://github.com/ishowshao/tinker/releases/tag/v1.11.0
|
|
213
229
|
[1.10.1]: https://github.com/ishowshao/tinker/releases/tag/v1.10.1
|
|
214
230
|
[1.9.0]: https://github.com/ishowshao/tinker/releases/tag/v1.9.0
|
package/README.md
CHANGED
|
@@ -194,7 +194,6 @@ Profile fields:
|
|
|
194
194
|
| `includeReasoningContent` | No | JSON boolean | `false` | No | Replay provider reasoning_content in Chat Completions history; ignored by Responses. |
|
|
195
195
|
| `stream` | No | JSON boolean | `true` | No | Use streaming transport for the selected model API. |
|
|
196
196
|
| `inputModalities` | No | Normalized modality array | `["text"]` | No | Accepted model input modalities; normalizes to ["text"] or ["text", "image"]. |
|
|
197
|
-
| `tokenEstimator` | With image | Object | — | Yes | Independent token estimator required for image profiles. |
|
|
198
197
|
|
|
199
198
|
`reasoning` fields:
|
|
200
199
|
|
|
@@ -205,17 +204,6 @@ Profile fields:
|
|
|
205
204
|
|
|
206
205
|
The optional `reasoning` object declares provider-specific effort values. Efforts must be unique non-whitespace strings, `reset` is reserved by the TUI command, and `defaultEffort` must appear in `supportedEfforts`. Omitting `reasoning` sends no effort parameter and disables `/reasoning` for that profile.
|
|
207
206
|
|
|
208
|
-
`tokenEstimator` fields:
|
|
209
|
-
|
|
210
|
-
| Field | Type / constraint | Secret | Description |
|
|
211
|
-
| --- | --- | --- | --- |
|
|
212
|
-
| `kind` | Literal `"moonshot-estimate-token-count-v1"` | No | Estimator protocol discriminator. |
|
|
213
|
-
| `model` | Non-empty string | No | Estimator model name. |
|
|
214
|
-
| `apiBase` | Non-empty string | No | Estimator API base URL. |
|
|
215
|
-
| `apiKey` | Non-empty string | Yes | Estimator API credential. |
|
|
216
|
-
| `timeoutMs` | Integer 1000–60000 | No | Estimator request timeout in milliseconds. |
|
|
217
|
-
| `maxRetries` | Literal `0` | No | Estimator retry count; retries are disabled. |
|
|
218
|
-
|
|
219
207
|
Text-only profile example:
|
|
220
208
|
|
|
221
209
|
```json
|
|
@@ -271,15 +259,7 @@ Image-capable profile example:
|
|
|
271
259
|
"inputModalities": [
|
|
272
260
|
"text",
|
|
273
261
|
"image"
|
|
274
|
-
]
|
|
275
|
-
"tokenEstimator": {
|
|
276
|
-
"kind": "moonshot-estimate-token-count-v1",
|
|
277
|
-
"model": "example-token-estimator",
|
|
278
|
-
"apiBase": "https://estimator.example.com/v1",
|
|
279
|
-
"apiKey": "your-estimator-api-key",
|
|
280
|
-
"timeoutMs": 30000,
|
|
281
|
-
"maxRetries": 0
|
|
282
|
-
}
|
|
262
|
+
]
|
|
283
263
|
}
|
|
284
264
|
}
|
|
285
265
|
}
|
|
@@ -370,7 +350,7 @@ configured profile.
|
|
|
370
350
|
### Image Input
|
|
371
351
|
|
|
372
352
|
Image attachment is enabled only for a profile whose `inputModalities` explicitly
|
|
373
|
-
includes `image
|
|
353
|
+
includes `image`. In the interactive
|
|
374
354
|
TUI, type `@` and select a file that is inside the workspace and visible to the
|
|
375
355
|
workspace search rules. One-shot commands, clipboard image bytes, remote URLs, and
|
|
376
356
|
files outside or ignored by the workspace search are not supported.
|
|
@@ -378,7 +358,11 @@ files outside or ignored by the workspace search are not supported.
|
|
|
378
358
|
Tinker accepts PNG (not APNG), JPEG, and static WebP. It rejects GIF, animated
|
|
379
359
|
WebP, and other formats. A message and provider request may contain at most eight
|
|
380
360
|
images; each image may be at most 20 MiB, 4096 pixels on either edge, and 8,847,360
|
|
381
|
-
pixels in total.
|
|
361
|
+
pixels in total. Provider requests preserve smaller images and proportionally
|
|
362
|
+
downscale larger images to a maximum 2048-pixel long edge. Context planning uses
|
|
363
|
+
fixed local token buckets derived from the materialized dimensions and performs no
|
|
364
|
+
independent token-estimator request. See the
|
|
365
|
+
[`image token bucket design`](docs/image-token-bucket-estimation-design.md) and
|
|
382
366
|
[`multimodal image input design`](docs/multimodal-image-input-design.md) for the
|
|
383
367
|
complete fixed policy and persistence contract.
|
|
384
368
|
|
package/package.json
CHANGED
|
@@ -2,13 +2,8 @@ import type { ModelContextBudget } from "../model/model-context-profile";
|
|
|
2
2
|
import type {
|
|
3
3
|
ModelRequestOutput,
|
|
4
4
|
ModelUsage,
|
|
5
|
-
MaterializedModelRequest,
|
|
6
5
|
PreparedModelRequest,
|
|
7
6
|
} from "../model/model-client";
|
|
8
|
-
import type {
|
|
9
|
-
InputTokenEstimate,
|
|
10
|
-
InputTokenEstimator,
|
|
11
|
-
} from "../model/input-token-estimator";
|
|
12
7
|
import {
|
|
13
8
|
assertContextBudget,
|
|
14
9
|
contextPressure,
|
|
@@ -68,7 +63,6 @@ export class ContextMeter {
|
|
|
68
63
|
private anchor?: MeasuredContextAnchor;
|
|
69
64
|
private lastProviderUsage?: ModelUsage;
|
|
70
65
|
private calibrationIdentity?: string;
|
|
71
|
-
private readonly providerEstimateCache = new Map<string, InputTokenEstimate>();
|
|
72
66
|
|
|
73
67
|
constructor(
|
|
74
68
|
private readonly budget: ModelContextBudget,
|
|
@@ -129,13 +123,14 @@ export class ContextMeter {
|
|
|
129
123
|
let guardedDeltaTokens: number | undefined;
|
|
130
124
|
if (anchor === undefined) {
|
|
131
125
|
source = "estimated_full";
|
|
132
|
-
usedInputTokens =
|
|
126
|
+
usedInputTokens = guardedEstimate(rawFullEstimate, correctionFactor);
|
|
133
127
|
} else {
|
|
134
128
|
source = "measured_plus_estimated_delta";
|
|
135
|
-
|
|
129
|
+
const rawDelta = estimatePromptSegments(
|
|
136
130
|
prepared.promptSegments.slice(anchor.segmentCount),
|
|
137
|
-
)
|
|
138
|
-
|
|
131
|
+
);
|
|
132
|
+
rawDeltaTokens = rawDelta.totalTokens;
|
|
133
|
+
guardedDeltaTokens = guardedEstimate(rawDelta, correctionFactor);
|
|
139
134
|
usedInputTokens = anchor.totalTokens + guardedDeltaTokens;
|
|
140
135
|
}
|
|
141
136
|
|
|
@@ -217,62 +212,6 @@ export class ContextMeter {
|
|
|
217
212
|
};
|
|
218
213
|
}
|
|
219
214
|
|
|
220
|
-
applyProviderEstimate(
|
|
221
|
-
prepared: PreparedModelRequest,
|
|
222
|
-
estimate: {
|
|
223
|
-
inputTokens: number;
|
|
224
|
-
coverage: "messages" | "full_request";
|
|
225
|
-
},
|
|
226
|
-
): ContextUsageSnapshot {
|
|
227
|
-
if (!Number.isSafeInteger(estimate.inputTokens) || estimate.inputTokens < 0) {
|
|
228
|
-
throw new Error("Provider input estimate must be a non-negative safe integer.");
|
|
229
|
-
}
|
|
230
|
-
const local = this.measure(prepared);
|
|
231
|
-
const measurement = this.measurements.get(prepared)!;
|
|
232
|
-
const guardedTools =
|
|
233
|
-
estimate.coverage === "messages"
|
|
234
|
-
? Math.ceil(
|
|
235
|
-
measurement.rawFullEstimate.toolSchemaTokens * local.correctionFactor,
|
|
236
|
-
)
|
|
237
|
-
: 0;
|
|
238
|
-
const providerGuarded = estimate.inputTokens + guardedTools;
|
|
239
|
-
if (providerGuarded <= local.usedInputTokens) {
|
|
240
|
-
return local;
|
|
241
|
-
}
|
|
242
|
-
const snapshot: ContextUsageSnapshot = {
|
|
243
|
-
...local,
|
|
244
|
-
usedInputTokens: providerGuarded,
|
|
245
|
-
source: "provider_estimated",
|
|
246
|
-
pressure: contextPressure(providerGuarded, this.budget),
|
|
247
|
-
};
|
|
248
|
-
this.measurements.set(prepared, {
|
|
249
|
-
rawFullEstimate: measurement.rawFullEstimate,
|
|
250
|
-
snapshot,
|
|
251
|
-
});
|
|
252
|
-
return snapshot;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async estimateProviderInput(
|
|
256
|
-
prepared: MaterializedModelRequest,
|
|
257
|
-
estimator: InputTokenEstimator,
|
|
258
|
-
options: { signal: AbortSignal },
|
|
259
|
-
): Promise<InputTokenEstimate> {
|
|
260
|
-
options.signal.throwIfAborted();
|
|
261
|
-
const key = providerEstimateCacheKey(
|
|
262
|
-
prepared,
|
|
263
|
-
estimator.compatibility.coverageVersion,
|
|
264
|
-
);
|
|
265
|
-
const cached = this.providerEstimateCache.get(key);
|
|
266
|
-
if (cached !== undefined) {
|
|
267
|
-
return cached;
|
|
268
|
-
}
|
|
269
|
-
const estimate = await estimator.estimate(prepared, options);
|
|
270
|
-
options.signal.throwIfAborted();
|
|
271
|
-
const frozen = Object.freeze({ ...estimate });
|
|
272
|
-
this.providerEstimateCache.set(key, frozen);
|
|
273
|
-
return frozen;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
215
|
assertWithinBudget(snapshot: ContextUsageSnapshot): void {
|
|
277
216
|
assertContextBudget({
|
|
278
217
|
usedInputTokens: snapshot.usedInputTokens,
|
|
@@ -295,7 +234,6 @@ export class ContextMeter {
|
|
|
295
234
|
this.lastProviderUsage = undefined;
|
|
296
235
|
this.measurements = new WeakMap();
|
|
297
236
|
this.calibration.clear();
|
|
298
|
-
this.providerEstimateCache.clear();
|
|
299
237
|
this.calibrationIdentity = nextIdentity;
|
|
300
238
|
throw new Error(
|
|
301
239
|
"Context revision changed the request configuration or tool schema.",
|
|
@@ -304,7 +242,6 @@ export class ContextMeter {
|
|
|
304
242
|
this.anchor = undefined;
|
|
305
243
|
this.lastProviderUsage = undefined;
|
|
306
244
|
this.measurements = new WeakMap();
|
|
307
|
-
this.providerEstimateCache.clear();
|
|
308
245
|
this.calibrationIdentity = nextIdentity;
|
|
309
246
|
}
|
|
310
247
|
|
|
@@ -315,7 +252,6 @@ export class ContextMeter {
|
|
|
315
252
|
this.measurements = new WeakMap();
|
|
316
253
|
this.calibration.clear();
|
|
317
254
|
this.calibrationIdentity = undefined;
|
|
318
|
-
this.providerEstimateCache.clear();
|
|
319
255
|
}
|
|
320
256
|
|
|
321
257
|
private usableAnchor(
|
|
@@ -330,10 +266,7 @@ export class ContextMeter {
|
|
|
330
266
|
anchor.requestConfigHash !== prepared.requestConfigHash ||
|
|
331
267
|
anchor.toolSchemaHash !== prepared.toolSchemaHash ||
|
|
332
268
|
anchor.segmentCount > prepared.promptSegments.length ||
|
|
333
|
-
prefixHashes[anchor.segmentCount] !== anchor.prefixHash
|
|
334
|
-
prepared.promptSegments
|
|
335
|
-
.slice(anchor.segmentCount)
|
|
336
|
-
.some((segment) => (segment.media?.length ?? 0) > 0)
|
|
269
|
+
prefixHashes[anchor.segmentCount] !== anchor.prefixHash
|
|
337
270
|
) {
|
|
338
271
|
this.anchor = undefined;
|
|
339
272
|
return undefined;
|
|
@@ -359,18 +292,14 @@ export class ContextMeter {
|
|
|
359
292
|
}
|
|
360
293
|
}
|
|
361
294
|
|
|
362
|
-
function
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
):
|
|
366
|
-
return
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
promptPrefixHashes(prepared.requestConfigHash, prepared.promptSegments),
|
|
371
|
-
),
|
|
372
|
-
coverageVersion,
|
|
373
|
-
].join(":");
|
|
295
|
+
function guardedEstimate(
|
|
296
|
+
breakdown: RawContextBreakdown,
|
|
297
|
+
correctionFactor: number,
|
|
298
|
+
): number {
|
|
299
|
+
return (
|
|
300
|
+
Math.ceil(breakdown.textAndProtocolTokens * correctionFactor) +
|
|
301
|
+
breakdown.imageTokens
|
|
302
|
+
);
|
|
374
303
|
}
|
|
375
304
|
|
|
376
305
|
function assertMeasuredContextAnchor(anchor: MeasuredContextAnchor): void {
|
package/src/agent/loop.ts
CHANGED
|
@@ -175,20 +175,7 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
|
|
|
175
175
|
assetStore: input.assetStore,
|
|
176
176
|
signal: input.signal,
|
|
177
177
|
});
|
|
178
|
-
|
|
179
|
-
preflight = input.contextMeter.measure(request);
|
|
180
|
-
} else {
|
|
181
|
-
const estimator = input.model.inputTokenEstimator;
|
|
182
|
-
if (estimator === undefined) {
|
|
183
|
-
throw new Error("Image model request has no input token estimator.");
|
|
184
|
-
}
|
|
185
|
-
const estimate = await input.contextMeter.estimateProviderInput(
|
|
186
|
-
request as MaterializedModelRequest,
|
|
187
|
-
estimator,
|
|
188
|
-
{ signal: input.signal },
|
|
189
|
-
);
|
|
190
|
-
preflight = input.contextMeter.applyProviderEstimate(request, estimate);
|
|
191
|
-
}
|
|
178
|
+
preflight = input.contextMeter.measure(request);
|
|
192
179
|
input.contextMeter.assertWithinBudget(preflight);
|
|
193
180
|
await input.runtimeSession.append({
|
|
194
181
|
type: "context.usage.updated",
|
|
@@ -581,11 +581,6 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
581
581
|
contextProfile: input.contextProfile,
|
|
582
582
|
messageProtocol: input.modelClient.messageProtocol,
|
|
583
583
|
inputModalities: input.modelClient.inputModalities ?? ["text"],
|
|
584
|
-
...(input.modelClient.inputTokenEstimator === undefined
|
|
585
|
-
? {}
|
|
586
|
-
: {
|
|
587
|
-
tokenEstimator: input.modelClient.inputTokenEstimator.compatibility,
|
|
588
|
-
}),
|
|
589
584
|
});
|
|
590
585
|
if (input.selection.mode === "resume") {
|
|
591
586
|
store.assertSessionCompatibility(compatibility);
|
|
@@ -1641,24 +1636,6 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1641
1636
|
{ assetStore: this.assetStore, signal: controller.signal },
|
|
1642
1637
|
);
|
|
1643
1638
|
admissionSnapshot = this.contextMeter.measure(admissionPrepared);
|
|
1644
|
-
if (
|
|
1645
|
-
prepared.mediaOccurrenceCount > 0 &&
|
|
1646
|
-
admissionSnapshot.source !== "measured_plus_estimated_delta"
|
|
1647
|
-
) {
|
|
1648
|
-
const estimator = this.input.modelClient.inputTokenEstimator;
|
|
1649
|
-
if (estimator === undefined) {
|
|
1650
|
-
throw new Error("Image model request has no input token estimator.");
|
|
1651
|
-
}
|
|
1652
|
-
const estimate = await this.contextMeter.estimateProviderInput(
|
|
1653
|
-
admissionPrepared,
|
|
1654
|
-
estimator,
|
|
1655
|
-
{ signal: controller.signal },
|
|
1656
|
-
);
|
|
1657
|
-
admissionSnapshot = this.contextMeter.applyProviderEstimate(
|
|
1658
|
-
admissionPrepared,
|
|
1659
|
-
estimate,
|
|
1660
|
-
);
|
|
1661
|
-
}
|
|
1662
1639
|
this.contextMeter.assertWithinBudget(admissionSnapshot);
|
|
1663
1640
|
controller.signal.throwIfAborted();
|
|
1664
1641
|
const turn = this.stageTurn(input.userMessage);
|
package/src/cli/config.ts
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
type ModelInputModality,
|
|
15
15
|
type ModelProfile,
|
|
16
16
|
type ModelProfiles,
|
|
17
|
-
type ModelTokenEstimatorProfile,
|
|
18
17
|
unknownProfileError,
|
|
19
18
|
} from "./model-profiles";
|
|
20
19
|
import type { MemoryEmbeddingConfig } from "../memory/contracts";
|
|
@@ -40,7 +39,6 @@ export type RunnerConfig = {
|
|
|
40
39
|
readonly contextBudget: ModelContextBudget;
|
|
41
40
|
readonly profileName?: string;
|
|
42
41
|
readonly inputModalities: readonly ModelInputModality[];
|
|
43
|
-
readonly tokenEstimator?: ModelTokenEstimatorProfile;
|
|
44
42
|
readonly bashGuardMode: "guard" | "yolo";
|
|
45
43
|
readonly bashGuardSource: "default" | "environment" | "cli";
|
|
46
44
|
};
|
|
@@ -184,9 +182,6 @@ function runnerConfigTemplateFromProfile(
|
|
|
184
182
|
inputModalities: profile.inputModalities,
|
|
185
183
|
bashGuardMode: environment.bashGuardMode,
|
|
186
184
|
bashGuardSource: environment.bashGuardSource,
|
|
187
|
-
...(profile.tokenEstimator === undefined
|
|
188
|
-
? {}
|
|
189
|
-
: { tokenEstimator: profile.tokenEstimator }),
|
|
190
185
|
});
|
|
191
186
|
}
|
|
192
187
|
|
|
@@ -10,9 +10,6 @@ import {
|
|
|
10
10
|
MODEL_PROFILE_FIELDS,
|
|
11
11
|
MODEL_PROFILES_DOCUMENT_FIELDS,
|
|
12
12
|
MODEL_REASONING_FIELDS,
|
|
13
|
-
MODEL_TOKEN_ESTIMATOR_FIELDS,
|
|
14
|
-
type ModelTokenEstimatorKind,
|
|
15
|
-
type ModelTokenEstimatorMaxRetries,
|
|
16
13
|
} from "./public-config-contract";
|
|
17
14
|
import type { MemoryEmbeddingConfig } from "../memory/contracts";
|
|
18
15
|
import type { ReasoningEffortConfig } from "../model/reasoning-effort";
|
|
@@ -29,20 +26,10 @@ export type ModelProfile = {
|
|
|
29
26
|
readonly includeReasoningContent: boolean;
|
|
30
27
|
readonly stream: boolean;
|
|
31
28
|
readonly inputModalities: readonly ModelInputModality[];
|
|
32
|
-
readonly tokenEstimator?: ModelTokenEstimatorProfile;
|
|
33
29
|
};
|
|
34
30
|
|
|
35
31
|
export type ModelInputModality = "text" | "image";
|
|
36
32
|
|
|
37
|
-
export type ModelTokenEstimatorProfile = {
|
|
38
|
-
readonly kind: ModelTokenEstimatorKind;
|
|
39
|
-
readonly model: string;
|
|
40
|
-
readonly apiBase: string;
|
|
41
|
-
readonly apiKey: string;
|
|
42
|
-
readonly timeoutMs: number;
|
|
43
|
-
readonly maxRetries: ModelTokenEstimatorMaxRetries;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
33
|
export type ModelProfiles = {
|
|
47
34
|
readonly defaultProfile: string;
|
|
48
35
|
readonly profiles: ReadonlyMap<string, ModelProfile>;
|
|
@@ -285,16 +272,6 @@ function parseProfile(
|
|
|
285
272
|
value.inputModalities,
|
|
286
273
|
`${where}: "inputModalities"`,
|
|
287
274
|
);
|
|
288
|
-
const tokenEstimator =
|
|
289
|
-
value.tokenEstimator === undefined
|
|
290
|
-
? undefined
|
|
291
|
-
: parseTokenEstimator(value.tokenEstimator, `${where}: "tokenEstimator"`);
|
|
292
|
-
if (inputModalities.includes("image") && tokenEstimator === undefined) {
|
|
293
|
-
throw new Error(
|
|
294
|
-
`${where}: image input requires a complete "tokenEstimator" configuration.`,
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
275
|
createModelContextProfile({
|
|
299
276
|
contextWindowTokens,
|
|
300
277
|
maxSupportedOutputTokens,
|
|
@@ -312,7 +289,6 @@ function parseProfile(
|
|
|
312
289
|
includeReasoningContent,
|
|
313
290
|
stream,
|
|
314
291
|
inputModalities,
|
|
315
|
-
...(tokenEstimator === undefined ? {} : { tokenEstimator }),
|
|
316
292
|
});
|
|
317
293
|
}
|
|
318
294
|
|
|
@@ -434,54 +410,7 @@ function parseInputModalities(
|
|
|
434
410
|
);
|
|
435
411
|
}
|
|
436
412
|
|
|
437
|
-
function parseTokenEstimator(value: unknown, name: string): ModelTokenEstimatorProfile {
|
|
438
|
-
if (!isRecord(value)) {
|
|
439
|
-
throw new Error(`${name} must be an object.`);
|
|
440
|
-
}
|
|
441
|
-
assertKnownKeys(
|
|
442
|
-
value,
|
|
443
|
-
MODEL_TOKEN_ESTIMATOR_FIELDS.map((field) => field.name),
|
|
444
|
-
name,
|
|
445
|
-
);
|
|
446
|
-
const kindField = tokenEstimatorField("kind");
|
|
447
|
-
if (value.kind !== kindField.literalValue) {
|
|
448
|
-
throw new Error(`${name}.kind must be ${JSON.stringify(kindField.literalValue)}.`);
|
|
449
|
-
}
|
|
450
|
-
const model = parseTokenEstimatorString(value, "model", name);
|
|
451
|
-
const apiBase = parseTokenEstimatorString(value, "apiBase", name);
|
|
452
|
-
const apiKey = parseTokenEstimatorString(value, "apiKey", name);
|
|
453
|
-
const timeoutField = tokenEstimatorField("timeoutMs");
|
|
454
|
-
if (timeoutField.valueKind !== "positive-integer") {
|
|
455
|
-
throw new Error("Token estimator timeoutMs contract kind is invalid.");
|
|
456
|
-
}
|
|
457
|
-
const timeoutMs = requirePositiveInteger(value.timeoutMs, `${name}.timeoutMs`);
|
|
458
|
-
if (
|
|
459
|
-
timeoutField.minimum === undefined ||
|
|
460
|
-
timeoutField.maximum === undefined ||
|
|
461
|
-
timeoutMs < timeoutField.minimum ||
|
|
462
|
-
timeoutMs > timeoutField.maximum
|
|
463
|
-
) {
|
|
464
|
-
throw new Error(
|
|
465
|
-
`${name}.timeoutMs must be between ${timeoutField.minimum} and ${timeoutField.maximum}.`,
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
const maxRetriesField = tokenEstimatorField("maxRetries");
|
|
469
|
-
if (value.maxRetries !== maxRetriesField.literalValue) {
|
|
470
|
-
throw new Error(`${name}.maxRetries must be ${maxRetriesField.literalValue}.`);
|
|
471
|
-
}
|
|
472
|
-
return Object.freeze({
|
|
473
|
-
kind: kindField.literalValue,
|
|
474
|
-
model,
|
|
475
|
-
apiBase,
|
|
476
|
-
apiKey,
|
|
477
|
-
timeoutMs,
|
|
478
|
-
maxRetries: maxRetriesField.literalValue,
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
413
|
type ModelProfileFieldName = (typeof MODEL_PROFILE_FIELDS)[number]["name"];
|
|
483
|
-
type ModelTokenEstimatorFieldName =
|
|
484
|
-
(typeof MODEL_TOKEN_ESTIMATOR_FIELDS)[number]["name"];
|
|
485
414
|
|
|
486
415
|
function modelProfileField<Name extends ModelProfileFieldName>(
|
|
487
416
|
name: Name,
|
|
@@ -496,21 +425,6 @@ function modelProfileField<Name extends ModelProfileFieldName>(
|
|
|
496
425
|
>;
|
|
497
426
|
}
|
|
498
427
|
|
|
499
|
-
function tokenEstimatorField<Name extends ModelTokenEstimatorFieldName>(
|
|
500
|
-
name: Name,
|
|
501
|
-
): Extract<(typeof MODEL_TOKEN_ESTIMATOR_FIELDS)[number], { readonly name: Name }> {
|
|
502
|
-
const field = MODEL_TOKEN_ESTIMATOR_FIELDS.find(
|
|
503
|
-
(candidate) => candidate.name === name,
|
|
504
|
-
);
|
|
505
|
-
if (field === undefined) {
|
|
506
|
-
throw new Error(`Missing token estimator field contract for ${name}.`);
|
|
507
|
-
}
|
|
508
|
-
return field as Extract<
|
|
509
|
-
(typeof MODEL_TOKEN_ESTIMATOR_FIELDS)[number],
|
|
510
|
-
{ readonly name: Name }
|
|
511
|
-
>;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
428
|
function parseProfileString(
|
|
515
429
|
value: Record<string, unknown>,
|
|
516
430
|
name: "model" | "apiBase" | "apiKey",
|
|
@@ -555,18 +469,6 @@ function parseProfileBoolean(
|
|
|
555
469
|
: parseBoolean(value[name], `${where}: ${JSON.stringify(name)}`);
|
|
556
470
|
}
|
|
557
471
|
|
|
558
|
-
function parseTokenEstimatorString(
|
|
559
|
-
value: Record<string, unknown>,
|
|
560
|
-
name: "model" | "apiBase" | "apiKey",
|
|
561
|
-
where: string,
|
|
562
|
-
): string {
|
|
563
|
-
const field = tokenEstimatorField(name);
|
|
564
|
-
if (field.valueKind !== "non-empty-string") {
|
|
565
|
-
throw new Error(`Token estimator field ${name} has an invalid contract kind.`);
|
|
566
|
-
}
|
|
567
|
-
return requireString(value[name], `${where}.${name}`);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
472
|
function assertKnownKeys(
|
|
571
473
|
value: Record<string, unknown>,
|
|
572
474
|
allowed: readonly string[],
|
|
@@ -242,11 +242,7 @@ export const PUBLIC_CONFIG_FIELDS = Object.freeze([
|
|
|
242
242
|
|
|
243
243
|
export type ModelProfileField = {
|
|
244
244
|
readonly name: string;
|
|
245
|
-
readonly valueKind:
|
|
246
|
-
| PublicConfigValueKind
|
|
247
|
-
| "input-modalities"
|
|
248
|
-
| "reasoning"
|
|
249
|
-
| "token-estimator";
|
|
245
|
+
readonly valueKind: PublicConfigValueKind | "input-modalities" | "reasoning";
|
|
250
246
|
readonly required: boolean;
|
|
251
247
|
readonly defaultValue?: string | number | boolean | readonly string[];
|
|
252
248
|
readonly secret: boolean;
|
|
@@ -337,13 +333,6 @@ export const MODEL_PROFILE_FIELDS = Object.freeze([
|
|
|
337
333
|
description:
|
|
338
334
|
'Accepted model input modalities; normalizes to ["text"] or ["text", "image"].',
|
|
339
335
|
}),
|
|
340
|
-
profileField({
|
|
341
|
-
name: "tokenEstimator",
|
|
342
|
-
valueKind: "token-estimator",
|
|
343
|
-
required: false,
|
|
344
|
-
secret: true,
|
|
345
|
-
description: "Independent token estimator required for image profiles.",
|
|
346
|
-
}),
|
|
347
336
|
]);
|
|
348
337
|
|
|
349
338
|
export type ModelReasoningField = {
|
|
@@ -375,82 +364,6 @@ export const MODEL_REASONING_FIELDS = Object.freeze([
|
|
|
375
364
|
}),
|
|
376
365
|
]);
|
|
377
366
|
|
|
378
|
-
export type ModelTokenEstimatorField = {
|
|
379
|
-
readonly name: string;
|
|
380
|
-
readonly valueKind: PublicConfigValueKind | "literal-string" | "literal-number";
|
|
381
|
-
readonly required: true;
|
|
382
|
-
readonly secret: boolean;
|
|
383
|
-
readonly literalValue?: string | number;
|
|
384
|
-
readonly minimum?: number;
|
|
385
|
-
readonly maximum?: number;
|
|
386
|
-
readonly description: string;
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
function estimatorField<const T extends ModelTokenEstimatorField>(
|
|
390
|
-
field: T,
|
|
391
|
-
): Readonly<T> {
|
|
392
|
-
return Object.freeze(field);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
export const MODEL_TOKEN_ESTIMATOR_FIELDS = Object.freeze([
|
|
396
|
-
estimatorField({
|
|
397
|
-
name: "kind",
|
|
398
|
-
valueKind: "literal-string",
|
|
399
|
-
required: true,
|
|
400
|
-
secret: false,
|
|
401
|
-
literalValue: "moonshot-estimate-token-count-v1",
|
|
402
|
-
description: "Estimator protocol discriminator.",
|
|
403
|
-
}),
|
|
404
|
-
estimatorField({
|
|
405
|
-
name: "model",
|
|
406
|
-
valueKind: "non-empty-string",
|
|
407
|
-
required: true,
|
|
408
|
-
secret: false,
|
|
409
|
-
description: "Estimator model name.",
|
|
410
|
-
}),
|
|
411
|
-
estimatorField({
|
|
412
|
-
name: "apiBase",
|
|
413
|
-
valueKind: "non-empty-string",
|
|
414
|
-
required: true,
|
|
415
|
-
secret: false,
|
|
416
|
-
description: "Estimator API base URL.",
|
|
417
|
-
}),
|
|
418
|
-
estimatorField({
|
|
419
|
-
name: "apiKey",
|
|
420
|
-
valueKind: "non-empty-string",
|
|
421
|
-
required: true,
|
|
422
|
-
secret: true,
|
|
423
|
-
description: "Estimator API credential.",
|
|
424
|
-
}),
|
|
425
|
-
estimatorField({
|
|
426
|
-
name: "timeoutMs",
|
|
427
|
-
valueKind: "positive-integer",
|
|
428
|
-
required: true,
|
|
429
|
-
secret: false,
|
|
430
|
-
minimum: 1_000,
|
|
431
|
-
maximum: 60_000,
|
|
432
|
-
description: "Estimator request timeout in milliseconds.",
|
|
433
|
-
}),
|
|
434
|
-
estimatorField({
|
|
435
|
-
name: "maxRetries",
|
|
436
|
-
valueKind: "literal-number",
|
|
437
|
-
required: true,
|
|
438
|
-
secret: false,
|
|
439
|
-
literalValue: 0,
|
|
440
|
-
description: "Estimator retry count; retries are disabled.",
|
|
441
|
-
}),
|
|
442
|
-
]);
|
|
443
|
-
|
|
444
|
-
export type ModelTokenEstimatorKind = Extract<
|
|
445
|
-
(typeof MODEL_TOKEN_ESTIMATOR_FIELDS)[number],
|
|
446
|
-
{ readonly name: "kind" }
|
|
447
|
-
>["literalValue"];
|
|
448
|
-
|
|
449
|
-
export type ModelTokenEstimatorMaxRetries = Extract<
|
|
450
|
-
(typeof MODEL_TOKEN_ESTIMATOR_FIELDS)[number],
|
|
451
|
-
{ readonly name: "maxRetries" }
|
|
452
|
-
>["literalValue"];
|
|
453
|
-
|
|
454
367
|
export type MemoryConfigField = {
|
|
455
368
|
readonly name: "profile" | "embedding";
|
|
456
369
|
readonly valueKind: "non-empty-string" | "embedding-profile";
|
|
@@ -66,7 +66,6 @@ export function createModelClient(
|
|
|
66
66
|
| "apiKey"
|
|
67
67
|
| "apiBase"
|
|
68
68
|
| "inputModalities"
|
|
69
|
-
| "tokenEstimator"
|
|
70
69
|
>,
|
|
71
70
|
env: NodeJS.ProcessEnv = process.env,
|
|
72
71
|
reasoningEffort?: ReasoningEffortController,
|
|
@@ -82,9 +81,6 @@ export function createModelClient(
|
|
|
82
81
|
...(activeReasoningEffort === undefined
|
|
83
82
|
? {}
|
|
84
83
|
: { reasoningEffort: activeReasoningEffort }),
|
|
85
|
-
...(config.tokenEstimator === undefined
|
|
86
|
-
? {}
|
|
87
|
-
: { tokenEstimator: config.tokenEstimator }),
|
|
88
84
|
...(env.TINKER_TEST_FAKE_MODEL_REQUEST_LOG === undefined ||
|
|
89
85
|
env.TINKER_TEST_FAKE_MODEL_REQUEST_LOG === ""
|
|
90
86
|
? {}
|
|
@@ -102,9 +98,6 @@ export function createModelClient(
|
|
|
102
98
|
...(activeReasoningEffort === undefined
|
|
103
99
|
? {}
|
|
104
100
|
: { reasoningEffort: activeReasoningEffort }),
|
|
105
|
-
...(config.tokenEstimator === undefined
|
|
106
|
-
? {}
|
|
107
|
-
: { tokenEstimator: config.tokenEstimator }),
|
|
108
101
|
};
|
|
109
102
|
if (config.api === "responses") {
|
|
110
103
|
return new OpenAIResponsesModelClient(common);
|
package/src/cli/tui-memory.ts
CHANGED
|
@@ -47,9 +47,6 @@ export async function initializeTuiMemory(input: {
|
|
|
47
47
|
stream: profile.stream,
|
|
48
48
|
contextBudget: memoryConfig.contextBudget,
|
|
49
49
|
inputModalities: profile.inputModalities,
|
|
50
|
-
...(profile.tokenEstimator === undefined
|
|
51
|
-
? {}
|
|
52
|
-
: { tokenEstimator: profile.tokenEstimator }),
|
|
53
50
|
},
|
|
54
51
|
input.env,
|
|
55
52
|
);
|