tinker-agent 1.3.0 → 1.5.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 +39 -1
- package/README.md +271 -72
- package/bin/tinker.js +75 -25
- package/package.json +12 -3
- package/src/agent/runtime-session.ts +113 -15
- package/src/cli/command-line.ts +291 -0
- package/src/cli/config.ts +158 -262
- package/src/cli/index.ts +33 -21
- package/src/cli/main.ts +213 -0
- package/src/cli/model-profiles.ts +226 -72
- package/src/cli/output.ts +113 -0
- package/src/cli/package-metadata.ts +36 -0
- package/src/cli/prompt-source.ts +229 -0
- package/src/cli/public-cli-contract.ts +69 -0
- package/src/cli/public-config-contract.ts +732 -0
- package/src/cli/run-runner.ts +17 -12
- package/src/cli/runner-dependencies.ts +108 -0
- package/src/cli/tui-memory.ts +67 -0
- package/src/cli/tui-runner.tsx +79 -49
- package/src/context/context-policy.ts +2 -2
- package/src/events/stdout-event-printer.ts +1 -0
- package/src/mcp/mcp-manager.ts +2 -19
- package/src/mcp/mcp-tool-executor.ts +3 -4
- package/src/memory/contracts.ts +148 -0
- package/src/memory/embedding-client.ts +105 -0
- package/src/memory/memory-coordinator.ts +556 -0
- package/src/memory/memory-extractor.ts +231 -0
- package/src/memory/memory-log.ts +88 -0
- package/src/memory/memory-search-tool.ts +100 -0
- package/src/memory/memory-store.ts +687 -0
- package/src/memory/vector.ts +153 -0
- package/src/model/fake-model-client.ts +971 -3
- package/src/model/model-context-profile.ts +0 -30
- package/src/observation/observation-builder.ts +20 -0
- package/src/session/session-store.ts +123 -0
- package/src/tools/bash.ts +8 -25
- package/src/tools/grep.ts +9 -1
- package/src/tools/registry.ts +19 -1
- package/src/tools/ripgrep.ts +24 -27
- package/src/tools/types.ts +16 -0
- package/src/tools/web-fetch/index.ts +2 -15
- package/src/tui/app.tsx +72 -2
- package/src/tui/clipboard.ts +22 -0
- package/src/tui/components/footer.tsx +9 -4
- package/src/tui/components/memory-browser.tsx +151 -0
- package/src/tui/components/prompt-input.tsx +6 -3
- package/src/tui/event-store.ts +9 -2
- package/src/tui/slash-commands.ts +88 -24
- package/src/tui/workspace-file-search.ts +78 -71
package/src/cli/main.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { SessionId } from "../ids/runtime-id";
|
|
2
|
+
import { createUuidV7 } from "../ids/uuid-v7";
|
|
3
|
+
import { parseCommandLine, type CommandLineResult } from "./command-line";
|
|
4
|
+
import type {
|
|
5
|
+
ResolvedPublicConfig,
|
|
6
|
+
RunnerConfig,
|
|
7
|
+
RunnerConfigSelection,
|
|
8
|
+
} from "./config";
|
|
9
|
+
import {
|
|
10
|
+
CliUsageError,
|
|
11
|
+
flushCliOutput,
|
|
12
|
+
renderCliFailure,
|
|
13
|
+
renderUsageError,
|
|
14
|
+
writeCliOutput,
|
|
15
|
+
type CliOutputWriter,
|
|
16
|
+
} from "./output";
|
|
17
|
+
import { loadPackageMetadata, type PackageMetadata } from "./package-metadata";
|
|
18
|
+
import {
|
|
19
|
+
PromptInputError,
|
|
20
|
+
resolvePromptSource,
|
|
21
|
+
type PromptReadable,
|
|
22
|
+
type PromptSource,
|
|
23
|
+
type ResolvedPrompt,
|
|
24
|
+
} from "./prompt-source";
|
|
25
|
+
|
|
26
|
+
export const BOOTSTRAP_FAILURE_MESSAGE =
|
|
27
|
+
"Tinker failed to start. Reinstall tinker-agent.\n";
|
|
28
|
+
|
|
29
|
+
export type MainInput = {
|
|
30
|
+
readonly args: readonly string[];
|
|
31
|
+
readonly stdin: PromptReadable;
|
|
32
|
+
readonly stdout: CliOutputWriter;
|
|
33
|
+
readonly stderr: CliOutputWriter;
|
|
34
|
+
readonly cwd: string;
|
|
35
|
+
readonly env: NodeJS.ProcessEnv;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type ConfigBoundary = {
|
|
39
|
+
readonly resolvePublicConfig: (input: {
|
|
40
|
+
readonly env: NodeJS.ProcessEnv;
|
|
41
|
+
readonly cwd: string;
|
|
42
|
+
}) => Promise<ResolvedPublicConfig>;
|
|
43
|
+
readonly deriveRunnerConfig: (
|
|
44
|
+
snapshot: ResolvedPublicConfig,
|
|
45
|
+
selection: RunnerConfigSelection,
|
|
46
|
+
) => RunnerConfig;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type TuiRunner = {
|
|
50
|
+
readonly runTui: (options: {
|
|
51
|
+
readonly publicConfig: ResolvedPublicConfig;
|
|
52
|
+
readonly initialRunnerConfig: RunnerConfig;
|
|
53
|
+
readonly env: NodeJS.ProcessEnv;
|
|
54
|
+
}) => Promise<void>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type OneShotRunner = {
|
|
58
|
+
readonly runOneShot: (
|
|
59
|
+
prompt: string,
|
|
60
|
+
options: {
|
|
61
|
+
readonly config: RunnerConfig;
|
|
62
|
+
readonly tooling: ResolvedPublicConfig["tooling"];
|
|
63
|
+
readonly stdout: CliOutputWriter;
|
|
64
|
+
readonly stderr: CliOutputWriter;
|
|
65
|
+
readonly env: NodeJS.ProcessEnv;
|
|
66
|
+
},
|
|
67
|
+
) => Promise<number>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type MainDependencies = {
|
|
71
|
+
readonly loadPackageMetadata: () => Promise<PackageMetadata>;
|
|
72
|
+
readonly parseCommandLine: (
|
|
73
|
+
args: readonly string[],
|
|
74
|
+
packageVersion: string,
|
|
75
|
+
) => Promise<CommandLineResult>;
|
|
76
|
+
readonly loadConfigBoundary: () => Promise<ConfigBoundary>;
|
|
77
|
+
readonly createSessionId: () => SessionId;
|
|
78
|
+
readonly resolvePromptSource: (
|
|
79
|
+
source: PromptSource,
|
|
80
|
+
input: { readonly stdin: PromptReadable; readonly cwd: string },
|
|
81
|
+
) => Promise<ResolvedPrompt>;
|
|
82
|
+
readonly loadTuiRunner: () => Promise<TuiRunner>;
|
|
83
|
+
readonly loadOneShotRunner: () => Promise<OneShotRunner>;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const DEFAULT_DEPENDENCIES: MainDependencies = {
|
|
87
|
+
loadPackageMetadata,
|
|
88
|
+
parseCommandLine,
|
|
89
|
+
loadConfigBoundary: () => import("./config"),
|
|
90
|
+
createSessionId: () => createUuidV7() as SessionId,
|
|
91
|
+
resolvePromptSource,
|
|
92
|
+
loadTuiRunner: () => import("./tui-runner"),
|
|
93
|
+
loadOneShotRunner: () => import("./run-runner"),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export async function main(
|
|
97
|
+
input: MainInput,
|
|
98
|
+
injected: Partial<MainDependencies> = {},
|
|
99
|
+
): Promise<number> {
|
|
100
|
+
const dependencies = { ...DEFAULT_DEPENDENCIES, ...injected };
|
|
101
|
+
const args = Object.freeze([...input.args]);
|
|
102
|
+
const env = Object.freeze({ ...input.env }) as NodeJS.ProcessEnv;
|
|
103
|
+
const cwd = input.cwd;
|
|
104
|
+
const finish = async (exitCode: number): Promise<number> => {
|
|
105
|
+
await flushCliOutput(input.stdout);
|
|
106
|
+
await flushCliOutput(input.stderr);
|
|
107
|
+
return exitCode;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
let metadata: PackageMetadata;
|
|
112
|
+
try {
|
|
113
|
+
metadata = await dependencies.loadPackageMetadata();
|
|
114
|
+
} catch {
|
|
115
|
+
await writeCliOutput(input.stderr, BOOTSTRAP_FAILURE_MESSAGE);
|
|
116
|
+
return finish(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let parsed: CommandLineResult;
|
|
120
|
+
try {
|
|
121
|
+
parsed = await dependencies.parseCommandLine(args, metadata.version);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof CliUsageError) {
|
|
124
|
+
await writeCliOutput(input.stderr, renderUsageError(error));
|
|
125
|
+
return finish(2);
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (parsed.type === "terminal") {
|
|
131
|
+
await writeCliOutput(input.stdout, parsed.stdout);
|
|
132
|
+
await writeCliOutput(input.stderr, parsed.stderr);
|
|
133
|
+
return finish(0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let configBoundary: ConfigBoundary;
|
|
137
|
+
let publicConfig: ResolvedPublicConfig;
|
|
138
|
+
let runnerConfig: RunnerConfig;
|
|
139
|
+
try {
|
|
140
|
+
configBoundary = await dependencies.loadConfigBoundary();
|
|
141
|
+
publicConfig = await configBoundary.resolvePublicConfig({ env, cwd });
|
|
142
|
+
runnerConfig = configBoundary.deriveRunnerConfig(publicConfig, {
|
|
143
|
+
sessionId: dependencies.createSessionId(),
|
|
144
|
+
...(parsed.command.profileName === undefined
|
|
145
|
+
? {}
|
|
146
|
+
: { profileName: parsed.command.profileName }),
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
await writeCliOutput(
|
|
150
|
+
input.stderr,
|
|
151
|
+
renderCliFailure("Configuration failed", error),
|
|
152
|
+
);
|
|
153
|
+
return finish(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (parsed.command.type === "tui") {
|
|
157
|
+
try {
|
|
158
|
+
const runner = await dependencies.loadTuiRunner();
|
|
159
|
+
await runner.runTui({
|
|
160
|
+
publicConfig,
|
|
161
|
+
initialRunnerConfig: runnerConfig,
|
|
162
|
+
env,
|
|
163
|
+
});
|
|
164
|
+
return finish(0);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
await writeCliOutput(input.stderr, renderCliFailure("Runtime failed", error));
|
|
167
|
+
return finish(1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let prompt: ResolvedPrompt;
|
|
172
|
+
try {
|
|
173
|
+
prompt = await dependencies.resolvePromptSource(parsed.command.promptSource, {
|
|
174
|
+
stdin: input.stdin,
|
|
175
|
+
cwd,
|
|
176
|
+
});
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error instanceof PromptInputError) {
|
|
179
|
+
await writeCliOutput(
|
|
180
|
+
input.stderr,
|
|
181
|
+
renderCliFailure("Prompt input failed", error),
|
|
182
|
+
);
|
|
183
|
+
return finish(error.exitCode);
|
|
184
|
+
}
|
|
185
|
+
await writeCliOutput(
|
|
186
|
+
input.stderr,
|
|
187
|
+
renderCliFailure("Prompt input failed", error),
|
|
188
|
+
);
|
|
189
|
+
return finish(1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const runner = await dependencies.loadOneShotRunner();
|
|
194
|
+
const exitCode = await runner.runOneShot(prompt.text, {
|
|
195
|
+
config: runnerConfig,
|
|
196
|
+
tooling: publicConfig.tooling,
|
|
197
|
+
stdout: input.stdout,
|
|
198
|
+
stderr: input.stderr,
|
|
199
|
+
env,
|
|
200
|
+
});
|
|
201
|
+
return finish(exitCode);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
await writeCliOutput(input.stderr, renderCliFailure("Runtime failed", error));
|
|
204
|
+
return finish(1);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
await writeCliOutput(
|
|
208
|
+
input.stderr,
|
|
209
|
+
renderCliFailure("Tinker failed unexpectedly", error),
|
|
210
|
+
);
|
|
211
|
+
return finish(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
2
|
import {
|
|
4
3
|
createModelContextProfile,
|
|
5
4
|
type ModelContextProfile,
|
|
6
5
|
} from "../model/model-context-profile";
|
|
6
|
+
import {
|
|
7
|
+
MEMORY_CONFIG_FIELDS,
|
|
8
|
+
MEMORY_EMBEDDING_FIELDS,
|
|
9
|
+
MODEL_PROFILE_FIELDS,
|
|
10
|
+
MODEL_PROFILES_DOCUMENT_FIELDS,
|
|
11
|
+
MODEL_TOKEN_ESTIMATOR_FIELDS,
|
|
12
|
+
type ModelTokenEstimatorKind,
|
|
13
|
+
type ModelTokenEstimatorMaxRetries,
|
|
14
|
+
} from "./public-config-contract";
|
|
15
|
+
import type { MemoryEmbeddingConfig } from "../memory/contracts";
|
|
7
16
|
|
|
8
17
|
export type ModelProfile = {
|
|
9
18
|
readonly name: string;
|
|
@@ -21,37 +30,26 @@ export type ModelProfile = {
|
|
|
21
30
|
export type ModelInputModality = "text" | "image";
|
|
22
31
|
|
|
23
32
|
export type ModelTokenEstimatorProfile = {
|
|
24
|
-
readonly kind:
|
|
33
|
+
readonly kind: ModelTokenEstimatorKind;
|
|
25
34
|
readonly model: string;
|
|
26
35
|
readonly apiBase: string;
|
|
27
36
|
readonly apiKey: string;
|
|
28
37
|
readonly timeoutMs: number;
|
|
29
|
-
readonly maxRetries:
|
|
38
|
+
readonly maxRetries: ModelTokenEstimatorMaxRetries;
|
|
30
39
|
};
|
|
31
40
|
|
|
32
41
|
export type ModelProfiles = {
|
|
33
42
|
readonly defaultProfile: string;
|
|
34
43
|
readonly profiles: ReadonlyMap<string, ModelProfile>;
|
|
44
|
+
readonly memory?: MemoryConfig;
|
|
35
45
|
};
|
|
36
46
|
|
|
37
|
-
export
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (value === undefined || value.trim() === "") {
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function loadModelProfiles(
|
|
48
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
49
|
-
): Promise<ModelProfiles | undefined> {
|
|
50
|
-
const configPath = modelsConfigPath(env);
|
|
51
|
-
if (configPath === undefined) {
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
47
|
+
export type MemoryConfig = {
|
|
48
|
+
readonly profile: string;
|
|
49
|
+
readonly embedding: MemoryEmbeddingConfig;
|
|
50
|
+
};
|
|
54
51
|
|
|
52
|
+
export async function loadModelProfiles(configPath: string): Promise<ModelProfiles> {
|
|
55
53
|
let raw: string;
|
|
56
54
|
try {
|
|
57
55
|
raw = await readFile(configPath, "utf8");
|
|
@@ -67,13 +65,8 @@ export async function loadModelProfiles(
|
|
|
67
65
|
|
|
68
66
|
export async function persistDefaultProfile(
|
|
69
67
|
profileName: string,
|
|
70
|
-
|
|
68
|
+
configPath: string,
|
|
71
69
|
): Promise<void> {
|
|
72
|
-
const configPath = modelsConfigPath(env);
|
|
73
|
-
if (configPath === undefined) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
70
|
let raw: string;
|
|
78
71
|
try {
|
|
79
72
|
raw = await readFile(configPath, "utf8");
|
|
@@ -128,6 +121,11 @@ export function parseModelProfiles(raw: string, sourcePath: string): ModelProfil
|
|
|
128
121
|
if (!isRecord(json)) {
|
|
129
122
|
throw new Error(`Model profiles ${sourcePath} must be a JSON object.`);
|
|
130
123
|
}
|
|
124
|
+
assertKnownKeys(
|
|
125
|
+
json,
|
|
126
|
+
MODEL_PROFILES_DOCUMENT_FIELDS.map((field) => field.name),
|
|
127
|
+
`Model profiles ${sourcePath}`,
|
|
128
|
+
);
|
|
131
129
|
|
|
132
130
|
const defaultProfile = json.default;
|
|
133
131
|
if (typeof defaultProfile !== "string" || defaultProfile.trim() === "") {
|
|
@@ -155,7 +153,15 @@ export function parseModelProfiles(raw: string, sourcePath: string): ModelProfil
|
|
|
155
153
|
profiles.set(profileName, parseProfile(profileName, profileValue, sourcePath));
|
|
156
154
|
}
|
|
157
155
|
|
|
158
|
-
|
|
156
|
+
const memory =
|
|
157
|
+
json.memory === undefined
|
|
158
|
+
? undefined
|
|
159
|
+
: parseMemoryConfig(json.memory, profiles, sourcePath);
|
|
160
|
+
return Object.freeze({
|
|
161
|
+
defaultProfile,
|
|
162
|
+
profiles,
|
|
163
|
+
...(memory === undefined ? {} : { memory }),
|
|
164
|
+
});
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
export function resolveModelProfile(
|
|
@@ -238,45 +244,31 @@ function parseProfile(
|
|
|
238
244
|
|
|
239
245
|
assertKnownKeys(
|
|
240
246
|
value,
|
|
241
|
-
|
|
242
|
-
"model",
|
|
243
|
-
"apiBase",
|
|
244
|
-
"apiKey",
|
|
245
|
-
"contextWindowTokens",
|
|
246
|
-
"maxSupportedOutputTokens",
|
|
247
|
-
"includeReasoningContent",
|
|
248
|
-
"stream",
|
|
249
|
-
"inputModalities",
|
|
250
|
-
"tokenEstimator",
|
|
251
|
-
],
|
|
247
|
+
MODEL_PROFILE_FIELDS.map((field) => field.name),
|
|
252
248
|
where,
|
|
253
249
|
);
|
|
254
250
|
|
|
255
|
-
const model =
|
|
256
|
-
const apiBase =
|
|
257
|
-
const apiKey =
|
|
251
|
+
const model = parseProfileString(value, "model", where);
|
|
252
|
+
const apiBase = parseProfileString(value, "apiBase", where);
|
|
253
|
+
const apiKey = parseProfileString(value, "apiKey", where);
|
|
258
254
|
|
|
259
|
-
const contextWindowTokens =
|
|
260
|
-
value
|
|
261
|
-
|
|
255
|
+
const contextWindowTokens = parseProfilePositiveInteger(
|
|
256
|
+
value,
|
|
257
|
+
"contextWindowTokens",
|
|
258
|
+
where,
|
|
262
259
|
);
|
|
263
|
-
const maxSupportedOutputTokens =
|
|
264
|
-
value
|
|
265
|
-
|
|
260
|
+
const maxSupportedOutputTokens = parseProfilePositiveInteger(
|
|
261
|
+
value,
|
|
262
|
+
"maxSupportedOutputTokens",
|
|
263
|
+
where,
|
|
266
264
|
);
|
|
267
265
|
|
|
268
|
-
const includeReasoningContent =
|
|
269
|
-
value
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
const stream =
|
|
277
|
-
value.stream === undefined
|
|
278
|
-
? true
|
|
279
|
-
: parseBoolean(value.stream, `${where}: "stream"`);
|
|
266
|
+
const includeReasoningContent = parseProfileBoolean(
|
|
267
|
+
value,
|
|
268
|
+
"includeReasoningContent",
|
|
269
|
+
where,
|
|
270
|
+
);
|
|
271
|
+
const stream = parseProfileBoolean(value, "stream", where);
|
|
280
272
|
|
|
281
273
|
const inputModalities = parseInputModalities(
|
|
282
274
|
value.inputModalities,
|
|
@@ -311,12 +303,63 @@ function parseProfile(
|
|
|
311
303
|
});
|
|
312
304
|
}
|
|
313
305
|
|
|
306
|
+
function parseMemoryConfig(
|
|
307
|
+
value: unknown,
|
|
308
|
+
profiles: ReadonlyMap<string, ModelProfile>,
|
|
309
|
+
sourcePath: string,
|
|
310
|
+
): MemoryConfig {
|
|
311
|
+
const where = `Model profiles ${sourcePath}: "memory"`;
|
|
312
|
+
if (!isRecord(value)) {
|
|
313
|
+
throw new Error(`${where} must be an object.`);
|
|
314
|
+
}
|
|
315
|
+
assertKnownKeys(
|
|
316
|
+
value,
|
|
317
|
+
MEMORY_CONFIG_FIELDS.map((field) => field.name),
|
|
318
|
+
where,
|
|
319
|
+
);
|
|
320
|
+
const profile = requireString(value.profile, `${where}.profile`);
|
|
321
|
+
if (!profiles.has(profile)) {
|
|
322
|
+
throw unknownProfileNamesError(profile, [...profiles.keys()]);
|
|
323
|
+
}
|
|
324
|
+
const embedding = parseMemoryEmbedding(value.embedding, `${where}.embedding`);
|
|
325
|
+
return Object.freeze({ profile, embedding });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function parseMemoryEmbedding(value: unknown, where: string): MemoryEmbeddingConfig {
|
|
329
|
+
if (!isRecord(value)) {
|
|
330
|
+
throw new Error(`${where} must be an object.`);
|
|
331
|
+
}
|
|
332
|
+
assertKnownKeys(
|
|
333
|
+
value,
|
|
334
|
+
MEMORY_EMBEDDING_FIELDS.map((field) => field.name),
|
|
335
|
+
where,
|
|
336
|
+
);
|
|
337
|
+
const name = requireString(value.name, `${where}.name`);
|
|
338
|
+
if (value.kind !== "openai-compatible") {
|
|
339
|
+
throw new Error(`${where}.kind must be "openai-compatible".`);
|
|
340
|
+
}
|
|
341
|
+
const model = requireString(value.model, `${where}.model`);
|
|
342
|
+
const apiBase = requireString(value.apiBase, `${where}.apiBase`);
|
|
343
|
+
requireHttpUrl(apiBase, `${where}.apiBase`);
|
|
344
|
+
const apiKey = requireString(value.apiKey, `${where}.apiKey`);
|
|
345
|
+
const dimensions = requirePositiveInteger(value.dimensions, `${where}.dimensions`);
|
|
346
|
+
return Object.freeze({
|
|
347
|
+
name,
|
|
348
|
+
kind: "openai-compatible",
|
|
349
|
+
model,
|
|
350
|
+
apiBase,
|
|
351
|
+
apiKey,
|
|
352
|
+
dimensions,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
314
356
|
function parseInputModalities(
|
|
315
357
|
value: unknown,
|
|
316
358
|
name: string,
|
|
317
359
|
): readonly ModelInputModality[] {
|
|
318
360
|
if (value === undefined) {
|
|
319
|
-
|
|
361
|
+
const defaultValue = modelProfileField("inputModalities").defaultValue;
|
|
362
|
+
return defaultValue;
|
|
320
363
|
}
|
|
321
364
|
if (!Array.isArray(value) || value.length === 0) {
|
|
322
365
|
throw new Error(`${name} must be a non-empty array.`);
|
|
@@ -345,32 +388,127 @@ function parseTokenEstimator(value: unknown, name: string): ModelTokenEstimatorP
|
|
|
345
388
|
}
|
|
346
389
|
assertKnownKeys(
|
|
347
390
|
value,
|
|
348
|
-
|
|
391
|
+
MODEL_TOKEN_ESTIMATOR_FIELDS.map((field) => field.name),
|
|
349
392
|
name,
|
|
350
393
|
);
|
|
351
|
-
|
|
352
|
-
|
|
394
|
+
const kindField = tokenEstimatorField("kind");
|
|
395
|
+
if (value.kind !== kindField.literalValue) {
|
|
396
|
+
throw new Error(`${name}.kind must be ${JSON.stringify(kindField.literalValue)}.`);
|
|
397
|
+
}
|
|
398
|
+
const model = parseTokenEstimatorString(value, "model", name);
|
|
399
|
+
const apiBase = parseTokenEstimatorString(value, "apiBase", name);
|
|
400
|
+
const apiKey = parseTokenEstimatorString(value, "apiKey", name);
|
|
401
|
+
const timeoutField = tokenEstimatorField("timeoutMs");
|
|
402
|
+
if (timeoutField.valueKind !== "positive-integer") {
|
|
403
|
+
throw new Error("Token estimator timeoutMs contract kind is invalid.");
|
|
353
404
|
}
|
|
354
|
-
const model = requireString(value.model, `${name}.model`);
|
|
355
|
-
const apiBase = requireString(value.apiBase, `${name}.apiBase`);
|
|
356
|
-
const apiKey = requireString(value.apiKey, `${name}.apiKey`);
|
|
357
405
|
const timeoutMs = requirePositiveInteger(value.timeoutMs, `${name}.timeoutMs`);
|
|
358
|
-
if (
|
|
359
|
-
|
|
406
|
+
if (
|
|
407
|
+
timeoutField.minimum === undefined ||
|
|
408
|
+
timeoutField.maximum === undefined ||
|
|
409
|
+
timeoutMs < timeoutField.minimum ||
|
|
410
|
+
timeoutMs > timeoutField.maximum
|
|
411
|
+
) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
`${name}.timeoutMs must be between ${timeoutField.minimum} and ${timeoutField.maximum}.`,
|
|
414
|
+
);
|
|
360
415
|
}
|
|
361
|
-
|
|
362
|
-
|
|
416
|
+
const maxRetriesField = tokenEstimatorField("maxRetries");
|
|
417
|
+
if (value.maxRetries !== maxRetriesField.literalValue) {
|
|
418
|
+
throw new Error(`${name}.maxRetries must be ${maxRetriesField.literalValue}.`);
|
|
363
419
|
}
|
|
364
420
|
return Object.freeze({
|
|
365
|
-
kind:
|
|
421
|
+
kind: kindField.literalValue,
|
|
366
422
|
model,
|
|
367
423
|
apiBase,
|
|
368
424
|
apiKey,
|
|
369
425
|
timeoutMs,
|
|
370
|
-
maxRetries:
|
|
426
|
+
maxRetries: maxRetriesField.literalValue,
|
|
371
427
|
});
|
|
372
428
|
}
|
|
373
429
|
|
|
430
|
+
type ModelProfileFieldName = (typeof MODEL_PROFILE_FIELDS)[number]["name"];
|
|
431
|
+
type ModelTokenEstimatorFieldName =
|
|
432
|
+
(typeof MODEL_TOKEN_ESTIMATOR_FIELDS)[number]["name"];
|
|
433
|
+
|
|
434
|
+
function modelProfileField<Name extends ModelProfileFieldName>(
|
|
435
|
+
name: Name,
|
|
436
|
+
): Extract<(typeof MODEL_PROFILE_FIELDS)[number], { readonly name: Name }> {
|
|
437
|
+
const field = MODEL_PROFILE_FIELDS.find((candidate) => candidate.name === name);
|
|
438
|
+
if (field === undefined) {
|
|
439
|
+
throw new Error(`Missing model profile field contract for ${name}.`);
|
|
440
|
+
}
|
|
441
|
+
return field as Extract<
|
|
442
|
+
(typeof MODEL_PROFILE_FIELDS)[number],
|
|
443
|
+
{ readonly name: Name }
|
|
444
|
+
>;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function tokenEstimatorField<Name extends ModelTokenEstimatorFieldName>(
|
|
448
|
+
name: Name,
|
|
449
|
+
): Extract<(typeof MODEL_TOKEN_ESTIMATOR_FIELDS)[number], { readonly name: Name }> {
|
|
450
|
+
const field = MODEL_TOKEN_ESTIMATOR_FIELDS.find(
|
|
451
|
+
(candidate) => candidate.name === name,
|
|
452
|
+
);
|
|
453
|
+
if (field === undefined) {
|
|
454
|
+
throw new Error(`Missing token estimator field contract for ${name}.`);
|
|
455
|
+
}
|
|
456
|
+
return field as Extract<
|
|
457
|
+
(typeof MODEL_TOKEN_ESTIMATOR_FIELDS)[number],
|
|
458
|
+
{ readonly name: Name }
|
|
459
|
+
>;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function parseProfileString(
|
|
463
|
+
value: Record<string, unknown>,
|
|
464
|
+
name: "model" | "apiBase" | "apiKey",
|
|
465
|
+
where: string,
|
|
466
|
+
): string {
|
|
467
|
+
const field = modelProfileField(name);
|
|
468
|
+
if (field.valueKind !== "non-empty-string") {
|
|
469
|
+
throw new Error(`Model profile field ${name} has an invalid contract kind.`);
|
|
470
|
+
}
|
|
471
|
+
return requireString(value[name], `${where}: ${JSON.stringify(name)}`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function parseProfilePositiveInteger(
|
|
475
|
+
value: Record<string, unknown>,
|
|
476
|
+
name: "contextWindowTokens" | "maxSupportedOutputTokens",
|
|
477
|
+
where: string,
|
|
478
|
+
): number {
|
|
479
|
+
const field = modelProfileField(name);
|
|
480
|
+
if (field.valueKind !== "positive-integer") {
|
|
481
|
+
throw new Error(`Model profile field ${name} has an invalid contract kind.`);
|
|
482
|
+
}
|
|
483
|
+
return requirePositiveInteger(value[name], `${where}: ${JSON.stringify(name)}`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function parseProfileBoolean(
|
|
487
|
+
value: Record<string, unknown>,
|
|
488
|
+
name: "includeReasoningContent" | "stream",
|
|
489
|
+
where: string,
|
|
490
|
+
): boolean {
|
|
491
|
+
const field = modelProfileField(name);
|
|
492
|
+
if (field.valueKind !== "boolean" || typeof field.defaultValue !== "boolean") {
|
|
493
|
+
throw new Error(`Model profile field ${name} has an invalid boolean contract.`);
|
|
494
|
+
}
|
|
495
|
+
return value[name] === undefined
|
|
496
|
+
? field.defaultValue
|
|
497
|
+
: parseBoolean(value[name], `${where}: ${JSON.stringify(name)}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function parseTokenEstimatorString(
|
|
501
|
+
value: Record<string, unknown>,
|
|
502
|
+
name: "model" | "apiBase" | "apiKey",
|
|
503
|
+
where: string,
|
|
504
|
+
): string {
|
|
505
|
+
const field = tokenEstimatorField(name);
|
|
506
|
+
if (field.valueKind !== "non-empty-string") {
|
|
507
|
+
throw new Error(`Token estimator field ${name} has an invalid contract kind.`);
|
|
508
|
+
}
|
|
509
|
+
return requireString(value[name], `${where}.${name}`);
|
|
510
|
+
}
|
|
511
|
+
|
|
374
512
|
function assertKnownKeys(
|
|
375
513
|
value: Record<string, unknown>,
|
|
376
514
|
allowed: readonly string[],
|
|
@@ -401,6 +539,22 @@ function requirePositiveInteger(value: unknown, name: string): number {
|
|
|
401
539
|
return value;
|
|
402
540
|
}
|
|
403
541
|
|
|
542
|
+
function requireHttpUrl(value: string, name: string): void {
|
|
543
|
+
let parsed: URL;
|
|
544
|
+
try {
|
|
545
|
+
parsed = new URL(value);
|
|
546
|
+
} catch {
|
|
547
|
+
throw new Error(`${name} must be a valid HTTP(S) URL.`);
|
|
548
|
+
}
|
|
549
|
+
if (
|
|
550
|
+
(parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
|
551
|
+
parsed.username !== "" ||
|
|
552
|
+
parsed.password !== ""
|
|
553
|
+
) {
|
|
554
|
+
throw new Error(`${name} must be a valid HTTP(S) URL.`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
404
558
|
function parseBoolean(value: unknown, name: string): boolean {
|
|
405
559
|
if (typeof value === "boolean") {
|
|
406
560
|
return value;
|